Penrose v1: bounded explorer with exact tile addressing#40
Open
n2p5 wants to merge 82 commits into
Open
Conversation
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.
ca19861 to
8e4852a
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Penrose tiling experiment: a tested cut-and-project / substitution engine and a bounded explorer at
/x/penrose/explorewhere 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 foldcoord' = −A·coord + m·onesis 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 inresearch/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