Skip to content

Commit

Permalink
feat(color): improve contrast calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
iamludal committed Jul 16, 2022
1 parent 5aea0b8 commit 854ac6d
Show file tree
Hide file tree
Showing 18 changed files with 276 additions and 212 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@rollup/plugin-node-resolve": "^8.0.0",
"@rollup/plugin-typescript": "^6.0.0",
"@tsconfig/svelte": "^1.0.0",
"@types/color": "^3.0.3",
"@types/howler": "^2.2.4",
"rollup": "^2.3.4",
"rollup-plugin-livereload": "^2.0.0",
Expand All @@ -27,6 +28,7 @@
"typescript": "^3.9.3"
},
"dependencies": {
"color": "^4.2.3",
"howler": "^2.2.3",
"robot3": "^0.3.1",
"svelte-i18n": "^3.4.0",
Expand Down
14 changes: 9 additions & 5 deletions src/components/App.svelte
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
<script lang="ts">
import Loader from './Loader.svelte';
import { fetchWordsList, loadSettings } from '../ts/utils';
import Game from './Game.svelte';
import { settings } from '../ts/store';
import settings from '../store/settings';
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import Color from 'color';
import { fetchWordsList } from '../services/words';
import { loadSettings } from '../services/settings';
let wordsPromise = fetchWordsList();
let lang = loadSettings().language;
onMount(() => {
settings.subscribe(newSettings => {
document.documentElement.style.setProperty('--color-primary', newSettings.colorTheme);
const { style } = document.documentElement;
const white = Color('white');
style.setProperty('--color-primary', newSettings.colorTheme.hex());
style.setProperty('--color-secondary', white.hex());
style.setProperty('--color-secondary-transparent', white.alpha(0.2).string());
if (newSettings.language !== lang) {
lang = newSettings.language;
wordsPromise = fetchWordsList();
Expand All @@ -30,8 +36,6 @@

<style>
:global(:root) {
--color-secondary: #fff;
--color-secondary-transparent: rgba(255, 255, 255, 0.15);
--font-family: 'Montserrat';
--transition: 0.3s;
--radius-high: 500em;
Expand Down
10 changes: 6 additions & 4 deletions src/components/Game.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import StartMenu from './StartMenu.svelte';
import EndScreen from './EndScreen.svelte';
import Game from '../ts/game';
import type { GameService, States, words } from '../ts/types';
import { getScores, loadSettings, saveScore } from '../ts/utils';
import type { GameService, States } from '../ts/types';
import { onMount } from 'svelte';
import { settings } from '../ts/store';
import settings from '../store/settings';
import { loadSettings } from '../services/settings';
import { getScores, saveScore } from '../services/score';
import type { Words } from '../services/words';
export let wordsList: words;
export let wordsList: Words;
let state: States = 'IDLE';
let game: Game;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Playground.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script lang="ts">
import { onMount } from 'svelte/internal';
import type Game from '../ts/game';
import type { WordProps } from '../ts/types';
import Word from './Word.svelte';
import { fade } from 'svelte/transition';
import MdStars from 'svelte-icons/md/MdStars.svelte';
import Icon from './shared/Icon.svelte';
import { _ } from 'svelte-i18n';
import type { WordProps } from '../services/words';
export let game: Game;
Expand Down
84 changes: 47 additions & 37 deletions src/components/Settings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,64 @@
import { fade } from 'svelte/transition';
import IoMdClose from 'svelte-icons/io/IoMdClose.svelte';
import IconButton from './shared/IconButton.svelte';
import { colors, COLOR_REGEX, LANGS } from '../ts/constants';
import { Settings, settings } from '../ts/store';
import { isTooBright, loadSettings } from '../ts/utils';
import { defaultColors, LANGS } from '../ts/constants';
import settings, { Settings } from '../store/settings';
import Icon from './shared/Icon.svelte';
import IoMdCheckmark from 'svelte-icons/io/IoMdCheckmark.svelte';
import MdDoNotDisturb from 'svelte-icons/md/MdDoNotDisturb.svelte';
import { locale, _ } from 'svelte-i18n';
import { _ } from 'svelte-i18n';
import Color from 'color';
import { loadSettings, setColorTheme, setLanguage, setSoundsEnabled } from '../services/settings';
let _settings: Settings = loadSettings();
$: errorMessageKey = getErrorMessageKey(_settings.colorTheme);
let localSettings: Settings = loadSettings();
let colorInput = localSettings.colorTheme.hex();
$: colorErrorMessageKey = getColorErrorMessageKey(colorInput);
const dispatch = createEventDispatcher();
const close = () => dispatch('close');
const toggleSounds = () => {
settings.update(oldSettings => ({ ...oldSettings, soundsEnabled: _settings.soundsEnabled }));
};
const setColor = (selectedColor: string) => {
settings.update(oldSettings => ({ ...oldSettings, colorTheme: selectedColor }));
};
const handleKeyUp = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
close();
}
};
const getErrorMessageKey = (color: string) => {
if (!COLOR_REGEX.test(color)) {
const getColorErrorMessageKey = (colorStr: string) => {
let color: Color;
try {
color = Color(colorStr);
} catch {
return 'color-invalid';
} else if (isTooBright(color)) {
}
if (color.contrast(Color('#fff')) < 1.5) {
return 'color-too-bright';
}
return null;
};
const handleInputChange = () => {
if (errorMessageKey === null) {
setColor(_settings.colorTheme);
}
const handleColorInputChange = () => {
if (colorErrorMessageKey !== null) return;
updateColorTheme(localSettings.colorTheme);
};
// handle select change event
const handleLanguageChange = () => {
settings.update(oldSettings => ({ ...oldSettings, language: _settings.language }));
locale.set(_settings.language);
const updateColorTheme = (color: Color) => {
setColorTheme(color);
console.log(color.hex());
colorInput = color.hex();
};
const handleLanguageChange = () => setLanguage(localSettings.language);
const handleSoundsChange = () => setSoundsEnabled(localSettings.soundsEnabled);
onMount(() => {
settings.subscribe(newSettings => (_settings = newSettings));
settings.subscribe(newSettings => (localSettings = newSettings));
});
</script>

Expand All @@ -73,7 +79,7 @@
<span class="section__title">{$_('settings.language')}</span>
<select
class="section__content"
bind:value={_settings.language}
bind:value={localSettings.language}
on:change={handleLanguageChange}
>
{#each Object.entries(LANGS) as [key, value]}
Expand All @@ -86,7 +92,11 @@
<div class="section">
<span class="section__title">{$_('settings.sound-effects')}</span>
<label class="switch section__content">
<input type="checkbox" bind:checked={_settings.soundsEnabled} on:change={toggleSounds} />
<input
type="checkbox"
bind:checked={localSettings.soundsEnabled}
on:change={handleSoundsChange}
/>
<span class="switch__slider" />
</label>
</div>
Expand All @@ -100,24 +110,24 @@
type="text"
class="color-input"
placeholder="#353535"
class:color-input--invalid={errorMessageKey !== null}
bind:value={_settings.colorTheme}
on:input={handleInputChange}
class:color-input--invalid={colorErrorMessageKey !== null}
bind:value={colorInput}
on:input={handleColorInputChange}
/>
{#if errorMessageKey !== null}
{#if colorErrorMessageKey !== null}
<div class="input-error">
<Icon icon={MdDoNotDisturb} />
{$_(`settings.theme.errors.${errorMessageKey}`)}
{$_(`settings.theme.errors.${colorErrorMessageKey}`)}
</div>
{/if}
</div>
{#each colors as color}
{#each defaultColors as color}
<div
class={`color ${_settings.colorTheme === color ? 'color-active' : ''}`}
class={`color ${localSettings.colorTheme === color ? 'color-active' : ''}`}
style={`--color: ${color}`}
on:click={() => setColor(color)}
on:click={() => updateColorTheme(color)}
>
{#if _settings.colorTheme === color}
{#if localSettings.colorTheme === color}
<Icon icon={IoMdCheckmark} />
{/if}
</div>
Expand Down
11 changes: 5 additions & 6 deletions src/components/StartMenu.svelte
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { getScores } from '../ts/utils';
import { createEventDispatcher, onMount } from 'svelte';
import PlayCircle from 'svelte-icons/md/MdPlayArrow.svelte';
import FaMedal from 'svelte-icons/fa/FaMedal.svelte';
import Rules from './Rules.svelte';
import Icon from './shared/Icon.svelte';
import type { Scores } from '../ts/types';
import IoMdHelpCircle from 'svelte-icons/io/IoMdHelpCircle.svelte';
import IoMdSettings from 'svelte-icons/io/IoMdSettings.svelte';
import FaGithubAlt from 'svelte-icons/fa/FaGithubAlt.svelte';
import Settings from './Settings.svelte';
import { settings } from '../ts/store';
import settings from '../store/settings';
import { _ } from 'svelte-i18n';
import { getScores, Scores } from '../services/score';
const GITHUB_LINK = 'https://github.com/iamludal/typospeed';
Expand All @@ -27,12 +26,12 @@
const hideRules = () => {
rulesOpened = false;
settings.update(oldSettings => ({ ...oldSettings, isNew: false }));
settings.update(oldSettings => ({ ...oldSettings, showRules: false }));
};
onMount(() => {
settings.subscribe(({ isNew }) => {
if (isNew) rulesOpened = true;
settings.subscribe(({ showRules }) => {
if (showRules) rulesOpened = true;
});
});
</script>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Word.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import type { WordProps } from '../ts/types';
import { fade } from 'svelte/transition';
import type { WordProps } from '../services/words';
export let props: WordProps;
Expand Down
30 changes: 30 additions & 0 deletions src/services/score.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export interface Scores {
best: number;
last: number;
}

/**
* Get the last score and best score from previous games. If no scores were
* found, set their values to 0
*
* @returns the scores
*/
export function getScores(): Scores {
const last = parseInt(localStorage.getItem('last')) || 0;
const best = parseInt(localStorage.getItem('best')) || last;

return { best, last };
}

/**
* Save the score into the local storage
*
* @param score the score
*/
export function saveScore(score: number): void {
const localBest = parseInt(localStorage.getItem('best')) || 0;
const best = Math.max(score, localBest);

localStorage.setItem('best', best.toString());
localStorage.setItem('last', score.toString());
}
80 changes: 80 additions & 0 deletions src/services/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import Color from 'color';
import { getLocaleFromNavigator, locale } from 'svelte-i18n';
import settings, { Settings } from '../store/settings';
import { defaultColors, Lang, LANGS } from '../ts/constants';

/**
* Get the correct language
* @param lang the initial language, that may be valid or not
* @returns the correct language
*/
const getLanguage = (lang?: string): Lang => {
if (!lang) {
lang = getLocaleFromNavigator();
}

if (lang in LANGS) {
return lang as Lang;
}

return 'en';
};

/**
* Load settings from local storage
* @returns the settings
*/
export const loadSettings = (): Settings => {
const settingsRaw = localStorage.getItem('settings');

let settings: any;

try {
settings = JSON.parse(settingsRaw);
} catch {
settings = {};
}

let colorTheme: Color;

if (settings?.colorTheme) {
try {
colorTheme = Color(settings?.colorTheme);
} catch {
colorTheme = Color(defaultColors[0]);
}
} else {
colorTheme = defaultColors[0];
}

return {
colorTheme,
showRules: Boolean(settings?.showRules ?? true),
language: getLanguage(settings?.language),
soundsEnabled: Boolean(settings?.soundsEnabled ?? true),
};
};

/**
* Save settings into local storage
* @param settings the settings to save
*/
export const saveSettings = (settings: Settings): void => {
const jsonSettings = { ...settings, colorTheme: settings.colorTheme.hex() };
localStorage.setItem('settings', JSON.stringify(jsonSettings));
};

// Setters

export const setLanguage = (language: Lang) => {
settings.update(s => ({ ...s, language }));
locale.set(language);
};

export const setColorTheme = (colorTheme: Color) => {
settings.update(s => ({ ...s, colorTheme }));
};

export const setSoundsEnabled = (soundsEnabled: boolean) => {
settings.update(s => ({ ...s, soundsEnabled }));
};
Loading

0 comments on commit 854ac6d

Please sign in to comment.