From e46778eb803d43895fde9c2007edb42e799592a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 03:09:27 +0000 Subject: [PATCH 1/8] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=20=EC=97=B0=EA=B5=AC=2098=EB=B0=94=ED=80=B4:=20?= =?UTF-8?q?=EC=83=88=20=ED=8C=A8=ED=84=B4=20=EC=97=B4=20=EA=B0=9C?= 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_01Bc4JkrYAzT3TWt21dU8pXk From 7fdf43bda147b8712e44e0bf6c9390a8f0751612 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 03:50:32 +0000 Subject: [PATCH 2/8] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20100=ED=8C=90:=20Gosper?= =?UTF-8?q?=20Islands=20=C2=B7=20Rolling=20Shutter?= 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_01Bc4JkrYAzT3TWt21dU8pXk --- .../pattern-study/patterns/gosper-islands.js | 178 +++++++++++++++++ .../pattern-study/patterns/rolling-shutter.js | 185 ++++++++++++++++++ 2 files changed, 363 insertions(+) create mode 100644 incoming/pattern-study/patterns/gosper-islands.js create mode 100644 incoming/pattern-study/patterns/rolling-shutter.js diff --git a/incoming/pattern-study/patterns/gosper-islands.js b/incoming/pattern-study/patterns/gosper-islands.js new file mode 100644 index 00000000..6309dde3 --- /dev/null +++ b/incoming/pattern-study/patterns/gosper-islands.js @@ -0,0 +1,178 @@ +// ===== Patternflow pattern ===== +// Title: Gosper Islands +// Family: fractal +// Idea: An endless zoom into the plane tiled with Gosper islands, worked out for every LED from its digits in base 2 - w over the Eisenstein integers, so seven islands always make one island the square root of 7 larger and turned 19.1 degrees, each turn of the loop grows the picture by exactly that, the big islands take their colour from one digit and their shade from the next, and the coasts between them are drawn as bright crinkled lines. +// Author: patternflow-lab +// Date: 2026-11-03 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Gosper island, the boundary of the flowsnake curve (Bill Gosper, 1973; published by Martin Gardner, Scientific American, December 1976; named by Benoit Mandelbrot, The Fractal Geometry of Nature, 1982), here as the tile of the base 2 - w number system on the Eisenstein integers with digits 0 and the six units; pattern round 100 +// Round: 100 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Depth=1..3, Hexagon=0..1 +// +// Knob 1: Hue. Turns the seven island colours round the wheel together. At 0 they run green, teal, cyan, azure (the centre island of each cluster), blue, violet and pink, and the coasts are the same hues paled almost to white; at 0.5 they run violet, pink, red, orange (the centre), yellow, green and teal. +// Knob 2: Speed. At 2 one turn of the loop, a zoom of 2.65 times and a turn of 19.1 degrees, takes 7 s; 70 s at 0.2 and 4.7 s at 3. +// Knob 3: Depth. How far down the coasts go. At the opening 1 they are drawn for islands whose centres sit 7.8 LEDs apart or more, fading in up to 20.7 and two LEDs wide from 12.7 up; at 3 they reach islands 2.4 LEDs apart, so every island shows its own seven sub-islands as a fine lace. +// Knob 4: Hexagon. Builds each island from coarser pieces, undoing the fractal one level at a time. Below 0.15 every coast is worked out from pieces under one LED, the true island; up to 0.45 from 49 hexagons an island, up to 0.8 from 7 (Gosper's flower), and above 0.8 every island is a plain hexagon, the tiling the islands grew from. + +// Eisenstein integers are the numbers a + b w with w = e^(2 pi i / 3); they +// sit on the triangular lattice. The base beta = 2 - w has norm +// 4 + 2 + 1 = 7, so there are seven classes mod beta, and 0 and the six +// units 1, 1 + w, w, -1, w^2, -w (in order round the circle) are one of +// each. In the quotient w = 2, so a + b w falls in class (a + 2b) mod 7. +// Take that digit off and divide by beta: +// +// (x + y w) / (2 - w) = ((3x - y) + (x + 2y) w) / 7, +// +// exact once the digit is gone. Repeating it writes every Eisenstein integer +// as a finite sum of digits times powers of beta: checked for all 361,201 +// with |a| and |b| up to 300, none needs more than 8 digits. +// +// The points sum d_k beta^-k over k >= 1 form one tile T, with +// beta T = T and its six neighbours T + u. Those seven pieces are the centre +// and the hexagonal ring, so T is the Gosper island: seven of it make one +// sqrt 7 = 2.65 times larger, turned by the angle of beta, arctan(sqrt 3 / 5) +// = 19.1 degrees, and copies of T on every lattice point tile the plane. +// Level l islands are beta^l (T + g); a point's island at level l is what +// remains of its nearest lattice point at level 0 after l digits are taken +// off, and the l-th digit says which of the seven pieces of its level +// l + 1 island it sits in. +// +// The camera looks at the plane through z beta^-t, t the loop phase from 0 +// to 1. Multiplying by beta moves every level up by one, so at t = 1 the +// picture is the one at t = 0 with every level renamed: the zoom loops with +// no seam. Level l sits 0.16 * sqrt(7)^(l + t) LEDs apart on the panel, +// level 0 under one LED, so the coast of every island larger than that is +// right to the LED; eight levels reach islands larger than the panel. +// +// Each LED takes its hue from the digit of its island at the level whose +// centres sit 55 LEDs apart, and its shade from the digit one level down, +// the centre piece brightest. Both slide one level down over the loop, +// smoothstepped, so a piece grows into the colour of its own digit as its +// parent leaves the panel. +// A coast is where two neighbouring LEDs lie in different islands of a +// level; levels fade in by size, and the largest draw both sides. +// The zoom centre is the panel centre, the centre of the centre island at +// every level, so the clusters spiral out round it. + +const W = 64, H = 128, N = W * H; +const { floor, round, sqrt, cos, sin, atan2, pow } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const S3 = sqrt(3); +// The angle of beta = 2 - w = 5/2 - i sqrt(3)/2. +const ARG = atan2(-S3 / 2, 2.5); +// Spacing of level 0 on the panel at t = 0, levels worked out, seconds a +// loop at Speed 1, and the level that colours. +const L0 = 0.16, LV = 8, CYCLE = 14, FL = 5; +// Class r = (a + 2b) mod 7 of a + b w: the digit in that class, and its +// place round the circle (-1 for the centre). +const DA = [0, 1, 0, 1, -1, 0, -1], DB = [0, 0, 1, 1, -1, -1, 0]; +const ANG = [-1, 0, 2, 1, 4, 5, 3]; +// Shade of a piece by its place: the centre full, the ring alternating. +const SHADE = ANG.map((a) => (a < 0 ? 1 : a % 2 ? 0.6 : 0.78)); +// Hue step of a piece by its place, the centre in the middle of the band. +const BAND = ANG.map((a) => 0.3 + 0.1 * (a < 0 ? 3 : a < 3 ? a : a + 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 - s * f), t = v * (1 - s * (1 - f)); + return [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; +} + +export function setup(params) { + params.s = { t: 0, hue: 0, depth: 1, hex: 0.06, key: new Int32Array(LV * N), dig: new Uint8Array(LV * N) }; +} + +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] || 0; + const speed = cl(k[1] == null ? 2 : k[1], 0.2, 3); + S.depth = cl(k[2] == null ? 1 : k[2], 1, 3); + S.hex = cl(k[3] == null ? 0.06 : k[3], 0, 1); + S.t = (S.t + (d * speed) / CYCLE) % 1; +} + +// For every LED and level, the island (as a key) and its digit. An island +// at level l is built from the nearest lattice point at level l - n, so n +// levels of pieces; n = LV means from level 0. +function islands(S, n) { + const t = S.t, key = S.key, dig = S.dig, kr = [], ki = []; + for (let l = 0; l < LV; l++) { + // z beta^-(l + t), divided by the level 0 spacing. + const m = pow(7, -(l + t) / 2) / L0, ph = -ARG * (l + t); + kr.push(m * cos(ph)); + ki.push(m * sin(ph)); + } + for (let y = 0; y < H; y++) { + const zy = y - 63.5; + for (let x = 0; x < W; x++) { + const zx = x - 31.5, p = y * W + x; + let from = -1, a = 0, b = 0, at = 0; + for (let l = 0; l < LV; l++) { + const f = l - n < 0 ? 0 : l - n; + if (f !== from) { + // Nearest lattice point: the rhombus of 1 and w is two equilateral + // triangles, so it is one of the four corners. + const cx = zx * kr[f] - zy * ki[f], cy = zx * ki[f] + zy * kr[f]; + const bf = (2 * cy) / S3, af = cx + bf / 2; + const a0 = floor(af), b0 = floor(bf), fa = af - a0, fb = bf - b0; + let best = 1e9; + for (let c = 0; c < 4; c++) { + const da = fa - (c & 1), db = fb - (c >> 1), d2 = da * da - da * db + db * db; + if (d2 < best) { + best = d2; + a = a0 + (c & 1); + b = b0 + (c >> 1); + } + } + from = at = f; + } + while (at < l) { + const r = (((a + 2 * b) % 7) + 7) % 7; + a -= DA[r]; + b -= DB[r]; + const na = (3 * a - b) / 7; + b = (a + 2 * b) / 7; + a = na; + at++; + } + key[l * N + p] = a * 8192 + b; + dig[l * N + p] = (((a + 2 * b) % 7) + 7) % 7; + } + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const hx = S.hex; + islands(S, hx < 0.15 ? LV : hx < 0.45 ? 2 : hx < 0.8 ? 1 : 0); + const t = S.t, key = S.key, dig = S.dig, wf = t * t * (3 - 2 * t); + // Coast strength by level: fades in over one level from u0, and the + // smaller islands stay dimmer. + const u0 = 4 - (S.depth - 1) * 0.6, cw = []; + for (let l = 0; l < LV; l++) { + const u = l + t; + cw.push(l === 0 ? 0 : cl(u - u0, 0, 1) * (0.5 + 0.5 * cl((u - 2.5) / 3, 0, 1))); + } + for (let y = 0; y < H; y++) { + for (let x = 0; x < W; x++) { + const p = y * W + x; + const r2 = dig[(FL + 1) * N + p], r1 = dig[FL * N + p], r0 = dig[(FL - 1) * N + p]; + const hh = S.hue + BAND[r2] + (BAND[r1] - BAND[r2]) * wf; + const sh = SHADE[r1] + (SHADE[r0] - SHADE[r1]) * wf; + let c = 0; + for (let l = 1; l < LV; l++) { + if (cw[l] <= c) continue; + const o = l * N + p, kk = key[o]; + const both = l + t > 4.5; + if ((x < W - 1 && key[o + 1] !== kk) || (y < H - 1 && key[o + W] !== kk) || (both && ((x > 0 && key[o - 1] !== kk) || (y > 0 && key[o - W] !== kk)))) c = cw[l]; + } + const col = hsv(hh, 0.82 * (1 - 0.75 * c), 0.48 * sh + (1 - 0.48 * sh) * c); + display.setPixel(x, y, round(col[0] * 255), round(col[1] * 255), round(col[2] * 255)); + } + } +} diff --git a/incoming/pattern-study/patterns/rolling-shutter.js b/incoming/pattern-study/patterns/rolling-shutter.js new file mode 100644 index 00000000..6c6275a2 --- /dev/null +++ b/incoming/pattern-study/patterns/rolling-shutter.js @@ -0,0 +1,185 @@ +// ===== Patternflow pattern ===== +// Title: Rolling Shutter +// Family: type +// Idea: The panel is a camera sensor read one row at a time from the top down while a word spins about the centre like a baton or a propeller, or races sideways past leaning posts, so every row holds the word as it was when a faint scan line last passed it, and the spinning letters bend into arcs and spirals and the racing word leans the way the car leans in Lartigue's 1912 Grand Prix photograph. +// Author: patternflow-lab +// Date: 2026-11-03 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: rolling-shutter distortion, a picture read out row by row while the subject moves, as in the forward-leaning car and the spectators leaning the other way in Jacques Henri Lartigue's photograph of a car at the Grand Prix of the Automobile Club de France (1912), taken with a focal-plane shutter; capitals drawn as stroke centre lines for this pattern; pattern round 100 +// Round: 100 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Mode=0..2, Readout=0..1 +// +// Knob 1: Hue. Turns the ink round the wheel. At 0 a row just read is cyan and a row read a whole sweep ago blue violet, so the colour tells how old each row is; at 0.5 the same runs from red to yellow. The scan line stays grey. +// Knob 2: Speed. At 2 the baton and the propeller turn 0.4 times a second, one turn in 2.5 s (25 s at 0.2, 1.7 s at 3), and the race word crosses the panel at 72 LEDs a second while the posts behind it run the other way at 96. +// Knob 3: Mode. 0 the baton, SHUTTER in 7 capitals 101 LEDs long and 17 tall, spinning about its middle; 1 a propeller of three blades with SPIN on each, tips 61 LEDs from the centre; 2 the race, GRAND over PRIX in 14-LED capitals, repeating every 100 LEDs, over dim upright posts 16 LEDs apart. +// Knob 4: Readout. The time one sweep of the scan line takes, 2.5 s times the square root of the knob: 0 is a global shutter and straight letters, the opening 0.06 a sweep of 0.61 s in which the baton turns 88 degrees, 0.25 a sweep of 1.25 s and 1 a sweep of 2.5 s, a whole turn at Speed 2, where the baton comes back to the same angle on every sweep and stands as a still spiral under the moving scan line. + +// A rolling shutter does not take the whole picture at once. The sensor +// resets and reads its rows one after another, top to bottom, and row y +// holds the scene at the moment the readout reached it, y / 128 of a sweep +// after row 0. Anything that moves during the sweep is drawn at a +// different place in every row: a turning bar bends into a curve, and a +// thing crossing the frame shears, its bottom drawn later than its top. +// Jacques Henri Lartigue's 1912 photograph of a racing car is the famous +// case. His focal-plane shutter took the scene from the bottom up and he +// panned after the car, so the car leans forward with oval wheels and the +// spectators, sliding the other way through the frame, lean back. This +// sensor reads from the top, so the race word, moving right, leans back, +// and the posts lean the other way. +// +// The panel is the sensor as it is read. The scan line goes down one row +// at a time and the sweeps follow on with no pause; each row keeps what it +// read until the line comes round again. Row y is therefore +// +// age = Readout * frac(phase - y / 128) +// +// seconds old, phase being how far the current sweep has gone: 0 just +// above the line and nearly a whole sweep just below it. Each row draws the +// word as it stood that long ago, so the picture tears at the scan line, +// where the newest row meets one a sweep old, and the line repaints the +// word as it passes. When the word comes back to the same place every +// sweep (a whole turn for the baton, a third of a turn for the propeller) +// the tear closes and the picture stands still, the stroboscope's trick. +// +// The capitals are centre lines in a box 4 units wide and 6 tall, drawn as +// a distance field with round ends, so they turn to any angle cleanly: an +// LED is ink where it lies within the stroke radius of a segment of its +// letter, with a one-LED soft edge. + +const W = 64, H = 128; +const { floor, round, sqrt, cos, sin, max, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// Capitals as centre lines in a box 4 wide and 6 tall, y down; each entry +// is a polyline of x, y pairs. +const FONT = { + S: [[4, 1, 3, 0, 1, 0, 0, 1, 0, 2, 1, 3, 3, 3, 4, 4, 4, 5, 3, 6, 1, 6, 0, 5]], + H: [[0, 0, 0, 6], [4, 0, 4, 6], [0, 3, 4, 3]], + U: [[0, 0, 0, 5, 1, 6, 3, 6, 4, 5, 4, 0]], + T: [[0, 0, 4, 0], [2, 0, 2, 6]], + E: [[4, 0, 0, 0, 0, 6, 4, 6], [0, 3, 3, 3]], + R: [[0, 6, 0, 0, 3, 0, 4, 1, 4, 2, 3, 3, 0, 3], [2, 3, 4, 6]], + P: [[0, 6, 0, 0, 3, 0, 4, 1, 4, 2, 3, 3, 0, 3]], + I: [[1, 0, 3, 0], [2, 0, 2, 6], [1, 6, 3, 6]], + N: [[0, 6, 0, 0, 4, 6, 4, 0]], + A: [[0, 6, 0, 2, 2, 0, 4, 2, 4, 6], [0, 4, 4, 4]], + G: [[4, 1, 3, 0, 1, 0, 0, 1, 0, 5, 1, 6, 3, 6, 4, 5, 4, 3, 2, 3]], + D: [[0, 0, 0, 6, 3, 6, 4, 5, 4, 1, 3, 0, 0, 0]], + X: [[0, 0, 4, 6], [4, 0, 0, 6]], +}; + +function segs(word) { + return [...word].map((ch) => { + const out = []; + for (const pl of FONT[ch] || []) for (let i = 0; i + 3 < pl.length; i += 2) out.push([pl[i], pl[i + 1], pl[i + 2], pl[i + 3]]); + return out; + }); +} + +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 - s * f), t = v * (1 - s * (1 - f)); + return [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; +} + +// A set word: letters, scale in LEDs a unit, stroke radius, advance. +function setWord(word, sc, rad, gap) { + const adv = 4 * sc + 2 * rad + gap; + return { g: segs(word), sc, rad, adv, half: (adv * word.length) / 2, top: 3 * sc + rad + 1 }; +} + +// Ink of a set word at (u, v) LEDs from its centre, 0..1. +function ink(w, u, v) { + if (v > w.top || v < -w.top) return 0; + const q = u + w.half; + if (q < 0 || q >= w.half * 2) return 0; + const i = floor(q / w.adv); + const lx = (q - i * w.adv - (w.adv - 4 * w.sc) / 2) / w.sc, ly = v / w.sc + 3; + let best = 1e9; + for (const s of w.g[i]) { + const dx = s[2] - s[0], dy = s[3] - s[1], px = lx - s[0], py = ly - s[1]; + let h = (px * dx + py * dy) / (dx * dx + dy * dy); + h = h < 0 ? 0 : h > 1 ? 1 : h; + const ex = px - dx * h, ey = py - dy * h, d = ex * ex + ey * ey; + if (d < best) best = d; + } + return cl(w.rad + 0.5 - sqrt(best) * w.sc, 0, 1); +} + +// The three words: SHUTTER for the baton, SPIN for each propeller blade +// (from 8 LEDs out), GRAND over PRIX for the race. +const BATON = setWord("SHUTTER", 2.2, 1.9, 2.2); +const BLADE = setWord("SPIN", 2.0, 1.8, 1.8), HUB = 8; +const RACE = [setWord("GRAND", 1.8, 1.8, 1.6), setWord("PRIX", 1.8, 1.8, 1.6)]; +// Turns a second, race word and post speeds in LEDs a second, at Speed 1; +// the race word's repeat and the post spacing in LEDs. +const TURN = 0.2, WORD = 36, POSTS = 48, REPEAT = 100, GAP = 16; + +export function setup(params) { + params.s = { phase: 0, ang: 0, pos: 0, pan: 0, hue: 0, mode: 0, tr: 0.61, speed: 2 }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + S.speed = cl(k[1] == null ? 2 : k[1], 0.2, 3); + S.mode = round(cl(k[2] == null ? 0 : k[2], 0, 2)); + S.tr = 2.5 * sqrt(cl(k[3] == null ? 0.06 : k[3], 0, 1)); + if (S.tr > 1e-3) S.phase = (S.phase + d / S.tr) % 1; + S.ang = (S.ang + d * S.speed * TURN * TAU) % TAU; + S.pos = (S.pos + d * S.speed * WORD) % REPEAT; + S.pan = (S.pan + d * S.speed * POSTS) % GAP; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const tr = S.tr, mode = S.mode, om = S.speed * TURN * TAU; + // How much of the readout to show: none at a global shutter, all from a + // 0.4 s sweep up, so a fast sweep does not flicker. + const k = cl(tr / 0.4, 0, 1); + const scan = floor(S.phase * H); + const post = hsv(S.hue + 0.68, 0.45, 0.22), blades = []; + for (let y = 0; y < H; y++) { + let a = S.phase - y / H; + a -= floor(a); + const age = tr * a; + const col = hsv(S.hue + 0.5 + 0.2 * a * k, 0.65, 1); + const yy = y - 63.5, now = y === scan && k > 0; + // The scene as this row last saw it. + const th = S.ang - om * age, c = cos(th), s = sin(th); + if (mode === 1) for (let j = 0; j < 3; j++) blades[j] = [cos(th + (j * TAU) / 3), sin(th + (j * TAU) / 3)]; + const wx = S.pos - S.speed * WORD * age, px = S.pan - S.speed * POSTS * age; + for (let x = 0; x < W; x++) { + const xx = x - 31.5; + let v = 0, bg = 0; + if (mode === 0) v = ink(BATON, xx * c + yy * s, -xx * s + yy * c); + else if (mode === 1) { + for (let j = 0; j < 3; j++) { + const u = xx * blades[j][0] + yy * blades[j][1]; + if (u > HUB - 3) v = max(v, ink(BLADE, u - HUB - BLADE.half, -xx * blades[j][1] + yy * blades[j][0])); + } + } else { + let u = (xx - wx) % REPEAT; + u = (u < 0 ? u + REPEAT : u) - REPEAT / 2; + v = max(ink(RACE[0], u, yy + 9), ink(RACE[1], u, yy - 9)); + let pp = (x + px) % GAP; + if (pp < 0) pp += GAP; + bg = pp < 2 ? 1 : 0; + } + let r = max(v * col[0], bg * post[0]), g = max(v * col[1], bg * post[1]), b = max(v * col[2], bg * post[2]); + // The row being read: a faint grey line, the ink on it near white. + if (now) { + const w = 0.3 * k + 0.7 * v; + r = max(r, w); + g = max(g, w); + b = max(b, w); + } + display.setPixel(x, y, round(cl(r, 0, 1) * 255), round(cl(g, 0, 1) * 255), round(cl(b, 0, 1) * 255)); + } + } +} From ca40ba6a6bfc58975ae9f6ad067976206a443c91 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 03:52:14 +0000 Subject: [PATCH 3/8] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20100=ED=8C=90:=20Axelro?= =?UTF-8?q?d=20Culture=20=C2=B7=20Roman=20Addition?= 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_01Bc4JkrYAzT3TWt21dU8pXk --- .../pattern-study/patterns/axelrod-culture.js | 223 ++++++++++ .../pattern-study/patterns/roman-addition.js | 420 ++++++++++++++++++ 2 files changed, 643 insertions(+) create mode 100644 incoming/pattern-study/patterns/axelrod-culture.js create mode 100644 incoming/pattern-study/patterns/roman-addition.js diff --git a/incoming/pattern-study/patterns/axelrod-culture.js b/incoming/pattern-study/patterns/axelrod-culture.js new file mode 100644 index 00000000..29541a9a --- /dev/null +++ b/incoming/pattern-study/patterns/axelrod-culture.js @@ -0,0 +1,223 @@ +// ===== Patternflow pattern ===== +// Title: Axelrod Culture +// Family: sim +// Idea: A lattice of 16 by 32 villages, 4 by 4 LEDs each, carries three cultural features per village shown as hue, value and saturation; a random village copies a feature from a random neighbour with a probability equal to the share of features they already have in common, so neighbours that are alike grow alike into regions, a dark line stands wherever two neighbours share nothing, and the map runs into one culture or freezes into a mosaic until drift seeds new traits or a fresh lattice rolls in from the top. +// Author: patternflow-lab +// Date: 2026-11-03 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Axelrod's model of the dissemination of culture (Robert Axelrod, The Dissemination of Culture, Journal of Conflict Resolution, 1997), its transition in the number of traits (Claudio Castellano, Matteo Marsili and Alessandro Vespignani, 2000) and cultural drift (Konstantin Klemm, Víctor M. Eguíluz, Raúl Toral and Maxi San Miguel, 2003); pattern round 100 +// Round: 100 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Traits=2..30, Drift=0..1 +// +// Knob 1: Hue. Turns the band the first feature's traits take on the wheel. At 0 they fall between green and magenta through cyan, blue and violet, never red; at 0.5 the band runs from violet through red and yellow to green. +// Knob 2: Speed. At 2 the lattice is swept 24 times a second, each sweep 512 tries, one per village on average; 2.4 sweeps a second at 0.2 and 36 at 3. The settled map is held 120 sweeps (5 s at 2) before a new one rolls in over 1.25 s. +// Knob 3: Traits. q, the traits each feature can take, 2 to 30; turning it starts a new lattice. At 2 there are 8 cultures and two random neighbours share a feature 7 times in 8, so the opening speckle merges into patches several villages across within 3 s and, without drift, into one culture after a median 2700 sweeps (two minutes at Speed 2). Without drift on this lattice the largest region ends with 81% of the villages at q = 8, 48% at 10 and 12% at 12, and at 30 the map locks within 80 sweeps into about 410 regions, the transition Castellano, Marsili and Vespignani found. +// Knob 4: Drift. A try also redraws one feature of its village with probability 0.02 times Drift squared, and a village that changes flashes white. At the opening 0.06 that is 0.9 redraws a second at Speed 2, enough to thaw a frozen map: over 14,400 sweeps at q = 10 it lifts the largest region from the 48% the map freezes at to 66%, averaged over six runs. From about 0.2 the new traits come faster than regions can swallow them (largest region 28% at q = 10, 13% at 0.3), and at 1 there are 250 redraws a second. + +// The model (Axelrod 1997): every site holds F = 3 features, each one of q +// traits. A try picks a site at random and one of its 2 to 4 neighbours at +// random (the lattice has edges, it does not wrap). If they share k of the +// F features, with 0 < k < F, then with probability k / F the site copies +// one of the features they differ on from the neighbour. Sites that share +// nothing never talk, and neither do sites that share everything, so the +// lattice freezes once every pair of neighbours is one or the other: a map +// of regions, each one culture, whose neighbours share no feature at all. +// +// Colour. The first feature's trait picks a hue in a band half the wheel +// wide, the second the value (0.55 to 1) and nudges the hue up to 0.05 either +// way, the third the saturation (0.3 to 1). Traits are spread by multiples +// of the golden ratio so consecutive traits land far apart at every q. Each +// 4 by 4 block keeps its right column and bottom row for the bond to the +// next site: the site's colour where the two share everything, 80% where +// they share two features, 40% where one, and dark where none, the way +// Axelrod's own figures shade the borders by similarity. The colours and +// lines ease over about 0.1 s so a border sliding along reads as a slide, +// not a flicker. +// +// Drift (Klemm, Eguíluz, Toral and San Miguel 2003) is noise: with rate r a +// try also changes one feature of the chosen site to a random trait. On a +// finite lattice a small r drives the map towards one culture, since a +// frozen border can be bridged by a site that drifts to share a feature with +// both sides; past a rate that falls as the lattice grows the noise wins and +// the map never settles. On these 512 villages that rate is near Drift 0.2. +// +// When at most 12 bonds have stayed active (sharing some features but not +// all) for 120 sweeps, the map counts as settled, and a fresh random lattice +// rolls in from the top over 1.25 s, each village flashing as it lands. + +const W = 64, H = 128, LW = 16, LH = 32, N = LW * LH, F = 3; +const { floor, round, min, max, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const PHI = 0.6180339887; + +// Sweeps a second at Speed 1, drift scale, settle rule, the wipe in seconds. +const RATE = 12, DRIFT = 0.02, CALM_BONDS = 12, CALM_SWEEPS = 120, WIPE = 1.25; +// Line level for 0, 1, 2 and 3 shared features. +const LINE = [0, 0.4, 0.8, 1]; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - s * f), t = v * (1 - s * (1 - f)); + const c = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + out[o] = c[0]; out[o + 1] = c[1]; out[o + 2] = c[2]; +} + +// Neighbours of each site, up to four, open edges. +const NB = []; +for (let i = 0; i < N; i++) { + const x = i % LW, y = floor(i / LW), a = []; + if (x > 0) a.push(i - 1); + if (x < LW - 1) a.push(i + 1); + if (y > 0) a.push(i - LW); + if (y < LH - 1) a.push(i + LW); + NB.push(a); +} + +function randomize(c, q) { + for (let i = 0; i < N * F; i++) c[i] = floor(Math.random() * q); +} + +export function setup(params) { + params.s = { + cult: new Uint8Array(N * F), + next: new Uint8Array(N * F), + reveal: new Float32Array(N), + disp: new Float32Array(N * 3), + line: new Float32Array(N * 2).fill(1), + flash: new Float32Array(N), + target: new Float32Array(N * 3), + q: 2, + hue: 0, + speed: 2, + drift: 0.06, + acc: 0, + calm: 0, + wipe: 0, + started: false, + dark: true, + }; +} + +// Start a fresh random lattice, revealed from the top down with a little jitter. +function restart(S) { + randomize(S.next, S.q); + for (let i = 0; i < N; i++) S.reveal[i] = (0.75 * floor(i / LW)) / LH * WIPE + Math.random() * 0.25 * WIPE; + S.wipe = 1e-6; + S.calm = 0; +} + +// One try of Axelrod's rule, with drift. +function attempt(S) { + const c = S.cult, i = floor(Math.random() * N), nb = NB[i], j = nb[floor(Math.random() * nb.length)]; + const a = i * F, b = j * F; + let k = 0; + for (let f = 0; f < F; f++) if (c[a + f] === c[b + f]) k++; + if (k > 0 && k < F && Math.random() * F < k) { + let pick = floor(Math.random() * (F - k)); + for (let f = 0; f < F; f++) + if (c[a + f] !== c[b + f]) { + if (pick === 0) { c[a + f] = c[b + f]; break; } + pick--; + } + } + if (S.rate > 0 && Math.random() < S.rate) { + const f = floor(Math.random() * F), t = floor(Math.random() * S.q); + if (c[a + f] !== t) { c[a + f] = t; S.flash[i] = 1; } + } +} + +// Bonds that share some features but not all. +function active(S) { + const c = S.cult; + let n = 0; + for (let i = 0; i < N; i++) + for (const j of [i + 1, i + LW]) { + if ((j === i + 1 && i % LW === LW - 1) || j >= N) continue; + let k = 0; + for (let f = 0; f < F; f++) if (c[i * F + f] === c[j * F + f]) k++; + if (k > 0 && k < F) n++; + } + return n; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 2, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0] || 0; + S.speed = cl(kv[1] == null ? 2 : kv[1], 0.2, 3); + const q = round(cl(kv[2] == null ? 2 : kv[2], 2, 30)); + S.drift = cl(kv[3] == null ? 0.06 : kv[3], 0, 1); + S.rate = DRIFT * S.drift * S.drift; + if (!S.started || q !== S.q) { + // A new q starts a new lattice; traits above q would never come back. + S.q = q; + S.started = true; + restart(S); + } + const sweeps = d * S.speed * RATE; + if (S.wipe > 0) { + S.wipe += d; + for (let i = 0; i < N; i++) + if (S.reveal[i] >= 0 && S.wipe >= S.reveal[i]) { + for (let f = 0; f < F; f++) S.cult[i * F + f] = S.next[i * F + f]; + S.reveal[i] = -1; + S.flash[i] = 0.6; + } + if (S.wipe >= WIPE) { S.wipe = 0; S.dark = false; } + } else { + S.acc += sweeps * N; + let n = min(floor(S.acc), 4 * N); + S.acc -= floor(S.acc); + while (n-- > 0) attempt(S); + if (active(S) <= CALM_BONDS) { + S.calm += sweeps; + if (S.calm >= CALM_SWEEPS) restart(S); + } else S.calm = 0; + } + // Ease the shown colours and lines towards the lattice. + const c = S.cult, tg = S.target, k = 1 - exp(-d / 0.1), fk = exp(-d / 0.35); + for (let i = 0; i < N; i++) { + const a = i * F; + const l1 = (c[a + 1] * PHI + 0.2) % 1; + const h = S.hue + 0.33 + 0.5 * ((c[a] * PHI + 0.05) % 1) + 0.1 * (l1 - 0.5); + const v = S.dark && S.reveal[i] >= 0 ? 0 : 0.55 + 0.45 * l1; + const s = 0.3 + 0.7 * ((c[a + 2] * PHI + 0.6) % 1); + hsv(h, s, v, tg, i * 3); + for (let m = 0; m < 3; m++) S.disp[i * 3 + m] += (tg[i * 3 + m] - S.disp[i * 3 + m]) * k; + for (let e = 0; e < 2; e++) { + const j = e === 0 ? i + 1 : i + LW; + let lv = 1; + if (!(e === 0 && i % LW === LW - 1) && j < N) { + let sh = 0; + for (let f = 0; f < F; f++) if (c[a + f] === c[j * F + f]) sh++; + lv = LINE[sh]; + } + S.line[i * 2 + e] += (lv - S.line[i * 2 + e]) * k; + } + S.flash[i] *= fk; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const D = S.disp, L = S.line, FL = S.flash; + for (let y = 0; y < H; y++) { + const sy = y >> 2, ry = y & 3; + for (let x = 0; x < W; x++) { + const sx = x >> 2, rx = x & 3, i = sy * LW + sx; + let lv = 1; + if (rx === 3 && ry === 3) { + lv = min(L[i * 2], L[i * 2 + 1]); + if (sx < LW - 1) lv = min(lv, L[(i + 1) * 2 + 1]); + if (sy < LH - 1) lv = min(lv, L[(i + LW) * 2]); + } else if (rx === 3) lv = L[i * 2]; + else if (ry === 3) lv = L[i * 2 + 1]; + const fl = rx < 3 && ry < 3 ? FL[i] : FL[i] * 0.4; + const r = (D[i * 3] * lv + fl) * 255, g = (D[i * 3 + 1] * lv + fl) * 255, b = (D[i * 3 + 2] * lv + fl) * 255; + display.setPixel(x, y, min(255, round(r)), min(255, round(g)), min(255, round(b))); + } + } +} diff --git a/incoming/pattern-study/patterns/roman-addition.js b/incoming/pattern-study/patterns/roman-addition.js new file mode 100644 index 00000000..6a39c3b0 --- /dev/null +++ b/incoming/pattern-study/patterns/roman-addition.js @@ -0,0 +1,420 @@ +// ===== Patternflow pattern ===== +// Title: Roman Addition +// Family: type +// Idea: A running sum is worked in Roman numerals down a slate the way they add without place value: the two terms are written, each subtractive pair glows and the symbols it stands for burst out of it (IV sends out IIII, CM sends out DCCCC) as every symbol drops into one line sorted from M down to I, then line after line five I, X or C or two V, L or D slide together and fuse with a flash into the next symbol up, and the result is folded back into subtractive form under a double rule and rises to the top to be the next first term. +// Author: patternflow-lab +// Date: 2026-11-03 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: adding Roman numerals by merging, sorting and carrying fives and twos, the written trace of the Roman counting board (Karl Menninger, Zahlwort und Ziffer, 1934; Georges Ifrah, Histoire universelle des chiffres, 1981); capitals 7 LEDs tall with 2-LED stems drawn for this pattern; pattern round 100 +// Round: 100 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Step=0..999, Notation=0..1 +// +// Knob 1: Hue. Turns the symbol colours round the wheel together. At 0 the units and fives (I, V) are aqua, the tens (X, L) azure, the hundreds (C, D) violet and the thousands (M) pink, 0.12 of a turn apart, with V, L and D paler than I, X and C; signs and rules are a pale grey of the same hue. +// Knob 2: Speed. At 2 the second term is written at 12.5 symbols a second, the merge drops a symbol every 0.06 s, each fusion line takes 1 s and the result is held 1.6 s. The opening sum, 1994 + 47, has 22 symbols to merge and six fusions and is worked, held and lifted in 14.1 s; every time goes as 2 / Speed. +// Knob 3: Step. What is added each beat. At 0 every beat adds a fresh term from 1 to 1500, small ones more often (1500 times the product of two uniform draws); any other value adds that number again and again, 250 as CCL, 999 as CMXCIX. When the total would pass 3999, the largest number the subtractive form writes without a bar, the slate fades and a new total starts below 100. +// Knob 4: Notation. Below 0.5 the terms and the result are written in the subtractive form (IV, IX, XL, XC, CD, CM): each pair glows for 0.7 s before the merge and its additive symbols burst out of it, and the result is folded back with a flash. From 0.5 up everything is written in the older additive form (IIII, VIIII, XXXX, DCCCC), the way many Roman inscriptions and most clock faces write four, and the slate goes straight to the merge. + +// Roman numerals carry no place value: MDCCCCLXXXXIIII is a pile of +// counters, one M, one D, four C and so on, and its value is their sum. To +// add two of them you pile the counters together, which is the whole of +// Roman addition, and then tidy the pile: five I make a V, two V an X, five +// X an L, two L a C, five C a D and two D an M. Menninger (1934) takes the +// numerals as the written trace of reckoning with counters, I, X, C and M on +// the lines of the board and V, L and D in the spaces between, where only +// the result was written down; the tidying is the board's carry. The +// subtractive pairs IV, IX, XL, XC, CD and CM are a writing habit on top of +// that, settled in the Middle Ages, and have to be unfolded before the +// piles are merged. +// +// The slate keeps every line: the first term, + and the second, a rule, the +// merged line sorted by value, one new line for each fusion, taken from the +// small end because a fusion of I can make the V that completes a pair, a +// double rule, and = with the result. A term below 4000 holds at most four +// I, so the two together hold at most eight and there is never more than +// one group to fuse at a level. Lines wrap after 60 LEDs with one blank row +// under a wrapped row and 4 to 10 under a line, as many as fill the panel; +// a slate still too tall scrolls with the writing. +// +// Each symbol on the slate is a sprite with a time to appear, a move and a +// time to fade, and the whole sum is laid out as soon as it starts, so a +// frame only evaluates the sprites at the slate's own clock. + +const W = 64, H = 128; +const { floor, round, min, max, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ramp = (t, a, b) => (t <= a ? 0 : t >= b ? 1 : (t - a) / (b - a)); +const ease = (u) => u * u * (3 - 2 * u); + +// I V X L C D M, then + and =: 7 rows, stems 2 LEDs wide. +const ROWS = [ + ["##", "##", "##", "##", "##", "##", "##"], + ["##.##", "##.##", "##.##", "##.##", ".###.", ".###.", "..#.."], + ["#...#", "##.##", ".###.", "..#..", ".###.", "##.##", "#...#"], + ["##..", "##..", "##..", "##..", "##..", "##..", "####"], + [".###", "##..", "##..", "##..", "##..", "##..", ".###"], + ["####.", "##.##", "##.##", "##.##", "##.##", "##.##", "####."], + ["##...##", "###.###", "##.#.##", "##.#.##", "##...##", "##...##", "##...##"], + ["...", "...", ".#.", "###", ".#.", "...", "..."], + ["...", "...", "###", "...", "###", "...", "..."], +]; +const PLUS = 7, EQ = 8; +const GW = ROWS.map((r) => r[0].length); +const GLYPH = ROWS.map((rows) => { + const px = []; + rows.forEach((row, y) => [...row].forEach((c, x) => c === "#" && px.push(x, y))); + return px; +}); + +// Row pitch inside a line, the left edge of the numerals, the hue step a decade. +const RP = 8, X0 = 4, DEC = 0.12, MAXV = 3999; +// Brightness of a line once the next one is written, and of the terms. +const DIM = 0.5, DIMT = 0.62; + +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 - s * f), t = v * (1 - s * (1 - f)); + return [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; +} + +// A number as symbols, each with the decade it was written for. +function numeral(n, sub) { + const out = []; + const ds = [floor(n / 1000), floor(n / 100) % 10, floor(n / 10) % 10, n % 10]; + for (let k = 0; k < 4; k++) { + const dec = 3 - k, one = 2 * dec, five = one + 1, ten = one + 2; + let d = ds[k]; + if (dec === 3) { for (let i = 0; i < d; i++) out.push({ s: 6, dec }); continue; } + if (sub && d === 4) { out.push({ s: one, dec }, { s: five, dec }); continue; } + if (sub && d === 9) { out.push({ s: one, dec }, { s: ten, dec }); continue; } + if (d >= 5) { out.push({ s: five, dec }); d -= 5; } + for (let i = 0; i < d; i++) out.push({ s: one, dec }); + } + return out; +} + +// Positions in a line: x from X0, wrapping to a new text row past column 63. +function layout(toks) { + let x = X0, r = 0; + for (const t of toks) { + const w = GW[t.s]; + if (x + w > W) { x = X0; r++; } + t.lx = x; t.lr = r; + x += w + 1; + } + return r + 1; +} + +const copy = (toks) => toks.map((t) => ({ s: t.s, dec: t.dec })); + +// Lay out one whole sum: lines, sprites, rules and the view's waypoints. +function build(a, b, sub, fresh) { + const sp = [], rules = [], view = []; + const aS = numeral(a, sub), aA = numeral(a, false), bS = numeral(b, sub), bA = numeral(b, false); + const rA = layout(aS), rB = layout(bS); + // The merged line, sorted by value, each symbol remembering where it came from. + const merged = [...aA.map((t) => ({ s: t.s, src: t, y: 0 })), ...bA.map((t) => ({ s: t.s, src: t, y: 1 }))]; + merged.sort((p, q) => q.s - p.s); + const lines = [merged]; + const steps = []; + for (let s = 0; s < 6; s++) { + const need = s % 2 ? 2 : 5; + for (;;) { + const prev = lines[lines.length - 1]; + const idx = []; + prev.forEach((t, i) => t.s === s && idx.length < need && idx.push(i)); + if (idx.length < need) break; + const next = [], map = []; + let at = -1; + prev.forEach((t, i) => { + if (idx.includes(i)) return; + if (at < 0 && t.s < s + 1) { at = next.length; next.push({ s: s + 1 }); } + map[i] = next.length; + next.push({ s: t.s }); + }); + if (at < 0) { at = next.length; next.push({ s: s + 1 }); } + // The five fusing symbols land on the new symbol's slot. + idx.forEach((i) => (map[i] = -1 - at)); + lines.push(next); + steps.push({ map, at }); + } + } + const last = lines[lines.length - 1]; + const res = numeral(a + b, sub); + const lineRows = lines.map((l) => layout(l)); + const rR = max(layout(res), layout(last.map((t) => ({ s: t.s })))); + // Line heights, then the leading between lines: as much as fills the + // panel, from 4 to 10 LEDs, with 2 more round a rule and 4 round the double. + const hs = [rA, rB, ...lineRows, rR].map((r) => (r - 1) * RP + 7); + const plain = lineRows.length; + const sumH = hs.reduce((p, q) => p + q, 0); + const lg = cl(floor((H - 4 - sumH - 6) / (plain + 2)), 4, 10); + let y = 0; + const yA0 = y; y += hs[0] + lg; + const yB0 = y; y += hs[1]; + const rule10 = y + floor((lg + 2) / 2); y += lg + 2; + const yL0 = lines.map((l, i) => { const v = y; y += hs[2 + i] + (i < lines.length - 1 ? lg : 0); return v; }); + const rule20 = y + floor((lg + 4) / 2) - 1; y += lg + 4; + const yR0 = y; y += hs[hs.length - 1]; + // Centre the slate; one too tall starts at the top and scrolls. + const y0 = max(2, floor((H - y) / 2)); + const yA = yA0 + y0, yB = yB0 + y0, rule1 = rule10 + y0, rule2 = rule20 + y0, yR = yR0 + y0; + const yL = yL0.map((v) => v + y0); + const bottom = y + y0, maxOff = max(0, bottom + 2 - H); + const off = (yb) => cl(yb + 2 - H, 0, maxOff); + const pos = (t, yy) => [t.lx, yy + t.lr * RP]; + const add = (o) => { const s = Object.assign({ m0: 0, m1: 0, a0: -1, a1: -1, d0: 1e9, d1: 1e9, f: -1, g0: -1, dimAt: 1e9, dim: DIM }, o); sp.push(s); return s; }; + + // The first term: there already, or faded in on a fresh slate. + layout(aS); + const spA = aS.map((t) => add({ g: t.s, x0: t.lx, y0: yA + t.lr * RP, x1: t.lx, y1: yA + t.lr * RP, a0: fresh ? 0 : -1, a1: fresh ? 0.4 : -1, dim: DIMT })); + let tt = 0.35; + // + and the second term, symbol by symbol. + layout(bS); + add({ g: PLUS, x0: 0, y0: yB, x1: 0, y1: yB, a0: tt, a1: tt + 0.15, sign: 1, dim: DIMT }); + tt += 0.15; + view.push([tt, off(yB + (rB - 1) * RP + 7)]); + const spB = bS.map((t, k) => add({ g: t.s, x0: t.lx, y0: yB + t.lr * RP, x1: t.lx, y1: yB + t.lr * RP, a0: tt + 0.08 * k, a1: tt + 0.08 * k + 0.12, dim: DIMT })); + tt += 0.08 * bS.length + 0.25; + rules.push({ y: rule1, a0: tt - 0.15, a1: tt + 0.25 }); + + // Each additive symbol starts from the written one it copies, or, where a + // decade holds a subtractive pair, bursts out of the middle of the pair, + // which glows first: IV sends out IIII, CM sends out DCCCC. + const tU = tt + 0.1; + let pairs = false; + const sources = (subT, subSp, addT) => { + for (let dec = 0; dec < 4; dec++) { + const o = [], n = []; + subT.forEach((t, i) => t.dec === dec && o.push(i)); + addT.forEach((t, i) => t.dec === dec && n.push(i)); + if (o.length === n.length) { + n.forEach((i, k) => { addT[i].fx = subSp[o[k]].x0; addT[i].fy = subSp[o[k]].y0; addT[i].burst = false; }); + continue; + } + pairs = true; + const cx = o.reduce((p, i) => p + subSp[i].x0 + GW[subT[i].s] / 2, 0) / o.length; + o.forEach((i) => (subSp[i].g0 = tU)); + n.forEach((i) => { addT[i].fx = cx - GW[addT[i].s] / 2; addT[i].fy = subSp[o[0]].y0; addT[i].burst = true; }); + } + }; + sources(aS, spA, aA); + sources(bS, spB, bA); + tt = pairs ? tU + 0.7 : tt + 0.1; + + // Merge: a copy of every symbol drops into the sorted line. + const tM = tt; + for (const s of [...spA, ...spB]) s.dimAt = min(s.dimAt, tM + 0.3); + view.push([tM, off(yL[0] + (lineRows[0] - 1) * RP + 7)]); + layout(merged); + const n0 = merged.length; + let prevSp = merged.map((t, k) => { + const to = pos(t, yL[0]), m0 = tM + 0.06 * k; + return add({ g: t.s, x0: t.src.fx, y0: t.src.fy, x1: to[0], y1: to[1], m0, m1: m0 + 0.7, a0: m0, a1: m0 + 0.08, f: t.src.burst ? m0 : -1 }); + }); + tt = tM + 0.06 * n0 + 0.85; + + // One line for each fusion. + steps.forEach((st, j) => { + const prev = lines[j], next = lines[j + 1], yp = yL[j], yn = yL[j + 1]; + layout(prev); layout(next); + const tc = tt; + prevSp.forEach((s) => (s.dimAt = min(s.dimAt, tc + 0.3))); + view.push([tc, off(yn + (lineRows[j + 1] - 1) * RP + 7)]); + const nextSp = new Array(next.length); + const slot = pos(next[st.at], yn); + prev.forEach((t, i) => { + const from = pos(t, yp), k = st.map[i]; + if (k >= 0) { + const to = pos(next[k], yn); + nextSp[k] = add({ g: t.s, x0: from[0], y0: from[1], x1: to[0], y1: to[1], m0: tc, m1: tc + 0.6, a0: tc, a1: tc }); + } else { + add({ g: t.s, x0: from[0], y0: from[1], x1: slot[0] + (GW[st.at >= 0 ? next[st.at].s : 0] - GW[t.s]) / 2, y1: slot[1], m0: tc, m1: tc + 0.5, a0: tc, a1: tc, d0: tc + 0.4, d1: tc + 0.55 }); + } + }); + nextSp[st.at] = add({ g: next[st.at].s, x0: slot[0], y0: slot[1], x1: slot[0], y1: slot[1], a0: tc + 0.48, a1: tc + 0.55, f: tc + 0.5 }); + prevSp = nextSp; + tt += 1.0; + }); + + // The result, folded back into subtractive form a decade at a time. + const tR = tt; + prevSp.forEach((s) => (s.dimAt = min(s.dimAt, tR + 0.4))); + rules.push({ y: rule2, a0: tR, a1: tR + 0.3 }, { y: rule2 + 2, a0: tR + 0.05, a1: tR + 0.35 }); + view.push([tR, off(yR + (rR - 1) * RP + 7)]); + const resSp = [add({ g: EQ, x0: 0, y0: yR, x1: 0, y1: yR, a0: tR + 0.1, a1: tR + 0.3, sign: 1, dim: 1, res: 1 })]; + const lastT = last.map((t) => ({ s: t.s, dec: t.s >> 1 })); + layout(lastT); layout(res); + const yLast = yL[lines.length - 1]; + let folded = false; + for (let dec = 0; dec < 4; dec++) { + const o = [], n = []; + lastT.forEach((t, i) => t.dec === dec && o.push(i)); + res.forEach((t, i) => t.dec === dec && n.push(i)); + const same = o.length === n.length && o.every((i, k) => lastT[i].s === res[n[k]].s); + if (same) { + o.forEach((i, k) => { + const from = pos(lastT[i], yLast), to = pos(res[n[k]], yR); + resSp.push(add({ g: lastT[i].s, x0: from[0], y0: from[1], x1: to[0], y1: to[1], m0: tR + 0.2, m1: tR + 0.8, a0: tR + 0.2, a1: tR + 0.2, dim: 1, res: 1 })); + }); + continue; + } + folded = true; + const ocx = o.reduce((p, i) => p + lastT[i].lx + GW[lastT[i].s] / 2, 0) / o.length; + const ncx = n.reduce((p, i) => p + res[i].lx + GW[res[i].s] / 2, 0) / n.length; + o.forEach((i) => { + const from = pos(lastT[i], yLast); + add({ g: lastT[i].s, x0: from[0], y0: from[1], x1: ncx - GW[lastT[i].s] / 2, y1: yR + res[n[0]].lr * RP, m0: tR + 0.2, m1: tR + 0.7, a0: tR + 0.2, a1: tR + 0.2, d0: tR + 0.55, d1: tR + 0.7 }); + }); + n.forEach((i) => { + const to = pos(res[i], yR); + resSp.push(add({ g: res[i].s, x0: ncx - GW[res[i].s] / 2, y0: to[1], x1: to[0], y1: to[1], m0: tR + 0.6, m1: tR + 0.95, a0: tR + 0.6, a1: tR + 0.7, f: tR + 0.65, dim: 1, res: 1 })); + }); + } + tt = tR + (folded ? 1.05 : 0.9); + const glint = tt + 0.2; + const rise = tt + 1.6; + return { sp, rules, view, resSp, yA, yR, open: tU, rise, glint, end: rise + 1.3, a, b, total: a + b }; +} + +// The term each beat adds: the knob's number, or at 0 a fresh one, small +// ones more often. +const nextStep = (S) => (S.step > 0 ? S.step : 1 + floor(Math.random() * Math.random() * 1500)); + +function newSum(S, a, b, fresh) { + S.sum = build(a, b, S.sub, fresh); + S.t = 0; + S.lifted = false; +} + +// A wiped slate starts a new total below 100. +function wipe(S) { + let b = nextStep(S); + const a = 1 + floor(Math.random() * 99); + if (a + b > MAXV) b = MAXV - a; + newSum(S, a, b, true); +} + +export function setup(params) { + params.s = { t: 0, hue: 0, speed: 2, step: 0, sub: true, sum: null, buf: new Float32Array(W * H * 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.hue = k[0] || 0; + S.speed = cl(k[1] == null ? 2 : k[1], 0.2, 3); + S.step = round(cl(k[2] || 0, 0, 999)); + S.sub = (k[3] == null ? 0.06 : k[3]) < 0.5; + // The first slate opens on 1994 + 47 (or the knob's step), both terms + // written and the pairs about to glow. + if (!S.sum) { + newSum(S, 1994, S.step > 0 ? S.step : 47, true); + S.t = S.sum.open - 0.2; + } + S.t += (d * S.speed) / 2; + const sum = S.sum; + // At the lift the next sum is laid out, so the result knows where to rise to. + if (!S.lifted && S.t >= sum.rise) { + S.lifted = true; + const nextA = sum.total, b = nextStep(S); + if (nextA + b > MAXV) { + for (const s of sum.resSp) { s.d0 = sum.rise; s.d1 = sum.rise + 0.5; } + sum.next = null; + } else { + const now = S.t; + newSum(S, nextA, b, false); + sum.next = S.sum; + S.sum = sum; + S.t = now; + S.lifted = true; + const cur = viewAt(sum, sum.rise), ty = sum.next.yA; + for (const s of sum.resSp) { + if (s.sign) { s.d0 = sum.rise; s.d1 = sum.rise + 0.4; continue; } + s.x0 = s.x1; s.y0 = s.y1; s.y1 = ty + cur + (s.y0 - sum.yR); + s.m0 = sum.rise + 0.25; s.m1 = sum.rise + 1.15; + } + } + for (const s of sum.sp) if (!s.res) { s.d0 = min(s.d0, sum.rise); s.d1 = min(s.d1, sum.rise + 0.5); } + for (const r of sum.rules) r.d0 = sum.rise; + } + if (S.t >= sum.end) { + if (sum.next) { S.sum = sum.next; S.t -= sum.end; S.lifted = false; } + else wipe(S); + } +} + +// The view's scroll at time t: eased from waypoint to waypoint. +function viewAt(sum, t) { + let v = 0; + for (const [t0, o] of sum.view) { + if (t < t0) break; + v += (o - v) * ease(ramp(t, t0, t0 + 0.6)); + } + return v; +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.sum) return; + const sum = S.sum, t = S.t, buf = S.buf; + buf.fill(0); + const off = viewAt(sum, t); + const cols = []; + for (let s = 0; s < 7; s++) cols.push(hsv(S.hue + 0.48 + DEC * (s >> 1), s % 2 ? 0.5 : 0.88, 1)); + const grey = hsv(S.hue + 0.48, 0.12, 0.85); + const plot = (x, y, r, g, b) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const i = (y * W + x) * 3; + buf[i] += r; buf[i + 1] += g; buf[i + 2] += b; + }; + // Rules: drawn from the left, faded at the lift. + for (const r of sum.rules) { + const u = ease(ramp(t, r.a0, r.a1)), fade = 1 - ramp(t, r.d0 || 1e9, (r.d0 || 1e9) + 0.5); + const yy = round(r.y - off), xe = round(60 * u); + const rl = 255 * 0.45 * fade; + for (let x = 0; x < xe; x++) plot(x + 2, yy, grey[0] * rl, grey[1] * rl, grey[2] * rl); + } + const gl = sum.glint; + for (const s of sum.sp) { + const al = (s.a0 < 0 ? 1 : ramp(t, s.a0, s.a1 + 1e-6)) * (1 - ramp(t, s.d0, s.d1 + 1e-6)); + if (al <= 0) continue; + const u = s.m1 > s.m0 ? ease(ramp(t, s.m0, s.m1)) : t >= s.m0 ? 1 : 0; + const x = s.x0 + (s.x1 - s.x0) * u, y = s.y0 + (s.y1 - s.y0) * u - off; + const lv = 1 + (s.dim - 1) * ramp(t, s.dimAt, s.dimAt + 0.4); + let fl = s.f >= 0 && t >= s.f ? 1.2 * exp(-(t - s.f) / 0.22) : 0; + if (s.g0 >= 0) fl += 0.8 * ramp(t, s.g0, s.g0 + 0.25) * (1 - ramp(t, s.g0 + 0.7, s.g0 + 1.1)); + if (s.res && t > gl && t < gl + 1.2) { + const gx = -6 + 76 * (t - gl) / 1.2, dx = (x + GW[s.g] / 2 - gx) / 4; + fl += 0.7 * exp(-dx * dx); + } + const c = s.sign ? grey : cols[s.g]; + const r = (c[0] * lv + fl) * al * 255, g = (c[1] * lv + fl) * al * 255, b = (c[2] * lv + fl) * al * 255; + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + const w00 = (1 - fx) * (1 - fy), w10 = fx * (1 - fy), w01 = (1 - fx) * fy, w11 = fx * fy; + const px = GLYPH[s.g]; + for (let p = 0; p < px.length; p += 2) { + const gx = ix + px[p], gy = iy + px[p + 1]; + plot(gx, gy, r * w00, g * w00, b * w00); + if (w10 > 0.004) plot(gx + 1, gy, r * w10, g * w10, b * w10); + if (w01 > 0.004) plot(gx, gy + 1, r * w01, g * w01, b * w01); + if (w11 > 0.004) plot(gx + 1, gy + 1, r * w11, g * w11, b * w11); + } + // A fusion glows round the new symbol. + if (fl > 0.05 && s.f >= 0) { + const hr = c[0] * fl * al * 70, hg = c[1] * fl * al * 70, hb = c[2] * fl * al * 70; + const cx = round(x + (GW[s.g] - 1) / 2), cy = round(y + 3); + for (let dy = -4; dy <= 4; dy++) + for (let dx = -4; dx <= 4; dx++) { + const d2 = dx * dx + dy * dy; + if (d2 > 0 && d2 <= 16) plot(cx + dx, cy + dy, hr / d2, hg / d2, hb / d2); + } + } + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = (y * W + x) * 3; + display.setPixel(x, y, min(255, round(buf[i])), min(255, round(buf[i + 1])), min(255, round(buf[i + 2]))); + } +} From 0ddd2fb5f236e84b9a837b00bc3750b88a16820d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 03:55:52 +0000 Subject: [PATCH 4/8] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20100=ED=8C=90:=20Tempo?= =?UTF-8?q?=20Canon=20=C2=B7=20Runestone?= 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_01Bc4JkrYAzT3TWt21dU8pXk --- incoming/pattern-study/patterns/runestone.js | 381 ++++++++++++++++++ .../pattern-study/patterns/tempo-canon.js | 257 ++++++++++++ 2 files changed, 638 insertions(+) create mode 100644 incoming/pattern-study/patterns/runestone.js create mode 100644 incoming/pattern-study/patterns/tempo-canon.js diff --git a/incoming/pattern-study/patterns/runestone.js b/incoming/pattern-study/patterns/runestone.js new file mode 100644 index 00000000..0b39a231 --- /dev/null +++ b/incoming/pattern-study/patterns/runestone.js @@ -0,0 +1,381 @@ +// ===== Patternflow pattern ===== +// Title: Runestone +// Family: type +// Idea: A serpent band 16 LEDs wide winds down the whole stone in U-turns from its head at the top left to its tail at the bottom, and Harald Bluetooth's inscription from the larger Jelling stone, in Younger Futhark runes cut across the band, slides along it toward the head, each rune carved stroke by stroke by a white spark where it enters at the tail and then painted, while the serpent swallows the oldest words at its jaws. +// Author: patternflow-lab +// Date: 2026-11-03 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the larger Jelling stone (DR 42, raised by Harald Bluetooth about 965), side A to C in its standard transliteration (Samnordisk runtextdatabas); Younger Futhark long-branch and short-twig runes drawn as strokes after the reference glyphs of the Unicode Runic block; the serpent band after the Uppland stones of the eleventh century; pattern round 100 +// Round: 100 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Turns=3..6, Twig=0..1 +// +// Knob 1: Hue. Turns the paint round the wheel. At 0 the runes and the band's two cut edges are painted yellow ochre on a deep blue serpent; at 0.87 the runes are red ochre on teal, at 0.5 blue on brown. +// Knob 2: Speed. The text slides 2.5 LEDs a second along the band for each unit: 5 at 2, a rune every 1.5 s, so the 153 signs of the inscription come round in 3.5 minutes; 0.5 a second at 0.2 and 7.5 at 3. +// Knob 3: Turns. The U-turns the band makes on its way down, 3 to 6. At 3 the turns are 36 rows across and the band holds about 30 signs; at 6 they are 18 rows across, the straight runs longer, and it holds about 52. +// Knob 4: Twig. Morphs every rune from the long-branch form of the Jelling stone (0 to 0.1) to the short-twig form of the Swedish and Norwegian stones (0.9 to 1): the crossing branches of n, a and ą shrink to one-sided twigs, h and m to a knot on the staff, s and R to half a staff, t loses a branch and the two bows of b open into two twigs, and the signs close up from 7.6 to 6.2 LEDs apart, so about 36 signs fit the band at 3 Turns instead of 30. f, u, þ, r, k, i and l are the same in both. + +// The band is a path: a straight neck from the head, then n + 1 level runs +// joined by n half circles of radius b, alternately on the right and the +// left, where b = 54 / n rows, then a tapering tail. Every LED near the path +// is given the arc length u of its nearest point on the path and its signed +// distance v from it (positive to the left of the direction of reading), and +// the text is set in that frame: a rune's staff runs across the band and the +// text runs along it, so on the leftward runs the runes stand on their heads, +// as on the stones, where the reader walks round. The map is rebuilt only +// when Turns changes. +// +// Runes are drawn from straight strokes in a 4 by 8 box, 4.25 by 8.5 LEDs +// on the panel (3.6 wide in the short-twig form), each stroke 1.9 LEDs +// thick, with 1.4 LEDs of dark band between a rune and the painted edges, so +// a rune reads on the LEDs at any angle the band takes. Every rune has the same five strokes in both forms, +// some of them shrunk to a point, so Twig moves each end in a straight line. +// +// The text slides toward the head. Where a sign enters the band at the tail +// end it is carved: a spark runs along its strokes in order, leaving pale +// fresh-cut stone, in the time the sign takes to move one place, and in the +// next two places the paint comes up in it. At the neck the signs go into +// the serpent's open jaws. A two-dot divider follows every word. + +const W = 64, H = 128; +const { floor, round, min, max, abs, sin, cos, exp, hypot, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// DR 42, the larger Jelling stone: "King Harald ordered this monument made +// in memory of Gorm, his father, and in memory of Thyre, his mother; that +// Harald who won for himself all of Denmark and Norway and made the Danes +// Christian." Read as one line across its three sides. +const TEXT = + "haraltr kunukR baþ kaurua kubl þausi aft kurm faþur sin auk aft þąurui muþur sina sa haraltr ias sąR uan tanmaurk ala auk nuruiak auk tani karþi kristną"; + +// Each rune as five strokes [x0, y0, x1, y1] in a 4 by 8 box (y down), the +// long-branch form first and then the short-twig form; a stroke with both +// ends on one point is a stroke that is not there in that form. +const P = (x, y) => [x, y, x, y]; +const RUNES = { + f: [[[0.6, 0, 0.6, 8], [0.6, 2.4, 3.2, 0], [0.6, 5.6, 3.8, 2.4]]], + u: [[[0.6, 8, 0.6, 0], [0.6, 0, 3.6, 3], [3.6, 3, 3.6, 8]]], + þ: [[[0.6, 0, 0.6, 8], [0.6, 1.8, 3.5, 4], [3.5, 4, 0.6, 6.2]]], + ą: [[[2, 0, 2, 8], [0, 0.8, 4, 3.6], [0, 4, 4, 6.8]], [[1.4, 0, 1.4, 8], [1.4, 2, 3.6, 3.8], [1.4, 5, 3.6, 6.8]]], + r: [[[0.6, 0, 0.6, 8], [0.6, 0, 3.4, 2.1], [3.4, 2.1, 0.6, 4.2], [0.6, 4.2, 3.6, 8]]], + k: [[[0.6, 0, 0.6, 8], [0.6, 3.6, 3.6, 0.2]]], + h: [[[2, 0, 2, 8], [0, 1.8, 4, 6.2], [4, 1.8, 0, 6.2]], [[2, 0, 2, 8], [1, 3.4, 3, 4.6], [3, 3.4, 1, 4.6]]], + n: [[[2, 0, 2, 8], [0, 2, 4, 6]], [[2, 0, 2, 8], [2, 3, 3.9, 5]]], + i: [[[2, 0, 2, 8]]], + a: [[[2, 0, 2, 8], [4, 2, 0, 6]], [[2, 0, 2, 8], [2, 3, 0.1, 5]]], + s: [[[0.4, 0, 0.4, 4.8], [0.4, 4.8, 3.6, 2.6], [3.6, 2.6, 3.6, 8]], [[2, 0, 2, 3.8], P(2, 3.8), P(2, 3.8)]], + t: [[[2, 0, 2, 8], [0, 2.6, 2, 0], [2, 0, 4, 2.6]], [[2.4, 0, 2.4, 8], [0.2, 2.6, 2.4, 0], P(2.4, 0)]], + b: [ + [[0.6, 0, 0.6, 8], [0.6, 0, 3.4, 2], [3.4, 2, 0.6, 4], [0.6, 4, 3.4, 6], [3.4, 6, 0.6, 8]], + [[0.6, 0, 0.6, 8], P(0.6, 3.2), [2.9, 0.9, 0.6, 3.2], P(0.6, 6.4), [2.9, 4.1, 0.6, 6.4]], + ], + m: [[[2, 0, 2, 8], [0, 0, 2, 2.8], [4, 0, 2, 2.8]], [[2, 0, 2, 8], [1.1, 0.3, 2, 1.3], [2.9, 0.3, 2, 1.3]]], + l: [[[0.6, 0, 0.6, 8], [0.6, 0, 3.6, 3]]], + R: [[[2, 0, 2, 8], [2, 5.2, 0, 8], [2, 5.2, 4, 8]], [[2, 4.2, 2, 8], P(2, 8), P(2, 8)]], +}; +const STROKES = 5; + +// Box to LEDs across the band, and along it at the long-branch and the +// short-twig end of Twig; the stroke radius in LEDs; a rune's advance along +// the band in LEDs at the two ends; a divider's advance as a share of a +// rune's; the divider's dot radius. +const SC = 1.0625, SCX0 = 1.0625, SCX1 = 0.9, RAD = 0.95, ADV0 = 7.6, ADV1 = 6.2, DIVU = 0.55, DOT = 1.15; +// The band: half width, the cut edge's width, the rows of the top and +// bottom runs, the head's and tail's lengths along the path. +const HW = 8, EDGE = 1.4, TOP = 11, BOTTOM = 119, HEAD = 15, TAIL = 15, DS = 0.25; +// LEDs a second for each unit of Speed, and how many places a sign moves +// while it is carved and while it is painted. +const RATE = 2.5, CARVE = 1, PAINT = 2; + +// Glyph table: 16 runes and the divider; each holds both forms' strokes. +const KEYS = Object.keys(RUNES); +const FORMS = KEYS.map((key) => { + const [L, Sh = L] = RUNES[key]; + const pad = (list) => { + const out = list.map((s) => s.slice()); + while (out.length < STROKES) out.push(P(list[0][2], list[0][3])); + return out; + }; + return [pad(L), pad(Sh)]; +}); + +// The text as a list of signs: a rune index, or -1 for a divider, with the +// start of each sign along the text and its advance, counted in runes. +const SIGNS = [], START = [], WIDE = []; +{ + let x = 0; + for (const word of TEXT.split(" ")) { + for (const ch of word) { + SIGNS.push(KEYS.indexOf(ch)); + START.push(x); + WIDE.push(1); + x += 1; + } + SIGNS.push(-1); + START.push(x); + WIDE.push(DIVU); + x += DIVU; + } + START.push(x); +} +const TOTAL = START[SIGNS.length]; + +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 - s * f), t = v * (1 - s * (1 - f)); + return [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; +} + +function hash(x, y) { + let h = (x * 374761393 + y * 668265263) | 0; + h = Math.imul(h ^ (h >>> 13), 1274126177); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +} + +// The path for n turns: samples every DS LEDs of arc length, starting at +// the snout (u = -HEAD), with position and unit tangent. +function path(n) { + const b = (BOTTOM - TOP) / (2 * n), xl = HW + b, xr = W - 1 - HW - b; + const X = [], Y = [], TX = [], TY = []; + let carry = 0; + const line = (x0, y0, x1, y1) => { + const L = hypot(x1 - x0, y1 - y0), tx = (x1 - x0) / L, ty = (y1 - y0) / L; + let s = carry; + for (; s < L; s += DS) X.push(x0 + tx * s), Y.push(y0 + ty * s), TX.push(tx), TY.push(ty); + carry = s - L; + }; + const arc = (cx, cy, side) => { + const L = PI * b; + let s = carry; + for (; s < L; s += DS) { + const th = -PI / 2 + s / b; + X.push(cx + side * b * cos(th)), Y.push(cy + b * sin(th)), TX.push(-side * sin(th)), TY.push(cos(th)); + } + carry = s - L; + }; + line(xl - HEAD, TOP, xl, TOP); + for (let j = 0; j <= n; j++) { + const y = TOP + 2 * b * j, right = j % 2 === 0; + line(right ? xl : xr, y, right ? xr : xl, y); + if (j < n) arc(right ? xr : xl, y + b, right ? 1 : -1); + } + const end = (X.length - 1) * DS - HEAD, last = n % 2 === 0 ? 1 : -1, xe = n % 2 === 0 ? xr : xl; + line(xe, BOTTOM, xe + last * (TAIL + 1), BOTTOM); + return { X, Y, TX, TY, end }; +} + +// Every LED's (u, v) in the band's frame, from its nearest path sample. +function map(n) { + const p = path(n), N = W * H, best = new Float32Array(N).fill(1e9), idx = new Int32Array(N).fill(-1); + const R = 12; + for (let i = 0; i < p.X.length; i++) { + const px = p.X[i], py = p.Y[i]; + for (let y = max(0, floor(py - R)); y <= min(H - 1, Math.ceil(py + R)); y++) + for (let x = max(0, floor(px - R)); x <= min(W - 1, Math.ceil(px + R)); x++) { + const d = (x - px) * (x - px) + (y - py) * (y - py), o = y * W + x; + if (d < best[o]) (best[o] = d), (idx[o] = i); + } + } + const U = new Float32Array(N), V = new Float32Array(N), IN = new Uint8Array(N); + for (let o = 0; o < N; o++) { + const i = idx[o]; + if (i < 0) continue; + const dx = (o % W) - p.X[i], dy = floor(o / W) - p.Y[i]; + U[o] = i * DS - HEAD + dx * p.TX[i] + dy * p.TY[i]; + V[o] = dx * p.TY[i] - dy * p.TX[i]; + IN[o] = 1; + } + return { n, path: p, U, V, IN, end: p.end }; +} + +// The band's half width at u: the head swells to an almond 1.3 times as +// wide and closes at the snout, the tail tapers to nothing. +function halfWidth(u, end) { + if (u < 0) { + const t = -u / HEAD; + return t >= 1 ? 0 : HW * 1.3 * sin(PI * (0.25 + 0.75 * t)) ** 0.7; + } + if (u > end) return HW * max(0, 1 - (u - end) / TAIL); + return HW; +} + +export function setup(params) { + const stone = new Float32Array(W * H); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + // Granite: a grey that wanders slowly, grains and the odd bright crystal. + const g = 0.5 * hash(x >> 2, y >> 2) + 0.5 * hash((x + 2) >> 2, (y + 1) >> 2), r = hash(x, y); + stone[y * W + x] = 0.075 + 0.035 * g + 0.04 * r + (r > 0.97 ? 0.08 : 0); + } + params.s = { scroll: 0, hue: 0, speed: 2, twig: 0.06, turns: 3, e: 0, adv: ADV0, scx: SCX0, m: null, stone, chips: [], since: 0, seg: new Float32Array(KEYS.length * STROKES * 4), first: true }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 3, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + S.speed = cl(k[1] == null ? 2 : k[1], 0.2, 3); + S.turns = round(cl(k[2] == null ? 3 : k[2], 3, 6)); + S.twig = cl(k[3] == null ? 0.06 : k[3], 0, 1); + // Long-branch up to 0.1, short-twig from 0.9, eased between. + const f = cl((S.twig - 0.1) / 0.8, 0, 1); + S.e = f * f * (3 - 2 * f); + S.adv = ADV0 + (ADV1 - ADV0) * S.e; + S.scx = SCX0 + (SCX1 - SCX0) * S.e; + if (!S.m || S.m.n !== S.turns) S.m = map(S.turns); + if (S.first) { + // Open with the first word just inside the jaws and the band full. + S.scroll = -2 / S.adv; + S.first = false; + } + // scroll counts runes, so the text keeps its speed in LEDs whatever Twig. + S.scroll += (d * S.speed * RATE) / S.adv; + // Chips fly from the spark. + S.since += d; + for (const c of S.chips) (c.x += c.vx * d), (c.y += c.vy * d), (c.vy += 40 * d), (c.life -= d); + S.chips = S.chips.filter((c) => c.life > 0); + if (S.spark && S.since > 0.06) { + S.since = 0; + const a = Math.random() * 2 * PI, v = 10 + 14 * Math.random(); + S.chips.push({ x: S.spark[0], y: S.spark[1], vx: v * cos(a), vy: v * sin(a) - 6, life: 0.25 + 0.25 * Math.random() }); + } +} + +// Distance in LEDs from box point (gx, gy) to sign g's strokes, the strokes +// cut only as far as `cut` of their total length when it is under 1. With +// `spark`, returns the point the cut has reached instead. +function strokes(S, g, gx, gy, cut, spark) { + const sx = S.scx, sy = SC; + if (g < 0) { + const a = cut < 1 ? cut * 2 : 2; + if (spark) return a < 1 ? [0.5, 2.2] : [0.5, 5.8]; + let dd = 1e9; + if (a > 0) dd = hypot((gx - 0.5) * sx, (gy - 2.2) * sy); + if (a > 1) dd = min(dd, hypot((gx - 0.5) * sx, (gy - 5.8) * sy)); + return dd - (DOT - RAD); + } + const seg = S.seg, o = g * STROKES * 4; + let left = cut < 1 ? cut * S.len[g] : 1e9, dd = 1e9, ex = seg[o], ey = seg[o + 1]; + for (let i = 0; i < STROKES && left > 0; i++) { + const ax = seg[o + i * 4], ay = seg[o + i * 4 + 1], bx = seg[o + i * 4 + 2], by = seg[o + i * 4 + 3]; + const L = hypot(bx - ax, by - ay), f = L > 0.01 ? min(1, left / L) : 1; + ex = ax + (bx - ax) * f; + ey = ay + (by - ay) * f; + left -= L; + if (spark) continue; + const vx = (ex - ax) * sx, vy = (ey - ay) * sy, px = (gx - ax) * sx, py = (gy - ay) * sy, l2 = vx * vx + vy * vy; + const t = l2 > 0 ? cl((px * vx + py * vy) / l2, 0, 1) : 0; + dd = min(dd, hypot(px - vx * t, py - vy * t)); + } + return spark ? [ex, ey] : dd; +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.m) return; + const M = S.m, U = M.U, V = M.V, IN = M.IN, end = M.end; + // Strokes for this Twig. + const e = S.e, A = S.adv; + S.len = S.len || new Float32Array(KEYS.length); + for (let g = 0; g < KEYS.length; g++) { + let L = 0; + for (let i = 0; i < STROKES; i++) + for (let c = 0; c < 4; c++) { + const a = FORMS[g][0][i][c], b = FORMS[g][1][i][c]; + S.seg[(g * STROKES + i) * 4 + c] = a + (b - a) * e; + } + for (let i = 0; i < STROKES; i++) { + const o = (g * STROKES + i) * 4; + L += hypot(S.seg[o + 2] - S.seg[o], S.seg[o + 3] - S.seg[o + 1]); + } + S.len[g] = L; + } + const paint = hsv(S.hue + 0.13, 0.78, 1), body = hsv(S.hue + 0.6, 0.78, 0.34), groove = [0.86, 0.84, 0.78]; + // The carving point: a sign is cut while its head-side edge moves from + // U1 to one sign's width past it, so the signs are cut one after another. + const U1 = end - ADV0 - 1; + + // Where the sign being carved has got to, for the spark and its light. + let spark = null; + { + const X = U1 / A + S.scroll, k = floor(X / TOTAL), xm = X - k * TOTAL; + let j = 0; + while (j + 1 < SIGNS.length && START[j + 1] <= xm) j++; + const sj = (k * TOTAL + START[j] - S.scroll) * A, c = (U1 - sj) / (CARVE * WIDE[j] * A); + const pt = strokes(S, SIGNS[j], 0, 0, c, true); + const cx = SIGNS[j] < 0 ? 0.5 : 2, su = sj + (WIDE[j] * A) / 2 + (pt[0] - cx) * S.scx, sv = (4 - pt[1]) * SC; + const pth = M.path, i = cl(round((su + HEAD) / DS), 0, pth.X.length - 1), du = su - (i * DS - HEAD); + spark = [pth.X[i] + pth.TX[i] * du + sv * pth.TY[i], pth.Y[i] + pth.TY[i] * du - sv * pth.TX[i]]; + } + S.spark = spark; + const flick = 0.8 + 0.2 * Math.random(); + + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const o = y * W + x, st = S.stone[o]; + let r = st, g = st * 1.02, b = st * 1.1; + if (IN[o]) { + const u = U[o], v = V[o], av = abs(v), hw = halfWidth(u, end); + let a = hw > 0 ? cl(hw + 0.5 - av, 0, 1) : 0; + if (a > 0) { + // The band: painted body and its two cut, painted edges. + const edge = cl(av - (hw - EDGE) + 0.5, 0, 1); + let cr = body[0] + (paint[0] * 0.85 - body[0]) * edge, cg = body[1] + (paint[1] * 0.85 - body[1]) * edge, cb = body[2] + (paint[2] * 0.85 - body[2]) * edge; + if (u < 0) { + // The head: an eye on the upper side, and the jaws open in a + // wedge from the neck's middle, rimmed with the edge paint. + const ex = u + HEAD * 0.16, ey = v - HW * 0.45, eye = cl(1.9 - hypot(ex, ey), 0, 1); + const mw = (-u - HEAD * 0.45) * 0.5, rim = mw > -1 ? cl(mw + EDGE + 0.5 - av, 0, 1) : 0; + const lip = max(eye, rim) * (rim > eye ? 0.85 : 1); + cr += (paint[0] - cr) * lip; + cg += (paint[1] - cg) * lip; + cb += (paint[2] - cb) * lip; + a *= 1 - cl(mw + 0.5 - av, 0, 1); + } else if (u < U1 + ADV0 && av < hw - EDGE) { + // The text. + const X = u / A + S.scroll, k = floor(X / TOTAL), xm = X - k * TOTAL; + let lo = 0, hi = SIGNS.length - 1; + while (lo < hi) { + const mid = (lo + hi + 1) >> 1; + if (START[mid] <= xm) lo = mid; + else hi = mid - 1; + } + const j = lo, gsign = SIGNS[j], centre = START[j] + WIDE[j] / 2; + const c = (U1 - (k * TOTAL + START[j] - S.scroll) * A) / (CARVE * WIDE[j] * A); + if (c > 0) { + const gx = ((xm - centre) * A) / S.scx + (gsign < 0 ? 0.5 : 2), gy = 4 - v / SC; + const dd = strokes(S, gsign, gx, gy, c, false), ink = cl(RAD + 0.55 - dd, 0, 1); + if (ink > 0) { + const pt = cl((c - CARVE) / PAINT, 0, 1), fresh = c < 1 ? 1 : 0; + const ir = (groove[0] + (paint[0] - groove[0]) * pt) * (1 + 0.15 * fresh); + const ig = (groove[1] + (paint[1] - groove[1]) * pt) * (1 + 0.1 * fresh); + const ib = groove[2] + (paint[2] - groove[2]) * pt; + cr += (ir - cr) * ink; + cg += (ig - cg) * ink; + cb += (ib - cb) * ink; + } + } + } + r += (cr - r) * a; + g += (cg - g) * a; + b += (cb - b) * a; + } + } + if (spark) { + // A white core and a warm light round it that reaches the stone. + const dx = x - spark[0], dy = y - spark[1], d2 = dx * dx + dy * dy; + const q = flick * (exp(-d2 / 2.4) + 0.35 * exp(-d2 / 18)); + r += q; + g += q * 0.9; + b += q * 0.65; + } + display.setPixel(x, y, round(cl(r, 0, 1) * 255), round(cl(g, 0, 1) * 255), round(cl(b, 0, 1) * 255)); + } + for (const c of S.chips) { + const x = round(c.x), y = round(c.y); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const l = cl(c.life * 3, 0, 1); + display.setPixel(x, y, round(255 * l), round(225 * l), round(160 * l)); + } +} diff --git a/incoming/pattern-study/patterns/tempo-canon.js b/incoming/pattern-study/patterns/tempo-canon.js new file mode 100644 index 00000000..6c7c2f0f --- /dev/null +++ b/incoming/pattern-study/patterns/tempo-canon.js @@ -0,0 +1,257 @@ +// ===== Patternflow pattern ===== +// Title: Tempo Canon +// Family: rhythm +// Idea: A player-piano roll scrolls up the tall panel with one lane for each voice of a canon, every voice playing the same melody at its own fixed tempo, and faint lines join the same note in every lane, so the lines arch one way before the convergence point, lie flat across the panel where all the voices strike one note together in a white flash, and arch the other way after it, one new canon after another. +// Author: patternflow-lab +// Date: 2026-11-03 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the tempo canons of Conlon Nancarrow's Studies for Player Piano (from 1948; Study No. 36 at 17:18:19:20, Study No. 33 at the square root of 2 to 2, Study No. 40 at e to pi), the convergence point as Kyle Gann named it (The Music of Conlon Nancarrow, 1995), pattern round 100 +// Round: 100 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Spread=-1..1, Voices=4..12 +// +// Knob 1: Hue. Turns the voice colours round the wheel together. At 0 the slowest voice in the middle lane is gold and the colours run through green to azure in the fastest lanes at the edges; at 0.5 the middle lane is blue and the colours run through violet and red to orange at the edges. The convergence stays white. +// Knob 2: Speed. The roll moves 6 rows a second for each unit: 12 at the opening 2, where a canon of about 115 rows passes the playhead in 10 s; 1.2 rows a second at 0.2 and 18 at 3. +// Knob 3: Spread. The tempo step between neighbouring voices, 1/4 at the opening 0, so four voices run 4:5:6:7 and the fastest plays the melody in 57% of the rows the slowest takes. Toward -1 the step falls to 1/17, where four voices run 17:18:19:20 as in Study No. 36 and the lines barely bend; toward 1 it rises to 1, four voices 1:2:3:4 and twelve 1:2:...:12, a sharp spindle. +// Knob 4: Voices. 4 to 12 lanes. The slowest voice takes a middle lane and the faster ones sit outward, left and right in turn, so the lane width falls from 16 LEDs to 5 and each canon sharpens from an arch of four columns into a spindle. + +// One canon is a melody of n notes, each with an onset s(k) and a length, +// counted in rows at the slowest tempo. Voice i plays it at tempo +// r_i = 1 + d i (d is Spread's step), so its note k sounds at +// +// t_i(k) = Tc + (s(k) - s(K)) / r_i, +// +// with one note K chosen for the canon. At Tc every voice strikes note K: +// that row is the convergence point. A note ahead of K is reached by the +// faster voices later than by the slow one, a note after K sooner, and the +// gap grows with the distance from K. The faint line through note k in +// every lane therefore bends toward the convergence row at the fast lanes, +// one way above it and the other way below, and lies dead flat at K. +// +// Nancarrow punched such canons into player-piano rolls by hand. Study +// No. 36 runs four voices at 17:18:19:20; Studies No. 33 and 40 use ratios +// that are not fractions at all, the square root of 2 to 2 and e to pi. +// Here the voices step by d, which Spread turns smoothly, so almost every +// setting is such an irrational canon. Kyle Gann's book calls the moment +// all the voices meet the convergence point. Each new canon here gets a new +// melody of 18 to 26 notes and puts its convergence on the first note (a +// quarter of the time), on the last (a quarter) or between 30% and 70% of +// the way through (the rest); the first canon always takes the middle. +// +// The roll runs down: earlier rows sit higher, the paper moves up, and +// notes strike as they cross the playhead at row 76, flashing white. A +// note is a bar 2 rows tall in its voice's lane, set across the lane by its +// pitch, one of 8 steps, with a thin tail for 72% of its note value at that +// voice's tempo. Notes already played are full colour, fading toward the +// top; notes to come are dimmer and paler. + +const W = 64, H = 128; +const { floor, round, min, max, abs, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The playhead row, rows a second for each unit of Speed, and the rows of +// silence at the slowest tempo between one canon and the next. +const PLAY = 76, RATE = 6, GAP = 14; +// Pitch steps across a lane, note values in rows at the slowest tempo, and +// how much of each value a bar fills. +const PITCHES = 8, VALUES = [3, 3, 4.5, 6, 6, 9], LEGATO = 0.72, HEAD = 2; +// Hue of the slowest voice at knob 0 and the band the voices spread over. +const HUE0 = 0.12, BAND = 0.46; +// Spread: d = 1/4 times 4.25^k below 0 and 4^k above, so 1/17 at -1, +// 1/4 at 0 and 1 at 1. +const stepOf = (k) => 0.25 * (k < 0 ? 4.25 : 4) ** k; +// Rows a strike stays white, and rows the convergence flash lasts. +const STRIKE = 7, FLASH = 22; + +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 - s * f), t = v * (1 - s * (1 - f)); + return [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; +} + +// A new canon whose slowest voice begins at row `start` of the score. +// `mid` forces the convergence into the middle third (the first canon). +function canon(start, mid) { + const n = 18 + floor(Math.random() * 9); + const s = new Float32Array(n), len = new Float32Array(n), p = new Int8Array(n); + let t = 0, pitch = floor(Math.random() * PITCHES); + for (let k = 0; k < n; k++) { + s[k] = t; + len[k] = VALUES[floor(Math.random() * VALUES.length)]; + t += len[k]; + p[k] = pitch; + const step = [-2, -1, -1, 0, 1, 1, 2][floor(Math.random() * 7)]; + pitch += step; + if (pitch < 0) pitch = -pitch; + if (pitch >= PITCHES) pitch = 2 * (PITCHES - 1) - pitch; + } + const u = Math.random(); + const K = mid || u >= 0.5 ? floor(n * (0.3 + 0.4 * Math.random())) : u < 0.25 ? 0 : n - 1; + return { n, s, len, p, K, Tc: start + s[K], start, end: start + t }; +} + +export function setup(params) { + params.s = { + pos: 0, + canons: [], + hue: 0, + speed: 2, + voices: 4, + d: 0.25, + R: new Float32Array(W * H), + G: new Float32Array(W * H), + B: new Float32Array(W * H), + }; + const S = params.s; + // The first canon is placed so its convergence reaches the playhead + // 20 rows (1.7 s at the opening speed) after the start. + const c = canon(0, true); + const shift = S.pos + 20 - c.Tc; + c.start += shift; + c.end += shift; + c.Tc += shift; + S.canons.push(c); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 4], d = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + S.speed = cl(k[1] == null ? 2 : k[1], 0.2, 3); + S.d = stepOf(cl(k[2] || 0, -1, 1)); + S.voices = round(cl(k[3] == null ? 4 : k[3], 4, 12)); + S.pos += d * S.speed * RATE; + const C = S.canons; + while (C[C.length - 1].start < S.pos + (H - PLAY) + 8) C.push(canon(C[C.length - 1].end + GAP, false)); + while (C.length > 1 && C[0].end + 12 < S.pos - PLAY) C.shift(); +} + +// Lanes left to right: the slowest voice (0) in the middle, faster ones +// outward, alternating sides: 4 voices sit 2 0 1 3. +function lanes(V) { + const order = [0]; + for (let i = 1; i < V; i++) if (i % 2) order.push(i); + else order.unshift(i); + return order; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const R = S.R, G = S.G, B = S.B; + R.fill(0); + G.fill(0); + B.fill(0); + const V = S.voices, order = lanes(V); + // Lane edges, content width (one dark LED between lanes), bar width. + const x0 = [], cw = []; + for (let j = 0; j < V; j++) { + const a = round((j * W) / V), b = round(((j + 1) * W) / V); + x0.push(a + (j === 0 ? 1 : 0)); + cw.push(b - a - 1 - (j === 0 ? 1 : 0)); + } + const bw = max(2, round(cw[0] * 0.36)), tail = bw > 3 ? 2 : 1, tx = (bw - tail) >> 1; + const tempo = [], col = []; + for (let i = 0; i < V; i++) { + tempo.push(1 + S.d * i); + col.push(hsv(S.hue + HUE0 + (BAND * i) / (V - 1), 0.8, 1)); + } + // Brightness by row: played notes fade from 1 at the playhead to 0.55 at + // the top; notes to come are at 0.62 and paled a third toward white, so a + // dim gold does not turn brown. + const shade = (y) => (y <= PLAY ? 0.55 + (0.45 * max(0, y)) / PLAY : 0.62); + const pale = (y) => (y <= PLAY ? 0 : 0.32); + + const add = (x, y, r, g, b, a) => { + if (x < 0 || x >= W || y < 0 || y >= H || a <= 0) return; + const o = y * W + x; + R[o] = min(1, R[o] + r * a); + G[o] = min(1, G[o] + g * a); + B[o] = min(1, B[o] + b * a); + }; + // An antialiased line (Xiaolin Wu's), added into the buffer. + const line = (xa, ya, xb, yb, c0, c1, a) => { + const steep = abs(yb - ya) > abs(xb - xa); + if (steep) [xa, ya, xb, yb] = [ya, xa, yb, xb]; + if (xa > xb) [xa, ya, xb, yb, c0, c1] = [xb, yb, xa, ya, c1, c0]; + const n = max(1, round(xb - xa)); + for (let i = 0; i <= n; i++) { + const f = i / n, x = round(xa) + i, y = ya + (yb - ya) * f, y0 = floor(y), w = y - y0; + const r = c0[0] + (c1[0] - c0[0]) * f, g = c0[1] + (c1[1] - c0[1]) * f, b = c0[2] + (c1[2] - c0[2]) * f; + if (steep) { + add(y0, x, r, g, b, a * (1 - w)); + add(y0 + 1, x, r, g, b, a * w); + } else { + add(x, y0, r, g, b, a * (1 - w)); + add(x, y0 + 1, r, g, b, a * w); + } + } + }; + const WHITE = [1, 1, 1]; + + for (const c of S.canons) { + const top = c.start - S.pos + PLAY, bottom = c.end - S.pos + PLAY; + if (bottom < -4 || top > H + 4) continue; + const yOf = (i, k) => c.Tc + (c.s[k] - c.s[c.K]) / tempo[i] - S.pos + PLAY; + const xOf = (j, k) => x0[j] + round(((cw[j] - bw) * c.p[k]) / (PITCHES - 1)); + // Rows since the convergence crossed the playhead. + const since = S.pos - c.Tc, flash = since >= 0 && since < FLASH ? 1 - since / FLASH : 0; + + // The fan: one faint line per note through all the lanes. + for (let k = 0; k < c.n; k++) { + const conv = k === c.K; + for (let j = 0; j + 1 < V; j++) { + const ia = order[j], ib = order[j + 1]; + const ya = yOf(ia, k), yb = yOf(ib, k); + if ((ya < -2 && yb < -2) || (ya > H + 2 && yb > H + 2)) continue; + const xa = xOf(j, k) + bw / 2, xb = xOf(j + 1, k) + bw / 2; + if (conv) line(xa - bw / 2, ya, xb + bw / 2, yb, WHITE, WHITE, 0.42 + 0.5 * flash); + else line(xa, ya, xb, yb, col[ia], col[ib], 0.62 * shade((ya + yb) / 2)); + } + } + // A glow over the convergence row as all the voices strike together. + if (flash > 0) { + const yc = c.Tc - S.pos + PLAY; + for (let dy = -3; dy <= 3; dy++) { + const y = round(yc) + dy, a = flash * 0.35 * exp(-dy * dy * 0.35); + for (let x = 0; x < W; x++) add(x, y, 1, 1, 1, a); + } + } + // The notes: a bar HEAD rows tall where each note strikes, and a thin + // tail as long as the note sounds at that voice's tempo, overwriting the + // lines under them. + for (let j = 0; j < V; j++) { + const i = order[j], cc = col[i]; + for (let k = 0; k < c.n; k++) { + const ya = yOf(i, k), yh = ya + HEAD, yb = max(yh, ya + (c.len[k] * LEGATO) / tempo[i]); + if (yb < 0 || ya >= H) continue; + const xs = xOf(j, k), age = PLAY - ya; + // White for STRIKE rows after it crosses the playhead. + const hit = age >= 0 && age < STRIKE ? 1 - age / STRIKE : 0; + const conv = k === c.K ? flash : 0, wt = max(hit, conv); + for (let y = max(0, floor(ya)); y < min(H, Math.ceil(yb)); y++) { + const inHead = y < yh, cov = min(y + 1, inHead ? yh : yb) - max(y, inHead ? ya : yh); + if (cov <= 0) continue; + const v = shade(y) * (inHead ? 1 : 0.42), q = max(pale(y), wt), m = max(v, wt); + const r = (cc[0] + (1 - cc[0]) * q) * m, g = (cc[1] + (1 - cc[1]) * q) * m, b = (cc[2] + (1 - cc[2]) * q) * m; + const a = inHead ? xs : xs + tx, z = inHead ? xs + bw : xs + tx + tail; + for (let x = a; x < z; x++) { + const o = y * W + x; + R[o] += (r - R[o]) * cov; + G[o] += (g - G[o]) * cov; + B[o] += (b - B[o]) * cov; + } + } + } + } + } + // The playhead: a short tick at each edge. + for (const x of [0, W - 1]) add(x, PLAY, 0.55, 0.55, 0.6, 1); + + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const o = y * W + x; + display.setPixel(x, y, round(R[o] * 255), round(G[o] * 255), round(B[o] * 255)); + } +} From 87e507a73fe75a1473e76d9586da32cb9cff856f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 04:02:36 +0000 Subject: [PATCH 5/8] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20100=ED=8C=90:=20Magic?= =?UTF-8?q?=20Lines=20=C2=B7=20Dokkaebibul?= 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_01Bc4JkrYAzT3TWt21dU8pXk --- .../pattern-study/patterns/dokkaebibul.js | 540 ++++++++++++++++++ .../pattern-study/patterns/magic-lines.js | 352 ++++++++++++ 2 files changed, 892 insertions(+) create mode 100644 incoming/pattern-study/patterns/dokkaebibul.js create mode 100644 incoming/pattern-study/patterns/magic-lines.js diff --git a/incoming/pattern-study/patterns/dokkaebibul.js b/incoming/pattern-study/patterns/dokkaebibul.js new file mode 100644 index 00000000..fa9b5fe1 --- /dev/null +++ b/incoming/pattern-study/patterns/dokkaebibul.js @@ -0,0 +1,540 @@ +// ===== Patternflow pattern ===== +// Title: Dokkaebibul +// Family: type +// Idea: Yun Dong-ju's Prelude is typed key by key on a two-set Korean keyboard at the foot of the panel and builds itself in 18-LED syllables as the input method builds them: a consonant typed after a whole syllable sits as its final, and when the next key is a vowel it leaps out as a cyan flame along a glowing arc to start the next syllable, the jump Korean typists call dokkaebibul, the will-o'-the-wisp. +// Author: patternflow-lab +// Date: 2026-11-03 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the two-set (dubeolsik) Hangul keyboard and its syllable automaton, standardised as KS C 5715 (1982, now KS X 5002); the text is Yun Dong-ju's Seosi (Prelude), written 20 November 1941; jamo strokes and slots after the Jamo Assembly pattern (round 63); pattern round 100 +// Round: 100 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Phrase=0..8, Trail=0..1 +// +// Knob 1: Hue. Turns all the colours. At 0 the ink is gold, the syllable being composed pale gold on a faint cyan box, the flame, its arc and its light cyan, the vowel keys lilac; at 0.5 the ink is blue and the flame red. +// Knob 2: Speed. At 2 about three keys a second (0.25 to 0.41 s apart, 0.25 s more after a space); a jump flies 0.42 s, and a finished line holds 2.4 s and fades in 0.8 s. The first line's 30 keys take 10.4 s, the whole poem with its 38 jumps 2 minutes. Times go as 2 / Speed. +// Knob 3: Phrase. The line to start from, 0 to 8, then on through the poem. Line 0 (죽는 날까지 하늘을 우러러) has 4 jumps in 30 keys; line 8 (오늘 밤에도 별이 바람에 스치운다.) has 5 in 38 and needs a sixth row, so the page scrolls. +// Knob 4: Trail. How long a jump's glow stays. Each point of the arc fades with a time constant of 0.31 s at the opening 0.06, 1.6 s at 0.5 and 10 s at 1, where a whole line's arcs stay; the arc widens from 5 to 8 LEDs, and a landed consonant cools to ink, and stops lighting the letters within 34 LEDs, over twice that time. + +// Nothing on the two-set keyboard says where a syllable ends, so the +// input method decides as the keys come. A consonant after an initial and +// a vowel is taken as the final (받침): 하 + ㄴ shows 한. Another consonant +// ends the syllable or makes a cluster such as ㅄ. A vowel cannot follow +// the final, since a syllable needs a consonant before its vowel: 한 + ㅡ +// becomes 하느, and a cluster gives up its second letter (없 + ㅓ is 업서). +// Users call that jump dokkaebibul, the goblin fire that hops at night. +// +// The keys come from the poem's syllables (ㅚ is ㅗ then ㅣ; ㄲ and ㅆ one +// key with shift); script() runs them through the automaton, which gives +// back each of the nine lines exactly. Syllables are 18 by 18 blocks, +// three to a row and five rows down; the page scrolls a row when a line +// needs a sixth. A letter is strokes on a 20 by 20 grid set in a slot that +// depends on the vowel's shape and on the final; when a key changes the +// syllable its letters slide to their new slots in 0.12 s. + +const W = 64, H = 128; +const { floor, round, min, max, sqrt, exp, cos, sin, PI, random } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const smooth = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +// Yun Dong-ju, Seosi, the nine lines without the title. +const POEM = [ + "죽는 날까지 하늘을 우러러", + "한 점 부끄럼이 없기를,", + "잎새에 이는 바람에도", + "나는 괴로워했다.", + "별을 노래하는 마음으로", + "모든 죽어 가는 것을 사랑해야지", + "그리고 나한테 주어진 길을", + "걸어가야겠다.", + "오늘 밤에도 별이 바람에 스치운다.", +]; + +// ---- Jamo. Initials, vowels and finals in Unicode order. +const LS = "ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ"; +const VS = "ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ"; +const TS = " ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ"; +// Compound vowels and clusters as the two keys that make them. +const PAIR = { ㅘ: "ㅗㅏ", ㅙ: "ㅗㅐ", ㅚ: "ㅗㅣ", ㅝ: "ㅜㅓ", ㅞ: "ㅜㅔ", ㅟ: "ㅜㅣ", ㅢ: "ㅡㅣ", ㄳ: "ㄱㅅ", ㄵ: "ㄴㅈ", ㄶ: "ㄴㅎ", ㄺ: "ㄹㄱ", ㄻ: "ㄹㅁ", ㄼ: "ㄹㅂ", ㄽ: "ㄹㅅ", ㄾ: "ㄹㅌ", ㄿ: "ㄹㅍ", ㅀ: "ㄹㅎ", ㅄ: "ㅂㅅ" }; +const JOIN = {}; +for (const k in PAIR) JOIN[PAIR[k]] = k; +// Tense consonants are drawn as two of the plain letter. +const TENSE = { ㄲ: "ㄱ", ㄸ: "ㄷ", ㅃ: "ㅂ", ㅉ: "ㅈ", ㅆ: "ㅅ" }; +const isVowel = (j) => VS.includes(j); + +// ---- The keyboard (KS X 5002): rows, and the shifted keys. +const ROWS = ["ㅂㅈㄷㄱㅅㅛㅕㅑㅐㅔ", "ㅁㄴㅇㄹㅎㅗㅓㅏㅣ", "ㅋㅌㅊㅍㅠㅜㅡ"]; +const SHIFTED = { ㅃ: "ㅂ", ㅉ: "ㅈ", ㄸ: "ㄷ", ㄲ: "ㄱ", ㅆ: "ㅅ", ㅒ: "ㅐ", ㅖ: "ㅔ" }; +const ROW_X = [2, 4, 8], KEY_Y = 110, PITCH = 6; +// 5 by 5 key caps, rows top to bottom, 16 the left column. +const CAP = { + ㅂ: [17, 17, 31, 17, 31], ㅈ: [31, 4, 10, 17, 17], ㄷ: [31, 16, 16, 16, 31], ㄱ: [31, 1, 1, 1, 1], ㅅ: [4, 4, 10, 17, 17], + ㅛ: [0, 10, 10, 31, 0], ㅕ: [2, 30, 2, 30, 2], ㅑ: [8, 15, 8, 15, 8], ㅐ: [10, 10, 14, 10, 10], ㅔ: [5, 5, 29, 5, 5], + ㅁ: [31, 17, 17, 17, 31], ㄴ: [16, 16, 16, 16, 31], ㅇ: [14, 17, 17, 17, 14], ㄹ: [31, 1, 31, 16, 31], ㅎ: [4, 31, 14, 17, 14], + ㅗ: [0, 4, 4, 31, 0], ㅓ: [2, 2, 30, 2, 2], ㅏ: [8, 8, 15, 8, 8], ㅣ: [4, 4, 4, 4, 4], + ㅋ: [31, 1, 31, 1, 1], ㅌ: [31, 16, 31, 16, 31], ㅊ: [4, 31, 4, 10, 17], ㅍ: [31, 10, 10, 10, 31], ㅠ: [0, 31, 10, 10, 0], + ㅜ: [0, 31, 4, 4, 0], ㅡ: [0, 0, 31, 0, 0], "⇧": [4, 14, 31, 14, 14], +}; +const KEYPOS = {}; +ROWS.forEach((row, r) => [...row].forEach((j, i) => (KEYPOS[j] = [ROW_X[r] + i * PITCH, KEY_Y + r * PITCH]))); +KEYPOS["⇧"] = [1, KEY_Y + 2 * PITCH]; + +// ---- Letters as strokes on a 20 by 20 grid (from Jamo Assembly, round 63). +// A stroke is a polyline of x, y pairs; one that starts with O is an +// ellipse cx, cy, rx, ry. +const O = -1; +const BASE = { + ㄱ: [[3, 3, 17, 3, 17, 18]], + ㄴ: [[3, 2, 3, 17, 18, 17]], + ㄷ: [[17, 3, 3, 3, 3, 17, 18, 17]], + ㄹ: [[3, 2, 17, 2, 17, 10, 3, 10, 3, 18, 18, 18]], + ㅁ: [[3, 3, 17, 3, 17, 17, 3, 17, 3, 3]], + ㅂ: [[3, 2, 3, 18, 17, 18, 17, 2], [3, 10, 17, 10]], + ㅅ: [[10, 2, 2, 18], [10, 2, 18, 18]], + ㅇ: [[O, 10, 10, 7.5, 7.5]], + ㅈ: [[3, 3, 17, 3], [10, 3, 3, 18], [10, 3, 17, 18]], + ㅊ: [[10, 0, 10, 4], [3, 5, 17, 5], [10, 5, 3, 19], [10, 5, 17, 19]], + ㅋ: [[3, 3, 17, 3, 17, 18], [3, 10, 17, 10]], + ㅌ: [[17, 2, 3, 2, 3, 18, 18, 18], [3, 10, 17, 10]], + ㅍ: [[2, 3, 18, 3], [2, 17, 18, 17], [7, 3, 7, 17], [13, 3, 13, 17]], + ㅎ: [[10, 0, 10, 3], [3, 5, 17, 5], [O, 10, 13, 5.5, 5]], + ㅣ: [[10, 1, 10, 19]], + ㅏ: [[7, 1, 7, 19], [7, 10, 17, 10]], + ㅑ: [[7, 1, 7, 19], [7, 7, 17, 7], [7, 13, 17, 13]], + ㅓ: [[13, 1, 13, 19], [3, 10, 13, 10]], + ㅕ: [[13, 1, 13, 19], [3, 7, 13, 7], [3, 13, 13, 13]], + ㅐ: [[5, 1, 5, 19], [5, 10, 13, 10], [15, 0, 15, 20]], + ㅒ: [[5, 1, 5, 19], [5, 7, 13, 7], [5, 13, 13, 13], [15, 0, 15, 20]], + ㅔ: [[2, 10, 9, 10], [9, 1, 9, 19], [16, 0, 16, 20]], + ㅖ: [[2, 7, 9, 7], [2, 13, 9, 13], [9, 1, 9, 19], [16, 0, 16, 20]], + ㅡ: [[1, 10, 19, 10]], + ㅗ: [[10, 4, 10, 13], [1, 13, 19, 13]], + ㅛ: [[7, 4, 7, 13], [13, 4, 13, 13], [1, 13, 19, 13]], + ㅜ: [[1, 7, 19, 7], [10, 7, 10, 16]], + ㅠ: [[1, 7, 19, 7], [7, 7, 7, 16], [13, 7, 13, 16]], + ".": [[3, 16, 4, 17]], + ",": [[4, 15, 2, 19]], +}; +const HORIZONTAL = "ㅗㅛㅜㅠㅡ"; +// Slots on the 20 by 20 block, [x, y, w, h]: initial, vowel parts, final. +const SLOTS = { + V: [[1, 3, 11, 14], [[12, 0, 7, 20]]], + VF: [[1, 1, 11, 10], [[12, 0, 7, 12]], [3, 13, 14, 7]], + H: [[3, 1, 14, 10], [[0, 10, 20, 9]]], + HF: [[3, 0, 14, 7], [[0, 6, 20, 6]], [3, 13, 14, 7]], + C: [[1, 1, 11, 9], [[0, 9, 14, 8], [13, 0, 7, 20]]], + CF: [[1, 0, 11, 6], [[0, 5, 14, 6], [13, 0, 7, 13]], [3, 13, 14, 7]], +}; +const ALONE = [3, 2, 14, 16]; + +// Cells: 21 LEDs apart, the 18 by 18 block inside each. +const CELL = 21, BLOCK = 18, COLS = 3, ROWS_SHOWN = 5, TOP = 1, TEXT_BOTTOM = 107; + +// Script timings in seconds at Speed 2. +const KEY = 0.33, SPACE = 0.25, FLY = 0.42, SLIDE = 0.12, HOLD = 2.4, FADE = 0.8, LEAD = 0.4; + +// ---- The automaton. A syllable is { L: [key], V: [keys], T: [keys] }. +const syl = (L, V, T) => ({ L, V, T }); +const copy = (s) => syl(s.L.slice(), s.V.slice(), s.T.slice()); +const PUNCT = ",."; + +// The keys of a line, from its syllables. +function keysOf(text) { + const keys = []; + for (const ch of text) { + const c = ch.charCodeAt(0) - 0xac00; + if (c < 0 || c >= 11172) keys.push(ch); + else { + const v = VS[floor((c % 588) / 28)], t = TS[c % 28]; + keys.push(LS[floor(c / 588)], ...(PAIR[v] || v)); + if (t !== " ") keys.push(...(PAIR[t] || t)); + } + } + return keys; +} + +// A typist's unevenness, the same every time a line comes round. +const jitter = (n) => { + const v = sin(n * 12.9898 + 4.1) * 43758.5453; + return 0.75 + 0.5 * (v - floor(v)); +}; + +// Each cell keeps the history of its syllable; each event is a key, the +// cell it went to and the jump it caused, if any. +function script(text) { + const cells = [], events = []; + let cur = -1, t = LEAD, row = 0, col = 0, n = text.length * 7; + const place = () => { + if (col === COLS) { col = 0; row++; } + cells.push({ row, col: col++, hist: [{ t: -1, s: syl([], [], []) }] }); + return cells.length - 1; + }; + const set = (i, s) => cells[i].hist.push({ t, s }); + const fresh = (s) => set((cur = place()), s); + for (const k of keysOf(text)) { + const ev = { t, key: k, cell: -1, jump: null }, h = cur >= 0 ? cells[cur].hist : null; + const s = h ? copy(h[h.length - 1].s) : null; + if (k === " " || PUNCT.includes(k)) { + // A space takes a cell, except where it would start a row. + cur = -1; + if (k !== " ") set(place(), syl([], [], [k])); + else if (col === COLS) { col = 0; row++; } + else if (col > 0) place(); + } else if (!isVowel(k)) { + if (s && s.L.length && s.V.length && !s.T.length && !"ㄸㅃㅉ".includes(k)) set(cur, ((s.T = [k]), s)); + else if (s && s.T.length === 1 && JOIN[s.T[0] + k]) set(cur, (s.T.push(k), s)); + else fresh(syl([k], [], [])); + } else if (s && !s.V.length) set(cur, ((s.V = [k]), s)); + else if (s && !s.T.length && s.V.length === 1 && JOIN[s.V[0] + k]) set(cur, (s.V.push(k), s)); + else if (s && s.T.length && s.L.length) { + // The jump: the final, or the second letter of a cluster, leaves. + const moved = s.T.pop(), from = cur; + set(cur, s); + fresh(syl([moved], [k], [])); + ev.jump = { from, to: cur, letter: moved, src: s.T.length }; + } else fresh(syl([], [k], [])); + if (k !== " " && !PUNCT.includes(k)) ev.cell = cur; + events.push(ev); + t += KEY * jitter(n++) + (k === " " ? SPACE : 0); + } + const rows = cells.length ? cells[cells.length - 1].row + 1 : 1, jumps = events.filter((e) => e.jump); + return { cells, events, end: t + HOLD, off: max(0, (ROWS_SHOWN - rows) / 2), third: jumps.length > 2 ? jumps[2].t : 0 }; +} + +// ---- Layout: the pieces of a syllable with their rects on the 20 grid. +// One letter, or two side by side, in a rect; a tense consonant is two. +function split(keys, r, role, out) { + const ls = keys.length === 1 && TENSE[keys[0]] ? [TENSE[keys[0]], TENSE[keys[0]]] : keys; + const w = ls.length === 1 ? r[2] : r[2] * 0.47; + ls.forEach((ch, i) => out.push({ id: role + i + ch, ch, r: [r[0] + i * (r[2] - w), r[1], w, r[3]] })); +} +function layout(s) { + const out = []; + if (PUNCT.includes(s.T[0])) return [{ id: "P", ch: s.T[0], r: [0, 0, 20, 20] }]; + if (!s.V.length) { + if (s.L.length) split(s.L, ALONE, "L", out); + return out; + } + if (!s.L.length) { + out.push({ id: "V0" + s.V[0], ch: s.V[0], r: ALONE }); + return out; + } + const shape = (s.V.length === 2 ? "C" : HORIZONTAL.includes(s.V[0]) ? "H" : "V") + (s.T.length ? "F" : ""); + const sl = SLOTS[shape]; + split(s.L, sl[0], "L", out); + s.V.forEach((v, i) => out.push({ id: "V" + i + v, ch: v, r: sl[1][i] })); + if (s.T.length) split(s.T, sl[2], "T", out); + return out; +} + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + o[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + o[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; + return o; +} + +export function setup(params) { + params.s = { + line: -1, chosen: -1, t: 0, sc: null, hue: 0, trail: 0.06, + buf: new Float32Array(W * H * 3), fire: new Float32Array(W * H * 3), c: [0, 0, 0], c2: [0, 0, 0], + }; +} + +function start(S, line, t0) { + S.line = line; + S.sc = script(POEM[line]); + S.t = t0; + S.fire.fill(0); +} + +// Cell i's block corner in LEDs at scroll offset sc (in rows). +function cellXY(S, i, scroll) { + const c = S.sc.cells[i]; + return [c.col * CELL + 1.5, TOP + (c.row - scroll + S.sc.off) * CELL + 1.5]; +} + +// The scroll, in rows, at time t: a row up whenever the cell being typed +// sits below the fifth row, eased over 0.3 s. +function scrollAt(S, t) { + let s = 0, from = 0, at = -1; + for (const e of S.sc.events) { + if (e.t > t) break; + if (e.cell < 0) continue; + const want = max(0, S.sc.cells[e.cell].row - (ROWS_SHOWN - 1)); + if (want > s) { from = s; s = want; at = e.t; } + } + return at < 0 ? s : from + (s - from) * smooth((t - at) / 0.3); +} + +// The flame: where a jumping letter is at time t, as a rect in LEDs, with +// the page scrolled as it is at time t or as given. +function flight(S, e, t, scroll) { + const u = (t - e.t) / FLY; + if (u < 0 || u >= 1) return null; + const sc = scroll == null ? scrollAt(S, t) : scroll, j = e.jump; + // From the final's slot (the cluster's second half) before the key, to + // the initial's slot in the block it lands in as that block stands now. + const sr = roleBox(S, j.from, e.t - 1e-6, sc, (p) => p.id[0] === "T" && +p.id[1] >= j.src), dr = roleBox(S, j.to, t, sc, isL); + if (!sr || !dr) return null; + const q = smooth(u), m = u * u * (3 - 2 * u); + // A hop: a quadratic arc whose top is about 10 LEDs over the higher end. + const sx = sr[0] + sr[2] / 2, sy = sr[1] + sr[3] / 2, ex = dr[0] + dr[2] / 2, ey = dr[1] + dr[3] / 2; + const cx = (sx + ex) / 2, cy = max(-8, min(sy, ey) - 20); + const x = (1 - q) * (1 - q) * sx + 2 * (1 - q) * q * cx + q * q * ex, y = (1 - q) * (1 - q) * sy + 2 * (1 - q) * q * cy + q * q * ey; + const w = sr[2] + (dr[2] - sr[2]) * m, h = sr[3] + (dr[3] - sr[3]) * m; + return { r: [x - w / 2, y - h / 2, w, h] }; +} + +// The rect in LEDs that the pieces of cell i chosen by keep fill at time t. +const isL = (p) => p.id[0] === "L"; +function roleBox(S, i, t, scroll, keep) { + const ps = layout(histAt(S.sc.cells[i].hist, t).s).filter(keep); + if (!ps.length) return null; + const [x, y] = cellXY(S, i, scroll), k = BLOCK / 20, a = ps[0].r, b = ps[ps.length - 1].r; + return [x + a[0] * k, y + a[1] * k, (b[0] + b[2] - a[0]) * k, (b[1] + b[3] - a[1]) * k]; +} + +function histAt(hist, t) { + let h = hist[0]; + for (const x of hist) { + if (x.t > t) break; + h = x; + } + return h; +} + +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] || 0; + const speed = cl(k[1] == null ? 2 : k[1], 0.2, 3); + const phrase = round(cl(k[2] || 0, 0, POEM.length - 1)); + S.trail = cl(k[3] == null ? 0.06 : k[3], 0, 1); + // The first line opens 2 s before its third jump is halfway, with most + // of its keys already struck; a line chosen later starts from its start. + if (phrase !== S.chosen) { + start(S, phrase, 0); + if (S.chosen < 0) S.t = max(0, S.sc.third + FLY / 2 - 2); + S.chosen = phrase; + } + const tau = (d * speed) / 2; + S.tau = tau; + S.t += tau; + if (S.t > S.sc.end + FADE) start(S, (S.line + 1) % POEM.length, 0); + // Sparks the flames shed: four a frame, rising 6 LEDs a second and + // fading with the trail. + const f = S.fire, fade = exp(-tau / trailTime(S)), c = S.c; + for (let i = 0; i < f.length; i++) f[i] *= fade; + S.rise = (S.rise || 0) + 6 * tau; + while (S.rise >= 1) { + S.rise -= 1; + f.copyWithin(0, W * 3); + f.fill(0, (H - 1) * W * 3); + } + for (const e of S.sc.events) { + if (!e.jump || e.t > S.t || S.t - e.t > FLY) continue; + const b = flight(S, e, S.t); + if (!b) continue; + hsv(S.hue + 0.5, 0.7, 1, c); + for (let i = 0; i < 4; i++) { + const sx = floor(b.r[0] + b.r[2] / 2 + (random() - 0.5) * 12), sy = floor(b.r[1] + b.r[3] / 2 + (random() - 0.3) * 12); + if (sx >= 0 && sx < W && sy >= 0 && sy <= TEXT_BOTTOM) put(f, sx, sy, c, 0.5 + 0.5 * random()); + } + } +} + +// The trail's time constant, in seconds at Speed 2: 0.31 s at Trail 0.06, +// 1.6 s at 0.5, 10 s at 1. A landed flame cools, and the light it throws +// on the page dies, over twice that. +const trailTime = (S) => 0.25 * 40 ** S.trail; +const coolTime = (S) => 2 * trailTime(S); + +// Max-blend colour c at level v into LED (x, y). +function put(buf, x, y, c, v) { + const i = (y * W + x) * 3; + for (let j = 0; j < 3; j++) if (buf[i + j] < c[j] * v) buf[i + j] = c[j] * v; +} + +// A soft disc of light, radius R, a at its centre. +function halo(buf, cx, cy, R, c, a) { + for (let y = max(0, floor(cy - R)); y <= min(TEXT_BOTTOM, floor(cy + R)); y++) + for (let x = max(0, floor(cx - R)); x <= min(W - 1, floor(cx + R)); x++) { + const d = sqrt((x + 0.5 - cx) ** 2 + (y + 0.5 - cy) ** 2) / R; + if (d < 1) put(buf, x, y, c, a * (1 - d) * (1 - d)); + } +} + +// A flying letter in its rect. +function stampPiece(buf, letter, fl, c, w) { + const ps = []; + split([letter], fl.r, "", ps); + for (const p of ps) strokes(buf, BASE[p.ch], p.r, w, c); +} + +// Max-blend a segment of width w into the buffer, above the keyboard. +function seg(buf, x0, y0, x1, y1, w, c) { + const r = w / 2 + 0.5; + const lx = max(0, floor(min(x0, x1) - r)), hx = min(W - 1, floor(max(x0, x1) + r)); + const ly = max(0, floor(min(y0, y1) - r)), hy = min(TEXT_BOTTOM, floor(max(y0, y1) + r)); + const dx = x1 - x0, dy = y1 - y0, L2 = dx * dx + dy * dy || 1e-9; + for (let y = ly; y <= hy; y++) + for (let x = lx; x <= hx; x++) { + const px = x + 0.5 - x0, py = y + 0.5 - y0; + const u = cl((px * dx + py * dy) / L2, 0, 1), ex = px - u * dx, ey = py - u * dy; + const f = cl(r - sqrt(ex * ex + ey * ey), 0, 1); + if (f > 0) put(buf, x, y, c, f); + } +} + +// A letter's strokes from the 20 grid into a rect in LEDs. +function strokes(buf, g, r, w, c) { + if (!g) return; + const X = (gx) => r[0] + (r[2] * gx) / 20, Y = (gy) => r[1] + (r[3] * gy) / 20; + for (const st of g) { + if (st[0] === O) { + let px = X(st[1] + st[3]), py = Y(st[2]); + for (let i = 1; i <= 16; i++) { + const a = (i / 16) * 2 * PI, nx = X(st[1] + st[3] * cos(a)), ny = Y(st[2] + st[4] * sin(a)); + seg(buf, px, py, nx, ny, w, c); + px = nx; + py = ny; + } + } else for (let i = 2; i < st.length; i += 2) seg(buf, X(st[i - 2]), Y(st[i - 1]), X(st[i]), Y(st[i + 1]), w, c); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.sc) return; + const buf = S.buf, c = S.c, c2 = S.c2, t = S.t, h = S.hue, sc = S.sc, k = BLOCK / 20; + buf.fill(0); + const out = 1 - smooth((t - sc.end) / FADE), scroll = scrollAt(S, t), cool = coolTime(S); + // The syllable being composed: the cell of the last key, until a space. + let last = null; + for (const e of sc.events) if (e.t <= t) last = e; + const cur = last && last.cell >= 0 && t < sc.end ? last.cell : -1; + if (cur >= 0) { + const [x0, y0] = cellXY(S, cur, scroll); + hsv(h + 0.5, 0.8, 0.13, c); + for (let y = max(0, floor(y0 - 1)); y <= min(TEXT_BOTTOM, floor(y0 + BLOCK)); y++) + for (let x = max(0, floor(x0 - 1)); x <= min(W - 1, floor(x0 + BLOCK)); x++) put(buf, x, y, c, 1); + } + // Jumps in the air hide their letter in the block they fly to. Landed + // ones burn there, cooling from flame to ink, in a soft disc of light + // 24 LEDs across. + // Every flame, flying or landed, lights the letters round it: a letter + // takes the flame's colour by 1.5 (1 - d / 34) of the flame's heat, up to 0.9. + const air = new Map(), glow = new Map(), flying = [], lights = []; + for (const e of sc.events) { + if (!e.jump || e.t > t) continue; + if (t < e.t + FLY) { + air.set(e.jump.to, true); + const fl = flight(S, e, t); + if (!fl) continue; + flying.push([e, fl]); + lights.push([fl.r[0] + fl.r[2] / 2, fl.r[1] + fl.r[3] / 2, out]); + } else { + const g = exp(-(t - e.t - FLY) / cool) * out; + glow.set(e.jump.to, g); + const b = roleBox(S, e.jump.to, t, scroll, isL); + if (!b || g < 0.02) continue; + const lx = b[0] + b[2] / 2, ly = b[1] + b[3] / 2; + lights.push([lx, ly, g]); + hsv(h + 0.5, 0.8, 1, c); + halo(buf, lx, ly, 12, c, 0.4 * g); + } + } + // Each flame's path stays as a soft line, 5 LEDs wide at Trail 0 and 8 + // at 1, each point fading from the moment the flame passed it. + const tt = trailTime(S); + for (const e of sc.events) { + if (!e.jump || e.t > t || exp(-(t - e.t - FLY) / tt) < 0.04) continue; + const top = min(1, (t - e.t) / FLY); + hsv(h + 0.5, 0.8, 1, c); + for (let i = 0; i <= 40; i++) { + const at = e.t + min(i / 40, top) * FLY, v = 0.8 * exp(-(t - at) / tt) * out; + const fl = flight(S, e, min(at, e.t + FLY - 1e-6), scroll); + if (fl && v > 0.03) halo(buf, fl.r[0] + fl.r[2] / 2, fl.r[1] + fl.r[3] / 2, 2.5 + 1.5 * S.trail, c, v); + if (i / 40 >= top) break; + } + } + const lightAt = (x, y) => { + let v = 0; + for (const [lx, ly, s] of lights) { + const d = 1 - sqrt((x - lx) ** 2 + (y - ly) ** 2) / 34; + if (d > 0) v += 1.5 * s * d; + } + return min(1, v); + }; + sc.cells.forEach((cell, i) => { + const hist = cell.hist; + const now = histAt(hist, t); + if (now.t < 0) return; + const [x0, y0] = cellXY(S, i, scroll); + if (y0 + BLOCK < 0) return; + let before = hist[0]; + for (const x of hist) { + if (x === now) break; + before = x; + } + const u = smooth((t - now.t) / SLIDE); + const pa = layout(before.s), pb = layout(now.s); + const composing = i === cur; + for (const p of pb) { + const old = pa.find((q) => q.id === p.id); + if (isL(p) && air.get(i)) continue; + const r = old ? p.r.map((v, j) => old.r[j] + (v - old.r[j]) * u) : p.r; + // Ink, brighter while composed; a new letter flashes white; a landed + // jumper burns in flame colour and cools. + hsv(h + 0.12, composing ? 0.25 : 0.6, (composing ? 1 : 0.9) * out, c); + const fl = old ? 0 : exp(-(t - now.t) / 0.15); + const rr = [x0 + r[0] * k, y0 + r[1] * k, r[2] * k, r[3] * k]; + const lit = 0.9 * lightAt(rr[0] + rr[2] / 2, rr[1] + rr[3] / 2); + if (lit > 0) { + hsv(h + 0.5, 0.65, out, c2); + for (let j = 0; j < 3; j++) c[j] += (c2[j] - c[j]) * lit; + } + const g = isL(p) && glow.has(i) ? glow.get(i) : 0; + if (g > 0) { + hsv(h + 0.5, 0.75, out, c2); + for (let j = 0; j < 3; j++) c[j] += (c2[j] - c[j]) * g; + } + for (let j = 0; j < 3; j++) c[j] += (out - c[j]) * fl; + strokes(buf, BASE[p.ch], rr, PUNCT.includes(p.ch) ? 2 : 1.5, c); + } + }); + // The flames in the air, crisp over their trails, each in a halo. + for (const [e, fl] of flying) { + hsv(h + 0.5, 0.8, 1, c); + halo(buf, fl.r[0] + fl.r[2] / 2, fl.r[1] + fl.r[3] / 2, 9, c, 0.45); + hsv(h + 0.5, 0.45, 1, c); + stampPiece(buf, e.jump.letter, fl, c, 1.6); + } + // The keyboard: dim caps, consonants in ink and vowels in lilac, the + // struck key and shift lit for 0.3 s, a jump's vowel in flame. + const lit = {}; + for (const e of sc.events) { + if (e.t > t || t - e.t > 0.3 || !CAP[SHIFTED[e.key] || e.key]) continue; + const base = SHIFTED[e.key] || e.key, a = 1 - (t - e.t) / 0.3; + lit[base] = { a, jump: !!e.jump }; + if (SHIFTED[e.key]) lit["⇧"] = { a, jump: false }; + } + for (const key in KEYPOS) { + const [kx, ky] = KEYPOS[key], cap = CAP[key], L = lit[key]; + const vowel = isVowel(key); + hsv(h + (vowel ? 0.78 : 0.12), 0.35, 0.34, c); + if (L) { + if (L.jump) hsv(h + 0.5, 0.75, 1, c2); + else hsv(h + 0.12, 0.1, 1, c2); + for (let j = 0; j < 3; j++) c[j] += (c2[j] - c[j]) * L.a; + } + for (let y = 0; y < 5; y++) + for (let x = 0; x < 5; x++) put(buf, kx + x, ky + y, c, (cap[y] >> (4 - x)) & 1 ? 1 : L ? 0.22 * L.a : 0); + } + const f = S.fire; + for (let p = 0, i = 0; p < W * H; p++, i += 3) + display.setPixel(p & 63, p >> 6, round(cl(max(buf[i], f[i] * out), 0, 1) * 255), round(cl(max(buf[i + 1], f[i + 1] * out), 0, 1) * 255), round(cl(max(buf[i + 2], f[i + 2] * out), 0, 1) * 255)); +} diff --git a/incoming/pattern-study/patterns/magic-lines.js b/incoming/pattern-study/patterns/magic-lines.js new file mode 100644 index 00000000..c4410021 --- /dev/null +++ b/incoming/pattern-study/patterns/magic-lines.js @@ -0,0 +1,352 @@ +// ===== Patternflow pattern ===== +// Title: Magic Lines +// Family: number +// Idea: One line runs through the cells of a magic rectangle or square in the order 1, 2, 3 and on, a step at a time, lighting each cell as it arrives, so the arithmetic comes out as an ornament with the grid's own symmetry: at the opening an 8 by 16 magic rectangle fills the panel and its line runs through 1 to 128 into a tall burst, and turned up the knob two squares stand one above the other and take turns, those up to order 5 with their numbers written into the cells. +// Author: patternflow-lab +// Date: 2026-11-03 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: magic lines (Claude Bragdon, Projective Ornament, 1915); the Siamese method for odd orders (Simon de la Loubère, 1693); Albrecht Dürer's 4 by 4 square in Melencolia I (1514); the 4 by 4 square at the Parshvanatha temple, Khajuraho (10th century); John Horton Conway's LUX method for orders 4m + 2; magic rectangles, which exist whenever the two sides have the same parity, 2 by 2 apart (Thomas Harmuth, 1881); pattern round 100 +// Round: 100 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Order=0..7, Fade=0..1 +// +// Knob 1: Hue. Turns the line's colours round the wheel. At 0 the line starts teal at the cell holding 1 and runs through azure and violet to magenta at the last cell, so the finished figure shows which way it was drawn; at 0.5 it runs from red through orange and yellow to green. +// Knob 2: Speed. At 2 the rectangle's 128 cells take 15.2 s to draw, the 3 by 3 square 3.3 s (0.41 s a number), the 4 by 4 squares 4 s and the 8 by 8 squares 8.8 s, with a rest of 3 s after a rectangle and 1.2 s after a square; every time goes as 2 / Speed. +// Knob 3: Order. 0 the 8 by 16 rectangle over the whole panel (two doubly even rectangles and two laced ones in turn), then squares two at a time: 1 the 3 by 3 Lo Shu, 2 three 4 by 4 squares in turn (Dürer's, the doubly even rule's, Khajuraho's), 3 order 5 and 5 order 7 (the Siamese square, then a knight's-step square), 4 order 6 and 7 order 10 (Conway's LUX), 6 order 8 (two doubly even squares). Each time its list comes round, the next square is turned or mirrored by the next of the square's eight symmetries, the rectangle's by the next of its four. +// Knob 4: Fade. How long each piece of line stays once drawn. At 0 and at the opening 0.06 a figure stays whole until the one that replaces it starts and then goes dark in 0.8 s; at 0.5 a piece of the rectangle's line lasts 41 steps (5 s) and a piece of the Lo Shu's 12; at 1 only the last 2.5 steps stay and the pen is a short comet. + +// A magic square of order n holds 1 to n^2 once each, and every row, +// every column and both diagonals add up to n (n^2 + 1) / 2. Bragdon drew +// a line from the centre of the cell holding 1 to the cell holding 2, then +// to 3, and on to n^2, and offered the figures as a source of ornament. +// The symmetry of the square comes out in the line: in Dürer's square the +// cells holding k and 17 - k sit opposite each other through the centre, +// so the step from k to k + 1 has a twin, from 17 - k to 16 - k, turned +// half round, and the whole figure is symmetric under the half turn. +// +// The squares here are built, except Dürer's and Khajuraho's, which are +// copied from their sources: +// +// Siamese (de la Loubère): 1 in the middle of the top row, each next +// number one up and one right, wrapping round the edges, and one down +// instead whenever that cell is taken. The line is n slanted runs joined +// by short drops, with long jumps where the wrap carries it across. +// Knight's step: each number two up and one right of the one before, +// wrapping, and after every n numbers a break move of one up and one left +// (order 5) or one up and three right (order 7). Those two squares are +// pandiagonal: the broken diagonals add up too, as Khajuraho's do. +// Doubly even rule (orders 4 and 8): write 1 to n^2 in reading order, then +// replace k by n^2 + 1 - k on the two diagonals of every 4 by 4 block. The +// second order-8 square conjugates it by the permutation 0 2 1 3 4 6 5 7 +// of rows and columns, which keeps both diagonals and moves the line. +// Dürer's is the 4 by 4 one of these with its two middle columns swapped. +// LUX (Conway, orders 6 and 10, n = 4m + 2): take the Siamese square of +// order 2m + 1, mark its first m + 1 rows L, the next U and the rest X, +// swap the middle U with the L above it, and fill each cell as a 2 by 2 +// block with 4k - 3 to 4k in the letter's order: L 4 1 / 2 3, U 1 4 / 2 3, +// X 1 4 / 3 2. +// +// The rectangle has 16 rows of 8, so every row adds up to 516 and every +// column to 1032. The doubly even rule works on it as it stands, since +// both sides are multiples of 4: k and 129 - k again sit opposite through +// the centre, and the line is a burst through the middle of the panel with +// the half-turn symmetry. The third rectangle conjugates it by 0 2 1 3 ... +// on rows and columns. The laced ones put 16 A + B + 1 in each cell, A the +// column in even rows and 7 minus the column in odd rows, B the row in +// columns 0, 2, 5 and 7 and 15 minus the row in the others, so each row +// holds every A once and each column every B once. Their line zigzags down +// between columns 0 and 7, back up between 1 and 6, and on; the fourth +// moves the columns so the four zigzags stand side by side. +// +// check() runs over every square and rectangle in setup. It asserts that +// the cells hold 1 to N once each, that every row and every column has the +// magic sum, and for squares both diagonals too; anything that failed +// would be dropped from its list. All 16 built here pass. The three +// pandiagonal squares were checked on their broken diagonals while the +// pattern was made, not at run time. + +const W = 64, H = 128; +const { floor, round, min, max, sqrt, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// ---- The constructions. A grid is an array of rows of numbers. +function siamese(n) { + const a = Array.from({ length: n }, () => new Array(n).fill(0)); + let r = 0, c = (n - 1) >> 1; + for (let k = 1; k <= n * n; k++) { + a[r][c] = k; + const nr = (r + n - 1) % n, nc = (c + 1) % n; + if (a[nr][nc]) r = (r + 1) % n; + else { r = nr; c = nc; } + } + return a; +} +// k - 1 = n A + B goes to (A p + B q) mod n from the cell of 1 at (0, c0). +function step(n, q, p, c0) { + const a = Array.from({ length: n }, () => new Array(n).fill(0)); + const m = (v) => ((v % n) + n) % n; + for (let k = 0; k < n * n; k++) { + const A = floor(k / n), B = k % n; + a[m(p[0] * A + q[0] * B)][m(c0 + p[1] * A + q[1] * B)] = k + 1; + } + return a; +} +// n columns, m rows; rp and cp permute the rows and columns read. +function doublyEven(n, m, rp, cp) { + const a = []; + for (let r = 0; r < m; r++) { + a.push([]); + for (let c = 0; c < n; c++) { + const R = rp ? rp[r] : r, C = cp ? cp[c] : c, k = R * n + C + 1, i = R % 4, j = C % 4; + a[r].push(i === j || i + j === 3 ? n * m + 1 - k : k); + } + } + return a; +} +function lux(n) { + const m = (n - 2) / 4, h = 2 * m + 1, s = siamese(h); + const L = [4, 1, 2, 3], U = [1, 4, 2, 3], X = [1, 4, 3, 2]; + const a = Array.from({ length: n }, () => new Array(n).fill(0)); + for (let r = 0; r < h; r++) + for (let c = 0; c < h; c++) { + let t = r <= m ? L : r === m + 1 ? U : X; + if (c === m && r === m) t = U; + if (c === m && r === m + 1) t = L; + const base = 4 * (s[r][c] - 1); + for (let i = 0; i < 4; i++) a[2 * r + (i >> 1)][2 * c + (i & 1)] = base + t[i]; + } + return a; +} +// Column j of the plain rectangle goes to column at[j]. +function rectangle(S, at) { + const a = []; + for (let i = 0; i < 16; i++) { + a.push(new Array(8)); + for (let j = 0; j < 8; j++) a[i][at[j]] = 16 * (i % 2 ? 7 - j : j) + (S.includes(j) ? i : 15 - i) + 1; + } + return a; +} +const DURER = [[16, 3, 2, 13], [5, 10, 11, 8], [9, 6, 7, 12], [4, 15, 14, 1]]; +const KHAJURAHO = [[7, 12, 1, 14], [2, 13, 8, 11], [16, 3, 10, 5], [9, 6, 15, 4]]; + +// Rows, columns, the numbers 1..N once each, and for a square both diagonals. +function check(a) { + const R = a.length, C = a[0].length, N = R * C, seen = new Uint8Array(N + 1); + for (const row of a) for (const v of row) { + if (v < 1 || v > N || seen[v]) return false; + seen[v] = 1; + } + const rs = (C * (N + 1)) / 2, cs = (R * (N + 1)) / 2; + for (const row of a) if (row.reduce((x, y) => x + y, 0) !== rs) return false; + for (let c = 0; c < C; c++) { + let t = 0; + for (let r = 0; r < R; r++) t += a[r][c]; + if (t !== cs) return false; + } + if (R === C) { + let d1 = 0, d2 = 0; + for (let i = 0; i < R; i++) { d1 += a[i][i]; d2 += a[i][R - 1 - i]; } + if (d1 !== rs || d2 !== rs) return false; + } + return true; +} + +const P8 = [0, 2, 1, 3, 4, 6, 5, 7], P16 = [0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11, 12, 14, 13, 15]; +function orders() { + const list = [ + [doublyEven(8, 16), rectangle([0, 7, 2, 5], [0, 1, 2, 3, 4, 5, 6, 7]), doublyEven(8, 16, P16, P8), rectangle([0, 7, 2, 5], [0, 2, 4, 6, 7, 5, 3, 1])], + [siamese(3)], + [DURER, doublyEven(4, 4), KHAJURAHO], + [siamese(5), step(5, [-2, 1], [1, 3], 2)], + [lux(6)], + [siamese(7), step(7, [-2, 1], [1, 2], 3)], + [doublyEven(8, 8), doublyEven(8, 8, P8, P8)], + [lux(10)], + ]; + return list.map((l) => l.filter(check)); +} + +// The eight symmetries of the square, applied to cell (r, c) of order n. +const SYM = [ + (r, c) => [r, c], (r, c, n) => [r, n - 1 - c], (r, c, n) => [n - 1 - r, n - 1 - c], (r, c, n) => [n - 1 - r, c], + (r, c) => [c, r], (r, c, n) => [c, n - 1 - r], (r, c, n) => [n - 1 - c, n - 1 - r], (r, c, n) => [n - 1 - c, r], +]; + +// Seconds at Speed 2: drawing takes 2.4 + 0.1 N, then a rest, longer for +// the rectangle, which has no twin to look at meanwhile. +const REST = 1.2, REST_RECT = 3, LIFE_MIN = 2.5; + +// 3 by 5 figures, rows top to bottom, 4 is the left column. +const DIG = [[7, 5, 5, 5, 7], [2, 6, 2, 2, 7], [7, 1, 7, 4, 7], [7, 1, 7, 1, 7], [5, 5, 7, 1, 1], [7, 4, 7, 1, 7], [7, 4, 7, 5, 7], [7, 1, 2, 2, 2], [7, 5, 7, 5, 7], [7, 5, 7, 1, 7]]; + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - s * f), t = v * (1 - s * (1 - f)); + o[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + o[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + o[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; + return o; +} + +export function setup(params) { + params.s = { lists: orders(), order: -1, g: 0, figs: [], next: 0, hue: 0, fade: 0.06, buf: new Float32Array(W * H * 3), c: [0, 0, 0] }; +} + +// Figure s of the current order: which grid, which symmetry, where. The +// rectangle turns through its four symmetries, a square through eight. +function figure(S, s, B) { + const list = S.lists[S.order], grid = list[s % list.length], R = grid.length, C = grid[0].length, N = R * C; + const rect = R !== C, sym = SYM[(floor(s / list.length) % (rect ? 4 : 8))]; + const frame = rect ? 0 : s % 2; + // Squares sit in 60 by 60 inside each 64 by 64 half; the rectangle's + // cells are 8 LEDs. + const cell = rect ? 8 : 60 / R, x0 = rect ? 0 : 2, y0 = rect ? 0 : 2 + 64 * frame; + const pos = new Float32Array(2 * N), box = []; + for (let r = 0; r < R; r++) + for (let c = 0; c < C; c++) { + const [rr, cc] = rect ? [sym(r, 0, R)[0], sym(0, c, C)[1]] : sym(r, c, R); + const k = grid[r][c] - 1; + pos[2 * k] = x0 + (cc + 0.5) * cell; + pos[2 * k + 1] = y0 + (rr + 0.5) * cell; + box[k] = [x0 + cc * cell, y0 + rr * cell]; + } + return { s, B, N, R, pos, box, cell, frame, end: B + N - 1 }; +} + +function begin(S, order) { + S.order = order; + const g0 = S.lists[order][0], N = g0.length * g0[0].length; + S.N = N; + S.R = g0.length; + S.twin = g0.length === g0[0].length; + S.stepSec = (2.4 + 0.1 * N) / (N - 1); + S.rest = (S.twin ? REST : REST_RECT) / S.stepSec; + // Open with the first square finished above and the second a third + // drawn below, or the rectangle with its first 15 steps drawn. + S.figs = [figure(S, 0, 0)]; + S.next = 1; + if (S.twin) { + const a = S.figs[0]; + S.figs.push(figure(S, S.next++, a.end + S.rest)); + S.g = a.end + S.rest + 0.35 * (N - 1); + } else S.g = 0.12 * (N - 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] || 0; + const speed = cl(k[1] == null ? 2 : k[1], 0.2, 3); + const order = round(cl(k[2] || 0, 0, 7)); + S.fade = cl(k[3] == null ? 0.06 : k[3], 0, 1); + if (order !== S.order) begin(S, order); + S.g += (d * speed) / 2 / S.stepSec; + const last = S.figs[S.figs.length - 1]; + if (S.g >= last.end + S.rest) { + S.figs.push(figure(S, S.next++, last.end + S.rest)); + while (S.figs.length > 3) S.figs.shift(); + } +} + +// Max-blend a disc or a thick segment into the buffer. +function seg(buf, x0, y0, x1, y1, w, c) { + const r = w / 2 + 0.5; + const lx = max(0, floor(min(x0, x1) - r)), hx = min(W - 1, floor(max(x0, x1) + r)); + const ly = max(0, floor(min(y0, y1) - r)), hy = min(H - 1, floor(max(y0, y1) + r)); + const dx = x1 - x0, dy = y1 - y0, L2 = dx * dx + dy * dy || 1e-9; + for (let y = ly; y <= hy; y++) + for (let x = lx; x <= hx; x++) { + const px = x + 0.5 - x0, py = y + 0.5 - y0; + const u = cl((px * dx + py * dy) / L2, 0, 1), ex = px - u * dx, ey = py - u * dy; + const f = cl(r - sqrt(ex * ex + ey * ey), 0, 1); + if (f <= 0) continue; + const i = (y * W + x) * 3; + if (buf[i] < c[0] * f) buf[i] = c[0] * f; + if (buf[i + 1] < c[1] * f) buf[i + 1] = c[1] * f; + if (buf[i + 2] < c[2] * f) buf[i + 2] = c[2] * f; + } +} +function put(buf, x, y, c, a) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const i = (y * W + x) * 3; + for (let j = 0; j < 3; j++) if (buf[i + j] < c[j] * a) buf[i + j] = c[j] * a; +} +// A number with its top left corner at (x, y). +function number(buf, v, x, y, sc, c, a) { + const s = String(v), x0 = round(x), y0 = round(y); + for (let d = 0; d < s.length; d++) { + const g = DIG[s.charCodeAt(d) - 48]; + for (let ry = 0; ry < 5 * sc; ry++) + for (let rx = 0; rx < 3 * sc; rx++) if (g[floor(ry / sc)] & (4 >> floor(rx / sc))) put(buf, x0 + d * 4 * sc + rx, y0 + ry, c, a); + } +} + +const smooth = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +export function draw(display, params) { + const S = params.s; + if (!S || S.order < 0) return; + const buf = S.buf, c = S.c, g = S.g, N = S.N; + buf.fill(0); + // A piece of line lives life steps after it is drawn and dims over the + // last ramp of them: at Fade 0 a quarter longer than it takes the square + // that replaces it to start, 2.5 steps at 1. + const span = 1.25 * ((S.twin ? 2 : 1) * (N - 1 + S.rest) + 2), out = max(2, 0.8 / S.stepSec); + const life = span * (LIFE_MIN / span) ** (S.fade ** 1.5), ramp = life * (0.1 + 0.5 * S.fade); + const wgt = (age) => (age < 0 ? 0 : cl((life - age) / ramp, 0, 1)); + const lineW = S.twin ? [0, 0, 0, 3, 2.6, 2.2, 2, 1.9, 1.8, 1.7, 1.5][S.R] : 1.5; + const digits = S.twin && S.R <= 5, sc = S.R <= 3 ? 2 : 1; + for (const F of S.figs) { + // A figure goes dark in 0.8 s (2 steps at least) once the next one + // starts in its frame. + let gone = 1; + for (const G of S.figs) if (G.s > F.s && G.frame === F.frame) gone = min(gone, 1 - cl((g - G.B) / out, 0, 1)); + if (gone <= 0) continue; + const now = g - F.B; + // Lattice dots while the square is drawing. + if (now < N) { + const a = 0.16 * gone; + for (let k = 0; k < N; k++) { + hsv(S.hue + 0.5 + (0.32 * k) / (N - 1), 0.5, 1, c); + put(buf, floor(F.pos[2 * k]), floor(F.pos[2 * k + 1]), c, a); + } + } + // Cells light as the pen reaches them, and a square's keep a faint fill. + for (let k = 0; k < N; k++) { + const age = now - k; + if (age < 0) break; + const v = wgt(age) * gone; + if (v <= 0) continue; + const flash = exp(-age / 1.2); + hsv(S.hue + 0.5 + (0.32 * k) / (N - 1), 0.85, 1, c); + const a = ((S.twin ? 0.1 : 0) + 0.32 * flash) * v, cs = F.cell, bx = F.box[k][0], by = F.box[k][1]; + for (let y = round(by) + 1; y < round(by + cs); y++) for (let x = round(bx) + 1; x < round(bx + cs); x++) put(buf, x, y, c, a); + if (digits) { + hsv(S.hue + 0.5 + (0.32 * k) / (N - 1), 0.35, 1, c); + number(buf, k + 1, bx + 2, by + 2, sc, c, (0.35 + 0.5 * flash) * v); + } + } + // The line, segment k from cell k + 1 to cell k + 2. + for (let k = 0; k < N - 1; k++) { + const age = now - k - 1; + if (age < -1) break; + const x0 = F.pos[2 * k], y0 = F.pos[2 * k + 1]; + let x1 = F.pos[2 * k + 2], y1 = F.pos[2 * k + 3]; + if (age < 0) { + const u = smooth(age + 1); + x1 = x0 + (x1 - x0) * u; + y1 = y0 + (y1 - y0) * u; + hsv(S.hue + 0.5 + (0.32 * k) / (N - 1), 0.2, 1, c); + seg(buf, x1, y1, x1, y1, lineW * 1.9, c); + } + // The newest pieces glow pale and settle to their colour in 1.5 steps. + const fresh = age < 0 ? 1 : exp(-age / 1.5), v = (age < 0 ? 1 : wgt(age)) * gone * (0.78 + 0.22 * fresh); + if (v <= 0) continue; + hsv(S.hue + 0.5 + (0.32 * (k + 0.5)) / (N - 1), 0.8 - 0.5 * fresh, v, c); + seg(buf, x0, y0, x1, y1, lineW, c); + } + } + for (let p = 0, i = 0; p < W * H; p++, i += 3) + display.setPixel(p & 63, p >> 6, round(cl(buf[i], 0, 1) * 255), round(cl(buf[i + 1], 0, 1) * 255), round(cl(buf[i + 2], 0, 1) * 255)); +} From ba85343d473391926d04a89beb4d1bb6185673a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 04:04:05 +0000 Subject: [PATCH 6/8] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20100=ED=8C=90:=20Desarg?= =?UTF-8?q?ues=20Configuration=20=C2=B7=20Zipf=20Words?= 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_01Bc4JkrYAzT3TWt21dU8pXk --- incoming/pattern-study/patterns/desargues.js | 309 +++++++++++++++++ incoming/pattern-study/patterns/zipf-words.js | 317 ++++++++++++++++++ 2 files changed, 626 insertions(+) create mode 100644 incoming/pattern-study/patterns/desargues.js create mode 100644 incoming/pattern-study/patterns/zipf-words.js diff --git a/incoming/pattern-study/patterns/desargues.js b/incoming/pattern-study/patterns/desargues.js new file mode 100644 index 00000000..bf779326 --- /dev/null +++ b/incoming/pattern-study/patterns/desargues.js @@ -0,0 +1,309 @@ +// ===== Patternflow pattern ===== +// Title: Desargues Configuration +// Family: space +// Idea: A three-sided pyramid hangs from its apex O at the top of the panel and two planes cut it in a cyan and a gold triangle, the scene turns slowly in space, and the sides of the two triangles, carried on past their corners, meet in pairs at three white points that always stand on one pink line, the line where the two planes cross, so every frame is Desargues's flat theorem drawn by the solid one. +// Author: patternflow-lab +// Date: 2026-11-03 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Desargues's theorem on two triangles in perspective from a point (Girard Desargues; first printed by Abraham Bosse in Manière universelle de M. Desargues, 1648) and its configuration of ten points and ten lines, three points on every line and three lines through every point (10_3), seen in space as two plane sections of one pyramid; pattern round 100 +// Round: 100 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Tilt=30..56, View=-1..1 +// +// Knob 1: Hue. Turns the colours round the wheel together. At 0 the pyramid's edges are periwinkle, the upper triangle and its plane cyan, the lower triangle and its plane gold, the axis pink and the points white; at 0.5 the triangles are red and blue and the axis green. +// Knob 2: Speed. At 2 the scene swings 25 degrees each way about the vertical through its middle and back in 12 s, and a light runs out along each pair of carried-on sides to its point on the axis every 2.4 s; every time goes as 2 / Speed. +// Knob 3: Tilt. The angle between the two cutting planes, which turn about the axis in opposite directions: 30 degrees at the opening, where the triangles stand one above the other, and 56 at the top, where the cyan one climbs into a narrow sliver near O and the gold one slides down the pyramid. The three points on the axis do not move at all, since each is where a face of the pyramid meets the axis. +// Knob 4: View. The camera's height, as the angle it looks down: 25 + 25 x View degrees, so 26.5 at the opening 0.06. At -1 it looks level, and near one end of each swing the eye comes close to the gold plane and the gold triangle thins to a sliver; at 1 it looks down at 50 degrees and the figure stands shorter and wider. + +// In the plane the theorem says: if the lines AA', BB' and CC' through the +// corresponding corners of two triangles meet in one point O, the three +// points where AB meets A'B', BC meets B'C' and CA meets C'A' lie on one +// line, the axis. Desargues worked it out in space, where it needs no +// proof at all. Take a pyramid with apex O and three edges, and cut it with +// two planes: one cut is the triangle ABC, the other A'B'C', and the lines +// AA', BB', CC' are the edges, so they meet at O. The sides AB and A'B' lie +// in the same face OAB of the pyramid, so they meet; AB lies in the first +// plane and A'B' in the second, so they meet on both, and two planes meet +// in one line. The same holds for the other two pairs of sides. Ten points +// (O, six corners, three meeting points) and ten lines (three edges, six +// sides, the axis), with three points on each line and three lines through +// each point: the configuration (10_3). +// +// The scene is built round its axis. The axis is a fixed line in space, +// sloping down and away; the two cutting planes are the planes through it +// at THETA + Tilt / 2 and THETA - Tilt / 2 about it. Each meeting point is +// where the plane of one face meets the axis, worked out from the face's +// normal, so it depends on the pyramid and the axis alone, and Tilt slides +// the triangles along the edges while the three points stay where they are. +// The pyramid, the axis and THETA were picked by a search that asked, for +// every swing, height and tilt the knobs reach, that both triangles stay +// wide, that the three points sit well apart, and that the axis passes +// below the triangles. +// +// A central projection keeps a line a line and a meeting point a meeting +// point, so whatever the camera does, the picture on the panel is the flat +// theorem: two triangles in perspective from the image of O, their sides +// meeting on the image of the axis. The space picture is what makes the +// flat one true, and the swing shows both at once. +// +// Drawing, from the back: each cutting plane as a faint sheet, the hull of +// its triangle and the three points on the axis; all ten lines of the +// configuration faint and whole across the panel; the axis 2 LEDs wide; the +// carried-on sides from the corner nearest each point out to it; the +// triangles filled at a third of their colour; the edges from O 2 LEDs +// wide; the triangle sides 2.6 LEDs; and the points as discs. Lines and +// discs are anti-aliased by their distance from each LED's centre, and +// every line is darker where it lies farther from the camera. + +const W = 64, H = 128; +const { floor, ceil, min, max, sin, cos, sqrt, exp, round, hypot, PI } = Math; +const D2R = PI / 180; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The pyramid: apex O above three feet on the ground, at these bearings +// (degrees) and distances from the vertical through O. +const OH = 3.021; +const FA = [-8.52, 91.4, 40.77], FR = [1.383, 0.7527, 1.0775]; +// The axis: through Q, at bearing LA and slope LB (degrees). The planes sit +// at THETA +- Tilt / 2 about it, measured from the side that faces O. +const QX = 0.1968, QY = -1.4095, QZ = 1.7674, LA = 39.04, LB = -46.14; +const THETA = 24.92; +// The swing each way (degrees), the camera distance, and the Tilt range. +const SWING = 25, DC = 11.62, TMIN = 30, TMAX = 56; + +const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; +const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; +const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +const nrm = (a) => { + const l = hypot(a[0], a[1], a[2]) || 1; + return [a[0] / l, a[1] / l, a[2] / l]; +}; + +const O = [0, OH, 0]; +const FOOT = FA.map((a, i) => [FR[i] * cos(a * D2R), 0, FR[i] * sin(a * D2R)]); +const Q = [QX, QY, QZ]; +const DIR = [cos(LA * D2R) * cos(LB * D2R), sin(LB * D2R), sin(LA * D2R) * cos(LB * D2R)]; + +// The three meeting points: the plane of face O, foot i, foot j meets the +// axis where its normal m gives m . (Q + k DIR - O) = 0. +const MEET = [[0, 1], [1, 2], [2, 0]].map(([i, j]) => { + const m = cross(sub(FOOT[i], O), sub(FOOT[j], O)), k = dot(m, sub(O, Q)) / dot(m, DIR); + return [Q[0] + k * DIR[0], Q[1] + k * DIR[1], Q[2] + k * DIR[2]]; +}); + +// The two triangles for a tilt: each plane holds the axis and the direction +// h cos(th) + v sin(th), h pointing from the axis towards O across it. +function triangles(tilt) { + let h = nrm(cross([0, 1, 0], DIR)); + if (dot(h, sub(O, Q)) < 0) h = h.map((x) => -x); + let v = nrm(cross(DIR, h)); + if (v[1] < 0) v = v.map((x) => -x); + return [THETA + tilt / 2, THETA - tilt / 2].map((th) => { + const c = cos(th * D2R), s = sin(th * D2R); + const n = cross(DIR, [h[0] * c + v[0] * s, h[1] * c + v[1] * s, h[2] * c + v[2] * s]); + return FOOT.map((f) => { + const d = sub(f, O), t = dot(n, sub(Q, O)) / dot(n, d); + return [O[0] + t * d[0], O[1] + t * d[1], O[2] + t * d[2]]; + }); + }); +} + +// Camera: turn by yaw about the vertical through CEN, look down by el, then +// a pinhole at distance DC. Returns screen x, y (up) and depth toward the eye. +const CEN = [0, (OH + QY) / 2, QZ / 2]; +function cam(p, yaw, el) { + const x = p[0] - CEN[0], y = p[1] - CEN[1], z = p[2] - CEN[2]; + const cy = cos(yaw), sy = sin(yaw), ce = cos(el), se = sin(el); + const x1 = x * cy + z * sy, z1 = -x * sy + z * cy; + const y2 = y * ce - z1 * se, z2 = y * se + z1 * ce, s = DC / (DC - z2); + return [x1 * s, y2 * s, z2]; +} + +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 - s * f), t = v * (1 - s * (1 - f)); + return [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; +} + +// Keep the brighter of what is there and colour c at strength a. +function put(buf, x, y, c, a) { + const p = (y * W + x) * 3; + if (c[0] * a > buf[p]) buf[p] = c[0] * a; + if (c[1] * a > buf[p + 1]) buf[p + 1] = c[1] * a; + if (c[2] * a > buf[p + 2]) buf[p + 2] = c[2] * a; +} + +// A segment wid LEDs wide, strength a0 at its start and a1 at its end, and +// with g in 0..1 a light 2.5 LEDs long running along it at that point. +function seg(buf, ax, ay, bx, by, wid, c, a0, a1, g) { + const r = wid / 2 + 0.5, dx = bx - ax, dy = by - ay, L2 = dx * dx + dy * dy || 1e-9, len = sqrt(L2); + const x0 = max(0, floor(min(ax, bx) - r)), x1 = min(W - 1, ceil(max(ax, bx) + r)); + const y0 = max(0, floor(min(ay, by) - r)), y1 = min(H - 1, ceil(max(ay, by) + r)); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const px = x + 0.5 - ax, py = y + 0.5 - ay, t = cl((px * dx + py * dy) / L2, 0, 1); + const ex = px - t * dx, ey = py - t * dy, cov = cl(wid / 2 + 0.5 - sqrt(ex * ex + ey * ey), 0, 1); + if (cov <= 0) continue; + let a = a0 + (a1 - a0) * t; + if (g >= 0) a = min(1, a + 0.75 * exp(-((((t - g) * len) / 2.5) ** 2))); + put(buf, x, y, c, cov * a); + } +} + +// The whole line through a and b, across the panel. +function line(buf, a, b, wid, c, al) { + const ux = b[0] - a[0], uy = b[1] - a[1], n = hypot(ux, uy) || 1, L = 300; + seg(buf, a[0] - (ux / n) * L, a[1] - (uy / n) * L, a[0] + (ux / n) * L, a[1] + (uy / n) * L, wid, c, al, al, -1); +} + +function disc(buf, x, y, rad, c, a) { + for (let yy = max(0, floor(y - rad - 1)); yy <= min(H - 1, floor(y + rad + 1)); yy++) + for (let xx = max(0, floor(x - rad - 1)); xx <= min(W - 1, floor(x + rad + 1)); xx++) { + const cov = cl(rad + 0.5 - hypot(xx + 0.5 - x, yy + 0.5 - y), 0, 1); + if (cov > 0) put(buf, xx, yy, c, cov * a); + } +} + +// Convex hull of screen points, monotone chain. +function hull(pts) { + const p = pts.slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]); + const turn = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); + const lo = [], hi = []; + for (const q of p) { + while (lo.length >= 2 && turn(lo[lo.length - 2], lo[lo.length - 1], q) <= 0) lo.pop(); + lo.push(q); + } + for (let i = p.length - 1; i >= 0; i--) { + while (hi.length >= 2 && turn(hi[hi.length - 2], hi[hi.length - 1], p[i]) <= 0) hi.pop(); + hi.push(p[i]); + } + return lo.slice(0, -1).concat(hi.slice(0, -1)); +} + +// Fill a convex polygon with colour c at strength a and a soft edge one LED +// wide; add puts the colour on top of what is there instead of keeping the +// brighter, so the two sheets mix where they overlap. +function poly(buf, P, c, a, add) { + let x0 = W, x1 = 0, y0 = H, y1 = 0, area = 0; + const n = P.length, ex = [], ey = [], el = []; + for (let i = 0; i < n; i++) { + const A = P[i], B = P[(i + 1) % n]; + x0 = min(x0, A[0]); x1 = max(x1, A[0]); y0 = min(y0, A[1]); y1 = max(y1, A[1]); + ex.push(B[0] - A[0]); ey.push(B[1] - A[1]); el.push(hypot(B[0] - A[0], B[1] - A[1]) || 1); + area += A[0] * B[1] - B[0] * A[1]; + } + const sgn = area < 0 ? -1 : 1; + x0 = max(0, floor(x0)); x1 = min(W - 1, ceil(x1)); y0 = max(0, floor(y0)); y1 = min(H - 1, ceil(y1)); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + let cov = 1; + for (let i = 0; i < n && cov > 0; i++) + cov = min(cov, cl((sgn * (ex[i] * (y + 0.5 - P[i][1]) - ey[i] * (x + 0.5 - P[i][0]))) / el[i] + 0.5, 0, 1)); + if (cov <= 0) continue; + if (!add) put(buf, x, y, c, cov * a); + else { + const p = (y * W + x) * 3; + buf[p] += c[0] * a * cov; + buf[p + 1] += c[1] * a * cov; + buf[p + 2] += c[2] * a * cov; + } + } +} + +export function setup(params) { + params.s = { buf: new Float32Array(W * H * 3), swing: 0, run: 0, hue: 0, tilt: TMIN, view: 0.06, fitView: null, fit: null }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, TMIN, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + const speed = cl(k[1] == null ? 2 : k[1], 0.2, 3); + S.tilt = cl(k[2] == null ? TMIN : k[2], TMIN, TMAX); + S.view = cl(k[3] == null ? 0.06 : k[3], -1, 1); + S.swing = (S.swing + (d * speed * 2 * PI) / 24) % (2 * PI); + S.run = (S.run + (d * speed) / 4.8) % 1; +} + +// Scale and centre so that every swing and every tilt fits columns 3 to 61 +// and rows 5 to 123. It is worked out again only when View moves, so +// turning Tilt leaves the three points where they are on the panel too. +function fit(S, el) { + if (S.fitView === S.view) return S.fit; + let x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9; + for (const tilt of [TMIN, (TMIN + TMAX) / 2, TMAX]) { + const T = triangles(tilt); + for (let i = 0; i <= 12; i++) { + const yaw = SWING * D2R * (i / 6 - 1); + for (const p of [O, ...T[0], ...T[1], ...MEET]) { + const q = cam(p, yaw, el); + x0 = min(x0, q[0]); x1 = max(x1, q[0]); y0 = min(y0, q[1]); y1 = max(y1, q[1]); + } + } + } + const K = min(58 / (x1 - x0), 118 / (y1 - y0)); + S.fit = { K, cx: 32 - (K * (x0 + x1)) / 2, cy: 64 + (K * (y0 + y1)) / 2 }; + S.fitView = S.view; + return S.fit; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const buf = S.buf; + buf.fill(0); + const el = (25 + 25 * S.view) * D2R, yaw = SWING * D2R * sin(S.swing), F = fit(S, el); + // Screen point, and how lit its depth leaves it: 1 near, 0.45 far. + const P = (p) => { + const q = cam(p, yaw, el); + return [F.cx + F.K * q[0], F.cy - F.K * q[1], cl(0.74 + 0.3 * q[2], 0.45, 1)]; + }; + const tri = triangles(S.tilt); + const o = P(O), T1 = tri[0].map(P), T2 = tri[1].map(P), M = MEET.map(P); + const c1 = hsv(S.hue + 0.5, 0.75, 1), c2 = hsv(S.hue + 0.14, 0.8, 1), ca = hsv(S.hue + 0.88, 0.5, 1); + const ce = hsv(S.hue + 0.64, 0.4, 1), white = [1, 1, 1]; + + poly(buf, hull([...T1, ...M]), c1, 0.13, true); + poly(buf, hull([...T2, ...M]), c2, 0.12, true); + for (let i = 0; i < 3; i++) { + line(buf, o, T2[i], 1, ce, 0.18); + line(buf, T1[i], T1[(i + 1) % 3], 1, c1, 0.18); + line(buf, T2[i], T2[(i + 1) % 3], 1, c2, 0.18); + } + line(buf, M[0], M[2], 2, ca, 0.85); + + // The carried-on sides: AB and A'B' run out to the first point, BC and + // B'C' to the second, CA and C'A' to the third, each pair lit together, + // and the point swells as the light reaches it. + const glow = [0, 0, 0]; + for (let m = 0; m < 3; m++) { + const i = m, j = (m + 1) % 3, X = M[m], g = (S.run + m / 3) % 1; + glow[m] = exp(-(((1 - g) * 10) ** 2)) + exp(-((g * 10) ** 2)); + for (const [T, c] of [[T1, c1], [T2, c2]]) { + const N = hypot(T[i][0] - X[0], T[i][1] - X[1]) < hypot(T[j][0] - X[0], T[j][1] - X[1]) ? T[i] : T[j]; + seg(buf, N[0], N[1], X[0], X[1], 1.6, c, 0.7 * N[2], 0.7 * X[2], g); + } + } + poly(buf, T1, c1, 0.32, false); + poly(buf, T2, c2, 0.3, false); + // Each edge from O runs to the farther of its two corners. + for (let i = 0; i < 3; i++) { + const a = T1[i], b = T2[i]; + const f = hypot(a[0] - o[0], a[1] - o[1]) > hypot(b[0] - o[0], b[1] - o[1]) ? a : b; + seg(buf, o[0], o[1], f[0], f[1], 2, ce, 0.85 * o[2], 0.85 * f[2], -1); + } + for (const [T, c] of [[T1, c1], [T2, c2]]) + for (let i = 0; i < 3; i++) { + const a = T[i], b = T[(i + 1) % 3]; + seg(buf, a[0], a[1], b[0], b[1], 2.6, c, a[2], b[2], -1); + } + for (const T of [T1, T2]) for (const q of T) disc(buf, q[0], q[1], 1.2, white, 0.55 + 0.45 * q[2]); + disc(buf, o[0], o[1], 2.2, white, 1); + for (let m = 0; m < 3; m++) disc(buf, M[m][0], M[m][1], 2.3 + 0.9 * glow[m], white, 1); + + for (let i = 0; i < W * H; i++) { + const p = i * 3; + display.setPixel(i % W, floor(i / W), round(cl(buf[p], 0, 1) * 255), round(cl(buf[p + 1], 0, 1) * 255), round(cl(buf[p + 2], 0, 1) * 255)); + } +} diff --git a/incoming/pattern-study/patterns/zipf-words.js b/incoming/pattern-study/patterns/zipf-words.js new file mode 100644 index 00000000..1f9ad40f --- /dev/null +++ b/incoming/pattern-study/patterns/zipf-words.js @@ -0,0 +1,317 @@ +// ===== Patternflow pattern ===== +// Title: Zipf Words +// Family: type +// Idea: The Gettysburg Address is read one word at a time in tall letters at the top of the panel, every word joins a tally, and the 13 most counted words stand ranked below it, each row the word written over and over in 3 by 5 capitals for as long as its count, so the bar chart of the speech is woven from the speech's own words, with Zipf's 1 / rank as a gold curve behind the rows and a log-log plot of every word at the foot that prints the slope as it is read. +// Author: patternflow-lab +// Date: 2026-11-03 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Zipf's law, a word's count falling about as 1 / its rank (George Kingsley Zipf, The Psycho-Biology of Language, 1935; seen before him in French shorthand by Jean-Baptiste Estoup, Gammes sténographiques, 1916); texts the Gettysburg Address (Abraham Lincoln, 1863, the Bliss copy), Genesis 1:1-5 and Psalm 23 in the King James Version (1611), all public domain; 3 by 5 capitals drawn for this pattern; pattern round 100 +// Round: 100 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Text=0..2, Power=0..1 +// +// Knob 1: Hue. Turns the colours round the wheel together. At 0 each word keeps its own colour from sea green through azure and blue to violet, set by the order the words first came in, and the 1 / rank curve and the progress line are pale gold; at 0.5 the words run from red through orange to yellow-green and the curve is pale blue. +// Knob 2: Speed. Words read a second, twice the knob: 4 at 2, so the 271 words of the address take 68 s; 0.4 at 0.2 and 6 at 3. At the end the table holds for ten words' time with the source in the top line, fades and starts again from the first word. +// Knob 3: Text. In whole steps: 0 the Gettysburg Address, 271 words, 138 of them different, THAT the most with 13; 1 Genesis 1:1-5, 89 words, 39 different, THE with 17; 2 Psalm 23, 118 words, 74 different, THE with 9. Each opens 40% of the way in, with the table already filled. +// Knob 4: Power. Each bar is count x rank^Power long, the longest of the 13 rows 60 LEDs. At the opening 0.06 the bars are nearly the plain counts and fall away down the rows; at 1 they are count x rank, which Zipf's law says is the same for every rank, so the bars' ends would stand in a straight edge on the gold curve, which then stands straight up. Genesis 1:1-5 comes close: at its end count x rank runs from 16 to 26 down the 13 rows and every bar ends between 37 and 60 LEDs. In the address it climbs from 13 to 65, so the bars lengthen down the table. + +// A word's rank is its place when the words are sorted by how often they +// came, most first, ties in the order the words first came. In long +// counts of words Zipf found the count of the word of rank r close to +// C / r, so that count x rank stays near C; Estoup had seen the same in +// French for his shorthand in 1916. In a text this +// short it holds only roughly. The fitted slope of log count against log +// rank, over every word seen so far, steepens as the text goes on and ends +// at -0.65 for the address, -0.80 for Genesis 1:1-5 and -0.54 for the +// psalm; Zipf's law is -1. +// +// The table. Row r, on rows 17 + 7 (r - 1) to 21 + 7 (r - 1), holds the +// word of rank r, its letters repeated without a gap from the left edge +// for as many 4-LED letters as its bar is long: THAT, counted 7 times at +// the opening, has a bar of 56 LEDs and reads THATTHATTHATTH. Its last +// letter brightens as the count grows into it. A word whose bar is shorter +// than itself still shows whole, the rest of its letters at 30%, so +// CONCEIVED, counted twice, reads CONC bright and the rest dim. When a +// word overtakes another the two rows +// slide past each other in a quarter of a second, the moving one covering +// the other. The bar scale is 60 LEDs for the longest of the 13 rows (8 +// LEDs a count while the top count is under 7.5), so the rows keep the +// width whatever has been read. The gold curve behind the letters is where +// each bar would end if the count of rank r were exactly the top count / r. +// +// The inset at the foot puts every word seen, all 138 in the address, at +// x = log rank and y = log count, on axes fixed to the whole text, so the +// dots climb as the words come. The gold line has slope -1 through the top +// word; the white one is the least-squares line through all the dots, and +// its slope is printed in the corner. The word just read flashes white in +// its row and in the inset, and stands in letters 6 by 10 at the top (3 by +// 10 for a word longer than nine letters), with a gold line under it that +// fills as the text goes. + +const W = 64, H = 128; +const { floor, round, min, max, log, exp, abs, sqrt } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const PHI = 0.6180339887; + +// The three texts as printed, read as words of letters joined by inner +// hyphens and apostrophes, so battle-field and name's are one word each. +const TEXTS = [ + "Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal. Now we are engaged in a great civil war, testing whether that nation, or any nation so conceived and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting place for those who here gave their lives that that nation might live. It is altogether fitting and proper that we should do this. But, in a larger sense, we can not dedicate -- we can not consecrate -- we can not hallow -- this ground. The brave men, living and dead, who struggled here, have consecrated it, far above our poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living, rather, to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us -- that from these honored dead we take increased devotion to that cause for which they gave the last full measure of devotion -- that we here highly resolve that these dead shall not have died in vain -- that this nation, under God, shall have a new birth of freedom -- and that government of the people, by the people, for the people, shall not perish from the earth.", + "In the beginning God created the heaven and the earth. And the earth was without form, and void; and darkness was upon the face of the deep. And the Spirit of God moved upon the face of the waters. And God said, Let there be light: and there was light. And God saw the light, that it was good: and God divided the light from the darkness. And God called the light Day, and the darkness he called Night. And the evening and the morning were the first day.", + "The LORD is my shepherd; I shall not want. He maketh me to lie down in green pastures: he leadeth me beside the still waters. He restoreth my soul: he leadeth me in the paths of righteousness for his name's sake. Yea, though I walk through the valley of the shadow of death, I will fear no evil: for thou art with me; thy rod and thy staff they comfort me. Thou preparest a table before me in the presence of mine enemies: thou anointest my head with oil; my cup runneth over. Surely goodness and mercy shall follow me all the days of my life: and I will dwell in the house of the LORD for ever.", +]; +// What the top line shows while the table holds at the end. +const SOURCES = ["LINCOLN", "GENESIS", "PSALM 23"]; + +// 3 by 5 capitals, figures and marks, each row three bits, left bit 4. +const FONT = { + A: [2, 5, 7, 5, 5], B: [6, 5, 6, 5, 6], C: [3, 4, 4, 4, 3], D: [6, 5, 5, 5, 6], E: [7, 4, 6, 4, 7], + F: [7, 4, 6, 4, 4], G: [3, 4, 5, 5, 3], H: [5, 5, 7, 5, 5], I: [7, 2, 2, 2, 7], J: [1, 1, 1, 5, 2], + K: [5, 5, 6, 5, 5], L: [4, 4, 4, 4, 7], M: [5, 7, 7, 5, 5], N: [6, 5, 5, 5, 5], O: [2, 5, 5, 5, 2], + P: [6, 5, 6, 4, 4], Q: [2, 5, 5, 6, 3], R: [6, 5, 6, 5, 5], S: [3, 4, 2, 1, 6], T: [7, 2, 2, 2, 2], + U: [5, 5, 5, 5, 7], V: [5, 5, 5, 5, 2], W: [5, 5, 7, 7, 5], X: [5, 5, 2, 5, 5], Y: [5, 5, 2, 2, 2], + Z: [7, 1, 2, 4, 7], "-": [0, 0, 7, 0, 0], "'": [2, 2, 0, 0, 0], ".": [0, 0, 0, 0, 2], " ": [0, 0, 0, 0, 0], + 0: [7, 5, 5, 5, 7], 1: [2, 6, 2, 2, 7], 2: [6, 1, 2, 4, 7], 3: [6, 1, 2, 1, 6], 4: [5, 5, 7, 1, 1], + 5: [7, 4, 6, 1, 6], 6: [3, 4, 7, 5, 7], 7: [7, 1, 2, 2, 2], 8: [7, 5, 7, 5, 7], 9: [7, 5, 7, 1, 6], +}; + +function prep(text) { + const words = text.toUpperCase().match(/[A-Z]+(?:['-][A-Z]+)*/g); + const index = new Map(), names = [], seq = []; + for (const w of words) { + if (!index.has(w)) { + index.set(w, names.length); + names.push(w); + } + seq.push(index.get(w)); + } + const final = new Int32Array(names.length); + for (const id of seq) final[id]++; + let cmax = 0; + for (const c of final) cmax = max(cmax, c); + return { names, seq, distinct: names.length, cmax }; +} +const PREP = TEXTS.map(prep); + +// Layout: the word being read on rows 1-10, the progress line on row 13, +// 13 ranked rows 7 apart from row 17, the log-log inset on rows 108-127. +const ROW0 = 17, PITCH = 7, ROWS = 13, TABLE_END = 106, INSET0 = 109, INSET1 = 126; +// Share of the text read before the first frame, and the hold at the end +// in word-times. +const PREROLL = 0.4, HOLD = 10; + +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 - s * f), t = v * (1 - s * (1 - f)); + return [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; +} + +function start(S, t, preroll) { + const T = PREP[t]; + S.text = t; + S.count = new Int32Array(T.distinct); + S.ydisp = new Float32Array(T.distinct).fill(-1); + S.ldisp = new Float32Array(T.distinct); + S.flash = new Float32Array(T.distinct); + S.order = []; + S.rank = new Int32Array(T.distinct).fill(-1); + S.pos = 0; + S.cur = -1; + S.head = 0; + S.hold = -1; + S.fade = 1; + S.top = 1; + const n = floor(T.seq.length * preroll); + for (let i = 0; i < n; i++) read(S, false); + for (const id of S.order) { + S.ydisp[id] = target(S, id); + S.ldisp[id] = S.count[id]; + } + if (S.order.length) S.top = S.count[S.order[0]]; +} + +const target = (S, id) => ROW0 + PITCH * S.rank[id]; + +function read(S, live) { + const T = PREP[S.text], id = T.seq[S.pos++]; + if (S.count[id] === 0) S.order.push(id); + S.count[id]++; + // Most counted first, ties in the order the words first came. + S.order.sort((a, b) => S.count[b] - S.count[a] || a - b); + S.order.forEach((w, r) => (S.rank[w] = r)); + S.cur = id; + if (live) { + S.flash[id] = 1; + S.head = 1; + if (S.ydisp[id] < 0) S.ydisp[id] = TABLE_END + 2; + } +} + +export function setup(params) { + const S = { buf: new Float32Array(W * H * 3), acc: 0, hue: 0, speed: 2, power: 0.06 }; + start(S, 0, PREROLL); + params.s = S; +} + +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] || 0; + S.speed = cl(k[1] == null ? 2 : k[1], 0.2, 3); + S.power = cl(k[3] == null ? 0.06 : k[3], 0, 1); + const t = cl(round(k[2] || 0), 0, PREP.length - 1); + if (t !== S.text) start(S, t, PREROLL); + const T = PREP[S.text], rate = 2 * S.speed; + S.acc += d * rate; + while (S.acc >= 1) { + S.acc -= 1; + if (S.hold >= 0) { + if (++S.hold >= HOLD) { + start(S, S.text, 0); + S.acc = 0; + break; + } + } else if (S.pos < T.seq.length) read(S, true); + else S.hold = 0; + } + // The table fades over the last three word-times of the hold. + S.fade = S.hold >= 0 ? cl((HOLD - S.hold - S.acc) / 3, 0, 1) : 1; + const ease = 1 - exp(-d * 12), grow = 1 - exp(-d * 10), cool = exp(-d * 3.5); + for (const id of S.order) { + const ty = S.rank[id] < ROWS ? target(S, id) : TABLE_END + 2; + S.ydisp[id] += (ty - S.ydisp[id]) * ease; + S.ldisp[id] += (S.count[id] - S.ldisp[id]) * grow; + S.flash[id] *= cool; + } + S.head *= exp(-d * 5); + if (S.order.length) S.top += (S.count[S.order[0]] - S.top) * grow; +} + +function add(buf, x, y, c, a) { + if (x < 0 || x >= W || y < 0 || y >= H || a <= 0) return; + const p = (y * W + x) * 3; + buf[p] = max(buf[p], c[0] * a); + buf[p + 1] = max(buf[p + 1], c[1] * a); + buf[p + 2] = max(buf[p + 2], c[2] * a); +} + +function glyph(buf, ch, x, y, sx, sy, c, a, y0, y1) { + const g = FONT[ch] || FONT[" "]; + for (let r = 0; r < 5; r++) + for (let q = 0; q < 3; q++) + if (g[r] & (4 >> q)) + for (let j = 0; j < sy; j++) { + const yy = y + r * sy + j; + if (yy < y0 || yy >= y1) continue; + for (let i = 0; i < sx; i++) add(buf, x + q * sx + i, yy, c, a); + } +} + +function text(buf, s, x, y, c, a) { + for (let i = 0; i < s.length; i++) glyph(buf, s[i], x + 4 * i, y, 1, 1, c, a, 0, H); +} + +// An anti-aliased line one LED wide. +function seg(buf, ax, ay, bx, by, c, a, y0, y1) { + const n = max(1, Math.ceil(max(abs(bx - ax), abs(by - ay)) * 2)); + for (let k = 0; k <= n; k++) { + const x = ax + ((bx - ax) * k) / n - 0.5, y = ay + ((by - ay) * k) / n - 0.5, ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + for (const [dx, dy, w] of [[0, 0, (1 - fx) * (1 - fy)], [1, 0, fx * (1 - fy)], [0, 1, (1 - fx) * fy], [1, 1, fx * fy]]) + if (iy + dy >= y0 && iy + dy < y1) add(buf, ix + dx, iy + dy, c, a * sqrt(w)); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const buf = S.buf, T = PREP[S.text]; + buf.fill(0); + const colour = (id) => hsv(S.hue + 0.4 + 0.4 * ((id * PHI) % 1), 0.55, 1); + const gold = hsv(S.hue + 0.12, 0.45, 1), white = [1, 1, 1]; + // Bar length in LEDs for a count: linear at one letter (4 LEDs) a count + // or less, logarithmic, and Scale between them. + // Bar length in LEDs: count x rank^Power, the longest of the 13 rows + // 60 LEDs (8 a count while the top count is under 7.5). + const p = S.power, c1 = max(1, S.top), top = min(8 * c1, 60); + const rankOf = (id) => max(1, (S.ydisp[id] - ROW0) / PITCH + 1); + let norm = 1e-9; + for (let r = 0; r < min(ROWS, S.order.length); r++) norm = max(norm, S.ldisp[S.order[r]] * (r + 1) ** p); + const bar = (c, r) => (c <= 0 ? 0 : (top * c * r ** p) / norm); + + // The word being read, or the source while the table holds. + { + const s = S.hold >= 0 ? SOURCES[S.text] : S.cur >= 0 ? T.names[S.cur] : ""; + const big = s.length <= 9, adv = big ? 7 : 5, sx = big ? 2 : 1, wpx = s.length * adv - (adv - 3 * sx); + const x0 = floor((W - wpx) / 2), c = S.hold >= 0 ? gold : colour(S.cur), f = S.hold >= 0 ? 0 : S.head; + const cc = [c[0] + (1 - c[0]) * f, c[1] + (1 - c[1]) * f, c[2] + (1 - c[2]) * f]; + for (let i = 0; i < s.length; i++) glyph(buf, s[i], x0 + i * adv, 1, sx, 2, cc, 1, 0, 12); + const prog = (S.pos / T.seq.length) * W; + for (let x = 0; x < W; x++) add(buf, x, 13, gold, x < prog ? 0.6 : 0.14); + } + + // Zipf's 1 / rank: where each row's bar would end if count x rank stayed + // at the top word's count, a curve behind the letters. + if (S.order.length) { + let px = -1, py = -1; + for (let r = 1; r <= ROWS; r++) { + const x = bar(c1 / r, r), y = ROW0 + PITCH * (r - 1) + 2.5; + if (px >= 0) seg(buf, px, py, x, y, gold, 0.55 * S.fade, 15, TABLE_END); + px = x; + py = y; + } + } + + // The ranked rows: each word written over and over, as many letters + // long as its bar; the first copy always whole, dim past the bar's end. + const rows = S.order.filter((id) => S.ydisp[id] < TABLE_END && S.ydisp[id] >= 12); + const off = (id) => abs(S.ydisp[id] - (S.rank[id] < ROWS ? target(S, id) : TABLE_END + 2)); + rows.sort((a, b) => off(a) - off(b)); + for (const id of rows) { + const y = round(S.ydisp[id]); + const name = T.names[id], len = name.length, L = bar(S.ldisp[id], rankOf(id)) / 4, c = colour(id), f = S.flash[id]; + const cc = [c[0] + (1 - c[0]) * f * 0.8, c[1] + (1 - c[1]) * f * 0.8, c[2] + (1 - c[2]) * f * 0.8]; + // A row sliding past another covers it. + if (off(id) > 0.5) + for (let yy = max(15, y - 1); yy < min(TABLE_END, y + 6); yy++) buf.fill(0, yy * W * 3, (yy + 1) * W * 3); + for (let k = 0; k < 16; k++) { + const a = max(cl(L - k, 0, 1), k < len ? 0.3 : 0) * S.fade; + if (a <= 0) break; + glyph(buf, name[k % len], 4 * k, y, 1, 1, cc, a, 15, TABLE_END); + } + } + + // The log-log inset: every word seen as a dot at (log rank, log count), + // axes fixed to the whole text; Zipf's slope -1 through the top word, and + // the least-squares line through all the dots with its slope. + { + const R = T.distinct, lr = log(R), lcm = log(T.cmax), span = INSET1 - INSET0; + const X = (r) => 1 + (61 * log(r)) / lr, Y = (c) => INSET1 - (span * log(c)) / lcm; + const N = S.order.length; + if (N) { + const c1 = S.count[S.order[0]]; + seg(buf, X(1), Y(c1), X(R), Y(c1 / R), gold, 0.4 * S.fade, INSET0 - 2, H); + } + let sx = 0, sy = 0, sxx = 0, sxy = 0; + for (let r = 0; r < N; r++) { + const id = S.order[r], x = log(r + 1), yv = log(S.count[id]); + sx += x; sy += yv; sxx += x * x; sxy += x * yv; + const c = colour(id), px = round(X(r + 1) - 0.5), py = round(Y(S.count[id]) - 0.5), f = S.flash[id]; + const cc = [c[0] + (1 - c[0]) * f, c[1] + (1 - c[1]) * f, c[2] + (1 - c[2]) * f]; + add(buf, px, py, cc, S.fade); + add(buf, px + 1, py, cc, 0.7 * S.fade); + if (f > 0.2) add(buf, px, py - 1, white, f * S.fade); + } + if (N >= 3) { + const b = (N * sxy - sx * sy) / (N * sxx - sx * sx), a = (sy - b * sx) / N; + seg(buf, X(1), Y(exp(a)), X(N), Y(exp(a + b * log(N))), white, 0.5 * S.fade, INSET0 - 2, H); + const s = (b < 0 ? "-" : "") + abs(b).toFixed(2); + text(buf, s, W - 4 * s.length, INSET0 - 1, white, 0.8 * S.fade); + } + } + + for (let i = 0; i < W * H; i++) { + const p = i * 3; + display.setPixel(i % W, floor(i / W), round(cl(buf[p], 0, 1) * 255), round(cl(buf[p + 1], 0, 1) * 255), round(cl(buf[p + 2], 0, 1) * 255)); + } +} From 855bcfed3fe2f8bf776d63a174cc398d52e3974f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 04:07:10 +0000 Subject: [PATCH 7/8] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20100=ED=8C=90=20README:?= =?UTF-8?q?=20=EC=97=B4=20=EA=B0=9C=EC=9D=98=20=ED=91=9C=EC=99=80=20?= =?UTF-8?q?=EC=A0=88,=20=EB=8B=A4=EC=9D=8C=20=ED=8C=90=20=EA=B0=90,=20?= =?UTF-8?q?=EB=8B=BF=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=B9=88=20=EC=B9=B8?= =?UTF-8?q?=EC=9D=98=20=EC=85=88?= 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_01Bc4JkrYAzT3TWt21dU8pXk --- incoming/pattern-study/README.md | 103 +++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 4 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index b8c6af8e..20b55041 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 728개를 쉰여섯 갈래로 나눠 만들었어요. 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판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 74판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 75판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 76판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 77판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 78판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 79판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 80판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 81판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 82판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 738개를 쉰여섯 갈래로 나눠 만들었어요. 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판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 74판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 75판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 76판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 77판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 78판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 79판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 80판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 81판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 82판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 83판부터 99판까지는 작품 하나를 깎는 공들이기 판이었어요. 100판은 다시 열 개를 만드는 판으로 돌아와, 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. [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판에서 세로로 다시 짰어요. @@ -10,7 +10,7 @@ ## 공들이기 -11월 1일부터 바퀴는 새 패턴을 열 개씩 만들지 않고, 작품 하나를 여러 바퀴에 걸쳐 깊게 깎아요. 82판까지 728개를 만들었는데 건질 것이 거의 없어서예요. 바퀴마다 판정단이 그림만 보고 가장 약한 곳 하나를 짚고, 그곳을 고친 판이 블라인드 비교에서 이겨야 남아요. 캐릭터마다 `craft/` 아래 폴더가 있고, 거기에 캐릭터 카드(`brief.md`), 바퀴 기록(`log.md`), 바퀴마다 한 장씩 그린 그림(`look-<판>.png`)이 쌓여요. +11월 1일부터 바퀴는 새 패턴을 열 개씩 만들지 않고, 작품 하나를 여러 바퀴에 걸쳐 깊게 깎아요. 82판까지 728개를 만들었는데 건질 것이 거의 없어서예요. 바퀴마다 판정단이 그림만 보고 가장 약한 곳 하나를 짚고, 그곳을 고친 판이 블라인드 비교에서 이겨야 남아요. 캐릭터마다 `craft/` 아래 폴더가 있고, 거기에 캐릭터 카드(`brief.md`), 바퀴 기록(`log.md`), 바퀴마다 한 장씩 그린 그림(`look-<판>.png`)이 쌓여요. 11월 3일에 창작자가 명화 말고 원래 하던 대로 여러 개를 만들자고 해서 공들이기는 닫았어요. 아래 표의 패턴과 `craft/` 는 그대로 두고, 100판부터 바퀴는 다시 새 패턴 열 개를 더해요. | 패턴 | 작품 | 한 줄 | | --- | --- | --- | @@ -1156,6 +1156,21 @@ | [Crofton Cell](patterns/crofton-cell.js) | 보로노이·셀 | 판을 가로지르는 푸아송 직선들이 움직이며 판을 볼록한 칸으로 자르고, 가운데 점을 품은 칸만 금빛 모서리 안의 옅은 내접원으로 빛나서 Big 을 올리면 그 칸이 커지며 변이 늘고 원에 가까워져요. | | [Phasor Noise](patterns/phasor-noise.js) | 노이즈·흐름 | 같은 가보르 커널을 더한 복소 장을 위 반에는 허수부로, 아래 반에는 위상만으로 그려서, 소용돌이와 안장을 따라 지문처럼 휘는 줄무늬가 위에서는 군데군데 흐려지고 아래에서는 같은 대비로 갈라지며 흘러요. | +### 100판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Runestone](patterns/runestone.js) | 글자 | 머리부터 꼬리까지 패널을 굽이굽이 내려오는 뱀 띠에 옐링 돌의 룬 글이 새겨져 머리 쪽으로 미끄러지고, 꼬리 끝에서 불꽃이 새 룬을 한 획씩 새기면 그 자리에 물감이 차올라요. | +| [Rolling Shutter](patterns/rolling-shutter.js) | 글자 | 패널을 위에서부터 한 줄씩 읽히는 센서로 두어, 도는 SHUTTER 와 SPIN 은 호와 나선으로 휘고 달리는 GRAND PRIX 는 라르티그의 경주차처럼 기울어요. | +| [Roman Addition](patterns/roman-addition.js) | 글자 | 로마 숫자 두 수를 석판에 적고 뺄셈 짝을 풀어 모든 기호를 M 부터 I 까지 한 줄로 늘어놓은 뒤, 다섯 I 나 두 V 가 미끄러져 모여 번쩍이며 윗 기호 하나가 되는 줄을 차례로 내려 쓰고, 답은 맨 위로 올라가 다음 합의 첫 수가 돼요. | +| [Zipf Words](patterns/zipf-words.js) | 글자 | 게티즈버그 연설을 한 낱말씩 읽어 가장 많이 나온 13 낱말을 순위대로 세우고, 막대마다 그 낱말을 되풀이해 쓴 글자로 짜요. | +| [Dokkaebibul](patterns/dokkaebibul.js) | 글자 | 두벌식 자판으로 「서시」를 한 키씩 치면 받침에 앉았던 자음이 다음 모음에 불꽃처럼 뛰어 다음 글자의 첫소리가 돼요. | +| [Tempo Canon](patterns/tempo-canon.js) | 리듬·펄스 | 같은 선율을 저마다 다른 빠르기로 치는 목소리들이 세로 피아노 롤의 칸에 서고, 같은 음을 잇는 선이 휘었다가 모든 목소리가 한 음을 같이 치는 수렴점에서 한 줄로 누워 하얗게 번쩍여요. | +| [Gosper Islands](patterns/gosper-islands.js) | 프랙탈 | 에이젠슈타인 정수를 밑 2 − ω 로 풀어 LED 마다 셈한 고스퍼 섬의 바둑판으로 끝없이 들어가며, 섬 일곱이 √7 배 큰 섬 하나가 되는 해안선을 밝은 선으로 그려요. | +| [Axelrod Culture](patterns/axelrod-culture.js) | 시뮬레이션 | 16×32 마을마다 문화 특징 셋을 색으로 칠하고 무작위 이웃끼리 이미 같은 몫만큼의 확률로 특징 하나를 베끼게 해서, 닮은 이웃은 한 지역으로 모이고 아무것도 나누지 않는 이웃 사이에는 검은 선이 서요. | +| [Desargues Configuration](patterns/desargues.js) | 공간·원근 | 꼭짓점 O 에서 내려온 삼각뿔을 두 평면이 청록과 금색 삼각형으로 자르고, 늘인 변들이 두 평면이 만나는 분홍 축 위 세 점에서 만나는 채로 장면이 천천히 흔들려요. | +| [Magic Lines](patterns/magic-lines.js) | 수론 | 마방진의 1, 2, 3… 이 든 칸을 차례로 잇는 선이 한 걸음씩 그어져 마방진의 대칭을 무늬로 드러내요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -4862,6 +4877,70 @@ Lab 에서 해 보기: 두 쪽 모두 마지막 줄 아래 122~127행이 비어 주제 5 「수학 · 추상」으로 노이즈·흐름 갈래에 선 패턴이에요. 아레스 라가에(Ares Lagae), 실뱅 르페브르(Sylvain Lefebvre), 조지 드레타키스(George Drettakis), 필립 뒤트레(Philip Dutré)가 2009년에 낸 가보르 노이즈(Gabor noise)는 가우스 함수에 평면파를 곱한 가보르 커널을 아무 자리에나 흩어 더해요. 커널의 주파수와 방향이 노이즈의 결이 되지만, 이웃 커널끼리 상쇄되는 자리에서는 줄무늬가 흐려져요. 티보 트리카르(Thibault Tricard)와 다섯 공저자가 2019년에 낸 페이저 노이즈(phasor noise)는 같은 커널의 복소 합 G 에서 크기를 버리고 위상만 남겨 profile(arg G) 를 그려요. 그러면 줄무늬가 어디서나 같은 대비로 서고, G = 0 인 점에서만 끊기거나 갈라져요. 패턴은 같은 64×63 창의 같은 G 를 두 번 그려요. 0~62번 줄은 Im G 를 |G| 의 제곱평균제곱근의 1.5배로 나눈 가보르 노이즈이고, 63~64번 줄은 비워 두고, 65~127번 줄이 페이저 노이즈예요. Im G = |G| sin(arg G) 라서 두 반의 줄무늬는 같은 자리에 서고, 위가 흐려진 자리마다 아래에 갈림이나 끝이 있어요. 커널은 8 LED 간격으로 흔든 격자에 169개이고, 표준편차 6.5 LED 에 18 LED 까지 닿아요. 커널의 방향은 소용돌이 하나와 안장 하나가 천천히 떠도는 방향장에서 오고, Speed 2 에서 방향장 전체가 50초에 한 바퀴 돌아요. 그래서 소용돌이 둘레의 줄무늬가 고리에서 나선을 거쳐 바큇살이 되고, 바큇살이 밖으로 벌어지는 만큼 아래 반은 갈림을 끼워 주기를 지켜요. 줄무늬는 3.3초에 한 주기씩 흐르고, 커널은 제자리에서 반지름 0.8~2 LED 의 원을 9초에 한 바퀴 돌아서 갈림이 옮겨 다녀요. 색은 줄무늬가 놓인 방향을 따라서, 누운 줄에서 선 줄로 가며 바다색 초록이 하늘색을 거쳐 보라가 돼요. 가우스 함수도 평면파도 x 와 y 로 갈라지니, 커널마다 가로 한 줄과 세로 한 줄의 복소수를 곱해 더해요. K3 Frequency 는 64 LED 에 놓이는 줄 수 8~11 로 주기 8~5.8 LED 이고, K4 Profile 은 아래 반의 파형을 사인, 삼각, 사각, 톱니로 바꿔요. 톱니에서는 줄마다 위상이 어느 쪽으로 도는지가 보여요. 잰 값은 움직임 0.490, 촘촘함 0.125, 가장자리 0.437, 복잡도 2.309, 채움 0.593, 색 0.182, 모양 바뀜 0.134 이고, thirds 는 64/57/56, 노브는 0.11, 0.30, 0.19, 0.11 이에요. 첫 판은 표준편차 5.2 LED 의 커널을 7 LED 간격에 두고, 소용돌이 둘과 안장 하나를 두고, 가보르 노이즈를 제곱평균제곱근의 0.8배로 나눴어요. 그때는 움직임 0.468, 복잡도 2.641, 채움 0.776, 노브 0.15, 0.31, 0.21, 0.11 이었어요. 0.8배로 나누니 위 반도 거의 어디서나 끝까지 밝아서 아래 반과 구별이 안 됐고, 1.5배로 나누자 흐려진 자리가 보였어요. 극 셋은 서로 17~18 LED 떨어져서 소용돌이 둘레에 고리가 한 겹 남짓밖에 서지 못해, 소용돌이 하나와 안장 하나로 줄였어요. G 의 영점을 위 반에 점으로 찍어 본 판은 창 하나에 마흔 개 남짓이 떠서 별밭처럼 보여 뺐어요. 커널을 6.5 LED 로 키우자 줄무늬가 길게 이어졌어요. K3 를 7~12 에서 8~11 로 옮겨 처음 주기를 9.1 LED 에서 8 LED 로 좁히니 복잡도가 2.653 으로 축 위 끝 2.7352 에 다가가서, 파형의 아래 14% 를 검게 두었어요. 그러자 복잡도가 2.309, 채움이 0.593 이 되고 줄무늬가 얼룩말처럼 또렷해졌어요. 방향장의 atan2 를 복소수 곱으로 바꿔 한 프레임이 2.1 ms 에서 1.7 ms 가 됐어요. +## 100판 + +100판은 11월 3일 창작자가 공들이기를 닫고 원래 하던 대로 여러 개를 만들자고 한 뒤의 첫 판이에요. 같이 도는 바퀴는 없었어요. 앞 바퀴 PR 에서 OWNER, MEMBER, COLLABORATOR 가 쓴 글 가운데 일꾼 글이 아닌 것은 셋이었어요. 83판(#347)과 84판(#348)을 닫는다는 hub 의 알림과 90판(#356)을 멈춘다는 창작자의 알림이라 따를 일은 없었어요. 83판과 84판은 머지되지 않고 닫혀서 그 스무 개는 이 폴더에 없지만, 같은 대상과 생각은 비켜 골랐어요. 주제 1 「지도의 빈 칸」은 채우지 못했어요. 짓기 전 737개에서 `*` 가 붙은 칸은 움직임 × 모양 바뀜 1,4 하나뿐인데, 셈으로 닿지 않는 칸이에요. 그 셈은 「다음 판 감」의 「지도의 빈 칸」에 적었어요. 주제 4 「세로 64×128」은 Tempo Canon 과 Runestone 이 채워요. 타이포 다섯은 「타이포 실험」의 출발점을 하나씩 나눠 맡았어요. Runestone 은 세로 패널다운 글, Rolling Shutter 는 글자의 몸, Roman Addition 은 숫자와 기호 놀이, Zipf Words 는 글자로 짠 무늬, Dokkaebibul 은 글자가 만들어지는 과정이에요. 다섯 다 글자 꼴을 코드에 적었어요. 수학 다섯은 82판 뒤 스물다섯으로 가장 적던 리듬·펄스, 프랙탈, 시뮬레이션, 공간·원근에 하나씩 서고, 스물여섯인 아홉 갈래 가운데 수론에 하나가 섰어요. 「낯선 수학 예」와 「다음 판 감」에서는 고르지 않았어요. 열 개는 다섯 갈래에 둘씩 나눠 따로 짓고 합쳤어요. + +### Runestone + +주제 7 「타이포」의 「세로 패널다운 글」을 겨눈 패턴이고, 주제 4 「세로 64×128」도 같이 채워요. 하랄 블루투스(Harald Bluetooth)가 965년쯤 옐링에 세운 큰 돌(DR 42)의 글을 표준 전사(Samnordisk runtextdatabas) 그대로 옮겼어요. haraltr kunukR baþ kaurua kubl þausi 로 시작해 kristną 로 끝나는 룬 125자이고, 「하랄 왕이 아버지 고름과 어머니 튀라를 기려 이 기념물을 세우게 했다. 덴마크 전부와 노르웨이를 얻고 데인인을 그리스도인으로 만든 그 하랄이다」라는 뜻이에요. 낱말마다 뒤에 두 점 구분표를 넣어 기호는 153개예요. 옐링 돌은 이 글을 가로 줄로 새겼지만, 패턴은 11세기 웁란드 돌들처럼 뱀 몸 띠에 옮겼어요. 띠는 16 LED 너비이고, 왼쪽 위의 머리에서 나와 가로로 곧은 줄 n + 1 개를 반지름 54/n 줄의 반원 n 개가 오른쪽과 왼쪽을 번갈아 이으며 내려가 오른쪽이나 왼쪽 아래의 꼬리로 끝나요. LED 마다 띠 위 가장 가까운 점까지의 호 길이 u 와 띠에서 떨어진 거리 v 를 셈해 두고, 그 틀에 룬을 놓아요. 그래서 룬의 줄기는 늘 띠를 가로지르고 글줄은 띠를 따라가며, 왼쪽으로 가는 줄에서는 룬이 거꾸로 서요. 돌 둘레를 돌며 읽는 실제 뱀 띠와 같아요. 룬은 4×8 틀의 곧은 획 다섯으로 적었고 패널에서 4.25×8.5 LED, 획 굵기 1.9 LED 이며, 띠의 칠한 테와 룬 사이에 1.4 LED 의 어두운 띠가 남아요. 글은 Speed 2 에서 1초에 5 LED 씩 머리 쪽으로 미끄러져 1.5초에 룬 하나가 지나고, 153기호가 3.5분에 한 바퀴 돌아요. 기호가 꼬리 쪽에서 띠에 들어오면 흰 불꽃이 획 차례대로 새겨요. 한 자리를 가는 동안 갓 깎은 옅은 돌빛이 남고, 다음 두 자리 동안 물감이 차올라요. 머리에 닿은 기호는 쐐기꼴로 벌린 턱 안으로 사라져요. K1 Hue 는 0 에서 룬과 띠의 두 테를 노란 황토색으로, 뱀 몸을 짙은 파랑으로 칠하고, 0.87 에서 룬이 붉은 황토색이 돼요. K3 Turns 는 굽이를 3에서 6으로 늘려, 굽이 지름이 36줄에서 18줄로 줄고 띠에 드는 기호가 30개쯤에서 52개쯤으로 늘어요. K4 Twig 는 룬을 옐링 돌의 긴가지 꼴에서 스웨덴과 노르웨이 돌의 짧은잔가지 꼴로 바꿔요. n, a, ą 의 가로지른 가지는 한쪽 잔가지로, h 와 m 은 줄기 위 매듭으로, s 와 R 은 반쪽 줄기로 줄고, 기호 사이가 7.6 LED 에서 6.2 LED 로 좁아져 Turns 3 에서 36개쯤이 들어요. 두 꼴은 Unicode Runic 블록의 참고 글리프를 따라 그렸어요. 잰 값은 움직임 0.304, 촘촘함 0.096, 가장자리 0.527, 복잡도 1.412, 채움 0.980, 색 0.277, 모양 바뀜 0.033 이고, thirds 는 97/98/97, 노브는 0.15, 0.08, 0.21, 0.07 이에요. 채움과 thirds 는 회색 화강암 바탕까지 센 값이라, 색이 있거나 70 을 넘는 LED 만 따로 세니 채움 0.483, thirds 45/54/45 였어요. 복잡도 1.412 도 대부분 화강암 알갱이에서 와요. 첫 판은 띠가 14 LED 이고 룬이 9 LED 높이라 획 끝이 칠한 테에 닿았어요. 패널에서는 띠가 노란 사다리처럼 보여서, 띠를 16 LED 로 넓히고 룬을 8.5 LED 로 줄였어요. Twig 는 처음에 꼴만 바꿔서 0.0081 로 검사에 걸렸어요. 꼴이 바뀌는 룬이 열여섯 가운데 아홉뿐이고 바뀌는 획도 짧았어요. 짧은잔가지 룬이 자리를 덜 차지하는 만큼 기호 사이를 좁히자 글줄 전체가 머리 쪽으로 당겨져 0.07 이 됐어요. 머리는 처음에 아몬드 꼴로만 그렸더니 LED 에서는 뭉툭한 덩어리였어요. 턱을 쐐기로 벌리고 테를 두르자 뱀 머리로 읽혔어요. + +Lab 에서 해 보기: 처음 노브(Turns 3)에서 첫 굽이 안쪽 0~46열 21~37행과 둘째 굽이 안쪽 18~62열 57~73행이 빈 돌이에요. Text 레이어에 HARALD 를 Silkscreen 8 px, 가운데 맞춤 x 23, y 29, 색 #ffd438 로 얹고, 965 를 같은 글꼴로 x 40, y 65 에 두면 룬 띠가 말하는 이름과 돌을 세운 해가 라틴 글자로 굽이 안에 서요. + +기능 감: Text 레이어가 글줄을 곡선 띠를 따라 흘리는 길이요. 이 패턴은 LED 마다 띠 위 가장 가까운 점의 호 길이와 거리를 손수 셈해 룬을 그 틀에 놓았어요. Text 레이어가 경로 하나를 받아 Silkscreen 글자를 거기 맞춰 휘어 놓을 수 있으면, 뱀 띠나 소용돌이나 둥근 도장 글을 코드 없이 시험할 수 있어요. 룬 열여섯 자도 Lab 글꼴에 없어서 획으로 직접 그렸어요. + +### Rolling Shutter + +주제 7 「타이포」의 「글자의 몸」을 겨눈 패턴이에요. 롤링 셔터는 센서의 줄을 위에서부터 차례로 읽어서, y 번째 줄에는 읽기가 그 줄에 닿은 때의 장면이 담겨요. 그사이 움직인 것은 줄마다 다른 자리에 찍혀서, 도는 막대는 휘고 지나가는 것은 기울어요. 자크 앙리 라르티그(Jacques Henri Lartigue)가 1912년 프랑스 자동차 클럽 그랑프리에서 찍은 경주차 사진이 그 보기예요. 포컬 플레인 셔터가 장면을 아래에서 위로 찍었고 그가 차를 따라 카메라를 돌려서, 차는 앞으로 기울어 바퀴가 타원이 됐고, 구경꾼은 반대로 기울었어요. 패턴은 패널을 읽히는 중인 센서로 둬요. 희미한 회색 주사선이 한 줄씩 내려가고 쓸기는 쉬지 않고 이어져서, 줄마다 선이 다시 올 때까지 읽은 것을 들고 있어요. 그래서 y 줄은 Readout × frac(phase − y/128) 초 전의 낱말을 그려요. 선 바로 위는 방금, 바로 아래는 한 쓸기 전이라 그림은 주사선에서 찢어지고, 선이 지나가며 낱말을 다시 칠해요. 글자는 4×6 칸에 획 중심선으로 적은 대문자 열세 자(S, H, U, T, E, R, P, I, N, A, G, D, X)를 둥근 끝 거리장으로 그려서 어느 각도로 돌아도 깨끗하고, 획은 3.6~3.8 LED 굵기예요. K3 Mode 0 은 SHUTTER 일곱 자(101 LED 길이, 17 LED 높이)가 가운데를 축으로 도는 바통이고, 1 은 날개마다 SPIN 을 단 세 날 프로펠러(끝이 가운데서 61 LED)예요. 2 는 14 LED 높이의 GRAND 와 PRIX 가 100 LED 마다 되풀이되며 오른쪽으로 달리고, 16 LED 간격의 흐린 기둥이 뒤에서 반대로 달려요. Speed 2 에서 바통과 프로펠러는 2.5초에 한 바퀴 돌고, 경주 낱말은 초당 72 LED, 기둥은 96 LED 로 움직여요. K4 Readout 은 한 쓸기 시간을 2.5초 × √노브 로 정해요. 0 은 전역 셔터라 글자가 곧고, 처음 0.06 은 0.61초라 그동안 바통이 88도 돌아 SHUTTER 가 호로 휘어요. 경주에서는 낱말이 한 줄에 0.34 LED(19도), 기둥이 0.46 LED(25도) 반대로 기울어요. 이 센서는 위에서부터 읽어서, 라르티그의 차와 달리 낱말이 뒤로 기울어요. Readout 1 은 2.5초라 Speed 2 에서 바통이 꼭 한 바퀴 돌아요. 그러면 줄마다 쓸기마다 같은 각을 보아 찢어짐이 닫히고, 바통은 움직이는 주사선 아래 멈춘 나선으로 서요. 한 쓸기 떨어진 두 프레임의 차이가 0.0000 이었어요. 프로펠러는 날이 셋이라 Readout 0.111 에서 같은 일이 일어나요(0.0002). K1 Hue 는 잉크를 돌리고, 색이 줄의 나이를 말해요. 처음 화면에서 방금 읽은 줄은 하늘색, 한 쓸기 전 줄은 청보라예요. 잰 값은 움직임 0.419, 촘촘함 0.037, 가장자리 0.824, 복잡도 0.267, 채움 0.125, 색 0.190, 모양 바뀜 0.173 이고, thirds 는 7/21/8, 노브는 0.05, 0.12, 0.14, 0.14 예요. 경주의 첫 판은 낱말 하나가 140 LED 마다 한 번 지나가게 했어요. 쓸기가 1.37초면 낱말은 쓸기마다 88 LED 를 옮겨서, 낱말 줄이 읽히는 때에 낱말이 판 밖에 있으면 그 쓸기에는 조각만 남았어요. 3분의 1초 간격 열두 프레임 가운데 넷이 거의 비어서 낱말을 100 LED 마다 되풀이했어요. 패널에서는 처음 화면의 휜 SHUTTER 가 호를 따라 짠 글처럼 읽혔어요. 셔터가 휘게 했다고 알려 주는 것은 주사선의 찢어짐과 줄마다 다른 색이에요. + +Lab 에서 해 보기: Text 레이어로 1912 를 Silkscreen 8 px, 회색 #8090a0 solid, 가운데 맞춤 x 32, y 6 에 얹으면 Mode 0 의 바통이 닿지 않는 0~12줄에 셔터를 거치지 않은 글이 곧게 서서, 휜 SHUTTER 와 한 화면에서 견줘져요. + +기능 감: Lab 레이어 효과로 지난 프레임을 쌓아 두고 줄마다 다른 프레임에서 읽는 롤링 셔터요. 이 패턴은 낱말의 자리를 시간의 식으로 적을 수 있어서 줄마다 그 식을 다시 셈했어요. 상태를 들고 가는 시뮬레이션이나 Text 레이어의 「흐르기」 글은 그런 식이 없어서 이렇게 못 휘어요. 2.5초 쓸기면 60fps 에서 150 프레임, 64×128×3 바이트씩 3.7 MB 예요. + +### Roman Addition + +주제 7 「타이포」의 「숫자와 기호 놀이」를 겨눈 패턴이에요. 로마 숫자에는 자리값이 없어서 MDCCCCLXXXXIIII 는 M 하나, D 하나, C 넷 같은 말 더미이고 값은 그 합이에요. 두 수를 더하는 것은 두 더미를 한데 모은 뒤 다섯 I 를 V 로, 두 V 를 X 로, 다섯 X 를 L 로 묶어 가며 정리하는 일이에요. 카를 메닝거(Karl Menninger)는 1934년 『Zahlwort und Ziffer』에서 로마 숫자를 셈판에서 말로 셈한 결과를 적어 둔 기록으로 읽었어요. I, X, C, M 은 셈판의 줄 위에, V, L, D 는 줄 사이에 놓이는 말이고, 묶기는 셈판의 받아올림이에요. 조르주 이프라(Georges Ifrah)도 1981년 『Histoire universelle des chiffres』에서 같은 셈판을 다뤄요. 패턴은 줄기 2 LED, 높이 7 LED 의 대문자 일곱을 직접 그렸어요. I 는 2, L 과 C 는 4, V, X, D 는 5, M 은 7 LED 너비라 MMMDCCCLXXXVIII 는 두 줄로 접혀요. 석판은 위에서부터 첫 수, + 와 둘째 수, 줄 하나, 값 차례로 늘어놓은 줄, 묶음마다 새 줄 하나, 겹줄과 = 와 답이에요. 뺄셈 꼴로 쓰면 IV, XC, CM 같은 짝이 0.7초 빛나고, 그 짝이 뜻하는 IIII, LXXXX, DCCCC 가 짝 가운데에서 희게 튀어나와 아래 줄로 날아가요. 다른 기호는 제자리에서 사본이 떨어져서, 0.06초 간격으로 M 부터 I 까지 값 차례 자리에 내려앉아요. 그다음 작은 쪽부터 다섯 I, 두 V, 다섯 X, 두 L, 다섯 C, 두 D 가 한 줄 아래로 내려가며 한 자리로 미끄러져 모이고, 그 자리에 윗 기호 하나가 흰 빛을 내며 나타나요. 4000 아래의 두 수는 I 를 여덟 개까지만 가져서 한 단계에 묶음은 늘 하나예요. 마지막 줄은 겹줄 아래로 내려오며 XXXX 가 XL 로 접히듯 뺄셈 꼴로 돌아가고, 빛 한 줄기가 답을 훑고 1.6초 머문 뒤 나머지는 지워지고 답이 맨 위로 올라가 다음 첫 수가 돼요. 줄 사이는 패널을 채울 만큼 4~10 LED 를 띄우고, 그래도 넘치면 화면이 쓰는 자리를 따라 내려가요. K1 Hue 는 기호 색을 함께 돌려요. 0 에서 I 와 V 는 물빛, X 와 L 은 하늘색, C 와 D 는 보라, M 은 분홍이고 자리마다 0.12 바퀴씩 떨어지며, V, L, D 는 옅어요. 처음 석판은 1994 + 47 = 2041 이에요. 늘어놓은 줄이 22 기호이고 묶음이 여섯 번이라 글줄이 열다섯이고, Speed 2 에서 쓰고 머물고 올라가는 데 14.1초 걸려요. K3 Step 은 0 에서 박자마다 1~1500 사이의 새 수를 더하고(작은 수가 더 자주), 다른 값이면 그 수를 거듭 더해요. 250 은 CCL, 999 는 CMXCIX 예요. 합이 3999 를 넘으면 석판을 지우고 100 아래에서 다시 시작해요. K4 Notation 은 0.5 아래에서 뺄셈 꼴이고, 0.5 부터는 로마 비문과 시계 판이 흔히 쓰는 IIII, VIIII 의 옛 덧셈 꼴이라 빛나는 짝과 마지막 접기가 빠져요. 잰 값은 움직임 0.355, 촘촘함 0.089, 가장자리 0.943, 복잡도 0.124, 채움 0.180, 색 0.221, 모양 바뀜 0.140 이고, thirds 는 26/20/6, 노브는 0.04, 0.07, 0.09, 0.09 예요. 아래 3분의 1이 6 인 것은 잰 2~10초 동안 첫 석판이 위에서부터 써 내려가는 중이라서예요. 첫 판은 3×5 대문자를 한 LED 획으로 그려서 채움 0.048, thirds 3/10/0, 노브 0.01, 0.02, 0.04, 0.02 였어요. 높이를 7 로, 줄기를 2 LED 로 키우자 채움이 0.112 가 됐어요. 그 판은 뺄셈 짝을 제자리에서 덧셈 꼴로 풀었는데, 푼 뒤에는 두 표기의 석판이 똑같아서 Notation 이 0.03 이었어요. 첫 두 줄은 쓴 그대로 두고 짝에서 기호가 튀어나오게 바꾸고, 처음 합을 짝이 넷인 1994 + 47 로 두자 Notation 이 0.09, 채움이 0.180 이 됐어요. 패널에서 새로 본 것은 색의 자리예요. 값 차례로 늘어놓으니 줄마다 왼쪽부터 분홍, 보라, 하늘색, 물빛 띠가 서고, 묶음 줄이 내려갈수록 줄이 짧아지며 오른쪽의 물빛과 하늘색이 보라와 분홍 쪽으로 넘어가요. 받아올림이 색이 왼쪽으로 옮겨 가는 것으로 읽혀요. Long Division(65판)은 연필 숫자가 계단으로 내려가는 자리값 셈이고, 여기서는 자리값 없이 기호가 움직여요. + +Lab 에서 해 보기: 없음. 석판이 4~63열과 2~126줄을 다 쓰고, 합이 박자마다 바뀌어서 Text 레이어에 고정한 낱말은 석판의 수와 맞지 않아요. 비는 곳은 부호가 서는 0~3열뿐이에요. + +기능 감: 코드 패턴이 Text 레이어로 낱말을 넘기는 칸이요. 답 옆에 아라비아 숫자 2041 을 Text 레이어의 Inter 글자로 세우고 싶었는데, Text 레이어의 낱말은 손으로 적은 고정 글이라 박자마다 바뀌는 합을 따라가지 못해요. 그래서 숫자 읽기 없이 로마 숫자만 남겼어요. + +### Zipf Words + +주제 7 「타이포」의 「글자로 짠 무늬」를 데이터로 겨눈 패턴이에요. 조지 킹즐리 지프(George Kingsley Zipf)는 1935년 『The Psycho-Biology of Language』에서 낱말의 횟수가 순위에 거의 반비례한다고 적었고, 장바티스트 에스투(Jean-Baptiste Estoup)가 1916년 프랑스어 속기를 다루며 먼저 본 것이에요. 글은 에이브러햄 링컨(Abraham Lincoln)의 1863년 게티즈버그 연설 Bliss 사본이고, K3 Text 로 킹 제임스 성경(1611)의 창세기 1장 1~5절과 시편 23편으로 바꿀 수 있어요. Speed 2 에서 1초에 네 낱말씩 맨 위 1~10행에 6×10 글자로 서고, 열 글자가 넘는 낱말은 3×10 으로 서요. 그 아래 17행부터 7행 간격으로 가장 많이 나온 13 낱말이 순위대로 서요. 줄마다 낱말을 틈 없이 되풀이해 쓴 것이 막대예요. 여는 화면에서 일곱 번 나온 THAT 은 56 LED 의 THATTHATTHATTH 이고, 막대가 제 낱말보다 짧은 CONCEIVED 는 CONC 만 밝고 나머지는 30% 로 남아서 낱말이 늘 다 읽혀요. 한 낱말이 다른 낱말을 앞지르면 두 줄이 0.25초에 서로 미끄러져 자리를 바꿔요. 줄 뒤에는 금색 곡선이 맨 위 낱말의 횟수를 순위로 나눈 자리, 곧 Zipf 의 1/순위를 그려요. 맨 아래 109~126행에는 지금까지 나온 낱말이 다 (log 순위, log 횟수) 점으로 찍히고, 기울기 −1 의 금색 선과 최소제곱 흰 선 옆에 그 기울기가 숫자로 서요. 글이 끝나면 맨 위에 LINCOLN 이 서고 표가 2.5초 머문 뒤 사라지고, 첫 낱말부터 다시 읽어요. K4 Power 는 막대를 횟수 × 순위^Power 로 바꿔요. 1 에서는 Zipf 법칙이 맞으면 막대 끝이 곧은 세로줄로 서요. 창세기 1~5절은 끝에서 횟수 × 순위가 16~26 이라 막대 끝이 37~60 LED 안에 들고, 연설은 13 에서 65 로 올라가서 막대가 아래로 갈수록 길어져요. Etaoin Shrdlu(66판)는 글자를 세어 기둥을 쌓았고, 여기서는 낱말을 세어 낱말 자신으로 막대를 써요. 3×5 대문자는 이 패턴에 손으로 그렸어요. 잰 값은 움직임 0.229, 촘촘함 0.170, 가장자리 0.886, 복잡도 0.269, 채움 0.209, 색 0.201, 모양 바뀜 0.043 이고 thirds 는 28/17/16, 노브는 0.06, 0.09, 0.11, 0.07 이에요. 첫 판의 K4 Scale 은 선형 막대와 로그 막대 사이를 오갔는데, 횟수가 1~7 인 동안은 둘이 거의 같아서 반응이 0.02 였어요. 횟수 × 순위의 지수로 바꾸니 0.07 이 됐어요. 한 횟수에 글자 하나(4 LED)를 쓴 첫 판은 여는 화면의 THAT 이 28 LED 라 오른쪽 절반이 비어서, 가장 긴 줄을 60 LED 로 맞췄어요. 패널에서 새로 본 것은 짧은 글의 기울기예요. 연설을 읽는 동안 흰 선의 기울기가 4분의 1 에서 −0.42, 절반에서 −0.52, 끝에서 −0.65 로 가팔라지고 −1 에는 닿지 않아요. 271 낱말 가운데 다른 낱말이 138 개이고 그 가운데 91 개가 한 번만 나와서, 아래 그림의 바닥에 꼬리가 길게 깔려요. + +Lab 에서 해 보기: 여는 화면에서 여섯째 줄 아래의 막대는 40열 앞에서 끝나서 44~63열, 56~105행이 비어요. Text 레이어로 1/R 을 Silkscreen 8 px, 색 #b09050, 오른쪽 맞춤 x 62, y 84 에 얹으면 금색 곡선 옆에 법칙의 꼴이 서요. K4 Power 를 1 쪽으로 돌리면 막대가 그 자리를 덮어 와서, 글이 법칙에 가까운 만큼 글자가 가려져요. + +기능 감: 코드 패턴에 긴 글을 넘기는 칸이요. 연설과 성경 두 대목을 코드 안 문자열로 박아서, 다른 글을 세어 보려면 코드를 고쳐야 해요. Text 레이어에 붙여 넣은 글을 패턴이 문자열로 받으면 누구나 제 글의 Zipf 기울기를 패널에서 볼 수 있어요. + +### Dokkaebibul + +주제 7 「타이포」의 「글자가 만들어지는 과정」을 겨눈 패턴이에요. 두벌식 자판(1982년 KS C 5715, 지금 KS X 5002)에는 음절이 어디서 끝나는지 알리는 키가 없어서, 입력기가 키가 올 때마다 정해요. 초성과 중성 뒤의 자음은 받침으로 앉혀 하 + ㄴ 이 한 이 되고, 다음에 모음이 오면 그 자음이 다음 글자의 초성으로 넘어가 한 + ㅡ 가 하느 가 돼요. 겹받침은 뒤 글자만 넘겨서 없 + ㅓ 는 업서 예요. 쓰는 사람들은 이 뜀을 도깨비불이라고 불러요. 패턴은 윤동주의 「서시」(1941년 11월 20일) 아홉 줄을 음절에서 키로 풀어(ㅚ 는 ㅗ 와 ㅣ 두 키, ㄲ 과 ㅆ 은 Shift 를 누른 한 키) 이 오토마톤에 넣고, 음절이 바뀌는 대로 그려요. 음절은 18×18 LED 블록이고 21 LED 간격으로 한 줄에 셋, 아래로 다섯 줄이에요. 자모는 63판 Jamo Assembly 의 20×20 획을 가져와 모음 꼴과 받침에 따라 정한 자리에 넣고, 키가 음절을 바꾸면 0.12초 동안 새 자리로 미끄러져요. 홀로 친 자음은 모음이 올 때까지 블록을 다 채워요. 받침이 뛰면 그 자음이 하늘색 불꽃이 되어 0.42초 동안 받침 자리에서 10 LED 쯤 솟는 호를 그리며 다음 블록의 초성 자리로 날아가요. 불꽃은 34 LED 안의 글자를 제 빛으로 물들이고, 지나간 길을 5 LED 너비의 빛줄기로 남기고, 내려앉은 자음은 불꽃 색에서 잉크 색으로 식어요. 패널 아래 18줄에는 5×5 글쇠 26개와 Shift 가 서서 친 키가 0.3초 켜지고, 뜀을 일으킨 모음 키는 불꽃 색으로 켜져요. 자음 글쇠는 금색, 모음 글쇠는 라일락이라 왼쪽 자음과 오른쪽 모음이 번갈아 켜지는 두벌식의 박자가 보여요. Speed 2 에서 1초에 키 셋쯤(0.25~0.41초 간격, 띄어쓰기 뒤 0.25초 더)이라 첫 줄 30키가 10.4초, 아홉 줄과 38번의 뜀이 2분이에요. K3 Phrase 는 시작할 줄이에요. 첫 줄(죽는 날까지 하늘을 우러러)은 30키에 넷이 뛰어서 깢이 까지, 한이 하느, 울이 우러, 럴이 러러가 되고, 여섯째와 아홉째 줄은 여섯 줄이 들어서 쪽이 한 줄 올라가요. K4 Trail 은 빛이 남는 시간이에요. 길의 한 점은 처음 자리 0.06 에서 0.31초, 1 에서 10초의 시간 상수로 바래고, 1 에서는 한 줄의 호가 다 남아 음절 사이에 다리를 놓아요. 처음 화면은 2초 뒤에 첫 줄의 셋째 뜀(울이 우러)이 반쯤 날아가 있도록 줄 중간에서 열려요. 잰 값은 움직임 0.062, 촘촘함 0.055, 가장자리 0.801, 복잡도 0.228, 채움 0.173, 색 0.097, 모양 바뀜 0.099 이고, thirds 는 12/22/17, 노브는 0.05, 0.03, 0.12, 0.03 이에요. 첫 판은 날아가는 글자 꼴을 통째로 잔상에 찍어서, 꼬리가 내려앉을 블록을 덮는 하늘색 얼룩이 되고 지 가 가려졌어요. 길은 부드러운 선으로 두고 글자는 머리에만 선명하게 그렸어요. 그 판의 Trail 반응은 0.0045 로 문턱 아래였어요. 꼬리가 LED 300개쯤만 건드렸기 때문이에요. 불꽃이 둘레 글자를 물들이게 하고, 줄 중간에서 열어도 앞선 뜀의 길이 남도록 길의 밝기를 버퍼 대신 지나간 시각으로 셈하자 0.03 이 됐어요. 잉크가 크림색(채도 0.28)일 때는 Hue 가 0.02 여서 금색(채도 0.6)으로 올렸어요. 하다 보니 두벌식은 ㄲ 도 받침으로 받아서 부끄럼의 ㄲ 이 붂 에 앉았다가 뛰고, 괴로워했다는 워 + ㅎ 이 웧 이 됐다가 워해 로 갈라져요. + +Lab 에서 해 보기: Text 레이어로 윤동주 1941 을 기기 Sans-serif 9 px, 라일락 solid, 가운데 맞춤 x 32, y 102 에 얹으면 쪽과 자판 사이 95~109줄의 빈 띠에 지은이와 해가 서요. 기기 글꼴에 한글이 있어야 하고, 여섯 줄이 드는 여섯째와 아홉째 줄에서는 맨 아랫줄 아래쪽 7줄과 겹쳐요. + +기능 감: Lab 에서 친 컴퓨터 키보드의 글쇠가 코드 패턴의 input 으로 들어오는 길이요. 지금은 「서시」 아홉 줄을 코드에 박고 그 음절을 거꾸로 키로 풀어 치는 척하는데, 친 글쇠가 그대로 오면 이 오토마톤에 제 글을 쳐 보며 도깨비불을 볼 수 있어요. + +### Tempo Canon + +주제 5 「수학 · 추상」으로 리듬·펄스 갈래에 서고, 주제 4 「세로 64×128」을 같이 채우는 패턴이에요. 콘론 낸캐로(Conlon Nancarrow)는 1948년쯤부터 자동 피아노 롤에 손으로 구멍을 뚫어 「Studies for Player Piano」를 썼어요. 그 가운데 템포 카논은 같은 선율을 여러 목소리가 정해진 빠르기 비로 쳐요. Study No. 36 은 네 목소리가 17:18:19:20 이고, Study No. 33 은 √2:2, Study No. 40 은 e:π 처럼 분수가 아닌 비를 써요. 카일 갠(Kyle Gann)은 1995년 『The Music of Conlon Nancarrow』에서 모든 목소리가 한 음을 같이 치는 자리를 수렴점(convergence point)이라고 불렀어요. 목소리 i 의 k 번째 음은 t_i(k) = T_c + (s(k) − s(K))/r_i 에 울려서, 수렴하는 음 K 에서 멀수록 빠른 목소리와 느린 목소리의 차이가 벌어져요. 패턴은 128줄을 시간으로 써요. 롤이 Speed 2 에서 1초에 12줄씩 위로 흐르고, 76번 줄의 재생선을 지나는 음이 7줄 동안 하얗게 번쩍여요. 목소리마다 칸이 하나이고, 가장 느린 목소리가 가운데 칸에, 빠른 목소리가 왼쪽과 오른쪽으로 번갈아 바깥에 서요. 음은 칸 안 8 음높이 자리에 2줄 높이 막대로 서고, 그 목소리의 빠르기로 음 길이의 72% 만큼 가는 꼬리를 끌어요. 같은 음을 모든 칸에 걸쳐 잇는 옅은 선이 주인공이에요. 수렴점 앞의 선은 바깥 칸에서 아래로 휘고, 수렴점에서는 패널을 가로지르는 곧은 한 줄로 누워 22줄 동안 하얗게 빛나고, 그 뒤의 선은 반대로 휘어요. 카논마다 18~26음의 새 선율을 짓고, 수렴점은 4분의 1은 첫 음, 4분의 1은 끝 음, 나머지는 30~70% 사이에 둬요. 한 카논은 115줄쯤이라 10초에 재생선을 지나요. K1 Hue 를 0 에 두면 가운데 칸이 금색이고 바깥으로 초록을 지나 하늘색이 돼요. K3 Spread 는 이웃 목소리의 빠르기 차이를 −1 에서 1/17, 처음 자리 0 에서 1/4, 1 에서 1 로 바꿔요. 네 목소리가 17:18:19:20, 4:5:6:7, 1:2:3:4 로 치는 자리예요. K4 Voices 는 칸을 4개에서 12개로 늘려 칸 너비가 16 LED 에서 5 LED 로 줄어요. 잰 값은 움직임 1.458, 촘촘함 0.081, 가장자리 0.599, 복잡도 0.836, 채움 0.291, 색 0.180, 모양 바뀜 0.144 이고, thirds 는 33/34/18, 노브는 0.07, 0.12, 0.09, 0.12 예요. 첫 판은 Spread 를 K4 에 두어 Study No. 36 의 1/17 에서 열었어요. 60줄 떨어진 음에서 선이 9줄만 휘어 화면이 네 줄기 블록 기둥으로 읽혔고, 음 길이 72% 를 다 채운 막대 사이에서 밝기 0.3 의 선은 패널에서 거의 사라졌어요. 그때 잰 값은 복잡도 0.749, 채움 0.339, 노브 0.10, 0.12, 0.13, 0.07 이었어요. Spread 를 K3 의 −1..1 로 옮겨 4:5:6:7 에서 열고, 막대를 2줄 머리와 가는 꼬리로 줄이고 선 밝기를 0.62 로 올리자 수렴점 위아래의 휜 선이 겹겹의 지붕처럼 섰어요. 아직 칠 음을 0.5 밝기로 두었더니 금색이 갈색으로 보여서, 0.62 밝기에 흰빛을 32% 섞었어요. 12 목소리에서는 칸마다 빠르기가 같은 차이로 오르는데 늦음은 1/빠르기 로 줄어서, 선이 곧은 꺾쇠가 아니라 느린 가운데 칸에서 가장 가파른 곡선으로 휘어요. + +### Gosper Islands + +주제 5 「수학 · 추상」으로 프랙탈 갈래에 서는 패턴이에요. 빌 고스퍼(Bill Gosper)가 1973년에 그린 플로우스네이크 곡선의 바깥 경계가 고스퍼 섬이에요. 마틴 가드너(Martin Gardner)가 1976년 12월 『사이언티픽 아메리칸』에 실었고, 브누아 망델브로(Benoit Mandelbrot)가 1982년 『자연의 프랙탈 기하학』에서 섬이라는 이름을 붙였어요. 섬 일곱이 모이면 √7 배 큰 섬 하나가 되고, 그 섬은 arctan(√3/5), 곧 19.1도 돌아가 있어요. 패턴은 곡선을 선으로 긋지 않고 에이젠슈타인 정수(a + bω, ω = e^(2πi/3))의 기수법으로 섬을 셈해요. 밑 β = 2 − ω 는 노름이 7 이라 β 로 나눈 나머지가 일곱 가지이고, 0 과 단위 여섯(±1, ±ω, ±ω²)이 하나씩 맡아요. 나머지는 (a + 2b) mod 7 이고, 그 숫자를 떼고 β 로 나누면 ((3a − b) + (a + 2b)ω)/7 이 정수로 떨어져요. |a| 와 |b| 가 300 이하인 361,201 개를 다 풀어 보니 여덟 자리 안에서 끝났어요. LED 마다 가장 가까운 격자점을 잡고 숫자를 한 자리씩 떼면, l 자리를 뗀 몫이 그 LED 가 든 l 단계 섬이고 l 번째 숫자는 그 섬이 한 단계 큰 섬의 일곱 조각 가운데 어느 것인지 말해요. 0 단계 격자는 0.16 LED 간격이라 그보다 큰 섬의 해안선은 LED 하나까지 맞아요. 화면은 판 가운데로 끝없이 들어가요. 평면을 β^−t 로 보면 t 가 1 오를 때 모든 단계가 한 칸씩 올라가 이름만 바뀐 같은 그림이 되니, Speed 2 에서 7초마다 2.65배 커지고 19.1도 돌며 이음매 없이 돌아요. 고리 끝과 처음 프레임의 차이는 0.0035 로, 보통 프레임 사이의 0.0017~0.0020 과 비슷해요. 색은 55 LED 간격 단계의 숫자로 정하고, 한 단계 아래 숫자로 밝기를 나눠요. 가운데 조각이 가장 밝아요. 둘 다 한 바퀴 동안 한 단계 아래로 미끄러져서, 작은 조각은 제 부모가 화면을 떠날 즈음 제 숫자의 색이 돼요. 해안선은 이웃 LED 가 다른 섬에 들 때 켜고, 12.7 LED 간격 섬부터는 양쪽을 다 켜서 2 LED 너비예요. K3 Depth 는 해안선을 어느 단계까지 그릴지 정해요. 1 에서는 7.8 LED 간격 섬부터 그리고, 3 에서는 2.4 LED 간격 섬까지 내려가 섬마다 제 일곱 조각이 레이스로 비쳐요. K4 Hexagon 은 섬을 더 굵은 조각으로 지어요. 0.15 아래는 참 섬, 0.45 까지는 육각형 49개, 0.8 까지는 일곱 개(고스퍼의 꽃), 그 위는 섬마다 육각형 하나라 섬이 자라 나온 육각 타일이 보여요. 잰 값은 움직임 0.634, 촘촘함 0.088, 가장자리 0.799, 복잡도 0.342, 채움 1.000, 색 0.266, 모양 바뀜 0.091 이고, thirds 는 100/100/100, 노브는 0.24, 0.26, 0.09, 0.14 예요. 첫 판은 55 LED 간격 섬만 칠해서 한 화면에 색이 두셋뿐이었고, 가운데 조각을 옅게 둔 탓에 판 한가운데가 회보라로 흐렸어요. 20 LED 간격 섬을 칠하자 색 타일이 빽빽해져서 섬 일곱이 하나를 이루는 것이 묻혔어요. 색은 큰 단계에, 밝기는 작은 단계에 나눠 주고서야 일곱 묶음이 읽혔어요. 고리를 5초로 두었을 때 움직임은 0.817 이었고, 7초로 늘려 0.634 가 됐어요. 패널에서는 해안선의 주름이 두 단계쯤만 보였어요. 처음 화면에서 육각형 49개로 지은 섬은 LED 의 6~8.5% 만 참 섬과 달라서, Hexagon 노브가 눈에 띄게 바꾸는 것은 꽃 단계(14~19%)와 육각형 단계(29~37%)예요. + +### Axelrod Culture + +주제 5 「수학 · 추상」으로 시뮬레이션 갈래에 서는 패턴이에요. 로버트 액설로드(Robert Axelrod)는 1997년 『Journal of Conflict Resolution』에 낸 「The Dissemination of Culture」에서 마을의 문화를 F 가지 특징으로, 특징마다 q 가지 형질 가운데 하나로 뒀어요. 무작위로 고른 마을은 무작위 이웃과 이미 같은 특징의 몫만큼의 확률로 만나고, 만나면 서로 다른 특징 하나를 이웃 것으로 바꿔요. 아무것도 나누지 않는 이웃끼리는 만나지 않아서, 닮을수록 더 닮다가 이웃 지역끼리 특징을 하나도 나누지 않는 지도로 얼어요. 패턴은 마을 16×32 개를 4×4 LED 칸에 깔고 F = 3 으로 둬요. 첫 특징은 반 바퀴 너비 색상 띠의 자리를, 둘째는 밝기(0.55~1)와 색상을 ±0.05 미는 몫을, 셋째는 채도(0.3~1)를 정해요. 형질은 황금비의 배수로 흩어서 이웃한 번호가 먼 색이 돼요. 칸마다 오른쪽 열과 아래 줄은 다음 마을과의 경계예요. 셋을 다 나누면 칸 색 그대로, 둘이면 80%, 하나면 40%, 하나도 없으면 검게 칠해요. 액설로드 논문의 그림이 닮은 정도를 경계 짙기로 그린 것과 같은 셈이에요. 색과 경계는 0.1초에 걸쳐 따라가서 미끄러지는 경계가 깜빡이지 않아요. Speed 2 에서 1초에 24번 쓸고, 한 번 쓸 때 512번 시도해요. 활성 결합(특징을 일부만 나누는 이웃)이 12 개 이하로 120번 쓰는 동안(Speed 2 에서 5초) 머물면 자리 잡은 것으로 보고, 새 무작위 격자가 1.25초에 걸쳐 위에서부터 반짝이며 내려와요. K1 Hue 는 색상 띠를 돌려요. 0 에서는 초록에서 청록, 파랑, 보라를 지나 마젠타까지이고 빨강은 없어요. K3 Traits 는 q 를 2~30 으로 바꾸고, 돌리면 새 격자가 시작돼요. 표류 없이 이 격자에서 q 마다 열두 번 돌리니 q 4 까지는 늘 한 문화로 끝났고 q 2 에서는 중간값 2690번 쓸었어요. 가장 큰 지역은 q 8 에서 81%, 10 에서 48%, 12 에서 12% 였고, 30 에서는 80번 만에 410 개 남짓한 지역으로 얼었어요. 클라우디오 카스텔라노(Claudio Castellano), 마테오 마르실리(Matteo Marsili), 알레산드로 베스피냐니(Alessandro Vespignani)가 2000년에 찾은 q 의 전이가 이 격자에서는 10 과 12 사이에 서요. K4 Drift 는 콘스탄틴 클렘(Konstantin Klemm), 빅토르 에길루스(Víctor M. Eguíluz), 라울 토랄(Raúl Toral), 막시 산 미겔(Maxi San Miguel)이 2003년에 더한 문화 표류예요. 시도마다 0.02 × Drift² 의 확률로 그 마을의 특징 하나를 무작위 형질로 다시 뽑고, 바뀐 마을은 희게 번쩍여요. 처음 자리 0.06 은 Speed 2 에서 1초에 0.9번이에요. q 10 에서 14,400번 쓸기를 여섯 번 돌려 견주니, 표류가 없을 때 48% 에서 얼던 가장 큰 지역이 0.06 에서는 66% 까지 컸어요. 얼어붙은 경계의 마을이 표류로 건너편과 특징 하나를 나누게 되면 다시 말을 섞기 때문이에요. 0.2 에서는 28%, 0.3 에서는 13% 로 잡음이 이겨요. 잰 값은 움직임 0.669, 촘촘함 0.056, 가장자리 0.358, 복잡도 0.678, 채움 0.991, 색 0.380, 모양 바뀜 0.106 이고, thirds 는 98/99/99, 노브는 0.32, 0.22, 0.30, 0.23 이에요. 첫 판은 첫 특징만 색상을 정해서 q 2 의 화면이 초록과 파랑 두 빛뿐이었어요. 둘째 특징이 색상을 ±0.05 밀게 하자 민트, 초록, 파랑, 보라가 섞인 지도가 됐고 노브는 0.36, 0.25, 0.28, 0.22 에서 지금 값으로 거의 그대로였어요. 패널에서 새로 본 것은 q 에 따라 그림의 결이 갈리는 것이에요. q 2 에서는 처음 무작위 이웃 8쌍 가운데 1쌍만 아무것도 나누지 않아서 검은 선이 거의 없고, 지역은 가장자리가 보글거리는 얼룩으로 커지다가 45초쯤에는 패널 위 절반이 초록 한 지역이 돼요. q 12 에서는 얼어붙은 지역마다 검은 선이 둘러서 납선을 두른 스테인드글라스처럼 읽혀요. Hegselmann-Krause(76판)는 연속 의견이 신뢰 구간 안에서 모였고, 여기서는 격자 위의 이산 문화가 겹친 만큼만 말을 섞어요. + +### Desargues Configuration + +주제 5 「수학 · 추상」으로 공간·원근 갈래에 서는 패턴이에요. 지라르 데자르그(Girard Desargues)의 삼각형 정리는 아브라암 보스(Abraham Bosse)가 1648년 『Manière universelle de M. Desargues』에 처음 실었어요. 두 삼각형의 대응하는 꼭짓점을 잇는 세 직선이 한 점 O 에서 만나면, 대응하는 변끼리 만나는 세 점이 한 직선 위에 선다는 정리예요. 점 열 개와 직선 열 개가 직선마다 점 셋, 점마다 직선 셋으로 얽혀서 배치 (10_3) 이라고 불러요. 평면에서는 증명이 필요하지만 공간에서는 그대로 보여요. 꼭짓점이 O 인 삼각뿔을 두 평면으로 자르면 두 단면이 O 에서 본 원근 삼각형이 되고, 대응하는 변은 같은 옆면에 있으니 만나요. 그 만남점은 두 평면에 다 있으니 두 평면이 만나는 직선 위에 있어요. 패턴은 이 장면을 그대로 그려요. 패널 위쪽 O 에서 모서리 셋이 2 LED 굵기로 내려오고, 위 평면의 삼각형은 청록, 아래 평면의 삼각형은 금색으로 2.6 LED 변과 3분의 1 밝기의 면을 가져요. 두 평면은 저마다 제 삼각형과 축 위 세 점을 감싼 옅은 판으로 깔려서, 분홍 축에서 만나는 것이 보여요. 변을 늘인 선은 1.6 LED 로 흰 원판 셋까지 가고, Speed 2 에서 2.4초마다 같은 점으로 가는 두 선에 빛이 같이 달려가 닿으면 원판이 부풀어요. 장면은 가운데 세로축을 두고 12초에 25도씩 양쪽으로 흔들려요. 투영은 직선을 직선으로, 만남점을 만남점으로 남기니 어느 프레임이나 평면 정리의 그림이에요. 만남점은 저마다 삼각뿔의 한 옆면이 축과 만나는 점이라 두 평면의 각에 매이지 않아요. 그래서 K3 Tilt 를 30도에서 56도로 돌리면 삼각형만 모서리를 따라 미끄러지고 세 점은 한 LED 도 움직이지 않아요. 화면 맞춤을 View 가 바뀔 때만 다시 셈해서 화면 위에서도 그래요. K4 View 는 카메라가 내려다보는 각을 0도에서 50도까지 바꾸고, 여는 값은 26.5도예요. 0도 쪽에서는 흔들림 한끝에서 눈이 금색 평면에 가까워져 금색 삼각형이 가는 조각으로 얇아져요. 공간·원근 갈래의 스물다섯 패턴은 곡면과 다면체, 렌즈를 그렸고, 점과 직선의 배치를 그린 것은 이것이 처음이에요. 잰 값은 움직임 0.148, 촘촘함 0.041, 가장자리 0.366, 복잡도 0.780, 채움 0.278, 색 0.120, 모양 바뀜 0.091 이고 thirds 는 12/48/21, 노브는 0.04, 0.09, 0.04, 0.07 이에요. 패널에서 새로 본 것은 구도의 값이에요. O 를 위에 두고 축을 아래에 가로로 두면, 64×128 안에 열 점이 다 드는 평면 배치를 찾아도 두 삼각형이 겹쳤어요. 가장 나은 배치가 두 중심 사이 거리 0.47(두 반지름 합에 대한 몫)에 축 위 점 사이 14 LED 였어요. 축이 기울어도 되게 하자 0.88 과 25 LED 가 나와서, 축을 공간에서 46도 내려가는 선으로 두었어요. 패널에서는 축이 왼쪽 가장자리 62행 언저리에서 오른쪽 아래 모서리로 비스듬히 흘러요. 축을 기울이기 전 구도에서는 Hue 0.03, Tilt 0.02 로 약했고, 지금 구도에서 모서리를 옅은 보라로 칠하고 삼각형 면을 채워 둘 다 0.04 가 됐어요. + +### Magic Lines + +주제 5 「수학 · 추상」으로 수론 갈래에 서는 패턴이에요. 클로드 브래그던(Claude Bragdon)은 1915년 『Projective Ornament』에서 마방진의 1 이 든 칸에서 2 가 든 칸으로, 다시 3 으로 n² 까지 선을 그으면 마방진의 대칭이 그대로 무늬로 나온다고 보이고, 그 선을 장식의 바탕으로 내놓았어요. 패턴은 마방진을 코드에서 지어요. 홀수 차수는 시몽 드 라 루베르(Simon de la Loubère)가 1693년에 전한 샴 방법과, 두 칸 위 한 칸 오른쪽으로 가는 나이트 걸음 방법이에요. 나이트 걸음으로 지은 5차와 7차는 꺾인 대각선까지 합이 맞는 범대각 마방진이에요. 4차는 알브레히트 뒤러(Albrecht Dürer)의 「멜랑콜리아 I」(1514) 마방진, 4의 배수 차수 규칙으로 지은 것, 카주라호 파르스바나타 사원(10세기)의 범대각 마방진 셋이고, 6차와 10차는 존 호턴 콘웨이(John Horton Conway)의 LUX 방법이에요. 처음 화면은 8×16 마방진 직사각형이에요. 토머스 하르무트(Thomas Harmuth)가 1881년에 두 변의 홀짝이 같으면 2×2 말고는 늘 있다고 보인 꼴이고, 8 LED 칸 128개가 패널을 다 채워요. 4의 배수 규칙을 16행 8열에 그대로 돌리면 행마다 516, 열마다 1032 가 되고, k 와 129 − k 가 가운데를 두고 마주 앉아서 1 부터 128 까지 잇는 선이 패널 한가운데로 모이는 반 바퀴 대칭의 빛살이 돼요. Speed 2 에서 직사각형은 15.2초, 3×3 은 3.3초, 8×8 은 8.8초에 다 그어지고, 펜 머리는 칸마다 잠깐 멈췄다 넘어가요. 선은 1 의 청록에서 마지막 칸의 자홍으로 색이 옮아서 다 그은 무늬에도 그은 차례가 남아요. 펜이 닿은 칸은 잠깐 밝아지고, 5차까지는 칸 왼쪽 위에 그 수가 3×5 숫자로 적혀요. K3 Order 는 0 이 직사각형 넷(4의 배수 규칙 둘, 짝지은 열 사이를 오가며 엮는 것 둘)이고, 1~7 이 3, 4, 5, 6, 7, 8, 10 차 마방진을 64×64 위아래 두 칸에 번갈아 그려요. 목록이 한 바퀴 돌 때마다 다음 대칭으로 돌리거나 뒤집어요. K4 Fade 는 그은 선이 남는 시간이에요. 처음 자리 0.06 에서는 다음 무늬가 시작할 때까지 무늬가 다 남고, 0.5 에서는 직사각형의 선 한 조각이 41걸음(5초) 뒤에 사라지고, 1 에서는 마지막 2.5 걸음만 남아 펜이 짧은 혜성이 돼요. setup 의 check() 가 열여섯 배열마다 1 부터 N 까지 한 번씩 들었는지, 행과 열, 정사각형이면 두 대각선의 합이 맞는지 따지고, 열여섯 다 통과해요. 잰 값은 움직임 0.046, 촘촘함 0.135, 가장자리 0.708, 복잡도 0.810, 채움 0.469, 색 0.315, 모양 바뀜 0.083 이고, thirds 는 42/56/41, 노브는 0.16, 0.07, 0.21, 0.15 예요. 첫 판은 3×3 낙서(Lo Shu)에서 열었고 움직임 0.109, 노브 0.13, 0.11, 0.25, 0.12 였어요. 직사각형은 처음에 짝지은 열 사이를 오가는 엮기 꼴만 있었는데, 패널에서는 56 LED 를 가로질러 8 LED 씩 내려가는 거의 가로인 선이 촘촘한 지그재그로만 보였어요. 4의 배수 규칙을 16×8 에 돌린 것이 훨씬 무늬다워서 첫 자리로 올렸어요. 그 직사각형을 반쯤 그린 채 열었더니 뒤 절반이 앞 절반을 반 바퀴 돌린 선이라 새 선이 옛 선 위에 겹쳐서, Speed 반응이 0.03 이었어요. 15걸음만 그린 채 열자 0.07 이 됐어요. 숫자는 처음에 칸 가운데에 적었는데, 선이 늘 칸 가운데를 지나서 다 가려졌어요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -4904,7 +4983,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 25일 `dev`(`f72355d`)의 328개에 43판부터 82판까지의 사백 개를 더한 728개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 25일 `dev`(`f72355d`)의 328개에 43판부터 82판까지의 사백 개를 더한 728개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. 100판은 공들이기 판의 아홉까지 든 737개를 다시 쟀어요. ### 지도의 빈 칸 @@ -5024,6 +5103,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 82판은 81판까지 든 718개에 빈 칸도, 셋 이하인 칸도 없어서 넷뿐인 칸 다섯 가운데 가장자리 × 촘촘함 2,4 와 복잡도 × 채움 4,1 을 겨눴어요. 고를 때는 같이 도는 81판(#345)이 아직 열려 있어서, 그 PR 이 밝힌 가장자리 × 촘촘함 1,3 과 촘촘함 × 복잡도 4,4 를 비켰어요. 가장자리 × 촘촘함 2,4 는 40/255 넘는 계단의 몫이 0.21~0.50 이면서 이웃 차이 평균이 0.297 을 넘는 칸이에요. 평균 76/255 를 넘는 차이를 계단 절반 아래로 내려면 한쪽 이웃은 크게 끊기고 다른 쪽 이웃은 무르게 이어져야 해요. Fork Hologram 은 거의 세로로 선 주기 3.3 LED 의 줄무늬마다 어둠에서 밝음으로 한 LED 에 뛰는 계단을 두고, 회색 세모파를 35% 남겨 위아래 이웃을 6~40 사이로 달리해서 가장자리 0.414, 촘촘함 0.326 이에요. 밝기를 위상 하나의 함수로만 칠한 줄무늬는 촘촘함 0.30 을 넘기면 가장자리가 0.427 아래로 내려오지 않았어요. 복잡도 × 채움 4,1 은 24/255 넘게 켜진 LED 가 0.302 아래이면서 한 프레임을 deflate 로 눌러도 LED 하나에 2.171 바이트 넘게 남는 칸이에요. 꺼진 LED 가 다 0 이면 그 자리는 거의 공짜로 눌려서, 꺼진 자리도 채널마다 0~24 를 LED 마다 달리 가져야 해요. 합성 프레임으로 재 보니 켜진 몫 0.15 에서는 2.16 을 넘지 못했고, 0.22~0.29 에서 넘었어요. Cantor Scalogram 은 웨이블릿 계수의 위상으로 LED 마다 색을 바꾸고, 신호에 섞은 잡음의 계수를 24 아래 바닥으로 칠해 복잡도 2.275, 채움 0.277 이에요. 열 개 다 일곱 축의 끝 안에 들어 칸 경계는 60판 그대로예요. 81판까지 든 718개에 82판 열 개를 넣은 728개에서 두 칸은 다섯이 됐고, 다섯이던 넷뿐인 칸은 셋이에요. 남은 셋은 가장자리 × 촘촘함 1,4, 복잡도 × 채움 4,2, 복잡도 × 모양 바뀜 4,4 예요. 가장자리 × 촘촘함 1,4 는 40/255 넘는 계단이 21% 아래이면서 평균 차이가 76/255 를 넘어야 해서, 계단을 모두 255 로, 나머지를 모두 40 으로 둬도 평균이 85 라 남는 틈이 좁아요. +100판을 짓기 전 737개에서 `*` 가 붙은 칸은 움직임 × 모양 바뀜 1,4 하나였어요. 1,3 도 비었지만 축 끝 패턴 하나를 빼면 차서 `*` 가 없어요. 1,4 는 움직임이 0.0367 아래이면서 모양 바뀜이 0.328 을 넘어야 하는 칸이에요. 둘은 같은 8초의 프레임에서 재요. 모양 바뀜은 첫 프레임과 어느 프레임 사이 16×8 칸 평균의 차이라서, 그 사이에 LED 가 프레임마다 바뀐 양을 다 더한 값보다 클 수 없어요. 움직임 0.0367 은 239번의 프레임 사이에 0.0367 × 7.97 = 0.293 을 쌓으니 0.328 에 닿지 않아요. 축 끝이 그대로인 동안 어떤 패턴도 설 수 없는 칸이라 겨누지 않았어요. 움직임 축 위 끝이 4.91 에서 9.3 을 넘어야 첫 열의 경계가 0.041 로 올라와 이 칸이 열려요. + ### 아직 없는 갈래 `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 가 섰어요. @@ -5198,9 +5279,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 82판에서는 Exclusion Process 와 Lensed Page 가 128줄을 써요. Exclusion Process 는 128줄이 지난 8초의 시공간 그림이라 59/62/65 이고, 첫 프레임 앞에 128줄을 미리 채워서 처음부터 가득 차요. Lensed Page 는 98행 쪽이 되풀이되며 위로 흘러서 43/47/53 이에요. 둘 다 벽이나 테두리는 없어요. 나머지 여덟 가운데 Crofton Cell 은 88/88/94, Fork Hologram 은 75/77/74, Phasor Noise 는 64/57/56 으로 판을 덮어요. Letter Retracts 는 글자 셋을 쌓아 44/30/27, Noordzij Cube 는 36/39/24, Word Search 는 36/30/31, Alphabet Lineage 는 24/30/24 예요. Cantor Scalogram 은 자리를 세로에 둬서 위 3분의 1이 가장 차 38/27/17 이에요. +100판에서는 Tempo Canon 과 Runestone 이 128줄을 써요. Tempo Canon 은 시간이 128줄을 타고 흐르는 피아노 롤이라 33/34/18 이에요. Runestone 은 뱀 띠가 머리부터 꼬리까지 세 굽이로 128줄을 내려가요. 회색 돌결이 판을 다 덮어 97/98/97 이고, 색이 든 LED 와 70 을 넘는 LED 만 세면 45/54/45 예요. 주제 4 로 겨누지 않은 Magic Lines 도 8×16 마방 직사각형의 선이 판을 다 써서 42/56/41 이에요. + ### 낯선 수학 -주제 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)예요. 74판은 다섯을 더했어요. 콘웨이의 유리 엉킴과 슈베르트의 2다리 매듭(Rational Tangles), 처치의 람다 계산과 트롬프의 람다 그림(Lambda Diagrams), 쉬첸베르거의 프로모션과 헤이먼의 주기(Promotion), 리드와 솔로몬의 부호를 쓰는 QR 부호(Reed-Solomon), 투란의 벽돌 공장 문제와 자란키에비치의 그림(Brick Factory)이에요. 75판은 다섯을 더했어요. 콘웨이의 분수 프로그램 PRIMEGAME(FRACTRAN), 야르니크와 쾨슬러의 문제와 길버트, 폴락의 비 추측(Steiner Tree), 미우라 고료의 접기(Miura-ori), 크러스컬의 카드 마술과 가드너의 소개(Kruskal Count), 수트너의 σ+ 놀이와 빛 쫓기(Lights Out)예요. 76판은 다섯을 더했어요. 비티와 레일리의 서로 보완하는 두 수열(Beatty Rhythm), 레비의 안정 분포와 망델브로의 레비 먼지(Lévy Dust), 뉴컴과 벤퍼드의 첫 자리 법칙과 바일의 고른 분포(Benford's Law), 헤겔스만과 크라우제의 신뢰 구간 의견 모형(Hegselmann-Krause), 샤모스와 호이의 차수 k 보로노이 그림과 리의 영역 셈(Order-k Voronoi)이에요. 77판은 다섯을 더했어요. 라비의 두 준위 진동과 그 셰브런(Rabi Chevron), 로사토와 동료들이 흔들어 크기로 가른 몬테카를로(Brazil Nut), 시트니코프의 축 위 셋째 몸과 모저의 기호(Sitnikov Problem), 소딘과 치렐손의 평면 가우스 해석 함수 영점(Gaussian Zeros), 커크패트릭, 젤라트, 베키의 모의 담금질(Simulated Annealing)이에요. 78판은 다섯을 더했어요. 에카르트와 가스파르, 라이스의 세 원판 산란(Three-Disk Scattering), 폭스의 매듭 색칠과 알렉산더의 닫힌 땋기(Fox Colorings), 몰드의 사슬 분수와 비긴스, 워너의 풀이(Chain Fountain), 오일러의 오각수 정리와 프랭클린의 짝짓기(Franklin's Involution), 갈릴레오의 홀수 법칙과 드레이크가 읽은 프렛(Galileo's Frets)이에요. 79판은 다섯을 더했어요. 램지의 분리 진동장(Ramsey Fringes), 아하로노프와 다비도비치, 자구리의 양자 걸음과 하다마르 동전(Quantum Walk), 부니모비치의 버섯 당구(Mushroom Billiard), 하위헌스의 이상한 공감(Huygens' Clocks), 라가리아스와 레인스, 슬론의 EKG 수열(EKG Sequence)이에요. 80판은 다섯을 더했어요. 비어든과 두베이코, 스티븐슨이 다룬 도일 나선(Doyle Spiral), 프롭과 윌슨의 과거로부터의 결합(Coupling from the Past), 멩거 스펀지의 대각선 단면(Menger Slices), 렌이 적은 곧은 선의 쌍곡면과 슈호프의 탑(Ruled Hyperboloid), 므나차카냔의 접선 쓸기 정리(Mamikon's Theorem)예요. 81판은 다섯을 더했어요. 클루스테르만과 베유의 셈으로 세는 모듈러 쌍곡선(Modular Hyperbola), 모호로비치치의 머리파와 페르마의 최소 시간(Head Wave), 덴의 꼬임과 리코리시의 생성(Dehn Twist), 말파티의 세 원과 잘갈레르, 로스의 탐욕(Malfatti Circles), 폴라드의 로 방법과 플로이드, 브렌트의 고리 찾기(Pollard's Rho)예요. 82판은 다섯을 더했어요. 바젠노프와 바스네초프, 소스킨의 갈래 홀로그램(Fork Hologram), 아르네오도와 그라소, 홀슈나이더가 읽은 칸토어 계단의 웨이블릿 변환(Cantor Scalogram), 스피처의 완전 비대칭 배타 과정과 데리다, 도마니, 무카멜의 상 그림(Exclusion Process), 마일스의 푸아송 직선과 켄들의 추측(Crofton Cell), 라가에와 동료들의 가보르 노이즈와 트리카르와 동료들의 페이저 노이즈(Phasor Noise)예요. +주제 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)예요. 74판은 다섯을 더했어요. 콘웨이의 유리 엉킴과 슈베르트의 2다리 매듭(Rational Tangles), 처치의 람다 계산과 트롬프의 람다 그림(Lambda Diagrams), 쉬첸베르거의 프로모션과 헤이먼의 주기(Promotion), 리드와 솔로몬의 부호를 쓰는 QR 부호(Reed-Solomon), 투란의 벽돌 공장 문제와 자란키에비치의 그림(Brick Factory)이에요. 75판은 다섯을 더했어요. 콘웨이의 분수 프로그램 PRIMEGAME(FRACTRAN), 야르니크와 쾨슬러의 문제와 길버트, 폴락의 비 추측(Steiner Tree), 미우라 고료의 접기(Miura-ori), 크러스컬의 카드 마술과 가드너의 소개(Kruskal Count), 수트너의 σ+ 놀이와 빛 쫓기(Lights Out)예요. 76판은 다섯을 더했어요. 비티와 레일리의 서로 보완하는 두 수열(Beatty Rhythm), 레비의 안정 분포와 망델브로의 레비 먼지(Lévy Dust), 뉴컴과 벤퍼드의 첫 자리 법칙과 바일의 고른 분포(Benford's Law), 헤겔스만과 크라우제의 신뢰 구간 의견 모형(Hegselmann-Krause), 샤모스와 호이의 차수 k 보로노이 그림과 리의 영역 셈(Order-k Voronoi)이에요. 77판은 다섯을 더했어요. 라비의 두 준위 진동과 그 셰브런(Rabi Chevron), 로사토와 동료들이 흔들어 크기로 가른 몬테카를로(Brazil Nut), 시트니코프의 축 위 셋째 몸과 모저의 기호(Sitnikov Problem), 소딘과 치렐손의 평면 가우스 해석 함수 영점(Gaussian Zeros), 커크패트릭, 젤라트, 베키의 모의 담금질(Simulated Annealing)이에요. 78판은 다섯을 더했어요. 에카르트와 가스파르, 라이스의 세 원판 산란(Three-Disk Scattering), 폭스의 매듭 색칠과 알렉산더의 닫힌 땋기(Fox Colorings), 몰드의 사슬 분수와 비긴스, 워너의 풀이(Chain Fountain), 오일러의 오각수 정리와 프랭클린의 짝짓기(Franklin's Involution), 갈릴레오의 홀수 법칙과 드레이크가 읽은 프렛(Galileo's Frets)이에요. 79판은 다섯을 더했어요. 램지의 분리 진동장(Ramsey Fringes), 아하로노프와 다비도비치, 자구리의 양자 걸음과 하다마르 동전(Quantum Walk), 부니모비치의 버섯 당구(Mushroom Billiard), 하위헌스의 이상한 공감(Huygens' Clocks), 라가리아스와 레인스, 슬론의 EKG 수열(EKG Sequence)이에요. 80판은 다섯을 더했어요. 비어든과 두베이코, 스티븐슨이 다룬 도일 나선(Doyle Spiral), 프롭과 윌슨의 과거로부터의 결합(Coupling from the Past), 멩거 스펀지의 대각선 단면(Menger Slices), 렌이 적은 곧은 선의 쌍곡면과 슈호프의 탑(Ruled Hyperboloid), 므나차카냔의 접선 쓸기 정리(Mamikon's Theorem)예요. 81판은 다섯을 더했어요. 클루스테르만과 베유의 셈으로 세는 모듈러 쌍곡선(Modular Hyperbola), 모호로비치치의 머리파와 페르마의 최소 시간(Head Wave), 덴의 꼬임과 리코리시의 생성(Dehn Twist), 말파티의 세 원과 잘갈레르, 로스의 탐욕(Malfatti Circles), 폴라드의 로 방법과 플로이드, 브렌트의 고리 찾기(Pollard's Rho)예요. 82판은 다섯을 더했어요. 바젠노프와 바스네초프, 소스킨의 갈래 홀로그램(Fork Hologram), 아르네오도와 그라소, 홀슈나이더가 읽은 칸토어 계단의 웨이블릿 변환(Cantor Scalogram), 스피처의 완전 비대칭 배타 과정과 데리다, 도마니, 무카멜의 상 그림(Exclusion Process), 마일스의 푸아송 직선과 켄들의 추측(Crofton Cell), 라가에와 동료들의 가보르 노이즈와 트리카르와 동료들의 페이저 노이즈(Phasor Noise)예요. 100판은 다섯을 더했어요. 낸캐로의 템포 카논과 갠이 이름 붙인 수렴점(Tempo Canon), 고스퍼의 섬과 에이젠슈타인 정수의 밑 2 − ω 기수법(Gosper Islands), 액설로드의 문화 전파 모형(Axelrod Culture), 데자르그의 정리와 보스가 낸 책(Desargues Configuration), 브래그던의 마법 선과 하르무트의 마방 직사각형(Magic Lines)이에요. 목록 밖에서 다음에 해 볼 것이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -5314,6 +5397,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 82판에서 본 것이에요. 둘은 정리가 말하는 극한이 패널에서 어디까지 오는지를 보였어요. Crofton Cell 의 영점 칸은 Big 1 에서 변이 8.5 개, 내접원이 바깥 원의 0.65 까지 둥글어지고 거기서 멈춰요. 원판 둘레의 모서리가 r 의 (l r)^(−2/3) 쯤 튀어나와서, 폭 64 LED 에는 더 둥근 칸이 들어가지 않아요. Exclusion Process 는 4만 단위 시간에서 흐름 0.185 와 0.254 로 정확한 값 0.1875 와 0.25 에 닿았고, 공존선 위의 충격파는 128줄에 9.8칸 떠돌아 2J/(1 − 2α) 의 셈과 맞았어요. 입자와 빈칸을 바꾸고 왼쪽과 오른쪽을 바꾸면 같은 과정이라서, 왼쪽의 색 실과 오른쪽의 빈칸 줄이 같은 기울기로 마주 서요. 둘은 소용돌이 꼴 영점이 줄무늬를 갈랐어요. Fork Hologram 의 갈림은 놓아 둔 소용돌이의 전하라서 전하보다 하나 많은 갈래가 정확히 서고, Phasor Noise 의 갈림은 커널이 서로 지운 자리에서 저절로 생겨요. Im G = |G| sin(arg G) 라서 위 반의 가보르 노이즈가 흐려진 자리마다 아래 반에 갈림이 있어요. Cantor Scalogram 에서는 deflate 가 되풀이를 거의 찾지 못해서 바이트 값의 퍼짐이 복잡도를 정했어요. 켜진 LED 바이트의 12% 가 25 아래로 내려와 바닥과 겹친 판은 1.98 이었고, 켜진 LED 를 25 의 회색 위에 얹자 2.20 이 됐어요. +100판에서 본 것이에요. 둘은 셈이 정확해서 그림이 그대로 돌아왔어요. Gosper Islands 는 밑 2 − ω 로 t 가 1 오를 때 모든 단계가 한 칸씩 올라가 이름만 바뀐 같은 그림이 되니, 7초마다 √7 배 커지고 19.1도 돌아도 고리 끝과 첫 프레임의 차이가 0.0035 로 보통 프레임 사이의 0.0017~0.0020 과 비슷해요. Magic Lines 는 열여섯 배열을 그리기 전에 코드 안에서 행과 열, 대각선을 다 검사해요. 둘은 그림이 버티는 범위를 쟀어요. Axelrod Culture 는 16×32 마을에서 드리프트 없이 q 마다 열두 판을 돌렸어요. 가장 큰 지역이 q = 4 까지 100%, 8 에서 81%, 10 에서 48%, 12 에서 12% 라서 전이가 10 과 12 사이에 있었어요. Desargues Configuration 은 O 를 위에, 축을 아래에 수평으로 두면 찾아본 배치마다 두 삼각형이 겹쳐서, 축을 비스듬히 눕혔어요. 사영은 점과 선의 만남을 지키니 장면이 흔들리는 동안 어느 프레임이나 평면의 데자르그 그림이에요. Tempo Canon 에서는 Lab 이 여는 노브 자리가 노브 차례를 정했어요. Spread 를 K4 에 두면 0.06 근처, 곧 17:18:19:20 에서 열려 선이 거의 휘지 않아서, K3 에 −1..1 로 두고 0 인 4:5:6:7 에서 열었어요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. @@ -5414,6 +5499,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 목록 밖에서 82판 뒤에 해 볼 것이에요. 계단에서 출발한 TASEP 은 밀도가 1 에서 0 까지 곧은 비탈로 퍼지는 희박 부채꼴(헤르만 로스트 Hermann Rost, 1981)을 세우고, 입자 자리를 높이로 옮긴 모서리 자람의 요동은 쿠르트 요한손(Kurt Johansson)이 2000년에 보인 트레이시-위돔 분포를 따라요. Exclusion Process 의 출발만 바꾸면 돼요. 존 나이(John Nye)와 마이클 베리(Michael Berry)가 1974년에 낸 파면 전위는 무작위 파동장에서 위상이 한 바퀴 감기는 영점이라, Phasor Noise 의 G = 0 만 감기는 방향대로 두 색으로 찍으면 짝으로 나고 짝으로 사라지는 것이 보여요. 베리는 2004년에 전하가 1/2 처럼 정수가 아닌 계단 위상에서 나온 빛이 부호가 번갈아 드는 소용돌이 사슬을 낸다고 보였어요. Fork Hologram 의 전하를 1 에서 2 로 천천히 돌리면 새 갈래가 자라는 자리가 보일 거예요. 데이비드 켄들(David G. Kendall)의 1984년 모양 공간에서 평면 세 점이 이루는 삼각형의 모양은 반지름 1/2 인 구면 위 한 점이고, 꼭짓점을 가우스 분포로 뽑으면 그 점이 구면에 고르게 퍼져요. 세운 패널 위에 삼각형을, 아래에 구면 위로 쌓이는 점을 둘 수 있어요. 스테판 자파르(Stéphane Jaffard)는 1996년에 리만 함수의 특이점 스펙트럼을 웨이블릿으로 셈했어요. Cantor Scalogram 의 셈을 62판 Riemann Function 의 신호에 걸면 유리수 p/q 마다 세기가 다른 원뿔이 서요. +목록 밖에서 100판 뒤에 해 볼 것이에요. 레오폴트 크로네커(Leopold Kronecker)의 근사 정리(1884)는 빠르기 비가 유리수로 서로 묶이지 않는 목소리들의 박이 꼭 같이 다시 모이지는 않아도 원하는 만큼 가깝게는 몇 번이고 다시 모인다고 해요. Tempo Canon 의 칸과 잇는 선을 끝없는 박 열에 그대로 두고, 모든 목소리의 가장 가까운 박이 ε 안에 들 때마다 수렴선을 밝히면 거의 수렴이 돌아오는 간격을 128줄에서 읽을 수 있어요. 임레 커터이(Imre Kátai)와 야노시 서보(János Szabó)가 1975년에 보인 가우스 정수의 밑 −2 + i, 숫자 0~4 기수법이에요. 섬 다섯이 √5 배 큰 섬 하나가 되고 한 단계에 153.4도 돌아서, islands() 의 나머지를 (a + 2b) mod 5 로, 나눗셈을 ((b − 2a) + (−a − 2b)i)/5 로, 격자를 정사각으로 바꾸면 같은 확대 고리가 돌아요. |a|, |b| 가 200 이하인 가우스 정수는 다 열 자리 안에서 끝나요. 후안 카를로스 곤살레스아벨랴(Juan Carlos González-Avella)와 동료들이 2005년에 보인 대로, 액설로드 모형에 모두가 듣는 대중 매체 문화를 하나 더하면 매체가 셀수록 오히려 문화가 여럿으로 갈라져요. Axelrod Culture 의 attempt() 에서 이웃 대신 확률 B 로 고정된 매체 문화를 고르게만 하면 되고, B 를 노브에 걸면 지도가 잘게 얼어붙는 자리가 보여요. 파스칼의 육각형 정리(Blaise Pascal, 『Essay pour les coniques』, 1640)를 해 볼 만해요. 타원 위 여섯 점이 만드는 육각형의 마주 보는 변 세 쌍이 한 직선 위에서 만나는 정리라, 이 패턴의 굵은 선, 만남점 원판, 축 그리기와 흔들림을 그대로 쓰고 삼각뿔 대신 노브로 타원 위를 미끄러지는 여섯 점을 두면 돼요. 윌리엄 베벌리(William Beverley)가 1848년에 낸 마법 나이트 투어예요. 나이트가 64칸을 한 번씩 밟은 차례가 행과 열마다 260 이 되는 배열이라, Magic Lines 의 orders() 에 8×8 배열 하나를 더하고 check() 에서 대각선 조건만 빼면 선이 나이트 걸음만으로 이어진 무늬가 돼요. + ### 타이포 주제 7 로 해 본 글자 실험과 다음에 해 볼 것이에요. 63판과 64판이 같이 돌며 다섯씩 처음 만들었어요. 63판의 다섯은 한글 자모가 날아와 모아쓰기로 잠기는 조립(Jamo Assembly), 놀턴과 하먼의 기호 모자이크(Type Mosaic), 호프스태터의 트립렛 그림자(Trip-let), 카프리카의 계산 종이(Kaprekar's Routine), 캐럴의 낱말 사다리(Word Ladder)예요. @@ -5570,6 +5657,14 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 82판 뒤에 해 볼 것이에요. 앨런 터로프(Allan Turoff)가 1972년에 만든 보글은 이웃한 칸을 어느 쪽으로든 꺾어 이어 낱말을 읽어서, Word Search 의 곧은 캡슐 대신 굽은 길이 남고 찾기는 접두사 나무를 따라가는 깊이 우선 탐색이 돼요. 스티븐 그레이(Stephen B. Gray)가 1971년에 낸 비트쿼드 셈은 2×2 칸 무늬의 수만 세어 조각 수에서 구멍 수를 뺀 오일러 수를 내서, 흐르는 글줄 옆에서 LED 네 개짜리 창이 글자의 구멍을 세는 판이 될 만해요. 로마 대문자에서 언셜(4세기)과 반언셜을 거쳐 투르 필사실의 카롤링거 소문자(800년쯤)로 가는 길은 Alphabet Lineage 의 다음 걸음이라 A 가 a 로, D 가 d 로 휘어요. 노르트제이 입방체의 셋째 모서리인 이어진 짜임은 글자마다 들어오는 획과 나가는 획을 따로 두고 그 끝끼리 이으면 이음 획이 다음 글자의 속공간을 가로지르지 않아서, 펜을 떼는 판과 떼지 않는 판을 버튼으로 오갈 수 있어요. +100판의 다섯은 옐링 돌의 룬 글을 새기며 내려오는 뱀 띠(Runestone), 위에서부터 한 줄씩 읽히는 센서에 휘는 낱말(Rolling Shutter), 기호를 늘어놓고 다섯씩 묶는 로마 숫자 덧셈(Roman Addition), 낱말을 되풀이해 짠 순위 막대(Zipf Words), 받침이 다음 글자로 뛰는 두벌식 입력(Dokkaebibul)이에요. + +100판에서 본 것이에요. 다섯 다 글자 꼴을 코드에 적었어요. Runestone 의 짧은 가지 룬은 유니코드 룬 블록의 참고 글리프를 그려 보고 맞췄어요. 글자를 담는 틀이 글자보다 먼저 정해졌어요. Runestone 은 띠가 14 LED 이면 9 LED 룬이 칠한 가장자리에 닿아 띠가 노란 사다리로 읽혀서 16 LED 로 넓혔어요. Rolling Shutter 는 글자를 돌려도 꼴이 남도록 획의 거리장으로 그렸어요. 노브가 바꾸는 것이 그림에 보여야 했어요. Zipf Words 는 막대를 곧은 길이와 로그 길이 사이로 바꾸는 노브가 0.02 였어요. 세는 수가 1~7 이라 두 막대가 거의 같았어요. 막대를 개수 × 순위^Power 로 바꾸자 0.07 이 됐고, Power 1 에서 개수 × 순위가 한 줄로 서는지 바로 보여요. Runestone 의 긴 가지에서 짧은 가지로 바뀌는 노브도 꼴만 바꾸면 0.0081 이었고, 룬 사이를 7.6 에서 6.2 LED 로 좁히게 하자 0.07 이 됐어요. 한글은 UTF-8 로 세 바이트라서 Dokkaebibul 은 「서시」 아홉 줄을 넣고 28.8 KB 에서 24.98 KB 로 줄였어요. Roman Addition 은 무작위 합 4,000 개로 덧셈 셈을 검사했어요. + +기능 감은 셋이 글을 코드 패턴과 Lab 사이에 주고받는 길을, 둘이 Text 레이어의 그리기를 넓히는 길을 바랐어요. Zipf Words 는 Text 레이어에 붙여 넣은 긴 글을 패턴이 문자열로 받기를, Dokkaebibul 은 Lab 에서 친 키보드 글쇠가 코드 패턴의 input 으로 오기를, Roman Addition 은 코드 패턴이 바뀌는 합을 Text 레이어로 넘기기를 바랐어요. Runestone 은 Text 레이어가 경로 하나를 따라 글줄을 휘어 놓기를, Rolling Shutter 는 지난 프레임을 쌓아 두고 줄마다 다른 프레임에서 읽는 레이어 효과를 바랐어요. + +100판 뒤에 해 볼 것이에요. 나뭇가지 룬(kvistrunor)은 룬 하나를 줄기 왼쪽 가지 수(몇째 묶음)와 오른쪽 가지 수(그 묶음의 몇째)로 적는 룬 암호예요. Runestone 의 같은 띠와 다섯 획 틀에 Twig 대신 Cipher 노브를 걸어, 낱말이 보통 룬과 가지 룬 사이를 오가게 해 볼 만해요. 결승선 사진기(photo finish)처럼 세로 틈 하나를 시간으로 펼치는 판이에요. 가운데 한 열만 읽어 오른쪽으로 시간을 쌓으면 빨리 지나간 글자는 좁게, 느린 글자는 넓게 찍혀요. 이 패턴의 줄마다 나이를 셈하는 틀에서 줄 대신 열을 시간 축으로 쓰면 돼요. 제르베르 도리야크(Gerbert d'Aurillac)가 980년쯤 되살린 셈판은 1~9 를 새긴 말(아피케스) 하나를 열마다 놓아서, 같은 덧셈이 기호 쌓기 대신 열마다 넘기는 받아올림이 돼요. Roman Addition 의 두 수를 아피케스 열로 나란히 세우면 자리값이 들어오는 순간이 한 판에 보여요. 한 글을 쪽으로 쓰되 처음 나온 낱말만 밝게, 되풀이된 낱말은 어둡게 써서 쪽이 내려갈수록 밝은 글자가 줄어드는 실험이에요. 새 낱말 수가 읽은 낱말 수의 거듭제곱으로 자란다는 Herdan(1960)과 Heaps(1978)의 법칙을 쪽의 밝기로 읽게 돼요. 공병우가 1949년에 만든 세벌식 타자기처럼 초성과 받침을 다른 키로 치는 세벌식으로 같은 줄을 아래 절반에 같이 쳐 보는 것이에요. 세벌식에는 도깨비불이 없어서, 위 두벌식 줄에서 뛰는 자음이 아래 줄에서는 처음부터 제자리에 앉는 것이 나란히 보여요. + ### 화장품 팝업 주제 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 85321a59b71bb5a93864a6a90ad978e3f1e1f2bd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 04:49:33 +0000 Subject: [PATCH 8/8] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20100=ED=8C=90=20index.h?= =?UTF-8?q?tml=20=EC=9D=84=20747=EA=B0=9C=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20?= =?UTF-8?q?=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_01Bc4JkrYAzT3TWt21dU8pXk --- incoming/pattern-study/README.md | 2 +- incoming/pattern-study/index.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 20b55041..abe450ff 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -5103,7 +5103,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 82판은 81판까지 든 718개에 빈 칸도, 셋 이하인 칸도 없어서 넷뿐인 칸 다섯 가운데 가장자리 × 촘촘함 2,4 와 복잡도 × 채움 4,1 을 겨눴어요. 고를 때는 같이 도는 81판(#345)이 아직 열려 있어서, 그 PR 이 밝힌 가장자리 × 촘촘함 1,3 과 촘촘함 × 복잡도 4,4 를 비켰어요. 가장자리 × 촘촘함 2,4 는 40/255 넘는 계단의 몫이 0.21~0.50 이면서 이웃 차이 평균이 0.297 을 넘는 칸이에요. 평균 76/255 를 넘는 차이를 계단 절반 아래로 내려면 한쪽 이웃은 크게 끊기고 다른 쪽 이웃은 무르게 이어져야 해요. Fork Hologram 은 거의 세로로 선 주기 3.3 LED 의 줄무늬마다 어둠에서 밝음으로 한 LED 에 뛰는 계단을 두고, 회색 세모파를 35% 남겨 위아래 이웃을 6~40 사이로 달리해서 가장자리 0.414, 촘촘함 0.326 이에요. 밝기를 위상 하나의 함수로만 칠한 줄무늬는 촘촘함 0.30 을 넘기면 가장자리가 0.427 아래로 내려오지 않았어요. 복잡도 × 채움 4,1 은 24/255 넘게 켜진 LED 가 0.302 아래이면서 한 프레임을 deflate 로 눌러도 LED 하나에 2.171 바이트 넘게 남는 칸이에요. 꺼진 LED 가 다 0 이면 그 자리는 거의 공짜로 눌려서, 꺼진 자리도 채널마다 0~24 를 LED 마다 달리 가져야 해요. 합성 프레임으로 재 보니 켜진 몫 0.15 에서는 2.16 을 넘지 못했고, 0.22~0.29 에서 넘었어요. Cantor Scalogram 은 웨이블릿 계수의 위상으로 LED 마다 색을 바꾸고, 신호에 섞은 잡음의 계수를 24 아래 바닥으로 칠해 복잡도 2.275, 채움 0.277 이에요. 열 개 다 일곱 축의 끝 안에 들어 칸 경계는 60판 그대로예요. 81판까지 든 718개에 82판 열 개를 넣은 728개에서 두 칸은 다섯이 됐고, 다섯이던 넷뿐인 칸은 셋이에요. 남은 셋은 가장자리 × 촘촘함 1,4, 복잡도 × 채움 4,2, 복잡도 × 모양 바뀜 4,4 예요. 가장자리 × 촘촘함 1,4 는 40/255 넘는 계단이 21% 아래이면서 평균 차이가 76/255 를 넘어야 해서, 계단을 모두 255 로, 나머지를 모두 40 으로 둬도 평균이 85 라 남는 틈이 좁아요. -100판을 짓기 전 737개에서 `*` 가 붙은 칸은 움직임 × 모양 바뀜 1,4 하나였어요. 1,3 도 비었지만 축 끝 패턴 하나를 빼면 차서 `*` 가 없어요. 1,4 는 움직임이 0.0367 아래이면서 모양 바뀜이 0.328 을 넘어야 하는 칸이에요. 둘은 같은 8초의 프레임에서 재요. 모양 바뀜은 첫 프레임과 어느 프레임 사이 16×8 칸 평균의 차이라서, 그 사이에 LED 가 프레임마다 바뀐 양을 다 더한 값보다 클 수 없어요. 움직임 0.0367 은 239번의 프레임 사이에 0.0367 × 7.97 = 0.293 을 쌓으니 0.328 에 닿지 않아요. 축 끝이 그대로인 동안 어떤 패턴도 설 수 없는 칸이라 겨누지 않았어요. 움직임 축 위 끝이 4.91 에서 9.3 을 넘어야 첫 열의 경계가 0.041 로 올라와 이 칸이 열려요. +100판을 짓기 전 737개에서 `*` 가 붙은 칸은 움직임 × 모양 바뀜 1,4 하나였어요. 1,3 도 비었지만 축 끝 패턴 하나를 빼면 차서 `*` 가 없어요. 1,4 는 움직임이 0.0367 아래이면서 모양 바뀜이 0.328 을 넘어야 하는 칸이에요. 둘은 같은 8초의 프레임에서 재요. 모양 바뀜은 첫 프레임과 어느 프레임 사이 16×8 칸 평균의 차이라서, 그 사이에 LED 가 프레임마다 바뀐 양을 다 더한 값보다 클 수 없어요. 움직임 0.0367 은 239번의 프레임 사이에 0.0367 × 7.97 = 0.293 을 쌓으니 0.328 에 닿지 않아요. 축 끝이 그대로인 동안 어떤 패턴도 설 수 없는 칸이라 겨누지 않았어요. 움직임 축 위 끝이 4.91 에서 9.3 을 넘어야 첫 열의 경계가 0.041 로 올라와 이 칸이 열려요. 100판 열 개를 더한 747개에서도 빈 칸은 이 둘 그대로예요. 축 끝을 옮긴 패턴은 없어요. ### 아직 없는 갈래 diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index b34c4f66..094c0153 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

패턴 연구

- - + +