Rubik's Jelly
jellycube
Winner
CheapestFastest
Sonnet wins. All four are real, solvable Rubik's cubes. Sonnet's looks most like jelly (deep translucent colour, suspended bubbles) for less than Opus, which wobbles hardest on every twist. Astra and Gemini went for solid candy colours; Gemini did it for $0.55.
Who won? You pick
Pick the build you liked best to see what everyone else thinks.
Scorecard
Sonnet1Opus1Astra0Gemini0
| Criterion | Sonnet | Opus | Astra | Gemini |
|---|---|---|---|---|
| Looks like jelly | Deep translucent flavours with suspended bubbles and darker depth | Glossy candy cubes, bright but more opaque | Bright classic Rubik's colours on a ceramic plate: glossy and solid, the least jelly-like | Opaque pastel cubes, soft and matte; framed so close the cube fills the shot |
| Wobble on a twist | About 13° overshoot with a rock-back, a little stiffer | About 15° overshoot, the layer shears and the whole cube rocks back | Spring-loaded pieces: the turning layer leans and shears, then settles (softness slider) | Biggest U swing of the four in the demo; turns on short drags |
| Cube logic | Exact: 10,000 moves and inverse return to solved, 2x2 to 4x4 drags all correct | Exact: 10,000 random moves stay legal, 476/476 drag tests turn the right layer | Real move model with 2x2, 3x3 and 4x4 sizes and drag-to-twist | Exact: 60 random moves plus inverse return to solved; drags turn the right layer, but 3x3 middle slices are logged as r / u |
| Scramble and Solve | Seeded scramble; Solve 3 to 6.5 s with a progress counter | Seeded scramble; Solve 5.5 s on 3x3 with an N / M counter | Seeded scramble (12 / 25 / 40 moves by size); Solve plays your simplified moves backwards | Seeded scramble; Solve with a progress counter |
| Easter egg | Melts into a rainbow puddle and re-forms with a badge | Melts into a puddle and re-forms | Solve it by hand (8+ moves, no Solve) and it melts and re-forms | Melts into a puddle and re-forms with the "Solved by hand" badge |
| Cost | $7.36 | $11.10 | $4.27 | $0.55 |
| Time | 1h03m06s | 1h23m15s | 1h13m37s | 32m51s |
| Overall | Winner | – | – | – |
The prompt
Build **"Rubik's Jelly"**: a real-time, interactive 3D **Rubik's cube made of jelly** that people **twist**, **spin**, **poke**, **drop**, **scramble** and **solve**. Every turn makes the cube wobble and jiggle, the cubelets squish against each other, and yet it stays a **correct, solvable Rubik's cube**, with **synthesised sound**, as ONE self-contained HTML file rendered with **WebGPU (WGSL)** and an automatic **WebGL2** fallback.
It should feel like a real puzzle you could actually solve, carved out of the wobbliest dessert on the table: every twist lands with a juicy overshoot and settles back into a perfect cube.
## 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:** a correct cube state and drag-to-turn that always turns the layer under your finger in the direction you drag, from any camera angle; the jelly twist (overshoot, squish, wobble) that always comes back to a perfectly aligned cube; translucent jelly with colours you can read at a glance; the demo mode.
2. **Must work:** Spin, Poke and Drop tools, Scramble / Solve / Undo / Reset, sizes 2x2 / 3x3 / 4x4, timer, milestones, sound, both renderers, the control card, the Easter egg.
3. **Polish:** coloured light through the jelly on the plate, a move ticker in standard notation, the whole cube rocking back against each twist, best times per size.
Never ship a feature half-working: if something from tier 3 can't be done well, leave it out cleanly.
## Deliverable
- One self-contained `.html` file with all JavaScript, WGSL, GLSL and CSS inline. Start it with `<!doctype html>`, `<meta charset="utf-8">` and a viewport meta.
- **No external requests of any kind**: no libraries, CDNs, web fonts, images, textures or audio files. Generate everything procedurally in code (sound included). Use system font stacks.
- It must run inside a sandboxed iframe (`sandbox="allow-scripts allow-pointer-lock"`). Do not use `localStorage`, cookies, `alert`, pop-ups or top-level navigation, and wrap any storage access in `try/catch`.
- This must be a live simulation, not a video, a pre-baked animation, a static render or a 2D fake.
- **Test hook:** if the URL has `?test=1`, expose `window.__toy` (no visible change) with: `move(notation)` (e.g. `"R"`, `"U'"`, `"F2"`, `"r"` on 4x4), `drag(x0, y0, x1, y1)` (a twist through the same code path as a real pointer drag, in canvas pixels), `pick(x, y)` (the cubelet and face under that point), `spin(dx, dy)`, `poke(x, y, strength)`, `drop()`, `scramble(seed)`, `solve()`, `undo()`, `reset()`, `setSize(n)`, `setTool(name)`, and `state()` returning `{ size, faces, solved, solvedFaces, moves, history, busy }` where `faces` is the 6 faces in U R F D L B order as colour letters and `busy` is true while any turn is still animating.
## The cube
- A **3x3 cube** by default: **26 jelly cubelets** (2x2: 8, 4x4: 56; only the outer shell exists, the hidden middle is a small frosted jelly core you only glimpse mid-turn), sitting on a **white ceramic plate** in a soft pastel studio (a seamless light lilac backdrop), seen from a classic three-quarter view from above so the **top, front and right faces** all show. The cube fills about 55% of a square frame.
- **Cubelets:** rounded jelly cubes (corner radius about 18% of the edge) with a small gap (about 4%) between neighbours. Each cubelet is one piece of jelly whose outward faces carry **thick coloured flavour layers** (about a quarter of the cubelet deep, so the colour reads as a slab of jelly, not a sticker), meeting at mitred seams on edges and corners. Inner faces are clear, faintly frosted jelly.
- **Six flavours**, standard scheme: white on top (coconut milk, soft and milky), yellow on the bottom (lemon), green front (lime), blue back (blueberry), red right (cherry), orange left (tangerine).
- **Readability is a hard rule:** from the default view all 27 visible facelets must read at a glance. Red vs orange and white vs yellow must never be confused, and translucency must never let colours from behind bleed through enough to muddle a face.
## Cube logic
- The puzzle state is **exact and discrete**: each cubelet has an integer slot and an integer orientation (a 3x3 rotation matrix of 0 / ±1, or a lookup of the 24 orientations). A quarter turn permutes slots and multiplies orientations exactly. Animation and jelly are purely visual on top of that state.
- After **10,000 random moves** the cube must still be perfectly aligned, with no floating-point drift, and `state()` must always describe a legal, solvable cube.
- A twist is **committed to the state when the layer passes the halfway point of its final quarter**, so a later move always sees the right state.
- Up to 3 moves can be queued. Parallel layers may turn together. A turn on a different axis starts as soon as the current layer first reaches its target angle; its remaining overshoot carries on as cubelet wobble, never as misalignment.
- Moves are recorded in **standard notation** relative to the cube's own frame (U white, F green): `U D L R F B`, with `'` and `2`, plus inner slices on 4x4 as lowercase (`r`, `u`...). Spinning the whole cube is not a move.
## Jelly physics
- **Each cubelet is a soft body:** a small lattice (about 3x3x3 nodes) solved with XPBD or shape matching with volume preservation, each node pulled towards its target pose in the cube by a spring. Render a smooth rounded-cube mesh (about 600 to 1,500 vertices) embedded in the lattice, never the lattice itself.
- **Twist:** the layer angle follows the finger while dragging. On release, a **damped spring** drives it to the target quarter turn with a jelly **overshoot of about 8 to 18 degrees** at default softness, then 1 to 3 visible oscillations. The turning cubelets lag behind the layer (they shear like jelly being dragged), their faces rub against the still layer with a visible squish, and the rest of the cube **rocks back a few degrees** against the twist and wobbles home.
- **Contact:** neighbouring cubelets push on each other and bulge slightly at the gaps, but never visibly pass through each other or the plate.
- **Never breaks:** deformation is clamped (no node moves more than about 30% of a cubelet's edge from its target), so the cube always reads as a cube, and everything returns to exact rest within about 2 to 3 seconds (faster when firmer). No jitter at rest, no slow creep, no explosions.
- **Fixed simulation timestep** (about 1/120 s) with bounded substeps, decoupled from frame rate. On a slow frame, drop simulation time rather than exploding.
## Tools
A tool picker with **Twist**, **Spin**, **Poke** and **Drop**. The default tool is Twist. A press that ends within 6 px and 450 ms counts as a tap. Right-drag or two fingers orbit the camera with any tool. Use pointer events with **pointer capture** so drags never drop, and support mouse, pen and touch.
### 1 Twist
- Press on a cubelet face and drag across it: after about 10 px, project the two in-face axes of the face you pressed to the screen, lock to the one that best matches the drag, and turn **the layer through that cubelet around the axis perpendicular to both**, in the direction of the drag. This must work from any camera angle and any cube orientation, including looking from below or with the cube upside down.
- Pick against the logical cube (slots at rest plus any turning layer's angle), never the wobbling mesh, so jelly motion can never select the wrong layer.
- Dragging further turns 180 degrees. Release snaps to the nearest quarter turn; a fast flick past about 30 degrees carries on to the next quarter. A drag that ends back at 0 records no move.
- Dragging off the cube spins the whole cube (as Spin). A tap pokes (as Poke).
### 2 Spin
- Drag anywhere to rotate the whole cube like a trackball, with inertia on release; the jelly lags behind and wobbles as it starts and stops. Arrow keys turn the whole cube 90 degrees.
### 3 Poke
- A tap presses a dent into the cubelet under the finger that rebounds with a wobble; neighbours jiggle in a ripple that fades across the cube. Holding presses deeper and the cubelet bulges sideways (volume preserved); dragging smears the dent across faces.
### 4 Drop
- A tap lifts the cube a little and drops it on the plate: it squashes about 15%, every cubelet ripples, it bounces once or twice and settles, still perfectly aligned. A hard drop must never scramble or misalign anything.
## Buttons, sizes and goals
- **Scramble:** 25 random moves on 3x3 (2x2: 12, 4x4: 40), never the same axis twice in a row, played fast (about 0.1 s each) with the whole cube jiggling. Seeded and reproducible.
- **Solve:** plays a real solution back to solved: the inverse of the move history, simplified first (merge consecutive turns of the same layer, drop full turns and cancelling pairs), sped up so it never takes more than about 8 s. The button shows progress ("Solving 12 / 30"). Twisting during a solve cancels it cleanly after the current turn.
- **Undo** reverses the last move. **Reset** returns a solved cube with a squishy hop and clears history and timer.
- **Size:** a 2x2 / 3x3 / 4x4 segmented control. Switching makes the old cube slump into the plate and the new one rise up out of it, solved.
- **Timer:** starts on the first twist after a Scramble and stops when solved. Keep the best time per size for the session (in memory only). A solve that used Solve or Undo is marked "assisted" and is not a best.
- **Milestones** (toasts, after a Scramble): "First twist", "First face!" (any face one colour), "First layer!" (a face plus its ring of side colours correct), "Two layers!", "Solved · 1:23 · 87 moves", "Under a minute!", "100 moves". A solved cube gives a happy jiggle and a short "Solved" celebration.
## Easter egg
- If the cube returns to solved **purely through the user's own twists** (no Solve, no Undo, no Reset) after at least 8 twists away from solved, the whole cube **melts**: cubelets sag, lose their edges and flow into a glossy rainbow puddle on the plate, then pull themselves back up cubelet by cubelet into a solved cube, with a small badge "Solved by hand. The jelly is impressed." This also fires for repeating a sequence until it comes back (R U R' U' six times). Never hint at it in the UI.
## Sound
Every action has a **synthesised sound**, generated live with the **Web Audio API** (oscillators, filtered noise, envelopes). **No audio files, no samples.**
- **Twist:** a short wet slide while turning (filtered noise following the layer speed) and a soft squelchy "thup" on the snap; the overshoot adds a low bloop with a little pitch wobble.
- **Spin:** a soft airy whoosh following the spin speed. **Poke:** a squish, deeper when held. **Drop:** a wobbly thud and a smaller one per bounce.
- **Scramble:** a fast ratchet of tiny squelches. **Solve:** each turn a rising note, ending in a bright wobbly chord. **Solved by hand:** a happy jingle.
- **Easter egg:** a gloopy descending slide for the melt and reversed bloops as it re-forms.
- **Size switch:** a slump and a pop. **Milestones:** a soft two-note chime.
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 so fast scrambles never turn into noise; wrap every audio call in `try/catch` so a failure can never break the toy. Sound is on by default; a **speaker button** and `M` mute it.
## Camera and keys
- **Camera:** right-drag or two fingers orbit within limits (never below the plate), the wheel or a pinch zooms within limits (from the default view to close enough to see the jelly grain), double-click the background or `0` resets the view and the cube's spin.
- **Keyboard:** `1` Twist, `2` Spin, `3` Poke, `4` Drop, `S` Scramble, `Enter` Solve, `Z` or Ctrl+Z Undo, `N` next size, arrow keys turn the whole cube, `C` Cinema, `M` mute, Space pauses or resumes, `R` resets.
## Rendering (WebGPU with a WebGL2 fallback)
**WebGPU (WGSL) when available, with an automatic WebGL2 fallback that looks the same.** Detect at start; if WebGPU is missing or adapter or device creation fails, use WebGL2. No libraries: write both paths yourself, sharing the scene data and the simulation. Show which one runs in the status pill.
Aim for a glossy dessert photo, not plastic and not glass.
- **Jelly:** thickness-dependent absorption (Beer-Lambert style) so colour deepens where the jelly is thicker and glows at thin edges and corners, a soft subsurface scatter, screen-space or cheap refraction of the backdrop and plate, wet specular highlights with Fresnel, and a faint internal grain and a few tiny suspended bubbles. Coconut white stays milky and opaque enough to read as white.
- **Light:** a large soft key light from upper left, a rim light, filmic tone mapping that never blows out the highlights, light bloom on the glints.
- **Plate:** glossy white ceramic with a soft contact shadow that follows the cube as it squashes and bounces, and (tier 3) coloured light through the jelly tinting the plate.
- Preallocate buffers and build all pipelines at start for the largest size (56 cubelets); never create shaders, pipelines or geometry during interaction. DPR capped at 2. An automatic **quality scaler** lowers resolution, refraction and bloom before it ever touches the jelly feel.
- Only if neither WebGPU nor WebGL2 exists, show a clear, well-designed message explaining why.
## Interface
A light interface that stays out of the way; the cube is the hero and UI never covers it.
**Top left:** small caps `POKEABLE · SPECIMEN 011`, a large italic serif heading on two lines: *Rubik's* / *Jelly.*, and caption lines "Twist it." / "It wobbles back." / "Still a real cube."
**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 "Cube"**, a floating card about 240px wide with 20px corners, soft off-white, a soft shadow, no hairlines, plain system sans, bold sentence-case title and section labels, and a **grape purple accent** (not one of the cube's colours). Tools as rounded tiles with small drawn icons and key numbers (the active one tinted and ringed in the accent), chunky sliders (6px rounded track filled with the accent, round white thumb) with live values in small monospace chips, secondary buttons as light-grey chips, on/off switches with the label on the left, and **Pause / Resume** as a solid dark full-width pill. Collapsible to a small button; a bottom sheet with rounded top corners on narrow screens.
- Tools: **Twist**, **Spin**, **Poke**, **Drop**.
- **Size:** 2x2 / 3x3 / 4x4 segmented control.
- Buttons: **Scramble**, **Solve**, **Undo**, **Reset**.
- Sliders: **Jelly softness** (firm gummy to wobbly panna cotta), **Twist speed**, **Sound**.
- Switches: **Moves and timer** (on), **Slow motion** (1/4 speed), **Cinema**.
- **Pause / Resume.**
**Bottom left:** a one-line hint per tool:
- Twist: "Drag across a face to turn it. Drag off the cube to spin."
- Spin: "Drag to spin the whole cube. Flick it."
- Poke: "Tap to poke. Hold to squish."
- Drop: "Tap to drop it on the plate."
Under it, live readouts from the state: **moves**, **time**, **solved faces** (x / 6), and (tier 3) the last 8 moves in notation. Values update smoothly, not every frame.
**Bottom right:** a collapsible **"How it works"** panel, 4 to 6 plain-English lines: the exact cube state underneath, how a drag picks the layer and direction, the soft lattice inside each cubelet, the spring that gives the overshoot, how Solve plays your moves back, how the sound is made.
**Toasts** top centre for milestones. **Cinema** (`C`) hides all UI and the cursor; a tap without drag, `C` or Esc brings it back. Respect `prefers-reduced-motion` (soften UI transitions and camera moves); the jelly still responds fully.
## 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. One fixed quality preset (no quality scaler), 3x3, default softness, the fixed default camera with the top, front and right faces showing.
- **Never create an `AudioContext`**, play nothing.
- Deterministic, no randomness; restart 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.
- Timeline:
- **0.0 to 0.7 s:** the solved cube drops a short way onto the plate, squashes and wobbles.
- **0.8 s:** `R` with a big overshoot; the right layer shears and the cube rocks back.
- **1.6 s:** `U` with a big overshoot, the most satisfying moment: the top layer swings past, bloops back and the whole cube jiggles.
- **2.1 to 2.4 s:** a poke dents the front centre cubelet; its neighbours ripple.
- **2.6 s:** `U'`, **3.1 s:** `R'`; the cube is solved again by 3.6 s.
- **3.6 to 4.0 s:** everything settles to exact rest, matching 0.0 s.
- Start moving within 1 s. It is recorded in a slow software renderer: **the overshoots and the poke must read clearly at 15 fps**.
- Support `?demo=1&t=<seconds>` to freeze the demo at that time (for screenshots).
## Before you finish
Self-check against this list and fix anything that fails:
- Twist every layer of every face in both directions from the front, from above, from below and with the cube spun upside down: the layer under the finger always turns the way you drag. Repeat on 2x2 and 4x4 (inner slices too).
- Run 10,000 random moves through `__toy.move` and confirm the cube is still perfectly aligned and `state()` is legal; R U R' U' six times returns to solved.
- Scramble, then Solve, at both softness extremes and both twist speeds; twist hard and fast while a turn is still wobbling; spam Undo; Drop during a scramble: never a misaligned, stretched or exploded cubelet, never jitter at rest.
- Every tool, slider, switch, button and key (1 to 4, S, Enter, Z, N, arrows, 0, C, M, Space, R), pause and resume, Slow motion, Cinema, resizing to phone width with touch.
- Timer, best times, every milestone, and the Easter egg by hand.
- Sound: every action has its effect, nothing before the first gesture, mute and volume work, a fast scramble never clips, a failing audio call never breaks the toy.
- `?demo=1` matches the timeline, loops cleanly, shows no UI and plays no sound; `?demo=1&t=1.7` freezes mid-overshoot.
- No console errors or WebGPU validation errors on WebGPU AND on the WebGL2 path, and it stays smooth on a mid-range phone.
Put a correct cube and a juicy twist first. It should be the cube people keep scrambling just to watch it wobble back.