# Assets and processing — Alderton Rowe

Alderton Rowe is an invented interior architecture practice. The name, the
projects, the addresses and the copy are all fictional. The site makes no
claims about awards, press, clients, credentials or trading history, and the
two places where a real practice would carry them are held as marked
placeholders instead.

## The film

One walkthrough was supplied for this concept. The original is kept at
`assets/alderton-rowe/residence-walkthrough-source.mp4`, outside the directory
Next serves.

| | resolution | bitrate | size |
|---|---|---|---|
| supplied | 1280×720, 24fps, 15.1s | 18.37 Mbps | 34.7 MB |
| `videos/residence-walkthrough.mp4` | same | 8.6 Mbps | 16 MB |
| `videos/residence-walkthrough.webm` | same | 6.3 Mbps | 12 MB |

x264 `-crf 16 -preset veryslow -tune film`, VP9 `-crf 24`. 18 Mbps is far past
the point at which the encoder is the limiting factor on 720p, so this halves
the file without the encode becoming a term in the result. 720p is the ceiling
the source sets; nothing here invents detail above it.

**The audio track is dropped.** The film is muted everywhere it appears — the
hero autoplays muted with no controls, and the case study starts paused — so
shipping an AAC track would be bandwidth nobody can hear.

## The scrubbed hero

The homepage film is not a `<video>`. It is a strip of stills painted on a
canvas and addressed by scroll position. A video cannot do this job: seeking a
long-GOP H.264 is slow and lands on the nearest keyframe, so a scrub over one
stutters and snaps. A strip is addressable by index.

Built from every source frame — all 361, the full 24fps the clip holds.

| Pack | Width | Size | Per frame | Served to |
|---|---|---|---|---|
| `seq-hd/` | 1280 | 37 MB | 105 kB | canvas backing store over 900px |
| `seq-sd/` | 800 | 15 MB | 42 kB | phones, where the store is about 780px |

### On resolution and frame rate

The obvious ask is 2160p60. It is not reachable from this source and no amount
of processing changes that: the supplied clip is **1280×720 at 24fps**.
Upscaling to 4K adds bytes and no detail, and 60fps means inventing two frames
in every three, which on a scrub shows up as smeared edges on the pans.

What was actually done instead:

- **every** frame rather than every second one — 361 rather than 181, which is
  twice the temporal resolution and all of it real
- quality 90 rather than 80, because the softness in motion was partly the
  encoder and there is nothing to hide behind at hero size
- the double resample removed. The canvas store was capped at 1600px and then
  stretched by CSS to about 3000 device pixels, so every frame was resampled
  twice. The store now matches the device, up to 2560, and the native frame is
  scaled once. Frames stay at native size in memory — pre-sizing a bitmap above
  the source would cost 14 MB each to invent nothing.

If a sharper hero is wanted, the only thing that delivers it is a
higher-resolution export of the original walkthrough.

The hero is four explicit layers, because leaving it to source order is what
broke it: poster 0, film 1, scrim 2, type 3. The poster is a fallback that
steps aside once a real frame is on the canvas.

Frames are decoded with `createImageBitmap`, pre-sized to the exact draw size,
so the decode happens off the main thread and `drawImage` is a blit rather than
a rescale. The cache is bounded by a memory budget and evicted frames are
`close()`d rather than left to the collector. Measured over a full scrub: a new
frame painted on **every** scroll event, zero stale paints, and rAF held at
60fps throughout — the smoothness is bounded by how often the browser reports
scroll, not by the player.

The rest of the strip is warmed into the HTTP cache after first paint, three at
a time, standing down entirely while the reader is scrubbing. It does not run
at all on a connection reporting `saveData` or 2g/3g.

`prefers-reduced-motion` does not switch the scrub off. That preference exists
to stop motion the reader did not ask for — autoplay, parallax, things that
move on their own. This moves only while a finger or a wheel is moving, exactly
in step with it, and stops dead when they stop; it is direct manipulation,
nearer to dragging a slider than to an animation. Disabling it did not reduce
motion so much as delete the film, which is the site.

What the preference does still turn off: the reveal transitions, the pulsing
rule beside "Scroll", and smooth scrolling.

## Stills

The photographs on the project pages are supplied renders of the same
apartment, at higher resolution than the film and better composed:

| File | Source | Intrinsic | Used as |
|---|---|---|---|
| `img/living-*.webp` | supplied render | 1774×887 | Work section, case-study opener |
| `img/kitchen-*.webp` | supplied render | 1774×887 | Case study, studio page |
| `img/bedroom-*.webp` | supplied render | 1774×887 | Case study, full bleed |
| `img/bathroom-*.webp` | supplied render | 1672×941 | Case study pair |
| `img/balcony-*.webp` | film, frame 336 | 1280×720 | Case study closing |
| `img/poster-*.webp` | film, frame 0 | 1280×720 | Hero poster and no-JS fallback |

Masters are kept in `assets/alderton-rowe/stills/`. Each ships at full width
and at 900px, WebP q90/q88 with sharp YUV.

**On intrinsic sizes.** `plate()` used to write `width="1280" height="720"` on
every image regardless of shape. Those attributes become the CSS height the
moment width is 100%, `object-fit` computes to `fill`, and the picture is
stretched to whatever number is in the markup — which is why the plates were
drawn 720px tall at every viewport and looked squashed. The generator now reads
each file's real size, and `img { height: auto }` means an attribute can never
do that again.

## One project

The site shows Parkside Residence and nothing else. There are no placeholder
panels and no invented addresses: the rhythm of the work section comes from
other rooms of the same apartment at different proportions.

## Cache and filenames

Every asset carries eight characters of its own md5 in its filename, and
`next.config.ts` serves `/alderton-rowe/*` as `immutable` for a year. Replacing
a file changes its URL, so a new image reaches people who have already visited.
`tools/gen_alderton.py` does the renaming on generation.

## What needs replacing before this is used for anything

- Studio address, postcode and telephone, marked in brown on the site
- `studio@aldertonrowe.example` — the `.example` TLD is reserved by RFC 2606 and
  cannot receive mail
- The Instagram link
- The team photograph and principal's details on the Studio page
- The contact form posts nowhere: it clears its own fields and transmits
  nothing. Point it at a real endpoint before use.

## Mobile

Checked on iPhone 15 Pro, iPhone SE, Pixel 7 and iPad Mini, in the engine each
of them actually runs.

The film scrubs on all four; the SD pack is chosen from the canvas backing
store, so a phone pulls 15 MB rather than 37 MB for detail a 780px store cannot
show. The menu opens full-screen with 71px targets, traps Tab, locks the page
behind it and closes on Escape. Form fields are 47px. The case-study film
starts paused with `playsinline` and `muted`, which is what iOS requires, and
plays on a tap.

Touch targets were the one real fault: the wordmark measured 17px, the menu
button 33, its close 26, the film control 30 and the disclosure bar's link 18.
All are now at least 44px, under `@media (pointer: coarse)` so the desktop
typography is untouched — it buys hit area, not size. Raising the disclosure
bar also raises `--strip`, which the pinned film sizes against, so the two stay
in step.

`tools/check-targets.mjs` walks all six pages on three handsets and reports
anything a fingertip would struggle with.
