How a user moves through each track — transitions, branches, terminal states. v5 is the new direction (gesture-driven object); v4 is the live Story + Levels lab; Tessra runs the v3 flow on mobile. See VERSIONS.md for the matrix.
v5 drops puzzles entirely. One full-screen mosaic, daily-seeded, breathing on its own. Every gesture feeds the same surface: tap fires a ripple, drag leaves a trail, a long-press re-seeds the whole tiling. With nothing touching it for 8 s the object "calls back" with a pulse from centre.
Underneath every state, each tile pulses on a slow sine
(~5.8s period, phase offset by tile id). The surface
looks alive even when untouched. A tap near an edge fires a
converging counter-ripple from the opposite point.
There is no "complete" state — the loop never ends, it only settles back to ambient. Rapid taps build a shared energy meter (decays ~1s half-life) that brightens edges. Re-seed fires a triple-pulse haptic.
The live web lab. Frames auto-progress; manual tap-skip; swipe-down close.
End-card → CTA into the levels. Mark-seen fires on close or on reaching the
end, stored in progress.storiesSeen[date].
Story data is deterministic by daily.date. Marked seen
in progress.storiesSeen[date]=true via onSeen()
on finish or close.
If the UTC date rolls over (midnight) mid-watch, the current Story isn't interrupted; the next launch after midnight loads a different (new) Story.
The same 20 canonical Penrose figures, rendered on real multigrid patches. The next level unlocks on completing the previous one. After L20 — a paywall stub for the future mobile-payment integration.
Every level declares a spec inside FIGURES_V2:
vertexSig / vertexN / chain /
decapod / sunWithRing / twinSuns.
The engine finds the figure inside a real multigrid patch by its
canonical signature.
progress.unlocked is the index of the first locked level.
On complete: setProgress({ unlocked: id + 1 }) + save to
penrose-v4-progress.
An infinite Penrose plane: pan / zoom / pinch, brush picker, painted tiles
saved to penrose-v4-canvas. Suspended — the
chunk-based multigrid has floating-point seams at chunk boundaries. The flow
below is the intended design; the fix is tracked in
IDEAS.md (and is a prerequisite for the v5 infinite canvas).
We don't "generate an infinite plane." Instead
tilesForViewport(vp) returns tiles inside the current bbox,
split into 12×12 world-unit chunks. Each chunk is the output of
generateInBox() from geometry-infinite.js,
cached in a Map<chunkKey, Tile[]>.
Tile ids derive from rounded vertex coordinates; at chunk boundaries
floating-point rounding produces duplicate / mismatched ids, so seams
show. Fix: sort intersections by line-index keys, not float-rounded
(x,y). Then v5 can adopt the same renderer.
On first launch — onboarding (4 steps: Touch / Play / Daily / Flow). Then Home with Today's Mosaic and Playground. The web v3 flow is ported; v4 (Story, Canvas) and the v5 object are not — see the port-gap note below.
Store-ready (v1.0.0 b1). The v3 flow is ported (geometry, daily,
levels, patch engine, paywall stub) plus a native rating prompt via
expo-store-review. 20/20 reached on the simulator.
Mobile's Daily is tile-reveal gameplay; web v4's Daily became a Story, and v5 drops puzzles for an instrument. To decide — port one direction to mobile, or split the tracks deliberately (see IDEAS.md).