From c25633ff26d192455f2e36d32f56b69deaafdda5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:02:26 +0000 Subject: [PATCH 01/10] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=2065=ED=8C=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From f14ad565c55fc10cf4032af63aafe5d69bd90b83 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:41:21 +0000 Subject: [PATCH 02/10] incoming: add Mouse's Tale and Foam Coarsening (pattern round 65) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JkphLb6hZbrmiGJN5Xd --- .../pattern-study/patterns/foam-coarsening.js | 286 ++++++++++++++++++ incoming/pattern-study/patterns/mouse-tale.js | 212 +++++++++++++ 2 files changed, 498 insertions(+) create mode 100644 incoming/pattern-study/patterns/foam-coarsening.js create mode 100644 incoming/pattern-study/patterns/mouse-tale.js diff --git a/incoming/pattern-study/patterns/foam-coarsening.js b/incoming/pattern-study/patterns/foam-coarsening.js new file mode 100644 index 000000000..7037efd7f --- /dev/null +++ b/incoming/pattern-study/patterns/foam-coarsening.js @@ -0,0 +1,286 @@ +// ===== Patternflow pattern ===== +// Title: Foam Coarsening +// Family: cells +// Idea: A fine two-dimensional foam coarsens by Potts-model Monte Carlo on the panel, every cell coloured by how many neighbours it has, so the cells with fewer than six sides glow one colour and shrink away with a spark while the cells with more than six glow the far colour and swell, whatever their size, as von Neumann's law says; when only a fraction of the cells are left a new fine foam is laid down from the top. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: von Neumann's law for two-dimensional foams (John von Neumann, 1952), grain-boundary motion (W. W. Mullins, 1956), Potts-model grain growth (M. P. Anderson, D. J. Srolovitz, G. S. Grest and P. S. Sahni, 1984), pattern round 65 +// Round: 65 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Size=0..1, Noise=0..1 +// +// Knob 1: Hue. Turns the side-count colours round the wheel together. At 0 a six-sided cell is cyan, the shrinking five-, four- and three-sided cells go green, lime and orange, and the growing seven-, eight- and nine-sided cells blue, violet and pink. +// Knob 2: Speed. Monte Carlo sweeps a second, 3.5 at 2 for the finest foam. At the start knobs the 640 seeds leave 608 cells once two sweeps have rounded their walls, and the first new foam comes down after 7.0 s, then one every 8.2 s; at 3 every 5.4 s, at 0.2 every 79 s. A coarser foam gets more sweeps, as the square root of its cell size, since its cells take longer to die. +// Knob 3: Size. How many cells a new foam is laid down with: 640 at 0, 12.8 LEDs each, down to 60 at 1, 137 LEDs each, evenly in the logarithm between. A foam is laid anew when fewer than 45% of its cells are left: every 8.2 s at 0, 19 s at 0.5 and 32 s at 1, at Speed 2. +// Knob 4: Noise. The temperature of the Monte Carlo, from 0.12 to 1.22 of a bond. Low, the walls are smooth arcs that slide; high, they shiver and roughen, a cell can lose a corner and win it back, and the foam coarsens faster, a new one every 6.6 s at 1. + +// In a two-dimensional foam the walls meet three at a time at 120 degrees +// and each wall moves toward its centre of curvature at a speed set by its +// curvature. Add up the turning round a cell with n sides: the walls must +// turn through 2 pi, the corners take n times pi / 3 of it, so the walls' +// curvature sums to (pi / 3)(6 - n), and the area changes as +// dA/dt = kappa (n - 6), independent of the cell's size (von Neumann 1952 for +// soap froth, Mullins 1956 for grain boundaries). A five-sided cell shrinks +// even when it is the biggest on the panel. +// +// The panel runs the large-Q Potts model the way Anderson, Srolovitz, Grest +// and Sahni grew grains in 1984: every LED holds the number of its grain, +// a bond between unlike neighbours costs one, and a Monte Carlo step offers +// an LED the grain of one of its eight neighbours and takes it when the cost +// does not rise, or with exp(-rise / T) when it does. The grid is a torus, +// so no cell touches a wall of the panel's own. +// +// Checked by running this file's own update at the start knobs for 300 s, +// the side counts read every half second and the frames with a front left +// out (two cells are neighbours when one LED face parts them). Grouped by n, +// the mean dA/dt in LEDs a second is -3.15 for four sides, -1.50 for five, +// 0.20 for six, 2.05 for seven, 3.92 for eight, 5.62 for nine and 7.31 for +// ten. The line through them is 1.77 (n - 5.85), r^2 0.9997, which at 3.5 +// sweeps a second is 0.51 LEDs a sweep for each side. Three-sided cells +// shrink at only 2.52: they are a few LEDs across, too small for the lattice +// to bend their walls. The mean side count is 5.87; a lattice cannot draw +// every short wall, so it stays under the 6 Euler's formula gives a torus. + +const W = 64, H = 128, N = W * H, MAXG = 2048, SHIFT = 11; +const { floor, min, exp, sqrt, abs, round, random, pow } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// Moore neighbours on the torus: right, left, down, up, then the corners. +const NB = new Int32Array(N * 8); +const DX = [1, -1, 0, 0, 1, -1, 1, -1], DY = [0, 0, 1, -1, 1, 1, -1, -1]; +for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) + for (let k = 0; k < 8; k++) NB[(y * W + x) * 8 + k] = ((y + DY[k] + H) % H) * W + ((x + DX[k] + W) % W); + +const FRESH = 640, FEWEST = 60, LEFT = 0.45; +const SPARK = 0.35; + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = 255 * (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p); + out[1] = 255 * (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p); + out[2] = 255 * (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p); +} + +export function setup(params) { + params.s = { + g: new Int32Array(N), area: new Int32Array(MAXG), sides: new Uint8Array(MAXG), shown: new Float32Array(MAXG), + keys: new Int32Array(N * 2), list: new Int32Array(FRESH), + alive: 0, C: 0, laid: 0, debt: 0, T: -1, boltz: new Float32Array(9), + front: -1, sx: null, sy: null, sid: null, + sparks: [], hue: 0, speed: 2, c: [0, 0, 0], palette: new Float32Array(13 * 3), first: true, + }; +} + +// The grain of the nearest new seed to an LED centre, on the torus. Only +// the seeds in a band of rows round it are tried: the band is three seed +// spacings each way, and a Poisson foam leaves a disc that wide empty about +// once in 10^12 tries. +function band(S, y) { + const { sy } = S, R = 3 * sqrt(N / sy.length) + 2, list = S.list; + let m = 0; + for (let i = 0; i < sy.length; i++) { + let dy = abs(y - sy[i]); + if (dy > H / 2) dy = H - dy; + if (dy <= R) list[m++] = i; + } + return m; +} + +function nearest(S, x, y, m) { + const { sx, sy, sid, list } = S; + let best = 1e9, id = sid[0]; + for (let j = 0; j < m; j++) { + const i = list[j]; + let dx = abs(x - sx[i]), dy = abs(y - sy[i]); + if (dx > W / 2) dx = W - dx; + if (dy > H / 2) dy = H - dy; + const d = dx * dx + dy * dy; + if (d < best) (best = d), (id = sid[i]); + } + return id; +} + +// Seeds for a new Poisson Voronoi foam, each with a grain number no LED holds. +function newSeeds(S, count) { + const { area } = S; + const sx = new Float32Array(count), sy = new Float32Array(count), sid = new Int32Array(count); + let next = 0; + for (let i = 0; i < count; i++) { + while (area[next] > 0) next++; + S.shown[next] = 99; + sid[i] = next++; + sx[i] = random() * W; + sy[i] = random() * H; + } + S.sx = sx; + S.sy = sy; + S.sid = sid; +} + +function give(S, i, id) { + const { g, area } = S, old = g[i]; + if (old === id) return; + if (--area[old] === 0) S.alive--; + if (area[id]++ === 0) S.alive++; + g[i] = id; +} + +function layRow(S, y) { + const m = band(S, y + 0.5); + for (let x = 0; x < W; x++) give(S, y * W + x, nearest(S, x + 0.5, y + 0.5, m)); +} + +function layAll(S) { + const { g, area } = S; + area.fill(0); + newSeeds(S, S.C); + for (let y = 0; y < H; y++) { + const m = band(S, y + 0.5); + for (let x = 0; x < W; x++) { + const id = nearest(S, x + 0.5, y + 0.5, m); + g[y * W + x] = id; + area[id]++; + } + } + S.alive = 0; + for (let i = 0; i < MAXG; i++) if (area[i] > 0) S.alive++; + S.laid = S.C; + for (let i = 0; i < MAXG; i++) S.shown[i] = 0; +} + +// Monte Carlo attempts: an LED is offered one neighbour's grain. +function steps(S, count) { + const { g, area, boltz, sparks } = S; + for (let a = 0; a < count; a++) { + const i = (random() * N) | 0, o = i * 8; + const b = g[NB[o + ((random() * 8) | 0)]], c = g[i]; + if (b === c) continue; + let rise = 0; + for (let k = 0; k < 8; k++) { + const q = g[NB[o + k]]; + if (q === c) rise++; + else if (q === b) rise--; + } + if (rise > 0 && random() >= boltz[rise]) continue; + g[i] = b; + area[b]++; + if (--area[c] === 0) { + S.alive--; + if (sparks.length < 64) sparks.push({ x: i % W, y: (i / W) | 0, life: SPARK }); + } + } +} + +// The side count of every grain: grains are neighbours when one LED face +// parts them. Pairs are packed into one integer, sorted and counted once. +function countSides(S) { + const { g, keys, sides } = S; + let m = 0; + for (let i = 0; i < N; i++) { + const a = g[i], r = g[NB[i * 8]], d = g[NB[i * 8 + 2]]; + if (r !== a) keys[m++] = a < r ? (a << SHIFT) | r : (r << SHIFT) | a; + if (d !== a) keys[m++] = a < d ? (a << SHIFT) | d : (d << SHIFT) | a; + } + const pairs = keys.subarray(0, m).sort(); + sides.fill(0); + let prev = -1; + for (let j = 0; j < m; j++) { + const k = pairs[j]; + if (k === prev) continue; + prev = k; + sides[k >> SHIFT]++; + sides[k & (MAXG - 1)]++; + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0]; + S.speed = cl(kv[1], 0.2, 3); + S.C = round(FRESH * pow(FEWEST / FRESH, cl(kv[2], 0, 1))); + const T = 0.12 + 1.1 * cl(kv[3], 0, 1); + if (T !== S.T) { + S.T = T; + for (let k = 0; k < 9; k++) S.boltz[k] = exp(-k / T); + } + if (S.first) { + S.first = false; + layAll(S); + steps(S, 2 * N); + countSides(S); + for (let i = 0; i < MAXG; i++) S.shown[i] = S.sides[i] - 6; + } + + // A new foam comes down from the top as a front, row by row. + if (S.front < 0 && (S.alive < LEFT * S.laid || abs(S.C - S.laid) > 0.25 * S.laid)) { + newSeeds(S, S.C); + S.laid = S.C; + S.front = 0; + } + if (S.front >= 0) { + const to = min(H, S.front + (d * S.speed * H) / 2.4); + for (let y = floor(S.front); y < floor(to); y++) layRow(S, y); + S.front = to >= H ? -1 : to; + } + + // Sweeps a second, faster for a coarse foam so its cells still come and go. + const rate = 1.75 * S.speed * sqrt(FRESH / S.laid); + S.debt += rate * d * N; + const n = floor(S.debt); + S.debt -= n; + steps(S, n); + countSides(S); + + const ease = min(1, d * 8); + for (let i = 0; i < MAXG; i++) { + if (S.area[i] === 0) continue; + const want = cl(S.sides[i] - 6, -3, 3); + // A grain just laid takes its colour at once. + S.shown[i] = S.shown[i] > 50 ? want : S.shown[i] + (want - S.shown[i]) * ease; + } + for (const s of S.sparks) s.life -= d * S.speed * 0.5; + S.sparks = S.sparks.filter((s) => s.life > 0); + + // Thirteen colours for n - 6 from -3 to 3 in half steps. + for (let j = 0; j < 13; j++) { + const v = j / 2 - 3; + hsv(S.hue + 0.5 + 0.13 * v, v === 0 ? 0.45 : 0.9, v === 0 ? 0.85 : 1, S.c); + S.palette[j * 3] = S.c[0]; + S.palette[j * 3 + 1] = S.c[1]; + S.palette[j * 3 + 2] = S.c[2]; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || S.first) return; + const { g, shown, palette } = S; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x, a = g[i]; + // A wall is the LED whose right or lower neighbour is another grain. + if (g[NB[i * 8]] !== a || g[NB[i * 8 + 2]] !== a) { + display.setPixel(x, y, 0, 0, 0); + continue; + } + const j = round((shown[a] + 3) * 2) * 3; + display.setPixel(x, y, palette[j], palette[j + 1], palette[j + 2]); + } + // A cell that has just shrunk away flashes white where it closed. + for (const s of S.sparks) { + const v = 255 * (s.life / SPARK); + display.setPixel(s.x, s.y, v, v, v); + if (s.life > SPARK / 2) { + const u = v * 0.45; + display.setPixel((s.x + 1) % W, s.y, u, u, u); + display.setPixel((s.x + W - 1) % W, s.y, u, u, u); + display.setPixel(s.x, (s.y + 1) % H, u, u, u); + display.setPixel(s.x, (s.y + H - 1) % H, u, u, u); + } + } +} diff --git a/incoming/pattern-study/patterns/mouse-tale.js b/incoming/pattern-study/patterns/mouse-tale.js new file mode 100644 index 000000000..70da04608 --- /dev/null +++ b/incoming/pattern-study/patterns/mouse-tale.js @@ -0,0 +1,212 @@ +// ===== Patternflow pattern ===== +// Title: Mouse's Tale +// Family: type +// Idea: Lewis Carroll's Mouse's Tale hangs down the panel as the tail it was printed as, one or two words a line, each line set where the tail's curve has got to and in smaller type the lower it hangs, from a hand-drawn 5 by 7 face through 4 by 6 and 3 by 5 to the bare shapes of the words, a dot a letter and a dot a word at the tip, while the tail sways slowly and the poem runs down it, new lines coming in at the top and the last ones fading at the tip. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: The Mouse's Tale, a figured poem (Lewis Carroll, Alice's Adventures in Wonderland, 1865), pattern round 65 +// Round: 65 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Sway=1..3, Taper=0..1 +// +// Knob 1: Hue. Turns the ink round the wheel. At 0 the top of the tail is aqua, and the ink turns through green to a yellow at the tip, deeper the lower it hangs. +// Knob 2: Speed. How fast the poem runs down the tail: half a line a second at 2, so the 53 lines come round every 106 s. At 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Sway. How far the tail swings. At 1 it bends gently and sways once every 9 s; at 3 the bends reach 1.6 times as far, the tip lashes from wall to wall, and it sways once every 4.5 s. +// Knob 4: Taper. How soon the type shrinks. Near 0 the 5 by 7 face holds the top 54 rows, six lines, then two lines of 4 by 6, three of 3 by 5, four of word shapes, six of a dot a letter and five of a dot a word: 26 lines. At 1 only the top line is big, and 42 lines hang there, 30 of them dots. + +// Carroll set the tale in Chapter III so that it reads down a mouse's long +// tail: short lines, each shifted to follow the curve, and the type stepping +// down in size until the last words are tiny. Here the poem loops: the line +// that comes in at the top is the one before the top line, so the tail always +// reads downward in the poem's order, and the last line, death, sits just +// above the first, Fury said. +// +// The text is the 1865 tale as the common reprints give it, with the italic +// "you" set plain. The three faces are drawn here, 5 by 7, 4 by 6 and 3 by 5, +// each with its descenders lifted into the cell as small LCD faces do. Below +// those every letter keeps only its word shape on 2 by 3: an ascender LED, +// a body LED or two, a descender LED. Then one LED a letter, then one a word. + +const W = 64, H = 128; +const { floor, round, min, max, sin, PI, pow } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = 255 * (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p); + out[1] = 255 * (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p); + out[2] = 255 * (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p); +} + +// The three drawn faces. Each glyph is its width, then one base-32 digit a +// row from the top, the leftmost LED the highest bit: "a" in 5 by 7 is +// 5, then 0 0 e 1 f h f, rows ..... ..... .###. ....# .#### #...# .#### +const FACES = [ + { "a": "500e1fhf", "b": "5ggmphhu", "c": "500egghe", "d": "511djhhf", "e": "500ehvge", "f": "5698s888", "g": "50fhhf1e", "h": "5ggmphhh", "i": "540c444e", "j": "520622ic", "k": "5ggikoki", "l": "5c44444e", "m": "500qllhh", "n": "500mphhh", "o": "500ehhhe", "p": "500uhugg", "r": "500mpggg", "s": "500ege1u", "t": "588s8896", "u": "500hhhjd", "v": "500hhha4", "w": "500hhlla", "y": "500hhf1e", "C": "5ehggghe", "F": "5vgguggg", "I": "5e44444e", "L": "5ggggggv", "S": "5fgge11u", "T": "5v444444", "W": "5hhhllla", ",": "20000312", ".": "20000033", "'": "23120000", ":": "20330033", ";": "20330312", "-": "5000v000", "\"": "5r9i0000" }, + { "a": "4007997", "b": "488e99e", "c": "4007887", "d": "4117997", "e": "4006f87", "f": "434e444", "g": "4079716", "h": "488e999", "i": "1101111", "j": "3101152", "k": "4889ae9", "l": "2222221", "m": "500ulll", "n": "400e999", "o": "4006996", "p": "40e9e88", "r": "3005644", "s": "4007c3e", "t": "3227221", "u": "4009997", "v": "3005552", "w": "500hlla", "y": "4099716", "C": "4788887", "F": "4f8e888", "I": "3722227", "L": "488888f", "S": "478611e", "T": "3722222", "W": "5hhllla", ",": "2000012", ".": "1000001", "'": "2120000", ":": "1001001", ";": "2001012", "-": "4000f00", "\"": "3550000" }, + { "a": "300353", "b": "344656", "c": "300343", "d": "311353", "e": "300376", "f": "212322", "g": "303536", "h": "344655", "i": "110111", "j": "210112", "k": "344565", "l": "111111", "m": "500ull", "n": "300655", "o": "300252", "p": "306564", "r": "200322", "s": "300326", "t": "322723", "u": "300553", "v": "300552", "w": "500lla", "y": "305536", "C": "334443", "F": "374644", "I": "372227", "L": "344447", "S": "334216", "T": "372222", "W": "5hhlla", ",": "100011", ".": "100001", "'": "111000", ":": "101001", ";": "101011", "-": "300700", "\"": "355000" }, +]; + +// The six sizes down the tail: the three faces, word shapes on 2 by 3, a +// dot a letter, a dot a word. Height, line pitch, the width of a space and +// the gap between letters. +const SIZES = [ + { h: 7, pitch: 9, space: 4, gap: 1 }, + { h: 6, pitch: 8, space: 3, gap: 1 }, + { h: 5, pitch: 6, space: 3, gap: 1 }, + { h: 3, pitch: 4, space: 2, gap: 1 }, + { h: 1, pitch: 2, space: 1, gap: 0 }, + { h: 1, pitch: 2, space: 1, gap: 0 }, +]; +const LAST = SIZES.length - 1; + +// Word shapes: a letter on 2 by 3 keeps whether it rises, sits or hangs. +const RISE = "bdfhklt", HANG = "gjpy", NARROW = "fijlrt", RIGHT = "dgjy"; +function shape(ch) { + if (ch === "I") return [1, [1, 1, 0]]; + if (ch === "W") return [3, [7, 7, 0]]; + if (ch >= "A" && ch <= "Z") return [2, [3, 3, 0]]; + if (ch === ",") return [1, [0, 0, 1]]; + if (ch === "'" || ch === '"') return [1, [1, 0, 0]]; + if (ch === "-") return [2, [0, 3, 0]]; + if (".:;".includes(ch)) return [1, [0, 1, 0]]; + const w = NARROW.includes(ch) ? 1 : ch === "m" || ch === "w" ? 3 : 2; + const stem = w === 1 ? 1 : RIGHT.includes(ch) ? 1 : 1 << (w - 1); + return [w, [RISE.includes(ch) ? stem : 0, (1 << w) - 1, HANG.includes(ch) ? stem : 0]]; +} + +function glyph(size, ch) { + if (size >= 3) return shape(ch); + const code = FACES[size][ch]; + if (!code) return [2, [0, 0, 0, 0, 0, 0, 0].slice(0, SIZES[size].h)]; + const rows = []; + for (let i = 1; i < code.length; i++) rows.push(parseInt(code[i], 32)); + return [+code[0], rows]; +} + +// One line drawn at one size: width, height and its lit LEDs. +function render(text, size) { + const S = SIZES[size], h = S.h; + if (size === LAST) { + const words = text.split(" ").length, w = 2 * words - 1, bits = new Uint8Array(w); + for (let i = 0; i < words; i++) bits[2 * i] = 1; + return { w, h, bits }; + } + const parts = []; + let w = 0, joined = false; + for (const ch of text) { + if (ch === " ") { + w += S.space; + joined = false; + continue; + } + if (joined) w += S.gap; + const [gw, rows] = size === 4 ? [1, [1]] : glyph(size, ch); + parts.push({ x: w, gw, rows }); + w += gw; + joined = true; + } + const bits = new Uint8Array(max(1, w) * h); + for (const p of parts) + for (let r = 0; r < h; r++) + for (let c = 0; c < p.gw; c++) if ((p.rows[r] >> (p.gw - 1 - c)) & 1) bits[r * w + p.x + c] = 1; + return { w, h, bits }; +} + +// The tale, broken into lines of one or two words that fit 50 LEDs in the +// largest face. The em dash after "you." is set as a dash. +const TALE = + "'Fury said to a mouse, That he met in the house, \"Let us both go to law: I will prosecute you.- Come, I'll take no denial; " + + "We must have a trial: For really this morning I've nothing to do.\" Said the mouse to the cur, \"Such a trial, dear Sir, " + + "With no jury or judge, would be wasting our breath.\" \"I'll be judge, I'll be jury,\" Said cunning old Fury: " + + "\"I'll try the whole cause, and condemn you to death.\"'"; + +function breakLines() { + const words = TALE.split(" "), lines = []; + for (let i = 0; i < words.length; ) { + const two = i + 1 < words.length ? words[i] + " " + words[i + 1] : null; + if (two && render(two, 0).w <= 50) (lines.push(two), (i += 2)); + else lines.push(words[i++]); + } + return lines; +} + +export function setup(params) { + const lines = breakLines(); + params.s = { + lines, + drawn: lines.map((t) => SIZES.map((_, k) => render(t, k))), + q: 0, t: 0, hue: 0, speed: 2, sway: 1, taper: 0.06, + Y: new Float32Array(80), buf: new Float32Array(W * H * 3), c: [0, 0, 0], + }; +} + +// How far down the sizes the tail is at row y, 0 at the top to 5.9 at the +// tip; the size is its whole part. +const depth = (S, y) => 5.9 * pow(cl(y / H, 0, 1), 1.85 - 1.25 * S.taper); +const sizeAt = (S, y) => min(LAST, floor(depth(S, y))); + +// The tail's centre line at row y and time t. +function centre(S, y) { + const u = cl(y / H, 0, 1), z = depth(S, y), s = S.sway; + const room = 6 + 4.6 * z; + const wag = 0.32 * s * sin(S.t * ((2 * PI) / 9) * (1 + 0.5 * (s - 1)) - 2.4 * u) * (0.4 + u); + return W / 2 + room * (0.62 + 0.19 * (s - 1)) * sin(2 * PI * (0.55 * u + 0.8 * u * u) + 0.7 + wag); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 1, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0]; + S.speed = cl(kv[1], 0.2, 3); + S.sway = cl(kv[2], 1, 3); + S.taper = cl(kv[3], 0, 1); + S.t += d; + S.q += (d * S.speed) / 4; + // Line tops down the tail, each a pitch of its own size below the last. + const Y = S.Y; + Y[0] = 2; + let n = 0; + while (Y[n] < H + 2 && n < Y.length - 1) { + Y[n + 1] = Y[n] + SIZES[sizeAt(S, Y[n])].pitch; + n++; + } + S.slots = n; +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.slots) return; + const { buf, Y, c, lines, drawn } = S, L = lines.length; + buf.fill(0); + const Q = floor(S.q), f = S.q - Q; + for (let i = -1; i < S.slots; i++) { + const y0 = i < 0 ? Y[0] - SIZES[0].pitch : Y[i], y1 = Y[i + 1]; + const y = round(y0 + f * (y1 - y0)); + if (y >= H) break; + const size = sizeAt(S, max(0, y)), z = depth(S, y), img = drawn[(((i - Q) % L) + L) % L][size]; + const x = cl(round(centre(S, y + img.h / 2) - img.w / 2), 0, W - img.w); + const u = cl(y / H, 0, 1); + // The ink deepens down the tail, and the last dots fade at the tip. + hsv(S.hue + 0.47 - 0.3 * u, 0.65 + 0.25 * u, cl((6.1 - z) / 0.9, 0.2, 1), c); + for (let r = 0; r < img.h; r++) { + const yy = y + r; + if (yy < 0 || yy >= H) continue; + for (let k = 0; k < img.w; k++) { + if (!img.bits[r * img.w + k]) continue; + const o = (yy * W + x + k) * 3; + buf[o] = c[0]; + buf[o + 1] = c[1]; + buf[o + 2] = c[2]; + } + } + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const o = (y * W + x) * 3; + display.setPixel(x, y, buf[o], buf[o + 1], buf[o + 2]); + } +} From c43180207a6b3165cf54a5fefdbdeffcfb506d3f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:41:52 +0000 Subject: [PATCH 03/10] incoming: add Inversions and Klein's Sails (pattern round 65) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JkphLb6hZbrmiGJN5Xd --- incoming/pattern-study/patterns/inversions.js | 284 ++++++++++++++++++ .../pattern-study/patterns/klein-sails.js | 282 +++++++++++++++++ 2 files changed, 566 insertions(+) create mode 100644 incoming/pattern-study/patterns/inversions.js create mode 100644 incoming/pattern-study/patterns/klein-sails.js diff --git a/incoming/pattern-study/patterns/inversions.js b/incoming/pattern-study/patterns/inversions.js new file mode 100644 index 000000000..d5f038afd --- /dev/null +++ b/incoming/pattern-study/patterns/inversions.js @@ -0,0 +1,284 @@ +// ===== Patternflow pattern ===== +// Title: Inversions +// Family: type +// Idea: A two-syllable word stands on the panel, one 64 x 64 syllable above the other, and the whole panel turns slowly about its centre, so that at half a turn the same strokes read as another word, 곰국 (bone broth) as 논문 (a thesis), rests there, and turns on back to the first. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: rotational lettering, Inversions (Scott Kim, 1981), pattern round 65 +// Round: 65 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Word=0..8, Weight=-1..1 +// +// Knob 1: Hue. The colour belongs to the panel, not to the word: a gradient runs from the top row to the bottom one, at 0 from sea green through cyan to blue, and the letters pass through it as they turn, so the syllable on top is always the top colour. The light a stroke leaves behind as it swings is a deeper shade of the same row. +// Knob 2: Speed. At 2 a half turn takes 1.6 s and the word rests 2 s at each end, so it reads the second word from 1.95 s to 3.95 s and the first again from 5.55 s. At 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Word. Nine words, each checked LED by LED against its turned reading: 곰국 turns into 논문, 공공 into 운운, 눅눅 into 녹녹, 용융 into itself, MOM into WOW, and NOON, SWIMS, pod and suns into themselves. +// Knob 4: Weight. The pen, from 2.1 LEDs at -1 through 5.9 at the start to 9.1 at 1, for Hangul and three letters; NOON and suns take 0.85 of it and SWIMS 0.7. The strokes are round-capped lines and rings sampled 16 times an LED, and the turned bitmap equals the other word's at every weight. +// Buttons: K1 tap starts the next half turn at once. + +const W = 64, H = 128, N = W * H, SS = 4; +const { floor, round, min, max, abs, sqrt, sin, cos, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ease = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + out[o + 1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + out[o + 2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; +} + +// The word list. Each turns half round into the word in the comment; the +// knob line above says how the pairs were checked. +const WORDS = ["곰국", "공공", "눅눅", "용융", "MOM", "NOON", "SWIMS", "pod", "suns"]; +// 논문, 운운, 녹녹, 용융, WOW, NOON, SWIMS, pod, suns + +// Primitives, all in panel coordinates with integer or half-integer ends so +// that turning about (32, 64) maps every one exactly onto another: +// [1, x0, x1, y] a horizontal stroke, [2, y0, y1, x] a vertical one, +// [0, x0, y0, x1, y1] a slanted one, [3, cx, cy, rx, ry, cut, flip] an +// elliptic ring, cut 0 whole, 1 its upper half, 2 the bowl of an S; flip +// reads the cut turned half round. +function line(out, x0, y0, x1, y1) { + if (y0 === y1) out.push([1, min(x0, x1), max(x0, x1), y0]); + else if (x0 === x1) out.push([2, min(y0, y1), max(y0, y1), x0]); + else out.push([0, x0, y0, x1, y1]); +} +const ring = (out, cx, cy, rx, ry, cut, flip) => out.push([3, cx, cy, rx, ry, cut || 0, flip || 0]); + +// ---- Hangul ---- +// A syllable with a horizontal vowel is three rows in its 64 x 64 cell: the +// initial consonant in a box at the top, the vowel's bar across the middle, +// the final in a box at the bottom. The bottom box is the top box turned half +// round the cell's centre (32, 32), and the bars of ㅗ and ㅜ sit two rows +// either side of row 32, so a turned syllable swaps its initial and final, +// turns each over, and is a syllable again when the turned shapes are jamo: +// ㄱ turns into ㄴ, ㅁ ㅇ ㄹ ㅍ into themselves, ㅗ into ㅜ, ㅛ into ㅠ, ㅡ stays. +const CHO = { 0: "g", 2: "n", 5: "r", 6: "m", 11: "o", 17: "p" }; +const JONG = { 1: "g", 4: "n", 8: "r", 16: "m", 21: "o", 26: "p" }; +const BOX_T = 6, BOX_B = 18, BOX_L = 11, BOX_R = 53; + +function consonant(out, k, l, t, r, b) { + const m = (t + b) / 2; + if (k === "g") line(out, l, t, r, t), line(out, r, t, r, b); + else if (k === "n") line(out, l, t, l, b), line(out, l, b, r, b); + else if (k === "m") line(out, l, t, r, t), line(out, r, t, r, b), line(out, l, b, r, b), line(out, l, t, l, b); + else if (k === "o") ring(out, (l + r) / 2, m, 13, (b - t) / 2); + else if (k === "r") line(out, l, t, r, t), line(out, r, t, r, m), line(out, l, m, r, m), line(out, l, m, l, b), line(out, l, b, r, b); + else if (k === "p") line(out, l, t, r, t), line(out, l, b, r, b), line(out, l + 10, t, l + 10, b), line(out, r - 10, t, r - 10, b); +} + +function vowel(out, v, oy) { + // 8 ㅗ, 12 ㅛ, 13 ㅜ, 17 ㅠ, 18 ㅡ. + if (v === 18) return line(out, 6, oy + 32, 58, oy + 32); + const up = v === 8 || v === 12, bar = oy + (up ? 35.5 : 28.5), tip = oy + (up ? 28.5 : 35.5); + line(out, 6, bar, 58, bar); + for (const x of v === 8 || v === 13 ? [32] : [24, 40]) line(out, x, tip, x, bar); +} + +function syllable(out, ch, oy) { + const c = ch.charCodeAt(0) - 0xac00, cho = floor(c / 588), jung = floor((c % 588) / 28), jong = c % 28; + consonant(out, CHO[cho], BOX_L, oy + BOX_T, BOX_R, oy + BOX_B); + vowel(out, jung, oy); + consonant(out, JONG[jong], BOX_L, oy + 64 - BOX_B, BOX_R, oy + 64 - BOX_T); +} + +// ---- Latin ---- +// Letters stand one above another at a whole-number pitch centred on row 64, +// so the turn sends slot i to slot n - 1 - i. M is drawn with upright sides +// so that W is the same strokes turned, n is an arch on two stems and u the +// arch turned, p and d share one bowl and a stem the length of the other's. +function letter(out, ch, l, t, r, b, a) { + const c = (l + r) / 2, m = (t + b) / 2, hx = (r - l) / 2, q = (b - t) / 4, v = round(0.62 * (b - t)); + if (ch === "N") line(out, l, b, l, t), line(out, l, t, r, b), line(out, r, b, r, t); + else if (ch === "O" || ch === "o") ring(out, c, m, hx, (b - t) / 2); + else if (ch === "I") line(out, c, t, c, b), line(out, c - 7, t, c + 7, t), line(out, c - 7, b, c + 7, b); + else if (ch === "M") line(out, l, b, l, t), line(out, l, t, c, t + v), line(out, c, t + v, r, t), line(out, r, t, r, b); + else if (ch === "W") line(out, l, t, l, b), line(out, l, b, c, b - v), line(out, c, b - v, r, b), line(out, r, b, r, t); + else if (ch === "S" || ch === "s") ring(out, c, t + q, hx, q, 2, 0), ring(out, c, b - q, hx, q, 2, 1); + else if (ch === "n") line(out, l, b, l, t + hx), line(out, r, b, r, t + hx), ring(out, c, t + hx, hx, hx, 1, 0); + else if (ch === "u") line(out, l, t, l, b - hx), line(out, r, t, r, b - hx), ring(out, c, b - hx, hx, hx, 1, 1); + else if (ch === "p") ring(out, c, m, hx, (b - t) / 2), line(out, l, t, l, b + a); + else if (ch === "d") ring(out, c, m, hx, (b - t) / 2), line(out, r, t - a, r, b); +} + +const LATIN = { + 3: { pitch: 42, pen: 1, caps: [12, 52, 34], low: [17, 47, 18, 9] }, + 4: { pitch: 32, pen: 0.85, caps: [12, 52, 24], low: [15, 49, 22, 0] }, + 5: { pitch: 25, pen: 0.7, caps: [13, 51, 16], low: [16, 48, 14, 0] }, +}; + +function wordPrims(word) { + const out = []; + if (word.charCodeAt(0) >= 0xac00) { + syllable(out, word[0], 0); + syllable(out, word[1], 64); + return out; + } + const n = word.length, L = LATIN[n]; + for (let i = 0; i < n; i++) { + const ch = word[i], yc = 64 + (i - (n - 1) / 2) * L.pitch; + const caps = ch < "a", [l, r, h, a] = caps ? [L.caps[0], L.caps[1], L.caps[2], 0] : L.low; + const narrow = ch === "S" ? (n === 5 ? 7 : 4) : ch === "s" ? 4 : 0; + letter(out, ch, l + narrow, yc - h / 2, r - narrow, yc + h / 2, a); + } + return out; +} + +// The pen for a weight, thinner for four and five letters, in eighths of an +// LED with an odd numerator: samples sit on odd eighths, so no sample is ever +// exactly half a pen from a straight stroke and none can tip either way. +function penOf(word, weight) { + const scale = word.charCodeAt(0) >= 0xac00 ? 1 : LATIN[word.length].pen; + return (2 * round((5.6 + 3.5 * weight) * scale * 4) + 1) / 8; +} + +// One sample against one primitive: within half the pen of the stroke. +function inside(p, px, py, hw, hw2) { + const k = p[0]; + if (k === 1) { + const dx = px < p[1] ? p[1] - px : px > p[2] ? px - p[2] : 0, dy = py - p[3]; + return dx * dx + dy * dy <= hw2; + } + if (k === 2) { + const dy = py < p[1] ? p[1] - py : py > p[2] ? py - p[2] : 0, dx = px - p[3]; + return dx * dx + dy * dy <= hw2; + } + if (k === 0) { + const ax = p[1], ay = p[2], ux = p[3] - ax, uy = p[4] - ay, l2 = ux * ux + uy * uy; + const t = cl(((px - ax) * ux + (py - ay) * uy) / l2, 0, 1), ex = ax + t * ux - px, ey = ay + t * uy - py; + return ex * ex + ey * ey <= hw2; + } + let dx = px - p[1], dy = py - p[2]; + if (p[6]) (dx = -dx), (dy = -dy); + const rx = p[3], ry = p[4], nx = dx / rx, ny = dy / ry; + if (p[5] === 1 && dy > 0) return false; + // The S bowl leaves out its lower right quarter and the upper right up to + // about 35 degrees, where the stroke ends. + if (p[5] === 2 && dx > 0 && (dy > 0 || -ny < 0.7 * nx)) return false; + const q = sqrt(nx * nx + ny * ny), g = sqrt((nx / rx) * (nx / rx) + (ny / ry) * (ny / ry)); + if (q === 0) return min(rx, ry) <= hw; + return (abs(q - 1) * q) / g <= hw; +} + +// The word's bitmap: 16 samples an LED, the count of those inside a stroke. +function render(prims, pen, out) { + const hw = pen / 2, hw2 = hw * hw, bits = new Uint16Array(N); + for (const p of prims) { + let x0, x1, y0, y1; + if (p[0] === 1) (x0 = p[1]), (x1 = p[2]), (y0 = y1 = p[3]); + else if (p[0] === 2) (y0 = p[1]), (y1 = p[2]), (x0 = x1 = p[3]); + else if (p[0] === 0) (x0 = min(p[1], p[3])), (x1 = max(p[1], p[3])), (y0 = min(p[2], p[4])), (y1 = max(p[2], p[4])); + else (x0 = p[1] - p[3]), (x1 = p[1] + p[3]), (y0 = p[2] - p[4]), (y1 = p[2] + p[4]); + const X0 = max(0, floor(x0 - hw - 1)), X1 = min(W - 1, floor(x1 + hw + 1)); + const Y0 = max(0, floor(y0 - hw - 1)), Y1 = min(H - 1, floor(y1 + hw + 1)); + for (let y = Y0; y <= Y1; y++) + for (let x = X0; x <= X1; x++) { + const o = y * W + x; + let b = bits[o]; + if (b === 0xffff) continue; + for (let j = 0; j < SS; j++) + for (let i = 0; i < SS; i++) { + const bit = 1 << (j * SS + i); + if (!(b & bit) && inside(p, x + (i + 0.5) / SS, y + (j + 0.5) / SS, hw, hw2)) b |= bit; + } + bits[o] = b; + } + } + for (let o = 0; o < N; o++) { + let b = bits[o], c = 0; + while (b) (b &= b - 1), c++; + out[o] = c; + } + return out; +} + +// Beats at Speed 2, in seconds: the rest at each end and the half turn. +const REST = 2, TURN = 1.6; + +export function setup(params) { + params.s = { + word: -1, pen: -1, hits: new Uint8Array(N), cov: new Float32Array(N), trail: new Float32Array(N), + ph: REST - 0.35, ang: 0, hue: -1, row: new Float32Array(H * 3), ghost: new Float32Array(H * 3), + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + const word = cl(round(kv[2]), 0, WORDS.length - 1), pen = penOf(WORDS[word], cl(kv[3], -1, 1)); + if (word !== S.word || pen !== S.pen) { + S.word = word; + S.pen = pen; + render(wordPrims(WORDS[word]), pen, S.hits); + } + if (input.btnPressed && input.btnPressed[0] && S.rest) S.ph = S.half * (REST + TURN) + REST; + S.ph += (d * cl(kv[1], 0.2, 3)) / 2; + const k = floor(S.ph / (REST + TURN)), u = S.ph - k * (REST + TURN); + S.rest = u < REST; + S.half = k; + S.ang = (k + (S.rest ? 0 : ease((u - REST) / TURN))) * PI; + S.dt = d; + if (kv[0] !== S.hue) { + S.hue = kv[0]; + for (let y = 0; y < H; y++) { + const h = S.hue + 0.42 + 0.2 * (y / (H - 1)); + hsv(h, 0.72, 1, S.row, y * 3); + hsv(h + 0.07, 1, 0.5, S.ghost, y * 3); + } + } +} + +// Bilinear read of the word's bitmap at word-space (x, y), LED centres on +// whole numbers. +function sample(hits, x, y) { + const x0 = floor(x), y0 = floor(y), fx = x - x0, fy = y - y0; + let v = 0; + if (y0 >= 0 && y0 < H) { + const o = y0 * W; + if (x0 >= 0 && x0 < W) v += hits[o + x0] * (1 - fx) * (1 - fy); + if (x0 + 1 >= 0 && x0 + 1 < W) v += hits[o + x0 + 1] * fx * (1 - fy); + } + if (y0 + 1 >= 0 && y0 + 1 < H) { + const o = (y0 + 1) * W; + if (x0 >= 0 && x0 < W) v += hits[o + x0] * (1 - fx) * fy; + if (x0 + 1 >= 0 && x0 + 1 < W) v += hits[o + x0 + 1] * fx * fy; + } + return v; +} + +export function draw(display, params) { + const S = params.s; + if (!S || S.word < 0) return; + const { hits, cov, trail, row, ghost } = S; + if (S.rest) { + // At rest the panel shows the bitmap itself, or the bitmap read from the + // far corner: LED (x, y) shows LED (63 - x, 127 - y). + const flip = S.half % 2 === 1; + for (let o = 0; o < N; o++) cov[o] = hits[flip ? N - 1 - o : o] / 16; + } else { + // Turning, the word shrinks so its 128 rows lie down across 64 columns + // at a quarter turn, losing a row or two of each end against the sides. + const a = S.ang, sn = sin(a), s = 1 - 0.47 * sn * sn, c = cos(a) / s, si = sn / s; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let v = 0; + for (let j = 0; j < 2; j++) + for (let i = 0; i < 2; i++) { + const u = x + 0.25 + 0.5 * i - 32, w = y + 0.25 + 0.5 * j - 64; + v += sample(hits, 31.5 + c * u + si * w, 63.5 - si * u + c * w); + } + cov[y * W + x] = v / 64; + } + } + const fade = exp(-S.dt / 0.09); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const o = y * W + x, c = cov[o], t = max(c, trail[o] * fade); + trail[o] = t; + const g = t - c; + display.setPixel(x, y, row[y * 3] * c + ghost[y * 3] * g, row[y * 3 + 1] * c + ghost[y * 3 + 1] * g, row[y * 3 + 2] * c + ghost[y * 3 + 2] * g); + } +} diff --git a/incoming/pattern-study/patterns/klein-sails.js b/incoming/pattern-study/patterns/klein-sails.js new file mode 100644 index 000000000..e712911e2 --- /dev/null +++ b/incoming/pattern-study/patterns/klein-sails.js @@ -0,0 +1,282 @@ +// ===== Patternflow pattern ===== +// Title: Klein's Sails +// Family: number +// Idea: The integer lattice flows along hyperbolas under the real powers of an integer matrix that fixes the line y = ax and its conjugate, with the X of the two lines standing lengthwise on the panel, and in each corner of the X the boundary of the convex hull of the lattice points, Klein's sail, slides along itself, its corners on the convergents of a towards y = ax, until after one unit of flow every point and every corner sits where another one was and the loop closes. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Klein's sails of continued fractions (Felix Klein, 1895), periodic continued fractions of quadratic irrationals (Joseph-Louis Lagrange, 1770), pattern round 65 +// Round: 65 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Number=0..5, Zoom=10..22 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the sail below the line y = ax and its copy through the centre are azure, the other two rose, the two lines a near-white lilac and the lattice points dim teal with short tails along their hyperbolas. +// Knob 2: Speed. The flow in hyperbolic angle, 0.32 a second at 2, so a point keeps its product of distances to the two lines and moves 37% further out along y = ax each second. One loop takes log e seconds over 0.32: 5.5 s for the square root of 2, 3.0 s for the golden ratio. At 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Number. Which quadratic irrational a, with its continued fraction and the matrix M that loops the flow: 0 the square root of 2 = [1; 2, 2, ...] with M = [[3, 2], [4, 3]], 1 the golden ratio [1; 1, 1, ...] with [[1, 1], [1, 2]], 2 the square root of 3 = [1; 1, 2, 1, 2, ...] with [[2, 1], [3, 2]], 3 the square root of 5 = [2; 4, 4, ...] with [[9, 4], [20, 9]], 4 the square root of 7 = [2; 1, 1, 1, 4, ...] with [[8, 3], [21, 8]], 5 (1 + square root of 13) / 2 = [2; 3, 3, ...] with [[4, 3], [9, 7]]. A sail edge runs through a_k - 1 lattice points between its corners, and its a_k is written beside it when there is room. +// Knob 4: Zoom. LEDs between neighbouring lattice points, 10 to 22. At 10 the panel is 6.4 lattice steps wide and each sail keeps two or three corners on it for the square root of 2, four for the golden ratio; at 22 it is 2.9 steps wide with one or two corners a sail, and an edge is long enough to count the lattice points along it. + +const W = 64, H = 128; +const { floor, ceil, round, min, max, abs, sqrt, exp, log, atan, cos, sin, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + out[1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + out[2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; + return out; +} + +// a is the larger root of a^2 = P a + Q, and the unit e = A + B a of Z[a] +// is the fundamental one. Multiplying by e in the basis 1, a is the integer +// matrix [[A, B], [B Q, A + B P]] whose determinant is the norm of e; it +// sends (1, a) to e (1, a) and (1, a') to e' (1, a'). With norm -1 it swaps +// two corners of the X, so the flow loops on its square. +const NUMS = [ + { P: 0, Q: 2, A: 1, B: 1 }, + { P: 1, Q: 1, A: 0, B: 1 }, + { P: 0, Q: 3, A: 2, B: 1 }, + { P: 0, Q: 5, A: 2, B: 1 }, + { P: 0, Q: 7, A: 8, B: 3 }, + { P: 1, Q: 3, A: 1, B: 1 }, +]; + +const gcd = (a, b) => { + a = abs(a); + b = abs(b); + while (b) [a, b] = [b, a % b]; + return a; +}; +const mul = (m, v) => [m[0] * v[0] + m[1] * v[1], m[2] * v[0] + m[3] * v[1]]; + +function prepare(num) { + const { P, Q, A, B } = num, root = sqrt(P * P + 4 * Q); + const al = (P + root) / 2, al2 = (P - root) / 2; + let M = [A, B, B * Q, A + B * P], e = A + B * al; + if (M[0] * M[3] - M[1] * M[2] < 0) (M = [M[0] * M[0] + M[1] * M[2], M[0] * M[1] + M[1] * M[3], M[2] * M[0] + M[3] * M[2], M[2] * M[1] + M[3] * M[3]]), (e *= e); + const Mi = [M[3], -M[1], -M[2], M[0]]; + // Eigen-coordinates: n = u (1, a) + w (1, a'). + const den = al - al2; + const eig = (x, y) => [(y - al2 * x) / den, (al * x - y) / den]; + // The corners of the X: K1 holds u > 0, w > 0 (the positive x axis, below + // y = a x), K2 holds u > 0, w < 0 (above it); K3 and K4 are their negatives. + const sails = []; + for (const sign of [1, -1]) { + const pts = []; + const R = 40; + for (let x = -R; x <= R; x++) + for (let y = -R; y <= R; y++) { + const [u, w] = eig(x, y); + if (u > 0 && sign * w > 0) pts.push([u, sign * w, x, y]); + } + pts.sort((p, q) => p[0] - q[0]); + // The lower left hull in (u, w): a point that does not lie lower than the + // last corner is inside the hull plus the corner's quadrant, and a corner + // that the next point sees on its far side is no corner. The turn is read + // from the integer coordinates, so points on an edge never count as + // corners by a rounding error. + const hull = []; + for (const p of pts) { + if (hull.length && p[1] >= hull[hull.length - 1][1]) continue; + while (hull.length >= 2) { + const o = hull[hull.length - 2], a = hull[hull.length - 1]; + const c = (a[2] - o[2]) * (p[3] - o[3]) - (a[3] - o[3]) * (p[2] - o[2]); + if (sign * c >= 0) hull.pop(); + else break; + } + hull.push(p); + } + // One period: from the corner nearest the middle of the corner of the X + // to its image under M, which is again a corner. + let i0 = 0; + for (let i = 1; i < hull.length; i++) if (abs(log(hull[i][0] / hull[i][1])) < abs(log(hull[i0][0] / hull[i0][1]))) i0 = i; + const to = mul(M, [hull[i0][2], hull[i0][3]]); + let j = i0 + 1; + while (j < hull.length && !(hull[j][2] === to[0] && hull[j][3] === to[1])) j++; + const period = hull.slice(i0, j).map((p) => [p[2], p[3]]); + // The whole sail, as far as the flow can bring it onto the panel. + const verts = []; + for (let k = -6; k <= 6; k++) { + let m = [1, 0, 0, 1]; + for (let s = 0; s < abs(k); s++) { + const n = k > 0 ? M : Mi; + m = [n[0] * m[0] + n[1] * m[2], n[0] * m[1] + n[1] * m[3], n[2] * m[0] + n[3] * m[2], n[2] * m[1] + n[3] * m[3]]; + } + for (const v of period) verts.push(mul(m, v)); + } + sails.push(verts.map((v) => [...v, ...eig(v[0], v[1])])); + } + // The K2 corner's bisector stands straight up. + const th1 = atan(al), th2 = atan(al2), gam = PI / 2 - (th1 + th2 + PI) / 2; + return { al, al2, e, le: log(e), M, eig, sails, cg: cos(gam), sg: sin(gam), th1, th2 }; +} + +export function setup(params) { + params.s = { idx: -1, num: null, t: 0, buf: new Float32Array(W * H * 3), tail: new Float32Array(W * H), hue: -1, cols: [] }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + const idx = cl(round(kv[2]), 0, NUMS.length - 1); + if (idx !== S.idx) { + S.idx = idx; + S.num = NUMS[idx].prep || (NUMS[idx].prep = prepare(NUMS[idx])); + S.t = 0; + S.tail.fill(0); + } + S.zoom = cl(kv[3], 10, 22); + S.t += (d * 0.16 * cl(kv[1], 0.2, 3)) / S.num.le; + S.t -= floor(S.t); + S.dt = d; + if (kv[0] !== S.hue) { + S.hue = kv[0]; + const h = kv[0] + 0.56; + S.cols = [hsv(h, 0.7, 1, [0, 0, 0]), hsv(h + 0.36, 0.55, 1, [0, 0, 0]), hsv(h + 0.2, 0.18, 0.88, [0, 0, 0]), hsv(h - 0.06, 0.55, 0.62, [0, 0, 0])]; + } +} + +// Lights a soft line from (x0, y0) to (x1, y1), its value kept where higher. +function seg(buf, x0, y0, x1, y1, wid, rgb, k) { + if ((x0 < -2 && x1 < -2) || (x0 > W + 1 && x1 > W + 1) || (y0 < -2 && y1 < -2) || (y0 > H + 1 && y1 > H + 1)) return; + const X0 = max(0, floor(min(x0, x1) - wid)), X1 = min(W - 1, ceil(max(x0, x1) + wid)); + const Y0 = max(0, floor(min(y0, y1) - wid)), Y1 = min(H - 1, ceil(max(y0, y1) + wid)); + const dx = x1 - x0, dy = y1 - y0, l2 = dx * dx + dy * dy || 1e-9; + for (let y = Y0; y <= Y1; y++) + for (let x = X0; x <= X1; x++) { + const px = x + 0.5 - x0, py = y + 0.5 - y0, t = cl((px * dx + py * dy) / l2, 0, 1); + const ex = px - t * dx, ey = py - t * dy, a = cl(wid / 2 + 0.5 - sqrt(ex * ex + ey * ey), 0, 1) * k; + if (a <= 0) continue; + const o = (y * W + x) * 3; + buf[o] = max(buf[o], rgb[0] * a); + buf[o + 1] = max(buf[o + 1], rgb[1] * a); + buf[o + 2] = max(buf[o + 2], rgb[2] * a); + } +} + +function dot(buf, cx, cy, r, rgb, k) { + const X0 = max(0, floor(cx - r - 1)), X1 = min(W - 1, floor(cx + r + 1)), Y0 = max(0, floor(cy - r - 1)), Y1 = min(H - 1, floor(cy + r + 1)); + for (let y = Y0; y <= Y1; y++) + for (let x = X0; x <= X1; x++) { + const ex = x + 0.5 - cx, ey = y + 0.5 - cy, a = cl(r + 0.5 - sqrt(ex * ex + ey * ey), 0, 1) * k; + if (a <= 0) continue; + const o = (y * W + x) * 3; + buf[o] = max(buf[o], rgb[0] * a); + buf[o + 1] = max(buf[o + 1], rgb[1] * a); + buf[o + 2] = max(buf[o + 2], rgb[2] * a); + } +} + +const DIG = ["111101101101111", "010110010010111", "111001111100111", "111001111001111", "101101111001001", "111100111001111", "111100111101111", "111001001001001", "111101111101111", "111101111001111"]; +function digit(buf, d, x0, y0, rgb) { + const b = DIG[d]; + for (let r = 0; r < 5; r++) + for (let q = 0; q < 3; q++) { + const x = x0 + q, y = y0 + r; + if (b[r * 3 + q] !== "1" || x < 0 || x >= W || y < 0 || y >= H) continue; + const o = (y * W + x) * 3; + buf[o] = rgb[0]; + buf[o + 1] = rgb[1]; + buf[o + 2] = rgb[2]; + } +} + +// The picture at flow time t: every lattice point n at M^t n, the sails +// carried along with it. M^t stretches u by e^t and shrinks w by e^t. +function frame(S, t, buf, tail) { + const N = S.num, z = S.zoom, et = exp(t * N.le), al = N.al, al2 = N.al2, { cg, sg } = N; + const [cA, cB, cL, cD] = S.cols; + // Plane (x, y) to panel. + const px = (x, y) => 32 + z * (x * cg - y * sg), py = (x, y) => 64 - z * (x * sg + y * cg); + const at = (u, w) => { + const U = u * et, V = w / et, x = U + V, y = U * al + V * al2; + return [px(x, y), py(x, y)]; + }; + buf.fill(0); + // Lattice points: the panel, two LEDs wider each way, pulled back by M^-t + // into the integer plane is a parallelogram; its columns are scanned. + const corners = []; + for (const [X, Y] of [[-2, -2], [W + 2, -2], [W + 2, H + 2], [-2, H + 2]]) { + const a = (X - 32) / z, b = (64 - Y) / z, x = a * cg + b * sg, y = -a * sg + b * cg; + const [u, w] = N.eig(x, y), U = u / et, V = w * et; + corners.push([U + V, U * al + V * al2]); + } + const xs = corners.map((c) => c[0]); + for (let x = ceil(min(...xs)); x <= floor(max(...xs)); x++) { + let lo = Infinity, hi = -Infinity; + for (let i = 0; i < 4; i++) { + const a = corners[i], b = corners[(i + 1) % 4]; + if ((a[0] - x) * (b[0] - x) > 0 || a[0] === b[0]) continue; + const y = a[1] + ((x - a[0]) * (b[1] - a[1])) / (b[0] - a[0]); + lo = min(lo, y); + hi = max(hi, y); + } + for (let y = ceil(lo); y <= floor(hi); y++) { + const [u, w] = N.eig(x, y), [X, Y] = at(u, w); + if (X < -2 || X > W + 2 || Y < -2 || Y > H + 2) continue; + // A small square of light split over the four LEDs nearest it, read + // from the LED centres so a point on a border splits evenly instead of + // tipping one way on a rounding error. + const cx = X - 0.5 + 1e-9, cy = Y - 0.5 + 1e-9, xi = floor(cx), yi = floor(cy), fx = cx - xi, fy = cy - yi; + for (const [dx, dy, a] of [[0, 0, (1 - fx) * (1 - fy)], [1, 0, fx * (1 - fy)], [0, 1, (1 - fx) * fy], [1, 1, fx * fy]]) { + const xx = xi + dx, yy = yi + dy; + if (xx >= 0 && xx < W && yy >= 0 && yy < H) tail[yy * W + xx] = max(tail[yy * W + xx], 0.3 + 0.7 * a); + } + } + } + for (let i = 0; i < W * H; i++) { + const v = tail[i] * 0.8; + buf[i * 3] = cD[0] * v; + buf[i * 3 + 1] = cD[1] * v; + buf[i * 3 + 2] = cD[2] * v; + } + // The two lines, through the centre. + for (const th of [N.th1, N.th2]) { + const c = cos(th) * 200, s = sin(th) * 200; + seg(buf, px(-c, -s), py(-c, -s), px(c, s), py(c, s), 1, cL, 1); + } + // The four sails, their corners and the lattice points on their edges. + for (let k = 0; k < 4; k++) { + const sail = N.sails[k & 1], sgn = k < 2 ? 1 : -1, col = k & 1 ? cB : cA; + let prev = null; + for (let i = 0; i < sail.length; i++) { + const v = sail[i], p = at(sgn * v[2], sgn * v[3]); + if (prev) { + seg(buf, prev[0], prev[1], p[0], p[1], 1.2, col, 0.85); + const a = sail[i - 1], g = gcd(v[0] - a[0], v[1] - a[1]); + for (let j = 1; j < g; j++) { + const q = at(sgn * (a[2] + ((v[2] - a[2]) * j) / g), sgn * (a[3] + ((v[3] - a[3]) * j) / g)); + dot(buf, q[0], q[1], 0.9, col, 1); + } + // The edge's a_k beside its middle, on the side away from the centre. + const mx = (prev[0] + p[0]) / 2, my = (prev[1] + p[1]) / 2, ln = Math.hypot(p[0] - prev[0], p[1] - prev[1]); + if (ln > 16.37 && g < 10 && mx > 3.13 && mx < W - 3.13 && my > 3.13 && my < H - 3.13) { + const ox = mx - 32, oy = my - 64, on = Math.hypot(ox, oy) || 1; + digit(buf, g, round(mx + (ox / on) * 4.5 - 1 + 1e-6), round(my + (oy / on) * 4.5 - 2 + 1e-6), col); + } + } + prev = p; + } + for (const v of sail) { + const p = at(sgn * v[2], sgn * v[3]); + dot(buf, p[0], p[1], 1.5, col, 1); + dot(buf, p[0], p[1], 0.6, [255, 255, 255], 0.9); + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.num) return; + const fade = exp(-S.dt / 0.3); + for (let i = 0; i < W * H; i++) S.tail[i] *= fade; + frame(S, S.t, S.buf, S.tail); + const b = S.buf; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const o = (y * W + x) * 3; + display.setPixel(x, y, b[o], b[o + 1], b[o + 2]); + } +} From d52728193784675caae00a0ea7d3d275a582bfec Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:45:08 +0000 Subject: [PATCH 04/10] incoming: add Anamorphosis and Jacobi Geodesics (pattern round 65) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JkphLb6hZbrmiGJN5Xd --- .../pattern-study/patterns/anamorphosis.js | 334 +++++++++++++++++ .../patterns/jacobi-geodesics.js | 354 ++++++++++++++++++ 2 files changed, 688 insertions(+) create mode 100644 incoming/pattern-study/patterns/anamorphosis.js create mode 100644 incoming/pattern-study/patterns/jacobi-geodesics.js diff --git a/incoming/pattern-study/patterns/anamorphosis.js b/incoming/pattern-study/patterns/anamorphosis.js new file mode 100644 index 000000000..2f03eafee --- /dev/null +++ b/incoming/pattern-study/patterns/anamorphosis.js @@ -0,0 +1,334 @@ +// ===== Patternflow pattern ===== +// Title: Anamorphosis +// Family: type +// Idea: A word is painted across slabs, boxes and pillars floating at many depths, as if thrown on them by a projector at one point of view, and the camera swings round the cluster so the coloured fragments scatter and slide past one another, closing up into the word only in the moment it passes that point. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: anamorphic painting from one point of view (Hans Holbein the Younger, The Ambassadors, 1533; Felice Varini's wall paintings), pattern round 65 +// Round: 65 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Word=0..4, Flatten=0..1 +// +// Knob 1: Hue. Turns the letter colours round the wheel together. Each letter keeps its own colour, evenly spaced, so the letters of a word are as far apart in hue as they can be: at 0 HERE is sky blue, violet, orange and green. +// Knob 2: Speed. The camera flies a figure eight round the cluster, out to 52 degrees either side and 17 degrees up and down, and crosses the point of view at its middle. At 2 it crosses at 4 s and then every 8 s, slowing there to a quarter of its pace, so the word stands whole for about a second. At 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Word. HERE, NOW, LOOK, 여기 (here) and 지금 (now), stacked down the panel a letter or a syllable at a time. +// Knob 4: Flatten. At 0 the thirteen pieces of the first cut (eight slabs, four boxes and a pillar) stand from 5.4 to 14.6 units from the point of view, each scaled by its distance so it fills its own rectangle of the picture there, and every fragment slides its own way once the camera leaves. Turned up they gather onto one plane 10 units away, the middle of the cluster, and at 1 the word is a flat sign that reads, foreshortened, from anywhere. +// Buttons: K1 tap cuts the picture into a new set of pieces. + +const W = 64, H = 128; +const { floor, round, min, max, abs, sqrt, sin, cos, PI, random } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +const F = 116; // focal length in LEDs, the same for the camera and the point of view +const DV = 10; // the point of view V stands at (0, 0, DV) and looks down -z at the origin +const YF = -8.6; // the floor +const MW = 128, MH = 256; // the word is drawn into V's picture at twice the panel's resolution + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// Glyphs as strokes in a unit box, y down: L a line, A an elliptic arc +// (centre, radii, start and end angle in degrees, clockwise on screen). +const GLYPHS = { + H: [["L", 0, 0, 0, 1], ["L", 1, 0, 1, 1], ["L", 0, 0.5, 1, 0.5]], + E: [["L", 0, 0, 0, 1], ["L", 0, 0, 1, 0], ["L", 0, 0.5, 0.82, 0.5], ["L", 0, 1, 1, 1]], + R: [["L", 0, 0, 0, 1], ["L", 0, 0, 0.66, 0], ["A", 0.66, 0.27, 0.34, 0.27, -90, 90], ["L", 0, 0.54, 0.66, 0.54], ["L", 0.5, 0.54, 1, 1]], + N: [["L", 0, 1, 0, 0], ["L", 0, 0, 1, 1], ["L", 1, 1, 1, 0]], + O: [["A", 0.5, 0.5, 0.5, 0.5, 0, 360]], + W: [["L", 0, 0, 0.22, 1], ["L", 0.22, 1, 0.5, 0.25], ["L", 0.5, 0.25, 0.78, 1], ["L", 0.78, 1, 1, 0]], + L: [["L", 0, 0, 0, 1], ["L", 0, 1, 1, 1]], + K: [["L", 0, 0, 0, 1], ["L", 1, 0, 0.08, 0.6], ["L", 0.36, 0.42, 1, 1]], + // Hangul syllables drawn whole, each in a square box. + "여": [["A", 0.27, 0.5, 0.25, 0.3, 0, 360], ["L", 0.86, 0, 0.86, 1], ["L", 0.6, 0.33, 0.86, 0.33], ["L", 0.6, 0.67, 0.86, 0.67]], + "기": [["L", 0.02, 0.1, 0.56, 0.1], ["L", 0.56, 0.1, 0.5, 0.92], ["L", 0.88, 0, 0.88, 1]], + "지": [["L", 0.04, 0.1, 0.62, 0.1], ["L", 0.36, 0.1, 0.04, 0.9], ["L", 0.36, 0.4, 0.64, 0.9], ["L", 0.9, 0, 0.9, 1]], + "금": [["L", 0.12, 0, 0.86, 0], ["L", 0.86, 0, 0.86, 0.3], ["L", 0, 0.48, 1, 0.48], ["L", 0.16, 0.68, 0.84, 0.68], ["L", 0.16, 0.68, 0.16, 1], ["L", 0.84, 0.68, 0.84, 1], ["L", 0.16, 1, 0.84, 1]], +}; +const WORDS = [["H", "E", "R", "E"], ["N", "O", "W"], ["L", "O", "O", "K"], ["여", "기"], ["지", "금"]]; + +function segDist(px, py, s) { + const dx = s[2] - s[0], dy = s[3] - s[1], l = dx * dx + dy * dy; + const t = l > 0 ? cl(((px - s[0]) * dx + (py - s[1]) * dy) / l, 0, 1) : 0; + const ex = s[0] + t * dx - px, ey = s[1] + t * dy - py; + return sqrt(ex * ex + ey * ey); +} + +// The word in V's picture: coverage 0..1 and the letter each texel belongs to. +const hitS = new Uint8Array(MW * MH * 4); +function buildWord(wi) { + const word = WORDS[wi], n = word.length, hangul = wi >= 3; + const S = MW / W; // texels per LED + const margin = 3, gap = hangul ? 5 : n === 4 ? 4 : 5; + const ch = (H - 2 * margin - (n - 1) * gap) / n, cw = 60; + const pen = hangul ? 7.4 : n === 4 ? 7.2 : 8.4; + const mask = new Float32Array(MW * MH), id = new Uint8Array(MW * MH); + for (let k = 0; k < n; k++) { + const x0 = (W - cw) / 2, y0 = margin + k * (ch + gap), hw = pen / 2; + const sx = cw - pen, sy = ch - pen; + const segs = []; + for (const st of GLYPHS[word[k]]) { + const P = (u, v) => [(x0 + hw + u * sx) * S, (y0 + hw + v * sy) * S]; + if (st[0] === "L") segs.push([...P(st[1], st[2]), ...P(st[3], st[4])]); + else { + const [, cx, cy, rx, ry, a0, a1] = st, m = max(4, Math.ceil(abs(a1 - a0) / 10)); + for (let i = 0; i < m; i++) { + const b0 = ((a0 + ((a1 - a0) * i) / m) * PI) / 180, b1 = ((a0 + ((a1 - a0) * (i + 1)) / m) * PI) / 180; + segs.push([...P(cx + rx * cos(b0), cy + ry * sin(b0)), ...P(cx + rx * cos(b1), cy + ry * sin(b1))]); + } + } + } + // Each stroke marks the samples within half a pen of it, two by two to a + // texel, then the texels count their marked samples. + const r = hw * S, SW = MW * 2; + const ty0 = max(0, floor((y0 - 1) * S)), ty1 = min(MH, Math.ceil((y0 + ch + 1) * S)); + hitS.fill(0, ty0 * 2 * SW, ty1 * 2 * SW); + for (const sg of segs) { + const xa = max(0, floor((min(sg[0], sg[2]) - r) * 2)), xb = min(SW - 1, Math.ceil((max(sg[0], sg[2]) + r) * 2)); + const ya = max(0, floor((min(sg[1], sg[3]) - r) * 2)), yb = min(MH * 2 - 1, Math.ceil((max(sg[1], sg[3]) + r) * 2)); + for (let y = ya; y <= yb; y++) + for (let x = xa; x <= xb; x++) if (!hitS[y * SW + x] && segDist((x + 0.5) / 2, (y + 0.5) / 2, sg) <= r) hitS[y * SW + x] = 1; + } + for (let y = ty0; y < ty1; y++) + for (let x = 0; x < MW; x++) { + const o = y * 2 * SW + x * 2, hit = hitS[o] + hitS[o + 1] + hitS[o + SW] + hitS[o + SW + 1]; + if (hit) { + mask[y * MW + x] = hit / 4; + id[y * MW + x] = k; + } + } + } + return { mask, id, n }; +} + +// V's picture cut into rectangles, each the front face of one piece. +function cutCells(random) { + const cells = []; + const cut = (x0, y0, x1, y1, d) => { + const w = x1 - x0, h = y1 - y0; + if (d > 0 && (w * h < 700 || (w * h < 1300 && random() < 0.4) || d > 5)) { + cells.push([x0, y0, x1, y1]); + return; + } + const vert = h > 1.7 * w ? false : w > 1.4 * h ? true : random() < 0.5; + const u = 0.32 + 0.36 * random(); + if (vert) { + const xm = round(x0 + w * u); + cut(x0, y0, xm, y1, d + 1); + cut(xm, y0, x1, y1, d + 1); + } else { + const ym = round(y0 + h * u); + cut(x0, y0, x1, ym, d + 1); + cut(x0, ym, x1, y1, d + 1); + } + }; + cut(0, 0, W, H, 0); + // Depths evenly spread, then shuffled, so near and far pieces mix. + const n = cells.length, rank = []; + for (let i = 0; i < n; i++) rank.push(-1 + (2 * (i + 0.5)) / n); + for (let i = n - 1; i > 0; i--) { + const j = floor(random() * (i + 1)), t = rank[i]; + rank[i] = rank[j]; + rank[j] = t; + } + return cells.map((c, i) => ({ c, r: rank[i], kind: c[3] - c[1] > 2.2 * (c[2] - c[0]) ? 2 : random() < 0.45 ? 0 : 1 })); +} + +// World boxes from the cells for a given flatness: a piece at distance d +// from V is scaled by d so its front face fills its cell exactly from V. +function buildBoxes(S) { + const spread = 0.5 * (1 - S.flat); + S.boxes = S.cells.map(({ c, r, kind }) => { + const d = DV * (1 + spread * r); + const x0 = ((c[0] - 32) * d) / F, x1 = ((c[2] - 32) * d) / F; + const y0 = ((64 - c[3]) * d) / F, y1 = ((64 - c[1]) * d) / F; + const zf = DV - d, w = x1 - x0, h = y1 - y0; + // 0 a slab, 1 a box as deep as its short side, 2 a pillar as deep as it is wide. + const th = kind === 0 ? 0.14 : kind === 1 ? min(w, h) : w; + return [x0, y0, zf - th, x1, y1, zf]; + }); + S.vr = new Float32Array(S.boxes.length * 4); + S.cr = new Float32Array(S.boxes.length * 4); + rects(S.boxes, 0, 0, DV, [0, 0, -1], [1, 0, 0], [0, 1, 0], S.vr); +} + +// Each box's bounding rectangle on the screen of a camera, so a ray only +// tests the boxes whose rectangle holds its LED. +function rects(boxes, ox, oy, oz, fw, rt, up, out) { + for (let i = 0; i < boxes.length; i++) { + const b = boxes[i]; + let x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9; + for (let c = 0; c < 8; c++) { + const X = b[c & 1 ? 3 : 0] - ox, Y = b[c & 2 ? 4 : 1] - oy, Z = b[c & 4 ? 5 : 2] - oz; + const z = X * fw[0] + Y * fw[1] + Z * fw[2]; + if (z < 0.05) { x0 = y0 = -1e9; x1 = y1 = 1e9; break; } + const sx = 32 + (F * (X * rt[0] + Y * rt[1] + Z * rt[2])) / z, sy = 64 - (F * (X * up[0] + Y * up[1] + Z * up[2])) / z; + x0 = min(x0, sx); x1 = max(x1, sx); y0 = min(y0, sy); y1 = max(y1, sy); + } + out[i * 4] = x0 - 1; out[i * 4 + 1] = x1 + 1; out[i * 4 + 2] = y0 - 1; out[i * 4 + 3] = y1 + 1; + } +} + +// A fixed generator, so the pieces stand where they were looked at; a K1 tap +// gives it a new seed. +function generator(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 SEED = 7; + +export function setup(params) { + params.s = { + cells: cutCells(generator(SEED)), boxes: [], vr: null, cr: null, flat: -1, word: -1, words: {}, w: null, + p: -0.25, hue: 0, cam: [0, 0, DV], fw: [0, 0, -1], rt: [1, 0, 0], up: [0, 1, 0], + hit: { t: 0, b: -1, ax: 0 }, col: [0, 0, 0], pal: new Float32Array(12), + }; +} + +const T_LOOP = 16; // seconds for the whole figure eight at Speed 2 + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0]; + const speed = cl(kv[1], 0.2, 3); + const wi = cl(round(kv[2]), 0, WORDS.length - 1); + if (wi !== S.word) { + S.word = wi; + S.w = S.words[wi] || (S.words[wi] = buildWord(wi)); + } + const flat = cl(kv[3], 0, 1); + if (input.btnPressed && input.btnPressed[0]) { + S.cells = cutCells(generator(floor(random() * 4294967296))); + S.flat = -1; + } + if (abs(flat - S.flat) > 1e-4) { + S.flat = flat; + buildBoxes(S); + } + S.p += (d * speed) / 2 / T_LOOP; + // A figure eight through V, slowed to a quarter of its pace there. + const tau = 2 * PI * S.p, ts = tau - 0.375 * sin(2 * tau); + const th = (52 * PI * sin(ts)) / 180, ph = (17 * PI * sin(2 * ts)) / 180; + const r = DV * (1 - 0.16 * sin(ts) * sin(ts)); + const cam = S.cam; + cam[0] = r * cos(ph) * sin(th); + cam[1] = r * sin(ph); + cam[2] = r * cos(ph) * cos(th); + const l = sqrt(cam[0] * cam[0] + cam[1] * cam[1] + cam[2] * cam[2]); + const fw = S.fw, rt = S.rt, up = S.up; + fw[0] = -cam[0] / l; fw[1] = -cam[1] / l; fw[2] = -cam[2] / l; + // right = forward x world up, up = right x forward + const rl = sqrt(fw[2] * fw[2] + fw[0] * fw[0]); + rt[0] = -fw[2] / rl; rt[1] = 0; rt[2] = fw[0] / rl; + up[0] = rt[1] * fw[2] - rt[2] * fw[1]; + up[1] = rt[2] * fw[0] - rt[0] * fw[2]; + up[2] = rt[0] * fw[1] - rt[1] * fw[0]; + rects(S.boxes, cam[0], cam[1], cam[2], fw, rt, up, S.cr); +} + +// The nearest box along o + t d for 0 < t < tmax, testing only the boxes +// whose screen rectangle in rc holds (sx, sy); fills h. +function trace(boxes, rc, sx, sy, ox, oy, oz, dx, dy, dz, tmax, h) { + const ix = 1 / (abs(dx) < 1e-9 ? 1e-9 : dx), iy = 1 / (abs(dy) < 1e-9 ? 1e-9 : dy), iz = 1 / (abs(dz) < 1e-9 ? 1e-9 : dz); + let best = tmax, bi = -1, bax = 0; + for (let i = 0; i < boxes.length; i++) { + if (sx < rc[i * 4] || sx > rc[i * 4 + 1] || sy < rc[i * 4 + 2] || sy > rc[i * 4 + 3]) continue; + const b = boxes[i]; + let t0 = (b[0] - ox) * ix, t1 = (b[3] - ox) * ix; + if (t0 > t1) { const q = t0; t0 = t1; t1 = q; } + let a0 = (b[1] - oy) * iy, a1 = (b[4] - oy) * iy; + if (a0 > a1) { const q = a0; a0 = a1; a1 = q; } + let c0 = (b[2] - oz) * iz, c1 = (b[5] - oz) * iz; + if (c0 > c1) { const q = c0; c0 = c1; c1 = q; } + let tn = t0, ax = 0; + if (a0 > tn) { tn = a0; ax = 1; } + if (c0 > tn) { tn = c0; ax = 2; } + const tf = min(t1, a1, c1); + if (tn > 1e-6 && tn <= tf && tn < best) { best = tn; bi = i; bax = ax; } + } + h.t = best; h.b = bi; h.ax = bax; + return bi; +} + +const LX = 0.42, LY = 0.78, LZ = 0.46; // light from above, right and in front + +export function draw(display, params) { + const S = params.s; + if (!S || !S.w) return; + const { boxes, cam, fw, rt, up, hit, col, pal } = S, wd = S.w, mask = wd.mask, ids = wd.id; + for (let k = 0; k < wd.n; k++) { + hsv(S.hue + 0.55 + k / wd.n, 1, 1, col); + pal[k * 3] = col[0]; pal[k * 3 + 1] = col[1]; pal[k * 3 + 2] = col[2]; + } + const ox = cam[0], oy = cam[1], oz = cam[2]; + for (let py = 0; py < H; py++) + for (let px = 0; px < W; px++) { + const sx = px + 0.5 - 32, sy = 64 - py - 0.5; + const dx = fw[0] * F + rt[0] * sx + up[0] * sy; + const dy = fw[1] * F + rt[1] * sx + up[1] * sy; + const dz = fw[2] * F + rt[2] * sx + up[2] * sy; + let r = 0, g = 0, b = 0; + const tf = dy < 0 ? (YF - oy) / dy : 1e9; + trace(boxes, S.cr, px + 0.5, py + 0.5, ox, oy, oz, dx, dy, dz, tf, hit); + const dl = sqrt(dx * dx + dy * dy + dz * dz), foot = (hit.t * dl) / F; // world size of one LED there + if (hit.b >= 0) { + const B = boxes[hit.b], t = hit.t, ax = hit.ax; + const X = ox + t * dx, Y = oy + t * dy, Z = oz + t * dz; + const dd = ax === 0 ? dx : ax === 1 ? dy : dz, sg = dd > 0 ? -1 : 1; + const lam = max(0, sg * (ax === 0 ? LX : ax === 1 ? LY : LZ)); + // Where V sees this point: its place in V's picture, and whether + // anything stands between it and V. + let paint = 0, pk = 0; + const depth = DV - Z; + if (depth > 0.1) { + const u = 32 + (F * X) / depth, v = 64 - (F * Y) / depth; + const mx = u * 2 - 0.5, my = v * 2 - 0.5; + if (mx >= 0 && my >= 0 && mx < MW - 1 && my < MH - 1) { + const ix = floor(mx), iy = floor(my), fx = mx - ix, fy = my - iy, o = iy * MW + ix; + const m = (mask[o] * (1 - fx) + mask[o + 1] * fx) * (1 - fy) + (mask[o + MW] * (1 - fx) + mask[o + MW + 1] * fx) * fy; + if (m > 0.02) { + pk = ids[mask[o] > 0 ? o : mask[o + 1] > 0 ? o + 1 : mask[o + MW] > 0 ? o + MW : o + MW + 1]; + const vx = X, vy = Y, vz = Z - DV; + if (trace(boxes, S.vr, u, v, 0, 0, DV, vx, vy, vz, 1 - 1e-3, hit) < 0) paint = m; + } + } + } + // The surface: dim slate, lit from one side, its edges drawn faintly. + let e; + if (ax === 0) e = min(Y - B[1], B[4] - Y, Z - B[2], B[5] - Z); + else if (ax === 1) e = min(X - B[0], B[3] - X, Z - B[2], B[5] - Z); + else e = min(X - B[0], B[3] - X, Y - B[1], B[4] - Y); + const edge = cl(1 - e / (0.9 * foot), 0, 1); + const base = 0.13 + 0.17 * lam + 0.2 * edge * (1 - paint); + r = base * 0.86; g = base * 0.93; b = base * 1.12; + if (paint > 0) { + const pv = min(1, 0.8 + 0.45 * lam); + r += (pal[pk * 3] * pv - r) * paint; + g += (pal[pk * 3 + 1] * pv - g) * paint; + b += (pal[pk * 3 + 2] * pv - b) * paint; + } + } else if (tf < 1e9) { + // The floor: a faint grid one unit square. + const X = ox + tf * dx, Z = oz + tf * dz, fo = (tf * dl) / F; + const gx = abs(X - round(X)), gz = abs(Z - round(Z)); + const ln = cl(1 - min(gx, gz) / max(0.02, 0.8 * fo), 0, 1) * cl(1.6 - fo * 5, 0, 1); + const fade = cl(1 - (X * X + Z * Z) / 260, 0, 1); + r = g = b = (0.03 + 0.14 * ln) * fade; + b *= 1.25; + } + display.setPixel(px, py, floor(cl(r, 0, 1) * 255), floor(cl(g, 0, 1) * 255), floor(cl(b, 0, 1) * 255)); + } +} diff --git a/incoming/pattern-study/patterns/jacobi-geodesics.js b/incoming/pattern-study/patterns/jacobi-geodesics.js new file mode 100644 index 000000000..fe90dd3b1 --- /dev/null +++ b/incoming/pattern-study/patterns/jacobi-geodesics.js @@ -0,0 +1,354 @@ +// ===== Patternflow pattern ===== +// Title: Jacobi Geodesics +// Family: space +// Idea: A three-axis ellipsoid stands the height of the panel and turns, and 36 geodesics leave one of its four umbilics in every direction at once: their front wraps the body and closes again to a single point at the opposite umbilic, then comes home; past the middle of K4 one long geodesic winds instead, filling the band between its two caustic lines of curvature. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: geodesics on the triaxial ellipsoid (Carl Gustav Jacob Jacobi, 1839), their caustics (Michel Chasles, 1846), pattern round 65 +// Round: 65 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Axes=0..1, Launch=0..1 +// +// Knob 1: Hue. Turns the colours round the wheel: at 0 the fan runs from cyan at its two ends through blue to violet in the middle of its directions, the body is a dim blue with rose lines of curvature, the long geodesic is violet and its caustics warm white. The body keeps to the hues from green to violet, which stay colours this dim. +// Knob 2: Speed. At 2 the front runs 27 LEDs a second, so the fan from the umbilic closes at the opposite one after 4.7 s and is home at 9.3 s, and a new fan leaves after a 0.6 s fade. The body turns half round in the time of one pass, so the closing point faces you. At 0.2 ten times slower. +// Knob 3: Axes. The half axes are 1 (60 LEDs, up the panel), 0.5 and c, with c from 0.2 at 0 to 0.45 at 1. The umbilics sit in the plane of the longest and shortest axes, at 0.884 of the half length at 0 and 0.970 at 1, nearer the tips as the body grows rounder, and the way from one to the opposite one is half the perimeter of that principal ellipse, 2.101 at 0 and 2.360 at 1. +// Knob 4: Launch. Up to 0.08 the fan starts exactly on the umbilic. To 0.5 its start slides along the same principal ellipse toward the waist and the front no longer closes but folds into cusps. From 0.5 one geodesic leaves the end of the middle axis at 10 degrees from the long axis, turning to 60 at 1: at Axes 0 it circles the short axis over both tips up to 0.68, passes through the umbilics there, and then circles the long axis in a belt round the waist that narrows until, at 1, its caustics run between 0.50 and 0.57 of the half length above and below it. + +const W = 64, H = 128; +const { floor, round, min, max, abs, sqrt, sin, cos, asin, atan2, PI, exp, hypot } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const SC = 60; // LEDs per unit: the long half axis a = 1 is 60 LEDs +const NB = 36; // rays in the fan +const DS = 0.02; // arc length between trail points +const STEP = 0.005; // largest RK4 step + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// The body x^2 + y^2/b^2 + z^2/c^2 = 1, x up the panel. +function setAxes(S, k) { + S.b = 0.5; + S.c = 0.5 * (0.4 + 0.5 * k); + S.A = 1; S.B = 1 / (S.b * S.b); S.C = 1 / (S.c * S.c); + const b2 = S.b * S.b, c2 = S.c * S.c; + // The four umbilics, in the plane of the long and short axes. + S.ux = sqrt((1 - b2) / (1 - c2)); + S.uz = S.c * sqrt((b2 - c2) / (1 - c2)); + // Half the perimeter of that principal ellipse: every geodesic from an + // umbilic reaches the opposite one after exactly this length. + let per = 0, px = 1, pz = 0; + for (let i = 1; i <= 2000; i++) { + const t = (PI * i) / 2000, x = cos(t), z = S.c * sin(t); + per += hypot(x - px, z - pz); + px = x; pz = z; + } + S.hp = per; +} + +// Geodesic acceleration on F = 0: x'' = -(v.Hv / |grad F|^2) grad F. +function acc(S, p, v, o) { + const gx = p[0] * S.A, gy = p[1] * S.B, gz = p[2] * S.C; + const k = -(v[0] * v[0] * S.A + v[1] * v[1] * S.B + v[2] * v[2] * S.C) / (gx * gx + gy * gy + gz * gz); + o[0] = k * gx; o[1] = k * gy; o[2] = k * gz; +} + +// Back onto the surface and the tangent plane, at unit speed. +function settle(S, p, v) { + const s = 1 / sqrt(p[0] * p[0] * S.A + p[1] * p[1] * S.B + p[2] * p[2] * S.C); + p[0] *= s; p[1] *= s; p[2] *= s; + const gx = p[0] * S.A, gy = p[1] * S.B, gz = p[2] * S.C; + const d = (v[0] * gx + v[1] * gy + v[2] * gz) / (gx * gx + gy * gy + gz * gz); + v[0] -= d * gx; v[1] -= d * gy; v[2] -= d * gz; + const n = hypot(v[0], v[1], v[2]); + v[0] /= n; v[1] /= n; v[2] /= n; +} + +const K1 = [0, 0, 0], K2 = [0, 0, 0], K3 = [0, 0, 0], K4 = [0, 0, 0], P2 = [0, 0, 0], V2 = [0, 0, 0], VA = [0, 0, 0], VB = [0, 0, 0]; +function rk4(S, p, v, h) { + acc(S, p, v, K1); + for (let i = 0; i < 3; i++) { P2[i] = p[i] + 0.5 * h * v[i]; V2[i] = v[i] + 0.5 * h * K1[i]; } + acc(S, P2, V2, K2); + for (let i = 0; i < 3; i++) { VA[i] = V2[i]; P2[i] = p[i] + 0.5 * h * VA[i]; V2[i] = v[i] + 0.5 * h * K2[i]; } + acc(S, P2, V2, K3); + for (let i = 0; i < 3; i++) { VB[i] = V2[i]; P2[i] = p[i] + h * VB[i]; V2[i] = v[i] + h * K3[i]; } + acc(S, P2, V2, K4); + for (let i = 0; i < 3; i++) { + p[i] += (h / 6) * (v[i] + 2 * VA[i] + 2 * VB[i] + V2[i]); + v[i] += (h / 6) * (K1[i] + 2 * K2[i] + 2 * K3[i] + K4[i]); + } + settle(S, p, v); +} + +// The two ellipsoidal coordinates c^2 < mu < b^2 < nu < 1 of a point on the +// body: the roots besides 0 of sum x_i^2 / (a_i^2 - s) = 1. +function ellc(S, x, y, z, out) { + const A2 = 1, B2 = S.b * S.b, C2 = S.c * S.c, x2 = x * x, y2 = y * y, z2 = z * z; + const bb = x2 + y2 + z2 - A2 - B2 - C2, cc = A2 * B2 + A2 * C2 + B2 * C2 - x2 * (B2 + C2) - y2 * (A2 + C2) - z2 * (A2 + B2); + const dq = sqrt(max(0, bb * bb - 4 * cc)); + out[0] = (-bb - dq) / 2; + out[1] = (-bb + dq) / 2; + return out; +} + +// A point on the principal ellipse of the long and short axes, at parameter t. +const onEllipse = (S, t) => [cos(t), 0, S.c * sin(t)]; + +export function setup(params) { + params.s = { + b: 0.5, c: 0.2, A: 1, B: 4, C: 25, ux: 0, uz: 0, hp: 2, + axes: -1, launch: -1, mode: 0, hue: 0, speed: 2, + s: 0, rho: 0, fade: 1, rays: [], lam: 0, caustic: [], + buf: new Float32Array(W * H * 3), G1: new Float32Array(W * H), G2: new Float32Array(W * H), SH: new Float32Array(W * H), + col: [0, 0, 0], e: [0, 0], + }; +} + +// The fan: NB rays from the start, one per direction. Launch below 0.08 +// starts on the umbilic. +function launchFan(S) { + const L = S.launch, tu = atan2(S.uz / S.c, S.ux); + const off = cl((L - 0.08) / 0.42, 0, 1); + const t = tu + off * 0.55 * (PI / 2 - tu); + const p0 = onEllipse(S, t), e1 = [-sin(t), 0, S.c * cos(t)], l = hypot(e1[0], e1[2]); + e1[0] /= l; e1[2] /= l; + S.rays = []; + for (let k = 0; k < NB; k++) { + const al = (2 * PI * (k + 0.5)) / NB; + const p = p0.slice(), v = [cos(al) * e1[0], sin(al), cos(al) * e1[2]]; + settle(S, p, v); + S.rays.push({ p, v, tr: [p[0], p[1], p[2]], acc: 0, al }); + } + S.s = 0; + S.fade = 1; +} + +// One long geodesic from the end of the middle axis, (0, b, 0), aimed at +// beta from the long axis, run 24 units ahead so its band is already half +// filled. Its constant is lambda = mu (t.e_nu)^2 + nu (t.e_mu)^2 with e_mu +// and e_nu the principal directions; here mu = c^2, nu = 1 and those are z +// and x, so lambda = c^2 cos^2 beta + sin^2 beta. The geodesic keeps to +// mu <= lambda, a ring round the short axis over both tips, when lambda < +// b^2, and to nu >= lambda, a belt round the long axis, when lambda > b^2. +function launchOne(S) { + const u = (S.launch - 0.5) / 0.5, be = ((10 + 50 * u) * PI) / 180; + const p = [0, S.b, 0], v = [cos(be), 0, sin(be)]; + settle(S, p, v); + S.lam = S.c * S.c * cos(be) * cos(be) + sin(be) * sin(be); + S.caustic = causticLines(S, S.lam); + const r = { p, v, tr: [p[0], p[1], p[2]], acc: 0, al: be }; + S.rays = [r]; + advance(S, r, 24, 3000); + S.s = 0; + S.fade = 1; +} + +// The lines of curvature at lambda, as point lists: two closed curves. +function causticLines(S, lam) { + const a2 = 1, b2 = S.b * S.b, c2 = S.c * S.c, out = []; + for (const sg of [1, -1]) { + const pts = []; + for (let i = 0; i <= 160; i++) { + const ph = (2 * PI * i) / 160; + let x, y, z; + if (lam > b2) { + const mu = c2 + (b2 - c2) * sin(ph) * sin(ph), nu = lam; + x = sg * sqrt(max(0, ((a2 - mu) * (a2 - nu)) / ((a2 - b2) * (a2 - c2)))); + y = (cos(ph) < 0 ? -1 : 1) * S.b * sqrt(max(0, ((b2 - mu) * (nu - b2)) / ((a2 - b2) * (b2 - c2)))); + z = (sin(ph) < 0 ? -1 : 1) * S.c * sqrt(max(0, ((mu - c2) * (nu - c2)) / ((a2 - c2) * (b2 - c2)))); + } else { + const mu = lam, nu = b2 + (a2 - b2) * sin(ph) * sin(ph); + x = (cos(ph) < 0 ? -1 : 1) * sqrt(max(0, ((a2 - mu) * (a2 - nu)) / ((a2 - b2) * (a2 - c2)))); + y = (sin(ph) < 0 ? -1 : 1) * S.b * sqrt(max(0, ((b2 - mu) * (nu - b2)) / ((a2 - b2) * (b2 - c2)))); + z = sg * S.c * sqrt(max(0, ((mu - c2) * (nu - c2)) / ((a2 - c2) * (b2 - c2)))); + } + pts.push(x, y, z); + } + out.push(pts); + } + return out; +} + +// Moves a ray on by length L, dropping a trail point every DS, keeping at most cap. +function advance(S, r, L, cap) { + let left = L; + while (left > 1e-9) { + const h = min(STEP, left, DS - r.acc + 1e-12); + rk4(S, r.p, r.v, h); + left -= h; + r.acc += h; + if (r.acc >= DS - 1e-9) { + r.acc = 0; + r.tr.push(r.p[0], r.p[1], r.p[2]); + if (r.tr.length > cap * 3) r.tr.splice(0, r.tr.length - cap * 3); + } + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0]; + S.speed = cl(kv[1], 0.2, 3); + const ax = cl(kv[2], 0, 1), L = cl(kv[3], 0, 1); + let fresh = false; + if (abs(ax - S.axes) > 1e-4) { S.axes = ax; setAxes(S, ax); fresh = true; } + if (abs(L - S.launch) > 1e-4) { S.launch = L; fresh = true; } + S.mode = S.launch < 0.5 ? 0 : 1; + if (fresh) (S.mode ? launchOne : launchFan)(S); + const ds = (0.45 * S.speed * d) / 2; + if (S.mode === 0) { + // Out to the opposite umbilic and home again, then a short fade and a new fan. + if (S.s < 2 * S.hp) { + const step = min(ds, 2 * S.hp - S.s); + for (const r of S.rays) advance(S, r, step, 45); + S.s += step; + } else { + S.fade -= d * 1.6; + if (S.fade <= 0) launchFan(S); + } + } else { + advance(S, S.rays[0], ds, 3000); + S.s += ds; + } + // Half a turn for every pass from umbilic to umbilic. + S.rho += (PI * ds) / S.hp; +} + +// How close a LED is to a level line of G, n lines over 0..1, in LEDs, from +// the step to its neighbours: 1 on the line, 0 a LED away. +function gridLine(G, SH, p, px, py, n) { + const g = G[p], gx = px + 1 < W && SH[p + 1] >= 0 ? abs(G[p + 1] - g) : 0, gy = py + 1 < H && SH[p + W] >= 0 ? abs(G[p + W] - g) : 0; + const gr = max(gx, gy, 1e-4), f = abs(g * n - round(g * n)) / n; + return cl(1 - f / gr / 0.9, 0, 1); +} + +function plot(buf, x, y, r, g, b) { + const x0 = floor(x - 0.5), y0 = floor(y - 0.5), fx = x - 0.5 - x0, fy = y - 0.5 - y0; + for (let j = 0; j < 2; j++) + for (let i = 0; i < 2; i++) { + const X = x0 + i, Y = y0 + j; + if (X < 0 || X >= W || Y < 0 || Y >= H) continue; + const w = (i ? fx : 1 - fx) * (j ? fy : 1 - fy) * 1.5, o = (Y * W + X) * 3; + buf[o] = max(buf[o], r * w); buf[o + 1] = max(buf[o + 1], g * w); buf[o + 2] = max(buf[o + 2], b * w); + } +} + +function line(buf, x0, y0, x1, y1, r, g, b) { + const n = max(1, Math.ceil(hypot(x1 - x0, y1 - y0) / 0.6)); + for (let i = 0; i <= n; i++) plot(buf, x0 + ((x1 - x0) * i) / n, y0 + ((y1 - y0) * i) / n, r, g, b); +} + +export function draw(display, params) { + const S = params.s; + if (!S || S.axes < 0) return; + const { buf, G1, G2, SH, col } = S; + const cr = cos(S.rho), sr = sin(S.rho), b = S.b, c = S.c, b2 = b * b, c2 = c * c; + const hb = 0.5 + S.hue, e = S.e; + buf.fill(0); + // The body: orthographic, seen along z after turning by rho about x. + for (let py = 0; py < H; py++) { + const x = (64 - py - 0.5) / SC; + for (let px = 0; px < W; px++) { + const p = py * W + px, yp = (px + 0.5 - 32) / SC; + // y = yp cr + zp sr, z = -yp sr + zp cr; solve for the front zp. + const qa = (sr * sr) / b2 + (cr * cr) / c2, qb = 2 * yp * cr * sr * (1 / b2 - 1 / c2); + const qc = x * x + yp * yp * ((cr * cr) / b2 + (sr * sr) / c2) - 1; + const disc = qb * qb - 4 * qa * qc; + if (disc <= 0) { SH[p] = -1; continue; } + const zp = (-qb + sqrt(disc)) / (2 * qa), y = yp * cr + zp * sr, z = -yp * sr + zp * cr; + const nx = x, ny = y / b2, nz = z / c2, nl = sqrt(nx * nx + ny * ny + nz * nz); + const vz = (ny * sr + nz * cr) / nl, vy = (ny * cr - nz * sr) / nl; + SH[p] = cl(0.35 + 0.65 * (0.55 * vz + 0.35 * (nx / nl) - 0.25 * vy), 0, 1) * cl(disc * 40, 0, 1); + ellc(S, x, y, z, e); + G1[p] = (2 / PI) * asin(sqrt(cl((e[0] - c2) / (b2 - c2), 0, 1))); + G2[p] = (2 / PI) * asin(sqrt(cl((e[1] - b2) / (1 - b2), 0, 1))); + } + } + hsv(hb + 0.42, 0.55, 1, col); + const lr = col[0], lg = col[1], lb = col[2]; + // The body's own dim colour keeps to the hues from green to violet, since + // red, orange and yellow read brown this dim. + hsv(0.3 + 0.55 * ((((hb + 0.12) % 1) + 1) % 1), 0.75, 1, col); + const br = col[0], bg = col[1], bb = col[2]; + for (let py = 0; py < H; py++) + for (let px = 0; px < W; px++) { + const p = py * W + px; + if (SH[p] < 0) continue; + const sh = SH[p], o = p * 3; + // Lines of curvature: six of mu, eight of nu, about a LED wide. + const ln = max(gridLine(G1, SH, p, px, py, 6), gridLine(G2, SH, p, px, py, 8)); + // The band a long geodesic keeps to, lit faintly. + let band = 0; + if (S.mode === 1) { + const m = (G1[p] * PI) / 2, nn = (G2[p] * PI) / 2; + const mu = c2 + (b2 - c2) * sin(m) * sin(m), nu = b2 + (1 - b2) * sin(nn) * sin(nn); + band = S.lam > b2 ? (nu >= S.lam ? 1 : 0) : mu <= S.lam ? 1 : 0; + } + const v = (0.09 + 0.16 * sh) * (1 + 0.9 * band); + buf[o] = v * br + ln * lr * 0.17 * (0.4 + 0.6 * sh); + buf[o + 1] = v * bg + ln * lg * 0.17 * (0.4 + 0.6 * sh); + buf[o + 2] = v * bb + ln * lb * 0.17 * (0.4 + 0.6 * sh); + } + // Body point to screen, and whether it faces us. + const proj = (x, y, z, out) => { + out[0] = 32 + SC * (y * cr - z * sr); + out[1] = 64 - SC * x; + out[2] = (y / b2) * sr + (z / c2) * cr > 0 ? 1 : 0.3; + return out; + }; + const q0 = [0, 0, 0], q1 = [0, 0, 0]; + // Caustics of the long geodesic. + if (S.mode === 1) { + hsv(hb + 0.62, 0.25, 1, col); + for (const pts of S.caustic) + for (let i = 3; i < pts.length; i += 3) { + proj(pts[i - 3], pts[i - 2], pts[i - 1], q0); + proj(pts[i], pts[i + 1], pts[i + 2], q1); + const w = 0.6 * min(q0[2], q1[2]); + line(buf, q0[0], q0[1], q1[0], q1[1], col[0] * w, col[1] * w, col[2] * w); + } + } + // Trails and the front. + const f = S.fade, nr = S.rays.length; + for (let k = 0; k < nr; k++) { + const r = S.rays[k], tr = r.tr, n = tr.length / 3; + hsv(S.mode ? hb + 0.3 : hb + 0.17 * (1 - cos(r.al)), 0.85, 1, col); + for (let i = 1; i < n; i++) { + proj(tr[i * 3 - 3], tr[i * 3 - 2], tr[i * 3 - 1], q0); + proj(tr[i * 3], tr[i * 3 + 1], tr[i * 3 + 2], q1); + const age = (n - i) / n, w = f * min(q0[2], q1[2]) * (S.mode ? 0.5 + 0.5 * exp(-age * 4) : 0.15 + 0.6 * (1 - age)); + line(buf, q0[0], q0[1], q1[0], q1[1], col[0] * w, col[1] * w, col[2] * w); + } + } + if (S.mode === 0 && S.s > 0) { + for (let k = 0; k < nr; k++) { + const r = S.rays[k], r2 = S.rays[(k + 1) % nr]; + proj(r.p[0], r.p[1], r.p[2], q0); + proj(r2.p[0], r2.p[1], r2.p[2], q1); + hsv(hb + 0.17 * (1 - cos(r.al)), 0.5, 1, col); + const w = f * min(q0[2], q1[2]); + line(buf, q0[0], q0[1], q1[0], q1[1], col[0] * w, col[1] * w, col[2] * w); + } + } else if (S.mode === 1) { + const r = S.rays[0]; + proj(r.p[0], r.p[1], r.p[2], q0); + plot(buf, q0[0], q0[1], q0[2], q0[2], q0[2]); + } + // The four umbilics. + for (const [sx, sz] of [[1, 1], [1, -1], [-1, 1], [-1, -1]]) { + proj(sx * S.ux, 0, sz * S.uz, q0); + const w = q0[2] > 0.5 ? 1 : 0.35; + plot(buf, q0[0], q0[1], w, w, w); + } + for (let p = 0; p < W * H; p++) { + const o = p * 3; + display.setPixel(p & 63, p >> 6, floor(cl(buf[o], 0, 1) * 255), floor(cl(buf[o + 1], 0, 1) * 255), floor(cl(buf[o + 2], 0, 1) * 255)); + } +} From 59a61195fbbf35e445509217deacb2c222c1edfa Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:46:47 +0000 Subject: [PATCH 05/10] incoming: add Hunminjeongeum and McMullen Carpet (pattern round 65) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JkphLb6hZbrmiGJN5Xd --- .../pattern-study/patterns/hunminjeongeum.js | 297 ++++++++++++++++++ .../pattern-study/patterns/mcmullen-carpet.js | 259 +++++++++++++++ 2 files changed, 556 insertions(+) create mode 100644 incoming/pattern-study/patterns/hunminjeongeum.js create mode 100644 incoming/pattern-study/patterns/mcmullen-carpet.js diff --git a/incoming/pattern-study/patterns/hunminjeongeum.js b/incoming/pattern-study/patterns/hunminjeongeum.js new file mode 100644 index 000000000..e3ad20a8e --- /dev/null +++ b/incoming/pattern-study/patterns/hunminjeongeum.js @@ -0,0 +1,297 @@ +// ===== Patternflow pattern ===== +// Title: Hunminjeongeum +// Family: type +// Idea: The letter-making chapter of the Hunminjeongeum Haerye grows down the panel as a family tree: the five basic consonants are drawn, each column copies its letter one step down and adds a stroke that lights up as it is drawn (ㄱ to ㅋ, ㄴ to ㄷ to ㅌ, ㅁ to ㅂ to ㅍ, ㅅ to ㅈ to ㅊ, ㅇ to ㆆ to ㅎ), the three letters of a different body (ㆁ, ㄹ, ㅿ) fade in apart, and below them the dot of heaven, the line of earth and the line of man fly together into the eight vowels. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the Jejahae, the chapter on how the letters were made, in the Hunminjeongeum Haerye (King Sejong; Jeong In-ji and seven scholars, 1446), pattern round 65 +// Round: 65 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Style=0..1, Weight=0..1 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the strokes a letter takes over are warm gold, the stroke it adds and the vowels' dots are cyan, a stroke being drawn glows near white, and the three letters of a different body are lilac. +// Knob 2: Speed. At 2 the eight basic letters are drawn stroke by stroke in the first 1.3 s, the second row of each tree grows from 1.05 s, the third from 2.1 s, and the odd three fade in from 3.3 s; from 4.5 s a glow runs down the trees every 2.5 s, and at 15 s everything but the basic letters fades and grows again. At 0.2 ten times slower; at 3 one and a half times faster. +// Knob 3: Style. 0 is the Haerye's own letterforms of 1446: geometric strokes of one weight, ㅇ a circle, the vowels' dots round and apart from their stem, ㅅ an even pair of legs, a short upright tick on ㅊ and ㅎ, in a warm ink. Turning it moves them to today's forms: the dots stretch into short strokes off the stem, the right leg of ㅅ, ㅈ and ㅊ starts from the middle of the left, the ticks lie flat, the ink cools to white, and the four letters no longer used (ㆍ, ㆁ, ㆆ, ㅿ) fade to an eighth. +// Knob 4: Weight. The stroke, from 1.5 LED at 0 (1.6 at the opening 0.06) to 3.3 LED at 1; the dots stay rounder than the stroke and grow less. + +const W = 64, H = 128; +const { floor, min, max, sqrt, abs, exp, atan2, cos, sin, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ease = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + out[1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + out[2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; + return out; +} + +// Strokes in a unit box, y down. C is a capsule from (x0, y0) to (x1, y1) +// with r times the pen's half width, and a second set of numbers gives +// today's form where it differs; a dot is a capsule of length 0 and a large +// r. O is a ring (centre, radius). A stroke with "from" is taken over from +// another letter, [letter, stroke]; one with add: 1 is the stroke this letter +// adds; acc: 1 is drawn in the accent colour. +const C = (x0, y0, x1, y1, o = {}) => ({ ...o, k: "C", h: [x0, y0, x1, y1, o.r || 1], m: o.m ? [...o.m, o.mr || 1] : [x0, y0, x1, y1, o.mr || o.r || 1] }); +const O = (cx, cy, r, o = {}) => ({ ...o, k: "O", h: [cx, cy, r] }); +const DOT = 2.1; + +// The letters: name, place (column, row, area) and strokes. The consonant +// tree has five columns and three rows; the odd three sit in their own row; +// the vowels have the three basic letters on one row and the eight made from +// them on two rows of four. +const L = {}; +const def = (id, area, col, row, lev, strokes, extra = {}) => (L[id] = { id, area, col, row, lev, strokes, ...extra }); +// Consonants, the Jejahae's five shapes: the tongue root closing the throat, +// the tongue touching the upper gum, the mouth, a tooth, the throat. +def("ㄱ", "c", 0, 0, 0, [C(0.12, 0.15, 0.86, 0.15), C(0.86, 0.15, 0.86, 0.9)]); +def("ㄴ", "c", 1, 0, 0, [C(0.15, 0.1, 0.15, 0.85), C(0.15, 0.85, 0.9, 0.85)]); +def("ㅁ", "c", 2, 0, 0, [C(0.15, 0.15, 0.15, 0.85), C(0.15, 0.15, 0.85, 0.15), C(0.85, 0.15, 0.85, 0.85), C(0.15, 0.85, 0.85, 0.85)]); +def("ㅅ", "c", 3, 0, 0, [C(0.5, 0.12, 0.1, 0.88), C(0.5, 0.12, 0.9, 0.88, { m: [0.31, 0.5, 0.88, 0.88] })]); +def("ㅇ", "c", 4, 0, 0, [O(0.5, 0.5, 0.37)]); +// Each stroke louder by one: ㄱ to ㅋ, ㄴ to ㄷ to ㅌ, ㅁ to ㅂ to ㅍ, +// ㅅ to ㅈ to ㅊ, ㅇ to ㆆ to ㅎ. +def("ㅋ", "c", 0, 1, 1, [C(0.12, 0.15, 0.86, 0.15, { from: ["ㄱ", 0] }), C(0.86, 0.15, 0.86, 0.9, { from: ["ㄱ", 1] }), C(0.12, 0.52, 0.86, 0.52, { add: 1 })]); +def("ㄷ", "c", 1, 1, 1, [C(0.15, 0.15, 0.15, 0.85, { from: ["ㄴ", 0] }), C(0.15, 0.85, 0.88, 0.85, { from: ["ㄴ", 1] }), C(0.15, 0.15, 0.88, 0.15, { add: 1 })]); +def("ㅌ", "c", 1, 2, 2, [C(0.15, 0.15, 0.15, 0.85, { from: ["ㄷ", 0] }), C(0.15, 0.85, 0.88, 0.85, { from: ["ㄷ", 1] }), C(0.15, 0.15, 0.88, 0.15, { from: ["ㄷ", 2] }), C(0.15, 0.5, 0.88, 0.5, { add: 1 })]); +def("ㅂ", "c", 2, 1, 1, [ + C(0.17, 0.45, 0.17, 0.88, { from: ["ㅁ", 0] }), C(0.17, 0.45, 0.83, 0.45, { from: ["ㅁ", 1] }), C(0.83, 0.45, 0.83, 0.88, { from: ["ㅁ", 2] }), + C(0.17, 0.88, 0.83, 0.88, { from: ["ㅁ", 3] }), C(0.17, 0.1, 0.17, 0.45, { add: 1 }), C(0.83, 0.1, 0.83, 0.45, { add: 1 }), +]); +// ㅍ has as many strokes as ㅂ in the Haerye's own letterforms, so nothing +// is added here: ㅂ's strokes move into ㅍ, and the letter keeps no accent. +def("ㅍ", "c", 2, 2, 2, [ + C(0.33, 0.5, 0.33, 0.85, { from: ["ㅂ", 0] }), C(0.08, 0.15, 0.92, 0.15, { from: ["ㅂ", 1] }), C(0.67, 0.5, 0.67, 0.85, { from: ["ㅂ", 2] }), + C(0.08, 0.85, 0.92, 0.85, { from: ["ㅂ", 3] }), C(0.33, 0.15, 0.33, 0.5, { from: ["ㅂ", 4] }), C(0.67, 0.15, 0.67, 0.5, { from: ["ㅂ", 5] }), +], { moved: 1 }); +def("ㅈ", "c", 3, 1, 1, [ + C(0.5, 0.16, 0.12, 0.88, { from: ["ㅅ", 0] }), C(0.5, 0.16, 0.88, 0.88, { from: ["ㅅ", 1], m: [0.31, 0.52, 0.86, 0.88] }), C(0.1, 0.16, 0.9, 0.16, { add: 1 }), +]); +def("ㅊ", "c", 3, 2, 2, [ + C(0.5, 0.34, 0.14, 0.9, { from: ["ㅈ", 0] }), C(0.5, 0.34, 0.86, 0.9, { from: ["ㅈ", 1], m: [0.33, 0.62, 0.86, 0.9] }), C(0.1, 0.34, 0.9, 0.34, { from: ["ㅈ", 2] }), + C(0.5, 0.04, 0.5, 0.24, { add: 1, m: [0.32, 0.14, 0.68, 0.14] }), +]); +def("ㆆ", "c", 4, 1, 1, [O(0.5, 0.63, 0.27, { from: ["ㅇ", 0] }), C(0.12, 0.2, 0.88, 0.2, { add: 1 })], { old: 1 }); +def("ㅎ", "c", 4, 2, 2, [O(0.5, 0.69, 0.23, { from: ["ㆆ", 0] }), C(0.12, 0.33, 0.88, 0.33, { from: ["ㆆ", 1] }), C(0.5, 0.04, 0.5, 0.22, { add: 1, m: [0.3, 0.14, 0.7, 0.14] })]); +// The three that are not made by adding a stroke: ㆁ takes the throat's +// circle, ㄹ and ㅿ copy the tongue and the tooth with a different body. +def("ㆁ", "x", 0, 0, 3, [C(0.5, 0.04, 0.5, 0.3), O(0.5, 0.63, 0.29)], { old: 1 }); +def("ㄹ", "x", 1, 0, 3, [C(0.12, 0.12, 0.86, 0.12), C(0.86, 0.12, 0.86, 0.5), C(0.86, 0.5, 0.14, 0.5), C(0.14, 0.5, 0.14, 0.88), C(0.14, 0.88, 0.88, 0.88)]); +def("ㅿ", "x", 3, 0, 3, [C(0.5, 0.12, 0.1, 0.86), C(0.5, 0.12, 0.9, 0.86), C(0.1, 0.86, 0.9, 0.86)], { old: 1 }); +// Vowels: heaven round, earth flat, man upright. +def("ㆍ", "b", 0, 0, 0, [C(0.5, 0.5, 0.5, 0.5, { r: DOT * 1.25, acc: 1, mr: DOT * 1.25 })], { old: 1 }); +def("ㅡ", "b", 1, 0, 0, [C(0.06, 0.5, 0.94, 0.5)]); +def("ㅣ", "b", 2, 0, 0, [C(0.5, 0.06, 0.5, 0.94)]); +// First out: one dot with earth or man. Again out: a second dot, from ㅣ. +const dot = (x, y, from, m, add) => C(x, y, x, y, { r: DOT, acc: 1, from, m, add }); +def("ㅗ", "v", 0, 0, 1, [C(0.06, 0.7, 0.94, 0.7, { from: ["ㅡ", 0] }), dot(0.5, 0.38, ["ㆍ", 0], [0.5, 0.7, 0.5, 0.4])]); +def("ㅏ", "v", 1, 0, 1, [C(0.36, 0.06, 0.36, 0.94, { from: ["ㅣ", 0] }), dot(0.7, 0.5, ["ㆍ", 0], [0.36, 0.5, 0.68, 0.5])]); +def("ㅜ", "v", 2, 0, 1, [C(0.06, 0.3, 0.94, 0.3, { from: ["ㅡ", 0] }), dot(0.5, 0.62, ["ㆍ", 0], [0.5, 0.3, 0.5, 0.6])]); +def("ㅓ", "v", 3, 0, 1, [C(0.64, 0.06, 0.64, 0.94, { from: ["ㅣ", 0] }), dot(0.3, 0.5, ["ㆍ", 0], [0.32, 0.5, 0.64, 0.5])]); +def("ㅛ", "v", 0, 1, 2, [C(0.06, 0.7, 0.94, 0.7, { from: ["ㅗ", 0] }), dot(0.31, 0.38, ["ㅗ", 1], [0.35, 0.7, 0.35, 0.4]), dot(0.69, 0.38, null, [0.65, 0.7, 0.65, 0.4], 1)]); +def("ㅑ", "v", 1, 1, 2, [C(0.36, 0.06, 0.36, 0.94, { from: ["ㅏ", 0] }), dot(0.7, 0.31, ["ㅏ", 1], [0.36, 0.35, 0.68, 0.35]), dot(0.7, 0.69, null, [0.36, 0.65, 0.68, 0.65], 1)]); +def("ㅠ", "v", 2, 1, 2, [C(0.06, 0.3, 0.94, 0.3, { from: ["ㅜ", 0] }), dot(0.31, 0.62, ["ㅜ", 1], [0.35, 0.3, 0.35, 0.6]), dot(0.69, 0.62, null, [0.65, 0.3, 0.65, 0.6], 1)]); +def("ㅕ", "v", 3, 1, 2, [C(0.64, 0.06, 0.64, 0.94, { from: ["ㅓ", 0] }), dot(0.3, 0.31, ["ㅓ", 1], [0.32, 0.35, 0.64, 0.35]), dot(0.3, 0.69, null, [0.32, 0.65, 0.64, 0.65], 1)]); +const ORDER = Object.keys(L); + +// Boxes on the panel. Consonants: five columns 12.8 LEDs apart, glyphs +// 12 x 14, rows 19 apart. The odd three on row 57. Vowels: the basic three +// on row 77, the eight on rows 94 and 111, glyphs 15 x 15. +function place(g) { + if (g.area === "c" || g.area === "x") { + const cx = 6.4 + g.col * 12.8; + return [cx - 6, g.area === "x" ? 57 : 1 + g.row * 19, 12, 14]; + } + if (g.area === "b") return [12 + g.col * 20 - 7.5, 77, 15, 15]; + return [8 + g.col * 16 - 7.5, 94 + g.row * 17, 15, 15]; +} +for (const id of ORDER) { + const g = L[id]; + g.box = place(g); + for (const s of g.strokes) if (s.acc === undefined) s.acc = s.add || 0; +} + +// Beats at Speed 2, in seconds of build time. +const TRACE = 0.26, FLY = 0.5, T_LEV = [0, 1.05, 2.1, 3.3], STAG = 0.12, GLOW = 0.7, PULSE = 2.5, T_CYCLE = 15, T_FADE = 1; + +function startOf(g) { + return T_LEV[g.lev] + (g.area === "v" ? 0.3 + g.col * STAG : g.area === "x" ? g.col * 0.25 : g.col * STAG); +} + +export function setup(params) { + params.s = { T: 0, cyc: 0, hue: 0, speed: 2, style: 0, w: 0.8, buf: new Float32Array(W * H * 3) }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0]; + S.speed = cl(kv[1], 0.2, 3); + S.style = cl(kv[2], 0, 1); + S.w = 0.75 + 0.9 * cl(kv[3], 0, 1); + S.T += (d * S.speed) / 2; + // After the hold, everything but the basic letters fades and grows again. + if (S.T > T_CYCLE + T_FADE) { + S.T = T_LEV[1] - 0.3; + S.cyc++; + } +} + +// One stroke's shape on the panel for a letter, at the current style: a +// capsule [x0, y0, x1, y1, r] or a ring [cx, cy, R, 0, 0] in LEDs. +function shape(S, g, s, out) { + const [bx, by, bw, bh] = g.box, st = S.style; + if (s.k === "O") { + out[0] = bx + s.h[0] * bw; + out[1] = by + s.h[1] * bh; + out[2] = s.h[2] * min(bw, bh); + out[3] = out[4] = 0; + return out; + } + for (let i = 0; i < 4; i++) { + const v = s.h[i] + (s.m[i] - s.h[i]) * st; + out[i] = (i & 1 ? by + v * bh : bx + v * bw); + } + // A dot is rounder than the pen and grows less with Weight. + const rad = (k) => (k > 1.5 ? (k > 2.2 ? 0.95 + 1.2 * S.w : 0.75 + S.w) : S.w); + out[4] = rad(s.h[4]) + (rad(s.m[4]) - rad(s.h[4])) * st; + return out; +} + +function segDist(px, py, x0, y0, x1, y1) { + 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 = x0 + t * dx - px, ey = y0 + t * dy - py; + return sqrt(ex * ex + ey * ey); +} + +// Paints one stroke: coverage over a 1-LED edge, plus a soft halo when it +// glows. u is how much of it is drawn (a capsule from its start, a ring +// from the top going left, the way ㅇ is written). +function paint(S, k, g5, u, rgb, alpha, glow, w) { + const buf = S.buf, ring = k === "O"; + let x0 = g5[0], y0 = g5[1], x1 = g5[2], y1 = g5[3], r = g5[4]; + if (!ring && u < 1) (x1 = x0 + (x1 - x0) * u), (y1 = y0 + (y1 - y0) * u); + const R = ring ? g5[2] : 0, pr = ring ? w : r, halo = glow > 0.01 ? 2.6 : 0; + const ext = (ring ? R + pr : pr) + halo + 1; + const xa = max(0, floor((ring ? x0 : min(x0, x1)) - ext)), xb = min(W - 1, Math.ceil((ring ? x0 : max(x0, x1)) + ext)); + const ya = max(0, floor((ring ? y0 : min(y0, y1)) - ext)), yb = min(H - 1, Math.ceil((ring ? y0 : max(y0, y1)) + ext)); + const sweep = u * 2 * PI; + for (let y = ya; y <= yb; y++) + for (let x = xa; x <= xb; x++) { + const px = x + 0.5, py = y + 0.5; + let d; + if (ring) { + const dx = px - x0, dy = py - y0; + let a = atan2(-dx, -dy); + if (a < 0) a += 2 * PI; + if (u >= 1 || a <= sweep) d = abs(sqrt(dx * dx + dy * dy) - R); + else { + const ex = x0 - R * sin(sweep), ey = y0 - R * cos(sweep); + d = min(sqrt((px - x0) ** 2 + (py - y0 + R) ** 2), sqrt((px - ex) ** 2 + (py - ey) ** 2)); + } + } else d = segDist(px, py, x0, y0, x1, y1); + const cov = cl(pr + 0.5 - d, 0, 1) * alpha; + let v = cov; + if (halo) v = max(v, glow * 0.55 * exp(-((d - pr) * (d - pr)) / 2.2) * alpha); + if (v <= 0.004) continue; + const o = (y * W + x) * 3; + buf[o] = max(buf[o], rgb[0] * v); + buf[o + 1] = max(buf[o + 1], rgb[1] * v); + buf[o + 2] = max(buf[o + 2], rgb[2] * v); + } +} + +function line(S, x0, y0, x1, y1, rgb, a) { + paint(S, "C", [x0, y0, x1, y1, 0.35], 1, rgb, a, 0, 0); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const buf = S.buf, T = S.T, st = S.style, h = S.hue; + buf.fill(0); + // The ink cools from the warm tone of the printed page to plain white. + const ink = hsv(h + 0.11 - 0.03 * st, 0.55 - 0.45 * st, 0.9, [0, 0, 0]); + const acc = hsv(h + 0.5, 0.72, 1, [0, 0, 0]); + const odd = hsv(h + 0.78, 0.38, 0.8, [0, 0, 0]); + const hot = hsv(h + 0.5, 0.15, 1, [0, 0, 0]); + const dim = hsv(h + 0.5, 0.5, 0.3, [0, 0, 0]); + // Fade of everything grown, at the end of a cycle. + const fadeAll = T > T_CYCLE ? 1 - (T - T_CYCLE) / T_FADE : 1; + const tmp = [0, 0, 0, 0, 0], src = [0, 0, 0, 0, 0], g5 = [0, 0, 0, 0, 0], col = [0, 0, 0]; + // Rules between consonants, the odd three and vowels. + if (T > T_LEV[3] - 0.2) { + const a = cl((T - T_LEV[3] + 0.2) / 0.6, 0, 1) * fadeAll * 0.5; + for (let x = 2; x < 62; x += 3) line(S, x, 54.5, x + 1, 54.5, dim, a); + } + const ar = cl(T / 0.8, 0, 1) * 0.5; + for (let x = 2; x < 62; x += 3) line(S, x, 74, x + 1, 74, dim, ar); + // The last pulse of the hold: a glow that runs down each tree. + const tEnd = T_LEV[3] + 1.2, pT = T > tEnd ? T - tEnd - floor((T - tEnd) / PULSE) * PULSE : -9; + for (const id of ORDER) { + const g = L[id], t0 = startOf(g), lt = g.lev === 0 && S.cyc > 0 ? 99 : T - t0; + if (lt < 0) continue; + const grown = g.lev > 0 ? fadeAll : 1; + const oldDim = g.old ? 1 - 0.88 * st : 1; + const isOdd = g.area === "x"; + // The tree's branch from the letter above. + if (g.area === "c" && g.lev > 0) { + const [bx, by] = g.box, cx = bx + 6, u = ease(lt / FLY); + line(S, cx, by - 4.2, cx, by - 4.2 + 3.4 * u, dim, grown * 0.8); + } + let nAdd = 0; + for (let i = 0; i < g.strokes.length; i++) { + const s = g.strokes[i]; + shape(S, g, s, g5); + let u = 1, alpha = grown * oldDim, glow = 0, rgb = ink; + if (isOdd) { + alpha *= ease(lt / 0.9); + rgb = odd; + } else if (g.lev === 0) { + // Basic letters: drawn stroke by stroke. + const a = lt - i * TRACE; + if (a < 0) continue; + u = ease(a / TRACE); + glow = a < TRACE ? 1 : exp(-(a - TRACE) / GLOW) * 0.6; + rgb = s.acc ? acc : ink; + } else if (s.from) { + // Taken over: flies from the letter it came from. + const f = L[s.from[0]]; + shape(S, f, f.strokes[s.from[1]], src); + const e = ease(lt / FLY); + for (let j = 0; j < 5; j++) tmp[j] = src[j] + (g5[j] - src[j]) * e; + for (let j = 0; j < 5; j++) g5[j] = tmp[j]; + rgb = s.acc ? acc : ink; + // ㅍ's strokes and the vowels' dots light as they land. + if (g.moved || s.acc) glow = lt < FLY ? 0.5 * e : exp(-(lt - FLY) / GLOW); + } else { + // The added stroke: drawn after the rest has landed, glowing. + const a = lt - FLY - nAdd * TRACE; + nAdd++; + if (a < 0) continue; + u = ease(a / TRACE); + glow = a < TRACE ? 1 : exp(-(a - TRACE) / GLOW); + rgb = acc; + } + if (pT >= 0 && !isOdd && (s.add || (g.moved && g.lev > 0) || (s.acc && g.lev > 0))) { + const q = pT - (g.lev - 1) * 0.45 - (g.area === "v" ? 0.2 : 0); + if (q >= 0) glow = max(glow, exp(-q / 0.45) * 0.9); + } + if (glow > 0.02) { + const k = min(1, glow) * 0.8; + for (let j = 0; j < 3; j++) col[j] = rgb[j] + (hot[j] - rgb[j]) * k; + paint(S, s.k, g5, u, col, alpha, glow, S.w); + } else paint(S, s.k, g5, u, rgb, alpha, 0, S.w); + } + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const o = (y * W + x) * 3; + display.setPixel(x, y, min(255, buf[o]) | 0, min(255, buf[o + 1]) | 0, min(255, buf[o + 2]) | 0); + } +} diff --git a/incoming/pattern-study/patterns/mcmullen-carpet.js b/incoming/pattern-study/patterns/mcmullen-carpet.js new file mode 100644 index 000000000..ddeffc2a3 --- /dev/null +++ b/incoming/pattern-study/patterns/mcmullen-carpet.js @@ -0,0 +1,259 @@ +// ===== Patternflow pattern ===== +// Title: McMullen Carpet +// Family: fractal +// Idea: A Bedford-McMullen self-affine carpet fills the panel: the square is cut into m columns and n > m rows, a few cells are kept and the same cut repeats inside each, every LED is shaded by how much of it stays kept level after level, and the view zooms m times across and n times down into one kept cell until the picture comes back exactly, which an even zoom never does. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: self-affine carpets (Tim Bedford, 1984; Curtis T. McMullen, 1984), pattern round 65 +// Round: 65 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Pattern=0..5, Iso=0..1 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the points that stay in the kept cells all the way down are near white, the holes cut at deeper levels step from dark blue through teal to pale cyan, the holes cut at the first level are black, and the two dimensions in the corner are pale gold. The holes skip orange, which reads brown when dark. +// Knob 2: Speed. How fast the view zooms. At 2 one level, m times across and n times down, takes 3 s; at 0.2 30 s and at 3 2 s. Each level dives into a kept cell picked at random, and since the picture at the end of a level is the picture at its start, the dive never jumps. +// Knob 3: Pattern. Six carpets, m columns across and n rows down, n always on the long side. The corner gives the Hausdorff dimension, log_m of the sum over columns of t_j^(log_n m) with t_j the kept cells in column j (McMullen), over the box dimension log_m r + log_n(N / r) with r the columns that keep a cell and N the kept cells. 0: 2 x 3, t = 3, 1, 1.585 (log_2 3) and 1.631. 1: 3 x 4, t = 4, 1, 2, 1.589 and 1.611. 2: 2 x 3, t = 2, 1, 1.350 and 1.369. 3: 3 x 5, t = 5, 1, 2, 1.569 and 1.609. 4: 2 x 6, t = 5, 1, 1.518 and 1.613. 5: 2 x 4, t = 2, 2, every column keeping the same number, and both are 1.500. +// Knob 4: Iso. Up to 0.1 the zoom is the affine one, m^t across and n^t down, and repeats every level. Above it the vertical rate slides from n towards m, so at 1 the zoom is the same in both directions: the picture never comes back, the carpet flattens into wide bands, and after two levels the view fades and starts again from the whole square. + +const W = 64, H = 128; +const { floor, min, max, log, pow, round } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The carpets: m columns across, n rows down, rows written top to bottom, +// X for a kept cell. +const PATTERNS = [ + { m: 2, n: 3, rows: ["X.", "XX", "X."] }, + { m: 3, n: 4, rows: ["X.X", "XX.", "X..", "X.X"] }, + { m: 2, n: 3, rows: ["X.", ".X", "X."] }, + { m: 3, n: 5, rows: ["X.X", "X..", "XX.", "X.X", "X.."] }, + { m: 2, n: 6, rows: ["X.", "X.", ".X", "X.", "X.", "X."] }, + { m: 2, n: 4, rows: ["X.", ".X", "X.", ".X"] }, +]; + +// Dimensions of one carpet. The column counts t_j go over the m columns, +// the strips of the weaker contraction 1/m. +function dims(P) { + const { m, n, rows } = P, t = new Array(m).fill(0); + let N = 0; + for (let b = 0; b < n; b++) for (let a = 0; a < m; a++) if (rows[b][a] === "X") t[a]++, N++; + const th = log(m) / log(n); + let s = 0, r = 0; + for (const x of t) if (x) (s += pow(x, th)), r++; + return { t, N, r, haus: log(s) / log(m), box: log(r) / log(m) + log(N / r) / log(n) }; +} + +// The holes are dark, and a dark orange reads brown on the panel, so their +// hues go round a wheel with orange (0.03 to 0.16) cut out. +const wheel = (h) => 0.16 + (h - floor(h)) * 0.87; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + out[o + 1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + out[o + 2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; +} + +// Samples per LED: 2 across and 4 down, since the cells flatten as they +// shrink and the vertical detail runs out first. +const SX = 2, SY = 4, CS = W * SX, RS = H * SY, KMAX = 12, PAL = 32; + +export function setup(params) { + params.s = { + pat: -1, P: null, D: null, info: null, + t: 0, cell: [0, 0], iso: 0, fade: 1, hue: 0, speed: 2, + xa: new Uint8Array(CS * KMAX), yb: new Uint8Array(RS * KMAX), + lev: new Float32Array(W * H * 3), pal: new Float32Array(3 * (PAL + 1)), + cs: new Float32Array(3 * KMAX), bl: new Float32Array(3 * KMAX), mean: new Float32Array(6), tmp: new Float32Array(3), L0: 4, + }; +} + +function choose(S, k) { + S.pat = k; + const P = PATTERNS[k]; + S.P = P; + S.D = new Uint8Array(P.m * P.n); + S.kept = []; + for (let b = 0; b < P.n; b++) for (let a = 0; a < P.m; a++) if (P.rows[b][a] === "X") (S.D[a * P.n + b] = 1), S.kept.push([a, b]); + S.info = dims(P); + // At t = 0 the digits run until a cell is about one and a half samples + // tall; the shades are spread over that many levels and one more. + S.L0 = log(RS / 1.5) / log(P.n); + S.t = 0; + S.fade = 1; + pick(S); +} + +// The next level dives into a kept cell picked at random, preferring one +// off the top and bottom rows so the dive stays on the panel's middle. +function pick(S) { + const n = S.P.n, inner = S.kept.filter(([, b]) => b > 0 && b < n - 1); + const pool = inner.length ? inner : S.kept; + S.cell = pool[floor(Math.random() * pool.length)]; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0]; + S.speed = cl(kv[1], 0.2, 3); + const k = cl(round(kv[2]), 0, PATTERNS.length - 1); + if (k !== S.pat) choose(S, k); + S.iso = kv[3] <= 0.1 ? 0 : cl((kv[3] - 0.1) / 0.9, 0, 1); + S.t += (d * S.speed) / 6; + if (S.iso === 0) { + S.fade = min(1, S.fade + d * 3); + if (S.t >= 1) { + S.t -= 1; + pick(S); + } + } else { + // The even zoom does not come back to its start, so it runs two levels + // and fades out, and the whole square fades back in. + const T = 2; + S.fade = S.t < 0.12 ? S.t / 0.12 : S.t > T - 0.25 ? max(0, (T - S.t) / 0.25) : 1; + if (S.t >= T) { + S.t = 0; + pick(S); + } + } +} + +// 3 x 5 digits and the letters the corner needs. +const GL = { + 0: "111101101101111", 1: "010110010010111", 2: "111001111100111", 3: "111001111001111", 4: "101101111001001", + 5: "111100111001111", 6: "111100111101111", 7: "111001001001001", 8: "111101111101111", 9: "111101111001111", + ".": "000000000000010", H: "101101111101101", B: "110101110101110", " ": "000000000000000", +}; + +function text(buf, s, x0, y0, r, g, b) { + for (let i = 0; i < s.length; i++) { + const m = GL[s[i]]; + if (!m) continue; + const w = s[i] === "." ? 2 : 4, dx = s[i] === "." ? -1 : 0; + for (let y = 0; y < 5; y++) + for (let x = 0; x < 3; x++) + if (m[y * 3 + x] === "1") { + const o = ((y0 + y) * W + x0 + x + dx) * 3; + buf[o] = r; + buf[o + 1] = g; + buf[o + 2] = b; + } + x0 += w; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.P) return; + const { m, n } = S.P, t = S.t, D = S.D, xa = S.xa, yb = S.yb, pal = S.pal, buf = S.lev; + const [a, b] = S.cell; + // The zoom's centre is the fixed point of the map into cell (a, b), + // (x, y) -> ((x + a) / m, (y + b) / n): x* = a / (m - 1), y* = b / (n - 1). + const fx = a / (m - 1), fy = b / (n - 1); + const sx = pow(m, -t), sy = pow(n, -t * (1 - S.iso)) * pow(m, -t * S.iso); + // Base-m digits of every column sample and base-n digits of every row + // sample, KMAX deep. A sample is in the level-k carpet when its first k + // digit pairs are all kept cells. + for (let c = 0; c < CS; c++) { + let x = fx + ((c + 0.5) / CS - fx) * sx; + x = cl(x, 0, 1 - 1e-12); + for (let k = 0; k < KMAX; k++) { + const dgt = floor(x * m); + xa[c * KMAX + k] = dgt; + x = x * m - dgt; + } + } + for (let r = 0; r < RS; r++) { + let y = fy + ((r + 0.5) / RS - fy) * sy; + y = cl(y, 0, 1 - 1e-12); + for (let k = 0; k < KMAX; k++) { + const dgt = floor(y * n); + yb[r * KMAX + k] = dgt; + y = y * n - dgt; + } + } + // Shading. A sample that leaves the kept cells at level e is a hole cut + // e levels down, and the level is read t levels down, v = e - t, so at + // t = 1 every value is what it was at t = 0 and the loop has no seam. + // Below the level whose cells are a sample tall the digits stop: the + // sample takes the mean shade of what a kept cell holds, since a share + // rho = N / (m n) of its area stays kept at every further level. Two stop + // levels are blended so the stop never pops. + const h0 = S.hue + 0.42, VC = S.L0 + 1, rho = S.info.N / (m * n); + for (let q = 0; q <= PAL; q++) { + const v = (q / PAL) * VC, u = v / VC; + if (q === PAL) hsv(wheel(h0 - 0.02), 0.12, 1, pal, q * 3); + else hsv(wheel(h0 + 0.06 - 0.1 * u), 0.8 - 0.5 * u, v < 0.02 ? 0 : 0.1 + 0.75 * pow(u, 0.9), pal, q * 3); + } + const shade = (v, out, o) => { + const p = v <= 0 ? 0 : v >= VC ? PAL : (v / VC) * PAL; + const q0 = floor(p), fr = p - q0, q1 = min(PAL, q0 + 1); + out[o] = pal[q0 * 3] * (1 - fr) + pal[q1 * 3] * fr; + out[o + 1] = pal[q0 * 3 + 1] * (1 - fr) + pal[q1 * 3 + 1] * fr; + out[o + 2] = pal[q0 * 3 + 2] * (1 - fr) + pal[q1 * 3 + 2] * fr; + }; + const Lf = log(RS / (1.5 * sy)) / log(n); + const ks = min(KMAX - 2, max(1, floor(Lf))), kf = cl(Lf - ks, 0, 1), cs = S.cs, mean = S.mean; + for (let e = 0; e <= ks + 1; e++) shade(e - t, cs, e * 3); + for (let L = ks; L <= ks + 1; L++) { + let R = 0, G = 0, B = 0, w = 1; + for (let j = 0; j < 24; j++) { + const pj = j === 23 ? w : w * (1 - rho); + shade(L + j - t, S.tmp, 0); + R += S.tmp[0] * pj; + G += S.tmp[1] * pj; + B += S.tmp[2] * pj; + w *= rho; + } + mean[(L - ks) * 3] = R; + mean[(L - ks) * 3 + 1] = G; + mean[(L - ks) * 3 + 2] = B; + } + // The colour of a sample that stays in e levels: below ks the mean if it + // got that far, the hole if not; the same at ks + 1; then the blend. + const f = S.fade / (SX * SY), k2 = ks + 1, bl = S.bl; + for (let e = 0; e <= k2; e++) + for (let c = 0; c < 3; c++) { + const v0 = e < ks ? cs[e * 3 + c] : mean[c], v1 = e < k2 ? cs[e * 3 + c] : mean[3 + c]; + bl[e * 3 + c] = (v0 + (v1 - v0) * kf) * f; + } + for (let py = 0; py < H; py++) + for (let px = 0; px < W; px++) { + let R = 0, G = 0, B = 0; + for (let j = 0; j < SY; j++) { + const yo = (py * SY + j) * KMAX; + for (let i = 0; i < SX; i++) { + const xo = (px * SX + i) * KMAX; + let e = 0; + while (e < k2 && D[xa[xo + e] * n + yb[yo + e]]) e++; + R += bl[e * 3]; + G += bl[e * 3 + 1]; + B += bl[e * 3 + 2]; + } + } + const o = (py * W + px) * 3; + buf[o] = R; + buf[o + 1] = G; + buf[o + 2] = B; + } + // The two dimensions, Hausdorff over box, on a dark plate in the corner. + const hs = "H" + S.info.haus.toFixed(3), bs = "B" + S.info.box.toFixed(3); + for (let y = H - 15; y < H; y++) + for (let x = 0; x < 25; x++) { + const o = (y * W + x) * 3; + buf[o] *= 0.2; + buf[o + 1] *= 0.2; + buf[o + 2] *= 0.2; + } + const tc = [0, 0, 0]; + hsv(S.hue + 0.12, 0.45, 0.95, tc, 0); + text(buf, hs, 2, H - 14, tc[0], tc[1], tc[2]); + text(buf, bs, 2, H - 7, tc[0] * 0.8, tc[1] * 0.8, tc[2] * 0.8); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const o = (y * W + x) * 3; + display.setPixel(x, y, buf[o] | 0, buf[o + 1] | 0, buf[o + 2] | 0); + } +} From aedeb692f23208d5004e18bf68525b60d82f0e23 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 11:08:16 +0000 Subject: [PATCH 06/10] incoming: add Long Division and Sedimenting Cloud (pattern round 65) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JkphLb6hZbrmiGJN5Xd --- .../pattern-study/patterns/long-division.js | 406 ++++++++++++++++++ .../patterns/sedimenting-cloud.js | 284 ++++++++++++ 2 files changed, 690 insertions(+) create mode 100644 incoming/pattern-study/patterns/long-division.js create mode 100644 incoming/pattern-study/patterns/sedimenting-cloud.js diff --git a/incoming/pattern-study/patterns/long-division.js b/incoming/pattern-study/patterns/long-division.js new file mode 100644 index 000000000..4f0e7ba06 --- /dev/null +++ b/incoming/pattern-study/patterns/long-division.js @@ -0,0 +1,406 @@ +// ===== Patternflow pattern ===== +// Title: Long Division +// Family: type +// Idea: Long division of 1/p is written down the panel in drawn digits: a zero falls from the dividend, a quotient digit goes up top, the product is written under and ruled off, the remainder goes below, until a remainder comes back, it and the numerator glow gold and a bar is drawn over the repeating quotient digits. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the period of 1/p as the order of 10 mod p (Carl Friedrich Gauss, 1801), Midy's theorem (Etienne Midy, 1836), pattern round 65 +// Round: 65 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Divisor=7..97, Base=10..16 +// +// Knob 1: Hue. Turns the sheet round the wheel: at 0 the working is cyan and the quotient violet, and the remainder that comes halfway through an even period is green. The two numbers that close the cycle, the bar over the repetend and Midy's sum are always gold. +// Knob 2: Speed. At 2 a step with a two-digit product takes 1.2 s and each later step 0.88 times the one before, down to 0.3 of it. 1/7 is worked out in 5.4 s, its bar is drawn by 7.0 s and Midy's sum by 8.6 s, and the next numerator comes at 11.1 s; 1/97 closes its cycle at 41.5 s. The first frame already has 2.4 s of the sheet written at the knob's speed. At 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Divisor. The prime p, snapped to the nearest prime from 7 to 97. In base 10 the quotient repeats after the order of 10 mod p: 6 digits for 7 and 13, 16 for 17, 18 for 19, 8 for 73, 96 for 97. When the sheet runs off the panel the view follows the staircase down and right, the quotient and dividend rows stay pinned at the top, and once the cycle closes the view runs back up the staircase while the bar is drawn. Each new sheet takes the next numerator, 1/p, 2/p, 3/p and on; for 7, 17, 19, 23, 29, 47, 59, 61 and 97 they all repeat the same digits from a different place. +// Knob 4: Base. Works the division in bases 10 to 16, with A to F past nine. 1/7 has period 6 in base 10 (142857) and 12 (186A35), 3 in base 11 (163) and 16 (249), 2 in base 13 (1B), 1 in base 15 (2) and stops after one digit in base 14 (2). When the period is even, the remainder p - n comes exactly halfway and the two halves of the repetend add to a row of the base's top digit: 142 + 857 = 999, 186 + A35 = BBB. The sum is written in the corner when the halves are four digits or less. +// Buttons: K1 tap goes on to the next numerator. + +const W = 64, H = 128; +const { floor, round, min, max, abs, sin, exp, pow } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ease = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + out[1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + out[2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; +} + +// Digits 0-9, A-F and a plus sign, 6 x 7 with two-LED downstrokes, the way a +// soft pencil leaves them on squared paper: an open 4, a flagged 1 with no +// foot, a 9 with a tail. +const GLYPHS = [ + [".####.", "##..##", "##..##", "##..##", "##..##", "##..##", ".####."], + ["..##..", ".###..", "..##..", "..##..", "..##..", "..##..", "..##.."], + [".####.", "##..##", "....##", "...##.", "..##..", ".##...", "######"], + [".####.", "##..##", "....##", "..###.", "....##", "##..##", ".####."], + ["##..##", "##..##", "##..##", "######", "....##", "....##", "....##"], + ["######", "##....", "#####.", "....##", "....##", "##..##", ".####."], + ["..###.", ".##...", "##....", "#####.", "##..##", "##..##", ".####."], + ["######", "....##", "...##.", "..##..", ".##...", ".##...", ".##..."], + [".####.", "##..##", "##..##", ".####.", "##..##", "##..##", ".####."], + [".####.", "##..##", "##..##", ".#####", "....##", "...##.", ".###.."], + ["..##..", ".####.", "##..##", "##..##", "######", "##..##", "##..##"], + ["#####.", "##..##", "##..##", "#####.", "##..##", "##..##", "#####."], + [".####.", "##..##", "##....", "##....", "##....", "##..##", ".####."], + ["####..", "##.##.", "##..##", "##..##", "##..##", "##.##.", "####.."], + ["######", "##....", "##....", "#####.", "##....", "##....", "######"], + ["######", "##....", "##....", "#####.", "##....", "##....", "##...."], + ["......", "..##..", "..##..", "######", "..##..", "..##..", "......"], +]; +const PLUS = 16; +const FONT = GLYPHS.map((rows) => { + const m = new Uint8Array(42); + for (let y = 0; y < 7; y++) for (let x = 0; x < 6; x++) m[y * 6 + x] = rows[y][x] === "#" ? 1 : 0; + return m; +}); + +// The squared paper: cells 7 LEDs wide and 9 high. Row 0 holds the quotient, +// row 1 the divisor, the bracket and the dividend; after that odd rows hold +// a number brought down and even rows the product under it, nudged up one LED +// so the rule under a product has a clear LED on both sides. +const CW = 7, CH = 9, X0 = 1, Y0 = 2, BR = 3, DP = 2, CLIP = 19; +const PRIMES = [7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]; + +const K_DIV = 0, K_NUM = 1, K_Q0 = 2, K_QUOT = 3, K_PROD = 4, K_REM = 5, K_DOWN = 6, K_MIDY = 7, K_SUM = 8; + +const digits = (v, b) => { + const d = []; + do { + d.unshift(v % b); + v = floor(v / b); + } while (v > 0); + return d; +}; +const rowY = (r) => Y0 + r * CH - (r >= 2 && r % 2 === 0 ? 1 : 0); + +export function setup(params) { + params.s = { + p: 0, b: 0, n: 1, sheet: null, t: 0, camX: 0, camY: 0, fresh: true, + hue: 0, speed: 2, buf: new Float32Array(W * H * 3), c: [0, 0, 0], + }; +} + +// Lays the whole sheet out as a timeline: every glyph and rule gets its cell +// and the sheet-second it is written, at Speed 2. +function newSheet(S) { + const p = S.p, b = S.b, n = S.n; + const pd = digits(p, b), nd = digits(n, b), lenP = pd.length, c1 = lenP + nd.length - 1; + const items = [], rules = [], steps = []; + const add = (k, g, row, col, t0, d) => { + items.push({ k, g, row, col, t0, d, glow: 0, half: 0 }); + return items.length - 1; + }; + pd.forEach((g, i) => add(K_DIV, g, 1, i, 0.06 * i, 0.12)); + const numItems = nd.map((g, i) => add(K_NUM, g, 1, lenP + i, 0.18 + 0.06 * i, 0.12)); + add(K_Q0, 0, 0, c1, 0.4, 0.12); + let t = 0.6, r = n, rowN = 1, k = 0, end = null; + let remItems = numItems.slice(); + const seen = new Map([[n, { step: 0, items: numItems.slice() }]]); + while (!end && k < 200) { + k++; + const f = max(0.3, pow(0.88, k - 1)); + const N = r * b, q = floor(N / p), P = q * p, r2 = N - P, cu = c1 + k; + const st = { k, q, cu, rowN, t0: t }; + // The zero for step 1 is the dividend's own first zero; after that each + // one falls from the dividend row to sit beside the remainder. + if (k > 1) { + remItems.push(add(K_DOWN, 0, rowN, cu, t, 0.3 * f)); + t += 0.36 * f; + } else remItems.push(-1); + st.tq = t; + add(K_QUOT, q, 0, cu, t, 0.2 * f); + t += 0.24 * f; + if (q > 0) { + const dp = digits(P, b); + for (let i = dp.length - 1; i >= 0; i--) { + add(K_PROD, dp[i], rowN + 1, cu - (dp.length - 1 - i), t, 0.1 * f); + t += 0.12 * f; + } + rules.push({ row: rowN + 1, a: cu - dp.length + 1, b: cu, t0: t, d: 0.12 * f }); + t += 0.14 * f; + const dr = digits(r2, b); + remItems = []; + for (let i = dr.length - 1; i >= 0; i--) { + remItems.unshift(add(K_REM, dr[i], rowN + 2, cu - (dr.length - 1 - i), t, 0.1 * f)); + t += 0.12 * f; + } + rowN += 2; + } + st.rowR = rowN; + steps.push(st); + if (r2 === 0) { + end = { term: true }; + for (const i of remItems) if (i >= 0) items[i].glow = 1; + } + else if (seen.has(r2)) end = { from: seen.get(r2), mine: remItems.filter((i) => i >= 0) }; + else seen.set(r2, { step: k, items: remItems.filter((i) => i >= 0) }); + r = r2; + t += 0.08 * f; + } + const S2 = { p, b, n, lenP, c1, items, rules, steps, tEnd: t, end, L: 0, vin: null, midy: false }; + let tail = t + 2.4; + if (end.from) { + const j = end.from.step, L = k - j; + S2.L = L; + for (const i of end.mine) items[i].glow = 1; + for (const i of end.from.items) items[i].glow = 1; + // Midy: with an even period the remainder halfway is p - n, and each + // quotient digit there on is the base's top digit minus the one half a + // period back. + if (L % 2 === 0) { + const half = steps[j + L / 2 - 1]; + for (const it of items) if (it.k === K_REM && it.row === half.rowR && half.q > 0) it.half = 1; + } + const dV = 0.5 + 0.035 * L; + S2.vin = { a: c1 + j + 1, b: c1 + k, j, t0: t + 0.9, d: dV }; + tail = t + 0.9 + dV + 0.3; + if (L % 2 === 0 && L / 2 <= 4) { + // The sum is written in the corner the staircase leaves empty. + const qs = steps.slice(j, k).map((s) => s.q), h = L / 2; + let tm = tail; + const put = (k2, g, row, col) => { + add(k2, g, row, col, tm, 0.1); + tm += 0.11; + }; + for (let i = 0; i < h; i++) put(K_MIDY, qs[i], 11, 1 + i); + put(K_MIDY, PLUS, 12, 0); + for (let i = 0; i < h; i++) put(K_MIDY, qs[h + i], 12, 1 + i); + S2.midyRule = { t0: tm, d: 0.15, a: 0, b: h }; + tm += 0.2; + for (let i = h - 1; i >= 0; i--) put(K_SUM, b - 1, 13, 1 + i); + S2.midy = true; + tail = tm; + } + tail += 1.6; + } + // The sheet slides up by its own height, so a short one is not followed by + // an empty panel. + S2.lift = min(H + 12, rowY(max(S2.midy ? 13 : 0, rowN)) + 12); + S2.tOut = tail; + S2.tNext = tail + 0.9; + S.sheet = S2; + S.t = 0; + S.cur = 0; +} + +// The x of a column before the view scrolls: the bracket takes three LEDs +// between the divisor and the dividend, and the decimal point two. +const colX = (Sh, c) => X0 + c * CW + (c >= Sh.lenP ? BR : 0) + (c > Sh.c1 ? DP : 0); + +function camTarget(S) { + const Sh = S.sheet, t = S.t; + let row = 1, col = Sh.c1 + 1; + if (Sh.vin && t >= Sh.vin.t0) { + // Back up the staircase as the bar is drawn from the last digit to the first. + const u = ease((t - Sh.vin.t0) / Sh.vin.d), L = Sh.L; + const s = Sh.steps[Sh.vin.j + max(0, L - 1 - floor(u * L))]; + row = s.rowR; + col = s.cu; + if (u >= 1) row = col = 0; + } else if (S.t >= 0.6) { + // Each step is framed when it starts: down to its remainder and across + // to the cell where the next zero will land. + const st = Sh.steps; + while (S.cur + 1 < st.length && st[S.cur + 1].t0 <= t) S.cur++; + const s = st[S.cur]; + row = s.rowR; + col = s.cu + (S.cur + 1 < st.length ? 1 : 0); + } + const tx = max(0, colX(Sh, col) + CW + 1 - W), ty = max(0, rowY(row) + 7 - (H - 1)); + return [tx, ty]; +} + +function advance(S, d) { + S.t += d; + const Sh = S.sheet; + if (S.t >= Sh.tNext) { + S.n = (S.n % (S.p - 1)) + 1; + newSheet(S); + S.camX = S.camY = 0; + return; + } + const [tx, ty] = camTarget(S), k = 1 - exp(-d * (Sh.vin && S.t >= Sh.vin.t0 ? 9 : 6)); + S.camX += (tx - S.camX) * k; + S.camY += (ty - S.camY) * k; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 7, 10]; + S.hue = kv[0] || 0; + S.speed = cl(kv[1], 0.2, 3); + const want = cl(kv[2], 7, 97); + let p = 7; + for (const q of PRIMES) if (abs(q - want) < abs(p - want)) p = q; + const b = cl(round(kv[3]), 10, 16); + if (p !== S.p || b !== S.b) { + S.p = p; + S.b = b; + S.n = 1; + newSheet(S); + S.camX = S.camY = 0; + } + if (input.btnPressed && input.btnPressed[0]) S.sheet.tNext = S.t; + const step = 1 / 30; + if (S.fresh) { + // The first frame comes with 2.4 s of the sheet already written. + S.fresh = false; + for (let i = 0; i < 72; i++) advance(S, (step * S.speed) / 2); + } + let rem = (cl(dt || 0, 0, 0.1) * S.speed) / 2; + while (rem > 1e-9) { + const d = min(step, rem); + advance(S, d); + rem -= d; + } +} + +function hash(a, b, c) { + let h = Math.imul(a, 73856093) ^ Math.imul(b, 19349663) ^ Math.imul(c, 83492791); + h = Math.imul(h ^ (h >>> 13), 1274126177); + return ((h ^ (h >>> 16)) & 255) / 255; +} + +function put(buf, x, y, r, g, b) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + if (r > buf[o]) buf[o] = r; + if (g > buf[o + 1]) buf[o + 1] = g; + if (b > buf[o + 2]) buf[o + 2] = b; +} + +// One glyph, written top to bottom as u goes from 0 to 1, with the grain of +// a pencil: each LED of each written glyph keeps its own pressure. +function glyph(S, g, x0, y0, u, rgb, seed, yMin, xMin) { + const m = FONT[g], rows = u >= 1 ? 7 : floor(u * 7.99), buf = S.buf; + for (let y = 0; y < rows; y++) { + const yy = y0 + y; + if (yy < yMin || yy >= H) continue; + for (let x = 0; x < 6; x++) { + if (!m[y * 6 + x] || x0 + x < xMin) continue; + const v = 0.8 + 0.2 * hash(seed, x, y); + put(buf, x0 + x, yy, rgb[0] * v, rgb[1] * v, rgb[2] * v); + } + } +} + +function hline(S, x0, x1, y, rgb, yMin) { + if (y < yMin || y >= H) return; + for (let x = max(0, x0); x <= min(W - 1, x1); x++) put(S.buf, x, y, rgb[0], rgb[1], rgb[2]); +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.sheet) return; + const Sh = S.sheet, buf = S.buf, t = S.t, c = S.c, h0 = S.hue + 0.5; + buf.fill(0); + const out = t > Sh.tOut ? round(ease((t - Sh.tOut) / 0.9) * Sh.lift) : 0; + const camX = round(S.camX), camY = round(S.camY), fx = colX(Sh, Sh.c1 + 1); + // Once the view has scrolled, the pinned rows keep one empty cell after the + // decimal point with an ellipsis in it, so 0.0103 never reads as 0.03. + const xCut = S.camX >= 1 ? fx + CW : -99; + const pulse = 0.82 + 0.18 * sin((t - Sh.tEnd) * 6); + const gold = [0, 0, 0], work = [0, 0, 0], quot = [0, 0, 0], ink = [0, 0, 0]; + hsv(0.13, 0.55, pulse, gold); + hsv(h0 + 0.25, 0.7, 1, quot); + const yMinWork = CLIP - out; + // Where a cell lands on the panel: rows 0 and 1 stay pinned at the top, + // and the columns at and left of the numerator's last digit stay put. + const at = (row, col) => { + const pinnedRow = row <= 1, frozen = pinnedRow && col <= Sh.c1; + const x = colX(Sh, col) - (frozen ? 0 : camX) + 1; + const y = rowY(row) - (pinnedRow ? 0 : camY) - out; + return [x, y, pinnedRow && !frozen]; + }; + // The dividend's zeros, as far as the paper goes. + hsv(h0, 0.5, 0.5, work); + const cur = Sh.steps.length ? Sh.c1 + Sh.steps.length + 2 : 0; + for (let col = Sh.c1 + 1; col < Sh.c1 + 199; col++) { + const [x, y] = at(1, col); + if (x >= W || col > cur) break; + if (x < xCut - 6 || t < 0.3 + 0.03 * (col - Sh.c1)) continue; + glyph(S, 0, x, y, 1, work, col * 7 + 3, -99, xCut); + } + // The bracket and the bar over the dividend, then the decimal points. + hsv(h0, 0.35, 0.8, ink); + const bx = colX(Sh, Sh.lenP) - 2, by = rowY(1) - out, y0 = rowY(0) - out; + const ub = cl((t - 0.1) / 0.4, 0, 1); + if (ub > 0) { + put(buf, bx, by - 1, ink[0], ink[1], ink[2]); + put(buf, bx, by, ink[0], ink[1], ink[2]); + for (let y = by + 1; y < by + 7; y++) put(buf, bx + 1, y, ink[0], ink[1], ink[2]); + put(buf, bx, by + 7, ink[0], ink[1], ink[2]); + hline(S, bx + 1, bx + 1 + round((W - bx) * ub), by - 1, ink, -99); + put(buf, fx - 1, by + 6, ink[0], ink[1], ink[2]); + if (t > 0.45) put(buf, fx - 1, y0 + 6, quot[0], quot[1], quot[2]); + if (xCut > 0) + for (let i = 1; i < 6; i += 2) { + put(buf, fx + i, by + 6, work[0], work[1], work[2]); + if (t > 0.6) put(buf, fx + i, y0 + 6, quot[0] * 0.6, quot[1] * 0.6, quot[2] * 0.6); + } + } + // The rules under the products. + hsv(h0, 0.5, 0.62, ink); + for (const r of Sh.rules) { + if (t < r.t0) break; + const [xa, y] = at(r.row, r.a), [xb] = at(r.row, r.b); + if (y + 8 < yMinWork || y >= H || xb + 6 < 0 || xa - 1 >= W) continue; + const u = cl((t - r.t0) / r.d, 0, 1); + hline(S, xb + 6 - round((xb + 7 - xa) * u), xb + 6, y + 8, ink, yMinWork); + } + if (Sh.midyRule && t >= Sh.midyRule.t0) { + const u = cl((t - Sh.midyRule.t0) / Sh.midyRule.d, 0, 1), y = rowY(12) + 8 - out; + hline(S, X0, X0 + round((Sh.midyRule.b * CW + 7) * u), y, ink, -99); + } + // Every glyph written so far. + const glowOn = t >= Sh.tEnd; + for (let i = 0; i < Sh.items.length; i++) { + const it = Sh.items[i]; + if (t < it.t0) break; + let x, y, pinned; + if (it.k === K_MIDY || it.k === K_SUM) { + x = X0 + it.col * CW + 1; + y = rowY(it.row) - out; + pinned = false; + } else [x, y, pinned] = at(it.row, it.col); + let u = (t - it.t0) / it.d; + if (it.k === K_DOWN && u < 1) { + // The zero falls from the dividend row to its place. + const y1 = rowY(1) - out, e = ease(u); + y = round(y1 + (y - y1) * e); + u = 1; + } + if (x <= -6 || x >= W || y <= -8 || y >= H) continue; + const yMin = it.row >= 2 ? yMinWork : -99; + let rgb; + if (it.glow && glowOn) rgb = gold; + else { + const k = it.k; + if (k === K_DIV) hsv(h0, 0.3, 0.95, c); + else if (k === K_QUOT || k === K_Q0 || k === K_MIDY) hsv(h0 + 0.25, 0.7, 1, c); + else if (k === K_SUM) hsv(0.13, 0.55, 1, c); + else if (it.half) hsv(h0 - 0.17, 0.6, 1, c); + else hsv(h0, 0.7, k === K_PROD ? 0.78 : k === K_DOWN ? 0.88 : 1, c); + // Fresh graphite: white as it is written, settling into its colour. + const w = 1 - cl((t - it.t0 - it.d) / 0.35, 0, 1); + c[0] += (255 - c[0]) * w * 0.85; + c[1] += (255 - c[1]) * w * 0.85; + c[2] += (255 - c[2]) * w * 0.85; + rgb = c; + } + glyph(S, it.g, x, y, cl(u, 0, 1), rgb, i * 13 + 5, yMin, pinned ? xCut : -99); + } + // The bar over the repetend, drawn from its last digit back to its first. + if (Sh.vin && t >= Sh.vin.t0) { + const v = Sh.vin, u = ease((t - v.t0) / v.d); + const [xa] = at(0, v.a), [xb] = at(0, v.b), y = y0 - 2; + const xl = round(xb + 6 - (xb + 6 - xa) * u); + hline(S, max(xl, xCut, fx - 1), xb + 6, y, gold, -99); + } + for (let p = 0; p < W * H; p++) display.setPixel(p & 63, p >> 6, buf[p * 3], buf[p * 3 + 1], buf[p * 3 + 2]); +} diff --git a/incoming/pattern-study/patterns/sedimenting-cloud.js b/incoming/pattern-study/patterns/sedimenting-cloud.js new file mode 100644 index 000000000..cf946fe08 --- /dev/null +++ b/incoming/pattern-study/patterns/sedimenting-cloud.js @@ -0,0 +1,284 @@ +// ===== Patternflow pattern ===== +// Title: Sedimenting Cloud +// Family: particle +// Idea: A ball of heavy particles sinks through syrup as one body, each particle moved by its own weight and the Stokeslet flow of all the others, so the cloud falls seven times faster than one particle alone, turns over inside like a drop, sheds a thin tail upward, opens a hole on its axis and comes apart, drawn as an elevation over a plan with the camera riding along. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Stokes' law (George Gabriel Stokes, 1851), the viscous drop (Jacques Hadamard and Witold Rybczynski, 1911), regularized Stokeslets (Ricardo Cortez, 2001), falling clouds of particles (Johannes Nitsche and George Batchelor, 1997; Bloen Metzger, Maxime Nicolas and Elisabeth Guazzelli, 2007), pattern round 65 +// Round: 65 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Particles=300..600, View=0..1 +// +// Knob 1: Hue. Turns the colours round the wheel. At 0 a particle sinking through the cloud faster than the cloud is cyan and one rising through it is violet, so the elevation shows the column going down the middle and the shell coming up the outside; the tail is pale gold. +// Knob 2: Speed. At 2 the story runs at 8 T a second, T being the time the cloud's collective speed takes to cover one starting radius, with one Adams-Bashforth step a frame (a step is never longer than 0.3 T). With 300 particles the hole on the axis shows in the plan at about 20 s and the cloud comes apart at about 31 s. At 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Particles. 300 to 600, in tens. The particles keep their spacing, so the cloud is drawn 13 LEDs across its radius at 300 and 16.4 at 600. Fewer particles come apart sooner: of 8 clouds of 300, six split between 176 and 310 T and two thinned into the tail instead; four clouds of 450 split between 349 and 435 T; of four clouds of 600, two split at about 455 T. A new cloud starts 40 T after a split, when the tail holds 65% of the particles, or at 300 + 0.4 N T. +// Knob 4: View. Turns the cloud about the vertical by up to half a turn, in both views at once. The plan, below the folding line, is the cloud seen from above with its far side up, laid out under the elevation as a first-angle drawing has it. + +const W = 64, H = 128; +const { floor, min, max, abs, sqrt, exp, log, sin, cos, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// Units: the cloud's starting radius is 1 and a lone particle settles at 1, +// z pointing down. N a / R = 5, so a particle's Stokeslet carries +// F / (8 pi mu) = 3a/4 = 3.75 / N, and the cloud should fall at about +// 1 + 1.2 N a / R = 7, the speed of a drop as viscous as the fluid around it +// (Hadamard and Rybczynski). T, the time for that collective speed of 6 to +// cover one radius, is the clock the story is told in. +const NAR = 5, T = 1 / (1.2 * NAR), EPS = 0.05, E2 = EPS * EPS; +// The drawing: a radius is 13 LEDs for 300 particles, and grows as the cube +// root of the count, so the particles keep their spacing on the panel. The +// elevation has the cloud's centre at row YS and squeezes heights above U0 +// radii logarithmically with scale LS; the plan has its centre at row YP. +const YS = 38, YP = 97, U0 = 1.5, LS = 0.8, XY = 68; + +export function setup(params) { + params.s = { n: 0, buf: new Float32Array(W * H * 3), c: [0, 0, 0], frame: 0 }; +} + +function newCloud(S, n) { + S.n = n; + S.k = 0.75 * (NAR / n); + for (const key of ["x", "y", "z", "vx", "vy", "vz", "wx", "wy", "wz"]) S[key] = new Float64Array(n); + S.tail = new Uint8Array(n); + for (let i = 0; i < n; i++) { + let x, y, z; + do { + x = 2 * Math.random() - 1; + y = 2 * Math.random() - 1; + z = 2 * Math.random() - 1; + } while (x * x + y * y + z * z > 1); + S.x[i] = x; + S.y[i] = y; + S.z[i] = z; + } + S.t = 0; + S.hp = 0; + S.seen = 0; + S.cx = S.cy = S.cz = 0; + S.camX = S.camY = S.camZ = 0; + S.split = -1; + S.fade = 0; +} + +// Velocities: each particle's own settling plus the regularized Stokeslet of +// every other, G = [I (r^2 + 2 e^2) + r r^T] / (r^2 + e^2)^(3/2) applied to +// the weight z. G is even in r, so one pair gives both particles the same +// vector. +function velocity(S, X, Y, Z, VX, VY, VZ) { + const n = S.n; + VX.fill(0); + VY.fill(0); + VZ.fill(0); + for (let i = 0; i < n; i++) { + const xi = X[i], yi = Y[i], zi = Z[i]; + let ax = 0, ay = 0, az = 0; + for (let j = i + 1; j < n; j++) { + const dx = X[j] - xi, dy = Y[j] - yi, dz = Z[j] - zi; + const s = dx * dx + dy * dy + dz * dz + E2, inv = 1 / (s * sqrt(s)); + const vx = dx * dz * inv, vy = dy * dz * inv, vz = (s + E2 + dz * dz) * inv; + ax += vx; + ay += vy; + az += vz; + VX[j] += vx; + VY[j] += vy; + VZ[j] += vz; + } + VX[i] += ax; + VY[i] += ay; + VZ[i] += az; + } + const k = S.k; + for (let i = 0; i < n; i++) { + VX[i] *= k; + VY[i] *= k; + VZ[i] = VZ[i] * k + 1; + } +} + +// One step of the two-step Adams-Bashforth rule, one velocity sum a step, +// with the weight of the last velocity scaled for a step that changed size. +function step(S, h) { + const n = S.n, { x, y, z, vx, vy, vz, wx, wy, wz } = S; + velocity(S, x, y, z, vx, vy, vz); + const c = S.hp > 0 ? h / (2 * S.hp) : 0; + for (let i = 0; i < n; i++) { + x[i] += h * (vx[i] + c * (vx[i] - wx[i])); + y[i] += h * (vy[i] + c * (vy[i] - wy[i])); + z[i] += h * (vz[i] + c * (vz[i] - wz[i])); + wx[i] = vx[i]; + wy[i] = vy[i]; + wz[i] = vz[i]; + } + S.hp = h; + S.t += h / T; +} + +// The cloud's centre: the mean of the particles that are not in the tail. +function centre(S) { + const n = S.n, { x, y, z, tail } = S; + let sx = 0, sy = 0, sz = 0, m = 0; + for (let i = 0; i < n; i++) { + if (tail[i]) continue; + sx += x[i]; + sy += y[i]; + sz += z[i]; + m++; + } + if (m > 0) { + S.cx = sx / m; + S.cy = sy / m; + S.cz = sz / m; + } +} + +// Friends of friends with a link of 0.32 radii. A particle in a group of +// fewer than ten, more than 1.2 radii above the centre, is in the tail; the +// groups of 15% or more are returned with their centres. +function groups(S) { + const n = S.n, { x, y, z } = S, lab = new Int32Array(n).fill(-1), out = [], sizes = [], stack = []; + const L2 = 0.32 * 0.32; + for (let i = 0; i < n; i++) { + if (lab[i] >= 0) continue; + const g = sizes.length; + lab[i] = g; + stack.push(i); + let size = 0, sx = 0, sy = 0; + while (stack.length) { + const p = stack.pop(); + size++; + sx += x[p]; + sy += y[p]; + for (let j = 0; j < n; j++) { + if (lab[j] >= 0) continue; + const dx = x[j] - x[p], dy = y[j] - y[p], dz = z[j] - z[p]; + if (dx * dx + dy * dy + dz * dz < L2) { + lab[j] = g; + stack.push(j); + } + } + } + sizes.push(size); + if (size >= 0.15 * n) out.push({ size, x: sx / size, y: sy / size }); + } + for (let i = 0; i < n; i++) S.tail[i] = sizes[lab[i]] < 10 && z[i] - S.cz < -1.2 ? 1 : 0; + return out.sort((a, b) => b.size - a.size); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 300, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0] || 0; + S.speed = cl(kv[1], 0.2, 3); + S.view = cl(kv[3], 0, 1); + const n = Math.round(cl(kv[2], 300, 600) / 10) * 10; + if (n !== S.n || (input.btnPressed && input.btnPressed[0])) newCloud(S, n); + // Speed 2 tells the story at 8 T a second, one step a frame. + const h = d * S.speed * 4 * T, parts = Math.ceil(h / (0.3 * T)); + for (let p = 0; p < parts; p++) step(S, h / parts); + S.frame++; + if (S.frame % 12 === 0) { + const big = groups(S); + // Split: the two biggest groups lie more than a radius apart across, three + // looks running (the ring's groups flicker before it really parts). + const apart = big.length >= 2 && Math.hypot(big[0].x - big[1].x, big[0].y - big[1].y) > 1; + S.seen = apart ? S.seen + 1 : 0; + if (S.split < 0 && S.seen >= 3) S.split = S.t; + } + centre(S); + // After a split the daughters fall apart for 40 T; then a new cloud. So + // too when the tail has taken 65% of the particles, or at 300 + 0.4 N T. + let left = 0; + for (let i = 0; i < n; i++) left += 1 - S.tail[i]; + if ((S.split >= 0 && S.t > S.split + 40) || left < 0.35 * n || S.t > 300 + 0.4 * n) S.fade += (d * S.speed) / 2; + if (S.fade > 1) newCloud(S, n); + const a = 1 - exp(-d * 6); + S.camX += (S.cx - S.camX) * a; + S.camY += (S.cy - S.camY) * a; + S.camZ += (S.cz - S.camZ) * min(1, a * 2); +} + +// The side view's row for a height u above the centre, in radii. +function rowOf(u, RP) { + if (u <= U0) return YS - RP * u; + return YS - RP * (U0 + LS * log(1 + (u - U0) / LS)); +} + +function splat(buf, px, py, r, g, b, I) { + if (px < -1 || px >= W || py < -1 || py >= H) return; + // Bilinear, so a particle moving a fraction of an LED still moves. + const x0 = floor(px), y0 = floor(py), fx = px - x0, fy = py - y0; + for (let q = 0; q < 4; q++) { + const xx = x0 + (q & 1), yy = y0 + (q >> 1); + if (xx < 0 || xx >= W || yy < 0 || yy >= H) continue; + const wgt = (q & 1 ? fx : 1 - fx) * (q >> 1 ? fy : 1 - fy) * I; + const o = (yy * W + xx) * 3; + buf[o] += r * wgt; + buf[o + 1] += g * wgt; + buf[o + 2] += b * wgt; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.n) return; + const { buf, c } = S, n = S.n; + for (let p = 0; p < buf.length; p++) buf[p] *= 0.86; + const ph = S.view * PI, cp = cos(ph), sp = sin(ph), RP = 13 * Math.cbrt(n / 300); + const fade = (1 - cl(S.fade, 0, 1)) * cl(S.t / 3, 0, 1); + const tailC = [0, 0, 0]; + hsv(S.hue + 0.14, 0.5, 1, tailC); + // The cloud's own sinking speed, to colour each particle by how fast it + // sinks or rises through the cloud. + let vm = 0, m = 0; + for (let i = 0; i < n; i++) + if (!S.tail[i]) { + vm += S.wz[i]; + m++; + } + vm /= max(1, m); + for (let i = 0; i < n; i++) { + const ax = S.x[i] - S.camX, ay = S.y[i] - S.camY, dz = S.z[i] - S.camZ; + // Turned about the vertical by the View knob: across the panel and depth. + const sx = ax * cp + ay * sp, w = ay * cp - ax * sp; + const px = 32 + sx * RP; + if (S.tail[i]) { + splat(buf, px, rowOf(-dz, RP), tailC[0], tailC[1], tailC[2], 0.55 * fade); + continue; + } + // Sky blue for a particle sinking through the cloud, violet for one + // rising through it. + const rise = cl(vm - S.wz[i], -1, 1); + hsv(S.hue + 0.6 + 0.16 * rise, 0.5 + 0.3 * abs(rise), 1, c); + // Elevation, lit by nearness to the viewer. + let near = cl(0.5 - w / 2.4, 0, 1); + splat(buf, px, rowOf(-dz, RP), c[0], c[1], c[2], (0.14 + 0.34 * near) * fade); + // Plan, seen from above and laid out below the elevation as in a + // first-angle drawing: the far side at the top, lit by height. + near = cl(0.5 - dz / 2, 0, 1); + splat(buf, px, YP - w * RP, c[0], c[1], c[2], (0.14 + 0.34 * near) * fade); + } + // The folding line between the two views, dotted and dim. + for (let x = 1; x < W; x += 3) { + const o = (XY * W + x) * 3; + buf[o] = max(buf[o], 0.1); + buf[o + 1] = max(buf[o + 1], 0.1); + buf[o + 2] = max(buf[o + 2], 0.12); + } + // Light adds up where particles crowd; the brightest channel is eased into + // range and the other two keep their ratio to it, so crowds stay coloured. + for (let p = 0; p < W * H; p++) { + const o = p * 3, L = max(buf[o], buf[o + 1], buf[o + 2]); + const k = L > 1e-4 ? (255 * (1 - exp(-1.6 * L))) / L : 0; + display.setPixel(p & 63, p >> 6, buf[o] * k, buf[o + 1] * k, buf[o + 2] * k); + } +} From e57a1d4da28984b04c43a685ead96a18105e6ce9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 11:18:15 +0000 Subject: [PATCH 07/10] incoming: README notes for pattern round 65 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JkphLb6hZbrmiGJN5Xd --- incoming/pattern-study/README.md | 101 +++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 4 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index bdcd0a73f..286285b1d 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 548개를 쉰여섯 갈래로 나눠 만들었어요. 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판은 주제 표가 바뀌어 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 네 갈래에 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 558개를 쉰여섯 갈래로 나눠 만들었어요. 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판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. [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판에서 세로로 다시 짰어요. @@ -870,6 +870,21 @@ | [Hill's Regions](patterns/hill-regions.js) | 입자 | 두 질량을 위아래로 세운 회전 좌표계에서 입자 일곱이 야코비 상수의 영속도 곡선 안에 갇혀 돌아요. Jacobi 를 내리면 L1 부터 L3 까지 목이 차례로 열려서 입자가 작은 질량 쪽으로 건너가고 패널 위로 빠져나가요. | | [Indian Rope Trick](patterns/indian-rope-trick.js) | 물리·스프링 | 세 마디 막대 사슬이 거꾸로 서서 패널 키를 다 채우고, 눈에 보이지 않을 만큼 작고 빠르게 떨리는 받침점 덕에 쓰러지지 않아요. 몇 초마다 톡 치면 흔들리다 수직으로 돌아오고, Shake 를 문턱 아래로 내리면 넘어져 받침 아래로 휘돌았다가 다시 세워져요. | +### 65판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Long Division](patterns/long-division.js) | 글자 | 1 을 소수로 나누는 손 나눗셈이 세로셈 계단으로 내려가다, 나머지가 처음 수로 돌아오면 금빛이 되고 순환마디 위에 선이 그어져요 | +| [Inversions](patterns/inversions.js) | 글자 | 위아래로 쌓은 두 음절이 판째 반 바퀴 돌면 곰국이 논문으로 읽혀요. 라틴 글자는 MOM 이 WOW 가 되고 SWIMS 는 제자리로 와요 | +| [Anamorphosis](patterns/anamorphosis.js) | 글자 | 깊이가 제각각인 판과 상자 열세 개에 한 시점에서 비춘 것처럼 낱말을 칠해 두고, 카메라가 둘레를 8자로 날아요. 그 시점을 지나는 1초 남짓만 색 조각이 이어져 HERE 가 읽혀요 | +| [Hunminjeongeum](patterns/hunminjeongeum.js) | 글자 | 해례본 제자해대로 기본자 ㄱ ㄴ ㅁ ㅅ ㅇ 에서 한 단 내려갈 때마다 새로 더한 획만 빛나며 자음 나무 다섯 열이 자라고, 아래에서는 ㆍ ㅡ ㅣ 가 모여 모음 여덟이 돼요 | +| [Mouse's Tale](patterns/mouse-tale.js) | 글자 | 루이스 캐럴의 생쥐 이야기가 책에 찍힌 꼬리 모양 그대로 패널을 타고 내려가요. 아래로 갈수록 글자가 5×7 에서 점으로 작아지고, 꼬리는 천천히 흔들려요 | +| [Foam Coarsening](patterns/foam-coarsening.js) | 보로노이·셀 | 2차원 거품이 굵어지며 여섯 변보다 적은 칸은 줄어 사라지고 많은 칸은 자라요. 칸을 변의 수로 칠해서 폰 노이만의 n − 6 법칙이 색으로 보여요 | +| [McMullen Carpet](patterns/mcmullen-carpet.js) | 프랙탈 | 가로 m 칸, 세로 n 칸으로 나눈 칸 몇 개 안에 같은 나눔을 되풀이한 자기 아핀 카펫이 가로 m 배, 세로 n 배로 커지며 한 단마다 같은 그림으로 돌아와요 | +| [Klein's Sails](patterns/klein-sails.js) | 수론 | 정수 격자가 쌍곡선을 따라 흐르고, 연분수 수렴분수를 꼭짓점 삼은 볼록 껍질 네 줄이 제 몸을 따라 미끄러지다 처음 그림으로 돌아와요 | +| [Jacobi Geodesics](patterns/jacobi-geodesics.js) | 공간·원근 | 세 축이 다른 타원면이 패널 키로 서서 돌고, 배꼽점 하나에서 사방으로 쏜 측지선 36개가 몸을 감싸며 내려가 맞은편 배꼽점 한 점에 다시 모여요 | +| [Sedimenting Cloud](patterns/sedimenting-cloud.js) | 입자 | 점성 유체 속 입자 구름이 한 덩이로 가라앉으며 안에서 뒤집히고 꼬리를 흘리다 고리가 되어 흩어져요. 위는 정면도, 아래는 위에서 본 평면도예요 | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -3424,6 +3439,70 @@ Lab 에서 해 보기: Text 레이어에 Newsreader 로 IT WAS 를 크게, 위 주제 5 와 4 로 아치슨의 인도 밧줄 묘기를 물리·스프링 갈래에 세웠어요. 앤드루 스티븐슨(1908)은 받침점을 위아래로 빠르게 떨면 거꾸로 선 진자가 선다는 것을 보였고, 표트르 카피차(1951)가 빠른 떨림을 평균한 유효 퍼텐셜로 그 까닭을 풀었어요. 데이비드 아치슨(1993)은 마디가 여럿인 사슬도 떨림으로 세울 수 있다는 정리를 냈고, 톰 멀린과 함께 『Nature』(1993)에 세 마디 진자를 거꾸로 세운 실험을 실었어요. 패널은 똑같은 막대 n 개를 경첩으로 이은 사슬의 라그랑주 운동 방정식을 받침점 좌표계의 중력 g − Aω² cos ωt 로 풀어요. 계산마다 n×n 질량 행렬을 풀고, 4차 룽게-쿠타로 떨림 한 주기를 25걸음씩 나눠 세 마디일 때 1초에 1,663걸음을 셈해요. 사슬 키 116 LED 에 대해 √(L/g) 를 0.75초로 두었더니 세 마디의 받침점은 66 Hz 로 0.74 LED 만 오르내려서, 패널에서는 떨림이 거의 보이지 않고 사슬이 그냥 서 있는 것처럼 보여요. Shake 를 사슬 전체 키로 잰 받침점 최고 속도 Aω/√(gL) 로 두자, 세 마디부터 여섯 마디까지 서는 문턱이 모두 1.25~1.3 으로 거의 같았어요. 막대 하나의 카피차 문턱 √(4/3) = 1.155 에 가까운 값이에요. 처음 판은 마디 마찰만 넣어서 톡 친 흔들림이 24초 동안 거의 줄지 않았고, 3초마다 오는 다음 탭이 쌓여 꼭대기가 29 LED 까지 벌어졌어요. 질량 행렬에 비례하는 공기 저항을 더하자 한 번 흔들릴 때마다 0.6 배로 줄며 수직으로 돌아왔어요. 마디의 경로를 실처럼 남기던 판은 가로줄이 사슬에 걸린 깃발처럼 읽혀서 지우고, 0.3초 잔광으로 흔들린 자리를 부채꼴로 남겼어요. 잰 값은 움직임 0.251, 촘촘함 0.019, 가장자리 0.321, 복잡도 0.452, 채움 0.145, 색 0.122, 모양 바뀜 0.104, thirds 16/12/15 이고, 노브는 0.04, 0.09, 0.03, 0.12 예요. 뜻밖이었던 것은 너무 세게 떨 때였어요. 서 있는 범위에는 위쪽 끝도 있어서, 여섯 마디는 Shake 2.15 부터, 다섯 마디는 2.7 부터 다시 무너져요. Shake 3 의 여섯 마디는 옆으로 넘어지는 대신 받침점 위에서 접히고 뒤엉켜 한 덩어리가 돼요. +## 65판 + +65판은 64판과 같은 표로 돌아서, 열 개 가운데 다섯이 주제 7 「타이포」이고 다섯이 주제 5 「수학 · 추상」이에요. 같이 돈 66판(#329)보다 먼저 고른 열 개와 지도 칸 둘을 PR 본문에 밝혔고, 66판이 그것을 읽고 겹치지 않게 골랐어요. 앞 바퀴 PR 65개에는 OWNER, MEMBER, COLLABORATOR 가 쓴 댓글도 리뷰도 없어서 따를 글이 없었어요. 타이포 다섯은 63판과 64판이 맡지 않은 생각을 하나씩 골랐어요. Long Division 은 숫자 놀이를 세로셈으로 하고, Inversions 는 돌려야 다른 글로 읽히고, Anamorphosis 는 한 시점에서만 글자가 맞춰지고, Hunminjeongeum 은 낱자가 어느 낱자에서 나왔는지를 그리고, Mouse's Tale 은 글이 모양의 길을 따라 흐르며 작아져요. 글자 꼴도 다섯이 달라요. 6×7 손글씨 숫자, 둥근 끝의 획과 타원 고리로 그린 자모 열한 개와 라틴 열두 자, 사각형 조각으로 적은 HERE 와 한글 넉 자, 단위 상자에 적은 획 78개, 5×7 에서 한 점까지 다섯 크기로 줄어드는 소문자예요. 수학 다섯은 548개에 없던 대상이고 「낯선 수학 예」에서는 하나도 고르지 않았어요. 추상 갈래 가운데 열아홉으로 가장 적은 보로노이·셀, 프랙탈, 수론, 공간·원근에 하나씩 넣고 입자에 하나를 넣었어요. 처음에는 공간·원근에 빌라르소 원을 두려다 접었어요. 토러스를 비스듬히 자른 두 원은 29판 Hopf Fibration 의 섬유와 같은 원이에요. 크루스칼-세케레시 도형도 60판 Penrose Diagram 이 가속 관측자의 지평선까지 그려서 접었고, 에밀 포스트의 태그 시스템은 테이프를 시간 아래로 쌓는 그림이 62판 Busy Beaver 와 같아서 접었어요. 주제 1 은 548개의 셋뿐인 칸 열셋 가운데 촘촘함 × 색 4,4 와 색 × 모양 바뀜 4,4 를 겨눴고, 주제 4 는 Long Division 과 Sedimenting Cloud 예요. + +### Long Division + +주제 7 과 4 로 손으로 하는 긴 나눗셈을 글자 갈래에 세웠어요. 1 을 소수 p 로 나누는 셈을 교과서 세로셈 꼴 그대로, 나누는 수와 괄호와 피제수 윗줄 위에 몫을 두고 가로 7, 세로 9 LED 모눈에 6×7 손글씨 숫자로 써요. 걸음마다 피제수 줄의 0 하나가 떨어져 내려와 나머지 옆에 붙고 몫 한 자리가 위에 서요. 그 아래에 곱을 오른쪽부터 쓰고 줄을 그은 뒤 나머지를 적어요. 나머지가 분자로 돌아오면 그 나머지와 피제수의 분자가 같이 금빛이 되고, 몫 위 선이 마지막 자리부터 첫 자리로 그어져요. 셈으로 세어 보니 순환마디는 7 과 13 이 6, 17 이 16, 19 가 18, 73 이 8, 97 이 96 이에요. 미디의 정리(에티엔 미디, 1836)대로 142+857=999, 076+923=999, 0136+9863=9999 이고, 1/97 의 48자리 두 반쪽을 더해도 9 가 48개 늘어서요. 반 주기 자리의 나머지는 늘 p−1 이라 그 줄을 초록으로 칠했어요. 7 에서 97 사이 소수 22개 가운데 분자를 바꿔도 같은 숫자가 자리만 돌아가는 수는 7, 17, 19, 23, 29, 47, 59, 61, 97 아홉이에요. 13 은 분자 열둘이 076923 과 153846 두 무리로 갈려요. 첫 판은 5×7 가는 숫자라 thirds 가 12/5/6 으로 가운데가 문턱에 걸렸고 Hue 반응도 0.02 였어요. 세로획을 두 LED 로 굵히고 소수점에 두 LED 를 따로 주자 thirds 20/9/11, 노브 0.05, 0.06, 0.08, 0.06 이 됐어요. 나머지 잰 값은 움직임 0.133, 촘촘함 0.088, 가장자리 0.699, 복잡도 0.389, 채움 0.140, 색 0.197, 모양 바뀜 0.096 이에요. 새로 본 것은 이 패널이 1/7 의 판과 꼭 맞는다는 거예요. 나누는 수 한 칸, 괄호 3 LED, 분자 한 칸, 소수점 2 LED, 0 여섯 칸이 64 LED 에 들고 열네 줄이 126 LED 라서 1/7 은 화면이 한 번도 움직이지 않고 끝나요. 판이 넘치는 1/17 부터는 표 계산기의 틀 고정처럼 몫 줄과 피제수 줄을 위에 붙여 두고 화면이 계단을 따라 오른쪽 아래로 가요. 이때 고정된 「0.」 바로 뒤에 흘러온 자리가 붙어 0.0103 이 0.03 으로 읽혀서, 한 칸을 비우고 말줄임표를 찍었어요. + +Lab 에서 해 보기: Text 레이어에 Newsreader 로 1/7 을 size 16 쯤, 색 #3c3c3c 로 x 40, y 22 에 두면 계단이 비워 둔 오른쪽 위에 공책 머리말처럼 문제가 서요. Divisor 를 돌려도 글은 따라 바뀌지 않으니 그때는 낱말을 손으로 고쳐요. + +기능 감: 패턴 코드가 Lab 의 픽셀 글꼴을 비트맵으로 빌려 쓰는 길(font.glyph("A") 같은 것)이 있으면 좋겠어요. 이번에는 0~9 와 A~F 를 6×7 로 직접 그렸어요. 지금 몇 진법인지 적는 머리말(base 12)은 글자를 더 그려야 해서 빼서, A~F 가 나오기 전에는 판에서 진법이 보이지 않아요. + +### Inversions + +주제 7 「타이포」로 180도 돌려도 글로 읽히는 낱말을 글자 갈래에 세웠어요. 스콧 킴이 『Inversions』(1981)에서 라틴 글자로 한 일을 한글에 옮기면, 가로 모음을 가진 받침 글자는 돌릴 때 초성과 받침이 자리를 바꿔요. ㄱ 과 ㄴ, ㅗ 와 ㅜ 가 서로 바뀌고 ㅁ ㅇ ㄹ ㅡ 는 그대로예요. 그래서 64×64 칸 둘에 위아래로 쌓은 「곰국」을 판째 반 바퀴 돌리면 「논문」이 되고, 「공공」과 「운운」, 「눅눅」과 「녹녹」도 짝이며 「용융」은 제자리예요. 글자는 둥근 끝의 선과 타원 고리로 그리고 LED 하나를 16번 표본해서, 돌린 비트맵과 짝 낱말을 따로 그린 비트맵을 표본 수까지 견줬어요. MOM↔WOW, NOON, SWIMS, pod, suns 를 더한 낱말 아홉에 굵기를 곱한 244 쌍이 모두 0 개 차이였고, 한 음절 짝 곡↔눈, 극↔는, 응, 근 도 같았어요. 받침 상자는 초성 상자와 같은 크기로, 칸 가운데를 축으로 돌린 자리에 둬야 해요. 보통 글꼴처럼 받침을 넓고 낮게 그리면 곰국과 논문이 켜진 LED 2,617개 가운데 612개에서 갈리고, ㄱ 의 내리긋는 획만 3 LED 비스듬히 해도 106개가 갈려요. 돌 때는 낱말이 90도에서 0.53 배로 줄어 128줄이 64칸에 눕고, 지나간 획은 0.09초 잔광으로 짧은 호를 남겨요. 색은 낱말이 아니라 패널에 붙어 있어서 반 바퀴 뒤에도 위 음절은 윗줄 색이에요. 잰 값은 움직임 0.651, 촘촘함 0.053, 가장자리 0.636, 복잡도 0.386, 채움 0.330, 색 0.370, 모양 바뀜 0.274, thirds 28/41/28 이고, 노브는 0.18, 0.23, 0.25, 0.25 예요. 이 놀이는 한글이 라틴 글자보다 잘 맞았어요. 한글은 반 바퀴 한 번에 한 칸의 자모 셋이 한꺼번에 바뀌는데, 라틴 낱말은 다섯 글자를 쌓으면 한 글자에 25줄뿐이라 SWIMS 의 S 가 16줄 상자에 눌려요. + +Lab 에서 해 보기: Word 를 6(SWIMS)에 두고 Text 레이어에 Silkscreen 으로 SWIMS 를 size 8 쯤 맨 아래 가운데에 얹으면, 반 바퀴마다 같은 글로 돌아오는 패턴 글자 밑에 돌지 않는 같은 낱말이 서요. 돌 때는 패턴 글자의 아래 끝이 레이어 글자에 겹치니 effect 를 twinkle 로 두어 가끔만 보이게 해요. + +기능 감: Text 레이어에 각도를 노브에 거는 돌리기 효과가 있고, 돌린 글이 다른 글과 LED 까지 같은지 견주는 도구가 있으면 좋겠어요. 지금은 자모 열한 개와 라틴 열두 자를 획으로 그렸고, 짝이 맞는지는 Lab 밖의 Node 스크립트로 셌어요. Text 레이어에 한글 글꼴이 없어서 레이어 쪽에서는 한글 짝을 시험할 수 없었어요. + +### Anamorphosis + +주제 7 과 주제 1 로 착시 원근을 글자 갈래에 세우고, 548개 가운데 Heawood Torus 와 Marbling, Soap Film 셋뿐이던 색 × 모양 바뀜 4,4 칸을 겨눴어요. 색이 0.5445 를 넘으면서 모양 바뀜이 0.3281 을 넘는 칸이에요. 한스 홀바인의 『대사들』(1533)은 그림 옆에 비스듬히 서야 해골이 보이고, 펠리체 바리니의 벽화는 정해진 한 점에 서야 여러 벽에 흩어진 조각이 도형 하나로 이어져요. 패널은 그 한 점 V 에서 본 그림을 사각형 열세 조각으로 잘라요. 조각마다 V 에서 5.4~14.6 단위 떨어진 판 여덟, 상자 넷, 기둥 하나 가운데 하나를 거리에 비례한 크기로 세워서, V 에서는 앞면들이 그림을 빈틈없이 덮어요. 카메라의 LED 마다 광선을 쏘아 맞은 점을 V 의 그림으로 되돌려 보고, 그 자리가 글자 칸이고 V 와 그 점 사이를 막는 것이 없으면 그 글자의 색으로 칠해요. V 에 영사기를 둔 것과 같아서, V 의 눈높이보다 낮은 상자는 윗면에도 칠이 묻어요. 카메라는 좌우 52도, 위아래 17도의 8자를 날다가 V 에서만 4분의 1 빠르기로 늦춰서, Speed 2 에서 4초와 그 뒤 8초마다 HERE 가 1초 남짓 온전히 서요. 첫 판은 칠을 면 밝기에 따라 0.82~1 로 두고 3.2초에 V 를 지나서 색 0.493, 모양 바뀜 0.323 으로 두 문턱에 다 못 미쳤어요. 앞을 보는 면의 칠을 1 로 올리고 V 를 4초로 늦춰, 재기 시작하는 2초째 화면이 아직 21도 옆에서 보이게 하자 두 문턱을 넘었어요. 지금 판은 색 0.616, 모양 바뀜 0.396 이고, 움직임 0.608, 촘촘함 0.067, 가장자리 0.626, 복잡도 0.594, 채움 0.792, thirds 76/83/78, 노브는 0.34, 0.28, 0.21, 0.21 이에요. 이 패널에서 새로 본 것은 낱말이 읽히는 각도가 좁다는 거예요. V 에서 6.6도 비낀 3초째에는 획이 조각마다 어긋나 두 겹으로 겨우 읽히고, 21도 비낀 2초째에는 무슨 낱말인지 알 수 없어요. Flatten 을 1 로 두어 조각을 한 평면에 모으면 같은 비행 내내 기울어진 간판처럼 읽혀서, 깊이가 흩어진 것만으로 낱말이 한 점에 묶인다는 것을 노브 하나로 볼 수 있어요. + +Lab 에서 해 보기: Text 레이어를 mask 로 얹고 Inter 로 E, Y, E 를 세 줄로 size 36 쯤 써서 가운데에 두면, 붙박인 EYE 모양 창 안으로만 조각이 지나가요. 화면의 낱말은 늘 읽히고 공간에 칠한 낱말은 V 에서만 읽혀서, 두 가지 읽힘이 한 화면에 겹쳐요. + +기능 감: 아래 레이어의 비트맵을 코드 레이어가 텍스처로 받는 길이 있으면 좋겠어요. 이 패턴이 V 에서 비추는 것은 64×128 그림 한 장이라 Text 레이어의 글이나 픽셀 레이어에 그린 그림이 그대로 들어갈 자리예요. 그런데 지금은 낱말 다섯과 한글 네 자(여, 기, 지, 금)를 획으로 손수 적어 코드에 박아야 했어요. + +### Hunminjeongeum + +주제 7 「타이포」의 「글자가 만들어지는 과정」으로, 『훈민정음』 해례본(1446) 제자해가 적은 가획 계보를 글자 갈래에 세웠어요. Jamo Assembly 가 자모를 한 칸에 모으고 Wongoji 가 원고지에 시를 썼다면, 여기서는 낱자가 어느 글자에서 나왔는지가 주인공이에요. 위쪽 다섯 열에 기본자 ㄱ ㄴ ㅁ ㅅ ㅇ 이 획 차례대로 그어지고, 한 단 내려갈 때마다 윗글자가 내려와 자리를 내준 뒤 새로 더한 획만 하늘색으로 빛나며 그어져요. 초성은 기본 다섯에 가획 아홉(ㅋ, ㄷ ㅌ, ㅂ ㅍ, ㅈ ㅊ, ㆆ ㅎ)과 이체 셋(ㆁ ㄹ ㅿ)을 더해 열일곱이고, 중성은 ㆍ ㅡ ㅣ 에 초출 넷과 재출 넷을 더해 열하나라서 모두 스물여덟 자예요. 이체 셋은 이어 주는 선 없이 따로 한 줄에 연보라로 떠오르고, 아래쪽에서는 점과 획이 날아와 ㅗ ㅏ ㅜ ㅓ 가 된 뒤 점 하나가 더 붙어 ㅛ ㅑ ㅠ ㅕ 가 돼요. 해례본 꼴로 획을 세면 ㅂ 과 ㅍ 이 똑같이 넷이라, 패널은 ㅍ 단에서 획을 더하지 않고 ㅂ 의 획을 옮겨 지은 뒤 하늘색 획 없이 남겨 둬요. 국립국어원 한글 자료도 ㅁ, ㅂ, ㅍ 은 가획만으로 풀기 어렵다고 적어요. Style 노브를 돌리면 둥근 점과 고른 기하 획이 오늘날 꼴로 옮겨 가요. 점은 줄기에 붙은 짧은 획이 되고, 지금 쓰지 않는 ㆍ ㆁ ㆆ ㅿ 이 흐려져서 스물넷이 남아요. 잰 값은 움직임 0.096, 촘촘함 0.082, 가장자리 0.654, 복잡도 0.454, 채움 0.221, 색 0.161, 모양 바뀜 0.087, thirds 30/17/18 이고, 노브는 0.04, 0.10, 0.03, 0.08 이에요. 첫 판은 노브를 재는 2~3초에 둘째 단까지만 서 있어서 Style 반응이 0.0106 으로 문턱 아래였어요. 단 사이를 1.3초에서 1.05초로 당겨 바뀌는 글자를 그 창에 더 넣고 잉크도 금빛에서 흰빛으로 식게 하자 0.03 이 됐어요. 폭 64 LED 에 다섯 열이라 자음 한 자가 12×14 LED 인데, 지름 5.5 LED 인 ㅎ 의 동그라미와 꼭지도 1.6 LED 획으로 읽혀요. + +Lab 에서 해 보기: Text 레이어에 system:serif 로 訓民正音 을 한 줄에 한 자씩 size 30 쯤으로 쓰고, 색을 #1c2233 처럼 어둡게 해서 blend add 로 얹어요. 그러면 나무 사이 빈자리에 책 이름이 비쳐요. 획 위에서는 더해진 빛이 거의 보이지 않아요. + +기능 감: 글자를 획 단위로 받는 길이요. 가획은 획 하나만 따로 밝혀야 해서 Text 레이어가 내는 비트맵으로는 할 수 없었어요. 그래서 28자를 단위 상자의 획 78개(선분 61, 점 13, 동그라미 4)로 손으로 적었어요. 획 순서와 방향을 담은 한글 획 글꼴이 Lab 에 있으면 63판이 「다음 판 감」에 적은 한글 사다리도 같은 길로 서요. + +### Mouse's Tale + +주제 7 「타이포」로, 글이 모양의 길을 따라 흐르며 작아지는 그림시(calligram)를 글자 갈래에 세웠어요. 루이스 캐럴은 『이상한 나라의 앨리스』(1865) 3장에서 생쥐가 들려주는 이야기를 생쥐 꼬리 모양으로 짰어요. 줄마다 한두 낱말을 꼬리의 굽이를 따라 옮겨 놓고 아래로 갈수록 활자를 줄였어요. 패널은 그 시를 5×7 에서 50 LED 안에 드는 한두 낱말씩 53줄로 끊고, 5×7 부터 3×5 까지 세 벌의 글꼴을 서른일곱 자씩 코드에 직접 그렸어요. 그 아래로는 글자가 위로 솟는지 아래로 처지는지만 남긴 2×3 낱말 꼴이 오고, 다음은 글자 하나에 LED 하나, 꼬리 끝은 낱말 하나에 LED 하나예요. Taper 0.06 에서는 5×7 이 위 54줄에 여섯 줄을 쓰고 26줄이 한꺼번에 걸리며, 1 에서는 42줄 가운데 30줄이 점이에요. 시는 1초에 반 줄씩 꼬리를 타고 내려가요. 위로 들어오는 줄은 맨 윗줄의 앞줄이라 위에서 아래로는 늘 시의 차례대로 읽히고, 끝줄 death 바로 아래에 첫 줄 Fury 가 와서 106초마다 한 바퀴 돌아요. 잰 값은 움직임 0.519, 촘촘함 0.111, 가장자리 1.000, 복잡도 0.098, 채움 0.088, 색 0.231, 모양 바뀜 0.042, thirds 12/9/4 이고, 노브는 0.06, 0.09, 0.07, 0.07 이에요. 첫 판은 큰 글자를 채도 0.3 의 옅은 민트로 칠해서 Hue 반응이 0.04 였어요. 채도를 0.65 로 올리자 0.06 이 됐어요. 새로 본 것은 2×3 낱말 꼴이에요. 글자는 읽히지 않는데 솟는 b, d, h, l, t 와 처지는 g, p, y 가 LED 하나씩 남아서, 길이가 같은 줄도 낱말마다 다른 무늬가 돼요. 조지 허버트의 「Easter Wings」(1633)는 넣지 않았어요. 가장 긴 줄이 44자라 3×5 로도 176 LED 가 들고, 원본처럼 옆으로 눕혀도 264줄이 필요해서 세운 패널에 날개 모양이 서지 않아요. + +Lab 에서 해 보기: Text 레이어에 Newsreader 로 tail? 을 9px 쯤 작게 써서 왼쪽 아래(x 2, y 114 언저리)에 두고, 색을 옅은 회색으로, effect 를 twinkle 에 노브를 낮게 두면 돼요. 앨리스가 tale 을 tail 로 알아들은 말장난이 꼬리 끝 옆에서 가끔 반짝여요. Sway 를 3 쪽으로 돌리면 꼬리 끝이 그 글자 위를 쓸고 지나가요. + +기능 감: 글꼴을 여러 크기로 쓸 길이 없어서 세 벌을 서른일곱 자씩 손으로 그렸고, 그래서 이 시에 나오는 소문자 스물셋과 대문자 C F I L S T W 일곱 자밖에 없어요. Text 레이어의 Silkscreen 을 크기별 비트맵으로 코드 레이어에 넘겨주는 길이 있으면 같은 꼬리에 다른 시를 실을 수 있어요. Text 레이어가 줄마다 크기와 가로 자리를 받는 길(경로를 따라 쓰기)이 있어도 돼요. + +### Foam Coarsening + +주제 1 과 5 로 2차원 거품이 굵어지는 폰 노이만의 법칙을 보로노이·셀 갈래에 세우고, 548개 가운데 Aztec Diamond, Hat Monotile, Millefiori 셋뿐이던 촘촘함 × 색 4,4 칸을 겨눴어요. 촘촘함이 0.2969 를 넘고 색이 0.5445 를 넘는 칸이에요. 변이 n 개인 칸의 넓이는 dA/dt = κ(n − 6) 로 바뀌어요(존 폰 노이만 1952, 결정립 경계는 W. W. 멀린스 1956). 패널은 앤더슨, 스롤로비츠, 그레스트, 사니(1984)처럼 64×128 토러스 위에서 큰 Q 포츠 모형을 돌려요. LED 하나에 이웃 여덟 가운데 한 결정의 번호를 권하고, 다른 결정과 맞닿는 수가 늘지 않으면 받아요. 칸은 변의 수로 칠해요. 여섯 변은 옅은 하늘색이고, 줄어드는 칸은 초록에서 주황 쪽으로, 자라는 칸은 파랑에서 분홍 쪽으로 가요. 31판 Foam Lather 도 거품이 굵어지지만 화장품 거품의 질감이고, 여기서는 칸의 색이 그 칸의 앞날을 정하는 변의 수예요. 패턴의 update 를 300초 돌려 칸을 n 으로 묶자 dA/dt 가 네 변 −3.15, 다섯 변 −1.50, 여섯 변 0.20, 일곱 변 2.05, 여덟 변 3.92, 아홉 변 5.62 LED/초였어요. 직선은 1.77(n − 5.85), r² 0.9997 이고, 한 번 훑을 때 변 하나에 0.51 LED 예요. 선이 0 을 지나는 자리가 6 이 아니라 5.85 이고 평균 변 수도 5.87 인 것은, 격자가 LED 하나도 안 되는 짧은 벽을 세지 못해 변을 조금씩 덜 세기 때문으로 보여요. 세 변 칸은 −2.52 로 네 변보다 느리게 줄었어요. 몇 LED 짜리라 격자가 벽을 굽힐 자리가 없어요. 시험 셈에서 촘촘함은 평균 31 LED 칸에서 0.29, 19 LED 칸에서 0.34 였어요. 그래서 씨앗 640개(칸당 12.8 LED)로 깔고 칸이 45% 아래로 줄면 위에서부터 1.2초에 걸쳐 새 거품을 깔아요. 처음 노브에서는 첫 새 거품이 7.0초에 내려와요. 잰 값은 움직임 0.355, 촘촘함 0.343, 가장자리 0.999, 복잡도 0.314, 채움 0.630, 색 0.628, 모양 바뀜 0.100, thirds 63/64/61 이고, 노브는 0.37, 0.30, 0.40, 0.35 예요. + +### McMullen Carpet + +주제 5 로 베드퍼드-맥멀런 자기 아핀 카펫을 프랙탈 갈래에 세웠어요. 팀 베드퍼드(박사 논문, 1984)와 커티스 맥멀런(1984)이 따로 셈한 카펫이에요. 단위 정사각형을 가로 m 칸, 세로 n 칸(n > m)으로 나눠 칸 몇 개를 고르고, 고른 칸 안에서 같은 나눔을 되풀이해요. 가로세로 줄임 비가 달라 닮음 프랙탈이 아니고, 하우스도르프 차원 log_m Σ t_j^(log_n m) 이 상자 차원 log_m r + log_n(N/r) 보다 작아요. 세로 패널이라 n 을 세로에 두면 t_j 는 줄이 아니라 j 번째 열의 칸 수여야 해요. 줄로 세면 2×6 카펫이 2.585 로 평면의 차원 2 를 넘어요. 베르누이 측도의 차원을 수치로 최대화한 값이 여섯 카펫 다 식과 넷째 자리까지 같았어요. 상자 세기의 기울기는 2진 격자 32~2048 에서 1.627(식 1.631)과 1.500(1.500)이, 3열 카펫은 3진 격자 27~6561 에서 1.608(1.611)과 1.610(1.609)이 나왔어요. 처음 열리는 2×3 카펫은 t = 3, 1 이라 하우스도르프 차원이 정확히 log₂3 = 1.585 로 시에르핀스키 삼각형과 같고, 상자 차원은 1.631 이에요. 확대는 고른 칸의 고정점을 향해 가로 m^t 배, 세로 n^t 배로 가서 t = 1 의 프레임이 처음 프레임과 평균 0.8/255 안에서 같아요. Iso 를 1 로 올려 두 방향을 같은 배율로 키우면 그림이 돌아오지 않고 넓은 띠로 납작해져요. 첫 판은 표본마다 빠진 단만으로 칠해서 깊은 단이 점 무늬 잡음이 됐고, 채움 0.349 의 어두운 남색이었어요. 칸이 표본 한 줄 높이로 얇아지는 단에서 자릿수를 멈추고, 그 아래는 단마다 넓이의 N/(mn) 이 남는 몫으로 평균 밝기를 채웠어요. 그러자 가로 결이 고르게 서며 채움이 0.740 이 됐어요. 세로 128 LED 에서 n = 3 이면 다섯 단 넘게 보이지만, n = 6 이면 세 단 만에 칸이 LED 보다 얇아져 가로 줄무늬만 남아요. 잰 값은 움직임 0.630, 촘촘함 0.047, 가장자리 0.364, 복잡도 0.191, 채움 0.740, 색 0.152, 모양 바뀜 0.215, thirds 63/99/58 이고, 노브는 0.12, 0.21, 0.24, 0.10 이에요. + +### Klein's Sails + +주제 5 로 클라인의 돛을 수론 갈래에 세웠어요. 펠릭스 클라인(1895)은 직선 y = αx 양쪽 정수 격자점의 볼록 껍질 경계가 그리는 꺾은선의 꼭짓점이 α 의 연분수 수렴분수라고 썼고, 이차 무리수의 연분수가 주기적이라는 조제프루이 라그랑주(1770)의 정리가 이 꺾은선을 주기적으로 만들어요. 패널은 √2, 황금비, √3, √5, √7, (1+√13)/2 가운데 하나를 골라 Z[α] 의 기본 단위로 정수 행렬 M 을 짓고(노름이 −1 인 넷은 M²), 실수 거듭제곱 M^t 로 격자를 쌍곡 회전시켜요. α 의 직선과 켤레 직선이 X 로 서고 그 네 구석에 돛이 하나씩 서요. 격자점은 꼬리를 끌며 쌍곡선을 따라 흐르고, 돛은 제 몸을 따라 미끄러지다 t = 1 에서 처음 그림으로 돌아와요. 셈으로 견주니 여섯 수 모두 아래 돛의 꼭짓점이 짝수 번째 수렴분수, 위 돛이 홀수 번째였고 모서리의 격자 길이가 부분 몫이었어요. √2 는 아래가 (1,1), (5,7), (29,41), (169,239), 위가 (2,3), (12,17), (70,99), (408,577) 이고, √7 = [2; 1, 1, 1, 4] 는 아래 모서리가 1 과 4 를 번갈아 하고 위 모서리는 모두 1 이에요. 반지름 40 의 격자에서 한 주기를 찾아 M 으로 늘린 돛은, 601×601 격자를 통째로 감싼 볼록 껍질과 반지름 150 안에서 꼭짓점 하나 다르지 않았어요. t = 1 과 t = 2 의 화면도 여섯 수와 Zoom 다섯 자리에서 t = 0 과 LED 까지 같았어요. 처음에는 Zoom 이 정수이고 X 를 돌리지 않는 √2 에서 격자점이 LED 경계에 정확히 걸려, 10⁻¹⁴ 의 오차로 점이 한 칸씩 뛰고 t = 1 화면이 720 채널 달랐어요. 점을 LED 중심에서 나눠 찍고 경계 판정을 격자에서 비키자 0 이 됐어요. 잰 값은 움직임 0.285, 촘촘함 0.083, 가장자리 0.494, 복잡도 1.038, 채움 0.225, 색 0.136, 모양 바뀜 0.035, thirds 20/25/20 이고, 노브는 0.05, 0.07, 0.14, 0.09 예요. 노름이 −1 인 단위는 돛 둘을 서로 바꿔요. 황금비의 M = [[0,1],[1,1]] 를 한 번 쓰면 아래 돛이 위 돛 자리로 가서, 흐름이 한 바퀴 돌아 닫히려면 피보나치 행렬을 제곱한 [[1,1],[1,2]] 를 써야 해요. + +### Jacobi Geodesics + +주제 5 로 세 축이 다른 타원면의 측지선을 공간·원근 갈래에 세웠어요. 카를 구스타프 야코비(1839)는 x² + y²/b² + z²/c² = 1 (1 > b > c) 위의 측지선을 타원 좌표로 갈라 적분했고, 미셸 샬(1846)은 한 측지선의 접선이 모두 같은 공초점 이차곡면 하나에 닿는다는 것을 보였어요. 패널은 긴 반축을 60 LED 로 세로에 세우고 b = 0.5, c = 0.2 에서 열어서, 측지선을 x'' = −(vᵀHv/|∇F|²)∇F 로 0.005 걸음 RK4 로 풀고 걸음마다 곡면으로 되돌려요. 곡면 전체를 1440×720 격자로 훑어 두 주곡률이 같아지는 점을 찾으면 넷뿐이고, 모두 긴 축과 짧은 축의 평면 위 (±0.8839, 0, ±0.0935) 에 있어요. 한 배꼽점에서 72 방향으로 쏜 측지선은 모두 원점 대칭인 맞은편 배꼽점을 지났어요. 가장 크게 비껴간 거리가 걸음 0.002 에서 반축의 1.6×10⁻⁶, 0.0005 에서 3.4×10⁻⁸ 이라 걸음을 줄이는 대로 0 으로 가요. 거기까지의 길이는 72개 모두 그 평면의 주단면 타원 둘레의 절반(c = 0.3 에서 2.192955)과 소수 여섯째 자리까지 같았고, b 를 0.45 에서 0.35 로 바꿔도 그대로였어요. 패턴 안의 36 갈래도 반 둘레를 간 순간 맞은편 배꼽점의 1.6×10⁻⁵ LED 안에 모여요. 배꼽점을 지나지 않는 측지선은 접선이 닿는 곡면의 매개변수 λ 가 200 단위를 가는 동안 소수 여섯째 자리까지 그대로였고, λ 가 b² 보다 크면 긴 축을 도는 허리띠에, 작으면 짧은 축을 도는 고리에 갇혔어요. 잰 값은 움직임 0.562, 촘촘함 0.030, 가장자리 0.209, 복잡도 0.975, 채움 0.473, 색 0.141, 모양 바뀜 0.134, thirds 39/62/39 이고 노브는 0.05, 0.08, 0.08, 0.05 예요. 이 패널에서 새로 본 것은 배꼽점이 끝에 붙어 있다는 거예요. 패널 폭에 맞춰 b 를 0.5 로 두면 배꼽점이 위 끝에서 7 LED 아래라, 다발은 위 끝을 감싼 고리가 몸을 따라 내려가 아래 끝 뒤쪽 한 점으로 오므라드는 모양이 돼요. 몸이 한 번 지나는 동안 반 바퀴 돌게 맞춰서, Speed 2 에서 오므라드는 4.7초에는 그 점이 앞을 봐요. + +### Sedimenting Cloud + +주제 5 와 4 로 점성 유체 속에서 한 덩이로 가라앉는 입자 구름을 입자 갈래에 세웠어요. 입자 300개를 반지름 1 공에 고르게 뿌리고, 입자마다 제 침강 속도 1 에 다른 입자 모두의 정규화 스토크슬렛(리카르도 코르테스, 2001, ε 0.05)을 더해 3차원으로 움직여요. N a/R 를 5 로 두면 하다마르와 리브친스키가 1911년에 푼 물방울 식(안팎 점성이 같을 때)이 구름 속도를 1 + 1.2 × 5 = 7 로 주는데, 여덟 번 돌린 처음 속도가 6.87~7.02 였어요. 입자 3000개로 안의 흐름을 따로 재면 가운데가 구름보다 1.35 빨리 가라앉고 반지름 0.9 둘레는 0.99 느리게 가요. 식의 값은 1.5 와 0.93 이에요. 고리가 되어 둘로 갈라진다는 줄거리(요하네스 니체와 조지 배철러, 1997)는 반만 맞았어요. 300개 여덟 번 가운데 여섯이 176~310 T(T 는 구름의 집단 속도로 반지름 하나를 가는 시간)에 갈라졌는데, 둘로 깨끗이 나뉘기보다 두세 덩이와 낱알로 흩어지는 일이 많았어요. 나머지 둘은 갈라지지 않고 512 T 까지 입자의 56% 와 77% 를 꼬리로 흘리며 풀렸어요. 450개는 넷 다 349~435 T 에 갈라졌고 600개는 넷 가운데 둘이 454, 458 T 에 갈라져서, 입자가 많을수록 늦어요. 꼬리로 빠진 몫은 64 T 에 15%, 갈라질 때 25~37% 이고, 축 둘레가 비는 고리는 기본 노브로 20초쯤 평면도에 보여요. 꼬리는 실 한 가닥이라 카메라가 구름을 따라가도 위 3분의 1 이 2% 에 그쳤어요. 그래서 유럽식 1각법 도면처럼 위에 정면도, 점선 접는 선 아래에 위에서 본 평면도를 두고, 꼬리는 1.5 반지름 위부터 로그로 눌렀어요. 입자는 구름보다 빨리 가라앉으면 시안, 거슬러 오르면 보라예요. 색 폭을 처음 2.5 로 잡았을 때는 거의 한 색이었고, 1.0 으로 줄이자 정면도에 내려가는 가운데 기둥과 올라오는 바깥 껍질이 갈려 보였어요. 잰 값은 움직임 0.145, 촘촘함 0.034, 가장자리 0.531, 복잡도 0.557, 채움 0.147, 색 0.156, 모양 바뀜 0.050, thirds 11/13/19(접는 선은 가운데 3분의 1 의 0.8%)이고, 노브는 0.05, 0.06, 0.06, 0.04 예요. 한 걸음에 속도를 두 번 셈하던 중점법을 2단 아담스-배시퍼스로 바꿔서 600개도 Node 에서 한 프레임 1.0 ms, Lab 에서 2.7 ms 예요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -3466,7 +3545,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 25일 `dev`(`f72355d`)의 328개에 43판부터 64판까지의 이백스무 개를 더한 548개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 25일 `dev`(`f72355d`)의 328개에 43판부터 65판까지의 이백서른 개를 더한 558개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -3552,6 +3631,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 64판은 62판까지 든 528개에 빈 칸도 둘뿐인 칸도 없어서, 셋뿐인 칸 열일곱 가운데 움직임 × 모양 바뀜 1,3 과 채움 × 색 1,4 를 겨눴어요. 같이 돈 63판(#326)보다 먼저 PR 본문에 적어서 두 바퀴가 같은 칸을 겨누지 않았어요. 움직임 × 모양 바뀜 1,3 은 1초에 바뀌는 양이 0.061 아래이면서 모양 바뀜이 0.211~0.328 인 칸이에요. 모양 바뀜은 2~8초의 블록 평균이 첫 화면에서 멀어진 정도라 그 여섯 초 동안 바뀐 양을 넘지 못하고, 그래서 이 칸은 모양 바뀜이 움직임의 3.5배를 넘어야 서요. 반짝임은 움직임만 보태서, Polynomial Amoeba 는 등고선 반짝임을 빼고 아메바가 한 방향으로 줄어드는 구간을 그 창에 맞춰 움직임 0.053, 모양 바뀜 0.265 예요. 채움 × 색 1,4 는 24/255 넘게 켜진 LED 가 0.302 아래이면서 색이 0.545 를 넘는 칸이에요. 이온 30개만 그리면 채움이 축의 아래 끝 0.117 에도 못 미쳐서, Kibble-Zurek Ions 는 사슬 양옆으로 지그재그의 역사를 갈비처럼 밀어내고 두 방향을 자홍과 초록으로 칠해 채움 0.281, 색 0.562 예요. 같은 패턴이 촘촘함 0.203 으로 겨누지 않은 촘촘함 × 채움 3,1 에도 섰어요. 열 개 다 어느 축의 끝도 넘지 않아서 칸 경계는 60판 그대로예요. 64판까지 든 538개에서 지도 21장은 다 `none` 이고, 셋뿐인 칸은 열일곱에서 열넷으로 줄었어요. 겨눈 두 칸은 둘 다 넷이 됐어요. +65판은 64판까지 든 548개에 빈 칸도 둘뿐인 칸도 없어서, 셋뿐인 칸 열셋 가운데 촘촘함 × 색 4,4 와 색 × 모양 바뀜 4,4 를 겨눴어요. 같이 돈 66판(#329)보다 먼저 PR 본문에 적었고, 66판은 촘촘함 × 모양 바뀜 4,4 와 복잡도 × 채움 4,1 을 골랐어요. 촘촘함 × 색 4,4 는 이웃 차이 평균이 0.2969 를 넘으면서 색이 0.5445 를 넘는 칸이에요. Foam Coarsening 은 칸당 12.8 LED 인 씨앗 640개로 깔고 칸 사이에 1 LED 벽을 어둡게 그어 촘촘함 0.343 이고, 변의 수마다 서로 먼 색을 줘서 색 0.628 이에요. 거품이 굵어지면 촘촘함이 떨어져서, 칸이 45% 아래로 줄면 위에서부터 새 거품을 깔아 2~8초 창을 고운 거품에 묶었어요. 색 × 모양 바뀜 4,4 는 색이 0.5445 를 넘으면서 2~8초의 블록 평균이 첫 화면에서 0.3281 넘게 멀어지는 칸이에요. Anamorphosis 는 글자마다 짙은 색을 주고 카메라가 그 창에서 8자를 크게 날아 색 0.616, 모양 바뀜 0.396 이에요. 처음에는 3.2초에 시점을 지나 색 0.493, 모양 바뀜 0.323 으로 두 문턱에 다 못 미쳤어요. 낱말이 서는 순간을 4초로 늦춰 재기 시작하는 2초째 화면이 아직 부서져 있게 하자 넘었어요. 열 개를 더한 558개에서 두 칸은 넷이 됐어요. 그런데 Mouse's Tale 의 채움 0.088 이 채움 축의 아래 끝을 내려서, 채움 칸의 경계가 0.302, 0.558, 0.815 에서 0.280, 0.544, 0.809 로 옮겨졌어요. 그 바람에 채움 × 색 1,4 는 넷에서 둘(Bertrand Paradox, Neon Alley)이 되고, 복잡도 × 채움 3,1 과 채움 × 모양 바뀜 1,4 가 새로 셋뿐인 칸이 되어 셋뿐인 칸은 그대로 열셋이에요. + ### 아직 없는 갈래 `families.json` 의 쉰여섯 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 29판이 연 위상·매듭 갈래는 Hopf Fibration 과 31판의 Boy's Surface, 33판의 Borromean Rings, 34판의 Lakes of Wada, 36판의 Heawood Torus, 38판의 Sperner's Lemma, 39판의 Borsuk-Ulam, 44판의 Ham Sandwich, 46판의 Reeb Graph, 48판의 Möbius Energy, 49판의 Square Peg, 51판의 Persistence Barcode, 53판의 Belt Trick, 55판의 Universal Cover 예요. 28판이 열려다 접은 갈래라 28판의 Hairy Ball 은 노이즈·흐름에 그대로 있어요. 세잎 매듭은 48판의 Möbius Energy 가 처음 세웠어요. 29판은 화장품 팝업 갈래도 열었어요. 주제 6 의 몫이라 주제 2 로 세지 않았고, Cream Swirl, Serum Gel, Glitter Fall, Loose Powder 넷이 다 들어 있어요. 30판의 Swatch Row, Pearl Sheen, Bi-Phase, Satin Fold 가 그 갈래에 넷을 더했고, 31판의 Lip Gloss, Foam Lather, Setting Mist, Sponge Blend 가 넷을 더 더해 열둘이에요. 32판의 Cushion Mesh, Clay Mask, Sugar Scrub, Chrome Powder 가 넷을 더해 열여섯이고, 33판의 Cleansing Balm, Gold Leaf, Essence Drop, Peel-Off 가 넷을 더해 스물이에요. 34판의 Cat Eye, Velvet Nap, Mood Tint, Hair Gloss 가 넷을 더해 스물넷이고, 35판의 Bath Fizz, Toner Pad, Sheet Mask, Gilded Cream 이 넷을 더해 스물여덟이고, 36판의 Lotion Ribbon, Shimmer Oil, Blooming Gel, Whipped Soap 이 넷을 더해 서른둘이에요. 38판의 Milk Ring, Couette Shimmer, Photochromic, Serum Fingers 가 넷을 더해 서른여섯이에요. 39판의 Soap Film, Blotting Paper, Serum Drip, Crackle Polish 가 넷을 더해, 37판의 넷까지 마흔넷이에요. 41판은 할로윈 갈래를 열었어요. 주제 6 의 몫이라 주제 2 로 세지 않았고, Jack-o'-Lantern, Sheet Ghosts, Spider Drop, Cauldron, Eyes in the Dark 다섯이 들어 있어요. 28판이 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table, 33판의 Goldbach Comet, 35판의 Kolakoski, 36판의 Elliptic Chords, 39판의 Explicit Formula, 44판의 Bulgarian Solitaire, 48판의 Chebyshev's Bias, 49판의 Curlicue, 51판의 Sato-Tate, 53판의 Gaussian Periods, 55판의 Question Mark Function 이에요. 27판이 연 철도 갈래는 Funicular 하나이고, 필름·영화 갈래는 Horse in Motion 하나예요. 두 갈래는 주제 표가 수학 쪽으로 바뀌기 전에 열었어요. 그 뒤로 ROUND.md 는 사물로 묶는 갈래를 더 열지 않아요. 26판이 연 얼음·극지 갈래는 Iceberg 와 27판의 Sled Dogs 이고, 미생물 갈래는 Dark Field 와 27판의 Mitosis 예요. 25판이 연 농사·밭 갈래는 Rice Terraces 와 26판의 Rice Planting, 27판의 Center Pivot 이고, 화산 갈래는 Eruption 과 26판의 Geyser, 27판의 Lava Lake 예요. 24판이 연 공사·건설 갈래는 Tower Crane 과 25판의 Excavation, 26판의 Implosion 이에요. 24판이 연 도자기 갈래는 Potter's Wheel 과 25판의 Kintsugi, 26판의 Noborigama 예요. 23판이 연 무대·공연 갈래는 Proscenium 과 24판의 Flying Trapeze, 25판의 Marionette, 26판의 Shadow Play 예요. 23판이 연 짐승 갈래는 Giraffe 와 24판의 Emergence, 25판의 Snow Monkeys 예요. 22판이 연 몸 갈래는 Circulation 과 23판의 Axon, 24판의 Breath, 27판의 Fluoroscope 예요. 22판이 연 비행 갈래는 Liftoff 와 23판의 Final Approach, 24판의 Thermal, 25판의 Skydiver 예요. 20판이 연 새 갈래는 Kingfisher 와 21판의 Woodpecker, 22판의 Weaver, 23판의 Skein 이에요. 20판이 연 화학·실험 갈래는 Chromatography 와 21판의 Chemical Garden, 22판의 Flame Test, 23판의 Elephant Toothpaste 예요. 21판이 연 종이 갈래는 Pop-up Book 과 22판의 Senbazuru, 23판의 Hanging Scroll, 26판의 Origami 예요. 21판이 연 운동·경기 갈래는 Lap Pool 과 22판의 Bowling Lane, 23판의 High Dive, 26판의 Pole Vault, 27판의 Climbing Wall 이에요. 위아래로 움직이는 경기는 High Dive 와 Pole Vault 뒤로 27판에 실내 암벽이 섰어요. 19판이 연 부엌 갈래는 Popcorn 과 20판의 Kettle, 21판의 Pancakes 이고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판이 연 유리 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori, 27판의 Glassblower 예요. 광물·결정 갈래는 Bismuth 와 19판의 Dripstone, 20판의 Agate 이고, 놀이공원 갈래는 Drop Tower 와 19판의 Helter Skelter, 20판의 High Striker, 25판의 Ferris Wheel 이에요. 말이 오르내리는 회전목마는 아직 없어요. 17판이 연 전자 회로 갈래는 Circuit Board 와 18판의 Server Rack, 21판의 Nixie 이고, 장난감 갈래는 Marble Run 과 18판의 Peg Light, 21판의 Slinky, 22판의 Pin Art 예요. 16판이 연 바다 갈래는 Abyss 와 17판의 Shoreline, 20판의 Sunglint, 23판의 Bait Ball 이고, 곤충 갈래는 Fireflies 와 17판의 Ant Farm, 20판의 Moth Lamp, 24판의 Waggle Dance 예요. 15판이 연 도시 갈래는 Metro Map 과 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판이 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables, 24판의 Pipe Organ 이에요. 4판이 연 직물 갈래는 Jacquard Loom 과 15판의 Plait, 19판의 Needlepoint, 23판의 Sequins 이고, 8판이 연 기계 갈래는 Gear Train 과 15판의 Cam Wave, 19판의 Paternoster 예요. 12판이 연 천체 갈래는 Star Trails 와 15판의 Analemma, 16판의 Lunation, 22판의 Galilean Moons 예요. 위성의 궤도는 Galilean Moons 가 그렸고, 해를 도는 행성은 아직 없어요. 리듬·펄스 갈래는 1판의 Euclid Pulse 와 Pulse Bars 뒤로 20판의 Change Ringing, 32판의 Arnold Tongues, 35판의 Chimera State, 38판의 Infinity Series, 41판의 Siteswap, 44판의 Kac Ring, 46판의 Stochastic Resonance 가 섰어요. @@ -3691,9 +3772,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 64판에서는 Wongoji 와 Indian Rope Trick 이 128줄을 써요. Wongoji 는 12 LED 칸 열 개가 세로로 서는 원고지라 격자만으로도 세 3분의 1 이 다 켜져서, 격자를 끄고 한 번 더 쟀어요. 글씨만으로 6/9/6 이고 격자까지 23/24/23 이에요. 시의 행이 대개 열 칸보다 짧아서 다 쓴 쪽도 아래 3분의 1 이 비기 쉬워, 「윤동주」와 「하늘을」이 아래에 드는 첫 장에서 열어요. Indian Rope Trick 은 사슬 키가 116 LED 라 서 있는 동안 thirds 가 16/12/15 이고, 기본 Shake 2 가 세 마디의 문턱 1.25~1.3 위라서 2~8초 내내 서 있어요. 나머지 여덟 가운데 Letter Sand 는 낱말이 무너지는 2~8초에 9/7/42 로 아래가 무겁고, 나머지 일곱은 세 3분의 1 이 다 13% 이상 켜져요. +65판에서는 Long Division 과 Sedimenting Cloud 가 128줄을 써요. Long Division 은 1/7 의 판이 열네 줄 126 LED 로 패널에 꼭 들어요. 가는 5×7 숫자로는 thirds 가 12/5/6 으로 가운데가 문턱에 걸려서, 세로획을 두 LED 로 굵히자 20/9/11 이 됐어요. Sedimenting Cloud 는 구름을 따라가는 정면도 하나로는 꼬리가 실 한 가닥이라 thirds 가 2/4/15 였어요. 위에 정면도, 점선 아래에 위에서 본 평면도를 두는 도면으로 바꾸고 꼬리를 로그로 눌러 11/13/19 가 됐어요. 나머지 여덟 가운데 Mouse's Tale 은 꼬리 끝으로 갈수록 글자가 점이 되어 12/9/4 로 아래 3분의 1 이 5 아래고, Hunminjeongeum 은 30/17/18 이에요. 나머지 여섯은 세 3분의 1 이 다 20% 이상 켜져요. + ### 낯선 수학 -주제 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)예요. +주제 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)이에요. 목록 밖에서 다음에 해 볼 것이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -3771,6 +3854,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 64판에서 본 것이에요. 이번에도 정리가 말하는 것을 패턴의 셈으로 지켜봤어요. Polynomial Amoeba 는 계수를 하르낙 부호 (-1)^(ij) 의 실수로 둔 Phase 0 의 넓이가 아메바가 줄었다 부푸는 다섯 자리에서 늘 가장 컸어요(t = 2 에서 3,372 LED, 다른 Phase 는 2,061~3,191 LED). 미할킨과 룰고르(2001)의 넓이 최대가 이 패널에서도 셈으로 맞았어요. Indian Rope Trick 은 받침점 최고 속도를 사슬 키로 재면 세 마디부터 여섯 마디까지 서는 문턱이 다 1.25~1.3 이라, 막대 하나의 카피차 문턱 √(4/3) = 1.155 가까이 모였어요. 위쪽 끝도 있어서 여섯 마디는 2.15 부터 다시 무너졌어요. Hill's Regions 의 야코비 상수는 RK4 한 프레임에 평균 1.5×10⁻⁹ 만 어긋났고, Kibble-Zurek Ions 는 담금질 60번씩 세어 Quench 0.2 에서 꼬임 0.15 개, 2 에서 1.9 개였어요. 처음에 기대한 것과 어긋난 자리는 Ammann-Beenker 였어요. 위상자 뒤집기가 하나씩 올 줄 알았는데, e1 이 e0 + e2 방향이라 한 묶음의 선 하나가 정사각 격자의 대각선 위 교차점을 한순간에 다 지나서 육각형 서너 개가 한 줄로 뒤집혀요. +65판에서 본 것이에요. 이번에도 정리가 말하는 수를 패턴의 셈으로 맞춰 봤어요. Klein's Sails 는 여섯 이차 무리수 모두 아래 돛의 꼭짓점이 짝수 번째 수렴분수, 위 돛이 홀수 번째였고 모서리의 격자 길이가 부분 몫이었어요. 노름이 −1 인 단위는 두 돛을 맞바꿔서, 흐름이 닫히려면 그 단위를 제곱해야 했어요. Jacobi Geodesics 는 곡면을 1440×720 으로 훑어 배꼽점이 꼭 넷이었고, 한 배꼽점에서 72 방향으로 쏜 측지선이 다 맞은편 배꼽점을 반축의 1.6×10⁻⁶ 안에서 지났으며, 길이는 다 주단면 타원 둘레의 절반이었어요. McMullen Carpet 은 상자 세기의 기울기가 두 차원 식과 소수 둘째 자리까지 맞았는데, 세로 패널에서는 t_j 를 줄이 아니라 열로 세야 식이 맞았어요. Foam Coarsening 의 dA/dt 는 1.77(n − 5.85)로 r² 0.9997 이었어요. 영점이 6 이 아니라 5.85 인 것은 격자가 LED 하나보다 짧은 벽을 세지 못해서로 보여요. 처음에 기대한 것과 어긋난 자리는 Sedimenting Cloud 였어요. 구름의 처음 속도는 하다마르-리브친스키 식의 7 에 대해 6.87~7.02 로 맞았는데, 300개 구름 여덟 번 가운데 여섯만 갈라졌고 그것도 둘로 깨끗이 나뉘기보다 두세 덩이와 낱알로 흩어지는 일이 많았어요. 나머지 둘은 갈라지지 않고 꼬리로 풀렸어요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. @@ -3835,6 +3920,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 목록 밖에서 64판 뒤에 해 볼 것이에요. 더 브라윈의 다중 격자(1981)를 30도 간격 여섯 묶음으로 늘리면 마름모가 30, 60, 90 도의 세 꼴인 12겹 타일링이 되고, Ammann-Beenker 의 교차점 열거와 뒤집기 감지를 그대로 써요. 아메바의 등뼈(미카엘 파사레와 한스 룰고르, 2004)는 Polynomial Amoeba 의 행 풀이가 arg w 마다 내는 z 의 근으로 론킨 함수를 줄마다 1차원 적분으로 구하면, 그 함수가 여집합 조각마다 평면인 자리를 이어 그릴 수 있어요. 프렌켈-콘토로바 모형(야코프 프렌켈과 타티야나 콘토로바, 1938)은 Kibble-Zurek Ions 의 감쇠 분자동역학 사슬을 주기 퍼텐셜 위에 눕히면 되고, 꼬임 표시와 갈비 역사로 어긋남이 생기고 움직이는 것을 보일 수 있어요. L1 둘레 랴푸노프 궤도의 안정·불안정 다양체가 이루는 관(왕상 쿤, 마틴 로, 제럴드 마스든, 셰인 로스, 2000)은 Hill's Regions 의 적분기와 야코비 껍질 되돌리기에 주기 궤도 찾기만 더하면 목을 지나는 길로 보여요. 마티외 방정식의 안정 지도(에밀 마티외, 1868)는 Indian Rope Trick 의 RK4 를 떨림 한 주기만 돌려 플로케 행렬의 대각합으로 LED 마다 칠하면, 거꾸로 선 사슬이 서는 혀 모양 영역이 한 장에 보여요. +목록 밖에서 65판 뒤에 해 볼 것이에요. 이차 무리수의 측지선이 위 반평면의 파레이 테셀레이션을 자르는 차례가 연분수를 그대로 적어서(캐럴라인 시리즈, 1985), Klein's Sails 의 행렬 M 을 SL(2,Z) 의 뫼비우스 변환으로 읽으면 닫힌 측지선 하나를 세로 패널에 세울 수 있어요. 타원면에서 배꼽점이 아닌 점의 첫 공액점 자취는 뾰족점이 꼭 넷이에요. 야코비가 강의에 적었고 이토 진이치와 기요하라 가즈요시가 2004년에 증명했어요. Jacobi Geodesics 의 다발에 야코비 장을 같이 적분하면 그 자취가 그대로 그려져요. 스티븐 랠리와 디미트리오스 가추라스(1992)의 카펫은 McMullen Carpet 의 자릿수 따라가기에 칸마다 높이가 다른 줄만 더하면 되고, 리처드 케니언과 유발 페레스(1996)의 자기 아핀 해면은 같은 셈을 세 방향으로 넓힌 거예요. 거품에서는 n 변 칸의 이웃이 평균 5 + 6/n 언저리 변을 가진다는 아보아브-웨어 법칙(D. A. Aboav, 1970; D. Weaire, 1974)을 Foam Coarsening 의 변 수 세기로 볼 수 있어요. 가라앉는 구름에 벽의 상 스토크슬렛(존 블레이크, 1971)을 더하면 바닥에 내려앉는 구름이 돼요. + ### 타이포 주제 7 로 해 본 글자 실험과 다음에 해 볼 것이에요. 63판과 64판이 같이 돌며 다섯씩 처음 만들었어요. 63판의 다섯은 한글 자모가 날아와 모아쓰기로 잠기는 조립(Jamo Assembly), 놀턴과 하먼의 기호 모자이크(Type Mosaic), 호프스태터의 트립렛 그림자(Trip-let), 카프리카의 계산 종이(Kaprekar's Routine), 캐럴의 낱말 사다리(Word Ladder)예요. @@ -3843,12 +3930,18 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 기능 감을 모으면 넷이 같은 것을 바랐어요. Jamo Assembly, Type Mosaic, Trip-let, Word Ladder 가 다 코드에 낱말 몇 개를 박아 두고 노브로 고르게 했는데, 코드 레이어가 Text 레이어의 글을 문자열로 받으면 그 글을 자모로 풀거나 큰 글자로 깎거나 사다리 규칙을 세어 볼 수 있어요. Kaprekar's Routine 은 Lab 글꼴로 숫자 한 자를 크기만 정해 비트맵으로 받는 길을 바랐어요. 자릿수마다 숫자 열 자를 다시 그렸기 때문이에요. -다음에 해 볼 것이에요. 주시경이 『말의 소리』(1914) 끝에 실은 풀어쓰기와 모아쓰기를 오가는 패턴, 공병우 세벌식 타자기(1949)의 빨랫줄 글꼴처럼 받침이 아래로 처지는 글줄이 있어요. 자모 하나씩 바꾸는 한글 사다리(물, 불, 뿔)는 획 순서를 담은 한글 획 글꼴이 먼저 있어야 해요. 캐럴이 1878년에 쓴 원래 이름 Word-Links 에는 글자를 더하거나 빼는 걸음과 순서를 바꾸는 걸음도 있어요. 기호 모자이크는 밝기만 맞췄는데, Xuemiao Xu, Linling Zhang, Tien-Tsin Wong(2010)의 구조 기반 ASCII 아트처럼 획의 방향까지 맞춰 /, \, |, - 를 고르면 큰 글자의 윤곽이 작은 글자로 이어져요. 손으로 하는 긴 나눗셈이 128줄을 내려가며 1/7 이나 1/17 의 나머지가 다시 나오는 줄에서 순환마디가 켜지는 판, 196 을 뒤집어 더하며 자릿수가 늘 때마다 글자가 작아지는 판도 있어요. 트립렛은 같은 셈을 한글 자모나 한 글자 셋으로 돌려 그림자가 다 서는 짝을 찾아볼 만하고, YES 처럼 다 서지 않는 낱말(12칸이 빠져요)은 빠지는 칸만 따로 빛내 볼 수 있어요. +다음에 해 볼 것이에요. 주시경이 『말의 소리』(1914) 끝에 실은 풀어쓰기와 모아쓰기를 오가는 패턴, 공병우 세벌식 타자기(1949)의 빨랫줄 글꼴처럼 받침이 아래로 처지는 글줄이 있어요. 자모 하나씩 바꾸는 한글 사다리(물, 불, 뿔)는 획 순서를 담은 한글 획 글꼴이 먼저 있어야 해요. 캐럴이 1878년에 쓴 원래 이름 Word-Links 에는 글자를 더하거나 빼는 걸음과 순서를 바꾸는 걸음도 있어요. 기호 모자이크는 밝기만 맞췄는데, Xuemiao Xu, Linling Zhang, Tien-Tsin Wong(2010)의 구조 기반 ASCII 아트처럼 획의 방향까지 맞춰 /, \, |, - 를 고르면 큰 글자의 윤곽이 작은 글자로 이어져요. 196 을 뒤집어 더하며 자릿수가 늘 때마다 글자가 작아지는 판도 있어요. 트립렛은 같은 셈을 한글 자모나 한 글자 셋으로 돌려 그림자가 다 서는 짝을 찾아볼 만하고, YES 처럼 다 서지 않는 낱말(12칸이 빠져요)은 빠지는 칸만 따로 빛내 볼 수 있어요. 주제 7 「타이포」로 해 볼 것이에요. 64판이 처음 다섯을 만들었고, 다섯 다 글꼴을 쓸 수 없어서 글자를 코드에 그렸어요. 다섯이 「기능 감」에 적은 것도 같은 길이었어요. 패턴 코드가 Text 레이어의 글이나 그 글자 비트맵, 글자마다 폭을 담은 글꼴 표를 받는 길이에요. 그 길이 있으면 Broad Nib 의 펜이 손으로 친 낱말을 쓰고, Wongoji 가 「진달래꽃」이든 아무 한글이든 원고지에 써요. 한글 픽셀 글꼴은 Wongoji 가 9×11 로 지은 조합 글꼴(자모 그림 391개)이 출발점이 될 수 있어요. 같은 펜 모델로 한글을 써 볼 만해요. Broad Nib 의 펜이 쓸고 지나가는 셈에 자모 획을 궁체 붓 각도로 적으면, 초성, 중성, 종성 차례로 펜이 한 칸에 모여요. 카운터 다음은 글자 사이의 흰 자리예요. Counterforms 의 flood fill 에 글자마다 볼록 껍질을 더하면 이웃한 두 글자 사이를 따로 떼어 빛나게 할 수 있고, 노브로 자간을 좁히면 빛이 줄어요. 글자의 몸은 모래 다음에 물로 바꿔 볼 만해요. Letter Sand 의 비트맵 글자를 입자 유체로 채우면 획이 녹아 바닥을 따라 퍼졌다가 다음 낱말 틀에 다시 부어져요. 한글 양쪽 맞춤도 남았어요. 한글 조판은 낱말 사이보다 글자 사이를 고르게 늘리는 일이 많아서, Rivers of White 의 문단을 한글로 짜면 강 대신 세로로 곧게 선 글자 열이 생기는지 볼 수 있어요. +65판이 다섯을 더 썼어요. 1 을 소수로 나누는 세로셈(Long Division), 스콧 킴의 반 바퀴 앰비그램을 한글로 옮긴 것(Inversions), 한 시점에서만 맞춰지는 착시 원근 낱말(Anamorphosis), 해례본 제자해의 가획 계보(Hunminjeongeum), 캐럴의 꼬리 시(Mouse's Tale)예요. 다섯이 이번에도 다 글꼴을 코드에 그렸어요. 돌려 읽기는 한글이 라틴 글자보다 잘 맞았어요. 가로 모음을 가진 받침 글자는 반 바퀴에 초성과 받침이 자리를 바꿔서 곰국과 논문, 공공과 운운이 짝인데, 받침 상자를 초성 상자와 같은 크기로 칸 가운데에 대칭으로 두지 않으면 켜진 LED 2,617개 가운데 612개가 갈렸어요. 착시 원근의 낱말은 읽히는 각도가 좁았어요. 시점에서 6.6도 비끼면 획이 두 겹으로 겨우 읽히고 21도면 무슨 낱말인지 알 수 없어요. 꼬리 시는 글자가 2×3 으로 줄면 읽히지 않는데도 솟는 글자와 처지는 글자가 LED 하나씩 남아 낱말마다 다른 무늬가 됐어요. + +기능 감을 모으면 넷이 글자 꼴을 코드 밖에서 받는 길을 바랐어요. Long Division 과 Mouse's Tale 은 Lab 의 픽셀 글꼴을 크기별 비트맵으로, Anamorphosis 는 아래 레이어의 비트맵을 텍스처로, Hunminjeongeum 은 획 순서와 방향을 담은 한글 획 글꼴로 받고 싶어 했어요. Inversions 는 Text 레이어에 노브로 각도를 거는 돌리기 효과와, 돌린 글이 다른 글과 LED 까지 같은지 견주는 도구를 바랐어요. + +다음에 해 볼 것이에요. 0 1 6 8 9 만 쓰는 수는 돌려도 숫자로 읽혀서 1961 은 돌려도 1961 이고, 그다음 그런 해는 6009 예요. Inversions 의 획 글꼴과 LED 견주기를 그대로 써요. 1/89 의 소수는 피보나치 수를 한 자리씩 밀어 더한 합이고, 1/998001 은 000 부터 997 까지 늘어놓다 998 만 건너뛰어서 Long Division 의 판에 나란히 세울 만해요. 원통 거울 착시 원근(장프랑수아 니스롱, 『La Perspective curieuse』, 1638)은 바닥에 휘어 그린 낱말이 가운데 세운 원통 거울에 비쳐야 바로 읽히는 그림이라, Anamorphosis 의 광선을 원통에서 한 번 꺾으면 돼요. 해례본 例義의 연서, 곧 ㅇ 을 입술소리 아래 이어 써서 순경음 ㅸ 을 짓는 법은 Hunminjeongeum 의 획 표로 그려요. 기욤 아폴리네르의 「Il pleut」(『Calligrammes』, 1918)는 글줄 다섯이 빗줄기처럼 기울어 내리는 그림시라 Mouse's Tale 의 셈을 그대로 써요. + ### 화장품 팝업 주제 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 e209a4945965a302d35bc8e752476aae57033230 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 11:56:43 +0000 Subject: [PATCH 08/10] incoming: rebuild index.html with pattern round 65 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JkphLb6hZbrmiGJN5Xd --- 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 28f72c962..daead6d82 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

패턴 연구

- - + + - + +