From f9eb5f032d8db6c8ac1e39daa47928ad866f8800 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:19:46 +0000 Subject: [PATCH 01/22] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=EC=97=B0=EA=B5=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=2075=ED=8C=90=EC=9D=B4?= =?UTF-8?q?=20=EC=84=A0=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_01Gdqi6PBtzYrhzPXCgRY4TG From df208a0ba5b08e24a085e1920fbe6ffdaf7697ff Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:41:58 +0000 Subject: [PATCH 02/22] =?UTF-8?q?Lights=20Out=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PBtzYrhzPXCgRY4TG --- incoming/pattern-study/patterns/lights-out.js | 260 ++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 incoming/pattern-study/patterns/lights-out.js diff --git a/incoming/pattern-study/patterns/lights-out.js b/incoming/pattern-study/patterns/lights-out.js new file mode 100644 index 000000000..020f8b8f9 --- /dev/null +++ b/incoming/pattern-study/patterns/lights-out.js @@ -0,0 +1,260 @@ +// ===== Patternflow pattern ===== +// Title: Lights Out +// Family: algo +// Idea: A random board of Lights Out the size of the panel is solved by light chasing: a front runs down pressing the cell below every lit cell, the lights turn into a record of presses behind it, the leftover on the bottom row is looked up as a set of presses on the top row, and a second chase runs the last lights off the bottom, leaving the record of what was pressed. +// Author: patternflow-lab +// Date: 2026-10-30 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Lights Out (Tiger Electronics, 1995), the sigma-plus game as linear algebra over GF(2) (Klaus Sutner, 1989), light chasing and the kernel (Marlow Anderson and Todd Feil, Turning Lights Out with Linear Algebra, 1998), pattern round 75 +// Round: 75 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Cells=1..4, Record=0..1 +// +// Knob 1: Hue. Turns the board round the wheel: at 0 the lights are amber, the record of presses is sky blue and a press flashes white. +// Knob 2: Speed. At 2 a board is built in 0.6 s, the first chase runs down 127 rows in 2.4 s, the bottom row is looked up in 0.7 s and the second chase takes 2.0 s, 7.7 s a board in all. At 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Cells. Cell size in LEDs: 1 is a board of 64 by 128 cells, 2 is 32 by 64, 3 is 21 by 42, 4 is 16 by 32. Every one of these boards has a kernel of 0, so any pattern of lights has exactly one solution. +// Knob 4: Record. How long a press stays on the board. At 0.06 and below the record stays until the board is done, and when the second chase ends it is the one solution, cell for cell the random presses that built the board. Turned up, the record fades, at 1 within 0.2 s, and only the lights and the flashes of the chase are left, as on the toy. + +const W = 64, H = 128, NP = W * H; +const { floor, round, min, max, exp, cos, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const WHITE = [1, 1, 1]; + +function hsv(h, s, v) { + 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); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- The game. Pressing a cell toggles it and its four neighbours. The +// board is a vector over GF(2) and a set of presses x lights A x, where A is +// the w h by w h matrix of the pluses; solving the board is solving A x = b. +function press(B, w, h, x, y) { + const i = y * w + x; + B[i] ^= 1; + if (x > 0) B[i - 1] ^= 1; + if (x < w - 1) B[i + 1] ^= 1; + if (y > 0) B[i - w] ^= 1; + if (y < h - 1) B[i + w] ^= 1; +} + +// ---- Light chasing. Pressing the cell below every lit cell of a row clears +// that row and touches nothing above it, so chasing row 0 to row h - 2 leaves +// lights only on the bottom row. Chasing is linear: the leftover is the sum +// of what each top-row press alone would leave. Column j of the chase matrix +// is the bottom row that pressing top cell j and chasing leaves; to clear a +// leftover r, press the top-row set t with C t = r and chase again. +// Worked in Node for the four boards here (64x128, 32x64, 21x42, 16x32): +// C has full rank, so the kernel of A is 0 and every board is solvable. Three +// rows less is another story: 64x125 has a kernel of dimension 36, and 5x5, +// the toy, has 2. +function chaseMatrix(w, h) { + const C = [], B = new Uint8Array(w * h); + for (let j = 0; j < w; j++) { + B.fill(0); + press(B, w, h, j, 0); + for (let y = 0; y < h - 1; y++) for (let x = 0; x < w; x++) if (B[y * w + x]) press(B, w, h, x, y + 1); + C.push(B.slice((h - 1) * w)); + } + return C; +} + +// Gaussian elimination over GF(2) for C t = r; free columns, if a board ever +// has any, are left unpressed. +function solveTop(C, r, w) { + const M = []; + for (let i = 0; i < w; i++) { + const row = new Uint8Array(w + 1); + for (let j = 0; j < w; j++) row[j] = C[j][i]; + row[w] = r[i]; + M.push(row); + } + const piv = []; + let k = 0; + for (let c = 0; c < w && k < w; c++) { + let p = -1; + for (let i = k; i < w; i++) if (M[i][c]) { p = i; break; } + if (p < 0) continue; + const tmp = M[k]; M[k] = M[p]; M[p] = tmp; + for (let i = 0; i < w; i++) if (i !== k && M[i][c]) for (let q = c; q <= w; q++) M[i][q] ^= M[k][q]; + piv.push(c); + k++; + } + const t = new Uint8Array(w); + for (let i = 0; i < piv.length; i++) t[piv[i]] = M[i][w]; + return t; +} + +// ---- One board's life, in seconds at Speed 2. +const PH = ["build", "hold", "chase1", "resid", "correct", "chase2", "solved", "fade", "dark"]; +const DUR = { build: 0.6, hold: 0.3, chase1: 2.4, resid: 0.7, correct: 0.35, chase2: 2.0, solved: 0.6, fade: 0.5, dark: 0.25 }; + +function sizeOf(S, c) { + if (!S.cache[c]) { + const w = floor(W / c), h = floor(H / c); + S.cache[c] = { c, w, h, ox: floor((W - w * c) / 2), oy: floor((H - h * c) / 2), C: chaseMatrix(w, h) }; + } + return S.cache[c]; +} + +function newBoard(S, c) { + const z = sizeOf(S, c), n = z.w * z.h; + S.z = z; + S.L = new Uint8Array(n); + S.R = new Uint8Array(n); + S.age = new Float32Array(n).fill(99); + S.fl = new Float32Array(n); + S.fk = new Uint8Array(n); + // The board is made the way the toy makes one: by pressing. Each cell is + // pressed with probability one half, so the lights are a uniform draw from + // the boards that can be solved (here all 2^(w h) of them). + const g = []; + for (let i = 0; i < n; i++) if (Math.random() < 0.5) g.push(i); + for (let i = g.length - 1; i > 0; i--) { + const j = floor(Math.random() * (i + 1)), t = g[i]; + g[i] = g[j]; + g[j] = t; + } + S.g = g; + S.gi = 0; + S.ph = 0; + S.u = 0; + S.row = 0; + S.t = null; +} + +// Chase rows up to (not including) row `to`: press below each lit cell. +function chaseTo(S, to) { + const { w, h } = S.z, L = S.L; + for (; S.row < min(to, h - 1); S.row++) { + const y = S.row; + for (let x = 0; x < w; x++) + if (L[y * w + x]) { + const i = (y + 1) * w + x; + press(L, w, h, x, y + 1); + mark(S, i); + } + } +} + +// A press goes into the record (presses are counted mod 2, so a second press +// on a cell takes the first back) and flashes: white when it adds to the +// record, rose when it cancels an earlier press. +function mark(S, i) { + S.R[i] ^= 1; + S.age[i] = 0; + S.fl[i] = 1; + S.fk[i] = S.R[i] ? 1 : 2; +} + +function enter(S, ph) { + S.ph = ph; + S.u = 0; + const { w, h } = S.z; + if (PH[ph] === "chase1" || PH[ph] === "chase2") S.row = 0; + if (PH[ph] === "resid") S.t = solveTop(S.z.C, S.L.subarray((h - 1) * w), w); + if (PH[ph] === "correct") + for (let x = 0; x < w; x++) + if (S.t[x]) { + press(S.L, w, h, x, 0); + mark(S, x); + } +} + +export function setup(params) { + params.s = { cache: {}, hue: 0, rec: 0.06, cells: 1, rgb: new Float32Array(NP * 3) }; + newBoard(params.s, 1); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 1, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.rec = cl(k[3], 0, 1); + const c = cl(round(k[2]), 1, 4); + if (c !== S.cells) { + S.cells = c; + newBoard(S, c); + } + // step is board time: seconds at Speed 2. + const step = (d * cl(k[1], 0.2, 3)) / 2, n = S.L.length, ff = exp(-step * 14); + for (let i = 0; i < n; i++) { + S.age[i] += step; + S.fl[i] *= ff; + } + S.u += step; + for (let guard = 0; guard < 16; guard++) { + const name = PH[S.ph], T = DUR[name], u = cl(S.u / T, 0, 1); + const { w, h } = S.z; + if (name === "build") { + const to = floor(S.g.length * u); + for (; S.gi < to; S.gi++) { + const i = S.g[S.gi]; + press(S.L, w, h, i % w, floor(i / w)); + S.fl[i] = max(S.fl[i], 0.55); + S.fk[i] = 0; + } + } else if (name === "chase1" || name === "chase2") chaseTo(S, floor(u * (h - 1) + 1e-9)); + if (S.u < T) break; + S.u -= T; + if (name === "dark") newBoard(S, S.cells); + else enter(S, S.ph + 1); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { c, w, h, ox, oy } = S.z, rgb = S.rgb; + const lit = hsv(0.11 + S.hue, 0.78, 0.86), rec = hsv(0.56 + S.hue, 0.72, 0.72), undo = hsv(0.94 + S.hue, 0.6, 1); + const name = PH[S.ph], u = cl(S.u / DUR[name], 0, 1); + // The fade at the end of a board, and a blink on the leftover bottom row. + const all = name === "fade" ? 1 - u : name === "dark" ? 0 : 1; + const blink = name === "resid" ? 0.35 + 0.65 * (0.5 + 0.5 * cos(u * 4 * PI)) : 1; + // Record fade per second of press age: 0 up to Record 0.06, 30 at 1. + const rate = (30 * max(0, S.rec - 0.06)) / 0.94; + rgb.fill(0); + const g = c >= 3 ? 1 : 0; + for (let j = 0; j < h; j++) + for (let i = 0; i < w; i++) { + const q = j * w + i; + let r = 0, gg = 0, b = 0; + if (S.L[q]) { + const a = j === h - 1 && name === "resid" ? blink : 1; + r = lit[0] * a; + gg = lit[1] * a; + b = lit[2] * a; + } else if (S.R[q]) { + const a = exp(-rate * S.age[q]); + r = rec[0] * a; + gg = rec[1] * a; + b = rec[2] * a; + } + const f = cl(S.fl[q], 0, 1), fc = S.fk[q] === 2 ? undo : WHITE; + r = (r + (fc[0] - r) * f) * all; + gg = (gg + (fc[1] - gg) * f) * all; + b = (b + (fc[2] - b) * f) * all; + if (r + gg + b <= 0) continue; + for (let yy = 0; yy < c - g; yy++) + for (let xx = 0; xx < c - g; xx++) { + const o = ((oy + j * c + yy) * W + ox + i * c + xx) * 3; + rgb[o] = r; + rgb[o + 1] = gg; + rgb[o + 2] = b; + } + } + // The lookup: a thin line rises from the leftover row to the top row, + // where the presses it found land. + if (name === "resid" && u > 0.45) { + const y = floor(oy + (h * c - 1) * (1 - (u - 0.45) / 0.55)); + for (let x = ox; x < ox + w * c; x++) { + const o = (y * W + x) * 3; + rgb[o] = max(rgb[o], 0.5); + rgb[o + 1] = max(rgb[o + 1], 0.5); + rgb[o + 2] = max(rgb[o + 2], 0.5); + } + } + for (let p = 0; p < NP; p++) + display.setPixel(p % W, floor(p / W), min(255, rgb[p * 3] * 255) | 0, min(255, rgb[p * 3 + 1] * 255) | 0, min(255, rgb[p * 3 + 2] * 255) | 0); +} From f5d8c730909d6fbe9fb61626256f711a711ef73e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:46:52 +0000 Subject: [PATCH 03/22] =?UTF-8?q?Coded=20Aperture=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PBtzYrhzPXCgRY4TG --- .../pattern-study/patterns/coded-aperture.js | 295 ++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 incoming/pattern-study/patterns/coded-aperture.js diff --git a/incoming/pattern-study/patterns/coded-aperture.js b/incoming/pattern-study/patterns/coded-aperture.js new file mode 100644 index 000000000..6fed38c6a --- /dev/null +++ b/incoming/pattern-study/patterns/coded-aperture.js @@ -0,0 +1,295 @@ +// ===== Patternflow pattern ===== +// Title: Coded Aperture +// Family: type +// Idea: A glowing word shines through a card punched with a modified uniformly redundant array onto a detector where photons land one by one as an unreadable jumble of overlapping shadows, and correlating that jumble with the array's decoding pattern brings the word up out of the noise as the counts pile up. +// Author: patternflow-lab +// Date: 2026-10-30 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: coded aperture imaging with a random array of pinholes (Robert Dicke, and J. G. Ables, 1968), uniformly redundant arrays (E. E. Fenimore and T. M. Cannon, 1978), modified uniformly redundant arrays from quadratic residues (S. R. Gottesman and E. E. Fenimore, 1989), pattern round 75 +// Round: 75 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Rate=0.2..3, Mask=0..2, Word=0..3 +// +// Knob 1: Hue. Turns the word and the detector round the wheel together: at 0 the word and its decoded image are pale gold and the photons on the detector cyan. +// Knob 2: Rate. Photons a second through the coded mask, 1500 per unit: 3000 at 2, 300 at 0.2, 4500 at 3. Every exposure lasts 6 s, so the rate sets how sharp the word comes up before the detector is cleared. +// Knob 3: Mask. 0 is the MURA for p = 17, 144 holes; 1 is a random card with about as many holes, as Dicke and Ables proposed, decoded the same way, which leaves ghosts; 2 is one pinhole at the centre. Each hole lets through the same light, so the pinhole gets 144 times fewer photons in the same 6 s. +// Knob 4: Word. What shines above the card: STAR, CRAB, VELA or LUX, in a 3 by 5 letter on the 17 by 17 sky. + +const W = 64, H = 128, NP = W * H; +const { floor, round, min, max, sqrt, exp, log, cos, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v) { + 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); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- Geometry. The sky, the card and the detector are all 17 by 17 +// elements, one element 3 LEDs wide and 2 tall, so each band is 51 by 34. +// From the top: the word (sky rows 5 to 11 only), the card, the detector, +// and the decoded sky. +const P = 17, C = 8, NE = P * P; +const CW = 3, CH = 2, X0 = 6, BW = P * CW, BH = P * CH; +const Y_OBJ = 0, OBJ_R0 = 5, OBJ_R1 = 11, Y_MASK = 18, Y_DET = 56, Y_DEC = 94; +const EXPO = 6, HOLD = 1.2, CYCLE = EXPO + HOLD; +// Detector background, counts per element per second, the same for every +// mask: the cosmic-ray and radioactive counts an X-ray detector sees. +const BKG = 1; +// The first measured frame comes 2 s into the run; the exposure opens 0.6 s in. +const T0 = 0.6; + +// ---- Letters, 3 by 5, on sky rows 6 to 10. +const FONT = { + S: [".##", "#..", ".#.", "..#", "##."], T: ["###", ".#.", ".#.", ".#.", ".#."], + A: [".#.", "#.#", "###", "#.#", "#.#"], R: ["##.", "#.#", "##.", "#.#", "#.#"], + C: [".##", "#..", "#..", "#..", ".##"], B: ["##.", "#.#", "##.", "#.#", "##."], + V: ["#.#", "#.#", "#.#", "#.#", ".#."], E: ["###", "#..", "##.", "#..", "###"], + L: ["#..", "#..", "#..", "#..", "###"], U: ["#.#", "#.#", "#.#", "#.#", "###"], + X: ["#.#", "#.#", ".#.", "#.#", "#.#"], +}; +const WORDS = ["STAR", "CRAB", "VELA", "LUX"]; + +function sky(word) { + const O = new Uint8Array(NE), n = word.length, c0 = floor((P - (4 * n - 1)) / 2); + for (let k = 0; k < n; k++) + for (let r = 0; r < 5; r++) for (let c = 0; c < 3; c++) if (FONT[word[k]][r][c] === "#") O[(6 + r) * P + c0 + 4 * k + c] = 1; + const lit = []; + for (let e = 0; e < NE; e++) if (O[e]) lit.push(e); + return { O, lit }; +} + +// ---- The cards, in display coordinates with the centre at (C, C). A photon +// from sky element s through hole h lands on detector element d = h - s + C +// (mod 17, each axis): the shadow of the whole card, shifted, as when the +// card is the 2 by 2 mosaic of one period. Decoding is the periodic +// correlation dec(o) = sum over d of D(d) Q(d + o - C), and for the MURA +// sum over x of M(x) Q(x + t) is 144 at t = 0 and exactly 0 everywhere else, +// so a noiseless detector decodes to 144 times the sky. Q sums to 1, so a +// flat background adds a flat offset. +function cards() { + const q = new Int8Array(P).fill(-1); + for (let x = 1; x < P; x++) q[(x * x) % P] = 1; + const mura = { M: new Uint8Array(NE), Q: new Int8Array(NE) }; + for (let i = 0; i < P; i++) + for (let j = 0; j < P; j++) { + // Gottesman and Fenimore: row 0 closed, column 0 open, else open where + // both indices are residues or both are not. + const a = i === 0 ? 0 : j === 0 ? 1 : q[i] * q[j] === 1 ? 1 : 0; + const e = ((i + C) % P) * P + ((j + C) % P); + mura.M[e] = a; + mura.Q[e] = i === 0 && j === 0 ? 1 : a ? 1 : -1; + } + // Dicke's and Ables's random card: each element open with chance 1/2, from + // a fixed hash so the card is the same every time. Decoded with 2M - 1. + const rand = { M: new Uint8Array(NE), Q: new Int8Array(NE) }; + for (let e = 0; e < NE; e++) { + let h = (e * 2654435761 + 12345) >>> 0; + h ^= h >>> 15; h = Math.imul(h, 0x2c1b3c6d) >>> 0; h ^= h >>> 12; h = Math.imul(h, 0x297a2d39) >>> 0; h ^= h >>> 15; + rand.M[e] = (h >>> 0) % 1000 < 498 ? 1 : 0; + rand.Q[e] = rand.M[e] ? 1 : -1; + } + const pin = { M: new Uint8Array(NE), Q: new Int8Array(NE) }; + pin.M[C * P + C] = 1; + pin.Q[C * P + C] = 1; + return [mura, rand, pin].map((m) => { + const holes = []; + let sq = 0; + for (let e = 0; e < NE; e++) { + if (m.M[e]) holes.push(e); + sq += m.Q[e]; + } + return { ...m, holes, sq }; + }); +} +const KMURA = 144; + +function gauss() { + const u = Math.random() || 1e-9, v = Math.random(); + return sqrt(-2 * log(u)) * cos(2 * PI * v); +} +function poisson(l) { + if (l <= 0) return 0; + if (l > 30) return max(0, round(l + sqrt(l) * gauss())); + const L = exp(-l); + let k = 0, p = Math.random(); + while (p > L) { + k++; + p *= Math.random(); + } + return k; +} + +function clear(S) { + S.D.fill(0); + S.dD.fill(0); + S.dec.fill(0); + S.cnt.fill(0); + S.nWord = 0; + S.nBkg = 0; +} + +export function setup(params) { + const S = (params.s = { + cards: cards(), mode: -1, word: -1, hue: 0, rate: 3000, t: 0, sky: null, + D: new Float32Array(NE), dD: new Float32Array(NE), dec: new Float32Array(NE), + cnt: new Float32Array(BW * BH), flash: new Float32Array(BW * BH), hole: new Float32Array(NE), spark: new Float32Array(NP), + nWord: 0, nBkg: 0, rgb: new Float32Array(NP * 3), wrap: new Int32Array(3 * P), + }); + for (let x = 0; x < 3 * P; x++) S.wrap[x] = x % P; +} + +function land(S, d, flash) { + S.D[d]++; + S.dD[d]++; + const di = floor(d / P), dj = d - di * P; + const led = (di * CH + floor(Math.random() * CH)) * BW + dj * CW + floor(Math.random() * CW); + S.cnt[led]++; + S.flash[led] = min(1.6, S.flash[led] + flash); +} + +function spark(S, x, y) { + const i = y * W + x; + S.spark[i] = min(1.2, S.spark[i] + 0.7); +} + +function expose(S, dt) { + const card = S.cards[S.mode], lit = S.sky.lit, holes = card.holes; + // Equal exposure: every hole passes the same flux, so the photon count + // scales with the number of holes. + const n = poisson((S.rate * dt * holes.length) / KMURA); + for (let k = 0; k < n; k++) { + const s = lit[floor(Math.random() * lit.length)], h = holes[floor(Math.random() * holes.length)]; + const si = floor(s / P), sj = s - si * P, hi = floor(h / P), hj = h - hi * P; + land(S, ((hi - si + C + P) % P) * P + ((hj - sj + C + P) % P), 1); + S.hole[h] = min(1, S.hole[h] + 0.5); + // One photon in three is drawn in flight, in the gaps above and below + // the card, over the element it left and the hole it went through. + if (Math.random() < 0.33) { + spark(S, X0 + sj * CW + floor(Math.random() * CW), Y_MASK - 4 + floor(Math.random() * 4)); + spark(S, X0 + hj * CW + floor(Math.random() * CW), Y_DET - 4 + floor(Math.random() * 4)); + } + } + S.nWord += n; + const b = poisson(BKG * NE * dt); + for (let k = 0; k < b; k++) land(S, floor(Math.random() * NE), 0.8); + S.nBkg += b; + // Fold this frame's counts into the decoded sky. + const Q = card.Q, wrap = S.wrap, dec = S.dec; + for (let d = 0; d < NE; d++) { + const c = S.dD[d]; + if (!c) continue; + S.dD[d] = 0; + const di = floor(d / P), dj = d - di * P; + for (let oi = 0; oi < P; oi++) { + const row = wrap[di + oi - C + P] * P, base = oi * P; + for (let oj = 0; oj < P; oj++) dec[base + oj] += c * Q[row + wrap[dj + oj - C + P]]; + } + } +} + +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.rate = 1500 * cl(k[1], 0.2, 3); + const mode = cl(round(k[2]), 0, 2), word = cl(round(k[3]), 0, 3); + if (mode !== S.mode || word !== S.word) { + if (S.mode < 0) S.t = T0; + S.mode = mode; + S.word = word; + S.sky = sky(WORDS[word]); + clear(S); + if (S.t > 0) { + // Open on an exposure already under way: replay the time since it began. + const was = S.t; + S.t = 0; + while (S.t < was) { + expose(S, min(0.1, was - S.t)); + S.t += 0.1; + } + S.t = was; + S.flash.fill(0); + } + } + const fade = exp(-d / 0.12), hf = exp(-d / 0.08); + for (let i = 0; i < S.flash.length; i++) S.flash[i] *= fade; + for (let i = 0; i < NE; i++) S.hole[i] *= hf; + for (let i = 0; i < NP; i++) S.spark[i] *= hf; + S.t += d; + if (S.t >= CYCLE) { + S.t -= CYCLE; + clear(S); + } + if (S.t < EXPO) expose(S, min(d, EXPO - S.t + d)); +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.sky) return; + const rgb = S.rgb, card = S.cards[S.mode], O = S.sky.O; + const gold = hsv(0.12 + S.hue, 0.38, 1), cyan = hsv(0.52 + S.hue, 0.55, 1), night = hsv(0.62 + S.hue, 0.5, 0.035); + rgb.fill(0); + const put = (x, y, c, a) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + rgb[o] += c[0] * a; + rgb[o + 1] += c[1] * a; + rgb[o + 2] += c[2] * a; + }; + // The word, with a soft glow round it. + for (let r = OBJ_R0; r <= OBJ_R1; r++) + for (let j = 0; j < P; j++) + for (let y = 0; y < CH; y++) + for (let x = 0; x < CW; x++) { + const px = X0 + j * CW + x, py = Y_OBJ + (r - OBJ_R0) * CH + y; + let g = 0; + for (let dr = -1; dr <= 1; dr++) + for (let dj = -1; dj <= 1; dj++) { + const rr = r + dr, jj = j + dj; + if (rr >= 0 && rr < P && jj >= 0 && jj < P && O[rr * P + jj]) g += dr || dj ? 0.05 : 0; + } + put(px, py, O[r * P + j] ? gold : night, O[r * P + j] ? 0.95 : 1); + put(px, py, gold, g); + } + // The card: dark, the holes lit from above, flickering as photons pass. + const plate = hsv(0.6 + S.hue, 0.25, 0.05), lightC = hsv(0.12 + S.hue, 0.3, 1); + for (let e = 0; e < NE; e++) { + const i = floor(e / P), j = e - i * P; + for (let y = 0; y < CH; y++) + for (let x = 0; x < CW; x++) { + const px = X0 + j * CW + x, py = Y_MASK + i * CH + y; + if (card.M[e]) put(px, py, lightC, (y === 0 ? 0.42 : 0.3) + 0.35 * S.hole[e]); + else put(px, py, plate, 1); + } + } + // The detector: counts per LED, and each new photon a flash. + // Linear in counts, so the overlapping shadows show through the grain: + // the mean LED reaches 0.4 at the end of an exposure through the MURA. + const ref = (S.rate * EXPO + BKG * NE * EXPO) / (BW * BH); + for (let y = 0; y < BH; y++) + for (let x = 0; x < BW; x++) { + const i = y * BW + x, v = (S.cnt[i] / ref) * 0.4, f = S.flash[i]; + put(X0 + x, Y_DET + y, cyan, v); + put(X0 + x, Y_DET + y, [1, 1, 1], 0.5 * f); + } + // The decoded sky: counts per sky element, the background's flat offset + // taken off, stretched so the word reaches 1 at the end of an exposure. + const full = (S.rate * EXPO * card.holes.length) / KMURA / S.sky.lit.length; + const off = (S.nBkg / NE) * card.sq; + for (let e = 0; e < NE; e++) { + const i = floor(e / P), j = e - i * P; + const v = cl((S.dec[e] - off) / full, 0, 1.3); + for (let y = 0; y < CH; y++) + for (let x = 0; x < CW; x++) { + put(X0 + j * CW + x, Y_DEC + i * CH + y, night, 1); + put(X0 + j * CW + x, Y_DEC + i * CH + y, gold, 0.85 * v); + } + } + for (let p = 0; p < NP; p++) if (S.spark[p] > 0.01) put(p % W, floor(p / W), p < Y_MASK * W ? gold : lightC, 0.6 * S.spark[p]); + for (let p = 0; p < NP; p++) { + const o = p * 3; + display.setPixel(p % W, floor(p / W), min(255, rgb[o] * 255) | 0, min(255, rgb[o + 1] * 255) | 0, min(255, rgb[o + 2] * 255) | 0); + } +} From 64f8d3438385d19edea650c0978de1f418341bd5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:46:59 +0000 Subject: [PATCH 04/22] =?UTF-8?q?Mesostic=20=ED=8C=A8=ED=84=B4=EC=9D=84=20?= =?UTF-8?q?=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PBtzYrhzPXCgRY4TG --- incoming/pattern-study/patterns/mesostic.js | 330 ++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 incoming/pattern-study/patterns/mesostic.js diff --git a/incoming/pattern-study/patterns/mesostic.js b/incoming/pattern-study/patterns/mesostic.js new file mode 100644 index 000000000..3c00cd324 --- /dev/null +++ b/incoming/pattern-study/patterns/mesostic.js @@ -0,0 +1,330 @@ +// ===== Patternflow pattern ===== +// Title: Mesostic +// Family: type +// Idea: A reader slides through a stretch of Thoreau's Walden in the bottom line until the next letter of a spine word that Cage's mesostic rule allows comes to the centre, the letter locks as a gold capital, the words the rule lets stand stay as its wings and the rest fade, and the page climbs a line, so the spine word reads down the middle of the panel again and again. +// Author: patternflow-lab +// Date: 2026-10-30 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: John Cage's mesostics (from the late 1960s; Writing through Finnegans Wake, 1978) and his 50% and 100% rules, written through Henry David Thoreau's Walden (1854); letters from the MatrixLight6 pixel font of trip5's Matrix-Fonts (CC-BY), as the device's firmware carries it; pattern round 75 +// Round: 75 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Word=0..3, Rule=0..1 +// +// Knob 1: Hue. Turns the capitals and the wings round the wheel together: at 0 the capitals are gold and the wing text pale sky blue. +// Knob 2: Speed. At 2 the reader slides for 0.45 to 0.95 s depending on how much text it skips, the capital locks in 0.4 s and the page climbs a line in 0.35 s, about 1.5 s a line. At 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Word. The spine: THOREAU, WALDEN, MUREAU (Cage's 1970 piece made from Thoreau's Journal, MUsic and thoREAU) or SILENCE (Cage's 1961 book). The page opens on the first 17 lines of the mesostic; turning it leaves a blank line and the reader starts a new mesostic from where it is. +// Knob 4: Rule. Below 0.5 a 50% mesostic: between two capitals the second capital's letter does not appear. From 0.5 a 100% mesostic: the first capital's letter does not appear there either, so the wings are shorter and more letters are passed over. Turning it leaves a blank line like Word. + +const W = 64, H = 128, NP = W * H; +const { floor, round, min, max, abs, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ease = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +function hsv(h, s, v) { + 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); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- The source. Three passages of Walden (1854), from the chapter "Where +// I Lived, and What I Lived For", in the order the chapter has them, quoted +// as printed. The reader drops the punctuation, turns hyphens into spaces +// and reads in lower case: 304 words. +const WALDEN = [ + "I went to the woods because I wished to live deliberately, to front only the essential facts of life, and see if I could not learn what it had to teach, and not, when I came to die, discover that I had not lived. I did not wish to live what was not life, living is so dear; nor did I wish to practise resignation, unless it was quite necessary. I wanted to live deep and suck out all the marrow of life, to live so sturdily and Spartan-like as to put to rout all that was not life, to cut a broad swath and shave close, to drive life into a corner, and reduce it to its lowest terms, and, if it proved to be mean, why then to get the whole and genuine meanness of it, and publish its meanness to the world; or if it were sublime, to know it by experience, and be able to give a true account of it in my next excursion.", + "Our life is frittered away by detail. An honest man has hardly need to count more than his ten fingers, or in extreme cases he may add his ten toes, and lump the rest. Simplicity, simplicity, simplicity! I say, let your affairs be as two or three, and not a hundred or a thousand; instead of a million count half a dozen, and keep your accounts on your thumb-nail.", + "Time is but the stream I go a-fishing in. I drink at it; but while I drink I see the sandy bottom and detect how shallow it is. Its thin current slides away, but eternity remains. I would drink deeper; fish in the sky, whose bottom is pebbly with stars. I cannot count one. I know not the first letter of the alphabet.", +]; +const WORDS = WALDEN.join(" ").toLowerCase().replace(/-/g, " ").replace(/[^a-z ]/g, "").split(/ +/).filter((w) => w.length > 0); +const N = WORDS.length; +const mod = (g) => ((g % N) + N) % N; +const word = (g) => WORDS[mod(g)]; + +const SPINES = ["THOREAU", "WALDEN", "MUREAU", "SILENCE"]; + +// ---- Letters: MatrixLight6, the 6-row pixel font in the device's +// firmware/patternflow/src/fonts/. Each glyph is its width, its first row +// (0 for capitals and ascenders, 1 for the x-height, 2 for a's short bowl) +// and one base-32 digit per row, the high bit on the left. Capitals are 5 +// rows tall; lower case runs 4 rows from row 1 with descenders on row 5. +const B32 = "0123456789abcdefghijklmnopqrstuv"; +const UP = "3025755 3065656 3025452 3065556 3074747 3074744 4068b97 4099f99 1011111 3011152 3055655 3044447 50allhh 409db99 4069996 3065644 40699a5 3065655 3034216 3072222 3055552 3055522 50lllla 3055255 3055222 3071247"; +const LO = "32353 3044656 313443 3011353 313563 3012722 3135312 3044655 1010111 30101152 3044565 1011111 51qlll 316555 312552 3165564 3135531 316544 313436 3027221 315553 315552 51llla 315225 3155312 317127"; +const glyph = (code) => ({ w: +code[0], top: +code[1], rows: [...code.slice(2)].map((c) => B32.indexOf(c)) }); +const CAPS = UP.split(" ").map(glyph), SMALL = LO.split(" ").map(glyph); +const gOf = (ch, cap) => (cap ? CAPS : SMALL)[ch.charCodeAt(0) - 97]; +// Width of a run of text with its trailing 1 LED gap; a space adds 2 more, +// so words stand 3 LEDs apart. +function runW(s) { + let x = 0; + for (const ch of s) x += ch === " " ? 2 : gOf(ch, false).w + 1; + return x; +} +// A capital stands on the middle of the panel, x 31.5. +const capX = (w) => round(31.5 - w / 2); + +// ---- The reading strip: the whole source in one line of lower case, words +// 3 LEDs apart. wordX0 is where each word starts; the strip repeats every +// TOTAL LEDs, so the reading can run round the source. +const wordW = WORDS.map((w) => runW(w) - 1); +const wordX0 = []; +let TOTAL = 0; +for (let i = 0; i < N; i++) (wordX0.push(TOTAL), (TOTAL += wordW[i] + 3)); +const wordX = (g) => floor(g / N) * TOTAL + wordX0[mod(g)]; +function readPoint(g, c) { + const w = word(g); + let x = wordX(g); + for (let j = 0; j < c; j++) x += gOf(w[j], false).w + 1; + return x + gOf(w[c], false).w / 2; +} + +// ---- The mesostic. Lines go through the source in order, one capital +// each, the capital in its own word, and every word shown whole: a line is +// [left wing words] prefix CAPITAL suffix [right wing words]. Cage's rules +// are about the text between two capitals, the right wing of one line and +// the left wing of the next: +// 50%: the second capital's letter does not appear there; +// 100%: nor does the first capital's letter. +// Text between the two wings may be passed over, as Cage passed it over. +// A word can carry the capital only where its own prefix and suffix keep +// the rule (the suffix lies before the next capital, so it may not hold the +// next spine letter) and where both fit their half of the panel. Wings take +// whole words outward until a word holds a forbidden letter or the half is +// full; when both wings want the same words in a short gap they split it. +// Checked in Node over 3000 lines for each spine and rule: no line breaks +// its rule, and THOREAU at 50% sets 49 capitals a pass through the 304 +// words on average, 35 at 100%. +function newGen(spine, full, from) { + return { spine, full, k: 0, from, cur: null }; +} +function pick(G, k, from) { + const S = G.spine, L = S.length; + const s = S[k % L].toLowerCase(), sp = S[(k + L - 1) % L].toLowerCase(), sn = S[(k + 1) % L].toLowerCase(); + for (let g = from; g < from + 4 * N; g++) { + const w = word(g), c = w.indexOf(s); + if (c < 0) continue; + const pre = w.slice(0, c), suf = w.slice(c + 1); + if (suf.includes(sn)) continue; + if (G.full && ((k > 0 && pre.includes(sp)) || suf.includes(s))) continue; + const cw = gOf(s, true).w, cx = capX(cw); + if (runW(pre) > cx || runW(suf) - 1 > W - (cx + cw + 1)) continue; + return { g, c, k, s, cx, cw, pre, suf, left: [], right: [] }; + } + return null; +} +const holds = (w, F) => F.some((f) => w.includes(f)); +function extendLeft(line, lo, F) { + const out = []; + let txt = line.pre.length ? " " + line.pre : " "; + for (let g = line.g - 1; g >= lo; g--) { + const w = word(g); + if (holds(w, F) || runW(w + txt) > line.cx) break; + out.unshift(g); + txt = " " + w + txt; + } + return out; +} +function extendRight(line, hi, F) { + const out = []; + let txt = line.suf; + for (let g = line.g + 1; g <= hi; g++) { + const w = word(g), t = txt + " " + w; + if (holds(w, F) || runW(t) - 1 > W - (line.cx + line.cw + 1)) break; + out.push(g); + txt = t; + } + return out; +} +// The next finished line: it needs the capital after it to know where its +// right wing must stop. +function nextLine(G) { + if (!G.cur) { + G.cur = pick(G, 0, G.from); + G.cur.left = extendLeft(G.cur, G.from, [G.cur.s]); + } + const cur = G.cur, nx = pick(G, cur.k + 1, cur.g + 1); + const F = G.full ? [nx.s, cur.s] : [nx.s]; + let right = extendRight(cur, nx.g - 1, F), left = extendLeft(nx, cur.g + 1, F); + if (right.length && left.length && left[0] <= right[right.length - 1]) { + const mid = floor((left[0] + right[right.length - 1]) / 2); + right = right.filter((g) => g <= mid); + left = left.filter((g) => g > mid); + } + cur.right = right; + nx.left = left; + G.cur = nx; + return layout(cur); +} + +// Glyphs of a finished line, placed outward from the capital. +function layout(line) { + const out = [{ gl: gOf(line.s, true), x: line.cx, cap: true }]; + const lt = line.left.map(word).join(" ") + (line.left.length ? " " : "") + line.pre; + const rt = line.suf + (line.right.length ? " " : "") + line.right.map(word).join(" "); + let x = line.cx; + for (let i = lt.length - 1; i >= 0; i--) { + if (lt[i] === " ") x -= 2; + else { + const gl = gOf(lt[i], false); + x -= gl.w + 1; + out.push({ gl, x, cap: false }); + } + } + x = line.cx + line.cw + 1; + for (const ch of rt) { + if (ch === " ") x += 2; + else { + const gl = gOf(ch, false); + out.push({ gl, x, cap: false }); + x += gl.w + 1; + } + } + const lastG = line.right.length ? line.right[line.right.length - 1] : line.g; + return { glyphs: out, s: line.s, rp: readPoint(line.g, line.c), end: lastG + 1, blank: false }; +} + +// ---- Page. Eighteen slots of 7 rows (6 for the font, 1 between), rows 1 +// to 126; the bottom slot is the reading strip, the 17 above hold the +// finished lines, the newest lowest. +const PITCH = 7, SLOTS = 18; +const slotY = (j) => 1 + PITCH * j; + +function start(S) { + S.gen = newGen(SPINES[S.word], S.full, 0); + S.page = []; + for (let i = 0; i < SLOTS - 1; i++) S.page.push(nextLine(S.gen)); + S.next = nextLine(S.gen); + S.from = S.page[S.page.length - 1].rp; + S.phase = 0; + S.t = 0; +} + +export function setup(params) { + params.s = { hue: 0, speed: 2, word: 0, full: false, seen: false, page: null, rgb: new Float32Array(NP * 3) }; + start(params.s); +} + +// Phase 0 slides the strip from the last capital to the next, 1 locks the +// capital and fades the passed-over text, 2 climbs the page a line. +function phaseDur(S) { + if (S.phase === 0) return 0.45 + 0.5 * min(1, (S.next.rp - S.from) / 600); + return S.phase === 1 ? 0.4 : 0.35; +} + +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); + const wd = cl(round(k[2]), 0, 3), full = cl(k[3], 0, 1) >= 0.5; + if (!S.seen) { + // The first frame opens on the mesostic the knobs ask for. + S.seen = true; + if (wd !== S.word || full !== S.full) ((S.word = wd), (S.full = full), start(S)); + } + if (wd !== S.word || full !== S.full) { + S.word = wd; + S.full = full; + // A new mesostic from where the reading stands, after a blank line. The + // line on its way is dropped unless it is already locking. + if (S.phase === 1) S.page.push(S.next); + const last = S.page[S.page.length - 1]; + S.gen = newGen(SPINES[wd], full, last.end); + S.page.push({ blank: true, glyphs: [], rp: last.rp, end: last.end }); + if (S.page.length > SLOTS + 1) S.page.shift(); + S.next = nextLine(S.gen); + S.from = last.rp; + S.phase = 2; + S.t = 0; + } + S.t += (d * S.speed) / 2; + for (let guard = 0; guard < 8; guard++) { + const T = phaseDur(S); + if (S.t < T) break; + S.t -= T; + if (S.phase === 0) S.phase = 1; + else if (S.phase === 1) { + S.page.push(S.next); + if (S.page.length > SLOTS + 1) S.page.shift(); + S.from = S.next.rp; + S.next = nextLine(S.gen); + S.phase = 2; + } else S.phase = 0; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const rgb = S.rgb; + rgb.fill(0); + const put = (x, y, c, a) => { + if (x < 0 || y < 0 || x >= W || y >= H || a <= 0.004) return; + const o = (y * W + x) * 3; + rgb[o] = max(rgb[o], c[0] * a); + rgb[o + 1] = max(rgb[o + 1], c[1] * a); + rgb[o + 2] = max(rgb[o + 2], c[2] * a); + }; + // A glyph at x (fractional while the strip slides, split over two + // columns) with its top on row y. + const stamp = (gl, x, y, c, a) => { + const xi = floor(x), f = x - xi; + for (let r = 0; r < gl.rows.length; r++) { + const bits = gl.rows[r], yy = y + gl.top + r; + for (let i = 0; i < gl.w; i++) + if ((bits >> (gl.w - 1 - i)) & 1) { + if (f < 0.01) put(xi + i, yy, c, a); + else (put(xi + i, yy, c, a * (1 - f)), put(xi + i + 1, yy, c, a * f)); + } + } + }; + const gold = hsv(0.12 + S.hue, 0.85, 1), sky = hsv(0.56 + S.hue, 0.5, 0.88), white = [1, 0.97, 0.9]; + const T = phaseDur(S), u = cl(S.t / T, 0, 1); + const climb = S.phase === 2 ? ease(u) : 1, shift = round(PITCH * (1 - climb)); + + // ---- Finished lines. The newest is lowest; each line dims a little with + // every slot it has climbed, to 55% at the top. + const n = S.page.length; + for (let j = 0; j < n; j++) { + const line = S.page[j]; + if (line.blank) continue; + const slot = SLOTS - 2 - (n - 1 - j), y = slotY(slot) + shift; + if (y < -6 || y >= H) continue; + const age = 0.55 + 0.45 * cl(y / slotY(SLOTS - 2), 0, 1); + for (const g of line.glyphs) stamp(g.gl, g.x, y, g.cap ? gold : sky, age); + } + + // ---- The reading strip in the bottom slot. Its text is dim, brighter + // under the reading point in the middle, and the letters the spine wants + // next glint gold as they pass. + const yS = slotY(SLOTS - 1) + shift; + let rp, sa; + if (S.phase === 0) (rp = S.from + (S.next.rp - S.from) * ease(u)), (sa = 1); + else if (S.phase === 1) (rp = S.next.rp), (sa = 1 - ease(u)); + else (rp = S.from), (sa = climb); + if (sa > 0) { + const off = 31.5 - rp, view = rp - 31.5; + let g = floor(view / TOTAL) * N; + while (wordX(g + 1) <= view) g++; + const want = S.next.s; + for (; wordX(g) + off < W; g++) { + const w = word(g); + let x = wordX(g) + off; + for (const ch of w) { + const gl = gOf(ch, false), dx = x + gl.w / 2 - 31.5; + const a = sa * (0.3 + 0.4 * exp(-(dx * dx) / 120)); + stamp(gl, x, yS, ch === want ? gold : sky, ch === want ? a * 1.2 : a); + x += gl.w + 1; + } + } + } + // The line that is locking fades in over the strip, its capital flashing + // white and cooling to gold. + if (S.phase === 1) { + const e = ease(u), cap = [0, 1, 2].map((q) => white[q] + (gold[q] - white[q]) * ease(u * 1.4)); + for (const g of S.next.glyphs) stamp(g.gl, g.x, yS, g.cap ? cap : sky, g.cap ? 0.5 + 0.5 * min(1, u * 4) : e); + } + + for (let p = 0; p < NP; p++) display.setPixel(p % W, (p / W) | 0, round(min(1, rgb[p * 3]) * 255), round(min(1, rgb[p * 3 + 1]) * 255), round(min(1, rgb[p * 3 + 2]) * 255)); +} From 261a44b203b73a7e6c27f4dd7589f4aa7e680d9f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:50:49 +0000 Subject: [PATCH 05/22] =?UTF-8?q?Euler=20Strokes=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PBtzYrhzPXCgRY4TG --- .../pattern-study/patterns/euler-strokes.js | 456 ++++++++++++++++++ 1 file changed, 456 insertions(+) create mode 100644 incoming/pattern-study/patterns/euler-strokes.js diff --git a/incoming/pattern-study/patterns/euler-strokes.js b/incoming/pattern-study/patterns/euler-strokes.js new file mode 100644 index 000000000..0e0b5a2fb --- /dev/null +++ b/incoming/pattern-study/patterns/euler-strokes.js @@ -0,0 +1,456 @@ +// ===== Patternflow pattern ===== +// Title: Euler Strokes +// Family: type +// Idea: A pen writes a word stacked down the panel letter by letter, each letter a graph of stroke ends, corners and junctions, in exactly as many strokes as Euler's count of its odd vertices allows, following a Hierholzer route: the odd vertices glow before the pen reaches them, every pen lift leaves a faint dotted trail, and beside each letter a digit and a row of pips keep its stroke count. +// Author: patternflow-lab +// Date: 2026-10-30 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the seven bridges of Königsberg and the odd-vertex count (Leonhard Euler, Solutio problematis ad geometriam situs pertinentis, 1736), the construction of an Euler path (Carl Hierholzer, 1873), one-stroke drawing (한붓그리기), pattern round 75 +// Round: 75 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Word=0..3, Order=0..1 +// +// Knob 1: Hue. Turns every colour together. At 0 the first stroke of a letter is cyan, the second gold, the third magenta, the fourth violet; the odd vertices glow warm white and the unwritten skeleton is a pale blue grey. +// Knob 2: Speed. At 2 the pen writes 34 LEDs a second and travels with the pen up at 90, and rests 0.12 s on each touch and lift. PATH takes 11.7 s in the Euler order, then holds for 1.8 s and fades for 0.9 s before it is written again by another route. At 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Word. PATH (1, 2, 2 and 3 strokes), 다리 (bridge: 3 and 2), EULER (2, 1, 1, 2, 1) or 한붓 (one brush: 6 and 6), the counts in the Euler order. Each letter is a vertex and edge list in the code; the word stacks one letter per cell down the 128 rows. +// Knob 4: Order. Below 0.5 the pen takes the fewest strokes, max(1, odd/2) for each connected piece, on a Hierholzer route that pairs the spare odd vertices with the shortest pen-up hops, and each pass picks a new route of the same count. Above 0.5 it follows school stroke order instead (ball and stick print for the Latin letters, 필순 for Hangul), and the counts grow: PATH 10 against 8, 다리 8 against 5, EULER 12 against 7, 한붓 14 against 12. Rings mark where each stroke touched down and lifted; in school order they land on even vertices too. + +const W = 64, H = 128, NP = W * H; +const { floor, round, min, max, sqrt, sin, cos, exp, abs, hypot, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v) { + 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); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- Letters as graphs. v is the vertex list (x, y pairs, y down) in a box +// 10 by 14 for a Latin capital and 14 by 14 for a Hangul syllable. An edge is +// [a, b] or [a, b, points between]; c is the school stroke order, each stroke +// a list of edges, ~i for edge i run from b to a. +const arc = (cx, cy, rx, ry, a0, a1, n) => { + const o = []; + for (let i = 1; i < n; i++) { + const a = a0 + ((a1 - a0) * i) / n; + o.push(cx + rx * cos(a), cy + ry * sin(a)); + } + return o; +}; +const bowl = () => [5.5, 0, ...arc(5.5, 3.75, 4, 3.75, -PI / 2, PI / 2, 10), 5.5, 7.5]; +const GLYPHS = { + // P: the stem's top has degree 2, the joint 3, the foot 1. One stroke. + P: { v: [1.5, 0, 1.5, 7.5, 1.5, 14], e: [[0, 1], [1, 2], [0, 1, bowl()]], c: [[0, 1], [2]] }, + // A: apex 2, feet 1, the crossbar's ends 3. Two strokes. + A: { v: [5, 0, 0.5, 14, 9.5, 14, 2.11, 9, 7.89, 9], e: [[0, 3], [3, 1], [0, 4], [4, 2], [3, 4]], c: [[0, 1], [2, 3], [4]] }, + // T: three ends and a junction of 3. Two strokes. + T: { v: [0.5, 0, 5, 0, 9.5, 0, 5, 14], e: [[0, 1], [1, 2], [1, 3]], c: [[2], [0, 1]] }, + // H: four ends and two junctions, six odd vertices. Three strokes. + H: { v: [1, 0, 1, 7, 1, 14, 9, 0, 9, 7, 9, 14], e: [[0, 1], [1, 2], [3, 4], [4, 5], [1, 4]], c: [[0, 1], [2, 3], [4]] }, + // E: three bar ends and the middle junction are odd, the corners even. Two strokes. + E: { v: [9, 0, 1, 0, 1, 7, 8, 7, 1, 14, 9, 14], e: [[1, 0], [1, 2], [2, 4], [2, 3], [4, 5]], c: [[1, 2], [0], [3], [4]] }, + U: { v: [1, 0, 9, 0], e: [[0, 1, [1, 9.5, ...arc(5, 9.5, 4, 4.5, PI, 0, 12), 9, 9.5]]], c: [[0]] }, + L: { v: [1.5, 0, 1.5, 14, 9, 14], e: [[0, 1], [1, 2]], c: [[0, 1]] }, + // R: the leg leaves from the joint, which takes degree 4. One stroke. + R: { v: [1.5, 0, 1.5, 7.5, 1.5, 14, 9.5, 14], e: [[0, 1], [1, 2], [0, 1, bowl()], [1, 3]], c: [[0, 1], [2, 3]] }, + // 다: ㄷ is an open path (1), ㅏ a stem with a branch (2). + 다: { + v: [7.2, 2.2, 1, 2.2, 1, 11.8, 7.6, 11.8, 10.4, 0, 10.4, 7, 10.4, 14, 13.4, 7], + e: [[0, 1], [1, 2], [2, 3], [4, 5], [5, 6], [5, 7]], + c: [[~0], [1, 2], [3, 4], [5]], + }, + // 리: ㄹ is one zigzag (1, written as 3), ㅣ a line (1). + 리: { + v: [1, 1.2, 7.4, 1.2, 7.4, 7, 1, 7, 1, 12.8, 7.8, 12.8, 11, 0, 11, 14], + e: [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [6, 7]], + c: [[0, 1], [~2], [3, 4], [5]], + }, + // 한: ㅎ is three pieces (tick, bar, ring), ㅏ two strokes, ㄴ one. + 한: { + v: [4.3, 0.2, 4.3, 1.5, 1, 2.4, 7.6, 2.4, 4.3, 3.5, 4.3, 8.1, 10.6, 0, 10.6, 4.6, 10.6, 9.2, 13.4, 4.6, 1.6, 10.2, 1.6, 13.6, 13, 13.6], + e: [[0, 1], [2, 3], [4, 5, arc(4.3, 5.8, 2.6, 2.3, -PI / 2, -1.5 * PI, 10)], [5, 4, arc(4.3, 5.8, 2.6, 2.3, PI / 2, -PI / 2, 10)], [6, 7], [7, 8], [7, 9], [10, 11], [11, 12]], + c: [[0], [1], [2, 3], [4, 5], [6], [7, 8]], + }, + // 붓: ㅂ has four odd vertices (2, written as 4), ㅜ 2, ㅅ 2. + 붓: { + v: [2.5, 0.3, 2.5, 2.7, 2.5, 5.2, 11.5, 0.3, 11.5, 2.7, 11.5, 5.2, 0.5, 7.2, 7, 7.2, 13.5, 7.2, 7, 9.6, 7, 10.3, 2.6, 13.8, 5.4, 11.57, 11.4, 13.8], + e: [[0, 1], [1, 2], [3, 4], [4, 5], [1, 4], [2, 5], [6, 7], [7, 8], [7, 9], [10, 12], [12, 11], [12, 13]], + c: [[0, 1], [2, 3], [4], [5], [6, 7], [8], [9, 10], [11]], + }, +}; +const WORDS = [["P", "A", "T", "H"], ["다", "리"], ["E", "U", "L", "E", "R"], ["한", "붓"]]; +const STROKE_HUE = [0, 0.36, 0.62, 0.8, 0.2, 0.5, 0.27, 0.9]; + +// 3 by 5 digits for the counts. +const DIGITS = [0x7b6f, 0x2c97, 0x73e7, 0x73cf, 0x5bc9, 0x79cf, 0x79ef, 0x7292, 0x7bef, 0x7bcf]; + +// Pen speeds in LEDs per second and rests in seconds, at Speed 2. +const V_DOWN = 34, V_UP = 90, REST = 0.12, HOLD = 1.8, FADE = 0.9; + +// ---- Euler's count and Hierholzer's route. Each connected piece with 2k odd +// vertices is drawn in k strokes: k - 1 pen-up hops pair off all but two odd +// vertices, the walk starts at one of those two, and Hierholzer's splice +// finds a walk that uses every edge once. The hops are where it lifts. +function* matchings(a) { + if (!a.length) { + yield []; + return; + } + const [x, ...rest] = a; + for (let i = 0; i < rest.length; i++) { + const y = rest[i], others = rest.filter((_, j) => j !== i); + for (const m of matchings(others)) yield [[x, y], ...m]; + } +} + +function eulerOrder(g) { + const nv = g.v.length / 2, deg = new Array(nv).fill(0), par = [...Array(nv).keys()]; + const find = (x) => (par[x] === x ? x : (par[x] = find(par[x]))); + for (const [a, b] of g.e) { + deg[a]++; + deg[b]++; + par[find(a)] = find(b); + } + const key = (i) => g.v[2 * i + 1] + 0.6 * g.v[2 * i]; + const dist = (a, b) => hypot(g.v[2 * a] - g.v[2 * b], g.v[2 * a + 1] - g.v[2 * b + 1]); + const comps = new Map(); + for (let i = 0; i < nv; i++) if (deg[i]) (comps.get(find(i)) || comps.set(find(i), []).get(find(i))).push(i); + const list = [...comps.values()].sort((A, B) => min(...A.map(key)) - min(...B.map(key))); + const strokes = []; + for (const C of list) { + const odd = C.filter((i) => deg[i] & 1); + let s = C.reduce((a, b) => (key(a) <= key(b) ? a : b)), hops = []; + if (odd.length) { + let best = Infinity; + for (const si of odd) + for (const ti of odd) { + if (si === ti) continue; + for (const m of matchings(odd.filter((x) => x !== si && x !== ti))) { + let c = 0.15 * key(si) + 0.8 * Math.random(); + for (const [a, b] of m) c += dist(a, b); + if (c < best) (best = c), (s = si), (hops = m); + } + } + } + // The piece's edges and the hops, with each vertex's list shuffled so + // each pass can take another route of the same count. + const ids = [], ends = []; + g.e.forEach((e, i) => { + if (find(e[0]) === find(C[0])) ids.push(i), ends.push([e[0], e[1]]); + }); + for (const [a, b] of hops) ids.push(-1), ends.push([a, b]); + const adj = new Map(); + ends.forEach(([a, b], j) => { + for (const x of [a, b]) (adj.get(x) || adj.set(x, []).get(x)).push(j); + }); + for (const l of adj.values()) + for (let i = l.length - 1; i > 0; i--) { + const j = floor(Math.random() * (i + 1)); + [l[i], l[j]] = [l[j], l[i]]; + } + const used = new Uint8Array(ends.length), ptr = new Map(), stack = [[s, -1]], walk = []; + while (stack.length) { + const [v, j] = stack[stack.length - 1], l = adj.get(v) || []; + let p = ptr.get(v) || 0, moved = false; + while (p < l.length) { + const jj = l[p++]; + if (used[jj]) continue; + used[jj] = 1; + stack.push([ends[jj][0] === v ? ends[jj][1] : ends[jj][0], jj]); + moved = true; + break; + } + ptr.set(v, p); + if (!moved) { + stack.pop(); + if (j >= 0) walk.push([j, v]); + } + } + walk.reverse(); + let at = s, cur = []; + for (const [j, to] of walk) { + if (ids[j] < 0) { + if (cur.length) strokes.push(cur); + cur = []; + } else cur.push(ends[j][0] === at && ends[j][1] === to ? ids[j] : ~ids[j]); + at = to; + } + if (cur.length) strokes.push(cur); + } + return strokes; +} + +// ---- Layout: one letter per cell down the panel, its count beside it. +function layout(S) { + const word = WORDS[S.word], n = word.length, ch = H / n, hangul = word[0].charCodeAt(0) > 255; + const bw = hangul ? 14 : 10, bh = 14, CWID = 15, GAP = 5; + // Latin capitals may stretch up to 1.4 times wider than tall. + const sy = min((W - 4 - CWID - GAP) / bw, (ch - (n > 3 ? 7 : 10)) / bh), sx = min((W - 4 - CWID - GAP) / bw, sy * (hangul ? 1 : 1.4)); + const gw = bw * sx, x0 = (W - gw - GAP - CWID) / 2; + S.r = 0.85 + 0.28 * sy; + S.glyphs = word.map((name, gi) => { + const g = GLYPHS[name], y0 = gi * ch + (ch - bh * sy) / 2; + const P = (x, y) => [x0 + x * sx, y0 + y * sy]; + const vx = [], deg = new Array(g.v.length / 2).fill(0); + for (let i = 0; i < g.v.length; i += 2) vx.push(P(g.v[i], g.v[i + 1])); + const edges = g.e.map(([a, b, mid]) => { + const pts = [...vx[a]]; + for (let i = 0; mid && i < mid.length; i += 2) pts.push(...P(mid[i], mid[i + 1])); + pts.push(...vx[b]); + deg[a]++; + deg[b]++; + return pts; + }); + return { g, vx, edges, deg, cx: x0 + gw + GAP, cy: gi * ch + ch / 2 }; + }); +} + +function strokePts(G, refs) { + const out = []; + for (const ref of refs) { + const id = ref >= 0 ? ref : ~ref, e = G.edges[id], n = e.length / 2; + for (let i = 0; i < n; i++) { + const k = ref >= 0 ? i : n - 1 - i; + if (out.length && i === 0) continue; + out.push(e[2 * k], e[2 * k + 1]); + } + } + return out; +} + +// Which vertex a stroke starts or ends on. +function vertexAt(G, x, y) { + let best = -1, bd = 0.5; + G.vx.forEach(([vx, vy], i) => { + const d = hypot(vx - x, vy - y); + if (d < bd) (bd = d), (best = i); + }); + return best; +} + +function track(pts) { + const cum = [0]; + for (let i = 2; i < pts.length; i += 2) cum.push(cum[cum.length - 1] + hypot(pts[i] - pts[i - 2], pts[i + 1] - pts[i - 1])); + return { pts, cum, len: cum[cum.length - 1] }; +} + +function newPass(S) { + S.moves = []; + S.counts = []; + S.marks = []; + S.ghost.fill(0); + let px = null, py = null; + S.glyphs.forEach((G, gi) => { + const strokes = S.mode ? G.g.c : eulerOrder(G.g); + S.counts[gi] = strokes.length; + S.marks[gi] = new Set(); + strokes.forEach((refs, k) => { + const pts = strokePts(G, refs), n = pts.length; + if (px !== null) S.moves.push({ up: true, gi, ...track([px, py, pts[0], pts[1]]) }); + const sv = vertexAt(G, pts[0], pts[1]), ev = vertexAt(G, pts[n - 2], pts[n - 1]); + S.moves.push({ up: false, gi, k, sv, ev, ...track(pts) }); + // The unwritten skeleton, faintly tinted with the stroke each piece + // will belong to, so the route shows before the pen takes it. + for (let i = 2; i < n; i += 2) capsule(S.ghost, null, pts[i - 2], pts[i - 1], pts[i], pts[i + 1], S.r, 1, 0, S.gk, k); + S.marks[gi].add(sv).add(ev); + px = pts[n - 2]; + py = pts[n - 1]; + }); + }); + S.mi = 0; + S.u = 0; + S.done = 0; + S.hold = -1; + S.ink.fill(0); + S.hot.fill(0); + S.touched = S.glyphs.map((G) => new Float32Array(G.vx.length).fill(-1)); + S.lit = S.glyphs.map(() => []); + S.pen = [S.moves[0].pts[0], S.moves[0].pts[1], false]; + S.started = -1; +} + +const dur = (m) => (m.up ? m.len / V_UP : REST + m.len / V_DOWN + REST); + +function posAt(m, s) { + const { pts, cum } = m; + let i = 1; + while (i < cum.length - 1 && cum[i] < s) i++; + const u = cum[i] > cum[i - 1] ? cl((s - cum[i - 1]) / (cum[i] - cum[i - 1]), 0, 1) : 1; + return [pts[2 * i - 2] + (pts[2 * i] - pts[2 * i - 2]) * u, pts[2 * i - 1] + (pts[2 * i + 1] - pts[2 * i - 1]) * u]; +} + +// Ink the move from arc length a to b: a capsule per piece for strokes, a dot +// every 3 LEDs for pen-up hops. +function inkSpan(S, m, a, b) { + if (b <= a) return; + if (m.up) { + // Dashes two LEDs long with two LEDs between them. + for (let s = floor(a) + 1; s <= b; s++) { + const [x, y] = posAt(m, s); + if (s > 2 && s < m.len - 2 && s % 4 < 2) capsule(S.ink, [0.3, 0.31, 0.36], x, y, x, y, 0.5, 1, 0); + } + return; + } + const col = S.strokeCol[m.k % 8]; + const { pts, cum } = m; + for (let i = 1; i < cum.length; i++) { + const s0 = max(a, cum[i - 1]), s1 = min(b, cum[i]); + if (s1 < s0 || cum[i] <= cum[i - 1]) continue; + const [x0, y0] = posAt(m, s0), [x1, y1] = posAt(m, s1); + capsule(S.ink, col, x0, y0, x1, y1, S.r, 1, 0.42); + capsule(S.hot, null, x0, y0, x1, y1, S.r, 1, 0.6); + } +} + +// A round-ended segment: a solid core of radius r and a soft glow around it. +// With col null it writes one channel (the ghost). +function capsule(buf, col, ax, ay, bx, by, r, a, glow, kb, kv) { + const R = r + (glow ? 5 : 1), dx = bx - ax, dy = by - ay, L2 = dx * dx + dy * dy; + for (let y = max(0, floor(min(ay, by) - R)); y <= min(H - 1, floor(max(ay, by) + R)); y++) + for (let x = max(0, floor(min(ax, bx) - R)); x <= min(W - 1, floor(max(ax, bx) + R)); x++) { + const px = x + 0.5 - ax, py = y + 0.5 - ay, t = L2 > 0 ? cl((px * dx + py * dy) / L2, 0, 1) : 0; + const d = hypot(px - t * dx, py - t * dy), core = cl(r + 0.5 - d, 0, 1), e = max(0, d - r); + const v = a * max(core, glow * exp(-e * e * 0.16)); + if (v <= 0.004) continue; + const p = y * W + x; + if (!col) { + if (v > buf[p]) (buf[p] = v), kb && (kb[p] = kv); + } + else for (let c = 0; c < 3; c++) buf[3 * p + c] = max(buf[3 * p + c], col[c] * v * (core > 0.5 ? 1 : 0.85)); + } +} + +export function setup(params) { + params.s = { word: -1, mode: 0, hue: -1, t: 0, ink: new Float32Array(NP * 3), ghost: new Float32Array(NP), gk: new Uint8Array(NP), hot: new Float32Array(NP), rgb: new Float32Array(NP * 3), strokeCol: [] }; +} + +function touch(S, gi, v) { + if (v >= 0 && S.touched[gi][v] < 0) S.touched[gi][v] = S.t; +} + +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); + if (k[0] !== S.hue) { + S.hue = k[0]; + S.strokeCol = STROKE_HUE.map((h) => hsv(0.5 + S.hue + h, 0.58, 1)); + } + const word = cl(round(k[2]), 0, 3), mode = k[3] >= 0.5 ? 1 : 0; + if (word !== S.word || mode !== S.mode) { + const relayout = word !== S.word; + S.word = word; + S.mode = mode; + if (relayout) layout(S); + newPass(S); + } + const step = (d * cl(k[1], 0.2, 3)) / 2, cool = exp(-d * 1.4); + S.t += step; + // Fresh ink is white hot and cools to its colour in about 0.7 s of real + // time, so a faster pen drags a longer hot tail. + for (let p = 0; p < NP; p++) S.hot[p] *= cool; + if (S.hold >= 0) { + S.hold += step; + if (S.hold > HOLD + FADE) newPass(S); + return; + } + S.u += step; + for (let guard = 0; guard < 64 && S.mi < S.moves.length; guard++) { + const m = S.moves[S.mi]; + if (!m.up && S.started !== S.mi) { + S.started = S.mi; + touch(S, m.gi, m.sv); + S.lit[m.gi][m.k] = 1; + } + const T = dur(m), s = m.up ? S.u * V_UP : cl((S.u - REST) * V_DOWN, 0, m.len); + inkSpan(S, m, S.done, min(s, m.len)); + S.done = max(S.done, min(s, m.len)); + const [x, y] = posAt(m, S.done); + S.pen = [x, y, !m.up]; + if (S.u < T) break; + if (!m.up) touch(S, m.gi, m.ev); + S.u -= T; + S.done = 0; + S.mi++; + } + if (S.mi >= S.moves.length) S.hold = 0; +} + +// Soft disc or ring added on top of the frame. +function spot(rgb, x, y, r, col, a, ring) { + const R = r + 2; + for (let j = max(0, floor(y - R)); j <= min(H - 1, floor(y + R)); j++) + for (let i = max(0, floor(x - R)); i <= min(W - 1, floor(x + R)); i++) { + const dd = hypot(i + 0.5 - x, j + 0.5 - y); + const v = a * (ring ? cl(1 - abs(dd - r) / 0.8, 0, 1) : cl(r + 0.5 - dd, 0, 1) + 0.35 * exp(-max(0, dd - r) * 0.9) * (dd > r ? 1 : 0)); + if (v <= 0) continue; + const o = 3 * (j * W + i); + for (let c = 0; c < 3; c++) rgb[o + c] = max(rgb[o + c], col[c] * v); + } +} + +function digit(rgb, n, x, y, col, z) { + const bits = DIGITS[n % 10]; + for (let r = 0; r < 5; r++) + for (let c = 0; c < 3; c++) + if ((bits >> (14 - r * 3 - c)) & 1) + for (let j = 0; j < z; j++) + for (let i = 0; i < z; i++) { + const X = round(x) + c * z + i, Y = round(y) + r * z + j; + if (X >= 0 && X < W && Y >= 0 && Y < H) for (let q = 0; q < 3; q++) rgb[3 * (Y * W + X) + q] = col[q]; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.glyphs) return; + const { rgb, ink, ghost, gk, hot, strokeCol } = S; + const fade = S.hold > HOLD ? cl((S.hold - HOLD) / FADE, 0, 1) : 0, keep = 1 - fade; + const bg = hsv(0.62 + S.hue, 0.6, 0.035), gc = hsv(0.6 + S.hue, 0.25, 0.2); + for (let p = 0; p < NP; p++) { + const g = ghost[p], o = 3 * p, sc = strokeCol[gk[p] % 8]; + for (let c = 0; c < 3; c++) rgb[o + c] = max(bg[c] + g * (0.4 * gc[c] + 0.28 * sc[c]), ink[o + c] * keep) + 0.6 * hot[p]; + } + const warm = hsv(0.12 + S.hue, 0.25, 1), rose = hsv(0.97 + S.hue, 0.5, 1), pale = hsv(0.6 + S.hue, 0.12, 0.85), pulse = 0.5 + 0.5 * sin(TAU * 1.3 * S.t); + const next = S.moves[S.mi]; + S.glyphs.forEach((G, gi) => { + // The vertices where the plan's strokes start or end glow until the pen + // touches them, then keep a ring: warm on an odd vertex, rose on an even + // one. In the Euler order every mark but one per closed piece is odd. + for (const v of S.marks[gi]) { + const [x, y] = G.vx[v], col = G.deg[v] & 1 ? warm : rose; + if (S.touched[gi][v] >= 0) { + spot(rgb, x, y, 2.4, col, 0.6 * keep * min(1, (S.t - S.touched[gi][v]) * 4 + 0.2), true); + continue; + } + const after = next && next.up ? S.moves[S.mi + 1] : next, soon = after && !after.up && after.gi === gi && after.sv === v ? 1 : 0; + spot(rgb, x, y, 2.2, col, (0.4 + 0.35 * pulse + 0.35 * soon) * keep + 0.2 * fade); + } + // The count: a digit and a pip for each stroke, lit as the stroke starts. + const z = H / S.glyphs.length >= 30 ? 3 : 2, n = S.counts[gi], cols = min(4, n), rows = Math.ceil(n / 4), top = G.cy - (5 * z + 3 + rows * 4 - 1) / 2; + digit(rgb, n, G.cx + 7.5 - 1.5 * z, top, S.mode ? rose : pale, z); + for (let q = 0; q < n; q++) { + const px = G.cx + (q % 4) * 4 + (4 - cols) * 2, py = top + 5 * z + 3 + floor(q / 4) * 4, on = S.lit[gi][q] && keep > 0; + const col = on ? S.strokeCol[q % 8] : gc; + for (let j = 0; j < 3; j++) + for (let i = 0; i < 3; i++) { + const X = round(px) + i, Y = round(py) + j; + if (X < W && Y < H) for (let c = 0; c < 3; c++) rgb[3 * (Y * W + X) + c] = col[c] * (on ? keep : 0.9); + } + } + }); + if (S.hold < 0) { + const [x, y, down] = S.pen, m = S.moves[S.mi], lc = down && m ? S.strokeCol[m.k % 8] : pale; + // The pen tip lights the paper round it in the colour it writes. + for (let j = max(0, floor(y - 22)); j <= min(H - 1, floor(y + 22)); j++) + for (let i = 0; i < W; i++) { + const dx = i + 0.5 - x, dy = j + 0.5 - y, v = 0.2 * exp(-(dx * dx + dy * dy) / 130); + if (v < 0.01) continue; + const o = 3 * (j * W + i); + for (let c = 0; c < 3; c++) rgb[o + c] += lc[c] * v; + } + spot(rgb, x, y, down ? 1.2 : 1.7, [1, 1, 1], down ? 1 : 0.8, !down); + } + for (let p = 0; p < NP; p++) display.setPixel(p % W, floor(p / W), min(255, rgb[3 * p] * 255) | 0, min(255, rgb[3 * p + 1] * 255) | 0, min(255, rgb[3 * p + 2] * 255) | 0); +} From b6112c7b0f0b8375197a3f7dcd48d99b70734499 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:52:36 +0000 Subject: [PATCH 06/22] =?UTF-8?q?FRACTRAN=20=ED=8C=A8=ED=84=B4=EC=9D=84=20?= =?UTF-8?q?=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PBtzYrhzPXCgRY4TG --- incoming/pattern-study/patterns/fractran.js | 287 ++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 incoming/pattern-study/patterns/fractran.js diff --git a/incoming/pattern-study/patterns/fractran.js b/incoming/pattern-study/patterns/fractran.js new file mode 100644 index 000000000..24f691480 --- /dev/null +++ b/incoming/pattern-study/patterns/fractran.js @@ -0,0 +1,287 @@ +// ===== Patternflow pattern ===== +// Title: FRACTRAN +// Family: number +// Idea: Conway's PRIMEGAME runs as a bead machine: the exponents of 2, 3, 5 and 7 stand as columns of beads, each step the first of the fourteen fractions that keeps n whole lights up while beads fly up into its denominator and out of its numerator, the columns pump up and down in sawtooth loops, and each time n is a bare power of two the 2 column alone holds p beads and the prime p joins the record at the top. +// Author: patternflow-lab +// Date: 2026-10-30 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: FRACTRAN and PRIMEGAME (John Horton Conway, 1987, "FRACTRAN: A Simple Universal Programming Language for Arithmetic"), pattern round 75 +// Round: 75 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Start=0..3, Trail=0..1 +// +// Knob 1: Hue. Turns the beads round the wheel together: at 0 the 2 column is gold, 3 cyan, 5 violet and 7 green, and the state lamps 11 to 29 blue. +// Knob 2: Speed. Steps a second, 2 times 50 to the power (Speed - 0.2) / 1.8: 100 at 2, so 2, 3, 5 and 7 arrive at 0.2, 0.7, 2.9 and 7.4 s; 2 at 0.2, one fraction at a time; 870 at 3. The machine stops for 12 steps' time on each prime. +// Knob 3: Start. Where the run begins: n = 2 at 0, or n = 2^11, 2^19 or 2^29, the moments PRIMEGAME reaches those primes, with the primes before them already in the record. The higher the start, the taller the sawtooth: from 2^29 the columns climb to 31 beads in long ramps. Every run ends after 2^41 and starts again. +// Knob 4: Trail. How long the light stays: at 0.06 a flying bead leaves a tail of 2 frames and a fraction fades in 0.1 s; at 1 the flights stay for 1.5 s and pile up into bright paths between the columns and the fractions that loop, and each column keeps a ghost of its last peak. + +const W = 64, H = 128, NP = W * H; +const { floor, round, min, max, sqrt, exp, pow, abs } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v) { + 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); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- The machine. n is held as its exponents over the ten primes the +// program uses, never as a number. 2, 3, 5 and 7 are the registers; 11 to 29 +// never go above 1 and act as the program's state. +const PR = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]; +const FR = [[17, 91], [78, 85], [19, 51], [23, 38], [29, 33], [77, 29], [95, 23], [77, 19], [1, 17], [11, 13], [13, 11], [15, 2], [1, 7], [55, 1]]; +function vec(n) { + const e = new Int8Array(10); + for (let i = 0; i < 10; i++) while (n % PR[i] === 0) (n /= PR[i]), e[i]++; + return e; +} +const NUM = FR.map((f) => vec(f[0])), DEN = FR.map((f) => vec(f[1])); +// Run in Node from n = 2, 2^p first appears at step 19, 69, 281, 710, 2375, +// 3893, 8102, 11361, 19268, 36981, 45680, 75417 and 101354 for p = 2 to 41. +const PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41]; +const STARTS = [1, 11, 19, 29]; + +// ---- Digits, 3 by 5. +const DIG = ["###.#.#.#.#.###", ".#.##..#..#.###", "###..#####..###", "###..#.##..####", "#.##.####..#..#", "####..###..####", "####..####.####", "###..#..#.#..#.", "####.#####.####", "####.####..####"]; +const textW = (s) => 4 * s.length - 1; + +// ---- Layout. The record of primes in two lines at the top, the fourteen +// fractions in two rows of seven, numerator over denominator, the state +// lamps 11 to 29, then the four register columns standing on row 120. +const REC_Y = [1, 7]; +const FR_Y = [15, 31]; +const ST_Y = 47; +const COL_X = [8, 24, 40, 56], FLOOR = 120, CEIL = 56; +const frX = (k) => 4.5 + 9 * (k % 7); +const frY = (k) => FR_Y[k < 7 ? 0 : 1]; +const stX = (j) => 6.5 + 10 * j; +const recPlace = (() => { + const out = []; + for (const [line, from, to] of [[0, 0, 7], [1, 7, 13]]) { + const items = PRIMES.slice(from, to).map(String); + let x = floor((W - (items.reduce((s, t) => s + textW(t), 0) + 3 * (items.length - 1))) / 2); + for (const t of items) { + out.push([x, REC_Y[line]]); + x += textW(t) + 3; + } + } + return out; +})(); + +function reset(S) { + const p = STARTS[S.start]; + S.e = new Int32Array(10); + S.e[0] = p; + S.found = PRIMES.filter((q) => q <= p && p > 1).length; + S.flights = []; + S.inc = new Int32Array(10); + S.heat.fill(0); + S.ghost = new Float32Array(4); + S.acc = 0; + S.pause = 0; + S.flash = 0; + S.end = 0; + S.peak = max(12, p + 1); + S.pitch = min(5, (FLOOR - CEIL) / S.peak); + S.glow.fill(0); + S.last = -1; +} + +export function setup(params) { + params.s = { start: -1, hue: 0, rate: 100, trail: 0.06, heat: new Float32Array(14), glow: new Float32Array(NP * 3), rgb: new Float32Array(NP * 3), sheat: new Float32Array(6), gain: 1, last: -1, dur: 0.3 }; +} + +// A bead in flight runs up or down its own column: up from the top of the +// stack and out of the column's head when a denominator takes it, down from +// the head onto the stack when a numerator gives it. +function flight(S, j, y0, y1) { + if (S.flights.length > 240) { + if (y1 > y0) S.inc[j]--; + return; + } + S.flights.push({ j, y0, y1, u: 0 }); +} + +function step(S) { + const e = S.e; + let k = 0; + for (; k < 14; k++) { + let ok = true; + for (let j = 0; j < 10 && ok; j++) if (e[j] < DEN[k][j]) ok = false; + if (ok) break; + } + S.heat[k] = min(1.6, S.heat[k] + 1); + S.last = k; + for (let j = 0; j < 10; j++) { + e[j] -= DEN[k][j]; + if (j < 4) for (let d = 0; d < DEN[k][j]; d++) flight(S, j, FLOOR - S.pitch * (e[j] + d + 0.5), CEIL - 2); + } + for (let j = 0; j < 10; j++) { + for (let n = 0; n < NUM[k][j]; n++) { + e[j]++; + if (j < 4) { + S.inc[j]++; + flight(S, j, CEIL - 2, FLOOR - S.pitch * (e[j] - 0.5)); + } + } + } + let bare = e[0] >= 2; + for (let j = 1; j < 10 && bare; j++) if (e[j]) bare = false; + if (bare) { + S.found = PRIMES.indexOf(e[0]) + 1; + S.flash = 1; + // Hold until every bead has landed, then a beat more, so the 2 column + // stands alone with its p beads. + S.pause = S.dur + cl(30 / S.rate, 0.25, 1.5); + if (e[0] >= PRIMES[PRIMES.length - 1]) S.end = 3; + } + for (let j = 0; j < 4; j++) if (e[j] + 1 > S.peak) S.peak = e[j] + 1; +} + +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.rate = 2 * pow(60, (cl(k[1], 0.2, 3) - 0.2) / 1.8); + S.trail = cl(k[3], 0, 1); + const start = cl(round(k[2]), 0, 3); + if (start !== S.start) { + S.start = start; + reset(S); + } + const tau = 0.05 * pow(30, S.trail); + // A longer trail lays each pass down fainter, so a path's brightness is + // its traffic, not how long the light stays. + S.gain = min(1, 0.08 / tau); + const fade = exp(-d / tau), hfade = exp(-d / max(0.1, tau, 0.6 / S.rate)); + for (let i = 0; i < NP * 3; i++) S.glow[i] *= fade; + for (let i = 0; i < 14; i++) S.heat[i] *= hfade; + S.flash *= exp(-d / 0.6); + // A flight takes 0.3 s at speed, up to 0.6 s when the steps come slower. + const dur = (S.dur = cl(2.2 / S.rate, 0.3, 0.6)); + const live = []; + for (const f of S.flights) { + const u0 = f.u; + f.u = min(1, f.u + d / dur); + trace(S, f, u0, f.u); + if (f.u >= 1) { + if (f.y1 > f.y0) S.inc[f.j]--; + } else live.push(f); + } + S.flights = live; + if (S.end > 0) { + S.end -= d; + if (S.end <= 0) reset(S); + } else if (S.pause > 0) S.pause -= d; + else { + S.acc += S.rate * d; + for (let guard = 0; S.acc >= 1 && guard < 200; guard++) { + S.acc--; + step(S); + if (S.pause > 0 || S.end > 0) { + S.acc = 0; + break; + } + } + } + S.pitch += (min(5, (FLOOR - CEIL) / S.peak) - S.pitch) * (1 - exp(-d / 0.4)); + for (let j = 0; j < 4; j++) { + const c = S.e[j] - S.inc[j]; + S.ghost[j] = max(c, S.ghost[j] - d * (2 + 30 * (1 - S.trail))); + } +} + +// Falling beads speed up, rising ones slow down, like a ball thrown up. +const fly = (f, u) => f.y0 + (f.y1 - f.y0) * (f.y1 > f.y0 ? u * u : u * (2 - u)); + +function trace(S, f, u0, u1) { + const a = fly(f, u0), b = fly(f, u1), col = S.cols[f.j]; + const lo = round(min(a, b)), hi = round(max(a, b)), x = COL_X[f.j]; + for (let y = lo; y <= hi; y++) { + if (y < 0 || y >= H) continue; + const w = S.gain * (0.25 + 0.5 * (f.y1 > f.y0 ? (y - lo + 1) / (hi - lo + 1) : (hi - y + 1) / (hi - lo + 1))); + for (let dx = -1; dx <= 1; dx++) { + const o = (y * W + x + dx) * 3, ww = dx ? 0.5 * w : w; + S.glow[o] += col[0] * ww; + S.glow[o + 1] += col[1] * ww; + S.glow[o + 2] += col[2] * ww; + } + } +} + +function put(S, x, y, c, a) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + S.rgb[o] += c[0] * a; + S.rgb[o + 1] += c[1] * a; + S.rgb[o + 2] += c[2] * a; +} + +function text(S, s, x, y, c, a) { + for (let i = 0; i < s.length; i++) { + const g = DIG[s.charCodeAt(i) - 48]; + for (let r = 0; r < 5; r++) for (let q = 0; q < 3; q++) if (g[r * 3 + q] === "#") put(S, x + 4 * i + q, y + r, c, a); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.e) return; + const h = S.hue, rgb = S.rgb; + S.cols = [hsv(0.11 + h, 0.6, 1), hsv(0.5 + h, 0.6, 1), hsv(0.8 + h, 0.5, 1), hsv(0.32 + h, 0.6, 1), hsv(0.61 + h, 0.5, 1)]; + const cols = S.cols, ink = hsv(0.58 + h, 0.3, 1), warm = hsv(0.1 + h, 0.4, 1); + rgb.fill(0); + // The record. + for (let i = 0; i < S.found; i++) { + const [x, y] = recPlace[i], t = String(PRIMES[i]), lastOne = i === S.found - 1; + if (lastOne && S.flash > 0.02) for (let yy = y - 1; yy <= y + 5; yy++) for (let xx = x - 1; xx <= x + textW(t); xx++) put(S, xx, yy, cols[0], 0.35 * S.flash); + text(S, t, x, y, lastOne ? [1, 1, 1] : warm, lastOne ? 0.75 + 0.25 * S.flash : 0.6); + } + // The fractions. + for (let k = 0; k < 14; k++) { + const x = frX(k), y = frY(k), hk = min(1, max(S.heat[k], k === S.last ? 0.55 : 0)), [a, b] = FR[k].map(String); + if (hk > 0.02) for (let yy = y - 1; yy <= y + 13; yy++) for (let xx = floor(x - 4); xx <= floor(x + 4); xx++) put(S, xx, yy, ink, 0.16 * hk); + const lv = 0.2 + 0.8 * hk; + text(S, a, round(x - textW(a) / 2), y, ink, lv); + for (let xx = round(x - 3.5); xx < round(x + 3.5); xx++) put(S, xx, y + 6, ink, lv * 0.8); + text(S, b, round(x - textW(b) / 2), y + 8, ink, lv); + } + // The state lamps. + for (let j = 0; j < 6; j++) { + const on = S.e[4 + j] - S.inc[4 + j] > 0; + S.sheat[j] = on ? 1 : S.sheat[j] * 0.7; + text(S, String(PR[4 + j]), round(stX(j) - 3.5), ST_Y, on ? [1, 1, 1] : cols[4], on ? 0.95 : 0.22 + 0.5 * S.sheat[j]); + } + // The columns: a rod, the ghost of the last peak, and the beads. + const pitch = S.pitch, bh = max(0.8, pitch - 1.4); + for (let j = 0; j < 4; j++) { + const c = cols[j], cx = COL_X[j], n = S.e[j] - S.inc[j]; + for (let y = CEIL; y <= FLOOR; y++) put(S, cx, y, c, 0.12); + text(S, String(PR[j]), cx - 1, FLOOR + 2, c, 0.7); + const top = S.ghost[j]; + for (let i = 0; i < Math.ceil(top); i++) { + const lit = i < n, yc = FLOOR - pitch * (i + 0.5), a = lit ? 1 : 0.22 * min(1, top - i); + const glow = j === 0 && S.flash > 0.02 && lit ? S.flash : 0; + for (let y = floor(yc - bh / 2 - 1); y <= floor(yc + bh / 2 + 1); y++) { + const cov = cl(min(y + 1, yc + bh / 2) - max(y, yc - bh / 2), 0, 1); + if (cov <= 0) continue; + for (let dx = -5; dx <= 5; dx++) { + const u = dx / 5.6, shade = 0.3 + 0.7 * sqrt(1 - u * u) - 0.25 * (y + 0.5 - yc) / (bh / 2 + 0.5); + put(S, cx + dx, y, c, a * cov * cl(shade, 0, 1) * 0.85); + if (glow) put(S, cx + dx, y, [1, 1, 1], 0.5 * glow * cov); + } + } + } + } + // Beads in flight: a small bright bead at the head of each trail. + for (const f of S.flights) { + const y = fly(f, f.u), c = S.cols[f.j]; + for (let dy = 0; dy < 2; dy++) for (let dx = -2; dx <= 2; dx++) put(S, COL_X[f.j] + dx, round(y) + dy - 1, abs(dx) < 2 ? [1, 1, 1] : c, abs(dx) < 2 ? 0.8 : 0.6); + } + for (let p = 0; p < NP * 3; p++) rgb[p] += S.glow[p]; + for (let p = 0; p < NP; p++) { + const o = p * 3; + display.setPixel(p % W, floor(p / W), min(255, rgb[o] * 255) | 0, min(255, rgb[o + 1] * 255) | 0, min(255, rgb[o + 2] * 255) | 0); + } +} From a20231603717bdf992b880a39b23ef9642b672c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:53:45 +0000 Subject: [PATCH 07/22] =?UTF-8?q?Prandtl's=20Membrane=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=20=EC=B4=88=EC=95=88=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PBtzYrhzPXCgRY4TG --- .../patterns/prandtl-membrane.js | 293 ++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 incoming/pattern-study/patterns/prandtl-membrane.js diff --git a/incoming/pattern-study/patterns/prandtl-membrane.js b/incoming/pattern-study/patterns/prandtl-membrane.js new file mode 100644 index 000000000..33cd97b71 --- /dev/null +++ b/incoming/pattern-study/patterns/prandtl-membrane.js @@ -0,0 +1,293 @@ +// ===== Patternflow pattern ===== +// Title: Prandtl's Membrane +// Family: type +// Idea: A bar whose cross-section is a word is twisted, and each letter is drawn as Prandtl's soap film stretched over a hole of its shape and blown up by pressure, so the letters breathe in and out as pillows and plateaus lit by a walking lamp, coloured by the shear stress that runs hot along their rims and hottest in their inside corners. +// Author: patternflow-lab +// Date: 2026-10-30 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: torsion of prismatic bars (Adhémar Barré de Saint-Venant, 1855), the membrane analogy (Ludwig Prandtl, 1903), thin-walled tubes (Rudolf Bredt, 1896), pattern round 75 +// Round: 75 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Twist=0.2..3, Word=0..3, Lamp=0..1 +// +// Knob 1: Hue. Turns the stress colours round the wheel: at 0 the unstressed crests are blue, the rims run through magenta and red, and the inside corners burn orange to yellow. +// Knob 2: Twist. How hard the bar is twisted, which is how hard the film is blown up. At 2 the film rises to 60 degrees at a 12 LED stem's rim; at 0.2 it barely leaves the outline and everything stays blue; at 3 the walls stand steep and the rims run yellow. Each breath swings the twist between 40% and 100% of this. +// Knob 3: Word. The cross-section: the syllable 숨 (breath) the height of the panel, then LIT, AIR and HOT stacked. A letter with a hole is a hollow section, and its hole is a flat plate the film lifts. +// Knob 4: Lamp. How high the lamp stands as it walks round the word once in 12 s: at 0.06 it rakes the film from 15 degrees up and each stem is lit on one side and dark on the other; at 1 it stands at 75 degrees and the film glows almost evenly. + +const W = 64, H = 128; +const { floor, round, sqrt, cos, sin, min, max, abs, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v, o, k) { + 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); + o[k] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + o[k + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + o[k + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// ---- Letters, as unions of convex polygons in LEDs. Latin capitals sit in +// a box 56 wide and 38 tall, stems 13 thick and bars 7 to 11; the syllable +// 숨 is drawn for the whole panel. Corners are left sharp on purpose: the +// film has zero slope at an outside corner and an infinite one at an inside +// corner, and both should show. +const R = (a, b, c, d) => [[a, b], [c, b], [c, d], [a, d]]; +const TOP = [[0, 9], [9, 0], [47, 0], [56, 9], [56, 11], [0, 11]]; +const LAT = { + L: [R(0, 0, 13, 38), R(13, 27, 56, 38)], + I: [R(6, 0, 50, 11), R(21, 11, 35, 27), R(6, 27, 50, 38)], + T: [R(0, 0, 56, 11), R(21, 11, 35, 38)], + A: [TOP, R(0, 11, 13, 38), R(43, 11, 56, 38), R(13, 20, 43, 29)], + R: [R(0, 0, 13, 38), [[13, 0], [47, 0], [56, 9], [56, 11], [13, 11]], R(43, 11, 56, 18), [[13, 18], [56, 18], [56, 21], [49, 28], [13, 28]], [[30, 28], [43, 28], [56, 36], [56, 38], [43, 38]]], + H: [R(0, 0, 13, 38), R(43, 0, 56, 38), R(13, 14, 43, 24)], + O: [TOP, [[0, 27], [56, 27], [56, 29], [47, 38], [9, 38], [0, 29]], R(0, 11, 13, 27), R(43, 11, 56, 27)], +}; +const SUM = [ + [[23, 2], [41, 2], [18, 46], [0.5, 46]], // ㅅ, left stroke + [[19, 30], [27, 17], [63.5, 38], [63.5, 46], [46, 46]], // ㅅ, right stroke + R(0.5, 51, 63.5, 65), R(24, 65, 40, 81), // ㅜ + R(3, 85, 61, 99), R(3, 112, 61, 126), R(3, 99, 18, 112), R(46, 99, 61, 112), // ㅁ +]; +const WORDS = ["숨", "LIT", "AIR", "HOT"]; + +function wordPolys(word) { + if (word === "숨") return SUM; + const out = []; + [...word].forEach((ch, i) => { + for (const poly of LAT[ch]) out.push(poly.map(([x, y]) => [x + 4, y + 3 + 43 * i])); + }); + return out; +} + +// Each convex polygon as its edge lines, outward normal (nx, ny) and offset, +// so the largest n.p - c over the edges is negative inside and positive out. +function edgesOf(poly) { + let cx = 0, cy = 0; + for (const [x, y] of poly) (cx += x / poly.length), (cy += y / poly.length); + return poly.map(([x0, y0], i) => { + const [x1, y1] = poly[(i + 1) % poly.length]; + let nx = y1 - y0, ny = x0 - x1; + const l = sqrt(nx * nx + ny * ny); + (nx /= l), (ny /= l); + if (nx * (cx - x0) + ny * (cy - y0) > 0) (nx = -nx), (ny = -ny); + return [nx, ny, nx * x0 + ny * y0]; + }); +} + +// ---- The film. Saint-Venant's stress function phi solves lap(phi) = -2 +// (twist and shear modulus taken as 1) inside the section, phi = 0 on its +// outline, and the shear stress is |grad phi|. Prandtl saw the same equation +// in a film stretched over a hole of that shape and blown up from below. It +// is solved on a grid of half LEDs, 128 by 256 nodes at ((i + 1/2)/2, +// (j + 1/2)/2), by successive over-relaxation, a few sweeps a frame until it +// settles, so a new word inflates as the solver relaxes. Near the outline +// each node uses the Shortley-Weller stencil with its true distance to the +// edge along the grid line, so a slanted or curved outline is not a +// staircase. A hole in a letter makes a hollow section: phi is constant on +// the hole's edge, a flat plate the film lifts, and its height K is set by +// the plate's balance (pressure on the plate's area against the film's pull +// round its rim), which is Bredt's condition that the stress circulates +// round the hole twice the hole's area. Checked in Node: for a circle of +// radius 20 the solver gives phi = (a^2 - r^2)/2 to within 0.3% and the +// stress at the rim is a to within 2%; a 40 by 8 strip has its largest +// stress at the middle of a long side. +const FW = 128, FH = 256, NN = FW * FH, HS = 0.5; +function buildFilm(word) { + // Each polygon is measured only over its own box and 2 LEDs round it; + // everywhere else is plainly outside. + const d = new Float32Array(NN).fill(2); + for (const poly of wordPolys(word)) { + const e = edgesOf(poly).flat(), ne = e.length, xs = poly.map((q) => q[0]), ys = poly.map((q) => q[1]); + const i0 = max(0, floor((min(...xs) - 2) / HS)), i1 = min(FW - 1, floor((max(...xs) + 2) / HS)); + const j0 = max(0, floor((min(...ys) - 2) / HS)), j1 = min(FH - 1, floor((max(...ys) + 2) / HS)); + for (let j = j0; j <= j1; j++) + for (let i = i0; i <= i1; i++) { + const x = (i + 0.5) * HS, y = (j + 0.5) * HS; + let m = -1e9; + for (let k = 0; k < ne; k += 3) { + const v = e[k] * x + e[k + 1] * y - e[k + 2]; + if (v > m) m = v; + } + if (m < d[j * FW + i]) d[j * FW + i] = m; + } + } + // 0 outside, 1 film, 2 + k hole k: a gap no path from the panel's edge reaches. + const type = new Int16Array(NN), seen = new Uint8Array(NN), stack = new Int32Array(4 * NN + 8); + for (let p = 0; p < NN; p++) if (d[p] < 0) type[p] = 1; + const fill = (starts, mark) => { + let top = 0, n = 0; + for (const p of starts) stack[top++] = p; + while (top) { + const p = stack[--top]; + if (seen[p] || type[p] === 1) continue; + seen[p] = 1; + type[p] = mark; + n++; + const x = p % FW; + if (x > 0) stack[top++] = p - 1; + if (x < FW - 1) stack[top++] = p + 1; + if (p >= FW) stack[top++] = p - FW; + if (p < NN - FW) stack[top++] = p + FW; + } + return n; + }; + const rim = []; + for (let i = 0; i < FW; i++) rim.push(i, NN - FW + i); + for (let j = 0; j < FH; j++) rim.push(j * FW, j * FW + FW - 1); + fill(rim, 0); + const area = []; + for (let p = 0; p < NN; p++) if (!seen[p] && type[p] !== 1) area.push(fill([p], 2 + area.length) * HS * HS); + const holes = area.length; + // Film nodes and their stencils. A neighbour slot points into V: a film + // node's own index, NN for the outline (0), or NN + 1 + k for hole k's rim. + const cells = []; + for (let p = 0; p < NN; p++) if (type[p] === 1) cells.push(p); + const n = cells.length, idx = new Int32Array(n * 4), cof = new Float32Array(n * 5), th = new Float32Array(n * 4); + const links = Array.from({ length: holes }, () => []); + const OFF = [-1, 1, -FW, FW]; + for (let c = 0; c < cells.length; c++) { + const p = cells[c]; + for (let k = 0; k < 4; k++) { + const q = p + OFF[k]; + if (type[q] === 1) (idx[c * 4 + k] = q), (th[c * 4 + k] = 1); + else { + const t = cl(d[p] / (d[p] - d[q]), 0.08, 1); + th[c * 4 + k] = t; + if (type[q] >= 2) (idx[c * 4 + k] = NN + 1 + type[q] - 2), links[type[q] - 2].push(c, t); + else idx[c * 4 + k] = NN; + } + } + // Shortley-Weller: d2/dx2 ~ 2/(a b (a+b)) (b uL + a uR - (a+b) u), a, b the arms. + for (let ax = 0; ax < 2; ax++) { + const a = th[c * 4 + ax * 2], b = th[c * 4 + ax * 2 + 1]; + cof[c * 5 + ax * 2] = 2 / (a * (a + b)); + cof[c * 5 + ax * 2 + 1] = 2 / (b * (a + b)); + cof[c * 5 + 4] += 2 / (a * b); + } + } + for (let k = 0; k < holes; k++) { + let corr = 0; + for (let i = 1; i < links[k].length; i += 2) corr += 0.5 - links[k][i]; + area[k] += corr * HS * HS; + } + return { cells: Int32Array.from(cells), idx, cof, th, links, area, holes, type, V: new Float32Array(NN + 1 + holes), n, settled: false, sweeps: 0, gx: new Float32Array(NN), gy: new Float32Array(NN), lgx: new Float32Array(W * H), lgy: new Float32Array(W * H), lin: new Uint8Array(W * H), lplate: new Uint8Array(W * H) }; +} + +// One sweep. The node update is the stencil solved for its centre, with the +// -2 on the right scaled by h^2. Each hole's plate then moves to the height +// where the film's pull round its rim, sum (K - phi)/theta over the links, +// balances twice its area. +function sweep(F, w) { + const { cells, idx, cof, V, n } = F, src = 2 * HS * HS; + let change = 0; + for (let c = 0; c < n; c++) { + const p = cells[c], i4 = c * 4, i5 = c * 5; + const g = (cof[i5] * V[idx[i4]] + cof[i5 + 1] * V[idx[i4 + 1]] + cof[i5 + 2] * V[idx[i4 + 2]] + cof[i5 + 3] * V[idx[i4 + 3]] + src) / cof[i5 + 4]; + const dv = w * (g - V[p]); + V[p] += dv; + if (abs(dv) > change) change = abs(dv); + } + for (let k = 0; k < F.holes; k++) { + const L = F.links[k]; + let s = 0, inv = 0; + for (let i = 0; i < L.length; i += 2) (s += V[cells[L[i]]] / L[i + 1]), (inv += 1 / L[i + 1]); + const K = (2 * F.area[k] + s) / inv, dv = w * (K - V[NN + 1 + k]); + V[NN + 1 + k] += dv; + if (abs(dv) > change) change = abs(dv); + } + return change; +} + +// Slope of the film at each node, from the parabola through the node and its +// two neighbours on each axis at their true distances (the outline or the +// plate counting as a neighbour at its crossing). +function slopes(F) { + const { cells, idx, th, V, n, gx, gy } = F; + for (let c = 0; c < n; c++) { + const p = cells[c], u = V[p]; + for (let ax = 0; ax < 2; ax++) { + const a = th[c * 4 + ax * 2] * HS, b = th[c * 4 + ax * 2 + 1] * HS, uL = V[idx[c * 4 + ax * 2]], uR = V[idx[c * 4 + ax * 2 + 1]]; + const g = (a * a * uR - b * b * uL + (b * b - a * a) * u) / (a * b * (a + b)); + if (ax) gy[p] = g; + else gx[p] = g; + } + } + // Each LED takes the mean slope of the film nodes among its four. + const { type, lgx, lgy, lin, lplate } = F; + for (let q = 0, y = 0; y < H; y++) + for (let x = 0; x < W; x++, q++) { + let sx = 0, sy = 0, m = 0, pl = 0; + for (let s = 0; s < 4; s++) { + const p = (2 * y + (s >> 1)) * FW + 2 * x + (s & 1); + if (type[p] === 1) (sx += gx[p]), (sy += gy[p]), m++; + else if (type[p] >= 2) pl++; + } + (lgx[q] = m ? sx / m : 0), (lgy[q] = m ? sy / m : 0), (lin[q] = m), (lplate[q] = pl); + } +} + +const BREATH = 4; // seconds a breath, fifteen a minute at rest +const LAMP_TURN = 12; +export function setup(params) { + params.s = { t: 0, hue: 0, twist: 2, word: -1, lamp: 0.06, film: null, films: [], rgb: new Float32Array(3) }; +} + +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.t += d; + S.hue = k[0]; + S.twist = cl(k[1], 0.2, 3); + S.lamp = cl(k[3], 0, 1); + const word = cl(round(k[2]), 0, 3); + if (word !== S.word) { + S.word = word; + if (!S.films[word]) S.films[word] = buildFilm(WORDS[word]); + S.film = S.films[word]; + } + const F = S.film; + if (!F.settled) { + let ch = 0; + for (let i = 0; i < 10; i++) ch = sweep(F, 1.92); + F.sweeps += 10; + slopes(F); + if (ch < 1e-2 || F.sweeps > 1500) F.settled = true; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.film) return; + const F = S.film, { lgx, lgy, lin, lplate } = F, rgb = S.rgb; + // The twist swings between 40% and 100% of the knob's on every breath. + const breath = 0.5 - 0.5 * cos((2 * PI * S.t) / BREATH); + const P = (S.twist / 2) * (0.4 + 0.6 * breath); + const Z = 0.12 * P; // film height per unit of phi + const az = (2 * PI * S.t) / LAMP_TURN + 0.6, el = ((15 + 60 * S.lamp) * PI) / 180; + const lx = cos(el) * cos(az), ly = cos(el) * sin(az), lz = sin(el); + let hx = lx, hy = ly, hz = lz + 1; + const hl = sqrt(hx * hx + hy * hy + hz * hz); + (hx /= hl), (hy /= hl), (hz /= hl); + const kt = P / 13; // the stress at which the colour reaches the hot end is 13 / P + const plate = 0.5 * (0.5 + 0.5 * lz); + for (let p = 0, y = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const n = lin[p]; + if (n) { + const gx = lgx[p], gy = lgy[p], ax = -Z * gx, ay = -Z * gy, inv = 1 / sqrt(ax * ax + ay * ay + 1); + const dif = (ax * lx + ay * ly + lz) * inv, nh = (ax * hx + ay * hy + hz) * inv; + let spec = nh > 0.6 ? nh * nh : 0; + spec *= spec * spec * spec; + spec *= spec * spec; // nh^24 + const tau = min(1.2, sqrt(gx * gx + gy * gy) * kt); + const v = (0.4 + 0.3 * (1 + dif) + 0.5 * spec) * (0.6 + 0.6 * tau) * n * 0.25; + hsv(0.62 + S.hue + 0.45 * min(1.15, tau), 1, v > 1 ? 1 : v, rgb, 0); + display.setPixel(x, y, round(255 * rgb[0]), round(255 * rgb[1]), round(255 * rgb[2])); + } else if (lplate[p]) { + hsv(0.62 + S.hue, 1, plate * lplate[p] * 0.25, rgb, 0); + display.setPixel(x, y, round(255 * rgb[0]), round(255 * rgb[1]), round(255 * rgb[2])); + } else display.setPixel(x, y, 0, 0, 0); + } +} From 4d957dbc9250292ed093f05c200bbabadaf44dee Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:56:26 +0000 Subject: [PATCH 08/22] =?UTF-8?q?Letter=20Mobile=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PBtzYrhzPXCgRY4TG --- .../pattern-study/patterns/letter-mobile.js | 426 ++++++++++++++++++ 1 file changed, 426 insertions(+) create mode 100644 incoming/pattern-study/patterns/letter-mobile.js diff --git a/incoming/pattern-study/patterns/letter-mobile.js b/incoming/pattern-study/patterns/letter-mobile.js new file mode 100644 index 000000000..03d872d35 --- /dev/null +++ b/incoming/pattern-study/patterns/letter-mobile.js @@ -0,0 +1,426 @@ +// ===== Patternflow pattern ===== +// Title: Letter Mobile +// Family: type +// Idea: A hanging mobile in the manner of Calder whose weights are the four letters of a word, each weighing its ink, hangs the full height of the panel with every bar balanced by moments and turning slowly on its thread before a lit wall that takes its shadow, and when one letter of the word changes, as in a word ladder, the bars above it tilt and their hanging points slide to the new balance. +// Author: patternflow-lab +// Date: 2026-10-30 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Alexander Calder's mobiles (1931-32, named by Marcel Duchamp), the law of the lever (Archimedes, On the Equilibrium of Planes), word ladders (Lewis Carroll's Doublets, 1879), pattern round 75 +// Round: 75 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Word=0..2, Sway=0..1 +// +// Knob 1: Hue. Turns the painted plates round the wheel together: at 0 they are Calder's yellow, blue, red and white, top to bottom. The wires stay steel and the wall a warm grey. +// Knob 2: Speed. One clock for the turning, the swing and the word: at 2 one letter changes every 3.6 s, the bars twist on their threads once in 9 to 14 s and the mobile swings once in 3.3 s. At 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Word. Which word ladder the mobile climbs, one letter at a time and back: 0 is Carroll's own first, HEAD HEAL TEAL TELL TALL TAIL, from 493 LEDs of ink to 330; 1 is FILL FILM FIRM FORM WORM WARM, light letters turning heavy; 2 is WOMB TOMB TOME TIME TILE TILL, heavy turning light. +// Knob 4: Sway. How far the whole mobile swings from its hook: at 0.06 the bottom letters move 3 LEDs either way, at 1 they move 25. + +const W = 64, H = 128, NP = W * H; +const { floor, ceil, round, min, max, sqrt, sin, cos, abs, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ss = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +function hsv(h, s, v) { + 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); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- Letters. Skeletons in a box one cap height tall (y down), width w, +// drawn with a round pen 4 LEDs wide into plates 22 LEDs tall. Straight +// strokes snap to LED edges so stems come out exactly 4 wide; curves don't. +// A letter weighs its ink, the LEDs its plate lights facing you: +// I 84, L 119, T 126, F 145, A 172, E 180, H 196, O 198, D 204, R 206, +// B 225, W 240, M 242. +function arc(cx, cy, rx, ry, a0, a1, n) { + const s = []; + for (let i = 0; i < n; i++) { + const t0 = a0 + ((a1 - a0) * i) / n, t1 = a0 + ((a1 - a0) * (i + 1)) / n; + s.push([cx + rx * cos(t0), cy + ry * sin(t0), cx + rx * cos(t1), cy + ry * sin(t1), 1]); + } + return s; +} +const HP = PI / 2; +const SK = { + A: [0.72, [[0, 1, 0.36, 0], [0.36, 0, 0.72, 1], [0.13, 0.64, 0.59, 0.64]]], + B: [0.56, [[0, 0, 0, 1], [0, 0, 0.3, 0], ...arc(0.3, 0.235, 0.22, 0.235, -HP, HP, 6), [0.3, 0.47, 0, 0.47], [0, 0.47, 0.32, 0.47], ...arc(0.32, 0.735, 0.24, 0.265, -HP, HP, 6), [0.32, 1, 0, 1]]], + D: [0.62, [[0, 0, 0, 1], [0, 0, 0.22, 0], ...arc(0.22, 0.5, 0.4, 0.5, -HP, HP, 10), [0.22, 1, 0, 1]]], + E: [0.52, [[0, 0, 0, 1], [0, 0, 0.52, 0], [0, 0.5, 0.44, 0.5], [0, 1, 0.52, 1]]], + F: [0.52, [[0, 0, 0, 1], [0, 0, 0.52, 0], [0, 0.5, 0.44, 0.5]]], + H: [0.62, [[0, 0, 0, 1], [0.62, 0, 0.62, 1], [0, 0.5, 0.62, 0.5]]], + I: [0, [[0, 0, 0, 1]]], + L: [0.5, [[0, 0, 0, 1], [0, 1, 0.5, 1]]], + M: [0.82, [[0, 1, 0, 0], [0, 0, 0.41, 0.66], [0.41, 0.66, 0.82, 0], [0.82, 0, 0.82, 1]]], + O: [0.72, arc(0.36, 0.5, 0.36, 0.5, 0, 2 * PI, 16)], + R: [0.58, [[0, 0, 0, 1], [0, 0, 0.3, 0], ...arc(0.3, 0.25, 0.24, 0.25, -HP, HP, 6), [0.3, 0.5, 0, 0.5], [0.26, 0.5, 0.58, 1]]], + T: [0.66, [[0, 0, 0.66, 0], [0.33, 0, 0.33, 1]]], + W: [0.94, [[0, 0, 0.21, 1], [0.21, 1, 0.47, 0.32], [0.47, 0.32, 0.73, 1], [0.73, 1, 0.94, 0]]], +}; +const GH = 22, PEN = 2, SC = GH - 2 * PEN; + +function glyph(ch) { + const [w, segs] = SK[ch], bw = ceil(w * SC + 2 * PEN) + 1, bits = new Uint8Array(bw * GH); + let x0 = bw, x1 = -1; + for (let j = 0; j < GH; j++) + for (let i = 0; i < bw; i++) { + const px = i + 0.5, py = j + 0.5; + let on = 0; + for (const [sx, sy, ex, ey, curved] of segs) { + const sn = (v) => (curved ? PEN + v * SC : floor(PEN + v * SC)); + const ax = sn(sx), ay = sn(sy), dx = sn(ex) - ax, dy = sn(ey) - ay, l2 = dx * dx + dy * dy; + const t = l2 ? cl(((px - ax) * dx + (py - ay) * dy) / l2, 0, 1) : 0; + const qx = ax + t * dx - px, qy = ay + t * dy - py; + if (qx * qx + qy * qy <= (PEN + 0.01) * (PEN + 0.01)) { + on = 1; + break; + } + } + if (on) (bits[j * bw + i] = 1), (x0 = min(x0, i)), (x1 = max(x1, i)); + } + // Trimmed to the ink, with a running sum per row for area sampling. + const gw = x1 - x0 + 1, g = new Uint8Array(gw * GH), cum = new Float32Array((gw + 1) * GH); + let ink = 0; + for (let j = 0; j < GH; j++) + for (let i = 0; i < gw; i++) { + const b = bits[j * bw + i + x0]; + g[j * gw + i] = b; + ink += b; + cum[j * (gw + 1) + i + 1] = cum[j * (gw + 1) + i] + b; + } + return { ch, gw, g, cum, ink }; +} +const GLYPHS = {}; +const glyphOf = (ch) => GLYPHS[ch] || (GLYPHS[ch] = glyph(ch)); + +// ---- Word ladders: one letter changes at a time, each step a word. +const LADDERS = [ + ["HEAD", "HEAL", "TEAL", "TELL", "TALL", "TAIL"], + ["FILL", "FILM", "FIRM", "FORM", "WORM", "WARM"], + ["WOMB", "TOMB", "TOME", "TIME", "TILE", "TILL"], +]; + +// ---- The mobile. Three bars in a chain, as Calder hung most of his: bar 0 +// carries letter 0 on its left end and everything below on its right, bar 1 +// carries that rest on its left and letter 1 on its right, bar 2 carries +// letters 2 and 3. Facing you the chain zigzags so the word reads down the +// panel. Each bar hangs from a loop D LEDs above it. +// Bar k: length and the thread below each end. The top hook hangs from +// HOOK. +const BAR = [ + { len: 28, drop: [3, 30] }, + { len: 26, drop: [30, 3] }, + { len: 32, drop: [11, 11] }, +]; +const HOOK = [30, 4], D = 7; + +// Weights on each end of bar k, given the four letters' ink. A sub-mobile +// weighs the sum of what hangs below it; wire and thread weigh nothing. +function loads(ink) { + const w2 = ink[2] + ink[3], w1 = ink[1] + w2; + return [[ink[0], w1], [w1, ink[1]], [ink[2], ink[3]]]; +} + +// The law of the lever: the hook divides the bar so that left weight times +// left arm equals right weight times right arm, so it sits at the fraction +// WR / (WL + WR) of the bar from the left end. +const balance = ([wl, wr]) => wr / (wl + wr); + +// Turning: each bar twists to and fro on its thread (amplitude, period at +// Speed 2, phase). +const TWIST = [[1.0, 17, -0.4], [1.25, 13, PI - 0.5], [1.75, 21, -0.35]]; + +// Lamp up and to the left in front of the mobile, the wall 26 LEDs behind +// the threads. +const LX = -80, LY = -120, LZ = 300, WZ = -26; +const STEP = 3.6, FLIP = 0.7, SLIDE_AT = 0.35, SLIDE = 1.1; + +function startWord(S, ladder) { + S.ladder = ladder; + S.step = 0; + S.dir = 1; + const word = LADDERS[ladder][0]; + S.letters = [0, 1, 2, 3].map((i) => ({ g: glyphOf(word[i]), next: null, flip: -1 })); + const L = loads(S.letters.map((l) => l.g.ink)); + S.bars = BAR.map((b, k) => ({ f: balance(L[k]), f0: 0, f1: 0, slide: -1, a: 0, av: 0 })); + S.clock = STEP - 2.1; +} + +export function setup(params) { + params.s = { t: 0, hue: 0, sway: 0.06, ladder: -1, fb: new Float32Array(NP * 3), sh: new Float32Array(NP), tmp: new Float32Array(NP), wall: null }; +} + +function ink(S) { + return S.letters.map((l) => l.g.ink); +} + +function nextWord(S) { + const lad = LADDERS[S.ladder]; + if (S.step + S.dir < 0 || S.step + S.dir >= lad.length) S.dir = -S.dir; + const from = lad[S.step], to = lad[S.step + S.dir]; + S.step += S.dir; + for (let i = 0; i < 4; i++) + if (from[i] !== to[i]) { + S.letters[i].next = glyphOf(to[i]); + S.letters[i].flip = 0; + } +} + +// The bar's tilt a about its hook. With the hook D above the bar, the weights +// hanging from the ends at arms aL, aR turn it by +// (WR aR - WL aL) cos a - (WL + WR) D sin a, +// so a bar out of balance by a moment M settles at tan a = M / (W D), and a +// balanced bar hangs level. +function torque(b, len, [wl, wr]) { + const al = b.f * len, ar = (1 - b.f) * len; + return { tq: (wr * ar - wl * al) * cos(b.a) - (wl + wr) * D * sin(b.a), I: wl * (al * al + D * D) + wr * (ar * ar + D * D) }; +} + +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.sway = cl(k[3], 0, 1); + const ladder = cl(round(k[2]), 0, 2); + if (ladder !== S.ladder) startWord(S, ladder); + const step = (d * cl(k[1], 0.2, 3)) / 2; + S.t += step; + S.clock += step; + if (S.clock >= STEP) { + S.clock -= STEP; + nextWord(S); + } + // A letter that changes spins once round on its thread and comes back as + // the new letter; its weight changes when it is edge-on, a quarter in. + let changed = false; + for (const l of S.letters) { + if (l.flip < 0) continue; + const was = l.flip; + l.flip += step / FLIP; + if (was < 0.25 && l.flip >= 0.25 && l.next) { + l.g = l.next; + l.next = null; + changed = true; + } + if (l.flip >= 1) l.flip = -1; + } + const L = loads(ink(S)); + if (changed) for (const b of S.bars) b.slide = 0; + // The hooks slide to the new balance once the bars have tipped. + S.bars.forEach((b, i) => { + if (b.slide < 0) return; + if (b.slide < SLIDE_AT && b.slide + step >= SLIDE_AT) b.f0 = b.f; + b.slide += step; + if (b.slide >= SLIDE_AT) { + const u = ss((b.slide - SLIDE_AT) / SLIDE); + b.f = b.f0 + (balance(L[i]) - b.f0) * u; + if (u >= 1) b.slide = -1; + } + }); + // Tilts, in four substeps. G sets the swing: a level bar knocked off + // rocks back about once in 1.4 s. + const G = 1300, DAMP = 1.5, n = 4, h = step / n; + for (let s = 0; s < n; s++) + S.bars.forEach((b, i) => { + const { tq, I } = torque(b, BAR[i].len, L[i]); + b.av += (G * tq / I - DAMP * b.av) * h; + b.a += b.av * h; + if (abs(b.a) > 0.75) (b.a = 0.75 * Math.sign(b.a)), (b.av = 0); + }); +} + +// ---- Geometry: points in LEDs, x right, y down, z towards you. +function layout(S) { + const t = S.t, phi = (0.012 + 0.2 * S.sway) * sin((2 * PI * t) / 3.3); + // The whole mobile swings about its hook at the top edge. + const sw = (p) => [HOOK[0] + (p[0] - HOOK[0]) * cos(phi) - p[1] * sin(phi), (p[0] - HOOK[0]) * sin(phi) + p[1] * cos(phi), p[2]]; + const lines = [], plates = []; + let piv = [HOOK[0], HOOK[1], 0]; + lines.push({ p: [HOOK[0], -1, 0], q: piv, c: 0, glint: 0 }); + for (let k = 0; k < 3; k++) { + const B = BAR[k], b = S.bars[k], [amp, per, ph] = TWIST[k]; + const th = amp * sin((2 * PI * t) / per + ph), ex = cos(th), ez = sin(th), ca = cos(b.a), sa = sin(b.a); + const bx = ca * ex, by = sa, bz = ca * ez; + // Hook, then the bar's own point under it, then the ends. + const hp = [piv[0] - sa * ex * D, piv[1] + ca * D, piv[2] - sa * ez * D]; + const al = b.f * B.len, ar = (1 - b.f) * B.len; + const le = [hp[0] - bx * al, hp[1] - by * al, hp[2] - bz * al], re = [hp[0] + bx * ar, hp[1] + by * ar, hp[2] + bz * ar]; + // The hook glints while it slides along the bar. + const sl = b.slide >= SLIDE_AT ? sin(PI * cl((b.slide - SLIDE_AT) / SLIDE, 0, 1)) : 0; + lines.push({ p: piv, q: hp, c: 1, glint: sl }, { p: le, q: re, c: 1, glint: 0 }); + const ends = [le, re]; + for (let side = 0; side < 2; side++) { + const e = ends[side], drop = B.drop[side], lo = [e[0], e[1] + drop, e[2]]; + lines.push({ p: e, q: lo, c: 0, glint: 0 }); + const li = k === 0 ? (side === 0 ? 0 : -1) : k === 1 ? (side === 1 ? 1 : -1) : 2 + side; + if (li < 0) piv = lo; + else { + const l = S.letters[li], fl = l.flip >= 0 ? 2 * PI * ss(l.flip) : 0; + plates.push({ top: lo, th: th + fl, l, pos: li }); + } + } + } + for (const L of lines) (L.p = sw(L.p)), (L.q = sw(L.q)); + for (const P of plates) P.top = sw(P.top); + return { lines, plates }; +} + +// Where a point lands on the wall, seen from the lamp. +function onWall(p) { + const s = (WZ - LZ) / (p[2] - LZ); + return [LX + s * (p[0] - LX), LY + s * (p[1] - LY)]; +} + +// A line laid down as samples a third of an LED apart, each shared among the +// four LEDs round it. +function put(buf, X, Y, v) { + if (X >= 0 && X < W && Y >= 0 && Y < H) buf[Y * W + X] += v; +} +function splatLine(buf, x0, y0, x1, y1, a) { + const n = max(2, ceil(sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)) * 3)), w = a / 3; + for (let i = 0; i <= n; i++) { + const x = x0 + ((x1 - x0) * i) / n - 0.5, y = y0 + ((y1 - y0) * i) / n - 0.5, ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + put(buf, ix, iy, w * (1 - fx) * (1 - fy)); + put(buf, ix + 1, iy, w * fx * (1 - fy)); + put(buf, ix, iy + 1, w * (1 - fx) * fy); + put(buf, ix + 1, iy + 1, w * fx * fy); + } +} + +// Ink in glyph row j left of column u. +function inkTo(g, j, u) { + if (u <= 0) return 0; + const gw = g.gw; + if (u >= gw) return g.cum[j * (gw + 1) + gw]; + const i = floor(u); + return g.cum[j * (gw + 1) + i] + (u - i) * g.g[j * gw + i]; +} + +// Shadows: every plate, wire and thread, cast by the lamp on the wall. +function castShadows(sh, lines, plates) { + sh.fill(0); + for (const L of lines) { + const a = onWall(L.p), b = onWall(L.q); + splatLine(sh, a[0], a[1], b[0], b[1], 0.9); + } + for (const P of plates) { + const g = P.l.g, gw = g.gw, ex = cos(P.th), ez = sin(P.th), nx = -ez, nz = ex, T = P.top; + let x0 = W, x1 = 0, y0 = H, y1 = 0; + for (let k = 0; k < 4; k++) { + const u = (k & 1 ? 0.5 : -0.5) * gw, c = onWall([T[0] + u * ex, T[1] + (k & 2 ? GH : 0), T[2] + u * ez]); + x0 = min(x0, c[0]); + x1 = max(x1, c[0]); + y0 = min(y0, c[1]); + y1 = max(y1, c[1]); + } + const nT = nx * (T[0] - LX) + nz * (T[2] - LZ), Dz = WZ - LZ; + for (let y = max(0, floor(y0)); y <= min(H - 1, ceil(y1)); y++) + for (let x = max(0, floor(x0)); x <= min(W - 1, ceil(x1)); x++) { + // The ray from the lamp to this LED of the wall, met with the plate. + const Dx = x + 0.5 - LX, Dy = y + 0.5 - LY, den = nx * Dx + nz * Dz; + if (abs(den) < 1e-6) continue; + const s = nT / den, qx = LX + s * Dx - T[0], qy = LY + s * Dy - T[1], qz = LZ + s * Dz - T[2]; + const u = floor(qx * ex + qz * ez + gw / 2), v = floor(qy); + if (u >= 0 && u < gw && v >= 0 && v < GH && g.g[v * gw + u]) sh[y * W + x] = 1; + } + } +} + +// Soften the shadow edge by one LED (the lamp is not a point) and lay the +// wall, under a spot aimed a little above the middle. +function paintWall(S) { + const { fb, sh, tmp } = S; + if (!S.wall) { + S.wall = new Float32Array(NP); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const r2 = (x - 30) * (x - 30) + (y - 52) * (y - 52) * 0.55; + S.wall[y * W + x] = 0.105 + 0.13 * exp(-r2 / 2600); + } + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + tmp[p] = (sh[p] * 2 + (x > 0 ? sh[p - 1] : sh[p]) + (x < W - 1 ? sh[p + 1] : sh[p])) / 4; + } + const wallC = hsv(0.58 + S.hue, 0.16, 1), wall = S.wall; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, s = min(1, (tmp[p] * 2 + (y > 0 ? tmp[p - W] : tmp[p]) + (y < H - 1 ? tmp[p + W] : tmp[p])) / 4); + const v = wall[p] * (1 - 0.62 * s); + fb[p * 3] = wallC[0] * v; + fb[p * 3 + 1] = wallC[1] * v; + fb[p * 3 + 2] = wallC[2] * v; + } +} + +function paintLine(fb, cov, p, q, wgt, col) { + splatLine(cov, p[0], p[1], q[0], q[1], wgt); + const xa = max(0, floor(min(p[0], q[0])) - 2), xb = min(W - 1, ceil(max(p[0], q[0])) + 2); + const ya = max(0, floor(min(p[1], q[1])) - 2), yb = min(H - 1, ceil(max(p[1], q[1])) + 2); + for (let y = ya; y <= yb; y++) + for (let x = xa; x <= xb; x++) { + const pi = y * W + x, a = min(1, cov[pi]); + if (a <= 0) continue; + cov[pi] = 0; + for (let ch = 0; ch < 3; ch++) fb[pi * 3 + ch] += (col[ch] - fb[pi * 3 + ch]) * a; + } +} + +// A plate, foreshortened by its turn: each LED takes the share of the glyph +// it covers, so a plate turned edge-on thins to its edge and turned past it +// shows its back, the letter mirrored. +function paintPlate(fb, g, T, th, base) { + const gw = g.gw, ex = cos(th), ez = sin(th), c = abs(ex) < 0.07 ? (ex < 0 ? -0.07 : 0.07) : ex, ac = abs(c); + // Lit by the lamp on whichever face it turns to it. + const cx = T[0], lx = LX - cx, ly = LY - T[1] - GH / 2, lz = LZ - T[2], ll = sqrt(lx * lx + ly * ly + lz * lz); + const shade = 0.5 + 0.5 * abs((-ez * lx + ex * lz) / ll), c0 = base[0] * shade, c1 = base[1] * shade, c2 = base[2] * shade; + const half = (gw / 2) * ac, xa = max(0, floor(cx - half)), xb = min(W - 1, ceil(cx + half)); + const ya = max(0, floor(T[1])), yb = min(H - 1, ceil(T[1] + GH)); + for (let x = xa; x <= xb; x++) { + let u0 = (x - cx) / c + gw / 2, u1 = (x + 1 - cx) / c + gw / 2; + if (u0 > u1) { + const t = u0; + u0 = u1; + u1 = t; + } + for (let y = ya; y <= yb; y++) { + // Rows blend where the plate sits between LEDs. + const v = y - T[1], j0 = floor(v), fv = v - j0; + let a = 0; + if (j0 >= 0 && j0 < GH) a += (1 - fv) * (inkTo(g, j0, u1) - inkTo(g, j0, u0)); + if (j0 + 1 >= 0 && j0 + 1 < GH) a += fv * (inkTo(g, j0 + 1, u1) - inkTo(g, j0 + 1, u0)); + a = min(1, a * ac); + if (a <= 0) continue; + const o = (y * W + x) * 3; + fb[o] += (c0 - fb[o]) * a; + fb[o + 1] += (c1 - fb[o + 1]) * a; + fb[o + 2] += (c2 - fb[o + 2]) * a; + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.letters) return; + const { fb, sh, tmp } = S; + const { lines, plates } = layout(S); + castShadows(sh, lines, plates); + paintWall(S); + // The mobile, far things first. + const items = []; + for (const L of lines) items.push({ z: (L.p[2] + L.q[2]) / 2, L, P: null }); + for (const P of plates) items.push({ z: P.top[2], L: null, P }); + items.sort((a, b) => a.z - b.z); + const steel = [0.62, 0.64, 0.68], thread = [0.36, 0.36, 0.38]; + const pal = [hsv(0.14 + S.hue, 0.85, 1), hsv(0.6 + S.hue, 0.8, 1), hsv(0.995 + S.hue, 0.85, 1), hsv(0.6 + S.hue, 0.07, 1)]; + tmp.fill(0); + for (const it of items) { + if (it.L) { + const { p, q, c, glint } = it.L; + paintLine(fb, tmp, p, q, c ? 1.15 : 0.8, glint ? steel.map((v) => v + (1 - v) * glint) : c ? steel : thread); + } else paintPlate(fb, it.P.l.g, it.P.top, it.P.th, pal[it.P.pos]); + } + for (let p = 0; p < NP; p++) + display.setPixel(p % W, floor(p / W), min(255, fb[p * 3] * 255) | 0, min(255, fb[p * 3 + 1] * 255) | 0, min(255, fb[p * 3 + 2] * 255) | 0); +} From bbce0d88b094819a20d7678c5b0ae6cac7ca8b3e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:57:17 +0000 Subject: [PATCH 09/22] =?UTF-8?q?Prandtl's=20Membrane=20=EC=9D=98=20?= =?UTF-8?q?=EB=B9=9B=EA=B3=BC=20=EC=83=89=EC=9D=84=20=EA=B3=A0=EC=B9=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PBtzYrhzPXCgRY4TG --- .../patterns/prandtl-membrane.js | 72 +++++++++++-------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/incoming/pattern-study/patterns/prandtl-membrane.js b/incoming/pattern-study/patterns/prandtl-membrane.js index 33cd97b71..bcb7355b9 100644 --- a/incoming/pattern-study/patterns/prandtl-membrane.js +++ b/incoming/pattern-study/patterns/prandtl-membrane.js @@ -1,20 +1,20 @@ // ===== Patternflow pattern ===== // Title: Prandtl's Membrane // Family: type -// Idea: A bar whose cross-section is a word is twisted, and each letter is drawn as Prandtl's soap film stretched over a hole of its shape and blown up by pressure, so the letters breathe in and out as pillows and plateaus lit by a walking lamp, coloured by the shear stress that runs hot along their rims and hottest in their inside corners. +// Idea: A bar whose cross-section is a word is twisted, and each letter is Prandtl's film stretched over a hole of its shape and blown up from below, so the letters breathe as lit pillows and hollow letters rise as plateaus round a flat plate, coloured by the shear stress, which is zero along each crest, runs hot along the rims and hottest in the inside corners. // Author: patternflow-lab // Date: 2026-10-30 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: torsion of prismatic bars (Adhémar Barré de Saint-Venant, 1855), the membrane analogy (Ludwig Prandtl, 1903), thin-walled tubes (Rudolf Bredt, 1896), pattern round 75 +// Lineage: torsion of prismatic bars (Adhémar Barré de Saint-Venant, 1855), the membrane analogy (Ludwig Prandtl, 1903), thin-walled tubes (Rudolf Bredt, 1896), the Shortley-Weller boundary stencil (George Shortley and Royal Weller, 1938), successive over-relaxation (David Young, 1950), pattern round 75 // Round: 75 // =============================== // @matrix 64x128 // @knobs Hue=0..1, Twist=0.2..3, Word=0..3, Lamp=0..1 // -// Knob 1: Hue. Turns the stress colours round the wheel: at 0 the unstressed crests are blue, the rims run through magenta and red, and the inside corners burn orange to yellow. -// Knob 2: Twist. How hard the bar is twisted, which is how hard the film is blown up. At 2 the film rises to 60 degrees at a 12 LED stem's rim; at 0.2 it barely leaves the outline and everything stays blue; at 3 the walls stand steep and the rims run yellow. Each breath swings the twist between 40% and 100% of this. -// Knob 3: Word. The cross-section: the syllable 숨 (breath) the height of the panel, then LIT, AIR and HOT stacked. A letter with a hole is a hollow section, and its hole is a flat plate the film lifts. -// Knob 4: Lamp. How high the lamp stands as it walks round the word once in 12 s: at 0.06 it rakes the film from 15 degrees up and each stem is lit on one side and dark on the other; at 1 it stands at 75 degrees and the film glows almost evenly. +// Knob 1: Hue. Turns the stress colours round the wheel: at 0 the crests, where the stress is zero, are blue, the rims run through magenta and red, and stress past the hot end burns orange to yellow. The plates in the holes take the blue, dim. +// Knob 2: Twist. How hard the bar is twisted, which is how hard the film is blown up. Each breath, one every 4 s, swings the twist from 40% to 100% of the knob. At 2 the film stands at 56 degrees at the rim of the 14 LED bar of ㅜ at the top of a breath and 31 at the bottom, and the colour reaches its hot end at a stress of 12.7. At 0.2 the film over that bar tilts 9 degrees at most and the colours stay blue and violet; at 3 the same rim stands at 66 degrees and runs yellow. +// Knob 3: Word. The cross-section: the syllable 숨 (breath) the height of the panel, then LIT, AIR and HOT, three capitals 56 by 38 LEDs stacked. A letter with a hole is a hollow section, and the hole is a flat plate the film lifts: at the same twist HOT carries four times the torque of LIT. +// Knob 4: Lamp. How high the lamp stands as it walks round the word once in 12 s: 19 degrees at 0.06, low enough that each stroke is lit on one side and shaded on the other and a glint runs along the crests; 75 degrees at 1, where the film is lit almost evenly and only the colours move. const W = 64, H = 128; const { floor, round, sqrt, cos, sin, min, max, abs, PI } = Math; @@ -28,11 +28,12 @@ function hsv(h, s, v, o, k) { o[k + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; } -// ---- Letters, as unions of convex polygons in LEDs. Latin capitals sit in -// a box 56 wide and 38 tall, stems 13 thick and bars 7 to 11; the syllable -// 숨 is drawn for the whole panel. Corners are left sharp on purpose: the -// film has zero slope at an outside corner and an infinite one at an inside -// corner, and both should show. +// ---- Letters, as unions of convex polygons in LEDs. The capitals sit in a +// box 56 wide and 38 tall, stems 13 thick and bars 7 to 11, at rows 3, 46 +// and 89; the syllable 숨 fills the panel, ㅅ in rows 2 to 46, ㅜ in 51 to +// 81 and ㅁ, a hollow box with walls 14 to 15 thick, in 85 to 126. Corners +// are left sharp: the film is flat at an outside corner and infinitely steep +// at an inside one, and both should show. const R = (a, b, c, d) => [[a, b], [c, b], [c, d], [a, d]]; const TOP = [[0, 9], [9, 0], [47, 0], [56, 9], [56, 11], [0, 11]]; const LAT = { @@ -77,22 +78,31 @@ function edgesOf(poly) { } // ---- The film. Saint-Venant's stress function phi solves lap(phi) = -2 -// (twist and shear modulus taken as 1) inside the section, phi = 0 on its -// outline, and the shear stress is |grad phi|. Prandtl saw the same equation -// in a film stretched over a hole of that shape and blown up from below. It -// is solved on a grid of half LEDs, 128 by 256 nodes at ((i + 1/2)/2, -// (j + 1/2)/2), by successive over-relaxation, a few sweeps a frame until it -// settles, so a new word inflates as the solver relaxes. Near the outline -// each node uses the Shortley-Weller stencil with its true distance to the -// edge along the grid line, so a slanted or curved outline is not a -// staircase. A hole in a letter makes a hollow section: phi is constant on -// the hole's edge, a flat plate the film lifts, and its height K is set by -// the plate's balance (pressure on the plate's area against the film's pull -// round its rim), which is Bredt's condition that the stress circulates -// round the hole twice the hole's area. Checked in Node: for a circle of -// radius 20 the solver gives phi = (a^2 - r^2)/2 to within 0.3% and the -// stress at the rim is a to within 2%; a 40 by 8 strip has its largest -// stress at the middle of a long side. +// (twist and shear modulus taken as 1) inside the section with phi = 0 on +// its outline, the shear stress is |grad phi| and the torque is twice the +// volume under phi. Prandtl saw the same equation in a film stretched over a +// hole of that shape and blown up from below. It is solved on a grid of half +// LEDs, 128 by 256 nodes at ((i + 1/2)/2, (j + 1/2)/2), by successive +// over-relaxation, ten sweeps a frame until no node moves by more than 0.01, +// so a new word inflates as the solver relaxes: 116 to 144 sweeps, half a +// second. Next to the outline a node uses the Shortley-Weller stencil with +// its true distance to the edge along each grid line, so a slanted outline is +// not a staircase. A hole makes a hollow section: phi is constant round the +// hole, a weightless flat plate the film lifts, and the plate settles where +// the pressure on its area balances the film's pull round its rim. That is +// the condition that the stress circulates round the hole twice the hole's +// area, the one Bredt's formula for thin tubes rests on. +// +// Checked in Node with this solver: on a circle of radius 20 the film peaks +// at 199.79 against a^2/2 = 200, phi is within 0.18 of (a^2 - r^2)/2 +// everywhere, and the largest stress is 19.98 against a = 20, at the rim. On +// a 40 by 8 strip the largest stress is at the middle of a long side, 7.50 +// at the node a quarter LED in from the edge, where the strip's 2y gives 7.5. +// In a square tube 40 across with walls 6 the plate stands at 108.7 against +// 102 from Bredt's thin-wall formula, and the stress in the middle of a wall +// is 17.6 against Bredt's 17.0. The walls of 숨's ㅁ are too thick for Bredt: +// its plate stands at 268, and across the bottom wall the stress climbs from +// 6 at the plate to 31 at the outline. const FW = 128, FH = 256, NN = FW * FH, HS = 0.5; function buildFilm(word) { // Each polygon is measured only over its own box and 2 LEDs round it; @@ -270,8 +280,8 @@ export function draw(display, params) { let hx = lx, hy = ly, hz = lz + 1; const hl = sqrt(hx * hx + hy * hy + hz * hz); (hx /= hl), (hy /= hl), (hz /= hl); - const kt = P / 13; // the stress at which the colour reaches the hot end is 13 / P - const plate = 0.5 * (0.5 + 0.5 * lz); + const kt = P / 11; // the stress at which the colour reaches the hot end is 11 / P + const plate = 0.6 * (0.5 + 0.5 * lz); for (let p = 0, y = 0; y < H; y++) for (let x = 0; x < W; x++, p++) { const n = lin[p]; @@ -282,8 +292,8 @@ export function draw(display, params) { spec *= spec * spec * spec; spec *= spec * spec; // nh^24 const tau = min(1.2, sqrt(gx * gx + gy * gy) * kt); - const v = (0.4 + 0.3 * (1 + dif) + 0.5 * spec) * (0.6 + 0.6 * tau) * n * 0.25; - hsv(0.62 + S.hue + 0.45 * min(1.15, tau), 1, v > 1 ? 1 : v, rgb, 0); + const v = (0.4 + 0.3 * (1 + dif)) * (0.6 + 0.6 * tau) + 0.7 * spec * n * 0.25; + hsv(0.62 + S.hue + 0.45 * min(1.15, tau), 1 - 0.3 * spec, v > 1 ? 1 : v, rgb, 0); display.setPixel(x, y, round(255 * rgb[0]), round(255 * rgb[1]), round(255 * rgb[2])); } else if (lplate[p]) { hsv(0.62 + S.hue, 1, plate * lplate[p] * 0.25, rgb, 0); From 388905ccaea8f8c08db95e39f85ea5130688996c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:57:27 +0000 Subject: [PATCH 10/22] =?UTF-8?q?Kruskal=20Count=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PBtzYrhzPXCgRY4TG --- .../pattern-study/patterns/kruskal-count.js | 325 ++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 incoming/pattern-study/patterns/kruskal-count.js diff --git a/incoming/pattern-study/patterns/kruskal-count.js b/incoming/pattern-study/patterns/kruskal-count.js new file mode 100644 index 000000000..e2e59a15a --- /dev/null +++ b/incoming/pattern-study/patterns/kruskal-count.js @@ -0,0 +1,325 @@ +// ===== Patternflow pattern ===== +// Title: Kruskal Count +// Family: line +// Idea: A shuffled deck is laid down the panel in a snake of rows, a chain of its own colour starts on each of the first ten cards and a reading head sweeps the deck from top to bottom, so every chain it passes hops forward by the value of its card, chains that land on the same card merge and pale together, and at the bottom the card where they end is turned up large before the deck is shuffled again. +// Author: patternflow-lab +// Date: 2026-10-30 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the Kruskal count (Martin Kruskal, 1970s), as Martin Gardner gave it in Mathematical Games (Scientific American, 1978), analysed by Jeffrey Lagarias, Eric Rains and Robert Vanderbei (The Kruskal Count, 2001), pattern round 75 +// Round: 75 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Court=5..10, Decks=1..2 +// +// Knob 1: Hue. Turns the ten chain colours round the wheel together; at 0 the first card's chain is cyan and the rest follow in steps of a tenth of the wheel. +// Knob 2: Speed. At 2 the head reads 8 cards a second, so a deal takes 6.5 s, the turned-up card stays 2.2 s and the shuffle 0.8 s. At 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Court. What a jack, queen or king counts, from 5 as in Gardner's telling to 10 as in blackjack. The mean jump grows from 5.4 to 6.5 cards and fewer chains meet: with one deck all ten merge in 58% of deals at 5 and 28% at 10. +// Knob 4: Decks. One deck of 52 cards in 13 rows of 4 with suits, or from 1.5 two decks of 104 in 13 rows of 8 with the suit shown only by the colour of the rank. All ten chains merge in 94% of two-deck deals at Court 5. Turning it deals again. + +const W = 64, H = 128, NP = W * H; +const { floor, round, min, max, abs, sqrt, exp, PI, sin } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ease = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +function hsv(h, s, v) { + 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); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- Card faces. Ranks in a 3 by 5 font, one row a number with the high +// bit on the left: A, 2 to 9, then the 1 and 0 of 10, J, Q, K. The 1 of 10 +// is one column wide so "10" fits in 5. +const RANK = [ + [2, 5, 7, 5, 5], [6, 1, 2, 4, 7], [6, 1, 2, 1, 6], [5, 5, 7, 1, 1], [7, 4, 6, 1, 6], [3, 4, 6, 5, 2], [7, 1, 2, 2, 2], [2, 5, 2, 5, 2], [2, 5, 3, 1, 6], + null, [1, 1, 1, 5, 2], [2, 5, 5, 6, 3], [5, 6, 4, 6, 5], +]; +const ZERO = [2, 5, 5, 5, 2]; +// Suits on 5 by 5: spades, hearts, diamonds, clubs. +const SUIT = [ + [4, 14, 31, 31, 4], + [10, 31, 31, 14, 4], + [4, 14, 31, 14, 4], + [14, 14, 31, 21, 4], +]; +const RED = [false, true, true, false]; + +// A rank's glyphs as [bits, width, dx]: 10 is a narrow 1 and a 0. +const GLYPHS = [null]; +for (let r = 1; r <= 13; r++) GLYPHS.push(r === 10 ? [[[1, 1, 1, 1, 1], 1, 0], [ZERO, 3, 2]] : [[RANK[r - 1], 3, 0]]); + +const valueOf = (r, court) => (r > 10 ? court : r); + +// ---- The deal and the sweep. A chain is a token on a card. The head reads +// the cards in order; when it reaches a token the token jumps forward by +// that card's value, and if a token already waits where it lands the two +// become one. Every token is always ahead of the head, so a chain can only +// meet another on a card the other has not left yet. A token whose jump +// would leave the deck stays: its card is that chain's last card. Counted +// in Node over 100000 deals: with one deck and Court 5 all ten chains end +// on one card in 58.2% of deals and the classic trick, a chain from the +// first card against one from a card picked among the first ten, works in +// 85.3%; at Court 10 it is 28.3% and 72.0%. With two decks at Court 5 all +// ten merge in 93.8%. +const CHAINS = 10; +const DEAL = 5.0, SHOW = 2.2, SHUFFLE = 0.8; + +function deal(S) { + const n = 52 * S.decks, cards = []; + for (let k = 0; k < S.decks; k++) for (let s = 0; s < 4; s++) for (let r = 1; r <= 13; r++) cards.push({ r, s }); + for (let i = n - 1; i > 0; i--) { + const j = floor(Math.random() * (i + 1)), t = cards[i]; + cards[i] = cards[j]; + cards[j] = t; + } + S.n = n; + S.cards = cards; + S.cols = S.decks === 1 ? 4 : 8; + // Tokens by card; trail colour index by card (-1 unvisited). + S.tok = new Array(n).fill(null); + S.trail = new Array(n).fill(null); + for (let i = 0; i < CHAINS; i++) { + S.tok[i] = { members: [i], id: i }; + S.trail[i] = S.tok[i].members.slice(); + } + S.arcs = []; + S.flights = []; + S.bursts = []; + S.head = -1; + S.phase = 0; + S.t = 0; + S.final = null; +} + +// Where a card sits: a snake of 13 rows, left to right then right to left. +function place(C, i) { + const row = floor(i / C), q = i % C, col = row % 2 ? C - 1 - q : q; + const cw = W / C, ch = H / 13, fw = cw - 2; + const x = col * cw + 1, y = round(row * ch + (ch - 7) / 2); + return { x, y, w: fw, cx: x + fw / 2 - 0.5, cy: y + 3 }; +} +const PLACES = { 4: [], 8: [] }; +for (let i = 0; i < 52; i++) PLACES[4].push(place(4, i)); +for (let i = 0; i < 104; i++) PLACES[8].push(place(8, i)); +const cell = (S, i) => PLACES[S.cols][i]; + +// A hop's path: a quadratic curve from card centre to card centre, bowed +// up (or to the right for a straight drop) by 0.3 of its length. +function bez(a, b, u) { + const dx = b.cx - a.cx, dy = b.cy - a.cy, L = sqrt(dx * dx + dy * dy) || 1; + let px = dy / L, py = -dx / L; + if (py > 0) ((px = -px), (py = -py)); + const k = 0.3 * L, mx = (a.cx + b.cx) / 2 + px * k, my = (a.cy + b.cy) / 2 + py * k, v = 1 - u; + return [v * v * a.cx + 2 * v * u * mx + u * u * b.cx, v * v * a.cy + 2 * v * u * my + u * u * b.cy, L]; +} +// Its LEDs in order, each once. +function arcPixels(a, b) { + const L = bez(a, b, 0)[2], steps = Math.ceil(L * 1.6) + 2, out = []; + let last = -1; + for (let s = 0; s <= steps; s++) { + const [x, y] = bez(a, b, s / steps), xi = round(x), yi = round(y); + if (xi < 0 || yi < 0 || xi >= W || yi >= H) continue; + const q = yi * W + xi; + if (q !== last) (out.push(q), (last = q)); + } + return out; +} + +function step(S) { + const h = ++S.head; + if (h >= S.n) return; + const tk = S.tok[h]; + if (!tk) return; + const t = h + valueOf(S.cards[h].r, S.court); + if (t >= S.n) return; + S.tok[h] = null; + const dur = min(0.4, valueOf(S.cards[h].r, S.court) * S.dt0 * 0.9); + const flight = { from: h, to: t, members: tk.members.slice(), age: 0, dur }; + S.flights.push(flight); + S.arcs.push({ members: flight.members, pts: arcPixels(cell(S, h), cell(S, t)), flight }); + const there = S.tok[t]; + if (there) { + there.members = there.members.concat(tk.members).sort((a, b) => a - b); + S.bursts.push({ at: t, age: -dur, members: there.members.slice() }); + } else S.tok[t] = { members: tk.members.slice(), id: tk.id }; + S.trail[t] = S.tok[t].members.slice(); +} + +export function setup(params) { + params.s = { hue: 0, speed: 2, court: 5, decks: 1, dt0: DEAL / 52, rgb: new Float32Array(NP * 3) }; + deal(params.s); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 5, 1], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.court = cl(round(k[2]), 5, 10); + const decks = cl(k[3], 1, 2) >= 1.5 ? 2 : 1; + if (decks !== S.decks) ((S.decks = decks), deal(S)); + S.dt0 = DEAL / S.n; + const g = (d * S.speed) / 2; + S.t += g; + for (const f of S.flights) f.age += g; + for (const b of S.bursts) b.age += g; + S.flights = S.flights.filter((f) => f.age < f.dur); + S.bursts = S.bursts.filter((b) => b.age < 0.6); + if (S.phase === 0) { + while (S.t >= S.dt0 && S.head < S.n - 1) { + S.t -= S.dt0; + step(S); + } + if (S.head >= S.n - 1 && !S.flights.length) { + // Where the chains ended, the card holding most of them first. + const ends = []; + for (let i = 0; i < S.n; i++) if (S.tok[i]) ends.push({ at: i, members: S.tok[i].members }); + ends.sort((a, b) => b.members.length - a.members.length || a.at - b.at); + S.final = ends; + S.phase = 1; + S.t = 0; + } + } else if (S.phase === 1 && S.t >= SHOW) ((S.phase = 2), (S.t = 0)); + else if (S.phase === 2 && S.t >= SHUFFLE) deal(S); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const rgb = S.rgb; + rgb.fill(0); + const put = (x, y, c, a) => { + if (x < 0 || y < 0 || x >= W || y >= H || a <= 0.004) return; + const o = (y * W + x) * 3; + rgb[o] = max(rgb[o], c[0] * a); + rgb[o + 1] = max(rgb[o + 1], c[1] * a); + rgb[o + 2] = max(rgb[o + 2], c[2] * a); + }; + const set = (x, y, c, a) => { + if (x < 0 || y < 0 || x >= W || y >= H) return; + const o = (y * W + x) * 3; + rgb[o] = c[0] * a; + rgb[o + 1] = c[1] * a; + rgb[o + 2] = c[2] * a; + }; + const bits = (rows, w, x, y, c, a, sc, plot) => { + for (let r = 0; r < rows.length; r++) + for (let i = 0; i < w; i++) + if ((rows[r] >> (w - 1 - i)) & 1) for (let v = 0; v < sc; v++) for (let u = 0; u < sc; u++) plot(x + i * sc + u, y + r * sc + v, c, a); + }; + + const chainCol = []; + for (let i = 0; i < CHAINS; i++) chainCol.push(hsv(S.hue + 0.5 + i / CHAINS, 0.85, 1)); + // A group's colour: its members' mean, paled towards white as it grows. + const memo = new Map(); + const groupCol = (m) => { + const key = m.key || (m.key = m.join(",")); + if (memo.has(key)) return memo.get(key); + const c = [0, 0, 0]; + for (const i of m) for (let q = 0; q < 3; q++) c[q] += chainCol[i][q] / m.length; + const top = max(c[0], c[1], c[2], 1e-3), w = (m.length - 1) / (CHAINS - 1); + const out = c.map((v) => (v / top) * (1 - w * 0.75) + w * 0.75); + memo.set(key, out); + return out; + }; + const ivory = [1, 0.93, 0.8], dim = S.phase === 1 ? 1 - 0.55 * ease(S.t / 0.4) : 1; + const fade = S.phase === 2 ? 1 - ease(S.t / SHUFFLE) : 1; + const redC = hsv(0.98, 0.6, 0.95), blackC = hsv(0.6, 0.18, 0.8); + + // ---- Cards. + for (let i = 0; i < S.n; i++) { + const p = cell(S, i), card = S.cards[i]; + const tk = S.tok[i], tr = S.trail[i]; + let face = ivory, fa = 0.13, ink = 0.8, dark = false; + if (tr && fade > 0) { + face = groupCol(tr); + fa = 0.13 + (0.55 - 0.13) * fade; + if (tk) ((fa = 0.13 + (0.85 - 0.13) * fade), (dark = fade > 0.5)); + } + const f0 = face[0] * fa * dim, f1 = face[1] * fa * dim, f2 = face[2] * fa * dim; + for (let y = 0; y < 7; y++) + for (let x = y === 0 || y === 6 ? 1 : 0, x1 = y === 0 || y === 6 ? p.w - 1 : p.w; x < x1; x++) { + const o = ((p.y + y) * W + p.x + x) * 3; + (rgb[o] = f0), (rgb[o + 1] = f1), (rgb[o + 2] = f2); + } + // While shuffling the faces flicker through other ranks. + const r = S.phase === 2 && Math.random() < 0.5 ? 1 + floor(Math.random() * 13) : card.r; + const inkC = dark ? [0, 0, 0] : S.cols === 8 && RED[card.s] ? redC : ivory; + const gl = GLYPHS[r], wide = r === 10 ? 5 : 3; + const rx = S.cols === 4 ? p.x + 2 : p.x + floor((p.w - wide) / 2); + for (const [rows, w, dx] of gl) bits(rows, w, rx + dx, p.y + 1, inkC, dark ? 1 : ink * dim, 1, dark ? set : put); + if (S.cols === 4) { + // Beside the rank the suit, or on a court card what it counts, in the + // suit's colour. + const sc = dark ? [0, 0, 0] : RED[card.s] ? redC : blackC, sa = dark ? 1 : 0.85 * dim, plot = dark ? set : put; + if (card.r <= 10) bits(SUIT[card.s], 5, p.x + 8, p.y + 1, sc, sa, 1, plot); + else if (S.court === 10) for (const [rows, w, dx] of GLYPHS[10]) bits(rows, w, p.x + 7 + dx, p.y + 1, sc, sa, 1, plot); + else bits(RANK[S.court - 1], 3, p.x + 9, p.y + 1, sc, sa, 1, plot); + } + } + + // ---- The hops so far, thin arcs that bow up, and the head. + const arcA = 0.7 * fade * dim; + for (const arc of S.arcs) { + const c = groupCol(arc.members), f = arc.flight, pts = arc.pts; + const upto = f.age < f.dur ? round(ease(f.age / f.dur) * pts.length) : pts.length; + for (let s = 0; s < upto; s++) { + const o = pts[s] * 3; + rgb[o] = max(rgb[o], c[0] * arcA); + rgb[o + 1] = max(rgb[o + 1], c[1] * arcA); + rgb[o + 2] = max(rgb[o + 2], c[2] * arcA); + } + } + for (const f of S.flights) { + const a = cell(S, f.from), b = cell(S, f.to), c = groupCol(f.members), u = ease(f.age / f.dur); + for (let tail = 0; tail < 5; tail++) { + const [x, y] = bez(a, b, max(0, u - tail * 0.05)); + const xi = round(x - 0.5), yi = round(y - 0.5), al = tail ? 0.6 - tail * 0.1 : 1; + put(xi, yi, c, al), put(xi + 1, yi, c, al), put(xi, yi + 1, c, al), put(xi + 1, yi + 1, c, al); + } + } + if (S.phase === 0 && S.head >= 0 && S.head < S.n) { + const p = cell(S, S.head); + for (let x = -1; x <= p.w; x++) (put(p.x + x, p.y - 1, ivory, 0.6), put(p.x + x, p.y + 7, ivory, 0.6)); + for (let y = 0; y < 7; y++) (put(p.x - 1, p.y + y, ivory, 0.6), put(p.x + p.w, p.y + y, ivory, 0.6)); + } + // Two chains meeting: a ring opens round the card. + for (const b of S.bursts) { + if (b.age < 0) continue; + const p = cell(S, b.at), c = groupCol(b.members), R = 2 + b.age * 22, a = 1 - b.age / 0.6; + for (let s = 0; s < 48; s++) put(round(p.cx + R * Math.cos((s / 48) * 2 * PI)), round(p.cy + R * 0.8 * sin((s / 48) * 2 * PI)), c, a); + } + + // ---- The card the chains came to, turned up large: rank and suit, and a + // dot for each of the ten chains, lit in its colour if it ended here. + if (S.phase >= 1 && S.final && S.final.length) { + const top = S.final[0], card = S.cards[top.at], c = groupCol(top.members); + const grow = S.phase === 1 ? ease(S.t / 0.35) : 1 - ease(S.t / 0.4); + const cw = round(36 * grow), chh = round(52 * grow), x0 = 32 - (cw >> 1), y0 = 64 - (chh >> 1); + // The ends themselves pulse on the deck. + for (const e of S.final) { + const p = cell(S, e.at), ec = groupCol(e.members), a = 0.5 + 0.5 * sin(S.t * 9); + for (let x = -1; x <= p.w; x++) (put(p.x + x, p.y - 1, ec, a), put(p.x + x, p.y + 7, ec, a)); + for (let y = 0; y < 7; y++) (put(p.x - 1, p.y + y, ec, a), put(p.x + p.w, p.y + y, ec, a)); + } + if (cw > 4 && chh > 4) { + for (let y = 0; y < chh; y++) + for (let x = 0; x < cw; x++) { + const edge = x === 0 || y === 0 || x === cw - 1 || y === chh - 1; + if ((x === 0 || x === cw - 1) && (y === 0 || y === chh - 1)) continue; + set(x0 + x, y0 + y, edge ? c : ivory, edge ? 1 : 0.16); + } + if (grow > 0.95) { + const r = card.r, wide = r === 10 ? 5 : 3, ink = RED[card.s] ? redC : ivory; + for (const [rows, w, dx] of GLYPHS[r]) bits(rows, w, x0 + ((cw - wide * 4) >> 1) + dx * 4, y0 + 4, ink, 1, 4, set); + bits(SUIT[card.s], 5, x0 + ((cw - 15) >> 1), y0 + 27, RED[card.s] ? redC : blackC, 1, 3, set); + for (let i = 0; i < CHAINS; i++) { + const on = top.members.includes(i), dx = x0 + 4 + (i % 5) * 6, dy = y0 + chh - 8 + floor(i / 5) * 3; + for (let v = 0; v < 2; v++) for (let u = 0; u < 4; u++) set(dx + u, dy + v, on ? chainCol[i] : ivory, on ? 1 : 0.12); + } + } + } + } + + for (let p = 0; p < NP; p++) display.setPixel(p % W, (p / W) | 0, round(min(1, rgb[p * 3]) * 255), round(min(1, rgb[p * 3 + 1]) * 255), round(min(1, rgb[p * 3 + 2]) * 255)); +} From 26100ee486b57d4929ab0b29ea545751fa974364 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:59:17 +0000 Subject: [PATCH 11/22] =?UTF-8?q?FRACTRAN=20=EC=9D=98=20=EA=B5=AC=EC=8A=AC?= =?UTF-8?q?=EC=9D=B4=20=EC=A0=9C=20=EA=B8=B0=EB=91=A5=20=EC=95=88=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=98=A4=EB=A5=B4=EB=82=B4=EB=A6=AC=EA=B2=8C=20?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PBtzYrhzPXCgRY4TG --- incoming/pattern-study/patterns/fractran.js | 68 +++++++++++++++------ 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/incoming/pattern-study/patterns/fractran.js b/incoming/pattern-study/patterns/fractran.js index 24f691480..3c6627477 100644 --- a/incoming/pattern-study/patterns/fractran.js +++ b/incoming/pattern-study/patterns/fractran.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: FRACTRAN // Family: number -// Idea: Conway's PRIMEGAME runs as a bead machine: the exponents of 2, 3, 5 and 7 stand as columns of beads, each step the first of the fourteen fractions that keeps n whole lights up while beads fly up into its denominator and out of its numerator, the columns pump up and down in sawtooth loops, and each time n is a bare power of two the 2 column alone holds p beads and the prime p joins the record at the top. +// Idea: Conway's PRIMEGAME runs as a bead machine: the exponents of 2, 3, 5 and 7 stand as columns of beads that pump up and down in sawtooth loops while beads rise out of them and fall back in, the first of the fourteen fractions that keeps n whole lights at each step, and whenever n is a bare power of two the machine stops with p beads in the 2 column alone and writes the prime p into the record at the top. // Author: patternflow-lab // Date: 2026-10-30 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -11,10 +11,10 @@ // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Start=0..3, Trail=0..1 // -// Knob 1: Hue. Turns the beads round the wheel together: at 0 the 2 column is gold, 3 cyan, 5 violet and 7 green, and the state lamps 11 to 29 blue. -// Knob 2: Speed. Steps a second, 2 times 50 to the power (Speed - 0.2) / 1.8: 100 at 2, so 2, 3, 5 and 7 arrive at 0.2, 0.7, 2.9 and 7.4 s; 2 at 0.2, one fraction at a time; 870 at 3. The machine stops for 12 steps' time on each prime. -// Knob 3: Start. Where the run begins: n = 2 at 0, or n = 2^11, 2^19 or 2^29, the moments PRIMEGAME reaches those primes, with the primes before them already in the record. The higher the start, the taller the sawtooth: from 2^29 the columns climb to 31 beads in long ramps. Every run ends after 2^41 and starts again. -// Knob 4: Trail. How long the light stays: at 0.06 a flying bead leaves a tail of 2 frames and a fraction fades in 0.1 s; at 1 the flights stay for 1.5 s and pile up into bright paths between the columns and the fractions that loop, and each column keeps a ghost of its last peak. +// Knob 1: Hue. Turns every colour together: at 0 the 2 column is gold, 3 cyan, 5 violet and 7 green, the fractions and the state lamps 11 to 29 blue and the record warm white. +// Knob 2: Speed. Steps a second, 2 times 60 to the power (Speed - 0.2) / 1.8: 120 at 2, where 2, 3, 5 and 7 enter the record at 0.2, 1.2, 3.5 and 7.7 s; 2 at 0.2, slow enough to follow every fraction and every bead; 1170 at 3, where 29 arrives at 37 s. On a prime the machine waits for its beads to land and 30 steps more, 0.55 s at 2. +// Knob 3: Start. Where a run begins: n = 2 at 0, then 2^11, 2^19 or 2^29, the states PRIMEGAME is in when it finds those primes, with the primes up to them already in the record. The higher the start, the taller the sawtooth: from 2^29 the 2 and 5 columns climb to 31 beads before 31 arrives at 73 s, and the beads thin from 5 rows to 2. A run ends 3 s after 2^41 and starts again. +// Knob 4: Trail. How long the light stays, 0.05 times 30 to the power Trail seconds. At 0.06 a flying bead leaves almost nothing behind and the tubes show single beads; from 0.4 up each bead draws a full tail, and at 1 the tails last 1.5 s and pile up into lit tubes as bright as their traffic, the fractions glow by how often they fire, and each column keeps a ghost of its last peak that sinks a bead a second. const W = 64, H = 128, NP = W * H; const { floor, round, min, max, sqrt, exp, pow, abs } = Math; @@ -77,6 +77,7 @@ function reset(S) { S.flights = []; S.inc = new Int32Array(10); S.heat.fill(0); + S.scan.fill(0); S.ghost = new Float32Array(4); S.acc = 0; S.pause = 0; @@ -89,7 +90,7 @@ function reset(S) { } export function setup(params) { - params.s = { start: -1, hue: 0, rate: 100, trail: 0.06, heat: new Float32Array(14), glow: new Float32Array(NP * 3), rgb: new Float32Array(NP * 3), sheat: new Float32Array(6), gain: 1, last: -1, dur: 0.3 }; + params.s = { start: -1, hue: 0, rate: 100, trail: 0.06, heat: new Float32Array(14), scan: new Float32Array(14), glow: new Float32Array(NP * 3), rgb: new Float32Array(NP * 3), sheat: new Float32Array(6), gain: 1, hgain: 1, last: -1, dur: 0.3 }; } // A bead in flight runs up or down its own column: up from the top of the @@ -110,8 +111,10 @@ function step(S) { let ok = true; for (let j = 0; j < 10 && ok; j++) if (e[j] < DEN[k][j]) ok = false; if (ok) break; + // Tried and passed over: its bar flickers. + S.scan[k] = min(1, S.scan[k] + 0.35); } - S.heat[k] = min(1.6, S.heat[k] + 1); + S.heat[k] = min(1.6, S.heat[k] + S.hgain); S.last = k; for (let j = 0; j < 10; j++) { e[j] -= DEN[k][j]; @@ -143,6 +146,8 @@ 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 h = S.hue; + S.cols = [hsv(0.11 + h, 0.6, 1), hsv(0.5 + h, 0.6, 1), hsv(0.8 + h, 0.5, 1), hsv(0.32 + h, 0.6, 1), hsv(0.61 + h, 0.5, 1)]; S.rate = 2 * pow(60, (cl(k[1], 0.2, 3) - 0.2) / 1.8); S.trail = cl(k[3], 0, 1); const start = cl(round(k[2]), 0, 3); @@ -153,14 +158,20 @@ export function update(dt, input, params) { const tau = 0.05 * pow(30, S.trail); // A longer trail lays each pass down fainter, so a path's brightness is // its traffic, not how long the light stays. - S.gain = min(1, 0.08 / tau); - const fade = exp(-d / tau), hfade = exp(-d / max(0.1, tau, 0.6 / S.rate)); + // At the shortest trail a bead leaves almost nothing behind it, so the + // tubes show single beads; the tail comes up with the knob. + S.gain = min(1, 0.35 / tau) * (0.12 + 0.88 * min(1, S.trail / 0.4)); + const htau = max(0.1, tau, 0.6 / S.rate), fade = exp(-d / tau), hfade = exp(-d / htau); + S.hgain = min(1, 0.12 / htau); for (let i = 0; i < NP * 3; i++) S.glow[i] *= fade; for (let i = 0; i < 14; i++) S.heat[i] *= hfade; + for (let i = 0; i < 14; i++) S.scan[i] *= exp(-d / 0.06); S.flash *= exp(-d / 0.6); // A flight takes 0.3 s at speed, up to 0.6 s when the steps come slower. const dur = (S.dur = cl(2.2 / S.rate, 0.3, 0.6)); const live = []; + S.busy = [0, 0, 0, 0]; + for (const f of S.flights) S.busy[f.j]++; for (const f of S.flights) { const u0 = f.u; f.u = min(1, f.u + d / dur); @@ -187,11 +198,17 @@ export function update(dt, input, params) { } S.pitch += (min(5, (FLOOR - CEIL) / S.peak) - S.pitch) * (1 - exp(-d / 0.4)); for (let j = 0; j < 4; j++) { - const c = S.e[j] - S.inc[j]; - S.ghost[j] = max(c, S.ghost[j] - d * (2 + 30 * (1 - S.trail))); + const c = shown(S, j); + S.ghost[j] = max(c, S.ghost[j] - d * (1 + 40 * (1 - S.trail) * (1 - S.trail))); } } +// The beads a column shows. Below 10 steps a second a given bead joins the +// stack when its flight lands; faster than that a flight outlasts many +// steps, so the stack shows the register as it is and the flights are the +// traffic through it. +const shown = (S, j) => S.e[j] - (S.rate < 10 ? S.inc[j] : 0); + // Falling beads speed up, rising ones slow down, like a ball thrown up. const fly = (f, u) => f.y0 + (f.y1 - f.y0) * (f.y1 > f.y0 ? u * u : u * (2 - u)); @@ -200,7 +217,7 @@ function trace(S, f, u0, u1) { const lo = round(min(a, b)), hi = round(max(a, b)), x = COL_X[f.j]; for (let y = lo; y <= hi; y++) { if (y < 0 || y >= H) continue; - const w = S.gain * (0.25 + 0.5 * (f.y1 > f.y0 ? (y - lo + 1) / (hi - lo + 1) : (hi - y + 1) / (hi - lo + 1))); + const w = S.gain * min(1, 14 / S.busy[f.j]) * (0.12 + 0.33 * (f.y1 > f.y0 ? (y - lo + 1) / (hi - lo + 1) : (hi - y + 1) / (hi - lo + 1))); for (let dx = -1; dx <= 1; dx++) { const o = (y * W + x + dx) * 3, ww = dx ? 0.5 * w : w; S.glow[o] += col[0] * ww; @@ -229,8 +246,7 @@ export function draw(display, params) { const S = params.s; if (!S || !S.e) return; const h = S.hue, rgb = S.rgb; - S.cols = [hsv(0.11 + h, 0.6, 1), hsv(0.5 + h, 0.6, 1), hsv(0.8 + h, 0.5, 1), hsv(0.32 + h, 0.6, 1), hsv(0.61 + h, 0.5, 1)]; - const cols = S.cols, ink = hsv(0.58 + h, 0.3, 1), warm = hsv(0.1 + h, 0.4, 1); + const cols = S.cols, ink = hsv(0.58 + h, 0.45, 1), warm = hsv(0.1 + h, 0.5, 1); rgb.fill(0); // The record. for (let i = 0; i < S.found; i++) { @@ -244,7 +260,7 @@ export function draw(display, params) { if (hk > 0.02) for (let yy = y - 1; yy <= y + 13; yy++) for (let xx = floor(x - 4); xx <= floor(x + 4); xx++) put(S, xx, yy, ink, 0.16 * hk); const lv = 0.2 + 0.8 * hk; text(S, a, round(x - textW(a) / 2), y, ink, lv); - for (let xx = round(x - 3.5); xx < round(x + 3.5); xx++) put(S, xx, y + 6, ink, lv * 0.8); + for (let xx = round(x - 3.5); xx < round(x + 3.5); xx++) put(S, xx, y + 6, ink, min(1, lv * 0.8 + 0.6 * S.scan[k])); text(S, b, round(x - textW(b) / 2), y + 8, ink, lv); } // The state lamps. @@ -256,12 +272,12 @@ export function draw(display, params) { // The columns: a rod, the ghost of the last peak, and the beads. const pitch = S.pitch, bh = max(0.8, pitch - 1.4); for (let j = 0; j < 4; j++) { - const c = cols[j], cx = COL_X[j], n = S.e[j] - S.inc[j]; + const c = cols[j], cx = COL_X[j], n = shown(S, j); for (let y = CEIL; y <= FLOOR; y++) put(S, cx, y, c, 0.12); text(S, String(PR[j]), cx - 1, FLOOR + 2, c, 0.7); const top = S.ghost[j]; for (let i = 0; i < Math.ceil(top); i++) { - const lit = i < n, yc = FLOOR - pitch * (i + 0.5), a = lit ? 1 : 0.22 * min(1, top - i); + const lit = i < n, yc = FLOOR - pitch * (i + 0.5), a = lit ? 1 : 0.45 * min(1, top - i); const glow = j === 0 && S.flash > 0.02 && lit ? S.flash : 0; for (let y = floor(yc - bh / 2 - 1); y <= floor(yc + bh / 2 + 1); y++) { const cov = cl(min(y + 1, yc + bh / 2) - max(y, yc - bh / 2), 0, 1); @@ -275,11 +291,23 @@ export function draw(display, params) { } } // Beads in flight: a small bright bead at the head of each trail. + // A crowded column dims its heads, so fast traffic reads as a lit tube in + // the column's colour rather than as white. + const busy = [0, 0, 0, 0]; + for (const f of S.flights) busy[f.j]++; for (const f of S.flights) { - const y = fly(f, f.u), c = S.cols[f.j]; - for (let dy = 0; dy < 2; dy++) for (let dx = -2; dx <= 2; dx++) put(S, COL_X[f.j] + dx, round(y) + dy - 1, abs(dx) < 2 ? [1, 1, 1] : c, abs(dx) < 2 ? 0.8 : 0.6); + const y = fly(f, f.u), c = S.cols[f.j], a = min(1, 14 / busy[f.j]); + const core = [0.5 + 0.5 * c[0], 0.5 + 0.5 * c[1], 0.5 + 0.5 * c[2]]; + for (let dy = 0; dy < 2; dy++) for (let dx = -2; dx <= 2; dx++) put(S, COL_X[f.j] + dx, round(y) + dy - 1, abs(dx) < 2 ? core : c, a * (abs(dx) < 2 ? 0.85 : 0.5)); + } + // The trails keep their colour: a pixel's glow is held under 0.7 of full, + // hue kept, so heavy traffic makes a coloured tube and not a white one. + for (let p = 0; p < NP * 3; p += 3) { + const g = S.glow, m = max(g[p], g[p + 1], g[p + 2]), q = m > 0.7 ? 0.7 / m : 1; + rgb[p] += g[p] * q; + rgb[p + 1] += g[p + 1] * q; + rgb[p + 2] += g[p + 2] * q; } - for (let p = 0; p < NP * 3; p++) rgb[p] += S.glow[p]; for (let p = 0; p < NP; p++) { const o = p * 3; display.setPixel(p % W, floor(p / W), min(255, rgb[o] * 255) | 0, min(255, rgb[o + 1] * 255) | 0, min(255, rgb[o + 2] * 255) | 0); From 608754600d3311801710a5dfc891f33ad46302b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 01:00:20 +0000 Subject: [PATCH 12/22] =?UTF-8?q?Letter=20Mobile=20=EC=9D=98=20=EB=A7=89?= =?UTF-8?q?=EB=8C=80=EB=A5=BC=20=EB=8F=8C=EA=B2=8C=20=ED=95=98=EA=B3=A0=20?= =?UTF-8?q?=ED=9D=94=EB=93=A4=EB=A6=BC=EC=9D=84=20=EB=8B=A4=EB=93=AC?= =?UTF-8?q?=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_01Gdqi6PBtzYrhzPXCgRY4TG --- .../pattern-study/patterns/letter-mobile.js | 65 +++++++++++-------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/incoming/pattern-study/patterns/letter-mobile.js b/incoming/pattern-study/patterns/letter-mobile.js index 03d872d35..c50695817 100644 --- a/incoming/pattern-study/patterns/letter-mobile.js +++ b/incoming/pattern-study/patterns/letter-mobile.js @@ -1,20 +1,20 @@ // ===== Patternflow pattern ===== // Title: Letter Mobile // Family: type -// Idea: A hanging mobile in the manner of Calder whose weights are the four letters of a word, each weighing its ink, hangs the full height of the panel with every bar balanced by moments and turning slowly on its thread before a lit wall that takes its shadow, and when one letter of the word changes, as in a word ladder, the bars above it tilt and their hanging points slide to the new balance. +// Idea: A hanging mobile in the manner of Calder whose weights are the four letters of a word, each weighing its ink, hangs the full height of the panel with every bar balanced by moments and turning slowly on its thread before a lit wall that takes its shadow, and when one letter changes, as in a word ladder, every bar above it tips and its hanging point slides to the new balance. // Author: patternflow-lab // Date: 2026-10-30 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: Alexander Calder's mobiles (1931-32, named by Marcel Duchamp), the law of the lever (Archimedes, On the Equilibrium of Planes), word ladders (Lewis Carroll's Doublets, 1879), pattern round 75 +// Lineage: Alexander Calder's first mobiles (1931-32, named by Marcel Duchamp), the law of the lever (Archimedes, On the Equilibrium of Planes, 3rd century BC), word ladders (Lewis Carroll's Doublets, 1879), pattern round 75 // Round: 75 // =============================== // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Word=0..2, Sway=0..1 // -// Knob 1: Hue. Turns the painted plates round the wheel together: at 0 they are Calder's yellow, blue, red and white, top to bottom. The wires stay steel and the wall a warm grey. -// Knob 2: Speed. One clock for the turning, the swing and the word: at 2 one letter changes every 3.6 s, the bars twist on their threads once in 9 to 14 s and the mobile swings once in 3.3 s. At 0.2 ten times slower, at 3 one and a half times faster. -// Knob 3: Word. Which word ladder the mobile climbs, one letter at a time and back: 0 is Carroll's own first, HEAD HEAL TEAL TELL TALL TAIL, from 493 LEDs of ink to 330; 1 is FILL FILM FIRM FORM WORM WARM, light letters turning heavy; 2 is WOMB TOMB TOME TIME TILE TILL, heavy turning light. -// Knob 4: Sway. How far the whole mobile swings from its hook: at 0.06 the bottom letters move 3 LEDs either way, at 1 they move 25. +// Knob 1: Hue. Turns the painted plates round the wheel together: at 0 they are Calder's yellow, blue, red and white, top to bottom. The wires stay steel; the wall is a grey that turns with the plates, blue-grey at 0. +// Knob 2: Speed. One clock for the turning, the swing and the word: at 2 one letter changes every 3.6 s, the bars turn round on their threads once in 30 to 44 s while a draft rocks each to and fro every 7 to 11 s, and the mobile swings once in 3.3 s. At 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Word. Which word ladder the mobile climbs, one letter at a time and back down: 0 is Carroll's own first, HEAD HEAL TEAL TELL TALL TAIL, from 752 LEDs of ink to 501; 1 is FILL FILM FIRM FORM WORM WARM, light letters turning heavy, 467 to 860; 2 is WOMB TOMB TOME TIME TILE TILL, heavy turning light, 905 to 448. +// Knob 4: Sway. How far the whole mobile swings from its hook: at 0.06 the bottom letters move 2.5 LEDs either way, at 1 they move 23. const W = 64, H = 128, NP = W * H; const { floor, ceil, round, min, max, sqrt, sin, cos, abs, exp, PI } = Math; @@ -105,14 +105,14 @@ const LADDERS = [ // carries that rest on its left and letter 1 on its right, bar 2 carries // letters 2 and 3. Facing you the chain zigzags so the word reads down the // panel. Each bar hangs from a loop D LEDs above it. -// Bar k: length and the thread below each end. The top hook hangs from -// HOOK. +// Bar k: length and the thread below each end. The top bar's hook hangs on +// a thread 10 rows long from the top edge at x 31. const BAR = [ - { len: 28, drop: [3, 30] }, - { len: 26, drop: [30, 3] }, - { len: 32, drop: [11, 11] }, + { len: 28, drop: [3, 28] }, + { len: 26, drop: [28, 3] }, + { len: 32, drop: [10, 10] }, ]; -const HOOK = [30, 4], D = 7; +const HOOK = [31, 10], D = 7; // Weights on each end of bar k, given the four letters' ink. A sub-mobile // weighs the sum of what hangs below it; wire and thread weigh nothing. @@ -124,16 +124,27 @@ function loads(ink) { // The law of the lever: the hook divides the bar so that left weight times // left arm equals right weight times right arm, so it sits at the fraction // WR / (WL + WR) of the bar from the left end. +// Checked in Node over all three ladders, 21 words at rest just before the +// next change: every bar's net moment is under 1e-12 (LEDs of ink times +// LEDs of arm) and its tilt under 1.8 degrees. HEAD weighs H 196, E 180, +// A 172, D 204; its top bar hangs 20.70 LEDs from the H and 7.30 from the +// 556 below (196 x 20.70 = 556 x 7.30), its bottom bar 17.36 from the A and +// 14.64 from the D. const balance = ([wl, wr]) => wr / (wl + wr); -// Turning: each bar twists to and fro on its thread (amplitude, period at -// Speed 2, phase). -const TWIST = [[1.0, 17, -0.4], [1.25, 13, PI - 0.5], [1.75, 21, -0.35]]; +// Turning: each bar turns round on its thread at its own rate, the bottom +// one fastest, and a draft rocks it back and forth on top of that. Per bar: +// angle at the start, seconds a turn at Speed 2 (signed), the draft's swing +// and its period. +const TURN = [[-0.3, 44, 0.35, 9], [0.35, -36, 0.35, 7], [-0.6, 30, 0.35, 11]]; // Lamp up and to the left in front of the mobile, the wall 26 LEDs behind // the threads. const LX = -80, LY = -120, LZ = 300, WZ = -26; -const STEP = 3.6, FLIP = 0.7, SLIDE_AT = 0.35, SLIDE = 1.1; +// A word holds 3.6 s at Speed 2; a changing letter spins round in 0.7 s and +// swaps when it has turned a quarter (ss(0.3264) = 0.25); the hooks start to +// slide 0.3 s after the swap and take 1.4 s. +const STEP = 3.6, FLIP = 0.7, SWAP = 0.3264, SLIDE_AT = 0.3, SLIDE = 1.4; function startWord(S, ladder) { S.ladder = ladder; @@ -142,7 +153,7 @@ function startWord(S, ladder) { const word = LADDERS[ladder][0]; S.letters = [0, 1, 2, 3].map((i) => ({ g: glyphOf(word[i]), next: null, flip: -1 })); const L = loads(S.letters.map((l) => l.g.ink)); - S.bars = BAR.map((b, k) => ({ f: balance(L[k]), f0: 0, f1: 0, slide: -1, a: 0, av: 0 })); + S.bars = BAR.map((b, k) => ({ f: balance(L[k]), f0: 0, slide: -1, a: 0, av: 0 })); S.clock = STEP - 2.1; } @@ -191,13 +202,13 @@ export function update(dt, input, params) { nextWord(S); } // A letter that changes spins once round on its thread and comes back as - // the new letter; its weight changes when it is edge-on, a quarter in. + // the new letter; its weight changes a quarter turn in. let changed = false; for (const l of S.letters) { if (l.flip < 0) continue; const was = l.flip; l.flip += step / FLIP; - if (was < 0.25 && l.flip >= 0.25 && l.next) { + if (was < SWAP && l.flip >= SWAP && l.next) { l.g = l.next; l.next = null; changed = true; @@ -205,7 +216,8 @@ export function update(dt, input, params) { if (l.flip >= 1) l.flip = -1; } const L = loads(ink(S)); - if (changed) for (const b of S.bars) b.slide = 0; + // Only the bars whose balance moved: the changed letter's and those above. + if (changed) S.bars.forEach((b, i) => abs(balance(L[i]) - b.f) > 1e-9 && (b.slide = 0)); // The hooks slide to the new balance once the bars have tipped. S.bars.forEach((b, i) => { if (b.slide < 0) return; @@ -217,9 +229,10 @@ export function update(dt, input, params) { if (u >= 1) b.slide = -1; } }); - // Tilts, in four substeps. G sets the swing: a level bar knocked off - // rocks back about once in 1.4 s. - const G = 1300, DAMP = 1.5, n = 4, h = step / n; + // Tilts, in four substeps. G and DAMP set the swing: when HEAD turns to + // HEAL the bottom bar tips to 35 degrees in 0.9 s, swings back past level + // to 6 degrees 1.3 s later and is within 1.5 degrees a second after that. + const G = 400, DAMP = 2.6, n = 4, h = step / n; for (let s = 0; s < n; s++) S.bars.forEach((b, i) => { const { tq, I } = torque(b, BAR[i].len, L[i]); @@ -232,14 +245,14 @@ export function update(dt, input, params) { // ---- Geometry: points in LEDs, x right, y down, z towards you. function layout(S) { const t = S.t, phi = (0.012 + 0.2 * S.sway) * sin((2 * PI * t) / 3.3); - // The whole mobile swings about its hook at the top edge. + // The whole mobile swings about the top of its thread. const sw = (p) => [HOOK[0] + (p[0] - HOOK[0]) * cos(phi) - p[1] * sin(phi), (p[0] - HOOK[0]) * sin(phi) + p[1] * cos(phi), p[2]]; const lines = [], plates = []; let piv = [HOOK[0], HOOK[1], 0]; lines.push({ p: [HOOK[0], -1, 0], q: piv, c: 0, glint: 0 }); for (let k = 0; k < 3; k++) { - const B = BAR[k], b = S.bars[k], [amp, per, ph] = TWIST[k]; - const th = amp * sin((2 * PI * t) / per + ph), ex = cos(th), ez = sin(th), ca = cos(b.a), sa = sin(b.a); + const B = BAR[k], b = S.bars[k], [th0, per, amp, dp] = TURN[k]; + const th = th0 + (2 * PI * t) / per + amp * sin((2 * PI * t) / dp), ex = cos(th), ez = sin(th), ca = cos(b.a), sa = sin(b.a); const bx = ca * ex, by = sa, bz = ca * ez; // Hook, then the bar's own point under it, then the ends. const hp = [piv[0] - sa * ex * D, piv[1] + ca * D, piv[2] - sa * ez * D]; From 7dc1d6dfcd9190cfab5dc92781df37b42185e6de Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 01:00:55 +0000 Subject: [PATCH 13/22] =?UTF-8?q?Mesostic=EC=99=80=20Kruskal=20Count=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PBtzYrhzPXCgRY4TG --- .../pattern-study/patterns/kruskal-count.js | 28 ++++++++++--------- incoming/pattern-study/patterns/mesostic.js | 8 +++--- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/incoming/pattern-study/patterns/kruskal-count.js b/incoming/pattern-study/patterns/kruskal-count.js index e2e59a15a..0a788369a 100644 --- a/incoming/pattern-study/patterns/kruskal-count.js +++ b/incoming/pattern-study/patterns/kruskal-count.js @@ -12,12 +12,12 @@ // @knobs Hue=0..1, Speed=0.2..3, Court=5..10, Decks=1..2 // // Knob 1: Hue. Turns the ten chain colours round the wheel together; at 0 the first card's chain is cyan and the rest follow in steps of a tenth of the wheel. -// Knob 2: Speed. At 2 the head reads 8 cards a second, so a deal takes 6.5 s, the turned-up card stays 2.2 s and the shuffle 0.8 s. At 0.2 ten times slower, at 3 one and a half times faster. -// Knob 3: Court. What a jack, queen or king counts, from 5 as in Gardner's telling to 10 as in blackjack. The mean jump grows from 5.4 to 6.5 cards and fewer chains meet: with one deck all ten merge in 58% of deals at 5 and 28% at 10. +// Knob 2: Speed. At 2 the head reads 10.4 cards a second (20.8 with two decks), so a deal takes 5 s, the turned-up card stays 2.2 s and the shuffle 0.8 s: a new deal every 8 s. At 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Court. What a jack, queen or king counts, from 5 as in Gardner's telling to 10 as in blackjack; with one deck a court card shows its count where the suit would be. The mean jump grows from 5.4 to 6.5 cards and fewer chains meet: with one deck all ten merge in 58% of deals at 5 and 28% at 10. // Knob 4: Decks. One deck of 52 cards in 13 rows of 4 with suits, or from 1.5 two decks of 104 in 13 rows of 8 with the suit shown only by the colour of the rank. All ten chains merge in 94% of two-deck deals at Court 5. Turning it deals again. const W = 64, H = 128, NP = W * H; -const { floor, round, min, max, abs, sqrt, exp, PI, sin } = Math; +const { floor, round, min, max, sqrt, PI, sin, cos } = Math; const cl = (v, a, b) => (v < a ? a : v > b ? b : v); const ease = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); @@ -27,9 +27,9 @@ function hsv(h, s, v) { return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; } -// ---- Card faces. Ranks in a 3 by 5 font, one row a number with the high -// bit on the left: A, 2 to 9, then the 1 and 0 of 10, J, Q, K. The 1 of 10 -// is one column wide so "10" fits in 5. +// ---- Card faces. Ranks in a 3 by 5 font, one number a row with the high +// bit on the left: A, 2 to 9, J, Q, K. Ten is a one-column 1 and a 0 (the +// table below), so "10" fits in 5 columns. const RANK = [ [2, 5, 7, 5, 5], [6, 1, 2, 4, 7], [6, 1, 2, 1, 6], [5, 5, 7, 1, 1], [7, 4, 6, 1, 6], [3, 4, 6, 5, 2], [7, 1, 2, 2, 2], [2, 5, 2, 5, 2], [2, 5, 3, 1, 6], null, [1, 1, 1, 5, 2], [2, 5, 5, 6, 3], [5, 6, 4, 6, 5], @@ -56,11 +56,12 @@ const valueOf = (r, court) => (r > 10 ? court : r); // become one. Every token is always ahead of the head, so a chain can only // meet another on a card the other has not left yet. A token whose jump // would leave the deck stays: its card is that chain's last card. Counted -// in Node over 100000 deals: with one deck and Court 5 all ten chains end -// on one card in 58.2% of deals and the classic trick, a chain from the -// first card against one from a card picked among the first ten, works in -// 85.3%; at Court 10 it is 28.3% and 72.0%. With two decks at Court 5 all -// ten merge in 93.8%. +// in Node with this code over 100000 deals, every chain's last card the +// same as walking it alone: with one deck and Court 5 all ten chains end +// on one card in 58.5% of deals, and the trick itself, the magician's +// chain from the first card against yours from a card you pick among the +// first ten, works in 85.4%. At Court 10 it is 28.4% and 72.2%; with two +// decks at Court 5, 93.8% and 97.9%. const CHAINS = 10; const DEAL = 5.0, SHOW = 2.2, SHUFFLE = 0.8; @@ -75,7 +76,8 @@ function deal(S) { S.n = n; S.cards = cards; S.cols = S.decks === 1 ? 4 : 8; - // Tokens by card; trail colour index by card (-1 unvisited). + // The token on each card, and the chains that have stood on it (null for + // a card no chain has reached). S.tok = new Array(n).fill(null); S.trail = new Array(n).fill(null); for (let i = 0; i < CHAINS; i++) { @@ -287,7 +289,7 @@ export function draw(display, params) { for (const b of S.bursts) { if (b.age < 0) continue; const p = cell(S, b.at), c = groupCol(b.members), R = 2 + b.age * 22, a = 1 - b.age / 0.6; - for (let s = 0; s < 48; s++) put(round(p.cx + R * Math.cos((s / 48) * 2 * PI)), round(p.cy + R * 0.8 * sin((s / 48) * 2 * PI)), c, a); + for (let s = 0; s < 48; s++) put(round(p.cx + R * cos((s / 48) * 2 * PI)), round(p.cy + R * 0.8 * sin((s / 48) * 2 * PI)), c, a); } // ---- The card the chains came to, turned up large: rank and suit, and a diff --git a/incoming/pattern-study/patterns/mesostic.js b/incoming/pattern-study/patterns/mesostic.js index 3c00cd324..e5c56345c 100644 --- a/incoming/pattern-study/patterns/mesostic.js +++ b/incoming/pattern-study/patterns/mesostic.js @@ -12,12 +12,12 @@ // @knobs Hue=0..1, Speed=0.2..3, Word=0..3, Rule=0..1 // // Knob 1: Hue. Turns the capitals and the wings round the wheel together: at 0 the capitals are gold and the wing text pale sky blue. -// Knob 2: Speed. At 2 the reader slides for 0.45 to 0.95 s depending on how much text it skips, the capital locks in 0.4 s and the page climbs a line in 0.35 s, about 1.5 s a line. At 0.2 ten times slower, at 3 one and a half times faster. +// Knob 2: Speed. At 2 the reader slides for 0.45 to 0.95 s depending on how much text it passes over, the capital locks in 0.4 s and the page climbs a line in 0.35 s: a new line every 1.28 s on average with THOREAU. At 0.2 ten times slower, at 3 one and a half times faster. // Knob 3: Word. The spine: THOREAU, WALDEN, MUREAU (Cage's 1970 piece made from Thoreau's Journal, MUsic and thoREAU) or SILENCE (Cage's 1961 book). The page opens on the first 17 lines of the mesostic; turning it leaves a blank line and the reader starts a new mesostic from where it is. // Knob 4: Rule. Below 0.5 a 50% mesostic: between two capitals the second capital's letter does not appear. From 0.5 a 100% mesostic: the first capital's letter does not appear there either, so the wings are shorter and more letters are passed over. Turning it leaves a blank line like Word. const W = 64, H = 128, NP = W * H; -const { floor, round, min, max, abs, exp } = Math; +const { floor, round, min, max, exp } = Math; const cl = (v, a, b) => (v < a ? a : v > b ? b : v); const ease = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); @@ -232,7 +232,7 @@ export function update(dt, input, params) { const last = S.page[S.page.length - 1]; S.gen = newGen(SPINES[wd], full, last.end); S.page.push({ blank: true, glyphs: [], rp: last.rp, end: last.end }); - if (S.page.length > SLOTS + 1) S.page.shift(); + while (S.page.length > SLOTS + 1) S.page.shift(); S.next = nextLine(S.gen); S.from = last.rp; S.phase = 2; @@ -246,7 +246,7 @@ export function update(dt, input, params) { if (S.phase === 0) S.phase = 1; else if (S.phase === 1) { S.page.push(S.next); - if (S.page.length > SLOTS + 1) S.page.shift(); + while (S.page.length > SLOTS + 1) S.page.shift(); S.from = S.next.rp; S.next = nextLine(S.gen); S.phase = 2; From 30ade9fd0c2671cfb25dbe4d021b21c340aad4ca Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 01:01:55 +0000 Subject: [PATCH 14/22] =?UTF-8?q?Coded=20Aperture=20=EC=97=90=20=EB=85=B8?= =?UTF-8?q?=EC=B6=9C=20=EC=8B=9C=EA=B3=84=EC=99=80=20=EC=9E=B0=20=EC=8B=A0?= =?UTF-8?q?=ED=98=B8=20=EB=8C=80=20=EC=9E=A1=EC=9D=8C=EC=9D=84=20=EC=A0=81?= =?UTF-8?q?=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_01Gdqi6PBtzYrhzPXCgRY4TG --- .../pattern-study/patterns/coded-aperture.js | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/incoming/pattern-study/patterns/coded-aperture.js b/incoming/pattern-study/patterns/coded-aperture.js index 6fed38c6a..91a518de7 100644 --- a/incoming/pattern-study/patterns/coded-aperture.js +++ b/incoming/pattern-study/patterns/coded-aperture.js @@ -1,20 +1,20 @@ // ===== Patternflow pattern ===== // Title: Coded Aperture // Family: type -// Idea: A glowing word shines through a card punched with a modified uniformly redundant array onto a detector where photons land one by one as an unreadable jumble of overlapping shadows, and correlating that jumble with the array's decoding pattern brings the word up out of the noise as the counts pile up. +// Idea: A glowing word shines through a card punched with a modified uniformly redundant array onto a detector where photons land one by one as an unreadable jumble of overlapping shadows, and correlating that jumble with the array's decoding pattern brings the word up out of the noise as the counts pile up, until the 6 s exposure ends and the detector is cleared. // Author: patternflow-lab // Date: 2026-10-30 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: coded aperture imaging with a random array of pinholes (Robert Dicke, and J. G. Ables, 1968), uniformly redundant arrays (E. E. Fenimore and T. M. Cannon, 1978), modified uniformly redundant arrays from quadratic residues (S. R. Gottesman and E. E. Fenimore, 1989), pattern round 75 +// Lineage: coded aperture imaging with random arrays of pinholes (Robert H. Dicke, 1968; J. G. Ables, 1968), uniformly redundant arrays (E. E. Fenimore and T. M. Cannon, 1978), modified uniformly redundant arrays from quadratic residues (S. R. Gottesman and E. E. Fenimore, 1989), pattern round 75 // Round: 75 // =============================== // @matrix 64x128 // @knobs Hue=0..1, Rate=0.2..3, Mask=0..2, Word=0..3 // -// Knob 1: Hue. Turns the word and the detector round the wheel together: at 0 the word and its decoded image are pale gold and the photons on the detector cyan. -// Knob 2: Rate. Photons a second through the coded mask, 1500 per unit: 3000 at 2, 300 at 0.2, 4500 at 3. Every exposure lasts 6 s, so the rate sets how sharp the word comes up before the detector is cleared. -// Knob 3: Mask. 0 is the MURA for p = 17, 144 holes; 1 is a random card with about as many holes, as Dicke and Ables proposed, decoded the same way, which leaves ghosts; 2 is one pinhole at the centre. Each hole lets through the same light, so the pinhole gets 144 times fewer photons in the same 6 s. -// Knob 4: Word. What shines above the card: STAR, CRAB, VELA or LUX, in a 3 by 5 letter on the 17 by 17 sky. +// Knob 1: Hue. Turns the light round the wheel: at 0 the word, the light in the holes and the decoded word are pale gold and the photons on the detector cyan. +// Knob 2: Rate. Photons a second through the coded card, 1500 per unit: 3000 at 2, 300 at 0.2, 4500 at 3, on top of a detector background of 1 count per element a second. Every exposure lasts 6 s and holds its result 1.2 s, so the rate sets how clear the word comes up: at the end it stands 3.8 noise widths above the empty sky at 2, 0.9 at 0.2 and 4.7 at 3. +// Knob 3: Mask. 0 is the MURA for p = 17, 144 holes of 289. 1 is a random card about half open, the kind Dicke and Ables proposed, decoded by balanced correlation; even without noise its shadows do not cancel, so 15 of STAR's 34 elements come out under half bright and a ghost in the sky reaches 38%. 2 is one pinhole in the middle. Every hole passes the same light, so in the same 6 s the pinhole gathers 123 photons among 1740 background counts, and the word ends 1.5 noise widths clear against 2.6 for the random card and 3.8 for the MURA. +// Knob 4: Word. What shines above the card: STAR, CRAB, VELA or LUX, in 3 by 5 letters on the 17 by 17 sky. The fewer the lit elements, the clearer the decoded word: LUX, 27 elements, ends at 4.7 where STAR, 34, ends at 3.8. const W = 64, H = 128, NP = W * H; const { floor, round, min, max, sqrt, exp, log, cos, PI } = Math; @@ -122,6 +122,11 @@ function poisson(l) { } function clear(S) { + // What the last exposure showed fades out over a quarter second. + if (S.view) { + S.oldDec.set(S.view); + S.oldA = 1; + } S.D.fill(0); S.dD.fill(0); S.dec.fill(0); @@ -136,6 +141,7 @@ export function setup(params) { D: new Float32Array(NE), dD: new Float32Array(NE), dec: new Float32Array(NE), cnt: new Float32Array(BW * BH), flash: new Float32Array(BW * BH), hole: new Float32Array(NE), spark: new Float32Array(NP), nWord: 0, nBkg: 0, rgb: new Float32Array(NP * 3), wrap: new Int32Array(3 * P), + view: new Float32Array(NE), oldDec: new Float32Array(NE), oldA: 0, }); for (let x = 0; x < 3 * P; x++) S.wrap[x] = x % P; } @@ -222,7 +228,8 @@ export function update(dt, input, params) { S.t -= CYCLE; clear(S); } - if (S.t < EXPO) expose(S, min(d, EXPO - S.t + d)); + S.oldA *= exp(-d / 0.12); + if (S.t < EXPO) expose(S, d); } export function draw(display, params) { @@ -280,13 +287,18 @@ export function draw(display, params) { const off = (S.nBkg / NE) * card.sq; for (let e = 0; e < NE; e++) { const i = floor(e / P), j = e - i * P; - const v = cl((S.dec[e] - off) / full, 0, 1.3); + const v = (S.view[e] = cl((S.dec[e] - off) / full, 0, 1.3)); + const u = max(v, S.oldDec[e] * S.oldA); for (let y = 0; y < CH; y++) for (let x = 0; x < CW; x++) { put(X0 + j * CW + x, Y_DEC + i * CH + y, night, 1); - put(X0 + j * CW + x, Y_DEC + i * CH + y, gold, 0.85 * v); + put(X0 + j * CW + x, Y_DEC + i * CH + y, gold, 0.85 * u); } } + // The exposure clock: a thread under the detector that runs out over the + // 6 s and stays full while the decoded word holds. + const run = min(1, S.t / EXPO) * BW; + for (let x = 0; x < run; x++) put(X0 + x, Y_DET + BH + 1, cyan, 0.3); for (let p = 0; p < NP; p++) if (S.spark[p] > 0.01) put(p % W, floor(p / W), p < Y_MASK * W ? gold : lightC, 0.6 * S.spark[p]); for (let p = 0; p < NP; p++) { const o = p * 3; From 84df2f02b550861882b06de0504015cec27f65eb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 01:04:09 +0000 Subject: [PATCH 15/22] =?UTF-8?q?Miura-ori=20=ED=8C=A8=ED=84=B4=20?= =?UTF-8?q?=EC=B4=88=EC=95=88=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PBtzYrhzPXCgRY4TG --- incoming/pattern-study/patterns/miura-ori.js | 187 +++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 incoming/pattern-study/patterns/miura-ori.js diff --git a/incoming/pattern-study/patterns/miura-ori.js b/incoming/pattern-study/patterns/miura-ori.js new file mode 100644 index 000000000..a94fa3a10 --- /dev/null +++ b/incoming/pattern-study/patterns/miura-ori.js @@ -0,0 +1,187 @@ +// ===== Patternflow pattern ===== +// Title: Miura-ori +// Family: space +// Idea: A Miura-ori sheet of congruent parallelograms folds and unfolds with its one degree of freedom, seen in 3D from above and a little below, so its rows of facets flip between the warm key light and the cool fill as the fold deepens, its mountain and valley creases show as light and dark lines, and the whole sheet shrinks in both directions at once and spreads out again. +// Author: patternflow-lab +// Date: 2026-10-30 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Miura-ori (Koryo Miura, 1970; "Method of packaging and deployment of large membranes in space", 1985), its rigid folding as a translational surface (Mark Schenk and Simon Guest, 2013), pattern round 75 +// Round: 75 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Sector=55..85, Light=0..1 +// +// Knob 1: Hue. The colour of the key light; the fill light from the other side takes the opposite hue. +// Knob 2: Speed. How fast the sheet folds and opens. +// Knob 3: Sector. The sector angle alpha of the parallelograms, in degrees. +// Knob 4: Light. Where the key light stands round the sheet. + +const W = 64, H = 128, SW = 128, SH = 256, NS = SW * SH; +const { floor, ceil, round, sqrt, cos, sin, acos, min, max, abs, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const DEG = PI / 180; + +function hsv(h, s, v, o, k) { + 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); + o[k] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + o[k + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + o[k + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// ---- The fold. A Miura-ori is a translational surface: P(i, j) = A(i) + B(j), +// with A a zigzag in the (v, z) plane whose steps are +// eb(i) = (0, b cos psi, +-b sin psi) +// and B a zigzag in the (u, v) plane whose steps are +// ea(j) = (a sin g, +-a cos g, 0). +// Every facet is then a parallelogram with sides eb(i) and ea(j), so its +// sides stay b and a whatever psi is, and its angle stays alpha (or pi - +// alpha, the same parallelogram turned over) as long as eb.ea = +-ab cos alpha, +// that is cos psi cos g = cos alpha. psi = 0 is the flat sheet (g = alpha); +// psi = alpha folds it flat into a packet (g = 0). The one parameter psi is +// the fold's one degree of freedom. u runs across the panel, v down it, z +// out of the sheet towards the eye. +const M = 17, N = 11, NV = (M + 1) * (N + 1); // facets down and across, vertices +const B_LEN = 12, A_LEN = 10; +function sheet(alpha, psi, out) { + const g = acos(cl(cos(alpha) / cos(psi), -1, 1)); + const bv = B_LEN * cos(psi), bz = B_LEN * sin(psi), au = A_LEN * sin(g), av = A_LEN * cos(g); + const u0 = (N * au) / 2, v0 = (M * bv + av) / 2; + let k = 0; + for (let i = 0; i <= M; i++) + for (let j = 0; j <= N; j++) { + out[k++] = j * au - u0; + out[k++] = i * bv + (j & 1) * av - v0; + out[k++] = (i & 1) * bz - bz / 2; + } + return g; +} + +export function setup(params) { + params.s = { + t: 0, hue: 0, speed: 2, alpha: 55 * DEG, light: 0.06, + P: new Float64Array(NV * 3), sx: new Float32Array(NV), sy: new Float32Array(NV), sz: new Float32Array(NV), + key: new Float32Array(3), fill: new Float32Array(3), + id: new Int16Array(NS), depth: new Float32Array(NS), mark: new Uint8Array(NS), + col: new Float32Array(M * N * 3), mv: new Int8Array(M * N * 2), rgb: new Float32Array(3), + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 55, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.alpha = cl(k[2], 55, 85) * DEG; + S.light = cl(k[3], 0, 1); + S.t += (d * S.speed) / 2; +} + +const TILT = 28 * DEG; // the eye leans this far from straight above, towards the bottom of the panel +const FOLD = 8; // seconds a fold and an opening at Speed 2 + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { P, sx, sy, sz, id, depth, mark, col, mv, rgb, key, fill } = S; + const f = 0.5 - 0.5 * cos((2 * PI * S.t) / FOLD); + const psi = S.alpha * (0.03 + 0.87 * f); + sheet(S.alpha, psi, P); + const ct = cos(TILT), st = sin(TILT); + // Vertices on the sample grid (two samples per LED each way), and their + // depth towards the eye. + for (let k = 0, q = 0; k < NV; k++, q += 3) { + sx[k] = (W / 2 + P[q]) * 2; + sy[k] = (H / 2 + P[q + 1] * ct - P[q + 2] * st) * 2; + sz[k] = P[q + 1] * st + P[q + 2] * ct; + } + // Lights, in sheet coordinates: the key from the Light knob's side, 40 + // degrees up; the fill from the opposite side, 12 degrees up. + const az = 2 * PI * (S.light + 0.69), e1 = 35 * DEG, e2 = 15 * DEG; + const az2 = az + PI / 2, k1x = cos(e1) * cos(az), k1y = cos(e1) * sin(az), k1z = sin(e1), k2x = cos(e2) * cos(az2), k2y = cos(e2) * sin(az2), k2z = sin(e2); + hsv(0.1 + S.hue, 0.7, 1, key, 0); + hsv(0.58 + S.hue, 0.9, 0.75, fill, 0); + id.fill(-1); + depth.fill(-1e9); + for (let i = 0; i < M; i++) + for (let j = 0; j < N; j++) { + const v0 = i * (N + 1) + j, v1 = v0 + 1, v3 = v0 + N + 1, v2 = v3 + 1, fi = i * N + j; + // Normal from the sides at P(i, j), turned to the front of the sheet (+z). + const q0 = v0 * 3, q1 = v1 * 3, q3 = v3 * 3; + const ax = P[q3] - P[q0], ay = P[q3 + 1] - P[q0 + 1], az3 = P[q3 + 2] - P[q0 + 2], bx = P[q1] - P[q0], by = P[q1 + 1] - P[q0 + 1], bz = P[q1 + 2] - P[q0 + 2]; + let nx = ay * bz - az3 * by, ny = az3 * bx - ax * bz, nz = ax * by - ay * bx; + const nl = sqrt(nx * nx + ny * ny + nz * nz) || 1; + (nx /= nl), (ny /= nl), (nz /= nl); + if (nz < 0) (nx = -nx), (ny = -ny), (nz = -nz); + // The back of the sheet shows where the turned normal points away from the eye. + const sgn = ny * st + nz * ct >= 0 ? 1 : -1; + const d1 = max(0, sgn * (nx * k1x + ny * k1y + nz * k1z)), d2 = max(0, sgn * (nx * k2x + ny * k2y + nz * k2z)), back = sgn < 0 ? 0.5 : 1; + for (let c = 0; c < 3; c++) col[fi * 3 + c] = back * (0.04 + key[c] * d1 + fill[c] * d2); + // Mountain or valley for the crease to the next facet down (side 0) + // and across (side 1): a mountain when that facet's far corner lies + // below this facet's plane. + if (i + 1 < M) { + const q = (v3 + N + 1) * 3; + mv[fi * 2] = (P[q] - P[q3]) * nx + (P[q + 1] - P[q3 + 1]) * ny + (P[q + 2] - P[q3 + 2]) * nz < 0 ? 1 : 2; + } + if (j + 1 < N) { + const q = (v1 + 1) * 3; + mv[fi * 2 + 1] = (P[q] - P[q1]) * nx + (P[q + 1] - P[q1 + 1]) * ny + (P[q + 2] - P[q1 + 2]) * nz < 0 ? 1 : 2; + } + // Rasterize: a sample is inside when it is on the inner side of all + // four edges; its depth comes from the facet's plane. + const X0 = sx[v0], Y0 = sy[v0], Z0 = sz[v0]; + const ux = sx[v1] - X0, uy = sy[v1] - Y0, uz = sz[v1] - Z0, wx = sx[v3] - X0, wy = sy[v3] - Y0, wz = sz[v3] - Z0; + const det = ux * wy - uy * wx; + if (abs(det) < 1e-6) continue; + const xmin = max(0, floor(min(X0, sx[v1], sx[v2], sx[v3]))), xmax = min(SW - 1, ceil(max(X0, sx[v1], sx[v2], sx[v3]))); + const ymin = max(0, floor(min(Y0, sy[v1], sy[v2], sy[v3]))), ymax = min(SH - 1, ceil(max(Y0, sy[v1], sy[v2], sy[v3]))); + const inv = 1 / det; + for (let y = ymin; y <= ymax; y++) { + const ry = y + 0.5 - Y0; + for (let x = xmin; x <= xmax; x++) { + const rx = x + 0.5 - X0; + // The sample as a + b along the two sides: inside when both lie in 0..1. + const a = (rx * wy - ry * wx) * inv, b = (ux * ry - uy * rx) * inv; + if (a < 0 || a > 1 || b < 0 || b > 1) continue; + const zz = Z0 + a * uz + b * wz, p = y * SW + x; + if (zz > depth[p]) (depth[p] = zz), (id[p] = fi); + } + } + } + // Lines: a sample whose right or lower neighbour belongs to another facet + // is on a crease (1 mountain, 2 valley) or on an edge where one part of + // the sheet passes in front of another (3). + for (let y = 0; y < SH; y++) + for (let x = 0; x < SW; x++) { + const p = y * SW + x, a = id[p]; + mark[p] = 0; + if (a < 0) continue; + let t = 0; + for (let side = 0; side < 2; side++) { + if (side === 0 ? x + 1 >= SW : y + 1 >= SH) continue; + const b = id[side === 0 ? p + 1 : p + SW]; + if (b === a || b < 0) continue; + const lo = min(a, b), d = abs(a - b); + let m = 3; + if (d === N) m = mv[lo * 2]; + else if (d === 1 && ((a / N) | 0) === ((b / N) | 0)) m = mv[lo * 2 + 1]; + if (m > t) t = m; + } + mark[p] = t; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let r = 0, g = 0, b = 0; + for (let s = 0; s < 4; s++) { + const p = (2 * y + (s >> 1)) * SW + 2 * x + (s & 1), a = id[p]; + if (a < 0) continue; + let cr = col[a * 3], cg = col[a * 3 + 1], cb = col[a * 3 + 2]; + const m = mark[p]; + if (m === 1) (cr = 0.45 + 0.55 * cr), (cg = 0.45 + 0.55 * cg), (cb = 0.45 + 0.55 * cb); + else if (m >= 2) (cr *= 0.2), (cg *= 0.2), (cb *= 0.2); + (r += cr), (g += cg), (b += cb); + } + display.setPixel(x, y, round(min(255, 63.75 * r)), round(min(255, 63.75 * g)), round(min(255, 63.75 * b))); + } +} From 438fc929706b842e8d455933b84c1ad05fc04d1b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 01:04:34 +0000 Subject: [PATCH 16/22] =?UTF-8?q?Coded=20Aperture=20=EC=99=80=20FRACTRAN?= =?UTF-8?q?=20=EC=9D=98=20=EC=A3=BC=EC=84=9D=EC=9D=84=20=EB=8B=A4=EB=93=AC?= =?UTF-8?q?=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_01Gdqi6PBtzYrhzPXCgRY4TG --- incoming/pattern-study/patterns/coded-aperture.js | 5 +++-- incoming/pattern-study/patterns/fractran.js | 12 ++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/incoming/pattern-study/patterns/coded-aperture.js b/incoming/pattern-study/patterns/coded-aperture.js index 91a518de7..8308eccce 100644 --- a/incoming/pattern-study/patterns/coded-aperture.js +++ b/incoming/pattern-study/patterns/coded-aperture.js @@ -37,7 +37,8 @@ const EXPO = 6, HOLD = 1.2, CYCLE = EXPO + HOLD; // Detector background, counts per element per second, the same for every // mask: the cosmic-ray and radioactive counts an X-ray detector sees. const BKG = 1; -// The first measured frame comes 2 s into the run; the exposure opens 0.6 s in. +// The pattern opens 0.6 s into an exposure, so that 2 s in the word +// already stands about 2.5 noise widths clear. const T0 = 0.6; // ---- Letters, 3 by 5, on sky rows 6 to 10. @@ -122,7 +123,7 @@ function poisson(l) { } function clear(S) { - // What the last exposure showed fades out over a quarter second. + // What the last exposure decoded fades out in about a quarter second. if (S.view) { S.oldDec.set(S.view); S.oldA = 1; diff --git a/incoming/pattern-study/patterns/fractran.js b/incoming/pattern-study/patterns/fractran.js index 3c6627477..12061ec4d 100644 --- a/incoming/pattern-study/patterns/fractran.js +++ b/incoming/pattern-study/patterns/fractran.js @@ -157,15 +157,16 @@ export function update(dt, input, params) { } const tau = 0.05 * pow(30, S.trail); // A longer trail lays each pass down fainter, so a path's brightness is - // its traffic, not how long the light stays. - // At the shortest trail a bead leaves almost nothing behind it, so the - // tubes show single beads; the tail comes up with the knob. + // its traffic, not how long the light stays. At the shortest trail a bead + // leaves almost nothing behind it and the tubes show single beads. S.gain = min(1, 0.35 / tau) * (0.12 + 0.88 * min(1, S.trail / 0.4)); const htau = max(0.1, tau, 0.6 / S.rate), fade = exp(-d / tau), hfade = exp(-d / htau); S.hgain = min(1, 0.12 / htau); for (let i = 0; i < NP * 3; i++) S.glow[i] *= fade; for (let i = 0; i < 14; i++) S.heat[i] *= hfade; - for (let i = 0; i < 14; i++) S.scan[i] *= exp(-d / 0.06); + const sfade = exp(-d / 0.06); + for (let i = 0; i < 14; i++) S.scan[i] *= sfade; + for (let j = 0; j < 6; j++) S.sheat[j] = S.e[4 + j] > 0 ? 1 : S.sheat[j] * exp(-d / 0.08); S.flash *= exp(-d / 0.6); // A flight takes 0.3 s at speed, up to 0.6 s when the steps come slower. const dur = (S.dur = cl(2.2 / S.rate, 0.3, 0.6)); @@ -265,8 +266,7 @@ export function draw(display, params) { } // The state lamps. for (let j = 0; j < 6; j++) { - const on = S.e[4 + j] - S.inc[4 + j] > 0; - S.sheat[j] = on ? 1 : S.sheat[j] * 0.7; + const on = S.e[4 + j] > 0; text(S, String(PR[4 + j]), round(stX(j) - 3.5), ST_Y, on ? [1, 1, 1] : cols[4], on ? 0.95 : 0.22 + 0.5 * S.sheat[j]); } // The columns: a rod, the ghost of the last peak, and the beads. From d4d1ab96bea8e675143466ee940a48e5d94edd82 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 01:08:47 +0000 Subject: [PATCH 17/22] =?UTF-8?q?Euler=20Strokes=EC=99=80=20Steiner=20Tree?= =?UTF-8?q?=20=ED=8C=A8=ED=84=B4=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PBtzYrhzPXCgRY4TG --- .../pattern-study/patterns/euler-strokes.js | 8 +- .../pattern-study/patterns/steiner-tree.js | 439 ++++++++++++++++++ 2 files changed, 443 insertions(+), 4 deletions(-) create mode 100644 incoming/pattern-study/patterns/steiner-tree.js diff --git a/incoming/pattern-study/patterns/euler-strokes.js b/incoming/pattern-study/patterns/euler-strokes.js index 0e0b5a2fb..1f4b1de14 100644 --- a/incoming/pattern-study/patterns/euler-strokes.js +++ b/incoming/pattern-study/patterns/euler-strokes.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Euler Strokes // Family: type -// Idea: A pen writes a word stacked down the panel letter by letter, each letter a graph of stroke ends, corners and junctions, in exactly as many strokes as Euler's count of its odd vertices allows, following a Hierholzer route: the odd vertices glow before the pen reaches them, every pen lift leaves a faint dotted trail, and beside each letter a digit and a row of pips keep its stroke count. +// Idea: A pen writes a word stacked down the panel letter by letter, each letter a graph of stroke ends, corners and junctions, in exactly as many strokes as Euler's count of its odd vertices allows, following a Hierholzer route: the unwritten skeleton is faintly tinted with the stroke each piece will belong to, the odd vertices glow before the pen reaches them, each pen lift leaves a faint dashed trail, and beside each letter a digit and a row of pips keep its stroke count. // Author: patternflow-lab // Date: 2026-10-30 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -11,10 +11,10 @@ // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Word=0..3, Order=0..1 // -// Knob 1: Hue. Turns every colour together. At 0 the first stroke of a letter is cyan, the second gold, the third magenta, the fourth violet; the odd vertices glow warm white and the unwritten skeleton is a pale blue grey. -// Knob 2: Speed. At 2 the pen writes 34 LEDs a second and travels with the pen up at 90, and rests 0.12 s on each touch and lift. PATH takes 11.7 s in the Euler order, then holds for 1.8 s and fades for 0.9 s before it is written again by another route. At 0.2 ten times slower, at 3 one and a half times faster. +// Knob 1: Hue. Turns every colour together. At 0 the first stroke of a letter is cyan, the second magenta, the third gold, the fourth green; the marks where strokes start and end are warm white on odd vertices and rose on even ones, and fresh ink is white hot for about 0.7 s. +// Knob 2: Speed. At 2 the pen writes 34 LEDs a second, travels with the pen up at 90 and rests 0.12 s on each touch and lift. PATH takes 10.5 to 10.8 s in the Euler order and 12.6 s in school order, then holds for 1.8 s and fades for 0.9 s before it is written again. At 0.2 ten times slower, at 3 one and a half times faster. // Knob 3: Word. PATH (1, 2, 2 and 3 strokes), 다리 (bridge: 3 and 2), EULER (2, 1, 1, 2, 1) or 한붓 (one brush: 6 and 6), the counts in the Euler order. Each letter is a vertex and edge list in the code; the word stacks one letter per cell down the 128 rows. -// Knob 4: Order. Below 0.5 the pen takes the fewest strokes, max(1, odd/2) for each connected piece, on a Hierholzer route that pairs the spare odd vertices with the shortest pen-up hops, and each pass picks a new route of the same count. Above 0.5 it follows school stroke order instead (ball and stick print for the Latin letters, 필순 for Hangul), and the counts grow: PATH 10 against 8, 다리 8 against 5, EULER 12 against 7, 한붓 14 against 12. Rings mark where each stroke touched down and lifted; in school order they land on even vertices too. +// Knob 4: Order. Below 0.5 the pen takes the fewest strokes, max(1, odd/2) for each connected piece, on a Hierholzer route that pairs the spare odd vertices with short pen-up hops, and each pass picks a new route of the same count (H has 11, 붓 32 seen in 3000 passes). Above 0.5 it follows school stroke order instead (ball and stick print for the Latin letters, 필순 for Hangul), and the counts grow: PATH 10 against 8, 다리 8 against 5, EULER 12 against 7, 한붓 14 against 12. A ring stays where each stroke touched down or lifted; rose rings on even vertices are lifts no Euler route needs. const W = 64, H = 128, NP = W * H; const { floor, round, min, max, sqrt, sin, cos, exp, abs, hypot, PI } = Math; diff --git a/incoming/pattern-study/patterns/steiner-tree.js b/incoming/pattern-study/patterns/steiner-tree.js new file mode 100644 index 000000000..623273b46 --- /dev/null +++ b/incoming/pattern-study/patterns/steiner-tree.js @@ -0,0 +1,439 @@ +// ===== Patternflow pattern ===== +// Title: Steiner Tree +// Family: geometry +// Idea: Four to seven terminals wander the panel, the minimum spanning tree joins them in faint dashes and the shortest network joins them in bright lines that meet three at a time at 120 degrees in marked Steiner points, and when two trees tie the network flips to the other one while the old one fades out amber; a bar at the foot reads the length ratio of the two trees against its floor of root 3 over 2. +// Author: patternflow-lab +// Date: 2026-10-30 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the Euclidean Steiner tree problem (Jakob Steiner's name; Vojtěch Jarník and Miloš Kössler, 1934), Steiner minimal trees and the root 3 over 2 ratio conjecture (E. N. Gilbert and H. O. Pollak, 1968), full Steiner topologies (Z. A. Melzak, 1961), the iteration for a fixed topology (Warren D. Smith, 1992), pattern round 75 +// Round: 75 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Terminals=4..7, Scatter=0..1 +// +// Knob 1: Hue. Turns every colour together: at 0 the Steiner tree is cyan, its Steiner points pink rings, the terminals warm white, the spanning tree dashed blue grey and a tree just left behind amber. +// Knob 2: Speed. How fast the terminals move. At 2 each wobbles about 2.5 LEDs round its place in 9 to 20 s and wanders at up to 6 LEDs a second when Scatter is up; at 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Terminals. 4 to 7. Every full Steiner topology is tried (3, 15, 105 or 945 of them) with Smith's iteration, a share each frame, and the tree on show is also checked against the 2 swaps across each of its inner edges every frame. +// Knob 4: Scatter. At 0 the terminals sit on a zigzag strip of equilateral triangles down the panel, side 60 LEDs for four terminals and 32 for seven. Four make a 60 degree rhombus whose two Steiner trees are mirror images of the same length, so the wobble flips the tree back and forth at a ratio near root 7 over 3, 0.882. Five and seven split into equilateral stars that share terminals, and the ratio sits on root 3 over 2, 0.866. At 1 the terminals wander the whole panel and the ratio mostly stays above 0.95. + +const W = 64, H = 128, NP = W * H; +const { floor, round, min, max, sqrt, sin, cos, exp, abs, hypot, PI } = Math; +const TAU = 2 * PI, FLOOR = sqrt(3) / 2; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v) { + 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); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- Full Steiner topologies. Terminals are nodes 0..n-1 and Steiner +// points n..2n-3, each of degree 3. Terminal k joins the topology of the +// first k by splitting one of its 2k - 3 edges, so there are 3, 15, 105 and +// 945 for n = 4 to 7. A degenerate tree, a Steiner point sitting on a +// terminal or two on one spot, is a full topology with a zero-length edge, +// so the shortest over all full topologies is the Steiner minimal tree. +const TOPS = {}; +function topologies(n) { + if (TOPS[n]) return TOPS[n]; + const out = []; + const rec = (edges, ins, k) => { + if (k === n) return void out.push(prep(n, edges, ins)); + const s = n + k - 2; + for (let i = 0; i < edges.length; i++) { + const [u, v] = edges[i], ne = edges.slice(); + ne[i] = [u, s]; + ne.push([s, v], [s, k]); + rec(ne, [...ins, [s, u, v, k]], k + 1); + } + }; + rec([[0, n], [1, n], [2, n]], [], 3); + return (TOPS[n] = out); +} + +// Adjacency and an order from the first Steiner point outwards, for the +// elimination in smith(). +function prep(n, edges, ins) { + const N = 2 * n - 2, adj = Array.from({ length: N }, () => []); + edges.forEach(([a, b], e) => { + adj[a].push([b, e]); + adj[b].push([a, e]); + }); + const order = [n], par = new Int8Array(N).fill(-1), seen = new Uint8Array(N); + seen[n] = 1; + for (let i = 0; i < order.length; i++) + for (const [b] of adj[order[i]]) + if (b >= n && !seen[b]) { + seen[b] = 1; + par[b] = order[i]; + order.push(b); + } + return { n, edges, ins, adj, order, par }; +} + +// A cold start: each Steiner point at the centroid of the three it was +// inserted between. +function initPos(T, X) { + const n = T.n; + X[2 * n] = (X[0] + X[2] + X[4]) / 3; + X[2 * n + 1] = (X[1] + X[3] + X[5]) / 3; + for (const [s, u, v, k] of T.ins) + for (let c = 0; c < 2; c++) X[2 * s + c] = (X[2 * u + c] + X[2 * v + c] + X[2 * k + c]) / 3; +} + +// Smith's iteration for a fixed topology: weight every edge by 1/length and +// solve the tree's linear system exactly, from the leaves of the Steiner +// points up and back down. The total length is convex in the Steiner points +// and each step can only shorten it. Returns the length after iters steps. +const WB = new Float64Array(16), AA = new Float64Array(16), BX = new Float64Array(16), BY = new Float64Array(16); +function smith(T, X, iters) { + const { n, edges, adj, order, par } = T; + let L = 0; + for (let it = 0; ; it++) { + L = 0; + for (let e = 0; e < edges.length; e++) { + const a = edges[e][0], b = edges[e][1], d = hypot(X[2 * a] - X[2 * b], X[2 * a + 1] - X[2 * b + 1]); + L += d; + WB[e] = 1 / max(d, 1e-4); + } + if (it === iters) return L; + for (let i = order.length - 1; i >= 0; i--) { + const c = order[i]; + let D = 0, bx = 0, by = 0, ap = 0; + for (const [b, e] of adj[c]) { + const w = WB[e]; + if (b < n) (D += w), (bx += w * X[2 * b]), (by += w * X[2 * b + 1]); + else if (b === par[c]) (D += w), (ap = w); + else (D += w * (1 - AA[b])), (bx += w * BX[b]), (by += w * BY[b]); + } + AA[c] = ap / D; + BX[c] = bx / D; + BY[c] = by / D; + } + for (const c of order) { + const p = par[c]; + X[2 * c] = (p >= 0 ? AA[c] * X[2 * p] : 0) + BX[c]; + X[2 * c + 1] = (p >= 0 ? AA[c] * X[2 * p + 1] : 0) + BY[c]; + } + } +} + +// The two other topologies across inner edge e: swap one neighbour of each +// end. Both ends start at the edge's middle, nudged towards their new pairs. +function swaps(T, X) { + const { n, edges } = T, out = []; + edges.forEach(([s1, s2], e) => { + if (s1 < n || s2 < n) return; + const nb = (s, o) => T.adj[s].filter(([q]) => q !== o); + const [[a], [b, eb]] = nb(s1, s2), [[c, ec], [d, ed]] = nb(s2, s1); + for (const [x, ex, y] of [[c, ec, d], [d, ed, c]]) { + // s1 keeps a and takes x from s2; s2 keeps y and takes b. + const ne = edges.map((p) => p.slice()); + ne[eb] = [s2, b]; + ne[ex] = [s1, x]; + const T2 = prep(n, ne, T.ins), X2 = X.slice(); + const mx = (X[2 * s1] + X[2 * s2]) / 2, my = (X[2 * s1 + 1] + X[2 * s2 + 1]) / 2; + for (const [s, p, q] of [[s1, a, x], [s2, b, y]]) { + const ux = (X[2 * p] + X[2 * q]) / 2 - mx, uy = (X[2 * p + 1] + X[2 * q + 1]) / 2 - my, l = hypot(ux, uy) || 1; + X2[2 * s] = mx + (0.4 * ux) / l; + X2[2 * s + 1] = my + (0.4 * uy) / l; + } + out.push([T2, X2]); + } + }); + return out; +} + +// The tree as drawn: edges shorter than 0.8 LED are contracted, and the key +// lists for every edge left the terminals on its far side from terminal 0. +// A change of topology that leaves the key alone does not show. +function drawnKey(T, X) { + const { n, edges } = T, N = 2 * n - 2, par = [...Array(N).keys()]; + const find = (x) => (par[x] === x ? x : (par[x] = find(par[x]))); + const len = (e) => hypot(X[2 * e[0]] - X[2 * e[1]], X[2 * e[0] + 1] - X[2 * e[1] + 1]); + for (const e of edges) if (len(e) < 0.8) par[find(e[0])] = find(e[1]); + const keep = edges.filter((e) => len(e) >= 0.8), ks = []; + for (const [a, b] of keep) { + let m = 0; + const seen = new Set([find(a), find(b)]), st = [find(b)]; + while (st.length) { + const c = st.pop(); + for (let t = 0; t < n; t++) if (find(t) === c) m |= 1 << t; + for (const [p, q] of keep) { + const fp = find(p), fq = find(q), o = fp === c ? fq : fq === c ? fp : -1; + if (o >= 0 && !seen.has(o)) seen.add(o), st.push(o); + } + } + if (m & 1) m = ((1 << n) - 1) & ~m; + ks.push(m); + } + return ks.sort((x, y) => x - y).join(","); +} + +function mst(n, X) { + const inT = new Uint8Array(n), d = new Float64Array(n).fill(1e9), from = new Int8Array(n).fill(-1), E = []; + d[0] = 0; + let L = 0; + for (let it = 0; it < n; it++) { + let u = -1; + for (let i = 0; i < n; i++) if (!inT[i] && (u < 0 || d[i] < d[u])) u = i; + inT[u] = 1; + L += d[u]; + if (from[u] >= 0) E.push([from[u], u]); + for (let v = 0; v < n; v++) + if (!inT[v]) { + const dd = hypot(X[2 * u] - X[2 * v], X[2 * u + 1] - X[2 * v + 1]); + if (dd < d[v]) (d[v] = dd), (from[v] = u); + } + } + return { L, E }; +} + +// ---- Where the terminals are. On the strip, terminal i sits on a zigzag of +// equilateral triangles of side a down the middle of the panel; free, it +// follows two sines in x and two in y over the whole panel. +function place(S, scatter) { + const n = S.n, a = min(60, 192 / (n - 1)), sep = (a * sqrt(3)) / 2, t = S.tau; + const sx = 32 + 2.5 * sin(0.043 * t), sy = 58 + 3 * sin(0.031 * t + 1); + for (let i = 0; i < n; i++) { + const f = S.freq[i]; + const wx = 0.035 * a * sin(f[0] * t + f[4]), wy = 0.035 * a * sin(f[1] * t + f[5]); + const lx = sx + (i % 2 ? 0.5 : -0.5) * sep + wx, ly = sy + (i - (n - 1) / 2) * (a / 2) + wy; + const fx = 32 + 26 * (0.62 * sin(f[2] * t + f[6]) + 0.38 * sin(f[3] * t * 1.7 + f[7])), fy = 58 + 50 * (0.62 * sin(f[3] * t + f[7]) + 0.38 * sin(f[2] * t * 1.3 + f[4])); + S.X[2 * i] = cl(lx + (fx - lx) * scatter, 3, 61); + S.X[2 * i + 1] = cl(ly + (fy - ly) * scatter, 4, 110); + } +} + +function resolve(S) { + // Every topology from a cold start, the shortest kept and polished. + let best = null; + for (const T of topologies(S.n)) { + const X = S.X.slice(); + initPos(T, X); + const L = smith(T, X, 25); + if (!best || L < best[2]) best = [T, X, L]; + } + S.T = best[0]; + S.X = best[1]; + S.L = smith(S.T, S.X, 60); + S.key = drawnKey(S.T, S.X); + S.sweep = 0; +} + +export function setup(params) { + const freq = []; + for (let i = 0; i < 7; i++) { + const r = () => Math.random(); + freq.push([TAU / (9 + 11 * r()), TAU / (9 + 11 * r()), 0.05 + 0.05 * r(), 0.05 + 0.05 * r(), TAU * r(), TAU * r(), TAU * r(), TAU * r()]); + } + params.s = { n: 0, tau: 0, t: 0, freq, X: null, T: null, L: 0, M: 1, pool: [], old: null, oldT: -9, boost: 0, sg: new Float64Array(80), F: new Float32Array(NP), rgb: new Float32Array(NP * 3) }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 4, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.t += d; + S.tau += (d * cl(k[1], 0.2, 3)) / 2; + const n = cl(round(k[2]), 4, 7), scatter = cl(k[3], 0, 1) ** 2; + if (n !== S.n) { + S.n = n; + S.X = new Float64Array(2 * (2 * n - 2)); + place(S, scatter); + resolve(S); + S.old = null; + S.pool = []; + } + place(S, scatter); + S.L = smith(S.T, S.X, 4); + // Candidates: the swaps across each inner edge, warm; a share of all + // topologies, cold; and the near misses of earlier sweeps, warm. Smith's + // steps only shorten, so a candidate shorter than the polished tree on + // show is truly shorter. + let cand = null; + const better = (T, X, L) => { + if (L < S.L * (1 - 2e-4) && (!cand || L < cand[2])) cand = [T, X, L]; + }; + for (const [T2, X2] of swaps(S.T, S.X)) better(T2, X2, smith(T2, X2, 12)); + for (const q of S.pool) { + q.X.set(S.X.subarray(0, 2 * n)); + q.L = smith(q.T, q.X, 10); + q.age += d; + better(q.T, q.X, q.L); + } + S.pool = S.pool.filter((q) => q.age < 3 && q.L < S.L * 1.03 && q.T !== S.T); + const all = topologies(n), per = min(all.length, 40); + for (let i = 0; i < per; i++) { + const T = all[(S.sweep + i) % all.length], X = S.X.slice(); + initPos(T, X); + const L = smith(T, X, 15); + better(T, X, L); + // A cold start a little longer than the tree on show may still be the + // shorter one once it settles, so it stays in the pool for 3 s. + if (L < S.L * 1.03 && T !== S.T && S.pool.length < 8 && !S.pool.some((q) => q.T === T)) S.pool.push({ T, X, L, age: 0 }); + } + S.sweep = (S.sweep + per) % all.length; + if (cand) { + const key = drawnKey(cand[0], cand[1]); + if (key !== S.key) { + // A visible flip: keep the old tree to fade out. + S.old = segs(S.T, S.X); + S.oldT = S.t; + S.boost = 1; + } + S.T = cand[0]; + S.X = cand[1]; + S.L = smith(S.T, S.X, 6); + S.key = key; + } + S.mst = mst(n, S.X); + S.M = S.mst.L; + S.boost *= exp(-d * 2.5); +} + +function segs(T, X) { + const out = []; + for (const [a, b] of T.edges) { + const ax = X[2 * a], ay = X[2 * a + 1], bx = X[2 * b], by = X[2 * b + 1]; + if (hypot(bx - ax, by - ay) > 0.05) out.push([ax, ay, bx, by]); + } + return out; +} + +// Soft line, added. +function line(rgb, ax, ay, bx, by, col, a, w, dash) { + const dx = bx - ax, dy = by - ay, L2 = dx * dx + dy * dy, L = sqrt(L2), R = w + 1.5; + for (let y = max(0, floor(min(ay, by) - R)); y <= min(H - 1, floor(max(ay, by) + R)); y++) { + // Only the stretch of the row the segment can reach. + let x0 = min(ax, bx), x1 = max(ax, bx); + if (abs(dy) > 1e-6) { + const t0 = cl((y + 0.5 - R - ay) / dy, 0, 1), t1 = cl((y + 0.5 + R - ay) / dy, 0, 1); + x0 = ax + min(t0, t1) * dx; + x1 = ax + max(t0, t1) * dx; + if (x0 > x1) [x0, x1] = [x1, x0]; + } + for (let x = max(0, floor(x0 - R)); x <= min(W - 1, floor(x1 + R)); x++) { + const px = x + 0.5 - ax, py = y + 0.5 - ay, t = L2 > 0 ? cl((px * dx + py * dy) / L2, 0, 1) : 0; + if (dash && (t * L) % 4 > 2) continue; + const dd = hypot(px - t * dx, py - t * dy), v = a * cl(w + 0.5 - dd, 0, 1); + if (v <= 0) continue; + const o = 3 * (y * W + x); + for (let c = 0; c < 3; c++) rgb[o + c] = max(rgb[o + c], col[c] * v); + } + } +} + +// A disc laid over what is there. +function spot(rgb, x, y, r, col, a) { + for (let j = max(0, floor(y - r - 1)); j <= min(H - 1, floor(y + r + 1)); j++) + for (let i = max(0, floor(x - r - 1)); i <= min(W - 1, floor(x + r + 1)); i++) { + const v = a * cl(r + 0.5 - hypot(i + 0.5 - x, j + 0.5 - y), 0, 1); + if (v <= 0) continue; + const o = 3 * (j * W + i); + for (let c = 0; c < 3; c++) rgb[o + c] = rgb[o + c] * (1 - v) + col[c] * v; + } +} + +// Field brightness by distance from the tree, in steps of 0.05 LED. +const RING = new Float32Array(3000).map((_, i) => { + const dd = i / 20, ring = Math.pow(0.5 + 0.5 * cos((TAU * dd) / 6), 6) * 0.17 * exp(-dd / 26); + return max(dd > 2 ? ring : 0, 0.3 * exp((-dd * dd) / 5)); +}); + +// 3 by 5 digits for the ratio. +const DIGITS = [0x7b6f, 0x2c97, 0x73e7, 0x73cf, 0x5bc9, 0x79cf, 0x79ef, 0x7292, 0x7bef, 0x7bcf]; +function digit(rgb, n, x, y, col) { + for (let r = 0; r < 5; r++) + for (let c = 0; c < 3; c++) + if ((DIGITS[n] >> (14 - r * 3 - c)) & 1) { + const o = 3 * ((y + r) * W + x + c); + for (let q = 0; q < 3; q++) rgb[o + q] = col[q]; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.T) return; + const { rgb } = S, h0 = 0.5 + S.hue; + const tree = hsv(h0, 0.5, 1), glowC = hsv(h0 + 0.03, 0.75, 1), pink = hsv(h0 + 0.42, 0.5, 1), warm = hsv(h0 + 0.6, 0.15, 1), amber = hsv(h0 + 0.6, 0.75, 1), mstC = hsv(h0 + 0.1, 0.3, 0.62), bg = hsv(h0 + 0.12, 0.7, 0.03); + const sg = segs(S.T, S.X), ns = sg.length, SG = S.sg, core = []; + sg.forEach(([ax, ay, bx, by], i) => { + const dx = bx - ax, dy = by - ay; + SG.set([ax, ay, dx, dy, 1 / (dx * dx + dy * dy)], 5 * i); + }); + // Distance from each LED to the tree: a glow along it and faint rings + // every 6 LEDs that spread out from it, looked up by distance. It is + // worked exactly on every second LED each way and within 3.5 LEDs of the + // tree, and read off its four exact neighbours everywhere else. + const F = S.F, dist = (px, py) => { + let m = 1e9; + for (let i = 0; i < 5 * ns; i += 5) { + const ex0 = px - SG[i], ey0 = py - SG[i + 1], dx = SG[i + 2], dy = SG[i + 3]; + let t = (ex0 * dx + ey0 * dy) * SG[i + 4]; + t = t < 0 ? 0 : t > 1 ? 1 : t; + const ex = ex0 - t * dx, ey = ey0 - t * dy, q = ex * ex + ey * ey; + if (q < m) m = q; + } + return sqrt(m); + }; + for (let y = 0; y < H; y += 2) for (let x = 0; x < W; x += 2) F[y * W + x] = dist(x + 0.5, y + 0.5); + for (let y = 0, p = 0; y < H; y++) { + const y0 = y & ~1, y1 = y0 + 2 < H ? y0 + 2 : y0, fy = y & 1 ? 0.5 : 0; + for (let x = 0; x < W; x++, p++) { + let dd = F[p]; + if ((x | y) & 1) { + const x0 = x & ~1, x1 = x0 + 2 < W ? x0 + 2 : x0, fx = x & 1 ? 0.5 : 0; + const a = F[y0 * W + x0], b = F[y0 * W + x1], c = F[y1 * W + x0], e = F[y1 * W + x1]; + dd = min(a, b, c, e) < 3.5 ? dist(x + 0.5, y + 0.5) : (a * (1 - fx) + b * fx) * (1 - fy) + (c * (1 - fx) + e * fx) * fy; + } + const v = RING[min(RING.length - 1, (dd * 20) | 0)], o = 3 * p; + rgb[o] = bg[0] + glowC[0] * v; + rgb[o + 1] = bg[1] + glowC[1] * v; + rgb[o + 2] = bg[2] + glowC[2] * v; + // The tree's own line, 1.7 LEDs wide, laid on after the dashes. + if (dd < 1.35) core.push(p, 1.35 - dd); + } + } + // The minimum spanning tree, dashed and faint. + for (const [a, b] of S.mst.E) line(rgb, S.X[2 * a], S.X[2 * a + 1], S.X[2 * b], S.X[2 * b + 1], mstC, 1, 0.45, true); + // A tree just left behind fades out amber. + const age = S.t - S.oldT; + if (S.old && age < 1.2) for (const [ax, ay, bx, by] of S.old) line(rgb, ax, ay, bx, by, amber, 0.9 * (1 - age / 1.2), 0.7); + const bright = [0, 1, 2].map((c) => tree[c] + (1 - tree[c]) * 0.7 * S.boost); + for (let i = 0; i < core.length; i += 2) { + const o = 3 * core[i], v = min(1, core[i + 1]); + for (let c = 0; c < 3; c++) rgb[o + c] = rgb[o + c] * (1 - v) + bright[c] * v; + } + // Steiner points that stand apart from every terminal are pink dots. + for (let s = S.n; s < 2 * S.n - 2; s++) { + const x = S.X[2 * s], y = S.X[2 * s + 1]; + let near = 1e9; + for (let i = 0; i < S.n; i++) near = min(near, hypot(x - S.X[2 * i], y - S.X[2 * i + 1])); + if (near > 1.2) spot(rgb, x, y, 1.5, pink, cl(near - 1.2, 0, 1)); + } + for (let i = 0; i < S.n; i++) spot(rgb, S.X[2 * i], S.X[2 * i + 1], 1.9, warm, 1); + // The ratio: a bar from root 3 over 2 at the left to 1 at the right, and + // the number above it. + const r = S.M > 0 ? S.L / S.M : 1, fill = cl((r - FLOOR) / (1 - FLOOR), 0, 1); + for (let x = 3; x <= 60; x++) { + const u = (x - 3) / 57, on = u <= fill; + for (let y = 121; y <= 124; y++) { + const o = 3 * (y * W + x), v = on ? 0.85 : 0.12; + for (let c = 0; c < 3; c++) rgb[o + c] = (on ? tree : mstC)[c] * v; + } + } + for (let y = 119; y <= 126; y++) for (let c = 0; c < 3; c++) rgb[3 * (y * W + 2) + c] = pink[c]; + const txt = r.toFixed(3); + let cx = 3; + for (const ch of txt) { + if (ch === ".") { + for (let c = 0; c < 3; c++) rgb[3 * (117 * W + cx) + c] = warm[c] * 0.85; + cx += 2; + continue; + } + digit(rgb, +ch, cx, 113, warm); + cx += 4; + } + for (let p = 0; p < NP; p++) display.setPixel(p % W, floor(p / W), min(255, rgb[3 * p] * 255) | 0, min(255, rgb[3 * p + 1] * 255) | 0, min(255, rgb[3 * p + 2] * 255) | 0); +} From 5bde353339ee079d0b5fc19ac11eae8638e7ae35 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 01:08:53 +0000 Subject: [PATCH 18/22] =?UTF-8?q?Miura-ori=20=EC=9D=98=20=EB=B9=9B?= =?UTF-8?q?=EA=B3=BC=20=EC=84=A4=EB=AA=85=EC=9D=84=20=EA=B3=A0=EC=B9=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PBtzYrhzPXCgRY4TG --- incoming/pattern-study/patterns/miura-ori.js | 103 ++++++++++++------- 1 file changed, 63 insertions(+), 40 deletions(-) diff --git a/incoming/pattern-study/patterns/miura-ori.js b/incoming/pattern-study/patterns/miura-ori.js index a94fa3a10..43ae740f4 100644 --- a/incoming/pattern-study/patterns/miura-ori.js +++ b/incoming/pattern-study/patterns/miura-ori.js @@ -1,20 +1,20 @@ // ===== Patternflow pattern ===== // Title: Miura-ori // Family: space -// Idea: A Miura-ori sheet of congruent parallelograms folds and unfolds with its one degree of freedom, seen in 3D from above and a little below, so its rows of facets flip between the warm key light and the cool fill as the fold deepens, its mountain and valley creases show as light and dark lines, and the whole sheet shrinks in both directions at once and spreads out again. +// Idea: A Miura-ori sheet of congruent parallelograms, bigger than the panel, folds and opens again on its one degree of freedom, seen in 3D from a little below, so its facets split into four kinds that flip between a warm key light, a cool fill, both and neither as the fold deepens, its mountain and valley creases show as light and dark lines, and at the deepest fold the sheet has shrunk across and down at once until its edges come into view on all four sides. // Author: patternflow-lab // Date: 2026-10-30 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: Miura-ori (Koryo Miura, 1970; "Method of packaging and deployment of large membranes in space", 1985), its rigid folding as a translational surface (Mark Schenk and Simon Guest, 2013), pattern round 75 +// Lineage: Miura-ori (Koryo Miura, 1970; "Method of packaging and deployment of large membranes in space", 1985), its closed-form rigid folding and in-plane Poisson ratio (Mark Schenk and Simon Guest, 2013), pattern round 75 // Round: 75 // =============================== // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Sector=55..85, Light=0..1 // -// Knob 1: Hue. The colour of the key light; the fill light from the other side takes the opposite hue. -// Knob 2: Speed. How fast the sheet folds and opens. -// Knob 3: Sector. The sector angle alpha of the parallelograms, in degrees. -// Knob 4: Light. Where the key light stands round the sheet. +// Knob 1: Hue. The colour of the two lights, which keep opposite hues. At 0 the key is amber and the fill blue, so a facet that faces both is cream, one that faces only the key amber, only the fill blue, and neither almost black; at 0.5 the key is sky blue and the fill salmon. +// Knob 2: Speed. At 2 the sheet folds from almost flat to its deepest fold in 4 s and opens again in 4 s; 40 s each way at 0.2 and 2.7 s at 3. +// Knob 3: Sector. The sector angle alpha, the acute angle between each parallelogram's 12 and 10 LED sides, from 55 to 85 degrees. At 55 the deepest fold shrinks the sheet to 57% across and 67% down, and halfway there it shrinks 0.66% across for every 1% down. At 70 it goes to 78% and 52%. At 85 the facets are nearly rectangles, the sheet only shortens, to 51%, and the creases run as straight bands. +// Knob 4: Light. Walks both lights once round the sheet. The key stands 45 degrees up and the fill 15 degrees up a quarter turn round from it; at 0.06 the key comes from the top of the panel and the fill from its right. const W = 64, H = 128, SW = 128, SH = 256, NS = SW * SH; const { floor, ceil, round, sqrt, cos, sin, acos, min, max, abs, PI } = Math; @@ -29,19 +29,27 @@ function hsv(h, s, v, o, k) { o[k + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; } -// ---- The fold. A Miura-ori is a translational surface: P(i, j) = A(i) + B(j), +// ---- The fold. A Miura-ori is a translational surface, P(i, j) = A(i) + B(j), // with A a zigzag in the (v, z) plane whose steps are // eb(i) = (0, b cos psi, +-b sin psi) // and B a zigzag in the (u, v) plane whose steps are // ea(j) = (a sin g, +-a cos g, 0). // Every facet is then a parallelogram with sides eb(i) and ea(j), so its // sides stay b and a whatever psi is, and its angle stays alpha (or pi - -// alpha, the same parallelogram turned over) as long as eb.ea = +-ab cos alpha, -// that is cos psi cos g = cos alpha. psi = 0 is the flat sheet (g = alpha); -// psi = alpha folds it flat into a packet (g = 0). The one parameter psi is -// the fold's one degree of freedom. u runs across the panel, v down it, z -// out of the sheet towards the eye. -const M = 17, N = 11, NV = (M + 1) * (N + 1); // facets down and across, vertices +// alpha, the same parallelogram turned over) while eb.ea = +-ab cos alpha, +// that is cos psi cos g = cos alpha. psi = 0 is the flat sheet (g = alpha) +// and psi = alpha the sheet folded flat into a packet (g = 0); psi is the +// fold's one degree of freedom. It is Schenk and Guest's closed form with +// sin psi = sin theta sin alpha, theta the angle between each facet and the +// sheet's plane. u runs across the panel, v down it, z out of the sheet +// towards the eye. Checked in Node for alpha from 55 to 85 degrees by 1 and +// psi from 0 to alpha in 400 steps, 12,431 shapes of 187 facets: every side +// stays 12 or 10 to within 6e-14, every corner's cosine stays cos alpha to +// within 2e-15, every facet stays flat to within 3e-14, and the four angles +// round each inside vertex add to 2 pi to within 6e-15. +// 17 facets down and 11 across: flat, the sheet is 191 rows by 90 LEDs on +// the panel; at the deepest fold at 55 degrees it is 125 by 52. +const M = 17, N = 11, NV = (M + 1) * (N + 1); const B_LEN = 12, A_LEN = 10; function sheet(alpha, psi, out) { const g = acos(cl(cos(alpha) / cos(psi), -1, 1)); @@ -60,10 +68,10 @@ function sheet(alpha, psi, out) { export function setup(params) { params.s = { t: 0, hue: 0, speed: 2, alpha: 55 * DEG, light: 0.06, - P: new Float64Array(NV * 3), sx: new Float32Array(NV), sy: new Float32Array(NV), sz: new Float32Array(NV), + P: new Float64Array(NV * 3), sx: new Float32Array(NV), sy: new Float32Array(NV), key: new Float32Array(3), fill: new Float32Array(3), - id: new Int16Array(NS), depth: new Float32Array(NS), mark: new Uint8Array(NS), - col: new Float32Array(M * N * 3), mv: new Int8Array(M * N * 2), rgb: new Float32Array(3), + id: new Int16Array(NS), mark: new Uint8Array(NS), + col: new Float32Array(M * N * 3), mv: new Int8Array(M * N * 2), }; } @@ -77,32 +85,36 @@ export function update(dt, input, params) { S.t += (d * S.speed) / 2; } -const TILT = 28 * DEG; // the eye leans this far from straight above, towards the bottom of the panel +const TILT = 24 * DEG; // how far the eye leans from straight above const FOLD = 8; // seconds a fold and an opening at Speed 2 export function draw(display, params) { const S = params.s; if (!S) return; - const { P, sx, sy, sz, id, depth, mark, col, mv, rgb, key, fill } = S; + const { P, sx, sy, id, mark, col, mv, key, fill } = S; const f = 0.5 - 0.5 * cos((2 * PI * S.t) / FOLD); - const psi = S.alpha * (0.03 + 0.87 * f); + // psi runs from 3% to 100% of its deepest, 0.9 alpha or 60 degrees, + // whichever is less: at 55 degrees the facets fold up to 68 degrees from + // the sheet's plane. + const psi = min(S.alpha * 0.9, PI / 3) * (0.03 + 0.97 * f); sheet(S.alpha, psi, P); const ct = cos(TILT), st = sin(TILT); - // Vertices on the sample grid (two samples per LED each way), and their - // depth towards the eye. + // Vertices on the sample grid, two samples per LED each way. The eye looks + // down at the sheet leaning TILT towards the bottom of the panel, so z + // lifts a point up the screen. for (let k = 0, q = 0; k < NV; k++, q += 3) { sx[k] = (W / 2 + P[q]) * 2; sy[k] = (H / 2 + P[q + 1] * ct - P[q + 2] * st) * 2; - sz[k] = P[q + 1] * st + P[q + 2] * ct; } - // Lights, in sheet coordinates: the key from the Light knob's side, 40 - // degrees up; the fill from the opposite side, 12 degrees up. - const az = 2 * PI * (S.light + 0.69), e1 = 35 * DEG, e2 = 15 * DEG; + // Lights, in sheet coordinates: the key from the Light knob's side, 45 + // degrees up; the fill a quarter turn round from it, 15 degrees up. A + // facet's four kinds (its row tilting up or down the sheet, its column + // left or right) take these two lights four ways. + const az = 2 * PI * (S.light + 0.69), e1 = 45 * DEG, e2 = 15 * DEG; const az2 = az + PI / 2, k1x = cos(e1) * cos(az), k1y = cos(e1) * sin(az), k1z = sin(e1), k2x = cos(e2) * cos(az2), k2y = cos(e2) * sin(az2), k2z = sin(e2); hsv(0.1 + S.hue, 0.7, 1, key, 0); hsv(0.58 + S.hue, 0.9, 0.75, fill, 0); id.fill(-1); - depth.fill(-1e9); for (let i = 0; i < M; i++) for (let j = 0; j < N; j++) { const v0 = i * (N + 1) + j, v1 = v0 + 1, v3 = v0 + N + 1, v2 = v3 + 1, fi = i * N + j; @@ -116,7 +128,7 @@ export function draw(display, params) { // The back of the sheet shows where the turned normal points away from the eye. const sgn = ny * st + nz * ct >= 0 ? 1 : -1; const d1 = max(0, sgn * (nx * k1x + ny * k1y + nz * k1z)), d2 = max(0, sgn * (nx * k2x + ny * k2y + nz * k2z)), back = sgn < 0 ? 0.5 : 1; - for (let c = 0; c < 3; c++) col[fi * 3 + c] = back * (0.04 + key[c] * d1 + fill[c] * d2); + for (let c = 0; c < 3; c++) col[fi * 3 + c] = back * (0.1 + key[c] * d1 + fill[c] * d2); // Mountain or valley for the crease to the next facet down (side 0) // and across (side 1): a mountain when that facet's far corner lies // below this facet's plane. @@ -128,25 +140,36 @@ export function draw(display, params) { const q = (v1 + 1) * 3; mv[fi * 2 + 1] = (P[q] - P[q1]) * nx + (P[q + 1] - P[q1 + 1]) * ny + (P[q + 2] - P[q1 + 2]) * nz < 0 ? 1 : 2; } - // Rasterize: a sample is inside when it is on the inner side of all - // four edges; its depth comes from the facet's plane. - const X0 = sx[v0], Y0 = sy[v0], Z0 = sz[v0]; - const ux = sx[v1] - X0, uy = sy[v1] - Y0, uz = sz[v1] - Z0, wx = sx[v3] - X0, wy = sy[v3] - Y0, wz = sz[v3] - Z0; + // Rasterize. Seen from straight above, the folded sheet is a height + // field over (u, v), and a ray from the eye leaning towards +v meets + // larger v first, so drawing the rows in order down the sheet leaves + // the nearest facet on top (checked against a depth buffer: the same + // frames). + const X0 = sx[v0], Y0 = sy[v0]; + const ux = sx[v1] - X0, uy = sy[v1] - Y0, wx = sx[v3] - X0, wy = sy[v3] - Y0; const det = ux * wy - uy * wx; if (abs(det) < 1e-6) continue; const xmin = max(0, floor(min(X0, sx[v1], sx[v2], sx[v3]))), xmax = min(SW - 1, ceil(max(X0, sx[v1], sx[v2], sx[v3]))); const ymin = max(0, floor(min(Y0, sy[v1], sy[v2], sy[v3]))), ymax = min(SH - 1, ceil(max(Y0, sy[v1], sy[v2], sy[v3]))); - const inv = 1 / det; + const inv = 1 / det, da = wy * inv, db = -uy * inv; for (let y = ymin; y <= ymax; y++) { - const ry = y + 0.5 - Y0; - for (let x = xmin; x <= xmax; x++) { - const rx = x + 0.5 - X0; - // The sample as a + b along the two sides: inside when both lie in 0..1. - const a = (rx * wy - ry * wx) * inv, b = (ux * ry - uy * rx) * inv; - if (a < 0 || a > 1 || b < 0 || b > 1) continue; - const zz = Z0 + a * uz + b * wz, p = y * SW + x; - if (zz > depth[p]) (depth[p] = zz), (id[p] = fi); + const ry = y + 0.5 - Y0, rx = xmin + 0.5 - X0; + // The sample as a + b along the two sides is inside when both lie in + // 0..1; along a row each is linear in x, so the row's run is solved + // for, not searched. + const a0 = (rx * wy - ry * wx) * inv, b0 = (ux * ry - uy * rx) * inv; + let lo = 0, hi = xmax - xmin; + for (let e = 0; e < 2; e++) { + const v0 = e ? b0 : a0, dv = e ? db : da; + if (abs(dv) < 1e-12) { + if (v0 < 0 || v0 > 1) hi = -1; + } else { + const t0 = -v0 / dv, t1 = (1 - v0) / dv; + lo = max(lo, ceil(t0 < t1 ? t0 : t1)); + hi = min(hi, floor(t0 < t1 ? t1 : t0)); + } } + for (let k = y * SW + xmin + lo, e = y * SW + xmin + hi; k <= e; k++) id[k] = fi; } } // Lines: a sample whose right or lower neighbour belongs to another facet From ae33f1269d39c4c47922c0b58e5b1f4446a7d3fe Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 01:10:03 +0000 Subject: [PATCH 19/22] =?UTF-8?q?Prandtl's=20Membrane=20=EC=99=80=20Miura-?= =?UTF-8?q?ori=20=ED=8C=A8=ED=84=B4=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PBtzYrhzPXCgRY4TG --- incoming/pattern-study/patterns/prandtl-membrane.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/patterns/prandtl-membrane.js b/incoming/pattern-study/patterns/prandtl-membrane.js index bcb7355b9..761b723af 100644 --- a/incoming/pattern-study/patterns/prandtl-membrane.js +++ b/incoming/pattern-study/patterns/prandtl-membrane.js @@ -11,10 +11,10 @@ // @matrix 64x128 // @knobs Hue=0..1, Twist=0.2..3, Word=0..3, Lamp=0..1 // -// Knob 1: Hue. Turns the stress colours round the wheel: at 0 the crests, where the stress is zero, are blue, the rims run through magenta and red, and stress past the hot end burns orange to yellow. The plates in the holes take the blue, dim. +// Knob 1: Hue. Turns the stress colours round the wheel: at 0 the crests, where the stress is zero, are blue, the rims run from magenta through red to orange, and the hottest places, inside corners and the outer walls of the hollow letters, burn yellow-orange. The plates in the holes take the blue, dim. // Knob 2: Twist. How hard the bar is twisted, which is how hard the film is blown up. Each breath, one every 4 s, swings the twist from 40% to 100% of the knob. At 2 the film stands at 56 degrees at the rim of the 14 LED bar of ㅜ at the top of a breath and 31 at the bottom, and the colour reaches its hot end at a stress of 12.7. At 0.2 the film over that bar tilts 9 degrees at most and the colours stay blue and violet; at 3 the same rim stands at 66 degrees and runs yellow. // Knob 3: Word. The cross-section: the syllable 숨 (breath) the height of the panel, then LIT, AIR and HOT, three capitals 56 by 38 LEDs stacked. A letter with a hole is a hollow section, and the hole is a flat plate the film lifts: at the same twist HOT carries four times the torque of LIT. -// Knob 4: Lamp. How high the lamp stands as it walks round the word once in 12 s: 19 degrees at 0.06, low enough that each stroke is lit on one side and shaded on the other and a glint runs along the crests; 75 degrees at 1, where the film is lit almost evenly and only the colours move. +// Knob 4: Lamp. How high the lamp stands as it walks round the word once in 12 s: 19 degrees at 0.06, low enough that each stroke is lit on one side and shaded on the other, with a pale glint halfway down the flank that faces the lamp; 75 degrees at 1, where the film is lit almost evenly, the glint rides the crests and mostly the colours move. const W = 64, H = 128; const { floor, round, sqrt, cos, sin, min, max, abs, PI } = Math; From e1872245dc26116bcdef3ec50aa839d0f69957e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 01:13:52 +0000 Subject: [PATCH 20/22] =?UTF-8?q?Euler=20Strokes=EC=99=80=20Steiner=20Tree?= =?UTF-8?q?=20=ED=8C=A8=ED=84=B4=EC=9D=84=20=EB=8B=A4=EB=93=AC=EB=8A=94?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PBtzYrhzPXCgRY4TG --- incoming/pattern-study/patterns/euler-strokes.js | 12 ++++++------ incoming/pattern-study/patterns/steiner-tree.js | 16 +++++++++------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/incoming/pattern-study/patterns/euler-strokes.js b/incoming/pattern-study/patterns/euler-strokes.js index 1f4b1de14..5e80187cf 100644 --- a/incoming/pattern-study/patterns/euler-strokes.js +++ b/incoming/pattern-study/patterns/euler-strokes.js @@ -14,7 +14,7 @@ // Knob 1: Hue. Turns every colour together. At 0 the first stroke of a letter is cyan, the second magenta, the third gold, the fourth green; the marks where strokes start and end are warm white on odd vertices and rose on even ones, and fresh ink is white hot for about 0.7 s. // Knob 2: Speed. At 2 the pen writes 34 LEDs a second, travels with the pen up at 90 and rests 0.12 s on each touch and lift. PATH takes 10.5 to 10.8 s in the Euler order and 12.6 s in school order, then holds for 1.8 s and fades for 0.9 s before it is written again. At 0.2 ten times slower, at 3 one and a half times faster. // Knob 3: Word. PATH (1, 2, 2 and 3 strokes), 다리 (bridge: 3 and 2), EULER (2, 1, 1, 2, 1) or 한붓 (one brush: 6 and 6), the counts in the Euler order. Each letter is a vertex and edge list in the code; the word stacks one letter per cell down the 128 rows. -// Knob 4: Order. Below 0.5 the pen takes the fewest strokes, max(1, odd/2) for each connected piece, on a Hierholzer route that pairs the spare odd vertices with short pen-up hops, and each pass picks a new route of the same count (H has 11, 붓 32 seen in 3000 passes). Above 0.5 it follows school stroke order instead (ball and stick print for the Latin letters, 필순 for Hangul), and the counts grow: PATH 10 against 8, 다리 8 against 5, EULER 12 against 7, 한붓 14 against 12. A ring stays where each stroke touched down or lifted; rose rings on even vertices are lifts no Euler route needs. +// Knob 4: Order. Below 0.5 the pen takes the fewest strokes, max(1, odd/2) for each connected piece, on a Hierholzer route that pairs the spare odd vertices with short pen-up hops, and each pass picks a new route of the same count (H has 11, 붓 32 seen in 3000 passes). Above 0.5 it follows school stroke order instead (ball and stick print for the Latin letters, 필순 for Hangul), and the counts grow: PATH 10 against 8, 다리 8 against 5, EULER 12 against 7, 한붓 14 against 12. A ring stays where each stroke touched down or lifted. Rose rings sit on even vertices: in the Euler order only a closed piece (the ring of ㅎ) touches one, in school order P, A, E, ㄷ, ㄹ and ㅂ do too. const W = 64, H = 128, NP = W * H; const { floor, round, min, max, sqrt, sin, cos, exp, abs, hypot, PI } = Math; @@ -281,8 +281,8 @@ function posAt(m, s) { return [pts[2 * i - 2] + (pts[2 * i] - pts[2 * i - 2]) * u, pts[2 * i - 1] + (pts[2 * i + 1] - pts[2 * i - 1]) * u]; } -// Ink the move from arc length a to b: a capsule per piece for strokes, a dot -// every 3 LEDs for pen-up hops. +// Ink the move from arc length a to b: a capsule per piece for strokes, faint +// dashes for pen-up hops. function inkSpan(S, m, a, b) { if (b <= a) return; if (m.up) { @@ -305,7 +305,8 @@ function inkSpan(S, m, a, b) { } // A round-ended segment: a solid core of radius r and a soft glow around it. -// With col null it writes one channel (the ghost). +// With col null it writes one channel (the ghost, with the stroke index in +// kb, or the hot ink). function capsule(buf, col, ax, ay, bx, by, r, a, glow, kb, kv) { const R = r + (glow ? 5 : 1), dx = bx - ax, dy = by - ay, L2 = dx * dx + dy * dy; for (let y = max(0, floor(min(ay, by) - R)); y <= min(H - 1, floor(max(ay, by) + R)); y++) @@ -317,8 +318,7 @@ function capsule(buf, col, ax, ay, bx, by, r, a, glow, kb, kv) { const p = y * W + x; if (!col) { if (v > buf[p]) (buf[p] = v), kb && (kb[p] = kv); - } - else for (let c = 0; c < 3; c++) buf[3 * p + c] = max(buf[3 * p + c], col[c] * v * (core > 0.5 ? 1 : 0.85)); + } else for (let c = 0; c < 3; c++) buf[3 * p + c] = max(buf[3 * p + c], col[c] * v * (core > 0.5 ? 1 : 0.85)); } } diff --git a/incoming/pattern-study/patterns/steiner-tree.js b/incoming/pattern-study/patterns/steiner-tree.js index 623273b46..882c16408 100644 --- a/incoming/pattern-study/patterns/steiner-tree.js +++ b/incoming/pattern-study/patterns/steiner-tree.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Steiner Tree // Family: geometry -// Idea: Four to seven terminals wander the panel, the minimum spanning tree joins them in faint dashes and the shortest network joins them in bright lines that meet three at a time at 120 degrees in marked Steiner points, and when two trees tie the network flips to the other one while the old one fades out amber; a bar at the foot reads the length ratio of the two trees against its floor of root 3 over 2. +// Idea: Four to seven terminals stand on a zigzag of equilateral triangles or wander the panel; the minimum spanning tree joins them in faint dashes and the shortest network in bright lines that meet three at a time at 120 degrees in pink Steiner points, and when another network becomes shorter the tree flips to it while the old one fades out amber; a bar at the foot reads the length ratio of the two trees against the conjectured floor of root 3 over 2. // Author: patternflow-lab // Date: 2026-10-30 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -11,10 +11,10 @@ // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Terminals=4..7, Scatter=0..1 // -// Knob 1: Hue. Turns every colour together: at 0 the Steiner tree is cyan, its Steiner points pink rings, the terminals warm white, the spanning tree dashed blue grey and a tree just left behind amber. -// Knob 2: Speed. How fast the terminals move. At 2 each wobbles about 2.5 LEDs round its place in 9 to 20 s and wanders at up to 6 LEDs a second when Scatter is up; at 0.2 ten times slower, at 3 one and a half times faster. -// Knob 3: Terminals. 4 to 7. Every full Steiner topology is tried (3, 15, 105 or 945 of them) with Smith's iteration, a share each frame, and the tree on show is also checked against the 2 swaps across each of its inner edges every frame. -// Knob 4: Scatter. At 0 the terminals sit on a zigzag strip of equilateral triangles down the panel, side 60 LEDs for four terminals and 32 for seven. Four make a 60 degree rhombus whose two Steiner trees are mirror images of the same length, so the wobble flips the tree back and forth at a ratio near root 7 over 3, 0.882. Five and seven split into equilateral stars that share terminals, and the ratio sits on root 3 over 2, 0.866. At 1 the terminals wander the whole panel and the ratio mostly stays above 0.95. +// Knob 1: Hue. Turns every colour together: at 0 the Steiner tree is cyan with faint rings spreading from it every 6 LEDs, its Steiner points pink, the terminals warm white, the spanning tree dashed blue grey and a tree just left behind amber. +// Knob 2: Speed. How fast the terminals move. At 2 each wobbles round its place on the strip by 3.5% of the triangle side (2.1 LEDs for four terminals), once in 9 to 20 s, the strip sways 2.5 LEDs across, and free terminals wander at up to about 6 LEDs a second. At 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Terminals. 4 to 7. Each frame tries 40 of the full Steiner topologies (3, 15, 105 or 945 in all) from a cold start with 15 of Smith's steps, keeps the near misses for 3 s with 10 more steps a frame, and tries both swaps across every inner edge of the tree on show. A shorter candidate takes over, and if the picture changes the old tree fades out amber over 1.2 s. +// Knob 4: Scatter, applied as its square. At 0 the terminals sit on a zigzag strip of equilateral triangles down the panel, side 60 LEDs for four terminals and 32 for seven. Four make a 60 degree rhombus whose two Steiner trees are mirror images of one length, root 7 over 3 (0.882) of the spanning tree, so the wobble flips the tree back and forth about 8 times a minute. Five and seven split into equilateral stars that share terminals: at rest the ratio is exactly root 3 over 2, and with the wobble it reads 0.867 to 0.904. At 1 they wander the whole panel, the ratio stays above 0.95 nine tenths of the time and seven terminals flip 27 times a minute. const W = 64, H = 128, NP = W * H; const { floor, round, min, max, sqrt, sin, cos, exp, abs, hypot, PI } = Math; @@ -211,7 +211,7 @@ function resolve(S) { for (const T of topologies(S.n)) { const X = S.X.slice(); initPos(T, X); - const L = smith(T, X, 25); + const L = smith(T, X, 15); if (!best || L < best[2]) best = [T, X, L]; } S.T = best[0]; @@ -415,7 +415,9 @@ export function draw(display, params) { for (let i = 0; i < S.n; i++) spot(rgb, S.X[2 * i], S.X[2 * i + 1], 1.9, warm, 1); // The ratio: a bar from root 3 over 2 at the left to 1 at the right, and // the number above it. - const r = S.M > 0 ? S.L / S.M : 1, fill = cl((r - FLOOR) / (1 - FLOOR), 0, 1); + // Smith's steps reach a degenerate tree slowly, so where the spanning tree + // is already the shortest the length can stand 0.04% above it: read 1. + const r = S.M > 0 ? min(1, S.L / S.M) : 1, fill = cl((r - FLOOR) / (1 - FLOOR), 0, 1); for (let x = 3; x <= 60; x++) { const u = (x - 3) / 57, on = u <= fill; for (let y = 121; y <= 124; y++) { From ff523a6ed056b58ca1bfa23d26b8ee6a2e0be498 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 01:22:03 +0000 Subject: [PATCH 21/22] =?UTF-8?q?README=20=EC=97=90=2075=ED=8C=90=20?= =?UTF-8?q?=EC=97=B0=EA=B5=AC=20=EB=85=B8=ED=8A=B8=EB=A5=BC=20=EB=8D=94?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PBtzYrhzPXCgRY4TG --- incoming/pattern-study/README.md | 101 ++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 3 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index eec0e99fe..3b40278c6 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 638개를 쉰여섯 갈래로 나눠 만들었어요. 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판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 61판도 수학 대상 열 개를 패턴이 적은 갈래부터 열 갈래에 하나씩 더했어요. 62판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 63판은 64판과 같이 주제 표에 「타이포」가 들어온 첫 판이라, 글자가 들어간 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 64판은 주제 표가 바뀌어 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 네 갈래에 더했어요. 65판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 66판도 같은 표로 돌아서 글자 패턴 넷을 글자 갈래에, 하나를 데이터 그림 갈래에 더하고, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 67판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 68판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 69판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 70판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 71판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 72판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 73판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 648개를 쉰여섯 갈래로 나눠 만들었어요. 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판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 61판도 수학 대상 열 개를 패턴이 적은 갈래부터 열 갈래에 하나씩 더했어요. 62판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 63판은 64판과 같이 주제 표에 「타이포」가 들어온 첫 판이라, 글자가 들어간 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 64판은 주제 표가 바뀌어 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 네 갈래에 더했어요. 65판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 66판도 같은 표로 돌아서 글자 패턴 넷을 글자 갈래에, 하나를 데이터 그림 갈래에 더하고, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 67판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 68판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 69판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 70판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 71판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 72판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 73판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 75판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. [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판에서 세로로 다시 짰어요. @@ -1005,6 +1005,21 @@ | [Steiner Deltoid](patterns/steiner-deltoid.js) | 선·궤적 | 외접원을 도는 점의 심슨 선이 패널 높이를 가로지르고, 그 자취가 쌓여 삼각형 세 변에 붙은 세 뿔 델토이드가 떠올라요. | | [Voss Dice](patterns/voss-dice.js) | 노이즈·흐름 | 이진 카운터의 비트가 바뀐 주사위만 다시 굴려 1/f 잡음을 만들고, 그 합을 백색 잡음과 갈색 잡음 사이에 나란히 흘려 내려요. | +### 75판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Coded Aperture](patterns/coded-aperture.js) | 글자 | 위에서 빛나는 낱말이 MURA 구멍 카드를 지나 검출기에 광자 하나씩 그림자를 겹쳐 쌓고, 읽히지 않는 그 그림자를 해독 배열과 상관시키면 맨 아래 띠에서 낱말이 잡음 위로 떠올라요. | +| [Euler Strokes](patterns/euler-strokes.js) | 글자 | 펜이 세로로 쌓은 낱말을 오일러가 센 가장 적은 획 수로 히어홀처의 길을 따라 쓰고, 홀수 꼭짓점이 펜보다 먼저 빛나며 글자마다 획 수가 숫자와 점으로 서요. | +| [Prandtl's Membrane](patterns/prandtl-membrane.js) | 글자 | 비틀린 막대의 단면인 낱말마다 프란틀의 막을 씌워 부풀리면, 막이 4초마다 숨 쉬듯 부풀었다 가라앉고 응력에 따라 등성이는 파랗게, 테두리와 안쪽 모서리는 주황과 노랑으로 달아올라요. | +| [Letter Mobile](patterns/letter-mobile.js) | 글자 | 네 글자 낱말을 잉크만큼 무게로 단 콜더식 모빌이 패널 높이로 걸려 돌고, 한 글자씩 바뀔 때마다 그 위 막대들이 기울었다가 고리를 새 균형점으로 옮겨 다시 수평이 돼요. | +| [Mesostic](patterns/mesostic.js) | 글자 | 소로의 『월든』을 맨 아래 줄에서 읽어 가다 케이지의 메조스틱 규칙이 허락하는 글자가 가운데에 오면 금빛 대문자로 세우고 둘레 낱말을 날개로 남겨서, 등뼈 낱말이 패널 가운데를 위에서 아래로 거듭 읽혀요. | +| [FRACTRAN](patterns/fractran.js) | 수론 | 콘웨이의 PRIMEGAME 이 2, 3, 5, 7 의 지수를 구슬 기둥으로 오르내리며 분수 열넷 가운데 맞는 첫 분수를 밝히고, n 이 2 의 거듭제곱이 될 때마다 2 기둥에 구슬 p 개만 남긴 채 소수 p 를 맨 위에 적어요. | +| [Steiner Tree](patterns/steiner-tree.js) | 기하·대칭 | 단자를 잇는 가장 짧은 망이 120도로 만나는 슈타이너 점을 두고 서서, 더 짧은 망이 나타나면 뒤집히며 옛 망을 호박색으로 지우고, 발치 막대가 최소 신장 트리와의 길이 비를 √3/2 바닥에 대어 읽어요. | +| [Miura-ori](patterns/miura-ori.js) | 공간·원근 | 합동인 평행사변형으로 된 미우라 접기 판이 자유도 하나로 접혔다 펴지며, 면이 두 빛 사이에서 네 빛깔로 뒤집히고 판이 가로세로로 함께 줄어요. | +| [Kruskal Count](patterns/kruskal-count.js) | 선·궤적 | 섞은 카드 52장을 뱀 꼴로 깔고 첫 열 장에서 저마다 색을 띤 사슬이 카드 값만큼 뛰어 내려가다 만나면 하나로 합쳐 하얘지고, 끝에 닿은 카드를 크게 뒤집어 보인 뒤 다시 섞어요. | +| [Lights Out](patterns/lights-out.js) | 알고리즘 | 패널 크기의 라이츠 아웃 판을 빛 쫓기로 풀어서, 앞선 줄이 내려가며 호박색 불을 하늘색 누르기 기록으로 바꾸고, 맨 윗줄을 고친 두 번째 쫓기가 마지막 불을 끄면 판을 만든 누르기가 그대로 남아요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -4135,6 +4150,70 @@ Lab 에서 해 보기: Text 레이어에 Silkscreen 으로 「20」을 size 8, s 주제 5 「수학 · 추상」으로, 주사위 몇 개로 1/f 잡음을 만드는 방법을 주사위째 보여 주려 했어요. 마틴 가드너(Martin Gardner)는 1978년 4월 Scientific American 의 Mathematical Games 칼럼 「White and brown music, fractal curves and one-over-f fluctuations」에서 리처드 F. 보스(Richard F. Voss)의 방법을 소개했어요. 주사위 하나하나를 이진 카운터의 비트 하나로 두고, 한 걸음마다 비트가 바뀐 주사위만 다시 굴리면 주사위 k 는 2^k 걸음마다 굴러요. 그러면 합이 매번 다 굴리는 백색 잡음과 주사위로 걷는 갈색 잡음 사이의 1/f 잡음이 돼요. 패턴은 위쪽에 주사위 n 개를 7×7 칸 얼굴로 그려요. 넷이면 14 LED 이고 다섯부터 7 LED 예요. 가장 빠른 주사위 0 을 오른쪽에 둬서 아래 비트 등이 걸음 수를 이진수로 읽히게 했고, 다시 굴린 주사위는 분홍으로 번쩍였다가 0.25초에 식어요. 그 아래 20 LED 너비 세 칸에 왼쪽부터 백색, 분홍, 갈색 합이 한 걸음에 2줄씩 위에서 아래로 흘러요. 백색은 주사위 n 개를 매 걸음 다 굴린 합이고, 갈색은 주사위 하나의 눈에서 3.5 를 뺀 만큼 걸어 창 끝에서 되접혀요. 세 칸 모두 평균 3.5n 위아래로 표준편차의 2.6 배를 둔 같은 창에 그려서, 주사위 넷이면 4~24 가운데 5.1~22.9 를 써요. Speed 2 에서 1초에 6 걸음이고, 첫 화면부터 패널이 차도록 140 걸음을 미리 굴려 둬요. 넷째 노브 Hold 는 이웃한 주사위가 얼굴을 쥐는 시간의 비 r 이에요. 주사위 k 는 floor(걸음 / r^k) 가 바뀔 때 구르고, r 이 2 이면 보스의 카운터 그대로예요. Lab 의 처음 값 0.06 이 정확히 2 가 되게 맞췄어요. r 을 1 로 내리면 가운데 칸이 왼쪽처럼 백색이 되고, 3.8 로 올리면 느린 주사위가 수백 걸음을 버텨 가운데 줄이 좀처럼 자리를 옮기지 않아요. 40만 걸음을 돌려 세어 보니 주사위 넷일 때 한 걸음에 평균 1.875 개가 구르고, 분홍 합은 한 걸음에 2.56 움직여 백색의 3.87 보다 작아요. 잰 값은 움직임 0.959, 촘촘함 0.083, 가장자리 0.759, 복잡도 0.184, 채움 0.294, 색 0.125, 모양 바뀜 0.074 이고, thirds 는 37/24/26, 노브는 0.05, 0.10, 0.14, 0.10 이에요. 처음 잰 판에서 바꾼 것은 주사위 차례와 주석뿐이라 잰 값이 그대로예요. 새로 본 것은 첫 화면의 주사위 넷이 8걸음마다, Speed 2 에서 1.33초마다 한꺼번에 번쩍인다는 거예요. 카운터가 0111 에서 1000 으로 넘어가는 걸음이라 넷이 다 구르고, 그 걸음의 분홍 합은 평균 3.87 을 뛰어 백색이 매 걸음 뛰는 폭과 같아요. 주사위를 여덟로 늘리면 백색의 한 걸음은 5.46 으로 커지는데 분홍은 2.61 로 거의 그대로라서, 가운데 칸만 차분해져요. +## 75판 + +75판은 74판(#337)과 같이 돌았어요. 앞 바퀴 PR 76개에는 OWNER, MEMBER, COLLABORATOR 가 쓴 댓글도 리뷰도 없어서 따를 글이 없었어요. 74판이 PR 본문에 먼저 밝힌 열 개와 지도 칸 둘을 읽고 겹치지 않게 골랐어요. 타이포 다섯은 「타이포 실험」의 출발점 넷을 나눠 맡았어요. Coded Aperture 는 글자와 빛, Euler Strokes 는 글자가 만들어지는 과정, Mesostic 은 세로 패널다운 글이고, Prandtl's Membrane 과 Letter Mobile 은 둘 다 글자의 몸이에요. 둘이 받는 힘이 달라요. Prandtl's Membrane 의 글자는 압력에 부풀고, Letter Mobile 의 글자는 제 무게로 매달려요. 글자 꼴도 다섯이 다 달라요. Coded Aperture 는 열한 자를 3×5 비트맵으로, Euler Strokes 는 대문자 여덟과 음절 넷을 꼭짓점과 변 목록으로 적었어요. Prandtl's Membrane 은 일곱 자와 숨 한 자를 볼록 다각형 31 개로 적어 윤곽까지의 거리를 셈하고, Letter Mobile 은 열세 자를 획 목록으로 적어 4 LED 둥근 펜으로 그렸어요. Mesostic 은 기기 펌웨어의 MatrixLight6 픽셀 글꼴을 옮겨 썼어요. 수학 다섯은 73판까지 든 638개에 없던 대상이고 「낯선 수학 예」에서는 고르지 않았어요. 수론, 기하·대칭, 공간·원근, 선·궤적, 알고리즘에 하나씩 넣었어요. 주제 1 은 넷뿐인 칸 스물하나 가운데 74판이 고른 채움 × 색 2,4 와 촘촘함 × 복잡도 3,3 을 빼고 촘촘함 × 모양 바뀜 4,4 와 가장자리 × 색 2,4 를 겨눴어요. 주제 4 는 Mesostic 과 Kruskal Count 예요. + +### Coded Aperture + +주제 7 「타이포」의 「글자와 빛」으로, 그림자가 글자를 그리는데 그 그림자에서는 글자가 읽히지 않고 셈을 거쳐야 글자가 나오는 것을 겨눴어요. 부호화 구멍 영상(coded aperture imaging)은 로버트 디키(Robert H. Dicke)와 J. G. 에이블스(J. G. Ables)가 1968년에 따로 낸 생각이에요. 렌즈로 모으지 못하는 X선을 바늘구멍 하나 대신 무작위로 뚫은 구멍 여럿으로 받고, 겹친 그림자를 셈으로 풀어요. E. E. 페니모어(E. E. Fenimore)와 T. M. 캐넌(T. M. Cannon)은 1978년에 자기상관의 곁값이 모두 같아 풀고 나면 고른 덧값만 남는 균일 중복 배열(uniformly redundant array)을 냈고, S. R. 고츠먼(S. R. Gottesman)과 페니모어는 1989년에 소수 p 의 이차 잉여로 정사각형 배열을 짓는 수정 균일 중복 배열(MURA)을 냈어요. 패턴은 p = 17 의 MURA 를 써요. 0행은 막고 0열은 열고, 나머지 칸은 행과 열 번호가 둘 다 17 의 잉여이거나 둘 다 아니면 열어서 289칸 가운데 144칸이 구멍이에요. 원소 하나를 가로 3, 세로 2 LED 로 그려 띠 하나가 51×34 LED 이고, 위에서부터 빛나는 낱말(17줄 하늘의 5~11줄), 카드, 검출기, 해독한 하늘을 쌓았어요. 광자는 낱말의 켜진 원소 하나와 구멍 하나를 골라 검출기 원소 d = h − s + 8 (mod 17) 에 떨어지고, 그 원소의 LED 여섯 가운데 하나에서 하얗게 번쩍인 뒤 쌓여요. 카드를 2×2 로 이어 붙인 실제 장치처럼, 어느 방향의 빛이든 카드 그림자 한 벌을 통째로 드리우는 셈이에요. 해독은 검출기와 MURA 해독 배열의 주기 상관이라 광자 하나가 289칸에 ±1 씩 더해요. Node 에서 잡음 없는 검출기를 네 낱말 모두 풀어 보니 낱말 원소는 정확히 144, 하늘은 정확히 0 이었어요. 검출기 배경은 원소마다 1초에 1개이고, 해독 배열의 합이 1 이라 배경은 고른 덧값으로만 남아서 빼고 그려요. Rate 2 에서 1초에 광자 3000개, 노출은 6초이고, 1.2초 결과를 보인 뒤 지우고 다시 받아요. 검출기 아래 한 줄이 노출 시계예요. Mask 노브는 구멍마다 같은 빛을 받는 MURA 와 반쯤 열린 무작위 카드와 가운데 바늘구멍 하나를 견줘요. 40번씩 돌려 6초 끝의 낱말이 빈 하늘 잡음 폭의 몇 배인지 재니 STAR 에서 MURA 3.78, 무작위 카드 2.57, 바늘구멍 1.49 였어요. 바늘구멍은 6초 동안 낱말 광자 123개와 배경 1740개를 받아요. 무작위 카드는 잡음이 없어도 그림자가 서로 지워지지 않아서 STAR 원소 34개 가운데 15개가 절반 밝기에 못 미쳐요. 잰 값은 움직임 0.662, 촘촘함 0.110, 가장자리 0.552, 복잡도 0.786, 채움 0.454, 색 0.116, 모양 바뀜 0.082 이고, thirds 는 44/59/32, 노브는 0.06, 0.14, 0.15, 0.09 예요. 첫 판은 검출기를 셈의 제곱근으로 밝혀서 그림자 무늬가 고른 청록에 묻혔고, 움직임 0.542, 복잡도 0.692, 채움 0.439, 모양 바뀜 0.108 이었어요. 셈에 비례해 밝히고, 광자 셋에 하나를 카드 위아래 4줄 틈에 불꽃으로 그리고, 노출 시계와 지난 해독의 0.25초 잔상을 더해 지금 값이 됐어요. 새로 본 것은 검출기의 알갱이예요. 6초 끝에 LED 하나에 평균 11.4개가 떨어져 알갱이가 ±30% 흔들리고, 잡음 없이 셈한 STAR 의 그림자도 원소마다 9겹에서 25겹이라 패널에서는 무늬보다 알갱이가 먼저 보여요. 그래도 같은 숫자를 해독 배열과 상관시키면 노출 2.6초에 낱말이 잡음 폭의 2.5배로 서요. + +Lab 에서 해 보기: 없음. 이 패턴의 낱말은 카드를 지나 검출기에 떨어지는 빛이라, Text 레이어로 친 글자는 카드를 지나지 못하고 그림 위에 그대로 얹힐 뿐이에요. + +기능 감: 코드 레이어가 아래 Text 레이어의 비트맵을 하늘로 받는 길이요. 그 길이 있으면 Lab 에서 친 아무 낱말이나 17×17 하늘에 깎아 MURA 로 찍고 풀 텐데, 지금은 STAR, CRAB, VELA, LUX 네 낱말에 쓰는 글자 열한 개를 3×5 비트맵으로 코드에 적었어요. + +### Euler Strokes + +주제 7 「타이포」의 「글자가 만들어지는 과정」을 채우는 패턴이에요. 글자를 그래프로 봤어요. 획 끝과 꺾인 자리와 갈라지는 자리가 꼭짓점이고, 그 사이의 획 조각이 변이에요. 레온하르트 오일러(Leonhard Euler)는 1736년 쾨니히스베르크 다리 논문(『Solutio problematis ad geometriam situs pertinentis』)에서 차수가 홀수인 꼭짓점의 수로 한붓그리기가 되는지를 갈랐어요. 그 셈을 넓히면 이어진 그림 하나에 드는 획은 홀수 꼭짓점 수의 절반이고, 홀수 꼭짓점이 없으면 한 획이에요. 그 수만큼의 획으로 실제로 그리는 길은 카를 히어홀처(Carl Hierholzer)가 1873년에 보였어요. 패턴은 P, A, T, H, E, U, L, R 여덟 대문자와 다, 리, 한, 붓 네 음절을 꼭짓점과 변 목록으로 적었어요(2.7 KB). 낱말은 PATH, 다리, EULER, 한붓 넷이에요. 한 칸에 한 글자씩 128줄을 내려가며 쌓아서, PATH 는 칸이 32줄이고 글자가 25×25 LED, 획 굵기가 2.7 LED 예요. 조각마다 남는 홀수 꼭짓점을 펜 든 짧은 뜀으로 짝짓고, 남은 두 점 가운데 하나에서 히어홀처의 길을 걸어 뜀 자리에서 끊으면 그 수의 획이 돼요. 꼭짓점마다 길 순서를 섞어서 다시 쓸 때마다 같은 획 수의 다른 길을 골라요. 3000번 짜 보니 H 는 11 가지, 붓은 32 가지 길이 나왔어요. 아직 안 쓴 뼈대는 앞으로 될 획의 색으로 흐리게 물들어 있어요. 획이 시작하거나 끝날 꼭짓점은 펜보다 먼저 맥박치듯 빛나다가 펜이 닿으면 고리로 남아요. 펜은 Speed 2 에서 1초에 34 LED 를 쓰고, 펜을 든 채로는 90 LED 를 움직이며 희미한 점선을 남겨요. 막 쓴 잉크는 0.7초쯤 하얗게 달았다가 제 색으로 식어요. 글자 오른쪽에는 그 글자의 획 수가 숫자로 서고, 획마다 점이 하나씩 켜져요. Order 노브를 0.5 위로 돌리면 학교에서 배우는 획 순서로 써요. 라틴 글자는 볼 앤 스틱 활자체, 한글은 필순이에요. 그러면 PATH 는 8획에서 10획, 다리는 5획에서 8획, EULER 는 7획에서 12획, 한붓은 12획에서 14획이 돼요. 짝수 꼭짓점에 닿은 자리는 장밋빛 고리로 남는데, 오일러 순서에서는 ㅎ 의 동그라미처럼 닫힌 조각만 짝수 꼭짓점에 닿아요. Node 에서 열두 글자를 300번씩 다시 짜서, 모든 길이 변을 한 번씩만 지나고 획 수가 늘 오일러의 셈과 같은 것을 봤어요. 잰 값은 움직임 0.065, 촘촘함 0.044, 가장자리 0.403, 복잡도 0.638, 채움 0.293, 색 0.138, 모양 바뀜 0.072 이고, thirds 는 30/34/22, 노브는 0.05, 0.04, 0.16, 0.05 예요. 첫 판은 글자가 19 LED 폭으로 좁고 획만 빛났어요. 그때는 움직임 0.022, 촘촘함 0.037, 가장자리 0.428, 복잡도 0.346, 채움 0.178, 색 0.118, 모양 바뀜 0.042, thirds 20/19/13, 노브 0.03, 0.02, 0.09, 0.02 였어요. 라틴 글자를 높이보다 1.4 배까지 넓히고, 막 쓴 잉크의 흰 열과 펜 끝 둘레의 빛을 더했어요. 안 쓴 뼈대를 앞으로 될 획의 색으로 물들이고 숫자를 세 배로 키우자 지금 값이 됐어요. 둘째 획을 금빛으로 두었을 때는 흐린 뼈대가 갈색으로 보여서, 둘째를 자홍으로, 셋째를 금빛으로 바꿨어요. 새로 본 것은 학교 순서가 획만 늘리지 않는다는 거예요. 학교 순서의 PATH 는 펜을 든 채 아홉 번에 걸쳐 232 LED 를 움직이고, 오일러 순서는 일곱 번에 88 LED 를 움직여요. 획은 둘 늘었는데 펜 든 길은 2.6 배이고, 다 쓰는 데 12.6초와 10.5~10.8초가 걸려요. + +Lab 에서 해 보기: Text 레이어에 H 를 Newsreader 로 쳐서 가운데 맞춤 x 22, y 112 에 두고, 크기는 36 언저리에서 H 의 위아래가 100행과 125행에 닿게 맞춰요. 채움은 #383838, blend 는 screen 이에요. K3 Word 를 0 에 두면 맨 아래 칸의 H 위에 세리프 H 가 흐리게 겹쳐요. 기둥 끝 넷에 양쪽 세리프가 붙으면 끝마다 홀수 꼭짓점이 하나에서 셋이 돼요. 그래서 세리프 H 는 홀수 꼭짓점이 14 개에 7획이고, 패턴의 H 는 6 개에 3획이에요. + +기능 감: 패턴이 쓸 수 있는 한 줄 글꼴이요. 획의 가운데 선을 꼭짓점과 변으로 주는 글꼴(앨런 허시 Allen Vincent Hershey 의 1967년 허시 글꼴 같은 것)이 있고 Lab 에 친 낱말을 패턴이 받을 수 있으면, 이 패턴이 아무 낱말이나 오일러 순서로 쓸 수 있어요. 이번에는 열두 자를 손으로 적느라(2.7 KB) 낱말이 넷뿐이에요. + +### Prandtl's Membrane + +주제 7 「타이포」의 「글자의 몸」과 주제 1 「지도의 빈 칸」을 같이 채우는 패턴이에요. 주제 1 은 Brushstrokes, Gear Train, Heawood Torus, Hyperbolic Band 넷뿐인 가장자리 × 색 2,4 칸을 겨눴어요. 가장자리가 0.210~0.500 이면서 색이 0.5445 를 넘는 칸이에요. 단면이 글자인 막대를 비튼다고 봐요. 아데마르 바레 드 생브낭(Adhémar Barré de Saint-Venant)이 1855년에 푼 비틀림에서 응력 함수 φ 는 단면 안에서 ∇²φ = −2 를 풀고 윤곽에서 0 이에요. 전단 응력은 그 기울기 |∇φ| 이고, 토크는 φ 아래 부피의 두 배예요. 루트비히 프란틀(Ludwig Prandtl)은 1903년에 같은 식이 단면 모양 구멍에 씌우고 아래에서 바람을 넣은 막의 높이라는 것을 보였어요. 그래서 이 패턴은 글자마다 막을 부풀려 그려요. 막은 반 LED 간격 128 × 256 마디에서 축차 과완화(데이비드 영 David Young, 1950)로 풀어요. ω 는 1.92 이고, 한 프레임에 열 번씩 훑다가 어느 마디도 0.01 넘게 움직이지 않으면 멈춰요. 낱말 넷이 116~144 번, 0.5초 안에 자리를 잡아서 새 낱말은 풀이가 퍼지는 대로 부풀어 올라요. 윤곽 옆 마디는 격자선을 따라 잰 윤곽까지의 참 거리로 쇼틀리-웰러 꼴(조지 쇼틀리 George Shortley, 로열 웰러 Royal Weller, 1938)을 써서 빗금 획이 계단이 되지 않아요. 구멍이 있는 글자는 속 빈 단면이에요. 구멍 둘레에서 φ 가 한 값이라 구멍은 막이 들어 올리는 무게 없는 평판이 되고, 판 넓이에 걸린 압력과 둘레에서 막이 당기는 힘이 맞는 높이에 서요. 응력이 구멍을 한 바퀴 돈 합이 구멍 넓이의 두 배라는 조건이고, 루돌프 브레트(Rudolf Bredt)의 얇은 관 공식(1896)이 기대는 조건이에요. Node 로 이 풀이를 셈했어요. 반지름 20 원에서 막 꼭대기는 a²/2 = 200 에 199.79, φ 는 어디서나 (a² − r²)/2 에서 0.18 안이었고, 최대 응력은 a = 20 에 19.98 로 테두리에 있었어요. 40 × 8 띠의 최대 응력은 긴 변 가운데, 가장자리에서 4분의 1 LED 안쪽 마디에서 7.50 이었고 띠 식 2y 가 7.5 예요. 벽 6 인 40 크기 네모 관에서는 판이 108.7 로 브레트 얇은 벽 식의 102 에 가깝고, 벽 가운데 응력은 17.6 으로 브레트의 17.0 에 가까웠어요. 낱말은 패널 높이를 다 쓰는 숨, 그리고 56 × 38 대문자 셋을 쌓은 LIT, AIR, HOT 이에요. 줄기는 13 LED 이고 모서리는 일부러 각지게 두었어요. 바깥 모서리에서는 막이 판판하고 안쪽 모서리에서는 한없이 가팔라지기 때문이에요. 숨 한 번은 4초이고, 비틀림이 Twist 의 40% 에서 100% 사이를 오가요. Twist 2 의 숨 꼭대기에서 ㅜ 의 14 LED 가로획 테두리는 56도로 서고, 숨 바닥에서는 31도로 누워요. 램프는 12초에 한 바퀴 돌고 Lamp 0.06 에서 19도 높이로 비춰요. 색은 응력이에요. 응력이 0 인 등성이는 파랗고, 테두리는 자홍에서 빨강을 지나 주황으로 가고, 안쪽 모서리가 노랗게 타요. 같은 비틀림에서 HOT 은 O 의 판 하나 덕에 LIT 의 4.0배 토크를 져요. 잰 값은 움직임 0.047, 촘촘함 0.042, 가장자리 0.261, 복잡도 1.211, 채움 0.600, 색 0.571, 모양 바뀜 0.123 이고, thirds 는 45/48/86, 노브는 0.38, 0.31, 0.34, 0.06 예요. 처음 판은 움직임 0.049, 촘촘함 0.024, 가장자리 0.180, 복잡도 0.767, 채움 0.379, 색 0.277, 모양 바뀜 0.144, thirds 24/27/62, 노브 0.12, 0.09, 0.12, 0.06 이었어요. 램프가 낮아 획의 반이 어두웠고, 색이 칸의 아래 끝 0.5445 의 절반이었어요. 밝기에 바닥을 깔자 색 0.427 에 가장자리 0.406 이 됐어요. 대문자를 52 × 36 에서 56 × 38 로, 줄기를 12 에서 13 으로 키우자 채움 0.548, 색 0.509 가 됐지만, 가장자리가 0.451 로 칸 위 끝 0.500 에 다가갔어요. 밝기를 응력에도 걸어 테두리가 밝고 등성이가 어둡게 하자 획 안의 부드러운 계단이 늘어 가장자리가 0.284 로 내려가고 색은 0.566 으로 올랐어요. 숨의 획을 굵히고 숨의 바닥을 60% 에서 40% 로 내렸어요. 램프 그늘을 다시 짙게 하고 흰빛이 도는 반짝임을 더하고, 색의 뜨거운 끝을 응력 13 에서 11 로 당기자 지금 값이 됐어요. 새로 본 것은 숨의 ㅁ 이 브레트의 얇은 관이 아니라는 거예요. 벽이 14~15 LED 로 구멍(28 × 13)만큼 두꺼워서, 아래 벽을 가로지르며 LED 의 응력이 판 쪽 6.7 에서 윤곽 쪽 31.7 로 다섯 배 가까이 올라요. 판은 268 높이에 서고, 패널에서는 ㅁ 의 바깥 반이 노랗고 안쪽 반이 자홍으로 갈려 보여요. + +Lab 에서 해 보기: 없음. 이 패턴에서 글자는 막을 거는 테두리라서, Text 레이어를 얹으면 응력 색을 덮을 뿐 막이 그 글자를 따라 부풀지 않아요. + +기능 감: 코드 레이어가 Text 레이어의 글자를 윤곽 다각형이나 부호 거리장으로 받는 길이에요. 막 풀이는 경계까지의 거리를 반 LED 아래로 알아야 해서(쇼틀리-웰러 꼴) 덮임 비트맵으로는 모자라요. 그래서 볼록 다각형 31 개로 L, I, T, A, R, H, O 일곱 자와 숨 한 자만 그렸어요. 그 길이 있으면 Lab 에서 친 아무 낱말이나 비틀어 볼 수 있어요. + +### Letter Mobile + +주제 7 「타이포」의 「글자의 몸」으로, 글자가 중력을 받으면 어떻게 되는지를 저울로 보여 주려 했어요. 알렉산더 콜더(Alexander Calder)는 1931~32년에 처음으로 움직이는 조각을 만들었고, 마르셀 뒤샹(Marcel Duchamp)이 거기에 「모빌」이라는 이름을 붙였어요. 콜더는 철사 막대 끝에 판을 매달고 막대마다 거는 자리를 옮겨 균형을 잡았어요. 그 자리는 아르키메데스(Archimedes)가 『평면의 평형에 대하여(On the Equilibrium of Planes)』에 적은 지레의 법칙대로, 왼쪽 무게와 왼쪽 팔의 곱이 오른쪽 무게와 오른쪽 팔의 곱과 같은 곳이에요. 패턴은 네 글자 낱말의 글자를 판으로 매달고, 글자의 무게를 그 판이 켜는 LED 수, 곧 잉크로 둬요. 글자 열셋을 획 목록으로 적어 굵기 4 LED 둥근 펜으로 높이 22 LED 판에 그렸고, 잉크는 I 84, L 119, T 126, F 145, A 172, E 180, H 196, O 198, D 204, R 206, B 225, W 240, M 242 예요. 막대 셋은 콜더가 흔히 쓴 사슬로 걸어요. 길이 28 LED 맨 위 막대는 왼쪽 끝에 첫 글자를, 오른쪽 끝에 그 아래 전부를 달아요. 길이 26 둘째 막대는 왼쪽에 나머지를, 오른쪽에 둘째 글자를 달고, 길이 32 셋째 막대가 셋째와 넷째 글자를 달아요. 아래 모빌의 무게는 그 아래 글자 잉크의 합이에요. HEAD 에서 H 196, E 180, A 172, D 204 일 때 맨 위 막대는 H 에서 20.70 LED, 아래 556 에서 7.30 LED 되는 자리에 걸리고, 맨 아래 막대는 A 에서 17.36, D 에서 14.64 에 걸려요. 거는 고리가 막대보다 7 LED 위에 있어서, 무게가 바뀐 막대는 tan a = M / (W·7) 인 기울기에서 멈추려고 흔들려요. 낱말은 루이스 캐럴(Lewis Carroll)이 1879년 『배니티 페어(Vanity Fair)』에 낸 「더블릿(Doublets)」처럼 한 번에 한 글자씩 바뀌어요. Word 0 은 캐럴의 첫 문제 HEAD HEAL TEAL TELL TALL TAIL 이에요. 1 은 가벼운 글자가 무거워지는 FILL 에서 WARM 까지, 2 는 무거운 글자가 가벼워지는 WOMB 에서 TILL 까지예요. Speed 2 에서는 3.6초마다 한 글자가 0.7초 동안 실 위에서 한 바퀴 돌며 새 글자가 되고, 무게는 4분의 1 바퀴에서 바뀌어요. 그 0.3초 뒤부터 1.4초 동안 그 위 막대들의 고리가 새 균형점으로 미끄러지고, 미끄러지는 고리는 하얗게 빛나요. HEAD 가 HEAL 이 될 때 맨 아래 막대는 0.9초 만에 35도 기울었다가 1.3초 뒤 반대쪽 6도까지 넘어가고, 고리는 A 쪽으로 4.27 LED 옮겨 가요. 같은 때 위의 두 막대도 8.4도와 9.1도 기울어요. 막대는 실에 매달린 채 44초, 36초, 30초에 한 바퀴씩 돌고 7~11초 주기로 0.35 라디안씩 앞뒤로 흔들려요. 그래서 판은 돌아간 만큼 좁아지고, 90도를 넘으면 뒤에서 본 거울 글자가 돼요. 왼쪽 위 앞의 등이 26 LED 뒤 벽에 모빌 그림자를 던지고, 그림자는 오른쪽으로 10 LED, 아래로 12~17 LED 비켜 따라 돌아요. Node 로 세 사다리의 낱말 21개를 다음 글자가 바뀌기 직전에 쟀더니 모든 막대의 알짜 모멘트가 1e-12 아래이고 기울기는 1.8도 아래였어요. 잰 값은 움직임 0.152, 촘촘함 0.024, 가장자리 0.391, 복잡도 0.737, 채움 0.975, 색 0.164, 모양 바뀜 0.084 이고, thirds 는 97/98/96, 노브는 0.07, 0.07, 0.06, 0.08 이에요. thirds 는 거의 벽의 몫이라 벽을 끄고 다시 재면 11/10/14, 채움 0.118 이에요. 첫 판은 글자가 18 LED 에 굵기 3 이고 벽이 따뜻한 회갈색이었어요. 그때 잰 값은 움직임 0.175, 촘촘함 0.016, 가장자리 0.325, 복잡도 0.663, 채움 0.992, 색 0.111, 모양 바뀜 0.040, thirds 98/99/99, 노브 0.01, 0.05, 0.03, 0.03 이라 Hue 가 0.0104 로 걸렸어요. 글자를 22 LED 로 키우고 벽을 Hue 따라 도는 청회색으로 바꾸자 Hue 가 0.07 이 됐어요. 막대도 처음에는 제자리에서 앞뒤로 비틀었는데, 맨 아래 막대가 1.75 라디안 끝에서 머물러 4초부터 9.5초까지 글자 두 개가 선 하나로 서 있었어요. 그래서 늘 한쪽으로 도는 것으로 바꿨어요. 흔들림도 처음에는 맨 위 막대가 0.84초 주기로 평형 기울기의 두 배까지 넘쳤고, WOMB 가 TOMB 가 될 때 T 가 패널 위로 나갔어요. 흔드는 힘을 1300 에서 400 으로 낮추고 모빌을 6줄 내렸어요. 새로 본 것은 글자가 바뀌어도 무게가 그대로일 수 있다는 거예요. E 는 180, A 는 172 라서 TELL 이 TALL 이 될 때 막대는 1.4도밖에 기울지 않아요. 그런데 D 204 가 L 119 가 되는 HEAD 에서 HEAL 은 맨 아래 막대를 35도 기울여요. + +Lab 에서 해 보기: Text 레이어에 Silkscreen 으로 「TAIL」을 size 8, sharp 로 쳐서 오른쪽 맞춤 x 63, y 6 에 두고 blend add 로 얹으면, 모빌 오른쪽 위 빈 벽(x 40~63, y 0~12)에 사다리가 닿을 낱말이 이름표처럼 붙어요. Word 를 1 로 돌릴 때 글을 「WARM」으로, 2 로 돌릴 때 「TILL」로 바꾸면 지금 걸린 낱말과 끝 낱말을 견주며 볼 수 있어요. + +기능 감: 코드 레이어가 Text 레이어가 래스터한 글자 비트맵을 받아 쓰는 길이요. 판으로 매달 열세 글자를 획 목록으로 다시 그렸어요. Lab 의 Silkscreen 이나 Inter 글자를 받을 수 있었다면 글꼴마다 잉크가 달라서, 같은 낱말의 모빌이 어떻게 다르게 걸리는지 견줄 수 있었을 거예요. + +### Mesostic + +주제 7 「타이포」의 「세로 패널다운 글」과 주제 4 「세로 64×128」을 같이 채우는 패턴이에요. 글줄마다 대문자 하나를 가운데 세로축에 꿰어, 64 LED 폭에서 세로로 읽히는 낱말과 가로로 읽히는 글이 한 쪽에 같이 서는지 보려 했어요. 존 케이지(John Cage)는 1960년대 끝 무렵부터 메조스틱(mesostic)을 썼어요. 등뼈가 되는 낱말의 글자를 한 줄에 하나씩 대문자로 가운데에 세우고, 그 둘레를 원문의 조각으로 채우는 시예요. 『Writing through Finnegans Wake』(1978)는 JAMES JOYCE 를 등뼈로 조이스의 책을 처음부터 끝까지 읽어 내려간 것이고, 『Mureau』(1970)는 헨리 데이비드 소로(Henry David Thoreau)의 일기에서 지은 글이에요. 두 대문자 사이에 뒤 대문자의 글자가 나오지 않으면 50% 메조스틱이라 하고, 앞 대문자의 글자도 나오지 않으면 100% 메조스틱이라 해요. 패턴은 소로의 『월든』(Walden, 1854) 「Where I Lived, and What I Lived For」 장에서 세 대목 304 낱말을 코드에 옮겨 두고, 구두점을 뺀 소문자로 읽어요. 대문자는 제 낱말 안에 서고 낱말은 늘 통째로 보여요. 대문자 낱말의 앞뒤 조각이 규칙을 지키고 패널 반쪽에 들어갈 때만 그 글자를 골라요. 날개는 바깥으로 낱말을 하나씩 붙이다가 금지된 글자를 품은 낱말이나 반쪽 끝에서 멈춰요. 글자는 기기 펌웨어에 든 MatrixLight6 픽셀 글꼴을 옮겨 썼어요. 소문자는 x 높이 4줄에 오름과 내림이 한 줄씩이라 한 글줄이 6줄을 써요. 7줄 간격으로 18칸을 세워 1행부터 126행까지 쓰고, 대문자는 x 31.5 를 가운데로 둬서 왼 날개는 29 LED, 오른 날개는 30 LED 안에 들어요. 맨 아래 칸에서는 원문이 한 줄로 흐리게 지나가고, 가운데 18 LED 폭쯤만 밝아져요. 등뼈가 다음에 찾는 글자는 지나가며 금빛으로 반짝여요. 규칙이 허락하는 글자가 가운데에 닿으면 하얗게 번쩍였다가 금빛 대문자로 식어요. 날개가 될 낱말만 하늘색으로 밝아지고 나머지는 사라진 뒤 쪽이 한 줄 올라가요. Speed 2 에서 미끄러지기 0.45~0.95초, 굳기 0.4초, 오르기 0.35초라 THOREAU 로는 평균 1.28초에 한 줄이 서요. 처음 화면은 THOREAU 의 첫 17줄이 쪽을 채운 채 열려요. Node 에서 이 코드 그대로 등뼈 넷과 규칙 둘마다 3000 줄씩 만들어 보니 규칙을 어긴 줄은 없었어요. THOREAU 는 304 낱말을 한 바퀴 읽는 동안 50% 에서 평균 49 줄, 100% 에서 35 줄을 세웠어요. 잰 값은 움직임 0.655, 촘촘함 0.125, 가장자리 0.973, 복잡도 0.162, 채움 0.143, 색 0.145, 모양 바뀜 0.057 이고, thirds 는 13/14/14, 노브는 0.05, 0.11, 0.12, 0.07 예요. 첫 판은 Hue 반응이 0.043 이었어요. 또 첫 화면이 늘 THOREAU 로 열려서, Word 나 Rule 을 돌린 채 시작하면 빈 줄 하나 뒤에야 새 등뼈가 나왔어요. 날개의 채도를 0.42 에서 0.5 로 올려 Hue 를 0.048 로 올렸고, 첫 화면이 노브가 고른 메조스틱으로 열리게 했어요. 그러자 Word 는 0.10 에서 0.12, Rule 은 0.08 에서 0.07 이 됐어요. 새로 본 것은 첫 세 줄이에요. i wenT to, tHe, wOods 라서 소로의 문장 I went to the woods 가 50% 규칙을 지나고도 등뼈 위에서 그대로 읽혀요. 100% 로 돌리면 둘째 줄이 i wisHed 로 바뀌어 그 문장이 끊기고, 한 바퀴에 서는 줄도 49 에서 35 로 줄어요. + +Lab 에서 해 보기: Text 레이어에 W, A, L, D, E, N 을 한 줄에 한 자씩 쳐서 Newsreader 크기 18, sharp 끔으로 두고 오른쪽 맞춤 x 63, y 64 에 색 #3a3020, blend add 로 얹으면, 오른쪽 끝에 아크로스틱처럼 세로 낱말이 하나 더 서서 가운데 등뼈의 메조스틱과 견줘져요. Word 를 1 로 돌리면 두 등뼈가 같은 낱말이 돼요. + +기능 감: Text 레이어에서 줄마다 정한 글자 하나를 같은 x 에 세우는 맞춤이요. 지금 맞춤은 왼쪽, 가운데, 오른쪽뿐이라, 대문자를 줄마다 x 31.5 에 꿰려고 코드에서 글자 너비를 하나씩 더해 날개를 양쪽으로 펼쳐야 했어요. 줄 안의 대문자를 축으로 삼는 맞춤이 있으면 Text 레이어 하나로 메조스틱 한 쪽을 세울 수 있어요. + +### FRACTRAN + +주제 5 「수학 · 추상」으로, 수 하나에 분수를 곱하기만 하는 프로그램이 소수를 차례로 내놓는 것을 구슬 기계로 보이려 했어요. 존 호턴 콘웨이(John Horton Conway)는 1987년 「FRACTRAN: A Simple Universal Programming Language for Arithmetic」에서 분수 목록 하나를 프로그램으로 삼았어요. n 에 목록에서 처음으로 정수를 만드는 분수를 곱하기를 되풀이하는 것이 전부예요. 그 글의 PRIMEGAME 은 17/91, 78/85, 19/51, 23/38, 29/33, 77/29, 95/23, 77/19, 1/17, 11/13, 13/11, 15/2, 1/7, 55/1 열네 분수예요. n = 2 에서 시작하면 그 사이에 나타나는 2 의 거듭제곱은 2^2, 2^3, 2^5, 2^7 처럼 지수가 소수인 것뿐이고, 소수가 하나도 빠지지 않고 차례로 와요. 패턴은 n 을 큰 수로 들지 않고 2 에서 29 까지 소수 열 개의 지수로 들고, 분수도 지수로 풀어 둬서 분모의 지수가 모두 n 의 지수 이하면 그 분수가 맞아요. 2, 3, 5, 7 은 값이 커지는 레지스터이고 11 에서 29 는 0 아니면 1 이라 프로그램의 상태예요. Node 에서 돌리니 2^2, 2^3, 2^5, 2^7, 2^11 이 19, 69, 281, 710, 2375 걸음에 나왔고 2^41 은 101354 걸음이었어요. 패턴 안의 기계도 2^37 까지 같은 걸음에서 소수를 적어요. 맨 위 두 줄이 찾은 소수의 기록이고, 그 아래 분수 열넷을 일곱씩 두 줄로 분자 위 분모 아래에 3×5 숫자로 적었어요. 그 아래가 상태 등 11~29 이고, 맨 아래 64줄 높이의 기둥 넷에 2, 3, 5, 7 의 지수만큼 구슬이 쌓여요. 구슬 하나는 5줄이고 레지스터가 12 에 이르면 얇아져요. 걸음마다 시험하고 지나친 분수는 가름줄이 깜박이고 맞은 분수는 칸이 밝아져요. 분모가 가져간 구슬은 기둥 머리로 올라가 사라지고, 분자가 준 구슬은 머리에서 떨어져요. Speed 2 에서 1초에 120걸음이라 2, 3, 5, 7 이 0.2, 1.2, 3.5, 7.7초에 기록에 들어오고, 0.2 에서는 1초에 2걸음, 3 에서는 1170걸음이에요. n 이 2^p 가 되면 기계가 구슬이 다 내려앉고 30걸음이 더 지날 때까지 0.55초 서서, 3, 5, 7 기둥은 비고 2 기둥에만 구슬 p 개가 남아요. Start 노브는 2^11, 2^19, 2^29 에서 시작해 톱니를 높이고, 2^29 에서 시작하면 2 와 5 기둥이 31 이 나오기 전까지 구슬 31개로 올라가요. 잰 값은 움직임 1.514, 촘촘함 0.090, 가장자리 0.674, 복잡도 0.417, 채움 0.277, 색 0.132, 모양 바뀜 0.123 이고, thirds 는 36/19/27, 노브는 0.05, 0.09, 0.11, 0.05 예요. 처음 짠 판은 구슬이 기둥과 맞은 분수 칸 사이를 호로 날아서, 1초에 100걸음이면 선이 분수표를 덮어 숫자를 읽을 수 없었어요. 구슬을 제 기둥 안에서만 오르내리게 한 판을 처음 쟀을 때는 움직임 1.186, 채움 0.249 에 Hue 가 0.03 이었어요. 비행을 0.08초에서 0.3초로 늘리자 구슬이 쌓이기 전에 다시 떠나 기둥이 비어 보여서, 1초에 10걸음이 넘으면 쌓인 구슬은 레지스터 값을 바로 보이고 나는 구슬은 그 사이의 흐름만 그리게 했어요. 분수 잉크의 채도를 0.12 에서 0.45 로 올려 Hue 가 0.053 이 됐고, Trail 은 아래 끝에서 꼬리를 거의 끄고 밝기를 흐르는 양으로 나눠 0.054 가 됐어요. 새로 본 것은 걸음 대부분이 두 걸음짜리 고리 셋이라는 거예요. 29/33 과 77/29 는 3 을 7 로 옮기고, 17/91 과 78/85 는 7 과 5 를 하나씩 덜어 2 와 3 에 하나씩 얹고, 23/38 과 95/23 은 2 를 5 로 옮겨요. 2^7 까지 710걸음의 78.6%, 2^41 까지 101354걸음의 94.8% 가 이 셋이라, 패널에서는 한 기둥이 비는 만큼 옆 기둥이 차는 맞물림이 거의 쉬지 않고 보여요. 55/1 은 후보 n 마다 꼭 한 번 맞아서 41 까지 40번이에요. + +### Steiner Tree + +주제 5 「수학 · 추상」을 채우는 패턴이에요. 점 n 개를 잇는 가장 짧은 망을 찾되 바깥에 점을 더해도 되는 유클리드 슈타이너 나무 문제예요. 더한 점(슈타이너 점)에서는 늘 세 변이 120도로 만나요. 이름은 야코프 슈타이너(Jakob Steiner)에게서 왔고, 점 n 개의 문제는 보이테흐 야르니크(Vojtěch Jarník)와 밀로시 쾨슬러(Miloš Kössler)가 1934년에 냈어요. Z. A. 멜자크(Z. A. Melzak)는 1961년에 위상이 정해진 나무를 정삼각형으로 두 점씩 줄여 짓는 법을 보였어요. 에드거 길버트(E. N. Gilbert)와 헨리 폴락(H. O. Pollak)은 1968년 「Steiner Minimal Trees」에서 슈타이너 나무가 최소 신장 트리의 √3/2 배보다 짧아질 수 없다는 추측을 냈어요. 1990년 두딩주(Ding-Zhu Du)와 프랭크 황(Frank K. Hwang)이 낸 증명은 2012년 알렉산드르 이바노프(Alexander Ivanov)와 알렉세이 투질린(Alexey Tuzhilin)이 빈틈을 보여서, 지금도 추측이에요. 패턴은 단자 4~7 개의 완전 위상을 미리 다 만들어 둬요. 슈타이너 점이 n − 2 개이고 모두 차수 3 인 나무이고, 3, 15, 105, 945 가지예요. 슈타이너 점이 단자에 내려앉은 나무는 길이 0 인 변을 가진 완전 위상이라, 모든 완전 위상에서 가장 짧은 것이 답이에요. 위상 하나의 슈타이너 점은 워런 스미스(Warren D. Smith)가 1992년에 낸 반복으로 풀어요. 변마다 길이의 역수를 무게로 두고 나무의 일차 방정식을 잎에서부터 한 번에 풀면, 걸음마다 길이가 줄어요. 한 프레임에 위상 40 개를 차가운 시작에서 15걸음씩 풀어요. 3% 안으로 아깝게 진 것은 3초 동안 프레임마다 10걸음씩 더 풀고, 지금 나무의 안쪽 변마다 양쪽 이웃을 바꾼 두 위상도 12걸음씩 풀어요. 0.02% 넘게 짧은 것이 나오면 나무가 바뀌고, 그림이 달라졌으면 옛 나무가 1.2초 동안 호박색으로 사라져요. 정확한지는 Node 에서 따로 쟀어요. 다섯 설정을 120초씩 돌리며 5프레임마다 945 개 위상을 다 150걸음씩 푼 값과 3,600 번 맞댔어요. 3,598 번은 0.035% 안이었고(0.02% 는 바꾸는 문턱이에요), 두 번은 0.155% 와 0.298% 였어요. 늘 정확하다고는 못 하고, 일곱 단자가 빨리 떠돌 때 1초 안쪽으로 뒤처질 때가 있어요. Scatter 0 에서 단자는 패널을 내려가는 정삼각형 지그재그 띠에 서요. 삼각형 변은 단자가 넷일 때 60 LED, 일곱일 때 32 LED 이고, 단자마다 변의 3.5% 만큼 흔들려요. 넷은 60도 마름모라 두 슈타이너 나무가 거울상으로 길이가 같아요(√7/3 = 0.882). 그래서 흔들림이 1분에 8.4번 나무를 뒤집고, 뒤집히는 순간에도 두 나무의 안쪽 변은 22.7 LED 씩 남아 있어요. 다섯과 일곱은 0.867~0.904 를 읽고, 여섯은 1분에 10.4번 뒤집혀요. Scatter 1 에서는 단자가 패널 전체를 떠돌아요. 비는 시간의 84~91% 동안 0.95 위에 있고, 일곱 단자는 1분에 26.8번 뒤집혀요. 나무에서 6 LED 마다 흐린 고리가 퍼지고, 최소 신장 트리는 흐린 점선이에요. 잰 값은 움직임 0.062, 촘촘함 0.055, 가장자리 0.207, 복잡도 0.935, 채움 0.238, 색 0.117, 모양 바뀜 0.065 이고, thirds 는 18/37/15, 노브는 0.05, 0.08, 0.10, 0.08 예요. 첫 판은 움직임 0.053, 촘촘함 0.051, 가장자리 0.182, 복잡도 0.944, 채움 0.221, 색 0.108, 모양 바뀜 0.036, thirds 15/36/13, 노브 0.05, 0.07, 0.09, 0.08 이었고, 한 프레임에 3.8 ms 가 들었어요. 고리 밝기를 거리표로 읽고, 거리를 두 LED 에 하나씩만 정확히 셈해 나머지는 이웃 넷에서 읽고 나무 3.5 LED 안만 다시 세자 1.7 ms 가 됐어요. 여는 Scatter 0.06 은 단자를 떠도는 길 쪽으로 6% 당겨 마름모의 대칭을 깨서, 첫 뒤집힘이 30초에야 왔어요. Scatter 를 제곱으로 걸자(0.0036) 잰 8초 안의 6.3초에 첫 뒤집힘이 와요. 흔들림을 2.5 LED 에서 변의 3.5% 로 줄이자, 일곱 단자의 비가 0.90~0.92 에서 0.870~0.898 로 내려왔어요. 스윕만 두었을 때는 더 짧은 위상을 늦게 찾아서, 한 번은 막대가 1.005 로 최소 신장 트리보다 긴 나무를 보였어요. 아깝게 진 위상을 붙들어 두자 가장 큰 뒤처짐이 0.68% 에서 0.30% 로 줄었어요. 새로 본 것은 다섯과 일곱 단자의 지그재그 띠가 정확히 √3/2, 0.86602540 을 낸다는 거예요. 정삼각형 세 꼭짓점과 같은 값이에요. 나무가 정삼각형마다 별 하나로 갈라져 꼭짓점에서만 만나기 때문이고, 여섯은 0.8718, 넷은 0.8819 예요. + +### Miura-ori + +주제 5 「수학 · 추상」을 채우는 패턴이에요. 미우라 고료(三浦公亮, Koryo Miura)가 1970년에 낸 접기로, 1985년 「Method of packaging and deployment of large membranes in space」에서 우주에 펼칠 막을 싸는 법으로 다뤘어요. 합동인 평행사변형을 산접기와 골접기로 이어서, 자유도 하나로 면이 휘지 않은 채 접히고 끝까지 접으면 납작한 꾸러미가 돼요. 접는 동안 판이 가로와 세로로 함께 줄어서 면내 푸아송 비가 음수예요. 패턴은 판을 평행 이동 면 P(i, j) = A(i) + B(j) 로 세워요. A 는 세로와 높이 평면의 지그재그로 걸음이 (0, b cos ψ, ±b sin ψ) 이고, B 는 판 평면의 지그재그로 걸음이 (a sin g, ±a cos g, 0) 이에요. 그러면 모든 면이 변 b 와 a 의 평행사변형이고, cos ψ cos g = cos α 이면 각 α 를 지켜요. 마크 솅크(Mark Schenk)와 사이먼 게스트(Simon Guest)가 2013년에 낸 닫힌 꼴과 같고, 면이 판 평면과 이루는 접힘각 θ 와는 sin ψ = sin θ sin α 로 이어져요. Node 로 α 를 55도에서 85도까지 1도씩, ψ 를 0 에서 α 까지 400 걸음씩 셈했어요. 12,431 꼴의 187 면에서 변 길이는 5.3e-14, 모서리 코사인은 1.8e-15, 면의 평평함은 2.2e-14, 안쪽 꼭짓점 둘레 네 각의 합은 5.3e-15 안에서 지켜졌어요. 판은 세로 17 면, 가로 11 면이고 변은 12 와 10 LED 예요. 펼치면 패널에서 191줄 × 90 LED 라 패널을 넘쳐요. Sector 55 의 가장 깊은 접힘에서는 125 × 52 로 줄어 네 변의 가장자리가 겨우 들어와요. 가장 깊은 접힘은 ψ 가 0.9α 와 60도 가운데 작은 쪽이고, α 55 에서는 면이 판 평면에서 68도까지 서요. Speed 2 에서 4초 동안 접히고 4초 동안 펴져요. 눈은 바로 위에서 패널 아래쪽으로 24도 기울어 내려다봐요. 위에서 보면 접힌 판은 (u, v) 위의 높이 함수라서, 행을 판 아래쪽으로 차례로 그리면 가까운 면이 위에 남아요. 깊이 버퍼로 그린 것과 프레임이 같았어요. LED 하나에 표본 2 × 2 로 그리고, 이웃 표본이 다른 면이면 산접기는 밝은 선으로, 골접기는 어두운 선으로 그어요. 빛은 둘이에요. 호박빛 주광이 45도 높이에서, 파란 보조광이 그로부터 4분의 1 바퀴 돌아 15도 높이에서 비춰요. 면은 행이 판 위아래로 기우는 쪽과 열이 좌우로 기우는 쪽으로 네 갈래라서, 둘 다 받는 크림, 주광만 받는 호박, 보조광만 받는 파랑, 어느 쪽도 못 받는 검정으로 갈려요. 펼친 판은 네 갈래가 다 크림빛이에요. 접히면서 위를 보던 행이 주광을 받고, 더 깊어지면 열이 좌우로 기울어 파랑과 검정이 번갈아 서요. 잰 값은 움직임 0.711, 촘촘함 0.092, 가장자리 0.655, 복잡도 0.239, 채움 0.951, 색 0.250, 모양 바뀜 0.346 이고, thirds 는 94/96/94, 노브는 0.24, 0.42, 0.29, 0.45 예요. 처음 판은 13 × 8 면이라 펼쳤을 때 패널에 맞았고, 주광과 보조광이 마주 섰고, 깊이 버퍼로 그렸어요. 잰 값은 움직임 0.424, 촘촘함 0.059, 가장자리 0.560, 복잡도 0.211, 채움 0.743, 색 0.181, 모양 바뀜 0.442, thirds 69/84/68, 노브 0.19, 0.48, 0.24, 0.17 이었어요. 접히면 판이 가로 36, 세로 109 로 줄며 둘레의 검은 바탕이 드러나서, 모양 바뀜 0.442 가 축 끝 0.413 을 넘었어요. 프레임도 5.6 ms 였어요. 판을 17 × 11 면으로 키워 가장 깊이 접힐 때만 가장자리가 보이게 하자 모양 바뀜이 0.341 로 내려왔어요. 행을 차례로 그리고 줄마다 면 안의 구간을 풀어 채우자 1.7 ms 가 됐어요. 두 빛을 4분의 1 바퀴 떨어뜨려 네 갈래가 네 빛깔이 되게 했어요. 큰 α 에서 깊이 접힌 판이 검게 꺼지지 않도록 바탕빛을 0.04 에서 0.1 로 올리고, 가장 깊은 접힘을 60도로 막았어요. 새로 본 것은 Sector 가 판이 줄어드는 비율을 바꾼다는 거예요. 반쯤 접혔을 때 판이 세로로 1% 줄면, 가로로는 α 55 에서 0.66%, 70 에서 0.18%, 85 에서 0.01% 줄어요. −1/(tan²α cos²θ) 과 맞아요. 그래서 85 에서는 판이 가로로 거의 그대로(99%)이고 세로로만 51% 까지 줄어, 음의 푸아송 비가 사라지는 것이 패널에서 보여요. + +### Kruskal Count + +주제 5 「수학 · 추상」과 주제 4 「세로 64×128」을 같이 채우는 패턴이에요. 따로 출발한 사슬들이 한 줄로 모이는 것을 128줄 아래로 흘려 보이려 했어요. 크러스컬 카운트(Kruskal count)는 마틴 크러스컬(Martin Kruskal)이 1970년대에 만든 카드 마술이에요. 섞은 카드를 늘어놓고 관객이 첫 열 장 가운데 한 장을 몰래 골라 그 값만큼 앞으로 세어 가기를 되풀이하면, 마술사가 첫 장부터 따로 세어 간 사슬과 거의 늘 같은 마지막 카드에 닿아요. 에이스는 1, 숫자 카드는 제 값, 그림 카드는 5 로 세요. 마틴 가드너(Martin Gardner)가 1978년 Scientific American 의 Mathematical Games 칼럼에 소개했고, 제프리 라가리아스(Jeffrey Lagarias), 에릭 레인스(Eric Rains), 로버트 밴더베이(Robert Vanderbei)가 2001년 「The Kruskal Count」에서 셈했어요. 패턴은 52장을 13줄에 넷씩, 줄마다 방향을 바꾸는 뱀 꼴로 깔아요. 카드는 14×7 LED 에 3×5 숫자와 5×5 무늬를 그렸어요. 첫 열 장에서 색상환을 10분의 1씩 나눈 사슬 열이 출발하고, 읽는 머리가 Speed 2 에서 1초에 10.4장씩 위에서 아래로 훑어요. 머리가 토큰이 앉은 카드에 닿으면 토큰은 그 카드 값만큼 위로 휜 호를 그리며 뛰어요. 이미 토큰이 기다리는 카드에 내리면 고리를 터뜨리며 둘이 하나가 돼요. 토큰은 늘 머리보다 앞에 있어서 사슬은 다른 사슬이 아직 떠나지 않은 카드에서만 만나요. 합친 사슬은 식구 색의 평균을 쓰고 식구가 늘수록 흰색에 가까워져서, 열이 다 모이면 흰 실 하나가 남아요. 지나간 카드는 그 사슬 색으로 물든 채 남아요. 판이 끝나면 사슬이 가장 많이 닿은 카드를 36×52 LED 로 크게 뒤집어 2.2초 보이고, 그 아래 점 열 개 가운데 거기 닿은 사슬의 점만 제 색으로 켜요. 0.8초 동안 숫자가 뒤섞이며 다시 섞어서 한 판은 8초예요. Court 는 그림 카드 값을 5 에서 10 까지 바꾸고, 덱이 하나일 때는 그림 카드의 무늬 자리에 그 값을 써요. Decks 는 104장을 13줄에 여덟씩 깔고, 무늬는 숫자 색으로만 보여요. Node 에서 이 코드로 10만 판을 돌렸어요. 덱 하나에 Court 5 에서 사슬 열이 다 한 카드로 모인 판은 58.5% 였어요. 마술 그대로 첫 장의 사슬과 첫 열 장에서 고른 한 장의 사슬이 만난 몫은 85.4% 였어요. Court 10 에서는 28.4% 와 72.2%, 덱 둘에 Court 5 에서는 93.8% 와 97.9% 예요. 잰 값은 움직임 0.280, 촘촘함 0.113, 가장자리 0.622, 복잡도 0.273, 채움 0.599, 색 0.195, 모양 바뀜 0.153 이고, thirds 는 63/68/47, 노브는 0.06, 0.09, 0.04, 0.16 이에요. 첫 판은 Court 반응이 0.030 이고 한 프레임이 2.2 ms 였어요. 같은 씨앗의 패는 첫 3초 동안 그림 카드를 몇 장 지나지 않아서, 값을 바꿔도 사슬 길이 조금만 달라졌어요. 지나간 카드의 물을 0.3 에서 0.55 로, 호를 0.55 에서 0.7 로 밝혔어요. 그림 카드에 값을 쓰고 한 판을 6.5초에서 5초로 줄여 Court 를 0.042 로 올렸어요. 그사이 움직임은 0.143 에서 0.280 이 됐어요. 호의 LED 를 프레임마다 셈하지 않고 뛸 때 한 번만 셈하게 바꿔서 한 프레임이 1.7 ms 가 됐어요. 새로 본 것은 처음 패에서 열 사슬이 24째 카드 4♠ 에서 다 모인다는 거예요. 위 여섯 줄 안에서 색이 다 섞이고, 아래 일곱 줄은 흰 실 하나만 내려가 44째 카드 9♥ 에 닿아요. 같은 패를 Court 10 으로 세면 여덟 사슬은 그대로 9♥ 에 닿아요. 남은 두 사슬은 바로 앞 43째 카드 Q♠ 에서 멈춰서, 마술이 한 장 차이로 틀려요. + +### Lights Out + +주제 5 「수학 · 추상」과 주제 1 「지도의 빈 칸」을 같이 채우는 패턴이에요. 주제 1 은 촘촘함 × 모양 바뀜 4,4 칸을 겨눴어요. 이웃 사이 차이가 평균 0.297 을 넘으면서 2~8초에 블록 평균이 0.328 넘게 바뀌어야 하는 칸이에요. 라이츠 아웃(Lights Out)은 타이거 일렉트로닉스(Tiger Electronics)가 1995년에 낸 5×5 불빛 놀이예요. 한 칸을 누르면 그 칸과 위아래 왼쪽 오른쪽 이웃이 뒤집혀요. 클라우스 수트너(Klaus Sutner)는 1989년에 이 σ+ 놀이를 GF(2) 위 선형대수로 다뤘어요. 판을 b, 누른 칸을 x 로 두면 A x = b 를 푸는 일이에요. 마를로 앤더슨(Marlow Anderson)과 토드 페일(Todd Feil)은 1998년 『Mathematics Magazine』의 「Turning Lights Out with Linear Algebra」에서 핵(kernel)으로 풀리는 판을 가르고, 빛 쫓기로 손으로 푸는 길을 적었어요. 빛 쫓기는 위에서부터 줄마다 켜진 칸 바로 아래를 눌러 불을 맨 아래 줄로 미는 방법이에요. 남은 줄을 보고 맨 윗줄에서 누를 칸을 찾은 뒤 한 번 더 쫓아요. 패턴은 LED 하나를 한 칸으로 삼아 64×128 판 전체를 놀이판으로 써요. 새 판은 놀이기처럼 누르기로 만들어요. 칸마다 1/2 확률로 누른 칸 4천여 개를 섞어 0.6초 동안 하얗게 반짝이며 떨어뜨려요. Speed 2 에서 첫 쫓기가 2.4초에 127줄을 내려가요. 켜진 불은 호박색이고, 누른 칸은 하얗게 반짝인 뒤 하늘색 기록으로 남아요. 누르기는 2를 법으로 세서 같은 칸을 두 번 누르면 앞의 누르기가 지워지고, 그 칸은 장밋빛으로 반짝여요. 그래서 앞선 줄 위는 하늘색 기록이고 아래는 호박색 불이에요. 맨 아래 줄에 남은 불이 0.7초 깜박이는 동안 빛줄 하나가 맨 윗줄로 올라가요. 거기서 찾은 칸들이 눌리고, 두 번째 쫓기가 2.0초 동안 내려가요. 다 꺼지면 기록을 0.6초 보여 주고 0.5초에 어두워져서 한 판이 7.7초예요. 맨 윗줄에서 누를 칸은 64×64 쫓기 행렬을 GF(2) 가우스 소거로 풀어 찾아요. 이 행렬의 j 번째 열은 맨 윗줄 j 칸 하나만 누르고 쫓았을 때 맨 아래에 남는 줄이에요. Node 로 셈해 보니 이 행렬의 계수가 64 라서 64×128 판의 핵은 0 차원이고, 2^8192 가지 판이 다 풀리며 답은 하나뿐이에요. Cells 의 32×64, 21×42, 16×32 도 0 이에요. 줄 셋을 빼면 달라져서 64×125 는 핵이 36 차원이고, 놀이기의 5×5 는 2 차원이에요. 판 크기마다 40판씩 돌려 두 번째 쫓기 뒤에 불이 하나도 남지 않고, 기록이 판을 만든 누르기와 칸마다 똑같은 것도 봤어요. 64×128 에서는 평균으로 첫 쫓기가 4066 칸을 눌러요. 맨 아래 줄에 32 개가 남고, 맨 윗줄에서 31 칸을 누르고, 두 번째 쫓기가 4065 칸을 눌러요. 잰 값은 움직임 0.426, 촘촘함 0.322, 가장자리 0.953, 복잡도 0.254, 채움 0.470, 색 0.302, 모양 바뀜 0.399 이고, thirds 는 47/46/47, 노브는 0.25, 0.36, 0.27, 0.15 예요. 첫 판도 이미 이 칸에 섰어요. 그때 잰 값은 움직임 0.426, 촘촘함 0.322, 가장자리 0.953, 복잡도 0.252, 채움 0.470, 색 0.299, 모양 바뀜 0.399 였어요. LED 하나짜리 칸을 반씩 켜면 이웃 차이가 밝기의 절반쯤이 돼요. 그래서 쫓기가 지운 줄에도 같은 밀도의 기록을 남기면 촘촘함이 내내 0.3 근처에 머물고, 판이 어두워질 때 블록 평균이 0.4 쯤 떨어진다고 셈하고 짰어요. 기록을 끈 Record 1, 곧 불만 보이는 놀이기 모습은 촘촘함 0.053, 채움 0.074, 모양 바뀜 0.281 이에요. 칸을 2 LED 로 키우면 촘촘함이 0.158 이에요. 첫 판 뒤에는 앞선 누르기를 지우는 칸을 장밋빛으로 갈랐더니 색이 0.299 에서 0.302 로, 복잡도가 0.252 에서 0.254 로 바뀌었어요. 새로 본 것은 맨 윗줄 31 칸이 패널 전체를 다시 쓴다는 거예요. 두 번째 쫓기가 4065 칸을 누르고 그 가운데 2041 칸이 첫 쫓기의 누르기를 지워요. 그래서 기록은 내내 절반이 켜진 채로 8192 칸 가운데 절반을 바꿔 써요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -4177,7 +4256,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 25일 `dev`(`f72355d`)의 328개에 43판부터 73판까지의 삼백열 개를 더한 638개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 25일 `dev`(`f72355d`)의 328개에 43판부터 73판까지와 75판의 삼백스무 개를 더한 648개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -4281,6 +4360,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 73판은 72판(#335)의 열 개까지 든 628개에 빈 칸도 셋 이하인 칸도 없어서, 넷뿐인 칸 스물셋 가운데 72판이 고른 움직임 × 색 1,4 와 가장자리 × 색 1,4 를 빼고 촘촘함 × 채움 4,1 과 움직임 × 모양 바뀜 1,3 을 겨눴어요. 촘촘함 × 채움 4,1 은 이웃 차이 평균이 0.297 을 넘으면서 24/255 넘게 켜진 LED 가 0.302 아래인 칸이에요. 71판이 다음 감으로 적은 대로 어두운 바탕에 외딴 밝은 점을 찍는 길이에요. 켜진 LED 마다 네 이웃이 꺼져 있으면 촘촘함은 그 LED 의 몫의 두 배에 밝기를 곱한 값이라, Damask 는 5매 위수자의 묶는 점이 가로세로로 닿지 않는 것을 그대로 썼어요. 바탕의 20% 가 외딴 날실 점이고 낱말은 천의 10.4% 라서 촘촘함 0.324, 채움 0.263 이에요. 움직임 × 모양 바뀜 1,3 은 1초에 바뀌는 양이 0.061 아래이면서 모양 바뀜이 0.211~0.328 인 칸이라, 2~8초에 바뀌는 LED 가 한 번씩만 한쪽으로 바뀌어야 해요. Dürer's Alphabet 은 펜 끝과 컴퍼스 바늘 말고는 움직이는 것을 두지 않고, 잉크가 글자 몸을 채우고 종이가 밝아지는 차례를 재는 창 안에 넣어 움직임 0.048, 모양 바뀜 0.247 이에요. 6초 동안 바뀐 양을 다 더해도 모양 바뀜의 1.17 배예요. 둘 다 처음 판은 칸 밖이었어요. Damask 는 움직임 0.017 로 움직임 축의 아래 끝 0.019 를 내려서 천 밑에 낮은 물결을 흘렸고, Dürer's Alphabet 은 모양 바뀜 0.179 로 칸 아래에 서서 여는 자리를 옮겼어요. 열 개 다 일곱 축의 끝 안에 있어서 칸 경계는 60판 그대로예요. 72판까지 든 628개에 73판 열 개를 더한 638개에서 지도 21장은 다 `none` 이고, 겨눈 두 칸은 둘 다 다섯이 됐어요. 넷뿐인 칸은 스물셋에서 스물하나로 줄었어요. +75판은 73판까지 든 638개에 빈 칸도 셋 이하인 칸도 없어서, 넷뿐인 칸 스물하나 가운데 같이 돈 74판(#337)이 고른 채움 × 색 2,4 와 촘촘함 × 복잡도 3,3 을 빼고 촘촘함 × 모양 바뀜 4,4 와 가장자리 × 색 2,4 를 겨눴어요. 촘촘함 × 모양 바뀜 4,4 는 이웃 차이 평균이 0.297 을 넘으면서 2~8초에 블록 평균이 0.328 넘게 바뀌어야 하는 칸이에요. 두 축의 끝이 0.375 와 0.413 이라 두 값 다 좁은 띠예요. Lights Out 은 LED 하나를 한 칸으로 삼아 판의 반을 켜고, 쫓기가 지운 줄에도 같은 밀도로 누르기 기록을 남겨서 촘촘함이 내내 0.3 근처에 머물러요. 한 판이 끝나 판이 어두워질 때 블록 평균이 0.4 쯤 떨어져요. 첫 판부터 촘촘함 0.322, 모양 바뀜 0.399 로 칸 안에 섰어요. 기록을 끈 놀이기 모습은 촘촘함 0.053 이라, 칸을 세운 것은 불이 아니라 누르기 기록이에요. 모양 바뀜이 축 끝에서 0.014 아래라, 그 축의 끝이 움직이면 먼저 칸을 벗어날 패턴이에요. 가장자리 × 색 2,4 는 40 을 넘는 계단의 몫이 0.210~0.500 이면서 색이 0.5445 를 넘는 칸이에요. Prandtl's Membrane 의 첫 판은 램프가 낮아 획의 반이 어두워서 가장자리 0.180, 색 0.277 이었어요. 밝기에 바닥을 깔자 색이 0.427 로 올랐고, 글자를 키우자 색 0.509 에 가장자리가 0.451 로 칸 위 끝에 다가갔어요. 밝기를 응력에도 걸어 테두리가 밝고 등성이가 어둡게 하자, 획 안에 무른 계단이 늘어 가장자리가 0.284 로 내려가고 색이 0.566 으로 올랐어요. 끝판은 가장자리 0.261, 색 0.571 이에요. 색은 칸 아래 끝에서 0.027 위예요. 열 개 다 일곱 축의 끝 안에 있어서 칸 경계는 60판 그대로예요. 73판까지 든 638개에 75판 열 개를 더한 648개에서 지도 21장은 다 `none` 이고, 겨눈 두 칸은 둘 다 다섯이 됐어요. 73판까지 든 638개에서 스물하나이던 넷뿐인 칸은 75판 열 개를 더하자 열아홉이 됐어요. + ### 아직 없는 갈래 `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 가 섰어요. @@ -4438,9 +4519,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 73판에서는 Abracadabra 와 Airy Beam 이 128줄을 써요. Abracadabra 는 열한 줄을 128/11 = 11.64 줄 간격으로 세워 1행부터 126행까지 쓰는데, 줄이 한 글자씩 짧아지는 계단이라 아래로 갈수록 켜진 몫이 줄어 thirds 가 28/18/11 이에요. 처음에는 빠진 글자가 다 부서진 뒤 아주 꺼져서 맨 아래 3분의 1 이 8 이었고, 32/255 의 그을음을 남기자 11 이 됐어요. Airy Beam 은 128줄을 ξ 0~8 로 잡아 빔이 내려가며 휘고, 아래로 갈수록 곁 로브가 주 로브 왼쪽을 채워서 52/64/84 예요. Dürer's Alphabet 도 64줄 정사각형 두 장을 위아래로 쌓아 67/64/62 예요. Topologist's Sine Curve 는 x 를 세로에 정비례로 세워 흔들림이 아래 극한 선분 쪽에 몰려서 11/33/75 이고, 주제 4 로는 세지 않았어요. +75판에서는 Mesostic 과 Kruskal Count 가 128줄을 써요. Mesostic 은 6줄짜리 MatrixLight6 글줄을 7줄 간격으로 18칸 세워 1행부터 126행까지 쓰고, 날개 길이가 줄마다 달라 켜진 몫은 작아도 셋이 13/14/14 로 고르게 차요. Kruskal Count 는 카드 52장을 13줄에 넷씩 뱀 꼴로 깔고 읽는 머리가 위에서 아래로 훑어서 63/68/47 이에요. Euler Strokes 도 한 칸에 한 글자씩 128줄을 내려가며 낱말을 쌓아 30/34/22 예요. Lights Out 은 64×128 판 전체가 놀이판이라 47/46/47 이고, Miura-ori 는 펼친 판이 패널을 넘쳐 94/96/94 예요. Letter Mobile 의 97/98/96 은 거의 뒤 벽의 몫이고, 벽을 끄면 11/10/14 예요. + ### 낯선 수학 -주제 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)예요. 61판도 열을 더했어요. 세스나와 다섯 사람의 영온 무작위장 이징 눈사태(Crackling Noise), 몽주의 흙 옮기기와 매캔의 옮김 보간(Monge Transport), 파울의 덫과 마티외 방정식(Paul Trap), 그린힐의 제 무게 기둥(Greenhill Column), 흐바탈의 미술관 정리와 피스크의 세 색 증명(Art Gallery), 홀디치의 고리(Holditch Ring), 톰슨의 꺾인 역선과 헤비사이드의 움직이는 전하(Thomson Kink), 슈나이더와 바이스의 쌍성 렌즈(Binary Lens), 테일러의 되섞기(Taylor Unmixing), 브누아, 칼로, 디에네르 부부의 카나르 폭발(Canard Explosion)이에요. 62판도 열을 더했어요. 아브리코소프와 클라이너, 로스, 오틀러의 소용돌이 격자(Abrikosov Lattice), 울리크니의 청색 잡음과 빈자리와 뭉치 방법(Void and Cluster), 파인먼과 힙스의 체커보드(Feynman Checkerboard), 라도의 바쁜 비버와 마크센, 분트로크의 다섯 상태 챔피언(Busy Beaver), 폭스와 커시너, 비치의 평평한 곡면 위 직선 흐름(Veech Octagon), 리만의 미분 불가능 함수와 거버, 다위스테르마트의 복소 곡선(Riemann's Function), 헤슈와 킨즐레의 한 타일 형(Heesch Tiles), 차크라보르티와 헤이스, 보고시안의 마당 장터 모형(Yard-Sale Model), 베르그렌의 피타고라스 세 수 나무(Berggren Tree), 더브라윈과 람, 렁의 단위근 합과 밀른의 완전히 균형 잡힌 리듬(Balanced Rhythms)이에요. 63판은 다섯을 더했어요. 모팻의 모서리 소용돌이와 다네다의 사진(Moffatt Eddies), 맥스웰의 물고기 눈 렌즈(Maxwell's Fish-Eye), 이지케비치의 스파이크 신경망(Izhikevich Network), 애들러와 하소퍼, 우슬리의 들뜸 집합 오일러 지표(Excursion Sets), 심슨과 블라이스의 역설(Simpson's Paradox)이에요. 64판은 다섯을 더했어요. 암만과 베인커르의 팔각 타일링을 더 브라윈의 다중 격자로 지은 것(Ammann-Beenker), 겔판트, 카프라노프, 젤레빈스키의 아메바와 포르스베리, 파사레, 치흐의 구멍(Polynomial Amoeba), 키블과 주렉의 결함 얼기와 피시먼, 데 키아라, 칼라르코, 모리지의 지그재그 전이(Kibble-Zurek Ions), 야코비의 적분과 힐의 영속도 곡선(Hill's Regions), 스티븐슨과 카피차의 거꾸로 선 진자와 아치슨, 멀린의 인도 밧줄 묘기(Indian Rope Trick)예요. 65판은 다섯을 더했어요. 폰 노이만과 멀린스의 거품 법칙(Foam Coarsening), 베드퍼드와 맥멀런의 자기 아핀 카펫(McMullen Carpet), 클라인의 돛과 라그랑주의 주기 연분수(Klein's Sails), 야코비의 측지선과 샬의 공초점 곡면(Jacobi Geodesics), 하다마르와 리브친스키의 물방울과 코르테스의 정규화 스토크슬렛(Sedimenting Cloud)이에요. 67판은 다섯을 더했어요. 라돈의 변환과 라마찬드란, 락슈미나라야난의 램프 필터(Radon Transform), 아르틴의 브레이드 군과 폭스, 노이워스의 배치 공간(Artin Braids), 월시의 함수계와 실베스터의 행렬, 페일리의 차례(Walsh Functions), 아레프의 깜박이는 소용돌이(Blinking Vortex), 뒤팽의 사이클라이드(Dupin Cyclide)예요. 68판은 다섯을 더했어요. 들로네의 빈 외접원과 로슨의 뒤집기(Delaunay Flips), 모네로-뒤멘의 피보나치 단어 프랙탈(Fibonacci Word), 로즈의 검출 기준(Rose Criterion), 요세푸스 문제와 바셰의 셈 놀이(Josephus Problem), 레비치비타의 평행 이동과 푸코의 진자(Parallel Transport)예요. 69판은 다섯을 더했어요. 랭퍼드의 짝과 데이비스의 존재 조건, 스콜렘의 수열(Langford Pairs), 아하로노프와 봄의 효과와 베리 등의 파면 전위(Aharonov-Bohm), 페니의 놀이와 콘웨이의 선행수(Penney's Game), 휘트니와 그라우스타인의 회전수 정리(Whitney-Graustein), 슈타이너의 대칭화와 그로스의 수렴(Steiner Symmetrization)이에요. 70판은 다섯을 더했어요. 아우렌하머의 파워 다이어그램과 기타가와, 메리고, 티베르의 뉴턴 수송(Laguerre Cells), 배빗과 루윈의 헥사코드 정리(Hexachordal Theorem), 프렌켈과 콘토로바의 사슬(Frenkel-Kontorova), 에스하위스, 판 데르 벨레, 판 데르 메이르, 로서의 알갱이 라이덴프로스트 효과(Granular Leidenfrost), 카안의 가우스 곱셈 카오스(Multiplicative Chaos)예요. 71판은 다섯을 더했어요. 페니의 −1+i 진법과 데이비스, 크누스의 쌍둥이 용(Twindragon), 노바키, 한체, 벤트의 평탄 3차원 다양체와 콘웨이, 로세티의 이름(Platycosms), 크누스의 미니맥스 마스터마인드(Mastermind), 스넬슨의 텐세그리티 프리즘(Tensegrity), 아폴로니오스의 접촉 문제와 비에트의 풀이(Problem of Apollonius)예요. 72판은 다섯을 더했어요. 루퍼트의 들로네 다듬기(Ruppert Refinement), 라그랑주와 가우스의 격자 기저 줄이기(Lattice Reduction), 커비와 패리스의 히드라(Hydra Game), 프랭크의 액정 전위와 교차 편광 무늬(Schlieren Texture), 콘웨이와 콕서터의 정수 띠(Conway-Coxeter Frieze)예요. 73판은 다섯을 더했어요. 베리와 발라즈의 에어리 파속과 시빌로글루, 크리스토도울리데스의 유한 에너지 빔(Airy Beam), 스틴과 시배크가 반례로 실은 위상수학자의 사인 곡선(Topologist's Sine Curve), 에렌페스트 부부의 항아리와 카츠의 귀환 시간(Ehrenfest Urn), 월리스의 심슨 선과 슈타이너의 델토이드(Steiner Deltoid), 가드너가 소개한 보스의 1/f 주사위(Voss Dice)예요. +주제 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)예요. 61판도 열을 더했어요. 세스나와 다섯 사람의 영온 무작위장 이징 눈사태(Crackling Noise), 몽주의 흙 옮기기와 매캔의 옮김 보간(Monge Transport), 파울의 덫과 마티외 방정식(Paul Trap), 그린힐의 제 무게 기둥(Greenhill Column), 흐바탈의 미술관 정리와 피스크의 세 색 증명(Art Gallery), 홀디치의 고리(Holditch Ring), 톰슨의 꺾인 역선과 헤비사이드의 움직이는 전하(Thomson Kink), 슈나이더와 바이스의 쌍성 렌즈(Binary Lens), 테일러의 되섞기(Taylor Unmixing), 브누아, 칼로, 디에네르 부부의 카나르 폭발(Canard Explosion)이에요. 62판도 열을 더했어요. 아브리코소프와 클라이너, 로스, 오틀러의 소용돌이 격자(Abrikosov Lattice), 울리크니의 청색 잡음과 빈자리와 뭉치 방법(Void and Cluster), 파인먼과 힙스의 체커보드(Feynman Checkerboard), 라도의 바쁜 비버와 마크센, 분트로크의 다섯 상태 챔피언(Busy Beaver), 폭스와 커시너, 비치의 평평한 곡면 위 직선 흐름(Veech Octagon), 리만의 미분 불가능 함수와 거버, 다위스테르마트의 복소 곡선(Riemann's Function), 헤슈와 킨즐레의 한 타일 형(Heesch Tiles), 차크라보르티와 헤이스, 보고시안의 마당 장터 모형(Yard-Sale Model), 베르그렌의 피타고라스 세 수 나무(Berggren Tree), 더브라윈과 람, 렁의 단위근 합과 밀른의 완전히 균형 잡힌 리듬(Balanced Rhythms)이에요. 63판은 다섯을 더했어요. 모팻의 모서리 소용돌이와 다네다의 사진(Moffatt Eddies), 맥스웰의 물고기 눈 렌즈(Maxwell's Fish-Eye), 이지케비치의 스파이크 신경망(Izhikevich Network), 애들러와 하소퍼, 우슬리의 들뜸 집합 오일러 지표(Excursion Sets), 심슨과 블라이스의 역설(Simpson's Paradox)이에요. 64판은 다섯을 더했어요. 암만과 베인커르의 팔각 타일링을 더 브라윈의 다중 격자로 지은 것(Ammann-Beenker), 겔판트, 카프라노프, 젤레빈스키의 아메바와 포르스베리, 파사레, 치흐의 구멍(Polynomial Amoeba), 키블과 주렉의 결함 얼기와 피시먼, 데 키아라, 칼라르코, 모리지의 지그재그 전이(Kibble-Zurek Ions), 야코비의 적분과 힐의 영속도 곡선(Hill's Regions), 스티븐슨과 카피차의 거꾸로 선 진자와 아치슨, 멀린의 인도 밧줄 묘기(Indian Rope Trick)예요. 65판은 다섯을 더했어요. 폰 노이만과 멀린스의 거품 법칙(Foam Coarsening), 베드퍼드와 맥멀런의 자기 아핀 카펫(McMullen Carpet), 클라인의 돛과 라그랑주의 주기 연분수(Klein's Sails), 야코비의 측지선과 샬의 공초점 곡면(Jacobi Geodesics), 하다마르와 리브친스키의 물방울과 코르테스의 정규화 스토크슬렛(Sedimenting Cloud)이에요. 67판은 다섯을 더했어요. 라돈의 변환과 라마찬드란, 락슈미나라야난의 램프 필터(Radon Transform), 아르틴의 브레이드 군과 폭스, 노이워스의 배치 공간(Artin Braids), 월시의 함수계와 실베스터의 행렬, 페일리의 차례(Walsh Functions), 아레프의 깜박이는 소용돌이(Blinking Vortex), 뒤팽의 사이클라이드(Dupin Cyclide)예요. 68판은 다섯을 더했어요. 들로네의 빈 외접원과 로슨의 뒤집기(Delaunay Flips), 모네로-뒤멘의 피보나치 단어 프랙탈(Fibonacci Word), 로즈의 검출 기준(Rose Criterion), 요세푸스 문제와 바셰의 셈 놀이(Josephus Problem), 레비치비타의 평행 이동과 푸코의 진자(Parallel Transport)예요. 69판은 다섯을 더했어요. 랭퍼드의 짝과 데이비스의 존재 조건, 스콜렘의 수열(Langford Pairs), 아하로노프와 봄의 효과와 베리 등의 파면 전위(Aharonov-Bohm), 페니의 놀이와 콘웨이의 선행수(Penney's Game), 휘트니와 그라우스타인의 회전수 정리(Whitney-Graustein), 슈타이너의 대칭화와 그로스의 수렴(Steiner Symmetrization)이에요. 70판은 다섯을 더했어요. 아우렌하머의 파워 다이어그램과 기타가와, 메리고, 티베르의 뉴턴 수송(Laguerre Cells), 배빗과 루윈의 헥사코드 정리(Hexachordal Theorem), 프렌켈과 콘토로바의 사슬(Frenkel-Kontorova), 에스하위스, 판 데르 벨레, 판 데르 메이르, 로서의 알갱이 라이덴프로스트 효과(Granular Leidenfrost), 카안의 가우스 곱셈 카오스(Multiplicative Chaos)예요. 71판은 다섯을 더했어요. 페니의 −1+i 진법과 데이비스, 크누스의 쌍둥이 용(Twindragon), 노바키, 한체, 벤트의 평탄 3차원 다양체와 콘웨이, 로세티의 이름(Platycosms), 크누스의 미니맥스 마스터마인드(Mastermind), 스넬슨의 텐세그리티 프리즘(Tensegrity), 아폴로니오스의 접촉 문제와 비에트의 풀이(Problem of Apollonius)예요. 72판은 다섯을 더했어요. 루퍼트의 들로네 다듬기(Ruppert Refinement), 라그랑주와 가우스의 격자 기저 줄이기(Lattice Reduction), 커비와 패리스의 히드라(Hydra Game), 프랭크의 액정 전위와 교차 편광 무늬(Schlieren Texture), 콘웨이와 콕서터의 정수 띠(Conway-Coxeter Frieze)예요. 73판은 다섯을 더했어요. 베리와 발라즈의 에어리 파속과 시빌로글루, 크리스토도울리데스의 유한 에너지 빔(Airy Beam), 스틴과 시배크가 반례로 실은 위상수학자의 사인 곡선(Topologist's Sine Curve), 에렌페스트 부부의 항아리와 카츠의 귀환 시간(Ehrenfest Urn), 월리스의 심슨 선과 슈타이너의 델토이드(Steiner Deltoid), 가드너가 소개한 보스의 1/f 주사위(Voss Dice)예요. 75판은 다섯을 더했어요. 콘웨이의 분수 프로그램 PRIMEGAME(FRACTRAN), 야르니크와 쾨슬러의 문제와 길버트, 폴락의 비 추측(Steiner Tree), 미우라 고료의 접기(Miura-ori), 크러스컬의 카드 마술과 가드너의 소개(Kruskal Count), 수트너의 σ+ 놀이와 빛 쫓기(Lights Out)예요. 목록 밖에서 다음에 해 볼 것이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -4536,6 +4619,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 73판에서 본 것이에요. 이번 다섯은 수학이 정한 몫이 그림의 밝기를 정했어요. Topologist's Sine Curve 의 띠가 가장자리에서 밝은 것은 sin(1/x) 가 x 의 몫으로 ±1 근처에 오래 머물러서예요. 처음에는 곡선의 길이도 그렇다고 짐작했는데, 셈해 보니 길이로 세면 띠가 고르게 차서 두 셈을 넷째 노브로 섞게 했어요. Voss Dice 는 주사위를 넷에서 여덟로 늘려도 분홍 합의 한 걸음이 2.56 에서 2.61 로 거의 그대로인데, 백색의 한 걸음은 3.87 에서 5.46 으로 커져요. 그래서 주사위가 많을수록 가운데 칸만 차분해 보여요. 기다림은 기댓값처럼 고르게 오지 않았어요. Ehrenfest Urn 의 공 8 개는 600초에 기대 23.4 번 가운데 20 번 모두 위로 돌아왔는데, 63.0초와 63.2초처럼 두 걸음 사이로 붙어 오고 47초를 비우기도 했어요. 정리가 못 박는 수는 이번에도 셈으로 먼저 지켜봤어요. Steiner Deltoid 는 무작위 삼각형 200 개에서 세 발이 이루는 넓이가 1e-15 아래였고, 감싸는 곡선이 구점원 중심에서 R/2 와 3R/2 사이에 들었어요. Airy Beam 은 FFT 로 지은 맨 윗줄이 Ai 의 멱급수와 1e−5 안에서 맞았고, 구슬이 가린 로브는 아래에서 자라도 자유 빔 세기의 70% 를 넘지 못했어요. +75판에서 본 것이에요. 이번 다섯은 셈이 정한 순간이 그림의 사건이 됐어요. Steiner Tree 를 짜기 전에는 슈타이너 점 둘 사이의 변이 0 으로 줄었다가 반대로 다시 서는 뒤집힘을 그리려 했어요. 그런데 네 변이 한 점에서 만나는 나무는 가장 짧을 수 없어서, 가장 짧은 나무는 두 위상의 길이가 같아지는 순간 안쪽 변을 22.7 LED 씩 남긴 채 뛰어 바뀌어요. 그래서 뒤집힘을 옛 나무가 호박색으로 사라지는 뜀으로 그렸어요. FRACTRAN 은 걸음 대부분이 두 걸음짜리 고리 셋이라, 2^41 까지 101354걸음의 94.8% 가 그 셋이었어요. Lights Out 의 64×128 판은 쫓기 행렬의 계수가 64 라 핵이 0 차원인데, 줄 셋을 뺀 64×125 는 36 차원이에요. Kruskal Count 는 그림 카드를 5 에서 10 으로 세면 열 사슬이 다 모이는 판이 58.5% 에서 28.4% 로 줄었어요. Miura-ori 는 Sector 를 85도로 열면 판이 가로로 99% 그대로인 채 세로로만 51% 까지 줄어, 음의 푸아송 비가 사라져요. 정리가 못 박는 수는 이번에도 셈으로 먼저 지켜봤어요. Steiner Tree 는 945 위상을 다 푼 값과 3,600 번 맞대 3,598 번이 0.035% 안이었고, Miura-ori 는 12,431 꼴에서 변 길이가 5.3e-14 안으로 지켜졌어요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. @@ -4618,6 +4703,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 목록 밖에서 73판 뒤에 해 볼 것이에요. 위상수학자의 사인 곡선의 오른쪽 끝을 호로 극한 선분에 이어 닫으면 바르샤바 원이 돼요. 원처럼 한 바퀴 도는 꼴인데 그 위의 닫힌 경로는 다 한 점으로 줄어들어서, Topologist's Sine Curve 의 띠 셈과 구슬에 호 하나만 더하면 돼요. 닫는 호의 끝 (1, sin 1) 이 패널에 들도록 X 를 1 까지 넓혀야 해요. 패트릭 모란(Patrick Moran)의 과정(1958)은 걸음마다 공 하나가 사라지고 다른 공의 빛깔을 베껴 새로 나서, Ehrenfest Urn 처럼 수가 한 걸음에 많아야 하나씩 움직이는데도 0 이나 N 에 붙으면 돌아오지 않아요. 같은 두 방과 띠로 두 사슬을 나란히 세울 수 있어요. 카를 빌헬름 포이어바흐(Karl Wilhelm Feuerbach)의 정리(1822)는 구점원이 내접원과 방접원 셋에 늘 닿는다는 것이라, Steiner Deltoid 의 바뀌는 삼각형 위에서 닿는 점 넷이 구점원을 미끄러지는 것을 그릴 수 있어요. 니콜라오스 에프레미디스(Nikolaos Efremidis)와 크리스토도울리데스의 자동 집속 빔(2010)은 마주 보는 에어리 빔 둘이라, Airy Beam 의 표를 거울로 하나 더 읽으면 줄곧 어둡던 가운데가 아래쪽 한 줄에서 갑자기 밝아져요. 브누아 망델브로(Benoit Mandelbrot)와 존 밴 네스(John Van Ness)의 분수 브라운 운동(1968)은 허스트 지수 H 하나로 거칠기가 이어서 바뀌어서, Voss Dice 의 갈색 칸을 노브 하나로 거칠게도 매끄럽게도 할 수 있어요. +목록 밖에서 75판 뒤에 해 볼 것이에요. 마빈 민스키(Marvin Minsky)의 두 카운터 기계(1967)는 카운터 여럿을 2^a 3^b 5^c 하나로 접어 카운터 둘로 무엇이든 셈해요. FRACTRAN 이 바로 그 접기라서, 같은 구슬 기둥 둘에 접힌 수가 얼마나 길게 차고 비는지 볼 만해요. 모리스 해넌(Maurice Hanan)이 1966년에 보인 대로 직각 거리의 슈타이너 나무는 단자의 가로세로 줄이 만드는 해넌 격자 위에서 찾으면 되고, 비의 바닥은 프랭크 황(Frank K. Hwang)이 1976년에 보인 2/3 이에요. LED 격자에 맞는 직각 나무를 Steiner Tree 의 유클리드 나무와 나란히 세워 볼 만해요. 마크 솅크(Mark Schenk)와 사이먼 게스트(Simon Guest)가 2013년 논문에서 함께 다룬 달걀판 접기는 두 지그재그를 다 높이 방향으로 세운 평행 이동 면이에요. 한쪽이 줄 때 다른 쪽이 늘어 푸아송 비가 양수라서, Miura-ori 의 지그재그 하나만 세우면 나란히 견줄 수 있어요. 제임스 프롭(James Propp)과 데이비드 윌슨(David Wilson)의 과거로부터의 결합(1996)은 모든 상태에서 같은 난수로 사슬을 돌리고, 다 합쳐지지 않으면 더 먼 과거에서 다시 시작해서 합쳐진 값이 정확히 정상 분포를 따라요. Kruskal Count 의 사슬 합치기를 작은 마르코프 사슬의 상태 열에 걸면 그려져요. 수트너는 어느 그래프에서든 다 켜진 판은 σ+ 놀이로 다 끌 수 있음을 보였어요. Lights Out 의 칸을 육각 격자나 글자 모양 영역으로 바꿔 전체 GF(2) 소거로 풀면, 풀리지 않는 판이 많은 영역에서도 다 켠 판만은 늘 꺼지는 것을 볼 수 있어요. + ### 타이포 주제 7 로 해 본 글자 실험과 다음에 해 볼 것이에요. 63판과 64판이 같이 돌며 다섯씩 처음 만들었어요. 63판의 다섯은 한글 자모가 날아와 모아쓰기로 잠기는 조립(Jamo Assembly), 놀턴과 하먼의 기호 모자이크(Type Mosaic), 호프스태터의 트립렛 그림자(Trip-let), 카프리카의 계산 종이(Kaprekar's Routine), 캐럴의 낱말 사다리(Word Ladder)예요. @@ -4702,6 +4789,14 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 73판 뒤에 해 볼 것이에요. 바빌로니아의 60진 쐐기 숫자는 세운 쐐기 아홉 다음 열이 갈고리 하나가 되고 갈고리 여섯이 윗자리 쐐기 하나가 돼서, Maya Numerals 의 받아 올림을 그대로 쓸 수 있어요. 고대 바빌로니아 숫자에는 처음에 0 기호가 없어서 빈 자리를 어떻게 보일지가 숙제예요. 한글 한 자를 마스터 넷으로 그려 Multiple Master 처럼 섞으면 굵기 축에서 ㅁ 과 ㅇ 의 속공간이 어느 차례로 닫히는지 보여요. Dürer's Alphabet 의 정사각형과 컴퍼스로 ㅇ 과 ㅎ 의 원, ㄱ 과 ㄴ 의 꺾임을 긋고 모아쓴 한 글자의 몸에 잉크를 흘리면 한글 짓기가 돼요. 레온 글래스(Leon Glass)의 짝 점 무늬(1969)는 점마다 짝 점을 조금 돌린 자리에 찍는데, 낱말 안에서만 돌리면 점 하나하나에는 무늬가 없고 짝끼리의 결이 글자로 읽혀요. Glass Patterns 의 셈으로 Damask 와 다른 길의 숨은 글을 짤 수 있어요. +75판의 다섯은 구멍 카드의 그림자에서 셈으로 되살리는 낱말(Coded Aperture), 홀수 꼭짓점 수만큼만 펜을 떼며 쓰는 낱말(Euler Strokes), 비틀린 막대의 단면으로 부푸는 글자(Prandtl's Membrane), 잉크만큼 무게를 단 모빌의 글자(Letter Mobile), 케이지의 규칙으로 『월든』에서 뽑은 메조스틱(Mesostic)이에요. + +75판에서 본 것이에요. 이번에도 다섯이 다 글자 꼴을 코드에 적었고, 옮겨 쓴 글꼴은 Mesostic 의 MatrixLight6 하나예요. 셋은 글자를 모양이 아니라 수로 썼어요. Letter Mobile 에서는 잉크가 무게라서, E 180 과 A 172 처럼 가까운 글자는 바뀌어도 막대가 1.4도만 기울고 D 204 가 L 119 가 되면 35도 기울어요. Euler Strokes 에서는 홀수 꼭짓점이 획 수를 정해서, 학교 순서의 PATH 는 획이 둘 늘 뿐인데 펜 든 길은 232 LED 로 오일러 순서의 2.6 배예요. Prandtl's Membrane 에서는 안쪽 모서리가 응력이 한없이 커지는 자리라 일부러 각지게 두었고, 구멍 하나가 토크를 바꿔 HOT 이 LIT 의 4.0배를 져요. 그림자로 그린 글은 셈 없이는 읽히지 않았어요. Coded Aperture 의 검출기에는 LED 마다 광자가 평균 11.4개 떨어져 무늬보다 알갱이가 먼저 보이는데, 해독 배열과 상관시키면 2.6초에 낱말이 잡음 폭의 2.5배로 서요. 규칙으로 뽑은 글은 원문을 남기기도 했어요. Mesostic 의 첫 세 줄은 i wenT to, tHe, wOods 라서 소로의 문장이 50% 규칙을 지나고도 등뼈 위에서 읽혀요. + +기능 감은 이번에도 글자를 코드 밖에서 받는 길로 모였고, 받으려는 꼴이 갈렸어요. Coded Aperture 와 Letter Mobile 은 Text 레이어가 래스터한 비트맵을 바랐어요. Prandtl's Membrane 은 윤곽 다각형이나 부호 거리장을 바랐어요. 막 풀이는 경계까지의 거리를 반 LED 아래로 알아야 해서 덮임 비트맵으로는 모자라요. Euler Strokes 는 획의 가운데 선을 꼭짓점과 변으로 주는 한 줄 글꼴을 바랐어요. 앨런 허시(Allen Vincent Hershey)의 1967년 글꼴 같은 것이에요. 한붓 셈에는 굵기가 아니라 뼈대가 있어야 해요. Mesostic 은 다른 것을 바랐어요. 줄마다 정한 글자 하나를 같은 x 에 세우는 맞춤이 있으면 Text 레이어 하나로 메조스틱 한 쪽을 세울 수 있어요. + +75판 뒤에 해 볼 것이에요. 잭슨 맥 로(Jackson Mac Low)가 1963년부터 쓴 디아스틱(diastic) 읽기는 씨앗 낱말의 k 번째 글자가 k 번째 자리에 든 낱말을 원문에서 차례로 골라요. Mesostic 의 읽는 띠를 두고 대문자를 줄 가운데 대신 낱말의 k 번째 자리에 세우면, 등뼈가 사선으로 내려가요. 글자 판을 실에 매다는 대신 물에 띄우면 아르키메데스의 『떠 있는 물체에 대하여』대로 잉크가 무게를, 잠긴 부분의 모양이 부력 중심을 정해서, 무게가 거의 같은 E 와 A 가 서로 다른 각으로 기울어 떠요. 아르파드 나다이(Árpád Nádai)의 모래 더미 비유(1923)는 완전 소성 비틀림을 같은 기울기로 쌓은 모래로 보여서, 글자 위에 모래를 부으면 능선이 글자의 중심축을 따라 서요. Prandtl's Membrane 의 다각형 글자에서 거리장만 구하면 돼요. 구멍을 낱말 꼴로 낸 카드 한 장으로 별 무리를 찍으면 별마다 그 낱말이 한 벌씩 겹쳐 찍히는 글자 보케가 되고, Coded Aperture 의 광자 셈과 띠를 그대로 쓰되 해독만 하지 않으면 돼요. 한글 흘림처럼 자모를 이어 붙여 음절 하나를 이어진 그래프 하나로 만들면, ㅁ, ㅇ, ㄹ 이 든 음절 가운데 몇이 정말 한붓에 써지는지 셀 수 있어요. Euler Strokes 는 자모를 떼어 두어서 「한」이 다섯 조각, 「붓」이 세 조각이에요. + ### 화장품 팝업 주제 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 마다 한 번만 바뀌는 셈으로 그릴 만해요. From 821bf3ddc86e4b99f20a06376e6f0afa7b4b4fa4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 02:24:16 +0000 Subject: [PATCH 22/22] =?UTF-8?q?index.html=20=EC=9D=84=2075=ED=8C=90?= =?UTF-8?q?=EA=B9=8C=EC=A7=80=20658=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_01Gdqi6PBtzYrhzPXCgRY4TG --- 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 669307817..359394865 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

패턴 연구

- - + +