Contents

Demo pictures

The demo's pictures come from two places:

  • Photographs in scripts/demo/assets/photos/: every picture of the products, on white, in rooms, outdoors and in the studio. They were made with an AI image model (OpenAI's), with renders of the products as references, so each product keeps its shape, colours and light ring from picture to picture. They are not stock photos and show no real brand.
  • Films in scripts/demo/assets/films/: the home page's teaser, a slow montage of photographs (see The teaser film).
  • Renders in scripts/demo/assets/renders/, made from code with three.js: the turntable video of Column, and the renderer that drew the products the photographs were made from. Nothing in a render is photographed, bought or downloaded, so the kit owns every pixel, and a new colour or a changed product is a change to the code.

pnpm seed:demo uploads every file in these folders to the Media library, with alt text in English and Danish. The Docker image carries the folders, so the nightly reset of a public demo uploads them too.

The photographs #

WebP files registered in PHOTOS in scripts/demo/media.ts, each with alt text in English and Danish. Pages and products refer to them by id: img['drift-by-the-sea'].

  • Products on white (column-chalk, dial-graphite …) are 2000 × 1500 on pure white, the product centred at the same size in each, so a product grid reads as one surface. A colour is an edit of the product's Chalk picture, so its shape and framing stay the same.
  • Everything else is wide: rooms, details and the places in the stories. Each has a focal point on the product, which is what a phone keeps when it crops the picture to its tall screen.
  • The openings (the pictures a page, product or story opens with, and those running edge to edge) are upscaled to twice their size, about 3800 × 1650 or 3350 × 1900: the image model makes about 1.6 megapixels, and an opening on a large, high-density screen is shown at up to three times that. They went through Real-ESRGAN (realesrgan-x4plus), and the result was blended with the original at 65 to 35, which keeps the crisp edges and brings back the texture of fabric and wood the upscaler smooths away. A picture you add as an opening needs the same.
  • To replace one, make a new picture with the product where the old one has it, save it as WebP under the same name, and check its focal point and alt text.
  • To add one, save it in the folder and give it an entry in PHOTOS.
  • tests/unit/demoImages.test.ts checks that every entry has its file and the folder holds no other, and that each has its own alt text in both languages.

The teaser film #

home-teaser.mp4 plays over the home page's opening picture (a Hero's Film): three photographs, each moving in slowly towards its product and fading into the next, a seamless 10-second loop at 2560 × 1440, about 7.5 MB.

pnpm demo:teaser

scripts/demo/films/encode.swift draws every frame with Core Graphics and encodes the film with macOS's own H.264 encoder (AVFoundation and VideoToolbox, High profile, 6 Mbit/s), one frame after the other, with the index at the front so a browser starts playing before the file is in; MP4 (H.264) plays in every browser, and no ffmpeg is needed. It needs a Mac with Xcode's command line tools. A browser's own recorder (MediaRecorder) is no substitute: its real-time encoder breaks the fades between shots into blocks. Its shots, their focal points, how far each moves in, and the length are in TEASER at the top of the file. The first shot is the hero's own picture, so the film starts where the picture leaves off. It is registered in FILMS in scripts/demo/media.ts; run it again after changing a photograph it uses.

The renderer #

Run it #

You need the kit installed (pnpm install) and Playwright's Chromium (pnpm exec playwright install chromium). No GPU is needed.

pnpm demo:render                      # every shot
pnpm demo:render room-drift share     # only these
pnpm demo:render --list               # each shot's file, size and what it shows

Its registry holds the turntable video; add a still to it to draw a product as a reference for a new photograph. It writes <id>.webp for a still and <id>.webm for a video, prints each file's size and time, and names any file in the folder that no shot makes.

How long it takes: a still takes a few seconds, a room scene up to seven. A full run takes about three minutes on a recent laptop, half of it the video.

The textures are drawn from seeded random numbers, so a run makes the same pixels every time: re-rendering a shot you did not change leaves its file as it was, and git shows only real changes. The video is the exception; its encoder varies a little from run to run.

How it works #

The scene runs in a web page, in Playwright's Chromium with software WebGL (SwiftShader), so it renders the same on every machine:

  1. A small static server serves the page and three.js from node_modules, on a free port on 127.0.0.1.
  2. For each shot, the runner opens the page and hands it the shot's entry from the registry.
  3. The page builds the set and renders it at twice the size, then scales it down (softer edges and finer texture than antialiasing alone gives) and returns it as WebP.
Path in scripts/demo/render/What it holds
shots.tsThe shot registry: every shot's id, size, environment, camera and what stands where. The only place a shot's id is written.
index.tsThe runner behind pnpm demo:render
server.tsThe static server; it serves nothing outside the page's folder and three.js
webm.tsFinishes a recorded video (see below)
scene/main.jsBuilds a shot and renders it, in the page
scene/products/One file per product
scene/props/Things that are not products: fabric swatches, metal samples, the collar in its stages, a vase, books
scene/environments/The sets: the white, light and dark studio, the room (bare, in window light, with a sideboard in the morning or at dusk, with a bench, at dusk), a wooden deck by the sea, a pale table and a steel work surface
scene/palette.jsThe four colours
scene/materials.js, scene/textures.jsKnit, aluminium, glass and the light rings; the textures, all drawn in code
scene/shadows.js, scene/geometry.jsSoft footprint shadows for the studio; shared shapes

tests/unit/demoRenders.test.ts checks the registry: unique ids, sizes the renderer can draw, products, props, colours and environments that exist, and a file in the folder for every shot and no other. It also checks that the server cannot be talked into serving a file outside its two folders.

Add a shot #

  1. Add an entry to SHOTS in shots.ts. Its key is the id and the file name. One unit is about 32 cm (Column is 1.9 tall); an item's y is the height it stands on, such as 1.65 on the sideboard or 1.3 on the bench.
  2. Run pnpm demo:render <id> and look at the file. Adjust the camera and run it again until it reads well.
  3. Give it alt text in English and Danish in RENDERS in scripts/demo/media.ts. The seed uploads every shot of the registry, so the type check fails until it has one. Pages and products refer to it by its id: img['<id>']. A still you only need as a reference for a photograph does not belong in the registry: render it, use it, and take it out again.
  4. Commit the shot and its file together; the unit test fails while one is missing.

What a picture is for #

These hold for a photograph as much as for a render.

  • On white: the packshots, the colour line-ups and the bleed-… pictures. The backdrop is pure white, so a picture melts into a white page: product grids and Media blocks show the product, not a box around it. A bleed-… picture is cut by one edge of its frame, for a Media block that runs off that side of the screen.
  • Openings (the rooms, the dark heroes, the places): pictures a page or a product opens with, edge to edge, its name at the bottom left. Keep the product in the upper half, or right of the middle: the words and the dark band behind them take the lower left. Give the picture a focal point on the product, in RENDERS or PHOTOS (scripts/demo/media.ts): it is what a phone keeps when it crops the picture to its tall screen.
  • Check every picture by eye: nothing floats (every object stands on something and has its shadow), no texture is stretched, and there is no text or logo.

Add a colour or a product #

  • A colour: add an entry to scene/palette.js (the knit, the aluminium, how satin it is, the trim, and whether it is dark), then add the shots that show it to shots.ts, such as column-<colour> with packshot('column', '<colour>').
  • A product: add a file to scene/products/ with a function that takes a colour and returns a group standing on y = 0, facing +z, with its footprint for the shadow in userData. Add it to scene/products/index.js, and give it a framing in PACKSHOT in shots.ts.

The video #

turntable-column.webm is Column turning once on white in six seconds, a seamless loop at 1920 × 1080 and 30 frames a second, about 1.8 MB. A shot with video in its entry becomes one.

The page renders the frames first, then plays them into the browser's own recorder (MediaRecorder, VP9) at a slow, steady pace, so no ffmpeg is needed and a busy machine does not drop frames. It plays the turn twice and keeps the second: the encoder's first frame comes out blocky. webm.ts then gives every frame its exact time, writes the length into the file and checks that no frame is missing.

If you do not want the demo #

The pictures exist only for the demo; nothing else in the kit reads them. If you remove the demo, delete scripts/demo/render/, scripts/demo/films/ and scripts/demo/assets/ with it, take three and the demo:render and demo:teaser scripts out of package.json, and delete tests/unit/demoRenders.test.ts.

These guides ship with Tolk Starter 0.6.0: buyers get the same pages in their repository, next to the code they describe.