[object Object]

← back to CelebritySignatures

signatures: show TRUE source colors, crisp — drop the invented per-signer recoloring

18cca45619739b0d3a870d6d6cd5f56b6948d7ea · 2026-09-09 16:13:19 -0700 · Steve

Steve: 'make all signatures original colors, not all diff colors on index page'
(+ a green box on the shirt, + blurry). Root causes & fixes:

- Gallery/detail (signature-preview): render the ORIGINAL resolved image directly
  instead of the canvas-normalized copy. SVGs stay vector (crisp at any size) and
  every signature keeps its true source colors — no more raster blur, no rainbow.
- Shirt (wear canvas): DEFAULT is now the original art (multiply drops the white
  ground, shows true colors) — no recolor. The green box was recolorInk's source-in
  fill painting every opaque pixel of a white-bg source; recolorInk now thresholds
  (ink→color, background→transparent) so a buyer's chosen color / white-on-dark
  colors only the strokes. 3D decal uses a matching knockoutWhite for the original.
- Print (server): recolor only when the buyer picked a color or the cloth is dark;
  otherwise keep the original art.
- Ink control default is now 'Original' (the signature's own colors); presets +
  Custom remain for buyers who want a specific color.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 18cca45619739b0d3a870d6d6cd5f56b6948d7ea
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Sep 9 16:13:19 2026 -0700

    signatures: show TRUE source colors, crisp — drop the invented per-signer recoloring
    
    Steve: 'make all signatures original colors, not all diff colors on index page'
    (+ a green box on the shirt, + blurry). Root causes & fixes:
    
    - Gallery/detail (signature-preview): render the ORIGINAL resolved image directly
      instead of the canvas-normalized copy. SVGs stay vector (crisp at any size) and
      every signature keeps its true source colors — no more raster blur, no rainbow.
    - Shirt (wear canvas): DEFAULT is now the original art (multiply drops the white
      ground, shows true colors) — no recolor. The green box was recolorInk's source-in
      fill painting every opaque pixel of a white-bg source; recolorInk now thresholds
      (ink→color, background→transparent) so a buyer's chosen color / white-on-dark
      colors only the strokes. 3D decal uses a matching knockoutWhite for the original.
    - Print (server): recolor only when the buyer picked a color or the cloth is dark;
      otherwise keep the original art.
    - Ink control default is now 'Original' (the signature's own colors); presets +
      Custom remain for buyers who want a specific color.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/assets/signature-preview.js | 37 +++++---------
 public/wear.html                   | 98 ++++++++++++++++++++++++--------------
 server.js                          |  7 ++-
 3 files changed, 79 insertions(+), 63 deletions(-)

diff --git a/public/assets/signature-preview.js b/public/assets/signature-preview.js
index 92bac8c..217bd62 100644
--- a/public/assets/signature-preview.js
+++ b/public/assets/signature-preview.js
@@ -1,4 +1,4 @@
-import {normalizeInk,imageSource,inkColorFor} from './signature-ink.js';
+import {imageSource} 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
@@ -41,28 +41,21 @@ async function resolveSource(url) {
   }
   return resolvedSources.get(title);
 }
-async function convert(source,ink) {
+async function convert(source) {
   let url=source;try{url=await resolveSource(source);}catch{}
-  let img;
-  try {img=await load(url);} catch {
-    // Some archives deny CORS even though their image is publicly viewable.
-    await load(url,false);return {url,mode:'fallback'};
-  }
-  try {
-    const scale=Math.min(1,1600/Math.max(img.naturalWidth,img.naturalHeight));
-    const canvas=document.createElement('canvas');canvas.width=Math.max(1,Math.round(img.naturalWidth*scale));canvas.height=Math.max(1,Math.round(img.naturalHeight*scale));
-    const ctx=canvas.getContext('2d',{willReadFrequently:true});ctx.drawImage(img,0,0,canvas.width,canvas.height);
-    const result=normalizeInk(ctx.getImageData(0,0,canvas.width,canvas.height),ink);
-    ctx.putImageData(new ImageData(result.data,result.width,result.height),0,0);
-    return {url:canvas.toDataURL('image/png'),mode:'clean'};
-  } catch { return {url,mode:'fallback'}; }
+  // Show the ORIGINAL signature art directly — crisp (an SVG stays vector at any
+  // size) and in its TRUE source colors, on the white ground. No canvas
+  // recolor/binarize: that rasterized (blurred) the art and flattened every
+  // signature to one invented color. A plain <img> needs no CORS, so more
+  // sources load cleanly too.
+  try { await load(url,false); return {url,mode:'clean'}; }
+  catch { return {url,mode:'fallback'}; }
 }
-function preview(url,ink,priority,retry) {
-  // Ink color is part of the identity of the cleaned image, so it keys the cache.
-  const key=url+'|'+(ink?ink.join(','):'-');
+function preview(url,priority,retry) {
+  const key=url;
   if(retry)cache.delete(key);
   if(cache.has(key))return cache.get(key);
-  const p=schedule(()=>convert(url,ink),priority);cache.set(key,p);
+  const p=schedule(()=>convert(url),priority);cache.set(key,p);
   p.catch(()=>{if(cache.get(key)===p)cache.delete(key);});
   // Bound retained pixel previews, including long gallery browsing sessions.
   if(cache.size>96)cache.delete(cache.keys().next().value);
@@ -73,13 +66,9 @@ export async function renderInto(target,source,{priority=false,retry=false,label
   target.dataset.phase='loading';target.replaceChildren();
   const waiting=document.createElement('span');waiting.className='signature-message';waiting.textContent='Loading signature…';target.append(waiting);
   const url=imageSource(source,location.href);
-  // A signature's distinct ink color is stable per signer. Prefer an explicit
-  // tint key (the person's name) so every version of one signer shares a color;
-  // fall back to the label, then the source URL.
-  const ink=inkColorFor(tintKey || (label||'').replace(/^Signature of\s+/i,'') || url || source);
   try {
     if(!url)throw new Error('Unsupported image source');
-    const result=await preview(url,ink,priority,retry);
+    const result=await preview(url,priority,retry);
     if(tokens.get(target)!==token || !target.isConnected)return;
     const img=new Image();img.alt=label;img.className='signature-image';img.src=result.url;
     await img.decode();
diff --git a/public/wear.html b/public/wear.html
index 8d34ff5..65c5f49 100644
--- a/public/wear.html
+++ b/public/wear.html
@@ -101,8 +101,7 @@
   .chip.sel { border-color:var(--ink); background:var(--ink); color:#fff; }
   .swatch { width:30px; height:30px; border-radius:50%; border:2px solid #fff; box-shadow:0 0 0 1px var(--line); cursor:pointer; }
   .swatch.sel { box-shadow:0 0 0 2px var(--ink); }
-  .ink-auto.sel { box-shadow:0 0 0 2px var(--green); }
-  .chip.ink-custom { padding:5px 12px; font-size:12px; }
+  .chip.ink-original, .chip.ink-custom { padding:5px 12px; font-size:12px; }
   .price { font:600 22px 'Playfair Display',serif; margin:10px 0; }
   .buy input[type=email] { width:100%; padding:10px 12px; border:1px solid var(--line); border-radius:8px; font:inherit; }
   .btn { display:inline-block; width:100%; text-align:center; margin-top:12px; padding:12px 20px; border:0; border-radius:999px; background:var(--ink); color:#fff; font:inherit; cursor:pointer; }
@@ -394,48 +393,71 @@ function luminance(hex){
   const n = parseInt(hex.slice(1),16), r=(n>>16)&255, g=(n>>8)&255, b=n&255;
   return (0.299*r + 0.587*g + 0.114*b) / 255;
 }
-// Repaint the signature's opaque strokes a solid ink color, preserving the
-// stroke's own alpha (anti-aliasing) via 'source-in'. Used to put the signer's
-// distinct color on light cloth and pure white on dark cloth.
+// Recolor ONLY the ink strokes to a solid color on a TRANSPARENT ground. It
+// thresholds each pixel (dark = ink → colored + opaque; light = background →
+// transparent) so a white-background source becomes colored strokes, NOT a
+// solid colored rectangle (the old source-in fill painted every opaque pixel,
+// which is what turned white-bg signatures into a green box). Only used when the
+// buyer picks a color or on dark cloth; the default keeps the original art.
 function recolorInk(img, hex){
-  const off = document.createElement('canvas'); off.width = img.width; off.height = img.height;
-  const octx = off.getContext('2d');
-  octx.drawImage(img, 0, 0);
-  octx.globalCompositeOperation = 'source-in';
-  octx.fillStyle = hex;
-  octx.fillRect(0, 0, off.width, off.height);
+  const w = img.width || img.naturalWidth || 300, h = img.height || img.naturalHeight || 150;
+  const off = document.createElement('canvas'); off.width = w; off.height = h;
+  const octx = off.getContext('2d', { willReadFrequently:true });
+  octx.drawImage(img, 0, 0, w, h);
+  let id; try { id = octx.getImageData(0, 0, w, h); } catch { return img; } // tainted → keep original
+  const d = id.data, n = parseInt(hex.slice(1),16), R=(n>>16)&255, G=(n>>8)&255, B=n&255;
+  for (let i = 0; i < d.length; i += 4){
+    const a = d[i+3]/255;
+    const lum = (0.299*d[i] + 0.587*d[i+1] + 0.114*d[i+2]) * a + 255*(1-a); // as seen over white
+    if (lum < 160){ d[i]=R; d[i+1]=G; d[i+2]=B; d[i+3]=255; }               // ink → chosen color
+    else { d[i+3] = 0; }                                                    // background → transparent
+  }
+  octx.putImageData(id, 0, 0);
   return off;
 }
-// The ink color for the CURRENT signature on garment color `col`: the signer's
-// own distinct color on light cloth (composited via 'multiply' so fabric shading
-// still reads), switching to pure WHITE on dark cloth where a dark color would
-// be invisible. `window.__signerInk` is provided by a module bridge (below);
-// fall back to a legible dark ink if it hasn't loaded yet.
-function inkHexFor(col){
-  if (state.inkColor) return state.inkColor;                      // buyer's override wins
-  if (luminance(col.hex) < 0.4) return '#ffffff';                 // auto: white on dark cloth
-  const key = state.sig && (state.sig.qid || state.sig.full_name);
-  return (window.__signerInk && key) ? window.__signerInk(key) : '#1a1a1a';  // auto: signer's color
+// Knock out a near-white/transparent background to transparent while KEEPING the
+// ink's original colors — for the 3D decal, whose alpha-tested texture needs a
+// transparent ground (it can't lean on the 2D 'multiply' trick) but must still
+// show the signature in its true source colors.
+function knockoutWhite(img){
+  const w = img.width || img.naturalWidth || 300, h = img.height || img.naturalHeight || 150;
+  const off = document.createElement('canvas'); off.width = w; off.height = h;
+  const octx = off.getContext('2d', { willReadFrequently:true });
+  octx.drawImage(img, 0, 0, w, h);
+  let id; try { id = octx.getImageData(0, 0, w, h); } catch { return img; }
+  const d = id.data;
+  for (let i = 0; i < d.length; i += 4){
+    const lum = 0.299*d[i] + 0.587*d[i+1] + 0.114*d[i+2];
+    if (d[i+3] < 10 || lum > 210) d[i+3] = 0;                  // transparent / near-white → transparent
+  }
+  octx.putImageData(id, 0, 0);
+  return off;
 }
-// The signer's own auto color (for the "Auto" swatch preview).
-function autoInkHex(){
-  const key = state.sig && (state.sig.qid || state.sig.full_name);
-  return (window.__signerInk && key) ? window.__signerInk(key) : '#1a1a1a';
+// The ink color for the CURRENT signature on garment color `col`:
+//   • buyer picked a color  → that color
+//   • dark cloth, no pick    → white (a dark signature would vanish on black)
+//   • light cloth, no pick   → null = keep the ORIGINAL art (true source colors,
+//                              composited via 'multiply' so the white ground
+//                              drops out and fabric shading still reads)
+function inkHexFor(col){
+  if (state.inkColor) return state.inkColor;
+  if (luminance(col.hex) < 0.4) return '#ffffff';
+  return null;
 }
-// Render the SIGNATURE INK chips: Auto (signer's color) + curated presets +
-// Custom…, reflecting the current state.inkColor selection.
+// Render the SIGNATURE INK chips: Original (the signature's own colors) + curated
+// presets + Custom…, reflecting the current state.inkColor selection.
 function buildInkChips(){
   const I = document.getElementById('inkChips'); if (!I) return;
   const cur = state.inkColor;
   const isPreset = cur && INK_PRESETS.some(p=>p.hex.toLowerCase()===cur.toLowerCase());
-  let html = `<span class="swatch ink-auto ${cur===null?'sel':''}" data-ink="auto" title="Auto — this signer's color (white on dark cloth)" style="background:${autoInkHex()}"></span>`;
+  let html = `<span class="chip ink-original ${cur===null?'sel':''}" data-ink="auto" title="Original — the signature's own colors">Original</span>`;
   html += INK_PRESETS.map(p=>`<span class="swatch ${cur&&cur.toLowerCase()===p.hex.toLowerCase()?'sel':''}" data-ink="${p.hex}" title="${p.label}" style="background:${p.hex}"></span>`).join('');
   html += `<span class="chip ink-custom ${cur&&!isPreset?'sel':''}" data-ink="custom">${cur&&!isPreset?cur.toUpperCase():'Custom…'}</span>`;
   I.innerHTML = html;
   I.querySelectorAll('[data-ink]').forEach(el=>el.onclick=()=>{
     const v = el.dataset.ink;
     if (v==='auto'){ state.inkColor=null; buildOptions(); draw(); }
-    else if (v==='custom'){ const p=document.getElementById('inkCustom'); p.value = (state.inkColor||autoInkHex()); p.click(); }
+    else if (v==='custom'){ const p=document.getElementById('inkCustom'); p.value = (state.inkColor||'#1a1a1a'); p.click(); }
     else { state.inkColor=v; buildOptions(); draw(); }
   });
   const picker=document.getElementById('inkCustom');
@@ -493,7 +515,8 @@ function update3DGarment(){
   const cnv = document.createElement('canvas'); cnv.width = 512; cnv.height = 512;
   const cctx = cnv.getContext('2d');
   const ar = sigImg.width/sigImg.height; let w = 420, h = w/ar; if (h > 200) { h = 200; w = h*ar; }
-  const drawSrc = recolorInk(sigImg, inkHexFor(state.color));   // signer color on light, white on dark
+  const _ink3d = inkHexFor(state.color);
+  const drawSrc = _ink3d ? recolorInk(sigImg, _ink3d) : knockoutWhite(sigImg);  // original art on transparent, else recolor
   cctx.drawImage(drawSrc, (512-w)/2, (512-h)/2, w, h);
   const decalTexture = new THREE.CanvasTexture(cnv);
   const decalMaterial = new THREE.MeshStandardMaterial({
@@ -553,13 +576,14 @@ function paintSignatures(ctx, g, col, tx, ty, s){
     const ar = sigImg.width/sigImg.height; let w=bw, h=w/ar; if(h>bh){ h=bh; w=h*ar; }
     const x = ob.x + (ob.w-w)/2, y = ob.y + (ob.h-h)/2;
     const cx = ob.x + ob.w/2, cy = ob.y + ob.h/2;
-    const dark = luminance(col.hex) < 0.4;
-    // Colored ink (signer's own color) on light cloth; white ink on dark cloth.
-    const src = recolorInk(sigImg, inkHexFor(col));
+    const ink = inkHexFor(col);
+    // null = original art (multiply on light cloth drops the white ground);
+    // a chosen/white ink = clean recolor on transparent (source-over).
+    const src = ink ? recolorInk(sigImg, ink) : sigImg;
     ctx.save();
     ctx.translate(tx, ty); ctx.scale(s, s);
     if (ob.rotation) { ctx.translate(cx, cy); ctx.rotate(ob.rotation * Math.PI / 180); ctx.translate(-cx, -cy); }
-    if (!dark) ctx.globalCompositeOperation = 'multiply';
+    if (!ink) ctx.globalCompositeOperation = 'multiply';
     if (ob.curve) {
       // Cap crowns are domed, not flat — a plain rectangle overlay reads as
       // a pasted sticker instead of a real DTF/embroidery print that would
@@ -617,8 +641,8 @@ function garmentMask(photoImg, w, h, key){
 function paintAllover(ctx, g, col, photoImg, tx, ty, s){
   if (!(sigImg && sigImg.width) || !photoImg) return;
   const W = ctx.canvas.width, H = ctx.canvas.height;
-  const dark = luminance(col.hex) < 0.4;
-  const src = recolorInk(sigImg, inkHexFor(col));   // signer color on light cloth, white on dark
+  const ink = inkHexFor(col);
+  const src = ink ? recolorInk(sigImg, ink) : sigImg;   // null = original art
   const tileW = Math.round(W * (g.alloverScale || 0.20));
   const tileH = Math.round(tileW * (src.height / src.width));
   const stepX = tileW + tileW*0.35, stepY = tileH + tileH*1.6;   // brick spacing
@@ -633,7 +657,7 @@ function paintAllover(ctx, g, col, photoImg, tx, ty, s){
   lx.globalAlpha = 1; lx.globalCompositeOperation = 'destination-in';
   lx.drawImage(garmentMask(photoImg, W, H, col.photo), 0, 0);     // clip to shirt
   ctx.save(); ctx.translate(tx, ty); ctx.scale(s, s);
-  if (!dark) ctx.globalCompositeOperation = 'multiply';           // tone-on-tone
+  if (!ink) ctx.globalCompositeOperation = 'multiply';            // original art: tone-on-tone
   ctx.drawImage(layer, 0, 0); ctx.restore();
 }
 
diff --git a/server.js b/server.js
index 358abce..2b604ee 100644
--- a/server.js
+++ b/server.js
@@ -1062,8 +1062,11 @@ ${paid ? `<div class="ok">✓</div><h1>Order confirmed</h1>
                     const g = (draftTpl.garments || []).find(x => x.id === it.garment);
                     const c = g && (g.colors || []).find(x => x.id === it.color);
                     const darkCloth = !!(c && wearLuminance(c.hex) < 0.4);
-                    const ink = safeInkHex(it.inkColor) || (darkCloth ? '#ffffff' : inkColorHexFor(it.qid));
-                    designUrl = await recolorSignatureInk(designUrl, ink);
+                    // Recolor ONLY when there's a reason: the buyer picked a color,
+                    // or the cloth is dark (a dark signature would vanish → white).
+                    // Otherwise keep the ORIGINAL art in its true source colors.
+                    const ink = safeInkHex(it.inkColor) || (darkCloth ? '#ffffff' : null);
+                    if (ink) designUrl = await recolorSignatureInk(designUrl, ink);
                   }
                   const draft = { draftedAt: new Date().toISOString(), orderId: compositeId, status: 'DRAFT_UNSENT', provider,
                     recipient_email: order.email, recipient: order.recipient, garment: it.garment, color: it.color, size: it.size,

← 68dd99d wear: full Signature Edit campaign hero — Lincoln seated in  ·  back to CelebritySignatures  ·  wear: raise crew-socks price $22 → $28 to clear cost/markup 6adf104 →