Jelly Prism

jellyprism

OpusKimi
Opus$21.25 · 1h22m57s
Claude Opus 5.5$21.25 · 1h22m57s
Claude Opus 5.5 $21.25 · 1h22m57s
Winner
Kimi$5.00 · 35m00s
Kimi K3.1 Pro$5.00 · 35m00s
Kimi K3.1 Pro $5.00 · 35m00s
CheapestFastest

Opus wins. Both trace a real rainbow through the wobbling jelly: poke it and the colours shiver, cut it and you get two. Opus wins on the physics and the look: its second prism folds the fan back into white light, in a room you can actually see. Kimi, which got Opus's build as an example, has the nicer controls for a quarter of the price.

Remix in Claude

Scorecard

Opus2Kimi1

CriterionOpusKimi
Rainbow follows the jellyTraced through the deformed faces: shivers on a poke, stretches on a squishSame traced rainbow, same response to pokes and squishes
Cut into two rainbowsTwo fans side by side on the screenTwo fans side by side on the screen
Second prismFolds the fan back into near-white lightFan only narrows, the rainbow stays on the screen
LookDark studio that stays readable, prism and table clearNear-black room, the scene sinks into the dark
InterfaceClean card, small readoutsBig readouts with a live spectrum strip, sub-labels on every control
Cost$21.25$5.00
Time1h22m57s35m00s
OverallWinner–

The prompt

Build **"Jelly Prism"**: a real-time, interactive 3D **triangular prism made of clear jelly** on a table in a dark room, with a thin beam of white light that enters it and **splits into a rainbow** on a screen beside it. People **poke** it, **squish** it, **cut** it in two, **move the light** and **turn the prism**, and the rainbow always follows the real, wobbling shape of the jelly, with **synthesised sound**, as ONE self-contained HTML file.

It should feel like **a premium physics-of-light film shot in a dark studio**: a clean bright beam cutting through faint haze, a jelly prism glowing where the light passes through it, a rainbow so saturated it looks like it hums. Someone scrolling past a still frame should stop. Spend your effort on the light first, the jelly feel second, features third.

**The one thing that matters most: the rainbow is computed, never painted.** Every frame, trace the light through the **current deformed surface** of the jelly with real refraction (Snell's law at every face, a different index of refraction per colour, total internal reflection when it happens), and draw the beam, the fan and the rainbow on the screen from those rays. When the jelly wobbles, the rainbow must wobble because the faces moved, not because a texture was shaken. When the prism is cut, each piece bends its own share of the light. A pre-drawn rainbow texture, a fixed fan mesh or a rainbow that moves on its own is a failure.

**It must also be fun.** Every touch changes the light: a poke sends a shiver through the rainbow, a squish stretches it wide, a cut splits it, turning the light makes it slide, fade and vanish. It should be **hypnotic**: something you keep poking just to watch the colours tremble on the wall.

## Tech

- three.js r170 with `WebGPURenderer` (automatic WebGL2 fallback). Load it with an importmap from jsDelivr: map "three", "three/webgpu" and "three/tsl" to `https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.webgpu.min.js`, and "three/addons/" to `https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/`. These are the **only** network requests allowed: no other libraries, fonts, images, models, textures or audio files. Use TSL node materials. Physics, ray tracing and simulation are your own code (no physics libraries).
- Start with `<!doctype html>`, `<meta charset="utf-8">` and a viewport meta tag. System font stacks only.
- It must run inside `<iframe sandbox="allow-scripts allow-pointer-lock">` (no `allow-same-origin`): `localStorage` and cookies throw there, so wrap every storage access in `try/catch` and keep values in memory. No `alert`, pop-ups or top-level navigation.
- Fixed simulation timestep (120 Hz) with bounded substeps, decoupled from frame rate; on a slow frame, drop simulation time rather than exploding. Let the jelly sleep at rest and wake on input (the light stays on and keeps being traced while asleep, but only needs retracing when something moved).
- Preallocate every buffer, geometry and material at start-up, including the fan and rainbow meshes at their maximum ray count; nothing is created during interaction.
- A quality scaler that lowers pixel ratio, then shadows, then post effects, then haze sprites when frames get slow, never the ray count below the minimum below, the jelly feel or the rainbow's smoothness. Target 60 fps on a mid-range phone.
- Known three r170 pitfalls, avoid them:
  - the scene background is drawn after transparent objects: draw the room as meshes (the jelly and the beam are transparent);
  - never `.clone()` a `CanvasTexture` (the clone uploads black);
  - create every `InstancedMesh` at its full count and never draw one with fewer than 2 instances (wrong-size uniform buffer, black screen); park unused instances out of view instead;
  - never let a helper overwrite a shared temp vector its caller still uses.

## Priorities

Build it in this order, and finish each tier properly before starting the next. A polished tier 1 beats a broken everything.

1. **Must be excellent:** the traced rainbow (smooth saturated bands on the screen, the visible beam and fan in the haze, coloured light on the table), the jelly prism (clear, glossy, glowing where the beam passes), the jelly feel (poke, squish, wobble, ring-down) and the rainbow following it, the dark room and its lighting, the demo mode.
2. **Must work:** Knife (two pieces, two rainbows), moving the light (including total internal reflection), turning the prism, flavours that absorb colours, Second prism, Sweep, Wobble, Pose, Reset, milestones, sound, both renderers, the control card, the secret.
3. **Polish:** faint secondary reflections off the prism faces, dust motes drifting through the beam, the rainbow playing a chord, a soft glow on the jelly where the beam enters, slow motion, Cinema extras.

Never ship a feature half-working: if something from tier 3 can't be done well, leave it out cleanly.

## The room and the prism

Scene units: 1 unit = 1 cm.

- **The prism:** an **equilateral triangular prism** of jelly, 7 cm on each triangle side, 6 cm long, lying on one rectangular face so a **triangle faces the camera** (the classic silhouette: a triangle pointing up), with softly rounded edges (about 3 mm radius). Water-clear jelly with the faintest warm tint, tiny suspended air bubbles at different depths, a faint internal grain, so its depth reads.
- **The table:** a deep graphite stone slab (matte, faint procedural grain), large enough to fill the bottom of the frame.
- **The light:** a small matte black **slit projector** (a short brass-rimmed tube on a stand, about 4 cm long) on the table at the left, about 25 cm from the prism, partly in frame. It throws a **thin ribbon of white light**: about 3 mm thick in the plane of the triangle and 4 cm tall along the prism's length, so it lights a band across both halves of the prism when it is cut (see Knife).
- **The screen:** a large curved matte **projection screen** standing on the table to the right and behind the prism, about 30 cm away, in a mid-dark warm grey, where the rainbow lands as a tall band of colour. Behind everything, a deep charcoal studio wall fading into darkness.
- **Haze:** a faint, even haze in the air so the incoming white beam and the outgoing colour fan are **visible as shafts of light**, like the cover's beam, and a few dust motes drift slowly through it.
- Default composition: the white beam enters from the left, the prism sits just left of centre, the fan opens to the right and lands on the screen; the rainbow on the screen is in frame. A small fan of coloured light also lands on the table between the prism and the screen.

## Optics (the core)

- **Index of refraction per colour:** jelly is mostly water, so use a Cauchy-style curve with a base IOR of about 1.34 at 550 nm, scaled by a **Dispersion** slider from "realistic" (a narrow fan) to "dramatic" (default: a fan about 10 to 12 degrees wide so the rainbow is generous on the screen). The default must look like the cover: a clear white beam in, a clear fan out.
- **Rays:** trace at least **16 wavelengths** from 400 to 700 nm (32 on desktop) times **at least 9 rays** across the ribbon's height, through the jelly's current deformed surface. Use a low-resolution collision copy of the skinned surface (a few hundred triangles per piece, updated every step from the soft body) with a simple bounding test, so tracing is cheap. At each face: refraction by Snell's law with that wavelength's IOR, Fresnel-weighted energy (Schlick is fine), **total internal reflection** when the angle is past critical, up to 4 internal bounces; follow every piece in the beam's path, so light leaving one piece can enter another.
- **The fan and the rainbow are drawn from the rays:** build the fan in the air as smooth additive ribbons between neighbouring wavelength rays (so it is a continuous gradient, never separate lines), coloured by a proper wavelength-to-sRGB curve normalised so the bands read red, orange, yellow, green, cyan, blue, violet, with brightness by the energy that made it through. Where the rays hit the screen and the table, draw the rainbow as a smooth emissive band built from the hit points (blurred slightly at its edges, brighter in its middle), and add a little coloured light to the nearby screen and table from it.
- **Inside the jelly** the beam is faintly visible as a glowing path (light scattering in the jelly), and the jelly glows a little where it enters and leaves.
- A small part of the white beam reflects off the first face as a dim white ray up and to the left (like the cover). Light that misses the prism carries on to the screen as a white stripe.
- It must stay stable: rays never flicker or jump between frames at rest, the rainbow never tears into dots, and nothing ever breaks into NaNs when the jelly is squished, cut or turned.

## Jelly

- A volumetric soft body: a tetrahedral mesh (about 1,500 to 3,000 tets) solved with **XPBD** (deviatoric or elastic constraints plus volume preservation), skinned to a smooth high-resolution surface with the rounded edges.
- **Feel:** dense, heavy, slow jelly like a big panna cotta, never rubber: a slow jiggle (2 to 4 Hz), the top lags behind the base, it keeps its volume (squashed it bulges, pulled it thins), squashy dead bounces, and a **ring-down** after every move: two or three shrinking wobbles, then perfectly still. The faces' wobble is what makes the rainbow shiver: make it visible but never chaotic.
- Floor contact with gentle friction, no sinking, no tunnelling, no inverted tets, no explosions. Every piece always returns to its own rest shape. At rest: no jitter and a perfectly still rainbow.

## Tools

A tool picker with **Hand** (`1`), **Knife** (`2`), **Light** (`3`) and **Turn** (`4`). The default tool is Hand. A press that ends within 6 px and 450 ms counts as a tap. One finger acts; two fingers or right-drag orbit. Nothing is hover-only. Pointer events with **pointer capture**, mouse, pen and touch.

- **Hand:**
  - **Poke:** a tap presses a dent that springs back with a wobble rippling through the jelly; the rainbow on the screen **shivers**: the bands wave and bend, the fan in the air trembles.
  - **Squish:** press and hold on the top edge: the prism squashes down and bulges out, its faces tilt, and the rainbow **stretches wider** and slides; let go and it springs back with a big wobble, the rainbow snapping back with it.
  - **Stretch:** grab any point and pull: the jelly stretches locally like taffy and the rainbow warps and bends; on release it snaps back.
  - **Drop:** double-tap the prism to lift it and drop it from a small height: a squashy landing and a big shiver of colour.
- **Knife:** a quick swipe across the prism cuts it along the vertical plane the swipe draws through the scene. The pieces really separate: split the tet mesh along the plane (split or duplicate crossing tets and vertices) into separate soft bodies with fresh, slightly wetter cut faces, and they slide apart a little. A cut **across the prism's length** gives two shorter prisms side by side in the light ribbon, each bending its own half of the beam: the halves drift and turn a little, so **the rainbow splits into two**, one above the other on the screen, wobbling independently. A cut through the triangle gives two odd pieces that throw broken, doubled and folded rainbows (light bouncing between them). Up to **4 pieces**; slivers too small to simulate are merged; beyond 4, extra cuts are ignored gracefully. Every piece can be grabbed, poked and turned on its own.
- **Light:** drag anywhere to swing the projector around the prism on an arc (and tilt it a little up and down): the beam's angle of incidence changes, the fan widens, narrows and **slides across the screen**, and past the critical angle the rainbow **slides off and vanishes** as the light is trapped and bounces inside the prism (total internal reflection, the jelly glowing brighter inside), then comes back. A thin readout shows the angle of incidence.
- **Turn:** drag left and right to rotate a piece around its vertical axis on the table (the one under the pointer, or the whole prism), with a little jelly lag and wobble when you stop. The fan swings and the rainbow sweeps across the screen.

## Big moments

- **Light** (switch and `L`): the projector switches on with a soft click, the beam **streaks in** through the haze over about 0.3 s, hits the prism and the fan **bursts open** onto the screen. Off: the beam fades out and the rainbow drains away.
- **Second prism** (button and `N`): an **upside-down second jelly prism** slides into the coloured fan between the prism and the screen: the colours bend back together and leave it as **one white beam again** (Newton's experiment), with a little rainbow fringe at its edges. Poking either prism makes the white beam blush into colours at its edges. Pressing again slides it away.
- **Sweep** (switch and `W`): the light swings slowly back and forth on its arc and the prism turns gently, so the rainbow **sweeps across the room** (screen, table and wall), occasionally sliding off into total internal reflection and back. Calm, endless, the screensaver of the toy.
- **Wobble** (button and `S`): the whole table jolts: every piece shivers hard for a second and the rainbow dances, then everything rings down.
- **Pose** (button and `F`): the pieces settle, the light moves to the angle that gives the widest clean rainbow, a soft white camera flash fires, and the frame holds still for 1 second with all UI and the cursor hidden (a clean screenshot); then the UI fades back. Never during demo mode.
- **Reset** (`R`): one whole prism, light on at its default angle, no second prism.

## Flavours

Round swatches. The jelly's colour comes from **absorption per wavelength** (Beer-Lambert through the distance each ray travels), so the flavour also changes the rainbow: a thicker path means more is eaten.

- **Clear** (default): water-clear, the full rainbow.
- **Cherry:** deep red jelly that eats the blues and greens: the rainbow shrinks to red, orange and a little yellow.
- **Lime:** green jelly: the rainbow keeps yellow, green and a little cyan.
- **Blue raspberry:** cyan-blue jelly: the reds die, cyan and blue stay.
- **Grape:** violet jelly that lets through red and blue but eats the middle: a rainbow with a dark gap where green should be.
The jelly itself shows the same colour, deep where it is thick and pale at its edges, and glows that colour where the beam passes. Different pieces keep the flavour they were cut from; switching flavour recolours every piece.

## Game

- Live readouts computed from the optics: **angle of incidence** (degrees), **spread** (degrees between the red and violet rays leaving the jelly), **colours on the screen** (how many of the seven named bands are visible), **pieces**. Values update smoothly, not every frame.
- Milestones, a small toast once each: "First rainbow" (light on), "Shiver" (poke while the light is on), "Trapped light" (first total internal reflection), "Two rainbows" (two separate fans on the screen), "White again" (Second prism recombines the beam), "Colour thief" (a flavour removes at least three bands), "Widest rainbow" (spread above 20 degrees with Squish).

## Secret

- **The cover.** With one whole prism, Clear flavour and the light on, turn the prism and the light so the beam enters at the left face and the fan leaves the right face perfectly symmetrically (minimum deviation, within 1.5 degrees), with the camera at the default view, and hold it still for 2 seconds. The room lights dim to black except the beam and the fan, the haze thickens a little so the white beam and the coloured fan read as two clean flat shafts, a soft **slow heartbeat** thumps twice, and a thin square outline fades in around the frame for 3 seconds, framing the scene like a record sleeve, before everything fades back. A small badge: "Perfect alignment." Never hint at it in the UI, and never show any band name, title or artwork.

## Sound

Every action has a **synthesised sound**, generated live with the **Web Audio API** (oscillators, filtered noise, envelopes). **No audio files, no samples.**

- **The rainbow plays:** while the light is on, a soft, airy pad chord sounds with one quiet sine voice per visible band (seven notes of a major-pentatonic-friendly scale, red lowest, violet highest), each voice's loudness following that band's brightness on the screen. When the rainbow shivers, the chord shimmers (a little vibrato driven by the wobble); when a flavour eats bands, their notes fall silent; when the light is trapped, the chord drops to a low muffled hum; two rainbows give a doubled, chorused chord; Second prism resolves to a single pure tone. Keep it soft and beautiful, never a drone that tires.
- Plus: a soft projector click and a rising "whoom" as the beam streaks in, squishy wet sounds for pokes and squishes, a jiggly rattle for Wobble, a clean wet "shlick" for the knife, a glassy slide for Second prism, a camera shutter for Pose, the heartbeat for the secret.

Rules: create the `AudioContext` only on the first user gesture and never before; resume a `suspended` context on interaction; master gain into a compressor, no clipping; cap at about 16 voices (the pad counts as its seven); wrap every audio call in `try/catch`. Sound on by default; a **speaker button** and `M` mute it.

## Look

Aim for a premium physics-of-light film in a dark studio, not a game asset.

- **Colour:** sRGB output, **AgX** tone mapping, exposure about 1.1, HDR internally: the beam and the rainbow are bright emissive light, far above the room, and must stay saturated (no washed-out pastel rainbow, no clipped white).
- **Lighting:** the beam and the rainbow are the stars, so keep the room light low: a procedural environment (`RoomEnvironment` through `PMREMGenerator`, no HDR files) at low intensity for reflections on the jelly, a dim warm key from the front-left, a very soft fill, and a cool rim from behind that outlines the prism's edges. Soft shadows (2048, 1024 on phones), a contact shadow under the prism and the projector, and the prism's own soft shadow from the beam on the table.
- **Materials:**
  - **Jelly:** a physical material with transmission 1, thickness from the real geometry (back faces into a thickness pass or the material's thickness node), IOR 1.34, its `dispersion` set to match the traced rays, attenuation colour and distance per flavour, roughness about 0.05, a strong Fresnel sheen at grazing angles, tiny bubbles and faint grain inside. Never glass-hard, never plastic, never opaque.
  - **Cut faces:** slightly rougher and wetter than the outer surface, so fresh cuts read at a glance.
  - **Table:** matte graphite stone; **screen:** matte warm mid-dark grey, so the rainbow on it is the brightest thing in the frame; **projector:** matte black with a brass rim.
- **Post:** bloom on the beam, the fan and the rainbow (strength about 0.5, threshold above 1, so the bands glow softly and the jelly's hot spots sparkle), a light vignette. No heavy chromatic aberration, grain or lens dirt.
- **Backdrop:** a deep charcoal studio, **never pure black**: the table, the screen and the prism's outline must stay readable at a glance (the site shows previews as tiles on a dark board).
- **Framing:** the default camera, slightly above and a little to the front-left, shows the prism, the incoming beam and the whole rainbow on the screen together, filling about 75% of a **square** frame; the prism is large and clearly a triangle.
- **Motion:** everything eases, overshoots a little and settles; nothing pops; the rainbow moves only because the light or the jelly moved.

## Interface

A dark, quiet interface that stays out of the way; the light is the hero and UI never covers the prism or the rainbow on desktop.

**Top left:** small caps `POKEABLE · SPECIMEN`, a large italic serif heading on two lines: *Jelly* / *Prism.*, and caption lines "White light in." / "Rainbow out." / "Poke it and watch."

**Top right:** a live status dot with `WEBGPU · LIVE` or `WEBGL2 · LIVE`, a **speaker icon button** (`ON` / `MUTED`), and `PAUSED` when paused.

**Right panel: control card "Prism"**, a floating card about 240px wide with 20px corners, dark charcoal, soft shadow, no hairlines, plain system sans, bold sentence-case labels, and an accent that is a thin rainbow gradient line under the title (the only colourful UI). Tools as rounded tiles with small drawn icons and key numbers (`1` Hand, `2` Knife, `3` Light, `4` Turn), **Light** as a big switch, **Second prism** as a big button, **Sweep** as a switch, **Wobble**, **Pose** and **Reset** as chips, flavours as round swatches, sliders for **Firmness**, **Dispersion**, **Haze** and **Sound** (6px rounded track, round white thumb, monospace value chips), switches for **Slow motion** (¼ speed) and **Cinema**, and **Pause / Resume** as a full-width pill. Collapsible to a small button; a bottom sheet on narrow screens.

**Bottom left:** a one-line hint per tool ("Tap to poke. Hold the top to squish.", "Swipe through the prism to cut it.", "Drag to move the light.", "Drag to turn the prism."), then the readouts.

**Cinema** (`C`) hides all UI and the cursor; a tap without drag, `C` or Esc brings it back. Respect `prefers-reduced-motion` for UI transitions and camera moves; the jelly and the light still respond fully.

If neither WebGPU nor WebGL2 works, a clear, designed message.

## Camera and keys

- Orbit with right-drag or two fingers within limits (never below the table, never behind the screen); zoom 0.7x to 1.6x; `0` resets the view. No automatic camera drift.
- Keys: `1` to `4` tools, `L` Light, `X` cut the prism across its length in the middle (the same as a Knife swipe), `N` Second prism, `W` Sweep, `S` Wobble, `F` Pose, `P` next flavour, `C` Cinema, `M` mute, Space pause, `R` reset, `0` reset view.

## Test hook

If the URL has `?test=1`, expose `window.__toy` with no visible change: `setTool(name)`, `poke(x, y, strength)`, `squish(amount, ms)`, `drag(x0, y0, x1, y1, ms)` (with the current tool, in canvas pixels, through the same code path as a real drag), `cut(x0, y0, x1, y1)`, `light(on)`, `lightAngle(deg)`, `turn(piece, deg)`, `secondPrism(on)`, `sweep(on)`, `wobble()`, `pose()`, `flavour(name)`, `set(name, value)` for every slider and switch, `reset()`, and `getState()` returning `{ renderer, fps, tool, pieces, light, incidence, spread, bands, rainbows, trapped, secondPrism, sweep, flavour, asleep, secret }` (`rainbows` = separate fans on the screen, `trapped` = total internal reflection right now).

## Demo mode (required)

If the URL has `?demo=1`, auto-play a 4-second loop with no user input: the object drops in, wobbles, gets poked or sliced once, then settles.

For this toy that means:
- Hide all UI and the cursor. Never create an `AudioContext`.
- One fixed quality preset (no quality scaler), the Clear flavour, the default Dispersion, the fixed default camera, no randomness.
- A deterministic loop restarting every 4.0 s of real time from the identical state; drive the simulation from real elapsed time with the fixed timestep so the loop lasts 4 s at any frame rate:
  - **0.0 to 0.5 s:** the dark room, light off; the prism drops onto the table and lands with a deep squash and a wobble.
  - **0.5 to 0.9 s:** the light switches on: the white beam **streaks in** through the haze, hits the prism and the fan **bursts open**, the rainbow landing on the screen.
  - **1.0 to 1.9 s:** an invisible finger pokes the top edge: the prism wobbles and **the rainbow shivers and bends** on the screen, the fan trembling in the air, ringing down.
  - **2.0 to 2.2 s:** a knife slices straight across the prism's length in the middle.
  - **2.2 to 3.2 s:** the two halves slide apart a little and the front one turns about 8 degrees: **the rainbow splits into two**, one above the other, both wobbling. The most striking moment.
  - **3.2 to 3.7 s:** both halves settle; two clean rainbows on the screen.
  - **3.7 to 4.0 s:** the light fades out and the rainbows drain away, ready for the loop to start again.
- Start moving within 1 s of load. It is recorded in a slow software renderer: **the white beam, the fan and the rainbows on the screen must read clearly at 15 fps**.
- Support `?demo=1&t=<seconds>` to freeze the demo at that time; `?demo=1&t=1.2` shows the shivering rainbow, `?demo=1&t=3.4` the two rainbows.

## Before you finish

Self-check against this list and fix anything that fails:
- **The light test:** poke the prism and watch the rainbow: it must bend and shiver in step with the visible wobble of the faces, settle when the jelly settles, and be perfectly still at rest. Squish it: the rainbow must stretch. Cut it: two pieces must give two rainbows. Turn the light past the critical angle: the rainbow must slide off and vanish, and return. Switch flavours: bands must disappear as listed. If the rainbow moves on its own, ignores the jelly, or looks like a texture, fix the optics before anything else.
- **The scroll-stop test:** still frames of the beam streaking in, the full fan, the shivering rainbow, a deep squish, the two rainbows, trapped light glowing inside, the Second prism making white again, every flavour, and the secret. Each must look like a frame from a premium light film: a clean bright beam, a smooth, saturated rainbow (never dotted, banded into stripes or pastel), a clear glossy jelly prism, a dark room that is still readable. If any frame looks flat, washed out, plastic or lost in black, fix the look.
- Poke every part 50 times fast, squish it flat repeatedly, stretch it from every side, cut it four ways and keep cutting, cut through the middle of a squish, turn every piece all the way round, swing the light along its whole arc while poking, leave Sweep on for five minutes: never an exploded or inverted body, jitter at rest, a flickering or torn rainbow, rays escaping through the floor, NaNs, a piece through the table, or a crash. After every test, Reset brings back one whole prism and a clean rainbow.
- Every tool, button, switch, slider, flavour and key; pause and resume; Slow motion; Cinema; resizing to phone width with touch; the secret by hand.
- Sound: every action has its effect, the rainbow chord follows the bands, nothing plays before the first gesture, mute works, no clipping.
- It works inside `<iframe sandbox="allow-scripts allow-pointer-lock">` with storage blocked.
- `?demo=1` matches the timeline, loops cleanly, shows no UI and plays no sound; `?test=1` exposes `window.__toy`.
- No console errors or WebGPU validation errors on WebGPU **and** on WebGL2 (force the fallback to check), and it stays smooth on a mid-range phone.

Put the traced rainbow first: it must always follow the real shape of the jelly. Poking the prism and watching the colours shiver on the screen should be the best part.

More like this