[object Object]

← back to CelebritySignatures

game/murals/wear: render cleaned colored-ink signatures via shared preview

6a759ce3b936f8f294bcb725547a62c499cafe84 · 2026-09-09 14:46:17 -0700 · Steve

Wire the signature-preview.css + signature-preview.js component into the game,
murals, and wear pages so their signatures get the same white-ground / colored-
ink cleanup as the gallery. Depends on the signature-ink.js crash fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ikfY9DhpMDXkpNRpb5ufG

Files touched

Diff

commit 6a759ce3b936f8f294bcb725547a62c499cafe84
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Sep 9 14:46:17 2026 -0700

    game/murals/wear: render cleaned colored-ink signatures via shared preview
    
    Wire the signature-preview.css + signature-preview.js component into the game,
    murals, and wear pages so their signatures get the same white-ground / colored-
    ink cleanup as the gallery. Depends on the signature-ink.js crash fix.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_014ikfY9DhpMDXkpNRpb5ufG
---
 public/game.html   | 14 ++++++++++----
 public/murals.html | 15 +++++++++++----
 public/wear.html   | 14 +++++++++++---
 3 files changed, 32 insertions(+), 11 deletions(-)

diff --git a/public/game.html b/public/game.html
index ba1e46e..4996cc4 100644
--- a/public/game.html
+++ b/public/game.html
@@ -45,6 +45,10 @@
   .stage .duo { display:flex; gap:16px; align-items:center; justify-content:center; flex-wrap:wrap; }
   .duo .d { text-align:center; }
   .duo .d img { max-height:110px; max-width:260px; background:#fff; border:1px solid var(--line); border-radius:8px; padding:8px; }
+  /* Colored-ink signatures on clean white via the shared preview component. */
+  .stage .sig.signature-preview { display:inline-flex; height:120px; max-height:120px; }
+  .stage .sig.signature-preview .signature-image { max-height:120px; }
+  .duo .d .signature-preview { height:110px; max-width:260px; background:#fff; border:1px solid var(--line); border-radius:8px; padding:8px; }
   .duo .d .lab { font:600 15px 'Playfair Display',Georgia,serif; margin-top:6px; }
   .choices { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
   .choice { padding:13px 16px; border:1px solid var(--line); border-radius:10px; background:#fff; cursor:pointer; font:inherit; font-size:14.5px; text-align:center; transition:transform .08s; }
@@ -120,6 +124,8 @@
 <meta property="og:url" content="https://celebsignatures.com/game">
 <meta property="og:image" content="https://celebsignatures.com/assets/og-share.jpg">
 <meta name="twitter:card" content="summary_large_image">
+<link rel="stylesheet" href="/assets/signature-preview.css">
+<script type="module" src="/assets/signature-preview.js"></script>
 <meta name="twitter:title" content="The Signature Games — how well do you know history’s hands?">
 <meta name="twitter:description" content="Six games: name the artist, name the painting, spot the earlier signature, and a 5-second Lightning Round. Play free and climb the leaderboard.">
 <meta name="twitter:image" content="https://celebsignatures.com/assets/og-share.jpg">
@@ -413,7 +419,7 @@ function showRound(){
     return;
   }
   if (gameKey==='whose' || gameKey==='century'){
-    stage.innerHTML = `<img class="sig" src="${esc(r.answer.signature_image_url)}" alt="Mystery signature">`;
+    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>`;
     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('');
   }
@@ -422,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 = `<img class="sig" src="${esc(r.answer.signature_image_url)}" alt="Signature of the artist">`;
+    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>`;
     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"><img referrerpolicy="no-referrer" src="${esc(r.left.url)}" alt="signature A"><div class="lab">A</div></div>
-      <div class="d"><img referrerpolicy="no-referrer" src="${esc(r.right.url)}" alt="signature B"><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.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>`;
     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/murals.html b/public/murals.html
index 19fdee1..7e5f143 100644
--- a/public/murals.html
+++ b/public/murals.html
@@ -138,6 +138,11 @@
   .mural .sig-grid{position:absolute;inset:0;display:grid;gap:2px;padding:3px;background:#fff}
   .mural .sg-cell{display:flex;align-items:center;justify-content:center;background:#fff;overflow:hidden}
   .mural .sg-cell img{max-width:94%;max-height:88%;object-fit:contain;pointer-events:none}
+  /* Colored-ink signatures on clean white via the shared preview component. */
+  .mural .sg-cell .signature-preview{width:100%;height:100%;pointer-events:none}
+  .roster .r-item .signature-preview{height:20px;width:50px;flex:none}
+  .finder .hit .signature-preview{height:26px;width:96px;flex:none}
+  #provSig .signature-preview{height:120px;width:100%}
   /* — scene presets in studio — */
   .scene-presets{display:flex;gap:6px;flex-wrap:wrap}
   .scene-presets button{font:inherit;font-size:12.5px;padding:6px 11px;border:1px solid var(--line);border-radius:8px;background:#fff;cursor:pointer;color:var(--muted)}
@@ -160,6 +165,8 @@
 <meta property="og:url" content="https://celebsignatures.com/murals">
 <meta property="og:image" content="https://celebsignatures.com/assets/og-share.jpg">
 <meta name="twitter:card" content="summary_large_image">
+<link rel="stylesheet" href="/assets/signature-preview.css">
+<script type="module" src="/assets/signature-preview.js"></script>
 <meta name="twitter:title" content="Celebrity Signature Murals — made to order">
 <meta name="twitter:description" content="A wall of history in their own hand. Custom-size signature murals from $10/sq ft, printed to order.">
 <meta name="twitter:image" content="https://celebsignatures.com/assets/og-share.jpg">
@@ -419,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">
-          <img loading="lazy" src="${r.signature_image_url}" alt=""><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,'&quot;')}" data-signature-tint-key="${(r.full_name||'').replace(/"/g,'&quot;')}"></span><span>${r.full_name}</span></button>`).join('') +
       `</div>`;
     body.dataset.filled = '1';
   }
@@ -428,7 +435,7 @@ function toggleRoster(code, trig){
 // — provenance modal (consensus #2) —
 function openProv(row){
   if(!row) return;
-  $('#provSig').innerHTML = `<img src="${row.signature_image_url}" alt="${row.full_name} signature">`;
+  $('#provSig').innerHTML = `<span class="signature-preview" data-signature-src="${row.signature_image_url}" data-signature-label="Signature of ${(row.full_name||'').replace(/"/g,'&quot;')}" data-signature-tint-key="${(row.full_name||'').replace(/"/g,'&quot;')}"></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';
@@ -453,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"><img src="${r.signature_image_url}" alt=""> <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,'&quot;')}" data-signature-tint-key="${(r.full_name||'').replace(/"/g,'&quot;')}"></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 => {
@@ -539,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,'&quot;')}"><img loading="lazy" src="${thumbUrl(r.signature_image_url)}" alt="${(r.full_name||'').replace(/"/g,'&quot;')} signature" onerror="this.closest('.sg-cell').style.visibility='hidden'"></div>`).join('')
+    shown.map(r=>`<div class="sg-cell" title="${(r.full_name||'').replace(/"/g,'&quot;')}"><span class="signature-preview" data-signature-src="${r.signature_image_url}" data-signature-label="Signature of ${(r.full_name||'').replace(/"/g,'&quot;')}" data-signature-tint-key="${(r.full_name||'').replace(/"/g,'&quot;')}"></span></div>`).join('')
   }</div>` };
 }
 
diff --git a/public/wear.html b/public/wear.html
index 7b6084d..e0fda68 100644
--- a/public/wear.html
+++ b/public/wear.html
@@ -24,7 +24,7 @@
   .cart-btn:hover { border-color:var(--ink); }
   .cart-badge { display:inline-block; margin-left:4px; background:var(--ink); color:#fff; border-radius:999px; font-size:11px; min-width:16px; padding:1px 5px; text-align:center; }
   .cart-item { display:flex; gap:10px; align-items:center; padding:10px 0; border-bottom:1px solid var(--line); }
-  .cart-item img { width:44px; height:44px; object-fit:contain; background:#f7f5f0; border-radius:8px; padding:4px; mix-blend-mode:multiply; }
+  .cart-item img { width:44px; height:44px; object-fit:contain; background:#fff; border-radius:8px; padding:4px; mix-blend-mode:multiply; }
   .cart-item .ci-info { flex:1; min-width:0; }
   .cart-item .ci-name { font-size:14px; font-weight:500; }
   .cart-item .ci-meta { font-size:12px; color:#6b6b6b; }
@@ -50,6 +50,9 @@
   .card { background:#fff; border:1px solid var(--line); border-radius:12px; padding:14px; cursor:pointer; text-align:center; transition:transform .12s, box-shadow .12s; }
   .card:hover { transform:translateY(-2px); box-shadow:0 8px 22px rgba(0,0,0,.07); }
   .card img { max-width:100%; height:64px; object-fit:contain; mix-blend-mode:multiply; }
+  /* Colored-ink signatures on clean white via the shared preview component. */
+  .card .signature-preview { height:64px; }
+  .ac-item .signature-preview { width:40px; height:34px; flex:0 0 40px; background:#fff; }
   .card .nm { font-size:13px; margin-top:8px; font-weight:500; }
   .card .cat { font-size:11px; color:#9a978f; text-transform:uppercase; letter-spacing:.05em; }
   .empty { background:#fff; border:1px dashed var(--line); border-radius:12px; padding:44px 24px; text-align:center; color:#6b6b6b; }
@@ -106,6 +109,8 @@
   .fine { font-size:12px; color:#9a978f; margin-top:12px; }
   .close { position:absolute; top:14px; right:18px; font-size:26px; color:#fff; cursor:pointer; z-index:41; }
 </style>
+<link rel="stylesheet" href="/assets/signature-preview.css">
+<script type="module" src="/assets/signature-preview.js"></script>
 </head>
 <body>
 <header>
@@ -191,6 +196,9 @@
 
 <script>
 const state = { data:null, sig:null, garment:null, color:null, size:null };
+// Top-level HTML-attribute escape (the autocomplete's own `esc` is scoped to
+// that block, so the grid renderer needs its own).
+const escA = s => String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
 
 // ---- cart (localStorage-backed; no login required) ----
 const CART_KEY = 'wear_cart';
@@ -296,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}">
-    <img src="${s.signature_image_url}" alt="${s.full_name}" loading="lazy" onerror="this.style.opacity=.15">
+    <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>
     <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));
 }
@@ -703,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}">
-      <img src="${s.signature_image_url}" alt="" loading="lazy" onerror="this.style.visibility='hidden'">
+      <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="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=>{

← 5499131 signatures: permanently exclude Q352 (brand safety) from eve  ·  back to CelebritySignatures  ·  signatures: render each signer in a distinct, WCAG-legible c 3a772dc →