Files
upbeatBytes/frontend/src/lib/games/match/palette.js
T
thejayman77 89c0fbe1f6 Sync repo to deployed state: SEO recovery, Publishing Desk, Play games, emoji picker
The deploy pipeline runs from the working tree, so a wave of shipped features
had never been committed. This snapshots git to what's actually running.

SEO impression recovery (live + verified):
- Duplicate /a/{id} now 301-redirect to their canonical twin instead of 404
  (a hard 404 silently dropped already-indexed URLs and tanked impressions).
- Dedup representative selection reworked: accepted/serveable -> established
  rep (URL stability) -> quality score, so an accepted page never retires to a
  rejected rep and an indexed canonical doesn't churn when a newer twin arrives.
- HEAD /a/{id} returns the same status as GET (api_route GET+HEAD) instead of
  falling through to the static mount and 404ing.
- `dedup --force-recluster`: cycle-locked, model-free re-cluster to re-apply the
  policy to the existing corpus (shared cycle_lock context manager).
- CLI honors GOODNEWS_DB for its default --db (was silently ignored).

Publishing Desk (admin tool to post highlights to X via Web Intents):
- publishing.py queue/rank/handle-resolution; admin UI; full searchable emoji
  picker (bundled data, no CDN) for the blurb editor.

Play games + site:
- Bloom (word-wheel), Memory Match, daily ritual set, Zen Den (dev-gated).
- English-only language gate; source prospecting; paywall + dedup hardening.

Tests: full suite green (349). Ignores tightened (node_modules, data/*.db).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 11:32:27 -04:00

51 lines
3.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Color Match palette — a hand-curated set of calm-but-distinct colors. The board
// builder picks a perceptually-spaced subset (see board.js) so two near-identical
// shades never land on the same board. Each color is NAMED (drives the aria-label
// and the optional colorblind "assist" glyph), and varies in LIGHTNESS as well as
// hue so confusable hue pairs (e.g. green/teal) still differ by brightness.
//
// `assist` is a short letter shown in the corner when the player turns assist on —
// kept to 12 chars, unique enough to disambiguate the swatch without reading as
// a full label. Default play is pure color; assist is opt-in.
const RAW = [
{ key: 'color-rose', name: 'Rose', hex: '#d76a86', assist: 'Ro' },
{ key: 'color-coral', name: 'Coral', hex: '#e07a52', assist: 'Co' },
{ key: 'color-amber', name: 'Amber', hex: '#e3a32f', assist: 'Am' },
{ key: 'color-gold', name: 'Gold', hex: '#cdb63c', assist: 'Go' },
{ key: 'color-lime', name: 'Lime', hex: '#8fb24a', assist: 'Li' },
{ key: 'color-green', name: 'Green', hex: '#4a9b5c', assist: 'Gr' },
{ key: 'color-teal', name: 'Teal', hex: '#2f9c8e', assist: 'Te' },
{ key: 'color-cyan', name: 'Cyan', hex: '#3aa6c4', assist: 'Cy' },
{ key: 'color-sky', name: 'Sky', hex: '#5b8fd4', assist: 'Sk' },
{ key: 'color-blue', name: 'Blue', hex: '#4f63c6', assist: 'Bl' },
{ key: 'color-indigo', name: 'Indigo', hex: '#6a59b2', assist: 'In' },
{ key: 'color-violet', name: 'Violet', hex: '#9460ba', assist: 'Vi' },
{ key: 'color-plum', name: 'Plum', hex: '#b25a95', assist: 'Pl' },
{ key: 'color-brown', name: 'Brown', hex: '#9c6b45', assist: 'Br' },
{ key: 'color-sand', name: 'Sand', hex: '#cdb38c', assist: 'Sa' },
{ key: 'color-slate', name: 'Slate', hex: '#5d6b78', assist: 'Sl' },
{ key: 'color-charcoal', name: 'Charcoal', hex: '#3a4250', assist: 'Ch' },
{ key: 'color-cream', name: 'Cream', hex: '#e6dfca', assist: 'Cm' },
];
function srgbToLin(c) { c /= 255; return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }
function hexToLab(hex) {
const r = srgbToLin(parseInt(hex.slice(1, 3), 16));
const g = srgbToLin(parseInt(hex.slice(3, 5), 16));
const b = srgbToLin(parseInt(hex.slice(5, 7), 16));
const X = r * 0.4124 + g * 0.3576 + b * 0.1805;
const Y = r * 0.2126 + g * 0.7152 + b * 0.0722;
const Z = r * 0.0193 + g * 0.1192 + b * 0.9505;
const f = (t) => (t > 0.008856 ? Math.cbrt(t) : 7.787 * t + 16 / 116);
const fx = f(X / 0.95047), fy = f(Y / 1), fz = f(Z / 1.08883);
return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
}
// CIE76 ΔE — adequate for "are these two swatches obviously different".
export function deltaE(a, b) { return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]); }
export const COLORS = RAW.map((c) => ({ ...c, lab: hexToLab(c.hex) }));
export const COLOR_BY_KEY = Object.fromEntries(COLORS.map((c) => [c.key, c]));
export const COLOR_KEYS = COLORS.map((c) => c.key);