From 530454fdb73b195948bafa0b72157c67156490fc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 14:32:58 +0000 Subject: [PATCH 1/8] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=20=EC=97=B0=EA=B5=AC=2062=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_01TTZyp5h85ewavQ9av2v4N1 From 4cbfdf771521570c85e11877db2f9400be92cba5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 15:25:46 +0000 Subject: [PATCH 2/8] patterns: Busy Beaver and Berggren Tree for round 62 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TTZyp5h85ewavQ9av2v4N1 --- .../pattern-study/patterns/berggren-tree.js | 220 ++++++++++ .../pattern-study/patterns/busy-beaver.js | 412 ++++++++++++++++++ 2 files changed, 632 insertions(+) create mode 100644 incoming/pattern-study/patterns/berggren-tree.js create mode 100644 incoming/pattern-study/patterns/busy-beaver.js diff --git a/incoming/pattern-study/patterns/berggren-tree.js b/incoming/pattern-study/patterns/berggren-tree.js new file mode 100644 index 000000000..17e2f8017 --- /dev/null +++ b/incoming/pattern-study/patterns/berggren-tree.js @@ -0,0 +1,220 @@ +// ===== Patternflow pattern ===== +// Title: Berggren Tree +// Family: number +// Idea: Berggren's ternary tree of primitive Pythagorean triples grows from (3, 4, 5) one generation at a time over the plane of the legs, odd leg up and even leg across, its A and C branches running along the parabolas c - b = k^2 and c - a = 2n^2 on whose crossings every primitive triple sits, its B branches jumping between them, while the window widens to keep the newest generation at the edge and the crossings that are multiples never light. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Euclid's formula for Pythagorean triples (Euclid, c. 300 BC), tree of primitive Pythagorean triples (B. Berggren, 1934), rediscovered (F. J. M. Barning, 1963; A. Hall, 1970), the count N / 2 pi of primitive triples with c below N (D. N. Lehmer, 1900), pattern round 62 +// Round: 62 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Reach=-1..1, Root=0..1 +// +// Knob 1: Hue. Turns the three matrices' colours round the wheel, skipping orange and yellow. At 0 the A branches and the triples A made are azure, C's rose and B's pale violet, with (3, 4, 5) white; at 0.25 violet, green and pink, at 0.5 red, cyan and pale green, at 0.75 green, blue and pale azure. The faint parabolas take the colour of their branches. +// Knob 2: Speed. At 2 the first tree opens three generations grown, the window reaching odd legs of 173 and even legs of 86, and takes the rest of a 12 s run to grow to the full Reach: at Reach 0 one generation every 1.1 s, full at 8.7 s. It holds 4 s while a pulse runs out from the root a generation at a time, then fades and starts again from (3, 4, 5) alone, every 16 s. At 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Reach. How far the window has widened when the run ends: odd legs up to 128 u and even legs up to 64 u, u = 4 x 2^Reach. At 0 the legs reach 512 and 256 and the window holds 61 triples down to the 10th generation, on 71 crossings of the two families, 10 of them multiples like 9 (3, 4, 5) that stay dark; at -1, 30 triples to the 6th generation, at 1, 124 to the 14th. The window starts at 0.18 of its last size at Reach 0 (0.28 at -1, 0.12 at 1) and widens as (G + 8)^2 with the generation G. +// Knob 4: Root. Bends the plane from the legs, z^2 for z = m + i n, at 0.06 and below, to z itself at 1, the plane of Euclid's (m, n): the parabolas straighten, the C branches stand upright with n fixed, the A branches run at 45 degrees with m - n fixed, and the triples sit on the lattice of coprime m > n of opposite parity. + +const W = 64, H = 128, NP = W * H; +const { floor, round, min, max, abs, sqrt, atan2, cos, sin, pow, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v, out) { + 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[0] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + out[1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + out[2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; +} +// The wheel without its orange and yellow, 0.03 to 0.17. +const wheel = (h) => 0.17 + (h - floor(h)) * 0.86; + +// Every primitive triple is (m^2 - n^2, 2mn, m^2 + n^2) for one pair m > n +// > 0, coprime and of opposite parity, and Berggren's matrices A, B and C on +// (a, b, c) are, on (m, n): +// A: (m, n) -> (2m - n, m) keeps m - n, so c - b = (m - n)^2 +// B: (m, n) -> (2m + n, m) +// C: (m, n) -> (m + 2n, n) keeps n, so c - a = 2 n^2 +// from (2, 1), which is (3, 4, 5). A child's c is always larger than its +// parent's, so cutting the tree at c <= CB keeps every triple up to CB. +const UMAX = 8, CB = 170 * UMAX; +const NM = [2], NN = [1], ND = [0], NX = [-1], NPAR = [-1]; +for (let i = 0; i < NM.length; i++) { + const m = NM[i], n = NN[i], kids = [[2 * m - n, m], [2 * m + n, m], [m + 2 * n, n]]; + for (let k = 0; k < 3; k++) { + const M = kids[k][0], N = kids[k][1]; + if (M * M + N * N > CB) continue; + NM.push(M); + NN.push(N); + ND.push(ND[i] + 1); + NX.push(k); + NPAR.push(i); + } +} +const NNODE = NM.length; +const RUN = 12, HOLD = 4, G0 = 8; + +export function setup(params) { + params.s = { + rgb: new Float32Array(NP * 3), old: new Float32Array(NP * 3), c: new Float32Array(3), cols: [0, 1, 2].map(() => new Float32Array(3)), + o: [0, 0], key: "", deep: 1, tau: 0, hold: 0, fade: 0, G: 0, started: false, + hue: 0, speed: 2, uf: 4, u: 1, p: 2, sc: 1, + }; +} + +// Where (m, n) lands: z = m + i n raised to the power p and scaled so that +// |z|^2 = 128 u reaches the top row. At p = 2 that is the odd leg a up and +// the even leg b across, u legs to an LED; at p = 1 the lattice of (m, n). +function proj(S, m, n, out) { + if (S.p === 2) { + out[0] = 2 * m * n * S.sc; + out[1] = H - 1 - (m * m - n * n) * S.sc; + return; + } + const r2 = m * m + n * n, th = atan2(n, m) * S.p, rp = pow(r2, S.p / 2) * S.sc; + out[0] = rp * sin(th); + out[1] = H - 1 - rp * cos(th); +} +const inside = (x, y) => x >= -0.5 && x < W - 0.5 && y >= -0.5 && y < H - 0.5; +function scale(S, u) { + S.u = u; + S.sc = 128 / pow(128 * u, S.p / 2); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.uf = 4 * pow(2, cl(k[2], -1, 1)); + S.p = 2 - cl((k[3] - 0.06) / 0.94, 0, 1); + // The deepest generation with a triple inside the window at full reach. + const key = S.uf.toFixed(4) + "," + S.p.toFixed(4); + if (key !== S.key) { + S.key = key; + scale(S, S.uf); + S.deep = 1; + for (let i = 0; i < NNODE; i++) { + proj(S, NM[i], NN[i], S.o); + if (inside(S.o[0], S.o[1])) S.deep = max(S.deep, ND[i]); + } + } + // The first tree opens three generations grown; the later ones start + // from (3, 4, 5) alone. + if (!S.started) { + S.started = true; + S.tau = (3 * RUN) / (S.deep + 1); + } + const g = d * (S.speed / 2); + S.fade = max(0, S.fade - d / 0.6); + if (S.tau < RUN) S.tau = min(RUN, S.tau + g); + else { + S.hold += g; + if (S.hold > HOLD) { + S.old.set(S.rgb); + S.tau = S.hold = 0; + S.fade = 1; + } + } + // One generation after another, each in the same share of the run, and + // the window widening as (G + G0)^2, the way the reach of a generation + // along A or C grows, so the newest one always spans the panel. + const D = S.deep + 1; + S.G = (D * S.tau) / RUN; + scale(S, S.uf * ((S.G + G0) / (D + G0)) ** 2); +} + +// While the grown tree holds, a pulse runs out from the root through the +// generations, one generation every RUN / (deep + 1) at Speed 2. +function wave(S, d) { + if (S.tau < RUN) return 0; + const at = (S.hold / HOLD) * (S.deep + 2) - 0.5, e = (d - at) / 0.6; + return exp(-e * e); +} + +function put(rgb, x, y, r, g, b) { + x = round(x); + y = round(y); + if (x < 0 || y < 0 || x >= W || y >= H) return; + const o = (y * W + x) * 3; + if (r > rgb[o]) rgb[o] = r; + if (g > rgb[o + 1]) rgb[o + 1] = g; + if (b > rgb[o + 2]) rgb[o + 2] = b; +} + +// A straight run in (m, n) from its start to t1 of the way, through the +// projection: for A and C the run lies on m - n = k or n = const, so the +// branch is drawn along its parabola. +function branch(S, rgb, m0, n0, m1, n1, t1, col, w) { + const o = S.o; + proj(S, m0, n0, o); + const x0 = o[0], y0 = o[1]; + proj(S, m0 + (m1 - m0) * t1, n0 + (n1 - n0) * t1, o); + const steps = max(2, min(300, floor((abs(o[0] - x0) + abs(o[1] - y0)) * 1.5))); + const r = col[0] * w, g = col[1] * w, b = col[2] * w; + for (let i = 0; i <= steps; i++) { + const t = (t1 * i) / steps; + proj(S, m0 + (m1 - m0) * t, n0 + (n1 - n0) * t, o); + if (o[1] < -1 || o[0] > W) return; + put(rgb, o[0], o[1], r, g, b); + } + // A branch still growing carries a white spark at its tip. + if (t1 < 1) put(rgb, o[0], o[1], 255, 255, 255); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { rgb, cols, o } = S; + rgb.fill(0); + const h = S.hue, G = S.G; + hsv(wheel(0.452 + h), 0.75, 1, cols[0]); // A + hsv(wheel(0.72 + h), 0.3, 1, cols[1]); // B + hsv(wheel(0.88 + h), 0.6, 1, cols[2]); // C + // The two families the branches run along, faint: m - n = k for odd k, + // c - b = k^2, starting up the left edge at a = k^2, and n = 1, 2, ..., + // c - a = 2 n^2, starting along the foot at b = 2 n^2. Every primitive + // triple sits on a crossing, but not every crossing is primitive: (6, 3) + // is 9 (3, 4, 5), and the tree never lights it. + const top = sqrt(200 * S.u) + 2; + hsv(wheel(0.452 + h), 0.5, 0.19, S.c); + for (let k = 1; k < top; k += 2) branch(S, rgb, k, 0, k + top, top, 1, S.c, 1); + hsv(wheel(0.88 + h), 0.45, 0.19, S.c); + for (let n = 1; n < top; n++) branch(S, rgb, n, n, n + 2 * top, n, 1, S.c, 1); + // The branches: growing for a generation, then A and C stay as the + // parabolas they lie on and B, which jumps between them, fades. + for (let i = 1; i < NNODE; i++) { + const f = G - (ND[i] - 1); + if (f <= 0) continue; + const j = NPAR[i], k = NX[i]; + proj(S, NM[j], NN[j], o); + if (!inside(o[0], o[1])) continue; + const w = (k === 1 ? (f < 1 ? 0.5 : 0) : f < 1 ? 1 : max(0.55, 1 - (f - 1) * 0.45)) + wave(S, ND[i]) * 0.5; + if (w > 0) branch(S, rgb, NM[j], NN[j], NM[i], NN[i], min(1, f), cols[k], w); + } + // The triples, five-LED crosses in the colour of the matrix that made + // them, white as they arrive and the root (3, 4, 5) white for good. + for (let i = 0; i < NNODE; i++) { + const age = G - ND[i]; + if (age < 0) continue; + proj(S, NM[i], NN[i], o); + const x = round(o[0]), y = round(o[1]); + if (x < -1 || y < -1 || x > W || y > H) continue; + const k = NX[i], fl = k < 0 ? 1 : max(0, 1 - age, wave(S, ND[i])), col = cols[max(0, k)]; + const R = col[0] + (255 - col[0]) * fl, Gg = col[1] + (255 - col[1]) * fl, B = col[2] + (255 - col[2]) * fl; + put(rgb, x, y, 160 + R * 0.4, 160 + Gg * 0.4, 160 + B * 0.4); + put(rgb, x + 1, y, R * 0.8, Gg * 0.8, B * 0.8); + put(rgb, x - 1, y, R * 0.8, Gg * 0.8, B * 0.8); + put(rgb, x, y + 1, R * 0.8, Gg * 0.8, B * 0.8); + put(rgb, x, y - 1, R * 0.8, Gg * 0.8, B * 0.8); + } + const fd = S.fade, old = S.old; + for (let p = 0, y = 0; y < H; y++) + for (let x = 0; x < W; x++, p += 3) { + let r = min(255, rgb[p]), gg = min(255, rgb[p + 1]), b = min(255, rgb[p + 2]); + if (fd > 0) (r = max(r, old[p] * fd)), (gg = max(gg, old[p + 1] * fd)), (b = max(b, old[p + 2] * fd)); + (rgb[p] = r), (rgb[p + 1] = gg), (rgb[p + 2] = b); + display.setPixel(x, y, r, gg, b); + } +} diff --git a/incoming/pattern-study/patterns/busy-beaver.js b/incoming/pattern-study/patterns/busy-beaver.js new file mode 100644 index 000000000..5e223f97f --- /dev/null +++ b/incoming/pattern-study/patterns/busy-beaver.js @@ -0,0 +1,412 @@ +// ===== Patternflow pattern ===== +// Title: Busy Beaver +// Family: algo +// Idea: The busy beaver Turing machines write their tapes as space-time diagrams, tape across and time down, the five-state champion's 47,176,870 steps folding into a stack of triangles that zooms out as it grows, one triangle for each pass of its Collatz-like loop. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: busy beaver game (Tibor Rado, 1962), S(3) = 21 (Shen Lin and Tibor Rado, 1965), S(4) = 107 (Allen Brady, 1983), five-state champion (Heiner Marxen and Juergen Buntrock, 1989), its Collatz-like loop (Pascal Michel, 1993), S(5) = 47,176,870 proved (bbchallenge, 2024), pattern round 62 +// Round: 62 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Machine=0..3, Clock=0..1 +// +// Knob 1: Hue. Turns the tape colour round the wheel, skipping orange and yellow. At 0 the tape is azure, pale where it holds solid 1s and deep blue where it holds the champion's stripes 100 100 100, averaged to a third; the head's left turns are gold and its right turns rose, the sides of every triangle. +// Knob 2: Speed. At 2 the champion opens 100 turns and 3592 steps in, and its turns grow 9.4 percent a second, a new triangle about every 6 s (0.6, 6.9, 13.0, 18.9, 24.8, 30.6, 36.3, 42.0 and 48.7 s). From 45.7 s, 9.9 million steps in, the panel spends no more than 2.4 million steps a second and the zoom waits for the machine; it halts at 62.2 s, holds 6 s and starts again. At 0.2 ten times slower, halting at 636 s; at 3 it halts at 41.5 s. The small machines take 3 s for their whole run at 2 and hold 3 s. +// Knob 3: Machine. 0 is the five-state champion of Heiner Marxen and Juergen Buntrock, 47,176,870 steps, 12,258 turns and 4098 ones over 12,289 cells; 1 is Allen Brady's four-state champion, 107 steps and 13 ones over 14 cells, 4 LEDs a cell and 1 row a step; 2 is the three-state machine of 21 steps and 5 ones, 12 LEDs a cell and 5 rows a step; 3 is the two-state one, 6 steps and 4 ones, 16 LEDs a cell and 15 rows a step. The counts sit at the top left for the champion and under the small diagrams, steps above and the 1s on the tape now below, and turn white at the halt. +// Knob 4: Clock. What a row of the champion counts. At 0 every sweep, turn to turn, gets the same height and the triangles have straight sides; at 1 every step does, and they swell into arches as the sweeps lengthen, until at the halt the last pass, 30.2 of the 47.2 million steps, takes the bottom 82 rows. At the opening 0.06 a row is nearly a sweep. The small machines always give each step its own band of rows. + +const W = 64, H = 128; +const { floor, round, min, max, abs, exp, log } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v, out) { + 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[0] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + out[1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + out[2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; +} + +// The machines in the notation of bbchallenge: for each state A, B, ... the +// moves on reading 0 and on reading 1, each the symbol to write, the way to +// go and the next state, Z for halt. +const MACHINES = [ + "1RB1LC_1RC1RB_1RD0LE_1LA1LD_1RZ0LA", // Marxen and Buntrock, 5 states + "1RB1LB_1LA0LC_1RZ1LD_1RD0RA", // Brady, 4 states + "1RB1RZ_1LB0RC_1LC1LA", // Lin and Rado, 3 states + "1RB1LB_1LA1RZ", // 2 states +]; +function parse(text) { + const st = text.split("_"), n = st.length; + const wr = new Int8Array(2 * n), dr = new Int8Array(2 * n), nx = new Int8Array(2 * n); + for (let s = 0; s < n; s++) + for (let r = 0; r < 2; r++) { + const m = st[s].slice(3 * r, 3 * r + 3), o = 2 * s + r; + wr[o] = m.charCodeAt(0) - 48; + dr[o] = m[1] === "R" ? 1 : -1; + nx[o] = m[2] === "Z" ? -1 : m.charCodeAt(2) - 65; + } + return { wr, dr, nx, n }; +} +const TABLES = MACHINES.map(parse); + +// The tape: the start cell sits at ORG, with room for the champion's 12,243 +// cells to the left and 45 to the right. +const TL = 16384, ORG = 13600; +// Snapshots of the tape, as running counts of 1s over blocks of 2^bs cells, +// never more than NB blocks, taken at every step for the first 256 and +// after that at a turn of the head once the steps or the turns have grown +// by a 200th since the last one: 1894 over the champion's run. +const SMAX = 4096, NB = 256, BLK = NB + 2; +const RATE = 0.09, TAU0 = 100, CAP = 40000, RUN = 3, HOLD = 3, HOLD5 = 6; +const SMALL_H = 110, SH = [0.55, 0.2, 0.75, 0.37, 0.9]; +const DIG = ["111101101101111", "010110010010111", "111001111100111", "111001111001111", "101101111001001", "111100111001111", "111100111101111", "111001001001001", "111101111101111", "111101111001111"]; + +export function setup(params) { + params.s = { + tape: new Uint8Array(TL), blk: new Int32Array(TL), pool: new Uint16Array(SMAX * BLK), + sStep: new Float64Array(SMAX), sTurn: new Int32Array(SMAX), sHead: new Int32Array(SMAX), sState: new Int8Array(SMAX), + sP0: new Int32Array(SMAX), sBs: new Int8Array(SMAX), sNb: new Int16Array(SMAX), sLo: new Int32Array(SMAX), sHi: new Int32Array(SMAX), + tStep: new Float64Array(16384), tPos: new Int32Array(16384), tState: new Int8Array(16384), + rgb: new Float32Array(W * H * 3), c: new Float32Array(3), cs: new Float32Array(SMAX), + tw: new Float32Array(W * H), trgb: new Float32Array(W * H * 3), tx: new Int16Array(16384), ty: new Int16Array(16384), + pal: new Float32Array(9), sc: [0, 1, 2, 3, 4].map(() => new Float32Array(3)), white: [255, 255, 255], + m: -1, hue: 0, speed: 2, q: 0.06, fins: [], + }; +} + +function fresh(S, m) { + S.m = m; + S.T = TABLES[m]; + S.tape.fill(0); + S.blk.fill(0); + S.h = ORG; + S.st = 0; + S.steps = 0; + S.lo = S.hi = ORG; + S.bs = 0; + S.lastD = 0; + S.nt = 0; + S.ns = 0; + S.lastSnapStep = 0; + S.lastSnapTurn = 0; + S.halted = false; + S.r = 0; + S.hold = 0; + S.tmp = false; + snap(S, false); + if (m === 0) advance(S, TAU0, 1e9); + else S.fin = S.fins[m] || (S.fins[m] = measure(S.T)); +} + +// A small machine's whole run on a scratch tape: its steps and the cells it +// visits, so its diagram can be laid out before it starts. +function measure(T) { + const tape = new Uint8Array(64); + let h = 32, st = 0, steps = 0, lo = 32, hi = 32; + while (st >= 0 && steps < 1000) { + const o = 2 * st + tape[h]; + tape[h] = T.wr[o]; + h += T.dr[o]; + st = T.nx[o]; + steps++; + lo = min(lo, h); + hi = max(hi, h); + } + return { steps, lo: lo - 32 + ORG, hi: hi - 32 + ORG }; +} + +function snap(S, tmp) { + if (S.ns >= SMAX) return; + S.tmp = tmp; + const j = S.ns++, bs = S.bs, p0 = S.lo >> bs, nb = (S.hi >> bs) - p0 + 1, o = j * BLK; + const { pool, blk } = S; + pool[o] = 0; + for (let b = 0; b < nb; b++) pool[o + b + 1] = pool[o + b] + blk[p0 + b]; + S.sStep[j] = S.steps; + S.sTurn[j] = S.nt; + S.sHead[j] = S.h; + S.sState[j] = S.st; + S.sP0[j] = p0; + S.sBs[j] = bs; + S.sNb[j] = nb; + S.sLo[j] = S.lo; + S.sHi[j] = S.hi; + if (!tmp) { + S.lastSnapStep = S.steps; + S.lastSnapTurn = S.nt; + } +} + +// Doubles the block size of the running counts when the tape outgrows NB. +function coarsen(S) { + const { blk } = S, n = TL >> S.bs; + for (let i = 0; i < n / 2; i++) blk[i] = blk[2 * i] + blk[2 * i + 1]; + for (let i = n / 2; i < n; i++) blk[i] = 0; + S.bs++; +} + +// Runs the machine until it has made `turns` turns or `steps` steps more. +function advance(S, turns, budget) { + if (S.halted) return 0; + // A snapshot left at the end of the last frame only stood for the bottom + // row; the run goes on from the one before it. + if (S.tmp) { + S.ns--; + S.tmp = false; + } + const { tape, blk, tStep, tPos, tState } = S, { wr, dr, nx } = S.T; + let h = S.h, st = S.st, steps = S.steps, lo = S.lo, hi = S.hi, lastD = S.lastD, bs = S.bs, nt = S.nt; + const end = steps + budget; + while (steps < end && nt < turns) { + const sym = tape[h], o = 2 * st + sym, w = wr[o], d = dr[o]; + let turned = false; + if (w !== sym) { + tape[h] = w; + blk[h >> bs] += w - sym; + } + if (d !== lastD) { + if (lastD !== 0) { + tStep[nt] = steps; + tPos[nt] = h; + tState[nt] = st; + nt++; + turned = true; + } + lastD = d; + } + h += d; + st = nx[o]; + steps++; + if (h < lo || h > hi) { + if (h < lo) lo = h; + else hi = h; + if ((hi >> bs) - (lo >> bs) >= NB) { + S.bs = bs; + coarsen(S); + bs = S.bs; + } + } + if (st < 0) { + S.halted = true; + break; + } + if (steps < 256 || (turned && ((steps - S.lastSnapStep) * 200 >= steps || (nt - S.lastSnapTurn) * 200 >= nt))) { + S.h = h; S.st = st; S.steps = steps; S.lo = lo; S.hi = hi; S.nt = nt; + snap(S, false); + } + } + const did = budget - (end - steps); + S.h = h; S.st = st; S.steps = steps; S.lo = lo; S.hi = hi; S.lastD = lastD; S.nt = nt; + if (S.halted) { + S.st = -1; + snap(S, false); + } else if (S.lastSnapStep !== steps) snap(S, true); + return did; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.q = cl(k[3], 0, 1); + const m = cl(floor(k[2] + 0.5), 0, 3); + if (m !== S.m) fresh(S, m); + const g = d * (S.speed / 2); + if (S.halted) { + S.hold += g; + if (S.hold > (m === 0 ? HOLD5 : HOLD)) fresh(S, m); + return; + } + if (m === 0) { + // The turns grow by RATE a second at Speed 2, unless that would take + // more than CAP steps a frame; then the zoom waits for the machine. + const r = S.r + g, target = floor(TAU0 * exp(RATE * r)), budget = floor(CAP * S.speed * 30 * d); + const did = target > S.nt ? advance(S, target, max(1, budget)) : 0; + S.r = did >= budget ? log(max(S.nt, TAU0) / TAU0) / RATE : r; + } else { + S.r += g; + const want = floor((S.fin.steps * S.r) / RUN); + if (want > S.steps) advance(S, 1e9, want - S.steps); + } +} + +// Where a snapshot sits down the panel, 0 to 1: the share of sweeps (each +// sweep, turn to turn, one unit, its steps spread evenly through it) mixed +// with the share of steps by the Clock knob. +function clocks(S) { + const { tStep, sStep, sTurn, cs } = S, nt = S.nt, now = S.steps, q = S.q; + const tk = nt > 0 ? tStep[nt - 1] : 0, prev = nt > 1 ? tStep[nt - 1] - tStep[nt - 2] : 1; + const curLen = max(1, now - tk, prev), sigNow = max(1e-9, nt + (now - tk) / curLen); + for (let j = 0; j < S.ns; j++) { + const s = sStep[j], kk = sTurn[j]; + const a = kk > 0 ? tStep[kk - 1] : 0, b = kk < nt ? tStep[kk] : a + curLen; + cs[j] = (1 - q) * ((kk + (s - a) / max(1, b - a)) / sigNow) + q * (s / max(1, now)); + } + return sigNow; +} + +// The running count of 1s left of cell p in snapshot j, blocks interpolated. +function cum(S, j, p) { + const bs = S.sBs[j], u = p / (1 << bs) - S.sP0[j], nb = S.sNb[j], o = j * BLK, pool = S.pool; + if (u <= 0) return 0; + if (u >= nb) return pool[o + nb]; + const i = floor(u); + return pool[o + i] + (pool[o + i + 1] - pool[o + i]) * (u - i); +} + +function shade(S, dn, out) { + const P = S.pal; + if (dn <= 1 / 3) { + const u = dn * 3; + for (let i = 0; i < 3; i++) out[i] = P[3 + i] + (P[i] - P[3 + i]) * u; + } else { + const u = (dn - 1 / 3) * 1.5; + for (let i = 0; i < 3; i++) out[i] = P[i] + (P[6 + i] - P[i]) * u; + } +} + +// The champion: every row the tape at one moment, the window the whole tape +// so far, each LED the share of 1s in its 1/64 of it. +function drawChampion(S) { + const { rgb, c, tw, trgb } = S, col = [0, 0, 0]; + const sigNow = clocks(S), cs = S.cs, lo = S.lo, ext = S.hi - S.lo + 1, C = ext / W; + let j = 0; + for (let y = 0; y < H; y++) { + const want = (y + 1) / H - 1e-9; + while (j + 1 < S.ns && cs[j + 1] <= want) j++; + const sl = S.sLo[j], sh = S.sHi[j] + 1; + // Each LED averages a whole number of the stripes' 3 cells and of the + // snapshot's blocks, so 100 100 100 comes out a third everywhere + // instead of beating against the LEDs. + const u = 3 << S.sBs[j], wf = u * Math.ceil(C / u - 1e-9); + for (let x = 0; x < W; x++) { + const a = lo + x * C, b = a + C; + if (b <= sl || a >= sh) continue; + const m = (a + b) / 2; + const dn = (cum(S, j, m + wf / 2) - cum(S, j, m - wf / 2)) / wf; + shade(S, cl(dn, 0, 1), col); + const o = (y * W + x) * 3; + rgb[o] = col[0]; + rgb[o + 1] = col[1]; + rgb[o + 2] = col[2]; + } + } + // Every turn of the head in the colour of the state it turned in. The + // champion turns left in A and right in D, so the head draws the sides + // of its own triangles. + tw.fill(0); + trgb.fill(0); + const { tStep, tPos, tState, nt } = S, q = S.q; + const TX = S.tx, TY = S.ty; + for (let i = 0; i < nt; i++) { + const cc = (1 - q) * ((i + 1) / sigNow) + q * (tStep[i] / max(1, S.steps)); + const y = min(H - 1, floor(cc * H)), x = min(W - 1, floor((tPos[i] - lo) / C)), P = S.sc[tState[i]]; + TX[i] = x; + TY[i] = y; + // Joined to the turn two before it, on the same side, so the sides + // stay whole lines while a sweep is taller than a row. + let x1 = x, y1 = y; + if (i >= 2 && tState[i - 2] === tState[i] && abs(TX[i - 2] - x) <= 3 && y - TY[i - 2] <= 8) (x1 = TX[i - 2]), (y1 = TY[i - 2]); + const n = max(1, abs(x - x1), y - y1); + for (let k = n === 1 && x1 === x && y1 === y ? 0 : 1; k <= n; k++) { + const px = round(x1 + ((x - x1) * k) / n), py = round(y1 + ((y - y1) * k) / n), p = py * W + px; + tw[p]++; + trgb[p * 3] += P[0]; + trgb[p * 3 + 1] += P[1]; + trgb[p * 3 + 2] += P[2]; + } + } + for (let p = 0; p < W * H; p++) { + if (!tw[p]) continue; + const w = min(1, 0.6 + tw[p] * 0.2), o = p * 3, n = tw[p]; + for (let i = 0; i < 3; i++) rgb[o + i] = rgb[o + i] * (1 - w) + (trgb[o + i] / n) * w; + } + // The head now, at the foot of the diagram. + if (!S.halted) { + const x = min(W - 1, floor((S.h - lo) / C)), P = S.sc[S.st]; + for (let y = H - 4; y < H; y++) { + const o = (y * W + x) * 3; + rgb[o] = y === H - 1 ? 255 : P[0]; + rgb[o + 1] = y === H - 1 ? 255 : P[1]; + rgb[o + 2] = y === H - 1 ? 255 : P[2]; + } + } +} + +// A small machine: one row band for every step, the cells as blocks over +// the tape its whole run will use, a dark line between bands and cells +// once they are 4 LEDs or more. +function drawSmall(S) { + const { rgb } = S, col = [0, 0, 0], F = S.fin, ext = F.hi - F.lo + 1; + const cw = floor(W / ext), x0 = floor((W - cw * ext) / 2), rh = floor(SMALL_H / (F.steps + 1)); + for (let j = 0; j < S.ns; j++) { + const sl = S.sLo[j], sh = S.sHi[j], hd = S.sHead[j], st = S.sState[j]; + for (let p = sl; p <= sh; p++) { + const one = cum(S, j, p + 1) - cum(S, j, p) > 0.5; + shade(S, one ? 1 : 0, col); + if (p === hd) { + const P = st >= 0 ? S.sc[st] : S.white; + for (let i = 0; i < 3; i++) col[i] = one ? P[i] * 0.75 + col[i] * 0.25 : P[i] * 0.8; + } + const xa = x0 + (p - F.lo) * cw, xb = xa + cw - (cw >= 4 && rh >= 4 ? 1 : 0); + const ya = j * rh, yb = ya + rh - (rh >= 4 ? 1 : 0); + for (let y = ya; y < yb; y++) + for (let x = xa; x < xb; x++) { + const o = (y * W + x) * 3; + rgb[o] = col[0]; + rgb[o + 1] = col[1]; + rgb[o + 2] = col[2]; + } + } + } +} + +function number(rgb, n, x, y, P) { + const s = String(n); + for (let i = 0; i < s.length; i++) { + const g = DIG[s.charCodeAt(i) - 48]; + for (let k = 0; k < 15; k++) + if (g[k] === "1") { + const o = ((y + floor(k / 3)) * W + x + 4 * i + (k % 3)) * 3; + rgb[o] = P[0]; + rgb[o + 1] = P[1]; + rgb[o + 2] = P[2]; + } + } +} + +// Orange and yellow read brown on an LED when dim, so the tape takes its +// hue from a wheel without 0.03 to 0.17. +const wheel = (h) => 0.17 + 0.86 * (h - floor(h)); + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { rgb, c } = S; + rgb.fill(0); + const h0 = wheel(0.477 + S.hue), P = S.pal; + hsv(h0, 0.9, 0.5, c); + P[0] = c[0]; P[1] = c[1]; P[2] = c[2]; + P[3] = 7; P[4] = 9; P[5] = 16; + hsv(h0 - 0.03, 0.28, 1, c); + P[6] = c[0]; P[7] = c[1]; P[8] = c[2]; + for (let i = 0; i < 5; i++) { + hsv(h0 + SH[i], 0.75, 1, c); + S.sc[i][0] = c[0]; S.sc[i][1] = c[1]; S.sc[i][2] = c[2]; + } + if (S.m === 0) drawChampion(S); + else drawSmall(S); + // The counts: steps, then the 1s on the tape now. + let ones = 0; + for (let b = S.lo >> S.bs; b <= S.hi >> S.bs; b++) ones += S.blk[b]; + hsv(h0 + 0.5, 0.12, S.halted ? 0.95 : 0.55, c); + const y = S.m === 0 ? 1 : 114; + number(rgb, S.steps, 1, y, c); + hsv(h0 - 0.03, 0.35, S.halted ? 1 : 0.7, c); + number(rgb, ones, 1, y + 7, c); + for (let p = 0, yy = 0; yy < H; yy++) + for (let x = 0; x < W; x++, p += 3) display.setPixel(x, yy, min(255, rgb[p]), min(255, rgb[p + 1]), min(255, rgb[p + 2])); +} From 54ce28a861d412ceecf57a63e513d38bc0d2698b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 15:26:27 +0000 Subject: [PATCH 3/8] patterns: Veech Octagon and Yard-Sale Model for round 62 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TTZyp5h85ewavQ9av2v4N1 --- .../pattern-study/patterns/veech-octagon.js | 319 ++++++++++++++++++ incoming/pattern-study/patterns/yard-sale.js | 251 ++++++++++++++ 2 files changed, 570 insertions(+) create mode 100644 incoming/pattern-study/patterns/veech-octagon.js create mode 100644 incoming/pattern-study/patterns/yard-sale.js diff --git a/incoming/pattern-study/patterns/veech-octagon.js b/incoming/pattern-study/patterns/veech-octagon.js new file mode 100644 index 000000000..7abcc1440 --- /dev/null +++ b/incoming/pattern-study/patterns/veech-octagon.js @@ -0,0 +1,319 @@ +// ===== Patternflow pattern ===== +// Title: Veech Octagon +// Family: topology +// Idea: Four straight lines on each surface run in one shared direction across the regular octagon with opposite sides glued, above, and the double regular pentagon, below, leaving by a side and coming back by the side glued to it; in most directions they fill their surface evenly, and in the directions Veech's dichotomy picks out they close up and the surface falls apart into two cylinders of closed lines, each its own colour. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: straight-line flow on flat surfaces (Ralph Fox and Richard Kershner, 1936), unique ergodicity in almost every direction (Steven Kerckhoff, Howard Masur and John Smillie, 1986), translation surfaces from regular polygons and the Veech dichotomy (William Veech, 1989), pattern round 62 +// Round: 62 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Direction=45..135, Trail=0..1 +// +// Knob 1: Hue. Turns the two cylinders round the wheel together. At 0 the shorter cylinder is azure and the longer violet, the lines pale lavender with a white comet at each head, and a surface whose direction fills it dark slate; at 0.25 violet and red, at 0.5 red and green, at 0.75 green and teal. Orange and dark yellow are skipped, as they read brown. +// Knob 2: Speed. The lines run 0.8 x Speed sides a second, 1.6 at 2 (36 LEDs a second), so a line in the octagon's middle cylinder, 2.41 sides round, closes every 1.5 s. Each line keeps the stretch it ran in the last Trail seconds, so Speed sets its length too: at 0.2 a line is 0.9 sides long and no longer reaches round its cylinder, at 3 it is 13 sides. +// Knob 3: Direction. The direction of every line, in degrees from the horizontal. Within 1 degree of a periodic direction it snaps to it. The octagon's are 45, 90 and 135, along its sides, where it splits into its middle band, 1 + sqrt 2 = 2.414 sides round, and the two trapezoids either side of it glued into one cylinder 2 + sqrt 2 = 3.414 round, with inverse moduli 2.414 and 4.828, one exactly twice the other; and 67.5 and 112.5, along its diagonals, where the cylinders are 1.848 and 4.461 round and both have inverse modulus 2 cot(pi/8) = 4.828, the thin one showing as two corner triangles. The double pentagon's are 72 and 108, along its sides, cylinders phi = 1.618 and phi^2 = 2.618 round with inverse moduli both 2 cot(pi/5) = 2.753, and 54, 90 and 126, through its corners, 4.980 and 8.057 round with inverse moduli both 26.07, tan^2 72 = 9.47 times thinner. At 90 both surfaces close. Between the snaps the knob passes periodic directions too, densely, but they close only on long lines, 11.90 and 16.84 sides at 56.70 for the octagon, and a direction off them by any amount fills. It opens at 45: the octagon closed, the pentagon filling. +// Knob 4: Trail. How many seconds of line each head keeps, T = 4 + 56 K4^1.3: 5.5 s at the opening, 8.7 sides at Speed 2, so each closed line in the octagon has been round 2.5 to 3.6 times and glows, while in the pentagon the same length lies as fresh parallel strokes. Every part is as bright as it is recent, falling to nothing at T. At 1 the trail is 60 s, 96 sides, and a surface that fills is an even silver. +// Buttons: K1 tap starts all eight lines again from new random points. The first ones start from fixed points, two in each cylinder of the octagon at 45 degrees. + +const W = 64, H = 128, NP = W * H; +const { floor, min, max, sqrt, cos, sin, abs, round, ceil, random, 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; +} +// The wheel without orange and dark yellow, 0.03 to 0.17, which read brown. +const wheel = (h) => 0.17 + 0.86 * (h - floor(h)); + +// A translation surface: convex polygons with side 1, y up, each side glued +// to a parallel side by a translation. For a polygon symmetric about its +// centre, or a polygon and its copy turned through a half turn, the side +// from a to b goes to the side from -b to -a, and the translation is +// -(a + b). +function build(polys, partner) { + return polys.map((V, p) => + V.map((a, i) => { + const b = V[(i + 1) % V.length], ex = b[0] - a[0], ey = b[1] - a[1], [to, j] = partner(p, i); + return { ax: a[0], ay: a[1], ex, ey, nx: -ey, ny: ex, len2: ex * ex + ey * ey, to, j, tx: -(a[0] + b[0]), ty: -(a[1] + b[1]) }; + }), + ); +} +// The regular octagon, sides horizontal at the top and bottom, side i glued +// to side i + 4. Area 2 (1 + sqrt 2) = 4.83. +const RO = (1 + sqrt(2)) / 2; +const OCT_V = [[-0.5, -RO], [0.5, -RO], [RO, -0.5], [RO, 0.5], [0.5, RO], [-0.5, RO], [-RO, 0.5], [-RO, -0.5]]; +// The double pentagon: a regular pentagon standing on a horizontal side and +// its copy turned through a half turn, glued along that side, every other +// side of one glued to the parallel side of the other. Area 3.44. +const C72 = cos((2 * PI) / 5), S72 = sin((2 * PI) / 5), HP = S72 + sin(PI / 5); +const PEN_A = [[-0.5, 0], [0.5, 0], [0.5 + C72, S72], [0, HP], [-0.5 - C72, S72]]; +const PEN_B = PEN_A.map(([x, y]) => [-x, -y]); + +// On the panel: 22.8 LEDs a side. The octagon, 55 LEDs across, at the top; +// the double pentagon, 37 across and 70 tall, below it. +const SC = 22.8; +const SURF = [ + { polys: [OCT_V], E: build([OCT_V], (p, i) => [0, (i + 4) % 8]), cx: 32, cy: 28.1, dirs: [45, 67.5, 90, 112.5, 135] }, + { polys: [PEN_A, PEN_B], E: build([PEN_A, PEN_B], (p, i) => [1 - p, i]), cx: 32, cy: 92.6, dirs: [54, 72, 90, 108, 126] }, +]; +const SNAP = 1; // degrees either side of a periodic direction that snap to it +const RAYS = 4; // lines on each surface +const VEL = 0.8; // sides a second at Speed 1 +const COMET = 0.5; // sides of white behind each head + +// From (x, y) in polygon p along (ux, uy): the distance to the side the line +// leaves by, that side, and where along it, 0 to 1. +const EX = [0, 0, 0]; +function exit(E, p, x, y, ux, uy, skip) { + const es = E[p]; + let bt = Infinity, bi = -1; + for (let i = 0; i < es.length; i++) { + if (i === skip) continue; + const e = es[i], un = ux * e.nx + uy * e.ny; + if (un >= -1e-15) continue; + const t = ((e.ax - x) * e.nx + (e.ay - y) * e.ny) / un; + if (t < bt) (bt = t), (bi = i); + } + if (bi < 0) return null; + const e = es[bi]; + EX[0] = bt < 0 ? 0 : bt; + EX[1] = bi; + EX[2] = ((x + bt * ux - e.ax) * e.ex + (y + bt * uy - e.ay) * e.ey) / e.len2; + return EX; +} +// The length of the closed line through (x0, y0) in polygon p0, or -1 when +// it runs into the corner, the one cone point of angle 6 pi, first. +function period(E, p0, x0, y0, ux, uy) { + let p = p0, x = x0, y = y0, skip = -1, L = 0; + for (let k = 0; k < 120; k++) { + const r = exit(E, p, x, y, ux, uy, skip); + if (!r) return -1; + const t = r[0], i = r[1], s = r[2]; + if (k > 0 && p === p0) { + const dx = x0 - x, dy = y0 - y, along = dx * ux + dy * uy; + if (abs(dx * uy - dy * ux) < 1e-9 && along > -1e-12 && along <= t + 1e-12) return L + along; + } + if (s < 1e-9 || s > 1 - 1e-9) return -1; + const e = E[p][i]; + x += t * ux + e.tx; + y += t * uy + e.ty; + L += t; + p = e.to; + skip = e.j; + } + return -1; +} +function inside(E, p, x, y) { + for (const e of E[p]) if ((x - e.ax) * e.nx + (y - e.ay) * e.ny < 0) return false; + return true; +} + +export function setup(params) { + // Every LED whose centre falls inside a polygon, with its point there. + const who = new Int8Array(NP).fill(-1), poly = new Int8Array(NP), px = new Float64Array(NP), py = new Float64Array(NP); + for (let s = 0; s < 2; s++) { + const S = SURF[s]; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const mx = (x + 0.5 - S.cx) / SC, my = (S.cy - y - 0.5) / SC; + for (let p = 0; p < S.E.length; p++) + if (inside(S.E, p, mx, my)) { + const o = y * W + x; + (who[o] = s), (poly[o] = p), (px[o] = mx), (py[o] = my); + } + } + } + params.s = { + who, poly, px, py, maps: {}, heads: [null, null], dir: 45, started: false, prev: false, + k: [0, 2, 45, 0.06], rgb: new Float32Array(NP * 3), tb: new Float32Array(NP), c: new Float32Array(24), clock: 0, + }; +} + +// The cylinders of surface s in direction d: every LED inside it gets the +// rank of its closed line's length, shortest first, so each cylinder is one +// number. +function cylinders(S, s, d) { + const key = s + ":" + d; + if (S.maps[key]) return S.maps[key]; + const E = SURF[s].E, ux = cos((d * PI) / 180), uy = sin((d * PI) / 180); + const len = new Float64Array(NP).fill(-1), lens = []; + for (let o = 0; o < NP; o++) { + if (S.who[o] !== s) continue; + const c = period(E, S.poly[o], S.px[o], S.py[o], ux, uy); + len[o] = c; + if (c > 0 && !lens.some((v) => abs(v - c) < 1e-6)) lens.push(c); + } + lens.sort((a, b) => a - b); + const rank = new Int8Array(NP).fill(-1); + for (let o = 0; o < NP; o++) if (len[o] > 0) rank[o] = lens.findIndex((v) => abs(v - len[o]) < 1e-6); + return (S.maps[key] = { rank, lens }); +} + +// The first lines start at fixed points, two in each cylinder of the +// octagon at 45 degrees; a tap of K1 draws new ones at random. +const FIRST = [ + [[0, 0.15, 0.1], [0, -0.75, 0.55], [0, 0.55, -0.85], [0, 0.95, 0.35]], + [[0, 0.05, 0.6], [0, -0.3, 1.0], [1, 0.2, -0.5], [1, -0.35, -1.0]], +]; +function launch(s) { + const E = SURF[s].E; + for (;;) { + const p = floor(random() * E.length), x = (random() - 0.5) * 3.2, y = (random() - 0.5) * 3.2; + if (inside(E, p, x, y)) return { p, x, y, skip: -1 }; + } +} +// Move a point a distance L along (ux, uy), through the glued sides. +function flow(E, h, L, ux, uy) { + for (let k = 0; k < 400 && L > 0; k++) { + const r = exit(E, h.p, h.x, h.y, ux, uy, h.skip); + if (!r) return; + const t = r[0], i = r[1]; + if (t > L) { + h.x += L * ux; + h.y += L * uy; + h.skip = -1; + return; + } + const e = E[h.p][i]; + h.x += t * ux + e.tx; + h.y += t * uy + e.ty; + h.p = e.to; + h.skip = e.j; + L -= t; + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 45, 0.06], d = cl(dt || 0, 0, 0.1); + S.k[0] = k[0]; + S.k[1] = cl(k[1], 0.2, 3); + S.k[2] = cl(k[2], 45, 135); + S.k[3] = cl(k[3], 0, 1); + const press = !!(input.btnPressed && input.btnPressed[0]); + if (!S.started || (press && !S.prev)) { + S.heads = S.started + ? [0, 1].map((s) => Array.from({ length: RAYS }, () => launch(s))) + : FIRST.map((f) => f.map(([p, x, y]) => ({ p, x, y, skip: -1 }))); + S.started = true; + } + S.prev = press; + // Snap to the nearest direction in which either surface is periodic. + let dir = S.k[2]; + for (const sf of SURF) for (const p of sf.dirs) if (abs(dir - p) <= SNAP) dir = p; + S.dir = dir; + const ux = cos((dir * PI) / 180), uy = sin((dir * PI) / 180); + for (let s = 0; s < 2; s++) for (const hd of S.heads[s]) flow(SURF[s].E, hd, VEL * S.k[1] * d, ux, uy); + S.clock += d; +} + +function splat(S, x, y, v, c, o) { + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + add(S, ix, iy, (1 - fx) * (1 - fy) * v, c, o); + add(S, ix + 1, iy, fx * (1 - fy) * v, c, o); + add(S, ix, iy + 1, (1 - fx) * fy * v, c, o); + add(S, ix + 1, iy + 1, fx * fy * v, c, o); +} +function splatT(S, x, y, v) { + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy, T = S.tb; + if (ix < 0 || iy < 0 || ix >= W - 1 || iy >= H - 1) return; + const p = iy * W + ix; + T[p] += (1 - fx) * (1 - fy) * v; + T[p + 1] += fx * (1 - fy) * v; + T[p + W] += (1 - fx) * fy * v; + T[p + W + 1] += fx * fy * v; +} +function add(S, x, y, v, c, o) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const p = (y * W + x) * 3; + S.rgb[p] += v * c[o]; + S.rgb[p + 1] += v * c[o + 1]; + S.rgb[p + 2] += v * c[o + 2]; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { rgb, c, who } = S, h = S.k[0], dir = S.dir; + rgb.fill(0); + S.tb.fill(0); + // Colours: 0 and 3 the two cylinders, 6 a surface in a direction that + // fills it, 9 the sides, 12 the line, 15 its head. + hsv(wheel(0.48 + h), 0.85, 0.42, c, 0); + hsv(wheel(0.7 + h), 0.7, 0.42, c, 3); + hsv(wheel(0.6 + h), 0.35, 0.11, c, 6); + hsv(wheel(0.6 + h), 0.25, 0.42, c, 9); + hsv(wheel(0.62 + h), 0.15, 1, c, 12); + c[15] = c[16] = c[17] = 1; + + for (let s = 0; s < 2; s++) { + const sf = SURF[s], periodic = sf.dirs.includes(dir), m = periodic ? cylinders(S, s, dir) : null; + for (let o = 0; o < NP; o++) { + if (who[o] !== s) continue; + const r = m ? m.rank[o] : -1, q = r === 0 ? 0 : r === 1 ? 3 : 6, p = o * 3; + rgb[p] = c[q]; + rgb[p + 1] = c[q + 1]; + rgb[p + 2] = c[q + 2]; + } + // The sides, the double pentagon's inner side dotted. + for (let pi = 0; pi < sf.E.length; pi++) + for (let i = 0; i < sf.E[pi].length; i++) { + const e = sf.E[pi][i], inner = s === 1 && i === 0; + if (inner && pi === 1) continue; + const x0 = sf.cx + SC * e.ax, y0 = sf.cy - SC * e.ay, n = ceil(SC * 3); + for (let j = 0; j <= n; j++) { + if (inner && j % 6 > 2) continue; + splat(S, x0 + (SC * e.ex * j) / n, y0 - (SC * e.ey * j) / n, inner ? 0.18 : 0.36, c, 9); + } + } + // The lines: back from each head as far as it came in the last Trail + // seconds, each part as bright as it is recent, so where a line closes + // on itself it adds up. + const Ltr = VEL * S.k[1] * (4 + 56 * S.k[3] ** 1.3), E = sf.E, wpass = 0.7 * min(1, sqrt(10 / Ltr)); + const ux = cos((dir * PI) / 180), uy = sin((dir * PI) / 180); + for (const hd of S.heads[s]) { + const b = { p: hd.p, x: hd.x, y: hd.y, skip: -1 }; + let done = 0; + for (let kk = 0; kk < 600 && done < Ltr; kk++) { + const r = exit(E, b.p, b.x, b.y, -ux, -uy, b.skip); + if (!r) break; + const t = min(r[0], Ltr - done), i = r[1]; + const n = max(1, ceil(t * SC * 3)); + for (let j = 0; j <= n; j++) { + const u = (t * j) / n, w = 1 - (done + u) / Ltr; + const ww = (t * SC) / n; + splatT(S, sf.cx + SC * (b.x - u * ux), sf.cy - SC * (b.y - u * uy), wpass * w * ww); + if (done + u < COMET) splat(S, sf.cx + SC * (b.x - u * ux), sf.cy - SC * (b.y - u * uy), 0.7 * (1 - (done + u) / COMET) * ww, c, 15); + } + done += t; + if (t < r[0]) break; + if (r[2] < 1e-9 || r[2] > 1 - 1e-9) break; + const e = E[b.p][i]; + b.x -= t * ux - e.tx; + b.y -= t * uy - e.ty; + b.p = e.to; + b.skip = e.j; + } + splat(S, sf.cx + SC * hd.x, sf.cy - SC * hd.y, 1.2, c, 15); + } + } + // The lines over the surfaces, through 1 - e^(-x), so a line that closes + // and runs over itself many times stays below the white of the heads. + for (let o = 0, p = 0; o < NP; o++, p += 3) { + const v = S.tb[o]; + if (v <= 0) continue; + const g = 0.86 * (1 - Math.exp(-1.6 * v)); + rgb[p] += g * c[12]; + rgb[p + 1] += g * c[13]; + rgb[p + 2] += g * c[14]; + } + + for (let p = 0, y = 0; y < H; y++) + for (let x = 0; x < W; x++, p += 3) display.setPixel(x, y, round(min(1, rgb[p]) * 255), round(min(1, rgb[p + 1]) * 255), round(min(1, rgb[p + 2]) * 255)); +} diff --git a/incoming/pattern-study/patterns/yard-sale.js b/incoming/pattern-study/patterns/yard-sale.js new file mode 100644 index 000000000..969facd0f --- /dev/null +++ b/incoming/pattern-study/patterns/yard-sale.js @@ -0,0 +1,251 @@ +// ===== Patternflow pattern ===== +// Title: Yard-Sale Model +// Family: data +// Idea: Sixty-four agents start with equal wealth and trade in random pairs, a fair coin deciding who takes a fraction of the poorer one's wealth, and the Lorenz curve on top sags from its diagonal, the sorted fortunes below fall away to one tall bar, and the Gini coefficient climbs toward 1 along the bottom, unless a tax pulls everyone back toward the mean. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Lorenz curve (Max Lorenz, 1905), Gini coefficient (Corrado Gini, 1912), yard-sale model of asset exchange (Anirban Chakraborti, 2002; named by Brian Hayes, 2002), wealth condensation and redistribution (Bruce Boghosian and coauthors, 2014 and 2017), pattern round 62 +// Round: 62 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Fraction=0.2..0.6, Tax=0..1 +// +// Knob 1: Hue. Turns the picture round the wheel. At 0 the gap between the diagonal and the Lorenz curve is azure, the bars rise from violet through blue to azure at the starting level and pale toward the top, and the curve, the trace and the one followed agent are cream; at 0.25 the gap is violet, at 0.5 red, at 0.75 green. Orange and dark yellow are skipped, as they read brown. +// Knob 2: Speed. The clock a run is played on. A run is 1600 rounds of 32 trades, one trade per agent a round on average, on a log clock: the rounds done plus 40 grow e-fold every 5.4 s at Speed 2, so rounds 0 to 40, 40 to 120, 120 to 280 and so on each take 3.7 s, and the run reaches 1600 at 20 s. The trace across the bottom is drawn against the same log clock. At 0.2 ten times slower, at 3 one and a half times faster. The first run opens 14 rounds in, already off the diagonal; every later one starts equal. +// Knob 3: Fraction. The share f of the poorer agent's wealth a trade moves. Over 16 economies at a time with no tax, the Gini coefficient averaged 0.54 at 50 rounds, 0.66 at 100, 0.77 at 200, 0.86 at 400, 0.92 at 800 and 0.95 at 1600 for f = 0.2; for f = 0.4 it was 0.77 at 50 rounds and 0.92 at 200, for f = 0.6 0.88 at 50. A run stops when the economy on the panel reaches 0.95 or 1600 rounds, holds 2.5 s and starts again equal: every 22 s at f = 0.2 (23 of 54 runs went the whole 1600 rounds), every 15 s at 0.4, every 11 s at 0.6. +// Knob 4: Tax. Up to 0.1 there is none; above it every agent pays chi = 0.08 ((K4 - 0.1) / 0.9)^2 of its wealth a round into a pot shared equally, Boghosian's redistribution, and the Gini settles: at f = 0.2 on 0.69 at K4 = 0.25 (chi = 0.0022), 0.41 at 0.5 (0.0158), 0.28 at 0.75 (0.042) and 0.21 at 1 (0.08). In the Fokker-Planck limit it depends on chi / f^2 only; at chi / f^2 = 0.395 the steady Gini was 0.407, 0.403 and 0.396 for f = 0.2, 0.3 and 0.4. +// Buttons: K1 tap restarts the economy with everyone equal. + +const W = 64, H = 128, NP = W * H; +const { floor, min, max, log, round, random, abs } = 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; +} +// The wheel without orange and dark yellow, 0.03 to 0.17, which read brown. +const wheel = (h) => 0.17 + 0.86 * (h - floor(h)); + +// N agents, W0 units each, so the total is N W0 = 64000 whole units and no +// trade or tax can make or lose one. +const N = 64, W0 = 1000, TOTAL = N * W0; +// The economy on the panel and 15 more run side by side from the same +// start; the band their Gini coefficients span is drawn behind its own. +const E = 16; +// A round is N / 2 trades, one for each agent on average. A run is 1600 +// rounds on a clock that runs in log time, r = R0 (e^(t / TAU) - 1), so +// rounds 0 to 40, 40 to 120, 120 to 280 and so on each take the same time: +// TAU = 5.4 s at Speed 2 and the whole run 20 s. +const R0 = 40, RUN = 1600, LRUN = log(1 + RUN / R0), TAU = 5.4; +const LZ = 63, BAR0 = 101, BARH = 34, TR0 = 127, TRH = 23; +const GSTOP = 0.95, HOLD = 2.5, BLOG = 20; + +export function setup(params) { + params.s = { + w: new Int32Array(N * E), sorted: new Int32Array(N), ids: new Int32Array(N), L: new Float32Array(N + 1), + trace: new Float32Array(W).fill(-1), lo: new Float32Array(W).fill(-1), hi: new Float32Array(W).fill(-1), + rgb: new Float32Array(NP * 3), c: new Float32Array(12), + old: new Float32Array(NP * 3), rounds: 0, clock: 0, gini: 0, hold: 0, fade: 0, started: false, + k: [0, 2, 0.2, 0.06], prev: false, + }; +} + +function restart(S) { + S.w.fill(W0); + S.trace.fill(-1); + S.lo.fill(-1); + S.hi.fill(-1); + S.trace[0] = S.lo[0] = S.hi[0] = 0; + S.rounds = S.clock = S.gini = S.hold = 0; + S.old.set(S.rgb); + S.fade = 1; +} + +// One round of economy e: N / 2 trades, then the tax. In a trade two agents +// are drawn, a fair coin picks the winner, and the winner takes f times the +// poorer agent's wealth, rounded at random to a whole unit so the mean is +// exact. The tax takes chi of every agent's wealth, rounded the same way, +// and shares the pot out equally; the units it cannot share go one each to +// agents drawn at random. +function round1(w, o, f, chi) { + for (let t = 0; t < N / 2; t++) { + const i = o + floor(random() * N); + let j = o + floor(random() * (N - 1)); + if (j >= i) j++; + const d = floor(f * min(w[i], w[j]) + random()); + if (random() < 0.5) (w[i] += d), (w[j] -= d); + else (w[i] -= d), (w[j] += d); + } + if (chi > 0) { + let pot = 0; + for (let i = o; i < o + N; i++) { + const p = floor(chi * w[i] + random()); + w[i] -= p; + pot += p; + } + const each = floor(pot / N); + for (let i = o; i < o + N; i++) w[i] += each; + for (let r = pot - each * N; r > 0; r--) w[o + floor(random() * N)]++; + } +} + +// Sort economy e by wealth and read its Gini coefficient off the Lorenz +// curve: G = 1 - 2 * (area under it), the area by trapezoids, exact in +// whole units. +function sortAndGini(S, e) { + const { w, ids, sorted } = S, o = e * N; + for (let i = 0; i < N; i++) ids[i] = i; + ids.sort((a, b) => w[o + a] - w[o + b] || a - b); + let cum = 0, area = 0; + for (let k = 0; k < N; k++) { + const v = w[o + ids[k]]; + sorted[k] = v; + area += 2 * cum + v; + cum += v; + } + return 1 - area / (N * TOTAL); +} +// The column of the trace a round falls in. +const colOf = (r) => min(W - 1, round(((W - 1) * log(1 + r / R0)) / LRUN)); + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0.2, 0.06], d = cl(dt || 0, 0, 0.1); + S.k[0] = k[0]; + S.k[1] = cl(k[1], 0.2, 3); + S.k[2] = cl(k[2], 0.2, 0.6); + S.k[3] = cl(k[3], 0, 1); + const f = S.k[2], t4 = max(0, (S.k[3] - 0.1) / 0.9), chi = 0.08 * t4 * t4; + const press = !!(input.btnPressed && input.btnPressed[0]); + if (!S.started || (press && !S.prev)) { + restart(S); + // The first run opens 0.3 TAU into its clock, 14 rounds in, so the + // curve has already left the diagonal; every later run starts equal. + if (!S.started) (S.clock = 0.3 * TAU), (S.fade = 0); + S.started = true; + } + S.prev = press; + S.fade = max(0, S.fade - d / 0.6); + if (S.hold > 0) { + S.hold += d; + if (S.hold > HOLD) restart(S); + } else { + S.clock += (d * S.k[1]) / 2; + const target = min(RUN, floor(R0 * (Math.exp(S.clock / TAU) - 1))); + while (S.rounds < target) { + for (let e = 0; e < E; e++) round1(S.w, e * N, f, chi); + S.rounds++; + const col = colOf(S.rounds); + if (col !== colOf(S.rounds + 1) || S.rounds === target) { + const g = sortAndGini(S, 0); + let lo = g, hi = g; + for (let e = 1; e < E; e++) { + const ge = sortAndGini(S, e); + lo = min(lo, ge); + hi = max(hi, ge); + } + S.trace[col] = g; + S.lo[col] = lo; + S.hi[col] = hi; + } + } + if (S.rounds >= RUN) S.hold = 1e-6; + } + S.gini = sortAndGini(S, 0); + if (S.hold === 0 && S.gini >= GSTOP) S.hold = 1e-6; +} + +function add(rgb, x, y, r, g, b) { + if (x < 0 || y < 0 || x >= W || y >= H) return; + const o = (y * W + x) * 3; + rgb[o] += r; + rgb[o + 1] += g; + rgb[o + 2] += b; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { rgb, c, sorted, L } = S, h = S.k[0]; + sortAndGini(S, 0); + rgb.fill(0); + // Colours: 0 the gap between the diagonal and the curve, 3 the share + // under the curve, 6 the curve and the trace. + hsv(wheel(0.48 + h), 0.75, 0.52, c, 0); + hsv(wheel(0.68 + h), 0.6, 0.1, c, 3); + hsv(0.12 + h, 0.15, 1, c, 6); + + // The Lorenz square, rows 0 to 63: share of agents across, share of + // wealth up. L(k) is the wealth of the poorest k agents over the total. + L[0] = 0; + for (let kk = 0; kk < N; kk++) L[kk + 1] = L[kk] + sorted[kk] / TOTAL; + for (let x = 0; x < W; x++) { + const u = (x + 0.5) / W, p = u * N, i = min(N - 1, floor(p)), lv = L[i] + (L[i + 1] - L[i]) * (p - i); + const yd = LZ - u * LZ, yl = LZ - lv * LZ; + for (let y = 0; y <= LZ; y++) { + if (y < yd - 0.5) continue; + if (y < yl - 0.5) add(rgb, x, y, c[0] * 255, c[1] * 255, c[2] * 255); + else add(rgb, x, y, c[3] * 255, c[4] * 255, c[5] * 255); + } + } + for (let x = 0; x < W; x++) add(rgb, x, round(LZ - ((x + 0.5) / W) * LZ), 40, 40, 40); + // A dot every tenth of the agents and every tenth of the wealth. + for (let i = 1; i < 10; i++) + for (let j = 1; j < 10; j++) add(rgb, round((i * W) / 10 - 0.5), round(LZ - (j * LZ) / 10), 34, 34, 38); + // The curve as an unbroken line: in each column, every row between its + // heights at the column's two edges. + for (let x = 0; x < W; x++) { + const ya = LZ - L[x] * LZ, yb = LZ - L[x + 1] * LZ; + for (let y = round(yb); y <= round(ya); y++) add(rgb, x, y, c[6] * 230, c[7] * 230, c[8] * 230); + } + + // The sorted fortunes, rows 67 to 101, poorest at the left, on a log + // scale, log(1 + w / 20) over log(1 + all / 20), each LED coloured by its + // height: violet low, the hue of the gap at the starting level, paler + // above it. Agent 0 is drawn cream, so one fortune can be followed as it + // changes rank. + const top = log(1 + TOTAL / BLOG), y0 = (BARH * log(1 + W0 / BLOG)) / top; + for (let kk = 0; kk < N; kk++) { + const hgt = (BARH * log(1 + sorted[kk] / BLOG)) / top; + for (let y = 0; y < hgt; y++) { + const a = min(1, hgt - y); + if (y < y0) hsv(wheel(0.72 + h - (0.24 * y) / y0), 0.8, 0.45 + (0.35 * y) / y0, c, 0); + else hsv(wheel(0.48 + h), 0.8 * (1 - (y - y0) / (BARH - y0)) + 0.1, 0.8 + (0.2 * (y - y0)) / (BARH - y0), c, 0); + if (S.ids[kk] === 0) add(rgb, kk, BAR0 - y, c[6] * 230 * a, c[7] * 230 * a, c[8] * 230 * a); + else add(rgb, kk, BAR0 - y, c[0] * 255 * a, c[1] * 255 * a, c[2] * 255 * a); + } + } + for (let x = 0; x < W; x += 2) add(rgb, x, round(BAR0 - y0), 50, 50, 50); + + // The Gini trace, rows 104 to 127, against rounds on a log scale: G = 0 at + // the bottom and 1 at the top, the highest a 64-agent economy can reach, + // 1 - 1/64, dotted. Behind the panel's own economy, bright, the band from + // the lowest to the highest of the sixteen. + for (let x = 0; x < W; x += 2) add(rgb, x, round(TR0 - (1 - 1 / N) * TRH), 50, 50, 50); + hsv(wheel(0.48 + h), 0.75, 0.42, c, 0); + hsv(wheel(0.48 + h), 0.75, 0.16, c, 3); + for (let x = 0; x < W; x++) { + const g = S.trace[x]; + if (g < 0) continue; + const yt = round(TR0 - g * TRH), ylo = round(TR0 - S.lo[x] * TRH), yhi = round(TR0 - S.hi[x] * TRH); + for (let y = yhi; y <= TR0; y++) { + const o = y <= ylo ? 0 : 3; + add(rgb, x, y, c[o] * 255, c[o + 1] * 255, c[o + 2] * 255); + } + add(rgb, x, yt, c[6] * 255, c[7] * 255, c[8] * 255); + } + + const fd = S.fade, old = S.old; + for (let p = 0, y = 0; y < H; y++) + for (let x = 0; x < W; x++, p += 3) { + let r = min(255, rgb[p]), g = min(255, rgb[p + 1]), b = min(255, rgb[p + 2]); + if (fd > 0) (r = r * (1 - fd) + old[p] * fd), (g = g * (1 - fd) + old[p + 1] * fd), (b = b * (1 - fd) + old[p + 2] * fd); + (rgb[p] = r), (rgb[p + 1] = g), (rgb[p + 2] = b); + display.setPixel(x, y, r, g, b); + } +} From 1b8dc8ad31aae232e448a7fe621e5e761f6b6643 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 15:26:38 +0000 Subject: [PATCH 4/8] patterns: Void and Cluster and Heesch Tiles for round 62 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TTZyp5h85ewavQ9av2v4N1 --- .../pattern-study/patterns/heesch-tiles.js | 408 ++++++++++++++++++ .../patterns/void-and-cluster.js | 283 ++++++++++++ 2 files changed, 691 insertions(+) create mode 100644 incoming/pattern-study/patterns/heesch-tiles.js create mode 100644 incoming/pattern-study/patterns/void-and-cluster.js diff --git a/incoming/pattern-study/patterns/heesch-tiles.js b/incoming/pattern-study/patterns/heesch-tiles.js new file mode 100644 index 000000000..01f5a0242 --- /dev/null +++ b/incoming/pattern-study/patterns/heesch-tiles.js @@ -0,0 +1,408 @@ +// ===== Patternflow pattern ===== +// Title: Heesch Tiles +// Family: cells +// Idea: One tile whose edges keep bending covers the whole panel with no gap and no overlap, each bent edge carried onto its partner the way Heesch paired them (translated T, half-turned C about its own midpoint, glide-reflected G, or turned about a shared corner, C3 and C4), and every 8 s the tiling passes on to the next of six Heesch types, each copy coloured by which way it is turned or flipped and brightest at its first corner. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the 28 types of plane tiling by one asymmetric tile (Heinrich Heesch, 1935), Flächenschluss (Heinrich Heesch and Otto Kienzle, 1963), the 93 isohedral tiling types (Branko Grünbaum and G. C. Shephard, 1977), isohedral tilings parameterised for drawing (Craig S. Kaplan and David H. Salesin, 2000), pattern round 62 +// Round: 62 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Type=0..5, Size=-1..1 +// +// Knob 1: Hue. Turns the class colours round a wheel that skips orange, which reads brown once shaded. At 0 the copies turned like the first tile are azure, the next class pink, then green and violet: two colours where the group has two classes (the half-turned copies of CCCC and TCTC, the mirrored ones of TG1G1TG2G2), three for C3C3C3C3C3C3, four for C4C4C4C4, and TTTT, where every copy is a translate, all azure. At 0.5 the four are red, cyan, violet and green. +// Knob 2: Speed. At 2 the edge bends run through their quickest wave in 12 s and their slowest in 22 s, the corners wander over 27 s, the tiling drifts 0.9 LEDs a second right and 1.6 up, and each type holds 8 s before the next sweeps down the panel in 1.2 s, tile by tile, so the six come round every 55 s. At 0.2 everything is ten times slower, at 3 one and a half times faster. +// Knob 3: Type. The type on the panel now; the cycle goes on from it. 0 C3C3C3C3C3C3 (group p3: a hexagon whose corners V0, V2, V4 are 3-fold centres and whose corner V1 wanders, three classes), 1 TG1G1TG2G2 (pg: one translated pair and two glide-reflected pairs, so mirrored columns alternate), 2 CCCC (p2: any quadrilateral, each edge half-turned about its own midpoint), 3 TCTC (p2: a parallelogram with one translated pair and two C edges), 4 TTTT (p1), 5 C4C4C4C4 (p4: a square turned a quarter about two opposite corners, four classes). +// Knob 4: Size. The area of one tile, 290 x 2^(1.6 K) LEDs: 96 at -1, about 85 tiles on the panel; 310 at the opening 0.06, about 26; 880 at 1, about 9. + +const W = 64, H = 128, N = W * H; +const { floor, round, sin, cos, sqrt, min, max, abs, atan2, PI, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const TAU = 2 * PI; + +function hsv(h, s, v, out) { + 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[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} +// Orange reads brown on an LED once it is shaded, so the class colours come +// from a wheel without 0.03 to 0.17. +const wheel = (h) => 0.17 + 0.86 * (h - floor(h)); + +// Isometries as [a, b, c, d, e, f]: (x, y) -> (a x + b y + e, c x + d y + f). +const mul = (M, K) => [ + M[0] * K[0] + M[1] * K[2], M[0] * K[1] + M[1] * K[3], + M[2] * K[0] + M[3] * K[2], M[2] * K[1] + M[3] * K[3], + M[0] * K[4] + M[1] * K[5] + M[4], M[2] * K[4] + M[3] * K[5] + M[5], +]; +const inv = (M) => { + const det = M[0] * M[3] - M[1] * M[2], a = M[3] / det, b = -M[1] / det, c = -M[2] / det, d = M[0] / det; + return [a, b, c, d, -(a * M[4] + b * M[5]), -(c * M[4] + d * M[5])]; +}; +const Tr = (x, y) => [1, 0, 0, 1, x, y]; +const Rot = (p, th) => { + const c = cos(th), s = sin(th); + return [c, -s, s, c, p[0] - c * p[0] + s * p[1], p[1] - s * p[0] - c * p[1]]; +}; +const Half = (p, q) => Rot([(p[0] + q[0]) / 2, (p[1] + q[1]) / 2], PI); +// Reflect in the vertical line x = xa, then move dy along it. +const Glide = (xa, dy) => [-1, 0, 0, 1, 2 * xa, dy]; +const ap = (M, p) => [M[0] * p[0] + M[1] * p[1] + M[4], M[2] * p[0] + M[3] * p[1] + M[5]]; + +// An edge from P to Q, NS steps, bent off the chord by h(s) times its length. +// Every bend is a sum of sin(k pi s) sin(pi s), so it leaves both corners +// along the chord and the corner angles of the polygon stay. +const NS = 10; +function edge(P, Q, h) { + const dx = Q[0] - P[0], dy = Q[1] - P[1], pts = []; + for (let i = 0; i <= NS; i++) { + const s = i / NS, o = h(s); + pts.push([P[0] + dx * s - dy * o, P[1] + dy * s + dx * o]); + } + return pts; +} +const mapPts = (M, pts) => pts.map((p) => ap(M, p)); +const rev = (pts) => pts.slice().reverse(); + +// Bends. A free edge (the first of a T, G, C3 or C4 pair) may take any +// shape; a C edge must turn into itself under a half-turn about its +// midpoint, so its bend is odd about s = 1/2. Seen from either end of its +// chord, an edge never strays further off it than the angle the chord +// makes there with the line to the tile's centre, times 0.9: the edge then +// stays inside a rhombus on its chord whose two halves lie in the fan +// triangles (centre, corner, corner) of the two tiles it parts. Those +// triangles never overlap, so no two bent edges meet but at a corner and +// the copies still cover the plane once. +function limit(u, A, tm) { + let r = 0; + for (let i = 1; i < 40; i++) { + const s = i / 40; + r = max(r, abs(u(s)) / min(s, 1 - s)); + } + const a = r > 0 ? min(A, tm / r) : A; + return (s) => a * u(s); +} +function freeBend(c, A, tm) { + return limit((s) => sin(PI * s) * (c[0] * sin(PI * s) + 0.8 * c[1] * sin(2 * PI * s) + 0.3 * c[2] * sin(3 * PI * s)), A, tm); +} +function oddBend(c, A, tm) { + return limit((s) => sin(PI * s) * (1.3 * c[0] * sin(2 * PI * s) + 0.5 * c[1] * sin(4 * PI * s)), A, tm); +} +// The tangent of the stray allowed: 0.9 of the smallest angle between a +// chord and the line from its end to the centre of the corners. +function stray(V) { + const n = V.length; + let cx = 0, cy = 0, m = PI; + for (const p of V) { + cx += p[0] / n; + cy += p[1] / n; + } + const ang = (o, a, b) => { + const ax = a[0] - o[0], ay = a[1] - o[1], bx = b[0] - o[0], by = b[1] - o[1]; + return abs(atan2(ax * by - ay * bx, ax * bx + ay * by)); + }; + for (let i = 0; i < n; i++) { + const p = V[i], q = V[(i + 1) % n], C = [cx, cy]; + m = min(m, ang(p, q, C), ang(q, p, C)); + } + return Math.tan(0.9 * m); +} + +// The six types. Each returns the corners V in order round the tile +// (counter-clockwise), the edges as point lists from V[i] to V[i+1], the +// isometry n[i] that carries the tile onto its neighbour across edge i, +// and how many orientation classes the group has. c(j) is edge j's slowly +// turning bend coefficients and g(j) a slow wave for the corners. +const TYPES = [ + { + name: "C3C3C3C3C3C3", m: 3, + build(c, g, A) { + // V0, V2, V4 are a fixed equilateral triangle of 3-fold centres; V1 + // wanders, and V3, V5 follow it by turns of -120 degrees. + const V = []; + for (let i = 0; i < 6; i++) V.push([cos((i * PI) / 3), sin((i * PI) / 3)]); + V[1] = [V[1][0] + 0.18 * g(0), V[1][1] + 0.18 * g(1)]; + const r0 = Rot(V[0], -TAU / 3), r2 = Rot(V[2], -TAU / 3), r4 = Rot(V[4], -TAU / 3); + V[3] = ap(r2, V[1]); + V[5] = ap(r4, V[3]); + const tm = stray(V); + const e1 = edge(V[1], V[2], freeBend(c(0), A, tm)), e3 = edge(V[3], V[4], freeBend(c(1), A, tm)), e5 = edge(V[5], V[0], freeBend(c(2), A, tm)); + const E = [rev(mapPts(r0, e5)), e1, rev(mapPts(r2, e1)), e3, rev(mapPts(r4, e3)), e5]; + return { V, E, n: [r0, inv(r2), r2, inv(r4), r4, inv(r0)] }; + }, + }, + { + name: "TG1G1TG2G2", m: 2, + build(c, g, A) { + // T moves the tile up by h; the glides g1, g2 reflect it in vertical + // lines and move it half of h, so mirrored columns alternate. + const w = 1, h = 1.25, s = 0.22 * g(0), p = 0.32 + 0.12 * g(1), q = -0.32 + 0.12 * g(2); + const V = [[0, 0], [w, s], [w + p, s + h / 2], [w, s + h], [0, h], [q, h / 2]], tm = stray(V); + const g1 = Glide(w + p / 2, h / 2), g2 = Glide(q / 2, -h / 2), t = Tr(0, h); + const e0 = edge(V[0], V[1], freeBend(c(0), A, tm)), e1 = edge(V[1], V[2], freeBend(c(1), A, tm)), e4 = edge(V[4], V[5], freeBend(c(2), A, tm)); + const E = [e0, e1, mapPts(g1, e1), rev(mapPts(t, e0)), e4, mapPts(g2, e4)]; + return { V, E, n: [inv(t), inv(g1), g1, t, inv(g2), g2] }; + }, + }, + { + name: "CCCC", m: 2, + build(c, g, A) { + // Any quadrilateral tiles by half-turns about its edge midpoints. + const V = [[0.12 * g(0), 0.1 * g(1)], [1.1 + 0.12 * g(2), 0.08 * g(3)], [1.18 + 0.1 * g(4), 1.02 + 0.1 * g(5)], [-0.12 + 0.1 * g(6), 0.9 + 0.1 * g(7)]]; + const E = [], n = [], tm = stray(V); + for (let i = 0; i < 4; i++) { + const P = V[i], Q = V[(i + 1) % 4]; + E.push(edge(P, Q, oddBend(c(i), A * 1.15, tm))); + n.push(Half(P, Q)); + } + return { V, E, n }; + }, + }, + { + name: "TCTC", m: 2, + build(c, g, A) { + const u = [1.15, 0.22 * g(0)], v = [0.28 * g(1), 1]; + const V = [[0, 0], u, [u[0] + v[0], u[1] + v[1]], v], tm = stray(V); + const t = Tr(v[0], v[1]); + const e0 = edge(V[0], V[1], freeBend(c(0), A, tm)), e1 = edge(V[1], V[2], oddBend(c(1), A * 1.15, tm)), e3 = edge(V[3], V[0], oddBend(c(2), A * 1.15, tm)); + const E = [e0, e1, rev(mapPts(t, e0)), e3]; + return { V, E, n: [inv(t), Half(V[1], V[2]), t, Half(V[3], V[0])] }; + }, + }, + { + name: "TTTT", m: 1, + build(c, g, A) { + const u = [1.1, 0.25 * g(0)], v = [0.3 * g(1), 1]; + const V = [[0, 0], u, [u[0] + v[0], u[1] + v[1]], v], tm = stray(V); + const tu = Tr(u[0], u[1]), tv = Tr(v[0], v[1]); + const e0 = edge(V[0], V[1], freeBend(c(0), A, tm)), e1 = edge(V[1], V[2], freeBend(c(1), A, tm)); + const E = [e0, e1, rev(mapPts(tv, e0)), rev(mapPts(inv(tu), e1))]; + return { V, E, n: [inv(tv), tu, tv, inv(tu)] }; + }, + }, + { + name: "C4C4C4C4", m: 4, + build(c, g, A) { + // A square with 4-fold centres at V0 and V2: edge V0V1 turns a quarter + // about V0 into V0V3, edge V2V3 a quarter about V2 into V2V1. + const V = [[0, 0], [1, 0], [1, 1], [0, 1]], tm = stray(V); + const r0 = Rot(V[0], PI / 2), r2 = Rot(V[2], PI / 2); + const e0 = edge(V[0], V[1], freeBend(c(0), A, tm)), e2 = edge(V[2], V[3], freeBend(c(1), A, tm)); + const E = [e0, rev(mapPts(r2, e2)), e2, rev(mapPts(r0, e0))]; + return { V, E, n: [inv(r0), r2, inv(r2), r0] }; + }, + }, +]; + +// At Size 0 each tile covers this many LEDs; every edge asks for a bend of +// BEND times its length, and gets it unless its stray limit says less. +const AREA = 290, BEND = 0.45; +const PERIOD = 8, WIPE = 1.2; + +export function setup(params) { + params.s = { + t: 0, hue: 0, size: 0, type: -1, base: -1, next: -1, since: 0, wipe: 1, + ox: 0, oy: 0, + own: [new Int32Array(N), new Int32Array(N)], cnt: [new Uint8Array(N), new Uint8Array(N)], + tiles: [[], []], frames: 0, bad: 0, c: [0, 0, 0], + }; +} + +function polygonArea(V) { + let a = 0; + for (let i = 0; i < V.length; i++) { + const p = V[i], q = V[(i + 1) % V.length]; + a += p[0] * q[1] - q[0] * p[1]; + } + return abs(a) / 2; +} + +// The tiling of type k at time t, rasterised into own (tile index per LED) +// and cnt (how many tiles cover each LED centre). +function tile(S, k, slot) { + const ty = TYPES[k], t = S.t; + const c = (j) => [sin(0.53 * t + 1.7 * j + 0.3 * k), sin(0.37 * t + 2.9 * j + 1.1 * k), sin(0.29 * t + 4.1 * j + 2.3 * k)]; + const g = (j) => sin(0.23 * t + 1.9 * j + k); + const T = ty.build(c, g, BEND); + const sc = sqrt((AREA * Math.pow(2, 1.6 * S.size)) / polygonArea(T.V)); + // The boundary, closed, in tile units. + const poly = []; + for (const e of T.E) for (let i = 0; i < e.length - 1; i++) poly.push(e[i]); + let cx = 0, cy = 0; + for (const p of T.V) { + cx += p[0] / T.V.length; + cy += p[1] / T.V.length; + } + let rad = 0; + for (const p of poly) rad = max(rad, sqrt((p[0] - cx) ** 2 + (p[1] - cy) ** 2)); + // Tile units to LEDs: scale, and put the first tile's centre at the + // panel's centre plus the drift. + const view = [sc, 0, 0, sc, 32 + S.ox - sc * cx, 64 + S.oy - sc * cy]; + const own = S.own[slot], cnt = S.cnt[slot], tiles = [], np = poly.length, Q = new Float64Array(2 * np); + own.fill(-1); + cnt.fill(0); + // Every copy is found from the first by crossing edges, and a copy is + // known by where its centre lands. + const seen = new Set([round(cx * 64) * 65536 + round(cy * 64)]), queue = [[1, 0, 0, 1, 0, 0]], reach = sc * rad + 2; + for (let qi = 0; qi < queue.length; qi++) { + const M = queue[qi], F = mul(view, M), ux = F[0] * cx + F[1] * cy + F[4], uy = F[2] * cx + F[3] * cy + F[5]; + if (ux < -reach || ux > W + reach || uy < -reach || uy > H + reach) continue; + const id = tiles.length, det = M[0] * M[3] - M[1] * M[2]; + // The orientation class: the turn of the tile, and for a mirrored copy + // the turn of its mirror image, in steps of a full turn over the + // rotations the group has. + const steps = det > 0 ? ty.m : ty.m / 2; + const ang = atan2(M[2], M[0]); + const cls = det > 0 || ty.m < 2 ? round((ang / TAU) * steps + steps) % steps : steps + (round((atan2(-M[2], -M[0]) / TAU) * steps + steps) % steps); + tiles.push({ Fi: inv(F), cy: uy, cls }); + for (let i = 0; i < np; i++) { + const x = poly[i][0], y = poly[i][1]; + Q[2 * i] = F[0] * x + F[1] * y + F[4]; + Q[2 * i + 1] = F[2] * x + F[3] * y + F[5]; + } + fill(Q, np, id, own, cnt); + for (const nb of T.n) { + const K = mul(M, nb), key = round((K[0] * cx + K[1] * cy + K[4]) * 64) * 65536 + round((K[2] * cx + K[3] * cy + K[5]) * 64); + if (seen.has(key)) continue; + seen.add(key); + queue.push(K); + } + } + S.tiles[slot] = tiles; + S.v0 = T.V[0]; + S.rad = rad; + return tiles; +} + +// Even-odd scanline fill of LED centres (x + 0.5, y + 0.5), half-open on +// both axes, so two tiles that share an edge split the LEDs along it. Each +// segment drops its crossings into the rows it spans. +const rowX = []; +for (let r = 0; r < H; r++) rowX.push([]); +function fill(Q, n, id, own, cnt) { + let r0 = H, r1 = -1; + for (let i = 0; i < n; i++) { + const j = (i + 1) % n; + let ax = Q[2 * i], ay = Q[2 * i + 1], bx = Q[2 * j], by = Q[2 * j + 1]; + if (ay === by) continue; + if (ay > by) { + const tx = ax, ty = ay; + ax = bx; + ay = by; + bx = tx; + by = ty; + } + // Rows whose centre yc has ay <= yc < by. + const ra = max(0, Math.ceil(ay - 0.5)), rb = min(H - 1, Math.ceil(by - 0.5) - 1); + if (ra > rb) continue; + const k = (bx - ax) / (by - ay); + for (let r = ra; r <= rb; r++) rowX[r].push(ax + (r + 0.5 - ay) * k); + if (ra < r0) r0 = ra; + if (rb > r1) r1 = rb; + } + for (let r = r0; r <= r1; r++) { + const xs = rowX[r]; + if (xs.length > 2) xs.sort((p, q) => p - q); + else if (xs.length === 2 && xs[0] > xs[1]) xs.reverse(); + for (let j = 0; j + 1 < xs.length; j += 2) { + const c0 = max(0, Math.ceil(xs[j] - 0.5)), c1 = min(W - 1, Math.ceil(xs[j + 1] - 0.5) - 1); + for (let x = c0, p = r * W + c0; x <= c1; x++, p++) { + own[p] = id; + cnt[p]++; + } + } + xs.length = 0; + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + const speed = cl(k[1], 0.2, 3), tau = (d * speed) / 2; + S.hue = k[0]; + S.size = cl(k[3], -1, 1); + const base = round(cl(k[2], 0, 5)); + if (base !== S.base) { + // A turned knob shows its type now, and the cycle goes on from there. + if (S.base < 0) S.type = base; + else { + S.next = base; + S.wipe = 0; + } + S.base = base; + S.since = 0; + } + S.t += tau; + S.since += tau; + S.ox += tau * 0.9; + S.oy -= tau * 1.6; + if (S.wipe >= 1 && S.since >= PERIOD) { + S.next = (S.type + 1) % TYPES.length; + S.wipe = 0; + S.since = 0; + } + if (S.wipe < 1) { + S.wipe += tau / WIPE; + if (S.wipe >= 1) { + S.type = S.next; + S.next = -1; + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const c = S.c; + const slots = [[S.type, 0]]; + if (S.next >= 0 && S.wipe < 1) slots.push([S.next, 1]); + const info = []; + for (const [k, slot] of slots) { + const tiles = tile(S, k, slot); + info.push({ k, slot, tiles, v0: S.v0, fall: -2.2 / (S.rad * S.rad) }); + S.frames++; + for (let p = 0; p < N; p++) if (S.cnt[slot][p] !== 1) S.bad++; + } + // The wipe runs down the panel tile by tile: a tile of the new type shows + // once its centre is above the line. + const line = -20 + (H + 40) * ease(cl(S.wipe, 0, 1)); + // Each class's colour at full brightness. + const pal = []; + for (let i = 0; i < 4; i++) { + hsv(wheel(S.hue + CLASS[i]), 0.78, 255, c); + pal.push([c[0], c[1], c[2]]); + } + // Which tiling each LED shows, and the tile there, as one number, so the + // grout also runs along the seam of a wipe. + const kk = S.kk || (S.kk = new Int32Array(N)), lay = S.lay || (S.lay = new Uint8Array(N)); + const own0 = S.own[0], own1 = S.own[1], two = info.length > 1; + for (let p = 0; p < N; p++) { + const id = own1[p]; + lay[p] = two && id >= 0 && info[1].tiles[id].cy < line ? 1 : 0; + kk[p] = lay[p] ? -2 - id : own0[p]; + } + for (let p = 0; p < N; p++) { + const x = p & 63, y = p >> 6, L = info[lay[p]]; + const id = lay[p] ? own1[p] : own0[p]; + if (id < 0 || S.cnt[L.slot][p] !== 1) { + // Never seen: a gap or an overlap would show white. + display.setPixel(x, y, 255, 255, 255); + continue; + } + const T = L.tiles[id], F = T.Fi, xc = x + 0.5, yc = y + 0.5, me = kk[p]; + // Grout where the LED to the right or below belongs to another tile. + const grout = (x < W - 1 && kk[p + 1] !== me) || (y < H - 1 && kk[p + W] !== me); + // Shade: brightest at the tile's first corner, so its turn shows. + const qx = F[0] * xc + F[1] * yc + F[4] - L.v0[0], qy = F[2] * xc + F[3] * yc + F[5] - L.v0[1]; + const v = (0.45 + 0.55 * exp((qx * qx + qy * qy) * L.fall)) * (grout ? 0.14 : 1); + const col = pal[T.cls & 3]; + display.setPixel(x, y, round(col[0] * v), round(col[1] * v), round(col[2] * v)); + } +} + +// Class colours on the wheel: azure, pink, green, violet at Hue 0. +const CLASS = [0.477, 0.849, 0.186, 0.686]; +const ease = (u) => u * u * (3 - 2 * u); diff --git a/incoming/pattern-study/patterns/void-and-cluster.js b/incoming/pattern-study/patterns/void-and-cluster.js new file mode 100644 index 000000000..9cd6019a9 --- /dev/null +++ b/incoming/pattern-study/patterns/void-and-cluster.js @@ -0,0 +1,283 @@ +// ===== Patternflow pattern ===== +// Title: Void and Cluster +// Family: noise +// Idea: Clumpy white noise rearranges itself into blue noise as Ulichney's void-and-cluster method moves the dot at the tightest cluster into the largest void, again and again, until the move would put it back; then the settled dots are ranked one by one into a dither array and a threshold falls and rises through the ranks, so the dots leave and return in rank order, coloured by rank. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: blue noise (Robert Ulichney, 1988), the void-and-cluster method for dither arrays (Robert Ulichney, 1993), pattern round 62 +// Round: 62 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Density=-1..1, Sigma=1.5..4 +// +// Knob 1: Hue. Turns the colours round the wheel together. At 0 the settling dots are azure, the crowded ones pink and pale, a dot a swap has just put down flashes white, and the ranked dots run from a pale azure at rank 0 through violet to pink at the top of the cycle; at 0.5 the dots are orange-red and the ranks run through yellow to green. The voids glow lavender to violet whatever the knob, as a dim warm glow reads brown. +// Knob 2: Speed. At 2 the white noise stands still for 0.5 s, the swaps start at 36 a second and reach 243 a second, 0.16 of the 1516 dots, 0.8 s later, and the pattern settles after 431 to 464 swaps at 2.6 s; the threshold then falls to 0.45 of the settled count, rises to 1.45 and comes back in 6.3 s, and fresh white noise falls at 8.9 s. At 0.2 everything is ten times slower, settling at 26 s; at 3 it settles at 1.8 s. +// Knob 3: Density. The share of LEDs lit in the settled pattern, 0.185 x 1.7^K: 891 dots at -1, 1516 at the opening 0, 2576 at 1, where the top of the cycle stops at 0.45 of the panel. A new value scatters fresh white noise. +// Knob 4: Sigma. The width in LEDs of the Gaussian that measures clusters and voids. It opens at Ulichney's 1.5, where every threshold of the dither array keeps 7 to 9 percent of white noise's power below half the principal frequency sqrt(g); at 2.5 the settled dots keep 37 percent and at 4 83 percent, as the filter grows too wide to see the dots' own spacing: the swaps stop after 187 instead of 448 and the small clumps stay. A new value scatters fresh white noise. +// Buttons: K1 tap scatters fresh white noise. + +const W = 64, H = 128, N = W * H; +const { floor, exp, ceil, min, max, round, random } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v, out) { + 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[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// The rank cycle after the dots settle, in seconds at Speed 2: hold, fall to +// LO of the settled count, rise to HI, fall back, hold. +const HOLD0 = 0.4, FALL = 1.4, RISE = 2.8, BACK = 1.2, HOLD1 = 0.5; +const LO = 0.45, HI = 1.45; +// Swaps a second at Speed 2, as a share of the dots. +const RATE = 0.16; +const STILL = 0.5, RAMP = 0.8, FLASH = 0.45, GHOST = 0.35; + +export function setup(params) { + params.s = { + B: new Uint8Array(N), E: new Float64Array(N), ones: new Int32Array(N), at: new Int32Array(N), n: 0, + B2: new Uint8Array(N), E2: new Float64Array(N), + rank: new Int32Array(N), top: 0, np: 0, + flash: new Float32Array(N), ghost: new Float32Array(N), + K: null, R: 0, KW: 0, Ks: 0, sigma: 0, rho: 0, + mode: 0, t: 0, budget: 0, swaps: 0, thr: 0, dir: 0, + hue: 0, c: [0, 0, 0], fresh: true, + }; +} + +function kernel(S, sigma) { + S.sigma = sigma; + S.R = min(15, ceil(3 * sigma)); + S.KW = 2 * S.R + 1; + S.K = new Float64Array(S.KW * S.KW); + S.Ks = 0; + for (let dy = -S.R; dy <= S.R; dy++) + for (let dx = -S.R; dx <= S.R; dx++) { + const v = exp(-(dx * dx + dy * dy) / (2 * sigma * sigma)); + S.K[(dy + S.R) * S.KW + dx + S.R] = v; + S.Ks += v; + } +} + +// Add (s = 1) or take away (s = -1) one dot's Gaussian from the energy E, +// on the torus 64 x 128 the panel wraps into. +function splat(S, E, p, s) { + const x = p & 63, y = p >> 6, R = S.R, KW = S.KW, K = S.K; + for (let dy = -R; dy <= R; dy++) { + const row = (((y + dy) % H) + H) % H * W, k = (dy + R) * KW + R; + for (let dx = -R; dx <= R; dx++) E[row + ((x + dx) & 63)] += s * K[k + dx]; + } +} + +function addOne(S, p) { + S.B[p] = 1; + S.at[p] = S.n; + S.ones[S.n++] = p; + splat(S, S.E, p, 1); +} +function removeOne(S, p) { + S.B[p] = 0; + const i = S.at[p], last = S.ones[--S.n]; + S.ones[i] = last; + S.at[last] = i; + splat(S, S.E, p, -1); +} + +// White noise: exactly rho N dots, every LED as likely as every other. +function scatter(S) { + S.B.fill(0); + S.E.fill(0); + S.n = 0; + const want = round(S.rho * N); + const idx = new Int32Array(N); + for (let i = 0; i < N; i++) idx[i] = i; + for (let i = 0; i < want; i++) { + const j = i + floor(random() * (N - i)), t = idx[i]; + idx[i] = idx[j]; + idx[j] = t; + addOne(S, idx[i]); + } + S.rank.fill(-1); + S.flash.fill(0); + S.ghost.fill(0); + S.mode = 0; + S.t = 0; + S.budget = 0; + S.swaps = 0; +} + +const tightest = (S) => { + let c = -1, m = -1; + for (let i = 0; i < S.n; i++) { + const p = S.ones[i]; + if (S.E[p] > m) { + m = S.E[p]; + c = p; + } + } + return c; +}; +const largestVoid = (B, E) => { + let v = -1, m = 1e30; + for (let p = 0; p < N; p++) + if (!B[p] && E[p] < m) { + m = E[p]; + v = p; + } + return v; +}; + +// One swap of the initial pattern. Returns false once the dot taken from +// the tightest cluster would go straight back into the largest void. +function swap(S) { + const c = tightest(S); + removeOne(S, c); + const v = largestVoid(S.B, S.E); + if (v === c) { + addOne(S, c); + return false; + } + addOne(S, v); + S.flash[v] = 1; + S.ghost[c] = 1; + S.swaps++; + return true; +} + +// The settled pattern is the prototype. Phase 1 takes its dots away, tightest +// cluster first, ranking them from np - 1 down to 0; phase 2 starts again +// from the prototype and fills the largest void, ranking from np up. +function rankPrototype(S) { + S.np = S.n; + S.B2.set(S.B); + S.E2.set(S.E); + for (let r = S.n - 1; r >= 0; r--) { + const c = tightest(S); + S.rank[c] = r; + removeOne(S, c); + } + S.top = S.np; +} +function rankUpTo(S, n) { + while (S.top < n) { + const v = largestVoid(S.B2, S.E2); + S.rank[v] = S.top++; + S.B2[v] = 1; + splat(S, S.E2, v, 1); + } +} + +const ease = (u) => u * u * (3 - 2 * u); +function threshold(S, t) { + const np = S.np; + if (t < HOLD0) return np; + t -= HOLD0; + if (t < FALL) return np + (LO - 1) * np * ease(t / FALL); + t -= FALL; + if (t < RISE) return LO * np + (HI - LO) * np * ease(t / RISE); + t -= RISE; + if (t < BACK) return HI * np + (1 - HI) * np * ease(t / BACK); + return np; +} +const CYCLE = HOLD0 + FALL + RISE + BACK + HOLD1; + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + const speed = cl(k[1], 0.2, 3), tau = (d * speed) / 2; + const rho = 0.185 * Math.pow(1.7, cl(k[2], -1, 1)), sigma = cl(k[3], 1.5, 4); + const press = input.btnPressed && input.btnPressed[0]; + if (S.fresh || press || Math.abs(rho - S.rho) > 0.004 || Math.abs(sigma - S.sigma) > 0.05) { + S.fresh = false; + S.rho = rho; + kernel(S, sigma); + scatter(S); + } + for (let p = 0; p < N; p++) { + if (S.flash[p] > 0) S.flash[p] = max(0, S.flash[p] - tau / FLASH); + if (S.ghost[p] > 0) S.ghost[p] = max(0, S.ghost[p] - tau / GHOST); + } + S.t += tau; + if (S.mode === 0) { + // The white noise stands still for STILL, then the swaps start slowly, + // one at a time to the eye, and reach RATE in RAMP more. + if (S.t > STILL) S.budget += tau * RATE * S.n * min(1, 0.15 + (S.t - STILL) / RAMP); + while (S.budget >= 1) { + S.budget -= 1; + if (!swap(S)) { + rankPrototype(S); + S.mode = 1; + S.t = 0; + break; + } + } + } else { + if (S.t >= CYCLE) { + scatter(S); + return; + } + const thr = min(0.45 * N, threshold(S, S.t)); + S.dir = thr > S.thr + 1e-9 ? 1 : thr < S.thr - 1e-9 ? -1 : 0; + S.thr = thr; + rankUpTo(S, ceil(S.thr) + 1); + } +} + +// A dot of the pattern being settled: the azure of the opening hue, going +// pink and pale the more crowded it is against white noise of the same +// density, and white for an instant where a swap has just put it. +function dotColour(S, E, p, h0, out) { + const z = (E[p] - 1) / (S.rho * (S.Ks - 1)), w = cl((z - 1.05) / 0.8, 0, 1), f = S.flash[p]; + hsv(h0 + 0.43 * w, (0.8 - 0.4 * w) * (1 - f), 1, out); +} +// A ranked dot: from the opening azure at rank 0 down the wheel through +// green to yellow at the top of the cycle. +function rankColour(S, r, h0, sat, out) { + const u = r / (HI * S.np); + hsv(h0 + 0.38 * u, (0.25 + 0.6 * u) * sat, 1, out); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const c = S.c, c2 = [0, 0, 0], h0 = S.hue + 0.55; + const put = (p, r, g, b) => display.setPixel(p & 63, p >> 6, round(r * 255), round(g * 255), round(b * 255)); + if (S.mode === 0) { + const mean = S.rho * S.Ks, vh = 0.74 + 0.12 * (S.hue - floor(S.hue)); + for (let p = 0; p < N; p++) { + if (S.B[p]) dotColour(S, S.E, p, h0, c); + else if (S.ghost[p] > 0) hsv(h0 + 0.43, 0.5, 0.6 * S.ghost[p], c); + else { + // The voids glow where the energy falls below half its mean, in a + // cool hue whatever the knob, since a dim warm one reads brown. + const g = cl(1 - S.E[p] / (0.5 * mean), 0, 1); + hsv(vh, 0.4, 0.5 * g, c); + } + put(p, c[0], c[1], c[2]); + } + return; + } + const thr = S.thr, mix = cl(S.t / HOLD0, 0, 1); + for (let p = 0; p < N; p++) { + const r = S.rank[p]; + if (r < 0 || r >= thr + 1) { + display.setPixel(p & 63, p >> 6, 0, 0, 0); + continue; + } + // The dots within 2% of the ranks below the threshold are arriving, white + // as they come, or leaving, fading as they go. + const a = cl((thr - r) / (0.02 * S.np), 0, 1); + rankColour(S, r, h0, S.dir > 0 ? a : 1, c); + if (mix < 1 && r < S.np) { + dotColour(S, S.E2, p, h0, c2); + for (let i = 0; i < 3; i++) c[i] = c2[i] + (c[i] - c2[i]) * mix; + } + const v = S.dir < 0 ? 0.25 + 0.75 * a : 1; + put(p, c[0] * v, c[1] * v, c[2] * v); + } +} From 2ecaf1722fb1af0fa20ee6d40e86f2a3076d681d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 15:27:36 +0000 Subject: [PATCH 5/8] patterns: Feynman Checkerboard and Abrikosov Lattice for round 62 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TTZyp5h85ewavQ9av2v4N1 --- .../patterns/abrikosov-lattice.js | 143 ++++++++++ .../patterns/feynman-checkerboard.js | 244 ++++++++++++++++++ 2 files changed, 387 insertions(+) create mode 100644 incoming/pattern-study/patterns/abrikosov-lattice.js create mode 100644 incoming/pattern-study/patterns/feynman-checkerboard.js diff --git a/incoming/pattern-study/patterns/abrikosov-lattice.js b/incoming/pattern-study/patterns/abrikosov-lattice.js new file mode 100644 index 000000000..73b7376fb --- /dev/null +++ b/incoming/pattern-study/patterns/abrikosov-lattice.js @@ -0,0 +1,143 @@ +// ===== Patternflow pattern ===== +// Title: Abrikosov Lattice +// Family: physics +// Idea: A rotating Bose-Einstein condensate fills the tall panel in the lowest Landau level, its phase drawn as hue and its density as brightness, so each quantised vortex is a black core round which every hue turns once, the cores stand in Abrikosov's triangular lattice and turn with the fluid, and turning up the rotation packs them closer, Feynman's 2 m Omega / h to each unit of area, while new ones come in from the dark rim. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Abrikosov vortex lattice (Alexei Abrikosov, 1957), triangular vortex lattice (W. H. Kleiner, L. M. Roth and S. H. Autler, 1964), areal density of quantised vortices (Richard Feynman, 1955), vortex lattices in rotating Bose-Einstein condensates (K. W. Madison, F. Chevy, W. Wohlleben and J. Dalibard, 2000; J. R. Abo-Shaeer, C. Raman, J. M. Vogels and W. Ketterle, 2001), rapidly rotating condensate in the lowest Landau level (Tin-Lun Ho, 2001), pattern round 62 +// Round: 62 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Rotation=0.3..0.95, Round=0..1 +// +// Knob 1: Hue. Turns the wheel the phase is drawn on. At 0 phase zero is azure, and the hues run azure, violet, magenta, red, gold, green and cyan anticlockwise round every core; at 0.5 phase zero is orange. Blue is paled toward periwinkle so that it does not read as a hole beside the cores, which are the only black. +// Knob 2: Speed. How fast the lattice turns, anticlockwise with the flow round the cores, at 0.25 Speed Omega / omega radians a second: at 2 and the opening Rotation 0.3 it turns 0.15 rad/s, a sixth of a turn, the lattice's own symmetry, in 7 s; at 0.2 in 70 s, at 3 in 4.7 s. +// Knob 3: Rotation. Omega / omega, from 0.3 to 0.95. Feynman's rule puts 2 m Omega / h vortices on each unit of area, so the spacing goes as 1 / sqrt(Omega): 25 LEDs at 0.3, where 9 to 13 cores sit in the bright cloud and 15 to 17 on the panel; 17.7 LEDs at 0.6, with 21 to 23 in the cloud; 14 LEDs at 0.95, with 31 to 37 in the cloud and 47 to 51 on the panel. The lattice closes in on the central core, so new vortices come in from the dark rim, and it turns faster in step with Omega. +// Knob 4: Round. The trap's aspect. The cloud's half axes are 33 LEDs across and 33 (2 - Round) down: 64 at the opening 0.06, so the cloud fills the tall panel; 49.5 at 0.5; 33 at 1, a round cloud with 7 cores in it at the opening Rotation and the top and bottom of the panel dark. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, sqrt, exp, cos, sin, atan2, round, abs, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const TAU = 2 * PI; + +function hsv(h, s, v, out) { + 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[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// Abrikosov's lattice function in the lowest Landau level, Landau gauge, +// lengths in the magnetic length l: Phi(x, y) = sum_n C_n e^(i q n y) +// e^(-(x - q n)^2 / 2), with C_n = 1 for even n and i for odd n and +// q^2 = pi sqrt 3, the triangular lattice of Kleiner, Roth and Autler. Its +// zeros sit at (q/2, Ly/4) + the lattice, Ly = 2 pi / q, one to each area +// 2 pi l^2, spacing D = sqrt(4 pi / sqrt 3) = 2.694 l. +const Q = sqrt(PI * sqrt(3)), LY = TAU / Q, D = sqrt((4 * PI) / sqrt(3)), X0 = Q / 2, Y0 = LY / 4; +const OUT = [0, 0]; +function phi(x, y) { + let re = 0, im = 0; + const n0 = round(x / Q), c = cos(Q * y), s = sin(Q * y); + // e^(i q n y) for n = n0 - 3 by one cos and sin, then by rotation. + let er = cos(Q * (n0 - 3) * y), ei = sin(Q * (n0 - 3) * y); + for (let n = n0 - 3; n <= n0 + 3; n++) { + const u = x - Q * n, g = exp(-u * u / 2); + if (n & 1) (re -= ei * g), (im += er * g); + else (re += er * g), (im += ei * g); + const t = er * c - ei * s; + ei = er * s + ei * c; + er = t; + } + OUT[0] = re; + OUT[1] = im; + return OUT; +} + +// Phi over one magnetic cell, x in [0, 2q) and y in [0, Ly), tabled once: +// Phi(x + 2q j, y + Ly m) = e^(2 i q j y) Phi(x, y), so the table and one +// phase give Phi anywhere. +const TX = 168, TY = 96, TRe = new Float32Array((TX + 1) * (TY + 1)), TIm = new Float32Array((TX + 1) * (TY + 1)); +for (let j = 0; j <= TY; j++) + for (let i = 0; i <= TX; i++) { + const f = phi((i / TX) * 2 * Q, (j / TY) * LY); + TRe[j * (TX + 1) + i] = f[0]; + TIm[j * (TX + 1) + i] = f[1]; + } +// The largest |Phi| in the table, the brightest point of every cell. +let MX = 0; +for (let o = 0; o < TRe.length; o++) MX = max(MX, sqrt(TRe[o] * TRe[o] + TIm[o] * TIm[o])); +// Phi at (x, y) from the table: modulus and argument. +const LOOK = [0, 0]; +function look(x, y) { + const jx = floor(x / (2 * Q)), my = floor(y / LY), u = ((x - jx * 2 * Q) / (2 * Q)) * TX, v = ((y - my * LY) / LY) * TY; + let i = floor(u), j = floor(v); + if (i >= TX) i = TX - 1; + if (j >= TY) j = TY - 1; + const fu = u - i, fv = v - j, o = j * (TX + 1) + i; + const re = (TRe[o] * (1 - fu) + TRe[o + 1] * fu) * (1 - fv) + (TRe[o + TX + 1] * (1 - fu) + TRe[o + TX + 2] * fu) * fv; + const im = (TIm[o] * (1 - fu) + TIm[o + 1] * fu) * (1 - fv) + (TIm[o + TX + 1] * (1 - fu) + TIm[o + TX + 2] * fu) * fv; + LOOK[0] = sqrt(re * re + im * im); + LOOK[1] = atan2(im, re) + 2 * Q * jx * y; + return LOOK; +} + +export function setup(params) { + params.s = { hue: 0, speed: 2, rot: 0.3, round: 0.06, angle: 0, ek: -1, E: new Float32Array(N), A: new Float32Array(N), P: new Float32Array(N) }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0.3, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.rot = cl(k[2], 0.3, 0.95); + S.round = cl(k[3], 0, 1); + S.angle += d * 0.25 * S.speed * S.rot; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + // Feynman's rule: vortices per area 2 m Omega / h, so the spacing goes as + // 1 / sqrt(Omega): 25 LEDs at Omega = 0.3 omega. + const dl = 25 * sqrt(0.3 / S.rot), sc = D / dl; // l per LED + const { A, P, E } = S, ca = cos(S.angle), sa = sin(S.angle), tmp = [0, 0, 0]; + if (S.ek !== S.round) { + // The cloud: half axes 33 LEDs across and 33 (2 - Round) down, its + // envelope exp(-r^6), flat inside and falling over about 8 LEDs. + S.ek = S.round; + const a = 33, b = 33 * (2 - S.round); + for (let Y = 0, p = 0; Y < H; Y++) + for (let X = 0; X < W; X++, p++) { + const r2 = ((X - 31.5) / a) ** 2 + ((Y - 63.5) / b) ** 2; + E[p] = exp(-(r2 * r2 * r2)); + } + } + // In the lab frame the condensate is this function in the symmetric gauge, + // f(z) e^(-|z|^2 / 4) with f holomorphic: Phi times e^(-i x y / 2), moved + // by a magnetic translation so a zero sits on the cloud's centre, and + // turned with the lattice, which keeps that form. So every zero winds + // the phase once anticlockwise. The cloud's envelope multiplies it, the + // slowly varying density times the lattice that many vortices make. + for (let Y = 0, p = 0; Y < H; Y++) + for (let X = 0; X < W; X++, p++) { + const px = (X - 31.5) * sc, py = (63.5 - Y) * sc; + // Into the lattice's turning frame, then onto Abrikosov's function + // with a zero on the cloud's centre. + const x = ca * px + sa * py, y = -sa * px + ca * py; + const f = look(x + X0, y + Y0), m = f[0]; + // Symmetric gauge about the centre: arg Phi - x y / 2 - x0 y. + P[p] = f[1] - (x * y) / 2 - X0 * y; + A[p] = m * E[p]; + } + // Brightness |psi|^1.25: a core is dark, below half brightness, over a + // disc 0.4 of the spacing across, 10 LEDs at the opening, where the + // steepest step between neighbours is 28 / 255. + for (let p = 0; p < N; p++) { + const u = min(1, A[p] / MX), v = u * sqrt(sqrt(u)), h = S.hue + 0.6 + P[p] / TAU, e = max(0, 1 - abs(h - floor(h) - 0.667) * 7); + hsv(h, 1 - 0.35 * e * e, v, tmp); + display.setPixel(p & 63, p >> 6, round(tmp[0] * 255), round(tmp[1] * 255), round(tmp[2] * 255)); + } +} diff --git a/incoming/pattern-study/patterns/feynman-checkerboard.js b/incoming/pattern-study/patterns/feynman-checkerboard.js new file mode 100644 index 000000000..edb81c7fa --- /dev/null +++ b/incoming/pattern-study/patterns/feynman-checkerboard.js @@ -0,0 +1,244 @@ +// ===== Patternflow pattern ===== +// Title: Feynman Checkerboard +// Family: wave +// Idea: Time runs down the panel and space across it on Feynman's light-cone lattice, where a particle steps one site left or right each row and every corner multiplies its amplitude by i epsilon: four such zigzag paths fall from the source with each corner marked in the colour of i^R, the exact sum over all 2^128 of them glows underneath as phase and brightness, and its gold mean position trembles from side to side, Schroedinger's Zitterbewegung. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Dirac equation (Paul Dirac, 1928), Zitterbewegung (Erwin Schroedinger, 1930), Feynman's checkerboard (Richard Feynman; Richard Feynman and Albert Hibbs, Quantum Mechanics and Path Integrals, 1965, problem 2-6), continuum limit of the checkerboard (Ted Jacobson and Lawrence Schulman, 1984), pattern round 62 +// Round: 62 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Mass=0.08..0.4, Focus=0..1 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the source's own phase is azure, so the field opens azure at the top and turns through violet, red, gold and green as its phase runs on down the panel; a corner dot is azure, violet, red or green as the path has made 0, 1, 2 or 3 corners mod 4, and the mean is pale gold. At 0.5 the field opens amber and the mean turns lavender. +// Knob 2: Speed. How fast the present, the row the paths have reached, runs down: 16 rows a second per unit, so at 2 a sweep takes 4 s and the paths fade for 0.6 s at the bottom before four new ones leave the source; at 0.2 a sweep takes 40 s, at 3 it takes 2.7 s. Rows the present has passed are lit fully, rows ahead of it at 0.6, and the opening sweep starts on row 40. +// Knob 3: Mass. epsilon = m dt, from 0.08 to 0.4. A path turns with probability eps / (1 + eps) a step, every 13.5 rows at 0.08 and every 3.5 at 0.4; the packet's phase turns once in 2 pi / atan(eps) rows, 79 at 0.08 and 16.5 at 0.4; and the mean trembles with a period of pi / atan(eps) rows, 39 at 0.08, where it swings 3 to 6 LEDs peak to peak, and 8.3 at 0.4, where it swings 0.5 to 1.2. +// Knob 4: Focus. The source packet's width, sigma = 1.5 (1 - Focus)^1.5 / eps sites, two sites to an LED: 17 sites at the opening 0.06, 1.4 Compton wavelengths 1 / eps, where the mean trembles clearly; 6.6 sites at 0.5; at 1 a single site, the point source of Feynman's propagator, whose cone edges are the one path with no corner (right) and the one that turns at once (left), whose phase bands are the hyperbolas of constant proper time, and whose mean runs right at 0.92 c and leaves the panel on row 69. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, sqrt, exp, atan2, round, ceil, hypot, PI, abs } = 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; +} + +// The lattice: two sites to an LED across, one time step to a row down. +// The panel shows sites M .. M + 127 of a line of NX = 384 sites, and the +// line runs 192 sites either side of the source, past the 128 that light +// covers in the panel's 128 steps. Amplitude that leaves the panel's sides +// is still carried, comes back when its paths do, and never meets an end: +// the panel shows the free propagator, exactly. +const C = 2, M = 128, NX = 128 + 2 * M, X0 = M + 64; +const ux = (s) => (s - M + 0.5) / C - 0.5; // site -> splat x + +export function setup(params) { + params.s = { + hue: 0, speed: 2, eps: 0.08, focus: 0.06, key: '', + Rr: new Float64Array(NX), Ri: new Float64Array(NX), Lr: new Float64Array(NX), Li: new Float64Array(NX), + nRr: new Float64Array(NX), nRi: new Float64Array(NX), nLr: new Float64Array(NX), nLi: new Float64Array(NX), + rho: new Float32Array(N), ph: new Float32Array(N), V: new Float32Array(N), mean: new Float64Array(H), tot: new Float64Array(H), + D: new Float32Array(N), + paths: [], R: new Float32Array(N), G: new Float32Array(N), B: new Float32Array(N), c: new Float32Array(15), + }; +} + +// One step of the checkerboard, unitary form: going straight costs +// a = 1 / sqrt(1 + eps^2), a corner b = i eps / sqrt(1 + eps^2), so a path +// with R corners carries (i eps)^R / (1 + eps^2)^(T/2). +// R(x, t+1) = a R(x-1, t) + b L(x-1, t) +// L(x, t+1) = a L(x+1, t) + b R(x+1, t) +function field(S) { + const eps = S.eps, a = 1 / sqrt(1 + eps * eps), b = eps / sqrt(1 + eps * eps); + let { Rr, Ri, Lr, Li, nRr, nRi, nLr, nLi } = S; + Rr.fill(0); Ri.fill(0); Lr.fill(0); Li.fill(0); + // The source: a Gaussian packet at rest in the right-moving component, + // width sigma sites, on the sites of one parity. + const sigma = S.sigma; + let n = 0; + for (let s = 0; s < NX; s++) { + const d = s - X0; + if (d & 1) continue; + const g = sigma < 0.5 ? +(d === 0) : exp((-d * d) / (4 * sigma * sigma)); + Rr[s] = g; + n += g * g; + } + n = 1 / sqrt(n); + for (let s = 0; s < NX; s++) Rr[s] *= n; + const { rho, ph, mean, tot } = S; + for (let t = 0; t < H; t++) { + let m = 0, p = 0; + for (let s = 0; s < NX; s++) { + const q = Rr[s] * Rr[s] + Ri[s] * Ri[s] + Lr[s] * Lr[s] + Li[s] * Li[s]; + p += q; + m += q * s; + } + mean[t] = m / p; + tot[t] = p; + // The brightness scale of the row: the densest site within t/2 + 2 sigma + // of the centre, so the two straight paths along a point source's cone + // do not black out the rest; brightness is (density / scale)^0.4. + let mx = 0; + const half = max(2, floor(t / 2 + 2 * S.sigma)); + for (let s = max(1, X0 - half); s <= min(NX - 2, X0 + half); s++) { + const q = Rr[s] * Rr[s] + Ri[s] * Ri[s] + Lr[s] * Lr[s] + Li[s] * Li[s]; + if (q > mx) mx = q; + } + const ref = mx > 0 ? mx : 1; + // The live site under LED X on row t is M + 2X + (t & 1). + for (let X = 0; X < W; X++) { + const s = M + 2 * X + ((t + X0) & 1), o = t * W + X; + rho[o] = Rr[s] * Rr[s] + Ri[s] * Ri[s] + Lr[s] * Lr[s] + Li[s] * Li[s]; + ph[o] = atan2(Ri[s] + Li[s], Rr[s] + Lr[s]) / (2 * PI); + S.V[o] = min(1, (rho[o] / ref) ** 0.4); + } + for (let s = 1; s < NX - 1; s++) { + nRr[s] = a * Rr[s - 1] - b * Li[s - 1]; + nRi[s] = a * Ri[s - 1] + b * Lr[s - 1]; + nLr[s] = a * Lr[s + 1] - b * Ri[s + 1]; + nLi[s] = a * Li[s + 1] + b * Rr[s + 1]; + } + nRr[0] = nRi[0] = nLr[0] = nLi[0] = 0; + nRr[NX - 1] = nRi[NX - 1] = nLr[NX - 1] = nLi[NX - 1] = 0; + [Rr, nRr] = [nRr, Rr]; + [Ri, nRi] = [nRi, Ri]; + [Lr, nLr] = [nLr, Lr]; + [Li, nLi] = [nLi, Li]; + } + Object.assign(S, { Rr, Ri, Lr, Li, nRr, nRi, nLr, nLi }); +} + +// A path sampled with weight |amplitude| = eps^R: a corner at each step with +// probability eps / (1 + eps), starting right-moving from a site drawn from +// the packet. +function sample(S) { + const eps = S.eps, pc = eps / (1 + eps), xs = new Int16Array(H), k = new Uint8Array(H); + let s = X0; + if (S.sigma >= 0.5) { + let g; + do g = sqrt(-2 * Math.log(1 - Math.random())) * Math.cos(2 * PI * Math.random()) * S.sigma; + while (abs(g) > 3 * S.sigma); + s = X0 + 2 * round(g / 2); + } + let dir = 1, R = 0; + for (let t = 0; t < H; t++) { + xs[t] = s; + k[t] = R; + if (Math.random() < pc) (dir = -dir), R++; + s += dir; + } + return { xs, k }; +} + +// The present: a row that runs down the panel at 16 rows a second per unit +// of Speed, then waits HOLD rows' worth of time at the bottom while the +// paths fade, and starts again from the source with new paths. +const HOLD = 20, NP = 4; + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0.08, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0]; + S.speed = cl(kv[1], 0.2, 3); + S.eps = cl(kv[2], 0.08, 0.4); + S.focus = cl(kv[3], 0, 1); + S.sigma = (1.5 * (1 - S.focus) ** 1.5) / S.eps; + const key = S.eps.toFixed(4) + '/' + S.sigma.toFixed(3); + if (key !== S.key) { + S.key = key; + field(S); + S.paths = []; + } + if (!S.paths.length) { + for (let i = 0; i < NP; i++) S.paths.push(sample(S)); + if (S.front === undefined) S.front = 40; + } + S.front += d * 16 * S.speed; + if (S.front > H + HOLD) { + S.front = 0; + for (let i = 0; i < NP; i++) S.paths[i] = sample(S); + } +} + +function splat(S, x, y, v, c, o) { + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + add(S, ix, iy, (1 - fx) * (1 - fy) * v, c, o); + add(S, ix + 1, iy, fx * (1 - fy) * v, c, o); + add(S, ix, iy + 1, (1 - fx) * fy * v, c, o); + add(S, ix + 1, iy + 1, fx * fy * v, c, o); +} +function add(S, x, y, v, c, o) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const p = y * W + x; + S.R[p] += v * c[o]; + S.G[p] += v * c[o + 1]; + S.B[p] += v * c[o + 2]; +} +function line(S, x0, y0, x1, y1, v, c, o) { + const L = hypot(x1 - x0, y1 - y0), n = max(1, ceil(L * 3)), w = (v * L) / n; + for (let i = 1; i <= n; i++) splat(S, x0 + ((x1 - x0) * i) / n, y0 + ((y1 - y0) * i) / n, w, c, o); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { R, G, B, c, V, ph, mean, D } = S; + R.fill(0); + G.fill(0); + B.fill(0); + D.fill(1); + const tmp = [0, 0, 0], now = S.front, head = min(H - 1, now), fade = now > H ? max(0, 1 - (now - H) / HOLD) : 1; + // Shade under the lines first, so they read over the field. + const shade = (x, y, k) => { + const ix = round(x), iy = round(y); + if (iy < 0 || iy >= H) return; + for (let X = ix - 1; X <= ix + 1; X++) if (X >= 0 && X < W) D[iy * W + X] = min(D[iy * W + X], k); + }; + for (let t = 0; t <= min(H - 1, now); t++) shade(ux(mean[t]), t, 0.35); + if (fade > 0) for (const P of S.paths) for (let t = 0; t <= head; t++) shade(ux(P.xs[t]), t, 1 - 0.65 * fade); + // The field: phase as hue, the amplitude as brightness, full in the rows + // the present has passed and brightest just behind it, faint ahead of it. + for (let t = 0; t < H; t++) { + const u = cl((now - t) / 8 + 0.5, 0, 1), lit = 0.34 + 0.2 * u * u * (3 - 2 * u) + (t <= now ? 0.14 * exp(-(now - t) / 10) : 0); + for (let X = 0; X < W; X++) { + const o = t * W + X; + hsv(S.hue + 0.55 + ph[o], 0.85, V[o] * lit * D[o], tmp, 0); + R[o] = tmp[0]; + G[o] = tmp[1]; + B[o] = tmp[2]; + } + } + c[0] = c[1] = c[2] = 0.3; // cone + c[3] = c[4] = c[5] = 1; // white + hsv(S.hue + 0.14, 0.55, 1, c, 9); // the mean + // The light cone of the packet's centre. + for (let t = 0; t < H; t += 2) { + splat(S, ux(X0 + t), t, 1, c, 0); + splat(S, ux(X0 - t), t, 1, c, 0); + } + // The paths, down to the present. + if (fade > 0) + for (const P of S.paths) { + for (let t = 0; t + 1 <= head; t++) { + const v = 0.3 + 0.65 * exp(-(head - t) / 24); + line(S, ux(P.xs[t]), t, ux(P.xs[t + 1]), t + 1, v * fade, c, 3); + } + for (let t = 1; t + 1 <= head; t++) + if (P.k[t + 1] !== P.k[t]) { + hsv(S.hue + 0.55 + P.k[t + 1] / 4, 1, 1, c, 6); + splat(S, ux(P.xs[t]), t, 2 * fade, c, 6); + } + const t0 = floor(head), f = head - t0; + if (t0 < H - 1) splat(S, ux(P.xs[t0] + (P.xs[t0 + 1] - P.xs[t0]) * f), head, 1.5 * fade, c, 3); + } + // The mean position, row by row: drawn through the past, dotted ahead. + for (let t = 0; t < H; t++) if (t <= now || t % 3 === 0) splat(S, ux(mean[t]), t, t <= now ? 1.2 : 0.6, c, 9); + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) + display.setPixel(x, y, round(min(1, R[p]) * 255), round(min(1, G[p]) * 255), round(min(1, B[p]) * 255)); +} From ca8b03c4098cfc80de39a4a2e0adaa7e4a1a1c18 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 15:42:26 +0000 Subject: [PATCH 6/8] patterns: Riemann's Function and Balanced Rhythms for round 62 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TTZyp5h85ewavQ9av2v4N1 --- .../patterns/balanced-rhythms.js | 324 +++++++++++ .../patterns/riemann-function.js | 502 ++++++++++++++++++ 2 files changed, 826 insertions(+) create mode 100644 incoming/pattern-study/patterns/balanced-rhythms.js create mode 100644 incoming/pattern-study/patterns/riemann-function.js diff --git a/incoming/pattern-study/patterns/balanced-rhythms.js b/incoming/pattern-study/patterns/balanced-rhythms.js new file mode 100644 index 000000000..f7f2145be --- /dev/null +++ b/incoming/pattern-study/patterns/balanced-rhythms.js @@ -0,0 +1,324 @@ +// ===== Patternflow pattern ===== +// Title: Balanced Rhythms +// Family: rhythm +// Idea: A playhead goes round a ring of steps and every onset it strikes adds its own unit vector to a walk drawn below, tip to tail, and when the onsets are perfectly balanced, their roots of unity summing to zero, the walk closes on its starting point at the end of every bar, even for the six onsets of 30 steps that no union of regular polygons can make. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: vanishing sums of roots of unity (László Rédei, 1950s; N. G. de Bruijn, 1953; I. J. Schoenberg, 1964), the sizes a vanishing sum can have (T. Y. Lam and K. H. Leung, 2000), perfectly balanced rhythms and scales (Andrew Milne, David Bulger, Steffen Herff and William Sethares, 2015), pattern round 62 +// Round: 62 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Rhythm=0..1, Rotate=0..1 +// +// Knob 1: Hue. Turns the components' colours round the wheel together. At 0 pentagons and the clave's three side are azure, triangles and its two side violet, digons green and a second triangle or digon pink; at 0.5 they are red, lime, magenta and cyan. The walk's edges and the wedges it sweeps take the colour of the component their onset came from. Orange is skipped, as it reads brown when dim. +// Knob 2: Speed. One bar, once round the ring, takes 6 / Speed seconds: 3 s at 2, ten steps a second on 30 steps, 30 s at 0.2, 2 s at 3. Each onset flashes, draws its vector from the ring's centre and adds the same vector to the walk below, drawn out over 0.22 s. +// Knob 3: Rhythm. Below 0.2, 6 onsets of 30 steps, 5, 6, 12, 18, 24 and 25: the pentagon from step 0 and the triangle from step 5, less the digon 0 and 15 joining them, dashed, so the walk's tent of two edges at 60 degrees either side of the top stands in for the pentagon's missing edge. It is balanced and no union of polygons; of the 2^30 sets of steps, 146,854 are balanced, 26,640 of them are no union of polygons, and the 30 smallest of those, 6 onsets each, are all turns of this one. To 0.4 a pentagon, a triangle and a digon, one for each prime of 30, 10 onsets; to 0.6 the pentagon with two vertices split by two triangles, 7 onsets; to 0.8 a triangle and two digons on 12 steps, 7 onsets, where with two primes every one of the 100 balanced sets is a union of polygons; then the son clave of 16 steps, 0, 3, 6, 10 and 12, which is not balanced: each bar's walk ends 0.082 of a step from its start, dashed, and the walk runs on four bars before it starts again. +// Knob 4: Rotate. Turns one component round the ring, and a balanced rhythm stays balanced. In the first rhythm the triangle goes two steps a stop, 5 to 13, which moves the split to another vertex of the pentagon and turns the whole rhythm by 12 steps; in the second and fourth a digon goes one step a stop, and where it lands on an onset already there that step counts twice, a bigger dot and a walk edge twice as long; in the third the second triangle splits vertex 6, 18, 12 or 24 in turn. In the clave it turns the two side a step at a time and the miss grows from 0.082 to 0.710, 1.385, 2.009 and 2.556 steps. It opens at 0.06, the rhythms as listed. + +const W = 64, H = 128, NP = W * H; +const { floor, min, max, sqrt, cos, sin, exp, round, ceil, abs, 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; +} +// The wheel without its orange, 0.03 to 0.15, which reads brown when dim. +const wheel = (h) => 0.15 + (h - floor(h)) * 0.88; + +// The ring: centre (31.5, 31.5), radius 27, step k at 2 pi k / N clockwise +// from the top. The walk below: centred on (31.5, 96.5) in a 58 x 54 box. +const CX = 31.5, CY = 31.5, RING = 27, WX = 31.5, WY = 96.5, WW = 58, WH = 54; +// A component is a regular m-gon from step `off`, or a set of steps; `turn` +// lists where Rotate puts it. `links` are digons taken away: the antipodal +// pair with one end in each of the two components named. +const RHYTHMS = [ + // A pentagon and a triangle, less the digon joining them: 6 onsets of 30, + // balanced and no union of polygons. + { N: 30, comps: [{ m: 5, off: 0, c: 0 }, { m: 3, off: 5, c: 1, turn: [5, 7, 9, 11, 13] }], links: [[0, 1]] }, + // A pentagon, a triangle and a digon, one for each prime of 30: 10 onsets. + { N: 30, comps: [{ m: 5, off: 1, c: 0 }, { m: 3, off: 0, c: 1 }, { m: 2, off: 2, c: 2, turn: [2, 3, 4, 5, 6] }], links: [] }, + // The pentagon with two of its vertices split by two triangles: 7 onsets. + { N: 30, comps: [{ m: 5, off: 0, c: 0 }, { m: 3, off: 5, c: 1 }, { m: 3, off: 11, c: 3, turn: [11, 13, 17, 19] }], links: [[0, 1], [0, 2]] }, + // 12 steps, two primes: a triangle and two digons, 7 onsets. + { N: 12, comps: [{ m: 3, off: 0, c: 1 }, { m: 2, off: 1, c: 2, turn: [1, 2, 3, 4, 5] }, { m: 2, off: 5, c: 3 }], links: [] }, + // The son clave of 16 steps, which is not balanced, its two side turned. + { N: 16, comps: [{ set: [0, 3, 6], c: 0 }, { set: [10, 12], c: 1, turn: [0, 1, 2, 3, 4] }], links: [] }, +]; + +// An unbalanced walk keeps four bars, then starts again. +const KEEP = 4; + +function build(S, which, j) { + const R = RHYTHMS[which], N = R.N; + const pts = R.comps.map((c) => { + const t = c.turn ? c.turn[min(c.turn.length - 1, round(j * (c.turn.length - 1)))] : c.off ?? 0; + if (c.set) return c.set.map((k) => (k + t) % N); + const out = []; + for (let i = 0; i < c.m; i++) out.push((t + (i * N) / c.m) % N); + return out; + }); + const coef = new Int32Array(N), owner = new Int32Array(N).fill(-1); + pts.forEach((p, ci) => + p.forEach((k) => { + coef[k]++; + if (owner[k] < 0) owner[k] = R.comps[ci].c; + }), + ); + const links = []; + for (const [a, b] of R.links) { + const k = pts[a].find((p) => pts[b].includes((p + N / 2) % N)); + links.push(k); + coef[k]--; + coef[(k + N / 2) % N]--; + } + // The onsets in time order, each with its vector (sin, -cos) on the + // screen, as long as its weight. + const on = []; + for (let k = 0; k < N; k++) if (coef[k] > 0) on.push(k); + const vx = new Float64Array(N), vy = new Float64Array(N); + for (let k = 0; k < N; k++) { + const a = (2 * PI * k) / N; + vx[k] = coef[k] * sin(a); + vy[k] = -coef[k] * cos(a); + } + let x = 0, y = 0; + const P = [[0, 0]]; + for (const k of on) P.push([(x += vx[k]), (y += vy[k])]); + const fx = x, fy = y, balanced = sqrt(fx * fx + fy * fy) < 1e-9; + // Fit the walk, one bar when it closes, four when it does not. + const fit = balanced ? 1 : KEEP; + let x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9; + for (let b = 0; b < fit; b++) + for (const [px, py] of P) { + x0 = min(x0, px + b * fx); + x1 = max(x1, px + b * fx); + y0 = min(y0, py + b * fy); + y1 = max(y1, py + b * fy); + } + const e = min(WW / max(1e-6, x1 - x0), WH / max(1e-6, y1 - y0), 24); + Object.assign(S, { + which, j, N, pts, coef, owner, links, on, vx, vy, P, fx, fy, balanced, e, + ox: WX - ((x0 + x1) / 2) * e, oy: WY - ((y0 + y1) / 2) * e, + phase: -1e-6, pos: [0, 0], start: [0, 0], verts: [[0, 0]], hist: [], kept: 0, + fire: new Float64Array(N).fill(-99), edgeAt: -99, closeAt: -99, + }); +} + +export function setup(params) { + params.s = null; +} + +export function update(dt, input, params) { + const k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + let S = params.s; + if (!S) S = params.s = { clock: 0, maxClose: 0, barCount: 0, col: new Float32Array(15), R: new Float32Array(NP), G: new Float32Array(NP), B: new Float32Array(NP) }; + const which = min(RHYTHMS.length - 1, floor(cl(k[2], 0, 1) * RHYTHMS.length)), j = cl(k[3], 0, 1); + const R = RHYTHMS[which], turns = R.comps.find((c) => c.turn)?.turn.length ?? 1; + const jj = round(j * (turns - 1)) / max(1, turns - 1); + if (S.which !== which || S.j !== jj) build(S, which, jj); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.clock += d; + // One bar takes 6 / Speed seconds. + const old = S.phase * S.N; + S.phase += d / (6 / S.speed); + const now = S.phase * S.N; + for (let a = floor(old) + 1; a <= floor(now); a++) { + const step = ((a % S.N) + S.N) % S.N; + if (step === 0) { + // A new bar. The walk should be back where the bar began. + const ex = S.pos[0] - S.start[0], ey = S.pos[1] - S.start[1]; + if (S.balanced && a > 0) S.maxClose = max(S.maxClose, sqrt(ex * ex + ey * ey)); + if (a > 0) S.barCount++; + if (!S.balanced) { + S.hist.push(S.verts); + if (++S.kept >= KEEP) (S.kept = 0), (S.hist = []), (S.pos = [0, 0]); + } + S.start = [S.pos[0], S.pos[1]]; + S.verts = [[S.pos[0], S.pos[1]]]; + } + if (S.coef[step] > 0) { + S.pos = [S.pos[0] + S.vx[step], S.pos[1] + S.vy[step]]; + S.verts.push(S.pos); + S.fire[step] = S.clock; + S.edgeAt = S.clock; + const cx = S.pos[0] - S.start[0], cy = S.pos[1] - S.start[1]; + if (S.balanced && S.verts.length > 2 && sqrt(cx * cx + cy * cy) < 1e-9) S.closeAt = S.clock; + } + } +} + +// Light into the float frame: a bilinear dot, and lines of them. +function dot(S, x, y, v, c, o) { + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + put(S, ix, iy, (1 - fx) * (1 - fy) * v, c, o); + put(S, ix + 1, iy, fx * (1 - fy) * v, c, o); + put(S, ix, iy + 1, (1 - fx) * fy * v, c, o); + put(S, ix + 1, iy + 1, fx * fy * v, c, o); +} +function put(S, x, y, v, c, o) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const p = y * W + x; + S.R[p] += v * c[o]; + S.G[p] += v * c[o + 1]; + S.B[p] += v * c[o + 2]; +} +function line(S, x0, y0, x1, y1, v, c, o, dash) { + const len = sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2), n = max(1, ceil(len * 2.5)), w = (v * len) / n; + for (let i = 0; i <= n; i++) { + if (dash && floor((i / n) * len / 2) % 2) continue; + dot(S, x0 + ((x1 - x0) * i) / n, y0 + ((y1 - y0) * i) / n, w, c, o); + } +} +// A line two LEDs wide: two lines half an LED either side. +function thick(S, x0, y0, x1, y1, v, c, o) { + const len = sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2) || 1, nx = (-(y1 - y0) / len) * 0.45, ny = ((x1 - x0) / len) * 0.45; + line(S, x0 + nx, y0 + ny, x1 + nx, y1 + ny, v * 0.75, c, o); + line(S, x0 - nx, y0 - ny, x1 - nx, y1 - ny, v * 0.75, c, o); +} +// A round dot of radius r, its rim soft. +function disc(S, x, y, r, v, c, o) { + for (let iy = floor(y - r - 1); iy <= ceil(y + r + 1); iy++) + for (let ix = floor(x - r - 1); ix <= ceil(x + r + 1); ix++) { + const d = sqrt((ix + 0.5 - x) ** 2 + (iy + 0.5 - y) ** 2), f = cl(r + 0.5 - d, 0, 1); + if (f > 0) put(S, ix, iy, f * v, c, o); + } +} +const WHITE = [1, 1, 1, 0.35, 0.35, 0.35, 1, 0.25, 0.3]; + +// Fill a convex polygon with v of colour c, row by row: a pixel is in when +// its centre is. Two wedges that share an edge share no pixel. +function fill(S, poly, v, c, o) { + const n = poly.length; + if (n < 3) return; + let y0 = 1e9, y1 = -1e9; + for (const q of poly) (y0 = min(y0, q[1])), (y1 = max(y1, q[1])); + for (let y = max(0, ceil(y0 - 0.5)); y <= min(H - 1, floor(y1 - 0.5)); y++) { + const py = y + 0.5 + 6.1e-5; + let xa = 1e9, xb = -1e9; + for (let i = 0; i < n; i++) { + const a = poly[i], b = poly[(i + 1) % n]; + if ((a[1] <= py && b[1] > py) || (b[1] <= py && a[1] > py)) { + const x = a[0] + ((py - a[1]) / (b[1] - a[1])) * (b[0] - a[0]); + xa = min(xa, x); + xb = max(xb, x); + } + } + // Centres x + 0.5 in [xa, xb). + for (let x = max(0, ceil(xa - 0.5 - 3.7e-5)); x <= min(W - 1, ceil(xb - 0.5 - 3.7e-5) - 1); x++) { + const p = y * W + x; + S.R[p] += v * c[o]; + S.G[p] += v * c[o + 1]; + S.B[p] += v * c[o + 2]; + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { R, G, B, col, N, coef, owner } = S; + R.fill(0); + G.fill(0); + B.fill(0); + // Component colours: azure, violet, green, pink at Hue 0. + for (let c = 0; c < 4; c++) hsv(wheel(0.49 + S.hue + [0, 0.19, 0.77, 0.4][c]), 0.75, 1, col, c * 3); + const t = S.clock; + const ang = (k) => (2 * PI * k) / N; + const rx = (k, r) => CX + r * sin(ang(k)), ry = (k, r) => CY - r * cos(ang(k)); + + // The ring, faint, and the playhead with its arc trailing behind and a + // faint hand from the centre. + const u = S.phase - floor(S.phase); + for (let i = 0; i < 160; i++) dot(S, CX + RING * sin((2 * PI * i) / 160), CY - RING * cos((2 * PI * i) / 160), 0.16, WHITE, 3); + for (let i = 0; i < 48; i++) { + const a = 2 * PI * u - (i / 48) * 1.1; + dot(S, CX + (RING + 3.5) * sin(a), CY - (RING + 3.5) * cos(a), 0.6 * (1 - i / 48), WHITE, 0); + } + disc(S, CX + (RING + 3.5) * sin(2 * PI * u), CY - (RING + 3.5) * cos(2 * PI * u), 1.2, 1, WHITE, 0); + line(S, CX, CY, CX + (RING - 3) * sin(2 * PI * u), CY - (RING - 3) * cos(2 * PI * u), 0.22, WHITE, 0); + // The polygons inside, each lit up a moment when one of its onsets is struck. + const comps = RHYTHMS[S.which].comps; + S.pts.forEach((p, ci) => { + const c = comps[ci].c, sorted = [...p].sort((a, b) => a - b); + let f = 0; + for (const k of p) f = max(f, exp(-(t - S.fire[k]) / 0.3)); + for (let i = 0; i < sorted.length; i++) { + const a = sorted[i], b = sorted[(i + 1) % sorted.length]; + if (sorted.length === 2 && i === 1) break; + line(S, rx(a, RING - 2), ry(a, RING - 2), rx(b, RING - 2), ry(b, RING - 2), 0.4 + 0.5 * f, col, c * 3); + } + }); + // The digons taken away, dashed. + for (const k of S.links) { + line(S, rx(k, RING - 2), ry(k, RING - 2), rx(k + N / 2, RING - 2), ry(k + N / 2, RING - 2), 0.55, WHITE, 3, true); + // Its ends, emptied: a hollow ring where each polygon lost a vertex. + for (const q of [k, k + N / 2]) + for (let i = 0; i < 16; i++) dot(S, rx(q, RING) + 2.2 * cos((PI * i) / 8), ry(q, RING) + 2.2 * sin((PI * i) / 8), 0.16, WHITE, 0); + } + // The steps: onsets in their colours, flashing white as they are struck, + // with their vectors from the centre; rests a dim grey. + for (let k = 0; k < N; k++) { + const x = rx(k, RING), y = ry(k, RING); + if (coef[k] > 0) { + const f = exp(-(t - S.fire[k]) / 0.35), c = owner[k] * 3; + const tmp = [col[c] + (1 - col[c]) * f, col[c + 1] + (1 - col[c + 1]) * f, col[c + 2] + (1 - col[c + 2]) * f]; + disc(S, x, y, (coef[k] > 1 ? 2.3 : 1.6) + 0.6 * f, 0.75 + 0.25 * f, tmp, 0); + const fv = exp(-(t - S.fire[k]) / 0.4); + if (fv > 0.02) thick(S, CX, CY, rx(k, RING - 2.5), ry(k, RING - 2.5), fv, col, c); + } else dot(S, x, y, 0.7, WHITE, 3); + } + + // The walk. + const e = S.e, X = (p) => S.ox + p[0] * e, Y = (p) => S.oy + p[1] * e; + const verts = S.verts, last = verts.length - 1; + // The edge struck last is drawn out over 0.22 s. + const ease = min(1, (t - S.edgeAt) / 0.22), ee = 1 - (1 - ease) ** 3; + let tipx = verts[last][0], tipy = verts[last][1]; + if (last > 0 && ease < 1) { + tipx = verts[last - 1][0] + (verts[last][0] - verts[last - 1][0]) * ee; + tipy = verts[last - 1][1] + (verts[last][1] - verts[last - 1][1]) * ee; + } + const tip = [X([tipx, tipy]), Y([tipx, tipy])]; + const own = (i) => owner[S.on[i % S.on.length]] * 3; + const sx = X(S.start), sy = Y(S.start); + if (S.balanced) { + // The whole bar's polygon, faint, from where this bar began. + const poly = S.P.slice(0, -1).map((p) => [X([p[0] + S.start[0], p[1] + S.start[1]]), Y([p[0] + S.start[0], p[1] + S.start[1]])]); + fill(S, poly, 0.12, col, 0); + for (let i = 0; i < S.on.length; i++) { + const a = poly[i], b = poly[(i + 1) % poly.length]; + line(S, a[0], a[1], b[0], b[1], 0.35, col, own(i)); + } + // The part swept so far this bar, a wedge from the start to each edge in + // the edge's colour. + for (let i = 1; i <= last; i++) { + const a = [X(verts[i - 1]), Y(verts[i - 1])], b = i === last ? tip : [X(verts[i]), Y(verts[i])]; + fill(S, [[sx, sy], a, b], 0.22, col, own(i - 1)); + } + } else { + // Earlier bars of the walk, dimmer, each with its miss dashed in red. + for (const h of S.hist) { + for (let i = 1; i < h.length; i++) line(S, X(h[i - 1]), Y(h[i - 1]), X(h[i]), Y(h[i]), 0.45, col, own(i - 1)); + line(S, X(h[h.length - 1]), Y(h[h.length - 1]), X(h[0]), Y(h[0]), 0.8, WHITE, 6, true); + disc(S, X(h[h.length - 1]), Y(h[h.length - 1]), 1, 0.8, WHITE, 6); + } + } + for (let i = 1; i < last; i++) thick(S, X(verts[i - 1]), Y(verts[i - 1]), X(verts[i]), Y(verts[i]), 0.9, col, own(i - 1)); + if (last > 0) thick(S, X(verts[last - 1]), Y(verts[last - 1]), tip[0], tip[1], 1.1, col, own(last - 1)); + if (!S.balanced && last > 0) line(S, tip[0], tip[1], sx, sy, 0.6, WHITE, 6, true); + // The start, ringing when the walk comes back to it, and the tip. + const fc = exp(-(t - S.closeAt) / 0.45); + disc(S, sx, sy, 1.3 + fc, 0.7 + 0.3 * fc, WHITE, 0); + if (fc > 0.02) for (let i = 0; i < 32; i++) dot(S, sx + (2.5 + 6 * (1 - fc)) * cos((2 * PI * i) / 32), sy + (2.5 + 6 * (1 - fc)) * sin((2 * PI * i) / 32), 0.6 * fc, WHITE, 0); + disc(S, tip[0], tip[1], 1.2, 1, WHITE, 0); + + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) + display.setPixel(x, y, round(min(1, R[p]) * 255), round(min(1, G[p]) * 255), round(min(1, B[p]) * 255)); +} diff --git a/incoming/pattern-study/patterns/riemann-function.js b/incoming/pattern-study/patterns/riemann-function.js new file mode 100644 index 000000000..ed8c17165 --- /dev/null +++ b/incoming/pattern-study/patterns/riemann-function.js @@ -0,0 +1,502 @@ +// ===== Patternflow pattern ===== +// Title: Riemann's Function +// Family: fractal +// Idea: A pen draws Duistermaat's curve phi(t), the complex form of Riemann's non-differentiable function, as a heart of spirals across the top of the panel, and the loupe below dives into one rational point of it, where the curve turns a right angle or, at p/q with p and q both odd, runs straight through with Gerver's derivative -1/2. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Riemann's non-differentiable function (Bernhard Riemann, c. 1861, reported by Karl Weierstrass, 1872), non-differentiability at the irrationals (G. H. Hardy, 1916), differentiability at p/q with p and q odd (Joseph Gerver, 1970), the complex curve phi (J. J. Duistermaat, 1991), the corner of a polygonal vortex filament under the binormal flow (Francisco de la Hoz and Luis Vega, 2014), pattern round 62 +// Round: 62 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Point=0..1, Depth=0..1 +// +// Knob 1: Hue. Turns the curve's colours round the wheel; they run by the distance from t = 1, so the heart is coloured the same on both halves, as phi(2 - t) = -conj phi(t) makes it. At 0 the top centre, t = 1, is azure, the sides violet and the tip, t = 0, pink; at 0.25 violet to green, at 0.5 yellow to cyan, at 0.75 green to violet. Orange is skipped, as it reads brown when dim. In the loupe the hue also swings by 0.07 once every factor 16 in s = t - t0, bands that flow outward as the loupe dives. +// Knob 2: Speed. One lap of the pen round t in [0, 2] takes 16 / Speed seconds: 8 s at 2, 80 s at 0.2, 5.3 s at 3. The last 0.3 s of its trail burns white, the curve it has passed stays bright for about a fifth of a lap and settles to 40 percent. The loupe dives in and comes out once a lap, deepest just as the pen passes its point, which it lights up; at Speed 2 that is 2.5 s in. +// Knob 3: Point. The rational t0 the loupe dives into, boxed on the heart: 1 below 0.2, 1/2 to 0.4, 1/3 to 0.6, 0 (the tip) to 0.8, then 2/3. At 1 and 1/3, p and q both odd, the curve passes straight through, horizontal, phi'(t0) = -1/2, a bowtie of small rosettes, one every 8 s^2 in s, closing on the point; the pattern's own sums give (phi(1 + s) - phi(1)) / s = -0.300 - 0.181i, -0.410, -0.456 and -0.490 at s = 1/32, 1/128, 1/512 and 1/2048. Elsewhere it turns a right angle along c e^(i pi / 4) sqrt s and -c e^(-i pi / 4) sqrt |s|, c the mean of e^(i pi p r^2 / q) over r mod 2q: arms up and left at 1/2, where |phi(t0 + s) - phi(t0)| / sqrt s reaches 0.70711 = 1 / sqrt 2 by s = 1e-7; the V of the tip, 0.99997 at s = 7.5e-9; arms up-left and down-left at 2/3, 0.57738 against 1 / sqrt 3 = 0.57735. The dim grey lines in the loupe are these predicted tangents. +// Knob 4: Depth. How far the loupe dives, 2^(4 + 4 Depth) times the loupe's opening scale, 200 LEDs a unit: 16 times at 0, 19 at the opening 0.06, where the loupe is 1/48 of the heart's width, 64 at 0.5 and 256 at 1, 1/644 of it. At t0 = 1 the bowtie stands 14 LEDs tall at the loupe's ends at 0.5 and 8 at 1, and 2 in the middle at both. + +const W = 64, H = 128, NP = W * H; +const { floor, min, max, sqrt, cos, sin, exp, round, ceil, abs, log2, 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; +} +// The wheel without its orange, 0.03 to 0.15, which reads brown when dim. +const wheel = (h) => 0.15 + (h - floor(h)) * 0.88; + +// phi(t) = sum over n >= 1 of e^(i pi n^2 t) / (i pi n^2). The table of the +// whole period sums the first NT terms: past n = 128 a term moves the top +// view by less than 1/600 of an LED. +const NT = 128; +// The whole period t in [0, 2), G samples. +const G = 12288, HG = 2 / G; +// The rational points the loupe can dive into, p / q. +const POINTS = [[1, 1], [1, 2], [1, 3], [0, 1], [2, 3]]; + +// The top view: 77 LEDs a unit, phi(1) = i pi / 12 near the top. +const TS = 77, TX = 31.5, TY = 31.2; +// The loupe: rows 74 to 127, centre (31.5, 100.5), half width RHO0 / M. +const LY0 = 74, LCX = 31.5, LCY = 100.5, RHO0 = 0.16; + +// Fresnel auxiliary functions f(w), g(w): the integral of e^(i pi u^2 / 2) +// from w to infinity is (g + i f) e^(i pi w^2 / 2). A power series below +// w = 3, the asymptotic series above. +const FG = [0, 0]; +function fresnelFG(w) { + if (w >= 3) { + const x = PI * w * w, x2 = 1 / (x * x); + FG[0] = (1 - 3 * x2 + 105 * x2 * x2 - 10395 * x2 * x2 * x2) / (PI * w); + FG[1] = (1 - 15 * x2 + 945 * x2 * x2) / (PI * PI * w * w * w); + return; + } + const a = PI / 2, w2 = w * w, w4 = w2 * w2; + let C = 0, S = 0, tc = w, ts = a * w * w2; + for (let n = 0; n < 32; n++) { + C += tc / (4 * n + 1); + S += ts / (4 * n + 3); + tc *= (-a * a * w4) / ((2 * n + 1) * (2 * n + 2)); + ts *= (-a * a * w4) / ((2 * n + 2) * (2 * n + 3)); + } + const th = a * w2, A = 0.5 - C, B = 0.5 - S, co = cos(th), si = sin(th); + FG[0] = B * co - A * si; + FG[1] = A * co + B * si; +} +// I(s), the terms past a cut at n = N summed as an integral from L = N + 1/2: +// the sum over n > N of (e^(i pi n^2 s) - 1) / (i pi n^2). Near a rational +// p/q every term there carries the same mean factor c, so c I(s) puts back +// the square-root corner the cut series would round off. +const IC = [0, 0]; +function tail(s, L) { + const as = abs(s), ph = PI * L * L * s; + let re = sin(ph) / (PI * L), im = (1 - cos(ph)) / (PI * L); + fresnelFG(L * sqrt(2 * as)); + const r = sqrt(2 * as), pa = PI * L * L * as, cp = cos(pa), sp = sin(pa); + const pr = r * (FG[1] * cp - FG[0] * sp), pi = r * (FG[1] * sp + FG[0] * cp); + if (s >= 0) (re += pr), (im += pi); + else (re -= pr), (im += pi); + IC[0] = re; + IC[1] = im; +} + +let GX = null, GY = null; +// The whole curve once, by turning each term's phasor a step at a time. +function curve() { + if (GX) return; + GX = new Float64Array(G + 1); + GY = new Float64Array(G + 1); + for (let n = 1; n <= NT; n++) { + const k = PI * n * n, a = 1 / k, rc = cos(k * HG), rs = sin(k * HG); + // 1 / (i pi n^2) = -i / (pi n^2) + let x = 0, y = -a; + for (let i = 0; i <= G; i++) { + GX[i] += x; + GY[i] += y; + const nx = x * rc - y * rs; + y = x * rs + y * rc; + x = nx; + } + } +} + +// The loupe's polyline for one point: shells about s = 0, each 4 times +// thinner than the last, from s = 1/8 down to 1/8 / 4^12 = 7.5e-9, each +// summed by turning its terms' phasors in even steps, then the rest of the +// period from the table. Near p/q with p and q odd the curve runs along its +// tangent as a train of small rosettes, one every 8 s^2 in s and 3.1 s^1.5 +// across (3.29 at s = 1/32, 3.11 at 1/256, summed to 3000 terms), so there +// the shells take enough steps for each rosette as the loupe will show it: +// 800 steps from s = 1/8 to 1/32, 16 to a rosette from 1/512 to 1/2048, where +// at 256 times a rosette is up to 5 LEDs across. Inside s = 1/2048 they are +// 1.7 LEDs at most and the 1024 terms a shell sums there no longer hold them, +// so those shells take 240 steps and draw the line. A shell sums 256 terms out +// to s = 1/32 and 1024 inside it. +const S0 = 1 / 8, SHELLS = 12, QODD = [800, 800, 1300, 2600, 3100], QCORNER = [600], QMIN = 240, GROUP = 120; +const CUT = (j) => (j < 2 ? 256 : 1024); +const NS = 1024; +function loupe(S, which) { + const [p, q] = POINTS[which], t0 = p / q; + // c, the mean of e^(i pi p r^2 / q) over r mod 2q: zero exactly when p and + // q are both odd. + let cr = 0, ci = 0; + for (let r = 0; r < 2 * q; r++) { + const a = (PI * p * r * r) / q; + cr += cos(a); + ci += sin(a); + } + cr /= 2 * q; + ci /= 2 * q; + if (abs(cr) < 1e-9) cr = 0; + if (abs(ci) < 1e-9) ci = 0; + const odd = cr === 0 && ci === 0; + // The terms e^(i pi n^2 t0) / (i pi n^2) = (sin - i cos) / (pi n^2). + // For each cut N: phi at t0 cut there, and the coefficients' own part past + // it. With B_n the partial sums of e^(i pi m^2 t0) - c, the terms past N + // add ( - B_N) times one more term, the mean of B over a period; + // this keeps the slope at the point, - c / 2, which comes to -1/2 at + // every p/q with p and q odd. + const AR = new Float64Array(NS + 1), AI = new Float64Array(NS + 1); + let px = 0, py = 0, br = 0, bi = 0, mr = 0, mi = 0; + const at = {}; + for (let n = 1; n <= NS; n++) { + const k = PI * n * n, ph = (PI * ((n * n * p) % (2 * q))) / q; + AR[n] = sin(ph) / k; + AI[n] = -cos(ph) / k; + px += AR[n]; + py += AI[n]; + br += cos(ph) - cr; + bi += sin(ph) - ci; + if (n <= 2 * q) (mr += br / (2 * q)), (mi += bi / (2 * q)); + if (n === NT || n === 256 || n === NS) at[n] = [px, py, br, bi]; + } + const cut = {}; + for (const N of [NT, 256, NS]) { + const [x, y, b1, b2] = at[N]; + cut[N] = [N + 0.5, mr - b1, mi - b2, x, y]; + } + const xs = [], ys = [], ss = []; + const put = (s, x, y, N) => { + const [L, kr, ki, ox, oy] = cut[N]; + tail(s, L); + // K (e^(i pi L^2 s) - 1) / (i pi L^2) + const ph = PI * L * L * s, fr = sin(ph) / (PI * L * L), fi = (1 - cos(ph)) / (PI * L * L); + xs.push(x - ox + cr * IC[0] - ci * IC[1] + kr * fr - ki * fi); + ys.push(y - oy + cr * IC[1] + ci * IC[0] + kr * fi + ki * fr); + ss.push(s); + }; + // Each term's phasor and its turn for one step, side by side. + const WT = new Float64Array(4 * (NS + 1)); + const shell = (sa, sb, Q, N) => { + // sa to sb in Q steps, the end sb left out. + const h = (sb - sa) / Q; + for (let n = 1, o = 4; n <= N; n++, o += 4) { + const k = PI * n * n, c0 = cos(k * sa), s0 = sin(k * sa); + WT[o] = AR[n] * c0 - AI[n] * s0; + WT[o + 1] = AR[n] * s0 + AI[n] * c0; + WT[o + 2] = cos(k * h); + WT[o + 3] = sin(k * h); + } + const end = 4 * N + 4; + for (let i = 0; i < Q; i++) { + let x = 0, y = 0; + for (let o = 4; o < end; o += 4) { + const wr = WT[o], wi = WT[o + 1], rc = WT[o + 2], rs = WT[o + 3]; + x += wr; + y += wi; + WT[o] = wr * rc - wi * rs; + WT[o + 1] = wr * rs + wi * rc; + } + put(sa + i * h, x, y, N); + } + }; + const QS = (j) => (odd ? QODD[j] : QCORNER[j]) || QMIN; + // At t0 = 0 and 1 the curve is its own mirror about the point, + // z(-s) = -conj z(s), from phi(2 - t) = -conj phi(t); one side is enough. + if (q === 1) { + for (let j = SHELLS - 1; j >= 0; j--) shell(S0 / 4 ** (j + 1), S0 / 4 ** j, QS(j), CUT(j)); + const m = xs.length; + for (let i = m - 1; i >= 0; i--) xs.push(-xs[i]), ys.push(ys[i]), ss.push(-ss[i]); + // The mirrored half runs from -S0 in to -S0 / 4^12: move it to the front + // so the line runs -S0 .. 0 .. S0. + const nx = xs.splice(m), ny = ys.splice(m), ns = ss.splice(m); + xs.unshift(...nx, 0); + ys.unshift(...ny, 0); + ss.unshift(...ns, 0); + } else { + for (let j = 0; j < SHELLS; j++) shell(-S0 / 4 ** j, -S0 / 4 ** (j + 1), QS(j), CUT(j)); + xs.push(0), ys.push(0), ss.push(0); + for (let j = SHELLS - 1; j >= 0; j--) shell(S0 / 4 ** (j + 1), S0 / 4 ** j, QS(j), CUT(j)); + } + // The rest of the period from the table, from t0 + S0 round to t0 - S0. + const i0 = ceil((t0 + S0) / HG - 1e-9), i1 = floor((t0 + 2 - S0) / HG + 1e-9); + for (let i = i0; i <= i1; i++) { + const g = ((i % G) + G) % G; + let s = i * HG - t0; + s -= 2 * floor((s + 1) / 2); + put(s, GX[g], GY[g], NT); + } + // Keep a sample only once the curve has moved a quarter of an LED from the + // last one kept, as the loupe shows it when that sample is at its edge: + // 1/180 of the distance to phi(t0), and no less than 1/200 of an LED at + // 256 times. + const n0 = xs.length, kx = [], ky = [], ks = [], gs = []; + let lx = 1e9, ly = 1e9; + for (let i = 0; i < n0; i++) { + const x = xs[i], y = ys[i], r = sqrt(x * x + y * y), d = sqrt((x - lx) ** 2 + (y - ly) ** 2); + if (ss[i] !== 0 && d < max(r / 180, 1e-7) && i < n0 - 1) continue; + if (kx.length % GROUP === 0) gs.push(kx.length); + kx.push(x), ky.push(y), ks.push(ss[i]); + (lx = x), (ly = y); + } + const n = kx.length, ng = gs.length; + gs.push(n); + const box = new Float64Array(ng * 5); + for (let g = 0; g < ng; g++) { + let x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9, len = 0; + for (let i = gs[g]; i <= gs[g + 1]; i++) { + const k = i % n, x = kx[k], y = ky[k]; + x0 = min(x0, x); + x1 = max(x1, x); + y0 = min(y0, y); + y1 = max(y1, y); + if (i > gs[g]) len += sqrt((x - kx[(i - 1) % n]) ** 2 + (y - ky[(i - 1) % n]) ** 2); + } + box.set([x0, x1, y0, y1, len], g * 5); + } + S.lx = Float64Array.from(kx); + S.ly = Float64Array.from(ky); + S.ls = Float64Array.from(ks); + S.lg = Int32Array.from(gs); + S.lbox = box; + S.t0 = t0; + S.c = [cr, ci]; + S.which = which; + S.lcol = null; +} + +// Top view splats: for each LED a line of the curve crosses, how much line +// from which stretch of t, NB stretches to the period. Built once. +const NB = 1024, PB = G / NB; +let TE = null; +function topSplats() { + if (TE) return; + const P = [], Wt = [], I = [], acc = new Float32Array(NP), seen = []; + const touch = (a, b, v) => { + if (a < 0 || a >= W || b < 0 || b >= LY0 - 2 || v <= 0) return; + const p = b * W + a; + if (acc[p] === 0) seen.push(p); + acc[p] += v; + }; + for (let bk = 0; bk < NB; bk++) { + for (let i = bk * PB; i < (bk + 1) * PB; i++) { + const x0 = TX + GX[i] * TS, y0 = TY - GY[i] * TS, x1 = TX + GX[i + 1] * TS, y1 = TY - GY[i + 1] * TS; + const len = sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2), n = max(1, ceil(len * 2)), w = len / n; + for (let k = 0; k < n; k++) { + const u = (k + 0.5) / n, x = x0 + (x1 - x0) * u, y = y0 + (y1 - y0) * u; + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + touch(ix, iy, (1 - fx) * (1 - fy) * w); + touch(ix + 1, iy, fx * (1 - fy) * w); + touch(ix, iy + 1, (1 - fx) * fy * w); + touch(ix + 1, iy + 1, fx * fy * w); + } + } + for (const p of seen) P.push(p), Wt.push(acc[p]), I.push(bk), (acc[p] = 0); + seen.length = 0; + } + TE = { p: Int32Array.from(P), w: Float32Array.from(Wt), i: Int32Array.from(I) }; +} + +export function setup(params) { + params.s = null; +} + +function init(params) { + curve(); + topSplats(); + const S = { + clock: 0, hue: -1, which: -1, + bcol: new Float32Array(NB * 3), bb: new Float32Array(NB), bw: new Float32Array(NB), + R: new Float32Array(NP), Gc: new Float32Array(NP), B: new Float32Array(NP), D: new Float32Array(NP), + }; + params.s = S; + return S; +} + +// Azure at t = 1, the top, to pink at t = 0 and 2, the tip: mirror-symmetric +// like the curve, phi(2 - t) = -conj phi(t). +const hueAt = (S, t, band = 0) => wheel(0.5 + S.hue + 0.42 * abs(1 - t) + band); + +export function update(dt, input, params) { + const S = params.s || init(params); + const k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.speed = cl(k[1], 0.2, 3); + const which = min(POINTS.length - 1, floor(cl(k[2], 0, 1) * POINTS.length)); + if (which !== S.which) loupe(S, which); + S.depth = 4 + 4 * cl(k[3], 0, 1); + if (k[0] !== S.hue) { + S.hue = k[0]; + for (let b = 0; b < NB; b++) hsv(hueAt(S, (b + 0.5) * PB * HG), 0.72, 1, S.bcol, b * 3); + S.lcol = null; + } + S.clock += d; +} + +function add(S, x, y, v, r, g, b) { + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + put(S, ix, iy, (1 - fx) * (1 - fy) * v, r, g, b); + put(S, ix + 1, iy, fx * (1 - fy) * v, r, g, b); + put(S, ix, iy + 1, (1 - fx) * fy * v, r, g, b); + put(S, ix + 1, iy + 1, fx * fy * v, r, g, b); +} +function put(S, x, y, v, r, g, b) { + if (x < 0 || x >= W || y < LY0 || y >= H) return; + const p = y * W + x; + S.R[p] += v * r; + S.Gc[p] += v * g; + S.B[p] += v * b; + S.D[p] += v; +} +// Light straight into the frame, after the tone curve: a bilinear dot, and +// a thin line of them kept to rows `top` and below. +function dot(S, x, y, v, top = 0) { + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + lit(S, ix, iy, (1 - fx) * (1 - fy) * v, top); + lit(S, ix + 1, iy, fx * (1 - fy) * v, top); + lit(S, ix, iy + 1, (1 - fx) * fy * v, top); + lit(S, ix + 1, iy + 1, fx * fy * v, top); +} +function lit(S, a, b, v, top) { + if (a < 0 || a >= W || b < top || b >= H) return; + const p = b * W + a; + S.R[p] += v; + S.Gc[p] += v; + S.B[p] += v; +} +function glow(S, x0, y0, x1, y1, v, top) { + const len = sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2), n = max(1, ceil(len * 1.5)), w = (v * len) / (n + 1); + for (let k = 0; k <= n; k++) dot(S, x0 + ((x1 - x0) * k) / n, y0 + ((y1 - y0) * k) / n, w, top); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { R, Gc, B, D, bcol, bb, bw } = S; + R.fill(0); + Gc.fill(0); + B.fill(0); + D.fill(0); + // One lap of the pen takes 16 / Speed seconds; at Speed 2 it reaches the + // loupe's point 2.5 s in, as deep as the loupe goes. + const T = 16 / S.speed, lap = S.clock / T + S.t0 / 2 - 2.5 / 8; + const tp = 2 * (lap - floor(lap)); + const base = 0.4, tau = T / 5; + const age = (t) => { + let a = tp - t; + a -= 2 * floor(a / 2); + return (a * T) / 2; + }; + for (let b = 0; b < NB; b++) bb[b] = base + (1 - base) * exp(-age((b + 0.5) * PB * HG) / tau); + // The top view. + const { p: EP, w: EW, i: EI } = TE; + // The last third of a second behind the pen burns white. + for (let b = 0; b < NB; b++) bw[b] = exp(-age((b + 0.5) * PB * HG) / 0.3); + for (let e = 0; e < EP.length; e++) { + const p = EP[e], b = EI[e], w = EW[e], v = w * bb[b], h = bw[b]; + R[p] += v * (bcol[b * 3] + (1 - bcol[b * 3]) * h); + Gc[p] += v * (bcol[b * 3 + 1] + (1 - bcol[b * 3 + 1]) * h); + B[p] += v * (bcol[b * 3 + 2] + (1 - bcol[b * 3 + 2]) * h); + D[p] += w; + } + + // The dive: magnification 2^(depth w), deepest when the pen is at t0. + let ph = lap - S.t0 / 2; + ph -= floor(ph); + const wv = (1 + cos(2 * PI * ph)) / 2, M = 2 ** (S.depth * wv); + const kz = (32 * M) / RHO0; + const { lx, ly, ls, lg, lbox } = S, n = lx.length, ng = lg.length - 1; + if (!S.lcol) { + S.lcol = new Float32Array(n * 3); + for (let i = 0; i < n; i++) { + let t = S.t0 + ls[i]; + t -= 2 * floor(t / 2); + // Bands of scale: the hue swings once every factor 16 in |s|. + const band = ls[i] === 0 ? 0 : 0.07 * cos((PI * log2(abs(ls[i]))) / 2); + hsv(hueAt(S, t, band), 0.72, 1, S.lcol, i * 3); + } + } + const lc = S.lcol; + // The loupe lights up as the pen passes its point. + const lb = 0.65 + 0.35 * exp(-age(S.t0) / tau); + const xmin = -1, xmax = W + 1, ymin = LY0 - 1, ymax = H + 1; + for (let g = 0; g < ng; g++) { + const o = g * 5; + if (LCX + lbox[o + 1] * kz < xmin || LCX + lbox[o] * kz > xmax || LCY - lbox[o + 2] * kz < ymin || LCY - lbox[o + 3] * kz > ymax) continue; + const a = lg[g], e = lg[g + 1], gap = (lbox[o + 4] * kz) / (e - a); + const step = gap >= 0.5 ? 1 : min(64, floor(0.5 / gap)); + for (let i = a; i < e; i += step) { + const j = min(i + step, e) % n; + let x0 = LCX + lx[i] * kz, y0 = LCY - ly[i] * kz, x1 = LCX + lx[j] * kz, y1 = LCY - ly[j] * kz; + if ((x0 < xmin && x1 < xmin) || (x0 > xmax && x1 > xmax) || (y0 < ymin && y1 < ymin) || (y0 > ymax && y1 > ymax)) continue; + let dx = x1 - x0, dy = y1 - y0, len = sqrt(dx * dx + dy * dy); + if (len > 40) { + // Clip a long segment to the loupe (Liang-Barsky). + let u0 = 0, u1 = 1, ok = true; + const P4 = [-dx, dx, -dy, dy], Q4 = [x0 - xmin, xmax - x0, y0 - ymin, ymax - y0]; + for (let c = 0; c < 4 && ok; c++) { + const pp = P4[c], qq = Q4[c]; + if (pp === 0) ok = qq >= 0; + else { + const r = qq / pp; + if (pp < 0) r > u1 ? (ok = false) : r > u0 && (u0 = r); + else r < u0 ? (ok = false) : r < u1 && (u1 = r); + } + } + if (!ok) continue; + const ax = x0 + dx * u0, ay = y0 + dy * u0; + x1 = x0 + dx * u1; + y1 = y0 + dy * u1; + x0 = ax; + y0 = ay; + dx = x1 - x0; + dy = y1 - y0; + len = sqrt(dx * dx + dy * dy); + } + const m = max(1, ceil(len)), w = len / m; + const r = lc[i * 3] * lb, gg = lc[i * 3 + 1] * lb, b = lc[i * 3 + 2] * lb; + for (let k = 0; k < m; k++) { + const u = (k + 0.5) / m; + add(S, x0 + dx * u, y0 + dy * u, w, r, gg, b); + } + } + } + + // Tone: the average colour times min(1, D)^0.6. + for (let p = 0; p < NP; p++) { + const dd = D[p]; + if (dd > 0) { + const f = min(1, dd) ** 0.6 / dd; + R[p] *= f; + Gc[p] *= f; + B[p] *= f; + } + } + + // Guides in the loupe: Gerver's tangent, or the corner's two arms along + // c e^(i pi/4) and -c e^(-i pi/4). + const [cr, ci] = S.c, gv = 0.18; + if (cr === 0 && ci === 0) glow(S, 0, LCY, W - 1, LCY, gv, LY0); + else { + const m = sqrt(cr * cr + ci * ci), e = 80; + for (const [ar, ai] of [[(cr - ci) / sqrt(2), (cr + ci) / sqrt(2)], [-(cr + ci) / sqrt(2), (cr - ci) / sqrt(2)]]) + glow(S, LCX, LCY, LCX + (ar / m) * e, LCY - (ai / m) * e, gv, LY0); + } + // The loupe's window on the top view, and its point. + const g0 = round(S.t0 / HG) % G, cx = TX + GX[g0] * TS, cy = TY - GY[g0] * TS; + const wx = (RHO0 / M) * TS, wy = wx * (27 / 32); + if (wx >= 1.5) { + const v = 0.45; + glow(S, cx - wx, cy - wy, cx + wx, cy - wy, v, 0); + glow(S, cx - wx, cy + wy, cx + wx, cy + wy, v, 0); + glow(S, cx - wx, cy - wy, cx - wx, cy + wy, v, 0); + glow(S, cx + wx, cy - wy, cx + wx, cy + wy, v, 0); + } + dot(S, cx, cy, 0.8); + // The pen. + { + const f = tp / HG, i = min(G - 1, floor(f)), u = f - i; + const x = TX + (GX[i] + (GX[i + 1] - GX[i]) * u) * TS, y = TY - (GY[i] + (GY[i + 1] - GY[i]) * u) * TS; + dot(S, x, y, 2.5); + for (const [ox, oy] of [[-1.2, 0], [1.2, 0], [0, -1.2], [0, 1.2]]) dot(S, x + ox, y + oy, 0.45); + } + // The loupe's top edge. + for (let x = 0; x < W; x += 2) dot(S, x, LY0 - 2, 0.22); + + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) + display.setPixel(x, y, round(min(1, R[p]) * 255), round(min(1, Gc[p]) * 255), round(min(1, B[p]) * 255)); +} From 93c4ed6582cc85196a9e5b8a8eb9ed3e5993c7b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 15:53:52 +0000 Subject: [PATCH 7/8] =?UTF-8?q?README=20=EC=97=90=2062=ED=8C=90=20?= =?UTF-8?q?=ED=91=9C=EC=99=80=20=EC=97=B0=EA=B5=AC=20=EB=85=B8=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EB=84=A3=EB=8A=94=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_01TTZyp5h85ewavQ9av2v4N1 --- incoming/pattern-study/README.md | 73 ++++++++++++++++++- .../patterns/balanced-rhythms.js | 2 +- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index ff3179db5..7ee72fa0a 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 508개를 쉰여섯 갈래로 나눠 만들었어요. 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판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 57판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 58판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 59판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 60판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 518개를 쉰여섯 갈래로 나눠 만들었어요. 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판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 57판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 58판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 59판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 60판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 62판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. [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판에서 세로로 다시 짰어요. @@ -810,6 +810,21 @@ | [Hough Transform](patterns/hough-transform.js) | 알고리즘 | 위 정사각형에 점이 하나씩 내려앉을 때마다 그 점의 사인 곡선이 아래 누산기로 왼쪽부터 쓸려 들어가요. 한 직선 위 점들의 곡선이 모두 만나는 봉우리를 골라 그 직선을 위에 같은 색으로 다시 그어요. | | [Jordan Curve](patterns/jordan-curve.js) | 위상·매듭 | 패널을 채운 구불구불한 닫힌 곡선 위로 탐침이 돌아다니며 패널 끝까지 반직선을 쏘고, 곡선을 건넌 곳에 금색 점을 찍어 세요. 그 수가 홀수면 안의 색, 짝수면 밖의 색이 탐침에서부터 번져 안과 밖이 맞물린 두 땅으로 드러나요. | +### 62판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Abrikosov Lattice](patterns/abrikosov-lattice.js) | 물리·스프링 | 빠르게 도는 보스-아인슈타인 응축체가 세운 패널을 채우고, 위상이 색이고 밀도가 밝기라서 양자 소용돌이마다 모든 색이 한 번씩 감기는 검은 심이 돼요. 심들은 삼각 격자로 서서 유체와 함께 돌고, Rotation 을 올리면 간격이 좁아지며 어두운 가장자리에서 새 소용돌이가 들어와요. | +| [Void and Cluster](patterns/void-and-cluster.js) | 노이즈·흐름 | 백색 잡음으로 뿌린 점 1,516개가 가장 빽빽한 뭉치에서 가장 큰 빈자리로 하나씩 옮겨 가며 고르게 퍼져요. 다 퍼진 점에는 순위가 매겨지고, 문턱이 내려갔다 오르면 점이 순위대로 떠났다 돌아와요. | +| [Feynman Checkerboard](patterns/feynman-checkerboard.js) | 물결·간섭 | 시간이 아래로, 공간이 가로로 놓인 빛 원뿔 격자에서 모퉁이마다 진폭에 iε 를 곱하는 지그재그 길 넷이 원천에서 떨어지며 모퉁이에 i^R 의 네 색 점을 찍어요. 그 밑에는 모든 길을 더한 장이 위상은 색, 크기는 밝기로 빛나고, 금빛 평균 위치가 지터베베궁으로 좌우로 떨려요. | +| [Busy Beaver](patterns/busy-beaver.js) | 알고리즘 | 튜링 기계가 테이프를 가로로, 시간을 아래로 써요. 다섯 상태 챔피언의 47,176,870 걸음이 1분 동안 뒤로 물러나는 그림 속에 고리 하나에 삼각형 하나씩 쌓이고, 머리가 꺾인 자리가 금색과 장밋빛 빗변을 그려요. | +| [Veech Octagon](patterns/veech-octagon.js) | 위상·매듭 | 위의 정팔각형과 아래의 이중 정오각형 위를 직선 넷씩이 같은 방향으로 달리다가, 한 변으로 나가면 그 변에 붙은 변으로 다시 들어와요. 대부분의 방향에서는 선이 면을 고르게 채우고, K3 이 주기 방향에 닿으면 선이 닫히며 면이 하늘색과 보라 원기둥 둘로 갈라져요. | +| [Riemann's Function](patterns/riemann-function.js) | 프랙탈 | 리만의 미분 불가능 함수를 복소평면에 옮긴 곡선 φ(t) 가 펜 끝에서 나선 꽃이 달린 하트로 그려지고, 아래 확대경이 그 위의 유리수 한 점으로 파고들어요. p 와 q 가 다 홀수인 점에서는 곡선이 작은 장미꼴을 줄줄이 달고 가로로 곧게 지나가고, 다른 점에서는 직각으로 꺾여요. | +| [Heesch Tiles](patterns/heesch-tiles.js) | 보로노이·셀 | 가장자리가 쉬지 않고 휘는 타일 하나가 틈도 겹침도 없이 패널을 덮어요. 짝지은 변은 헤슈가 붙인 이름 T, C, G, C3, C4 대로 서로 옮겨 붙고, 8초마다 다음 형이 위에서부터 한 장씩 덮어 오며, 돌거나 뒤집힌 복사본은 색이 달라요. | +| [Yard-Sale Model](patterns/yard-sale.js) | 데이터 그림 | 공정한 동전으로 거래하는 64명의 재산이 위의 로렌츠 곡선을 대각선에서 처지게 하고, 정렬한 막대를 한 사람에게 몰아요. 맨 아래의 지니 계수는 1 을 향해 오르다가 K4 로 세금을 올리면 그 아래에서 멈춰요. | +| [Berggren Tree](patterns/berggren-tree.js) | 수론 | 피타고라스 세 수가 (3, 4, 5) 에서 한 세대씩 자라요. A 와 C 가지는 c − b = k² 과 c − a = 2n² 포물선을 따라 하늘색과 장밋빛으로 뻗고, 원시 세 수는 두 포물선이 만나는 마디에만 켜져요. | +| [Balanced Rhythms](patterns/balanced-rhythms.js) | 리듬·펄스 | 재생 헤드가 위의 걸음 고리를 돌며 친 걸음마다 중심에서 그 단위 벡터를 긋고, 아래에서는 같은 벡터를 꼬리에 머리를 이어 붙인 걸음이 마디 끝마다 출발점으로 돌아와요. 첫 리듬은 오각형과 삼각형에서 둘을 잇는 이각형을 뺀 여섯 걸음이라, 어느 정다각형의 합집합도 아닌데 걸음이 닫혀요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -3148,6 +3163,50 @@ E8 의 근 240개를 고셋의 4₂₁ 다면체 꼭짓점으로 그렸어요. 주제 5 로 조르당 곡선 정리를 위상·매듭 갈래에 세웠고, 겨눈 지도 칸은 없어요. 카미유 조르당(1887)은 평면의 단순 닫힌 곡선이 평면을 안과 밖 꼭 둘로 나눈다고 했고, 오즈월드 베블런(1905)의 증명이 처음으로 온전하다고 받아들여졌어요. 곡선이 아주 구불구불하면 눈으로는 어느 쪽이 안인지 모르지만, 그 점에서 가장자리까지 그은 반직선이 곡선을 홀수 번 건너면 안이에요. M. 심랏(1962)이 다각형 안의 점을 가리는 알고리즘으로 낸 셈이에요. 패널은 간격 5.5 LED 로 흔든 삼각 격자의 칸마다 값을 두고, 삼각형마다 선형으로 이은 함수의 0 집합을 곡선으로 그려서 곡선이 제 몸에 닿을 수가 없어요. 칸은 이웃 여섯이 안과 밖 한 줄씩으로 나뉠 때만 곡선 길이에 무게를 둔 메트로폴리스 걸음으로 편을 바꿔서, 720 번 센 칸의 연결 성분은 매번 안 하나, 밖 하나였어요. 처음 판은 탐침의 정확한 자리에서 반직선을 쏘고 반올림한 LED 에서 칠을 시작해서, 240 프레임 가운데 12 프레임에서 둘이 어긋났고 그때마다 밖의 큰 덩어리가 안의 색으로 칠해졌어요. 반직선을 LED 한가운데에서 쏘자 3600 프레임 모두 홀짝과 칠이 맞았고, 반직선을 패널의 먼 끝으로 겨누자 교차가 평균 3.7 번에서 8.0 번으로 늘었어요. 잰 값은 움직임 0.361, 촘촘함 0.064, 가장자리 0.530, 복잡도 1.077, 채움 1.000, 색 0.359, 모양 바뀜 0.079 이고, 곡선 길이는 처음 노브에서 1220 LED 쯤이에요. 채움은 축의 위 끝 1 과 같아요. 뜻밖이었던 것은 칸 하나가 편을 바꾸는 0.7초 사이에 LED 로 센 안이 두 조각이 되기도 해서, 720 번 가운데 9 번은 1 LED 보다 가는 목 너머로 칠이 이웃에서 번져 와야 했다는 거예요. +## 62판 + +62판도 열 개 모두 주제 5 「수학 · 추상」이에요. 같이 돈 61판(#278)이 PR 본문에 밝힌 갈래와 수학, 지도 칸 둘을 읽고 피했고, 앞 바퀴 PR 63개에는 댓글도 리뷰도 없어서 따를 글이 없었어요. dev 의 508개에 61판이 고른 열 갈래를 하나씩 더해 세면, 추상 갈래 열다섯 가운데 노이즈·흐름과 입자, 리듬·펄스, 보로노이·셀, 데이터 그림, 프랙탈, 수론, 위상·매듭이 열여덟이고 나머지 일곱이 열아홉이에요. 열여덟인 갈래 가운데 입자를 뺀 일곱에 하나씩 넣고, 남은 셋은 열아홉인 물결·간섭과 물리·스프링, 알고리즘에 넣었어요. 입자에는 원판의 무작위 순차 흡착을 두려다, 그림이 Rényi Parking 과 원판을 쌓는 그림들에 가까워서 접었어요. 리듬에 두려던 피츠휴-나구모 신경 세포의 일관성 공명도, 61판의 Canard Explosion 이 같은 꼴의 반 데르 폴 진동자를 써서 접었어요. 주제 1 은 508개에 빈 칸도 둘뿐인 칸도 없어서, 셋뿐인 칸 스물셋 가운데 61판이 고른 움직임 × 모양 바뀜 1,4 와 채움 × 모양 바뀜 1,4 를 빼고 가장자리 × 색 1,4 에 Abrikosov Lattice 를, 촘촘함 × 채움 4,1 에 Void and Cluster 를 세웠어요. 주제 4 는 Feynman Checkerboard 와 Busy Beaver 예요. 열 개 다 「흔한 것」에도 「낯선 수학 예」에도 없어요. Busy Beaver 와 Feynman Checkerboard 는 격자 위의 시공간 그림이지만, 칸마다 같은 규칙을 한꺼번에 쓰는 셀룰러 오토마타가 아니에요. 앞의 것은 머리 하나가 한 칸씩 읽고 쓰는 튜링 기계이고, 뒤의 것은 복소 진폭을 더하는 선형 점화식이라서 흔한 것과 다르다고 봤어요. 열 개는 일꾼 다섯이 레포 밖 worktree 에서 둘씩 짓고, 접촉 시트(처음 노브를 시간에 따라 여섯 장, 노브마다 다섯 자리)로 하나씩 본 뒤 합쳤어요. + +### Abrikosov Lattice + +주제 1 과 5 로 아브리코소프의 소용돌이 격자를 물리·스프링 갈래에 세우고, 패턴이 셋뿐이던 가장자리 × 색 1,4 칸을 겨눴어요. 가장자리가 0.210 아래이면서 색이 0.545 를 넘는 칸이에요. 알렉세이 아브리코소프(1957)는 제2종 초전도체에 든 자기 선속이 양자화된 소용돌이의 격자로 선다고 셈했고, W. H. 클라이너, L. M. 로스, S. H. 오틀러(1964)가 그 격자가 삼각형이라는 것을 보였어요. 같은 격자가 도는 보스-아인슈타인 응축체에서 K. W. 매디슨과 동료들(2000), J. R. 아보샤에르와 동료들(2001)의 사진에 찍혔고, 틴룬 호(2001)는 아주 빠르게 도는 응축체를 가장 낮은 란다우 준위의 Π(z − z_k) e^(−|z|²/2) 로 썼어요. 패널은 아브리코소프의 급수 Σ C_n e^(iqny) e^(−(x − qn)²/2) 를 한 칸만 표로 지어 두고 자기 병진으로 어디서든 읽어요. 여기에 e^(−ixy/2) 를 곱한 대칭 게이지의 위상이 실험실에서 본 응축체의 위상이라 그대로 색으로 칠하고, 세로로 긴 덩이 모양을 곱한 크기를 밝기로 써요. 리처드 파인먼(1955)의 규칙대로 소용돌이 밀도가 2mΩ/h 라서 Rotation 을 0.3 에서 0.95 로 올리면 간격이 25 LED 에서 14 LED 로 줄어요. 첫 판은 간격 21 LED 에 밝기를 |ψ| 그대로 써서 가장자리 0.000, 색 0.585 였는데, 어두워 보이는 파란 바큇살이 심처럼 읽혔어요. 간격을 25 LED 로 넓히고 밝기를 |ψ|^1.25 로 하자 심은 간격의 0.4 인 10 LED 원판이 됐어요. 파랑은 채도를 0.35 덜어 연보라로 바꿨고, 밝은 곳을 하얗게 바래 격자를 드러내려던 판은 색이 0.490 으로 칸 밖에 떨어져서 되돌렸어요. 이웃 LED 의 밝기 차이는 가장 커도 28/255 예요. 잰 값은 움직임 0.111, 촘촘함 0.031, 가장자리 0.000, 복잡도 2.452, 채움 0.873, 색 0.562, 모양 바뀜 0.124 예요. 뜻밖이었던 것은 위상이 심 하나하나보다 덩이 전체를 따라 감긴다는 거예요. 어떤 둘레를 돌든 그 안의 소용돌이 수만큼 색이 돌아서 바큇살이 가장자리로 갈수록 늘고, 패널 테두리를 한 바퀴 돌면 색이 열다섯에서 열일곱 번 돌아요. + +### Void and Cluster + +주제 1 과 5 로 울리크니의 빈자리와 뭉치 방법을 노이즈·흐름 갈래에 세우고, 패턴이 셋뿐이던 촘촘함 × 채움 4,1 칸을 겨눴어요. 촘촘함이 0.297 을 넘으면서 채움이 0.302 아래인 칸이에요. 로버트 울리크니(1988)는 낮은 주파수의 힘이 빠진 점 무늬를 청색 잡음이라 불렀고, 1993년에는 그런 무늬로 디더 행렬을 짓는 빈자리와 뭉치 방법을 냈어요. 점 무늬를 가우스 필터로 걸러서 값이 가장 큰 점을 빼고 값이 가장 작은 빈 LED 에 넣기를 되풀이하다가, 뺀 자리가 곧 가장 큰 빈자리가 되면 멈춰요. 패널은 64×128 을 위아래와 좌우가 이어진 원환면으로 두고 점 1,516개를 백색 잡음으로 뿌리며, 뭉친 점은 옅은 분홍으로, 걸러진 값이 평균의 절반 아래인 빈자리는 흐린 라벤더빛으로 칠해요. 점은 431~464번 옮겨 2.6초에 멈추고, 그 무늬에서 뭉치를 하나씩 빼며 아래 순위를, 빈자리를 하나씩 채우며 위 순위를 매겨요. 그 뒤 문턱이 멈춘 수의 0.45 배로 내려갔다가 1.45 배로 올라요. 처음 판은 바꾸기가 두 배 빨라 1초 만에 멈춰서 뭉치가 풀리는 것이 거의 보이지 않았고, 순위 색이 파랑과 남색이라 패널에서 어두웠어요. 백색 잡음을 0.5초 세워 두고 바꾸기가 1초에 36번에서 243번으로 오르게 했고, 순위 색은 옅은 하늘색에서 보라를 지나 분홍으로 바꿨어요. 그때 촘촘함 0.306 이 칸 경계 0.297 에 가까워서, 점의 몫을 0.17 에서 0.185 로 올리자 촘촘함 0.331, 채움 0.177 이 됐어요. 나머지는 움직임 0.126, 가장자리 0.998, 복잡도 0.477, 색 0.209, 모양 바뀜 0.188 이고 노브는 0.11, 0.11, 0.20, 0.11 이에요. 이웃과 떨어진 밝은 LED 는 이웃 넷과 다 달라서 촘촘함이 채움의 두 배에 가깝고, 같은 몫의 백색 잡음은 붙은 점 때문에 2g(1 − g) 인 0.30 에 머물러요. 뜻밖이었던 것은 Sigma 노브였어요. 필터를 4 LED 로 넓히면 바꾸기가 448번이 아니라 187번 만에 멈추고, 멈춘 무늬가 √g 의 절반 아래 주파수에 백색 잡음 힘의 83% 를 남겨서 작은 뭉치가 그대로 있어요. 점 사이 간격이 2.3 LED 인데 필터가 그보다 훨씬 넓으면 큰 빈자리만 보이고 붙어 있는 두 점은 보이지 않아요. + +### Feynman Checkerboard + +주제 4 와 5 로 파인먼의 체커보드를 물결·간섭 갈래에 세웠어요. 리처드 파인먼은 1+1 차원 입자가 한 걸음마다 빛의 속도로 한 칸씩 오른쪽이나 왼쪽으로 가고 꺾일 때마다 진폭에 iε 를 곱하면, 모든 길의 합이 디랙 방정식의 전파 인자가 된다고 보았어요. 앨버트 힙스와 낸 『Quantum Mechanics and Path Integrals』(1965)의 문제 2-6 이고, 테드 제이컵슨과 로런스 슐먼(1984)이 연속 극한을 증명했어요. 패널은 한 줄이 한 걸음, 가로 한 LED 가 두 칸이고, 빛이 128걸음에 가는 거리보다 긴 384칸 줄 위에서 두 성분 점화식을 돌려서 패널의 128줄은 자유 전파 인자 그대로예요. 원천은 오른쪽으로 가는 성분에 놓은 가우스 덩이로, 폭 17칸이 콤프턴 길이 1/ε 의 1.4배예요. 그 위로 진폭의 크기 ε^R 을 무게로 뽑은 길 넷이 초당 32줄씩 내려가며 모퉁이마다 i^R 의 네 색 점을 찍어요. 금빛 평균 위치는 빛 원뿔을 따라 떠났다가 39.4줄 주기로 3~6 LED 를 오가는데, 에르빈 슈뢰딩거(1930)가 짚은 지터베베궁이에요. 첫 판은 덩이를 콤프턴 길이의 2.4배로 넓혀서 무지개 띠가 패널을 다 채웠고, 길과 평균이 그 위에 묻혔어요. 덩이를 1.5배로 좁히고 장을 절반 밝기로 낮췄어요. 선 밑을 어둡게 깔자 길과 평균이 무지개 위에서 읽혔어요. 길을 엇갈려 떨어뜨리던 판은 Speed 반응이 0.03 이어서, 길 넷이 「지금」 줄 하나를 같이 내려가며 지난 줄을 밝히게 바꾸자 0.08 이 됐어요. 잰 값은 움직임 0.106, 촘촘함 0.050, 가장자리 0.617, 복잡도 2.482, 채움 0.959, 색 0.286, 모양 바뀜 0.106, thirds 90/97/99 예요. 뜻밖이었던 것은 길과 합이 다른 곳에 산다는 거예요. Focus 를 1 로 둔 점 원천에서 뽑은 길은 13.5걸음마다 꺾이며 가운데를 맴도는데, 위상까지 더한 합은 원뿔 두 변에 몰려서 원뿔이 패널 옆에 닿는 64째 줄 오른쪽 변 한 칸의 확률 0.66 을 한 번도 꺾이지 않은 길 하나가 실어요. + +### Busy Beaver + +주제 4 와 5 로 바쁜 비버를 알고리즘 갈래에 세웠어요. 티보르 라도(1962)는 빈 테이프에서 시작해 서는 n 상태 2 기호 튜링 기계 가운데 가장 오래 도는 것을 물었고, 셴 린과 라도(1965)가 S(3) = 21 을, 앨런 브래디(1983)가 S(4) = 107 을 셈했어요. 하이너 마크센과 위르겐 분트로크(1989)가 찾은 다섯 상태 챔피언은 47,176,870 걸음 만에 서고, 그보다 오래 돌다 서는 기계가 없다는 것은 bbchallenge(2024)가 증명했어요. 패널은 테이프를 가로로, 시간을 아래로 두고 한 줄이 머리가 꺾이는 한 번을 세게 해서, 챔피언의 꺾임 12,258 번을 128줄에 다 담아요. 테이프 창은 늘 지금까지 쓴 칸 전체이고 LED 하나는 그 64분의 1 에 든 1 의 몫이라, 첫 걸음부터 지금까지가 한 장에 남은 채 그림이 뒤로 물러나요. 챔피언은 1 이 n 개 깔린 테이프를 100 100 100 줄무늬로 바꾼 뒤 왼쪽 끝에서 1 을 다시 쌓는 고리를 돌아서, 고리 한 번이 옅은 삼각형 하나가 돼요. 고리를 시작할 때의 1 은 n 이 3 의 배수면 (5n + 12)/3, 3 으로 나눠 1 이 남으면 (5n + 16)/3 이 되어 3, 9, 19, 37 을 지나 12,287 까지 자라고, 파스칼 미셸(1993)이 콜라츠 꼴이라고 부른 이 셈은 나머지 2 가 처음 나오는 12,287 에서 멈춰요. 처음 판은 꺾인 자리를 LED 에 조금씩 더해 찍어서 옅은 테이프 위에서 보이지 않았고, 줄무늬는 LED 와 어긋나 물결무늬로 끓었어요. 꺾인 자리를 상태 색으로 덮고 같은 쪽의 두 번 앞 자리와 잇자 A 의 금색과 D 의 장밋빛이 삼각형의 두 빗변을 그렸어요. LED 마다 3 칸과 스냅숏 블록의 공배수만큼 평균하자 줄무늬는 1 이 꼭 3분의 1 인 짙은 파랑으로 고르게 칠해졌어요. 잰 값은 움직임 0.176, 촘촘함 0.032, 채움 0.552, 색 0.253, thirds 24/55/86 이에요. Speed 2 에서 꺾임은 1초에 9.4% 씩 늘다가 45.7초부터 한 프레임 8만 걸음에 막혀 기계를 기다리고, 62.2초에 서요. Machine 노브의 1~3 은 네 상태와 셋, 둘 상태 기계를 한 걸음 한 줄로, 칸을 4~16 LED 블록으로 그려요. 뜻밖이었던 것은 Clock 을 1 로 돌려 걸음을 세면 삼각형이 아치로 부푼다는 거예요. 마지막 고리 하나가 4,718만 걸음 가운데 3,021만 걸음이라 패널 아래 82줄을 차지해요. + +### Veech Octagon + +주제 5 로 비치의 이분법을 위상·매듭 갈래에 세웠고, 겨눈 지도 칸은 없어요. 맞변을 붙인 정팔각형과, 정오각형 둘을 한 변으로 잇고 남은 평행한 변끼리 붙인 이중 정오각형은 둘 다 꼭짓점이 모두 각 6π 인 한 점으로 모이는 종수 2 의 평평한 곡면이에요. 랠프 폭스와 리처드 커시너(1936)는 이런 곡면의 직선 흐름이 셀 수 있는 만큼을 뺀 모든 방향에서 면 전체를 지난다고 보였고, 윌리엄 비치(1989)는 이 두 곡면에서는 방향마다 둘 가운데 하나라는 것을 보였어요. 모든 선이 닫혀 면이 닫힌 선의 원기둥으로 갈라지거나, 모든 선이 면을 고르게 채워요. 패널은 한 변을 22.8 LED 로 두고 위에 팔각형, 아래에 이중 오각형을 그려서 곡면마다 직선 넷이 K3 의 한 방향으로 달려요. 주기 방향이면 LED 마다 그 점을 지나는 닫힌 선의 길이를 셈해 짧은 원기둥을 하늘색, 긴 원기둥을 보라로 칠하고, 아니면 어두운 슬레이트 위에 지나간 선만 남겨요. 주기 방향은 측도가 0 이라, K3 이 팔각형의 45, 67.5, 90, 112.5, 135 도나 오각형의 54, 72, 90, 108, 126 도에서 1도 안에 들면 그 방향으로 붙어요. 첫 판은 곡면마다 선이 하나이고 꼬리를 길이로 정해서 닫힌 고리 위에서 머리만 움직였고, Speed 반응이 0.0094 로 검사에 걸렸으며 움직임도 0.013 으로 축 끝 0.019 아래였어요. 선을 곡면마다 넷으로 늘리고 꼬리를 초로 바꿔 Speed 가 꼬리 길이도 정하게 하자 Speed 가 0.07 이 됐어요. 오각형을 빽빽하게 메우던 크림색 빗금은 패널에서 갈색 섞인 카키로 보여서, 처음 꼬리를 13초에서 5.5초로 줄이고 선을 연한 라벤더로 바꿨어요. 마지막 값은 움직임 0.054, 촘촘함 0.077, 가장자리 0.611, 복잡도 0.847, 채움 0.561, 색 0.176, 모양 바뀜 0.014 이고, 노브는 0.10, 0.07, 0.12, 0.10 이에요. 뜻밖이었던 것은 팔각형의 두 주기 방향이 서로 다르게 갈라진다는 거예요. 변을 따라가면 가운데 띠와 사다리꼴 둘을 붙인 원기둥의 둘레를 높이로 나눈 값이 2.414 와 4.828 로 꼭 두 배이고, 대각선을 따라가면 둘 다 2 cot(π/8) = 4.828 로 같았어요. + +### Riemann's Function + +주제 5 로 리만의 미분 불가능 함수를 프랙탈 갈래에 세웠고, 겨눈 지도 칸은 없어요. 베른하르트 리만이 1861년쯤 R(t) = Σ sin(πn²t)/n² 를 어디서도 미분되지 않는 함수로 들었다고 카를 바이어슈트라스(1872)가 전했고, G. H. 하디(1916)는 무리수에서 미분되지 않는다는 것을, 조지프 거버(1970)는 p 와 q 가 다 홀수인 p/q 에서는 미분된다는 것을 보였어요. 패널은 J. J. 다위스테르마트(1991)의 복소 곡선 φ(t) = Σ e^(iπn²t)/(iπn²) 를 그리는데, 이 꼴에서 거버의 도함수는 −1/2 이고 프란시스코 데 라 오스와 루이스 베가(2014)는 이중법선 흐름에 실린 정다각형 소용돌이 실의 꼭짓점이 이 곡선을 그린다는 것을 셈으로 보였어요. 위 72줄에는 한 주기 t ∈ [0, 2] 가 한 단위 77 LED 의 하트로 서고, 펜이 8초에 한 바퀴 돌며 지나간 자리를 밝혀요. 아래 54줄의 확대경은 고른 점 t0 쪽으로 처음 노브에서 열아홉 배까지 파고들었다가 나오고, 펜이 t0 를 지날 때 가장 깊어요. 확대경은 s = t − t0 둘레를 네 배씩 얇아지는 껍질 열두 겹으로 나눠 항을 1024개까지 더하고, p/q 가까이에서는 잘린 항들이 같은 평균 계수를 갖는다는 것을 써서 꼬리를 프레넬 적분으로 메워요. 처음 판은 256항에서 자르고 꼬리를 버려서 깊이 들어가면 모서리가 반지름 1/256 쯤으로 둥글어졌는데, 꼬리를 메우자 256배에서도 모서리가 꺾였어요. 40만 항으로 따로 셈한 곡선과 견주니 t = 1 둘레에서 껍질마다 240점으로는 장미꼴이 엉뚱하게 이어지고 s = 1/512 안쪽의 장미꼴은 빠져 있어서, 안쪽 껍질을 1024항과 3100점까지 늘렸어요. 한 프레임 3.7 ms 이던 첫 판은 점을 120개씩 묶어 화면 밖 묶음을 건너뛰고 LED 보다 잘게 찍힌 묶음을 솎아 1.5 ms 안팎이 됐어요. Depth 의 반응은 0.03 이었는데, 범위를 2^4.5~2^8 배에서 2^4~2^8 배로 넓히고 확대경의 밝기를 0.42 에서 0.65 로 올리자 0.05 가 됐어요. 잰 값은 움직임 0.578, 촘촘함 0.062, 가장자리 0.646, 복잡도 0.769, 채움 0.234, 색 0.179, 모양 바뀜 0.105 예요. 뜻밖이었던 것은 t = 1 을 곧게 지나는 선이 장미꼴의 줄로 서 있다는 거예요. s 가 8s² 늘 때마다 3.1·s^1.5 너비의 장미꼴이 하나씩 놓이는데 이웃과는 4s² 떨어져 있어서, s = 1/32 에서도 네 겹 넘게 겹치고 64 LED 폭에서는 꽉 찬 나비넥타이로 보여요. + +### Heesch Tiles + +주제 5 로 헤슈의 타일 형을 보로노이·셀 갈래에 세웠고, 겨눈 지도 칸은 없어요. 하인리히 헤슈(1935)는 대칭이 없는 타일 하나로, 어느 두 복사본도 타일링의 대칭 하나로 서로 옮겨 가게 평면을 덮는 길이 28가지임을 보였어요. 오토 킨즐레와 함께 낸 『Flächenschluss』(1963)에서는 변의 짝에 이름을 붙였는데, 옮겨 붙는 T, 제 가운데를 두고 반 바퀴 도는 C, 미끄러 뒤집히는 G, 꼭짓점을 두고 3분의 1 바퀴와 4분의 1 바퀴 도는 C3 과 C4 예요. 브랑코 그륀바움과 G. C. 셰퍼드(1977)는 이를 등면 타일링 93형 안에 정리했고, 크레이그 캐플런과 데이비드 세일신(2000)은 그리기 좋게 매개변수로 풀었어요. 패널은 C3C3C3C3C3C3, TG1G1TG2G2, CCCC, TCTC, TTTT, C4C4C4C4 를 8초씩 차례로 깔아요. 짝마다 앞 변 하나만 sin 합으로 휘고 짝 변은 그 등거리 변환으로 얻으며, 복사본은 첫 장에서 변을 건너 찾고 LED 중심을 반열린 주사선으로 칠해요. 처음 판은 첫 장을 찾은 목록에 중심이 아닌 원점으로 넣어서, C3 말고 다섯 형에서 첫 장이 두 번 칠해졌고 덮임 검사가 그 자리를 하얗게 비췄어요. C3 은 V1 이 떠돌아도 V3, V5 가 120도씩 돌려 따라와 세 꼭짓점의 움직임이 서로 지워지고 중심이 원점에 그대로 있어서 걸리지 않았어요. Wiggle 을 0.6 으로 올리자 이번에는 변끼리 엇갈렸어요. 그래서 변이 현의 양 끝에서 보아 현과 타일 중심 쪽 선 사이 각의 0.9 배 넘게 벗어나지 못하게 했고, 그러면 변은 두 타일의 부채꼴 삼각형에 걸친 마름모 안에만 있어서 모서리에서만 만나요. 여섯 형과 크기 다섯에서 2만 160번 깐 판의 LED 1억 6,515만 720개가 모두 한 번씩 덮였어요. 그 한계가 처음 값부터 변의 78~93% 를 붙잡아 Wiggle 노브의 반응이 0.03~0.04 에 그쳐서, 넷째 노브를 타일 크기로 바꾸자 0.26 이 됐어요. 마지막 값은 움직임 0.203, 촘촘함 0.096, 가장자리 0.451, 복잡도 1.200, 채움 0.895, 색 0.457, 모양 바뀜 0.116 이고 노브는 0.34, 0.21, 0.26, 0.25 예요. 이 패널에서 새로 본 것은 그 한계가 형마다 다르다는 거예요. C3 육각형은 44~48도까지 휘게 두지만 모서리가 직각에서 벗어나는 CCCC, TCTC, TTTT 의 사각형은 31~38도에 묶여서, 같은 휨을 바라도 휨 크기가 육각형은 평균 0.35, 사각형은 0.24~0.27 이었어요. + +### Yard-Sale Model + +주제 5 로 마당 장터 모형을 데이터 그림 갈래에 세웠고, 겨눈 지도 칸은 없어요. 아니르반 차크라보르티(2002)의 교환 모형에서는 둘이 만나 동전을 던지고, 이긴 쪽이 가난한 쪽 재산의 f 를 가져가요. 브라이언 헤이스(2002)가 이것을 마당 장터 모형이라고 불렀어요. 거래마다 공정한데도 재산은 한쪽으로 응축해요. 브루스 보고시안과 동료들(2014, 2017)은 지니 계수가 1 로 가서 한 사람이 거의 다 가지고, 모두를 평균 쪽으로 당기는 세금 χ 를 넣으면 1 아래의 값에서 멈춘다는 것을 보였어요. 패널 위쪽 64줄은 로렌츠 곡선과 대각선이고, 그 아래 35줄에 정렬한 재산을 로그 눈금 막대로 세우고 맨 아래 24줄에 지니 계수의 자취를 그려요. 재산은 한 사람에 1000 단위인 정수라서 거래와 세금의 끝수는 무작위 반올림으로 셈하고, 세금이 나누지 못한 단위는 무작위로 한 사람씩 받아요. 같은 규칙의 경제 열여섯을 나란히 돌려, 그 지니 계수가 걸친 띠를 패널 경제의 자취 뒤에 깔았어요. 첫 판은 1초에 100 라운드를 돌려 0.5초 만에 지니 계수가 0.54 가 됐고, 그 뒤로는 기어서 시트의 여섯 장이 거의 같은 그림이었어요. 라운드 수에 40 을 더한 값이 5.4초에 e 배가 되는 로그 시계로 바꾸고 자취도 같은 눈금에 그리자 곡선이 20초 동안 고르게 처졌지만, 2~3초에 라운드가 스물 남짓이라 세금 반응이 0.02 로 떨어졌어요. 첫 판만 14 라운드에서 열고, 곡선 아래를 0.1 밝기로 낮추고, 세금의 끝을 χ = 0.08 로 올리자 노브가 0.09, 0.05, 0.07, 0.05 가 됐어요. 마지막 값은 움직임 0.073, 촘촘함 0.033, 가장자리 0.637, 복잡도 0.129, 채움 0.396, 색 0.204, 모양 바뀜 0.071 이에요. 뜻밖이었던 것은 응축의 마지막 걸음이 가장 길다는 거예요. f = 0.2 에서 지니 계수 0.9 까지는 중앙값 643 라운드였는데, 한 사람이 90% 를 가진 경제는 30번 가운데 절반이 넘게 6000 라운드 안에 나오지 않았어요. 가장 부유한 둘이 서로 만날 확률이 라운드마다 63분의 1 쯤이라 둘의 결투가 끝나는 데 오래 걸려요. + +### Berggren Tree + +주제 5 로 베르그렌 나무를 수론 갈래에 세웠어요. 원시 피타고라스 세 수는 서로소이고 홀짝이 다른 m > n 마다 하나씩 (m² − n², 2mn, m² + n²) 이고, B. 베르그렌(1934)은 (3, 4, 5) 에 세 행렬을 거듭 곱하면 원시 세 수가 다 꼭 한 번씩 나온다는 것을 보였어요. F. J. M. 바닝(1963)과 A. 홀(1970)이 같은 나무를 다시 찾았어요. (m, n) 으로 옮겨 쓰면 A 는 m − n 을, C 는 n 을 그대로 두어서, A 가지는 c − b = (m − n)² 인 포물선 위를, C 가지는 c − a = 2n² 인 포물선 위를 달리고 B 만 둘 사이를 건너뛰어요. 패널은 홀수 변을 위로, 짝수 변을 옆으로 두고, 가지를 (m, n) 의 곧은 선분을 제곱으로 옮긴 곡선 그대로 그려요. 처음 판은 창을 고정하고 가지를 다 남겨서, B 가지가 가운데를 빗금으로 덮고 세 수의 점은 선에 묻혔어요. 창을 세대 G 에 따라 (G + 8)² 로 넓혀 가장 새 세대가 늘 가장자리에 닿게 했어요. B 가지는 자라는 동안만 옅게 그리고 점 가운데를 흰빛으로 밝히자, 포물선 두 무리의 그물과 마디마다 선 세 수가 읽혔어요. Reach 0 의 마지막 창에는 세 수 61 개가 10 세대까지 서고, m − n 이 홀수인 마디 71 개 가운데 9 (3, 4, 5) 같은 배수 10 개는 끝내 켜지지 않아요. B 가지를 지우자 Hue 반응이 0.03 으로 내려가서, 옅은 포물선 그물을 0.13 에서 0.19 밝기로 올리고 첫 나무를 세 세대 자란 채로 열었더니 0.05 가 됐어요. 잰 값은 움직임 0.612, 촘촘함 0.112, 채움 0.190, 색 0.165, 모양 바뀜 0.078 이고, 노브는 0.05, 0.13, 0.15, 0.13 이에요. 한 바퀴는 Speed 2 에서 12초 동안 자라고, 4초 동안 뿌리에서 바깥으로 빛이 한 세대씩 지나간 뒤 (3, 4, 5) 하나에서 다시 자라요. 뜻밖이었던 것은 Root 를 1 로 돌린 (m, n) 평면이었어요. 포물선이 곧게 펴지면서 C 가지는 세로줄, A 가지는 45도 줄이 되어, 다리 평면의 휜 그물이 정수 격자를 제곱한 것일 뿐이라는 게 한눈에 보여요. + +### Balanced Rhythms + +주제 5 로 완전히 균형 잡힌 리듬을 리듬 갈래에 세웠고, 겨눈 지도 칸은 없어요. 원 위 N 걸음 가운데 친 걸음을 N 번째 단위근으로 읽어 그 합이 0 이면 리듬의 무게중심이 원의 중심에 오고, 앤드루 밀른 등(2015)이 이런 리듬을 완전히 균형 잡혔다고 부르며 음악에 썼어요. N 의 소인수가 둘 이하면 그런 집합이 다 정다각형을 겹치지 않게 모은 것이라는 것을 N. G. 더 브라윈(1953)이 보였고, T. Y. 람과 K. H. 렁(2000)은 N 번째 단위근 k 개의 합이 0 이 되는 k 가 N 의 소인수를 음 아닌 정수 배로 더한 수뿐이라는 것을 보였어요. 소인수가 셋인 30 에서는 오각형 {0, 6, 12, 18, 24} 와 삼각형 {5, 15, 25} 를 더하고 둘을 잇는 이각형 {0, 15} 를 빼면, 어느 정다각형의 합집합도 아닌 여섯 걸음 {5, 6, 12, 18, 24, 25} 가 남아요. 패널은 위 64줄에 반지름 27 의 걸음 고리를 두고, 재생 헤드가 친 걸음마다 중심에서 그 벡터를 긋고 아래에서는 같은 벡터를 꼬리에 머리를 이어 붙여요. 변의 방향이 친 차례대로 시계 방향으로 돌아서 닫힌 걸음은 늘 볼록다각형이고, 첫 리듬에서는 오각형 걸음의 위로 향한 변 하나가 위에서 ±60° 기운 두 변으로 바뀌어, 출발점이 뾰족한 육각형이 돼요. 처음 판은 걸음을 가는 선과 거의 보이지 않는 채움으로 그려서 움직임이 0.039, Speed 반응이 0.02 였어요. 변을 두 LED 로 굵히고 마디 동안 쓸고 지나간 쐐기를 변의 색으로 칠하고 한 마디를 4초에서 3초로 줄이자 움직임 0.150, Speed 반응 0.04 가 됐어요. 쐐기 둘이 맞닿은 변 위의 LED 가 두 번 칠해져 밝은 줄이 남던 것은 픽셀 중심을 아주 조금 비켜 재서 지웠고, 칠을 행마다 구간을 셈하게 바꾸자 한 프레임이 1.7 ms 에서 1.2 ms 가 됐어요. 잰 값은 움직임 0.150, 촘촘함 0.043, 가장자리 0.519, 복잡도 0.568, 채움 0.253, 색 0.124, 모양 바뀜 0.066 이에요. 뜻밖이었던 것은 손 클라베였어요. 16걸음의 {0, 3, 6, 10, 12} 는 균형 잡히지 않았는데도 한 마디의 걸음이 출발점에서 한 걸음의 0.082 만 비껴서, 네 마디를 이어 가도 패널에서는 거의 닫힌 오각형으로 보여요. Rotate 로 둘 쪽을 한 걸음씩 돌리면 비낌이 0.710, 1.385, 2.009, 2.556 으로 커져요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -3190,7 +3249,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 25일 `dev`(`f72355d`)의 328개에 43판부터 60판까지의 백여든 개를 더한 508개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 25일 `dev`(`f72355d`)의 328개에 43판부터 60판까지의 백여든 개와 62판의 열 개를 더한 518개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -3268,6 +3327,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 60판은 58판까지 든 488개에 빈 칸도 둘뿐인 칸도 없어서, 셋뿐인 칸 스물여섯 가운데 같이 돈 59판이 고른 움직임 × 촘촘함 1,4 와 가장자리 × 채움 1,1 을 빼고 움직임 × 채움 4,1 과 움직임 × 색 1,4 를 겨눴어요. 4,1 은 움직임이 1.529 를 넘으면서 채움이 0.302 아래인 칸이에요. 움직임은 LED 8192개의 평균이라 드문 점이 거의 다 프레임마다 자리를 옮겨야 해요. RANDU Planes 는 점 1800개가 구르는 정육면체로 움직임 2.159, 채움 0.206 이고, 겨누지 않은 Rutherford Shadow 도 꼬리 단 입자의 흐름으로 움직임 1.668, 채움 0.295 라 같은 칸에 섰어요. 1,4 는 움직임이 0.061 아래이면서 색이 0.5445 를 넘는 칸이에요. 가장 밝은 채널로 재는 움직임은 채도 높은 색끼리 바꿔 칠해도 늘지 않아서, Euler's Officers 는 판의 71% 를 채도 0.88 의 색으로 덮고 연대 하나씩만 밝히며 움직임 0.036, 색 0.625 예요. 채도를 1 로 올리면 색이 축의 위 끝 0.675 를 넘을 판이라 0.88 에 두었어요. 열 개 다 어느 축의 끝도 넘지 않아서 칸 경계는 56판 그대로예요. 59판과 60판까지 든 508개에서 지도 21장은 다 `none` 이고, 셋뿐인 칸은 59판 뒤의 스물다섯에서 스물셋으로 줄었어요. 겨눈 두 칸은 다섯과 넷이 됐어요. +62판은 60판까지 든 508개에 빈 칸도 둘뿐인 칸도 없어서, 셋뿐인 칸 스물셋 가운데 같이 돈 61판이 고른 움직임 × 모양 바뀜 1,4 와 채움 × 모양 바뀜 1,4 를 빼고 가장자리 × 색 1,4 와 촘촘함 × 채움 4,1 을 겨눴어요. 가장자리 × 색 1,4 는 이웃 차이 가운데 40 을 넘는 몫이 0.210 아래이면서 색이 0.545 를 넘는 칸이에요. 가장자리는 가장 밝은 채널로만 재서, 위상을 색상으로 칠하고 크기를 밝기로 두면 색상이 아무리 빨리 돌아도 가장자리에 들지 않아요. Abrikosov Lattice 는 소용돌이 심 둘레에서만 밝기가 0 으로 내려가는 크기 장이라 이웃 LED 차이가 가장 커도 28/255 이고, 가장자리 0.000, 색 0.562 예요. 밝은 곳을 하얗게 바래 격자를 드러내려던 판은 색이 0.490 으로 칸 밖이었어요. 촘촘함 × 채움 4,1 은 이웃 차이 평균이 0.297 을 넘으면서 채움이 0.302 아래인 칸이에요. 이웃과 닿지 않는 밝은 점은 촘촘함이 채움의 두 배쯤이라 청색 잡음이 그대로 맞았어요. Void and Cluster 는 점의 몫 0.185 로 촘촘함 0.331, 채움 0.177 이고, 같은 몫의 백색 잡음은 붙은 점 때문에 2g(1 − g) 인 0.30 에 머물러요. 열 개 다 어느 축의 끝도 넘지 않아서 칸 경계는 60판 그대로예요. 62판까지 든 518개에서 지도 21장은 다 `none` 이고, 셋뿐인 칸은 스물셋에서 열아홉으로 줄었어요. 겨눈 두 칸은 둘 다 넷이 됐어요. + ### 아직 없는 갈래 `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 가 섰어요. @@ -3400,9 +3461,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 60판에서는 Penrose Diagram 과 Rutherford Shadow 가 128줄을 써요. Penrose Diagram 은 민코프스키 시공간의 반지름 쪽 절반이 너비 π, 높이 2π 의 삼각형이라 패널 꼴 그대로이고, i⁺ 와 i⁻ 로 좁아지는 위아래 3분의 1 도 32% 와 18% 가 켜져요. 삼각형이 127줄이라 맨 아래 한 줄은 늘 꺼져 있어요. Rutherford Shadow 는 빔이 바닥에서 올라와 핵 위로 그림자를 남기며 꼭대기까지 가서 thirds 가 28/30/29 예요. 나머지 여덟도 세 3분의 1 이 다 9% 이상 켜져요. +62판에서는 Feynman Checkerboard 와 Busy Beaver 가 128줄을 써요. Feynman Checkerboard 는 한 줄이 한 걸음이라 원천에서 128걸음이 패널 높이 그대로이고, 빛이 그동안 가는 거리보다 긴 384칸 줄 위에서 셈한 장이 패널을 다 덮어서 thirds 가 90/97/99 예요. Busy Beaver 는 다섯 상태 챔피언이 머리를 꺾는 12,258 번을 128줄에 다 담아서, 첫 걸음부터 지금까지가 한 장에 남은 채 아래로 자라요. 삼각형이 오른쪽 위 꼭짓점에서 벌어지며 내려와 thirds 는 24/55/86 이에요. 나머지 여덟도 세 3분의 1 이 다 13% 이상 켜져요. + ### 낯선 수학 -주제 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)이에요. 57판도 열을 더했어요. 블룸의 중심축과 들불 변환(Medial Axis), 스메일과 윌리엄스의 솔레노이드(Solenoid), 솔로몬의 무작위 환경 속 걸음과 시나이의 걸음(Sinai's Walk), 테렐과 펜로즈의 회전(Terrell Rotation), 갈페린의 π 당구(Galperin Billiard), 페르마의 두 제곱수 정리와 자기에의 한 문장 증명(Zagier's Windmills), 스토크스 표류와 게르스트너의 트로코이드 파도(Stokes Drift), 라인하르트부터 맨, 매클라우드맨, 폰 데라우까지의 볼록 오각형 타일링(Pentagon Tilings), 페로와 하위헌스의 트랙트릭스와 자전거 바퀴 자국(Bicycle Tracks), 오스트발트 숙성과 LSW 이론(Ostwald Ripening)이에요. 58판도 열을 더했어요. 야블란과 게르데스의 거울 곡선(Mirror Curves), 가운트와 피셔, 백스터의 딱딱한 정사각형 격자 기체(Hard Squares), 다니엘 베르누이의 매달린 사슬과 베셀 함수 J0(Hanging Chain), 레일리와 테일러의 불안정성(Rayleigh-Taylor), 터키의 반평면 깊이와 라도의 중심점 정리(Tukey Depth), 롤러와 베르너의 브라운 고리 수프(Loop Soup), 카돔체프-페트비아시빌리 방정식과 코다마의 선 솔리톤 웹(KP Web), 크누스가 이름 붙인 베케트-그레이 코드(Beckett-Gray), 파스칼의 신비 육각형과 슈타이너, 커크먼의 점(Hexagrammum Mysticum), 드 스밋과 렌스트라의 판화 화랑 사상(Print Gallery)이에요. 59판도 열을 더했어요. 르장드르와 가우스의 이차 상호 법칙(Quadratic Reciprocity), 위그너 함수와 유르케, 스톨러의 켜 고양이(Wigner Cat), 아비단과 샤미르의 심 카빙(Seam Carving), 슐리히팅과 노르트마르크, 에거스의 알갱이 기체 속 맥스웰의 도깨비(Granular Demon), 클렙시의 대각 3차 곡면과 케일리, 새먼의 직선 27개(Clebsch Surface), 보데르베르크의 나선 타일링(Voderberg Tiling), 자이페르트의 곡면(Seifert Surface), 호크스의 자기 흥분 점과정(Hawkes Process), 코호넨의 자기조직 지도(Kohonen Map), 레일리의 위상자 걸음과 굿맨의 스페클 통계(Laser Speckle)예요. 60판도 열을 더했어요. 마살리아가 짚은 RANDU 의 평면(RANDU Planes), 오일러의 36 장교와 보스, 슈리칸데, 파커의 10차 그레코-라틴 방진(Euler's Officers), 펜로즈와 카터의 등각 콤팩트화 도형(Penrose Diagram), 러더퍼드 산란의 포물선 그림자(Rutherford Shadow), 오일러 나선과 프레넬 적분, 코르뉴의 회절 셈(Cornu Spiral), 리처드슨의 디바이더와 망델브로의 해안선 차원(Richardson Dividers), 에농, 홀, 페익스의 1차원 자기중력 면 모형과 린든-벨의 격렬한 이완(Phase Spiral), 에르되시와 레니의 거대 성분과 아클리옵타스, 드수자, 스펜서의 폭발적 스미기(Giant Component), 허프와 두다, 하트의 직선 찾기(Hough Transform), 조르당의 곡선 정리와 심랏의 교차 세기(Jordan Curve)예요. +주제 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)이에요. 57판도 열을 더했어요. 블룸의 중심축과 들불 변환(Medial Axis), 스메일과 윌리엄스의 솔레노이드(Solenoid), 솔로몬의 무작위 환경 속 걸음과 시나이의 걸음(Sinai's Walk), 테렐과 펜로즈의 회전(Terrell Rotation), 갈페린의 π 당구(Galperin Billiard), 페르마의 두 제곱수 정리와 자기에의 한 문장 증명(Zagier's Windmills), 스토크스 표류와 게르스트너의 트로코이드 파도(Stokes Drift), 라인하르트부터 맨, 매클라우드맨, 폰 데라우까지의 볼록 오각형 타일링(Pentagon Tilings), 페로와 하위헌스의 트랙트릭스와 자전거 바퀴 자국(Bicycle Tracks), 오스트발트 숙성과 LSW 이론(Ostwald Ripening)이에요. 58판도 열을 더했어요. 야블란과 게르데스의 거울 곡선(Mirror Curves), 가운트와 피셔, 백스터의 딱딱한 정사각형 격자 기체(Hard Squares), 다니엘 베르누이의 매달린 사슬과 베셀 함수 J0(Hanging Chain), 레일리와 테일러의 불안정성(Rayleigh-Taylor), 터키의 반평면 깊이와 라도의 중심점 정리(Tukey Depth), 롤러와 베르너의 브라운 고리 수프(Loop Soup), 카돔체프-페트비아시빌리 방정식과 코다마의 선 솔리톤 웹(KP Web), 크누스가 이름 붙인 베케트-그레이 코드(Beckett-Gray), 파스칼의 신비 육각형과 슈타이너, 커크먼의 점(Hexagrammum Mysticum), 드 스밋과 렌스트라의 판화 화랑 사상(Print Gallery)이에요. 59판도 열을 더했어요. 르장드르와 가우스의 이차 상호 법칙(Quadratic Reciprocity), 위그너 함수와 유르케, 스톨러의 켜 고양이(Wigner Cat), 아비단과 샤미르의 심 카빙(Seam Carving), 슐리히팅과 노르트마르크, 에거스의 알갱이 기체 속 맥스웰의 도깨비(Granular Demon), 클렙시의 대각 3차 곡면과 케일리, 새먼의 직선 27개(Clebsch Surface), 보데르베르크의 나선 타일링(Voderberg Tiling), 자이페르트의 곡면(Seifert Surface), 호크스의 자기 흥분 점과정(Hawkes Process), 코호넨의 자기조직 지도(Kohonen Map), 레일리의 위상자 걸음과 굿맨의 스페클 통계(Laser Speckle)예요. 60판도 열을 더했어요. 마살리아가 짚은 RANDU 의 평면(RANDU Planes), 오일러의 36 장교와 보스, 슈리칸데, 파커의 10차 그레코-라틴 방진(Euler's Officers), 펜로즈와 카터의 등각 콤팩트화 도형(Penrose Diagram), 러더퍼드 산란의 포물선 그림자(Rutherford Shadow), 오일러 나선과 프레넬 적분, 코르뉴의 회절 셈(Cornu Spiral), 리처드슨의 디바이더와 망델브로의 해안선 차원(Richardson Dividers), 에농, 홀, 페익스의 1차원 자기중력 면 모형과 린든-벨의 격렬한 이완(Phase Spiral), 에르되시와 레니의 거대 성분과 아클리옵타스, 드수자, 스펜서의 폭발적 스미기(Giant Component), 허프와 두다, 하트의 직선 찾기(Hough Transform), 조르당의 곡선 정리와 심랏의 교차 세기(Jordan Curve)예요. 62판도 열을 더했어요. 아브리코소프와 클라이너, 로스, 오틀러의 소용돌이 격자(Abrikosov Lattice), 울리크니의 청색 잡음과 빈자리와 뭉치 방법(Void and Cluster), 파인먼과 힙스의 체커보드(Feynman Checkerboard), 라도의 바쁜 비버와 마크센, 분트로크의 다섯 상태 챔피언(Busy Beaver), 폭스와 커시너, 비치의 평평한 곡면 위 직선 흐름(Veech Octagon), 리만의 미분 불가능 함수와 거버, 다위스테르마트의 복소 곡선(Riemann's Function), 헤슈와 킨즐레의 한 타일 형(Heesch Tiles), 차크라보르티와 헤이스, 보고시안의 마당 장터 모형(Yard-Sale Model), 베르그렌의 피타고라스 세 수 나무(Berggren Tree), 더브라윈과 람, 렁의 단위근 합과 밀른의 완전히 균형 잡힌 리듬(Balanced Rhythms)이에요. 목록 밖에서 다음에 해 볼 것이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -3472,6 +3535,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 60판에서 본 것이에요. 정리가 못 박는 수는 이번에도 패턴의 셈으로 지켜봤어요. RANDU Planes 는 점 8000개의 평면 번호 (9X − 6Y + Z)/2³¹ 이 다 정수였고 −5 부터 9 까지 열다섯이었어요. Euler's Officers 는 10차 방진의 100쌍이 다 한 번씩이었고, 6차는 무작위 방진 40개를 훑어도 34쌍이 끝이었어요. Rutherford Shadow 의 위치 57만 2천 개 가운데 포물선 안으로 들어간 것은 0.00003 LED 하나였고, Penrose Diagram 의 a = 1 쌍곡선은 R = π/2 의 곧은 세로줄이었어요. Giant Component 는 변 0.75n 에서 가장 큰 성분이 이론의 58.3% 에 0.1~0.8 포인트 안으로 붙었고, Cornu Spiral 의 프레넬 적분은 열 자리까지 맞았어요. 셈이 이론과 어긋난 자리도 있었어요. Richardson Dividers 의 기울기는 1 − D 보다 0.01~0.025 얕았는데, 벌림이 열 배 단위로 1.39 칸만 줄고 넓은 쪽 끝이 양 끝 거리에 막혀서예요. Hough Transform 의 봉우리는 도는 직선보다 평균 2.2도 뒤에 섰고, Jordan Curve 는 LED 로 센 안이 칸 하나가 넘어가는 0.7초 동안 두 조각이 되기도 했어요. Rutherford Shadow 의 끌림 쪽에는 처음에 기대한 모임선이 없었어요. 평면 빔에서는 판별식이 0 이 되지 않아서, 궤도는 핵 뒤에서 엇갈리기만 해요. +62판에서 본 것이에요. 정리가 못 박는 수는 이번에도 패턴의 셈으로 지켜봤어요. Busy Beaver 의 다섯 상태 챔피언은 네 번 돌려 네 번 다 47,176,870 걸음에 1 을 4098 개 남기고 섰어요. Feynman Checkerboard 의 점화식은 모퉁이 수로 센 길의 합과 1.2e-14 안에서 같았고, Abrikosov Lattice 는 2 × 2 칸 144만 개의 위상 감김이 다 0 이거나 꼭 2π 였어요. Veech Octagon 은 주기 방향마다 격자점 69만~83만 개를 흘려 보내 하나도 빠짐없이 닫혔고, Riemann's Function 의 차분몫은 t = 1 에서 s 를 4분의 1씩 줄일 때마다 거버의 −1/2 과의 차가 절반이 됐어요. Balanced Rhythms 는 Z_30 의 부분집합 2^30 개 가운데 균형 잡힌 것 146,854개를 셌고, 그 가운데 26,640개가 정다각형의 합집합이 아니었어요. Heesch Tiles 는 2만 160번 깐 판의 LED 1억 6,515만 720개가 다 꼭 한 타일에 덮였고, Berggren Tree 는 c ≤ 1,000,000 에서 세 수 159,139개로 레머의 N/2π 와 16개 차이였어요. Yard-Sale Model 의 경제 열여섯은 9만 프레임 동안 늘 64,000 단위였고, 세금이 있을 때의 정상 지니 계수는 χ/f² 하나로 정해졌어요. Void and Cluster 의 멈춘 무늬는 가장 가까운 점까지가 평균 1.69 LED 로, 같은 몫의 백색 잡음 1.32 보다 멀었어요. 처음에 기대한 것과 어긋난 자리도 둘 있었어요. 정팔각형의 원기둥은 어느 주기 방향에서나 모듈러스가 같으리라 봤는데, 대각선 방향에서만 같고 변 방향에서는 꼭 두 배였어요. 21 걸음에 서는 셋 상태 챔피언은 1 을 6 개가 아니라 5 개 남겼어요. 셋 상태 기계 16,777,216 개를 다 돌려 보니 6 개를 남기는 것은 14 걸음에 서는 다른 기계였어요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. @@ -3528,6 +3593,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 목록 밖에서 60판 뒤에 해 볼 것이에요. 10차 직교 라틴 방진은 둘까지 알려졌고 셋이 서로 직교하는 묶음은 아직 아무도 찾지 못했어요. Euler's Officers 의 정확 덮개 탐색을 셋째 방진 쪽으로 돌리면, 두 방진의 가로지르기가 몇 개까지 겹치는지가 그림이 될 만해요. 마틴 크루스칼(1960)과 죄르지 세케레시(1960)의 좌표로 슈바르츠실트 블랙홀을 끝까지 펴면, 펜로즈 도형이 지평선 둘이 엇갈려 나눈 네 구역과 위아래의 특이점 선으로 바뀌어요. Penrose Diagram 의 아크탄젠트 셈을 그대로 쓸 수 있어요. 요한 라돈(1917)의 변환은 허프 변환의 연속판이라, Hough Transform 의 누산기를 사인 곡선 대신 투영의 합으로 채우고 여과 역투영으로 위 정사각형을 되살리는 CT 그림이 될 만해요. 아르투어 쇤플리스(1906)는 조르당 곡선의 안이 원판과 같은 꼴이라는 것을 보였어요. Jordan Curve 의 안을 원판으로 펴는 등각 사상은 곡선이 구불거릴수록 가지 끝마다 원 둘레의 아주 좁은 호로 몰려요. 에르되시-레니 그래프에서 둘째로 큰 성분은 문턱 n/2 에서 가장 커졌다가 거대 성분에 먹혀 줄어들어서, Giant Component 의 그래프에 둘째 곡선을 겹치면 문턱이 봉우리로 보여요. 로버트 코베유와 로버트 맥퍼슨(1967)의 스펙트럼 검사로 선형 합동 생성기의 평면 간격을 2차원부터 6차원까지 셈하면, RANDU 가 3차원에서 갑자기 무너지는 것이 막대 하나로 보여요. +목록 밖에서 62판 뒤에 해 볼 것이에요. 격자가 단단한 판이 아니라 탄성체라서 생기는 트카첸코 물결(Vito Tkachenko, 1966)을 소용돌이 자리를 흔들어 그려 볼 만한데, I. 코딩턴, P. 엥겔스, V. 슈바이크하르트, E. 코넬(2003)이 도는 응축체에서 찍었어요. 고른 회색 하나를 오차 확산(로버트 플로이드와 루이스 스타인버그, 1976)으로 찍을 때 회색 값마다 서는 벌레 무늬를, 같은 몫의 빈자리와 뭉치 문턱과 나란히 볼 만해요. 같은 격자 위 길을 i 없이 크기만으로 더하면 시드니 골드스타인(1951)과 마크 카츠(1974)의 전신 방정식 걸음이 되니, 두 합을 나란히 그리면 위상이 확산을 빛 원뿔 쪽으로 미는 것이 보여요. 에밀 포스트(1921)의 태그 시스템은 낱말 앞에서 세 글자를 지우고 지운 첫 글자가 0 이면 00 을, 1 이면 1101 을 뒤에 붙이는데, 모든 낱말이 끝나는지 아직 아무도 몰라서 낱말을 한 줄씩 시간 아래로 쌓으면 같은 시공간 그림이 될 만해요. 커티스 맥멀런(2003)과 캐리언 칼타(2004)가 찾은 종수 2 의 L자 탁자는 정다각형이 아닌데도 변의 비가 2차 무리수이면 비치의 이분법을 따라서, 같은 닫힘과 채움을 L 모양 하나로 그릴 수 있어요. 스테판 자파르(1996)가 셈한 리만 함수의 다중프랙탈 스펙트럼, 곧 점마다 다른 횔더 지수 α 가 1/2 에서 3/4 사이에 퍼지고 지수가 α 인 점들의 차원이 4α − 2 라는 것을 t 축 위의 색으로 그려 볼 만해요. 평면을 덮지 못하는 도형을 몇 겹까지 둘러쌀 수 있는지 묻는 헤슈 수(하인리히 헤슈, 1968)를, 다섯 겹까지 둘러싸이는 케이시 맨(2004)의 도형으로 그려 볼 만해요. 장필리프 부쇼와 마르크 메자르(2000)의 부의 응축 모형은 곱으로 불어나는 재산에 이웃과 나누는 몫을 더해서, 나누는 몫이 작아지면 꼬리가 파레토 법칙에서 한 사람 몫의 응축으로 넘어가요. 안드레이 마르코프(1880)의 마르코프 세 수 x² + y² + z² = 3xyz 도 (1, 1, 1) 에서 z 를 3xy − z 로 바꾸는 비에타 뛰기로 가지를 치는 나무라서, 같은 꼴의 다리 그림에 그 나무를 올려 볼 만해요. 밀턴 배빗이 1950년대에 쓰고 데이비드 루인(1959)이 증명한 육음 정리를 리듬에 옮기면 12걸음의 여섯 걸음 리듬과 그 여집합은 사이 간격의 목록이 같아서, 둘의 걸음을 나란히 세워 볼 수 있어요. + ### 화장품 팝업 주제 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/balanced-rhythms.js b/incoming/pattern-study/patterns/balanced-rhythms.js index f7f2145be..8daa0094d 100644 --- a/incoming/pattern-study/patterns/balanced-rhythms.js +++ b/incoming/pattern-study/patterns/balanced-rhythms.js @@ -5,7 +5,7 @@ // Author: patternflow-lab // Date: 2026-10-26 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: vanishing sums of roots of unity (László Rédei, 1950s; N. G. de Bruijn, 1953; I. J. Schoenberg, 1964), the sizes a vanishing sum can have (T. Y. Lam and K. H. Leung, 2000), perfectly balanced rhythms and scales (Andrew Milne, David Bulger, Steffen Herff and William Sethares, 2015), pattern round 62 +// Lineage: vanishing sums of roots of unity (N. G. de Bruijn, 1953; I. J. Schoenberg, 1964), the sizes a vanishing sum can have (T. Y. Lam and K. H. Leung, 2000), perfectly balanced rhythms and scales (Andrew Milne, David Bulger, Steffen Herff and William Sethares, 2015), pattern round 62 // Round: 62 // =============================== // @matrix 64x128 From 8c21b62e7bcd94a754d3816f0190e9afcdfaf801 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 16:54:21 +0000 Subject: [PATCH 8/8] =?UTF-8?q?index.html=20=EC=9D=84=2062=ED=8C=90?= =?UTF-8?q?=EA=B9=8C=EC=A7=80=20528=EA=B0=9C=EB=A1=9C=20=EB=8B=A4=EC=8B=9C?= =?UTF-8?q?=20=EC=A7=93=EB=8A=94=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_01TTZyp5h85ewavQ9av2v4N1 --- 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 41fedb492..5f95f40cc 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

패턴 연구

- - + +