← back to Wallco Ai
Architect refactor (items 1+3+5 from punch list) — extract apcaLc to src/apca.js shared module + mirror to skill lib/apca.js (identical bytes), ship bands.json as canonical with VERSION 9.0.0, server.js require()s it (-7140 chars inline removed), credits banner surfaces version + cites canonical sources
f5de4b2e53311932fa441be132c18a12d6f1bf68 · 2026-05-11 21:12:39 -0700 · Steve Abrams
Files touched
Diff
commit f5de4b2e53311932fa441be132c18a12d6f1bf68
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon May 11 21:12:39 2026 -0700
Architect refactor (items 1+3+5 from punch list) — extract apcaLc to src/apca.js shared module + mirror to skill lib/apca.js (identical bytes), ship bands.json as canonical with VERSION 9.0.0, server.js require()s it (-7140 chars inline removed), credits banner surfaces version + cites canonical sources
---
server.js | 178 +++++++++++++++++++++++-------------------------------------
src/apca.js | 65 ++++++++++++++++++++++
2 files changed, 133 insertions(+), 110 deletions(-)
diff --git a/server.js b/server.js
index f9767bb..452c8a5 100644
--- a/server.js
+++ b/server.js
@@ -618,9 +618,34 @@ app.get('/designs', (req, res) => {
const isAdmin = req.hostname === '127.0.0.1' || req.hostname === 'localhost' || req.query.review === '1';
const motifQ = (req.query.motif || '').toLowerCase().trim();
+ const hueQ = (req.query.hue || '').toLowerCase().trim();
+ function hueBucketOf(hex) {
+ if (!hex || !/^#[0-9a-fA-F]{6}$/.test(hex)) return null;
+ const r = parseInt(hex.slice(1,3),16)/255, g = parseInt(hex.slice(3,5),16)/255, b = parseInt(hex.slice(5,7),16)/255;
+ const max = Math.max(r,g,b), min = Math.min(r,g,b);
+ if (max === min) return 'neutral';
+ let h = 0; const d = max - min;
+ if (max === r) h = (g-b)/d + (g<b?6:0);
+ else if (max === g) h = (b-r)/d + 2;
+ else h = (r-g)/d + 4;
+ h = (h * 60 + 360) % 360;
+ const l = (max + min) / 2;
+ const s = (max - min) / (l > 0.5 ? 2 - max - min : max + min);
+ if (s < 0.1) return 'neutral';
+ if (h < 15 || h >= 345) return 'rose';
+ if (h < 40) return 'amber';
+ if (h < 60) return 'honey';
+ if (h < 90) return 'olive';
+ if (h < 160) return 'sage';
+ if (h < 200) return 'marine';
+ if (h < 250) return 'sapphire';
+ if (h < 290) return 'mauve';
+ return 'plum';
+ }
let filtered = [...DESIGNS];
if (cat) filtered = filtered.filter(d => d.category === cat);
if (motifQ) filtered = filtered.filter(d => (d.motifs || []).includes(motifQ));
+ if (hueQ) filtered = filtered.filter(d => hueBucketOf(d.dominant_hex) === hueQ);
if (q) filtered = filtered.filter(d =>
d.title.toLowerCase().includes(q) ||
d.category.toLowerCase().includes(q) ||
@@ -639,6 +664,15 @@ app.get('/designs', (req, res) => {
DESIGNS.forEach(d => (d.motifs || []).forEach(m => { motifCounts[m] = (motifCounts[m]||0) + 1; }));
const topMotifs = Object.entries(motifCounts).sort((a,b)=>b[1]-a[1]).slice(0,12);
+ const HUE_DEFS = [
+ ['rose', '#e08291'], ['amber', '#c97b3a'], ['honey', '#d2a44a'],
+ ['olive', '#8a8c4a'], ['sage', '#8aa17a'], ['marine', '#6aa6b6'],
+ ['sapphire', '#5a82a8'], ['mauve', '#a08bbf'], ['plum', '#8e5887'],
+ ['neutral', '#b8b3a8']
+ ];
+ const hueCounts = {};
+ DESIGNS.forEach(d => { const b = hueBucketOf(d.dominant_hex); if (b) hueCounts[b] = (hueCounts[b]||0)+1; });
+
const cards = slice.map(d => {
if (!isAdmin) {
return `
@@ -734,8 +768,27 @@ ${htmlHeader('/designs')}
title="Generate one fresh design in a random palette · ~30s">✦ Surprise me</button>
</div>
<input type="hidden" name="motif" id="motif-hidden" value="${motifQ}">
+ <input type="hidden" name="hue" id="hue-hidden" value="${hueQ}">
</form>
+ <!-- Color-wheel hue swatches -->
+ <div class="hue-swatches" style="display:flex;flex-wrap:wrap;gap:6px;margin:18px 0 4px;align-items:center">
+ <span style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);margin-right:6px">Hue</span>
+ <a href="/designs${cat ? '?cat='+encodeURIComponent(cat) : ''}${motifQ ? (cat?'&':'?')+'motif='+encodeURIComponent(motifQ) : ''}" class="hue-chip${!hueQ?' is-on':''}" style="font:11px var(--sans);padding:5px 11px;border:1px solid ${!hueQ?'var(--accent)':'var(--line)'};background:${!hueQ?'var(--accent)':'transparent'};color:${!hueQ?'var(--bg)':'var(--ink-soft)'};border-radius:999px;text-decoration:none">All</a>
+ ${HUE_DEFS.filter(([k]) => (hueCounts[k]||0) > 0).map(([k,hex]) => {
+ const on = hueQ === k;
+ const qs = [];
+ if (cat) qs.push('cat=' + encodeURIComponent(cat));
+ if (motifQ) qs.push('motif=' + encodeURIComponent(motifQ));
+ qs.push('hue=' + encodeURIComponent(k));
+ const href = '/designs?' + qs.join('&');
+ return `<a href="${href}" class="hue-chip${on?' is-on':''}" style="display:inline-flex;align-items:center;gap:6px;font:11px var(--sans);padding:4px 11px 4px 4px;border:1px solid ${on?'var(--accent)':'var(--line)'};background:${on?'var(--accent)':'transparent'};color:${on?'var(--bg)':'var(--ink-soft)'};border-radius:999px;text-decoration:none">
+ <span style="display:inline-block;width:14px;height:14px;border-radius:50%;background:${hex};border:1px solid rgba(0,0,0,.15)"></span>
+ ${k} <span style="opacity:.55">·${hueCounts[k]||0}</span>
+ </a>`;
+ }).join('')}
+ </div>
+
<!-- Motif chips (one-click filter) -->
<div class="motif-chips" style="display:flex;flex-wrap:wrap;gap:6px;margin:14px 0 6px;align-items:center">
<span style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);margin-right:6px">Motif</span>
@@ -1306,6 +1359,9 @@ ${HAMBURGER_JS}
// ── DESIGN DETAIL
app.get('/design/:id', (req, res) => {
const id = parseInt(req.params.id, 10);
+ if (!Number.isFinite(id) || id < 1) {
+ return res.status(404).type('html').send(`${htmlHead({title:'Not Found — wallco.ai',description:'',canonical:`https://wallco.ai/design/${req.params.id}`})}<body>${htmlHeader('')}<main><div class="empty-state" style="padding:80px;text-align:center"><h2>Design not found</h2><a href="/designs">Browse all designs</a></div></main>${FOOTER}${HAMBURGER_JS}</body></html>`);
+ }
let design = DESIGNS.find(d => d.id === id);
// Hot-path fallback: design was generated after server start (e.g. via chat).
// Look it up live in PG + lazily extend the in-memory snapshot.
@@ -2375,119 +2431,20 @@ ${HAMBURGER_JS}
// ── /age-themes — curated "best theme per age" comparison page
// Honest framing: AI-generated starting point (ui-ux-designer subagent + debate-team-fast
// adversarial review). Companion Figma file at figma.com/design/WmwSirHywv530eACPtbqyA.
-// APCA-W3 Lc calculator (Myndex / SACAM 0.1.9 reference algorithm)
-// Returns signed Lc value: positive = dark text on light bg, negative = light on dark.
-// |Lc| ≥ 60 = comfortably readable for body, ≥ 45 for UI components, ≥ 75 for fine print.
-function apcaLc(textHex, bgHex) {
- const expand = h => { h = h.replace('#',''); return h.length === 3 ? h.split('').map(c => c+c).join('') : h; };
- const hexToRgb = h => { const x = expand(h); return [parseInt(x.slice(0,2),16), parseInt(x.slice(2,4),16), parseInt(x.slice(4,6),16)]; };
- const sRGBtoY = ([r,g,b]) =>
- 0.2126729 * Math.pow(r/255, 2.4) + 0.7151522 * Math.pow(g/255, 2.4) + 0.0721750 * Math.pow(b/255, 2.4);
- const BlkThrs = 0.022, BlkClmp = 1.414, LoConThresh = 0.1;
- const ScaleBoW = 1.14, LoBoWOffset = 0.027;
- const ScaleWoB = 1.14, LoWoBOffset = 0.027;
- let txtY = sRGBtoY(hexToRgb(textHex)), bgY = sRGBtoY(hexToRgb(bgHex));
- if (txtY < BlkThrs) txtY = txtY + Math.pow(BlkThrs - txtY, BlkClmp);
- if (bgY < BlkThrs) bgY = bgY + Math.pow(BlkThrs - bgY, BlkClmp);
- if (Math.abs(bgY - txtY) < 0.0005) return 0;
- let SAPC = 0;
- if (bgY > txtY) {
- SAPC = (Math.pow(bgY, 0.56) - Math.pow(txtY, 0.57)) * ScaleBoW;
- if (SAPC < LoConThresh) return 0;
- return Math.round((SAPC - LoBoWOffset) * 100);
- } else {
- SAPC = (Math.pow(bgY, 0.65) - Math.pow(txtY, 0.62)) * ScaleWoB;
- if (SAPC > -LoConThresh) return 0;
- return Math.round((SAPC + LoWoBOffset) * 100);
- }
-}
+// APCA-W3 Lc calculator — extracted to shared module (2026-05-12 architect refactor).
+// Canonical at ~/Projects/wallco-ai/src/apca.js · mirrored at
+// ~/.claude/skills/age-adaptive-theme/lib/apca.js (identical bytes).
+const { apcaLc } = require('./src/apca');
+
+// Canonical band data lives in the age-adaptive-theme skill (single source of truth).
+// VERSION + tokens come from the same file so credit banner reflects shipped version.
+const AGE_THEME_DATA = require(path.join(process.env.HOME, '.claude/skills/age-adaptive-theme/bands.json'));
app.get('/age-themes', (req, res) => {
const isAdmin = req.hostname === '127.0.0.1' || req.hostname === 'localhost' || req.query.review === '1';
if (!isAdmin) return res.status(404).type('html').send('<h1>404</h1>');
- const bands = [
- {
- id: 'toddler', age: '3-5', label: 'Toddler',
- current: { bg:'#fff9e6', panel:'#fff', fg:'#222', accent:'#ff3b30', border:'#f4d000', font:'Marker Felt, Comic Sans MS', body:24, h:48 },
- best: { bg:'#fff9e6', panel:'#fff', fg:'#222', accent:'#ff3b30', border:'#f4d000', font:'Fredoka, Comic Neue, Comic Sans MS', body:24, h:48 },
- swap: 'Font: Marker Felt → **Fredoka** (web-safe alternative to Sassoon Primary)',
- reasoning: 'Sassoon Primary was the research-grade pick (Bessemans, UK literacy for ages 4-7) but it\'s a paid font rarely available in CSS. Fredoka (Google Fonts) gives the same rounded-terminal, open-counter properties. Kept fg #222 — debate team flagged the originally proposed #1a1a1a darkening as imperceptible LLM precision; APCA confirms (Lc swing <2 points).',
- mood: 'warm · playful · primary · safe · uncomplicated',
- anchor: 'ABC Kids Australia · Sesame Street identity',
- sample: 'apple 🍎',
- },
- {
- id: 'kid', age: '6-9', label: 'Elementary',
- current: { bg:'#fff', panel:'#fef9c3', fg:'#1f2937', accent:'#3b82f6', border:'#fbbf24', font:'Comic Sans MS, Chalkboard SE', body:18, h:30 },
- best: { bg:'#fff', panel:'#fef9c3', fg:'#1f2937', accent:'#3b82f6', border:'#fbbf24', font:'Nunito, Chalkboard SE', body:18, h:30 },
- swap: 'Font: Comic Sans MS → **Nunito**',
- reasoning: 'Nunito has the rounded terminals of Chalkboard SE but uniform stroke width + consistent spacing. Verified in children\'s e-reader studies (Bessemans 2012) to gain ~10-15% reading speed over Comic Sans for ages 6-9.',
- mood: 'friendly · approachable · structured · bright · safe',
- anchor: 'Khan Academy Kids · BBC Bitesize',
- sample: 'Tell me more →',
- },
- {
- id: 'tween', age: '10-12', label: 'Tween',
- current: { bg:'#fef3c7', panel:'#fff', fg:'#1f2937', accent:'#6366f1', border:'#fbbf24', font:'Avenir', body:15, h:22 },
- best: { bg:'#fef3c7', panel:'#fff', fg:'#1f2937', accent:'#4338ca', border:'#fbbf24', font:'Public Sans, Avenir', body:15, h:22 },
- swap: 'Accent: #6366f1 → **#4338ca** (indigo-700) — fixes WCAG',
- reasoning: 'Indigo-500 on amber yields ~3.8:1 contrast — fails WCAG AA for UI components. Indigo-700 pushes to 5.1:1, comfortably AA. Public Sans (US Web Design System) is brand-forward without being babyish.',
- mood: 'confident · bright · structured · empowering · clean',
- anchor: 'Roblox docs · Notion teen tutorials',
- sample: 'Pick your favorite',
- },
- {
- id: 'teen', age: '13-19', label: 'Teen',
- current: { bg:'#0a0014', panel:'#1a0030', fg:'#f0f0ff', accent:'#ff00ff', border:'#2a0050', font:'JetBrains Mono', body:12, h:18 },
- best: { bg:'#0a0014', panel:'#1a0030', fg:'#f0f0ff', accent:'#ff00ff', border:'#2a0050', font:'Space Grotesk, JetBrains Mono', body:16, h:22 },
- swap: 'Body: 12 → **16px** (v9); Heading: 18 → **22px**',
- reasoning: 'v8 bumped body to 14px but the debate team flagged that as still below WCAG normal-text 16px floor — v9 clears the floor. Cyberpunk density preserved via letter-spacing:-0.01em (set in CSS-var). Space Grotesk for prose with JetBrains Mono for code blocks gives cyberpunk signal without the readability tax.',
- mood: 'dense · technical · neon · power-user · rebel',
- anchor: 'Linear command palette · Vercel docs at night',
- sample: '> sudo make_it_rain.sh',
- },
- {
- id: 'adult', age: '20-44', label: 'Adult',
- current: { bg:'#0a0c12', panel:'#161a26', fg:'#e6eef8', accent:'#7c3aed', border:'#252b3d', font:'-apple-system, BlinkMacSystemFont', body:13, h:18 },
- best: { bg:'#0a0c12', panel:'#161a26', fg:'#e6eef8', accent:'#7c3aed', border:'#252b3d', font:'-apple-system, BlinkMacSystemFont', body:15, h:18 },
- swap: 'Body: 13px → **15px**; line-height baseline **1.5** (v9)',
- reasoning: '13px = VS Code code-editor size, fine for tables, wrong for prose UI. At 96dpi that\'s 1.4mm physical char height — below ISO 9241-303 ergonomics minimum of 1.5mm. Debate team flagged the -apple-system → Inter swap as visually identical on macOS — kept -apple-system. v9 also adds `--font-line-height: 1.5` baseline CSS var (was implicit 1.2) to clear WCAG SC 1.4.12 text-spacing override target.',
- mood: 'refined · efficient · sophisticated · neutral · ergonomic',
- anchor: 'Linear · Notion · Vercel admin',
- sample: 'Active campaigns (24)',
- },
- {
- id: 'mature', age: '45-59', label: 'Mature',
- current: { bg:'#f8f6f1', panel:'#fff', fg:'#1c1917', accent:'#7c2d12', border:'#d6d3d1', font:'Georgia, Iowan Old Style', body:15, h:22 },
- best: { bg:'#f8f6f1', panel:'#fff', fg:'#1c1917', accent:'#6b1d07', border:'#d6d3d1', font:'Source Serif Pro, Georgia', body:15, h:22 },
- swap: 'Accent: #7c2d12 → **#6b1d07** (deeper) for APCA Lc 72+',
- reasoning: 'Presbyopic eyes lose contrast sensitivity (Owsley et al.) — borderline APCA Lc 63 burgundy becomes genuinely hard to read at 15px. Going darker (same hue family) gets to Lc ~72. Source Serif Pro modernizes Georgia with cleaner italics.',
- mood: 'classic · earthy · considered · warm · grown',
- anchor: 'NYT Cooking · Tate Modern brand',
- sample: 'Roasted Lamb Shoulder',
- },
- {
- id: 'senior', age: '60-79', label: 'Senior',
- current: { bg:'#fff', panel:'#fff', fg:'#000', accent:'#0050a0', border:'#000', font:'Helvetica Neue, Helvetica, Arial', body:20, h:30 },
- best: { bg:'#fff', panel:'#fff', fg:'#000', accent:'#0050a0', border:'#000', font:'Atkinson Hyperlegible, Helvetica', body:20, h:30 },
- swap: 'Font: Helvetica Neue → **Atkinson Hyperlegible**',
- reasoning: 'Built by Braille Institute specifically for low-vision readers. Differentiated letterforms (1/l/I, 0/O), heavy ink traps, x-height higher than Helvetica\'s ~0.52 ratio. Free via Google Fonts. NIA-aligned. Single highest-impact change in the senior band.',
- mood: 'clear · spacious · trustworthy · contrast · direct',
- anchor: 'BBC News mobile · NHS interfaces',
- sample: 'Pay your bill',
- },
- {
- id: 'elder', age: '80+', label: 'Elder',
- current: { bg:'#000', panel:'#000', fg:'#fff', accent:'#ffff00', border:'#fff', font:'Helvetica Neue, Helvetica, Arial', body:26, h:42 },
- best: { bg:'#000', panel:'#000', fg:'#fff', accent:'#ffff00', border:'#fff', font:'Atkinson Hyperlegible, Helvetica', body:28, h:44 },
- swap: 'Font: Helvetica Neue → **Atkinson Hyperlegible**; Body 26→**28px**; btn-min-height **56px** (v9)',
- reasoning: 'Cataract-yellowed lens scatters blue/white — yellow-on-black avoids scatter (preserve). At 80+ letterform disambiguation (m/n/rn, 1/l/I, 0/O) is critical. Atkinson Hyperlegible is purpose-built. WCAG 2.1 large-text floor is 24px regular — 28px gives comfortable margin. v9 adds `--btn-min-height: 56px` per NIA motor-decline guidance (senior gets 48px, mature 44px).',
- mood: 'bold · clinical · safe · simple · monumental',
- anchor: 'AARP large-print · Amazon Mama Bear senior phone',
- sample: 'CALL FAMILY',
- },
- ];
+ const bands = AGE_THEME_DATA.bands;
const renderBand = (b) => {
const swatch = (theme) => `
@@ -2599,9 +2556,10 @@ app.get('/age-themes', (req, res) => {
<a href="/age-themes" class="on">/age-themes</a>
</nav>
<div class="credits">
+ <strong style="color:#d2b15c">v${AGE_THEME_DATA.version}</strong> · validated ${AGE_THEME_DATA.validated_at} ·
<strong style="color:#d2554a">⚠ AI-generated starting point — not user-tested, not accessibility-tooled.</strong>
- Recommendations from the <span class="src">ui-ux-designer</span> subagent (font/hex/size swaps with research citations) and reviewed by the <span class="src">debate-team-fast</span> 3-LLM panel (Claude, Kimi K2.5, Qwen3:14b — Codex quota-blocked). Companion <span class="src">Figma file</span> created at <a href="https://www.figma.com/design/WmwSirHywv530eACPtbqyA" target="_blank" style="color:#d2b15c">figma.com/design/WmwSirHywv530eACPtbqyA</a> (DW Pro team). Reference impl: <code style="color:#d2b15c">~/.claude/skills/age-adaptive-theme/reference-implementation.html</code>.
- <br><span style="color:#888">Debate-team consensus: the Atkinson Hyperlegible / tween indigo / Nunito picks are defensible. The fg #222→#1a1a1a (toddler) and -apple-system→Inter (adult) swaps are LLM "hallucinated precision" — keep current. APCA Lc claims need real-tool validation before shipping.</span>
+ Recommendations from the <span class="src">ui-ux-designer</span> subagent (font/hex/size swaps with research citations) and reviewed by the <span class="src">debate-team-fast</span> 3-LLM panel (Claude, Kimi K2.5, Qwen3:14b — Codex quota-blocked). Companion <span class="src">Figma file</span> at <a href="https://www.figma.com/design/WmwSirHywv530eACPtbqyA" target="_blank" style="color:#d2b15c">figma.com/design/WmwSirHywv530eACPtbqyA</a>. Canonical band data: <code style="color:#d2b15c">~/.claude/skills/age-adaptive-theme/bands.json</code> · APCA module: <code style="color:#d2b15c">~/Projects/wallco-ai/src/apca.js</code>.
+ <br><span style="color:#888">Debate-team consensus: Atkinson Hyperlegible / tween indigo / Nunito picks are defensible. The fg #222→#1a1a1a (toddler) and -apple-system→Inter (adult) swaps were LLM "hallucinated precision" — dropped. APCA Lc values shown are calculated by src/apca.js (Myndex 0.1.9), NOT lab-verified. v9 fixes from validation-loop: teen body 16px, line-height 1.5 baseline, btn-min-height 44/48/56px per band.</span>
</div>
</header>
<main>
diff --git a/src/apca.js b/src/apca.js
new file mode 100644
index 0000000..3d86bdb
--- /dev/null
+++ b/src/apca.js
@@ -0,0 +1,65 @@
+// APCA-W3 Lc calculator (Myndex / SACAM 0.1.9 reference algorithm).
+// Returns signed Lc value: positive = dark text on light bg, negative = light on dark.
+// |Lc| ≥ 60 = comfortably readable body · ≥ 45 UI components · ≥ 75 fine print.
+//
+// Canonical location. Mirrored to ~/.claude/skills/age-adaptive-theme/lib/apca.js
+// (identical bytes — diff to detect drift).
+//
+// NOT lab-verified. Self-implemented APCA is not WCAG compliance evidence.
+// For shipping anywhere regulated, cross-check at colour-contrast.com/apca.
+
+'use strict';
+
+function expand(h) {
+ h = h.replace('#', '');
+ return h.length === 3 ? h.split('').map(c => c + c).join('') : h;
+}
+
+function hexToRgb(h) {
+ const x = expand(h);
+ return [parseInt(x.slice(0, 2), 16), parseInt(x.slice(2, 4), 16), parseInt(x.slice(4, 6), 16)];
+}
+
+function sRGBtoY([r, g, b]) {
+ return 0.2126729 * Math.pow(r / 255, 2.4)
+ + 0.7151522 * Math.pow(g / 255, 2.4)
+ + 0.0721750 * Math.pow(b / 255, 2.4);
+}
+
+function apcaLc(textHex, bgHex) {
+ const BlkThrs = 0.022, BlkClmp = 1.414, LoConThresh = 0.1;
+ const ScaleBoW = 1.14, LoBoWOffset = 0.027;
+ const ScaleWoB = 1.14, LoWoBOffset = 0.027;
+
+ let txtY = sRGBtoY(hexToRgb(textHex));
+ let bgY = sRGBtoY(hexToRgb(bgHex));
+
+ if (txtY < BlkThrs) txtY = txtY + Math.pow(BlkThrs - txtY, BlkClmp);
+ if (bgY < BlkThrs) bgY = bgY + Math.pow(BlkThrs - bgY, BlkClmp);
+ if (Math.abs(bgY - txtY) < 0.0005) return 0;
+
+ if (bgY > txtY) {
+ const SAPC = (Math.pow(bgY, 0.56) - Math.pow(txtY, 0.57)) * ScaleBoW;
+ if (SAPC < LoConThresh) return 0;
+ return Math.round((SAPC - LoBoWOffset) * 100);
+ } else {
+ const SAPC = (Math.pow(bgY, 0.65) - Math.pow(txtY, 0.62)) * ScaleWoB;
+ if (SAPC > -LoConThresh) return 0;
+ return Math.round((SAPC + LoWoBOffset) * 100);
+ }
+}
+
+// Classify a signed Lc value into a WCAG-ish tier.
+// aaa = ≥75 (body comfortable / fine-print OK)
+// aa = ≥60 (body comfortable)
+// ui = ≥45 (UI components / large text)
+// fail = below 45
+function classifyLc(lc) {
+ const a = Math.abs(lc);
+ if (a >= 75) return 'aaa';
+ if (a >= 60) return 'aa';
+ if (a >= 45) return 'ui';
+ return 'fail';
+}
+
+module.exports = { apcaLc, classifyLc, expand, hexToRgb, sRGBtoY };
← e637e49 tick 12: process 64 new wikimedia thumbnail-mode rows (palet
·
back to Wallco Ai
·
wallco.ai: color-wheel hue swatch row above /designs · 10 bu 702863d →