From 8e1a106cd5027dbacf690482332eb96061f4a055 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 15:19:37 +0000 Subject: [PATCH 01/21] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=EC=97=B0=EA=B5=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=2069?= =?UTF-8?q?=ED=8C=90=20=ED=8C=A8=ED=84=B4=20=EC=97=B4=20=EA=B0=9C=EA=B0=80?= =?UTF-8?q?=20=EC=84=A0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 From d21fdb4d3029f1453faca1b4d93630591dbc32bb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 15:45:26 +0000 Subject: [PATCH 02/21] =?UTF-8?q?Boustrophedon=20=EC=9D=84=20=EB=8D=94?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- .../pattern-study/patterns/boustrophedon.js | 261 ++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 incoming/pattern-study/patterns/boustrophedon.js diff --git a/incoming/pattern-study/patterns/boustrophedon.js b/incoming/pattern-study/patterns/boustrophedon.js new file mode 100644 index 000000000..60acb748e --- /dev/null +++ b/incoming/pattern-study/patterns/boustrophedon.js @@ -0,0 +1,261 @@ +// ===== Patternflow pattern ===== +// Title: Boustrophedon +// Family: type +// Idea: A plough point of light works a field of twenty furrows line after line, turning up last pass's dimming letters ahead of it and writing Hesiod's crane passage into the furrow behind it, mirrored on the lines it ploughs back, so the eye follows the ox across the panel and down. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: boustrophedon, the archaic Greek way of writing as the ox turns (the law code of Gortyn, 5th century BC), reverse boustrophedon as in rongorongo; text from Hesiod, Works and Days 448-451, translated by Hugh G. Evelyn-White (1914); pattern round 69 +// Round: 69 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Way=-1..1, Words=0..2 +// +// Knob 1: Hue. Turns the field and the letters round the wheel together: at 0 the furrows run from deep blue in the troughs to pale blue on the ridges, fresh letters are cream gold and old ones cool to pale blue. +// Knob 2: Speed. How fast the plough writes: 8 letters a second at 2, so a line of 15 takes under 2 s and the whole field of 300 about 40 s; 0.8 a second at 0.2, 12 at 3. +// Knob 3: Way. How the plough turns at the end of a line, taken up at the start of the next one. At 0 boustrophedon: the plough turns and writes the next line right to left with every letter mirrored. At -1 every line runs left to right and the plough lifts and flies back to the left edge. At 1 reverse boustrophedon, as on the rongorongo tablets: the return lines are turned upside down, as if the field were turned round for each. +// Knob 4: Words. How words are parted. At 0 by a column of three dots, the word divider of many archaic inscriptions; at 1 not at all, the letters run on as scriptio continua; at 2 by a blank, as we write now. + +const W = 64, H = 128; +const { floor, round, min, max, exp, sin, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- Capitals, 3 by 5, rows from the top. Archaic inscriptions were cut in +// capitals with no small letters. Glyph 26 is the word divider, three dots in +// a column; 27 is a blank. +const GLYPHS = [ + "010101111101101", "110101110101110", "011100100100011", "110101101101110", "111100110100111", + "111100110100100", "011100101101011", "101101111101101", "111010010010111", "001001001101010", + "101101110101101", "100100100100111", "101111111101101", "111101101101101", "010101101101010", + "110101110100100", "010101101110011", "110101110101101", "011100010001110", "111010010010010", + "101101101101111", "101101101101010", "101101111111101", "101101010101101", "101101010010010", + "111001010100111", "010000010000010", "000000000000000", +]; +const FONT = GLYPHS.map((g) => Uint8Array.from(g, (c) => +c)); +const DIV = 26, BLANK = 27; + +// Hesiod, Works and Days 448-451, in Evelyn-White's translation: "Mark, when +// you hear the voice of the crane who cries year by year from the clouds +// above, for she gives the signal for ploughing and shows the season of rainy +// winter; but she vexes the heart of the man who has no oxen." The stone has +// no commas: only letters and word breaks are kept, and the end of the +// sentence is one more word break before it starts again. +const SRC = + "MARK WHEN YOU HEAR THE VOICE OF THE CRANE WHO CRIES YEAR BY YEAR FROM THE CLOUDS ABOVE FOR SHE GIVES THE SIGNAL FOR PLOUGHING AND SHOWS THE SEASON OF RAINY WINTER BUT SHE VEXES THE HEART OF THE MAN WHO HAS NO OXEN "; +// The text as cells for each way of parting words, and where each cell came +// from in SRC, so turning Words mid-line carries on from the same place. +const SEQ = [[], [], []], SPOS = [[], [], []]; +for (let m = 0; m < 3; m++) + for (let i = 0; i < SRC.length; i++) { + const ch = SRC[i]; + if (ch === " ") { + if (m === 1) continue; + SEQ[m].push(m === 0 ? DIV : BLANK); + } else SEQ[m].push(ch.charCodeAt(0) - 65); + SPOS[m].push(i); + } + +// ---- The field. 15 letters a line, 4 LEDs a cell (3 and a gap), from x = 2; +// 20 lines 6 rows apart from y = 4, the letter in the 5 rows of the furrow +// and the ridge in the sixth. The furrows run on into the 4-row margins. +const NC = 15, NL = 20, CW = 4, LP = 6, X0 = 2, Y0 = 4, CELLS = NC * NL; +// Across a furrow the ground climbs and falls by D a row, trough in the +// middle of the letter, ridge between lines. Along it the soil is grained a +// column at a time, so every pair of neighbours differs a little. +const LO = 6, D = 24; +const TRI = [2, 1, 0, 1, 2, 3]; +const ROWB = new Float32Array(H), ROWT = new Float32Array(H); +for (let y = 0; y < H; y++) { + const r = (((y - Y0) % LP) + LP) % LP; + ROWB[y] = LO + D * TRI[r]; + ROWT[y] = TRI[r] / 3; +} +const GRAIN = new Float32Array(W * H); +for (let p = 0; p < W * H; p++) { + let h = (p * 2654435761) >>> 0; + h = Math.imul(h ^ (h >>> 15), 2246822519) >>> 0; + h = ((h ^ (h >>> 13)) >>> 0) / 4294967296; + GRAIN[p] = (p % W) & 1 ? 20 + 10 * h : 6 * h; +} + +// Letters: fresh ones stand C_NEW above the trough and cool to C_OLD as the +// pass goes on; they keep part of the furrow's shape and all of its grain. +const C_NEW = 226, C_OLD = 216, TAU = 30, KEEP_SHAPE = 0.33, KEEP_GRAIN = 0.7; + +// Cells a second at Speed s; a turn costs one cell's time, the flight back +// to the left edge three. +const RATE = 4; +const turnLen = (way) => (way < 0 ? 3 : 1); +// Direction and orientation of grid line l written the given way: +// orientation 0 upright, 1 mirrored, 2 turned 180 degrees. +const dirOf = (way, l) => (way !== 0 && way !== 1) || l % 2 === 0 ? 1 : -1; +const orientOf = (way, l) => (dirOf(way, l) > 0 ? 0 : way === 0 ? 1 : 2); + +export function setup(params) { + params.s = { + t: 0, ready: false, hue: 0, speed: 2, way: 0, words: 0, + line: 0, pos: 0, lineWay: 0, j: 0, cell: -1, + glyph: new Uint8Array(CELLS), orient: new Uint8Array(CELLS), birth: new Float32Array(CELLS), + // What lay in the cell under the plough before it got there. + oldG: 0, oldO: 0, oldB: 0, + L: new Float32Array(W * H), rgb: new Float32Array(W * H * 3), + }; +} + +// Grid cell of in-line step i on grid line l, written in direction dir. +const cellAt = (l, i, dir) => l * NC + (dir > 0 ? i : NC - 1 - i); + +// The field the plough finds when it starts: the pass before, written the +// same way, ending with the cells just before the opening words, its oldest +// letters already cooled. +function prefill(S) { + const seq = SEQ[S.words], P = seq.length, rate = RATE * S.speed; + const per = NC + turnLen(S.way); + for (let l = 0; l < NL; l++) { + const dir = dirOf(S.way, l), o = orientOf(S.way, l); + for (let i = 0; i < NC; i++) { + const k = l * NC + i, c = cellAt(l, i, dir); + S.glyph[c] = seq[(((k - CELLS) % P) + P) % P]; + S.orient[c] = o; + S.birth[c] = -(NL * per - (l * per + i)) / rate; + } + } + S.j = 0; + S.lineWay = S.way; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.way = cl(round(k[2]), -1, 1); + const words = cl(round(k[3]), 0, 2); + if (words !== S.words) { + // Carry on from the same place in the sentence with the new parting. + const from = SPOS[S.words][S.j % SPOS[S.words].length]; + const sp = SPOS[words]; + let j = 0; + while (j < sp.length && sp[j] < from) j++; + S.j = j % sp.length; + S.words = words; + } + if (!S.ready) { + prefill(S); + S.ready = true; + } + S.t += d; + S.pos += d * RATE * S.speed; + const end = NC + turnLen(S.lineWay); + if (S.pos >= end) { + S.pos -= end; + S.line++; + S.lineWay = S.way; + S.cell = -1; + } + const l = S.line % NL, i = floor(S.pos); + if (i < NC) { + const c = cellAt(l, i, dirOf(S.lineWay, l)); + if (c !== S.cell) { + // The share reaches a new cell: what was there is turned under as the + // next letter of the text goes in. + S.cell = c; + S.oldG = S.glyph[c], S.oldO = S.orient[c], S.oldB = S.birth[c]; + const seq = SEQ[S.words]; + S.glyph[c] = seq[S.j % seq.length]; + S.j = (S.j + 1) % seq.length; + S.orient[c] = orientOf(S.lineWay, l); + S.birth[c] = S.t; + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.ready) return; + const { L, rgb } = S, t = S.t, hue = S.hue; + const trough = hsv(0.66 + hue, 0.78, 1), ridge = hsv(0.55 + hue, 0.42, 1); + const fresh = hsv(0.12 + hue, 0.38, 1), cool = hsv(0.6 + hue, 0.5, 1); + + // Cloud shadows drift over the field from the upper left. + const cx = 0.07, cy = 0.045, wx = 0.035, wy = -0.06; + const ca = t * 0.9, cb = t * 0.7 + 1.7; + + for (let y = 0; y < H; y++) { + const base = ROWB[y], tt = ROWT[y]; + let r0 = trough[0] + (ridge[0] - trough[0]) * tt, g0 = trough[1] + (ridge[1] - trough[1]) * tt, b0 = trough[2] + (ridge[2] - trough[2]) * tt; + const m = max(r0, g0, b0); + (r0 /= m), (g0 /= m), (b0 /= m); + for (let x = 0; x < W; x++) { + const p = y * W + x; + const cloud = 0.81 + 0.095 * (sin(cx * x + cy * y - ca) + sin(wx * x + wy * y + cb)); + const v = (base + GRAIN[p]) * cloud; + L[p] = cloud; + rgb[p * 3] = r0 * v, rgb[p * 3 + 1] = g0 * v, rgb[p * 3 + 2] = b0 * v; + } + } + + // The plough: where it is along the current line, and the column of the + // cell it is in that its share has reached. + const l = S.line % NL, dir = dirOf(S.lineWay, l), i = floor(S.pos), f = S.pos - i; + for (let c = 0; c < CELLS; c++) { + const col = c % NC, row = floor(c / NC), x0 = X0 + col * CW, y0 = Y0 + row * LP; + const under = c === S.cell && i < NC; + for (let q = 0; q < 3; q++) { + // Under the plough, columns it has passed show the new letter and the + // rest the old one. + let g = S.glyph[c], o = S.orient[c], b = S.birth[c]; + if (under) { + const u = dir > 0 ? (q + 0.5) / CW : (CW - 0.5 - q) / CW; + if (u >= f) (g = S.oldG), (o = S.oldO), (b = S.oldB); + } + const gl = FONT[g], age = t - b, w = exp(-age / TAU); + const lvl = C_OLD + (C_NEW - C_OLD) * w, glint = age < 0.5 ? 40 * (1 - age / 0.5) : 0; + const cr = cool[0] + (fresh[0] - cool[0]) * w, cg = cool[1] + (fresh[1] - cool[1]) * w, cb2 = cool[2] + (fresh[2] - cool[2]) * w; + const cm = max(cr, cg, cb2); + for (let r = 0; r < 5; r++) { + const sq = o === 0 ? q : 2 - q, sr = o === 2 ? 4 - r : r; + if (!gl[sr * 3 + sq]) continue; + const y = y0 + r, x = x0 + q, p = y * W + x; + const v = min(255, lvl + glint + KEEP_SHAPE * (ROWB[y] - LO) + KEEP_GRAIN * GRAIN[p]); + rgb[p * 3] = (cr / cm) * v, rgb[p * 3 + 1] = (cg / cm) * v, rgb[p * 3 + 2] = (cb2 / cm) * v; + } + } + } + + // The point of the share, a warm light in the furrow; on the flight back + // to the left edge it is lifted and dim. + let tx, ty, a = 1; + const yl = Y0 + l * LP + 2; + if (i < NC) { + const col = dir > 0 ? i : NC - 1 - i; + tx = X0 + col * CW + (dir > 0 ? f * CW : (1 - f) * CW); + ty = yl; + } else { + const u = (S.pos - NC) / turnLen(S.lineWay), sm = u * u * (3 - 2 * u); + const nl = (l + 1) % NL, xe = dir > 0 ? X0 + NC * CW : X0; + const xs = S.lineWay < 0 ? X0 : dirOf(S.lineWay, nl) > 0 ? X0 : X0 + NC * CW; + const ny = nl === 0 ? Y0 + 2 : Y0 + nl * LP + 2; + tx = xe + (xs - xe) * sm; + ty = yl + (ny - yl) * sm; + if (S.lineWay < 0) a = 0.45 + 0.55 * Math.abs(1 - 2 * u); + } + const lamp = hsv(0.1 + hue, 0.3, 1); + const x0 = max(0, floor(tx - 9)), x1 = min(W - 1, floor(tx + 9)), y0 = max(0, floor(ty - 9)), y1 = min(H - 1, floor(ty + 9)); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const dx = x + 0.5 - tx, dy = y + 0.5 - ty, r2 = dx * dx + dy * dy; + const v = a * (160 * exp(-r2 / 2.2) + 45 * exp(-r2 / 24)); + const p = (y * W + x) * 3; + rgb[p] += lamp[0] * v, rgb[p + 1] += lamp[1] * v, rgb[p + 2] += lamp[2] * v; + } + + for (let p = 0; p < W * H; p++) + display.setPixel(p % W, floor(p / W), min(255, rgb[p * 3]) | 0, min(255, rgb[p * 3 + 1]) | 0, min(255, rgb[p * 3 + 2]) | 0); +} From cbf5fabdcef07168dbd62eba6ed45e5fa45f6169 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 15:49:24 +0000 Subject: [PATCH 03/21] =?UTF-8?q?Aharonov-Bohm=20=EB=A5=BC=20=EB=8D=94?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- .../pattern-study/patterns/aharonov-bohm.js | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 incoming/pattern-study/patterns/aharonov-bohm.js diff --git a/incoming/pattern-study/patterns/aharonov-bohm.js b/incoming/pattern-study/patterns/aharonov-bohm.js new file mode 100644 index 000000000..09841d7c2 --- /dev/null +++ b/incoming/pattern-study/patterns/aharonov-bohm.js @@ -0,0 +1,199 @@ +// ===== Patternflow pattern ===== +// Title: Aharonov-Bohm +// Family: wave +// Idea: Plane waves run down the panel past a thin tube of magnetic flux a third of the way down, where no field reaches them, yet behind the tube the crests on the two sides of its shadow line come out shifted against each other by the flux in quanta and end at a torn seam, coloured by how far each crest has slipped behind the wave that would pass with no tube. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Aharonov-Bohm effect (Yakir Aharonov and David Bohm, 1959; Werner Ehrenberg and Raymond Siday, 1949), wavefront dislocations (Michael Berry, Robert Chambers, Mark Large, Colin Upstill and J. C. Walmsley, 1980), pattern round 69 +// Round: 69 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Wavelength=7..16, Flux=0.5..1.5 +// +// Knob 1: Hue. Turns the colours round the wheel. At 0 a crest that passes as if there were no tube is blue; at the opening flux of half a quantum the crests left of the shadow line come out a quarter wave ahead and turn green, those on the right a quarter wave behind and turn magenta, and a crest a whole wave out would be blue again. +// Knob 2: Speed. How fast the crests run down: 8 LEDs a second at 2, 0.8 at 0.2 and 12 at 3. +// Knob 3: Wavelength. The distance between crests, 7 LEDs at the opening, 16 at the top. A longer wave diffracts more: the band behind the tube where the two sides blend is about the square root of wavelength times distance wide, 25 LEDs across at the bottom row at 7 and 37 at 16. +// Knob 4: Flux. The flux through the tube in flux quanta h/e. At 0.5 the two sides come out half a wave apart and the shadow line is an exact dark seam; at 1 the wave passes as if the tube were not there; at 1.5 it is half a wave again, the mirror image of 0.5. The wave only knows the flux modulo one quantum. + +const W = 64, H = 128, N = W * H, HALF = W / 2; +const { floor, ceil, min, max, abs, sqrt, sin, cos, exp, pow, cbrt, atan2, round, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// The tube sits on the corner between four LEDs, on the middle line, a third +// of the way down. +const TX = 32, TY = 42; + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + o[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + o[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; + return o; +} + +// Gamma on [1, 2] by Lanczos (g = 7, nine terms), good to about 1e-15. +const LZ = [0.99999999999980993, 676.5203681218851, -1259.1392167224028, 771.32342877765313, -176.61502916214059, 12.507343278686905, -0.13857109526572012, 9.9843695780195716e-6, 1.5056327351493116e-7]; +function gamma(x) { + x -= 1; + let a = LZ[0]; + const t = x + 7.5; + for (let i = 1; i < 9; i++) a += LZ[i] / (x + i); + return sqrt(2 * PI) * pow(t, x + 0.5) * exp(-t) * a; +} + +// Bessel functions J of orders nu0, nu0 + 1, ..., nu0 + M at z, for one +// fractional part nu0 in [0, 1), by Miller's backward recurrence +// J(v - 1) = (2 v / z) J(v) - J(v + 1) +// started from nothing well above z, then scaled by Neumann's sum +// (z / 2)^nu0 = sum over k of (nu0 + 2k) Gamma(nu0 + k) / k! J(nu0 + 2k), +// whose weights w[k] depend on nu0 only. +function weights(nu0, K) { + const w = new Float64Array(K + 1); + w[0] = gamma(nu0 + 1); + let g = w[0]; + for (let k = 1; k <= K; k++) { + w[k] = (nu0 + 2 * k) * g; + g = (g * (nu0 + k)) / (k + 1); + } + return w; +} +function besselRun(nu0, z, M, w, out) { + let fp = 0, f = 1e-30, sum = (M & 1) === 0 ? w[M >> 1] * f : 0; + out[M] = f; + for (let n = M; n >= 1; n--) { + const fm = ((2 * (nu0 + n)) / z) * f - fp; + fp = f; + f = fm; + out[n - 1] = f; + if (((n - 1) & 1) === 0) sum += w[(n - 1) >> 1] * f; + if (abs(f) > 1e200) { + for (let j = n - 1; j <= M; j++) out[j] *= 1e-200; + (sum *= 1e-200), (fp *= 1e-200), (f *= 1e-200); + } + } + const sc = pow(z / 2, nu0) / sum; + for (let n = 0; n <= M; n++) out[n] *= sc; +} + +// The Aharonov-Bohm wave. Round the tube the vector potential circles with +// A = a / r in units of the flux quantum, and the wave equation splits into +// angular orders m, each with the radial Bessel function of order |m - a|. +// With theta measured from the way the wave runs (down the panel), the +// solution that is regular at the tube and whose incoming part is the plane +// wave exp(i k x) is +// psi = sum over m of (-1)^m (-i)^|m - a| J_|m - a|(k r) exp(i m theta), +// Aharonov and Bohm's series turned to face down the panel. At a = 0 it is +// the plane wave to 1e-14; at a = 1/2 the orders m and 1 - m pair off and +// cancel on theta = 0, so the shadow line is an exact zero. +// +// Which a? The flux F in quanta enters as exp(2 pi i F) round any loop, so +// F and F + 1 are the same physics, and the gauge with the whole quanta +// taken out (a = F - round(F), here F - 1 on the knob's range) draws them as +// the same picture: F = 1 is exactly the bare plane wave. Every orders' sum +// is done for an LED and its mirror across the shadow line at once, since +// they share r and differ only in the sign of theta. +const MM = 260; +function field(S, F, lam) { + const a = F - 1, k = (2 * PI) / lam; + const fr = (x) => x - floor(x); + const nuP = fr(-a), nuQ = fr(a); + const wP = weights(nuP, MM), wQ = weights(nuQ, MM); + const JP = S.JP, JQ = S.JQ, fam = S.fam, nn = S.nn, cr = S.cr, ci = S.ci; + for (let m = -MM; m <= MM; m++) { + const d = m - a, j = m + MM, sg = m & 1 ? -1 : 1, nu = abs(d); + if (d >= 0) (fam[j] = 0), (nn[j] = round(d - nuP)); + else (fam[j] = 1), (nn[j] = round(-d - nuQ)); + cr[j] = sg * cos((PI * nu) / 2); + ci[j] = -sg * sin((PI * nu) / 2); + } + const { amp, ph, lag } = S; + for (let y = 0; y < H; y++) { + const X = y + 0.5 - TY; + for (let x = HALF; x < W; x++) { + const Y = x + 0.5 - TX, r = sqrt(X * X + Y * Y), th = atan2(Y, X), z = k * r; + let M = ceil(z + 8 * cbrt(z) + 16); + M = min(MM - 2, M + (M & 1)); + besselRun(nuP, z, M + 1, wP, JP); + besselRun(nuQ, z, M + 1, wQ, JQ); + // A = sum c_m J cos(m theta), B = sum c_m J sin(m theta), both complex; + // psi(theta) = A + iB and psi(-theta) = A - iB. + let Ar = 0, Ai = 0, Br = 0, Bi = 0, cm = 1, sm = 0; + const c1 = cos(th), s1 = sin(th); + for (let m = 0; m <= M; m++) { + let j = m + MM, J = fam[j] ? JQ[nn[j]] : JP[nn[j]]; + (Ar += cr[j] * J * cm), (Ai += ci[j] * J * cm), (Br += cr[j] * J * sm), (Bi += ci[j] * J * sm); + if (m > 0) { + (j = MM - m), (J = fam[j] ? JQ[nn[j]] : JP[nn[j]]); + (Ar += cr[j] * J * cm), (Ai += ci[j] * J * cm), (Br -= cr[j] * J * sm), (Bi -= ci[j] * J * sm); + } + const t = cm * c1 - sm * s1; + sm = sm * c1 + cm * s1; + cm = t; + } + const pr = y * W + x, pl = y * W + (W - 1 - x); + const ox = k * X; + for (const [p, re, im] of [ + [pr, Ar - Bi, Ai + Br], + [pl, Ar + Bi, Ai - Br], + ]) { + amp[p] = sqrt(re * re + im * im); + ph[p] = atan2(im, re); + // The slip behind the bare wave exp(i k x), in turns, -1/2 to 1/2. + let l = (ph[p] - ox) / (2 * PI); + lag[p] = l - round(l); + } + } + } +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, lam: NaN, flux: NaN, t: 0, + amp: new Float32Array(N), ph: new Float32Array(N), lag: new Float32Array(N), + JP: new Float64Array(MM + 2), JQ: new Float64Array(MM + 2), + fam: new Int8Array(2 * MM + 1), nn: new Int32Array(2 * MM + 1), cr: new Float64Array(2 * MM + 1), ci: new Float64Array(2 * MM + 1), + c: [0, 0, 0], + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 7, 0.5], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + // The field is worked out again only when the wavelength or the flux has + // moved by a visible step. + const lam = round(cl(k[2], 7, 16) * 20) / 20, flux = round(cl(k[3], 0.5, 1.5) * 200) / 200; + if (lam !== S.lam || flux !== S.flux) { + S.lam = lam; + S.flux = flux; + field(S, flux, lam); + } + S.t += d; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { amp, ph, lag, c } = S; + // Crests run down at 4 * Speed LEDs a second: the phase turns by k v t. + const wt = ((2 * PI) / S.lam) * 4 * S.speed * S.t, h0 = 0.62 + S.hue; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, A = amp[p]; + const re = A * cos(ph[p] - wt); + const crest = re > 0 ? pow(re, 1.6) : 0; + const v = min(1, 0.07 * A + 0.93 * crest); + hsv(h0 + lag[p], 0.85 - 0.35 * crest * crest, v, c); + display.setPixel(x, y, (255 * c[0]) | 0, (255 * c[1]) | 0, (255 * c[2]) | 0); + } + // The tube itself, end on: a small white core. + for (const [x, y] of [ + [31, 41], + [32, 41], + [31, 42], + [32, 42], + ]) + display.setPixel(x, y, 235, 235, 245); +} From 0876ba5382906453a51dc6a9ddc1e4ff470560bf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 15:49:58 +0000 Subject: [PATCH 04/21] =?UTF-8?q?Nine-fold=20Seal=20=EB=A5=BC=20=EB=8D=94?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- .../pattern-study/patterns/nine-fold-seal.js | 345 ++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 incoming/pattern-study/patterns/nine-fold-seal.js diff --git a/incoming/pattern-study/patterns/nine-fold-seal.js b/incoming/pattern-study/patterns/nine-fold-seal.js new file mode 100644 index 000000000..fb173fbe6 --- /dev/null +++ b/incoming/pattern-study/patterns/nine-fold-seal.js @@ -0,0 +1,345 @@ +// ===== Patternflow pattern ===== +// Title: Nine-fold Seal +// Family: type +// Idea: A tall vermilion seal impression of eight Hangul syllables, read down the right column and then the left, folds every horizontal stroke back and forth into serpentines of one-LED lines until each syllable's box is full, as the nine-fold seal script of official seals did, and every few seconds it unfolds to plain letters and refolds into the next text. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: jiudie zhuan (九疊篆), the nine-fold seal script of official seals from the Song dynasty on and of Joseon official seals, applied to Hangul; texts: the heading of the Hangul Hunminjeongeum in Worin seokbo (1459), its gloss "the right sounds that teach", and two old wishes for long life; pattern round 69 +// Round: 69 +// =============================== +// @matrix 64x128 +// @knobs Ink=0..1, Speed=0.2..3, Plain=0..1, Press=0..1 +// +// Knob 1: Ink. Turns the ink round the colour wheel at a fixed brightness. At 0 it is seal vermilion, at 0.5 a blue-green, and the paper stays a warm grey. +// Knob 2: Speed. At 2 a text holds folded for 3.6 s and the whole change, unfold, swap and refold, takes 4 s more; 0.2 is ten times slower and 3 half as fast again. +// Knob 3: Plain. How far the strokes may fold: 0 folds them until every box is full, 0.5 lets them fold seven lines deep, 1 shows the plain letters. +// Knob 4: Press. How unevenly the seal is pressed. The pressure rocks round the seal once in 6 s, and the side it leaves prints with bare patches: almost none at 0, up to three quarters of the ink missing in the far corner at 1. +// Buttons: any button starts the change to the next text at once. + +const W = 64, H = 128; +const { floor, round, ceil, min, max, abs, cos, sin, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- The seal. A border two LEDs thick runs round the panel's edge, a +// divider two thick splits it into two columns, and every box is 27 by 29 +// LEDs. Strokes lie on a lattice of nodes two LEDs apart, 14 across and 15 +// down a box, so any two strokes that do not join keep one LED of paper +// between them. That one-LED rhythm of ink and paper is the whole look of +// the nine-fold script. +const NI = 14, NJ = 15, N = NI * NJ; +// The right column is read first, top to bottom, then the left. +const COLX = [34, 3], ROWY = [3, 34, 65, 96]; + +const TEXTS = ["세종어제훈민정음", "가르치는바른소리", "만수무강하옵소서", "천년만년살고지고"]; + +// Jamo as strokes [u0, v0, u1, v1] in their region, 0 to 1, every stroke +// level or upright, as seal script draws them. A fifth number marks a +// ring: ㅇ is a ring with its corners cut, so it never reads as ㅁ. +// Initials by their Unicode index; finals are looked up through FIN. +const CONS = { + 0: [[0, 0, 1, 0], [1, 0, 1, 1]], // ㄱ + 2: [[0, 0, 0, 1], [0, 1, 1, 1]], // ㄴ + 3: [[0, 0, 1, 0], [0, 0, 0, 1], [0, 1, 1, 1]], // ㄷ + 5: [[0, 0, 1, 0], [1, 0, 1, 0.5], [0, 0.5, 1, 0.5], [0, 0.5, 0, 1], [0, 1, 1, 1]], // ㄹ + 6: [[0, 0, 1, 0], [0, 0, 0, 1], [1, 0, 1, 1], [0, 1, 1, 1]], // ㅁ + 7: [[0, 0, 0, 1], [1, 0, 1, 1], [0, 0.5, 1, 0.5], [0, 1, 1, 1]], // ㅂ + // ㅅ squared off the way seal script squares 人: a stem and two legs + // that step outward twice. + 9: [[0.5, 0, 0.5, 0.3], [0.25, 0.3, 0.75, 0.3], [0.25, 0.3, 0.25, 0.65], [0.75, 0.3, 0.75, 0.65], [0, 0.65, 0.25, 0.65], [0.75, 0.65, 1, 0.65], [0, 0.65, 0, 1], [1, 0.65, 1, 1]], // ㅅ + 11: [[0.1, 0, 0.9, 1, 1]], // ㅇ + 12: [[0, 0, 1, 0], [0.5, 0, 0.5, 0.3], [0.25, 0.3, 0.75, 0.3], [0.25, 0.3, 0.25, 0.65], [0.75, 0.3, 0.75, 0.65], [0, 0.65, 0.25, 0.65], [0.75, 0.65, 1, 0.65], [0, 0.65, 0, 1], [1, 0.65, 1, 1]], // ㅈ + 14: [[0.3, 0, 0.7, 0], [0, 0.22, 1, 0.22], [0.5, 0.22, 0.5, 0.45], [0.25, 0.45, 0.75, 0.45], [0.25, 0.45, 0.25, 0.72], [0.75, 0.45, 0.75, 0.72], [0, 0.72, 0.25, 0.72], [0.75, 0.72, 1, 0.72], [0, 0.72, 0, 1], [1, 0.72, 1, 1]], // ㅊ + 18: [[0.3, 0, 0.7, 0], [0, 0.25, 1, 0.25], [0.15, 0.5, 0.85, 1, 1]], // ㅎ +}; +const FIN = { 1: 0, 4: 2, 7: 3, 8: 5, 16: 6, 17: 7, 19: 9, 21: 11, 22: 12, 23: 14, 27: 18 }; +const VOW = { + 0: [[0, 0, 0, 1], [0, 0.5, 0.75, 0.5]], // ㅏ + 4: [[0.25, 0.5, 1, 0.5], [1, 0, 1, 1]], // ㅓ + 5: [[0, 0.5, 0.4, 0.5], [0.4, 0, 0.4, 1], [1, 0, 1, 1]], // ㅔ + 6: [[0.25, 0.33, 1, 0.33], [0.25, 0.67, 1, 0.67], [1, 0, 1, 1]], // ㅕ + 8: [[0.5, 0, 0.5, 1], [0, 1, 1, 1]], // ㅗ + 13: [[0, 0, 1, 0], [0.5, 0, 0.5, 1]], // ㅜ + 18: [[0, 0.5, 1, 0.5]], // ㅡ + 20: [[0.5, 0, 0.5, 1]], // ㅣ +}; + +// Regions [i0, j0, i1, j1] in nodes for the initial, the vowel and the final. +function regions(V, T) { + if (V === 0 || V === 4 || V === 5 || V === 6 || V === 20) { + const wide = V === 5, ic = wide ? 6 : 7, iv = wide ? 8 : 9; + return T ? [[0, 0, ic, 7], [iv, 0, 13, 8], [1, 10, 12, 14]] : [[0, 1, ic, 13], [iv, 0, 13, 14], null]; + } + return T ? [[1, 0, 12, 4], [0, 6, 13, 8], [1, 10, 12, 14]] : [[1, 0, 12, 7], [0, 9, 13, 13], null]; +} + +// One syllable: its plain strokes in LEDs, the corner LEDs of its rings, and +// the fold runs. Folding gives every free node of the box to the nearest +// level stroke straight above or below it, and the nodes a stroke gets lie +// in rows 1, 2, 3 ... away from it. Each run of such nodes hangs from the row +// before at one end, the right end and the left end taking turns, so the +// stroke becomes one ribbon folded back and forth. Nodes no level stroke can +// reach, beside an upright with nothing level over it, go the same way to +// the nearest upright stroke left or right of them. +function build(ch) { + const c = ch.charCodeAt(0) - 0xac00; + const L = floor(c / 588), V = floor(c / 28) % 21, T = c % 28; + const reg = regions(V, T); + const strokes = [], corners = []; + const busy = new Uint8Array(N), hOwn = new Int16Array(N).fill(-1), vOwn = new Int16Array(N).fill(-1); + const add = (shape, r) => { + if (!shape || !r) return; + const [a, b, c2, d] = r; + const mi = (u) => a + round(u * (c2 - a)), mj = (v) => b + round(v * (d - b)); + for (const s of shape) { + const i0 = mi(s[0]), j0 = mj(s[1]), i1 = mi(s[2]), j1 = mj(s[3]); + if (s[4]) { + strokes.push({ o: 1, i0: i0 + 1, j0, i1: i1 - 1, j1: j0 }); + strokes.push({ o: 0, i0, j0: j0 + 1, i1: i0, j1: j1 - 1 }); + strokes.push({ o: 0, i0: i1, j0: j0 + 1, i1, j1: j1 - 1 }); + strokes.push({ o: 1, i0: i0 + 1, j0: j1, i1: i1 - 1, j1 }); + for (const [ci, cj, dx, dy] of [[i0, j0, 1, 1], [i1, j0, -1, 1], [i0, j1, 1, -1], [i1, j1, -1, -1]]) { + busy[ci + cj * NI] = 2; + corners.push(2 * ci + dx, 2 * cj + dy); + } + } else strokes.push({ o: j0 === j1 ? 1 : 0, i0: min(i0, i1), j0: min(j0, j1), i1: max(i0, i1), j1: max(j0, j1) }); + } + }; + add(CONS[L], reg[0]); + add(VOW[V], reg[1]); + if (T) add(CONS[FIN[T]], reg[2]); + strokes.forEach((s, k) => { + for (let j = s.j0; j <= s.j1; j++) + for (let i = s.i0; i <= s.i1; i++) { + busy[i + j * NI] = 1; + if (s.o) hOwn[i + j * NI] = k; + else vOwn[i + j * NI] = k; + } + }); + + // Claims: stroke, side (+1 below or right of it, -1 above or left) and + // distance, first for the level strokes, then for the upright ones. + const own = new Int16Array(N).fill(-1), side = new Int8Array(N), dist = new Int8Array(N), kind = new Uint8Array(N); + for (let n = 0; n < N; n++) { + if (busy[n]) continue; + const i = n % NI, j = floor(n / NI); + let bd = 99; + for (let jj = j - 1; jj >= 0; jj--) { + const m = i + jj * NI; + if (!busy[m]) continue; + if (busy[m] === 1 && hOwn[m] >= 0) (own[n] = hOwn[m]), (side[n] = 1), (bd = dist[n] = j - jj); + break; + } + for (let jj = j + 1; jj < NJ; jj++) { + const m = i + jj * NI; + if (!busy[m]) continue; + if (busy[m] === 1 && hOwn[m] >= 0 && jj - j < bd) (own[n] = hOwn[m]), (side[n] = -1), (dist[n] = jj - j); + break; + } + if (own[n] >= 0) kind[n] = 1; + } + for (let n = 0; n < N; n++) if (kind[n]) busy[n] = 3; + for (let n = 0; n < N; n++) { + if (busy[n]) continue; + const i = n % NI, j = floor(n / NI); + let bd = 99; + for (let ii = i - 1; ii >= 0; ii--) { + const m = ii + j * NI; + if (!busy[m]) continue; + if (busy[m] === 1 && vOwn[m] >= 0) (own[n] = vOwn[m]), (side[n] = 1), (bd = dist[n] = i - ii); + break; + } + for (let ii = i + 1; ii < NI; ii++) { + const m = ii + j * NI; + if (!busy[m]) continue; + if (busy[m] === 1 && vOwn[m] >= 0 && ii - i < bd) (own[n] = vOwn[m]), (side[n] = -1), (dist[n] = ii - i); + break; + } + if (own[n] >= 0) kind[n] = 2; + } + + // Runs: [level, connector x, y, start x, y, end x, y], all in LEDs. + const runs = []; + let deepest = 0; + for (let n = 0; n < N; n++) { + if (!kind[n]) continue; + const i = n % NI, j = floor(n / NI), lv = dist[n], sd = side[n], k = own[n], hz = kind[n] === 1; + // Only the first node of a run starts it. + const pi = hz ? i - 1 : i, pj = hz ? j : j - 1; + if (pi >= 0 && pj >= 0) { + const m = pi + pj * NI; + if (kind[m] === kind[n] && own[m] === k && side[m] === sd && dist[m] === lv) continue; + } + let e = hz ? i : j; + for (;;) { + const m = hz ? e + 1 + j * NI : i + (e + 1) * NI; + if ((hz ? e + 1 >= NI : e + 1 >= NJ) || kind[m] !== kind[n] || own[m] !== k || side[m] !== sd || dist[m] !== lv) break; + e++; + } + const lo = hz ? i : j; + // The turn falls at the far end and the near end in turn, row by row. + const atHi = (lv + (sd > 0 ? 1 : 0)) % 2 === 0; + const cA = atHi ? e : lo, cB = atHi ? lo : e; + if (hz) runs.push(lv, 2 * cA, 2 * j - sd, 2 * cA, 2 * j, 2 * cB, 2 * j); + else runs.push(lv, 2 * i - sd, 2 * cA, 2 * i, 2 * cA, 2 * i, 2 * cB); + deepest = max(deepest, lv); + } + const plain = []; + for (const s of strokes) plain.push(2 * s.i0, 2 * s.j0, 2 * s.i1, 2 * s.j1); + return { plain, corners, runs: Int16Array.from(runs), deepest }; +} + +// Seconds at Speed 2: the hold, the fold rate in rows a second, the lag of +// each syllable behind the one before it, in rows, and the swap. +const HOLD = 3, RATE = 14, STAG = 0.8, FOLD = (NJ + 7 * STAG) / RATE, SWAP = 1; +// Ink and paper levels, and how far the two dimmer channels wander per LED +// (times -128..127). The brightest channel wanders by 3 at most, so two +// neighbours of one kind never differ by more than 6. +const INK_V = 214, PAPER = [58, 40, 32], MOTTLE = 0.45, GRAIN = 0.24; + +function impression(S) { + // A new impression: the paper grain and the ink's mottle per LED, and the + // patches where ink fails to take, coarse blots with a fine speckle in them. + for (let p = 0; p < W * H * 3; p++) S.grain[p] = floor(Math.random() * 256); + const GW = 24, GH = 46, g = new Float32Array(GW * GH); + for (let q = 0; q < g.length; q++) g[q] = Math.random(); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const gx = x / 3, gy = y / 3, ix = floor(gx), iy = floor(gy), fx = gx - ix, fy = gy - iy; + const a = g[ix + iy * GW], b = g[ix + 1 + iy * GW], c = g[ix + (iy + 1) * GW], d = g[ix + 1 + (iy + 1) * GW]; + const v = (a * (1 - fx) + b * fx) * (1 - fy) + (c * (1 - fx) + d * fx) * fy; + S.blot[y * W + x] = 0.6 * v + 0.4 * Math.random(); + } + // Spread the blot values evenly over 0..1 by rank, so Press means the + // share of ink that fails. + const order = Array.from(S.blot, (v, p) => p).sort((a, b) => S.blot[a] - S.blot[b]); + for (let r = 0; r < order.length; r++) S.rank[order[r]] = r / order.length; +} + +export function setup(params) { + const S = (params.s = { ink: new Uint8Array(W * H), grain: new Uint8Array(W * H * 3), blot: new Float32Array(W * H), rank: new Float32Array(W * H) }); + S.glyphs = TEXTS.map((t) => Array.from(t, build)); + S.text = 0; + S.ph = 0; + // The first text holds 2.4 s, so the first change comes soon. + S.pt = HOLD - 2.4; + S.tau = 0; + S.hue = 0; + S.plain = 0; + S.press = 0.06; + S.F = new Float32Array(8); + S.q = 1; + impression(S); +} + +export function update(dt, input, params) { + const S = params.s; + const k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + const sp = cl(k[1], 0.05, 3) / 2; + S.hue = k[0]; + S.plain = cl(k[2], 0, 1); + S.press = cl(k[3], 0, 1); + S.tau += dt * sp; + S.pt += dt * sp; + if (S.ph === 0 && input.btnPressed && input.btnPressed.some(Boolean)) S.pt = HOLD; + if (S.ph === 0 && S.pt >= HOLD) (S.ph = 1), (S.pt = 0); + if (S.ph === 1 && S.pt >= FOLD) (S.ph = 2), (S.pt = 0); + if (S.ph === 2 && S.pt >= SWAP / 2 && !S.swapped) { + S.text = (S.text + 1) % TEXTS.length; + S.swapped = 1; + impression(S); + } + if (S.ph === 2 && S.pt >= SWAP) (S.ph = 3), (S.pt = 0), (S.swapped = 0); + if (S.ph === 3 && S.pt >= FOLD) (S.ph = 0), (S.pt = 0); + const u = S.pt * RATE, cap = (1 - S.plain) * NJ; + for (let i = 0; i < 8; i++) { + let f = NJ; + if (S.ph === 1) f = NJ - max(0, u - i * STAG); + else if (S.ph === 2) f = 0; + else if (S.ph === 3) f = max(0, u - i * STAG); + S.F[i] = min(cl(f, 0, NJ), cap); + } + S.q = S.ph === 2 ? abs(1 - (2 * S.pt) / SWAP) : 1; +} + +function line(ink, x0, y0, x1, y1, count) { + const dx = Math.sign(x1 - x0), dy = Math.sign(y1 - y0); + for (let s = 0; s < count; s++) { + const x = x0 + dx * s, y = y0 + dy * s; + ink[y * W + x] = 1; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S.glyphs) return; + const ink = S.ink; + ink.fill(0); + for (let x = 0; x < W; x++) ink[x] = ink[W + x] = ink[(H - 2) * W + x] = ink[(H - 1) * W + x] = 1; + for (let y = 0; y < H; y++) ink[y * W] = ink[y * W + 1] = ink[y * W + W - 2] = ink[y * W + W - 1] = 1; + for (let y = 2; y < H - 2; y++) ink[y * W + 31] = ink[y * W + 32] = 1; + + const text = S.glyphs[S.text], q = S.q; + for (let k = 0; k < 8; k++) { + const g = text[k], ox = COLX[k >> 2], oy = ROWY[k & 3], F = S.F[k]; + const pl = g.plain; + for (let s = 0; s < pl.length; s += 4) { + const len = abs(pl[s + 2] - pl[s]) + abs(pl[s + 3] - pl[s + 1]) + 1; + line(ink, ox + pl[s], oy + pl[s + 1], ox + pl[s + 2], oy + pl[s + 3], round(q * len)); + } + if (q > 0.6) for (let s = 0; s < g.corners.length; s += 2) ink[(oy + g.corners[s + 1]) * W + ox + g.corners[s]] = 1; + const r = g.runs; + for (let s = 0; s < r.length; s += 7) { + const u = F - (r[s] - 1); + if (u <= 0) continue; + const len = abs(r[s + 5] - r[s + 3]) + abs(r[s + 6] - r[s + 4]) + 1; + const shown = min(len + 1, ceil(u * (len + 1))); + ink[(oy + r[s + 2]) * W + ox + r[s + 1]] = 1; + if (shown > 1) line(ink, ox + r[s + 3], oy + r[s + 4], ox + r[s + 5], oy + r[s + 6], shown - 1); + } + } + + // Colour. Every ink LED has the same brightest channel and every paper LED + // the same level, so the picture has only hard steps; the sheen and the + // mottle move the other two channels only. + const hue = 0.025 + S.hue, full = hsv(hue, 0.8, 1), pale = hsv(hue, 0.38, 1); + let mi = 0; + for (let c = 1; c < 3; c++) if (full[c] > full[mi]) mi = c; + const c1 = (mi + 1) % 3, c2 = (mi + 2) % 3; + const f1 = full[c1] * INK_V, f2 = full[c2] * INK_V, p1 = pale[c1] * INK_V - f1, p2 = pale[c2] * INK_V - f2; + // The pressure rocks round the seal; the side it has left prints thin. + const ang = (S.tau * 2 * PI) / 6, ca = cos(ang), sa = sin(ang), press = S.press; + // A sheen of pressure sweeps down the seal once every 5 s at Speed 2. + const band = ((S.tau / 5) % 1) * 230 - 40; + const rgb = [0, 0, 0]; + const grain = S.grain, blot = S.rank; + for (let y = 0; y < H; y++) { + const ny = (y - 63.5) / 64; + for (let x = 0; x < W; x++) { + const p = y * W + x, j1 = grain[3 * p] - 128, j2 = grain[3 * p + 1] - 128, j3 = (grain[3 * p + 2] % 7) - 3; + let on = ink[p]; + if (on) { + const t = 0.5 + 0.5 * (((x - 31.5) / 32) * ca + ny * sa); + if (blot[p] < press * (0.15 + 0.85 * t * t)) on = 0; + } + if (on) { + const sh = max(0, 1 - abs(x * 0.5 + y - band) / 10); + const v = INK_V + j3; + rgb[mi] = v; + rgb[c1] = cl(f1 + p1 * sh + j1 * MOTTLE, 0, v) | 0; + rgb[c2] = cl(f2 + p2 * sh + (0.6 * j1 + 0.3 * j2) * MOTTLE, 0, v) | 0; + display.setPixel(x, y, rgb[0], rgb[1], rgb[2]); + } else { + const v = PAPER[0] + j3; + display.setPixel(x, y, v, cl(PAPER[1] + j1 * GRAIN, 0, v) | 0, cl(PAPER[2] + (0.6 * j1 + 0.3 * j2) * GRAIN, 0, v) | 0); + } + } + } +} From 3696430d2ffe27f3a81d93241cba48c9cd54a352 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 15:52:00 +0000 Subject: [PATCH 05/21] =?UTF-8?q?Letter=20Melt=20=EB=A5=BC=20=EB=8D=94?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- .../pattern-study/patterns/letter-melt.js | 507 ++++++++++++++++++ 1 file changed, 507 insertions(+) create mode 100644 incoming/pattern-study/patterns/letter-melt.js diff --git a/incoming/pattern-study/patterns/letter-melt.js b/incoming/pattern-study/patterns/letter-melt.js new file mode 100644 index 000000000..8f1aae201 --- /dev/null +++ b/incoming/pattern-study/patterns/letter-melt.js @@ -0,0 +1,507 @@ +// ===== Patternflow pattern ===== +// Title: Letter Melt +// Family: type +// Idea: A word of liquid stands in an invisible mould at the top of the panel, the mould lets go from the feet of the letters up so the strokes sag, drip and run down the full height into a pool on the floor, and then threads of the pool are drawn up into the mould of the next word, MELT, 물, FLOW, 녹다, DRIP, which fills from the top of each letter down and sets. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 69, after Letter Sand (pattern round 64), particle fluid by double density relaxation (Simon Clavet, Philippe Beaudoin and Pierre Poulin, 2005) +// Round: 69 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Word=0..5, Heat=0..1 +// +// Knob 1: Hue. Turns the liquid round the wheel. At 0 it is a teal body with a pale aqua rim, at 0.3 violet, at 0.5 rose, at 0.75 gold. +// Knob 2: Speed. At 2 the simulation runs 60 steps a second and a word takes about 8 s to stand, melt, pool and pour back; at 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Word. 0 pours the five words in turn; 1 to 5 hold one of MELT, 물, FLOW, 녹다 and DRIP, and the pool is poured back into that same mould every time. +// Knob 4: Heat. Cold liquid is thick: at 0.06 it sags slowly, hangs in threads and the mould holds it for 1.3 s. Hot liquid is thin and shivers in the mould: at 1 the mould opens after 0.5 s and the letters run down in loose drops and splash. + +const W = 64, H = 128; +const { floor, min, max, abs, sqrt, round, PI, cos, sin } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The fluid, in LEDs and simulation steps. One step is 1/60 s at Speed 2. +const SP = 1.4; // rest spacing of the particles +const HR = 3.0; // interaction radius +const GRAV = 0.025; // LEDs a step, a step +const RISE = 1.3; // how fast a poured thread climbs, LEDs a step +const POUR = 1.6; // seconds a pour draws particles for +const KS = 0.06; // stiffness against the rest density +const KN = 0.15; // near stiffness, which keeps particles apart and gives the surface its tension +const STROKE = 2.2; // stroke radius of the mould letters +const GX = Math.ceil(W / HR), GY = Math.ceil(H / HR); + +// Stroke centre lines in a unit box, y down. "O" is a stadium ring. +const LATIN = { + M: [[0, 1, 0, 0, 0.5, 0.55, 1, 0, 1, 1]], + E: [[1, 0, 0, 0, 0, 1, 1, 1], [0, 0.5, 0.8, 0.5]], + L: [[0, 0, 0, 1, 1, 1]], + T: [[0, 0, 1, 0], [0.5, 0, 0.5, 1]], + F: [[1, 0, 0, 0, 0, 1], [0, 0.47, 0.8, 0.47]], + O: "ring", + W: [[0, 0, 0.2, 1, 0.5, 0.45, 0.8, 1, 1, 0]], + D: [[0, 0, 0, 1, 0.45, 1, 1, 0.78, 1, 0.22, 0.45, 0, 0, 0]], + R: [[0, 1, 0, 0, 0.65, 0, 1, 0.14, 1, 0.34, 0.65, 0.48, 0, 0.48], [0.45, 0.48, 1, 1]], + I: [[0.5, 0, 0.5, 1], [0.1, 0, 0.9, 0], [0.1, 1, 0.9, 1]], + P: [[0, 1, 0, 0, 0.65, 0, 1, 0.15, 1, 0.37, 0.65, 0.52, 0, 0.52]], +}; +// Hangul syllables drawn jamo by jamo in the same way. +const HANGUL = { + 물: [ + [0.18, 0, 0.82, 0, 0.82, 0.25, 0.18, 0.25, 0.18, 0], + [0, 0.42, 1, 0.42], + [0.5, 0.42, 0.5, 0.55], + [0.18, 0.66, 0.82, 0.66, 0.82, 0.83, 0.18, 0.83, 0.18, 1, 0.82, 1], + ], + 녹: [[0.12, 0, 0.12, 0.25, 0.88, 0.25], [0.5, 0.34, 0.5, 0.5], [0, 0.5, 1, 0.5], [0.1, 0.67, 0.9, 0.67, 0.9, 1]], + 다: [[0.5, 0.16, 0, 0.16, 0, 0.84, 0.5, 0.84], [0.8, 0, 0.8, 1], [0.8, 0.5, 1, 0.5]], +}; +const WORDS = ["MELT", "물", "FLOW", "녹다", "DRIP"]; + +// Glyph boxes for a word: [char, x, y, w, h]. +function boxes(word) { + const n = [...word].length; + if (HANGUL[word[0]]) { + const w = n === 1 ? 56 : 29; + return [...word].map((c, i) => [c, n === 1 ? 4 : 2 + i * 31, 5, w, 44]); + } + return [...word].map((c, i) => [c, 1.5 + i * 16, 6, 13, 36]); +} + +// The mould's stroke segments in LEDs. +function segments(word) { + const out = []; + for (const [c, bx, by, bw, bh] of boxes(word)) { + const x0 = bx + STROKE, y0 = by + STROKE, sw = bw - 2 * STROKE, sh = bh - 2 * STROKE; + const g = LATIN[c] || HANGUL[c]; + if (g === "ring") { + const r = sw / 2, cx = x0 + r, ya = y0 + r, yb = y0 + sh - r, pts = []; + for (let k = 0; k <= 12; k++) pts.push(cx + r * cos(PI + (k * PI) / 12), ya + r * sin(PI + (k * PI) / 12)); + for (let k = 0; k <= 12; k++) pts.push(cx + r * cos((k * PI) / 12), yb + r * sin((k * PI) / 12)); + pts.push(pts[0], pts[1]); + for (let i = 0; i + 3 < pts.length; i += 2) out.push(pts[i], pts[i + 1], pts[i + 2], pts[i + 3]); + continue; + } + for (const pl of g) + for (let i = 0; i + 3 < pl.length; i += 2) out.push(x0 + pl[i] * sw, y0 + pl[i + 1] * sh, x0 + pl[i + 2] * sw, y0 + pl[i + 3] * sh); + } + return out; +} + +function segDist(px, py, s) { + let best = 1e9; + for (let i = 0; i < s.length; i += 4) { + const ax = s[i], ay = s[i + 1], vx = s[i + 2] - ax, vy = s[i + 3] - ay; + const t = cl(((px - ax) * vx + (py - ay) * vy) / (vx * vx + vy * vy || 1), 0, 1); + const dx = px - ax - t * vx, dy = py - ay - t * vy; + best = min(best, dx * dx + dy * dy); + } + return sqrt(best); +} + +// The mould of a word: its sites, a hexagonal lattice at the rest spacing +// inside the strokes, read from the top row down, and the spouts under each +// glyph that a pour draws its threads up through, one for every 16 LEDs of +// width. Each spout keeps its sites top row first, so a pour fills each +// letter from its top down. +function mould(word) { + const s = segments(word), bx = boxes(word), xy = [], spouts = [], lists = []; + for (const [, x, y, w, h] of bx) { + const m = max(1, round(w / 16)); + for (let k = 0; k < m; k++) { + spouts.push(x + (w * (k + 0.5)) / m, y + h + 2); + lists.push([]); + } + } + for (let j = 0; ; j++) { + const y = 3 + j * SP * 0.866; + if (y > 52) break; + for (let x = 0.5 + (j % 2) * SP * 0.5; x < W; x += SP) { + if (segDist(x, y, s) >= STROKE - 0.3) continue; + // The nearest spout of the glyph whose box holds the site. + let best = 0, bd = 1e9, k = 0; + for (const [, gx, , gw] of bx) { + const m = max(1, round(gw / 16)); + for (let e = 0; e < m; e++, k++) { + const d = abs(spouts[2 * k] - x) + (x >= gx - 1 && x <= gx + gw + 1 ? 0 : 100); + if (d < bd) { + bd = d; + best = k; + } + } + } + lists[best].push(xy.length / 2); + xy.push(x, y); + } + } + const of = new Int32Array(xy.length / 2); + lists.forEach((l, k) => l.forEach((i) => (of[i] = k))); + return { xy: new Float64Array(xy), spouts: new Float64Array(spouts), lists: lists.map((l) => new Int32Array(l)), of, n: xy.length / 2 }; +} + +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; +} + +export function setup(params) { + const MOULDS = WORDS.map(mould); + let most = 0; + for (const m of MOULDS) most = max(most, m.n); + const N = min(1500, most + 460), MP = N * 16; + // Rest density: what a particle in the middle of the lattice counts. + let rho0 = 0; + for (let j = -4; j <= 4; j++) + for (let i = -4; i <= 4; i++) { + const x = (i + (j % 2) * 0.5) * SP, y = j * SP * 0.866, r = sqrt(x * x + y * y); + if (r > 0 && r < HR) rho0 += (1 - r / HR) ** 2; + } + params.s = { + N, MOULDS, rho0, word: -1, phase: 0, started: false, + // Doubles from the start, so the object keeps one shape. + pt: 0.5, acc: 0.5, hue: 0.5, + x: new Float64Array(N), y: new Float64Array(N), px: new Float64Array(N), py: new Float64Array(N), + vx: new Float64Array(N), vy: new Float64Array(N), st: new Uint8Array(N), site: new Int32Array(N), + rel: new Float64Array(N), rho: new Float64Array(N), rhn: new Float64Array(N), + pa: new Int32Array(MP), pb: new Int32Array(MP), pn: 0, qx: new Float64Array(MP), qy: new Float64Array(MP), qq: new Float64Array(MP), + cnt: new Int32Array(GX * GY + 1), fillc: new Int32Array(GX * GY), loose: new Int32Array(GX * GY), hd: new Uint8Array(N), idx: new Int32Array(N), cell: new Int32Array(N), + F: new Float32Array(W * H), V: new Float32Array(W * H), pal: new Float32Array(64 * 3), rim: new Float32Array(3), + nk: new Int32Array(8), arrived: 0, target: -1, + }; + return params.s; +} + +// Put word w in its mould and the rest of the particles in a pool on the +// floor. +function place(S, w) { + const st = S.MOULDS[w].xy, n = S.MOULDS[w].n; + S.word = w; + for (let i = 0; i < S.N; i++) { + if (i < n) { + S.x[i] = st[2 * i]; + S.y[i] = st[2 * i + 1]; + S.st[i] = 1; + S.site[i] = i; + } else { + const k = i - n, per = floor((W - 1) / SP), j = floor(k / per); + S.x[i] = 0.7 + (k % per) * SP + (j % 2) * SP * 0.5; + S.y[i] = H - 0.7 - j * SP * 0.866; + S.st[i] = 0; + } + S.vx[i] = S.vy[i] = 0; + } + S.phase = 0; + S.pt = 0; +} + +function start(S, w) { + // Two steps with a particle in every state, so each branch of a step has + // run before the first frame and the compiled step does not fall back + // the first time the mould lets go or a pour begins. + place(S, w); + S.target = (w + 1) % WORDS.length; + S.phase = 1; + for (let i = 0; i < 9; i++) { + S.st[i] = 1 + (i % 3); + S.rel[i] = i % 2; + } + step(S, 0.3, 0.1); + step(S, 0.3, 0.1); + S.target = -1; + place(S, w); +} + +// Counting sort of the particles into cells one interaction radius wide, +// then every pair closer than the radius, each once, with its unit vector +// and 1 - r/h, adding up both densities on the way. +function neighbours(S) { + const { N, x, y, cnt, idx, cell, pa, pb, qx, qy, qq, rho, rhn, st, hd } = S; + cnt.fill(0); + // Particles the mould holds still skip each other: their springs keep + // them apart. While the mould lets go every pair counts. + for (let i = 0; i < N; i++) hd[i] = (st[i] === 1 && S.phase !== 1) || st[i] === 3 ? 1 : 0; + rho.fill(0); + rhn.fill(0); + const loose = S.loose; + loose.fill(0); + for (let i = 0; i < N; i++) { + const c = cl(floor(y[i] / HR), 0, GY - 1) * GX + cl(floor(x[i] / HR), 0, GX - 1); + cell[i] = c; + cnt[c + 1]++; + if (!hd[i]) loose[c]++; + } + for (let c = 0; c < GX * GY; c++) cnt[c + 1] += cnt[c]; + const fill = S.fillc; + fill.set(cnt.subarray(0, GX * GY)); + for (let i = 0; i < N; i++) idx[fill[cell[i]]++] = i; + let n = 0; + const h2 = HR * HR, MP = pa.length; + for (let i = 0; i < N; i++) { + const c = cell[i], cx = c % GX, cy = (c - cx) / GX, xi = x[i], yi = y[i], hi = hd[i]; + let ri = 0, ni = 0; + for (let oy = 0; oy <= 1; oy++) + for (let ox = -1; ox <= 1; ox++) { + if (oy === 0 && ox < 0) continue; + const nx = cx + ox, ny = cy + oy; + if (nx < 0 || nx >= GX || ny >= GY) continue; + const cc = ny * GX + nx, own = cc === c; + if (hi && !loose[cc]) continue; + for (let k = cnt[cc], e = cnt[cc + 1]; k < e; k++) { + const j = idx[k]; + if (own && j <= i) continue; + const dx = x[j] - xi, dy = y[j] - yi, r2 = dx * dx + dy * dy; + if (r2 >= h2 || n >= MP || (hi && hd[j])) continue; + const r = sqrt(r2) || 1e-4, q = 1 - r / HR, q2 = q * q, q3 = q2 * q; + pa[n] = i; + pb[n] = j; + qx[n] = dx / r; + qy[n] = dy / r; + qq[n++] = q; + ri += q2; + ni += q3; + rho[j] += q2; + rhn[j] += q3; + } + } + rho[i] += ri; + rhn[i] += ni; + } + S.pn = n; +} + +// One step of the double density relaxation scheme: gravity and steering, +// viscous impulses, a free step, the two densities and their pressure +// displacements, the mould's springs, the walls, and velocity from the +// change in position. +function step(S, visc, jit) { + const { N, x, y, px, py, vx, vy, st, site, pa, pb, qx, qy, qq, rho, rhn } = S; + const sites = S.MOULDS[S.word].xy, M = S.MOULDS[S.target >= 0 ? S.target : S.word], target = M.xy; + for (let i = 0; i < N; i++) { + if (st[i] === 0) vy[i] += GRAV; + else if (st[i] === 2) { + // Drawn up: a thread rises at RISE LEDs a step to the spout under the + // glyph, and from there each particle heads for its own site. + const k = M.of[site[i]], spx = M.spouts[2 * k], spy = M.spouts[2 * k + 1]; + let dvx, dvy; + if (y[i] > spy) { + dvx = cl((spx - x[i]) * 0.15, -0.8, 0.8); + dvy = -RISE; + } else { + const dx = target[2 * site[i]] - x[i], dy = target[2 * site[i] + 1] - y[i], d = sqrt(dx * dx + dy * dy) + 1e-6; + const sc = min(RISE, d * 0.25) / d; + dvx = dx * sc; + dvy = dy * sc; + } + vx[i] += (dvx - vx[i]) * 0.35; + vy[i] += (dvy - vy[i]) * 0.35; + } + } + // Viscosity over last step's pairs and their unit vectors. + const n0 = S.pn; + for (let p = 0; p < n0; p++) { + const i = pa[p], j = pb[p], ux = qx[p], uy = qy[p]; + const u = (vx[i] - vx[j]) * ux + (vy[i] - vy[j]) * uy; + if (u <= 0) continue; + const I = qq[p] * (visc * u + 0.25 * u * u) * 0.5, ix = I * ux, iy = I * uy; + vx[i] -= ix; + vy[i] -= iy; + vx[j] += ix; + vy[j] += iy; + } + for (let i = 0; i < N; i++) { + const sp = vx[i] * vx[i] + vy[i] * vy[i]; + if (sp > 6.25) { + const f = 2.5 / sqrt(sp); + vx[i] *= f; + vy[i] *= f; + } + px[i] = x[i]; + py[i] = y[i]; + x[i] += vx[i]; + y[i] += vy[i]; + } + neighbours(S); + const n = S.pn, rho0 = S.rho0; + for (let i = 0; i < N; i++) { + rho[i] = S.hd[i] ? 0 : KS * (rho[i] - rho0); + rhn[i] = S.hd[i] ? 0 : KN * rhn[i]; + } + for (let p = 0; p < n; p++) { + const i = pa[p], j = pb[p], q = qq[p]; + const d = ((rho[i] + rho[j]) * q + (rhn[i] + rhn[j]) * q * q) * 0.5; + const dx = d * qx[p], dy = d * qy[p]; + x[i] -= dx; + y[i] -= dy; + x[j] += dx; + y[j] += dy; + } + for (let i = 0; i < N; i++) { + if (st[i] === 1) { + // The mould: a spring to the site, weakening while the mould lets go. + const k = S.phase === 1 ? 0.25 * cl((S.rel[i] - S.pt) / 0.9, 0, 1) ** 2 : 0.25; + if (k <= 0) st[i] = 0; + else { + x[i] += (sites[2 * site[i]] - x[i]) * k + (Math.random() - 0.5) * jit; + y[i] += (sites[2 * site[i] + 1] - y[i]) * k + (Math.random() - 0.5) * jit; + } + } else if (st[i] === 2) { + const sx = target[2 * site[i]], sy = target[2 * site[i] + 1], dx = sx - x[i], dy = sy - y[i]; + if (dx * dx + dy * dy < 0.8) { + st[i] = 3; + S.arrived++; + } + } else if (st[i] === 3) { + // Arrived: the new mould holds it. + x[i] += (target[2 * site[i]] - x[i]) * 0.25 + (Math.random() - 0.5) * jit; + y[i] += (target[2 * site[i] + 1] - y[i]) * 0.25 + (Math.random() - 0.5) * jit; + } + if (x[i] < 0.5) x[i] = 0.5; + else if (x[i] > W - 0.5) x[i] = W - 0.5; + if (y[i] < 0.5) y[i] = 0.5; + else if (y[i] > H - 0.5) y[i] = H - 0.5; + vx[i] = x[i] - px[i]; + vy[i] = y[i] - py[i]; + } +} + +// The free particle nearest the top of the pool under a spout. +function pick(S, spx) { + const { N, st, x, y } = S; + let best = -1, bs = 1e9; + for (let i = 0; i < N; i++) { + if (st[i] !== 0) continue; + const sc = abs(x[i] - spx) * 2 + y[i]; + if (sc < bs) { + bs = sc; + best = i; + } + } + return best; +} + +// The cycle, one step at a time: the mould holds, lets go, and a pour fills +// the next mould. +function tick(S, sel, heat) { + const { N, st, y } = S; + S.pt += 1 / 60; + if (S.phase === 0 && S.pt > 1.3 - 0.8 * heat) { + // The mould lets go from the feet of the letters up. + S.phase = 1; + S.pt = 0; + const spread = 1.1 - 0.6 * heat; + for (let i = 0; i < N; i++) if (st[i] === 1) S.rel[i] = 0.1 + spread * cl((52 - y[i]) / 47, 0, 1) + Math.random() * 0.3; + } else if (S.phase === 1 && S.pt > 1.6) { + let held = 0, high = 0; + for (let i = 0; i < N; i++) { + if (st[i] === 1) held++; + else if (y[i] < 80) high++; + } + if (!held && (high < N * 0.03 || S.pt > 5)) { + // Pour: each spout of the next word draws its sites, top row first, + // from the particles near the top of the pool under it. + S.phase = 2; + S.pt = 0; + S.target = sel ? sel - 1 : (S.word + 1) % WORDS.length; + S.nk.fill(0); + S.arrived = 0; + } + } else if (S.phase === 2) { + const M = S.MOULDS[S.target], f = min(1, S.pt / POUR); + let done = true; + for (let k = 0; k < M.lists.length; k++) { + const l = M.lists[k]; + while (S.nk[k] < f * l.length) { + const i = pick(S, M.spouts[2 * k]); + if (i < 0) break; + st[i] = 2; + S.site[i] = l[S.nk[k]++]; + } + if (S.nk[k] < l.length) done = false; + } + if (done && (S.arrived >= M.n || S.pt > POUR + 4)) { + // Set: the new word stands in its mould. + for (let i = 0; i < N; i++) if (st[i] >= 2) st[i] = 1; + S.word = S.target; + S.target = -1; + S.phase = 0; + S.pt = 0; + } + } +} + +export function update(dt, input, params) { + const S = params.s || setup(params); + const kv = input.knobValues || [0, 2, 0, 0.06]; + const rate = cl(kv[1], 0.2, 3) / 2, sel = cl(round(kv[2]), 0, 5), heat = cl(kv[3], 0, 1); + S.hue = kv[0] || 0; + if (!S.started) { + S.started = true; + S.acc = 0; + start(S, sel ? sel - 1 : 0); + S.pt = 0.4; + } + const visc = 0.6 * (1 - heat) ** 2 + 0.01, jit = 0.03 + 0.35 * heat; + S.acc += cl(dt || 0, 0, 0.1) * rate * 60; + while (S.acc >= 1) { + S.acc -= 1; + tick(S, sel, heat); + step(S, visc, jit); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.started) return; + const { F, V, x, y, vx, vy, N, pal, rim } = S; + F.fill(0); + V.fill(0); + // Splat every particle into a density field and a speed field. + const R = 2.1, R2 = R * R; + for (let i = 0; i < N; i++) { + const cx = x[i], cy = y[i], sp = sqrt(vx[i] * vx[i] + vy[i] * vy[i]); + const x0 = max(0, floor(cx - R + 0.5)), x1 = min(W - 1, floor(cx + R - 0.5)); + const y0 = max(0, floor(cy - R + 0.5)), y1 = min(H - 1, floor(cy + R - 0.5)); + for (let Y = y0; Y <= y1; Y++) { + const dy = Y + 0.5 - cy; + for (let X = x0; X <= x1; X++) { + const dx = X + 0.5 - cx, d2 = dx * dx + dy * dy; + if (d2 >= R2) continue; + const w = (1 - d2 / R2) ** 2, o = Y * W + X; + F[o] += w; + V[o] += w * sp; + } + } + } + // The body runs from a deep shade inside to the lit colour near the + // surface; the rim is pale. + const h0 = 0.47 + S.hue; + for (let k = 0; k < 64; k++) { + const u = k / 63; + hsv(h0 + 0.07 * u, 0.82 + 0.1 * u, 0.78 - 0.4 * u, pal, k * 3); + } + hsv(h0 - 0.03, 0.35, 1, rim, 0); + const LX = -0.45, LY = -0.89; + for (let Y = 0; Y < H; Y++) + for (let X = 0; X < W; X++) { + const o = Y * W + X, f = F[o]; + if (f < 0.35) continue; + const a = cl((f - 0.35) / 0.55, 0, 1); + const gx = (X < W - 1 ? F[o + 1] : f) - (X > 0 ? F[o - 1] : f); + const gy = (Y < H - 1 ? F[o + W] : f) - (Y > 0 ? F[o - W] : f); + const gl = sqrt(gx * gx + gy * gy) + 1e-6; + // Outward normal and its light. + const lit = (-gx * LX - gy * LY) / gl, edge = cl(1 - (f - 0.9) / 1.3, 0, 1); + const spec = edge * cl(lit, 0, 1) ** 3 * cl(gl, 0, 1); + const deep = cl((f - 0.9) / 2.2, 0, 1), k = floor(deep * 63) * 3; + const foam = cl((V[o] / f - 0.35) * 0.8, 0, 0.6); + const sh = 0.75 + 0.25 * cl(lit, -1, 1) * edge; + const m = max(spec, foam); + const r = (pal[k] * sh * (1 - m) + rim[0] * m) * a; + const g = (pal[k + 1] * sh * (1 - m) + rim[1] * m) * a; + const b = (pal[k + 2] * sh * (1 - m) + rim[2] * m) * a; + display.setPixel(X, Y, min(255, r) | 0, min(255, g) | 0, min(255, b) | 0); + } +} From a571754d6e764dd66b263d5e8c3603fdeda0a367 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 15:52:04 +0000 Subject: [PATCH 06/21] =?UTF-8?q?Boustrophedon=20=EA=B3=BC=20Penney's=20Ga?= =?UTF-8?q?me=20=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- .../pattern-study/patterns/boustrophedon.js | 30 ++- .../pattern-study/patterns/penney-game.js | 241 ++++++++++++++++++ 2 files changed, 264 insertions(+), 7 deletions(-) create mode 100644 incoming/pattern-study/patterns/penney-game.js diff --git a/incoming/pattern-study/patterns/boustrophedon.js b/incoming/pattern-study/patterns/boustrophedon.js index 60acb748e..c8fecb26c 100644 --- a/incoming/pattern-study/patterns/boustrophedon.js +++ b/incoming/pattern-study/patterns/boustrophedon.js @@ -91,6 +91,8 @@ const C_NEW = 226, C_OLD = 216, TAU = 30, KEEP_SHAPE = 0.33, KEEP_GRAIN = 0.7; // Cells a second at Speed s; a turn costs one cell's time, the flight back // to the left edge three. const RATE = 4; +// Light of the share's point and of the turned soil behind it. +const SHARE = 220, WAKE = 60; const turnLen = (way) => (way < 0 ? 3 : 1); // Direction and orientation of grid line l written the given way: // orientation 0 upright, 1 mirrored, 2 turned 180 degrees. @@ -229,29 +231,43 @@ export function draw(display, params) { } } - // The point of the share, a warm light in the furrow; on the flight back - // to the left edge it is lifted and dim. - let tx, ty, a = 1; + // The point of the share, a warm light in the furrow, and the turned soil + // behind it glowing for a few cells. On the flight back to the left edge + // the share is lifted and dim. + let tx, ty, a = 1, wake = 1, wx0; const yl = Y0 + l * LP + 2; if (i < NC) { const col = dir > 0 ? i : NC - 1 - i; tx = X0 + col * CW + (dir > 0 ? f * CW : (1 - f) * CW); ty = yl; + wx0 = tx; } else { const u = (S.pos - NC) / turnLen(S.lineWay), sm = u * u * (3 - 2 * u); const nl = (l + 1) % NL, xe = dir > 0 ? X0 + NC * CW : X0; const xs = S.lineWay < 0 ? X0 : dirOf(S.lineWay, nl) > 0 ? X0 : X0 + NC * CW; - const ny = nl === 0 ? Y0 + 2 : Y0 + nl * LP + 2; + const ny = Y0 + nl * LP + 2; tx = xe + (xs - xe) * sm; ty = yl + (ny - yl) * sm; - if (S.lineWay < 0) a = 0.45 + 0.55 * Math.abs(1 - 2 * u); + wx0 = xe; + wake = 1 - u; + if (S.lineWay < 0) a = 0.35 + 0.65 * Math.abs(1 - 2 * u); + } + const lamp = hsv(0.1 + hue, 0.3, 1), soil = hsv(0.07 + hue, 0.55, 1); + for (let y = yl - 2; y <= yl + 3; y++) { + const rw = y === yl + 3 ? 0.5 : 1; + for (let x = 0; x < W; x++) { + const ahead = (x + 0.5 - wx0) * dir; + const v = WAKE * wake * rw * (ahead > 0 ? exp(-ahead * ahead) : exp(ahead / 7)); + if (v < 1) continue; + const p = (y * W + x) * 3; + rgb[p] += soil[0] * v, rgb[p + 1] += soil[1] * v, rgb[p + 2] += soil[2] * v; + } } - const lamp = hsv(0.1 + hue, 0.3, 1); const x0 = max(0, floor(tx - 9)), x1 = min(W - 1, floor(tx + 9)), y0 = max(0, floor(ty - 9)), y1 = min(H - 1, floor(ty + 9)); for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) { const dx = x + 0.5 - tx, dy = y + 0.5 - ty, r2 = dx * dx + dy * dy; - const v = a * (160 * exp(-r2 / 2.2) + 45 * exp(-r2 / 24)); + const v = a * (SHARE * exp(-r2 / 1.8) + 50 * exp(-r2 / 30)); const p = (y * W + x) * 3; rgb[p] += lamp[0] * v, rgb[p + 1] += lamp[1] * v, rgb[p + 2] += lamp[2] * v; } diff --git a/incoming/pattern-study/patterns/penney-game.js b/incoming/pattern-study/patterns/penney-game.js new file mode 100644 index 000000000..0b2663893 --- /dev/null +++ b/incoming/pattern-study/patterns/penney-game.js @@ -0,0 +1,241 @@ +// ===== Patternflow pattern ===== +// Title: Penney's Game +// Family: data +// Idea: A fair coin is tossed into a shift register that runs down the panel one game a row, each game ending where A's or B's three tosses first appear in the window at its right end, so the winning triples stack into a column under the two choices while a tall bar of wins settles onto the odds Conway's leading numbers give. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Penney's game (Walter Penney, Journal of Recreational Mathematics, 1969), its odds by John Horton Conway's leading numbers (Martin Gardner, Scientific American, 1974), pattern round 69 +// Round: 69 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, A=0..7, Rival=0..6 +// +// Knob 1: Hue. Turns all four colours round the wheel together: at 0 heads are gold and tails azure, A's wins pink and B's green. +// Knob 2: Speed. Tosses a second: 10 at 2, 1 at 0.2, 15 at 3. A game of HHT against THH takes 6.5 tosses on average, so at 2 a new row comes down about once a second. +// Knob 3: A. The triple A picks, in this order: HHT, THH, TTH, HTT, HTH, THT, HHH, TTT. The first four are a loop: each one is B's best reply to the one before it, and B's best reply to HTT is HHT again. Turning it starts the tally again. +// Knob 4: Rival. B's triple, from the seven A left, ranked by its chance against A: 0 is the best reply, (not x2) x1 x2, which wins 2, 3 or 7 times to 1; 6 is the worst, from an even game against HHH or TTT down to losing 7 times to 1 against THH or HTT. Turning it starts the tally again. + +const W = 64, H = 128; +const { floor, round, min, max, sqrt, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- Conway's leading numbers. A triple is three bits, oldest toss first, +// heads 0 and tails 1. XY sets the bit worth 2^(L-1) when the last L tosses +// of X are the first L of Y. B's odds against A are (AA - AB) : (BB - BA). +// Checked against 200,000 simulated games for each of the eight A and its +// best reply: the share B won came within 0.0013 of these numbers. +const N = 3, MASK = 7; +const corr = (X, Y) => { + let c = 0; + for (let L = N; L >= 1; L--) if ((X & ((1 << L) - 1)) === Y >> (N - L)) c += 1 << (L - 1); + return c; +}; +// HHT, THH, TTH, HTT first: B's best reply to each is the next, and to HTT +// it is HHT. Then HTH, THT, HHH and TTT. +const A_ORDER = [1, 4, 6, 3, 2, 5, 0, 7]; +function matchup(A, rank) { + const rivals = []; + for (let B = 0; B < 8; B++) { + if (B === A) continue; + const a = corr(A, A) - corr(A, B), b = corr(B, B) - corr(B, A); + rivals.push({ B, a, b, p: a / (a + b) }); + } + rivals.sort((u, v) => v.p - u.p || u.B - v.B); + const r = rivals[rank]; + const g = (x, y) => (y ? g(y, x % y) : x); + const d = g(r.a, r.b); + // pB is B's chance of winning a game; the odds read A : B. + return { A, B: r.B, pB: r.p, oddsA: r.b / d, oddsB: r.a / d }; +} + +// ---- Type: digits and the letters A and B, 3 by 5. +const GLYPH = { + 0: "111101101101111", 1: "010110010010111", 2: "111001111100111", 3: "111001111001111", 4: "101101111001001", + 5: "111100111001111", 6: "111100111101111", 7: "111001010010010", 8: "111101111101111", 9: "111101111001111", + A: "010101111101101", B: "110101110101110", +}; + +// ---- Layout. The stream starts at row 14, one game every 5 rows: coins +// 3 by 3 on a 4 LED pitch, an underline row and a gap. Each game is right +// aligned on the window, the three slots at x = 44, 48 and 52 that the coin +// has to match, so the triples that won stand in one column under A's and +// B's own. 13 slots a row; a longer game shows its last 12 behind a mark. +const TOP = 14, PITCH = 5, XW = 52, SLOTS = 13, ROWS = 23; +const BX = 58, BW = 5, BY = TOP, BH = H - TOP; +const RATE = 5; + +export function setup(params) { + params.s = { + t: 0, hue: 0, speed: 2, key: "", m: null, + cur: [], games: [], aw: 0, bw: 0, + phase: 0, hold: 0, winner: -1, acc: 0, slide: 0, last: -9, won: -9, + rgb: new Float32Array(W * H * 3), + }; +} + +// One game from the first toss to the end, for filling the stream at once. +function playOut(m) { + const g = []; + let w = 0; + for (;;) { + const c = Math.random() < 0.5 ? 0 : 1; + g.push(c); + w = ((w << 1) | c) & MASK; + if (g.length >= N && w === m.A) return { g, w: 0 }; + if (g.length >= N && w === m.B) return { g, w: 1 }; + } +} + +function restart(S) { + S.games = []; + S.aw = S.bw = 0; + for (let i = 0; i < ROWS; i++) { + const r = playOut(S.m); + S.games.push(r); + if (r.w) S.bw++; + else S.aw++; + } + S.cur = []; + S.phase = 0; + S.acc = 0; + S.slide = 0; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.t += d; + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + const ai = cl(round(k[2]), 0, 7), ri = cl(round(k[3]), 0, 6), key = ai + "," + ri; + if (key !== S.key) { + S.key = key; + S.m = matchup(A_ORDER[ai], ri); + restart(S); + } + const m = S.m; + if (S.phase === 0) { + S.acc += d * RATE * S.speed; + while (S.acc >= 1 && S.phase === 0) { + S.acc -= 1; + const c = Math.random() < 0.5 ? 0 : 1; + S.cur.push(c); + S.last = S.t; + const n = S.cur.length; + if (n >= N) { + const w = (S.cur[n - 3] << 2) | (S.cur[n - 2] << 1) | S.cur[n - 1]; + if (w === m.A || w === m.B) { + S.winner = w === m.A ? 0 : 1; + if (S.winner) S.bw++; + else S.aw++; + S.phase = 1; + S.hold = 0; + S.won = S.t; + } + } + } + } else { + // The winning triple holds a moment, then the row goes down. + S.hold += d; + if (S.hold >= min(1.5, 0.7 / S.speed)) { + S.games.unshift({ g: S.cur, w: S.winner }); + if (S.games.length > ROWS + 1) S.games.pop(); + S.cur = []; + S.phase = 0; + S.acc = 0; + S.slide += PITCH; + } + } + S.slide = max(0, S.slide - d * PITCH * 6); +} + +function put(rgb, x, y, c, v) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const p = (y * W + x) * 3; + rgb[p] += c[0] * v, rgb[p + 1] += c[1] * v, rgb[p + 2] += c[2] * v; +} +// A coin: a 3 by 3 disc with soft corners. +function coin(rgb, x, y, c, v) { + for (let j = 0; j < 3; j++) for (let i = 0; i < 3; i++) put(rgb, x + i, y + j, c, i !== 1 && j !== 1 ? v * 0.35 : v); +} +function glyph(rgb, ch, x, y, c, v) { + const g = GLYPH[ch]; + for (let r = 0; r < 5; r++) for (let q = 0; q < 3; q++) if (g[r * 3 + q] === "1") put(rgb, x + q, y + r, c, v); +} +function number(rgb, n, xr, y, c, v) { + const s = String(n); + for (let i = 0; i < s.length; i++) glyph(rgb, s[s.length - 1 - i], xr - 3 - 4 * i, y, c, v); +} + +// One game's row with its top at y. live: the game still being tossed. +function row(S, rgb, g, w, y, live, cols) { + if (y + 3 < TOP - 1 || y >= H) return; + const n = g.length, show = n > SLOTS ? SLOTS - 1 : n, t = S.t; + for (let j = 0; j < show; j++) { + const c = g[n - 1 - j], x = XW - 4 * j; + let v = j < N ? 0.95 : 0.62; + if (live) { + v = j < N ? 0.85 : 0.6; + if (j === 0) v = min(1, v + 0.6 * exp(-(t - S.last) * 8)); + } + if (y >= TOP - 1) coin(rgb, x, y, c ? cols.T : cols.H, v); + } + if (n > SLOTS) for (let j = 0; j < 3; j++) put(rgb, XW - 4 * (SLOTS - 1) + j, y + 1, cols.dim, 0.5); + if (w >= 0 && n >= N && y + 3 >= TOP) { + let v = 0.7; + if (live) v = 0.6 + 0.4 * (0.5 + 0.5 * Math.cos((t - S.won) * 18)); + for (let x = XW - 8; x <= XW + 2; x++) put(rgb, x, y + 3, w ? cols.B : cols.A, v); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.m) return; + const rgb = S.rgb, m = S.m, hue = S.hue; + rgb.fill(0); + const cols = { + H: hsv(0.14 + hue, 0.66, 1), T: hsv(0.57 + hue, 0.8, 1), + A: hsv(0.9 + hue, 0.62, 1), B: hsv(0.36 + hue, 0.72, 1), + dim: hsv(0.6 + hue, 0.1, 1), ink: hsv(0.6 + hue, 0.06, 1), + }; + + // Header: count, letter, triple, and the player's side of the odds. + for (let s = 0; s < 2; s++) { + const y = 1 + 6 * s, c = s ? cols.B : cols.A, X = s ? m.B : m.A; + number(rgb, s ? S.bw : S.aw, 35, y, c, 0.75); + glyph(rgb, s ? "B" : "A", 38, y, c, 1); + for (let j = 0; j < N; j++) coin(rgb, XW - 8 + 4 * j, y + 1, (X >> (N - 1 - j)) & 1 ? cols.T : cols.H, 1); + glyph(rgb, String(s ? m.oddsB : m.oddsA), 59, y, cols.ink, 0.9); + } + + // The stream: the game being tossed at the top, the finished ones below. + const sl = round(S.slide); + row(S, rgb, S.cur, S.phase === 1 ? S.winner : -1, TOP - sl, true, cols); + for (let k = 0; k < S.games.length; k++) { + const y = TOP + PITCH * (k + 1) - sl; + if (y >= H) break; + row(S, rgb, S.games[k].g, S.games[k].w, y, false, cols); + } + + // The tally: A's share of the games from the top of the bar, B's below, + // and a white tick where Conway's numbers put the line. + const tot = S.aw + S.bw, ha = tot ? round((BH * S.aw) / tot) : 0; + for (let y = BY; y < H; y++) { + const isA = y - BY < ha, c = isA ? cols.A : cols.B; + for (let x = BX; x < BX + BW; x++) put(rgb, x, y, c, 0.55); + } + const yt = BY + round(BH * (1 - m.pB)); + for (let x = BX - 2; x < W; x++) put(rgb, x, yt, cols.ink, x >= BX && x < BX + BW ? 0.45 : 1); + + for (let p = 0; p < W * H; p++) { + const r = rgb[p * 3], g = rgb[p * 3 + 1], b = rgb[p * 3 + 2]; + if (r + g + b > 0) display.setPixel(p % W, floor(p / W), min(255, 255 * r) | 0, min(255, 255 * g) | 0, min(255, 255 * b) | 0); + } +} From 36284d6587fe70fefa1fa3bc6ff82e85f3a5eeb7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 15:55:39 +0000 Subject: [PATCH 07/21] =?UTF-8?q?Calculator=20Words=20=EA=B3=BC=20Aharonov?= =?UTF-8?q?-Bohm=20=EB=A5=BC=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- .../pattern-study/patterns/aharonov-bohm.js | 15 +- .../patterns/calculator-words.js | 315 ++++++++++++++++++ 2 files changed, 324 insertions(+), 6 deletions(-) create mode 100644 incoming/pattern-study/patterns/calculator-words.js diff --git a/incoming/pattern-study/patterns/aharonov-bohm.js b/incoming/pattern-study/patterns/aharonov-bohm.js index 09841d7c2..5a1939d6d 100644 --- a/incoming/pattern-study/patterns/aharonov-bohm.js +++ b/incoming/pattern-study/patterns/aharonov-bohm.js @@ -87,12 +87,15 @@ function besselRun(nu0, z, M, w, out) { // the plane wave to 1e-14; at a = 1/2 the orders m and 1 - m pair off and // cancel on theta = 0, so the shadow line is an exact zero. // -// Which a? The flux F in quanta enters as exp(2 pi i F) round any loop, so -// F and F + 1 are the same physics, and the gauge with the whole quanta -// taken out (a = F - round(F), here F - 1 on the knob's range) draws them as -// the same picture: F = 1 is exactly the bare plane wave. Every orders' sum -// is done for an LED and its mirror across the shadow line at once, since -// they share r and differ only in the sign of theta. +// Which a? The flux F in quanta enters only as exp(2 pi i F) round a loop, +// so F and F + 1 are the same physics, and a whole quantum can be taken out +// by a gauge change. The picture takes one out, a = F - 1, so F = 1 is the +// bare plane wave exactly, as F = 0 would be. Over the knob's range a runs +// from -1/2 to 1/2: the two ends are both half a quantum, drawn in gauges a +// quantum apart, and come out as mirror images, the same seam with the +// colours of its sides swapped. The orders' sum is done for an LED and its +// mirror across the shadow line at once, since they share r and differ only +// in the sign of theta. const MM = 260; function field(S, F, lam) { const a = F - 1, k = (2 * PI) / lam; diff --git a/incoming/pattern-study/patterns/calculator-words.js b/incoming/pattern-study/patterns/calculator-words.js new file mode 100644 index 000000000..6b00fda75 --- /dev/null +++ b/incoming/pattern-study/patterns/calculator-words.js @@ -0,0 +1,315 @@ +// ===== Patternflow pattern ===== +// Title: Calculator Words +// Family: type +// Idea: A pocket calculator fills the panel and keys in a little sum, its keys lighting as they go down, then turns itself upside down so the answer on its seven-segment display reads as a word, 0.7734 as hELLO with the decimal point hanging at the top of the line, holds it, and turns back for the next riddle. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: upside-down calculator spelling on seven-segment displays (BEGHILOS, the 1970s pocket calculator pastime), pattern round 69 +// Round: 69 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Words=0..3, Glow=0..1 +// +// Knob 1: Hue. The colour of the display, with the operator keys and the body following it round the wheel: at 0 a blue-green vacuum fluorescent display over orange operator keys on a slate body; at 0.54 the red of the first LED calculators. +// Knob 2: Speed. How fast the machine works: at 2 a key goes down every 0.22 s and the turn takes 1.3 s, so hELLO is keyed, turned, read and turned back in 7.7 s; at 0.2 it takes ten times as long, at 3 two thirds. +// Knob 3: Words. Which riddles: 0 the classics (hELLO, BOSS, LOSE, GLOBE, GOOSE, EGGSHELL); 1 short words (hI, BEE, OIL, BIG, EGG, ShOE, SOIL); 2 long ones made by multiplying two numbers (LEGIBLE, SLEIGh, BOGGLE, OBLIGE, GIGGLE, SIZZLE, hOBBIES); 3 answers under 1, whose decimal point turns up at the top of the word (IGLOO, hELLO, SILO, SOLO, hOBO, BOO). +// Knob 4: Glow. How much light the display spills: at 0 the unlit segments are almost gone and the digits stand sharp on dark glass; at the opening 0.06 the unlit segments show faintly as a ghost 8 in every place; at 1 the ghosts glow at half strength and a haze spreads round every lit digit. +// Buttons: any knob's button clears the calculator and starts the next riddle, when the calculator stands the right way up. + +const W = 64, H = 128; +const { floor, min, max, abs, sqrt, sin, cos, 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) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- The riddles. Each is the sum the machine keys after C, written with * +// and / for the keys x and the division sign; = follows. Read upside down, +// a digit becomes the letter its segments make when turned: 0 O, 1 I, 2 Z, +// 3 E, 4 h, 5 S, 7 L, 8 B, 9 G (a 9 with its tail turns into a 6 shape). +// The order of the digits turns round as well, so 0.7734 reads hELLO. +// Every answer was checked by turning the segment masks below, not by a +// table of letters: the display is only ever drawn the right way up. +const SETS = [ + // 0.7734 hELLO, 5508 BOSS, 3507 LOSE, 38079 GLOBE, 35009 GOOSE, and SHELL + // (77345) times 1000 plus 993, 77345993 EGGSHELL, the longest the 8 places + // hold. + ["1-.2266", "68*81", "21*167", "9*4231", "13*2693", "155*499*1000+993"], + // 14 hI, 338 BEE, 710 OIL, 918 BIG, 993 EGG, 3045 ShOE, 7105 SOIL. + ["2*7", "13*26", "10*71", "27*34", "3*331", "35*87", "29*245"], + // 3781937 LEGIBLE, 491375 SLEIGh, 379908 BOGGLE, 391780 OBLIGE, 379919 + // GIGGLE, 372215 SIZZLE, 5318804 hOBBIES. + ["967*3911", "125*3931", "519*732", "380*1031", "181*2099", "493*755", "4*1329701"], + // 0.0791 IGLOO, 0.7734 hELLO, 0.715 SILO, 0.705 SOLO, 0.804 hOBO, 0.08 BOO. + [".113*.7", "3.867/5", "1.43/2", "1.41/2", "4.02/5", ".2*.4"], +]; + +// ---- Seven segments, bit 0 to 6 for a to g: a the top bar, b and c the +// right side from the top, d the bottom bar, e and f the left side from the +// bottom, g the middle bar. A 6 keeps its top bar and a 9 its tail, a 7 has +// no f: so a turned 7 is an L and not a 4 with a hook. +const SEG = { 0: 63, 1: 6, 2: 91, 3: 79, 4: 102, 5: 109, 6: 125, 7: 7, 8: 127, 9: 111, "-": 64 }; +// A digit is 5 LEDs wide and 11 tall, 1 LED strokes, with the corners left +// dark so every segment stands apart; the decimal point sits one column to +// the right of the bottom bar. +const SEGPX = [[], [], [], [], [], [], []]; +for (let i = 1; i <= 3; i++) SEGPX[0].push([i, 0]), SEGPX[3].push([i, 10]), SEGPX[6].push([i, 5]); +for (let j = 1; j <= 4; j++) SEGPX[1].push([4, j]), SEGPX[5].push([0, j]), SEGPX[2].push([4, j + 5]), SEGPX[4].push([0, j + 5]); +const DIG_X = 5, DIG_P = 7, DIG_Y = 11; + +// ---- The keypad: 4 columns of 13 LEDs, 5 rows of 15, labels in 3 by 5. +const GLYPH = { + 0: "111101101101111", 1: "010110010010111", 2: "111001111100111", 3: "111001111001111", 4: "101101111001001", + 5: "111100111001111", 6: "111100111101111", 7: "111001001001001", 8: "111101111101111", 9: "111101111001111", + C: "011100100100011", p: "010111010000111", "%": "101001010100101", d: "010000111000010", x: "000101010101000", + "-": "000000111000000", "+": "000010111010000", "=": "000111000111000", ".": "000000000000010", +}; +const LAYOUT = [ + ["C", "p", "%", "d"], + ["7", "8", "9", "x"], + ["4", "5", "6", "-"], + ["1", "2", "3", "+"], + ["0", null, ".", "="], +]; +const KEYS = []; +LAYOUT.forEach((row, r) => + row.forEach((id, c) => { + if (!id) return; + const wide = id === "0"; + KEYS.push({ id, x: 3 + 15 * c, y: 32 + 19 * r, w: wide ? 28 : 13, kind: r === 0 ? 0 : c === 3 || id === "=" ? 2 : 1 }); + }), +); +const KEYIX = {}; +KEYS.forEach((k, i) => (KEYIX[k.id] = i)); + +// Machine time is seconds at Speed 2. +const KEY = 0.22, HOLD = 0.8, TURN = 1.3, BACK = 1.1, PAUSE = 0.35; + +// ---- The arithmetic of a plain pocket calculator: an operator key works +// out what is pending, left to right, and shows it. +function fmt(v) { + if (!isFinite(v) || abs(v) >= 1e8) return "E"; + const neg = v < 0; + v = abs(v); + const ints = v < 1 ? 1 : floor(Math.log10(v)) + 1; + let s = v.toFixed(max(0, 8 - ints)); + if (s.indexOf(".") >= 0) s = s.replace(/0+$/, "").replace(/\.$/, ""); + return (neg ? "-" : "") + s; +} +function apply(a, op, b) { + return op === "+" ? a + b : op === "-" ? a - b : op === "x" ? a * b : b === 0 ? NaN : a / b; +} +function press(S, id) { + const c = S.calc; + if (id === "C") (c.acc = 0), (c.op = null), (c.entry = null), (c.disp = "0"); + else if ((id >= "0" && id <= "9") || id === ".") { + if (c.entry === null) c.entry = id === "." ? "0." : id; + else if (c.entry.replace(/\D/g, "").length < 8 && !(id === "." && c.entry.includes("."))) c.entry += id; + c.disp = c.entry; + } else if (id === "=" || id === "+" || id === "-" || id === "x" || id === "d") { + if (c.entry !== null) { + c.acc = c.op ? apply(c.acc, c.op, Number(c.entry)) : Number(c.entry); + c.entry = null; + c.disp = fmt(c.acc); + } + c.op = id === "=" ? null : id; + } + if (KEYIX[id] !== undefined) S.pressed[KEYIX[id]] = S.clock; +} + +// The display string, right aligned in 8 places, as segment masks with bit +// 7 for the decimal point. +function cells(disp) { + const out = new Uint8Array(8); + if (disp === "E") return (out[7] = 121), out; + let i = 7; + for (let j = disp.length - 1; j >= 0 && i >= 0; j--) { + const ch = disp[j]; + if (ch === ".") { + j--; + out[i--] = SEG[disp[j]] | 128; + } else out[i--] = SEG[ch]; + } + return out; +} + +function startRiddle(S) { + const list = SETS[S.set]; + const expr = list[S.ri % list.length]; + S.keys = ["C", ...expr.split("").map((ch) => (ch === "*" ? "x" : ch === "/" ? "d" : ch)), "="]; + S.ki = 0; + S.mt = 0; + S.tEq = (S.keys.length - 1) * KEY; + S.read = 0; +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, glow: 0.06, set: -1, ri: 0, keys: [], ki: 0, mt: 0, tEq: 0, read: 0, clock: 0, + theta: 0, calc: { acc: 0, op: null, entry: null, disp: "0" }, + pressed: new Float32Array(KEYS.length).fill(-99), + face: new Float32Array(W * H * 3), halo: new Float32Array(W * 32), tmp: new Float32Array(W * 32), + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.glow = cl(k[3], 0, 1); + const set = min(3, floor(cl(k[2], 0, 3))); + if (S.set < 0) (S.set = set), startRiddle(S); + const step = (d * S.speed) / 2; + S.mt += step; + S.clock += step; + // Keys go down on the beat; a long frame may hold more than one. + while (S.ki < S.keys.length && S.mt >= S.ki * KEY) press(S, S.keys[S.ki++]); + if (S.ki === S.keys.length && !S.read) S.read = 1.6 + 0.15 * S.calc.disp.replace(/\D/g, "").length; + const t0 = S.tEq + HOLD, t1 = t0 + TURN, t2 = t1 + (S.read || 9), t3 = t2 + BACK; + // Over, then back the way it came. + S.theta = S.mt < t1 ? PI * ease((S.mt - t0) / TURN) : PI * (1 - ease((S.mt - t2) / BACK)); + let next = S.mt >= t3 + PAUSE; + if (input.btnPressed && input.btnPressed.some(Boolean) && S.theta === 0) next = true; + if (next) { + if (set !== S.set) (S.set = set), (S.ri = 0); + else S.ri++; + startRiddle(S); + } +} + +// ---- Drawing the face, always the right way up, into a buffer of its own. +function put(F, x, y, c, m) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + (F[o] = c[0] * m), (F[o + 1] = c[1] * m), (F[o + 2] = c[2] * m); +} +function add(F, x, y, c, m) { + const o = (y * W + x) * 3; + (F[o] += c[0] * m), (F[o + 1] += c[1] * m), (F[o + 2] += c[2] * m); +} + +function drawFace(S) { + const F = S.face, hd = 0.46 + S.hue, g = S.glow; + const body = hsv(hd + 0.15, 0.32, 0.15), bezel = hsv(hd + 0.15, 0.3, 0.06), glass = hsv(hd, 0.7, 0.035 + 0.1 * g); + const lit = hsv(hd, 0.55, 1), ghost = hsv(hd, 0.65, 0.07 + 0.45 * g), haze = hsv(hd, 0.8, 1); + F.fill(0); + // Body: a rounded slab 62 by 126 with a lit top edge and a dark bottom. + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const cx = x + 0.5, cy = y + 0.5; + const qx = max(5 - cx, 0, cx - 59), qy = max(5 - cy, 0, cy - 123), a = cl(4.5 - sqrt(qx * qx + qy * qy), 0, 1); + if (a <= 0) continue; + const sh = y <= 1 ? 1.5 : y >= 126 ? 0.55 : x <= 1 ? 1.2 : x >= 62 ? 0.75 : 1; + put(F, x, y, body, a * sh); + } + for (let y = 5; y <= 27; y++) for (let x = 2; x <= 61; x++) put(F, x, y, y === 27 ? body : bezel, y === 27 ? 1.35 : 1); + for (let y = 6; y <= 26; y++) for (let x = 3; x <= 60; x++) put(F, x, y, glass, 1); + + // Display. The haze is the lit segments blurred twice by a 5-wide box. + const cs = cells(S.calc.disp), Hz = S.halo, T = S.tmp; + Hz.fill(0); + for (let i = 0; i < 8; i++) { + const x0 = DIG_X + DIG_P * i; + for (let s = 0; s < 7; s++) { + const on = (cs[i] >> s) & 1; + for (const [dx, dy] of SEGPX[s]) { + put(F, x0 + dx, DIG_Y + dy, on ? lit : ghost, 1); + if (on) Hz[(DIG_Y + dy - 4) * W + x0 + dx] = 1; + } + } + const dp = (cs[i] >> 7) & 1; + put(F, x0 + 5, DIG_Y + 10, dp ? lit : ghost, 1); + if (dp) Hz[(DIG_Y + 6) * W + x0 + 5] = 1; + } + for (let pass = 0; pass < 2; pass++) { + for (let y = 0; y < 32; y++) + for (let x = 0; x < W; x++) { + let a = 0; + for (let j = -2; j <= 2; j++) a += Hz[y * W + cl(x + j, 0, W - 1)]; + T[y * W + x] = a / 5; + } + for (let y = 0; y < 32; y++) + for (let x = 0; x < W; x++) { + let a = 0; + for (let j = -2; j <= 2; j++) a += T[cl(y + j, 0, 31) * W + x]; + Hz[y * W + x] = a / 5; + } + } + const hz = 0.25 + 1.6 * g; + for (let y = 6; y <= 26; y++) for (let x = 3; x <= 60; x++) add(F, x, y, haze, hz * Hz[(y - 4) * W + x]); + + // Keys: function keys pale, number keys dark, operator keys in colour. + // A key that goes down drops one row, loses its shadow and lights up, + // fading over a quarter of a second of machine time. + const face = [hsv(hd + 0.15, 0.1, 0.42), hsv(hd + 0.15, 0.2, 0.22), hsv(hd + 0.62, 0.8, 0.62)]; + const glowc = [hsv(hd + 0.15, 0.05, 0.85), hsv(hd, 0.45, 0.7), hsv(hd + 0.62, 0.55, 1)]; + const ink = [hsv(hd + 0.15, 0.3, 0.07), hsv(hd + 0.15, 0.05, 0.8), hsv(hd + 0.62, 0.15, 1)]; + const white = [1, 1, 1]; + for (let i = 0; i < KEYS.length; i++) { + const K = KEYS[i], P = max(0, Math.exp(-(S.clock - S.pressed[i]) / 0.25)), dy = P > 0.35 ? 1 : 0, kd = K.kind; + const col = [0, 1, 2].map((j) => face[kd][j] + (glowc[kd][j] - face[kd][j]) * P); + if (!dy) for (let x = K.x + 1; x < K.x + K.w - 1; x++) put(F, x, K.y + 15, body, 0.45); + for (let r = 0; r < 15; r++) + for (let q = 0; q < K.w; q++) { + if ((r === 0 || r === 14) && (q === 0 || q === K.w - 1)) continue; + put(F, K.x + q, K.y + dy + r, col, r === 0 ? 1.3 : r === 14 ? 0.62 : q === K.w - 1 ? 0.85 : 1); + } + const gl = GLYPH[K.id], gx = K.x + (K.w >> 1) - 1, gy = K.y + dy + 5; + const ic = [0, 1, 2].map((j) => ink[kd][j] + (white[j] - ink[kd][j]) * P * (kd === 0 ? 0.3 : 1)); + for (let r = 0; r < 5; r++) for (let q = 0; q < 3; q++) if (gl[r * 3 + q] === "1") put(F, gx + q, gy + r, ic, 1); + } + // The display's light falls on the body and the top rows of keys below + // it, as a vacuum fluorescent display does in a dark room. + const sp = 0.3 * g * g + 0.1 * g; + if (sp > 0) + for (let y = 28; y < 100; y++) { + const m = sp * Math.exp(-(y - 28) / 16); + for (let x = 1; x < 63; x++) add(F, x, y, haze, m * (0.6 + 0.4 * sin((PI * (x - 1)) / 61))); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + drawFace(S); + const F = S.face, th = S.theta, c = cos(th), s = sin(th); + const out = (x, y, r, g, b) => display.setPixel(x, y, (255 * min(1, r)) | 0, (255 * min(1, g)) | 0, (255 * min(1, b)) | 0); + if (s < 1e-6) { + // At rest, either way up, the face lands on the LEDs one for one: a half + // turn about the panel's centre sends LED (x, y) to (63 - x, 127 - y). + const up = c > 0; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const o = ((up ? y : H - 1 - y) * W + (up ? x : W - 1 - x)) * 3; + out(x, y, F[o], F[o + 1], F[o + 2]); + } + return; + } + // Turning: the face shrinks just enough that its corners stay on the + // panel, and holds at 0.45 while it lies more than 63 degrees over, so it + // does not swell at 90 degrees; every LED takes four bilinear samples. + const f = s > 2 * abs(c) ? 143.11 : 64 * abs(c) + 128 * s, sc = 64 / f; + const ic = c / sc, is = s / sc; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let r = 0, g = 0, b = 0; + for (let k = 0; k < 4; k++) { + const px = x + 0.25 + 0.5 * (k & 1) - 32, py = y + 0.25 + 0.5 * (k >> 1) - 64; + const u = ic * px + is * py + 31.5, v = -is * px + ic * py + 63.5; + const u0 = floor(u), v0 = floor(v), fu = u - u0, fv = v - v0; + for (let j = 0; j < 4; j++) { + const uu = u0 + (j & 1), vv = v0 + (j >> 1); + if (uu < 0 || uu >= W || vv < 0 || vv >= H) continue; + const wgt = ((j & 1) ? fu : 1 - fu) * ((j >> 1) ? fv : 1 - fv), o = (vv * W + uu) * 3; + (r += F[o] * wgt), (g += F[o + 1] * wgt), (b += F[o + 2] * wgt); + } + } + out(x, y, r / 4, g / 4, b / 4); + } +} From e3066cd42c8a4b5270f138c7c73b2b1f04c0f2ae Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 15:57:02 +0000 Subject: [PATCH 08/21] =?UTF-8?q?Boustrophedon=20=EA=B3=BC=20Penney's=20Ga?= =?UTF-8?q?me=20=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- .../pattern-study/patterns/boustrophedon.js | 22 ++++++++++--------- .../pattern-study/patterns/penney-game.js | 8 ++++--- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/incoming/pattern-study/patterns/boustrophedon.js b/incoming/pattern-study/patterns/boustrophedon.js index c8fecb26c..e56c39330 100644 --- a/incoming/pattern-study/patterns/boustrophedon.js +++ b/incoming/pattern-study/patterns/boustrophedon.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Boustrophedon // Family: type -// Idea: A plough point of light works a field of twenty furrows line after line, turning up last pass's dimming letters ahead of it and writing Hesiod's crane passage into the furrow behind it, mirrored on the lines it ploughs back, so the eye follows the ox across the panel and down. +// Idea: A point of light ploughs a field of twenty furrows line after line, turning under the cooled letters of its last pass and writing Hesiod's crane passage into the furrow behind it, mirrored on every line it ploughs back, so the eye follows the ox across the panel and down. // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -11,13 +11,13 @@ // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Way=-1..1, Words=0..2 // -// Knob 1: Hue. Turns the field and the letters round the wheel together: at 0 the furrows run from deep blue in the troughs to pale blue on the ridges, fresh letters are cream gold and old ones cool to pale blue. +// Knob 1: Hue. Turns the field and the letters round the wheel together: at 0 the furrows run from deep blue in the troughs to pale blue on the ridges, fresh letters are cream gold and cool to light blue over half a minute. // Knob 2: Speed. How fast the plough writes: 8 letters a second at 2, so a line of 15 takes under 2 s and the whole field of 300 about 40 s; 0.8 a second at 0.2, 12 at 3. // Knob 3: Way. How the plough turns at the end of a line, taken up at the start of the next one. At 0 boustrophedon: the plough turns and writes the next line right to left with every letter mirrored. At -1 every line runs left to right and the plough lifts and flies back to the left edge. At 1 reverse boustrophedon, as on the rongorongo tablets: the return lines are turned upside down, as if the field were turned round for each. // Knob 4: Words. How words are parted. At 0 by a column of three dots, the word divider of many archaic inscriptions; at 1 not at all, the letters run on as scriptio continua; at 2 by a blank, as we write now. const W = 64, H = 128; -const { floor, round, min, max, exp, sin, PI } = Math; +const { floor, round, min, max, exp, sin } = Math; const cl = (v, a, b) => (v < a ? a : v > b ? b : v); function hsv(h, s, v) { @@ -84,8 +84,9 @@ for (let p = 0; p < W * H; p++) { GRAIN[p] = (p % W) & 1 ? 20 + 10 * h : 6 * h; } -// Letters: fresh ones stand C_NEW above the trough and cool to C_OLD as the -// pass goes on; they keep part of the furrow's shape and all of its grain. +// Letters give their own light, so cloud shadows pass them by. A fresh one +// stands at C_NEW and sinks to C_OLD as it cools; each keeps a third of the +// furrow's shape and most of its grain, so the strokes are not flat. const C_NEW = 226, C_OLD = 216, TAU = 30, KEEP_SHAPE = 0.33, KEEP_GRAIN = 0.7; // Cells a second at Speed s; a turn costs one cell's time, the flight back @@ -95,7 +96,9 @@ const RATE = 4; const SHARE = 220, WAKE = 60; const turnLen = (way) => (way < 0 ? 3 : 1); // Direction and orientation of grid line l written the given way: -// orientation 0 upright, 1 mirrored, 2 turned 180 degrees. +// orientation 0 upright, 1 mirrored, 2 turned 180 degrees. Rongorongo is +// read from the bottom line up; here every way ploughs from the top down, so +// only the turn at the end of a line changes. const dirOf = (way, l) => (way !== 0 && way !== 1) || l % 2 === 0 ? 1 : -1; const orientOf = (way, l) => (dirOf(way, l) > 0 ? 0 : way === 0 ? 1 : 2); @@ -106,7 +109,7 @@ export function setup(params) { glyph: new Uint8Array(CELLS), orient: new Uint8Array(CELLS), birth: new Float32Array(CELLS), // What lay in the cell under the plough before it got there. oldG: 0, oldO: 0, oldB: 0, - L: new Float32Array(W * H), rgb: new Float32Array(W * H * 3), + rgb: new Float32Array(W * H * 3), }; } @@ -181,11 +184,11 @@ export function update(dt, input, params) { export function draw(display, params) { const S = params.s; if (!S || !S.ready) return; - const { L, rgb } = S, t = S.t, hue = S.hue; + const { rgb } = S, t = S.t, hue = S.hue; const trough = hsv(0.66 + hue, 0.78, 1), ridge = hsv(0.55 + hue, 0.42, 1); const fresh = hsv(0.12 + hue, 0.38, 1), cool = hsv(0.6 + hue, 0.5, 1); - // Cloud shadows drift over the field from the upper left. + // Cloud shadows drift over the soil, darkening it by up to 38%. const cx = 0.07, cy = 0.045, wx = 0.035, wy = -0.06; const ca = t * 0.9, cb = t * 0.7 + 1.7; @@ -198,7 +201,6 @@ export function draw(display, params) { const p = y * W + x; const cloud = 0.81 + 0.095 * (sin(cx * x + cy * y - ca) + sin(wx * x + wy * y + cb)); const v = (base + GRAIN[p]) * cloud; - L[p] = cloud; rgb[p * 3] = r0 * v, rgb[p * 3 + 1] = g0 * v, rgb[p * 3 + 2] = b0 * v; } } diff --git a/incoming/pattern-study/patterns/penney-game.js b/incoming/pattern-study/patterns/penney-game.js index 0b2663893..618325ee6 100644 --- a/incoming/pattern-study/patterns/penney-game.js +++ b/incoming/pattern-study/patterns/penney-game.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Penney's Game // Family: data -// Idea: A fair coin is tossed into a shift register that runs down the panel one game a row, each game ending where A's or B's three tosses first appear in the window at its right end, so the winning triples stack into a column under the two choices while a tall bar of wins settles onto the odds Conway's leading numbers give. +// Idea: A fair coin is tossed into the top row from the right, each toss pushing the earlier ones left, until A's or B's triple fills the three slots at the right end; the finished game slides down a row, so the winning triples stack into a column under the two players' choices while a tall bar of wins settles onto the odds from Conway's leading numbers. // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -17,7 +17,7 @@ // Knob 4: Rival. B's triple, from the seven A left, ranked by its chance against A: 0 is the best reply, (not x2) x1 x2, which wins 2, 3 or 7 times to 1; 6 is the worst, from an even game against HHH or TTT down to losing 7 times to 1 against THH or HTT. Turning it starts the tally again. const W = 64, H = 128; -const { floor, round, min, max, sqrt, exp } = Math; +const { floor, round, min, max, exp } = Math; const cl = (v, a, b) => (v < a ? a : v > b ? b : v); function hsv(h, s, v) { @@ -30,7 +30,9 @@ function hsv(h, s, v) { // heads 0 and tails 1. XY sets the bit worth 2^(L-1) when the last L tosses // of X are the first L of Y. B's odds against A are (AA - AB) : (BB - BA). // Checked against 200,000 simulated games for each of the eight A and its -// best reply: the share B won came within 0.0013 of these numbers. +// best reply: the share B won came within 0.0013 of these numbers. This +// pattern's own tally, run for an hour at Speed 3 (about 5,500 games a +// matchup), came within one standard error on five matchups. const N = 3, MASK = 7; const corr = (X, Y) => { let c = 0; From df7c4aa215d53c8536266bb25f1a72310a82db4d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 15:57:04 +0000 Subject: [PATCH 09/21] =?UTF-8?q?Caustic=20Letters=20=EB=A5=BC=20=EB=8D=94?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- .../pattern-study/patterns/caustic-letters.js | 346 ++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 incoming/pattern-study/patterns/caustic-letters.js diff --git a/incoming/pattern-study/patterns/caustic-letters.js b/incoming/pattern-study/patterns/caustic-letters.js new file mode 100644 index 000000000..6e18913fd --- /dev/null +++ b/incoming/pattern-study/patterns/caustic-letters.js @@ -0,0 +1,346 @@ +// ===== Patternflow pattern ===== +// Title: Caustic Letters +// Family: type +// Idea: The panel is the floor of a pool lit from above, and the water surface is shaped so the light it bends spells a word: while a gust or a dropped stone roughens it the light runs in a net of bright caustic lines, and as the ripples calm the same light gathers into the word, until the next disturbance breaks it and brings the next word. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: goal-based caustics (Marios Papas, Wojciech Jarosz, Wenzel Jakob, Szymon Rusinkiewicz, Wojciech Matusik and Tim Weyrich, Goal-Based Caustics, 2011), high-contrast computational caustic design (Yuliy Schwartzburg, Romain Testuz, Andrea Tagliasacchi and Mark Pauly, 2014); where the papers solve for an optimal-transport map, this pattern uses the Knothe-Rosenblatt rearrangement in its place (Murray Rosenblatt, 1952; Herbert Knothe, 1957); pattern round 69 +// Round: 69 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Ripple=1..3, Focus=-1..1 +// +// Knob 1: Hue. Turns the water light round the wheel: at 0 the dim light is a deep azure and the brightest lines a pale aqua. +// Knob 2: Speed. A word's whole cycle, from the gust through the calm to the next gust, takes 8 s at 2, 80 s at 0.2 and 5.3 s at 3; the waves travel faster or slower with it. +// Knob 3: Ripple. How high the waves stand, as a multiple of the opening height. At 1 the net of caustic lines has round cells 15 to 20 LEDs across and the calm word lies nearly still; at 3 the lines fold into sharp-cornered shards and the word trembles even in the calm. +// Knob 4: Focus. Where the floor lies against the depth the surface was shaped for. At 0 the word is sharp. At -1 the floor is at half that depth and the word is a soft glow half way from even light. At 1 it is half as deep again and the light overshoots: each stroke pinches to a thin bright line, and the rows of the map show as faint threads across the gaps between letters. +// Buttons: any knob's button drops a stone, which breaks the word now and brings the next one. + +const W = 64, H = 128; +const { floor, min, max, sqrt, sin, cos, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const smooth = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- Letters, as strokes of round-ended pen. Latin capitals are drawn in a +// unit box and stacked down the panel like a hotel sign; the three jamo of +// 빛 (ㅂ, ㅣ and the final ㅊ) are drawn straight onto the 64 by 128 panel. +const G_ARC = []; +for (let a = 40; a <= 350; a += 15) G_ARC.push([0.5 + 0.5 * cos((a * PI) / 180), 0.5 - 0.5 * sin((a * PI) / 180)]); +G_ARC.push([0.55, 0.5 - 0.5 * sin((350 * PI) / 180)]); +const LATIN = { + L: [26, [[[0, 0], [0, 1], [1, 1]]]], + I: [18, [[[0.5, 0], [0.5, 1]], [[0.1, 0], [0.9, 0]], [[0.1, 1], [0.9, 1]]]], + G: [34, [G_ARC]], + H: [32, [[[0, 0], [0, 1]], [[1, 0], [1, 1]], [[0, 0.5], [1, 0.5]]]], + T: [36, [[[0, 0], [1, 0]], [[0.5, 0], [0.5, 1]]]], + W: [46, [[[0, 0], [0.22, 1], [0.5, 0.32], [0.78, 1], [1, 0]]]], + A: [38, [[[0, 1], [0.5, 0], [1, 1]], [[0.21, 0.66], [0.79, 0.66]]]], + V: [38, [[[0, 0], [0.5, 1], [1, 0]]]], + E: [28, [[[1, 0], [0, 0], [0, 1], [1, 1]], [[0, 0.5], [0.85, 0.5]]]], +}; +const BIT = [ + [[[9, 8], [9, 58]], [[38, 8], [38, 58]], [[9, 33], [38, 33]], [[9, 58], [38, 58]]], + [[[53, 5], [53, 70]]], + [[[25, 77], [37, 77]], [[9, 88], [54, 88]], [[31, 92], [9, 121]], [[25.5, 100], [55, 121]]], +]; + +// A word becomes a list of segments [x0, y0, x1, y1] in LEDs and a pen radius. +function layout(word) { + if (word === "빛") return { r: 3.2, segs: BIT.flat().flatMap(segsOf((x, y) => [x, y])) }; + const n = word.length, ch = 124 / n, r = n > 4 ? 2.5 : 2.8, gap = n > 4 ? 4.2 : 5; + const segs = []; + for (let i = 0; i < n; i++) { + const [w, strokes] = LATIN[word[i]]; + const top = 2 + i * ch + r + gap / 2, h = ch - 2 * r - gap, left = 32 - w / 2; + for (const pl of strokes) segs.push(...segsOf((x, y) => [left + x * w, top + y * h])(pl)); + } + return { r, segs }; +} +function segsOf(map) { + return (pl) => { + const out = []; + for (let k = 0; k + 1 < pl.length; k++) { + const [a, b] = map(pl[k][0], pl[k][1]), [c, d] = map(pl[k + 1][0], pl[k + 1][1]); + out.push([a, b, c, d]); + } + return out; + }; +} + +// The light the floor should receive: the word's mask, softened by one pass +// of a 1-2-1 blur, over a floor of 0.1 so some light lands everywhere. +const FLOOR = 0.1; +function density(word) { + const { r, segs } = layout(word); + const d2 = new Float32Array(W * H).fill(1e9), m = new Float32Array(W * H), rho = new Float32Array(W * H); + // Each segment only looks at the LEDs inside its own box. + for (const [a, b, c, d] of segs) { + const ex = c - a, ey = d - b, l2 = ex * ex + ey * ey; + const x0 = max(0, floor(min(a, c) - r - 1)), x1 = min(W - 1, floor(max(a, c) + r + 1)); + const y0 = max(0, floor(min(b, d) - r - 1)), y1 = min(H - 1, floor(max(b, d) + r + 1)); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const px = x + 0.5, py = y + 0.5; + const t = l2 > 0 ? cl(((px - a) * ex + (py - b) * ey) / l2, 0, 1) : 0; + const qx = px - a - t * ex, qy = py - b - t * ey, q = qx * qx + qy * qy; + if (q < d2[y * W + x]) d2[y * W + x] = q; + } + } + for (let p = 0; p < W * H; p++) m[p] = cl(r + 0.5 - sqrt(d2[p]), 0, 1); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let s = 0; + for (let dy = -1; dy <= 1; dy++) + for (let dx = -1; dx <= 1; dx++) s += m[cl(y + dy, 0, H - 1) * W + cl(x + dx, 0, W - 1)] * (2 - (dx & 1)) * (2 - (dy & 1)); + rho[y * W + x] = FLOOR + s / 16; + } + return rho; +} + +// ---- Photons. Two by two per LED leave the surface evenly, each from a +// fixed random point inside its own quarter of the LED; on a regular grid +// the rows of photons landed as dashed lines across the dim floor. The waves +// are summed on the 65 by 129 corners of the LEDs, and each photon reads the +// push under it bilinearly from its cell (PC) at the offsets in PFG. +const PX = 128, PY = 256, NP = PX * PY, GW = W + 1, GH = H + 1; +const SU = new Float32Array(NP), SV = new Float32Array(NP); +const jit = (p, k) => { + let h = Math.imul(p ^ (k * 0x9e3779b9), 0x85ebca6b) >>> 0; + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35) >>> 0; + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; +const PC = new Int32Array(NP), PFG = new Float64Array(2 * NP); +for (let j = 0; j < PY; j++) + for (let i = 0; i < PX; i++) { + const p = j * PX + i, u = (i + jit(p, 1)) / 2, v = (j + jit(p, 2)) / 2; + const cx = min(W - 1, floor(u)), cy = min(H - 1, floor(v)); + SU[p] = u, SV[p] = v, PC[p] = cy * GW + cx, PFG[2 * p] = u - cx, PFG[2 * p + 1] = v - cy; + } + +// ---- The Knothe-Rosenblatt map T from even light onto a density rho. The +// height comes first: a photon's share of the way down the surface is +// matched to the same share of rho's row sums. Then, inside the row it lands +// in, its share across is matched to the same share of that row. Both are +// inverse cumulative sums, read linearly inside an LED. T moves light only +// down and then across, so it is not the cheapest map the papers solve for, +// but it carries exactly rho. +function knothe(rho) { + const Mc = new Float64Array(H + 1), C = new Float64Array(H * (W + 1)); + for (let r = 0; r < H; r++) { + let s = 0; + for (let c = 0; c < W; c++) (C[r * (W + 1) + c] = s), (s += rho[r * W + c]); + C[r * (W + 1) + W] = s; + Mc[r + 1] = Mc[r] + s; + } + const TX = new Float32Array(NP), TY = new Float32Array(NP), M = Mc[H]; + for (let p = 0; p < NP; p++) { + const v = (SV[p] / H) * M; + let lo = 0, hi = H - 1; + while (lo < hi) { + const mid = (lo + hi + 1) >> 1; + if (Mc[mid] <= v) lo = mid; + else hi = mid - 1; + } + const r = lo, row = Mc[r + 1] - Mc[r], w = (SU[p] / W) * row, o = r * (W + 1); + let a = 0, b = W - 1; + while (a < b) { + const mid = (a + b + 1) >> 1; + if (C[o + mid] <= w) a = mid; + else b = mid - 1; + } + TY[p] = r + (v - Mc[r]) / row; + TX[p] = a + (w - C[o + a]) / rho[r * W + a]; + } + return [TX, TY]; +} + +const WORDS = ["LIGHT", "빛", "WAVE"]; +// A word's map takes about 20 ms, so each is made the first time it is needed. +const MAPS = []; +const mapOf = (i) => MAPS[i] || (MAPS[i] = knothe(density(WORDS[i]))); + +// ---- Waves. A plane wave is a travelling sine on the surface; a photon is +// pushed along the wave's direction by the surface slope, a cos(k.u - wt), +// where a is set by f = a |k|, the fold number: past f = 1 neighbouring +// photons cross and a caustic line is born. Deep-water dispersion, +// w = sqrt(g k), lets long waves overtake short ones. +const GRAV = 25; +function wave(dir, lambda, f) { + const k = (2 * PI) / lambda; + return { kx: k * cos(dir), ky: k * sin(dir), w: sqrt(GRAV * k), ph: Math.random() * 2 * PI, a: f / k, ex: cos(dir), ey: sin(dir) }; +} +function gust() { + const d0 = Math.random() * 2 * PI, out = []; + for (let i = 0; i < 4; i++) out.push(wave(d0 + (i - 1.5) * 0.75 + (Math.random() - 0.5) * 0.5, 10 + 14 * Math.random(), 1.1)); + return out; +} + +const CYCLE = 8; + +function startCycle(S, drop) { + S.prev = S.word; + S.word = (S.word + 1) % WORDS.length; + S.ct = 0; + S.n++; + S.gust = gust(); + // Every other word is broken by a drop instead of a gust: a ring of waves + // from one point, with a weaker gust under it. + S.drop = drop || S.n % 2 === 0; + S.dx = 10 + 44 * Math.random(); + S.dy = 16 + 96 * Math.random(); +} + +export function setup(params) { + const res = []; + for (let i = 0; i < 3; i++) res.push(wave(0.6 + i * 2.1, 13 + 5 * i, 0.07)); + params.s = { + t: 0, ct: 0.6, n: 0, word: 0, prev: 0, gust: gust(), drop: false, dx: 32, dy: 64, res, + hue: 0, speed: 2, ripple: 1, focus: 0.06, + key: "", B: new Float64Array(2 * NP), + GX: new Float64Array(GW * GH), GY: new Float64Array(GW * GH), E: new Float64Array((W + 2) * (H + 2)), + AX: new Float64Array(9), AY: new Float64Array(9), + CX: new Float64Array(9 * GW), SX: new Float64Array(9 * GW), CY: new Float64Array(9 * GH), SY: new Float64Array(9 * GH), + GR: new Float32Array(800), LUT: new Uint8Array(3072), + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 1, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.ripple = cl(k[2], 1, 3); + S.focus = cl(k[3], -1, 1); + // Time runs in the seconds of Speed 2. + const step = (d * S.speed) / 2; + S.t += step; + S.ct += step; + if (S.ct >= CYCLE) startCycle(S, false); + if (input.btnPressed && input.btnPressed.some(Boolean) && S.ct > 0.5) startCycle(S, true); +} + +// Wave q's push and its two tables of sines for this frame. +function setWave(S, q, w, g) { + const { CX, SX, CY, SY, AX, AY } = S; + AX[q] = w.a * g * w.ex; + AY[q] = w.a * g * w.ey; + for (let i = 0; i < GW; i++) (CX[q * GW + i] = cos(w.kx * i)), (SX[q * GW + i] = sin(w.kx * i)); + for (let j = 0; j < GH; j++) { + const b = w.ky * j - w.w * S.t + w.ph; + (CY[q * GH + j] = cos(b)), (SY[q * GH + j] = sin(b)); + } +} + +// The drop: a ring packet leaving the point at 20 LEDs a second, spreading +// as it goes and weakening about as 1 / sqrt(r). GR holds its radial push. +function addDrop(S, ct, amp) { + const { GX, GY, GR } = S; + const R = 20 * ct, sg = 3 + 1.6 * ct, kd = (2 * PI) / 9, ad = (2.6 / kd) * amp * exp(-ct / 2.2) * smooth(ct / 0.15); + for (let m = 0; m < 800; m++) { + const r = m * 0.25, u = (r - R) / sg; + GR[m] = (ad * exp(-u * u) * sin(kd * (r - R))) / sqrt(1 + r / 12); + } + for (let j = 0; j < GH; j++) + for (let i = 0; i < GW; i++) { + const ux = i - S.dx, uy = j - S.dy, r = sqrt(ux * ux + uy * uy); + if (r < 0.5) continue; + const g = GR[min(799, floor(r * 4))] / r; + GX[j * GW + i] += g * ux; + GY[j * GW + i] += g * uy; + } +} + +// The push of the slope at each corner: cos(A + B) with A from the column +// and B from the row, so the sines come from two short tables per wave. +function sumWaves(S, nw) { + const { GX, GY, CX, SX, CY, SY, AX, AY } = S; + GX.fill(0); + GY.fill(0); + for (let q = 0; q < nw; q++) { + const ax = AX[q], ay = AY[q], qx = q * GW; + for (let j = 0, o = 0; j < GH; j++, o += GW) { + const cy = CY[q * GH + j], sy = SY[q * GH + j]; + for (let i = 0; i < GW; i++) { + const c = CX[qx + i] * cy - SX[qx + i] * sy; + GX[o + i] += ax * c; + GY[o + i] += ay * c; + } + } + } +} + +// Land every photon and splat it bilinearly into E, which has a border of +// one LED all round so the splat needs no tests. +function splat(S, s) { + const { GX, GY, E, B } = S, EW = W + 2; + E.fill(0); + for (let p = 0; p < NP; p++) { + const c = PC[p], f = PFG[2 * p], g = PFG[2 * p + 1], c2 = c + GW; + const dx = (GX[c] + (GX[c + 1] - GX[c]) * f) * (1 - g) + (GX[c2] + (GX[c2 + 1] - GX[c2]) * f) * g; + const dy = (GY[c] + (GY[c + 1] - GY[c]) * f) * (1 - g) + (GY[c2] + (GY[c2 + 1] - GY[c2]) * f) * g; + const fx = B[2 * p] + s * dx, fy = B[2 * p + 1] + s * dy; + const x0 = floor(fx), y0 = floor(fy); + if (x0 < -1 || x0 >= W || y0 < -1 || y0 >= H) continue; + const ax = fx - x0, ay = fy - y0, o = (y0 + 1) * EW + x0 + 1, by = 1 - ay; + E[o] += (1 - ax) * by; + E[o + 1] += ax * by; + E[o + EW] += (1 - ax) * ay; + E[o + EW + 1] += ax * ay; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const ct = S.ct, amp = S.ripple; + + // The gust rises to its height at 0.9 s and dies away; the water is + // nearly still by 4.5 s. + const tau = 0.9, env = (ct / tau) * exp(1 - ct / tau); + let nw = 0; + if (env > 0.01) for (const w of S.gust) setWave(S, nw++, w, env * amp * (S.drop ? 0.3 : 1)); + for (const w of S.res) setWave(S, nw++, w, amp); + sumWaves(S, nw); + if (S.drop && ct < 6) addDrop(S, ct, amp); + + // The floor at depth s of the shaped depth gets u + s (T(u) - u + slope + // push): s = 1 is in focus. + const s = 1 + 0.5 * S.focus; + // How much of the shaped relief shows through the waves: none while the + // gust stands above half its height, all of it once the water is still. + // The relief changes to the next word at 1 s, while none of it shows. + const calm = 1 - smooth(min(1, env / 0.5)), wi = ct < 1 ? S.prev : S.word; + // Where each photon would land on still water, kept until the focus, the + // calm or the word changes. + const key = s + "," + calm + "," + wi, B = S.B, EW = W + 2; + if (key !== S.key) { + S.key = key; + const [TX, TY] = mapOf(wi), sc = s * calm; + for (let p = 0; p < NP; p++) { + B[2 * p] = SU[p] + sc * (TX[p] - SU[p]) - 0.5; + B[2 * p + 1] = SV[p] + sc * (TY[p] - SV[p]) - 0.5; + } + } + splat(S, s); + + // Tone map 1 - exp(-k E) through a table of 1024 steps of 1/16 photon: + // four photons an LED is even light, and the deep colour gives way to the + // pale one as the light piles up. + const deep = hsv(0.56 + S.hue, 0.85, 1), pale = hsv(0.47 + S.hue, 0.28, 1), LUT = S.LUT, E = S.E; + for (let m = 0; m < 1024; m++) { + const b = 1 - exp(-0.13 * (m / 16)), w = b * b; + for (let ch = 0; ch < 3; ch++) LUT[m * 3 + ch] = 255 * b * (deep[ch] + (pale[ch] - deep[ch]) * w); + } + for (let y = 0; y < H; y++) + for (let x = 0, p = (y + 1) * EW + 1; x < W; x++, p++) { + const m = min(1023, (E[p] * 16) | 0) * 3; + display.setPixel(x, y, LUT[m], LUT[m + 1], LUT[m + 2]); + } +} From 8b47e21c0cde47df6104ff791f6bd988678e8a2b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 15:57:49 +0000 Subject: [PATCH 10/21] =?UTF-8?q?Calculator=20Words=20=EA=B3=BC=20Aharonov?= =?UTF-8?q?-Bohm=20=EB=A5=BC=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- .../pattern-study/patterns/aharonov-bohm.js | 86 ++++++++++--------- .../patterns/calculator-words.js | 2 +- 2 files changed, 46 insertions(+), 42 deletions(-) diff --git a/incoming/pattern-study/patterns/aharonov-bohm.js b/incoming/pattern-study/patterns/aharonov-bohm.js index 5a1939d6d..ffa06ba1f 100644 --- a/incoming/pattern-study/patterns/aharonov-bohm.js +++ b/incoming/pattern-study/patterns/aharonov-bohm.js @@ -96,22 +96,37 @@ function besselRun(nu0, z, M, w, out) { // colours of its sides swapped. The orders' sum is done for an LED and its // mirror across the shadow line at once, since they share r and differ only // in the sign of theta. -const MM = 260; -function field(S, F, lam) { - const a = F - 1, k = (2 * PI) / lam; - const fr = (x) => x - floor(x); - const nuP = fr(-a), nuQ = fr(a); - const wP = weights(nuP, MM), wQ = weights(nuQ, MM); - const JP = S.JP, JQ = S.JQ, fam = S.fam, nn = S.nn, cr = S.cr, ci = S.ci; +const MM = 260, ROWS = 32; + +// A field is worked out in a buffer of its own, ROWS rows a frame, while the +// last finished one stays on the panel; the first appears after 4 frames. +function newField() { + return { amp: new Float32Array(N), ph: new Float32Array(N), lag: new Float32Array(N), lam: 0, flux: 0, row: 0, a: 0, k: 0, nuP: 0, nuQ: 0, wP: null, wQ: null }; +} +function begin(S, G, F, lam) { + const a = F - 1, fr = (x) => x - floor(x); + Object.assign(G, { lam, flux: F, row: 0, a, k: (2 * PI) / lam, nuP: fr(-a), nuQ: fr(a) }); + G.wP = weights(G.nuP, MM); + G.wQ = weights(G.nuQ, MM); + const { fam, nn, cr, ci } = S; for (let m = -MM; m <= MM; m++) { const d = m - a, j = m + MM, sg = m & 1 ? -1 : 1, nu = abs(d); - if (d >= 0) (fam[j] = 0), (nn[j] = round(d - nuP)); - else (fam[j] = 1), (nn[j] = round(-d - nuQ)); + if (d >= 0) (fam[j] = 0), (nn[j] = round(d - G.nuP)); + else (fam[j] = 1), (nn[j] = round(-d - G.nuQ)); cr[j] = sg * cos((PI * nu) / 2); ci[j] = -sg * sin((PI * nu) / 2); } - const { amp, ph, lag } = S; - for (let y = 0; y < H; y++) { +} +function rows(S, G, y0, y1) { + const { JP, JQ, fam, nn, cr, ci } = S, { amp, ph, lag, k, nuP, nuQ, wP, wQ } = G; + const put = (p, re, im, ox) => { + amp[p] = sqrt(re * re + im * im); + ph[p] = atan2(im, re); + // The slip behind the bare wave exp(i k x), in turns, -1/2 to 1/2. + const l = (ph[p] - ox) / (2 * PI); + lag[p] = l - round(l); + }; + for (let y = y0; y < y1; y++) { const X = y + 0.5 - TY; for (let x = HALF; x < W; x++) { const Y = x + 0.5 - TX, r = sqrt(X * X + Y * Y), th = atan2(Y, X), z = k * r; @@ -134,26 +149,15 @@ function field(S, F, lam) { sm = sm * c1 + cm * s1; cm = t; } - const pr = y * W + x, pl = y * W + (W - 1 - x); - const ox = k * X; - for (const [p, re, im] of [ - [pr, Ar - Bi, Ai + Br], - [pl, Ar + Bi, Ai - Br], - ]) { - amp[p] = sqrt(re * re + im * im); - ph[p] = atan2(im, re); - // The slip behind the bare wave exp(i k x), in turns, -1/2 to 1/2. - let l = (ph[p] - ox) / (2 * PI); - lag[p] = l - round(l); - } + put(y * W + x, Ar - Bi, Ai + Br, k * X); + put(y * W + W - 1 - x, Ar + Bi, Ai - Br, k * X); } } } export function setup(params) { params.s = { - hue: 0, speed: 2, lam: NaN, flux: NaN, t: 0, - amp: new Float32Array(N), ph: new Float32Array(N), lag: new Float32Array(N), + hue: 0, speed: 2, t: 0, cur: null, job: null, spare: [newField(), newField()], JP: new Float64Array(MM + 2), JQ: new Float64Array(MM + 2), fam: new Int8Array(2 * MM + 1), nn: new Int32Array(2 * MM + 1), cr: new Float64Array(2 * MM + 1), ci: new Float64Array(2 * MM + 1), c: [0, 0, 0], @@ -165,23 +169,29 @@ export function update(dt, input, params) { const S = params.s, k = input.knobValues || [0, 2, 7, 0.5], d = cl(dt || 0, 0, 0.1); S.hue = k[0]; S.speed = cl(k[1], 0.2, 3); - // The field is worked out again only when the wavelength or the flux has - // moved by a visible step. + // A new field starts only when the wavelength or the flux has moved by a + // visible step; turning the knob again restarts it. const lam = round(cl(k[2], 7, 16) * 20) / 20, flux = round(cl(k[3], 0.5, 1.5) * 200) / 200; - if (lam !== S.lam || flux !== S.flux) { - S.lam = lam; - S.flux = flux; - field(S, flux, lam); + const aim = S.job || S.cur; + if (!aim || aim.lam !== lam || aim.flux !== flux) { + S.job = S.spare.find((G) => G !== S.cur); + begin(S, S.job, flux, lam); + } + if (S.job) { + const G = S.job, y1 = min(H, G.row + ROWS); + rows(S, G, G.row, y1); + G.row = y1; + if (y1 >= H) (S.cur = G), (S.job = null); } S.t += d; } export function draw(display, params) { const S = params.s; - if (!S) return; - const { amp, ph, lag, c } = S; + if (!S || !S.cur) return; + const { amp, ph, lag, lam } = S.cur, c = S.c; // Crests run down at 4 * Speed LEDs a second: the phase turns by k v t. - const wt = ((2 * PI) / S.lam) * 4 * S.speed * S.t, h0 = 0.62 + S.hue; + const wt = ((2 * PI) / lam) * 4 * S.speed * S.t, h0 = 0.62 + S.hue; for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) { const p = y * W + x, A = amp[p]; @@ -192,11 +202,5 @@ export function draw(display, params) { display.setPixel(x, y, (255 * c[0]) | 0, (255 * c[1]) | 0, (255 * c[2]) | 0); } // The tube itself, end on: a small white core. - for (const [x, y] of [ - [31, 41], - [32, 41], - [31, 42], - [32, 42], - ]) - display.setPixel(x, y, 235, 235, 245); + for (let p = 0; p < 4; p++) display.setPixel(31 + (p & 1), 41 + (p >> 1), 235, 235, 245); } diff --git a/incoming/pattern-study/patterns/calculator-words.js b/incoming/pattern-study/patterns/calculator-words.js index 6b00fda75..3f0de589c 100644 --- a/incoming/pattern-study/patterns/calculator-words.js +++ b/incoming/pattern-study/patterns/calculator-words.js @@ -14,7 +14,7 @@ // Knob 1: Hue. The colour of the display, with the operator keys and the body following it round the wheel: at 0 a blue-green vacuum fluorescent display over orange operator keys on a slate body; at 0.54 the red of the first LED calculators. // Knob 2: Speed. How fast the machine works: at 2 a key goes down every 0.22 s and the turn takes 1.3 s, so hELLO is keyed, turned, read and turned back in 7.7 s; at 0.2 it takes ten times as long, at 3 two thirds. // Knob 3: Words. Which riddles: 0 the classics (hELLO, BOSS, LOSE, GLOBE, GOOSE, EGGSHELL); 1 short words (hI, BEE, OIL, BIG, EGG, ShOE, SOIL); 2 long ones made by multiplying two numbers (LEGIBLE, SLEIGh, BOGGLE, OBLIGE, GIGGLE, SIZZLE, hOBBIES); 3 answers under 1, whose decimal point turns up at the top of the word (IGLOO, hELLO, SILO, SOLO, hOBO, BOO). -// Knob 4: Glow. How much light the display spills: at 0 the unlit segments are almost gone and the digits stand sharp on dark glass; at the opening 0.06 the unlit segments show faintly as a ghost 8 in every place; at 1 the ghosts glow at half strength and a haze spreads round every lit digit. +// Knob 4: Glow. How much light the display spills: at 0 the unlit segments are almost gone and the digits stand sharp on dark glass; at the opening 0.06 the unlit segments show faintly as a ghost 8 in every place; at 1 the ghosts glow at half strength, a haze spreads round every lit digit, and the display's light falls over the body and the top rows of keys. // Buttons: any knob's button clears the calculator and starts the next riddle, when the calculator stands the right way up. const W = 64, H = 128; From cc2eb92184ac8026de559b5513877781850f7d61 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 15:57:55 +0000 Subject: [PATCH 11/21] =?UTF-8?q?Letter=20Melt=20=EB=A5=BC=20=EB=8B=A4?= =?UTF-8?q?=EB=93=AC=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- .../pattern-study/patterns/letter-melt.js | 284 +++++++++--------- 1 file changed, 149 insertions(+), 135 deletions(-) diff --git a/incoming/pattern-study/patterns/letter-melt.js b/incoming/pattern-study/patterns/letter-melt.js index 8f1aae201..f7bea43b7 100644 --- a/incoming/pattern-study/patterns/letter-melt.js +++ b/incoming/pattern-study/patterns/letter-melt.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Letter Melt // Family: type -// Idea: A word of liquid stands in an invisible mould at the top of the panel, the mould lets go from the feet of the letters up so the strokes sag, drip and run down the full height into a pool on the floor, and then threads of the pool are drawn up into the mould of the next word, MELT, 물, FLOW, 녹다, DRIP, which fills from the top of each letter down and sets. +// Idea: A word of liquid stands in an invisible mould at the top of the panel, the mould lets go from the feet of the letters up so the strokes sag, drip and run down the full height into a pool on the floor, and then threads drawn up out of the pool fill the mould of the next word, MELT, 물, FLOW, 녹다, DRIP, from the top of each letter down until it sets. // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -11,10 +11,10 @@ // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Word=0..5, Heat=0..1 // -// Knob 1: Hue. Turns the liquid round the wheel. At 0 it is a teal body with a pale aqua rim, at 0.3 violet, at 0.5 rose, at 0.75 gold. -// Knob 2: Speed. At 2 the simulation runs 60 steps a second and a word takes about 8 s to stand, melt, pool and pour back; at 0.2 ten times slower, at 3 half as fast again. -// Knob 3: Word. 0 pours the five words in turn; 1 to 5 hold one of MELT, 물, FLOW, 녹다 and DRIP, and the pool is poured back into that same mould every time. -// Knob 4: Heat. Cold liquid is thick: at 0.06 it sags slowly, hangs in threads and the mould holds it for 1.3 s. Hot liquid is thin and shivers in the mould: at 1 the mould opens after 0.5 s and the letters run down in loose drops and splash. +// Knob 1: Hue. Turns the liquid round the wheel. At 0 it is a teal body with a pale aqua rim, at 0.25 blue violet, at 0.5 rose, at 0.65 gold. +// Knob 2: Speed. At 2 the fluid takes 60 steps a second and a word holds 1.8 s, melts in about 2.4 s and pours back in 2.9 s, so the five words come round in 36 s; at 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Word. 0 pours the five words in turn, MELT, 물, FLOW, 녹다 and DRIP. 1 to 5 hold one of them, and every pour goes back into that same mould. +// Knob 4: Heat. Cold liquid is thick: at 0.06 the mould holds the word 1.8 s, the lower strokes sag for about a second before they drip, and the strokes hang in long threads on the way down. Hot liquid is thin and shivers in the mould: at 1 the mould lets go after 0.5 s and the letters fall apart in loose blobs that splash. const W = 64, H = 128; const { floor, min, max, abs, sqrt, round, PI, cos, sin } = Math; @@ -68,23 +68,20 @@ function boxes(word) { return [...word].map((c, i) => [c, 1.5 + i * 16, 6, 13, 36]); } -// The mould's stroke segments in LEDs. -function segments(word) { - const out = []; - for (const [c, bx, by, bw, bh] of boxes(word)) { - const x0 = bx + STROKE, y0 = by + STROKE, sw = bw - 2 * STROKE, sh = bh - 2 * STROKE; - const g = LATIN[c] || HANGUL[c]; - if (g === "ring") { - const r = sw / 2, cx = x0 + r, ya = y0 + r, yb = y0 + sh - r, pts = []; - for (let k = 0; k <= 12; k++) pts.push(cx + r * cos(PI + (k * PI) / 12), ya + r * sin(PI + (k * PI) / 12)); - for (let k = 0; k <= 12; k++) pts.push(cx + r * cos((k * PI) / 12), yb + r * sin((k * PI) / 12)); - pts.push(pts[0], pts[1]); - for (let i = 0; i + 3 < pts.length; i += 2) out.push(pts[i], pts[i + 1], pts[i + 2], pts[i + 3]); - continue; - } - for (const pl of g) - for (let i = 0; i + 3 < pl.length; i += 2) out.push(x0 + pl[i] * sw, y0 + pl[i + 1] * sh, x0 + pl[i + 2] * sw, y0 + pl[i + 3] * sh); +// The stroke segments of one glyph in its box, in LEDs. +function segments(c, bx, by, bw, bh) { + const out = [], x0 = bx + STROKE, y0 = by + STROKE, sw = bw - 2 * STROKE, sh = bh - 2 * STROKE; + const g = LATIN[c] || HANGUL[c]; + if (g === "ring") { + const r = sw / 2, cx = x0 + r, ya = y0 + r, yb = y0 + sh - r, pts = []; + for (let k = 0; k <= 12; k++) pts.push(cx + r * cos(PI + (k * PI) / 12), ya + r * sin(PI + (k * PI) / 12)); + for (let k = 0; k <= 12; k++) pts.push(cx + r * cos((k * PI) / 12), yb + r * sin((k * PI) / 12)); + pts.push(pts[0], pts[1]); + for (let i = 0; i + 3 < pts.length; i += 2) out.push(pts[i], pts[i + 1], pts[i + 2], pts[i + 3]); + return out; } + for (const pl of g) + for (let i = 0; i + 3 < pl.length; i += 2) out.push(x0 + pl[i] * sw, y0 + pl[i + 1] * sh, x0 + pl[i + 2] * sw, y0 + pl[i + 3] * sh); return out; } @@ -105,33 +102,24 @@ function segDist(px, py, s) { // width. Each spout keeps its sites top row first, so a pour fills each // letter from its top down. function mould(word) { - const s = segments(word), bx = boxes(word), xy = [], spouts = [], lists = []; - for (const [, x, y, w, h] of bx) { - const m = max(1, round(w / 16)); + const xy = [], spouts = [], lists = []; + for (const [c, gx, gy, gw, gh] of boxes(word)) { + const m = max(1, round(gw / 16)), first = lists.length, segs = segments(c, gx, gy, gw, gh); for (let k = 0; k < m; k++) { - spouts.push(x + (w * (k + 0.5)) / m, y + h + 2); + spouts.push(gx + (gw * (k + 0.5)) / m, gy + gh + 2); lists.push([]); } - } - for (let j = 0; ; j++) { - const y = 3 + j * SP * 0.866; - if (y > 52) break; - for (let x = 0.5 + (j % 2) * SP * 0.5; x < W; x += SP) { - if (segDist(x, y, s) >= STROKE - 0.3) continue; - // The nearest spout of the glyph whose box holds the site. - let best = 0, bd = 1e9, k = 0; - for (const [, gx, , gw] of bx) { - const m = max(1, round(gw / 16)); - for (let e = 0; e < m; e++, k++) { - const d = abs(spouts[2 * k] - x) + (x >= gx - 1 && x <= gx + gw + 1 ? 0 : 100); - if (d < bd) { - bd = d; - best = k; - } - } + for (let j = 0; ; j++) { + const y = 3 + j * SP * 0.866; + if (y > gy + gh) break; + if (y < gy) continue; + for (let x = 0.5 + (j % 2) * SP * 0.5; x < gx + gw; x += SP) { + if (x < gx || segDist(x, y, segs) >= STROKE - 0.3) continue; + // The nearest spout of the glyph. + const k = first + min(m - 1, floor(((x - gx) / gw) * m)); + lists[k].push(xy.length / 2); + xy.push(x, y); } - lists[best].push(xy.length / 2); - xy.push(x, y); } } const of = new Int32Array(xy.length / 2); @@ -165,7 +153,7 @@ export function setup(params) { pt: 0.5, acc: 0.5, hue: 0.5, x: new Float64Array(N), y: new Float64Array(N), px: new Float64Array(N), py: new Float64Array(N), vx: new Float64Array(N), vy: new Float64Array(N), st: new Uint8Array(N), site: new Int32Array(N), - rel: new Float64Array(N), rho: new Float64Array(N), rhn: new Float64Array(N), + rel: new Float64Array(N), kap: new Float64Array(N), tx: new Float64Array(N), ty: new Float64Array(N), rho: new Float64Array(N), rhn: new Float64Array(N), pa: new Int32Array(MP), pb: new Int32Array(MP), pn: 0, qx: new Float64Array(MP), qy: new Float64Array(MP), qq: new Float64Array(MP), cnt: new Int32Array(GX * GY + 1), fillc: new Int32Array(GX * GY), loose: new Int32Array(GX * GY), hd: new Uint8Array(N), idx: new Int32Array(N), cell: new Int32Array(N), F: new Float32Array(W * H), V: new Float32Array(W * H), pal: new Float32Array(64 * 3), rim: new Float32Array(3), @@ -174,22 +162,31 @@ export function setup(params) { return params.s; } +// Particle i takes site j of mould M in a given state, on a spring. +function fix(S, i, M, j, state) { + S.st[i] = state; + S.site[i] = j; + S.kap[i] = 0.25; + S.tx[i] = M.xy[2 * j]; + S.ty[i] = M.xy[2 * j + 1]; +} + // Put word w in its mould and the rest of the particles in a pool on the // floor. function place(S, w) { - const st = S.MOULDS[w].xy, n = S.MOULDS[w].n; + const M = S.MOULDS[w], per = floor((W - 1) / SP); S.word = w; for (let i = 0; i < S.N; i++) { - if (i < n) { - S.x[i] = st[2 * i]; - S.y[i] = st[2 * i + 1]; - S.st[i] = 1; - S.site[i] = i; + if (i < M.n) { + fix(S, i, M, i, 1); + S.x[i] = S.tx[i]; + S.y[i] = S.ty[i]; } else { - const k = i - n, per = floor((W - 1) / SP), j = floor(k / per); + const k = i - M.n, j = floor(k / per); S.x[i] = 0.7 + (k % per) * SP + (j % 2) * SP * 0.5; S.y[i] = H - 0.7 - j * SP * 0.866; S.st[i] = 0; + S.kap[i] = 0; } S.vx[i] = S.vy[i] = 0; } @@ -197,26 +194,9 @@ function place(S, w) { S.pt = 0; } -function start(S, w) { - // Two steps with a particle in every state, so each branch of a step has - // run before the first frame and the compiled step does not fall back - // the first time the mould lets go or a pour begins. - place(S, w); - S.target = (w + 1) % WORDS.length; - S.phase = 1; - for (let i = 0; i < 9; i++) { - S.st[i] = 1 + (i % 3); - S.rel[i] = i % 2; - } - step(S, 0.3, 0.1); - step(S, 0.3, 0.1); - S.target = -1; - place(S, w); -} - // Counting sort of the particles into cells one interaction radius wide, // then every pair closer than the radius, each once, with its unit vector -// and 1 - r/h, adding up both densities on the way. +// and 1 - r/h, adding up both densities on the way. Returns the count. function neighbours(S) { const { N, x, y, cnt, idx, cell, pa, pb, qx, qy, qq, rho, rhn, st, hd } = S; cnt.fill(0); @@ -269,39 +249,53 @@ function neighbours(S) { rho[i] += ri; rhn[i] += ni; } - S.pn = n; + return n; } -// One step of the double density relaxation scheme: gravity and steering, -// viscous impulses, a free step, the two densities and their pressure -// displacements, the mould's springs, the walls, and velocity from the -// change in position. +// One step of the double density relaxation scheme: gravity, the pour's +// steering, viscous impulses, a free step, the two densities and their +// pressure displacements, the mould's springs, the walls, and velocity from +// the change in position. Each part is a loop of its own with as few rare +// branches as it can have. function step(S, visc, jit) { - const { N, x, y, px, py, vx, vy, st, site, pa, pb, qx, qy, qq, rho, rhn } = S; - const sites = S.MOULDS[S.word].xy, M = S.MOULDS[S.target >= 0 ? S.target : S.word], target = M.xy; + const { N, st, vy } = S; + for (let i = 0; i < N; i++) vy[i] += st[i] === 0 ? GRAV : 0; + if (S.phase === 2) steer(S); + viscosity(S, visc); + advance(S); + S.pn = neighbours(S); + relax(S); + if (S.phase === 1) melt(S); + springs(S, jit); + if (S.phase === 2) arrive(S); +} + +// Drawn up: a thread rises at RISE LEDs a step to the spout under the +// glyph, and from there each particle heads for its own site. +function steer(S) { + const { N, st, x, y, vx, vy, site } = S, M = S.MOULDS[S.target]; for (let i = 0; i < N; i++) { - if (st[i] === 0) vy[i] += GRAV; - else if (st[i] === 2) { - // Drawn up: a thread rises at RISE LEDs a step to the spout under the - // glyph, and from there each particle heads for its own site. - const k = M.of[site[i]], spx = M.spouts[2 * k], spy = M.spouts[2 * k + 1]; - let dvx, dvy; - if (y[i] > spy) { - dvx = cl((spx - x[i]) * 0.15, -0.8, 0.8); - dvy = -RISE; - } else { - const dx = target[2 * site[i]] - x[i], dy = target[2 * site[i] + 1] - y[i], d = sqrt(dx * dx + dy * dy) + 1e-6; - const sc = min(RISE, d * 0.25) / d; - dvx = dx * sc; - dvy = dy * sc; - } - vx[i] += (dvx - vx[i]) * 0.35; - vy[i] += (dvy - vy[i]) * 0.35; + if (st[i] !== 2) continue; + const k = M.of[site[i]], spx = M.spouts[2 * k], spy = M.spouts[2 * k + 1]; + let dvx, dvy; + if (y[i] > spy) { + dvx = cl((spx - x[i]) * 0.15, -0.8, 0.8); + dvy = -RISE; + } else { + const dx = M.xy[2 * site[i]] - x[i], dy = M.xy[2 * site[i] + 1] - y[i], d = sqrt(dx * dx + dy * dy) + 1e-6; + const sc = min(RISE, d * 0.25) / d; + dvx = dx * sc; + dvy = dy * sc; } + vx[i] += (dvx - vx[i]) * 0.35; + vy[i] += (dvy - vy[i]) * 0.35; } - // Viscosity over last step's pairs and their unit vectors. - const n0 = S.pn; - for (let p = 0; p < n0; p++) { +} + +// Viscous impulses between approaching neighbours, over last step's pairs. +function viscosity(S, visc) { + const { pa, pb, qx, qy, qq, vx, vy } = S, n = S.pn; + for (let p = 0; p < n; p++) { const i = pa[p], j = pb[p], ux = qx[p], uy = qy[p]; const u = (vx[i] - vx[j]) * ux + (vy[i] - vy[j]) * uy; if (u <= 0) continue; @@ -311,23 +305,29 @@ function step(S, visc, jit) { vx[j] += ix; vy[j] += iy; } +} + +function advance(S) { + const { N, x, y, px, py, vx, vy } = S; for (let i = 0; i < N; i++) { - const sp = vx[i] * vx[i] + vy[i] * vy[i]; - if (sp > 6.25) { - const f = 2.5 / sqrt(sp); - vx[i] *= f; - vy[i] *= f; - } + const f = min(1, 2.5 / sqrt(vx[i] * vx[i] + vy[i] * vy[i] + 1e-12)); + vx[i] *= f; + vy[i] *= f; px[i] = x[i]; py[i] = y[i]; x[i] += vx[i]; y[i] += vy[i]; } - neighbours(S); - const n = S.pn, rho0 = S.rho0; +} + +// Double density relaxation: a pressure from each density, and every pair +// pushed apart (or pulled together, near the surface) along its line. +function relax(S) { + const { N, x, y, pa, pb, qx, qy, qq, rho, rhn, hd } = S, n = S.pn, rho0 = S.rho0; for (let i = 0; i < N; i++) { - rho[i] = S.hd[i] ? 0 : KS * (rho[i] - rho0); - rhn[i] = S.hd[i] ? 0 : KN * rhn[i]; + const free = 1 - hd[i]; + rho[i] = free * KS * (rho[i] - rho0); + rhn[i] = free * KN * rhn[i]; } for (let p = 0; p < n; p++) { const i = pa[p], j = pb[p], q = qq[p]; @@ -338,35 +338,47 @@ function step(S, visc, jit) { x[j] += dx; y[j] += dy; } +} + +// The mould lets go: each held particle's spring weakens to nothing by its +// release time, so the lower strokes sag before they drip. +function melt(S) { + const { N, st, kap, rel } = S, t = S.pt; for (let i = 0; i < N; i++) { - if (st[i] === 1) { - // The mould: a spring to the site, weakening while the mould lets go. - const k = S.phase === 1 ? 0.25 * cl((S.rel[i] - S.pt) / 0.9, 0, 1) ** 2 : 0.25; - if (k <= 0) st[i] = 0; - else { - x[i] += (sites[2 * site[i]] - x[i]) * k + (Math.random() - 0.5) * jit; - y[i] += (sites[2 * site[i] + 1] - y[i]) * k + (Math.random() - 0.5) * jit; - } - } else if (st[i] === 2) { - const sx = target[2 * site[i]], sy = target[2 * site[i] + 1], dx = sx - x[i], dy = sy - y[i]; - if (dx * dx + dy * dy < 0.8) { - st[i] = 3; - S.arrived++; - } - } else if (st[i] === 3) { - // Arrived: the new mould holds it. - x[i] += (target[2 * site[i]] - x[i]) * 0.25 + (Math.random() - 0.5) * jit; - y[i] += (target[2 * site[i] + 1] - y[i]) * 0.25 + (Math.random() - 0.5) * jit; - } - if (x[i] < 0.5) x[i] = 0.5; - else if (x[i] > W - 0.5) x[i] = W - 0.5; - if (y[i] < 0.5) y[i] = 0.5; - else if (y[i] > H - 0.5) y[i] = H - 0.5; + if (st[i] !== 1) continue; + const u = cl((rel[i] - t) / 0.9, 0, 1); + kap[i] = 0.25 * u * u; + if (u <= 0) st[i] = 0; + } +} + +// The springs, a shiver as hot as the liquid, the walls, and velocity. +function springs(S, jit) { + const { N, x, y, px, py, vx, vy, kap, tx, ty } = S, j4 = jit * 4; + for (let i = 0; i < N; i++) { + const k = kap[i]; + const xi = x[i] + (tx[i] - x[i]) * k + (Math.random() - 0.5) * j4 * k; + const yi = y[i] + (ty[i] - y[i]) * k + (Math.random() - 0.5) * j4 * k; + x[i] = min(W - 0.5, max(0.5, xi)); + y[i] = min(H - 0.5, max(0.5, yi)); vx[i] = x[i] - px[i]; vy[i] = y[i] - py[i]; } } +// A drawn particle that reaches its site is held by the new mould. +function arrive(S) { + const { N, st, x, y, site } = S, M = S.MOULDS[S.target]; + for (let i = 0; i < N; i++) { + if (st[i] !== 2) continue; + const dx = M.xy[2 * site[i]] - x[i], dy = M.xy[2 * site[i] + 1] - y[i]; + if (dx * dx + dy * dy < 0.8) { + fix(S, i, M, site[i], 3); + S.arrived++; + } + } +} + // The free particle nearest the top of the pool under a spout. function pick(S, spx) { const { N, st, x, y } = S; @@ -387,7 +399,7 @@ function pick(S, spx) { function tick(S, sel, heat) { const { N, st, y } = S; S.pt += 1 / 60; - if (S.phase === 0 && S.pt > 1.3 - 0.8 * heat) { + if (S.phase === 0 && S.pt > 1.9 - 1.4 * heat) { // The mould lets go from the feet of the letters up. S.phase = 1; S.pt = 0; @@ -417,13 +429,14 @@ function tick(S, sel, heat) { const i = pick(S, M.spouts[2 * k]); if (i < 0) break; st[i] = 2; + S.kap[i] = 0; S.site[i] = l[S.nk[k]++]; } if (S.nk[k] < l.length) done = false; } if (done && (S.arrived >= M.n || S.pt > POUR + 4)) { // Set: the new word stands in its mould. - for (let i = 0; i < N; i++) if (st[i] >= 2) st[i] = 1; + for (let i = 0; i < N; i++) if (st[i] >= 2) fix(S, i, M, S.site[i], 1); S.word = S.target; S.target = -1; S.phase = 0; @@ -440,10 +453,10 @@ export function update(dt, input, params) { if (!S.started) { S.started = true; S.acc = 0; - start(S, sel ? sel - 1 : 0); + place(S, sel ? sel - 1 : 0); S.pt = 0.4; } - const visc = 0.6 * (1 - heat) ** 2 + 0.01, jit = 0.03 + 0.35 * heat; + const visc = 0.5 * (1 - heat) ** 2 + 0.08, jit = 0.02 + 0.12 * heat; S.acc += cl(dt || 0, 0, 0.1) * rate * 60; while (S.acc >= 1) { S.acc -= 1; @@ -496,8 +509,9 @@ export function draw(display, params) { const lit = (-gx * LX - gy * LY) / gl, edge = cl(1 - (f - 0.9) / 1.3, 0, 1); const spec = edge * cl(lit, 0, 1) ** 3 * cl(gl, 0, 1); const deep = cl((f - 0.9) / 2.2, 0, 1), k = floor(deep * 63) * 3; - const foam = cl((V[o] / f - 0.35) * 0.8, 0, 0.6); + const foam = cl((V[o] / f - 0.6) * 0.6, 0, 0.5); const sh = 0.75 + 0.25 * cl(lit, -1, 1) * edge; + // The lit side of the surface and fast liquid take the pale rim colour. const m = max(spec, foam); const r = (pal[k] * sh * (1 - m) + rim[0] * m) * a; const g = (pal[k + 1] * sh * (1 - m) + rim[1] * m) * a; From c1e93de4eba7514c8ec79d7f833d897ae946fc12 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 16:01:22 +0000 Subject: [PATCH 12/21] =?UTF-8?q?Caustic=20Letters=20=EA=B3=BC=20Langford?= =?UTF-8?q?=20Pairs=20=EB=A5=BC=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- .../pattern-study/patterns/langford-pairs.js | 289 ++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 incoming/pattern-study/patterns/langford-pairs.js diff --git a/incoming/pattern-study/patterns/langford-pairs.js b/incoming/pattern-study/patterns/langford-pairs.js new file mode 100644 index 000000000..3d43bae61 --- /dev/null +++ b/incoming/pattern-study/patterns/langford-pairs.js @@ -0,0 +1,289 @@ +// ===== Patternflow pattern ===== +// Title: Langford Pairs +// Family: number +// Idea: A backtracking search stacks two blocks of each of n colours into a tower the height of the panel, placing the pairs from the largest down so that the two blocks of colour k have k blocks between them, with an arc beside the tower joining each pair, and every arrangement it finds slides aside onto a shelf of finished towers while the search goes on. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Langford's problem (C. Dudley Langford, Mathematical Gazette, 1958), solutions only for n ≡ 0 or 3 mod 4 (Roy O. Davies, 1959), Skolem sequences (Thoralf Skolem, 1957), pattern round 69 +// Round: 69 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Pairs=7..12, Rule=0..1 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 pairs 1, 2 and 3 are red, blue and yellow, the blocks Langford watched his son stack, and each larger pair is 0.58 of a turn on from the one before. +// Knob 2: Speed. 40 steps a second at 2, where a step places a pair or takes one back; 4 at 0.2 and 60 at 3. While nothing turns up the search doubles its pace every 1.5 s, up to 30,000 steps a second, and falls back at each arrangement it finds. +// Knob 3: Pairs. n from 7 to 12, so the tower is 14 to 24 blocks of 128 / 2n rows. Counted once with its reversal, 7 has 26 arrangements, 8 has 150, 11 has 17,792 and 12 has 108,144; 9 and 10 have none, and their searches run out and fail. When a search has tried everything, n goes up by one, and 12 goes back to 7. +// Knob 4: Rule. Below 0.5 Langford's rule, k blocks between the two of colour k. From 0.5 Skolem's, the two of colour k stand k places apart, so the two 1s touch; that works for n = 8 (252), 9 (1,328) and 12 (227,968), and fails for 7, 10 and 11. +// Buttons: any knob's button gives up on this n and moves on to the next. + +const W = 64, H = 128; +const { floor, round, min, max, sqrt, sin, cos, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- Layout. The search tower stands at the right, 10 LEDs wide, with the +// arcs to its left and a progress line in the last column. Finished towers +// are 5 LEDs wide, 7 apart, the newest at x 29 and the oldest falling off +// the left edge. +const TX = 52, TW = 10, PBX = 63, SW = 5, SS = 7, S0 = 29, NMIN = 7, NMAX = 12; +const top = (n, i) => round((i * H) / (2 * n)); + +// ---- The search. Pairs go in from n down to 1; pair k tries each place i +// whose partner i + gap is free, left to right. Langford's gap is k + 1 (k +// blocks between), Skolem's is k. A reversed arrangement is the same one, so +// pair n only tries the top half of its places; when it sits exactly in the +// middle an arrangement and its reversal both turn up, and only the one that +// reads smaller from the top is kept. +// Enumerated this way: Langford 3:1, 4:1, 7:26, 8:150, 11:17792, 12:108144, +// and none for 5, 6, 9, 10; Skolem 4:3, 5:5, 8:252, 9:1328, 12:227968. +const gap = (S, k) => k + 1 - S.rule; +const hiOf = (S, k) => { + const last = 2 * S.n - 1 - gap(S, k); + return k === S.n ? floor(last / 2) : last; +}; + +function fresh(S, n, rule) { + S.n = n; + S.rule = rule; + S.slot = new Int8Array(2 * n); + S.pos = new Int16Array(n + 2).fill(-1); + S.cur = new Int16Array(n + 2); + S.glow = new Float32Array(n + 2); + S.ghost = new Float32Array(n + 2); + S.gpos = new Int16Array(n + 2); + S.k = n; + S.found = 0; + S.stuck = 0; + S.acc = 0; + S.hold = 0; + S.end = -1; +} + +function place(S, k, i) { + S.slot[i] = S.slot[i + gap(S, k)] = k; + S.pos[k] = i; + S.glow[k] = 1; +} +function unplace(S, k) { + const i = S.pos[k]; + S.slot[i] = S.slot[i + gap(S, k)] = 0; + S.pos[k] = -1; + S.ghost[k] = 1; + S.gpos[k] = i; +} +function reversedFirst(S) { + const n = S.n, L = 2 * n, i = S.pos[n]; + if (i !== L - 1 - gap(S, n) - i) return false; + for (let a = 0; a < L; a++) if (S.slot[a] !== S.slot[L - 1 - a]) return S.slot[a] > S.slot[L - 1 - a]; + return false; +} + +// One step: place the next pair, or take one back. Returns 0 placed, +// 1 taken back, 2 found, 3 found the reversal of one already kept, 4 done. +function step(S) { + if (S.k === 0) { + unplace(S, 1); + S.k = 1; + return 1; + } + const k = S.k, g = gap(S, k), hi = hiOf(S, k), sl = S.slot; + for (let i = S.cur[k]; i <= hi; i++) { + if (sl[i] || sl[i + g]) continue; + place(S, k, i); + S.cur[k] = i + 1; + if (k > 1) { + S.k = k - 1; + S.cur[k - 1] = 0; + return 0; + } + S.k = 0; + return reversedFirst(S) ? 3 : 2; + } + if (k === S.n) return 4; + unplace(S, k + 1); + S.k = k + 1; + return 1; +} + +// How far through the tree the search is: pair n's place out of its range, +// then pair n - 1's inside that, and so on four levels down. +function progress(S) { + if (S.end >= 0) return 1; + let p = 0, w = 1; + for (let k = S.n; k > S.n - 4 && k >= 1; k--) { + const span = hiOf(S, k) + 1, at = S.pos[k] >= 0 ? S.pos[k] : min(span, S.cur[k]); + p += (w * at) / span; + w /= span; + if (S.pos[k] < 0) break; + } + return cl(p, 0, 1); +} + +export function setup(params) { + params.s = { t: 0, hue: 0, speed: 2, kn: -1, krule: -1, shelf: [], flash: 0, rgb: new Float32Array(W * H * 3) }; +} + +function advance(S) { + fresh(S, S.n >= NMAX ? NMIN : S.n + 1, S.rule); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 7, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0]; + S.speed = cl(kv[1], 0.2, 3); + const n = cl(round(kv[2]), NMIN, NMAX), rule = kv[3] >= 0.5 ? 1 : 0; + if (n !== S.kn || rule !== S.krule) { + S.kn = n; + S.krule = rule; + fresh(S, n, rule); + } + // Time in the seconds of Speed 2. + const nom = (d * S.speed) / 2; + S.t += d; + const fade = exp(-d / 0.25), gfade = exp(-d / 0.12); + for (let k = 1; k <= S.n; k++) (S.glow[k] *= fade), (S.ghost[k] *= gfade); + S.flash *= exp(-d / 0.3); + // The shelf eases toward its places; a tower that has left the panel goes. + S.shelf.forEach((tw, j) => (tw.x += (S0 - SS * j - tw.x) * (1 - exp(-d * 6)))); + while (S.shelf.length && S.shelf[S.shelf.length - 1].x < -SW - 1) S.shelf.pop(); + + if (input.btnPressed && input.btnPressed.some(Boolean)) return advance(S); + if (S.end >= 0) { + // The search has run out: 1.4 s to show it, then the next n. + S.end += nom; + if (S.end > 1.4) advance(S); + return; + } + if (S.hold > 0) { + S.hold -= nom; + return; + } + S.stuck += nom; + const rate = min(30000, 20 * S.speed * 2 ** (max(0, S.stuck - 2) / 1.5)); + S.acc += d * rate; + let steps = floor(S.acc); + if (steps > 1000) (steps = 1000), (S.acc = 0); + else S.acc -= steps; + while (steps-- > 0) { + const r = step(S); + if (r === 2) { + S.found++; + S.shelf.unshift({ seq: Int8Array.from(S.slot), n: S.n, x: TX }); + if (S.shelf.length > 8) S.shelf.length = 8; + S.hold = 0.35; + S.stuck = 0; + S.flash = 1; + S.acc = 0; + break; + } + if (r === 4) { + S.end = 0; + S.acc = 0; + break; + } + } +} + +// A block from row y0 to y1 - 1 between columns x0 and x1 - 1, its last row +// left dark as the seam to the next; the top row catches the light. +function block(rgb, x0, x1, y0, y1, c, v) { + const last = y1 - 1 - (y1 - y0 > 3 ? 1 : 0); + for (let y = y0; y < last; y++) { + const sh = v * (y === y0 ? 1 : y === last - 1 ? 0.58 : 0.8); + for (let x = max(0, x0); x < min(W, x1); x++) { + const o = (y * W + x) * 3; + rgb[o] += c[0] * sh; + rgb[o + 1] += c[1] * sh; + rgb[o + 2] += c[2] * sh; + } + } +} + +function dot(rgb, x, y, c, a) { + const x0 = floor(x), y0 = floor(y), fx = x - x0, fy = y - y0; + for (let j = 0; j < 2; j++) + for (let i = 0; i < 2; i++) { + const px = x0 + i, py = y0 + j; + if (px < 0 || px >= W || py < 0 || py >= H) continue; + const w = a * (i ? fx : 1 - fx) * (j ? fy : 1 - fy), o = (py * W + px) * 3; + rgb[o] += c[0] * w; + rgb[o + 1] += c[1] * w; + rgb[o + 2] += c[2] * w; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.slot) return; + const rgb = S.rgb; + rgb.fill(0); + // Pair k's colour steps 0.58 of a turn, so 1, 2 and 3 open red, blue and + // yellow and neighbours never share a hue. + const pal = []; + for (let k = 0; k <= NMAX; k++) pal.push(hsv(S.hue + 0.58 * (k - 1), 0.78, 1)); + + // The shelf. A tower leaving the search narrows from 10 LEDs to 5 as it goes. + for (const tw of S.shelf) { + const u = cl((tw.x - S0) / (TX - S0), 0, 1), x0 = round(tw.x), w = round(SW + (TW - SW) * u); + for (let i = 0; i < 2 * tw.n; i++) block(rgb, x0, x0 + w, top(tw.n, i), top(tw.n, i + 1), pal[tw.seq[i]], 0.72 + 0.23 * u); + } + + // The search tower: placed blocks, the empty places as faint frames, and a + // pair just taken back fading where it stood. + const n = S.n, L = 2 * n, fl = S.flash; + const failing = S.end >= 0 && S.found === 0, blink = failing && (S.end * 3) % 1 < 0.5; + const frame = failing ? [blink ? 0.75 : 0.25, 0.12, 0.08] : S.end >= 0 ? [0.4, 0.45, 0.5] : [0.11, 0.13, 0.17]; + for (let i = 0; i < L; i++) { + const y0 = top(n, i), y1 = top(n, i + 1), k = S.slot[i]; + if (k) { + const g = S.glow[k] * 0.45 + fl * 0.3, c = pal[k]; + block(rgb, TX, TX + TW, y0, y1, [c[0] + (1 - c[0]) * g, c[1] + (1 - c[1]) * g, c[2] + (1 - c[2]) * g], 1); + continue; + } + for (let y = y0; y < y1 - 1; y++) + for (let x = TX; x < TX + TW; x++) { + if (y !== y0 && y !== y1 - 2 && x !== TX && x !== TX + TW - 1) continue; + const o = (y * W + x) * 3; + rgb[o] += frame[0], rgb[o + 1] += frame[1], rgb[o + 2] += frame[2]; + } + } + for (let k = 1; k <= n; k++) { + const a = S.ghost[k]; + if (a < 0.03 || S.pos[k] >= 0) continue; + for (const i of [S.gpos[k], S.gpos[k] + gap(S, k)]) if (!S.slot[i]) block(rgb, TX + 2, TX + TW - 2, top(n, i), top(n, i + 1), pal[k], 0.45 * a); + } + + // Arcs: each placed pair joined by half an ellipse bulging left from the + // centres of its two blocks, wider for larger k. + for (let k = 1; k <= n; k++) { + const i = S.pos[k]; + if (i < 0) continue; + const j = i + gap(S, k), ya = (top(n, i) + top(n, i + 1) - 1) / 2, yb = (top(n, j) + top(n, j + 1) - 1) / 2; + const yc = (ya + yb) / 2, ry = (yb - ya) / 2, rx = 2.5 + (11 * (k - 1)) / (n - 1); + const len = PI * sqrt((rx * rx + ry * ry) / 2), m = Math.ceil(len * 2.5), c = pal[k]; + const a = 0.5 * (0.75 + S.glow[k] * 0.5 + fl * 0.4); + for (let s = 0; s <= m; s++) { + const th = -PI / 2 + (PI * s) / m; + dot(rgb, TX - 1.5 - rx * cos(th), yc + ry * sin(th), c, a); + } + } + + // The progress line: how much of the tree lies behind the search. + const py = progress(S) * H; + for (let y = 0; y < H; y++) { + const v = y < py ? (failing ? 0.5 : 0.32) : 0.06, o = (y * W + PBX) * 3; + rgb[o] += v * (failing ? 1 : 0.8); + rgb[o + 1] += v * (failing ? 0.25 : 0.85); + rgb[o + 2] += v * (failing ? 0.15 : 1); + } + + for (let p = 0; p < W * H; p++) + display.setPixel(p % W, floor(p / W), (255 * min(1, rgb[p * 3])) | 0, (255 * min(1, rgb[p * 3 + 1])) | 0, (255 * min(1, rgb[p * 3 + 2])) | 0); +} From 0f1f0e4d335817edaaca9a4f25b2f3a7c52457bc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 16:03:43 +0000 Subject: [PATCH 13/21] =?UTF-8?q?Caustic=20Letters=20=EA=B3=BC=20Langford?= =?UTF-8?q?=20Pairs=20=EB=A5=BC=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- incoming/pattern-study/patterns/caustic-letters.js | 6 +++--- incoming/pattern-study/patterns/langford-pairs.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/incoming/pattern-study/patterns/caustic-letters.js b/incoming/pattern-study/patterns/caustic-letters.js index 6e18913fd..a892ac90a 100644 --- a/incoming/pattern-study/patterns/caustic-letters.js +++ b/incoming/pattern-study/patterns/caustic-letters.js @@ -5,7 +5,7 @@ // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: goal-based caustics (Marios Papas, Wojciech Jarosz, Wenzel Jakob, Szymon Rusinkiewicz, Wojciech Matusik and Tim Weyrich, Goal-Based Caustics, 2011), high-contrast computational caustic design (Yuliy Schwartzburg, Romain Testuz, Andrea Tagliasacchi and Mark Pauly, 2014); where the papers solve for an optimal-transport map, this pattern uses the Knothe-Rosenblatt rearrangement in its place (Murray Rosenblatt, 1952; Herbert Knothe, 1957); pattern round 69 +// Lineage: goal-based caustics (Marios Papas, Wojciech Jarosz, Wenzel Jakob, Szymon Rusinkiewicz, Wojciech Matusik and Tim Weyrich, Goal-Based Caustics, 2011), high-contrast computational caustic design (Yuliy Schwartzburg, Romain Testuz, Andrea Tagliasacchi and Mark Pauly, 2014); the Knothe-Rosenblatt rearrangement (Murray Rosenblatt, 1952; Herbert Knothe, 1957) stands in here for the optimal-transport map the 2014 paper solves for; pattern round 69 // Round: 69 // =============================== // @matrix 64x128 @@ -129,8 +129,8 @@ for (let j = 0; j < PY; j++) // matched to the same share of rho's row sums. Then, inside the row it lands // in, its share across is matched to the same share of that row. Both are // inverse cumulative sums, read linearly inside an LED. T moves light only -// down and then across, so it is not the cheapest map the papers solve for, -// but it carries exactly rho. +// down and then across, so it is not the cheapest map, the one the 2014 +// paper solves for by optimal transport, but it carries exactly rho. function knothe(rho) { const Mc = new Float64Array(H + 1), C = new Float64Array(H * (W + 1)); for (let r = 0; r < H; r++) { diff --git a/incoming/pattern-study/patterns/langford-pairs.js b/incoming/pattern-study/patterns/langford-pairs.js index 3d43bae61..9b6f8fac5 100644 --- a/incoming/pattern-study/patterns/langford-pairs.js +++ b/incoming/pattern-study/patterns/langford-pairs.js @@ -12,7 +12,7 @@ // @knobs Hue=0..1, Speed=0.2..3, Pairs=7..12, Rule=0..1 // // Knob 1: Hue. Turns every colour round the wheel together. At 0 pairs 1, 2 and 3 are red, blue and yellow, the blocks Langford watched his son stack, and each larger pair is 0.58 of a turn on from the one before. -// Knob 2: Speed. 40 steps a second at 2, where a step places a pair or takes one back; 4 at 0.2 and 60 at 3. While nothing turns up the search doubles its pace every 1.5 s, up to 30,000 steps a second, and falls back at each arrangement it finds. +// Knob 2: Speed. 40 steps a second at 2, where a step places a pair or takes one back; 4 at 0.2 and 60 at 3. After 2 s with nothing found the search doubles its pace every 1.5 s, up to 30,000 steps a second, so 9 gets through its 19,781 placements and fails about 15 s in; at each arrangement it finds it falls back to the knob's pace. // Knob 3: Pairs. n from 7 to 12, so the tower is 14 to 24 blocks of 128 / 2n rows. Counted once with its reversal, 7 has 26 arrangements, 8 has 150, 11 has 17,792 and 12 has 108,144; 9 and 10 have none, and their searches run out and fail. When a search has tried everything, n goes up by one, and 12 goes back to 7. // Knob 4: Rule. Below 0.5 Langford's rule, k blocks between the two of colour k. From 0.5 Skolem's, the two of colour k stand k places apart, so the two 1s touch; that works for n = 8 (252), 9 (1,328) and 12 (227,968), and fails for 7, 10 and 11. // Buttons: any knob's button gives up on this n and moves on to the next. @@ -35,7 +35,7 @@ const TX = 52, TW = 10, PBX = 63, SW = 5, SS = 7, S0 = 29, NMIN = 7, NMAX = 12; const top = (n, i) => round((i * H) / (2 * n)); // ---- The search. Pairs go in from n down to 1; pair k tries each place i -// whose partner i + gap is free, left to right. Langford's gap is k + 1 (k +// whose partner i + gap is free, from the top down. Langford's gap is k + 1 (k // blocks between), Skolem's is k. A reversed arrangement is the same one, so // pair n only tries the top half of its places; when it sits exactly in the // middle an arrangement and its reversal both turn up, and only the one that From 380e3a5721a7d614168546ced95c58324a8219a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 16:06:52 +0000 Subject: [PATCH 14/21] =?UTF-8?q?Nine-fold=20Seal=20=EA=B3=BC=20Whitney-Gr?= =?UTF-8?q?austein=20=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- .../patterns/whitney-graustein.js | 370 ++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 incoming/pattern-study/patterns/whitney-graustein.js diff --git a/incoming/pattern-study/patterns/whitney-graustein.js b/incoming/pattern-study/patterns/whitney-graustein.js new file mode 100644 index 000000000..c4addc941 --- /dev/null +++ b/incoming/pattern-study/patterns/whitney-graustein.js @@ -0,0 +1,370 @@ +// ===== Patternflow pattern ===== +// Title: Whitney-Graustein +// Family: topology +// Idea: A closed curve drawn as a bright line coloured by the direction of its tangent keeps reshaping without ever stopping, so it never leaves its class of turning number, the regions it encloses shaded by their winding number and a dial counting how often the tangent turns, and a change of class has to pass through a cusp: a small loop pulled tight until it vanishes, or a kink born from one. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Whitney-Graustein theorem (Hassler Whitney, On regular closed curves in the plane, 1937, crediting William Caspar Graustein), pattern round 69 +// Round: 69 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Turns=-2..3, Wobble=0..1 +// +// Knob 1: Hue. Turns the colour wheel the tangent is painted with, and the region shades with it. At 0 a tangent pointing right is azure, regions of winding number 1 and 2 are violet and lilac, and regions of winding -1 are green. +// Knob 2: Speed. At 2 the white marker runs round the curve in 4 s and a change of class takes 5 s, 2.4 of them on the way into and out of the cusp; 0.2 is ten times slower. +// Knob 3: Turns. The turning number k, from -2 to 3, opening on 0, a figure eight. Each step of the knob is one change of class through one cusp, so a jump of three classes passes three. +// Knob 4: Wobble. How far the curve strays from the plain shape of its class: three Fourier terms of the tangent angle, once, twice and three times round, reach 0.38, 0.28 and 0.18 radians at 0 and three times that at 1. +// Buttons: a press moves the curve one class up (down from 3) through a cusp; a second press brings it back. + +const W = 64, H = 128; +const { floor, round, min, max, abs, sqrt, sin, cos, atan2, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const smooth = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- The curve. Its tangent angle is theta(t) = th0 + k t + p(t) for t +// round the circle, so the tangent turns k times; p is periodic, three +// Fourier terms whose sizes and phases drift. Its speed r(t) is solved +// below so that the curve closes and never stops, so it stays regular and +// every change of p is a regular homotopy: the class cannot change. +const NS = 600, DT = TAU / NS; +// The plain shape of each class: theta = k t + b sin t. For k = 0 the +// tangent swings 2.405 radians each way, the first zero of the Bessel +// function J0, where the figure eight closes by itself. +const BASE = { "-2": 1.25, "-1": 0.5, 0: 2.405, 1: 0.5, 2: 1.25, 3: 1.6 }; +const base = (k) => BASE[k] ?? 1.4; +// Top strip for the dial and the number; the curve lives below it. +const TOP = 21, MARGIN = 3; + +function localTurn(S, d) { + // The kink, after the normal form (u^2, u^3 - s u) of a loop that is + // pulled tight into a cusp at s = 0 and smoothed away for s < 0. Its + // tangent turns a full circle when s > 0 and not at all when s < 0. The + // normal form's tangent only creeps back to straight, as 1/u; past + // |u| = 1.5 it is eased the rest of the way by |u| = 3, so the kink + // stays local. + const u = d / S.w, s = S.s; + let a = atan2(-2 * u, 3 * u * u - s); + const far = s > 0 && u > 0 ? TAU : 0; + if (s > 0 && u > 0) a += TAU; + const e = smooth((abs(u) - 1.5) / 1.5); + return a + (far - a) * e; +} + +function build(S) { + const { kb, th0, b1, amp, ph, loop, mu, fade } = S; + const damp = S.damp; + // Wrapped Cauchy: (1 + rho) / (1 - rho), for the spread turn. + const q = (1 + S.rho) / (1 - S.rho); + const TH = S.TH; + let e0 = 0, e1 = 0, n = 0; + if (loop) { + e0 = localTurn(S, -PI); + e1 = localTurn(S, PI - 1e-9); + n = S.s > 0 ? 1 : 0; + } + for (let i = 0; i < NS; i++) { + const t = i * DT; + let th = th0 + kb * t + b1 * sin(t) + damp * (amp[0] * sin(t + ph[0]) + amp[1] * sin(2 * t + ph[1]) + amp[2] * sin(3 * t + ph[2])); + if (loop) { + let d = t - S.t0; + d -= TAU * floor((d + PI) / TAU); + // The kink's turn, made exactly periodic. + let L = fade * (localTurn(S, d) - (e0 + ((e1 - e0 - TAU * n) * (d + PI)) / TAU)); + // The same full turn as a wrapped Cauchy step: as rho falls to 0 it + // spreads evenly round the curve, where it is the turn of class k+1. + if (mu > 0) L += mu * (2 * Math.atan(q * Math.tan(d / 2)) + PI - L); + th += L; + } + TH[i] = th; + } + // Closure. The speed is r = exp(Re(conj(beta) e^(i theta))), positive + // for any beta, and beta minimises the convex sum of r over the samples: + // its gradient is the sum of r e^(i theta), the gap the curve would leave, + // and the minimum exists because the tangent points every way, or for + // k = 0 through more than a half turn. A few Newton steps from the last + // frame's beta find it. + const CS = S.CS, SN = S.SN; + for (let i = 0; i < NS; i++) (CS[i] = cos(TH[i])), (SN[i] = sin(TH[i])); + let bu = S.bu, bv = S.bv; + for (let it = 0; it < 12; it++) { + let gx = 0, gy = 0, hxx = 0, hxy = 0, hyy = 0; + for (let i = 0; i < NS; i++) { + const c = CS[i], sn = SN[i], e = Math.exp(bu * c + bv * sn); + gx += e * c; + gy += e * sn; + hxx += e * c * c; + hxy += e * c * sn; + hyy += e * sn * sn; + } + const det = hxx * hyy - hxy * hxy; + let du = (hyy * gx - hxy * gy) / det, dv = (hxx * gy - hxy * gx) / det; + const st = sqrt(du * du + dv * dv); + if (st > 0.5) (du *= 0.5 / st), (dv *= 0.5 / st); + bu -= du; + bv -= dv; + if (st < 1e-7) break; + } + // No closed curve has these tangents (a k = 0 curve whose tangent swings + // less than a half turn): report it and leave the last good curve alone. + let gx = 0, gy = 0; + for (let i = 0; i < NS; i++) { + const e = Math.exp(bu * CS[i] + bv * SN[i]); + gx += e * CS[i]; + gy += e * SN[i]; + } + S.ok = Number.isFinite(bu) && Number.isFinite(bv) && abs(gx) + abs(gy) < 1e-3 * NS; + if (!S.ok) return; + S.bu = bu; + S.bv = bv; + S.beta = sqrt(bu * bu + bv * bv); + let x = 0, y = 0; + const X = S.X, Y = S.Y; + for (let i = 0; i < NS; i++) { + const r = Math.exp(bu * CS[i] + bv * SN[i]); + S.HT[i] = TH[i]; + X[i] = x; + Y[i] = y; + x += r * CS[i] * DT; + y += r * SN[i] * DT; + } +} + +export function setup(params) { + params.s = { + TH: new Float64Array(NS), HT: new Float64Array(NS), CS: new Float64Array(NS), SN: new Float64Array(NS), bu: 0, bv: 0, X: new Float64Array(NS), Y: new Float64Array(NS), + PX: new Float32Array(NS), PY: new Float32Array(NS), + rows: Array.from({ length: H }, () => []), + wind: new Int8Array(W * H), + kb: null, th0: 0, b1: 0, amp: [0, 0, 0], ph: [0, 0, 0], + loop: 0, mu: 0, rho: 0, damp: 1, fade: 0, s: 0, w: 0.4, t0: 0, phase: 0, pt: 0, dir: 0, + tau: 0, mark: 0, flash: 0, off: 0, hue: 0, wob: 0.3, + // Each harmonic of p drifts at its own pace. + rate: [0.11 + 0.05 * Math.random(), 0.17 + 0.05 * Math.random(), 0.23 + 0.05 * Math.random()], + seed: [Math.random() * TAU, Math.random() * TAU, Math.random() * TAU], + }; +} + +// Seconds at Speed 2 for easing the kink in, for the cusp, and for spreading +// the new turn round the curve. +const EASE = 0.6, CUSP = 2.4, SPREAD = 2; +const S_MAX = 2; +// The spread turn starts as wide as the kink: (1 - RHO) / (1 + RHO) = 0.2. +const RHO = 0.667; + +export function update(dt, input, params) { + const S = params.s; + const k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + const sp = cl(k[1], 0.05, 3) / 2; + S.hue = k[0]; + S.wob = 0.5 + 1.0 * cl(k[3], 0, 1); + if (input.btnPressed && input.btnPressed.some(Boolean)) S.off = S.off ? 0 : 1; + const pick = cl(round(k[2]), -2, 3); + const target = S.off ? (pick < 3 ? pick + 1 : pick - 1) : pick; + if (S.kb === null) (S.kb = target), (S.b1 = base(target)); + S.tau += dt * sp; + // The marker goes round the curve once in 4 s at Speed 2. + S.mark = (S.mark + (dt * sp) / 4) % 1; + for (let m = 0; m < 3; m++) { + S.amp[m] = S.wob * [0.75, 0.55, 0.35][m] * sin(S.rate[m] * TAU * S.tau * 0.5 + S.seed[m]); + S.ph[m] = S.seed[m] * 2 + S.tau * S.rate[m] * 1.3; + } + S.th0 += dt * sp * 0.05; + + // Class changes, one step at a time. + if (!S.phase && target !== S.kb) { + S.dir = target > S.kb ? 1 : -1; + S.phase = 1; + S.pt = 0; + S.loop = 0; + build(S); + // The kink sits where the curve is farthest from its middle. + let cx = 0, cy = 0, best = -1; + for (let i = 0; i < NS; i++) (cx += S.X[i]), (cy += S.Y[i]); + cx /= NS; + cy /= NS; + for (let i = 0; i < NS; i += 3) { + const d = (S.X[i] - cx) ** 2 + (S.Y[i] - cy) ** 2; + if (d > best) (best = d), (S.t0 = i * DT); + } + // Next to class 0 the kink goes at t = 0, where the turn adds to the + // swing b1 sin t instead of cancelling it: a k = 0 curve needs its + // tangent to swing more than a half turn, or nothing closes it. + const lo = S.dir > 0 ? S.kb : S.kb - 1; + if (lo === 0 || lo === -1) S.t0 = 0; + S.loop = 1; + if (S.dir > 0) (S.fade = 0), (S.s = -S_MAX), (S.mu = 0), (S.rho = RHO); + else { + // Write the class-k curve as a class k-1 curve plus a full turn + // spread evenly round it, then gather that turn into a loop. + S.kb -= 1; + S.th0 -= PI - S.t0; + S.fade = 1; + S.s = S_MAX; + S.mu = 1; + S.rho = 0; + } + } + if (S.phase) { + S.pt += dt * sp; + // s runs from -S_MAX to S_MAX, slowest at the cusp so it can be seen. + const g = (u) => { + const v = 2 * cl(u, 0, 1) - 1; + return S_MAX * v * (0.2 + 0.8 * v * v); + }; + const kLo = S.kb, kHi = S.kb + 1; + // Up runs the steps forward; down runs the same steps backward. + const p = S.dir > 0 ? S.pt : EASE + CUSP + SPREAD - S.pt; + // A bump grows, sharpens into a cusp, and a kink is born from it; the + // kink's full turn takes a wrapped Cauchy profile and spreads round the + // curve. + S.fade = cl(p / EASE, 0, 1); + S.s = g((p - EASE) / CUSP); + const v = cl((p - EASE - CUSP) / SPREAD, 0, 1); + S.mu = smooth(v / 0.3); + const spread = smooth((v - 0.3) / 0.7); + S.rho = RHO * (1 - spread); + S.b1 = base(kLo) + (base(kHi) - base(kLo)) * spread; + if (abs(S.s) < 0.1 && S.fade > 0.99 && !S.mu) S.flash = 1; + if (S.pt >= EASE + CUSP + SPREAD) { + if (S.dir > 0) (S.kb += 1), (S.th0 += PI - S.t0); + S.loop = 0; + S.phase = 0; + S.mu = 0; + S.b1 = base(S.kb); + } + } + S.flash = max(0, S.flash - dt * 1.5); + // Keep |beta| under 1.2, so no stretch of the curve runs more than 11 + // times faster than another: where the wobble would push it higher, the + // wobble is turned down, and it comes back slowly. + S.damp = min(1, S.damp + dt * 0.3); + build(S); + for (let n = 0; n < 12 && (!S.ok || S.beta > 1.2); n++) { + S.damp *= 0.8; + build(S); + } +} + +// 3 by 5 digits and a minus. +const DIGITS = ["111101101101111", "010110010010111", "111001111100111", "111001111001111", "101101111001001"]; + +export function draw(display, params) { + const S = params.s; + if (!S.X || S.kb === null) return; + const { X, Y, HT: TH, PX, PY, rows, wind } = S; + // Fit the curve under the strip, a little taller than it is if there is room. + let x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9; + for (let i = 0; i < NS; i++) (x0 = min(x0, X[i])), (x1 = max(x1, X[i])), (y0 = min(y0, Y[i])), (y1 = max(y1, Y[i])); + const aw = W - 2 * MARGIN, ah = H - TOP - 2 * MARGIN; + const fx = aw / max(1e-6, x1 - x0), fy = ah / max(1e-6, y1 - y0); + const sx = min(fx, fy), sy = min(fy, sx * 1.8); + const cx = W / 2, cy = TOP + MARGIN + ah / 2, mx = (x0 + x1) / 2, my = (y0 + y1) / 2; + for (let i = 0; i < NS; i++) { + PX[i] = cx + (X[i] - mx) * sx; + PY[i] = cy - (Y[i] - my) * sy; + } + + // Winding numbers, row by row: each edge crossing the row's middle adds + // +1 going down the panel and -1 going up, counted from the left. + for (let y = 0; y < H; y++) rows[y].length = 0; + for (let i = 0; i < NS; i++) { + const j = (i + 1) % NS, ax = PX[i], ay = PY[i], bx = PX[j], by = PY[j]; + if (ay === by) continue; + const dir = by > ay ? 1 : -1, lo = min(ay, by), hi = max(ay, by); + for (let y = Math.ceil(lo - 0.5); y + 0.5 < hi && y < H; y++) { + if (y < 0 || y + 0.5 < lo) continue; + rows[y].push(ax + ((y + 0.5 - ay) * (bx - ax)) / (by - ay), dir); + } + } + wind.fill(0); + for (let y = 0; y < H; y++) { + const r = rows[y]; + if (!r.length) continue; + const idx = []; + for (let q = 0; q < r.length; q += 2) idx.push(q); + idx.sort((a, b) => r[a] - r[b]); + let w = 0, q = 0; + for (let x = 0; x < W; x++) { + while (q < idx.length && r[idx[q]] < x + 0.5) (w += r[idx[q] + 1]), q++; + wind[y * W + x] = w; + } + } + const hue0 = S.hue + 0.58; + const shade = (w) => { + if (!w) return null; + const a = min(3, abs(w)); + return hsv(hue0 + (w > 0 ? 0.1 : -0.12) + 0.05 * a * Math.sign(w), 0.5, 0.16 + 0.12 * a); + }; + const shades = {}; + for (let w = -4; w <= 4; w++) shades[w] = shade(w); + for (let y = TOP; y < H; y++) + for (let x = 0; x < W; x++) { + const c = shades[cl(wind[y * W + x], -4, 4)]; + if (c) display.setPixel(x, y, c[0] * 255, c[1] * 255, c[2] * 255); + } + + // The line, coloured by the direction it is drawn in. + const dirHue = (th) => atan2(sy * sin(th), sx * cos(th)) / TAU; + for (let i = 0; i < NS; i++) { + const j = (i + 1) % NS, ax = PX[i], ay = PY[i], bx = PX[j], by = PY[j]; + const c = hsv(hue0 + dirHue(TH[i]), 0.8, 1); + const steps = max(1, Math.ceil(2 * max(abs(bx - ax), abs(by - ay)))); + for (let q = 0; q < steps; q++) { + const u = q / steps; + display.setPixel(ax + (bx - ax) * u, ay + (by - ay) * u, c[0] * 255, c[1] * 255, c[2] * 255); + } + } + + // The marker on the curve, and the dial: the colour wheel with a dot at + // the marker's tangent direction, which goes round k times while the + // marker goes round once. + const mi = floor(S.mark * NS) % NS, mth = TH[mi]; + for (const [dx, dy] of [[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]]) display.setPixel(PX[mi] + dx, PY[mi] + dy, 255, 255, 255); + const DX = 11, DY = 10, DR = 7.5; + for (let y = 1; y < 20; y++) + for (let x = 2; x < 21; x++) { + const dx = x + 0.5 - DX, dy = y + 0.5 - DY, d = sqrt(dx * dx + dy * dy); + if (abs(d - DR) < 0.75) { + const c = hsv(hue0 + atan2(-dy, dx) / TAU, 0.8, 0.55); + display.setPixel(x, y, c[0] * 255, c[1] * 255, c[2] * 255); + } + } + const ma = atan2(sy * sin(mth), sx * cos(mth)); + const ddx = DX + DR * cos(ma), ddy = DY - DR * sin(ma); + for (let y = floor(ddy - 2); y <= ddy + 2; y++) + for (let x = floor(ddx - 2); x <= ddx + 2; x++) { + const d = sqrt((x + 0.5 - ddx) ** 2 + (y + 0.5 - ddy) ** 2); + if (d < 1.7) display.setPixel(x, y, 255, 255, 255); + } + for (let y = floor(DY - 1); y <= DY; y++) for (let x = floor(DX - 1); x <= DX; x++) display.setPixel(x, y, 120, 120, 120); + + // The turning number, doubled 3 by 5 digits. + const k = S.kb + (S.loop && (S.s > 0 || S.mu > 0) ? 1 : 0); + const lv = 150 + 105 * S.flash; + const dg = DIGITS[abs(k)] || DIGITS[0]; + const ox = 40, oy = 5; + for (let b = 0; b < 15; b++) + if (dg[b] === "1") + for (let e = 0; e < 4; e++) display.setPixel(ox + 2 * (b % 3) + (e & 1), oy + 2 * floor(b / 3) + (e >> 1), lv, lv, lv); + if (k < 0) for (let x = 31; x < 37; x++) for (let e = 0; e < 2; e++) display.setPixel(x, 9 + e, lv, lv, lv); + + // The cusp flashes white, with a ring opening round it. + if (S.loop && S.flash > 0) { + const ci = floor(((S.t0 / TAU) % 1) * NS), px = PX[ci], py = PY[ci], rad = 2 + 7 * (1 - S.flash); + for (let y = floor(py - rad - 2); y <= py + rad + 2; y++) + for (let x = floor(px - rad - 2); x <= px + rad + 2; x++) { + const d = sqrt((x + 0.5 - px) ** 2 + (y + 0.5 - py) ** 2); + const v = 255 * S.flash; + if (abs(d - rad) < 0.7 || d < 1.5) display.setPixel(x, y, v, v, v); + } + } +} From 1bf25db8b57f2e7573b9b8b654fce8253af28a2d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 16:08:36 +0000 Subject: [PATCH 15/21] =?UTF-8?q?Nine-fold=20Seal=20=EA=B3=BC=20Whitney-Gr?= =?UTF-8?q?austein=20=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- incoming/pattern-study/patterns/nine-fold-seal.js | 8 ++++---- incoming/pattern-study/patterns/whitney-graustein.js | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/incoming/pattern-study/patterns/nine-fold-seal.js b/incoming/pattern-study/patterns/nine-fold-seal.js index fb173fbe6..b22887cc0 100644 --- a/incoming/pattern-study/patterns/nine-fold-seal.js +++ b/incoming/pattern-study/patterns/nine-fold-seal.js @@ -11,10 +11,10 @@ // @matrix 64x128 // @knobs Ink=0..1, Speed=0.2..3, Plain=0..1, Press=0..1 // -// Knob 1: Ink. Turns the ink round the colour wheel at a fixed brightness. At 0 it is seal vermilion, at 0.5 a blue-green, and the paper stays a warm grey. -// Knob 2: Speed. At 2 a text holds folded for 3.6 s and the whole change, unfold, swap and refold, takes 4 s more; 0.2 is ten times slower and 3 half as fast again. -// Knob 3: Plain. How far the strokes may fold: 0 folds them until every box is full, 0.5 lets them fold seven lines deep, 1 shows the plain letters. -// Knob 4: Press. How unevenly the seal is pressed. The pressure rocks round the seal once in 6 s, and the side it leaves prints with bare patches: almost none at 0, up to three quarters of the ink missing in the far corner at 1. +// Knob 1: Ink. Turns the ink round the colour wheel at a fixed brightness. At 0 it is seal vermilion, at 0.5 a cyan blue, and the paper stays a dim warm grey. +// Knob 2: Speed. At 2 a text holds folded for 3 s (the first for 2.4 s) and the change to the next, unfold, swap and refold, takes 4 s more; 0.2 is ten times slower and 3 half as fast again. +// Knob 3: Plain. How far the strokes may fold: 0 folds them until every box is full, 0.5 stops every fold seven or eight lines out from its stroke, 1 shows the plain letters. +// Knob 4: Press. How unevenly the seal is pressed. The pressure rocks round the seal once in 6 s, and the side it leaves prints with bare patches: none at 0, a few LEDs in a hundred at the opening 0.06, while at 1 one LED of ink in seven fails everywhere and the far corner prints almost bare. // Buttons: any button starts the change to the next text at once. const W = 64, H = 128; diff --git a/incoming/pattern-study/patterns/whitney-graustein.js b/incoming/pattern-study/patterns/whitney-graustein.js index c4addc941..b9eda03a3 100644 --- a/incoming/pattern-study/patterns/whitney-graustein.js +++ b/incoming/pattern-study/patterns/whitney-graustein.js @@ -243,9 +243,9 @@ export function update(dt, input, params) { } } S.flash = max(0, S.flash - dt * 1.5); - // Keep |beta| under 1.2, so no stretch of the curve runs more than 11 - // times faster than another: where the wobble would push it higher, the - // wobble is turned down, and it comes back slowly. + // Where the wobble would push |beta| past 1.2, so that one stretch of the + // curve runs 11 times faster than another, or leaves nothing that closes, + // the wobble is turned down, and it comes back slowly. S.damp = min(1, S.damp + dt * 0.3); build(S); for (let n = 0; n < 12 && (!S.ok || S.beta > 1.2); n++) { From ddfb747c33588561477ca0f89c792573eb41bea7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 16:08:49 +0000 Subject: [PATCH 16/21] =?UTF-8?q?Letter=20Melt=20=EA=B3=BC=20Steiner=20Sym?= =?UTF-8?q?metrization=20=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- .../patterns/steiner-symmetrization.js | 384 ++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 incoming/pattern-study/patterns/steiner-symmetrization.js diff --git a/incoming/pattern-study/patterns/steiner-symmetrization.js b/incoming/pattern-study/patterns/steiner-symmetrization.js new file mode 100644 index 000000000..87abac37b --- /dev/null +++ b/incoming/pattern-study/patterns/steiner-symmetrization.js @@ -0,0 +1,384 @@ +// ===== Patternflow pattern ===== +// Title: Steiner Symmetrization +// Family: line +// Idea: An irregular shape drawn as a hatch of parallel one-LED chords slides every chord along itself, each at its own moment so a shear wave runs down the stack, until its midpoint sits on a faint axis and the chords cut by a gap close up into one, then the hatch turns to a new axis and slides again, while a bar at the foot shrinks the perimeter toward that of the disc of the same area. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Steiner symmetrization (Jakob Steiner, 1838), repeated symmetrizations converging to the disc (Wilhelm Gross, 1917, after Wilhelm Blaschke's selection theorem in Kreis und Kugel, 1916), symmetrization in a finite set of directions (Daniel Klain, 2012), pattern round 69 +// Round: 69 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Shape=0..5, Axes=0..8 +// +// Knob 1: Hue. Turns the hatch round the wheel. At 0 the chords run from azure to violet along the axis, with the bar in amber. +// Knob 2: Speed. At 2 a symmetrization takes about 2.3 s: the chords slide for 1.45 s, rest 0.25 s and the hatch turns for 0.55 s. At 0.2 ten times slower, at 3 two thirds as long. +// Knob 3: Shape. 0 takes the five starting shapes in turn, a crescent, an egg with a hole, a seven pointed star, two blobs and a comb; 1 to 5 start from one of them every time. +// Knob 4: Axes. 0 turns the axis by 68.75 degrees, the golden angle of a half turn, every time, so no axis comes back and the shape rounds into the disc. 1 to 8 use that many axes, evenly spaced and drawn faintly: one axis does all its work in the first slide, and two or more settle the shape into one with a mirror for every axis that is not a disc. + +const W = 64, H = 128, OX = 32, OY = 64; +const { floor, min, max, abs, sqrt, sin, cos, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// Chords one LED apart, s from -72 to 72 along the axis, at most MAXP +// pieces each. +const K = 144, S0 = -72, MAXP = 8, MAXC = 16; +const GOLD = (PI * (3 - sqrt(5))) / 2; +const BAR = 60; + +// Shapes as rings of points; a point is inside when a ray from it crosses +// the rings an odd number of times, so a ring inside another is a hole. +function arc(cx, cy, h, b, n, out, back) { + // A circular arc from (cx, cy - h) to (cx, cy + h) bulging left by b. + const R = (h * h + b * b) / (2 * b), p0 = Math.asin(h / R), C = cx - b + R; + for (let i = 0; i <= n; i++) { + const u = back ? 1 - i / n : i / n, p = -p0 + 2 * p0 * u; + out.push(C - R * cos(p), cy + R * sin(p)); + } + return out; +} +function ellipse(cx, cy, rx, ry, n, out) { + for (let i = 0; i < n; i++) out.push(cx + rx * cos((2 * PI * i) / n), cy + ry * sin((2 * PI * i) / n)); + return out; +} +const SHAPES = [ + // A crescent between two arcs on the same chord. + () => [arc(52, 64, 56, 12, 40, arc(52, 64, 56, 40, 60, []), true).slice(0, -2)], + // An egg with a round hole off its centre. + () => [ellipse(32, 64, 29, 40, 72, []), ellipse(39, 54, 14, 14, 40, [])], + // A seven pointed star, stretched to the panel. + () => { + const out = []; + for (let i = 0; i < 14; i++) { + const a = (PI * i) / 7 - PI / 2 + 0.2, r = i % 2 ? 15 : 30; + out.push(32 + r * cos(a), 64 + 1.55 * r * sin(a)); + } + return [out]; + }, + // Two blobs apart. + () => [ellipse(22, 37, 17, 24, 48, []), ellipse(42, 92, 19, 21, 48, [])], + // A comb of three teeth on a spine. + () => [[12, 14, 60, 14, 60, 30, 26, 30, 26, 56, 60, 56, 60, 72, 26, 72, 26, 98, 60, 98, 60, 114, 12, 114]], +]; + +function perimeter(rings) { + let p = 0; + for (const r of rings) + for (let i = 0; i < r.length; i += 2) { + const j = (i + 2) % r.length; + p += sqrt((r[j] - r[i]) ** 2 + (r[j + 1] - r[i + 1]) ** 2); + } + return p; +} + +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; +} + +export function setup(params) { + params.s = { + rings: null, shape: -1, step: 0, n: 0, phase: 1, pt: 0.5, phi: PI / 2, from: PI / 2, to: PI / 2, + P0: 1, P: 1, Pfrom: 1, Pto: 1, A: 1, prevP: 1, still: 0, fade: 1, dir: 1, lo: 0, hi: 0, + cross: new Float64Array(K * MAXC), nc: new Int32Array(K), + a0: new Float64Array(K * MAXP), b0: new Float64Array(K * MAXP), a1: new Float64Array(K * MAXP), b1: new Float64Array(K * MAXP), + cnt: new Int32Array(K), L: new Float64Array(K), del: new Float64Array(K), dur: new Float64Array(K), u: new Float64Array(K), sp: new Float64Array(K), + col: new Float64Array(3), tmp: new Float64Array(3), ax: new Float64Array(16), + next: null, prog: 0.5, hue: 0.5, axes: 0, + }; + return params.s; +} + +// The pieces of every chord at angle phi: the line s = const through the +// rings, crossings sorted along t and taken in pairs. +function cut(S, phi, a, b, cnt) { + const { cross, nc } = S, ca = cos(phi), sa = sin(phi); + nc.fill(0); + for (const r of S.rings) + for (let i = 0; i < r.length; i += 2) { + const j = (i + 2) % r.length; + const x1 = r[i] - OX, y1 = r[i + 1] - OY, x2 = r[j] - OX, y2 = r[j + 1] - OY; + const s1 = x1 * ca + y1 * sa, s2 = x2 * ca + y2 * sa; + if (s1 === s2) continue; + const t1 = -x1 * sa + y1 * ca, t2 = -x2 * sa + y2 * ca; + const lo = min(s1, s2), hi = max(s1, s2); + // Chords with lo <= s < hi, so a vertex is counted once. Both ends + // are tested on the same s, or rounding counts a vertex twice. + for (let k = max(0, floor(lo - S0 - 0.5)); k < K; k++) { + const s = S0 + k + 0.5; + if (s < lo) continue; + if (s >= hi) break; + const t = t1 + ((s - s1) / (s2 - s1)) * (t2 - t1); + let m = nc[k]; + if (m >= MAXC) continue; + const o = k * MAXC; + while (m > 0 && cross[o + m - 1] > t) { + cross[o + m] = cross[o + m - 1]; + m--; + } + cross[o + m] = t; + nc[k]++; + } + } + for (let k = 0; k < K; k++) { + const n = min(MAXP, nc[k] >> 1), o = k * MAXC; + cnt[k] = n; + for (let j = 0; j < n; j++) { + a[k * MAXP + j] = cross[o + 2 * j]; + b[k * MAXP + j] = cross[o + 2 * j + 1]; + } + } +} + +// The total length of the chord at s, for the ends of the new shape. +function lengthAt(S, phi, s) { + const ca = cos(phi), sa = sin(phi), ts = []; + for (const r of S.rings) + for (let i = 0; i < r.length; i += 2) { + const j = (i + 2) % r.length; + const x1 = r[i] - OX, y1 = r[i + 1] - OY, x2 = r[j] - OX, y2 = r[j + 1] - OY; + const s1 = x1 * ca + y1 * sa, s2 = x2 * ca + y2 * sa; + if (s1 === s2 || s < min(s1, s2) || s >= max(s1, s2)) continue; + ts.push(-x1 * sa + y1 * ca + ((s - s1) / (s2 - s1)) * (-x2 * sa + y2 * ca + x1 * sa - y1 * ca)); + } + ts.sort((p, q) => p - q); + let L = 0; + for (let i = 0; i + 1 < ts.length; i += 2) L += ts[i + 1] - ts[i]; + return L; +} + +// Start a symmetrization about the axis through the centre at angle phi: +// each chord's pieces now, and where they end, stacked in their order into +// one chord centred on the axis. The new shape is a ring through the ends +// of the centred chords for every run of chords that are not empty, so two +// blobs no chord crosses stay two. +function begin(S, phi) { + const { a0, b0, a1, b1, cnt, L, del, dur, u, sp } = S; + S.phi = S.from = S.to = phi; + cut(S, phi, a0, b0, cnt); + const ca = cos(phi), sa = sin(phi); + let lo = 1e9, hi = -1e9; + for (const r of S.rings) + for (let i = 0; i < r.length; i += 2) { + const s = (r[i] - OX) * ca + (r[i + 1] - OY) * sa; + lo = min(lo, s); + hi = max(hi, s); + } + for (let k = 0; k < K; k++) { + let len = 0; + for (let j = 0; j < cnt[k]; j++) len += b0[k * MAXP + j] - a0[k * MAXP + j]; + L[k] = len; + let at = -len / 2; + for (let j = 0; j < cnt[k]; j++) { + const o = k * MAXP + j, l = b0[o] - a0[o]; + a1[o] = at; + b1[o] = at + l; + at += l; + } + // The shear wave runs along the axis, the other way each time. + const w = (S0 + k + 0.5 - lo) / max(1, hi - lo); + del[k] = 0.45 * (S.dir > 0 ? w : 1 - w) + 0.08 * Math.random(); + dur[k] = 0.7 * (0.8 + 0.4 * Math.random()); + u[k] = 0; + sp[k] = 0; + } + S.dir = -S.dir; + // Runs of (s, length). A run's open end reaches past its last chord as + // far as the lengths' slope says, at most to the empty chord beside it. + const e = 1e-4, runs = []; + let run = null; + const end = (r, back) => { + const n = r.length, l0 = back ? r[n - 1] : r[1], l1 = n > 2 ? (back ? r[n - 3] : r[3]) : 0; + const d = n > 2 ? min(1, l0 / max(1e-3, l1 - l0)) : 0.5, s = (back ? r[n - 2] : r[0]) + (back ? d : -d); + if (back) r.push(s, 0); + else r.unshift(s, 0); + }; + for (let k = 0; k < K; k++) { + const s = S0 + k + 0.5; + if (s <= lo + e || s >= hi - e) continue; + if (L[k] > 0) { + if (!run) run = []; + run.push(s, L[k]); + } else if (run) { + end(run, true); + runs.push(run); + run = null; + } + } + if (run) runs.push(run); + if (!runs.length) runs.push([]); + // The shape's own ends, measured exactly. + if (runs[0][0] !== undefined && runs[0][1] === 0) runs[0].splice(0, 2); + runs[0].unshift(lo + e, lengthAt(S, phi, lo + e)); + for (let i = 1; i < runs.length; i++) end(runs[i], false); + runs[runs.length - 1].push(hi - e, lengthAt(S, phi, hi - e)); + // Keep the area the shape started with: sampling a curved edge at one LED + // loses a sliver every time, so the lengths are scaled back up. + let area = 0; + for (const r of runs) for (let i = 0; i + 3 < r.length; i += 2) area += ((r[i + 2] - r[i]) * (r[i + 1] + r[i + 3])) / 2; + const f = area > 0 ? S.A / area : 1; + S.next = runs.map((r) => { + const ring = []; + for (let i = 0; i < r.length; i += 2) ring.push(OX + r[i] * ca + ((f * r[i + 1]) / 2) * sa, OY + r[i] * sa - ((f * r[i + 1]) / 2) * ca); + for (let i = r.length - 2; i >= 0; i -= 2) ring.push(OX + r[i] * ca - ((f * r[i + 1]) / 2) * sa, OY + r[i] * sa + ((f * r[i + 1]) / 2) * ca); + return ring; + }); + S.Pfrom = S.P; + S.Pto = perimeter(S.next); + S.phase = 0; + S.pt = 0; +} + +function newShape(S, sel) { + S.shape = sel ? sel - 1 : (S.shape + 1) % SHAPES.length; + S.rings = SHAPES[S.shape](); + S.P0 = S.P = S.prevP = perimeter(S.rings); + S.n = 0; + S.still = 0; + S.pt = 0; + S.phi = S.from = S.to = PI / 2; + // The area, for the disc's perimeter, from the chords of the first cut. + cut(S, S.phi, S.a0, S.b0, S.cnt); + let A = 0; + for (let k = 0; k < K; k++) for (let j = 0; j < S.cnt[k]; j++) A += S.b0[k * MAXP + j] - S.a0[k * MAXP + j]; + S.A = A; +} + +const ease = (x) => x * x * (3 - 2 * x); + +export function update(dt, input, params) { + const S = params.s || setup(params); + const kv = input.knobValues || [0, 2, 0, 0.06]; + S.hue = kv[0] || 0; + const rate = cl(kv[1], 0.2, 3) / 2, sel = cl(Math.round(kv[2]), 0, 5), axes = cl(Math.round(kv[3]), 0, 8); + S.axes = axes; + if (!S.rings) newShape(S, sel); + const d = cl(dt || 0, 0, 0.1) * rate; + S.pt += d; + if (S.phase === 0) { + // Slide: each chord eases from its pieces to the centred chord. + let all = true, sum = 0; + for (let k = 0; k < K; k++) { + const x = cl((S.pt - S.del[k]) / S.dur[k], 0, 1), e = ease(x); + S.sp[k] = d > 0 ? (e - S.u[k]) / d : 0; + S.u[k] = e; + sum += e; + if (x < 1) all = false; + } + S.prog = sum / K; + S.P = S.Pfrom + (S.Pto - S.Pfrom) * S.prog; + if (all) { + S.rings = S.next; + S.P = S.Pto; + S.n++; + S.still = abs(S.prevP - S.P) < 0.002 * S.P ? S.still + 1 : 0; + S.prevP = S.P; + S.phase = 1; + S.pt = 0; + S.sp.fill(0); + } + } else if (S.phase === 1 && S.pt > (S.n ? 0.2 : 0.15)) { + // Rest, then turn to the next axis, or let a round or settled shape go. + const disc = 2 * sqrt(PI * S.A); + if (S.n && (S.P < disc * 1.006 || S.still >= 2 || S.n >= 12)) { + S.phase = 3; + S.pt = 0; + } else if (!S.n) begin(S, S.phi); + else { + S.from = S.phi; + S.to = S.phi + (axes ? PI / axes : GOLD); + S.phase = axes === 1 ? 2.5 : 2; + S.pt = 0; + } + } else if (S.phase === 2) { + const x = cl(S.pt / 0.5, 0, 1); + S.phi = S.from + (S.to - S.from) * ease(x); + if (x >= 1) begin(S, S.to % (2 * PI)); + } else if (S.phase === 2.5) { + // One axis: nothing to turn to. + if (S.pt > 0.3) begin(S, S.phi); + } else if (S.phase === 3) { + // Hold the result, fade, and start again from a new shape. + S.fade = S.pt < 1.3 ? 1 : max(0, 1 - (S.pt - 1.3) / 0.3); + if (S.pt > 1.6) { + newShape(S, sel); + S.phase = 4; + S.pt = 0; + } + } else if (S.phase === 4) { + S.fade = min(1, S.pt / 0.3); + if (S.pt > 0.6) { + S.fade = 1; + begin(S, S.phi); + } + } + if (S.phase !== 0) S.prog = 0; +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.rings) return; + const sliding = S.phase === 0, { a0, b0, a1, b1, cnt, u, sp } = S; + if (!sliding) cut(S, S.phi, a0, b0, cnt); + const ca = cos(S.phi), sa = sin(S.phi), h0 = 0.56 + S.hue, f = S.fade, col = S.col; + // Every axis of a finite set, drawn faintly; the current one a little + // brighter. + const na = S.axes || 1, rd = sqrt(S.A / PI), ax = S.ax; + for (let m = 1; m < na; m++) { + ax[2 * m] = sin(S.phi + (PI * m) / na); + ax[2 * m + 1] = cos(S.phi + (PI * m) / na); + } + for (let Y = 0; Y < H - 6; Y++) + for (let X = 0; X < W; X++) { + const x = X + 0.5 - OX, y = Y + 0.5 - OY, s = x * ca + y * sa, t = -x * sa + y * ca; + const k = floor(s - S0); + let lit = 0, piece = 0; + if (k >= 0 && k < K) + for (let j = 0; j < cnt[k]; j++) { + const o = k * MAXP + j; + const A = sliding ? a0[o] + (a1[o] - a0[o]) * u[k] : a0[o], B = sliding ? b0[o] + (b1[o] - b0[o]) * u[k] : b0[o]; + const c = min(t - A, B - t) + 0.5; + if (c > lit) { + lit = min(1, c); + piece = j; + } + } + if (lit > 0) { + // Hue runs along the axis, odd and even chords alternate in + // brightness, a moving chord flares and pieces cut apart by a gap + // keep their own tint until they meet. + const w = (s + 40) / 80, mv = sliding ? cl(abs(sp[k]) * 0.012 * (abs(a1[k * MAXP + piece] - a0[k * MAXP + piece]) + 4), 0, 1) : 0; + const tint = sliding && piece % 2 ? 0.22 * (1 - u[k]) : 0; + hsv(h0 + 0.16 * cl(w, 0, 1) + tint, 0.75 - 0.45 * mv, (k & 1 ? 0.9 : 0.56) + 0.12 * mv, col); + const g = lit * f * (abs(t) < 0.5 ? 1.15 : 1); + display.setPixel(X, Y, min(255, col[0] * g) | 0, min(255, col[1] * g) | 0, min(255, col[2] * g) | 0); + continue; + } + // Outside: the axes, and the disc of the same area as a faint ring. + let v = 0; + if (abs(t) < 0.5) v = 0.24; + else if (na > 1) + for (let m = 1; m < na; m++) if (abs(-x * ax[2 * m] + y * ax[2 * m + 1]) < 0.5) v = 0.13; + if (abs(sqrt(x * x + y * y) - rd) < 0.5) v = max(v, 0.16); + if (v > 0) { + hsv(h0 + 0.08, 0.45, v * f, col); + display.setPixel(X, Y, col[0] | 0, col[1] | 0, col[2] | 0); + } + } + // The perimeter bar: full width is the starting shape's perimeter, the + // white tick the disc's 2 sqrt(pi A). + const len = (BAR * S.P) / S.P0, tick = 2 + (BAR * 2 * sqrt(PI * S.A)) / S.P0; + const bar = hsv(h0 + 0.5, 0.8, 0.95, S.tmp); + for (let X = 2; X < 2 + BAR; X++) { + const c = cl(len - (X - 2), 0, 1); + for (let Y = 124; Y < 126; Y++) { + if (c > 0) display.setPixel(X, Y, (bar[0] * c * f) | 0, (bar[1] * c * f) | 0, (bar[2] * c * f) | 0); + else display.setPixel(X, Y, 30 * f, 30 * f, 34 * f); + } + } + const T = floor(tick); + for (let Y = 122; Y < 128; Y++) display.setPixel(T, Y, 255 * f, 255 * f, 255 * f); +} From 9e7d43fb9dc7e5aea4218942de82ff396aa2ecfb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 16:11:36 +0000 Subject: [PATCH 17/21] =?UTF-8?q?Steiner=20Symmetrization=20=EC=9D=84=20?= =?UTF-8?q?=EB=8B=A4=EB=93=AC=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- .../patterns/steiner-symmetrization.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/incoming/pattern-study/patterns/steiner-symmetrization.js b/incoming/pattern-study/patterns/steiner-symmetrization.js index 87abac37b..d7b854e7d 100644 --- a/incoming/pattern-study/patterns/steiner-symmetrization.js +++ b/incoming/pattern-study/patterns/steiner-symmetrization.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Steiner Symmetrization // Family: line -// Idea: An irregular shape drawn as a hatch of parallel one-LED chords slides every chord along itself, each at its own moment so a shear wave runs down the stack, until its midpoint sits on a faint axis and the chords cut by a gap close up into one, then the hatch turns to a new axis and slides again, while a bar at the foot shrinks the perimeter toward that of the disc of the same area. +// Idea: An irregular shape drawn as a hatch of parallel one-LED chords slides every chord along itself, each at its own moment so a shear wave runs down the stack, until its midpoint sits on a faint axis and chords cut by a gap close into one, then the hatch turns to a new axis and slides again, while a faint circle marks the disc of the same area and a bar at the foot shrinks the perimeter toward the disc's. // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -12,9 +12,9 @@ // @knobs Hue=0..1, Speed=0.2..3, Shape=0..5, Axes=0..8 // // Knob 1: Hue. Turns the hatch round the wheel. At 0 the chords run from azure to violet along the axis, with the bar in amber. -// Knob 2: Speed. At 2 a symmetrization takes about 2.3 s: the chords slide for 1.45 s, rest 0.25 s and the hatch turns for 0.55 s. At 0.2 ten times slower, at 3 two thirds as long. +// Knob 2: Speed. At 2 a symmetrization takes about 2.1 s: the chords slide for up to 1.4 s, rest 0.2 s and the hatch turns for 0.5 s, and a shape that has come round holds 1.3 s and fades into the next. At 0.2 ten times slower, at 3 two thirds as long. // Knob 3: Shape. 0 takes the five starting shapes in turn, a crescent, an egg with a hole, a seven pointed star, two blobs and a comb; 1 to 5 start from one of them every time. -// Knob 4: Axes. 0 turns the axis by 68.75 degrees, the golden angle of a half turn, every time, so no axis comes back and the shape rounds into the disc. 1 to 8 use that many axes, evenly spaced and drawn faintly: one axis does all its work in the first slide, and two or more settle the shape into one with a mirror for every axis that is not a disc. +// Knob 4: Axes. 0 turns the axis by 68.75 degrees, the golden angle of a half turn, every time, so no axis comes back and the shape rounds into the disc. 1 to 8 use that many axes, evenly spaced and drawn faintly. One axis does all its work in the first slide. With a few the shape settles into one with a mirror for every axis that is still not the disc: with two axes the star stops at 1.24 times the disc's perimeter, with three at 1.03, and from four on it comes within 1%. const W = 64, H = 128, OX = 32, OY = 64; const { floor, min, max, abs, sqrt, sin, cos, PI } = Math; @@ -22,6 +22,8 @@ const cl = (v, a, b) => (v < a ? a : v > b ? b : v); // Chords one LED apart, s from -72 to 72 along the axis, at most MAXP // pieces each. const K = 144, S0 = -72, MAXP = 8, MAXC = 16; +// The golden angle of a half turn, pi (2 - phi) for the golden ratio phi: +// 68.75 degrees, so no two axes are ever the same. const GOLD = (PI * (3 - sqrt(5))) / 2; const BAR = 60; @@ -81,8 +83,8 @@ function hsv(h, s, v, out) { export function setup(params) { params.s = { - rings: null, shape: -1, step: 0, n: 0, phase: 1, pt: 0.5, phi: PI / 2, from: PI / 2, to: PI / 2, - P0: 1, P: 1, Pfrom: 1, Pto: 1, A: 1, prevP: 1, still: 0, fade: 1, dir: 1, lo: 0, hi: 0, + rings: null, shape: -1, n: 0, phase: 1, pt: 0.5, phi: PI / 2, from: PI / 2, to: PI / 2, + P0: 1, P: 1, Pfrom: 1, Pto: 1, A: 1, prevP: 1, still: 0, fade: 1, dir: 1, cross: new Float64Array(K * MAXC), nc: new Int32Array(K), a0: new Float64Array(K * MAXP), b0: new Float64Array(K * MAXP), a1: new Float64Array(K * MAXP), b1: new Float64Array(K * MAXP), cnt: new Int32Array(K), L: new Float64Array(K), del: new Float64Array(K), dur: new Float64Array(K), u: new Float64Array(K), sp: new Float64Array(K), @@ -179,7 +181,7 @@ function begin(S, phi) { at += l; } // The shear wave runs along the axis, the other way each time. - const w = (S0 + k + 0.5 - lo) / max(1, hi - lo); + const w = cl((S0 + k + 0.5 - lo) / max(1, hi - lo), 0, 1); del[k] = 0.45 * (S.dir > 0 ? w : 1 - w) + 0.08 * Math.random(); dur[k] = 0.7 * (0.8 + 0.4 * Math.random()); u[k] = 0; From 7679a16054b220e491ac3c2c934cfc74b1f5eda5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 16:13:33 +0000 Subject: [PATCH 18/21] =?UTF-8?q?Letter=20Melt=20=EA=B3=BC=20Steiner=20Sym?= =?UTF-8?q?metrization=20=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- incoming/pattern-study/patterns/steiner-symmetrization.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/incoming/pattern-study/patterns/steiner-symmetrization.js b/incoming/pattern-study/patterns/steiner-symmetrization.js index d7b854e7d..df52b6d23 100644 --- a/incoming/pattern-study/patterns/steiner-symmetrization.js +++ b/incoming/pattern-study/patterns/steiner-symmetrization.js @@ -303,7 +303,7 @@ export function update(dt, input, params) { // One axis: nothing to turn to. if (S.pt > 0.3) begin(S, S.phi); } else if (S.phase === 3) { - // Hold the result, fade, and start again from a new shape. + // Hold the shape it settled into, then fade into a new one. S.fade = S.pt < 1.3 ? 1 : max(0, 1 - (S.pt - 1.3) / 0.3); if (S.pt > 1.6) { newShape(S, sel); From fad5ba8a40b37cb64859cfdcc197a197bd5246ab Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 16:16:49 +0000 Subject: [PATCH 19/21] =?UTF-8?q?69=ED=8C=90=20README=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=20=EB=85=B8=ED=8A=B8=EB=A5=BC=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- incoming/pattern-study/README.md | 101 ++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 3 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 2b1f246f8..3c7140f1b 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 578개를 쉰여섯 갈래로 나눠 만들었어요. 1판에서 열세 갈래로 23개를 만들고, 2판에서 1판에 없던 아홉 갈래로 16개를, 3판에서 하나를, 4판에서 새 갈래인 직물로 하나를 더했어요. 5판은 새 패턴 없이 1판의 Euclid Pulse 를 다시 짰고, 6판은 세운 패널의 128줄을 위아래로 다 쓰는 패턴 하나를 더했어요. 7판은 또렷한 모양이 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 8판은 새 갈래인 기계로 하나를 더했어요. 9판은 새 패턴 없이 1판의 Sprout 을 다시 짰어요. 10판은 세운 패널을 떨어지는 블록의 우물로 쓰는 패턴 하나를 더했어요. 11판은 여러 색이 꽤 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 12판은 새 갈래인 천체로 하나를 더했어요. 13판은 새 패턴 없이 1판의 Glyph Rain 을 다시 짰어요. 14판은 절차가 바뀌기 전에 띄운 바퀴라서, 세운 패널의 높이를 다 떨어지는 폭포 하나를 더했어요. 15판은 바뀐 절차의 첫 판이라 열 갈래에 새 패턴을 하나씩, 모두 열 개를 더했어요. 도시와 음악 갈래는 그때 새로 열었어요. 16판도 열 갈래에 하나씩 열 개를 더했고, 바다와 곤충 갈래를 새로 열었어요. 17판도 열 갈래에 하나씩 열 개를 더했고, 전자 회로와 장난감 갈래를 새로 열었어요. 18판도 열 갈래에 하나씩 열 개를 더했고, 유리와 광물·결정, 놀이공원 갈래를 새로 열었어요. 19판도 열 갈래에 하나씩 열 개를 더했고, 부엌과 버섯·균사 갈래를 새로 열었어요. 20판도 열 갈래에 하나씩 열 개를 더했고, 새와 화학·실험 갈래를 새로 열었어요. 21판도 열 갈래에 하나씩 열 개를 더했고, 종이와 운동·경기 갈래를 새로 열었어요. 22판도 열 갈래에 하나씩 열 개를 더했고, 몸과 비행 갈래를 새로 열었어요. 23판도 열 갈래에 하나씩 열 개를 더했고, 무대·공연과 짐승 갈래를 새로 열었어요. 24판도 열 갈래에 하나씩 열 개를 더했고, 공사·건설과 도자기 갈래를 새로 열었어요. 25판도 열 갈래에 하나씩 열 개를 더했고, 농사·밭과 화산 갈래를 새로 열었어요. 26판도 열 갈래에 하나씩 열 개를 더했고, 얼음·극지와 미생물 갈래를 새로 열었어요. 27판도 열 갈래에 하나씩 열 개를 더했고, 철도와 필름·영화 갈래를 새로 열었어요. 28판은 주제 표가 수학 쪽으로 바뀐 첫 판이라 이름 있는 수학 대상 열 개를 열 갈래에 하나씩 더했고, 수론 갈래를 새로 열었어요. 29판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 첫 판이라, 이름 있는 수학 대상 여섯과 화장품 매장 벽에 틀 질감 넷을 더했고, 위상·매듭과 화장품 팝업 갈래를 새로 열었어요. 30판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 매장의 LED 벽에 틀 질감 넷을 화장품 팝업 갈래에 더했어요. 31판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 열지 않았어요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽의 질감 넷을 화장품 팝업 갈래에 더했어요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 41판은 주제 표가 수학·추상 다섯과 할로윈 다섯으로 바뀐 첫 판이라, 수학 대상 다섯을 다섯 갈래에 하나씩 더하고 할로윈 갈래를 새로 열어 10월 31일에 틀 패턴 다섯을 넣었어요. 42판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 41판이 연 할로윈 갈래에 더했어요. 43판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 44판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 45판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 열 개 아래인 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 46판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 47판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 48판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 49판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 50판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 51판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 52판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 53판은 할로윈 몫이 쉬게 된 첫 판이라, 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 54판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 55판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 56판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 57판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 58판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 59판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 60판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 61판도 수학 대상 열 개를 패턴이 적은 갈래부터 열 갈래에 하나씩 더했어요. 62판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 63판은 64판과 같이 주제 표에 「타이포」가 들어온 첫 판이라, 글자가 들어간 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 64판은 주제 표가 바뀌어 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 네 갈래에 더했어요. 65판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 66판도 같은 표로 돌아서 글자 패턴 넷을 글자 갈래에, 하나를 데이터 그림 갈래에 더하고, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 67판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 588개를 쉰여섯 갈래로 나눠 만들었어요. 1판에서 열세 갈래로 23개를 만들고, 2판에서 1판에 없던 아홉 갈래로 16개를, 3판에서 하나를, 4판에서 새 갈래인 직물로 하나를 더했어요. 5판은 새 패턴 없이 1판의 Euclid Pulse 를 다시 짰고, 6판은 세운 패널의 128줄을 위아래로 다 쓰는 패턴 하나를 더했어요. 7판은 또렷한 모양이 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 8판은 새 갈래인 기계로 하나를 더했어요. 9판은 새 패턴 없이 1판의 Sprout 을 다시 짰어요. 10판은 세운 패널을 떨어지는 블록의 우물로 쓰는 패턴 하나를 더했어요. 11판은 여러 색이 꽤 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 12판은 새 갈래인 천체로 하나를 더했어요. 13판은 새 패턴 없이 1판의 Glyph Rain 을 다시 짰어요. 14판은 절차가 바뀌기 전에 띄운 바퀴라서, 세운 패널의 높이를 다 떨어지는 폭포 하나를 더했어요. 15판은 바뀐 절차의 첫 판이라 열 갈래에 새 패턴을 하나씩, 모두 열 개를 더했어요. 도시와 음악 갈래는 그때 새로 열었어요. 16판도 열 갈래에 하나씩 열 개를 더했고, 바다와 곤충 갈래를 새로 열었어요. 17판도 열 갈래에 하나씩 열 개를 더했고, 전자 회로와 장난감 갈래를 새로 열었어요. 18판도 열 갈래에 하나씩 열 개를 더했고, 유리와 광물·결정, 놀이공원 갈래를 새로 열었어요. 19판도 열 갈래에 하나씩 열 개를 더했고, 부엌과 버섯·균사 갈래를 새로 열었어요. 20판도 열 갈래에 하나씩 열 개를 더했고, 새와 화학·실험 갈래를 새로 열었어요. 21판도 열 갈래에 하나씩 열 개를 더했고, 종이와 운동·경기 갈래를 새로 열었어요. 22판도 열 갈래에 하나씩 열 개를 더했고, 몸과 비행 갈래를 새로 열었어요. 23판도 열 갈래에 하나씩 열 개를 더했고, 무대·공연과 짐승 갈래를 새로 열었어요. 24판도 열 갈래에 하나씩 열 개를 더했고, 공사·건설과 도자기 갈래를 새로 열었어요. 25판도 열 갈래에 하나씩 열 개를 더했고, 농사·밭과 화산 갈래를 새로 열었어요. 26판도 열 갈래에 하나씩 열 개를 더했고, 얼음·극지와 미생물 갈래를 새로 열었어요. 27판도 열 갈래에 하나씩 열 개를 더했고, 철도와 필름·영화 갈래를 새로 열었어요. 28판은 주제 표가 수학 쪽으로 바뀐 첫 판이라 이름 있는 수학 대상 열 개를 열 갈래에 하나씩 더했고, 수론 갈래를 새로 열었어요. 29판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 첫 판이라, 이름 있는 수학 대상 여섯과 화장품 매장 벽에 틀 질감 넷을 더했고, 위상·매듭과 화장품 팝업 갈래를 새로 열었어요. 30판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 매장의 LED 벽에 틀 질감 넷을 화장품 팝업 갈래에 더했어요. 31판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 열지 않았어요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽의 질감 넷을 화장품 팝업 갈래에 더했어요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 41판은 주제 표가 수학·추상 다섯과 할로윈 다섯으로 바뀐 첫 판이라, 수학 대상 다섯을 다섯 갈래에 하나씩 더하고 할로윈 갈래를 새로 열어 10월 31일에 틀 패턴 다섯을 넣었어요. 42판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 41판이 연 할로윈 갈래에 더했어요. 43판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 44판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 45판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 열 개 아래인 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 46판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 47판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 48판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 49판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 50판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 51판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 52판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 53판은 할로윈 몫이 쉬게 된 첫 판이라, 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 54판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 55판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 56판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 57판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 58판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 59판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 60판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 61판도 수학 대상 열 개를 패턴이 적은 갈래부터 열 갈래에 하나씩 더했어요. 62판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 63판은 64판과 같이 주제 표에 「타이포」가 들어온 첫 판이라, 글자가 들어간 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 64판은 주제 표가 바뀌어 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 네 갈래에 더했어요. 65판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 66판도 같은 표로 돌아서 글자 패턴 넷을 글자 갈래에, 하나를 데이터 그림 갈래에 더하고, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 67판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 69판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. [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판에서 세로로 다시 짰어요. @@ -915,6 +915,21 @@ | [Blinking Vortex](patterns/blinking-vortex.js) | 시뮬레이션 | 아레프의 깜박이는 소용돌이 둘이 위아래로 60 LED 떨어져 반 주기씩 번갈아 돌고, 청록과 보라의 부드러운 띠가 늘고 접혀 LED 보다 가늘어지면 한 빛깔로 섞이다가 새 물감이 깔려요. | | [Dupin Cyclide](patterns/dupin-cyclide.js) | 기하·대칭 | 위에서 도는 원환면을 아래의 구면 반전으로 뒤집은 뒤팽 사이클라이드가 같이 돌고, 원환면의 원 28 개가 상에서도 모두 원으로 남아 청록과 장미색 곡률선 두 무리를 이뤄요. | +### 69판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Caustic Letters](patterns/caustic-letters.js) | 글자 | 물결이 거칠 때는 수영장 바닥에 늘 보던 빛 그물이 일렁이다가, 물이 잔잔해지면 같은 빛이 세로로 쌓은 LIGHT, 큰 한 글자 빛, WAVE 로 모이고, 바람이나 떨어진 돌이 다시 흔들면 흩어져 다음 낱말이 와요. | +| [Calculator Words](patterns/calculator-words.js) | 글자 | 패널을 채운 계산기가 1 − .2266 = 같은 작은 셈을 키를 밝히며 쳐 넣고 180도 돌아누우면, 7세그먼트의 0.7734 가 hELLO 로 읽히고 소수점은 낱말 위쪽에 걸려 있다가 다시 돌아와 다음 셈을 쳐요. | +| [Nine-fold Seal](patterns/nine-fold-seal.js) | 글자 | 주홍 인주로 찍은 세로 인영에 한글 여덟 음절이 오른쪽 줄부터 내리읽히고, 가로획마다 한 LED 선으로 겹겹이 접혀 칸을 메웠다가 몇 초마다 민 글자로 펴진 뒤 다음 글로 다시 접혀요. | +| [Boustrophedon](patterns/boustrophedon.js) | 글자 | 빛 한 점이 쟁기처럼 고랑 스무 줄을 갈며 헤시오도스의 두루미 대목을 한 줄은 왼쪽에서 오른쪽으로, 다음 줄은 글자를 거울에 비춘 채 오른쪽에서 왼쪽으로 새기고, 앞에서는 지난번에 써서 식은 글자를 갈아엎어요. | +| [Letter Melt](patterns/letter-melt.js) | 글자 | MELT, 물, FLOW, 녹다, DRIP 이 보이지 않는 거푸집에 담긴 물로 위에 서 있다가, 거푸집이 글자 발끝부터 놓아 주면 획이 늘어지고 방울져 패널 끝까지 흘러내려 바닥에 고이고, 웅덩이에서 실줄기가 빨려 올라가 다음 낱말을 글자 위부터 채워요. | +| [Langford Pairs](patterns/langford-pairs.js) | 수론 | 패널 높이의 블록 탑에 랭퍼드의 짝이 큰 것부터 놓였다 빠지며 짝마다 옆의 호로 이어지고, 찾은 배열은 왼쪽 선반으로 비켜나 5 LED 너비 탑으로 줄지어 서요. | +| [Aharonov-Bohm](patterns/aharonov-bohm.js) | 물결·간섭 | 평면파가 패널을 위에서 아래로 내려오며 3분의 1 높이의 가는 자속관을 지나면, 자기장이 닿지 않는데도 관 뒤 그림자 선 양쪽의 마루가 자속만큼 어긋난 채 어두운 솔기에서 끊기고, 마루 빛깔이 관이 없을 때보다 얼마나 밀렸는지를 보여요. | +| [Penney's Game](patterns/penney-game.js) | 데이터 그림 | 동전이 맨 위 줄에 오른쪽에서 한 칸씩 밀려 들어오다 A 나 B 의 세 수가 오른쪽 끝 세 칸을 채우면 그 판이 한 줄 내려가서, 이긴 세 수가 두 사람이 고른 줄 밑에 한 기둥으로 쌓이고 옆의 긴 막대가 콘웨이의 수가 매긴 눈금으로 다가가요. | +| [Whitney-Graustein](patterns/whitney-graustein.js) | 위상·매듭 | 접선 방향을 무지개빛으로 두른 닫힌 곡선이 멈추지 않고 모양을 바꾸며 감은 수대로 칸을 칠하고, Turns 를 돌리면 작은 고리가 뾰족점으로 조여 사라지거나 뾰족점에서 고리가 태어나며 위 다이얼의 회전수가 바뀌어요. | +| [Steiner Symmetrization](patterns/steiner-symmetrization.js) | 선·궤적 | 초승달, 구멍 난 달걀, 일곱 뿔 별, 떨어진 두 덩이, 빗이 한 LED 현의 빗금으로 그려지고, 현마다 제 박자로 미끄러져 가운데가 흐린 축에 오고 틈으로 갈렸던 현은 한 줄로 붙은 뒤, 빗금이 새 축으로 돌아 다시 미끄러지는 동안 아래 막대가 같은 넓이 원의 눈금까지 줄어요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -3661,6 +3676,70 @@ Lab 에서 해 보기: Text 레이어를 패턴 아래에 두고 sans-serif 로 주제 5 「수학 · 추상」으로 뒤팽 사이클라이드를 그렸어요. 샤를 뒤팽(Charles Dupin)이 1822년에 다룬 이 곡면은 곡률선 두 무리가 모두 원이에요. 원환면을 구면 반전으로 뒤집으면 이 곡면이 되고, 원기둥이나 원뿔을 뒤집어도 돼요. 패널 위에서는 원환면이 서서 돌고, 아래에서는 그 원환면을 밑에 둔 반전 중심으로 뒤집은 상이 같은 각으로 돌아요. 원환면의 자오선 18 개와 위선 10 개가 반전 뒤에도 원이라 코드는 상의 세 점으로 원을 정하는데, 노브 35 자리에서 351,000 점을 직접 반전해 맞대 보니 그 원에서 가장 멀리 벗어난 점도 4.6×10⁻¹⁴ 였어요. Shape 노브가 관의 반지름을 중심원 반지름의 0.5 배에서 1.6 배까지 키우면 1 배에서 구멍이 한 점으로 닫혀 뿔 사이클라이드가 되고, 그 뒤로는 곡면이 제 몸을 뚫는 방추 사이클라이드가 돼요. Distance 노브는 원환면과 반전 중심 사이를 바깥 반지름의 1.2 배에서 29 배까지 벌려요. 처음 자리 0.06 에서는 상의 관이 아래쪽에서 위쪽의 3.1 배로 굵고, 29 배에서는 1.09 배라 원환면과 거의 같아요. 첫 판은 자오선을 원환면에 고르게 놓아서 상의 가는 쪽과 굵은 쪽의 원 간격이 2.93 배 벌어졌고, 반전이 늘리는 배율의 제곱근을 따라 놓자 두 곡면 다 1.72 배가 됐어요. 잰 값은 움직임 0.541, 촘촘함 0.052, 가장자리 0.284, 복잡도 1.122, 채움 0.305, 색 0.095, 모양 바뀜 0.076 이고, thirds 는 32/29/30, 노브는 0.04, 0.08, 0.07, 0.05 예요. 새로 본 것은 고리 사이클라이드가 이 패널에 홀로 서지 못한다는 거예요. 고리를 마주 보면 너비와 높이가 같아서 64 LED 너비에 맞추면 128 줄의 절반만 차고, 그래서 뒤집기 전 원환면을 위에 같이 세웠어요. +## 69판 + +69판은 68판(#331)과 같이 돌았어요. 앞 바퀴 PR 에는 OWNER, MEMBER, COLLABORATOR 가 쓴 댓글도 리뷰도 없어서 따를 글이 없었어요. 68판이 PR 본문에 먼저 밝힌 열 개와 지도 칸 둘을 읽고 겹치지 않게 골랐어요. 타이포 다섯은 「타이포 실험」의 출발점을 하나씩 맡았어요. Caustic Letters 는 글자와 빛, Calculator Words 는 숫자와 기호 놀이, Nine-fold Seal 은 글자로 짠 무늬, Boustrophedon 은 노브와 글, Letter Melt 는 글자의 몸이에요. 글자 꼴도 다섯이 다 달라요. Caustic Letters 는 대문자 아홉 자와 「빛」의 자모 셋을 선분 좌표로 적어 둥근 펜으로 긋고, Calculator Words 는 7세그먼트 여덟 칸을 돌려 읽어요. Nine-fold Seal 은 2 LED 마디 격자에 초성 11 꼴, 중성 8 꼴, 받침 5 꼴을 곧은 획으로 그렸고, Boustrophedon 은 3×5 대문자 26자를 거울과 반 바퀴 회전으로 써요. Letter Melt 는 13×36 LED 상자에 끝이 둥근 획으로 라틴 글자를, 선분 22 개로 한글 세 음절을 적었어요. 64판이 「다음 판 감」에 적은 물로 녹는 글자를 Letter Melt 에서 해 봤어요. 수학 다섯은 67판까지 든 578개에 없던 대상이고 「낯선 수학 예」에서는 고르지 않았어요. 수론, 물결·간섭, 데이터 그림, 위상·매듭, 선·궤적에 하나씩 넣었어요. 주제 1 은 셋뿐인 칸 일곱 가운데 68판이 고른 가장자리 × 촘촘함 1,2 와 복잡도 × 채움 4,2 를 빼고 가장자리 × 복잡도 4,4 와 가장자리 × 촘촘함 2,4 를 겨눴어요. 주제 4 는 Letter Melt 와 Langford Pairs 예요. + +### Caustic Letters + +주제 7 「타이포」의 「글자와 빛」으로, 글자를 그리는 것이 빛을 꺾는 물의 꼴인 패턴이에요. 마리오스 파파스(Marios Papas)와 보이체흐 야로슈(Wojciech Jarosz)를 비롯한 여섯이 2011년에 낸 「Goal-Based Caustics」와, 율리 슈바르츠부르크(Yuliy Schwartzburg)와 마크 파울리(Mark Pauly)를 비롯한 넷이 2014년에 낸 「High-contrast Computational Caustic Design」은 유리나 물의 겉면을 깎아서 그 밑 바닥에 고른 그림이 빛으로 맺히게 했어요. 2014년 논문은 고른 빛을 그림의 밝기로 옮기는 최적 수송 지도를 풀지만, 이 패턴은 그 자리에 크노테-로젠블랫 재배열(Murray Rosenblatt 1952, Herbert Knothe 1957)을 써요. 먼저 줄마다 더한 밝기의 누적합으로 광자가 내려앉을 높이를 정하고, 그 줄 안의 누적합으로 가로 자리를 정해요. 가장 싼 지도는 아니어도 목표 밝기를 그대로 옮겨요. LED 하나에 광자 넷, 모두 32,768 개가 물 위에서 고르게 출발해서 T(u) 에 물결 기울기만큼 밀린 자리에 쌍선형으로 떨어지고, 1 − exp(−0.13 E) 로 짙은 하늘색에서 옅은 물빛까지 칠해져요. 목표 밝기는 획을 둥근 펜으로 그은 글자에 한 번 흐림을 주고 0.1 의 바닥을 더한 것이에요. 바람은 깊은 물의 분산 w = sqrt(g k) 를 따르는 진행파 넷이고, 파마다 접힘 수 a|k| 가 1.1 이라 겹치는 자리에서 광자가 서로 건너며 빛줄이 생겨요. 둘째 낱말마다 바람 대신 돌이 떨어져 1초에 20 LED 로 퍼지는 고리 물결을 보내요. 한 낱말은 Speed 2 에서 8초를 돌아요. 잰 값은 움직임 0.413, 촘촘함 0.073, 가장자리 0.136, 복잡도 1.788, 채움 0.927, 색 0.162, 모양 바뀜 0.160 이고 thirds 는 92/92/93, 노브는 0.22, 0.24, 0.12, 0.13 이에요. 첫 판은 거친 동안에도 낱말의 지도를 켜 둬서 물결이 낱말을 가로 줄무늬로 문지를 뿐 그물이 서지 않았고, 글자 사이 빈 줄은 광자가 모자라 검은 띠로 남았어요. 그때 움직임이 0.290, 모양 바뀜이 0.092 였어요. 바람이 제 높이의 절반을 넘는 동안은 깎은 겉면이 하나도 비치지 않게 하고 물이 잔잔해지는 만큼만 드러나게 하자 고른 빛 위에 흔한 수영장 그물이 섰고, 움직임 0.401, 모양 바뀜 0.165 가 됐어요. 광자를 바둑판 자리에서 출발시킨 판은 어두운 바닥에 점선 줄이 보여서 LED 4분의 1 칸 안의 정해진 무작위 자리로 옮겼고, 물결을 광자마다 셈하던 것을 65×129 꼭짓점에서 셈해 보간하자 한 프레임이 3.5 ms 에서 2 ms 안팎이 됐어요. 새로 본 것은 Focus 를 1 로 올려 빛이 초점을 지나칠 때 지도의 차례가 보인다는 거예요. 획마다 가는 빛줄로 오므라드는 사이, 글자 사이 빈 자리에 가로 실선이 몇 가닥 지나가요. 높이를 먼저 맞추고 가로를 나중에 맞춘 크노테 지도의 줄 이음매이고, 최적 수송 지도라면 남지 않을 자국이에요. + +Lab 에서 해 보기: Text 레이어를 mask 로 두고 maskInvert 를 켠 뒤 Inter 로 「SEA」를 size 26, sharp 로 가운데에 쓰면, 물에 뜬 글자의 그림자처럼 빛 그물 한가운데 SEA 꼴이 검게 뚫리고, 물이 잔잔해지면 패턴이 모은 낱말이 그 그림자에 반쯤 가려져요. + +기능 감: 코드 레이어가 불러 쓸 획 글꼴이요. 대문자 아홉 자와 빛의 자모 셋을 선분 좌표로 손수 적었는데, ㅊ 의 꼭지를 세로 획으로 그렸더니 패널에서 大 로 읽혀서 가로 획으로 바꿨어요. 자모가 선분 목록으로 있으면 굵기와 크기를 코드가 정하니 거리장이나 빛의 목표 밝기로 바로 쓸 수 있어요. + +### Calculator Words + +주제 7 의 「숫자와 기호 놀이」로, 숫자가 뒤집히면 글자가 되는 놀이를 겨눴어요. 1970년대에 주머니 계산기를 산 사람들은 0.7734 를 쳐서 뒤집으면 hELLO 가 되고 5508 이 BOSS 가 되는 것을 돌려 보며 놀았고, 이 놀이는 쓸 수 있는 글자 B, E, G, h, I, L, O, S 를 따서 BEGHILOS 라고도 불려요. 패널에는 62×126 LED 계산기 한 대가 서요. 위에는 5×11 LED 칸 여덟 개의 7세그먼트 표시창이 있고 꺼진 세그먼트는 흐린 8 로 남아요. 아래에는 4열 5줄에 키 19개가 있어서, 기계가 누르는 키가 한 줄 내려앉으며 빛나요. 셈이 끝나면 계산기가 1.3초 동안 180도 도는데, 모서리가 패널을 벗어나지 않게 63.4도에서 0.447 배까지 줄었다가 116.6도부터 다시 커져요. 코드에 글자 표는 없어요. 표시창은 늘 바로 선 채 그려지고 화면 전체가 LED (x, y) 에서 (63 − x, 127 − y) 로 옮겨 찍히니, 3 의 a, b, c, d, g 획이 돌아 d, e, f, a, g 인 E 가 돼요. 26 수수께끼의 답을 세그먼트 비트를 돌려 읽어 보니 모두 맞았고, 0.7734 는 hELL˙O 로 소수점이 마지막 O 의 왼쪽 위에 걸려요. 7 은 f 획 없이, 9 는 꼬리 d 획을 달아 그려서 돌린 7 이 L, 돌린 9 가 G 가 돼요. Words 노브는 hELLO 부터 155 × 499 × 1000 + 993 = 77345993 인 EGGSHELL 까지의 고전 여섯, hI 와 BEE 같은 짧은 낱말 일곱, 곱셈 하나로 만든 긴 낱말 일곱(967 × 3911 = 3781937 이 LEGIBLE), 1 보다 작은 답 여섯(0.113 × 0.7 = 0.0791 이 IGLO˙O)을 골라요. 첫 판은 Glow 노브가 꺼진 세그먼트와 표시창 아지랑이만 밝혔는데, 둘이 패널의 15% 인 유리창 안에만 있어서 노브 반응이 0.01 이었어요. 표시창 빛이 몸체와 윗줄 키에 내려앉게 하자 0.04 가 됐어요. 잰 값은 움직임 0.491, 촘촘함 0.037, 가장자리 0.307, 복잡도 0.377, 채움 0.609, 색 0.127, 모양 바뀜 0.202 이고 thirds 는 53/81/47, 노브는 0.07, 0.16, 0.17, 0.04 예요. 새로 본 것은 줄 맞춤도 같이 뒤집힌다는 거예요. 계산기는 숫자를 오른쪽에 붙이니 뒤집힌 낱말은 왼쪽에 붙고, 남은 자리의 흐린 8 이 hELLO888 처럼 낱말 뒤에 줄지어 서요. + +Lab 에서 해 보기: Text 레이어에 Silkscreen 으로 hELLO 를 size 8, sharp, 흰색으로 y 16 쯤에 두고 blend 를 add 로 얹으면, 바로 선 계산기가 0.7734 를 칠 때는 글꼴의 hELLO 가 숫자 위에 겹쳐 있다가 뒤집힌 뒤에는 맨 아래 세그먼트 hELLO 와 맨 위 글꼴 hELLO 가 한 화면에서 마주 봐요. + +기능 감: 레이어를 돌리고 줄이는 변환이요. 계산기 얼굴을 버퍼에 따로 그린 뒤 LED 마다 2×2 표본을 쌍선형으로 뽑아 돌려 찍는 코드를 손으로 짰는데, Lab 레이어에 회전과 배율 칸이 있으면 Text 레이어의 낱말도 180도 돌려서 이 놀이를 사람이 친 숫자로 해 볼 수 있어요. + +### Nine-fold Seal + +주제 7 「타이포」와 주제 1 「지도의 빈 칸」을 함께 맡았어요. 주제 7 에서는 「글자로 짠 무늬」 쪽으로, 글자 꼴이 칸을 고르게 메우도록 접히는 서체를 겨눴어요. 구첩전(九疊篆)은 송나라 때부터 관인에 쓰인 전서로, 가로획을 몇 번이고 꺾어 접어 글자가 인면의 네모를 빈틈없이 채우게 했고, 조선의 관인에도 이 서체가 쓰였어요. 아홉은 접는 횟수가 아니라 많다는 뜻이에요. 패널에는 두께 2 LED 테두리와 가운데 세로 칸막이 안에 27×29 LED 칸 여덟이 서고, 오른쪽 줄을 위에서 아래로 읽은 뒤 왼쪽 줄로 넘어가요. 획은 2 LED 간격의 마디 격자(칸마다 14×15)에만 놓여서, 이어지지 않은 두 획 사이에는 늘 한 LED 의 종이가 남아요. 자모는 초성 11 꼴, 중성 8 꼴, 받침 5 꼴을 곧은 획으로 그렸어요. ㅅ 은 전서가 人 을 각지게 펴듯 두 번 밖으로 꺾이는 다리로 그렸고, ㅇ 은 모서리를 깎은 고리라서 ㅁ 과 갈려요. 음절은 유니코드 코드에서 초성, 중성, 종성을 풀어 짜요. 접기는 빈 마디마다 같은 열에서 위나 아래로 가장 가까운 가로획에 주고, 받은 마디를 그 획에서 떨어진 줄 수대로 묶어 오른쪽 끝과 왼쪽 끝을 번갈아 앞 줄에 매달아요. 그러면 가로획 하나가 앞뒤로 접힌 한 가닥 띠가 되고, 가로획이 닿지 않는 마디는 세로획이 좌우로 같은 방법으로 받아요. 글은 「세종어제훈민정음」, 「가르치는바른소리」, 「만수무강하옵소서」, 「천년만년살고지고」 넷이에요. 바뀔 때는 접힌 줄이 바깥 줄부터 띠를 거둬들이며 읽는 차례대로 펴지고, 민 획이 줄어들었다가 다음 음절로 자란 뒤 다시 접혀요. 인주 압력은 6초에 한 바퀴 인면을 돌고, 압력이 떠난 쪽에는 Press 만큼 인주가 덜 묻어요. 주제 1 은 Agate, Dead Leaves, Smalti 셋뿐이던 가장자리 × 복잡도 4,4 칸이에요. 획과 종이를 한 밝기씩으로만 그리고 안티에일리어싱을 두지 않아서 이웃 사이 밝기 차는 0 이거나 156 이고, 복잡도는 가장 밝은 채널은 둔 채 나머지 두 채널을 LED 마다 흔들어 올렸어요. 첫 판은 인주의 G 와 B 를 ±13, 종이를 ±9 흔들어 복잡도가 1.778 이었어요. 인주 ±58, 종이 ±31 에 가장 밝은 채널 ±3 을 더하자 2.403 이 됐어요. 가장 밝은 채널은 3 까지만 흔들어서 같은 재료 이웃끼리의 차가 6 을 넘지 않고, 6 이하의 차는 가장자리 셈에 들지 않아요. 다만 G 와 B 를 따로 흔들면 인주가 주황 점과 분홍 점으로 갈려 보여서, 둘을 한 값으로 같이 흔들자 인주의 결로 읽혔고 복잡도는 2.379 가 됐어요. Speed 반응은 첫 판에 0.02 였는데, 첫 글이 접힌 채 머무는 시간을 2.4초로 줄여 노브를 재는 2~3초 사이에 펴기가 갈리게 하자 0.06 이 됐어요. Press 도 0.02 였고, 얼룩 값을 순위로 펴서 고르게 하자 0.06 이 됐어요. 잰 값은 움직임 0.083, 촘촘함 0.217, 가장자리 1.000, 복잡도 2.379, 채움 1.000, 색 0.392, 모양 바뀜 0.229 이고, thirds 는 100/100/100, 노브는 0.29, 0.06, 0.07, 0.06 이에요. 새로 본 것은 접기가 글마다 다른 먹의 양을 한 밀도로 모은다는 거예요. 민 글자일 때는 네 글에서 칸의 12.4% 에서 15.2% 가 인주인데, 다 접으면 50.4% 에서 52.3% 사이로 모여요. 한 LED 선과 한 LED 틈이 번갈면 절반이 상한이라, 접기가 칸을 채울수록 무슨 글이든 같은 밀도에 닿아요. + +Lab 에서 해 보기: Text 레이어를 패턴 위에 두고 role 을 mask 로 바꾼 뒤 Sans-serif 로 「인」 한 자를 size 110, sharp 로 가운데에 쓰면, 접힌 미로가 큰 「인」 꼴 안에서만 남아요. Plain 을 1 로 올리면 큰 민 글자 안에 작은 민 글자들이 흩어져 서요. + +기능 감: 코드 레이어가 Lab 에서 친 글을 문자열로 받는 길이요. 이 패턴은 자모를 직접 짜서 위의 초성, 중성, 받침으로 된 음절이면 무엇이든 접는데, 글이 코드에 박혀 있어서 제 이름으로 도장을 찍어 보려면 TEXTS 줄을 고쳐야 해요. + +### Boustrophedon + +주제 7 「타이포」의 「노브와 글」로, 글을 읽는 눈의 길을 노브로 바꾸는 패턴이에요. 주제 1 은 셋뿐인 가장자리 × 촘촘함 2,4 칸을 겨눴고, 128줄을 다 써요. 부스트로페돈(boustrophedon)은 「소가 도는 대로」라는 그리스 말로, 한 줄은 왼쪽에서 오른쪽으로 쓰고 다음 줄은 글자를 뒤집어 오른쪽에서 왼쪽으로 쓰는 옛 그리스 비문의 쓰기예요. 기원전 5세기 크레타의 고르튄 법전이 이렇게 새겨졌고, 이스터섬의 롱고롱고 판은 되돌아오는 줄을 180도 돌려 쓰는 역부스트로페돈이에요. 글은 헤시오도스의 『일과 날』 448~451행을 휴 에벌린화이트(Hugh G. Evelyn-White)가 1914년에 옮긴 두루미 대목이에요. 두루미가 쟁기질 철을 알린다는 대목이라 밭을 가는 쓰기와 맞아요. 문장 부호는 빼고 대문자 170자 44낱말만 남겼고, 낱말 사이에 옛 비문의 세 점 구분자를 넣으면 한 바퀴가 214칸이에요. 패널은 한 줄 15자, 6줄 간격 20줄의 300칸 밭이고, 3×5 대문자 26자를 코드에 그렸어요. 고랑은 한 줄에 24씩 오르내리는 삼각형 단면이라 골은 글자 가운데에, 이랑은 줄 사이에 와요. 빛 한 점이 Speed 2 에서 1초에 8자씩 갈며 지나간 칸에 새 글자를 세로줄 하나씩 새기고, 아직 안 간 칸에는 지난번에 써서 식은 글자가 남아서 밭이 늘 차 있어요. 새 글자는 크림빛 금색으로 들어와 30초에 걸쳐 하늘색으로 식어요. Way 노브는 줄 끝에서 쟁기가 도는 법을 바꿔요. 0 은 부스트로페돈이고, -1 은 줄마다 왼쪽에서 쓰느라 쟁기를 들고 왼쪽 끝으로 날아가요. 1 로 올리면 되돌아오는 줄이 거꾸로 서요. 잰 값은 움직임 0.033, 촘촘함 0.321, 가장자리 0.387, 복잡도 2.098, 채움 0.897, 색 0.126, 모양 바뀜 0.030 이고, thirds 는 88/91/89, 노브는 0.10, 0.09, 0.10, 0.19 예요. 첫 판은 글자를 185 에서 140 으로 식히고 구름 그림자가 글자까지 덮어서 촘촘함이 0.205 였어요. 이웃 쌍 가운데 40 을 넘는 단차의 몫은 0.379 였는데 그 평균이 113.5 에 그쳤어요. 글자가 제 빛을 내게 해서 구름이 흙만 덮게 하고, 글자를 226 에서 216 으로만 식히고 골 바닥을 6 으로 내리자 단차 평균이 190.7 이 되어 촘촘함이 0.321 로 칸 안에 들었어요. 그동안 굳은 단차의 몫은 0.379 그대로라 가장자리는 0.38 에 머물렀어요. 글꼴과 칸 배치가 가장자리를 정하고 밝기가 촘촘함을 정한 셈이에요. 글자가 구름을 안 받자 움직임이 0.020 으로 줄어서, 구름 그림자를 38% 까지 짙고 빠르게 하고 쟁기 뒤에 갓 엎은 흙빛을 두어 0.033 으로 올렸어요. 새로 본 것은 거울 줄이 생각보다 잘 읽힌다는 거예요. 이 3×5 글꼴에서 좌우가 같은 글자가 A, H, I, M, N, O, T, U, V, W, X, Y 열둘이라 두루미 대목 170자 가운데 98자(58%)가 거울에 비춰도 그대로예요. 180도 돌려도 같은 글자는 H, I, O, S, X, Z 여섯으로 55자(32%)뿐이라 역부스트로페돈의 거꾸로 선 줄이 훨씬 낯설어요. + +Lab 에서 해 보기: Text 레이어에 Inter 로 O 와 X 를 두 줄로 size 60, sharp 로 가운데에 쓰고 role 을 mask 로 두면 밭이 두 글자 안에서만 보여요. O 와 X 는 좌우로도 위아래로도 대칭이라 Way 를 어디에 둬도 큰 글자는 그대로이고 그 안의 작은 글줄만 뒤집혀요. + +기능 감: Text 레이어에 줄마다 번갈아 좌우를 뒤집거나 180도 돌리는 칸이요. 부스트로페돈을 레이어로 짜려면 한 줄 건너 거울 글자가 있어야 하는데, Text 레이어는 늘 왼쪽에서 오른쪽으로 바로 쓰기만 해서 3×5 대문자 26자와 거울과 회전을 다 코드에 적었어요. + +### Letter Melt + +주제 7 「타이포」의 「글자의 몸」과 주제 4 「세로 64×128」을 같이 맡아, 64판 Letter Sand 의 README 가 다음 감으로 적은 「글자를 입자 유체로 채워 녹이기」를 해 봤어요. Letter Sand 는 낟알이 칸을 옮겨 다니는 모래 놀이이고 네 글자를 세로로 쌓았는데, 여기서는 한 줄 낱말이 위 3분의 1에 서고 몸은 물이에요. 물은 시몽 클라베(Simon Clavet), 필리프 보두앵(Philippe Beaudoin), 피에르 풀랭(Pierre Poulin)이 2005년 「Particle-based Viscoelastic Fluid Simulation」에 낸 이중 밀도 이완으로 움직여요. 입자마다 반지름 h 안의 이웃으로 밀도 (1 − r/h)² 의 합과 가까운 밀도 (1 − r/h)³ 의 합을 세고, 앞의 것은 쉴 때의 밀도에서 벗어난 만큼, 뒤의 것은 그대로 압력이 되어 이웃 쌍을 그 둘을 잇는 선을 따라 밀어요. 표면에서는 앞의 압력이 음수가 되어 입자를 당기니 따로 표면 장력을 넣지 않아도 획 끝이 둥글게 맺혀요. 패널에는 입자 1,048 개가 1.4 LED 간격으로 있고, h 는 3 LED, 이웃은 3 LED 칸 22×43 개의 공간 해시로 찾아요. 낱말마다 거푸집 자리가 녹다 459 개에서 물 588 개까지이고 나머지는 늘 바닥 웅덩이에 있어요. 라틴 글자는 13×36 LED 상자에 끝이 둥근 4.4 LED 획으로, 한글 세 음절은 선분 22 개로 코드에 적었어요. 입자는 점으로 찍지 않고 반지름 2.1 LED 커널로 밀도장에 뿌린 뒤 문턱에서 잘라, 장의 기울기로 왼쪽 위에서 오는 빛과 옅은 테두리를 그리고 빨리 움직이는 물은 테두리 색 쪽으로 밝혀요. Speed 2, Heat 0.06 에서 낱말은 1.8 초 서 있다가 2.4 초 동안 녹고 2.9 초 동안 다시 부어져서 다섯 낱말이 36 초에 한 바퀴 돌아요. 잰 값은 움직임 0.963, 촘촘함 0.044, 가장자리 0.486, 복잡도 0.621, 채움 0.268, 색 0.186, 모양 바뀜 0.158 이고, thirds 는 27/12/40, ms 는 1.8~2.1, 노브는 0.09, 0.12, 0.09, 0.15 예요. 첫 판은 강성을 0.09 와 0.22 로 두어서 웅덩이가 끓었어요. 가만히 있어야 할 웅덩이 입자의 평균 속력이 한 걸음에 0.25~1.2 LED 였고 물보라가 y 60 까지 튀었는데, 0.06 과 0.15 로 낮추자 0.018 LED 로 가라앉았어요. 첫 판은 ms 도 3.7 이었어요. 이웃 찾기와 밀도 셈을 한 고리로 합치고, 거푸집이 잡고 있는 입자끼리는 쌍을 세지 않고, 한 걸음을 작은 고리 여럿으로 나누자 1.8~2.1 이 됐어요. 마지막 것은 V8 이 컴파일한 step 을 8초에 65 번 버리고 느린 코드로 돌아가던 일을 3 번으로 줄였어요. 붓는 길도 처음에는 입자마다 웅덩이에서 제 자리로 곧장 날아가 점구름이 올라갔는데, 글자 폭 16 LED 마다 주둥이 하나를 두고 그 밑에서 실줄기로 올라가게 하자 물로 읽혔어요. 새로 본 것은 실줄기의 굵기가 주둥이마다 맡은 몫을 그대로 보여 준다는 거예요. 물 의 주둥이 넷은 자리 94, 205, 194, 95 개를 같은 1.6 초에 채워서, 가운데 둘은 이어진 줄기로, 바깥 둘은 구슬 꿴 줄로 올라가요. + +Lab 에서 해 보기: Text 레이어를 mask 로 두고 maskInvert 를 켠 뒤 Inter 로 「물」 한 자를 size 60, sharp 로 패널 가운데 3분의 1에 놓으면, 흘러내리는 방울과 올라가는 실줄기가 그 자리를 지날 때만 끊겨서 가운데에 빈 「물」 자가 물줄기로 그려져요. Heat 를 1 로 올리면 줄기가 굵은 덩이로 떨어져서 빈 글자가 더 또렷해요. + +기능 감: 코드 레이어가 Text 레이어의 덮임 비트맵을 거푸집으로 받는 길이요. 그러면 사람이 친 낱말 어느 것이나 이 물로 채울 수 있는데, 지금은 낱말 다섯을 코드에 두고 한글 세 음절을 선분 22 개로 손수 적었어요. + +### Langford Pairs + +주제 5 「수학 · 추상」과 주제 4 「세로 64×128」을 같이 채우는 패턴이에요. C. 더들리 랭퍼드(C. Dudley Langford)는 아들이 색 블록을 두 개씩 쌓는 것을 보다가 빨강 둘 사이에 하나, 파랑 둘 사이에 둘, 노랑 둘 사이에 셋이 끼어 있는 것을 알아채고 1958년 Mathematical Gazette 에 문제로 냈어요. 색이 n 가지면 로이 데이비스(Roy O. Davies)가 1959년에 보인 대로 n 을 4 로 나눈 나머지가 0 이나 3 일 때만 풀려요. 코드와 같은 순서로 따로 세어 보니 뒤집은 것을 하나로 쳐서 3 이 1, 4 가 1, 7 이 26, 8 이 150, 11 이 17,792, 12 가 108,144 였고, 5, 6, 9, 10 은 없었어요. 토랄프 스콜렘(Thoralf Skolem)이 1957년에 다룬 수열은 둘 사이가 k 칸 떨어진 쪽이라 4 가 3, 5 가 5, 8 이 252, 9 가 1,328, 12 가 227,968 이에요. 패널 오른쪽에 10 LED 너비의 탑이 128줄을 다 쓰고 서서, n 이 7 이면 9.1 줄짜리 블록 14 칸이 돼요. 짝은 n 부터 1 까지 위에서부터 빈 자리를 찾아 놓이고, 막히면 하나씩 빠지며 빠진 자리에 흐린 자국이 0.4 초쯤 남아요. 놓인 짝은 두 블록의 가운데를 잇는 반 타원으로 탑 왼쪽에 이어지고, 큰 짝일수록 호가 넓어요. 첫 짝은 위쪽 절반만 시도해서 거울에 비친 배열은 한 번만 나와요. 다 놓이면 0.35 초 멈춘 뒤 그 탑이 너비를 반으로 줄이며 왼쪽 선반으로 미끄러지고, 오래된 탑은 왼쪽 끝으로 밀려 나가요. Speed 2 에서 1초에 40 걸음이고, 2초 동안 아무것도 못 찾으면 1.5초마다 두 배로 빨라져 1초에 30,000 걸음까지 가요. 그래서 놓기 19,781 번의 9 는 15.5 초쯤, 138,452 번의 10 은 23.5 초쯤에 탑의 빈 칸이 빨갛게 깜박이며 실패하고 다음 n 으로 넘어가요. 일정한 40 걸음이었다면 9 하나에 16분이 걸렸을 거예요. 잰 값은 움직임 0.930, 촘촘함 0.096, 가장자리 0.761, 복잡도 0.316, 채움 0.381, 색 0.287, 모양 바뀜 0.268 이고, thirds 는 39/39/35, 노브는 0.10, 0.13, 0.13, 0.10 이에요. 첫 판은 선반의 탑을 0.62 밝기로 두었더니 노란 짝이 올리브색으로, 보라가 거의 검게 읽혀서 0.72 로 올렸고, 색이 0.268 에서 0.287, 움직임이 0.891 에서 0.930 이 됐어요. 새로 본 것은 선반 맨 아랫단이에요. 7 의 26 배열 가운데 8 개는 빨간 1 두 개가 12번째와 14번째 칸에 앉아서, 선반이 차면 아랫단에 빨간 블록이 자주 줄을 서요. + +### Aharonov-Bohm + +주제 5 「수학 · 추상」으로, 장이 없는 곳에서도 퍼텐셜이 파동의 위상을 바꾸는 그림을 겨눴어요. 야키르 아하로노프(Yakir Aharonov)와 데이비드 봄(David Bohm)이 1959년에, 그보다 먼저 베르너 에렌베르크(Werner Ehrenberg)와 레이먼드 시데이(Raymond Siday)가 1949년에, 가는 자속관 옆을 지나는 전자파는 자기장을 만나지 않아도 한쪽이 다른 쪽보다 Φ/Φ0 파장만큼 밀린다고 봤어요. 1980년에 마이클 베리(Michael Berry), 로버트 체임버스(Robert Chambers), 마크 라지(Mark Large), 콜린 업스틸(Colin Upstill), J. C. 월름슬리(J. C. Walmsley)가 이 파동의 파면이 관 뒤에서 찢기는 전위를 그렸고, 소용돌이를 지나는 물결로 같은 모양을 냈어요. 코드는 아하로노프와 봄의 급수, 차수 |m − a| 인 베셀 함수의 합을 LED 마다 정확히 풀어요. 분수 차수 베셀 함수는 밀러의 역방향 점화식으로 구하고 노이만의 합으로 맞춰서, a = 0 에서 평면파와 1e-14 안으로 같아요. 패널에는 파장 7 LED 의 평면파가 1초에 8 LED 씩 내려오고, 42번째 줄 가운데의 흰 점이 자속관이에요. 노브가 여는 반 양자에서는 그림자 선이 정확한 0 이라 50번째 줄 아래로 그 양옆 두 열의 진폭이 0.13 을 넘지 않는 어두운 솔기가 서고, 왼쪽 마루는 초록, 오른쪽 마루는 자홍으로 반 파장 어긋나요. 빛깔은 마루가 관이 없을 때의 파동보다 얼마나 밀렸는지를 색상환에 올린 거예요. 자속 1 에서는 평면파와 1.4e-8 안으로 같아서 관이 없는 것처럼 지나가요. 첫 판은 급수 계수를 i^|m − a| 로 적어서 0 인 선이 관 앞, 파동이 오는 쪽에 섰고, 파장 5 에서 관 뒤 80 LED, 옆으로 25 LED 인 두 점의 밀림 차이가 0.048 바퀴뿐이었어요. 아하로노프와 봄의 계수를 이 방향으로 돌린 (−1)^m (−i)^|m − a| 로 고치자 0 이 그림자 선으로 옮겨 가고 그 차이가 0.452 바퀴가 됐어요. 파장 5 에서 열리던 첫 판은 마루가 LED 줄에 맞춰 굵고 가늘기를 되풀이해서, 범위를 7~16 으로 옮기자 움직임이 2.543 에서 1.781 로, 촘촘함이 0.166 에서 0.122 로 내려왔어요. 잰 값은 움직임 1.781, 촘촘함 0.122, 가장자리 0.524, 복잡도 1.837, 채움 0.438, 색 0.261, 모양 바뀜 0.055 이고 thirds 는 46/43/41, 노브는 0.15, 0.26, 0.25, 0.27 이에요. 새로 본 것은 64 LED 폭이 찢김을 다 담지 못한다는 거예요. 반 양자에서 맨 아랫줄 양 끝의 밀림 차이는 0.5 가 아니라 0.445 바퀴예요. 양 끝이 그림자 선에서 30 LED 인데, 관 뒤 85 LED 에서의 프레넬 폭 √(λx) 가 24 LED 라 회절이 아직 남아서예요. + +### Penney's Game + +주제 5 「수학 · 추상」으로, 확률의 비이행성을 동전 줄로 보여 주는 패턴이에요. 월터 페니(Walter Penney)가 1969년 『Journal of Recreational Mathematics』에 낸 이 놀이는 A 가 앞뒤 세 번의 줄을 고르면 B 가 다른 줄을 고르고, 둘 가운데 하나가 먼저 나올 때까지 동전을 던져요. A 가 무엇을 고르든 B 에게는 더 자주 이기는 답이 있고, A = x1 x2 x3 에 대한 최선은 (x2 의 반대) x1 x2 예요. 승산은 존 호턴 콘웨이(John Horton Conway)의 선행수(leading number)로 셈해요. 마틴 가드너(Martin Gardner)가 1974년 『Scientific American』에 소개한 방법으로, X 의 끝 L 개가 Y 의 앞 L 개와 같을 때 2^(L-1) 자리를 켠 이진수 XY 를 만들면 B 대 A 의 승산이 (AA - AB) : (BB - BA) 예요. 코드는 이 식으로 여덟 줄마다 일곱 상대의 승산을 셈하고, 따로 돌린 20만 판의 시뮬레이션과 0.0013 안에서 맞았어요. 패턴 자체를 Speed 3 에서 한 시간 돌린 집계도 다섯 대진 모두 표준오차 하나 안이었고, HHT 대 THH 는 5,475 판에서 B 가 0.7512 를 가져가 3:1 의 0.75 에 닿았어요. 패널 맨 위 줄에는 금색 앞면과 하늘색 뒷면 동전이 오른쪽에서 1초에 10개씩 밀려 들어오고, 오른쪽 끝 세 칸이 A 나 B 의 줄과 같아지면 그 판이 끝나요. 끝난 판은 밑줄이 A 의 분홍이나 B 의 초록으로 켜진 채 한 줄 내려가고, 판마다 오른쪽 끝을 맞춰 두어서 이긴 세 수가 머리에 그린 A 와 B 의 줄 바로 밑에 한 기둥으로 쌓여요. 오른쪽의 키 큰 막대는 위에서부터 A 가 이긴 몫을 분홍으로, 나머지를 초록으로 칠하고, 흰 눈금이 콘웨이의 수가 매긴 자리에 서요. A 노브의 처음 넷 HHT, THH, TTH, HTT 는 저마다 앞 것의 최선 답이고 HTT 의 최선 답이 다시 HHT 라서, 한 칸씩 돌리면 방금 이긴 줄이 다음 대진에서 지는 쪽에 서요. 잰 값은 움직임 0.403, 촘촘함 0.108, 가장자리 0.999, 복잡도 0.084, 채움 0.258, 색 0.229, 모양 바뀜 0.028 이고, thirds 는 21/27/28, 노브는 0.09, 0.07, 0.08, 0.07 이에요. 첫 판은 이긴 세 수 밖의 동전을 0.42 로 어둡게 두어 금색 앞면이 갈색에 가까운 올리브로 보였어요. 0.62 로 올리자 0.35 배로 둔 동전 모서리가 37 에서 55 로 올라 부드러운 단차가 굳은 단차로 바뀌어서 가장자리가 0.865 에서 0.999 로, Speed 반응이 0.05 에서 0.07 로 움직였어요. 새로 본 것은 A 가 이긴 줄의 생김새예요. 처음 두 번이 앞면이 아니면 HH 가 처음 나오는 자리 바로 앞에 늘 T 가 있어서 THH 가 HHT 보다 먼저 서요. 그래서 HHT 대 THH 에서 분홍 밑줄이 붙은 줄은 하나같이 금색만 이어지다 하늘색 하나로 끝나고, 처음 두 번이 앞면일 확률 1/4 이 바로 콘웨이의 1:3 이에요. HHH 를 고르면 분홍 줄은 모두 꼭 세 칸이에요. + +### Whitney-Graustein + +주제 5 「수학 · 추상」으로, 곡선의 회전수(turning number)를 위상·매듭 갈래에 세웠어요. 해슬러 휘트니(Hassler Whitney)는 「On regular closed curves in the plane」(1937)에서 평면의 정칙 닫힌 곡선 둘이 정칙 곡선만 거쳐 서로 바뀔 수 있는 것은 접선이 도는 횟수가 같을 때뿐이라는 것을 보였고, 이 정리를 윌리엄 캐스퍼 그라우스타인(William Caspar Graustein)의 몫으로 돌렸어요. 코드는 곡선을 접선각 θ(t) = kt + p(t) 로 적어요. p 는 한 번, 두 번, 세 번 도는 푸리에 항의 크기와 위상이 천천히 떠도는 주기 함수예요. 속력 r = exp(Re(β̄ e^{iθ})) 는 어떤 β 에서도 양수이고, β 는 r 의 합이라는 볼록 함수의 최소점이에요. 그 기울기가 곡선이 남길 틈이라서, 뉴턴 걸음 몇 번으로 틈이 0 이 되는 β 를 찾으면 p 가 어떻게 바뀌어도 곡선은 닫히고 멈추지 않아요. k = 0 은 θ = 2.405 sin t 인 8자로 여는데, 2.405 는 베셀 함수 J0 의 첫 영점이라 이 8자는 속력을 고치지 않아도 닫혀요. 칸은 줄마다 곡선이 지나는 방향을 세어 감은 수로 칠하고, 1 과 2 는 보라와 연보라, -1 은 초록이에요. 위 띠의 다이얼은 접선 색상환 위에서 흰 표지의 접선 방향을 가리켜서, 표지가 곡선을 한 바퀴 도는 동안 k 바퀴 돌아요. 반을 넘을 때는 고리의 표준형 (u², u³ − su) 의 접선각을 곡선에 얹고 s 를 -2 에서 2 로 옮겨요. 혹이 날카로워져 s = 0 에서 뾰족점이 되면 그 자리가 흰빛으로 번쩍이고, 이어서 작은 고리가 생겨 그 안에 감은 수 2 의 칸이 열려요. 고리의 한 바퀴는 폭이 점점 넓어지는 감싼 코시 분포의 누적 함수로 곡선 전체에 퍼져 새 반의 곡선이 돼요. 첫 판은 속력을 1 + Re(β̄ e^{iθ}) 로 두고 β 를 2×2 연립방정식으로 풀었는데, 8자 끝에 고리를 얹을 때 |β| 가 1.47 까지 올라 속력이 음수가 되고 위 고리가 가시처럼 오그라들었어요. 속력을 늘 양수인 지수 꼴로 바꾸자 이번에는 -1 에서 0 으로 갈 때 접선이 반 바퀴도 쓸지 못해 어떤 속력으로도 닫히지 않는 순간이 드러났어요. 0 반 옆에서는 고리를 t = 0 에 두어 퍼지는 바퀴가 b sin t 의 흔들림에 더해지게 하자, Wobble 0 과 1 에서 잰 열네 가지 전이가 모두 닫혔어요. 잰 값은 움직임 0.231, 촘촘함 0.045, 가장자리 1.000, 복잡도 0.162, 채움 0.412, 색 0.186, 모양 바뀜 0.095 이고, thirds 는 28/48/47, 노브는 0.08, 0.09, 0.11, 0.05 예요. 새로 본 것은 1 에서 0 으로 내려가는 길이 올라가는 길보다 곡선을 훨씬 세게 비튼다는 거예요. 0 에서 1 로 갈 때 |β| 는 1.12 에 그치는데, 1 에서 0 으로 바퀴를 모을 때는 흔들림을 다 꺼도 1.39 까지 올라 한 구간이 다른 구간보다 16배 빨리 달리고, 곡선이 V 자로 꺾였다가 8자로 돌아와요. + +### Steiner Symmetrization + +주제 5 「수학 · 추상」으로, 모양을 원으로 다듬는 가장 오래된 손놀림을 빗금 한 장으로 보여 주려 했어요. 야코프 슈타이너(Jakob Steiner)는 1838년 Crelle 지에 낸 등주 문제 증명에서, 축을 하나 정하고 축에 수직인 현을 모두 제 길이 그대로 축 가운데로 옮기는 대칭화를 썼어요. 한 직선 위에 현이 여럿이면 길이를 더한 현 하나가 돼요. 넓이는 그대로이고 둘레는 늘지 않아요. 축을 바꿔 가며 되풀이하면 원으로 간다는 것은 빌헬름 그로스(Wilhelm Gross)가 1917년에 방향의 차례를 골라 보였고, 거기 쓰인 수렴 부분열은 빌헬름 블라슈케(Wilhelm Blaschke)가 1916년 『Kreis und Kugel』에서 낸 선택 정리에서 와요. 축 몇 개만 돌려 쓰면 그 축마다 거울 대칭인 모양에서 멈춘다는 것은 대니얼 클레인(Daniel Klain)이 2012년에 보였어요. 패널에는 1 LED 간격 현 144 개가 축을 따라 서고, 모양은 점의 고리로 들고 있다가 현마다 고리와 만나는 점을 정렬해 둘씩 묶어 조각을 얻어요. 조각들은 제 차례대로 쌓여 가운데 맞춘 현 하나가 되고, 현마다 축을 따라 0.45 초에 걸쳐 늦게 출발하며 0.56~0.84 초 동안 미끄러져서 빗금 더미에 층밀림 물결이 지나가요. 새 모양은 가운데 맞춘 현 끝을 잇는 고리이고, 1 LED 로 곡선을 샘플링하며 잃는 넓이는 길이를 그만큼 늘려 되돌려요. Axes 0 에서 축은 반 바퀴의 황금각 68.75 도씩 돌아요. 초승달의 둘레는 원의 1.520 배에서 1.371, 1.020, 1.013, 1.006, 1.004 배로 다섯 번에 들어오고, 구멍 난 달걀은 1.569 배에서 여덟 번 만에 1.005 배가 돼요. 잰 값은 움직임 0.361, 촘촘함 0.084, 가장자리 0.902, 복잡도 0.228, 채움 0.326, 색 0.289, 모양 바뀜 0.184 이고, thirds 는 11/71/14, ms 는 0.5~0.7, 노브는 0.14, 0.11, 0.14, 0.06 이에요. 첫 판은 새 고리의 꼭짓점이 현과 정확히 같은 높이에 놓여서 반올림으로 꼭짓점 하나가 두 번 세어졌고, 현 24 개가 홀수 개의 교점을 얻어 모양을 검은 줄무늬가 가로질렀어요. 현의 두 끝 검사를 같은 s 값으로 하자 사라졌어요. 떨어진 두 덩이는 고리 하나로 이어지며 가는 목이 생겨서 첫 대칭화에 둘레가 255.3 에서 262.5 로 늘었는데, 빈 현에서 고리를 끊자 255.6 이 됐어요. Axes 노브는 0.03 으로 약했는데, 처음 쉼을 0.6 초에서 0.15 초로 줄이고 가장 긴 미끄럼을 1.59 초에서 1.37 초로 당겨 둘째 미끄럼이 2~3 초 창에 들어오자 0.06 이 됐어요. 새로 본 것은 빗이에요. 세로 축의 첫 대칭화는 가로 현이 모두 한 조각이라 E 꼴 빗을 王 자 꼴로 가운데 맞출 뿐이고 둘레는 432.0 에서 424.2 로 2% 줄어요. 158.8 도의 둘째 축에서 이 셋을 가로지르는 현이 붙으면서 둘레가 243.5 로 한 번에 43% 줄어요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -3795,6 +3874,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 67판은 66판까지 든 558개에 빈 칸도 둘뿐인 칸도 없어서, 셋뿐인 칸 열하나 가운데 같이 돈 65판(#328)이 고른 촘촘함 × 색 4,4 와 색 × 모양 바뀜 4,4 를 빼고 복잡도 × 모양 바뀜 4,4 와 가장자리 × 움직임 1,4 를 겨눴어요. 복잡도 × 모양 바뀜 4,4 는 복잡도가 2.171 을 넘으면서 모양 바뀜이 0.328 을 넘는 칸이에요. 모양 바뀜 축의 끝은 Cloth 의 0.413 이라 이 칸은 0.328 에서 0.413 사이의 좁은 띠예요. Radon Transform 은 광자를 세는 잡음으로 사이노그램과 재구성을 deflate 가 줄이지 못하는 낟알로 채우고, 한 바퀴가 끝날 때 두 칸을 함께 어둡게 해서 블록 평균을 통째로 옮겨요. 첫 판은 복잡도 1.866, 모양 바뀜 0.167 이었고, 검정까지 지운 판은 0.488 로 축 끝을 넘어 모든 칸의 경계를 옮겼어요. 빛의 15% 에서 멈추자 복잡도 2.333, 모양 바뀜 0.375 가 됐어요. 가장자리 × 움직임 1,4 는 40 을 넘는 계단의 몫이 0.210 아래이면서 움직임이 1.529 를 넘는 칸이에요. 가장자리는 가장 밝은 채널로만 재서, Blinking Vortex 는 물감 두 색을 파랑이 가장 밝은 청록과 보라로 골라 색이 바뀌는 자리에 밝기 계단을 두지 않고, 밝기는 띠마다 코사인으로 오르내리게 했어요. 그것만으로는 가장자리 0.027, 움직임 0.263 으로 너무 고요해서, 반 주기를 0.45초로 줄이고 물감을 12/μ 반 주기마다 새로 깔자 가장자리 0.129, 움직임 1.952 가 됐어요. 다 섞인 물은 LED 마다 같은 평균색이라 아무리 빨리 돌아도 움직임에 들지 않았어요. 열 개 다 일곱 축의 끝 안에 있어서 칸 경계는 60판 그대로예요. 66판까지 든 558개에 67판 열 개만 더한 568개에서 지도 21장은 다 `none` 이고, 셋뿐인 칸은 열하나에서 아홉으로 줄었어요. 겨눈 두 칸은 둘 다 넷이 됐어요. 먼저 머지된 65판까지 든 578개에서는 셋뿐인 칸이 일곱이에요. +69판은 67판까지 든 578개에 빈 칸도 둘뿐인 칸도 없어서, 셋뿐인 칸 일곱 가운데 같이 돈 68판(#331)이 고른 가장자리 × 촘촘함 1,2 와 복잡도 × 채움 4,2 를 빼고 가장자리 × 복잡도 4,4 와 가장자리 × 촘촘함 2,4 를 겨눴어요. 가장자리 × 복잡도 4,4 는 40 을 넘는 계단의 몫이 0.79 를 넘으면서 복잡도가 2.171 을 넘는 칸이에요. Nine-fold Seal 은 획과 종이를 한 밝기씩으로만 칠하고 안티에일리어싱을 두지 않아서 이웃 사이 밝기 차가 0 이거나 156 이라 가장자리가 1.000 이에요. 복잡도는 가장 밝은 채널을 3 까지만 흔들고 나머지 두 채널을 LED 마다 크게 흔들어 올렸어요. 첫 판은 인주의 G 와 B 를 ±13, 종이를 ±9 흔들어 1.778 이었고, 인주 ±58, 종이 ±31 로 키우자 2.403 이 됐어요. G 와 B 를 따로 흔들면 인주가 주황 점과 분홍 점으로 갈려서 한 값으로 같이 흔들었고, 그 판이 2.379 예요. 종이를 LED 마다 켜 두어 채움이 1.000 인데, 그 축의 끝이 이미 1.0 이라 축을 늘리지 않아요. 가장자리 × 촘촘함 2,4 는 가장자리가 0.21 에서 0.50 사이이면서 촘촘함이 0.297 을 넘는 칸이에요. 굳은 계단은 글자가, 무른 계단은 고랑이 맡아야 해요. Boustrophedon 은 고랑을 한 줄에 24씩 오르내리는 삼각형 단면으로 깎아 이웃 쌍 대부분이 7 에서 40 사이의 무른 계단이고, 3×5 글자의 테두리가 굳은 계단의 몫 0.38 을 정해요. 첫 판은 글자를 185 에서 140 으로 식히고 구름 그림자가 글자까지 덮어 굳은 계단의 평균이 113.5 에 그쳐서 촘촘함이 0.205 였어요. 글자가 제 빛을 내고 구름이 흙만 덮게 하고, 글자를 226 에서 216 으로만 식히고 골 바닥을 6 으로 내리자 평균이 190.7 이 되어 촘촘함 0.321, 가장자리 0.387 로 칸 안에 들었어요. 글꼴과 칸 배치가 가장자리를 정하고 밝기가 촘촘함을 정한 셈이에요. 열 개 다 일곱 축의 끝 안에 있어서 칸 경계는 60판 그대로예요. 69판까지 든 588개에서 지도 21장은 다 `none` 이고, 셋뿐인 칸은 일곱에서 다섯이 됐어요. + ### 아직 없는 갈래 `families.json` 의 쉰여섯 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 29판이 연 위상·매듭 갈래는 Hopf Fibration 과 31판의 Boy's Surface, 33판의 Borromean Rings, 34판의 Lakes of Wada, 36판의 Heawood Torus, 38판의 Sperner's Lemma, 39판의 Borsuk-Ulam, 44판의 Ham Sandwich, 46판의 Reeb Graph, 48판의 Möbius Energy, 49판의 Square Peg, 51판의 Persistence Barcode, 53판의 Belt Trick, 55판의 Universal Cover 예요. 28판이 열려다 접은 갈래라 28판의 Hairy Ball 은 노이즈·흐름에 그대로 있어요. 세잎 매듭은 48판의 Möbius Energy 가 처음 세웠어요. 29판은 화장품 팝업 갈래도 열었어요. 주제 6 의 몫이라 주제 2 로 세지 않았고, Cream Swirl, Serum Gel, Glitter Fall, Loose Powder 넷이 다 들어 있어요. 30판의 Swatch Row, Pearl Sheen, Bi-Phase, Satin Fold 가 그 갈래에 넷을 더했고, 31판의 Lip Gloss, Foam Lather, Setting Mist, Sponge Blend 가 넷을 더 더해 열둘이에요. 32판의 Cushion Mesh, Clay Mask, Sugar Scrub, Chrome Powder 가 넷을 더해 열여섯이고, 33판의 Cleansing Balm, Gold Leaf, Essence Drop, Peel-Off 가 넷을 더해 스물이에요. 34판의 Cat Eye, Velvet Nap, Mood Tint, Hair Gloss 가 넷을 더해 스물넷이고, 35판의 Bath Fizz, Toner Pad, Sheet Mask, Gilded Cream 이 넷을 더해 스물여덟이고, 36판의 Lotion Ribbon, Shimmer Oil, Blooming Gel, Whipped Soap 이 넷을 더해 서른둘이에요. 38판의 Milk Ring, Couette Shimmer, Photochromic, Serum Fingers 가 넷을 더해 서른여섯이에요. 39판의 Soap Film, Blotting Paper, Serum Drip, Crackle Polish 가 넷을 더해, 37판의 넷까지 마흔넷이에요. 41판은 할로윈 갈래를 열었어요. 주제 6 의 몫이라 주제 2 로 세지 않았고, Jack-o'-Lantern, Sheet Ghosts, Spider Drop, Cauldron, Eyes in the Dark 다섯이 들어 있어요. 28판이 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table, 33판의 Goldbach Comet, 35판의 Kolakoski, 36판의 Elliptic Chords, 39판의 Explicit Formula, 44판의 Bulgarian Solitaire, 48판의 Chebyshev's Bias, 49판의 Curlicue, 51판의 Sato-Tate, 53판의 Gaussian Periods, 55판의 Question Mark Function 이에요. 27판이 연 철도 갈래는 Funicular 하나이고, 필름·영화 갈래는 Horse in Motion 하나예요. 두 갈래는 주제 표가 수학 쪽으로 바뀌기 전에 열었어요. 그 뒤로 ROUND.md 는 사물로 묶는 갈래를 더 열지 않아요. 26판이 연 얼음·극지 갈래는 Iceberg 와 27판의 Sled Dogs 이고, 미생물 갈래는 Dark Field 와 27판의 Mitosis 예요. 25판이 연 농사·밭 갈래는 Rice Terraces 와 26판의 Rice Planting, 27판의 Center Pivot 이고, 화산 갈래는 Eruption 과 26판의 Geyser, 27판의 Lava Lake 예요. 24판이 연 공사·건설 갈래는 Tower Crane 과 25판의 Excavation, 26판의 Implosion 이에요. 24판이 연 도자기 갈래는 Potter's Wheel 과 25판의 Kintsugi, 26판의 Noborigama 예요. 23판이 연 무대·공연 갈래는 Proscenium 과 24판의 Flying Trapeze, 25판의 Marionette, 26판의 Shadow Play 예요. 23판이 연 짐승 갈래는 Giraffe 와 24판의 Emergence, 25판의 Snow Monkeys 예요. 22판이 연 몸 갈래는 Circulation 과 23판의 Axon, 24판의 Breath, 27판의 Fluoroscope 예요. 22판이 연 비행 갈래는 Liftoff 와 23판의 Final Approach, 24판의 Thermal, 25판의 Skydiver 예요. 20판이 연 새 갈래는 Kingfisher 와 21판의 Woodpecker, 22판의 Weaver, 23판의 Skein 이에요. 20판이 연 화학·실험 갈래는 Chromatography 와 21판의 Chemical Garden, 22판의 Flame Test, 23판의 Elephant Toothpaste 예요. 21판이 연 종이 갈래는 Pop-up Book 과 22판의 Senbazuru, 23판의 Hanging Scroll, 26판의 Origami 예요. 21판이 연 운동·경기 갈래는 Lap Pool 과 22판의 Bowling Lane, 23판의 High Dive, 26판의 Pole Vault, 27판의 Climbing Wall 이에요. 위아래로 움직이는 경기는 High Dive 와 Pole Vault 뒤로 27판에 실내 암벽이 섰어요. 19판이 연 부엌 갈래는 Popcorn 과 20판의 Kettle, 21판의 Pancakes 이고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판이 연 유리 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori, 27판의 Glassblower 예요. 광물·결정 갈래는 Bismuth 와 19판의 Dripstone, 20판의 Agate 이고, 놀이공원 갈래는 Drop Tower 와 19판의 Helter Skelter, 20판의 High Striker, 25판의 Ferris Wheel 이에요. 말이 오르내리는 회전목마는 아직 없어요. 17판이 연 전자 회로 갈래는 Circuit Board 와 18판의 Server Rack, 21판의 Nixie 이고, 장난감 갈래는 Marble Run 과 18판의 Peg Light, 21판의 Slinky, 22판의 Pin Art 예요. 16판이 연 바다 갈래는 Abyss 와 17판의 Shoreline, 20판의 Sunglint, 23판의 Bait Ball 이고, 곤충 갈래는 Fireflies 와 17판의 Ant Farm, 20판의 Moth Lamp, 24판의 Waggle Dance 예요. 15판이 연 도시 갈래는 Metro Map 과 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판이 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables, 24판의 Pipe Organ 이에요. 4판이 연 직물 갈래는 Jacquard Loom 과 15판의 Plait, 19판의 Needlepoint, 23판의 Sequins 이고, 8판이 연 기계 갈래는 Gear Train 과 15판의 Cam Wave, 19판의 Paternoster 예요. 12판이 연 천체 갈래는 Star Trails 와 15판의 Analemma, 16판의 Lunation, 22판의 Galilean Moons 예요. 위성의 궤도는 Galilean Moons 가 그렸고, 해를 도는 행성은 아직 없어요. 리듬·펄스 갈래는 1판의 Euclid Pulse 와 Pulse Bars 뒤로 20판의 Change Ringing, 32판의 Arnold Tongues, 35판의 Chimera State, 38판의 Infinity Series, 41판의 Siteswap, 44판의 Kac Ring, 46판의 Stochastic Resonance 가 섰어요. @@ -3940,9 +4021,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 67판에서는 Radon Transform 과 Artin Braids 가 128줄을 써요. Radon Transform 은 위 64×64 칸에 재구성이, 아래 64×64 칸에 사이노그램이 서고 광자 잡음이 빈 바탕까지 낟알로 덮어서 thirds 가 95/94/87 이에요. Artin Braids 는 위 30줄 띠 아래 98줄이 점들의 자취라, setup 에서 101 줄을 미리 그어 첫 프레임부터 가닥이 바닥까지 내려와서 24/36/35 이고, 띠 테두리 두 줄을 빼면 20/36/35 예요. 글자 다섯 가운데 Distance Field 는 56×116 LED 쯤 되는 글자 하나가 서서 71/64/71 이고, Hanging Line 은 탈네모꼴 글줄 사이가 비어 13/16/9 로 가장 엷어요. +69판에서는 Letter Melt 와 Langford Pairs 가 128줄을 써요. Letter Melt 는 낱말이 위 3분의 1에 서고 웅덩이가 바닥에 고이며 그 사이를 방울과 실줄기만 지나가서 thirds 가 27/12/40 이에요. 가운데가 엷은 것은 녹는 2.4초와 붓는 2.9초 동안만 물이 그 자리를 지나서예요. Langford Pairs 는 오른쪽 탑이 128줄을 다 쓰고 왼쪽 선반의 탑도 같은 높이라 39/39/35 예요. 글자 다섯 가운데 Boustrophedon 은 고랑 스무 줄이 밭을 늘 채워 88/91/89 이고, Nine-fold Seal 은 종이를 LED 마다 켜 두어 100/100/100 이에요. Steiner Symmetrization 은 모양이 가운데로 모여 둥글어지니 11/71/14 로 위아래가 가장 엷어요. + ### 낯선 수학 -주제 5 로 쓴 수학과 다음에 해 볼 수학이에요. 28판이 처음 열 개를 썼어요. 슈람-뢰브너 진화(Loewner Trace), 포드 원(Ford Circles), 털 난 공 정리(Hairy Ball), 아즈텍 다이아몬드와 북극권(Aztec Diamond), 부니모비치 경기장(Stadium Billiard), 라우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 다면체와 E8(Gosset Polytope), 콘웨이의 보고 말하기 수열(Look and Say), 위토프 게임의 그런디 값(Wythoff Nim), 쌍곡 평면의 띠 모형(Hyperbolic Band)이에요. 29판은 여섯을 더했어요. 클라인의 j 불변량(J-Invariant), 호프 올뭉치(Hopf Fibration), 다이슨 브라운 운동(Dyson Gas), 가케야 바늘 문제의 페론 나무(Perron Tree), 임계선 위의 리만 제타 함수(Zeta Loops), 길버트-섀넌-리즈 리플과 베이어-디아코니스의 일곱 번(Seven Shuffles)이에요. 30판도 여섯을 더했어요. 세 간격 정리(Three Gaps), 호프스태터 나비와 TKNN 정수(Hofstadter Butterfly), 노이만의 외부 당구(Outer Billiards), 볼드의 탄도 퇴적과 KPZ 앞면(Ballistic Deposition), 아르놀트의 미앤더(Meander), 브로드벤트와 해머슬리의 스미기(Percolation)예요. 31판도 여섯을 더했어요. 마스킷 조각의 클라인 군 극한 집합(Indra's Pearls), 리틀우드 다항식의 근(Littlewood Roots), 고든의 가우스 해자(Gaussian Moat), 아르놀트의 고양이 사상(Cat Map), 자부스키와 크루스칼의 KdV 솔리톤(KdV Solitons), 보이 곡면(Boy's Surface)이에요. ROUND.md 의 「낯선 수학 예」 여덟 가운데 쓴 것은 슈람-뢰브너 진화와 아즈텍 다이아몬드, 모듈러 형식, 호프 올뭉치, 세 간격 정리, 외부 당구 여섯이었고, 31판이 리틀우드 다항식의 근과 클라인 군의 극한 집합을 써서 여덟을 다 썼어요. 32판도 여섯을 더했어요. 탤벗 효과와 양자 카펫(Talbot Carpet), 길브레스 추측의 표(Gilbreath Table), 원 사상의 아르놀트 혀(Arnold Tongues), 콘웨이와 라딘의 핀휠 타일링(Pinwheel Tiling), 윌킨슨과 윌렘센의 침입 스미기(Invasion Percolation), 울람의 문제와 해머슬리 과정(Hammersley Lines)이에요. 33판도 여섯을 더했어요. 에인절, 홀로이드, 로믹, 비라그의 무작위 정렬 네트워크(Sorting Network), 베르트랑의 역설(Bertrand Paradox), 펜로즈의 비출 수 없는 방(Penrose Room), 당들랭 구(Dandelin Spheres), 테이트의 보로메오 고리(Borromean Rings), 골드바흐 혜성(Goldbach Comet)이에요. 34판도 여섯을 더했어요. 톰슨 문제와 오일러가 남기는 다섯 이웃 열둘(Thomson Problem), 켈빈의 배 물결(Kelvin Wake), 요네야마가 낸 와다의 호수(Lakes of Wada), 샤이데거의 강 그물(Scheidegger Rivers), 모저의 소파 문제와 해머슬리의 소파(Moving Sofa), 크누스의 춤추는 링크로 푼 펜토미노 덮기(Pentomino Cover)예요. 35판도 여섯을 더했어요. 뢸로의 폭이 일정한 곡선(Constant Width), 렌니의 주차 문제(Rényi Parking), 카리의 비주기 왕 타일(Kari-Culik Tiles), 콜라코스키 수열(Kolakoski), 쿠라모토와 바톡토흐의 키메라 상태(Chimera State), 주콥스키 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했어요. 그레이슨의 정리가 말하는 곡선 단축 흐름(Curve Shortening), 헤우드의 일곱 색 토러스 지도(Heawood Torus), 슈바르츠 랜턴(Schwarz Lantern), 윌브러햄과 깁스의 현상(Gibbs Phenomenon), 타원 곡선의 현과 접선(Elliptic Chords), 지먼의 파국 기계(Catastrophe Machine)예요. 37판도 여섯을 더했어요. 포천의 쓸기 선 알고리즘(Fortune's Sweep), 앙투안의 목걸이(Antoine's Necklace), 카르만 소용돌이 길의 안정성(Kármán Street), 단단한 원판의 헥사틱 상(Hexatic Phase), 존슨-멜 쪽매와 아브라미 법칙(Johnson-Mehl), 쿠라모토-시바신스키 방정식(Kuramoto-Sivashinsky)이에요. 38판도 여섯을 더했어요. 앤더슨 국소화(Anderson Localization), 무어가 찾고 Chenciner 와 Montgomery 가 증명한 세 몸 문제의 8자 해(Figure-Eight Orbit), 예센과 빈트너, 에르되시의 베르누이 합성곱(Bernoulli Convolutions), 브라우어르의 고정점 정리를 증명하는 슈페르너 보조정리(Sperner's Lemma), 에겐베르거와 폴리아의 항아리(Pólya Urn), 뇌르고르의 무한 수열(Infinity Series)이에요. 39판도 여섯을 더했어요. 고든, 웹, 월퍼트의 등스펙트럼 북(Isospectral Drums), 폰 망골트의 꼴로 쓴 리만의 명시 공식(Explicit Formula), 요한 베르누이의 최단 강하선(Brachistochrone), 슈타이너의 포리즘(Steiner Chain), 모런 집단으로 그린 킹먼의 합류(Kingman Coalescent), 보르수크-울람 정리(Borsuk-Ulam)예요. 40판도 여섯을 더했어요. 앨더스의 브라운 연속 랜덤 나무(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 함수(Hydrogen Orbital), 젤도비치 근사의 팬케이크(Zeldovich Pancakes), 망델브로가 내다보고 롤러, 슈람, 베르너가 증명한 브라운 운동의 바깥 경계(Brownian Frontier), 복소 긴즈부르크-란다우 방정식의 결함 난류(Defect Turbulence), 루퍼트 왕자의 정육면체(Prince Rupert's Cube)예요. 41판은 다섯을 더했어요. 홀과 왜건의 현수선 길과 정다각형 바퀴(Square Wheels), 퐁슬레의 포리즘(Poncelet Porism), 슐레플리의 4차원 정다포체를 자른 단면(Polytope Slices), 클림켁과 티먼, 마그누손의 사이트스왑(Siteswap), Dudeney 의 잡화상 퍼즐과 월리스-보여이-게르비엔 정리(Hinged Dissection)예요. 42판은 다섯을 더했어요. 바른스도르프 규칙으로 도는 기사 여행(Knight's Tour), 페르미와 파스타, 울람, 칭구의 되돌아옴(FPUT Recurrence), 포슬리에-립킨 반전기(Peaucellier Linkage), 몰리의 삼등분선 정리(Morley Triangle), 랄과 마드라스, 소칼의 피벗 알고리즘으로 돌린 자기 회피 걸음(Pivot Walk)이에요. 43판은 다섯을 더했어요. 월리스 워커의 칼레이도사이클(Kaleidocycle), 쌍곡형에서 록소드롬형으로 감기는 뫼비우스 변환의 흐름(Möbius Loxodrome), 터트의 용수철 정리(Tutte Embedding), 디펀트와 크래비츠의 고리 정리를 품은 히토메자시 사시코(Hitomezashi), 룽게 현상과 체비쇼프 점(Runge Phenomenon)이에요. 44판은 다섯을 더했어요. 마크 카츠의 고리 모형(Kac Ring), 스톤과 튜키의 햄 샌드위치 정리(Ham Sandwich), 브란트의 불가리아 솔리테어(Bulgarian Solitaire), 엘스켄스와 프리시의 탄도 소멸(Ballistic Annihilation), 페레그린의 솔리톤(Peregrine Soliton)이에요. 45판은 다섯을 더했어요. 로건-셰프와 베르시크-케로프의 극한 곡선으로 다가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도브루신 경계 조건을 건 임계 이징 모형의 SLE(3) 경계(Ising Interface), 프리예제프와 동료들의 오일러 걷개로 쌓는 로터 라우터 응집과 레빈-페레스의 스매시 합(Rotor-Router), 폴리아의 과수원 문제(Pólya's Orchard), 넬슨의 평면 색칠 문제와 아이즈벨의 일곱 색(Hadwiger-Nelson)이에요. 46판은 다섯을 더했어요. 브릴루앙의 구역(Brillouin Zones), 모스 이론과 레브의 그래프(Reeb Graph), 로렌츠 기체와 시나이의 당구(Lorentz Gas), 리언 글래스의 점무늬(Glass Patterns), 벤치와 수테라, 불피아니의 확률 공명(Stochastic Resonance)이에요. 47판은 다섯을 더했어요. 오키프와 홍, 스트로가츠의 스워멀레이터(Swarmalators), 빌스의 외로운 달리기 선수 추측(Lonely Runner), 샤츠의 올로이드와 그 전개도(Oloid), 쾨베의 원 채우기 정리와 콜린스-스티븐슨의 반복(Koebe Packing), 콘웨이의 병사와 황금비 무게(Conway's Soldiers)예요. 48판은 다섯을 더했어요. 오하라의 매듭 뫼비우스 에너지(Möbius Energy), 체비쇼프의 편향과 루빈스타인-사르낙의 밀도(Chebyshev's Bias), 섀든과 레키엔, 마스든의 이중 소용돌이 능선(Double Gyre), 아자리와 프로스트의 깜빡이는 래칫(Flashing Ratchet), 망델브로의 프랙탈 스미기(Fractal Percolation)예요. 49판은 다섯을 더했어요. 뒤러의 전개 문제와 셰퍼드의 추측(Dürer's Unfolding), 베리와 골드버그가 재규격화한 세타 합의 꼬부랑 곡선(Curlicue), 오일러 방정식과 푸앵소의 폴호드로 본 테니스 라켓 정리(Tennis Racket), 켐프의 사슬과 네 색 정리(Kempe Chains), 퇴플리츠의 내접 정사각형 문제와 그린-로브의 직사각형(Square Peg)이에요. 50판은 다섯을 더했어요. 크레이치넌과 배철러의 2차원 난류가 거꾸로 가는 캐스케이드와 맥윌리엄스의 소용돌이(Inverse Cascade), 사포발과 로소, 구예의 확산 앞줄(Diffusion Front), 필리와 마르케티, 케이츠와 타이외르의 운동성 상분리(Motility Clusters), 박과 스네펜의 진화 모형(Bak-Sneppen), 리세의 영원한 가속(Risset Rhythm)이에요. 51판은 다섯을 더했어요. 스미스와 마이어스, 캐플런, 굿먼-스트라우스의 모자 타일과 H/T/P/F 메타타일 치환(Hat Monotile), 반도와 동료들의 최적 속도 모형과 스기야마의 원형 트랙 실험(Phantom Jam), 사토와 테이트의 법칙과 타원곡선의 a_p(Sato-Tate), 칼라비와 야우의 공간에서 핸슨이 잘라 그린 5차 페르마 곡선(Calabi-Yau Quintic), 에델스브루너와 레처, 조모로디언의 지속 호몰로지와 바코드(Persistence Barcode)예요. 52판은 다섯을 더했어요. 슈람과 셰필드가 등고선을 SLE(4) 로 보인 2차원 가우스 자유장(Gaussian Free Field), 당튀와 마즈무다르, 베링거의 광탄성 힘 사슬(Force Chains), 앤스컴의 사중주와 마테이카, 피츠모리스의 같은 통계 담금질(Anscombe's Quartet), 드 브라윈 수열과 BEST 정리(de Bruijn Sequence), 라인스의 베타 평면 띠 제트(Zonal Jets)예요. 53판은 열을 더했어요. 듀크, 가르시아, 루츠가 그린 가우스 주기(Gaussian Periods), 부르가 보인 현수면과 나선면의 휨(Catenoid-Helicoid), 디랙의 벨트 묘기(Belt Trick), 헬름홀츠와 러브의 연기 고리 개구리뜀(Leapfrogging Vortices), 브룩스, 스미스, 스톤, 텃의 정사각형 분할(Squared Rectangle), 포드와 풀커슨의 최대 흐름 최소 절단(Max-Flow Min-Cut), 코스타스 배열(Costas Array), 바라니, 비어든, 칸의 무게중심 분할(Barycentric Subdivision), 무코파디아야와 크네저의 네 꼭짓점 정리(Four-Vertex Theorem), 오일러의 엘라스티카(Euler's Elastica)예요. 54판은 열을 더했어요. 베리의 무작위 파동과 보고몰니, 슈미트가 스미기로 센 마디 영역(Nodal Domains), 피어험버트가 사인 흐름에서 찾은 이상 고유 모드(Strange Eigenmode), 피어시 적분과 베리, 업스틸의 첨점 회절 파국(Pearcey Cusp), 나겔과 바이스의 STIT 모자이크(STIT Tessellation), 레비의 아크사인 법칙(Arcsine Law), 스퇴르메르가 다룬 쌍극자 장 속 대전 입자(Størmer Trap), 게일과 섀플리의 지연 수락(Stable Marriage), 스메일의 말굽(Smale Horseshoe), 울람 수열과 슈타이너베르거가 찾은 숨은 진동수(Ulam Sequence), 위어와 펠런의 거품과 켈빈의 깎은 팔면체(Weaire-Phelan)예요. 55판은 열을 더했어요. 밀러와 셰필드의 허수 기하(Imaginary Geometry), 티소의 지시 타원과 메르카토르 투영(Tissot's Indicatrix), 민코프스키의 물음표 함수(Question Mark Function), 버로스와 휠러의 변환(Burrows-Wheeler), 뷔퐁의 바늘과 바르비에의 국수(Buffon's Noodle), 부자의 리듬 타일링 캐넌(Vuza Canon), 하인과 내시의 헥스와 게일의 헥스 정리(Hex Theorem), 8자 고리의 범피복과 자유군의 케일리 그래프(Universal Cover), 오브친니코프와 젤도비치, 투생과 윌첵의 확산 제한 소멸(Toussaint-Wilczek), 헬름홀츠와 켈빈의 와류 시트와 크라스니의 와동 덩이(Vortex Sheet)예요. 56판은 열을 더했어요. 마테롱의 죽은 잎과 켄들, 퇴네스가 아래에서 본 완전 표본(Dead Leaves), 골롬의 트로미노 깔기(Golomb Trominoes), 윌슨의 고리 지운 걸음과 롤러, 슈람, 베르너의 균등 생성나무 페아노 곡선(Tree Peano Curve), 블로흐와 제너의 진동과 워니어의 사다리(Bloch Oscillations), 앤드루스의 곡선과 아시모프의 그랜드 투어(Andrews Curves), 지니브르와 기르코의 원 법칙과 좀머스, 크리산티, 솜폴린스키, 스타인의 타원 법칙(Elliptic Law), 에르되시의 불일치 문제와 보웨인, 최, 쿤스의 3진 수열(Erdős Discrepancy), 망델브로의 곱셈 폭포와 카안, 페이리에르의 문턱(Multiplicative Cascade), 코르테, 체이킨, 골럽, 파인의 무작위 조직화(Random Organization), 칼루가레아누, 화이트, 풀러의 꼬임과 뒤틀림(Twist and Writhe)이에요. 57판도 열을 더했어요. 블룸의 중심축과 들불 변환(Medial Axis), 스메일과 윌리엄스의 솔레노이드(Solenoid), 솔로몬의 무작위 환경 속 걸음과 시나이의 걸음(Sinai's Walk), 테렐과 펜로즈의 회전(Terrell Rotation), 갈페린의 π 당구(Galperin Billiard), 페르마의 두 제곱수 정리와 자기에의 한 문장 증명(Zagier's Windmills), 스토크스 표류와 게르스트너의 트로코이드 파도(Stokes Drift), 라인하르트부터 맨, 매클라우드맨, 폰 데라우까지의 볼록 오각형 타일링(Pentagon Tilings), 페로와 하위헌스의 트랙트릭스와 자전거 바퀴 자국(Bicycle Tracks), 오스트발트 숙성과 LSW 이론(Ostwald Ripening)이에요. 58판도 열을 더했어요. 야블란과 게르데스의 거울 곡선(Mirror Curves), 가운트와 피셔, 백스터의 딱딱한 정사각형 격자 기체(Hard Squares), 다니엘 베르누이의 매달린 사슬과 베셀 함수 J0(Hanging Chain), 레일리와 테일러의 불안정성(Rayleigh-Taylor), 터키의 반평면 깊이와 라도의 중심점 정리(Tukey Depth), 롤러와 베르너의 브라운 고리 수프(Loop Soup), 카돔체프-페트비아시빌리 방정식과 코다마의 선 솔리톤 웹(KP Web), 크누스가 이름 붙인 베케트-그레이 코드(Beckett-Gray), 파스칼의 신비 육각형과 슈타이너, 커크먼의 점(Hexagrammum Mysticum), 드 스밋과 렌스트라의 판화 화랑 사상(Print Gallery)이에요. 59판도 열을 더했어요. 르장드르와 가우스의 이차 상호 법칙(Quadratic Reciprocity), 위그너 함수와 유르케, 스톨러의 켜 고양이(Wigner Cat), 아비단과 샤미르의 심 카빙(Seam Carving), 슐리히팅과 노르트마르크, 에거스의 알갱이 기체 속 맥스웰의 도깨비(Granular Demon), 클렙시의 대각 3차 곡면과 케일리, 새먼의 직선 27개(Clebsch Surface), 보데르베르크의 나선 타일링(Voderberg Tiling), 자이페르트의 곡면(Seifert Surface), 호크스의 자기 흥분 점과정(Hawkes Process), 코호넨의 자기조직 지도(Kohonen Map), 레일리의 위상자 걸음과 굿맨의 스페클 통계(Laser Speckle)예요. 60판도 열을 더했어요. 마살리아가 짚은 RANDU 의 평면(RANDU Planes), 오일러의 36 장교와 보스, 슈리칸데, 파커의 10차 그레코-라틴 방진(Euler's Officers), 펜로즈와 카터의 등각 콤팩트화 도형(Penrose Diagram), 러더퍼드 산란의 포물선 그림자(Rutherford Shadow), 오일러 나선과 프레넬 적분, 코르뉴의 회절 셈(Cornu Spiral), 리처드슨의 디바이더와 망델브로의 해안선 차원(Richardson Dividers), 에농, 홀, 페익스의 1차원 자기중력 면 모형과 린든-벨의 격렬한 이완(Phase Spiral), 에르되시와 레니의 거대 성분과 아클리옵타스, 드수자, 스펜서의 폭발적 스미기(Giant Component), 허프와 두다, 하트의 직선 찾기(Hough Transform), 조르당의 곡선 정리와 심랏의 교차 세기(Jordan Curve)예요. 61판도 열을 더했어요. 세스나와 다섯 사람의 영온 무작위장 이징 눈사태(Crackling Noise), 몽주의 흙 옮기기와 매캔의 옮김 보간(Monge Transport), 파울의 덫과 마티외 방정식(Paul Trap), 그린힐의 제 무게 기둥(Greenhill Column), 흐바탈의 미술관 정리와 피스크의 세 색 증명(Art Gallery), 홀디치의 고리(Holditch Ring), 톰슨의 꺾인 역선과 헤비사이드의 움직이는 전하(Thomson Kink), 슈나이더와 바이스의 쌍성 렌즈(Binary Lens), 테일러의 되섞기(Taylor Unmixing), 브누아, 칼로, 디에네르 부부의 카나르 폭발(Canard Explosion)이에요. 62판도 열을 더했어요. 아브리코소프와 클라이너, 로스, 오틀러의 소용돌이 격자(Abrikosov Lattice), 울리크니의 청색 잡음과 빈자리와 뭉치 방법(Void and Cluster), 파인먼과 힙스의 체커보드(Feynman Checkerboard), 라도의 바쁜 비버와 마크센, 분트로크의 다섯 상태 챔피언(Busy Beaver), 폭스와 커시너, 비치의 평평한 곡면 위 직선 흐름(Veech Octagon), 리만의 미분 불가능 함수와 거버, 다위스테르마트의 복소 곡선(Riemann's Function), 헤슈와 킨즐레의 한 타일 형(Heesch Tiles), 차크라보르티와 헤이스, 보고시안의 마당 장터 모형(Yard-Sale Model), 베르그렌의 피타고라스 세 수 나무(Berggren Tree), 더브라윈과 람, 렁의 단위근 합과 밀른의 완전히 균형 잡힌 리듬(Balanced Rhythms)이에요. 63판은 다섯을 더했어요. 모팻의 모서리 소용돌이와 다네다의 사진(Moffatt Eddies), 맥스웰의 물고기 눈 렌즈(Maxwell's Fish-Eye), 이지케비치의 스파이크 신경망(Izhikevich Network), 애들러와 하소퍼, 우슬리의 들뜸 집합 오일러 지표(Excursion Sets), 심슨과 블라이스의 역설(Simpson's Paradox)이에요. 64판은 다섯을 더했어요. 암만과 베인커르의 팔각 타일링을 더 브라윈의 다중 격자로 지은 것(Ammann-Beenker), 겔판트, 카프라노프, 젤레빈스키의 아메바와 포르스베리, 파사레, 치흐의 구멍(Polynomial Amoeba), 키블과 주렉의 결함 얼기와 피시먼, 데 키아라, 칼라르코, 모리지의 지그재그 전이(Kibble-Zurek Ions), 야코비의 적분과 힐의 영속도 곡선(Hill's Regions), 스티븐슨과 카피차의 거꾸로 선 진자와 아치슨, 멀린의 인도 밧줄 묘기(Indian Rope Trick)예요. 65판은 다섯을 더했어요. 폰 노이만과 멀린스의 거품 법칙(Foam Coarsening), 베드퍼드와 맥멀런의 자기 아핀 카펫(McMullen Carpet), 클라인의 돛과 라그랑주의 주기 연분수(Klein's Sails), 야코비의 측지선과 샬의 공초점 곡면(Jacobi Geodesics), 하다마르와 리브친스키의 물방울과 코르테스의 정규화 스토크슬렛(Sedimenting Cloud)이에요. 67판은 다섯을 더했어요. 라돈의 변환과 라마찬드란, 락슈미나라야난의 램프 필터(Radon Transform), 아르틴의 브레이드 군과 폭스, 노이워스의 배치 공간(Artin Braids), 월시의 함수계와 실베스터의 행렬, 페일리의 차례(Walsh Functions), 아레프의 깜박이는 소용돌이(Blinking Vortex), 뒤팽의 사이클라이드(Dupin Cyclide)예요. +주제 5 로 쓴 수학과 다음에 해 볼 수학이에요. 28판이 처음 열 개를 썼어요. 슈람-뢰브너 진화(Loewner Trace), 포드 원(Ford Circles), 털 난 공 정리(Hairy Ball), 아즈텍 다이아몬드와 북극권(Aztec Diamond), 부니모비치 경기장(Stadium Billiard), 라우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 다면체와 E8(Gosset Polytope), 콘웨이의 보고 말하기 수열(Look and Say), 위토프 게임의 그런디 값(Wythoff Nim), 쌍곡 평면의 띠 모형(Hyperbolic Band)이에요. 29판은 여섯을 더했어요. 클라인의 j 불변량(J-Invariant), 호프 올뭉치(Hopf Fibration), 다이슨 브라운 운동(Dyson Gas), 가케야 바늘 문제의 페론 나무(Perron Tree), 임계선 위의 리만 제타 함수(Zeta Loops), 길버트-섀넌-리즈 리플과 베이어-디아코니스의 일곱 번(Seven Shuffles)이에요. 30판도 여섯을 더했어요. 세 간격 정리(Three Gaps), 호프스태터 나비와 TKNN 정수(Hofstadter Butterfly), 노이만의 외부 당구(Outer Billiards), 볼드의 탄도 퇴적과 KPZ 앞면(Ballistic Deposition), 아르놀트의 미앤더(Meander), 브로드벤트와 해머슬리의 스미기(Percolation)예요. 31판도 여섯을 더했어요. 마스킷 조각의 클라인 군 극한 집합(Indra's Pearls), 리틀우드 다항식의 근(Littlewood Roots), 고든의 가우스 해자(Gaussian Moat), 아르놀트의 고양이 사상(Cat Map), 자부스키와 크루스칼의 KdV 솔리톤(KdV Solitons), 보이 곡면(Boy's Surface)이에요. ROUND.md 의 「낯선 수학 예」 여덟 가운데 쓴 것은 슈람-뢰브너 진화와 아즈텍 다이아몬드, 모듈러 형식, 호프 올뭉치, 세 간격 정리, 외부 당구 여섯이었고, 31판이 리틀우드 다항식의 근과 클라인 군의 극한 집합을 써서 여덟을 다 썼어요. 32판도 여섯을 더했어요. 탤벗 효과와 양자 카펫(Talbot Carpet), 길브레스 추측의 표(Gilbreath Table), 원 사상의 아르놀트 혀(Arnold Tongues), 콘웨이와 라딘의 핀휠 타일링(Pinwheel Tiling), 윌킨슨과 윌렘센의 침입 스미기(Invasion Percolation), 울람의 문제와 해머슬리 과정(Hammersley Lines)이에요. 33판도 여섯을 더했어요. 에인절, 홀로이드, 로믹, 비라그의 무작위 정렬 네트워크(Sorting Network), 베르트랑의 역설(Bertrand Paradox), 펜로즈의 비출 수 없는 방(Penrose Room), 당들랭 구(Dandelin Spheres), 테이트의 보로메오 고리(Borromean Rings), 골드바흐 혜성(Goldbach Comet)이에요. 34판도 여섯을 더했어요. 톰슨 문제와 오일러가 남기는 다섯 이웃 열둘(Thomson Problem), 켈빈의 배 물결(Kelvin Wake), 요네야마가 낸 와다의 호수(Lakes of Wada), 샤이데거의 강 그물(Scheidegger Rivers), 모저의 소파 문제와 해머슬리의 소파(Moving Sofa), 크누스의 춤추는 링크로 푼 펜토미노 덮기(Pentomino Cover)예요. 35판도 여섯을 더했어요. 뢸로의 폭이 일정한 곡선(Constant Width), 렌니의 주차 문제(Rényi Parking), 카리의 비주기 왕 타일(Kari-Culik Tiles), 콜라코스키 수열(Kolakoski), 쿠라모토와 바톡토흐의 키메라 상태(Chimera State), 주콥스키 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했어요. 그레이슨의 정리가 말하는 곡선 단축 흐름(Curve Shortening), 헤우드의 일곱 색 토러스 지도(Heawood Torus), 슈바르츠 랜턴(Schwarz Lantern), 윌브러햄과 깁스의 현상(Gibbs Phenomenon), 타원 곡선의 현과 접선(Elliptic Chords), 지먼의 파국 기계(Catastrophe Machine)예요. 37판도 여섯을 더했어요. 포천의 쓸기 선 알고리즘(Fortune's Sweep), 앙투안의 목걸이(Antoine's Necklace), 카르만 소용돌이 길의 안정성(Kármán Street), 단단한 원판의 헥사틱 상(Hexatic Phase), 존슨-멜 쪽매와 아브라미 법칙(Johnson-Mehl), 쿠라모토-시바신스키 방정식(Kuramoto-Sivashinsky)이에요. 38판도 여섯을 더했어요. 앤더슨 국소화(Anderson Localization), 무어가 찾고 Chenciner 와 Montgomery 가 증명한 세 몸 문제의 8자 해(Figure-Eight Orbit), 예센과 빈트너, 에르되시의 베르누이 합성곱(Bernoulli Convolutions), 브라우어르의 고정점 정리를 증명하는 슈페르너 보조정리(Sperner's Lemma), 에겐베르거와 폴리아의 항아리(Pólya Urn), 뇌르고르의 무한 수열(Infinity Series)이에요. 39판도 여섯을 더했어요. 고든, 웹, 월퍼트의 등스펙트럼 북(Isospectral Drums), 폰 망골트의 꼴로 쓴 리만의 명시 공식(Explicit Formula), 요한 베르누이의 최단 강하선(Brachistochrone), 슈타이너의 포리즘(Steiner Chain), 모런 집단으로 그린 킹먼의 합류(Kingman Coalescent), 보르수크-울람 정리(Borsuk-Ulam)예요. 40판도 여섯을 더했어요. 앨더스의 브라운 연속 랜덤 나무(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 함수(Hydrogen Orbital), 젤도비치 근사의 팬케이크(Zeldovich Pancakes), 망델브로가 내다보고 롤러, 슈람, 베르너가 증명한 브라운 운동의 바깥 경계(Brownian Frontier), 복소 긴즈부르크-란다우 방정식의 결함 난류(Defect Turbulence), 루퍼트 왕자의 정육면체(Prince Rupert's Cube)예요. 41판은 다섯을 더했어요. 홀과 왜건의 현수선 길과 정다각형 바퀴(Square Wheels), 퐁슬레의 포리즘(Poncelet Porism), 슐레플리의 4차원 정다포체를 자른 단면(Polytope Slices), 클림켁과 티먼, 마그누손의 사이트스왑(Siteswap), Dudeney 의 잡화상 퍼즐과 월리스-보여이-게르비엔 정리(Hinged Dissection)예요. 42판은 다섯을 더했어요. 바른스도르프 규칙으로 도는 기사 여행(Knight's Tour), 페르미와 파스타, 울람, 칭구의 되돌아옴(FPUT Recurrence), 포슬리에-립킨 반전기(Peaucellier Linkage), 몰리의 삼등분선 정리(Morley Triangle), 랄과 마드라스, 소칼의 피벗 알고리즘으로 돌린 자기 회피 걸음(Pivot Walk)이에요. 43판은 다섯을 더했어요. 월리스 워커의 칼레이도사이클(Kaleidocycle), 쌍곡형에서 록소드롬형으로 감기는 뫼비우스 변환의 흐름(Möbius Loxodrome), 터트의 용수철 정리(Tutte Embedding), 디펀트와 크래비츠의 고리 정리를 품은 히토메자시 사시코(Hitomezashi), 룽게 현상과 체비쇼프 점(Runge Phenomenon)이에요. 44판은 다섯을 더했어요. 마크 카츠의 고리 모형(Kac Ring), 스톤과 튜키의 햄 샌드위치 정리(Ham Sandwich), 브란트의 불가리아 솔리테어(Bulgarian Solitaire), 엘스켄스와 프리시의 탄도 소멸(Ballistic Annihilation), 페레그린의 솔리톤(Peregrine Soliton)이에요. 45판은 다섯을 더했어요. 로건-셰프와 베르시크-케로프의 극한 곡선으로 다가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도브루신 경계 조건을 건 임계 이징 모형의 SLE(3) 경계(Ising Interface), 프리예제프와 동료들의 오일러 걷개로 쌓는 로터 라우터 응집과 레빈-페레스의 스매시 합(Rotor-Router), 폴리아의 과수원 문제(Pólya's Orchard), 넬슨의 평면 색칠 문제와 아이즈벨의 일곱 색(Hadwiger-Nelson)이에요. 46판은 다섯을 더했어요. 브릴루앙의 구역(Brillouin Zones), 모스 이론과 레브의 그래프(Reeb Graph), 로렌츠 기체와 시나이의 당구(Lorentz Gas), 리언 글래스의 점무늬(Glass Patterns), 벤치와 수테라, 불피아니의 확률 공명(Stochastic Resonance)이에요. 47판은 다섯을 더했어요. 오키프와 홍, 스트로가츠의 스워멀레이터(Swarmalators), 빌스의 외로운 달리기 선수 추측(Lonely Runner), 샤츠의 올로이드와 그 전개도(Oloid), 쾨베의 원 채우기 정리와 콜린스-스티븐슨의 반복(Koebe Packing), 콘웨이의 병사와 황금비 무게(Conway's Soldiers)예요. 48판은 다섯을 더했어요. 오하라의 매듭 뫼비우스 에너지(Möbius Energy), 체비쇼프의 편향과 루빈스타인-사르낙의 밀도(Chebyshev's Bias), 섀든과 레키엔, 마스든의 이중 소용돌이 능선(Double Gyre), 아자리와 프로스트의 깜빡이는 래칫(Flashing Ratchet), 망델브로의 프랙탈 스미기(Fractal Percolation)예요. 49판은 다섯을 더했어요. 뒤러의 전개 문제와 셰퍼드의 추측(Dürer's Unfolding), 베리와 골드버그가 재규격화한 세타 합의 꼬부랑 곡선(Curlicue), 오일러 방정식과 푸앵소의 폴호드로 본 테니스 라켓 정리(Tennis Racket), 켐프의 사슬과 네 색 정리(Kempe Chains), 퇴플리츠의 내접 정사각형 문제와 그린-로브의 직사각형(Square Peg)이에요. 50판은 다섯을 더했어요. 크레이치넌과 배철러의 2차원 난류가 거꾸로 가는 캐스케이드와 맥윌리엄스의 소용돌이(Inverse Cascade), 사포발과 로소, 구예의 확산 앞줄(Diffusion Front), 필리와 마르케티, 케이츠와 타이외르의 운동성 상분리(Motility Clusters), 박과 스네펜의 진화 모형(Bak-Sneppen), 리세의 영원한 가속(Risset Rhythm)이에요. 51판은 다섯을 더했어요. 스미스와 마이어스, 캐플런, 굿먼-스트라우스의 모자 타일과 H/T/P/F 메타타일 치환(Hat Monotile), 반도와 동료들의 최적 속도 모형과 스기야마의 원형 트랙 실험(Phantom Jam), 사토와 테이트의 법칙과 타원곡선의 a_p(Sato-Tate), 칼라비와 야우의 공간에서 핸슨이 잘라 그린 5차 페르마 곡선(Calabi-Yau Quintic), 에델스브루너와 레처, 조모로디언의 지속 호몰로지와 바코드(Persistence Barcode)예요. 52판은 다섯을 더했어요. 슈람과 셰필드가 등고선을 SLE(4) 로 보인 2차원 가우스 자유장(Gaussian Free Field), 당튀와 마즈무다르, 베링거의 광탄성 힘 사슬(Force Chains), 앤스컴의 사중주와 마테이카, 피츠모리스의 같은 통계 담금질(Anscombe's Quartet), 드 브라윈 수열과 BEST 정리(de Bruijn Sequence), 라인스의 베타 평면 띠 제트(Zonal Jets)예요. 53판은 열을 더했어요. 듀크, 가르시아, 루츠가 그린 가우스 주기(Gaussian Periods), 부르가 보인 현수면과 나선면의 휨(Catenoid-Helicoid), 디랙의 벨트 묘기(Belt Trick), 헬름홀츠와 러브의 연기 고리 개구리뜀(Leapfrogging Vortices), 브룩스, 스미스, 스톤, 텃의 정사각형 분할(Squared Rectangle), 포드와 풀커슨의 최대 흐름 최소 절단(Max-Flow Min-Cut), 코스타스 배열(Costas Array), 바라니, 비어든, 칸의 무게중심 분할(Barycentric Subdivision), 무코파디아야와 크네저의 네 꼭짓점 정리(Four-Vertex Theorem), 오일러의 엘라스티카(Euler's Elastica)예요. 54판은 열을 더했어요. 베리의 무작위 파동과 보고몰니, 슈미트가 스미기로 센 마디 영역(Nodal Domains), 피어험버트가 사인 흐름에서 찾은 이상 고유 모드(Strange Eigenmode), 피어시 적분과 베리, 업스틸의 첨점 회절 파국(Pearcey Cusp), 나겔과 바이스의 STIT 모자이크(STIT Tessellation), 레비의 아크사인 법칙(Arcsine Law), 스퇴르메르가 다룬 쌍극자 장 속 대전 입자(Størmer Trap), 게일과 섀플리의 지연 수락(Stable Marriage), 스메일의 말굽(Smale Horseshoe), 울람 수열과 슈타이너베르거가 찾은 숨은 진동수(Ulam Sequence), 위어와 펠런의 거품과 켈빈의 깎은 팔면체(Weaire-Phelan)예요. 55판은 열을 더했어요. 밀러와 셰필드의 허수 기하(Imaginary Geometry), 티소의 지시 타원과 메르카토르 투영(Tissot's Indicatrix), 민코프스키의 물음표 함수(Question Mark Function), 버로스와 휠러의 변환(Burrows-Wheeler), 뷔퐁의 바늘과 바르비에의 국수(Buffon's Noodle), 부자의 리듬 타일링 캐넌(Vuza Canon), 하인과 내시의 헥스와 게일의 헥스 정리(Hex Theorem), 8자 고리의 범피복과 자유군의 케일리 그래프(Universal Cover), 오브친니코프와 젤도비치, 투생과 윌첵의 확산 제한 소멸(Toussaint-Wilczek), 헬름홀츠와 켈빈의 와류 시트와 크라스니의 와동 덩이(Vortex Sheet)예요. 56판은 열을 더했어요. 마테롱의 죽은 잎과 켄들, 퇴네스가 아래에서 본 완전 표본(Dead Leaves), 골롬의 트로미노 깔기(Golomb Trominoes), 윌슨의 고리 지운 걸음과 롤러, 슈람, 베르너의 균등 생성나무 페아노 곡선(Tree Peano Curve), 블로흐와 제너의 진동과 워니어의 사다리(Bloch Oscillations), 앤드루스의 곡선과 아시모프의 그랜드 투어(Andrews Curves), 지니브르와 기르코의 원 법칙과 좀머스, 크리산티, 솜폴린스키, 스타인의 타원 법칙(Elliptic Law), 에르되시의 불일치 문제와 보웨인, 최, 쿤스의 3진 수열(Erdős Discrepancy), 망델브로의 곱셈 폭포와 카안, 페이리에르의 문턱(Multiplicative Cascade), 코르테, 체이킨, 골럽, 파인의 무작위 조직화(Random Organization), 칼루가레아누, 화이트, 풀러의 꼬임과 뒤틀림(Twist and Writhe)이에요. 57판도 열을 더했어요. 블룸의 중심축과 들불 변환(Medial Axis), 스메일과 윌리엄스의 솔레노이드(Solenoid), 솔로몬의 무작위 환경 속 걸음과 시나이의 걸음(Sinai's Walk), 테렐과 펜로즈의 회전(Terrell Rotation), 갈페린의 π 당구(Galperin Billiard), 페르마의 두 제곱수 정리와 자기에의 한 문장 증명(Zagier's Windmills), 스토크스 표류와 게르스트너의 트로코이드 파도(Stokes Drift), 라인하르트부터 맨, 매클라우드맨, 폰 데라우까지의 볼록 오각형 타일링(Pentagon Tilings), 페로와 하위헌스의 트랙트릭스와 자전거 바퀴 자국(Bicycle Tracks), 오스트발트 숙성과 LSW 이론(Ostwald Ripening)이에요. 58판도 열을 더했어요. 야블란과 게르데스의 거울 곡선(Mirror Curves), 가운트와 피셔, 백스터의 딱딱한 정사각형 격자 기체(Hard Squares), 다니엘 베르누이의 매달린 사슬과 베셀 함수 J0(Hanging Chain), 레일리와 테일러의 불안정성(Rayleigh-Taylor), 터키의 반평면 깊이와 라도의 중심점 정리(Tukey Depth), 롤러와 베르너의 브라운 고리 수프(Loop Soup), 카돔체프-페트비아시빌리 방정식과 코다마의 선 솔리톤 웹(KP Web), 크누스가 이름 붙인 베케트-그레이 코드(Beckett-Gray), 파스칼의 신비 육각형과 슈타이너, 커크먼의 점(Hexagrammum Mysticum), 드 스밋과 렌스트라의 판화 화랑 사상(Print Gallery)이에요. 59판도 열을 더했어요. 르장드르와 가우스의 이차 상호 법칙(Quadratic Reciprocity), 위그너 함수와 유르케, 스톨러의 켜 고양이(Wigner Cat), 아비단과 샤미르의 심 카빙(Seam Carving), 슐리히팅과 노르트마르크, 에거스의 알갱이 기체 속 맥스웰의 도깨비(Granular Demon), 클렙시의 대각 3차 곡면과 케일리, 새먼의 직선 27개(Clebsch Surface), 보데르베르크의 나선 타일링(Voderberg Tiling), 자이페르트의 곡면(Seifert Surface), 호크스의 자기 흥분 점과정(Hawkes Process), 코호넨의 자기조직 지도(Kohonen Map), 레일리의 위상자 걸음과 굿맨의 스페클 통계(Laser Speckle)예요. 60판도 열을 더했어요. 마살리아가 짚은 RANDU 의 평면(RANDU Planes), 오일러의 36 장교와 보스, 슈리칸데, 파커의 10차 그레코-라틴 방진(Euler's Officers), 펜로즈와 카터의 등각 콤팩트화 도형(Penrose Diagram), 러더퍼드 산란의 포물선 그림자(Rutherford Shadow), 오일러 나선과 프레넬 적분, 코르뉴의 회절 셈(Cornu Spiral), 리처드슨의 디바이더와 망델브로의 해안선 차원(Richardson Dividers), 에농, 홀, 페익스의 1차원 자기중력 면 모형과 린든-벨의 격렬한 이완(Phase Spiral), 에르되시와 레니의 거대 성분과 아클리옵타스, 드수자, 스펜서의 폭발적 스미기(Giant Component), 허프와 두다, 하트의 직선 찾기(Hough Transform), 조르당의 곡선 정리와 심랏의 교차 세기(Jordan Curve)예요. 61판도 열을 더했어요. 세스나와 다섯 사람의 영온 무작위장 이징 눈사태(Crackling Noise), 몽주의 흙 옮기기와 매캔의 옮김 보간(Monge Transport), 파울의 덫과 마티외 방정식(Paul Trap), 그린힐의 제 무게 기둥(Greenhill Column), 흐바탈의 미술관 정리와 피스크의 세 색 증명(Art Gallery), 홀디치의 고리(Holditch Ring), 톰슨의 꺾인 역선과 헤비사이드의 움직이는 전하(Thomson Kink), 슈나이더와 바이스의 쌍성 렌즈(Binary Lens), 테일러의 되섞기(Taylor Unmixing), 브누아, 칼로, 디에네르 부부의 카나르 폭발(Canard Explosion)이에요. 62판도 열을 더했어요. 아브리코소프와 클라이너, 로스, 오틀러의 소용돌이 격자(Abrikosov Lattice), 울리크니의 청색 잡음과 빈자리와 뭉치 방법(Void and Cluster), 파인먼과 힙스의 체커보드(Feynman Checkerboard), 라도의 바쁜 비버와 마크센, 분트로크의 다섯 상태 챔피언(Busy Beaver), 폭스와 커시너, 비치의 평평한 곡면 위 직선 흐름(Veech Octagon), 리만의 미분 불가능 함수와 거버, 다위스테르마트의 복소 곡선(Riemann's Function), 헤슈와 킨즐레의 한 타일 형(Heesch Tiles), 차크라보르티와 헤이스, 보고시안의 마당 장터 모형(Yard-Sale Model), 베르그렌의 피타고라스 세 수 나무(Berggren Tree), 더브라윈과 람, 렁의 단위근 합과 밀른의 완전히 균형 잡힌 리듬(Balanced Rhythms)이에요. 63판은 다섯을 더했어요. 모팻의 모서리 소용돌이와 다네다의 사진(Moffatt Eddies), 맥스웰의 물고기 눈 렌즈(Maxwell's Fish-Eye), 이지케비치의 스파이크 신경망(Izhikevich Network), 애들러와 하소퍼, 우슬리의 들뜸 집합 오일러 지표(Excursion Sets), 심슨과 블라이스의 역설(Simpson's Paradox)이에요. 64판은 다섯을 더했어요. 암만과 베인커르의 팔각 타일링을 더 브라윈의 다중 격자로 지은 것(Ammann-Beenker), 겔판트, 카프라노프, 젤레빈스키의 아메바와 포르스베리, 파사레, 치흐의 구멍(Polynomial Amoeba), 키블과 주렉의 결함 얼기와 피시먼, 데 키아라, 칼라르코, 모리지의 지그재그 전이(Kibble-Zurek Ions), 야코비의 적분과 힐의 영속도 곡선(Hill's Regions), 스티븐슨과 카피차의 거꾸로 선 진자와 아치슨, 멀린의 인도 밧줄 묘기(Indian Rope Trick)예요. 65판은 다섯을 더했어요. 폰 노이만과 멀린스의 거품 법칙(Foam Coarsening), 베드퍼드와 맥멀런의 자기 아핀 카펫(McMullen Carpet), 클라인의 돛과 라그랑주의 주기 연분수(Klein's Sails), 야코비의 측지선과 샬의 공초점 곡면(Jacobi Geodesics), 하다마르와 리브친스키의 물방울과 코르테스의 정규화 스토크슬렛(Sedimenting Cloud)이에요. 67판은 다섯을 더했어요. 라돈의 변환과 라마찬드란, 락슈미나라야난의 램프 필터(Radon Transform), 아르틴의 브레이드 군과 폭스, 노이워스의 배치 공간(Artin Braids), 월시의 함수계와 실베스터의 행렬, 페일리의 차례(Walsh Functions), 아레프의 깜박이는 소용돌이(Blinking Vortex), 뒤팽의 사이클라이드(Dupin Cyclide)예요. 69판은 다섯을 더했어요. 랭퍼드의 짝과 데이비스의 존재 조건, 스콜렘의 수열(Langford Pairs), 아하로노프와 봄의 효과와 베리 등의 파면 전위(Aharonov-Bohm), 페니의 놀이와 콘웨이의 선행수(Penney's Game), 휘트니와 그라우스타인의 회전수 정리(Whitney-Graustein), 슈타이너의 대칭화와 그로스의 수렴(Steiner Symmetrization)이에요. 목록 밖에서 다음에 해 볼 것이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -4026,6 +4109,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 67판에서 본 것이에요. 셈이 정확한 대상은 이번에도 패턴 안에서 먼저 맞춰 봤어요. Dupin Cyclide 는 원환면의 원을 반전한 세 점으로 정해 그리는데, 노브 35 자리에서 351,000 점을 직접 반전해 보니 그 원에서 가장 먼 점이 4.6×10⁻¹⁴ 였어요. Walsh Functions 는 sequency 차례의 k 번째 행이 부호를 꼭 k 번 바꾸지 않으면 setup 에서 멈추게 했고, 정수만 쓰는 변환은 무작위 블록 200개에서 정의대로 셈한 값과 하나도 다르지 않았어요. Radon Transform 은 타원의 닫힌 꼴 투영이 수치 적분과 0.003 안에서 맞았고, 잡음 없이 투영 수를 1 에서 64 까지 늘리면 반지름 30 안의 평균 오차가 0.50 에서 0.027 로 줄었어요. 같은 광자 8,000개를 투영끼리 나눠 쓰게 하자 오차는 투영 16개부터 0.19 언저리에서 멈췄어요. 수학이 그린 그림이 다른 패턴과 겹치기도 했어요. Artin Braids 의 보로메오 브레이드 (σ1σ2⁻¹)³ 은 15판 Plait 의 세 가닥 머리 땋기와 같은 그림이었어요. 셈하는 법이 그림을 바꾼 곳도 있었어요. Blinking Vortex 는 LED 마다 두 점을 찍어 물감을 고르면 섞인 가운데가 반짝이 잡음이 돼서, 회전의 야코비안을 끌고 와 LED 한 칸이 처음 물감에서 덮는 자리를 평균했어요. 점을 찍던 첫 판은 움직임이 1.366 으로, 평균한 판의 두 배 넘게 반짝임을 움직임으로 셌어요. 3차원 곡면은 이번에도 패널의 비율이 그림을 정했어요. 고리 사이클라이드를 마주 보면 너비와 높이가 같아서 64 LED 너비에 맞추면 128줄의 절반만 차고, 그래서 Dupin Cyclide 는 뒤집기 전 원환면을 위에 같이 세웠어요. +69판에서 본 것이에요. 셈이 정확한 대상은 이번에도 패턴 안에서 먼저 맞춰 봤어요. Langford Pairs 는 코드와 같은 순서로 따로 세어 뒤집은 것을 하나로 친 3, 4, 7, 8, 11, 12 의 배열 수가 1, 1, 26, 150, 17,792, 108,144 였고, 스콜렘 수열은 4, 5, 8, 9, 12 에서 3, 5, 252, 1,328, 227,968 이었어요. Penney's Game 은 콘웨이의 선행수로 셈한 승산이 따로 돌린 20만 판과 0.0013 안에서 맞았고, 패턴을 Speed 3 에서 한 시간 돌린 HHT 대 THH 5,475 판에서 B 가 0.7512 를 가져갔어요. Aharonov-Bohm 은 분수 차수 베셀 함수를 밀러의 역방향 점화식과 노이만의 합으로 구해서, 자속 0 에서 평면파와 1e-14 안으로, 자속 1 에서 1.4e-8 안으로 같아요. 정리가 수를 못 박는 대상은 패널에서 지켜봤어요. Steiner Symmetrization 의 초승달은 황금각 차례의 축 다섯 번에 둘레가 원의 1.520 배에서 1.004 배로 들어왔어요. 황금각 차례가 늘 원으로 간다는 정리는 찾지 못했고, 다섯 모양이 다 1% 안으로 들어오는 것만 봤어요. Whitney-Graustein 은 회전수가 1 에서 0 으로 내려갈 때가 0 에서 1 로 오를 때보다 곡선을 훨씬 세게 비틀어서, 흔들림을 다 꺼도 한 구간이 다른 구간보다 16배 빨리 달려요. 패널의 폭이 정리를 다 담지 못한 곳도 있었어요. Aharonov-Bohm 의 반 양자에서 맨 아랫줄 양 끝의 밀림 차이는 0.5 가 아니라 0.445 바퀴예요. 양 끝이 그림자 선에서 30 LED 인데, 관 뒤 85 LED 에서의 프레넬 폭 √(λx) 가 24 LED 라 회절이 아직 남아서예요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. @@ -4096,6 +4181,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 목록 밖에서 67판 뒤에 해 볼 것이에요. 스테판 카치마르츠(Stefan Kaczmarz)의 반복법(1937)은 사이노그램을 한 줄씩 풀어 가는 재구성이라, Radon Transform 의 걸러 되비추기 옆에 세우면 투영이 적을 때 별 자국 대신 어떤 얼룩이 남는지 견줄 수 있어요. 같은 브레이드를 다르게 적은 두 낱말은 F. A. 가사이드(F. A. Garside, 1969)의 정규형에서 한 줄로 모여서, Artin Braids 의 시공간 그림 둘을 나란히 두고 모이는 것을 볼 만해요. 알프레드 하르(Alfréd Haar, 1910)의 함수계는 Walsh 함수와 같은 ±1 값이지만 받침이 좁아서, 같은 그림을 되살리면 계수가 가장자리 둘레에 모여요. Blinking Vortex 가 LED 마다 끌고 온 야코비안의 큰 특이값은 유한 시간 랴푸노프 지수라, 조지 할러(George Haller)와 위안궈청(Guocheng Yuan)의 라그랑주 응집 구조(2000)를 그 위에 바로 그릴 수 있어요. 원환면의 비스듬한 빌라르소 원(이봉 빌라르소, 1848)도 반전 뒤에 원으로 남으니, Dupin Cyclide 의 한 점을 지나는 원 넷을 같이 그려 볼 만해요. +목록 밖에서 69판 뒤에 해 볼 것이에요. 토노무라 아키라(Akira Tonomura) 등이 1986년에 초전도체로 감싼 고리 자석으로 찍은 전자 홀로그램처럼, 고리 안과 밖을 지난 간섭 줄무늬가 반 칸 어긋나는 그림을 Aharonov-Bohm 의 급수로 그려 볼 만해요. 자속관 둘을 반대 자속으로 나란히 두면 두 그림자 선 사이의 띠만 밀린 채 내려와요. 페니의 놀이를 네 번 줄로 넓히면 (x2 의 반대) x1 x2 x3 규칙이 열여섯 가운데 여섯에서 최선이 아니에요. HHTH 에 규칙이 고른 THHT 는 7:5 로 이기지만 HHHT 는 2:1 로 이겨서, 그 어긋남을 노브 하나로 넘겨 보는 판이 돼요. 휘트니가 같은 논문에 적은 공식, 곧 교차점마다 매긴 부호의 합에 ±1 을 더하면 회전수가 된다는 것을 Whitney-Graustein 의 곡선 위에 교차점 표시로 보여 줄 만하고, 블라디미르 아르놀트(Vladimir Arnold)가 1994년에 낸 평면 곡선의 불변량 J+, J−, St 는 곡선이 제 몸에 접하거나 삼중점을 지날 때만 바뀌어서 그 순간을 셀 수 있어요. 한 직선에 대해 거울쌍 두 점 가운데 한쪽만 옮기는 극 대칭화(polarization)는 Steiner Symmetrization 의 빗금 틀에서 현이 미끄러지는 대신 조각이 축 건너편으로 뒤집혀 건너가요. 같은 색 블록을 셋씩 쌓아 이웃한 둘 사이마다 k 블록이 끼게 하는 랭퍼드 문제의 확장은 Langford Pairs 의 탑에 그대로 서고, 도널드 크누스(Donald Knuth)의 춤추는 연결(2000)로 선택지가 가장 적은 칸부터 고르는 탐색을 옆에 두면 두 탐색 나무의 크기를 나란히 잴 수 있어요. + ### 타이포 주제 7 로 해 본 글자 실험과 다음에 해 볼 것이에요. 63판과 64판이 같이 돌며 다섯씩 처음 만들었어요. 63판의 다섯은 한글 자모가 날아와 모아쓰기로 잠기는 조립(Jamo Assembly), 놀턴과 하먼의 기호 모자이크(Type Mosaic), 호프스태터의 트립렛 그림자(Trip-let), 카프리카의 계산 종이(Kaprekar's Routine), 캐럴의 낱말 사다리(Word Ladder)예요. @@ -4108,7 +4195,7 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 주제 7 「타이포」로 해 볼 것이에요. 64판이 처음 다섯을 만들었고, 다섯 다 글꼴을 쓸 수 없어서 글자를 코드에 그렸어요. 다섯이 「기능 감」에 적은 것도 같은 길이었어요. 패턴 코드가 Text 레이어의 글이나 그 글자 비트맵, 글자마다 폭을 담은 글꼴 표를 받는 길이에요. 그 길이 있으면 Broad Nib 의 펜이 손으로 친 낱말을 쓰고, Wongoji 가 「진달래꽃」이든 아무 한글이든 원고지에 써요. 한글 픽셀 글꼴은 Wongoji 가 9×11 로 지은 조합 글꼴(자모 그림 391개)이 출발점이 될 수 있어요. -같은 펜 모델로 한글을 써 볼 만해요. Broad Nib 의 펜이 쓸고 지나가는 셈에 자모 획을 궁체 붓 각도로 적으면, 초성, 중성, 종성 차례로 펜이 한 칸에 모여요. 카운터 다음은 글자 사이의 흰 자리예요. Counterforms 의 flood fill 에 글자마다 볼록 껍질을 더하면 이웃한 두 글자 사이를 따로 떼어 빛나게 할 수 있고, 노브로 자간을 좁히면 빛이 줄어요. 글자의 몸은 모래 다음에 물로 바꿔 볼 만해요. Letter Sand 의 비트맵 글자를 입자 유체로 채우면 획이 녹아 바닥을 따라 퍼졌다가 다음 낱말 틀에 다시 부어져요. 한글 양쪽 맞춤도 남았어요. 한글 조판은 낱말 사이보다 글자 사이를 고르게 늘리는 일이 많아서, Rivers of White 의 문단을 한글로 짜면 강 대신 세로로 곧게 선 글자 열이 생기는지 볼 수 있어요. +같은 펜 모델로 한글을 써 볼 만해요. Broad Nib 의 펜이 쓸고 지나가는 셈에 자모 획을 궁체 붓 각도로 적으면, 초성, 중성, 종성 차례로 펜이 한 칸에 모여요. 카운터 다음은 글자 사이의 흰 자리예요. Counterforms 의 flood fill 에 글자마다 볼록 껍질을 더하면 이웃한 두 글자 사이를 따로 떼어 빛나게 할 수 있고, 노브로 자간을 좁히면 빛이 줄어요. 한글 양쪽 맞춤도 남았어요. 한글 조판은 낱말 사이보다 글자 사이를 고르게 늘리는 일이 많아서, Rivers of White 의 문단을 한글로 짜면 강 대신 세로로 곧게 선 글자 열이 생기는지 볼 수 있어요. 65판이 다섯을 더 썼어요. 1 을 소수로 나누는 세로셈(Long Division), 스콧 킴의 반 바퀴 앰비그램을 한글로 옮긴 것(Inversions), 한 시점에서만 맞춰지는 착시 원근 낱말(Anamorphosis), 해례본 제자해의 가획 계보(Hunminjeongeum), 캐럴의 꼬리 시(Mouse's Tale)예요. 다섯이 이번에도 다 글꼴을 코드에 그렸어요. 돌려 읽기는 한글이 라틴 글자보다 잘 맞았어요. 가로 모음을 가진 받침 글자는 반 바퀴에 초성과 받침이 자리를 바꿔서 곰국과 논문, 공공과 운운이 짝인데, 받침 상자를 초성 상자와 같은 크기로 칸 가운데에 대칭으로 두지 않으면 켜진 LED 2,617개 가운데 612개가 갈렸어요. 착시 원근의 낱말은 읽히는 각도가 좁았어요. 시점에서 6.6도 비끼면 획이 두 겹으로 겨우 읽히고 21도면 무슨 낱말인지 알 수 없어요. 꼬리 시는 글자가 2×3 으로 줄면 읽히지 않는데도 솟는 글자와 처지는 글자가 LED 하나씩 남아 낱말마다 다른 무늬가 됐어요. @@ -4132,6 +4219,14 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 67판 뒤에 해 볼 것이에요. 섀넌의 「Prediction and Entropy of Printed English」(1951)처럼 패널이 다음 글자를 가리고 사람이 노브로 맞히게 하면, 맞히기까지 고른 횟수가 영어 한 글자의 엔트로피 어림이 돼요. 라인하르트 될(Reinhard Döhl)의 「Apfel」(1965)은 Apfel 로 채운 사과 꼴에 Wurm 하나를 숨긴 구체시라, Silencio 처럼 노브가 그 낱말의 자리를 옮기게 해 볼 만해요. 빅토르 흘룸스키(Viktor Chlumský)의 다채널 거리장(2015)은 거리장 셋의 가운데 값으로 모서리를 지켜서, 같은 텍셀 수로 Distance Field 의 둥글어진 W 와 견줄 수 있어요. 윌리엄 문(William Moon)이 1845년에 만든 문 활자는 로마자를 단순한 돋은 선으로 줄인 글자라, Braille Relief 의 램프 아래에서 점 대신 선의 그림자가 글자를 그려요. 공병우의 세벌식 타자기(1949)가 찍던 탈네모꼴 글줄을 글쇠 차례대로 다시 찍으면, 글꼴이 아니라 기계가 만든 탈네모꼴이 Hanging Line 과 나란히 서요. +69판의 다섯은 물결이 잔잔해지면 굴절된 빛이 낱말로 모이는 목표 기반 커스틱(Caustic Letters), 7세그먼트 숫자를 뒤집어 낱말로 읽는 계산기(Calculator Words), 한글 획을 접어 인면을 메우는 구첩전(Nine-fold Seal), 줄마다 방향을 바꿔 쟁기질하듯 쓰는 헤시오도스(Boustrophedon), 입자 유체로 부은 낱말이 녹아 흘러내리는 거푸집(Letter Melt)이에요. + +69판에서 본 것이에요. 이번에도 다섯이 다 글자 꼴을 코드에 적었고, 옮겨 쓴 글꼴은 없어요. 돌리고 비추는 글은 글꼴의 대칭이 읽기를 정했어요. Boustrophedon 의 3×5 대문자에서 좌우가 같은 글자는 A, H, I, M, N, O, T, U, V, W, X, Y 열둘이라 두루미 대목 170자 가운데 98자(58%)가 거울에 비춰도 그대로이고, 180도 돌려도 같은 글자는 H, I, O, S, X, Z 여섯이라 55자(32%)뿐이에요. 그래서 역부스트로페돈의 거꾸로 선 줄이 거울 줄보다 훨씬 낯설어요. Calculator Words 는 글자 표 없이 화면을 (63 − x, 127 − y) 로 옮겨 찍기만 해서 3 의 a, b, c, d, g 획이 d, e, f, a, g 인 E 가 되고, 계산기가 숫자를 오른쪽에 붙이니 뒤집힌 낱말은 왼쪽에 붙어 남은 칸의 흐린 8 이 hELLO888 처럼 뒤에 줄지어 서요. 접는 글은 먹의 양을 고르게 했어요. Nine-fold Seal 의 네 글은 민 글자일 때 칸의 12.4% 에서 15.2% 가 인주인데, 다 접으면 50.4% 에서 52.3% 사이로 모여요. 한 LED 선과 한 LED 틈이 번갈면 절반이 상한이라 무슨 글이든 같은 밀도에 닿아요. 빛으로 모으는 글은 지도의 차례가 자국을 남겼어요. Caustic Letters 의 Focus 를 1 로 올려 빛이 초점을 지나치면 글자 사이 빈 자리에 가로 실선이 몇 가닥 지나가는데, 높이를 먼저 맞추고 가로를 나중에 맞춘 크노테 지도의 줄 이음매예요. 녹는 글은 붓는 길이 물을 물로 읽히게 했어요. Letter Melt 의 첫 판은 입자마다 웅덩이에서 제 자리로 곧장 날아가 점구름이 올라갔는데, 글자 폭 16 LED 마다 주둥이 하나를 두자 실줄기로 올라가고, 그 굵기가 주둥이마다 맡은 몫을 그대로 보여요. + +기능 감은 이번에도 글자를 코드 밖에서 받는 길로 모였고, 받으려는 것이 다섯 다 달랐어요. Nine-fold Seal 은 Lab 에서 친 글을 문자열로 받기를 바랐어요. 위의 초성, 중성, 받침으로 된 음절이면 무엇이든 접는데 글이 코드에 박혀 있어서 제 이름으로 도장을 찍어 보려면 코드를 고쳐야 해요. Letter Melt 는 Text 레이어의 덮임 비트맵을 거푸집으로 받기를, Caustic Letters 는 코드 레이어가 불러 쓸 획 글꼴을 바랐어요. 「빛」의 ㅊ 꼭지를 세로 획으로 그렸더니 패널에서 大 로 읽혀서 가로 획으로 바꿨어요. Calculator Words 는 레이어를 돌리고 줄이는 변환을, Boustrophedon 은 Text 레이어가 줄마다 번갈아 좌우를 뒤집거나 180도 돌리는 칸을 바랐어요. + +69판 뒤에 해 볼 것이에요. 7세그먼트를 돌려 한글 자모로 읽는 놀이가 있어요. 2 는 그대로 ㄹ, 돌린 7 은 ㄴ, 0 은 ㅁ, 1 은 ㅣ 로 읽혀서 숫자 몇 개로 「르」나 「니」 같은 음절을 칠 수 있어요. Nine-fold Seal 의 접기 규칙을 백문(白文)으로 뒤집어 글자를 종이색으로 남기고 바탕을 인주로 찍거나, 칸의 마디를 한 가닥 길로 모두 이어 한 음절이 한붓그리기 미로가 되게 해 볼 만해요. 롱고롱고처럼 아래 왼쪽 줄에서 시작해 위로 올라가며 판을 돌려 읽는 쓰기나, 한 단은 내려가고 다음 단은 올라오는 세로 부스트로페돈도 Boustrophedon 의 밭에서 돼요. 같은 64×128 바닥에서 크노테 지도 대신 싱크혼(Sinkhorn) 반복으로 푼 최적 수송 지도를 미리 구워 두면, Focus 를 지나칠 때 두 지도가 남기는 자국을 나란히 세울 수 있어요. 클라베 등의 논문에서 Letter Melt 가 뺀 점탄성 용수철을 더하면 녹은 획이 꿀처럼 실을 끌다가 끊어질 거예요. + ### 화장품 팝업 주제 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 46133e8073b99e4b7b8ec389d62cfdfd5b878308 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 16:55:06 +0000 Subject: [PATCH 20/21] =?UTF-8?q?69=ED=8C=90=20index.html=20=EC=9D=84=20?= =?UTF-8?q?=EB=8B=A4=EC=8B=9C=20=EC=A7=93=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6PxvAVdZrqAhycB4c9 --- 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 0cd34775e..9e44fc883 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

패턴 연구

- - + + - + +