From bc1ddf7f137aa659a08212704edbaef98e8e8825 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 21:08:35 +0000 Subject: [PATCH 01/12] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=2082=ED=8C=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018svN6XCxZJqDPz16WwyFg6 From 109af6e30deaee148af94462d84640726af7a640 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 21:36:53 +0000 Subject: [PATCH 02/12] =?UTF-8?q?=ED=8C=A8=ED=84=B4=2082=ED=8C=90:=20Exclu?= =?UTF-8?q?sion=20Process=20=EC=99=80=20Word=20Search?= 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_018svN6XCxZJqDPz16WwyFg6 --- .../patterns/exclusion-process.js | 162 +++++++++ .../pattern-study/patterns/word-search.js | 341 ++++++++++++++++++ 2 files changed, 503 insertions(+) create mode 100644 incoming/pattern-study/patterns/exclusion-process.js create mode 100644 incoming/pattern-study/patterns/word-search.js diff --git a/incoming/pattern-study/patterns/exclusion-process.js b/incoming/pattern-study/patterns/exclusion-process.js new file mode 100644 index 00000000..b52d0bde --- /dev/null +++ b/incoming/pattern-study/patterns/exclusion-process.js @@ -0,0 +1,162 @@ +// ===== Patternflow pattern ===== +// Title: Exclusion Process +// Family: particle +// Idea: Particles enter a line of 64 sites across the panel at rate alpha, each hops one site right at rate 1 only when that site is empty and leaves the last site at rate beta, and the tall panel is the space-time diagram with the newest row at the top, so every particle draws its own coloured thread up and to the right, a jam stands as a block of steep threads whose holes run back to the left, and at the opening rates, equal and below one half, the front between a sparse stream and a jam wanders to and fro. +// Author: patternflow-lab +// Date: 2026-10-31 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: totally asymmetric simple exclusion process (Frank Spitzer, Interaction of Markov processes, 1970; first written down for ribosomes on messenger RNA by Carolyn MacDonald, Julian Gibbs and Allen Pipkin, 1968), its exact phase diagram with open ends (Bernard Derrida, Eytan Domany and David Mukamel, 1992; Gunter Schütz and Eytan Domany, 1993); pattern round 82 +// Round: 82 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Alpha=0.25..1, Beta=0.25..1 +// +// Knob 1: Hue. Turns the band of thread colours round the wheel. At 0 the particles take hues from cyan through azure and violet to pink, each 0.618 of the band on from the one that entered before it, so neighbouring threads never share a colour; at 0.5 the band runs from red through yellow to green. +// Knob 2: Speed. At 2 the diagram takes 16 new rows a second at the top, each row one unit of the process's time, in which a particle with an empty site ahead hops once on average, so the 128 rows hold the last 8 s; 80 s at 0.2, 5.3 s at 3. +// Knob 3: Alpha. The rate at which a particle enters an empty first site. At the opening 0.25, equal to beta, a stream at density 0.25 meets a jam at density 0.75 and the front between them wanders about 10 sites over the 128 rows; any alpha above beta (with beta below 1/2) is the high-density phase, where the jam fills the line at density 1 - beta and its front backs out of the left end, a quarter of a site a row at alpha 0.5. +// Knob 4: Beta. The rate at which the particle on the last site leaves. Above alpha (and alpha below 1/2) it is the low-density phase, a free stream at density alpha with no jam; with alpha and beta both above 1/2 it is the maximal-current phase, density 1/2 in the middle of the line and a quarter of a particle through every bond per row, however much higher the two rates go. + +// Each site holds at most one particle. In every unit of time each of +// the 65 bonds (the entry, the 63 between sites and the exit) is tried +// once on average, in random order: a particle crosses a bond inside the +// line when the site beyond it is empty, comes in through the entry with +// probability alpha when site 0 is empty and goes out through the exit +// with probability beta. That is random sequential update, the process in +// continuous time sampled once a row. +// +// Derrida, Domany and Mukamel solved the stationary state exactly. The +// current through the line is alpha (1 - alpha) when alpha < beta and +// alpha < 1/2, beta (1 - beta) in the mirror case, and 1/4 when both pass +// 1/2. On alpha = beta < 1/2 the two densities alpha and 1 - beta carry +// the same current J = alpha (1 - alpha), so the front between them, a +// shock, has no drift and walks at random. Its variance grows by +// 2 J / (1 - 2 alpha) a unit of time: 0.75 at the opening, and here it +// wanders 9.8 sites (root mean square) in the 128 rows the panel holds. +// +// Every particle gets a number when it comes in and keeps it, and its +// colour comes from that number. Particles never pass one another, so +// neighbours on the line always hold consecutive numbers, and consecutive +// numbers sit 0.618 of the hue band apart, wrapped. A row stores which particle sits on each +// site at the end of the row, and dimly every site a particle passed +// through during the row, so a particle that hopped three sites leaves a +// short step instead of a gap and its thread stays joined. +// +// Before the first frame the line starts as a step between the two +// densities the ends ask for, runs 120 units to settle and then fills all +// 128 rows, so the panel opens full. + +const W = 64, H = 128, N = W * H; +const { floor, round } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// Time units of the process per row, rows per second at Speed 1. +const DT = 1, RATE = 8; +// Time run before the first row is kept, and the hue band the ids fill. +const SETTLE = 120, BAND = 0.42, PHI = 0.6180339887; + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h), f = h - i, 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 % 6]; +} + +export function setup(params) { + params.s = { + occ: new Int32Array(W), + hist: new Int32Array(N), + trail: new Int32Array(N), + head: 0, + next: 1, + acc: 0, + alpha: 0.25, + beta: 0.25, + hue: 0, + ready: false, + pal: new Uint8Array(256 * 3), + palHue: -1, + }; +} + +// One row: DT units of random sequential update over the W + 1 bonds. +function row(S, keep) { + const occ = S.occ, a = S.alpha, b = S.beta; + let hi = 0, ti = 0; + if (keep) { + S.head = (S.head + 1) % H; + hi = S.head * W; + for (let x = 0; x < W; x++) S.trail[hi + x] = occ[x]; + ti = hi; + } + const tries = (W + 1) * DT; + for (let n = 0; n < tries; n++) { + const k = floor(Math.random() * (W + 1)); + if (k === 0) { + if (occ[0] === 0 && Math.random() < a) { + occ[0] = S.next++; + if (keep) S.trail[ti] = occ[0]; + } + } else if (k === W) { + if (occ[W - 1] !== 0 && Math.random() < b) occ[W - 1] = 0; + } else if (occ[k - 1] !== 0 && occ[k] === 0) { + occ[k] = occ[k - 1]; + occ[k - 1] = 0; + if (keep) S.trail[ti + k] = occ[k]; + } + } + if (keep) for (let x = 0; x < W; x++) S.hist[hi + x] = occ[x]; +} + +function start(S) { + // A step between the two densities the boundaries ask for, then let it settle. + const left = S.alpha < 0.5 ? S.alpha : 0.5, right = S.beta < 0.5 ? 1 - S.beta : 0.5; + for (let x = W - 1; x >= 0; x--) S.occ[x] = Math.random() < (x < W / 2 ? left : right) ? S.next++ : 0; + for (let t = 0; t < SETTLE; t++) row(S, false); + for (let r = 0; r < H; r++) row(S, true); + S.ready = true; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0.25, 0.25], 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.alpha = cl(k[2] == null ? 0.25 : k[2], 0, 1); + S.beta = cl(k[3] == null ? 0.25 : k[3], 0, 1); + if (!S.ready) start(S); + S.acc += d * speed * RATE; + while (S.acc >= 1) { + S.acc -= 1; + row(S, true); + } +} + +function palette(S) { + if (S.palHue === S.hue) return; + S.palHue = S.hue; + for (let i = 0; i < 256; i++) { + const f = (i * PHI) % 1; + const c = hsv(S.hue + 0.47 + f * BAND, 0.75, 1); + S.pal[i * 3] = round(c[0] * 255); + S.pal[i * 3 + 1] = round(c[1] * 255); + S.pal[i * 3 + 2] = round(c[2] * 255); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.ready) return; + palette(S); + const pal = S.pal; + for (let y = 0; y < H; y++) { + const r = ((S.head - y) % H + H) % H, o = r * W; + const fade = 1 - 0.35 * (y / (H - 1)); + for (let x = 0; x < W; x++) { + const id = S.hist[o + x], tr = S.trail[o + x]; + let v = 0, c = 0; + if (id) { c = id & 255; v = fade; } + else if (tr) { c = tr & 255; v = fade * 0.45; } + if (v > 0) display.setPixel(x, y, round(pal[c * 3] * v), round(pal[c * 3 + 1] * v), round(pal[c * 3 + 2] * v)); + else display.setPixel(x, y, 0, 0, 0); + } + } +} diff --git a/incoming/pattern-study/patterns/word-search.js b/incoming/pattern-study/patterns/word-search.js new file mode 100644 index 00000000..c9ba15d5 --- /dev/null +++ b/incoming/pattern-study/patterns/word-search.js @@ -0,0 +1,341 @@ +// ===== Patternflow pattern ===== +// Title: Word Search +// Family: type +// Idea: A word search of 160 capitals, 10 across and 16 down the tall panel, hides twelve words of type vocabulary in all eight directions among random letters, and a white cursor reads it cell by cell the plain way, sending a probe out in each of the eight directions from every letter that starts a word still unfound, until a probe runs a whole word and a rounded capsule in that word's colour closes round it and stays; when all twelve are ringed the grid dissolves and the next puzzle types itself in. +// Author: patternflow-lab +// Date: 2026-10-31 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: word search puzzle (Norman E. Gibat, Selenby Digest, Norman, Oklahoma, 1968), solved by the plain search that tries all eight directions from every cell; capitals 5 by 7 drawn for this pattern; pattern round 82 +// Round: 82 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Puzzle=0..5, Fill=0..1 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the grid is pale blue-grey, the probe reads in pale gold, a failed letter flashes rose, and the capsules come in cyan, violet, rose, azure, magenta and on, each 0.382 of a 0.62 band on from the one before. +// Knob 2: Speed. At 2 each comparison takes 0.03 s and each find holds the search 0.4 s, so the opening puzzle types in for 1 s, has all twelve words ringed by 20 s, holds 1.6 s and dissolves in 1.2 s; at 0.2 everything takes ten times as long, at 3 two thirds as long. +// Knob 3: Puzzle. Which of six lists of twelve words is hidden: 0 SERIF, KERN, GLYPH, BOWL, STEM, SPUR, EAR, TAIL, LEADING, BASELINE, COUNTER and ASCENDER; 1 more parts of a letter, APEX to XHEIGHT; 2 words of the page, TRACKING to HYPHEN; 3 classes of type, ROMAN to MONO; 4 metal type, SORT to FURNITURE; 5 accents and marks, ACUTE to MACRON. Each new puzzle on a list is laid again from its own seed. +// Knob 4: Fill. How a found word is marked. At 0 and the opening 0.06 a capsule outline one LED wide, which for words across and down runs in the gaps between the letters; at 0.5 the capsule is filled at 40% of its colour, and at 1 it is solid at 80% with the word's letters cut out of it dark while every letter outside the found words dims to half. + +// Placing: the words go in longest first, each at the first of 400 random +// starts where every cell is empty or already holds the same letter. Each +// word tries a direction of its own for the first 200 starts, three +// eighths of a turn on from the word before, so a grid has words running +// backwards, upwards and along both diagonals. The cells left over get +// random letters, and a word that the filler happens to spell again is +// found wherever the search reaches it first. +// +// Searching: the cursor visits the cells in reading order. A cell whose +// letter starts no word still unfound costs one step. From any other cell +// the probe tries east, then each direction clockwise; it reads one letter +// a step, keeps the words whose next letter matches and stops at the first +// letter that matches none, which flashes. When a word runs out on a match +// it is found. Directions that leave the grid at once cost nothing. +// +// The capsule round a word is the set of LEDs at distance 3 from the +// segment joining its first and last letter centres, measured with the +// vertical squeezed by 3/4. So the outline sits on the gap row 4 LEDs +// above and below a word that runs across, on the gap column 3 LEDs either +// side of a word that runs down, and the ends round off between the two. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, round, sqrt, abs } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// 10 columns by 16 rows of 5 by 7 capitals, 6 LEDs apart across and 8 down. +const COLS = 10, ROWS = 16, CELLS = COLS * ROWS, CW = 6, CH = 8, X0 = 2, Y0 = 1; +// Clockwise from east, the order the search tries them in. +const DIRS = [[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]]; +// Seconds at Speed 2: one comparison, the pause on a find, the hold when +// every word is found, the dissolve and the typing in of the next grid. +const STEP = 0.03, FIND = 0.4, HOLD = 1.6, MELT = 1.2, TYPE = 1.0; +// Capsule radius across the word, in LEDs; down the panel it is 4/3 of it. +const R = 3, SY = 0.75; + +const LISTS = [ + ["SERIF", "KERN", "GLYPH", "BOWL", "STEM", "SPUR", "EAR", "TAIL", "LEADING", "BASELINE", "COUNTER", "ASCENDER"], + ["APEX", "VERTEX", "SPINE", "ARM", "LEG", "LOOP", "LINK", "CROSSBAR", "SHOULDER", "TERMINAL", "DESCENDER", "XHEIGHT"], + ["TRACKING", "MEASURE", "GUTTER", "INDENT", "WIDOW", "ORPHAN", "RAG", "RIVER", "JUSTIFY", "MARGIN", "COLUMN", "HYPHEN"], + ["ROMAN", "ITALIC", "GOTHIC", "SLAB", "SCRIPT", "GROTESK", "DIDONE", "UNCIAL", "TEXTURA", "FRAKTUR", "ANTIQUA", "MONO"], + ["SORT", "QUOIN", "CHASE", "GALLEY", "FORME", "PLATEN", "PUNCH", "MATRIX", "SLUG", "LIGATURE", "COMPOSE", "FURNITURE"], + ["ACUTE", "GRAVE", "TILDE", "CEDILLA", "OGONEK", "UMLAUT", "CARET", "DAGGER", "PILCROW", "ASTERISK", "OBELUS", "MACRON"], +]; + +// Capitals 5 wide and 7 tall, top row first. +const FONT = ( + ".###. #...# #...# ##### #...# #...# #...#|####. #...# #...# ####. #...# #...# ####.|.###. #...# #.... #.... #.... #...# .###.|" + + "####. #...# #...# #...# #...# #...# ####.|##### #.... #.... ####. #.... #.... #####|##### #.... #.... ####. #.... #.... #....|" + + ".###. #...# #.... #.### #...# #...# .####|#...# #...# #...# ##### #...# #...# #...#|.###. ..#.. ..#.. ..#.. ..#.. ..#.. .###.|" + + "..### ...#. ...#. ...#. ...#. #..#. .##..|#...# #..#. #.#.. ##... #.#.. #..#. #...#|#.... #.... #.... #.... #.... #.... #####|" + + "#...# ##.## #.#.# #.#.# #...# #...# #...#|#...# #...# ##..# #.#.# #..## #...# #...#|.###. #...# #...# #...# #...# #...# .###.|" + + "####. #...# #...# ####. #.... #.... #....|.###. #...# #...# #...# #.#.# #..#. .##.#|####. #...# #...# ####. #.#.. #..#. #...#|" + + ".#### #.... #.... .###. ....# ....# ####.|##### ..#.. ..#.. ..#.. ..#.. ..#.. ..#..|#...# #...# #...# #...# #...# #...# .###.|" + + "#...# #...# #...# #...# #...# .#.#. ..#..|#...# #...# #...# #.#.# #.#.# #.#.# .#.#.|#...# #...# .#.#. ..#.. .#.#. #...# #...#|" + + "#...# #...# .#.#. ..#.. ..#.. ..#.. ..#..|##### ....# ...#. ..#.. .#... #.... #####" +) + .split("|") + .map((g) => g.split(" ").map((row) => [...row].map((c) => c === "#"))); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h), f = h - i, 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 % 6]; +} + +function rng(seed) { + let a = seed >>> 0; + return () => { + a = (a + 0x6d2b79f5) >>> 0; + let t = a; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +const code = (ch) => ch.charCodeAt(0) - 65; +const inGrid = (c, r) => c >= 0 && c < COLS && r >= 0 && r < ROWS; + +function puzzle(list, gen) { + const rand = rng(list * 7919 + gen * 104729 + 17); + const grid = new Int8Array(CELLS).fill(-1); + const words = LISTS[list].slice().sort((a, b) => b.length - a.length); + const turn = floor(rand() * 8), laid = []; + for (const [n, w] of words.entries()) { + for (let tries = 0; tries < 400; tries++) { + // Each word tries its own direction first, so all eight turn up. + const d = DIRS[tries < 200 ? (turn + n * 3) % 8 : floor(rand() * 8)], c0 = floor(rand() * COLS), r0 = floor(rand() * ROWS); + const c1 = c0 + d[0] * (w.length - 1), r1 = r0 + d[1] * (w.length - 1); + if (!inGrid(c1, r1)) continue; + let ok = true; + for (let i = 0; i < w.length && ok; i++) { + const g = grid[(r0 + d[1] * i) * COLS + c0 + d[0] * i]; + if (g >= 0 && g !== code(w[i])) ok = false; + } + if (!ok) continue; + for (let i = 0; i < w.length; i++) grid[(r0 + d[1] * i) * COLS + c0 + d[0] * i] = code(w[i]); + laid.push(w); + break; + } + } + for (let i = 0; i < CELLS; i++) if (grid[i] < 0) grid[i] = floor(rand() * 26); + return { grid, words: laid.map((w) => w.split("").map(code)) }; +} + +export function setup(params) { + params.s = { + list: -1, gen: 0, grid: null, words: [], left: [], found: [], + phase: 0, t: 0, acc: 0, wait: 0, + cell: 0, dir: -1, k: 0, live: [], miss: -1, next: false, + seen: new Uint8Array(CELLS), + capIn: new Int8Array(N), capOut: new Int8Array(N), + hue: 0, fill: 0.06, + rgb: new Uint8Array(N * 3), + melt: null, + }; + const S = params.s; + const rand = rng(99); + S.melt = new Float32Array(N); + for (let i = 0; i < N; i++) S.melt[i] = rand(); +} + +function begin(S, list, gen) { + const p = puzzle(list, gen); + S.list = list; + S.gen = gen; + S.grid = p.grid; + S.words = p.words; + S.left = p.words.map((_, i) => i); + S.found = []; + S.phase = 0; + S.t = 0; + S.acc = 0; + S.wait = 0; + S.cell = 0; + S.dir = -1; + S.k = 0; + S.live = []; + S.miss = -1; + S.next = false; + S.seen.fill(0); + S.capIn.fill(0); + S.capOut.fill(0); +} + +const centre = (c, r) => [X0 + c * CW + 2, Y0 + r * CH + 3]; + +// Mark the capsule round a found word in the two LED maps. +function capsule(S, f) { + const [ax, ay] = centre(f.c, f.r), [bx, by] = centre(f.c + f.d[0] * (f.n - 1), f.r + f.d[1] * (f.n - 1)); + const ux = bx - ax, uy = (by - ay) * SY, uu = ux * ux + uy * uy || 1; + const x0 = max(0, min(ax, bx) - 5), x1 = min(W - 1, max(ax, bx) + 5), y0 = max(0, min(ay, by) - 6), y1 = min(H - 1, max(ay, by) + 6); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const px = x - ax, py = (y - ay) * SY; + const t = cl((px * ux + py * uy) / uu, 0, 1); + const ex = px - t * ux, ey = py - t * uy, d = sqrt(ex * ex + ey * ey); + const p = y * W + x; + if (abs(d - R) < 0.5) S.capOut[p] = f.i + 1; + else if (d < R) S.capIn[p] = f.i + 1; + } +} + +// The cells a probe from the cursor has read so far. +function cellAt(S, i) { + const c = (S.cell % COLS) + DIRS[S.dir][0] * i, r = floor(S.cell / COLS) + DIRS[S.dir][1] * i; + return inGrid(c, r) ? r * COLS + c : -1; +} + +function nextDir(S) { + S.k = 0; + S.miss = -1; + for (S.dir++; S.dir < 8; S.dir++) { + S.live = S.left.filter((w) => S.words[w][0] === S.grid[S.cell]); + if (S.live.length && cellAt(S, 1) >= 0) return; + } + S.seen[S.cell] = 1; + S.cell++; + S.dir = -1; +} + +// One comparison of the plain search. +function tick(S) { + if (S.next) { + S.next = false; + nextDir(S); + if (S.dir < 0) return; + } + if (S.cell >= CELLS || S.left.length === 0) { + S.phase = 2; + S.t = 0; + return; + } + if (S.dir < 0) { + // On a new cell: does any word left start with its letter? + if (S.left.some((w) => S.words[w][0] === S.grid[S.cell])) nextDir(S); + else { + S.seen[S.cell] = 1; + S.cell++; + } + return; + } + const at = cellAt(S, S.k + 1); + const live = at < 0 ? [] : S.live.filter((w) => S.words[w].length > S.k + 1 && S.words[w][S.k + 1] === S.grid[at]); + if (!live.length) { + S.miss = at; + S.next = true; + return; + } + S.k++; + S.live = live; + const done = live.find((w) => S.words[w].length === S.k + 1); + if (done != null) { + const f = { i: S.found.length, w: done, c: S.cell % COLS, r: floor(S.cell / COLS), d: DIRS[S.dir], n: S.k + 1 }; + S.found.push(f); + S.left = S.left.filter((w) => w !== done); + capsule(S, f); + S.wait = FIND; + S.live = live.filter((w) => w !== done); + if (!S.live.length) S.next = true; + } +} + +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 list = round(cl(k[2] || 0, 0, LISTS.length - 1)); + S.fill = cl(k[3] == null ? 0.06 : k[3], 0, 1); + if (list !== S.list) begin(S, list, 0); + const step = (d * speed) / 2; + S.t += step; + if (S.wait > 0) S.wait = max(0, S.wait - step); + if (S.phase === 0) { + if (S.t >= TYPE) { + S.phase = 1; + S.t = 0; + } + } else if (S.phase === 1) { + if (S.wait > 0) return; + S.acc += step; + while (S.acc >= STEP && S.phase === 1 && S.wait <= 0) { + S.acc -= STEP; + tick(S); + } + } else if (S.phase === 2) { + if (S.t >= HOLD) { + S.phase = 3; + S.t = 0; + } + } else if (S.t >= MELT) begin(S, S.list, S.gen + 1); +} + +function put(rgb, p, c, v) { + rgb[p * 3] = round(c[0] * v * 255); + rgb[p * 3 + 1] = round(c[1] * v * 255); + rgb[p * 3 + 2] = round(c[2] * v * 255); +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.grid) return; + const rgb = S.rgb, h = S.hue, f = S.fill; + rgb.fill(0); + const wordCol = S.found.map((w) => hsv(h + 0.5 + ((w.i * 0.381966) % 1) * 0.62, 0.7, 1)); + const fresh = S.wait > 0 ? S.found.length - 1 : -1; + // Capsules: a fill under the letters as Fill rises, and the outline. + for (let p = 0; p < N; p++) { + const o = S.capOut[p], i = S.capIn[p]; + if (o) put(rgb, p, wordCol[o - 1], o - 1 === fresh ? 1 : 0.85); + else if (i && f > 0.02) put(rgb, p, wordCol[i - 1], 0.8 * f); + } + const inWord = new Int8Array(CELLS).fill(-1); + for (const w of S.found) for (let j = 0; j < w.n; j++) inWord[(w.r + w.d[1] * j) * COLS + w.c + w.d[0] * j] = w.i; + const probe = new Int8Array(CELLS); + if (S.phase === 1 && S.dir >= 0) { + for (let j = 1; j <= S.k; j++) probe[cellAt(S, j)] = 1; + if (S.miss >= 0) probe[S.miss] = 2; + } + const base = hsv(h + 0.58, 0.28, 1), dim = hsv(h + 0.6, 0.4, 1); + const hit = hsv(h + 0.14, 0.45, 1), miss = hsv(h + 0.93, 0.6, 1), white = [1, 1, 1]; + const knock = cl((f - 0.45) / 0.45, 0, 1), rest = 1 - 0.5 * f; + const typed = S.phase === 0 ? (S.t / TYPE) * (CELLS + 20) : 1e9; + const melt = S.phase === 3 ? S.t / MELT : -1; + for (let ci = 0; ci < CELLS; ci++) { + if (ci > typed) continue; + const c = ci % COLS, r = floor(ci / COLS), g = FONT[S.grid[ci]]; + let col = base, v = 0.55 * rest; + if (inWord[ci] >= 0) { + col = wordCol[inWord[ci]]; + v = inWord[ci] === fresh ? 1 : 1 - 0.85 * knock; + if (inWord[ci] !== fresh && knock < 0.5) col = col.map((x) => x + (1 - x) * 0.35); + } else if (S.phase === 1 && ci === S.cell) { + col = white; + v = 1; + } else if (probe[ci] === 1) { + col = hit; + v = 1; + } else if (probe[ci] === 2) { + col = miss; + v = 0.9; + } else if (S.seen[ci]) { + col = dim; + v = 0.32 * rest; + } + const x0 = X0 + c * CW, y0 = Y0 + r * CH; + for (let y = 0; y < 7; y++) + for (let x = 0; x < 5; x++) if (g[y][x]) put(rgb, (y0 + y) * W + x0 + x, col, v); + } + // The cursor's box in the gaps round its cell. + if (S.phase === 1 && S.cell < CELLS) { + const x0 = X0 + (S.cell % COLS) * CW - 1, y0 = Y0 + floor(S.cell / COLS) * CH - 1; + for (let x = x0; x <= x0 + 6; x++) + for (const y of [y0, y0 + 8]) if (x >= 0 && x < W && y >= 0 && y < H) put(rgb, y * W + x, white, 0.6); + for (let y = y0; y <= y0 + 8; y++) + for (const x of [x0, x0 + 6]) if (x >= 0 && x < W && y >= 0 && y < H) put(rgb, y * W + x, white, 0.6); + } + for (let p = 0; p < N; p++) { + if (melt >= 0 && S.melt[p] < melt) display.setPixel(p % W, floor(p / W), 0, 0, 0); + else display.setPixel(p % W, floor(p / W), rgb[p * 3], rgb[p * 3 + 1], rgb[p * 3 + 2]); + } +} From 284eb902097d7719f2fbd5f6eb8a79c36a73f1ce Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 23:01:51 +0000 Subject: [PATCH 03/12] =?UTF-8?q?=ED=8C=A8=ED=84=B4=2082=ED=8C=90:=20?= =?UTF-8?q?=EA=B0=88=EB=9E=98=20crofton=20=EC=A4=91=EA=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../patterns/alphabet-lineage.js | 378 +++++++++++++++++ .../pattern-study/patterns/crofton-cell.js | 381 ++++++++++++++++++ 2 files changed, 759 insertions(+) create mode 100644 incoming/pattern-study/patterns/alphabet-lineage.js create mode 100644 incoming/pattern-study/patterns/crofton-cell.js diff --git a/incoming/pattern-study/patterns/alphabet-lineage.js b/incoming/pattern-study/patterns/alphabet-lineage.js new file mode 100644 index 00000000..93dfceff --- /dev/null +++ b/incoming/pattern-study/patterns/alphabet-lineage.js @@ -0,0 +1,378 @@ +// ===== Patternflow pattern ===== +// Title: Alphabet Lineage +// Family: type +// Idea: A four letter Latin word stacked down the tall panel starts as Proto-Sinaitic pictures, an ox head, a house, water, a human head, and every letter's strokes bend through Phoenician, written right to left, turn over like a card to face the other way as Greek, and settle as Roman capitals with thick and thin strokes and serifs, the letters one after another so the column can hold four eras at once. +// Author: patternflow-lab +// Date: 2026-10-31 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the alphabet's descent from Proto-Sinaitic (Wadi el-Hol and Serabit el-Khadim, c. 1850 BCE; read by Alan Gardiner, The Egyptian origin of the Semitic alphabet, 1916) through Phoenician (c. 1050 BCE) and archaic Greek (c. 750 BCE) to Roman inscriptional capitals (1st century); letter shapes drawn for this pattern, pattern round 82 +// Round: 82 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Word=0..5, Lag=0..1 +// +// Knob 1: Hue. Turns the four era colours round the wheel together. At 0 the pictographs are gold, Phoenician purple, Greek azure and the Roman capitals warm white. +// Knob 2: Speed. At 2 a letter holds each era 1.2 to 2.6 s and takes 1.8 s to bend into the next, and a whole history with its 1.4 s rewind takes 13.4 s; every time goes as 2 / Speed. +// Knob 3: Word. 0 ROMA, 1 AMOR, 2 ARMA, 3 DONA, 4 BONA, 5 MORA, Latin words that between them use all seven letters drawn here: A, B, D, M, N, O and R. +// Knob 4: Lag. How far each letter runs behind the one above, in eras: 0.06 at the opening, so the word changes almost as one; at 1 the column shows four eras at once, the top letter a Roman capital while the bottom one is still a picture. + +// Each letter is a few strokes, and each stroke is a polyline in every era, +// on a 10 by 10 grid with y down. A stroke is resampled to the same number +// of points by arc length in every era, so one era's stroke bends into the +// next point by point. A stroke that is not there in an era has alpha 0 and +// borrows its shape from the next era that has it. +// +// Between two eras a letter may also turn. The ox head lies down on its +// side as Phoenician aleph and stands up again, upside down, as alpha; the +// Greek delta lies on its side to round into D. When Greek settled on +// writing left to right, letters that face a way turned to face the other: +// Phoenician beth, mem, nun and resh look left, Greek beta, mu, nu and rho +// look right. So between Phoenician and Greek every letter except the ox +// head turns over about its upright axis, like a card. The morph bends the +// stroke toward the next era's shape seen from before the turn, and turns +// it on the way; at the end of the morph the turn is complete and the +// stroke is the next era's. +// +// The pen is round and a little thinner each era back. The Roman capitals +// get the shading of a broad edged brush held at 30 degrees, thick where a +// stroke runs across the edge and thin along it (Edward Catich, The Origin +// of the Serif, 1968), and short serifs fade in at their feet. +// +// Phoenician and archaic Greek shapes and stroke directions were checked +// against the Unicode reference glyphs in Noto Sans Phoenician and Noto +// Sans Old Italic; the coordinates are drawn here, not copied. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, round, sqrt, sin, cos, abs, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const K = 36; + +// Era colours at Hue 0 (h, s), widths of the round pen in grid units. +const ERA = [[0.12, 0.62], [0.82, 0.55], [0.56, 0.62], [0.09, 0.1]]; +const PEN = [0.3, 0.34, 0.37, 0.4]; +// Seconds at Speed 2: hold in each era, the bend between, the rewind. +const HOLD = [1.6, 1.2, 1.2, 2.6], BEND = 1.8, BACK = 1.4; +const CYCLE = HOLD[0] + HOLD[1] + HOLD[2] + HOLD[3] + 3 * BEND + BACK; + +const WORDS = ["ROMA", "AMOR", "ARMA", "DONA", "BONA", "MORA"]; + +// Per letter: the turn between eras 0-1, 1-2, 2-3 as [degrees clockwise, +// mirrored], and the strokes as [alpha per era, shape per era]. A shape is +// "x,y x,y ..."; a closed stroke ends where it starts; "" borrows. +const LETTERS = { + // ʾalp, the ox head, muzzle down -> aleph on its side -> alpha -> A. + A: { + turn: [[90, 0], [90, 0], [0, 0]], + strokes: [ + [[1, 1, 1, 1], [ + "3,3.2 3.4,6 4.1,8.2 5,9.1 5.9,8.2 6.6,6 7,3.2", + "7.9,1.3 4.5,3.7 1,6.3 5.4,6.2 9.7,6.1", + "8.4,9.5 6.7,5 5,0.6 3.3,5 1.6,9.5", + "9,9.4 7,4.9 5,0.4 3,4.9 1,9.4", + ]], + [[1, 1, 1, 1], [ + "0.6,0.8 1.4,2.4 2.6,3.3 5,3.7 7.4,3.3 8.6,2.4 9.4,0.8", + "3.6,0.6 4.8,5 6.1,9.4", + "7.1,5.6 2.9,6.4", + "7.2,6.5 2.8,6.5", + ]], + [[1, 0, 0, 0], ["4.1,5 4.3,5.2", "", "", ""]], + [[1, 0, 0, 0], ["5.9,5 5.7,5.2", "", "", ""]], + [[0, 0, 0, 1], ["", "", "", "-0.2,9.4 2.4,9.4"]], + [[0, 0, 0, 1], ["", "", "", "7.6,9.4 10.2,9.4"]], + ], + }, + // bayt, a house with its door -> beth -> beta -> B. + B: { + turn: [[0, 0], [0, 1], [0, 0]], + strokes: [ + [[1, 1, 1, 1], [ + "8.6,5 8.6,1.4 1.4,1.4 1.4,8.6 4,8.6", + "6.9,5 1.2,4.6 5.8,0.6 6.6,2.6 6.9,5 6.4,7.6 4.4,9 1.8,9.4", + "2.4,4.8 6.4,4.2 7.4,2.4 6.2,0.8 2.4,0.6 2.4,9.4 7,9.2 8.2,7.2 7,5.2 2.4,4.8", + "2.2,4.7 5.6,4.6 6.8,3.8 7,2.5 6.6,1.3 5.4,0.6 2.2,0.6 2.2,9.4 6,9.4 7.6,8.6 8,7 7.4,5.5 5.8,4.8 2.2,4.7", + ]], + [[1, 0, 0, 0], ["6,8.6 8.6,8.6", "", "", ""]], + [[0, 0, 0, 1], ["", "", "", "0.8,0.6 2.4,0.6"]], + [[0, 0, 0, 1], ["", "", "", "0.8,9.4 2.4,9.4"]], + ], + }, + // dalt, a door with its leaf -> daleth -> delta -> D, the delta laid on + // its side as western Greek and Latin wrote it. + D: { + turn: [[0, 0], [0, 1], [90, 0]], + strokes: [ + [[1, 1, 1, 1], [ + "2.4,0.8 7.6,0.8 7.6,9.2 2.4,9.2 2.4,0.8", + "4.6,0.8 1.6,6 6.4,6 4.6,0.8", + "5,0.8 0.8,9.2 9.2,9.2 5,0.8", + "2.2,0.6 5,0.6 7.4,1.8 8.6,3.8 8.8,5 8.6,6.2 7.4,8.2 5,9.4 2.2,9.4 2.2,0.6", + ]], + [[1, 1, 0, 0], ["5.6,2.2 6.4,5 5.6,7.8", "6.4,6 8.2,9.2", "9.2,9.2 9.2,9.2", ""]], + [[0, 0, 0, 1], ["", "", "", "0.8,0.6 2.4,0.6"]], + [[0, 0, 0, 1], ["", "", "", "0.8,9.4 2.4,9.4"]], + ], + }, + // mem, water -> mem with its tail -> mu, long left leg -> M. + M: { + turn: [[0, 0], [0, 1], [0, 0]], + strokes: [ + [[1, 1, 1, 1], [ + "0.4,4 1.6,6 2.8,4 4,6 5.2,4 6.4,6 7.6,4 8.8,6 9.6,4.6", + "1.1,0.6 3,3 4.7,1.3 6.4,3 9,0.8 8.4,4.8 6.6,8 4,9.6", + "0.9,9.4 3,0.6 5.4,5.9 7.7,0.6 9.6,5.2", + "1,9.4 2.1,0.6 5,9.4 7.9,0.6 9,9.4", + ]], + [[0, 0, 0, 1], ["", "", "", "-0.2,9.4 2,9.4"]], + [[0, 0, 0, 1], ["", "", "", "8,9.4 10.2,9.4"]], + ], + }, + // nun, a snake -> nun -> nu, long left leg -> N. + N: { + turn: [[0, 0], [0, 1], [0, 0]], + strokes: [ + [[1, 1, 1, 1], [ + "1.4,1.4 2.4,0.6 3.4,1.2 3.6,2.8 5.4,3.4 7.4,2.6 8,4.2 6.6,6 4.6,7 4.4,8.6 6,9.6", + "1.8,0.6 3.8,3.6 7.4,1.2 6.4,5.6 3.2,9.6", + "1.2,9.4 3.2,0.6 6.4,6.4 8.8,0.6", + "1.6,9.4 1.6,0.6 8.4,9.4 8.4,0.6", + ]], + [[0, 0, 0, 1], ["", "", "", "0.6,9.4 2.6,9.4"]], + [[0, 0, 0, 1], ["", "", "", "7.4,0.6 9.4,0.6"]], + ], + }, + // ʿayin, an eye -> ayin -> omicron -> O. + O: { + turn: [[0, 0], [0, 1], [0, 0]], + strokes: [ + [[1, 1, 1, 1], [ + "0.4,5 1.8,3.4 3.4,2.5 5,2.2 6.6,2.5 8.2,3.4 9.6,5 8.2,6.6 6.6,7.5 5,7.8 3.4,7.5 1.8,6.6 0.4,5", + "circle 5,5 3.4", + "circle 5,5 3.9", + "circle 5,5 4.5", + ]], + [[1, 0, 0, 0], ["circle 5,5 1.3", "circle 5,5 0.2", "", ""]], + ], + }, + // rōš, a head in profile looking left -> resh -> rho -> R, whose leg + // Latin kept to tell it from its own P. + R: { + turn: [[0, 0], [0, 1], [0, 0]], + strokes: [ + [[1, 1, 1, 1], [ + "5.8,9.6 5.8,6.8 7.2,5 7.6,3 6.6,1.2 4.4,0.6 2.4,1.6 1.2,3.4 2.2,3.9 2,4.8 2.8,5.8 4.2,6.2 5.8,6.8", + "7.8,9.6 6.4,4.4 5.2,0.6 1.4,3.6 6.4,4.4", + "2.4,9.4 2.4,0.6 6,0.6 7.6,1.6 7.8,3 6.8,4.6 2.4,5", + "2.2,9.4 2.2,0.6 5.4,0.6 7,1.2 7.6,2.6 7,4 5.4,4.8 2.2,4.8", + ]], + [[0, 0, 1, 1], ["5.8,6.8 5.8,6.8", "6.4,4.4 6.4,4.4", "4.2,4.9 4.2,4.9", "4.4,4.8 6.4,7 8.6,9.4"]], + [[1, 0, 0, 0], ["3.4,2.6 3.6,2.8", "", "", ""]], + [[0, 0, 0, 1], ["", "", "", "0.8,0.6 2.4,0.6"]], + [[0, 0, 0, 1], ["", "", "", "0.8,9.4 3.6,9.4"]], + [[0, 0, 0, 1], ["", "", "", "7.8,9.4 10,9.4"]], + ], + }, +}; + +function parse(text) { + if (text.startsWith("circle")) { + const [, c, r] = text.split(" "), [x, y] = c.split(",").map(Number), rr = Number(r), pts = []; + // Starts at the left, as the eye does, and runs clockwise. + for (let i = 0; i <= 24; i++) pts.push([x - rr * cos((i / 24) * 2 * PI), y - rr * sin((i / 24) * 2 * PI)]); + return pts; + } + return text.split(" ").map((t) => t.split(",").map(Number)); +} + +// K points evenly along the polyline. +function resample(pts) { + const L = [0]; + for (let i = 1; i < pts.length; i++) L.push(L[i - 1] + Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1])); + const total = L[L.length - 1], out = []; + let j = 1; + for (let k = 0; k < K; k++) { + const s = (total * k) / (K - 1); + while (j < pts.length - 1 && L[j] < s) j++; + const seg = L[j] - L[j - 1] || 1, t = total ? cl((s - L[j - 1]) / seg, 0, 1) : 0; + out.push([pts[j - 1][0] + (pts[j][0] - pts[j - 1][0]) * t, pts[j - 1][1] + (pts[j][1] - pts[j - 1][1]) * t]); + } + return out; +} + +// The turn between two eras, about the middle of the grid, and back. +function turn(pts, deg, mir, back) { + const a = ((back ? -deg : deg) * PI) / 180, c = cos(a), s = sin(a); + return pts.map(([x, y]) => { + let dx = x - 5, dy = y - 5; + if (mir && !back) dx = -dx; + let rx = dx * c - dy * s, ry = dx * s + dy * c; + if (mir && back) rx = -rx; + return [5 + rx, 5 + ry]; + }); +} + +const closed = (p) => Math.hypot(p[0][0] - p[K - 1][0], p[0][1] - p[K - 1][1]) < 1e-6; +const gap = (a, b) => a.reduce((s, p, i) => s + (p[0] - b[i][0]) ** 2 + (p[1] - b[i][1]) ** 2, 0); + +// The target turned to face the source the nearest way: reversed if open, +// and for a closed stroke also started at the best point. +function align(src, tgt) { + let best = tgt, bg = gap(src, tgt); + const rev = tgt.slice().reverse(); + if (gap(src, rev) < bg) { + best = rev; + bg = gap(src, rev); + } + if (closed(tgt)) { + const ring = tgt.slice(0, K - 1); + for (const dir of [ring, ring.slice().reverse()]) + for (let sft = 0; sft < K - 1; sft++) { + const c = []; + for (let i = 0; i < K - 1; i++) c.push(dir[(i + sft) % (K - 1)]); + c.push(c[0]); + const g = gap(src, c); + if (g < bg) { + bg = g; + best = c; + } + } + } + return best; +} + +// Every letter's strokes for every era and every bend. +function build() { + const out = {}; + for (const ch in LETTERS) { + const L = LETTERS[ch], strokes = []; + for (const [alpha, shapes] of L.strokes) { + const g = shapes.map((t) => (t ? resample(parse(t)) : null)); + for (let e = 1; e < 4; e++) if (!g[e] && g[e - 1]) g[e] = turn(g[e - 1], L.turn[e - 1][0], L.turn[e - 1][1], false); + for (let e = 2; e >= 0; e--) if (!g[e] && g[e + 1]) g[e] = turn(g[e + 1], L.turn[e][0], L.turn[e][1], true); + // For each bend, the next era's shape seen from before the turn. + const to = [0, 1, 2].map((e) => align(g[e], turn(g[e + 1], L.turn[e][0], L.turn[e][1], true))); + strokes.push({ alpha, g, to }); + } + out[ch] = { turn: L.turn, strokes }; + } + 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 i === 0 ? [v, t, p] : i === 1 ? [q, v, p] : i === 2 ? [p, v, t] : i === 3 ? [p, q, v] : i === 4 ? [t, p, v] : [v, p, q]; +} + +const ease = (u) => u * u * (3 - 2 * u); + +// The era position 0..3 of a letter whose own clock reads t seconds. +function eraAt(t) { + if (t <= 0) return 0; + t %= CYCLE; + let x = 0; + for (let e = 0; e < 3; e++) { + if (t < HOLD[e]) return e; + t -= HOLD[e]; + if (t < BEND) return e + t / BEND; + t -= BEND; + } + if (t < HOLD[3]) return 3; + t -= HOLD[3]; + x = 3 * (1 - ease(t / BACK)); + return x; +} + +export function setup(params) { + params.s = { glyphs: build(), clock: 0, hue: 0, word: 0, lag: 0.06, ink: new Float32Array(N), owner: new Int8Array(N) }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + const speed = cl(k[1] == null ? 2 : k[1], 0.2, 3); + S.clock += cl(dt || 0, 0, 0.1) * (speed / 2); + S.hue = k[0] || 0; + S.word = round(cl(k[2] || 0, 0, WORDS.length - 1)); + S.lag = cl(k[3] == null ? 0.06 : k[3], 0, 1); +} + +// Distance from (px, py) to the segment a-b. +function seg(px, py, ax, ay, bx, by) { + const dx = bx - ax, dy = by - ay, L2 = dx * dx + dy * dy; + const t = L2 > 1e-9 ? cl(((px - ax) * dx + (py - ay) * dy) / L2, 0, 1) : 0; + const ex = px - ax - t * dx, ey = py - ay - t * dy; + return sqrt(ex * ex + ey * ey); +} + +// Ink a segment with a round pen of radius w and a soft halo. +function stamp(S, band, ax, ay, bx, by, w, a) { + const x0 = max(0, floor(min(ax, bx) - w - 2)), x1 = min(W - 1, floor(max(ax, bx) + w + 2)); + const y0 = max(0, floor(min(ay, by) - w - 2)), y1 = min(H - 1, floor(max(ay, by) + w + 2)); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const d = seg(x + 0.5, y + 0.5, ax, ay, bx, by); + const v = a * max(cl(w + 0.5 - d, 0, 1), 0.3 * exp(-((d - w) * (d - w)) / 2.2)); + const i = y * W + x; + if (v > S.ink[i]) { + S.ink[i] = v; + S.owner[i] = band; + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + S.ink.fill(0); + const word = WORDS[S.word], step = HOLD[1] + BEND; + // Grid unit in LEDs and the 32 row band of each letter. + const U = 2.7, colours = []; + for (let n = 0; n < 4; n++) { + const G = S.glyphs[word[n]], yc = n * 32 + 16, xc = 33; + const x = eraAt(S.clock - n * S.lag * step); + const e = min(2, floor(x)), u = ease(x - e); + const [deg, mir] = G.turn[e]; + const a = ((deg * u) * PI) / 180, ca = cos(a), sa = sin(a); + const flip = mir ? cos(PI * u) : 1; + const shade = mir ? 0.5 + 0.5 * abs(flip) : 1; + // Roman shading comes in over the last bend. + const roman = e === 2 ? u : 0; + const pen = PEN[e] + (PEN[e + 1] - PEN[e]) * u; + const c0 = hsv(S.hue + ERA[e][0], ERA[e][1], 1), c1 = hsv(S.hue + ERA[e + 1][0], ERA[e + 1][1], 1); + colours.push(c0.map((v, i) => (v + (c1[i] - v) * u) * 255 * shade)); + for (const st of G.strokes) { + const al = st.alpha[e] + (st.alpha[e + 1] - st.alpha[e]) * u; + if (al < 0.01) continue; + const src = st.g[e], tgt = st.to[e]; + let px = 0, py = 0; + for (let i = 0; i < K; i++) { + let gx = src[i][0] + (tgt[i][0] - src[i][0]) * u - 5, gy = src[i][1] + (tgt[i][1] - src[i][1]) * u - 5; + gx *= flip; + const rx = gx * ca - gy * sa, ry = gx * sa + gy * ca; + const qx = xc + rx * U, qy = yc + ry * U; + if (i > 0) { + // A broad edge at 30 degrees: thick across it, thin along it. + const dx = qx - px, dy = qy - py, L = Math.hypot(dx, dy) || 1; + const nib = abs((dx / L) * 0.5 + (dy / L) * 0.866); + const w = (pen + roman * (0.22 + 0.5 * nib - pen)) * U; + stamp(S, n, px, py, qx, qy, w, al); + } + px = qx; + py = qy; + } + } + // The era rail at the left: four ticks and where the letter is. + for (let t = 0; t < 4; t++) stamp(S, n, 6, yc - 10.5 + 7 * t, 6, yc - 10.5 + 7 * t, 0.2, 0.35); + stamp(S, n, 6, yc - 10.5 + 7 * x, 6, yc - 10.5 + 7 * x, 0.9, 1); + // The writing direction at the right: left for Phoenician, then turned. + const dirA = e === 0 ? u : 1, f = mir ? flip : e === 0 ? 1 : -1; + const ax = 57 + 2.5 * f, bx = 57 - 2.5 * f; + stamp(S, n, ax, yc - 3.5, bx, yc, 0.35, 0.8 * dirA); + stamp(S, n, bx, yc, ax, yc + 3.5, 0.35, 0.8 * dirA); + } + for (let i = 0; i < N; i++) { + const v = S.ink[i], c = colours[S.owner[i]]; + display.setPixel(i % W, (i / W) | 0, round(c[0] * v), round(c[1] * v), round(c[2] * v)); + } +} diff --git a/incoming/pattern-study/patterns/crofton-cell.js b/incoming/pattern-study/patterns/crofton-cell.js new file mode 100644 index 00000000..9e7163b9 --- /dev/null +++ b/incoming/pattern-study/patterns/crofton-cell.js @@ -0,0 +1,381 @@ +// ===== Patternflow pattern ===== +// Title: Crofton Cell +// Family: cells +// Idea: Isotropic Poisson lines drift, are born and die across the tall panel and cut it into convex cells tinted by their number of sides, while the one cell that holds the fixed point in the middle, the Crofton cell, is lit gold with its incircle drawn in it, so when the Big knob keeps every line at least r away from that point the cell swells and its gold corners thin to slivers round a nearly circular disc, as Kendall conjectured. +// Author: patternflow-lab +// Date: 2026-10-31 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Poisson line tessellation and its zero cell (Roger E. Miles, Random polygons determined by random lines in a plane, 1964); D. G. Kendall's conjecture that large zero cells are nearly round (1940s), proved for the plane by Igor Kovalenko (1997) and in general by Daniel Hug, Matthias Reitzner and Rolf Schneider (2004); pattern round 82 +// Round: 82 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Lines=16..56, Big=0..1 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 triangles are teal, quadrilaterals azure, pentagons violet, hexagons pink and cells of seven sides or more lime, and the Crofton cell is a pale disc in gold corners. +// Knob 2: Speed. At 2 a line slides along its normal at up to 3 LEDs a second, turns at up to 0.05 radians a second and lives 10 s on average, so about one line a second is born somewhere and one dies. Everything goes as Speed / 2. +// Knob 3: Lines. The mean number of lines that cross the panel, 10 to 40; the cells come out about 120 at 20 lines and 420 at 40. +// Knob 4: Big. No line may come nearer the fixed point than r = 28 Big^0.6 LEDs, which is the Poisson process conditioned on the zero cell holding that disc: 5 LEDs at the opening 0.06, 12 at 0.25 and 28 at 1, where with 40 lines the cell is a 9 to 13 sided polygon whose incircle fills about 0.9 of its outer circle. + +// A line is (theta, p): the points x with x . (cos theta, sin theta) = p, +// p >= 0, measured from the fixed point O in the middle of the panel. The +// isotropic Poisson line process is a Poisson point process in (theta, p) +// with uniform density, and by Crofton's formula the lines that hit a convex +// set K number perimeter(K) times that density on average. So the panel +// keeps every line with p < R, R past the panel's corners, uniformly in +// theta and p: the panel's perimeter of 384 LEDs against the circle's 2 pi R +// says how many of those cross it. +// +// Each line slides in p and turns in theta at its own constant rate, which +// moves the points of (theta, p) without squeezing them, so the law stays +// the Poisson one. A line whose p passes 0 is the same line seen from the +// other side, (theta + pi, -p). At p = R it turns back. Lines are also born +// uniformly at a constant rate and each dies at a constant rate, a spatial +// birth and death process whose resting law is the same Poisson process. +// +// The zero cell is the cell that contains O. Conditioning the process on +// the zero cell holding the disc of radius r round O is the same as +// keeping only the lines with p >= r, since a Poisson process on a set and +// on its complement are independent. So Big only hides lines: a line that +// slides in to p = r dies at the point where it touches the disc, and one +// that slides out is born there. Miles found the zero cell has pi^2 / 2 = +// 4.93 sides on average where a typical cell has 4. Kendall's question +// was what a large zero cell looks like, and the answer is a disc: with r +// large against the mean spacing of the lines, many lines lie just past r +// and the cell is a many sided polygon hugging the circle. +// +// Two LEDs are in the same cell when no line separates them, so each LED's +// cell is the set of lines it lies beyond, hashed. A cell's polygon is the +// square of side 2R clipped by the half plane of every line on that cell's +// side; its edges give the number of sides. The zero cell's largest +// inscribed disc comes from the three edges that pin it. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, sqrt, sin, cos, abs, atan2, exp, pow, log, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +const OX = 32, OY = 64; +// Lines are kept while they pass within R of O; the corners are 71.6 away. +const R = 76; +// Lines crossing the panel per line kept: perimeter over 2 pi R. +const CROSS = 384 / (TAU * R); +// Mean life, the speed limits, all at Speed 2. +const LIFE = 10, VP = 3, VT = 0.05; +const MAXL = 160; +const SLOTS = 2048; + +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 i === 0 ? [v, t, p] : i === 1 ? [q, v, p] : i === 2 ? [p, v, t] : i === 3 ? [p, q, v] : i === 4 ? [t, p, v] : [v, p, q]; +} + +function poisson(mu) { + const L = exp(-mu); + let k = 0, prod = Math.random(); + while (prod > L && k < MAXL) { + k++; + prod *= Math.random(); + } + return k; +} + +function newLine(S, p) { + if (S.n >= MAXL) return; + const i = S.n++; + S.th[i] = Math.random() * TAU; + S.p[i] = p == null ? Math.random() * R : p; + S.v[i] = (Math.random() * 2 - 1) * VP; + S.w[i] = (Math.random() * 2 - 1) * VT; + S.life[i] = -log(1 - Math.random()) * LIFE; + S.key[i] = (Math.random() * 4294967296) >>> 0 | 1; + S.glow[i] = 0; + S.vis[i] = 0; +} + +function kill(S, i) { + // A dying line that the panel shows leaves a fading ghost. + if (S.vis[i] && S.p[i] < 73 && S.ghosts.length < 8) S.ghosts.push([cos(S.th[i]), sin(S.th[i]), S.p[i], 1]); + const j = --S.n; + for (const a of [S.th, S.p, S.v, S.w, S.life, S.key, S.glow, S.vis]) a[i] = a[j]; +} + +export function setup(params) { + params.s = { + n: 0, th: new Float64Array(MAXL), p: new Float64Array(MAXL), v: new Float64Array(MAXL), w: new Float64Array(MAXL), + life: new Float64Array(MAXL), key: new Uint32Array(MAXL), glow: new Float32Array(MAXL), vis: new Uint8Array(MAXL), + ghosts: [], sparks: [], started: false, hue: 0, r: 4, mu: 20, + hash: new Uint32Array(N), dmin: new Float32Array(N), near: new Int16Array(N), cell: new Int16Array(N), + skey: new Uint32Array(SLOTS), sused: new Uint8Array(SLOTS), sid: new Int16Array(SLOTS), + rep: new Int32Array(SLOTS), sides: new Uint8Array(SLOTS), area: new Float32Array(SLOTS), + px: new Float64Array(96), py: new Float64Array(96), pl: new Int16Array(96), qx: new Float64Array(96), qy: new Float64Array(96), ql: new Int16Array(96), + zero: null, + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 16, 0.06]; + const d = cl(dt || 0, 0, 0.1) * cl(k[1] == null ? 2 : k[1], 0.2, 3) / 2; + S.hue = k[0] || 0; + S.mu = cl(k[2] == null ? 16 : k[2], 16, 56) / CROSS; + S.r = 20 * pow(cl(k[3] == null ? 0.06 : k[3], 0, 1), 0.6); + if (!S.started) { + S.started = true; + for (let m = poisson(S.mu); m > 0; m--) newLine(S); + for (let i = 0; i < S.n; i++) S.vis[i] = S.p[i] >= S.r ? 1 : 0; + } + // Births at the resting rate, plus a quick catch up when the knob moves. + const births = (S.mu / LIFE + max(0, S.mu - S.n) / 0.6) * d; + let extra = (max(0, S.n - S.mu) / 0.6) * d; + for (let b = births; b > 0; b--) if (Math.random() < b) { + newLine(S); + S.glow[S.n - 1] = 1; + } + for (; extra > 0; extra--) if (Math.random() < extra && S.n > 0) kill(S, floor(Math.random() * S.n)); + for (let i = S.n - 1; i >= 0; i--) { + let p = S.p[i] + S.v[i] * d; + S.th[i] += S.w[i] * d; + if (p < 0) { + p = -p; + S.th[i] += PI; + S.v[i] = -S.v[i]; + } + if (p > R) { + p = 2 * R - p; + S.v[i] = -S.v[i]; + } + S.p[i] = p; + S.glow[i] = max(0, S.glow[i] - d * 1.6); + S.life[i] -= d; + if (S.life[i] <= 0) { + kill(S, i); + continue; + } + // Big hides the lines nearer than r: they die and are born touching the disc. + const vis = p >= S.r ? 1 : 0; + if (vis !== S.vis[i]) { + if (S.sparks.length < 24) S.sparks.push([OX + S.r * cos(S.th[i]), OY + S.r * sin(S.th[i]), 1]); + if (vis) S.glow[i] = 0.8; + S.vis[i] = vis; + } + } + for (const g of S.ghosts) g[3] -= d * 2; + S.ghosts = S.ghosts.filter((g) => g[3] > 0); + for (const s of S.sparks) s[2] -= d * 2.2; + S.sparks = S.sparks.filter((s) => s[2] > 0); +} + +// Clip the square of side 2R by the lines on one side, sign per line from +// the LED at rep, or every line on O's side when rep < 0. Leaves the polygon +// in S.px, S.py with edge labels in S.pl (-1 for the square) and returns its +// vertex count. +function polygon(S, act, rep) { + let px = S.px, py = S.py, pl = S.pl, qx = S.qx, qy = S.qy, ql = S.ql; + px[0] = -R; py[0] = -R; px[1] = R; py[1] = -R; px[2] = R; py[2] = R; px[3] = -R; py[3] = R; + pl[0] = pl[1] = pl[2] = pl[3] = -1; + let n = 4; + const rx = rep < 0 ? 0 : (rep % W) + 0.5 - OX, ry = rep < 0 ? 0 : floor(rep / W) + 0.5 - OY; + for (let j = 0; j < act.n; j++) { + const a = act.a[j], b = act.b[j], p = act.p[j]; + // Keep s (a x + b y - p) >= 0. + const s = a * rx + b * ry - p > 0 ? 1 : -1; + let m = 0; + for (let i = 0; i < n; i++) { + const i2 = i + 1 === n ? 0 : i + 1; + const f1 = s * (a * px[i] + b * py[i] - p), f2 = s * (a * px[i2] + b * py[i2] - p); + if (f1 >= 0) { + qx[m] = px[i]; qy[m] = py[i]; ql[m] = pl[i]; m++; + if (f2 < 0) { + const t = f1 / (f1 - f2); + qx[m] = px[i] + t * (px[i2] - px[i]); qy[m] = py[i] + t * (py[i2] - py[i]); ql[m] = j; m++; + } + } else if (f2 >= 0) { + const t = f1 / (f1 - f2); + qx[m] = px[i] + t * (px[i2] - px[i]); qy[m] = py[i] + t * (py[i2] - py[i]); ql[m] = pl[i]; m++; + } + if (m > 90) break; + } + [px, qx] = [qx, px]; + [py, qy] = [qy, py]; + [pl, ql] = [ql, pl]; + n = m; + if (n < 3) break; + } + S.px = px; S.py = py; S.pl = pl; S.qx = qx; S.qy = qy; S.ql = ql; + return n; +} + +// The largest disc in the convex polygon S.px, S.py: centre and radius from +// the three edges it touches, tried over every three. +function incircle(S, n) { + const ex = [], ey = [], ec = []; + for (let i = 0; i < n; i++) { + const i2 = i + 1 === n ? 0 : i + 1; + const dx = S.px[i2] - S.px[i], dy = S.py[i2] - S.py[i], L = sqrt(dx * dx + dy * dy); + if (L < 1e-6) continue; + // The square runs clockwise on the panel (y down), so the inward + // normal is the edge turned a quarter to the right. + const nx = -dy / L, ny = dx / L; + ex.push(nx); ey.push(ny); ec.push(nx * S.px[i] + ny * S.py[i]); + } + const m = ex.length; + let best = [0, 0, 0]; + for (let i = 0; i < m; i++) + for (let j = i + 1; j < m; j++) + for (let k = j + 1; k < m; k++) { + // n . x - rho = c for the three: inside means n . x >= c + rho. + const a1 = ex[i], b1 = ey[i], c1 = ec[i], a2 = ex[j], b2 = ey[j], c2 = ec[j], a3 = ex[k], b3 = ey[k], c3 = ec[k]; + const det = a1 * (b2 * -1 - -1 * b3) - b1 * (a2 * -1 - -1 * a3) + -1 * (a2 * b3 - b2 * a3); + if (abs(det) < 1e-9) continue; + const dx = c1 * (b2 * -1 - -1 * b3) - b1 * (c2 * -1 - -1 * c3) + -1 * (c2 * b3 - b2 * c3); + const dy = a1 * (c2 * -1 - -1 * c3) - c1 * (a2 * -1 - -1 * a3) + -1 * (a2 * c3 - c2 * a3); + const dr = a1 * (b2 * c3 - c2 * b3) - b1 * (a2 * c3 - c2 * a3) + c1 * (a2 * b3 - b2 * a3); + const x = dx / det, y = dy / det, rho = dr / det; + if (!(rho > best[2])) continue; + let ok = true; + for (let q = 0; q < m && ok; q++) if (ex[q] * x + ey[q] * y < ec[q] + rho - 1e-7) ok = false; + if (ok) best = [x, y, rho]; + } + return best; +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.started) return; + const { hash, dmin, near, cell, skey, sused, sid, rep, sides } = S; + // The lines the panel shows. + const act = { n: 0, a: [], b: [], p: [], key: [], glow: [] }; + for (let i = 0; i < S.n; i++) + if (S.vis[i] && S.p[i] < 73) { + act.a.push(cos(S.th[i])); act.b.push(sin(S.th[i])); act.p.push(S.p[i]); act.key.push(S.key[i]); act.glow.push(S.glow[i]); + act.n++; + } + hash.fill(0); + dmin.fill(99); + near.fill(-1); + for (let j = 0; j < act.n; j++) { + const a = act.a[j], b = act.b[j], key = act.key[j]; + for (let y = 0; y < H; y++) { + let dd = a * (0.5 - OX) + b * (y + 0.5 - OY) - act.p[j]; + for (let x = 0, i = y * W; x < W; x++, i++, dd += a) { + if (dd > 0) hash[i] ^= key; + const ad = dd < 0 ? -dd : dd; + if (ad < dmin[i]) { + dmin[i] = ad; + near[i] = j; + } + } + } + } + // Cells: hash every LED's line set into a table. + sused.fill(0); + let cells = 0; + for (let i = 0; i < N; i++) { + const h = hash[i]; + let s = Math.imul(h, 2654435761) >>> 21; + while (sused[s] && skey[s] !== h) s = (s + 1) & (SLOTS - 1); + if (!sused[s]) { + sused[s] = 1; + skey[s] = h; + sid[s] = cells; + rep[cells] = i; + cells++; + } + cell[i] = sid[s]; + } + for (let c = 0; c < cells; c++) { + const n = polygon(S, act, rep[c]); + let e = 0, ar = 0; + for (let i = 0; i < n; i++) { + if (S.pl[i] >= 0) e++; + const i2 = i + 1 === n ? 0 : i + 1; + ar += S.px[i] * S.py[i2] - S.px[i2] * S.py[i]; + } + sides[c] = e; + S.area[c] = abs(ar) / 2; + } + // The zero cell, every line on O's side. + const zn = polygon(S, act, -1); + const [cx, cy, rin] = incircle(S, zn); + let rout = 0, zs = 0; + for (let i = 0; i < zn; i++) { + rout = max(rout, sqrt((S.px[i] - cx) ** 2 + (S.py[i] - cy) ** 2)); + if (S.pl[i] >= 0) zs++; + } + S.zero = { sides: zs, rin, rout }; + const icx = cx + OX, icy = cy + OY; + + const hue = S.hue; + // Hue by number of sides, a little brighter the larger the cell. + const tone = new Float32Array(cells * 3); + for (let c = 0; c < cells; c++) { + const n = sides[c], h = n <= 3 ? 0.47 : n === 4 ? 0.6 : n === 5 ? 0.74 : n === 6 ? 0.88 : 0.24; + const col = hsv(hue + h, 0.7, 0.32 + 0.26 * cl(log(1 + S.area[c]) / log(3000), 0, 1)); + tone[c * 3] = col[0] * 255; tone[c * 3 + 1] = col[1] * 255; tone[c * 3 + 2] = col[2] * 255; + } + const gold = hsv(hue + 0.12, 0.8, 1).map((v) => v * 255); + const pale = hsv(hue + 0.55, 0.2, 0.95).map((v) => v * 255); + for (let i = 0; i < N; i++) { + const x = i % W, y = (i / W) | 0, px = x + 0.5, py = y + 0.5; + const c = cell[i]; + let r, g, b; + if (hash[i] === 0) { + const q = sqrt((px - icx) ** 2 + (py - icy) ** 2); + const t = cl(q - rin + 0.5, 0, 1); + r = pale[0] + (gold[0] - pale[0]) * t; + g = pale[1] + (gold[1] - pale[1]) * t; + b = pale[2] + (gold[2] - pale[2]) * t; + } else { + r = tone[c * 3]; g = tone[c * 3 + 1]; b = tone[c * 3 + 2]; + } + // The lines themselves are dark seams; a new line flashes white. + const dm = dmin[i]; + const seam = cl((dm - 0.3) / 0.45, 0, 1); + r *= seam; g *= seam; b *= seam; + if (near[i] >= 0) { + const gl = act.glow[near[i]]; + if (gl > 0) { + const f = gl * exp(-dm * dm * 1.5) * 255; + r += f; g += f; b += f; + } + } + // Dying lines fade out where they stood. + for (const gh of S.ghosts) { + const dd = abs(gh[0] * (px - OX) + gh[1] * (py - OY) - gh[2]); + if (dd < 1.2) { + const f = gh[3] * (1 - dd / 1.2) * 150; + r += f; g += f; b += f; + } + } + // Where a line touches the forbidden disc it dies or is born. + for (const sp of S.sparks) { + const dd = (px - sp[0]) ** 2 + (py - sp[1]) ** 2; + if (dd < 9) { + const f = sp[2] * (1 - dd / 9) * 255; + r += f; g += f * 0.9; b += f * 0.7; + } + } + // The disc no line may enter, a faint ring round O. + if (S.r > 1.5) { + const fr = 1 - abs(sqrt((px - OX) ** 2 + (py - OY) ** 2) - S.r) / 0.6; + if (fr > 0) { + r *= 1 - 0.35 * fr; g *= 1 - 0.3 * fr; b *= 1 - 0.15 * fr; + } + } + // The incircle of the zero cell, solid, and its outer circle round the + // same centre, dashed, over everything. + const q = sqrt((px - icx) ** 2 + (py - icy) ** 2); + const ri = 1 - abs(q - rin) / 0.75; + if (ri > 0 && rin > 1) { + r += (255 - r) * ri; g += (255 - g) * ri; b += (255 - b) * ri; + } + const ro = 1 - abs(q - rout) / 0.7; + if (ro > 0 && floor(((atan2(py - icy, px - icx) + PI) / TAU) * 40) % 2 === 0) { + r += (255 - r) * ro * 0.55; g += (255 - g) * ro * 0.55; b += (255 - b) * ro * 0.55; + } + // O itself, a dark point. + if (abs(px - OX) < 1 && abs(py - OY) < 1) r = g = b = 0; + display.setPixel(x, y, min(255, r) | 0, min(255, g) | 0, min(255, b) | 0); + } +} From d42c0720fe7f7402bd612920f4bfdf9af4f4f7d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 23:01:51 +0000 Subject: [PATCH 04/12] =?UTF-8?q?=ED=8C=A8=ED=84=B4=2082=ED=8C=90:=20?= =?UTF-8?q?=EA=B0=88=EB=9E=98=20scalo=20=EC=A4=91=EA=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../patterns/cantor-scalogram.js | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 incoming/pattern-study/patterns/cantor-scalogram.js diff --git a/incoming/pattern-study/patterns/cantor-scalogram.js b/incoming/pattern-study/patterns/cantor-scalogram.js new file mode 100644 index 00000000..7160548d --- /dev/null +++ b/incoming/pattern-study/patterns/cantor-scalogram.js @@ -0,0 +1,249 @@ +// ===== Patternflow pattern ===== +// Title: Cantor Scalogram +// Family: data +// Idea: The wavelet transform of the Cantor staircase fills the tall panel, position down the 128 rows and log scale across the 64 columns, brightness its modulus and hue the phase of a complex Morlet wavelet, so the modulus branches into a tree of cones exactly as the middle-thirds Cantor set does while the view zooms in by three and slides one level along the scale axis back to the same picture. +// Author: patternflow-lab +// Date: 2026-10-31 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: continuous wavelet transform (Alex Grossmann and Jean Morlet, 1984); wavelet transform of fractal signals, the Cantor staircase and the tree of its modulus maxima (Alain Arneodo, Guy Grasseau and Matthias Holschneider, 1988); the middle-thirds Cantor set (Georg Cantor, 1883); pattern round 82 +// Round: 82 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Noise=1..8, Omega=3..8 +// +// Knob 1: Hue. Turns the phase wheel: at 0 a phase of 0 is cyan, at 0.5 it is red, and the hue goes once round the wheel for every turn of the phase. +// Knob 2: Speed. At 2 the view zooms in by three every 6 s while the tree slides 21 columns towards the coarse side; 60 s at 0.2, 4 s at 3. +// Knob 3: Noise. Complex noise on every coefficient, 0.1 times Noise^0.85 of the lit threshold at the coarsest column and 1.54 times that at the finest: at 1 it is a dim static under the cones and 28% of the LEDs are lit, at 8 the fine branches sink into it and 55% are lit. +// Knob 4: Omega. The Morlet wavelet's centre frequency, 3 to 8 radians per unit of its width. At 3 the cones are smooth and the hue turns 2.8 radians a row at the finest column; from 5 the two halves of each branch cancel between them, so each cone opens into a striped oval. + +// Rows are the position b, columns the scale a, 21 columns to a factor of +// 3, from a = 0.243 on the left to 0.009 on the right. The Morlet wavelet +// psi(t) = exp(-t^2 / 2) (exp(i w t) - exp(-w^2 / 2)) is taken against the +// Cantor measure mu, the derivative of the staircase F: +// +// T(a, b) = (1/a) * integral psi((x - b) / a) dmu(x), +// +// which is -1/a times the transform of F itself with psi' as wavelet. The +// staircase rises as |x - x0|^alpha at each point of the set, alpha = +// log 2 / log 3, so T grows as a^(alpha - 1) on its maxima, and every +// column is multiplied by a^(1 - alpha). Then the measure's own symmetry, +// mu on [0, 1/3] being half of mu shrunk by 3, makes the scaled T at +// (a/3, b/3) equal to the scaled T at (a, b): zooming in by 3 about b = 0 and moving 21 columns gives +// the picture back, and the loop has no seam. The mass of [2/3, 1] is 3.95 +// wavelet widths from the bottom row at the end of the loop. +// +// At scale a the measure is cut at the stage k where the intervals are +// 1/5.5 to 1/16.5 of a, each interval a point mass at its centre. The +// spread of the measure inside an interval, variance (3^-k)^2 / 8, is put +// back with the second derivative of psi, so the sum changes by under 0.1% +// when k steps. Only the masses within 4a of the panel count, 8 to 64 of +// them and 410 to 1860 terms a column, and down the rows each mass runs by +// recurrence: the Gaussian by two products and the phase by one rotation. +// +// The 72% of LEDs with the smallest modulus stay at 24 or under, drawn as +// the log of the modulus over a range of e^4; the rest climb from 25 to +// 255. The hue is the phase. Noise on the coefficients is what lights the +// gaps of the set, where the staircase is flat and its transform is all +// but zero. + +const W = 64, H = 128; +const { floor, ceil, min, max, round, sqrt, sin, cos, exp, log, pow, atan2, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const LN3 = log(3); +const ALPHA = log(2) / LN3; + +// Rows: position b from -0.04 to 1.04 at the start of the loop. +const B0 = -0.04, DB = 1.08 / H; +// Columns: scale a from AMAX at column 0 down by 3 every PER columns. +const AMAX = 0.243, PER = 21; +// The level of point masses keeps a / 3^-k in [RMIN, 3 RMIN). +const RMIN = 5.5, KMAX = 9; +// Histogram of the modulus for the exposure. +const HB = 512; +// Share of LEDs left under the lit threshold. +const DARK = 0.72; +// Under the threshold the brightness is the log of the modulus, over e^LOGR. +const LOGR = 4; +// Noise on the coefficients at Noise 1, as a share of the threshold. +const NOISE = 0.1; + +// Centres of the 2^k intervals of the k-th stage of the Cantor set, sorted. +const CENTRES = []; +for (let k = 0; k <= KMAX; k++) { + const n = 1 << k, w = pow(3, -k), arr = new Float64Array(n); + for (let j = 0; j < n; j++) { + let x = 0, s = 1 / 3; + for (let i = k - 1; i >= 0; i--) { + if ((j >> i) & 1) x += 2 * s; + s /= 3; + } + arr[j] = x + w / 2; + } + CENTRES.push(arr); +} +const firstAtLeast = (arr, x) => { + let lo = 0, hi = arr.length; + while (lo < hi) { + const m = (lo + hi) >> 1; + if (arr[m] < x) lo = m + 1; + else hi = m; + } + return lo; +}; + +// A small generator of its own, seeded once from Math.random, for the +// 16384 noise draws a frame; two uniforms make a triangle of variance 1. +let seed = 1; +const uni = () => { + seed ^= seed << 13; + seed ^= seed >>> 17; + seed ^= seed << 5; + return (seed >>> 0) / 4294967296; +}; +const tri = () => (uni() + uni() - 1) * 2.449; + +export function setup(params) { + seed = (Math.random() * 4294967295) | 1; + params.s = { + u: 0, + hue: 0, + noise: 1, + omega: 3, + re: new Float64Array(W * H), + im: new Float64Array(W * H), + mod: new Float64Array(W * H), + hist: new Uint32Array(HB), + // Complex noise on every coefficient, refreshed a little every frame. + noiseRI: new Float32Array(W * H * 2).map(tri), + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 1, 3], 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.noise = cl(k[2] == null ? 1 : k[2], 1, 8); + S.omega = cl(k[3] == null ? 3 : k[3], 3, 8); + S.u = (S.u + (d * speed) / 12) % 1; + // The noise drifts: each value keeps exp(-dt / 0.15 s) of itself. + const keep = exp(-d / 0.15), fresh = sqrt(1 - keep * keep), nz = S.noiseRI; + for (let i = 0; i < nz.length; i++) nz[i] = nz[i] * keep + fresh * tri(); +} + +// Adds the transform of the Cantor measure at one column into re, im. +function column(S, c) { + const z = pow(3, -S.u), om = S.omega, kap = exp(-om * om / 2); + const a = AMAX * pow(3, -c / PER) * z; + const db = DB * z, b0 = (B0 + 0.5 * DB) * z; + const k = cl(ceil(log(RMIN / a) / LN3), 0, KMAX); + const r = a * pow(3, k), v2 = 1 / (16 * r * r), norm = pow(r, -ALPHA); + const reach = 4 * a, xs = CENTRES[k]; + const delta = db / a, D = exp(-delta * delta), cw = cos(om * delta), sw = sin(om * delta); + const re = S.re, im = S.im, off = c; + for (let n = 0; n < H; n++) re[n * W + off] = im[n * W + off] = 0; + const end = b0 + (H - 1) * db + reach; + for (let j = firstAtLeast(xs, b0 - reach); j < xs.length && xs[j] <= end; j++) { + const x = xs[j]; + const n0 = max(0, ceil((x - reach - b0) / db)), n1 = min(H - 1, floor((x + reach - b0) / db)); + let t = (x - (b0 + n0 * db)) / a; + let G = exp(-t * t / 2), Q = exp(t * delta - delta * delta / 2); + let zr = cos(om * t), zi = sin(om * t); + for (let n = n0; n <= n1; n++) { + const tt = t * t; + const p = 1 + v2 * (tt - 1 - om * om), q = -2 * v2 * om * t; + const kk = kap * (1 + v2 * (tt - 1)); + re[n * W + off] += G * (zr * p - zi * q - kk); + im[n * W + off] += G * (zr * q + zi * p); + G *= Q; + Q *= D; + t -= delta; + const tr = zr * cw + zi * sw; + zi = zi * cw - zr * sw; + zr = tr; + } + } + for (let n = 0; n < H; n++) { + re[n * W + off] *= norm; + im[n * W + off] *= norm; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + for (let c = 0; c < W; c++) column(S, c); + const re = S.re, im = S.im, mod = S.mod, hist = S.hist, N = W * H; + // Exposure from the staircase alone: the lit threshold is the modulus that + // DARK of the LEDs stay under, full brightness the top 0.5%. + let top = 0; + for (let p = 0; p < N; p++) { + const m = sqrt(re[p] * re[p] + im[p] * im[p]); + mod[p] = m; + if (m > top) top = m; + } + hist.fill(0); + const hs = (HB - 1) / (top || 1); + for (let p = 0; p < N; p++) hist[floor(mod[p] * hs)]++; + let mf = 0, mt = top; + for (let i = 0, acc = 0; i < HB; i++) { + acc += hist[i]; + if (!mf && acc >= DARK * N) mf = (i + 1) / hs; + if (acc >= 0.995 * N) { + mt = (i + 1) / hs; + break; + } + } + mt = max(mt, mf * 1.05); + const nz = S.noiseRI, hue = S.hue + 0.5; + for (let c = 0; c < W; c++) { + // The noise grows as a^-(alpha - 1/2) towards fine scales, as white + // noise in the staircase would against the Cantor part. + const sig = NOISE * mf * pow(S.noise, 0.85) * pow(3, (c / PER) * (ALPHA - 0.5)); + const cmax = 3 * sig; + for (let n = 0; n < H; n++) { + const p = n * W + c; + let nr = nz[2 * p] * sig, ni = nz[2 * p + 1] * sig; + const nm = sqrt(nr * nr + ni * ni); + if (nm > cmax) { + nr *= cmax / nm; + ni *= cmax / nm; + } + const zr = re[p] + nr, zi = im[p] + ni; + const m = sqrt(zr * zr + zi * zi); + // Under the threshold the LED stays under 24; over it, it climbs. + let v, sat; + if (m < mf) { + v = m > 0 ? 24.4 * cl(1 + log(m / mf) / LOGR, 0, 1) : 0; + // The more of the coefficient is noise, the deeper its colour. + sat = 0.3 + (0.65 * nm) / (nm + mod[p] + 1e-9); + } else { + const x = min(1, (m - mf) / (mt - mf)); + v = 25 + 230 * pow(x, 0.8); + sat = 0.85 - 0.4 * x * x; + } + // Hue: the phase, once round the wheel per turn. + let h = atan2(zi, zr) / TAU + hue; + h = (h - floor(h)) * 6; + const i = floor(h), fr = h - i; + const P = v * (1 - sat), Q = v * (1 - sat * fr), T = v * (1 - sat * (1 - fr)); + let R, G, B; + if (i === 0) (R = v), (G = T), (B = P); + else if (i === 1) (R = Q), (G = v), (B = P); + else if (i === 2) (R = P), (G = v), (B = T); + else if (i === 3) (R = P), (G = Q), (B = v); + else if (i === 4) (R = T), (G = P), (B = v); + else (R = v), (G = P), (B = Q); + if (m >= mf) { + // A lit LED sits on a grey of 25, so none of its channels is as dim + // as the floor. + const k = (v - 25) / v; + R = 25 + R * k; + G = 25 + G * k; + B = 25 + B * k; + } + display.setPixel(c, n, round(R), round(G), round(B)); + } + } +} From 72597899fcf0e82ffe58f0b1c7799c26acf394f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 23:01:51 +0000 Subject: [PATCH 05/12] =?UTF-8?q?=ED=8C=A8=ED=84=B4=2082=ED=8C=90:=20?= =?UTF-8?q?=EA=B0=88=EB=9E=98=20fork=20=EC=A4=91=EA=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pattern-study/patterns/fork-hologram.js | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 incoming/pattern-study/patterns/fork-hologram.js diff --git a/incoming/pattern-study/patterns/fork-hologram.js b/incoming/pattern-study/patterns/fork-hologram.js new file mode 100644 index 00000000..71356f8e --- /dev/null +++ b/incoming/pattern-study/patterns/fork-hologram.js @@ -0,0 +1,175 @@ +// ===== Patternflow pattern ===== +// Title: Fork Hologram +// Family: wave +// Idea: DRAFT +// Author: patternflow-lab +// Date: 2026-10-31 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: DRAFT; pattern round 82 +// Round: 82 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Charge=3..7, Carrier=0..1 +// +// Knob 1: Hue. DRAFT +// Knob 2: Speed. DRAFT +// Knob 3: Charge. DRAFT +// Knob 4: Carrier. DRAFT + +const W = 64, H = 128; +const { floor, min, max, abs, sqrt, sin, cos, atan2, exp, pow, round, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// Pairs kept on the panel, the box the vortices may wander in (a margin +// past every edge), the carrier tilt, the gain that flattens the fringe tops. +const PAIRS = 5, MARGIN = 6, KY = 0.04, BEND = 0.6, P0 = 3.0; + +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 vortex(x, y, l, vx, vy) { + return { x, y, l, vx, vy, wx: 0, wy: 0, kick: 1, age: 0 }; +} + +function charge(top) { + // Half the pairs carry the top charge, the rest anything from 1 up. + return Math.random() < 0.5 ? top : 1 + floor(Math.random() * top); +} + +function birth(S, x, y, top) { + const l = charge(top), a = Math.random() * TAU, v = 7; + S.v.push(vortex(x + cos(a) * 0.4, y + sin(a) * 0.4, l, cos(a) * v, sin(a) * v)); + S.v.push(vortex(x - cos(a) * 0.4, y - sin(a) * 0.4, -l, -cos(a) * v, -sin(a) * v)); +} + +function seed(S, top) { + S.v = []; + for (let i = 0; i < PAIRS; i++) { + const l = charge(top); + S.v.push(vortex(6 + Math.random() * 52, 6 + Math.random() * 116, l, 0, 0)); + S.v.push(vortex(6 + Math.random() * 52, 6 + Math.random() * 116, -l, 0, 0)); + } + for (const p of S.v) p.kick = 0; +} + +export function setup(params) { + params.s = { v: null, t: 0, hue: 0, top: 3, f: 0.31, wait: 0, slide: 0 }; +} + +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; + const speed = cl(k[1] == null ? 2 : k[1], 0.2, 3); + S.top = round(cl(k[2] == null ? 3 : k[2], 3, 7)); + S.f = 1 / (P0 * pow(2, 1.6 * cl(k[3] == null ? 0.06 : k[3], 0, 1))); + if (!S.v) seed(S, S.top); + const h = (d * speed) / 2; + S.t += h; + S.slide += h * 0.12; + const V = S.v; + // Overdamped motion: opposite charges pull together, like charges push + // apart, both as 1/r, and every vortex wanders on a slow random heading. + for (const p of V) { + let fx = 0, fy = 0; + for (const q of V) { + if (q === p) continue; + const dx = p.x - q.x, dy = p.y - q.y, r2 = dx * dx + dy * dy + 4; + const s = Math.sign(p.l * q.l) * sqrt(abs(p.l * q.l)); + fx += (s * dx) / r2; + fy += (s * dy) / r2; + } + p.wx += (Math.random() - 0.5) * 6 * h - p.wx * 0.4 * h; + p.wy += (Math.random() - 0.5) * 6 * h - p.wy * 0.4 * h; + p.kick *= exp(-h * 1.6); + const ux = 9 * fx + p.wx + p.vx * p.kick, uy = 9 * fy + p.wy + p.vy * p.kick; + const sp = sqrt(ux * ux + uy * uy), cap = 9 / max(9, sp); + p.x += ux * cap * h; + p.y += uy * cap * h; + if (p.x < -MARGIN) { p.x = -MARGIN; p.wx = abs(p.wx); } + if (p.x > W + MARGIN) { p.x = W + MARGIN; p.wx = -abs(p.wx); } + if (p.y < -MARGIN) { p.y = -MARGIN; p.wy = abs(p.wy); } + if (p.y > H + MARGIN) { p.y = H + MARGIN; p.wy = -abs(p.wy); } + p.age += h; + } + // Opposite charges that meet add up: equal ones annihilate, unequal ones + // leave one vortex with the difference. + for (let i = 0; i < V.length; i++) + for (let j = i + 1; j < V.length; j++) { + const a = V[i], b = V[j]; + if (a.l * b.l >= 0) continue; + const dx = a.x - b.x, dy = a.y - b.y; + if (dx * dx + dy * dy > 1.2) continue; + const l = a.l + b.l; + if (l === 0) { + V.splice(j, 1); + V.splice(i, 1); + } else { + const keep = abs(a.l) > abs(b.l) ? a : b; + keep.l = l; + V.splice(keep === a ? j : i, 1); + } + i = -1; + break; + } + // A vortex of charge 2 or more is unstable and now and then sheds a unit one. + for (let i = V.length - 1; i >= 0; i--) { + const p = V[i]; + if (abs(p.l) > 1 && p.age > 2 && Math.random() < h * 0.08 * (abs(p.l) - 1)) { + const s = Math.sign(p.l), a = Math.random() * TAU; + p.l -= s; + p.age = 0; + const q = vortex(p.x, p.y, s, cos(a) * 6, sin(a) * 6); + p.vx = -cos(a) * 6 / abs(p.l); + p.vy = -sin(a) * 6 / abs(p.l); + p.kick = 1; + V.push(q); + } + } + // New pairs are born out of nothing, a pair at a time. + S.wait -= h; + if (V.length < 2 * PAIRS && S.wait <= 0) { + birth(S, 8 + Math.random() * 48, 10 + Math.random() * 108, S.top); + S.wait = 0.6 + Math.random() * 1.2; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.v) return; + const V = S.v, n = V.length, f = S.f; + const ky = KY * f * 3.2, kx = sqrt(f * f - ky * ky), k0 = f; + const base = S.hue + 0.5; + for (let y = 0; y < H; y++) { + for (let x = 0; x < W; x++) { + // The vortex phases multiplied as unit complex numbers, one atan2 at the end. + let re = 1, im = 0, gx = kx, gy = ky, c = 1; + for (let j = 0; j < n; j++) { + const p = V[j], dx = x - p.x, dy = y - p.y, r2 = dx * dx + dy * dy + 1e-6, ir = 1 / sqrt(r2); + const ux = dx * ir, uy = (p.l > 0 ? dy : -dy) * ir; + for (let m = abs(p.l); m > 0; m--) { + const t = re * ux - im * uy; + im = re * uy + im * ux; + re = t; + } + const w = p.l / (TAU * r2); + gx -= dy * w; + gy += dx * w; + const r0 = 1.1 * abs(p.l); + c *= r2 / (r2 + r0); + } + let u = kx * x + ky * y + S.slide + atan2(im, re) / TAU; + u -= floor(u); + const tri = 1 - abs(2 * u - 1); + const v = 0.5 + (tri - BEND * sin(TAU * tri) / TAU - 0.5) * c; + const g = sqrt(gx * gx + gy * gy) / k0 - 1; + const hue = base + 0.14 * (g / (1 + abs(g))); + const rgb = hsv(hue, 0.55 - 0.2 * (1 - c), v); + display.setPixel(x, y, round(rgb[0] * 255), round(rgb[1] * 255), round(rgb[2] * 255)); + } + } +} From bd9b1ee60066467b07115c66a5fa8dfb9647dda0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 23:01:51 +0000 Subject: [PATCH 06/12] =?UTF-8?q?=ED=8C=A8=ED=84=B4=2082=ED=8C=90:=20Phaso?= =?UTF-8?q?r=20Noise=20=EC=99=80=20Noordzij=20Cube?= 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_018svN6XCxZJqDPz16WwyFg6 --- .../pattern-study/patterns/noordzij-cube.js | 321 ++++++++++++++++++ .../pattern-study/patterns/phasor-noise.js | 174 ++++++++++ 2 files changed, 495 insertions(+) create mode 100644 incoming/pattern-study/patterns/noordzij-cube.js create mode 100644 incoming/pattern-study/patterns/phasor-noise.js diff --git a/incoming/pattern-study/patterns/noordzij-cube.js b/incoming/pattern-study/patterns/noordzij-cube.js new file mode 100644 index 00000000..73425fb8 --- /dev/null +++ b/incoming/pattern-study/patterns/noordzij-cube.js @@ -0,0 +1,321 @@ +// ===== Patternflow pattern ===== +// Title: Noordzij Cube +// Family: type +// Idea: A pen writes three lowercase words down the panel stroke by stroke, and the width at every point of every stroke comes from Gerrit Noordzij's two kinds of contrast, the broad nib's translation, thick or thin by the stroke's direction against the nib, and the pointed pen's expansion, swelling on the downstrokes, so turning the knobs moves the whole page across his cube on one unchanged skeleton. +// Author: patternflow-lab +// Date: 2026-10-31 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the theory of the stroke and its cube of contrast, translation, expansion and the amount of contrast (Gerrit Noordzij, De streek, 1985; The stroke: theory of writing, 2005); lowercase skeletons drawn for this pattern, pattern round 82 +// Round: 82 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Model=0..1, Hairline=0..1 +// +// Knob 1: Hue. Turns the ink round the wheel. At 0 every stroke runs from cyan where the pen comes down to violet where it lifts, deep where the ink is thick and pale in a hairline. +// Knob 2: Speed. At 2 the pen moves 44 LEDs a second, a page of three words takes 17 to 19 s and holds for 2.5 s before it fades; at 0.2 the pen is ten times slower. +// Knob 3: Model. Where the page stands between translation at 0, a broad nib held at 30 degrees, thickest across the nib and thinnest along it, and expansion at 1, a pointed pen that swells on every downstroke and runs thin up and across. The skeleton and the order of the strokes never change. +// Knob 4: Hairline. The thinnest stroke as a share of the thickest, which is 4.8 LEDs, the amount of contrast read the other way: at the opening 0.06 a hairline is the floor of 0.8 LED; at 1 every stroke is 4.8 LEDs and translation and expansion give the same letters, the edge of the cube where there is no contrast to tell them apart. + +// Noordzij reads every contrast in written letters as the trace of a pen. +// A broad nib moved without turning, translation, leaves its full width +// where it travels across its edge and a hairline where it travels along +// it, so the thick and thin of a letter follow the direction of each stroke +// and the angle of the nib. A pointed flexible nib, expansion, is thin until +// it is pressed, and the hand presses on the way down, so the thick falls on +// the downstrokes and swells in and out. His cube sets these two models on +// one edge, the amount of contrast on another, and interrupted against +// running construction on the third. Here the third stays interrupted: the +// pen lifts between the strokes of a letter and the letters do not join. +// +// Every letter is one centreline, written in order and in one direction. +// Each point of it carries both widths, worked out once: the broad nib's +// |sin(direction - 30 degrees)| and the pointed pen's pressure, the share of +// the stroke's motion that runs down, smoothed forwards and backwards over +// 3.2 LEDs so it comes and goes. Model blends the two, Hairline sets how +// thin the thinnest is, and a round soft-edged pen of that width inks the +// line. Nothing about the letters changes but those two numbers. + +const W = 64, H = 128, N = W * H; +const { floor, ceil, min, max, abs, sqrt, sin, cos, atan2, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// Skeleton lowercase in a box 7 wide, y down from the x-line at 0 to the +// base line at 10, ascender to -6.5 and descender to 16.5: the strokes in +// the order and direction a hand writes them. M moves, L draws a line, and +// E cx cy rx ry a0 a1 an arc of an ellipse from angle a0 to a1 in degrees +// (0 right, 90 down), so a falling angle runs anticlockwise on the panel. +const GLYPHS = { + a: "E3.5 5 3.5 5 330 30|M7 0L7 10", + b: "M0 -6.5L0 10|E3.5 5 3.5 5 205 515", + e: "M0.2 5L7 5E3.5 5 3.5 5 0 -318", + g: "E3.5 5 3.5 5 330 30|M7 0L7 12.5E3.5 12.5 3.5 4 0 165", + n: "M0 0L0 10|E3.5 3.6 3.5 3.6 180 360L7 10", + o: "E3.5 5 3.5 5 290 -66", +}; +const PAGES = [ + ["nano", "bone", "ego"], + ["oboe", "nag", "eon"], +]; +// LEDs per unit across and down, the space between letters in LEDs, the +// thickest stroke, the nib angle and the length over which a pointed pen's +// pressure comes and goes, in LEDs. +const KX = 1.38, KY = 2.1, GAP = 6.5, THICK = 4.8, NIB = (30 * PI) / 180, SWELL = 3.2; + +function parse(src) { + return src.split("|").map((s) => { + const tk = s.match(/[MLE]|-?[\d.]+/g), pts = []; + for (let i = 0; i < tk.length; ) { + const c = tk[i++]; + if (c === "M" || c === "L") pts.push([+tk[i++], +tk[i++]]); + else { + const [cx, cy, rx, ry, a0, a1] = tk.slice(i, i + 6).map(Number); + i += 6; + const n = ceil(abs(a1 - a0) / 6); + for (let j = 0; j <= n; j++) { + const a = ((a0 + ((a1 - a0) * j) / n) * PI) / 180; + pts.push([cx + rx * cos(a), cy + ry * sin(a)]); + } + } + } + return pts; + }); +} +const FONT = {}; +for (const c in GLYPHS) FONT[c] = parse(GLYPHS[c]); + +// A stroke in LEDs, cut into steps of at most an LED. For each point: +// tr, the width a broad nib at NIB leaves, |sin(direction - nib)|, and ex, +// the pressure on a pointed pen: how much the stroke runs down, smoothed +// along it forwards and backwards so it swells in and out. +function stroke(pts) { + const P = []; + for (let i = 0; i < pts.length; i++) { + if (!i) { + P.push(pts[0]); + continue; + } + const [qx, qy] = P[P.length - 1], [px, py] = pts[i], n = ceil(sqrt((px - qx) ** 2 + (py - qy) ** 2)); + for (let j = 1; j <= n; j++) P.push([qx + ((px - qx) * j) / n, qy + ((py - qy) * j) / n]); + } + const n = P.length, xy = new Float32Array(2 * n), cum = new Float32Array(n), tr = new Float32Array(n), ex = new Float32Array(n); + const raw = new Float32Array(n), fw = new Float32Array(n); + for (let i = 0; i < n; i++) { + xy[2 * i] = P[i][0]; + xy[2 * i + 1] = P[i][1]; + if (i) cum[i] = cum[i - 1] + sqrt((P[i][0] - P[i - 1][0]) ** 2 + (P[i][1] - P[i - 1][1]) ** 2); + const a = P[max(0, i - 1)], b = P[min(n - 1, i + 1)], dx = b[0] - a[0], dy = b[1] - a[1], L = sqrt(dx * dx + dy * dy) || 1; + // The direction with y up, as the nib angle is measured. + tr[i] = abs(sin(atan2(-dy, dx) - NIB)); + raw[i] = max(0, dy / L); + } + for (let i = 0, f = 0; i < n; i++) { + const k = 1 - exp(-(i ? cum[i] - cum[i - 1] : 1) / SWELL); + f += (raw[i] - f) * k; + fw[i] = f; + } + for (let i = n - 1, f = 0; i >= 0; i--) { + const k = 1 - exp(-(i < n - 1 ? cum[i + 1] - cum[i] : 1) / SWELL); + f += (raw[i] - f) * k; + ex[i] = min(fw[i], f) ** 0.8; + } + return { xy, cum, tr, ex, len: cum[n - 1] }; +} + +// One page: each word on a line of its own, centred, the lines spread +// evenly down the panel by their ascenders and descenders. +function layout(words) { + const lines = words.map((w) => ({ + w, + up: /b/.test(w) ? 6.5 : 0, + down: /g/.test(w) ? 6.5 : 0, + width: w.length * 7 * KX + (w.length - 1) * GAP, + })); + const tall = lines.reduce((s, l) => s + (10 + l.up + l.down) * KY, 0), gap = (H - tall) / (lines.length + 1); + const strokes = [], guides = []; + let y = gap; + for (const l of lines) { + const top = y + l.up * KY; + let x = (W - 1 - l.width) / 2; + for (const c of l.w) { + for (const pts of FONT[c]) strokes.push(stroke(pts.map(([u, v]) => [x + u * KX, top + v * KY]))); + x += 7 * KX + GAP; + } + guides.push(Math.round(top), Math.round(top + 10 * KY)); + y += (10 + l.up + l.down) * KY + gap; + } + return { strokes, guides }; +} + +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.nc = { + page: -1, strokes: [], guides: [], si: 0, d: 0, mode: "lift", lift: 0, liftT: 0.1, from: [32, -6], to: [32, -6], pen: [32, -6], timer: 0, fade: 1, started: false, + hue: 0, speed: 2, model: 0, hair: 0.06, + cov: new Float32Array(N), fat: new Float32Array(N), pos: new Float32Array(N), out: new Float32Array(N * 3), + }; +} + +function nextPage(B) { + B.page = (B.page + 1) % PAGES.length; + const L = layout(PAGES[B.page]); + B.strokes = L.strokes; + B.guides = L.guides; + B.si = 0; + B.d = 0; + B.fade = 1; + startLift(B); +} + +function startLift(B) { + const s = B.strokes[B.si]; + B.mode = "lift"; + B.from = [B.pen[0], B.pen[1]]; + B.to = [s.xy[0], s.xy[1]]; + B.lift = 0; + B.liftT = max(0.08, sqrt((B.to[0] - B.from[0]) ** 2 + (B.to[1] - B.from[1]) ** 2) / (3 * rate(B))); +} + +const rate = (B) => 22 * B.speed; + +// The point at distance d along a stroke. +function at(s, d) { + const n = s.cum.length; + if (d >= s.len) return [s.xy[2 * n - 2], s.xy[2 * n - 1]]; + let i = 0; + while (i < n - 2 && s.cum[i + 1] < d) i++; + const f = (d - s.cum[i]) / max(1e-6, s.cum[i + 1] - s.cum[i]); + return [s.xy[2 * i] + (s.xy[2 * i + 2] - s.xy[2 * i]) * f, s.xy[2 * i + 1] + (s.xy[2 * i + 3] - s.xy[2 * i + 1]) * f]; +} + +function step(B, dt) { + if (B.mode === "write") { + const s = B.strokes[B.si]; + B.d += rate(B) * dt; + B.pen = at(s, B.d); + if (B.d >= s.len) { + B.si++; + if (B.si < B.strokes.length) startLift(B); + else (B.mode = "hold"), (B.timer = 2.5); + } + } else if (B.mode === "lift") { + B.lift = min(1, B.lift + dt / B.liftT); + const e = B.lift * B.lift * (3 - 2 * B.lift); + B.pen = [B.from[0] + (B.to[0] - B.from[0]) * e, B.from[1] + (B.to[1] - B.from[1]) * e]; + if (B.lift >= 1) (B.mode = "write"), (B.d = 0); + } else if (B.mode === "hold") { + B.timer -= (dt * B.speed) / 2; + if (B.timer <= 0) (B.mode = "fade"), (B.timer = 0.8); + } else { + B.timer -= dt; + B.fade = max(0, B.timer / 0.8); + if (B.timer <= 0) nextPage(B); + } +} + +export function update(dt, input, params) { + if (!params.nc) setup(params); + const B = params.nc, k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + B.hue = k[0] || 0; + B.speed = cl(k[1] == null ? 2 : k[1], 0.2, 3); + B.model = cl(k[2] || 0, 0, 1); + B.hair = cl(k[3] == null ? 0.06 : k[3], 0, 1); + if (!B.started) { + // The panel opens 13.5 s into the page: at Speed 2 the pen has written + // all but the last word. + B.started = true; + nextPage(B); + for (let i = 0; i < 405; i++) step(B, 1 / 30); + } + step(B, dt); +} + +// The width of a stroke at a point: the two models blended by Model, the +// result laid between the hairline and the thickest stroke. +function width(B, s, i) { + const f = (1 - B.model) * s.tr[i] + B.model * s.ex[i], w = max(0.8, THICK * (B.hair + (1 - B.hair) * f)); + return [w, w / THICK]; +} + +// Ink one piece of a stroke with a round pen whose radius runs from ra to +// rb along it, its edge softened over one LED. fat keeps how thick the +// stroke is there and pos how far along it, from 0 to 1, for the colour. +function piece(B, ax, ay, bx, by, ra, rb, fa, fb, ua, ub) { + const r = max(ra, rb) + 1; + 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)); + const dx = bx - ax, dy = by - ay, L2 = dx * dx + dy * dy || 1e-9, { cov, fat, pos } = B; + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const t = cl(((x - ax) * dx + (y - ay) * dy) / L2, 0, 1), ex = x - ax - t * dx, ey = y - ay - t * dy; + const c = cl(ra + (rb - ra) * t - sqrt(ex * ex + ey * ey) + 0.5, 0, 1); + const p = y * W + x; + if (c > cov[p]) { + cov[p] = c; + fat[p] = fa + (fb - fa) * t; + pos[p] = ua + (ub - ua) * t; + } + } +} + +export function draw(display, params) { + const B = params.nc; + if (!B || !B.started) return; + const { cov, fat } = B; + cov.fill(0); + const done = B.mode === "write" ? B.si + 1 : B.si; + for (let k = 0; k < done && k < B.strokes.length; k++) { + const s = B.strokes[k], lim = k === B.si && B.mode === "write" ? B.d : 1e9; + let [wa, fa] = width(B, s, 0); + for (let i = 0; i + 1 < s.cum.length && s.cum[i] < lim; i++) { + let [wb, fb] = width(B, s, i + 1), bx = s.xy[2 * i + 2], by = s.xy[2 * i + 3]; + if (s.cum[i + 1] > lim) { + const f = (lim - s.cum[i]) / (s.cum[i + 1] - s.cum[i]); + bx = s.xy[2 * i] + (bx - s.xy[2 * i]) * f; + by = s.xy[2 * i + 1] + (by - s.xy[2 * i + 1]) * f; + } + piece(B, s.xy[2 * i], s.xy[2 * i + 1], bx, by, wa / 2, wb / 2, fa, fb, s.cum[i] / s.len, min(lim, s.cum[i + 1]) / s.len); + wa = wb; + fa = fb; + } + } + // Each stroke runs from cyan where the pen comes down to violet where it + // lifts, so the colour shows the way it was written; thick ink is deep, + // a hairline pale. + const { out, pos } = B, F = B.fade, rule = new Uint8Array(H); + for (const g of B.guides) if (g >= 0 && g < H) rule[g] = 1; + for (let p = 0, y = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const c = cov[p] * F, bg = rule[y] && x % 3 === 0 ? (34 / 255) * F : 0; + let r = bg * 0.7, g = bg * 0.8, b = bg; + if (c > 0) { + const [ir, ig, ib] = hsv(B.hue + 0.48 + 0.24 * pos[p], 0.2 + 0.7 * fat[p], 1); + r += (ir - r) * c; + g += (ig - g) * c; + b += (ib - b) * c; + } + out[p * 3] = r; + out[p * 3 + 1] = g; + out[p * 3 + 2] = b; + } + // The pen: a white point as wide as the stroke it is writing, dim and + // small while it is lifted between strokes. + if (B.mode === "write" || B.mode === "lift") { + const s = B.strokes[min(B.si, B.strokes.length - 1)]; + let i = 0; + while (i < s.cum.length - 1 && s.cum[i + 1] <= B.d) i++; + const rr = B.mode === "write" ? width(B, s, i)[0] / 2 + 0.4 : 1; + const v = B.mode === "write" ? 1 : 0.6, [px, py] = B.pen; + for (let y = max(0, floor(py - rr - 1)); y <= min(H - 1, ceil(py + rr + 1)); y++) + for (let x = max(0, floor(px - rr - 1)); x <= min(W - 1, ceil(px + rr + 1)); x++) { + const c = cl(rr - sqrt((x - px) ** 2 + (y - py) ** 2) + 0.5, 0, 1), p = (y * W + x) * 3; + for (let j = 0; j < 3; j++) out[p + j] += (v - out[p + j]) * c; + } + } + for (let p = 0; p < N; p++) display.setPixel(p % W, floor(p / W), out[p * 3] * 255, out[p * 3 + 1] * 255, out[p * 3 + 2] * 255); +} diff --git a/incoming/pattern-study/patterns/phasor-noise.js b/incoming/pattern-study/patterns/phasor-noise.js new file mode 100644 index 00000000..902cf70d --- /dev/null +++ b/incoming/pattern-study/patterns/phasor-noise.js @@ -0,0 +1,174 @@ +// ===== Patternflow pattern ===== +// Title: Phasor Noise +// Family: noise +// Idea: One set of Gabor kernels, little wave packets turned by a field with a whirl and a saddle drifting through it, is summed into a complex field and shown twice: on top as Gabor noise, its imaginary part, whose stripes fade to flat wherever the kernels cancel, and below as phasor noise, a profile of its phase alone, the same curving bands at full contrast everywhere and broken only at the points where the sum is zero. +// Author: patternflow-lab +// Date: 2026-10-31 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: phasor noise (Thibault Tricard, Semyon Efremov, Cédric Zanni, Fabrice Neyret, Jonàs Martínez and Sylvain Lefebvre, Procedural phasor noise, 2019), built on sparse convolution Gabor noise (Ares Lagae, Sylvain Lefebvre, George Drettakis and Philip Dutré, Procedural noise using sparse Gabor convolution, 2009); pattern round 82 +// Round: 82 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Frequency=8..11, Profile=0..1 +// +// Knob 1: Hue. Turns the colours round the wheel. Each band takes its colour from the way it runs: at 0 level bands are sea green, slanting ones azure and upright ones violet. +// Knob 2: Speed. At 2 the bands travel one period every 3.3 s, each kernel circles its place every 9 s and the direction field turns once in 50 s; all of it goes as Speed / 2. +// Knob 3: Frequency. Stripes across the 64 LEDs of the panel: 8 is a period of 8 LEDs, 11 a period of 5.8. The kernels keep their size, 6.5 LEDs to one standard deviation, so at 11 each holds 1.4 times as many stripes. +// Knob 4: Profile. The wave the phase is drawn with in the lower half, blended between a sine at 0, a triangle at 0.33, a square at 0.67 and a sawtooth at 1, whose sharp side shows which way each band's phase runs. The upper half is Gabor noise at every setting. + +// A Gabor kernel is a Gaussian times a complex plane wave, +// +// g_i(x) = exp(-|x - x_i|^2 / (2 s^2)) exp(i (k_i . (x - x_i) + phi_i)), +// +// and the sum G(x) of a few hundred of them, scattered on a jittered grid, +// is complex noise with the frequency and orientation of its kernels. Lagae +// and his co-authors showed Im G, Gabor noise: wherever the kernels around a +// point happen to cancel, |G| is small and the stripes fade out. Tricard and +// his co-authors kept the phase and threw the amplitude away, profile(arg G): +// stripes of one contrast everywhere, with the same orientation and period, +// broken only where G = 0, the points the phase winds round. Both halves of +// the panel here are the same 64 x 63 window of the same G, so every place +// the upper stripes fade to flat is a fork or an end in the lower ones. +// +// The kernel direction comes from a field with a whirl and a saddle in it, +// theta = theta0 + the angle round the whirl - the angle round the saddle. +// Round the whirl the wave vectors point outwards (rings), across (spokes) +// or in between (spirals) as theta0 turns; spokes and spirals spread apart +// as they go out, and phasor noise can only keep its period by forking. +// +// Each kernel is separable, the Gaussian and the wave both, so it is two +// short complex rows, one along x and one along y, multiplied pixel by pixel. + +const W = 64, H = 128, FH = 63, LOW = 65; +const { floor, min, max, round, sqrt, sin, cos, exp, atan2, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// Kernel size and reach in LEDs, and the grid the kernels sit on. +const SIG = 6.5, R = 18, STEP = 8; + +function hsv(h, s, v, o, k) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[k] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + o[k + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + o[k + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +export function setup(params) { + const ks = []; + for (let y = -R; y < FH + R; y += STEP) + for (let x = -R; x < W + R; x += STEP) + ks.push({ + hx: x + (Math.random() - 0.5) * STEP * 0.8, + hy: y + (Math.random() - 0.5) * STEP * 0.8, + ph: Math.random() * TAU, + wa: Math.random() * TAU, + wr: 0.8 + 1.2 * Math.random(), + ws: Math.random() < 0.5 ? -1 : 1, + cx: 0, cy: 0, kx: 0, ky: 0, + }); + params.s = { + ks, t: 0, hue: 0, freq: 8, prof: 0.06, + gr: new Float32Array(W * FH), gi: new Float32Array(W * FH), u: [0, 0], th: new Float32Array(W * FH), + xr: new Float32Array(2 * R + 2), xi: new Float32Array(2 * R + 2), out: new Float32Array(W * H * 3), + }; +} + +// A whirl and a saddle, each on a slow loop of its own. +function poles(t) { + return [24 + 8 * sin(0.071 * t), 20 + 6 * cos(0.053 * t + 1), 40 + 8 * sin(0.062 * t + 2), 46 + 6 * cos(0.047 * t + 0.5)]; +} + +// The kernel direction at (x, y) as a unit complex number, +// e^(i theta0) (z - whirl) / |z - whirl| times the conjugate for the saddle. +function dir(P, c0, s0, x, y, o) { + const ar = x - P[0], ai = y - P[1], br = x - P[2], bi = -(y - P[3]); + const pr = ar * br - ai * bi, pi = ar * bi + ai * br; + const qr = pr * c0 - pi * s0, qi = pr * s0 + pi * c0, m = sqrt(qr * qr + qi * qi) || 1; + o[0] = qr / m; + o[1] = qi / m; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 8, 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.freq = cl(k[2] == null ? 8 : k[2], 8, 11); + S.prof = cl(k[3] == null ? 0.06 : k[3], 0, 1); + S.t += (d * speed) / 2; +} + +function field(S) { + const t = S.t, P = poles(t), th0 = (TAU * t) / 50 + 0.4, c0 = cos(th0), s0 = sin(th0), kk = (TAU * S.freq) / W; + const { gr, gi, xr, xi, u } = S; + gr.fill(0); + gi.fill(0); + const a = 1 / (2 * SIG * SIG), drift = (TAU * t) / 3.3; + for (const K of S.ks) { + const w = ((TAU * t) / 9) * K.ws + K.wa; + const cx = K.hx + K.wr * cos(w), cy = K.hy + K.wr * sin(w); + dir(P, c0, s0, cx, cy, u); + const kx = kk * u[0], ky = kk * u[1]; + const x0 = max(0, Math.ceil(cx - R)), x1 = min(W - 1, floor(cx + R)); + const y0 = max(0, Math.ceil(cy - R)), y1 = min(FH - 1, floor(cy + R)); + if (x0 > x1 || y0 > y1) continue; + for (let x = x0; x <= x1; x++) { + const dx = x - cx, e = exp(-a * dx * dx), p = kx * dx; + xr[x - x0] = e * cos(p); + xi[x - x0] = e * sin(p); + } + for (let y = y0; y <= y1; y++) { + const dy = y - cy, e = exp(-a * dy * dy), p = ky * dy + K.ph - drift; + const yr = e * cos(p), yi = e * sin(p); + let o = y * W + x0; + for (let x = x0; x <= x1; x++, o++) { + const ar = xr[x - x0], ai = xi[x - x0]; + gr[o] += ar * yr - ai * yi; + gi[o] += ar * yi + ai * yr; + } + } + } + // cos 2 theta at every LED, which way its stripes run, for the colour. + const T = S.th; + for (let y = 0, o = 0; y < FH; y++) + for (let x = 0; x < W; x++, o++) { + dir(P, c0, s0, x, y, u); + T[o] = u[0] * u[0] - u[1] * u[1]; + } +} + +// Brightness of a band at height v of its wave; the lowest seventh is black. +const lum = (v) => (v < 0.14 ? 0 : ((v - 0.14) / 0.86) ** 2); + +// The wave drawn from the phase: 1 on the crest of sin, 0 in the trough. +function profile(ph, m) { + const s = 0.5 + 0.5 * sin(ph); + if (m <= 0) return s; + let u = ph / TAU - 0.25; + u -= floor(u); + const tri = Math.abs(2 * u - 1), sq = cl(0.5 + 2.6 * sin(ph), 0, 1), saw = 1 - u; + const z = m * 3, i = min(2, floor(z)), f = z - i; + const A = [s, tri, sq, saw]; + return A[i] + (A[i + 1] - A[i]) * f; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + field(S); + const { gr, gi, out } = S, n = W * FH; + let e = 0; + for (let i = 0; i < n; i++) e += gr[i] * gr[i] + gi[i] * gi[i]; + const rms = sqrt(e / n) || 1, hb = S.hue + 0.55; + out.fill(0); + for (let i = 0; i < n; i++) { + const h = hb + 0.16 * S.th[i]; + const g = cl(gi[i] / (1.5 * rms), -1, 1) * 0.5 + 0.5; + const p = profile(atan2(gi[i], gr[i]), S.prof); + hsv(h, 0.75 - 0.35 * g, lum(g), out, i * 3); + hsv(h, 0.75 - 0.35 * p, lum(p), out, (i + LOW * W) * 3); + } + for (let i = 0; i < W * H; i++) display.setPixel(i % W, floor(i / W), round(out[i * 3] * 255), round(out[i * 3 + 1] * 255), round(out[i * 3 + 2] * 255)); +} From 03f2996ef417331245863fc56b1fd7f866388956 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 23:05:28 +0000 Subject: [PATCH 07/12] =?UTF-8?q?=ED=8C=A8=ED=84=B4=2082=ED=8C=90:=20Croft?= =?UTF-8?q?on=20Cell=20=EA=B3=BC=20Alphabet=20Lineage?= 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_018svN6XCxZJqDPz16WwyFg6 --- .../pattern-study/patterns/alphabet-lineage.js | 16 ++++++++-------- incoming/pattern-study/patterns/crofton-cell.js | 15 +++++++++------ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/incoming/pattern-study/patterns/alphabet-lineage.js b/incoming/pattern-study/patterns/alphabet-lineage.js index 93dfceff..d01c91ef 100644 --- a/incoming/pattern-study/patterns/alphabet-lineage.js +++ b/incoming/pattern-study/patterns/alphabet-lineage.js @@ -48,7 +48,7 @@ const cl = (v, a, b) => (v < a ? a : v > b ? b : v); const K = 36; // Era colours at Hue 0 (h, s), widths of the round pen in grid units. -const ERA = [[0.12, 0.62], [0.82, 0.55], [0.56, 0.62], [0.09, 0.1]]; +const ERA = [[0.12, 0.7], [0.82, 0.62], [0.56, 0.7], [0.09, 0.16]]; const PEN = [0.3, 0.34, 0.37, 0.4]; // Seconds at Speed 2: hold in each era, the bend between, the rewind. const HOLD = [1.6, 1.2, 1.2, 2.6], BEND = 1.8, BACK = 1.4; @@ -82,17 +82,17 @@ const LETTERS = { [[0, 0, 0, 1], ["", "", "", "7.6,9.4 10.2,9.4"]], ], }, - // bayt, a house with its door -> beth -> beta -> B. + // bayt, the plan of a house with its door in the front wall -> beth -> + // beta -> B. B: { turn: [[0, 0], [0, 1], [0, 0]], strokes: [ [[1, 1, 1, 1], [ - "8.6,5 8.6,1.4 1.4,1.4 1.4,8.6 4,8.6", + "6,8.6 8.6,8.6 8.6,1.4 1.4,1.4 1.4,8.6 4,8.6", "6.9,5 1.2,4.6 5.8,0.6 6.6,2.6 6.9,5 6.4,7.6 4.4,9 1.8,9.4", "2.4,4.8 6.4,4.2 7.4,2.4 6.2,0.8 2.4,0.6 2.4,9.4 7,9.2 8.2,7.2 7,5.2 2.4,4.8", "2.2,4.7 5.6,4.6 6.8,3.8 7,2.5 6.6,1.3 5.4,0.6 2.2,0.6 2.2,9.4 6,9.4 7.6,8.6 8,7 7.4,5.5 5.8,4.8 2.2,4.7", ]], - [[1, 0, 0, 0], ["6,8.6 8.6,8.6", "", "", ""]], [[0, 0, 0, 1], ["", "", "", "0.8,0.6 2.4,0.6"]], [[0, 0, 0, 1], ["", "", "", "0.8,9.4 2.4,9.4"]], ], @@ -307,12 +307,12 @@ function seg(px, py, ax, ay, bx, by) { // Ink a segment with a round pen of radius w and a soft halo. function stamp(S, band, ax, ay, bx, by, w, a) { - const x0 = max(0, floor(min(ax, bx) - w - 2)), x1 = min(W - 1, floor(max(ax, bx) + w + 2)); - const y0 = max(0, floor(min(ay, by) - w - 2)), y1 = min(H - 1, floor(max(ay, by) + w + 2)); + const x0 = max(0, floor(min(ax, bx) - w - 3)), x1 = min(W - 1, floor(max(ax, bx) + w + 3)); + const y0 = max(0, floor(min(ay, by) - w - 3)), y1 = min(H - 1, floor(max(ay, by) + w + 3)); for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) { const d = seg(x + 0.5, y + 0.5, ax, ay, bx, by); - const v = a * max(cl(w + 0.5 - d, 0, 1), 0.3 * exp(-((d - w) * (d - w)) / 2.2)); + const v = a * max(cl(w + 0.5 - d, 0, 1), 0.34 * exp(-((d - w) * (d - w)) / 4.5)); const i = y * W + x; if (v > S.ink[i]) { S.ink[i] = v; @@ -327,7 +327,7 @@ export function draw(display, params) { S.ink.fill(0); const word = WORDS[S.word], step = HOLD[1] + BEND; // Grid unit in LEDs and the 32 row band of each letter. - const U = 2.7, colours = []; + const U = 3, colours = []; for (let n = 0; n < 4; n++) { const G = S.glyphs[word[n]], yc = n * 32 + 16, xc = 33; const x = eraAt(S.clock - n * S.lag * step); diff --git a/incoming/pattern-study/patterns/crofton-cell.js b/incoming/pattern-study/patterns/crofton-cell.js index 9e7163b9..4ae44453 100644 --- a/incoming/pattern-study/patterns/crofton-cell.js +++ b/incoming/pattern-study/patterns/crofton-cell.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Crofton Cell // Family: cells -// Idea: Isotropic Poisson lines drift, are born and die across the tall panel and cut it into convex cells tinted by their number of sides, while the one cell that holds the fixed point in the middle, the Crofton cell, is lit gold with its incircle drawn in it, so when the Big knob keeps every line at least r away from that point the cell swells and its gold corners thin to slivers round a nearly circular disc, as Kendall conjectured. +// Idea: Isotropic Poisson lines drift, are born and die across the tall panel and cut it into convex cells tinted by their number of sides, while the one cell that holds the fixed point in the middle, the Crofton cell, is lit as a pale incircle in gold corners under a dashed outer circle, so when the Big knob keeps every line at least r away from that point the cell swells, gains sides and its gold corners thin round the disc, as Kendall conjectured. // Author: patternflow-lab // Date: 2026-10-31 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -11,10 +11,10 @@ // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Lines=16..56, Big=0..1 // -// Knob 1: Hue. Turns every colour round the wheel together. At 0 triangles are teal, quadrilaterals azure, pentagons violet, hexagons pink and cells of seven sides or more lime, and the Crofton cell is a pale disc in gold corners. -// Knob 2: Speed. At 2 a line slides along its normal at up to 3 LEDs a second, turns at up to 0.05 radians a second and lives 10 s on average, so about one line a second is born somewhere and one dies. Everything goes as Speed / 2. -// Knob 3: Lines. The mean number of lines that cross the panel, 10 to 40; the cells come out about 120 at 20 lines and 420 at 40. -// Knob 4: Big. No line may come nearer the fixed point than r = 28 Big^0.6 LEDs, which is the Poisson process conditioned on the zero cell holding that disc: 5 LEDs at the opening 0.06, 12 at 0.25 and 28 at 1, where with 40 lines the cell is a 9 to 13 sided polygon whose incircle fills about 0.9 of its outer circle. +// Knob 1: Hue. Turns every colour round the wheel together. At 0 triangles are teal, quadrilaterals azure, pentagons violet, hexagons pink and cells of seven sides or more green, and the Crofton cell is a pale disc in gold corners. +// Knob 2: Speed. At 2 a line slides along its normal at up to 3 LEDs a second, turns at up to 0.05 radians a second and lives 10 s on average, so at the opening about 1.6 lines a second are born where the panel shows them and as many die. Everything goes as Speed / 2. +// Knob 3: Lines. The mean number of lines that cross the panel, 16 to 56: the panel shows about 52 cells at 16, 190 at 36 and 390 at 56. +// Knob 4: Big. No line may come nearer the fixed point than r = 20 Big^0.6 LEDs, which is the Poisson process conditioned on the zero cell holding that disc: 3.7 LEDs at the opening 0.06, 8.7 at 0.25, 13.2 at 0.5 and 20 at 1. With 56 lines the zero cell has 4.95 sides on average at 0, 5.9 at the opening and 8.5 at 1, and its incircle grows from 0.36 to 0.65 of the radius of its outer circle. // A line is (theta, p): the points x with x . (cos theta, sin theta) = p, // p >= 0, measured from the fixed point O in the middle of the panel. The @@ -41,7 +41,10 @@ // 4.93 sides on average where a typical cell has 4. Kendall's question // was what a large zero cell looks like, and the answer is a disc: with r // large against the mean spacing of the lines, many lines lie just past r -// and the cell is a many sided polygon hugging the circle. +// and the cell is a many sided polygon hugging the circle. It comes slowly: +// with l lines per LED of p per radian, the cell's corners stand out past +// the disc by about (l r)^(-2/3) of r, so on 64 LEDs the cell gets to 8 or +// 9 sides, rounder than its neighbours but still a polygon. // // Two LEDs are in the same cell when no line separates them, so each LED's // cell is the set of lines it lies beyond, hashed. A cell's polygon is the From 46353097cccb8bf4dfe5b2b1fa8b3025e6e9caa9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 23:06:52 +0000 Subject: [PATCH 08/12] =?UTF-8?q?=ED=8C=A8=ED=84=B4=2082=ED=8C=90:=20Canto?= =?UTF-8?q?r=20Scalogram=20=EA=B3=BC=20Letter=20Retracts?= 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_018svN6XCxZJqDPz16WwyFg6 --- .../pattern-study/patterns/letter-retracts.js | 397 ++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 incoming/pattern-study/patterns/letter-retracts.js diff --git a/incoming/pattern-study/patterns/letter-retracts.js b/incoming/pattern-study/patterns/letter-retracts.js new file mode 100644 index 00000000..5f68475a --- /dev/null +++ b/incoming/pattern-study/patterns/letter-retracts.js @@ -0,0 +1,397 @@ +// ===== Patternflow pattern ===== +// Title: Letter Retracts +// Family: type +// Idea: Three bold capitals stacked down the tall panel thin layer by layer to their skeletons without ever opening or closing a hole, then lose their free ends one LED at a time, so each letter shrinks onto its homotopy type, a point for X, T or Y, one loop round the counter for O, A or D and two loops for B, coloured by that hole count, and from those few LEDs the next word of the same classes grows. +// Author: patternflow-lab +// Date: 2026-10-31 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: deformation retract and homotopy type (Karol Borsuk, 1931); parallel thinning in two subiterations (T. Y. Zhang and C. Y. Suen, 1984), in the version of Zicheng Guo and Richard W. Hall (1989); simple points (Shigeru Yokoi, Jun-ichiro Toriwaki and Teruo Fukumura, 1975); capitals drawn for this pattern; pattern round 82 +// Round: 82 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Set=0..3, Weight=0..1 +// +// Knob 1: Hue. Turns the three class colours round the wheel together. At 0 a letter with no hole is green, with one hole sky blue and with two holes magenta; at 0.5 they are violet, red-orange and green. +// Knob 2: Speed. At 2 a letter loses one layer of thinning every 0.16 s and one LED off each free end every 0.1 s, so BOX retracts in 3.3 s and BAG in 6.6 s; every time goes as 2 / Speed. +// Knob 3: Set. Four lists of words whose letters keep the same hole counts, so each word grows out of the last one's points and loops: 0 BOX, BAT, BAG, BOY (two, one, none); 1 DOG, ROT, PAL, DOT (one, one, none); 2 KEY, LEG, YET, ZEN (none); 3 ROD, POD, PRO, ODD (one each). +// Knob 4: Weight. Stroke width, 7 LEDs at 0 to 11.5 at 1 (7.5 at the opening 0.06): the bolder the letter, the more layers it loses before its skeleton shows, 11 for B at the opening and 17 at 1. + +// Each letter is drawn as round-ended strokes in a 48 by 40 box and lit +// where an LED centre is within half the weight of a stroke. Then it is +// taken apart and every LED remembers the step at which it went: +// +// 1. Thinning, Guo and Hall's two subiterations, each one step, shown +// 1.6 times as long as a pruning step. A pixel goes when it has one +// run of neighbours, two or three neighbouring pairs and does not face +// the side the subiteration keeps; that peels the strokes from both +// sides down to one LED wide, keeps every hole and keeps every free +// end. +// 2. Pruning. At each step every pixel that is simple for 8-connected +// letters on 4-connected ground, Yokoi's connectivity number 1, is +// taken, rechecked as it goes. On a one-LED skeleton only the free +// ends are simple, so every branch loses one LED a step. A pixel on +// a loop is never simple, since taking it would join the hole to the +// ground; a single pixel is not either. +// +// What is left cannot shrink further without changing the topology: one +// pixel, one loop, or two loops sharing an arc. The number of holes, the +// 4-connected pieces of ground the letter closes off, is counted once for +// the colour and is the same at every step. + +const W = 64, H = 128; +const { floor, min, max, round, sqrt, sin, cos, abs, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// Glyph strokes in a unit box, v downwards: [x0, y0, x1, y1] segments and +// [cx, cy, rx, ry, from, to] elliptic arcs in degrees (0 right, 90 down). +// The first number of each entry is the glyph's width in the box. +const GLYPHS = { + A: [1, [0, 1, 0.5, 0], [0.5, 0, 1, 1], [0.15, 0.7, 0.85, 0.7]], + B: [0.9, [0, 0, 0, 1], [0, 0, 0.58, 0], [0.58, 0.235, 0.34, 0.235, -90, 90], [0.58, 0.47, 0, 0.47], [0, 0.47, 0.62, 0.47], [0.62, 0.735, 0.38, 0.265, -90, 90], [0.62, 1, 0, 1]], + D: [0.9, [0, 0, 0, 1], [0, 0, 0.42, 0], [0.42, 0.5, 0.58, 0.5, -90, 90], [0.42, 1, 0, 1]], + E: [0.78, [0, 0, 0, 1], [0, 0, 1, 0], [0, 0.5, 0.85, 0.5], [0, 1, 1, 1]], + G: [0.95, [0.5, 0.5, 0.5, 0.5, 25, 310], [0.52, 0.56, 1, 0.56], [1, 0.56, 0.953, 0.711]], + K: [0.88, [0, 0, 0, 1], [1, 0, 0, 0.64], [0.32, 0.435, 1, 1]], + L: [0.72, [0, 0, 0, 1], [0, 1, 1, 1]], + N: [0.9, [0, 1, 0, 0], [0, 0, 1, 1], [1, 1, 1, 0]], + O: [0.95, [0.5, 0.5, 0.5, 0.5, 0, 360]], + P: [0.85, [0, 0, 0, 1], [0, 0, 0.52, 0], [0.52, 0.28, 0.48, 0.28, -90, 90], [0.52, 0.56, 0, 0.56]], + R: [0.9, [0, 0, 0, 1], [0, 0, 0.5, 0], [0.5, 0.27, 0.46, 0.27, -90, 90], [0.5, 0.54, 0, 0.54], [0.42, 0.54, 1, 1]], + T: [0.95, [0, 0, 1, 0], [0.5, 0, 0.5, 1]], + X: [0.95, [0, 0, 1, 1], [1, 0, 0, 1]], + Y: [0.95, [0, 0, 0.5, 0.52], [1, 0, 0.5, 0.52], [0.5, 0.52, 0.5, 1]], + Z: [0.85, [0, 0, 1, 0], [1, 0, 0, 1], [0, 1, 1, 1]], +}; + +const SETS = [ + ["BOX", "BAT", "BAG", "BOY"], + ["DOG", "ROT", "PAL", "DOT"], + ["KEY", "LEG", "YET", "ZEN"], + ["ROD", "POD", "PRO", "ODD"], +]; + +// Letter boxes: 48 wide by 40 tall, three down the panel. +const BW = 48, BH = 40, BX = 3, BY = [2, 44, 86]; +// The sign of each letter's homotopy type stands at this column. +const SX = 57.5; +const KEEP = 32767; + +// Segments of a glyph in LED coordinates for a stroke radius r. +function segmentsOf(ch, r) { + const g = GLYPHS[ch], gw = g[0]; + const w = (BW - 2 * r) * gw, h = BH - 2 * r, ox = (BW - w) / 2, oy = r; + const P = (u, v) => [ox + u * w, oy + v * h]; + const out = []; + for (let i = 1; i < g.length; i++) { + const s = g[i]; + if (s.length === 4) out.push([...P(s[0], s[1]), ...P(s[2], s[3])]); + else { + const n = max(8, round(abs(s[5] - s[4]) / 7.5)); + let prev = null; + for (let j = 0; j <= n; j++) { + const t = ((s[4] + ((s[5] - s[4]) * j) / n) * PI) / 180; + const q = P(s[0] + s[2] * cos(t), s[1] + s[3] * sin(t)); + if (prev) out.push([...prev, ...q]); + prev = q; + } + } + } + return out; +} + +function rasterize(ch, r) { + const segs = segmentsOf(ch, r), img = new Uint8Array(BW * BH), r2 = r * r; + for (let y = 0; y < BH; y++) + for (let x = 0; x < BW; x++) { + const px = x + 0.5, py = y + 0.5; + for (const [x0, y0, x1, y1] of segs) { + const dx = x1 - x0, dy = y1 - y0, L = dx * dx + dy * dy; + const t = L > 0 ? cl(((px - x0) * dx + (py - y0) * dy) / L, 0, 1) : 0; + const ex = px - x0 - t * dx, ey = py - y0 - t * dy; + if (ex * ex + ey * ey <= r2) { + img[y * BW + x] = 1; + break; + } + } + } + return img; +} + +// Neighbours of (x, y) in the box, outside counts as ground. +const at = (img, x, y) => (x < 0 || y < 0 || x >= BW || y >= BH ? 0 : img[y * BW + x]); + +// Yokoi's 8-connectivity number: 1 for a simple pixel or a free end. +function simple(img, x, y) { + const n = [at(img, x + 1, y), at(img, x + 1, y - 1), at(img, x, y - 1), at(img, x - 1, y - 1), at(img, x - 1, y), at(img, x - 1, y + 1), at(img, x, y + 1), at(img, x + 1, y + 1)]; + let c = 0; + for (let k = 0; k < 8; k += 2) { + const a = 1 - n[k], b = 1 - n[k + 1], d = 1 - n[(k + 2) & 7]; + c += a - a * b * d; + } + return c === 1; +} + +// Pieces of 4-connected ground that do not reach the box edge. +function holes(img) { + const seen = new Uint8Array(BW * BH), stack = []; + let count = 0; + for (let s = 0; s < BW * BH; s++) { + if (img[s] || seen[s]) continue; + let edge = false; + stack.push(s); + seen[s] = 1; + while (stack.length) { + const p = stack.pop(), x = p % BW, y = (p / BW) | 0; + if (x === 0 || y === 0 || x === BW - 1 || y === BH - 1) edge = true; + for (const [nx, ny] of [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]) { + if (nx < 0 || ny < 0 || nx >= BW || ny >= BH) continue; + const q = ny * BW + nx; + if (!img[q] && !seen[q]) { + seen[q] = 1; + stack.push(q); + } + } + } + if (!edge) count++; + } + return count; +} + +// Takes a letter apart: the step each pixel goes at, KEEP for what stays. +function retract(ch, r) { + const img = rasterize(ch, r), N = BW * BH; + const gone = new Int16Array(N).fill(-1); + for (let p = 0; p < N; p++) if (img[p]) gone[p] = KEEP; + const kind = holes(img); + let step = 0; + const mark = []; + // Thinning, Guo and Hall. + for (let pass = 0, any = 0; pass < 200; pass++) { + const iter = pass & 1; + if (!iter) any = 0; + mark.length = 0; + for (let y = 0; y < BH; y++) + for (let x = 0; x < BW; x++) { + if (!img[y * BW + x]) continue; + const p2 = at(img, x, y - 1), p3 = at(img, x + 1, y - 1), p4 = at(img, x + 1, y), p5 = at(img, x + 1, y + 1); + const p6 = at(img, x, y + 1), p7 = at(img, x - 1, y + 1), p8 = at(img, x - 1, y), p9 = at(img, x - 1, y - 1); + const C = (!p2 & (p3 | p4)) + (!p4 & (p5 | p6)) + (!p6 & (p7 | p8)) + (!p8 & (p9 | p2)); + const N1 = (p9 | p2) + (p3 | p4) + (p5 | p6) + (p7 | p8), N2 = (p2 | p3) + (p4 | p5) + (p6 | p7) + (p8 | p9); + const n = min(N1, N2); + const m = iter === 0 ? (p6 | p7 | !p9) & p8 : (p2 | p3 | !p5) & p4; + if (C === 1 && n >= 2 && n <= 3 && m === 0) mark.push(y * BW + x); + } + if (!mark.length) { + if (iter === 1 && !any) break; + continue; + } + any = 1; + step++; + for (const p of mark) { + img[p] = 0; + gone[p] = step; + } + } + const thin = step; + // Pruning: every simple pixel of the step before goes, if still simple. + for (;;) { + mark.length = 0; + for (let p = 0; p < N; p++) if (img[p] && simple(img, p % BW, (p / BW) | 0)) mark.push(p); + if (!mark.length) break; + step++; + let took = 0; + for (const p of mark) + if (simple(img, p % BW, (p / BW) | 0)) { + img[p] = 0; + gone[p] = step; + took++; + } + if (!took) { + step--; + break; + } + } + let point = -1; + if (!kind) for (let p = 0; p < N; p++) if (gone[p] === KEEP) point = p; + return { gone, kind, thin, last: step, point, end: SLOW * thin + step - thin }; +} + +// A thinning layer takes SLOW times as long as a pruning step. +const SLOW = 1.6; +const stepAt = (L, tau) => (tau < SLOW * L.thin ? tau / SLOW : L.thin + tau - SLOW * L.thin); + +function wordOf(text, r) { + const letters = [...text].map((ch) => retract(ch, r)); + return { text, letters, last: max(...letters.map((l) => l.end)) }; +} + +const hsv = (h, s, v) => { + h = ((h % 1) + 1) % 1; + const i = floor(h * 6), f = h * 6 - i; + const 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 % 6]; +}; +// Class hues: no hole green, one hole sky blue, two holes magenta. +const HUES = [0.3, 0.56, 0.86]; + +// Phases of one word, in steps: grow from the points and loops, stand, +// retract, rest, then cross to the next word's points and loops. +const STAND = 10, REST = 12, CROSS = 6; + +export function setup(params) { + params.s = { set: -1, weight: -1, index: 0, word: null, next: null, phase: "stand", t: 0, clock: 0, hue: 0 }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + const speed = cl(k[1] == null ? 2 : k[1], 0.2, 3); + const set = round(cl(k[2] || 0, 0, 3)); + const r = round((3.5 + 2.25 * cl(k[3] == null ? 0.06 : k[3], 0, 1)) * 4) / 4; + const list = SETS[set]; + if (set !== S.set) { + S.set = set; + S.index = 0; + S.weight = r; + S.word = wordOf(list[0], r); + S.next = wordOf(list[1], r); + S.phase = "stand"; + S.t = 0; + } else if (r !== S.weight) { + S.weight = r; + S.word = wordOf(list[S.index], r); + S.next = wordOf(list[(S.index + 1) % list.length], r); + } + S.clock += d * speed; + let left = d * speed * 5; + // Walk the phases with the steps this frame has. + while (left > 0) { + const w = S.word; + if (S.phase === "grow") { + const go = min(left * 1.5, S.t); + S.t -= go; + left -= go / 1.5; + if (S.t <= 0) (S.phase = "stand"), (S.t = 0); + } else if (S.phase === "stand") { + const go = min(left, STAND - S.t); + S.t += go; + left -= go; + if (S.t >= STAND) (S.phase = "retract"), (S.t = 0); + } else if (S.phase === "retract") { + const go = min(left, w.last - S.t); + S.t += go; + left -= go; + if (S.t >= w.last) (S.phase = "rest"), (S.t = 0); + } else if (S.phase === "rest") { + const go = min(left, REST - S.t); + S.t += go; + left -= go; + if (S.t >= REST) (S.phase = "cross"), (S.t = 0); + } else { + const go = min(left, CROSS - S.t); + S.t += go; + left -= go; + if (S.t >= CROSS) { + S.index = (S.index + 1) % list.length; + S.word = S.next; + S.next = wordOf(list[(S.index + 1) % list.length], S.weight); + S.phase = "grow"; + S.t = S.word.last; + } + } + } +} + +// One word at retraction step tau, faded by f, into the frame. +function paint(frame, word, tau, f, S, glow) { + const pulse = 0.75 + 0.25 * sin(S.clock * 4); + for (let li = 0; li < 3; li++) { + const L = word.letters[li], hue = HUES[L.kind] + S.hue, oy = BY[li]; + const body = hsv(hue, 0.85, 1), ghost = hsv(hue, 0.45, 0.2), tip = hsv(hue, 0.25, 1); + const st = stepAt(L, tau), pruning = st > L.thin; + for (let y = 0; y < BH; y++) + for (let x = 0; x < BW; x++) { + const g = L.gone[y * BW + x]; + if (g < 0) continue; + let c, a; + if (g === KEEP) { + // What stays: brighter once nothing else is left. + const done = st >= L.last; + c = done ? hsv(hue, 0.6, glow ? pulse : 1) : body; + a = 1; + } else if (g > st + 1) { + c = body; + // A soft depth: pixels that go late are brighter. + a = 0.62 + (0.38 * min(g, L.thin + 1)) / (L.thin + 1); + } else if (g > st) { + // The layer or end that goes next, fading out. + c = pruning ? tip : body; + a = 0.2 + 0.8 * (g - st); + } else { + c = ghost; + a = 1; + } + const p = ((oy + y) * W + BX + x) * 3; + frame[p] += c[0] * a * f; + frame[p + 1] += c[1] * a * f; + frame[p + 2] += c[2] * a * f; + } + // Beside the letter, the sign of what it retracts to: a dot, a ring, or + // two rings that touch. Dim until the letter gets there. + { + const there = st >= L.last, c = hsv(hue, 0.55, 1), sy = oy + BH / 2; + const a0 = (there ? (glow ? pulse : 1) : 0.3) * f; + for (let y = -6; y <= 6; y++) + for (let x = -4; x <= 4; x++) { + const px = floor(SX) + x + 0.5 - SX, py = y; + let a = 0; + if (L.kind === 0) a = max(0, 1 - sqrt(px * px + py * py) / 1.6); + else if (L.kind === 1) a = max(0, 1 - abs(sqrt(px * px + py * py) - 3.2) / 0.7); + else { + const d1 = abs(sqrt(px * px + (py + 2.6) ** 2) - 2.6), d2 = abs(sqrt(px * px + (py - 2.6) ** 2) - 2.6); + a = max(0, 1 - min(d1, d2) / 0.7); + } + if (!a) continue; + const p = ((sy + y) * W + floor(SX) + x) * 3; + frame[p] += c[0] * a * a0; + frame[p + 1] += c[1] * a * a0; + frame[p + 2] += c[2] * a * a0; + } + } + // A point is one LED, so it gets a halo once it is all that is left. + if (L.point >= 0 && st >= L.last) { + const px = L.point % BW, py = (L.point / BW) | 0, c = hsv(hue, 0.5, 1); + for (let dy = -3; dy <= 3; dy++) + for (let dx = -3; dx <= 3; dx++) { + const d = sqrt(dx * dx + dy * dy); + if (!d || d > 3.2) continue; + const a = 0.5 * (1 - d / 3.4) * (glow ? pulse : 1) * f, p = ((oy + py + dy) * W + BX + px + dx) * 3; + frame[p] += c[0] * a; + frame[p + 1] += c[1] * a; + frame[p + 2] += c[2] * a; + } + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.word) return; + const frame = (S.frame = S.frame || new Float32Array(W * H * 3)); + frame.fill(0); + if (S.phase === "grow") paint(frame, S.word, S.t, 1, S, false); + else if (S.phase === "stand") paint(frame, S.word, 0, 1, S, false); + else if (S.phase === "retract") paint(frame, S.word, S.t, 1, S, false); + else if (S.phase === "rest") paint(frame, S.word, S.word.last, 1, S, true); + else { + const f = S.t / CROSS; + paint(frame, S.word, S.word.last, 1 - f, S, true); + paint(frame, S.next, S.next.last, f, S, true); + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = (y * W + x) * 3; + display.setPixel(x, y, round(min(1, frame[p]) * 255), round(min(1, frame[p + 1]) * 255), round(min(1, frame[p + 2]) * 255)); + } +} From 4d5e3c45d89fa382640972b4cc402fd05b4209ea Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 23:07:55 +0000 Subject: [PATCH 09/12] =?UTF-8?q?=ED=8C=A8=ED=84=B4=2082=ED=8C=90:=20Fork?= =?UTF-8?q?=20Hologram?= 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_018svN6XCxZJqDPz16WwyFg6 --- .../pattern-study/patterns/fork-hologram.js | 97 +++++++++++++------ 1 file changed, 67 insertions(+), 30 deletions(-) diff --git a/incoming/pattern-study/patterns/fork-hologram.js b/incoming/pattern-study/patterns/fork-hologram.js index 71356f8e..6869fce2 100644 --- a/incoming/pattern-study/patterns/fork-hologram.js +++ b/incoming/pattern-study/patterns/fork-hologram.js @@ -1,35 +1,56 @@ // ===== Patternflow pattern ===== // Title: Fork Hologram // Family: wave -// Idea: DRAFT +// Idea: The tall panel is the fork grating you record when a beam with phase singularities meets a tilted plane wave, fine near-vertical fringes that split into l + 1 tines wherever a vortex of charge l sits, and some ten vortices of charge 1 to 7 and both signs drift over it, pull their opposites in, annihilate or leave the difference, and are born again in pairs, so forks open, merge and vanish, the crowded side of each fork tinted blue and the thinned side green. // Author: patternflow-lab // Date: 2026-10-31 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: DRAFT; pattern round 82 +// Lineage: fork grating, the interferogram of a beam with a screw dislocation of charge l and a tilted plane wave (V. Yu. Bazhenov, M. V. Vasnetsov and M. S. Soskin, Laser beams with screw dislocations in their wavefronts, 1990), made as a binary computer-generated hologram (N. R. Heckenberg, R. McDuff, C. P. Smith and A. G. White, 1992); wave dislocations (John Nye and Michael Berry, 1974); pattern round 82 // Round: 82 // =============================== // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Charge=3..7, Carrier=0..1 // -// Knob 1: Hue. DRAFT -// Knob 2: Speed. DRAFT -// Knob 3: Charge. DRAFT -// Knob 4: Carrier. DRAFT +// Knob 1: Hue. Turns the colours round the wheel together. At 0 the plain grating is pale aqua, the side of a fork where the extra fringes crowd in is blue and the side where they thin out is green; at 0.3 the grating is lilac with red and blue. +// Knob 2: Speed. At 2 the vortices drift 0.5 to 4 LEDs a second, faster as an opposite closes in, new pairs are born one every 0.6 to 1.8 s while fewer than ten vortices are left, and the fringes slide sideways 0.12 of a period a second; every time goes as 2 / Speed. +// Knob 3: Charge. The highest charge a new pair carries: half the pairs carry it and the rest 1 up to it, so at the opening 3 the forks have 2, 3 or 4 tines and at 7 up to 8. A vortex of charge 2 or more sheds a unit vortex of its own sign, a charge 2 about every 12 s and a charge 3 every 6 s. +// Knob 4: Carrier. The fringe period, 3.3 LEDs at the opening 0.06, 19 fringes across the panel, doubling by 0.68 and 9.4 LEDs at 1, where every tine of a fork stands apart. The fringes always lean 14 degrees. + +// A beam whose phase winds by 2 pi l round a point, l times atan2 about it, +// meets a plane wave tilted by a carrier k. Where they add, the intensity +// follows the phase difference +// +// phi = 2 pi (k . r) + sum over vortices of l_j atan2(y - y_j, x - x_j), +// +// and the fringes are its level lines. Round a vortex phi gains 2 pi l, so +// l more fringes cross a circle on one side of it than on the other: the +// fork. The phases are multiplied as unit complex numbers and one atan2 +// per LED gives phi, so 10 vortices cost no more than one. The phase +// gradient comes alongside in closed form: where it is longer than the +// carrier the fringes crowd (blue at the opening), where shorter they thin +// (green). At each core the beam is dark, so the fringes fade to the mean +// within about sqrt(0.7 l) LEDs. +// +// Each fringe is drawn the way Heckenberg's binary holograms were cut, a +// hard step from dark to bright, with the grey interferogram left in at +// 35%: a triangle wave of phi runs 0 to 0.17 across a dark fringe and 0.83 +// to 1 across a bright one. The step keeps the fringes sharp; the ramp +// keeps every LED a little different from the LED above it. +// +// The vortices move overdamped: opposite charges pull together and like +// charges push apart, as 1 / r, with a slow random heading each. Opposite +// charges that meet add up, equal ones annihilate and unequal ones leave +// the difference. Total charge stays 0, since pairs are born as +l and -l. const W = 64, H = 128; const { floor, min, max, abs, sqrt, sin, cos, atan2, exp, pow, round, PI } = Math; const TAU = 2 * PI; const cl = (v, a, b) => (v < a ? a : v > b ? b : v); -// Pairs kept on the panel, the box the vortices may wander in (a margin -// past every edge), the carrier tilt, the gain that flattens the fringe tops. -const PAIRS = 5, MARGIN = 6, KY = 0.04, BEND = 0.6, P0 = 3.0; - -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]; -} +// Pairs kept on the panel, the margin past every edge the vortices may +// wander into, the carrier's tilt as ky / k, the grey share of a fringe, +// the period in LEDs at Carrier 0, the dark core's size per unit of charge. +const PAIRS = 5, MARGIN = 6, SLANT = 0.25, RAMP = 0.35, P0 = 3.09, CORE = 0.7; function vortex(x, y, l, vx, vy) { return { x, y, l, vx, vy, wx: 0, wy: 0, kick: 1, age: 0 }; @@ -142,34 +163,50 @@ export function draw(display, params) { const S = params.s; if (!S || !S.v) return; const V = S.v, n = V.length, f = S.f; - const ky = KY * f * 3.2, kx = sqrt(f * f - ky * ky), k0 = f; - const base = S.hue + 0.5; + const ky = SLANT * f, kx = sqrt(f * f - ky * ky); + const px = new Float64Array(n), py = new Float64Array(n), pl = new Int8Array(n); + for (let j = 0; j < n; j++) { + px[j] = V[j].x; + py[j] = V[j].y; + pl[j] = V[j].l; + } + const base = S.hue + 0.5, shift = S.slide; for (let y = 0; y < H; y++) { for (let x = 0; x < W; x++) { - // The vortex phases multiplied as unit complex numbers, one atan2 at the end. + // The vortex phases multiplied as unit complex numbers, one atan2 at the end; + // the phase gradient in cycles per LED alongside, for the colour. let re = 1, im = 0, gx = kx, gy = ky, c = 1; for (let j = 0; j < n; j++) { - const p = V[j], dx = x - p.x, dy = y - p.y, r2 = dx * dx + dy * dy + 1e-6, ir = 1 / sqrt(r2); - const ux = dx * ir, uy = (p.l > 0 ? dy : -dy) * ir; - for (let m = abs(p.l); m > 0; m--) { + const l = pl[j], dx = x - px[j], dy = y - py[j], r2 = dx * dx + dy * dy + 1e-6, ir = 1 / sqrt(r2); + const ux = dx * ir, uy = (l > 0 ? dy : -dy) * ir; + for (let m = l > 0 ? l : -l; m > 0; m--) { const t = re * ux - im * uy; im = re * uy + im * ux; re = t; } - const w = p.l / (TAU * r2); + const w = l / (TAU * r2); gx -= dy * w; gy += dx * w; - const r0 = 1.1 * abs(p.l); - c *= r2 / (r2 + r0); + c *= r2 / (r2 + CORE * (l > 0 ? l : -l)); } - let u = kx * x + ky * y + S.slide + atan2(im, re) / TAU; + let u = kx * x + ky * y + shift + atan2(im, re) / TAU; u -= floor(u); const tri = 1 - abs(2 * u - 1); - const v = 0.5 + (tri - BEND * sin(TAU * tri) / TAU - 0.5) * c; - const g = sqrt(gx * gx + gy * gy) / k0 - 1; - const hue = base + 0.14 * (g / (1 + abs(g))); - const rgb = hsv(hue, 0.55 - 0.2 * (1 - c), v); - display.setPixel(x, y, round(rgb[0] * 255), round(rgb[1] * 255), round(rgb[2] * 255)); + const v = 0.5 + (RAMP * tri + (tri > 0.5 ? 1 - RAMP : 0) - 0.5) * c; + // Hue and saturation from how crowded the fringes are against the carrier. + const g = sqrt(gx * gx + gy * gy) / f - 1, ag = min(1, 4 * abs(g)); + let h = base + (g > 0 ? 0.17 : -0.13) * ag; + const s = (0.3 + 0.5 * ag) * c; + h = (h - floor(h)) * 6; + const i = floor(h), q = h - i, lo = v * (1 - s), dn = v * (1 - s * q), up = v * (1 - s * (1 - q)); + let r, gg, b; + if (i === 0) { r = v; gg = up; b = lo; } + else if (i === 1) { r = dn; gg = v; b = lo; } + else if (i === 2) { r = lo; gg = v; b = up; } + else if (i === 3) { r = lo; gg = dn; b = v; } + else if (i === 4) { r = up; gg = lo; b = v; } + else { r = v; gg = lo; b = dn; } + display.setPixel(x, y, round(r * 255), round(gg * 255), round(b * 255)); } } } From 4835d026ea61f2f26260ebb5981fa26c4888b51e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 23:11:53 +0000 Subject: [PATCH 10/12] =?UTF-8?q?=ED=8C=A8=ED=84=B4=2082=ED=8C=90:=20Fork?= =?UTF-8?q?=20Hologram=20=EA=B3=BC=20Lensed=20Page?= 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_018svN6XCxZJqDPz16WwyFg6 --- .../pattern-study/patterns/lensed-page.js | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 incoming/pattern-study/patterns/lensed-page.js diff --git a/incoming/pattern-study/patterns/lensed-page.js b/incoming/pattern-study/patterns/lensed-page.js new file mode 100644 index 00000000..9b40f9ba --- /dev/null +++ b/incoming/pattern-study/patterns/lensed-page.js @@ -0,0 +1,185 @@ +// ===== Patternflow pattern ===== +// Title: Lensed Page +// Family: type +// Idea: A tall column of Newton's first Query from Opticks, set in 3 by 5 capitals, climbs past an invisible point mass in the middle of the panel, and every LED reads the page through the point-lens equation, so the lines bow into arcs round the mass, letters near it show twice, once outside the Einstein ring and once turned over inside it, and the letters right behind it spread into the ring itself. +// Author: patternflow-lab +// Date: 2026-10-31 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the point-mass gravitational lens and its ring (Orest Chwolson, 1924; Albert Einstein, 1936); text from Isaac Newton, Opticks (1704), Query 1, public domain; 3 by 5 capitals drawn for this pattern; pattern round 82 +// Round: 82 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Mass=13..29, Drift=0..1 +// +// Knob 1: Hue. Turns the colours round the wheel together. At 0 the ink is pale sky blue on dark blue paper, the turned-over images inside the ring are lilac, and the ink pales to white across the ring; at 0.5 the ink is peach and the inner images green. +// Knob 2: Speed. At 2 the page climbs 5 rows a second, a line every 1.4 s and the whole 98-row page, the query and the signature, in 19.6 s, and a Drift swing takes 10 s; every time goes as 2 / Speed. +// Knob 3: Mass. The Einstein radius in LEDs, 13 at the opening and 29 at the top, where the ring is 58 LEDs across. A letter t LEDs off the line of sight shows twice, outside the ring at (t + sqrt(t^2 + 4 E^2)) / 2 and turned over inside it at (sqrt(t^2 + 4 E^2) - t) / 2 on the far side; a line 40 LEDs from the mass is pushed 4.2 LEDs outward at 13 and 21 at 29. +// Knob 4: Drift. Swings the mass sideways across the column, 24 LEDs times Drift each way: 1.4 at the opening 0.06, so the ring stays in the middle, and 24 at 1, where the ring runs off the side of the panel and the dark beyond the page's edge shows inside it, turned over, as a black disc. + +// Put the mass at the origin of the sky and the page far behind it. Light +// that reaches the LED at angle theta from the mass passed the page at +// +// beta = theta - thetaE^2 theta / |theta|^2, +// +// the point-lens equation (Chwolson 1924, Einstein 1936). So each LED +// looks up the page at beta, and nothing is scattered or added up. A page +// point at distance b from the line of sight has two images, both on the +// line through the mass, at (b + sqrt(b^2 + 4 thetaE^2)) / 2 outside the +// Einstein ring and (b - sqrt(b^2 + 4 thetaE^2)) / 2 inside it, on the +// other side and turned over; the point right behind the mass is the whole +// ring. Lensing keeps surface brightness, so the LED takes the page's +// level as it is, bilinear between page LEDs, and a magnified letter is +// bigger, never brighter. Inside the ring the page is shrunk: an LED at r +// from the mass covers 1 + thetaE^2 / r^2 page LEDs radially, so it reads +// a box-blurred copy of the page that wide instead of aliasing. +// +// The page is one LED to the page LED far from the mass: 16 characters of +// 4 LEDs to a line, lines 7 rows apart, centred, the query wrapped by words +// and the signature after it, then the same page again below. Past the +// column's two sides there is no page, and that dark is what shows in the +// middle of the ring, where the inner image reaches far out sideways. + +const W = 64, H = 128; +const { floor, abs, sin, log2, round, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// Isaac Newton, Opticks (1704), the first of the Queries. +const TEXT = "QU. 1. DO NOT BODIES ACT UPON LIGHT AT A DISTANCE, AND BY THEIR ACTION BEND ITS RAYS; AND IS NOT THIS ACTION (CAETERIS PARIBUS) STRONGEST AT THE LEAST DISTANCE?"; +const SIGN = "NEWTON 1704"; + +// Capitals, figures and stops, 3 wide and 5 tall, rows top to bottom. +const GLYPHS = { + A: "010101111101101", B: "110101110101110", C: "011100100100011", D: "110101101101110", + E: "111100110100111", F: "111100110100100", G: "011100101101011", H: "101101111101101", + I: "111010010010111", J: "001001001101010", K: "101101110101101", L: "100100100100111", + M: "101111111101101", N: "110101101101101", O: "010101101101010", P: "110101110100100", + Q: "010101101110011", R: "110101110101101", S: "011100010001110", T: "111010010010010", + U: "101101101101111", V: "101101101101010", W: "101101111111101", X: "101101010101101", + Y: "101101010010010", Z: "111001010100111", + 0: "111101101101111", 1: "010110010010111", 4: "101101111001001", 7: "111001010010010", + ".": "000000000000010", ",": "000000000010100", ";": "000010000010100", "?": "110001010000010", + "(": "001010010010001", ")": "100010010010100", " ": "000000000000000", +}; + +// Characters per line, LEDs per character and per line, page ground level. +const COLS = 16, ADV = 4, PITCH = 7, GROUND = 0.09, LEVELS = 5; + +function wrap(text) { + const lines = []; + let line = ""; + for (const word of text.split(" ")) { + if (line && line.length + 1 + word.length > COLS) { + lines.push(line); + line = word; + } else line = line ? line + " " + word : word; + } + if (line) lines.push(line); + return lines; +} + +// The page: one copy of the query and the signature, PITCH rows a line, +// 64 wide, repeating down. Level 1 is ink, GROUND the paper. +function page() { + const lines = [...wrap(TEXT), "", SIGN, ""]; + const ph = lines.length * PITCH, p = new Float32Array(W * ph).fill(GROUND); + lines.forEach((text, li) => { + const x0 = floor((W - (ADV * text.length - 1)) / 2); + for (let c = 0; c < text.length; c++) { + const g = GLYPHS[text[c]] || GLYPHS[" "]; + for (let r = 0; r < 5; r++) + for (let q = 0; q < 3; q++) if (g[r * 3 + q] === "1") p[(li * PITCH + 1 + r) * W + x0 + c * ADV + q] = 1; + } + }); + return { p, ph }; +} + +// Box-blurred copies of the page, 2^L LEDs wide, for LEDs that see a wide +// patch of it (the demagnified image inside the ring). +function mips(p, ph) { + const out = [p]; + for (let L = 1; L < LEVELS; L++) { + const s = 1 << L, a = new Float32Array(W * ph), b = new Float32Array(W * ph); + for (let y = 0; y < ph; y++) + for (let x = 0; x < W; x++) { + let t = 0; + for (let i = 0; i < s; i++) { + const xx = x + i - (s >> 1); + t += xx >= 0 && xx < W ? p[y * W + xx] : 0; + } + a[y * W + x] = t / s; + } + for (let y = 0; y < ph; y++) + for (let x = 0; x < W; x++) { + let t = 0; + for (let i = 0; i < s; i++) t += a[(((y + i - (s >> 1)) % ph) + ph) % ph * W + x]; + b[y * W + x] = t / s; + } + out.push(b); + } + return out; +} + +// Bilinear sample: x off the page is dark, y wraps. +function sample(p, ph, x, y) { + const x0 = floor(x), y0 = floor(y), fx = x - x0, fy = y - y0; + const ya = ((y0 % ph) + ph) % ph, yb = ya + 1 === ph ? 0 : ya + 1; + const a0 = x0 >= 0 && x0 < W ? p[ya * W + x0] : 0, a1 = x0 + 1 >= 0 && x0 + 1 < W ? p[ya * W + x0 + 1] : 0; + const b0 = x0 >= 0 && x0 < W ? p[yb * W + x0] : 0, b1 = x0 + 1 >= 0 && x0 + 1 < W ? p[yb * W + x0 + 1] : 0; + return (a0 + (a1 - a0) * fx) * (1 - fy) + (b0 + (b1 - b0) * fx) * fy; +} + +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) { + const { p, ph } = page(); + params.s = { mip: mips(p, ph), ph, scroll: 20, swing: 0, hue: 0, mass: 13, drift: 0.06 }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 13, 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.mass = cl(k[2] == null ? 13 : k[2], 13, 29); + S.drift = cl(k[3] == null ? 0.06 : k[3], 0, 1); + S.scroll = (S.scroll + d * speed * 2.5) % S.ph; + S.swing = (S.swing + (d * speed * TAU) / 20) % TAU; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { mip, ph } = S, e2 = S.mass * S.mass; + const lx = W / 2 + S.drift * 24 * sin(S.swing), ly = H / 2; + // Ink is the page colour outside the ring and turned inside it, paling to + // white at the ring; the paper keeps the page colour everywhere. + const out = hsv(S.hue + 0.56, 0.5, 1), inn = hsv(S.hue + 0.86, 0.5, 1), paper = hsv(S.hue + 0.56, 0.65, 1); + const rim = [], inner = []; + for (let i = 0; i <= 10; i++) { + rim.push(hsv(S.hue + 0.56, 0.05 * i, 1)); + inner.push(hsv(S.hue + 0.86, 0.05 * i, 1)); + } + for (let y = 0; y < H; y++) { + for (let x = 0; x < W; x++) { + // Point lens: the LED at theta from the mass sees the page at + // beta = theta (1 - thetaE^2 / |theta|^2). + const tx = x - lx, ty = y - ly, r2 = tx * tx + ty * ty + 1e-6, k = 1 - e2 / r2; + const sx = lx + tx * k, sy = ly + ty * k + S.scroll; + // How many LEDs of page the LED spans radially: 1 + thetaE^2 / r^2. + const span = 1 + e2 / r2, lev = cl(log2(span) - 1, 0, LEVELS - 1.001), L0 = floor(lev), fl = lev - L0; + let v = sample(mip[L0], ph, sx, sy); + if (fl > 0) v += (sample(mip[L0 + 1], ph, sx, sy) - v) * fl; + // Outer images keep the page colour, the inverted inner images turn, + // and the ring between them goes white. + const t = (r2 - e2) / (r2 + e2), ink = cl((v - GROUND) / (1 - GROUND), 0, 1); + const c = t > 0 ? (abs(t) < 0.2 ? rim[round(abs(t) * 50)] : out) : abs(t) < 0.2 ? inner[round(abs(t) * 50)] : inn; + const s = 255 * v; + display.setPixel(x, y, round((paper[0] + (c[0] - paper[0]) * ink) * s), round((paper[1] + (c[1] - paper[1]) * ink) * s), round((paper[2] + (c[2] - paper[2]) * ink) * s)); + } + } +} From b4eabdae890761eda774b9616bc0df74ec04ec30 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 23:18:09 +0000 Subject: [PATCH 11/12] =?UTF-8?q?=ED=8C=A8=ED=84=B4=2082=ED=8C=90:=20READM?= =?UTF-8?q?E=20=EC=97=90=20=ED=91=9C=EC=99=80=20=ED=8C=90=20=EC=A0=88,=20?= =?UTF-8?q?=EB=8B=A4=EC=9D=8C=20=ED=8C=90=20=EA=B0=90=EC=9D=84=20=EB=8D=94?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018svN6XCxZJqDPz16WwyFg6 --- incoming/pattern-study/README.md | 101 ++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 3 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 8e11811c..5bedfc36 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 718개를 쉰여섯 갈래로 나눠 만들었어요. 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판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 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 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. 패널은 세로로 서요. 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판에서 세로로 다시 짰어요. @@ -1125,6 +1125,21 @@ | [Malfatti Circles](patterns/malfatti-circles.js) | 기하·대칭 | 천천히 모양을 바꾸는 삼각형 하나가 위아래로 두 번 서서, 위의 말파티 세 원보다 아래의 탐욕 세 원이 늘 더 넓게 덮어요. | | [Pollard's Rho](patterns/pollards-rho.js) | 알고리즘 | x² + c 를 숨은 소인수로 나눈 나머지가 세로로 선 ρ 를 그리고, 그 위를 쫓는 거북이와 토끼가 만나면 gcd 가 1 에서 그 소인수로 뛰어요. | +### 82판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Letter Retracts](patterns/letter-retracts.js) | 글자 | 세로로 쌓은 굵은 대문자 셋이 구멍을 열지도 닫지도 않고 한 겹씩 뼈대로 가늘어진 뒤 끝을 한 LED 씩 잃어, 구멍 없는 글자는 한 점, 구멍 하나는 고리, 구멍 둘은 고리 둘로 줄고, 그 점과 고리에서 구멍 수가 같은 다음 낱말이 자라요. | +| [Alphabet Lineage](patterns/alphabet-lineage.js) | 글자 | 사람 머리, 눈, 물, 소머리의 원시 시나이 그림 넷이 패널 아래로 쌓여 페니키아 글자로 휘고, 카드처럼 뒤집혀 오른쪽을 보는 그리스 글자가 된 뒤 굵고 가는 획과 세리프를 단 로마 대문자 ROMA 로 서요. | +| [Word Search](patterns/word-search.js) | 글자 | 10×16 칸 대문자 격자에 숨은 타이포 낱말 열둘을 흰 커서가 칸마다 여덟 방향으로 더듬어 찾고, 찾은 낱말마다 제 색 캡슐이 둘러져 남다가 다 찾으면 격자가 흩어지고 새 판이 찍혀요. | +| [Lensed Page](patterns/lensed-page.js) | 글자 | 뉴턴 『광학』의 첫 물음이 3×5 대문자로 패널을 타고 올라가며 가운데의 보이지 않는 점 질량을 지나고, 줄은 그 둘레로 휘며, 가까운 글자는 아인슈타인 고리 밖과 안에 두 번 서고 바로 뒤의 글자는 고리로 번져요. | +| [Noordzij Cube](patterns/noordzij-cube.js) | 글자 | 펜이 nano, bone, ego 를 한 획씩 쓰고, 노브 둘이 넓은 펜촉의 병진과 뾰족한 펜의 팽창 사이와 대비의 양을 옮겨서 같은 뼈대의 모든 획 굵기를 다시 칠해요. | +| [Fork Hologram](patterns/fork-hologram.js) | 물결·간섭 | 위상이 감긴 소용돌이 열 개쯤이 떠다니는 빛과 비스듬한 평면파가 만든 가는 세로 줄무늬에서, 소용돌이마다 줄이 전하보다 하나 많은 갈래로 갈라지고, 반대 전하끼리 만나면 갈래가 합쳐지거나 사라졌다가 새 쌍으로 다시 생겨요. | +| [Cantor Scalogram](patterns/cantor-scalogram.js) | 데이터 그림 | 칸토어 계단의 웨이블릿 변환을 세로로 자리, 가로로 로그 크기에 펼치면 계수 크기가 칸토어 집합처럼 둘씩 갈라지는 나무가 되고, 위상을 색으로 칠해 가는 가지일수록 LED 마다 색이 바뀌며, 6초마다 3배 당겨 들어가 같은 그림으로 돌아와요. | +| [Exclusion Process](patterns/exclusion-process.js) | 입자 | 64칸 줄에 왼쪽으로 들어온 입자가 앞 칸이 빌 때만 오른쪽으로 한 칸씩 뛰는 것을 가장 새 줄이 위에 오는 시공간 그림으로 쌓아서 입자마다 제 색 실이 남고, 처음 노브에서는 성긴 흐름과 막힌 줄 사이의 경계가 이리저리 떠돌아요. | +| [Crofton Cell](patterns/crofton-cell.js) | 보로노이·셀 | 판을 가로지르는 푸아송 직선들이 움직이며 판을 볼록한 칸으로 자르고, 가운데 점을 품은 칸만 금빛 모서리 안의 옅은 내접원으로 빛나서 Big 을 올리면 그 칸이 커지며 변이 늘고 원에 가까워져요. | +| [Phasor Noise](patterns/phasor-noise.js) | 노이즈·흐름 | 같은 가보르 커널을 더한 복소 장을 위 반에는 허수부로, 아래 반에는 위상만으로 그려서, 소용돌이와 안장을 따라 지문처럼 휘는 줄무늬가 위에서는 군데군데 흐려지고 아래에서는 같은 대비로 갈라지며 흘러요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -4767,6 +4782,70 @@ Lab 에서 해 보기: 없음. 세 글자가 128행을 다 쓰고 가로로도 6 주제 5 「수학 · 추상」으로 알고리즘 갈래에 선 패턴이에요. 존 폴라드(John M. Pollard)가 1975년에 낸 로 방법(rho method)은 x 를 x² + c 로 거듭 보내며 n 으로 나눈 나머지를 셈해요. n 의 숨은 소인수 p 로 나눈 나머지는 값이 p 개뿐이라 언젠가 본 값으로 돌아오고, 그 뒤로는 같은 고리만 돌아요. 꼬리 하나에 고리 하나라서 그리스 글자 ρ 꼴이에요. 패널은 그 ρ 를 세워 x₀ = 2 에서 시작한 꼬리를 아래에서 올리고 고리를 위에 두며, 구슬마다 x_i mod p 를 적어요. 알고리즘은 ρ 를 보지 못하고 n 으로 나눈 값만 봐요. 로버트 플로이드(Robert W. Floyd)의 것으로 알려진 고리 찾기대로 거북이는 한 박자에 한 칸, 토끼는 두 칸 가고, 꼬리 옆 글줄에는 두 값과 그 차이가 n 으로 나눈 값으로 떠요. 고리 가운데의 gcd(|x_t − x_h|, n) 는 1 로 있다가 둘이 같은 구슬에 서는 박자에 p 로 뛰고, 고리가 번쩍인 뒤 아래에 =83×71 처럼 n 의 두 인수가 적혀요. 그다음 같은 수를 리처드 브렌트(Richard P. Brent)가 1980년에 낸 방식으로 한 번 더 풀어요. 토끼가 한 박자에 한 칸 가고 거북이는 1, 2, 4, 8 걸음마다 토끼 자리로 건너뛰어서 f 를 한 박자에 한 번만 셈해요. 시간을 f 를 셈한 수로 흘려서, 두 방식이 같은 수를 두고 같은 잣대로 겨뤄요. 잰 값은 움직임 0.180, 촘촘함 0.087, 가장자리 0.777, 복잡도 0.333, 채움 0.193, 색 0.193, 모양 바뀜 0.086 이고, thirds 는 22/19/15, 노브는 0.05, 0.07, 0.08, 0.06 이에요. 첫 판도 이 값 그대로 검사를 지났고, 고친 것은 실패하는 순간 하나예요. c = 1 의 4307 = 59 × 73 은 두 소수의 고리가 같은 박자에 닫혀서 gcd 가 n 자체인 4307 이 되는데, 네 자리 수가 만난 구슬을 두른 토끼 고리에 닿아 4 가 반쯤 묻혔어요. 그 수에만 한 LED 둘레의 어두운 바탕을 깔았어요. 패널에 세워 보고 안 것은 c 하나가 어느 인수를 먼저 잡을지 바꾼다는 거예요. 5893 은 c = 1 에서 83 을 법으로 한 꼬리 4, 고리 5 의 ρ 가 먼저 닫혀 83 이 나오는데, c = 5 에서는 71 을 법으로 한 꼬리 2, 고리 6 의 ρ 가 먼저 닫혀서 71 이 나와요. +## 82판 + +82판을 띄울 때 같이 도는 81판(#345)이 열려 있었어요. 앞 바퀴 PR 에는 OWNER, MEMBER, COLLABORATOR 가 쓴 글 가운데 일꾼 글이 아닌 것이 없어서 따를 글이 없었어요. 주제와 칸, 갈래는 81판이 PR 에 먼저 밝힌 것을 비켜 골랐어요. 타이포 다섯은 「타이포 실험」의 출발점을 하나씩 나눠 맡았어요. Letter Retracts 는 글자와 빛 가운데 카운터, Alphabet Lineage 는 글자가 만들어지는 과정, Word Search 는 글자로 짠 무늬, Lensed Page 는 세로 패널다운 글, Noordzij Cube 는 노브와 글이에요. 다섯 다 글자 꼴을 코드에 적었고, 옮겨 쓴 글꼴은 없어요. 수학 다섯은 80판 뒤 스물다섯으로 가장 적던 열두 갈래 가운데 81판이 고른 수론, 물리·스프링, 위상·매듭을 뺀 아홉에서 물결·간섭, 데이터 그림, 입자, 보로노이·셀, 노이즈·흐름에 하나씩 섰어요. 「낯선 수학 예」와 「다음 판 감」에서는 고르지 않았어요. 열 개는 다섯 갈래에 둘씩 나눠 따로 짓고 합쳤어요. + +### Letter Retracts + +주제 7 「타이포」의 「글자와 빛」 가운데 카운터, 곧 글자의 구멍이 주인공인 실험이에요. 카롤 보르수크(Karol Borsuk)가 1931년에 다진 수축(retract)으로 보면, 글자를 구멍을 열지도 닫지도 않고 줄일 수 있는 데까지 줄였을 때 그 글자의 호모토피 꼴만 남아요. 구멍 없는 C, T, X 는 한 점, A, D, O, P, R 은 카운터를 두르는 고리 하나, B 와 8 은 고리 둘이에요. 알파벳을 구멍 수로 가르는 위상수학의 오랜 농담이 패널에서 그대로 보여요. 64판의 Counterforms 는 카운터를 안에서 켠 창으로 빛나게 했고, 여기서는 글자를 끝까지 줄여도 카운터의 수가 남는다는 것이 주인공이에요. 패턴은 48×40 상자에 끝이 둥근 획으로 직접 그린 대문자 셋을 세로로 쌓고, 두 단계로 떼어 내요. 먼저 궈쯔청(Zicheng Guo)과 리처드 홀(Richard W. Hall)이 1989년에 낸 두 부반복 세선화로 획을 양쪽에서 한 겹씩 벗겨 한 LED 너비의 뼈대로 만들어요. T. Y. 장(T. Y. Zhang)과 C. Y. 순(C. Y. Suen)의 1984년 방법을 고친 것이에요. 그다음 요코이 시게루(Shigeru Yokoi), 도리와키 준이치로(Jun-ichiro Toriwaki), 후쿠무라 데루오(Teruo Fukumura)가 1975년에 낸 연결수가 1 인 단순한 점만 한 걸음에 한 번씩 떼요. 한 LED 너비의 뼈대에서 단순한 점은 끝뿐이라 가지마다 한 걸음에 한 LED 씩 짧아지고, 고리 위의 점은 떼면 구멍이 바깥과 이어지니 남아요. LED 마다 떨어진 걸음을 기억해서, 거꾸로 돌리면 남은 점과 고리에서 글자가 다시 자라요. 글자는 구멍 수로 칠해요. 없으면 초록, 하나면 하늘색, 둘이면 자홍이에요. 글자 오른쪽 57열 언저리에 남을 꼴의 표지(점, 고리, 맞닿은 고리 둘)가 흐리게 서 있다가 글자가 거기 닿으면 밝아져요. 처음 낱말은 BOX 예요. B 와 O 는 11겹을 벗으면 뼈대가 곧 고리라 거기서 멈추고, X 는 9겹 뒤 네 팔이 한 LED 씩 줄어 가운데 한 LED 로 모여요. Speed 2 에서 한 겹은 0.16초, 끝 한 LED 는 0.1초라 BOX 는 3.3초에 다 줄어요. 다음 낱말로는 남은 점과 고리가 0.6초 동안 다음 낱말의 점과 고리로 바뀐 뒤 거기서 자라요. K3 Set 의 네 목록은 낱말끼리 자리마다 구멍 수가 같아서(BOX, BAT, BAG, BOY 는 둘, 하나, 없음), 고리는 고리로, 점은 점으로 넘어가요. 같은 부류의 글자는 남은 꼴에서 서로 바꿔 자랄 수 있다는 것이 보여요. K4 Weight 는 획을 7.5 LED 에서 11.5 LED 까지 굵혀서, B 가 뼈대까지 벗는 겹이 11 에서 17 로 늘어요. 잰 값은 움직임 0.038, 촘촘함 0.045, 가장자리 0.904, 복잡도 0.100, 채움 0.342, 색 0.131, 모양 바뀜 0.090 이고, thirds 는 44/30/27, 노브는 0.06, 0.11, 0.09, 0.08 이에요. 겨눈 칸은 없어요. 열다섯 글자를 획 반지름 0.25 간격으로 떼어 남은 꼴의 구멍과 조각 수를 처음 글자와 견주는 검사를 붙였더니, 첫 판의 40×38 상자에서는 반지름 5.5 에서 A 의 카운터가 막혀 점으로 줄었고 5.25 부터 G 의 입이 닫혀 고리가 됐어요. A 의 가로획을 높이의 0.66 에서 0.70 으로 내리고 G 의 호를 320도에서 310도로 줄였어요. 상자를 48×40 으로 키운 뒤에는 반지름 6 까지 다 맞고 6.25 에서 A, B, G 가 틀려서, Weight 는 5.75 에서 멈췄어요. 첫 그림은 획이 6 LED 라 윤곽 글꼴처럼 가늘었고, 구멍 없는 부류의 금색은 흐린 잔상에서 갈색으로 보였어요. 가장 가는 획을 7 LED 로 올리고 금색을 초록으로 바꿨어요. 한 LED 짜리 점은 패널에서 거의 보이지 않아서 반지름 3 LED 의 무리를 둘렀어요. 표지를 붙이기 전 줄은 움직임 0.035, 복잡도 0.077, 채움 0.334 였어요. + +Lab 에서 해 보기: 없음. 세 글자가 3~51열과 2~126줄을 쓰고 오른쪽 열에 남을 꼴의 표지가 서서, Text 레이어 글자를 얹을 빈 자리는 왼쪽 0~4열 다섯 줄 너비뿐이에요. + +기능 감: 코드 패턴이 같은 프로젝트의 Text 레이어 비트맵을 읽는 길이요. 세선화는 어느 비트맵에나 돌아가는데 글자를 코드 안에 둬야 해서, 대문자 열다섯을 획 목록으로 손수 그리고 굵기마다 구멍 수를 따로 검사했어요. Text 레이어의 Inter 나 Silkscreen 글자를 받을 수 있으면 소문자 a, e, g 의 구멍도 바로 셀 수 있어요. + +### Alphabet Lineage + +주제 7 「타이포」의 「글자가 만들어지는 과정」을 겨눈 패턴이에요. 라틴 낱말 네 글자를 32줄씩 세로로 쌓아요. 글자마다 원시 시나이 문자(기원전 1850년쯤, 와디 엘홀과 세라비트 엘카딤), 페니키아 문자(기원전 1050년쯤), 옛 그리스 문자(기원전 750년쯤), 로마 비문 대문자(1세기)를 지나요. 앨런 가디너(Alan Gardiner)가 1916년에 세라비트 엘카딤의 새김글에서 바알라트(Baalat)를 읽어 내며 이 문자가 이집트 그림에서 왔다고 봤어요. 글자는 10×10 칸에 획 꺾은선으로 적었어요. 시대마다 같은 획을 호의 길이로 36점씩 다시 뽑아서, 한 시대의 획이 다음 시대의 획으로 점마다 휘어요. ʾalp 소머리는 주둥이가 아래를 향하고, 시계 방향으로 90도 누워 페니키아 aleph 가 된 뒤 다시 90도 돌아 거꾸로 선 소머리인 alpha 가 돼요. 뿔은 aleph 를 가로지르는 획을 거쳐 A 의 가로획이 돼요. 그리스가 왼쪽에서 오른쪽으로 쓰기로 굳으면서 방향이 있는 글자는 돌아섰어요. beth, mem, nun, resh 는 왼쪽을 보고 beta, mu, nu, rho 는 오른쪽을 봐요. 그래서 페니키아에서 그리스로 휠 때 A 말고는 모든 글자가 세로축을 두고 카드처럼 뒤집히고, 오른쪽 가장자리의 작은 화살도 같이 뒤집혀 쓰는 방향을 보여 줘요. delta 는 로마로 가며 옆으로 누워 둥글어져 D 가 되고, R 은 P 와 갈리려고 다리를 얻어요. 마지막 1.8초 동안 30도로 쥔 넓은 붓의 굵고 가는 획(에드워드 카티치 Edward Catich, 『The Origin of the Serif』, 1968)과 발치의 세리프가 들어와요. Speed 2 에서 시대마다 1.2~2.6초 머물고 1.8초 동안 휘며, 1.4초에 처음으로 되감겨서 한 바퀴가 13.4초예요. K3 Word 는 ROMA, AMOR, ARMA, DONA, BONA, MORA 이고, 여섯 낱말이 여기 그린 A, B, D, M, N, O, R 일곱 글자를 다 써요. K4 Lag 는 글자마다 위 글자보다 몇 시대 늦게 가는지예요. 처음 자리 0.06 에서는 낱말이 거의 한꺼번에 바뀌고, 1 에서는 맨 위가 로마 대문자일 때 맨 아래가 아직 그림이에요. 왼쪽 레일의 점이 글자마다 지금 시대를 짚어요. 잰 값은 움직임 0.157, 촘촘함 0.045, 가장자리 0.219, 복잡도 0.765, 채움 0.264, 색 0.155, 모양 바뀜 0.093 이고, thirds 는 24/30/24, 노브는 0.04, 0.11, 0.08, 0.06 이에요. 첫 판은 움직임 0.127, 채움 0.173, 색 0.120, thirds 15/20/15, 노브 0.03, 0.08, 0.07, 0.04 였어요. 글자 폭이 27 LED 이고 펜 둘레의 빛이 2 LED 뿐이라 K1 Hue 가 0.03 에 머물렀어요. 칸을 2.7 LED 에서 3 LED 로 키우고 펜 둘레 빛을 3 LED 로 넓히고, 네 시대 색의 채도를 0.06~0.08 올리자 Hue 가 0.04(따로 셈해 0.043), 채움이 0.264 가 됐어요. 집 그림은 처음에 오른쪽 벽 아래 절반이 빠져 ⊏ 에 밑줄을 그은 꼴이었어요. 이집트의 집 상형처럼 앞벽 가운데에 문을 낸 평면으로 고쳤어요. 페니키아 글자의 꼴과 획 방향은 Noto Sans Phoenician 과 Noto Sans Old Italic 의 Unicode 참고 글리프를 그려 보고 맞췄고, 좌표는 이 패턴에서 적었어요. 하다 보니 디필론 항아리(기원전 740년쯤)와 네스토르의 잔(기원전 730년쯤)에 새긴 그리스 글은 아직 오른쪽에서 왼쪽으로 가요. 글자가 돌아선 것은 그리스 시대 안의 일이라, 패턴은 그 뒤집힘을 페니키아와 그리스 사이의 휨 안에 넣었어요. + +Lab 에서 해 보기: 없음. 네 글자가 32줄씩 128줄을 다 쓰고, 왼쪽 열 칸에는 시대 레일이, 오른쪽에는 방향 화살이 서요. 글자 사이에 남는 줄은 4줄뿐이라 Text 레이어 낱말을 얹을 자리가 글자 위밖에 없어요. + +기능 감: Lab 에 획 편집기요. 10×10 칸 위에 꺾은선을 찍으면 "x,y x,y" 꼴 좌표로 내 주는 것이에요. 일곱 글자의 네 시대에 걸친 획 쉰일곱 줄을 숫자로 적고, PNG 로 그려 보며 고쳤어요. + +### Word Search + +주제 7 「타이포」의 「글자로 짠 무늬」를 놀이로 푼 패턴이에요. 낱말 찾기 퍼즐은 노먼 기바트(Norman E. Gibat)가 1968년 오클라호마주 노먼의 『Selenby Digest』에 처음 실었다고 알려져 있어요. 패턴은 5×7 대문자 26자를 직접 그려 가로 6, 세로 8 LED 간격으로 10×16 칸, 160자를 1~127행에 깔아요. 낱말은 SERIF, KERN, GLYPH, BOWL 같은 타이포 낱말 열둘이에요. 긴 것부터 무작위 출발점 400곳을 차례로 보고, 칸이 비었거나 같은 글자인 첫 자리에 놓아요. 처음 200곳은 낱말마다 앞 낱말에서 8분의 3바퀴 돈 제 방향으로만 시험해서, 한 판에 여덟 방향이 다 들어요. 남은 칸은 무작위 글자예요. 흰 커서는 읽는 차례로 칸을 지나요. 남은 낱말의 첫 글자가 아닌 칸은 한 걸음에 지나고, 첫 글자인 칸에서는 동쪽부터 시계 방향으로 여덟 방향에 탐침을 내요. 탐침은 한 걸음에 한 글자씩 읽으며 맞는 동안 연한 금빛으로 늘고, 어느 낱말에도 맞지 않는 글자에서 장밋빛으로 깜빡이고 멈춰요. 한 낱말을 끝까지 읽으면 그 낱말 색의 캡슐이 둘러져 남아요. 캡슐은 첫 글자와 끝 글자 가운데를 잇는 선분에서 거리 3 인 LED 예요. 세로를 3/4 로 눌러 재서, 가로 낱말의 테두리는 위아래 4 LED 의 틈 줄에, 세로 낱말의 테두리는 양옆 3 LED 의 틈 칸에 앉아요. Speed 2 에서 한 걸음은 0.03초이고 찾을 때마다 0.4초 멈춰요. 처음 퍼즐은 1초 동안 찍힌 뒤 20초에 열둘을 다 찾아요. 그다음 1.6초 머물고 1.2초 동안 흩어져요. K3 Puzzle 은 열두 낱말 목록 여섯 가운데 하나를 골라요. 0 은 위의 SERIF 에서 ASCENDER 까지이고, 4 는 SORT 에서 FURNITURE 까지 금속 활자 낱말이에요. K4 Fill 은 찾은 낱말을 표시하는 법이에요. 0.06 에서는 1 LED 테두리이고, 0.5 에서는 제 색 40% 로 채운 캡슐이에요. 1 에서는 80% 로 꽉 찬 캡슐에 글자가 어둡게 파이고, 찾지 않은 글자는 절반으로 어두워져요. 잰 값은 움직임 0.040, 촘촘함 0.208, 가장자리 0.983, 복잡도 0.159, 채움 0.327, 색 0.139, 모양 바뀜 0.036 이고, thirds 는 36/30/31, 노브는 0.05, 0.12, 0.14, 0.08 이에요. 첫 판은 시도마다 방향을 새로 뽑았는데, 처음 퍼즐의 열두 낱말이 다 가로나 세로였어요. 너비 10칸에는 긴 낱말이 대각선으로 들어갈 자리가 적어서, 대각선 시도가 버려지는 사이 16칸인 세로가 먼저 자리를 얻었어요. 낱말마다 제 방향을 먼저 시험하게 하자 처음 퍼즐의 열둘 가운데 여섯이 대각선이 됐어요. 첫 판의 K4 Fill 은 0.02 였어요. 연구가 노브를 보는 2~3초에는 GLYPH 하나만 찾은 뒤라 캡슐 하나의 안쪽만 바뀌었어요. Fill 을 올릴수록 찾지 않은 글자를 절반까지 어둡게 하자 0.08 이 됐고, 찾은 낱말이 판에서 떠오르는 것도 그만큼 세졌어요. 움직임은 0.051 에서 0.040 으로, 모양 바뀜은 0.053 에서 0.036 으로 조금 내렸어요. + +Lab 에서 해 보기: Text 레이어의 역할을 mask 로 두고 A 를 Inter size 80, 가운데 맞춤 x 32, y 64 로 얹으면 큰 A 안의 격자만 남아요. 5×7 글자 수십 개와 커서가 큰 A 하나를 짜고, 캡슐은 A 의 획 안에서만 보여요. + +기능 감: Lab 미리보기의 시간 막대요. 처음 퍼즐이 다 풀려 흩어지는 것은 21초 뒤라, 캡슐이 쌓인 판과 흩어지는 판을 보려고 Lab 밖에서 21초와 39초의 프레임을 뽑았어요. 77판 기능 감의 시간 막대와 같은 것이에요. + +### Lensed Page + +주제 7 「타이포」의 「글자와 빛」과 「세로 패널다운 글」을 겨누고, 주제 4 「세로 64×128」을 같이 채우는 패턴이에요. 오레스트 흐볼손(Orest Chwolson)은 1924년에, 알베르트 아인슈타인(Albert Einstein)은 1936년에, 별 하나가 다른 별과 일직선으로 서면 뒤 별의 빛이 고리로 보인다고 적었어요. 점 질량 렌즈의 식은 β = θ − θ_E² θ/|θ|² 이고, 패턴은 LED 마다 이 식으로 뒤에 놓인 쪽을 읽어요. 쪽에 적힌 글은 아이작 뉴턴(Isaac Newton)의 『광학(Opticks)』(1704) 끝에 붙은 물음 가운데 첫째예요. 물체가 떨어져서도 빛에 작용해 빛을 휘게 하지 않느냐는 물음을, 바로 그 휨을 거쳐 읽게 했어요. 3×5 대문자를 손으로 그려 한 줄에 16자, 7행 간격으로 낱말 단위로 줄을 바꾸니 11줄이에요. NEWTON 1704 서명과 빈 줄 둘을 더한 98행 쪽이 아래로 되풀이되고, Speed 2 에서 1초에 5행씩 올라가요. 처음 화면의 아인슈타인 반지름은 13 LED 라서 지름 26 LED 고리가 64행에 서요. 렌즈에서 t LED 떨어진 글자는 고리 밖 (t + √(t² + 4θ_E²))/2 자리와 고리 안 반대쪽에 뒤집혀 두 번 서고, 바로 뒤의 글자는 고리로 번져요. 질량에서 40 LED 떨어진 줄도 4.2 LED 밖으로 밀려서 쪽 전체가 렌즈 둘레로 휘어요. 렌즈는 면 밝기를 지켜서 커진 글자는 커지기만 하고 밝아지지 않아요. 값은 쌍선형으로 읽고, 고리 안처럼 쪽이 줄어드는 곳에서는 LED 하나가 덮는 쪽 너비 1 + θ_E²/r² 만큼 흐린 쪽 사본을 읽어요. 바깥 상은 하늘색, 고리 안의 뒤집힌 상은 라일락이고, 고리 언저리의 잉크는 흰색으로 바래요. 쪽 바닥은 23/255 의 어두운 파랑으로 어디서나 한 색이라 글자가 없으면 렌즈는 보이지 않아요. 64열 밖에는 쪽이 없어서, 고리 한가운데에 쪽 양옆의 어둠이 뒤집혀 들어온 검은 자리가 생겨요. 잰 값은 움직임 0.825, 촘촘함 0.163, 가장자리 0.660, 복잡도 1.469, 채움 0.482, 색 0.172, 모양 바뀜 0.098 이고, thirds 는 43/47/53, 노브는 0.14, 0.20, 0.20, 0.17 이에요. 첫 판은 처음 반지름이 10 LED 였어요. 잰 값은 움직임 0.822, 채움 0.474, 노브 0.12, 0.20, 0.20, 0.15 로 거의 같았지만, 고리가 지름 20 LED 라 안쪽이 분홍 얼룩으로만 보여서 Mass 범위를 10~26 에서 13~29 로 옮겼어요. 그 판은 바닥 색까지 고리 안에서 분홍으로 바꿔서, 뒤에 글자가 없어도 렌즈 원판이 드러났어요. 바닥은 한 색으로 두고 잉크만 색을 바꾸게 했어요. 패널에서는 고리에서 3 LED 밖까지 글자가 고리를 따라 세 배 넘게 늘어나요. 고리 안의 뒤집힌 글자는 지름 방향으로 3분의 1 아래로 눌려서 낱자로는 잘 읽히지 않아요. + +Lab 에서 해 보기: Text 레이어로 BEND 를 Silkscreen 8 px, 흰색 solid, 가운데 맞춤 x 32, y 64 에 얹으면 휘지 않은 낱말이 고리 안의 뒤집힌 상 위에 서서, 렌즈를 지난 글과 지나지 않은 글이 한 화면에서 견줘져요. + +기능 감: 코드 패턴이 아래 레이어의 픽셀을 좌표로 읽어 오는 칸이요. 그게 있으면 이 렌즈로 Text 레이어에 쓴 아무 글이나 휘게 할 수 있는데, 지금은 뉴턴의 물음을 코드 안에 3×5 비트맵으로 박고 낱말 줄 바꿈도 손으로 짰어요. + +### Noordzij Cube + +주제 7 「타이포」의 「노브와 글」과 「글자의 몸」을 겨눈 패턴이에요. 헤릿 노르트제이(Gerrit Noordzij)는 1985년 『De streek』(영어판 『The stroke: theory of writing』, 2005)에서 글자의 굵고 가는 차이를 모두 펜이 남긴 자국으로 읽었어요. 넓은 펜촉을 돌리지 않고 움직이는 병진(translation)에서는 획이 펜촉 날을 가로지를 때 가장 굵고 날을 따라갈 때 가장 가늘어서, 획의 방향과 펜촉 각도가 굵기를 정해요. 끝이 뾰족하고 낭창한 펜의 팽창(expansion)에서는 누르는 만큼 굵어지고 손은 내려 그을 때 누르니, 굵은 자리가 내림획에 와서 부풀었다 빠져요. 노르트제이의 대비 입방체는 이 두 모델을 한 모서리에, 대비의 양을 다른 모서리에, 끊긴 짜임과 이어진 짜임을 셋째 모서리에 둬요. 패턴은 소문자 a, b, e, g, n, o 여섯을 쓰는 차례와 방향이 있는 가운데선으로 적고, nano, bone, ego 와 oboe, nag, eon 을 한 줄에 한 낱말씩 세로로 쌓아요. x 높이는 21 LED, 글자 너비는 9.7 LED 예요. 가운데선의 점마다 두 굵기를 한 번 셈해 둬요. 하나는 30도로 든 넓은 펜촉의 |sin(방향 − 30°)| 이고, 다른 하나는 획이 아래로 가는 몫을 3.2 LED 길이로 앞뒤로 고르게 한 뾰족한 펜의 누름이에요. K3 Model 이 둘을 섞고, K4 Hairline 이 가장 가는 획을 가장 굵은 4.8 LED 의 몇 분의 1로 둘지 정해요. 둥글고 가장자리가 1 LED 에 걸쳐 흐린 펜이 그 굵기로 가운데선을 따라가요. 노브가 바꾸는 것은 이 두 수뿐이고 뼈대와 획 순서는 그대로예요. Model 을 0 에서 1 로 돌리면 o 의 굵은 자리가 왼쪽 아래와 오른쪽 위의 비낀 축에서 왼쪽 내림획 하나로 옮겨 가고, n 의 아치 위가 머리카락 획으로 가늘어져요. Hairline 을 1 로 돌리면 모든 획이 4.8 LED 가 되어 두 모델이 같은 글자를 내요. 대비가 없으면 병진과 팽창을 가를 것도 없는 입방체의 모서리예요. 펜은 Speed 2 에서 1초에 44 LED 를 가고, 한 쪽을 16.6~18.8초에 쓰고 2.5초 둔 뒤 0.8초에 걸쳐 지워요. 획마다 펜이 닿는 자리의 청록에서 떼는 자리의 보라로 색이 바뀌어 쓴 방향이 보이고, 굵은 잉크는 짙게, 머리카락 획은 옅게 칠해요. 잰 값은 움직임 0.060, 촘촘함 0.080, 가장자리 0.697, 복잡도 0.838, 채움 0.337, 색 0.365, 모양 바뀜 0.251 이고, thirds 는 36/39/24, 노브는 0.17, 0.18, 0.06, 0.05 예요. 첫 판은 첫 낱말만 써 둔 채 열려서 움직임 0.024, 채움 0.195, thirds 34/22/0, 노브 0.08, 0.09, 0.02, 0.02 였어요. 2~8초 사이에 펜이 둘째 줄을 쓰는 동안 아래 3분의 1은 비어 있었고, 잉크가 적어서 굵기를 바꾸는 두 노브가 거의 잡히지 않았어요. 마지막 줄 앞까지 써 두고 열자 K3 가 0.05, K4 가 0.04 가 됐지만 K2 Speed 가 0.03 으로 내려갔어요. 미리 쓰는 시간을 노브의 빠르기로 12초 셈하게 바꾸자 K2 가 0.14 가 됐는데, 이번에는 쪽을 지우는 0.8초가 8초 뒤로 밀려 움직임이 0.021 로 축 아래 끝 0.019 에 붙었어요. 13.5초로 늘려 지우기가 7.1~8.0초에 들게 하자 지금 값이 됐어요. 처음 그린 3.6 LED 획으로는 두 모델의 차이가 잘 보이지 않아 4.6, 4.8 LED 로 키웠고, x 높이도 20 에서 21 LED 로 키웠어요. Lab 은 K4 를 0.06 에서 여는데, 대비 노브가 거기서 열리면 거의 단선 글자라 두 모델을 가를 수 없어요. 그래서 K4 를 대비를 거꾸로 읽는 Hairline 으로 두었어요. 셋째 모서리는 끊긴 짜임에 묶어 두어서, 펜은 획 사이마다 들려 흐린 점으로 옮겨 가요. 이어진 짜임을 넣으려고 따져 보니, 한 글자의 끝에서 다음 글자의 시작으로 곧게 이으면 n 에서 a 로, n 에서 o 로, b 에서 o 로 가는 이음 획이 모두 다음 글자의 속공간을 가로질렀어요. + +Lab 에서 해 보기: 두 쪽 모두 마지막 줄 아래 122~127행이 비어 있어요. Text 레이어에 1985 를 Silkscreen 8 px, 가운데 맞춤 x 32, y 124, 색 #5a6a88 로 얹으면 『De streek』가 나온 해가 거기 서요. K3 Model 을 돌리는 동안 Text 레이어 숫자의 굵기는 그대로라서, 펜 글자만 입방체를 옮겨 다니는 것이 나란히 보여요. + +기능 감: `@knobs` 에 노브마다 여는 값을 적는 칸이요. 대비 노브는 높은 대비에서 열려야 두 모델이 갈리는데, Lab 은 K4 를 0.06 에서 열어서 Contrast 를 0..1 로 두면 거의 단선 글자로 열려요. 그래서 노브의 이름과 뜻을 뒤집어 Hairline 으로 두었어요. + +### Fork Hologram + +주제 5 「수학 · 추상」으로 물결·간섭 갈래에 서고, 주제 1 「지도의 빈 칸」을 같이 채우는 패턴이에요. Boustrophedon, Hard Squares, Night Flight, Sunglint 넷뿐인 가장자리 × 촘촘함 2,4 칸을 겨눴어요. 가장자리가 0.21~0.50 이면서 이웃 LED 사이 차이가 평균 0.297 을 넘어야 하는 칸이에요. 바젠노프(V. Yu. Bazhenov), 바스네초프(M. V. Vasnetsov), 소스킨(M. S. Soskin)은 1990년에 파면에 나사 전위(screw dislocation)가 있는 레이저 빛을 비스듬한 평면파와 겹쳐 기록했어요. 위상이 한 점 둘레로 2πl 감기는 빛과 평면파가 만든 줄무늬는 그 점에서 줄이 l 개 더 끼어들어, |l|+1 갈래로 갈라지는 포크가 돼요. 헤켄버그(N. R. Heckenberg), 맥더프(R. McDuff), 스미스(C. P. Smith), 화이트(A. G. White)는 1992년에 이 포크를 계산해 흑백 이진 홀로그램으로 찍고, 그 격자로 소용돌이 빛을 만들었어요. 패턴은 위상 φ = 2π(k·r) + Σ l_j atan2(y − y_j, x − x_j) 의 등위선을 그려요. 소용돌이마다 위상을 단위 복소수로 곱해서 LED 하나에 atan2 는 한 번만 들어요. 처음 화면은 주기 3.3 LED 로 14도 기운 줄 19개가 64열을 덮고, 전하 1~3 의 소용돌이 열 개쯤이 1초에 0.5~4 LED 씩 떠다녀요. 반대 전하는 1/r 로 당기고 같은 전하는 밀어내요. 만나면 전하를 더해서, 크기가 같으면 둘 다 사라지고 다르면 차이만 남아요. 전하가 2 이상인 소용돌이는 가끔 단위 소용돌이 하나를 떨궈요. 열 개 아래로 줄면 0.6~1.8초마다 +l 과 −l 한 쌍이 한 점에서 나와 벌어져서, 전하를 다 더하면 늘 0 이에요. 소용돌이 한가운데는 빛이 어두워 줄이 평균 밝기로 흐려져요. 색은 위상 기울기의 길이로 칠해요. 줄이 몰리는 쪽은 파랑, 성겨지는 쪽은 초록이라 포크마다 두 색 날개가 붙어요. 잰 값은 움직임 0.293, 촘촘함 0.326, 가장자리 0.414, 복잡도 2.597, 채움 0.761, 색 0.252, 모양 바뀜 0.026 이고, thirds 는 75/77/74, 노브는 0.18, 0.51, 0.36, 0.35 예요. 겨눈 2,4 칸에 서요. 첫 판은 줄을 위상의 세모파로 그리고 꼭대기를 1.3 배로 눌러 잘랐는데, 가장자리가 0.609 라 칸 오른쪽에 섰어요. 세로 이웃 차이가 40/255 바로 아래여서, 소용돌이가 기울기를 조금만 바꿔도 단단한 차이로 넘어갔어요. 세모파를 사인 쪽으로 구부린 둘째 판은 가장자리 0.534, 촘촘함 0.282 였어요. 위상 하나의 함수로 그린 줄은 가로 이웃이 거의 다 단단하고 세로 이웃이 거의 다 부드러워요. 줄무늬만 재 봐도 촘촘함 0.30 을 넘는 모양 가운데 가장자리가 가장 낮은 것이 0.427 이었고, 소용돌이가 그 위에 0.03~0.1 을 더했어요. 헤켄버그의 이진 홀로그램처럼 어둠에서 밝음으로 한 LED 에 뛰는 계단을 넣고 회색 세모파를 35% 남기자 0.414 와 0.326 이 됐어요. 계단이 가로 차이를 키우고, 남긴 세모파가 위아래 LED 를 늘 조금씩 다르게 해요. 패널에서는 처음 주기의 포크가 파랑과 초록 얼룩 속 작은 Y 로 보여요. K4 Carrier 를 1 로 올려 주기를 9.4 LED 로 넓히면 전하 5 의 여섯 갈래가 하나하나 떨어져 보여요. + +### Cantor Scalogram + +주제 5 「수학 · 추상」으로 데이터 그림 갈래에 서고, 주제 1 「지도의 빈 칸」을 같이 채우는 패턴이에요. 복잡도 × 채움 4,1 칸을 겨눴어요. LED 의 70% 넘게 24 아래로 꺼진 채, 한 프레임을 deflate 로 눌러도 LED 하나에 2.171 바이트 넘게 남아야 하는 칸이에요. 알렉스 그로스만(Alex Grossmann)과 장 모를레(Jean Morlet)가 1984년에 낸 연속 웨이블릿 변환은 신호를 자리 b 와 크기 a 의 두 축에 펼쳐요. 알랭 아르네오도(Alain Arneodo), 기 그라소(Guy Grasseau), 마티아스 홀슈나이더(Matthias Holschneider)는 1988년에 이 변환으로 칸토어 계단 같은 프랙탈 신호의 특이점을 읽었어요. 계수가 가장 큰 자리를 이은 선은 크기를 줄일수록 갈라져서, 가운데 3분의 1을 거듭 빼는 칸토어 집합과 똑같이 둘씩 가지를 쳐요. 패턴은 128줄을 자리, 64열을 로그 크기로 써요. 넷째 단계 구간은 집합의 1/81 이라 128줄에서는 1.46줄이지만 64열에서는 0.73 LED 라서, 자리를 긴 쪽에 뒀어요. 크기는 21열마다 3분의 1로 줄어 왼쪽 0.243 에서 오른쪽 0.009 까지 세 단계예요. 밝기는 복소 모를레 웨이블릿(ω0 = 3)으로 잰 계수의 크기이고 색상은 위상이에요. 위상은 가장 가는 열에서 한 줄에 2.8 라디안씩 돌아서, 가는 가지는 LED 마다 색이 바뀌는 줄무늬로 서요. 계단은 집합의 점마다 |x − x0|^0.631 로 오르니, 열마다 a^0.369 를 곱해 모든 단계가 같은 밝기로 서요. 그러면 [0, 1/3] 의 측도가 전체를 3분의 1로 줄인 것의 절반이라는 대칭 때문에, b = 0 둘레로 3배 당겨 들어가며 21열 옮기면 같은 그림이 돼요. Speed 2 에서 6초에 한 번이고, 한 바퀴 끝과 처음의 계수 차이는 평균 크기의 10만분의 3이에요. 계수는 크기마다 구간이 a 의 1/5.5~1/16.5 인 단계에서 측도를 점 질량으로 잘라 셈하고, 구간 안의 퍼짐(분산 (3^-k)²/8)은 웨이블릿의 이계도함수로 되돌려요. 세 단계 더 잘게 자른 셈과 견주면 차이가 0.004% 이고, ω0 = 8 에서 0.09% 예요. 패널에서 4a 안의 질량만 세어 한 열에 8~64 개이고, 줄을 따라서는 가우스를 곱셈 둘로, 위상을 회전 하나로 이어 가요. 잰 값은 움직임 1.506, 촘촘함 0.103, 가장자리 0.582, 복잡도 2.275, 채움 0.277, 색 0.189, 모양 바뀜 0.225 이고, thirds 는 38/27/17, 노브는 0.10, 0.22, 0.25, 0.17 이에요. 겨눈 4,1 칸에 서요. 다섯 프레임으로 잰 첫 판은 계수 크기를 1.4 제곱으로 밝혀서 채움이 0.70 이었어요. 거친 열은 집합 전체를 한 덩이로 봐서 왼쪽 3분의 1이 다 켜졌어요. 크기가 작은 73% 를 24 아래에 두고 나머지만 25 에서 255 로 올리자 나무가 섰지만, 열마다 따로 매끈한 잡음을 얹은 판은 거친 열이 세로 줄무늬로 갈라졌어요. 문턱 아래를 로그 밝기로 그리고 잡음을 문턱의 4% 로 줄이자 채움 0.267 에 복잡도가 1.98 이었어요. 이 그림에서 deflate 는 되풀이를 거의 찾지 못해서, 바이트 값이 얼마나 고르게 퍼졌는지가 복잡도를 정했어요. 어두운 LED 는 이미 하나에 1.72 바이트로 0~24 를 거의 다 썼고, 막힌 곳은 켜진 LED 바이트의 12% 가 25 아래로 내려와 바닥과 겹친 것이었어요. 잡음을 LED 마다 따로 문턱의 10% 로 두어 2.13, 켜진 LED 를 25 의 회색 위에 얹어 2.20 이 됐어요. 바닥의 채도를 계수 가운데 잡음의 몫으로 정해 2.25, 문턱을 72% 로 내려 2.275 가 됐어요. K3 Noise 는 처음에 노브의 1.5 제곱으로 잡음을 키워서, 4 에서 채움 0.69, 8 에서 0.94 로 패널이 잡음뿐이었어요. 0.85 제곱으로 바꿔 8 에서 0.555 이고, 가는 가지부터 잡음에 묻혀요. K4 Omega 를 5 넘게 돌리면 한 가지의 두 반쪽이 사이에서 서로 지워서 원뿔이 줄무늬 진 타원으로 열려요. 잡음 없이 Omega 8 에서 열마다 가장 큰 계수를 6열 간격으로 세어 보니, 6, 24, 48열에서 0.15 이고 그 사이에서는 0.33~0.54 였어요. 칸토어 집합이 3배마다 같은 꼴이라는 것이 로그 크기 축에 21열 주기로 남은 거예요. + +### Exclusion Process + +주제 5 「수학 · 추상」으로 입자 갈래에 서고, 주제 4 「세로 64×128」을 같이 채우는 패턴이에요. 완전 비대칭 단순 배타 과정(TASEP)은 캐럴린 맥도널드(Carolyn MacDonald), 줄리언 깁스(Julian Gibbs), 앨런 핍킨(Allen Pipkin)이 1968년에 전령 RNA 위를 가는 리보솜의 모형으로 처음 적었고, 프랭크 스피처(Frank Spitzer)가 1970년 「Interaction of Markov processes」에서 상호작용하는 입자계로 다뤘어요. 한 칸에는 입자가 하나까지 앉고, 입자는 앞 칸이 비었을 때만 율 1 로 오른쪽으로 한 칸 뛰어요. 패턴은 64칸 줄을 패널 너비에 깔고, 비어 있는 첫 칸에 율 α 로 입자를 넣고 마지막 칸의 입자를 율 β 로 내보내요. 단위 시간마다 결합 65개(들어오는 곳, 칸 사이 63개, 나가는 곳)를 평균 한 번씩 무작위 차례로 건드리고, 그 단위 시간 하나가 한 줄이에요. 가장 새 줄이 맨 위에 서고 지난 줄은 아래로 밀려서, 입자마다 왼쪽 아래에서 오른쪽 위로 오르는 실을 남겨요. Speed 2 에서 1초에 16줄이 들어와 128줄이 지난 8초를 담아요. 베르나르 데리다(Bernard Derrida), 에이탄 도마니(Eytan Domany), 데이비드 무카멜(David Mukamel)이 1992년에, 군터 쉬츠(Gunter Schütz)와 도마니가 1993년에 이 열린 줄의 정상 상태를 정확히 풀었어요. α < β 이고 α < 1/2 이면 밀도 α 의 저밀도 상이고, 거울 꼴이면 밀도 1 − β 의 고밀도 상이에요. 둘 다 1/2 을 넘으면 밀도 1/2, 흐름 1/4 의 최대 흐름 상이에요. α = β < 1/2 에서는 밀도 α 와 1 − α 가 같은 흐름을 날라서, 둘 사이의 충격파가 쏠림 없이 마구 걸어요. 처음 노브는 α = β = 0.25 로 이 공존선 위예요. 밀도 0.25 의 성긴 흐름이 밀도 0.75 의 막힌 줄과 만나고, 그 경계가 128줄 동안 평균 9.8칸 떠돌아요. 분산이 단위 시간마다 2J/(1 − 2α) = 0.75 씩 는다는 셈과 맞아요. 따로 4만 단위 시간을 돌려 잰 흐름은 공존선에서 0.185, 최대 흐름 상에서 0.254 로 정확한 값 0.1875 와 0.25 에 맞았어요. 경계가 어디에나 같은 확률로 서니, 평균 밀도는 왼쪽 0.31 에서 오른쪽 0.64 까지 곧은 비탈이에요. 입자는 들어올 때 받은 번호를 지니고, 색은 번호마다 띠의 0.618 씩 건너뛰어요. 입자는 서로 앞지르지 못해서 줄 위의 이웃은 늘 이어진 번호이고, 그래서 이웃한 실은 같은 색이 되지 않아요. 한 줄 동안 여러 칸을 뛴 입자는 지나간 칸을 45% 밝기로 남겨서 실이 끊기지 않아요. K3 Alpha 와 K4 Beta 는 0.25~1 이에요. 0~1 로 두면 K3 이 0 에서 열려 아무것도 들어오지 않아서, 공존선의 0.25 를 아래 끝으로 잡았어요. 0.2 에서는 경계가 128줄에 8.2칸, 0.3 에서는 11.9칸 떠돌아요. 0.25 는 두 밀도의 차이를 크게 두면서도 경계가 눈에 띄게 떠돌았어요. Alpha 를 올리면 고밀도 상이 되어 막힌 줄의 경계가 왼쪽 끝으로 빠져나가요. α 0.5 에서 그 빠르기는 한 줄에 1/4 칸이에요. Beta 를 올리면 막힘이 오른쪽으로 빠지고 성긴 흐름만 남아요. 잰 값은 움직임 2.823, 촘촘함 0.216, 가장자리 1.000, 복잡도 1.667, 채움 0.625, 색 0.438, 모양 바뀜 0.155 이고, thirds 는 59/62/65, 노브는 0.29, 0.27, 0.24, 0.27 이에요. 가장자리 1.000 은 축 끝과 같고 넘지는 않아요. 첫 판이 이 값 그대로였고, 고친 것은 머리 주석과 쓰지 않는 이름 둘뿐이에요. 첫 프레임 앞에 두 밀도의 계단에서 출발해 120 단위 시간을 가라앉힌 뒤 128줄을 채워서, 패널은 처음부터 가득 차서 열려요. 패널에서 본 것은 거울이에요. 성긴 쪽의 입자는 한 줄에 0.75칸 오른쪽으로 가고, 막힌 쪽의 빈칸은 한 줄에 0.75칸 왼쪽으로 가요. 그래서 공존선에서는 왼쪽의 색 실과 오른쪽의 어두운 빈칸 줄이 같은 기울기로 마주 서요. 입자와 빈칸을 바꾸고 왼쪽과 오른쪽을 바꾸면 같은 과정이라는 대칭이 그림으로 보였어요. Phantom Jam(51판)은 고리 도로의 결정론적 차 모형에서 정체를 키웠고, 여기서는 두 끝의 율만으로 상이 갈려요. + +### Crofton Cell + +주제 5 「수학 · 추상」으로 보로노이·셀 갈래에 서는 패턴이에요. 로저 마일스(Roger E. Miles)가 1964년 「Random polygons determined by random lines in a plane」에서 다룬 등방 푸아송 직선은 평면을 볼록 다각형으로 잘라요. 직선 하나는 고정점 O 에서 잰 방향 θ 와 거리 p 로 정해지는 점이고, 직선 과정은 (θ, p) 평면에 고르게 흩뿌린 푸아송 점이에요. 크로프턴 공식대로 볼록한 판을 지나는 직선 수는 그 둘레에 비례해요. 판 둘레 384 LED 를 반지름 76 원의 둘레와 견주면, 남겨 둔 직선의 0.804 가 판을 지나요. 직선은 저마다 p 를 따라 1초에 3 LED 까지 미끄러지고 θ 로 0.05 라디안까지 돌아요. 이 흐름은 (θ, p) 의 넓이를 지켜서 법칙이 그대로이고, p 가 0 을 지나면 반대편에서 본 같은 직선 (θ + π, −p) 가 돼요. 직선은 평균 10초를 살고 같은 빠르기로 새로 태어나요. 그래서 처음 화면에서 판에 보이는 자리에 1초에 1.6 개가 하얗게 번쩍이며 태어나고, 그만큼이 흐려지며 사라져요. LED 마다 어느 직선 너머에 있는지를 해시해서 칸을 나누고, 칸마다 반지름 76 정사각형을 그 칸 쪽 반평면들로 잘라 변을 세요. 세모는 청록, 네모는 하늘색, 다섯모는 보라, 여섯모는 분홍, 일곱모 넘게는 초록이에요. 가운데 점을 품은 칸이 크로프턴 칸(zero cell)이에요. 넓이에 비례해 뽑히는 칸이라 평균 변이 π²/2 = 4.93 이고, 패턴 안에서 세어도 56 줄에서 4.95 가 나와요. 이 칸은 내접원 안을 옅게, 남은 모서리를 금빛으로 칠하고, 내접원의 중심에서 가장 먼 꼭짓점까지를 점선 원으로 그려요. 데이비드 켄들(D. G. Kendall)은 1940년대에 큰 영점 칸은 원에 가깝다고 추측했어요. 평면에서는 이고르 코발렌코(Igor Kovalenko)가 1997년에, 일반 차원에서는 다니엘 후크(Daniel Hug), 마티아스 라이츠너(Matthias Reitzner), 롤프 슈나이더(Rolf Schneider)가 2004년에 증명했어요. K4 Big 은 O 에서 r = 20 Big^0.6 LED 안으로 직선이 들지 못하게 해요. 푸아송 과정은 겹치지 않는 두 곳에서 서로 독립이라, 이건 크로프턴 칸이 그 원판을 품는다는 조건을 그대로 건 과정이에요. 그래서 Big 은 직선을 숨기기만 해요. 원판으로 미끄러져 들어간 직선은 닿은 자리에서 불꽃을 내며 죽고, 미끄러져 나온 직선은 거기서 태어나요. 56 줄에서 Big 0 의 칸은 변이 4.95 개이고 내접원 반지름이 바깥 원의 0.36 인데, Big 1 에서는 변 8.5 개에 0.65 예요. 원에 가까워지기는 해도 느려요. 원판 둘레의 직선 밀도를 l 이라 하면 모서리가 원판 밖으로 r 의 (l r)^(−2/3) 쯤 튀어나와서, 폭 64 LED 에서는 이웃보다 둥근 여덟아홉모에서 멈춰요. 잰 값은 움직임 0.332, 촘촘함 0.074, 가장자리 0.678, 복잡도 0.731, 채움 0.904, 색 0.338, 모양 바뀜 0.162 이고, thirds 는 88/88/94, 노브는 0.28, 0.17, 0.26, 0.37 이에요. 첫 판은 Lines 가 10~40, r 이 28 Big^0.6 이었어요. 10 줄에 r 5 에서 크로프턴 칸의 내접원 반지름이 평균 21.8 LED 라 폭 64 를 거의 다 덮었고, r 28 에서는 칸이 판 양옆 밖으로 나가서 다각형으로 보이지 않았어요. 바깥 원도 판 밖이라 둥근지 볼 수 없었어요. Lines 를 16~56 으로 올리고 r 의 끝을 20 으로 줄이자, 처음 화면의 칸은 내접원 14 LED, 56 줄 Big 1 에서는 23.5 LED 가 됐어요. 그 판을 study 로 처음 잰 줄은 움직임 0.331, 가장자리 0.682, 복잡도 0.726 이었어요. O 둘레의 금지 원판을 옅은 고리로 그린 뒤에도 0.332, 0.678, 0.731 로 거의 그대로예요. 한 프레임은 처음 화면에서 1.4~1.8 ms, 56 줄 Big 1 에서 3 ms 예요. + +### Phasor Noise + +주제 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 가 됐어요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -4809,7 +4888,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 25일 `dev`(`f72355d`)의 328개에 43판부터 81판까지의 삼백아흔 개를 더한 718개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 25일 `dev`(`f72355d`)의 328개에 43판부터 82판까지의 사백 개를 더한 728개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -4927,6 +5006,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 81판은 79판까지 든 698개에 빈 칸도, 셋 이하인 칸도 없어서 넷뿐인 칸 아홉 가운데, 같이 도는 80판(#344)이 고른 가장자리 × 복잡도 4,4 와 가장자리 × 촘촘함 2,3 을 빼고 가장자리 × 촘촘함 1,3 과 촘촘함 × 복잡도 4,4 를 겨눴어요. 가장자리 × 촘촘함 1,3 은 40/255 를 넘는 계단의 몫이 0.210 아래이면서 이웃 차이 평균이 0.189~0.297 인 칸이에요. 오르막이 거의 다 무르고 띠마다 한 번만 크게 떨어지는 톱니가 길이에요. Head Wave 는 첫 도착 시각을 톱니로 칠하되 LED 마다 이웃 차이가 39.5 를 넘지 않는 가장 높은 띠를 그려서, 가장자리 0.144, 촘촘함 0.197 이에요. 진원을 (6, 44)에 두고 광선을 그은 첫 판은 가장자리 0.185, 촘촘함 0.150 으로 1,2 칸이었어요. 앞선이 행이나 열과 나란하면 오르막이 한쪽 이웃에만 실려서, 둥근 앞선만으로는 촘촘함이 0.20 언저리에서 멈춰요. 진원을 왼쪽 끝으로 옮겨 앞선이 패널 대각선으로 기운 머리파 쐐기를 키우자 칸에 섰어요. 촘촘함 × 복잡도 4,4 는 이웃 차이 평균이 0.297 을 넘으면서 복잡도가 2.172 를 넘는 칸이에요. Modular Hyperbola 는 소수 127 로 나눈 곱셈표를 큐브헬릭스 톱니로 칠해 촘촘함 0.336, 복잡도 2.391 이에요. 색상환에 밝기를 곱한 램프로 견주어 보니 LED 마다 한 채널이 0 이라 deflate 가 잘 눌러서 복잡도가 1.910 에 머물렀어요. 열 개 다 일곱 축의 끝 안에 들어 칸 경계는 60판 그대로예요. 79판까지 든 698개에 81판 열 개를 넣은 708개에서 두 칸은 다섯이 됐고, 아홉이던 넷뿐인 칸은 일곱이에요. 80판이 PR 에 올린 열 개까지 넣은 718개에서는 다섯이에요. 남은 다섯은 가장자리 × 촘촘함 1,4 와 2,4, 복잡도 × 채움 4,1 과 4,2, 복잡도 × 모양 바뀜 4,4 예요. +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 라 남는 틈이 좁아요. + ### 아직 없는 갈래 `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 가 섰어요. @@ -5099,9 +5180,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 81판에서는 Baudot Tape 와 Head Wave 가 128줄을 써요. Baudot Tape 는 너비 41 LED 의 종이 테이프가 위아래 끝까지 이어져 65/64/64 이고, 벽이나 테두리는 그리지 않았어요. Head Wave 는 땅의 세로 단면이라 128행이 깊이이고, 띠가 판을 다 덮어 87/89/90 이에요. 나머지 여덟 가운데 Cornsweet Letters 는 바탕이 판을 다 채워 100/100/100, YBC 7289 는 점토가 덮어 95/98/99, Modular Hyperbola 는 90/90/90 이에요. Dehn Twist 는 원환면과 붙인 정사각형이 위아래에 서서 63/58/82, Malfatti Circles 는 삼각형 둘이 쌓여 29/41/55 예요. Permutation Poem 은 23/23/23, Pollard's Rho 는 22/19/15, Beading Letters 는 17/19/18 이에요. +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 이에요. + ### 낯선 수학 -주제 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)예요. +주제 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)예요. 목록 밖에서 다음에 해 볼 것이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -5213,6 +5296,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 81판에서 본 것이에요. 다섯 가운데 셋은 한 대상을 두 판으로 나란히 세웠어요. Malfatti Circles 는 같은 삼각형에 말파티의 원과 탐욕의 원을 넣어요. 정삼각형에서는 72.9% 와 73.9% 로 1 포인트 차이인데, 23도 쐐기에서는 55.9% 와 69.8% 로 14 포인트가 벌어져요. 그 쐐기에서 탐욕의 둘째와 셋째 원은 다른 모서리로 가지 않고 가장 뾰족한 모서리에 같은 비 0.664 로 줄지어 들어가요. Pollard's Rho 는 같은 수를 플로이드와 브렌트의 고리 찾기로 한 번씩 풀어요. 같은 5893 도 c 하나에 따라 먼저 닫히는 ρ 가 달라서, c = 1 에서는 83 을, c = 5 에서는 71 을 잡아요. Dehn Twist 는 같은 곡선을 원환면과 붙인 정사각형에 그려요. 두 번 꼬였다 두 번 풀려 같은 류 (1,0) 으로 돌아와도 곡선은 관을 따라 조금씩 밀려나 있었어요. 류가 같다고 곡선이 제자리인 것은 아니라서, 펴진 뒤 0.4초 동안 곡선을 제자리로 밀어 두었어요. Head Wave 의 띠는 64행에서 42도쯤 꺾여요. 머리파 앞선은 경계와 임계각으로 만나고, 바로 아래 맨틀의 앞선은 경계를 스치는 굴절파라 거의 곧게 서요. Modular Hyperbola 는 무아레가 될 줄 알았던 판이 매끈했어요. p = 127 이면 위 절반 어디서나 톱니가 한 LED 에 반 바퀴를 넘지 않아서, 쌍곡선 xy = c + 127k 서른두 개가 접히지 않고 다 보였어요. +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 이 됐어요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. @@ -5311,6 +5396,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 목록 밖에서 81판 뒤에 해 볼 것이에요. 니콜라스 카츠(Nicholas Katz)가 1988년에 보인 대로, 소수 p 를 정하고 a 를 1 부터 p − 1 까지 돌린 클루스테르만 합 K(1, a; p)/2√p 는 반원 분포로 쌓여요. 한 행에 소수 하나씩 그 분포를 막대로 내려 쓰면 p 가 커질수록 반원이 서는 것이 보일 거예요. 잉에 레만(Inge Lehmann)은 1936년에 P파 그림자 띠 안으로 희미하게 들어오는 파를 보고 내핵이 있다고 읽었어요. 지구 반쪽 단면을 세운 패널에 놓고 층 셋의 첫 도착을 같은 페르마 원리로 셈하면, 그림자 띠와 그 안으로 꺾여 드는 내핵 파가 같이 서요. 자오선 꼬임과 경도선 꼬임을 번갈아 걸면 곡선의 류가 (1,0) 에서 (1,1), (2,1), (2,3), (5,3) 으로 피보나치 수를 따라 자라요. 리코리시(1962)의 말대로 원환면에서는 이 두 꼬임으로 모든 사상류를 낼 수 있어서, 다음에 걸 꼬임을 노브로 고르는 판을 해 볼 만해요. 프레더릭 소디(Frederick Soddy)가 1937년 Nature 에 낸 육각 사슬(hexlet)은 서로 닿는 세 구에 모두 닿는 구 여섯이 어디서 시작해도 고리로 닫혀요. 입체 사영으로 세워 돌리면 말파티 원 다음 판이 될 만해요. 존 폴라드가 1978년에 낸 이산 로그의 캥거루 방법(lambda method)은 두 마리가 뛴 자국이 한 번 겹치면 그 뒤로 같은 길을 가서 λ 꼴이 돼요. 세로 패널에 λ 를 세우면 이번 ρ 와 짝이 돼요. +목록 밖에서 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 마다 세기가 다른 원뿔이 서요. + ### 타이포 주제 7 로 해 본 글자 실험과 다음에 해 볼 것이에요. 63판과 64판이 같이 돌며 다섯씩 처음 만들었어요. 63판의 다섯은 한글 자모가 날아와 모아쓰기로 잠기는 조립(Jamo Assembly), 놀턴과 하먼의 기호 모자이크(Type Mosaic), 호프스태터의 트립렛 그림자(Trip-let), 카프리카의 계산 종이(Kaprekar's Routine), 캐럴의 낱말 사다리(Word Ladder)예요. @@ -5459,6 +5546,14 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 81판 뒤에 해 볼 것이에요. 레몽 크노(Raymond Queneau)가 1961년에 낸 『시 백조 편(Cent mille milliards de poèmes)』처럼, 소네트 열네 줄마다 열 갈래 가운데 하나를 고르는 판을 노브로 넘겨 볼 만해요. 갈래 글은 직접 지어 넣어야 해요. 에른스트 마흐(Ernst Mach)가 1865년에 적은 마흐 띠는 밝기 기울기가 꺾이는 자리에 그리지 않은 밝은 줄과 어두운 줄이 보이는 착시예요. 글자 테두리에 계단 대신 기울기의 꺾임만 두면 그리지 않은 선으로 글자가 서요. 길버트 버넘(Gilbert Vernam)이 1917년에 만든 암호 테이프를 해 볼 만해요. 평문 테이프와 열쇠 테이프의 구멍을 줄마다 XOR 해 셋째 테이프에 찍으면 패널에 테이프 셋이 나란히 서고, 같은 열쇠를 한 번 더 더하면 글이 돌아와요. 오토 노이게바우어(Otto Neugebauer)와 에이브러햄 잭스(Abraham Sachs)가 1945년에 풀어 읽은 Plimpton 322 는 피타고라스 세 수에서 나온 값을 60진법으로 적은 표예요. 줄마다 쐐기로 눌러 쓰고, 셈이 맞는 칸을 켜는 판이 될 만해요. 로버트 디건(Robert D. Deegan)과 동료들이 1997년에 밝힌 커피 얼룩 효과로 글자를 써 보면, 물이 마르며 획 가장자리로 색이 몰려 테두리만 진한 글자가 남아요. +82판의 다섯은 구멍 수만 남기고 줄어드는 글자(Letter Retracts), 원시 시나이 그림에서 로마 대문자까지 휘는 글자(Alphabet Lineage), 커서가 여덟 방향으로 더듬는 낱말 찾기(Word Search), 점 질량 렌즈 너머로 읽는 뉴턴의 물음(Lensed Page), 같은 뼈대에서 병진과 팽창을 오가는 펜 글씨(Noordzij Cube)예요. + +82판에서 본 것이에요. 이번에도 다섯이 다 글자 꼴을 코드에 적었고, 옮겨 쓴 글꼴은 없어요. 둘은 글자를 움직이는 셈보다 글자 꼴을 먼저 검사해야 했어요. Letter Retracts 는 열다섯 글자를 획 반지름 0.25 간격으로 떼어 구멍 수를 처음 글자와 견주는 검사로, 40×38 상자에서 A 의 카운터가 반지름 5.5 에서 막히고 G 의 입이 5.25 부터 닫혀 고리가 되는 것을 찾았어요. Alphabet Lineage 는 페니키아 글자의 꼴과 획 방향을 유니코드 참고 글리프를 그려 보고 맞췄어요. 쓰는 방향이 그림을 정하기도 했어요. 페니키아에서 그리스로 휠 때 A 말고는 일곱 글자가 다 세로축을 두고 카드처럼 뒤집혀요. Word Search 는 너비가 10칸뿐이라 긴 낱말의 대각선 시도가 버려져서 처음 퍼즐의 열둘이 다 가로나 세로였고, 낱말마다 제 방향을 먼저 시험하자 여섯이 대각선이 됐어요. Lab 이 여는 노브 자리가 노브의 뜻을 바꾸기도 했어요. Noordzij Cube 는 K4 가 0.06 에서 열려서, 대비 노브를 거꾸로 읽는 Hairline 으로 두었어요. Lensed Page 의 고리 안 뒤집힌 글자는 지름 방향으로 3분의 1 아래로 눌려서 낱자로는 읽히지 않아요. + +기능 감은 셋이 글자나 그림을 코드 밖에서 받는 길을, 둘이 Lab 의 노브와 미리보기를 손보는 길을 바랐어요. Letter Retracts 는 코드 패턴이 같은 프로젝트의 Text 레이어 비트맵을 읽기를, Lensed Page 는 아래 레이어의 픽셀을 좌표로 읽어 오기를, Alphabet Lineage 는 칸 위에 꺾은선을 찍으면 좌표로 내 주는 획 편집기를 바랐어요. Noordzij Cube 는 @knobs 에 노브마다 여는 값을 적는 칸을, Word Search 는 미리보기의 시간 막대를 바랐어요. + +82판 뒤에 해 볼 것이에요. 앨런 터로프(Allan Turoff)가 1972년에 만든 보글은 이웃한 칸을 어느 쪽으로든 꺾어 이어 낱말을 읽어서, Word Search 의 곧은 캡슐 대신 굽은 길이 남고 찾기는 접두사 나무를 따라가는 깊이 우선 탐색이 돼요. 스티븐 그레이(Stephen B. Gray)가 1971년에 낸 비트쿼드 셈은 2×2 칸 무늬의 수만 세어 조각 수에서 구멍 수를 뺀 오일러 수를 내서, 흐르는 글줄 옆에서 LED 네 개짜리 창이 글자의 구멍을 세는 판이 될 만해요. 로마 대문자에서 언셜(4세기)과 반언셜을 거쳐 투르 필사실의 카롤링거 소문자(800년쯤)로 가는 길은 Alphabet Lineage 의 다음 걸음이라 A 가 a 로, D 가 d 로 휘어요. 노르트제이 입방체의 셋째 모서리인 이어진 짜임은 글자마다 들어오는 획과 나가는 획을 따로 두고 그 끝끼리 이으면 이음 획이 다음 글자의 속공간을 가로지르지 않아서, 펜을 떼는 판과 떼지 않는 판을 버튼으로 오갈 수 있어요. + ### 화장품 팝업 주제 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 cb95020c3d0607d8fef513c377af44f68548ca39 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 23:49:42 +0000 Subject: [PATCH 12/12] =?UTF-8?q?=ED=8C=A8=ED=84=B4=2082=ED=8C=90:=20index?= =?UTF-8?q?.html=20=EC=9D=84=20=EB=8B=A4=EC=8B=9C=20=EC=A7=93=EB=8A=94?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018svN6XCxZJqDPz16WwyFg6 --- incoming/pattern-study/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index 83fb934e..3311972a 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

패턴 연구

- - + +