- TypeScript 55.6%
- Svelte 29.9%
- JavaScript 11.1%
- CSS 2.4%
- HTML 0.6%
- Other 0.4%
Training model change: - Words now come from a built-in corpus (app/src/color/corpus.ts) instead of being typed by the user. - The user picks each word's target color themselves in a setup flow (ui/Setup.svelte); the target is NEVER shown during training — you reproduce it from memory. Removes the anchor-seed, learn phase, and target/guess reveals. - Progress cloud shows only the user's own reproductions clustering (no target marker), so nothing leaks the color. Reminders (Mastodon-style, same origin): - Push API served under color.collar.sh/push (Traefik PathPrefix + StripPrefix), no extra subdomain. server/Dockerfile added (Node 24 runs the TS entrypoint). - Client push URL builder fixed to respect a base that includes a path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| app | ||
| server | ||
| .gitignore | ||
| README.md | ||
Huemory
Train stable, personal associations between words and exact colors, then drive down the variance of your reproductions over daily sessions. It's a calibration trainer, not a quiz: each day you reproduce the color you associate with a word from memory, the app measures how far off you are (perceptually), nudges you back, and tracks whether your picks are converging.
No numbers, ever — you only ever navigate and see colors.
The idea
Two constraints shape everything:
- Perceptual, but never clipping. OKLab/OKLCh is perceptually uniform, but its
reachable region (the display gamut) is a lumpy shape — navigating it hits
clipping "walls" where a color can't actually be shown. So instead of navigating
raw perceptual space, Huemory precomputes a rigid 3-D box (a parallelepiped
with a fixed, linearly-independent basis) inscribed entirely inside the display
gamut, oriented along the gamut's own principal axes (PCA). Every point in the
box maps to a real, un-clipped color, and the axes are independent — so moving
the point is a straight, uniform perceptual move. See
app/src/color/solid.ts. - You choose the color; it's never shown again. The app supplies the words
(from a built-in corpus —
app/src/color/corpus.ts); for each, you pick the color you'll associate with it. During training that target is never displayed — you reproduce it purely from memory, and the app tracks the spread of your reproductions (the std-dev we minimize) as a tightening cluster. Seeapp/src/session/target.ts.
When you miss, the nudge is a directional hint — arrows showing which way to move in the box — never the color itself, never a number.
Layout
huemory/
app/ # the trainer — Vite + Svelte 5 PWA (installable, offline)
src/color/ inscribed color solid, word corpus, OKLab math (culori) + tests
src/session/ reproductions + spread, the daily state machine
src/picker/ canvas color-plane + lightness bar, directional nudge overlay
src/store/ IndexedDB (idb): words (+ chosen colors), picks, settings
src/pwa/ service worker (push) + client subscribe
src/ui/ screens (setup, session, words, settings)
server/ # the only networked piece: Web Push sender for daily reminders
Develop
cd app
npm install
npm run dev # http://localhost:5173
npm test # color-solid + session unit tests
npm run build # type-check + production PWA build
The color-solid tests prove the core promise: 20 000 random points in the box all convert to in-gamut sRGB (no clipping), and the basis is independent + orthogonal.
Daily reminders
Reminders use Web Push via the small sender in server/. Like Mastodon, it runs
on the same origin — the push API is served under color.collar.sh/push
(Traefik strips the prefix), so there's no extra subdomain. The app is pointed at
it via app/.env (VITE_PUSH_SERVER_URL=https://color.collar.sh/push +
VITE_VAPID_PUBLIC_KEY). See server/README.md for the
server env (VAPID keypair, APP_URL, data path).
On mobile, "Add to Home Screen" first — installed PWAs are what can receive push.