# Product mockup assets

There are two ways to render a product. The app supports both and switching is a
data change — no component code.

| | Vector (default today) | Photo (what you want for realism) |
| --- | --- | --- |
| Source | `garment-art.component.ts` | an image file in this folder |
| Colours | any, instantly | any, from **one** neutral photo |
| Looks like | a clean illustration | a real T-shirt |

## Drop in a real photo

Put a square blank-garment photo here and point the product data at it:

```
public/assets/products/tshirt/front.png
public/assets/products/tshirt/back.png
public/assets/products/hoodie/front.png
public/assets/products/hoodie/back.png
public/assets/products/mug/front.png
```

Then in `src/app/core/data/catalog.data.ts`:

```ts
const TSHIRT_MOCKUPS: MockupView3D[] = [
  {
    view: 'front',
    label: 'Front',
    printArea: { x: 33, y: 30, width: 34, height: 38 },
    image: '/assets/products/tshirt/front.png', // ← the only line you add
  },
  { view: 'back', label: 'Back', printArea: { x: 32, y: 27, width: 36, height: 42 },
    image: '/assets/products/tshirt/back.png' },
];
```

That is it. The renderer immediately:

1. draws the photo,
2. multiplies the selected garment colour through it, masked to the garment's
   own silhouette — so **one white blank covers white, black, beige, navy and
   grey**,
3. draws the customer's design inside the print area,
4. lays the photo back over the design with `multiply`, so the blank's real
   folds and shadows fall across the print.

### Photo requirements — these matter

| Property | Value | Why |
| --- | --- | --- |
| Background | **transparent** (PNG or WebP with alpha) | the colour tint is masked by the alpha channel; a white JPEG background would get tinted too |
| Colour | **white or light grey** blank | multiply can only darken — you cannot tint a black shirt to white |
| Aspect | **1:1 square**, garment centred | the mockup box is square |
| Size | 1200–1600 px | large enough for the customizer, small enough to download fast |
| Lighting | soft, even, from the top-left | matches the shading the rest of the UI assumes |
| Style | flat-lay or ghost-mannequin, no model | a model's body changes the print area per photo |

Good sources: buy a mockup pack (Placeit, Creative Market, Yellow Images), shoot
one yourself against a white sweep and cut it out, or use a properly licensed
free blank. **Do not** save images out of a Google Images results page — those
are other people's photographs and almost all of them are licensed.

### Per-product notes

**T-shirts** — done. `front.png` and `back.png` are in place, and all three tee
products share them (`TSHIRT_MOCKUPS` in the catalogue).

**Hoodies** — done. The front print area has to clear two things a tee does not
have: the hood, and the top of the kangaroo pocket, which is why it is the
tightest print area in the catalogue. The back area starts lower than the front
because the hood lies over the shoulders. Both hoodie products share
`HOODIE_MOCKUPS`.

**Polos** — done, but the two views came from two different garments, which is
worth knowing before you re-shoot. The front is a white ghost-mannequin polo and
went through `prepare-mockup.ps1` like the others. Every white polo *back* we
could find was either watermarked across the fabric or worn by a model, so the
back was built from a purple one and decoloured — see `decolour-blank.ps1`
below. The consequence is that the back is a slightly different cut from the
front: it is framed to the same bounding-box area, so the toggle does not jump,
but the sleeves sit a little wider and the body a little shorter. One matched
pair would be better. Both polo products share `POLO_MOCKUPS`.

## Reproducing the current cut-outs

Every asset here was cut from a photograph by `tools/prepare-mockup.ps1`. The
settings are per-source, not universal — they follow from measuring the actual
pixels, so if you re-shoot, re-measure. `tools/debug-trimap.ps1` paints the
key's decision over the photo (green garment, red backdrop, blue undecided),
which is the fastest way to see why a cut is going wrong.

```powershell
# T-shirt - a PNG that already carries its own alpha, so there is nothing to
# key. -KeepBackground skips straight to trimming and resizing.
$tee = 'tshirt-front-back.png'
$teeArgs = '-Size 1200 -KeepBackground -EdgeSoften 1 -Sharpen 0.4 -SharpenRadius 1'
./tools/prepare-mockup.ps1 -Path $tee -Crop '60,0,760,980'  -Out public/assets/products/tshirt/front.png @teeArgs
./tools/prepare-mockup.ps1 -Path $tee -Crop '815,0,760,980' -Out public/assets/products/tshirt/back.png  @teeArgs

# Hoodie - a beige garment on plain white, so a brightness key is enough
$hood = 'hoodie-front-back.jpg'
$hoodArgs = '-Size 1200 -Tolerance 12 -Feather 14 -Erode 0 -EdgeSoften 1 -Neutral 248 -Sharpen 0.5 -SharpenRadius 1 -KeepLargest'
./tools/prepare-mockup.ps1 -Path $hood -Crop '0,0,294,360'   -Out public/assets/products/hoodie/front.png @hoodArgs
./tools/prepare-mockup.ps1 -Path $hood -Crop '294,0,246,360' -Out public/assets/products/hoodie/back.png  @hoodArgs
# The drawcords hang across the print area, so bring them up above it
./tools/erase-detail.ps1 -Path public/assets/products/hoodie/front.png `
  -Region '520,250,190,300' -ShortenTo 385 -Grow 5

# Cap - white cap on a dark charcoal backdrop, so a plain brightness key works.
./tools/prepare-mockup.ps1 -Path cap.jpg -Out public/assets/products/cap/front.png `
  -Size 1200 -Tolerance 38 -Feather 14 -ShadowCut 120 `
  -MaxHoleArea 0.00015 -MaxPatchArea 0.00015 -Erode 1 -EdgeSoften 2 -KeepLargest `
  -Sharpen 0.4 -SharpenRadius 1

# Polo back - a purple garment, so a different tool entirely (see below).
./tools/decolour-blank.ps1 -Path 39906_2__04954.webp -Report   # read the numbers first
./tools/decolour-blank.ps1 -Path 39906_2__04954.webp `
  -Out public/assets/products/polo/back.png `
  -Reference public/assets/products/polo/front.png `
  -ChromaLow 6 -ChromaHigh 50 -NeutralTarget 232 -Gamma 0.55
```

## When the only photo of a view is a coloured garment

`decolour-blank.ps1` is the tool for that, and it is a different job from
`prepare-mockup.ps1` rather than a flag on it.

Keying is *easier* here, not harder. A backdrop and the shadow the garment casts
on it are both neutral, and a dyed garment never is, so chroma alone separates
the three: the polo source is 47.9% of frame at chroma ≤ 2 and only reaches
49.4% by chroma 60, then jumps to 97.9% by 80. That 1.4% gap between 2 and 60 is
the antialiased rim, and ramping alpha across it is what keeps the edge soft.
`-ChromaLow 6 -ChromaHigh 50` straddles it. No `-ShadowCut` is needed, because
the cast shadow is neutral and goes with the backdrop in the same pass.

Decolouring is the part that is easy to get wrong. `prepare-mockup -Neutral`
takes the Rec.709 luma, which is 71% green — and on a purple garment green is
the *darkest* channel (the measured dye is BGR 137,61,89, so green is at 61).
Lifting that to white needs a gain near 2.9 and multiplies the channel's noise
up with it, while flattening the piqué texture into the bargain. Dividing each
channel by the dye instead recovers the shading term the dye was modulating —
which is exactly the greyscale a white blank would have photographed as — and
averaging the three channels' estimates of it cancels most of the noise.

`-Gamma` is below 1 for a physical reason, not a cosmetic one: a dark dye
records more range than white cloth would under the same lights, because white
clips its own highlights. The purple source runs p05 0.82 → p95 1.15 about its
median; the white front runs 207 → 246 about 236, which is a good deal tighter.
`-NeutralTarget 232 -Gamma 0.55` lands the back at 215 / 231 / 244 against the
front's 207 / 236 / 246. Leave the gamma at 1 and roughly 7% of the garment
clips to pure white and loses its sheen; at 0.55 it is 0.05%.

`-Reference` is what makes front and back read as one garment. Two photos of two
garments never share a scale, and their bounding boxes cannot both be matched
without distorting one, so the scale is chosen to make the boxes equal in
**area** — that splits the error evenly (here +6% wide, −6% tall) instead of
piling it all onto one axis — and the result is centred on the reference's box
centre so shoulders and hem stay put as the customizer toggles views.

`-ShadowCut` is what takes the cast shadow off the backdrop, and it is worth
understanding because no threshold alone can do it. The shadow and the product's
own darkest material overlap in brightness: loosen the key enough to take the
shadow and it eats into the product; tighten it to protect the product and the
shadow stays as a grey halo. What separates them is position — the shadow lies
on the backdrop and so joins the frame edge, while a dark seam *inside* the
product is enclosed by bright material and never touches it. So the pass walks
in from the border through anything dark and clears what it reaches.

`-MaxHoleArea` is unusually tight here for a related reason. The pockets between
the cap's crown and its peak are genuinely see-through, and the hole fill exists
to rescue small details the key ate — at its default it treated those pockets as
holes, filled them, and left the backdrop's own colour sitting in them as black
blobs.

`-ShortenTo` lifts the cords off the garment, paints the cloth back over where
they were, and lays them down again compressed towards the hood. Cutting them
off part way instead leaves a blunt stub with no metal tip, which is what a cord
never looks like. The region has to start below the hood opening — row 250 here
— or the dark V inside the hood is read as part of the detail and compressed
along with the cords.

Why those numbers, since none of them are obvious:

- **A cut-out that arrives already made beats one keyed here**, every time. The
  tee took several rounds of keying to get right and still had a dark outline
  and a ragged shoulder; the same garment as a PNG with alpha needed one line
  and came out exact. Look for that first.
- **The colour bleed is not optional**, and it is the reason for the dark
  outline if you ever see one. A cut-out carries alpha, but the colour under a
  transparent pixel is still whatever the backdrop was. That does not show while
  the edge is hard — and then the edge gets feathered, or the browser scales the
  image, and those pixels get a share of the coverage. The garment is then
  composited wearing an outline of the studio it was shot in. Growing the cloth
  colour outwards first is what stops it; it runs for every source, keyed here
  or not.
- **`-Neutral`** strips the dye out of a coloured blank. The tint multiplies a
  colour through the garment, and multiplying a beige hoodie by white leaves it
  beige. A blank is one material under one light, so all its shape is in the
  luminance and the hue is only dye — dropping it costs nothing.
- **`-ChromaAxis rb`** keys on how far a pixel leans warm or cool. A blank is
  neutral at every brightness while a tinted sweep is not, which separates them
  where brightness cannot: the tee's own shadows run darker than the backdrop's
  highlights, so no `-Tolerance` works on that photograph. The direction is read
  off the corners, so a cool grey sweep and a warm beige one key the same way.
  `gb` and `mx` are the same idea measured differently, for a backdrop that
  casts a shadow of its own — in shade red falls back to meet blue, so
  red-minus-blue reads the penumbra as neutral as the cloth.
- **`-ChromaCut` sits halfway between the two.** Cloth measures 0–1 on that axis
  and a backdrop 8–12, so the cut goes at 3–6. Put it up near the backdrop's own
  value and the blurred signal crosses the threshold well inside the garment,
  eating the sleeves.
- **`-ChromaBlur` is large** because colour is stored at half resolution in a
  JPEG and coded in blocks, so an unsmoothed cut arrives as a staircase. With
  the cut mid-gap the smoothing is symmetric and does not move the edge.
- **`-ChromaMedian` where the two sides are close.** Averaging is the wrong tool
  when a garment and the shadow it casts sit a unit or two apart on the hue
  axis: a mean wide enough to smooth the block pattern also drags the shadow
  towards the cloth, and the shadow ends up keyed as garment. A median throws
  the blocks out without moving the boundary.
- **`-ChromaKeep 1`** marks what is certainly cloth. Everything between that and
  the cut is settled by connectivity: the backdrop reaches the frame border, and
  light bouncing off the backdrop onto a sleeve does not.

### A note on picking a source

Resolution is not the thing to judge a blank on. The hoodie here is a 540 × 360
render and cuts far more cleanly than a 3497 × 1960 photograph did, because it
is smooth — smooth gradients survive an upscale, and its plain white background
leaves nothing to disagree about. The photograph's extra pixels were going into
fabric grain and a soft shadow the key could not tell from the cloth.

What actually matters, in order: a background the garment cannot be confused
with, even in shadow; soft even lighting; and no watermark. A stock preview with
marks across the garment is unusable however large it is.

**Mugs** — needs **one** photo, and two things matter:

- **Which side the handle is on.** The print area is currently placed for a
  handle on the *right*. If your photo has it on the left, the print area needs
  mirroring — tell me and I will flip it.
- **Shoot it straight on**, not at an angle. The cylinder's shading is what the
  conform filter uses to bend the artwork around the curve, so a square-on shot
  gives a much more convincing wrap than a three-quarter view.

### After swapping in a photo

Re-check the `printArea` percentages. They are measured against the full square,
so if the garment sits differently in your frame the print box needs nudging.
Turn on the customizer (`/customize/classic-unisex-tshirt`) — the dashed outline
shows you exactly where it lands.

### Already have one photo per colour?

Set `tintable: false` on the mockup and swap `image` yourself, or extend
`MockupView3D` with an `imagesByColor: Record<string, string>` map and resolve it
in `MockupCanvasComponent.photo()`. Nothing else changes.

## The vector fallback

Until a photo is supplied, `GarmentArtComponent` draws the garment as inline SVG:
every edge is a curve, a lit-turbulence filter supplies the woven texture, folds
and seams are drawn *over* the print with `multiply`, and the design is pushed
through a displacement filter so its edges ripple with the weave.

It is a good placeholder. It is not a photograph, and it is not trying to be.
