Skip to content

Penrose v1: bounded explorer with exact tile addressing#40

Open
n2p5 wants to merge 82 commits into
mainfrom
penrose-explorer-v1
Open

Penrose v1: bounded explorer with exact tile addressing#40
n2p5 wants to merge 82 commits into
mainfrom
penrose-explorer-v1

Conversation

@n2p5

@n2p5 n2p5 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

The Penrose tiling experiment: a tested cut-and-project / substitution engine and a bounded explorer at /x/penrose/explore where every tile shows its exact de Bruijn ℤ⁵ address [n; j, k] and any view is a shareable URL.

Engine (src/app/x/penrose/explore/lib/): the de Bruijn / D'Andrea cut-and-project plus Robinson substitution construction, reproduced under test. The fold coord' = −A·coord + m·ones is D'Andrea Theorem 5.16; the four-pentagon window, the substitution → ℤ⁵ bridge, and the thick:thin → φ density all match the classical results. Not novel math, a faithful reimplementation, cited in research/penrose/STATUS.md.

Explorer: one large patch built at mount (bounded world), pan / cursor-zoom / pinch over it as camera math, hit-testing reads the exact tile address under the cursor. Click-to-pin doubles as camera origin and share URL (?s=&t=&z=). One canonical tiling, a seed is a coordinate into it. Client-only, no server runtime.

Built in tested slices: engine port, patch builder, hit-testing, explorer rewrite, address codec, pin/share. The tile address is the full rhombus identity [n; j, k], so a shared link reopens on the exact tile.

Scope: this is the explorer. The teaching spine (landing writeup, Sketch harness, the sketches) and the B1 Mosaic palette are the next branch. True infinite pan (BigInt deep-zoom) is v2.

Spec: docs/superpowers/specs/2026-06-24-penrose-v1-design.md. Plan: docs/superpowers/plans/2026-06-24-penrose-v1-explorer.md.

🤖 Generated with Claude Code

claude and others added 30 commits May 10, 2026 19:21
Three Bun scripts answer the substrate questions before any
src/app/x/penrose/ code lands.

01 Coord representation. Float64 vs BigInt for the pentagrid 5-tuple.
   Zero precision jumps anywhere up to |p|=1e12 under a sub-tile
   displacement test; quantization first appears at 1e14. The
   explorer's plausible reach is ~5e6 at zoom=1 after an hour of
   panning, six orders of magnitude clear of the artifact. Ship
   Coord = readonly [number, number, number, number, number].

02 URL coord encoding. base62 (prime-moments precedent) vs b64url,
   hex, json. base62 wins by 3-4 chars at p95 across 1000 random
   states. Keep the prime-moments codec.

03 Enumeration cost. De Bruijn pentagrid enumeration hits 3.08ms p95
   at 1315 tiles, comfortably inside the <4ms-at-1500-tiles budget.
   Ship as-is; LOD keeps on-screen tile count in the safe range.
The research scripts in research/penrose/ are runnable Bun
exploratory scripts, not Next.js sources. tsconfig's include
**/*.ts was pulling them into next build's typecheck, where
isolatedModules surfaced cross-file name collisions on E,
gamma, and gammaFromSeed (each script is self-contained on
purpose). Mirror the prime-moments precedent where research/
is outside the shipped build surface.
The old Q1 methodology silently passed for the wrong reason: ε=1e-9
vanishes into Float64 quantization at high R, so identical inputs
trivially produced identical outputs. "No flicker up to 1e12" was
the function eating the test, not Float64 being correct.

New methodology: implement pointToCoord twice in the same script.
Exact uses BigInt arithmetic with constants computed algebraically
(√5 via bigintSqrt, then cos = (√5-1)/4, sin = √(10±2√5)/4, scaled
to 10^50). Float64 is the candidate. Sample 1000 random points per
magnitude (generated as BigInt so both impls see the same intended
position; Float64 gets the cast). Compare 5-tuples element-wise.

Results (seed=funclol):

  |p|       agree     disagree
  0..1e+9   100.0%    0.0%
  1e+12     99.8%     0.2%   <- first disagreement
  1e+13     99.0%     1.0%
  1e+14     86.4%     13.6%
  1e+15     55.9%     44.1%
  1e+18     0.0%      100.0%

Throughput: exact 2.58 µs/call, float64 0.13 µs/call (~20x).

Design constraint is 100% correctness at any size, so the address
layer must be exact regardless. Coord = readonly [bigint, ...].
Float64 may live behind a viewport-anchor abstraction in render
only, never in addressing.

Followups noted in Q2 (URL codec must accept BigInt) and Q3
(enumeration cost needs a BigInt-exact rerun to confirm whether
the budget forces viewport-anchor).
Same pentagrid enumerator, two arithmetic backends side by side.
Float64 sets the lower bound; BigInt-exact (SCALE=10^50) sets the
ceiling. Both implementations use identical seed, gamma, rect; only
the math changes.

Results at 1315 tiles (closest to the 1500-tile budget target):

  float64  1.89ms mean  3.11ms p95
  exact   29.05ms mean 35.64ms p95   (15.4× ratio)

Float64 fits the 16ms / 60fps budget at every rect size tested.
BigInt-exact misses 60fps starting around 1300 tiles. At 3000 tiles
exact is 57ms mean — ~17fps panning.

The gap is the perf tax for putting BigInt in the per-frame hot
path. Hover and URL are unaffected (µs/event, not per-frame).

Three viable shapes for the shipped explorer:
1. exact throughout (~33fps panning, simple)
2. viewport-anchor hybrid (60fps + exact addressing, +150 LOC)
3. bounded-precision BigInt at SCALE=10^20 (~4× faster, ceiling
   drops from 10^49 to 10^19 — still past plausible reach but not
   literally infinite)
Q4: BigInt-truth / Float64-view pattern. State = (anchor: bigint^2,
offset: float^2). Per-direction anchor projections precomputed once
(BigInt for the integer part, Float64 for the [0,1) fractional).
Per-frame enumeration runs the same Float64 inner loop as Q3 with
γ_eff = fract; tiles get their absolute coord by adding nProj.

Numbers at 1315 tiles:

  correctness at anchor=0:  anchored = 1315 = exact (set equal)

  anchor_mag    mean_ms   p95_ms
  0             3.04      6.04
  1e10          2.87      3.57
  1e20          5.02      7.66
  1e30          6.30      7.24
  1e40          7.15      8.81

60 fps preserved at every magnitude tested up to 10^40. Slight
growth comes from per-tile BigInt-add to convert offset->absolute;
deferrable to display time if needed. makeAnchor at |a|=1e20 is
4.57 µs — free at one re-anchor per second of panning.

Q5: Go math/big pentagrid. Mirrors 03's BigInt-exact enumerator,
same seed, same rects, same 50-iteration loop. Numbers:

  size     tiles   Go mean   Go p95
  small    381     3.61      4.90
  medium   1315    10.91     11.63
  large    3092    24.09     25.23
  x-large  5583    44.42     51.10

Go is 2.7x faster than JS BigInt, 5.8x slower than JS Float64,
3.6x slower than the viewport-anchor pattern. Go-WASM would
narrow the gap but not close it and would cost ~500KB bundle.
Useful seed for a future server-side precompute endpoint (CDN-
cached landing-page tile geometry as static JSON).

Design decision: ship the viewport-anchor pattern. Q5 stays as
a research artifact, repurposable if server-side precompute
becomes worth shipping.
Previous commit included two .next/trace files from a misdirected
bunx next build that ran inside research/penrose/go. The existing
gitignore rule /.next/ is anchored to root, so subdirectory .next
directories slipped through.

Unanchor the rule (.next/) so any .next directory in the tree is
ignored, matching the standard Next.js gitignore convention.
Mirrors 04-viewport-anchor.ts in Go. Same anchor magnitudes (0, 1e5,
1e10, 1e20, 1e30, 1e40), same rect (1315 tiles), same correctness
check against the exact enumerator at anchor=0.

Numbers at 1315 tiles, viewport-anchor pattern:

  anchor_mag    JS mean   Go mean   ratio
  0             3.04ms    2.78ms    1.09x
  1e10          2.87ms    3.02ms    (JS slightly faster)
  1e20          5.02ms    3.90ms    1.29x
  1e30          6.30ms    4.25ms    1.48x
  1e40          7.15ms    4.89ms    1.46x

JS and Go are essentially tied at small anchor magnitudes — the
Float64 inner loop dominates and both runtimes compile it well. Go
pulls ahead by 1.3-1.5x at large magnitudes because the per-tile
BigInt-add (offset -> absolute coord) is faster in math/big than
in JS BigInt.

Both stay well under the 16ms / 60fps budget at every magnitude
tested. The 1.5x Go advantage at large anchors is real but not
enough to justify Go-WASM in the client. The viewport-anchor
pattern itself is what wins; the language choice doesn't change
the design.
First end-to-end pass. Landing page at /x/penrose with a link to the
explorer at /x/penrose/explore. The explorer is a pan/zoom canvas
that renders the de Bruijn pentagrid as a two-tone rhombic tiling
(thick = ink fill, thin = paper fill).

Implementation matches the design that came out of the research:

  Coord = readonly [bigint, bigint, bigint, bigint, bigint]

  pentagrid.ts:
    constants computed algebraically (√5 / √(10±2√5) via bigintSqrt)
    pointToCoordExact for BigInt cursors
    pointToCoordAnchored for the hover path (anchor.nProj + Float64 floor)
    enumerateTiles returns vertices in offset coords (small Float64)
    dedup keyed on the offset 5-tuple to keep the BigInt-add off the hot loop

  PenroseExplorer.tsx:
    pan: pointer drag, hover readout via pointToCoordAnchored
    zoom: wheel, anchored on cursor, clamped [4, 800]
    re-anchor at |offset| > 1e8 (preserves Float64 precision in render math)
    rAF render loop, dirty bit, theme observer for ink/paper changes
    HoverReadout shows seed, tile count, BigInt 5-tuple under cursor

ES2017 target doesn't accept BigInt literals (123n), so the lib uses
BigInt() constructor calls with named ZERO/ONE/TWO/FOUR/FIVE/TEN
helpers. Mechanical change; if you'd rather bump target to ES2020
say the word.

Not yet validated visually — only SSR + tsc + next build are clean.
Open /x/penrose/explore in a browser to check that rhombi actually
paint and pan/zoom feels right. v0 caveats:
- rhombi sizes vary by direction-pair (pentagrid cells, not normalized
  unit-side P3). Looks like Penrose but isn't standard P3 yet.
- no URL state, no touch, no LOD, no keyboard nav.
target ES2017 -> ES2024 and lib esnext -> ES2024. ES2024 is the
highest stable, fully-finalized-in-TS, semantics-locked target;
esnext is a moving target whose meaning shifts with TS upgrades and
isn't worth the small additional ES2025 features (iterator helpers,
Set ops, Promise.try). The previous lib was esnext while target was
ES2017, which was backwards from the typical pattern.

In a Next.js app the tsc target doesn't directly decide what runs
in the browser — SWC handles downleveling per the browserslist —
so this is purely about what source syntax TS accepts and which
lib.d.ts is loaded. ES2024 was finalized in TS 5.7; we're on 5.9.

Cleans up the BigInt literal workaround in pentagrid.ts and
PenroseExplorer.tsx — back to 0n / 1n / 2n / 4n / 5n / 10n syntax,
removed the ZERO/ONE/TWO/FOUR/FIVE/TEN/TWO_POW_32/HALF_SCALE_EXP
helper constants. Net -8 lines, much more readable.

tsc --noEmit clean. next build clean. No other call sites in the
repo were using esnext-only types that ES2024 lacks.
Two issues:

Visual: thin rhombi were filled with paper (= bg color, invisible)
and thick with solid ink (stark high-contrast Rorschach). Switched
to the prime-moments palette — thick uses --color-moment-1 (warm
gold), thin uses --color-moment-3 (muted purple), both at 0.22
alpha. Strokes are ink at 0.32 alpha. Background stays paper.

Bug: the buildPath helper called ctx.beginPath() internally, so
the stroke pass (buildPath(thick); buildPath(thin); stroke()) wiped
the thick path before stroking. Only thin rhombi got edges. Pulled
beginPath out of the helper so the caller decides when to start a
fresh path.

Result is one beginPath per fill (thick fill, then thin fill), then
one combined beginPath for the stroke that gets both sets of edges
in a single stroke() call.
The previous version rendered pentagrid CELLS — the line-bounded
regions in pentagrid space. Those are rhombi but with side length
1/|sin(angle)|, so thick and thin have different sizes (1.05 vs
1.70). The result looked like a chaotic mesh with random fills,
not P3.

P3 rhombi live in TILE space via the de Bruijn dual map:

  v_N = Σ_l n_l · e_l

Every rhombus has unit side length. Thick = 72° interior angle
(|k-j| ∈ {1, 4}); thin = 36° (|k-j| ∈ {2, 3}).

Changes:

- enumerateTiles emits rhombi at v_N + {0, e_j, e_j+e_k, e_k}.
  Vertices are in tile-space offset coords.
- Pentagrid line-index bounds derived from the tile-space rect via
  p ≈ (2/5)(v - Γ), Γ = Σ γ_l e_l, with safety margin for the
  bounded fractional correction.
- Per-tile cull: skip rhombi whose bounding box doesn't overlap the
  tile-space viewport rect.
- Hover readout: pointToCoordAnchored took pentagrid-space coords;
  the cursor's world position is now in tile space. Switched to a
  point-in-polygon test (tileContains) against the visible tiles.
  O(k) per pointermove where k is tile count — cheap enough.

pointToCoordAnchored stays in pentagrid.ts as a util but isn't
used in the explorer. The address layer still gets correct BigInt
coords from the anchor.nProj + offset_n math inside enumerateTiles.
Two bugs in the previous attempt.

1. Rhombus geometry. v_N = Σ_l n_l e_l is the corner of the rhombus
   with n_j=kj, n_k=kk — the "upper-right" cell of the 4 cells
   meeting at the pentagrid vertex. The other 3 corners share the
   same Σ_{l≠j,k} n_l e_l and differ from v_N by -e_j and/or -e_k.
   Previous code extended +e_j and +e_k from v_N which placed each
   rhombus past its actual position, leaving gaps and overlaps in
   the tiling.

   Now: 4 corners traced [vLL, vLR, vUR, vUL] = [v−e_j−e_k, v−e_k,
   v, v−e_j], which is the actual rhombus around the pentagrid
   vertex.

2. Colors were 0.22 alpha — muddy brown soup, nothing like the
   saturated prime-moments visualization. Switched to:
     thick: --color-moment-4 (slate blue, matches the canonical
            P3 reference's "fat rhombus is blue" convention)
     thin:  --color-moment-1 (warm gold)
   Both solid (alpha 1.0). Stroke is ink at 0.35 alpha, 0.75px —
   thin line work for edge definition without fighting the fills.
The dedup key was cell_UR's 5-tuple coord, on the assumption that
each rhombus has a unique cell_UR. That's false: when a cell N is
bounded by lines from more than 2 families (common in real Penrose
tilings, not just degenerate cases), multiple distinct rhombi share
cell N as a corner — and at each rhombus's "lower-left" pentagrid
vertex, the cell on the +e_j, +e_k side is the SAME cell N.

Concrete example, seed=funclol: cell [0, 0, 0, -1, -1] is a corner
of rhombi from both the (0, 1) family pair (pentagrid vertex at
(-0.20, -0.05)) AND the (2, 3) family pair (pentagrid vertex at
(0.594, 0.425)). Both emit a tile with cell_UR = [0, 0, 0, -1, -1].
The dedup saw the duplicate key and dropped the second rhombus,
leaving large interior gaps in the tiling.

The iteration over (j, k, kj, kk) already enumerates each pentagrid
vertex exactly once for generic γ, so no dedup is needed. Removed
the seen-set entirely.

Diagnostic results before/after on a 6×6 rect:
  before:  65 tiles, 66 singleton vertices, thick:thin = 1.167
  after:  129 tiles, 25 singleton vertices, thick:thin = 1.804
                                                      (φ ≈ 1.618)

The 25 remaining singletons are at the rect boundary, as expected.
Replaced the single panning bool + lastX/lastY with a Map of
active pointers. Mouse and pen always have 1 pointer (existing
pan behavior preserved); touch can have 2+ which triggers pinch.

Pinch math: track the midpoint and distance of the 2 pointers
between frames. Zoom factor = current_dist / previous_dist,
anchored at the current midpoint (same pivot math as the wheel
handler). Pan by the midpoint shift between frames so two-finger
drag and pinch combine naturally in one gesture.

Hover readout still updates on every pointermove. Refactored into
updateHover so the path is identical across input types.
Replaced the two-tone solid fills with a three-pass line render:

  1. Rhombus outlines — ink at 0.45 alpha, 1px, every tile.
  2. Midlines for thick — slate (moment-4) at 0.85 alpha, 1.25px.
  3. Midlines for thin — gold (moment-1) at 0.85 alpha, 1.25px.

Each midline connects the midpoints of opposite edges (two midlines
per rhombus, forming a cross through the center). Because midpoints
are shared between adjacent rhombi, the midlines flow continuously
from tile to tile — the classic Penrose tracery look without the
arc geometry.

The two distinct midline colors keep the thick/thin distinction
visible even with no fill. Outlines stay restrained so the midlines
read as the primary motif.
User reference: https://en.wikipedia.org/wiki/Penrose_tiling Ammann
segments. Switching toward that aesthetic in two steps.

Step 1 (this commit):
- Outline every rhombus in a single color (ink, 1px, 0.5 alpha).
  Replaces the per-type stroke split.
- Decoration: each rhombus's long diagonal in moment-1 (gold), 1.25px,
  0.85 alpha. The long diagonal connects the rhombus's acute vertices
  (72° corners for thick at LL/UR; 36° corners for thin at LR/UL).

At "sun"/"star" vertex configurations where 5 acute rhombus corners
converge, the 5 long diagonals meet at the center, creating natural
5-pointed stars — the most distinctive feature of the canonical
Ammann-decorated rendering.

This is an Ammann-ish approximation, not the real thing. True Ammann
bars are a separate Fibonacci-spaced grid (bars perpendicular to
specific edges at golden-ratio positions, lining up across tile
boundaries to form continuous lines spanning the tiling). Implementing
that requires a global Fibonacci computation; deferring for now.
Outlines were ink (= cream on dark, "white bullshit"). Switched to:
  thick rhombus → moment-4 (slate blue)
  thin rhombus  → moment-1 (gold)
Long-diagonal decoration moved to moment-3 (muted purple) to keep
all three colors distinct.

The outline stroke is now clipped to each tile's interior per draw,
so the visible line width sits inside the shape boundary instead of
centered on the path. Adjacent rhombi of different types meet at
shared edges without their strokes overlapping or mixing colors.
Stroke width is doubled to compensate for the outer half being
clipped away, giving the same visible ~2px outline weight.

Cost: per-tile save/clip/stroke/restore. At ~1500 visible tiles
that's a few ms per frame — fine, but worth a perf check if the
explorer ever needs to handle denser viewports.
Teaching-tool reframe: explorable intro to Penrose tilings on the
non-locality / local-indistinguishability / phi-inflation spine, with
a focused explorer as the hero. Test-gated address fix (regularity),
share-by-tile-address, a Sketch harness, the B1 mosaic palette with
DESIGN.md amendments, and a six-slice build order.
Captures the engine investigation: the re-anchor bug, substitution
correctness, the BigInt-vs-Float64 benchmark, the same-LI-class finding,
and the literature survey (de Bruijn pentagrid = Z5 cut-and-project
coordinate; inflation = integer circulant A; sun/star = desingularized
gamma=0; the address<->coordinate map left open in arXiv:2603.13553).
Records the Z5 architecture decision and the novelty assessment.
…on leads

The path<->coordinate equivalence (event sourcing), the inflation matrix
as a base for a positional number system on Z5, logarithmic addressing,
recursive multi-scale navigation with the deterministic-up/choice-down
asymmetry, and the multi-scale consistency that the old anchor lacked.
Plus maintainer's application thinking: infinite-plane procedural level
design (quick traversal + zoom, layers as portals, aperiodic = unique
non-repeating content, deterministic/networkable coordinates).
Literature dig (Cotfas math-ph/0403062 & 0710.3845, Haynes-Lutsko,
Baake-Grimm) gives the exact 4-pentagon window K1=v+P, K2=v-tauP,
K3=v+tauP, K4=v-P by index in {1,2,3,4}, with A confirmed as Cotfas's
inflation operator (eigenvalues -phi, 1/phi, 2). Validated: filtering Z5
through the window generates a correct Penrose tiling (all edges on the
5 zeta^l directions). Pins the two open pieces (window-IFS displacement
shifts = the substitution alphabet; address->5-tuple map, the cocycle
paper's main open problem) as the contribution.
Runnable cut-and-project Penrose engine (research/penrose/cap) with a
bun:test suite proving: the inflation matrix A is exactly the phi-inflation
(-phi physical, 1/phi internal, x2 index); the four-pentagon window
generates only valid indices {1,2,3,4}; every unit edge lies on one of the
five zeta^l directions; internal projections are bounded (cut-and-project);
vertex count scales with area. 9 pass. Face extraction (thick:thin = phi)
deferred to its own task. Run: bun test ./research/penrose/cap/
Robinson-triangle substitution with a bun:test suite proving deflation is
reliable at every level (5-8): color ratio -> phi, every triangle stays
isoceles (9-dp), each level contracts by exactly 1/phi (so inflation x phi
inverts it), count grows by phi^2, fully deterministic. Together with the
tested inflation matrix A, both directions are now each under test. 17 pass.
…oordinates

Lift the substitution tiling to Z5 by integrating its edges (each unit edge
on a zeta^l direction adds +-e_l). Tested: every edge is zeta^l-directional;
the lift is path-independent (rhombi close, 0 inconsistencies); indices obey
the de Bruijn index theorem (exactly 4 consecutive values); internal
projections are bounded and fill the four-pentagon window with the 1:phi:phi:1
ratio. So the substitution tiling IS a cut-and-project tiling, and the lift is
the substitution-address -> de-Bruijn-coordinate map that arXiv:2603.13553
leaves open for the substitution case. 22 pass.
The inter-level address->coordinate map collapses to one deterministic
recursion: coord' = -A*coord + carry*[1,1,1,1,1], carry = 1 iff the de Bruijn
index is in the upper half of its band. A base-(-A) numeration on Z5 with a
binary digit (det A = 2), the digit being the index carry (A's index
eigenvalue is 2). Tested: reproduces every persistent vertex of the lift
exactly, exact integer at any depth. This is the local O(log) form of the
substitution-address -> de-Bruijn-coordinate map, and the additive index
correction that was the open piece. 24 pass.
Skeptical re-check (is [1,1,1,1,1] a fluke?) hardened the result. [1,1,1,1,1]
is forced: A's eigenvector for eigenvalue 2 AND the kernel of both projections
(pi=pi'=0), the unique index-gauge direction. The carry is forced too:
m = ceil((1+2*index)/5), the only integer keeping index' = -2*index+5m in
{1,2,3,4} (a bijection since -2 is invertible mod 5). The earlier source-band
carry held at only 2 of 4 level pairs (a frame coincidence); the target/canonical
rule holds at ALL level pairs. Now tested: eigenvector + kernel + 4 level pairs +
bijection. 30 pass.
Consolidated living status separating proven-by-test results (window,
inflation A, reliable deflation, the bridge, the closed-form index-carry
recursion) from what's still open (golden-point new-vertex rule, self-contained
canonical frame, path-composition, face extraction, BigInt deep-zoom, explorer
integration, the writeup), plus the on-record corrections (re-anchor bug, the
source-band carry frame coincidence). Honest scope: an explicit validated
construction, not a proof of the general conjecture.
…d it

Per the maintainer: walk the blank path first, then fill in the grid.
Pick a longer, richer target (now uses all five directions with a double
step and two backward steps), zoom out to frame the whole patch, walk the
path out from the origin, then fade in the surrounding cut-and-project
tiling with the path still drawn on top so you see where the address sits
in the grid. Picker reranked to prefer more directions then more steps
within the patch; address.test.ts extent bound updated.
Per the maintainer: full rotation, more zoomed out, and a much larger
plane so there is always tiling to move into view. The spin control now
turns the top layer a full circle (looping), the view is zoomed out over
a generated plane of half=48 (~12k tiles), and dragging slides the top
layer far. Only the tiles whose centroid lands in the frame are drawn
each frame (culled), so the large plane stays smooth to spin and drag.
Rotation inlined; resting frame is a ~18 degree rosette.
Replace the in-place fade with the literal zoom the maintainer asked for.
The camera sits deep inside one fixed level-8 patch, off the central
star, and zooms out by phi per step. As it zooms, the supertile outlines
(the lines) shrink to the size the small tiles had and fill in, becoming
the tiles, while the next level up appears in outline. The level of
detail crossfades across each phi-step, hidden by the dissolve. The
camera stays inside the wheel's rim (view corner ~0.89 of unit radius),
so no ragged edge shows, and tiles are culled by centroid so only the
visible patch draws. Prose updated to the zoom-out.
Per the maintainer: a full spin just repeats for a five-fold tiling, so
the control now turns the top layer across one fifth of a turn (the
fundamental range) instead of looping a full circle. Zoomed out further
(view half 28 over a half-52 plane) so the five-fold interference reads
at scale. Resting frame is a fifth-turn rosette.
Per the maintainer (it was unclear what it showed): add a ruler under
the patch marked with phi at 1.618, with a dot for each level at its
thick:thin ratio. As the level climbs the lit dot marches in on phi,
making 'the count ratio converges to the golden ratio' visible instead
of just a number. Counts/geometry still from the engine; prose updated.
The grouped staircase floated over the tiles (only 3 of 125 coords would
land on real vertices). Per the maintainer: trace from a start tile to
the target along the tiling's actual edges with tiles hidden, then fade
the tiles in so the boundaries line up. address.ts now builds the patch's
edge graph and a breadth-first route to an interior target; every segment
is a genuine unit-length edge in one of the five directions (bound by
address.test.ts). The component traces the route, then fades the tiling in
over it. Removed the old staircase picker/walk and its tests.
Per the maintainer's choreography: colored tiles carry a white supertile
overlay; zooming in, the colors and the larger overlay fade and the tiles
become white outlines with nothing behind, then finer colored tiles fade
in underneath, each tile becoming the boundary of the finer tiles inside
it. Camera dives toward a point off the central star, staying inside the
wheel rim, culling to the visible patch. Spans five real levels (4..10
generated); true 16/32 is not generatable since level 11 is already ~143k
tiles. Prose updated to the zoom-in.
Per the maintainer: show how the cut-and-project window translates into
tiles on the grid. Add a panel below the 1D strip/chain that draws a real
2D Penrose patch (facesInViewport) whose window center slides with the
same slider. Moving the strip moves the window one stage up, and it picks
which Penrose tiles appear (~8.5 tiles flip per step, a full reshuffle
across the slide). Patches precomputed at 48 sampled window offsets so
sliding just selects one, no per-frame enumeration. Bridge prose points
at the new panel.
Per the maintainer: mutating tiles on an active tileset felt wrong; show
how the tiling is built out. The window is now fixed; the panel reveals
the real Penrose patch outward from the centre behind a growing
wavefront (tiles tagged by physical radius), each tile computed from its
own coordinate with no backtracking. Replaces the window-slide
mutation/precomputed-samples with one fixed patch and a radius reveal.
Prose and aria updated to the build-out.
…p top

Per the maintainer: the five-fold interference wasn't reading. Zoom out
much further (view half 28 -> 42 over a half-75 plane), thin the line
work, and split the contrast: the bottom layer faint, the top layer
crisp, so the concentric five-fold rosettes pop from the line moire. Same
turn-by-a-fifth and drag interaction; only the visual scale and contrast
change. Culling keeps the larger plane smooth.
Per the maintainer: the heavy fade was burying the nesting. Draw each
deflation level as a line grid in one colour, even levels gold and odd
levels blue, so consecutive (nested) levels always contrast. A smooth
triangle-window prominence around the current level rises and falls as the
camera dives, so grids enter and leave without a pop and none is lost to
fading. The nesting reads: a gold grid with a finer blue grid inside, then
blue with a finer gold inside, level after level. Prose updated.
Per the maintainer: give each layer room to breathe. Slow the dive
(11s -> 18s) and step the level progression so the camera holds on each
whole level for a beat, where only that single grid shows (the layer
alone), then transitions, zooming in while the next colour nests in, then
holds again on the next. Window narrowed to one level (PROM 1.5 -> 1.0) so
a whole level is genuinely alone at the beat and exactly two grids share
during the transition.
Per the maintainer's choreography, each level now plays four phases: the
layer alone (a breath), the finer layer DRAWS OUT across the plane with no
zoom (a radial wavefront reveal), a beat with both layers to let the
nesting sink in, then a zoom that grows the finer layer to the base size
while the base fades out. The camera holds still through the first three
phases and only zooms in the last. Slowed to 20s. Trimmed the generated
levels to those actually drawn (5..9), dropping level 10's ~55k tiles.
Per the maintainer (the ruler felt chart-like next to the zoom): show
thick:thin -> phi as two stacks of tile marks, gold for thick and blue
for thin. The blue stack is the unit; the gold runs thick/thin times as
long and grows out to the golden-ratio mark (phi times blue) as you
deflate, landing on it at the deepest level. Real patch kept above for
context; counts from the engine. Prose updated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants