From 11365a8a9d526a31602761f0463b60c25626788d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 05:40:57 +0000 Subject: [PATCH 01/10] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=2056=ED=8C=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UUXdHR3tRiDEQfEfGbQxb9 From e39002eba501d8a2f2a8e7f9b9c4b06d4f7697d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 06:19:01 +0000 Subject: [PATCH 02/10] =?UTF-8?q?56=ED=8C=90:=20Dead=20Leaves,=20Bloch=20O?= =?UTF-8?q?scillations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UUXdHR3tRiDEQfEfGbQxb9 --- .../patterns/bloch-oscillations.js | 195 ++++++++++++++++++ .../pattern-study/patterns/dead-leaves.js | 171 +++++++++++++++ 2 files changed, 366 insertions(+) create mode 100644 incoming/pattern-study/patterns/bloch-oscillations.js create mode 100644 incoming/pattern-study/patterns/dead-leaves.js diff --git a/incoming/pattern-study/patterns/bloch-oscillations.js b/incoming/pattern-study/patterns/bloch-oscillations.js new file mode 100644 index 000000000..2b7585be2 --- /dev/null +++ b/incoming/pattern-study/patterns/bloch-oscillations.js @@ -0,0 +1,195 @@ +// ===== Patternflow pattern ===== +// Title: Bloch Oscillations +// Family: wave +// Idea: A quantum particle on a chain of sites laid across the panel feels a steady force along the chain, and with time running down the scrolling panel it never slides away: started on one site it breathes out into a lens of Bessel fringes 50 sites wide and back to a point once every Bloch period, a broad packet swings from side to side instead, and the hue gives the quasi-momentum, which the force winds steadily through the band. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Bloch oscillations (Felix Bloch, 1929; Clarence Zener, 1934) and the Wannier-Stark ladder (Gregory Wannier, 1960), pattern round 56 +// Round: 56 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Force=0.16..0.64, Width=0..1 +// +// Knob 1: Hue. Turns the wheel the quasi-momentum k is read on, k being the phase step from each site to the next. At 0 the bottom of the band (k = 0) is azure, a packet moving right at full speed (k = pi/2) violet, one moving left (k = -pi/2) green, and the top of the band (k = pi), where a broad packet is Bragg-reflected and turns back, red; orange takes a narrower slice of k than the other colours. The lens from one site opens violet on its right edge and lime on its left, and as the force winds k down, the right edge turns through blue to azure and the left edge through red to pink. +// Knob 2: Speed. How fast the carpet climbs: one row is 0.75 in units of hbar / J. At 2 sixteen rows a second, so one Bloch period at the opening Force passes in 3.3 s; at 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Force. F in units of J per site, 0.16 to 0.64. The Bloch period 2 pi / F is 52 rows at 0.16, 26 at 0.32 and 13 at 0.64, and the lens from one site opens to 4J/F either side, 25, 12.5 and 6.3 sites, which is 57, 31 and 17 lit LEDs at its widest. A broad packet swings 24.9, 12.4 and 6.2 sites from side to side, against 4J/F. Turning it mid-run changes the field from the next row on; if more than half the particle has swung off the panel it starts again in the middle. +// Knob 4: Width. The starting packet. Up to 0.1 a single site, and the rows are J_n(4J/F sin(Ft/2))^2 to within 3e-8 of the closed form; above, a Gaussian of width 6 u^2 sites (u from 0 at 0.1 to 1 at 1) moving right at k = pi/2. At 0.3 (0.3 sites) the lens keeps its fringes; at 0.4 (0.7 sites) they are gone and a bright violet edge, the part moving right, runs down the lens from upper left to lower right; at 0.5 (1.2 sites) the lens has become one band swinging in an S, 25 sites peak to peak at the opening Force, and at 1 (6 sites) the band is 38 LEDs wide. Each turn starts the packet again. + +const W = 64, H = 128; +// Sites on the chain, and the one under the panel's middle. The panel +// shows sites C - 32 to C + 31; the chain runs 64 further each way, past +// any swing the knobs allow, so its ends are never reached. +const NS = 192, C = 96; +// Time per row in units of hbar / J, cut into SUB exact steps; the row +// shows the probability averaged over them. +const TROW = 0.75, SUB = 4, DT = TROW / SUB; +// Rows per second at Speed 1. +const RATE = 8; +// Bessel orders kept in one step: J_m(2 dt) for m > MB is below 1e-12. +const MB = 10; +// Brightness from the probability on a log scale: 10^LMIN is dark, 10^(LMIN + LSPAN) full. +const LMIN = -3.2, LSPAN = 2.4; +const { floor, min, max, abs, sin, cos, sqrt, exp, round, atan2, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, F: 0.16, wid: 0, begun: false, + re: new Float64Array(NS), im: new Float64Array(NS), nr: new Float64Array(NS), ni: new Float64Array(NS), + // The step's kernel: Re and Im of i^m J_m(a) e^{-i m F dt / 2}, m = -MB..MB. + kr: new Float64Array(2 * MB + 1), ki: new Float64Array(2 * MB + 1), kF: -1, + // The last 128 rows: probability and phase per site, newest at head - 1. + rho: new Float32Array(W * H), ph: new Float32Array(W * H), head: 0, prog: 0, launched: 0, + acc: new Float64Array(W), rgb: new Float32Array(W * H * 3), + }; +} + +// J_0..J_MB at a small argument, by the power series. +function besselSmall(a, out) { + for (let m = 0; m <= MB; m++) { + let term = (a / 2) ** m, s = 0; + for (let k = 1; k <= m; k++) term /= k; + for (let k = 0; k < 20; k++) { + s += term; + term *= -((a / 2) ** 2) / ((k + 1) * (k + 1 + m)); + } + out[m] = s; + } +} + +// The exact propagator of the tilted chain over one step dt, found by +// taking out the phase e^{-i F n t} and summing the hopping in momentum +// space with the Jacobi-Anger expansion. With +// H = -J sum (|n>= 0 ? b[m] : m & 1 ? -b[-m] : b[-m], p = m * (PI / 2 - (F * DT) / 2); + S.kr[m + MB] = Jm * cos(p); + S.ki[m + MB] = Jm * sin(p); + } + S.kF = F; +} + +function step(S) { + if (S.kF !== S.F) kernel(S); + const { re, im, nr, ni, kr, ki } = S, F = S.F; + for (let n = 0; n < NS; n++) { + let sr = 0, si = 0; + const lo = max(-MB, -n), hi = min(MB, NS - 1 - n); + for (let m = lo; m <= hi; m++) { + const a = kr[m + MB], b = ki[m + MB], x = re[n + m], y = im[n + m]; + sr += a * x - b * y; + si += a * y + b * x; + } + const p = -F * (n - C) * DT, c = cos(p), s = sin(p); + nr[n] = sr * c - si * s; + ni[n] = sr * s + si * c; + } + re.set(nr); + im.set(ni); +} + +// A packet on the middle site: a Gaussian of width sigma (one site at 0) +// with momentum pi / 2, so a broad one starts at full speed to the right +// and swings evenly about the middle. +function launch(S) { + const sg = S.wid, { re, im } = S; + let nrm = 0; + for (let n = 0; n < NS; n++) { + const x = n - C, g = sg > 0 ? exp(-(x * x) / (4 * sg * sg)) : x === 0 ? 1 : 0; + re[n] = g * cos((PI / 2) * x); + im[n] = g * sin((PI / 2) * x); + nrm += g * g; + } + nrm = 1 / sqrt(nrm); + for (let n = 0; n < NS; n++) (re[n] *= nrm), (im[n] *= nrm); + S.launched = S.wid; +} + +// One row: SUB steps, probability averaged over them, phase at the last. +function row(S) { + const { acc, re, im } = S; + acc.fill(0); + for (let k = 0; k < SUB; k++) { + step(S); + for (let x = 0; x < W; x++) { + const n = C - 32 + x; + acc[x] += (re[n] * re[n] + im[n] * im[n]) / SUB; + } + } + // Half the probability left the panel after a turn of Force mid-swing: + // start again from the middle. + let on = 0; + for (let x = 0; x < W; x++) on += acc[x]; + if (on < 0.5) launch(S); + const o = S.head * W; + for (let x = 0; x < W; x++) { + const n = C - 32 + x; + S.rho[o + x] = acc[x]; + // The local quasi-momentum, the phase step from this site to the next. + S.ph[o + x] = atan2(re[n] * im[n + 1] - im[n] * re[n + 1], re[n] * re[n + 1] + im[n] * im[n + 1]); + } + S.head = (S.head + 1) % H; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0.16, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = cl(k[0], 0, 1); + S.speed = cl(k[1], 0.2, 3); + S.F = cl(k[2], 0.16, 0.64); + // Width: one site up to 0.1, then sigma = 6 u^2 sites, so the turn from + // breathing to swinging (sigma 0.5 to 2) gets the first half of the knob. + const u = max(0, (cl(k[3], 0, 1) - 0.1) / 0.9); + S.wid = 6 * u * u; + if (!S.begun) { + launch(S); + for (let r = 0; r < H; r++) row(S); + S.begun = true; + } + if (abs(S.wid - S.launched) > 1e-6) launch(S); + S.prog += d * S.speed * RATE; + while (S.prog >= 1) { + S.prog -= 1; + row(S); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { rho, ph, rgb } = S, hb = 0.55 + S.hue; + for (let y = 0; y < H; y++) { + const r = (S.head + y) % H; + for (let x = 0; x < W; x++) { + const p = r * W + x, o = (y * W + x) * 3; + const L = cl((Math.log10(rho[p] + 1e-12) - LMIN) / LSPAN, 0, 1); + if (L <= 0) { + rgb[o] = rgb[o + 1] = rgb[o + 2] = 0; + continue; + } + // k round the wheel, with orange given a narrower slice of it. Dim + // orange and yellow read brown and olive, so there they are lifted + // and paled toward peach and butter. + const u = hb + ph[p] / (2 * PI), h = u + 0.07 * sin(2 * PI * (u - 0.11)); + const wm = cl(1 - abs(h - floor(h) - 0.12) / 0.1, 0, 1); + hsv(h, (0.85 - 0.45 * L * L) * (1 - 0.3 * wm), min(1, (0.06 + 0.94 * L) * (1 + 0.35 * wm)), rgb, o); + } + } + for (let y = 0, o = 0; y < H; y++) + for (let x = 0; x < W; x++, o += 3) display.setPixel(x, y, round(255 * rgb[o]), round(255 * rgb[o + 1]), round(255 * rgb[o + 2])); +} diff --git a/incoming/pattern-study/patterns/dead-leaves.js b/incoming/pattern-study/patterns/dead-leaves.js new file mode 100644 index 000000000..323724ae4 --- /dev/null +++ b/incoming/pattern-study/patterns/dead-leaves.js @@ -0,0 +1,171 @@ +// ===== Patternflow pattern ===== +// Title: Dead Leaves +// Family: noise +// Idea: Flat discs with radii from half an LED to 20 LEDs come down one after another, each slipping in under the ones already there so it shows only in the gaps, until the last hole closes, a white ring runs out from it and the panel holds an exact sample of the scale-invariant dead leaves picture, where hue gives each leaf's size and every octave of size covers the same share. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: dead leaves model (Georges Matheron, 1968), scale invariant with radii by r^-3 (Ann Lee, David Mumford and Jinggang Huang, 2001), sampled exactly from below (Wilfrid Kendall and Elke Thönnes, 1999), pattern round 56 +// Round: 56 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Grain=0.5..4, Law=-1..1 +// +// Knob 1: Hue. Turns the size colours round the wheel together. At 0 leaves of radius half an LED are pink-magenta and each doubling of the radius steps 0.072 back round the wheel, through violet and blue to cyan at 16 to 20 LEDs; at 0.25 the small leaves are orange-red and the big ones violet, at 0.5 green down to red, at 0.75 blue down to lime. A leaf whose hue falls between orange and yellow takes only the two bright levels, so it never reads brown or olive. +// Knob 2: Speed. At 2 the leaves come down 3.2 deep a second, about 4,900 leaves a second at the opening Grain: the panel is 95% covered after 0.9 s and the last hole closes after 2.8 s (median of 40 samples, 2.3 to 3.2 s for the middle 80%), and the white ring runs out at 60 LEDs a second while the finished sample holds 2.2 s before the next one starts over it. At 0.2 everything is ten times slower, at 3 half as fast again. +// Knob 3: Grain. The smallest radius, 0.5 to 4 LEDs. At 0.5 a sample takes about 13,500 leaves and the five octaves of radius from half an LED to 16 each cover 19 to 20% of the panel; at 4 only the octaves from 4 up are left, 500 leaves make a sample, and the picture is big overlapping discs in blue and cyan. +// Knob 4: Law. The exponent a of the radius density r^-a, 3 + K4 outside a dead zone of 0.1 either side of 0, so the opening 0.06 is exactly 3, the law under which the picture looks the same at every scale. At -1 (a = 2) the big leaves take over: radii 8 to 16 cover 42% of the panel and those under 1 LED 2.5%. At 1 (a = 4) the smallest octave covers 51% and the panel turns to magenta grain. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, sqrt, PI, ceil } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// The largest leaf's radius in LEDs. +const RMAX = 20; +// Leaf depth per second at Speed 1: how many leaves, on average, have come +// down over any one LED. The uncovered share is e^-depth. +const RATE = 1.6; +// Seconds the finished sample holds at Speed 1, and how fast the ring runs +// out, in LEDs a second at Speed 1. +const HOLD = 4.4, RING = 30; +// Every leaf is one of four flat levels, 45 apart, and the holes show the +// sample before at one dim level, so two leaves side by side either match +// in level or differ by 45 or more. +const TIERS = [255, 210, 165, 120], GHOST = 60; +// Hue tells a leaf's size: the smallest, half an LED across in radius, are +// magenta at the opening, and each doubling of the radius steps OCT back +// round the wheel, through violet, blue and cyan to green at 20 LEDs. Each +// leaf strays a little either side, by up to JIT. +const H0 = 0.88, OCT = 0.072, JIT = 0.03; +// Dim orange and yellow read brown and olive: a leaf whose hue falls there +// takes one of the two bright tiers instead. +const warm = (h) => ((h = h - floor(h)), h > 0.03 && h < 0.22); + +// hsv with an integer value v: the brightest channel is v exactly. +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = (v * (1 - s) + 0.5) | 0, q = (v * (1 - f * s) + 0.5) | 0, t = (v * (1 - (1 - f) * s) + 0.5) | 0; + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +function sample() { + return { + id: new Int32Array(N).fill(-1), left: N, n: 0, depth: 0, + cx: [], cy: [], r: [], hu: [], sa: [], lv: [], + }; +} + +export function setup(params) { + params.s = { + cur: sample(), old: null, hue: 0, speed: 2, rmin: 0.5, alpha: 3, + phase: 0, pt: 0, last: [32, 64], rgb: new Uint8Array(N * 3), + }; +} + +// One radius from the density r^-alpha on [rmin, RMAX], by inverting its +// distribution function. +function radius(a, alpha) { + const u = Math.random(); + if (abs(alpha - 1) < 1e-6) return a * (RMAX / a) ** u; + const e = 1 - alpha, lo = a ** e, hi = RMAX ** e; + return (lo + u * (hi - lo)) ** (1 / e); +} + +// The leaves are a Poisson process of discs. Only those that reach the +// panel matter: a leaf of radius r reaches it when its centre lies in the +// panel grown by r on every side, so radii are drawn from r^-alpha times +// that box's area (by rejection) and the centre is uniform in the box. A +// leaf then adds pi r^2 / box to the mean depth over every LED. +function leaf(S, D) { + const big = (W + 2 * RMAX) * (H + 2 * RMAX); + let r, box; + do { + r = radius(S.rmin, S.alpha); + box = (W + 2 * r) * (H + 2 * r); + } while (Math.random() * big > box); + const cx = -r + Math.random() * (W + 2 * r), cy = -r + Math.random() * (H + 2 * r); + D.depth += (PI * r * r) / box; + // Seen from below: the leaf shows only on LEDs no earlier leaf covers. + const k = D.n++; + D.cx.push(cx); + D.cy.push(cy); + D.r.push(r); + D.hu.push(-OCT * Math.log2(r / 0.5) + (Math.random() - 0.5) * 2 * JIT); + D.sa.push(0.55 + 0.35 * Math.random()); + D.lv.push(floor(Math.random() * TIERS.length)); + const x0 = max(0, ceil(cx - r - 0.5)), x1 = min(W - 1, floor(cx + r - 0.5)); + const y0 = max(0, ceil(cy - r - 0.5)), y1 = min(H - 1, floor(cy + r - 0.5)); + const r2 = r * r, id = D.id; + for (let y = y0; y <= y1; y++) { + const dy = y + 0.5 - cy; + for (let x = x0; x <= x1; x++) { + const p = y * W + x; + if (id[p] >= 0) continue; + const dx = x + 0.5 - cx; + if (dx * dx + dy * dy > r2) continue; + id[p] = k; + if (--D.left === 0) S.last = [x, y]; + } + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0.5, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = cl(kv[0], 0, 1); + S.speed = cl(kv[1], 0.2, 3); + S.rmin = cl(kv[2], 0.5, 4); + // Law: a dead zone of 0.1 round the middle keeps the exponent at 3. + const k4 = cl(kv[3], -1, 1), e = abs(k4) > 0.1 ? (Math.sign(k4) * (abs(k4) - 0.1)) / 0.9 : 0; + S.alpha = 3 + e; + const g = d * S.speed; + if (S.phase === 1) { + S.pt += g; + if (S.pt > HOLD) { + // The held sample sinks to one dim level and shows through the holes + // of the next one until they close. + S.old = S.cur; + S.cur = sample(); + S.phase = 0; + } + } + if (S.phase === 0) { + const D = S.cur, goal = D.depth + g * RATE; + while (D.left > 0 && D.depth < goal) leaf(S, D); + if (D.left === 0) (S.phase = 1), (S.pt = 0); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const rgb = S.rgb, D = S.cur, O = S.old, hb = H0 + S.hue; + const ring = S.phase === 1 ? S.pt * RING : -1, [lx, ly] = S.last; + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const o = p * 3; + let k = D.id[p], L = D; + if (k < 0 && O) (L = O), (k = O.id[p]); + if (k < 0) { + rgb[o] = rgb[o + 1] = rgb[o + 2] = 0; + continue; + } + const h = hb + L.hu[k], t = L.lv[k], v = L === D ? TIERS[warm(h) ? t & 1 : t] : GHOST; + // Each leaf pales toward its centre, so a disc reads as a disc even + // where only a sliver of it shows. Only the weaker channels change. + const dx = x + 0.5 - L.cx[k], dy = y + 0.5 - L.cy[k], f = min(1, sqrt(dx * dx + dy * dy) / L.r[k]); + hsv(h, L.sa[k] * (0.6 + 0.4 * f), v, rgb, o); + } + // The moment the last hole closes the sample is exact: a white ring runs + // out from where that hole was. + if (ring >= 0) { + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const dd = sqrt((x - lx) ** 2 + (y - ly) ** 2); + if (dd <= ring && dd > ring - 1.5) rgb[p * 3] = rgb[p * 3 + 1] = rgb[p * 3 + 2] = 255; + } + } + for (let y = 0, o = 0; y < H; y++) + for (let x = 0; x < W; x++, o += 3) display.setPixel(x, y, rgb[o], rgb[o + 1], rgb[o + 2]); +} From 9f1e7b5f182fb05be6f27f64b768e9c1659a4e7d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 06:20:56 +0000 Subject: [PATCH 03/10] =?UTF-8?q?56=ED=8C=90:=20Random=20Organization,=20E?= =?UTF-8?q?rd=C5=91s=20Discrepancy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UUXdHR3tRiDEQfEfGbQxb9 --- .../patterns/erdos-discrepancy.js | 168 ++++++++++++ .../patterns/random-organization.js | 247 ++++++++++++++++++ 2 files changed, 415 insertions(+) create mode 100644 incoming/pattern-study/patterns/erdos-discrepancy.js create mode 100644 incoming/pattern-study/patterns/random-organization.js diff --git a/incoming/pattern-study/patterns/erdos-discrepancy.js b/incoming/pattern-study/patterns/erdos-discrepancy.js new file mode 100644 index 000000000..916d1b38d --- /dev/null +++ b/incoming/pattern-study/patterns/erdos-discrepancy.js @@ -0,0 +1,168 @@ +// ===== Patternflow pattern ===== +// Title: Erdős Discrepancy +// Family: rhythm +// Idea: A rhythm of hits (+1) and rests (-1) is heard by 64 listeners side by side, listener d taking only every d-th beat, and each writes the running sum of what it hears down its own column, one row a step with the newest at the bottom over a row of the beat itself, so the completely multiplicative rhythm weaves a quiet tartan, one set of nested bands coloured column by column by the rhythm's own signs, while coin-flip beats tear it into loud streaks, and whenever a sum beats the record so far the cells that did it light white. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Erdős discrepancy problem (Paul Erdős, 1932), solved by Terence Tao (2015), with the character-like functions of Peter Borwein, Stephen Choi and Michael Coons (2010), pattern round 56 +// Round: 56 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Noise=0..1, Prime=3..13 +// +// Knob 1: Hue. Turns both colours round the wheel together. At 0 sums above zero are aqua and sums below zero magenta, with records and the beat row white; at 0.25 blue and red, at 0.5 pink and green, at 0.75 lime and azure. The wheel skips orange and yellow, which read brown and olive in the dim cells of small sums. +// Knob 2: Speed. Steps a second, 4 x Speed: 8 at 2, so the 127 rows of carpet turn over in 16 s, 0.8 at 0.2 and 12 at 3. A run goes on to step 6,561 = 3^8, 13.7 minutes at 2, and starts again from the first 128 steps with fresh coin flips. +// Knob 3: Noise. Each beat is a coin flip instead of the multiplicative rhythm with chance Noise squared: 6% at 0.25, 25% at 0.5, 56% at 0.75, every beat at 1. At 0 every column is plus or minus the same walk, so the carpet is one column of bands times the rhythm's first 64 signs. At step 1,000 the 64 sums average 4 at 0 (all equal in size), 10 at 0.25, 18 at 0.5 and 25 at 0.75 and 1, and the record by then is 6 at 0 against 92 at 1 (mean of eight runs). +// Knob 4: Prime. The prime p of the character-like rhythm, the one of 3, 5, 7, 11 and 13 nearest the knob: x_n is the residue symbol mod p of n with every factor p taken out. Its running sum is a sum over the base-p digits of the step, so it grows like the digit count: at p = 3 each digit 1 adds one, and the largest sum up to step m is floor(log3(2m + 1)), never above log3 m + 1 (checked to m = 600,000), with the records at m = 1, 4, 13, 40, 121, 364, 1093 and 3280 lighting whole rows. Records by step 6,561 reach 8 at p = 3, 6 at 5, 10 at 7, 11 at 11 and 7 at 13; at 5 and 13 the sums change sign, so the bands swap colours row by row. + +const W = 64, H = 128; +const { floor, min, max, abs } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// Steps a second at Speed 1, and the step at which the walk starts over. +const RATE = 4, LAST = 6561; +const PRIMES = [3, 5, 7, 11, 13]; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// Quadratic residues mod p, as +1 / -1 by residue. +function legendre(p) { + const t = new Int8Array(p).fill(-1); + t[0] = 0; + for (let a = 1; a < p; a++) t[(a * a) % p] = 1; + return t; +} + +// A 32-bit integer hash, for the coin flips: each beat n has its own draw, +// fixed for the run, so every listener hears the same beat the same way. +function hash(n) { + n = Math.imul(n ^ (n >>> 16), 0x7feb352d); + n = Math.imul(n ^ (n >>> 15), 0x846ca68b); + return (n ^ (n >>> 16)) >>> 0; +} + +export function setup(params) { + params.s = { + M: 0, acc: 0, seed: 0, p: 3, noise: 0, hue: 0, speed: 2, chi: null, + cur: new Int32Array(W + 1), ring: new Int16Array(H * W), mark: new Uint8Array(H * W), + rec: 0, pal: new Float32Array(2), palHue: -1, key: '', + }; +} + +// Beat n of the rhythm. Borwein, Choi and Coons' character-like function +// takes out every factor p and reads the rest as a residue mod p; with +// Noise the beat is a coin flip instead, with that chance. +function term(S, n) { + const h = hash(n + S.seed); + if ((h >>> 8) / 16777216 < S.noise) return h & 1 ? 1 : -1; + const p = S.p; + while (n % p === 0) n /= p; + return S.chi[n % p]; +} + +// One step m for all 64 listeners: listener d hears beat m d. +function step(S) { + const m = ++S.M, row = (m % H) * W, cur = S.cur; + let top = 0; + for (let d = 1; d <= W; d++) { + cur[d] += term(S, m * d); + S.ring[row + d - 1] = cur[d]; + S.mark[row + d - 1] = 0; + top = max(top, abs(cur[d])); + } + // A new record discrepancy: the cells that set it. + if (top > S.rec) { + S.rec = top; + for (let d = 1; d <= W; d++) if (abs(cur[d]) === top) S.mark[row + d - 1] = 1; + } +} + +// Start over from step 0 and run up to step `to`, as after a knob turn. +function rebuild(S, to) { + S.M = 0; + S.rec = 0; + S.cur.fill(0); + S.ring.fill(0); + S.mark.fill(0); + while (S.M < to) step(S); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 3], d = cl(dt || 0, 0, 0.1); + S.hue = cl(kv[0], 0, 1); + S.speed = cl(kv[1], 0.2, 3); + // The share of coin flips is the knob squared, so the first half of its + // turn opens the carpet up gradually. + const noise = cl(kv[2], 0, 1) ** 2; + // The prime nearest the knob. + let p = 3; + for (const q of PRIMES) if (abs(q - kv[3]) < abs(p - kv[3])) p = q; + if (!S.chi) S.seed = floor(Math.random() * 1e9); + // A turned knob rewrites the whole walk so far with the new rhythm. + const key = p + ':' + noise.toFixed(3); + if (key !== S.key) { + S.key = key; + S.p = p; + S.chi = legendre(p); + S.noise = noise; + rebuild(S, max(H, S.M)); + } + S.acc += d * S.speed * RATE; + while (S.acc >= 1) { + S.acc -= 1; + if (S.M >= LAST) { + S.seed = floor(Math.random() * 1e9); + rebuild(S, H); + } else step(S); + } +} + +// The hue wheel skips orange through yellow, which read brown and olive on +// the dim cells of small sums. +const wheel = (h) => 0.25 + 0.78 * (h - floor(h)); +function palette(S) { + const P = S.pal; + P[0] = wheel(0.282 + S.hue); // sums above zero: aqua at Hue 0 + P[1] = wheel(0.732 + S.hue); // sums below zero: magenta + S.palHue = S.hue; +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.chi) return; + if (S.palHue !== S.hue) palette(S); + const P = S.pal, c = [0, 0, 0], M = S.M, beat = 1 - S.acc; + for (let y = 0; y < H; y++) { + // The bottom row plays the newest step; the carpet rises above it, the + // newest sums on row 126. + const m = y === H - 1 ? M : M - (H - 2 - y), row = (((m % H) + H) % H) * W; + // Older rows dim a little toward the top, so the carpet reads as + // written from the bottom. + const age = 0.7 + 0.3 * (y / (H - 2)); + for (let x = 0; x < W; x++) { + let r = 0, g = 0, b = 0; + if (m >= 1 && y === H - 1) { + // The beat itself: a hit lights, a rest stays dark, fading over the step. + const prev = m > 1 ? S.ring[(((m - 1) % H) * W) + x] : 0; + if (S.ring[row + x] > prev) r = g = b = 0.3 + 0.7 * beat; + } else if (m >= 1) { + const s = S.ring[row + x], a = abs(s); + if (S.mark[row + x]) (r = 1), (g = 1), (b = 1); + else if (a) { + // Brightness climbs from 0.28 at 1 to full at 5, then the colour + // whitens a step for each doubling, nearly white past 80. + hsv(s > 0 ? P[0] : P[1], 0.9 - 0.7 * cl(Math.log2(a / 5) / 4, 0, 1), min(1, 0.1 + 0.18 * a), c, 0); + (r = c[0] * age), (g = c[1] * age), (b = c[2] * age); + } + } + display.setPixel(x, y, (255 * r) | 0, (255 * g) | 0, (255 * b) | 0); + } + } +} diff --git a/incoming/pattern-study/patterns/random-organization.js b/incoming/pattern-study/patterns/random-organization.js new file mode 100644 index 000000000..024743746 --- /dev/null +++ b/incoming/pattern-study/patterns/random-organization.js @@ -0,0 +1,247 @@ +// ===== Patternflow pattern ===== +// Title: Random Organization +// Family: sim +// Idea: Disks in a periodic box are sheared, one side sliding up and the other down and back once a cycle, every disk that touched another during the cycle is kicked a short random step, and the share kicked, traced along the bottom, falls cycle by cycle until below a critical strain the box finds places where nobody touches and freezes for good, while above it the flashing never stops. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: random organization under periodic shear (Laurent Corté, Paul Chaikin, Jerry Gollub and David Pine, 2008), an absorbing-state phase transition, pattern round 56 +// Round: 56 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Strain=1..4, Density=0.2..0.35 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 calm disks are teal, disks that have touched another this cycle gold, disks touching right now pale gold, a kick flashes white, and the material grid is slate; at 0.25 calm disks are violet and touched ones green, at 0.5 pink and blue, at 0.75 green and magenta. The calm colour skips orange and yellow, which read brown and olive at its level. +// Knob 2: Speed. Shear cycles a second, 0.75 x Speed: 1.5 at 2, 0.15 at 0.2, 2.25 at 3. At the opening Strain and Density the box froze after a median of 89 cycles over 30 runs, a minute at Speed 2; it then shears on calm for 10 cycles and a fresh box is dropped. +// Knob 3: Strain. The strain amplitude gamma0, 1 to 4: the panel's side columns travel 32 x gamma0 rows each way and back every cycle, and the grid's squares lean into parallelograms that steep. At the opening Density (83 disks) the median run froze after 89 cycles at 1, 219 at 2 and 504 at 2.5; at 3 half of 8 runs were still going after 6,000 cycles, and at 3.5 and 4 none stopped, with 52% and 63% of the disks kicked every cycle. +// Knob 4: Density. The disks' area fraction, 0.2 to 0.35: 83 to 146 disks of radius 2.5 LEDs in the 64 x 128 box, added at random or taken away at the end of a cycle. At the opening Strain the median run froze after 89 cycles at 0.2, 216 at 0.24 and 839 at 0.27; at 0.3 none of 8 runs stopped in 6,000 cycles, 44% of the disks kicked every cycle, and 65% at 0.35. + +const W = 64, H = 128, NP = W * H; +const { floor, min, max, abs, sqrt, cos, sin, round, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// Disk radius in LEDs, the kick (a random step up to KICK diameters), the +// most disks the arrays hold, and the calm cycles held before a fresh box. +const R = 2.5, D = 2 * R, KICK = 0.75, NMAX = 260, HOLD = 10; +// Shear cycles a second at Speed 1. +const RATE = 0.75; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} +// The knob's wheel skips orange through yellow, which reads brown and olive +// on dim disks. +const wheel = (h) => 0.18 + 0.85 * (h - floor(h)); + +export function setup(params) { + params.s = { + n: 0, X: new Float64Array(NMAX), Y: new Float64Array(NMAX), + first: new Float32Array(NMAX), age: new Float32Array(NMAX), flash: new Float32Array(NMAX), + pa: new Int16Array(2048), pb: new Int16Array(2048), lo: new Float32Array(2048), hi: new Float32Array(2048), np: 0, + phase: 0, g0: 1, gNext: 1, phi: 0.2, cps: 2, hue: 0, born: false, + hist: new Float32Array(W).fill(-1), quiet: 0, cycles: 0, f: 1, + rgb: new Float32Array(NP * 3), pal: new Float32Array(18), palHue: -1, + }; +} + +const count = (phi) => cl(round((phi * W * H) / (PI * R * R)), 4, NMAX); + +function scatter(S, i) { + S.X[i] = Math.random() * W; + S.Y[i] = Math.random() * H; + S.age[i] = 0; + S.flash[i] = 1; +} + +// A fresh box: disks dropped anywhere, overlaps and all. +function stir(S) { + S.n = count(S.phi); + for (let i = 0; i < S.n; i++) scatter(S, i); + S.hist.fill(-1); + S.quiet = 0; + S.cycles = 0; +} + +// The whole cycle's contacts, worked out before it starts. Under the shear +// y -> y + g (x - 32) a pair with reference separation (dx, dy) sits at +// (dx, dy + g dx), so it touches at strain g when |dy + g dx| < sqrt(D^2 - dx^2). +// That is an interval of g; where it meets [0, g0] the pair collides this +// cycle, and its lower end is the strain of first contact. The box is +// periodic in x and y, and its x images ride the shear with it +// (Lees-Edwards), so dx is the nearest image and dy is tried a box up and down. +function contacts(S) { + const { n, X, Y, first } = S, g0 = S.g0; + first.fill(1e9, 0, n); + S.np = 0; + for (let i = 0; i < n; i++) + for (let j = i + 1; j < n; j++) { + let dx = X[j] - X[i]; + dx -= W * round(dx / W); + if (dx >= D || dx <= -D) continue; + const h = sqrt(D * D - dx * dx); + for (let m = -1; m <= 1; m++) { + const dy = Y[j] - Y[i] + m * H; + let a, b; + if (abs(dx) < 1e-9) { + if (abs(dy) >= h) continue; + (a = 0), (b = g0); + } else { + const u = (-h - dy) / dx, v = (h - dy) / dx; + (a = max(0, min(u, v))), (b = min(g0, max(u, v))); + if (a >= b) continue; + } + if (a < first[i]) first[i] = a; + if (a < first[j]) first[j] = a; + if (S.np < 2048) { + S.pa[S.np] = i; + S.pb[S.np] = j; + S.lo[S.np] = a; + S.hi[S.np] = b; + S.np++; + } + } + } +} + +// End of a cycle, back at zero strain: every disk that touched anyone is +// moved a random step up to KICK diameters, the rest stay exactly put. +function endCycle(S) { + let k = 0; + for (let i = 0; i < S.n; i++) { + if (S.first[i] > 1e8) { + S.age[i] += 1; + continue; + } + k++; + const a = Math.random() * TAU, m = Math.random() * KICK * D; + S.X[i] = (S.X[i] + m * cos(a) + W) % W; + S.Y[i] = (S.Y[i] + m * sin(a) + H) % H; + S.age[i] = 0; + S.flash[i] = 1; + } + S.f = S.n ? k / S.n : 0; + S.hist.copyWithin(0, 1); + S.hist[W - 1] = S.f; + S.cycles++; + S.quiet = k ? 0 : S.quiet + 1; +} + +// Density turned: disks are added at random or the last ones taken away. +function resize(S) { + const want = count(S.phi); + while (S.n < want) scatter(S, S.n++); + if (S.n > want) S.n = want; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 1, 0.2], d = cl(dt || 0, 0, 0.1); + S.hue = cl(kv[0], 0, 1); + S.cps = cl(kv[1], 0.2, 3); + S.gNext = cl(kv[2], 1, 4); + S.phi = cl(kv[3], 0.2, 0.35); + if (!S.born) { + stir(S); + S.g0 = S.gNext; + contacts(S); + S.born = true; + } + for (let i = 0; i < S.n; i++) S.flash[i] = max(0, S.flash[i] - d * 4); + S.phase += d * S.cps * RATE; + while (S.phase >= 1) { + S.phase -= 1; + endCycle(S); + if (S.quiet >= HOLD) stir(S); + resize(S); + S.g0 = S.gNext; + contacts(S); + } +} + +function palette(S) { + const P = S.pal, h = S.hue; + hsv(wheel(h + 0.376), 0.65, 1, P, 0); // calm disks, teal at Hue 0 + hsv(h + 0.13, 0.85, 0.9, P, 3); // disks that touched this cycle, gold + hsv(wheel(h + 0.5), 0.45, 0.3, P, 6); // the sheared material grid + hsv(h + 0.13, 0.4, 1, P, 9); // touching right now, pale gold + hsv(h + 0.13, 0.55, 0.85, P, 12); // activity trace + S.palHue = S.hue; +} + +// One disk at lab centre (cx, cy), soft at the rim, kept brightest per LED. +function disk(S, cx, cy, r, g, b) { + const rgb = S.rgb, x0 = floor(cx - R - 1), y0 = floor(cy - R - 1); + for (let yy = y0; yy <= y0 + 2 * R + 2; yy++) { + const py = ((yy % H) + H) % H, ey = yy + 0.5 - cy; + for (let xx = x0; xx <= x0 + 2 * R + 2; xx++) { + if (xx < 0 || xx >= W) continue; + const ex = xx + 0.5 - cx, k = cl(R + 0.5 - sqrt(ex * ex + ey * ey), 0, 1); + if (k <= 0) continue; + const p = (py * W + xx) * 3; + if (rgb[p] < r * k) rgb[p] = r * k; + if (rgb[p + 1] < g * k) rgb[p + 1] = g * k; + if (rgb[p + 2] < b * k) rgb[p + 2] = b * k; + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.n) return; + if (S.palHue !== S.hue) palette(S); + const P = S.pal, rgb = S.rgb, g = (S.g0 * (1 - cos(TAU * S.phase))) / 2; + // How far the strain has reached this cycle: contacts up to here have happened. + const reach = S.phase < 0.5 ? g : S.g0; + rgb.fill(0); + // The material grid: squares 16 LEDs on a side in the unsheared box. The + // shear tilts their rows into parallelograms and slides the columns + // along themselves. + for (let x = 0; x < W; x++) { + const off = g * (x + 0.5 - 32); + for (let y = 0; y < H; y++) { + const v = (((y + 0.5 - off) % 16) + 16) % 16, k = max(cl(1.2 - abs(v - 8), 0, 1), x % 16 === 8 ? 0.6 : 0); + if (k <= 0) continue; + const p = (y * W + x) * 3; + rgb[p] = P[6] * k; + rgb[p + 1] = P[7] * k; + rgb[p + 2] = P[8] * k; + } + } + // Which disks touch right now. + const now = new Uint8Array(S.n); + for (let q = 0; q < S.np; q++) if (g > S.lo[q] && g < S.hi[q]) now[S.pa[q]] = now[S.pb[q]] = 1; + for (let i = 0; i < S.n; i++) { + let o = S.first[i] <= reach ? 3 : 0; + if (now[i]) o = 9; + // A calm disk kicked lately is brighter, settling over a few cycles to + // the steady level of disks that have stopped moving. + const f = S.flash[i], v = o ? 1 : 0.5 + 0.35 * Math.exp(-S.age[i] / 4); + let r = P[o] * v, gg = P[o + 1] * v, b = P[o + 2] * v; + (r += f * (1 - r)), (gg += f * (1 - gg)), (b += f * (1 - b)); + const cx = S.X[i], cy = S.Y[i] + g * (S.X[i] - 32); + disk(S, cx, cy, r, gg, b); + // Images across the side edges ride the shear 64 * g further. + if (cx < R + 1) disk(S, cx + W, cy + g * W, r, gg, b); + if (cx > W - R - 1) disk(S, cx - W, cy - g * W, r, gg, b); + } + // Activity: the share of disks kicked in each of the last 64 cycles, one + // column a cycle, newest on the right, full height 16 rows. + for (let x = 0; x < W; x++) { + const f = S.hist[x]; + if (f < 0) continue; + const top = round(f * 16); + for (let k = 0; k <= top; k++) { + const p = ((H - 1 - k) * W + x) * 3, a = k === top ? 1 : 0.35; + rgb[p] = max(rgb[p], P[12] * a); + rgb[p + 1] = max(rgb[p + 1], P[13] * a); + rgb[p + 2] = max(rgb[p + 2], P[14] * a); + } + } + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p += 3) + display.setPixel(x, y, (255 * min(1, rgb[p])) | 0, (255 * min(1, rgb[p + 1])) | 0, (255 * min(1, rgb[p + 2])) | 0); +} From 0a56e821d3305858be8acaab38b692e435fd086b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 06:23:46 +0000 Subject: [PATCH 04/10] =?UTF-8?q?56=ED=8C=90:=20Elliptic=20Law,=20Twist=20?= =?UTF-8?q?and=20Writhe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UUXdHR3tRiDEQfEfGbQxb9 --- .../pattern-study/patterns/elliptic-law.js | 411 ++++++++++++++++++ .../pattern-study/patterns/twist-writhe.js | 347 +++++++++++++++ 2 files changed, 758 insertions(+) create mode 100644 incoming/pattern-study/patterns/elliptic-law.js create mode 100644 incoming/pattern-study/patterns/twist-writhe.js diff --git a/incoming/pattern-study/patterns/elliptic-law.js b/incoming/pattern-study/patterns/elliptic-law.js new file mode 100644 index 000000000..ea3e6abbe --- /dev/null +++ b/incoming/pattern-study/patterns/elliptic-law.js @@ -0,0 +1,411 @@ +// ===== Patternflow pattern ===== +// Title: Elliptic Law +// Family: particle +// Idea: The eigenvalues of a random real matrix whose entries wander drift as glowing particles inside an ellipse that stands up the panel, mirrored in pairs across the real axis down its middle, and whenever a mirrored pair meets on the axis it splits into two real eigenvalues that run apart along it, or two real ones meet and leave it as a pair. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: circular law (Jean Ginibre, 1965; Vyacheslav Girko, 1984), elliptic law (Hans-Jürgen Sommers, Andrea Crisanti, Haim Sompolinsky and Yaakov Stein, 1988), real eigenvalues of a real Gaussian matrix (Alan Edelman, Eric Kostlan and Michael Shub, 1994), pattern round 56 +// Round: 56 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Tau=0.333..1, Size=40..80 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the complex eigenvalues are cyan over a deep blue glow and the real ones pale violet, at 0.25 violet and red, at 0.5 rose and green, at 0.75 green and azure. The wheel skips the orange and yellow stretch, which reads brown when dim. +// Knob 2: Speed. How fast the matrix entries wander. At 2 an entry's correlation with where it was first falls to zero after 4.9 s, an eigenvalue in the middle of the ellipse moves about 12 LEDs a second, and somewhere on the axis a pair meets or parts about 6.5 times a second. At 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Tau. The correlation between the entries J_ij and J_ji, from 1/3 to 1. The ellipse has half-axes 1 + Tau along the real axis and 1 - Tau across it, drawn so the real half-axis is always 61 LEDs: at 1/3 it is 61 LEDs wide and fills the panel, at 0.6 30 wide, at 0.8 14, and at 1 the matrix is symmetric and every eigenvalue lies on the axis. A 40 x 40 matrix has 7.6 real eigenvalues on average at 1/3, 10.4 at 0.6 and 15.1 at 0.8 (4,000 matrices each). +// Knob 4: Size. The matrix is 40 x 40 to 80 x 80. The ellipse stays the same size and fills more evenly, and the share of real eigenvalues falls: at Tau 1/3 a 40 x 40 matrix has 7.6 of them on average and an 80 x 80 one 10.5, close to Peter Forrester and Taro Nagao's sqrt(2N/pi (1 + Tau)/(1 - Tau)) (2008), which gives 7.1 and 10.1. + +const W = 64, H = 128, NPX = W * H, NM = 80; +const { floor, min, max, sqrt, exp, sin, cos, log, abs, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// The real half-axis in LEDs, and the centre of the panel. +const RPX = 61, CX = 32, CY = 64; +// Entry clock: units of the entries' own time per second at Speed 1. In one +// unit a resting entry turns one radian of its oscillation (below). +const RATE = 0.22; +// Damping of each entry's velocity, per unit of entry time. +const GAMMA = 0.8; +// Short trails fade by e in TRAIL seconds, the long exposure in EXPO. +const TRAIL = 0.3, EXPO = 3.5; +// A spark where a pair met on the axis lasts this long. +const SPARK = 0.4; + +function gauss() { + let u = 0; + while (u < 1e-12) u = Math.random(); + return sqrt(-2 * log(u)) * cos(TAU * Math.random()); +} + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} +// The wheel skips 0.03..0.18, orange through yellow, which reads brown and +// olive when dim, and runs the rest a little faster. +const wheel = (h) => 0.18 + 0.85 * (h - 0.18 - floor(h - 0.18)); + +export function setup(params) { + const g = new Float64Array(NM * NM), v = new Float64Array(NM * NM); + for (let i = 0; i < NM * NM; i++) (g[i] = gauss()), (v[i] = gauss()); + params.s = { + g, v, hue: 0, speed: 2, tau: 1 / 3, n: 40, + a: new Float64Array(NM * NM), wr: new Float64Array(NM), wi: new Float64Array(NM), + // Eigenvalues as LED positions, this frame and last. + px: new Float32Array(NM), py: new Float32Array(NM), re: new Uint8Array(NM), + qx: new Float32Array(NM), qy: new Float32Array(NM), qre: new Uint8Array(NM), qn: 0, + trail: new Float32Array(NPX * 3), expo: new Float32Array(NPX * 2), rgb: new Float32Array(NPX * 3), flash: [], + pal: new Float32Array(15), palHue: -1, solved: false, fails: 0, + }; +} + +// Each entry of the matrix is built from N(0, 1) numbers g that move as the +// position of a damped oscillator kicked by noise, a two-dimensional +// Ornstein-Uhlenbeck process whose resting law is N(0, 1) for g and its +// velocity v alike. A step of h units turns (g, v) by h radians, which keeps +// that law, then takes v through an exact Ornstein-Uhlenbeck step, which keeps it +// too; so at every moment the matrix is a fresh draw of the ensemble, and +// g moves smoothly enough for the eigenvalues to glide instead of jitter. +function wander(S, h) { + const { g, v, n } = S, c = cos(h), s = sin(h), e = exp(-GAMMA * h), k = sqrt(1 - e * e); + for (let i = 0; i < n; i++) + for (let j = 0, o = i * NM; j < n; j++, o++) { + const a = g[o], b = v[o]; + g[o] = a * c + b * s; + v[o] = (b * c - a * s) * e + k * gauss(); + } +} + +// The elliptic ensemble from those numbers: above the diagonal g holds the +// symmetric parts, below it the antisymmetric ones, so +// J_ij = (c1 g_ij + c2 g_ji) / sqrt N, J_ji = (c1 g_ij - c2 g_ji) / sqrt N, +// with c1^2 = (1 + tau)/2 and c2^2 = (1 - tau)/2: every entry has variance +// 1/N and E J_ij J_ji = tau/N. +function build(S) { + const { g, a, n, tau } = S, r = 1 / sqrt(n), c1 = sqrt((1 + tau) / 2) * r, c2 = sqrt((1 - tau) / 2) * r; + for (let i = 0; i < n; i++) { + a[i * n + i] = sqrt(1 + tau) * g[i * NM + i] * r; + for (let j = i + 1; j < n; j++) { + const p = g[i * NM + j], q = g[j * NM + i]; + a[i * n + j] = c1 * p + c2 * q; + a[j * n + i] = c1 * p - c2 * q; + } + } +} + +// Reduction to upper Hessenberg form by stabilised elementary similarity +// transforms (row and column swaps, then elimination below the subdiagonal). +function hessenberg(a, n) { + for (let m = 1; m < n - 1; m++) { + let x = 0, i = m; + for (let j = m; j < n; j++) if (abs(a[j * n + m - 1]) > abs(x)) (x = a[j * n + m - 1]), (i = j); + if (i !== m) { + for (let j = m - 1; j < n; j++) { + const t = a[i * n + j]; + a[i * n + j] = a[m * n + j]; + a[m * n + j] = t; + } + for (let j = 0; j < n; j++) { + const t = a[j * n + i]; + a[j * n + i] = a[j * n + m]; + a[j * n + m] = t; + } + } + if (x === 0) continue; + for (i = m + 1; i < n; i++) { + let y = a[i * n + m - 1]; + if (y === 0) continue; + y /= x; + a[i * n + m - 1] = y; + for (let j = m; j < n; j++) a[i * n + j] -= y * a[m * n + j]; + for (let j = 0; j < n; j++) a[j * n + m] += y * a[j * n + i]; + } + } +} + +// Francis's implicit double-shift QR on the Hessenberg matrix, eigenvalues +// only. Each sweep chases a 3 x 3 Householder bulge down the subdiagonal +// with the two shifts the trailing 2 x 2 block gives, a pair of complex +// conjugates kept in real arithmetic; a small subdiagonal entry splits off +// one real eigenvalue or a 2 x 2 block whose roots are a real pair or a +// conjugate pair. Real roots come back with wi exactly 0. +function francis(a, n, wr, wi) { + let nn = n - 1, t = 0, anorm = 0, p = 0, q = 0, r = 0, x, y, z, w, s, u, v, l, m; + for (let i = 0; i < n; i++) for (let j = max(i - 1, 0); j < n; j++) anorm += abs(a[i * n + j]); + while (nn >= 0) { + let its = 0; + do { + for (l = nn; l > 0; l--) { + s = abs(a[(l - 1) * n + l - 1]) + abs(a[l * n + l]); + if (s === 0) s = anorm; + if (abs(a[l * n + l - 1]) + s === s) { + a[l * n + l - 1] = 0; + break; + } + } + x = a[nn * n + nn]; + if (l === nn) { + wr[nn] = x + t; + wi[nn--] = 0; + } else { + y = a[(nn - 1) * n + nn - 1]; + w = a[nn * n + nn - 1] * a[(nn - 1) * n + nn]; + if (l === nn - 1) { + p = 0.5 * (y - x); + q = p * p + w; + z = sqrt(abs(q)); + x += t; + if (q >= 0) { + z = p + (p >= 0 ? z : -z); + wr[nn - 1] = wr[nn] = x + z; + if (z) wr[nn] = x - w / z; + wi[nn - 1] = wi[nn] = 0; + } else { + wr[nn - 1] = wr[nn] = x + p; + wi[nn - 1] = -(wi[nn] = z); + } + nn -= 2; + } else { + if (its === 60) return false; + // Exceptional shifts after 10 and 20 sweeps without a split. + if (its === 10 || its === 20) { + t += x; + for (let i = 0; i <= nn; i++) a[i * n + i] -= x; + s = abs(a[nn * n + nn - 1]) + abs(a[(nn - 1) * n + nn - 2]); + y = x = 0.75 * s; + w = -0.4375 * s * s; + } + ++its; + // Where the bulge can start: two small subdiagonals in a row. + for (m = nn - 2; m >= l; m--) { + z = a[m * n + m]; + r = x - z; + s = y - z; + p = (r * s - w) / a[(m + 1) * n + m] + a[m * n + m + 1]; + q = a[(m + 1) * n + m + 1] - z - r - s; + r = a[(m + 2) * n + m + 1]; + s = abs(p) + abs(q) + abs(r); + p /= s; + q /= s; + r /= s; + if (m === l) break; + u = abs(a[m * n + m - 1]) * (abs(q) + abs(r)); + v = abs(p) * (abs(a[(m - 1) * n + m - 1]) + abs(z) + abs(a[(m + 1) * n + m + 1])); + if (u + v === v) break; + } + for (let i = m; i < nn - 1; i++) { + a[(i + 2) * n + i] = 0; + if (i !== m) a[(i + 2) * n + i - 1] = 0; + } + // Chase the bulge. + for (let k = m; k < nn; k++) { + if (k !== m) { + p = a[k * n + k - 1]; + q = a[(k + 1) * n + k - 1]; + r = k + 1 !== nn ? a[(k + 2) * n + k - 1] : 0; + if ((x = abs(p) + abs(q) + abs(r)) !== 0) (p /= x), (q /= x), (r /= x); + } + const sg = sqrt(p * p + q * q + r * r); + if ((s = p >= 0 ? sg : -sg) === 0) continue; + if (k === m) { + if (l !== m) a[k * n + k - 1] = -a[k * n + k - 1]; + } else a[k * n + k - 1] = -s * x; + p += s; + x = p / s; + y = q / s; + z = r / s; + q /= p; + r /= p; + for (let j = k; j <= nn; j++) { + p = a[k * n + j] + q * a[(k + 1) * n + j]; + if (k + 1 !== nn) { + p += r * a[(k + 2) * n + j]; + a[(k + 2) * n + j] -= p * z; + } + a[(k + 1) * n + j] -= p * y; + a[k * n + j] -= p * x; + } + const top = nn < k + 3 ? nn : k + 3; + for (let i = l; i <= top; i++) { + p = x * a[i * n + k] + y * a[i * n + k + 1]; + if (k + 1 !== nn) { + p += z * a[i * n + k + 2]; + a[i * n + k + 2] -= p * r; + } + a[i * n + k + 1] -= p * q; + a[i * n + k] -= p; + } + } + } + } + } while (l < nn - 1); + } + return true; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 1 / 3, 40], d = cl(dt || 0, 0, 0.1); + S.hue = cl(kv[0], 0, 1); + S.speed = cl(kv[1], 0.2, 3); + S.tau = cl(kv[2], 1 / 3, 1); + S.n = cl(floor(kv[3] + 0.5), 40, NM); + wander(S, d * S.speed * RATE); + build(S); + hessenberg(S.a, S.n); + // A failed solve keeps last frame's eigenvalues; none failed in 18,000 + // frames at the knobs' ends. + if (!francis(S.a, S.n, S.wr, S.wi)) { + S.fails++; + return; + } + // Last frame's positions, for the trails and to spot a meeting on the axis. + S.qx.set(S.px); + S.qy.set(S.py); + S.qre.set(S.re); + S.qn = S.solved ? S.qn : 0; + const sc = RPX / (1 + S.tau); + for (let k = 0; k < S.n; k++) { + S.px[k] = CX + S.wi[k] * sc; + S.py[k] = CY - S.wr[k] * sc; + S.re[k] = S.wi[k] === 0 ? 1 : 0; + } + const qn = S.qn; + S.qn = S.n; + S.solved = true; + // An eigenvalue whose nearest one last frame was of the other kind has + // just crossed over: a real one born from a conjugate pair meeting on the + // axis, or a complex one from two real ones meeting. A spark marks where. + if (qn === S.n) + for (let k = 0; k < S.n; k++) { + let best = 1e9, bj = -1; + for (let j = 0; j < qn; j++) { + const dx = S.qx[j] - S.px[k], dy = S.qy[j] - S.py[k], e = dx * dx + dy * dy; + if (e < best) (best = e), (bj = j); + } + if (bj < 0 || S.qre[bj] === S.re[k] || best > 36) continue; + const fy = S.re[k] ? S.py[k] : S.qy[bj]; + if (S.flash.some((f) => abs(f.y - fy) < 3 && f.age < 0.2)) continue; + if (S.flash.length < 12) S.flash.push({ y: fy, age: 0, real: S.re[k] }); + } + for (const f of S.flash) f.age += d; + S.flash = S.flash.filter((f) => f.age < SPARK); + S.dt = d; +} + +function palette(S) { + const P = S.pal, h = S.hue; + hsv(wheel(0.58 + h), 0.75, 1, P, 0); // complex heads and trails + hsv(wheel(0.9 + h), 0.6, 1, P, 3); // real heads and trails + hsv(wheel(0.63 + h), 0.85, 1, P, 6); // long exposure, complex + hsv(wheel(0.92 + h), 0.7, 1, P, 9); // long exposure, real + hsv(wheel(0.6 + h), 0.5, 1, P, 12); // the law's ellipse + S.palHue = S.hue; +} + +// A point spread over its four nearest LEDs by how close it falls. +function splat(buf, x, y, c0, c1, c2, a) { + const fx = x - 0.5, fy = y - 0.5, ix = floor(fx), iy = floor(fy), tx = fx - ix, ty = fy - iy; + for (let dy = 0; dy < 2; dy++) { + const yy = iy + dy; + if (yy < 0 || yy >= H) continue; + const wy = dy ? ty : 1 - ty; + for (let dx = 0; dx < 2; dx++) { + const xx = ix + dx; + if (xx < 0 || xx >= W) continue; + const k = (dx ? tx : 1 - tx) * wy * a, p = (yy * W + xx) * 3; + buf[p] = max(buf[p], c0 * k); + buf[p + 1] = max(buf[p + 1], c1 * k); + buf[p + 2] = max(buf[p + 2], c2 * k); + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.solved) return; + if (S.palHue !== S.hue) palette(S); + const P = S.pal, n = S.n, tr = S.trail, ex = S.expo, rgb = S.rgb, d = S.dt || 1 / 30; + // Trails and exposure fade with panel time, so they keep their length in + // LEDs at any Speed. + const ft = exp(-d / TRAIL), fe = exp(-d / EXPO); + for (let i = 0; i < NPX * 3; i++) tr[i] *= ft; + for (let i = 0; i < NPX * 2; i++) ex[i] *= fe; + // Each eigenvalue draws a line back to the nearest of last frame's, and + // drops a little light into the long exposure, which builds up the + // ellipse's even density and the line of real eigenvalues. + for (let k = 0; k < n; k++) { + const x = S.px[k], y = S.py[k], o = S.re[k] ? 3 : 0; + let best = 1e9, bx = x, by = y; + for (let j = 0; j < S.qn; j++) { + const dx = S.qx[j] - x, dy = S.qy[j] - y, e = dx * dx + dy * dy; + if (e < best) (best = e), (bx = S.qx[j]), (by = S.qy[j]); + } + if (best > 100) (bx = x), (by = y); + const m = Math.ceil(sqrt((bx - x) ** 2 + (by - y) ** 2) * 2) || 1; + for (let e = 0; e <= m; e++) splat(tr, bx + ((x - bx) * e) / m, by + ((y - by) * e) / m, P[o] * 0.8, P[o + 1] * 0.8, P[o + 2] * 0.8, 1.6); + const fx = x - 0.5, fy = y - 0.5, ix = floor(fx), iy = floor(fy), tx = fx - ix, ty = fy - iy, ch = S.re[k] ? 1 : 0; + for (let dy = 0; dy < 2; dy++) + for (let dx = 0; dx < 2; dx++) { + const xx = ix + dx, yy = iy + dy; + if (xx < 0 || xx >= W || yy < 0 || yy >= H) continue; + ex[(yy * W + xx) * 2 + ch] += (dx ? tx : 1 - tx) * (dy ? ty : 1 - ty) * d * (ch ? 3 : 24); + } + } + // The law: the ellipse with half-axes 1 + tau and 1 - tau, a thin line. + const sc = RPX / (1 + S.tau), ah = (1 - S.tau) * sc, bh = (1 + S.tau) * sc; + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const o = p * 3; + let R = tr[o], G = tr[o + 1], B = tr[o + 2]; + // Exposure, through a soft knee so a busy spot does not burn out. + const e0 = ex[p * 2], e1 = ex[p * 2 + 1]; + const k0 = 0.4 * (1 - exp(-e0 * 1.4)), k1 = 0.5 * (1 - exp(-e1 * 1.2)); + R += P[6] * k0 + P[9] * k1; + G += P[7] * k0 + P[10] * k1; + B += P[8] * k0 + P[11] * k1; + // Distance to the ellipse, roughly in LEDs. + const u = (x + 0.5 - CX) / max(ah, 0.3), v = (y + 0.5 - CY) / bh, rr = sqrt(u * u + v * v); + const gx = u / max(ah, 0.3), gy = v / bh, gl = sqrt(gx * gx + gy * gy) || 1; + const dist = (abs(rr - 1) * rr) / gl; + if (dist < 1.2 && ah > 1) { + const a = 0.3 * (1 - dist / 1.2); + R += P[12] * a; + G += P[13] * a; + B += P[14] * a; + } + (rgb[o] = R), (rgb[o + 1] = G), (rgb[o + 2] = B); + } + // Sparks where a pair met on the axis: a short bar across the axis that + // shrinks as it fades, in the colour of what was born there. + for (const f of S.flash) { + const u = f.age / SPARK, a = (1 - u) * (1 - u), o = f.real ? 3 : 0, half = 4.5 * (1 - u) + 1; + const iy = floor(f.y); + for (let dy = -1; dy <= 1; dy++) { + const yy = iy + dy; + if (yy < 0 || yy >= H) continue; + for (let xx = floor(CX - half); xx < CX + half; xx++) { + if (xx < 0 || xx >= W) continue; + const w = a * (dy ? 0.45 : 1) * min(1, half - abs(xx + 0.5 - CX) + 0.5), q = (yy * W + xx) * 3; + for (let e = 0; e < 3; e++) rgb[q + e] += (0.6 + P[o + e]) * w; + } + } + } + for (let k = 0; k < n; k++) { + const x = S.px[k], y = S.py[k], o = S.re[k] ? 3 : 0; + const ix = floor(x - 0.5), iy = floor(y - 0.5); + for (let dy = 0; dy < 2; dy++) + for (let dx = 0; dx < 2; dx++) { + const xx = ix + dx, yy = iy + dy; + if (xx < 0 || xx >= W || yy < 0 || yy >= H) continue; + const q = (yy * W + xx) * 3; + for (let e = 0; e < 3; e++) rgb[q + e] = max(rgb[q + e], 0.45 + P[o + e] * 0.6); + } + } + for (let y = 0, o = 0; y < H; y++) + for (let x = 0; x < W; x++, o += 3) display.setPixel(x, y, (255 * min(1, rgb[o])) | 0, (255 * min(1, rgb[o + 1])) | 0, (255 * min(1, rgb[o + 2])) | 0); +} diff --git a/incoming/pattern-study/patterns/twist-writhe.js b/incoming/pattern-study/patterns/twist-writhe.js new file mode 100644 index 000000000..4c3c6c466 --- /dev/null +++ b/incoming/pattern-study/patterns/twist-writhe.js @@ -0,0 +1,347 @@ +// ===== Patternflow pattern ===== +// Title: Twist and Writhe +// Family: topology +// Idea: A closed ribbon, twisted Lk times round a flat circle, folds through a figure-eight into a tall supercoil while its two coloured edges stay linked the same number of times, so the twist of its two-toned faces unwinds as the writhe of its centre line grows, and a bar along the bottom shows Tw and Wr adding up to the same whole number all the way. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Călugăreanu-White-Fuller theorem Lk = Tw + Wr (Gheorghe Călugăreanu, 1959; James H. White, 1969; F. Brock Fuller, 1971), supercoiled DNA (Jerome Vinograd, 1965), pattern round 56 +// Round: 56 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Lk=3..6, Coil=3..6 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the ribbon's front face is azure and its back violet, its edges pink and lime; at 0.25 purple and rose with green and aqua edges, at 0.5 red and green with cyan and blue, at 0.75 green and sky blue with violet and magenta. The wheel skips orange and yellow, which read brown on the shaded far side. +// Knob 2: Speed. At 2 a cycle takes 12 s: the loop lies flat for 1.2 s, coils for 4.5 s, holds for 1.8 s and uncoils for 4.5 s, while the view turns half a turn, slowly while the loop is flat. At 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Lk. The linking number of the two edges, a whole number from 3 to 6. On the flat circle it is all twist: the ribbon turns over 2 Lk times round the loop, a half-turn every 47 LEDs of ribbon at 3 and every 24 at 6. Turning it cuts the ribbon, turns one end and joins it again, as a type I topoisomerase does to a ring of DNA one turn at a time. +// Knob 4: Coil. The writhe the centre line reaches at the deepest point of the cycle, 3 to 6, so the twist left there is Lk - Coil. At 3 the supercoil winds 4.3 half-turns and stands 106 LEDs tall and 14 across; at 6 it winds 8.1 and is 8 across, its strands 6 LEDs apart. Past Lk the ribbon comes out twisted the other way, and the Tw bar runs back to the mark, striped. + +const W = 64, H = 128, NPX = W * H; +const { floor, min, max, sqrt, sin, cos, abs, atan2, asin, round, hypot, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// Points on the centre line, and its length in LEDs: a circle of radius 45. +const M = 144, LEN = TAU * 45; +// Half the ribbon's width, in LEDs. +const HW = 3; +// Where the middle of the ribbon sits on the panel; the bar is below. +const CX = 32, CY = 57; +// The cycle at Speed 2, in seconds: flat, coiling, held, uncoiling. +const FLAT = 1.2, UP = 4.5, HOLD = 1.8, DOWN = 4.5, CYCLE = FLAT + UP + HOLD + DOWN; +// Writhe of the coiling family below at q = 0, 0.5, 1, ... 9 half-turns, +// from the same Gauss integral the pattern runs, used only to pick the q +// whose writhe is the knob's target. The writhe shown is computed afresh. +const WR_OF_Q = [0, 0.124, 0.454, 0.823, 1.189, 1.584, 1.979, 2.375, 2.771, 3.166, 3.56, 3.954, 4.347, 4.74, 5.132, 5.525, 5.917, 6.309, 6.701]; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} +// Dim orange and yellow read brown and olive, so the wheel skips 0.03..0.18. +const wheel = (h) => 0.18 + 0.85 * (h - 0.18 - floor(h - 0.18)); + +export function setup(params) { + const K = M * 4; + params.s = { + t: 0, hue: 0, speed: 2, lk: 3, coil: 3, q: 0, + P: new Float64Array(M * 3), T: new Float64Array(M * 3), U: new Float64Array(M * 3), P1: new Float64Array(M * 3), + Q: new Float64Array(K * 3), cum: new Float64Array(K + 1), + wr: 0, tw: 0, phi: 0, yaw: 0, + sx: new Float32Array(M * 2), sy: new Float32Array(M * 2), sz: new Float32Array(M * 2), face: new Float32Array(M), + zb: new Float32Array(NPX), rgb: new Float32Array(NPX * 3), pal: new Float32Array(18), palHue: -1, + }; +} + +// The coiling family, q half-turns deep. An oval in the xz-plane, a circle +// at q = 0 and a rounded long loop (superellipse exponent 0.6) from q = 2, +// is twisted about its long axis z by q pi from bottom to top, so its two +// long sides wind round each other q half-turns as the two strands of a +// supercoil; the oval stretches so the strands keep a pitch of about 42 +// degrees. It never passes through itself: at each height its two strands +// sit on opposite sides of the axis. The curve is then laid out at equal +// steps of arc length and scaled to LEN, so the ribbon keeps its length. +function shape(S, q) { + const { P, Q, cum } = S, K = M * 4; + const b = sqrt(1 + ((q * PI) / 1.8) ** 2), p = 1 - 0.4 * min(1, q / 2), kap = -(q * PI) / (2 * b); + for (let i = 0; i < K; i++) { + const th = (TAU * i) / K, c = cos(th), s = sin(th); + const x = (c < 0 ? -1 : 1) * abs(c) ** p, z = b * (s < 0 ? -1 : 1) * abs(s) ** p, ps = kap * z; + Q[i * 3] = x * cos(ps); + Q[i * 3 + 1] = x * sin(ps); + Q[i * 3 + 2] = z; + } + cum[0] = 0; + for (let i = 0; i < K; i++) { + const j = ((i + 1) % K) * 3, o = i * 3; + cum[i + 1] = cum[i] + hypot(Q[j] - Q[o], Q[j + 1] - Q[o + 1], Q[j + 2] - Q[o + 2]); + } + const k0 = LEN / cum[K]; + for (let i = 0, k = 0; i < M; i++) { + const s = (cum[K] * i) / M; + while (cum[k + 1] < s) k++; + const f = (s - cum[k]) / (cum[k + 1] - cum[k] || 1), o = k * 3, j = ((k + 1) % K) * 3; + for (let e = 0; e < 3; e++) P[i * 3 + e] = (Q[o + e] + f * (Q[j + e] - Q[o + e])) * k0; + } +} + +// The writhe by the Gauss double integral over the closed polygon, each +// pair of segments in closed form: the solid angle the one sees the other +// under, signed, over 4 pi (Konstantin Klenin and Jörg Langowski, 2000). +function writhe(P) { + let w = 0; + const r = S12, n = N12; + for (let i = 0; i < M; i++) { + const a = i * 3, b = ((i + 1) % M) * 3, fx = P[b] - P[a], fy = P[b + 1] - P[a + 1], fz = P[b + 2] - P[a + 2]; + for (let j = i + 2; j < M; j++) { + if (i === 0 && j === M - 1) continue; + const c = j * 3, d = ((j + 1) % M) * 3; + // r13, r14, r24, r23: from the ends of segment i to those of j. + (r[0] = P[c] - P[a]), (r[1] = P[c + 1] - P[a + 1]), (r[2] = P[c + 2] - P[a + 2]); + (r[3] = P[d] - P[a]), (r[4] = P[d + 1] - P[a + 1]), (r[5] = P[d + 2] - P[a + 2]); + (r[6] = P[d] - P[b]), (r[7] = P[d + 1] - P[b + 1]), (r[8] = P[d + 2] - P[b + 2]); + (r[9] = P[c] - P[b]), (r[10] = P[c + 1] - P[b + 1]), (r[11] = P[c + 2] - P[b + 2]); + for (let k = 0; k < 12; k += 3) { + const v = (k + 3) % 12; + const x = r[k + 1] * r[v + 2] - r[k + 2] * r[v + 1], y = r[k + 2] * r[v] - r[k] * r[v + 2], z = r[k] * r[v + 1] - r[k + 1] * r[v]; + const l = sqrt(x * x + y * y + z * z) || 1; + (n[k] = x / l), (n[k + 1] = y / l), (n[k + 2] = z / l); + } + let om = 0; + for (let k = 0; k < 12; k += 3) { + const v = (k + 3) % 12; + om += asin(cl(n[k] * n[v] + n[k + 1] * n[v + 1] + n[k + 2] * n[v + 2], -1, 1)); + } + // The sign: (r34 x r12) . r13. + const ex = P[d] - P[c], ey = P[d + 1] - P[c + 1], ez = P[d + 2] - P[c + 2]; + const sg = (ey * fz - ez * fy) * r[0] + (ez * fx - ex * fz) * r[1] + (ex * fy - ey * fx) * r[2]; + w += sg > 0 ? om : sg < 0 ? -om : 0; + } + } + return (2 * w) / (4 * PI); +} +const S12 = new Float64Array(12), N12 = new Float64Array(12); + +// Rotate v (at o in V) about the unit axis that turns t0 into t1, by the +// angle between them: parallel transport along the polygon, which adds no +// twist. Writes into out at p. +function transport(V, o, t0, t1, out, p) { + const ax = t0[1] * t1[2] - t0[2] * t1[1], ay = t0[2] * t1[0] - t0[0] * t1[2], az = t0[0] * t1[1] - t0[1] * t1[0]; + const s = hypot(ax, ay, az), c = t0[0] * t1[0] + t0[1] * t1[1] + t0[2] * t1[2]; + const vx = V[o], vy = V[o + 1], vz = V[o + 2]; + if (s < 1e-12) { + (out[p] = vx), (out[p + 1] = vy), (out[p + 2] = vz); + return; + } + const kx = ax / s, ky = ay / s, kz = az / s, kd = (kx * vx + ky * vy + kz * vz) * (1 - c); + out[p] = vx * c + (ky * vz - kz * vy) * s + kx * kd; + out[p + 1] = vy * c + (kz * vx - kx * vz) * s + ky * kd; + out[p + 2] = vz * c + (kx * vy - ky * vx) * s + kz * kd; +} + +// The ribbon. Tangents at the points, a parallel-transported normal P1 +// round the loop, and its holonomy phi: how far P1 has turned about the +// tangent when it comes back to the start. A closed ribbon turns about the +// curve a whole number of times relative to P1 less phi / 2 pi, so its +// twist is m - phi / 2 pi. The twist is spread evenly, as in an elastic rod +// at rest, and m is the whole number that makes the two edges link Lk +// times: by the theorem the one that brings Tw + Wr nearest Lk. Tw is then +// measured off the built ribbon, angle by angle. Over whole cycles at every +// Lk with Coil 3, 4.5 and 6, the Gauss linking integral of the two edges +// came out Lk exactly each time, and Tw + Wr stayed within 0.005 of it. +function ribbon(S) { + const { P, T, U, P1 } = S, t0 = [0, 0, 0], t1 = [0, 0, 0], tmp = [0, 0, 0]; + for (let i = 0; i < M; i++) { + const a = ((i + M - 1) % M) * 3, b = ((i + 1) % M) * 3, o = i * 3; + const x = P[b] - P[a], y = P[b + 1] - P[a + 1], z = P[b + 2] - P[a + 2], l = hypot(x, y, z) || 1; + T[o] = x / l; + T[o + 1] = y / l; + T[o + 2] = z / l; + } + // Start P1 pointing into the loop, square to the tangent. + { + const d = -(P[0] * T[0] + P[1] * T[1] + P[2] * T[2]); + let x = -P[0] + d * T[0], y = -P[1] + d * T[1], z = -P[2] + d * T[2]; + const l = hypot(x, y, z) || 1; + (P1[0] = x / l), (P1[1] = y / l), (P1[2] = z / l); + } + const tv = (o, out) => ((out[0] = T[o]), (out[1] = T[o + 1]), (out[2] = T[o + 2])); + for (let i = 1; i <= M; i++) { + tv((i - 1) * 3, t0); + tv((i % M) * 3, t1); + if (i < M) transport(P1, (i - 1) * 3, t0, t1, P1, i * 3); + else transport(P1, (M - 1) * 3, t0, t1, tmp, 0); + } + // tmp is P1 carried once round; phi is its angle from P1 at the start. + const p2x = T[1] * P1[2] - T[2] * P1[1], p2y = T[2] * P1[0] - T[0] * P1[2], p2z = T[0] * P1[1] - T[1] * P1[0]; + S.phi = atan2(tmp[0] * p2x + tmp[1] * p2y + tmp[2] * p2z, tmp[0] * P1[0] + tmp[1] * P1[1] + tmp[2] * P1[2]); + const m = round(S.lk - S.wr + S.phi / TAU), tw = m - S.phi / TAU; + for (let i = 0; i < M; i++) { + const o = i * 3, th = (TAU * tw * i) / M, c = cos(th), s = sin(th); + const qx = T[o + 1] * P1[o + 2] - T[o + 2] * P1[o + 1], qy = T[o + 2] * P1[o] - T[o] * P1[o + 2], qz = T[o] * P1[o + 1] - T[o + 1] * P1[o]; + U[o] = c * P1[o] + s * qx; + U[o + 1] = c * P1[o + 1] + s * qy; + U[o + 2] = c * P1[o + 2] + s * qz; + } + // The twist of the ribbon as built: each U carried to the next point, + // and the angle it is then short of that point's U, about the tangent. + let sum = 0; + for (let i = 0; i < M; i++) { + const j = ((i + 1) % M) * 3; + tv(i * 3, t0); + tv(j, t1); + transport(U, i * 3, t0, t1, tmp, 0); + const wx = t1[1] * tmp[2] - t1[2] * tmp[1], wy = t1[2] * tmp[0] - t1[0] * tmp[2], wz = t1[0] * tmp[1] - t1[1] * tmp[0]; + sum += atan2(U[j] * wx + U[j + 1] * wy + U[j + 2] * wz, U[j] * tmp[0] + U[j + 1] * tmp[1] + U[j + 2] * tmp[2]); + } + S.tw = sum / TAU; +} + +// The q whose writhe is w, from the table. +function qFor(w) { + if (w <= 0) return 0; + for (let k = 1; k < WR_OF_Q.length; k++) + if (WR_OF_Q[k] >= w) return 0.5 * (k - 1 + (w - WR_OF_Q[k - 1]) / (WR_OF_Q[k] - WR_OF_Q[k - 1])); + return 9; +} + +const ease = (x) => x * x * (3 - 2 * x); + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 3, 3], d = cl(dt || 0, 0, 0.1); + S.hue = cl(kv[0], 0, 1); + S.speed = cl(kv[1], 0.2, 3); + S.lk = cl(round(kv[2]), 3, 6); + S.coil = cl(kv[3], 3, 6); + S.t = (S.t + (d * S.speed) / 2) % CYCLE; + const t = S.t; + const g = t < FLAT ? 0 : t < FLAT + UP ? ease((t - FLAT) / UP) : t < FLAT + UP + HOLD ? 1 : ease(1 - (t - FLAT - UP - HOLD) / DOWN); + S.q = qFor(S.coil * g); + shape(S, S.q); + S.wr = writhe(S.P); + ribbon(S); + // The view turns half a turn a cycle, slowly while the loop is flat so it + // is seen at a slant (it would not fit the panel face on), faster while + // it is coiled. + const ph = t / CYCLE; + S.yaw = (2.1 + PI * ph - 0.44 * sin(TAU * ph)) % TAU; +} + +function palette(S) { + const P = S.pal, h = S.hue; + hsv(wheel(0.65 + h), 0.75, 1, P, 0); // front face + hsv(wheel(0.86 + h), 0.8, 0.7, P, 3); // back face + hsv(wheel(1.05 + h), 0.6, 1, P, 6); // edge one + hsv(wheel(0.26 + h), 0.7, 1, P, 9); // edge two + hsv(wheel(0.65 + h), 0.6, 1, P, 12); // Tw bar, the front face's colour + hsv(wheel(0.1 + h), 0.12, 0.95, P, 15); // Wr bar + S.palHue = S.hue; +} + +// Fill a triangle with depth test, at LED centres. +function tri(S, x0, y0, z0, x1, y1, z1, x2, y2, z2, r, g, b) { + const area = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0); + if (abs(area) < 1e-6) return; + const { zb, rgb } = S; + const xa = max(0, floor(min(x0, x1, x2))), xb = min(W - 1, floor(max(x0, x1, x2))); + const ya = max(0, floor(min(y0, y1, y2))), yb = min(H - 1, floor(max(y0, y1, y2))); + for (let y = ya; y <= yb; y++) + for (let x = xa; x <= xb; x++) { + const px = x + 0.5, py = y + 0.5; + const w0 = ((x1 - px) * (y2 - py) - (x2 - px) * (y1 - py)) / area; + const w1 = ((x2 - px) * (y0 - py) - (x0 - px) * (y2 - py)) / area; + const w2 = 1 - w0 - w1; + if (w0 < -1e-4 || w1 < -1e-4 || w2 < -1e-4) continue; + const z = w0 * z0 + w1 * z1 + w2 * z2, p = y * W + x; + if (z <= zb[p]) continue; + zb[p] = z; + (rgb[p * 3] = r), (rgb[p * 3 + 1] = g), (rgb[p * 3 + 2] = b); + } +} + +// Nearer is brighter. +const shadeOf = (z) => 0.5 + 0.5 * cl((z + 46) / 92, 0, 1); + +export function draw(display, params) { + const S = params.s; + if (!S || !S.T) return; + if (S.palHue !== S.hue) palette(S); + const { P, U, T, sx, sy, sz, face, zb, rgb } = S, C = S.pal; + zb.fill(-1e9); + rgb.fill(0); + const cy = cos(S.yaw), sy0 = sin(S.yaw), el = 0.3, ce = cos(el), se = sin(el); + // Project: turn about the vertical, tip the top away by el, look along +y. + // k = 0 the edge at +HW U, k = 1 the one at -HW U. + const proj = (x, y, z, o) => { + const x1 = x * cy - y * sy0, y1 = x * sy0 + y * cy; + const y2 = y1 * ce + z * se, z2 = z * ce - y1 * se; + sx[o] = CX + x1; + sy[o] = CY - z2; + sz[o] = -y2; + }; + for (let i = 0; i < M; i++) { + const o = i * 3; + for (let k = 0; k < 2; k++) { + const s = k ? -HW : HW; + proj(P[o] + s * U[o], P[o + 1] + s * U[o + 1], P[o + 2] + s * U[o + 2], i * 2 + k); + } + // Which face looks at us: the normal T x U against the view. + const nx = T[o + 1] * U[o + 2] - T[o + 2] * U[o + 1], ny = T[o + 2] * U[o] - T[o] * U[o + 2], nz = T[o] * U[o + 1] - T[o + 1] * U[o]; + const y1 = nx * sy0 + ny * cy; + face[i] = -(y1 * ce + nz * se); + } + // Faces, as two triangles a segment. + for (let i = 0; i < M; i++) { + const j = (i + 1) % M, a = i * 2, b = j * 2, f = (face[i] + face[j]) / 2; + const o = f >= 0 ? 0 : 3, lit = (0.35 + 0.65 * min(1, abs(f) * 1.4)) * shadeOf((sz[a] + sz[b] + sz[a + 1] + sz[b + 1]) / 4); + const r = C[o] * lit, g = C[o + 1] * lit, bl = C[o + 2] * lit; + tri(S, sx[a], sy[a], sz[a], sx[b], sy[b], sz[b], sx[b + 1], sy[b + 1], sz[b + 1], r, g, bl); + tri(S, sx[a], sy[a], sz[a], sx[b + 1], sy[b + 1], sz[b + 1], sx[a + 1], sy[a + 1], sz[a + 1], r, g, bl); + } + // The two edges, a line each, a little in front of the faces they bound. + for (let k = 0; k < 2; k++) { + const o = 6 + k * 3; + for (let i = 0; i < M; i++) { + const a = i * 2 + k, b = ((i + 1) % M) * 2 + k, n = Math.ceil(hypot(sx[b] - sx[a], sy[b] - sy[a]) * 2.5) || 1; + for (let e = 0; e < n; e++) { + const u = e / n, x = sx[a] + (sx[b] - sx[a]) * u, y = sy[a] + (sy[b] - sy[a]) * u, z = sz[a] + (sz[b] - sz[a]) * u + 1.5; + const ix = floor(x), iy = floor(y); + if (ix < 0 || ix >= W || iy < 0 || iy >= H) continue; + const p = iy * W + ix; + if (z < zb[p]) continue; + zb[p] = z; + const l = 0.25 + 0.75 * shadeOf(z); + (rgb[p * 3] = C[o] * l), (rgb[p * 3 + 1] = C[o + 1] * l), (rgb[p * 3 + 2] = C[o + 2] * l); + } + } + } + // The bar: Wr from 0, then Tw from where Wr ends, which always stops at + // the mark at Lk. A negative Tw runs back from Wr to Lk, striped. + const X0 = 6, UNIT = 8, row = (y, a, b, o, stripe) => { + const lo = min(a, b), hi = max(a, b); + for (let x = floor(X0 + lo * UNIT); x < X0 + hi * UNIT; x++) { + if (x < 0 || x >= W) continue; + const cover = min(1, x + 1 - (X0 + lo * UNIT), X0 + hi * UNIT - x); + if (cover <= 0) continue; + const l = cover * (stripe && (x & 1) ? 0.45 : 1); + for (let e = 0; e < 3; e++) rgb[(y * W + x) * 3 + e] = C[o + e] * l; + } + }; + for (const y of [119, 120]) row(y, 0, S.wr, 15, false); + for (const y of [122, 123]) row(y, S.wr, S.wr + S.tw, 12, S.tw < 0); + for (let v = 0; v <= 7; v++) { + const x = X0 + v * UNIT; + if (x >= W) break; + rgb[(125 * W + x) * 3] = rgb[(125 * W + x) * 3 + 1] = rgb[(125 * W + x) * 3 + 2] = 0.3; + } + // The mark at Lk in the two edges' colours: it counts how often they link. + const lx = X0 + S.lk * UNIT; + for (let y = 118; y <= 126; y++) { + const o = y < 122 ? 6 : 9; + for (let e = 0; e < 3; e++) rgb[(y * W + lx) * 3 + e] = C[o + e]; + } + for (let p = 0; p < NPX; p++) display.setPixel(p % W, (p / W) | 0, (255 * min(1, rgb[p * 3])) | 0, (255 * min(1, rgb[p * 3 + 1])) | 0, (255 * min(1, rgb[p * 3 + 2])) | 0); +} From 24b8a79fe293257278bddf75555377de54fbca2b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 06:24:56 +0000 Subject: [PATCH 05/10] =?UTF-8?q?56=ED=8C=90:=20Golomb=20Trominoes,=20Andr?= =?UTF-8?q?ews=20Curves?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pattern-study/patterns/andrews-curves.js | 228 +++++++++++++++++ .../patterns/golomb-trominoes.js | 233 ++++++++++++++++++ 2 files changed, 461 insertions(+) create mode 100644 incoming/pattern-study/patterns/andrews-curves.js create mode 100644 incoming/pattern-study/patterns/golomb-trominoes.js diff --git a/incoming/pattern-study/patterns/andrews-curves.js b/incoming/pattern-study/patterns/andrews-curves.js new file mode 100644 index 000000000..b4be17a7e --- /dev/null +++ b/incoming/pattern-study/patterns/andrews-curves.js @@ -0,0 +1,228 @@ +// ===== Patternflow pattern ===== +// Title: Andrews Curves +// Family: data +// Idea: Forty synthetic points in five dimensions, three Gaussian clusters of 13 and one outlier, each become Andrews' curve f(t) = x1/sqrt 2 + x2 sin t + x3 cos t + x4 sin 2t + x5 cos 2t with t running down the panel, so the clusters hang as three braided bundles and the outlier strays, and as a grand tour turns the data the bundles twist past each other while the shaded gap between the outlier's white curve and its neighbour's keeps one root mean square, the fixed bar along the bottom. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Andrews plots (David F. Andrews, 1972), turned by the grand tour (Daniel Asimov, 1985), pattern round 56 +// Round: 56 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Spread=0.12..0.6, Dims=5..9 +// +// Knob 1: Hue. Turns the three clusters round the wheel together. At 0 they are cyan, violet and green; at 0.25 indigo, rose and cyan; at 0.5 rose, green and blue. The marked pair stays white with a grey gap, and the wheel skips orange and yellow, which read brown and olive on the faint edges of the curves. +// Knob 2: Speed. How fast the grand tour turns. At 2 each of the ten coordinate planes of five dimensions turns at its own rate between 0.10 and 0.20 radians a second, the rates linearly independent so the tour never repeats; 0.2 is ten times slower and 3 half as fast again. +// Knob 3: Spread. Each cluster's standard deviation in every coordinate, as a share of R, the distance from the middle to a cluster centre; the centres stand R sqrt 3 = 1.73 R apart and the outlier 1.72 R from each. At 0.12 a point lies on average 0.26 R from its centre in five dimensions and the bundles are tight ropes; at 0.6 it lies 1.3 R off, the clusters overlap and the curves spread into one tangle. +// Knob 4: Dims. The data's dimension, 5 to 9; each two coordinates past the first add a harmonic, so the curves run to cos 2t at 5, cos 3t at 7 and cos 4t at 9 and the braids wind tighter. The clusters differ only in the first five coordinates, the rest is spread, and the gap's bar is 14.6 LEDs at 5, 12.6 at 7 and 11.3 at 9, held to three decimals through a minute of touring at each (the axis narrows as the dimension grows). + +const W = 64, H = 128, NP = W * H; +const { floor, min, max, abs, sqrt, sin, cos, exp, round, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// The data: three synthetic Gaussian clusters of PER points each in up to +// DMAX dimensions, and one outlier. Cluster centres sit R from the middle, +// 120 degrees apart, so any two are R sqrt 3 apart. +const DMAX = 9, PER = 13, NC = 3, N = NC * PER + 1, R = 1; +// Tour radians a second at Speed 1. +const RATE = 0.11; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// Dim orange and yellow read brown and olive, so the wheel skips them. +const wheel = (h) => 0.18 + 0.85 * (h - 0.18 - floor(h - 0.18)); + +function gauss() { + const u = Math.random() || 1e-9, v = Math.random(); + return sqrt(-2 * Math.log(u)) * cos(2 * PI * v); +} + +export function setup(params) { + // Two orthonormal directions in the first five coordinates carry the + // triangle of centres; a third, square to both, carries the outlier. + const basis = []; + for (let k = 0; k < 3; k++) { + const v = new Float64Array(DMAX); + for (let i = 0; i < 5; i++) v[i] = gauss(); + for (const b of basis) { + let d = 0; + for (let i = 0; i < DMAX; i++) d += v[i] * b[i]; + for (let i = 0; i < DMAX; i++) v[i] -= d * b[i]; + } + let n = 0; + for (let i = 0; i < DMAX; i++) n += v[i] * v[i]; + for (let i = 0; i < DMAX; i++) v[i] /= sqrt(n); + basis.push(v); + } + const centre = [], z = []; + for (let c = 0; c < NC; c++) { + const a = (2 * PI * c) / NC, v = new Float64Array(DMAX); + for (let i = 0; i < DMAX; i++) v[i] = R * (cos(a) * basis[0][i] + sin(a) * basis[1][i]); + centre.push(v); + } + // Each point keeps its own standard normal draw; Spread scales it. + for (let p = 0; p < N; p++) { + const v = new Float64Array(DMAX); + for (let i = 0; i < DMAX; i++) v[i] = gauss(); + z.push(v); + } + // Asimov's torus method: every coordinate plane turns at its own rate, + // the rates linearly independent over the rationals (fractional parts of + // square roots of primes), so the product of turns wanders densely + // through all rotations. + const primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151]; + const rate = []; + for (let i = 0, k = 0; i < DMAX; i++) for (let j = i + 1; j < DMAX; j++, k++) rate.push([i, j, 0.3 + 0.7 * (sqrt(primes[k]) % 1)]); + // Sines and cosines of each row's t, for the harmonics. + const trig = new Float64Array(H * DMAX); + for (let r = 0; r < H; r++) { + const t = -PI + (2 * PI * (r + 0.5)) / H; + trig[r * DMAX] = 1 / Math.SQRT2; + for (let i = 1; i < DMAX; i++) { + const k = (i + 1) >> 1; + trig[r * DMAX + i] = i & 1 ? sin(k * t) : cos(k * t); + } + } + // The outlier stands 1.4 R from the middle, square to the centres' plane, + // and is marked together with the member of the first cluster nearest to + // it at the opening Spread. + const outlier = basis[2].map((x) => 1.4 * R * x); + let near = 0, best = 1e9; + for (let p = 0; p < PER; p++) { + let d2 = 0; + for (let i = 0; i < 5; i++) d2 += (centre[0][i] + 0.12 * z[p][i] - outlier[i]) ** 2; + if (d2 < best) (best = d2), (near = p); + } + params.s = { + centre, z, outlier, rate, trig, tau: 0, + hue: 0, speed: 2, spread: 0.12, d: 5, + Q: new Float64Array(DMAX * DMAX), x: new Float64Array(N * DMAX), y: new Float64Array(N * DMAX), + f: new Float32Array(N * H), acc: new Float32Array(NP * 3), col: new Float32Array(NC * 3 + 3), pair: [near, N - 1], + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0.12, 5], d = cl(dt || 0, 0, 0.1); + S.hue = cl(kv[0], 0, 1); + S.speed = cl(kv[1], 0.2, 3); + S.spread = cl(kv[2], 0.12, 0.6); + S.d = cl(round(kv[3]), 5, DMAX); + S.tau += d * S.speed * RATE; +} + +// The tour's rotation of the first D coordinates at time tau: the turns of +// every plane among them, taken in a fixed order. +function tour(S, D) { + const Q = S.Q; + Q.fill(0); + for (let i = 0; i < D; i++) Q[i * DMAX + i] = 1; + for (const [i, j, w] of S.rate) { + if (j >= D) continue; + const a = w * S.tau, c = cos(a), s = sin(a); + for (let k = 0; k < D; k++) { + const qi = Q[i * DMAX + k], qj = Q[j * DMAX + k]; + Q[i * DMAX + k] = c * qi - s * qj; + Q[j * DMAX + k] = s * qi + c * qj; + } + } +} + +// Splat a point onto its four nearest LEDs, adding colour c scaled by a. +function splat(acc, x, y, c, o, a) { + const fx = x - 0.5, fy = y - 0.5, ix = floor(fx), iy = floor(fy), tx = fx - ix, ty = fy - iy; + for (let dy = 0; dy < 2; dy++) { + const yy = iy + dy; + if (yy < 0 || yy >= H) continue; + const wy = dy ? ty : 1 - ty; + for (let dx = 0; dx < 2; dx++) { + const xx = ix + dx; + if (xx < 0 || xx >= W) continue; + const w = (dx ? tx : 1 - tx) * wy * a, p = (yy * W + xx) * 3; + acc[p] += c[o] * w; + acc[p + 1] += c[o + 1] * w; + acc[p + 2] += c[o + 2] * w; + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const D = S.d, { x, y, f, acc, trig, Q, col } = S; + // The observations in their own coordinates. + for (let p = 0; p < N; p++) + for (let i = 0; i < DMAX; i++) { + const base = p < NC * PER ? S.centre[floor(p / PER)][i] + S.spread * S.z[p][i] : S.outlier[i]; + x[p * DMAX + i] = i < D ? base : 0; + } + tour(S, D); + for (let p = 0; p < N; p++) + for (let i = 0; i < D; i++) { + let v = 0; + for (let k = 0; k < D; k++) v += Q[i * DMAX + k] * x[p * DMAX + k]; + y[p * DMAX + i] = v; + } + // Andrews' curves: f(t) = y1 / sqrt 2 + y2 sin t + y3 cos t + y4 sin 2t + ... + // By Cauchy-Schwarz |f| is at most |y| sqrt(D / 2), and a turn keeps |y|, + // so the axis is fixed for each D: R sqrt(D / 2), the most a centre's + // curve can reach, is 22 LEDs either side of the middle. + const sc = 22 / (R * sqrt(D / 2)); + for (let p = 0; p < N; p++) + for (let r = 0; r < H; r++) { + let v = 0; + for (let i = 0; i < D; i++) v += y[p * DMAX + i] * trig[r * DMAX + i]; + f[p * H + r] = W / 2 + v * sc; + } + // Cyan, violet and green at Hue 0. + for (let c = 0; c < NC; c++) hsv(wheel(0.58 + S.hue + [0, 0.27, 0.73][c]), 0.8, 1, col, c * 3); + col[9] = col[10] = col[11] = 1; + acc.fill(0); + const [pa, pb] = S.pair; + // The gap between the two marked curves, shaded faintly. + for (let r = 0; r < H; r++) { + const a = f[pa * H + r], b = f[pb * H + r], lo = max(0, Math.ceil(min(a, b))), hi = min(W - 1, floor(max(a, b))); + for (let c = lo; c <= hi; c++) { + const o = (r * W + c) * 3; + acc[o] += 0.1; + acc[o + 1] += 0.1; + acc[o + 2] += 0.12; + } + } + for (let p = 0; p < N; p++) { + const ci = p < NC * PER ? floor(p / PER) * 3 : 9, mark = p === pa || p === pb; + const a = mark ? 0.7 : 0.2; + for (let r = 0; r + 1 < H; r++) { + const xa = f[p * H + r], xb = f[p * H + r + 1], len = Math.hypot(xb - xa, 1), m = Math.ceil(len * 3); + for (let e = 0; e < m; e++) splat(acc, xa + ((xb - xa) * e) / m, r + 0.5 + e / m, col, mark ? 9 : ci, ((a * len) / m) * 3); + } + } + // Parseval: the mean of (f_x - f_y)^2 over t is |x - y|^2 / 2, and a + // turn keeps |x - y|, so the gap's root mean square over the 128 rows + // (exact for these few harmonics) never moves while the curves do. It is + // measured from the drawn curves each frame and laid as a bar, that many + // LEDs long, across the bottom two rows with a dark row above. + let ss = 0; + for (let r = 0; r < H; r++) ss += (f[pa * H + r] - f[pb * H + r]) ** 2; + const half = sqrt(ss / H) / 2; + for (let c = 0; c < W; c++) { + const e = half - abs(c + 0.5 - W / 2); + if (e < -2) continue; + for (let r = H - 3; r < H; r++) { + const o = (r * W + c) * 3; + acc[o] = acc[o + 1] = acc[o + 2] = r === H - 3 ? 0 : cl(e + 0.5, 0, 1); + } + } + // Where curves pile up the light saturates on the brightest channel and + // the others follow in proportion, so a dense bundle glows in its own + // colour instead of washing to white. + for (let p = 0, o = 0; p < NP; p++, o += 3) { + const m = max(acc[o], acc[o + 1], acc[o + 2]), k = m > 0 ? (255 * (1 - exp(-2.2 * m))) / m : 0; + display.setPixel(p % W, (p / W) | 0, (acc[o] * k) | 0, (acc[o + 1] * k) | 0, (acc[o + 2] * k) | 0); + } +} diff --git a/incoming/pattern-study/patterns/golomb-trominoes.js b/incoming/pattern-study/patterns/golomb-trominoes.js new file mode 100644 index 000000000..76c89ec11 --- /dev/null +++ b/incoming/pattern-study/patterns/golomb-trominoes.js @@ -0,0 +1,233 @@ +// ===== Patternflow pattern ===== +// Title: Golomb Trominoes +// Family: cells +// Idea: Two 64 x 64 boards, each short one cell, are tiled with 3-LED L trominoes exactly as Golomb's proof lays them, and as each board's blinking hole walks one LED at a time the trominoes between its old and new place re-lay in a white flash, one tromino inside a 2 x 2 block but up to 63 when it crosses the board's middle, while the field's hue marks the nested L-shaped shells of the proof closing in on the hole. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: tromino tiling of the deficient 2^n board (Solomon W. Golomb, 1954), pattern round 56 +// Round: 56 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Lift=0..4, Cell=1..4 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the 2 x 2 trominoes are azure and navy in the outermost shell and turn cyan, aqua, green, lime and yellow shell by shell towards the hole; the 4 x 4 trominoes are plum, 8 x 8 gold, 16 x 16 lime and 32 x 32 rose, and the trominoes the proof lays round the hole, one a level, are white. At 0.5 the shells run orange, red, pink and violet and the levels aqua, blue and violet. The dim colours skip the orange and yellow part of the wheel, which reads brown and olive. +// Knob 2: Speed. The hole walks 5 LEDs a second for each unit, 10 at the opening 2, on a straight line that reflects off the board's edges. A step that stays inside a 2 x 2 block re-lays that one tromino; a step over the line between two blocks of side 2^k re-lays at most 2^k - 1 trominoes (3, 7, 15, 31, 63) and about two thirds of that on average. At 2 the two holes cross their board's middle at 1.4 s and 1.2 s and then on average every 6.3 s. 0.2 is ten times slower. +// Knob 3: Lift. Leaves the lowest levels of the proof unlaid. At 0 every tromino is laid; 1 to 4 leave whole blocks of 4, 8, 16 and 32 cells a side (on a 16-cell board no more than 8), each one square in its tone short the one cell that the trominoes above it, or the hole, took. Each such square is the proof's induction hypothesis: a smaller board short one cell, which the same proof tiles. +// Knob 4: Cell. LEDs to a cell: 1 below 1.5, 2 below 3.5, then 4, so each half of the panel is a board of 64, 32 or 16 cells a side, laid with 1365, 341 or 85 trominoes. The hole still walks 10 LEDs a second at Speed 2, so on the bigger cells it takes fewer, larger steps. + +// Each half of the panel is one square board, W LEDs a side. +const W = 64, H = 128; +const { floor, min, max, exp, round, cos, sin } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// Three colours are enough for Golomb's tiling, and they come from the +// proof itself. The 2 x 2 trominoes fill three quarters of the board in +// two tones: bright where the block is missing its top left or bottom +// right cell, dark where it is missing the other two. Two trominoes of the +// same tone never share an edge, and no two trominoes laid at a bigger +// level ever touch each other, so those all take the third colour, with +// its hue by the level that laid them. Checked for every hole of boards +// 16, 32 and 64 cells wide: the rule breaks on at most four edges, all +// within two cells of the hole. +// The field's hue says how far the cell sits from the hole in the proof: +// its shell is the smallest block holding both, and shell k is the three +// quarters of the hole's 2^k block that the hole is not in: an L tromino +// scaled up 2^(k-1) times. Outermost shell azure, then cyan, aqua, green, +// lime and yellow inwards. +const SHUE = [0.6, 0.5, 0.42, 0.34, 0.27, 0.2]; +// Bigger levels: plum for 4 x 4, gold for 8 x 8, lime, rose. The board's +// middle tromino always lies round the hole and shows white. +const LHUE = [0, 0, 0.98, 0.13, 0.3, 0.98, 0.75]; +// Palette slots: 2 (n - shell) + tone for the field (0 bright, 1 dark), +// 12 + level for levels 2..6, 19 for the trominoes the proof lays round +// the hole and 20 for the hole. +// The block sides Lift leaves unlaid: 1 lays every tromino; a 2 x 2 block +// short one cell is a tromino already, so the next is 4. +const LIFT = [0, 2, 3, 4, 5]; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} +// Dim orange and yellow read brown and olive, so the wheel skips them. +const wheel = (h) => 0.18 + 0.85 * (h - 0.18 - floor(h - 0.18)); + +function makeBoard(y0, u, v, ang) { + const M = W * W; + return { + y0, u, v, dx: cos(ang), dy: sin(ang), hx: -1, hy: -1, + lev: new Int8Array(M), ori: new Int8Array(M), lev2: new Int8Array(M), ori2: new Int8Array(M), + bori: Array.from({ length: 7 }, () => new Int8Array(M)), + rgb: new Float32Array(M * 3), flash: new Float32Array(M), fresh: true, + }; +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, lift: 0, c: 0, n: 6, S: 64, acc: 0, t: 0, + // Two boards, one on each half of the panel, their holes set off on + // straight lines that cross the boards' middles early. + boards: [makeBoard(0, 0.37, 0.29, 0.56), makeBoard(H - W, 0.71, 0.4, 2.2)], + pal: new Float32Array(63), palHue: -1, + }; +} + +// Golomb's proof as a program: in a block with one cell taken, lay one +// tromino over the inner corners of the three quarters that do not hold +// it, and each of those quarters now has a cell taken too. Every cell +// records the level of the tromino that covers it (the block's side is +// 2^level) and which corner of the 2 x 2 at the block's centre that +// tromino leaves out. +function lay(B, n, hx, hy) { + const S = 1 << n, lev = B.lev2, ori = B.ori2; + lev[hy * S + hx] = 0; + ori[hy * S + hx] = -1; + const rec = (x0, y0, L, hx, hy) => { + if (!L) return; + const h = 1 << (L - 1), cx = x0 + h, cy = y0 + h, q = (hx >= cx ? 1 : 0) + (hy >= cy ? 2 : 0); + B.bori[L][(y0 >> L) * (S >> L) + (x0 >> L)] = q; + for (let k = 0; k < 4; k++) { + const ix = cx - 1 + (k & 1), iy = cy - 1 + (k >> 1), qx = x0 + (k & 1 ? h : 0), qy = y0 + (k & 2 ? h : 0); + if (k === q) rec(qx, qy, L - 1, hx, hy); + else { + lev[iy * S + ix] = L; + ori[iy * S + ix] = q; + rec(qx, qy, L - 1, ix, iy); + } + } + }; + rec(0, 0, n, hx, hy); +} + +// Shell of a cell: the side, as a level, of the smallest block holding both +// it and the hole. +const shellOf = (x, y, hx, hy) => 32 - Math.clz32((x ^ hx) | (y ^ hy)); + +// Move the hole one cell along its line: whichever of the two grid lines +// ahead it reaches first. At the board's edge the line reflects. +function step(B, S) { + for (let guard = 0; guard < 4; guard++) { + const px = B.u * S, py = B.v * S, cx = floor(px), cy = floor(py); + const tx = B.dx > 0 ? (cx + 1 - px) / B.dx : B.dx < 0 ? (px - cx) / -B.dx : 1e9; + const ty = B.dy > 0 ? (cy + 1 - py) / B.dy : B.dy < 0 ? (py - cy) / -B.dy : 1e9; + if (tx <= ty) { + const nx = cx + (B.dx > 0 ? 1 : -1); + if (nx < 0 || nx >= S) { + B.dx = -B.dx; + continue; + } + B.u = (nx + (B.dx > 0 ? 1e-6 : 1 - 1e-6)) / S; + B.v = (py + B.dy * tx) / S; + } else { + const ny = cy + (B.dy > 0 ? 1 : -1); + if (ny < 0 || ny >= S) { + B.dy = -B.dy; + continue; + } + B.v = (ny + (B.dy > 0 ? 1e-6 : 1 - 1e-6)) / S; + B.u = (px + B.dx * ty) / S; + } + return; + } +} + +// Re-lay the board for the hole's cell; every cell whose tromino changed +// flashes. +function relay(S, B, n) { + const N = 1 << n, hx = cl(floor(B.u * N), 0, N - 1), hy = cl(floor(B.v * N), 0, N - 1); + if (hx === B.hx && hy === B.hy && !B.fresh) return; + lay(B, n, hx, hy); + const M = N * N; + for (let p = 0; p < M; p++) { + if (!B.fresh && (B.lev[p] !== B.lev2[p] || B.ori[p] !== B.ori2[p])) B.flash[p] = 1; + B.lev[p] = B.lev2[p]; + B.ori[p] = B.ori2[p]; + } + if (B.fresh) B.flash.fill(0), (B.snap = true); + B.fresh = false; + B.hx = hx; + B.hy = hy; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 1], d = cl(dt || 0, 0, 0.1); + S.hue = cl(kv[0], 0, 1); + S.speed = cl(kv[1], 0.2, 3); + const c = [1, 1, 2, 2, 4][cl(round(kv[3]), 1, 4)]; + if (c !== S.c) { + S.c = c; + S.n = 6 - (c === 1 ? 0 : c === 2 ? 1 : 2); + S.S = 64 / c; + for (const B of S.boards) B.fresh = true; + } + const n = S.n, N = S.S; + S.lift = min(LIFT[cl(round(kv[2]), 0, 4)], n - 1); + // The hole walks five LEDs a second for each unit of Speed, whatever the + // cell size. + S.t += d; + S.acc += (d * S.speed * 5) / c; + let steps = floor(S.acc); + S.acc -= steps; + for (const B of S.boards) { + relay(S, B, n); + for (let k = 0; k < steps; k++) { + step(B, N); + relay(S, B, n); + } + // Each cell eases to its slot's colour in about an eighth of a second, + // so a shell that changes hue dissolves into the next; flashes fade. + if (S.palHue !== S.hue) palette(S); + const P = S.pal, u = S.lift, fv = B.snap ? 1 : min(1, d * 8), ff = exp(-d / 0.25), rgb = B.rgb; + B.snap = false; + for (let y = 0, p = 0; y < N; y++) + for (let x = 0; x < N; x++, p++) { + const o = B.ori[p], L = B.lev[p]; + const sh = shellOf(x, y, B.hx, B.hy); + let k; + if (o < 0) (k = 20), (B.flash[p] = 0); + else if (L === sh && L > u) k = 19; + else if (L > max(u, 1)) k = 12 + L; + else { + // Below the lift a cell takes the tone of its whole unlaid block. + const q = u ? B.bori[u][(y >> u) * (N >> u) + (x >> u)] : o; + k = 2 * (n - sh) + (q === 0 || q === 3 ? 0 : 1); + } + for (let e = 0; e < 3; e++) rgb[p * 3 + e] += (P[k * 3 + e] - rgb[p * 3 + e]) * fv; + B.flash[p] *= ff; + } + } +} + +function palette(S) { + const P = S.pal, h = S.hue; + for (let k = 0; k < 6; k++) { + hsv(SHUE[k] + h, 0.72, 1, P, 6 * k); + hsv(wheel(SHUE[k] + 0.05 + h), 0.9, 0.27, P, 6 * k + 3); + } + // The 4 x 4 trominoes are half bright, so their hue skips orange too. + for (let L = 2; L <= 6; L++) hsv(L === 2 ? wheel(LHUE[2] + h) : LHUE[L] + h, 0.75, L === 2 ? 0.62 : 0.95, P, (12 + L) * 3); + hsv(0.6 + h, 0.04, 1, P, 57); + P[60] = P[61] = P[62] = 0; + S.palHue = h; +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.c) return; + const c = S.c, N = S.S, blink = S.t * 3 - floor(S.t * 3) < 0.5; + for (const B of S.boards) { + for (let y = 0; y < W; y++) { + const cy = floor(y / c); + for (let x = 0; x < W; x++) { + const p = cy * N + floor(x / c), o = p * 3, r = B.rgb; + // The hole blinks white three times a second. + const f = B.ori[p] < 0 ? (blink ? 1 : 0) : B.flash[p] * 0.8; + display.setPixel(x, B.y0 + y, (255 * (r[o] + (1 - r[o]) * f)) | 0, (255 * (r[o + 1] + (1 - r[o + 1]) * f)) | 0, (255 * (r[o + 2] + (1 - r[o + 2]) * f)) | 0); + } + } + } +} From 246ce2cda4a374d2fbf4417eb3b41f69edec539d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 06:37:03 +0000 Subject: [PATCH 06/10] =?UTF-8?q?56=ED=8C=90:=20Tree=20Peano=20Curve,=20Mu?= =?UTF-8?q?ltiplicative=20Cascade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UUXdHR3tRiDEQfEfGbQxb9 --- .../patterns/multiplicative-cascade.js | 157 +++++++++ .../patterns/tree-peano-curve.js | 324 ++++++++++++++++++ 2 files changed, 481 insertions(+) create mode 100644 incoming/pattern-study/patterns/multiplicative-cascade.js create mode 100644 incoming/pattern-study/patterns/tree-peano-curve.js diff --git a/incoming/pattern-study/patterns/multiplicative-cascade.js b/incoming/pattern-study/patterns/multiplicative-cascade.js new file mode 100644 index 000000000..7bd0b66e9 --- /dev/null +++ b/incoming/pattern-study/patterns/multiplicative-cascade.js @@ -0,0 +1,157 @@ +// ===== Patternflow pattern ===== +// Title: Multiplicative Cascade +// Family: fractal +// Idea: The panel's mass is halved thirteen times, across and down in turn, down to single LEDs, each half taking a random log-normal weight of mean 1, and every LED shows the log of its own share as light and the share of its 8 x 8 block as hue, so a patchwork of mint and violet rectangles of every size glitters at the LED level while the weights turn over level by level from the two halves down to the single LEDs and mass flows from half to half at each scale; Spread pushes the weights across the Kahane-Peyrière threshold, past which the total mass drains away and what is left gathers into a few sparks. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: multiplicative cascade (Benoit Mandelbrot, 1974), with the degeneracy threshold of Jean-Pierre Kahane and Jacques Peyrière (1976), pattern round 56 +// Round: 56 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Spread=0.4..1.6, Couple=-1..1 +// +// Knob 1: Hue. Turns the colours round the wheel. At 0 heavy 8 x 8 blocks are mint, middling ones blue and light ones violet, with the heaviest LEDs near white; at 0.25 heavy blocks turn blue and light ones red; at 0.5 heavy ones pink and light ones green; at 0.75 heavy ones yellow and light ones azure. Dim orange and yellow are lifted by up to half so they do not read brown. +// Knob 2: Speed. The weights turn over one level at a time, from the panel's two halves down to the single LEDs. At 2 a level takes 1.2 s to turn from its old draws to new ones, the next level starts 0.4 s later, and the sweep comes round every 6.4 s. At 0.2 everything is ten times slower, at 3 half as fast again. +// Knob 3: Spread. tau = E[W log2 W] for each weight W = exp(sigma G - sigma^2 / 2), from 0.4 to 1.6 (sigma 0.74 to 1.49). Kahane and Peyrière's threshold is tau = 1: below it the mass survives every halving, at and above it the total dies away. Medians of 2,000 cascades: at 0.4 the total mass is 0.64 after 7 halvings and 0.61 after 13, 98% of the LEDs show above the black floor and the heaviest 1% hold half the mass; at 1 the total falls from 0.24 to 0.15, half the LEDs show and the top 1% hold 86%; at 1.6 it falls from 0.057 to 0.019, 16% show and the top 1% hold 95%. The floor stays at 1/8100 of an even share, so lost mass shows as dark. +// Knob 4: Couple. The correlation of the two halves' log weights at every split, -1 to 1. At -1 one half gains in log exactly what the other loses, so mass trades across every split line and each split shows a hard step; at the opening 0.06 the halves are close to independent, Mandelbrot's canonical cascade; at 1 both halves of a split take the same weight, no split line has a step of its own and the patches grow larger and softer. Each weight keeps its own law, so the threshold stays at tau = 1 (John Biggins, 1977), though after 13 halvings at Spread 1 the median total mass is 0.25 at -1, 0.15 at 0 and 0.08 at 1. + +const W = 64, H = 128, N = W * H; +// Thirteen halvings, across and down in turn, take the panel to single +// LEDs. Node 1 is the panel, nodes 2n and 2n + 1 the halves of node n, and +// nodes 8192..16383 the LEDs. +const L = 13, NODES = 1 << (L + 1); +const { floor, min, max, sqrt, cos, sin, log, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// The window of log mass shown, in units of the natural log of mass times +// 8192: 0 is an LED's even share of the panel's mass, LO is shown black and +// HI white. It does not move with the knobs, so lost mass shows as dark. +const LO = -9, HI = 2; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +function gauss() { + let u = 0; + while (u === 0) u = Math.random(); + return sqrt(-2 * log(u)) * cos(2 * PI * Math.random()); +} + +// Which LED each leaf is: bit k of the path from the panel down picks the +// upper or lower half for odd k and the left or right half for even k. +const LED = new Int16Array(N); +for (let n = 0; n < N; n++) { + let x = 0, y = 0; + for (let k = 1; k <= L; k++) { + const b = (n >> (L - k)) & 1; + if (k & 1) y = y * 2 + b; + else x = x * 2 + b; + } + LED[n] = y * W + x; +} + +// The weights change one level at a time, coarse to fine: level k starts +// k - 1 steps after the panel's own halves, takes SPAN steps to turn from +// its old draws to new ones, and the sweep comes round every CYCLE steps. +const SPAN = 3, CYCLE = 16, STEPS = 2.5; +// Steps of block weight in the colour table. +const HB = 16; + +export function setup(params) { + const S = { + hue: -1, speed: 2, tau: 0.4, rho: 0, phi: 0, cyc: new Int32Array(L + 1).fill(-1), + a: new Float32Array(NODES), b: new Float32Array(NODES), + acc: new Float32Array(NODES), g: new Float32Array(NODES), pal: new Float32Array(HB * 256 * 3), + }; + // Each weight's normal G turns from one draw a to the next b along a + // quarter circle, cos(t) a + sin(t) b, which is again standard normal at + // every moment, so the cascade's law holds while it moves. + for (let n = 2; n < NODES; n++) { + S.a[n] = gauss(); + S.b[n] = gauss(); + } + params.s = S; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0.4, 0.06], d = cl(dt || 0, 0, 0.1); + S.speed = cl(kv[1], 0.2, 3); + S.tau = cl(kv[2], 0.4, 1.6); + S.rho = cl(kv[3], -1, 1); + if (kv[0] !== S.hue) palette(S, cl(kv[0], 0, 1)); + const { a, b, g: G, acc } = S; + S.phi += (d * S.speed * STEPS) / 2; + for (let k = 1; k <= L; k++) { + const sk = S.phi - (k - 1), c = floor(sk / CYCLE), u = sk - c * CYCLE; + if (c !== S.cyc[k]) { + // A new turn for this level: the old target is the new start. + S.cyc[k] = c; + for (let n = 1 << k; n < 2 << k; n++) { + a[n] = b[n]; + b[n] = gauss(); + } + } + const p = cl(u / SPAN, 0, 1), t = (PI / 2) * p * p * (3 - 2 * p), ct = cos(t), st = sin(t); + for (let n = 1 << k; n < 2 << k; n++) G[n] = ct * a[n] + st * b[n]; + } + // Spread is tau = E[W log2 W]; for W = exp(sigma G - sigma^2 / 2) that is + // sigma^2 / (2 ln 2). + const sig = sqrt(2 * Math.LN2 * S.tau), half = (sig * sig) / 2; + const r = S.rho, r2 = sqrt(1 - r * r); + // The first half takes its own normal; the second takes rho times its + // sibling's plus its own, so both are standard normal and their + // correlation is rho. Each half's log weight adds to its parent's log mass. + acc[1] = 0; + for (let n = 2; n < NODES; n += 2) { + const g1 = G[n], g2 = r * g1 + r2 * G[n + 1], up = acc[n >> 1] - half; + acc[n] = up + sig * g1; + acc[n + 1] = up + sig * g2; + } +} + +// Colour by two depths of the same cascade: the hue says how heavy the +// LED's 8 x 8 block (seven halvings down) is against a typical block at +// this Spread, the light says how heavy the LED itself is against an even +// share. Rows of the table are HB steps of block weight, columns 256 steps +// of LED weight. +function palette(S, hue) { + S.hue = hue; + for (let j = 0; j < HB; j++) { + const z = (2 * j) / (HB - 1) - 1; + for (let i = 0; i < 256; i++) { + const v = i / 255; + const h = 0.62 + hue - 0.2 * z - 0.05 * v, hh = h - floor(h); + // Orange and yellow are lifted so that dim ones do not read brown, and + // deep blue is paled a little, since it reads darkest on the LEDs. + const warm = max(0, cos(2 * PI * (hh - 0.11))) ** 2, blue = max(0, cos(2 * PI * (hh - 0.67))); + const sat = 0.95 - 0.12 * blue - 0.7 * v ** 5; + hsv(h, sat, i ? min(1, (0.08 + 0.92 * v) * (1 + 0.5 * warm)) : 0, S.pal, (j * 256 + i) * 3); + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { acc, pal } = S, k = 255 / (HI - LO); + // A block seven halvings down carries seven weights; its log weight + // against an even share has mean -7 sigma^2 / 2 and spread sigma sqrt 7. + const sig = sqrt(2 * Math.LN2 * S.tau), m7 = (-7 * sig * sig) / 2, s7 = 1 / (sig * sqrt(7)); + for (let n = 0; n < N; n++) { + const p = LED[n], x = p & 63, y = p >> 6; + const v = acc[N + n] - LO; + if (v <= 0) { + display.setPixel(x, y, 0, 0, 0); + continue; + } + const z = Math.tanh((acc[(N + n) >> 6] - m7) * s7 * 0.8); + const c = ((((z + 1) * (HB - 1)) / 2 + 0.5) | 0) * 768 + min(255, (v * k) | 0) * 3; + display.setPixel(x, y, (255 * pal[c]) | 0, (255 * pal[c + 1]) | 0, (255 * pal[c + 2]) | 0); + } +} diff --git a/incoming/pattern-study/patterns/tree-peano-curve.js b/incoming/pattern-study/patterns/tree-peano-curve.js new file mode 100644 index 000000000..592d0af8d --- /dev/null +++ b/incoming/pattern-study/patterns/tree-peano-curve.js @@ -0,0 +1,324 @@ +// ===== Patternflow pattern ===== +// Title: Tree Peano Curve +// Family: line +// Idea: A uniform spanning tree of the panel's 32 x 64 blocks of 2 x 2 LEDs grows by Wilson's loop-erased random walks, and its Peano curve, which winds round the tree through every one of the 8192 LEDs once on its way from the bottom left corner to the top left, is coloured green to magenta in the order it passes them, so its space-filling sweep up the panel shows as fractal fronts while pale fronts and short runners flow along it, and every 8 seconds a new tree is grown and its curve traced in. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: uniform spanning tree by loop-erased random walks (David Wilson, 1996), its Peano curve converges to SLE(8) (Gregory Lawler, Oded Schramm and Wendelin Werner, 2004), pattern round 56 +// Round: 56 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Bands=1..8, Stretch=-1..1 +// +// Knob 1: Hue. Turns the curve's colours round the wheel. At 0 the curve runs from green at the bottom left corner through teal and blue to magenta at the top left; at 0.25 from azure through violet and red to orange; at 0.5 from violet through red and yellow to teal; at 0.75 from red through yellow and green to azure. Orange and yellow are lifted most of the way to full light between fronts, so they never sink to brown. +// Knob 2: Speed. At 2 each tree lasts 8 s: colour flows over its curve for 3.6 s, Wilson's walks grow the next tree in 2 s (about 5,600 steps of 840 walks, the first slowly), and the next curve is traced in from the corner in 2.4 s. Fronts run 1,024 LEDs a second along the curve and the 51 runners 40. At 0.2 everything is ten times slower, at 3 half as fast again. +// Knob 3: Bands. How many pale fronts run along the curve at once, 1 to 8. A front lights the stretch of curve it has just passed, so its edge is the outline of the region the curve has filled so far, the discrete SLE(8) hull. At 1 one front sweeps up the panel and each LED is passed every 8 s at Speed 2; at 8 eight fronts run 1,024 LEDs apart and each LED is passed every second. +// Knob 4: Stretch. An upright edge of the grid weighs e^(2.5 x Stretch) times a level one, 12 times at 1, and the walks step up and down that much more often, which makes the tree the weighted uniform one. At -1 a fifth of the tree's 2,047 edges stand upright and the curve lies in long level folds, at the opening 0.06 just over half do, and at 1 four fifths do and the curve hangs in tall columns. + +const W = 64, H = 128, N = W * H; +// The tree lives on the 32 x 64 grid of 2 x 2 LED blocks. +const X = 32, Y = 64, M = X * Y; +const { floor, min, max, exp, pow } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// Direction bits of a tree edge out of a block. +const E = 1, WE = 2, S_ = 4, NO = 8; +// Seconds at Speed 2: colour flows over the finished curve, then Wilson's +// walks grow the next tree, then the next curve is traced in from a to b. +const FLOW = 3.6, GROW = 2.0, TRACE = 2.4; +// A front runs the whole cycle in this many seconds at Speed 2, whatever +// the number of bands, so more bands pass each LED more often. +const LAP = 8; +// The hue at a at knob 0, and how far round the wheel it turns by b. +const HUE0 = 0.3, SPAN = 0.6; +// Runners: a bright head every GAP LEDs along the curve with a tail TAIL +// LEDs long, running from a to b at RUN LEDs a second at Speed 2, so the +// way the curve winds shows LED by LED. +const GAP = 160, TAIL = 32, RUN = 40; +const TL = new Float32Array(GAP); +for (let i = 0; i < TAIL; i++) TL[i] = 0.8 * Math.pow(1 - i / TAIL, 1.6); + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// The wired arc: the blocks along the bottom, up the right side and back +// along the top, joined in one path that every tree contains. The left side +// is free. The curve then runs from the bottom left corner (a) to the top +// left corner (b), which is Lawler, Schramm and Werner's set-up. +const ARC = []; +for (let i = 0; i < X; i++) ARC.push((Y - 1) * X + i); +for (let j = Y - 2; j >= 0; j--) ARC.push(j * X + X - 1); +for (let i = X - 2; i >= 0; i--) ARC.push(i); + +export function setup(params) { + params.s = { + hue: 0, speed: 2, bands: 1, stretch: 0, + adj: new Uint8Array(M), nadj: new Uint8Array(M), + sOld: new Int16Array(N).fill(-1), sNew: new Int16Array(N), + log: new Int16Array(1 << 16), nlog: 0, + inT: new Uint8Array(M), par: new Int16Array(M), born: new Float32Array(M), blen: new Int16Array(M), + path: new Int16Array(M), pos: new Int16Array(M), plen: 0, k: 0, + phase: -1, pt: 0, phi: 0, psi: 0, + rgb: new Float32Array(N * 3), pal: new Float32Array(1024 * 3), warm: new Float32Array(1024), palKey: -1, + }; +} + +function link(adj, p, out) { + const x = p & 63, y = p >> 6, b = (y >> 1) * X + (x >> 1), sx = x & 1, sy = y & 1; + out[0] = adj[b] & (sx ? E : WE) ? p + (sx ? 1 : -1) : p ^ 64; + out[1] = adj[b] & (sy ? S_ : NO) ? p + (sy ? W : -W) : p ^ 1; +} + +const dirOf = (a, b) => (b === a + 1 ? E : b === a - 1 ? WE : b === a + X ? S_ : NO); + +// Wilson's algorithm with the arc as root: from each block not yet in the +// tree a random walk runs until it meets the tree, and its loop erasure is +// added. Edge weights wh (across) and wv (up and down) bias the walk, which +// makes the tree the weighted uniform one. Every step is logged so the +// walks can be shown afterwards at the pace of the panel. +function wilson(S, wh, wv) { + const inT = new Uint8Array(M), nxt = new Int16Array(M).fill(-1), adj = S.nadj; + adj.fill(0); + for (const v of ARC) inT[v] = 1; + for (let k = 0; k + 1 < ARC.length; k++) { + const a = ARC[k], b = ARC[k + 1]; + adj[a] |= dirOf(a, b); + adj[b] |= dirOf(b, a); + } + const order = new Int16Array(M); + for (let i = 0; i < M; i++) order[i] = i; + for (let i = M - 1; i > 0; i--) { + const j = floor(Math.random() * (i + 1)), t = order[i]; + order[i] = order[j]; + order[j] = t; + } + let n = 0, log = S.log; + const put = (v) => { + if (n >= log.length) { + const big = new Int16Array(log.length * 2); + big.set(log); + log = S.log = big; + } + log[n++] = v; + }; + for (let o = 0; o < M; o++) { + const s = order[o]; + if (inT[s]) continue; + put(s); + let u = s; + while (!inT[u]) { + const i = u % X, j = (u / X) | 0; + const a = i > 0 ? wh : 0, b = i < X - 1 ? wh : 0, c = j > 0 ? wv : 0, d = j < Y - 1 ? wv : 0; + let r = Math.random() * (a + b + c + d); + const v = (r -= a) < 0 ? u - 1 : (r -= b) < 0 ? u + 1 : (r -= c) < 0 ? u - X : u + X; + nxt[u] = v; + put(v); + u = v; + } + for (u = s; !inT[u]; u = nxt[u]) { + inT[u] = 1; + adj[u] |= dirOf(u, nxt[u]); + adj[nxt[u]] |= dirOf(nxt[u], u); + } + } + S.nlog = n; +} + +// The curve. Each block starts as a ring round its own four LEDs; every tree +// edge cuts the two facing sides of two rings and joins them across, so the +// tree's 2047 edges join the 2048 rings into one cycle through all 8192 +// LEDs, the outline of the thickened tree. It is cut where it leaves the +// bottom left corner: the Peano path from a to b first, then 254 LEDs back +// round the outside of the arc. +function trace(adj, s) { + s.fill(-1); + const nb = [0, 0]; + let prev = 127 * W, p = 126 * W, k = 0; + while (k < N && s[p] < 0) { + s[p] = k++; + link(adj, p, nb); + const q = nb[0] !== prev ? nb[0] : nb[1]; + prev = p; + p = q; + } + return k; +} + +function startGrow(S) { + const a = S.stretch * 1.25; + wilson(S, exp(-a), exp(a)); + S.inT.fill(0); + for (const v of ARC) S.inT[v] = 1; + S.born.fill(-9); + S.pos.fill(-1); + S.plen = 0; + S.k = 0; +} + +// One logged step of the walks: start, step, erase a loop, or join the tree. +// Erasing each loop as it closes gives back the same tree Wilson's run built. +function replay(S) { + const v = S.log[S.k++], { path, pos, inT } = S; + if (S.plen === 0) { + path[0] = v; + pos[v] = 0; + S.plen = 1; + } else if (inT[v]) { + for (let i = 0; i < S.plen; i++) { + const u = path[i]; + inT[u] = 1; + pos[u] = -1; + S.par[u] = i + 1 < S.plen ? path[i + 1] : v; + S.born[u] = S.pt; + S.blen[u] = S.plen; + } + S.plen = 0; + } else if (pos[v] >= 0) { + for (let i = pos[v] + 1; i < S.plen; i++) pos[path[i]] = -1; + S.plen = pos[v] + 1; + } else { + pos[v] = S.plen; + path[S.plen++] = v; + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 1, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = cl(kv[0], 0, 1); + S.speed = cl(kv[1], 0.2, 3); + S.bands = cl(kv[2], 1, 8); + S.stretch = cl(kv[3], -1, 1); + const g = (d * S.speed) / 2; + S.phi += (g * S.bands) / LAP; + S.psi += g * RUN; + if (S.phase < 0) { + // The first tree is grown at once and traced in from black. + startGrow(S); + S.adj.set(S.nadj); + trace(S.adj, S.sNew); + S.phase = 2; + S.pt = 0; + return; + } + S.pt += g; + if (S.phase === 0 && S.pt >= FLOW) { + S.phase = 1; + S.pt = 0; + S.sOld.set(S.sNew); + startGrow(S); + } else if (S.phase === 1) { + const want = floor(pow(cl(S.pt / GROW, 0, 1), 2.5) * S.nlog); + while (S.k < want) replay(S); + if (S.pt >= GROW) { + S.adj.set(S.nadj); + trace(S.adj, S.sNew); + S.phase = 2; + S.pt = 0; + } + } else if (S.phase === 2 && S.pt >= TRACE) { + S.phase = 0; + S.pt = 0; + } +} + +// A monotone turn of the hue circle that hurries through orange and +// yellow, which read brown and olive when dim. +const warp = (h) => h + (0.5 / (2 * Math.PI)) * Math.sin(2 * Math.PI * (h - 0.11)); + +// Hue by place along the whole curve, from a to b; brightness and a +// whitening by how long ago the last front passed. +function palette(S) { + const key = S.hue; + if (key === S.palKey) return; + S.palKey = key; + const P = S.pal, h0 = HUE0 + S.hue; + for (let i = 0; i < 1024; i++) { + // Deep blue is paled a little, since it reads darkest on the LEDs. + const h = warp(h0 + SPAN * (i / 1023)); + hsv(h, 0.88 - 0.3 * max(0, Math.cos(2 * Math.PI * (h - 0.66))), 1, P, i * 3); + // Orange and yellow keep most of their light even long after a front, + // so they never sink to brown and olive. + S.warm[i] = 0.65 * pow(max(0, Math.cos(2 * Math.PI * (h - 0.11))), 2); + } +} +// Palette index by place on the cycle: up the hues along the Peano path, +// then back down them along the 254 LEDs that close the cycle round the +// outside of the arc, so the closing run meets a in a's own colour. +const PATH = N - 254, HP = new Int16Array(N); +for (let s = 0; s < N; s++) HP[s] = s < PATH ? ((s * 1023) / PATH) | 0 : (((N - s) * 1023) / (N - PATH)) | 0; +const VL = new Float32Array(1024), WL = new Float32Array(1024); +for (let i = 0; i < 1024; i++) { + const u = i / 1023; + VL[i] = 0.45 + 0.55 * u; + WL[i] = 0.55 * Math.pow(u, 8); +} + +export function draw(display, params) { + const S = params.s; + if (!S || S.phase < 0) return; + palette(S); + const rgb = S.rgb, P = S.pal, K = S.bands, phi = S.phi, sN = S.sNew, sO = S.sOld; + const ph = S.phase, pt = S.pt; + const head = ph === 2 ? (pt / TRACE) * N : N + 1e9; + const dim = ph === 1 ? 1 - 0.6 * cl(pt / 0.4, 0, 1) : 0.4; + for (let p = 0, o = 0; p < N; p++, o += 3) { + // Behind the head of a trace the new curve, ahead of it the old one. + let s = sN[p], f = 1, tip = 0; + if (ph === 1 || s > head) { + s = sO[p]; + f = dim; + if (s < 0) { + rgb[o] = rgb[o + 1] = rgb[o + 2] = 0; + continue; + } + } else if (ph === 2 && head - s < 160) tip = (1 - (head - s) / 160) ** 2; + let u = (K * s) / N - phi; + u -= floor(u); + const i = HP[s], c = i * 3, ui = (u * 1023) | 0; + // A runner's tail, if one is passing. + let a = (S.psi - s) % GAP; + if (a < 0) a += GAP; + const t = ph === 1 ? 0 : TL[a | 0]; + const lo = VL[ui] + (1 - VL[ui]) * S.warm[i]; + f *= lo + (1 - lo) * t; + const w = WL[ui] + (1 - WL[ui]) * t * 0.6; + for (let e = 0; e < 3; e++) { + const v = (P[c + e] + (1 - P[c + e]) * w) * f; + // The tip of the curve being traced in glows white. + rgb[o + e] = v + (1 - v) * tip; + } + } + if (ph === 1) { + // Wilson's walks: the loop-erased path still walking, and the branches + // that have just joined the tree, drawn through the top left LED of + // each block. + const at = (v, c) => { + const x = (v % X) * 2, y = ((v / X) | 0) * 2, o = (y * W + x) * 3; + for (let e = 0; e < 3; e++) rgb[o + e] += (1 - rgb[o + e]) * c; + }; + const mid = (a, b, c) => { + const x = (a % X) + (b % X), y = ((a / X) | 0) + ((b / X) | 0), o = (y * W + x) * 3; + for (let e = 0; e < 3; e++) rgb[o + e] += (1 - rgb[o + e]) * c; + }; + for (let v = 0; v < M; v++) { + const age = pt - S.born[v]; + if (age < 0 || age > 0.6) continue; + // Long branches flash longer than the many short ones at the end. + const c = 0.85 * min(1, S.blen[v] / 8) * exp(-age / 0.18); + at(v, c); + mid(v, S.par[v], c); + } + for (let i = 0; i < S.plen; i++) { + at(S.path[i], 0.95); + if (i) mid(S.path[i - 1], S.path[i], 0.95); + } + } + for (let y = 0, o = 0; y < H; y++) + for (let x = 0; x < W; x++, o += 3) display.setPixel(x, y, (255 * cl(rgb[o], 0, 1)) | 0, (255 * cl(rgb[o + 1], 0, 1)) | 0, (255 * cl(rgb[o + 2], 0, 1)) | 0); +} From 15d074e896886c7655d9922cdea28e4c8756d8f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 06:44:27 +0000 Subject: [PATCH 07/10] =?UTF-8?q?56=ED=8C=90:=20=EB=91=90=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=EC=9D=B4=20=EC=A7=80=EB=8F=84=20=EC=B6=95=EC=9D=98=20?= =?UTF-8?q?=EB=81=9D=EC=9D=84=20=EC=98=AE=EA=B8=B0=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EA=B2=8C=20=EA=B3=A0=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dead Leaves 는 다음 표본이 앞 표본 위로 다시 찰 때 앞 표본을 한 단 낮추고 채도를 빼서 모양 바뀜을 0.454 에서 0.196 으로 내리고, Golomb Trominoes 는 빈칸이 1초에 18 LED 를 걸어 움직임을 0.014 에서 0.023 으로 올린다. 둘이 움직임과 모양 바뀜 축의 끝을 넓히면 움직임 × 모양 바뀜 1,4 칸이 비었다. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UUXdHR3tRiDEQfEfGbQxb9 --- incoming/pattern-study/patterns/dead-leaves.js | 18 +++++++++--------- .../pattern-study/patterns/golomb-trominoes.js | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/incoming/pattern-study/patterns/dead-leaves.js b/incoming/pattern-study/patterns/dead-leaves.js index 323724ae4..d29ec55f5 100644 --- a/incoming/pattern-study/patterns/dead-leaves.js +++ b/incoming/pattern-study/patterns/dead-leaves.js @@ -12,7 +12,7 @@ // @knobs Hue=0..1, Speed=0.2..3, Grain=0.5..4, Law=-1..1 // // Knob 1: Hue. Turns the size colours round the wheel together. At 0 leaves of radius half an LED are pink-magenta and each doubling of the radius steps 0.072 back round the wheel, through violet and blue to cyan at 16 to 20 LEDs; at 0.25 the small leaves are orange-red and the big ones violet, at 0.5 green down to red, at 0.75 blue down to lime. A leaf whose hue falls between orange and yellow takes only the two bright levels, so it never reads brown or olive. -// Knob 2: Speed. At 2 the leaves come down 3.2 deep a second, about 4,900 leaves a second at the opening Grain: the panel is 95% covered after 0.9 s and the last hole closes after 2.8 s (median of 40 samples, 2.3 to 3.2 s for the middle 80%), and the white ring runs out at 60 LEDs a second while the finished sample holds 2.2 s before the next one starts over it. At 0.2 everything is ten times slower, at 3 half as fast again. +// Knob 2: Speed. At 2 the leaves come down 3.2 deep a second, about 4,900 leaves a second at the opening Grain: the panel is 95% covered after 0.9 s and the last hole closes after 2.8 s (median of 40 samples, 2.3 to 3.2 s for the middle 80%), and the white ring runs out at 60 LEDs a second while the finished sample holds 2.2 s before the next one starts under it, the old leaves showing through its holes one level darker and greyed. At 0.2 everything is ten times slower, at 3 half as fast again. // Knob 3: Grain. The smallest radius, 0.5 to 4 LEDs. At 0.5 a sample takes about 13,500 leaves and the five octaves of radius from half an LED to 16 each cover 19 to 20% of the panel; at 4 only the octaves from 4 up are left, 500 leaves make a sample, and the picture is big overlapping discs in blue and cyan. // Knob 4: Law. The exponent a of the radius density r^-a, 3 + K4 outside a dead zone of 0.1 either side of 0, so the opening 0.06 is exactly 3, the law under which the picture looks the same at every scale. At -1 (a = 2) the big leaves take over: radii 8 to 16 cover 42% of the panel and those under 1 LED 2.5%. At 1 (a = 4) the smallest octave covers 51% and the panel turns to magenta grain. @@ -27,10 +27,10 @@ const RATE = 1.6; // Seconds the finished sample holds at Speed 1, and how fast the ring runs // out, in LEDs a second at Speed 1. const HOLD = 4.4, RING = 30; -// Every leaf is one of four flat levels, 45 apart, and the holes show the -// sample before at one dim level, so two leaves side by side either match -// in level or differ by 45 or more. -const TIERS = [255, 210, 165, 120], GHOST = 60; +// Every leaf is one of four flat levels, 45 apart. The holes show the +// sample before, each leaf one level down and greyed, so two leaves side by +// side, old or new, either match in level or differ by 45 or more. +const TIERS = [255, 210, 165, 120, 75], OLD_SAT = 0.35; // Hue tells a leaf's size: the smallest, half an LED across in radius, are // magenta at the opening, and each doubling of the radius steps OCT back // round the wheel, through violet, blue and cyan to green at 20 LEDs. Each @@ -123,8 +123,8 @@ export function update(dt, input, params) { if (S.phase === 1) { S.pt += g; if (S.pt > HOLD) { - // The held sample sinks to one dim level and shows through the holes - // of the next one until they close. + // The held sample steps one level down, greys, and shows through the + // holes of the next one until they close. S.old = S.cur; S.cur = sample(); S.phase = 0; @@ -151,11 +151,11 @@ export function draw(display, params) { rgb[o] = rgb[o + 1] = rgb[o + 2] = 0; continue; } - const h = hb + L.hu[k], t = L.lv[k], v = L === D ? TIERS[warm(h) ? t & 1 : t] : GHOST; + const h = hb + L.hu[k], t = L.lv[k], fresh = L === D, v = TIERS[(warm(h) ? t & 1 : t) + (fresh ? 0 : 1)]; // Each leaf pales toward its centre, so a disc reads as a disc even // where only a sliver of it shows. Only the weaker channels change. const dx = x + 0.5 - L.cx[k], dy = y + 0.5 - L.cy[k], f = min(1, sqrt(dx * dx + dy * dy) / L.r[k]); - hsv(h, L.sa[k] * (0.6 + 0.4 * f), v, rgb, o); + hsv(h, L.sa[k] * (0.6 + 0.4 * f) * (fresh ? 1 : OLD_SAT), v, rgb, o); } // The moment the last hole closes the sample is exact: a white ring runs // out from where that hole was. diff --git a/incoming/pattern-study/patterns/golomb-trominoes.js b/incoming/pattern-study/patterns/golomb-trominoes.js index 76c89ec11..ce463f81b 100644 --- a/incoming/pattern-study/patterns/golomb-trominoes.js +++ b/incoming/pattern-study/patterns/golomb-trominoes.js @@ -12,9 +12,9 @@ // @knobs Hue=0..1, Speed=0.2..3, Lift=0..4, Cell=1..4 // // Knob 1: Hue. Turns every colour round the wheel together. At 0 the 2 x 2 trominoes are azure and navy in the outermost shell and turn cyan, aqua, green, lime and yellow shell by shell towards the hole; the 4 x 4 trominoes are plum, 8 x 8 gold, 16 x 16 lime and 32 x 32 rose, and the trominoes the proof lays round the hole, one a level, are white. At 0.5 the shells run orange, red, pink and violet and the levels aqua, blue and violet. The dim colours skip the orange and yellow part of the wheel, which reads brown and olive. -// Knob 2: Speed. The hole walks 5 LEDs a second for each unit, 10 at the opening 2, on a straight line that reflects off the board's edges. A step that stays inside a 2 x 2 block re-lays that one tromino; a step over the line between two blocks of side 2^k re-lays at most 2^k - 1 trominoes (3, 7, 15, 31, 63) and about two thirds of that on average. At 2 the two holes cross their board's middle at 1.4 s and 1.2 s and then on average every 6.3 s. 0.2 is ten times slower. +// Knob 2: Speed. The hole walks 9 LEDs a second for each unit, 18 at the opening 2, on a straight line that reflects off the board's edges. A step that stays inside a 2 x 2 block re-lays that one tromino; a step over the line between two blocks of side 2^k re-lays at most 2^k - 1 trominoes (3, 7, 15, 31, 63) and about two thirds of that on average. At 2 the two holes cross their board's middle at 0.8 s and 0.7 s and then on average every 3.5 s. 0.2 is ten times slower. // Knob 3: Lift. Leaves the lowest levels of the proof unlaid. At 0 every tromino is laid; 1 to 4 leave whole blocks of 4, 8, 16 and 32 cells a side (on a 16-cell board no more than 8), each one square in its tone short the one cell that the trominoes above it, or the hole, took. Each such square is the proof's induction hypothesis: a smaller board short one cell, which the same proof tiles. -// Knob 4: Cell. LEDs to a cell: 1 below 1.5, 2 below 3.5, then 4, so each half of the panel is a board of 64, 32 or 16 cells a side, laid with 1365, 341 or 85 trominoes. The hole still walks 10 LEDs a second at Speed 2, so on the bigger cells it takes fewer, larger steps. +// Knob 4: Cell. LEDs to a cell: 1 below 1.5, 2 below 3.5, then 4, so each half of the panel is a board of 64, 32 or 16 cells a side, laid with 1365, 341 or 85 trominoes. The hole still walks 18 LEDs a second at Speed 2, so on the bigger cells it takes fewer, larger steps. // Each half of the panel is one square board, W LEDs a side. const W = 64, H = 128; @@ -166,10 +166,10 @@ export function update(dt, input, params) { } const n = S.n, N = S.S; S.lift = min(LIFT[cl(round(kv[2]), 0, 4)], n - 1); - // The hole walks five LEDs a second for each unit of Speed, whatever the + // The hole walks nine LEDs a second for each unit of Speed, whatever the // cell size. S.t += d; - S.acc += (d * S.speed * 5) / c; + S.acc += (d * S.speed * 9) / c; let steps = floor(S.acc); S.acc -= steps; for (const B of S.boards) { From 8312ccf1b920f60c3db96df20eaa6418a320cd94 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 07:15:24 +0000 Subject: [PATCH 08/10] =?UTF-8?q?56=ED=8C=90:=20README=20=EC=97=90=20?= =?UTF-8?q?=ED=8C=90=20=ED=91=9C=EC=99=80=20=ED=8C=90=20=EC=A0=88,=20?= =?UTF-8?q?=EB=8B=A4=EC=9D=8C=20=ED=8C=90=20=EA=B0=90=EC=9D=84=20=EB=8D=94?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UUXdHR3tRiDEQfEfGbQxb9 --- incoming/pattern-study/README.md | 75 ++++++++++++++++++- .../pattern-study/patterns/dead-leaves.js | 4 +- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 6f33b06da..8e9993524 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 458개를 쉰여섯 갈래로 나눠 만들었어요. 1판에서 열세 갈래로 23개를 만들고, 2판에서 1판에 없던 아홉 갈래로 16개를, 3판에서 하나를, 4판에서 새 갈래인 직물로 하나를 더했어요. 5판은 새 패턴 없이 1판의 Euclid Pulse 를 다시 짰고, 6판은 세운 패널의 128줄을 위아래로 다 쓰는 패턴 하나를 더했어요. 7판은 또렷한 모양이 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 8판은 새 갈래인 기계로 하나를 더했어요. 9판은 새 패턴 없이 1판의 Sprout 을 다시 짰어요. 10판은 세운 패널을 떨어지는 블록의 우물로 쓰는 패턴 하나를 더했어요. 11판은 여러 색이 꽤 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 12판은 새 갈래인 천체로 하나를 더했어요. 13판은 새 패턴 없이 1판의 Glyph Rain 을 다시 짰어요. 14판은 절차가 바뀌기 전에 띄운 바퀴라서, 세운 패널의 높이를 다 떨어지는 폭포 하나를 더했어요. 15판은 바뀐 절차의 첫 판이라 열 갈래에 새 패턴을 하나씩, 모두 열 개를 더했어요. 도시와 음악 갈래는 그때 새로 열었어요. 16판도 열 갈래에 하나씩 열 개를 더했고, 바다와 곤충 갈래를 새로 열었어요. 17판도 열 갈래에 하나씩 열 개를 더했고, 전자 회로와 장난감 갈래를 새로 열었어요. 18판도 열 갈래에 하나씩 열 개를 더했고, 유리와 광물·결정, 놀이공원 갈래를 새로 열었어요. 19판도 열 갈래에 하나씩 열 개를 더했고, 부엌과 버섯·균사 갈래를 새로 열었어요. 20판도 열 갈래에 하나씩 열 개를 더했고, 새와 화학·실험 갈래를 새로 열었어요. 21판도 열 갈래에 하나씩 열 개를 더했고, 종이와 운동·경기 갈래를 새로 열었어요. 22판도 열 갈래에 하나씩 열 개를 더했고, 몸과 비행 갈래를 새로 열었어요. 23판도 열 갈래에 하나씩 열 개를 더했고, 무대·공연과 짐승 갈래를 새로 열었어요. 24판도 열 갈래에 하나씩 열 개를 더했고, 공사·건설과 도자기 갈래를 새로 열었어요. 25판도 열 갈래에 하나씩 열 개를 더했고, 농사·밭과 화산 갈래를 새로 열었어요. 26판도 열 갈래에 하나씩 열 개를 더했고, 얼음·극지와 미생물 갈래를 새로 열었어요. 27판도 열 갈래에 하나씩 열 개를 더했고, 철도와 필름·영화 갈래를 새로 열었어요. 28판은 주제 표가 수학 쪽으로 바뀐 첫 판이라 이름 있는 수학 대상 열 개를 열 갈래에 하나씩 더했고, 수론 갈래를 새로 열었어요. 29판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 첫 판이라, 이름 있는 수학 대상 여섯과 화장품 매장 벽에 틀 질감 넷을 더했고, 위상·매듭과 화장품 팝업 갈래를 새로 열었어요. 30판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 매장의 LED 벽에 틀 질감 넷을 화장품 팝업 갈래에 더했어요. 31판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 열지 않았어요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽의 질감 넷을 화장품 팝업 갈래에 더했어요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 41판은 주제 표가 수학·추상 다섯과 할로윈 다섯으로 바뀐 첫 판이라, 수학 대상 다섯을 다섯 갈래에 하나씩 더하고 할로윈 갈래를 새로 열어 10월 31일에 틀 패턴 다섯을 넣었어요. 42판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 41판이 연 할로윈 갈래에 더했어요. 43판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 44판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 45판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 열 개 아래인 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 46판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 47판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 48판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 49판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 50판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 51판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 52판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 53판은 할로윈 몫이 쉬게 된 첫 판이라, 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 54판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 55판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 468개를 쉰여섯 갈래로 나눠 만들었어요. 1판에서 열세 갈래로 23개를 만들고, 2판에서 1판에 없던 아홉 갈래로 16개를, 3판에서 하나를, 4판에서 새 갈래인 직물로 하나를 더했어요. 5판은 새 패턴 없이 1판의 Euclid Pulse 를 다시 짰고, 6판은 세운 패널의 128줄을 위아래로 다 쓰는 패턴 하나를 더했어요. 7판은 또렷한 모양이 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 8판은 새 갈래인 기계로 하나를 더했어요. 9판은 새 패턴 없이 1판의 Sprout 을 다시 짰어요. 10판은 세운 패널을 떨어지는 블록의 우물로 쓰는 패턴 하나를 더했어요. 11판은 여러 색이 꽤 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 12판은 새 갈래인 천체로 하나를 더했어요. 13판은 새 패턴 없이 1판의 Glyph Rain 을 다시 짰어요. 14판은 절차가 바뀌기 전에 띄운 바퀴라서, 세운 패널의 높이를 다 떨어지는 폭포 하나를 더했어요. 15판은 바뀐 절차의 첫 판이라 열 갈래에 새 패턴을 하나씩, 모두 열 개를 더했어요. 도시와 음악 갈래는 그때 새로 열었어요. 16판도 열 갈래에 하나씩 열 개를 더했고, 바다와 곤충 갈래를 새로 열었어요. 17판도 열 갈래에 하나씩 열 개를 더했고, 전자 회로와 장난감 갈래를 새로 열었어요. 18판도 열 갈래에 하나씩 열 개를 더했고, 유리와 광물·결정, 놀이공원 갈래를 새로 열었어요. 19판도 열 갈래에 하나씩 열 개를 더했고, 부엌과 버섯·균사 갈래를 새로 열었어요. 20판도 열 갈래에 하나씩 열 개를 더했고, 새와 화학·실험 갈래를 새로 열었어요. 21판도 열 갈래에 하나씩 열 개를 더했고, 종이와 운동·경기 갈래를 새로 열었어요. 22판도 열 갈래에 하나씩 열 개를 더했고, 몸과 비행 갈래를 새로 열었어요. 23판도 열 갈래에 하나씩 열 개를 더했고, 무대·공연과 짐승 갈래를 새로 열었어요. 24판도 열 갈래에 하나씩 열 개를 더했고, 공사·건설과 도자기 갈래를 새로 열었어요. 25판도 열 갈래에 하나씩 열 개를 더했고, 농사·밭과 화산 갈래를 새로 열었어요. 26판도 열 갈래에 하나씩 열 개를 더했고, 얼음·극지와 미생물 갈래를 새로 열었어요. 27판도 열 갈래에 하나씩 열 개를 더했고, 철도와 필름·영화 갈래를 새로 열었어요. 28판은 주제 표가 수학 쪽으로 바뀐 첫 판이라 이름 있는 수학 대상 열 개를 열 갈래에 하나씩 더했고, 수론 갈래를 새로 열었어요. 29판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 첫 판이라, 이름 있는 수학 대상 여섯과 화장품 매장 벽에 틀 질감 넷을 더했고, 위상·매듭과 화장품 팝업 갈래를 새로 열었어요. 30판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 매장의 LED 벽에 틀 질감 넷을 화장품 팝업 갈래에 더했어요. 31판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 열지 않았어요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽의 질감 넷을 화장품 팝업 갈래에 더했어요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 41판은 주제 표가 수학·추상 다섯과 할로윈 다섯으로 바뀐 첫 판이라, 수학 대상 다섯을 다섯 갈래에 하나씩 더하고 할로윈 갈래를 새로 열어 10월 31일에 틀 패턴 다섯을 넣었어요. 42판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 41판이 연 할로윈 갈래에 더했어요. 43판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 44판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 45판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 열 개 아래인 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 46판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 47판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 48판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 49판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 50판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 51판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 52판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 53판은 할로윈 몫이 쉬게 된 첫 판이라, 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 54판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 55판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 56판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. 패널은 세로로 서요. 64 LED 너비에 128 LED 높이가 기본이라서 모든 패턴의 머리에 `@matrix 64x128` 이 있어요. 레포의 패널 설정은 그대로예요. `shared/constants.json` 의 `PANEL_GEOM_DEFAULT` 는 `128x64_SINGLE` 이고, 패턴이 가로세로를 바꾼 판을 적으면 펌웨어의 `firmware/patternflow/src/core_canvas.h` 가 90도 돌려 패널을 꽉 채워요. 1판 23개는 처음에 가로 128×64 로 만들었다가 2판에서 세로로 다시 짰어요. @@ -735,6 +735,21 @@ | [Toussaint-Wilczek](patterns/toussaint-wilczek.js) | 입자 | 초록과 자홍 두 종의 입자가 판 가득 뿌려져 걷다가 서로 만나면 짝지어 사라지고, 처음 우연히 더 많았던 쪽이 남은 자리마다 같은 색 영역이 걸음 수의 제곱근으로 넓어지며, 그 사이 이음매는 어둡게 남아 입자가 사라지는 자리에서 잿빛으로 반짝여요. K3 은 처음 뿌리는 밀도(LED 마다 80~320), K4 는 A 가 B 보다 많은 몫이라 올리면 자홍이 사라지고 초록이 판을 채워요. 단추를 누르면 새로 뿌려요. | | [Vortex Sheet](patterns/vortex-sheet.js) | 시뮬레이션 | 패널을 세로로 가르는 와류 시트를 사이에 두고 왼쪽 물은 아래로, 오른쪽 물은 위로 흐르면, 시트의 작은 물결이 자라 두 갈래 나선으로 말려 들며 청록과 보라 두 물을 점점 얇은 겹으로 감아 넣어요. K3 은 크라스니의 덩이 δ(0.15~0.5)라 작을수록 나선이 촘촘히 감기고, K4 는 패널에 쌓는 파장 수(1~4)예요. 단추를 누르면 새 물결로 다시 시작해요. | +### 56판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Dead Leaves](patterns/dead-leaves.js) | 노이즈·흐름 | 반지름이 반 LED 부터 20 LED 까지인 원판이 이미 놓인 원판 밑으로 들어가 빈틈에만 보이다가, 마지막 구멍이 닫히면 그 자리에서 흰 고리가 퍼져요. 색은 원판 반지름의 옥타브를 알려 주고, 다음 표본이 차는 동안 구멍으로는 앞 표본이 잿빛으로 비쳐요. | +| [Golomb Trominoes](patterns/golomb-trominoes.js) | 보로노이·셀 | 64 × 64 판 둘이 한 칸씩 빠진 채 3 LED 짜리 L 트로미노로 덮여요. 깜박이는 빈칸이 한 LED 씩 걸을 때마다 그 사이 트로미노가 흰빛으로 다시 깔리고, 바탕 색은 빈칸을 둘러싼 증명의 L 자 껍질을 따라가요. | +| [Tree Peano Curve](patterns/tree-peano-curve.js) | 선·궤적 | 한 줄로 이어진 곡선이 LED 8192개를 한 번씩 다 지나며 왼쪽 아래 모서리에서 왼쪽 위 모서리까지 판을 메우고, 메운 순서대로 초록에서 파랑을 거쳐 자홍으로 물든 위를 옅은 앞선과 짧은 꼬리가 곡선을 따라 흘러요. 8초마다 고리 지운 걸음이 하얀 줄로 새 나무를 그리고, 새 곡선이 모서리에서부터 다시 차올라요. | +| [Bloch Oscillations](patterns/bloch-oscillations.js) | 물결·간섭 | 한 자리에서 출발한 입자가 기운 사슬 위에서 렌즈 모양으로 50자리까지 퍼졌다가 한 점으로 돌아오기를 되풀이하며 위로 흘러가요. 색은 준운동량이라서 힘이 띠를 감는 동안 렌즈 가장자리 색이 돌아요. | +| [Andrews Curves](patterns/andrews-curves.js) | 데이터 그림 | 다섯 차원의 합성 점 40 개가 앤드루스 곡선이 되어 세 빛깔 다발로 꼬여요. 그랜드 투어가 데이터를 돌리는 동안 흰 곡선과 이웃 곡선 사이 틈은 부풀었다 오므라들지만, 바닥의 흰 막대는 길이가 그대로예요. | +| [Elliptic Law](patterns/elliptic-law.js) | 입자 | 세로로 선 타원 안에서 무작위 실수 행렬의 고윳값이 짧은 꼬리를 끌며 떠다니고, 가운데 실수축에서 거울 짝이 만나면 두 실수 고윳값으로 갈라져 축을 따라 달아나요. | +| [Erdős Discrepancy](patterns/erdos-discrepancy.js) | 리듬·펄스 | 64개 열이 같은 리듬을 d 박마다 하나씩 듣고 그 합을 한 줄씩 써 내려가요. 곱셈적 리듬은 청록과 자홍의 차분한 타탄을 짜고 기록이 깨지는 걸음마다 한 줄이 통째로 하얘지며, 동전 던지기 박이 섞이면 시끄러운 세로 줄로 찢어져요. | +| [Multiplicative Cascade](patterns/multiplicative-cascade.js) | 프랙탈 | 판을 가로세로 번갈아 열세 번 반씩 나눈 무게가 8 × 8 칸마다 민트와 보라 조각으로, LED 하나하나는 반짝이는 밝기로 보이고, 무게가 큰 반쪽부터 LED 까지 한 단씩 바뀌며 질량이 반쪽 사이를 오가요. Spread 를 올리면 판이 거의 꺼지고 몇 점의 불꽃만 남아요. | +| [Random Organization](patterns/random-organization.js) | 시뮬레이션 | 원판 83개가 오른쪽은 아래로, 왼쪽은 위로 밀렸다 돌아오고, 그동안 서로 닿은 원판만 금색으로 빛났다가 주기 끝에 조금씩 튕겨 나가요. 1분쯤 지나면 아무도 닿지 않는 자리를 찾아 청록으로 멈춰요. | +| [Twist and Writhe](patterns/twist-writhe.js) | 위상·매듭 | 세 번 꼬인 고리 리본이 납작한 원에서 8자를 지나 키 큰 초나선으로 감기는 동안, 아래 막대에서 꼬임수 Tw 가 줄어든 만큼 뒤틀림수 Wr 가 늘어 둘의 합은 늘 같은 정수 눈금에 멈춰요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -2853,6 +2868,50 @@ E8 의 근 240개를 고셋의 4₂₁ 다면체 꼭짓점으로 그렸어요. 주제 5 로 헬름홀츠(1868)와 켈빈(1871)의 와류 시트 불안정을 세웠어요. 크라스니(1986)가 무어(1979)의 곡률 특이점 너머까지 셈한 와동 덩이 방법 그대로, 한 파장의 시트를 점 와동 96개로 시작해 틈이 0.015 파장을 넘으면 점을 넣어 340개까지 늘리며 RK4 로 풀고, 패널에는 그 주기를 Waves 만큼 쌓아요. 덩이를 0.05 로 줄이면 작은 물결의 성장률이 2.97 로 맨 시트의 π 에 다가갔고, 다 말리는 동안 순환으로 가중한 시트의 가로 무게중심은 9e-6 파장 안에서 그대로였어요. 첫 판은 틈을 0.0075 로 잡아 2초 안에 420 점을 다 썼고, 한 프레임이 5.0 ms 에 시트가 2.5초마다 새로 섰어요. 틈을 두 배로 넓히고 커널을 점마다 다섯 수로 묶어 읽자 2.2 ms 에 시트 하나가 8.6초를 살아요. 새 시트마다 섞던 둘째 고조파는 덩이 0.25 아래에서 작은 둘째 나선으로 말려 첫 나선과 엉켜서 뺐고, 덩이 0.1 에서는 400 점으로도 코어를 붙잡지 못해 4.5초부터 시트가 해져서 K3 은 0.15 에서 열어요. 4초가 지나면 코어의 겹이 LED 보다 촘촘해져 연보라 원판으로 차지만, 그 둘레의 겹은 청록과 보라 두 물의 띠로 갈려 또렷이 읽혀요. 움직임 0.671, 노브는 0.18, 0.10, 0.07, 0.09 이고 `thirds` 는 100/100/100 이에요. +## 56판 + +56판도 열 개 모두 주제 5 「수학 · 추상」이에요. 같이 돈 54판(#265)이 고른 수학과 지도 칸 둘을 피해서 골랐고, 앞 바퀴 PR 들에 따를 글은 없었어요. 55판까지 든 448개에 54판의 열 개를 더해 세면, 추상 갈래 가운데 노이즈·흐름이 열셋으로 가장 적고 보로노이·셀과 데이터 그림, 프랙탈, 입자, 시뮬레이션, 물결·간섭, 선·궤적, 리듬·펄스, 위상·매듭이 열넷이에요. 그 열 갈래에 하나씩 넣었어요. 주제 1 은 그 458개에서 패턴이 둘뿐인 칸 여섯 가운데 가장자리 × 복잡도 4,4 와 움직임 × 촘촘함 1,4 를 겨눴고, 주제 4 는 Tree Peano Curve 와 Bloch Oscillations, Andrews Curves 를 비롯한 다섯이 128줄을 써요. 열 개는 일꾼 다섯이 레포 밖 worktree 에서 둘씩 짓고, 접촉 시트로 하나씩 본 뒤 합쳤어요. + +### Dead Leaves + +주제 5 와 1 로 죽은 잎 모형을 노이즈·흐름 갈래에 세우고, 가장자리 × 복잡도 4,4 칸을 겨눴어요. 조르주 마테롱(1968)의 모형은 원판이 끝없이 떨어져 새 잎이 옛 잎을 덮는 그림이에요. 반지름 밀도를 r^-3 으로 두면 앤 리와 데이비드 멈퍼드, 징강 황(2001)이 자연 영상의 가림 모형으로 쓴 대로 어느 배율로 봐도 같은 그림이 돼요. 패널은 아래에서 올려다본 쪽이라 새 잎이 이미 놓인 잎 밑으로 들어가고, 빈틈이 다 닫히는 순간 그림은 정상 상태의 정확한 표본이 돼요. 윌프리드 켄들과 엘케 퇴네스(1999)의 완전 표본이에요. Speed 2 에서 0.9초면 95% 가 덮이고, 마지막 구멍은 표본 40개의 가운데 값으로 2.8초에 닫혀요. 그 자리에서 흰 고리가 퍼지고 2.2초 쉰 뒤 다음 표본이 차기 시작하면, 그 구멍으로는 앞 표본이 한 단 어둡게 잿빛으로 비쳐요. 잎은 밝기 255, 210, 165, 120 가운데 하나로 평평하게 칠해서 이웃한 두 잎은 밝기가 같거나 45 넘게 벌어져요. 그래서 가장자리 1.000, 복잡도 2.337 로 Agate 와 Smalti 만 있던 칸에 섰어요. 처음엔 잎마다 색을 넓게 흩었더니 보라와 청록 색종이처럼 탁했어요. 색을 반지름의 옥타브에 매기자, 반 LED 부터 16 LED 까지 다섯 옥타브가 저마다 패널의 19~20% 를 덮는 것이 색의 몫으로 보였어요. Law 를 -1 로 돌리면 반지름 8~16 의 청록 잎이 42% 를 차지하고, 1 로 돌리면 자홍 알갱이가 51% 가 돼요. + +### Golomb Trominoes + +주제 5 와 1 로 골롬의 트로미노 깔기를 보로노이·셀 갈래에 세우고, 움직임 × 촘촘함 1,4 칸을 겨눴어요. 솔로몬 골롬(1954)은 2^n × 2^n 판에서 한 칸을 빼면 언제나 L 트로미노로 덮인다는 것을 귀납으로 보였어요. 가운데에 트로미노 하나를 놓아 빈칸 없는 세 사분면의 안쪽 귀퉁이를 막고, 네 사분면으로 내려가요. 패널 위아래 반에 64 × 64 판을 하나씩 두고 LED 하나를 한 칸으로 해서, 판마다 1365 개의 트로미노를 그 증명대로 깔아요. 빈칸은 판 가장자리에서 튕기는 직선을 따라 1초에 18 LED 씩 걸어요. 2 × 2 안에서 걸으면 트로미노 하나가 다시 깔리고, 변이 2^k 인 블록의 경계를 넘으면 많아야 2^k − 1 개가 다시 깔려요. 판 가운데 선을 넘으면 63 개까지 흰빛으로 번쩍여요. 처음에는 밝기를 빠진 귀퉁이로만 갈랐더니 같은 밝기의 이웃이 붙어서 패널이 대각선 줄무늬로만 읽혔어요. 재 보니 2 × 2 트로미노는 빠진 귀퉁이가 왼쪽 위나 오른쪽 아래인 쪽과 나머지로 나누면 같은 쪽끼리 변을 대지 않았고, 더 큰 단계의 트로미노끼리는 아예 닿지 않았어요. 변이 16 칸부터 64 칸까지인 판의 모든 빈칸에서 이 세 색 규칙이 깨지는 변은 많아야 넷이었고, 모두 빈칸 두 칸 안이었어요. 판 하나의 깔기는 빈칸이 어디에 있든 190 칸 넘게 달라지지 않아서 Speed 노브가 0.010 으로 검사에 걸렸어요. 바탕 색을 빈칸의 껍질로 칠하자 0.05 가 됐어요. 껍질은 빈칸과 함께 드는 가장 작은 블록이고, 껍질마다 다시 큰 L 트로미노 꼴이에요. 움직임 0.023, 촘촘함 0.355 로 Knight's Tour 와 One Bit 둘뿐이던 칸에 서요. + +### Tree Peano Curve + +주제 5 와 4 로 균등 생성나무의 페아노 곡선을 선·궤적 갈래에 세웠어요. 데이비드 윌슨(1996)의 고리 지운 걸음으로 2 × 2 LED 칸 32 × 64 개의 생성나무를 뽑고, 나무를 굵힌 테두리를 따라가면 LED 8192개를 한 번씩 다 지나는 고리가 돼요. 그레고리 롤러, 오데드 슈람, 벤델린 베르너(2004)가 이 곡선의 극한이 SLE(8) 임을 보였어요. 그들처럼 왼쪽 변만 풀어 두고 나머지 변은 나무에 묶어서, 곡선이 왼쪽 아래 모서리에서 왼쪽 위 모서리까지 7,938 LED 를 지나고 254 LED 로 바깥을 돌아 닫혀요. 나무 290그루에서 매번 모든 LED 가 꼭 한 번씩 나왔고, 걸음 기록을 다시 풀어 지운 나무도 매번 윌슨이 만든 2,047 변과 같았어요. 처음엔 밝기 띠로만 칠해서 청록과 남색 두 빛의 얼룩이 됐는데, 곡선 위 자리로 초록에서 자홍까지 색을 매기니 아래에서 위로 메워 가는 순서가 읽혔어요. 18 LED 마다 점선을 흘렸을 때는 복잡도가 2.34 로 잡음에 가까워서, 160 LED 마다 꼬리 32 LED 짜리 51개로 바꾸니 0.95 가 됐어요. 고리를 닫는 254 LED 가 자홍 테두리로 튀어서 그 구간은 색을 거꾸로 되돌렸어요. `thirds` 는 98/99/99 예요. + +### Bloch Oscillations + +주제 5 와 4 로 블로흐 진동을 물결·간섭 갈래에 세웠어요. 펠릭스 블로흐(1929)와 클래런스 제너(1934)가 본 대로 격자 위 입자에 일정한 힘 F 를 걸면 달아나지 않고 주기 2π/F 로 제자리를 오가요. 그레고리 워니어(1960)의 사다리는 그 에너지 준위가 F 간격으로 놓인다는 같은 말이에요. 패널 가로 64칸이 사슬의 자리이고, 시간은 줄마다 0.75 씩 쌓이며 위로 올라가요. 근사 없이 한 걸음짜리 정확한 전파자로 풀어서, 한 자리 출발의 줄들이 닫힌 꼴 J_n(4J/F sin(Ft/2))² 과 3×10⁻⁸ 안에서 맞았고 RK4 와는 1.6×10⁻⁸ 안에서 맞았어요. 여는 힘 0.16 에서 주기는 52줄이고 렌즈는 양쪽으로 25자리씩 열려 57 LED 가 켜져요. 넓은 묶음은 24.9자리를 오가서 4J/F = 25 와 맞아요. 처음엔 위상을 그대로 색으로 칠했더니 자리마다 무지개 줄이 서서 시끄러웠어요. 이웃 자리 사이의 위상차, 곧 준운동량으로 바꾸자 렌즈 오른쪽 가장자리는 보라에서 하늘색으로, 왼쪽은 라임에서 빨강을 지나 분홍으로 바뀌었어요. 넓은 묶음은 한 주기에 색상환을 한 바퀴 돌고, 띠 꼭대기에서 브래그 반사로 돌아서는 자리가 빨강이에요. Anderson Localization 의 원뿔은 멈춰 서지만 여기서는 렌즈가 닫혔다 열리기를 그치지 않아요. + +### Andrews Curves + +주제 5 와 4 로 데이비드 앤드루스(1972)의 곡선 그림을 데이터 그림 갈래에 세웠어요. 점 x 를 f(t) = x1/√2 + x2 sin t + x3 cos t + x4 sin 2t + … 로 바꾸면, 두 곡선 차의 제곱을 −π 에서 π 까지 더한 값이 두 점 거리 제곱의 π 배예요(파스발). 그래서 가까운 점은 가까운 곡선이 돼요. t 를 128 줄 아래로 내리고 f 를 64 칸에 눕혔어요. 데이터는 합성이에요. 5차원에 13 점짜리 가우스 무리 셋과 동떨어진 점 하나를 두었어요. 대니얼 아시모프(1985)의 그랜드 투어 가운데 토러스 방법으로 좌표 평면마다 제 빠르기로 돌려요. 무리 셋은 청록, 보라, 초록 다발로 서로 감기고, 동떨어진 점의 흰 곡선은 다발 사이를 떠돌아요. 거리를 보이려고 그 흰 곡선과 가장 가까운 청록 점의 곡선 사이를 회색으로 채웠어요. 그 틈의 제곱평균제곱근은 매 프레임 그린 곡선에서 다시 재서 바닥 두 줄에 막대로 놓았어요. 틈은 부풀었다 오므라들지만 막대는 1분 동안 14.6 LED 에서 소수 셋째 자리까지 움직이지 않았어요. 처음에는 빛을 채널마다 더해 잘랐더니 다발이 겹치는 가운데가 흰빛으로 바랬어요. 가장 밝은 채널만 포화시키고 나머지가 비례로 따라오게 하자 겹친 다발도 제 색을 지켰고, 색이 0.226 에서 0.348 이 됐어요. + +### Elliptic Law + +주제 5 와 4 로 타원 법칙을 입자 갈래에 세웠어요. 장 지니브르(1965)의 무작위 행렬은 고윳값이 원판을 고르게 채우고(비아체슬라프 기르코, 1984), 한스위르겐 좀머스와 동료들(1988)은 J_ij 와 J_ji 의 상관이 τ 이면 그 원판이 반축 1 + τ, 1 − τ 의 타원으로 늘어남을 보였어요. 실수축을 세로로 세우고 τ 를 1/3 에서 열어서 타원이 64 × 128 을 꽉 채우고, 켤레 짝은 축을 사이에 두고 거울처럼 움직여요. 행렬 항목은 감쇠 진동자에 잡음을 준 2차원 오른스타인-울렌벡 과정으로 움직이고, 고윳값은 매 프레임 헤센베르크 축약과 프랜시스 이중 이동 QR 로 새로 풀어요. 40 × 40 행렬 4,000 개에서 실수 고윳값은 평균 7.6 개였고, 앨런 에델만, 에릭 코스틀란, 마이클 슈브(1994)의 √(2N/π) 를 τ 만큼 늘린 포레스터와 나가오의 값 7.1 에 가까워요. 축에서 짝이 만나는 일은 Speed 2 에서 1초에 6.5 번이나 됐고, 대부분 0.1초 안에 다시 짝으로 돌아가는 짧은 들락거림이었어요. 처음에는 만날 때마다 고리를 그렸더니 판이 고리로 덮여서, 축을 가로지르는 짧은 막대가 0.4초 빛나다 줄어드는 불꽃으로 바꿨어요. 항목 시계도 0.55 에서 0.22 로 늦춰서 가운데 고윳값이 1초에 17.5 LED 대신 12 LED 를 가요. 긴 노출을 두 배로 올리자 채움이 0.24 에서 0.32 로, `thirds` 가 21/30/21 에서 26/41/26 이 됐어요. + +### Erdős Discrepancy + +주제 5 와 4 로 에르되시 불일치 문제를 리듬·펄스 갈래에 세웠어요. 에르되시 팔(1932)은 +1 과 -1 로 된 수열이 무엇이든 x_d + x_2d + … + x_md 가 어떤 C 도 넘느냐고 물었고, 테런스 타오(2015)가 늘 넘는다고 증명했어요. 패널은 열 d = 1..64 마다 그 합을 한 걸음에 한 줄씩 쓰고, 맨 아래 줄에 지금 박의 쳐짐과 쉼을 두고 위로 밀어 올려요. 피터 보웨인, 스티븐 최, 마이클 쿤스(2010)의 3진 수열은 x_3n = x_n 이고 나머지는 n mod 3 으로 정하는 완전 곱셈적 수열이라, 열마다 같은 걸음에 x_d 만 곱해져요. 그래서 한 열의 띠 무늬에 수열의 처음 64개 부호가 곱해진 타탄이 짜여요. 그 합은 m 의 3진 자릿수 가운데 1 의 개수와 같아서, m 까지의 최대가 floor(log3(2m + 1)) 이고 log3 m + 1 을 넘지 않음을 m = 600,000 까지 셈해 맞췄어요. 기록이 깨지는 m = 1, 4, 13, 40, 121, 364, 1093, 3280 에서는 64개 열이 한꺼번에 기록에 닿아 한 줄이 통째로 하얘져요. 박을 모두 동전 던지기로 바꾸면 1,000번째 걸음까지의 기록이 6 에서 92 로 뛰어요. 처음 판은 청록과 자홍이 한 LED 씩 번갈아서, 줄인 시트에서 보랏빛 회색으로 뭉갰어요. 합이 1 일 때 밝기를 0.28 로 올리고 5 에서 꽉 차게 하자 LED 크기에서 띠가 또렷한 타탄이 됐어요. Noise 는 섞는 몫을 노브의 제곱으로 해서, 6% 만 섞여도 무늬가 무너지는 것이 노브의 첫 4분의 1에 들어와요. + +### Multiplicative Cascade + +주제 5 로 브누아 망델브로(1974)의 곱셈 폭포를 프랙탈 갈래에 세웠어요. 질량 1 을 가로와 세로로 번갈아 열세 번 반씩 나누며 반쪽마다 평균 1 인 로그정규 무게를 곱하면 LED 하나까지 내려가고, 장피에르 카안과 자크 페이리에르(1976)는 E[W log₂ W] 가 1 보다 작을 때만 총질량이 살아남는다고 보였어요. Spread 가 바로 그 값이고 문턱 1 은 노브 한가운데예요. 2,000판의 중앙값으로 0.4 에서는 일곱 번 나눈 뒤 0.64, 열세 번 뒤 0.61 로 버티고, 1 에서는 0.24 에서 0.15 로, 1.6 에서는 0.057 에서 0.019 로 줄어요. 밝기 바닥을 고른 몫의 1/8100 에 묶어 두어서 1.6 에서는 LED 16% 만 켜지고, 가장 무거운 1% 가 질량의 95% 를 쥐어요. LED 무게만 밝기로 그린 첫 판은 시트에서 파란 잡음으로만 보였는데, 8 × 8 칸의 무게를 색으로 얹으니 큰 반쪽부터 작은 칸까지 겹친 직사각형이 읽혔어요. 무게도 노드마다 따로 흐르게 했다가 반쪽에서 LED 까지 한 단씩 차례로 돌게 바꿨어요. Couple 로 형제 반쪽의 상관을 바꿔도 문턱은 그대로지만(존 비긴스, 1977), Spread 1 에서 열세 번 나눈 뒤 총질량은 -1 에서 0.25, 1 에서 0.08 이에요. 복잡도가 2.716 라서 Agate 다음으로 높아요. + +### Random Organization + +주제 5 로 무작위 조직화를 시뮬레이션 갈래에 세웠어요. 로랑 코르테, 폴 체이킨, 제리 골럽, 데이비드 파인(2008)이 묽은 현탁액을 주기마다 밀었다 되돌리자, 한 주기 동안 부딪친 입자만 조금씩 자리를 옮기다가 어느 변형 아래에서는 아무도 부딪치지 않는 배치를 찾아 영영 멈췄어요. 흡수 상태 상전이예요. 패널은 64 × 128 주기 상자에 반지름 2.5 LED 원판 83개를 두고, 오른쪽 열이 아래로, 왼쪽 열이 위로 밀렸다 돌아오는 단순 전단을 한 주기마다 걸어요. 두 원판의 거리는 변형에 대해 1차라서, [0, γ0] 의 어느 변형에서 닿는지를 구간으로 정확히 셈해요. 주기 끝에는 닿았던 원판만 지름의 4분의 3까지 아무 쪽으로 튕겨요. 바닥 16줄에는 주기마다 튕긴 몫이 64주기 쌓여요. 여는 자리 Strain 1 에서는 30판의 가운데가 89주기 만에 멈췄고, Strain 2.5 에서는 504주기가 걸렸어요. Strain 3 에서는 8판 가운데 넷이 6,000주기가 지나도 멈추지 않았고, 3.5 부터는 하나도 멈추지 않았어요. 처음 판은 Strain 0.5, Density 0.15 에서 열어서 원판이 듬성하고 전단이 거의 보이지 않았어요. 16 LED 정사각 격자를 깔자 변형이 평행사변형으로 기울어 보였어요. 원판에 지난 충돌의 금색을 섞었더니 남색과 섞여 회색이 돼서, 섞기를 빼고 밝기만 남겼어요. 튕김을 지름의 절반에서 4분의 3으로 키워 여는 자리의 멈춤을 1분 안으로 당겼어요. + +### Twist and Writhe + +주제 5 로 칼루가레아누-화이트-풀러 정리를 위상·매듭 갈래에 세웠어요. 닫힌 리본의 두 모서리가 얽힌 횟수 Lk 는 리본이 중심선을 감아 도는 꼬임수 Tw 와 중심선 자체의 뒤틀림수 Wr 의 합이에요(게오르게 칼루가레아누 1959, 제임스 화이트 1969, 브록 풀러 1971). 패널에서는 Lk 번 꼬인 원 리본이 돌면서 8자를 지나 106 LED 높이의 초나선으로 감겼다가 풀려요. 리본 앞면은 하늘색, 뒷면은 보라, 두 모서리는 분홍과 연두예요. 맨 아래 막대는 Wr 를 0 에서부터, Tw 를 Wr 가 끝난 자리에서부터 그려서 늘 Lk 눈금에서 멈춰요. Wr 는 매 프레임 144 점 다각형에 가우스 이중 적분을 클레닌과 랑고프스키(2000)의 닫힌 꼴로 돌려 구하고, Tw 는 지어진 리본에서 각을 하나씩 재요. Lk 3 부터 6 까지 Coil 3, 4.5, 6 으로 한 바퀴씩 돌려 보니, 두 모서리의 가우스 연결 적분은 늘 정확히 Lk 였고 Tw + Wr 는 Lk 에서 0.005 안에 있었어요. 중심선을 늘이는 첫 식은 감을수록 키가 줄어서 초나선이 60 LED 에 그쳤고, 반 바퀴당 Wr 도 0.39 였어요. 가닥이 축과 약 42도를 이루게 늘이자 106 LED 로 섰고, 반 바퀴당 Wr 도 0.74 가 됐어요. 반지름 45 원은 정면으로는 패널에 안 들어가서, 납작할 때는 시점이 비스듬한 채로 천천히 돌고 감겼을 때 빨리 돌아요. 처음 색은 앞면과 한쪽 모서리가 둘 다 초록빛이라 섞여서 앞면을 하늘색으로 옮겼어요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -2895,7 +2954,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 25일 `dev`(`f72355d`)의 328개에 43판부터 55판까지의 백서른 개를 더한 458개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 25일 `dev`(`f72355d`)의 328개에 43판부터 56판까지의 백마흔 개를 더한 468개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -2963,6 +3022,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 15판부터 22판까지의 여든 개는 어느 축의 끝도 넘지 않아서 칸 경계가 14판과 같아요. 씨앗을 타는 패턴은 씨앗 여럿으로 재서 칸을 확인해요. 15판의 Marbling 은 처음 판이 씨앗 열여섯 가운데 일곱만 겨눈 칸에 들었고, 연구의 씨앗이 그 일곱 가운데 가장 높았어요. 패턴의 slug 로 씨앗을 정하니 이름만 바꿔서 재면 돼요. 복잡도 축은 어두운 기울기의 끝자리 흔들림도 세요. 16판의 Fireflies 는 채널마다 4 의 배수로 반올림하자 눈에는 같은 화면의 복잡도가 0.490 에서 0.310 으로 내려갔어요. 거꾸로 17판의 Frost 는 22/255 아래로 누른 바탕에 채널마다 ±4.5 의 결을 넣어 복잡도가 1.712 에서 2.383 이 됐고, Spectrogram 은 같은 그림의 색표를 256칸에서 1024칸으로 늘려 2.151 에서 2.443 이 됐어요. deflate 는 같은 세 바이트가 거듭 나오는 것을 찾아서, 색이 몇 가지냐가 밝기보다 크게 들어요. 다른 여섯 축은 가장 밝은 채널로 재고 deflate 만 세 채널을 다 읽어요. 18판의 Zen Garden 은 볕 든 자갈에서 가장 밝은 빨강을 두고 초록과 파랑만 알갱이마다 ±15 흔들어서, 촘촘함과 가장자리는 그대로 둔 채 복잡도를 1.287 에서 1.618 로 올렸어요. 복잡도를 다른 축과 따로 고르는 손잡이예요. 내보내는 색을 끊는 단계는 가장자리를 움직여요. 같은 그림을 8 단계로 끊으면 이웃 차이가 0 아니면 8 이상이라 잔 차이가 다 무른 계단으로 들어서 가장자리가 0.454 였고, 6 단계로 끊으면 차이가 꼭 6 인 쌍이 셈에서 빠져 0.585 였어요. 19판에서는 잡음 값의 가짓수가 손잡이였어요. Needlepoint 는 털실 결을 약한 두 채널에 ±6 의 연속 값으로 흔들자 복잡도가 1.373 에서 2.164 로 뛰었고, -10, 0, +10 세 값을 네 LED 에 하나꼴로 넣자 1.888 이었어요. Rabbit Ears 의 회색 눈은 12 단계라 deflate 로 LED 당 0.60 바이트밖에 안 됐어요. 그 패턴의 복잡도는 눈이 아니라 그림 위에 얹힌 옅은 색 낟알에서 와요. 패널 가장자리의 부드러운 비네트 하나도 LED 마다 값을 달리 만들어 복잡도를 0.2 쯤 올렸어요. Smalti 는 15/255 아래의 줄눈에서 한 채널만 5~11 로 흔든 결이 복잡도를 0.14 올렸어요. 20판의 Moth Lamp 는 꺼진 LED 가 deflate 로 15 비트쯤, 켜진 LED 가 23.5 비트쯤이라 복잡도가 1.875 + 1.06 × 채움 언저리에 묶여 있었어요. 짙은 주황 등빛에서는 켜진 벽돌의 파랑이 24 아래에 머물러 2.1 에서 멈췄고, 등을 금빛 고압 나트륨으로 바꿔 켜진 LED 의 세 채널이 다 24 를 넘게 하자 채움 0.274 에서 2.201 이 됐어요. 21판에서는 내보내는 값을 끊는 단위가 결의 크기보다 센 손잡이였어요. Bread Mould 의 같은 화면이 끊지 않으면 2.870, 짝수로 끊으면 2.366, 3 단위로 끊으면 2.023 이에요. Pancakes 는 체커 결의 흔들림을 세 값에서 두 값으로 줄이자 다른 값은 그대로인 채 복잡도만 2.361 에서 2.276 으로 내려갔어요. 나무껍질과 숲을 LED 마다 다른 빛으로 그린 Woodpecker 는 연구의 씨앗으로 2.583 인데, 씨앗 열여섯으로 재면 2.727 까지 올라 축 끝 2.735 에 붙어요. +56판은 54판까지 든 458개에 남은 둘뿐인 칸 여섯 가운데, 같이 돈 54판이 겨눈 복잡도 × 채움 4,2 와 촘촘함 × 모양 바뀜 4,3 을 빼고 가장자리 × 복잡도 4,4 와 움직임 × 촘촘함 1,4 를 겨눴어요. 가장자리 × 복잡도 4,4 는 이웃 차이 가운데 40 을 넘는 몫이 0.79 를 넘으면서 한 화면이 deflate 로 LED 당 2.17 바이트 넘게 눌리는 칸이에요. 가장자리는 가장 밝은 채널로만 재고 복잡도는 세 채널을 다 눌러서, Dead Leaves 는 잎의 밝기를 45 씩 떨어진 네 단으로만 두고 색상은 잎마다 조금씩 흩었어요. 가장자리 1.000, 복잡도 2.337 예요. 움직임 × 촘촘함 1,4 는 1초에 바뀌는 밝기가 0.061 아래로 고요하면서 이웃 차이 평균이 0.297 을 넘는 칸이에요. Golomb Trominoes 는 LED 하나를 한 칸으로 깔고 빈칸만 걸려서, 한 걸음에 다시 깔리는 트로미노가 하나에서 63개뿐이에요. 움직임 0.023, 촘촘함 0.355 이에요. 두 패턴의 첫 판은 축의 끝을 넓혔어요. Golomb Trominoes 는 빈칸이 1초에 10 LED 를 걸어 움직임이 0.014 로 그 축의 아래 끝 0.019 보다 낮았고, 모양 바뀜도 0.005 였어요. Dead Leaves 는 다음 표본이 시작될 때 앞 표본 전체가 밝기 60 으로 가라앉아 모양 바뀜이 0.454 로 위 끝 0.413 을 넘었어요. 둘이 같이 들어가자 움직임 칸의 경계가 0.061, 0.305, 1.529 에서 0.048, 0.262, 1.434 로, 모양 바뀜 칸의 경계가 0.099, 0.214, 0.330 에서 0.099, 0.229, 0.360 으로 옮겨져서, 움직임 × 모양 바뀜 1,4 가 비고 촘촘함 × 모양 바뀜 4,4 에는 Ham Sandwich 하나만 남을 판이었어요. 빈칸을 1초에 18 LED 로 걷게 하고 앞 표본은 한 단만 낮춰 잿빛으로 비치게 하자, 움직임 0.023, 모양 바뀜 0.196 가 돼서 움직임 축과 모양 바뀜 축의 위 끝은 그대로였어요. Golomb Trominoes 의 모양 바뀜 0.008 만 그 축의 아래 끝을 0.016 에서 내려서, 모양 바뀜 칸의 경계가 0.099, 0.214, 0.330 에서 0.093, 0.211, 0.328 로 옮겨졌어요. 하나뿐이거나 빈 칸은 생기지 않았어요. 56판까지 든 468개에서 지도 21장은 다 `none`이고, 겨눈 두 칸에는 셋째가 섰어요. 둘뿐인 칸은 가장자리 × 촘촘함 1,3 과 1,4, 2,4, 촘촘함 × 모양 바뀜 4,4 넷이 남아요. + ### 아직 없는 갈래 `families.json` 의 쉰여섯 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 29판이 연 위상·매듭 갈래는 Hopf Fibration 과 31판의 Boy's Surface, 33판의 Borromean Rings, 34판의 Lakes of Wada, 36판의 Heawood Torus, 38판의 Sperner's Lemma, 39판의 Borsuk-Ulam, 44판의 Ham Sandwich, 46판의 Reeb Graph, 48판의 Möbius Energy, 49판의 Square Peg, 51판의 Persistence Barcode, 53판의 Belt Trick, 55판의 Universal Cover 예요. 28판이 열려다 접은 갈래라 28판의 Hairy Ball 은 노이즈·흐름에 그대로 있어요. 세잎 매듭은 48판의 Möbius Energy 가 처음 세웠어요. 29판은 화장품 팝업 갈래도 열었어요. 주제 6 의 몫이라 주제 2 로 세지 않았고, Cream Swirl, Serum Gel, Glitter Fall, Loose Powder 넷이 다 들어 있어요. 30판의 Swatch Row, Pearl Sheen, Bi-Phase, Satin Fold 가 그 갈래에 넷을 더했고, 31판의 Lip Gloss, Foam Lather, Setting Mist, Sponge Blend 가 넷을 더 더해 열둘이에요. 32판의 Cushion Mesh, Clay Mask, Sugar Scrub, Chrome Powder 가 넷을 더해 열여섯이고, 33판의 Cleansing Balm, Gold Leaf, Essence Drop, Peel-Off 가 넷을 더해 스물이에요. 34판의 Cat Eye, Velvet Nap, Mood Tint, Hair Gloss 가 넷을 더해 스물넷이고, 35판의 Bath Fizz, Toner Pad, Sheet Mask, Gilded Cream 이 넷을 더해 스물여덟이고, 36판의 Lotion Ribbon, Shimmer Oil, Blooming Gel, Whipped Soap 이 넷을 더해 서른둘이에요. 38판의 Milk Ring, Couette Shimmer, Photochromic, Serum Fingers 가 넷을 더해 서른여섯이에요. 39판의 Soap Film, Blotting Paper, Serum Drip, Crackle Polish 가 넷을 더해, 37판의 넷까지 마흔넷이에요. 41판은 할로윈 갈래를 열었어요. 주제 6 의 몫이라 주제 2 로 세지 않았고, Jack-o'-Lantern, Sheet Ghosts, Spider Drop, Cauldron, Eyes in the Dark 다섯이 들어 있어요. 28판이 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table, 33판의 Goldbach Comet, 35판의 Kolakoski, 36판의 Elliptic Chords, 39판의 Explicit Formula, 44판의 Bulgarian Solitaire, 48판의 Chebyshev's Bias, 49판의 Curlicue, 51판의 Sato-Tate, 53판의 Gaussian Periods, 55판의 Question Mark Function 이에요. 27판이 연 철도 갈래는 Funicular 하나이고, 필름·영화 갈래는 Horse in Motion 하나예요. 두 갈래는 주제 표가 수학 쪽으로 바뀌기 전에 열었어요. 그 뒤로 ROUND.md 는 사물로 묶는 갈래를 더 열지 않아요. 26판이 연 얼음·극지 갈래는 Iceberg 와 27판의 Sled Dogs 이고, 미생물 갈래는 Dark Field 와 27판의 Mitosis 예요. 25판이 연 농사·밭 갈래는 Rice Terraces 와 26판의 Rice Planting, 27판의 Center Pivot 이고, 화산 갈래는 Eruption 과 26판의 Geyser, 27판의 Lava Lake 예요. 24판이 연 공사·건설 갈래는 Tower Crane 과 25판의 Excavation, 26판의 Implosion 이에요. 24판이 연 도자기 갈래는 Potter's Wheel 과 25판의 Kintsugi, 26판의 Noborigama 예요. 23판이 연 무대·공연 갈래는 Proscenium 과 24판의 Flying Trapeze, 25판의 Marionette, 26판의 Shadow Play 예요. 23판이 연 짐승 갈래는 Giraffe 와 24판의 Emergence, 25판의 Snow Monkeys 예요. 22판이 연 몸 갈래는 Circulation 과 23판의 Axon, 24판의 Breath, 27판의 Fluoroscope 예요. 22판이 연 비행 갈래는 Liftoff 와 23판의 Final Approach, 24판의 Thermal, 25판의 Skydiver 예요. 20판이 연 새 갈래는 Kingfisher 와 21판의 Woodpecker, 22판의 Weaver, 23판의 Skein 이에요. 20판이 연 화학·실험 갈래는 Chromatography 와 21판의 Chemical Garden, 22판의 Flame Test, 23판의 Elephant Toothpaste 예요. 21판이 연 종이 갈래는 Pop-up Book 과 22판의 Senbazuru, 23판의 Hanging Scroll, 26판의 Origami 예요. 21판이 연 운동·경기 갈래는 Lap Pool 과 22판의 Bowling Lane, 23판의 High Dive, 26판의 Pole Vault, 27판의 Climbing Wall 이에요. 위아래로 움직이는 경기는 High Dive 와 Pole Vault 뒤로 27판에 실내 암벽이 섰어요. 19판이 연 부엌 갈래는 Popcorn 과 20판의 Kettle, 21판의 Pancakes 이고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판이 연 유리 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori, 27판의 Glassblower 예요. 광물·결정 갈래는 Bismuth 와 19판의 Dripstone, 20판의 Agate 이고, 놀이공원 갈래는 Drop Tower 와 19판의 Helter Skelter, 20판의 High Striker, 25판의 Ferris Wheel 이에요. 말이 오르내리는 회전목마는 아직 없어요. 17판이 연 전자 회로 갈래는 Circuit Board 와 18판의 Server Rack, 21판의 Nixie 이고, 장난감 갈래는 Marble Run 과 18판의 Peg Light, 21판의 Slinky, 22판의 Pin Art 예요. 16판이 연 바다 갈래는 Abyss 와 17판의 Shoreline, 20판의 Sunglint, 23판의 Bait Ball 이고, 곤충 갈래는 Fireflies 와 17판의 Ant Farm, 20판의 Moth Lamp, 24판의 Waggle Dance 예요. 15판이 연 도시 갈래는 Metro Map 과 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판이 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables, 24판의 Pipe Organ 이에요. 4판이 연 직물 갈래는 Jacquard Loom 과 15판의 Plait, 19판의 Needlepoint, 23판의 Sequins 이고, 8판이 연 기계 갈래는 Gear Train 과 15판의 Cam Wave, 19판의 Paternoster 예요. 12판이 연 천체 갈래는 Star Trails 와 15판의 Analemma, 16판의 Lunation, 22판의 Galilean Moons 예요. 위성의 궤도는 Galilean Moons 가 그렸고, 해를 도는 행성은 아직 없어요. 리듬·펄스 갈래는 1판의 Euclid Pulse 와 Pulse Bars 뒤로 20판의 Change Ringing, 32판의 Arnold Tongues, 35판의 Chimera State, 38판의 Infinity Series, 41판의 Siteswap, 44판의 Kac Ring, 46판의 Stochastic Resonance 가 섰어요. @@ -3021,6 +3082,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. +56판의 열 개는 가장 낮은 노브가 Elliptic Law 의 K1 Hue 와 Golomb Trominoes 의 K2 Speed 로 둘 다 0.05 이고, Twist and Writhe 의 K1 Hue 와 K3 Lk 0.06 이 그다음이에요. 노브 마흔 개가 다 0.03 을 넘어요. 고요한 그림은 노브가 화면에 늘 있는 것을 바꿔야 창 안에서 보였어요. Golomb Trominoes 의 깔기는 빈칸이 어디로 걸어도 판마다 190 칸 넘게 달라지지 않아서, Speed 노브는 처음에 0.010 이었어요. 바탕을 빈칸의 껍질로 칠해 빈칸이 큰 블록의 경계를 넘을 때 판 넓이의 색이 바뀌게 하자 0.05 가 됐어요. 가는 점과 선만 켜지는 그림은 이번에도 색 노브가 약했어요. Elliptic Law 는 채움이 0.317 이고 고윳값의 꼬리가 어두워서 Hue 가 0.05 이에요. 여러 노브가 정리의 다른 자리를 골라요. Erdős Discrepancy 의 K4 는 보웨인, 최, 쿤스 수열의 소수 3, 5, 7, 11, 13 을, Multiplicative Cascade 의 K4 는 형제 무게의 상관을 골라요. Twist and Writhe 의 K3 은 정수 Lk 라서 노브가 다섯 자리에서만 그림을 바꿔요. + ### 세로 64×128 2~8초 동안 켜진 LED 가 5% 아래인 3분의 1이 있는 패턴이 넷이에요. `study.ts` 표의 `thirds` 칸이 이 값이에요. Jelly 4.5% 와 Pulse Bars 2.6% 는 위 3분의 1이 비고, Sparks 2.0% 와 Dragon Fold 4.9% 는 아래 3분의 1이 비어요. Dragon Fold 는 문턱 바로 아래예요. 6판의 Jacob's Ladder 는 위, 가운데, 아래가 27.0%, 21.4%, 35.1% 예요. 햇빛층에서 심해까지 내려가는 바다는 16판의 Abyss 가 했고, 덩굴은 15판의 Ivy 가 했어요. 어두운 판 위의 가는 빛 하나는 128줄을 다 지나가도 3분의 1마다 10% 를 넘기 어려워요. 6판의 처음 아크는 가운데 3분의 1을 7.4% 켰어요. 9판의 Sprout 은 접시꽃 둘이 위 3분의 1을 8.1% 켜요. 이렇게 철이 도는 패턴은 2~8초가 어느 철에 걸리느냐에 따라 값이 몇 배씩 달라서, 씨앗 여럿으로 재 봐야 해요. 테두리를 그리는 패턴은 테두리를 빼고 재요. 10판의 Tetromino Well 은 양쪽 벽 2 LED 만으로 3분의 1마다 6.3% 를 켜서, 벽을 넣어 재면 위 3분의 1이 늘 문턱을 넘어요. 바탕을 다 그리는 패턴은 `thirds` 칸이 3분의 1마다 90 을 넘어요. 14판의 Waterfall 은 98/93/97 이에요. 그런 패턴은 2~8초 동안 밝기가 24/255 넘게 바뀌는 LED 로 세어야 그림이 128줄을 쓰는지 보여요. 그렇게 세면 Waterfall 의 위 3분의 1은 25.9% 예요. 15판의 Ivy 는 벽을 그려 `thirds` 가 84/89/82 이고, 바뀐 LED 로 세면 48.7%, 70.6%, 29.2% 예요. 가만히 있는 가는 선은 켜져 있어도 바뀌지 않아요. 15판의 Incense 는 곧은 연기 실이 가운데 3분의 1을 지나도 바뀐 LED 가 3.7% 였고, 소용돌이가 그 3분의 1 안에서 시작되게 하자 15.5% 가 됐어요. 16판의 Abyss 는 물을 다 칠해서 `thirds` 가 100/80/22 이고, 바뀐 LED 로 세면 84.5%, 59.3%, 36.0% 예요. 한밤층은 아귀와 불꽃, 잠수정 등빛뿐이라 아래 3분의 1이 가장 낮아요. 16판의 Harp 는 액자를 빼고 줄만 재면 48/53/12 이고, 바뀐 LED 는 아래 3분의 1이 9.8% 예요. 거기는 받침과 공명통 발이고, 줄은 300 LED 남짓한 삼각형에만 있어요. 17판의 Marble Run 은 레일과 리프트가 서 있어서 `thirds` 가 39/30/68 이고, 바뀐 LED 는 40.7%, 54.3%, 59.7% 예요. 17판의 Ant Farm 은 모래가 바탕이라 `thirds` 가 100/97/96 이고, 바뀐 LED 는 6.9%, 34.5%, 53.4% 예요. 위 3분의 1은 모래 윗면과 그 위의 어두운 유리라서 개미가 드나드는 입구와 더미만 바뀌어요. 18판의 Server Rack 은 앞판을 13~23 으로 24 아래에 두어서, `thirds` 63/63/61 이 다 드라이브와 코드와 LED 의 빛이에요. 바뀐 LED 셈은 2~8초 가운데 한 프레임만 24 를 넘어도 그 LED 를 세서, 무작위 반짝임이 값을 부풀려요. 18판의 Pint 는 잔 기포 반짝임을 켜면 50.3%, 60.2%, 40.0% 이고 끄면 50.2%, 46.8%, 26.3% 예요. 빛의 흔들림도 그래요. Longcase Clock 은 촛불일 때 41.8%, 45.6%, 45.3% 이고 흔들림 없는 달빛에서는 2.6%, 4.2%, 5.9% 예요. 자라는 그림은 미리 돌리기를 시간 대신 상태로 멈추면 씨앗마다 같은 자리에서 열려요. 18판의 Bismuth 는 무리 꼭대기가 20번 줄에 닿을 때 멈춰요. 14번 줄까지 기다리면 2초에 이미 다 자라 있어서 모양 바뀜이 0.027 로 떨어졌어요. 19판의 Dripstone 은 잘린 암반이 4/255 쯤이라 위아래가 검게 보여 `thirds` 가 68/99/77 이었고, 암반에 제 빛을 주자 92/98/99 가 됐어요. Foxfire 는 땅을 80번 줄에 두었을 때 위 3분의 1이 5% 였고, 64번 줄로 올리고 그루터기를 세우자 16% 가 됐어요. 쌓이는 그림은 2~8초에 쌓이는 자리가 보이게 열어야 해요. Popcorn 은 더미가 높게 시작하면 아래 3분의 1의 바뀐 LED 가 4.4% 였고, 낮게 시작해 자라게 하자 27.9% 가 됐어요. 넓은 면을 물들이는 조명은 반짝임 없이도 바뀐 LED 를 올려요. Dripstone 은 세 조명이 15초에 한 바퀴 번갈아 켜져서 57.6%, 84.9%, 79.2% 예요. 20판에서는 한 번 일어나는 일이 2~8초 창에 드는지가 값을 갈랐어요. High Striker 는 7.3초에 종이 한 번 울려서 위 3분의 1의 바뀐 LED 가 6.5% 에서 95.6% 가 됐고, Kettle 은 7.1초에 스위치가 꺼지는 것이 창에 들어 모양 바뀜이 0.065 에서 0.168 이 됐어요. 위로 오르는 앞선은 모양 바뀜을 적게 내요. Chromatography 는 6초 동안 앞선이 26줄을 올라도 블록이 16줄 높이라 0.040 이었어요. 21판의 세로 일곱은 Nixie 를 빼고 방이나 바탕을 다 그려서 `thirds` 가 3분의 1마다 70 을 넘었어요. 그런 그림은 바탕을 지우거나 멈추고 한 번 더 재야 주인공이 128줄을 쓰는지 보여요. Chemical Garden 은 유리와 방을 검게 지우자 정원만의 위 3분의 1이 6.6% 였고, 씨앗 열여섯 가운데 하나는 3.2% 였어요. Lap Pool 은 바닥 햇빛 무늬의 시간을 멈추자 바뀐 LED 가 3분의 1마다 18.7~52.1% 였어요. Woodpecker 는 새를 빼고 재도 바뀐 LED 가 57.3/35.1/50.0% 라서, 그 값은 거의 다 잎 사이로 움직이는 햇빛 반점에서 와요. 조금 위에서 본 장면은 세로를 싸게 얻어요. Pop-up Book 은 누운 페이지가 깊이의 5분의 1 줄만 써서, 책을 아래 34줄에 두고도 선 카드가 위 3분의 1까지 닿아요. 닉시관 하나는 지름이 높이의 절반쯤이라 Nixie 의 IN-18 은 54 LED 너비에 109줄로 서요.Slinky 는 계단과 벽을 빼고 스프링과 그림자만 세면 켜진 LED 가 0.4/2.6/3.0% 이고, 스프링이 켠 계단등까지 세면 20.9/16.5/8.6% 예요. 22판의 세로 다섯 가운데 넷은 바탕을 다 칠해서 `thirds` 가 말해 주는 것이 적었어요. 바탕을 검게 지운 사본으로 다시 재면 Liftoff 는 로켓만의 위 3분의 1이 켜진 LED 3.7%, 바뀐 LED 8.2% 라서, 그 3분의 1의 변화는 대부분 카메라가 고개를 들며 어두워지는 하늘에서 와요. Bowling Lane 은 공을 던지지 않게 하면 바뀐 LED 가 5.2/9.5/6.6% 로 떨어져서, 그 값은 거의 다 공과 그 빛이에요. Circulation 은 바탕을 21 아래로 내려 몸만 켜진 LED 로 세게 해서 `thirds` 가 34/50/26 이에요. 남은 자리로 짠 Flame Test 는 실험대가 어두워 아래 3분의 1의 바뀐 LED 가 4.6% 였어요. 불꽃빛의 몫을 0.42 에서 0.52 로 올려도 1.3 포인트만 올라서, 어두운 바닥은 빛을 비추는 것보다 2~8초 창 안에 그 자리로 가는 움직임이 있어야 바뀐 LED 가 올라요. 23판의 세로 일곱 가운데 Proscenium 은 금박 아치가 세 3분의 1을 다 지나가서, 아치를 검게 바꿔 다시 재면 `thirds` 가 94/97/69 에서 63/85/65 가 돼요. Final Approach 는 위 3분의 1을 밤하늘이 채워서, 도시 빛을 받은 천정을 33/255 로 두어 문턱 24 를 넘겼어요. 가는 선 하나가 지나가는 3분의 1은 둘레를 같이 밝혀야 해요. Axon 의 가운데 3분의 1은 축삭 한 줄이 지나가는 자리라서, 수초의 빛과 비스듬히 지나가는 이웃 축삭 둘을 두어 바뀐 LED 가 23.6~35.5% 가 됐어요. 한 번 일어나는 일은 2~8초 창에 들게 짰어요. Hanging Scroll 은 첫 그림이 걸려 있는 시간을 2.2초로 줄여 4.8~6.8초에 말려 올라가게 했고, Giraffe 는 첫 물 마시기를 3.4초에 두었어요. Elephant Toothpaste 의 아래 3분의 1은 바뀐 LED 가 9.7% 로 일곱 가운데 가장 낮아요. 기둥이 6.8초에 꺾이기 전까지는 실린더와 실험대가 서 있기만 해서예요. 24판의 세로 일곱 가운데 다섯은 바탕을 다 칠해서 바뀐 LED 로 봤어요. 23판이 물은 어두운 하늘의 떼는 Emergence 가 답했어요. 하늘을 지우고 박쥐만 세면 3분의 1마다 13.4~13.6, 9.9~10.2, 5.1~5.3% 가 켜져서, 기둥이 좁은 아래 3분의 1은 문턱을 겨우 넘어요. 2~8초 동안 바뀐 LED 로 세면 아래 3분의 1도 23.5~25.0% 예요. 자라는 것의 키가 위 3분의 1을 정해요. Potter's Wheel 은 그릇을 64~90줄로 빚을 때 씨앗 넷 가운데 둘이 위 3분의 1의 바뀐 LED 0.6% 와 0.0% 였고, 76~92줄로 키우자 열여섯 모두 15.2% 를 넘었어요. 바탕의 움직임을 멈추고 다시 재면 주인공의 몫이 보여요. Tower Crane 은 구름을 멈춰도 바뀐 LED 가 45.2/54.1/36.3% 이고, Pipe Organ 은 장미창 빛을 꺼도 3분의 1마다 18.2% 를 넘어요. Flying Trapeze 는 `thirds` 가 81/35/34 로 일곱 가운데 가장 고르지 않지만, 팔로 스폿 빛줄기가 연무 속을 쓸어서 바뀐 LED 는 93.9/87.9/57.5% 예요. 25판의 세로 여섯 가운데 넷은 바탕을 다 칠해서 `thirds` 가 3분의 1마다 97 을 넘었고, 바탕을 멈추거나 지운 사본으로 다시 쟀어요. Eruption 은 하늘의 불빛과 부푸는 지층을 멈춰도 바뀐 LED 가 56.4~67.4/39.1~55.5/15.7~17.7% 이고, 아래 3분의 1의 값은 마그마 방의 대류와 화도를 오르는 가스 덩어리에서 와요. 빨강이 255 에 붙은 불빛은 가장 밝은 채널로 재면 결이 보이지 않아요. 플리니식에서 마그마 방이 밝기 끝에 붙자 아래 3분의 1의 바뀐 LED 가 5.3% 였고, 방의 온도가 기둥의 맥박을 따라 0.16 오르내리게 하자 17.7% 가 됐어요. Excavation 은 구름을 멈추면 24.3/12.5/15.3%, Skydiver 는 구름과 그림자를 멈추면 26.7/66.2/23.7% 예요. Rice Terraces 는 하늘 36줄을 빼도 위 3분의 1의 바뀐 LED 가 12.5% 라서, 맨 위 논들의 구름 반사가 그 3분의 1을 움직여요. 가는 선도 쓸고 지나가면 바뀐 LED 를 많이 켜요. Marionette 는 무대를 검게 지우면 인형과 실, 조종대가 켜는 LED 가 20.5/18.1/10.4% 인데 바뀐 LED 는 67.3/76.8/64.2% 예요. 조종대가 기울 때마다 실 일곱이 옆으로 쓸려서, 15판 Incense 의 곧은 연기 실이 3.7% 였던 것과 반대예요. 물에 비친 그림은 아래 3분의 1을 싸게 채워요. Ferris Wheel 은 위 장면을 0.72 로 눌러 41줄 호수에 비춰서 아래 3분의 1의 바뀐 LED 가 80~81% 예요. 이야기가 있는 데이터 그림은 여는 해가 세로를 정해요. Population Pyramid 는 1820년에서 열 때 막대가 0~78살에만 있어서, `thirds` 13/20/40 의 위 3분의 1이 연도와 라벨뿐이었어요. 라벨과 눈금을 지운 사본의 위 3분의 1은 1975년에서 열면 5.6%, 1985년 6.2%, 1995년 7.3%, 2000년 7.8% 여서 1995년에서 열어요. 26판의 세로 여섯 가운데 다섯은 바탕을 다 칠해서 `thirds` 가 3분의 1마다 79 이상이었고, 바탕을 지운 사본으로 다시 쟀어요. 단면을 물높이나 땅에서 자르면 주인공이 세 3분의 1에 다 걸쳐요. Iceberg 는 물높이를 30번 줄에 두어 빙산의 9분의 8이 물 아래에 서서, 하늘과 물을 지워도 빙산만으로 켜진 LED 가 30/67/30% 예요. Geyser 는 땅속 화도와 물방이 아래 3분의 1을 맡아서, 하늘과 땅을 지운 사본의 아래 3분의 1이 13.6% 켜지고 14.2~14.7% 바뀌어요. 가마 불꽃은 Eruption 의 마그마 방과 같은 문턱에 걸렸어요. Noborigama 의 흰 열 칸은 빨강이 255 에 붙고 일렁임이 온도로만 갈 때 2~8초 동안 바뀐 LED 가 0.8% 와 4.2% 였어요. 빨강을 232 에서 멈추고 밝기를 0.72~1 배로 흔들자 96.9% 와 97.0% 가 됐어요. 가장 밝은 채널이 끝에 붙은 불빛은 색만 흔들어서는 바뀐 LED 로 잡히지 않아요. 위에서 내려다본 판은 3분의 1마다 맡을 것을 따로 두어야 해요. Rice Planting 은 심은 모가 위, 일꾼 줄이 가운데, 모판과 못자리가 아래를 맡아서 물을 지운 사본의 켜진 LED 가 15/16~18/40% 예요. 못자리까지 지우면 아래 3분의 1이 3.9~5.0% 라서, 논둑 길에 모판 둘을 놓아 5.7~6.7% 로 올렸어요. 한 번 일어나는 일은 이번에도 2~8초 창에 두었어요. Implosion 은 2.4초에 발파해 7초쯤 먼지가 꼭대기에 닿고, Pole Vault 는 1.97초에 달려 나가 5.14초에 가로대 위 정점을 찍어요. 가는 선이 쓸고 지나가는 그림은 켜진 LED 가 적어도 바뀐 LED 가 많아요. Pole Vault 는 경기장을 지우면 선수와 장대가 위 3분의 1을 1.4~1.7% 만 켜지만, 2~8초 동안 바뀐 LED 는 22.8~23.2% 이고 장대가 휘며 쓰는 가운데 3분의 1은 92.8% 예요. 27판의 Funicular 는 그 물음을 케이블로 다시 쟀어요. 바탕을 지우고 케이블만 남기면 2~8초 동안 바뀐 LED 가 2.7%, 3.3%, 0.3% 이고, 객차와 함께 두면 케이블이 보태는 몫은 위 3분의 1의 0.1%p 예요. 제자리에서 길이 방향으로 꼬임만 미끄러지는 선은 제 밑의 LED 만 바꿔요. 가는 선은 패널을 가로질러 쓸고 가야 바뀐 LED 를 싸게 얻어요. Climbing Wall 은 홀드를 높이 전체에 고르게 박아도 사람과 로프만 재면 켜진 LED 가 2.1%, 4.1%, 6.1% 로 Pole Vault 와 같은 자리였어요. 키 20줄 남짓한 사람 하나는 어느 3분의 1에 있든 그 칸을 몇 % 밖에 켜지 못하고, 그 사람을 따라 오르는 스폿 빛이 바뀐 LED 를 22% 언저리로 올려요. Glassblower 는 덩어리가 105번 줄까지만 내려와 아래 3분의 1이 1.1% 였고, 잘린 조각이 떨어져 부서지는 컬릿 더미를 바닥에 깔자 41% 가 됐어요. 떨어지는 것이 닿을 바닥을 그리는 것이 아래 3분의 1을 채우는 가장 싼 길이었어요. @@ -3077,9 +3140,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 55판에서는 Imaginary Geometry 와 Tissot's Indicatrix, Universal Cover 가 128줄을 써요. Imaginary Geometry 는 아래 가운데 한 점에서 떠난 흐름선이 위 가운데 한 점까지 판을 세로로 건너서, 뒤에 깐 방향 색을 22/255 로 낮춰 선만 세어도 `thirds` 가 33/37/34 예요. Tissot's Indicatrix 는 위도 ±85.05도를 128줄에 놓은 메르카토르라 원이 위아래 끝에서 가장 커서 41/72/48 이고, 경위선을 빼고 원만 세어도 33/60/40 이에요. Universal Cover 는 피복 공간을 바닥 공간 위에 그리는 그대로 나무를 위에, 8자 고리를 아래에 두어 20/21/15 예요. Burrows-Wheeler(82/81/81)와 Vortex Sheet(100/100/100), Toussaint-Wilczek(67/68/63), Question Mark Function(32/61/61), Hex Theorem(27/64/30), Vuza Canon(24/24/24), Buffon's Noodle(21/18/18)도 세 칸이 다 5 를 넘지만 주제 4 로 세지 않았어요. Vuza Canon 은 길잡이 줄을 빼면 6/7/8 이에요. +56판에서는 Tree Peano Curve 와 Bloch Oscillations, Andrews Curves, Elliptic Law, Erdős Discrepancy 가 128줄을 써요. Tree Peano Curve 는 곡선이 LED 를 다 지나서 `thirds` 가 98/99/99 예요. 메우는 순서가 아래에서 위로 가서, 세운 패널이 곡선의 시간축이 돼요. Bloch Oscillations 는 시간이 한 줄에 0.75 씩 쌓여 렌즈 둘 반이 패널에 서고 59/58/58 예요. Andrews Curves 는 t 가 −π 에서 π 까지 128줄을 내려가서 45/51/44 이고, Erdős Discrepancy 는 한 걸음이 한 줄이라 84/96/92 예요. Elliptic Law 는 실수축을 세로로 세워 τ 1/3 의 타원이 반축 비 2 로 패널에 꼭 맞아서 26/41/26 예요. 타원은 위아래 끝이 좁아서, 꼬리를 두 배로 늘이기 전에는 21/30/21 이었어요. Twist and Writhe 의 초나선은 106 LED 로 서지만 리본만 켜져서 16/17/10 예요. + ### 낯선 수학 -주제 5 로 쓴 수학과 다음에 해 볼 수학이에요. 28판이 처음 열 개를 썼어요. 슈람-뢰브너 진화(Loewner Trace), 포드 원(Ford Circles), 털 난 공 정리(Hairy Ball), 아즈텍 다이아몬드와 북극권(Aztec Diamond), 부니모비치 경기장(Stadium Billiard), 라우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 다면체와 E8(Gosset Polytope), 콘웨이의 보고 말하기 수열(Look and Say), 위토프 게임의 그런디 값(Wythoff Nim), 쌍곡 평면의 띠 모형(Hyperbolic Band)이에요. 29판은 여섯을 더했어요. 클라인의 j 불변량(J-Invariant), 호프 올뭉치(Hopf Fibration), 다이슨 브라운 운동(Dyson Gas), 가케야 바늘 문제의 페론 나무(Perron Tree), 임계선 위의 리만 제타 함수(Zeta Loops), 길버트-섀넌-리즈 리플과 베이어-디아코니스의 일곱 번(Seven Shuffles)이에요. 30판도 여섯을 더했어요. 세 간격 정리(Three Gaps), 호프스태터 나비와 TKNN 정수(Hofstadter Butterfly), 노이만의 외부 당구(Outer Billiards), 볼드의 탄도 퇴적과 KPZ 앞면(Ballistic Deposition), 아르놀트의 미앤더(Meander), 브로드벤트와 해머슬리의 스미기(Percolation)예요. 31판도 여섯을 더했어요. 마스킷 조각의 클라인 군 극한 집합(Indra's Pearls), 리틀우드 다항식의 근(Littlewood Roots), 고든의 가우스 해자(Gaussian Moat), 아르놀트의 고양이 사상(Cat Map), 자부스키와 크루스칼의 KdV 솔리톤(KdV Solitons), 보이 곡면(Boy's Surface)이에요. ROUND.md 의 「낯선 수학 예」 여덟 가운데 쓴 것은 슈람-뢰브너 진화와 아즈텍 다이아몬드, 모듈러 형식, 호프 올뭉치, 세 간격 정리, 외부 당구 여섯이었고, 31판이 리틀우드 다항식의 근과 클라인 군의 극한 집합을 써서 여덟을 다 썼어요. 32판도 여섯을 더했어요. 탤벗 효과와 양자 카펫(Talbot Carpet), 길브레스 추측의 표(Gilbreath Table), 원 사상의 아르놀트 혀(Arnold Tongues), 콘웨이와 라딘의 핀휠 타일링(Pinwheel Tiling), 윌킨슨과 윌렘센의 침입 스미기(Invasion Percolation), 울람의 문제와 해머슬리 과정(Hammersley Lines)이에요. 33판도 여섯을 더했어요. 에인절, 홀로이드, 로믹, 비라그의 무작위 정렬 네트워크(Sorting Network), 베르트랑의 역설(Bertrand Paradox), 펜로즈의 비출 수 없는 방(Penrose Room), 당들랭 구(Dandelin Spheres), 테이트의 보로메오 고리(Borromean Rings), 골드바흐 혜성(Goldbach Comet)이에요. 34판도 여섯을 더했어요. 톰슨 문제와 오일러가 남기는 다섯 이웃 열둘(Thomson Problem), 켈빈의 배 물결(Kelvin Wake), 요네야마가 낸 와다의 호수(Lakes of Wada), 샤이데거의 강 그물(Scheidegger Rivers), 모저의 소파 문제와 해머슬리의 소파(Moving Sofa), 크누스의 춤추는 링크로 푼 펜토미노 덮기(Pentomino Cover)예요. 35판도 여섯을 더했어요. 뢸로의 폭이 일정한 곡선(Constant Width), 렌니의 주차 문제(Rényi Parking), 카리의 비주기 왕 타일(Kari-Culik Tiles), 콜라코스키 수열(Kolakoski), 쿠라모토와 바톡토흐의 키메라 상태(Chimera State), 주콥스키 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했어요. 그레이슨의 정리가 말하는 곡선 단축 흐름(Curve Shortening), 헤우드의 일곱 색 토러스 지도(Heawood Torus), 슈바르츠 랜턴(Schwarz Lantern), 윌브러햄과 깁스의 현상(Gibbs Phenomenon), 타원 곡선의 현과 접선(Elliptic Chords), 지먼의 파국 기계(Catastrophe Machine)예요. 37판도 여섯을 더했어요. 포천의 쓸기 선 알고리즘(Fortune's Sweep), 앙투안의 목걸이(Antoine's Necklace), 카르만 소용돌이 길의 안정성(Kármán Street), 단단한 원판의 헥사틱 상(Hexatic Phase), 존슨-멜 쪽매와 아브라미 법칙(Johnson-Mehl), 쿠라모토-시바신스키 방정식(Kuramoto-Sivashinsky)이에요. 38판도 여섯을 더했어요. 앤더슨 국소화(Anderson Localization), 무어가 찾고 Chenciner 와 Montgomery 가 증명한 세 몸 문제의 8자 해(Figure-Eight Orbit), 예센과 빈트너, 에르되시의 베르누이 합성곱(Bernoulli Convolutions), 브라우어르의 고정점 정리를 증명하는 슈페르너 보조정리(Sperner's Lemma), 에겐베르거와 폴리아의 항아리(Pólya Urn), 뇌르고르의 무한 수열(Infinity Series)이에요. 39판도 여섯을 더했어요. 고든, 웹, 월퍼트의 등스펙트럼 북(Isospectral Drums), 폰 망골트의 꼴로 쓴 리만의 명시 공식(Explicit Formula), 요한 베르누이의 최단 강하선(Brachistochrone), 슈타이너의 포리즘(Steiner Chain), 모런 집단으로 그린 킹먼의 합류(Kingman Coalescent), 보르수크-울람 정리(Borsuk-Ulam)예요. 40판도 여섯을 더했어요. 앨더스의 브라운 연속 랜덤 나무(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 함수(Hydrogen Orbital), 젤도비치 근사의 팬케이크(Zeldovich Pancakes), 망델브로가 내다보고 롤러, 슈람, 베르너가 증명한 브라운 운동의 바깥 경계(Brownian Frontier), 복소 긴즈부르크-란다우 방정식의 결함 난류(Defect Turbulence), 루퍼트 왕자의 정육면체(Prince Rupert's Cube)예요. 41판은 다섯을 더했어요. 홀과 왜건의 현수선 길과 정다각형 바퀴(Square Wheels), 퐁슬레의 포리즘(Poncelet Porism), 슐레플리의 4차원 정다포체를 자른 단면(Polytope Slices), 클림켁과 티먼, 마그누손의 사이트스왑(Siteswap), Dudeney 의 잡화상 퍼즐과 월리스-보여이-게르비엔 정리(Hinged Dissection)예요. 42판은 다섯을 더했어요. 바른스도르프 규칙으로 도는 기사 여행(Knight's Tour), 페르미와 파스타, 울람, 칭구의 되돌아옴(FPUT Recurrence), 포슬리에-립킨 반전기(Peaucellier Linkage), 몰리의 삼등분선 정리(Morley Triangle), 랄과 마드라스, 소칼의 피벗 알고리즘으로 돌린 자기 회피 걸음(Pivot Walk)이에요. 43판은 다섯을 더했어요. 월리스 워커의 칼레이도사이클(Kaleidocycle), 쌍곡형에서 록소드롬형으로 감기는 뫼비우스 변환의 흐름(Möbius Loxodrome), 터트의 용수철 정리(Tutte Embedding), 디펀트와 크래비츠의 고리 정리를 품은 히토메자시 사시코(Hitomezashi), 룽게 현상과 체비쇼프 점(Runge Phenomenon)이에요. 44판은 다섯을 더했어요. 마크 카츠의 고리 모형(Kac Ring), 스톤과 튜키의 햄 샌드위치 정리(Ham Sandwich), 브란트의 불가리아 솔리테어(Bulgarian Solitaire), 엘스켄스와 프리시의 탄도 소멸(Ballistic Annihilation), 페레그린의 솔리톤(Peregrine Soliton)이에요. 45판은 다섯을 더했어요. 로건-셰프와 베르시크-케로프의 극한 곡선으로 다가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도브루신 경계 조건을 건 임계 이징 모형의 SLE(3) 경계(Ising Interface), 프리예제프와 동료들의 오일러 걷개로 쌓는 로터 라우터 응집과 레빈-페레스의 스매시 합(Rotor-Router), 폴리아의 과수원 문제(Pólya's Orchard), 넬슨의 평면 색칠 문제와 아이즈벨의 일곱 색(Hadwiger-Nelson)이에요. 46판은 다섯을 더했어요. 브릴루앙의 구역(Brillouin Zones), 모스 이론과 레브의 그래프(Reeb Graph), 로렌츠 기체와 시나이의 당구(Lorentz Gas), 리언 글래스의 점무늬(Glass Patterns), 벤치와 수테라, 불피아니의 확률 공명(Stochastic Resonance)이에요. 47판은 다섯을 더했어요. 오키프와 홍, 스트로가츠의 스워멀레이터(Swarmalators), 빌스의 외로운 달리기 선수 추측(Lonely Runner), 샤츠의 올로이드와 그 전개도(Oloid), 쾨베의 원 채우기 정리와 콜린스-스티븐슨의 반복(Koebe Packing), 콘웨이의 병사와 황금비 무게(Conway's Soldiers)예요. 48판은 다섯을 더했어요. 오하라의 매듭 뫼비우스 에너지(Möbius Energy), 체비쇼프의 편향과 루빈스타인-사르낙의 밀도(Chebyshev's Bias), 섀든과 레키엔, 마스든의 이중 소용돌이 능선(Double Gyre), 아자리와 프로스트의 깜빡이는 래칫(Flashing Ratchet), 망델브로의 프랙탈 스미기(Fractal Percolation)예요. 49판은 다섯을 더했어요. 뒤러의 전개 문제와 셰퍼드의 추측(Dürer's Unfolding), 베리와 골드버그가 재규격화한 세타 합의 꼬부랑 곡선(Curlicue), 오일러 방정식과 푸앵소의 폴호드로 본 테니스 라켓 정리(Tennis Racket), 켐프의 사슬과 네 색 정리(Kempe Chains), 퇴플리츠의 내접 정사각형 문제와 그린-로브의 직사각형(Square Peg)이에요. 50판은 다섯을 더했어요. 크레이치넌과 배철러의 2차원 난류가 거꾸로 가는 캐스케이드와 맥윌리엄스의 소용돌이(Inverse Cascade), 사포발과 로소, 구예의 확산 앞줄(Diffusion Front), 필리와 마르케티, 케이츠와 타이외르의 운동성 상분리(Motility Clusters), 박과 스네펜의 진화 모형(Bak-Sneppen), 리세의 영원한 가속(Risset Rhythm)이에요. 51판은 다섯을 더했어요. 스미스와 마이어스, 캐플런, 굿먼-스트라우스의 모자 타일과 H/T/P/F 메타타일 치환(Hat Monotile), 반도와 동료들의 최적 속도 모형과 스기야마의 원형 트랙 실험(Phantom Jam), 사토와 테이트의 법칙과 타원곡선의 a_p(Sato-Tate), 칼라비와 야우의 공간에서 핸슨이 잘라 그린 5차 페르마 곡선(Calabi-Yau Quintic), 에델스브루너와 레처, 조모로디언의 지속 호몰로지와 바코드(Persistence Barcode)예요. 52판은 다섯을 더했어요. 슈람과 셰필드가 등고선을 SLE(4) 로 보인 2차원 가우스 자유장(Gaussian Free Field), 당튀와 마즈무다르, 베링거의 광탄성 힘 사슬(Force Chains), 앤스컴의 사중주와 마테이카, 피츠모리스의 같은 통계 담금질(Anscombe's Quartet), 드 브라윈 수열과 BEST 정리(de Bruijn Sequence), 라인스의 베타 평면 띠 제트(Zonal Jets)예요. 53판은 열을 더했어요. 듀크, 가르시아, 루츠가 그린 가우스 주기(Gaussian Periods), 부르가 보인 현수면과 나선면의 휨(Catenoid-Helicoid), 디랙의 벨트 묘기(Belt Trick), 헬름홀츠와 러브의 연기 고리 개구리뜀(Leapfrogging Vortices), 브룩스, 스미스, 스톤, 텃의 정사각형 분할(Squared Rectangle), 포드와 풀커슨의 최대 흐름 최소 절단(Max-Flow Min-Cut), 코스타스 배열(Costas Array), 바라니, 비어든, 칸의 무게중심 분할(Barycentric Subdivision), 무코파디아야와 크네저의 네 꼭짓점 정리(Four-Vertex Theorem), 오일러의 엘라스티카(Euler's Elastica)예요. 54판은 열을 더했어요. 베리의 무작위 파동과 보고몰니, 슈미트가 스미기로 센 마디 영역(Nodal Domains), 피어험버트가 사인 흐름에서 찾은 이상 고유 모드(Strange Eigenmode), 피어시 적분과 베리, 업스틸의 첨점 회절 파국(Pearcey Cusp), 나겔과 바이스의 STIT 모자이크(STIT Tessellation), 레비의 아크사인 법칙(Arcsine Law), 스퇴르메르가 다룬 쌍극자 장 속 대전 입자(Størmer Trap), 게일과 섀플리의 지연 수락(Stable Marriage), 스메일의 말굽(Smale Horseshoe), 울람 수열과 슈타이너베르거가 찾은 숨은 진동수(Ulam Sequence), 위어와 펠런의 거품과 켈빈의 깎은 팔면체(Weaire-Phelan)예요. 55판은 열을 더했어요. 밀러와 셰필드의 허수 기하(Imaginary Geometry), 티소의 지시 타원과 메르카토르 투영(Tissot's Indicatrix), 민코프스키의 물음표 함수(Question Mark Function), 버로스와 휠러의 변환(Burrows-Wheeler), 뷔퐁의 바늘과 바르비에의 국수(Buffon's Noodle), 부자의 리듬 타일링 캐넌(Vuza Canon), 하인과 내시의 헥스와 게일의 헥스 정리(Hex Theorem), 8자 고리의 범피복과 자유군의 케일리 그래프(Universal Cover), 오브친니코프와 젤도비치, 투생과 윌첵의 확산 제한 소멸(Toussaint-Wilczek), 헬름홀츠와 켈빈의 와류 시트와 크라스니의 와동 덩이(Vortex Sheet)예요. +주제 5 로 쓴 수학과 다음에 해 볼 수학이에요. 28판이 처음 열 개를 썼어요. 슈람-뢰브너 진화(Loewner Trace), 포드 원(Ford Circles), 털 난 공 정리(Hairy Ball), 아즈텍 다이아몬드와 북극권(Aztec Diamond), 부니모비치 경기장(Stadium Billiard), 라우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 다면체와 E8(Gosset Polytope), 콘웨이의 보고 말하기 수열(Look and Say), 위토프 게임의 그런디 값(Wythoff Nim), 쌍곡 평면의 띠 모형(Hyperbolic Band)이에요. 29판은 여섯을 더했어요. 클라인의 j 불변량(J-Invariant), 호프 올뭉치(Hopf Fibration), 다이슨 브라운 운동(Dyson Gas), 가케야 바늘 문제의 페론 나무(Perron Tree), 임계선 위의 리만 제타 함수(Zeta Loops), 길버트-섀넌-리즈 리플과 베이어-디아코니스의 일곱 번(Seven Shuffles)이에요. 30판도 여섯을 더했어요. 세 간격 정리(Three Gaps), 호프스태터 나비와 TKNN 정수(Hofstadter Butterfly), 노이만의 외부 당구(Outer Billiards), 볼드의 탄도 퇴적과 KPZ 앞면(Ballistic Deposition), 아르놀트의 미앤더(Meander), 브로드벤트와 해머슬리의 스미기(Percolation)예요. 31판도 여섯을 더했어요. 마스킷 조각의 클라인 군 극한 집합(Indra's Pearls), 리틀우드 다항식의 근(Littlewood Roots), 고든의 가우스 해자(Gaussian Moat), 아르놀트의 고양이 사상(Cat Map), 자부스키와 크루스칼의 KdV 솔리톤(KdV Solitons), 보이 곡면(Boy's Surface)이에요. ROUND.md 의 「낯선 수학 예」 여덟 가운데 쓴 것은 슈람-뢰브너 진화와 아즈텍 다이아몬드, 모듈러 형식, 호프 올뭉치, 세 간격 정리, 외부 당구 여섯이었고, 31판이 리틀우드 다항식의 근과 클라인 군의 극한 집합을 써서 여덟을 다 썼어요. 32판도 여섯을 더했어요. 탤벗 효과와 양자 카펫(Talbot Carpet), 길브레스 추측의 표(Gilbreath Table), 원 사상의 아르놀트 혀(Arnold Tongues), 콘웨이와 라딘의 핀휠 타일링(Pinwheel Tiling), 윌킨슨과 윌렘센의 침입 스미기(Invasion Percolation), 울람의 문제와 해머슬리 과정(Hammersley Lines)이에요. 33판도 여섯을 더했어요. 에인절, 홀로이드, 로믹, 비라그의 무작위 정렬 네트워크(Sorting Network), 베르트랑의 역설(Bertrand Paradox), 펜로즈의 비출 수 없는 방(Penrose Room), 당들랭 구(Dandelin Spheres), 테이트의 보로메오 고리(Borromean Rings), 골드바흐 혜성(Goldbach Comet)이에요. 34판도 여섯을 더했어요. 톰슨 문제와 오일러가 남기는 다섯 이웃 열둘(Thomson Problem), 켈빈의 배 물결(Kelvin Wake), 요네야마가 낸 와다의 호수(Lakes of Wada), 샤이데거의 강 그물(Scheidegger Rivers), 모저의 소파 문제와 해머슬리의 소파(Moving Sofa), 크누스의 춤추는 링크로 푼 펜토미노 덮기(Pentomino Cover)예요. 35판도 여섯을 더했어요. 뢸로의 폭이 일정한 곡선(Constant Width), 렌니의 주차 문제(Rényi Parking), 카리의 비주기 왕 타일(Kari-Culik Tiles), 콜라코스키 수열(Kolakoski), 쿠라모토와 바톡토흐의 키메라 상태(Chimera State), 주콥스키 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했어요. 그레이슨의 정리가 말하는 곡선 단축 흐름(Curve Shortening), 헤우드의 일곱 색 토러스 지도(Heawood Torus), 슈바르츠 랜턴(Schwarz Lantern), 윌브러햄과 깁스의 현상(Gibbs Phenomenon), 타원 곡선의 현과 접선(Elliptic Chords), 지먼의 파국 기계(Catastrophe Machine)예요. 37판도 여섯을 더했어요. 포천의 쓸기 선 알고리즘(Fortune's Sweep), 앙투안의 목걸이(Antoine's Necklace), 카르만 소용돌이 길의 안정성(Kármán Street), 단단한 원판의 헥사틱 상(Hexatic Phase), 존슨-멜 쪽매와 아브라미 법칙(Johnson-Mehl), 쿠라모토-시바신스키 방정식(Kuramoto-Sivashinsky)이에요. 38판도 여섯을 더했어요. 앤더슨 국소화(Anderson Localization), 무어가 찾고 Chenciner 와 Montgomery 가 증명한 세 몸 문제의 8자 해(Figure-Eight Orbit), 예센과 빈트너, 에르되시의 베르누이 합성곱(Bernoulli Convolutions), 브라우어르의 고정점 정리를 증명하는 슈페르너 보조정리(Sperner's Lemma), 에겐베르거와 폴리아의 항아리(Pólya Urn), 뇌르고르의 무한 수열(Infinity Series)이에요. 39판도 여섯을 더했어요. 고든, 웹, 월퍼트의 등스펙트럼 북(Isospectral Drums), 폰 망골트의 꼴로 쓴 리만의 명시 공식(Explicit Formula), 요한 베르누이의 최단 강하선(Brachistochrone), 슈타이너의 포리즘(Steiner Chain), 모런 집단으로 그린 킹먼의 합류(Kingman Coalescent), 보르수크-울람 정리(Borsuk-Ulam)예요. 40판도 여섯을 더했어요. 앨더스의 브라운 연속 랜덤 나무(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 함수(Hydrogen Orbital), 젤도비치 근사의 팬케이크(Zeldovich Pancakes), 망델브로가 내다보고 롤러, 슈람, 베르너가 증명한 브라운 운동의 바깥 경계(Brownian Frontier), 복소 긴즈부르크-란다우 방정식의 결함 난류(Defect Turbulence), 루퍼트 왕자의 정육면체(Prince Rupert's Cube)예요. 41판은 다섯을 더했어요. 홀과 왜건의 현수선 길과 정다각형 바퀴(Square Wheels), 퐁슬레의 포리즘(Poncelet Porism), 슐레플리의 4차원 정다포체를 자른 단면(Polytope Slices), 클림켁과 티먼, 마그누손의 사이트스왑(Siteswap), Dudeney 의 잡화상 퍼즐과 월리스-보여이-게르비엔 정리(Hinged Dissection)예요. 42판은 다섯을 더했어요. 바른스도르프 규칙으로 도는 기사 여행(Knight's Tour), 페르미와 파스타, 울람, 칭구의 되돌아옴(FPUT Recurrence), 포슬리에-립킨 반전기(Peaucellier Linkage), 몰리의 삼등분선 정리(Morley Triangle), 랄과 마드라스, 소칼의 피벗 알고리즘으로 돌린 자기 회피 걸음(Pivot Walk)이에요. 43판은 다섯을 더했어요. 월리스 워커의 칼레이도사이클(Kaleidocycle), 쌍곡형에서 록소드롬형으로 감기는 뫼비우스 변환의 흐름(Möbius Loxodrome), 터트의 용수철 정리(Tutte Embedding), 디펀트와 크래비츠의 고리 정리를 품은 히토메자시 사시코(Hitomezashi), 룽게 현상과 체비쇼프 점(Runge Phenomenon)이에요. 44판은 다섯을 더했어요. 마크 카츠의 고리 모형(Kac Ring), 스톤과 튜키의 햄 샌드위치 정리(Ham Sandwich), 브란트의 불가리아 솔리테어(Bulgarian Solitaire), 엘스켄스와 프리시의 탄도 소멸(Ballistic Annihilation), 페레그린의 솔리톤(Peregrine Soliton)이에요. 45판은 다섯을 더했어요. 로건-셰프와 베르시크-케로프의 극한 곡선으로 다가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도브루신 경계 조건을 건 임계 이징 모형의 SLE(3) 경계(Ising Interface), 프리예제프와 동료들의 오일러 걷개로 쌓는 로터 라우터 응집과 레빈-페레스의 스매시 합(Rotor-Router), 폴리아의 과수원 문제(Pólya's Orchard), 넬슨의 평면 색칠 문제와 아이즈벨의 일곱 색(Hadwiger-Nelson)이에요. 46판은 다섯을 더했어요. 브릴루앙의 구역(Brillouin Zones), 모스 이론과 레브의 그래프(Reeb Graph), 로렌츠 기체와 시나이의 당구(Lorentz Gas), 리언 글래스의 점무늬(Glass Patterns), 벤치와 수테라, 불피아니의 확률 공명(Stochastic Resonance)이에요. 47판은 다섯을 더했어요. 오키프와 홍, 스트로가츠의 스워멀레이터(Swarmalators), 빌스의 외로운 달리기 선수 추측(Lonely Runner), 샤츠의 올로이드와 그 전개도(Oloid), 쾨베의 원 채우기 정리와 콜린스-스티븐슨의 반복(Koebe Packing), 콘웨이의 병사와 황금비 무게(Conway's Soldiers)예요. 48판은 다섯을 더했어요. 오하라의 매듭 뫼비우스 에너지(Möbius Energy), 체비쇼프의 편향과 루빈스타인-사르낙의 밀도(Chebyshev's Bias), 섀든과 레키엔, 마스든의 이중 소용돌이 능선(Double Gyre), 아자리와 프로스트의 깜빡이는 래칫(Flashing Ratchet), 망델브로의 프랙탈 스미기(Fractal Percolation)예요. 49판은 다섯을 더했어요. 뒤러의 전개 문제와 셰퍼드의 추측(Dürer's Unfolding), 베리와 골드버그가 재규격화한 세타 합의 꼬부랑 곡선(Curlicue), 오일러 방정식과 푸앵소의 폴호드로 본 테니스 라켓 정리(Tennis Racket), 켐프의 사슬과 네 색 정리(Kempe Chains), 퇴플리츠의 내접 정사각형 문제와 그린-로브의 직사각형(Square Peg)이에요. 50판은 다섯을 더했어요. 크레이치넌과 배철러의 2차원 난류가 거꾸로 가는 캐스케이드와 맥윌리엄스의 소용돌이(Inverse Cascade), 사포발과 로소, 구예의 확산 앞줄(Diffusion Front), 필리와 마르케티, 케이츠와 타이외르의 운동성 상분리(Motility Clusters), 박과 스네펜의 진화 모형(Bak-Sneppen), 리세의 영원한 가속(Risset Rhythm)이에요. 51판은 다섯을 더했어요. 스미스와 마이어스, 캐플런, 굿먼-스트라우스의 모자 타일과 H/T/P/F 메타타일 치환(Hat Monotile), 반도와 동료들의 최적 속도 모형과 스기야마의 원형 트랙 실험(Phantom Jam), 사토와 테이트의 법칙과 타원곡선의 a_p(Sato-Tate), 칼라비와 야우의 공간에서 핸슨이 잘라 그린 5차 페르마 곡선(Calabi-Yau Quintic), 에델스브루너와 레처, 조모로디언의 지속 호몰로지와 바코드(Persistence Barcode)예요. 52판은 다섯을 더했어요. 슈람과 셰필드가 등고선을 SLE(4) 로 보인 2차원 가우스 자유장(Gaussian Free Field), 당튀와 마즈무다르, 베링거의 광탄성 힘 사슬(Force Chains), 앤스컴의 사중주와 마테이카, 피츠모리스의 같은 통계 담금질(Anscombe's Quartet), 드 브라윈 수열과 BEST 정리(de Bruijn Sequence), 라인스의 베타 평면 띠 제트(Zonal Jets)예요. 53판은 열을 더했어요. 듀크, 가르시아, 루츠가 그린 가우스 주기(Gaussian Periods), 부르가 보인 현수면과 나선면의 휨(Catenoid-Helicoid), 디랙의 벨트 묘기(Belt Trick), 헬름홀츠와 러브의 연기 고리 개구리뜀(Leapfrogging Vortices), 브룩스, 스미스, 스톤, 텃의 정사각형 분할(Squared Rectangle), 포드와 풀커슨의 최대 흐름 최소 절단(Max-Flow Min-Cut), 코스타스 배열(Costas Array), 바라니, 비어든, 칸의 무게중심 분할(Barycentric Subdivision), 무코파디아야와 크네저의 네 꼭짓점 정리(Four-Vertex Theorem), 오일러의 엘라스티카(Euler's Elastica)예요. 54판은 열을 더했어요. 베리의 무작위 파동과 보고몰니, 슈미트가 스미기로 센 마디 영역(Nodal Domains), 피어험버트가 사인 흐름에서 찾은 이상 고유 모드(Strange Eigenmode), 피어시 적분과 베리, 업스틸의 첨점 회절 파국(Pearcey Cusp), 나겔과 바이스의 STIT 모자이크(STIT Tessellation), 레비의 아크사인 법칙(Arcsine Law), 스퇴르메르가 다룬 쌍극자 장 속 대전 입자(Størmer Trap), 게일과 섀플리의 지연 수락(Stable Marriage), 스메일의 말굽(Smale Horseshoe), 울람 수열과 슈타이너베르거가 찾은 숨은 진동수(Ulam Sequence), 위어와 펠런의 거품과 켈빈의 깎은 팔면체(Weaire-Phelan)예요. 55판은 열을 더했어요. 밀러와 셰필드의 허수 기하(Imaginary Geometry), 티소의 지시 타원과 메르카토르 투영(Tissot's Indicatrix), 민코프스키의 물음표 함수(Question Mark Function), 버로스와 휠러의 변환(Burrows-Wheeler), 뷔퐁의 바늘과 바르비에의 국수(Buffon's Noodle), 부자의 리듬 타일링 캐넌(Vuza Canon), 하인과 내시의 헥스와 게일의 헥스 정리(Hex Theorem), 8자 고리의 범피복과 자유군의 케일리 그래프(Universal Cover), 오브친니코프와 젤도비치, 투생과 윌첵의 확산 제한 소멸(Toussaint-Wilczek), 헬름홀츠와 켈빈의 와류 시트와 크라스니의 와동 덩이(Vortex Sheet)예요. 56판은 열을 더했어요. 마테롱의 죽은 잎과 켄들, 퇴네스가 아래에서 본 완전 표본(Dead Leaves), 골롬의 트로미노 깔기(Golomb Trominoes), 윌슨의 고리 지운 걸음과 롤러, 슈람, 베르너의 균등 생성나무 페아노 곡선(Tree Peano Curve), 블로흐와 제너의 진동과 워니어의 사다리(Bloch Oscillations), 앤드루스의 곡선과 아시모프의 그랜드 투어(Andrews Curves), 지니브르와 기르코의 원 법칙과 좀머스, 크리산티, 솜폴린스키, 스타인의 타원 법칙(Elliptic Law), 에르되시의 불일치 문제와 보웨인, 최, 쿤스의 3진 수열(Erdős Discrepancy), 망델브로의 곱셈 폭포와 카안, 페이리에르의 문턱(Multiplicative Cascade), 코르테, 체이킨, 골럽, 파인의 무작위 조직화(Random Organization), 칼루가레아누, 화이트, 풀러의 꼬임과 뒤틀림(Twist and Writhe)이에요. 목록 밖에서 다음에 해 볼 것이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -3139,6 +3204,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 55판에서 본 것이에요. 정리가 못 박는 것은 이번에도 셈으로 지켜봤어요. Imaginary Geometry 는 노브 여섯 조합에서 흐름선 13,338개가 한 번도 건너지 않았고, Hex Theorem 은 24,000판에서 늘 한 색만 이겼어요. Buffon's Noodle 은 길이 셋과 휨 다섯에 200,000번씩 떨어뜨려 평균이 2L/(πd) 와 0.005 안에서 맞았고, Universal Cover 는 100걸음 뒤 낱말이 평균 50.2 글자로 케스텐의 탈출 속도 1/2 에 맞았어요. Burrows-Wheeler 는 구간 교환 부호 250개의 마지막 열이 모두 글자마다 한 덩어리였어요. 수학이 정한 비율이 패널에 꼭 맞기도 했어요. 메르카토르를 위도 ±85.0511도에서 자르면 경도 180도와 정확히 1:2 라서, Tissot's Indicatrix 는 웹 메르카토르가 자르는 자리에서 잘랐어요. 연속이나 극한을 말하는 수학은 이번에도 LED 한 칸에서 멈췄어요. Vortex Sheet 는 4초가 지나면 코어의 겹이 LED 보다 촘촘해져 연보라 원판으로 차고, Question Mark Function 은 ?(0.1) 이 2^-9 라 계단이 8번 열 언저리에서야 바닥을 떠나요. κ 가 4 쪽으로 가면 흐름선이 LED 하나에 몇 라디안씩 꺾여서, Imaginary Geometry 는 장을 흐리는 폭을 4.1 LED 까지 넓혀야 했어요. 정리가 한 줄로 서는 그림은 패널에서도 한 줄로 읽혔어요. Vuza Canon 은 어느 줄에도 음이 꼭 하나라서 타일링이 1 LED 줄 하나로 보이고, Buffon's Noodle 은 다 휜 고리가 매번 선 두 개를 건너 오차 막대가 π 선에 납작하게 누워요. +56판에서 본 것이에요. 정리가 못 박는 수는 이번에도 패턴의 셈으로 지켜봤어요. Tree Peano Curve 는 나무 290그루에서 늘 LED 8192개를 한 번씩 지났고, Twist and Writhe 는 Lk 넷과 Coil 셋의 조합마다 두 모서리의 연결 적분이 정확히 Lk 였어요. Erdős Discrepancy 는 3진 수열의 합이 m = 600,000 까지 log3 m + 1 을 넘지 않았고, Bloch Oscillations 는 베셀 함수의 닫힌 꼴과 3×10⁻⁸ 안에서 맞았어요. Andrews Curves 는 그랜드 투어가 1분 도는 동안 곡선 사이 거리가 소수 셋째 자리까지 그대로였어요. 극한을 말하는 정리는 유한한 판에서 무른 문턱이 됐어요. Multiplicative Cascade 는 열세 번 나눈 판이 τ = 1 에서 갑자기 꺼지지 않고 Spread 를 따라 서서히 어두워졌고, Random Organization 의 원판 83개는 Strain 3 에서 판마다 멈추기도 하고 6,000주기를 넘기도 했어요. Elliptic Law 의 실수 고윳값 7.6 개도 큰 N 의 √(2N/π) 보다 τ 만큼 많아요. 재는 값이 밝기만 보는 것을 쓴 그림도 있어요. 가장자리는 가장 밝은 채널의 계단만 세서, Dead Leaves 는 밝기를 네 단에 묶고 색상만 흩어 가장자리 1 과 높은 복잡도를 같이 얻었어요. 고요한 패턴은 축의 아래 끝을 넓히기 쉬웠어요. Golomb Trominoes 의 첫 판은 움직임 0.014 로 그 축의 끝을 옮겨서 다른 칸 하나를 비울 뻔했어요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. @@ -3185,6 +3252,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 목록 밖에서 55판 뒤에 해 볼 것이에요. 같은 각도의 흐름선을 판 여러 점에서 띄우면 만나는 대로 합쳐져 나무가 되고, 그 나무를 따라 도는 곡선이 판을 다 메우는 SLE(16/κ) 예요. 밀러와 셰필드의 『Imaginary geometry IV』(2017)에 있어요. 같은 원들을 원통 대신 원뿔과 방위 투영에 얹으면 왜곡이 한 원이나 한 점 둘레로 모여서, 람베르트가 1772년에 낸 정각 원뿔 투영과 방위 등적 투영을 Tissot's Indicatrix 의 K4 로 오가게 해 볼 만해요. 콘웨이의 상자 함수(1976)는 물음표 함수를 뒤집은 꼴이라 같은 계단을 눕혀 실수 전체로 늘려 볼 수 있고, 슈테른의 이원자 수열(모리츠 슈테른, 1858)도 128줄을 채우는 산맥이 될 만해요. 천과 폭스, 린던의 린던 분해(1958)는 글을 사전순으로 커지지 않는 린던 단어들로 자르는 것이라, 조각마다 색이 다른 띠로 그려 볼 만해요. 크로프턴 공식(모건 크로프턴, 1868)은 Buffon's Noodle 과 같은 셈으로, 무작위 직선이 곡선을 건너는 수로 곡선의 길이를 재요. 리듬 타일링의 다른 끝은 푸글레드의 스펙트럼 추측(벤트 푸글레드, 1974)이고, 타오(2004)가 5차원에서 반례를 찾았어요. 삼각 격자 스미기에서 사각형을 가로지를 확률은 카디의 공식(존 카디, 1992)을 따르고 스미르노프(2001)가 증명해서, Hex Theorem 의 판 모양을 바꾸며 건넌 횟수를 세면 공식의 곡선에 다가가요. 자유군의 부분군은 8자 고리의 유한 피복이고, 스톨링스(1983)의 접기가 생성 낱말의 꽃잎 고리에서 같은 글자의 변을 하나씩 접어 그 피복을 만들어요. A + B → 0 에 입자를 끊임없이 고르게 넣어도 정상 상태에서 영역이 갈라져서(린덴버그, 웨스트, 코펠만, 1988), 영역이 패널 크기까지 퍼진 채 흔들리는 판을 세울 수 있어요. 한 주기에 두 파장을 넣고 반 파장 섭동을 더하면 이웃 나선이 서로 돌다 하나로 합치는 짝짓기(위넌트와 브라우언드, 1974)가 나와요. +목록 밖에서 56판 뒤에 해 볼 것이에요. 잎을 반투명하게 겹치는 투명 죽은 잎 모형(브뤼노 갈른, 얀 구소, 2012)은 잎이 투명해질수록 가우스 장으로 다가가서, Dead Leaves 의 셈에 노브 하나로 두 그림 사이를 오갈 수 있어요. 골롬의 렙타일(1964)로 L 트로미노를 같은 꼴 넷으로 거듭 나누면 의자 타일링이 돼요. 같은 생성나무를 해럴드 템펄리(1974)의 전단사로 도미노 타일링으로 옮겨 그려 볼 만해요. 힘을 교류로 흔들면 세기와 진동수의 비가 J_0 의 영점에 놓일 때 묶음이 퍼지지 않고 제자리에 묶여요. 데이비드 던랩과 V. M. 켄크리(1986)의 동적 국소화예요. 알프레드 인셀버그(1985)의 평행 좌표는 다차원 점을 세로축 여러 개를 잇는 꺾은선으로 그리고, 한 직선 위의 점들은 이웃한 두 축 사이에서 한 점을 지나요. 하타노-넬슨 모형(하타노 나오미치, 데이비드 넬슨, 1996)은 한쪽으로 기운 뜀을 넣은 무질서 사슬이라, 고윳값이 실수축에서 떨어져 나와 고리를 그려요. 리우빌 함수의 합이 2 이상에서 늘 0 이하라는 포여의 추측(조지 포여, 1919)은 C. 브라이언 해즐그로브(1958)가 깼고, 처음 양수가 되는 n = 906,150,257 까지 가는 길을 그려 볼 만해요. 이진 칸 없이 로그 상관 가우스장의 지수로 같은 다중 프랙탈을 만드는 장피에르 카안(1985)의 가우스 곱셈 혼돈도 있어요. 무작위 조직화의 임계점에서는 입자 배치가 초균일해져서(대니얼 헥스너, 도브 레빈, 2015), 큰 상자의 밀도 요동으로 그릴 수 있어요. 미첼 불안정성(존 헨리 미첼, 1889)은 꼬인 탄성 고리가 꼬임이 문턱을 넘을 때까지 납작하게 버티다가 한순간 평면 밖으로 휘는 일이에요. + ### 화장품 팝업 주제 6 으로 쓴 질감과 다음에 해 볼 질감이에요. 29판이 처음 넷을 썼어요. 팔레트 나이프로 펴 바르는 크림(Cream Swirl), 구슬이 뜬 젤과 빛 그물(Serum Gel), 쏟아져 쌓이는 글리터(Glitter Fall), 퍼프가 눌렀다 든 압축 파우더(Loose Powder)예요. 넷 다 K1 이 색상환 한 바퀴라서, 색을 돌리면 주황과 노랑이 갈색이나 올리브로 보이는 자리가 있었어요. Serum Gel 은 그 둘레에서만 밝기를 0.62 와 0.3 에서 0.92 와 0.68 로 올렸어요. 밝은 바탕은 반짝임을 지웠어요. 200 위에 55 를 더한 반짝임은 거의 안 보여서 Glitter Fall 은 어두운 바탕에 섰고, 흰 바탕과 금색 한 줄은 Loose Powder 의 아래 띠로만 썼어요. 크림과 젤은 가장자리 0.028 과 0.079 로 연구에서 가장 번지는 그림이에요. 그 둘이 가장자리 축의 아래 끝을 옮겨서 지도에 새 빈 칸이 났어요. 크림은 빛만 움직여서는 움직임이 0.019 에 머물렀어요. 결이 깊어야 빛이 돌 때 바뀌는 LED 가 생겨요. 비누막이 터질 때 구멍 가장자리가 테일러와 컬릭의 속도(1959, 1960)로 고르게 벌어지며 물방울 테를 남기는 것은 Soap Film 의 끝을 늘인 그림이에요. 클렌징 오일이 물을 만나 가장자리부터 우윳빛으로 유화되는 것은 Blotting Paper 처럼 LED 마다 한 번만 바뀌는 셈으로 그릴 만해요. diff --git a/incoming/pattern-study/patterns/dead-leaves.js b/incoming/pattern-study/patterns/dead-leaves.js index d29ec55f5..d37656bc9 100644 --- a/incoming/pattern-study/patterns/dead-leaves.js +++ b/incoming/pattern-study/patterns/dead-leaves.js @@ -123,8 +123,8 @@ export function update(dt, input, params) { if (S.phase === 1) { S.pt += g; if (S.pt > HOLD) { - // The held sample steps one level down, greys, and shows through the - // holes of the next one until they close. + // The held sample steps one level down and greys. It shows through + // the holes of the next one until they close. S.old = S.cur; S.cur = sample(); S.phase = 0; From acc04778f0e737e1e9ea0755bfafa2ed5a0e8965 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 08:19:08 +0000 Subject: [PATCH 09/10] =?UTF-8?q?56=ED=8C=90:=20index.html=20=EC=9D=84=204?= =?UTF-8?q?68=EA=B0=9C=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20=EC=A7=93=EB=8A=94?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UUXdHR3tRiDEQfEfGbQxb9 --- incoming/pattern-study/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index 149d3463d..6a466d5fd 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

패턴 연구

- - + +