PROMPT:
Build "Island Jelly", an interactive material study: one self-contained HTML file, native WebGPU + WGSL, no libraries, no external requests (no fonts, CDNs or images). It belongs to an editorial series called "MATERIAL STUDIES": quiet paper backdrop, serif italic headline, small uppercase sans labels, a glassy control panel. The subject is a square slice of jelly-sea cut out like a dessert: a small sandy island with three gummy palms in a block of tinted, wobbly "sea jelly". You can stir it with a finger, tip the whole block, and throw coconuts and pebbles into it.
━━━ 1. DELIVERABLE & CONSTRAINTS ━━━
- One HTML file (~120 KB). CSS, JS and WGSL all inline. Plain JS, no build-time dependencies in the output.
- WebGPU only. If navigator.gpu, the adapter or the device is missing, or init throws, show the fallback overlay (copy in §12). Handle device.lost the same way.
- Fixed-step simulation: water at 1/120 s, bodies at 1/240 s, with an accumulator and a max catch-up per frame. Render once per rAF.
- Cap the backbuffer at ~2.4 megapixels (scale DPR down if needed).
- Expose window.__ij, a debug hook with state, counts, water energy and a way to step/drop/reset, for automated tests.
━━━ 2. WORLD & TERRAIN (block-local coordinates, y up, block base at y=0) ━━━
- Square footprint: side 5.2 (HALF = 2.6). Rest water level WL0 = 1.3. Block height 2.0.
- Water grid GN = 168 cells per side (GDX = 5.2/168). Real gravity 9.8 is used for bodies; the water uses a softer effective g (see §3).
- terrainH(x,z), a smooth analytic function:
· Seabed that slopes gently along the diagonal: 0.88 − 0.34·smoothstep(−3.8, 4.2, x+z), plus low-amplitude value noise and fine sand ripples.
· Island centred at (−0.3, −0.5), elliptical radii 1.75 × 1.48 (ix=(x+0.3)/1.75, iz=(z+0.5)/1.48, r=length). Two-tier mound: +0.3·(1−smoothstep(0.1,0.9,r)) + 0.45·(1−smoothstep(0.6,1.2,r)). Result: the island covers ~12% of the square, its top sits ~0.2 above the water, and its beaches are gentle (pebbles must be able to rest on them).
· A submerged sandbar at (1.35, 1.05) where waves visibly slow and steepen.
· Two small reef rocks at (−1.7, 1.35) and (1.75, −1.2).
· Water depth along the four cut walls should be roughly 0.4–0.76, so the sea reads as a thick slab in section.
- Provide terrainNormal (finite differences) and terrainGrid (heights sampled at water-cell centres).
━━━ 3. THE SEA — CPU SHALLOW-WATER JELLY ━━━
Linear shallow-water equations on a staggered grid: heights h at cell centres, u on x-faces, w on z-faces.
- Depth D = level − terrain at each cell; cells with D < 0.012 are dry, and those faces are closed walls. Face depth = min of the two neighbours. Wave speed is √(g·D), so waves slow down over the bar and around the island.
- Effective gravity: g = 3.4·(0.75 + 0.5·firm). This makes the sea slower and springier than water.
- Surface tension: a σ·∂(∇²h) term in the momentum update, with σ = 0.00012·(0.5 + firm). It gives short ripples a jelly snap. Keep σ small; larger values blow up.
- Damping: velocities are multiplied by exp(−(0.18 + 1.6·damp)·dt).
- Tilt forcing: the in-plane component of gravity in the block frame is added as a body force at 60% strength. "Jelly water half-holds its shape": it sloshes to the low side and settles level again.
- Continuity: h −= dt·div(D_face·vel)/dx. Clamp h to [−0.9·D, 0.45].
- Mean-level relaxation: subtract mean·min(1, 1.5·dt) each step. This undoes drift from clamps and finger strokes.
- addVolume(x, z, vol, radius): a Gaussian bump normalised to the exact volume, wet cells only. Use it for splashes, finger dents, floating bodies' displacement and bubble pops.
- stir(x, z, speed): addVolume(x, z, −0.0016·(0.35 + clamp(speed,0,3)), 0.085). A moving dent whose wake makes the waves.
- Tide: setLevel(WL0 + tide), tide in ±0.1. Recompute D and the face depths; the shoreline moves.
- Idle shimmer: occasional tiny ripples so the sea is never dead flat.
- Each frame, pack an rgba32float texture of (h, dh/dx, dh/dz, terrainH) and upload it. In WGSL, sample it bilinearly by hand with textureLoad, because rgba32float is not filterable.
- energy = Σh². It drives the "sea liveliness" meter in the panel.
━━━ 4. THE BLOCK — TILT & JELLY WOBBLE ━━━
- The whole block (sea, sand, palms and bodies inside it) can be tipped about x and z by dragging its side. TILT_MAX = 0.17 rad (~10°).
- While held, a stiff spring follows the pointer target. On release, a spring (K = 70, C = 7.5) returns it to level, with a soft "thunk": squash velocity −= |tilt velocity|·0.5.
- Lift the block so its lowest bottom corner always touches the studio floor.
- Keep a transform M / Minv plus helpers toWorld, toLocal and dirToLocal. Local gravity g_local feeds the water and the bodies.
- Jelly wobble: two damped oscillators.
· Shear: ω = 2π(2 + 2.2·firm), driven by −(block angular acceleration)·0.9.
· Squash: ω = 2π(2.6 + 2.4·firm).
· kick(dir, squash) adds shear velocity d·0.25 and squash velocity squash·0.3. Splashes and heavy landings call it.
· Apply the wobble in the vertex shader (a wob() function) to everything inside the block: shear grows with height above the base, and squash is volume-preserving. The block visibly quivers after a release or a big splash.
━━━ 5. PALMS (procedural gummy palms) ━━━
- Three palms in block-local coords:
· (−0.8, −0.54), height 1.2, 9 fronds
· (0.06, −0.28), height 1.05, 9 fronds
· (−0.32, −1.04), height 0.88, 8 fronds
- Trunks: curved, slightly leaning, with ringed segments (gummy candy look). Fronds: arched pinnate leaves with leaflets on both sides, drooping at the tips.
- Vertex layout, 12 floats: pos3, nrm3, uv2, extra4 = (palm index, bend weight 0 at the base → 1 at the crown, flutter weight, phase).
- Each crown is a 2D bend spring, K = (2π·1.05)², damped. It is driven by:
· tilt gravity (it leans downhill when the block tips)
· block angular acceleration (whips on release)
· a gentle breeze (off under prefers-reduced-motion)
· the user's push (drag a palm; it springs back when released)
- Deform on the GPU from a small per-palm uniform: bend × weight², plus a leaf flutter.
- coconutSpots(): 3 per palm under the crown, which move with the bend.
━━━ 6. BODIES — PEBBLES & COCONUTS (spheres) ━━━
- Kinds:
· pebble: radius ~0.075–0.1, density 2.4, pastel candy colours, sinks
· coconut: radius ~0.11, density 0.62, floats and bobs
- Cap 40 bodies.
- Reset puts 9 coconuts hanging in the crowns. They stay attached and follow the crown bend until picked or knocked off.
- Reset also places 7 pebbles on the beach automatically. Search outward from the island centre along golden-angle rays for a spot just above the waterline (WL0 + 0.05…0.09) with normal.y > 0.93.
- "Drop a pebble" / "Drop a coconut" spawn over random deep water at y = 2.7 (local). Space drops a random one.
- Per-step physics, in the block frame with local gravity:
· Buoyancy = submerged volume (spherical cap vs the sampled surface height) × ρ_water × g, directed along the local water-surface normal, not straight up. Floaters slide downhill on a tilted sea correctly.
· Linear and quadratic drag relative to the local surface flow (u, w); vertical drag damps the bob.
· Displaced volume: each step, push Δ(submerged volume) into the water with addVolume. Bobbing makes rings, and a coconut sitting in a wave rides it.
· Water entry: crater (negative volume) + a ring of bubbles + block.kick() scaled by impact speed and mass.
· Sand contact against terrainH/terrainNormal: restitution, plus Coulomb-like rolling resistance (μ 0.62 pebble, 0.42 coconut). Pebbles on the beach must stay put at rest and only roll on a real tilt.
· Collisions with the four inner walls of the block, the palm trunks (capsules) and each other (sphere pairs).
· Sleep when slow for a while; wake on contact, on waves or on tilt.
· A visual squash spring per body on impacts.
- Picking: grab a body, carry it on a plane under the pointer, throw it with the release velocity (from a smoothed pointer history).
- Tally: Afloat (touching the water, not resting on sand), Sunk (fully underwater on the bottom), Ashore (on dry sand or in a crown).
━━━ 7. BUBBLES ━━━
- Up to 260 rising bubbles (instanced, small refractive spheres). They come from splashes, from sinking pebbles and occasionally from the seabed. They rise with a wobble and pop into a small ripple (addVolume ±) at the surface.
- 80 decor bubbles held still inside the jelly, which give it a "set dessert" look. They move only with the block wobble.
━━━ 8. RENDERING PIPELINE (WebGPU) ━━━
Targets:
- MSAA 4× HDR rgba16float colour
- a second MSAA colour target holding linear view distance (resolved to an r32float or rgba16float)
- depth24plus
Passes, in order:
1. Shadow pass: sun-space orthographic depth map, 2048², extent ±4.1. Contains terrain, soil, palms and bodies. Sample it with 3×3 PCF and a comparison sampler.
2. Caustics pass: a grid with one vertex per water cell. Refract the sun direction through the heightfield normal, intersect with the seabed (terrainH), and splat additively into an rgba16float 768² texture over the footprint. Intensity = area ratio (undisturbed / refracted), computed with dpdx/dpdy of the refracted position in the fragment shader; clamp it.
3. Opaque pass: studio backdrop (paper sweep with a soft vignette), studio floor, terrain, soil cut walls, palms, bodies and the instanced decor bubbles. Write colour and linear distance.
4. Water back pass: back faces only (cull front) of the water surface + water cut walls into an r32float distance target.
5. Water front pass. Copy the opaque scene to a sampled texture first (fsCopy), then draw the surface and walls (front faces):
· Screen-space refraction: offset the scene lookup by the surface normal.
· Beer–Lambert absorption: exp(−σ·path), with path = min(opaque distance, back-face distance) − front distance. When the exit is a far cut wall, add +0.3 to the path so the slab reads deep.
· An in-scatter tint toward the flavour tint.
· Fresnel reflection of a soft studio environment, plus a sun specular.
· Light shafts: 6 caustic samples along the view ray inside the water.
· Thin bright rim lines where the surface meets the cut walls, a meniscus darkening/brightening along that line, and faint vertical corner lines.
6. Composite: a tone shoulder above 0.8, linear→sRGB, and a small blue-noise/hash dither.
Shading notes:
- Terrain (sand): warm sand with fine grain and sand ripples. Wet-sand darkening just below and above the waterline. Caustics on the submerged sand (attenuated by depth). Sun shadow.
- Soil cut walls: candy strata in section (bottom to top): chocolate, caramel, a thin pink shell band, vanilla, then a wet-sand band up to the terrain line. Slightly wavy layer boundaries, and small pebbles pressed into the section.
- Palms: gummy, glossy, a little translucent. Leaf and trunk colours from the flavour palette.
- Bodies: coconuts with a fibrous darker shell and a hint of a lighter cap; pebbles as smooth pastel candy stones.
- Floor shadow of the whole block: for floor points, cast a ray toward the sun and do a ray/AABB test against the block. Tint by the absorption along the path through the water, fleck with caustics, and cap ≤ 1 (the shadow must never be brighter than the lit floor).
- The vertex shaders of everything inside the block apply wob() before the block transform.
━━━ 9. FLAVOURS (palettes) ━━━
Each flavour sets the water absorption σ (rgb), the in-scatter tint, plus sand, strata s0–s4, leaf, trunk, coconut and pebble colours.
- Lagoon (default): σ = [2.3, 0.52, 0.42], tint = [0.03, 0.34, 0.37]. Turquoise sea.
- Raspberry: σ = [0.34, 2.2, 1.25], tint = [0.44, 0.05, 0.17]. Pink-red sea.
- Lime: σ = [1.45, 0.5, 2.6], tint = [0.12, 0.25, 0.035]. Yellow-green sea.
Switching is instant. The swatch buttons show radial-gradient candy dots:
- Lagoon:
#b8f4ee →
#2bb5b0 →
#0d6f73
- Raspberry:
#ffc6d6 →
#e2477a →
#93184a
- Lime:
#efffb0 →
#a6d33a →
#5b8a12
━━━ 10. CAMERA & INTERACTION ━━━
- Orbit camera: start az 0.62, el 0.42, target (0, 0.85, 0), FOV 34°.
- The distance is fitted to the viewport: about 11.2 on desktop; portrait phones fit wider, up to 28. Zoom range 3.5–28.
- On desktop, shift the projection so the block sits in the free space between the masthead (left) and the panel (right).
- Pointer Events with pointer capture. Pinch to zoom, wheel to zoom, right-drag always orbits. "Reset view" eases back.
- Pick priority on pointerdown:
1. a body (beats a palm if within ~0.4 of the hit)
2. a palm (push its crown)
3. water, only if the ray enters through the top surface (stir)
4. the block's side or edge (tilt)
5. empty space (orbit)
- Tilt mapping: screen dx tips the top toward camera-right; dragging down tips the top toward the viewer.
- Stirring: sample the stroke along the pointer path and call stir() at each point with the stroke speed.
- Hover cursors: is-pick (body/palm), is-water (crosshair), is-tilt (move), else grab/grabbing.
- Keyboard: Space drops a random item.
- prefers-reduced-motion: no breeze, no idle shimmer, gentler wobble.
━━━ 11. CONTROLS → SIM ━━━
- Firmness 0–100 (default 50) → firm 0..1. Affects the water g and σ, and the wobble frequencies.
- Wave damping 0–100 (default 30) → damp 0..1.
- Tide −100..100 (default 0) → ±0.1 level. Its output shows centimetres (e.g. "+4 cm").
- Reset: water flat, block level, bodies back to 9 coconuts in the crowns and 7 beach pebbles.
- Pause: freezes the sim and dims the status dot.
- The meter bar width follows the smoothed sea energy.
━━━ 12. PAGE & COPY (use exactly) ━━━
Palette:
- --paper
#ece9e3, --ink
#241f1d, --ink-2
#5b544e, --ink-3
#958c84, --rule
#d5d0c8
- --glass rgba(247,245,241,.86), --accent
#178a8a, --button
#352b29 (hover
#4a3c39)
- Serif stack: Iowan Old Style / Palatino / Georgia. Sans: the system stack. Mono: ui-monospace.
Layout:
- Full-screen canvas (touch-action:none). Overlay layers with pointer-events:none, except the panel and status (pointer-events:auto). Without this the panel won't be clickable.
- Masthead, top-left:
· eyebrow "Material Studies"
· H1 in serif italic, huge (clamp(56px, 8.4vw, 116px)): "Island<br>Jelly."
· a short rule
· caption "A sandbar, three palms and a sea that wobbles back."
- Status pill, top-right: dot + "WebGPU · Live" / "WebGPU · Paused" / "WebGPU · Unavailable" (green / amber / red dot).
- Panel, right, 268px wide, glass with a backdrop blur. Sections:
· "The sea": two dark buttons "Drop a pebble" | "Drop a coconut"; a 2px accent meter; a tally row "Afloat 0 · Sunk 0 · Ashore 0" (numbers in mono).
· "Flavour": three square swatches "Lagoon", "Raspberry", "Lime" (aria-pressed).
· Sliders "Firmness" (50), "Wave damping" (30), "Tide" (0), each with a mono output; thin 1px track, round paper thumb.
· Three outline buttons "Reset" | "Pause" | "Reset view".
- Footer, bottom-left:
· eyebrow "How to play"
· serif italic "Stir the sea. Tip the block. Throw a coconut."
· small line: "Drag across the water to make waves · drag the side of the block to tip it and watch the sea slosh · pick up a coconut or a pebble and throw it · drag empty space or right-drag to orbit · scroll or pinch to zoom"
- Fallback overlay: eyebrow "Material Studies", H2 "Island Jelly.", and the text "This study needs WebGPU, which this browser doesn't seem to offer. Try a recent desktop Chrome or Edge, or Safari 18 or later, with hardware acceleration switched on."
- Canvas aria-label: "A jelly island in a block of sea you can stir, tip and throw things into".
━━━ 13. RESPONSIVE ━━━
- ≤900px: the panel becomes a bottom sheet with a 54px sticky handle bar ("Controls" + grip). It is collapsed by default and opens to ≤62vh. While it is open, hide the caption and the footer.
- Smaller masthead (H1 clamp(40px, 13vw, 58px)); the footer sits above the sheet bar and respects safe-area insets.
- Short landscape phones: hide the caption and the footer, H1 34px.
- Desktop with short height: the panel scrolls; hide the small how-to line.
- The block must be fully framed on every aspect ratio, including tall portrait (use the wider fit distance).
━━━ 14. QUALITY BAR ━━━
- 60 fps on a mid laptop GPU. No NaNs: if the water goes bad, reset it silently.
- Waves must look like jelly: slower than water, with a springy rebound, visibly refracting at the bar and the island, reflecting off the cut walls, and caustics dancing on the sand.
- Tip and release: the sea sloshes to the low side and back, the palms lean and whip, floating coconuts slide downhill, and the whole block quivers and settles. Beach pebbles stay put unless the tilt is real.
- Cut walls read as a dessert cross-section, with a clean bright rim line where the sea meets the glassy wall.
- Coconuts float and bob, pebbles sink with bubbles, and every entry makes a ring of waves.
- Verify with headful Playwright Chromium (xvfb-run, --enable-unsafe-webgpu, SwiftShader is fine):
· it loads with no console errors
· __ij is ready
· stir, tilt, drop and throw all work
· the tally updates
· the flavours switch
· the sliders apply
· pause and reset work
· the mobile sheet opens and closes
· the panel is clickable
· the fallback appears when navigator.gpu is removed