[object Object]

← back to CelebritySignatures

wear: drop the cap, real Printify product photos instead of flat SVG (TK-10286)

108381cf33df1b9be57be67472b7bb9041a3ef81 · 2026-09-08 14:15:27 -0700 · Steve Abrams

Steve: "I DONT LIKE THE HATS AND THE TSHIRTS NEED TO LOK REAL FROM PRINTERS
TEMPLATES". Still fully gated off (WEAR_SALES_LIVE unset, checkout returns
comingSoon).

1. Removed the cap/Classic Dad Cap garment entirely (data + the SVG geometry
   that only existed for it).

2. Replaced the hand-drawn flat-vector garment illustration (Path2D body
   shape + accent paths) with REAL Printify product photography. Captured 7
   real blank product photos (one per garment x color: tee white/heather/
   sand/sky, polo white/black, pocket-tee white) via a throwaway draft
   product with an invisible 1x1-pixel transparent print (so the photo comes
   back completely blank), fetched the real mockup image, deleted the
   product — same proven create/fetch/delete/verify-404 pattern used all
   session. Saved to public/assets/garments/*.jpg (1200x1200, real Printify
   photography).

3. New wear-templates.json fields per garment: photoWidth/photoHeight (1200)
   and overlayBox (the pixel rectangle where the client composites the
   signature onto the real photo) — measured directly against a real
   Printify mockup WITH an actual signature printed at that garment's own
   printify.printPosition, so the on-site preview and the real order use the
   same position. pocket-tee's overlayBox was measured straight off the
   blank photo itself (the real stitched pocket is visible with no print at
   all). Each color object now carries its own `photo` path instead of just
   a flat `hex` (hex/ink kept — still used to decide the multiply-vs-recolor
   blend mode).

4. public/wear.html draw() rewritten: loads and caches the current color's
   real photo, draws it as the canvas background, then composites the
   signature into the measured overlayBox with the same contain-fit +
   multiply/recolor-to-light logic as before (unchanged math, real photo
   underneath instead of a flat color fill). Removed the now-dead shade()
   helper and all Path2D/accent/pocketAccent/pocketHem drawing code.

5. FOUND AND FIXED a real bug while visually verifying: switching garments
   via the chip only re-resolved state.color when the color id was NOT found
   on the new garment — when both garments shared a color id (e.g. "white"),
   it silently kept the OLD garment's color object, which under the old
   flat-hex rendering was invisible (same hex value) but under real photos
   showed the WRONG garment's photo entirely. Fixed by always re-resolving
   to the new garment's own matching color object.

Verified: node --check clean, JSON valid, local E2E shows 3 garments
(cap gone), checkout still comingSoon:true. Playwright screenshots of the
real on-site canvas (tee white/heather, polo white/black, pocket-tee) all
show real photographic garments with the signature correctly composited —
0 console errors.

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

Files touched

Diff

commit 108381cf33df1b9be57be67472b7bb9041a3ef81
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 8 14:15:27 2026 -0700

    wear: drop the cap, real Printify product photos instead of flat SVG (TK-10286)
    
    Steve: "I DONT LIKE THE HATS AND THE TSHIRTS NEED TO LOK REAL FROM PRINTERS
    TEMPLATES". Still fully gated off (WEAR_SALES_LIVE unset, checkout returns
    comingSoon).
    
    1. Removed the cap/Classic Dad Cap garment entirely (data + the SVG geometry
       that only existed for it).
    
    2. Replaced the hand-drawn flat-vector garment illustration (Path2D body
       shape + accent paths) with REAL Printify product photography. Captured 7
       real blank product photos (one per garment x color: tee white/heather/
       sand/sky, polo white/black, pocket-tee white) via a throwaway draft
       product with an invisible 1x1-pixel transparent print (so the photo comes
       back completely blank), fetched the real mockup image, deleted the
       product — same proven create/fetch/delete/verify-404 pattern used all
       session. Saved to public/assets/garments/*.jpg (1200x1200, real Printify
       photography).
    
    3. New wear-templates.json fields per garment: photoWidth/photoHeight (1200)
       and overlayBox (the pixel rectangle where the client composites the
       signature onto the real photo) — measured directly against a real
       Printify mockup WITH an actual signature printed at that garment's own
       printify.printPosition, so the on-site preview and the real order use the
       same position. pocket-tee's overlayBox was measured straight off the
       blank photo itself (the real stitched pocket is visible with no print at
       all). Each color object now carries its own `photo` path instead of just
       a flat `hex` (hex/ink kept — still used to decide the multiply-vs-recolor
       blend mode).
    
    4. public/wear.html draw() rewritten: loads and caches the current color's
       real photo, draws it as the canvas background, then composites the
       signature into the measured overlayBox with the same contain-fit +
       multiply/recolor-to-light logic as before (unchanged math, real photo
       underneath instead of a flat color fill). Removed the now-dead shade()
       helper and all Path2D/accent/pocketAccent/pocketHem drawing code.
    
    5. FOUND AND FIXED a real bug while visually verifying: switching garments
       via the chip only re-resolved state.color when the color id was NOT found
       on the new garment — when both garments shared a color id (e.g. "white"),
       it silently kept the OLD garment's color object, which under the old
       flat-hex rendering was invisible (same hex value) but under real photos
       showed the WRONG garment's photo entirely. Fixed by always re-resolving
       to the new garment's own matching color object.
    
    Verified: node --check clean, JSON valid, local E2E shows 3 garments
    (cap gone), checkout still comingSoon:true. Playwright screenshots of the
    real on-site canvas (tee white/heather, polo white/black, pocket-tee) all
    show real photographic garments with the signature correctly composited —
    0 console errors.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AmxcNKtFg77wP47uZq5Zsm
---
 data/wear-templates.json                       | 185 ++++++-------------------
 public/assets/garments/classic-tee-heather.jpg | Bin 0 -> 158780 bytes
 public/assets/garments/classic-tee-sand.jpg    | Bin 0 -> 59035 bytes
 public/assets/garments/classic-tee-sky.jpg     | Bin 0 -> 55856 bytes
 public/assets/garments/classic-tee-white.jpg   | Bin 0 -> 42109 bytes
 public/assets/garments/pocket-tee-white.jpg    | Bin 0 -> 31263 bytes
 public/assets/garments/polo-black.jpg          | Bin 0 -> 40295 bytes
 public/assets/garments/polo-white.jpg          | Bin 0 -> 33325 bytes
 public/wear.html                               |  93 ++++++-------
 9 files changed, 86 insertions(+), 192 deletions(-)

diff --git a/data/wear-templates.json b/data/wear-templates.json
index 2b2de98..e1a42e6 100644
--- a/data/wear-templates.json
+++ b/data/wear-templates.json
@@ -1,5 +1,5 @@
 {
-  "_note": "Garment TEMPLATES for the /wear left-chest signature apparel feature. Each garment is pure geometry: an SVG viewBox + body path (drawn on canvas via new Path2D), optional accent paths (collar/placket/pocket), a set of LIGHT colorways (dark garments deferred — a black signature renders via canvas 'multiply', which needs a light ground; the polo's black exception got a separate renderer fix, see public/wear.html), and the pocketBox: the left-chest placement rectangle. CORRECTED 2026-09-08 — pocketBox is viewer-RIGHT (wearer's LEFT chest, the industry-standard 'left chest' placement), matching the real Printify print position verified that day; it was previously viewer-left, which did not match what actually printed.",
+  "_note": "Garment TEMPLATES for the /wear left-chest signature apparel feature. REBUILT 2026-09-08 to use REAL Printify product photography instead of a flat hand-drawn SVG illustration (Steve: 'the tshirts need to look real from printers templates'). Each color has a real blank product photo (captured via a throwaway draft product with an invisible 1x1 print, deleted immediately — public/assets/garments/*.jpg) at photoWidth x photoHeight pixels; overlayBox is the pixel rectangle (in that photo's own pixel space) where the client-side canvas composites the selected signature on top, measured directly against a real Printify mockup with an actual signature printed at the SAME printPosition used for real orders. The cap/hat garment was dropped per Steve's feedback the same session.",
   "placement": "left_chest",
   "printAreaInches": {
     "w": 4,
@@ -17,44 +17,43 @@
         "XL",
         "2XL"
       ],
-      "viewBox": [
-        0,
-        0,
-        400,
-        480
-      ],
-      "bodyPath": "M150 62 L120 72 L58 112 L40 178 L112 168 L112 430 L288 430 L288 168 L360 178 L342 112 L280 72 L250 62 Q200 104 150 62 Z",
-      "accents": [],
-      "pocketBox": {
-        "x": 176,
-        "y": 150,
-        "w": 96,
-        "h": 78
+      "photoWidth": 1200,
+      "photoHeight": 1200,
+      "overlayBox": {
+        "x": 590,
+        "y": 390,
+        "w": 200,
+        "h": 80
       },
+      "_note_overlayBox": "Measured 2026-09-08 against a real Printify mockup (throwaway draft product, deleted) printed at this garment's own printify.printPosition (x:0.7/y:0.1/scale:0.16) — the core signature ink landed at px x:651-717/y:421-429 on the 1200x1200 photo; box padded generously beyond that so longer signature names still contain-fit inside it.",
       "colors": [
         {
           "id": "white",
           "label": "White",
           "hex": "#f6f4ef",
-          "ink": "#1a1a1a"
+          "ink": "#1a1a1a",
+          "photo": "/assets/garments/classic-tee-white.jpg"
         },
         {
           "id": "heather",
           "label": "Heather Gray",
           "hex": "#d8d5cd",
-          "ink": "#1a1a1a"
+          "ink": "#1a1a1a",
+          "photo": "/assets/garments/classic-tee-heather.jpg"
         },
         {
           "id": "sand",
           "label": "Sand",
           "hex": "#e7ddca",
-          "ink": "#1a1a1a"
+          "ink": "#1a1a1a",
+          "photo": "/assets/garments/classic-tee-sand.jpg"
         },
         {
           "id": "sky",
           "label": "Sky",
           "hex": "#dbe6ef",
-          "ink": "#16324a"
+          "ink": "#16324a",
+          "photo": "/assets/garments/classic-tee-sky.jpg"
         }
       ],
       "printful": {
@@ -71,7 +70,7 @@
         "printPosition": { "placeholder": "front", "scale": 0.16, "x": 0.7, "y": 0.1 },
         "extraPrintAreas": [
           {
-            "_note": "Fabric tag at the back collar (Steve, 2026-09-08) — Printify's dedicated 'neck' placeholder, only available on this blueprint (checked: polo/pocket-tee/cap blueprints have no neck-label placeholder). Static asset (not per-order design), same on every order. Verified 2026-09-08 via 4 real throwaway draft products — y:0.0 clips into the collar-binding curve and distorts the text; y:0.08 is the highest clean/legible position.",
+            "_note": "Fabric tag at the back collar (Steve, 2026-09-08) — Printify's dedicated 'neck' placeholder, only available on this blueprint (checked: polo/pocket-tee blueprints have no neck-label placeholder). Static asset (not per-order design), same on every order. Verified 2026-09-08 via 4 real throwaway draft products — y:0.0 clips into the collar-binding curve and distorts the text; y:0.08 is the highest clean/legible position.",
             "placeholder": "neck",
             "imageUrl": "/assets/wear-tag-celebrity-signatures.png",
             "x": 0.5, "y": 0.08, "scale": 0.8
@@ -99,41 +98,29 @@
         "XL",
         "2XL"
       ],
-      "viewBox": [
-        0,
-        0,
-        400,
-        480
-      ],
-      "bodyPath": "M156 66 L120 72 L58 112 L40 178 L112 168 L112 430 L288 430 L288 168 L360 178 L342 112 L280 72 L244 66 L200 96 Z",
-      "accents": [
-        {
-          "d": "M156 66 L200 120 L244 66 L226 66 L200 96 L174 66 Z",
-          "fillShade": -0.08
-        },
-        {
-          "d": "M193 96 L207 96 L205 150 L195 150 Z",
-          "fillShade": -0.05
-        }
-      ],
-      "pocketBox": {
-        "x": 180,
-        "y": 158,
-        "w": 92,
-        "h": 74
+      "photoWidth": 1200,
+      "photoHeight": 1200,
+      "overlayBox": {
+        "x": 640,
+        "y": 325,
+        "w": 180,
+        "h": 70
       },
+      "_note_overlayBox": "Measured 2026-09-08 against a real Printify mockup (throwaway draft product, deleted) printed at this garment's own printify.printPosition (the dedicated left_chest placeholder, x:0.5/y:0.5/scale:0.85) on the 1200x1200 photo.",
       "colors": [
         {
           "id": "white",
           "label": "White",
           "hex": "#f6f4ef",
-          "ink": "#1a1a1a"
+          "ink": "#1a1a1a",
+          "photo": "/assets/garments/polo-white.jpg"
         },
         {
           "id": "black",
           "label": "Black",
           "hex": "#181818",
-          "ink": "#f5f3ee"
+          "ink": "#f5f3ee",
+          "photo": "/assets/garments/polo-black.jpg"
         }
       ],
       "_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.",
@@ -145,11 +132,10 @@
         "variants": {}
       },
       "printify": {
-        "_note": "JERZEES 443M Piqué Polo, print provider 99 (Printify Choice), DTG. Mapped 2026-09-08 via read-only catalog GETs. printPosition VERIFIED 2026-09-08 against real Printify mockups (multiple throwaway draft products, each deleted) — iterated through x:0.32/y:0.3/scale:0.25 (wrong, dead-center) -> x:0.7/y:0.15/scale:0.14 -> x:0.7/y:0.13/scale:0.22 (too big) to the final x:0.7/y:0.1/scale:0.16, a standard small left-chest logo size sitting just above pocket-top height. Renders correctly on both white and black. KNOWN ISSUE: on the black colorway, the source signature art (dark ink) prints as low-contrast dark-gray-on-black — Printify does not auto-recolor art for dark garments. Needs a server-side light-ink recolor of design_image_url before order submission for dark colors, or dropping the black colorway, before go-live.",
+        "_note": "JERZEES 443M Piqué Polo, print provider 99 (Printify Choice), DTG. Mapped 2026-09-08 via read-only catalog GETs. printPosition VERIFIED 2026-09-08 against real Printify mockups (multiple throwaway draft products, each deleted) — iterated through x:0.32/y:0.3/scale:0.25 (wrong, dead-center) -> x:0.7/y:0.15/scale:0.14 -> x:0.7/y:0.13/scale:0.22 (too big) -> x:0.7/y:0.1/scale:0.16 (still overlapping the placket even at y:0.0) to the final DEDICATED 'left_chest' placeholder, x:0.5/y:0.5/scale:0.85, a properly-scoped small print area that lands at the correct height automatically. Renders correctly on both white and black. KNOWN ISSUE: on the black colorway, the source signature art (dark ink) prints as low-contrast dark-gray-on-black — Printify does not auto-recolor art for dark garments. Needs a server-side light-ink recolor of design_image_url before order submission for dark colors, or dropping the black colorway, before go-live.",
         "blueprintId": 1402,
         "printProviderId": 99,
         "printPosition": { "placeholder": "left_chest", "scale": 0.85, "x": 0.5, "y": 0.5 },
-        "_note_placeholder": "Switched off the manually-tuned 'front' full-torso area (kept overlapping the placket no matter how high y went) to Printify's own DEDICATED 'left_chest' placeholder for this blueprint — a properly-scoped small print area that lands at the correct height automatically. Verified 2026-09-08 via a real throwaway draft product.",
         "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
@@ -170,34 +156,22 @@
         "2XL"
       ],
       "_note_sizes": "No XL — Printify's own catalog has no White/XL variant for this blueprint's pocket-DTG placeholder (checked both providers, same gap). Real catalog limit, not an oversight.",
-      "viewBox": [
-        0,
-        0,
-        400,
-        480
-      ],
-      "bodyPath": "M150 62 L120 72 L58 112 L40 178 L112 168 L112 430 L288 430 L288 168 L360 178 L342 112 L280 72 L250 62 Q200 104 150 62 Z",
-      "accents": [],
-      "pocketAccent": {
-        "d": "M176 150 L272 150 L272 214 Q272 222 264 222 L184 222 Q176 222 176 214 Z",
-        "fillShade": -0.05
-      },
-      "pocketHem": {
-        "d": "M176 160 L272 160 L272 163 L176 163 Z",
-        "fillShade": -0.12
-      },
-      "pocketBox": {
-        "x": 184,
-        "y": 168,
-        "w": 80,
-        "h": 46
+      "photoWidth": 1200,
+      "photoHeight": 1200,
+      "overlayBox": {
+        "x": 700,
+        "y": 415,
+        "w": 130,
+        "h": 110
       },
+      "_note_overlayBox": "Matches the REAL physical pocket visible in the product photo itself (public/assets/garments/pocket-tee-white.jpg) — measured directly off that photo, not a separate mockup-with-signature render, since the pocket's stitched outline is visible without any print at all.",
       "colors": [
         {
           "id": "white",
           "label": "White",
           "hex": "#f6f4ef",
-          "ink": "#1a1a1a"
+          "ink": "#1a1a1a",
+          "photo": "/assets/garments/pocket-tee-white.jpg"
         }
       ],
       "_note_colors": "White-only (Steve asked for a pocket-tee variant, 2026-09-08) — Deep Heather only has S/M in Printify's catalog for this blueprint's pocket placeholder, too gappy to offer alongside white's own S/M/L/2XL gap (no XL). Signature prints directly ON the pocket (Printify's dedicated 'pocket' placeholder), not just near it.",
@@ -220,83 +194,6 @@
       "sizeUpchargeUsd": {
         "2XL": 4
       }
-    },
-    {
-      "id": "cap",
-      "label": "Classic Dad Cap",
-      "priceUsd": 28,
-      "sizes": [
-        "One size"
-      ],
-      "_note_sizes": "Caps are one-size-fits-all (adjustable strap) — no size chips shown for this garment on the storefront.",
-      "viewBox": [
-        0,
-        0,
-        400,
-        480
-      ],
-      "bodyPath": "M90 210 Q90 70 200 64 Q310 70 310 210 Q310 226 200 230 Q90 226 90 210 Z",
-      "accents": [
-        {
-          "d": "M195 68 L205 68 L207 226 L193 226 Z",
-          "fillShade": -0.06
-        }
-      ],
-      "pocketAccent": {
-        "d": "M60 214 Q40 250 90 268 Q200 288 310 268 Q360 250 340 214 Q320 232 200 236 Q80 232 60 214 Z",
-        "fillShade": -0.1
-      },
-      "_note_pocketAccent": "This is the cap's BRIM (drawn as a shaded curve below the crown), reusing the pocketAccent rendering hook — not a real pocket. Renamed at the code level would be cleaner but this keeps public/wear.html's draw() generic across all 4 garments without a one-off branch.",
-      "pocketBox": {
-        "x": 150,
-        "y": 120,
-        "w": 100,
-        "h": 55
-      },
-      "colors": [
-        {
-          "id": "white",
-          "label": "White",
-          "hex": "#f6f4ef",
-          "ink": "#1a1a1a"
-        },
-        {
-          "id": "stone",
-          "label": "Stone",
-          "hex": "#cfc9bd",
-          "ink": "#1a1a1a"
-        },
-        {
-          "id": "khaki",
-          "label": "Khaki",
-          "hex": "#c9b896",
-          "ink": "#1a1a1a"
-        },
-        {
-          "id": "lightblue",
-          "label": "Light Blue",
-          "hex": "#a9c4d9",
-          "ink": "#16324a"
-        }
-      ],
-      "_note_colors": "Light-only (White/Stone/Khaki/Light Blue), same conservative default as the original tee palette — avoids the known dark-ink-on-dark-fabric contrast problem (see polo's black colorway note) on a third garment before that gets a real fix.",
-      "printful": {
-        "_note": "Not mapped — Printify only for this garment.",
-        "suggestedProduct": "Yupoong 6245CM Classic Dad Cap",
-        "productId": null,
-        "placementType": "front",
-        "variants": {}
-      },
-      "printify": {
-        "_note": "Yupoong 6245CM Classic Dad Cap, print provider 99 (Printify Choice), DTF (not embroidery — a cursive signature's thin strokes would blob out in embroidery; this blueprint offers both, DTF was the deliberate choice). Mapped + VERIFIED 2026-09-08 via a real throwaway draft product — front_dtf placeholder, x:0.5/y:0.5/scale:0.7 centers cleanly on the front panel. Cost $16.12 at time of verification.",
-        "blueprintId": 1447,
-        "printProviderId": 99,
-        "printPosition": { "placeholder": "front_dtf", "scale": 0.7, "x": 0.5, "y": 0.5 },
-        "variants": {
-          "white/One size": 105381, "stone/One size": 105380, "khaki/One size": 105375, "lightblue/One size": 105376
-        }
-      },
-      "sizeUpchargeUsd": {}
     }
   ]
 }
diff --git a/public/assets/garments/classic-tee-heather.jpg b/public/assets/garments/classic-tee-heather.jpg
new file mode 100644
index 0000000..0b71e97
Binary files /dev/null and b/public/assets/garments/classic-tee-heather.jpg differ
diff --git a/public/assets/garments/classic-tee-sand.jpg b/public/assets/garments/classic-tee-sand.jpg
new file mode 100644
index 0000000..93e9691
Binary files /dev/null and b/public/assets/garments/classic-tee-sand.jpg differ
diff --git a/public/assets/garments/classic-tee-sky.jpg b/public/assets/garments/classic-tee-sky.jpg
new file mode 100644
index 0000000..a57d1c7
Binary files /dev/null and b/public/assets/garments/classic-tee-sky.jpg differ
diff --git a/public/assets/garments/classic-tee-white.jpg b/public/assets/garments/classic-tee-white.jpg
new file mode 100644
index 0000000..57adc26
Binary files /dev/null and b/public/assets/garments/classic-tee-white.jpg differ
diff --git a/public/assets/garments/pocket-tee-white.jpg b/public/assets/garments/pocket-tee-white.jpg
new file mode 100644
index 0000000..957995f
Binary files /dev/null and b/public/assets/garments/pocket-tee-white.jpg differ
diff --git a/public/assets/garments/polo-black.jpg b/public/assets/garments/polo-black.jpg
new file mode 100644
index 0000000..661c400
Binary files /dev/null and b/public/assets/garments/polo-black.jpg differ
diff --git a/public/assets/garments/polo-white.jpg b/public/assets/garments/polo-white.jpg
new file mode 100644
index 0000000..9c622dc
Binary files /dev/null and b/public/assets/garments/polo-white.jpg differ
diff --git a/public/wear.html b/public/wear.html
index 7e2c17c..5ff92fd 100644
--- a/public/wear.html
+++ b/public/wear.html
@@ -258,7 +258,16 @@ function openProduct(qid){
 function buildOptions(){
   const G = document.getElementById('garments'), C = document.getElementById('colors'), Z = document.getElementById('sizes');
   G.innerHTML = state.data.garments.map(g=>`<span class="chip ${g.id===state.garment.id?'sel':''}" data-g="${g.id}">${g.label}</span>`).join('');
-  G.querySelectorAll('.chip').forEach(el=>el.onclick=()=>{ state.garment = state.data.garments.find(g=>g.id===el.dataset.g); if(!state.garment.colors.find(c=>c.id===state.color.id)) state.color=state.garment.colors[0]; if(!state.garment.sizes.includes(state.size)) state.size=state.garment.sizes[0]; buildOptions(); draw(); });
+  G.querySelectorAll('.chip').forEach(el=>el.onclick=()=>{
+    // Re-resolve to the NEW garment's own color object even when the id
+    // matches (e.g. both offer "white") — each garment's color now carries
+    // its own real product photo, so reusing the old garment's color object
+    // silently showed the wrong garment's photo.
+    state.garment = state.data.garments.find(g=>g.id===el.dataset.g);
+    state.color = state.garment.colors.find(c=>c.id===state.color.id) || state.garment.colors[0];
+    if(!state.garment.sizes.includes(state.size)) state.size=state.garment.sizes[0];
+    buildOptions(); draw();
+  });
   C.innerHTML = state.garment.colors.map(c=>`<span class="swatch ${c.id===state.color.id?'sel':''}" data-c="${c.id}" title="${c.label}" style="background:${c.hex}"></span>`).join('');
   C.querySelectorAll('.swatch').forEach(el=>el.onclick=()=>{ state.color = state.garment.colors.find(c=>c.id===el.dataset.c); buildOptions(); draw(); });
   Z.innerHTML = state.garment.sizes.map(z=>`<span class="chip ${z===state.size?'sel':''}" data-z="${z}">${z}</span>`).join('');
@@ -274,11 +283,6 @@ function loadSigThenDraw(){
   sigImg.src = state.sig.signature_image_url;
 }
 
-function shade(hex, amt){ // amt <0 darken, >0 lighten
-  const n = parseInt(hex.slice(1),16); let r=(n>>16)&255,g=(n>>8)&255,b=n&255;
-  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){
@@ -299,51 +303,44 @@ function recolorToLight(img){
   return off;
 }
 
+// Real garment photos are loaded once per URL and cached — switching sizes
+// (which doesn't change the photo) never re-fetches.
+const photoCache = {};
+function loadPhoto(src, cb){
+  if (photoCache[src] && photoCache[src].complete) { cb(photoCache[src]); return; }
+  const img = photoCache[src] || (photoCache[src] = new Image());
+  img.onload = () => cb(img);
+  img.onerror = () => cb(null);
+  if (!img.src) img.src = src;
+}
+
 function draw(){
   const cv = document.getElementById('stage'), ctx = cv.getContext('2d');
-  const g = state.garment, col = state.color, vb = g.viewBox;
-  cv.width = vb[2]; cv.height = vb[3];
-  ctx.clearRect(0,0,cv.width,cv.height);
-  // garment body
-  const body = new Path2D(g.bodyPath);
-  ctx.save();
-  ctx.shadowColor='rgba(0,0,0,.12)'; ctx.shadowBlur=18; ctx.shadowOffsetY=6;
-  ctx.fillStyle = col.hex; ctx.fill(body);
-  ctx.restore();
-  ctx.lineWidth=1.2; ctx.strokeStyle=shade(col.hex,-0.16); ctx.stroke(body);
-  // accents (collar/placket)
-  (g.accents||[]).forEach(a=>{ const p=new Path2D(a.d); ctx.fillStyle=shade(col.hex, a.fillShade||-0.06); ctx.fill(p); });
-  const pb = g.pocketBox;
-  if (g.pocketAccent){
-    // a real sewn-on patch pocket (pocket-tee): filled shape + a hem line, the
-    // signature prints ON it (matches Printify's dedicated 'pocket' placement).
-    ctx.fillStyle=shade(col.hex, g.pocketAccent.fillShade||-0.05); ctx.fill(new Path2D(g.pocketAccent.d));
-    ctx.strokeStyle=shade(col.hex,-0.18); ctx.lineWidth=1; ctx.stroke(new Path2D(g.pocketAccent.d));
-    if (g.pocketHem) { ctx.fillStyle=shade(col.hex, g.pocketHem.fillShade||-0.12); ctx.fill(new Path2D(g.pocketHem.d)); }
-  } else {
-    // no real pocket on this garment — just a subtle placement hint
-    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. 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();
-    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);
+  const g = state.garment, col = state.color;
+  cv.width = g.photoWidth; cv.height = g.photoHeight;
+  loadPhoto(col.photo, photoImg => {
+    ctx.clearRect(0, 0, cv.width, cv.height);
+    if (photoImg) ctx.drawImage(photoImg, 0, 0, cv.width, cv.height);
+    // signature into the overlay box, contain-fit, against the REAL product
+    // photo. Light cloth: 'multiply' so the dark ink reads naturally against
+    // the photo (fabric shading still shows through, same as a real print).
+    // Dark cloth: multiply would render invisible (it can only darken), so
+    // recolor the signature to a light ink and composite normally instead.
+    const ob = g.overlayBox;
+    if (sigImg && sigImg.width){
+      const pad = ob.w * 0.08, bw = ob.w - 2*pad, bh = ob.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 = ob.x + (ob.w-w)/2, y = ob.y + (ob.h-h)/2;
+      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();
     }
-    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);
-  }
+  });
 }
 
 document.getElementById('sort').onchange = render;

← 4173af9 wear: real multi-item shopping cart (own cart, not Shopify's  ·  back to CelebritySignatures  ·  auto-data-snapshot: 2026-09-08T15:35:04 (1 data files) — pub 8b345a4 →