From f88496440e6615cd359836d3ac5e0d659aa1c3c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:47:24 +0000 Subject: [PATCH 01/24] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=2068=ED=8C=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 92c6dd5ef64ad11a8d1f4b8a4429ccdf397f9410 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:20:41 +0000 Subject: [PATCH 02/24] incoming: Ogham, pattern round 68 --- incoming/pattern-study/patterns/ogham.js | 374 +++++++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 incoming/pattern-study/patterns/ogham.js diff --git a/incoming/pattern-study/patterns/ogham.js b/incoming/pattern-study/patterns/ogham.js new file mode 100644 index 000000000..824203b59 --- /dev/null +++ b/incoming/pattern-study/patterns/ogham.js @@ -0,0 +1,374 @@ +// ===== Patternflow pattern ===== +// Title: Ogham +// Family: type +// Idea: A standing stone is seen corner-on with its edge running up the whole panel as the stemline, and an Ogham inscription is cut into it stroke by stroke from the bottom up, one to five strokes a letter, the B aicme on the right face, the H aicme on the left, the M aicme slanting across the edge from one face onto the other and the vowels notched on the edge itself, while a small Latin letter beside each group counts its strokes as they land (B, L, F, S, then N for five on the right) and the view climbs the stone with the chisel. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the Ogham alphabet of four aicmí of five letters as the Ogam tract of the Book of Ballymote sets it out (George Calder, Auraicept na n-Éces, 1917), its Old Irish letter names and the MAQI and MUCOI of the Primitive Irish memorial stones (Damian McManus, A Guide to Ogam, 1991); Typewriter's 3 by 5 capitals (pattern round 18); pattern round 68 +// Round: 68 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Text=0..3, Turn=-1..1 +// +// Knob 1: Hue. Turns the light round the wheel: at 0 the cuts are pale gold on blue-grey slate, at 0.5 pale blue on a brown sandstone, the gloss letters a whiter tint of the cuts. +// Knob 2: Speed. Strokes cut a second: 5 at the opening 2, so the alphabet's 60 strokes and 20 letters take 16 seconds before the stone holds 3 seconds with its top in view and a fresh one is started; 0.8 at 0.2, 13.6 at 3. +// Knob 3: Text. Which inscription, each on its own stone: 0 the twenty letters in their four aicmí, 60 strokes over 302 rows of stone; 1 the four aicme names BEITHE HUATHA MUINE AILME, 59 strokes of which 40 are vowel notches; 2 the formula words ANM MAQI MUCOI, 34 strokes; 3 the Old Irish names of the first aicme, BEITHE LUIS FERN SAIL NIN, 76 strokes over 365 rows. +// Knob 4: Turn. Turns the stone about its upright axis, 13 degrees at the opening 0.06 so the left face shows wider, 40 degrees each way at the ends: at -1 the right face comes round to you and the left one shrinks to a sliver, at 1 the reverse, and the strokes on the turning face shorten while the slanting M strokes kink at the edge. + +const W = 64, H = 128; +const { floor, ceil, round, min, max, abs, sqrt, sin, cos, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- The alphabet. Four aicmí ("families") of five letters, each letter +// one to five strokes: the B aicme to the right of the stemline as it is +// read upward, the H aicme to the left, the M aicme slanting across it, the +// vowels notched on it. On a stone the stemline is the arris itself. The +// third letter is F as the manuscripts give fern; the oldest stones wrote V +// with it. +const AICME = [ + ["B", "L", "F", "S", "N"], + ["H", "D", "T", "C", "Q"], + ["M", "G", "NG", "Z", "R"], + ["A", "O", "U", "E", "I"], +]; +const WHERE = {}; +AICME.forEach((a, g) => a.forEach((L, i) => (WHERE[L] = [g, i + 1]))); +// The inscriptions, a word to a group of letters. The alphabet in aicme +// order; the aicmí named after their first letters; the words of the +// memorial stones, ANM ("name") that opens some and the MAQI ("son of") and +// MUCOI ("of the kin of") of X MAQI Y MUCOI Z, without any names; the Old +// Irish names of the first five letters. +const TEXTS = [ + ["BLFSN", "HDTCQ", "MGNGZR", "AOUEI"], + ["BEITHE", "HUATHA", "MUINE", "AILME"], + ["ANM", "MAQI", "MUCOI"], + ["BEITHE", "LUIS", "FERN", "SAIL", "NIN"], +]; +const parse = (w) => { + const out = []; + for (let i = 0; i < w.length; i++) { + if (w[i] === "N" && w[i + 1] === "G") out.push("NG"), i++; + else out.push(w[i]); + } + return out; +}; + +// Typewriter's 3 by 5 capitals (pattern round 18), the letters the gloss needs. +const FONT = { + A: "010101111101101", B: "110101110101110", C: "011100100100011", D: "110101101101110", E: "111100110100111", + F: "111100110100100", G: "011100101101011", H: "101101111101101", I: "111010010010111", L: "100100100100111", + M: "101111111101101", N: "110101101101101", O: "010101101101010", Q: "010101101111011", R: "110101110101101", + S: "011100010001110", T: "111010010010010", U: "101101101101011", Z: "111001010100111", +}; + +// ---- The stone: a square pillar seen corner-on, its axis at column CX, its +// half diagonal one of RS, a stone to each text; heights v in rows, the +// view climbing the stone with the chisel a whole row at a time so every +// cut lands on one row of LEDs. +const CX = 35.5, RS = [24, 22.5, 25.5, 23.5], CAM0 = -10, VOFF = 40; +const PITCH = 3, LGAP = 8, WGAP = 18, BASE = 9; +// How far each aicme's strokes reach along a face, in face units (one +// unit is one LED when the face is square to you), and the M slant. +const REACH = [12.5, 12.5, 8.5, 2.3], SLANT = 0.42, CUT = 0.62; + +const hash = (a, b, c) => { + let h = (a * 374761393 + b * 668265263 + c * 1013904223) | 0; + h = Math.imul(h ^ (h >>> 13), 1274126177); + h = Math.imul(h ^ (h >>> 16), 2246822519); + return ((h ^ (h >>> 13)) >>> 0) / 4294967296; +}; + +function newStone(S, ti) { + const letters = [], cuts = []; + let v = BASE - LGAP, t = 0.3; + TEXTS[ti].forEach((word, wi) => { + if (wi) (v += WGAP - LGAP), (t += 1.4); + for (const L of parse(word)) { + const [g, n] = WHERE[L]; + v += LGAP; + if (letters.length) t += 0.9; + const v0 = v, first = cuts.length; + for (let i = 0; i < n; i++) { + if (i) v += PITCH; + cuts.push({ g, v, t0: t, li: letters.length }); + t += 1; + } + letters.push({ L, g, n, mid: (v0 + v) / 2, first }); + } + }); + S.letters = letters; + S.cuts = cuts; + S.end = t + 0.4; + S.top = v + 20; + S.r = RS[ti]; + S.side = S.r * Math.SQRT2; + // Each face's grain: coarse blotches, a finer mottle and single-LED + // speckle, and the jagged line of its top, highest at the edge. + const U = ceil(S.side) + 2, V = S.top + VOFF + 30; + S.U = U; + S.V = V; + S.tex = [0, 1].map((f) => { + const T = new Float32Array(U * V); + const val = (gx, gy, sc, k) => { + const x = gx / sc, y = gy / sc, x0 = floor(x), y0 = floor(y), fx = x - x0, fy = y - y0; + const sx = fx * fx * (3 - 2 * fx), sy = fy * fy * (3 - 2 * fy); + const a = hash(x0, y0, k), b = hash(x0 + 1, y0, k), c = hash(x0, y0 + 1, k), d = hash(x0 + 1, y0 + 1, k); + return a + (b - a) * sx + (c - a) * sy + (a - b - c + d) * sx * sy; + }; + const seed = ti * 4 + f * 2 + 1; + for (let y = 0; y < V; y++) + for (let x = 0; x < U; x++) + T[y * U + x] = 0.62 + 0.42 * val(x, y, 9, seed) + 0.26 * val(x, y, 3, seed + 40) + 0.2 * hash(x, y, seed + 80); + return T; + }); + S.tops = [0, 1].map((f) => { + const T = new Float32Array(U); + for (let u = 0; u < U; u++) T[u] = S.top - 12 * (u / S.side) ** 2 - 2.5 * hash(u >> 1, f, ti + 9); + return T; + }); + S.t = 0; + S.cam = CAM0; + S.phase = 0; + S.parts = []; + S.ci = 0; +} + +export function setup(params) { + const S = (params.s = { ti: -1, hue: 0, rate: 5, turn: 0, dim: 1, emit: 0 }); + S.Cov = new Float32Array(W * H); + S.Fre = new Float32Array(W * H); + S.Sh = new Float32Array(W * H); + S.F = new Float32Array(W * H * 3); + S.colF = new Int8Array(W); + S.colU = new Float32Array(W); +} + +// The cut in progress: the last one whose time has come. +function current(S) { + const c = S.cuts; + let i = S.ci || 0; + if (i >= c.length || c[i].t0 > S.t) i = 0; + while (i + 1 < c.length && c[i + 1].t0 <= S.t) i++; + S.ci = i; + return i; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + S.rate = 5 * exp(cl(k[1], 0.2, 3) - 2); + S.turn = cl(k[3] || 0, -1, 1); + const ti = round(cl(k[2] || 0, 0, 3)); + if (ti !== S.ti) { + S.ti = ti; + newStone(S, ti); + } + // Carve, hold the finished stone with its top in view, darken, and + // start again on a fresh one. + let target; + if (S.phase === 0) { + S.t += dt * S.rate; + S.dim = min(1, S.dim + dt / 0.5); + const c = S.cuts[current(S)]; + target = c.v + (c.g === 2 ? REACH[2] * SLANT : 0) - 80; + if (S.t >= S.end) (S.phase = 1), (S.hold = 0); + } else { + target = S.top - 112; + S.hold += dt; + if (S.hold > 3) S.dim -= dt / 0.7; + if (S.dim <= 0) { + S.dim = 0; + newStone(S, S.ti); + } + } + S.cam += (max(CAM0, target) - S.cam) * (1 - exp(-dt * 2.5)); + // Stone dust off the chisel, in panel columns and stone rows. + const P = S.parts; + for (const p of P) { + p.x += p.vx * dt; + p.v += p.vv * dt; + p.vv -= 60 * dt; + p.life -= dt; + } + if (P.length && P[0].life <= 0) S.parts = P.filter((p) => p.life > 0); + if (S.tip) { + S.emit += dt * 30; + while (S.emit >= 1) { + S.emit -= 1; + const sd = S.tip[2]; + S.parts.push({ x: S.tip[0], v: S.tip[1], vx: sd * (6 + 14 * Math.random()) + 6 * (Math.random() - 0.5), vv: 4 + 14 * Math.random(), life: 0.3 + 0.4 * Math.random() }); + } + } +} + +// One straight piece of a cut as a capsule of radius rad, into the cover +// buffer, its freshness alongside and its shadow a row above it. +function seg(S, a, b, rad, fresh) { + const [ax, ay] = a, [bx, by] = b; + const x0 = max(0, floor(min(ax, bx) - rad - 1)), x1 = min(W - 1, ceil(max(ax, bx) + rad + 1)); + const y0 = max(1, floor(min(ay, by) - rad - 1)), y1 = min(H - 1, ceil(max(ay, by) + rad + 1)); + const dx = bx - ax, dy = by - ay, L2 = dx * dx + dy * dy; + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const px = x + 0.5 - ax, py = y + 0.5 - ay; + const u = L2 ? cl((px * dx + py * dy) / L2, 0, 1) : 0; + const ex = px - u * dx, ey = py - u * dy; + const c = cl(rad + 0.5 - sqrt(ex * ex + ey * ey), 0, 1); + if (c <= 0) continue; + const o = y * W + x; + if (c > S.Cov[o]) S.Cov[o] = c; + if (c * fresh > S.Fre[o]) S.Fre[o] = c * fresh; + if (c > S.Sh[o - W]) S.Sh[o - W] = c; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.cuts) return; + const { Cov, Fre, Sh, F, colF, colU } = S; + Cov.fill(0); + Fre.fill(0); + Sh.fill(0); + F.fill(0); + // The edge and the far corners across the panel, and how much of a face + // unit each face shows: cos(45 degrees + turn) on the right, cos(45 - turn) + // on the left. + const th = 0.7 * S.turn + 0.18 * (1 - S.turn * S.turn), r = S.r, side = S.side, ve = round(S.cam) + 64; + const sF = CX + r * sin(th), sR = CX + r * cos(th), sL = CX - r * cos(th); + const cs = [cos(PI / 4 + th), -cos(PI / 4 - th)]; + const P = (f, u, v) => [sF + u * cs[f], 63.5 - (v - ve)]; + // Each panel column: its face and how far along it. + for (let x = 0; x < W; x++) { + const X = x + 0.5, f = X >= sF && X < sR ? 0 : X < sF && X >= sL ? 1 : -1; + colF[x] = f; + if (f >= 0) colU[x] = (X - sF) / cs[f]; + } + const lit = (a) => 0.36 + 0.64 * max(0, (-0.55 * sin(a) + 0.8 * cos(a)) / 0.971); + const shade = [lit(th + PI / 4), lit(th - PI / 4)]; + const hue = S.hue; + const stone = hsv(hue + 0.58, 0.3, 84), cutC = hsv(hue + 0.13, 0.42, 245), gloss = hsv(hue + 0.13, 0.18, 230); + const sky = hsv(hue + 0.62, 0.7, 1); + const Ye = (v) => 63.5 - (v - ve); + + // ---- The cuts. Strokes start at the edge and run out along a face; an + // M stroke runs from the left face across the edge onto the right one. + const ci = S.phase === 0 ? current(S) : -1; + S.tip = null; + for (let i = 0; i < S.cuts.length; i++) { + const c = S.cuts[i]; + if (c.t0 > S.t) break; + const yc = Ye(c.v); + if (yc < -12 || yc > H + 12) continue; + const p = cl((S.t - c.t0) / CUT, 0, 1); + const fresh = p < 1 ? 1 : exp(-(S.t - c.t0 - CUT) / S.rate / 0.9); + const L = REACH[c.g], E = P(0, 0, c.v); + let tip, sd = 1; + if (c.g < 2) { + tip = P(c.g, L * p, c.v); + sd = c.g ? -1 : 1; + seg(S, E, tip, 0.5, fresh); + } else if (c.g === 2) { + const a = P(1, L, c.v - L * SLANT), e = P(1, 0, c.v); + if (p <= 0.5) { + tip = P(1, L * (1 - 2 * p), c.v - L * SLANT * (1 - 2 * p)); + sd = -1; + seg(S, a, tip, 0.5, fresh); + } else { + tip = P(0, L * (2 * p - 1), c.v + L * SLANT * (2 * p - 1)); + seg(S, a, e, 0.5, fresh); + seg(S, E, tip, 0.5, fresh); + } + } else { + tip = P(0, L * p, c.v); + seg(S, P(1, L * p, c.v), tip, 0.5, fresh); + } + if (i === ci && p < 1) S.tip = [tip[0], ve + 63.5 - tip[1], sd]; + } + + // ---- Stone, sky and the cuts over them. + const top0 = S.tops; + for (let x = 0; x < W; x++) { + const f = colF[x], u = colU[x], ui = f < 0 ? 0 : min(S.U - 1, max(0, floor(u))); + // Lighter towards the edge, which stands nearest, and the edge itself + // catching the light along its whole height. + const e = abs(x + 0.5 - sF), edge = e < 1.3 ? 0.45 * (1.3 - e) : 0, near = f < 0 ? 0 : 1 - 0.3 * (u / side) ** 1.5; + for (let y = 0; y < H; y++) { + const o = y * W + x; + const v = ve + 63 - y; + let R, G, B; + if (f < 0 || v > top0[f][ui]) { + const s = 3 + 7 * (y / H); + R = sky[0] * s; + G = sky[1] * s; + B = sky[2] * s; + } else { + const vi = cl(floor(v) + VOFF, 0, S.V - 1); + let l = shade[f] * near * S.tex[f][vi * S.U + ui] + edge; + l *= 1 - 0.55 * Sh[o] * (1 - Cov[o]); + const c = Cov[o], fr = Fre[o], cc = (0.7 + 0.3 * shade[f]) * (1 - fr); + R = stone[0] * l * (1 - c) + c * (cutC[0] * cc + 255 * fr); + G = stone[1] * l * (1 - c) + c * (cutC[1] * cc + 255 * fr); + B = stone[2] * l * (1 - c) + c * (cutC[2] * cc + 250 * fr); + } + F[o * 3] = R; + F[o * 3 + 1] = G; + F[o * 3 + 2] = B; + } + } + const add = (x, y, c, a) => { + x = floor(x); + y = floor(y); + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + F[o] += c[0] * a; + F[o + 1] += c[1] * a; + F[o + 2] += c[2] * a; + }; + + // ---- The chisel point and its dust. + if (S.tip) { + const tx = S.tip[0], ty = Ye(S.tip[1]), fl = 0.75 + 0.25 * Math.random(); + for (let dy = -3; dy <= 3; dy++) + for (let dx = -3; dx <= 3; dx++) { + const d = sqrt((dx + 0.5 - (tx - floor(tx))) ** 2 + (dy + 0.5 - (ty - floor(ty))) ** 2); + const a = cl(1 - d / 3, 0, 1); + if (a > 0) add(tx + dx, ty + dy, [255, 245, 220], a * a * fl); + } + } + for (const p of S.parts) add(p.x, Ye(p.v), gloss, 0.8 * min(1, p.life / 0.3)); + + // ---- The gloss: beside each letter, the letter its strokes spell so + // far, so a five-stroke N reads B, L, F, S and then N as it is cut. + for (const Lt of S.letters) { + const yc = Ye(Lt.mid); + if (yc < -4 || yc > H + 4) continue; + let done = 0, last = -1; + for (let j = 0; j < Lt.n; j++) { + const c = S.cuts[Lt.first + j]; + if (S.t >= c.t0 + CUT) (done = j + 1), (last = c.t0 + CUT); + } + if (!done) continue; + const key = AICME[Lt.g][done - 1], age = (S.t - last) / S.rate; + const a = done === Lt.n ? 0.75 + 0.25 * exp(-age / 0.6) : 1; + const col = done === Lt.n ? gloss : [255, 250, 240]; + let x0 = 4 - ((key.length * 4 - 1) >> 1); + const y0 = round(yc - 3); + for (const ch of key) { + const b = FONT[ch]; + for (let j = 0; j < 15; j++) if (b[j] === "1") add(x0 + (j % 3), y0 + floor(j / 3), col, a); + x0 += 4; + } + } + + const g0 = S.dim; + for (let y = 0, o = 0; y < H; y++) + for (let x = 0; x < W; x++, o += 3) display.setPixel(x, y, round(min(255, F[o] * g0)), round(min(255, F[o + 1] * g0)), round(min(255, F[o + 2] * g0))); +} From 677887c7700c1cba6df5e13f3076f56ccddcccf7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:21:45 +0000 Subject: [PATCH 03/24] incoming: Delaunay Flips, pattern round 68 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JktahMn4mzzFyveZ8fo --- .../pattern-study/patterns/delaunay-flips.js | 458 ++++++++++++++++++ 1 file changed, 458 insertions(+) create mode 100644 incoming/pattern-study/patterns/delaunay-flips.js diff --git a/incoming/pattern-study/patterns/delaunay-flips.js b/incoming/pattern-study/patterns/delaunay-flips.js new file mode 100644 index 000000000..d7262ee79 --- /dev/null +++ b/incoming/pattern-study/patterns/delaunay-flips.js @@ -0,0 +1,458 @@ +// ===== Patternflow pattern ===== +// Title: Delaunay Flips +// Family: cells +// Idea: Points inside a rounded hull start in a bad triangulation, a fan from one corner or a sweep, and Lawson's flips take the worst illegal edge, draw the circle that has a fourth point inside it and swap the edge for the other diagonal, a dozen a second, until no triangle's circle holds a point, every triangle filled by its smallest angle from magenta slivers to teal near-equilateral ones; then the points drift and the flips keep coming. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Lawson flips (Charles L. Lawson, 1977) toward the Delaunay triangulation (Boris Delaunay, 1934), pattern round 68 +// Round: 68 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Points=30..90, Circles=0..1 +// +// Knob 1: Hue. Turns the colours round a wheel that skips red, orange and yellow. At 0 slivers, illegal edges and circles with a point inside are magenta, near-equilateral triangles, legal edges and empty circles teal, and the triangles between go violet and blue; at 0.25 slivers are green and good triangles blue, at 0.5 cyan and magenta, at 0.75 blue and green. +// Knob 2: Speed. Flips 6 times Speed a second for 43 points and in proportion for more: at 2 the opening fan of 43 points needs 76 flips and is Delaunay 6.4 s after it is drawn; at 0.2 one flip every 0.83 s, each circle in plain view. In the drift that follows the points move 1.4 times Speed LEDs a second, less as they crowd, so 113 points move 0.62 of that. +// Knob 3: Points. 30 to 90 points scattered inside, plus 13 to 23 fixed on the hull, so 43 points and 71 triangles at 30 and 113 points and 201 triangles at 90. Each whole step scatters a new set; the opening fan of 113 points took 324 flips, Delaunay 10.3 s after it was drawn at Speed 2. +// Knob 4: Circles. Up to 0.08 only the circle under test and the fading circles of the last flips. Above it a growing share of all the triangles' circumcircles, all of them at 1: magenta while some point lies inside, teal when empty, so the bad start is a tangle of magenta circles and lines and the end is teal. +// Buttons: K1 tap scatters a new set in the next bad triangulation. + +// Delaunay (1934): a triangulation of a point set where no point lies inside +// any triangle's circumcircle. Lawson (1977): an edge shared by triangles +// abc and dcb is illegal when d lies inside abc's circle, which is when the +// angles at a and d add up past pi. Swapping it for ad raises the smallest +// of the six angles, and flipping illegal edges in any order ends at the +// Delaunay triangulation. The hull points never move, so the hull stays +// convex and its edges are never flipped. + +const W = 64, H = 128; +const { floor, ceil, round, min, max, abs, sqrt, sin, cos, atan2, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sg = (v) => (v < 0 ? -1 : 1); +const frac = (v) => v - floor(v); +// A colour wheel that skips red, orange and yellow (hue 0.98 round to 0.27), +// which go maroon, brown and olive at the dim levels of the fills. +const wheel = (p) => 0.27 + 0.71 * p; + +// The hull: a superellipse |x/a|^3 + |y/b|^3 = 1 round the panel's middle, +// of area AREA. Interior points keep 5 LEDs inside it. +const CX = 31.5, CY = 63.5, HA = 30.6, HB = 62.6, SP = 3, AREA = 6769; +const NMAX = 90 + 32, TMAX = 2 * NMAX; +const BUILD = 0.9, PROOF = 1.6, DRIFT = 10, ICEPS = 1e-6; +const inner = (x, y) => abs((x - CX) / (HA - 5)) ** SP + abs((y - CY) / (HB - 5)) ** SP < 1; + +function hsv(h, s, v, out) { + h = frac(h) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// ---- Predicates. Every stored triangle has orient > 0. ---- +const orient = (P, a, b, c) => + (P[2 * b] - P[2 * a]) * (P[2 * c + 1] - P[2 * a + 1]) - (P[2 * b + 1] - P[2 * a + 1]) * (P[2 * c] - P[2 * a]); +// > 0 when d lies inside the circle through a, b, c (orient(a, b, c) > 0). +function incircle(P, a, b, c, d) { + const dx = P[2 * d], dy = P[2 * d + 1]; + const ax = P[2 * a] - dx, ay = P[2 * a + 1] - dy, bx = P[2 * b] - dx, by = P[2 * b + 1] - dy; + const cx = P[2 * c] - dx, cy = P[2 * c + 1] - dy; + const a2 = ax * ax + ay * ay, b2 = bx * bx + by * by, c2 = cx * cx + cy * cy; + return ax * (by * c2 - b2 * cy) - ay * (bx * c2 - b2 * cx) + a2 * (bx * cy - by * cx); +} +// The angle at a in the triangle a, b, c. +function angle(P, a, b, c) { + const ux = P[2 * b] - P[2 * a], uy = P[2 * b + 1] - P[2 * a + 1], vx = P[2 * c] - P[2 * a], vy = P[2 * c + 1] - P[2 * a + 1]; + return atan2(abs(ux * vy - uy * vx), ux * vx + uy * vy); +} +// The circle through a, b, c: centre x, centre y, radius. +function circum(P, a, b, c, out) { + const ax = P[2 * a], ay = P[2 * a + 1]; + const bx = P[2 * b] - ax, by = P[2 * b + 1] - ay, cx = P[2 * c] - ax, cy = P[2 * c + 1] - ay; + const d = 2 * (bx * cy - by * cx), b2 = bx * bx + by * by, c2 = cx * cx + cy * cy; + const ux = (cy * b2 - by * c2) / d, uy = (bx * c2 - cx * b2) / d; + out[0] = ax + ux; + out[1] = ay + uy; + out[2] = sqrt(ux * ux + uy * uy); +} + +// ---- A new point set: the hull points first, in order round it. ---- +function scatter(S) { + const P = S.P, n = S.n, nh = (S.nh = 8 + round(n / 6)); + // Hull points evenly by arc length. + const M = 720, xs = [], ys = [], L = [0]; + for (let i = 0; i <= M; i++) { + const th = (i / M) * 2 * PI, c = cos(th), s = sin(th); + xs.push(CX + HA * sg(c) * abs(c) ** (2 / SP)); + ys.push(CY + HB * sg(s) * abs(s) ** (2 / SP)); + if (i) L.push(L[i - 1] + sqrt((xs[i] - xs[i - 1]) ** 2 + (ys[i] - ys[i - 1]) ** 2)); + } + const off = Math.random(); + for (let k = 0, j = 0; k < nh; k++) { + const at = ((k + off) / nh) * L[M]; + while (j < M - 1 && L[j + 1] < at) j++; + const f = (at - L[j]) / (L[j + 1] - L[j]); + P[2 * k] = xs[j] + (xs[j + 1] - xs[j]) * f; + P[2 * k + 1] = ys[j] + (ys[j + 1] - ys[j]) * f; + } + // Interior points by dart throwing, no two closer than dmin. + let dmin = (S.dmin = 0.72 * sqrt(AREA / (n + nh))), np = nh, tries = 0; + while (np < nh + n) { + const x = 1 + Math.random() * 62, y = 1 + Math.random() * 126; + if (++tries > 3000) (dmin *= 0.9), (tries = 0); + if (!inner(x, y)) continue; + let ok = true; + for (let j = 0; j < np && ok; j++) ok = (P[2 * j] - x) ** 2 + (P[2 * j + 1] - y) ** 2 >= dmin * dmin; + if (!ok) continue; + P[2 * np] = x; + P[2 * np + 1] = y; + S.th[np++] = Math.random() * 2 * PI; + } + S.np = np; +} + +// ---- Bad triangulations, as flat lists of triangles. ---- +// A fan from hull point pv: the others by angle round it, a wedge between +// each two neighbours, then a Graham scan along the fan's rim closes the +// pockets between the rim and the hull. +function fan(S, pv) { + const P = S.P, ids = [], tris = []; + const d2 = (i) => (P[2 * i] - P[2 * pv]) ** 2 + (P[2 * i + 1] - P[2 * pv + 1]) ** 2; + for (let i = 0; i < S.np; i++) if (i !== pv) ids.push(i); + ids.sort((p, q) => -orient(P, pv, p, q) || d2(p) - d2(q)); + for (let k = 0; k + 1 < ids.length; k++) tris.push(pv, ids[k], ids[k + 1]); + const st = [ids[0], ids[1]]; + for (let k = 2; k < ids.length; k++) { + const q = ids[k]; + for (let m = st.length; m >= 2 && orient(P, st[m - 2], st[m - 1], q) < 0; m--) tris.push(st[m - 2], q, st.pop()); + st.push(q); + } + return tris; +} +// A sweep: the points by distance from (qx, qy). Each new one lies outside +// the hull of those before it and is joined to every hull edge it sees. +function sweep(S, qx, qy) { + const P = S.P, ids = []; + const d2 = (i) => (P[2 * i] - qx) ** 2 + (P[2 * i + 1] - qy) ** 2; + for (let i = 0; i < S.np; i++) ids.push(i); + ids.sort((p, q) => d2(p) - d2(q)); + let hull = orient(P, ids[0], ids[1], ids[2]) > 0 ? ids.slice(0, 3) : [ids[0], ids[2], ids[1]]; + const tris = hull.slice(); + for (let k = 3; k < ids.length; k++) { + const q = ids[k], m = hull.length, vis = hull.map((h, j) => orient(P, h, hull[(j + 1) % m], q) < 0); + let s = 0, c = 0; + while (s < m && !(vis[s] && !vis[(s + m - 1) % m])) s++; + if (s === m) continue; + for (; c < m && vis[(s + c) % m]; c++) tris.push(hull[(s + c + 1) % m], hull[(s + c) % m], q); + const next = [hull[s], q]; + for (let j = (s + c) % m; j !== s; j = (j + 1) % m) next.push(hull[j]); + hull = next; + } + return tris; +} + +// Store the triangles and find each one's neighbours. Nb[3t + i] is the +// triangle across t's side opposite its vertex i, or -1 on the hull. +function link(S, tris) { + const V = S.V, Nb = S.Nb, T = (S.T = tris.length / 3), seen = new Map(); + for (let i = 0; i < tris.length; i++) V[i] = tris[i]; + Nb.fill(-1); + for (let e = 0; e < 3 * T; e++) { + const t = (e / 3) | 0, b = V[3 * t + ((e + 1) % 3)], c = V[3 * t + ((e + 2) % 3)]; + const key = min(b, c) * 1024 + max(b, c), o = seen.get(key); + if (o === undefined) seen.set(key, e); + else (Nb[e] = (o / 3) | 0), (Nb[o] = t); + } +} + +// Fans from the corners top left and bottom right take turns with sweeps +// from the middle and from far above (which goes row by row). +function restart(S) { + scatter(S); + const P = S.P, kind = S.cycle % 4; + if (kind % 2) link(S, kind === 1 ? sweep(S, CX, CY) : sweep(S, CX, -4000)); + else { + const s = kind ? 1 : -1; + let pv = 0; + for (let i = 1; i < S.nh; i++) if (s * ((P[2 * i] - P[2 * pv]) / HA + (P[2 * i + 1] - P[2 * pv + 1]) / HB) > 0) pv = i; + link(S, fan(S, pv)); + } + Object.assign(S, { phase: 0, pt: 0, prog: 0, cand: -1, built: 0, flips: 0 }); + S.rec.length = 0; +} + +// ---- Edges and flips. Edge e = 3t + i is t's side opposite vertex i. ---- +// Into q: a (t's vertex i), b, c (the shared side), d across it, u, and +// d's place j in u. +function quad(S, e, q) { + const V = S.V, t = (e / 3) | 0, i = e % 3, u = S.Nb[e]; + if (u < 0) return false; + let j = 0; + while (S.Nb[3 * u + j] !== t) j++; + q[0] = V[e]; + q[1] = V[3 * t + ((i + 1) % 3)]; + q[2] = V[3 * t + ((i + 2) % 3)]; + q[3] = V[3 * u + j]; + q[4] = u; + q[5] = j; + return true; +} +function illegal(S, e) { + const q = S.q, P = S.P; + if (!quad(S, e, q)) return false; + const [a, b, c, d] = q; + return incircle(P, a, b, c, d) > ICEPS && orient(P, a, b, d) > 0 && orient(P, d, c, a) > 0; +} +// The illegal edge whose two opposite angles add up to the most. +function pick(S) { + const P = S.P, q = S.q; + let best = -1, score = PI; + for (let e = 0; e < 3 * S.T; e++) { + if (S.Nb[e] < ((e / 3) | 0) || !illegal(S, e)) continue; + const s = angle(P, q[0], q[1], q[2]) + angle(P, q[3], q[2], q[1]); + if (s > score) (score = s), (best = e); + } + return best; +} +// t = a b c and u = d c b become t = a b d and u = d c a. +function flip(S, e) { + const V = S.V, Nb = S.Nb, q = S.q; + quad(S, e, q); + const [a, b, c, d, u, j] = q, t = (e / 3) | 0, i = e % 3; + const nca = Nb[3 * t + ((i + 1) % 3)], nab = Nb[3 * t + ((i + 2) % 3)]; + const nbd = Nb[3 * u + ((j + 1) % 3)], ndc = Nb[3 * u + ((j + 2) % 3)]; + circum(S.P, a, b, c, S.cc); + S.rec.push({ a, d, b, c, t, u, at: S.t, cx: S.cc[0], cy: S.cc[1], r: S.cc[2] }); + if (S.rec.length > 24) S.rec.shift(); + V.set([a, b, d], 3 * t); + Nb.set([nbd, u, nab], 3 * t); + V.set([d, c, a], 3 * u); + Nb.set([nca, t, ndc], 3 * u); + for (let k = 0; k < 3; k++) { + if (nbd >= 0 && Nb[3 * nbd + k] === u) Nb[3 * nbd + k] = t; + if (nca >= 0 && Nb[3 * nca + k] === t) Nb[3 * nca + k] = u; + } + S.flips++; +} + +// ---- Drift: the interior points wander and push off close neighbours; a +// step that would fold a triangle over is taken back. ---- +function drift(S, d) { + const P = S.P, Q = S.Q, V = S.V, v0 = 1.4 * S.speed * sqrt(43 / S.np), nh = S.nh, dm = S.dmin * 0.8; + Q.set(P); + for (let i = nh; i < S.np; i++) { + const x = P[2 * i], y = P[2 * i + 1]; + let th = S.th[i] + (Math.random() - 0.5) * 5 * sqrt(d); + if (!inner(x, y)) th = atan2(CY - y, CX - x) + Math.random() - 0.5; + S.th[i] = th; + Q[2 * i] += cos(th) * v0 * d; + Q[2 * i + 1] += sin(th) * v0 * d; + } + for (let e = 0; e < 3 * S.T; e++) { + const t = (e / 3) | 0; + if (S.Nb[e] <= t) continue; + const a = V[3 * t + ((e + 1) % 3)], b = V[3 * t + ((e + 2) % 3)]; + const ex = P[2 * b] - P[2 * a], ey = P[2 * b + 1] - P[2 * a + 1], l = sqrt(ex * ex + ey * ey); + if (l >= dm || l < 1e-6) continue; + const f = ((dm - l) / l) * 0.6 * d * S.speed; + if (a >= nh) (Q[2 * a] -= ex * f), (Q[2 * a + 1] -= ey * f); + if (b >= nh) (Q[2 * b] += ex * f), (Q[2 * b + 1] += ey * f); + } + for (let pass = 0, bad = true; bad; pass++) { + bad = false; + if (pass === 6) Q.set(P); + else + for (let t = 0; t < S.T; t++) { + if (orient(Q, V[3 * t], V[3 * t + 1], V[3 * t + 2]) > 0.4) continue; + for (let k = 3 * t; k < 3 * t + 3; k++) (Q[2 * V[k]] = P[2 * V[k]]), (Q[2 * V[k] + 1] = P[2 * V[k] + 1]); + bad = true; + } + } + P.set(Q); +} + +export function setup(params) { + params.s = { + t: 0, hue: 0, speed: 2, circ: 0.06, n: 0, nh: 0, np: 0, cycle: 0, dmin: 8, btn: false, + P: new Float64Array(2 * NMAX), Q: new Float64Array(2 * NMAX), th: new Float64Array(NMAX), + V: new Int32Array(3 * TMAX), Nb: new Int32Array(3 * TMAX), T: 0, + phase: 0, pt: 0, prog: 0, cand: -1, built: 0, flips: 0, rec: [], q: [0, 0, 0, 0, 0, 0], + acc: new Float32Array(W * H * 3), fl: new Float32Array(TMAX), c: [0, 0, 0], cc: [0, 0, 0], + }; + return params.s; +} + +// Phases: 0 the bad triangulation drawn in, 1 flipping, 2 every circle +// shown empty, 3 drifting with flips, then a new set. +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 30, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + S.speed = cl(k[1], 0.2, 3); + S.circ = cl(k[3], 0, 1); + const n = round(cl(k[2], 30, 90)), btn = !!(input.btnPressed && input.btnPressed[0]); + if (btn && !S.btn && S.n) S.cycle++, restart(S); + S.btn = btn; + if (n !== S.n) (S.n = n), restart(S); + S.t += d; + S.pt += d; + if (S.phase === 0) { + S.built = min(S.T, floor((S.T * S.pt) / BUILD)); + if (S.pt >= BUILD) (S.phase = 1), (S.pt = 0), (S.built = S.T); + return; + } + if (S.phase === 2) { + if (S.pt >= PROOF) (S.phase = 3), (S.pt = 0); + return; + } + if (S.phase === 3) { + if (S.pt >= DRIFT) return S.cycle++, restart(S); + drift(S, d); + } + if (S.cand >= 0 && !illegal(S, S.cand)) S.cand = -1; + if (S.cand < 0) (S.cand = pick(S)), (S.prog = 0); + if (S.cand >= 0) S.prog += (6 * S.speed * S.np * d) / 43; + for (; S.prog >= 1 && S.cand >= 0; S.prog--) flip(S, S.cand), (S.cand = pick(S)); + if (S.cand < 0) { + S.prog = 0; + if (S.phase === 1) (S.phase = 2), (S.pt = 0); + } +} + +// ---- Drawing into acc, a float RGB buffer. ---- +function put(acc, x, y, c, w, sum) { + if (x < 0 || x >= W || y < 0 || y >= H || w <= 0) return; + const k = (y * W + x) * 3; + for (let i = 0; i < 3; i++) acc[k + i] = sum ? acc[k + i] + c[i] * w : max(acc[k + i], c[i] * w); +} +// A line covering two LEDs across it, in proportion (Wu's way). +function line(acc, x0, y0, x1, y1, c, w) { + const st = abs(x1 - x0) < abs(y1 - y0); + if (st) [x0, y0, x1, y1] = [y0, x0, y1, x1]; + if (x1 < x0) [x0, y0, x1, y1] = [x1, y1, x0, y0]; + const s = x1 > x0 ? (y1 - y0) / (x1 - x0) : 0; + for (let x = max(0, ceil(x0)); x <= min(st ? H - 1 : W - 1, floor(x1)); x++) { + const y = y0 + (x - x0) * s, iy = floor(y), f = y - iy; + if (st) put(acc, iy, x, c, w * (1 - f)), put(acc, iy + 1, x, c, w * f); + else put(acc, x, iy, c, w * (1 - f)), put(acc, x, iy + 1, c, w * f); + } +} +// A circle: by rows where it runs steep, by columns where it runs flat. +function ring(acc, cx, cy, r, c, w) { + if (!(r > 0.5 && r < 1e5)) return; + const h = r * 0.7072; + for (let y = max(0, ceil(cy - h)); y <= min(H - 1, floor(cy + h)); y++) { + const dx = sqrt(max(0, r * r - (y - cy) ** 2)); + for (const x of [cx - dx, cx + dx]) { + const ix = floor(x), f = x - ix; + put(acc, ix, y, c, w * (1 - f), 1), put(acc, ix + 1, y, c, w * f, 1); + } + } + for (let x = max(0, ceil(cx - h)); x <= min(W - 1, floor(cx + h)); x++) { + const dy = sqrt(max(0, r * r - (x - cx) ** 2)); + for (const y of [cy - dy, cy + dy]) { + const iy = floor(y), f = y - iy; + put(acc, x, iy, c, w * (1 - f), 1), put(acc, x, iy + 1, c, w * f, 1); + } + } +} +// Triangle t filled by rows through the LED centres. +function fill(S, t, c) { + const P = S.P, V = S.V, acc = S.acc, xs = [], ys = []; + for (let k = 0; k < 3; k++) xs.push(P[2 * V[3 * t + k]]), ys.push(P[2 * V[3 * t + k] + 1]); + for (let y = max(0, ceil(min(...ys))); y <= min(H - 1, floor(max(...ys))); y++) { + let lo = W, hi = -1; + for (let e = 0; e < 3; e++) { + const ya = ys[e], yb = ys[(e + 1) % 3]; + if ((ya - y) * (yb - y) > 0 || ya === yb) continue; + const x = xs[e] + ((y - ya) * (xs[(e + 1) % 3] - xs[e])) / (yb - ya); + (lo = min(lo, x)), (hi = max(hi, x)); + } + for (let x = max(0, ceil(lo)); x <= min(W - 1, floor(hi)); x++) acc.set(c, (y * W + x) * 3); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.T) return; + const P = S.P, V = S.V, Nb = S.Nb, acc = S.acc, c = S.c, cc = S.cc, q = S.q, fl = S.fl; + const pw = frac(0.831 + S.hue), pc = frac(0.324 + S.hue), hw = wheel(pw), hc = wheel(pc); + const built = S.phase === 0 ? S.built : S.T; + acc.fill(0); + // The two triangles a flip has just made light up for a moment. + fl.fill(0); + for (const r of S.rec) fl[r.t] = fl[r.u] = max(fl[r.t], exp(-(S.t - r.at) / 0.3)); + // Each triangle by its smallest angle m: below 10 degrees bright in the + // sliver colour, from 50 degrees dim in the near-equilateral one. + for (let t = 0; t < built; t++) { + const a = V[3 * t], b = V[3 * t + 1], d = V[3 * t + 2]; + const m = min(angle(P, a, b, d), angle(P, b, d, a), angle(P, d, a, b)), u = cl(m / 0.87, 0, 1); + hsv(wheel(pw + (pc - pw) * u), 0.8, 0.2 + 0.36 * (1 - u) * (1 - u), c); + for (let i = 0; i < 3; i++) c[i] += fl[t] * 0.35; + fill(S, t, c); + } + // Edges: legal ones pale, illegal ones in the sliver colour, the hull dim. + for (let e = 0; e < 3 * built; e++) { + const t = (e / 3) | 0, u = Nb[e]; + if (u >= 0 && u < t) continue; + const a = V[3 * t + ((e + 1) % 3)], b = V[3 * t + ((e + 2) % 3)]; + if (u < 0) hsv(hc, 0.4, 0.4, c); + else if (illegal(S, e)) hsv(hw, 0.55, 0.85, c); + else hsv(hc, 0.2, 0.32, c); + line(acc, P[2 * a], P[2 * a + 1], P[2 * b], P[2 * b + 1], c, 1); + } + // The circumcircles the Circles knob asks for. + const show = (S.circ - 0.08) / 0.92, dim = min(1, sqrt(71 / S.T)); + for (let t = 0; t < S.T && show > 0 && S.phase; t++) { + const w = dim * cl((show - frac(t * 0.618034 + 0.13)) / 0.04, 0, 1); + if (w <= 0) continue; + const a = V[3 * t], b = V[3 * t + 1], d = V[3 * t + 2]; + let hit = false; + for (let p = 0; p < S.np && !hit; p++) hit = p !== a && p !== b && p !== d && incircle(P, a, b, d, p) > ICEPS; + circum(P, a, b, d, cc); + hsv(hit ? hw : hc, hit ? 0.5 : 0.45, hit ? 0.36 : 0.3, c); + ring(acc, cc[0], cc[1], cc[2], c, w); + } + // The Delaunay moment: every circle at once, every one of them empty. + if (S.phase === 2) { + hsv(hc, 0.4, 0.45 * sin((PI * S.pt) / PROOF) ** 2, c); + for (let t = 0; t < S.T; t++) { + circum(P, V[3 * t], V[3 * t + 1], V[3 * t + 2], cc); + ring(acc, cc[0], cc[1], cc[2], c, 1); + } + } + // Flips just made: the old edge and its circle fade, the new edge flashes. + for (const r of S.rec) { + const age = S.t - r.at; + if (age > 1.5) continue; + hsv(hw, 0.5, 1, c); + line(acc, P[2 * r.b], P[2 * r.b + 1], P[2 * r.c], P[2 * r.c + 1], c, exp(-age / 0.25)); + ring(acc, r.cx, r.cy, r.r, c, 0.45 * exp(-age / 0.3)); + line(acc, P[2 * r.a], P[2 * r.a + 1], P[2 * r.d], P[2 * r.d + 1], [1, 1, 1], exp(-age / 0.45)); + } + // The edge under test, its circle brightening toward the flip, and the + // point inside that circle. + if (S.cand >= 0 && quad(S, S.cand, q)) { + const g = 0.45 + 0.55 * S.prog; + circum(P, q[0], q[1], q[2], cc); + hsv(hw, 0.3, 1, c); + ring(acc, cc[0], cc[1], cc[2], c, g); + line(acc, P[2 * q[1]], P[2 * q[1] + 1], P[2 * q[2]], P[2 * q[2] + 1], c, 1); + ring(acc, P[2 * q[3]], P[2 * q[3] + 1], 2.2, c, 0.6 * g); + } + // The points, white inside and tinted on the hull. + for (let i = 0; i < S.np; i++) { + const x = P[2 * i], y = P[2 * i + 1], ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + if (i < S.nh) hsv(hc, 0.35, 0.8, c); + else c[0] = c[1] = c[2] = 1; + put(acc, ix, iy, c, min(1, 1.6 * (1 - fx) * (1 - fy))); + put(acc, ix + 1, iy, c, min(1, 1.6 * fx * (1 - fy))); + put(acc, ix, iy + 1, c, min(1, 1.6 * (1 - fx) * fy)); + put(acc, ix + 1, iy + 1, c, min(1, 1.6 * fx * fy)); + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const k = (y * W + x) * 3; + display.setPixel(x, y, (255 * min(1, acc[k])) | 0, (255 * min(1, acc[k + 1])) | 0, (255 * min(1, acc[k + 2])) | 0); + } +} From a9b2a6b946162d246c308cd22710dcbf1312b58d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:22:01 +0000 Subject: [PATCH 04/24] incoming: Parallel Transport, pattern round 68 --- .../patterns/parallel-transport.js | 404 ++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 incoming/pattern-study/patterns/parallel-transport.js diff --git a/incoming/pattern-study/patterns/parallel-transport.js b/incoming/pattern-study/patterns/parallel-transport.js new file mode 100644 index 000000000..d084d9231 --- /dev/null +++ b/incoming/pattern-study/patterns/parallel-transport.js @@ -0,0 +1,404 @@ +// ===== Patternflow pattern ===== +// Title: Parallel Transport +// Family: space +// Idea: A lit globe turns on the upper half and a gold arrow is carried round a loop on it without ever turning, leaving ghost arrows behind and tinting the area its loop has swept, and when the loop closes the arrow stands beside its white starting self turned by exactly that area, which a dial on the lower half has been adding up all the way round. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: parallel transport (Tullio Levi-Civita, 1917), its holonomy as enclosed area by Gauss-Bonnet (Carl Friedrich Gauss, 1827; Pierre Ossian Bonnet, 1848), Foucault's pendulum (Léon Foucault, 1851), pattern round 68 +// Round: 68 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Loop=0..3, Size=-1..1 +// +// Knob 1: Hue. Turns every colour round the wheel together: at 0 the globe is blue, the arrow gold, the swept area rose and the area swept backwards sea green; the globe keeps to the hues from green to violet so it never dims to brown. +// Knob 2: Speed. At 2 the arrow goes round its loop in 6 s, then holds 2.6 s and fades 0.6 s before it starts again; at 0.2 a lap takes 60 s, at 3 it takes 4 s. +// Knob 3: Loop. 0 the triangle from the north pole down a meridian, along the equator and back up another, which turns the arrow by its angle at the pole; 1 a circle of latitude, which turns it by 2 pi (1 - sin latitude), the same as 2 pi sin latitude the other way, the turn of Foucault's pendulum in a sidereal day; 2 a small geodesic square, whose four corners are each a little over 90 degrees and turn it by their excess; 3 two circles touching at a point, the first run counterclockwise and the second clockwise, so their areas cancel. +// Knob 4: Size. The triangle's angle at the pole from 10 to 170 degrees, 90 at the opening 0.06; the latitude from 8 degrees (turn 309.9) to 85 (turn 1.4), 48.85 at 0.06, the Pantheon in Paris, turn 88.9; the square's centre to corner from 2 degrees (turn 0.14) to 30 (turn 32.8), 8 at 0.06, turn 2.2; the two circles 11 and 33 degrees at -1 (turn -51.5), both 22 at 0.06 (turn 0), 33 and 11 at 1 (turn 51.5). + +const W = 64, H = 128, TOP = 64; +const { floor, ceil, min, max, abs, sqrt, sin, cos, atan2, acos, asin, exp, log, round, PI } = Math; +const TAU = 2 * PI, DEG = PI / 180; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const fr = (x) => x - floor(x); +const wrap = (a) => a - TAU * round(a / TAU); + +function hsv(h, s, v, o) { + h = fr(h) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + o[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + o[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; + return o; +} + +// Points and tangent vectors are plain [x, y, z] on the unit sphere, z north. +const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +const crs = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; +const nrm = (a) => { + const l = sqrt(dot(a, a)) || 1; + return [a[0] / l, a[1] / l, a[2] / l]; +}; +const lin = (a, ka, b, kb) => [a[0] * ka + b[0] * kb, a[1] * ka + b[1] * kb, a[2] * ka + b[2] * kb]; +const sph = (lat, lon) => [cos(lat) * cos(lon), cos(lat) * sin(lon), sin(lat)]; + +// Parallel transport of a tangent vector v at a along the great circle to b. +// It is the rotation about a x b that takes a to b, so it is exact for any +// step; a loop sampled at 900 points is carried exactly round the geodesic +// polygon through them. +function carry(a, b, v) { + const d = dot(v, b) / (1 + dot(a, b)); + return [v[0] - d * (a[0] + b[0]), v[1] - d * (a[1] + b[1]), v[2] - d * (a[2] + b[2])]; +} +// Signed angle from u to v about the outward normal n, counterclockwise as +// seen from outside the sphere. +const ang = (u, v, n) => atan2(dot(crs(u, v), n), dot(u, v)); + +// Pieces of a loop: a great arc from a to b, or an arc of the small circle of +// angular radius r about c, from x through the signed angle sweep. +function gArc(a, b) { + const w = acos(cl(dot(a, b), -1, 1)), s = sin(w); + return { len: w, at: (t) => lin(a, sin((1 - t) * w) / s, b, sin(t * w) / s) }; +} +function cArc(c, r, x, sweep) { + const e1 = nrm(lin(x, 1, c, -dot(x, c))), e2 = crs(c, e1); + return { len: abs(sweep) * sin(r), at: (t) => lin(c, cos(r), lin(e1, cos(sweep * t), e2, sin(sweep * t)), sin(r)) }; +} + +// Size runs -1..1 and opens at 0.06; pivot puts lo, mid, hi at -1, 0.06, 1. +const pivot = (u, lo, mid, hi) => (u < 0.06 ? mid + ((lo - mid) * (0.06 - u)) / 1.06 : mid + ((hi - mid) * (u - 0.06)) / 0.94); + +const KS = 900, NB = 720; + +// The loop, the arrow carried round it, the gauge it is measured against and +// the area swept so far. The gauge at a point is the start arrow carried +// back to the fan centre c and out again along great circles; the angle from +// the gauge to the carried arrow is then the holonomy of the closed path +// c, start, along the loop, here, c, which is the area of the fan from c over +// the loop so far. At the end the gauge is the start arrow itself. +function build(S, which, u) { + const N = [0, 0, 1]; + let pieces, c, rb, want; + if (which === 0) { + const th = pivot(u, 10, 90, 170) * DEG, A = sph(0, -th / 2), B = sph(0, th / 2); + pieces = [gArc(N, A), gArc(A, B), gArc(B, N)]; + c = nrm(lin(lin(N, 1, A, 1), 1, B, 1)); + rb = acos(dot(c, A)), want = th; + } else if (which === 1) { + const ph = pivot(u, 8, 48.85, 85) * DEG; + pieces = [cArc(N, PI / 2 - ph, sph(ph, -0.6), TAU)]; + c = N, rb = PI / 2 - ph, want = TAU * (1 - sin(ph)); + } else if (which === 2) { + const rho = exp(pivot(u, log(2), log(8), log(30))) * DEG, la = 0.35; + c = sph(la, 0); + const e1 = [-sin(la), 0, cos(la)], e2 = crs(c, e1); + const V = (a) => lin(c, cos(rho), lin(e1, cos(a), e2, sin(a)), sin(rho)); + const q = PI / 4, M = nrm(lin(V(3 * q), 1, V(5 * q), 1)); + pieces = [gArc(M, V(5 * q)), gArc(V(5 * q), V(7 * q)), gArc(V(7 * q), V(q)), gArc(V(q), V(3 * q)), gArc(V(3 * q), M)]; + rb = rho, want = 8 * Math.atan(1 / cos(rho)) - TAU; + } else { + const d = pivot(u, -0.5, 0, 0.5), r1 = 22 * DEG * (1 + d), r2 = 22 * DEG * (1 - d), P = sph(0.3, 0), E = [0, 1, 0]; + const c1 = lin(P, cos(r1), E, sin(r1)), c2 = lin(P, cos(r2), E, -sin(r2)), F = cArc(c1, r1, P, -PI / 2).at(1); + // From the top of the first circle a quarter round to P, the second + // circle clockwise, then the rest of the first. + pieces = [cArc(c1, r1, F, PI / 2), cArc(c2, r2, P, -TAU), cArc(c1, r1, P, 1.5 * PI)]; + c = P, rb = 2 * max(r1, r2), want = TAU * (cos(r2) - cos(r1)); + } + let L = 0; + for (const p of pieces) L += p.len; + const ns = pieces.map((p) => max(4, round((KS * p.len) / L))); + const K = ns.reduce((a, b) => a + b, 0); + const P = new Float64Array(3 * (K + 1)), V = new Float64Array(3 * (K + 1)); + const cum = new Float64Array(K + 1), psi = new Float64Array(K + 1), area = new Float64Array(K + 1); + let k = 0, prev = pieces[0].at(0); + P.set(prev, 0); + pieces.forEach((pc, i) => { + for (let j = 1; j <= ns[i]; j++) { + const q = nrm(pc.at(j / ns[i])); + k++; + P.set(q, 3 * k); + cum[k] = cum[k - 1] + acos(cl(dot(prev, q), -1, 1)); + prev = q; + } + }); + const pt = (i) => [P[3 * i], P[3 * i + 1], P[3 * i + 2]]; + const p0 = pt(0), t0 = nrm(lin(pt(1), 1, p0, -dot(pt(1), p0))); + // The arrow starts pointing back over the loop's right side, 135 degrees + // clockwise from the way it goes, so neither it nor the returned arrow lies + // along a leg at the openings. + let v = lin(t0, -Math.SQRT1_2, crs(p0, t0), -Math.SQRT1_2), g = carry(p0, c, v), ps = 0, ar = 0, last = 0; + V.set(v, 0); + // Azimuth bins about c remember which sample first swept them, how far + // from c the loop is there and which way it went. + const e1 = nrm(abs(c[2]) < 0.9 ? crs([0, 0, 1], c) : crs([1, 0, 0], c)), e2 = crs(c, e1); + const bk = new Int32Array(NB).fill(-1), br = new Float32Array(NB), bs = new Int8Array(NB); + const azi = (p) => atan2(dot(p, e2), dot(p, e1)); + for (let i = 0; i < K; i++) { + const a = pt(i), b = pt(i + 1); + v = nrm(carry(a, b, v)); + V.set(v, 3 * (i + 1)); + const G = carry(c, b, g), x = ang(G, v, b); + ps += wrap(x - last), last = x, psi[i + 1] = ps; + const det = dot(c, crs(a, b)); + ar += 2 * atan2(det, 1 + dot(c, a) + dot(a, b) + dot(b, c)); + area[i + 1] = ar; + const da = acos(cl(dot(a, c), -1, 1)), db = acos(cl(dot(b, c), -1, 1)); + if (da < 2e-3 || db < 2e-3) continue; + const za = azi(a), dz = wrap(azi(b) - za), sg = dz >= 0 ? 1 : -1; + const ia = floor(((za + PI) / TAU) * NB), n = round(abs(dz / TAU) * NB); + for (let j = 0; j <= n; j++) { + const bb = (((ia + sg * j) % NB) + NB) % NB; + if (bk[bb] < 0) (bk[bb] = i), (br[bb] = da + ((db - da) * j) / max(1, n)), (bs[bb] = sg); + } + } + Object.assign(S, { which, u, K, P, V, cum, psi, area, L, c, e1, e2, bk, br, bs, rb, want }); + S.zoomT = max(1, (which === 2 ? 20 : 13) / (30 * sin(min(rb, PI / 2)))); +} + +export function setup(params) { + const S = (params.s = { + t: 0, hue: 0, speed: 2, which: -1, u: 0.06, s: 0, phase: 0, pt: 0, fresh: 1, + vlat: 0, vlon: 0, zoom: 1, buf: new Float32Array(W * H * 3), col: [0, 0, 0], + }); + build(S, 0, 0.06); + return S; +} + +function sampleAt(S, s) { + let lo = 0, hi = S.K; + while (hi - lo > 1) { + const m = (lo + hi) >> 1; + if (S.cum[m] <= s) lo = m; + else hi = m; + } + const f = cl((s - S.cum[lo]) / max(1e-9, S.cum[hi] - S.cum[lo]), 0, 1), P = S.P, V = S.V, a = 3 * lo, b = 3 * hi; + const p = nrm([P[a] + (P[b] - P[a]) * f, P[a + 1] + (P[b + 1] - P[a + 1]) * f, P[a + 2] + (P[b + 2] - P[a + 2]) * f]); + let v = [V[a] + (V[b] - V[a]) * f, V[a + 1] + (V[b + 1] - V[a + 1]) * f, V[a + 2] + (V[b + 2] - V[a + 2]) * f]; + v = nrm(lin(v, 1, p, -dot(v, p))); + return { p, v, k: lo, f, psi: S.psi[lo] + (S.psi[hi] - S.psi[lo]) * f, area: S.area[lo] + (S.area[hi] - S.area[lo]) * f }; +} + +export function update(dt, input, params) { + const S = params.s || setup(params); + const kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.t += d; + S.hue = kv[0] || 0; + S.speed = cl(kv[1] ?? 2, 0.2, 3); + const which = cl(round(kv[2] || 0), 0, 3), u = cl(kv[3] ?? 0.06, -1, 1); + if (which !== S.which || abs(u - S.u) > 0.002) build(S, which, u); + // A lap takes 12 / Speed seconds; the hold and the fade shorten more gently. + const slow = sqrt(2 / S.speed); + if (S.phase === 0) { + S.s += (S.speed / 12) * d; + if (S.s >= 1) (S.s = 1), (S.phase = 1), (S.pt = 0); + } else { + S.pt += d / slow; + if (S.pt > 3.2) (S.phase = 0), (S.s = 0); + } + // The view looks at a point between the fan centre and the arrow, from + // above the latitude it is at, north up; small loops are looked at whole + // and zoomed in until they are 13 LEDs across or more. + const now = sampleAt(S, S.s * S.L); + // Near the close it turns to the start point, so the two arrows face us. + const big = S.rb > 0.6 && S.which !== 3, w = big ? 1.3 + 3.7 * cl((S.s - 0.8) / 0.2, 0, 1) : S.which === 3 ? 0.8 * (1 - S.s) : 0; + const tv = nrm(lin(S.c, 1, now.p, w)); + const tlat = cl(asin(tv[2]), -1.5, 1.5), tlon = atan2(tv[1], tv[0]) + (0.16 * sin(0.33 * S.t)) / S.zoom; + if (S.fresh) (S.vlat = tlat), (S.vlon = tlon), (S.zoom = S.zoomT), (S.fresh = 0); + const e = 1 - exp(-1.8 * d); + S.vlat += (tlat - S.vlat) * e; + S.vlon += wrap(tlon - S.vlon) * e; + S.zoom *= exp(log(S.zoomT / S.zoom) * e); + S.now = now; +} + +// Additive drawing into the float buffer, clipped to the rows lo..hi-1. +function splat(B, x, y, r, g, b, lo, hi) { + const i = floor(x - 0.5), j = floor(y - 0.5), fx = x - 0.5 - i, fy = y - 0.5 - j; + for (let q = 0; q < 4; q++) { + const ii = i + (q & 1), jj = j + (q >> 1); + if (ii < 0 || ii >= W || jj < lo || jj >= hi) continue; + const w = (q & 1 ? fx : 1 - fx) * (q >> 1 ? fy : 1 - fy), k = (jj * W + ii) * 3; + B[k] += r * w, B[k + 1] += g * w, B[k + 2] += b * w; + } +} +function line(B, x0, y0, x1, y1, r, g, b, lo, hi) { + const len = sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)), n = max(1, ceil(len / 0.4)), w = max(len, 0.4) / n; + for (let i = 0; i < n; i++) { + const f = (i + 0.5) / n; + splat(B, x0 + (x1 - x0) * f, y0 + (y1 - y0) * f, r * w, g * w, b * w, lo, hi); + } +} + +// 3x5 glyphs, rows of three bits, 4 the left column. +const FONT = { + 0: [7, 5, 5, 5, 7], 1: [2, 6, 2, 2, 7], 2: [7, 1, 7, 4, 7], 3: [7, 1, 3, 1, 7], 4: [5, 5, 7, 1, 1], + 5: [7, 4, 7, 1, 7], 6: [7, 4, 7, 5, 7], 7: [7, 1, 1, 2, 2], 8: [7, 5, 7, 5, 7], 9: [7, 5, 7, 1, 7], + '-': [0, 0, 7, 0, 0], '.': [0, 0, 0, 0, 4], 'o': [2, 5, 2, 0, 0], ' ': [0, 0, 0, 0, 0], + T: [7, 2, 2, 2, 2], U: [5, 5, 5, 5, 7], R: [6, 5, 6, 5, 5], N: [6, 5, 5, 5, 5], A: [2, 5, 7, 5, 5], E: [7, 4, 6, 4, 7], '=': [0, 7, 0, 7, 0], +}; +const sc = (v) => (v < 0.85 ? v : 0.85 + 0.15 * (1 - exp(-(v - 0.85) / 0.15))); +const gw = (ch) => (ch === '.' ? 1 : 3); +function text(B, str, cx, y, r, g, b) { + let wd = -1; + for (const ch of str) wd += gw(ch) + 1; + let x = round(cx - wd / 2); + for (const ch of str) { + const gl = FONT[ch] || FONT[' ']; + for (let row = 0; row < 5; row++) + for (let col = 0; col < gw(ch); col++) + if ((gl[row] >> (gw(ch) === 1 ? 2 : 2 - col)) & 1) { + const k = ((y + row) * W + x + col) * 3; + if (x + col >= 0 && x + col < W) (B[k] += r), (B[k + 1] += g), (B[k + 2] += b); + } + x += gw(ch) + 1; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.now) return; + const B = S.buf, col = S.col, now = S.now, t = S.t; + B.fill(0); + const CX = 32, CY = 32, Rz = 30 * S.zoom; + const f = sph(S.vlat, S.vlon), rt = [-sin(S.vlon), cos(S.vlon), 0]; + const up = [-sin(S.vlat) * cos(S.vlon), -sin(S.vlat) * sin(S.vlon), cos(S.vlat)]; + const proj = (p) => [CX + Rz * dot(p, rt), CY - Rz * dot(p, up), dot(p, f)]; + const fade = S.phase === 1 ? cl((3.2 - S.pt) / 0.6, 0, 1) : 1; + // Colours. + const bh = 0.3 + 0.55 * fr(S.hue + 0.52); + const body = hsv(bh, 0.7, 1, [0, 0, 0]), grid = hsv(bh, 0.3, 1, [0, 0, 0]); + const gold = hsv(S.hue + 0.13, 0.75, 1, [0, 0, 0]), pos = hsv(S.hue + 0.92, 0.6, 1, [0, 0, 0]), neg = hsv(S.hue + 0.45, 0.7, 1, [0, 0, 0]); + const pale = hsv(S.hue + 0.13, 0.2, 1, [0, 0, 0]); + // Graticule spacing: the coarsest of 30, 15, 10, 5, 2.5, 1 degrees that + // still leaves 9 LEDs between lines. + let gs = 30; + for (const g of [1, 2.5, 5, 10, 15, 30]) + if (g * DEG * Rz >= 9) { + gs = g; + break; + } + const gr = gs * DEG; + // The globe, lit from the upper left; the swept fan tinted. + const Lx = -0.45, Ly = 0.5, Lz = 0.74, kNow = S.phase === 0 ? now.k : S.K; + const { c, e1, e2, bk, br, bs } = S; + for (let y = 0; y < TOP; y++) { + const b = (CY - y - 0.5) / Rz; + for (let x = 0; x < W; x++) { + const a = (x + 0.5 - CX) / Rz, q = a * a + b * b; + if (q >= 1) continue; + const z = sqrt(1 - q), edge = cl((1 - sqrt(q)) * Rz * 1.5, 0, 1); + const p = [a * rt[0] + b * up[0] + z * f[0], a * rt[1] + b * up[1] + z * f[1], a * rt[2] + b * up[2] + z * f[2]]; + const sh = max(0, a * Lx + b * Ly + z * Lz), lit = 0.08 + 0.42 * sh * sh; + const lat = asin(cl(p[2], -1, 1)), lon = atan2(p[1], p[0]); + // Meridians close up toward the poles, so every second, fourth, ... + // one is dropped where they would come nearer than 9 LEDs. + const cg = gr * cos(lat) * Rz, mg = gr * (cg >= 9 ? 1 : min(64, 2 ** ceil(Math.log2(9 / max(cg, 0.01))))); + const dl = abs(lat / gr - round(lat / gr)) * gr * Rz, dm = abs(lon / mg - round(lon / mg)) * mg * cos(lat) * Rz; + const gl = max(0, 1 - dl / 0.8, 1 - dm / 0.8) * (0.07 + 0.1 * sh); + let tr = 0, tg = 0, tb = 0; + const zc = atan2(dot(p, e2), dot(p, e1)), bb = floor(((zc + PI) / TAU) * NB) % NB, kk = bk[bb]; + if (kk >= 0 && kk < kNow) { + const dd = acos(cl(dot(p, c), -1, 1)), m = cl((br[bb] - dd) * Rz + 0.5, 0, 1) * fade * (0.16 + 0.14 * sh); + const tc = bs[bb] > 0 ? pos : neg; + (tr = tc[0] * m), (tg = tc[1] * m), (tb = tc[2] * m); + } + const k = (y * W + x) * 3; + B[k] = (body[0] * lit + grid[0] * gl + tr) * edge; + B[k + 1] = (body[1] * lit + grid[1] * gl + tg) * edge; + B[k + 2] = (body[2] * lit + grid[2] * gl + tb) * edge; + } + } + // The loop: bright where the arrow has been, dim ahead of it. + const P = S.P, V = S.V; + let q0 = proj([P[0], P[1], P[2]]); + for (let i = 1; i <= S.K; i++) { + const q1 = proj([P[3 * i], P[3 * i + 1], P[3 * i + 2]]); + if (q0[2] > 0 && q1[2] > 0) { + const w = (i <= kNow ? 0.5 : 0.22) * (0.4 + 0.6 * fade); + line(B, q0[0], q0[1], q1[0], q1[1], pale[0] * w, pale[1] * w, pale[2] * w, 0, TOP); + } + q0 = q1; + } + // An arrow: a great-circle shaft 10 LEDs long and a head of two 4 LED barbs. + const Lr = 0.34 / S.zoom, Lh = 0.42 * Lr; + const arrow = (p, v, cc, w) => { + let a = proj(p); + if (a[2] <= 0) return; + splat(B, a[0], a[1], cc[0] * w, cc[1] * w, cc[2] * w, 0, TOP); + for (let i = 1; i <= 5; i++) { + const s = (Lr * i) / 5, b = proj(lin(p, cos(s), v, sin(s))); + if (b[2] > 0) line(B, a[0], a[1], b[0], b[1], cc[0] * w, cc[1] * w, cc[2] * w, 0, TOP); + a = b; + } + const tip = lin(p, cos(Lr), v, sin(Lr)), fw = lin(p, -sin(Lr), v, cos(Lr)), sd = crs(tip, fw), ta = proj(tip); + for (const sg of [-1, 1]) { + const dv = lin(fw, -cos(0.6), sd, sg * sin(0.6)), e = proj(lin(tip, cos(Lh), dv, sin(Lh))); + if (e[2] > 0 && ta[2] > 0) line(B, ta[0], ta[1], e[0], e[1], cc[0] * w, cc[1] * w, cc[2] * w, 0, TOP); + } + }; + // Ghost arrows every 8 LEDs or so of loop, left as the arrow passes. + const nG = cl(round((S.L * Rz) / 8), 5, 16), sNow = S.s * S.L; + for (let j = 1; j < nG; j++) { + const sj = (j * S.L) / nG; + if (sj > sNow) break; + const g = sampleAt(S, sj), w = 0.42 * fade * cl((sNow - sj) * Rz * 0.3, 0, 1); + arrow(g.p, g.v, gold, w); + } + const p0 = [P[0], P[1], P[2]], v0 = [V[0], V[1], V[2]], white = [0.85, 0.9, 1]; + const hold = S.phase === 1 ? fade : 0; + arrow(p0, v0, white, 0.45 + 0.5 * hold); + arrow(now.p, now.v, gold, 1.15 * fade); + // At the close, an arc from the start arrow round to the returned one. + const turn = now.psi; + if (hold > 0) { + const ra = 0.62 * Lr, n0 = crs(p0, v0), n = max(8, ceil(abs(turn) * ra * Rz)); + let a = null; + const blink = 0.75 + 0.25 * sin(t * 6); + for (let i = 0; i <= n; i++) { + const al = (turn * i) / n, dir = lin(v0, cos(al), n0, sin(al)), b = proj(lin(p0, cos(ra), dir, sin(ra))); + const tc = turn >= 0 ? pos : neg, w = hold * blink; + if (a && a[2] > 0 && b[2] > 0) line(B, a[0], a[1], b[0], b[1], tc[0] * w, tc[1] * w, tc[2] * w, 0, TOP); + a = b; + } + } + // The dial: the area swept so far as a pie from twelve o'clock, positive + // counterclockwise, and the arrow's turn against the gauge as the needle. + const DX = 32, DY = 94, DR = 25, HUB = 9.5, A = S.phase === 0 ? now.area : S.area[S.K]; + const tc = A >= 0 ? pos : neg; + for (let y = DY - DR - 2; y <= DY + DR + 2; y++) + for (let x = 0; x < W; x++) { + const dx = x + 0.5 - DX, dy = y + 0.5 - DY, r = sqrt(dx * dx + dy * dy); + if (r > DR + 1.5) continue; + const k = (y * W + x) * 3, ring = max(0, 1 - abs(r - DR) / 0.8) * 0.2; + (B[k] += ring * grid[0]), (B[k + 1] += ring * grid[1]), (B[k + 2] += ring * grid[2]); + if (r >= DR - 0.5 || r <= HUB) continue; + const al = atan2(-dx, -dy), a1 = A >= 0 ? fr(al / TAU) * TAU : fr(-al / TAU) * TAU; + const m = cl((abs(A) - a1) * r + 0.5, 0, 1) * cl(r - HUB, 0, 1) * (0.14 + 0.3 * (r / DR)) * (0.5 + 0.5 * fade); + (B[k] += tc[0] * m), (B[k + 1] += tc[1] * m), (B[k + 2] += tc[2] * m); + } + const ray = (al, r0, r1, cc, w) => + line(B, DX - sin(al) * r0, DY - cos(al) * r0, DX - sin(al) * r1, DY - cos(al) * r1, cc[0] * w, cc[1] * w, cc[2] * w, TOP, H); + for (let i = 0; i < 12; i++) ray((i * PI) / 6, i % 3 ? DR - 2.5 : DR - 4, DR - 0.5, grid, i % 3 ? 0.35 : 0.6); + // Where this loop's turn will end. + const wt = S.want, wtc = wt >= 0 ? pos : neg; + ray(wt, DR - 1, DR + 2, wtc, 0.9); + ray(0, HUB + 1, DR - 2, white, 0.4 + 0.4 * hold); + const nd = S.phase === 0 ? turn : S.psi[S.K]; + ray(nd, HUB + 1, DR - 2, gold, 1.1); + const tx = DX - sin(nd) * (DR - 2), ty = DY - cos(nd) * (DR - 2); + for (const sg of [-1, 1]) { + const bd = nd + PI + sg * 0.55; + line(B, tx, ty, tx - sin(bd) * 4.2, ty - cos(bd) * 4.2, gold[0] * 1.1, gold[1] * 1.1, gold[2] * 1.1, TOP, H); + } + // The turn in degrees in the hub, one decimal under 10. + const dg = (nd * 180) / PI; + let str = abs(dg) < 9.95 ? (abs(dg) < 0.05 ? '0.0' : dg.toFixed(1)) : String(round(dg)); + str += 'o'; + const tw = 0.75 + 0.25 * hold; + text(B, str, DX, DY - 2, tw, tw, tw); + text(B, 'TURN=AREA', DX, 122, pale[0] * 0.45, pale[1] * 0.45, pale[2] * 0.45); + for (let p = 0; p < W * H; p++) { + const o = p * 3; + display.setPixel(p & 63, p >> 6, floor(sc(B[o]) * 255), floor(sc(B[o + 1]) * 255), floor(sc(B[o + 2]) * 255)); + } +} From bc46d35b0ab2ac26a250e26348e7cf7b86d82694 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:22:47 +0000 Subject: [PATCH 05/24] incoming: Rose Criterion, pattern round 68 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JktahMn4mzzFyveZ8fo --- .../pattern-study/patterns/rose-criterion.js | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 incoming/pattern-study/patterns/rose-criterion.js diff --git a/incoming/pattern-study/patterns/rose-criterion.js b/incoming/pattern-study/patterns/rose-criterion.js new file mode 100644 index 000000000..686b52d4f --- /dev/null +++ b/incoming/pattern-study/patterns/rose-criterion.js @@ -0,0 +1,186 @@ +// ===== Patternflow pattern ===== +// Title: Rose Criterion +// Family: noise +// Idea: A contrast-detail grid of fifty discs, five diameters across and ten contrasts down, is exposed photon by photon with Poisson counts and a little read noise, so the normalised picture starts as static, settles into grain and lets the discs rise out of it in order of contrast times diameter, while a faint line marks Rose's k = 5, the hyperbola C·d = const that sweeps from the top left corner toward the bottom right and stops where the dose runs out; then the exposure clears and builds again. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Rose's detection criterion k = C·sqrt(nA) (Albert Rose, "The sensitivity performance of the human eye on an absolute scale", J. Opt. Soc. Am. 38, 1948), pattern round 68 +// Round: 68 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Exposure=0..1, Read noise=0..1 +// +// Knob 1: Hue. The colour of the light: at 0 cyan grain with a gold k = 5 line, at 0.25 violet with a green line, at 0.5 coral with a blue line, at 0.75 lime with a magenta line. +// Knob 2: Speed. How fast each exposure builds: at 2 the dose comes in over 3 s, holds 1.2 s and clears; at 0.2 it comes in over 30 s, at 3 over 2 s. The photon flux is the dose over that time. +// Knob 3: Exposure. The dose a build stops at, in photons per LED: 25 at 0, 224 at 0.5, 2000 at 1. At 25 the k = 5 line stops at C·d = 1.15 with 7 of the 50 discs still in the grain; from 0.27 on every disc is seen, the last one (contrast 0.15, 9 LEDs) at 124 photons, and the rest of the dose only calms the grain. +// Knob 4: Read noise. Gaussian noise each channel adds every time it reads its LED, 1 photon rms at 0 and 9 at 1. It adds to the shot noise, so the grain coarsens and the k = 5 line, drawn with it counted in, slows: at 1 the first disc needs 8 photons instead of 1.3, and at 25 photons 24 discs stay hidden. + +// Rose (1948): a disc of area A whose brightness differs by a fraction C +// from a background that has caught n photons per unit area is seen when +// k = C·sqrt(n·A), its excess count over the noise of the background count +// in the same area, passes about 5. With A = pi·d²/4 the threshold is +// C·d = 5 / sqrt(pi·n/4): at a given dose the seen and unseen discs of a +// contrast-detail grid are split by a hyperbola, which moves out as 1/sqrt(n). +// +// Here A is the disc's own LED count (97, 69, 37, 21 and 9 for diameters +// 11, 9, 7, 5 and 3) and n the photons per LED. Each LED keeps one count, +// read by each of its three channels with its own read noise sigma, so one +// read has variance n + sigma² and the line drawn is +// C·sqrt(A)·n / sqrt(n + sigma²) = 5. + +const W = 64, H = 128; +const { floor, min, max, sqrt, exp, log, cos, sin, pow, round, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + o[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + o[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; + return o; +} + +// The grid: diameters across, contrasts down. Each disc is the LEDs whose +// centres lie within d/2 of the centre of the LED at (COLX, ROWY), so every +// disc in a column has the same shape and the same area. +const DIAM = [11, 9, 7, 5, 3]; +const COLX = [6, 19, 32, 44, 57]; +const ROWY = [6, 19, 32, 44, 57, 70, 83, 96, 108, 121]; +const CONTRAST = ROWY.map((_, r) => 0.6 - 0.05 * r); +const K_ROSE = 5; +const CXS = COLX.map((x) => x + 0.5), CYS = ROWY.map((y) => y + 0.5); + +// The display: the count normalised by what the background should have +// caught, so u = 1 on the background, then an exponential toe and a soft +// shoulder, 1 - exp(-a·exp(BETA·(u - 1))). Equal steps of u read as equal +// steps of brightness on the LEDs, and the background sits near L0 with +// grain on both sides of it instead of piling up at black. +const L0 = 13, BETA = 6.5; +// The gain runs out below this many expected photons, so the panel comes +// up from black instead of blowing up the first few counts. +const FLOOR = 3.5; +// How bright the k = 5 line is drawn. +const LINE = 150; +// The tone curve, tabulated over t = BETA·(u - 1) in steps of 1/128. +const T0 = -10, TS = 128, TN = 15 * TS; +const TONE = new Float32Array(TN + 1); +for (let i = 0; i <= TN; i++) TONE[i] = 1 - exp(-(L0 / 255) * exp(T0 + i / TS)); + +function poisson(l, gauss) { + if (l <= 0) return 0; + if (l > 15) return max(0, round(l + sqrt(l) * gauss())); + const u = Math.random(); + let k = 0, p = exp(-l), s = p; + while (u > s && k < 60) { + k++; + p *= l / k; + s += p; + } + return k; +} + +export function setup(params) { + const cidx = new Uint8Array(W * H); + const area = DIAM.map(() => 0); + DIAM.forEach((d, c) => { + const r2 = (d / 2) * (d / 2), R = d >> 1; + for (let i = -R; i <= R; i++) + for (let j = -R; j <= R; j++) { + if (i * i + j * j > r2) continue; + area[c]++; + ROWY.forEach((y, r) => (cidx[(y + j) * W + COLX[c] + i] = r + 1)); + } + }); + // Standard normal deviates, drawn once and picked at random at every read. + const norm = new Float32Array(4096); + for (let i = 0; i < 4096; i += 2) { + const a = sqrt(-2 * log(1 - Math.random())), b = 2 * PI * Math.random(); + norm[i] = a * cos(b); + norm[i + 1] = a * sin(b); + } + params.s = { + cidx, deff: area.map((a) => sqrt((4 * a) / PI)), + gauss: () => norm[(Math.random() * 4096) | 0], + count: new Float32Array(W * H), f: new Float32Array(W * H), dx: new Float32Array(W), + n: 0, hold: 0, stop: 25, sr: 1, hue: 0, lam: new Float32Array(11), + w: new Float32Array(3), lc: new Float32Array(3), + }; + return params.s; +} + +// The k = 5 threshold on C·d at dose n and read noise sr. +const lineK = (n, sr) => (n > 0 ? (K_ROSE * sqrt(n + sr * sr)) / (n * sqrt(PI / 4)) : 1e9); + +export function update(dt, input, params) { + const S = params.s || setup(params); + const kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0] || 0; + const speed = cl(kv[1] ?? 2, 0.2, 3); + S.stop = 25 * pow(80, cl(kv[2] || 0, 0, 1)); + const rn = cl(kv[3] ?? 0.06, 0, 1); + S.sr = 1 + 8 * rn * rn; + + if (S.n >= S.stop) { + S.hold += d * speed; + if (S.hold > 2.4) { + S.n = 0; + S.hold = 0; + S.count.fill(0); + } + return; + } + // Constant flux: the dose comes in over 6/speed seconds. A disc of + // contrast C catches 1 + C times the background's photons. + S.hold = 0; + const dn = min(S.stop - S.n, (S.stop * speed * d) / 6); + S.n += dn; + for (let k = 0; k <= 10; k++) S.lam[k] = dn * (k ? 1 + CONTRAST[k - 1] : 1); + const cnt = S.count, ci = S.cidx, lam = S.lam, g = S.gauss; + for (let i = 0; i < W * H; i++) cnt[i] += poisson(lam[ci[i]], g); +} + +// Piecewise linear through the grid's centres, carried on past the ends. +function interp(xs, ys, v) { + let i = 0; + while (i < xs.length - 2 && v > xs[i + 1]) i++; + return ys[i] + ((v - xs[i]) * (ys[i + 1] - ys[i])) / (xs[i + 1] - xs[i]); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const cnt = S.count, sr = S.sr, gauss = S.gauss, w = hsv(0.5 + S.hue, 0.55, 1, S.w); + const w0 = 255 * w[0], w1 = 255 * w[1], w2 = 255 * w[2]; + // One read: t = BETA·(u - 1) as a row of the tone table. + const sc = (BETA * TS) / max(S.n, FLOOR), off = (-BETA - T0) * TS, rs = sr * sc; + const tone = (c) => { + const t = (c + rs * gauss() + off) | 0; + return TONE[t < 0 ? 0 : t > TN ? TN : t]; + }; + + // The line: where C(y)·d(x) crosses the k = 5 threshold, drawn on the seen side. + const K = lineK(S.n, sr), f = S.f, dx = S.dx; + for (let x = 0; x < W; x++) dx[x] = max(0.3, interp(CXS, S.deff, x + 0.5)); + for (let y = 0; y < H; y++) { + const C = max(0.005, interp(CYS, CONTRAST, y + 0.5)); + for (let x = 0; x < W; x++) f[y * W + x] = C * dx[x] - K; + } + const lc = hsv(S.hue + 0.13, 0.75, 1, S.lc); + const l0 = lc[0] * LINE, l1 = lc[1] * LINE, l2 = lc[2] * LINE; + + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x, c = cnt[i] * sc; + // One count per LED, read once by each of its three channels. + let r = w0 * tone(c), g = w1 * tone(c), b = w2 * tone(c); + const v = f[i]; + if (v >= 0 && ((x > 0 && f[i - 1] < 0) || (x < W - 1 && f[i + 1] < 0) || (y > 0 && f[i - W] < 0) || (y < H - 1 && f[i + W] < 0))) { + r = max(r, l0); + g = max(g, l1); + b = max(b, l2); + } + display.setPixel(x, y, r | 0, g | 0, b | 0); + } +} From 9c31b8ae9fa3c2c3018b9fddbbde96f76b90a567 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:22:54 +0000 Subject: [PATCH 06/24] incoming: Fibonacci Word, pattern round 68 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JktahMn4mzzFyveZ8fo --- .../pattern-study/patterns/fibonacci-word.js | 362 ++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 incoming/pattern-study/patterns/fibonacci-word.js diff --git a/incoming/pattern-study/patterns/fibonacci-word.js b/incoming/pattern-study/patterns/fibonacci-word.js new file mode 100644 index 000000000..5b2328b61 --- /dev/null +++ b/incoming/pattern-study/patterns/fibonacci-word.js @@ -0,0 +1,362 @@ +// ===== Patternflow pattern ===== +// Title: Fibonacci Word +// Family: fractal +// Idea: A pen reads the Fibonacci word 0100101001001... and takes one step for every digit, turning a quarter turn at every 0, left in an even place and right in an odd one, so 144 crisp steps of 4 LEDs draw a self-avoiding curve of squares nested in squares over the coarse curve of the first 34 digits, each coarse step giving way to the 5 or 3 fine steps that 0 -> 01001 and 1 -> 010 make of it; the three copies of those 34 digits inside the 144 then light in turn, and the panel dives into the first with a quarter turn and a 1 + sqrt 2 zoom until it stands where the whole stood, mirrored, and the pen draws the whole over it again. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the Fibonacci word fractal and its odd-even drawing rule (Alexis Monnerot-Dumaine, 2009), pattern round 68 +// Round: 68 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Order=12..15, Angle=-1..1 +// +// Knob 1: Hue. Turns the colour that runs half the wheel along the word, first digit to last: at 0 sea green through blue and violet to rose, at 0.25 blue through magenta and orange to yellow, at 0.5 rose through amber and lime to sea green, at 0.75 yellow through green and cyan to blue. The coarse curve and the tape of digits on the right edge take the same colours. +// Knob 2: Speed. The pen draws 70 LEDs of line a second for every unit, 140 at the opening 2, so the 144 steps of order 12 take 4.1 s and a whole cycle of drawing, the 1.8 s look at the three copies and the 2.2 s dive takes 8.1 s; at 0.2 the drawing alone takes 41 s, at 3 it takes 2.7 s and the look and the dive shorten by a fifth. +// Knob 3: Order. How many digits the pen reads, the Fibonacci number F(n): 12 for 144 steps of 4 LEDs (48 by 112 LEDs), 13 for 233 steps of 2 LEDs in a square 56 by 58, 14 for 377 steps of 2 LEDs (56 by 82), 15 for 610 steps of 2 LEDs, 58 by 138, so 6 rows at the top and 5 at the bottom run off the panel. The coarse curve under the pen is always order n - 3, and the order keeps its shape every three: 12 and 15 are long, 13 square, 14 one and a half times as long as wide. +// Knob 4: Angle. The turn at each 0. From -0.1 to 0.1 it is exactly 90 degrees and the line stays on the LED grid; it goes down to 60 degrees at -1, where the squares open into a wiggling line that still never touches itself, and up to 120 degrees at 1, where the curve folds over itself into a lattice of triangles and 36 of the 144 steps retrace an earlier one. Off 90 degrees the line is antialiased and the whole is turned and scaled to fit the panel. + +// The rule. The Fibonacci word is the limit of S(0) = 0, S(1) = 01, +// S(n) = S(n-1) S(n-2); its k-th digit, k from 1, is 2 + floor(k phi) - +// floor((k + 1) phi). Read it from k = 1: step forward, and on a 0 turn +// left when k is even and right when k is odd. Counting k from 0 instead +// gives the mirror image. At 90 degrees the first F(n) digits draw the +// curve of order n; a scratch walk to 28,657 steps found no point visited +// twice and no two points one lattice step apart unless they are +// consecutive, which is why even a 1-LED step would stay a clean line. +// Of the F(n) steps, F(n-1) turn and F(n-2) go straight. +// +// Self-similarity. The word is fixed by 0 -> 01, 1 -> 0, so three orders +// up each digit becomes a block, 0 -> 01001 and 1 -> 010, and the curve of +// order n is the curve of order n - 3 with every step replaced by its block. +// It follows the coarse curve mirrored and 1 + sqrt 2 = 2.414 times larger: +// a least-squares fit of the block ends to the coarse corners is a mirror +// map of scale 2.420 at 2,584 -> 10,946 steps. Length grows by phi^3 = +// 4.236 for that, so the dimension of the limit is 3 log phi / +// log(1 + sqrt 2) = 1.6379. With a turn angle a the +// fitted ratio follows 1 + cos a + sqrt((1 + cos a)^2 + 1): 3.30 at 60 +// degrees, 1.64 against 1.618 at 120, where 3 log phi / log of it would be +// 3, more than a plane curve can hold, and the curve overlaps itself. +// +// Also S(n) = S(n-3) S(n-4) S(n-3) S(n-3) S(n-4): the first F(n-3) digits +// come back at F(n-2) and F(n-1), mirrored where that offset is odd. Those +// are the three copies the panel lights before the dive. + +const W = 64, H = 128; +const { floor, ceil, round, min, max, abs, sign, sin, cos, pow, atan2, hypot, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const smooth = (t) => (t <= 0 ? 0 : t >= 1 ? 1 : t * t * (3 - 2 * t)); + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + o[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + o[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; + return o; +} + +// The word to 987 digits, and the Fibonacci numbers with F(1) = F(2) = 1. +const WORD = (() => { + let a = [0], b = [0, 1]; + while (b.length < 987) [a, b] = [b, b.concat(a)]; + return b; +})(); +const FIB = [0, 1]; +while (FIB.length < 20) FIB.push(FIB[FIB.length - 1] + FIB[FIB.length - 2]); +// Coarse step j becomes fine steps BLK[j] .. BLK[j + 1] - 1 three orders up. +const BLK = new Int32Array(WORD.length + 1); +for (let j = 0; j < WORD.length; j++) BLK[j + 1] = BLK[j] + (WORD[j] ? 3 : 5); + +// The walk in world coordinates. At exactly 90 degrees the turns are done +// on integers so the points land exactly on the LED grid. +function walk(N, deg, P) { + if (deg === 90) { + const DX = [1, 0, -1, 0], DY = [0, 1, 0, -1]; + let x = 0, y = 0, d = 0; + P[0] = 0, P[1] = 0; + for (let i = 0; i < N; i++) { + x += DX[d], y += DY[d]; + P[2 * i + 2] = x, P[2 * i + 3] = y; + if (WORD[i] === 0) d = (d + ((i + 1) % 2 === 0 ? 1 : 3)) % 4; + } + return; + } + const th = (deg * PI) / 180; + let x = 0, y = 0, a = 0; + P[0] = 0, P[1] = 0; + for (let i = 0; i < N; i++) { + x += cos(a), y += sin(a); + P[2 * i + 2] = x, P[2 * i + 3] = y; + if (WORD[i] === 0) a += (i + 1) % 2 === 0 ? th : -th; + } +} + +// A camera is screen = a * w' + b in complex numbers, w' the world point +// with y times mir. On the grid a is s or i s with s whole and b whole; off +// it, the rotation that lets the curve stand largest, kept unless another +// beats it by 3%. +function camera(P, N, mir, grid, keep) { + const tryRot = (c, s) => { + let x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9; + for (let i = 0; i <= N; i++) { + const x = P[2 * i], y = mir * P[2 * i + 1], X = c * x - s * y, Y = s * x + c * y; + if (X < x0) x0 = X; + if (X > x1) x1 = X; + if (Y < y0) y0 = Y; + if (Y > y1) y1 = Y; + } + return [x0, x1 - x0, y0, y1 - y0]; + }; + if (grid) { + let [x0, bw, y0, bh] = tryRot(1, 0), c = 1, s = 0; + if (bw > bh) ([x0, bw, y0, bh] = tryRot(0, 1)), (c = 0), (s = 1); + // 138.5 rows lets order 15 take 2 LEDs a step and run 5 rows off each end. + const k = max(1, floor(min(60 / max(1, bw), 138.5 / max(1, bh)))); + return { ar: k * c, ai: k * s, br: floor((60 - k * bw) / 2) - k * x0, bi: floor((127 - k * bh) / 2) - k * y0, mir, psi: s ? 90 : 0 }; + } + let best = null; + for (let d = 0; d < 180; d += 3) { + const r = (d * PI) / 180, c = cos(r), s = sin(r), [x0, bw, y0, bh] = tryRot(c, s); + const k = min(58 / max(1e-6, bw), 125 / max(1e-6, bh)); + const cand = { k, c, s, x0, bw, y0, bh, psi: d }; + if (!best || k > best.k) best = cand; + if (keep === d) keep = cand; + } + if (keep && typeof keep === "object" && keep.k >= best.k * 0.97) best = keep; + const { k, c, s, x0, bw, y0, bh, psi } = best; + return { ar: k * c, ai: k * s, br: 1 + (58 - k * bw) / 2 - k * x0, bi: (127 - k * bh) / 2 - k * y0, mir, psi }; +} + +function project(P, N, cam, X, Y) { + for (let i = 0; i <= N; i++) { + const x = P[2 * i], y = cam.mir * P[2 * i + 1]; + X[i] = cam.ar * x - cam.ai * y + cam.br; + Y[i] = cam.ai * x + cam.ar * y + cam.bi; + } +} + +// Least-squares similarity z -> alpha z + beta (or alpha conj z + beta) +// taking the points (ax, ay) onto (bx, by). Returns [ar, ai, br, bi, err]. +function simFit(n, ax, ay, bx, by, conj) { + let mx = 0, my = 0, nx = 0, ny = 0; + for (let j = 0; j < n; j++) mx += ax(j), my += (conj ? -1 : 1) * ay(j), nx += bx(j), ny += by(j); + mx /= n, my /= n, nx /= n, ny /= n; + let sr = 0, si = 0, ss = 0; + for (let j = 0; j < n; j++) { + const u = ax(j) - mx, v = (conj ? -1 : 1) * ay(j) - my, p = bx(j) - nx, q = by(j) - ny; + sr += u * p + v * q, si += u * q - v * p, ss += u * u + v * v; + } + const ar = sr / max(1e-9, ss), ai = si / max(1e-9, ss), br = nx - (ar * mx - ai * my), bi = ny - (ai * mx + ar * my); + let err = 0; + for (let j = 0; j < n; j++) { + const u = ax(j), v = (conj ? -1 : 1) * ay(j); + err += hypot(ar * u - ai * v + br - bx(j), ai * u + ar * v + bi - by(j)); + } + return [ar, ai, br, bi, err]; +} + +// Everything the panel needs for one order, angle and mirror: the fine +// curve on screen and the coarse curve of order n - 3 placed by the +// similarity that best lays its corners on the ends of their blocks. +function geometry(n, deg, mir, keepPsi) { + const N = FIB[n], Nc = FIB[n - 3], grid = deg === 90; + const P = new Float64Array(2 * N + 2); + walk(N, deg, P); + const cam = camera(P, N, mir, grid, keepPsi); + const X = new Float64Array(N + 1), Y = new Float64Array(N + 1); + project(P, N, cam, X, Y); + const fits = [false, true].map((cj) => + simFit(Nc + 1, (j) => P[2 * j], (j) => P[2 * j + 1], (j) => X[BLK[j]], (j) => Y[BLK[j]], cj)); + const f = fits[0][4] < fits[1][4] ? fits[0] : fits[1], cj = f === fits[1]; + const GX = new Float64Array(Nc + 1), GY = new Float64Array(Nc + 1); + for (let j = 0; j <= Nc; j++) { + const u = P[2 * j], v = (cj ? -1 : 1) * P[2 * j + 1]; + GX[j] = f[0] * u - f[1] * v + f[2]; + GY[j] = f[1] * u + f[0] * v + f[3]; + } + return { n, N, Nc, deg, mir, grid, cam, X, Y, GX, GY, step: hypot(cam.ar, cam.ai) }; +} + +// How bright the coarse curve waits under the pen. +const GHOST = 0.72; + +const angleOf = (k) => { + const a = abs(k); + return a <= 0.1 ? 90 : 90 + (k > 0 ? 30 : -30) * min(1, (a - 0.1) / 0.9); +}; + +function startDraw(S, ghost) { + S.phase = 0, S.pen = 0, S.pt = 0, S.gl = ghost; +} + +export function setup(params) { + const S = (params.s = { t: 0, hue: 0, speed: 2, phase: 0, pen: 0, pt: 0, gl: GHOST, zt: 0, acc: new Float32Array(W * H * 3), c: [0, 0, 0] }); + S.g = geometry(12, 90, 1, -1); + startDraw(S, GHOST); +} + +// The dive: the screen similarity that takes the coarse prefix as drawn now +// onto where the next (mirrored) picture's coarse curve stands. A power of +// it, about its fixed point, is the zoom. +function startZoom(S) { + const g = S.g, nx = geometry(g.n, g.deg, -g.mir, g.cam.psi); + const f = simFit(g.Nc + 1, (j) => g.X[j], (j) => g.Y[j], (j) => nx.GX[j], (j) => nx.GY[j], false); + const [ar, ai, br, bi] = f, dr = 1 - ar, di = -ai, dd = dr * dr + di * di; + S.z = { next: nx, r: hypot(ar, ai), phi: atan2(ai, ar), cx: (br * dr + bi * di) / max(1e-9, dd), cy: (bi * dr - br * di) / max(1e-9, dd) }; + S.phase = 2, S.zt = 0; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 12, 0.06], d = cl(dt || 0, 0, 0.1); + S.t += d; + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + const n = cl(round(k[2]), 12, 15), deg = angleOf(cl(k[3], -1, 1)); + const f = 0.55 + 0.225 * S.speed, g0 = S.g; + if (n !== g0.n) { + S.g = geometry(n, deg, g0.mir, -1); + startDraw(S, GHOST); + } else if (abs(deg - g0.deg) > 0.05 || (deg === 90) !== g0.grid) { + S.g = geometry(n, deg, g0.mir, g0.cam.psi); + if (S.phase === 2) startDraw(S, GHOST); + } + const g = S.g; + if (S.phase === 0) { + S.pen += (70 * S.speed * d) / max(0.05, g.step); + S.gl += (GHOST - S.gl) * min(1, d * 2.5); + if (S.pen >= g.N) (S.pen = g.N), (S.phase = 1), (S.pt = 0); + } else if (S.phase === 1) { + S.pt += (d * f) / 1.8; + if (S.pt >= 1) startZoom(S); + } else { + S.zt += (d * f) / 2.2; + if (S.zt >= 1) { + S.g = S.z.next; + startDraw(S, 1); + } + } +} + +// Additive light, one LED per unit of line. +function splat(acc, x, y, w, r, g, b) { + const i = floor(x), j = floor(y), fx = x - i, fy = y - j; + for (let q = 0; q < 4; q++) { + const ii = i + (q & 1), jj = j + (q >> 1); + if (ii < 0 || ii >= W || jj < 0 || jj >= H) continue; + const ww = w * (q & 1 ? fx : 1 - fx) * (q >> 1 ? fy : 1 - fy), o = (jj * W + ii) * 3; + acc[o] += r * ww, acc[o + 1] += g * ww, acc[o + 2] += b * ww; + } +} +function lineAA(acc, x0, y0, x1, y1, r, g, b, upto) { + if ((x0 < -2 && x1 < -2) || (x0 > W + 1 && x1 > W + 1) || (y0 < -2 && y1 < -2) || (y0 > H + 1 && y1 > H + 1)) return; + const L = hypot(x1 - x0, y1 - y0), m = max(1, ceil((L * upto) / 0.45)), w = (L * upto) / m; + for (let u = 0; u < m; u++) { + const t = ((u + 0.5) / m) * upto; + splat(acc, x0 + (x1 - x0) * t, y0 + (y1 - y0) * t, w, r, g, b); + } +} +// On the grid: the LEDs from (x0, y0) toward (x1, y1), the far end left to +// the next step, cut at the share upto. +function lineGrid(acc, x0, y0, x1, y1, r, g, b, upto) { + const L = abs(x1 - x0) + abs(y1 - y0), sx = sign(x1 - x0), sy = sign(y1 - y0), m = upto >= 1 ? L : floor(L * upto + 1e-9) + 1; + for (let u = 0; u < m; u++) { + const x = x0 + sx * u, y = y0 + sy * u; + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const o = (y * W + x) * 3; + acc[o] += r, acc[o + 1] += g, acc[o + 2] += b; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.g) return; + const acc = S.acc, c = S.c, h0 = 0.42 + S.hue; + acc.fill(0); + const g = S.g, N = g.N, Nc = g.Nc; + const col = (u, v, white) => { + hsv(h0 + 0.5 * u, 0.78 * (1 - white), v, c); + return c; + }; + if (S.phase === 2) { + // The dive: every point of the curve under the similarity's power. + const z = S.z, e = smooth(S.zt), rr = pow(z.r, e), ph = z.phi * e, er = rr * cos(ph), ei = rr * sin(ph); + const out = 1 - smooth(S.zt / 0.6); + let px = 0, py = 0; + for (let i = 0; i <= N; i++) { + const dx = g.X[i] - z.cx, dy = g.Y[i] - z.cy, x = z.cx + er * dx - ei * dy, y = z.cy + ei * dx + er * dy; + if (i > 0) { + const j = i - 1, pre = j < Nc; + const u = pre ? ((j + 0.5) / N) * (1 - e) + ((BLK[j] + BLK[j + 1]) / (2 * N)) * e : (j + 0.5) / N; + const v = pre ? 1 : out * (1 - 0.4 * e); + if (v > 0.01) { + const w = pre ? 0.55 * (1 - e) : 0; + col(u, v, w); + lineAA(acc, px, py, x, y, c[0], c[1], c[2], 1); + } + } + px = x, py = y; + } + } else { + // The coarse curve, each step fading as the pen finishes its block. + const rate = max(0.5, 70 / max(0.05, g.step)); + for (let j = 0; j < Nc; j++) { + const gone = (S.pen - BLK[j + 1]) / (0.3 * rate), a = S.gl * (1 - cl(gone, 0, 1)); + if (a <= 0.01) continue; + col((BLK[j] + BLK[j + 1]) / (2 * N), a, 0.15); + lineAA(acc, g.GX[j], g.GY[j], g.GX[j + 1], g.GY[j + 1], c[0], c[1], c[2], 1); + } + // The fine curve up to the pen. In the look (phase 1) the three copies + // of the first F(n-3) digits light in turn and the rest dims. + const offs = [0, FIB[g.n - 2], FIB[g.n - 1]]; + let spot = [0, 0, 0], dim = 1; + if (S.phase === 1) { + const p = S.pt; + spot = [0.22, 0.46, 0.7].map((m0, q) => max(0, 1 - abs(p - m0) / 0.13)); + spot[0] = max(spot[0], smooth((p - 0.8) / 0.2)); + dim = 1 - 0.55 * smooth(p / 0.12); + } + const last = S.phase === 0 ? floor(S.pen) : N - 1, frac = S.phase === 0 ? S.pen - last : 1; + for (let i = 0; i <= min(last, N - 1); i++) { + let s = 0; + for (let q = 0; q < 3; q++) if (i >= offs[q] && i < offs[q] + Nc) s = max(s, spot[q]); + const v = dim + (1 - dim) * s; + col((i + 0.5) / N, v, 0.6 * s); + const up = i < last ? 1 : frac; + if (up <= 0) continue; + if (g.grid) lineGrid(acc, g.X[i], g.Y[i], g.X[i + 1], g.Y[i + 1], c[0], c[1], c[2], up); + else lineAA(acc, g.X[i], g.Y[i], g.X[i + 1], g.Y[i + 1], c[0], c[1], c[2], up); + } + if (S.phase !== 0) { + col(1, dim, 0); + if (g.grid) lineGrid(acc, g.X[N], g.Y[N], g.X[N], g.Y[N], c[0], c[1], c[2], 1); + } else { + // The pen: a white point with a glow of the colour it is laying down. + const i = min(last, N - 1), x = g.X[i] + (g.X[i + 1] - g.X[i]) * frac, y = g.Y[i] + (g.Y[i + 1] - g.Y[i]) * frac; + col((i + 0.5) / N, 1, 0.3); + const px = g.grid ? round(x) : x, py = g.grid ? round(y) : y; + splat(acc, px, py, 1.6, 1, 1, 1); + for (const [ox, oy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) splat(acc, px + ox, py + oy, 0.45, c[0], c[1], c[2]); + } + } + // The tape: the word itself down the right edge, two rows a digit, the + // digit being read on row 64. A 0 is two LEDs wide, a 1 one. Read digits + // are lit in their colour, the rest wait dim; in the dive it rewinds. + const tape = S.phase === 0 ? S.pen : S.phase === 1 ? N : N * (1 - smooth(S.zt)); + for (let y = 0; y < H; y++) { + const q = tape + (y - 64) / 2, k = floor(q); + if (k < 0 || k >= N || q - k >= 0.5) continue; + const now = k === floor(tape) && S.phase === 0; + col((k + 0.5) / N, k < tape ? 1 : 0.32, now ? 1 : 0.1); + for (let x = WORD[k] ? 63 : 62; x < 64; x++) { + const o = (y * W + x) * 3; + acc[o] += c[0], acc[o + 1] += c[1], acc[o + 2] += c[2]; + } + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const o = (y * W + x) * 3; + display.setPixel(x, y, (255 * min(1, acc[o])) | 0, (255 * min(1, acc[o + 1])) | 0, (255 * min(1, acc[o + 2])) | 0); + } +} From 5acc1601d65bf80d5ace09a487d2a866894a5b2a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:23:10 +0000 Subject: [PATCH 07/24] incoming: Structure ASCII, pattern round 68 --- .../pattern-study/patterns/structure-ascii.js | 358 ++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 incoming/pattern-study/patterns/structure-ascii.js diff --git a/incoming/pattern-study/patterns/structure-ascii.js b/incoming/pattern-study/patterns/structure-ascii.js new file mode 100644 index 000000000..5e11431e6 --- /dev/null +++ b/incoming/pattern-study/patterns/structure-ascii.js @@ -0,0 +1,358 @@ +// ===== Patternflow pattern ===== +// Title: Structure ASCII +// Family: type +// Idea: The outline of a big letter sways and melts from A to S to & to the Hangul syllable 글 behind a grid of character cells, and each cell the outline crosses takes the ASCII character whose strokes lie nearest the piece of outline inside it, so slashes, bars, underscores, parentheses and ticks join into the letter's contour, as in structure-based ASCII art; a fourth setting picks by ink instead, as tone-based ASCII art does. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Structure-based ASCII Art (Xuemiao Xu, Linling Zhang and Tien-Tsin Wong, SIGGRAPH 2010), pattern round 68 +// Round: 68 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Set=0..3, Cell=0..3 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the straight strokes are turquoise, curves and angles azure, corners and crossings green, dots and ticks lilac, and the outline under them a dim blue; at 0.5 the strokes are red. +// Knob 2: Speed. At 2 a letter holds 4.5 s and melts into the next in 1.5 s, so the four take 24 s, while the drawing sways 0.12 radian (about 7 degrees) each way every 9 s. At 0.2 it all takes ten times as long, at 3 two thirds as long. +// Knob 3: Set. The characters a cell may take, each set with the space. At 0 the 27 of the structure set (. , ' ` " - _ = | / \ ( ) < > ^ v ~ o + L J 7 T Y V X); at 1 twelve of them (/ \ | - _ . ' , ( ) < >); at 2 only / \ | and -. At 3 the tone ramp . : - = + * # % @, picked by how much of the cell the outline darkens, not by its shape. +// Knob 4: Cell. The size of a character cell in LEDs: 5 by 8 at 0 (a grid of 12 by 16), 4 by 6 at 1 (16 by 21), 3 by 5 at 2 (21 by 25), and 7 by 11 at 3 (9 by 11), where each character is big enough to read. + +const W = 64, H = 128; +const { floor, round, min, max, abs, sqrt, sin, cos, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const smooth = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + out[1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + out[2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; +} + +// The paper (Xu, Zhang and Wong 2010) compares a cell and a character with an +// alignment-insensitive shape similarity and also deforms the drawing to suit +// the characters. Here the drawing is never deformed, and the comparison is a +// symmetric chamfer distance that lets the character slide by up to two LEDs +// across and one down, each LED of slide costing a little. + +// ---- The characters. Each is drawn as pen strokes in a unit cell, across +// then down in tenths, so (0,0) is the top left corner and (10,10) the +// bottom right; a stroke of one point is a dot. Strokes run to the cell's +// edge where the character's line would go on into its neighbour, so two +// slashes side by side meet. The second field is a class for colour: +// 0 straight lines, 1 curves and angles, 2 corners and crossings, 3 dots. +const GLYPHS = [ + [" ", 3, []], + [".", 3, [[5, 10]]], + [",", 3, [[6, 8, 4, 10]]], + ["'", 3, [[5, 0, 5, 2]]], + ["`", 3, [[3, 0, 6, 3]]], + ['"', 3, [[2, 0, 2, 2], [8, 0, 8, 2]]], + [":", 3, [[5, 3], [5, 8]]], + ["-", 0, [[0, 5, 10, 5]]], + ["_", 0, [[0, 10, 10, 10]]], + ["=", 0, [[0, 3, 10, 3], [0, 7, 10, 7]]], + ["|", 0, [[5, 0, 5, 10]]], + ["/", 0, [[0, 10, 10, 0]]], + ["\\", 0, [[0, 0, 10, 10]]], + ["(", 1, [[8, 0, 4, 2, 2, 5, 4, 8, 8, 10]]], + [")", 1, [[2, 0, 6, 2, 8, 5, 6, 8, 2, 10]]], + ["<", 1, [[10, 0, 0, 5, 10, 10]]], + [">", 1, [[0, 0, 10, 5, 0, 10]]], + ["^", 1, [[0, 5, 5, 0, 10, 5]]], + ["v", 1, [[0, 4, 5, 10, 10, 4]]], + ["~", 1, [[0, 6, 3, 4, 7, 6, 10, 4]]], + ["o", 1, [[3, 4, 7, 4, 9, 6, 9, 8, 7, 10, 3, 10, 1, 8, 1, 6, 3, 4]]], + ["+", 2, [[0, 5, 10, 5], [5, 1, 5, 9]]], + ["L", 2, [[0, 0, 0, 10, 10, 10]]], + ["J", 2, [[10, 0, 10, 10, 0, 10]]], + ["7", 2, [[0, 0, 10, 0, 3, 10]]], + ["T", 2, [[0, 0, 10, 0], [5, 0, 5, 10]]], + ["Y", 2, [[0, 0, 5, 5, 10, 0], [5, 5, 5, 10]]], + ["V", 2, [[0, 0, 5, 10, 10, 0]]], + ["X", 2, [[0, 0, 10, 10], [10, 0, 0, 10]]], + ["*", 2, [[5, 2, 5, 8], [1, 3, 9, 7], [9, 3, 1, 7]]], + ["#", 2, [[3, 1, 3, 9], [7, 1, 7, 9], [0, 3, 10, 3], [0, 7, 10, 7]]], + ["%", 2, [[10, 0, 0, 10], [1, 1], [9, 9]]], + ["@", 2, [[10, 10, 0, 10, 0, 0, 10, 0, 10, 7, 4, 7, 4, 4, 7, 4]]], +]; +const GI = {}; +GLYPHS.forEach((g, i) => (GI[g[0]] = i)); +const pick = (s) => Array.from(s, (c) => GI[c]); +// The character sets, each with the space: 27 characters for structure, 12 +// of them, the four straight strokes, and a tone ramp of nine by ink. +const SETS = [ + pick(" .,'`\"-_=|/\\()<>^v~o+LJ7TYVX"), + pick(" /\\|-_.',()<>"), + pick(" /\\|-"), + pick(" .:-=+*#%@"), +]; +const RAMP = SETS[3]; + +// The cell sizes: width, height. +const CELLS = [ + [5, 8], + [4, 6], + [3, 5], + [7, 11], +]; +// The distance is capped at CAP LEDs; a shift of the character by one LED +// across or down costs SHIFT. +const CAP = 3, SHIFT = 0.12; +const SX = (cw) => (cw >= 4 ? 2 : 1), SY = 1; + +// Each character at each cell size: its bitmap, its lit LEDs, and the +// distance from each LED of the cell and a border round it to the nearest +// lit LED, so it can be compared shifted. +function raster(strokes, cw, ch) { + const bm = new Uint8Array(cw * ch); + for (const pl of strokes) { + const P = []; + for (let i = 0; i < pl.length; i += 2) P.push([(pl[i] / 10) * (cw - 1), (pl[i + 1] / 10) * (ch - 1)]); + if (P.length === 1) bm[round(P[0][1]) * cw + round(P[0][0])] = 1; + for (let i = 0; i + 1 < P.length; i++) { + const [x0, y0] = P[i], [x1, y1] = P[i + 1]; + const n = max(1, Math.ceil(max(abs(x1 - x0), abs(y1 - y0)))); + for (let k = 0; k <= n; k++) bm[floor(y0 + ((y1 - y0) * k) / n + 0.5) * cw + floor(x0 + ((x1 - x0) * k) / n + 0.5)] = 1; + } + } + return bm; +} +const FONT = CELLS.map(([cw, ch]) => { + const bx = SX(cw), by = SY, ew = cw + 2 * bx, eh = ch + 2 * by; + return GLYPHS.map((g) => { + const bm = raster(g[2], cw, ch); + const ink = []; + for (let j = 0; j < ch; j++) for (let i = 0; i < cw; i++) if (bm[j * cw + i]) ink.push(i, j); + const dist = new Float32Array(ew * eh); + for (let b = 0; b < eh; b++) + for (let a = 0; a < ew; a++) { + let d = CAP * CAP; + for (let k = 0; k < ink.length; k += 2) { + const dx = a - bx - ink[k], dy = b - by - ink[k + 1]; + d = min(d, dx * dx + dy * dy); + } + dist[b * ew + a] = sqrt(d); + } + return { bm, ink, dist }; + }); +}); + +// ---- The big letters, as centre lines in a box 52 by 108 LEDs about the +// panel's middle, y down; the outline is where the distance to the centre +// line equals the stroke's half width. +function arc(cx, cy, rx, ry, a0, a1, n) { + const pts = []; + for (let i = 0; i <= n; i++) { + const a = ((a0 + ((a1 - a0) * i) / n) * PI) / 180; + pts.push(cx + rx * cos(a), cy + ry * sin(a)); + } + return pts; +} +const LETTERS = [ + { r: 7, lines: [[-22, 52, 0, -50, 22, 52], [-12, 18, 12, 18]] }, + { r: 7, lines: [[...arc(1, -28, 20, 24, -25, -270, 12), ...arc(0, 25, 22, 27, -90, 155, 12)]] }, + { r: 6, lines: [[23, 52, -10, 0, ...arc(-3, -32, 12, 19, 130, 405, 12), -16, 8, ...arc(-4, 32, 20, 21, 210, 60, 10), 21, 18]] }, + { r: 4.5, lines: [[-22, -52, 20, -52, 20, -31], [-27, -14, 27, -14], [-20, 5, 20, 5, 20, 26, -20, 26, -20, 47, 22, 47]] }, +]; +for (const L of LETTERS) { + const s = []; + for (const pl of L.lines) for (let i = 0; i + 3 < pl.length; i += 2) s.push(pl[i], pl[i + 1], pl[i + 2], pl[i + 3]); + L.seg = new Float32Array(s); +} + +function lineDist(seg, x, y) { + let best = 1e9; + for (let i = 0; i < seg.length; i += 4) { + const ax = seg[i], ay = seg[i + 1], bx = seg[i + 2] - ax, by = seg[i + 3] - ay; + const px = x - ax, py = y - ay, L = bx * bx + by * by; + let t = L > 0 ? (px * bx + py * by) / L : 0; + t = t < 0 ? 0 : t > 1 ? 1 : t; + const dx = px - t * bx, dy = py - t * by, d = dx * dx + dy * dy; + if (d < best) best = d; + } + return sqrt(best); +} + +const LW = W + 1, LH = H + 1, CW = 33, CH = 65; + +export function setup(params) { + params.s = { + t: 0, hue: 0, set: 0, cell: 0, + // The field on every second LED corner, then on every corner. + g: new Float32Array(CW * CH), f: new Float32Array(LW * LH), + // The outline's piece in each LED square: up to two segments. + sq: new Float32Array(W * H * 8), nsq: new Uint8Array(W * H), + // Per cell: its character, and a hash of the LEDs the outline crossed. + ch: new Int16Array(32 * 32), sig: new Int32Array(32 * 32), + dt: new Float32Array(32 * 32), segs: new Float32Array(400), tl: new Int16Array(200), + rgb: [0, 0, 0], + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + S.hue = k[0]; + S.t += cl(dt || 0, 0, 0.1) * cl(k[1], 0.2, 3) / 2; + S.set = cl(round(k[2]), 0, 3); + const c = cl(round(k[3]), 0, CELLS.length - 1); + if (c !== S.cell || S.set !== S.lastSet) { S.ch.fill(0); S.sig.fill(0); } + S.cell = c; + S.lastSet = S.set; +} + +// One letter holds HOLD units of the clock, then melts into the next in MELT. +const HOLD = 4.5, MELT = 1.5, PER = HOLD + MELT; + +function field(S) { + const n = floor(S.t / PER), u = S.t - n * PER; + const A = LETTERS[n % LETTERS.length], B = LETTERS[(n + 1) % LETTERS.length]; + const m = smooth((u - HOLD) / MELT); + const th = 0.12 * sin((2 * PI * S.t) / 9), cs = cos(th), sn = sin(th), sc = 1 / 0.9; + const g = S.g, f = S.f; + for (let j = 0; j < CH; j++) + for (let i = 0; i < CW; i++) { + const X = 2 * i - 32, Y = 2 * j - 64; + const x = (cs * X + sn * Y) * sc, y = (-sn * X + cs * Y) * sc; + let v = lineDist(A.seg, x, y) - A.r; + if (m > 0) v = (1 - m) * v + m * (lineDist(B.seg, x, y) - B.r); + g[j * CW + i] = v; + } + // In between, the mean of the neighbours on the coarse lattice. + for (let j = 0; j < LH; j++) { + const j0 = j >> 1, j1 = (j + 1) >> 1; + for (let i = 0; i < LW; i++) { + const i0 = i >> 1, i1 = (i + 1) >> 1; + f[j * LW + i] = 0.25 * (g[j0 * CW + i0] + g[j0 * CW + i1] + g[j1 * CW + i0] + g[j1 * CW + i1]); + } + } +} + +// Marching squares on the LED corners: the outline's piece in each LED square. +function contour(S) { + const f = S.f, sq = S.sq, nsq = S.nsq; + const P = new Float32Array(8); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const a = f[y * LW + x], b = f[y * LW + x + 1], c = f[(y + 1) * LW + x + 1], d = f[(y + 1) * LW + x]; + const q = y * W + x, ia = a < 0, ib = b < 0, ic = c < 0, id = d < 0; + nsq[q] = 0; + if (ia === ib && ib === ic && ic === id) continue; + // Where the outline crosses the top, right, bottom and left edges. + let np = 0; + if (ia !== ib) { P[np++] = x + a / (a - b); P[np++] = y; } + if (ib !== ic) { P[np++] = x + 1; P[np++] = y + b / (b - c); } + if (id !== ic) { P[np++] = x + d / (d - c); P[np++] = y + 1; } + if (ia !== id) { P[np++] = x; P[np++] = y + a / (a - d); } + const o = q * 8; + for (let k = 0; k < np; k++) sq[o + k] = P[k]; + if (np === 8 && (a + b + c + d < 0) !== ia) { + // A saddle: pair top with left and right with bottom. + sq[o + 2] = P[6]; sq[o + 3] = P[7]; sq[o + 4] = P[2]; sq[o + 5] = P[3]; sq[o + 6] = P[4]; sq[o + 7] = P[5]; + } + nsq[q] = np >> 2; + } +} + +// The chamfer distance between a character shifted by sx, sy and the +// outline: the distance from each of its lit LEDs to the outline, and from +// each LED the outline crosses to its nearest lit LED, over the count of both. +function cham(G, dT, tl, nt, ew, bx, by, sx, sy) { + const ink = G.ink, dist = G.dist; + let cost = 0; + for (let k = 0; k < ink.length; k += 2) cost += dT[(ink[k + 1] + sy + by) * ew + ink[k] + sx + bx]; + const so = -sy * ew - sx; + for (let k = 0; k < nt; k++) cost += dist[tl[k] + so]; + return cost / (ink.length / 2 + nt); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + field(S); + contour(S); + const [cw, ch] = CELLS[S.cell], font = FONT[S.cell]; + const cols = floor(W / cw), rows = floor(H / ch); + const ox = floor((W - cols * cw) / 2), oy = floor((H - rows * ch) / 2); + const set = SETS[S.set], tone = S.set === 3; + const bx = SX(cw), by = SY, ew = cw + 2 * bx, eh = ch + 2 * by; + const sq = S.sq, nsq = S.nsq, rgb = S.rgb, dT = S.dt, segs = S.segs, tl = S.tl; + for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) display.setPixel(x, y, 0, 0, 0); + const h0 = 0.47 + S.hue; + // The outline itself, faint. + hsv(h0 + 0.18, 0.7, 0.24, rgb); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) + if (nsq[y * W + x]) display.setPixel(x, y, rgb[0] | 0, rgb[1] | 0, rgb[2] | 0); + for (let r = 0; r < rows; r++) + for (let c = 0; c < cols; c++) { + const gx = ox + c * cw, gy = oy + r * ch, id = r * 32 + c; + // The outline's pieces in this cell, and the LEDs they cross. + let ns = 0, nt = 0; + for (let j = 0; j < ch; j++) + for (let i = 0; i < cw; i++) { + const q = (gy + j) * W + gx + i; + if (!nsq[q]) continue; + tl[nt++] = j * ew + i + by * ew + bx; + for (let k = 0; k < nsq[q] * 4; k += 4) { + const o = q * 8 + k; + segs[ns++] = sq[o] - gx; segs[ns++] = sq[o + 1] - gy; segs[ns++] = sq[o + 2] - gx; segs[ns++] = sq[o + 3] - gy; + } + } + if (!nt) { S.ch[id] = 0; continue; } + // Only a cell whose crossed LEDs changed is matched again. + let sig = nt; + for (let k = 0; k < nt; k++) sig = (sig * 31 + tl[k]) | 0; + if (sig === S.sig[id] && S.ch[id]) { paint(display, S, S.ch[id], font, gx, gy, cw, ch, h0); continue; } + S.sig[id] = sig; + // Distance from each LED centre of the cell and its border to the outline. + for (let b = 0; b < eh; b++) + for (let a = 0; a < ew; a++) { + const px = a - bx + 0.5, py = b - by + 0.5; + let best = CAP * CAP; + for (let i = 0; i < ns; i += 4) { + const ax = segs[i], ay = segs[i + 1], ux = segs[i + 2] - ax, uy = segs[i + 3] - ay; + const vx = px - ax, vy = py - ay, L = ux * ux + uy * uy; + let t = L > 0 ? (vx * ux + vy * uy) / L : 0; + t = t < 0 ? 0 : t > 1 ? 1 : t; + const dx = vx - t * ux, dy = vy - t * uy, d = dx * dx + dy * dy; + if (d < best) best = d; + } + dT[b * ew + a] = sqrt(best); + } + let g = 0; + if (tone) { + // Tone: how much of the cell the outline darkens, on the ramp from the space. + let ink = 0; + for (let j = 0; j < ch; j++) for (let i = 0; i < cw; i++) ink += max(0, 1 - dT[(j + by) * ew + i + bx]); + g = RAMP[min(9, round((ink / (cw * ch)) * 22))]; + } else { + // Each character of the set at every shift; the cheapest wins. + const prev = S.ch[id]; + let bestC = 1e9, prevC = 1e9; + for (const gi of set) { + const G = font[gi]; + let cbest = 1e9; + for (let sy = -by; sy <= by; sy++) + for (let sx = -bx; sx <= bx; sx++) { + const cost = cham(G, dT, tl, nt, ew, bx, by, sx, sy) + SHIFT * (abs(sx) + abs(sy)); + if (cost < cbest) cbest = cost; + } + if (cbest < bestC) { bestC = cbest; g = gi; } + if (gi === prev) prevC = cbest; + } + // Hold on to the last character unless a new one fits clearly better. + if (prevC < bestC + 0.08) g = prev; + } + S.ch[id] = g; + if (g) paint(display, S, g, font, gx, gy, cw, ch, h0); + } +} + +function paint(display, S, g, font, gx, gy, cw, ch, h0) { + const G = GLYPHS[g], bm = font[g].bm, rgb = S.rgb; + hsv(h0 + [0, 0.12, -0.1, 0.3][G[1]], G[1] === 3 ? 0.5 : 0.85, 1, rgb); + for (let j = 0; j < ch; j++) + for (let i = 0; i < cw; i++) if (bm[j * cw + i]) display.setPixel(gx + i, gy + j, rgb[0] | 0, rgb[1] | 0, rgb[2] | 0); +} From 2a0d739ea9f520cc2bb2f9deaf29b265f043c356 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:23:24 +0000 Subject: [PATCH 08/24] incoming: Ogham, pattern round 68 --- incoming/pattern-study/patterns/ogham.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/incoming/pattern-study/patterns/ogham.js b/incoming/pattern-study/patterns/ogham.js index 824203b59..7876969bc 100644 --- a/incoming/pattern-study/patterns/ogham.js +++ b/incoming/pattern-study/patterns/ogham.js @@ -178,7 +178,7 @@ export function update(dt, input, params) { S.t += dt * S.rate; S.dim = min(1, S.dim + dt / 0.5); const c = S.cuts[current(S)]; - target = c.v + (c.g === 2 ? REACH[2] * SLANT : 0) - 80; + target = c.v + (c.g === 2 ? REACH[2] * SLANT : 0) - 92; if (S.t >= S.end) (S.phase = 1), (S.hold = 0); } else { target = S.top - 112; From b7645f7671d313ab3f0818c21c4cdded58fb32e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:23:41 +0000 Subject: [PATCH 09/24] incoming: Rose Criterion, fix the Exposure knob note Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JktahMn4mzzFyveZ8fo --- incoming/pattern-study/patterns/rose-criterion.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/incoming/pattern-study/patterns/rose-criterion.js b/incoming/pattern-study/patterns/rose-criterion.js index 686b52d4f..7574c3ba3 100644 --- a/incoming/pattern-study/patterns/rose-criterion.js +++ b/incoming/pattern-study/patterns/rose-criterion.js @@ -13,7 +13,7 @@ // // Knob 1: Hue. The colour of the light: at 0 cyan grain with a gold k = 5 line, at 0.25 violet with a green line, at 0.5 coral with a blue line, at 0.75 lime with a magenta line. // Knob 2: Speed. How fast each exposure builds: at 2 the dose comes in over 3 s, holds 1.2 s and clears; at 0.2 it comes in over 30 s, at 3 over 2 s. The photon flux is the dose over that time. -// Knob 3: Exposure. The dose a build stops at, in photons per LED: 25 at 0, 224 at 0.5, 2000 at 1. At 25 the k = 5 line stops at C·d = 1.15 with 7 of the 50 discs still in the grain; from 0.27 on every disc is seen, the last one (contrast 0.15, 9 LEDs) at 124 photons, and the rest of the dose only calms the grain. +// Knob 3: Exposure. The dose a build stops at, in photons per LED: 25 at 0, 224 at 0.5, 2000 at 1. At 25 the k = 5 line stops at C·d = 1.15 with 7 of the 50 discs still in the grain; from 0.37 on every disc is seen, the last one (contrast 0.15, 9 LEDs) at 124 photons, and the rest of the dose only calms the grain. // Knob 4: Read noise. Gaussian noise each channel adds every time it reads its LED, 1 photon rms at 0 and 9 at 1. It adds to the shot noise, so the grain coarsens and the k = 5 line, drawn with it counted in, slows: at 1 the first disc needs 8 photons instead of 1.3, and at 25 photons 24 discs stay hidden. // Rose (1948): a disc of area A whose brightness differs by a fraction C From 64aa99db88a99235199551c61f9c07e327f872f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:24:23 +0000 Subject: [PATCH 10/24] incoming: Josephus Problem, pattern round 68 --- incoming/pattern-study/patterns/josephus.js | 277 ++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 incoming/pattern-study/patterns/josephus.js diff --git a/incoming/pattern-study/patterns/josephus.js b/incoming/pattern-study/patterns/josephus.js new file mode 100644 index 000000000..949a59d45 --- /dev/null +++ b/incoming/pattern-study/patterns/josephus.js @@ -0,0 +1,277 @@ +// ===== Patternflow pattern ===== +// Title: Josephus Problem +// Family: rhythm +// Idea: A circle of n lamps fills the top half and a hand goes round it one place a tick, each lamp it passes still lit a beat and every k-th beat an accent that puts that lamp out, so the beat thins into rests as the circle empties, with a roll inside the ring drawing that rhythm and the survivor's number coming up when one lamp is left, while the bottom half holds the table of every circle from 1 to 64 lamps, one row each, coloured by the order its places went out with the survivor white, so the survivors J(n,k) run down the panel as a sawtooth and the row of the circle on top lights as it plays. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Josephus problem (Flavius Josephus, The Jewish War, 1st century; counting-out puzzle in Claude Gaspard Bachet's Problèmes plaisans et délectables, 1612), closed form J(2^m + L) = 2L + 1 (Graham, Knuth and Patashnik, Concrete Mathematics, 1989, chapter 1), pattern round 68 +// Round: 68 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Step=2..7, Circle=8..64 +// +// Knob 1: Hue. Turns the colours of the places put out round the wheel: at 0 the first ones out are a deep blue and the later ones teal, green and gold up to the white survivor, with the lamps still lit a cool white; at 0.25 they run from rose through orange and yellow to green, at 0.5 from yellow-green through red and magenta to blue. +// Knob 2: Speed. The counting tempo: at 2 the hand passes 2(2 + n/6) places a second while every lamp is lit, 6.7 on the opening circle of 8 and 25 on a circle of 64, and hurries by the square root of n over the lamps left as they go out, so the circle of 8 is down to one lamp in 2.1 s and the circle of 64 in 6.4 s, each survivor then holding 1.3 s. At 0.2 the circle of 8 takes 21 s, at 3 it takes 1.4 s. +// Knob 3: Step. k, the beat that goes out, 2 to 7. At 2, the classic, every second lamp goes, a circle of 16 halves its beats every time round (xXxX, x.X., x...X...) and the survivor column climbs 2 places a row and drops back to place 1 at 4, 8, 16, 32 and 64; at 3 it climbs 3 a row and drops at 4, 6, 9, 14, 21, 31 and 47; at 7 there are 15 drops and the roll counts up 1 to 7 in a staircase. A new k redraws the table and starts the circle on top again. +// Knob 4: Circle. The circle the top starts from, 8 to 64 lamps, and the row of the table that lights with it. After each survivor the next circle has one lamp more, so from 8 the top reaches 64 in 6 minutes at Speed 2, and after 64 it comes back to this one. + +const W = 64, H = 128, NMAX = 64, TOP = 63; +const { floor, round, min, max, abs, sqrt, exp, atan2, pow, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The circle: centre (32, 32) in LED units, lamps between radius 22 and +// 30.5, the hand's marker just inside them, place 0 at twelve o'clock and +// the places counting clockwise. +const CX = 32, CY = 32, R1 = 22, R2 = 30.5, HR0 = 18.8, HR1 = 21.2, BR = 17.5; +// The rhythm roll inside the circle: one column per tick, newest on the +// right, bars standing on row SB. +const SX0 = 14, SX1 = 49, SB = 43, SN = SX1 - SX0 + 1; +// The hue of the first place out at knob 0, a deep blue. +const HUE0 = 0.7, WHITE = [1, 1, 1]; + +// 3 x 5 digits, row by row from the top, three bits a row. +const FONT = [ + 0b111101101101111, 0b010110010010111, 0b111001111100111, 0b111001111001111, 0b101101111001001, + 0b111100111001111, 0b111100111101111, 0b111001001001001, 0b111101111101111, 0b111101111001111, +]; + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + o[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + o[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; + return o; +} + +// Every pixel of the top square that a lamp or the hand can light, with its +// angle as a fraction of a turn clockwise from twelve and its radius. +const RING = (() => { + const idx = [], ang = [], rad = []; + for (let y = 0; y < 64; y++) + for (let x = 0; x < W; x++) { + const dx = x + 0.5 - CX, dy = y + 0.5 - CY, r = sqrt(dx * dx + dy * dy); + if (r < BR || r > R2 + 0.6) continue; + let a = atan2(dx, -dy) / (2 * PI); + if (a < 0) a += 1; + idx.push(y * W + x), ang.push(a), rad.push(r); + } + return { idx: Int32Array.from(idx), ang: Float32Array.from(ang), rad: Float32Array.from(rad) }; +})(); + +// The table: for each circle n = 1..64 and each place, the turn at which it +// went out, 0 first and n - 1 the survivor. Counting starts at place 0 as 1, +// the k-th lamp counted goes out and counting goes on from the next lamp +// still lit, the same walk the hand makes on top; its survivor is J(n, k), +// with J(1) = 0 and J(n) = (J(n - 1) + k) mod n. +function table(k) { + const ord = [], surv = new Int32Array(NMAX + 1); + for (let n = 1; n <= NMAX; n++) { + const o = new Int32Array(n), on = new Uint8Array(n).fill(1); + let left = n, count = 0, r = 0; + for (let j = 0; left > 1; j++) { + const q = j % n; + if (on[q] && ++count === k) (on[q] = 0), (o[q] = r++), (count = 0), left--; + } + surv[n] = on.indexOf(1); + o[surv[n]] = n - 1; + ord[n] = o; + } + return { ord, surv }; +} + +// A fresh circle of n lamps, all lit, the hand just before place 0. +function begin(S, n) { + S.n = n, S.alive.fill(0), S.alive.fill(1, 0, n), S.left = n, S.count = 0; + S.j = 0, S.ph = 0.55, S.phase = n > 1 ? 0 : 1, S.ht = 0, S.born = S.t; + S.flash.fill(0), S.gone.fill(-9); + roll(S, -1); +} + +function roll(S, mark, c) { + S.roll[S.rh] = mark; + if (c) S.rollc[S.rh * 3] = c[0], S.rollc[S.rh * 3 + 1] = c[1], S.rollc[S.rh * 3 + 2] = c[2]; + S.rh = (S.rh + 1) % SN; +} + +// The colour of place i of circle m by the turn it went out at: the first +// ones out a deep blue, then teal and green to gold for the last ones out, +// rising in light, and the survivor white. +function cellColour(S, m, i, o) { + const r = S.tab.ord[m][i]; + if (r >= m - 1) return (o[0] = 1), (o[1] = 1), (o[2] = 1), o; + const u = m > 2 ? r / (m - 2) : 0; + return hsv(HUE0 + S.hue - 0.56 * u, 0.9 - 0.3 * u, 0.17 + 0.56 * pow(u, 1.8), o); +} + +// The hand reaches place j mod n: a lit lamp is a beat, the k-th beat puts +// it out; a lamp already out is a rest. +function tick(S) { + const q = S.j % S.n; + S.j++; + if (!S.alive[q]) return roll(S, 0); + S.count++; + S.flash[q] = 1; + if (S.count < S.k) return roll(S, S.count); + S.alive[q] = 0, S.gone[q] = S.t, S.count = 0, S.left--; + roll(S, 9, cellColour(S, S.n, q, S.c)); + if (S.left <= 1) (S.phase = 1), (S.ht = 0); +} + +export function setup(params) { + const S = (params.s = { + t: 0, hue: 0, speed: 2, k: 2, n0: 8, n: 8, alive: new Uint8Array(NMAX), flash: new Float32Array(NMAX), + gone: new Float32Array(NMAX), left: 8, count: 0, j: 0, ph: 0, phase: 0, ht: 0, born: 0, + roll: new Int8Array(SN).fill(-2), rollc: new Float32Array(SN * 3), rh: 0, + fb: new Float32Array(W * H * 3), lamp: new Float32Array(NMAX * 3), burst: new Float32Array(NMAX), c: [0, 0, 0], pale: [0, 0, 0], tab: table(2), + }); + begin(S, 8); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 2, 8], d = cl(dt || 0, 0, 0.1); + S.t += d; + S.hue = kv[0]; + S.speed = cl(kv[1], 0.2, 3); + const k = cl(round(kv[2]), 2, 7), n0 = cl(round(kv[3]), 8, NMAX); + if (k !== S.k) (S.k = k), (S.tab = table(k)), begin(S, S.n); + if (n0 !== S.n0) (S.n0 = n0), begin(S, n0); + for (let i = 0; i < S.n; i++) S.flash[i] *= exp(-d / 0.09); + if (S.phase === 0) { + // Ticks a second: more for a bigger circle, and faster as it empties. + let left = d; + while (left > 0 && S.phase === 0) { + const rate = S.speed * (2 + S.n / 6) * sqrt(S.n / S.left), need = (1 - S.ph) / rate; + if (need > left) { + S.ph += left * rate; + break; + } + left -= need, S.ph = 0; + tick(S); + } + } else if ((S.ht += d) > 0.7 + 1.2 / S.speed) { + // The next circle has one lamp more; after 64 the top goes back to the + // circle the Circle knob names. + begin(S, S.n < NMAX ? S.n + 1 : S.n0); + } +} + +function add(fb, p, r, g, b, a) { + fb[p * 3] += r * a, fb[p * 3 + 1] += g * a, fb[p * 3 + 2] += b * a; +} + +// A number in doubled 3 x 5 digits, centred on column cx, its top on row y. +function number(fb, v, cx, y, col, a) { + const str = String(v), x0 = round(cx - (str.length * 8 - 2) / 2); + for (let d = 0; d < str.length; d++) { + const g = FONT[str.charCodeAt(d) - 48]; + for (let r = 0; r < 5; r++) + for (let q = 0; q < 3; q++) + if ((g >> (14 - r * 3 - q)) & 1) + for (let e = 0; e < 4; e++) add(fb, (y + r * 2 + (e >> 1)) * W + x0 + d * 8 + q * 2 + (e & 1), col[0], col[1], col[2], a); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const fb = S.fb, c = S.c, n = S.n, t = S.t, lamp = S.lamp; + fb.fill(0); + // Lamps still lit are a cool white that the hue knob tints a little. + const pale = hsv(HUE0 + S.hue - 0.1, 0.14, 0.9, S.pale); + const fin = S.phase === 1, glow = fin ? 0.8 + 0.2 * Math.cos(S.ht * 9) : 0; + const fresh = cl((t - S.born) / 0.25, 0, 1); + + // Lamp colours for this frame. + for (let i = 0; i < n; i++) { + let r, g, b; + if (S.alive[i]) { + const f = S.flash[i], v = fin ? glow : (0.5 + 0.5 * f) * fresh; + r = fin ? 1 : pale[0] + (1 - pale[0]) * f, g = fin ? 0.97 : pale[1] + (1 - pale[1]) * f, b = fin ? 0.9 : pale[2] + (1 - pale[2]) * f; + r *= v, g *= v, b *= v; + } else { + cellColour(S, n, i, c); + const a = exp(-(t - S.gone[i]) / 0.22), dim = fin ? 0.7 : 0.42; + r = c[0] * dim + a, g = c[1] * dim + a * 0.95, b = c[2] * dim + a * 0.9; + } + lamp[i * 3] = r, lamp[i * 3 + 1] = g, lamp[i * 3 + 2] = b; + S.burst[i] = S.alive[i] ? 0 : exp(-(t - S.gone[i]) / 0.2); + } + + // The lamps, as n wedges of the ring with a gap of about one LED between + // them, a lamp put out bursting inward for a moment, and the hand as a + // short bright mark just inside them. + const hand = fin ? -1 : (((S.j - 1 + S.ph) % n) + n) % n; + const I = RING.idx, A = RING.ang, RR = RING.rad; + for (let q = 0; q < I.length; q++) { + const r = RR[q], p = I[q]; + const u = A[q] * n + 0.5, i = floor(u) % n, f = u - floor(u); + const side = cl(min(f, 1 - f) * ((2 * PI * r) / n) - 0.1, 0, 1); + if (r >= R1 - 0.5) { + const a = side * cl(r - R1 + 0.5, 0, 1) * cl(R2 - r + 0.5, 0, 1); + if (a > 0) add(fb, p, lamp[i * 3], lamp[i * 3 + 1], lamp[i * 3 + 2], a); + continue; + } + if (S.burst[i] > 0.02) add(fb, p, 1, 0.95, 0.9, S.burst[i] * side * (0.35 + 0.65 * cl((r - BR) / (R1 - BR), 0, 1))); + if (hand >= 0 && r >= HR0 && r <= HR1) { + let da = abs(A[q] - hand / n); + if (da > 0.5) da = 1 - da; + const a = cl(1.7 - da * 2 * PI * r, 0, 1); + if (a > 0) add(fb, p, 1, 1, 1, a * 0.9); + } + } + + // The circle's size on top inside the ring, and once it is down to one + // lamp the survivor's place counted from 1, in white. + if (fin) number(fb, S.tab.surv[n] + 1, CX, 15, WHITE, glow); + else number(fb, n, CX, 15, pale, 0.55); + + // The rhythm roll: a rest is a dot, a beat a bar as tall as its count, the + // k-th beat a full bar in the colour of the lamp it put out; a new circle + // leaves a dotted rule. + for (let s = 0; s < SN; s++) { + const h = (S.rh + s) % SN, m = S.roll[h], x = SX0 + s, v = 0.3 + 0.7 * ((s + 1) / SN); + if (m === -2) continue; + if (m === -1) for (let y = SB - 12; y <= SB; y += 2) add(fb, y * W + x, 0.35, 0.35, 0.4, v); + else if (m === 0) add(fb, SB * W + x, 0.4, 0.38, 0.36, v); + else if (m < 9) { + const bh = 1 + round((10 * m) / S.k); + for (let y = SB - bh + 1; y <= SB; y++) add(fb, y * W + x, pale[0], pale[1], pale[2], v * 0.8); + } else { + const rc = S.rollc; + for (let y = SB - 12; y <= SB; y++) add(fb, y * W + x, rc[h * 3] * 0.6 + 0.4, rc[h * 3 + 1] * 0.6 + 0.4, rc[h * 3 + 2] * 0.6 + 0.4, v); + } + } + + // The table: circle m on panel row 63 + m, place i in column i, the + // current row lit and filling in as the circle plays it, with the hand + // as a white cell. + for (let m = 1; m <= NMAX; m++) { + const y = TOP + m, cur = m === n, gain = cur ? 1 : 0.9; + for (let i = 0; i < m; i++) { + const p = y * W + i; + if (cur && S.alive[i] && !fin) { + add(fb, p, pale[0], pale[1], pale[2], 0.55 * fresh + 0.45 * S.flash[i]); + continue; + } + cellColour(S, m, i, c); + let a = gain; + if (cur && !fin) a += exp(-(t - S.gone[i]) / 0.22); + if (cur && fin && S.tab.surv[m] === i) a = 0.5 + 0.7 * glow; + add(fb, p, c[0], c[1], c[2], a); + } + if (cur) { + // The current row runs out to the right edge as a dotted line. + for (let x = m + 1; x < W; x += 2) add(fb, y * W + x, pale[0], pale[1], pale[2], 0.35); + if (hand >= 0) add(fb, y * W + (round(hand) % m), 1, 1, 1, 1); + } + } + + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const k = (y * W + x) * 3; + display.setPixel(x, y, min(255, fb[k] * 255) | 0, min(255, fb[k + 1] * 255) | 0, min(255, fb[k + 2] * 255) | 0); + } +} From 0649cc7879dc9158b126be919de26c630bf5e1ee Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:24:25 +0000 Subject: [PATCH 11/24] incoming: Structure ASCII, pattern round 68 --- .../pattern-study/patterns/structure-ascii.js | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/incoming/pattern-study/patterns/structure-ascii.js b/incoming/pattern-study/patterns/structure-ascii.js index 5e11431e6..d519fa64b 100644 --- a/incoming/pattern-study/patterns/structure-ascii.js +++ b/incoming/pattern-study/patterns/structure-ascii.js @@ -33,7 +33,8 @@ function hsv(h, s, v, out) { // alignment-insensitive shape similarity and also deforms the drawing to suit // the characters. Here the drawing is never deformed, and the comparison is a // symmetric chamfer distance that lets the character slide by up to two LEDs -// across and one down, each LED of slide costing a little. +// across (one in a 3 by 5 cell) and one up or down, at 0.12 for each LED of +// slide; the character is still drawn in its own cell. // ---- The characters. Each is drawn as pen strokes in a unit cell, across // then down in tenths, so (0,0) is the top left corner and (10,10) the @@ -256,16 +257,15 @@ function contour(S) { } // The chamfer distance between a character shifted by sx, sy and the -// outline: the distance from each of its lit LEDs to the outline, and from -// each LED the outline crosses to its nearest lit LED, over the count of both. -function cham(G, dT, tl, nt, ew, bx, by, sx, sy) { - const ink = G.ink, dist = G.dist; - let cost = 0; - for (let k = 0; k < ink.length; k += 2) cost += dT[(ink[k + 1] + sy + by) * ew + ink[k] + sx + bx]; - const so = -sy * ew - sx; - for (let k = 0; k < nt; k++) cost += dist[tl[k] + so]; - return cost / (ink.length / 2 + nt); -} +// outline is the distance from each of its lit LEDs to the outline, plus +// the distance from each LED the outline crosses to its nearest lit LED, +// over the count of both, plus SHIFT for each LED of shift. The shifts of +// each cell size, cheapest first: sx, sy, cost. +const SHIFTS = CELLS.map(([cw]) => { + const L = []; + for (let sy = -SY; sy <= SY; sy++) for (let sx = -SX(cw); sx <= SX(cw); sx++) L.push([sx, sy, SHIFT * (abs(sx) + abs(sy))]); + return L.sort((a, b) => a[2] - b[2]).flat(); +}); export function draw(display, params) { const S = params.s; @@ -275,7 +275,7 @@ export function draw(display, params) { const [cw, ch] = CELLS[S.cell], font = FONT[S.cell]; const cols = floor(W / cw), rows = floor(H / ch); const ox = floor((W - cols * cw) / 2), oy = floor((H - rows * ch) / 2); - const set = SETS[S.set], tone = S.set === 3; + const set = SETS[S.set], tone = S.set === 3, shifts = SHIFTS[S.cell]; const bx = SX(cw), by = SY, ew = cw + 2 * bx, eh = ch + 2 * by; const sq = S.sq, nsq = S.nsq, rgb = S.rgb, dT = S.dt, segs = S.segs, tl = S.tl; for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) display.setPixel(x, y, 0, 0, 0); @@ -328,19 +328,28 @@ export function draw(display, params) { for (let j = 0; j < ch; j++) for (let i = 0; i < cw; i++) ink += max(0, 1 - dT[(j + by) * ew + i + bx]); g = RAMP[min(9, round((ink / (cw * ch)) * 22))]; } else { - // Each character of the set at every shift; the cheapest wins. + // Each character of the set at every shift, the cell's last character + // first; a shift is given up as soon as its sum cannot win. const prev = S.ch[id]; let bestC = 1e9, prevC = 1e9; - for (const gi of set) { - const G = font[gi]; + for (let k = -1; k < set.length; k++) { + const gi = k < 0 ? prev : set[k]; + if (k >= 0 && gi === prev) continue; + const G = font[gi], ink = G.ink, dist = G.dist, n = ink.length / 2 + nt; let cbest = 1e9; - for (let sy = -by; sy <= by; sy++) - for (let sx = -bx; sx <= bx; sx++) { - const cost = cham(G, dT, tl, nt, ew, bx, by, sx, sy) + SHIFT * (abs(sx) + abs(sy)); - if (cost < cbest) cbest = cost; - } + for (let m = 0; m < shifts.length; m += 3) { + const sx = shifts[m], sy = shifts[m + 1], pen = shifts[m + 2]; + const lim = ((k < 0 ? cbest : min(cbest, bestC)) - pen) * n; + if (lim <= 0) break; + let cost = 0; + const io = (sy + by) * ew + sx + bx; + for (let i = 0; i < ink.length && cost < lim; i += 2) cost += dT[ink[i + 1] * ew + ink[i] + io]; + const so = -sy * ew - sx; + for (let i = 0; i < nt && cost < lim; i++) cost += dist[tl[i] + so]; + if (cost < lim) cbest = cost / n + pen; + } + if (k < 0) prevC = cbest; if (cbest < bestC) { bestC = cbest; g = gi; } - if (gi === prev) prevC = cbest; } // Hold on to the last character unless a new one fits clearly better. if (prevC < bestC + 0.08) g = prev; From f0b5db062ff97a18fff301290d73c42b9ac68d1d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:24:44 +0000 Subject: [PATCH 12/24] incoming: Fibonacci Word, pattern round 68 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JktahMn4mzzFyveZ8fo --- .../pattern-study/patterns/fibonacci-word.js | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/incoming/pattern-study/patterns/fibonacci-word.js b/incoming/pattern-study/patterns/fibonacci-word.js index 5b2328b61..eea831e45 100644 --- a/incoming/pattern-study/patterns/fibonacci-word.js +++ b/incoming/pattern-study/patterns/fibonacci-word.js @@ -138,7 +138,9 @@ function project(P, N, cam, X, Y) { // Least-squares similarity z -> alpha z + beta (or alpha conj z + beta) // taking the points (ax, ay) onto (bx, by). Returns [ar, ai, br, bi, err]. -function simFit(n, ax, ay, bx, by, conj) { +// With snap the turn of alpha is rounded to a quarter, so a curve on the +// grid stays on it. +function simFit(n, ax, ay, bx, by, conj, snap) { let mx = 0, my = 0, nx = 0, ny = 0; for (let j = 0; j < n; j++) mx += ax(j), my += (conj ? -1 : 1) * ay(j), nx += bx(j), ny += by(j); mx /= n, my /= n, nx /= n, ny /= n; @@ -147,7 +149,12 @@ function simFit(n, ax, ay, bx, by, conj) { const u = ax(j) - mx, v = (conj ? -1 : 1) * ay(j) - my, p = bx(j) - nx, q = by(j) - ny; sr += u * p + v * q, si += u * q - v * p, ss += u * u + v * v; } - const ar = sr / max(1e-9, ss), ai = si / max(1e-9, ss), br = nx - (ar * mx - ai * my), bi = ny - (ai * mx + ar * my); + let ar = sr / max(1e-9, ss), ai = si / max(1e-9, ss); + if (snap) { + const r = hypot(ar, ai), q = (((round(atan2(ai, ar) / (PI / 2)) % 4) + 4) % 4); + (ar = r * [1, 0, -1, 0][q]), (ai = r * [0, 1, 0, -1][q]); + } + const br = nx - (ar * mx - ai * my), bi = ny - (ai * mx + ar * my); let err = 0; for (let j = 0; j < n; j++) { const u = ax(j), v = (conj ? -1 : 1) * ay(j); @@ -167,7 +174,7 @@ function geometry(n, deg, mir, keepPsi) { const X = new Float64Array(N + 1), Y = new Float64Array(N + 1); project(P, N, cam, X, Y); const fits = [false, true].map((cj) => - simFit(Nc + 1, (j) => P[2 * j], (j) => P[2 * j + 1], (j) => X[BLK[j]], (j) => Y[BLK[j]], cj)); + simFit(Nc + 1, (j) => P[2 * j], (j) => P[2 * j + 1], (j) => X[BLK[j]], (j) => Y[BLK[j]], cj, grid)); const f = fits[0][4] < fits[1][4] ? fits[0] : fits[1], cj = f === fits[1]; const GX = new Float64Array(Nc + 1), GY = new Float64Array(Nc + 1); for (let j = 0; j <= Nc; j++) { @@ -289,7 +296,7 @@ export function draw(display, params) { if (i > 0) { const j = i - 1, pre = j < Nc; const u = pre ? ((j + 0.5) / N) * (1 - e) + ((BLK[j] + BLK[j + 1]) / (2 * N)) * e : (j + 0.5) / N; - const v = pre ? 1 : out * (1 - 0.4 * e); + const v = pre ? 1 : 0.45 * out; if (v > 0.01) { const w = pre ? 0.55 * (1 - e) : 0; col(u, v, w); @@ -302,10 +309,11 @@ export function draw(display, params) { // The coarse curve, each step fading as the pen finishes its block. const rate = max(0.5, 70 / max(0.05, g.step)); for (let j = 0; j < Nc; j++) { - const gone = (S.pen - BLK[j + 1]) / (0.3 * rate), a = S.gl * (1 - cl(gone, 0, 1)); + const gone = (S.pen - BLK[j + 1]) / (0.3 * rate) + (S.phase === 1 ? 8 * S.pt : 0), a = S.gl * (1 - cl(gone, 0, 1)); if (a <= 0.01) continue; col((BLK[j] + BLK[j + 1]) / (2 * N), a, 0.15); - lineAA(acc, g.GX[j], g.GY[j], g.GX[j + 1], g.GY[j + 1], c[0], c[1], c[2], 1); + if (g.grid) lineGrid(acc, round(g.GX[j]), round(g.GY[j]), round(g.GX[j + 1]), round(g.GY[j + 1]), c[0], c[1], c[2], 1); + else lineAA(acc, g.GX[j], g.GY[j], g.GX[j + 1], g.GY[j + 1], c[0], c[1], c[2], 1); } // The fine curve up to the pen. In the look (phase 1) the three copies // of the first F(n-3) digits light in turn and the rest dims. @@ -313,9 +321,9 @@ export function draw(display, params) { let spot = [0, 0, 0], dim = 1; if (S.phase === 1) { const p = S.pt; - spot = [0.22, 0.46, 0.7].map((m0, q) => max(0, 1 - abs(p - m0) / 0.13)); - spot[0] = max(spot[0], smooth((p - 0.8) / 0.2)); - dim = 1 - 0.55 * smooth(p / 0.12); + spot = [0.33, 0.52, 0.71].map((m0) => max(0, 1 - abs(p - m0) / 0.11)); + spot[0] = max(spot[0], smooth((p - 0.8) / 0.15)); + dim = 1 - 0.55 * smooth((p - 0.1) / 0.12); } const last = S.phase === 0 ? floor(S.pen) : N - 1, frac = S.phase === 0 ? S.pen - last : 1; for (let i = 0; i <= min(last, N - 1); i++) { From 93f47b80e8a66f4ac48b573626f5345cf4ca2711 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:24:53 +0000 Subject: [PATCH 13/24] incoming: Huffman Tree, pattern round 68 --- .../pattern-study/patterns/huffman-tree.js | 418 ++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 incoming/pattern-study/patterns/huffman-tree.js diff --git a/incoming/pattern-study/patterns/huffman-tree.js b/incoming/pattern-study/patterns/huffman-tree.js new file mode 100644 index 000000000..a2a94ec1f --- /dev/null +++ b/incoming/pattern-study/patterns/huffman-tree.js @@ -0,0 +1,418 @@ +// ===== Patternflow pattern ===== +// Title: Huffman Tree +// Family: type +// Idea: The symbols of a short text stand along the foot of the panel on dotted stems as tall as the logarithm of their counts, the two lightest nodes merge again and again into a parent that rises above them until one root stands at the top, every merge writing a 0 into the codes of the leaves under its left branch and a 1 under its right so the codes grow up the columns over the labels, and then the text streams down the tree into a bit string that comes out shorter than the fixed-length code under it. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: minimum-redundancy codes (David A. Huffman, 1952); coding the opening words of Charles Dickens's A Tale of Two Cities (1859), the perfect pangram Mr Jock, TV quiz PhD, bags few lynx, the first 100 digits of pi, and the first two lines of Yun Dong-ju's Seosi (1941) split into jamo; the 3 by 5 capitals of the Typewriter pattern (round 18), pattern round 68 +// Round: 68 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Text=0..3, Tie=0..2 +// +// Knob 1: Hue. Turns the colours round the wheel: at 0 a 0 bit is azure, a 1 bit amber and the fixed-length strip rose; at 0.5 a 0 is red-orange, a 1 blue and the fixed strip green. Stems and labels keep a pale tint of the 0 colour. +// Knob 2: Speed. At 2 the lightest merge takes 0.22 s and the root 0.48 s, so Dickens's 12 merges stand in 4.1 s, its 50 symbols stream in 7 s and the whole run with 3 s of totals and a 1 s fade lasts 15.7 s; at 0.2 it all takes ten times as long, at 3 two thirds as long. +// Knob 3: Text. 0 Dickens, 13 symbols over 50 places, 169 bits against 200 at 4 bits a symbol; 1 the pangram, 27 symbols over 33 places, 148 against 165 at 5 bits, its 26 letters a level comb of 5 and 6 bit codes beside the space's 00; 2 the first 100 digits of pi, 332 against 400 at the 4 bits of binary-coded decimal, six digits taking 3 bits and four taking 4; 3 Seosi in jamo, 16 symbols over 58 places, 223 against 232. +// Knob 4: Tie. Which of two equal weights merges first: 0 the older (leaves before merged nodes, earlier symbols first), 1 the newer, 2 a coin drawn anew each time the tree is rebuilt. The tree changes shape and the total never does: Dickens takes 169 bits under every rule while its longest code is 5 bits at 0 and 6 at 1, and the pangram's four 6-bit letters are M R J O at 0 and E W L Y at 1. + +const W = 64, H = 128; +const { floor, round, min, max, sqrt, exp, log2, ceil } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (v) => { + const t = cl(v, 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 3 by 5 face, rows top to bottom, one bit per column: Typewriter's +// capitals, the figures, and a space drawn as an open box. +const G3 = { + A: "010101111101101", B: "110101110101110", C: "011100100100011", D: "110101101101110", + E: "111100110100111", F: "111100110100100", G: "011100101101011", H: "101101111101101", + I: "111010010010111", J: "001001001101010", K: "101101110101101", L: "100100100100111", + M: "101111111101101", N: "110101101101101", O: "010101101101010", P: "110101110100100", + Q: "010101101111011", R: "110101110101101", S: "011100010001110", T: "111010010010010", + U: "101101101101011", V: "101101101010010", W: "101101111111101", X: "101101010101101", + Y: "101101010010010", Z: "111001010100111", + 0: "111101101101111", 1: "010110010010111", 2: "111001111100111", 3: "111001011001111", + 4: "101101111001001", 5: "111100111001111", 6: "111100111101111", 7: "111001010010010", + 8: "111101111101111", 9: "111101111001111", + " ": "000000000101111", "+": "000010111010000", "=": "000111000111000", "<": "001010100010001", +}; +// Jamo drawn 5 by 5 for the Korean line, and its space. +const G5 = { + "ㄱ": "1111100001000010000100001", "ㄲ": "1101101001010010100101001", "ㄴ": "1000010000100001000011111", + "ㄹ": "1111100001111111000011111", "ㅁ": "1111110001100011000111111", "ㅂ": "1000110001111111000111111", + "ㅅ": "0010000100010100101010001", "ㅇ": "0111010001100011000101110", "ㅈ": "1111100100001000101010001", + "ㅎ": "0010011111011101000101110", "ㅏ": "0100001000011100100001000", "ㅓ": "0001000010011100001000010", + "ㅜ": "0000011111001000010000100", "ㅡ": "0000000000111110000000000", "ㅣ": "0010000100001000010000100", + " ": "0000000000000001000111111", +}; + +const TEXTS = [ + // A Tale of Two Cities (1859), its first words, letters and spaces only. + "IT WAS THE BEST OF TIMES IT WAS THE WORST OF TIMES", + // A perfect pangram: every letter once, so 26 weights of 1 and the spaces. + "MR JOCK TV QUIZ PHD BAGS FEW LYNX", + // The first 100 digits of pi, 3 and 99 decimals. + "3141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117067", + // Yun Dong-ju, Seosi (1941): 죽는 날까지 하늘을 우러러 / 한 점 부끄럼이 없기를, + // split into the keys of the two-set keyboard (없 is ㅇ ㅓ ㅂ ㅅ, ㄲ one key). + "ㅈㅜㄱㄴㅡㄴ ㄴㅏㄹㄲㅏㅈㅣ ㅎㅏㄴㅡㄹㅇㅡㄹ ㅇㅜㄹㅓㄹㅓ ㅎㅏㄴ ㅈㅓㅁ ㅂㅜㄲㅡㄹㅓㅁㅇㅣ ㅇㅓㅂㅅㄱㅣㄹㅡㄹ", +]; + +// Rows. The text line, the Huffman strip and the fixed strip at the top, the +// root under them; labels at the foot with the code band above them. +const LINE_Y = 1, HS_Y = 8, FS_Y = 16, STRIP_R = 7, ROOT_Y = 28; +// Times in seconds at Speed 2. +const T0 = 0.3, TM = 0.32, PAUSE = 0.6, STREAM = 7, HOLD = 3, FADE = 1; + +// Huffman's construction. Symbols are counted in order of first appearance +// and become leaves 0..k-1; each merge takes the two lightest nodes, the +// first taken goes left (bit 0), and the parent joins the pool. Ties go by +// the rule: 0 the older node first, 1 the newer, 2 a random key. +function huffman(text, rule) { + const idx = new Map(), sym = [], seq = []; + for (const ch of text) { + if (!idx.has(ch)) idx.set(ch, sym.length), sym.push({ ch, w: 0 }); + const i = idx.get(ch); + sym[i].w++; + seq.push(i); + } + const nodes = sym.map((s, i) => ({ w: s.w, leaf: i, a: -1, b: -1, up: -1, bit: 0, r: Math.random(), m: -1 })); + const pool = nodes.map((_, i) => i), merges = []; + const first = (i, j) => { + const p = nodes[i], q = nodes[j]; + if (p.w !== q.w) return p.w < q.w; + return rule === 0 ? i < j : rule === 1 ? i > j : p.r < q.r; + }; + const take = () => { + let bi = 0; + for (let i = 1; i < pool.length; i++) if (first(pool[i], pool[bi])) bi = i; + return pool.splice(bi, 1)[0]; + }; + while (pool.length > 1) { + const a = take(), b = take(), id = nodes.length; + nodes.push({ w: nodes[a].w + nodes[b].w, leaf: -1, a, b, up: -1, bit: 0, r: Math.random(), m: merges.length }); + nodes[a].up = id, nodes[b].up = id, nodes[b].bit = 1; + merges.push(id); + pool.push(id); + } + const root = pool[0]; + // Codes, read from the root: 0 for the left branch, 1 for the right. + const codes = sym.map((_, i) => { + let s = ""; + for (let n = i; nodes[n].up >= 0; n = nodes[n].up) s = nodes[n].bit + s; + return s; + }); + let total = 0, sigma = 0; + for (const i of seq) total += codes[i].length; + for (const id of merges) sigma += nodes[id].w; + return { sym, seq, nodes, merges, root, codes, total, sigma, maxLen: max(...codes.map((c) => c.length)) }; +} + +function prepare(S) { + const text = TEXTS[S.text], T = huffman(text, S.rule), k = T.sym.length, N = T.seq.length; + const big = S.text === 3, gw = big ? 5 : 3, m = (gw - 1) / 2; + // In-order leaves: the tree draws without crossings. + const order = []; + const walk = (n) => { + const q = T.nodes[n]; + if (q.leaf >= 0) order.push(n); + else walk(q.a), walk(q.b); + }; + walk(T.root); + const pitch = (W - 2 * m) / k, stagger = pitch < gw + 1; + const band = max(T.maxLen, huffman(text, 0).maxLen, huffman(text, 1).maxLen); + const labelY = stagger ? 116 : 122, codeBottom = labelY - 2, Y0 = codeBottom - 2 * band + 1 - 3; + order.forEach((n, pos) => { + const q = T.nodes[n]; + q.x = round(m + (pos + 0.5) * pitch - 0.5); + q.ly = stagger && pos % 2 === 0 ? 122 : labelY; + }); + // Heights: a node of weight w stands log2(w) levels above the baseline, + // so the drop from the root to a leaf is log2(N / w), the leaf's ideal + // length in bits. Parents keep 2 rows above their children and never + // below an earlier merge, so the tree grows upwards in merge order. + let L = (Y0 - ROOT_Y) / log2(N); + for (let tries = 0; tries < 40; tries++) { + for (const q of T.nodes) if (q.leaf >= 0) q.y = Y0 - round(log2(q.w) * L); + let prev = Y0; + for (const id of T.merges) { + const q = T.nodes[id], A = T.nodes[q.a], B = T.nodes[q.b]; + q.x = (A.x + B.x) >> 1; + q.y = min(Y0 - round(log2(q.w) * L), A.y - 2, B.y - 2, prev); + prev = q.y; + } + if (T.nodes[T.root].y >= ROOT_Y) break; + L *= 0.97; + } + // Each leaf's ancestors from the leaf up, and where its bits go in the + // Huffman strip for every place in the text. + T.path = order.map(() => null); + for (let i = 0; i < k; i++) { + const up = []; + for (let n = i; T.nodes[n].up >= 0; n = T.nodes[n].up) up.push(n); + T.path[i] = up; + } + T.pos = new Int32Array(N + 1); + for (let j = 0; j < N; j++) T.pos[j + 1] = T.pos[j] + T.codes[T.seq[j]].length; + T.fb = ceil(log2(k)); + T.fixed = T.fb * N; + T.cw = T.fixed <= STRIP_R * 32 ? 2 : 1; + T.chars = [...text]; + T.N = N, T.k = k, T.big = big, T.gw = gw, T.Y0 = Y0, T.codeBottom = codeBottom, T.L = L; + // A merge takes longer the heavier it is: the twigs go quickly, the + // trunk slowly, 0.6 to 1.5 times TM. + T.mS = [], T.mD = [], T.mT = []; + let at = T0; + for (const id of T.merges) { + const dur = TM * (0.5 + sqrt(T.nodes[id].w / N)); + T.mS.push(at), T.mD.push(dur), T.mT.push(at + 0.8 * dur); + at += dur; + } + T.buildEnd = at; + T.ts = min(0.25, STREAM / N); + T.streamAt = T.buildEnd + PAUSE; + T.holdAt = T.streamAt + N * T.ts; + T.end = T.holdAt + HOLD + FADE; + S.T = T; + S.c = 0; +} + +export function setup(params) { + params.s = { t: 0, c: 0, text: 0, rule: 0, hue: 0, speed: 2, T: null, buf: new Float32Array(W * H * 3) }; + prepare(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.t += d; + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + const text = cl(round(k[2]), 0, 3), rule = cl(round(k[3]), 0, 2); + if (text !== S.text || rule !== S.rule) { + S.text = text, S.rule = rule; + prepare(S); + return; + } + S.c += (d * S.speed) / 2; + if (S.c >= S.T.end) prepare(S); +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.T) return; + const T = S.T, c = S.c, buf = S.buf, nodes = T.nodes; + buf.fill(0); + const h0 = 0.55 + S.hue; + const BIT = [hsv(h0, 0.8, 1), hsv(h0 + 0.58, 0.85, 1)]; + const PALE = hsv(h0, 0.55, 1), FIX = hsv(h0 + 0.3, 0.55, 1); + const fade = c > T.end - FADE ? 1 - sm((c - (T.end - FADE)) / FADE) : 1; + const put = (x, y, col, v) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const i = (y * W + x) * 3; + buf[i] = max(buf[i], col[0] * v), buf[i + 1] = max(buf[i + 1], col[1] * v), buf[i + 2] = max(buf[i + 2], col[2] * v); + }; + const vline = (x, y0, y1, col, v) => { + for (let y = min(y0, y1); y <= max(y0, y1); y++) put(x, y, col, v); + }; + const hline = (x0, x1, y, col, v) => { + for (let x = min(x0, x1); x <= max(x0, x1); x++) put(x, y, col, v); + }; + const glyph = (ch, x0, y0, col, v) => { + const g5 = T.big && G5[ch], g = g5 || G3[ch]; + if (!g) return; + const gw = g5 ? 5 : 3; + if (x0 + gw > W) return; + for (let r = 0; r < 5; r++) for (let q = 0; q < gw; q++) if (g[r * gw + q] === "1") put(x0 + q, y0 + r, col, v); + }; + const WHITE = [1, 1, 1]; + const N = T.N, M = T.merges.length, wN = nodes[T.root].w; + const br = (w) => 0.42 + 0.58 * sqrt(w / wN); + + // Merge clock: how many merges are done, which one runs and how far. + let done = 0; + while (done < M && c >= T.mT[done]) done++; + let running = -1, u = 0; + if (c >= T0 && c < T.buildEnd) { + running = done < M && c >= T.mS[done] ? done : done - 1; + u = (c - T.mS[running]) / T.mD[running]; + } + let front = T.Y0; + if (c >= T0) { + if (running >= 0) { + const prev = running > 0 ? nodes[T.merges[running - 1]].y : T.Y0; + front = round(prev + (nodes[T.merges[running]].y - prev) * sm(u / 0.45)); + } else front = nodes[T.root].y; + } + const isDone = (id) => nodes[id].leaf >= 0 || nodes[id].m < done; + + // Leaves' dotted stems: the count on a log scale. + for (let i = 0; i < T.k; i++) { + const q = nodes[i]; + for (let y = T.Y0; y > q.y; y -= 2) put(q.x, y, PALE, 0.4); + } + // Faint level lines a bit apart: a node j lines under the root would + // ideally have a code j bits long. + const top = nodes[T.root].y; + for (let j = 0; T.Y0 - j * T.L > top + 1; j++) { + const y = round(T.Y0 - j * T.L); + for (let x = (j & 1) * 2; x < W; x += 4) put(x, y, PALE, 0.1); + } + // Under each bar a faint light down to the higher child: the 0 side left + // of the parent, the 1 side right of it. + for (let j = 0; j < done; j++) { + const q = nodes[T.merges[j]], A = nodes[q.a], B = nodes[q.b], low = min(A.y, B.y), hh = low - q.y; + const e = 1 - exp(-(c - T.mT[j]) / 0.3); + for (let y = q.y + 1; y < low; y++) { + const v = e * 0.21 * (1 - (0.75 * (y - q.y)) / hh); + for (let x = A.x + 1; x < B.x; x++) if (x !== q.x) put(x, y, BIT[x < q.x ? 0 : 1], v); + } + } + // Edges. A merged node's stem and half bar take its bit's colour, brighter + // for heavier nodes; an unmerged node's stem grows white up to the front. + for (let n = 0; n < nodes.length; n++) { + const q = nodes[n]; + if (!isDone(n)) continue; + const p = q.up >= 0 ? nodes[q.up] : null; + if (p && p.m < done) { + vline(q.x, q.y, p.y, BIT[q.bit], br(q.w)); + hline(q.x, p.x, p.y, BIT[q.bit], br(q.w)); + // Heavy branches run two LEDs wide, inwards and upwards. + if (q.leaf < 0 && q.w * 5 >= wN) { + vline(q.x + (q.bit ? -1 : 1), q.y - 1, p.y, BIT[q.bit], 0.8 * br(q.w)); + hline(q.x, p.x, p.y - 1, BIT[q.bit], 0.8 * br(q.w)); + } + } else if (p && p.m === running) { + const top = min(q.y, front); + vline(q.x, q.y, top, WHITE, 0.95); + if (u > 0.45) { + const f = sm((u - 0.45) / 0.35), xe = round(q.x + (p.x - q.x) * f); + if (f > 0) hline(q.x, xe, p.y, BIT[q.bit], 1); + } + } else if (n !== T.root) { + if (front < q.y) vline(q.x, q.y, front, PALE, 0.55); + } + // The node itself. + if (n === T.root && done === M) vline(q.x, q.y, q.y - 2, WHITE, 1); + put(q.x, q.y, WHITE, q.leaf >= 0 ? 0.9 : 0.7); + } + // A merge just done flashes its bar. + if (done > 0) { + const id = T.merges[done - 1], q = nodes[id], e = exp(-(c - T.mT[done - 1]) / 0.25); + if (e > 0.05) hline(nodes[q.a].x, nodes[q.b].x, q.y, WHITE, e); + } + + // Codes over the labels, growing upwards a bit per merge above the leaf. + for (let i = 0; i < T.k; i++) { + const q = nodes[i], up = T.path[i], len = up.length; + let vis = 0; + while (vis < len && nodes[nodes[up[vis]].up].m < done) vis++; + for (let j = 0; j < vis; j++) { + // j counts from the leaf: the bit of up[j]; it sits j rows above the bottom. + const n = up[j], y = T.codeBottom - 2 * j; + const fresh = exp(-(c - T.mT[nodes[nodes[n].up].m]) / 0.3); + put(q.x, y, BIT[nodes[n].bit], 0.85); + if (fresh > 0.05) put(q.x, y, WHITE, fresh); + } + // The leaf's column down to its label. + vline(q.x, T.codeBottom + 1, q.ly - 1, PALE, 0.3); + } + + // Streaming: the current symbol's path lights from the root down, and its + // bits go into the Huffman strip while its fixed code goes in below. + let cur = -1, cu = 0, wrote = 0, wroteF = 0; + if (c >= T.streamAt) { + const s = (c - T.streamAt) / T.ts; + cur = min(N - 1, floor(s)); + cu = s >= N ? 1 : s - cur; + const leaf = T.seq[cur], len = T.codes[leaf].length; + const shown = min(len, 1 + floor(cu * 1.6 * len)); + wrote = T.pos[cur] + shown; + wroteF = cur * T.fb + min(T.fb, 1 + floor(cu * 1.6 * T.fb)); + // Path: up[len-1] is the root's child; edges from the root down. + const up = T.path[leaf], e = s >= N ? 0 : 1 - 0.5 * cu; + for (let j = len - 1; j >= len - shown; j--) { + const q = nodes[up[j]], p = nodes[q.up]; + vline(q.x, q.y, p.y, WHITE, e); + hline(q.x, p.x, p.y, WHITE, e); + } + const lf = nodes[leaf]; + if (shown === len) for (let j = 0; j < len; j++) put(lf.x, T.codeBottom - 2 * j, WHITE, e); + } + + // Labels. + for (let i = 0; i < T.k; i++) { + const q = nodes[i], lit = cur >= 0 && T.seq[cur] === i && c < T.holdAt; + glyph(T.sym[i].ch, q.x - (T.gw >> 1), q.ly, lit ? WHITE : PALE, lit ? 1 : 0.85); + } + + // Strips: the Huffman strip fills with placeholders a merge at a time, + // w cells for a merge of weight w, and they come to the code's length; + // the fixed strip waits at N times the fixed length from the start. + const cw = T.cw, per = W / cw; + const cell = (y0, i, col, v) => { + const x = (i % per) * cw, y = y0 + floor(i / per); + for (let q = 0; q < cw; q++) put(x + q, y, col, v); + }; + let held = 0; + for (let j = 0; j < done; j++) held += nodes[T.merges[j]].w; + const GREY = FIX; + let bit = 0; + for (let i = 0; i < T.total; i++) { + if (i < wrote) { + // Which bit of which symbol: walk the text alongside. + while (T.pos[bit + 1] <= i) bit++; + const b = T.codes[T.seq[bit]][i - T.pos[bit]]; + cell(HS_Y, i, BIT[b === "1" ? 1 : 0], i >= T.pos[cur] && c < T.holdAt ? 1 : 0.8); + } else if (i < held) cell(HS_Y, i, PALE, 0.25); + } + for (let i = 0; i < T.fixed; i++) { + if (i < wroteF) { + const j = floor(i / T.fb), b = (T.seq[j] >> (T.fb - 1 - (i % T.fb))) & 1; + cell(FS_Y, i, GREY, b ? 0.7 : 0.32); + } else cell(FS_Y, i, GREY, 0.22); + } + + // The line: the text before the first merge, each merge's sum while the + // tree grows, the text running through while it streams, then the totals. + const P = T.big ? 6 : 4; + if (done === 0 && running < 1) { + for (let j = 0; j * P < W && j < N; j++) glyph(T.chars[j], j * P + 1, LINE_Y, PALE, 0.6); + } else if (c < T.streamAt) { + const id = T.merges[running >= 0 ? running : M - 1], q = nodes[id]; + const parts = [[String(nodes[q.a].w), BIT[0]], ["+", PALE], [String(nodes[q.b].w), BIT[1]]]; + if (running < 0 || u >= 0.8) parts.push(["=", PALE], [String(q.w), WHITE]); + let len = 0; + for (const [s] of parts) len += s.length; + let x = (W - len * 4 + 1) >> 1; + for (const [s, col] of parts) for (const ch of s) glyph(ch, x, LINE_Y, col, 1), (x += 4); + } else if (c < T.holdAt) { + const chars = T.chars; + for (let j = 0; j < 16; j++) { + const at = cur - 3 + j; + if (at < 0 || at >= N || j * P >= W) continue; + glyph(chars[at], j * P + 1, LINE_Y, at === cur ? WHITE : PALE, at === cur ? 1 : at < cur ? 0.35 : 0.65); + } + } else { + const a = String(T.total), b = String(T.fixed), len = a.length + 1 + b.length; + let x = (W - len * 4 + 1) >> 1; + for (const ch of a) glyph(ch, x, LINE_Y, BIT[1], 1), (x += 4); + glyph("<", x, LINE_Y, WHITE, 1), (x += 4); + for (const ch of b) glyph(ch, x, LINE_Y, GREY, 0.8), (x += 4); + } + + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = (y * W + x) * 3; + display.setPixel(x, y, (255 * min(1, buf[i] * fade)) | 0, (255 * min(1, buf[i + 1] * fade)) | 0, (255 * min(1, buf[i + 2] * fade)) | 0); + } +} From 160c225fce8cc74dbdcd4be2966dc90a755af39c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:25:51 +0000 Subject: [PATCH 14/24] incoming: Huffman Tree level lines from the root, pattern round 68 --- incoming/pattern-study/patterns/huffman-tree.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/incoming/pattern-study/patterns/huffman-tree.js b/incoming/pattern-study/patterns/huffman-tree.js index a2a94ec1f..3614b4a7c 100644 --- a/incoming/pattern-study/patterns/huffman-tree.js +++ b/incoming/pattern-study/patterns/huffman-tree.js @@ -260,12 +260,13 @@ export function draw(display, params) { const q = nodes[i]; for (let y = T.Y0; y > q.y; y -= 2) put(q.x, y, PALE, 0.4); } - // Faint level lines a bit apart: a node j lines under the root would - // ideally have a code j bits long. - const top = nodes[T.root].y; - for (let j = 0; T.Y0 - j * T.L > top + 1; j++) { - const y = round(T.Y0 - j * T.L); - for (let x = (j & 1) * 2; x < W; x += 4) put(x, y, PALE, 0.1); + // Faint lines a bit apart, counted down from where the root would stand: + // a leaf whose node sits between lines j and j + 1 has an ideal length + // log2(N / w) between j and j + 1 bits. + for (let j = 1; ; j++) { + const y = round(T.Y0 - (log2(N) - j) * T.L); + if (y > T.Y0) break; + for (let x = (j & 1) * 2; x < W; x += 4) put(x, y, PALE, 0.14); } // Under each bar a faint light down to the higher child: the 0 side left // of the parent, the 1 side right of it. From 213393f5ae1f6773b12b9dc72d614da7e09e5fe3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:28:56 +0000 Subject: [PATCH 15/24] incoming: Ogham, pattern round 68 --- incoming/pattern-study/patterns/ogham.js | 50 ++++++++++++++---------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/incoming/pattern-study/patterns/ogham.js b/incoming/pattern-study/patterns/ogham.js index 7876969bc..df005bf3c 100644 --- a/incoming/pattern-study/patterns/ogham.js +++ b/incoming/pattern-study/patterns/ogham.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Ogham // Family: type -// Idea: A standing stone is seen corner-on with its edge running up the whole panel as the stemline, and an Ogham inscription is cut into it stroke by stroke from the bottom up, one to five strokes a letter, the B aicme on the right face, the H aicme on the left, the M aicme slanting across the edge from one face onto the other and the vowels notched on the edge itself, while a small Latin letter beside each group counts its strokes as they land (B, L, F, S, then N for five on the right) and the view climbs the stone with the chisel. +// Idea: A standing stone is seen corner-on with its edge running up the whole panel as the stemline, and an Ogham inscription is cut into it stroke by stroke from the bottom up, one to five strokes a letter, the B aicme on the right face, the H aicme on the left, the M aicme slanting across the edge from one face onto the other and the vowels notched on the edge itself, while a small Latin letter beside each group counts its strokes as they land (B, L, F, S, then N for five on the right) and the view climbs the stone with the chisel near the top, each cut a groove of a lit lower wall and a shaded upper one. // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -12,8 +12,8 @@ // @knobs Hue=0..1, Speed=0.2..3, Text=0..3, Turn=-1..1 // // Knob 1: Hue. Turns the light round the wheel: at 0 the cuts are pale gold on blue-grey slate, at 0.5 pale blue on a brown sandstone, the gloss letters a whiter tint of the cuts. -// Knob 2: Speed. Strokes cut a second: 5 at the opening 2, so the alphabet's 60 strokes and 20 letters take 16 seconds before the stone holds 3 seconds with its top in view and a fresh one is started; 0.8 at 0.2, 13.6 at 3. -// Knob 3: Text. Which inscription, each on its own stone: 0 the twenty letters in their four aicmí, 60 strokes over 302 rows of stone; 1 the four aicme names BEITHE HUATHA MUINE AILME, 59 strokes of which 40 are vowel notches; 2 the formula words ANM MAQI MUCOI, 34 strokes; 3 the Old Irish names of the first aicme, BEITHE LUIS FERN SAIL NIN, 76 strokes over 365 rows. +// Knob 2: Speed. Strokes cut a second: 5 at the opening 2, 0.8 at 0.2, 13.6 at 3. Every fresh stone has its first 100 rows (the whole B aicme of the alphabet) cut at 25 a second, in 0.9 s, so the writing stands the panel's height at once; at 2 the alphabet's 60 strokes are done at 12.9 s (74 s at 0.2, 5.3 s at 3), then the stone holds 3 s with its top in view and a fresh one is started. +// Knob 3: Text. Which inscription, each on its own stone: 0 the twenty letters in their four aicmí, 60 strokes over 380 rows of stone; 1 the four aicme names BEITHE HUATHA MUINE AILME, 59 strokes of which 40 are vowel notches; 2 the formula words ANM MAQI MUCOI, 34 strokes; 3 the Old Irish names of the first aicme, BEITHE LUIS FERN SAIL NIN, 76 strokes over 460 rows. // Knob 4: Turn. Turns the stone about its upright axis, 13 degrees at the opening 0.06 so the left face shows wider, 40 degrees each way at the ends: at -1 the right face comes round to you and the left one shrinks to a sliver, at 1 the reverse, and the strokes on the turning face shorten while the slanting M strokes kink at the edge. const W = 64, H = 128; @@ -72,10 +72,14 @@ const FONT = { // view climbing the stone with the chisel a whole row at a time so every // cut lands on one row of LEDs. const CX = 35.5, RS = [24, 22.5, 25.5, 23.5], CAM0 = -10, VOFF = 40; -const PITCH = 3, LGAP = 8, WGAP = 18, BASE = 9; +const PITCH = 4, LGAP = 10, WGAP = 20, BASE = 9; // How far each aicme's strokes reach along a face, in face units (one // unit is one LED when the face is square to you), and the M slant. -const REACH = [12.5, 12.5, 8.5, 2.3], SLANT = 0.42, CUT = 0.62; +const REACH = [16, 16, 9, 2.4], SLANT = 0.42, CUT = 0.62; +// A fresh stone's first RUN rows are cut at RUSH strokes a second, so the +// writing stands the panel's height within a second; then the chisel goes +// at the Speed knob's pace with the view holding it LEAD rows below the top. +const RUN = 100, RUSH = 25, LEAD = 116; const hash = (a, b, c) => { let h = (a * 374761393 + b * 668265263 + c * 1013904223) | 0; @@ -143,7 +147,7 @@ export function setup(params) { const S = (params.s = { ti: -1, hue: 0, rate: 5, turn: 0, dim: 1, emit: 0 }); S.Cov = new Float32Array(W * H); S.Fre = new Float32Array(W * H); - S.Sh = new Float32Array(W * H); + S.Wl = new Float32Array(W * H); S.F = new Float32Array(W * H * 3); S.colF = new Int8Array(W); S.colU = new Float32Array(W); @@ -175,10 +179,12 @@ export function update(dt, input, params) { // start again on a fresh one. let target; if (S.phase === 0) { - S.t += dt * S.rate; + const i = current(S), c = S.cuts[i], n = S.cuts[min(i + 1, S.cuts.length - 1)]; + S.t += dt * (c.v < RUN ? max(S.rate, RUSH) : S.rate); S.dim = min(1, S.dim + dt / 0.5); - const c = S.cuts[current(S)]; - target = c.v + (c.g === 2 ? REACH[2] * SLANT : 0) - 92; + // The view aims between this cut and the next, so it climbs evenly. + const a = n === c ? 0 : cl((S.t - c.t0) / (n.t0 - c.t0), 0, 1); + target = c.v + (n.v - c.v) * a + REACH[2] * SLANT - LEAD; if (S.t >= S.end) (S.phase = 1), (S.hold = 0); } else { target = S.top - 112; @@ -189,7 +195,7 @@ export function update(dt, input, params) { newStone(S, S.ti); } } - S.cam += (max(CAM0, target) - S.cam) * (1 - exp(-dt * 2.5)); + S.cam += (max(CAM0, target) - S.cam) * (1 - exp(-dt * 6)); // Stone dust off the chisel, in panel columns and stone rows. const P = S.parts; for (const p of P) { @@ -209,8 +215,10 @@ export function update(dt, input, params) { } } -// One straight piece of a cut as a capsule of radius rad, into the cover -// buffer, its freshness alongside and its shadow a row above it. +// One straight piece of a cut as a capsule of radius rad. A cut is a groove +// with two walls: under light from above, the lower wall faces up and is +// lit (the cover buffer, its freshness alongside) and the upper wall a row +// above it faces down into shade (the wall buffer). function seg(S, a, b, rad, fresh) { const [ax, ay] = a, [bx, by] = b; const x0 = max(0, floor(min(ax, bx) - rad - 1)), x1 = min(W - 1, ceil(max(ax, bx) + rad + 1)); @@ -226,17 +234,17 @@ function seg(S, a, b, rad, fresh) { const o = y * W + x; if (c > S.Cov[o]) S.Cov[o] = c; if (c * fresh > S.Fre[o]) S.Fre[o] = c * fresh; - if (c > S.Sh[o - W]) S.Sh[o - W] = c; + if (c > S.Wl[o - W]) S.Wl[o - W] = c; } } export function draw(display, params) { const S = params.s; if (!S || !S.cuts) return; - const { Cov, Fre, Sh, F, colF, colU } = S; + const { Cov, Fre, Wl, F, colF, colU } = S; Cov.fill(0); Fre.fill(0); - Sh.fill(0); + Wl.fill(0); F.fill(0); // The edge and the far corners across the panel, and how much of a face // unit each face shows: cos(45 degrees + turn) on the right, cos(45 - turn) @@ -311,12 +319,12 @@ export function draw(display, params) { B = sky[2] * s; } else { const vi = cl(floor(v) + VOFF, 0, S.V - 1); - let l = shade[f] * near * S.tex[f][vi * S.U + ui] + edge; - l *= 1 - 0.55 * Sh[o] * (1 - Cov[o]); - const c = Cov[o], fr = Fre[o], cc = (0.7 + 0.3 * shade[f]) * (1 - fr); - R = stone[0] * l * (1 - c) + c * (cutC[0] * cc + 255 * fr); - G = stone[1] * l * (1 - c) + c * (cutC[1] * cc + 255 * fr); - B = stone[2] * l * (1 - c) + c * (cutC[2] * cc + 250 * fr); + const l = shade[f] * near * S.tex[f][vi * S.U + ui] + edge; + const c = Cov[o], w = Wl[o] * (1 - c), fr = Fre[o], fw = y < H - 1 ? 0.5 * Fre[o + W] : 0; + const cc = (0.7 + 0.3 * shade[f]) * (1 - fr), cw = 0.4 * (0.7 + 0.3 * shade[f]) * (1 - fw); + R = stone[0] * l * (1 - c - w) + c * (cutC[0] * cc + 255 * fr) + w * (cutC[0] * cw + 200 * fw); + G = stone[1] * l * (1 - c - w) + c * (cutC[1] * cc + 255 * fr) + w * (cutC[1] * cw + 200 * fw); + B = stone[2] * l * (1 - c - w) + c * (cutC[2] * cc + 250 * fr) + w * (cutC[2] * cw + 200 * fw); } F[o * 3] = R; F[o * 3 + 1] = G; From dfd2e456bd82823a3fdb4dbc99c71d3a2b3c74f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:31:12 +0000 Subject: [PATCH 16/24] incoming: Palimpsest, pattern round 68 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JktahMn4mzzFyveZ8fo --- incoming/pattern-study/patterns/palimpsest.js | 326 ++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 incoming/pattern-study/patterns/palimpsest.js diff --git a/incoming/pattern-study/patterns/palimpsest.js b/incoming/pattern-study/patterns/palimpsest.js new file mode 100644 index 000000000..66a8610e8 --- /dev/null +++ b/incoming/pattern-study/patterns/palimpsest.js @@ -0,0 +1,326 @@ +// ===== Patternflow pattern ===== +// Title: Palimpsest +// Family: type +// Idea: A parchment leaf carries two Greek texts at right angles, a prayer written across the page in dark iron-gall ink over a scraped-off treatise of Archimedes that runs down it, and the imaging band sweeps from plain light, where the old text is a faint ghost, to ultraviolet, where the parchment glows and the old ink shows dark, to the pseudocolour view that turns the old text red and leaves the prayer dark, while an X-ray beam rasters down the leaf and lights the iron of both inks behind it. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the Archimedes Palimpsest (Archimedes' treatises copied in the 10th century, scraped and written over as a prayer book in 1229; multispectral imaging by Roger Easton, Keith Knox and William Christens-Barry and X-ray fluorescence imaging by Uwe Bergmann in the 2000s), Greek minuscule drawn for this pattern, pattern round 68 +// Round: 68 +// =============================== +// @matrix 64x128 +// @knobs Band=0..1, Speed=0.2..3, Page=0..3, Ink=-1..1 +// +// Knob 1: Band. The light the leaf is seen in. 0 to 0.15 is plain light: cream parchment, brown-black prayer, the old text a ghost 8% darker than the page. 0.4 to 0.6 is ultraviolet: the parchment glows blue and both inks put the glow out, the prayer nearly black and the old text dark blue. 0.85 to 1 is pseudocolour: a red-light picture in the red channel and the ultraviolet one in green and blue, so the old text comes out red and the prayer dark grey. Between, one blends into the next. +// Knob 2: Speed. How fast the X-ray beam steps down the leaf: 26 rows a second at 2, a pass of the 128 rows every 6.5 s; 2.6 rows a second at 0.2. Behind the beam, for about 16 rows, the leaf shows its iron map: both inks pale blue on black, the scraped text as plain as the prayer. +// Knob 3: Page. Which pair of texts, rounded to 0 to 3. 0: the Trisagion over the title of the Method. 1: the Lord's Prayer over the first proposition of the Measurement of a Circle. 2: Kyrie eleison over On Floating Bodies. 3: Glory to thee over the Stomachion. +// Knob 4: Ink. How much of the old ink the scraper left and how far both inks have bled into the parchment. At -1 the strokes keep to 1 LED and the old text is all but gone even in ultraviolet; at 1 they bleed about 2 LEDs wide and the old text shows brown in plain light. + +const W = 64, H = 128, N = W * H; +const { floor, round, min, max, abs, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const smooth = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +// Greek minuscule, at most 5 LEDs wide on 9 rows: 0 and 1 hold ascenders, +// 2 to 6 the body, 7 and 8 descenders. Each glyph is its first inked row and +// then its rows downward. No accents or breathings. +const FONT = { + α: [2, ".##.#", "#..#.", "#..#.", "#..#.", ".##.#"], + β: [0, ".##.", "#..#", "#..#", "###.", "#..#", "#..#", "###.", "#...", "#..."], + γ: [2, "#...#", ".#.#.", ".#.#.", "..#..", "..#..", ".#.#.", "..#.."], + δ: [0, ".###", "#...", ".##.", "#..#", "#..#", "#..#", ".##."], + ε: [2, ".###", "#...", "###.", "#...", ".###"], + ζ: [0, "####", "..#.", ".#..", "#...", "#...", "#...", ".##.", "...#", "..#."], + η: [2, "#.#.", "##.#", "#..#", "#..#", "#..#", "...#", "...#"], + θ: [0, ".##.", "#..#", "#..#", "####", "#..#", "#..#", ".##."], + ι: [2, "#.", "#.", "#.", "#.", ".#"], + κ: [2, "#..#", "#.#.", "##..", "#.#.", "#..#"], + λ: [0, "#....", "#....", ".#...", ".#...", "..#..", ".#.#.", "#...#"], + μ: [2, "#..#.", "#..#.", "#..#.", "#..#.", "###.#", "#....", "#...."], + ν: [2, "#...#", "#...#", ".#..#", ".#.#.", "..#.."], + ξ: [0, "####", ".#..", "..#.", ".##.", "#...", "#...", ".##.", "...#", "..#."], + ο: [2, ".##.", "#..#", "#..#", "#..#", ".##."], + π: [2, "#####", ".#.#.", ".#.#.", ".#.#.", ".#.#."], + ρ: [2, ".##.", "#..#", "#..#", "#..#", "###.", "#...", "#..."], + σ: [2, ".####", "#..#.", "#..#.", "#..#.", ".##.."], + ς: [2, ".###", "#...", "#...", ".##.", "...#", "..#."], + τ: [2, "####", ".#..", ".#..", ".#..", "..##"], + υ: [2, "#..#", "#..#", "#..#", "#..#", ".##."], + φ: [0, "..#..", "..#..", ".###.", "#.#.#", "#.#.#", "#.#.#", ".###.", "..#..", "..#.."], + χ: [2, "#...#", ".#.#.", "..#..", ".#.#.", "#...#"], + ψ: [0, "..#..", "..#..", "#.#.#", "#.#.#", "#.#.#", ".###.", "..#..", "..#..", "..#.."], + ω: [2, "#...#", "#...#", "#.#.#", "#.#.#", ".#.#."], +}; +const GLYPH = {}; +for (const ch in FONT) { + const [top, ...rows] = FONT[ch]; + const w = max(...rows.map((r) => r.length)), px = []; + rows.forEach((r, j) => { + for (let i = 0; i < r.length; i++) if (r[i] === "#") px.push(i, top + j); + }); + GLYPH[ch] = { w, px }; +} + +// The pairs of texts: a prayer written across the leaf over the title or +// opening words of one of Archimedes' treatises running down it. The words +// repeat until the leaf is full. A sigma that ends a word is written final. +const PAGES = [ + ["αγιοσ ο θεοσ αγιοσ ισχυροσ αγιοσ αθανατοσ ελεησον ημασ", "περι των μηχανικων θεωρηματων προσ ερατοσθενη εφοδοσ"], + ["πατερ ημων ο εν τοισ ουρανοισ αγιασθητω το ονομα σου ελθετω η βασιλεια σου", "κυκλου μετρησισ πασ κυκλοσ ισοσ εστι τριγωνω ορθογωνιω"], + ["κυριε ελεησον", "περι οχουμενων"], + ["δοξα σοι ο θεοσ ημων δοξα σοι", "στομαχιον"], +].map((pair) => pair.map((t) => t.split(" ").map((w) => w.replace(/σ$/, "ς")))); + +// The prayer: 12 lines 10 LEDs apart, 60 LEDs long from x 2. The treatise: +// 5 lines 12 LEDs apart, the first at the right, each 124 LEDs long from y 2, +// its letters turned a quarter turn clockwise so it reads downward. +const LINE = 10, OVER_LINES = 12, UNDER_LINE = 12, UNDER_LINES = 5, SPACE = 3; +// Plain light: the parchment's colour, how much of each channel the prayer's +// ink takes at full strength, and the grain, a share of the page's level. +const VIS = [226, 208, 168], INK = [0.6, 0.72, 0.84], GRAIN = 0.09; + +function lay(words, len, lines, put) { + // The words run on without a break, as a scribe filled a line: a word + // that reaches the end of a line goes on at the start of the next. + const text = words.join(" ") + " "; + let i = 0, line = 0, pos = 0; + while (line < lines) { + const ch = text[i++ % text.length]; + if (ch === " ") { + if (pos > 0) pos += SPACE; + continue; + } + const g = GLYPH[ch]; + if (pos + g.w > len) (line++, (pos = 0)); + if (line >= lines) break; + // Pen pressure: each letter its own weight. + put(g, line, pos, 0.8 + 0.2 * Math.random()); + pos += g.w + 1; + } +} + +// Separable Gaussian blur of src into dst, sigma in LEDs. +function blur(src, dst, tmp, sigma) { + const r = Math.ceil(sigma * 2.5), k = []; + let sum = 0; + for (let i = -r; i <= r; i++) (k.push(exp((-i * i) / (2 * sigma * sigma))), (sum += k[k.length - 1])); + for (let i = 0; i < k.length; i++) k[i] /= sum; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let v = 0; + for (let i = -r; i <= r; i++) { + const xx = x + i; + if (xx >= 0 && xx < W) v += src[y * W + xx] * k[i + r]; + } + tmp[y * W + x] = v; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let v = 0; + for (let i = -r; i <= r; i++) { + const yy = y + i; + if (yy >= 0 && yy < H) v += tmp[yy * W + x] * k[i + r]; + } + dst[y * W + x] = v; + } +} + +// Smooth value noise from a coarse random lattice, cell size c. +function lattice(c) { + const gw = floor(W / c) + 2, gh = floor(H / c) + 2, g = new Float32Array(gw * gh), out = new Float32Array(N); + for (let i = 0; i < g.length; i++) g[i] = Math.random() * 2 - 1; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const fx = x / c, fy = y / c, ix = floor(fx), iy = floor(fy), u = smooth(fx - ix), v = smooth(fy - iy); + const a = g[iy * gw + ix], b = g[iy * gw + ix + 1], d = g[(iy + 1) * gw + ix], e = g[(iy + 1) * gw + ix + 1]; + out[y * W + x] = (a + (b - a) * u) * (1 - v) + (d + (e - d) * u) * v; + } + return out; +} + +// Grain whose neighbours keep apart: white noise with its local mean taken +// out twice, each time spread back evenly over -1..1 by rank, then cut into +// three equal parts, -1, 0 and 1. It has no clumps, and two neighbours +// differ by a whole step four times in five. +function grain() { + let n = new Float32Array(N); + for (let p = 0; p < N; p++) n[p] = Math.random(); + const idx = new Int32Array(N); + for (let pass = 0; pass < 2; pass++) { + const h = new Float32Array(N); + for (let p = 0; p < N; p++) { + const x = p % W, y = floor(p / W); + h[p] = n[p] - 0.25 * (n[y * W + ((x + 1) % W)] + n[y * W + ((x + W - 1) % W)] + n[((y + 1) % H) * W + x] + n[((y + H - 1) % H) * W + x]); + } + for (let p = 0; p < N; p++) idx[p] = p; + idx.sort((a, b) => h[a] - h[b]); + for (let r = 0; r < N; r++) n[idx[r]] = ((r + 0.5) / N) * 2 - 1; + } + for (let p = 0; p < N; p++) n[p] = n[p] > 1 / 3 ? 1 : n[p] < -1 / 3 ? -1 : 0; + return n; +} + +export function setup(params) { + // The parchment: a slow mottle in 2% steps, fibres that drink the ink + // unevenly, and a leaf edge that has gone darker. The grain goes on in + // build(), where the ink is known. + const base = new Float32Array(N), vig = new Float32Array(N), fibre = new Float32Array(N); + const m1 = lattice(16), m2 = lattice(5), f = lattice(2); + for (let p = 0; p < N; p++) { + const x = p % W, y = floor(p / W); + const edge = min(x + 0.5, W - 0.5 - x, y + 0.5, H - 0.5 - y); + base[p] = round((0.9 + 0.06 * m1[p] + 0.035 * m2[p]) * 50) / 50; + vig[p] = 0.72 + 0.28 * smooth(edge / 4); + fibre[p] = 0.85 + 0.15 * f[p]; + } + params.s = { + P: new Float32Array(N), base, vig, gr: grain(), fibre, + oMask: new Float32Array(N), uMask: new Float32Array(N), tmp: new Float32Array(N), + O: new Float32Array(N), U: new Float32Array(N), Fe: new Float32Array(N), + last: new Float32Array(N * 3), fade: 1, + page: -1, ink: 9, band: 0, speed: 2, T: 0, beam: -12, + }; +} + +function build(S, page, ink) { + const { oMask, uMask } = S; + if (page !== S.page) { + oMask.fill(0); + uMask.fill(0); + const [over, under] = PAGES[page]; + lay(over, 60, OVER_LINES, (g, line, pos, wt) => { + for (let i = 0; i < g.px.length; i += 2) { + const x = 2 + pos + g.px[i], y = 4 + line * LINE + g.px[i + 1]; + if (x < W && y < H) oMask[y * W + x] = wt; + } + }); + lay(under, 124, UNDER_LINES, (g, line, pos, wt) => { + const left = 51 - line * UNDER_LINE; + for (let i = 0; i < g.px.length; i += 2) { + const x = left + 8 - g.px[i + 1], y = 2 + pos + g.px[i]; + if (x >= 0 && y < H) uMask[y * W + x] = wt; + } + }); + } + const s = (ink + 1) / 2; + blur(oMask, S.O, S.tmp, 0.5 + 0.3 * s); + blur(uMask, S.U, S.tmp, 0.35 + 0.6 * s); + // The X-ray map sees the iron, not the colour: the old text's iron is + // still there where the scraper took the colour, and the beam is broader + // than a pen stroke. + const { O, U, Fe, tmp, fibre } = S; + const iron = 0.55 + 0.35 * s; + for (let p = 0; p < N; p++) Fe[p] = oMask[p] + iron * uMask[p]; + blur(Fe, Fe, tmp, 0.75); + for (let p = 0; p < N; p++) { + O[p] = 1 - exp(-3.4 * O[p] * fibre[p]); + U[p] = 1 - exp(-2.8 * U[p] * fibre[p]); + Fe[p] = 1 - exp(-2.6 * Fe[p]); + } + // The grain, except on the flanks of a stroke, where the ink has + // soaked in and the step from page to ink is the texture. + const { P, base, vig, gr } = S; + for (let p = 0; p < N; p++) { + const x = p % W, y = floor(p / W), o = O[p]; + let d = 0; + if (x > 0) d = max(d, abs(o - O[p - 1])); + if (x < W - 1) d = max(d, abs(o - O[p + 1])); + if (y > 0) d = max(d, abs(o - O[p - W])); + if (y < H - 1) d = max(d, abs(o - O[p + W])); + P[p] = (base[p] + GRAIN * gr[p] * (1 - smooth(d / 0.5))) * vig[p]; + } + S.page = page; + S.ink = ink; +} + +export function update(dt, input, params) { + const S = params.s; + const k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + S.band = cl(k[0], 0, 1); + S.speed = cl(k[1], 0.05, 3); + const page = cl(round(k[2]), 0, PAGES.length - 1); + const ink = cl(k[3], -1, 1); + if (page !== S.page || abs(ink - S.ink) > 0.015) { + if (S.page >= 0 && page !== S.page) S.fade = 0; + build(S, page, ink); + } + S.fade = min(1, S.fade + dt / 0.6); + S.T += dt; + // The beam steps down 13 rows a second per unit of Speed and starts again + // above the leaf once it has left the bottom. + S.beam += dt * 13 * S.speed; + if (S.beam > H + 30) S.beam -= H + 42; +} + +export function draw(display, params) { + const S = params.s; + if (S.page < 0) return; + const { P, O, U, Fe, gr, last, T } = S; + const s = (S.ink + 1) / 2; + // How much the old ink darkens each picture. + const aVis = 0.12 + 0.9 * s * s, aRed = 0.3 * aVis, qUV = 0.35 + 0.55 * s; + // Band weights: plain light, ultraviolet, pseudocolour. + const b = S.band; + const tU = smooth((b - 0.15) / 0.25), tP = smooth((b - 0.6) / 0.25); + const wV = 1 - tU, wP = tP, wU = tU - tP; + const beam = S.beam, spot = (T * 150) % (W + 20) - 10; + const fade = smooth(S.fade); + for (let y = 0; y < H; y++) { + // The iron map behind the beam: full for 10 rows, then fading. + const d = beam - y; + const xr = d < 0 ? exp(-d * d * 0.5) : d < 10 ? 1 : exp(-(d - 10) / 6); + const line = abs(d) < 2 ? 1 - abs(d) / 2 : 0; + for (let x = 0; x < W; x++) { + const p = y * W + x, pa = P[p], o = O[p], u = U[p]; + let r = 0, g = 0, bl = 0; + if (wV > 0) { + // Plain light: iron-gall brown absorbs blue more than red, and the + // faded old ink is a pale ochre. + r += wV * VIS[0] * pa * (1 - INK[0] * o) * (1 - 0.35 * aVis * u); + g += wV * VIS[1] * pa * (1 - INK[1] * o) * (1 - 0.6 * aVis * u); + bl += wV * VIS[2] * pa * (1 - INK[2] * o) * (1 - aVis * u); + } + // Ultraviolet: the parchment fluoresces; any iron left quenches it. + const F = pa * (1 - 0.88 * o) * (1 - qUV * u); + if (wU > 0) { + r += wU * (20 + 90 * F); + g += wU * (24 + 120 * F); + bl += wU * (40 + 200 * F); + } + if (wP > 0) { + // Pseudocolour: red light in red, ultraviolet in green and blue. + const R = pa * (1 - 0.85 * o) * (1 - aRed * u); + const G = cl((F - 0.2) * 1.35, 0, 1); + r += wP * 214 * R; + g += wP * 196 * G; + bl += wP * 200 * G; + } + if (xr > 0.004) { + // X-ray fluorescence: iron in either ink, pale blue on near black. + // Counting noise: the map is speckled where there is little iron. + const fe = Fe[p], sn = (1 - fe) * gr[p]; + const k2 = xr * (0.9 + 0.1 * pa); + r += k2 * (14 + 6 * sn + 112 * fe - r); + g += k2 * (22 + 9 * sn + 176 * fe - g); + bl += k2 * (44 + 16 * sn + 196 * fe - bl); + } + if (line > 0) { + const sp = abs(x - spot) < 3 ? 1 - abs(x - spot) / 3 : 0; + r += line * (30 + 120 * sp); + g += line * (44 + 140 * sp); + bl += line * (52 + 150 * sp); + } + if (fade < 1) { + r = last[p * 3] + (r - last[p * 3]) * fade; + g = last[p * 3 + 1] + (g - last[p * 3 + 1]) * fade; + bl = last[p * 3 + 2] + (bl - last[p * 3 + 2]) * fade; + } else { + last[p * 3] = r; + last[p * 3 + 1] = g; + last[p * 3 + 2] = bl; + } + display.setPixel(x, y, min(255, max(0, r)), min(255, max(0, g)), min(255, max(0, bl))); + } + } +} From 0b208466cc90377c5a5a3ed52bf411f418663b35 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:31:40 +0000 Subject: [PATCH 17/24] incoming: Composing Stick, pattern round 68 --- .../pattern-study/patterns/composing-stick.js | 438 ++++++++++++++++++ 1 file changed, 438 insertions(+) create mode 100644 incoming/pattern-study/patterns/composing-stick.js diff --git a/incoming/pattern-study/patterns/composing-stick.js b/incoming/pattern-study/patterns/composing-stick.js new file mode 100644 index 000000000..df6990ee5 --- /dev/null +++ b/incoming/pattern-study/patterns/composing-stick.js @@ -0,0 +1,438 @@ +// ===== Patternflow pattern ===== +// Title: Composing Stick +// Family: type +// Idea: Sorts fly one at a time from a type case whose lowercase boxes are sized by how often each letter is used into a composing stick, where the line reads left to right with every letter upside down (p shows as b, d as q) and is justified against the knee with em, en, thin and hair spaces, then turns over into the forme, where it reads right to left in mirror image (p as q), is inked and pressed onto a sheet at the foot of the panel, the only place it reads right, before the type flies back to its boxes. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: hand composition of movable type as Joseph Moxon set it out in Mechanick Exercises on the Whole Art of Printing (1683-84), with a simplified job case after the California job case (lowercase boxes sized by letter frequency, e the largest; capitals in equal boxes, J and U after Z); the pangram Sphinx of black quartz, judge my vow and the epitaph Benjamin Franklin wrote for himself (1728); an 8-row pixel face drawn for this pattern, in which b, d, p and q are one bitmap; pattern round 68 +// Round: 68 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Text=0..3, Spacing=0..1 +// +// Knob 1: Hue. The ink on the slab, the roller, the inked faces and the print: blue at 0, magenta at 0.25, orange at 0.5, green at 0.75. Metal, wood and paper keep their colours. +// Knob 2: Speed. At 2 a sort leaves the case about every 0.3 s, and mind your p's and q's goes from case to sheet and back every 11 s, the Sphinx's three lines every 18 s; at 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Text. Each with its own measure, and the knee moves to it: 0 mind your p's and q's on 52 LEDs, 1 Sphinx of black quartz, judge my vow. on 60, 2 bob pop bid bud dub on 60, picked because in the stick bob reads pop and pop bob, and in the forme bid, bud and dub read as themselves, 3 The Body of B. Franklin, Printer on 56. Turning it starts the shop over on the new text. +// Knob 4: Spacing. How a line is filled out to the measure: below 0.25 justified, each 3-em word space topped up with em, en, thin and hair spaces (the opening 0.06); to 0.5 letterspaced, one space between every two letters set as the letters go in and the rest in the word spaces; to 0.75 ragged, quads at the end; above that centred, quads at both ends. The last line is always quadded out. Hair spaces show as brass. + +const W = 64, H = 128, N = W * H; +const { floor, round, min, max, abs, cos, sin, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const mix = (a, b, t) => a.map((v, i) => v + (b[i] - v) * t); +const hsv = (h, s, v) => [5, 3, 1].map((n) => ((k) => 255 * v * (1 - s * max(0, min(k, 4 - k, 1))))((n + (h - floor(h)) * 6) % 6)); + +// The face: 8 rows, 2 for ascenders, 4 for the x-height and 2 for +// descenders; capitals stand on the upper 6. Rows top down, a missing row is +// blank. b, d, p and q are one bitmap turned and mirrored. +const FONT = { + a: ",,.###,#..#,#..#,.###", b: "#...,#...,###.,#..#,#..#,###.", c: ",,.##,#..,#..,.##", + d: "...#,...#,.###,#..#,#..#,.###", e: ",,.##.,####,#...,.###", f: ".##,.#.,###,.#.,.#.,.#.", + g: ",,.###,#..#,#..#,.###,...#,.##.", h: "#...,#...,###.,#..#,#..#,#..#", i: "#,,#,#,#,#", + j: ".#,,.#,.#,.#,.#,.#,#.", k: "#..,#..,#.#,##.,#.#,#.#", l: "#,#,#,#,#,#", + m: ",,####.,#.#.#,#.#.#,#.#.#", n: ",,###.,#..#,#..#,#..#", o: ",,.##.,#..#,#..#,.##.", + p: ",,###.,#..#,#..#,###.,#...,#...", q: ",,.###,#..#,#..#,.###,...#,...#", r: ",,#.##,##..,#...,#...", + s: ",,.###,##..,..##,###.", t: ",.#.,###,.#.,.#.,..#", u: ",,#..#,#..#,#..#,.##.", + v: ",,#.#,#.#,#.#,.#.", w: ",,#...#,#.#.#,#.#.#,.#.#.", x: ",,#..#,.##.,.##.,#..#", + y: ",,#..#,#..#,#..#,.###,...#,.##.", z: ",,####,..#.,.#..,####", + "'": "#,#", ",": ",,,,,#,#", ".": ",,,,,#", + B: "###.,#..#,###.,#..#,#..#,###.", F: "###,#..,##.,#..,#..,#..", P: "###.,#..#,#..#,###.,#...,#...", + S: ".###,#...,.##.,...#,...#,###.", T: "###,.#.,.#.,.#.,.#.,.#.", +}; +const GL = {}; +for (const ch in FONT) { + const rows = FONT[ch].split(","), w = max(...rows.map((r) => r.length)), bits = new Uint8Array(8 * w); + rows.forEach((r, y) => [...r].forEach((v, x) => (bits[y * w + x] = v === "#"))); + GL[ch] = { w, bits }; +} + +// The body is 10 LEDs, so an em quad is 10 wide; the smaller spaces are its +// fractions rounded to whole LEDs. A sort is its letter plus one LED of body. +const SPW = { em: 10, en: 5, thick: 3, thin: 2, hair: 1 }; +const QUADS = ["em", "en", "thin", "hair"]; +const LX = 2, BODY = 10, LINES = 3; +const STICK = 34, ROLL = 49, FORME = 55, SHEET = 93, PAPER0 = 90; + +// Each text with the measure its job is set to, in LEDs. +const TEXTS = [ + ["mind your p's and q's", 52], + ["Sphinx of black quartz, judge my vow.", 60], + ["bob pop bid bud dub", 60], + ["The Body of B. Franklin, Printer", 56], +]; + +// The case. Lowercase on the left in four bands, each box about as big as +// its letter is common; capitals on the right in equal boxes. A band gives +// its rows, keys and inside widths; a divider follows every box. +const BANDS = [ + [1, 4, ["'", ",", ".", "k", "hair", "thin", "em", "fi", "fl", "ff", "ffi"], [2, 2, 2, 4, 3, 4, 6, 2, 2, 2, 2]], + [6, 12, ["j", "b", "c", "d", "e", "i", "s", "f", "g"], [2, 2, 3, 4, 9, 4, 4, 3, 2]], + [14, 20, ["z", "l", "m", "n", "h", "o", "y", "p", "w", "en"], [2, 3, 3, 4, 4, 4, 3, 3, 3, 3]], + [22, 29, ["x", "q", "v", "u", "t", "thick", "a", "r"], [2, 2, 3, 3, 6, 6, 6, 6]], +]; +const CAPS = ["1234567", "890$&?!", "ABCDEFG", "HIKLMNO", "PQRSTVW", "XYZJU;:"]; +const CASE_H = 31, CN = W * CASE_H; + +const METAL = [206, 210, 218], WOOD = [150, 96, 48], FLOOR = [16, 10, 7], STEEL = [150, 158, 170], BED = [40, 44, 52]; +const IRON = [70, 74, 84], FURN = [132, 92, 52], DARK = [18, 18, 22], PAPER = [196, 186, 160]; +// Pieces: body, edge and the shank's three rows; spaces by kind, hair spaces brass. +const BODYC = [86, 88, 96], SEAM = [50, 52, 58], SHANK = [[116, 120, 128], [28, 30, 34], [76, 80, 88]]; +const SPC = { em: [36, 38, 44], en: [36, 38, 44], thick: [54, 56, 62], thin: [54, 56, 62], hair: [176, 132, 52] }; + +function buildCase(S) { + const boxes = (S.boxes = []), at = (S.at = {}); + const add = (key, x0, y0, x1, y1) => { + at[key] = boxes.length; + boxes.push({ x0, y0, x1, y1, cx: (x0 + x1 - 1) / 2, cy: (y0 + y1) / 2, fill: 0.4 + 0.25 * Math.random(), flash: 0 }); + }; + for (const [y0, y1, keys, ws] of BANDS) for (let i = 0, x = 1; i < keys.length; x += ws[i++] + 1) add(keys[i], x, y0, x + ws[i], y1 + 1); + CAPS.forEach((row, r) => [...row].forEach((ch, c) => add(/[A-Z]/.test(ch) ? ch : "c" + r + c, 43 + 3 * c, 1 + 5 * r, 45 + 3 * c, 5 + 5 * r))); + // Each box holds a heap, higher in the middle, so a big box reads as a big mound. + S.owner = new Int16Array(CN).fill(-1); + S.heap = new Float32Array(CN); + boxes.forEach((b, i) => { + for (let y = b.y0; y < b.y1; y++) + for (let x = b.x0; x < b.x1; x++) { + S.owner[y * W + x] = i; + S.heap[y * W + x] = cl(1.5 - ((2 * x + 1 - b.x0 - b.x1) / (b.x1 - b.x0 + 1)) ** 2 - ((2 * y + 1 - b.y0 - b.y1) / (b.y1 - b.y0 + 1)) ** 2, 0, 1); + } + }); + S.tex = Float32Array.from({ length: CN }, Math.random); + S.tone = Float32Array.from({ length: CN }, Math.random); +} + +const sortOf = (S, ch) => ({ ch, g: GL[ch], w: GL[ch].w + 1, box: S.at[ch] }); +const spaceOf = (S, k, extra) => ({ sp: k, w: SPW[k], box: S.at[k], extra }); +const widthOf = (ps) => ps.reduce((s, p) => s + p.w, 0); +function split(n, out, S, extra = true) { + for (const k of QUADS) for (; n >= SPW[k]; n -= SPW[k]) out.push(spaceOf(S, k, extra)); +} + +// Greedy line breaks with a 3-em word space. +function setText(S) { + const [text, M] = TEXTS[S.text]; + S.M = M; + S.P0 = 32 - M / 2; + const lines = (S.lines = []); + let cur = [], cw = 0; + for (const w of text.split(" ").map((w) => [...w].map((ch) => sortOf(S, ch)))) { + if (cur.length && cw + 3 + widthOf(w) > M) [cur, cw] = [(lines.push(cur), []), 0]; + cw += (cur.length ? 3 : 0) + widthOf(w); + cur.push(w); + } + lines.push(cur); +} + +// The line filled out to the measure by the Spacing knob's method. The last +// line of a paragraph is never spread; it is quadded out. +function justify(S, line, last) { + const mode = last && S.mode < 2 ? 2 : S.mode, gaps = line.length - 1; + const slack = S.M - line.reduce((s, w) => s + widthOf(w), 0) - 3 * gaps; + const word = new Array(gaps).fill(0), letter = line.map((w) => new Array(w.length).fill(0)); + let lead = 0, tail = 0; + if (mode === 0 && gaps) { + // Equal shares; the odd LEDs go first after punctuation, then from the left. + const q = floor(slack / gaps), punct = (i) => (/[,.]/.test(line[i].at(-1).ch) ? 1 : 0); + let r = slack - q * gaps; + word.fill(q); + for (const i of [...word.keys()].sort((a, b) => punct(b) - punct(a) || a - b)) if (r > 0) (word[i]++, r--); + } else if (mode === 1) { + // Letterspacing: the same space between every two letters, set as the + // letters are, and what is left in the word spaces. + const lg = line.reduce((s, w) => s + w.length - 1, 0), u = lg ? floor(slack / (lg + 2 * gaps)) : 0, r = slack - u * lg; + line.forEach((w, wi) => letter[wi].fill(u, 1)); + for (let i = 0; i < gaps; i++) word[i] = floor(r / gaps) + (i < r % gaps ? 1 : 0); + if (!gaps) tail = r; + } else if (mode === 3) tail = slack - (lead = floor(slack / 2)); + else tail = slack; + const out = []; + split(lead, out, S); + line.forEach((w, wi) => { + if (wi) { + out.push(spaceOf(S, "thick", false)); + split(word[wi - 1], out, S); + } + w.forEach((p, j) => { + split(letter[wi][j], out, S, false); + out.push({ ...p }); + }); + }); + split(tail, out, S); + return out; +} + +// What a piece shows at column c, row r, the way it prints: 1 body, 2 face, +// 3 the body's edge, 4 a space, which stands lower than the type. +function cell(p, c, r) { + if (p.sp) return 4; + if (!c) return 3; + return r > 0 && r < 9 && p.g.bits[(r - 1) * p.g.w + c - 1] ? 2 : 1; +} + +function layout(ps) { + let x = 0; + return ps.map((p) => ({ p, x: (x += p.w) - p.w, gone: false })); +} + +// The shop as it opens: the forme full and about to be inked, the last +// sheet printed, and the first word of the next copy in the stick. +function begin(S) { + setText(S); + S.just = S.lines.map((l, i) => justify(S, l, i === S.lines.length - 1)); + S.forme = { lines: S.just.map(layout), phase: "lock", t: 0.25, inkY: 0, ink: 0, roll: ROLL }; + S.sheet = { lines: S.forme.lines, P0: S.P0, x: 0, cut: H, press: -1 }; + S.fl = []; + S.boxes.forEach((b) => (b.flash = 0)); + newLine(S, 0); + const C = S.comp; + for (const p of C.ps) { + if (p.sp === "thick") break; + p.on = !p.extra; + } + targets(C); + C.ps.forEach((p) => (p.x = p.tx)); +} + +function newLine(S, li) { + S.comp = { li, ps: S.just[li].map((p) => ({ ...p, on: false, x: LX, tx: LX })), phase: "set", t: 0, busy: false }; +} + +function targets(C) { + let x = LX; + for (const p of C.ps) x += (p.tx = x, p.on ? p.w : 0); +} + +const fly = (S, x0, y0, x1, y1, T, col, land, ret) => S.fl.push({ x0, y0, x1, y1, t: 0, T, col, land, ret }); + +// The compositor: sets the line sort by sort, then the spaces that justify +// it, waits for the forme and slides the line in. +function stepComp(S, d) { + const C = S.comp; + C.t -= d; + if (C.phase === "set" || C.phase === "just") { + if (C.busy || C.t > 0) return; + const p = C.ps.find((q) => !q.on && (C.phase === "just" || !q.extra)); + if (!p) return C.phase === "set" ? (C.phase = "just") : Object.assign(C, { phase: "tight", t: 0.4 }); + targets(C); + const b = S.boxes[p.box]; + b.flash = 1; + b.fill = max(0.2, b.fill - 0.05); + C.busy = true; + fly(S, b.cx, b.cy, p.tx + p.w / 2, STICK + 4, p.sp ? 0.2 : 0.24, p.sp ? [120, 124, 132] : METAL, () => { + p.on = true; + targets(C); + p.x = p.tx; + Object.assign(C, { busy: false, t: 0.05 }); + }); + } else if (C.phase === "tight") { + if (C.t <= 0) C.phase = "wait"; + } else if (C.phase === "wait") { + if (S.forme.phase === "fill" && S.forme.lines.length === C.li) Object.assign(C, { phase: "move", t: 0.7, bits: lineBits(C.ps, S.M) }); + } else if (C.t <= 0) { + S.forme.lines.push(layout(C.ps)); + newLine(S, (C.li + 1) % S.lines.length); + } +} + +// A line as print-side codes, M by 10, hair spaces as 5. +function lineBits(list, M) { + const bits = new Uint8Array(M * BODY); + let x = 0; + for (const p of list) { + for (let c = 0; c < p.w; c++) for (let r = 0; r < BODY; r++) if (x + c < M) bits[r * M + x + c] = p.sp === "hair" ? 5 : cell(p, c, r); + x += p.w; + } + return bits; +} + +// The forme's round, with each step's length at Speed 2. +const PH = { lock: 0.35, ink: 1.3, swap: 0.6, press: 0.9, hold: 0.7 }; +const NEXT = { fill: "lock", lock: "ink", ink: "swap", swap: "press", press: "hold", hold: "dist" }; + +function stepForme(S, d) { + const F = S.forme, SH = S.sheet; + F.t -= d; + const u = 1 - F.t / (PH[F.phase] || 1); + if (F.phase === "ink") { + // The roller runs down over the forme and back to the slab. + const bot = FORME + BODY * LINES - 2; + F.roll = u < 0.55 ? ROLL + (bot - ROLL) * (u / 0.55) : bot + (ROLL - bot) * ((u - 0.55) / 0.45); + F.inkY = max(F.inkY, F.roll + 3); + F.ink = 1; + } else if (F.phase === "swap") { + // The last print slides out to the right, a fresh sheet in from the left. + SH.x = round(u < 0.5 ? u * 132 : (u - 1) * 132); + if (u >= 0.5) SH.lines = null; + } else if (F.phase === "press") SH.press = SH.cut = PAPER0 - 3 + (H + 3 - PAPER0) * u; + else if (F.phase === "dist") + // Distribution: each piece goes back to its own box. + for (; F.t <= 0; F.t += 0.045) { + let q = null, k = 0; + for (; k < F.lines.length && !q; k++) q = F.lines[k].find((e) => !e.gone); + if (!q) { + if (!S.fl.some((f) => f.ret)) Object.assign(F, { lines: [], inkY: 0, ink: 0, phase: "fill", t: 0 }); + break; + } + q.gone = true; + const b = S.boxes[q.p.box]; + const back = () => { + b.flash = 0.7; + b.fill = min(0.95, b.fill + 0.05); + }; + fly(S, 63 - S.P0 - q.x - q.p.w / 2, FORME + BODY * (k - 1) + 4, b.cx, b.cy, 0.32, q.p.sp ? [90, 94, 100] : mix(METAL, S.ink, 0.6), back, true); + } + if (F.phase === "fill" ? F.lines.length >= S.lines.length : F.phase !== "dist" && F.t <= 0) { + if (F.phase === "ink") F.roll = ROLL; + if (F.phase === "swap") Object.assign(SH, { x: 0, lines: F.lines, P0: S.P0, cut: PAPER0 - 3 }); + if (F.phase === "press") { + Object.assign(SH, { cut: H, press: -1 }); + F.ink = 0.55; + } + F.phase = NEXT[F.phase]; + F.t = PH[F.phase] || 0; + } +} + +function put(F, x, y, c, a = 1) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + F[o] += (c[0] - F[o]) * a; + F[o + 1] += (c[1] - F[o + 1]) * a; + F[o + 2] += (c[2] - F[o + 2]) * a; +} + +export function setup(params) { + const S = (params.s = { text: -1, mode: -1, F: new Float32Array(N * 3), base: new Float32Array(N * 3) }); + buildCase(S); + const B = S.base, g = (c, k) => c.map((v) => v * k); + // Case wood with a grain, the stick, the chase full of furniture. + for (let i = 0; i < CN; i++) put(B, i % W, floor(i / W), S.owner[i] >= 0 ? FLOOR : g(WOOD, 0.8 + 0.25 * S.tone[i - (i % W) + (((i % W) * 7) % W)])); + for (let x = 0; x < W; x++) { + put(B, x, STICK - 1, STEEL); + put(B, x, STICK + BODY + 3, g(STEEL, 0.85)); + for (let y = STICK; y < STICK + BODY + 3; y++) put(B, x, y, x < LX ? STEEL : BED); + } + const top = FORME - 2, bot = FORME + BODY * LINES + 1; + for (let y = top; y <= bot; y++) + for (let x = 0; x < W; x++) put(B, x, y, y === top || y === bot || !x || x === W - 1 ? IRON : g(FURN, 0.75 + 0.2 * (((x * 5 + y * 11) % 7) / 7))); + S.paper = new Float32Array(W * (H - PAPER0)); + for (let i = 0; i < S.paper.length; i++) S.paper[i] = 1 - 0.12 * (floor(i / W) / (H - PAPER0)) - 0.05 * (abs((i % W) - 31.5) / 31.5); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + S.hue = cl(k[0] || 0, 0, 1); + S.sp = cl(k[1] ?? 2, 0.2, 3) / 2; + S.ink = hsv(0.6 + S.hue, 0.85, 1); + const text = round(cl(k[2] || 0, 0, 3)), mode = min(3, floor(cl(k[3] ?? 0.06, 0, 1) * 4)); + if (text !== S.text || mode !== S.mode) { + Object.assign(S, { text, mode }); + begin(S); + } + const d = dt * S.sp; + for (const f of S.fl) if ((f.t += d) >= f.T && !f.done) (f.done = true), f.land(); + S.fl = S.fl.filter((f) => !f.done); + stepComp(S, d); + stepForme(S, d); + for (const p of S.comp.ps) if (p.on) p.x += (p.tx - p.x) * min(1, d * 14); + for (const b of S.boxes) b.flash = max(0, b.flash - dt * 3); +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.comp) return; + const F = S.F, ink = S.ink, FL = S.forme, SH = S.sheet, C = S.comp, M = S.M, P0 = S.P0; + F.set(S.base); + const colOf = (p, code, face) => (code === 2 ? face : code === 1 ? BODYC : code === 3 ? SEAM : SPC[p.sp]); + + // The case: each box a heap of sorts as full as it is, lit when touched. + for (let i = 0; i < CN; i++) { + const b = S.boxes[S.owner[i]]; + if (!b) continue; + const h = S.heap[i], v = (40 + 90 * S.tone[i]) * (0.55 + 0.45 * h); + if (S.tex[i] < b.fill * h) F.set([v, v * 1.02, v * 1.1], i * 3); + if (b.flash > 0) put(F, i % W, floor(i / W), [255, 226, 150], b.flash * 0.7); + } + + // The knee, set to the measure, lights when the line is tight. + for (let y = STICK - 1; y < STICK + BODY + 4; y++) for (let x = LX + M; x < LX + M + 2; x++) put(F, x, y, C.phase === "tight" ? [240, 240, 228] : STEEL); + // Sorts in the stick, seen from the compositor's side: the line reads left + // to right and every face is upside down, the mirror of the print turned + // round. Below each face its shank, nick up, the nicks in one line. + if (C.phase !== "move") + for (const p of C.ps) { + if (!p.on) continue; + for (let c = 0, x = round(p.x); c < p.w; c++, x++) { + if (x < LX || x >= LX + M) continue; + for (let r = 0; r < BODY; r++) put(F, x, STICK + r, colOf(p, cell(p, c, BODY - 1 - r), METAL)); + for (let j = 0; j < 3; j++) put(F, x, STICK + BODY + j, p.sp ? SPC[p.sp] : c ? SHANK[j] : SEAM, p.sp ? 1 - 0.1 * j : 1); + } + } + + // The ink slab, and the roller resting on it or running over the forme. + for (let y = ROLL - 1; y < ROLL + 4; y++) for (let x = 0; x < W; x++) put(F, x, y, mix([0, 0, 0], ink, 0.42 + 0.1 * ((x * 3 + y * 5) % 4))); + const ry = round(FL.roll); + for (let x = 0; x < W; x++) for (let j = 0; j < 3; j++) put(F, x, ry + j, x < 2 || x > 61 ? STEEL : mix([0, 0, 0], ink, 0.95 - 0.25 * j)); + if (FL.phase === "ink") for (let x = 0; x < W; x++) put(F, x, ry - 1, IRON); + + // The forme: the lines seen from the face, mirrored and reading right to + // left, inked where the roller has passed. Slots past the text stay + // furniture; quoins at the foot open when it is locked. + const face = mix(METAL, ink, FL.ink); + for (let k = 0; k < S.lines.length; k++) { + for (let y = FORME + BODY * k; y < FORME + BODY * (k + 1); y++) for (let x = P0; x < P0 + M; x++) put(F, x, y, DARK); + for (const q of FL.lines[k] || []) + if (!q.gone) + for (let c = 0; c < q.p.w; c++) + for (let r = 0, y = FORME + BODY * k; r < BODY; r++, y++) put(F, 63 - (P0 + q.x + c), y, colOf(q.p, cell(q.p, c, r), y < FL.inkY ? face : METAL)); + } + const locked = FL.phase !== "fill" && FL.phase !== "dist"; + for (const qx of [18, 40]) for (let i = 0; i < (locked ? 6 : 4); i++) put(F, qx + i, FORME + BODY * LINES, locked ? [200, 204, 210] : [110, 114, 120]); + + // The line on its way from stick to forme turns over: upside down to the + // right way up, then mirrored, so for an instant it reads as it will print. + if (C.phase === "move") { + const s = cl(1 - C.t / 0.7, 0, 1), sy = cos(PI * cl(2 * s, 0, 1)), sx = cos(PI * cl(2 * s - 1, 0, 1)); + const cx = LX + M / 2 + (32 - LX - M / 2) * s, cy = STICK + 5 + (FORME + BODY * C.li - STICK) * s; + const hw = (M / 2) * abs(sx), hh = (BODY / 2) * abs(sy), lit = 0.6 + 0.4 * abs(sx * sy); + const pal = [null, BODYC, METAL, SEAM, SPC.thick, SPC.hair].map((c) => c && c.map((v) => v * lit)); + for (let y = floor(cy - hh); y < cy + hh; y++) + for (let x = floor(cx - hw); x < cx + hw; x++) { + const u = floor((x + 0.5 - cx) / (sx || 1e-3) + M / 2), v = floor((y + 0.5 - cy) / (sy || 1e-3) + BODY / 2); + const code = u >= 0 && u < M && v >= 0 && v < BODY ? C.bits[(BODY - 1 - v) * M + u] : 0; + if (code) put(F, x, y, pal[code]); + } + } + + // The sheet, sliding in fresh before each impression; it takes the print + // as the cylinder passes, and only here does the line read right. + const tone = mix(PAPER, hsv(0.6 + S.hue, 0.95, 0.8), 0.94); + for (let i = 0; i < S.paper.length; i++) { + const x = i % W, X = x + SH.x, g = S.paper[i], o = (PAPER0 * W + i - x + X) * 3; + if (!x || x === W - 1 || X < 0 || X >= W) continue; + F[o] = PAPER[0] * g; + F[o + 1] = PAPER[1] * g; + F[o + 2] = PAPER[2] * g; + } + if (SH.lines) + SH.lines.forEach((line, k) => { + for (const q of line) + for (let c = 1; c < q.p.w && !q.p.sp; c++) + for (let r = 1, y = SHEET + BODY * k + 1; r < 9; r++, y++) if (y < SH.cut && cell(q.p, c, r) === 2) put(F, SH.P0 + q.x + c + SH.x, y, tone); + }); + if (SH.press >= 0) for (let x = 0; x < W; x++) for (let j = 0; j < 3; j++) put(F, x, round(SH.press) + j, [210 - 40 * j, 216 - 40 * j, 224 - 40 * j]); + + // Sorts in flight, with a short trail. + for (const f of S.fl) { + const u = cl(f.t / f.T, 0, 1), e = u * u * (3 - 2 * u); + for (const [back, a] of [[0.08, 0.35], [0, 1]]) { + const ue = max(0, e - back); + const x = round(f.x0 + (f.x1 - f.x0) * ue), y = round(f.y0 + (f.y1 - f.y0) * ue - 6 * sin(PI * ue)); + for (let j = 0; j < 4; j++) put(F, x + (j & 1), y + (j >> 1), f.col, j > 1 ? a * 0.8 : a); + } + } + + for (let y = 0, o = 0; y < H; y++) for (let x = 0; x < W; x++, o += 3) display.setPixel(x, y, F[o], F[o + 1], F[o + 2]); +} From 2dd1959c9515a8af8871393726731f747a1ca902 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:33:17 +0000 Subject: [PATCH 18/24] incoming: Palimpsest, pattern round 68 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JktahMn4mzzFyveZ8fo --- incoming/pattern-study/patterns/palimpsest.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/incoming/pattern-study/patterns/palimpsest.js b/incoming/pattern-study/patterns/palimpsest.js index 66a8610e8..056179084 100644 --- a/incoming/pattern-study/patterns/palimpsest.js +++ b/incoming/pattern-study/patterns/palimpsest.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Palimpsest // Family: type -// Idea: A parchment leaf carries two Greek texts at right angles, a prayer written across the page in dark iron-gall ink over a scraped-off treatise of Archimedes that runs down it, and the imaging band sweeps from plain light, where the old text is a faint ghost, to ultraviolet, where the parchment glows and the old ink shows dark, to the pseudocolour view that turns the old text red and leaves the prayer dark, while an X-ray beam rasters down the leaf and lights the iron of both inks behind it. +// Idea: A parchment leaf carries two Greek texts at right angles, a prayer written across the page in brown iron-gall ink over a scraped-off treatise of Archimedes that runs down it, and the imaging band sweeps from plain light, where the old text is a faint ghost, to ultraviolet, where the parchment glows and the old ink shows dark, to the pseudocolour view that turns the old text red and leaves the prayer dark, while an X-ray beam rasters down the leaf and lights the iron of both inks behind it. // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -11,10 +11,10 @@ // @matrix 64x128 // @knobs Band=0..1, Speed=0.2..3, Page=0..3, Ink=-1..1 // -// Knob 1: Band. The light the leaf is seen in. 0 to 0.15 is plain light: cream parchment, brown-black prayer, the old text a ghost 8% darker than the page. 0.4 to 0.6 is ultraviolet: the parchment glows blue and both inks put the glow out, the prayer nearly black and the old text dark blue. 0.85 to 1 is pseudocolour: a red-light picture in the red channel and the ultraviolet one in green and blue, so the old text comes out red and the prayer dark grey. Between, one blends into the next. -// Knob 2: Speed. How fast the X-ray beam steps down the leaf: 26 rows a second at 2, a pass of the 128 rows every 6.5 s; 2.6 rows a second at 0.2. Behind the beam, for about 16 rows, the leaf shows its iron map: both inks pale blue on black, the scraped text as plain as the prayer. -// Knob 3: Page. Which pair of texts, rounded to 0 to 3. 0: the Trisagion over the title of the Method. 1: the Lord's Prayer over the first proposition of the Measurement of a Circle. 2: Kyrie eleison over On Floating Bodies. 3: Glory to thee over the Stomachion. -// Knob 4: Ink. How much of the old ink the scraper left and how far both inks have bled into the parchment. At -1 the strokes keep to 1 LED and the old text is all but gone even in ultraviolet; at 1 they bleed about 2 LEDs wide and the old text shows brown in plain light. +// Knob 1: Band. The light the leaf is seen in. 0 to 0.15 is plain light: ivory parchment, brown prayer, the old text an ochre ghost 9% darker than the page in red and 25% in blue. 0.4 to 0.6 is ultraviolet: the parchment glows blue and the iron in both inks puts the glow out, the prayer down to 42% of the page and the old text to 64%. 0.85 to 1 is pseudocolour: the red-light picture in the red channel and the ultraviolet one in green and blue, so the old text, pale in red and dark in ultraviolet, comes out red, and the prayer, dark in both, near black. Between, one picture fades into the next. +// Knob 2: Speed. How fast the X-ray beam steps down the leaf, a spot running along it: 26 rows a second at 2, a pass every 6.5 s; 2.6 rows a second at 0.2. For about 16 rows behind the beam the leaf shows its iron map, both inks pale blue on near black, the scraped text as plain as the prayer. +// Knob 3: Page. Which pair of texts, rounded to 0 to 3. 0: the Trisagion over the title of the Method. 1: the Lord's Prayer over the title and first words of the Measurement of a Circle. 2: Kyrie eleison over On Floating Bodies. 3: Glory to thee, our God over the Stomachion. +// Knob 4: Ink. How much of the old ink the scraper left and how far both inks have bled into the parchment. At -1 the prayer's strokes spread 0.5 LED and the old text's 0.35, and even in ultraviolet the old text is a thin line at 76% of the page; at 1 they spread 0.8 and 0.95 LED and the old text shows ochre-brown in plain light, 20% darker than the page in red and 57% in blue. const W = 64, H = 128, N = W * H; const { floor, round, min, max, abs, exp } = Math; @@ -31,7 +31,7 @@ const FONT = { δ: [0, ".###", "#...", ".##.", "#..#", "#..#", "#..#", ".##."], ε: [2, ".###", "#...", "###.", "#...", ".###"], ζ: [0, "####", "..#.", ".#..", "#...", "#...", "#...", ".##.", "...#", "..#."], - η: [2, "#.#.", "##.#", "#..#", "#..#", "#..#", "...#", "...#"], + η: [2, "###.", "#..#", "#..#", "#..#", "#..#", "...#", "...#"], θ: [0, ".##.", "#..#", "#..#", "####", "#..#", "#..#", ".##."], ι: [2, "#.", "#.", "#.", "#.", ".#"], κ: [2, "#..#", "#.#.", "##..", "#.#.", "#..#"], @@ -289,10 +289,11 @@ export function draw(display, params) { bl += wU * (40 + 200 * F); } if (wP > 0) { - // Pseudocolour: red light in red, ultraviolet in green and blue. - const R = pa * (1 - 0.85 * o) * (1 - aRed * u); + // Pseudocolour: red light in red, ultraviolet in green and blue, + // both stretched the same way. + const R = cl((pa * (1 - 0.85 * o) * (1 - aRed * u) - 0.2) * 1.35, 0, 1); const G = cl((F - 0.2) * 1.35, 0, 1); - r += wP * 214 * R; + r += wP * 216 * R; g += wP * 196 * G; bl += wP * 200 * G; } From 4c65cc461b7e8df763a0d1ef442963b95cb7b711 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:37:07 +0000 Subject: [PATCH 19/24] incoming: Palimpsest, pattern round 68 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JktahMn4mzzFyveZ8fo --- incoming/pattern-study/patterns/palimpsest.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/incoming/pattern-study/patterns/palimpsest.js b/incoming/pattern-study/patterns/palimpsest.js index 056179084..d33ce156b 100644 --- a/incoming/pattern-study/patterns/palimpsest.js +++ b/incoming/pattern-study/patterns/palimpsest.js @@ -11,10 +11,10 @@ // @matrix 64x128 // @knobs Band=0..1, Speed=0.2..3, Page=0..3, Ink=-1..1 // -// Knob 1: Band. The light the leaf is seen in. 0 to 0.15 is plain light: ivory parchment, brown prayer, the old text an ochre ghost 9% darker than the page in red and 25% in blue. 0.4 to 0.6 is ultraviolet: the parchment glows blue and the iron in both inks puts the glow out, the prayer down to 42% of the page and the old text to 64%. 0.85 to 1 is pseudocolour: the red-light picture in the red channel and the ultraviolet one in green and blue, so the old text, pale in red and dark in ultraviolet, comes out red, and the prayer, dark in both, near black. Between, one picture fades into the next. +// Knob 1: Band. The light the leaf is seen in. 0 to 0.15 is plain light: ivory parchment, brown prayer, the old text an ochre ghost 8% darker than the page in red and 25% in blue. 0.4 to 0.6 is ultraviolet: the parchment glows blue and the iron in both inks puts the glow out, the prayer down to 42% of the page and the old text to 65%. 0.85 to 1 is pseudocolour: the red-light picture in the red channel and the ultraviolet one in green and blue, so the old text, pale in red and dark in ultraviolet, comes out red, and the prayer, dark in both, near black. Between, one picture fades into the next. // Knob 2: Speed. How fast the X-ray beam steps down the leaf, a spot running along it: 26 rows a second at 2, a pass every 6.5 s; 2.6 rows a second at 0.2. For about 16 rows behind the beam the leaf shows its iron map, both inks pale blue on near black, the scraped text as plain as the prayer. // Knob 3: Page. Which pair of texts, rounded to 0 to 3. 0: the Trisagion over the title of the Method. 1: the Lord's Prayer over the title and first words of the Measurement of a Circle. 2: Kyrie eleison over On Floating Bodies. 3: Glory to thee, our God over the Stomachion. -// Knob 4: Ink. How much of the old ink the scraper left and how far both inks have bled into the parchment. At -1 the prayer's strokes spread 0.5 LED and the old text's 0.35, and even in ultraviolet the old text is a thin line at 76% of the page; at 1 they spread 0.8 and 0.95 LED and the old text shows ochre-brown in plain light, 20% darker than the page in red and 57% in blue. +// Knob 4: Ink. How much of the old ink the scraper left and how far both inks have bled into the parchment. At -1 the prayer's strokes spread 0.5 LED and the old text's 0.35, and even in ultraviolet the old text is a thin line at 75% of the page; at 1 they spread 0.8 and 0.95 LED and the old text shows ochre-brown in plain light, 21% darker than the page in red and 59% in blue. const W = 64, H = 128, N = W * H; const { floor, round, min, max, abs, exp } = Math; @@ -77,7 +77,7 @@ const PAGES = [ const LINE = 10, OVER_LINES = 12, UNDER_LINE = 12, UNDER_LINES = 5, SPACE = 3; // Plain light: the parchment's colour, how much of each channel the prayer's // ink takes at full strength, and the grain, a share of the page's level. -const VIS = [226, 208, 168], INK = [0.6, 0.72, 0.84], GRAIN = 0.09; +const VIS = [226, 208, 168], INK = [0.62, 0.72, 0.84], GRAIN = 0.08; function lay(words, len, lines, put) { // The words run on without a break, as a scribe filled a line: a word From 65a1a21e622c52761d3c3c0d892dedca7053f1a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:40:31 +0000 Subject: [PATCH 20/24] incoming: Structure ASCII, pattern round 68 --- .../pattern-study/patterns/structure-ascii.js | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/incoming/pattern-study/patterns/structure-ascii.js b/incoming/pattern-study/patterns/structure-ascii.js index d519fa64b..c981c047b 100644 --- a/incoming/pattern-study/patterns/structure-ascii.js +++ b/incoming/pattern-study/patterns/structure-ascii.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Structure ASCII // Family: type -// Idea: The outline of a big letter sways and melts from A to S to & to the Hangul syllable 글 behind a grid of character cells, and each cell the outline crosses takes the ASCII character whose strokes lie nearest the piece of outline inside it, so slashes, bars, underscores, parentheses and ticks join into the letter's contour, as in structure-based ASCII art; a fourth setting picks by ink instead, as tone-based ASCII art does. +// Idea: The outline of a big letter sways and melts from A to S to & to the Hangul syllable 글 behind a grid of character cells, lit only by a dim rim just inside its edge, and each cell the outline crosses takes the ASCII character whose strokes lie nearest the piece of outline inside it, so slashes, bars, underscores, parentheses and ticks join into the letter's contour, as in structure-based ASCII art; a fourth setting picks by ink instead, as tone-based ASCII art does. // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -11,7 +11,7 @@ // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Set=0..3, Cell=0..3 // -// Knob 1: Hue. Turns every colour round the wheel together. At 0 the straight strokes are turquoise, curves and angles azure, corners and crossings green, dots and ticks lilac, and the outline under them a dim blue; at 0.5 the strokes are red. +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the straight strokes are turquoise, curves and angles azure, corners and crossings green, dots and ticks lilac, and a rim 4 LEDs deep just inside the outline a dim blue (16, 19, 45), so the characters stand on the edge of a shape; at 0.5 the strokes are red. // Knob 2: Speed. At 2 a letter holds 4.5 s and melts into the next in 1.5 s, so the four take 24 s, while the drawing sways 0.12 radian (about 7 degrees) each way every 9 s. At 0.2 it all takes ten times as long, at 3 two thirds as long. // Knob 3: Set. The characters a cell may take, each set with the space. At 0 the 27 of the structure set (. , ' ` " - _ = | / \ ( ) < > ^ v ~ o + L J 7 T Y V X); at 1 twelve of them (/ \ | - _ . ' , ( ) < >); at 2 only / \ | and -. At 3 the tone ramp . : - = + * # % @, picked by how much of the cell the outline darkens, not by its shape. // Knob 4: Cell. The size of a character cell in LEDs: 5 by 8 at 0 (a grid of 12 by 16), 4 by 6 at 1 (16 by 21), 3 by 5 at 2 (21 by 25), and 7 by 11 at 3 (9 by 11), where each character is big enough to read. @@ -100,6 +100,8 @@ const CELLS = [ // The distance is capped at CAP LEDs; a shift of the character by one LED // across or down costs SHIFT. const CAP = 3, SHIFT = 0.12; +// The brightness and depth of the rim inside the letter. +const IN = 0.18, BAND = 4; const SX = (cw) => (cw >= 4 ? 2 : 1), SY = 1; // Each character at each cell size: its bitmap, its lit LEDs, and the @@ -278,13 +280,17 @@ export function draw(display, params) { const set = SETS[S.set], tone = S.set === 3, shifts = SHIFTS[S.cell]; const bx = SX(cw), by = SY, ew = cw + 2 * bx, eh = ch + 2 * by; const sq = S.sq, nsq = S.nsq, rgb = S.rgb, dT = S.dt, segs = S.segs, tl = S.tl; - for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) display.setPixel(x, y, 0, 0, 0); - const h0 = 0.47 + S.hue; - // The outline itself, faint. - hsv(h0 + 0.18, 0.7, 0.24, rgb); + const h0 = 0.47 + S.hue, f = S.f; + // A faint rim BAND LEDs deep inside the outline, so the characters stand + // on the edge of a shape. + hsv(h0 + 0.18, 0.65, IN, rgb); + const ir = rgb[0] | 0, ig = rgb[1] | 0, ib = rgb[2] | 0; for (let y = 0; y < H; y++) - for (let x = 0; x < W; x++) - if (nsq[y * W + x]) display.setPixel(x, y, rgb[0] | 0, rgb[1] | 0, rgb[2] | 0); + for (let x = 0; x < W; x++) { + const c = 0.25 * (f[y * LW + x] + f[y * LW + x + 1] + f[(y + 1) * LW + x] + f[(y + 1) * LW + x + 1]); + if (c < 0 && c > -BAND) display.setPixel(x, y, ir, ig, ib); + else display.setPixel(x, y, 0, 0, 0); + } for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) { const gx = ox + c * cw, gy = oy + r * ch, id = r * 32 + c; From 6468a6bd8573483ee55075bd276fda4595c3d178 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:40:45 +0000 Subject: [PATCH 21/24] incoming: Fibonacci Word, band round the coarse curve lifts coverage Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JktahMn4mzzFyveZ8fo --- .../pattern-study/patterns/fibonacci-word.js | 57 ++++++++++++++++--- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/incoming/pattern-study/patterns/fibonacci-word.js b/incoming/pattern-study/patterns/fibonacci-word.js index eea831e45..8027e1385 100644 --- a/incoming/pattern-study/patterns/fibonacci-word.js +++ b/incoming/pattern-study/patterns/fibonacci-word.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Fibonacci Word // Family: fractal -// Idea: A pen reads the Fibonacci word 0100101001001... and takes one step for every digit, turning a quarter turn at every 0, left in an even place and right in an odd one, so 144 crisp steps of 4 LEDs draw a self-avoiding curve of squares nested in squares over the coarse curve of the first 34 digits, each coarse step giving way to the 5 or 3 fine steps that 0 -> 01001 and 1 -> 010 make of it; the three copies of those 34 digits inside the 144 then light in turn, and the panel dives into the first with a quarter turn and a 1 + sqrt 2 zoom until it stands where the whole stood, mirrored, and the pen draws the whole over it again. +// Idea: A pen reads the Fibonacci word 0100101001001... and takes one step for every digit, turning a quarter turn at every 0, left in an even place and right in an odd one, so 144 crisp steps of 4 LEDs draw a self-avoiding curve of squares nested in squares over the coarse curve of the first 34 digits, which lies in a faint band 5 LEDs wide, each coarse step giving way to the 5 or 3 fine steps that 0 -> 01001 and 1 -> 010 make of it; the three copies of those 34 digits inside the 144 then light in turn, and the panel dives into the first with a quarter turn and a 1 + sqrt 2 zoom until it stands where the whole stood, mirrored, and the pen draws the whole over it again. // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -182,11 +182,15 @@ function geometry(n, deg, mir, keepPsi) { GX[j] = f[0] * u - f[1] * v + f[2]; GY[j] = f[1] * u + f[0] * v + f[3]; } - return { n, N, Nc, deg, mir, grid, cam, X, Y, GX, GY, step: hypot(cam.ar, cam.ai) }; + // The band round the coarse curve: 2 LEDs each side when its steps are + // near 10 LEDs long, 1 when they are near 5. + const hw = cl(round(0.22 * hypot(GX[1] - GX[0], GY[1] - GY[0])), 1, 2); + return { n, N, Nc, deg, mir, grid, cam, X, Y, GX, GY, hw, step: hypot(cam.ar, cam.ai) }; } -// How bright the coarse curve waits under the pen. -const GHOST = 0.72; +// How bright the coarse curve waits under the pen, and its band. The band +// stays above 40 of 255, so its rim is a hard edge and not a glow. +const GHOST = 0.72, BANDV = 0.19; const angleOf = (k) => { const a = abs(k); @@ -198,7 +202,10 @@ function startDraw(S, ghost) { } export function setup(params) { - const S = (params.s = { t: 0, hue: 0, speed: 2, phase: 0, pen: 0, pt: 0, gl: GHOST, zt: 0, acc: new Float32Array(W * H * 3), c: [0, 0, 0] }); + const S = (params.s = { + t: 0, hue: 0, speed: 2, phase: 0, pen: 0, pt: 0, gl: GHOST, zt: 0, c: [0, 0, 0], + acc: new Float32Array(W * H * 3), band: new Float32Array(W * H), bandU: new Float32Array(W * H), + }); S.g = geometry(12, 90, 1, -1); startDraw(S, GHOST); } @@ -276,11 +283,29 @@ function lineGrid(acc, x0, y0, x1, y1, r, g, b, upto) { } } +// Every LED within hw of a segment joins the band at level v, the highest +// level keeping its colour u, so crossings do not add up. +function bandSeg(S, x0, y0, x1, y1, hw, v, u) { + if (v <= 0.01) return; + const B = S.band, U = S.bandU, r = hw + 0.45, dx = x1 - x0, dy = y1 - y0, L2 = dx * dx + dy * dy; + const xa = max(0, floor(min(x0, x1) - r)), xb = min(W - 1, ceil(max(x0, x1) + r)); + const ya = max(0, floor(min(y0, y1) - r)), yb = min(H - 1, ceil(max(y0, y1) + r)); + for (let y = ya; y <= yb; y++) + for (let x = xa; x <= xb; x++) { + const t = L2 > 0 ? cl(((x - x0) * dx + (y - y0) * dy) / L2, 0, 1) : 0, ex = x0 + dx * t - x, ey = y0 + dy * t - y; + if (ex * ex + ey * ey <= r * r) { + const p = y * W + x; + if (v > B[p]) (B[p] = v), (U[p] = u); + } + } +} + export function draw(display, params) { const S = params.s; if (!S || !S.g) return; const acc = S.acc, c = S.c, h0 = 0.42 + S.hue; acc.fill(0); + S.band.fill(0); const g = S.g, N = g.N, Nc = g.Nc; const col = (u, v, white) => { hsv(h0 + 0.5 * u, 0.78 * (1 - white), v, c); @@ -289,7 +314,16 @@ export function draw(display, params) { if (S.phase === 2) { // The dive: every point of the curve under the similarity's power. const z = S.z, e = smooth(S.zt), rr = pow(z.r, e), ph = z.phi * e, er = rr * cos(ph), ei = rr * sin(ph); - const out = 1 - smooth(S.zt / 0.6); + const out = 1 - smooth(S.zt / 0.6), zx = (x, y) => z.cx + er * (x - z.cx) - ei * (y - z.cy), zy = (x, y) => z.cy + ei * (x - z.cx) + er * (y - z.cy); + // The old band rides the dive out; a new one grows round the first copy, + // which is the next picture's coarse curve. + const vo = BANDV * (1 - smooth(e / 0.35)), vn = BANDV * smooth(e / 0.5); + for (let j = 0; j < Nc; j++) { + const ax = g.GX[j], ay = g.GY[j], bx = g.GX[j + 1], by = g.GY[j + 1]; + bandSeg(S, zx(ax, ay), zy(ax, ay), zx(bx, by), zy(bx, by), g.hw, vo, (BLK[j] + BLK[j + 1]) / (2 * N)); + const cx = g.X[j], cy = g.Y[j], dx = g.X[j + 1], dy = g.Y[j + 1]; + bandSeg(S, zx(cx, cy), zy(cx, cy), zx(dx, dy), zy(dx, dy), z.next.hw * e, vn, (BLK[j] + BLK[j + 1]) / (2 * N)); + } let px = 0, py = 0; for (let i = 0; i <= N; i++) { const dx = g.X[i] - z.cx, dy = g.Y[i] - z.cy, x = z.cx + er * dx - ei * dy, y = z.cy + ei * dx + er * dy; @@ -306,8 +340,10 @@ export function draw(display, params) { px = x, py = y; } } else { - // The coarse curve, each step fading as the pen finishes its block. - const rate = max(0.5, 70 / max(0.05, g.step)); + // The coarse curve, each step fading as the pen finishes its block, and + // its band, which stays. + const rate = max(0.5, 70 / max(0.05, g.step)), rd = g.grid ? round : (v) => v; + for (let j = 0; j < Nc; j++) bandSeg(S, rd(g.GX[j]), rd(g.GY[j]), rd(g.GX[j + 1]), rd(g.GY[j + 1]), g.hw, BANDV, (BLK[j] + BLK[j + 1]) / (2 * N)); for (let j = 0; j < Nc; j++) { const gone = (S.pen - BLK[j + 1]) / (0.3 * rate) + (S.phase === 1 ? 8 * S.pt : 0), a = S.gl * (1 - cl(gone, 0, 1)); if (a <= 0.01) continue; @@ -348,6 +384,11 @@ export function draw(display, params) { for (const [ox, oy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) splat(acc, px + ox, py + oy, 0.45, c[0], c[1], c[2]); } } + for (let p = 0; p < W * H; p++) { + if (S.band[p] <= 0) continue; + col(S.bandU[p], S.band[p], 0); + acc[3 * p] += c[0], acc[3 * p + 1] += c[1], acc[3 * p + 2] += c[2]; + } // The tape: the word itself down the right edge, two rows a digit, the // digit being read on row 64. A 0 is two LEDs wide, a 1 one. Read digits // are lit in their colour, the rest wait dim; in the dive it rewinds. From 5e7a9b159cb6682691ffdac4b096986b8772076c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:43:00 +0000 Subject: [PATCH 22/24] =?UTF-8?q?incoming:=2068=ED=8C=90=20=ED=91=9C?= =?UTF-8?q?=EC=99=80=20=EC=A0=88,=20=EB=8B=A4=EC=9D=8C=20=ED=8C=90=20?= =?UTF-8?q?=EA=B0=90=EC=9D=84=20README=20=EC=97=90=20=EC=A0=81=EB=8A=94?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JktahMn4mzzFyveZ8fo --- incoming/pattern-study/README.md | 103 +++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 4 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 79e792d9b..e3f36cefc 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 568개를 쉰여섯 갈래로 나눠 만들었어요. 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판도 같은 표로 돌아서 글자 패턴 넷을 글자 갈래에, 하나를 데이터 그림 갈래에 더하고, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 578개를 쉰여섯 갈래로 나눠 만들었어요. 1판에서 열세 갈래로 23개를 만들고, 2판에서 1판에 없던 아홉 갈래로 16개를, 3판에서 하나를, 4판에서 새 갈래인 직물로 하나를 더했어요. 5판은 새 패턴 없이 1판의 Euclid Pulse 를 다시 짰고, 6판은 세운 패널의 128줄을 위아래로 다 쓰는 패턴 하나를 더했어요. 7판은 또렷한 모양이 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 8판은 새 갈래인 기계로 하나를 더했어요. 9판은 새 패턴 없이 1판의 Sprout 을 다시 짰어요. 10판은 세운 패널을 떨어지는 블록의 우물로 쓰는 패턴 하나를 더했어요. 11판은 여러 색이 꽤 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 12판은 새 갈래인 천체로 하나를 더했어요. 13판은 새 패턴 없이 1판의 Glyph Rain 을 다시 짰어요. 14판은 절차가 바뀌기 전에 띄운 바퀴라서, 세운 패널의 높이를 다 떨어지는 폭포 하나를 더했어요. 15판은 바뀐 절차의 첫 판이라 열 갈래에 새 패턴을 하나씩, 모두 열 개를 더했어요. 도시와 음악 갈래는 그때 새로 열었어요. 16판도 열 갈래에 하나씩 열 개를 더했고, 바다와 곤충 갈래를 새로 열었어요. 17판도 열 갈래에 하나씩 열 개를 더했고, 전자 회로와 장난감 갈래를 새로 열었어요. 18판도 열 갈래에 하나씩 열 개를 더했고, 유리와 광물·결정, 놀이공원 갈래를 새로 열었어요. 19판도 열 갈래에 하나씩 열 개를 더했고, 부엌과 버섯·균사 갈래를 새로 열었어요. 20판도 열 갈래에 하나씩 열 개를 더했고, 새와 화학·실험 갈래를 새로 열었어요. 21판도 열 갈래에 하나씩 열 개를 더했고, 종이와 운동·경기 갈래를 새로 열었어요. 22판도 열 갈래에 하나씩 열 개를 더했고, 몸과 비행 갈래를 새로 열었어요. 23판도 열 갈래에 하나씩 열 개를 더했고, 무대·공연과 짐승 갈래를 새로 열었어요. 24판도 열 갈래에 하나씩 열 개를 더했고, 공사·건설과 도자기 갈래를 새로 열었어요. 25판도 열 갈래에 하나씩 열 개를 더했고, 농사·밭과 화산 갈래를 새로 열었어요. 26판도 열 갈래에 하나씩 열 개를 더했고, 얼음·극지와 미생물 갈래를 새로 열었어요. 27판도 열 갈래에 하나씩 열 개를 더했고, 철도와 필름·영화 갈래를 새로 열었어요. 28판은 주제 표가 수학 쪽으로 바뀐 첫 판이라 이름 있는 수학 대상 열 개를 열 갈래에 하나씩 더했고, 수론 갈래를 새로 열었어요. 29판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 첫 판이라, 이름 있는 수학 대상 여섯과 화장품 매장 벽에 틀 질감 넷을 더했고, 위상·매듭과 화장품 팝업 갈래를 새로 열었어요. 30판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 매장의 LED 벽에 틀 질감 넷을 화장품 팝업 갈래에 더했어요. 31판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 열지 않았어요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽의 질감 넷을 화장품 팝업 갈래에 더했어요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 41판은 주제 표가 수학·추상 다섯과 할로윈 다섯으로 바뀐 첫 판이라, 수학 대상 다섯을 다섯 갈래에 하나씩 더하고 할로윈 갈래를 새로 열어 10월 31일에 틀 패턴 다섯을 넣었어요. 42판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 41판이 연 할로윈 갈래에 더했어요. 43판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 44판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 45판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 열 개 아래인 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 46판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 47판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 48판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 49판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 50판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 51판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 52판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 53판은 할로윈 몫이 쉬게 된 첫 판이라, 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 54판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 55판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 56판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 57판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 58판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 59판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 60판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 61판도 수학 대상 열 개를 패턴이 적은 갈래부터 열 갈래에 하나씩 더했어요. 62판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 63판은 64판과 같이 주제 표에 「타이포」가 들어온 첫 판이라, 글자가 들어간 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 64판은 주제 표가 바뀌어 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 네 갈래에 더했어요. 65판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 66판도 같은 표로 돌아서 글자 패턴 넷을 글자 갈래에, 하나를 데이터 그림 갈래에 더하고, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 68판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. [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판에서 세로로 다시 짰어요. @@ -900,6 +900,21 @@ | [Hodograph](patterns/hodograph.js) | 물리·스프링 | 해 하나를 이심률이 다른 천체 셋이 케플러의 박자로 돌며 같은 시간 부채를 쓸고, 아래에서는 한 원점에서 그은 속도 화살의 끝이 원점에서 반지름의 e 배만큼 비켜 선 원을 따라 돌아요. | | [Shapley-Folkman](patterns/shapley-folkman.js) | 기하·대칭 | 모양 하나를 1, 2, 4, 8 개 더해 그 수로 나눈 민코프스키 평균이 띠 넷으로 쌓이고, 오목한 만이 띠마다 절반씩 메워지며 흐린 볼록 껍질에 다가가요. | +### 68판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Ogham](patterns/ogham.js) | 글자 | 선돌 모서리를 줄기 삼아 오검 글이 아래에서 위로 한 획씩 새겨지고, 옆의 라틴 글자가 획 수를 세어요. | +| [Composing Stick](patterns/composing-stick.js) | 글자 | 활자 상자에서 한 개씩 날아온 활자가 조판 막대에 거꾸로 서서 스페이스로 줄을 맞추고, 판에서는 거울 글자로 오른쪽에서 왼쪽으로 읽히다가 아래 종이에 찍혀서야 바로 읽혀요. | +| [Palimpsest](patterns/palimpsest.js) | 글자 | 긁어낸 아르키메데스의 글 위에 직각으로 쓴 그리스어 기도문을 비추는 빛의 띠를 노브로 바꾸면, 평범한 빛에서 거의 사라진 옛 글이 자외선에서 어둡게 살아나고 의사색에서 붉게 갈라져요. | +| [Huffman Tree](patterns/huffman-tree.js) | 글자 | 글의 기호를 세어 가장 가벼운 둘씩 묶은 나무가 꼭대기 뿌리까지 자라고, 그 갈래를 따라 글이 0과 1로 흘러 고정 길이 부호보다 짧은 비트 줄이 돼요. | +| [Structure ASCII](patterns/structure-ascii.js) | 글자 | 큰 A, S, &, 글 이 안쪽 테두리만 어둡게 켠 채 흔들리다 다음 글자로 녹아들고, 외곽선이 지나는 5×8 칸마다 그 조각에 획이 가장 가까운 아스키 문자를 골라 빗금과 밑줄이 이어진 윤곽을 그려요. | +| [Delaunay Flips](patterns/delaunay-flips.js) | 보로노이·셀 | 한 모서리에서 편 부채꼴 삼각분할을 로슨의 뒤집기가 외접원 안에 든 점을 하나씩 찾아 풀고, 삼각형마다 가장 작은 각에 따라 자홍 바늘에서 청록 정삼각형으로 바뀌다가 들로네가 되면 빈 외접원 71 개가 한꺼번에 떠요. | +| [Fibonacci Word](patterns/fibonacci-word.js) | 프랙탈 | 피보나치 단어 0100101001001… 을 한 자씩 읽는 펜이 0 마다 직각으로 꺾어, 희미한 띠에 놓인 굵은 곡선 위에 정사각형 속 정사각형 곡선을 그리고, 첫 토막으로 1+√2 배 들어가면 그 토막이 다시 전체가 돼요. | +| [Rose Criterion](patterns/rose-criterion.js) | 노이즈·흐름 | 광자를 하나씩 쌓아 원판 50개가 대비 × 지름 차례로 낟알 위로 떠오르고, 금빛 쌍곡선이 k = 5 경계를 그어요. | +| [Josephus Problem](patterns/josephus.js) | 리듬·펄스 | 램프 n 개의 원을 손이 돌며 k 번째 박마다 하나씩 끄고, 아래 64줄 표에서 살아남는 자리가 톱니로 내려가요. | +| [Parallel Transport](patterns/parallel-transport.js) | 공간·원근 | 도는 구 위에서 금빛 화살을 돌리지 않고 고리를 따라 옮기면, 한 바퀴 뒤 흰 처음 화살 옆에 고리가 두른 넓이만큼 돌아서 서고, 아래 다이얼이 그 넓이를 내내 더해 가요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -3582,6 +3597,70 @@ Lab 에서 해 보기: Text 레이어에 「KS」를 Silkscreen 으로 카운터 주제 5 「수학 · 추상」으로, 볼록하지 않은 모양을 여러 번 더해 평균하면 볼록해진다는 것을 기하 갈래에 세웠어요. 섀플리-포크먼 보조정리는 로이드 섀플리와 존 포크먼이 증명했고, 로스 스타가 1969년 『Econometrica』 37권 1호에 낸 「Quasi-equilibria in markets with non-convex preferences」의 부록에 처음 실렸어요. 평면에서 n 개 집합의 합의 볼록 껍질에 든 점은 집합마다 점 하나씩 n 개의 합으로 쓸 수 있고, 그 가운데 집합 밖 껍질에서 빌려 오는 점은 많아야 둘이에요. 그래서 모양 S 를 n 번 더해 n 으로 나눈 평균 A(n) 은 껍질의 어느 점에서도 2D/n 안에 와요. D 는 n=1 일 때 껍질이 S 에서 가장 멀리 떨어진 거리예요. 패널은 위에서부터 32줄짜리 띠 넷에 A(1), A(2), A(4), A(8) 을 같은 크기로 그려요. 모양은 59×28 격자에서 LED 하나가 격자점 하나이고, 합은 격자점 집합을 비트맵으로 정확히 더해요. 8 번 더하면 465×217 격자가 돼요. S 를 A 의 모든 점으로 옮겨 OR 하는 대신 A 의 가장자리 점으로만 옮기고 S 의 조각마다 A 를 한 벌 더하는데, 모든 점으로 옮겨 OR 한 결과와 노브 자리 9 곳에서 40 프레임씩 2,520 번 맞춰 봐서 한 비트도 다르지 않았어요. LED 밝기는 그 칸에 든 A(n) 격자점의 몫이에요. 한 점에 닿는 길의 수로 밝히면 중심 극한 정리의 종 모양이 되니, 그건 이 보조정리와 다른 이야기라서 쓰지 않았어요. 첫 판은 띠마다 한 색이고 이번 띠에서 새로 찬 곳만 옅게 칠해서 띠 넷이 따로 노는 그림 넷으로 읽혔고, 한 프레임이 2.9 ms 였어요. A(1)⊂A(2)⊂A(4)⊂A(8) 로 포개지니 점마다 처음 나타난 띠의 색을 주자, 맨 아래 띠가 청록 달과 하늘색, 남색, 보라 테를 나이테처럼 다 품게 됐어요. S 의 행을 0~31 비트 옮긴 꼴로 미리 두고 껍질 변을 미리 셈해서 2.1 ms 가 됐어요. Hull 노브의 금빛 메움은 0.5 에서 갈색으로 보여서 빗금으로 바꿨어요. 달을 위상 12 곳에서 재면 껍질 가운데 덮이지 않은 몫이 n=1, 2, 4, 8 에서 42.6, 19.6, 8.6, 3.7% 이고, 하우스도르프 거리는 11.7, 5.7, 2.8, 1.37 LED 예요. n 을 곱하면 11.7, 11.5, 11.4, 10.9 로 거의 그대로라 거리는 1/n 으로 줄어요. 넓이는 n 을 곱한 값이 0.43 에서 0.30 으로 내려가 1/n 보다 조금 빨리 줄어요. 별은 n=1 부터 16 까지 n 을 곱한 거리가 5.78 로 똑같아요. 모양 다섯을 위상 12 곳에서 n=1 부터 16 까지 잰 960 경우가 모두 2D/n 안에 들었어요. 잰 값은 움직임 0.214, 촘촘함 0.051, 가장자리 0.502, 복잡도 0.359, 채움 0.429, 색 0.347, 모양 바뀜 0.163 이고, 노브는 0.18, 0.15, 0.20, 0.07 이에요. 새로 본 것은 1/n 이 흐려지는 모습이 아니라 홈의 수로 보인다는 거예요. 달의 현을 따라 A(n) 의 빈 자리가 n 개의 홈으로 갈라지고(위상 여섯 곳 가운데 넷에서 1, 2, 4, 8 개 그대로, 나머지 둘에서는 하나가 더 잡혔어요), 가장 깊은 홈이 1/n 로 얕아져요. +## 68판 + +68판은 67판(#330)과 같이 돌았어요. 앞 바퀴 PR 68개에는 댓글도 리뷰도 하나 없어서 따를 글이 없었어요. 67판이 PR 본문에 먼저 밝힌 열 개와 지도 칸 둘을 읽고 겹치지 않게 골랐어요. 67판의 Braille Relief 가 낮은 해로 돋은 점을 비추는 그림이라, Ogham 은 비스듬한 빛 대신 모서리를 따라 오르는 글줄을 주인공으로 두었어요. 타이포 다섯은 「타이포 실험」의 출발점 다섯을 하나씩 맡았어요. Ogham 은 세로 패널다운 글, Composing Stick 은 글자가 만들어지는 과정, Palimpsest 는 글자와 빛, Huffman Tree 는 숫자와 기호 놀이, Structure ASCII 는 글자로 짠 무늬예요. 글자 꼴은 셋이 새로 그렸어요. Composing Stick 은 어센더와 디센더 자리를 둔 8줄 글꼴을, Palimpsest 는 그리스 소문자 25자를, Structure ASCII 는 칸 가장자리까지 긋는 펜 획 28자를 지었어요. Ogham 과 Huffman Tree 는 18판 Typewriter 의 3×5 대문자를 옮겨 썼고, Huffman Tree 는 두벌식 자모 열다섯을 5×5 로 더 그렸어요. 수학 다섯은 568개에 없던 대상이고 「낯선 수학 예」에서는 하나도 고르지 않았어요. 67판의 다섯을 넣어도 패턴이 스물로 가장 적은 보로노이·셀, 프랙탈, 노이즈·흐름, 리듬·펄스, 공간·원근에 하나씩 넣었어요. 주제 1 은 셋뿐인 칸 일곱 가운데 67판이 고른 둘을 빼고 복잡도 × 채움 4,2 에 Rose Criterion 을, 가장자리 × 촘촘함 1,2 에 Palimpsest 를 세웠어요. 주제 4 는 Ogham 과 Josephus Problem 이 맡았고, Huffman Tree 도 잎이 바닥에, 뿌리가 꼭대기에 서요. 열 개를 따로 만든 뒤 합쳐 세어 보니 Fibonacci Word 의 채움 0.077 과 Structure ASCII 의 0.091 이 Dragon Fold 의 0.117 아래로 채움 축의 아래 끝을 늘였어요. 그러자 칸 경계가 0.302 에서 0.271 로 내려가, 66판의 Wilkinson's Polynomial 이 겨눠 넷이 된 복잡도 × 채움 4,1 이 하나로, 채움 × 색 1,4 와 채움 × 모양 바뀜 1,4 가 넷에서 둘로 줄었어요. 그래서 두 패턴을 축 안으로 당겼어요. + +### Ogham + +주제 7 「타이포」의 「세로 패널다운 글」과 주제 4 「세로 64×128 에서 잘 읽히는 것」을 같이 겨눴어요. 오검은 선돌의 모서리를 줄기(druim)로 삼아 아래에서 위로 읽는 글이라 세운 패널에 그대로 맞아요. 글자 스물은 다섯씩 네 아이크메(aicme)로 나뉘고 한 글자는 획 1개에서 5개예요. 위로 읽을 때 B 아이크메(B L F S N)는 줄기 오른쪽, H 아이크메(H D T C Q)는 왼쪽에 새기고, M 아이크메(M G NG Z R)는 줄기를 비스듬히 가로지르며, 모음(A O U E I)은 줄기 위의 눈금이에요. 글자 차례는 조지 콜더(George Calder)가 1917년에 낸 『Auraicept na n-Éces』에 실린 Book of Ballymote 의 오검 소책(Ogam tract)대로예요. 고대 아일랜드어 글자 이름과 비문의 MAQI(아들), MUCOI(겨레)는 데이미언 맥매너스(Damian McManus)의 『A Guide to Ogam』(1991)를 따랐어요. 읽기를 확신할 수 없어서 실제 비문은 넣지 않았고, 사람 이름도 지어내지 않았어요. 돌은 글마다 하나씩이고 대각선 반이 22.5~25.5 LED 인 사각 기둥을 모서리 쪽에서 봐요. 왼쪽 면이 빛을 받아 밝고 오른쪽 면은 어두워요. 모서리는 빛을 받은 한 줄로 128줄을 다 올라가요. 획 하나는 LED 두 줄의 홈이에요. 빛이 위에서 오니 위를 보는 아래 벽은 밝고, 아래를 보는 위 벽은 그 0.4 밝기로 그늘져요. 여는 자리에서 B 획은 오른쪽 면으로 8.5 LED, H 획은 왼쪽 면으로 13.6 LED 를 가요. 한 글자 안의 획은 4줄 간격이고 글자 사이는 10줄을 띄워요. 끌 끝에는 흰 불꽃이 튀고 돌가루가 떨어지며, 갓 판 획은 하얗다가 1초 남짓에 제 색으로 식어요. 왼쪽 7열에는 18판 Typewriter 의 3×5 대문자로 글자마다 지금까지 판 획이 뜻하는 글자를 적어요. 그래서 오른쪽 다섯 획 N 을 팔 때 B, L, F, S, N 이 차례로 바뀌어요. 새 돌은 아래 100줄을 1초에 25획씩 0.9초 만에 새겨서 글이 곧바로 패널 높이를 다 채워요. 그다음부터는 끌이 노브의 빠르기로 오르고, 화면은 끌을 위에서 10~16째 줄에 두고 따라 올라가요. 다 새기면 돌 꼭대기를 3초 보여 주고, 0.7초에 어두워진 뒤 새 돌에 다시 새겨요. Hue 는 획과 돌의 빛을 돌려서 0 에서는 청회색 점판암에 연한 금빛, 0.5 에서는 갈색 사암에 연한 파랑이에요. Speed 는 1초에 획 0.8개에서 13.6개까지예요. 여는 값 2 에서는 5개라 알파벳 60획이 12.9초에 끝나요. Text 는 네 글이에요. 0 은 아이크메 차례의 알파벳으로 돌 380줄을 쓰고, 1 은 네 아이크메 이름 BEITHE HUATHA MUINE AILME 로 59획 가운데 40이 모음 눈금이에요. 2 는 ANM MAQI MUCOI 34획이고, 3 은 첫 아이크메 이름 BEITHE LUIS FERN SAIL NIN 76획이에요. Turn 은 돌을 세운 축으로 돌려요. 양 끝에서 40도이고 여는 0.06 에서는 13도예요. 한 면이 옆으로 돌아갈수록 그 면의 획이 짧아지고, M 획은 모서리에서 꺾여 두 면에 걸친 것이 보여요. 잰 값은 움직임 1.164, 촘촘함 0.042, 가장자리 0.280, 복잡도 1.240, 채움 0.676, 색 0.080, 모양 바뀜 0.044, thirds 67/67/67 이고 노브는 0.06, 0.06, 0.06, 0.10 이에요. 돌 면과 하늘을 검게 두고 글만 재면 thirds 가 7.0/7.7/7.1 이에요. 앞 판은 thirds 66/65/67 이 거의 돌의 몫이었고 글만으로는 1.4/3.5/2.9 였어요. 끌이 돌 밑동에서 시작하고 화면이 끌을 위에서 36째 줄에 두어서, 재는 2~8초 가운데 5초 넘게 위 3분의 1 이 아직 새기지 않은 돌이었어요. 홈을 LED 두 줄로 파고 획을 12.5 칸에서 16 칸으로 늘렸어요. 새 돌의 첫 100줄을 몰아 새기고 끌을 위쪽에 두자 글만으로 세 칸이 다 5 를 넘었어요. 획 간격이 3줄에서 4줄로 늘어 같은 빠르기에도 화면이 1초에 24줄쯤 오르니, 움직임은 0.534 에서 1.164 가 됐어요. 그 전에는 눈높이를 패널 가운데에 두고 거리 150 의 원근을 걸었는데, 눈보다 45줄 아래의 첫 B 획이 9 LED 를 가며 3.6줄 솟아서 M 획처럼 비스듬해 보였어요. 원근을 덜어도 화면이 소수 줄씩 오르면 1 LED 굵기의 획이 두 줄에 반씩 걸쳐 흐려져서, 원근을 빼고 화면을 한 줄씩만 올렸어요. 여는 자리도 처음에는 2.4도라 M 획이 곧게만 보였어요. Turn 에 0.18(1 − Turn²) 라디안을 더해 13도로 열자 왼쪽 면이 0.85, 오른쪽 면이 0.53 으로 보이고 M 획이 모서리에서 꺾여요. + +Lab 에서 해 보기: Text 레이어에 ᚛ᚋᚐᚊᚔ᚜ 를 쓰고 Noto Sans Ogham 같은 오검 .ttf 를 파일 글꼴로 올려요. sharp 를 켜고 size 12, y 8 에 두면 Text 2 의 돌이 꼭대기를 보여 줄 때 하늘 자리에 원고처럼 왼쪽에서 오른쪽으로 읽는 가로 MAQI 가 떠요. 그 아래 돌에는 아래에서 위로 읽는 세로 MAQI 가 있어서 둘을 견줄 수 있어요. + +기능 감: Text 레이어를 90도 돌리거나 세로로 쓰는 길이요. 유니코드 오검(U+1680 블록)은 가로로 왼쪽에서 오른쪽으로 쓰지만 돌에서는 모서리를 따라 아래에서 위로 읽어요. 돌과 같은 방향으로 ᚛ᚋᚐᚊᚔ᚜ 를 세우려면 글줄을 반시계로 90도 돌려야 해요. 지금 Text 레이어는 가로 글줄만 짜서, 줄바꿈으로 한 자씩 쌓으면 글자마다 줄기가 가로로 끊겨 이어진 세로 줄기가 생기지 않아요. + +### Composing Stick + +주제 7 「타이포」의 「글자가 만들어지는 과정」을 손 조판으로 겨눴어요. 같은 갈래의 Typewriter 는 활자대가 종이를 치고 Etaoin Shrdlu 는 라이노타이프 자판의 빈도를 세는데, 이 패턴의 주인공은 활자 한 개(sort)가 거울 글자라는 것과 줄을 스페이스로 맞추는 일이에요. 막대에 활자를 세우고 판을 짜서 찍는 차례는 조지프 목슨(Joseph Moxon)이 『Mechanick Exercises on the Whole Art of Printing』(1683~84)에 적은 손 조판을 따랐어요. 패널 위 31줄은 캘리포니아 잡 케이스(California job case)를 줄인 활자 상자예요. 소문자 칸은 쓰이는 횟수만큼 크고 e 칸이 가장 크며, 대문자 칸은 크기가 다 같고 J 와 U 가 Z 뒤에 온다는 데까지는 확인했어요. 칸 하나하나의 자리는 확인하지 못해서 소문자 칸의 배치는 이 패턴이 정한 것이에요. e 칸이 안쪽 9×7 = 63 LED 로 가장 크고 a, t, r 과 3-em 스페이스 칸이 6×8 = 48 LED, j 와 z 가 2×7 = 14 LED 예요. 그 아래 33~47줄이 조판 막대, 48~52줄이 잉크판, 53~86줄이 판(forme), 90~127줄이 종이예요. 글자는 이 패턴을 위해 그린 8줄 픽셀 글꼴이고, 위 2줄은 어센더, 아래 2줄은 디센더 자리예요. 활자 한 개는 글자 폭에 몸 1 LED 를 더한 폭이고, 몸이 10 LED 라서 em 쿼드가 10 LED, en 이 5, 3-em 이 3, thin 이 2, hair 가 1 LED 예요. 활자는 상자에서 0.3초마다 한 개씩 막대로 날아가요. 막대는 식자공 쪽에서 본 모습이라 글줄이 왼쪽에서 오른쪽으로 읽히고 글자마다 위아래가 뒤집혀서 p 가 b 로, d 가 q 로 보이고, 몸 아래로는 홈(nick)이 한 줄로 이어져요. 줄이 차면 3-em 낱말 사이에 em, en, thin, hair 스페이스를 하나씩 날려 와 knee 에 꽉 맞추고, 맞춘 줄은 뒤집히면서 판으로 내려가요. 판은 활자 얼굴 쪽에서 본 모습이라 글줄이 오른쪽에서 왼쪽으로 읽히고 p 가 q 로, b 가 d 로 보여요. 롤러가 판을 지나가며 잉크를 묻히고 새 종이 위로 실린더가 지나가면 그제야 글이 바로 읽혀요. 다 찍으면 활자가 제 칸으로 날아 돌아가는데, mind your p's and q's 를 낱말 사이로 맞추면 글자 17개와 스페이스 7개예요. Speed 2 에서 이 한 바퀴가 11초, 세 줄인 Sphinx 는 18초예요. Hue 는 잉크 색을 돌려서 0 이 파랑이고 0.5 가 주황이에요. Text 는 글 넷을 고르는데 글마다 measure 가 달라서 knee 가 52, 60, 60, 56 LED 로 옮겨 가요. 2번 글 bob pop bid bud dub 은 막대에서 bob 과 pop 이 서로 바뀌고, 판에서는 bid 와 bud 와 dub 이 제 모습으로 읽혀요. 3번은 벤저민 프랭클린이 1728년에 스스로 쓴 묘비명의 첫머리 The Body of B. Franklin, Printer 예요. Spacing 은 줄을 채우는 법이에요. 0.25 아래는 낱말 사이로 맞추고 0.5 까지는 글자 사이마다 같은 스페이스를 넣어 자간을 벌려요. 0.75 까지는 줄 끝을 쿼드로 채워 흘리고, 그 위는 양 끝에 쿼드를 나눠 넣어 가운데에 둬요. 마지막 줄은 늘 쿼드로 채우고 hair 스페이스는 놋쇠빛으로 보여요. 첫 판은 노브가 0.03, 0.10, 0.06, 0.03 이었고 프레임마다 3.5 ms 가 걸렸어요. 모든 글을 60 LED 에 짰더니 mind your p's 가 56 LED 를 차지해서 남는 자리가 4 LED 뿐이었고, Spacing 의 네 방법이 그 4 LED 안에서만 달랐어요. 글마다 measure 를 주고 0번을 52 LED 로 좁히자 mind your 와 p's and q's 로 갈리면서 첫 줄에 11 LED 가 남았고, Spacing 이 0.040 이 됐어요. Hue 는 잉크가 글자 픽셀에만 있어서 낮았는데, 롤러 아래에 5줄짜리 잉크판을 깔자 0.040 이 됐어요. 픽셀마다 색 배열을 새로 만들던 것을 걷어 내자 1.5 ms 가 됐어요. 잰 값은 움직임 0.119, 촘촘함 0.096, 가장자리 0.450, 복잡도 0.666, 채움 0.825, 색 0.211, 모양 바뀜 0.188, thirds 74/83/89 이고, 노브는 0.040, 0.100, 0.060, 0.040 이에요. 새로 본 것은 막대의 글자가 거울 글자가 아니라는 거예요. 활자 얼굴은 좌우가 뒤집힌 글자인데 막대에는 그 활자를 위아래로 돌려 세우니, 둘을 겹치면 좌우는 제자리로 오고 위아래만 뒤집혀요. 그래서 p 와 q 가 서로 바뀌어 보이는 곳은 얼굴 쪽에서 본 판뿐이고, 막대에서 서로 바뀌는 짝은 p 와 b 예요. 이 글꼴에서 p 를 위아래로 뒤집으면 b 와, 좌우로 뒤집으면 q 와 픽셀까지 같아요. 막대에서 판으로 내려가는 줄은 위아래로 한 번, 좌우로 한 번 뒤집히는데, 그 사이 한순간은 종이에 찍힐 모습 그대로 읽혀요. + +Lab 에서 해 보기: Text 레이어를 paint 로 두고 Newsreader 로 p q b d 를 size 12, sharp 를 끄고 색은 #1d4fd8 로 x 32 가운데 정렬, y 120 에 놓아요. Text 노브 0 과 2 에서는 두 줄만 찍혀 종이 아래 16줄이 비는데, 거기에 세리프 글꼴의 네 글자가 들어가서 이 패턴의 8줄 픽셀 글자와 나란히 놓여요. + +기능 감: Text 레이어에 좌우 뒤집기와 180도 돌리기가 있으면 좋겠어요. 이 패턴은 같은 글줄을 막대에서는 위아래가 뒤집힌 채로, 판에서는 거울 글자로, 종이에서는 바로 보여 주는데, Lab 의 Newsreader 나 Inter 로 쓴 p 를 돌리면 b 가 되는지 보려 해도 Lab 안에서는 뒤집을 길이 없었어요. 그래서 이 패턴 글꼴의 비트맵만 스크래치 스크립트로 뒤집어 확인했어요. + +### Palimpsest + +주제 7 「타이포」의 「글자와 빛」이면서, 주제 1 로 셋뿐이던 가장자리 × 촘촘함 1,2 칸을 겨눈 패턴이에요. 아르키메데스 팔림프세스트는 10세기에 옮겨 쓴 아르키메데스의 글을 1229년에 긁어내고 그 양피지에 기도서를 다시 쓴 책이에요. 옛 낱장을 돌려 반으로 접어 새 책을 엮어서 두 글이 직각으로 엇갈려요. 2000년대에 Roger Easton 과 Keith Knox 를 비롯한 영상 팀이 여러 파장의 빛으로 이 책을 찍었고, Uwe Bergmann 은 X선 형광으로 잉크의 철을 찍었어요. 패널에는 기도문이 10 LED 간격으로 12줄 가로로 놓이고, 아르키메데스의 글은 시계 방향으로 돌아 12 LED 간격으로 5줄이 124 LED 씩 내려가요. 글자는 이 패턴에 그린 그리스 소문자 25자이고, 폭은 5 LED 이하, 위아래로 뻗는 획까지 9줄이에요. 낱말은 줄 끝에서 끊겨 다음 줄로 이어지고, Page 노브 0 에서는 트리사기온 기도 123자가 『방법』의 제목 106자를 덮어요. Band 노브 0 은 평범한 빛이에요. 상아색 양피지 (202,185,150) 위에 갈색 기도문 (106,82,51) 이 또렷하고, 옛 글은 바탕보다 빨강에서 8%, 파랑에서 25% 어두운 황토빛 자국이에요. 0.5 는 자외선이에요. 양피지가 (100,131,218) 로 푸르게 빛나고 두 잉크의 철이 그 빛을 꺼서, 기도문은 바탕의 42% 로, 옛 글은 65% 로 어두워져요. 1 은 붉은 빛 사진을 빨강 채널에, 자외선 사진을 초록과 파랑 채널에 넣은 의사색이에요. 옛 글은 붉은 빛에서 밝고 자외선에서 어두워 (183,80,81) 로 붉게 뜨고, 둘 다에서 어두운 기도문은 (35,17,18) 로 거의 검어요. Speed 노브는 X선 빔이 내려가는 빠르기예요. 2 에서 초당 26줄을 내려가 6.5 초마다 한 번 지나가고, 빔 뒤 16줄쯤은 철 지도라서 긁힌 옛 글도 기도문만큼 옅은 파랑으로 떠요. Page 노브는 기도문과 아르키메데스의 글 넷을 짝지어 고르고, Ink 노브는 긁고 남은 옛 잉크의 양과 두 잉크가 번진 폭이에요. -1 에서 기도문 획은 0.5 LED 만 번지고, 1 에서는 0.8 LED 까지 번져 옛 글이 평범한 빛에서도 빨강 21%, 파랑 59% 어두운 황토색으로 보여요. 잰 값은 움직임 0.138, 촘촘함 0.088, 가장자리 0.178, 복잡도 2.674, 채움 1.000, 색 0.154, 모양 바뀜 0.059 이고, 노브는 0.21, 0.09, 0.09, 0.09 예요. 촘촘함이 0.082 를 넘고 가장자리가 0.210 아래라 1,2 칸에 넷째로 섰어요. 첫 판은 낱말을 통째로 다음 줄로 넘겨 줄이 반쯤 비었고, 촘촘함 0.065 로 칸 아래에 섰어요. 낱말을 줄 끝에서 끊자 0.075 가 됐고, 그 뒤로는 잉크를 진하게 하든 결을 거칠게 하든 두 값이 한 줄 위를 같이 움직였어요. 촘촘함 0.079 에 가장자리 0.097 이던 것이 촘촘함 0.095 에서는 가장자리 0.223 이었어요. 1 LED 획은 바탕에서 심까지 두 계단에 내려가서, 잉크가 밝기를 80 넘게 빼면 두 계단이 다 40 을 넘어요. 결 없이 글자만 켜면 촘촘함 0.068 에 가장자리 0.198 로 칸 경계에 걸렸어요. 여기서 쓴 것은 이 연구가 LED 의 밝기를 가장 밝은 채널로 잰다는 점이에요. 기도문 잉크는 빨강을 62%, 파랑을 84% 먹어서 눈에는 진한 갈색인데 재는 밝기로는 계단이 낮아요. 결은 흰 잡음 대신 이웃 평균을 두 번 빼고 셋으로 나눈 결이라, 이웃의 79.6% 가 서로 다르면서 결만의 계단은 36 을 넘지 않아요. 이 결을 바탕에 깔자 가장자리가 0.178 로 내려가고 촘촘함은 0.088 로 올랐어요. 같은 결을 획 가장자리에도 얹으면 40 언저리 계단이 넘쳐 가장자리 0.226 으로 칸을 벗어나서, 획 가장자리에서는 결을 껐어요. + +Lab 에서 해 보기: Text 레이어에 Newsreader 로 EUREKA 를 size 22, 색 #5a3b22 로 가운데 두고 blend 를 multiply, opacity 0.35 로 하면, 세 번째 손이 쓴 듯한 갈색 글줄이 기도문 위에 얹혀요. Band 를 자외선으로 돌려도 이 글줄만은 빛을 따라 바뀌지 않아서, 양피지 안의 두 글과 위에 얹은 글이 바로 갈려요. + +기능 감: Text 레이어에 글줄 방향이 있으면 좋겠어요. TextSpec 에는 x, y, align 만 있어서 글이 늘 가로로 누워요. 이 패턴의 밑글은 시계 방향으로 돌아 세로로 내려가야 해서 글자 칸의 (열, 행) 을 코드에서 (8 - 행, 열) 로 바꿔 찍었는데, Text 레이어를 90도 돌릴 수 있으면 Lab 에서 친 두 낱말을 직각으로 겹쳐 팔림프세스트를 바로 만들어 볼 수 있어요. + +### Huffman Tree + +주제 7 의 「숫자와 기호 놀이」와 주제 4 를 겨눴어요. 데이비드 허프먼(David A. Huffman)이 1952년에 낸 최소 중복 부호는 글의 기호를 센 뒤 가장 가벼운 두 마디를 무게를 더한 부모 하나로 묶고, 뿌리 하나가 남을 때까지 그 일을 되풀이해요. 66판 Etaoin Shrdlu 는 글자를 세는 데서 멈췄고, 이 패턴은 센 다음의 묶음과 부호가 주인공이에요. 패널 맨 아래에 기호가 18판 Typewriter 의 3×5 글자로 서고, 기호마다 점선 줄기가 센 수의 log2 에 비례하는 높이까지 올라가요. 그러면 뿌리에서 잎의 마디까지 내려온 높이가 log2(N/w), 곧 그 기호가 이상적으로 가질 부호 길이가 되고, 그 길에 걸린 가로대 수가 허프먼 부호의 실제 길이가 돼요. 디킨스 글에서 11번 나오는 공백은 이상적인 길이 2.18비트 자리에서 부호 01 을 얻고, 한 번씩 나오는 B 와 R 은 5.64비트 자리에서 5비트를 얻어요. 묶을 때마다 먼저 뽑힌 가벼운 쪽은 왼쪽의 하늘색 0, 다른 쪽은 오른쪽의 호박색 1 이 되고, 가로대 밑에는 그 두 색으로 갈린 흐린 빛이 걸려요. 같은 순간 그 아래 잎들의 부호 칸 맨 위에 비트가 하나씩 얹혀서, 일찍 묶이는 드문 기호의 부호는 라벨 위로 길게 자라고 늦게 묶이는 잦은 기호의 부호는 짧게 남아요. 맨 위 줄에는 지금 묶는 무게가 2+2=4 꼴로 뜨고, 그 밑의 허프먼 띠는 무게 w 인 묶음마다 빈 칸 w 개를 얻어요. 묶음 무게를 다 더한 값이 부호 전체의 비트 수와 같아서, 나무가 다 서는 순간 빈 칸이 꼭 그 길이에서 멈춰요. 그다음 글이 한 기호씩 뿌리에서 잎까지 흰 길을 그리며 그 칸을 채우고, 그 밑의 장밋빛 띠는 같은 글을 고정 길이 부호로 채워요. 끝에는 169<200 처럼 두 합계가 3초 서 있어요. Text 노브는 글 넷을 골라요. 디킨스의 『두 도시 이야기』(1859) 첫 구절 50자는 13기호로 169비트 대 200비트이고, 글자마다 한 번씩 나오는 팬그램 Mr Jock, TV quiz PhD, bags few lynx 는 27기호로 148 대 165 예요. 원주율의 처음 100자리는 10기호로 332 대 BCD 400 이고, 윤동주 「서시」(1941) 첫 두 줄을 두벌식 자판의 키로 쪼갠 58자는 16기호로 223 대 232 예요. 자모 열다섯 자와 공백은 5×5 로 직접 그렸어요. 팬그램은 26글자가 다 무게 1이라 1+1=2 가 열세 번 한 높이에서 일어나 고른 빗이 서고, 무게 7인 공백만 00 으로 뿌리 곁에 붙어요. 자모는 기호가 꼭 16개라 4비트 고정 부호에 빈 자리가 없어서 9비트만 줄고, 원주율은 열 숫자 가운데 여섯이 3비트를 얻어 68비트가 줄어요. Tie 노브는 무게가 같은 두 마디 가운데 무엇을 먼저 뽑을지 정해요. 0 은 오래된 쪽을, 1 은 새 쪽을 먼저 뽑아요. 2 에서는 나무를 다시 지을 때마다 동전을 새로 던져요. 디킨스에서 가장 긴 부호가 0 에서는 5비트, 1 에서는 6비트로 나무 모양이 바뀌어도 합계는 어느 규칙에서나 169비트예요. 네 글과 세 규칙 모두 스크립트로 어느 부호도 다른 부호의 앞머리가 아닌 것을 확인했어요. 합계는 묶음 무게의 합과 같고, 크래프트 부등식을 지키는 길이 배정 가운데 가장 작은 합을 동적 계획법으로 구한 값과도 같았어요. Hue 는 0 의 하늘색과 1 의 호박색을 함께 돌리고, Speed 2 에서 가장 가벼운 묶음은 0.22초, 뿌리는 0.48초가 걸려 디킨스 한 바퀴가 15.7초예요. 첫 판은 가는 선만 그려서 Hue 반응이 표에 0.01 로 찍혔어요. 노브를 재는 2~3초에는 아래쪽 잔가지 몇 개만 서 있어서 색이 바뀌는 LED 가 적었어요. 가로대 밑에 흐린 빛을 걸고, 묶음 시간을 무게에 따라 0.7~1.5배로 바꿔 잔가지를 빨리 지나가게 하자 0.035 가 됐어요. 잰 값은 움직임 0.076, 촘촘함 0.089, 가장자리 0.817, 복잡도 0.212, 채움 0.271, 색 0.160, 모양 바뀜 0.074 이고, thirds 는 35/30/14, 노브는 0.04, 0.05, 0.08, 0.04 예요. 이 패널에서 새로 본 것은 높이를 log2(w) 로 두기만 해도 나무가 위로만 자란다는 거예요. 허프먼이 묶는 무게는 줄지 않아서 새 부모는 늘 앞 부모와 같거나 더 높은 줄에 서요. 가로대가 겹칠까 봐 부모를 자식보다 적어도 두 줄 위에 두는 틈을 넣었는데, 네 글과 두 규칙 어디서도 한 번도 걸리지 않았어요. 비슷한 무게끼리 묶이니 부모와 자식이 가장 가까운 자리도 자모의 1+2=3 에서 7줄 떨어져 있었어요. + +Lab 에서 해 보기: Text 레이어에 Silkscreen 으로 1952 를 5 LED 높이(size 8 언저리)로, 색 #505050 으로 x 44, y 23 에 두면 장밋빛 고정 띠와 뿌리 사이에 남은 줄에 허프먼이 부호를 낸 해가 서요. 네 글 모두 뿌리의 오른쪽 가로대가 x 42 안에서 끝나서 글이 나무에 닿지 않아요. + +기능 감: Lab 미리보기에서 노브 하나를 다섯 자리로 돌려, 새로 시작한 뒤 2~3초의 화면 차이를 재 주는 버튼이 있으면 좋겠어요. 첫 판 Hue 가 표에 0.01 로 찍힌 것을 study.ts 를 돌리고서야 알았고, 어느 줄에서 차이가 나는지 보려고 16줄 띠마다 나눠 재는 스크립트를 따로 썼어요. 글자 패턴은 획이 가늘어서 색 노브 반응이 늘 문턱 근처라, 그 자리에서 보는 값이 쓸모 있어요. + +### Structure ASCII + +주제 7 의 「글자로 짠 무늬」와 「기호가 이어져 그림이 되는 것」을 한 패턴에서 겨눴어요. 63판 Type Mosaic 은 놀턴과 하먼처럼 칸의 밝기에 글자의 잉크를 맞췄는데, Xuemiao Xu, Linling Zhang 과 Tien-Tsin Wong 은 「Structure-based ASCII Art」(SIGGRAPH 2010, ACM Transactions on Graphics 29권 4호)에서 밝기로 고른 아스키 아트는 망점 그림처럼 되고 글자 수가 많아야 읽힌다고 보고, 선 그림의 선을 글자의 모양으로 따랐어요. 논문은 칸과 글자가 어긋나도 견디는 모양 거리(AISS)를 새로 만들고 그림도 조금 비틀어 글자에 맞추지만, 이 패턴은 그림을 비틀지 않고 더 단순한 대칭 챔퍼 거리를 써요. 글자의 켜진 LED 마다 외곽선까지의 거리와, 외곽선이 지나는 LED 마다 글자의 가장 가까운 켜진 LED 까지의 거리를 3 LED 에서 잘라 더한 뒤 둘의 개수로 나눈 값이에요. 글자는 옆으로 2 LED(3×5 칸에서는 1), 위아래로 1 LED 까지 밀어 대 보고 1 LED 에 0.12 씩 더 셈해서, 칸 가장자리로 지나는 세로선도 | 를 받아요. 그려지는 자리는 언제나 제 칸이에요. 큰 그림은 A, S, &, 글 네 자예요. 중심선을 손으로 적고 중심선에서 7, 7, 6, 4.5 LED 떨어진 자리를 외곽선으로 삼아 0.9 배로 패널에 넣었고, 9초 주기로 좌우 0.12 라디안(약 7도)씩 흔들어요. 한 자가 4.5초 서 있다가 1.5초 동안 두 거리장을 섞으며 다음 자로 녹아들어서 넷이 24초에 한 바퀴예요. 거리장은 LED 모서리를 한 칸 걸러 33×65 점에서 재고 사이는 이웃한 점의 평균으로 채운 뒤, marching squares 로 LED 한 칸마다 외곽선 조각을 끊어요. 5×8 칸 192개 가운데 프레임마다 85칸쯤에 글자가 서고, 글자는 공백까지 28자예요. 글자마다 단위 칸 안의 펜 획으로 적어 획이 칸 가장자리까지 닿게 했고, 그래서 대각선으로 이웃한 칸의 / 둘이 모서리에서 만나 한 줄이 돼요. 외곽선 안쪽 4 LED 는 어두운 파랑(16, 19, 45)으로 켜서, 글자들이 한 모양의 가장자리에 서요. 색은 Hue 0 에서 직선 획이 청록(38, 255, 215), 곡선과 꺾임이 하늘색, 모서리와 교차가 초록, 점과 따옴표가 연보라예요. Hue 는 이 색을 다 같이 돌리고, Speed 는 2 에서 위의 빠르기로 움직여 0.2 에서 열 배 느려지고 3 에서 1.5 배 빨라져요. Set 은 0 에서 27자를 다 쓰고 1 에서 / \ | - _ . ' , ( ) < > 열두 자만, 2 에서 / \ | - 넷만 써요. 3 에서는 같은 칸을 모양이 아니라 외곽선이 칸을 얼마나 덮는지로 재서 . : - = + * # % @ 사다리에서 골라요. Cell 은 칸을 5×8(192칸)에서 4×6(336칸), 3×5(525칸), 7×11(99칸)로 바꿔요. 24초 한 바퀴를 세어 보니 5×8 에서 | 가 15.0%, - 가 12.2%, _ 가 12.0%, " 가 10.4% 였고, 고른 글자와 외곽선 사이 평균 거리는 27자에서 0.58 LED, 열두 자에서 0.63, 넷에서 0.94, 밝기 사다리에서 1.49 였어요. 잰 값은 움직임 0.128, 촘촘함 0.066, 가장자리 1.000, 복잡도 0.100, 채움 0.202, 색 0.199, 모양 바뀜 0.088 이고, 세 토막은 15/24/20, 프레임당 1.4 ms, 노브는 넷 다 0.05 예요. 첫 판은 흐린 마스크 거리로 골랐는데, 칸 옆 가장자리 가까이 지나는 세로선이 | 대신 [ 나 ] 를 받았어요. 5×8 칸에서 | 는 다섯 열 가운데 셋째 열에 서고 [ 의 기둥은 둘째 열이라 더 가까웠기 때문이에요. 챔퍼 거리로 바꾸고 2 LED 까지 밀어 대게 해도 [ 와 ] 가 1.8% 남아서, 위아래 짧은 가로획이 윤곽에 가시처럼 붙는 두 자를 뺐어요. 모든 칸을 프레임마다 다시 맞추던 판은 study 가 3.2 ms 를 쟀고, 외곽선이 지나는 LED 가 바뀐 칸만 다시 맞추게 하자 85칸 가운데 22칸쯤만 맞춰서 1.3 ms 가 됐어요. 그 판은 글자와 외곽선 한 줄만 켜서 채움이 0.091 이었는데, 이건 모든 패턴의 바닥이던 Dragon Fold 의 0.1168 보다 낮아서 지도의 채움 축을 늘리고 다른 판이 채운 칸을 비웠어요. 글자 안을 다 33/255 로 칠하니 채움이 0.341 로 넘치고, 칠한 가장자리가 무른 단차가 되어 가장자리 값이 0.837 로 떨어졌어요. 외곽선 안쪽 4 LED 만 45/255 로 켜자 채움이 0.202 가 됐고, 검정과의 단차가 40 을 넘어서 가장자리는 1.000 그대로예요. 따로 그리던 흐린 외곽선은 그 테두리의 바깥 끝이 대신해서 뺐어요. 뜻밖이었던 것은 " 예요. 아스키에는 윗줄 긋는 글자가 없어서 S 의 위 그릇처럼 외곽선이 칸 맨 위로 지나면 위쪽에 짧은 획 둘을 세운 " 가 가장 가까운 글자가 되고, 그게 열 자에 한 자꼴이에요. + +Lab 에서 해 보기: Text 레이어에 JetBrains Mono 로 / \ | _ ( ) < > 를 size 8 에 sharp 를 켜서 y 4 에 두면, Cell 0 에서 큰 글자가 24초 한 바퀴 동안 한 번도 켜지 않는 맨 위 여덟 줄(0~7)에 진짜 고정폭 글꼴의 같은 기호가 서요. 이 패턴이 칸 모서리까지 그은 5×8 글자와 나란히 놓고, 진짜 글꼴의 / 가 이웃 칸의 / 와 이어지는지 견줘 볼 수 있어요. + +기능 감: 코드 레이어가 Text 레이어의 덮임 비트맵을 입력으로 읽는 길이요. 이 패턴은 거리장의 0 등고선을 marching squares 로 따는데, 덮임 값의 반 높이 등고선도 같은 코드로 딸 수 있어요. 그 길이 있으면 A, S, &, 글 네 자를 중심선으로 손으로 적지 않고 Text 레이어에 친 아무 낱말이나 아스키로 다시 쓸 수 있고, 아스키 28자도 칸 크기마다 손으로 그은 획 대신 고정폭 글꼴의 비트맵으로 맞출 수 있어요. + +### Delaunay Flips + +주제 5 「수학 · 추상」으로, 셀 갈래에 들로네 삼각분할을 완성된 그물이 아니라 고쳐 가는 과정으로 세웠어요. 보리스 들로네(Boris Delaunay)가 1934년에 정의한 삼각분할은 어느 삼각형의 외접원 안에도 다른 점이 없는 분할이에요. 찰스 로슨(Charles L. Lawson)의 1977년 뒤집기는 삼각형 abc 와 dcb 가 나눠 쓰는 변 bc 에서 d 가 abc 의 외접원 안에 있으면, 곧 a 와 d 에서 그 변을 마주 보는 두 각의 합이 π 를 넘으면 변을 다른 대각선 ad 로 바꿔요. 그런 변을 아무 차례로 뒤집어도 끝은 들로네 삼각분할이에요. 패널은 세로로 긴 초타원 둘레에 점 13 개를 박고 안쪽에 30 개를 흩어 점 43 개, 삼각형 71 개로 시작해요. 첫 분할은 왼쪽 위 모서리에 가장 가까운 둘레 점에서 나머지 점을 모두 잇는 부채꼴이라, 71 개 가운데 46 개의 가장 작은 각이 10° 아래이고 가운데값이 4.9° 예요. 삼각형은 가장 작은 각으로 칠해서 10° 아래의 바늘은 밝은 자홍, 50° 를 넘어 정삼각형에 가까운 것은 어두운 청록이고, 그 사이는 보라에서 파랑으로 가요. 뒤집기는 마주 보는 두 각의 합이 가장 큰 변부터 해요. 그 변이 자홍으로 서고 네 번째 점을 품은 외접원이 뒤집기 직전까지 밝아진 뒤, 새 대각선이 흰빛으로 번쩍이고 옛 변과 원은 0.3 초 남짓에 꺼져요. 부채꼴은 처음에 내부 변 100 개 가운데 23 개만 어긋나 있는데, 뒤집을 때마다 둘레 변이 새로 어긋나서 76 번을 뒤집어야 끝나요. 끝난 분할의 가장 작은 각은 14.9°, 가운데값은 39.1° 예요. 그 순간 외접원 71 개가 1.6 초 동안 한꺼번에 청록으로 떠서 어느 원도 점을 품지 않은 것을 보여 주고, 그 뒤 안쪽 점들이 10 초 동안 떠돌며 변을 다시 어긋나게 해요. 다음 점 집합부터는 부채꼴이 왼쪽 위와 오른쪽 아래에서 번갈아 펴지고, 그 사이마다 점을 한 곳에서 먼 차례로 하나씩 더하는 쓸기가 끼어요. 가운데에서 쓸면 고리 모양의 가는 삼각형이 감기고 38 번이면 풀려요. 스크래치 스크립트로 2 분을 돌려 보니 뒤집기 467 번이 모두 두 삼각형의 가장 작은 각을 키웠고, 가장 적게 키운 것이 0.096° 였어요. 들로네가 됐다고 넘어가는 순간마다 모든 원과 모든 점을 맞대어 봐도 원 안에 든 점은 없었어요. Hue 는 빨강에서 노랑까지를 건너뛰는 색상환을 돌려서 0 에서는 바늘이 자홍, 좋은 삼각형이 청록이고 0.5 에서는 둘이 맞바뀌어요. Speed 는 점 43 개일 때 1 초에 6 × Speed 번 뒤집고 점이 늘면 그만큼 빨라져서, 2 에서 첫 부채꼴이 6.4 초 만에 풀리고 0.2 에서는 0.83 초에 한 번씩 뒤집어요. Points 는 안쪽 점을 30 에서 90 개까지 바꾸고, 90 이면 점 113 개, 삼각형 201 개의 부채꼴을 324 번 뒤집어 10.3 초 만에 풀어요. Circles 는 0.08 을 넘으면 삼각형의 외접원을 늘려 그려 1 에서 모두 보여 주는데, 점을 품은 원은 자홍이고 빈 원은 청록이에요. 잰 값은 움직임 0.346, 촘촘함 0.066, 가장자리 0.393, 복잡도 1.678, 채움 0.826, 색 0.197, 모양 바뀜 0.162 이고, 노브는 0.19, 0.07, 0.17, 0.17 이에요. 새로 본 것은 처음 시험하는 원이 원으로 보이지 않는다는 거예요. 첫 부채꼴에서 맨 처음 시험한 원의 반지름은 9,819 LED, 아홉 번째는 18,568 LED 라서 패널을 곧은 선으로 가로지르고, 반지름이 패널 폭의 절반인 32 LED 안으로 들어오는 원은 32 번째 뒤집기에서야 처음 나와요. 첫 판은 점 90 개에 Speed 3 이면 떠도는 점이 뒤집기를 앞질러, 떠도는 동안 평균 38.4 개의 변이 자홍으로 남았어요. 떠도는 속도를 점 사이 간격에 맞춰 √(43/점 수) 배로 줄이자 8.2 개로 내려갔어요. + +### Fibonacci Word + +주제 5 「수학 · 추상」으로 알렉시 모네로-뒤멘(Alexis Monnerot-Dumaine)의 피보나치 단어 프랙탈(2009)을 그렸어요. S(0) = 0, S(1) = 01, S(n) = S(n-1)S(n-2) 의 극한인 0100101001001… 을 k = 1 부터 한 자씩 읽으며 한 걸음씩 가고, 0 이 짝수 자리면 왼쪽, 홀수 자리면 오른쪽으로 직각만큼 꺾어요. k 를 0 부터 세면 거울상이 나와요. 패널은 처음 144자를 한 걸음 4 LED 로 그려 48×112 LED 를 차지해요. 그 밑에는 세 차수 아래인 34자 곡선이 한 걸음 9.75 LED 의 굵은 선으로 먼저 서 있고, 그 둘레 5 LED 폭에 같은 색의 희미한 띠가 깔려 있어요. 펜이 지나가면 굵은 한 걸음이 치환 0 → 01001, 1 → 010 이 만든 가는 다섯 걸음이나 세 걸음으로 바뀌어요. 띠는 그대로 남아서 가는 곡선이 굵은 곡선에서 얼마나 비켜 가는지 보여 줘요. 다 그리면 S(n) = S(n-3)S(n-4)S(n-3)S(n-3)S(n-4) 에 따라 첫 34자와 같은 토막이 55 번째와 89 번째 자리에서 다시 나오고, 패널은 처음 토막부터 차례로 밝혀요. 뒤의 둘은 홀수 자리라 거울상이에요. 그다음 첫 토막으로 4분의 1 바퀴 돌며 들어가요. 최소제곱으로 맞춘 확대는 2.437 배, 회전은 90.00° 였어요. 다 들어가면 첫 토막이 전체가 있던 자리에 거울상으로 서고, 띠도 그 둘레에서 새로 자라요. 오른쪽 끝 두 열에는 읽는 중인 단어가 한 줄로 올라가는데, 0 은 두 LED, 1 은 한 LED 예요. Hue 는 첫 자리에서 끝 자리까지 반 바퀴 도는 색을 돌리고, 0 에서는 바다 초록에서 장밋빛까지예요. Speed 는 1 마다 펜이 초당 70 LED 를 긋게 해서, 여는 값 2 에서 144 걸음이 4.1 초, 한 바퀴가 8.1 초예요. Order 는 12 부터 15 까지 그릴 자수 F(n) 을 144, 233, 377, 610 으로 골라요. 세 차수마다 모양이 돌아와서 144 와 610 은 길쭉하고 233 은 정사각형이에요. 굵은 곡선의 걸음이 5 LED 안팎인 13 부터는 띠가 3 LED 폭으로 좁아져요. Angle 은 ±0.1 안에서 정확히 90° 라 선이 LED 격자에 붙어 있어요. -1 의 60° 에서는 정사각형이 풀려 구불구불한 선이 되는데, 28,657 걸음까지 한 점도 두 번 밟지 않았어요. 1 의 120° 에서는 삼각형 격자로 접히고 144 걸음 가운데 36 걸음이 앞서 간 변을 되밟아요. 첫 판은 채움이 0.077 이었어요. 그때까지 가장 낮던 Dragon Fold 의 0.1168 보다도 낮아서, 587개 가운데 맨 끝에 서며 채움 축을 늘였어요. 4 LED 걸음의 1 LED 선은 판의 7% 남짓만 켜고, 굵은 곡선은 펜이 지나가면 꺼졌기 때문이에요. 굵은 곡선 둘레에 48/255 의 띠를 깔고 펜이 지나가도 남기자 채움이 0.210 이 됐어요. 띠의 밝기를 40/255 위에 둔 것은 띠의 가장자리가 번짐이 아니라 또렷한 경계로 세어지게 하려는 것이고, 가장자리 값은 0.827 에서 0.866 으로 올랐어요. 같은 띠 덕에 Hue 반응도 0.040 에서 0.061 이 됐어요. 이 패널에서 새로 본 것은 폭이에요. 28,657 걸음까지 걸어 봐도 곡선에서 이어지지 않은 두 점이 격자에서 이웃한 일은 없어서, 한 걸음 1 LED 로 그려도 선이 붙지 않아요. 그래도 1,597 걸음 곡선은 69×99 라 64열에 다섯 열이 모자랐어요. 그래서 이 패널에 들어가는 가장 촘촘한 곡선은 2 LED 걸음의 610 이고, 58×138 이라 위 6줄과 아래 5줄이 밖으로 나가요. 잰 값은 움직임 0.541, 가장자리 0.866, 채움 0.210, 색 0.167 이고 노브 반응은 0.06, 0.05, 0.12, 0.12 예요. + +### Rose Criterion + +주제 5 와 1 로 앨버트 로즈(Albert Rose)의 검출 기준을 노이즈 갈래에 세우고, Frost 와 Smalti, Strange Eigenmode 셋만 서 있던 복잡도 × 채움 4,2 칸을 겨눴어요. 복잡도가 2.171 을 넘으면서 채움이 0.302~0.558 인 칸이에요. 로즈는 「The sensitivity performance of the human eye on an absolute scale」(J. Opt. Soc. Am. 38, 1948)에서, 단위 넓이에 광자 n 개를 받은 바탕 위에 놓인 넓이 A, 대비 C 의 원판은 k = C√(nA) 가 5 쯤을 넘어야 보인다고 적었어요. 넓이를 지름으로 바꿔 쓰면 경계는 C·d = 상수라서, 대비와 지름으로 짠 격자 위에서는 쌍곡선이에요. 패널은 가로로 지름 11, 9, 7, 5, 3 LED 의 원판(97, 69, 37, 21, 9 LED)을 두고, 세로로 대비를 0.60 에서 0.15 까지 0.05 씩 낮춘 열 줄을 둬서 원판이 50개예요. LED 마다 프레임마다 푸아송 광자를 더하고, 원판 LED 는 바탕의 1 + C 배를 받아요. 쌓인 셈은 바탕이 받았어야 할 수로 나눠 바탕이 u = 1 이 되게 하고, 지수 꼴 아래 끝과 부드러운 어깨를 단 곡선으로 밝기를 매겨서 바탕이 13 근처에 앉아요. LED 의 세 채널은 같은 셈을 저마다 1 광자 rms 의 읽기 잡음을 얹어 읽어요. 금빛 선은 C·√A·n/√(n + σ²) = 5 의 등고선이고, 선이 그어진 쪽이 보인 원판이에요. 여는 자리에서는 3 초 동안 LED 당 광자 25 개까지 쌓이고 1.2 초 멈춘 뒤 지워져요. 첫 원판(대비 0.60, 97 LED)이 1.3 광자에서 선을 넘고, 25 에서 선은 C·d = 1.15 에 멈춰 원판 7 개가 낟알 속에 남아요. 선을 믿어도 되는지 스크래치 셈으로 확인했어요. 100 번 돌려 선이 원판 한가운데를 지나는 4,300 번의 프레임에서 그 원판의 k 를 패턴이 가진 광자 셈과 같은 프레임 바탕의 평균, 표준편차로 다시 재 보니 평균 5.11, 표준편차 1.16 이었고, 선이 그 프레임에 말하는 k 도 5.11 이었어요. 읽기 잡음 9 광자에서도 5.03 과 5.09 예요. Hue 는 빛을 시안에서 보라, 산호, 라임으로 돌리고 선은 금에서 초록, 파랑, 자홍으로 따라 돌아요. Speed 는 한 번 쌓는 시간을 2 에서 3 초, 0.2 에서 30 초로 바꾸고, Exposure 는 멈추는 선량을 25 에서 2000 광자까지 로그로 올려요. 0.37 부터는 마지막 원판(대비 0.15, 9 LED)도 124 광자에서 보이고, 나머지 선량은 낟알만 가라앉혀요. Read noise 는 읽기 잡음을 1 에서 9 광자 rms 로 올려서, 9 에서는 첫 원판이 8 광자를 기다리고 25 광자에서 24 개가 숨어요. 잰 값은 움직임 1.654, 촘촘함 0.244, 가장자리 0.566, 복잡도 2.300, 채움 0.484, 색 0.253, 모양 바뀜 0.255 이고, 노브는 0.12, 0.17, 0.12, 0.17 이에요. 첫 판은 광자를 빛의 색에 따라 세 채널에 나눠 세게 해서 채널마다 낟알이 따로 튀었어요. 화면이 무지개 색종이처럼 되고 채움 0.741, 색 0.554 로 넘쳤는데, LED 하나에 셈 하나를 두자 채움이 0.436 으로 내려왔어요. 새로 본 것은 deflate 가 채널 사이의 상관을 본다는 거예요. 세 채널이 읽기 한 번을 나눠 쓰면 복잡도가 1.826 이고, 채널마다 따로 읽으면 2.308 이에요. 바이트 엔트로피는 둘 다 6.29~6.30 비트로 같아요. 어두운 LED 가 같은 셈을 가지면 같은 세 바이트가 되풀이되고, deflate 는 그걸 앞에서 본 문자열로 찾아 줄여요. + +### Josephus Problem + +주제 5 「수학 · 추상」과 주제 4 를 같이 겨눴어요. 요세푸스는 『유대 전쟁사』(1세기)에 요타파타 동굴에서 자기와 한 사람만 살아남은 일을 적었고, 바셰(Claude Gaspard Bachet)의 『Problèmes plaisans et délectables』(1612)는 그 일을 셈 놀이로 풀어서, 41명이 둘러서서 셋째마다 빠지면 요세푸스가 31번째 자리에 서야 혼자 남는다고 해요. 코드로 세어 보니 J(41,3) 은 정말 31 이었어요. 그레이엄, 커누스, 파타슈닉의 『Concrete Mathematics』(1989) 1장은 k=2 일 때 n = 2^m + L 이면 남는 자리가 2L+1 이라는 닫힌 꼴을 줘요. 패널 위 64줄은 램프 n 개가 둘러선 원이에요. 손이 한 틱에 한 자리씩 돌고, 켜진 램프를 지나면 박이 되어 하얗게 번쩍여요. k 번째 박은 그 램프를 끄고 안쪽으로 빛이 튀어요. 꺼진 자리를 지나는 틱은 쉼표라서 원이 빌수록 박이 성겨져요. 램프 16개 원은 xXxX 로 시작해 x.X., x...X..., x.......X 로 바퀴마다 박이 반으로 줄고, 64개 원은 353 틱 가운데 126 틱만 박이에요. 링 안의 36칸 롤이 이 리듬을 한 틱 한 칸으로 그리고, 그 위에 n 이 떠 있다가 램프가 하나만 남으면 그 자리 번호가 하얗게 바뀌어요. 아래 64줄은 n = 1 부터 64 까지 원 하나가 한 줄이고 자리 i 가 열 i 예요. 빠진 차례대로 짙은 파랑에서 청록과 초록을 지나 금빛으로 밝아지고 살아남은 자리만 하얘요. 그래서 k=2 에서는 하얀 칸이 한 줄 내려갈 때마다 두 칸씩 오른쪽으로 가다가 4, 8, 16, 32, 64 에서 첫 자리로 떨어지는 톱니가 돼요. 위 원이 돌고 있는 n 의 줄은 밝게 켜지고, 램프가 빠지는 대로 칸이 제 색으로 채워지며 오른쪽 끝까지 점선이 이어져요. 표는 자리를 하나씩 지우는 모의와 점화식 J(n) = (J(n−1)+k) mod n 을 n 64개와 k 여섯 가지에서 맞춰 봤고, k=2 는 닫힌 꼴과도 맞았어요. 패턴을 k 마다 패널 시간 40분씩 돌려 끝난 원 1,724개의 생존자도 다 맞았어요. Hue 는 빠진 차례의 색을 돌리고, 0 에서 램프는 차가운 흰빛이에요. Speed 는 박자예요. Speed 2 에서 8개 원은 2.1초, 64개 원은 6.4초에 끝나요. Step 은 k 를 2 에서 7 까지 바꿔요. 3 이면 톱니가 4, 6, 9, 14, 21, 31, 47 에서 떨어지고, 7 이면 롤이 1 부터 7 까지 계단으로 세요. Circle 은 위 원이 시작하는 n 이에요. 원이 끝날 때마다 램프가 하나씩 늘어서 Speed 2 로 8 에서 64 까지 6분쯤 걸려요. 잰 값은 움직임 0.260, 촘촘함 0.079, 가장자리 0.744, 복잡도 0.742, 채움 0.448, 색 0.138, 모양 바뀜 0.059 이고 thirds 는 38/28/67, 노브는 0.07, 0.05, 0.11, 0.06 이에요. 첫 판은 빠진 차례를 색상환 0.42 바퀴에 얹고 밝기를 0.85 까지 올렸어요. 늦게 빠진 칸이 분홍으로 밝아 하얀 생존자 줄을 덮었고, 파랑에서 금빛으로 가는 색에 밝기를 0.73 에서 멈추자 톱니가 떠올랐어요. 아직 돌지 않은 줄을 0.5 밝기의 그림자로 둔 판도 있었는데, 8 에서 64 까지 6분이 걸려 패널 아래가 거의 늘 그림자였어요. 표를 다 켜도 Circle 반응은 0.09 에서 0.07 로만 줄어서 그림자를 뺐어요. 새로 본 것은 표를 곧게 내려가는 어두운 세로줄이에요. 첫 바퀴는 원 크기와 상관없이 k−1, 2k−1, … 자리를 같은 차례로 끄기 때문에, k=7 에서는 6, 13, 20 … 열이 표 끝까지 가장 어두운 줄로 내려가요. + +### Parallel Transport + +주제 5 「수학 · 추상」으로 구면의 평행 이동과 그 홀로노미를 공간·원근 갈래에 세웠어요. 툴리오 레비치비타(Tullio Levi-Civita)는 1917년 휜 곡면에서 접벡터를 돌리지 않고 옮기는 법을 정했어요. 단위 구면에서 닫힌 고리를 따라 옮긴 화살은 고리가 두른 넓이만큼 돌아서 돌아오고, 그 까닭은 가우스(1827)와 보네(1848)의 정리예요. 위 64줄에는 반지름 30 LED 구를 정사영으로 그려 왼쪽 위에서 비추고, 30도 간격 경위선을 흐리게 깔았어요. 구는 화살을 따라 천천히 돌아요. 고리는 900점으로 나누고, 화살은 이웃한 두 점을 잇는 대원을 축으로 돌려 넘겨요. 금빛 화살은 대가 10 LED, 촉 두 갈래가 4 LED 이고, 8 LED 쯤 갈 때마다 흐린 유령 화살을 남겨요. 지나온 넓이는 구 위에서 장밋빛으로 물들어요. 아래 다이얼은 같은 넓이를 열두 시 방향부터 부채로 채우고, 바늘은 화살이 기준장에서 돈 각을 가리켜요. 기준장은 처음 화살을 고리 안쪽 한 점까지 대원으로 옮겼다가 지금 자리까지 다시 대원으로 옮긴 화살이에요. 부채는 구면 삼각형 넓이 공식으로, 바늘은 옮긴 화살로 따로 셌는데 900점 모두에서 10⁻¹² 도 안에서 같았어요. 한 바퀴를 마치면 흰 처음 화살과 돌아온 화살이 한 자리에 서고 둘 사이에 호가 그어져요. 판 밖 스크립트에서는 걸음마다 접평면에 투영하고 다시 정규화하는 식으로 옮겨 봤어요. 북극에서 자오선을 타고 적도로 내려가 90도를 간 뒤 다른 자오선으로 올라오는 세모에서, 처음 화살을 다리에 45도로 두면 걸음 0.01 에서 90.224 도, 0.0001 에서 90.00225 도로 걸음에 비례해 90 에 다가갔어요. 위도 48.85도 원은 걸음 0.0001 에서 88.9226 도로 2π(1 − sin φ) = 88.9238 도에 닿았고, 동쪽과 북쪽으로 잡은 틀에서 재면 −271.08 도예요. 레옹 푸코(Léon Foucault)가 1851년 파리 팡테옹에 매단 진자가 항성일 하루에 시계 방향으로 도는 360 sin φ 도와 같은 값이에요. Hue 는 색을 함께 돌리고, Speed 2 에서 한 바퀴가 6초예요. Loop 는 북극 세모와 위도 원을 고르고, 그다음 두 자리는 작은 측지 정사각형과 한 점에서 맞닿은 두 원이에요. 두 원은 하나를 반시계, 하나를 시계 방향으로 돌아서 8자가 돼요. Size 는 세모의 극 각을 10~170도(여는 자리 90)로, 위도를 8~85도(여는 자리 48.85)로 바꿔요. 정사각형은 중심에서 모서리까지 2~30도이고, 8자는 두 원의 반지름을 11도와 33도 사이에서 맞바꿔 −51.5도에서 51.5도까지 돌려요. 위도 8도의 원은 화살을 309.9도 돌리고, 2도 정사각형은 0.14도만 돌려요. 잰 값은 움직임 0.188, 촘촘함 0.042, 가장자리 0.367, 복잡도 0.936, 채움 0.362, 색 0.112, 모양 바뀜 0.055, thirds 67/29/11 이고 노브는 0.05, 0.06, 0.11, 0.05 예요. 이 패널에서 새로 본 것은 여는 세모의 90도가 화살을 다리 위에 눕힌다는 거예요. 처음 화살을 고리에 직각으로 바깥쪽에 두면 돌아온 화살이 첫 자오선과 겹쳤어요. 안쪽에 두면 처음 화살이 둘째 자오선과 겹쳐서, LED 에서는 화살과 고리가 한 줄로 보였어요. 가는 쪽에서 시계 방향 135도로 두자 북극 위에서 위쪽을 보는 흰 화살과 왼쪽을 보는 금빛 화살이 따로 읽혀요. 8자도 처음엔 −0.4도로 닫혔어요. 첫 원의 꼭대기에서 4분의 1 바퀴를 가면 맞닿은 점에 닿을 줄 알았는데, 그 점에서 동쪽으로 난 대원은 위도선이 아니라서 꼭대기가 비껴 있었어요. 맞닿은 점에서 거꾸로 4분의 1 바퀴 간 점을 출발점으로 잡자 0.0000 으로 닫혔어요. 첫 판의 구는 밝기가 0.07 + 0.30 sh² 로 어두워서 Hue 반응이 0.03 이었고, 0.08 + 0.42 sh² 로 밝히자 0.05 가 됐어요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -3624,7 +3703,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 25일 `dev`(`f72355d`)의 328개에 43판부터 66판까지의 이백마흔 개를 더한 568개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 25일 `dev`(`f72355d`)의 328개에 43판부터 66판까지의 이백마흔 개와 68판의 열 개를 더한 578개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -3714,6 +3793,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 66판은 64판까지 든 548개에 빈 칸도 둘뿐인 칸도 없어서, 셋뿐인 칸 열셋 가운데 같이 돈 65판(#328)이 고른 촘촘함 × 색 4,4 와 색 × 모양 바뀜 4,4 를 빼고 촘촘함 × 모양 바뀜 4,4 와 복잡도 × 채움 4,1 을 겨눴어요. 촘촘함 × 모양 바뀜 4,4 는 이웃 차이 평균이 0.297 을 넘으면서 모양 바뀜이 0.328 을 넘는 칸이에요. 고운 결은 16×8 블록 안에서 평균되어 지워지니, 결은 둔 채로 블록 평균이 통째로 옮겨 가야 해요. Barrier Grid 는 낱말과 음화를 한 칸씩 엇갈려 짠 인쇄 위로 막대 격자가 미끄러져, 금빛 글자가 파란 바탕의 검은 글자로 뒤집혀요. 첫 판은 촘촘함 0.250, 모양 바뀜 0.228 이었고, 막대에 바둑판 구멍을 내고 「빛」의 세로획을 4 LED 의 배수에 세우자 0.316 과 0.381 이 됐어요. 복잡도 × 채움 4,1 은 복잡도가 2.172 를 넘으면서 채움이 0.302 아래인 칸이에요. Wilkinson's Polynomial 은 의사근 집합 안의 알갱이마다 켤지와 밝기를 따로 뽑아 deflate 가 줄이지 못하게 하고, 집합 밖 알갱이는 24 아래에 두어 복잡도 2.203, 채움 0.283 이에요. 성운을 24 아래 먼지로만 둔 첫 판은 1.259 와 0.060 이었어요. 열 개 다 일곱 축의 끝 안에 있어서 칸 경계는 60판 그대로예요. 66판까지 든 558개에서 지도 21장은 다 `none` 이고, 셋뿐인 칸은 열셋에서 열하나로 줄었어요. 겨눈 두 칸은 둘 다 넷이 됐어요. +68판은 66판까지 든 568개에 같이 돈 67판(#330)의 열 개를 더해 세어도 빈 칸도 둘뿐인 칸도 없어서, 셋뿐인 칸 일곱 가운데 67판이 고른 복잡도 × 모양 바뀜 4,4 와 가장자리 × 움직임 1,4 를 빼고 복잡도 × 채움 4,2 와 가장자리 × 촘촘함 1,2 를 겨눴어요. 복잡도 × 채움 4,2 는 복잡도가 2.171 을 넘으면서 채움이 0.302~0.558 인 칸이에요. 프레임이 거의 눌리지 않을 만큼 LED 마다 밝기가 제멋대로이면서, 패널의 반 가까이는 24 아래로 어두워야 해요. Rose Criterion 은 광자 낟알을 LED 마다 채널마다 따로 읽어 복잡도 2.300 이고, 바탕이 13 근처의 낟알로 앉아 채움이 0.484 예요. 세 채널이 읽기 한 번을 나눠 쓰던 판은 바이트 엔트로피가 6.29 비트로 같은데도 복잡도가 1.826 이었어요. 어두운 LED 가 같은 셈을 가지면 같은 세 바이트가 되풀이되고, deflate 는 그걸 앞에서 본 문자열로 찾아 줄였어요. 가장자리 × 촘촘함 1,2 는 가장자리가 0.210 아래이면서 촘촘함이 0.082~0.189 인 칸이라, 이웃 계단이 많되 거의 다 40 아래여야 해요. Palimpsest 는 양피지 결을 이웃 평균을 두 번 빼고 셋으로 나눈 세 단계 결로 깔아서, 이웃의 79.6% 가 서로 다르면서 결만의 계단은 36 을 넘지 않아요. 결 없이 글자만 켜면 촘촘함 0.068 에 가장자리 0.198 이었고, 결을 깔자 0.088 과 0.178 이 됐어요. 같은 결을 획 가장자리에도 얹으면 40 언저리 계단이 넘쳐 가장자리 0.226 으로 칸을 벗어났어요. 이 연구가 LED 밝기를 가장 밝은 채널로 재는 것도 도왔어요. 기도문 잉크는 빨강을 62%, 파랑을 84% 먹어 눈에는 진한 갈색인데 재는 밝기로는 계단이 낮아요. 촘촘함이 칸 아래 끝에서 0.006 위라, 촘촘함 축의 끝이 움직이면 먼저 빠질 패턴이에요. 열 개를 다 넣고 67판의 열 개까지 더해 588개로 세니 겨눈 두 칸에 넷째가 섰고, 셋뿐인 칸은 가장자리 × 촘촘함 1,3, 1,4, 2,4 와 가장자리 × 복잡도 4,4, 촘촘함 × 복잡도 4,4 다섯이 남았어요. + ### 아직 없는 갈래 `families.json` 의 쉰여섯 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 29판이 연 위상·매듭 갈래는 Hopf Fibration 과 31판의 Boy's Surface, 33판의 Borromean Rings, 34판의 Lakes of Wada, 36판의 Heawood Torus, 38판의 Sperner's Lemma, 39판의 Borsuk-Ulam, 44판의 Ham Sandwich, 46판의 Reeb Graph, 48판의 Möbius Energy, 49판의 Square Peg, 51판의 Persistence Barcode, 53판의 Belt Trick, 55판의 Universal Cover 예요. 28판이 열려다 접은 갈래라 28판의 Hairy Ball 은 노이즈·흐름에 그대로 있어요. 세잎 매듭은 48판의 Möbius Energy 가 처음 세웠어요. 29판은 화장품 팝업 갈래도 열었어요. 주제 6 의 몫이라 주제 2 로 세지 않았고, Cream Swirl, Serum Gel, Glitter Fall, Loose Powder 넷이 다 들어 있어요. 30판의 Swatch Row, Pearl Sheen, Bi-Phase, Satin Fold 가 그 갈래에 넷을 더했고, 31판의 Lip Gloss, Foam Lather, Setting Mist, Sponge Blend 가 넷을 더 더해 열둘이에요. 32판의 Cushion Mesh, Clay Mask, Sugar Scrub, Chrome Powder 가 넷을 더해 열여섯이고, 33판의 Cleansing Balm, Gold Leaf, Essence Drop, Peel-Off 가 넷을 더해 스물이에요. 34판의 Cat Eye, Velvet Nap, Mood Tint, Hair Gloss 가 넷을 더해 스물넷이고, 35판의 Bath Fizz, Toner Pad, Sheet Mask, Gilded Cream 이 넷을 더해 스물여덟이고, 36판의 Lotion Ribbon, Shimmer Oil, Blooming Gel, Whipped Soap 이 넷을 더해 서른둘이에요. 38판의 Milk Ring, Couette Shimmer, Photochromic, Serum Fingers 가 넷을 더해 서른여섯이에요. 39판의 Soap Film, Blotting Paper, Serum Drip, Crackle Polish 가 넷을 더해, 37판의 넷까지 마흔넷이에요. 41판은 할로윈 갈래를 열었어요. 주제 6 의 몫이라 주제 2 로 세지 않았고, Jack-o'-Lantern, Sheet Ghosts, Spider Drop, Cauldron, Eyes in the Dark 다섯이 들어 있어요. 28판이 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table, 33판의 Goldbach Comet, 35판의 Kolakoski, 36판의 Elliptic Chords, 39판의 Explicit Formula, 44판의 Bulgarian Solitaire, 48판의 Chebyshev's Bias, 49판의 Curlicue, 51판의 Sato-Tate, 53판의 Gaussian Periods, 55판의 Question Mark Function 이에요. 27판이 연 철도 갈래는 Funicular 하나이고, 필름·영화 갈래는 Horse in Motion 하나예요. 두 갈래는 주제 표가 수학 쪽으로 바뀌기 전에 열었어요. 그 뒤로 ROUND.md 는 사물로 묶는 갈래를 더 열지 않아요. 26판이 연 얼음·극지 갈래는 Iceberg 와 27판의 Sled Dogs 이고, 미생물 갈래는 Dark Field 와 27판의 Mitosis 예요. 25판이 연 농사·밭 갈래는 Rice Terraces 와 26판의 Rice Planting, 27판의 Center Pivot 이고, 화산 갈래는 Eruption 과 26판의 Geyser, 27판의 Lava Lake 예요. 24판이 연 공사·건설 갈래는 Tower Crane 과 25판의 Excavation, 26판의 Implosion 이에요. 24판이 연 도자기 갈래는 Potter's Wheel 과 25판의 Kintsugi, 26판의 Noborigama 예요. 23판이 연 무대·공연 갈래는 Proscenium 과 24판의 Flying Trapeze, 25판의 Marionette, 26판의 Shadow Play 예요. 23판이 연 짐승 갈래는 Giraffe 와 24판의 Emergence, 25판의 Snow Monkeys 예요. 22판이 연 몸 갈래는 Circulation 과 23판의 Axon, 24판의 Breath, 27판의 Fluoroscope 예요. 22판이 연 비행 갈래는 Liftoff 와 23판의 Final Approach, 24판의 Thermal, 25판의 Skydiver 예요. 20판이 연 새 갈래는 Kingfisher 와 21판의 Woodpecker, 22판의 Weaver, 23판의 Skein 이에요. 20판이 연 화학·실험 갈래는 Chromatography 와 21판의 Chemical Garden, 22판의 Flame Test, 23판의 Elephant Toothpaste 예요. 21판이 연 종이 갈래는 Pop-up Book 과 22판의 Senbazuru, 23판의 Hanging Scroll, 26판의 Origami 예요. 21판이 연 운동·경기 갈래는 Lap Pool 과 22판의 Bowling Lane, 23판의 High Dive, 26판의 Pole Vault, 27판의 Climbing Wall 이에요. 위아래로 움직이는 경기는 High Dive 와 Pole Vault 뒤로 27판에 실내 암벽이 섰어요. 19판이 연 부엌 갈래는 Popcorn 과 20판의 Kettle, 21판의 Pancakes 이고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판이 연 유리 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori, 27판의 Glassblower 예요. 광물·결정 갈래는 Bismuth 와 19판의 Dripstone, 20판의 Agate 이고, 놀이공원 갈래는 Drop Tower 와 19판의 Helter Skelter, 20판의 High Striker, 25판의 Ferris Wheel 이에요. 말이 오르내리는 회전목마는 아직 없어요. 17판이 연 전자 회로 갈래는 Circuit Board 와 18판의 Server Rack, 21판의 Nixie 이고, 장난감 갈래는 Marble Run 과 18판의 Peg Light, 21판의 Slinky, 22판의 Pin Art 예요. 16판이 연 바다 갈래는 Abyss 와 17판의 Shoreline, 20판의 Sunglint, 23판의 Bait Ball 이고, 곤충 갈래는 Fireflies 와 17판의 Ant Farm, 20판의 Moth Lamp, 24판의 Waggle Dance 예요. 15판이 연 도시 갈래는 Metro Map 과 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판이 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables, 24판의 Pipe Organ 이에요. 4판이 연 직물 갈래는 Jacquard Loom 과 15판의 Plait, 19판의 Needlepoint, 23판의 Sequins 이고, 8판이 연 기계 갈래는 Gear Train 과 15판의 Cam Wave, 19판의 Paternoster 예요. 12판이 연 천체 갈래는 Star Trails 와 15판의 Analemma, 16판의 Lunation, 22판의 Galilean Moons 예요. 위성의 궤도는 Galilean Moons 가 그렸고, 해를 도는 행성은 아직 없어요. 리듬·펄스 갈래는 1판의 Euclid Pulse 와 Pulse Bars 뒤로 20판의 Change Ringing, 32판의 Arnold Tongues, 35판의 Chimera State, 38판의 Infinity Series, 41판의 Siteswap, 44판의 Kac Ring, 46판의 Stochastic Resonance 가 섰어요. @@ -3857,9 +3938,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 66판에서는 Golomb Ruler 와 Zermelo Navigation 이 128줄을 써요. Golomb Ruler 는 눈금 14개인 최적 자의 길이가 127 이라 눈금 0 이 맨 위 줄에, 127 이 맨 아래 줄에 서고, 91개 호가 그 사이를 덮어 thirds 가 55/68/48 이에요. 왼쪽 자와 오른쪽 길이 자를 빼고 재도 49/63/44 예요. Zermelo Navigation 은 121번 줄 항구에서 0.5초마다 떠난 앞선이 1초에 18 LED 씩 올라가 패널 끝에 닿아서 66/64/39 예요. Etaoin Shrdlu 도 위에 글줄이, 아래에 막대가 서서 26/17/49 이고, 늘 켜 둔 라이노타이프 줄을 빼면 27/17/44 예요. 가운데 3분의 1 은 떨어지는 글자만 지나가서 가장 비어요. 나머지 가운데 Wilkinson's Polynomial 은 근 1~7 이 거의 움직이지 않는 점으로 남아 아래 3분의 1 이 4 예요. +68판에서는 Ogham 과 Josephus Problem 이 128줄을 써요. Ogham 은 선돌의 모서리가 패널 높이를 다 올라가고 thirds 가 67/67/67 인데, 첫 판은 그 거의 다가 돌 면의 몫이라 돌과 하늘을 빼고 글만 재면 1.4/3.5/2.9 였어요. 끌이 돌 밑동에서 시작하고 화면이 끌을 위에서 36째 줄에 두어서, 재는 2~8초 가운데 5초 넘게 위 3분의 1 이 아직 새기지 않은 돌이었어요. 홈을 LED 두 줄로 파고 새 돌의 아래 100줄을 0.9초 만에 새기자 글만으로 7.0/7.7/7.1 이 됐어요. Josephus Problem 은 위 64줄에 램프 원이, 아래 64줄에 n = 1 부터 64 까지의 표가 서서 38/28/67 이에요. Huffman Tree 도 잎이 바닥에 서고 뿌리가 꼭대기에 닿아 35/30/14 이에요. 나머지 일곱 가운데 Composing Stick 은 활자 상자, 막대, 판, 종이가 위에서 아래로 서서 74/83/89 이고, Parallel Transport 는 구가 위 64줄에 있어 67/29/11 이에요. + ### 낯선 수학 -주제 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)이에요. +주제 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)이에요. 68판은 다섯을 더했어요. 들로네의 빈 외접원과 로슨의 뒤집기(Delaunay Flips), 모네로-뒤멘의 피보나치 단어 프랙탈(Fibonacci Word), 로즈의 검출 기준(Rose Criterion), 요세푸스 문제와 바셰의 셈 놀이(Josephus Problem), 레비치비타의 평행 이동과 푸코의 진자(Parallel Transport)예요. 목록 밖에서 다음에 해 볼 것이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -4009,6 +4092,10 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 목록 밖에서 66판 뒤에 해 볼 것이에요. 제임스 싱어(James Singer, 1938)의 완전 차집합은 91 을 법으로 {0, 1, 3, 9, 27, 49, 56, 61, 77, 81} 열 점이 0 아닌 나머지를 꼭 한 번씩 재서, Golomb Ruler 의 호 그림을 91줄 원통에 감으면 빈칸도 겹침도 없는 길이 자가 돼요. 아이작 뉴턴의 회전 궤도 정리(『프린키피아』 1권 9절, 1687)는 Hodograph 의 케플러 풀이에서 해에서 본 각도만 k 배로 늘리면 되고, 역세제곱 힘을 더한 궤도의 호도그래프가 원에서 벗어나는 모습을 나란히 볼 수 있어요. Zermelo Navigation 의 32방향 데이크스트라에 물살 대신 언덕 높이를 넣으면, 한 점에서 퍼지는 측지선 앞선이 만나 접히는 절단 궤적(앙리 푸앵카레, 1905)이 그려져요. Shapley-Folkman 의 격자 민코프스키 합으로 두 모양을 섞은 (1−t)A + tB 의 넓이를 띠마다 재면, 넓이의 제곱근이 t 에 대해 오목하다는 브룬-민코프스키 부등식(헤르만 브룬, 1887; 헤르만 민코프스키, 1896)이 보여요. 같은 스무 근을 번스타인 기저로 적으면 의사근 구름이 얼마나 줄어드는지(리다 파루키와 V. T. 라잔, 1987)를 Wilkinson's Polynomial 의 동시 반복과 의사근 지도로 패널 위아래에 나란히 그릴 수 있어요. +68판에서 본 것이에요. 이번에도 정리가 말하는 수를 패턴의 셈으로 맞춰 봤어요. Delaunay Flips 는 2분 동안의 뒤집기 467번이 모두 두 삼각형의 가장 작은 각을 키웠고, 가장 적게 키운 것이 0.096° 였어요. 들로네가 됐다고 넘어가는 순간마다 모든 원과 점을 맞대도 원 안에 든 점은 없었어요. Josephus Problem 의 표는 하나씩 지우는 모의와 점화식이 n 64개와 k 여섯 가지에서 맞았고, 바셰의 41명이 셋째마다 빠지면 정말 31번째 자리가 남았어요. Parallel Transport 는 걸음마다 접평면에 투영하고 다시 정규화하는 적분으로 위도 48.85도 원이 88.9226도를 돌아 2π(1 − sin φ) = 88.9238도에 닿았어요. Rose Criterion 은 선이 원판 한가운데를 지나는 4,300 프레임에서 원판의 k 를 다시 재 보니 평균 5.11 이었고, 선이 그 프레임에 말하는 k 도 5.11 이었어요. 패널이 그림을 바꾼 것도 있어요. Delaunay Flips 의 첫 부채꼴에서 맨 처음 시험한 외접원은 반지름이 9,819 LED 라 패널을 곧은 선으로 가로질렀고, 반지름 32 LED 안의 원은 32번째 뒤집기에서야 처음 나왔어요. Fibonacci Word 는 28,657 걸음까지 걸어도 곡선에서 이어지지 않은 두 점이 격자에서 이웃한 일이 없어서 한 걸음 1 LED 로도 선이 붙지 않지만, 1,597 걸음 곡선은 69×99 라 64열에 다섯 열이 모자랐어요. Josephus Problem 의 표에는 k−1, 2k−1, … 열이 곧게 가장 어두운 줄로 내려가요. 첫 바퀴는 원 크기와 상관없이 그 자리를 같은 차례로 끄기 때문이에요. Parallel Transport 의 8자 고리는 처음에 −0.4도로 닫혔어요. 맞닿은 점에서 동쪽으로 난 대원이 위도선이 아니어서, 출발점을 맞닿은 점에서 거꾸로 잡자 0.0000 으로 닫혔어요. + +목록 밖에서 68판 뒤에 해 볼 것이에요. 짐 루퍼트의 들로네 다듬기(Jim Ruppert, 1995)는 가장 작은 각이 기준보다 작은 삼각형의 외접원 중심에 점을 더하고 로슨 뒤집기로 다시 들로네를 맞춰요. 모든 각이 20.7° 를 넘을 때까지 자홍 바늘이 하나씩 사라지는 그림이라, Delaunay Flips 의 뒤집기와 외접원, 각 색칠을 그대로 써요. 스투름 단어(Marston Morse, Gustav Hedlund, 1940)를 기울기 α 노브로 바꿔 가며 같은 홀짝 규칙으로 걸어 볼 만해요. α = 1/φ² 에서 Fibonacci Word 의 곡선이 되고, 그 파일의 걷기와 닮음 맞춤, 확대를 그대로 써요. 캠벨-롭슨 대비 감도 도표(Fergus Campbell, John Robson, 1968)는 Rose Criterion 의 광자 셈과 k 등고선 코드를 그대로 써요. 원판 대신 오른쪽으로 갈수록 촘촘해지고 위로 갈수록 대비가 낮아지는 사인 줄무늬를 두면, 낟알 속에서 보이는 경계가 선량에 따라 움직여요. 럭키 넘버 체(Verna Gardiner, Roger Lazarus, Nicholas Metropolis, Stanislaw Ulam, 1956)는 남은 수가 다음에 지울 간격을 정하는 요세푸스식 제거라서, Josephus Problem 의 표 코드로 체를 한 번 거를 때마다 한 줄씩 쌓을 수 있어요. 광섬유를 나선으로 감으면 접선이 방향의 구 위에 원을 그리고, 편광면이 그 원이 두른 입체각만큼 돌아요. 아키라 도미타(Akira Tomita)와 레이먼드 치아오(Raymond Chiao)가 1986년에 잰 마이클 베리(Michael Berry, 1984)의 기하 위상이에요. 이 파일의 구와 carry 함수, 넓이 다이얼을 그대로 쓰고 아래 절반에 감긴 섬유를 세우면 돼요. + ### 타이포 주제 7 로 해 본 글자 실험과 다음에 해 볼 것이에요. 63판과 64판이 같이 돌며 다섯씩 처음 만들었어요. 63판의 다섯은 한글 자모가 날아와 모아쓰기로 잠기는 조립(Jamo Assembly), 놀턴과 하먼의 기호 모자이크(Type Mosaic), 호프스태터의 트립렛 그림자(Trip-let), 카프리카의 계산 종이(Kaprekar's Routine), 캐럴의 낱말 사다리(Word Ladder)예요. @@ -4035,7 +4122,15 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 기능 감은 이번에도 Text 레이어의 글을 코드가 받는 길로 모였고, 받는 꼴이 패턴마다 달랐어요. Etaoin Shrdlu 는 글을 받아 세려는데 Text 레이어가 200자까지라(`TEXT_MAX_CHARS`) 앨리스 첫 두 문장의 464글자가 157글자로 줄어요. Barrier Grid 는 걸음마다 다른 낱말이 필요해서 낱말 목록과 낱말마다의 덮임 비트맵을 바랐고, Subpixel 은 글꼴의 글자 윤곽을 2차 베지에 점 목록으로 받기를 바랐어요. Syllable Odometer 는 패턴끼리 글꼴을 나눠 쓰는 길을 바랐어요. Lab 의 사이트 글꼴 넷은 latin subset 만 실어서 한글 픽셀 글꼴이 아직 없어요. Gridfonts 는 다른 것을 바랐어요. Style 노브의 일곱 자리에 이름을 달 수 있게 `@knobs` 가 칸 이름을 받으면, 지금 어느 스타일인지 주석을 읽지 않아도 알 수 있어요. -66판 뒤에 해 볼 것이에요. 데이비드 허프먼의 부호(1952)는 Etaoin Shrdlu 가 세고 줄 세운 열여섯 칸 위에서 지을 수 있어요. 가장 낮은 두 막대가 하나로 합쳐 올라가기를 되풀이하면 패널 위로 자라는 나무가 되고, 잦은 글자일수록 뿌리까지 가지가 짧아요. 게럴드 오스터와 니시지마 야스노리(Scientific American, 1963)가 보인 대로 주기 구조 둘을 겹치면 모아레가 생기니, Barrier Grid 의 인쇄와 막대를 동심원 주기로 짜면 낱말이 원 띠마다 빛과 그늘로 갈려요. 크리스 그린이 Valve 에서 SIGGRAPH 2007 에 낸 거리장 글자 확대는 Subpixel 의 윤곽을 거친 격자의 거리장으로 바꿔 키운 뒤 문턱으로 자르면 되고, 모서리가 둥글게 녹는 것이 픽셀 단위로 보여요. 안상수체(안상수, 1985)는 자모마다 꼴이 한 벌뿐인 탈네모꼴이라, Syllable Odometer 의 바퀴 셋이 고른 자모를 제자리에 겹쳐 놓기만 해도 11,172 음절이 다 서요. +66판 뒤에 해 볼 것이에요. 게럴드 오스터와 니시지마 야스노리(Scientific American, 1963)가 보인 대로 주기 구조 둘을 겹치면 모아레가 생기니, Barrier Grid 의 인쇄와 막대를 동심원 주기로 짜면 낱말이 원 띠마다 빛과 그늘로 갈려요. 크리스 그린이 Valve 에서 SIGGRAPH 2007 에 낸 거리장 글자 확대는 Subpixel 의 윤곽을 거친 격자의 거리장으로 바꿔 키운 뒤 문턱으로 자르면 되고, 모서리가 둥글게 녹는 것이 픽셀 단위로 보여요. 안상수체(안상수, 1985)는 자모마다 꼴이 한 벌뿐인 탈네모꼴이라, Syllable Odometer 의 바퀴 셋이 고른 자모를 제자리에 겹쳐 놓기만 해도 11,172 음절이 다 서요. + +68판의 다섯은 선돌 모서리를 따라 아래에서 위로 새기는 오검(Ogham), 거울 글자 활자를 막대에 세워 스페이스로 맞추고 찍는 손 조판(Composing Stick), 긁어낸 밑글이 빛의 띠를 바꾸면 떠오르는 양피지(Palimpsest), 글의 기호를 둘씩 묶어 자라는 허프먼 부호 나무(Huffman Tree), 큰 글자 윤곽을 획 방향이 맞는 아스키 문자로 잇는 구조 기반 아스키 아트(Structure ASCII)예요. + +68판에서 본 것이에요. 셋이 글꼴을 새로 그렸고 둘이 18판 Typewriter 의 3×5 대문자를 옮겨 썼는데, 옮겨 쓰자 남의 글꼴에 빠진 것이 보였어요. Typewriter 의 N 은 패널에서 소문자 n 으로 읽혀 Ogham 의 NG 가 nG 로 보이고, Etaoin Shrdlu 의 숫자 표에는 7 이 없어서 원주율 100자리를 쓰는 Huffman Tree 가 더했어요. 뒤집힌 글자는 생각과 달랐어요. 활자 얼굴은 좌우가 뒤집힌 글자인데 막대에는 그 활자를 위아래로 돌려 세워서, Composing Stick 의 막대에서는 위아래만 뒤집혀 p 와 b 가 바뀌고 p 와 q 는 얼굴 쪽에서 본 판에서야 바뀌어요. 아스키에는 윗줄을 긋는 글자가 없어서, Structure ASCII 에서 외곽선이 칸 맨 위로 지나면 짧은 획 둘을 세운 " 가 가장 가까운 글자가 되고 열 자에 한 자꼴로 나와요. 가는 획은 화면이 움직일 때 무너졌어요. Ogham 은 화면이 소수 줄씩 오르자 1 LED 획이 두 줄에 반씩 걸쳐 흐려져서, 화면을 한 줄씩만 올렸어요. 가는 획의 색 노브도 이번에 다시 문턱에 걸렸어요. Huffman Tree 의 Hue 는 가는 선만으로 0.01 이었고 가로대 밑에 흐린 빛을 걸자 0.035 가 됐어요. Composing Stick 은 롤러 아래에 5줄 잉크판을 깔아 0.040 이 됐어요. + +기능 감은 이번에 셋이 글줄의 방향으로 모였어요. Ogham 은 Text 레이어를 반시계로 90도 돌려 오검 유니코드를 돌 모서리처럼 세우기를, Palimpsest 는 글줄 방향을 두어 Lab 에서 친 두 낱말을 직각으로 겹치기를, Composing Stick 은 좌우 뒤집기와 180도 돌리기를 바랐어요. Structure ASCII 는 코드가 Text 레이어의 덮임 비트맵을 읽어 그 반 높이 등고선을 아스키로 다시 쓰기를 바랐고, Huffman Tree 는 Lab 미리보기에서 노브 하나를 다섯 자리로 돌려 새로 시작한 뒤 2~3초의 화면 차이를 재 주는 버튼을 바랐어요. + +68판 뒤에 해 볼 것이에요. 몽골 문자도 세로 줄기 하나에 글자 조각이 양쪽으로 붙고 위에서 아래로 써요. 1204년 테무진이 나이만에서 사로잡은 위구르 서기 타타통아(Tata-tonga)가 위구르 문자를 몽골어에 맞췄다고 전해요. Ogham 의 줄기와 좌우 획, 한 줄씩 오르는 화면을 거꾸로 내려가게 쓰면 돼요. 톨버트 랜스턴(Tolbert Lanston)의 모노타입(1887)은 자판이 em 을 18 단위로 나눈 폭으로 종이 리본에 구멍을 뚫고, 주조기는 리본을 거꾸로 읽어서 줄의 스페이스 폭을 첫 활자보다 먼저 알아요. Composing Stick 의 활자와 스페이스 목록, 줄 맞추기, 줄이 뒤집히는 장면을 그대로 쓰고 막대 대신 리본을 그리면 돼요. 키케로 『국가론』 팔림프세스트(Angelo Mai, 1819)는 아우구스티누스의 시편 주석 아래에서 찾은 라틴어 글이라, Palimpsest 의 두 겹 글과 빛의 띠를 그대로 두고 펌웨어 Matrix Fonts 의 대문자로 밑글만 바꿔 쓸 수 있어요. 섀넌-파노 부호(Robert Fano, 1949)는 Huffman Tree 의 잎과 띠 코드를 그대로 쓰고 나무를 뿌리에서 아래로 쪼개 내려가요. 위에서 나눈 부호가 아래에서 묶은 허프먼보다 길어지는 글이 보이고, 셈해 보니 팬그램은 151비트로 허프먼 148보다 3비트 길었어요. IBM PC(1981)의 코드 페이지 437 에 든 상자 그리기 문자 40자로 같은 외곽선을 맞춰 볼 만해요. 홑줄과 겹줄 상자 문자는 획이 칸 가운데서만 꺾이고 칸 변의 가운데로만 나가서, Structure ASCII 의 marching squares 와 챔퍼 거리에 글자 표만 바꿔 끼우면 아스키 윤곽과 상자 문자 윤곽을 한 노브로 견줄 수 있어요. ### 화장품 팝업 From c390d1e1fa116872cad78c845a56b70afb4ffc03 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 15:14:55 +0000 Subject: [PATCH 23/24] =?UTF-8?q?incoming:=2068=ED=8C=90=20=EC=97=B4=20?= =?UTF-8?q?=EA=B0=9C=EB=A5=BC=20=EB=84=A3=EC=96=B4=20index.html=20?= =?UTF-8?q?=EC=9D=84=20=EB=8B=A4=EC=8B=9C=20=EC=A7=93=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JktahMn4mzzFyveZ8fo --- incoming/pattern-study/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index 5831084d0..5176d29d0 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

패턴 연구

- - + + - + +