From dadcb6d2328372ccb8240d1d4f1db3083728d353 Mon Sep 17 00:00:00 2001 From: engmung <122682380+engmung@users.noreply.github.com> Date: Sun, 16 Aug 2026 23:05:33 +0900 Subject: [PATCH 1/6] feat(web): the atlas learns from two weeks in the field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Twenty-five patterns were made against the map since it shipped; nine earned pins. The hit rate went 8% -> 67% week over week, and the map now records what that fortnight proved: - six entries move to verified (mirror, ising, sandpile, invasion, choke, flame), each impl line naming the pattern that proved it β€” choke is the invented continent's first verification - the prompt's Rendering craft gains the device budget the failures drew: transcendentals stay out of the per-pixel loop, one full-res pass, no per-pixel resampling of the previous frame, no O(nΒ²), no Float64Array. Every hardware casualty of the fortnight (per-pixel shader synthesis, warp feedback, brute-force boids) dies on one of those lines - feedback (0 for 2 on hardware) is re-specced to integer-shift recursion; smoke and fluid re-route their ink through particles - eight new entries, all budget-native by construction: four waterfall (kpz, traffic, oslo, voter β€” 1D physics + scroll, the cheapest architecture the field data found) and four lattice statistical mechanics (rps, potts, forestfire, xy β€” integer local rules, the richest vein). 57 entries total. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- web/src/lib/atlas/data.ts | 148 ++++++++++++++++++++++++++++++++------ 1 file changed, 126 insertions(+), 22 deletions(-) diff --git a/web/src/lib/atlas/data.ts b/web/src/lib/atlas/data.ts index a4fb1ff..63005cf 100644 --- a/web/src/lib/atlas/data.ts +++ b/web/src/lib/atlas/data.ts @@ -166,13 +166,13 @@ export const ENTRIES: AtlasEntry[] = [ topic: "Pick one unused attractor (Ikeda map, Tinkerbell, Svensson, Aizawa, or a Sprott flow) and render it as fading orbit ink", hints: ["scan several candidates quickly; keep only filament textures that differ clearly from Clifford/De Jong"] }, - { id: "flame", nm: "ν”„λž™νƒˆ ν”Œλ ˆμž„", nmEn: "Fractal flame", en: "FRACTAL FLAME", f: "ink", st: "hold", x: 52, y: 42, + { id: "flame", nm: "ν”„λž™νƒˆ ν”Œλ ˆμž„", nmEn: "Fractal flame", en: "FRACTAL FLAME", f: "ink", st: "verified", x: 52, y: 42, tex: "variation λΈ”λ Œλ“œλ‘œ λΉ„μ„ ν˜• μ™œκ³‘λœ IFS β€” λΉ›λ‚˜λŠ” κΉƒν„Έ/μ—°κΈ° ν•„λΌλ©˜νŠΈ.", texEn: "An IFS warped by blended nonlinear variations β€” glowing feather and smoke filaments.", knob: "variation κ°€μ€‘μΉ˜ λΈ”λ Œλ“œ β€” ν˜•νƒœ 문법 μžμ²΄κ°€ 바뀐닀.", knobEn: "variation weight blend β€” the form grammar itself changes", - risk: "ν•œ 번 μ‹œλ„, 평가 μ• λ§€. variation 폭을 λ„“ν˜€ μž¬λ„μ „ν•  κ°€μΉ˜.", - riskEn: "Tried once, verdict ambiguous. Worth retrying with a much wider variation set.", + impl: "PhoenixEmber(2026-08-11 ν•€) β€” μΉ΄μ˜€μŠ€κ²Œμž„ ~2천 iter/frame + log-density, μ‹€κΈ° 톡과.", + implEn: "PhoenixEmber (pinned 2026-08-11) β€” chaos game at ~2k iters/frame + log-density, passed hardware.", topic: "Fractal flame β€” an IFS with blended nonlinear variations, accumulated as log-density ink", hints: ["log-density tone mapping is what makes flames glow"] }, @@ -184,11 +184,13 @@ export const ENTRIES: AtlasEntry[] = [ topic: "Refraction caustics β€” photons passed through a slowly morphing wavefront, accumulated where they land", hints: ["thousands of photons per frame", "build the wavefront from a sum of slow low-frequency noises"] }, - { id: "mirror", nm: "λ°˜μ‚¬ μ»€μŠ€ν‹± Β· 쀑λ ₯ 렌즈", nmEn: "Reflection caustics Β· gravity lens", en: "FOLDED LIGHT", f: "optics", st: "unexplored", x: 46, y: 64, + { id: "mirror", nm: "λ°˜μ‚¬ μ»€μŠ€ν‹± Β· 쀑λ ₯ 렌즈", nmEn: "Reflection caustics Β· gravity lens", en: "FOLDED LIGHT", f: "optics", st: "verified", x: 46, y: 64, tex: "곑면 거울/μ§ˆλŸ‰μž₯이 μ ‘λŠ” λΉ› β€” μ»€μŠ€ν‹±μ˜ ν˜•μ œ, 더 이질적인 μ ‘νž˜(fold/cusp).", texEn: "Light folded by curved mirrors or a mass field β€” sibling of caustics with stranger folds and cusps.", knob: "곑λ₯ /μ§ˆλŸ‰ 뢄포 β€” μ»€μŠ€ν‹± κ°€μ§€κ°€ νƒœμ–΄λ‚˜κ³  ν•©μ³μ§€λŠ” 전이.", knobEn: "curvature / mass distribution β€” caustic folds and cusps are born and merge", + impl: "ParabolicWavefrontΒ·DualLenseQuadrupole(2026-08-10 ν•€) β€” ν”½μ…€λ‹Ή trig ~4개 단일 νŒ¨μŠ€κ°€ μ‹€κΈ° μƒν•œμ„  근처.", + implEn: "ParabolicWavefront Β· DualLenseQuadrupole (pinned 2026-08-10) β€” ~4 trig per pixel in a single pass sits near the hardware ceiling.", topic: "Reflection caustics off a curved mirror, or gravitational-lens style deflection β€” photon landing accumulation" }, { id: "interf", nm: "이동 광원 κ°„μ„­ λˆ„μ ", nmEn: "Slow interference accumulation", en: "SLOW INTERFERENCE", f: "optics", st: "unexplored", x: 30, y: 52, @@ -231,8 +233,11 @@ export const ENTRIES: AtlasEntry[] = [ knobEn: "buoyancy / turbulence scale β€” laminar column ↔ curling turbulence", vert: "μƒμŠΉ μžμ²΄κ°€ 주인곡 β€” μ„Έλ‘œ ν”„λ ˆμž„κ³Ό μ²œμƒμ—°λΆ„.", vertEn: "Rising itself is the protagonist β€” born for the tall frame.", - topic: "Rising smoke β€” a curl-noise (divergence-free) velocity field plus buoyancy advecting a density field upward", - hints: ["no solver needed: the curl of a noise potential is already incompressible"] }, + risk: "8/9 SmokeFluid νƒˆλ½ β€” ν”½μ…€λ‹Ή μž₯ ν•©μ„± + semi-Lagrangian λ¦¬μƒ˜ν”Œμ€ μ‹€κΈ° μ˜ˆμ‚° 초과. μž…μž 이λ₯˜(GaleInk 방식)둜만.", + riskEn: "SmokeFluid (8/9) failed hardware: per-pixel field synthesis + semi-Lagrangian resampling blows the budget. Particle advection (the GaleInk route) is the way in.", + topic: "Rising smoke β€” a curl-noise (divergence-free) velocity field plus buoyancy, made visible by advected PARTICLES depositing ink", + hints: ["no solver needed: the curl of a noise potential is already incompressible", + "advect a few hundred particles through the curl field and deposit ink β€” do NOT synthesize the velocity field per pixel per frame, and no semi-Lagrangian density resampling (both failed on hardware)"] }, { id: "lic", nm: "LIC μœ μ„  직물", nmEn: "LIC streamline fabric", en: "LINE INTEGRAL CONVOLUTION", f: "advect", st: "unexplored", x: 43, y: 50, tex: "벑터μž₯ 전체λ₯Ό λ…Έμ΄μ¦ˆλ‘œ 문질러 μ–»λŠ” μ‘°λ°€ν•œ μœ μ„  직물 β€” 철가루 자기μž₯의 질감. ν”Œλ‘œμš°ν•„λ“œ ν΄λ¦¬μ…°μ˜ 비클리셰 μƒμœ„ν˜Έν™˜. (연ꡬ μˆ˜μž…)", @@ -250,16 +255,18 @@ export const ENTRIES: AtlasEntry[] = [ texEn: "The whole frame re-composited onto itself through rotate + zoom + warp β€” an infinite tunnel made material.", knob: "zoom=1 경계 β€” 수렴 ↔ 폭주의 μΉΌλ‚  μœ„.", knobEn: "the zoomβ‰ˆ1 boundary β€” the knife edge between convergence and blow-up", - risk: "단독 ν‰κ°€λŠ” μ• λ§€ν–ˆμŒ. ꡐ배 κΈ°κ³„λ‘œμ„œ 더 κ°€μΉ˜.", - riskEn: "Ambiguous on its own; more valuable as a crossbreeding machine.", - topic: "Recursive frame feedback β€” the whole frame re-composited onto itself through rotate + zoom + warp" }, + risk: "단독 ν‰κ°€λŠ” μ• λ§€ν–ˆμŒ. 그리고 8/14 μ›Œν”„-λ¦¬μƒ˜ν”Œ μ‹œλ„ 2건 μ „νŒ¨ β€” ν”½μ…€λ‹Ή λ°”μ΄λ¦¬λ‹ˆμ–΄ ν”Όλ“œλ°±μ€ μ‹€κΈ° μ λŒ€μ . μ •μˆ˜ μ‹œν”„νŠΈ(ν–‰ 볡사 슀크둀/νšŒμ „)λ‚˜ 타일 λ‹¨μœ„ μž¬κ·€λ‘œλ§Œ.", + riskEn: "Ambiguous on its own β€” and both 8/14 warp-resample attempts failed hardware. Per-pixel bilinear feedback is device-hostile; feedback must move by whole-pixel integer shifts (row-copy scroll/rotate) or at tile level.", + topic: "Recursive frame feedback via integer-pixel shifts β€” the frame re-composited onto itself through whole-pixel scroll/rotate steps and decay, never per-pixel resampling" }, { id: "hybrid", nm: "ν”Όλ“œλ°± ꡐ배", nmEn: "Feedback crossbreed", en: "CROSSBREED", f: "feedback", st: "unexplored", x: 82, y: 56, tex: "κ²€μ¦λœ μ†ŒμŠ€(μ»€μŠ€ν‹±Β·μž‰ν¬)λ₯Ό ν”Όλ“œλ°± 기계에 μ”¨μ•—μœΌλ‘œ β€” μ†ŒμŠ€μ˜ 질감이 μž¬κ·€λ‘œ μ¦ν­λœλ‹€.", texEn: "A proven source (caustic ink, attractor ink) fed as the seed of the feedback machine β€” its texture amplified by recursion.", knob: "ν”Όλ“œλ°± 게인 Γ— μ†ŒμŠ€ 강도 β€” μ†ŒμŠ€ μ§€λ°° ↔ μž¬κ·€ μ§€λ°°.", knobEn: "feedback gain Γ— source strength β€” source-dominated ↔ recursion-dominated", - topic: "A feedback crossbreed β€” feed a proven source (caustic ink, attractor ink) as the seed of a rotate+zoom+warp frame feedback" }, + risk: "ν”Όλ“œλ°± λ³€ν™˜μ€ μ •μˆ˜ μ‹œν”„νŠΈ ν•œμ • (ouroboros μ°Έμ‘°) β€” μ›Œν”„-λ¦¬μƒ˜ν”Œ 판 2건 μ‹€κΈ° μ „νŒ¨.", + riskEn: "The feedback transform must be integer shifts (see ouroboros) β€” both warp-resample builds failed hardware.", + topic: "A feedback crossbreed β€” feed a proven source (caustic ink, attractor ink) as the seed of an integer-shift frame feedback" }, { id: "cgl", nm: "CGL 결함 λ‚œλ₯˜", nmEn: "CGL defect turbulence", en: "BOILING", f: "pde", st: "verified", x: 86, y: 72, tex: "λ‚˜μ„  결함듀이 νƒœμ–΄λ‚˜κ³  μ†Œλ©Έν•˜λŠ” λ“λŠ” μž₯ β€” 'λ“λŠ” 것'의 μ •μˆ˜.", @@ -299,15 +306,47 @@ export const ENTRIES: AtlasEntry[] = [ hints: ["4th-order PDE: use semi-implicit or spectral stepping", "if spectral on a real field: enforce Hermitian symmetry every step (hard-won lesson β€” roundoff ghosts grow exponentially otherwise)"] }, - { id: "ising", nm: "Ising 어닐링", nmEn: "Ising annealing", en: "CRITICAL DOMAINS", f: "pde", st: "unexplored", x: 46, y: 86, + { id: "ising", nm: "Ising 어닐링", nmEn: "Ising annealing", en: "CRITICAL DOMAINS", f: "pde", st: "verified", x: 46, y: 86, tex: "μžν™” 도메인이 μ˜¨λ„μ— 따라 μ–Όκ³  λ…ΉλŠ” ν†΅κ³„μ—­ν•™μ˜ μž₯.", texEn: "Magnetization domains freezing and melting with temperature β€” statistical mechanics as a field.", knob: "T(μ˜¨λ„) β€” μž„κ³„μ  근방의 ν”„λž™νƒˆ μš”λ™μ΄ ν•˜μ΄λΌμ΄νŠΈ.", knobEn: "temperature T β€” the critical point's fractal flicker is the highlight", + impl: "IsingModelPattern(2026-08-14 ν•€) β€” μ²΄μ»€λ³΄λ“œ Metropolis + κ΅­μ†Œ μžν™” 평균, μ‹€κΈ° 톡과.", + implEn: "IsingModelPattern (pinned 2026-08-14) β€” checkerboard Metropolis + local magnetization average, passed hardware.", topic: "Ising-model annealing β€” magnetization domains freezing and melting with temperature, fractal fluctuations near criticality", hints: ["checkerboard update for parallel-friendly sweeps", "render local magnetization averages, not raw spins (raw spins are pixel noise)"] }, + { id: "potts", nm: "Potts 결정립 μ‘°λŒ€ν™”", nmEn: "Potts grain growth", en: "GRAIN GROWTH", f: "pde", st: "unexplored", x: 33, y: 85, + tex: "μˆ˜μ‹­ 결정립이 μ„œλ‘œλ₯Ό μ‚Όν‚€λ©° κ΅΅μ–΄μ§€λŠ” κ±°ν’ˆ β€” μž…κ³„μ˜ 그물이 느리게 흐λ₯΄κ³ , μœ„μ—μ„œ μƒˆ 립이 내렀와 μ‘°λŒ€ν™”κ°€ λλ‚˜μ§€ μ•ŠλŠ”λ‹€.", + texEn: "Dozens of grains swallowing one another into a coarsening foam β€” a slow-flowing net of boundary walls, fresh grains fed from above so it never finishes.", + knob: "μ˜¨λ„ T = 0..1.2 (J λ‹¨μœ„) β€” λ™κ²°λœ 각진 립 ↔ λ°°νšŒν•˜λŠ” λΆ€λ“œλŸ¬μš΄ μž…κ³„ ↔ λ“λŠ” λ¬΄μ§ˆμ„œ.", + knobEn: "temperature T = 0..1.2 (units of J) β€” frozen faceted grains ↔ soft wandering boundaries ↔ boiling disorder", + topic: "q-state Potts model grain growth β€” a foam of domains coarsening forever, rendered as boundary walls over faint grain shades", + hints: ["q β‰ˆ 24 states in a Uint8Array; checkerboard Metropolis exactly like Ising, comparing neighbor agreement counts β€” all integer energy math", + "render boundary density (how many neighbors disagree) bright over a dim per-grain shade (state * constant mod 1)", + "coarsening stalls at equilibrium: keep feeding fresh random grains into the top rows and let renewal flow down the tall axis"] }, + + { id: "rps", nm: "κ°€μœ„λ°”μœ„λ³΄ λ‚˜μ„ ", nmEn: "Rock-paper-scissors spirals", en: "CYCLIC COMPETITION", f: "pde", st: "unexplored", x: 66, y: 80, + tex: "μ„Έ 쒅이 μˆœν™˜ν•˜λ©° μ„œλ‘œλ₯Ό λ¨ΉλŠ”λ‹€ β€” μ˜ν† κ°€ μ˜μ›νžˆ λ’€μ§‘νžˆκ³ , 침곡 전선이 감겨 λ„λŠ” λ‚˜μ„  νŒ”μ΄ λœλ‹€.", + texEn: "Three species each devouring the next in a cycle β€” territory forever overturning, invasion fronts winding into rotating spiral arms.", + knob: "이동도 = 0.5..8 β€” μ–½νžŒ μŠ€νŽ™ν΄ ↔ 맑은 νšŒμ „ λ‚˜μ„  ↔ ν•œ μ’…μ˜ μ„κΆŒ(λ©Έμ’… 전이).", + knobEn: "mobility = 0.5..8 β€” entangled speckle ↔ clean rotating spirals ↔ one species sweeps the board (the mobility-driven extinction transition)", + topic: "May-Leonard rock-paper-scissors on a lattice β€” cyclic invasion fronts winding into rotating spiral arms, territory forever overturning", + hints: ["states empty/A/B/C in a Uint8Array; random sequential pair events: selection (A kills B leaving empty), reproduction into empty, and neighbor exchange (mobility) β€” zero transcendentals", + "thousands of single-site events per frame; render species as three fixed tones with an invasion-age EMA so fresh fronts glow", + "the knob is the exchange-to-reaction ratio: past a critical mobility spirals outgrow the box and one species wins β€” reseed on extinction"] }, + + { id: "xy", nm: "XY μ†Œμš©λŒμ΄ 해리", nmEn: "XY vortex unbinding", en: "KOSTERLITZ-THOULESS", f: "pde", st: "unexplored", x: 57, y: 89, + tex: "μœ„μƒ μŠ€ν•€ μž₯의 비단결 β€” μ†Œμš©λŒμ΄ 쌍이 λ¬Άμ˜€λ‹€ ν’€λ Έλ‹€ ν•˜λ©°, 결이 흐λ₯΄λŠ” 천과 κ·Έ 결함이 ν•œ 화면에.", + texEn: "The silk grain of a phase-spin field β€” vortex pairs binding and unbinding, flowing cloth and its defects in one frame.", + knob: "μ˜¨λ„ T = 0.3..1.6 β€” 묢인 μ†Œμš©λŒμ΄ 쌍의 λ§€λˆν•œ 비단 ↔ T_KTβ‰ˆ0.89 ↔ 자유 μ†Œμš©λŒμ΄ ν”ŒλΌμ¦ˆλ§ˆ.", + knobEn: "temperature T = 0.3..1.6 β€” bound vortex pairs in smooth silk ↔ T_KT β‰ˆ 0.89 ↔ a free-vortex plasma (the Kosterlitz-Thouless transition)", + topic: "The 2D XY model crossing the Kosterlitz-Thouless transition β€” spin-phase silk with vortex pairs binding and unbinding", + hints: ["quantize angles to 256 bins (Uint8Array) and precompute a 256-entry cosine table in setup β€” the microcontroller cannot afford per-site trig, and with the table every energy evaluation is a lookup", + "checkerboard Metropolis; proposal = current angle Β± a small random step", + "render local alignment (mean table-cosine of the four neighbor differences) so silk reads bright and vortex cores dark; warm up below T_KT in setup"] }, + { id: "schrod", nm: "SchrΓΆdinger νŒŒμ†", nmEn: "SchrΓΆdinger packet", en: "QUANTUM CAUSTICS", f: "pde", st: "unexplored", x: 56, y: 82, tex: "λ³΅μ†Œ νŒŒλ™ν•¨μˆ˜ |ψ|²의 κ°„μ„­Β·ν„°λ„λ§Β·μ‚°λž€ β€” μ–‘μž μ»€μŠ€ν‹±.", texEn: "|ψ|Β² of a complex wavefunction β€” interference, tunneling, scattering. Quantum caustics.", @@ -321,8 +360,8 @@ export const ENTRIES: AtlasEntry[] = [ texEn: "A genealogy of flame-front chaos β€” cells born, merging and splitting, woven downward.", knob: "Domain(L)=16..64 β€” 동결 κΈ°λ‘₯ ↔ 세포 병합 ↔ λ•‹μž„ λ‚œλ₯˜.", knobEn: "domain length L = 16..64 β€” frozen columns ↔ breathing cell merges ↔ braided turbulence", - impl: "_temp/exploration/s01-waterfall/lineage.js Β· κΈ°μˆ κ²€μ¦ 톡과, νŒμ • λŒ€κΈ°", - implEn: "_temp/exploration/s01-waterfall/lineage.js Β· passed tech verification, awaiting verdict", + impl: "_temp/s01-waterfall/lineage.js Β· κΈ°μˆ κ²€μ¦ 톡과, νŒμ • λŒ€κΈ° (νŠΈλžœμŠ€ν¬λ¦½νŠΈμ—μ„œ 볡ꡬ됨)", + implEn: "_temp/s01-waterfall/lineage.js Β· passed tech verification, awaiting verdict (recovered from transcript)", topic: "A vertical history waterfall of 1D Kuramoto-Sivashinsky flame-front chaos β€” newest row on top, ancestry scrolling down", hints: ["spectral IMEX + Hermitian projection every step β€” roundoff otherwise grows a non-Hermitian ghost that blows up every ~300 time units", "an N=64 dealiased grid only resolves dissipation up to Lβ‰ˆ64"] }, @@ -332,8 +371,8 @@ export const ENTRIES: AtlasEntry[] = [ texEn: "Bright turbulent lace eating through a dark laminar field β€” a waterfall of infection fronts.", knob: "Chaos(a)=1.44..2.0 β€” μΈ΅λ₯˜ ↔ νΌμ½œλ ˆμ΄μ…˜ ↔ μ „λ©΄ λ‚œλ₯˜.", knobEn: "map coefficient a = 1.44..2.0 β€” laminar ↔ percolating fronts ↔ full turbulence (directed-percolation-like)", - impl: "_temp/exploration/s01-waterfall/blight.js Β· κΈ°μˆ κ²€μ¦ 톡과, νŒμ • λŒ€κΈ°", - implEn: "_temp/exploration/s01-waterfall/blight.js Β· passed tech verification, awaiting verdict", + impl: "_temp/s01-waterfall/blight.js Β· κΈ°μˆ κ²€μ¦ 톡과, νŒμ • λŒ€κΈ° (νŠΈλžœμŠ€ν¬λ¦½νŠΈμ—μ„œ 볡ꡬ됨)", + implEn: "_temp/s01-waterfall/blight.js Β· passed tech verification, awaiting verdict (recovered from transcript)", topic: "A waterfall of coupled-map-lattice spatiotemporal intermittency, rendered by the recurrence distance |x(t) - x(t-2)|", hints: ["render the recurrence distance, not spatial roughness β€” the laminar zigzag dithers into 1px stripes otherwise", "a slow EMA plus 3-tap smoothing fuses period-2 flicker into continuous filaments"] }, @@ -366,6 +405,50 @@ export const ENTRIES: AtlasEntry[] = [ topic: "A vertical waterfall of 1D complex Ginzburg-Landau β€” phase defects born and dying as spacetime stains", hints: ["complex field: use an amplitude ceiling as the divergence guard"] }, + { id: "kpz", nm: "KPZ 계면 μ„±μž₯", nmEn: "KPZ interface growth", en: "ROUGHENING FRONT", f: "waterfall", st: "unexplored", x: 69, y: 28, + tex: "μžλΌλŠ” κ³„λ©΄μ˜ 기울기 μž₯이 μ§€μΈ΅μœΌλ‘œ μŒ“μΈλ‹€ β€” λ§€λˆν•¨μ΄ 보편λ₯˜ 거칠기둜 λ¬΄λ„ˆμ§€λŠ” 기둝.", + texEn: "A growing interface's slope field laid down as strata β€” the record of smoothness collapsing into universal roughness.", + knob: "λΉ„μ„ ν˜• Ξ» = 0..4 β€” Edwards-Wilkinson λ§€λˆν•œ 이완 ↔ KPZ 탄도 거칠어짐 (보편λ₯˜ 전이).", + knobEn: "nonlinearity lambda = 0..4 β€” Edwards-Wilkinson smooth relaxation ↔ KPZ ballistic roughening (a universality-class crossing)", + topic: "A history waterfall of a 1D KPZ growing interface β€” each row the local slope of the height field, roughening strata scrolling down", + hints: ["h_i += dt * (nu * laplacian(h) + (lambda/2) * (grad h)^2 + noise); canonical nu = 1, dt = 0.05, seeded noise", + "raw height drifts unboundedly β€” subtract the mean height every step and render SLOPE or curvature, never raw h", + "64 cells on a ring; multiple substeps per frame scroll multiple rows"] }, + + { id: "traffic", nm: "유령 정체 μ‹œκ³΅κ°„", nmEn: "Phantom-jam spacetime", en: "NAGEL-SCHRECKENBERG", f: "waterfall", st: "unexplored", x: 50, y: 18, + tex: "아무도 브레이크λ₯Ό λ°Ÿμ§€ μ•Šμ•˜λŠ”λ° νƒœμ–΄λ‚˜λŠ” 정체 β€” 밝은 자유λ₯˜ 직쑰λ₯Ό 거슬러 였λ₯΄λŠ” μ–΄λ‘μš΄ 사선 μΆ©κ²©νŒŒλ“€.", + texEn: "Jams born from nothing β€” dark diagonal shockwaves climbing upstream through a bright free-flow weave.", + knob: "μ°¨λŸ‰ 밀도 ρ = 0.05..0.5 β€” 자유λ₯˜ ↔ μž„κ³„ ↔ μŠ€ν†±μ•€κ³  μ •μ²΄νŒŒ.", + knobEn: "car density rho = 0.05..0.5 β€” free flow ↔ critical ↔ stop-and-go jam waves (a genuine flow transition)", + risk: "μ°¨κ°€ μ…€ 수 μžˆλŠ” 개체둜 λ³΄μ΄λŠ” μˆœκ°„ 사망 β€” 속도μž₯/밀도μž₯으둜만 λ Œλ”.", + riskEn: "The moment cars are countable it dies β€” render the velocity/density field, never vehicles.", + topic: "A spacetime waterfall of Nagel-Schreckenberg traffic β€” backward-drifting jam shocks as dark diagonals in a bright free-flow weave", + hints: ["canonical: vmax = 5, randomization p = 0.25, a 64-cell ring", + "render local mean VELOCITY smoothed over ~3 cells with an EMA β€” raw occupancy is countable-car pixel noise", + "integer state only; several update sweeps per frame scroll several rows"] }, + + { id: "oslo", nm: "Oslo μŒ€λ”λ―Έ μ§€μΈ΅", nmEn: "Oslo rice-pile strata", en: "STICK-SLIP AVALANCHES", f: "waterfall", st: "unexplored", x: 71, y: 22, + tex: "ν•œ μ•Œμ”© 먹인 더미가 λ―Έλ„λŸ¬μ§ˆ λ•Œλ§Œ 흐λ₯Έλ‹€ β€” μ‚¬νƒœ ν™œλ™μ΄ μ§€μΈ΅μœΌλ‘œ μŒ“μ΄λŠ” λ©±λ²•μΉ™μ˜ 기둝.", + texEn: "A pile fed one grain at a time, flowing only when it slips β€” stick-slip activity laid down as power-law strata.", + knob: "ꡬ동λ₯  = ν”„λ ˆμž„λ‹Ή λ‚Ÿμ•Œ 1..12 β€” 고립된 μ‚¬νƒœ νŽ„μŠ€ ↔ μž„κ³„ κ²ΉμΉ¨ ↔ 연속 μš”λ™ (μ‹œκ°„μ²™λ„ 뢄리가 κΈ‰μ†Œ).", + knobEn: "drive rate = grains per frame 1..12 β€” isolated avalanche pulses ↔ critical overlap ↔ continuous churn (timescale separation is the dial)", + topic: "The Oslo rice-pile model as a history waterfall β€” local slope and topple activity written as ink, stick-slip avalanches leaving strata", + hints: ["1D pile, 64 sites; the Oslo rule: each site's critical slope is randomly 1 or 2, redrawn after every topple β€” this quenched randomness is what makes it critical (a plain 1D BTW pile is trivial)", + "render topple ACTIVITY with an EMA afterglow, layered over dim local slope β€” never raw heights", + "all integer arithmetic; feed grains at the top-left region so avalanches run down-slope"] }, + + { id: "voter", nm: "ν•©μ˜μ˜ μ›”λ“œλΌμΈ", nmEn: "Consensus worldlines", en: "COALESCING WALLS", f: "waterfall", st: "unexplored", x: 36, y: 16, + tex: "μˆ˜μ‹­ 의견의 경계 벽듀이 λžœλ€μ›Œν¬ν•˜λ‹€ λ§Œλ‚˜λ©΄ μ†Œλ©Έν•œλ‹€ β€” ν•©μ³μ§€κΈ°λ§Œ ν•˜λŠ” μ‹€λ“€μ˜ 직쑰, λŒμ—°λ³€μ΄κ°€ μƒˆ 싀을 λΏŒλ¦°λ‹€.", + texEn: "Walls between dozens of opinions random-walking until they meet and annihilate β€” a weave of threads that only ever merge, mutation seeding new ones.", + knob: "λŒμ—°λ³€μ΄μœ¨ = 0..0.02 β€” 순수 병합(ν•©μ˜λ‘œ 수렴) ↔ 정상 μƒνƒœ λ‹€μ–‘μ„± (흑수 상전이).", + knobEn: "mutation rate = 0..0.02 β€” pure coalescence toward consensus ↔ steady-state diversity (an absorbing-state transition)", + risk: "벽이 μ…€ 수 μžˆλŠ” μ„ μœΌλ‘œ 남을 μœ„ν—˜ β€” 초기 밀도와 λŒμ—°λ³€μ΄ κ³΅κΈ‰μœΌλ‘œ 직쑰 밀도λ₯Ό 지킬 것.", + riskEn: "Walls risk reading as countable lines β€” keep the weave dense via initial diversity and mutation supply.", + topic: "A 1D multi-opinion voter model as a history waterfall β€” domain walls as glowing worldlines that wander, collide and merge, mutation reseeding fresh threads", + hints: ["init every cell to a distinct opinion (Uint8Array, up to 255 states) so the weave starts saturated", + "render CHANGE: a wall glows where neighbors differ, with an EMA trail β€” the state values themselves stay invisible", + "random sequential updates: a site copies a random neighbor; mutation assigns a fresh state"] }, + { id: "lenia", nm: "Lenia", nmEn: "Lenia", en: "CONTINUOUS LIFE", f: "life", st: "unexplored", x: 60, y: 72, tex: "연속 컀널 CA β€” λΆ€λ“œλŸ¬μš΄ 생λͺ… ν˜•νƒœκ°€ ν—€μ—„μΉ˜λŠ” μž₯.", texEn: "The continuous-kernel cellular automaton β€” soft life forms swimming as a field.", @@ -392,18 +475,34 @@ export const ENTRIES: AtlasEntry[] = [ texEn: "A velocity field evolving itself (advection, pressure projection) while carrying ink β€” vortices that truly interact.", knob: "점성 / vorticity confinement β€” λˆμ ν•œ 흐름 ↔ κ²©λ ¬ν•œ λ‚œλ₯˜.", knobEn: "viscosity / vorticity confinement β€” sticky flow ↔ violent turbulence", - topic: "Stable Fluids β€” a coarse self-evolving velocity field (semi-Lagrangian advection + pressure projection) carrying a full-res ink density", - hints: ["a 32Γ—64 velocity grid carrying 64Γ—128 ink is enough", "a few Jacobi iterations of pressure projection suffice"] }, - - { id: "sandpile", nm: "Abelian μ‚¬νƒœ", nmEn: "Abelian avalanches", en: "SELF-ORGANIZED CRITICALITY", f: "critical", st: "unexplored", x: 79, y: 52, + risk: "밀도μž₯ semi-Lagrangian λ¦¬μƒ˜ν”Œμ€ μ‹€κΈ° μ˜ˆμ‚° 초과 (8μ›” ν•„λ“œ 데이터) β€” μž‰ν¬λŠ” μž…μžλ‘œ μ‹€μ–΄λ‚˜λ₯Ό 것.", + riskEn: "Semi-Lagrangian density resampling blows the hardware budget (August field data) β€” carry the ink with particles instead.", + topic: "Stable Fluids β€” a coarse self-evolving velocity field (semi-Lagrangian advection + pressure projection on the coarse grid only), its ink carried by advected particles depositing at full res", + hints: ["a 32Γ—64 velocity grid is enough; resample only the coarse velocity field, never a full-res density field", + "a few Jacobi iterations of pressure projection suffice", + "carry ink as particles depositing into a fade buffer β€” the GaleInk route"] }, + + { id: "sandpile", nm: "Abelian μ‚¬νƒœ", nmEn: "Abelian avalanches", en: "SELF-ORGANIZED CRITICALITY", f: "critical", st: "verified", x: 79, y: 52, tex: "μž„κ³„ 격자의 μ‚¬νƒœ 연쇄 파면 β€” 멱법칙: μž”λ°˜μ§μž„ 속 가끔 화면을 μ‚Όν‚€λŠ” λŒ€λΆ•κ΄΄.", texEn: "Cascade fronts of a critical lattice β€” power law: small flickers, and occasionally a collapse that swallows the screen.", knob: "λ‚™μ‚¬μœ¨/μ†Œμ‚° β€” μ•„μž„κ³„ ↔ 자기쑰직 μž„κ³„.", knobEn: "drop rate / dissipation β€” subcritical ↔ self-organized criticality", + impl: "WaveCoupling(2026-08-12 ν•€) β€” μœ„μƒ 연동 λ¬Έν„± λΆ•κ΄΄ λ³€μ£Ό, μ‹€κΈ° 톡과.", + implEn: "WaveCoupling (pinned 2026-08-12) β€” a phase-locked-threshold cascade variant, passed hardware.", topic: "An Abelian sandpile β€” render the cascading avalanche FRONTS as brightness, never individual grains", hints: ["render avalanche wavefronts with fade; hide the raw lattice values", "power-law sizes: mostly small flickers, occasionally a screen-swallowing collapse"] }, + { id: "forestfire", nm: "μ‚°λΆˆ μž„κ³„ μˆœν™˜", nmEn: "Forest-fire criticality", en: "DROSSEL-SCHWABL", f: "critical", st: "unexplored", x: 85, y: 60, + tex: "μžλΌλŠ” 숲, 벼락, 지도λ₯Ό λ¨Ήμ–΄μΉ˜μš°λŠ” ν™”μ„  β€” νƒœμš°κ³  λ‹€μ‹œ 자라며 슀슀둜 μž„κ³„λ‘œ λŒμ•„μ˜€λŠ” λͺ¨μžμ΄ν¬.", + texEn: "A regrowing forest, lightning, fronts that eat the map β€” a mosaic burning and regrowing its way back to criticality.", + knob: "μ„±μž₯/벼락 λΉ„ p/f = 50..2000 β€” μž”λΆˆ λ°˜μ§μž„ ↔ 척도 μ—†λŠ” ν™”μž¬ λͺ¨μžμ΄ν¬ ↔ 화면을 μ‚Όν‚€λŠ” λŒ€ν™”μž¬.", + knobEn: "growth-to-lightning ratio p/f = 50..2000 β€” small sparks ↔ scale-free fire mosaics ↔ system-spanning burns", + topic: "The Drossel-Schwabl forest-fire model β€” burning fronts sweeping a regrowing forest, fire scars flowing as dark rivers", + hints: ["states empty/tree/burning in a Uint8Array; per sweep: fire ignites neighboring trees, burning becomes empty, empty regrows with probability p, lightning strikes trees with probability f β€” all integer", + "render fire bright over dim forest, with an EMA afterglow buffer so scars fade like rivers of ash", + "the timescale separation f << p is what self-organizes criticality; keep both under one ratio knob"] }, + { id: "dla", nm: "DLA 응집 ν™•λ₯ μž₯", nmEn: "DLA probability field", en: "AGGREGATION FIELD", f: "critical", st: "unexplored", x: 52, y: 33, tex: "ν™•μ‚° 응집을 개체 μ—†λŠ” ν™•λ₯ μž₯으둜 β€” μ„œλ¦¬ κ°€μ§€κ°€ μžλΌλŠ” μž₯.", texEn: "Diffusive aggregation as an object-free probability field β€” frost branches growing as a field.", @@ -526,11 +625,13 @@ export const ENTRIES: AtlasEntry[] = [ "render v from 1 - |field|, sharpened around the zero crossings, so the nodal lines glow", "grade the mode index along y for a vertical ladder of complexity"] }, - { id: "invasion", nm: "침투 νΌμ½œλ ˆμ΄μ…˜", nmEn: "Invasion percolation", en: "WEAKEST-PATH INVASION", f: "critical", st: "unexplored", x: 55, y: 47, + { id: "invasion", nm: "침투 νΌμ½œλ ˆμ΄μ…˜", nmEn: "Invasion percolation", en: "WEAKEST-PATH INVASION", f: "critical", st: "verified", x: 55, y: 47, tex: "동결 λ¬΄μ§ˆμ„œ κ²©μžμ—μ„œ 항상 κ°€μž₯ μ•½ν•œ μ΄μ›ƒλ§Œ 뚫고 λ²ˆμ§€λŠ” 침투 β€” ν™•μ‚° μ—†λŠ” ν”„λž™νƒˆ 손가락, μž„κ³„κ°€ λ‚΄μž₯된 μ„±μž₯. (연ꡬ μˆ˜μž…)", texEn: "An invasion that always breaks the weakest neighboring site of a frozen disorder field β€” fractal fingers without diffusion; growth with criticality built in. (research import)", knob: "λ¬΄μ§ˆμ„œ 상관길이 / νŠΈλž˜ν•‘ κ·œμΉ™ β€” κ°€λŠ” ν”„λž™νƒˆ 손가락 ↔ λ­‰νˆ­ν•œ μ••μΆ• μ „μ„ .", knobEn: "disorder correlation length / trapping rule β€” thin fractal fingers ↔ blunt compact fronts", + impl: "FantasiaGarden(2026-08-11 ν•€) β€” μ €ν•­ 뱅크 슀크둀 + 침투 λ‚˜μ΄ λ Œλ”, μ‹€κΈ° 톡과.", + implEn: "FantasiaGarden (pinned 2026-08-11) β€” scrolling resistance bank + invasion-age render, passed hardware.", topic: "Invasion percolation β€” a front that always advances through the weakest site of a quenched random resistance field, rendered as an object-free invasion-age field", hints: ["keep a frontier set; each step invade the minimum-resistance frontier site (a small heap, or a periodic min-scan, is fine at this scale)", "render invasion AGE as tone β€” the freshly invaded glows, old territory fades; never individual sites", @@ -576,11 +677,13 @@ export const ENTRIES: AtlasEntry[] = [ "let the hidden signal itself morph slowly (large smooth abstract blobs) so the surfacing picture is alive", "bonus curtain: sweep the noise amplitude along y so a resonant band glows mid-frame"] }, - { id: "choke", nm: "Choke β€” 배타 μˆ˜μ†‘ 좩격파", nmEn: "Choke β€” exclusion shockwaves", en: "ASEP BOUNDARY PHASES", f: "invent", st: "invented", x: 78, y: 24, + { id: "choke", nm: "Choke β€” 배타 μˆ˜μ†‘ 좩격파", nmEn: "Choke β€” exclusion shockwaves", en: "ASEP BOUNDARY PHASES", f: "invent", st: "verified", x: 78, y: 24, tex: "ν•œ λ°©ν–₯으둜만 흐λ₯΄λŠ” 배타 μž…μžλ“€μ˜ 밀도μž₯ β€” 정체 μΆ©κ²©νŒŒκ°€ 흐름을 거슬러 κΈ°μ–΄μ˜€λ₯΄κ³  ν¬λ°•νŒŒκ°€ λΆ€μ±„κΌ΄λ‘œ νŽ΄μ§„λ‹€. μž…μžλŠ” μ•ˆ 보이고 λ°€λ„μ˜ μ§€μΈ΅λ§Œ 흐λ₯Έλ‹€.", texEn: "The density field of one-way excluding particles β€” jam shockwaves crawling upstream, rarefaction fans spreading. No particles visible, only strata of density flowing.", knob: "μ£Όμž…/배좜λ₯  β€” 저밀도 / 고밀도 / μ΅œλŒ€λ₯˜ 상을 κ°€λ₯΄λŠ” μ‹€μ œ 경계 상전이 (1μ°¨ 전이 포함).", knobEn: "injection/extraction rates β€” the boundary-driven phase diagram (low-density / high-density / maximal-current, with a genuine first-order line)", + impl: "DefectCascade(2026-08-14 ν•€) β€” 발λͺ… λŒ€λ₯™ 첫 검증. 1D 동역학 + 이λ ₯ 슀크둀 = μ‹€κΈ° μ΅œμ €λΉ„μš© 골격의 증λͺ….", + implEn: "DefectCascade (pinned 2026-08-14) β€” the invented continent's first verification. 1D dynamics + history scroll, proof of the cheapest hardware architecture.", topic: "A boundary-driven exclusion process (ASEP) rendered as a coarse-grained density field flowing down the frame β€” jam shocks climbing against the flow, rarefaction fans, and boundary-rate phase transitions", hints: ["simulate a 1D ASEP with random sequential updates per row-time and render coarse-grained density as a history waterfall β€” never individual particles", "alpha (inject) and beta (extract) span the phase diagram: alpha0.5 maximal current", @@ -652,6 +755,7 @@ Rendering craft β€” most attempts die here, read carefully: - Simulate in the display's own orientation. Internal buffers are 64 wide Γ— 128 tall (index = y * 64 + x), matching // @matrix 64x128. Do NOT build a 128Γ—64 landscape simulation and rotate or remap it inside draw() β€” no axis swaps, no dispX = y tricks. If you catch yourself writing const w = 128, h = 64, stop: swap them. (Most LED-matrix code online is landscape; this device stands tall.) - Choose @knobs ranges so the pattern is at its best near the MIDDLE of every range, with nobody touching anything. Knob extremes may be calm or violent; the default position is the show. - Simulations must sit in their interesting regime at those defaults β€” use the canonical parameter values from the hints below when given; do not invent your own. +- This code is also compiled for a 240 MHz microcontroller, where sin/cos/exp/pow/atan2 each cost hundreds of cycles. Budget transcendentals: keep them OUT of the per-pixel loop β€” per-agent, per-row, or per-timestep math is fine, and integer/add/multiply lattice rules are free. At most one full-resolution pass per frame, carrying no more than a couple of trig calls per pixel. Never resample the previous frame per pixel (no warp/zoom/bilinear feedback), never do O(nΒ²) all-pairs interactions, and use Float32Array only β€” never Float64Array (doubles are software-emulated). Taste direction (settled by experiment on this device β€” treat as hard constraints, on top of everything above): - No countable objects. Thousands of accumulated operations must read as one continuous "material". The moment dots/creatures/cars can be counted, the pattern is dead. @@ -659,7 +763,7 @@ Taste direction (settled by experiment on this device β€” treat as hard constrai - Refining "creative control mapping" above: Knob 1 must grip a real coefficient of the underlying equation (the critical knob) β€” turning it must cross a phase transition or bifurcation, not just restyle. Knob 3 = density/scale, knob 4 = fade/persistence (wire ↔ smoke) have worked well. - Morphing: let secondary coefficients breathe slowly on incommensurate periods so that five minutes in it is not the same picture. The autonomous morphing must NOT ride the critical coefficient itself β€” pumping it injects energy and can blow the system up; morph through harmless axes (time compression, render transforms). - The long vertical axis is the protagonist: falling, rising, columns, history scrolling downward. -- Reliability: clamp dt (~0.1 max), detect divergence and auto-reseed, use a seeded RNG instead of Math.random, allocate every buffer in setup, attach helper functions to params as closures (survives layer flattening), and stay ESP32-friendly (tens of thousands of operations per frame). +- Reliability: clamp dt (~0.1 max), detect divergence and auto-reseed, use a seeded RNG instead of Math.random, allocate every buffer in setup, attach helper functions to params as closures (survives layer flattening), and stay inside the microcontroller budget stated under Rendering craft. - Each pattern must declare in its first comment: "critical knob = ___, turning it crosses ___ ↔ ___". - Discard any idea that fails these rules β€” output only survivors. If you cannot execute and test the code, mark the pattern "UNVERIFIED".`; From 123637586a10854374eb7f5e34f0a0f0b6827492 Mon Sep 17 00:00:00 2001 From: engmung <122682380+engmung@users.noreply.github.com> Date: Sun, 16 Aug 2026 23:07:35 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat(web):=20research=20notes=20=E2=80=94?= =?UTF-8?q?=20the=20atlas=20keeps=20its=20failures=20on=20record?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Of twenty-five field patterns, sixteen didn't earn a tile β€” and deleting them would delete the data. atlas_pins gains a kind column: "pin" stays the exemplar tile; "research" files an attempt against the entry it came from. Research rows draw nothing on the map β€” they live behind a "Research attempts Β· N" fold in the entry panel, exactly where someone about to walk the same ground would look. A research note may reference a private pattern (a failure often is): the read path shows those only to their author and moderators, so the shared map leaks nothing. The picker now lists private patterns too, tagged "private β†’ research" β€” dropping one files it as research and must land near an entry, because an unmoored note would be invisible everywhere. The pin panel demotes a tile to research and promotes a public note back. Verified in the browser anonymously: research rows render no tiles, the fold lists the public note, and the private note stays hidden. Owner-side flows (place, promote, demote) are code-reviewed only β€” no login session in dev. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- web/drizzle/0022_atlas-pin-kind.sql | 1 + web/drizzle/meta/_journal.json | 9 +- web/src/app/api/community/atlas/route.ts | 43 ++++- web/src/app/community/atlas/page.tsx | 21 ++- web/src/components/community/Atlas.module.css | 7 + web/src/components/community/AtlasClient.tsx | 156 ++++++++++++++++-- web/src/lib/community/queries.ts | 19 ++- web/src/lib/community/schema.ts | 8 + 8 files changed, 226 insertions(+), 38 deletions(-) create mode 100644 web/drizzle/0022_atlas-pin-kind.sql diff --git a/web/drizzle/0022_atlas-pin-kind.sql b/web/drizzle/0022_atlas-pin-kind.sql new file mode 100644 index 0000000..2452ff2 --- /dev/null +++ b/web/drizzle/0022_atlas-pin-kind.sql @@ -0,0 +1 @@ +ALTER TABLE `atlas_pins` ADD `kind` text DEFAULT 'pin' NOT NULL; diff --git a/web/drizzle/meta/_journal.json b/web/drizzle/meta/_journal.json index af5ad77..fdc1b81 100644 --- a/web/drizzle/meta/_journal.json +++ b/web/drizzle/meta/_journal.json @@ -155,6 +155,13 @@ "when": 1786860000000, "tag": "0021_pattern-performances", "breakpoints": true + }, + { + "idx": 22, + "version": "6", + "when": 1786880000000, + "tag": "0022_atlas-pin-kind", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/web/src/app/api/community/atlas/route.ts b/web/src/app/api/community/atlas/route.ts index f8ccf57..8749b55 100644 --- a/web/src/app/api/community/atlas/route.ts +++ b/web/src/app/api/community/atlas/route.ts @@ -9,17 +9,21 @@ import { rateLimit } from "@/lib/community/ratelimit"; import { atlasPins, patterns } from "@/lib/community/schema"; // GET /api/community/atlas β€” every pattern pinned on the atlas. -// POST β€” place or move a pin: { patternId, x, y } in the atlas's 0..100 -// data space. Only the pattern's author (or a moderator) may. +// POST β€” place or move a pin: { patternId, x, y, entryId?, kind? } in the +// atlas's 0..100 data space. Only the pattern's author (or a +// moderator) may. kind "pin" (default) is an exemplar tile on the +// shared map; kind "research" files the pattern against an entry as a +// field note β€” failures stay on record where they happened, without +// occupying the map. // DELETE β€” take a pin off the map: { patternId }. Same permission. // // A pin says "this work lives at this spot of pattern space". One per -// pattern; placing again just moves it. +// pattern; placing again just moves it (and can change its kind). export async function GET(request: Request) { const blocked = originBlocked(request); if (blocked) return blocked; - return withCors(request, await handleGet()); + return withCors(request, await handleGet(request)); } export async function POST(request: Request) { @@ -36,17 +40,21 @@ export async function DELETE(request: Request) { export const OPTIONS = preflight; -async function handleGet() { +async function handleGet(request: Request) { if (!communityEnabled()) { return Response.json({ error: "Community is not enabled on this deployment." }, { status: 503 }); } - const pins = await listAtlasPins(); + const session = await getAuth().api.getSession({ headers: request.headers }); + const pins = await listAtlasPins( + session ? { id: session.user.id, isAdmin: isAdminSession(session) } : null, + ); return Response.json({ pins: pins.map((pin) => ({ patternId: pin.patternId, x: pin.x, y: pin.y, entryId: pin.entryId, + kind: pin.kind, title: pin.title, userId: pin.userId, username: pin.username, @@ -107,7 +115,15 @@ async function handleWrite(request: Request, mode: "place" | "remove") { return Response.json({ ok: true }); } - if (pattern.visibility !== "public") { + const kind = payload.kind === undefined ? "pin" : payload.kind; + if (kind !== "pin" && kind !== "research") { + return Response.json({ error: "Unknown pin kind." }, { status: 400 }); + } + + // A map pin is a tile everyone sees, so it must be public. A research row is + // a field note β€” private failures are allowed, because the read path already + // shows those only to their author and moderators. + if (kind === "pin" && pattern.visibility !== "public") { return Response.json( { error: "Only public patterns can sit on the shared map." }, { status: 400 }, @@ -131,13 +147,22 @@ async function handleWrite(request: Request, mode: "place" | "remove") { return Response.json({ error: "No such point on the map." }, { status: 400 }); } + // A research row exists to remember where an attempt happened β€” unmoored + // from any entry it would be invisible everywhere, so refuse the no-op. + if (kind === "research" && entryId === null) { + return Response.json( + { error: "Research notes attach to a point β€” drop it closer to one." }, + { status: 400 }, + ); + } + const now = new Date(); await getDb() .insert(atlasPins) - .values({ patternId, x, y, entryId, updatedAt: now }) + .values({ patternId, x, y, entryId, kind, updatedAt: now }) .onConflictDoUpdate({ target: atlasPins.patternId, - set: { x, y, entryId, updatedAt: now }, + set: { x, y, entryId, kind, updatedAt: now }, }); return Response.json({ ok: true }); diff --git a/web/src/app/community/atlas/page.tsx b/web/src/app/community/atlas/page.tsx index 73afa79..8aebb1a 100644 --- a/web/src/app/community/atlas/page.tsx +++ b/web/src/app/community/atlas/page.tsx @@ -35,16 +35,23 @@ export default async function AtlasPage() { const viewerId = session?.user.id ?? null; const isAdmin = isAdminSession(session); - const pins = await listAtlasPins(); + const pins = await listAtlasPins(viewerId ? { id: viewerId, isAdmin } : null); - // The picker: the viewer's own public patterns that are not on the map yet, - // newest first (listPatternsByUser already orders that way) and carrying - // their code β€” you pick by looking at the pattern, not by reading a title. + // The picker: the viewer's own patterns that are not on the map yet, newest + // first (listPatternsByUser already orders that way) and carrying their code + // β€” you pick by looking at the pattern, not by reading a title. Private ones + // ride along flagged: they can only be filed as research notes, never as + // map tiles, and the client routes them there. const pinned = new Set(pins.map((pin) => pin.patternId)); const myPatterns = viewerId ? (await listPatternsByUser(viewerId, viewerId)) - .filter((pattern) => pattern.visibility === "public" && !pinned.has(pattern.id)) - .map((pattern) => ({ id: pattern.id, title: pattern.title, code: pattern.code })) + .filter((pattern) => !pinned.has(pattern.id)) + .map((pattern) => ({ + id: pattern.id, + title: pattern.title, + code: pattern.code, + isPublic: pattern.visibility === "public", + })) : []; return ( @@ -54,6 +61,8 @@ export default async function AtlasPage() { x: pin.x, y: pin.y, entryId: pin.entryId, + kind: pin.kind === "research" ? ("research" as const) : ("pin" as const), + isPublic: pin.visibility === "public", title: pin.title, code: pin.code, userId: pin.userId, diff --git a/web/src/components/community/Atlas.module.css b/web/src/components/community/Atlas.module.css index 98484ed..b002b72 100644 --- a/web/src/components/community/Atlas.module.css +++ b/web/src/components/community/Atlas.module.css @@ -205,6 +205,13 @@ color: var(--pfc-ink); } +/* Private patterns in the picker can only become research notes β€” the tag + says where the drop will land before anything is dropped. */ +.researchTag { + color: var(--pfc-led, #e8552e); + opacity: 0.85; +} + .pickerHead { font-family: var(--pf-mono, monospace); font-size: 9.5px; diff --git a/web/src/components/community/AtlasClient.tsx b/web/src/components/community/AtlasClient.tsx index fe5bb14..b9b769b 100644 --- a/web/src/components/community/AtlasClient.tsx +++ b/web/src/components/community/AtlasClient.tsx @@ -64,6 +64,13 @@ export type AtlasPinData = { x: number; y: number; entryId: string | null; + /** "pin" = an exemplar tile on the map. "research" = a field note filed + * against an entry β€” kept as data, revealed from the entry panel, never + * drawn as a tile. Failures are worth remembering where they happened. */ + kind: "pin" | "research"; + /** Whether the pattern itself is public β€” a research note is allowed to be + * private, and a private note can never be promoted to a map tile. */ + isPublic: boolean; title: string; code: string; userId: string; @@ -105,6 +112,14 @@ const UI = { madeFrom: "Made from prompt", linkNone: "β€” none β€”", patternsFrom: "Patterns from this prompt", + researchFrom: (n: number) => `Research attempts Β· ${n}`, + researchBadge: "research note", + researchHint: "Kept as data against this point β€” not drawn on the map.", + fileResearch: "File as research (hide the tile)", + promotePin: "Put on the map as a tile", + placeBannerResearch: (title: string) => + `Filing β€œ${title}” as research β€” drop it on the point it came from`, + privateTag: "private β†’ research", knobs: "Knobs", addPattern: "Add my pattern", pickerHead: (n: number) => `Your patterns Β· newest first (${n})`, @@ -143,6 +158,13 @@ const UI = { madeFrom: "μΆœμ‹  ν”„λ‘¬ν”„νŠΈ", linkNone: "β€” μ—†μŒ β€”", patternsFrom: "이 ν”„λ‘¬ν”„νŠΈμ—μ„œ λ‚˜μ˜¨ νŒ¨ν„΄", + researchFrom: (n: number) => `연ꡬ 기둝 Β· ${n}`, + researchBadge: "연ꡬ 기둝", + researchHint: "이 ν¬μΈνŠΈμ— λ°μ΄ν„°λ‘œλ§Œ λ‚¨μŒ β€” μ§€λ„μ—λŠ” κ·Έλ €μ§€μ§€ μ•ŠλŠ”λ‹€.", + fileResearch: "연ꡬ 기둝으둜 내리기 (타일 μˆ¨κΉ€)", + promotePin: "지도 νƒ€μΌλ‘œ 올리기", + placeBannerResearch: (title: string) => `β€œ${title}” 연ꡬ 기둝 쀑 β€” μΆœμ‹  포인트 μœ„μ— 놓기`, + privateTag: "λΉ„κ³΅κ°œ β†’ 연ꡬ 기둝", knobs: "λ…ΈλΈŒ", addPattern: "λ‚΄ νŒ¨ν„΄ 올리기", pickerHead: (n: number) => `λ‚΄ νŒ¨ν„΄ Β· μ΅œμ‹ μˆœ (${n})`, @@ -159,8 +181,10 @@ export default function AtlasClient({ pins: AtlasPinData[]; viewerId: string | null; isAdmin: boolean; - /** The viewer's own public patterns not yet on the map β€” the picker. */ - myPatterns: Array<{ id: string; title: string; code: string }>; + /** The viewer's own patterns not yet on the map β€” the picker. Private ones + * can only be filed as research notes, and the placement flow routes them + * there. */ + myPatterns: Array<{ id: string; title: string; code: string; isPublic: boolean }>; }) { const router = useRouter(); // Reading straight from the store (rather than syncing state in an effect) @@ -180,7 +204,12 @@ export default function AtlasClient({ const [view, setView] = useState({ k: 1, tx: 0, ty: 0 }); // "Add my pattern": pick from the toolbar, then click the map to drop. const [pickerOpen, setPickerOpen] = useState(false); - const [placing, setPlacing] = useState<{ id: string; title: string; code: string } | null>(null); + const [placing, setPlacing] = useState<{ + id: string; + title: string; + code: string; + isPublic: boolean; + } | null>(null); // While placing: the entry the cursor would link to (nearest within radius). const [placingNear, setPlacingNear] = useState(null); // The picker lists your own patterns newest first, as thumbnails β€” you @@ -232,6 +261,10 @@ export default function AtlasClient({ const entryPos = (e: AtlasEntry): [number, number] => positions[e.id] ?? [e.x, e.y]; const canEditPin = (pin: AtlasPinData) => isAdmin || (viewerId !== null && viewerId === pin.userId); + // Only exemplars are drawn on the map; research rows surface from the entry + // panel. Both live in `pins`, so selecting either opens the same panel. + const mapPins = pins.filter((pin) => pin.kind === "pin"); + const selectedEntry = selection?.kind === "entry" ? ENTRIES.find((e) => e.id === selection.id) ?? null : null; const selectedPin = @@ -342,7 +375,7 @@ export default function AtlasClient({ if (!placing || dragMovedRef.current) return; const coords = toDataCoords(ev.clientX, ev.clientY); if (!coords) return; - void placePattern(placing.id, coords); + void placePattern(placing, coords); }; /* ── server writes ── */ @@ -351,7 +384,7 @@ export default function AtlasClient({ await fetch("/api/community/atlas", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ patternId: pin.patternId, x, y, entryId: pin.entryId }), + body: JSON.stringify({ patternId: pin.patternId, x, y, entryId: pin.entryId, kind: pin.kind }), }); router.refresh(); }; @@ -361,25 +394,42 @@ export default function AtlasClient({ await fetch("/api/community/atlas", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ patternId: pin.patternId, x, y, entryId }), + body: JSON.stringify({ patternId: pin.patternId, x, y, entryId, kind: pin.kind }), + }); + router.refresh(); + }; + + /** Demote a tile to a research note, or promote a note back to a tile. */ + const setPinKind = async (pin: AtlasPinData, kind: "pin" | "research") => { + const [x, y] = pinPos(pin); + await fetch("/api/community/atlas", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ patternId: pin.patternId, x, y, entryId: pin.entryId, kind }), }); router.refresh(); }; - const placePattern = async (patternId: string, coords: [number, number]) => { + const placePattern = async (pattern: NonNullable, coords: [number, number]) => { // Dropping NEAR a prompt point is pointing at it β€” the nearest entry // within LINK_RADIUS becomes the lineage. Dropping in open sea links to // nothing; the pin panel's select stays for corrections either way. + // + // A private pattern files as a research note, and a note without a point + // would be invisible everywhere β€” so that drop must land near an entry + // (the banner says so; a miss is a no-op, not an error). const entryId = nearestEntry(coords); + const kind = pattern.isPublic ? "pin" : "research"; + if (kind === "research" && !entryId) return; const res = await fetch("/api/community/atlas", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ patternId, x: coords[0], y: coords[1], entryId }), + body: JSON.stringify({ patternId: pattern.id, x: coords[0], y: coords[1], entryId, kind }), }); if (res.ok) { setPlacing(null); setPlacingNear(null); - setSelection({ kind: "pin", id: patternId }); + setSelection({ kind: "pin", id: pattern.id }); router.refresh(); } }; @@ -490,7 +540,12 @@ export default function AtlasClient({ className={styles.pinWell} /> - {pattern.title} + + {pattern.title} + {!pattern.isPublic && ( + {t.privateTag} + )} + ))} @@ -533,7 +588,7 @@ export default function AtlasClient({ {placing && (
- {t.placeBanner(placing.title)} + {placing.isPublic ? t.placeBanner(placing.title) : t.placeBannerResearch(placing.title)} {placingNear && ENTRY_BY_ID.has(placingNear) && ( {" "}β†’ {entryName(ENTRY_BY_ID.get(placingNear)!)} @@ -612,7 +667,7 @@ export default function AtlasClient({ {/* Lineage threads: a pin hangs from the prompt that made it. */} - {pins.map((pin) => { + {mapPins.map((pin) => { if (!pin.entryId) return null; const entry = ENTRY_BY_ID.get(pin.entryId); if (!entry) return null; @@ -684,7 +739,7 @@ export default function AtlasClient({ ); })} - {pins.map((pin) => { + {mapPins.map((pin) => { const [px, py] = pinPos(pin); const isSelected = selection?.kind === "pin" && selection.id === pin.patternId; const draggable = editMode && canEditPin(pin); @@ -752,6 +807,13 @@ export default function AtlasClient({
{t.pinBy} {selectedPin.displayUsername ?? selectedPin.username ?? "?"}
+ {selectedPin.kind === "research" && ( +
+ + {t.researchBadge} + +
+ )}
{t.madeFrom}
@@ -761,7 +823,9 @@ export default function AtlasClient({ value={selectedPin.entryId ?? ""} onChange={(ev) => void setPinLink(selectedPin, ev.target.value || null)} > - + {/* A research note without a point would be invisible + everywhere, so that one option disappears for them. */} + {selectedPin.kind === "pin" && } {ENTRIES.map((entry) => (
+ + )} +
{t.secPrompt}
{buildPrompt(selectedEntry)}
diff --git a/web/src/lib/community/queries.ts b/web/src/lib/community/queries.ts index aa24c13..0af820e 100644 --- a/web/src/lib/community/queries.ts +++ b/web/src/lib/community/queries.ts @@ -644,16 +644,21 @@ export async function listPresence(): Promise { /** * Patterns placed on the atlas (/community/atlas), with enough of the pattern - * row to render a live tile. Only public patterns appear β€” an unlisted work is - * link-only everywhere else, and a spot on the shared map would un-unlist it. + * row to render a live tile. Map pins ("pin") are public patterns only β€” an + * unlisted work is link-only everywhere else, and a spot on the shared map + * would un-unlist it. Research rows ("research") may be private: everyone sees + * the public ones, but a private failure is shown only to its author (or a + * moderator) β€” pass the viewer so the filter can tell. */ -export async function listAtlasPins() { - return getDb() +export async function listAtlasPins(viewer?: { id: string; isAdmin: boolean } | null) { + const rows = await getDb() .select({ patternId: atlasPins.patternId, x: atlasPins.x, y: atlasPins.y, entryId: atlasPins.entryId, + kind: atlasPins.kind, + visibility: patterns.visibility, title: patterns.title, code: patterns.code, userId: patterns.userId, @@ -662,8 +667,12 @@ export async function listAtlasPins() { .from(atlasPins) .innerJoin(patterns, eq(atlasPins.patternId, patterns.id)) .innerJoin(user, eq(patterns.userId, user.id)) - .where(eq(patterns.visibility, "public")) .orderBy(atlasPins.updatedAt); + return rows.filter((row) => { + if (row.visibility === "public") return true; + if (row.kind !== "research") return false; // a map pin never carries a non-public pattern + return Boolean(viewer && (viewer.isAdmin || viewer.id === row.userId)); + }); } /** diff --git a/web/src/lib/community/schema.ts b/web/src/lib/community/schema.ts index edd48d2..55b1d77 100644 --- a/web/src/lib/community/schema.ts +++ b/web/src/lib/community/schema.ts @@ -479,6 +479,14 @@ export const atlasPins = sqliteTable("atlas_pins", { * panel can retarget it. Null = placed in open water, tied to nothing. */ entryId: text("entry_id"), + /** + * "pin" = an exemplar tile on the shared map. "research" = a field note: an + * attempt filed against an entry, kept as data but not drawn as a tile β€” + * failures are worth remembering exactly where they happened. Research rows + * may reference private patterns (a failure often is); the read path only + * shows those to their author and moderators, so nothing private leaks. + */ + kind: text("kind").notNull().default("pin"), updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), }); From 85c2a4c6a6abd44c906071bff47d73c54400cc0c Mon Sep 17 00:00:00 2001 From: engmung <122682380+engmung@users.noreply.github.com> Date: Sun, 16 Aug 2026 23:17:46 +0900 Subject: [PATCH 3/6] feat(web): the map admits which ground was walked and dropped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Uncharted" was carrying two meanings that are nothing alike: land nobody has walked, and land walked and abandoned. Sixteen entries sat in the second group while the legend called them the first β€” and this session read that legend literally and recommended one of them as an untouched mine. The evidence against that was already in the field report: Aizawa was sprott's expedition, the moire piece was interf's, SmokeFluid was smoke's, all logged as pattern failures and never carried back to the entry. So statuses gain "retired": walked and dropped, because it disappointed or because the device could not hold it. The sixteen move there, drawn faintest of anything on the map β€” a record, not an invitation β€” and "uncharted" once again means only what it says. Separately, and the reason this came up: at 57 points you cannot see what you just added. Entries carry an `added` batch tag and the chips gain "New Β· N", derived from the newest tag present β€” tag an import with its date and it becomes the new arrivals while the previous batch ages out by itself, nothing to switch off. Verified in the browser: New shows exactly the eight from today, Abandoned sixteen, Uncharted the same eight, and the panel badges a new entry. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 --- web/src/components/community/AtlasClient.tsx | 48 +++++++++-- web/src/lib/atlas/data.ts | 83 +++++++++++++------- 2 files changed, 96 insertions(+), 35 deletions(-) diff --git a/web/src/components/community/AtlasClient.tsx b/web/src/components/community/AtlasClient.tsx index b9b769b..73c1a39 100644 --- a/web/src/components/community/AtlasClient.tsx +++ b/web/src/components/community/AtlasClient.tsx @@ -25,6 +25,7 @@ import { FAMILIES, STATUSES, buildPrompt, + isNewEntry, type AtlasEntry, type AtlasStatusId, } from "@/lib/atlas/data"; @@ -56,7 +57,9 @@ const MAX_ZOOM = 8; const LINK_RADIUS = 80; type Lang = "en" | "ko"; -type Filter = AtlasStatusId | "all"; +/** Status chips, plus "all" and "new" β€” the latest batch, which cuts across + * statuses and is the only way to find an import in a map this size. */ +type Filter = AtlasStatusId | "all" | "new"; type Selection = { kind: "entry"; id: string } | { kind: "pin"; id: string } | null; export type AtlasPinData = { @@ -86,6 +89,8 @@ const UI = { copyLayout: "Copy layout", copiedLayout: "Copied (JSON)", allChip: "All", + newChip: (n: number) => `New Β· ${n}`, + newBadge: "new", pinChip: "Pinned", axisOrder: "ORDER β†’", axisChaos: "β†’ CHAOS", @@ -132,6 +137,8 @@ const UI = { copyLayout: "배치 볡사", copiedLayout: "볡사됨 (JSON)", allChip: "전체", + newChip: (n: number) => `μƒˆλ‘œ μΆ”κ°€ Β· ${n}`, + newBadge: "μƒˆλ‘œ μΆ”κ°€", pinChip: "ν•€", axisOrder: "μ§ˆμ„œ ORDER β†’", axisChaos: "β†’ 혼돈 CHAOS", @@ -481,13 +488,18 @@ export default function AtlasClient({ (pattern) => query === "" || pattern.title.toLowerCase().includes(query), ); + const newCount = ENTRIES.filter(isNewEntry).length; const chipList: Array<[Filter, string]> = [ ["all", t.allChip], + ...(newCount > 0 ? ([["new", t.newChip(newCount)]] as Array<[Filter, string]>) : []), ...(Object.keys(STATUSES) as AtlasStatusId[]).map( (s) => [s, statusLabel(s)] as [Filter, string], ), ]; + const passesFilter = (e: AtlasEntry) => + filter === "all" || (filter === "new" ? isNewEntry(e) : e.st === filter); + const worldTransform = `translate(${view.tx} ${view.ty}) scale(${view.k})`; const inv = 1 / view.k; @@ -686,12 +698,23 @@ export default function AtlasClient({ })} - {ENTRIES.filter((e) => filter === "all" || e.st === filter).map((e) => { + {ENTRIES.filter(passesFilter).map((e) => { const [px, py] = entryPos(e); const color = FAMILIES[e.f].color; - const dim = e.st === "unexplored" ? 0.45 : e.st === "hold" ? 0.6 : e.st === "invented" ? 0.8 : 1; - const dash = e.st === "unexplored" ? "2.5 2.5" : e.st === "invented" ? "1 3" : undefined; - const glow = e.st === "verified" ? 7 : e.st === "invented" ? 5 : 3; + // Abandoned ground is drawn faintest of all β€” it stays on the + // chart as a record, not as an invitation. + const dim = + e.st === "retired" ? 0.22 + : e.st === "unexplored" ? 0.55 + : e.st === "hold" ? 0.6 + : e.st === "invented" ? 0.8 + : 1; + const dash = + e.st === "retired" ? "1 4" + : e.st === "unexplored" ? "2.5 2.5" + : e.st === "invented" ? "1 3" + : undefined; + const glow = e.st === "verified" ? 7 : e.st === "invented" ? 5 : e.st === "retired" ? 0 : 3; const isSelected = selection?.kind === "entry" && selection.id === e.id; return ( 0 ? { filter: `drop-shadow(0 0 ${glow}px ${color})` } : undefined} /> - {entryName(e)} + + {entryName(e)} + ); })} @@ -909,6 +938,9 @@ export default function AtlasClient({ {statusLabel(selectedEntry.st)} + {isNewEntry(selectedEntry) && ( + {t.newBadge} + )} x {entryPos(selectedEntry)[0]} Β· y {entryPos(selectedEntry)[1]} diff --git a/web/src/lib/atlas/data.ts b/web/src/lib/atlas/data.ts index 63005cf..edd3a1b 100644 --- a/web/src/lib/atlas/data.ts +++ b/web/src/lib/atlas/data.ts @@ -22,7 +22,7 @@ export type AtlasFamilyId = | "feedback" | "pde" | "life" | "critical" | "invent"; export type AtlasStatusId = - | "verified" | "active" | "hold" | "unexplored" | "invented"; + | "verified" | "active" | "hold" | "unexplored" | "retired" | "invented"; export type AtlasEntry = { id: string; @@ -50,6 +50,13 @@ export type AtlasEntry = { riskEn?: string; impl?: string; implEn?: string; + /** + * The batch this entry joined the map with (YYYY-MM-DD). The original chart + * carries none. The newest tag present is what the map calls "new" β€” past + * fifty points, "which ones did I just add?" stops being answerable by + * looking, and a research import will only make that worse. + */ + added?: string; /** Prompt subject line (English). */ topic?: string; /** Prompt implementation hints (English). */ @@ -88,8 +95,13 @@ export const STATUSES: Record = new Map( ENTRIES.map((entry) => [entry.id, entry]), ); + +/** + * The latest batch tag on the map β€” what the "new" filter shows. + * + * Derived rather than declared: tag an import with today's date and it becomes + * the new arrivals while the previous batch ages out on its own. Nothing to + * remember to switch off. + */ +export const NEWEST_BATCH: string | null = ENTRIES.reduce( + (newest, entry) => + entry.added && (newest === null || entry.added > newest) ? entry.added : newest, + null, +); + +export function isNewEntry(entry: AtlasEntry): boolean { + return NEWEST_BATCH !== null && entry.added === NEWEST_BATCH; +} From f7f63988021b46150cd92137ce689db25aeed57f Mon Sep 17 00:00:00 2001 From: engmung <122682380+engmung@users.noreply.github.com> Date: Sun, 16 Aug 2026 23:22:46 +0900 Subject: [PATCH 4/6] docs(web): abandoned ground was usually dull, not expensive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The author's account of why sixteen entries were dropped: some were too heavy, but far more were simply not interesting β€” nothing good came out of them. The legend said those two causes in the wrong order, so it now leads with the one that actually did the killing. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 --- web/src/lib/atlas/data.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/lib/atlas/data.ts b/web/src/lib/atlas/data.ts index edd3a1b..254ef88 100644 --- a/web/src/lib/atlas/data.ts +++ b/web/src/lib/atlas/data.ts @@ -100,8 +100,8 @@ export const STATUSES: Record Date: Sun, 16 Aug 2026 23:29:37 +0900 Subject: [PATCH 5/6] feat(web): two techniques worth keeping from the embedded-graphics survey MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A research pass on cheap microcontroller graphics came back mostly inapplicable β€” its memory budget assumes 128 KB of SRAM where this device has ~15 KB of runtime heap, and half its effect catalogue is the uniform periodic texture the field data already killed. Two things in it are real, and both fix an entry that was stuck. Lenia was blocked on its wide radial kernel. A sliding-window box sum computes any radius in O(1) per pixel β€” running sum along the scanline, add the entering column, drop the leaving one β€” so the ring kernel becomes the difference of two blurs and the radius stops costing anything. Multiscale Turing rides on the same sums and is the cheaper thing to try first. Ouroboros was re-specced this morning to integer-shift feedback, which is correct and, on its own, looks mechanical: whole-pixel steps pin the image to the grid. A 3x3 blur after each shift redistributes brightness across neighbours and reads as viscosity β€” that blur is what buys back the continuity bilinear sampling was there to provide. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 --- web/src/lib/atlas/data.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web/src/lib/atlas/data.ts b/web/src/lib/atlas/data.ts index 254ef88..f093712 100644 --- a/web/src/lib/atlas/data.ts +++ b/web/src/lib/atlas/data.ts @@ -269,7 +269,9 @@ export const ENTRIES: AtlasEntry[] = [ knobEn: "the zoomβ‰ˆ1 boundary β€” the knife edge between convergence and blow-up", risk: "단독 ν‰κ°€λŠ” μ• λ§€ν–ˆμŒ. 그리고 8/14 μ›Œν”„-λ¦¬μƒ˜ν”Œ μ‹œλ„ 2건 μ „νŒ¨ β€” ν”½μ…€λ‹Ή λ°”μ΄λ¦¬λ‹ˆμ–΄ ν”Όλ“œλ°±μ€ μ‹€κΈ° μ λŒ€μ . μ •μˆ˜ μ‹œν”„νŠΈ(ν–‰ 볡사 슀크둀/νšŒμ „)λ‚˜ 타일 λ‹¨μœ„ μž¬κ·€λ‘œλ§Œ.", riskEn: "Ambiguous on its own β€” and both 8/14 warp-resample attempts failed hardware. Per-pixel bilinear feedback is device-hostile; feedback must move by whole-pixel integer shifts (row-copy scroll/rotate) or at tile level.", - topic: "Recursive frame feedback via integer-pixel shifts β€” the frame re-composited onto itself through whole-pixel scroll/rotate steps and decay, never per-pixel resampling" }, + topic: "Recursive frame feedback via integer-pixel shifts β€” the frame re-composited onto itself through whole-pixel scroll/rotate steps and decay, never per-pixel resampling", + hints: ["integer shifts alone pin the image to the grid and look mechanical β€” follow every shift with a cheap 3x3 blur, which redistributes brightness across neighbours and reads as viscosity. That blur is what buys back the continuity bilinear sampling was there to provide", + "decay by bit-shift equivalent (v -= v/32) rather than a float multiply"] }, { id: "hybrid", nm: "ν”Όλ“œλ°± ꡐ배", nmEn: "Feedback crossbreed", en: "CROSSBREED", f: "feedback", st: "retired", x: 82, y: 56, tex: "κ²€μ¦λœ μ†ŒμŠ€(μ»€μŠ€ν‹±Β·μž‰ν¬)λ₯Ό ν”Όλ“œλ°± 기계에 μ”¨μ•—μœΌλ‘œ β€” μ†ŒμŠ€μ˜ 질감이 μž¬κ·€λ‘œ μ¦ν­λœλ‹€.", @@ -469,7 +471,8 @@ export const ENTRIES: AtlasEntry[] = [ risk: "'생λͺ…체'κ°€ 개체둜 λ³΄μ΄λŠ” μˆœκ°„ 사망 β€” κ΅°μ§‘/μž₯ μŠ€μΌ€μΌ 레짐으둜만.", riskEn: "The moment a creature is countable it dies β€” colony/field-scale regimes only.", topic: "Lenia, the continuous cellular automaton β€” tuned to colony/field-scale regimes (tissues and blooms, never a single creature)", - hints: ["separable or small kernels to fit the ESP32 convolution budget", + hints: ["the wide radial kernel is the whole cost problem, and a sliding-window box sum solves it: a running sum along each scanline (S += in - out) computes any radius in O(1) per pixel, independent of radius, and three cascaded box passes approximate a Gaussian. Approximate the ring kernel as the difference of two such blurs", + "cheaper cousin worth trying first: multiscale Turing (McCabe) β€” 2-4 scales of activator/inhibitor built from those same box sums, each site stepping toward whichever scale has the least local variation", "if it reads as one countable creature the pattern is dead β€” stay at colony/field scale"] }, { id: "nca", nm: "Neural CA", nmEn: "Neural CA", en: "LEARNED RULES", f: "life", st: "retired", x: 52, y: 76, From 417b2064460a6940dcd011998a0ebc10e2d18fa5 Mon Sep 17 00:00:00 2001 From: engmung <122682380+engmung@users.noreply.github.com> Date: Sun, 16 Aug 2026 23:41:31 +0900 Subject: [PATCH 6/6] feat(web): seven systems survive the structure-hunt surveys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three commissioned surveys came back β€” eighteen 2D structure-generating systems, fourteen 1D spacetime systems, and a second pass at rescuing the cost casualties. Thirty-five-plus candidates went through the three filters (does it birth pointable structures, is the knob a real transition, does it fit the device), and seven hold up: pde: active nematic defect turbulence (self-propelled +1/2 comets, with a local u ∝ -Ξ±βˆ‡Β·Q approximation so no Stokes solve), 2D Kuramoto- Sivashinsky boiling terrain, Lugiato-Lefever cavity solitons, and the damped driven sine-Gordon fluxon gas (table sin; the undriven integrable case is verdict-dull and the entry says so). waterfall: Rule 54's glider gas (the cheapest simulation on the map β€” the entry's real cargo is the XOR-the-background render trick), 1D Gray-Scott pulse-splitting genealogy, and zero-range-process condensation rivers (a rigorous transition at b = 2). The rejects are accounted for by name: retired-ground cousins (SH snaking, NS vorticity, MIPS), duplicates of this morning's batch, uniform-texture and countable-object failures (2D spot mitosis, quantum-walk moirΓ©, Vicsek bands), and stiffness casualties (phase-field crystal, MHD plasmoids). Carried into existing ground: Barkley meander as fhn's retry vehicle, an external dullness verdict on fpu's displacement render, a cyclic-CA fallback on rps, and a coarse-control-grid clause in the prompt budget. Batch-tagged 2026-08-16b β€” the New chip now shows exactly these seven. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- web/src/lib/atlas/data.ts | 92 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 7 deletions(-) diff --git a/web/src/lib/atlas/data.ts b/web/src/lib/atlas/data.ts index f093712..6e6d08f 100644 --- a/web/src/lib/atlas/data.ts +++ b/web/src/lib/atlas/data.ts @@ -51,10 +51,12 @@ export type AtlasEntry = { impl?: string; implEn?: string; /** - * The batch this entry joined the map with (YYYY-MM-DD). The original chart - * carries none. The newest tag present is what the map calls "new" β€” past - * fifty points, "which ones did I just add?" stops being answerable by - * looking, and a research import will only make that worse. + * The batch this entry joined the map with (YYYY-MM-DD; a second batch the + * same day appends a letter, e.g. 2026-08-16b β€” the tags compare + * lexicographically). The original chart carries none. The newest tag + * present is what the map calls "new" β€” past fifty points, "which ones did + * I just add?" stops being answerable by looking, and a research import + * only makes that worse. */ added?: string; /** Prompt subject line (English). */ @@ -299,7 +301,8 @@ export const ENTRIES: AtlasEntry[] = [ risk: "μ „μ„ λ§Œ 보이면 저밀도 β€” μœ λ³΄μ˜€μŒ. breakup 레짐 μœ„μ£Όλ‘œ μž¬λ„μ „.", riskEn: "Lone fronts read as low density β€” held. Retry in the breakup regime.", topic: "A FitzHugh-Nagumo excitable medium in its spiral-wave breakup regime", - hints: ["only the breakup regime has enough density; lone traveling fronts read as low-density props"] }, + hints: ["only the breakup regime has enough density; lone traveling fronts read as low-density props", + "use Barkley kinetics for the retry β€” u(1-u)(u-(v+b)/a) is piecewise-cheap with zero transcendentals, and near a β‰ˆ 0.75, b β‰ˆ 0.06 the spiral tips MEANDER, tracing epicyclic flowers instead of closed circles (the extra drama the first attempt lacked)"] }, { id: "sh", nm: "Swift-Hohenberg", nmEn: "Swift-Hohenberg", en: "FREEZING", f: "pde", st: "hold", x: 27, y: 72, tex: "μ€„λ¬΄λŠ¬/윑각 νŒ¨ν„΄μ΄ κ΅³μ–΄κ°€λŠ” μž₯ β€” 'κ΅³λŠ” 것'.", @@ -349,7 +352,8 @@ export const ENTRIES: AtlasEntry[] = [ topic: "May-Leonard rock-paper-scissors on a lattice β€” cyclic invasion fronts winding into rotating spiral arms, territory forever overturning", hints: ["states empty/A/B/C in a Uint8Array; random sequential pair events: selection (A kills B leaving empty), reproduction into empty, and neighbor exchange (mobility) β€” zero transcendentals", "thousands of single-site events per frame; render species as three fixed tones with an invasion-age EMA so fresh fronts glow", - "the knob is the exchange-to-reaction ratio: past a critical mobility spirals outgrow the box and one species wins β€” reseed on extinction"] }, + "the knob is the exchange-to-reaction ratio: past a critical mobility spirals outgrow the box and one species wins β€” reseed on extinction", + "all-integer fallback with the same spiral engine: the cyclic cellular automaton (Griffeath) β€” N states, advance when β‰₯ kappa neighbors hold your successor; kappa 1..3 crosses turbulent ↔ crystalline spirals"] }, { id: "xy", nm: "XY μ†Œμš©λŒμ΄ 해리", nmEn: "XY vortex unbinding", en: "KOSTERLITZ-THOULESS", f: "pde", st: "unexplored", x: 57, y: 89, added: "2026-08-16", tex: "μœ„μƒ μŠ€ν•€ μž₯의 비단결 β€” μ†Œμš©λŒμ΄ 쌍이 λ¬Άμ˜€λ‹€ ν’€λ Έλ‹€ ν•˜λ©°, 결이 흐λ₯΄λŠ” 천과 κ·Έ 결함이 ν•œ 화면에.", @@ -361,6 +365,46 @@ export const ENTRIES: AtlasEntry[] = [ "checkerboard Metropolis; proposal = current angle Β± a small random step", "render local alignment (mean table-cosine of the four neighbor differences) so silk reads bright and vortex cores dark; warm up below T_KT in setup"] }, + { id: "nematic", nm: "ν™œμ„± λ„€λ§ˆν‹± 결함 λ‚œλ₯˜", nmEn: "Active nematic defect turbulence", en: "COMET DEFECTS", f: "pde", st: "unexplored", x: 78, y: 76, added: "2026-08-16b", + tex: "슀슀둜 μ—λ„ˆμ§€λ₯Ό νƒœμš°λŠ” μ•‘μ • β€” +1/2 ν˜œμ„± 결함이 제 발둜 달리며 λ§€μ§ˆμ„ νœ˜μ “κ³ , -1/2 μ„ΈμžŽ 결함과 λ§Œλ‚˜ μ†Œλ©Έν•œλ‹€. 결함이 엔진인 λ‚œλ₯˜. (연ꡬ μˆ˜μž…)", + texEn: "A liquid crystal burning its own fuel β€” +1/2 comet defects self-propel, stirring the medium, annihilating on -1/2 trefoils. Turbulence whose engines are the defects themselves. (research import)", + knob: "ν™œμ„± 응λ ₯ Ξ± β€” μ •μ§€ λ„€λ§ˆν‹± ↔ 결함 해리 λ‚œλ₯˜ (μ‹€μž¬ν•˜λŠ” ν™œμ„± 전이).", + knobEn: "active stress alpha β€” quiescent nematic ↔ defect-unbinding turbulence (a genuine activity transition)", + topic: "Active nematic defect turbulence β€” a Q-tensor orientation field driven by active stress, +1/2 comet defects self-propelling and annihilating on -1/2 trefoils; render orientational order so comets read as darting bright wakes", + hints: ["evolve the two Q-tensor components with a 5-point stencil relaxation plus active terms; skip the full Stokes solve β€” the local approximation u ∝ -alpha * div(Q) keeps the comets motile with no Poisson iteration (loses exact incompressibility, keeps the phenomenon)", + "render the local nematic order S (or |Q|) with an EMA: defect cores read dark, comet wakes bright", + "15-35 defects on screen in the good regime; annihilation balances pair creation, so it runs forever"] }, + + { id: "ks2d", nm: "2D ν™”μ—Όλ©΄ λ“λŠ” μ§€ν˜•", nmEn: "2D flame-front boiling terrain", en: "KURAMOTO-SIVASHINSKY 2D", f: "pde", st: "unexplored", x: 76, y: 84, added: "2026-08-16b", + tex: "포물선 첨점 세포듀이 νƒœμ–΄λ‚˜κ³  κ°ˆλΌμ§€κ³  ν•©μ³μ§€λŠ” λ“λŠ” μ§€ν˜• β€” lineage 폭포의 2차원 λ³Έν† . λŠ₯μ„ κ³Ό 첨점이 끝없이 μž¬λ°°μ—΄λœλ‹€. (연ꡬ μˆ˜μž…)", + texEn: "A boiling terrain of parabolic cusp cells β€” the 2D homeland of the lineage waterfall. Ridges and cusps born, splitting, merging, forever rearranging. (research import)", + knob: "μ˜μ—­ 크기 / 첨점 파μž₯ λΉ„ β€” 동결 세포 격자 ↔ μ‹œκ³΅κ°„ 혼돈.", + knobEn: "domain-to-wavelength ratio β€” frozen cell lattice ↔ full spatiotemporal chaos", + topic: "The 2D Kuramoto-Sivashinsky equation β€” h_t = -lap(h) - biharm(h) + |grad h|^2/2 β€” a boiling terrain of cusp cells splitting and merging; render curvature or mean-subtracted height", + hints: ["explicit stepping is stable at dt β‰ˆ 0.02 with unit grid spacing β€” 6-10 substeps per frame is enough motion; the 13-point biharmonic is two nested 5-point Laplacians, zero transcendentals", + "raw h drifts unboundedly: subtract the mean every step, and render curvature (the Laplacian you already computed) or mean-subtracted height through an EMA-normalized smoothstep", + "the instability wavelength is ~9 px, so 64x128 holds ~7x14 cells β€” 25-50 cusps live at once"] }, + + { id: "lle", nm: "곡진기 μ†Œμ‚° 솔리톀", nmEn: "Cavity dissipative solitons", en: "LUGIATO-LEFEVER", f: "pde", st: "unexplored", x: 44, y: 78, added: "2026-08-16b", + tex: "μ–΄λ‘μš΄ 곡진기 λ°”λ‹€ μœ„μ˜ λΉ› ν•€λ“€ β€” μ§„λ™ν•˜λŠ” 꼬리둜 μ„œλ‘œλ₯Ό λΆ™μž‘μ•„ λΆ„μžλ₯Ό 이루고, λ– λ‹€λ‹ˆκ³ , μˆ¨μ‰¬κ³ , 흩어진닀. (연ꡬ μˆ˜μž…)", + texEn: "Pinpoints of light on a dark cavity sea β€” locking one another through oscillating tails into molecules that drift, breathe, and scatter. (research import)", + knob: "곡진기 이쑰 Ξ± β‰ˆ 1.7..2.5 β€” 균질 λ°°κ²½ ↔ 솔리톀 쑴재 μ°½ ↔ λΆ•κ΄΄ (μ‹€μž¬ν•˜λŠ” 쑴재 경계).", + knobEn: "cavity detuning alpha β‰ˆ 1.7..2.5 β€” homogeneous background ↔ the soliton existence window ↔ collapse (a genuine existence boundary)", + topic: "Lugiato-Lefever cavity solitons β€” a driven damped nonlinear SchrΓΆdinger field, E_t = -(1+i*alpha)E + i|E|^2 E + i*lap(E) + F; sharp bright solitons on a dark background, binding into molecules through their oscillatory tails", + hints: ["split E into real/imaginary Float32 fields with 5-point Laplacians β€” the same machinery as CGL, canonical F β‰ˆ 1.3", + "render |E|^2 with a peak EMA; the dark homogeneous background is part of the picture β€” do not normalize it away", + "inject weak seeded noise continuously so fresh solitons keep nucleating as others drift off or merge"] }, + + { id: "sinegordon", nm: "ꡬ동 사인-κ³ λ“  ν”ŒλŸ­μ† 기체", nmEn: "Driven sine-Gordon fluxon gas", en: "BALLISTIC SOLITONS", f: "pde", st: "unexplored", x: 38, y: 68, added: "2026-08-16b", + tex: "μœ„μƒμž₯의 κ΅­μ†Œ 맀듭듀이 νƒ„λ„λ‘œ 달리며 λΆ€λ”ͺνžŒλ‹€ β€” 퍼지지 μ•Šκ³ , λ­‰κ°œμ§€μ§€ μ•Šκ³ , μ •ν™•νžˆ μΆ©λŒν•˜κ³  μ†Œλ©Έν•˜λŠ” μž…μž 같은 νŒŒλ™. (연ꡬ μˆ˜μž…)", + texEn: "Localized knots of a phase field running ballistically β€” waves that behave like particles: no dispersion, no smearing, exact collisions and annihilations. (research import)", + knob: "감쇠 Ξ± Γ— λ°”μ΄μ–΄μŠ€ Ξ³ β€” μ •μ§€ 격자 ↔ ν”ŒλŸ­μ† 증식 혼돈 (νƒˆν”Όλ‹ 전이). 무ꡬ동 적뢄가λŠ₯κ³„λŠ” 지루함이 νŒμ •λœ λ•… β€” ꡬ동·감쇠가 생λͺ…이닀.", + knobEn: "damping alpha Γ— bias gamma β€” pinned lattice ↔ chaotic fluxon proliferation (a depinning transition). The undriven integrable system is verdict-dull β€” drive and damping are what make it live", + topic: "The damped driven 2D sine-Gordon equation β€” phi_tt = lap(phi) - sin(phi) - alpha*phi_t + gamma β€” a gas of ballistic fluxon solitons colliding and annihilating; canonical alpha β‰ˆ 0.05, gamma β‰ˆ 0.4", + hints: ["leapfrog the wave equation with sin from a 256-entry table β€” two Float32 fields, ~15 flops per site, no library trig", + "render sin^2(phi/2) so kinks read as bright cores on dark ground; an energy-density render also works", + "gamma feeds energy that annihilations spend β€” modulate gamma slowly (harmless axis) so the population breathes"] }, + { id: "schrod", nm: "SchrΓΆdinger νŒŒμ†", nmEn: "SchrΓΆdinger packet", en: "QUANTUM CAUSTICS", f: "pde", st: "retired", x: 56, y: 82, tex: "λ³΅μ†Œ νŒŒλ™ν•¨μˆ˜ |ψ|²의 κ°„μ„­Β·ν„°λ„λ§Β·μ‚°λž€ β€” μ–‘μž μ»€μŠ€ν‹±.", texEn: "|ψ|Β² of a complex wavefunction β€” interference, tunneling, scattering. Quantum caustics.", @@ -463,6 +507,38 @@ export const ENTRIES: AtlasEntry[] = [ "render CHANGE: a wall glows where neighbors differ, with an EMA trail β€” the state values themselves stay invisible", "random sequential updates: a site copies a random neighbor; mutation assigns a fresh state"] }, + { id: "rule54", nm: "Rule 54 글라이더 기체", nmEn: "Rule 54 glider gas", en: "SOLITONIC LATTICE GAS", f: "waterfall", st: "unexplored", x: 57, y: 15, added: "2026-08-16b", + tex: "이진 격자의 κ²°μ •μ§ˆ λ°°κ²½ μœ„λ₯Ό λ‹¬λ¦¬λŠ” 글라이더 μž…μžλ“€ β€” μ‚°λž€ν•˜κ³ , μœ„μƒμ΄ 밀리고, μ†Œλ©Έν•˜λŠ” 이산 μ†”λ¦¬ν†€μ˜ λ•‹μž„. (연ꡬ μˆ˜μž…)", + texEn: "Glider particles running over a binary lattice's crystalline background β€” scattering, phase-shifting, annihilating: a braid of discrete solitons. (research import)", + knob: "경계 μ£Όμž…λ₯  β€” μ„±κΈ΄ 탄도 기체 ↔ μ‘°λ°€ν•œ μ‚°λž€ λ•‹μž„ ↔ 포화 (μž…μž μˆ˜μ§€κ°€ κΈ‰μ†Œ).", + knobEn: "boundary injection rate β€” sparse ballistic gas ↔ dense scattering braid ↔ saturation (the particle budget is the dial)", + risk: "μ›μ‹œ μƒνƒœλŠ” μ£ΌκΈ°-4 배경이 μ§€λ°°ν•΄ 무늬둜만 보인닀 β€” 배경을 XOR둜 벗겨내야 μ›”λ“œλΌμΈλ§Œ λ‚¨λŠ”λ‹€.", + riskEn: "The raw state is dominated by the period-4 background and reads as wallpaper β€” XOR the background away so only the worldlines remain.", + topic: "Elementary cellular automaton Rule 54 as a history waterfall β€” a deterministic lattice gas of glider particles over a period-4 background, scattering and annihilating; render the state XOR the unperturbed background, with an EMA glow", + hints: ["one 8-entry rule lookup per cell β€” the cheapest simulation on this map; run many rows per frame", + "the essential render trick: XOR each cell against the deterministic period-4 background pattern, then glow worldlines with an EMA β€” raw states hide everything", + "on 64 cells collisions eventually thin the gas: inject gliders at the boundaries (or rare seeded bit flips) to hold a steady population"] }, + + { id: "mitosis", nm: "λΆ„μ—΄ νŽ„μŠ€ 계보", nmEn: "Splitting-pulse genealogy", en: "PULSE MITOSIS FALLS", f: "waterfall", st: "unexplored", x: 60, y: 25, added: "2026-08-16b", + tex: "νŽ„μŠ€κ°€ 제 심μž₯을 ꡢ겨 λ‘˜λ‘œ κ°ˆλΌμ§„λ‹€ β€” Y자 뢄기듀이 폭포λ₯Ό μ±„μš°λŠ” 가계도. 가지끼리 λ§Œλ‚˜λ©΄ κ΅Άμ£Όλ¦° μͺ½μ΄ μ£½λŠ”λ‹€. (연ꡬ μˆ˜μž…)", + texEn: "A pulse starves its own core and splits in two β€” Y-forks filling the fall like a family tree. Where branches meet, the hungrier one dies. (research import)", + knob: "곡급λ₯  F β€” λΆ•κ΄΄ ↔ 자기볡제 λΆ„μ—΄ ↔ μ •μ§€ 튜링 λ°°μ—΄ (μ‹€μž¬ν•˜λŠ” λΆ„μ—΄ λ¬Έν„±).", + knobEn: "feed rate F β€” decay ↔ self-replicating splitting ↔ frozen Turing array (a genuine splitting threshold)", + topic: "1D Gray-Scott in its pulse-splitting regime, drawn as a history waterfall β€” activator pulses that starve centrally and split into diverging children, a branching genealogy; canonical F β‰ˆ 0.030, k β‰ˆ 0.062, Du β‰ˆ 1.0, Dv β‰ˆ 0.5", + hints: ["64-cell explicit stepping, ~50 substeps per frame at dt β‰ˆ 0.2 β€” two Float32 arrays and a 3-point Laplacian", + "render the activator's spatial gradient (or v^2), not raw concentration β€” the substrate background must stay invisible", + "the frozen-Turing-array endgame is the boredom risk: breathe F slowly across the splitting threshold so lattices keep collapsing and reseeding"] }, + + { id: "zrp", nm: "응좕 강쀄기", nmEn: "Condensation rivers", en: "ZERO-RANGE PROCESS", f: "waterfall", st: "unexplored", x: 26, y: 20, added: "2026-08-16b", + tex: "ν™•λ₯  λ„μ•½ν•˜λŠ” μ§ˆλŸ‰μ΄ 슀슀둜 μ‘μΆ•ν•œλ‹€ β€” κ°€λŠ” μ§€λ₯˜λ“€μ΄ λΉ›λ‚˜λŠ” λ³Έλ₯˜λ‘œ λͺ¨μ—¬λ“€κ³ , λ³Έλ₯˜λŠ” λΆ€μŠ€λŸ¬κΈ°λ₯Ό 흘리며 λ– λˆλ‹€. (연ꡬ μˆ˜μž…)", + texEn: "Stochastically hopping mass condensing on its own β€” faint tributaries feeding glowing trunks that drift and shed grains. (research import)", + knob: "도약 μ§€μˆ˜ b (u(n)=1+b/n) β€” b<2 균질 흐름 ↔ b>2 싀곡간 응좕 (μ—„λ°€ν•œ 응좕 상전이).", + knobEn: "hop exponent b in u(n)=1+b/n β€” b<2 homogeneous flow ↔ b>2 real-space condensation (a rigorous condensation transition)", + topic: "A zero-range process with chipping, drawn as a history waterfall β€” site masses hopping at rate u(n)=1+b/n, condensing into drifting trunk rivers fed by tributary showers; canonical b β‰ˆ 5, density β‰ˆ 3", + hints: ["integer masses in one array; per step pick random sites, move one unit to a neighbor with probability ∝ 1 + b/n β€” all integer arithmetic", + "occupancies span orders of magnitude: render log2(1 + n), or the trunks whiteout and the tributaries vanish", + "total coarsening into one eternal trunk is the boredom end β€” cap site mass and burst-split any site that hits the cap, so rivers keep being born"] }, + { id: "lenia", nm: "Lenia", nmEn: "Lenia", en: "CONTINUOUS LIFE", f: "life", st: "retired", x: 60, y: 72, tex: "연속 컀널 CA β€” λΆ€λ“œλŸ¬μš΄ 생λͺ… ν˜•νƒœκ°€ ν—€μ—„μΉ˜λŠ” μž₯.", texEn: "The continuous-kernel cellular automaton β€” soft life forms swimming as a field.", @@ -675,6 +751,8 @@ export const ENTRIES: AtlasEntry[] = [ { id: "fpu", nm: "FPU β€” 유령 νšŒκ·€", nmEn: "FPU β€” ghost recurrence", en: "FERMI-PASTA-ULAM", f: "invent", st: "invented", x: 33, y: 28, tex: "λΉ„μ„ ν˜• μ‚¬μŠ¬μ— 뢀은 μ—λ„ˆμ§€κ°€ λͺ¨λ“œλ“€λ‘œ ν©μ–΄μ‘Œλ‹€κ°€, 였랜 λ°©ν™© 끝에 유령처럼 처음 ν˜•νƒœλ‘œ λ˜λŒμ•„μ˜¨λ‹€ β€” 흩어짐과 νšŒκ·€μ˜ κΈ΄ 호흑이 폭포둜 흐λ₯Έλ‹€.", texEn: "Energy poured into a nonlinear chain scatters across its modes β€” then, after a long wander, returns like a ghost to its original shape. The long breath of dispersal and recurrence, flowing as a waterfall.", + risk: "μ™ΈλΆ€ μ„œλ² μ΄ νŒμ •(8/16): λ³€μœ„ ν­ν¬λŠ” λ§€λˆν•œ μ „μ—­ μ •μƒνŒŒ β€” κ΅­μ†Œ ꡬ쑰가 μ—†μ–΄ 지루함. λͺ¨λ“œ μ—λ„ˆμ§€ λ°΄λ“œ λ Œλ”λ‘œλ§Œ μŠΉμ‚°μ΄ μžˆλ‹€.", + riskEn: "External survey verdict (8/16): the displacement waterfall is smooth global standing waves β€” no localized structures, monotonous. Only the mode-energy-band render has a chance.", knob: "λΉ„μ„ ν˜• 강도 Ξ² β€” μ™„μ „ νšŒκ·€ ↔ μ€€νšŒκ·€ ↔ μ—΄ν™”(에λ₯΄κ³ λ”•)의 λ¬Έν„±.", knobEn: "nonlinearity beta β€” clean recurrence ↔ partial recurrence ↔ the thermalization threshold", topic: "Fermi-Pasta-Ulam-Tsingou recurrence as a vertical history waterfall β€” a nonlinear oscillator chain seeded in its lowest mode, energy spreading through the spectrum and ghost-returning; render the displacement field, or the per-mode energies as vertical bands", @@ -770,7 +848,7 @@ Rendering craft β€” most attempts die here, read carefully: - Simulate in the display's own orientation. Internal buffers are 64 wide Γ— 128 tall (index = y * 64 + x), matching // @matrix 64x128. Do NOT build a 128Γ—64 landscape simulation and rotate or remap it inside draw() β€” no axis swaps, no dispX = y tricks. If you catch yourself writing const w = 128, h = 64, stop: swap them. (Most LED-matrix code online is landscape; this device stands tall.) - Choose @knobs ranges so the pattern is at its best near the MIDDLE of every range, with nobody touching anything. Knob extremes may be calm or violent; the default position is the show. - Simulations must sit in their interesting regime at those defaults β€” use the canonical parameter values from the hints below when given; do not invent your own. -- This code is also compiled for a 240 MHz microcontroller, where sin/cos/exp/pow/atan2 each cost hundreds of cycles. Budget transcendentals: keep them OUT of the per-pixel loop β€” per-agent, per-row, or per-timestep math is fine, and integer/add/multiply lattice rules are free. At most one full-resolution pass per frame, carrying no more than a couple of trig calls per pixel. Never resample the previous frame per pixel (no warp/zoom/bilinear feedback), never do O(nΒ²) all-pairs interactions, and use Float32Array only β€” never Float64Array (doubles are software-emulated). +- This code is also compiled for a 240 MHz microcontroller, where sin/cos/exp/pow/atan2 each cost hundreds of cycles. Budget transcendentals: keep them OUT of the per-pixel loop β€” per-agent, per-row, per-timestep math is fine, expensive fields can be computed on a coarse control grid (a few hundred nodes) and interpolated up, and integer/add/multiply lattice rules are free. At most one full-resolution pass per frame, carrying no more than a couple of trig calls per pixel. Never resample the previous frame per pixel (no warp/zoom/bilinear feedback), never do O(nΒ²) all-pairs interactions, and use Float32Array only β€” never Float64Array (doubles are software-emulated). Taste direction (settled by experiment on this device β€” treat as hard constraints, on top of everything above): - No countable objects. Thousands of accumulated operations must read as one continuous "material". The moment dots/creatures/cars can be counted, the pattern is dead.