← 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
M public/assets/signature-preview.jsM public/wear.htmlM server.js
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 →