How Helen Gets Made A making-of, not the live show Watch her live

Workshop · the court · the three.js build · the shop

The 3D shop: a three.js room that sells twenty-two print-on-demand pieces, and the code between a card and a press

The shop on the live site is a WebGL room. Every mug, tee, tote and frame in it is plain three.js geometry in the colour of the real blank, with the artwork laid on top from the file that goes to the press. Hover shows a price, a click opens the piece, the bag lives in the browser, and checkout never leaves the domain.

Behind that, a 130-line Printify module and one Stripe webhook turn a paid intent into a print order, and the bug that shipped every t-shirt as a Small is the reason the catalog now refuses to guess.

The live three.js shop: a dark salon with gold-framed prints on the back wall, black tees on a rack, and mugs on the centre table
The room, as a visitor sees it. Twenty-two print-on-demand pieces in one salon, including the Court Annex console. Walk it at helenthecatlive.com/shop/.

Two things are for sale on helenthecatlive.com: a $2 treat, which is its own page, and the Royal Collection, which is this one. The collection is twenty-two print-on-demand pieces in six zones (back wall, centre table, rack, bench, cabinet and the Court Annex console) fulfilled by Printify and sold from a three.js room that shares its renderer, quality loop and dither with the court. The room does not load the generated cat; the Tripo-to-GLB path is on the 3D generation page, and everything in the shop is procedural geometry with a texture. What follows is read from the source: src/shopGoods.ts, src/shopRoom.ts, src/shopCourt.ts, src/site/catalog.ts, server/api.mjs, server/printify.mjs and scripts/sync-printify-variants.mjs, plus the live catalog.json and decal manifest the site serves.

22print-on-demand pieces
6room zones
20PNG print files in the decal manifest
$0prices the browser is allowed to set
The salon · six zones · camera at z = 4.18 1 · Back wall 2 · Rack 3 · Centre table 4 · Cabinet 5 · Annex console 6 · Bench You stand here

Layer 1 of the workshop layout: a static zone map. Each zone links to the section below. The live room uses one annex fly-to waypoint for console goods so six small targets do not fight for camera space.

The Royal Portrait print

Print Flagship portrait

Royal Portrait tee

Tee Portrait on black cotton

Royal Portrait mug

Mug Wrap around ceramic

Royal Portrait phone case

Case Print on the back

The Gilded Frame print

Frame Helen, Softly

Goodnight Moon mug

Mug Crescent moon

Royal Crest tee

Tee Gold crest

Long Live the Queen tee

Tee The decree

Monogram tee

Tee Crowned H

Royal Crest tote

Tote Natural canvas

Royal Mug

Mug Everyday crest

Royal sticker sheet

Stickers Four-piece set

Rescued Queen print

Print Rescued Queen

Royal Crest hoodie

Hoodie Soft black

Royal Portrait pillow

Pillow Two-sided

Royal Crest journal

Journal Ruled pages

Printify studio mockups of the original sixteen pieces. The 3D room no longer wears these photographs. It wears the print files below. Six annex SKUs are visible in the room as coming soon until Printify products are bound.

Back wall 4 clickable

Three flagship prints stay centred; the left decorative photo became the Royal Decree poster so the wall sells the joke instead of hanging a non-stock portrait beside it.

Clothing rack 5 clickable

Four hanging tees and one hoodie. The Winter Park tee took the leftmost rod slot so the rescue story is wearable, not only on the wall.

Centre table 5 clickable

Mugs face the camera with atan2 so the printed band reads from the doorway. Stickers and the journal share the tabletop without stacking sellables on top of each other.

Cabinet & floor 3 clickable

The phone case sits on a lit shelf; two totes lean on the floor as a deliberate pair. Shelf props (book stacks, trinket trays) are set dressing only.

Court Annex console 4 clickable + decor

A new lacquered console against the right wall holds small-format goods. Clicking any annex sellable flies the camera to one shared waypoint; a sticker sample and a folded tee on the lower shelf are non-clickable scale references.

Bench 4 clickable

Folded tee stacks repeat rack designs for visual weight; only the top fold in each stack is sellable. The portrait pillow props against the arm.

The room: real blanks, real print files

The first version of the room textured each object with its Printify studio mockup, a photograph of the finished product on a white sweep. A cylinder wore a photo of a mug, a plane on the rack wore a photo of a shirt with its white background, and a gilt frame contained a photo of a framed print. The fix was to stop using pictures of products and use the artwork itself: the print files Printify sends to the press are flat, unlit, 3600×3600 and mostly carry alpha already. They were pulled down once, resized, and written to public/merch/decal/ with a manifest beside them.

Royal Portrait print file with transparent background
Print
Royal Portrait tee print file
Tee
Mug print file, wide band with alpha
Mug band
Royal Crest artwork used on the tee, hoodie and journal
Crest
Crest artwork on the tote panel
Tote

The files that go to the press. Checkerboard is alpha. The mug band is wide because it only covers the printable arc, not the handle.

public/merch/decal/decals.json (two of the sixteen entries; the hoodie and the journal reuse the crest tee’s art)
{
  "royal-portrait-mug": {
    "file": "/merch/decal/royal-portrait-mug.png",
    "w": 768,
    "h": 319,
    "aspect": 2.4075,
    "kind": "mug"
  },
  "royal-crest-hoodie": {
    "file": "/merch/decal/royal-crest-tee.png",
    "w": 512,
    "h": 512,
    "aspect": 1,
    "kind": "hoodie"
  }
}

Sixteen entries, fourteen PNG files, 444 kB on disk. Each builder in shopGoods.ts makes the object out of the material it is printed on (0xeeece7 ceramic, 0x17130f black cotton, 0xe9e3d7 natural canvas, 0x14100e case shell) and then adds one extra mesh for the art, sized from the decal’s own aspect so nothing stretches. The mug band is a partial cylinder rather than a full wrap, because the print file covers the printable area and leaves a gap behind the handle; wrapping it 360° distorted the art nearly two to one. The tee’s chest plane, the tote’s front panel, the pillow face and the phone-case back all read w and h from the manifest the same way.

src/shopGoods.ts — the material every decal mesh starts with
function decalMaterial(opts: { roughness?: number; metalness?: number } = {}) {
  return new THREE.MeshPhysicalMaterial({
    color: 0xffffff,
    transparent: true,
    // Starts invisible. A white lit material at full alpha with no map is an
    // opaque white card, so every product wore a blank rectangle for the whole
    // texture load - and permanently if the manifest fetch failed.
    opacity: 0,
    roughness: opts.roughness ?? 0.62,
    metalness: opts.metalness ?? 0.02,
    envMapIntensity: 0.18,
    // Decals sit a fraction of a millimetre off their surface. Without this the
    // two coplanar faces fight and the art flickers as the camera moves.
    polygonOffset: true,
    polygonOffsetFactor: -2,
    polygonOffsetUnits: -2,
    depthWrite: false,
  });
}

Two details in that function are the whole trick. The decal starts at opacity: 0 and fades up when its texture lands, because a white lit material with no map is an opaque white card, and every product wore a blank rectangle for as long as the texture took to load, or forever if the manifest fetch failed. And the decal sits a fraction of a millimetre off its surface with polygonOffset and depthWrite: false, otherwise the two coplanar faces fight and the art flickers as the camera moves. Only meshes tagged with userData.decalKey ever receive a texture; an earlier version sniffed for placeholder colours and stamped product art onto anything that happened to share a hex.

What changed when mockups became decals
ObjectBeforeAfter
Mugcylinder, map = photo of a mugwhite ceramic body, gold rim and handle, printed band wrapped around the printable arc
Teerectangle, map = photo of a shirtt-shirt silhouette in black cotton with sheen, chest print sized from the decal’s aspect
Totebox, map = photo of a totenatural canvas body, black webbing straps, crest on the front panel
Phone casemap = photo of a caseblack shell with clearcoat, print on the back
Framed printframe containing a photo of a framed printframe containing the poster artwork
Textures per visit23 files, 2,675 kB, 1.55 MB of it never drawnthe decals plus three small portraits already cached from the home page: 571 kB

The room is checked without a browser. npm run check-shop bundles buildShopRoom() under Node with no GPU and fails if a catalog product is missing from the scene, if an object is tagged with a slug that is not in the catalog, if a decal key has no artwork, or if any of the framed photographs that are set dressing becomes clickable. That last one was a real bug: for months two wall frames showed photographs of Helen but were tagged as prints that looked nothing like them, so clicking the photo opened a different product.

The salon · camera at z = 4.18, looking toward the far wall Rack Winter Park + three tees + hoodie Table Three mugs, stickers, journal. Faced to camera. Cabinet Phone case