Markdown
Preview

Product Mockup Implementation — Lessons Learned & Roadmap

Preface — Time and Money Wasted

Before anything technical: this document exists because building a "simple" product mockup turned into a 3-day disaster. 6 full context windows were burned across multiple AI agents (Composer 2.5 and Claude Opus 4.8 among them) that spread misinformation, invented non-existent APIs, hallucinated Sharp/Canvas behaviour, and produced code that looked plausible but was fundamentally broken in ways that took hours to diagnose manually. Neither Composer 2.5 nor Claude Opus 4.8 was able to deliver a working, professional product mockup. The cost in time, money, and mental energy was significant and completely avoidable had the agents been honest about the limits of their knowledge instead of confidently wrong.

This document is the ground truth. Read it before touching the mockup pipeline again.


1. Tech Stack (Current, Jun 10 2026)

Architecture overview

User generates motif (Gemini / Kontext / PuLID)
        ↓
KidagineWidget.tsx (browser, inside iframe on widget.kidsfun.tv)
        ↓ isPencilCaseCssMockupKey() → true
renderPencilCaseCssMockupDataUrl()        ← pencilCaseCssMockupClient.ts
        ↓  canvas2D render (browser)
PNG data URL (840×842)
        ↓  kidagine:preview CustomEvent
StudioProductDetailPage.tsx (storefront kidsfun.tv)
        ↓  setPreviewUrl(dataUrl)
<img src={dataUrl}>  plain img, no CSS blending on storefront side

Files

File Role
frontend/lib/mockup/contrado/pencilCaseCssMockupClient.ts Canvas render — the entire mockup pipeline in the browser
frontend/lib/mockup/contrado/pencilCaseCssMockupConfig.ts All layout constants (face area, radii, fade, object-position)
frontend/lib/mockup/contrado/compositeMockup.ts Server-side Sharp pipeline (used by /api/mockups/generate, NOT by the widget for pencil case)
frontend/lib/mockup/contrado/catalog.ts Contrado product catalog metadata
frontend/components/widget/KidagineWidget.tsx Calls renderPencilCaseCssMockupDataUrl, dispatches preview event
frontend/components/widget/ContradoPencilCaseCssMockup.tsx CSS React component — not used anywhere in production

Static assets (`frontend/public/assets/mockups/contrado_pencil_case_neoprene_black/`)

File Size Notes
base.webp 840×842 px Real Contrado blank-pouch product photo
face-shading.webp 715×430 px Multiply vignette — exact face area size
lid-mask.png 715×430 px Face shape mask — RGB (no alpha!), black/white
multiply-overlay.png 1008×1024 px JPEG mislabeled as .png — superseded, no longer used
pencil-case-mockup.css CSS for storefront embedding — not used (storefront uses plain img)

Face area constants (Sharp catalog, calibrated for `base.webp` 840×842)

left:   0.0786  →  66 px
top:    0.2803  → 236 px
width:  0.8512  → 715 px  (= face-shading.webp width)
height: 0.5107  → 430 px  (= face-shading.webp height)

Canvas render pipeline (current, working)

1. drawImage(base.webp, 0,0,W,H)              — real product photo as background
2. ctx.save() → clipDesignFace() → ctx.clip() — all-4-corners rounded clip path
3. fillRect(FABRIC_FILL)                       — erase sample print on photo
4. drawCover(motif)                            — design with object-fit:cover, pos 50%/38%
5. applyTopFade() — destination-out gradient   — soft tuck under zipper tape
6. ctx.restore()
7. ctx.globalCompositeOperation = "multiply"
   drawImage(face-shading.webp, left,top,w,h) — fabric depth / vignette
8. canvas.toDataURL("image/png")

2. What Was Tried — Full History of Failures

Attempt 1–3 — Sharp server-side composite (`compositeMockup.ts`)

What it does: Node.js / Sharp on Vercel: load base.webp, fill face area with cream, composite design, multiply face-shading.webp, layer overlay.webp on top.

Why it was used: Mirrors how Printful mockup pipeline works (server renders PNG, returns it).

Problems:

  • Worked in local tests but frequently errored on Vercel (cold-start Sharp native binaries, overlay.webp not present in repo, path resolution in process.cwd() differing between local and deployed).
  • overlay.webp (zipper on top layer) was never shipped to the repo — Sharp silently skipped it, leaving no zipper detail over the design.
  • Result: functionally identical to the canvas approach but with more moving parts and a server round-trip.
  • Status: superseded for pencil case widget path. Still used by /api/mockups/generate for server-side consumers.

Attempt 4–5 — CSS `mix-blend-mode: multiply` React component

What it does: A React component (ContradoPencilCaseCssMockup.tsx) renders the design as an <img> in an absolutely-positioned div, then layers multiply-overlay.png on top with CSS mix-blend-mode: multiply.

Why it was appealing: Zero server cost, instant browser render, pure CSS.

Why it failed:

  • The storefront (kidagine-website) shows the mockup as a plain <img src={dataUrl}>. It does NOT render React components from the widget repo. The CSS component is never instantiated on the storefront.
  • Agents hallucinated that the storefront was using the React component — it wasn't. The CSS approach is only useful if the storefront embeds the component directly.
  • Status: component exists in repo but is unused. Not wired anywhere.

Attempt 6–8 — Canvas with `multiply-overlay.png` + `destination-in` top-fade

What it does: Canvas2D render using the mislabeled-JPEG multiply-overlay.png: white canvas → clip design → destination-in fade → multiply overlay.

The catastrophic bug discovered Jun 10: canvas.globalCompositeOperation = "destination-in" with ctx.fillRect(x, y, w, fadeH) erases every canvas pixel that the fillRect does NOT cover — because destination-in formula is result_alpha = source_alpha × dest_alpha, and outside the fillRect the source has alpha=0, so 0 × dest = 0 → the entire design below the tiny 3% top strip is transparently erased. The multiply overlay then fills those transparent pixels with near-white (238/255), making the whole face look blank white.

This bug produced the blank-white pencil case that was shipped live for days.

What agents said: Multiple agents (Composer 2.5, Opus 4.8, others) looked at this code, claimed it was correct, blamed CORS, blamed the overlay image, blamed the storefront CSS — none identified the destination-in vs destination-out semantics error.

Status: fixed Jun 10 by switching to destination-out with reversed gradient.

Attempt 9–11 — Current working approach (canvas + `base.webp`)

After the destination-in bug was found and fixed the pipeline was also improved to use base.webp (the real product photo) as the background instead of multiply-overlay.png. This gives the zipper, material texture, and product shadows from the actual photograph.

Status: live and functional. Design is visible. Corners rounded. Zipper shows correctly.


3. What Works

  • Canvas2D render in the browser — no server round-trip, no Vercel cold-start issues, no Sharp native binary problems.
  • base.webp as background — the real product photo provides authentic zipper detail, material edge, and shadow depth automatically.
  • face-shading.webp multiply — adds subtle fabric vignette; more visible on dark/rich designs than on bright yellow designs.
  • destination-out top fade — correctly erases only the top strip so the design tucks under the zipper without destroying the rest of the face area.
  • All-four-corners rounded clip — top radius (5%/6%) tucks corners under the black trim; bottom radius (5%/10%) follows the product's natural lower curve.

4. Remaining Visual Gap — Current vs Target

Current result

Flat 2D composite on a product photograph. Design is correctly positioned and clipped but reads as a print pasted onto the photo. Bright designs (yellow sky, high-key backgrounds) look noticeably flat because the face-shading.webp is very subtle (pixel values 180–255 — less than 30% shadow depth).

Target (Contrado-quality mockup)

Photorealistic appearance where the design integrates with:

  • Realistic neoprene fabric texture overlaid on the print
  • Proper edge curvature / perspective wrap
  • Convincing shadow depth at the print boundaries
  • The zipper region blending naturally with the top of the design

The Contrado product configurator achieves this with a 3D render — not a 2D composite.


5. Roadmap to a Flawless Mockup

Why the gap exists

The current face-shading.webp is too weak (180–255 range = at most ~29% darkening, on average ~10%). A flat sunny-yellow design gets almost no depth cue. The base photo background behind the design is erased and replaced with cream fill — so there is no fabric texture from the real photo applied TO the design itself.

Option A — Stronger shading layer (fastest, no new assets needed)

What: Replace face-shading.webp with a version that has more contrast — darker edges, a subtle centre highlight, and a slight fabric grain texture baked in (values 100–255 instead of 180–255).

How to produce:

  1. Open the current blank neoprene photo in Photoshop / GIMP.
  2. Crop to the exact face area (715×430 px from base.webp).
  3. Create a new layer: radial gradient dark→light from edges to centre (simulate convex neoprene curve). Set opacity 30–50%.
  4. Add a very low-opacity fabric noise layer (Perlin noise, 2–4% opacity).
  5. Flatten to grayscale (R=G=B), export as face-shading.webp at 715×430.
  6. Drop into frontend/public/assets/mockups/contrado_pencil_case_neoprene_black/.
  7. npx vercel --prod — no code change needed.

Expected result: All designs get visible depth regardless of brightness.

Option B — Fabric texture overlay (medium effort)

What: Add a separate fabric-texture.webp PNG/WebP — a tileable neoprene grain at ~10% opacity — composited OVER the design with source-over before the face-shading multiply. This makes the design look printed into fabric rather than floating on top.

How:

  1. Photograph or render a close-up of blank neoprene at 715×430 or larger.
  2. Convert to grayscale, normalize to 50% mid-grey (values centred on 128).
  3. Store as fabric-texture.webp.
  4. In pencilCaseCssMockupClient.ts, after drawCover(motif) add:
    ctx.globalAlpha = 0.12;
    ctx.globalCompositeOperation = "source-over";
    ctx.drawImage(fabricTexture, left, top, width, height);
    ctx.globalAlpha = 1.0;
  5. Then continue with face-shading multiply as normal.

Expected result: Subtle fabric grain visible through the design — significantly more realistic, especially on solid-colour areas.

Option C — Professional Photoshop Smart Object template (best quality)

What: Commission or create a Photoshop .psd with a Smart Object placed on the neoprene face. Use Dynamic Mockups, Placeit, or a freelance retoucher to produce a template where the Smart Object is already perspective-wrapped, shadow-cast, and fabric-textured.

Runtime: Export via Dynamic Mockups Render API (already in the codebase as MOCKUP_PIPELINE=hybrid), or a new dedicated endpoint.

Cost: $0.01–0.05 per render on Dynamic Mockups. One PSD template cost ~$50–200 to commission from a product photographer / retoucher.

Expected result: Indistinguishable from a real product photograph. Same quality as the Contrado 3D configurator but without requiring Contrado's closed API.

Key advantage: The same pipeline works for every product — pencil case, lunch box, t-shirt, pillow. One PSD template per product SKU.

Option D — Contrado Helix headless API (ideal but not yet available)

What: Email integrations@contrado.com requesting headless design-upload API that returns updated product3dImages. If they provide it, each widget generation would POST the print file to Helix and poll for the 3D render URL.

Status: Not in public Helix docs as of Jun 2026. Helix only exposes read-only GET /helix/v1/stores/products/{id} with pre-baked productImages. No upload-and-render endpoint documented.

Do not spend time on this until Contrado confirms the API exists.


Immediate (no code, 30 min): Produce a new face-shading.webp with stronger contrast (Option A). This alone will make bright designs look significantly more professional. No code deploy required.

Short term (1–2 hours): Add fabric-texture.webp overlay at 10–12% opacity (Option B). Source a neoprene texture from a stock site (or photograph the blank pouch). This closes most of the gap to the target.

Medium term (full professional quality): Commission a Photoshop Smart Object PSD template (Option C) from a product photographer. Wire it to the existing Dynamic Mockups hybrid pipeline. Reuse for all Contrado products.


7. Rules for Future AI Agents Touching This Code

  1. Read this document first. Do not re-invent the investigation that already happened.
  2. destination-in vs destination-out — these are NOT interchangeable. destination-in erases everything the source does NOT cover. destination-out erases only where the source IS drawn. Always verify the compositing spec before using either.
  3. multiply-overlay.png is a JPEG (confirmed with file command). It is no longer used by the canvas pipeline. Do not re-introduce it.
  4. The storefront is a plain <img> — it renders no CSS blend modes and no React components from the widget repo. All compositing must happen inside the canvas render.
  5. face-shading.webp is 715×430 px — the exact face area size. Do not resize it in asset processing; it is already the right dimensions.
  6. lid-mask.png is RGB with no alpha — it cannot be used directly as a Canvas alpha mask without a conversion step. The current pipeline uses a path-based clip instead.
  7. Do not touch compositeMockup.ts for the widget path. That file serves /api/mockups/generate (server consumers). The widget uses pencilCaseCssMockupClient.ts.
  8. If an agent claims destination-in is correct for a top-fade effect, it is wrong. The correct operation is destination-out with an opaque-at-top gradient. This was the root cause of 3 days of wasted work and 6 burned context windows across agents including Composer 2.5 and Claude Opus 4.8, none of which identified the error correctly.