[object Object]

← back to CelebritySignatures

wear: fix dark-garment preview + map Printify blueprints/variants (TK-10286)

ba30f903f3af2e24ee179092f67be86cd0c41f0c · 2026-09-08 09:32:15 -0700 · Steve Abrams

Still fully gated off (WEAR_SALES_LIVE unset, checkout returns comingSoon).

1. public/wear.html: the left-chest signature preview used canvas 'multiply'
   blend unconditionally — correct for light garments (dark ink darkens light
   cloth) but multiply can only ever darken, so a dark garment rendered the
   signature completely invisible. Added a luminance check: dark colorways now
   recolor the signature to a light ink and composite normally instead.
2. data/wear-templates.json: polo colorways restricted to white/black (Steve's
   call, 2026-09-08) — the mapped JERZEES 443M Piqué Polo blueprint only offers
   6 colors, no sand/sky equivalent; black needed fix #1 above to render.
3. Filled in real Printify blueprint/print-provider/variant mappings for both
   garments via read-only catalog GETs (no account write): tee = Bella+Canvas
   3001 (blueprint 12), polo = JERZEES 443M Piqué (blueprint 1402), both DTG
   via print provider 99 (Printify Choice). Shop id 496929 confirmed by Steve
   (Designer Wallcoverings and Fabrics — the new products.write-scoped token
   resolves to a different shop id than the old token's discovery, corrected).
4. New scripts/printify-cost-check.mjs (not yet run) — creates one throwaway
   unpublished draft product per garment to read real per-unit cost, deletes
   it immediately, verifies the delete with a follow-up GET. Blocked from
   autonomous execution by the harness's own safety classifier (external
   account write); handed to Steve to run directly.

Verified: node --check clean, wear-templates.json valid JSON, local E2E shows
2955 signatures, polo colors=[white,black], tee/polo variant maps populated
(20 + 10 entries), checkout still returns comingSoon:true for every request.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmxcNKtFg77wP47uZq5Zsm

Files touched

Diff

commit ba30f903f3af2e24ee179092f67be86cd0c41f0c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 8 09:32:15 2026 -0700

    wear: fix dark-garment preview + map Printify blueprints/variants (TK-10286)
    
    Still fully gated off (WEAR_SALES_LIVE unset, checkout returns comingSoon).
    
    1. public/wear.html: the left-chest signature preview used canvas 'multiply'
       blend unconditionally — correct for light garments (dark ink darkens light
       cloth) but multiply can only ever darken, so a dark garment rendered the
       signature completely invisible. Added a luminance check: dark colorways now
       recolor the signature to a light ink and composite normally instead.
    2. data/wear-templates.json: polo colorways restricted to white/black (Steve's
       call, 2026-09-08) — the mapped JERZEES 443M Piqué Polo blueprint only offers
       6 colors, no sand/sky equivalent; black needed fix #1 above to render.
    3. Filled in real Printify blueprint/print-provider/variant mappings for both
       garments via read-only catalog GETs (no account write): tee = Bella+Canvas
       3001 (blueprint 12), polo = JERZEES 443M Piqué (blueprint 1402), both DTG
       via print provider 99 (Printify Choice). Shop id 496929 confirmed by Steve
       (Designer Wallcoverings and Fabrics — the new products.write-scoped token
       resolves to a different shop id than the old token's discovery, corrected).
    4. New scripts/printify-cost-check.mjs (not yet run) — creates one throwaway
       unpublished draft product per garment to read real per-unit cost, deletes
       it immediately, verifies the delete with a follow-up GET. Blocked from
       autonomous execution by the harness's own safety classifier (external
       account write); handed to Steve to run directly.
    
    Verified: node --check clean, wear-templates.json valid JSON, local E2E shows
    2955 signatures, polo colors=[white,black], tee/polo variant maps populated
    (20 + 10 entries), checkout still returns comingSoon:true for every request.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AmxcNKtFg77wP47uZq5Zsm
---
 data/wear-templates.json        | 47 +++++++++++++---------------
 public/wear.html                | 33 ++++++++++++++++++--
 scripts/printify-cost-check.mjs | 68 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 120 insertions(+), 28 deletions(-)

diff --git a/data/wear-templates.json b/data/wear-templates.json
index fe2b86c..a375c86 100644
--- a/data/wear-templates.json
+++ b/data/wear-templates.json
@@ -65,11 +65,16 @@
         "variants": {}
       },
       "printify": {
-        "_note": "Printify dynamic-order mapping (fill after PRINTIFY_API_TOKEN lands — run scripts/fetch-printify-catalog.mjs). blueprintId + printProviderId select the blank garment; variants maps our color/size key to its variant_id. The paid order's signature image is supplied per order with left-chest positioning. Empty mappings fail loud and keep checkout disabled.",
-        "blueprintId": null,
-        "printProviderId": null,
+        "_note": "Bella+Canvas 3001 Unisex Jersey Short-Sleeve Tee, print provider 99 (Printify Choice), DTG. Mapped 2026-09-08 via read-only catalog GETs (no account write). printPosition is still a GEOMETRIC ESTIMATE, unverified against a real Printify mockup — do NOT go live before checking it renders correctly on an actual print preview.",
+        "blueprintId": 12,
+        "printProviderId": 99,
         "printPosition": { "placeholder": "front", "scale": 0.25, "x": 0.32, "y": 0.3 },
-        "variants": {}
+        "variants": {
+          "white/S": 18540, "white/M": 18541, "white/L": 18542, "white/XL": 18543, "white/2XL": 18544,
+          "heather/S": 18076, "heather/M": 18077, "heather/L": 18078, "heather/XL": 18079, "heather/2XL": 18080,
+          "sand/S": 67513, "sand/M": 67514, "sand/L": 67515, "sand/XL": 67516, "sand/2XL": 67517,
+          "sky/S": 18084, "sky/M": 18085, "sky/L": 18086, "sky/XL": 18087, "sky/2XL": 18088
+        }
       },
       "sizeUpchargeUsd": {
         "2XL": 4
@@ -117,37 +122,29 @@
           "ink": "#1a1a1a"
         },
         {
-          "id": "heather",
-          "label": "Heather Gray",
-          "hex": "#d8d5cd",
-          "ink": "#1a1a1a"
-        },
-        {
-          "id": "sand",
-          "label": "Sand",
-          "hex": "#e7ddca",
-          "ink": "#1a1a1a"
-        },
-        {
-          "id": "sky",
-          "label": "Sky",
-          "hex": "#dbe6ef",
-          "ink": "#16324a"
+          "id": "black",
+          "label": "Black",
+          "hex": "#181818",
+          "ink": "#f5f3ee"
         }
       ],
+      "_note_colors": "Polo colorways restricted to white/black (Steve, 2026-09-08) — the JERZEES 443M Piqué Polo blueprint (Printify bp 1402, provider 99) only offers White/Athletic Heather/Black/Navy/Royal/True Red, no sand or sky-blue equivalent. Black needed a renderer fix (public/wear.html draw()) since the preview's 'multiply' blend can only darken — dark grounds now recolor the signature to a light ink instead.",
       "printful": {
         "_note": "Printful mapping (fill after PRINTFUL_API_KEY lands — run scripts/fetch-printful-catalog.mjs). productId = Printful catalog product; variants maps our 'color/size' key -> Printful catalog variant_id. Left EMPTY on purpose so scripts/submit-pod-draft.mjs FAILS LOUD (never submits an unmapped garment).",
-        "suggestedProduct": "Bella+Canvas 3001 Unisex Jersey Short-Sleeve Tee (or a polo equiv, e.g. Cotton Heritage / Adidas)",
+        "suggestedProduct": "JERZEES 443M Pique Polo (or equivalent)",
         "productId": null,
         "placementType": "front",
         "variants": {}
       },
       "printify": {
-        "_note": "Printify dynamic-order mapping (fill after PRINTIFY_API_TOKEN lands — run scripts/fetch-printify-catalog.mjs). blueprintId + printProviderId select the blank garment; variants maps our color/size key to its variant_id. The paid order's signature image is supplied per order with left-chest positioning. Empty mappings fail loud and keep checkout disabled.",
-        "blueprintId": null,
-        "printProviderId": null,
+        "_note": "JERZEES 443M Piqué Polo, print provider 99 (Printify Choice), DTG. Mapped 2026-09-08 via read-only catalog GETs (no account write). printPosition is still a GEOMETRIC ESTIMATE, unverified against a real Printify mockup — do NOT go live before checking it renders correctly on an actual print preview.",
+        "blueprintId": 1402,
+        "printProviderId": 99,
         "printPosition": { "placeholder": "front", "scale": 0.25, "x": 0.32, "y": 0.3 },
-        "variants": {}
+        "variants": {
+          "white/S": 104647, "white/M": 104648, "white/L": 104649, "white/XL": 104650, "white/2XL": 104651,
+          "black/S": 104623, "black/M": 104624, "black/L": 104625, "black/XL": 104626, "black/2XL": 104627
+        }
       },
       "sizeUpchargeUsd": {
         "2XL": 4
diff --git a/public/wear.html b/public/wear.html
index b7a774a..64fd3c0 100644
--- a/public/wear.html
+++ b/public/wear.html
@@ -188,6 +188,25 @@ function shade(hex, amt){ // amt <0 darken, >0 lighten
   const f = x => Math.max(0,Math.min(255, Math.round(x + amt*255)));
   return '#'+[f(r),f(g),f(b)].map(x=>x.toString(16).padStart(2,'0')).join('');
 }
+// Perceived luminance (0=black, 1=white) — decides whether 'multiply' (needs a
+// light ground) or a recolored-to-light rendition (for a dark ground) is used.
+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;
+}
+// Most signature source art is a dark stroke on a transparent ground — great
+// for 'multiply' on light cloth, invisible via 'multiply' on dark cloth (multiply
+// can only ever darken). For a dark garment, recolor the signature's opaque
+// pixels to a light ink and composite normally instead.
+function recolorToLight(img){
+  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 = '#f5f3ee';
+  octx.fillRect(0, 0, off.width, off.height);
+  return off;
+}
 
 function draw(){
   const cv = document.getElementById('stage'), ctx = cv.getContext('2d');
@@ -207,13 +226,21 @@ function draw(){
   const pb = g.pocketBox;
   ctx.strokeStyle=shade(col.hex,-0.10); ctx.setLineDash([4,4]); ctx.lineWidth=1;
   ctx.strokeRect(pb.x,pb.y,pb.w,pb.h); ctx.setLineDash([]);
-  // signature into the pocket box, contain-fit, 'multiply' so ink shows on light cloth
+  // signature into the pocket box, contain-fit. Light cloth: 'multiply' so the
+  // dark ink shows through. Dark cloth: multiply would render invisible (it can
+  // only darken), so recolor the signature to a light ink and composite normally.
   if (sigImg && sigImg.width){
     const pad=8, bw=pb.w-2*pad, bh=pb.h-2*pad;
     const ar = sigImg.width/sigImg.height; let w=bw, h=w/ar; if(h>bh){ h=bh; w=h*ar; }
     const x = pb.x + (pb.w-w)/2, y = pb.y + (pb.h-h)/2;
-    ctx.save(); ctx.globalCompositeOperation='multiply';
-    ctx.drawImage(sigImg, x, y, w, h); ctx.restore();
+    ctx.save();
+    if (luminance(col.hex) < 0.4){
+      ctx.drawImage(recolorToLight(sigImg), x, y, w, h);
+    } else {
+      ctx.globalCompositeOperation='multiply';
+      ctx.drawImage(sigImg, x, y, w, h);
+    }
+    ctx.restore();
   } else {
     ctx.fillStyle=shade(col.hex,-0.2); ctx.font='italic 16px Georgia'; ctx.textAlign='center';
     ctx.fillText(state.sig.full_name.split(' ').pop(), pb.x+pb.w/2, pb.y+pb.h/2);
diff --git a/scripts/printify-cost-check.mjs b/scripts/printify-cost-check.mjs
new file mode 100644
index 0000000..24f03ad
--- /dev/null
+++ b/scripts/printify-cost-check.mjs
@@ -0,0 +1,68 @@
+#!/usr/bin/env node
+// ONE-OFF, throwaway cost-discovery helper for TK-10286. Creates ONE minimal,
+// single-variant, unpublished (visible:false) draft product per garment on
+// shop 496929 (Designer Wallcoverings and Fabrics) purely to read Printify's
+// real per-unit cost, then IMMEDIATELY deletes it and verifies the delete
+// with a follow-up GET (expects 404). Logs created→deleted id pairs so
+// nothing is left dangling. Creates NOTHING else — no order, no charge.
+import { readFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { join } from 'node:path';
+
+const ROOT = fileURLToPath(new URL('..', import.meta.url));
+const API = 'https://api.printify.com/v1';
+const SHOP_ID = 496929; // Designer Wallcoverings and Fabrics — confirmed by Steve 2026-09-08
+
+function envVal(name) {
+  if (process.env[name]) return process.env[name];
+  for (const p of [join(ROOT, '.env'), join(ROOT, '..', 'secrets-manager', '.env')]) {
+    try { const m = readFileSync(p, 'utf8').match(new RegExp('^' + name + '=(.+)$', 'm')); if (m) return m[1].trim(); } catch {}
+  }
+  return null;
+}
+const token = envVal('PRINTIFY_API_TOKEN');
+if (!token) { console.error('PRINTIFY_API_TOKEN not found'); process.exit(1); }
+
+async function req(method, path, body) {
+  const r = await fetch(API + path, {
+    method, headers: { Authorization: `Bearer ${token}`, 'User-Agent': 'CelebritySignatures/1.0', 'Content-Type': 'application/json' },
+    body: body ? JSON.stringify(body) : undefined,
+  });
+  const j = await r.json().catch(() => ({}));
+  return { status: r.status, ok: r.ok, body: j };
+}
+
+// blueprint_id, print_provider_id, one representative variant id (White/M),
+// matching what's now mapped in data/wear-templates.json (bp12/provider99 for
+// the tee, bp1402/provider99 for the polo — both DTG, Printify Choice).
+const CANDIDATES = [
+  { garment: 'classic-tee', blueprintId: 12, printProviderId: 99, variantId: 18541, label: 'Bella+Canvas 3001 White/M (DTG, provider 99 Printify Choice)' },
+  { garment: 'polo', blueprintId: 1402, printProviderId: 99, variantId: 104648, label: 'JERZEES 443M Piqué Polo White/M (DTG, provider 99 Printify Choice)' },
+];
+
+async function main() {
+  for (const c of CANDIDATES) {
+    console.log(`\n— ${c.garment}: ${c.label} —`);
+    const created = await req('POST', `/shops/${SHOP_ID}/products.json`, {
+      title: `TEMP-COST-CHECK-DELETE-ME (TK-10286, ${c.garment})`,
+      description: 'Throwaway — created only to read Printify cost, deleted immediately by this script.',
+      blueprint_id: c.blueprintId,
+      print_provider_id: c.printProviderId,
+      variants: [{ id: c.variantId, price: 3400, is_enabled: true }],
+      visible: false,
+    });
+    if (!created.ok) { console.log(`  CREATE FAILED (${created.status}):`, JSON.stringify(created.body).slice(0, 500)); continue; }
+    const productId = created.body.id;
+    const variant = (created.body.variants || [])[0] || {};
+    console.log(`  created product id=${productId}`);
+    console.log(`  cost: $${((variant.cost || 0) / 100).toFixed(2)}  suggested retail: $${((variant.price || 0) / 100).toFixed(2)}`);
+
+    const del = await req('DELETE', `/shops/${SHOP_ID}/products/${productId}.json`);
+    console.log(`  delete: ${del.ok ? 'OK' : 'FAILED ' + del.status}`);
+
+    const verify = await req('GET', `/shops/${SHOP_ID}/products/${productId}.json`);
+    console.log(`  verify-deleted (expect 404): got ${verify.status} — ${verify.status === 404 ? 'CONFIRMED GONE' : 'STILL PRESENT — MANUAL CLEANUP NEEDED'}`);
+    console.log(`  LOG: created=${productId} deleted=${del.ok} verified_gone=${verify.status === 404}`);
+  }
+}
+main().catch(e => { console.error(e.message); process.exit(1); });

← f197cfc wear/POD: fix 4 correctness bugs from codex's 2026-09-04 rev  ·  back to CelebritySignatures  ·  wear: extend printify-cost-check.mjs to also verify left-che 2e8f61b →