Added color randomization logic, small design changes
This commit is contained in:
@@ -91,8 +91,10 @@
|
||||
"kv_edit_rules": "Jeopardy Rules",
|
||||
"kv_edit_how_to": "How to Play?",
|
||||
"kv_settings_colors": "Colors",
|
||||
"kv_settings_primary": "Primary",
|
||||
"kv_settings_secondary": "Secondary",
|
||||
"kv_randomize": "Randomize",
|
||||
"kv_randomize_all_colors": "Randomize All Colors",
|
||||
"kv_settings_primary": "Primary Color",
|
||||
"kv_settings_secondary": "Secondary Color",
|
||||
"kv_settings_text_color": "Text",
|
||||
"kv_settings_background": "Background",
|
||||
"kv_settings_reset": "Reset Settings",
|
||||
|
||||
@@ -91,6 +91,8 @@
|
||||
"kv_edit_rules": "Kuldvillaku reeglid",
|
||||
"kv_edit_how_to": "Kuidas mängida?",
|
||||
"kv_settings_colors": "Värvid",
|
||||
"kv_randomize": "Suvaline",
|
||||
"kv_randomize_all_colors": "Muuda suvaliselt kõiki värve",
|
||||
"kv_settings_primary": "Primaarne",
|
||||
"kv_settings_secondary": "Sekundaarne",
|
||||
"kv_settings_text_color": "Tekst",
|
||||
|
||||
@@ -670,7 +670,6 @@
|
||||
<input
|
||||
type="text"
|
||||
bind:value={hexInput}
|
||||
oninput={updateFromHex}
|
||||
onblur={updateFromHex}
|
||||
onkeydown={(e) => e.key === "Enter" && updateFromHex()}
|
||||
class="w-full bg-kv-black border-2 md:border-4 border-black px-2 md:px-3 py-1.5 md:py-2 text-kv-white font-kv-body text-base md:text-lg uppercase"
|
||||
|
||||
@@ -217,7 +217,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Color Swatches -->
|
||||
<div class="flex flex-col gap-4 w-full">
|
||||
<div class="flex flex-col gap-6 w-full">
|
||||
<!-- Primary Color -->
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span
|
||||
@@ -225,11 +225,21 @@
|
||||
>
|
||||
{m.kv_settings_primary()}
|
||||
</span>
|
||||
<ColorPicker
|
||||
bind:value={themeStore.primary}
|
||||
onchange={(c) => (themeStore.primary = c)}
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<ColorPicker
|
||||
bind:value={themeStore.primary}
|
||||
onchange={(c) => (themeStore.primary = c)}
|
||||
/>
|
||||
<KvButtonSecondary
|
||||
onclick={() =>
|
||||
(themeStore.primary = themeStore.getRandomColor())}
|
||||
class="!px-3 !py-1 !text-sm"
|
||||
>
|
||||
{m.kv_randomize()}
|
||||
</KvButtonSecondary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Secondary Color -->
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span
|
||||
@@ -237,11 +247,22 @@
|
||||
>
|
||||
{m.kv_settings_secondary()}
|
||||
</span>
|
||||
<ColorPicker
|
||||
bind:value={themeStore.secondary}
|
||||
onchange={(c) => (themeStore.secondary = c)}
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<ColorPicker
|
||||
bind:value={themeStore.secondary}
|
||||
onchange={(c) => (themeStore.secondary = c)}
|
||||
/>
|
||||
<KvButtonSecondary
|
||||
onclick={() =>
|
||||
(themeStore.secondary =
|
||||
themeStore.getRandomColor())}
|
||||
class="!px-3 !py-1 !text-sm"
|
||||
>
|
||||
{m.kv_randomize()}
|
||||
</KvButtonSecondary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text Color -->
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span
|
||||
@@ -249,11 +270,21 @@
|
||||
>
|
||||
{m.kv_settings_text_color()}
|
||||
</span>
|
||||
<ColorPicker
|
||||
bind:value={themeStore.text}
|
||||
onchange={(c) => (themeStore.text = c)}
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<ColorPicker
|
||||
bind:value={themeStore.text}
|
||||
onchange={(c) => (themeStore.text = c)}
|
||||
/>
|
||||
<KvButtonSecondary
|
||||
onclick={() =>
|
||||
(themeStore.text = themeStore.getRandomColor())}
|
||||
class="!px-3 !py-1 !text-sm"
|
||||
>
|
||||
{m.kv_randomize()}
|
||||
</KvButtonSecondary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Background Color -->
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span
|
||||
@@ -261,28 +292,51 @@
|
||||
>
|
||||
{m.kv_settings_background()}
|
||||
</span>
|
||||
<ColorPicker
|
||||
bind:value={themeStore.background}
|
||||
onchange={(c) => (themeStore.background = c)}
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<ColorPicker
|
||||
bind:value={themeStore.background}
|
||||
onchange={(c) => (themeStore.background = c)}
|
||||
/>
|
||||
<KvButtonSecondary
|
||||
onclick={() =>
|
||||
(themeStore.background =
|
||||
themeStore.getRandomColor())}
|
||||
class="!px-3 !py-1 !text-sm"
|
||||
>
|
||||
{m.kv_randomize()}
|
||||
</KvButtonSecondary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reset Settings Button -->
|
||||
<KvButtonPrimary
|
||||
onclick={handleResetSettings}
|
||||
class="!text-2xl !py-4 !px-4"
|
||||
>
|
||||
{m.kv_settings_reset()}
|
||||
</KvButtonPrimary>
|
||||
<!-- Randomize All Button -->
|
||||
<div class="flex justify-center w-full mt-2">
|
||||
<KvButtonSecondary
|
||||
onclick={themeStore.randomizeColors}
|
||||
class="!px-4 !py-2"
|
||||
>
|
||||
{m.kv_randomize_all_colors()}
|
||||
</KvButtonSecondary>
|
||||
</div>
|
||||
|
||||
<!-- Save and Exit Button -->
|
||||
<KvButtonSecondary
|
||||
onclick={handleSaveAndExit}
|
||||
class="!text-2xl !py-4 !px-4"
|
||||
>
|
||||
{m.kv_settings_save_exit()}
|
||||
</KvButtonSecondary>
|
||||
<!-- Buttons Container -->
|
||||
<div class="flex flex-row gap-4 w-full">
|
||||
<!-- Reset Settings Button -->
|
||||
<KvButtonPrimary
|
||||
onclick={handleResetSettings}
|
||||
class="!text-2xl !py-4 !px-4 flex-1"
|
||||
>
|
||||
{m.kv_settings_reset()}
|
||||
</KvButtonPrimary>
|
||||
|
||||
<!-- Save and Exit Button -->
|
||||
<KvButtonSecondary
|
||||
onclick={handleSaveAndExit}
|
||||
class="!text-2xl !py-4 !px-4 flex-1"
|
||||
>
|
||||
{m.kv_settings_save_exit()}
|
||||
</KvButtonSecondary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation Dialog -->
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
}: Props = $props();
|
||||
|
||||
const baseClasses =
|
||||
"inline-flex items-center justify-center px-6 py-4 bg-kv-yellow text-black kv-btn-text cursor-pointer transition-opacity hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed kv-shadow-button border-4 border-black box-border focus:outline-none focus:ring-2 focus:ring-kv-blue focus:ring-offset-2 focus:ring-offset-black";
|
||||
"inline-flex items-center justify-center px-6 py-4 bg-kv-yellow text-kv-black kv-btn-text cursor-pointer transition-opacity hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed kv-shadow-button border-4 border-black box-border focus:outline-none focus:ring-2 focus:ring-kv-blue focus:ring-offset-2 focus:ring-offset-black";
|
||||
</script>
|
||||
|
||||
{#if href && !disabled}
|
||||
|
||||
@@ -17,6 +17,78 @@ export const DEFAULT_THEME = {
|
||||
background: "#000000",
|
||||
};
|
||||
|
||||
// --- WCAG & Color Utilities (Updated for HSL generation/checking) ---
|
||||
const ColorUtils = {
|
||||
// Convert Hex to RGB array
|
||||
hexToRgb(hex: string) {
|
||||
const bigint = parseInt(hex.replace('#', ''), 16);
|
||||
return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255];
|
||||
},
|
||||
|
||||
// Calculate Relative Luminance (WCAG 2.0 formula)
|
||||
getLuminance(r: number, g: number, b: number) {
|
||||
const a = [r, g, b].map((v) => {
|
||||
v /= 255;
|
||||
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
|
||||
},
|
||||
|
||||
// Calculate Contrast Ratio
|
||||
getContrast(hex1: string, hex2: string) {
|
||||
const rgb1 = this.hexToRgb(hex1);
|
||||
const rgb2 = this.hexToRgb(hex2);
|
||||
const l1 = this.getLuminance(rgb1[0], rgb1[1], rgb1[2]);
|
||||
const l2 = this.getLuminance(rgb2[0], rgb2[1], rgb2[2]);
|
||||
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
||||
},
|
||||
|
||||
// Convert Hex to HSL for checking hue distance
|
||||
hexToHsl(hex: string): [number, number, number] {
|
||||
let r = 0, g = 0, b = 0;
|
||||
const rgb = this.hexToRgb(hex);
|
||||
r = rgb[0] / 255; g = rgb[1] / 255; b = rgb[2] / 255;
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
let h = 0, s = 0, l = (max + min) / 2;
|
||||
|
||||
if (max !== min) {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
||||
case g: h = (b - r) / d + 2; break;
|
||||
case b: h = (r - g) / d + 4; break;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
return [h * 360, s, l]; // Return H in degrees, S/L as 0-1.
|
||||
},
|
||||
|
||||
// Generate a color with strictly controlled HSL (Luminance and Saturation)
|
||||
generateHslColor(hueRange: [number, number] | null, satRange: [number, number], lumRange: [number, number]): string {
|
||||
const h = hueRange
|
||||
? Math.floor(Math.random() * (hueRange[1] - hueRange[0])) + hueRange[0]
|
||||
: Math.floor(Math.random() * 360);
|
||||
|
||||
const s = Math.floor(Math.random() * (satRange[1] - satRange[0])) + satRange[0];
|
||||
const l = Math.floor(Math.random() * (lumRange[1] - lumRange[0])) + lumRange[0];
|
||||
|
||||
return this.hslToHex(h, s, l);
|
||||
},
|
||||
|
||||
hslToHex(h: number, s: number, l: number) {
|
||||
l /= 100;
|
||||
const a = s * Math.min(l, 1 - l) / 100;
|
||||
const f = (n: number) => {
|
||||
const k = (n + h / 30) % 12;
|
||||
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||
return Math.round(255 * color).toString(16).padStart(2, '0');
|
||||
};
|
||||
return `#${f(0)}${f(8)}${f(4)}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Load initial values from localStorage
|
||||
function getInitialTheme() {
|
||||
if (browser) {
|
||||
@@ -113,7 +185,82 @@ function resetToDefaults() {
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
// Reset and save (for full reset)
|
||||
// Returns a simple random hex (utility for single color needs)
|
||||
function getRandomColor() {
|
||||
// High saturation for a beautiful single random color
|
||||
return ColorUtils.generateHslColor(null, [70, 100], [40, 60]);
|
||||
}
|
||||
|
||||
// Smart Randomization with WCAG and Vibrancy Enforcement
|
||||
function randomizeColors() {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 50;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
attempts++;
|
||||
|
||||
// 1. Decide Polarity
|
||||
const isDarkTheme = Math.random() < 0.5;
|
||||
|
||||
// 2. Generate Background (Low Saturation, Extreme Luminance)
|
||||
const newBg = ColorUtils.generateHslColor(
|
||||
null,
|
||||
[10, 30], // Low Saturation for a neutral BG
|
||||
isDarkTheme ? [4, 10] : [95, 99] // Extreme Dark or Light
|
||||
);
|
||||
|
||||
// 3. Generate Text (Pure White or Near Black)
|
||||
const newText = isDarkTheme ? "#FFFFFF" : "#050505";
|
||||
|
||||
// 4. Generate Secondary (The "Pop" Color - 7:1 against BG)
|
||||
const newSec = ColorUtils.generateHslColor(
|
||||
null,
|
||||
[75, 100], // High Saturation (Vibrant!)
|
||||
isDarkTheme ? [60, 85] : [20, 45] // Opposite L to BG
|
||||
);
|
||||
|
||||
// 5. Generate Primary (The "Brand" Color - Mid-Tone Vibrancy)
|
||||
const newPrim = ColorUtils.generateHslColor(
|
||||
null,
|
||||
[70, 100], // High Saturation
|
||||
isDarkTheme ? [30, 60] : [40, 60] // Mid-Tone Luminance for True Color
|
||||
);
|
||||
|
||||
// --- VALIDATION ---
|
||||
|
||||
// Check Contrast
|
||||
const ratio_Prim_Text = ColorUtils.getContrast(newPrim, newText);
|
||||
const ratio_Sec_Bg = ColorUtils.getContrast(newSec, newBg);
|
||||
const ratio_Prim_Sec = ColorUtils.getContrast(newPrim, newSec);
|
||||
|
||||
// Check Hue Diversity (Ensures Primary and Secondary don't look like the same color)
|
||||
const primHsl = ColorUtils.hexToHsl(newPrim);
|
||||
const secHsl = ColorUtils.hexToHsl(newSec);
|
||||
// We use Math.min to handle the 360-degree color wheel wrap
|
||||
const hueDiff = Math.min(Math.abs(primHsl[0] - secHsl[0]), 360 - Math.abs(primHsl[0] - secHsl[0]));
|
||||
// Require at least 40 degrees difference for visual distinction
|
||||
const distinctHues = hueDiff >= 40;
|
||||
|
||||
if (
|
||||
ratio_Prim_Text >= 4.5 && // WCAG AA for normal text (Relaxed for vibrancy)
|
||||
ratio_Sec_Bg >= 7.0 && // WCAG AAA (Ensures Secondary/CTA pops)
|
||||
ratio_Prim_Sec >= 3.0 && // WCAG for non-text/graphical elements (Ensures distinction)
|
||||
distinctHues // Ensures visual diversity
|
||||
) {
|
||||
primary = newPrim;
|
||||
secondary = newSec;
|
||||
text = newText;
|
||||
background = newBg;
|
||||
applyTheme();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback if 50 attempts fail
|
||||
console.warn("Could not generate vibrant palette with accessibility. Resetting to defaults.");
|
||||
resetToDefaults();
|
||||
}
|
||||
|
||||
function reset() {
|
||||
resetToDefaults();
|
||||
save();
|
||||
@@ -133,4 +280,6 @@ export const themeStore = {
|
||||
revert,
|
||||
reset,
|
||||
resetToDefaults,
|
||||
};
|
||||
getRandomColor,
|
||||
randomizeColors,
|
||||
};
|
||||
@@ -771,7 +771,7 @@
|
||||
{@const bottomRowCount = count > 3 ? count - 3 : 0}
|
||||
{@const topRow = sorted.slice(0, topRowCount)}
|
||||
{@const bottomRow = sorted.slice(topRowCount)}
|
||||
<div class="flex-1 flex flex-col bg-kv-black p-8 gap-4">
|
||||
<div class="flex-1 flex flex-col bg-kv-black gap-4">
|
||||
<!-- Top row -->
|
||||
<div
|
||||
class="flex-1 grid gap-4"
|
||||
|
||||
Reference in New Issue
Block a user