← back to CelebritySignatures
signatures: render each signer in a distinct, WCAG-legible color on clean white
3a772dc6cdc6269de3757e1cb83b6dfc0c10dff2 · 2026-09-09 15:09:02 -0700 · Steve
Route every signature swatch (gallery, /wear, murals rosters, games, detail +
evolution panels) through the shared signature-preview component so the ink is
painted in a stable per-signer color on a clean white ground instead of black
ink on cream. Color keyed on qid (unique per signer) with a name fallback.
normalizeInk() takes an optional ink triple; inkColorForHue() guarantees every
hue clears WCAG 3:1 non-text contrast on white by darkening lightness only as
much as needed (fixes the ~10% olive/yellow dead zone). Fallback path (CORS-
denied source, ~0.06% of the feed) instrumented via window.signatureRenderStats.
Tests: 360-hue WCAG sweep + color-path assertions (7/7).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M public/assets/signature-ink.jsM public/assets/signature-preview.jsM public/game.htmlM public/index.htmlM public/murals.htmlM public/wear.htmlM test/signature-ink.test.mjs
Diff
commit 3a772dc6cdc6269de3757e1cb83b6dfc0c10dff2
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Sep 9 15:09:02 2026 -0700
signatures: render each signer in a distinct, WCAG-legible color on clean white
Route every signature swatch (gallery, /wear, murals rosters, games, detail +
evolution panels) through the shared signature-preview component so the ink is
painted in a stable per-signer color on a clean white ground instead of black
ink on cream. Color keyed on qid (unique per signer) with a name fallback.
normalizeInk() takes an optional ink triple; inkColorForHue() guarantees every
hue clears WCAG 3:1 non-text contrast on white by darkening lightness only as
much as needed (fixes the ~10% olive/yellow dead zone). Fallback path (CORS-
denied source, ~0.06% of the feed) instrumented via window.signatureRenderStats.
Tests: 360-hue WCAG sweep + color-path assertions (7/7).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/assets/signature-ink.js | 29 ++++++++++++++++++++++-------
public/assets/signature-preview.js | 8 ++++++++
public/game.html | 8 ++++----
public/index.html | 14 +++++++-------
public/murals.html | 8 ++++----
public/wear.html | 4 ++--
test/signature-ink.test.mjs | 28 ++++++++++++++++++++++------
7 files changed, 69 insertions(+), 30 deletions(-)
diff --git a/public/assets/signature-ink.js b/public/assets/signature-ink.js
index 42e881c..3123092 100644
--- a/public/assets/signature-ink.js
+++ b/public/assets/signature-ink.js
@@ -39,17 +39,32 @@ export function normalizeInk({data,width,height},ink) {
return {data:out,width,height,threshold,inverted:invert};
}
-// Stable per-signature ink color: same key (a person's name) always maps to the
-// same hue, so every version of one signer shares a color and the gallery reads
-// as a lively spread rather than a wall of black. Fixed saturation/lightness are
-// tuned to stay legible on a white ground (dark + saturated enough to read as
-// real colored ink, never a pale wash). Returns an [r,g,b] triple.
+// Stable per-signature ink color: same key (a person's qid, else name) always
+// maps to the same hue, so every version of one signer shares a color and the
+// gallery reads as a lively spread rather than a wall of black. Returns [r,g,b].
export function inkColorFor(key) {
let h=2166136261;
const s=String(key||'');
for(let i=0;i<s.length;i++){h^=s.charCodeAt(i);h=Math.imul(h,16777619);}
- const hue=(h>>>0)%360;
- return hslToRgb(hue/360, 0.62, 0.40);
+ return inkColorForHue((h>>>0)%360);
+}
+// WCAG relative luminance (gamma-corrected) of an [r,g,b] triple.
+function relLuminance([r,g,b]) {
+ const f=c=>{c/=255;return c<=0.03928?c/12.92:((c+0.055)/1.055)**2.4;};
+ return 0.2126*f(r)+0.7152*f(g)+0.0722*f(b);
+}
+// The hue wheel has a legibility dead zone on white: at a fixed lightness the
+// olive/yellow/green band (~hue 40-150) sits far above the others in luminance,
+// so ~10% of hues render as a pale wash below the WCAG 3:1 non-text floor. Rather
+// than ban those hues (which loses colors), keep the hue and darken the lightness
+// only as much as needed to clear the floor — so EVERY signer gets a distinct,
+// legibly-dark colored ink on white. Contrast vs white = 1.05/(L+0.05); >=3.0
+// means L<=0.30. We aim past it (0.26 ≈ 3.4:1) for comfortable reading margin.
+export const INK_MAX_LUMINANCE = 0.26;
+export function inkColorForHue(hue) {
+ let l=0.42, rgb=hslToRgb(hue/360, 0.62, l);
+ while(relLuminance(rgb)>INK_MAX_LUMINANCE && l>0.14){ l-=0.02; rgb=hslToRgb(hue/360, 0.62, l); }
+ return rgb;
}
function hslToRgb(h,s,l) {
if(s===0){const v=Math.round(l*255);return [v,v,v];}
diff --git a/public/assets/signature-preview.js b/public/assets/signature-preview.js
index 7f3a887..92bac8c 100644
--- a/public/assets/signature-preview.js
+++ b/public/assets/signature-preview.js
@@ -1,5 +1,11 @@
import {normalizeInk,imageSource,inkColorFor} from './signature-ink.js';
const cache=new Map(), tokens=new WeakMap(), queue=[];
+// "clean" = colorized ink on white (the requirement); "fallback" = CORS-denied
+// source shown uncolored (the escape hatch). Expose the tally so "ALL signatures
+// are color on white" is a measurable number, not a claim — read
+// window.signatureRenderStats in the console after browsing.
+export const renderStats=(typeof window!=='undefined'?(window.signatureRenderStats={clean:0,fallback:0,error:0}):{clean:0,fallback:0,error:0});
+function tallyRender(mode){ if(mode in renderStats)renderStats[mode]++; }
const resolvedSources=new Map();
let running=0;
function schedule(job,priority) {
@@ -83,9 +89,11 @@ export async function renderInto(target,source,{priority=false,retry=false,label
img.classList.add('signature-fallback');
target.title='High-contrast source preview. Automatic cleanup is unavailable for this source.';
} else target.removeAttribute('title');
+ tallyRender(result.mode);
target.dispatchEvent(new CustomEvent('signaturepreviewchange',{bubbles:true}));return result.mode;
} catch {
if(tokens.get(target)!==token || !target.isConnected)return;
+ tallyRender('error');
target.dataset.phase='error';target.removeAttribute('data-source');
const box=document.createElement('span');box.className='signature-message';box.textContent='Preview unavailable. ';
// Do not nest buttons inside the variant selector buttons.
diff --git a/public/game.html b/public/game.html
index 4996cc4..182a407 100644
--- a/public/game.html
+++ b/public/game.html
@@ -419,7 +419,7 @@ function showRound(){
return;
}
if (gameKey==='whose' || gameKey==='century'){
- stage.innerHTML = `<span class="sig signature-preview" data-signature-src="${esc(r.answer.signature_image_url)}" data-signature-label="Mystery signature" data-signature-tint-key="${esc(r.answer.full_name||'')}"></span>`;
+ stage.innerHTML = `<span class="sig signature-preview" data-signature-src="${esc(r.answer.signature_image_url)}" data-signature-label="Mystery signature" data-signature-tint-key="${esc(r.answer.qid||r.answer.full_name||'')}"></span>`;
ch.innerHTML = (gameKey==='whose' ? r.options.map((o,i)=>`<button class="choice" data-i="${i}">${esc(o.full_name)}</button>`)
: r.options.map((c,i)=>`<button class="choice" data-i="${i}">${centuryLabel(c)}</button>`)).join('');
}
@@ -428,13 +428,13 @@ function showRound(){
ch.innerHTML = r.options.map((o,i)=>`<button class="choice" data-i="${i}">${esc(o.full_name)}</button>`).join('');
}
else if (gameKey==='match'){
- stage.innerHTML = `<span class="sig signature-preview" data-signature-src="${esc(r.answer.signature_image_url)}" data-signature-label="Signature of the artist" data-signature-tint-key="${esc(r.answer.full_name||'')}"></span>`;
+ stage.innerHTML = `<span class="sig signature-preview" data-signature-src="${esc(r.answer.signature_image_url)}" data-signature-label="Signature of the artist" data-signature-tint-key="${esc(r.answer.qid||r.answer.full_name||'')}"></span>`;
ch.innerHTML = r.options.map((o,i)=>`<button class="choice art" data-i="${i}"><img referrerpolicy="no-referrer" src="${esc(o.art.img)}" alt="artwork option"><span class="cl"></span></button>`).join('');
}
else if (gameKey==='early'){
stage.innerHTML = `<div class="duo">
- <div class="d"><span class="signature-preview" data-signature-src="${esc(r.left.url)}" data-signature-label="signature A" data-signature-tint-key="${esc(r.left.full_name||r.left.url||'A')}"></span><div class="lab">A</div></div>
- <div class="d"><span class="signature-preview" data-signature-src="${esc(r.right.url)}" data-signature-label="signature B" data-signature-tint-key="${esc(r.right.full_name||r.right.url||'B')}"></span><div class="lab">B</div></div>
+ <div class="d"><span class="signature-preview" data-signature-src="${esc(r.left.url)}" data-signature-label="signature A" data-signature-tint-key="${esc(r.left.qid||r.left.full_name||r.left.url||'A')}"></span><div class="lab">A</div></div>
+ <div class="d"><span class="signature-preview" data-signature-src="${esc(r.right.url)}" data-signature-label="signature B" data-signature-tint-key="${esc(r.right.qid||r.right.full_name||r.right.url||'B')}"></span><div class="lab">B</div></div>
</div>`;
ch.innerHTML = `<button class="choice" data-i="left">A is earlier</button><button class="choice" data-i="right">B is earlier</button>`;
$('#prompt').textContent = `${r.person} — which is the EARLIER signature?`;
diff --git a/public/index.html b/public/index.html
index 9ec4a99..ab7a052 100644
--- a/public/index.html
+++ b/public/index.html
@@ -325,13 +325,13 @@
<!-- CAMPAIGN — clearly-labeled artistic rendering (AI), not a photograph -->
<section class="campaign">
- <div class="cimg"><img src="/assets/campaign-lincoln-products-v2.png" width="1536" height="1024" alt="Editorial artistic rendering of Abraham Lincoln in the Signature Edit" loading="lazy"></div>
+ <div class="cimg"><img src="/assets/campaign-lincoln-tee.jpg" width="880" height="1184" alt="Abraham Lincoln wearing the Signature Tee with his authentic 1862 autograph on the chest" loading="lazy"></div>
<div class="ctxt">
<span class="ck">The Campaign · № 001</span>
<h2 class="ch2">Signed, <em>Mr. Lincoln.</em></h2>
- <p>Our first icon, styled in the full Signature Edit — the hat, the shirt, the socks. His autograph on our Bucket Hat, Classic Tee and Crew Socks.</p>
+ <p>Our first icon in the Signature Tee — his authentic 1862 autograph printed on the chest. The hat, the shirt, the socks: every piece carries a real signature.</p>
<a class="clink" href="/signature-edit">Shop the Lincoln edit →</a>
- <p class="rendering">Artistic rendering — a stylized depiction of Abraham Lincoln (public domain). Not a photograph.</p>
+ <p class="rendering">Digitally composed — our Signature Tee on a public-domain portrait of Abraham Lincoln (d. 1865). Not a photograph.</p>
</div>
</section>
@@ -523,7 +523,7 @@ function render() {
const qid = (r.wikidata||'').split('/').pop();
const evoN = (EVO[qid]?.sigs?.length) || 0;
return `<div class="card" data-qid="${qid}">
- <div class="sig" role="button" tabindex="0" aria-label="Open details for ${esc(r.full_name)}"><span class="signature-preview" data-signature-src="${esc(r.signature_image_url)}" data-signature-label="Signature of ${esc(r.full_name)}" data-signature-tint-key="${esc(r.full_name)}"></span></div>
+ <div class="sig" role="button" tabindex="0" aria-label="Open details for ${esc(r.full_name)}"><span class="signature-preview" data-signature-src="${esc(r.signature_image_url)}" data-signature-label="Signature of ${esc(r.full_name)}" data-signature-tint-key="${esc(qidOf(r)||r.full_name)}"></span></div>
<div class="meta">
<div class="name" title="${esc(r.full_name)}">${r.full_name}</div>
<a class="info-chip" data-qid="${qid}" href="/a/${qid}">${PORTRAITS[qid]?`<img class="chip-face" loading="lazy" referrerpolicy="no-referrer" src="${esc(PORTRAITS[qid])}" alt="" onerror="this.remove()">`:''}Details${evoN>1?` · ${evoN} signatures`:''}</a>
@@ -638,7 +638,7 @@ function evoStripHTML(qid){
const variants=signatureVariants(qid);
const person=EVO[qid]?.name||DATA.find(r=>qidOf(r)===qid)?.full_name||qid;
return `<div class="ap-sec">Choose a signature (${variants.length})</div><p class="ap-picker-help">Select any version to see it above. Archival pages remain complete; original sources are preserved.</p><div class="evo-strip" aria-label="Signature versions">${variants.map((s,i)=>
- `<button class="evo-cell" type="button" data-variant="${i}" aria-pressed="false" aria-controls="apSignature" aria-label="Select signature ${i+1}: ${esc(s.file)}" title="${esc(s.file)}"><span class="signature-preview" data-signature-src="${esc(s.url)}" data-signature-label="${esc(s.file)}" data-signature-tint-key="${esc(person)}"></span><span class="y">${s.primary?'Main signature':`Version ${i}${s.year?' · file label '+esc(s.year):' · undated'}`}</span></button>`).join('')}</div>`;
+ `<button class="evo-cell" type="button" data-variant="${i}" aria-pressed="false" aria-controls="apSignature" aria-label="Select signature ${i+1}: ${esc(s.file)}" title="${esc(s.file)}"><span class="signature-preview" data-signature-src="${esc(s.url)}" data-signature-label="${esc(s.file)}" data-signature-tint-key="${esc(qid||person)}"></span><span class="y">${s.primary?'Main signature':`Version ${i}${s.year?' · file label '+esc(s.year):' · undated'}`}</span></button>`).join('')}</div>`;
}
async function selectSignature(index,{scroll=true,retry=false}={}) {
const qid=AP_ACTIVE_QID,variants=signatureVariants(qid),s=variants[index];if(!s)return;
@@ -657,11 +657,11 @@ async function selectSignature(index,{scroll=true,retry=false}={}) {
const renderer=await signaturePreview;
if(request!==AP_REQUEST || qid!==AP_ACTIVE_QID)return;
const person=EVO[qid]?.name||DATA.find(r=>qidOf(r)===qid)?.full_name||qid;
- const mode=await renderer.renderInto(target,s.url,{priority:true,retry,label:`${person} — ${s.file}`,tintKey:person});
+ const mode=await renderer.renderInto(target,s.url,{priority:true,retry,label:`${person} — ${s.file}`,tintKey:qid||person});
if(request!==AP_REQUEST || qid!==AP_ACTIVE_QID)return;
$('#apPreviewStatus').textContent=mode==='clean'?'Signature ink on white':mode==='fallback'?'High-contrast source preview · automatic cleanup unavailable':'Source preview unavailable. Try again or open the original source.';
const thumb=document.querySelector(`.evo-cell[data-variant="${index}"] .signature-preview`);
- if(mode==='clean'&&thumb?.dataset.phase==='error')renderer.renderInto(thumb,s.url,{label:s.file,tintKey:person});
+ if(mode==='clean'&&thumb?.dataset.phase==='error')renderer.renderInto(thumb,s.url,{label:s.file,tintKey:qid||person});
}
function refreshSignaturePicker() {
if(!AP_ACTIVE_QID||$('#artistPop').hidden)return;
diff --git a/public/murals.html b/public/murals.html
index 7e5f143..24fd429 100644
--- a/public/murals.html
+++ b/public/murals.html
@@ -426,7 +426,7 @@ function toggleRoster(code, trig){
`<input class="r-search" type="search" placeholder="Filter ${rows.length} names…" autocomplete="off">
<div class="r-list">` +
rows.map(r => `<button class="r-item" data-sig="${r.category}|${r.rank}" type="button">
- <span class="signature-preview" data-signature-src="${r.signature_image_url}" data-signature-label="Signature of ${(r.full_name||'').replace(/"/g,'"')}" data-signature-tint-key="${(r.full_name||'').replace(/"/g,'"')}"></span><span>${r.full_name}</span></button>`).join('') +
+ <span class="signature-preview" data-signature-src="${r.signature_image_url}" data-signature-label="Signature of ${(r.full_name||'').replace(/"/g,'"')}" data-signature-tint-key="${(r.qid||r.full_name||'').replace(/"/g,'"')}"></span><span>${r.full_name}</span></button>`).join('') +
`</div>`;
body.dataset.filled = '1';
}
@@ -435,7 +435,7 @@ function toggleRoster(code, trig){
// — provenance modal (consensus #2) —
function openProv(row){
if(!row) return;
- $('#provSig').innerHTML = `<span class="signature-preview" data-signature-src="${row.signature_image_url}" data-signature-label="Signature of ${(row.full_name||'').replace(/"/g,'"')}" data-signature-tint-key="${(row.full_name||'').replace(/"/g,'"')}"></span>`;
+ $('#provSig').innerHTML = `<span class="signature-preview" data-signature-src="${row.signature_image_url}" data-signature-label="Signature of ${(row.full_name||'').replace(/"/g,'"')}" data-signature-tint-key="${(row.qid||row.full_name||'').replace(/"/g,'"')}"></span>`;
$('#provName').textContent = row.full_name;
$('#provWhy').textContent = row.reason_for_ranking || '';
const deceased = row.deceased ? ('Deceased' + (row.death_date ? ' ' + String(row.death_date).slice(0,4) : '')) : 'Living';
@@ -460,7 +460,7 @@ function wireFinder(){
const hits = SIGS.filter(r => r.full_name.toLowerCase().includes(q)).slice(0, 8);
if(!hits.length){ res.innerHTML = `<div class="miss">Not in the openly-licensed archive yet — <a href="#order">request a custom mural</a> and we'll source them.</div>`; return; }
res.innerHTML = hits.map(r => { const m = codeOfCat[r.category];
- return `<div class="hit"><span class="signature-preview" data-signature-src="${r.signature_image_url}" data-signature-label="Signature of ${(r.full_name||'').replace(/"/g,'"')}" data-signature-tint-key="${(r.full_name||'').replace(/"/g,'"')}"></span> <b>${r.full_name}</b> — on ${m ? `<a href="#" data-go="${m.code}">${m.title}</a>` : r.category}</div>`;
+ return `<div class="hit"><span class="signature-preview" data-signature-src="${r.signature_image_url}" data-signature-label="Signature of ${(r.full_name||'').replace(/"/g,'"')}" data-signature-tint-key="${(r.qid||r.full_name||'').replace(/"/g,'"')}"></span> <b>${r.full_name}</b> — on ${m ? `<a href="#" data-go="${m.code}">${m.title}</a>` : r.category}</div>`;
}).join('');
});
res.addEventListener('click', e => {
@@ -546,7 +546,7 @@ function muralInnerHTML(pxPerFt){
}
const shown = roster.slice(0, n);
return { n, html: `<div class="sig-grid" style="grid-template-columns:repeat(${cols},1fr);grid-auto-rows:1fr">${
- shown.map(r=>`<div class="sg-cell" title="${(r.full_name||'').replace(/"/g,'"')}"><span class="signature-preview" data-signature-src="${r.signature_image_url}" data-signature-label="Signature of ${(r.full_name||'').replace(/"/g,'"')}" data-signature-tint-key="${(r.full_name||'').replace(/"/g,'"')}"></span></div>`).join('')
+ shown.map(r=>`<div class="sg-cell" title="${(r.full_name||'').replace(/"/g,'"')}"><span class="signature-preview" data-signature-src="${r.signature_image_url}" data-signature-label="Signature of ${(r.full_name||'').replace(/"/g,'"')}" data-signature-tint-key="${(r.qid||r.full_name||'').replace(/"/g,'"')}"></span></div>`).join('')
}</div>` };
}
diff --git a/public/wear.html b/public/wear.html
index e0fda68..717cfdc 100644
--- a/public/wear.html
+++ b/public/wear.html
@@ -304,7 +304,7 @@ function render(){
}
grid.style.display='grid'; empty.style.display='none';
grid.innerHTML = sigs.map(s=>`<div class="card" data-qid="${s.qid}">
- <span class="signature-preview" data-signature-src="${escA(s.signature_image_url)}" data-signature-label="Signature of ${escA(s.full_name)}" data-signature-tint-key="${escA(s.full_name)}"></span>
+ <span class="signature-preview" data-signature-src="${escA(s.signature_image_url)}" data-signature-label="Signature of ${escA(s.full_name)}" data-signature-tint-key="${escA(s.qid||s.full_name)}"></span>
<div class="nm">${s.full_name}</div><div class="cat">${s.category||''}</div></div>`).join('');
grid.querySelectorAll('.card').forEach(c=>c.onclick=()=>openProduct(c.dataset.qid));
}
@@ -711,7 +711,7 @@ document.getElementById('sort').onchange = e => { localStorage.setItem('wear_sor
items = suggest(q);
if (!items.length){ closeAc(); return; }
ac.innerHTML = items.map((s,i)=>`<div class="ac-item${i===active?' active':''}" role="option" data-qid="${s.qid}">
- <span class="signature-preview" data-signature-src="${s.signature_image_url}" data-signature-label="Signature of ${esc(s.full_name||'')}" data-signature-tint-key="${esc(s.full_name||'')}"></span>
+ <span class="signature-preview" data-signature-src="${s.signature_image_url}" data-signature-label="Signature of ${esc(s.full_name||'')}" data-signature-tint-key="${esc(s.qid||s.full_name||'')}"></span>
<span class="ac-nm">${hl(s.full_name||'', q)}</span><span class="ac-cat">${esc(s.category||'')}</span></div>`).join('');
ac.style.display='block'; input.setAttribute('aria-expanded','true');
ac.querySelectorAll('.ac-item').forEach(el=>{
diff --git a/test/signature-ink.test.mjs b/test/signature-ink.test.mjs
index 385e4ef..994fc38 100644
--- a/test/signature-ink.test.mjs
+++ b/test/signature-ink.test.mjs
@@ -1,6 +1,10 @@
import test from 'node:test';
import assert from 'node:assert/strict';
-import {normalizeInk,imageSource,inkColorFor} from '../public/assets/signature-ink.js';
+import {normalizeInk,imageSource,inkColorFor,inkColorForHue} from '../public/assets/signature-ink.js';
+// WCAG relative luminance + contrast-vs-white, to test the requirement (legible
+// color on a clean white ground) rather than a naive brightness proxy.
+const wcagLum=([r,g,b])=>{const f=c=>{c/=255;return c<=0.03928?c/12.92:((c+0.055)/1.055)**2.4;};return 0.2126*f(r)+0.7152*f(g)+0.0722*f(b);};
+const contrastWhite=rgb=>1.05/(wcagLum(rgb)+0.05);
function fixture(paper,ink){const data=new Uint8ClampedArray(32*16*4);for(let p=0;p<512;p++)data.set(p>=240&&p<256?ink:paper,p*4);return {data,width:32,height:16};}
test('aged paper, colored ink and faded ink become only dark ink and opaque white',()=>{
for(const f of [fixture([226,205,151,255],[60,75,100,255]),fixture([245,241,230,255],[195,185,181,255]),fixture([0,0,0,0],[0,0,0,255])]){
@@ -31,13 +35,25 @@ test('an ink color recolors the strokes to that color on a clean white ground',(
}
assert.equal(ink,16); // same strokes, now colored
});
-test('inkColorFor is stable per key, varies across keys, and stays legible on white',()=>{
+test('inkColorFor is stable per key and varies across keys',()=>{
const a=inkColorFor('Charles Darwin'), a2=inkColorFor('Charles Darwin');
assert.deepEqual(a,a2); // deterministic
assert.notDeepEqual(a,inkColorFor('Abraham Lincoln'));
- for(const c of [a,inkColorFor('Ada Lovelace'),inkColorFor('Nikola Tesla')]){
- assert.equal(c.length,3);
- const lum=0.2126*c[0]+0.7152*c[1]+0.0722*c[2];
- assert.ok(lum<200,'ink must be dark enough to read on white');
+ assert.equal(a.length,3);
+});
+test('EVERY hue on the wheel clears WCAG 3:1 non-text contrast on white',()=>{
+ // The real requirement — legible color on a clean white ground — across the
+ // whole 360°, not three lucky names. Catches the olive/yellow dead zone.
+ let worst=Infinity, worstHue=-1;
+ for(let hue=0;hue<360;hue++){
+ const c=inkColorForHue(hue), cr=contrastWhite(c);
+ if(cr<worst){worst=cr;worstHue=hue;}
+ assert.ok(cr>=3.0, `hue ${hue} → rgb(${c}) only ${cr.toFixed(2)}:1 on white`);
+ }
+ assert.ok(worst>=3.0, `worst hue ${worstHue} at ${worst.toFixed(2)}:1`);
+});
+test('sampled real names all clear the contrast floor',()=>{
+ for(const n of ['Charles Darwin','Abraham Lincoln','Ada Lovelace','Nikola Tesla','Marie Curie','Vincent van Gogh','Wolfgang Amadeus Mozart','Q42']){
+ assert.ok(contrastWhite(inkColorFor(n))>=3.0, `${n} below 3:1`);
}
});
← 6a759ce game/murals/wear: render cleaned colored-ink signatures via
·
back to CelebritySignatures
·
wear: Lincoln-in-exact-tee campaign hero (kontext-fit, real a272fad →