From 578161939f9eccf73f0790c369f735e86ce27ffc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 21:30:24 +0000 Subject: [PATCH 1/8] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=20=EC=97=B0=EA=B5=AC=2073=ED=8C=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6N31ff3CzvqRufZu6W From 9f4f8980833376d115c93d11989aa84a2c666820 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 22:04:22 +0000 Subject: [PATCH 2/8] pattern-study: add multiple-master and voss-dice Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6N31ff3CzvqRufZu6W --- .../pattern-study/patterns/multiple-master.js | 382 ++++++++++++++++++ incoming/pattern-study/patterns/voss-dice.js | 185 +++++++++ 2 files changed, 567 insertions(+) create mode 100644 incoming/pattern-study/patterns/multiple-master.js create mode 100644 incoming/pattern-study/patterns/voss-dice.js diff --git a/incoming/pattern-study/patterns/multiple-master.js b/incoming/pattern-study/patterns/multiple-master.js new file mode 100644 index 000000000..43aee1c70 --- /dev/null +++ b/incoming/pattern-study/patterns/multiple-master.js @@ -0,0 +1,382 @@ +// ===== Patternflow pattern ===== +// Title: Multiple Master +// Family: type +// Idea: The word MASTER is stored as four master outlines, light and black at condensed and extended width, all with the same points in the same order, and the big word on top is the instance made by blending the four point lists bilinearly at the weight and width of the knobs, while it wanders slowly round the knob point on the design-space square below, whose corners hold the four masters of one letter at a time. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Multiple Master fonts (Adobe, 1991; the first faces Myriad MM and Minion MM, 1992), the axis tags wght and wdth of OpenType Font Variations (2016), letters and masters drawn for this pattern, pattern round 73 +// Round: 73 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Weight=-1..1, Width=-1..1 +// +// Knob 1: Hue. Turns the picture round the wheel: at 0 the word is pale azure, the design space blue and the instance dot gold. +// Knob 2: Speed. How fast the instance wanders round the knob point and the corners change letter: at 2 the slowest part of the wander comes round in 14 s and each letter holds the corners 3 s; at 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Weight. Where the knob point sits on the weight axis, light at -1 and black at 1; the instance wanders up to 0.24 of the axis either side of it and folds back at the ends. The stems go from 1.6 LEDs to 4.1 at black condensed and 5.6 at black extended, the counters of A, R and S close up and the vertex of M lifts off the base line. +// Knob 4: Width. The same on the width axis, condensed at -1 and extended at 1: the letters and the space between them stretch together, the top line from 29 LEDs of ink at light condensed to 55 at light extended and 58 at black extended. +// Buttons: K3 tap leans the word 8 degrees, or stands it up again: a slant axis, whose ends differ by a shear alone. + +const W = 64, H = 128, N = W * H; +const { floor, ceil, round, min, max, abs, sqrt, sin, cos, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ss = (a, b, v) => { + const t = cl((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; + +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 masters. Master m is weight (m >> 1) and width (m & 1): 0 light +// condensed, 1 light extended, 2 black condensed, 3 black extended. Units are +// the cap height, y up from the base line. Each letter is drawn by one +// function from the master's numbers below, so all four come out with the +// same contours of the same point counts; the code checks that and stops if +// they do not. Pieces overlap (an E is a stem and three arms) and fill by +// the nonzero rule, as OpenType variable outlines may; Type 1 MM outlines +// could not overlap. +const SV = [0.072, 0.076, 0.185, 0.255]; // vertical stem +const SB = [0.05, 0.085, 0.042, 0.072]; // side bearing +// Body width of each letter at the four masters, and how much of the side +// bearing it keeps on the left and right (open sides take less). +const BW = { + M: [0.48, 0.88, 0.64, 0.94], + A: [0.4, 0.78, 0.52, 0.84], + S: [0.34, 0.62, 0.5, 0.7], + T: [0.36, 0.68, 0.44, 0.74], + E: [0.3, 0.56, 0.42, 0.62], + R: [0.38, 0.66, 0.5, 0.74], +}; +const SIDE = { M: [1, 1], A: [0.25, 0.25], S: [0.8, 0.8], T: [0.3, 0.3], E: [1, 0.6], R: [1, 0.45] }; + +const rect = (x0, y0, x1, y1) => [x0, y0, x1, y0, x1, y1, x0, y1]; +// A ring between two superellipses from angle a0 to a1 (degrees), n points +// along each edge: the bowls of S and R. +function ring(cx, cy, rxo, ryo, rxi, ryi, a0, a1, n, ex) { + const o = [], e = 2 / ex; + const pt = (rx, ry, a) => { + const c = cos((a * PI) / 180), s = sin((a * PI) / 180); + o.push(cx + rx * Math.sign(c) * abs(c) ** e, cy + ry * Math.sign(s) * abs(s) ** e); + }; + for (let i = 0; i < n; i++) pt(rxo, ryo, a0 + ((a1 - a0) * i) / (n - 1)); + for (let i = n - 1; i >= 0; i--) pt(rxi, ryi, a0 + ((a1 - a0) * i) / (n - 1)); + return o; +} +// The horizontal width of a stroke of perpendicular thickness t that rises +// dy over dx: t * length / dy, solved with the stroke's own width in dx. +function slantW(t, dx0, k, dy) { + let hx = t; + for (let i = 0; i < 4; i++) { + const dx = dx0 - k * hx; + hx = (t * sqrt(dx * dx + dy * dy)) / dy; + } + return hx; +} + +function design(ch, m) { + const w = m >> 1, bw = BW[ch][m], sv = SV[m], sh = sv * (w ? 0.84 : 0.92), td = sv * 0.96; + const c = []; + if (ch === "M") { + // Vertex on the base line when light; lifted 0.15 at black condensed so + // the two notches keep some light. + const vx = bw / 2, vy = [0, 0, 0.15, 0.05][m], hx = slantW(td, vx, 0, 1 - vy); + c.push(rect(0, 0, sv, 1), rect(bw - sv, 0, bw, 1)); + c.push([0, 1, hx, 1, vx + hx / 2, vy, vx - hx / 2, vy]); + c.push([bw, 1, bw - hx, 1, vx - hx / 2, vy, vx + hx / 2, vy]); + } else if (ch === "A") { + // Flat apex ft wide; the bar drops as the weight rises so the counter + // above it is the last thing to close. + const ft = [0.05, 0.06, 0.12, 0.16][m], yb = [0.27, 0.27, 0.12, 0.2][m]; + const dx = bw / 2 - ft / 2, hx = slantW(td, dx, 0, 1); + c.push([0, 0, hx, 0, dx + hx, 1, dx, 1]); + c.push([bw, 0, bw - hx, 0, bw - dx - hx, 1, bw - dx, 1]); + const yt = yb + sh * 0.9; + c.push(rect(dx * yt, yb, bw - dx * yt, yt)); + } else if (ch === "S") { + // Two rings of superellipses, the upper a little narrower, meeting in the + // spine at y = 0.53, with the terminals cut at a0 degrees. + const mid = 0.53, a0 = [34, 30, 44, 38][m], ex = [2.2, 2.3, 2.7, 2.8][m]; + const ryU = (1 - mid + sh / 2) / 2, ryL = (mid + sh / 2) / 2, rxU = bw * 0.47, rxL = bw * 0.5; + c.push(ring(bw / 2, 1 - ryU, rxU, ryU, rxU - sv, ryU - sh, a0, 270, 16, ex)); + c.push(ring(bw / 2, ryL, rxL, ryL, rxL - sv, ryL - sh, 90, a0 - 180, 16, ex)); + } else if (ch === "T") { + c.push(rect(0, 1 - sh, bw, 1), rect(bw / 2 - sv / 2, 0, bw / 2 + sv / 2, 1)); + } else if (ch === "E") { + const mE = 0.52; + c.push(rect(0, 0, sv, 1), rect(0, 1 - sh, bw, 1), rect(0, 0, bw, sh), rect(0, mE - sh / 2, bw * 0.88, mE + sh / 2)); + } else if (ch === "R") { + const yl = [0.44, 0.44, 0.4, 0.41][m], bowlR = bw * [0.93, 0.9, 0.94, 0.92][m]; + const bx = sv + 0.4 * (bowlR - 2 * sv), rxo = bowlR - bx, ryo = (1 - yl) / 2, ex = [2.2, 2.3, 2.7, 2.8][m]; + c.push(rect(0, 0, sv, 1), rect(0, 1 - sh, bx, 1), rect(0, yl, bx, yl + sh)); + c.push(ring(bx, yl + ryo, rxo, ryo, rxo - sv, ryo - sh, 90, -90, 14, ex)); + const top = yl + sh, lx = bx - 0.3 * td, hx = slantW(td, bw - lx, 1, top); + c.push([lx, top, lx + hx, top, bw, 0, bw - hx, 0]); + } + // Every contour counter-clockwise (y up), so overlapping pieces add. + for (const p of c) { + let a = 0; + for (let i = 0; i < p.length; i += 2) { + const j = (i + 2) % p.length; + a += p[i] * p[j + 1] - p[j] * p[i + 1]; + } + if (a < 0) for (let i = 0, j = p.length - 2; i < j; i += 2, j -= 2) [p[i], p[i + 1], p[j], p[j + 1]] = [p[j], p[j + 1], p[i], p[i + 1]]; + } + const sb = SB[m]; + return { pts: c.map((p) => Float32Array.from(p)), bw, l: sb * SIDE[ch][0], r: sb * SIDE[ch][1] }; +} + +const GLYPH = {}; +for (const ch of "MASTER") { + const ms = [0, 1, 2, 3].map((m) => design(ch, m)); + // Same contours, same point counts, in all four masters. + for (const g of ms) { + if (g.pts.length !== ms[0].pts.length) throw new Error("Multiple Master: contour count differs in " + ch); + g.pts.forEach((p, i) => { + if (p.length !== ms[0].pts[i].length) throw new Error("Multiple Master: point count differs in " + ch); + }); + } + GLYPH[ch] = ms; +} +const LINES = ["MAS", "TER"]; + +// ---- Size. The widest line at any master, with the full slant added, sets +// the cap height: MAS at black extended is 2.65 caps of ink, plus 0.14 of +// lean, so 62 LEDs hold a cap height of 22. +const SLANT = 0.14; +function inkWidth(line, m) { + let a = 0; + for (const ch of line) a += GLYPH[ch][m].l + GLYPH[ch][m].bw + GLYPH[ch][m].r; + return a - GLYPH[line[0]][m].l - GLYPH[line[line.length - 1]][m].r; +} +let widest = 0; +for (const line of LINES) for (let m = 0; m < 4; m++) widest = max(widest, inkWidth(line, m)); +const CAP = floor(62 / (widest + SLANT)); +const BASE = [5 + CAP, 14 + 2 * CAP]; + +// ---- The design-space square: width across, weight down, corners at +// (SQ_X0, SQ_Y0) and (SQ_X0 + SQ, SQ_Y0 + SQ); the master glyphs sit +// outside each corner, 11 LEDs high. +const SQ = 32, SQ_X0 = 16, SQ_Y0 = BASE[1] + 21, MCAP = 11; +// A 3 by 5 pixel font for the four letters of the axis names. +const TINY = { + W: ["#.#", "#.#", "###", "###", "#.#"], + D: ["##.", "#.#", "#.#", "#.#", "##."], + T: ["###", ".#.", ".#.", ".#.", ".#."], + H: ["#.#", "#.#", "###", "#.#", "#.#"], + G: [".##", "#..", "#.#", "#.#", ".##"], +}; +for (const k in TINY) TINY[k] = TINY[k].join(""); + +// ---- Fill with anti-aliased coverage. Five sub-rows a row; on each, the +// crossings with every edge are sorted and walked with the nonzero rule, +// and each span adds its exact horizontal overlap with every LED. +const SUB = 5, EDGE = new Float32Array(5 * 1200), XS = new Float32Array(256), DS = new Int8Array(256), ROW = new Float32Array(W); +function fill(cov, polys, wgt) { + let ne = 0, ylo = 1e9, yhi = -1e9; + for (const p of polys) + for (let i = 0; i < p.length; i += 2) { + const j = (i + 2) % p.length, y0 = p[i + 1], y1 = p[j + 1]; + if (y0 === y1) continue; + const o = ne * 5; + EDGE[o] = p[i], EDGE[o + 1] = y0, EDGE[o + 2] = p[j], EDGE[o + 3] = y1, EDGE[o + 4] = y1 > y0 ? 1 : -1; + ne++; + ylo = min(ylo, y0, y1), yhi = max(yhi, y0, y1); + } + const r0 = max(0, floor(ylo)), r1 = min(H - 1, ceil(yhi)); + for (let r = r0; r <= r1; r++) { + ROW.fill(0); + for (let s = 0; s < SUB; s++) { + const ys = r + (s + 0.5) / SUB; + let n = 0; + for (let e = 0; e < ne; e++) { + const o = e * 5, y0 = EDGE[o + 1], y1 = EDGE[o + 3]; + if (y0 < y1 ? ys < y0 || ys >= y1 : ys < y1 || ys >= y0) continue; + const x = EDGE[o] + ((ys - y0) * (EDGE[o + 2] - EDGE[o])) / (y1 - y0); + let k = n++; + while (k > 0 && XS[k - 1] > x) (XS[k] = XS[k - 1]), (DS[k] = DS[k - 1]), k--; + XS[k] = x, DS[k] = EDGE[o + 4]; + } + let wind = 0, xa = 0; + for (let k = 0; k < n; k++) { + const was = wind; + wind += DS[k]; + if (was === 0 && wind !== 0) xa = XS[k]; + else if (was !== 0 && wind === 0) span(xa, XS[k]); + } + } + const o = r * W; + for (let x = 0; x < W; x++) if (ROW[x] > 0) cov[o + x] = min(1, cov[o + x] + (ROW[x] / SUB) * wgt); + } +} +function span(a, b) { + a = max(0, a), b = min(W, b); + if (b <= a) return; + const ia = floor(a), ib = floor(b); + if (ia === ib) return void (ROW[ia] += b - a); + ROW[ia] += ia + 1 - a; + for (let i = ia + 1; i < ib; i++) ROW[i] += 1; + if (ib < W) ROW[ib] += b - ib; +} + +// One glyph at the blend c (four master weights) into panel polygons: x0 is +// the left of its body, base the base line, cap the cap height in LEDs. +function place(ch, c, x0, base, cap, lean, out) { + const g = GLYPH[ch]; + for (let k = 0; k < g[0].pts.length; k++) { + const a = g[0].pts[k], b = g[1].pts[k], d = g[2].pts[k], e = g[3].pts[k]; + const p = new Float32Array(a.length); + for (let i = 0; i < a.length; i += 2) { + const x = c[0] * a[i] + c[1] * b[i] + c[2] * d[i] + c[3] * e[i]; + const y = c[0] * a[i + 1] + c[1] * b[i + 1] + c[2] * d[i + 1] + c[3] * e[i + 1]; + p[i] = x0 + (x + lean * (y - 0.5)) * cap; + p[i + 1] = base - y * cap; + } + out.push(p); + } +} +const mix = (c, ch, key) => c[0] * GLYPH[ch][0][key] + c[1] * GLYPH[ch][1][key] + c[2] * GLYPH[ch][2][key] + c[3] * GLYPH[ch][3][key]; + +export function setup(params) { + params.s = { + ph: 2.1, lc: 0, u: 0.5, v: 0.5, u0: 0.5, v0: 0.5, hue: 0, lean: 0, leanT: 0, + trail: [], word: new Float32Array(N), map: new Float32Array(N), rgb: new Float32Array(N * 3), + }; +} + +// Reflect into the square, so the wander folds back at an edge. +const fold = (x) => cl(x < 0 ? -x : x > 1 ? 2 - x : x, 0, 1); + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + const speed = cl(k[1], 0.2, 3); + S.u0 = (cl(k[2], -1, 1) + 1) / 2; + S.v0 = (cl(k[3], -1, 1) + 1) / 2; + if (input.btnPressed && input.btnPressed[2]) S.leanT = 1 - S.leanT; + S.lean += cl(S.leanT - S.lean, -d * 2, d * 2); + // The wander: two sines an axis at unrelated rates, 0.24 of the square at + // most each way, so it never closes into a figure. 0.42 rad/s a unit of + // Speed: the slowest term comes round in 14 s at 2. + S.ph += d * speed * 0.42; + const A = 0.24, p = S.ph; + S.u = fold(S.u0 + A * (0.62 * sin(0.71 * p + 0.3) + 0.38 * sin(1.37 * p + 2.1))); + S.v = fold(S.v0 + A * (0.62 * sin(0.53 * p + 1.7) + 0.38 * sin(1.19 * p + 0.6))); + S.trail.push(S.u, S.v); + if (S.trail.length > 72) S.trail.splice(0, 2); + S.lc += (d * speed) / 6; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { word, map, rgb } = S, u = S.u, v = S.v; + const c = [(1 - u) * (1 - v), (1 - u) * v, u * (1 - v), u * v]; + const lean = SLANT * ss(0, 1, S.lean); + const hue = 0.54 + S.hue; + const ink = hsv(hue, 0.4, 1), guide = hsv(hue, 0.7, 0.17), blue = hsv(hue + 0.06, 0.62, 1), gold = hsv(hue + 0.58, 0.68, 1); + const li = floor(S.lc) % 6, lf = S.lc - floor(S.lc), fresh = ss(0, 0.08, lf); + const letter = "MASTER"[li], before = "MASTER"[(li + 5) % 6]; + + // ---- The instance: two lines, centred on their ink. + word.fill(0); + const ticks = []; + for (let L = 0; L < 2; L++) { + const line = LINES[L], polys = []; + let ink0 = 0; + for (const ch of line) ink0 += mix(c, ch, "l") + mix(c, ch, "bw") + mix(c, ch, "r"); + ink0 -= mix(c, line[0], "l") + mix(c, line[2], "r"); + let x = (W - ink0 * CAP) / 2 - mix(c, line[0], "l") * CAP; + ticks.push([L, x]); + for (let i = 0; i < 3; i++) { + const ch = line[i], l = mix(c, ch, "l"), bw = mix(c, ch, "bw"), r = mix(c, ch, "r"); + place(ch, c, x + l * CAP, BASE[L], CAP, lean, polys); + if (L * 3 + i === li) ticks.push([L, x + (l + bw / 2) * CAP, 1]); + x += (l + bw + r) * CAP; + ticks.push([L, x]); + } + fill(word, polys, 1); + } + + // ---- The masters of one letter at the four corners, the incoming letter + // fading over the first 0.24 s at Speed 2. + map.fill(0); + const corner = (ch, wgt) => { + for (let m = 0; m < 4; m++) { + const g = GLYPH[ch][m], cm = [0, 0, 0, 0]; + cm[m] = 1; + const right = m & 1, low = m >> 1, polys = []; + const x0 = right ? SQ_X0 + SQ + 3 : SQ_X0 - 3 - g.bw * MCAP; + const base = low ? SQ_Y0 + SQ + 3 + MCAP : SQ_Y0 - 3; + place(ch, cm, x0, base, MCAP, lean, polys); + fill(map, polys, wgt * (0.5 + 0.5 * c[m])); + } + }; + corner(letter, fresh); + if (fresh < 1) corner(before, 1 - fresh); + + rgb.fill(0); + const add = (x, y, col, a) => { + if (x < 0 || y < 0 || x >= W || y >= H || a <= 0) return; + const o = (y * W + x) * 3; + rgb[o] = max(rgb[o], col[0] * a), rgb[o + 1] = max(rgb[o + 1], col[1] * a), rgb[o + 2] = max(rgb[o + 2], col[2] * a); + }; + // Cap line and base line of each line of type. + for (let L = 0; L < 2; L++) + for (let x = 0; x < W; x++) { + add(x, BASE[L], guide, 1); + add(x, BASE[L] - CAP - 1, guide, 1); + } + // Side-bearing ticks under the base line, and a gold dot under the letter + // the corners show. + for (const [L, x, mark] of ticks) { + const xi = round(x - 0.5), y = BASE[L] + 2; + if (mark) { + add(xi, y + 1, gold, 1), add(xi + 1, y + 1, gold, 1), add(xi, y + 2, gold, 0.5), add(xi + 1, y + 2, gold, 0.5); + } else for (let j = 0; j < 3; j++) add(xi, y + j, blue, 0.32); + } + // The square, its quarter lines, and pale lines from the instance to each + // master, as bright as that master's share of the blend. + for (let i = 0; i <= SQ; i++) + for (const q of [0, 1, 2, 3, 4]) { + const a = q === 0 || q === 4 ? 0.42 : 0.13; + add(SQ_X0 + i, SQ_Y0 + (q * SQ) / 4, blue, a); + add(SQ_X0 + (q * SQ) / 4, SQ_Y0 + i, blue, a); + } + // Axis names as the OpenType variation tags spell them: wdth across the + // top, wght down the left side. + const label = (s, x, y, dx, dy) => { + for (let i = 0; i < s.length; i++) + for (let b = 0; b < 15; b++) if (TINY[s[i]][b] === "#") add(x + i * dx + (b % 3), y + i * dy + floor(b / 3), blue, 0.5); + }; + label("WDTH", SQ_X0 + SQ / 2 - 7, SQ_Y0 - 7, 4, 0); + label("WGHT", SQ_X0 - 9, SQ_Y0 + SQ / 2 - 11, 0, 6); + const px = SQ_X0 + v * SQ, py = SQ_Y0 + u * SQ; + for (let m = 0; m < 4; m++) { + const cx = SQ_X0 + (m & 1) * SQ, cy = SQ_Y0 + (m >> 1) * SQ, n = ceil(max(abs(cx - px), abs(cy - py))); + for (let i = 1; i < n; i++) add(round(px + ((cx - px) * i) / n), round(py + ((cy - py) * i) / n), gold, 0.08 + 0.25 * c[m]); + } + // The knob point: a small cross. + const kx = round(SQ_X0 + S.v0 * SQ), ky = round(SQ_Y0 + S.u0 * SQ); + for (let j = -2; j <= 2; j++) if (j) add(kx + j, ky, gold, 0.4), add(kx, ky + j, gold, 0.4); + // Trail, oldest dimmest, then the instance itself. + const T = S.trail, nt = T.length / 2; + for (let i = 0; i < nt; i++) add(round(SQ_X0 + T[i * 2 + 1] * SQ), round(SQ_Y0 + T[i * 2] * SQ), gold, 0.1 + 0.5 * (i / nt)); + for (let j = -1; j <= 1; j++) + for (let i = -1; i <= 1; i++) { + const dx = round(px) + i + 0.5 - px, dy = round(py) + j + 0.5 - py; + add(round(px) + i, round(py) + j, gold, cl(1.25 - 0.55 * sqrt(dx * dx + dy * dy), 0, 1)); + } + + // ---- Ink over everything. + for (let p = 0; p < N; p++) { + const o = p * 3, a = word[p], b = map[p]; + if (a > 0) for (let q = 0; q < 3; q++) rgb[o + q] = rgb[o + q] * (1 - a) + ink[q] * a; + if (b > 0) for (let q = 0; q < 3; q++) rgb[o + q] = max(rgb[o + q], ink[q] * b); + } + for (let p = 0; p < N; p++) display.setPixel(p % W, (p / W) | 0, round(min(1, rgb[p * 3]) * 255), round(min(1, rgb[p * 3 + 1]) * 255), round(min(1, rgb[p * 3 + 2]) * 255)); +} diff --git a/incoming/pattern-study/patterns/voss-dice.js b/incoming/pattern-study/patterns/voss-dice.js new file mode 100644 index 000000000..7905de6e6 --- /dev/null +++ b/incoming/pattern-study/patterns/voss-dice.js @@ -0,0 +1,185 @@ +// ===== Patternflow pattern ===== +// Title: Voss Dice +// Family: noise +// Idea: A row of dice stands for the bits of a binary counter and at every step the dice whose bits changed are rerolled, so die k rerolls every 2^k steps and their sum is pink 1/f noise, drawn flowing down the panel between white noise (all the dice rerolled every step) on the left and brown noise (a walk stepped by one die) on the right. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Richard F. Voss's dice for 1/f noise, as Martin Gardner gave it with white and brown music in Mathematical Games (Scientific American, April 1978), pattern round 73 +// Round: 73 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Dice=4..8, Hold=-1..1 +// +// Knob 1: Hue. Turns the dice and the middle and right traces round the wheel together: at 0 the middle trace and the dice's flashes are pink and the right trace brown. The left trace stays near white. +// Knob 2: Speed. Steps a second: 6 at 2, 0.6 at 0.2, 9 at 3. Each step moves the traces down 2 rows. +// Knob 3: Dice. How many dice, 4 to 8, the same for all three traces. The slowest die of the middle trace rerolls every 8 steps with 4 dice and every 128 with 8, so each die more adds an octave at the low end. From 5 dice up the faces shrink from 14 to 7 LEDs. +// Knob 4: Hold. How much longer each die holds its face than the one to its right. At the opening 0.06 it is exactly 2, Voss's counter; at -0.94 and below it is 1, every die rerolls every step and the middle trace turns white like the left; at 1 it is 3.8, die 1 rerolls every 3 or 4 steps and die 3 every 56, so the middle trace jitters by one die round levels that seldom move. + +const W = 64, H = 128, N = W * H; +const { floor, round, min, max, abs, sqrt, exp, pow } = 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]; +} + +const roll = () => 1 + floor(Math.random() * 6); + +// ---- Faces on a 7 by 7 grid: pips one cell at columns and rows 1, 3, 5, +// the four corner cells left dark to round the die. Drawn at 2 LEDs a cell +// for 4 dice (14 LED faces) and 1 for 5 to 8. +const PIPS = [ + [], + [[3, 3]], + [[1, 1], [5, 5]], + [[1, 1], [3, 3], [5, 5]], + [[1, 1], [5, 1], [1, 5], [5, 5]], + [[1, 1], [5, 1], [3, 3], [1, 5], [5, 5]], + [[1, 1], [5, 1], [1, 3], [5, 3], [1, 5], [5, 5]], +]; + +// ---- The three games. Every step: +// white: all n dice rerolled, the sum taken; +// pink: Voss's dice, die k rerolled when floor(step / r^k) changes, which for +// r = 2 is when bit k of the step count changes, so die 0 every step, +// die 1 every 2nd, die k every 2^k-th; +// brown: a walk that moves by a die's roll less 3.5 (-2.5 to +2.5) and folds +// back at the ends of the window the other two are drawn in. +// The window is the mean of n dice, 3.5n, plus and minus 2.6 standard +// deviations (each die's variance is 35/12): of 4 to 24 for 4 dice that is +// 5.1 to 22.9, of 8 to 48 for 8 dice 15.4 to 40.6. Values are kept as -1..1 +// across the window, so the history survives a change of Dice. +// Checked over 400,000 steps: 1.875 dice reroll a step on average with 4 +// dice and 1.992 with 8, so the pink sum moves 2.56 and 2.61 a step against +// 3.87 and 5.46 for the white; the brown always moves 1.5. +const HIST = 140; + +function windowOf(n) { + return [3.5 * n, 2.6 * sqrt((n * 35) / 12)]; +} + +function step(S) { + const n = S.n, r = S.r, t = ++S.count; + for (let k = 0; k < n; k++) { + const p = pow(r, k); + if (floor(t / p) !== floor((t - 1) / p)) { + S.dice[k] = roll(); + S.flash[k] = 1; + } + } + let pink = 0, white = 0; + for (let k = 0; k < n; k++) (pink += S.dice[k]), (white += roll()); + const [mean, half] = windowOf(n); + let b = S.brown + roll() - 3.5; + if (b > mean + half) b = 2 * (mean + half) - b; + if (b < mean - half) b = 2 * (mean - half) - b; + S.brown = b; + const at = S.head = (S.head + 1) % HIST; + S.hw[at] = (white - mean) / half; + S.hp[at] = (pink - mean) / half; + S.hb[at] = (b - mean) / half; +} + +export function setup(params) { + params.s = { + n: 4, r: 2, count: 0, ph: 0, hue: 0, ready: false, + dice: new Int8Array(8).fill(1), flash: new Float32Array(8), brown: 14, + head: 0, hw: new Float32Array(HIST), hp: new Float32Array(HIST), hb: new Float32Array(HIST), + rgb: new Float32Array(N * 3), + }; + for (let k = 0; k < 8; k++) params.s.dice[k] = roll(); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 4, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + const speed = cl(k[1], 0.2, 3), n = cl(round(k[2]), 4, 8); + // Hold ratio: 2^(k + 0.94), so the Lab's opening 0.06 gives exactly 2. + S.r = max(1, pow(2, cl(k[3], -1, 1) + 0.94)); + if (n !== S.n) { + // Dice added come in rolled; the walk keeps its place in the window. + const [m0, h0] = windowOf(S.n), [m1, h1] = windowOf(n); + S.brown = m1 + ((S.brown - m0) / h0) * h1; + S.n = n; + } + if (!S.ready) { + // A full panel of history before the first frame: 140 steps. + S.brown = 3.5 * n; + for (let i = 0; i < HIST; i++) step(S); + S.flash.fill(0); + S.ready = true; + } + S.ph += d * speed * 3; + while (S.ph >= 1) { + S.ph -= 1; + step(S); + } + // A reroll flash fades in a quarter of a second. + const f = exp(-d / 0.09); + for (let i = 0; i < 8; i++) S.flash[i] *= f; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { rgb, n } = S, hue = S.hue; + const pinkC = hsv(0.93 + hue, 0.5, 1), brownC = hsv(0.07 + hue, 0.82, 0.68), whiteC = hsv(0.6 + hue, 0.06, 0.92); + const ivory = hsv(0.93 + hue, 0.1, 0.78), hot = hsv(0.93 + hue, 0.65, 1); + rgb.fill(0); + const put = (x, y, c, a) => { + if (x < 0 || y < 0 || x >= W || y >= H || a <= 0) return; + const o = (y * W + x) * 3; + rgb[o] = max(rgb[o], c[0] * a), rgb[o + 1] = max(rgb[o + 1], c[1] * a), rgb[o + 2] = max(rgb[o + 2], c[2] * a); + }; + + // ---- Dice row, the counter's bit lamps under it. + const cell = n <= 4 ? 2 : 1, face = 7 * cell, pitch = floor(W / n), x0 = floor((W - pitch * n + pitch - face) / 2); + const bitsY = 2 + face + cell, top = bitsY + 2 * cell + 3; + for (let k = 0; k < n; k++) { + // Die 0, the fastest, on the right, so the lamps read as the step count + // in binary. + const dx = x0 + (n - 1 - k) * pitch, fl = S.flash[k]; + const col = [0, 1, 2].map((q) => ivory[q] + (hot[q] - ivory[q]) * fl); + const pips = PIPS[S.dice[k]]; + for (let j = 0; j < 7; j++) + for (let i = 0; i < 7; i++) { + if ((i === 0 || i === 6) && (j === 0 || j === 6)) continue; + let pip = false; + for (const [pi, pj] of pips) if (pi === i && pj === j) pip = true; + const a = pip ? 0.05 * (1 - fl) : 0.9 + 0.1 * fl; + for (let v = 0; v < cell; v++) for (let u = 0; u < cell; u++) put(dx + i * cell + u, 2 + j * cell + v, col, a); + } + const bit = floor(S.count / pow(S.r, k)) & 1; + for (let u = cell; u < face - cell; u++) for (let v = 0; v < cell; v++) put(dx + u, bitsY + v, pinkC, bit ? 0.9 : 0.12); + } + + // ---- Three traces, newest at the top, 2 rows a step, sliding down + // smoothly between steps. Each is a step plot: a bright line at the value + // with a dim fill back to the column's middle. + const R = 2, cols = [[0, S.hw, whiteC], [22, S.hp, pinkC], [44, S.hb, brownC]]; + for (const [cx, hist, c] of cols) { + const mid = cx + 9.5; + let prev = null; + for (let y = top; y < H; y++) { + const age = floor((y - top) / R - S.ph + 1); + if (age >= HIST) break; + const v = hist[(S.head - max(0, age) + HIST * 2) % HIST]; + const x = mid + cl(v, -1, 1) * 9; + // Fill from the middle. + const a = min(x, mid), b = max(x, mid); + for (let i = floor(a); i <= floor(b); i++) put(i, y, c, 0.2); + // The line, split over two LEDs by where it falls between them, and a + // horizontal run where the value changed since the row above. + const xi = floor(x - 0.5), fr = x - 0.5 - xi; + put(xi, y, c, 1 - fr), put(xi + 1, y, c, fr); + if (prev !== null && abs(prev - x) > 1) for (let i = round(min(prev, x)); i <= round(max(prev, x)) - 1; i++) put(i, y, c, 0.85); + prev = x; + } + } + + for (let p = 0; p < N; p++) display.setPixel(p % W, (p / W) | 0, round(min(1, rgb[p * 3]) * 255), round(min(1, rgb[p * 3 + 1]) * 255), round(min(1, rgb[p * 3 + 2]) * 255)); +} From 9c57f28097d3db5af40c71ff953f59d03ba00d7e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 22:06:00 +0000 Subject: [PATCH 3/8] pattern-study: add damask and steiner-deltoid Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6N31ff3CzvqRufZu6W --- incoming/pattern-study/patterns/damask.js | 204 +++++++++++++ .../pattern-study/patterns/steiner-deltoid.js | 279 ++++++++++++++++++ 2 files changed, 483 insertions(+) create mode 100644 incoming/pattern-study/patterns/damask.js create mode 100644 incoming/pattern-study/patterns/steiner-deltoid.js diff --git a/incoming/pattern-study/patterns/damask.js b/incoming/pattern-study/patterns/damask.js new file mode 100644 index 000000000..05ea227f4 --- /dev/null +++ b/incoming/pattern-study/patterns/damask.js @@ -0,0 +1,204 @@ +// ===== Patternflow pattern ===== +// Title: Damask +// Family: type +// Idea: A cloth of one ivory thread woven at one LED per crossing carries a big word in warp-faced satin on a weft-faced satin ground, so only the light tells letter from ground, and as a low lamp walks round the table the word flips from brighter than the cloth to darker than it and back. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: damask weave (figure in warp-faced satin on a weft-faced satin ground of the same thread), five-end satin, anisotropic thread sheen (James Kajiya and Timothy Kay, 1989), pattern round 73 +// Round: 73 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Word=0..3, Lamp=0..1 +// +// Knob 1: Hue. The colour of the one thread, always pale: linen ivory at 0, mint at 0.25, a cold blue at 0.5 and rose at 0.75. +// Knob 2: Speed. How fast the lamp walks round the table: once in 64 s at 2, so the word turns from light to dark every 16 s; 43 s at 3 and over ten minutes at 0.2. +// Knob 3: Word. What the cloth says: LUX, the Hangul syllable 빛 (light), NOX, or 밤 (night). Each word is woven with the stem width that makes it 10 to 11% of the cloth: 4.0 LEDs for LUX, 3.3 to 3.5 for the others. +// Knob 4: Lamp. How high the lamp stands: at 0.06 it rakes the cloth from 8 degrees up and the word is sharp; towards 1 it climbs to 68 degrees, both threads catch the light alike and the word sinks into the cloth. + +const W = 64, H = 128, NP = W * H; +const { floor, round, sqrt, cos, sin, tan, min, abs, 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 weave. The warp runs down the panel (one end per column), the weft +// across (one pick per row), and each LED is one crossing: lit by whichever +// thread lies on top there. The ground is a five-end weft-faced satin with +// move number 2: the warp comes up where (y + 2x) mod 5 = 0, once in every +// five crossings of each row and each column, and never next to another +// binding point across or down (the four neighbours give 1, 2, 3, 4 mod 5). +// The figure is the back of the same cloth: at every crossing the other +// thread is on top, so the word is warp-faced satin whose weft binding points +// sit exactly where the ground's warp binding points would be. +const groundWarp = (x, y) => (y + 2 * x) % 5 === 0; + +// ---- Letters, as strokes in a box one cap height tall: [x0, y0, x1, y1, w], +// w the weight (1 a stem, 0.72 a hairline, 0.62 a serif). The capitals are +// a plain roman with thick and thin strokes and bar serifs; the Hangul is one +// weight. +function arc(cx, cy, rx, ry, a0, a1, n, w0, w1) { + const out = []; + for (let i = 0; i < n; i++) { + const a = a0 + ((a1 - a0) * i) / n, b = a0 + ((a1 - a0) * (i + 1)) / n, m = (a + b) / 2; + out.push([cx + rx * cos(a), cy + ry * sin(a), cx + rx * cos(b), cy + ry * sin(b), w0 + (w1 - w0) * abs(cos(m))]); + } + return out; +} +const SERIF = 0.62, HAIR = 0.72; +const GLYPH = { + L: { w: 0.78, s: [[0.17, 0, 0.17, 1, 1], [0.17, 0.985, 0.9, 0.985, HAIR], [0, 0.015, 0.36, 0.015, SERIF], [0, 0.985, 0.2, 0.985, SERIF], [0.9, 1, 0.92, 0.8, SERIF]] }, + U: { + w: 0.96, + s: [[0.17, 0, 0.17, 0.64, 1], [0.83, 0, 0.83, 0.64, HAIR], [0, 0.015, 0.36, 0.015, SERIF], [0.68, 0.015, 1, 0.015, SERIF], ...arc(0.5, 0.64, 0.33, 0.345, 0, PI, 8, HAIR, 1)], + }, + X: { w: 0.98, s: [[0.1, 0, 0.9, 1, 1], [0.88, 0, 0.12, 1, HAIR], [-0.04, 0.015, 0.3, 0.015, SERIF], [0.72, 0.015, 1.04, 0.015, SERIF], [-0.04, 0.985, 0.28, 0.985, SERIF], [0.7, 0.985, 1.04, 0.985, SERIF]] }, + N: { w: 0.96, s: [[0.13, 0, 0.13, 1, HAIR], [0.13, 0, 0.87, 1, 1], [0.87, 0, 0.87, 1, HAIR], [-0.02, 0.015, 0.16, 0.015, SERIF], [0.7, 0.015, 1.02, 0.015, SERIF], [-0.02, 0.985, 0.3, 0.985, SERIF]] }, + O: { w: 1, s: arc(0.5, 0.5, 0.4, 0.49, 0, 2 * PI, 20, HAIR, 1) }, +}; +// Hangul syllables in a box 1 wide and 1.75 tall: the initial ㅂ top left, +// the vowel on the right, the final consonant under both. +const B = [[0.1, 0.04, 0.1, 0.86, 1], [0.58, 0.04, 0.58, 0.86, 1], [0.1, 0.45, 0.58, 0.45, 1], [0.08, 0.86, 0.6, 0.86, 1]]; +const HANGUL = { + 빛: [...B, [0.86, 0, 0.86, 1.02, 1], [0.5, 1.04, 0.5, 1.2, 1], [0.14, 1.24, 0.86, 1.24, 1], [0.5, 1.28, 0.12, 1.71, 1], [0.38, 1.45, 0.88, 1.71, 1]], + 밤: [...B, [0.8, 0, 0.8, 1.02, 1], [0.8, 0.46, 0.98, 0.46, 1], [0.16, 1.16, 0.84, 1.16, 1], [0.84, 1.16, 0.84, 1.7, 1], [0.84, 1.7, 0.16, 1.7, 1], [0.16, 1.7, 0.16, 1.16, 1]], +}; + +// Each word as strokes in panel LEDs. LUX and NOX stand one letter above +// another, caps 28 tall from rows 8, 50 and 92; a Hangul syllable is 48 wide +// and 84 tall, centred. +function wordStrokes(word) { + const out = []; + if (HANGUL[word]) { + const s = 48, x0 = 32 - s / 2, y0 = 64 - (1.75 * s) / 2; + for (const [a, b, c, d, w] of HANGUL[word]) out.push([x0 + a * s, y0 + b * s, x0 + c * s, y0 + d * s, w]); + return out; + } + const cap = 28; + [...word].forEach((ch, i) => { + const g = GLYPH[ch], x0 = 32 - (g.w * cap) / 2, y0 = 8 + 42 * i; + for (const [a, b, c, d, w] of g.s) out.push([x0 + a * cap, y0 + b * cap, x0 + c * cap, y0 + d * cap, w]); + }); + return out; +} + +function segDist(px, py, s) { + const dx = s[2] - s[0], dy = s[3] - s[1], l2 = dx * dx + dy * dy; + const t = l2 > 0 ? cl(((px - s[0]) * dx + (py - s[1]) * dy) / l2, 0, 1) : 0; + const ex = px - s[0] - t * dx, ey = py - s[1] - t * dy; + return sqrt(ex * ex + ey * ey); +} + +// A crossing is figure when its centre lies within half a stroke's width of +// the stroke. The stem width is found by halving until the word covers its +// share of the 8192 crossings (SHARE), so each word keeps about the same +// balance of figure and ground whatever its stroke length. Done once per word, +// the first time the Word knob lands on it: 2 to 15 ms in Node. +const WORDS = ["LUX", "빛", "NOX", "밤"]; +const SHARE = [0.104, 0.115, 0.104, 0.115]; +function figureMask(word, share) { + const st = wordStrokes(word), near = new Float32Array(NP).fill(1e9); + // For each crossing, the stem width at which it would just fall inside. + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let r = 1e9; + for (const s of st) r = min(r, segDist(x + 0.5, y + 0.5, s) / s[4]); + near[y * W + x] = r; + } + let lo = 0.5, hi = 12; + for (let k = 0; k < 30; k++) { + const mid = (lo + hi) / 2; + let n = 0; + for (let p = 0; p < NP; p++) if (near[p] < mid / 2) n++; + if (n / NP > share) hi = mid; + else lo = mid; + } + const m = new Uint8Array(NP); + for (let p = 0; p < NP; p++) m[p] = near[p] < lo / 2 ? 1 : 0; + return m; +} +const MASKS = []; + +// Which thread is on top at each crossing: 1 warp, 0 weft. +function topThreads(mask) { + const t = new Uint8Array(NP); + for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) t[y * W + x] = groundWarp(x, y) ? 1 - mask[y * W + x] : mask[y * W + x]; + return t; +} + +// Linen is not even: each warp end and each weft pick has its own lustre, +// between 0.95 and 1, so the floats show faint streaks along their threads. +function hash(n) { + let h = Math.imul(n ^ 0x9e3779b9, 0x85ebca6b) >>> 0; + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35) >>> 0; + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +} +const WARP = Float32Array.from({ length: W }, (_, x) => 0.95 + 0.05 * hash(x + 11)); +const WEFT = Float32Array.from({ length: H }, (_, y) => 0.95 + 0.05 * hash(y + 977)); + +// ---- The lamp. It stands 300 LEDs from the middle of the cloth and walks +// round it; its elevation at the middle is set by the Lamp knob. A float +// lying along T, seen from straight above and lit from the unit direction L, +// shines as (1 - (T.L)^2)^(P/2) (Kajiya and Kay's fibre highlight with the +// eye overhead, where T.V = 0), times a peak level of 232. P is 4 here, so +// the sheen is the square of 1 - (T.L)^2. A warp float (T down the +// panel) is brightest with the lamp to its side, a weft float with the lamp +// above or below the panel, so the two systems trade places every quarter +// turn of the lamp. +const R = 300, PEAK = 232; +// The lamp starts 28.1 degrees short of due right of the cloth and walks +// 5.625 degrees a second at Speed 2, so at 5 s it stands square to the +// warp. Checked over every crossing and frame from 2 s to 8 s: the lamp is +// never more than 30.5 degrees off square, a weft float never rises above +// 0.074 of the peak (17 of 255, under the 24 that counts as lit), a warp +// float never falls below 0.56 of it, and the word stays bright on a dark +// ground. The two sheens are equal where dx^2 = dy^2: a straight line at 45 +// degrees through the foot of the lamp, which crosses the cloth from the +// bottom right corner at 10.7 s to the top left at 15.3 s and passes the +// middle at 13 s. The word vanishes along that line and comes back dark. +const A0 = (-28.125 * PI) / 180, RATE = (5.625 * PI) / 180; + +export function setup(params) { + params.s = { a: A0, hue: 0, word: 0, lamp: 0.06, top: null, wordShown: -1 }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.a += (d * RATE * cl(k[1], 0.2, 3)) / 2; + S.word = cl(round(k[2]), 0, 3); + S.lamp = cl(k[3], 0, 1); + if (S.wordShown !== S.word) { + if (!MASKS[S.word]) MASKS[S.word] = figureMask(WORDS[S.word], SHARE[S.word]); + S.top = topThreads(MASKS[S.word]); + S.wordShown = S.word; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.top) return; + const top = S.top; + // Elevation 8 degrees at Lamp 0.06, 68 at 1. + const elev = ((4 + 64 * S.lamp) * PI) / 180, hz = R * tan(elev); + const lx = 32 + R * cos(S.a), ly = 64 - R * sin(S.a); + const c = hsv(0.115 + S.hue, 0.36, 1); + for (let y = 0; y < H; y++) { + const dy = ly - (y + 0.5); + for (let x = 0; x < W; x++) { + const dx = lx - (x + 0.5), inv = 1 / (dx * dx + dy * dy + hz * hz); + const p = y * W + x; + // T.L squared: dy^2 / |D|^2 for a warp float, dx^2 / |D|^2 for a weft. + let s, f; + if (top[p]) (s = 1 - dy * dy * inv), (f = WARP[x]); + else (s = 1 - dx * dx * inv), (f = WEFT[y]); + const v = PEAK * f * s * s; + display.setPixel(x, y, round(v * c[0]), round(v * c[1]), round(v * c[2])); + } + } +} diff --git a/incoming/pattern-study/patterns/steiner-deltoid.js b/incoming/pattern-study/patterns/steiner-deltoid.js new file mode 100644 index 000000000..d751282ad --- /dev/null +++ b/incoming/pattern-study/patterns/steiner-deltoid.js @@ -0,0 +1,279 @@ +// ===== Patternflow pattern ===== +// Title: Steiner Deltoid +// Family: line +// Idea: A point runs round the circumcircle of a slowly changing triangle, the feet of its perpendiculars to the three side lines stand on one straight line drawn the full height of the panel, and the fading trace of those Simson lines piles up into a bright three-cusped deltoid that turns and slides as the triangle changes. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Simson line (William Wallace, 1799), Steiner's deltoid as the envelope of the Simson lines (Jakob Steiner, 1856), pattern round 73 +// Round: 73 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Shape=0..1, Trail=0.5..4 +// +// Knob 1: Hue. Turns every colour together: at 0 the traces run teal to blue round the deltoid, the triangle and its circle are pale gold, its feet gold, and the point and the current Simson line near white. +// Knob 2: Speed. How fast the point runs round the circle: one turn in 5 s at 2, 3.3 s at 3 and 50 s at 0.2. The triangle changes shape on the same clock. +// Knob 3: Shape. How far the triangle strays from equilateral. At 0 its corners swing up to 14 degrees each way round the circle and the deltoid barely leaves the middle; at 1 they swing up to 46, the triangle goes obtuse, the orthocentre leaves it, and the deltoid rides up to 13.5 LEDs up and down the panel. +// Knob 4: Trail. How long the earlier Simson lines stay, in turns of the point: at 0.5 a line fades to 37% in half a turn and the deltoid is one bright edge among separate rays; at 4 the lines stay for 20 s at Speed 2, each laid fainter, and the inside of the deltoid, crossed by three lines a turn, glows over the outside, crossed by one. + +const W = 64, H = 128, NP = W * H; +const { floor, ceil, round, cos, sin, exp, min, max, abs, hypot, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- The geometry, checked in Node for 200 random triangles with the point +// at 2000 places round the circle each: the three feet are collinear (the +// area they span stays under 1e-15 of R^2); the Simson line turns 180 degrees +// the other way while the point goes once round; the line for the point +// diametrically opposite a vertex is the opposite side; and the envelope's +// nearest and farthest points lie R/2 and 3R/2 from the nine-point centre N +// (to 1e-6), so the nine-point circle is the deltoid's incircle and its +// cusps sit 1.5 R out. For one triangle, counting sign changes over 20,000 +// places of the point, each of 36 test points within R/2 of N lay on three +// Simson lines a turn and each of 36 beyond 1.5 R on one. With the +// circumcentre at 0 and R = 1, in complex numbers, N = (a + b + c) / 2 and +// the line for p touches the deltoid at (a + b + c + 2p + abc / p^2) / 2, +// which the code uses for the spark; that formula matched the intersection +// of neighbouring lines to 1e-6. +// +// R is 18 LEDs, so the cusps lie 27 from N and the deltoid fits the 64 +// columns when N stands on the middle one. The view slides sideways to keep +// it there: the circumcentre sits on row 64 at x = 32 - Hx / 2, where H is +// the orthocentre's offset from it, and N rides up and down the panel with +// Hy / 2. Over 5000 s at Shape 1 the circle reaches column 63.5 at most and +// N stays within 13.5 rows of row 64. +const R = 18; +// One turn of the point takes TURN seconds at Speed 2. +const TURN = 5; + +// The triangle at clock u (seconds at Speed 2). Three corners spaced a third +// of a turn, each swinging round the circle on its own slow beat (47, 59 and +// 71 s), and the whole triangle turning once in 200 s against the point, so +// over the 2.5 s a trace lasts at the start the deltoid hardly moves and its +// edge stays sharp. amp is the swing in radians: 0.25 at Shape 0, 0.8 at +// Shape 1. A corner can never pass the next one: two neighbours close by at +// most 2 * 0.8 rad, short of 2.09, and over 5000 s they never come closer +// than 28 degrees. +const BEAT = [47, 59, 71], PH = [0.9, 2.6, 4.4]; +function triangle(u, amp, out) { + const rho = (-TAU * u) / 200; + let hx = 0, hy = 0; + for (let i = 0; i < 3; i++) { + const a = rho + PI / 2 + (TAU * i) / 3 + amp * sin((TAU * u) / BEAT[i] + PH[i]); + out.ca[i] = cos(a); + out.sa[i] = sin(a); + hx += out.ca[i]; + hy += out.sa[i]; + } + // Orthocentre H = O + R (a + b + c); the circumcentre O keeps the + // nine-point centre N = O + R (a + b + c) / 2 on column 32. + out.ox = 32 - (R * hx) / 2; + out.oy = 64; + out.hx = hx; + out.hy = hy; + for (let i = 0; i < 3; i++) (out.x[i] = out.ox + R * out.ca[i]), (out.y[i] = out.oy + R * out.sa[i]); + return out; +} +const newTri = () => ({ ca: [0, 0, 0], sa: [0, 0, 0], x: [0, 0, 0], y: [0, 0, 0], ox: 0, oy: 0, hx: 0, hy: 0 }); + +// Foot of the perpendicular from (px, py) to the line through corners i, j. +function foot(T, i, j, px, py, o, k) { + const ux = T.x[i], uy = T.y[i], dx = T.x[j] - ux, dy = T.y[j] - uy; + const t = ((px - ux) * dx + (py - uy) * dy) / (dx * dx + dy * dy); + o[k] = ux + t * dx; + o[k + 1] = uy + t * dy; +} + +// The Simson line of the point at angle th: the three feet into F (on BC, +// CA, AB), and the line through the two farthest apart as n . q = c with n +// a unit normal, into L. +function simson(T, th, F, L) { + const px = T.ox + R * cos(th), py = T.oy + R * sin(th); + foot(T, 1, 2, px, py, F, 0); + foot(T, 2, 0, px, py, F, 2); + foot(T, 0, 1, px, py, F, 4); + let bi = 0, bj = 2, bd = -1; + for (const [i, j] of [[0, 2], [2, 4], [0, 4]]) { + const d = hypot(F[j] - F[i], F[j + 1] - F[i + 1]); + if (d > bd) (bd = d), (bi = i), (bj = j); + } + const dx = (F[bj] - F[bi]) / bd, dy = (F[bj + 1] - F[bi + 1]) / bd; + L[0] = -dy; + L[1] = dx; + L[2] = -dy * F[bi] + dx * F[bi + 1]; + L[3] = px; + L[4] = py; +} + +// Add a soft line n . q = c, w LEDs to each side, into a float RGB buffer. +// It walks rows for a steep line and columns for a flat one, so a line +// costs about 400 pixel visits, not 8192. +function addLine(buf, nx, ny, c, w, amt, col) { + if (abs(nx) >= abs(ny)) { + const sp = w / abs(nx); + for (let y = 0; y < H; y++) { + const xc = (c - ny * (y + 0.5)) / nx - 0.5; + for (let x = max(0, ceil(xc - sp)); x <= min(W - 1, floor(xc + sp)); x++) { + const f = 1 - abs(nx * (x + 0.5) + ny * (y + 0.5) - c) / w; + if (f > 0) { + const o = (y * W + x) * 3; + buf[o] += amt * f * col[0], buf[o + 1] += amt * f * col[1], buf[o + 2] += amt * f * col[2]; + } + } + } + } else { + const sp = w / abs(ny); + for (let x = 0; x < W; x++) { + const yc = (c - nx * (x + 0.5)) / ny - 0.5; + for (let y = max(0, ceil(yc - sp)); y <= min(H - 1, floor(yc + sp)); y++) { + const f = 1 - abs(nx * (x + 0.5) + ny * (y + 0.5) - c) / w; + if (f > 0) { + const o = (y * W + x) * 3; + buf[o] += amt * f * col[0], buf[o + 1] += amt * f * col[1], buf[o + 2] += amt * f * col[2]; + } + } + } + } +} + +// Soft segment and dot, brightest-wins, into the 0..255 frame. +function seg(rgb, x0, y0, x1, y1, w, col) { + const dx = x1 - x0, dy = y1 - y0, l2 = dx * dx + dy * dy || 1e-9; + const xa = max(0, floor(min(x0, x1) - w)), xb = min(W - 1, ceil(max(x0, x1) + w)); + const ya = max(0, floor(min(y0, y1) - w)), yb = min(H - 1, ceil(max(y0, y1) + w)); + for (let y = ya; y <= yb; y++) + for (let x = xa; x <= xb; x++) { + const qx = x + 0.5 - x0, qy = y + 0.5 - y0, t = cl((qx * dx + qy * dy) / l2, 0, 1); + const f = 1 - hypot(qx - t * dx, qy - t * dy) / w; + if (f > 0) put(rgb, y * W + x, f, col); + } +} +function dot(rgb, cx, cy, r, col) { + for (let y = max(0, floor(cy - r)); y <= min(H - 1, ceil(cy + r)); y++) + for (let x = max(0, floor(cx - r)); x <= min(W - 1, ceil(cx + r)); x++) { + const f = 1 - hypot(x + 0.5 - cx, y + 0.5 - cy) / r; + if (f > 0) put(rgb, y * W + x, min(1, 1.6 * f), col); + } +} +function ring(rgb, cx, cy, rad, w, col) { + for (let y = max(0, floor(cy - rad - w)); y <= min(H - 1, ceil(cy + rad + w)); y++) + for (let x = max(0, floor(cx - rad - w)); x <= min(W - 1, ceil(cx + rad + w)); x++) { + const f = 1 - abs(hypot(x + 0.5 - cx, y + 0.5 - cy) - rad) / w; + if (f > 0) put(rgb, y * W + x, f, col); + } +} +function put(rgb, p, f, col) { + const o = p * 3; + rgb[o] = max(rgb[o], f * col[0]); + rgb[o + 1] = max(rgb[o + 1], f * col[1]); + rgb[o + 2] = max(rgb[o + 2], f * col[2]); +} + +// ---- The trace. Every time the point passes a multiple of STEP (6 degrees, +// 60 lines a turn) that place's Simson line is laid into ACC, LINE strong, +// and the buffer fades by exp(-u / tau), tau the Trail knob in turns. The +// lines are kept apart like threads in string art: near the deltoid +// neighbours overlap and add up to a bright edge, farther out they part, 3 +// degrees apart, and run as separate rays to the panel's ends. Being tied +// to the point's angle, not to frames, the trace is the same at any frame +// rate and speed. +const STEP = (6 * PI) / 180, LINE = 0.5, LW = 0.75; +function deposit(S, T, th0, th1) { + const F = S.F, L = S.L; + for (let k = floor(th0 / STEP) + 1; k * STEP <= th1; k++) { + const th = k * STEP; + simson(T, th, F, L); + // The trace's colour runs from its hue to 0.16 past it and back once a + // turn, so each arc of the deltoid keeps its own tint. + const col = hsv(0.47 + S.hue + 0.08 * (1 - cos(th)), 0.75, 1); + addLine(S.acc, L[0], L[1], L[2], LW, LINE * (0.5 / S.trail) ** 0.7, col); + } +} + +function advance(S, du) { + const T = triangle(S.u, S.amp, S.T); + const th1 = S.th + (TAU * du) / TURN; + const fade = exp(-du / (S.trail * TURN)), acc = S.acc; + for (let i = 0; i < NP * 3; i++) acc[i] *= fade; + deposit(S, T, S.th, th1); + S.th = th1; + S.u += du; +} + +export function setup(params) { + params.s = { + u: 0, th: 0, hue: 0, amp: 0.25, trail: 0.5, warm: false, + acc: new Float32Array(NP * 3), rgb: new Float32Array(NP * 3), tmp: new Float32Array(NP * 3), + T: newTri(), F: new Float64Array(6), L: new Float64Array(5), + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.5], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.amp = 0.25 + 0.55 * cl(k[2], 0, 1); + S.trail = cl(k[3], 0.5, 4); + if (!S.warm) { + // First frame: run the last three trail lengths (at most 60 s) at 30 + // steps a second, so the deltoid is already built when the panel opens. + // 90 lines and 10 to 20 ms in Node at Trail 0.5; 720 lines and about + // 60 ms at 4, most of it fading the buffer 1800 times. + const back = min(60, 3 * S.trail * TURN); + S.u = -back; + S.th = (-TAU * back) / TURN; + for (let i = 0, n = round(back * 30); i < n; i++) advance(S, 1 / 30); + S.u = 0; + S.th = 0; + S.warm = true; + } + advance(S, (d * cl(k[1], 0.2, 3)) / 2); +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.warm) return; + const T = triangle(S.u, S.amp, S.T), F = S.F, L = S.L, rgb = S.rgb, acc = S.acc; + // The trace, through 1 - exp(-acc) per channel, so where lines pile up the + // colour runs to white. + for (let i = 0; i < NP * 3; i++) rgb[i] = 255 * (1 - exp(-acc[i])); + + const gold = hsv(0.12 + S.hue, 0.38, 1), grey = hsv(0.12 + S.hue, 0.2, 1), pale = hsv(0.47 + S.hue, 0.18, 1); + const sc = (c, v) => [c[0] * v, c[1] * v, c[2] * v]; + // The three side lines, run off the panel both ways: the feet often land + // on them outside the triangle. + const side = [], dim = sc(grey, 50); + for (const [i, j] of [[1, 2], [2, 0], [0, 1]]) { + const dx = T.x[j] - T.x[i], dy = T.y[j] - T.y[i], l = hypot(dx, dy); + side.push([-dy / l, dx / l, (-dy * T.x[i] + dx * T.y[i]) / l]); + } + const lines = S.tmp.fill(0); + for (const [nx, ny, c] of side) addLine(lines, nx, ny, c, 0.9, 1, dim); + for (let i = 0; i < NP * 3; i++) rgb[i] = max(rgb[i], min(dim[i % 3], lines[i])); + ring(rgb, T.ox, T.oy, R, 0.9, sc(gold, 95)); + for (const [i, j] of [[0, 1], [1, 2], [2, 0]]) seg(rgb, T.x[i], T.y[i], T.x[j], T.y[j], 1, sc(gold, 210)); + + // The point, its perpendiculars, the feet and its Simson line. + simson(T, S.th, F, L); + const px = L[3], py = L[4]; + for (let k = 0; k < 3; k++) seg(rgb, px, py, F[2 * k], F[2 * k + 1], 0.8, sc(gold, 95)); + const line = S.tmp.fill(0); + addLine(line, L[0], L[1], L[2], 1.1, 1, sc(pale, 235)); + for (let i = 0; i < NP * 3; i++) if (line[i] > rgb[i]) rgb[i] = line[i]; + for (let k = 0; k < 3; k++) dot(rgb, F[2 * k], F[2 * k + 1], 1.8, sc(gold, 255)); + dot(rgb, px, py, 2.4, [255, 255, 255]); + // Where the line touches the deltoid: (a + b + c + 2p + abc / p^2) / 2. + const ar = T.ca[0] * T.ca[1] - T.sa[0] * T.sa[1], ai = T.ca[0] * T.sa[1] + T.sa[0] * T.ca[1]; + const br = ar * T.ca[2] - ai * T.sa[2], bi = ar * T.sa[2] + ai * T.ca[2]; + const tr = br * cos(-2 * S.th) - bi * sin(-2 * S.th), ti = br * sin(-2 * S.th) + bi * cos(-2 * S.th); + const tx = T.ox + (R * (T.hx + 2 * cos(S.th) + tr)) / 2, ty = T.oy + (R * (T.hy + 2 * sin(S.th) + ti)) / 2; + dot(rgb, tx, ty, 1.6, [255, 255, 255]); + + for (let p = 0; p < NP; p++) display.setPixel(p % W, (p / W) | 0, min(255, rgb[p * 3]) | 0, min(255, rgb[p * 3 + 1]) | 0, min(255, rgb[p * 3 + 2]) | 0); +} From 138bcc2a2712b28c3f2a3061fd11845208d504f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 22:07:57 +0000 Subject: [PATCH 4/8] pattern-study: add abracadabra and ehrenfest-urn Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6N31ff3CzvqRufZu6W --- .../pattern-study/patterns/abracadabra.js | 267 +++++++++++++++ .../pattern-study/patterns/ehrenfest-urn.js | 306 ++++++++++++++++++ 2 files changed, 573 insertions(+) create mode 100644 incoming/pattern-study/patterns/abracadabra.js create mode 100644 incoming/pattern-study/patterns/ehrenfest-urn.js diff --git a/incoming/pattern-study/patterns/abracadabra.js b/incoming/pattern-study/patterns/abracadabra.js new file mode 100644 index 000000000..c4dcfb76e --- /dev/null +++ b/incoming/pattern-study/patterns/abracadabra.js @@ -0,0 +1,267 @@ +// ===== Patternflow pattern ===== +// Title: Abracadabra +// Family: type +// Idea: The fever charm from Serenus Sammonicus stands down all 128 rows as eleven lines of ABRACADABRA, one letter shorter each line, and a fever runs down through it again and again, writing each line hot as it passes, while the one letter each line leaves out flares, burns, cools to ash and crumbles to a faint scorch, until only the last A carries the heat off the foot of the panel. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the ABRACADABRA cone in Quintus Serenus Sammonicus, Liber Medicinalis (usually dated to the early 3rd century AD), pattern round 73 +// Round: 73 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Form=0..2, Cooling=0..1 +// +// Knob 1: Hue. Turns the colours round the wheel together: at 0 the fever is hot pink going white where it is hottest, the written lines a pale teal, the letter each line leaves out burns amber, and its ash is a grey blue. +// Knob 2: Speed. The fever crosses one line a beat: 0.6 s at 2, so it goes down the 128 rows in 6.6 s and comes round again every 8.4 s; ten times slower at 0.2 and half again as fast at 3. +// Knob 3: Form. Which end of the line loses its letter, rounded to 0, 1 or 2. 0 drops the last letter, the staircase whose left column reads eleven A's; 1 drops the first, the mirrored staircase; 2 drops the last and the first in turn, the centred cone that ends on the A in the middle of the word. Every letter stays in its own column, so the three forms end on an A. +// Knob 4: Cooling. How long the heat stays behind the fever: at 0.06 a line is back to ink a beat or so after it was written and the ash of a left-out letter has crumbled to its scorch 3 to 5 beats after it caught; at 1 the cone stays pink about 4 beats behind the front and the ash crumbles over 6 to 17 beats, so about a third of it is still there when the next fever comes round. + +const W = 64, H = 128, NP = W * H; +const { floor, round, min, max, 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]; +} + +// ---- Letters. The word needs five: A B R C D. Eleven letters must fit 64 +// columns, so each is 4 wide on a 5 LED pitch (54 columns, 5 spare on each +// side) and 9 tall, one stroke wide. A line has 11.64 rows, so the eleven +// lines fill the 128 rows with 2 or 3 dark rows between them. +const GLYPH = { + A: [".##.", "#..#", "#..#", "#..#", "####", "#..#", "#..#", "#..#", "#..#"], + B: ["###.", "#..#", "#..#", "#..#", "###.", "#..#", "#..#", "#..#", "###."], + R: ["###.", "#..#", "#..#", "#..#", "###.", "#.#.", "#..#", "#..#", "#..#"], + C: [".##.", "#..#", "#...", "#...", "#...", "#...", "#...", "#..#", ".##."], + D: ["###.", "#..#", "#..#", "#..#", "#..#", "#..#", "#..#", "#..#", "###."], +}; +const WORD = "ABRACADABRA", N = 11, GW = 4, GH = 9, X0 = 5, PX = 5, LH = H / N; +const TOP = []; +for (let i = 0; i < N; i++) TOP.push(round(i * LH + (LH - GH) / 2)); +// Rows 1, 13, 25, 36, 48, 60, 71, 83, 94, 106, 118: the last line ends on row 126. + +// Every place in the 11 by 11 grid: line i, column j holds WORD[j], always in +// the same column, so a line that loses its first letter does not shift. +// pix lists the lit LEDs of each place; rnd is a fixed number per LED that +// sets when that LED of an ash letter crumbles. +const CELL = []; +for (let i = 0; i < N; i++) + for (let j = 0; j < N; j++) { + const g = GLYPH[WORD[j]], pix = []; + for (let r = 0; r < GH; r++) + for (let c = 0; c < GW; c++) if (g[r][c] === "#") pix.push((TOP[i] + r) * W + X0 + PX * j + c); + CELL.push({ i, j, pix }); + } +function hash(n) { + let h = Math.imul(n ^ 0x9e3779b9, 0x85ebca6b) >>> 0; + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35) >>> 0; + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +} +const RND = new Float32Array(NP); +for (let p = 0; p < NP; p++) RND[p] = hash(p + 17); + +// ---- The forms. Line i keeps the columns a..b of the word. L[j] is the line +// on which column j is left out, the line its letter burns on; the column +// that is never left out gets 11, below the last line. Staircase: line i +// keeps 0..10-i, so L = 11, 10, 9, ..., 1 and the letters burn in the order +// A R B A D A C A R B, the word backwards, with the first A left to carry the +// last of the heat. Mirrored: line i keeps i..10. Cone: the last and the +// first in turn, line i keeps floor(i/2)..10-ceil(i/2), and the middle A, +// column 5, is left. +const FORMS = [0, 1, 2].map((f) => { + const L = new Int8Array(N).fill(N); + for (let i = N - 1; i >= 0; i--) { + const a = f === 0 ? 0 : f === 1 ? i : floor(i / 2), b = f === 0 ? N - 1 - i : f === 1 ? N - 1 : N - 1 - Math.ceil(i / 2); + for (let j = 0; j < N; j++) if (j < a || j > b) L[j] = i; + } + let last = 0; + for (let j = 0; j < N; j++) if (L[j] === N) last = j; + return { L, last }; +}); +// Checked: every form keeps 11 - i letters on line i (66 in all), burns one +// letter on each of lines 1..10, and leaves an A: column 0, 10 or 5. +for (const F of FORMS) { + for (let i = 0; i < N; i++) { + let k = 0, burn = 0; + for (let j = 0; j < N; j++) (k += i < F.L[j] ? 1 : 0), (burn += i === F.L[j] ? 1 : 0); + if (k !== N - i || burn !== (i ? 1 : 0)) throw new Error("Abracadabra form is off at line " + i); + } + if (WORD[F.last] !== "A") throw new Error("Abracadabra does not end on an A"); +} + +// ---- Time, in beats. The fever's front is at row (beat - 0.5) * 11.64, so +// it crosses one line a beat, enters the top half a beat into the cycle and +// leaves the foot at 11.5. The cycle is 14 beats; the last 2.5 are the heat +// leaving under the last A and the ash crumbling. A fresh start comes in at +// beat 2.6, with the first two lines already written and burning. +const CYCLE = 14, ENTER = 0.5, START = 2.6; +const INLINE = new Uint8Array(H); +for (const t of TOP) for (let r = 0; r < GH; r++) INLINE[t + r] = 1; +const lineOf = (y) => { + // The line the front is going to at row y: the gap above a line counts as that line's. + for (let i = 0; i < N; i++) if (y <= TOP[i] + GH - 1) return i; + return N; +}; + +export function setup(params) { + params.s = { + beat: START, hue: 0, form: 0, cool: 0.06, t: 0, + rgb: new Float32Array(NP * 3), glow: new Float32Array(NP), tmp: new Float32Array(NP), + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + const speed = cl(k[1], 0.2, 3); + S.form = cl(round(k[2]), 0, 2); + S.cool = cl(k[3], 0, 1); + // One beat is 1.2 / Speed seconds: 0.6 s at 2. + S.beat += (d * speed) / 1.2; + S.t += d; +} + +function blur(src, tmp) { + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + tmp[p] = (2 * src[p] + (x > 0 ? src[p - 1] : 0) + (x < W - 1 ? src[p + 1] : 0)) / 4; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + src[p] = (2 * tmp[p] + (y > 0 ? tmp[p - W] : 0) + (y < H - 1 ? tmp[p + W] : 0)) / 4; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { rgb, glow } = S, F = FORMS[S.form]; + const tau = ((S.beat % CYCLE) + CYCLE) % CYCLE, Y = (tau - ENTER) * LH; + const hF = 0.9 + S.hue; + const inkF = hsv(hF + 0.58, 0.45, 0.74), ember = hsv(hF, 0.85, 1), white = hsv(hF, 0.1, 1); + const flare = hsv(hF + 0.2, 0.15, 1), flame = hsv(hF + 0.2, 0.75, 1); + const ash = hsv(hF + 0.72, 0.3, 0.34), speck = hsv(hF + 0.04, 0.7, 0.9), scorch = hsv(hF + 0.72, 0.3, 0.125); + // The heat in a written letter falls by a factor e every tk beats; an LED of + // ash lasts 2.4 + ta * (0.3 + its own number 0..1) beats after it caught. + const tk = 0.8 + 3.2 * S.cool, ta = 1.5 + 10 * S.cool; + // Embers flicker 12 times a second, each LED on its own. + const fl = floor(S.t * 12); + rgb.fill(0); + glow.fill(0); + const put = (p, c, v) => { + rgb[p * 3] = c[0] * v, rgb[p * 3 + 1] = c[1] * v, rgb[p * 3 + 2] = c[2] * v; + }; + // Beats since the front last passed row y: this cycle's pass, or the last one's. + const since = (y) => { + const s = (Y - y) / LH; + return s < 0 ? s + CYCLE : s; + }; + + for (const C of CELL) { + const Lj = F.L[C.j]; + if (C.i > Lj) continue; + if (C.i < Lj) { + // A kept letter: written hot as the front goes through it row by row, + // then cooling back to ink, which fades from 100% to 64% over the + // cycle (e every 5 beats), so the lines the fever has just written + // stand out from the ones it is coming to. + for (const p of C.pix) { + const s = since(floor(p / W)); + const h = s < 0.12 ? s / 0.12 : exp(-(s - 0.12) / tk), m = 0.62 + 0.38 * exp(-s / 5); + const ink = [inkF[0] * m, inkF[1] * m, inkF[2] * m]; + let c; + if (h > 0.55) { + const u = (h - 0.55) / 0.45; + c = [ember[0] + (white[0] - ember[0]) * u, ember[1] + (white[1] - ember[1]) * u, ember[2] + (white[2] - ember[2]) * u]; + } else { + const u = h / 0.55; + c = [ink[0] + (ember[0] - ink[0]) * u, ink[1] + (ember[1] - ink[1]) * u, ink[2] + (ember[2] - ink[2]) * u]; + } + put(p, c, 255); + glow[p] += h; + } + continue; + } + // The letter this line leaves out burns in a colour of its own, a fifth + // of the wheel past the fever (amber at Hue 0): white for 0.4 beat as the + // front reaches it, a flickering flame for a beat, then by beat 2.4 it + // cools to ash, turning back round the wheel through the fever's pink so + // it never passes through a dim orange. Each LED then crumbles at its own + // time, lighting once more as it goes and leaving a scorch at 32 of 255 + // that the next fever burns again. + for (const p of C.pix) { + const s = since(floor(p / W)), r = RND[p], life = 2.4 + ta * (0.3 + r); + if (s > life) put(p, scorch, 255); + else if (s < 0.4) { + put(p, flare, 255); + glow[p] += 2; + } else if (s < 1.4) { + const f = 0.45 + 0.55 * hash(p * 31 + fl); + put(p, flame, 255 * f); + glow[p] += 1.4 * f; + } else if (s < 2.4) { + const u = s - 1.4; + put(p, hsv(hF + 0.2 - 0.48 * u, 0.75 - 0.45 * u, 1 - 0.66 * u), 255); + glow[p] += 0.6 * (1 - u); + } else if (s > life - 0.3) { + put(p, speck, 255 * (0.5 + 0.5 * hash(p * 7 + fl))); + glow[p] += 0.4; + } else put(p, ash, 255); + } + } + + // ---- The front: a drop of heat in every column still carrying it, which + // is every column whose letter has not yet been left out above this row. + // It shows only in the dark rows between lines (inside a line the letter + // itself lights row by row), and the survivor's runs on off the foot. + const lf = lineOf(Y); + if (Y > -2 && Y < H + 2) + for (let j = 0; j < N; j++) { + if (F.L[j] < lf) continue; + const cx = X0 + PX * j + 1.5; + for (let dy = -4; dy <= 1; dy++) { + const y = floor(Y) + dy; + if (y < 0 || y >= H || INLINE[y]) continue; + const v = dy > 0 ? 0.5 : exp(dy / 1.6); + for (const x of [floor(cx), floor(cx) + 1]) { + const p = y * W + x; + glow[p] += 1.2 * v; + rgb[p * 3] = max(rgb[p * 3], white[0] * 255 * v); + rgb[p * 3 + 1] = max(rgb[p * 3 + 1], white[1] * 255 * v); + rgb[p * 3 + 2] = max(rgb[p * 3 + 2], white[2] * 255 * v); + } + } + } + + // ---- The heat off the foot: once the front has left row 127 the last of + // the fever spreads along the foot under the last A and fades in 2 beats. + const sb = (Y - H) / LH; + if (sb > 0 && sb < 3) { + const xs = X0 + PX * F.last + 1.5, wdt = 4 + 12 * sb, a = exp(-sb / 1.1) * 3; + for (let y = H - 14; y < H; y++) + for (let x = 0; x < W; x++) { + const v = a * exp(-(((x + 0.5 - xs) / wdt) ** 2)) * exp(-(H - 0.5 - y) / (1.5 + 2.5 * sb)); + if (v > 0.02) glow[y * W + x] += v; + } + } + + // Heat glows round itself in the fever's colour. + blur(glow, S.tmp); + blur(glow, S.tmp); + for (let p = 0; p < NP; p++) { + const g = min(1.6, glow[p]) * 140, o = p * 3; + if (g < 1) continue; + rgb[o] += ember[0] * g, rgb[o + 1] += ember[1] * g, rgb[o + 2] += ember[2] * g; + } + + for (let p = 0; p < NP; p++) { + const o = p * 3, r = rgb[o], g = rgb[o + 1], b = rgb[o + 2]; + if (r < 1 && g < 1 && b < 1) continue; + display.setPixel(p % W, (p - (p % W)) / W, min(255, r) | 0, min(255, g) | 0, min(255, b) | 0); + } +} diff --git a/incoming/pattern-study/patterns/ehrenfest-urn.js b/incoming/pattern-study/patterns/ehrenfest-urn.js new file mode 100644 index 000000000..0806d63d3 --- /dev/null +++ b/incoming/pattern-study/patterns/ehrenfest-urn.js @@ -0,0 +1,306 @@ +// ===== Patternflow pattern ===== +// Title: Ehrenfest Urn +// Family: particle +// Idea: N balls start in the upper of two chambers stacked down the panel and joined by a narrow neck, and every step one ball chosen at random darts through the neck to the other side, while a strip at the foot traces the count in the upper chamber falling from N to N/2 along its expected curve and then hovering inside the binomial spread, with the time spent at each count piling up beside it into the binomial bell, and the chambers flash each time the count comes back to the state the fourth knob watches. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Ehrenfest urn model (Paul and Tatjana Ehrenfest, 1907), its mean recurrence times (Mark Kac, 1947), pattern round 73 +// Round: 73 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Balls=8..200, Watch=0..1 +// +// Knob 1: Hue. Turns the two chambers' colours round the wheel together, nearly half a wheel apart: at 0 the balls in the upper chamber are gold and those in the lower one azure, and a ball changes colour as it goes through the neck. +// Knob 2: Speed. Steps a second, 5 times Speed times the square root of N / 8: 10 at 2 with 8 balls, so the all-up state, 256 steps apart on average, comes back about every 26 s; 50 at 2 with 200 balls. A step's flight takes 1.8 steps' time, between 0.12 and 0.5 s. +// Knob 3: Balls. N, 8 to 200. Turning it starts again with every ball in the upper chamber. At 8 the count drops to 4 within a second and swings by 1.4 either side; at 200 the distance to 100 halves every 69 steps, 1.4 s at Speed 2, and the count swings by 7, and the all-up state would take 2^200 steps to come back. +// Knob 4: Watch. Which count the flash watches, from N (every ball up, the start) below 0.08 down to N/2 at 1. Kac's mean return time to count k is 2^N / C(N, k) steps: 256 for k = 8 of 8, 9.1 for k = 6 and 3.7 for k = 4, so the flash goes from rare to nearly every step. A dotted line on the trace marks the count, and a tick above the trace each visit. + +const W = 64, H = 128, NP = W * H; +const { floor, round, min, max, sqrt, exp, abs, pow, log, cos, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- The vessel. Two rounded chambers 62 by 44 LEDs, the upper from row 1 +// to 44 and the lower from 51 to 94, joined by a neck 6 LEDs wide (x 29 to +// 34) and 6 rows long. The trace takes rows 96 to 126. +const CH = [ + { x0: 1, y0: 51, x1: 62, y1: 94 }, // side 0: lower + { x0: 1, y0: 1, x1: 62, y1: 44 }, // side 1: upper +]; +const NX0 = 29, NX1 = 34, NECK = 31.5; +const MOUTH = [52.5, 43.5]; // the row a ball passes as it leaves each chamber + +// Wall brightness 0..1 per LED: the outline of each rounded rectangle (corner +// radius 5), open where the neck meets it, and the neck's two sides. +const WALL = new Float32Array(NP); +function rrect(px, py, c) { + const r = 5, cx = (c.x0 + c.x1) / 2, cy = (c.y0 + c.y1) / 2, hx = (c.x1 - c.x0) / 2 - r, hy = (c.y1 - c.y0) / 2 - r; + const qx = abs(px - cx) - hx, qy = abs(py - cy) - hy; + return sqrt(max(qx, 0) ** 2 + max(qy, 0) ** 2) + min(max(qx, qy), 0) - r; +} +for (let y = 0; y < 96; y++) + for (let x = 0; x < W; x++) { + const px = x, py = y; + let v = 0; + for (const c of CH) v = max(v, 1 - abs(rrect(px, py, c))); + if (x >= NX0 && x <= NX1 && y >= 43 && y <= 52) v = 0; + if ((x === NX0 - 1 || x === NX1 + 1) && y >= 44 && y <= 51) v = 1; + WALL[y * W + x] = cl(v, 0, 1); + } +// The LEDs inside either chamber or the neck. +const INSIDE = new Uint8Array(NP); +for (let y = 0; y < 96; y++) + for (let x = 0; x < W; x++) + INSIDE[y * W + x] = WALL[y * W + x] <= 0 && (rrect(x, y, CH[0]) < 0 || rrect(x, y, CH[1]) < 0 || (x >= NX0 && x <= NX1 && y > 44 && y < 51)) ? 1 : 0; + +// ---- The trace. Count n of N maps to row TY1 - n / N * (TY1 - TY0): all up +// on row 99, none up on row 126. 52 columns of history (x 0 to 51), then a +// gap, then the time spent at each count as bars from x 54 to 63. +const TY0 = 99, TY1 = 126, TCOLS = 52, HX = 54, HW = 10, RUG = 97; +const rowOf = (n, N) => TY1 - (n / N) * (TY1 - TY0); + +// log C(N, k), for the binomial law the count settles to: C(N, k) / 2^N. +function logC(N, k) { + let s = 0; + for (let i = 1; i <= k; i++) s += log(N - k + i) - log(i); + return s; +} + +function gauss() { + // Box-Muller from the seeded Math.random. + const u = 1 - Math.random(), v = Math.random(); + return sqrt(-2 * log(u)) * cos(2 * PI * v); +} + +function reset(S, N) { + S.N = N; + S.n = N; + S.x = new Float32Array(N), S.y = new Float32Array(N), S.vx = new Float32Array(N), S.vy = new Float32Array(N); + S.side = new Uint8Array(N).fill(1); + // A flight: start time, length, and the four points start, mouth, other mouth, end. + S.ft = new Float32Array(N).fill(-1), S.fd = new Float32Array(N), S.fp = new Float32Array(N * 8); + const c = CH[1]; + for (let i = 0; i < N; i++) { + S.x[i] = c.x0 + 4 + Math.random() * (c.x1 - c.x0 - 8); + S.y[i] = c.y0 + 4 + Math.random() * (c.y1 - c.y0 - 8); + } + // Radius 3.3 at N = 8, 1.25 at 200. + S.r = 3.3 * pow(8 / N, 0.3); + // Steps per trace column: 2 at N = 8, 10 at N = 200, so the 52 columns + // hold 104 and 520 steps, a little over 5 relaxation times at 200. + S.per = max(1, round(2 * sqrt(N / 8))); + S.cols = []; + S.cur = { mn: N, mx: N, last: N, hit: 1, g: 0 }; + S.steps = 0; + S.g = 0; + S.time = new Float64Array(N + 1); + S.time[N] = 1; + // The law the count settles to, C(N, k) / 2^N, summed into trace rows. + S.law = new Float64Array(H); + const l2 = N * log(2); + for (let k = 0; k <= N; k++) S.law[round(rowOf(k, N))] += exp(logC(N, k) - l2); + S.lawMax = 0; + for (let y = TY0; y <= TY1; y++) S.lawMax = max(S.lawMax, S.law[y]); + S.flash = 1; +} + +function step(S) { + const N = S.N, i = floor(Math.random() * N); + const from = S.side[i], to = 1 - from; + S.side[i] = to; + S.n += to ? 1 : -1; + // Fly from wherever the ball is now through the neck to a random place in + // the other chamber. A ball still flying that has not yet passed the + // middle of the neck turns round inside the chamber it is in. + const p = ballAt(S, i, S.clock), c = CH[to], ox = NECK + (Math.random() - 0.5) * (NX1 - NX0 - 1.5); + const f = S.fp, o = i * 8, here = p[1] < 48 ? 1 : 0; + f[o] = p[0], f[o + 1] = p[1]; + if (here === to) (f[o + 2] = f[o + 4] = p[0]), (f[o + 3] = f[o + 5] = p[1]); + else (f[o + 2] = f[o + 4] = ox), (f[o + 3] = MOUTH[here]), (f[o + 5] = MOUTH[to]); + f[o + 6] = c.x0 + 4 + Math.random() * (c.x1 - c.x0 - 8); + f[o + 7] = c.y0 + 4 + Math.random() * (c.y1 - c.y0 - 8); + S.ft[i] = S.clock; + S.fd[i] = S.fly; + // The trace and the clock of counts. + const cur = S.cur, n = S.n; + cur.mn = min(cur.mn, n), cur.mx = max(cur.mx, n), cur.last = n; + S.time[n]++; + if (n === S.kw) (cur.hit = 1), (S.flash = 1); + S.steps++; + if (S.steps % S.per === 0) { + S.cols.push(cur); + if (S.cols.length > TCOLS - 1) S.cols.shift(); + S.g++; + S.cur = { mn: n, mx: n, last: n, hit: 0, g: S.g }; + } +} + +// Where ball i is drawn at time `at`: on its flight path, or resting in its chamber. +function ballAt(S, i, at) { + const t0 = S.ft[i]; + if (t0 < 0) return [S.x[i], S.y[i], -1]; + const u = cl((at - t0) / S.fd[i], 0, 1), e = u * u * (3 - 2 * u), f = S.fp, o = i * 8; + const l1 = sqrt((f[o + 2] - f[o]) ** 2 + (f[o + 3] - f[o + 1]) ** 2), l2 = abs(f[o + 5] - f[o + 3]); + const l3 = sqrt((f[o + 6] - f[o + 4]) ** 2 + (f[o + 7] - f[o + 5]) ** 2), L = l1 + l2 + l3; + let d = e * L, k = 0; + if (d > l1) (d -= l1), (k = 2); + if (k === 2 && d > l2) (d -= l2), (k = 4); + const seg = k === 0 ? l1 : k === 2 ? l2 : l3, a = seg > 0 ? d / seg : 1; + // The third value says how far through the neck it is, 0 before, 1 after + // (and 1 all along for a ball turning round on its own side). + const thr = l1 + l2 === 0 || k === 4 ? 1 : k === 2 ? a : 0; + return [f[o + k] + (f[o + k + 2] - f[o + k]) * a, f[o + k + 1] + (f[o + k + 3] - f[o + k + 1]) * a, thr, u]; +} + +export function setup(params) { + params.s = { clock: 0, acc: 0.6, hue: 0, rate: 10, fly: 0.18, kw: 8, rgb: new Float32Array(NP * 3) }; + reset(params.s, 8); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 8, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + const speed = cl(k[1], 0.2, 3), N = cl(round(k[2]), 8, 200), w = cl(k[3], 0, 1); + if (N !== S.N) reset(S, N); + S.kw = N - round((max(0, w - 0.08) / 0.92) * (N / 2)); + S.rate = 5 * speed * sqrt(N / 8); + S.fly = cl(1.8 / S.rate, 0.12, 0.5); + S.clock += d; + S.flash *= exp(-d / 0.3); + // A gentle jiggle for every ball at rest: velocities relax in 0.6 s and + // run at about 5 LEDs a second, and the walls turn them back. + const sig = 9.1 * sqrt(d), r = S.r; + for (let i = 0; i < N; i++) { + if (S.ft[i] >= 0) { + if (S.clock - S.ft[i] < S.fd[i]) continue; + // Landed: rest where the flight ended. + S.x[i] = S.fp[i * 8 + 6], S.y[i] = S.fp[i * 8 + 7], S.ft[i] = -1; + } + S.vx[i] += -S.vx[i] * (d / 0.6) + sig * gauss(); + S.vy[i] += -S.vy[i] * (d / 0.6) + sig * gauss(); + S.x[i] += S.vx[i] * d, S.y[i] += S.vy[i] * d; + const c = CH[S.side[i]], a0 = c.x0 + 1.5 + r, a1 = c.x1 - 1.5 - r, b0 = c.y0 + 1.5 + r, b1 = c.y1 - 1.5 - r; + if (S.x[i] < a0) (S.x[i] = a0), (S.vx[i] = abs(S.vx[i])); + if (S.x[i] > a1) (S.x[i] = a1), (S.vx[i] = -abs(S.vx[i])); + if (S.y[i] < b0) (S.y[i] = b0), (S.vy[i] = abs(S.vy[i])); + if (S.y[i] > b1) (S.y[i] = b1), (S.vy[i] = -abs(S.vy[i])); + } + S.acc += d * S.rate; + for (let s = 0; s < 40 && S.acc >= 1; s++) (S.acc -= 1), step(S); +} + +// A ball: a disc of radius r with a brighter middle and a halo that falls +// from a fifth of its brightness at the rim to nothing a radius and a half out. +function disc(rgb, x, y, r, c, a) { + const R = 2.5 * r + 1; + for (let j = max(0, floor(y - R)); j <= min(H - 1, floor(y + R)); j++) + for (let i = max(0, floor(x - R)); i <= min(W - 1, floor(x + R)); i++) { + const dx = i + 0.5 - x, dy = j + 0.5 - y, dd = sqrt(dx * dx + dy * dy); + const core = cl(r + 0.5 - dd, 0, 1) * (1 + 0.5 * exp(-(dd * dd) / (0.3 * r * r))); + const v = a * max(core, 0.2 * cl(1 - (dd - r) / (1.5 * r), 0, 1) ** 2); + if (v <= 0.5) continue; + const o = (j * W + i) * 3; + rgb[o] += c[0] * v, rgb[o + 1] += c[1] * v, rgb[o + 2] += c[2] * v; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { rgb, N } = S; + const hT = 0.12 + S.hue, hB = hT + 0.45; + // Walls, band and mean line are only tinted (saturation 0.2 to 0.3): at Hue 0.5 + // the lower colour is orange, and a dim orange would read brown. + const up = hsv(hT, 0.78, 1), lo = hsv(hB, 0.78, 1), pale = hsv(hT, 0.12, 1), band = hsv(hB, 0.3, 1); + rgb.fill(0); + + // ---- Walls, flashing white on a visit to the watched count, and the + // inside of both chambers lit to 40 with them. + const fl = S.flash, wc = hsv(hB, 0.2 * (1 - fl), 1), wv = 62 + 170 * fl, fv = 40 * fl; + for (let p = 0; p < 96 * W; p++) { + const v = WALL[p]; + if (v > 0) rgb[p * 3] = wc[0] * wv * v, rgb[p * 3 + 1] = wc[1] * wv * v, rgb[p * 3 + 2] = wc[2] * wv * v; + else if (fv > 1 && INSIDE[p]) rgb[p * 3] = pale[0] * fv, rgb[p * 3 + 1] = pale[1] * fv, rgb[p * 3 + 2] = pale[2] * fv; + } + // ---- Balls. + const r = S.r; + for (let i = 0; i < N; i++) { + const b = ballAt(S, i, S.clock); + if (b[2] < 0) { + disc(rgb, b[0], b[1], r, S.side[i] ? up : lo, 235); + continue; + } + // In flight: brighter, with the colour turning in the neck and two + // fading copies behind it. + const to = S.side[i], t = b[2], c0 = to ? lo : up, c1 = to ? up : lo; + const c = [c0[0] + (c1[0] - c0[0]) * t, c0[1] + (c1[1] - c0[1]) * t, c0[2] + (c1[2] - c0[2]) * t]; + // The copies are paled towards white so a dim one does not read brown. + for (let k = 2; k >= 0; k--) { + const q = ballAt(S, i, S.clock - k * 0.025), a = 0.3 * k; + const ck = [c[0] + (1 - c[0]) * a, c[1] + (1 - c[1]) * a, c[2] + (1 - c[2]) * a]; + disc(rgb, q[0], q[1], r * (1 - 0.2 * k), ck, 255 * pow(0.5, k)); + } + } + + // ---- The trace. The binomial spread first: rows within one standard + // deviation sqrt(N)/2 of N/2 at 30, within two at 18. + const sd = sqrt(N) / 2, m = N / 2; + for (let y = TY0 - 1; y <= TY1 + 1; y++) { + const n = ((TY1 - y) / (TY1 - TY0)) * N, z = abs(n - m) / sd; + const v = z <= 1 ? 30 : z <= 2 ? 18 : 0; + if (!v) continue; + for (let x = 0; x < TCOLS; x++) { + const o = (y * W + x) * 3; + rgb[o] += band[0] * v, rgb[o + 1] += band[1] * v, rgb[o + 2] += band[2] * v; + } + } + const put = (x, y, c, v) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + rgb[o] = max(rgb[o], c[0] * v), rgb[o + 1] = max(rgb[o + 1], c[1] * v), rgb[o + 2] = max(rgb[o + 2], c[2] * v); + }; + // The mean N/2, dashed, and the watched count, dotted. + const ym = round(rowOf(m, N)), yk = round(rowOf(S.kw, N)); + for (let x = 0; x < TCOLS; x++) { + if (x % 4 < 2) put(x, ym, band, 120); + if (x % 3 === 0) put(x, yk, pale, 110); + } + const cols = S.cols.concat([S.cur]); + // The expected count from the start, N/2 + (N/2)(1 - 2/N)^t, dim, for the + // columns still on the trace since the start. + for (let x = 0; x < cols.length; x++) { + const t = (cols[x].g + 1) * S.per, e = m + m * pow(1 - 2 / N, t); + if (e - m < 0.5) continue; + put(x, round(rowOf(e, N)), pale, 70); + } + let prev = cols.length ? cols[0].mx : N; + for (let x = 0; x < cols.length; x++) { + const c = cols[x], a = round(rowOf(max(c.mx, prev), N)), b = round(rowOf(min(c.mn, prev), N)); + for (let y = a; y <= b; y++) put(x, y, up, 255); + if (c.hit) (put(x, RUG, pale, 255), put(x, RUG - 1, pale, 140)); + prev = c.last; + } + // The time spent at each count, against the binomial law (bright dots). + let tot = 0; + for (let k = 0; k <= N; k++) tot += S.time[k]; + const rows = new Float64Array(H); + for (let k = 0; k <= N; k++) rows[round(rowOf(k, N))] += S.time[k] / tot; + for (let y = TY0; y <= TY1; y++) { + const len = min(HW, (rows[y] / S.lawMax) * (HW - 2)), want = (S.law[y] / S.lawMax) * (HW - 2); + for (let x = 0; x < len - 0.3; x++) put(HX + x, y, up, 70 + 110 * min(1, len - x)); + if (want >= 0.5) put(HX + round(want), y, pale, 230); + } + + for (let p = 0; p < NP; p++) { + const o = p * 3, R = rgb[o], G = rgb[o + 1], B = rgb[o + 2]; + if (R < 1 && G < 1 && B < 1) continue; + display.setPixel(p % W, (p - (p % W)) / W, min(255, R) | 0, min(255, G) | 0, min(255, B) | 0); + } +} From a9dc3ac39ea394641c8b6568124be51aa4604fc1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 22:11:29 +0000 Subject: [PATCH 5/8] pattern-study: add maya-numerals and airy-beam Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6N31ff3CzvqRufZu6W --- incoming/pattern-study/patterns/airy-beam.js | 226 ++++++++++++ .../pattern-study/patterns/maya-numerals.js | 329 ++++++++++++++++++ 2 files changed, 555 insertions(+) create mode 100644 incoming/pattern-study/patterns/airy-beam.js create mode 100644 incoming/pattern-study/patterns/maya-numerals.js diff --git a/incoming/pattern-study/patterns/airy-beam.js b/incoming/pattern-study/patterns/airy-beam.js new file mode 100644 index 000000000..2ed17169d --- /dev/null +++ b/incoming/pattern-study/patterns/airy-beam.js @@ -0,0 +1,226 @@ +// ===== Patternflow pattern ===== +// Title: Airy Beam +// Family: wave +// Idea: A finite-energy Airy beam enters at the top and runs down the panel, its main lobe and the side lobes beside it bending together along parabolas while the field's phase runs along them as a green and blue tint, and a small bead drifting back and forth near the top blocks a lobe that grows back further down. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the nonspreading Airy wave packet (Michael Berry and Nandor Balazs, 1979), finite-energy accelerating Airy beams (Georgios Siviloglou, John Broky, Aristide Dogariu and Demetrios Christodoulides, 2007), self-healing Airy beams (John Broky, Georgios Siviloglou, Aristide Dogariu and Demetrios Christodoulides, 2008), pattern round 73 +// Round: 73 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Truncation=0.04..0.4, Tilt=-1..1 +// +// Knob 1: Hue. Turns the light round the wheel: at 0 the beam is cyan with near-white peaks, and the field's phase tints each lobe a little towards green or towards azure, the two sides of a lobe boundary always opposite. The bead is a pale stone in any hue. +// Knob 2: Speed. The tint follows the phase turning once a second at 2 (a tenth of that at 0.2, 1.5 at 3), so green and azure run down the lobes. The bead's drift to and fro across the beam takes 9 s at 2, 90 s at 0.2 and 6 s at 3. +// Knob 3: Truncation. The a of the launch field Ai(s) exp(a s). At 0.04 the main lobe keeps a third of its peak to the bottom row and side lobes fill everything to its left down the whole panel. At 0.15 the far lobes fade first and the bottom row's brightest point is a tenth of the top's. At 0.4 the main lobe has lost 99.7% of its light by the bottom row and what is left there is a wide glow. The brightness is set row by row against the free beam, so the spreading shows as lobes dissolving rather than as the panel going dark. +// Knob 4: Tilt. The launch angle v = -1.25 + 1.25 Tilt, a factor exp(i v s) at launch, which shears the whole beam by v xi. At the opening 0.06 the main lobe enters at x 28, swings left to x 22 at row 38 and bends right to x 54 at the bottom. At -1 it falls to x 26 at row 80 and turns back to x 34, like a ball thrown upward. At 1 it is launched straight and bends 63 LEDs, leaving the panel at row 121. + +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); + +function hsv(h, s, v, o, k) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[k] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + o[k + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + o[k + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// ---- Units. The paraxial equation i dphi/dxi + (1/2) d2phi/ds2 = 0 in +// s = x / x0 across and xi = z / (k x0^2) down. One LED is 1/4 of s, and the +// 128 rows run xi from 0 at row 0 (the launch plane) to 8. +const X0 = 4, XI = 8, DXI = XI / H; +// The beam is worked on N points one LED apart, s from S0 = -96 to 32. At +// any tilt the panel sees the untilted beam only between s = -13.6 and 22.2. +// At a = 0.04 the launch field at -96 is down to 0.8% of the main lobe, and +// the spectrum at the grid's highest wavenumber, 4 pi, to 0.2%. +const N = 512, S0 = -96, MASK = N - 1; + +// ---- FFT, radix 2, in place, the sum with exp(+2 pi i m j / N) and no 1/N. +const REV = new Uint16Array(N); +for (let i = 0, b = Math.log2(N); i < N; i++) { + let r = 0; + for (let k = 0; k < b; k++) r |= ((i >> k) & 1) << (b - 1 - k); + REV[i] = r; +} +const TC = new Float64Array(N / 2), TS = new Float64Array(N / 2); +for (let i = 0; i < N / 2; i++) (TC[i] = cos((2 * PI * i) / N)), (TS[i] = sin((2 * PI * i) / N)); +function ifft(re, im) { + for (let i = 0; i < N; i++) { + const j = REV[i]; + if (j > i) { + let t = re[i]; + (re[i] = re[j]), (re[j] = t); + t = im[i]; + (im[i] = im[j]), (im[j] = t); + } + } + for (let len = 2; len <= N; len <<= 1) { + const half = len >> 1, step = N / len; + for (let i = 0; i < N; i += len) + for (let k = 0; k < half; k++) { + const c = TC[k * step], s = TS[k * step], a = i + k, b = a + half; + const xr = re[b] * c - im[b] * s, xi = re[b] * s + im[b] * c; + (re[b] = re[a] - xr), (im[b] = im[a] - xi), (re[a] += xr), (im[a] += xi); + } + } +} +// Wavenumber of FFT bin m. +const KM = new Float64Array(N); +for (let m = 0; m < N; m++) KM[m] = (2 * PI * (m < N / 2 ? m : m - N) * X0) / N; + +// ---- The free-space propagator over d rows on this grid: the inverse FFT of +// exp(-i k^2 d DXI / 2), divided by N. Convolving a field with KR/KI[d] is +// the same as one FFT step of split-step propagation, exactly. +const KR = new Float32Array(H * N), KI = new Float32Array(H * N); +{ + const re = new Float64Array(N), im = new Float64Array(N); + for (let d = 1; d < H; d++) { + for (let m = 0; m < N; m++) { + const ph = (-KM[m] * KM[m] * d * DXI) / 2; + (re[m] = cos(ph) / N), (im[m] = sin(ph) / N); + } + ifft(re, im); + KR.set(re, d * N); + KI.set(im, d * N); + } +} + +// ---- The free beam for truncation a, every row. The launch field +// Ai(s) exp(a s) has the spectrum exp(a^3/3) exp(-a k^2) exp(i (k^3/3 - a^2 k)) +// (shift the contour of Ai's integral by i a), and each plane wave picks up +// exp(-i k^2 xi / 2) on the way down. Checked against a power series of Ai +// at a = 0.04: row 0 gives 0.355028 at s = 0, 0.514561 at -1, -0.335979 at +// -3 and 0.026987 at -10, all within 1e-5; row 64 agrees with a direct +// O(N^2) transform of row 0 to five places. +function freeBeam(S, a) { + const re = new Float64Array(N), im = new Float64Array(N), c = (X0 / N) * exp((a * a * a) / 3); + for (let r = 0; r < H; r++) { + const xi = r * DXI; + for (let m = 0; m < N; m++) { + const k = KM[m], amp = c * exp(-a * k * k), ph = (k * k * k) / 3 - a * a * k + k * S0 - (k * k * xi) / 2; + (re[m] = amp * cos(ph)), (im[m] = amp * sin(ph)); + } + ifft(re, im); + let peak = 0; + for (let j = 0; j < N; j++) peak = max(peak, re[j] * re[j] + im[j] * im[j]); + S.FR.set(re, r * N); + S.FI.set(im, r * N); + S.RM[r] = peak; + } + S.a = a; +} + +// Main lobe of Ai at s = -1.0188, the first maximum. +const A1 = -1.0188; +// The bead: its row, its half width in LEDs, and its drift about the main +// lobe, from 6 LEDs right of it to 11 left. Set squarely on the main lobe at +// a = 0.04 it leaves the lobe 20% of the free beam's intensity 8 rows below, +// 47% by row 60 and 62% by row 80: the side lobes feed it back. It never +// heals all the way, since the bead keeps what it took. +const OBS_ROW = 22, OBS_W = 2.6, OBS_MID = -2.5, OBS_AMP = 8.5, OBS_T = 9; + +export function setup(params) { + params.s = { + t: 0, hue: 0, a: -1, v: 0, + FR: new Float32Array(H * N), FI: new Float32Array(H * N), RM: new Float32Array(H), + I: new Float32Array(W * H), C: new Float32Array(W * H), rgb: new Float32Array(W * H * 3), + ub: new Int32Array(16), ur: new Float32Array(16), ui: new Float32Array(16), GR: new Float32Array(W + 1), GI: new Float32Array(W + 1), + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0.04, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.t += (d * cl(k[1], 0.2, 3)) / 2; + // Steps of 0.0025 in a, so a knob held still never rebuilds the beam. A + // rebuild is 128 FFTs, about 13 ms in Node; the propagator table above is + // as many again, once at load. A frame after that is about 1.4 ms. + const a = Math.round(cl(k[2], 0.04, 0.4) * 400) / 400; + if (a !== S.a) freeBeam(S, a); + S.v = -1.25 + 1.25 * cl(k[3], -1, 1); +} + +export function draw(display, params) { + const S = params.s; + if (!S || S.a < 0) return; + const { FR, FI, RM, I, C, rgb, ub, ur, ui, GR, GI, v } = S; + // The main lobe runs along s = A1 + v xi + xi^2 / 4 in the panel's frame. + // The middle of its range across the panel sits at x = 38. + let lo = A1, hi = A1; + for (let r = 0; r < H; r++) { + const xi = r * DXI, s = A1 + v * xi + (xi * xi) / 4; + (lo = min(lo, s)), (hi = max(hi, s)); + } + const sL = (lo + hi) / 2 - 38 / X0; + // The beam's own grid point under LED x of row r is x + off(r): the tilt + // is the shear s -> s + v xi of the untilted beam. + const off = (r) => (sL - S0) * X0 - v * r * DXI * X0; + + // ---- The bead and the light it takes out, on the beam's grid. + const xio = OBS_ROW * DXI, xm = (A1 + v * xio + (xio * xio) / 4 - sL) * X0; + const co = xm + OBS_MID + OBS_AMP * sin((2 * PI * S.t) / OBS_T); + const jc = co + off(OBS_ROW); + let nb = 0; + for (let j = Math.ceil(jc - OBS_W - 0.5); j <= floor(jc + OBS_W + 0.5); j++) { + const b = cl(OBS_W + 0.5 - Math.abs(j - jc), 0, 1); + if (b <= 0) continue; + const q = OBS_ROW * N + (j & MASK); + (ub[nb] = j), (ur[nb] = b * FR[q]), (ui[nb] = b * FI[q]), nb++; + } + + // ---- The field at every LED: the free beam minus what the bead took out, + // carried down by the propagator, on the 65 grid points under the row, + // then linear between the two either side of each LED. + const wc = cos(2 * PI * S.t), ws = sin(2 * PI * S.t); + for (let r = 0; r < H; r++) { + const o = off(r), jb = floor(o), f = o - jb, d = r - OBS_ROW, kb = d * N; + for (let e = 0; e <= W; e++) { + const j = jb + e, q = r * N + (j & MASK); + let pr = FR[q], pi = FI[q]; + if (d > 0) + for (let n = 0; n < nb; n++) { + const kk = kb + ((j - ub[n]) & MASK), kr = KR[kk], ki = KI[kk]; + pr -= kr * ur[n] - ki * ui[n]; + pi -= kr * ui[n] + ki * ur[n]; + } + else if (d === 0) { + const b = 1 - cl(OBS_W + 0.5 - Math.abs(j - jc), 0, 1); + (pr *= b), (pi *= b); + } + (GR[e] = pr), (GI[e] = pi); + } + for (let x = 0; x < W; x++) { + const fr = GR[x] + f * (GR[x + 1] - GR[x]), fi = GI[x] + f * (GI[x + 1] - GI[x]); + const ii = fr * fr + fi * fi, p = r * W + x; + I[p] = ii; + C[p] = ii > 1e-12 ? (fr * wc + fi * ws) / sqrt(ii) : 0; + } + } + + // ---- Light. Brightness is intensity over the geometric mean of 0.25 and + // the free beam's brightest point in that row, to the power 0.55: a row + // whose free beam peaks 100 times below 0.25 is drawn as if 10 times + // below. The tint is cos(phase - 2 pi t), 0.05 of the wheel either way. + for (let r = 0; r < H; r++) { + const ref = sqrt(0.25 * RM[r]); + for (let x = 0; x < W; x++) { + const p = r * W + x, val = min(1, Math.pow(I[p] / ref, 0.55)); + hsv(0.5 + S.hue + 0.05 * C[p], 1 - 0.55 * val * val, val, rgb, p * 3); + } + } + // The bead: a pale stone, lit on top by the light that falls on it from + // three rows up. + for (let y = OBS_ROW - 2; y <= OBS_ROW + 1; y++) + for (let x = floor(co - OBS_W - 1); x <= co + OBS_W + 1; x++) { + if (x < 0 || x >= W) continue; + const dx = (x + 0.5 - co) / (OBS_W + 0.6), dy = (y + 0.5 - (OBS_ROW - 0.2)) / 1.9, rr = dx * dx + dy * dy; + if (rr > 1) continue; + const lit = min(1, sqrt(I[max(0, OBS_ROW - 3) * W + x] / 0.15)), sh = 0.3 + 0.25 * (1 - rr) + 0.45 * lit * (dy < 0 ? 1 : 0.4); + hsv(0.08 + S.hue, 0.15, sh, rgb, (y * W + x) * 3); + } + for (let p = 0; p < W * H; p++) display.setPixel(p % W, floor(p / W), (rgb[p * 3] * 255) | 0, (rgb[p * 3 + 1] * 255) | 0, (rgb[p * 3 + 2] * 255) | 0); +} diff --git a/incoming/pattern-study/patterns/maya-numerals.js b/incoming/pattern-study/patterns/maya-numerals.js new file mode 100644 index 000000000..8679e2615 --- /dev/null +++ b/incoming/pattern-study/patterns/maya-numerals.js @@ -0,0 +1,329 @@ +// ===== Patternflow pattern ===== +// Title: Maya Numerals +// Family: type +// Idea: A number five places tall is counted up in Maya numerals, the lowest place at the bottom: each new dot slides in from the right, five dots slide together and melt into a bar, and a place that fills (twenty, or eighteen in the Long Count's second place) collapses into a gold dot that climbs to the place above and leaves a shell for zero behind, so carries ripple up the panel. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Maya place-value numerals in base 20, a dot for one, a bar for five and a shell for zero, the places written one above the other (Dresden Codex); the Long Count, whose second place counts to 18 (worked out from the Dresden Codex by Ernst Förstemann, 1880s); pattern round 73 +// Round: 73 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Mode=0..1, Step=0..6 +// +// Knob 1: Hue. Turns the signs round the wheel: at 0 the dots and bars are jade, the shells ivory and a dot on its way up a place gold. The faint rules between the places take the jade. +// Knob 2: Speed. At 2 a dot slides in in 0.30 s, five dots melt into a bar in 0.40 s, a full place collapses in 0.36 s and its dot climbs in 0.42 s. Counting by one runs 1.7 numbers a second, so the second place moves about every 12 s. The opening carry is through three places at 4.8 s, or reaches 13.0.0.0.0 at 6.0 s in the Long Count. At 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Mode. 0 is plain base 20: every place carries at 20, four bars. It opens on 12.7.19.19.17 (top to bottom) and three counts later the carry runs up to 12.8.0.0.0. 1 is the Long Count of days: kin 20, winal 18 (18 winals of 20 days make a tun of 360 days), then 20 for the tun, katun and baktun. The winal carries when a third dot joins its three bars. It opens on 12.19.19.17.16, four days before 13.0.0.0.0, the day the Long Count reached on 21 December 2012 in the GMT correlation, and the carry runs up four places. Turning it starts the count again from that mode's opening. +// Knob 4: Step. How much each addition adds, from 0 to 6: 1, 13, 20, 260, 360, 365 or 584. The step is written in the mode's places and its bars and dots slide in one at a time, lowest place first, each fusing and carrying as it lands. 260 is 13.0 in both modes; 360 is 18.0 in base 20 but 1.0.0 in the Long Count, one dot in the third place; 365 is 18.5 against 1.0.5; 584, the Venus period of the Dresden Codex's Venus table, is 1.9.4 against 1.11.4. At Speed 2 a step of 13 adds 0.38 times a second and 584 adds 0.22 times, moving the third place every 4.5 s. + +const W = 64, H = 128, NP = W * H; +const { floor, round, min, max, sqrt, abs } = 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)); +const ss = (a, b, v) => ease((v - a) / (b - a)); +const lerp = (a, b, u) => a + (b - a) * 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]; +} + +// ---- Layout. Five places, the lowest at the bottom, each a cell 25.6 rows +// tall. A numeral is bars stacked from the bottom of the glyph, one slot +// each, and a row of up to four dots in the slot above them; the glyph is +// centred in its cell. Zero is a shell. +const P = 5, CELL = H / P, CX = 32; +const BL = 38, BT = 4.4, SP = 6, DR = 2.8, DS = 9.5; +const cyOf = (p) => H - CELL * (p + 0.5); +const slotY = (p, n, k) => cyOf(p) + ((n - 1) / 2 - k) * SP; +const dotX = (i, d) => CX + (i - (d - 1) / 2) * DS; + +// Sprites: kind 0 dot, 1 bar, 2 shell. +const DOT = 0, BAR = 1, SHELL = 2; + +// Sprites carry a position, a size s (a bar's length as a share of BL), an +// alpha a and g, how far the sign is tinted gold and glows. +function staticPlace(out, p, b, d, a) { + const n = b + (d > 0 ? 1 : 0); + if (n === 0) out.push({ k: SHELL, x: CX, y: cyOf(p), s: 1, a, g: 0 }); + for (let k = 0; k < b; k++) out.push({ k: BAR, x: CX, y: slotY(p, n, k), s: 1, a, g: 0 }); + for (let i = 0; i < d; i++) out.push({ k: DOT, x: dotX(i, d), y: slotY(p, n, b), s: 1, a, g: 0 }); +} + +// ---- Counting. Radix of each place: 20, or in the Long Count 20 for the +// kin, 18 for the winal (18 winals of 20 days make a tun of 360), then 20. +const RADIX = [[20, 20, 20, 20, 20], [20, 18, 20, 20, 20]]; +const STEPS = [1, 13, 20, 260, 360, 365, 584]; +// Openings, lowest place first. Base 20: 12.7.19.19.17, three counts before +// a carry runs up three places to 12.8.0.0.0. Long Count: 12.19.19.17.16, +// four days before 13.0.0.0.0. Counted through at Speed 2 (op times below), +// the first lands at 4.76 s and the second at 6.00 s. +// The steps are written in each mode's places when an addition starts: +// 584 = 1.9.4 in base 20 (400 + 180 + 4) and 1.11.4 in the Long Count +// (360 + 220 + 4); 365 = 18.5 against 1.0.5. Counted over 120 s at Speed 2, +// base 20 makes 202 additions of 1 and 26 of 584; 360 makes 37 in base 20, +// six signs each, and 202 in the Long Count, one dot each. +const OPEN = [[17, 19, 19, 7, 12], [16, 17, 19, 19, 12]]; + +// Op durations in seconds at Speed 2. +const DUR = { side: 0.3, bar: 0.36, climb: 0.42, fuse: 0.4, carry: 0.36, pause: 0.16 }; +const opDur = (op) => (op.k === "in" ? (op.from === "below" ? DUR.climb : op.sign === BAR ? DUR.bar : DUR.side) : DUR[op.k]); + +function reset(S) { + const o = OPEN[S.mode]; + S.b = o.map((v) => floor(v / 5)); + S.d = o.map((v) => v % 5); + S.q = []; + S.cur = null; + S.u = 0; +} + +function addition(S) { + const R = RADIX[S.mode]; + let n = STEPS[S.step]; + for (let p = 0; p < P && n > 0; p++) { + const dg = n % R[p]; + n = floor(n / R[p]); + for (let i = 0; i < floor(dg / 5); i++) S.q.push({ k: "in", p, sign: BAR, from: "side" }); + for (let i = 0; i < dg % 5; i++) S.q.push({ k: "in", p, sign: DOT, from: "side" }); + } + S.q.push({ k: "pause" }); +} + +// What happens when an op ends: the place takes the sign, and anything it +// sets off goes to the front of the queue. +function finish(S, op) { + const p = op.p, R = RADIX[S.mode]; + if (op.k === "pause") return; + if (p >= P) return; + const val = () => S.b[p] * 5 + S.d[p]; + if (op.k === "in") { + if (op.sign === BAR) S.b[p]++; + else S.d[p]++; + if (S.d[p] === 5) S.q.unshift({ k: "fuse", p }); + else if (val() >= R[p]) S.q.unshift({ k: "carry", p }); + } else if (op.k === "fuse") { + S.d[p] -= 5; + S.b[p]++; + if (val() >= R[p]) S.q.unshift({ k: "carry", p }); + } else if (op.k === "carry") { + const v = val() - R[p]; + S.b[p] = floor(v / 5); + S.d[p] = v % 5; + S.q.unshift({ k: "in", p: p + 1, sign: DOT, from: "below" }); + } +} + +export function setup(params) { + params.s = { + hue: 0, mode: 0, step: 0, b: null, d: null, q: [], cur: null, u: 0, + rgb: new Float32Array(NP * 3), sig: new Float32Array(NP * 3), hot: new Float32Array(NP * 3), tmp: new Float32Array(NP * 3), blr: new Float32Array(NP * 3), blh: new Float32Array(NP * 3), + }; + reset(params.s); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + const mode = round(cl(k[2], 0, 1)), step = cl(round(k[3]), 0, 6); + S.step = step; + if (mode !== S.mode) { + S.mode = mode; + reset(S); + } + S.u += (d * cl(k[1], 0.2, 3)) / 2; + for (let guard = 0; guard < 64; guard++) { + if (!S.cur) { + if (!S.q.length) addition(S); + S.cur = S.q.shift(); + } + const T = opDur(S.cur); + if (S.u < T) break; + S.u -= T; + finish(S, S.cur); + S.cur = null; + } +} + +// ---- Sprites for the frame. +function sprites(S) { + const out = [], op = S.cur, u = op ? S.u / opDur(op) : 0; + for (let p = 0; p < P; p++) { + const b = S.b[p], d = S.d[p]; + if (!op || op.p !== p || op.k === "pause") { + staticPlace(out, p, b, d, 1); + continue; + } + const e = ease(u); + if (op.k === "in") { + const nb = op.sign === BAR ? b + 1 : b, nd = op.sign === BAR ? d : d + 1; + const n0 = b + (d > 0 ? 1 : 0), n1 = nb + (nd > 0 ? 1 : 0); + if (b + d === 0) out.push({ k: SHELL, x: CX, y: cyOf(p), s: 1 - 0.3 * e, a: 1 - e, g: 0 }); + for (let k = 0; k < b; k++) out.push({ k: BAR, x: CX, y: lerp(slotY(p, n0, k), slotY(p, n1, k), e), s: 1, a: 1, g: 0 }); + for (let i = 0; i < d; i++) + out.push({ k: DOT, x: lerp(dotX(i, d), dotX(i, nd), e), y: lerp(slotY(p, n0, b), slotY(p, n1, nb), e), s: 1, a: 1, g: 0 }); + // The newcomer. + const tx = op.sign === BAR ? CX : dotX(d, nd), ty = slotY(p, n1, b); + if (op.from === "below") { + // Climbing from the middle of the place below, with a fading tail. + const sy = cyOf(p - 1); + for (let tr = 4; tr >= 0; tr--) { + const mm = ease(u - tr * 0.06); + out.push({ k: DOT, x: lerp(CX, tx, mm), y: lerp(sy, ty, mm), s: 1 - tr * 0.12, a: tr ? 0.35 - tr * 0.06 : 1, g: 1 - 0.6 * ss(0.7, 1, u) }); + } + } else { + const sx = W + (op.sign === BAR ? BL / 2 + 2 : DR + 2), mm = 1 - (1 - u) * (1 - u) * (1 - u); + out.push({ k: op.sign, x: lerp(sx, tx, mm), y: ty, s: 1, a: 1, g: 0.35 * (1 - u) }); + } + } else if (op.k === "fuse") { + const n = b + 1; + for (let k = 0; k < b; k++) out.push({ k: BAR, x: CX, y: slotY(p, n, k), s: 1, a: 1, g: 0 }); + // The five slide together until they touch (a chain 26 LEDs long), + // melt into one rod as thick as a dot, and the rod draws out to a bar. + const y = slotY(p, n, b), m = ss(0, 0.45, u), gap = lerp(DS, 2 * DR * 0.92, m), fade = 1 - ss(0.38, 0.6, u); + for (let i = 0; i < 5; i++) out.push({ k: DOT, x: CX + (i - 2) * gap, y, s: 1, a: fade, g: 0.7 * m }); + const melt = ss(0.35, 0.6, u), draw = ss(0.55, 1, u), chain = (8 * DR * 0.92 + 2 * DR) / BL; + if (melt > 0) out.push({ k: BAR, x: CX, y, s: lerp(chain, 1, draw), th: lerp(2 * DR, BT, draw), a: melt, g: 0.7 * (1 - draw) }); + } else if (op.k === "carry") { + // b, d still hold the place before the carry is taken out. + const R = RADIX[S.mode][p], v = b * 5 + d - R, rb = floor(v / 5), rd = v % 5; + const n = b + (d > 0 ? 1 : 0), ga = ease(u / 0.55), cy = cyOf(p); + const gone = 1 - ss(0.4, 0.6, u); + for (let k = 0; k < b; k++) out.push({ k: BAR, x: CX, y: lerp(slotY(p, n, k), cy, ga), s: 1 - 0.85 * ga, a: gone, g: ga }); + for (let i = 0; i < d; i++) out.push({ k: DOT, x: lerp(dotX(i, d), CX, ga), y: lerp(slotY(p, n, b), cy, ga), s: 1 - 0.4 * ga, a: gone, g: ga }); + const pop = ss(0.4, 0.65, u); + if (pop > 0) out.push({ k: DOT, x: CX, y: cy, s: pop, a: 1, g: 1 }); + const back = ss(0.55, 1, u); + if (back > 0) { + staticPlace(out, p, rb, rd, back); + } + } + } + // A carry off the top place climbs off the panel. + if (op && op.k === "in" && op.p >= P) { + for (let tr = 4; tr >= 0; tr--) { + const mm = ease(u - tr * 0.06); + out.push({ k: DOT, x: CX, y: lerp(cyOf(P - 1), -8, mm), s: 1 - tr * 0.12, a: tr ? 0.35 - tr * 0.06 : 1, g: 1 }); + } + } + return out; +} + +// ---- Drawing the signs, lit from the upper left. +const LX = -0.45, LY = -0.6, LZ = 0.66; + +function bead(buf, x, y, r, col, a) { + for (let j = max(0, floor(y - r - 1)); j <= min(H - 1, floor(y + r + 1)); j++) + for (let i = max(0, floor(x - r - 1)); i <= min(W - 1, floor(x + r + 1)); i++) { + const dx = i + 0.5 - x, dy = j + 0.5 - y, dd = sqrt(dx * dx + dy * dy), cov = cl(r + 0.5 - dd, 0, 1) * a; + if (cov <= 0) continue; + const nx = dx / (r + 0.3), ny = dy / (r + 0.3), nz = sqrt(max(0, 1 - nx * nx - ny * ny)); + const lam = max(0, nx * LX + ny * LY + nz * LZ), spec = Math.pow(lam, 12); + const v = 0.28 + 0.72 * lam; + const o = (j * W + i) * 3; + for (let c = 0; c < 3; c++) buf[o + c] = max(buf[o + c], cov * (col[c] * v + 0.6 * spec)); + } +} + +function rod(buf, x, y, len, th, col, a) { + const r = th / 2, hx = max(0, len / 2 - r); + for (let j = max(0, floor(y - r - 1)); j <= min(H - 1, floor(y + r + 1)); j++) + for (let i = max(0, floor(x - hx - r - 1)); i <= min(W - 1, floor(x + hx + r + 1)); i++) { + const dx0 = i + 0.5 - x, dy = j + 0.5 - y, dx = dx0 - cl(dx0, -hx, hx), dd = sqrt(dx * dx + dy * dy), cov = cl(r + 0.5 - dd, 0, 1) * a; + if (cov <= 0) continue; + const nx = dx / (r + 0.3), ny = dy / (r + 0.3), nz = sqrt(max(0, 1 - nx * nx - ny * ny)); + const lam = max(0, nx * LX + ny * LY + nz * LZ), spec = Math.pow(lam, 12); + const v = 0.28 + 0.72 * lam; + const o = (j * W + i) * 3; + for (let c = 0; c < 3; c++) buf[o + c] = max(buf[o + c], cov * (col[c] * v + 0.5 * spec)); + } +} + +// The shell: an oval 21 by 12, pointed a little at the ends, with a rim, +// a long curved slit through the middle and short teeth along both lips, +// like a cowrie turned over. Ivory, lit from the upper left. +function shell(buf, x, y, sc, col, a) { + const rx = 10.5 * sc, ry = 5.9 * sc; + for (let j = max(0, floor(y - ry - 1)); j <= min(H - 1, floor(y + ry + 1)); j++) + for (let i = max(0, floor(x - rx - 1)); i <= min(W - 1, floor(x + rx + 1)); i++) { + const u = (i + 0.5 - x) / rx, v = (j + 0.5 - y) / ry, au = abs(u), q = Math.pow(Math.pow(au, 1.7) + Math.pow(abs(v), 2), 1 / 2); + if (q > 1.1) continue; + const rimD = abs(q - 1) * ry * 1.1; + let val = cl(1.15 - rimD, 0, 1); + if (q < 1) { + // Body, brighter up and left. + val = max(val, 0.2 + 0.28 * cl(0.5 - 0.5 * u - 0.9 * v, 0, 1)); + // The slit: a gentle arc through the middle, dark. + const sv = 0.06 - 0.12 * u * u, ds = abs(v - sv) * ry; + if (au < 0.82) { + if (ds < 0.55) val *= 0.15; + // Teeth: one-LED ticks across each lip, every third column. + else if (ds < 2 && (((i - round(x)) % 3) + 3) % 3 === 1) val = max(val, 0.85 * a); + } + } + val *= a * cl((1.1 - q) * 8, 0, 1); + const o = (j * W + i) * 3; + for (let c = 0; c < 3; c++) buf[o + c] = max(buf[o + c], val * col[c]); + } +} + +// A 5-tap binomial blur, across and then down, over rows y0..y1 only. +function blur(src, tmp, out, y0, y1) { + const R = W * 3; + y0 = max(0, y0); + y1 = min(H - 1, y1); + for (let y = max(0, y0 - 2); y <= min(H - 1, y1 + 2); y++) + for (let x = 0; x < W; x++) + for (let c = 0; c < 3; c++) { + const p = (y * W + x) * 3 + c; + tmp[p] = (6 * src[p] + 4 * ((x > 0 ? src[p - 3] : 0) + (x < W - 1 ? src[p + 3] : 0)) + (x > 1 ? src[p - 6] : 0) + (x < W - 2 ? src[p + 6] : 0)) / 16; + } + for (let y = y0; y <= y1; y++) + for (let x = 0; x < W; x++) + for (let c = 0; c < 3; c++) { + const p = (y * W + x) * 3 + c; + out[p] = (6 * tmp[p] + 4 * ((y > 0 ? tmp[p - R] : 0) + (y < H - 1 ? tmp[p + R] : 0)) + (y > 1 ? tmp[p - 2 * R] : 0) + (y < H - 2 ? tmp[p + 2 * R] : 0)) / 16; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { rgb, sig, hot, tmp, blr, blh } = S; + const jade = hsv(0.4 + S.hue, 0.6, 1), gold = hsv(0.13 + S.hue, 0.5, 1), ivory = hsv(0.11 + S.hue, 0.18, 0.95); + sig.fill(0); + hot.fill(0); + // Rows the moving signs cover, so the wide glow is worked only there. + let h0 = H, h1 = -1; + for (const sp of sprites(S)) { + if (sp.a <= 0.01) continue; + const g = cl(sp.g, 0, 1); + const col = [lerp(jade[0], gold[0], g), lerp(jade[1], gold[1], g), lerp(jade[2], gold[2], g)]; + // Signs on the move also go into the hot buffer, which glows wider. + for (const [buf, a] of g > 0.02 ? [[sig, sp.a], [hot, sp.a * g]] : [[sig, sp.a]]) { + if (sp.k === DOT) bead(buf, sp.x, sp.y, DR * sp.s, col, a); + else if (sp.k === BAR) rod(buf, sp.x, sp.y, max(BT, BL * sp.s), sp.th || BT, col, a); + else shell(buf, sp.x, sp.y, sp.s, ivory, a); + } + if (g > 0.02) (h0 = min(h0, floor(sp.y) - 4)), (h1 = max(h1, floor(sp.y) + 4)); + } + blur(sig, tmp, blr, 0, H - 1); + blh.fill(0); + if (h1 >= 0) { + blur(hot, tmp, blh, h0 - 4, h1 + 4); + hot.fill(0); + blur(blh, tmp, hot, h0 - 6, h1 + 6); + } + const bg = hsv(0.4 + S.hue, 0.5, 0.05); + for (let y = 0; y < H; y++) { + const line = abs(((y + 0.5) % CELL) - 0.5) < 0.6 && y > 2 ? 0.07 : 0; + for (let x = 0; x < W; x++) { + const p = y * W + x, o = p * 3; + for (let c = 0; c < 3; c++) rgb[o + c] = bg[c] + line * jade[c] + 0.85 * sig[o + c] + 0.45 * blr[o + c] + 1.4 * hot[o + c]; + } + } + for (let p = 0; p < NP; p++) + display.setPixel(p % W, floor(p / W), min(255, rgb[p * 3] * 255) | 0, min(255, rgb[p * 3 + 1] * 255) | 0, min(255, rgb[p * 3 + 2] * 255) | 0); +} From aadde486d741cfbcc8d44c0d4c5995b89b1a1e64 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 22:12:06 +0000 Subject: [PATCH 6/8] pattern-study: raise damask motion with a swell under the cloth Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6N31ff3CzvqRufZu6W --- incoming/pattern-study/patterns/damask.js | 38 ++++++++++++++++++----- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/incoming/pattern-study/patterns/damask.js b/incoming/pattern-study/patterns/damask.js index 05ea227f4..22197a5c6 100644 --- a/incoming/pattern-study/patterns/damask.js +++ b/incoming/pattern-study/patterns/damask.js @@ -12,9 +12,9 @@ // @knobs Hue=0..1, Speed=0.2..3, Word=0..3, Lamp=0..1 // // Knob 1: Hue. The colour of the one thread, always pale: linen ivory at 0, mint at 0.25, a cold blue at 0.5 and rose at 0.75. -// Knob 2: Speed. How fast the lamp walks round the table: once in 64 s at 2, so the word turns from light to dark every 16 s; 43 s at 3 and over ten minutes at 0.2. +// Knob 2: Speed. How fast the lamp walks round the table: once in 64 s at 2, so the word turns from light to dark every 16 s; 43 s at 3 and over ten minutes at 0.2. The low swells under the cloth keep the same clock. // Knob 3: Word. What the cloth says: LUX, the Hangul syllable 빛 (light), NOX, or 밤 (night). Each word is woven with the stem width that makes it 10 to 11% of the cloth: 4.0 LEDs for LUX, 3.3 to 3.5 for the others. -// Knob 4: Lamp. How high the lamp stands: at 0.06 it rakes the cloth from 8 degrees up and the word is sharp; towards 1 it climbs to 68 degrees, both threads catch the light alike and the word sinks into the cloth. +// Knob 4: Lamp. How high the lamp stands: at 0.06 it rakes the cloth from 8 degrees up, the word is sharp and every swell in the cloth shows; towards 1 it climbs to 68 degrees, both threads catch the light alike, the swells flatten out and the word sinks into the cloth. const W = 64, H = 128, NP = W * H; const { floor, round, sqrt, cos, sin, tan, min, abs, PI } = Math; @@ -153,24 +153,39 @@ const R = 300, PEAK = 232; // The lamp starts 28.1 degrees short of due right of the cloth and walks // 5.625 degrees a second at Speed 2, so at 5 s it stands square to the // warp. Checked over every crossing and frame from 2 s to 8 s: the lamp is -// never more than 30.5 degrees off square, a weft float never rises above -// 0.074 of the peak (17 of 255, under the 24 that counts as lit), a warp -// float never falls below 0.56 of it, and the word stays bright on a dark +// never more than 30.5 degrees off square, and with the swell below a weft +// float never rises above 17.6 of 255 (under the 24 that counts as lit), a +// warp float never falls below 92, and the word stays bright on a dark // ground. The two sheens are equal where dx^2 = dy^2: a straight line at 45 // degrees through the foot of the lamp, which crosses the cloth from the // bottom right corner at 10.7 s to the top left at 15.3 s and passes the // middle at 13 s. The word vanishes along that line and comes back dark. const A0 = (-28.125 * PI) / 180, RATE = (5.625 * PI) / 180; +// ---- The swell. The cloth is not quite flat: two low swells run under it, +// like a tablecloth lifting in a draught. Each is [direction in radians from +// the x axis, wavelength in LEDs, speed in LEDs a second at Speed 2, the +// steepest slope it gives]. A tilt changes how much light the cloth takes +// from the lamp: with the surface normal (-gx, -gy, 1) and D the way to the +// lamp, the light goes as N.D / D.z = 1 - (gx dx + gy dy) / hz. With the +// lamp 8 degrees up, D runs about 7.3 LEDs across for each one up, so a +// slope of 0.03 toward it dims or brightens the threads by 22%; with the +// lamp 68 degrees up, by 1%, so the swell shows only in raking light, as +// folds do. At Speed 2 the swells pass each crossing every 6 and 5.6 s, +// and from 2 s to 8 s they scale the light by 0.62 to 1.39. +const SWELL = [[0.15, 36, 6, 0.03], [0.9, 28, 5, 0.02]].map(([a, l, v, g]) => ({ ux: cos(a), uy: sin(a), k: (2 * PI) / l, w: (2 * PI * v) / l, g })); + export function setup(params) { - params.s = { a: A0, hue: 0, word: 0, lamp: 0.06, top: null, wordShown: -1 }; + params.s = { a: A0, t: 0, hue: 0, word: 0, lamp: 0.06, top: null, wordShown: -1 }; } export function update(dt, input, params) { if (!params.s) setup(params); const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); S.hue = k[0]; - S.a += (d * RATE * cl(k[1], 0.2, 3)) / 2; + const clock = (d * cl(k[1], 0.2, 3)) / 2; + S.a += RATE * clock; + S.t += clock; S.word = cl(round(k[2]), 0, 3); S.lamp = cl(k[3], 0, 1); if (S.wordShown !== S.word) { @@ -192,12 +207,19 @@ export function draw(display, params) { const dy = ly - (y + 0.5); for (let x = 0; x < W; x++) { const dx = lx - (x + 0.5), inv = 1 / (dx * dx + dy * dy + hz * hz); + let gx = 0, gy = 0; + for (const w of SWELL) { + const q = w.g * cos(w.k * (w.ux * x + w.uy * y) - w.w * S.t); + gx += q * w.ux; + gy += q * w.uy; + } + const lit = 1 - (gx * dx + gy * dy) / hz; const p = y * W + x; // T.L squared: dy^2 / |D|^2 for a warp float, dx^2 / |D|^2 for a weft. let s, f; if (top[p]) (s = 1 - dy * dy * inv), (f = WARP[x]); else (s = 1 - dx * dx * inv), (f = WEFT[y]); - const v = PEAK * f * s * s; + const v = PEAK * f * s * s * (lit > 0 ? lit : 0); display.setPixel(x, y, round(v * c[0]), round(v * c[1]), round(v * c[2])); } } From dce35e2b6454eb820ca6f29c416629ae14820ef9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 22:13:21 +0000 Subject: [PATCH 7/8] pattern-study: add durer-alphabet and topologists-sine-curve Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6N31ff3CzvqRufZu6W --- .../pattern-study/patterns/durer-alphabet.js | 504 ++++++++++++++++++ .../patterns/topologists-sine-curve.js | 222 ++++++++ 2 files changed, 726 insertions(+) create mode 100644 incoming/pattern-study/patterns/durer-alphabet.js create mode 100644 incoming/pattern-study/patterns/topologists-sine-curve.js diff --git a/incoming/pattern-study/patterns/durer-alphabet.js b/incoming/pattern-study/patterns/durer-alphabet.js new file mode 100644 index 000000000..e4cd2dea6 --- /dev/null +++ b/incoming/pattern-study/patterns/durer-alphabet.js @@ -0,0 +1,504 @@ +// ===== Patternflow pattern ===== +// Title: Dürer's Alphabet +// Family: type +// Idea: Two ruled squares stand one above the other, and in each in turn a Roman capital is constructed by Dürer's method, its stems a tenth of the square, a travelling pen ruling the stem edges and sweeping compass arcs for the bowls and serif brackets, then ivory ink runs through the body, the paper lights up around it, and the plate holds while the next letter is drawn in the other square. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Roman capitals constructed with ruler and compass in a square, Underweysung der Messung (Albrecht Dürer, 1525), pattern round 73 +// Round: 73 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Letter=0..12, Stem=0.1..0.2 +// +// Knob 1: Hue. Turns the plate round the wheel: at 0 the ink is ivory, the construction lines pale teal and the paper deep blue; at 0.5 the ink is pale blue, the lines salmon and the paper dark amber. +// Knob 2: Speed. At 2 a letter takes 8 s: 4 s of ruler and compass, 1.6 s for the ink to run through the body, 1 s for the paper, so each finished plate stands about 9 s. At 0.2 it is ten times slower. +// Knob 3: Letter. Which of the thirteen capitals A C D E H I L M N O T V X comes first; the rest follow in that order and wrap round. At 0 the panel opens on a finished A with the C being ruled below it. +// Knob 4: Stem. The thick stroke as a share of the square's side. At 0.1 it is Dürer's tenth, 5.9 LEDs; at 0.2 the letters are twice as heavy, and the thin strokes, the serif brackets and the bowls' counters follow. + +const W = 64, H = 128, SH = 64, NL = 4096; +const { floor, round, min, max, sqrt, sin, cos, tan, acos, atan2, abs, 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 square. Each sheet is 64 rows; its square runs from LED centre +// 2.5 to 61.5, so the outline sits on whole LEDs and the side is 59. Letters +// are written in units of a tenth of the side (u right, v down, 0..10): one +// unit is 5.9 LEDs, and at Stem 0.1 one unit is the thick stroke, the +// proportion Dürer gives. The thin stroke is a third of the thick one, which +// is how Alan Hoenig's METAFONT reading of the rules has it (TUGboat 11:3). +// The centres of the bowls and the serif brackets are this pattern's own. +const S0 = 2.5, UN = 5.9; +const toX = (u) => S0 + u * UN; + +// Clip the infinite line through a and b to the square: a ruler line. +function ruled(a, b) { + const d = [b[0] - a[0], b[1] - a[1]]; + let t0 = -1e9, t1 = 1e9; + for (let k = 0; k < 2; k++) { + if (abs(d[k]) < 1e-9) continue; + const p = (0 - a[k]) / d[k], q = (10 - a[k]) / d[k]; + t0 = max(t0, min(p, q)); + t1 = min(t1, max(p, q)); + } + return { k: 0, a: [a[0] + d[0] * t0, a[1] + d[1] * t0], b: [a[0] + d[0] * t1, a[1] + d[1] * t1] }; +} + +// ---- The capitals. Each is a union of convex polygons (k 0), rings (k 1: +// inside one disk, outside its hole disks, on the right side of its clip +// lines) and serif brackets (k 2: the kite between the corner, the two +// tangent points and the compass centre, less the compass disk). Building a +// letter also lists its construction: ruler lines (k 0) and compass arcs +// (k 1), in the order they are drawn. +function build(ch, w) { + const t = w / 3, r = 0.6 + 0.25 * w, P = [], G = [], ends = new Set(); + const poly = (pts) => P.push({ k: 0, pts }); + const edgeAt = (e, v) => e[0][0] + ((e[1][0] - e[0][0]) * (v - e[0][1])) / (e[1][1] - e[0][1]); + const hwOf = (dx, th) => (th / 2) * sqrt(dx * dx + 100) / 10; + // A straight stroke over the full height: centre ut on the cap line, ub on + // the base line, th thick across. Thick ones are ruled along both edges, + // thin ones along the middle. + const stroke = (ut, ub, th, v0 = 0, v1 = 10) => { + const hw = (th / 2) * sqrt((ub - ut) ** 2 + (v1 - v0) ** 2) / (v1 - v0); + const s = { L: [[ut - hw, v0], [ub - hw, v1]], R: [[ut + hw, v0], [ub + hw, v1]], c: [[ut, v0], [ub, v1]] }; + poly([s.L[0], s.R[0], s.R[1], s.L[1]]); + if (th > 0.6 * w) G.push(ruled(s.L[0], s.L[1]), ruled(s.R[0], s.R[1])); + else G.push(ruled(s.c[0], s.c[1])); + return s; + }; + // A horizontal thin bar, ruled along its middle. + const bar = (u0, u1, v0, v1) => { + poly([[u0, v0], [u1, v0], [u1, v1], [u0, v1]]); + G.push(ruled([0, (v0 + v1) / 2], [10, (v0 + v1) / 2])); + }; + // A serif on stroke s: end 1 its foot on the base line, 0 its head on the + // cap line; sides 1 left, 2 right, 3 both. A thin bar runs along the line + // and a quarter-round bracket joins it to each edge. On a slanted stroke + // the acute side's bracket comes out longer, tangent length r / tan(half + // the angle). + const serif = (s, end, sides) => { + const vb = end ? s.L[1][1] : s.L[0][1], vq = end ? vb - t : vb + t; + let bl = max(edgeAt(s.L, vb), edgeAt(s.L, vq)), br = min(edgeAt(s.R, vb), edgeAt(s.R, vq)); + for (let side = 0; side < 2; side++) { + if (!(sides & (side + 1))) continue; + const e = side ? s.R : s.L, p = [edgeAt(e, vq), vq], d1 = [side ? 1 : -1, 0]; + let d2 = end ? [e[0][0] - e[1][0], e[0][1] - e[1][1]] : [e[1][0] - e[0][0], e[1][1] - e[0][1]]; + const n = Math.hypot(d2[0], d2[1]); + d2 = [d2[0] / n, d2[1] / n]; + // A very acute corner would throw the bracket far out, so its + // tangent length stops at 1.9 units and the radius shrinks to match. + const h = acos(cl(d1[0] * d2[0] + d1[1] * d2[1], -1, 1)) / 2, tl = min(r / tan(h), 1.9), rr = tl * tan(h), pc = tl / cos(h); + const bx = d1[0] + d2[0], by = d1[1] + d2[1], bn = Math.hypot(bx, by); + const c = [p[0] + (bx / bn) * pc, p[1] + (by / bn) * pc], T1 = [p[0] + d1[0] * tl, p[1]], T2 = [p[0] + d2[0] * tl, p[1] + d2[1] * tl]; + P.push({ k: 2, kite: [p, T1, c, T2], c, r: rr }); + const a1 = atan2(T1[1] - c[1], T1[0] - c[0]); + let da = atan2(T2[1] - c[1], T2[0] - c[0]) - a1; + da -= 2 * PI * round(da / (2 * PI)); + G.push({ k: 1, c, r: rr, a0: a1, a1: a1 + da }); + if (side) br = T1[0]; + else bl = T1[0]; + } + poly([[bl, min(vb, vq)], [br, min(vb, vq)], [br, max(vb, vq)], [bl, max(vb, vq)]]); + ends.add(end); + }; + const circ = (c, R) => G.push({ k: 1, c, r: R, a0: -PI / 2, a1: 1.5 * PI }); + // The O and the C: a disk of radius 5 less two disks of radius 5 - w whose + // centres sit w - t either side of the middle along an axis leaning 20 + // degrees to the left, so the ring is t thick at 11 and 5 o'clock and w + // thick at 2 and 8. + const ring = (clip) => { + const e = w - t, ax = [-sin(0.35), -cos(0.35)], h1 = [5 + ax[0] * e, 5 + ax[1] * e, 5 - w], h2 = [5 - ax[0] * e, 5 - ax[1] * e, 5 - w]; + P.push({ k: 1, o: [5, 5, 5], h: [h1, h2], clip }); + G.push(ruled([5 - ax[0] * 5, 5 - ax[1] * 5], [5 + ax[0] * 5, 5 + ax[1] * 5])); + circ([5, 5], 5), circ([h1[0], h1[1]], h1[2]), circ([h2[0], h2[1]], h2[2]); + }; + let s, s2; + switch (ch) { + case "A": { + const kt = 5.2, kh = hwOf(3.2, w), ut = kt - kh + hwOf(-3.4, t); + s2 = stroke(ut, 1.6, t), s = stroke(kt, 8.4, w); + const v0 = 6.3, v1 = 6.3 + t, at = (e, v) => edgeAt(e, v); + poly([[at(s2.c, v0), v0], [at(s.c, v0), v0], [at(s.c, v1), v1], [at(s2.c, v1), v1]]); + G.push(ruled([0, v0 + t / 2], [10, v0 + t / 2])); + serif(s2, 1, 3), serif(s, 1, 3); + break; + } + case "C": + ring([[-1, 0, 8.6]]); + break; + case "D": { + s = stroke(1.4 + w / 2, 1.4 + w / 2, w); + const c = 4.4, hc = 9.4 - w - (5 - t); + bar(1.4 + w, c, 0, t), bar(1.4 + w, c, 10 - t, 10); + P.push({ k: 1, o: [c, 5, 5], h: [[hc, 5, 5 - t]], clip: [[1, 0, -c]] }); + circ([c, 5], 5), circ([hc, 5], 5 - t); + serif(s, 0, 1), serif(s, 1, 1); + break; + } + case "E": { + const x = 1.6 + w; + s = stroke(1.6 + w / 2, 1.6 + w / 2, w); + bar(x, 8.2, 0, t), bar(x, 7.4, 4.85 - t / 2, 4.85 + t / 2), bar(x, 8.6, 10 - t, 10); + poly([[8.2 - t, 0], [8.2, 0], [8.2, 1.8], [8.2 - t, 1.8]]); + poly([[7.4 - t, 4.15], [7.4, 4.15], [7.4, 5.55], [7.4 - t, 5.55]]); + poly([[8.6 - t, 8.2], [8.6, 8.2], [8.6, 10], [8.6 - t, 10]]); + serif(s, 0, 1), serif(s, 1, 1); + break; + } + case "H": + s = stroke(1.5 + w / 2, 1.5 + w / 2, w), s2 = stroke(8.5 - w / 2, 8.5 - w / 2, w); + bar(1.5 + w, 8.5 - w, 4.7, 4.7 + t); + serif(s, 0, 3), serif(s, 1, 3), serif(s2, 0, 3), serif(s2, 1, 3); + break; + case "I": + s = stroke(5, 5, w); + serif(s, 0, 3), serif(s, 1, 3); + break; + case "L": + s = stroke(1.8 + w / 2, 1.8 + w / 2, w); + bar(1.8 + w, 8.6, 10 - t, 10); + poly([[8.6 - t, 8.0], [8.6, 8.0], [8.6, 10], [8.6 - t, 10]]); + serif(s, 0, 3), serif(s, 1, 1); + break; + case "M": { + s = stroke(1.6, 1.2, t); + stroke(2.0, 5.0, w), stroke(8.0, 5.0, t); + s2 = stroke(8.4, 8.8, w); + serif(s, 1, 3), serif(s2, 1, 3), serif(s, 0, 1), serif(s2, 0, 2); + break; + } + case "N": { + s = stroke(1.6, 1.6, t), s2 = stroke(8.4, 8.4, t); + const hk = hwOf(6, w); + stroke(1.6 - t / 2 + hk, 8.4 + t / 2 - hk, w); + serif(s, 1, 3), serif(s, 0, 1), serif(s2, 0, 3); + break; + } + case "O": + ring([]); + break; + case "T": + bar(0.9, 9.1, 0, t); + poly([[0.9, 0], [0.9 + t, 0], [0.9 + t, 1.8], [0.9, 1.8]]); + poly([[9.1 - t, 0], [9.1, 0], [9.1, 1.8], [9.1 - t, 1.8]]); + s = stroke(5, 5, w, t, 10); + serif(s, 1, 3); + break; + case "V": { + const kb = 4.8, hk = hwOf(2.9, w); + s = stroke(1.9, kb, w), s2 = stroke(8.1, kb + hk - hwOf(-3.3, t), t); + serif(s, 0, 3), serif(s2, 0, 3); + break; + } + case "X": + s = stroke(2.0, 8.0, w), s2 = stroke(7.6, 2.4, t); + serif(s, 0, 3), serif(s, 1, 3), serif(s2, 0, 3), serif(s2, 1, 3); + break; + } + // The serif heights are ruled first, right across the square. + const pre = []; + if (ends.has(0)) pre.push(ruled([0, t], [10, t])); + if (ends.has(1)) pre.push(ruled([0, 10 - t], [10, 10 - t])); + return { P, G: pre.concat(G) }; +} + +// Each piece becomes a box, a list of half-planes a u + b v + c >= 0, a disk +// it must be in, and disks it must stay out of. +function compile(P) { + const half = (pts) => { + let area = 0; + for (let i = 0; i < pts.length; i++) { + const a = pts[i], b = pts[(i + 1) % pts.length]; + area += a[0] * b[1] - b[0] * a[1]; + } + const sg = area > 0 ? 1 : -1, hp = []; + for (let i = 0; i < pts.length; i++) { + const a = pts[i], b = pts[(i + 1) % pts.length], du = b[0] - a[0], dv = b[1] - a[1]; + hp.push(-dv * sg, du * sg, (dv * a[0] - du * a[1]) * sg); + } + return hp; + }; + return P.map((q) => { + let pts = q.pts || q.kite, o = null, out = []; + if (q.k === 1) { + pts = [[q.o[0] - q.o[2], q.o[1] - q.o[2]], [q.o[0] + q.o[2], q.o[1] + q.o[2]]]; + o = [q.o[0], q.o[1], q.o[2] ** 2]; + out = q.h.map((h) => [h[0], h[1], h[2] ** 2]); + } else if (q.k === 2) out = [[q.c[0], q.c[1], q.r * q.r]]; + const us = pts.map((p) => p[0]), vs = pts.map((p) => p[1]); + let hp = q.k === 1 ? [] : half(pts); + if (q.k === 1) for (const c of q.clip) hp.push(c[0], c[1], c[2]); + return { u0: min(...us), u1: max(...us), v0: min(...vs), v1: max(...vs), hp, o, out }; + }); +} + +function inside(Q, u, v) { + next: for (const q of Q) { + if (u < q.u0 || u > q.u1 || v < q.v0 || v > q.v1) continue; + const hp = q.hp; + for (let i = 0; i < hp.length; i += 3) if (hp[i] * u + hp[i + 1] * v + hp[i + 2] < -1e-12) continue next; + if (q.o && (u - q.o[0]) ** 2 + (v - q.o[1]) ** 2 > q.o[2]) continue; + for (const h of q.out) if ((u - h[0]) ** 2 + (v - h[1]) ** 2 < h[2]) continue next; + return true; + } + return false; +} + +// Distance in 1 and 1.414 steps over the 64 x 64 sheet, from every LED +// already at 0. With a mask it only walks inside the mask, a geodesic: +// Dijkstra with a binary heap. +const STEPS = [[1, 0, 1], [-1, 0, 1], [0, 1, 1], [0, -1, 1], [1, 1, 1.414], [-1, 1, 1.414], [1, -1, 1.414], [-1, -1, 1.414]]; +function distance(d, mask) { + const heap = [], key = (i) => d[heap[i]]; + const push = (p) => { + heap.push(p); + for (let i = heap.length - 1; i > 0; ) { + const j = (i - 1) >> 1; + if (key(j) <= key(i)) break; + const tmp = heap[i]; + heap[i] = heap[j], heap[j] = tmp, i = j; + } + }; + const pop = () => { + const top = heap[0], last = heap.pop(); + if (heap.length) { + heap[0] = last; + for (let i = 0; ; ) { + const a = 2 * i + 1, b = a + 1; + let m = i; + if (a < heap.length && key(a) < key(m)) m = a; + if (b < heap.length && key(b) < key(m)) m = b; + if (m === i) break; + const tmp = heap[i]; + heap[i] = heap[m], heap[m] = tmp, i = m; + } + } + return top; + }; + const done = new Uint8Array(NL); + for (let p = 0; p < NL; p++) if (d[p] === 0) push(p); + while (heap.length) { + const p = pop(); + if (done[p]) continue; + done[p] = 1; + const x = p & 63, y = p >> 6; + for (let k = 0; k < 8; k++) { + const xx = x + STEPS[k][0], yy = y + STEPS[k][1], c = STEPS[k][2]; + if (xx < 0 || yy < 0 || xx > 63 || yy > 63) continue; + const q = yy * 64 + xx; + if ((mask && !mask[q]) || d[p] + c >= d[q]) continue; + d[q] = d[p] + c; + push(q); + } + } +} + +// ---- One letter on a 64 x 64 sheet: ink coverage from 4 x 4 samples an +// LED, when the ink reaches each LED (geodesic from the top left of the +// body, 0..1), when the paper lights (by distance from the letter, 0..1), +// and the construction lines with the moment the pen passes each LED. +// A letter is worked out the first time it is needed, 5 to 10 ms in Node +// once warm, and kept until Stem moves; the first frame works out two. +const SEQ = "ACDEHILMNOTVX"; +function letter(ch, w) { + const { P, G } = build(ch, w), Q = compile(P); + const cov = new Float32Array(NL), mask = new Uint8Array(NL); + for (let y = 0; y < 64; y++) + for (let x = 0; x < 64; x++) { + let n = 0; + for (let j = 0; j < 4; j++) + for (let i = 0; i < 4; i++) if (inside(Q, (x + (i + 0.5) / 4 - S0) / UN, (y + (j + 0.5) / 4 - S0) / UN)) n++; + cov[y * 64 + x] = n / 16; + mask[y * 64 + x] = n >= 6 ? 1 : 0; + } + // Ink: from the masked LED nearest the top left. + const fill = new Float32Array(NL).fill(1e9); + let seed = -1, best = 1e9; + for (let p = 0; p < NL; p++) if (mask[p] && (p >> 6) + 0.5 * (p & 63) < best) (best = (p >> 6) + 0.5 * (p & 63)), (seed = p); + fill[seed] = 0; + distance(fill, mask); + let fm = 0; + for (let p = 0; p < NL; p++) if (mask[p] && fill[p] > fm) fm = fill[p]; + for (let p = 0; p < NL; p++) { + if (!cov[p] || mask[p]) continue; + const x = p & 63, y = p >> 6; + let b = 1e9; + for (let j = -1; j <= 1; j++) for (let i = -1; i <= 1; i++) if (x + i >= 0 && x + i < 64 && y + j >= 0 && y + j < 64 && mask[(y + j) * 64 + x + i]) b = min(b, fill[(y + j) * 64 + x + i] + 1); + fill[p] = b; + } + for (let p = 0; p < NL; p++) fill[p] = cov[p] ? min(1, fill[p] / (fm + 1)) : 1; + // Paper: distance from the body. + const gd = new Float32Array(NL); + for (let p = 0; p < NL; p++) gd[p] = mask[p] ? 0 : 1e9; + distance(gd, null); + let gm = 0; + for (let p = 0; p < NL; p++) if (inPaper(p) && gd[p] > gm) gm = gd[p]; + for (let p = 0; p < NL; p++) gd[p] = min(1, gd[p] / gm); + // Construction: each element gets a share of the 4 s of 0.6 plus its + // length over 2.5 units, so the O's outer circle takes 1.35 s at Speed 2. + let tot = 0; + const len = G.map((g) => (g.k ? g.r * abs(g.a1 - g.a0) : Math.hypot(g.b[0] - g.a[0], g.b[1] - g.a[1]))); + len.forEach((l) => (tot += 0.6 + l / 2.5)); + const gw = new Float32Array(NL), gt = new Float32Array(NL).fill(2); + let acc = 0; + G.forEach((g, i) => { + g.t0 = acc / tot; + acc += 0.6 + len[i] / 2.5; + g.t1 = acc / tot; + const n = max(2, round((len[i] * UN) / 0.3)); + for (let k = 0; k <= n; k++) { + const tt = g.t0 + ((g.t1 - g.t0) * k) / n; + splat(at(g, k / n), (q, wt) => { + if (wt < 0.05) return; + if (wt > gw[q]) gw[q] = wt; + if (tt < gt[q]) gt[q] = tt; + }); + } + }); + return { cov, fill, gd, gw, gt, G }; +} + +// Where element g is at s (0..1), in sheet LEDs. +function at(g, s) { + if (g.k === 0) return [toX(g.a[0] + (g.b[0] - g.a[0]) * s), toX(g.a[1] + (g.b[1] - g.a[1]) * s)]; + const a = g.a0 + (g.a1 - g.a0) * s; + return [toX(g.c[0] + g.r * cos(a)), toX(g.c[1] + g.r * sin(a))]; +} + +// A point split over its four nearest LEDs of the sheet, by area. +function splat([px, py], f) { + const fx = px - 0.5, fy = py - 0.5, ix = floor(fx), iy = floor(fy), ax = fx - ix, ay = fy - iy; + for (let j = 0; j < 2; j++) + for (let i = 0; i < 2; i++) { + const xx = ix + i, yy = iy + j; + if (xx >= 0 && yy >= 0 && xx < 64 && yy < 64) f(yy * 64 + xx, (i ? ax : 1 - ax) * (j ? ay : 1 - ay)); + } +} + +// The paper: LEDs 2..61 of the sheet, the square and its outline. +const inPaper = (p) => (p & 63) >= 2 && (p & 63) <= 61 && p >> 6 >= 2 && p >> 6 <= 61; + +// Paper grain: value noise on a 5 LED lattice, 0.75..1.25, the same under +// every letter. +const GRAIN = new Float32Array(NL); +{ + const hs = (x, y) => { + let h = (x * 374761393 + y * 668265263) >>> 0; + h = Math.imul(h ^ (h >>> 13), 1274126177) >>> 0; + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; + }; + for (let p = 0; p < NL; p++) { + const fx = (p & 63) / 5, fy = (p >> 6) / 5, ix = floor(fx), iy = floor(fy), ax = fx - ix, ay = fy - iy; + const a = hs(ix, iy), b = hs(ix + 1, iy), c = hs(ix, iy + 1), d = hs(ix + 1, iy + 1); + GRAIN[p] = 0.75 + 0.5 * ((a + (b - a) * ax) * (1 - ay) + (c + (d - c) * ax) * ay); + } +} + +// The frame every sheet keeps: the square and its two diagonals. +const FRAME = new Float32Array(NL); +for (const g of [ruled([0, 0], [10, 0]), ruled([0, 0], [0, 10]), ruled([10, 0], [10, 10]), ruled([0, 10], [10, 10]), ruled([0, 0], [10, 10]), ruled([10, 0], [0, 10])]) { + const n = round((Math.hypot(g.b[0] - g.a[0], g.b[1] - g.a[1]) * UN) / 0.3); + for (let k = 0; k <= n; k++) splat(at(g, k / n), (q, wt) => (FRAME[q] = max(FRAME[q], wt))); +} + +// ---- Time, in seconds at Speed 2. A letter's turn is 8 s in one sheet: +// the old plate there goes dark in 0.8 s, from the paper's edge in to the +// letter; 4 s of construction; the ink runs 1.6 s; the paper lights in 1 s. +// Turns alternate between the sheets, so a finished plate stands through +// the other sheet's whole turn. A fresh start is 2.2 s into turn 1, the +// lower sheet, with turn 0's letter standing finished in the upper one, so +// at Speed 2 the ink and paper of the lower plate (2.6..5.2 s) and the +// darkening of the upper (5.8..6.6 s) both fall in the study's 2..8 s, and +// almost every LED that changes there changes once: the study measures +// drift 0.247 at motion 0.048, and motion over those 6 s adds up to only +// 1.17 times the drift. +const TURN = 8, ER = 0.8, CN = 4, F0 = 4.8, FN = 1.6, G0 = 6.4, GN = 1, START = TURN + 2.2; +const L_FRAME = 0.2, L_GUIDE = 0.42, L_PAPER = 0.17; + +export function setup(params) { + params.s = { tau: START, hue: 0, first: 0, w: 1, cache: {}, rgb: new Float32Array(W * H * 3) }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.1], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.tau += (d * cl(k[1], 0.2, 3)) / 2; + S.first = cl(round(k[2]), 0, 12); + const w = round(cl(k[3], 0.1, 0.2) * 400) / 40; + if (w !== S.w) (S.w = w), (S.cache = {}); +} + +function get(S, n) { + if (n < 0) return null; + const ch = SEQ[(S.first + n) % SEQ.length]; + return S.cache[ch] || (S.cache[ch] = letter(ch, S.w)); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const rgb = S.rgb, h = S.hue; + const ink = hsv(0.11 + h, 0.3, 1), line = hsv(0.5 + h, 0.55, 1), paper = hsv(0.62 + h, 0.75, 1), frame = hsv(0.56 + h, 0.35, 1), pen = hsv(0.13 + h, 0.15, 1); + rgb.fill(0); + const n = floor(S.tau / TURN); + for (let sh = 0; sh < 2; sh++) { + const oy = sh * SH * W; + // Which letter this sheet shows and how far along it is. + let L, ph, erase = 0; + if (n % 2 === sh) { + ph = S.tau - n * TURN; + if (ph < ER) (L = get(S, n - 2)), (erase = ph / ER), (ph = TURN); + else L = get(S, n); + } else (L = get(S, n - 1)), (ph = TURN); + const cp = (ph - ER) / CN, fp = (ph - F0) / FN, gp = (ph - G0) / GN; + for (let p = 0; p < NL; p++) { + const o = (oy + p) * 3; + let r = FRAME[p] * L_FRAME * frame[0], g = FRAME[p] * L_FRAME * frame[1], b = FRAME[p] * L_FRAME * frame[2]; + if (L) { + // Erasing goes from the paper's edge in: far paper first, the letter last. + const keep = erase ? 1 - cl((erase - (1 - L.gd[p]) * 0.6) / 0.4, 0, 1) : 1; + if (keep > 0) { + const gl = L.gw[p] ? L.gw[p] * cl((cp - L.gt[p]) * CN * 20, 0, 1) * L_GUIDE : 0; + const pa = inPaper(p) ? cl((gp - L.gd[p] * 0.75) / 0.25, 0, 1) * L_PAPER * GRAIN[p] : 0; + const a = L.cov[p] ? L.cov[p] * cl((fp - L.fill[p] * 0.85) / 0.15, 0, 1) : 0; + r = ((1 - a) * (r + gl * line[0] + pa * paper[0]) + a * ink[0]) * keep + r * (1 - keep); + g = ((1 - a) * (g + gl * line[1] + pa * paper[1]) + a * ink[1]) * keep + g * (1 - keep); + b = ((1 - a) * (b + gl * line[2] + pa * paper[2]) + a * ink[2]) * keep + b * (1 - keep); + } + } + rgb[o] = r, rgb[o + 1] = g, rgb[o + 2] = b; + } + // The pen, while the construction is being drawn: a bright point at the + // tip and, for an arc, the compass needle in its centre. + if (L && !erase && cp > 0 && cp < 1) { + const e = L.G.find((q) => cp >= q.t0 && cp < q.t1); + if (e) { + const s = (cp - e.t0) / (e.t1 - e.t0), [px, py] = at(e, s); + dot(rgb, px, py + sh * SH, 1, pen); + if (e.k === 1 && e.r > 1.5) dot(rgb, toX(e.c[0]), toX(e.c[1]) + sh * SH, 0.55, pen); + } + } + } + for (let p = 0; p < W * H; p++) + display.setPixel(p & 63, p >> 6, min(255, rgb[p * 3] * 255) | 0, min(255, rgb[p * 3 + 1] * 255) | 0, min(255, rgb[p * 3 + 2] * 255) | 0); +} + +// A point of light split over the four nearest LEDs, with a faint ring. +function dot(rgb, x, y, a, c) { + const fx = x - 0.5, fy = y - 0.5, ix = floor(fx), iy = floor(fy), ax = fx - ix, ay = fy - iy; + for (let j = -1; j <= 2; j++) + for (let i = -1; i <= 2; i++) { + const xx = ix + i, yy = iy + j; + if (xx < 0 || yy < 0 || xx >= W || yy >= H) continue; + const wx = i === 0 ? 1 - ax : i === 1 ? ax : 0, wy = j === 0 ? 1 - ay : j === 1 ? ay : 0; + const v = a * (wx * wy + 0.12); + const o = (yy * W + xx) * 3; + rgb[o] += c[0] * v, rgb[o + 1] += c[1] * v, rgb[o + 2] += c[2] * v; + } +} diff --git a/incoming/pattern-study/patterns/topologists-sine-curve.js b/incoming/pattern-study/patterns/topologists-sine-curve.js new file mode 100644 index 000000000..bbb89c1e5 --- /dev/null +++ b/incoming/pattern-study/patterns/topologists-sine-curve.js @@ -0,0 +1,222 @@ +// ===== Patternflow pattern ===== +// Title: Topologist's Sine Curve +// Family: topology +// Idea: The graph of sin(1/x) stands on the panel with x running down to the limit segment drawn across near the bottom, its swings packing tighter until they merge into a band that is bright at both edges and dim in the middle by the arcsine law, while a slow zoom toward x = 0 draws new swings out of the band and a bead walks down the curve, swinging faster and faster, and never reaches the segment. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: topologist's sine curve (Lynn Arthur Steen and J. Arthur Seebach Jr., Counterexamples in Topology, 1970), pattern round 73 +// Round: 73 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Zoom=0..1, Measure=0..1 +// +// Knob 1: Hue. Turns the colours round the wheel: at 0 the curve is aqua where it swings right on its way down and violet where it swings left, the band where the two can no longer be told apart is their periwinkle mix, the segment is pale gold and the bead leaves a gold trail. +// Knob 2: Speed. At 2 a walk takes about 9 s and the zoom breathes in and out once every 30 s; at 0.2 both are ten times slower, at 3 half as fast again. +// Knob 3: Zoom. How near x = 0 the top of the panel is: at 0 the panel shows x from 0.16 down to 0, at 1 from 0.005, where the band takes nearly the whole height. The slow zoom then goes four times nearer and back. +// Knob 4: Measure. What a LED's brightness counts. At 0 it is how much of its row's stretch of x the curve spends in that LED, which in the band is the arcsine law, bright edges and a dim middle. At 1 it is how much curve length passes through the LED, and the band fills flat. + +const W = 64, H = 128, N = W * H; +const { floor, ceil, min, max, sqrt, sin, cos, asin, exp, log, abs, 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 panel. The curve's x runs down the panel, x = X at the top edge +// and x = 0 on row 121 (Y = 121.5), where the limit segment {0} x [-1, 1] +// lies across. Its value sin(1/x) runs across: -1 at column 4.5, +1 at 59.5. +const YS = 121.5, CX = 32, AMP = 27.5; +const colOf = (s) => CX + AMP * s; + +// In the band a row holds so many swings that the share of it the curve +// spends in each column is the arcsine law: (asin s1 - asin s0) / pi for the +// column's stretch s0..s1. Column 4, which only half meets the curve, gets +// 0.061 of the row, 5.25 times the 0.0116 of a middle column, and column 5 +// gets 3.9 times. Across, every swing crosses each column twice. +const ARC = new Float32Array(W), OVL = new Float32Array(W); +for (let c = 0; c < W; c++) { + const s0 = cl((c - CX) / AMP, -1, 1), s1 = cl((c + 1 - CX) / AMP, -1, 1); + ARC[c] = (asin(s1) - asin(s0)) / PI; + OVL[c] = (s1 - s0) * AMP; +} + +// ---- One row, exactly. Between turning points, u = 1/x walks one way and +// sin u is monotone, with inverse u = k pi + asin((-1)^k s) on the k-th +// piece. Every crossing of a column boundary is then an exact x, so the +// share of the row's x in each column, and the chord length through it, are +// sums of differences: a box filter with no samples to alias. Odd pieces, +// where the curve swings left on its way down, are also kept apart in L. +function rowExact(j, ulo, uhi, span, yScale, M, L, A) { + const k0 = floor(ulo / PI + 0.5), k1 = floor(uhi / PI + 0.5), o = j * W; + for (let k = k0; k <= k1; k++) { + const ua = max(ulo, (k - 0.5) * PI), ub = min(uhi, (k + 0.5) * PI); + if (ub <= ua) continue; + const sg = k & 1 ? -1 : 1, cb = colOf(sin(ub)), dir = cb >= colOf(sin(ua)) ? 1 : -1; + let p = colOf(sin(ua)), xp = 1 / ua; + for (let guard = 0; guard < 70; guard++) { + let q = dir > 0 ? floor(p) + 1 : ceil(p) - 1, last = false; + if (dir > 0 ? q >= cb : q <= cb) (q = cb), (last = true); + const xq = last ? 1 / ub : 1 / (k * PI + asin(cl((sg * (q - CX)) / AMP, -1, 1))); + const c = floor((p + q) / 2), dx = abs(xp - xq); + if (c >= 0 && c < W) { + M[o + c] += dx / span; + if (sg < 0) L[o + c] += dx / span; + A[o + c] += sqrt((q - p) ** 2 + (dx * yScale) ** 2); + } + if (last) break; + (p = q), (xp = xq); + } + } +} + +export function setup(params) { + params.s = { + t: 0, tz: 0, walk: 0, hue: 0, zoom: 0, meas: 0.06, X: 0.16, dt: 0, prevU: 0, + M: new Float32Array(N), L: new Float32Array(N), A: new Float32Array(N), T: new Float32Array(N), tmp: new Float32Array(N), F: new Float32Array(N * 3), + C: [0, 1, 2].map(() => new Float32Array(N)), O: [0, 1, 2].map(() => new Float32Array(N)), + }; +} + +// ---- Time, in seconds at Speed 2. The bead's share of the way from the +// segment, q = x / X, is (1 - t / 8)^1.25: it comes down the panel at a +// walker's pace, 19 rows a second at first and slower near the end, and +// would touch the segment at 8 s. It cannot: there sin(1/x) has no limit, +// so its swings come without bound, past the frame rate for the last second +// or so, and the walk stops 0.4 LED short, in the band. A new one starts 0.8 s +// later. The zoom divides X by up to 4 and back over 30 s. +const TW = 8, PW = 1.25, Q_END = 0.4 / YS, PAUSE = 0.8, TZ = 30; +const WALK = TW * (1 - Q_END ** (1 / PW)); + +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); + const e = (d * cl(k[1], 0.2, 3)) / 2; + S.hue = k[0]; + S.zoom = cl(k[2], 0, 1); + S.meas = cl(k[3], 0, 1); + S.tz += e; + S.walk += e; + S.dt = e; + if (S.walk > WALK + PAUSE) (S.walk -= WALK + PAUSE), (S.prevU = 0); + S.X = 0.16 * 10 ** (-1.5 * S.zoom) * exp(-log(4) * 0.5 * (1 - cos((2 * PI * S.tz) / TZ))); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { M, L, A, T, tmp, F, C, O } = S, [CR, CG, CB] = C, X = S.X, span = X / YS, yScale = YS / X; + M.fill(0), L.fill(0), A.fill(0); + // ---- The curve, row by row down to the segment's row. + for (let j = 0; j <= 121; j++) { + const xb = X * (1 - j / YS), xa = max(0, X * (1 - (j + 1) / YS)); + const ulo = 1 / xb, uhi = xa > 0 ? 1 / xa : Infinity; + if (uhi - ulo < 10 * PI) rowExact(j, ulo, uhi, span, yScale, M, L, A); + else { + // More than five whole swings: the arcsine law, half of it swinging + // each way, and the chord length of two crossings a swing. + const f = (xb - xa) / span, sw = min(40, (uhi - ulo) / (2 * PI)), o = j * W; + for (let c = 4; c < 60; c++) (M[o + c] += f * ARC[c]), (L[o + c] += 0.5 * f * ARC[c]), (A[o + c] += 2 * sw * OVL[c]); + } + } + // ---- Colour. Brightness from the measure: a share of 1/22 of the row + // reads 0.63. The curve swings right on its way down in aqua and left in + // violet, so a visible swing shows both, and in the band, where each row + // holds as much of one as of the other, they mix to periwinkle. Both hues + // drift by depth, ln(0.16 / x), so the colour travels with the zoom. + // The bright edges belong to the measure of x, not to the curve's length: + // counted by length (Measure 1), every swing crosses each column twice, + // nearly level, and a band row is the same all across. Checked on row 116 + // of the opening view: 255 at the edges and 97 in the middle at Measure 0, + // 255 all across at Measure 1. + const m = S.meas, h0 = S.hue; + for (let y = 0; y < H; y++) { + const dh = 0.03 * cl(log(0.16 / (X * max(1e-6, 1 - (y + 0.5) / YS))), -2, 6); + const ca = hsv(0.47 + h0 + dh, 0.85, 1), cv = hsv(0.71 + h0 + dh, 0.7, 1); + for (let x = 0, p = y * W; x < W; x++, p++) { + if (!(M[p] > 0)) { + CR[p] = CG[p] = CB[p] = 0; + continue; + } + const v = (1 - m) * (1 - exp(-22 * M[p])) + m * (1 - exp(-1.3 * A[p])), l = L[p] / M[p]; + CR[p] = v * (ca[0] + (cv[0] - ca[0]) * l); + CG[p] = v * (ca[1] + (cv[1] - ca[1]) * l); + CB[p] = v * (ca[2] + (cv[2] - ca[2]) * l); + } + } + // Glow: a 5-tap blur across and down of each channel, added at 0.7. + for (let i = 0; i < 3; i++) blur(C[i], tmp, O[i]); + + // ---- The bead. Its path since the last frame is sampled in u, every 0.1 + // rad at most, into a trail that fades in 0.3 s; once it swings faster + // than the frame, the samples spread across the row, a smear as wide as + // the band. + const w = S.walk, fade = exp(-S.dt / 0.3); + for (let p = 0; p < N; p++) T[p] *= fade; + let head = null; + if (w < WALK) { + const q = (1 - w / TW) ** PW, u = 1 / (q * X), u0 = S.prevU > 0 && S.prevU < u ? S.prevU : u - 1e-3; + const n = min(400, max(2, ceil((u - u0) / 0.1))); + for (let i = 1; i <= n; i++) { + const ui = u0 + ((u - u0) * i) / n; + dep(T, colOf(sin(ui)), YS * (1 - 1 / (ui * X)), 1.6 / n + 0.02); + } + S.prevU = u; + head = [colOf(sin(u)), YS * (1 - q)]; + } + // The trail is added on top in gold. + const seg = hsv(0.13 + h0, 0.35, 1), gold = hsv(0.12 + h0, 0.6, 1), [OR, OG, OB] = O; + for (let p = 0; p < N; p++) { + const tr = min(1, T[p]); + F[p * 3] = min(1, CR[p] + 0.7 * OR[p] + gold[0] * tr); + F[p * 3 + 1] = min(1, CG[p] + 0.7 * OG[p] + gold[1] * tr); + F[p * 3 + 2] = min(1, CB[p] + 0.7 * OB[p] + gold[2] * tr); + } + // ---- The limit segment on row 121, closed at both ends, and a faint + // glow on the rows either side. + for (let x = 4; x < 60; x++) { + const end = x === 4 || x === 59; + for (const [dy, a] of [[-1, 0.18], [0, end ? 1 : 0.85], [1, 0.25], [2, 0.08]]) add(F, x, 121 + dy, seg, a); + } + // The bead itself: white, with a soft halo 1.5 LEDs across. + if (head) + for (let j = -3; j <= 3; j++) + for (let i = -3; i <= 3; i++) { + const x = floor(head[0]) + i, y = floor(head[1]) + j, dx = x + 0.5 - head[0], dy = y + 0.5 - head[1]; + add(F, x, y, [1, 0.97, 0.9], 1.3 * exp(-(dx * dx + dy * dy) / 1.1)); + } + for (let p = 0; p < N; p++) display.setPixel(p % W, floor(p / W), (F[p * 3] * 255) | 0, (F[p * 3 + 1] * 255) | 0, (F[p * 3 + 2] * 255) | 0); +} + +// Light added on top of the frame buffer, held to 1. +function add(F, x, y, c, a) { + if (x < 0 || y < 0 || x >= W || y >= H) return; + const o = (y * W + x) * 3; + (F[o] = min(1, F[o] + c[0] * a)), (F[o + 1] = min(1, F[o + 1] + c[1] * a)), (F[o + 2] = min(1, F[o + 2] + c[2] * a)); +} + +// A point of the bead's path, split over its four nearest LEDs. +function dep(T, x, y, a) { + const fx = x - 0.5, fy = y - 0.5, ix = floor(fx), iy = floor(fy), ax = fx - ix, ay = fy - iy; + for (let j = 0; j < 2; j++) + for (let i = 0; i < 2; i++) { + const xx = ix + i, yy = iy + j; + if (xx >= 0 && yy >= 0 && xx < W && yy < H) T[yy * W + xx] += a * (i ? ax : 1 - ax) * (j ? ay : 1 - ay); + } +} + +function blur(src, tmp, out) { + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + tmp[p] = (6 * src[p] + 4 * ((x > 0 ? src[p - 1] : 0) + (x < W - 1 ? src[p + 1] : 0)) + (x > 1 ? src[p - 2] : 0) + (x < W - 2 ? src[p + 2] : 0)) / 16; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + out[p] = (6 * tmp[p] + 4 * ((y > 0 ? tmp[p - W] : 0) + (y < H - 1 ? tmp[p + W] : 0)) + (y > 1 ? tmp[p - 2 * W] : 0) + (y < H - 2 ? tmp[p + 2 * W] : 0)) / 16; + } +} From 0f18305f05abb602afe26f8d23f83947902ce02c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 22:24:26 +0000 Subject: [PATCH 8/8] =?UTF-8?q?73=ED=8C=90=20=EC=97=B0=EA=B5=AC=20?= =?UTF-8?q?=EB=85=B8=ED=8A=B8=EB=A5=BC=20README=20=EC=97=90=20=EC=A0=81?= =?UTF-8?q?=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6N31ff3CzvqRufZu6W --- incoming/pattern-study/README.md | 105 +++++++++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 5 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 6eb57138e..0d9b1d4c4 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 618개를 쉰여섯 갈래로 나눠 만들었어요. 1판에서 열세 갈래로 23개를 만들고, 2판에서 1판에 없던 아홉 갈래로 16개를, 3판에서 하나를, 4판에서 새 갈래인 직물로 하나를 더했어요. 5판은 새 패턴 없이 1판의 Euclid Pulse 를 다시 짰고, 6판은 세운 패널의 128줄을 위아래로 다 쓰는 패턴 하나를 더했어요. 7판은 또렷한 모양이 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 8판은 새 갈래인 기계로 하나를 더했어요. 9판은 새 패턴 없이 1판의 Sprout 을 다시 짰어요. 10판은 세운 패널을 떨어지는 블록의 우물로 쓰는 패턴 하나를 더했어요. 11판은 여러 색이 꽤 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 12판은 새 갈래인 천체로 하나를 더했어요. 13판은 새 패턴 없이 1판의 Glyph Rain 을 다시 짰어요. 14판은 절차가 바뀌기 전에 띄운 바퀴라서, 세운 패널의 높이를 다 떨어지는 폭포 하나를 더했어요. 15판은 바뀐 절차의 첫 판이라 열 갈래에 새 패턴을 하나씩, 모두 열 개를 더했어요. 도시와 음악 갈래는 그때 새로 열었어요. 16판도 열 갈래에 하나씩 열 개를 더했고, 바다와 곤충 갈래를 새로 열었어요. 17판도 열 갈래에 하나씩 열 개를 더했고, 전자 회로와 장난감 갈래를 새로 열었어요. 18판도 열 갈래에 하나씩 열 개를 더했고, 유리와 광물·결정, 놀이공원 갈래를 새로 열었어요. 19판도 열 갈래에 하나씩 열 개를 더했고, 부엌과 버섯·균사 갈래를 새로 열었어요. 20판도 열 갈래에 하나씩 열 개를 더했고, 새와 화학·실험 갈래를 새로 열었어요. 21판도 열 갈래에 하나씩 열 개를 더했고, 종이와 운동·경기 갈래를 새로 열었어요. 22판도 열 갈래에 하나씩 열 개를 더했고, 몸과 비행 갈래를 새로 열었어요. 23판도 열 갈래에 하나씩 열 개를 더했고, 무대·공연과 짐승 갈래를 새로 열었어요. 24판도 열 갈래에 하나씩 열 개를 더했고, 공사·건설과 도자기 갈래를 새로 열었어요. 25판도 열 갈래에 하나씩 열 개를 더했고, 농사·밭과 화산 갈래를 새로 열었어요. 26판도 열 갈래에 하나씩 열 개를 더했고, 얼음·극지와 미생물 갈래를 새로 열었어요. 27판도 열 갈래에 하나씩 열 개를 더했고, 철도와 필름·영화 갈래를 새로 열었어요. 28판은 주제 표가 수학 쪽으로 바뀐 첫 판이라 이름 있는 수학 대상 열 개를 열 갈래에 하나씩 더했고, 수론 갈래를 새로 열었어요. 29판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 첫 판이라, 이름 있는 수학 대상 여섯과 화장품 매장 벽에 틀 질감 넷을 더했고, 위상·매듭과 화장품 팝업 갈래를 새로 열었어요. 30판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 매장의 LED 벽에 틀 질감 넷을 화장품 팝업 갈래에 더했어요. 31판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 열지 않았어요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽의 질감 넷을 화장품 팝업 갈래에 더했어요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 41판은 주제 표가 수학·추상 다섯과 할로윈 다섯으로 바뀐 첫 판이라, 수학 대상 다섯을 다섯 갈래에 하나씩 더하고 할로윈 갈래를 새로 열어 10월 31일에 틀 패턴 다섯을 넣었어요. 42판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 41판이 연 할로윈 갈래에 더했어요. 43판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 44판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 45판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 열 개 아래인 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 46판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 47판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 48판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 49판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 50판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 51판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 52판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 53판은 할로윈 몫이 쉬게 된 첫 판이라, 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 54판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 55판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 56판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 57판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 58판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 59판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 60판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 61판도 수학 대상 열 개를 패턴이 적은 갈래부터 열 갈래에 하나씩 더했어요. 62판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 63판은 64판과 같이 주제 표에 「타이포」가 들어온 첫 판이라, 글자가 들어간 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 64판은 주제 표가 바뀌어 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 네 갈래에 더했어요. 65판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 66판도 같은 표로 돌아서 글자 패턴 넷을 글자 갈래에, 하나를 데이터 그림 갈래에 더하고, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 67판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 68판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 69판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 70판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 71판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 628개를 쉰여섯 갈래로 나눠 만들었어요. 1판에서 열세 갈래로 23개를 만들고, 2판에서 1판에 없던 아홉 갈래로 16개를, 3판에서 하나를, 4판에서 새 갈래인 직물로 하나를 더했어요. 5판은 새 패턴 없이 1판의 Euclid Pulse 를 다시 짰고, 6판은 세운 패널의 128줄을 위아래로 다 쓰는 패턴 하나를 더했어요. 7판은 또렷한 모양이 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 8판은 새 갈래인 기계로 하나를 더했어요. 9판은 새 패턴 없이 1판의 Sprout 을 다시 짰어요. 10판은 세운 패널을 떨어지는 블록의 우물로 쓰는 패턴 하나를 더했어요. 11판은 여러 색이 꽤 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 12판은 새 갈래인 천체로 하나를 더했어요. 13판은 새 패턴 없이 1판의 Glyph Rain 을 다시 짰어요. 14판은 절차가 바뀌기 전에 띄운 바퀴라서, 세운 패널의 높이를 다 떨어지는 폭포 하나를 더했어요. 15판은 바뀐 절차의 첫 판이라 열 갈래에 새 패턴을 하나씩, 모두 열 개를 더했어요. 도시와 음악 갈래는 그때 새로 열었어요. 16판도 열 갈래에 하나씩 열 개를 더했고, 바다와 곤충 갈래를 새로 열었어요. 17판도 열 갈래에 하나씩 열 개를 더했고, 전자 회로와 장난감 갈래를 새로 열었어요. 18판도 열 갈래에 하나씩 열 개를 더했고, 유리와 광물·결정, 놀이공원 갈래를 새로 열었어요. 19판도 열 갈래에 하나씩 열 개를 더했고, 부엌과 버섯·균사 갈래를 새로 열었어요. 20판도 열 갈래에 하나씩 열 개를 더했고, 새와 화학·실험 갈래를 새로 열었어요. 21판도 열 갈래에 하나씩 열 개를 더했고, 종이와 운동·경기 갈래를 새로 열었어요. 22판도 열 갈래에 하나씩 열 개를 더했고, 몸과 비행 갈래를 새로 열었어요. 23판도 열 갈래에 하나씩 열 개를 더했고, 무대·공연과 짐승 갈래를 새로 열었어요. 24판도 열 갈래에 하나씩 열 개를 더했고, 공사·건설과 도자기 갈래를 새로 열었어요. 25판도 열 갈래에 하나씩 열 개를 더했고, 농사·밭과 화산 갈래를 새로 열었어요. 26판도 열 갈래에 하나씩 열 개를 더했고, 얼음·극지와 미생물 갈래를 새로 열었어요. 27판도 열 갈래에 하나씩 열 개를 더했고, 철도와 필름·영화 갈래를 새로 열었어요. 28판은 주제 표가 수학 쪽으로 바뀐 첫 판이라 이름 있는 수학 대상 열 개를 열 갈래에 하나씩 더했고, 수론 갈래를 새로 열었어요. 29판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 첫 판이라, 이름 있는 수학 대상 여섯과 화장품 매장 벽에 틀 질감 넷을 더했고, 위상·매듭과 화장품 팝업 갈래를 새로 열었어요. 30판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 매장의 LED 벽에 틀 질감 넷을 화장품 팝업 갈래에 더했어요. 31판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 열지 않았어요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽의 질감 넷을 화장품 팝업 갈래에 더했어요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 41판은 주제 표가 수학·추상 다섯과 할로윈 다섯으로 바뀐 첫 판이라, 수학 대상 다섯을 다섯 갈래에 하나씩 더하고 할로윈 갈래를 새로 열어 10월 31일에 틀 패턴 다섯을 넣었어요. 42판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 41판이 연 할로윈 갈래에 더했어요. 43판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 44판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 45판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 열 개 아래인 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 46판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 47판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 48판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 49판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 50판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 51판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 52판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 53판은 할로윈 몫이 쉬게 된 첫 판이라, 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 54판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 55판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 56판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 57판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 58판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 59판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 60판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 61판도 수학 대상 열 개를 패턴이 적은 갈래부터 열 갈래에 하나씩 더했어요. 62판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 63판은 64판과 같이 주제 표에 「타이포」가 들어온 첫 판이라, 글자가 들어간 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 64판은 주제 표가 바뀌어 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 네 갈래에 더했어요. 65판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 66판도 같은 표로 돌아서 글자 패턴 넷을 글자 갈래에, 하나를 데이터 그림 갈래에 더하고, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 67판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 68판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 69판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 70판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 71판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 73판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. [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판에서 세로로 다시 짰어요. @@ -975,6 +975,21 @@ | [Tensegrity](patterns/tensegrity.js) | 물리·스프링 | 서로 닿지 않는 막대 15개가 팽팽한 케이블 그물에 걸려 패널 높이의 탑으로 서서 16초에 한 바퀴 천천히 돌고, Load 를 올리면 꼭대기의 추가 탑을 17% 짧게 누르며 위쪽을 74도 더 비틀어요. | | [Problem of Apollonius](patterns/apollonius-problem.js) | 기하·대칭 | 세 흰 원이 패널 위를 떠다니는 동안 셋 모두에 접하는 원 여덟 개가 안팎 접촉에 따라 제 색으로 그려지고 접점마다 밝은 점이 찍히다가, 흰 원 둘이 겹치면 넷이 짝지어 합쳐지며 사라졌다 떨어지면 돌아와요. | +### 73판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Abracadabra](patterns/abracadabra.js) | 글자 | ABRACADABRA 열한 줄이 한 글자씩 줄며 128줄을 내려가고, 열이 위에서 아래로 지나가며 줄마다 빠지는 한 글자를 태워 재로 부숴요. | +| [Multiple Master](patterns/multiple-master.js) | 글자 | 굵기와 폭의 끝에 둔 마스터 넷의 점을 섞어 MASTER 를 짓고, 아래 설계 공간 정사각형에서 그 자리가 노브 둘레를 천천히 떠돌아요. | +| [Dürer's Alphabet](patterns/durer-alphabet.js) | 글자 | 위아래 두 정사각형에서 번갈아 펜이 획 가장자리를 자로 긋고 둥근 획과 세리프 받침을 컴퍼스로 돌리면, 상아빛 잉크가 로마 대문자의 몸을 채우고 둘레의 종이가 밝아져요. | +| [Damask](patterns/damask.js) | 글자 | 한 가지 실로 LED 하나에 한 교차씩 짠 천에 큰 낱말이 경수자로 들어 있어서, 낮게 도는 램프를 따라 글자가 바탕보다 밝았다가 어두워져요. | +| [Maya Numerals](patterns/maya-numerals.js) | 글자 | 다섯 자리 마야 숫자가 맨 아래 자리부터 하나씩 늘면서 점 다섯이 모여 막대 하나로 녹고, 다 찬 자리는 금빛 점 하나로 오그라들어 윗자리로 올라가며 조개 껍데기 0 을 남겨요. | +| [Airy Beam](patterns/airy-beam.js) | 물결·간섭 | 유한 에너지 에어리 빔이 위에서 들어와 주 로브와 곁 로브가 함께 포물선으로 휘며 내려가고, 위쪽에서 오가는 작은 구슬이 가린 로브는 아래에서 다시 자라요. | +| [Topologist's Sine Curve](patterns/topologists-sine-curve.js) | 위상·매듭 | sin(1/x) 의 흔들림이 아래 극한 선분으로 갈수록 촘촘해져 가장자리가 밝은 띠로 합쳐지고, 곡선을 따라 내려가는 구슬은 점점 빨리 흔들릴 뿐 선분에 닿지 못해요. | +| [Ehrenfest Urn](patterns/ehrenfest-urn.js) | 입자 | 위아래로 쌓은 두 방의 공 N 개 가운데 하나가 걸음마다 좁은 목을 지나 건너가고, 아래 띠가 위 방의 공 수가 N 에서 N/2 로 내려와 이항분포의 폭 안에서 떠도는 것을 그려요. | +| [Steiner Deltoid](patterns/steiner-deltoid.js) | 선·궤적 | 외접원을 도는 점의 심슨 선이 패널 높이를 가로지르고, 그 자취가 쌓여 삼각형 세 변에 붙은 세 뿔 델토이드가 떠올라요. | +| [Voss Dice](patterns/voss-dice.js) | 노이즈·흐름 | 이진 카운터의 비트가 바뀐 주사위만 다시 굴려 1/f 잡음을 만들고, 그 합을 백색 잡음과 갈색 잡음 사이에 나란히 흘려 내려요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -3977,6 +3992,70 @@ Lab 에서 해 보기: Text 레이어에 Silkscreen 으로 「lorem」을 size 1 주제 5 「수학 · 추상」으로, 세 원에 모두 접하는 원을 찾는 문제를 움직이는 그림으로 겨눴어요. 페르가의 아폴로니오스(Apollonius of Perga)가 기원전 200년쯤 쓴 『접촉(Tangencies)』은 사라져 파푸스(Pappus)의 요약으로만 전해지고, 프랑수아 비에트(François Viète)가 1600년 『Apollonius Gallus』에서 풀이를 다시 세웠어요. 코드는 (x − xi)² + (y − yi)² = (r + si·ri)² 세 식에서 첫 식을 빼 x, y, r 에 대해 일차인 두 식을 얻고, 그 가운데 둘을 남은 하나의 일차식으로 풀어 첫 식에 넣은 이차방정식을 풀어요. 부호 셋과 그 반대는 같은 식이라 네 식이 근 둘씩 원 여덟을 내고, 세 중심이 한 줄에 가까워도 깨지지 않게 2×2 행렬식이 가장 큰 쪽을 골라요. 흰 원 셋은 반지름 5.6~16 LED 로 저마다 제 띠 안에서 37~71초 주기의 사인을 따라 떠다니고, 해 원은 세 원 가운데 어느 것과 안쪽으로 접하는지에 따라 여덟 빛깔을 받으며 반대 서명은 색상환의 맞은편에 서요. 네 크기에서 30분씩 돌리며 찾은 원과 세 원의 접촉 오차를 쟀더니 가장 나쁜 것이 3.7e-9 LED 였고, 접점은 두 테두리에서 4e-11 LED 안에 있었어요. 한 식이 낸 두 원의 중심은 흰 원 셋의 근심(radical centre)과 2e-12 라디안 안에서 한 줄에 섰는데, 그 줄 위에서 r = 0 인 점이 바로 근심이기 때문이에요. 여는 크기에서 여덟이 다 있는 때는 60.7%, 넷은 36.0%, 하나도 없는 때는 3.2% 였고, 무작위로 고른 세 원 40만 벌을 풀어도 0, 4, 8 말고는 나오지 않았어요. 겹치는 쌍이 하나나 둘이면 넷이 한꺼번에 짝지어 합쳐지며 사라지고, 세 쌍이 다 겹치면 여덟이 다시 서요. 잰 값은 움직임 0.886, 촘촘함 0.095, 가장자리 0.772, 복잡도 0.745, 채움 0.200, 색 0.190, 모양 바뀜 0.124 이고 thirds 는 14/27/18, 노브는 0.06, 0.14, 0.07, 0.15 예요. 새로 본 것은 여덟이 늘 서명 하나씩은 아니라는 거예요. 세 중심이 한 줄에 가까우면 한 식의 두 근이 다 양수가 되어 같은 색 원 둘이 나오고 반대 색은 없어서, 여덟이 있는 프레임 가운데 서명 여덟이 다 있는 것은 40% 뿐이었어요. +## 73판 + +73판은 72판(#335)과 같이 돌았어요. 앞 바퀴 PR 에는 OWNER, MEMBER, COLLABORATOR 가 쓴 댓글도 리뷰도 없어서 따를 글이 없었어요. 72판이 PR 본문에 먼저 밝힌 열 개와 지도 칸 둘을 읽고 겹치지 않게 골랐어요. 타이포 다섯은 「타이포 실험」의 출발점을 하나씩 맡았어요. Abracadabra 는 세로 패널다운 글, Multiple Master 는 노브와 글, Dürer's Alphabet 은 글자가 만들어지는 과정, Damask 는 글자로 짠 무늬와 글자와 빛, Maya Numerals 는 숫자와 기호 놀이예요. 글자 꼴도 다섯이 다 달라요. Abracadabra 는 다섯 글자를 4×9 비트맵으로 그렸고, Multiple Master 는 여섯 자를 점 수가 같은 네 마스터의 다각형으로 적었어요. Dürer's Alphabet 은 열세 자를 볼록 다각형과 컴퍼스 원으로 적어 LED 하나를 4×4 로 표본하고, Damask 는 대문자 다섯과 자모 다섯을 획 목록으로 적어 거리로 굵기를 맞췄어요. Maya Numerals 는 점과 막대와 조개 껍데기를 빛이 드는 구슬처럼 그렸어요. 71판이 「다음 판 감」에 적은 ABRACADABRA 삼각형과 뒤러의 『측정 지침』, 70판이 골랐다가 짜지 않은 멀티플 마스터를 이번에 해 봤어요. 수학 다섯은 72판까지 든 628개에 없던 대상이고 「낯선 수학 예」에서는 고르지 않았어요. 갈래는 스물둘로 가장 적은 추상 갈래 열 가운데 바로 앞 71판이 쓴 프랙탈과 공간·원근이 아닌 물결·간섭, 위상·매듭, 입자, 선·궤적, 노이즈·흐름에 하나씩 넣었어요. 주제 1 은 넷뿐인 칸 스물셋 가운데 72판이 고른 움직임 × 색 1,4 와 가장자리 × 색 1,4 를 빼고 촘촘함 × 채움 4,1 과 움직임 × 모양 바뀜 1,3 을 겨눴어요. 주제 4 는 Abracadabra 와 Airy Beam 이에요. + +### Abracadabra + +주제 7 「타이포」의 「세로 패널다운 글」과 주제 4 「세로 64×128」을 같이 채우는 패턴이에요. 퀸투스 세레누스 사모니쿠스(Quintus Serenus Sammonicus)의 라틴어 의학 시 『Liber Medicinalis』는 흔히 3세기 초로 잡는데, 열병을 떨구려면 ABRACADABRA 를 종이에 쓰고 그 아래에 거듭 쓰되 줄마다 한 글자씩 빼서 글자가 좁은 원뿔이 되게 한 뒤 목에 걸라고 적었어요. 64 칸에 열한 글자가 들어야 해서 A, B, R, C, D 다섯 글자를 4×9 LED 비트맵으로 그려 5 LED 간격으로 놓았고, 열한 줄을 128/11 = 11.64 줄 간격으로 세워 1행부터 126행까지 써요. 글자는 늘 제 칸에 서서 j 번째 칸은 어느 줄에서나 낱말의 j 번째 글자이고, Form 노브는 줄마다 어느 칸을 빼는지만 바꿔요. 0 은 끝 글자를 빼는 계단이라 왼쪽 칸이 A 열한 개로 읽히고, 1 은 첫 글자를 빼는 거울 계단, 2 는 끝과 처음을 번갈아 빼서 가운데 A 로 끝나는 원뿔이에요. 낱말의 첫째, 여섯째, 열한째 글자가 모두 A 라서 세 꼴이 다 A 하나로 끝나요. 열은 Speed 2 에서 한 줄을 0.6초에 지나며 위에서 내려와, 지나간 줄을 분홍으로 달궈 다시 쓰고 한 박자쯤 뒤에 청록 잉크로 식혀요. 줄 사이 두세 줄의 틈에서는 아직 열을 나르는 칸마다 하얀 방울이 떨어지고, 방울 줄은 한 줄 내려갈 때마다 하나씩 줄어요. 그 줄이 빼는 글자는 호박색으로 0.4박자 하얗게 타고 한 박자 깜박이다가 2.4박자에 잿빛이 되고, LED 마다 제 때에 한 번 반짝이며 부서져 32/255 의 그을음만 남아요. 계단에서는 A R B A D A C A R B 차례로 타서 낱말이 거꾸로 지워지고, 남은 첫 A 의 방울은 패널 발치 밖으로 떨어지며 바닥에 번져요. 한 바퀴는 14박자, 8.4초예요. 잉크는 한 바퀴 동안 100% 에서 64% 로 바래서 방금 쓴 줄과 열이 오기 전 줄이 갈려요. 잰 값은 움직임 0.048, 촘촘함 0.106, 가장자리 0.858, 복잡도 0.312, 채움 0.197, 색 0.119, 모양 바뀜 0.054 이고, thirds 는 28/18/11, 노브는 0.04, 0.05, 0.08, 0.05 예요. 첫 판은 재가 다 부서진 뒤 아주 꺼져서 thirds 가 26/16/8 이었고, 그을음을 남기자 28/18/11 이 됐어요. 방울을 글자 안에도 그렸더니 글자가 점박이로 보여서 줄 사이에만 그렸어요. 그러자 Speed 반응이 0.03 으로 내려가서, 뺀 글자를 더 오래 태우고 잉크가 바래게 해 0.046 으로 올렸어요. 새로 본 것은 계단 옆에 남은 그을음이에요. 발치에서 위로 읽으면 B R A C A D A B R A 라서 빠진 글자 열 개가 첫 A 만 빼고 주문을 한 번 더 쓰고, 그 첫 A 는 끝까지 빠지지 않고 맨 아래 줄에 홀로 서는 글자예요. + +Lab 에서 해 보기: Text 레이어에 FEBRIS 를 Silkscreen 크기 8, sharp 로 쳐서 오른쪽 맞춤 x 62, y 112 에 두고 blend add 로 얹은 뒤 twinkle 효과를 K2 에 걸면, Form 0 의 계단이 비워 둔 오른쪽 아래(x 30~63, 80~127행)에 라틴어로 열이라는 낱말이 불씨처럼 깜박여요. Form 을 1 로 돌리면 계단이 오른쪽으로 붙어 글자와 겹치니 x 2 왼쪽 맞춤으로 옮겨요. + +기능 감: Text 레이어의 줄 간격이요. 한 줄의 높이가 글자 크기와 같아서(textRaster.ts 가 줄을 top + index × size 에 놓아요) Silkscreen 을 크기 8 로 두면 열한 줄이 88줄만 쓰고, 128줄에 고루 펴려면 크기가 11.64 여야 해요. 크기와 따로 줄 간격을 정하는 칸이 있으면 이 주문의 열한 줄을 Text 레이어 하나로 세우고, 코드 레이어는 열과 재만 그리면 돼요. + +### Multiple Master + +주제 7 「타이포」의 「노브와 글」로, 노브를 돌리는 손이 글자를 굵히고 넓히는 것을 겨눴어요. 어도비(Adobe)는 1991년에 PostScript Type 1 을 넓혀 멀티플 마스터(Multiple Master) 글꼴을 냈고, 첫 글꼴 Myriad MM 과 Minion MM 은 1992년에 나왔어요. 한 글꼴을 굵기와 폭 같은 설계 축의 끝에 놓인 마스터 윤곽으로 저장하고, 마스터마다 점의 수와 차례를 같게 맞춰 그 사이의 어느 인스턴스든 점 좌표를 섞어 만드는 방식이에요. 패턴은 M, A, S, T, E, R 여섯 자를 글자마다 한 함수로 네 마스터(가는 좁은꼴, 가는 넓은꼴, 굵은 좁은꼴, 굵은 넓은꼴)에서 그려 두고, 처음에 네 마스터의 윤곽 수와 점 수가 같은지 맞춰 보고 다르면 멈춰요. 매 프레임 굵기 u 와 폭 v 로 (1−u)(1−v), (1−u)v, u(1−v), uv 를 점마다 곱해 더하고, 그 다각형을 LED 한 줄을 다섯 줄로 나눠 줄마다 가로 범위를 정확히 셈한 덮임 값으로 채워요. 낱말은 MAS 와 TER 두 줄이고 대문자 높이는 22 LED 예요. 굵은 넓은꼴의 MAS 2.65 캡에 기울임 0.14 를 더한 너비가 62 LED 에 들게 정한 값이에요. 줄기는 가는 쪽에서 1.6 LED, 굵은 좁은꼴에서 4.1, 굵은 넓은꼴에서 5.6 LED 예요. 윗줄의 먹 너비는 가는 좁은꼴 29 LED 에서 가는 넓은꼴 55, 굵은 넓은꼴 58 LED 까지 바뀌어요. 굵어질수록 A 의 가로획이 내려가 그 위 카운터가 맨 나중에 닫히고, M 의 꼭짓점은 굵은 좁은꼴에서 기준선 위로 3.3 LED 떠요. 아래에는 폭을 가로, 굵기를 세로로 둔 32 LED 정사각형이 있어요. 네 모서리 밖에는 한 글자의 네 마스터가 11 LED 높이로 서서 Speed 2 에서 3초마다 다음 글자로 바뀌고, 마스터마다 인스턴스에 섞인 몫만큼 밝아요. 축 이름은 OpenType 가변 글꼴(2016)의 축 태그대로 wdth 와 wght 로 적었어요. 인스턴스는 노브 자리를 중심으로 축의 0.24 까지 사인 넷으로 떠돌아서 노브를 놓아도 낱말이 숨 쉬고, K3 을 누르면 8도 기울임 축이 켜져요. 잰 값은 움직임 0.023, 촘촘함 0.065, 가장자리 0.735, 복잡도 0.321, 채움 0.236, 색 0.135, 모양 바뀜 0.036 이고, thirds 는 33/20/16, 노브는 0.05, 0.05, 0.08, 0.12 예요. 첫 판은 떠도는 폭이 0.17 이라 움직임이 0.015, Hue 와 Speed 가 0.04 였어요. 폭을 0.24 로 넓히고 낱말 색의 채도를 0.3 에서 0.4 로 올리자 0.023 과 0.05 가 됐어요. 새로 본 것은 Width 의 반응 0.12 가 Weight 의 0.08 보다 크다는 거예요. 낱말의 먹은 굵기 축에서 417 LED 에서 964 LED 로 131% 늘고 폭 축에서는 545 LED 로 31% 느는데도 그래요. 폭은 줄을 가운데에 둔 채 늘여서 윗줄 두 끝이 각각 12.5 LED 씩 옮겨 가고, 굵기는 획 가장자리만 제자리에서 두껍게 해요. + +Lab 에서 해 보기: Text 레이어에 Silkscreen 으로 MM 을 size 8, sharp, color #ffc040 으로 쳐서 오른쪽 맞춤 x 62, y 95 에 두고 blend 를 add 로 하면, 정사각형 오른쪽의 빈자리(x 51~63, y 82~108)에 멀티플 마스터 글꼴 이름 끝에 붙던 두 글자가 서요. + +기능 감: Text 레이어의 굵기(wght)를 노브에 거는 칸이요. `app/layout.tsx` 는 Inter 를 weight 없이 싣고 Inter 는 가변 글꼴이라 wght 축이 브라우저에 이미 있을 텐데, TextSpec 에는 글꼴, 크기, 색, sharp, 효과만 있어요. 그래서 노브로 굵기와 폭을 바꾸려고 여섯 자를 네 마스터로 코드에 다각형으로 적었어요(18.0 KB 가운데 글자 설계가 4 KB 쯤). + +### Dürer's Alphabet + +주제 7 「타이포」의 「글자가 만들어지는 과정」과 주제 1 「지도의 빈 칸」을 같이 채우는 패턴이에요. 주제 1 은 Day Cycle, Plancherel Growth, Polynomial Amoeba, Serum Fingers 넷뿐인 움직임 × 모양 바뀜 1,3 칸을 겨눴어요. 알브레히트 뒤러(Albrecht Dürer)는 1525년 뉘른베르크에서 낸 『Underweysung der Messung』에서 로마 대문자를 정사각형 안에 자와 컴퍼스로 짓는 법을 보였고, 굵은 획을 정사각형 변의 10분의 1 로 잡았어요. 가는 획은 앨런 회니그(Alan Hoenig)가 TUGboat 11:3 에서 METAFONT 로 다시 지으며 읽은 대로 굵은 획의 3분의 1 이고, 둥근 획과 세리프 받침의 중심은 이 패턴이 정했어요. 패널은 64줄씩 위아래 두 장이고, 정사각형이 LED 중심 2.5 에서 61.5 까지라 변이 59 LED, 굵은 획이 5.9 LED, 가는 획이 1.97 LED 예요. A C D E H I L M N O T V X 열세 자를 볼록 다각형과 원 사이의 고리와 세리프 받침으로 적고, LED 하나를 4×4 로 표본해 칠해요. 세리프 받침은 모서리와 두 접점과 컴퍼스 중심이 이루는 연 꼴에서 컴퍼스 원을 뺀 것이라, 기운 획에서는 예각 쪽 받침이 길게 나와요. O 와 C 는 반지름 5 원에서 반지름 4 원 둘을 빼는데, 두 원의 중심이 왼쪽으로 20도 기운 축을 따라 0.667 단위씩 벌어져 있어서 11시와 5시가 가늘고 2시와 8시가 굵어요. Speed 2 에서 한 글자는 8초예요. 펜 끝이 4초 동안 획 가장자리를 정사각형 끝까지 자로 긋고 고리와 받침을 컴퍼스로 돌리는데, 원을 돌 때는 중심에 바늘 자리가 켜져요. 이어서 1.6초 동안 잉크가 몸 안을 왼쪽 위에서부터 측지 거리 차례로 흐르고, 1초 동안 종이가 글자에서 바깥으로 밝아져요. 두 장이 번갈아 지어서 다 된 판은 다른 장이 짓는 동안 8.6초 서 있다가, 제 차례가 오면 0.8초 동안 종이 가장자리부터 안으로 어두워져요. Stem 노브는 굵은 획을 변의 0.1 에서 0.2 까지 키우고, 가는 획과 받침과 속공간이 따라가요. 이 칸에 서려면 2~8초에 바뀌는 LED 가 한 번씩만 바뀌어야 해서, 펜 끝과 바늘 말고는 움직이는 것을 두지 않았어요. 잰 값은 움직임 0.048, 촘촘함 0.067, 가장자리 0.480, 복잡도 0.993, 채움 0.645, 색 0.130, 모양 바뀜 0.247 이고, thirds 는 67/64/62, 노브는 0.07, 0.05, 0.14, 0.08 이에요. 처음 판은 아래 장이 제 차례에 0.6초 들어간 자리에서 열리고 종이 밝기가 0.14 여서, 위 판이 어두워지는 일이 8초 안에 끝나지 않았어요. 움직임 0.035, 모양 바뀜 0.179 로 칸 아래에 섰고 Speed 노브는 0.02 였어요. 여는 자리를 차례 2.2초로 옮기고 종이를 0.17 로 올리며 0.75~1.25 배의 종이 결을 깔자 모양 바뀜 0.247, 움직임 0.048 로 칸 안에 들었어요. 6초 동안의 움직임을 다 더해도 모양 바뀜의 1.17 배예요. 종이 결은 복잡도를 0.655 에서 0.993 으로 올렸어요. 새로 본 것은 종이가 글자에서 바깥으로 밝아질 때 가장 늦게 밝아지는 자리가 C 의 속공간 한가운데, 아래 장의 (32, 96) 근처라는 거예요. 5초쯤 앞서 바깥 원을 돌 때 컴퍼스 바늘이 섰던 자리예요. + +Lab 에서 해 보기: Text 레이어에 C 를 Newsreader 로 쳐서 가운데 맞춤 x 32, y 96 에 두고, 크기는 C 의 위아래가 아래 장 정사각형(y 66.5~125.5)에 닿게 맞춰요. 채움은 #404040, blend 는 screen 이에요. K3 Letter 를 0 에 두면 처음 14초 남짓 아래 장에 C 가 서고, 오늘 쓰는 세리프 C 가 뒤러식 짓기를 벗어나는 자리만 흐린 회색으로 떠요. 굵은 자리를 잇는 축이 얼마나 기우는지, 위 끝을 어떻게 맺는지가 거기서 갈려요. C 는 104초마다 다시 와요. + +기능 감: Text 레이어의 글자를 시간이나 노브로 넘기는 길이요. 이 패턴은 8초마다 다음 대문자를 짓는데 Text 레이어의 C 는 한 자로 서 있어서, 겹쳐 볼 수 있는 판이 104초에 한 번뿐이에요. 글자 목록과 넘기는 박자나 노브를 받는 Text 레이어가 있으면 Newsreader 의 같은 글자를 판마다 따라 얹을 수 있어요. 대문자 열세 자를 다각형과 원으로 손수 적는 build 함수에 6.8 KB 가 들었고, 그 가운데 글자별 정의가 2.9 KB 예요. + +### Damask + +주제 7 「타이포」의 「글자로 짠 무늬」와 「글자와 빛」, 그리고 주제 1 「지도의 빈 칸」을 같이 채우는 패턴이에요. 주제 1 은 Curlicue, Hydrogen Orbital, One Bit, Void and Cluster 넷뿐인 촘촘함 × 채움 4,1 칸을 겨눴어요. 다마스크는 한 가지 실로 무늬와 바탕을 다 짜는 천이에요. 무늬는 날실이 겉에 뜬 경수자이고 바탕은 씨실이 겉에 뜬 위수자라서, 실 색이 아니라 빛이 실 방향을 어떻게 비추느냐로만 글자와 바탕이 갈려요. 패널에서는 LED 하나가 실이 엇갈리는 자리 하나예요. 날실은 세로로 64올, 씨실은 가로로 128올이에요. 바탕은 (y + 2x) mod 5 = 0 인 자리에서만 날실이 올라오는 5매 위수자라서, 묶는 점이 줄마다 다섯 칸에 하나씩 서고 가로세로 이웃끼리는 닿지 않아요. 글자는 같은 천의 뒷면이라 모든 자리에서 다른 실이 위에 와요. 실의 반짝임은 제임스 카지야(James Kajiya)와 티모시 케이(Timothy Kay)가 1989년에 털을 그리려고 낸 섬유 하이라이트에서 눈을 바로 위에 둔 꼴인 (1 − (T·L)²)² 로 셈했어요. 그래서 날실은 램프가 옆에 있을 때, 씨실은 램프가 위나 아래에 있을 때 밝아요. 램프는 천 가운데에서 300 LED 떨어진 곳에서 8도 높이로 돌아요. Speed 2 에서 한 바퀴가 64초라 16초마다 글자가 밝음과 어둠을 바꿔요. 천은 판판하지 않아서 그 밑으로 낮은 물결 둘이 Speed 2 에서 1초에 6 LED 와 5 LED 씩 지나가요. 8도 높이의 램프 빛은 기울기 0.03 에도 실을 22% 밝히거나 어둡게 해요. 램프를 68도로 올리면 같은 기울기가 1% 만 바꿔서, 물결은 낮은 빛에서만 보여요. 낱말은 LUX, 빛, NOX, 밤 넷이에요. 로만 대문자 다섯과 한글 자모 다섯을 획 목록으로 적고, 낱말이 천의 10~11% 를 덮는 굵기를 반씩 나눠 찾았어요. 그래서 LUX 는 줄기가 4.0 LED, 나머지는 3.3~3.5 LED 예요. 어두운 바탕에서는 묶는 점 하나가 켜지고 그 네 이웃이 꺼져 있어서, 촘촘함은 대략 0.4 × 밝기 차 / 255 이고 채움은 0.2 + 0.6 × 글자 몫이에요. 그래서 램프를 날실과 직각이 되는 자리보다 28.1도 앞에서 출발시켜, 재는 2~8초 동안 바탕이 어두운 쪽에 머물게 했어요. Node 로 그 6초의 모든 자리와 프레임을 셈하니 램프는 직각에서 30.5도 넘게 비끼지 않았어요. 물결까지 얹어도 씨실은 17.6/255 를 넘지 않았고 날실은 92 아래로 떨어지지 않았어요. 잰 값은 움직임 0.033, 촘촘함 0.324, 가장자리 0.974, 복잡도 0.878, 채움 0.263, 색 0.136, 모양 바뀜 0.057 이고, thirds 는 25/26/27, 노브는 0.07, 0.09, 0.13, 0.46 이에요. 처음 판은 대문자 높이 32, 가는 획 무게 0.55, 글자 몫 9.2% 였어요. 이때도 촘촘함 0.329, 채움 0.255 로 칸 안에 들었지만, 2 LED 굵기의 가는 획이 수자의 어두운 점에 끊겨 보였어요. 대문자를 28 로 줄이고 가는 획과 세리프를 0.72 와 0.62 로 굵히고 몫을 10.4% 로 올리자 채움만 0.263 으로 올랐어요. 그런데 그 판은 움직임이 0.017 이라 움직임 축 끝 0.019 아래였어요. 처음 얹은 물결은 1초에 3 LED 로 느려서 움직임이 0.018 에 그쳤어요. 기울기를 두 배로 하니 밝은 실이 255 에서 잘려 0.021 이었어요. 물결을 1초에 5~6 LED 로 빠르게 하자 움직임 0.033 이 됐고, 촘촘함은 0.327 에서 0.324 로 조금 내렸어요. 새로 본 것은 글자가 사라지는 경계가 램프 발밑을 지나는 45도 직선이라는 거예요. 두 실의 반짝임이 같아지는 곳이 dx² = dy² 이라서, 그 선이 10.7초에 오른쪽 아래 모서리를 떠나 15.3초에 왼쪽 위에 닿아요. 세로로 쌓은 LUX 는 맨 아래 X 가 먼저 뒤집히고 맨 위 L 이 마지막으로 뒤집혀요. + +Lab 에서 해 보기: Text 레이어에 Newsreader 로 NOX 를 size 22, sharp 로 쳐서 가운데 맞춤 x 32, y 64 에 두고, role 을 mask 로 하고 maskInvert 를 켜 보세요. 짠 LUX 의 U 위에 NOX 가 검게 뚫려요. 13초 무렵 램프가 LUX 를 어둡게 뒤집으면, 뚫린 NOX 와 짠 LUX 가 둘 다 바탕보다 어두운 글자가 돼요. + +기능 감: Text 레이어의 글자를 거리장으로 코드 레이어에 넘기는 길이요. 거리장은 칸마다 가장 가까운 획까지의 거리예요. 낱말이 천의 10~11% 를 덮게 하려면 획 굵기를 반씩 나눠 찾아야 하고, 그러려면 칸마다 그 거리가 있어야 해요. 그래서 대문자 다섯(L, U, X, N, O)과 자모 다섯(ㅂ, ㅣ, ㅊ, ㅏ, ㅁ)을 손으로 획 목록에 적었어요. 그 길이 있으면 Lab 에서 친 아무 낱말이나 같은 셈으로 굵기를 맞춰 짤 수 있어요. + +### Maya Numerals + +주제 7 「타이포」의 「숫자와 기호 놀이」로, 시계가 아닌 셈을 마야 숫자의 세 기호로 보여 주려 했어요. 마야 사람들은 점 하나를 1, 막대 하나를 5, 조개 껍데기를 0 으로 쓰고, 20진 자리를 위아래로 쌓아 가장 낮은 자리를 맨 아래에 두었어요. 드레스덴 코덱스의 수가 이렇게 적혀 있고, 1880년대에 드레스덴 도서관의 에른스트 푀르스테만(Ernst Förstemann)이 이 코덱스에서 셈법과 장기력(Long Count)을 읽어 냈어요. 장기력은 날수를 세는 자리셈이라 둘째 자리인 위날만 18 에서 넘어가요. 20일이 한 위날이고 18위날이 360일인 한 툰이에요. 패턴은 25.6줄 높이의 칸 다섯을 쌓고, 길이 38 LED 막대와 지름 5.6 LED 점을 6줄 간격으로 놓아 왼쪽 위에서 빛이 드는 구슬과 막대로 그려요. 0 은 21×12 LED 껍데기로, 뒤집은 개오지 조개처럼 가운데 홈 위아래에 세 칸마다 이빨을 찍었어요. 새 점은 오른쪽에서 0.30초에 미끄러져 들어와요. 다섯째 점이 오면 다섯이 맞닿을 때까지 모였다가 점만큼 굵은 막대로 녹고, 0.40초 끝에 막대 길이로 늘어나요. 자리가 차면 0.36초 동안 모든 기호가 가운데로 오그라들어 금빛 점이 되고, 그 점이 0.42초 동안 꼬리를 끌며 윗자리로 올라가요. Speed 2 에서 하나씩 세면 1초에 1.7 번이에요. Mode 0 은 모든 자리가 20 에서 넘어가는 20진으로, 12.7.19.19.17 에서 열려 4.8초에 받아 올림이 12.8.0.0.0 까지 올라가요. Mode 1 은 12.19.19.17.16 에서 열려 6.0초에 13.0.0.0.0 이 되는데, GMT 상관으로 2012년 12월 21일이에요. Step 은 1, 13, 20, 260, 360, 365, 584 를 그 모드의 자리로 적어 기호를 하나씩 들여보내요. 584 는 드레스덴 코덱스의 금성 표가 쓰는 금성 주기로, 20진으로는 1.9.4 이고 장기력으로는 1.11.4 예요. 잰 값은 움직임 0.129, 촘촘함 0.045, 가장자리 0.503, 복잡도 0.523, 채움 0.278, 색 0.157, 모양 바뀜 0.120 이고, thirds 는 33/26/23, 노브는 0.11, 0.10, 0.05, 0.12 예요. 첫 판은 막대가 34 LED 에 점 반지름이 2.6 이었고, 다섯 점이 한 점으로 모인 뒤 가운데서 막대가 자라서 움직임 0.111, 가장자리 0.438 이었어요. 기호를 키우고 움직이는 기호만 따로 두 번 흐려 넓게 빛나게 하고, 점이 맞닿은 사슬에서 막대로 녹게 바꾸자 움직임 0.129, 가장자리 0.503 이 됐어요. 새로 본 것은 덧셈 하나에 드는 시간이 수의 크기가 아니라 들어오는 기호 수로 정해진다는 거예요. 360 은 장기력에서 셋째 자리 점 하나라 Speed 2 에서 1초에 1.68 번 더하는데, 20진에서는 둘째 자리 막대 셋과 점 셋이라 1초에 0.31 번이에요. + +Lab 에서 해 보기: Text 레이어에 Silkscreen 으로 「20」을 size 8, sharp 로 쳐서 오른쪽 맞춤 x 63, y 90 에 두고 blend add 로 얹으면, 아래에서 둘째 칸 막대 오른쪽 빈자리(x 52~63)에 그 자리가 넘어가는 수가 서요. Mode 를 1 로 돌릴 때 글을 「18」로 바꾸면 위날 자리가 왜 막대 셋과 점 셋에서 금빛 점이 되는지 옆에서 읽혀요. + +기능 감: 코드 레이어가 Text 레이어의 글을 프레임마다 바꿔 넣는 길이요. 지금 세는 수를 아라비아 숫자로 기호 옆에 읽어 주고 싶었는데, Text 레이어의 글은 손으로 친 그대로라 1초에 1.7 번 바뀌는 수를 따라가지 못해서 뺐어요. + +### Airy Beam + +주제 5 「수학 · 추상」과 주제 4 「세로 64×128」을 같이 채우는 패턴이에요. 마이클 베리(Michael Berry)와 난도르 발라즈(Nandor Balazs)는 1979년에 자유 공간의 슈뢰딩거 방정식에서 에어리 함수 꼴의 파속이 퍼지지 않고 포물선을 따라 가속한다는 것을 보였어요. 이 파속은 에너지가 무한해서 그대로는 만들 수 없어요. 게오르기오스 시빌로글루(Georgios Siviloglou)와 데메트리오스 크리스토도울리데스(Demetrios Christodoulides)가 2007년에 exp(as) 를 곱해 자른 유한 에너지 에어리 빔을 냈고, 같은 해 존 브로키(John Broky), 아리스티드 도가리우(Aristide Dogariu)와 함께 휘는 빛줄기를 실험으로 봤어요. 2008년에는 같은 넷이 주 로브를 가려도 아래에서 다시 생기는 자기 치유를 보였어요. 패턴은 근축 방정식 i ∂φ/∂ξ + (1/2)∂²φ/∂s² = 0 을 가로 s, 세로 ξ 로 두고, LED 하나를 s 의 1/4 로, 128줄을 ξ 0~8 로 잡아요. Ai(s)exp(as) 의 스펙트럼 exp(a³/3)exp(−ak²)exp(i(k³/3 − a²k)) 에 줄마다 exp(−ik²ξ/2) 를 곱해, 512점 FFT 128번으로 빔 전체를 13 ms 에 지어요. 맨 윗줄을 Ai 의 멱급수와 견주니 s = 0, −1, −3, −10 에서 1e−5 안으로 맞았어요. 구슬은 22번 줄에서 폭 5.2 LED 를 가리고, 주 로브 오른쪽 6 LED 에서 왼쪽 11 LED 까지를 Speed 2 에서 9초에 한 번 오가요. 매 프레임 FFT 를 다시 돌리지 않고 가린 몫만 같은 격자의 전파 핵으로 아래 줄에 날라 자유 빔에서 빼서, 한 장을 1.4 ms 에 그려요. 분할 단계 FFT 와 똑같은 셈이에요. 기울기는 exp(ivs) 를 곱하는 것이라 빔을 s → s + vξ 로 미는 것과 같아서, 같은 표를 줄마다 옆으로 밀어 읽어요. 처음 노브에서 주 로브는 x 28 에서 들어와 38번 줄에서 x 22 까지 왼쪽으로 갔다가 맨 아래에서 x 54 로 휘고, 그 왼쪽을 곁 로브가 채워요. 밝기는 세기를 0.25 와 그 줄 자유 빔 최댓값의 기하 평균으로 나눠 0.55 제곱하고, 색은 위상을 따라 초록과 하늘색 쪽으로 0.05 씩 기울여 1초에 한 바퀴 돌려요. 잰 값은 움직임 0.074, 촘촘함 0.043, 가장자리 0.116, 복잡도 2.189, 채움 0.667, 색 0.248, 모양 바뀜 0.082 이고, thirds 는 52/64/84, 노브는 0.19, 0.06, 0.09, 0.16 이에요. 첫 판은 LED 하나를 s 의 1/3 로 잡아 아래 왼쪽의 로브 간격이 2.3 LED 까지 좁아졌고, 위상 색 줄무늬와 겹쳐 빗살무늬가 섰어요. 1/4 로 바꾸자 그 간격이 3.4 LED 가 되고 thirds 가 47/62/86 에서 52/64/84 로 바뀌었어요. 새로 본 것은 치유가 끝까지 가지 않는다는 거예요. 구슬을 주 로브 한가운데 두면 8줄 아래에서 자유 빔 세기의 20% 였던 로브가 60번 줄에 47%, 80번 줄에 62% 로 돌아오지만, 맨 아래까지 70% 를 넘지 못해요. 구슬이 가져간 빛은 돌아오지 않아서예요. + +### Topologist's Sine Curve + +주제 5 「수학 · 추상」으로 위상수학자의 사인 곡선을 위상·매듭 갈래에 세웠어요. S = {(x, sin(1/x)) : 0 < x ≤ 1} 에 극한 선분 {0} × [−1, 1] 을 붙인 닫힘은 연결되어 있지만, 곡선 위의 점에서 선분까지 가는 경로는 없어요. 린 아서 스틴(Lynn Arthur Steen)과 J. 아서 시배크 주니어(J. Arthur Seebach Jr.)가 1970년에 낸 『Counterexamples in Topology』의 반례이고, 1978년 판에서는 116번이에요. 패널은 x 를 세로로 세워 맨 위가 x = X, 121줄 한가운데(Y = 121.5)가 x = 0 이고, 그 줄에 극한 선분을 4~59열에 걸쳐 금빛으로 그어요. sin(1/x) 는 가로로 4.5열이 −1, 59.5열이 +1 이에요. 줄마다 그 줄이 맡은 x 구간을 정확히 나눠요. u = 1/x 의 꺾이는 점 사이에서 sin u 는 한쪽으로만 가서 k 번째 조각의 역함수가 u = kπ + asin((−1)^k s) 이고, 열 경계를 지나는 x 를 하나하나 셈할 수 있어서, 줄의 x 가운데 몇 할이 어느 LED 에 떨어지는지가 표본 없이 나와요. 한 줄에 흔들림이 다섯 번을 넘으면 아크사인 분포 (asin s1 − asin s0)/π 를 그대로 써요. 가장자리 4열은 줄의 0.061 을, 가운데 열은 0.0116 을 받아서 5.25 배이고, 첫 화면 116줄을 읽으면 가장자리가 255, 가운데가 97 이에요. 오른쪽으로 흔들리며 내려가는 조각은 청록, 왼쪽으로 가는 조각은 보라로 칠해서, 둘을 가를 수 없는 띠는 섞인 빛깔이 돼요. 확대는 30초에 한 번 X 를 넷으로 나눴다 되돌리고, 그동안 띠에서 새 흔들림이 빠져나와 올라가요. 구슬은 q = x/X 가 (1 − t/8)^1.25 로 줄어서 처음에는 1초에 19줄을 내려와요. 8초에 선분에 닿을 듯하지만 sin(1/x) 에 극한이 없어서 흔들림이 끝없이 빨라지고, 마지막 1초 남짓은 프레임보다 빨라 띠 너비의 금빛 번짐이 돼요. 선분 0.4 LED 앞에서 걷기를 멈추고 0.8초 뒤 맨 위에서 다시 걸어요. 잰 값은 움직임 0.953, 촘촘함 0.089, 가장자리 0.397, 복잡도 1.171, 채움 0.398, 색 0.242, 모양 바뀜 0.072 이고, thirds 는 11/33/75, 노브는 0.09, 0.12, 0.24, 0.08 이에요. 처음 짠 구슬은 x 를 exp(−t/1.5) 로 줄여서 1.5초 만에 패널의 63% 를 내려가고 남은 6초를 띠 속에서 보냈어요. 그래서 고른 빠르기에 가깝게 내려오도록 바꿨어요. 그 판을 처음 잰 값은 움직임 0.941, 색 0.233, 모양 바뀜 0.067, Measure 노브 0.05 였어요. 색이 깊이로만 바뀌어 이웃한 흔들림이 한 덩이로 보였는데, 방향으로 색을 나누고 번짐을 채널마다 0.7 로 더하자 색 0.242, Measure 노브 0.08 이 됐어요. 새로 본 것은 띠의 밝은 가장자리가 곡선의 길이가 아니라 x 의 몫에서 나온다는 거예요. 길이로 세면 띠 안의 흔들림은 거의 수평으로 열마다 두 번씩 지나가서 고르게 차고, Measure 를 1 로 돌리면 같은 116줄이 처음부터 끝까지 255 예요. + +### Ehrenfest Urn + +주제 5 「수학 · 추상」으로, 한쪽으로만 흘러가는 것처럼 보이는 일과 모든 상태가 돌아온다는 일을 한 패널에 같이 세워 봤어요. 파울 에렌페스트(Paul Ehrenfest)와 타티아나 에렌페스트(Tatjana Ehrenfest)는 1907년에 볼츠만의 H 정리에 대한 두 반론을 다룬 글에서, 번호 붙은 공 N 개를 두 항아리에 나눠 담고 걸음마다 번호 하나를 뽑아 그 공을 다른 항아리로 옮기는 모형을 냈어요. 한쪽에 다 모인 데서 시작하면 위 방의 수 n 의 기댓값은 N/2 + (N/2)(1 − 2/N)^t 로 N/2 에 다가가고 그 둘레를 표준편차 √N/2 로 흔들려요. 그래도 상태가 N+1 개뿐인 사슬이라 어느 상태든 다시 와요. 마크 카츠(Mark Kac)는 1947년에 상태 k 로 돌아오는 평균 걸음이 2^N / C(N, k) 라는 것을 셈했어요. 패턴은 62×44 LED 의 둥근 방 둘을 위아래로 쌓아 폭 6 LED, 길이 6줄의 목으로 잇고, 걸음마다 공 하나를 골라 목을 지나 다른 방의 아무 자리로 날려요. 걸음은 1초에 5 × Speed × √(N/8) 번이라 Speed 2 에서 공 8 개는 10 번, 200 개는 50 번이고, 한 번 나는 데 0.12~0.5초가 걸려요. 쉬는 공은 0.6초에 풀리는 속도로 1초에 5 LED 쯤 떠돌아요. 위 방의 공은 금빛, 아래 방의 공은 하늘빛이고 목을 지나며 빛깔이 바뀌어요. 96행 아래 띠는 52 칸에 위 방의 수를 그리고, N/2 점선과 ±1, ±2 표준편차 띠, 시작에서 내려오는 기댓값 곡선을 깔아요. 오른쪽 10 칸은 수마다 머문 걸음을 막대로 쌓아 C(N, k)/2^N 점과 견줘요. Watch 노브가 고른 수에 닿을 때마다 방이 번쩍이고 띠 위에 눈금이 서요. 공 8 개에서 모두 위로 돌아오는 데 평균 256 걸음, 25.6초가 걸리고, 200 개에서는 2^200 걸음이라 돌아오지 않아요. 잰 값은 움직임 0.569, 촘촘함 0.043, 가장자리 0.552, 복잡도 0.451, 채움 0.197, 색 0.161, 모양 바뀜 0.098 이고, thirds 는 14/17/27, 노브는 0.04, 0.06, 0.12, 0.08 이에요. 첫 판은 번쩍임이 벽에만 와서 Watch 반응이 0.02 였는데, 두 방 안을 40 으로 같이 밝히자 0.08 이 됐어요. Hue 0.5 에서 벽과 띠가 어두운 주황이라 갈색으로 보여서 채도를 0.2~0.3 으로 묽혔어요. 그러자 Hue 반응이 0.03 으로 내려가서, 공 반지름을 3 에서 3.3 으로 키우고 흐린 테를 둘러 0.04 로 올렸어요. 새로 본 것은 돌아오는 때가 몰려 온다는 거예요. Lab 의 씨앗으로 공 8 개를 600초 돌렸어요. 기대는 6000 / 256 = 23.4 번이고, 모두 위로 돌아온 번쩍임은 실제로 20 번 왔어요. 그런데 63.0초와 63.2초, 317.4초와 317.6초와 319.6초처럼 두 걸음 사이로 붙어 왔고, 첫 번째는 0.17초에 두 걸음 만에 왔다가 다음은 47.6초에야 왔어요. 7 에서 한 걸음에 8 로 돌아갈 확률이 1/8 이라 한 번 돌아온 수는 곧 또 돌아오기 쉬워요. + +### Steiner Deltoid + +주제 5 「수학 · 추상」을 채우는 패턴이에요. 삼각형의 외접원 위 점 P 에서 세 변의 연장선에 수선을 내리면, 그 발 셋은 언제나 한 직선 위에 있어요. 이 선에는 로버트 심슨(Robert Simson)의 이름이 붙었지만 처음 낸 사람은 윌리엄 월리스(William Wallace)이고, 1799년이에요. P 가 원을 돌 때 이 심슨 선들이 감싸는 곡선이 세 뿔 델토이드이고, 야코프 슈타이너(Jakob Steiner)가 1856년에 이 곡선을 다뤘어요. 패턴은 외접원 반지름을 18 LED 로 둬요. P 는 Speed 2 에서 5초에 한 바퀴를 돌고, 6도 갈 때마다 그 자리의 심슨 선을 패널 끝까지 그어 남겨서 한 바퀴에 60 줄이 쌓여요. 남은 줄은 반 바퀴에 37% 로 바래요. 이웃한 줄은 기울기가 3도씩 달라서, 델토이드 가까이에서는 겹쳐 흰 가장자리가 되고 멀리서는 갈라져 따로 선 빛살로 패널 위아래 끝까지 가요. 지금 줄은 흰빛이고 P 와 세 발은 금빛 점이에요. 지금 줄이 델토이드에 닿는 점은 복소수로 (a + b + c + 2p + abc/p²)/2 라서, 그 자리에 흰 불꽃을 찍어요. 꼭짓점 셋은 47, 59, 71초 박자로 원 위를 오가고, 삼각형 전체는 200초에 한 바퀴 돌아요. 패널 폭이 64 라 구점원 중심은 늘 가운데 열에 두고, 외접원 중심은 64번째 줄 위에서 옆으로만 밀려요. Node 로 무작위 삼각형 200 개에 P 를 2000 자리씩 놓고 셈했어요. 세 발이 한 직선 위에 있었고(넓이 1e-15 아래), P 가 한 바퀴 도는 동안 심슨 선은 거꾸로 180도 돌았어요. 꼭짓점 맞은편 점의 심슨 선은 맞은 변이었어요. 감싸는 곡선에서 구점원 중심에 가장 가까운 점은 R/2, 가장 먼 점은 3R/2 였어요(1e-6 안). 그러니 구점원이 델토이드의 내접원이고, 뿔 끝은 1.5R = 27 LED 에 서요. 델토이드 안의 점은 한 바퀴에 심슨 선 셋이 지나가고 1.5R 밖의 점은 하나가 지나가는 것도 셈했어요. 잰 값은 움직임 0.324, 촘촘함 0.081, 가장자리 0.244, 복잡도 1.884, 채움 0.406, 색 0.110, 모양 바뀜 0.071 이고, thirds 는 27/57/36, 노브는 0.06, 0.08, 0.09, 0.07 이에요. 처음 판은 0.6도마다 심슨 선을 이어 그어 빛을 쌓았어요. 그때 잰 값은 움직임 0.339, 촘촘함 0.022, 가장자리 0.305, 복잡도 1.232, 채움 0.302, 색 0.086, 모양 바뀜 0.066, thirds 12/56/21, 노브 0.06, 0.07, 0.04, 0.29 였어요. 그런데 삼각형이 80초에 한 바퀴씩 돌아서 자취가 남는 동안 델토이드가 번졌어요. 또 이어 그은 선은 닿는 점에서 멀어질수록 1/s 로 어두워져서, 가장자리 대신 뿌연 빛 덩어리가 됐어요. 삼각형을 200초로 늦추고 빛을 줄이자 채움이 0.144 로, 색이 0.050 으로 떨어졌어요. 0.050 은 색 축 끝 0.052 아래예요. 그래서 6도마다 한 줄씩 실처럼 떨어뜨려 긋게 바꿨더니 채움 0.441, 색 0.124 가 됐어요. 한 줄의 세기를 0.6 에서 0.5 로 줄이고 삼각형을 밝히니 지금 값이 됐어요. 새로 본 것은 삼각형의 세 변이 델토이드 가장자리에 붙어 보인다는 거예요. 세 변이 그 자체로 심슨 선(맞은 꼭짓점의 반대쪽 점의 것)이라서, 금빛 변 셋이 델토이드의 오목한 세 가장자리를 따라 길게 겹쳐요. 그래서 삼각형이 델토이드 안에 끼워진 것처럼 보여요. + +### Voss Dice + +주제 5 「수학 · 추상」으로, 주사위 몇 개로 1/f 잡음을 만드는 방법을 주사위째 보여 주려 했어요. 마틴 가드너(Martin Gardner)는 1978년 4월 Scientific American 의 Mathematical Games 칼럼 「White and brown music, fractal curves and one-over-f fluctuations」에서 리처드 F. 보스(Richard F. Voss)의 방법을 소개했어요. 주사위 하나하나를 이진 카운터의 비트 하나로 두고, 한 걸음마다 비트가 바뀐 주사위만 다시 굴리면 주사위 k 는 2^k 걸음마다 굴러요. 그러면 합이 매번 다 굴리는 백색 잡음과 주사위로 걷는 갈색 잡음 사이의 1/f 잡음이 돼요. 패턴은 위쪽에 주사위 n 개를 7×7 칸 얼굴로 그려요. 넷이면 14 LED 이고 다섯부터 7 LED 예요. 가장 빠른 주사위 0 을 오른쪽에 둬서 아래 비트 등이 걸음 수를 이진수로 읽히게 했고, 다시 굴린 주사위는 분홍으로 번쩍였다가 0.25초에 식어요. 그 아래 20 LED 너비 세 칸에 왼쪽부터 백색, 분홍, 갈색 합이 한 걸음에 2줄씩 위에서 아래로 흘러요. 백색은 주사위 n 개를 매 걸음 다 굴린 합이고, 갈색은 주사위 하나의 눈에서 3.5 를 뺀 만큼 걸어 창 끝에서 되접혀요. 세 칸 모두 평균 3.5n 위아래로 표준편차의 2.6 배를 둔 같은 창에 그려서, 주사위 넷이면 4~24 가운데 5.1~22.9 를 써요. Speed 2 에서 1초에 6 걸음이고, 첫 화면부터 패널이 차도록 140 걸음을 미리 굴려 둬요. 넷째 노브 Hold 는 이웃한 주사위가 얼굴을 쥐는 시간의 비 r 이에요. 주사위 k 는 floor(걸음 / r^k) 가 바뀔 때 구르고, r 이 2 이면 보스의 카운터 그대로예요. Lab 의 처음 값 0.06 이 정확히 2 가 되게 맞췄어요. r 을 1 로 내리면 가운데 칸이 왼쪽처럼 백색이 되고, 3.8 로 올리면 느린 주사위가 수백 걸음을 버텨 가운데 줄이 좀처럼 자리를 옮기지 않아요. 40만 걸음을 돌려 세어 보니 주사위 넷일 때 한 걸음에 평균 1.875 개가 구르고, 분홍 합은 한 걸음에 2.56 움직여 백색의 3.87 보다 작아요. 잰 값은 움직임 0.959, 촘촘함 0.083, 가장자리 0.759, 복잡도 0.184, 채움 0.294, 색 0.125, 모양 바뀜 0.074 이고, thirds 는 37/24/26, 노브는 0.05, 0.10, 0.14, 0.10 이에요. 처음 잰 판에서 바꾼 것은 주사위 차례와 주석뿐이라 잰 값이 그대로예요. 새로 본 것은 첫 화면의 주사위 넷이 8걸음마다, Speed 2 에서 1.33초마다 한꺼번에 번쩍인다는 거예요. 카운터가 0111 에서 1000 으로 넘어가는 걸음이라 넷이 다 구르고, 그 걸음의 분홍 합은 평균 3.87 을 뛰어 백색이 매 걸음 뛰는 폭과 같아요. 주사위를 여덟로 늘리면 백색의 한 걸음은 5.46 으로 커지는데 분홍은 2.61 로 거의 그대로라서, 가운데 칸만 차분해져요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -4019,7 +4098,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 25일 `dev`(`f72355d`)의 328개에 43판부터 71판까지의 이백아흔 개를 더한 618개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 25일 `dev`(`f72355d`)의 328개에 43판부터 73판까지의 삼백 개를 더한 628개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -4119,6 +4198,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 71판은 69판까지 든 598개에 빈 칸도 둘뿐인 칸도 없어서, 셋뿐인 칸 셋 가운데 같이 돈 70판(#333)이 고른 가장자리 × 촘촘함 1,3 과 촘촘함 × 복잡도 4,4 를 빼고 남은 가장자리 × 촘촘함 1,4 를 겨눴어요. 남은 한 자리는 넷뿐인 칸 스물여섯 가운데 움직임 × 촘촘함 1,3 에 줬어요. 가장자리 × 촘촘함 1,4 는 58판이 Mirror Curves 에서 쓴 한 LED 체커가 길이에요. Twindragon 은 가우스 정수 z 를 LED (1+i)z 에 두니 격자가 그대로 x + y 가 짝수인 체커가 되고, 켜진 LED 마다 네 이웃이 꺼져 있어서 촘촘함은 켜진 밝기의 평균, 가장자리는 켜진 LED 가운데 40 을 넘는 몫이에요. 흐린 타일을 40 에, 세는 창을 255 에 두고 창을 18.55% 로 잡아 촘촘함 0.310, 가장자리 0.186 이고, 패널이 K = 12 타일 하나의 넓이라 창의 LED 는 어느 프레임에서나 760 개예요. 움직임 × 촘촘함 1,3 은 1초에 바뀌는 양이 0.061 아래이면서 이웃 차이 평균이 0.189~0.297 인 칸이에요. Book from the Sky 는 9×9 LED 글자 55 자의 먹과 종이 사이 계단이 촘촘함 0.254 를 내고, 움직임은 새 종이가 1초에 5 LED 씩 덮이고 줄마다 찍히는 것뿐이라 0.048 이에요. 첫 판은 젖은 먹 번짐과 1초에 7.5 LED 씩 덮이는 종이로 0.109 였어요. 열 개 다 일곱 축의 끝 안에 있어서 칸 경계는 60판 그대로예요. 먼저 머지된 70판까지 든 608개에 71판 열 개를 더한 618개에서 지도 21장은 다 `none` 이고, 셋뿐인 칸은 하나도 남지 않았어요. 넷뿐인 칸은 스물일곱이에요. +73판은 72판(#335)의 열 개까지 든 628개에 빈 칸도 셋 이하인 칸도 없어서, 넷뿐인 칸 스물셋 가운데 72판이 고른 움직임 × 색 1,4 와 가장자리 × 색 1,4 를 빼고 촘촘함 × 채움 4,1 과 움직임 × 모양 바뀜 1,3 을 겨눴어요. 촘촘함 × 채움 4,1 은 이웃 차이 평균이 0.297 을 넘으면서 24/255 넘게 켜진 LED 가 0.302 아래인 칸이에요. 71판이 다음 감으로 적은 대로 어두운 바탕에 외딴 밝은 점을 찍는 길이에요. 켜진 LED 마다 네 이웃이 꺼져 있으면 촘촘함은 그 LED 의 몫의 두 배에 밝기를 곱한 값이라, Damask 는 5매 위수자의 묶는 점이 가로세로로 닿지 않는 것을 그대로 썼어요. 바탕의 20% 가 외딴 날실 점이고 낱말은 천의 10.4% 라서 촘촘함 0.324, 채움 0.263 이에요. 움직임 × 모양 바뀜 1,3 은 1초에 바뀌는 양이 0.061 아래이면서 모양 바뀜이 0.211~0.328 인 칸이라, 2~8초에 바뀌는 LED 가 한 번씩만 한쪽으로 바뀌어야 해요. Dürer's Alphabet 은 펜 끝과 컴퍼스 바늘 말고는 움직이는 것을 두지 않고, 잉크가 글자 몸을 채우고 종이가 밝아지는 차례를 재는 창 안에 넣어 움직임 0.048, 모양 바뀜 0.247 이에요. 6초 동안 바뀐 양을 다 더해도 모양 바뀜의 1.17 배예요. 둘 다 처음 판은 칸 밖이었어요. Damask 는 움직임 0.017 로 움직임 축의 아래 끝 0.019 를 내려서 천 밑에 낮은 물결을 흘렸고, Dürer's Alphabet 은 모양 바뀜 0.179 로 칸 아래에 서서 여는 자리를 옮겼어요. 열 개 다 일곱 축의 끝 안에 있어서 칸 경계는 60판 그대로예요. 72판까지 든 628개에 73판 열 개를 더한 638개에서 지도 21장은 다 `none` 이고, 겨눈 두 칸은 둘 다 다섯이 됐어요. 넷뿐인 칸은 스물셋에서 스물하나로 줄었어요. + ### 아직 없는 갈래 `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 가 섰어요. @@ -4272,9 +4353,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 71판에서는 Sator Square 와 Tensegrity 가 128줄을 써요. Sator Square 는 네 끝에 A 와 O 를 단 A-PATERNOSTER-O 열세 자의 세로줄이 패널 높이를 다 써서 thirds 가 16/44/14 이고, 돌판을 빼면 9/25/10, 십자가 뒤에 깐 흐린 빛 띠까지 빼면 6/19/7 이라 글자만으로도 셋 다 5 를 넘어요. 가운데가 두꺼운 것은 돌판이 y 33~95 에 서서 가운데 3분의 1을 다 덮기 때문이에요. Tensegrity 는 막대 15 개의 탑이 121 LED 높이로 서서 26/33/31 이에요. 글자 다섯 가운데 Book from the Sky 는 괘선 친 다섯 줄이 쪽을 다 채워 78/75/77 이고, Catoptric Anamorphosis 는 원기둥을 위에 두고 바닥 종이가 아래를 채워 52/96/100 이에요. Lorem Ipsum 은 글이 위에, 더미가 아래에 서서 17/8/17 로 가운데가 엷어요. +73판에서는 Abracadabra 와 Airy Beam 이 128줄을 써요. Abracadabra 는 열한 줄을 128/11 = 11.64 줄 간격으로 세워 1행부터 126행까지 쓰는데, 줄이 한 글자씩 짧아지는 계단이라 아래로 갈수록 켜진 몫이 줄어 thirds 가 28/18/11 이에요. 처음에는 빠진 글자가 다 부서진 뒤 아주 꺼져서 맨 아래 3분의 1 이 8 이었고, 32/255 의 그을음을 남기자 11 이 됐어요. Airy Beam 은 128줄을 ξ 0~8 로 잡아 빔이 내려가며 휘고, 아래로 갈수록 곁 로브가 주 로브 왼쪽을 채워서 52/64/84 예요. Dürer's Alphabet 도 64줄 정사각형 두 장을 위아래로 쌓아 67/64/62 예요. Topologist's Sine Curve 는 x 를 세로에 정비례로 세워 흔들림이 아래 극한 선분 쪽에 몰려서 11/33/75 이고, 주제 4 로는 세지 않았어요. + ### 낯선 수학 -주제 5 로 쓴 수학과 다음에 해 볼 수학이에요. 28판이 처음 열 개를 썼어요. 슈람-뢰브너 진화(Loewner Trace), 포드 원(Ford Circles), 털 난 공 정리(Hairy Ball), 아즈텍 다이아몬드와 북극권(Aztec Diamond), 부니모비치 경기장(Stadium Billiard), 라우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 다면체와 E8(Gosset Polytope), 콘웨이의 보고 말하기 수열(Look and Say), 위토프 게임의 그런디 값(Wythoff Nim), 쌍곡 평면의 띠 모형(Hyperbolic Band)이에요. 29판은 여섯을 더했어요. 클라인의 j 불변량(J-Invariant), 호프 올뭉치(Hopf Fibration), 다이슨 브라운 운동(Dyson Gas), 가케야 바늘 문제의 페론 나무(Perron Tree), 임계선 위의 리만 제타 함수(Zeta Loops), 길버트-섀넌-리즈 리플과 베이어-디아코니스의 일곱 번(Seven Shuffles)이에요. 30판도 여섯을 더했어요. 세 간격 정리(Three Gaps), 호프스태터 나비와 TKNN 정수(Hofstadter Butterfly), 노이만의 외부 당구(Outer Billiards), 볼드의 탄도 퇴적과 KPZ 앞면(Ballistic Deposition), 아르놀트의 미앤더(Meander), 브로드벤트와 해머슬리의 스미기(Percolation)예요. 31판도 여섯을 더했어요. 마스킷 조각의 클라인 군 극한 집합(Indra's Pearls), 리틀우드 다항식의 근(Littlewood Roots), 고든의 가우스 해자(Gaussian Moat), 아르놀트의 고양이 사상(Cat Map), 자부스키와 크루스칼의 KdV 솔리톤(KdV Solitons), 보이 곡면(Boy's Surface)이에요. ROUND.md 의 「낯선 수학 예」 여덟 가운데 쓴 것은 슈람-뢰브너 진화와 아즈텍 다이아몬드, 모듈러 형식, 호프 올뭉치, 세 간격 정리, 외부 당구 여섯이었고, 31판이 리틀우드 다항식의 근과 클라인 군의 극한 집합을 써서 여덟을 다 썼어요. 32판도 여섯을 더했어요. 탤벗 효과와 양자 카펫(Talbot Carpet), 길브레스 추측의 표(Gilbreath Table), 원 사상의 아르놀트 혀(Arnold Tongues), 콘웨이와 라딘의 핀휠 타일링(Pinwheel Tiling), 윌킨슨과 윌렘센의 침입 스미기(Invasion Percolation), 울람의 문제와 해머슬리 과정(Hammersley Lines)이에요. 33판도 여섯을 더했어요. 에인절, 홀로이드, 로믹, 비라그의 무작위 정렬 네트워크(Sorting Network), 베르트랑의 역설(Bertrand Paradox), 펜로즈의 비출 수 없는 방(Penrose Room), 당들랭 구(Dandelin Spheres), 테이트의 보로메오 고리(Borromean Rings), 골드바흐 혜성(Goldbach Comet)이에요. 34판도 여섯을 더했어요. 톰슨 문제와 오일러가 남기는 다섯 이웃 열둘(Thomson Problem), 켈빈의 배 물결(Kelvin Wake), 요네야마가 낸 와다의 호수(Lakes of Wada), 샤이데거의 강 그물(Scheidegger Rivers), 모저의 소파 문제와 해머슬리의 소파(Moving Sofa), 크누스의 춤추는 링크로 푼 펜토미노 덮기(Pentomino Cover)예요. 35판도 여섯을 더했어요. 뢸로의 폭이 일정한 곡선(Constant Width), 렌니의 주차 문제(Rényi Parking), 카리의 비주기 왕 타일(Kari-Culik Tiles), 콜라코스키 수열(Kolakoski), 쿠라모토와 바톡토흐의 키메라 상태(Chimera State), 주콥스키 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했어요. 그레이슨의 정리가 말하는 곡선 단축 흐름(Curve Shortening), 헤우드의 일곱 색 토러스 지도(Heawood Torus), 슈바르츠 랜턴(Schwarz Lantern), 윌브러햄과 깁스의 현상(Gibbs Phenomenon), 타원 곡선의 현과 접선(Elliptic Chords), 지먼의 파국 기계(Catastrophe Machine)예요. 37판도 여섯을 더했어요. 포천의 쓸기 선 알고리즘(Fortune's Sweep), 앙투안의 목걸이(Antoine's Necklace), 카르만 소용돌이 길의 안정성(Kármán Street), 단단한 원판의 헥사틱 상(Hexatic Phase), 존슨-멜 쪽매와 아브라미 법칙(Johnson-Mehl), 쿠라모토-시바신스키 방정식(Kuramoto-Sivashinsky)이에요. 38판도 여섯을 더했어요. 앤더슨 국소화(Anderson Localization), 무어가 찾고 Chenciner 와 Montgomery 가 증명한 세 몸 문제의 8자 해(Figure-Eight Orbit), 예센과 빈트너, 에르되시의 베르누이 합성곱(Bernoulli Convolutions), 브라우어르의 고정점 정리를 증명하는 슈페르너 보조정리(Sperner's Lemma), 에겐베르거와 폴리아의 항아리(Pólya Urn), 뇌르고르의 무한 수열(Infinity Series)이에요. 39판도 여섯을 더했어요. 고든, 웹, 월퍼트의 등스펙트럼 북(Isospectral Drums), 폰 망골트의 꼴로 쓴 리만의 명시 공식(Explicit Formula), 요한 베르누이의 최단 강하선(Brachistochrone), 슈타이너의 포리즘(Steiner Chain), 모런 집단으로 그린 킹먼의 합류(Kingman Coalescent), 보르수크-울람 정리(Borsuk-Ulam)예요. 40판도 여섯을 더했어요. 앨더스의 브라운 연속 랜덤 나무(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 함수(Hydrogen Orbital), 젤도비치 근사의 팬케이크(Zeldovich Pancakes), 망델브로가 내다보고 롤러, 슈람, 베르너가 증명한 브라운 운동의 바깥 경계(Brownian Frontier), 복소 긴즈부르크-란다우 방정식의 결함 난류(Defect Turbulence), 루퍼트 왕자의 정육면체(Prince Rupert's Cube)예요. 41판은 다섯을 더했어요. 홀과 왜건의 현수선 길과 정다각형 바퀴(Square Wheels), 퐁슬레의 포리즘(Poncelet Porism), 슐레플리의 4차원 정다포체를 자른 단면(Polytope Slices), 클림켁과 티먼, 마그누손의 사이트스왑(Siteswap), Dudeney 의 잡화상 퍼즐과 월리스-보여이-게르비엔 정리(Hinged Dissection)예요. 42판은 다섯을 더했어요. 바른스도르프 규칙으로 도는 기사 여행(Knight's Tour), 페르미와 파스타, 울람, 칭구의 되돌아옴(FPUT Recurrence), 포슬리에-립킨 반전기(Peaucellier Linkage), 몰리의 삼등분선 정리(Morley Triangle), 랄과 마드라스, 소칼의 피벗 알고리즘으로 돌린 자기 회피 걸음(Pivot Walk)이에요. 43판은 다섯을 더했어요. 월리스 워커의 칼레이도사이클(Kaleidocycle), 쌍곡형에서 록소드롬형으로 감기는 뫼비우스 변환의 흐름(Möbius Loxodrome), 터트의 용수철 정리(Tutte Embedding), 디펀트와 크래비츠의 고리 정리를 품은 히토메자시 사시코(Hitomezashi), 룽게 현상과 체비쇼프 점(Runge Phenomenon)이에요. 44판은 다섯을 더했어요. 마크 카츠의 고리 모형(Kac Ring), 스톤과 튜키의 햄 샌드위치 정리(Ham Sandwich), 브란트의 불가리아 솔리테어(Bulgarian Solitaire), 엘스켄스와 프리시의 탄도 소멸(Ballistic Annihilation), 페레그린의 솔리톤(Peregrine Soliton)이에요. 45판은 다섯을 더했어요. 로건-셰프와 베르시크-케로프의 극한 곡선으로 다가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도브루신 경계 조건을 건 임계 이징 모형의 SLE(3) 경계(Ising Interface), 프리예제프와 동료들의 오일러 걷개로 쌓는 로터 라우터 응집과 레빈-페레스의 스매시 합(Rotor-Router), 폴리아의 과수원 문제(Pólya's Orchard), 넬슨의 평면 색칠 문제와 아이즈벨의 일곱 색(Hadwiger-Nelson)이에요. 46판은 다섯을 더했어요. 브릴루앙의 구역(Brillouin Zones), 모스 이론과 레브의 그래프(Reeb Graph), 로렌츠 기체와 시나이의 당구(Lorentz Gas), 리언 글래스의 점무늬(Glass Patterns), 벤치와 수테라, 불피아니의 확률 공명(Stochastic Resonance)이에요. 47판은 다섯을 더했어요. 오키프와 홍, 스트로가츠의 스워멀레이터(Swarmalators), 빌스의 외로운 달리기 선수 추측(Lonely Runner), 샤츠의 올로이드와 그 전개도(Oloid), 쾨베의 원 채우기 정리와 콜린스-스티븐슨의 반복(Koebe Packing), 콘웨이의 병사와 황금비 무게(Conway's Soldiers)예요. 48판은 다섯을 더했어요. 오하라의 매듭 뫼비우스 에너지(Möbius Energy), 체비쇼프의 편향과 루빈스타인-사르낙의 밀도(Chebyshev's Bias), 섀든과 레키엔, 마스든의 이중 소용돌이 능선(Double Gyre), 아자리와 프로스트의 깜빡이는 래칫(Flashing Ratchet), 망델브로의 프랙탈 스미기(Fractal Percolation)예요. 49판은 다섯을 더했어요. 뒤러의 전개 문제와 셰퍼드의 추측(Dürer's Unfolding), 베리와 골드버그가 재규격화한 세타 합의 꼬부랑 곡선(Curlicue), 오일러 방정식과 푸앵소의 폴호드로 본 테니스 라켓 정리(Tennis Racket), 켐프의 사슬과 네 색 정리(Kempe Chains), 퇴플리츠의 내접 정사각형 문제와 그린-로브의 직사각형(Square Peg)이에요. 50판은 다섯을 더했어요. 크레이치넌과 배철러의 2차원 난류가 거꾸로 가는 캐스케이드와 맥윌리엄스의 소용돌이(Inverse Cascade), 사포발과 로소, 구예의 확산 앞줄(Diffusion Front), 필리와 마르케티, 케이츠와 타이외르의 운동성 상분리(Motility Clusters), 박과 스네펜의 진화 모형(Bak-Sneppen), 리세의 영원한 가속(Risset Rhythm)이에요. 51판은 다섯을 더했어요. 스미스와 마이어스, 캐플런, 굿먼-스트라우스의 모자 타일과 H/T/P/F 메타타일 치환(Hat Monotile), 반도와 동료들의 최적 속도 모형과 스기야마의 원형 트랙 실험(Phantom Jam), 사토와 테이트의 법칙과 타원곡선의 a_p(Sato-Tate), 칼라비와 야우의 공간에서 핸슨이 잘라 그린 5차 페르마 곡선(Calabi-Yau Quintic), 에델스브루너와 레처, 조모로디언의 지속 호몰로지와 바코드(Persistence Barcode)예요. 52판은 다섯을 더했어요. 슈람과 셰필드가 등고선을 SLE(4) 로 보인 2차원 가우스 자유장(Gaussian Free Field), 당튀와 마즈무다르, 베링거의 광탄성 힘 사슬(Force Chains), 앤스컴의 사중주와 마테이카, 피츠모리스의 같은 통계 담금질(Anscombe's Quartet), 드 브라윈 수열과 BEST 정리(de Bruijn Sequence), 라인스의 베타 평면 띠 제트(Zonal Jets)예요. 53판은 열을 더했어요. 듀크, 가르시아, 루츠가 그린 가우스 주기(Gaussian Periods), 부르가 보인 현수면과 나선면의 휨(Catenoid-Helicoid), 디랙의 벨트 묘기(Belt Trick), 헬름홀츠와 러브의 연기 고리 개구리뜀(Leapfrogging Vortices), 브룩스, 스미스, 스톤, 텃의 정사각형 분할(Squared Rectangle), 포드와 풀커슨의 최대 흐름 최소 절단(Max-Flow Min-Cut), 코스타스 배열(Costas Array), 바라니, 비어든, 칸의 무게중심 분할(Barycentric Subdivision), 무코파디아야와 크네저의 네 꼭짓점 정리(Four-Vertex Theorem), 오일러의 엘라스티카(Euler's Elastica)예요. 54판은 열을 더했어요. 베리의 무작위 파동과 보고몰니, 슈미트가 스미기로 센 마디 영역(Nodal Domains), 피어험버트가 사인 흐름에서 찾은 이상 고유 모드(Strange Eigenmode), 피어시 적분과 베리, 업스틸의 첨점 회절 파국(Pearcey Cusp), 나겔과 바이스의 STIT 모자이크(STIT Tessellation), 레비의 아크사인 법칙(Arcsine Law), 스퇴르메르가 다룬 쌍극자 장 속 대전 입자(Størmer Trap), 게일과 섀플리의 지연 수락(Stable Marriage), 스메일의 말굽(Smale Horseshoe), 울람 수열과 슈타이너베르거가 찾은 숨은 진동수(Ulam Sequence), 위어와 펠런의 거품과 켈빈의 깎은 팔면체(Weaire-Phelan)예요. 55판은 열을 더했어요. 밀러와 셰필드의 허수 기하(Imaginary Geometry), 티소의 지시 타원과 메르카토르 투영(Tissot's Indicatrix), 민코프스키의 물음표 함수(Question Mark Function), 버로스와 휠러의 변환(Burrows-Wheeler), 뷔퐁의 바늘과 바르비에의 국수(Buffon's Noodle), 부자의 리듬 타일링 캐넌(Vuza Canon), 하인과 내시의 헥스와 게일의 헥스 정리(Hex Theorem), 8자 고리의 범피복과 자유군의 케일리 그래프(Universal Cover), 오브친니코프와 젤도비치, 투생과 윌첵의 확산 제한 소멸(Toussaint-Wilczek), 헬름홀츠와 켈빈의 와류 시트와 크라스니의 와동 덩이(Vortex Sheet)예요. 56판은 열을 더했어요. 마테롱의 죽은 잎과 켄들, 퇴네스가 아래에서 본 완전 표본(Dead Leaves), 골롬의 트로미노 깔기(Golomb Trominoes), 윌슨의 고리 지운 걸음과 롤러, 슈람, 베르너의 균등 생성나무 페아노 곡선(Tree Peano Curve), 블로흐와 제너의 진동과 워니어의 사다리(Bloch Oscillations), 앤드루스의 곡선과 아시모프의 그랜드 투어(Andrews Curves), 지니브르와 기르코의 원 법칙과 좀머스, 크리산티, 솜폴린스키, 스타인의 타원 법칙(Elliptic Law), 에르되시의 불일치 문제와 보웨인, 최, 쿤스의 3진 수열(Erdős Discrepancy), 망델브로의 곱셈 폭포와 카안, 페이리에르의 문턱(Multiplicative Cascade), 코르테, 체이킨, 골럽, 파인의 무작위 조직화(Random Organization), 칼루가레아누, 화이트, 풀러의 꼬임과 뒤틀림(Twist and Writhe)이에요. 57판도 열을 더했어요. 블룸의 중심축과 들불 변환(Medial Axis), 스메일과 윌리엄스의 솔레노이드(Solenoid), 솔로몬의 무작위 환경 속 걸음과 시나이의 걸음(Sinai's Walk), 테렐과 펜로즈의 회전(Terrell Rotation), 갈페린의 π 당구(Galperin Billiard), 페르마의 두 제곱수 정리와 자기에의 한 문장 증명(Zagier's Windmills), 스토크스 표류와 게르스트너의 트로코이드 파도(Stokes Drift), 라인하르트부터 맨, 매클라우드맨, 폰 데라우까지의 볼록 오각형 타일링(Pentagon Tilings), 페로와 하위헌스의 트랙트릭스와 자전거 바퀴 자국(Bicycle Tracks), 오스트발트 숙성과 LSW 이론(Ostwald Ripening)이에요. 58판도 열을 더했어요. 야블란과 게르데스의 거울 곡선(Mirror Curves), 가운트와 피셔, 백스터의 딱딱한 정사각형 격자 기체(Hard Squares), 다니엘 베르누이의 매달린 사슬과 베셀 함수 J0(Hanging Chain), 레일리와 테일러의 불안정성(Rayleigh-Taylor), 터키의 반평면 깊이와 라도의 중심점 정리(Tukey Depth), 롤러와 베르너의 브라운 고리 수프(Loop Soup), 카돔체프-페트비아시빌리 방정식과 코다마의 선 솔리톤 웹(KP Web), 크누스가 이름 붙인 베케트-그레이 코드(Beckett-Gray), 파스칼의 신비 육각형과 슈타이너, 커크먼의 점(Hexagrammum Mysticum), 드 스밋과 렌스트라의 판화 화랑 사상(Print Gallery)이에요. 59판도 열을 더했어요. 르장드르와 가우스의 이차 상호 법칙(Quadratic Reciprocity), 위그너 함수와 유르케, 스톨러의 켜 고양이(Wigner Cat), 아비단과 샤미르의 심 카빙(Seam Carving), 슐리히팅과 노르트마르크, 에거스의 알갱이 기체 속 맥스웰의 도깨비(Granular Demon), 클렙시의 대각 3차 곡면과 케일리, 새먼의 직선 27개(Clebsch Surface), 보데르베르크의 나선 타일링(Voderberg Tiling), 자이페르트의 곡면(Seifert Surface), 호크스의 자기 흥분 점과정(Hawkes Process), 코호넨의 자기조직 지도(Kohonen Map), 레일리의 위상자 걸음과 굿맨의 스페클 통계(Laser Speckle)예요. 60판도 열을 더했어요. 마살리아가 짚은 RANDU 의 평면(RANDU Planes), 오일러의 36 장교와 보스, 슈리칸데, 파커의 10차 그레코-라틴 방진(Euler's Officers), 펜로즈와 카터의 등각 콤팩트화 도형(Penrose Diagram), 러더퍼드 산란의 포물선 그림자(Rutherford Shadow), 오일러 나선과 프레넬 적분, 코르뉴의 회절 셈(Cornu Spiral), 리처드슨의 디바이더와 망델브로의 해안선 차원(Richardson Dividers), 에농, 홀, 페익스의 1차원 자기중력 면 모형과 린든-벨의 격렬한 이완(Phase Spiral), 에르되시와 레니의 거대 성분과 아클리옵타스, 드수자, 스펜서의 폭발적 스미기(Giant Component), 허프와 두다, 하트의 직선 찾기(Hough Transform), 조르당의 곡선 정리와 심랏의 교차 세기(Jordan Curve)예요. 61판도 열을 더했어요. 세스나와 다섯 사람의 영온 무작위장 이징 눈사태(Crackling Noise), 몽주의 흙 옮기기와 매캔의 옮김 보간(Monge Transport), 파울의 덫과 마티외 방정식(Paul Trap), 그린힐의 제 무게 기둥(Greenhill Column), 흐바탈의 미술관 정리와 피스크의 세 색 증명(Art Gallery), 홀디치의 고리(Holditch Ring), 톰슨의 꺾인 역선과 헤비사이드의 움직이는 전하(Thomson Kink), 슈나이더와 바이스의 쌍성 렌즈(Binary Lens), 테일러의 되섞기(Taylor Unmixing), 브누아, 칼로, 디에네르 부부의 카나르 폭발(Canard Explosion)이에요. 62판도 열을 더했어요. 아브리코소프와 클라이너, 로스, 오틀러의 소용돌이 격자(Abrikosov Lattice), 울리크니의 청색 잡음과 빈자리와 뭉치 방법(Void and Cluster), 파인먼과 힙스의 체커보드(Feynman Checkerboard), 라도의 바쁜 비버와 마크센, 분트로크의 다섯 상태 챔피언(Busy Beaver), 폭스와 커시너, 비치의 평평한 곡면 위 직선 흐름(Veech Octagon), 리만의 미분 불가능 함수와 거버, 다위스테르마트의 복소 곡선(Riemann's Function), 헤슈와 킨즐레의 한 타일 형(Heesch Tiles), 차크라보르티와 헤이스, 보고시안의 마당 장터 모형(Yard-Sale Model), 베르그렌의 피타고라스 세 수 나무(Berggren Tree), 더브라윈과 람, 렁의 단위근 합과 밀른의 완전히 균형 잡힌 리듬(Balanced Rhythms)이에요. 63판은 다섯을 더했어요. 모팻의 모서리 소용돌이와 다네다의 사진(Moffatt Eddies), 맥스웰의 물고기 눈 렌즈(Maxwell's Fish-Eye), 이지케비치의 스파이크 신경망(Izhikevich Network), 애들러와 하소퍼, 우슬리의 들뜸 집합 오일러 지표(Excursion Sets), 심슨과 블라이스의 역설(Simpson's Paradox)이에요. 64판은 다섯을 더했어요. 암만과 베인커르의 팔각 타일링을 더 브라윈의 다중 격자로 지은 것(Ammann-Beenker), 겔판트, 카프라노프, 젤레빈스키의 아메바와 포르스베리, 파사레, 치흐의 구멍(Polynomial Amoeba), 키블과 주렉의 결함 얼기와 피시먼, 데 키아라, 칼라르코, 모리지의 지그재그 전이(Kibble-Zurek Ions), 야코비의 적분과 힐의 영속도 곡선(Hill's Regions), 스티븐슨과 카피차의 거꾸로 선 진자와 아치슨, 멀린의 인도 밧줄 묘기(Indian Rope Trick)예요. 65판은 다섯을 더했어요. 폰 노이만과 멀린스의 거품 법칙(Foam Coarsening), 베드퍼드와 맥멀런의 자기 아핀 카펫(McMullen Carpet), 클라인의 돛과 라그랑주의 주기 연분수(Klein's Sails), 야코비의 측지선과 샬의 공초점 곡면(Jacobi Geodesics), 하다마르와 리브친스키의 물방울과 코르테스의 정규화 스토크슬렛(Sedimenting Cloud)이에요. 67판은 다섯을 더했어요. 라돈의 변환과 라마찬드란, 락슈미나라야난의 램프 필터(Radon Transform), 아르틴의 브레이드 군과 폭스, 노이워스의 배치 공간(Artin Braids), 월시의 함수계와 실베스터의 행렬, 페일리의 차례(Walsh Functions), 아레프의 깜박이는 소용돌이(Blinking Vortex), 뒤팽의 사이클라이드(Dupin Cyclide)예요. 68판은 다섯을 더했어요. 들로네의 빈 외접원과 로슨의 뒤집기(Delaunay Flips), 모네로-뒤멘의 피보나치 단어 프랙탈(Fibonacci Word), 로즈의 검출 기준(Rose Criterion), 요세푸스 문제와 바셰의 셈 놀이(Josephus Problem), 레비치비타의 평행 이동과 푸코의 진자(Parallel Transport)예요. 69판은 다섯을 더했어요. 랭퍼드의 짝과 데이비스의 존재 조건, 스콜렘의 수열(Langford Pairs), 아하로노프와 봄의 효과와 베리 등의 파면 전위(Aharonov-Bohm), 페니의 놀이와 콘웨이의 선행수(Penney's Game), 휘트니와 그라우스타인의 회전수 정리(Whitney-Graustein), 슈타이너의 대칭화와 그로스의 수렴(Steiner Symmetrization)이에요. 70판은 다섯을 더했어요. 아우렌하머의 파워 다이어그램과 기타가와, 메리고, 티베르의 뉴턴 수송(Laguerre Cells), 배빗과 루윈의 헥사코드 정리(Hexachordal Theorem), 프렌켈과 콘토로바의 사슬(Frenkel-Kontorova), 에스하위스, 판 데르 벨레, 판 데르 메이르, 로서의 알갱이 라이덴프로스트 효과(Granular Leidenfrost), 카안의 가우스 곱셈 카오스(Multiplicative Chaos)예요. 71판은 다섯을 더했어요. 페니의 −1+i 진법과 데이비스, 크누스의 쌍둥이 용(Twindragon), 노바키, 한체, 벤트의 평탄 3차원 다양체와 콘웨이, 로세티의 이름(Platycosms), 크누스의 미니맥스 마스터마인드(Mastermind), 스넬슨의 텐세그리티 프리즘(Tensegrity), 아폴로니오스의 접촉 문제와 비에트의 풀이(Problem of Apollonius)예요. +주제 5 로 쓴 수학과 다음에 해 볼 수학이에요. 28판이 처음 열 개를 썼어요. 슈람-뢰브너 진화(Loewner Trace), 포드 원(Ford Circles), 털 난 공 정리(Hairy Ball), 아즈텍 다이아몬드와 북극권(Aztec Diamond), 부니모비치 경기장(Stadium Billiard), 라우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 다면체와 E8(Gosset Polytope), 콘웨이의 보고 말하기 수열(Look and Say), 위토프 게임의 그런디 값(Wythoff Nim), 쌍곡 평면의 띠 모형(Hyperbolic Band)이에요. 29판은 여섯을 더했어요. 클라인의 j 불변량(J-Invariant), 호프 올뭉치(Hopf Fibration), 다이슨 브라운 운동(Dyson Gas), 가케야 바늘 문제의 페론 나무(Perron Tree), 임계선 위의 리만 제타 함수(Zeta Loops), 길버트-섀넌-리즈 리플과 베이어-디아코니스의 일곱 번(Seven Shuffles)이에요. 30판도 여섯을 더했어요. 세 간격 정리(Three Gaps), 호프스태터 나비와 TKNN 정수(Hofstadter Butterfly), 노이만의 외부 당구(Outer Billiards), 볼드의 탄도 퇴적과 KPZ 앞면(Ballistic Deposition), 아르놀트의 미앤더(Meander), 브로드벤트와 해머슬리의 스미기(Percolation)예요. 31판도 여섯을 더했어요. 마스킷 조각의 클라인 군 극한 집합(Indra's Pearls), 리틀우드 다항식의 근(Littlewood Roots), 고든의 가우스 해자(Gaussian Moat), 아르놀트의 고양이 사상(Cat Map), 자부스키와 크루스칼의 KdV 솔리톤(KdV Solitons), 보이 곡면(Boy's Surface)이에요. ROUND.md 의 「낯선 수학 예」 여덟 가운데 쓴 것은 슈람-뢰브너 진화와 아즈텍 다이아몬드, 모듈러 형식, 호프 올뭉치, 세 간격 정리, 외부 당구 여섯이었고, 31판이 리틀우드 다항식의 근과 클라인 군의 극한 집합을 써서 여덟을 다 썼어요. 32판도 여섯을 더했어요. 탤벗 효과와 양자 카펫(Talbot Carpet), 길브레스 추측의 표(Gilbreath Table), 원 사상의 아르놀트 혀(Arnold Tongues), 콘웨이와 라딘의 핀휠 타일링(Pinwheel Tiling), 윌킨슨과 윌렘센의 침입 스미기(Invasion Percolation), 울람의 문제와 해머슬리 과정(Hammersley Lines)이에요. 33판도 여섯을 더했어요. 에인절, 홀로이드, 로믹, 비라그의 무작위 정렬 네트워크(Sorting Network), 베르트랑의 역설(Bertrand Paradox), 펜로즈의 비출 수 없는 방(Penrose Room), 당들랭 구(Dandelin Spheres), 테이트의 보로메오 고리(Borromean Rings), 골드바흐 혜성(Goldbach Comet)이에요. 34판도 여섯을 더했어요. 톰슨 문제와 오일러가 남기는 다섯 이웃 열둘(Thomson Problem), 켈빈의 배 물결(Kelvin Wake), 요네야마가 낸 와다의 호수(Lakes of Wada), 샤이데거의 강 그물(Scheidegger Rivers), 모저의 소파 문제와 해머슬리의 소파(Moving Sofa), 크누스의 춤추는 링크로 푼 펜토미노 덮기(Pentomino Cover)예요. 35판도 여섯을 더했어요. 뢸로의 폭이 일정한 곡선(Constant Width), 렌니의 주차 문제(Rényi Parking), 카리의 비주기 왕 타일(Kari-Culik Tiles), 콜라코스키 수열(Kolakoski), 쿠라모토와 바톡토흐의 키메라 상태(Chimera State), 주콥스키 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했어요. 그레이슨의 정리가 말하는 곡선 단축 흐름(Curve Shortening), 헤우드의 일곱 색 토러스 지도(Heawood Torus), 슈바르츠 랜턴(Schwarz Lantern), 윌브러햄과 깁스의 현상(Gibbs Phenomenon), 타원 곡선의 현과 접선(Elliptic Chords), 지먼의 파국 기계(Catastrophe Machine)예요. 37판도 여섯을 더했어요. 포천의 쓸기 선 알고리즘(Fortune's Sweep), 앙투안의 목걸이(Antoine's Necklace), 카르만 소용돌이 길의 안정성(Kármán Street), 단단한 원판의 헥사틱 상(Hexatic Phase), 존슨-멜 쪽매와 아브라미 법칙(Johnson-Mehl), 쿠라모토-시바신스키 방정식(Kuramoto-Sivashinsky)이에요. 38판도 여섯을 더했어요. 앤더슨 국소화(Anderson Localization), 무어가 찾고 Chenciner 와 Montgomery 가 증명한 세 몸 문제의 8자 해(Figure-Eight Orbit), 예센과 빈트너, 에르되시의 베르누이 합성곱(Bernoulli Convolutions), 브라우어르의 고정점 정리를 증명하는 슈페르너 보조정리(Sperner's Lemma), 에겐베르거와 폴리아의 항아리(Pólya Urn), 뇌르고르의 무한 수열(Infinity Series)이에요. 39판도 여섯을 더했어요. 고든, 웹, 월퍼트의 등스펙트럼 북(Isospectral Drums), 폰 망골트의 꼴로 쓴 리만의 명시 공식(Explicit Formula), 요한 베르누이의 최단 강하선(Brachistochrone), 슈타이너의 포리즘(Steiner Chain), 모런 집단으로 그린 킹먼의 합류(Kingman Coalescent), 보르수크-울람 정리(Borsuk-Ulam)예요. 40판도 여섯을 더했어요. 앨더스의 브라운 연속 랜덤 나무(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 함수(Hydrogen Orbital), 젤도비치 근사의 팬케이크(Zeldovich Pancakes), 망델브로가 내다보고 롤러, 슈람, 베르너가 증명한 브라운 운동의 바깥 경계(Brownian Frontier), 복소 긴즈부르크-란다우 방정식의 결함 난류(Defect Turbulence), 루퍼트 왕자의 정육면체(Prince Rupert's Cube)예요. 41판은 다섯을 더했어요. 홀과 왜건의 현수선 길과 정다각형 바퀴(Square Wheels), 퐁슬레의 포리즘(Poncelet Porism), 슐레플리의 4차원 정다포체를 자른 단면(Polytope Slices), 클림켁과 티먼, 마그누손의 사이트스왑(Siteswap), Dudeney 의 잡화상 퍼즐과 월리스-보여이-게르비엔 정리(Hinged Dissection)예요. 42판은 다섯을 더했어요. 바른스도르프 규칙으로 도는 기사 여행(Knight's Tour), 페르미와 파스타, 울람, 칭구의 되돌아옴(FPUT Recurrence), 포슬리에-립킨 반전기(Peaucellier Linkage), 몰리의 삼등분선 정리(Morley Triangle), 랄과 마드라스, 소칼의 피벗 알고리즘으로 돌린 자기 회피 걸음(Pivot Walk)이에요. 43판은 다섯을 더했어요. 월리스 워커의 칼레이도사이클(Kaleidocycle), 쌍곡형에서 록소드롬형으로 감기는 뫼비우스 변환의 흐름(Möbius Loxodrome), 터트의 용수철 정리(Tutte Embedding), 디펀트와 크래비츠의 고리 정리를 품은 히토메자시 사시코(Hitomezashi), 룽게 현상과 체비쇼프 점(Runge Phenomenon)이에요. 44판은 다섯을 더했어요. 마크 카츠의 고리 모형(Kac Ring), 스톤과 튜키의 햄 샌드위치 정리(Ham Sandwich), 브란트의 불가리아 솔리테어(Bulgarian Solitaire), 엘스켄스와 프리시의 탄도 소멸(Ballistic Annihilation), 페레그린의 솔리톤(Peregrine Soliton)이에요. 45판은 다섯을 더했어요. 로건-셰프와 베르시크-케로프의 극한 곡선으로 다가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도브루신 경계 조건을 건 임계 이징 모형의 SLE(3) 경계(Ising Interface), 프리예제프와 동료들의 오일러 걷개로 쌓는 로터 라우터 응집과 레빈-페레스의 스매시 합(Rotor-Router), 폴리아의 과수원 문제(Pólya's Orchard), 넬슨의 평면 색칠 문제와 아이즈벨의 일곱 색(Hadwiger-Nelson)이에요. 46판은 다섯을 더했어요. 브릴루앙의 구역(Brillouin Zones), 모스 이론과 레브의 그래프(Reeb Graph), 로렌츠 기체와 시나이의 당구(Lorentz Gas), 리언 글래스의 점무늬(Glass Patterns), 벤치와 수테라, 불피아니의 확률 공명(Stochastic Resonance)이에요. 47판은 다섯을 더했어요. 오키프와 홍, 스트로가츠의 스워멀레이터(Swarmalators), 빌스의 외로운 달리기 선수 추측(Lonely Runner), 샤츠의 올로이드와 그 전개도(Oloid), 쾨베의 원 채우기 정리와 콜린스-스티븐슨의 반복(Koebe Packing), 콘웨이의 병사와 황금비 무게(Conway's Soldiers)예요. 48판은 다섯을 더했어요. 오하라의 매듭 뫼비우스 에너지(Möbius Energy), 체비쇼프의 편향과 루빈스타인-사르낙의 밀도(Chebyshev's Bias), 섀든과 레키엔, 마스든의 이중 소용돌이 능선(Double Gyre), 아자리와 프로스트의 깜빡이는 래칫(Flashing Ratchet), 망델브로의 프랙탈 스미기(Fractal Percolation)예요. 49판은 다섯을 더했어요. 뒤러의 전개 문제와 셰퍼드의 추측(Dürer's Unfolding), 베리와 골드버그가 재규격화한 세타 합의 꼬부랑 곡선(Curlicue), 오일러 방정식과 푸앵소의 폴호드로 본 테니스 라켓 정리(Tennis Racket), 켐프의 사슬과 네 색 정리(Kempe Chains), 퇴플리츠의 내접 정사각형 문제와 그린-로브의 직사각형(Square Peg)이에요. 50판은 다섯을 더했어요. 크레이치넌과 배철러의 2차원 난류가 거꾸로 가는 캐스케이드와 맥윌리엄스의 소용돌이(Inverse Cascade), 사포발과 로소, 구예의 확산 앞줄(Diffusion Front), 필리와 마르케티, 케이츠와 타이외르의 운동성 상분리(Motility Clusters), 박과 스네펜의 진화 모형(Bak-Sneppen), 리세의 영원한 가속(Risset Rhythm)이에요. 51판은 다섯을 더했어요. 스미스와 마이어스, 캐플런, 굿먼-스트라우스의 모자 타일과 H/T/P/F 메타타일 치환(Hat Monotile), 반도와 동료들의 최적 속도 모형과 스기야마의 원형 트랙 실험(Phantom Jam), 사토와 테이트의 법칙과 타원곡선의 a_p(Sato-Tate), 칼라비와 야우의 공간에서 핸슨이 잘라 그린 5차 페르마 곡선(Calabi-Yau Quintic), 에델스브루너와 레처, 조모로디언의 지속 호몰로지와 바코드(Persistence Barcode)예요. 52판은 다섯을 더했어요. 슈람과 셰필드가 등고선을 SLE(4) 로 보인 2차원 가우스 자유장(Gaussian Free Field), 당튀와 마즈무다르, 베링거의 광탄성 힘 사슬(Force Chains), 앤스컴의 사중주와 마테이카, 피츠모리스의 같은 통계 담금질(Anscombe's Quartet), 드 브라윈 수열과 BEST 정리(de Bruijn Sequence), 라인스의 베타 평면 띠 제트(Zonal Jets)예요. 53판은 열을 더했어요. 듀크, 가르시아, 루츠가 그린 가우스 주기(Gaussian Periods), 부르가 보인 현수면과 나선면의 휨(Catenoid-Helicoid), 디랙의 벨트 묘기(Belt Trick), 헬름홀츠와 러브의 연기 고리 개구리뜀(Leapfrogging Vortices), 브룩스, 스미스, 스톤, 텃의 정사각형 분할(Squared Rectangle), 포드와 풀커슨의 최대 흐름 최소 절단(Max-Flow Min-Cut), 코스타스 배열(Costas Array), 바라니, 비어든, 칸의 무게중심 분할(Barycentric Subdivision), 무코파디아야와 크네저의 네 꼭짓점 정리(Four-Vertex Theorem), 오일러의 엘라스티카(Euler's Elastica)예요. 54판은 열을 더했어요. 베리의 무작위 파동과 보고몰니, 슈미트가 스미기로 센 마디 영역(Nodal Domains), 피어험버트가 사인 흐름에서 찾은 이상 고유 모드(Strange Eigenmode), 피어시 적분과 베리, 업스틸의 첨점 회절 파국(Pearcey Cusp), 나겔과 바이스의 STIT 모자이크(STIT Tessellation), 레비의 아크사인 법칙(Arcsine Law), 스퇴르메르가 다룬 쌍극자 장 속 대전 입자(Størmer Trap), 게일과 섀플리의 지연 수락(Stable Marriage), 스메일의 말굽(Smale Horseshoe), 울람 수열과 슈타이너베르거가 찾은 숨은 진동수(Ulam Sequence), 위어와 펠런의 거품과 켈빈의 깎은 팔면체(Weaire-Phelan)예요. 55판은 열을 더했어요. 밀러와 셰필드의 허수 기하(Imaginary Geometry), 티소의 지시 타원과 메르카토르 투영(Tissot's Indicatrix), 민코프스키의 물음표 함수(Question Mark Function), 버로스와 휠러의 변환(Burrows-Wheeler), 뷔퐁의 바늘과 바르비에의 국수(Buffon's Noodle), 부자의 리듬 타일링 캐넌(Vuza Canon), 하인과 내시의 헥스와 게일의 헥스 정리(Hex Theorem), 8자 고리의 범피복과 자유군의 케일리 그래프(Universal Cover), 오브친니코프와 젤도비치, 투생과 윌첵의 확산 제한 소멸(Toussaint-Wilczek), 헬름홀츠와 켈빈의 와류 시트와 크라스니의 와동 덩이(Vortex Sheet)예요. 56판은 열을 더했어요. 마테롱의 죽은 잎과 켄들, 퇴네스가 아래에서 본 완전 표본(Dead Leaves), 골롬의 트로미노 깔기(Golomb Trominoes), 윌슨의 고리 지운 걸음과 롤러, 슈람, 베르너의 균등 생성나무 페아노 곡선(Tree Peano Curve), 블로흐와 제너의 진동과 워니어의 사다리(Bloch Oscillations), 앤드루스의 곡선과 아시모프의 그랜드 투어(Andrews Curves), 지니브르와 기르코의 원 법칙과 좀머스, 크리산티, 솜폴린스키, 스타인의 타원 법칙(Elliptic Law), 에르되시의 불일치 문제와 보웨인, 최, 쿤스의 3진 수열(Erdős Discrepancy), 망델브로의 곱셈 폭포와 카안, 페이리에르의 문턱(Multiplicative Cascade), 코르테, 체이킨, 골럽, 파인의 무작위 조직화(Random Organization), 칼루가레아누, 화이트, 풀러의 꼬임과 뒤틀림(Twist and Writhe)이에요. 57판도 열을 더했어요. 블룸의 중심축과 들불 변환(Medial Axis), 스메일과 윌리엄스의 솔레노이드(Solenoid), 솔로몬의 무작위 환경 속 걸음과 시나이의 걸음(Sinai's Walk), 테렐과 펜로즈의 회전(Terrell Rotation), 갈페린의 π 당구(Galperin Billiard), 페르마의 두 제곱수 정리와 자기에의 한 문장 증명(Zagier's Windmills), 스토크스 표류와 게르스트너의 트로코이드 파도(Stokes Drift), 라인하르트부터 맨, 매클라우드맨, 폰 데라우까지의 볼록 오각형 타일링(Pentagon Tilings), 페로와 하위헌스의 트랙트릭스와 자전거 바퀴 자국(Bicycle Tracks), 오스트발트 숙성과 LSW 이론(Ostwald Ripening)이에요. 58판도 열을 더했어요. 야블란과 게르데스의 거울 곡선(Mirror Curves), 가운트와 피셔, 백스터의 딱딱한 정사각형 격자 기체(Hard Squares), 다니엘 베르누이의 매달린 사슬과 베셀 함수 J0(Hanging Chain), 레일리와 테일러의 불안정성(Rayleigh-Taylor), 터키의 반평면 깊이와 라도의 중심점 정리(Tukey Depth), 롤러와 베르너의 브라운 고리 수프(Loop Soup), 카돔체프-페트비아시빌리 방정식과 코다마의 선 솔리톤 웹(KP Web), 크누스가 이름 붙인 베케트-그레이 코드(Beckett-Gray), 파스칼의 신비 육각형과 슈타이너, 커크먼의 점(Hexagrammum Mysticum), 드 스밋과 렌스트라의 판화 화랑 사상(Print Gallery)이에요. 59판도 열을 더했어요. 르장드르와 가우스의 이차 상호 법칙(Quadratic Reciprocity), 위그너 함수와 유르케, 스톨러의 켜 고양이(Wigner Cat), 아비단과 샤미르의 심 카빙(Seam Carving), 슐리히팅과 노르트마르크, 에거스의 알갱이 기체 속 맥스웰의 도깨비(Granular Demon), 클렙시의 대각 3차 곡면과 케일리, 새먼의 직선 27개(Clebsch Surface), 보데르베르크의 나선 타일링(Voderberg Tiling), 자이페르트의 곡면(Seifert Surface), 호크스의 자기 흥분 점과정(Hawkes Process), 코호넨의 자기조직 지도(Kohonen Map), 레일리의 위상자 걸음과 굿맨의 스페클 통계(Laser Speckle)예요. 60판도 열을 더했어요. 마살리아가 짚은 RANDU 의 평면(RANDU Planes), 오일러의 36 장교와 보스, 슈리칸데, 파커의 10차 그레코-라틴 방진(Euler's Officers), 펜로즈와 카터의 등각 콤팩트화 도형(Penrose Diagram), 러더퍼드 산란의 포물선 그림자(Rutherford Shadow), 오일러 나선과 프레넬 적분, 코르뉴의 회절 셈(Cornu Spiral), 리처드슨의 디바이더와 망델브로의 해안선 차원(Richardson Dividers), 에농, 홀, 페익스의 1차원 자기중력 면 모형과 린든-벨의 격렬한 이완(Phase Spiral), 에르되시와 레니의 거대 성분과 아클리옵타스, 드수자, 스펜서의 폭발적 스미기(Giant Component), 허프와 두다, 하트의 직선 찾기(Hough Transform), 조르당의 곡선 정리와 심랏의 교차 세기(Jordan Curve)예요. 61판도 열을 더했어요. 세스나와 다섯 사람의 영온 무작위장 이징 눈사태(Crackling Noise), 몽주의 흙 옮기기와 매캔의 옮김 보간(Monge Transport), 파울의 덫과 마티외 방정식(Paul Trap), 그린힐의 제 무게 기둥(Greenhill Column), 흐바탈의 미술관 정리와 피스크의 세 색 증명(Art Gallery), 홀디치의 고리(Holditch Ring), 톰슨의 꺾인 역선과 헤비사이드의 움직이는 전하(Thomson Kink), 슈나이더와 바이스의 쌍성 렌즈(Binary Lens), 테일러의 되섞기(Taylor Unmixing), 브누아, 칼로, 디에네르 부부의 카나르 폭발(Canard Explosion)이에요. 62판도 열을 더했어요. 아브리코소프와 클라이너, 로스, 오틀러의 소용돌이 격자(Abrikosov Lattice), 울리크니의 청색 잡음과 빈자리와 뭉치 방법(Void and Cluster), 파인먼과 힙스의 체커보드(Feynman Checkerboard), 라도의 바쁜 비버와 마크센, 분트로크의 다섯 상태 챔피언(Busy Beaver), 폭스와 커시너, 비치의 평평한 곡면 위 직선 흐름(Veech Octagon), 리만의 미분 불가능 함수와 거버, 다위스테르마트의 복소 곡선(Riemann's Function), 헤슈와 킨즐레의 한 타일 형(Heesch Tiles), 차크라보르티와 헤이스, 보고시안의 마당 장터 모형(Yard-Sale Model), 베르그렌의 피타고라스 세 수 나무(Berggren Tree), 더브라윈과 람, 렁의 단위근 합과 밀른의 완전히 균형 잡힌 리듬(Balanced Rhythms)이에요. 63판은 다섯을 더했어요. 모팻의 모서리 소용돌이와 다네다의 사진(Moffatt Eddies), 맥스웰의 물고기 눈 렌즈(Maxwell's Fish-Eye), 이지케비치의 스파이크 신경망(Izhikevich Network), 애들러와 하소퍼, 우슬리의 들뜸 집합 오일러 지표(Excursion Sets), 심슨과 블라이스의 역설(Simpson's Paradox)이에요. 64판은 다섯을 더했어요. 암만과 베인커르의 팔각 타일링을 더 브라윈의 다중 격자로 지은 것(Ammann-Beenker), 겔판트, 카프라노프, 젤레빈스키의 아메바와 포르스베리, 파사레, 치흐의 구멍(Polynomial Amoeba), 키블과 주렉의 결함 얼기와 피시먼, 데 키아라, 칼라르코, 모리지의 지그재그 전이(Kibble-Zurek Ions), 야코비의 적분과 힐의 영속도 곡선(Hill's Regions), 스티븐슨과 카피차의 거꾸로 선 진자와 아치슨, 멀린의 인도 밧줄 묘기(Indian Rope Trick)예요. 65판은 다섯을 더했어요. 폰 노이만과 멀린스의 거품 법칙(Foam Coarsening), 베드퍼드와 맥멀런의 자기 아핀 카펫(McMullen Carpet), 클라인의 돛과 라그랑주의 주기 연분수(Klein's Sails), 야코비의 측지선과 샬의 공초점 곡면(Jacobi Geodesics), 하다마르와 리브친스키의 물방울과 코르테스의 정규화 스토크슬렛(Sedimenting Cloud)이에요. 67판은 다섯을 더했어요. 라돈의 변환과 라마찬드란, 락슈미나라야난의 램프 필터(Radon Transform), 아르틴의 브레이드 군과 폭스, 노이워스의 배치 공간(Artin Braids), 월시의 함수계와 실베스터의 행렬, 페일리의 차례(Walsh Functions), 아레프의 깜박이는 소용돌이(Blinking Vortex), 뒤팽의 사이클라이드(Dupin Cyclide)예요. 68판은 다섯을 더했어요. 들로네의 빈 외접원과 로슨의 뒤집기(Delaunay Flips), 모네로-뒤멘의 피보나치 단어 프랙탈(Fibonacci Word), 로즈의 검출 기준(Rose Criterion), 요세푸스 문제와 바셰의 셈 놀이(Josephus Problem), 레비치비타의 평행 이동과 푸코의 진자(Parallel Transport)예요. 69판은 다섯을 더했어요. 랭퍼드의 짝과 데이비스의 존재 조건, 스콜렘의 수열(Langford Pairs), 아하로노프와 봄의 효과와 베리 등의 파면 전위(Aharonov-Bohm), 페니의 놀이와 콘웨이의 선행수(Penney's Game), 휘트니와 그라우스타인의 회전수 정리(Whitney-Graustein), 슈타이너의 대칭화와 그로스의 수렴(Steiner Symmetrization)이에요. 70판은 다섯을 더했어요. 아우렌하머의 파워 다이어그램과 기타가와, 메리고, 티베르의 뉴턴 수송(Laguerre Cells), 배빗과 루윈의 헥사코드 정리(Hexachordal Theorem), 프렌켈과 콘토로바의 사슬(Frenkel-Kontorova), 에스하위스, 판 데르 벨레, 판 데르 메이르, 로서의 알갱이 라이덴프로스트 효과(Granular Leidenfrost), 카안의 가우스 곱셈 카오스(Multiplicative Chaos)예요. 71판은 다섯을 더했어요. 페니의 −1+i 진법과 데이비스, 크누스의 쌍둥이 용(Twindragon), 노바키, 한체, 벤트의 평탄 3차원 다양체와 콘웨이, 로세티의 이름(Platycosms), 크누스의 미니맥스 마스터마인드(Mastermind), 스넬슨의 텐세그리티 프리즘(Tensegrity), 아폴로니오스의 접촉 문제와 비에트의 풀이(Problem of Apollonius)예요. 73판은 다섯을 더했어요. 베리와 발라즈의 에어리 파속과 시빌로글루, 크리스토도울리데스의 유한 에너지 빔(Airy Beam), 스틴과 시배크가 반례로 실은 위상수학자의 사인 곡선(Topologist's Sine Curve), 에렌페스트 부부의 항아리와 카츠의 귀환 시간(Ehrenfest Urn), 월리스의 심슨 선과 슈타이너의 델토이드(Steiner Deltoid), 가드너가 소개한 보스의 1/f 주사위(Voss Dice)예요. 목록 밖에서 다음에 해 볼 것이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -4366,6 +4449,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 71판에서 본 것이에요. 셈이 정확한 대상은 이번에도 패턴 안에서 먼저 맞춰 봤어요. Twindragon 은 |a|, |b| ≤ 200 인 가우스 정수 160,801 개가 모두 22 자리 안에 끝나 제 값으로 돌아왔고, 22 자리까지의 2^22 가지 줄이 다 다른 수였어요. Mastermind 는 1296 가지 비밀을 다 풀어 크누스의 4.476 수와 1, 6, 62, 533, 694 판의 나뉨이 논문과 같았고, 어빙과 쿠이는 평균이 더 짧은 대신 가장 길면 6 수였어요. Problem of Apollonius 는 네 크기에서 30분씩 돌린 접촉 오차가 3.7e-9 LED 를 넘지 않았고, 무작위 세 원 40만 벌의 해의 수가 0, 4, 8 뿐이었어요. Platycosms 는 덱 변환을 11³ 큐브에서 재어 사본마다 여섯 방향에 붙임이 하나뿐이었고, didicosm 의 αβγ = 1 이 맞았어요. Tensegrity 는 프리즘 하나를 떼어 돌리자 옆 케이블을 따라 잰 비틀림이 n = 3, 4, 5, 6 에서 π/2 − π/n 에 소수 넷째 자리까지 모였어요. 정리 밖의 것은 패널에서 봤어요. 쌓은 텐세그리티의 단은 혼자일 때보다 덜 비틀려 n = 3 에서 20~24 도였고, 추를 얹으면 34~38 도로 프리즘 혼자의 30 도를 넘었어요. 아폴로니오스의 원 여덟은 늘 서명 여덟이 아니어서, 세 중심이 한 줄에 가까우면 한 식이 같은 서명 둘을 내고 반대 서명은 없어요. 여덟이 선 프레임 가운데 서명이 다 다른 것은 40% 였어요. 마스터마인드에서 비밀 없이 가장 큰 무리를 남기는 속이는 출제자를 만나면 크누스와 어빙은 300 판 모두 5 수, 쿠이는 모두 6 수였어요. +73판에서 본 것이에요. 이번 다섯은 수학이 정한 몫이 그림의 밝기를 정했어요. Topologist's Sine Curve 의 띠가 가장자리에서 밝은 것은 sin(1/x) 가 x 의 몫으로 ±1 근처에 오래 머물러서예요. 처음에는 곡선의 길이도 그렇다고 짐작했는데, 셈해 보니 길이로 세면 띠가 고르게 차서 두 셈을 넷째 노브로 섞게 했어요. Voss Dice 는 주사위를 넷에서 여덟로 늘려도 분홍 합의 한 걸음이 2.56 에서 2.61 로 거의 그대로인데, 백색의 한 걸음은 3.87 에서 5.46 으로 커져요. 그래서 주사위가 많을수록 가운데 칸만 차분해 보여요. 기다림은 기댓값처럼 고르게 오지 않았어요. Ehrenfest Urn 의 공 8 개는 600초에 기대 23.4 번 가운데 20 번 모두 위로 돌아왔는데, 63.0초와 63.2초처럼 두 걸음 사이로 붙어 오고 47초를 비우기도 했어요. 정리가 못 박는 수는 이번에도 셈으로 먼저 지켜봤어요. Steiner Deltoid 는 무작위 삼각형 200 개에서 세 발이 이루는 넓이가 1e-15 아래였고, 감싸는 곡선이 구점원 중심에서 R/2 와 3R/2 사이에 들었어요. Airy Beam 은 FFT 로 지은 맨 윗줄이 Ai 의 멱급수와 1e−5 안에서 맞았고, 구슬이 가린 로브는 아래에서 자라도 자유 빔 세기의 70% 를 넘지 못했어요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. @@ -4444,6 +4529,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 목록 밖에서 71판 뒤에 해 볼 것이에요. 윌리엄 길버트(William J. Gilbert)가 1982년 『Mathematical Intelligencer』에 그린 −n+i 진법의 타일은 Twindragon 의 체커와 세는 창을 그대로 쓰고 n 만 노브에 걸면, 쌍둥이 용이 다른 꼴의 타일로 넘어가는 것이 보여요. 바츨라프 흐바탈(Vašek Chvátal)이 1983년 『Combinatorica』에서 다룬 n 칸 n 색 마스터마인드는 Mastermind 의 격자에 칸 수만 바꿔 올려서 답 무리가 갈리는 꼴을 견줄 수 있어요. 막대 여섯의 텐세그리티 이십면체는 로버트 코넬리(Robert Connelly)와 월터 화이틀리(Walter Whiteley)의 프리스트레스 안정성(1996)으로 어느 방향이 가장 무른지 셈할 수 있어서, Tensegrity 의 시뮬레이션에 그 방향으로 누르는 노브를 달면 돼요. 데카르트의 원 정리(1643)는 서로 접한 세 원에 접하는 두 원의 곡률을 한 식으로 주어서, Problem of Apollonius 의 흰 원 셋을 서로 닿게 몰면 그 두 원을 식과 나란히 세울 수 있어요. 육각기둥을 3분의 1 바퀴와 6분의 1 바퀴로 붙이는 tricosm 과 hexacosm, 사본이 거울상으로 서는 비가향 amphicosm 은 Platycosms 의 사본 투영에 기본 영역만 바꿔 넣으면 돼요. +목록 밖에서 73판 뒤에 해 볼 것이에요. 위상수학자의 사인 곡선의 오른쪽 끝을 호로 극한 선분에 이어 닫으면 바르샤바 원이 돼요. 원처럼 한 바퀴 도는 꼴인데 그 위의 닫힌 경로는 다 한 점으로 줄어들어서, Topologist's Sine Curve 의 띠 셈과 구슬에 호 하나만 더하면 돼요. 닫는 호의 끝 (1, sin 1) 이 패널에 들도록 X 를 1 까지 넓혀야 해요. 패트릭 모란(Patrick Moran)의 과정(1958)은 걸음마다 공 하나가 사라지고 다른 공의 빛깔을 베껴 새로 나서, Ehrenfest Urn 처럼 수가 한 걸음에 많아야 하나씩 움직이는데도 0 이나 N 에 붙으면 돌아오지 않아요. 같은 두 방과 띠로 두 사슬을 나란히 세울 수 있어요. 카를 빌헬름 포이어바흐(Karl Wilhelm Feuerbach)의 정리(1822)는 구점원이 내접원과 방접원 셋에 늘 닿는다는 것이라, Steiner Deltoid 의 바뀌는 삼각형 위에서 닿는 점 넷이 구점원을 미끄러지는 것을 그릴 수 있어요. 니콜라오스 에프레미디스(Nikolaos Efremidis)와 크리스토도울리데스의 자동 집속 빔(2010)은 마주 보는 에어리 빔 둘이라, Airy Beam 의 표를 거울로 하나 더 읽으면 줄곧 어둡던 가운데가 아래쪽 한 줄에서 갑자기 밝아져요. 브누아 망델브로(Benoit Mandelbrot)와 존 밴 네스(John Van Ness)의 분수 브라운 운동(1968)은 허스트 지수 H 하나로 거칠기가 이어서 바뀌어서, Voss Dice 의 갈색 칸을 노브 하나로 거칠게도 매끄럽게도 할 수 있어요. + ### 타이포 주제 7 로 해 본 글자 실험과 다음에 해 볼 것이에요. 63판과 64판이 같이 돌며 다섯씩 처음 만들었어요. 63판의 다섯은 한글 자모가 날아와 모아쓰기로 잠기는 조립(Jamo Assembly), 놀턴과 하먼의 기호 모자이크(Type Mosaic), 호프스태터의 트립렛 그림자(Trip-let), 카프리카의 계산 종이(Kaprekar's Routine), 캐럴의 낱말 사다리(Word Ladder)예요. @@ -4502,7 +4589,7 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 기능 감은 이번에 Lab 안에서 해 볼 수 없던 크기와 모양으로 모였어요. Quousque Tandem 은 Text 레이어 크기 하한이 4 LED 라 글자가 1 LED 아래로 줄어 회색 결이 되는 것을 Lab 에서 해 볼 수 없어서, 크기를 0.5 까지 내리고 덮임을 그대로 밝기로 쓰는 길을 바랐어요. Eight Principles of Yong 은 12~16 LED 한자 픽셀 글꼴을 바랐어요. 사이트 글꼴 넷에는 한자가 없고 system 글꼴은 보는 기기마다 달라요. Photon Disk 는 Text 레이어를 한 점을 중심으로 돌리거나 원 둘레에 앉히는 길을 바랐어요. 67판과 68판이 바란 90도 돌린 글줄을 원으로 넓힌 셈이에요. Ink Traps 는 코드가 Text 레이어의 글자 비트맵을 읽어 Lab 에서 친 낱말을 잉크로 번지게 하기를 바랐어요. 63판부터 여러 판이 바란 길과 같아요. Hellschreiber 는 미리보기를 LED 몇 개 크기로 크게 보는 돋보기를 바랐어요. 글자가 어느 줄에서 꺾이는지 미리보기 크기로는 보이지 않아서, 띠를 10배로 잘라 PNG 로 뽑아 보고서야 BRWNN 을 찾았어요. -70판 뒤에 해 볼 것이에요. 매튜 카터가 1996년 마이크로소프트에 그린 Verdana 는 종이 대신 화면 픽셀에 맞춘 글자라, Ink Traps 의 볼록 조각 글자와 4×4 표본 래스터를 작은 크기로 돌리면 픽셀 격자에 맞춘 획과 맞추지 않은 획을 위아래 견본으로 견줄 수 있어요. 왕희지의 「난정서(蘭亭序)」(353년)에는 之 자가 스무 번 나오고 하나도 같게 쓰지 않았다고 전해요. Eight Principles of Yong 의 붓과 붓털 문턱, 마디 시간표로 之 스무 벌을 세로로 쌓을 수 있어요. 루돌프 헬의 디지셋(Digiset, 1965)은 원판 대신 음극선관이 글자를 주사선으로 그려 필름에 찍었어요. Photon Disk 의 필름 띠와 섬광 자리에 글자를 한 줄씩 긋는 빔을 두면 돼요. 피에르시몽 푸르니에(Pierre-Simon Fournier)의 『Manuel typographique』(1764)는 활자 크기를 포인트로 맞춘 표를 실었어요. Quousque Tandem 의 넓이 합 표로 등비 대신 그 표의 크기들을 짜면 크기 사이가 고르지 않은 견본이 서요. 새뮤얼 모스(Samuel Morse)의 1844년 워싱턴–볼티모어 선은 기록기가 점과 선을 종이 테이프에 눌러 찍었어요. Hellschreiber 의 띠와 양식지, 잡음을 그대로 쓰면 같은 메시지가 점과 선으로 찍힌 띠가 헬의 글자 띠와 나란히 붙어요. 이번에 골랐다가 짜지 않은 것도 있어요. 어도비의 멀티플 마스터(Adobe Multiple Master, 1991)는 굵기와 너비 같은 축의 끝에 마스터 글꼴을 두고 그 사이를 보간해서, 노브 둘을 두 축에 걸면 한 글자가 설계 공간을 미끄러지는 「노브와 글」이 돼요. +70판 뒤에 해 볼 것이에요. 매튜 카터가 1996년 마이크로소프트에 그린 Verdana 는 종이 대신 화면 픽셀에 맞춘 글자라, Ink Traps 의 볼록 조각 글자와 4×4 표본 래스터를 작은 크기로 돌리면 픽셀 격자에 맞춘 획과 맞추지 않은 획을 위아래 견본으로 견줄 수 있어요. 왕희지의 「난정서(蘭亭序)」(353년)에는 之 자가 스무 번 나오고 하나도 같게 쓰지 않았다고 전해요. Eight Principles of Yong 의 붓과 붓털 문턱, 마디 시간표로 之 스무 벌을 세로로 쌓을 수 있어요. 루돌프 헬의 디지셋(Digiset, 1965)은 원판 대신 음극선관이 글자를 주사선으로 그려 필름에 찍었어요. Photon Disk 의 필름 띠와 섬광 자리에 글자를 한 줄씩 긋는 빔을 두면 돼요. 피에르시몽 푸르니에(Pierre-Simon Fournier)의 『Manuel typographique』(1764)는 활자 크기를 포인트로 맞춘 표를 실었어요. Quousque Tandem 의 넓이 합 표로 등비 대신 그 표의 크기들을 짜면 크기 사이가 고르지 않은 견본이 서요. 새뮤얼 모스(Samuel Morse)의 1844년 워싱턴–볼티모어 선은 기록기가 점과 선을 종이 테이프에 눌러 찍었어요. Hellschreiber 의 띠와 양식지, 잡음을 그대로 쓰면 같은 메시지가 점과 선으로 찍힌 띠가 헬의 글자 띠와 나란히 붙어요. 71판의 다섯은 원기둥 거울에 비쳐야 바로 서는 바닥 글(Catoptric Anamorphosis), 폼페이 낱말 네모의 25자가 날아 PATERNOSTER 십자가가 되는 판(Sator Square), 큰 본 글자를 따라 돌면 작고 기울고 홀쭉한 펀치를 깎는 링크(Benton Pantograph), 읽을 수 없는 지은 글자를 세로줄로 찍는 쉬빙의 책(Book from the Sky), 키케로의 문장에서 글자가 떨어져 자리채움 글이 되는 정렬(Lorem Ipsum)이에요. @@ -4510,7 +4597,15 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 기능 감은 이번에도 글자를 코드 밖에서 받는 길로 모였고, 받으려는 것이 글자의 자리와 꼴로 갈렸어요. Sator Square 는 Text 레이어가 글자마다 제 비트맵과 자리를 넘겨 주기를, Lorem Ipsum 은 정한 폭으로 줄을 나누고 양쪽을 맞춘 글자의 자리를 넘겨 주기를 바랐어요. Benton Pantograph 는 Lab 글꼴의 윤곽을 점 목록으로 받기를 바랐어요. Catoptric Anamorphosis 는 코드가 고른 상자와 해상도에 그린 덮임 배열을 바랐는데, 거울에 비칠 낱말을 LED 하나당 4×4 로 그려야 해서예요. Book from the Sky 는 기기 글꼴의 한자 비트맵을 받아 지어낸 글자가 실제 글자와 겹치는지 견주기를 바랐어요. -71판 뒤에 해 볼 것이에요. 장루이 볼즈자르(Jean-Louis Vaulezard)가 1630년에 다룬 원뿔 거울 착시는 원뿔 꼭짓점 위에서 내려다볼 때만 둥근 바닥 그림이 낱말로 모여서, Catoptric Anamorphosis 의 광선을 원뿔에서 꺾고 카메라를 위에 두면 돼요. 퀸투스 세레누스 사모니쿠스의 『Liber Medicinalis』(3세기 무렵)가 부적으로 적게 한 ABRACADABRA 삼각형은 줄마다 끝 글자를 하나씩 떼어 열한 줄로 줄어서, Sator Square 의 글꼴 세 벌로 128줄을 위에서 아래로 쓸 수 있어요. 알브레히트 뒤러의 『측정 지침』(1525)은 자와 컴퍼스로 로마 대문자를 짓는 법이라, Benton Pantograph 의 윤곽 다각형 대신 원호와 직선이 차례로 그어지며 글자가 서는 판이 돼요. 쉬빙이 1990년대 중반부터 쓴 영문 방체 서법(Square Word Calligraphy)은 영어 낱말의 알파벳을 한자처럼 한 네모에 모아 읽히게 써서, Book from the Sky 의 틀 여덟에 부품 대신 라틴 글자를 넣으면 같은 쪽에서 읽히는 가짜 한자가 나와요. 블라디미르 레벤시테인(Vladimir Levenshtein)의 편집 거리(1965)처럼 바꾸기를 허락하면 Lorem Ipsum 에서 잘리고 더해지던 글자 쌍이 제자리에서 다른 글자로 바뀌어 더미로 가는 글자가 줄어요. +71판 뒤에 해 볼 것이에요. 장루이 볼즈자르(Jean-Louis Vaulezard)가 1630년에 다룬 원뿔 거울 착시는 원뿔 꼭짓점 위에서 내려다볼 때만 둥근 바닥 그림이 낱말로 모여서, Catoptric Anamorphosis 의 광선을 원뿔에서 꺾고 카메라를 위에 두면 돼요. 쉬빙이 1990년대 중반부터 쓴 영문 방체 서법(Square Word Calligraphy)은 영어 낱말의 알파벳을 한자처럼 한 네모에 모아 읽히게 써서, Book from the Sky 의 틀 여덟에 부품 대신 라틴 글자를 넣으면 같은 쪽에서 읽히는 가짜 한자가 나와요. 블라디미르 레벤시테인(Vladimir Levenshtein)의 편집 거리(1965)처럼 바꾸기를 허락하면 Lorem Ipsum 에서 잘리고 더해지던 글자 쌍이 제자리에서 다른 글자로 바뀌어 더미로 가는 글자가 줄어요. + +73판의 다섯은 줄마다 한 글자씩 빠져 128줄을 내려가는 부적 삼각형(Abracadabra), 네 마스터의 점을 섞어 굵기와 폭을 노브로 연주하는 낱말(Multiple Master), 정사각형 안에서 자와 컴퍼스로 짓는 로마 대문자(Dürer's Alphabet), 한 가지 실로 짠 천에서 빛의 방향만으로 드러나는 낱말(Damask), 점 다섯이 막대로 녹고 다 찬 자리가 윗자리로 오르는 20진 셈(Maya Numerals)이에요. + +73판에서 본 것이에요. 이번에도 다섯이 다 글자 꼴을 코드에 적었고, 옮겨 쓴 글꼴은 없어요. 노브로 글자를 바꾸는 판에서는 먹의 양보다 가장자리가 옮겨 가는 거리가 화면을 더 바꿨어요. Multiple Master 의 먹은 굵기 축에서 131%, 폭 축에서 31% 느는데, 노브 반응은 Width 가 0.12 로 Weight 의 0.08 보다 커요. 폭은 윗줄 두 끝을 12.5 LED 씩 옮기고, 굵기는 획 가장자리만 제자리에서 두껍게 하기 때문이에요. 빛으로 드러나는 글은 빛이 지나가는 선을 따라 뒤집혔어요. Damask 에서 두 실의 반짝임이 같아지는 곳은 램프 발밑을 지나는 45도 직선이라, 세로로 쌓은 LUX 는 맨 아래 X 가 먼저 뒤집히고 맨 위 L 이 마지막에 뒤집혀요. 빠진 글자도 글이 됐어요. Abracadabra 의 계단 옆 그을음을 발치에서 위로 읽으면 B R A C A D A B R A 라서, 빠진 열 자가 첫 A 만 빼고 주문을 한 번 더 써요. 기호로 세는 셈은 수의 크기가 아니라 들어오는 기호 수만큼 시간이 들었어요. Maya Numerals 에서 360 을 더하는 것은 장기력에서 셋째 자리 점 하나라 Speed 2 에서 1초에 1.68 번인데, 20진에서는 둘째 자리 막대 셋과 점 셋이라 0.31 번이에요. 짓는 글자는 짓는 차례가 자국으로 남았어요. Dürer's Alphabet 에서 종이가 가장 늦게 밝아지는 자리는 C 의 속공간 한가운데로, 5초쯤 앞서 컴퍼스 바늘이 섰던 자리예요. + +기능 감은 이번에 둘로 갈렸어요. 셋은 Text 레이어가 시간과 노브를 따라 바뀌기를 바랐어요. Multiple Master 는 Inter 가 가변 글꼴이라 브라우저에 이미 있을 wght 축을 노브에 걸기를, Dürer's Alphabet 은 글자 목록을 시간이나 노브로 넘기기를, Maya Numerals 는 코드 레이어가 Text 레이어의 글을 프레임마다 바꿔 넣기를 바랐어요. 둘은 Text 레이어의 글자를 다른 꼴로 받기를 바랐어요. Damask 는 글자를 거리장으로 받아 덮는 몫에 맞춰 굵기를 고르기를 바랐어요. Abracadabra 는 크기와 따로 줄 간격을 정하는 칸을 바랐는데, 지금은 열한 줄을 128줄에 고루 펴려면 글자 크기가 11.64 여야 해요. + +73판 뒤에 해 볼 것이에요. 바빌로니아의 60진 쐐기 숫자는 세운 쐐기 아홉 다음 열이 갈고리 하나가 되고 갈고리 여섯이 윗자리 쐐기 하나가 돼서, Maya Numerals 의 받아 올림을 그대로 쓸 수 있어요. 고대 바빌로니아 숫자에는 처음에 0 기호가 없어서 빈 자리를 어떻게 보일지가 숙제예요. 한글 한 자를 마스터 넷으로 그려 Multiple Master 처럼 섞으면 굵기 축에서 ㅁ 과 ㅇ 의 속공간이 어느 차례로 닫히는지 보여요. Dürer's Alphabet 의 정사각형과 컴퍼스로 ㅇ 과 ㅎ 의 원, ㄱ 과 ㄴ 의 꺾임을 긋고 모아쓴 한 글자의 몸에 잉크를 흘리면 한글 짓기가 돼요. 레온 글래스(Leon Glass)의 짝 점 무늬(1969)는 점마다 짝 점을 조금 돌린 자리에 찍는데, 낱말 안에서만 돌리면 점 하나하나에는 무늬가 없고 짝끼리의 결이 글자로 읽혀요. Glass Patterns 의 셈으로 Damask 와 다른 길의 숨은 글을 짤 수 있어요. ### 화장품 팝업