From c6c02e8ae0e095a204c2fae87031e1aa7bb2bd47 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 23:23:44 +0000 Subject: [PATCH 01/15] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=EC=97=B0=EA=B5=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=2074?= =?UTF-8?q?=ED=8C=90=20=ED=8C=A8=ED=84=B4=20=EC=97=B4=20=EA=B0=9C=EA=B0=80?= =?UTF-8?q?=20=EC=84=A0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4Jkt9PaeQhb1WkHMuJUh From fde7ed00ba9782dab063cbdb3726735444d3c2b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 23:49:54 +0000 Subject: [PATCH 02/15] =?UTF-8?q?incoming:=20Autostereogram=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4Jkt9PaeQhb1WkHMuJUh --- .../pattern-study/patterns/autostereogram.js | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 incoming/pattern-study/patterns/autostereogram.js diff --git a/incoming/pattern-study/patterns/autostereogram.js b/incoming/pattern-study/patterns/autostereogram.js new file mode 100644 index 000000000..141247a7e --- /dev/null +++ b/incoming/pattern-study/patterns/autostereogram.js @@ -0,0 +1,200 @@ +// ===== Patternflow pattern ===== +// Title: Autostereogram +// Family: type +// Idea: A three-letter word stacked down the panel in capitals 42 LEDs wide and 38 tall is hidden in coloured random dots as a wall-eyed single-image stereogram, every LED copying the dot one period to its left except where a letter pulls that copy 2 LEDs nearer, so the dots look like nothing but noise until a band sweeps down and shows each LED against the LED one whole period to its left, dark where they agree and white where they differ, and the word appears letter by letter in that comparison, glows, fades, and a wipe of fresh dots brings the next word. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: random-dot stereogram (Béla Julesz, Bell System Technical Journal, 1960), single-image random-dot stereogram (Christopher Tyler and Maureen Clarke, made 1979, published at SPIE Stereoscopic Displays and Applications, 1990), after the wallpaper effect (David Brewster, 1844), pattern round 74 +// Round: 74 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Depth=-3..3, Period=11..15, Reveal=0..1 +// +// Knob 1: Hue. Turns the four dot colours round the wheel together over a ground that stays deep indigo. At 0 they are teal, azure, violet and pale gold; at 0.5 rose, amber, lime and blue. The comparison stays warm white. +// Knob 2: Depth. How many LEDs nearer than the period a letter's dots copy, rounded: at 2 (the opening) the word floats in front of the dots for a wall-eyed viewer, at -3 it sinks 3 LEDs behind them, and at 0 the dots are a plain wallpaper with nothing hidden, so the sweep finds no word. +// Knob 3: Period. The wallpaper's repeat, 11 to 15 LEDs: 5.8 repeats across the 64 LEDs at 11, 4.3 at 15. A wider period needs the eyes to diverge further and widens the random strip down the left edge. +// Knob 4: Reveal. Up to 0.1 (the opening 0.06) the comparison shows only in the band, 24 rows tall, that sweeps down at 40 rows a second, 3.8 s a sweep, once for each word; between sweeps the dots carry no trace of the word. Above 0.1 the comparison also shows over the whole panel at that strength: at 0.55 the dots are half dimmed and the word stands half white all the time, at 1 the dots are gone and only the comparison is left. + +// A random-dot stereogram hides a shape in dots that carry no outline at +// all: the shape exists only as a shift between what the left eye and the +// right eye see. Julesz made them as pairs; Tyler and Clarke put both views +// in one picture. Each row starts with a strip of random dots one period +// wide, and every later LED copies the LED one separation to its left. Where +// the depth map is flat the separation is the period P and the row repeats +// exactly. Inside a letter it is P - D, so for eyes diverged by P the dots +// of the letter pair up at a different disparity and the letter stands out +// D LEDs nearer (D > 0) or further (D < 0). +// +// The comparison is the reveal: an LED that copied from P to its left +// always equals the LED P to its left, and inside a letter it copied from +// P - D, so it differs from the LED at P whenever the two dots were drawn +// differently, 70% of the time here. Matching LEDs go dark, differing ones +// light, and the letter appears without any free-viewing. + +const W = 64, H = 128; +const { floor, round, min, max, abs, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const frac = (v) => v - floor(v); + +// Letter boxes: 42 x 38 LEDs from x = 19, three of them at rows 2, 45 and +// 88. The box starts right of the widest separation (15 + 3), so a letter +// never needs a dot left of the panel. +const BX = 19, BW = 42, BH = 38, TOPS = [2, 45, 88]; +// Timing in seconds: the wipe of new dots, the quiet before the sweep, the +// sweep's speed in rows a second, the quiet after, and the glow's decay. +const WIPE = 0.5, PRE = 1.5, V = 40, POST = 2.0, TAU = 0.9, BAND = 24; +const LIT = 0.52; // share of dots that take one of the four colours +const SHADE = 0.025, SH2 = (2 * SHADE) / 255; + +// A bold 7 x 7 capital for each letter, drawn for this pattern. Each cell +// is 6 x 5.43 LEDs; sampling the cells bilinearly and cutting at one half +// rounds the corners and turns the stairs of A, S and Y into slopes. +const FONT = { + A: ["..###..", ".##.##.", "##...##", "##...##", "#######", "##...##", "##...##"], + D: ["#####..", "##..##.", "##...##", "##...##", "##...##", "##..##.", "#####.."], + E: ["#######", "##.....", "##.....", "######.", "##.....", "##.....", "#######"], + F: ["#######", "##.....", "##.....", "######.", "##.....", "##.....", "##....."], + H: ["##...##", "##...##", "##...##", "#######", "##...##", "##...##", "##...##"], + I: [".#####.", "..###..", "..###..", "..###..", "..###..", "..###..", ".#####."], + O: [".#####.", "##...##", "##...##", "##...##", "##...##", "##...##", ".#####."], + R: ["######.", "##...##", "##...##", "######.", "##.##..", "##..##.", "##...##"], + S: [".#####.", "##...##", "##.....", ".#####.", ".....##", "##...##", ".#####."], + T: ["#######", "#######", "..###..", "..###..", "..###..", "..###..", "..###.."], + Y: ["##...##", "##...##", ".##.##.", "..###..", "..###..", "..###..", "..###.."], +}; +const WORDS = ["SEE", "EYE", "AHA", "DOT", "HID", "FAR", "ODD", "TOE"]; + +function cell(g, i, j) { + return i < 0 || j < 0 || i > 6 || j > 6 ? 0 : g[j][i] === "#" ? 1 : 0; +} + +// The depth map of one word: 1 inside a letter, 0 elsewhere. +function wordMask(word) { + const m = new Uint8Array(W * H), cw = BW / 7, ch = BH / 7; + for (let n = 0; n < 3; n++) { + const g = FONT[word[n]], top = TOPS[n]; + for (let y = top; y < top + BH; y++) + for (let x = BX; x < BX + BW; x++) { + const u = (x - BX + 0.5) / cw - 0.5, v = (y - top + 0.5) / ch - 0.5; + const i = floor(u), j = floor(v), fu = u - i, fv = v - j; + const a = cell(g, i, j) * (1 - fu) + cell(g, i + 1, j) * fu; + const b = cell(g, i, j + 1) * (1 - fu) + cell(g, i + 1, j + 1) * fu; + if (a * (1 - fv) + b * fv > 0.5) m[y * W + x] = 1; + } + } + return m; +} + +function hsv(h, s, v, out, o) { + 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[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// New random dots for a row: the dot each LED would take if it had nothing +// to copy, and a shade for each LED's three channels. The shade is the +// LED's own and is never copied. At 2.5% either way the eye cannot tell +// it and the dots still fuse, but no copy is then the same three bytes as +// its source. Without it a compressor finds every repeat and the study's +// complexity reads 0.46 bytes an LED for a field that looks like noise; +// with it, 1.78. +function roll(S, y) { + const o = y * W; + for (let x = 0; x < W; x++) { + S.R[o + x] = Math.random() < LIT ? 1 + floor(Math.random() * 4) : 0; + for (let c = 0; c < 3; c++) S.sh[(o + x) * 3 + c] = floor(Math.random() * 256); + } +} + +// One row of the stereogram, left to right. +function build(S, y) { + const o = y * W, P = S.P, D = S.D, m = S.masks[S.rowWord[y]], R = S.R, I = S.idx; + for (let x = 0; x < W; x++) { + const sep = P - D * m[o + x]; + I[o + x] = x < sep ? R[o + x] : I[o + x - sep]; + } +} + +export function setup(params) { + const S = (params.s = { + P: 11, D: 2, hue: 0, floor: 0, word: 0, ct: 0, wiped: H, + R: new Uint8Array(W * H), sh: new Uint8Array(W * H * 3), idx: new Uint8Array(W * H), + glow: new Float32Array(W * H), cover: new Float32Array(H), rowWord: new Uint8Array(H), + masks: WORDS.map(wordMask), pal: new Float32Array(15), + }); + for (let y = 0; y < H; y++) roll(S, y), build(S, y); + return S; +} + +const CYCLE = PRE + (H + BAND) / V + POST; + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 11, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + const D = round(cl(k[1] == null ? 2 : k[1], -3, 3)), P = round(cl(k[2] || 11, 11, 15)); + S.floor = cl(((k[3] == null ? 0.06 : k[3]) - 0.1) / 0.9, 0, 1); + if (D !== S.D || P !== S.P) { + S.D = D; + S.P = P; + for (let y = 0; y < H; y++) build(S, y); + } + S.ct += d; + if (S.ct >= CYCLE) { + S.ct = 0; + S.word = (S.word + 1) % WORDS.length; + S.wiped = 0; + } + // The wipe: rows take new dots and the new word as its front passes. + const front = min(H, floor((S.ct / WIPE) * H)); + while (S.wiped < front) { + const y = S.wiped++; + S.rowWord[y] = S.word; + roll(S, y); + build(S, y); + } + // The band: each row's share of comparison, with a 3-row soft edge, and + // never less than the Reveal floor. + const yb = -BAND / 2 + (S.ct - PRE) * V, half = BAND / 2, on = S.ct > PRE; + const fall = exp(-d / TAU), I = S.idx, G = S.glow; + for (let y = 0; y < H; y++) { + const c = max(S.floor, on ? cl((half - abs(y + 0.5 - yb)) / 3, 0, 1) : 0); + S.cover[y] = c; + const o = y * W; + for (let x = 0; x < W; x++) { + let g = G[o + x] * fall; + if (c > 0 && x >= S.P && I[o + x] !== I[o + x - S.P] && c > g) g = c; + G[o + x] = g; + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + // The ground and the four dot colours, levels 0..255. + const pal = S.pal, h = S.hue; + hsv(0.7, 0.85, 0.2, pal, 0); + hsv(0.47 + h, 0.9, 0.62, pal, 3); + hsv(0.58 + h, 0.8, 0.66, pal, 6); + hsv(0.77 + h, 0.65, 0.62, pal, 9); + hsv(0.13 + h, 0.5, 0.66, pal, 12); + const I = S.idx, G = S.glow, sh = S.sh; + for (let y = 0; y < H; y++) { + const dim = 1 - 0.9 * S.cover[y], o = y * W; + for (let x = 0; x < W; x++) { + const i = o + x, p = I[i] * 3, g = G[i], j = i * 3; + // Each channel within 2.5% either side of the dot's colour. + let r = pal[p] * (1 - SHADE + sh[j] * SH2) * 255 * dim; + let gg = pal[p + 1] * (1 - SHADE + sh[j + 1] * SH2) * 255 * dim; + let b = pal[p + 2] * (1 - SHADE + sh[j + 2] * SH2) * 255 * dim; + r += g * 255; + gg += g * 236; + b += g * 200; + display.setPixel(x, y, round(cl(r, 0, 255)), round(cl(gg, 0, 255)), round(cl(b, 0, 255))); + } + } +} From 01dc0d836a4eae87dbc9943b823174601ecd9dd9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 23:55:06 +0000 Subject: [PATCH 03/15] =?UTF-8?q?incoming:=20Trajan=20Helix=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4Jkt9PaeQhb1WkHMuJUh --- .../pattern-study/patterns/trajan-helix.js | 295 ++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 incoming/pattern-study/patterns/trajan-helix.js diff --git a/incoming/pattern-study/patterns/trajan-helix.js b/incoming/pattern-study/patterns/trajan-helix.js new file mode 100644 index 000000000..7362717ed --- /dev/null +++ b/incoming/pattern-study/patterns/trajan-helix.js @@ -0,0 +1,295 @@ +// ===== Patternflow pattern ===== +// Title: Trajan Helix +// Family: type +// Idea: A marble column fills the height of the panel and turns slowly under a raking lamp while a helical band winds round it from foot to top, the way the frieze winds round Trajan's Column, but the band carries the column's own base inscription in Roman capitals cut and painted red, full width where the band faces you, squeezed thin toward the column's edges and gone behind it, so as the column turns and the view climbs the line slides along the helix and reads on without end, SENATVS POPVLVSQVE ROMANVS and on to SIT EGESTVS, while the band itself creeps slowly up like a barber's pole and the letters lost from the stone pass as a broken patch with their restoration ghosted in. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the base inscription of Trajan's Column (CIL VI 960, the column dedicated in AD 113 and attributed to Apollodorus of Damascus), the model of the Roman capital, with stroke weights after Edward Catich's flat-brush account in The Origin of the Serif (1968) and the red letters after Pliny's minium for inscriptions (Natural History 33.122); capitals drawn for this pattern as strokes, pattern round 74 +// Round: 74 +// =============================== +// @matrix 64x128 +// @knobs Paint=0..1, Turn=0.2..3, Turns=5..9, Light=0..1 +// +// Knob 1: Paint. The colour in the cut letters: at 0 minium, a vermilion red; 0.1 gold, 0.3 green, 0.6 blue, 0.8 purple, and back to red at 1. The marble stays marble and the lost letters stay bare. +// Knob 2: Turn. How fast the column turns: at 2 the front of the column moves 12 LEDs a second, a letter and two thirds, a full turn takes 13.1 s, the band creeps up half a pitch a turn (12.8 rows at the opening 5 turns) and the whole inscription, 172 letters and 34 stops, passes in 1 minute 53 s; at 0.2 a letter goes by every 6 s and the inscription takes 19 minutes, at 3 two and a half letters go by a second. +// Knob 3: Turns. How many turns of the band stand on the panel: 5 at the start, a pitch of 25.6 rows with 13.6 rows of plain marble between the turns; at 9 the pitch is 14.2 rows and the turns almost touch, 2.2 rows apart, so the panel is a screw of text as the real frieze is a screw of figures. +// Knob 4: Light. Where the lamp stands round the column, 30 degrees above it: at the opening 0.06 it rakes in from 57 degrees to the left, so the left of the shaft is lit, the right falls into shadow and every cut letter shows a lit wall and a dark one; at 0.5 the lamp is in front and the column goes flat; at 1 it rakes from 65 degrees to the right and the shading turns over. + +// The column is a cylinder of radius 25 LEDs seen from far off, so the LED +// x across the shaft is R sin(beta) for the angle beta round from the front, +// and a stroke on the stone shrinks by cos(beta) as it turns toward the edge. +// The band is a helix: its top edge stands at Y0 - P beta / 2 pi, P the +// pitch, and the text coordinate along it is the arc R (beta - 2 pi m) for +// the m-th turn counted up from the panel's foot. Turning the column by phi +// slides the text along the band by R phi. Turning alone would make the +// band climb a whole pitch a turn, the barber's pole, and the panel would +// show the same stretch of stone for ever; so the view climbs too, half a +// pitch a turn, new stone and the next words come in at the top, and the +// band still creeps up by the other half. Every letter is fixed to the +// stone. +// +// The capitals are strokes (straight or elliptic arcs) in a box 8 LEDs tall. +// Their weights follow a flat brush held with its edge 20 degrees above the +// horizontal, Catich's account of how the letters were laid out before the +// cutting: uprights and down-right strokes come out thick (1.8 LEDs), the +// horizontals and down-left strokes thin (0.85), and the N's uprights are +// set thin by hand as on the stone. Each LED's coverage comes from its +// distance to the strokes, measured on the stone and turned into LEDs +// through the squeeze cos(beta), so letters thin out smoothly at the edges. + +const W = 64, H = 128; +const { floor, round, min, max, abs, sqrt, sin, cos, asin, atan2, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const frac = (v) => v - floor(v); +function hsv(h, s, v) { + 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); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +const CX = 32, R = 25; +// The band: 12 rows, a shadowed seam on top, the letters 8 rows tall from +// row 2, a lit lip at the bottom. +const BAND = 12, CAP = 8, TOP = 2, Y0 = 118; +const HAIR = 0.85, THICK = 1.8, GAP = 1.7; +const ELEV = (30 * PI) / 180, FOLLOW = 0.5; + +// ---- The capitals, in units of the cap height, y down. ["l", x0, y0, x1, +// y1, wt] a straight stroke, ["a", cx, cy, rx, ry, from, to, wt] an +// elliptic arc between two angles in degrees (0 right, 90 down), ["d", x, +// y] the stop between words. wt left out takes the brush's weight for the +// stroke's direction; 0 is a hairline. Serifs are short hairlines at the +// ends of the stems. Only the 19 letters the inscription uses are drawn. +const s0 = (x, y, a = 0.11) => ["l", x - a, y, x + a, y, 0]; +const GLYPHS = { + A: [0.86, [["l", 0.43, 0, 0.05, 1], ["l", 0.43, 0, 0.82, 1], ["l", 0.21, 0.64, 0.66, 0.64, 0], s0(0.05, 1), s0(0.82, 1)]], + B: [0.58, [["l", 0.1, 0, 0.1, 1], ["l", 0.1, 0, 0.32, 0, 0], ["a", 0.32, 0.24, 0.2, 0.24, -90, 90], ["l", 0.1, 0.48, 0.34, 0.48, 0], + ["a", 0.34, 0.74, 0.22, 0.26, -90, 90], ["l", 0.1, 1, 0.34, 1, 0], s0(0.04, 0, 0.06), s0(0.04, 1, 0.06)]], + C: [0.82, [["a", 0.46, 0.5, 0.44, 0.5, -48, -312], ["l", 0.76, 0.06, 0.76, 0.2, 0]]], + D: [0.88, [["l", 0.1, 0, 0.1, 1], ["l", 0.1, 0, 0.42, 0, 0], ["l", 0.1, 1, 0.42, 1, 0], ["a", 0.42, 0.5, 0.42, 0.5, -90, 90], s0(0.04, 0, 0.06), s0(0.04, 1, 0.06)]], + E: [0.5, [["l", 0.1, 0, 0.1, 1], ["l", 0.1, 0, 0.47, 0, 0], ["l", 0.1, 0.5, 0.42, 0.5, 0], ["l", 0.1, 1, 0.5, 1, 0], + ["l", 0.47, 0, 0.47, 0.12, 0], ["l", 0.5, 1, 0.5, 0.86, 0], s0(0.04, 0, 0.06), s0(0.04, 1, 0.06)]], + F: [0.5, [["l", 0.1, 0, 0.1, 1], ["l", 0.1, 0, 0.48, 0, 0], ["l", 0.1, 0.5, 0.42, 0.5, 0], ["l", 0.48, 0, 0.48, 0.12, 0], s0(0.04, 0, 0.06), s0(0.12, 1, 0.14)]], + G: [0.86, [["a", 0.46, 0.5, 0.44, 0.5, -48, -340], ["l", 0.84, 0.56, 0.84, 0.96], ["l", 0.76, 0.06, 0.76, 0.2, 0]]], + I: [0.24, [["l", 0.12, 0, 0.12, 1], s0(0.12, 0), s0(0.12, 1)]], + L: [0.5, [["l", 0.1, 0, 0.1, 1], ["l", 0.1, 1, 0.5, 1, 0], ["l", 0.5, 1, 0.5, 0.86, 0], s0(0.12, 0, 0.14), s0(0.04, 1, 0.06)]], + M: [1.0, [["l", 0.15, 0, 0.05, 1], ["l", 0.15, 0, 0.5, 1], ["l", 0.85, 0, 0.5, 1], ["l", 0.85, 0, 0.95, 1], s0(0.05, 1), s0(0.95, 1)]], + N: [0.82, [["l", 0.08, 0, 0.08, 1, 0], ["l", 0.74, 0, 0.74, 1, 0], ["l", 0.08, 0, 0.74, 1], s0(0.08, 1), s0(0.74, 0)]], + O: [0.94, [["a", 0.47, 0.5, 0.46, 0.5, 0, 360]]], + P: [0.56, [["l", 0.1, 0, 0.1, 1], ["l", 0.1, 0, 0.32, 0, 0], ["a", 0.32, 0.26, 0.22, 0.26, -90, 90], ["l", 0.1, 0.52, 0.32, 0.52, 0], s0(0.04, 0, 0.06), s0(0.12, 1, 0.14)]], + Q: [0.94, [["a", 0.47, 0.5, 0.46, 0.5, 0, 360], ["l", 0.52, 0.94, 1.1, 1.1]]], + R: [0.66, [["l", 0.1, 0, 0.1, 1], ["l", 0.1, 0, 0.32, 0, 0], ["a", 0.32, 0.25, 0.22, 0.25, -90, 90], ["l", 0.1, 0.5, 0.32, 0.5, 0], + ["l", 0.3, 0.5, 0.62, 1], s0(0.04, 0, 0.06), s0(0.12, 1, 0.14), s0(0.66, 1, 0.06)]], + S: [0.5, [["l", 0.44, 0.06, 0.44, 0.18, 0], ["a", 0.26, 0.25, 0.2, 0.25, -30, -270], ["a", 0.24, 0.75, 0.22, 0.25, -90, 90], ["l", 0.06, 0.82, 0.06, 0.94, 0]]], + T: [0.72, [["l", 0, 0, 0.72, 0, 0], ["l", 0.36, 0, 0.36, 1], ["l", 0, 0, 0, 0.12, 0], ["l", 0.72, 0, 0.72, 0.12, 0], s0(0.36, 1, 0.14)]], + V: [0.84, [["l", 0.04, 0, 0.42, 1], ["l", 0.8, 0, 0.42, 1], s0(0.04, 0), s0(0.8, 0)]], + X: [0.74, [["l", 0.06, 0, 0.68, 1], ["l", 0.66, 0, 0.08, 1], s0(0.06, 0), s0(0.66, 0), s0(0.08, 1), s0(0.68, 1)]], + "·": [0.2, [["d", 0.1, 0.55]]], +}; + +// The inscription, its six lines run together, stops between the words as +// on the stone. Square brackets mark the letters lost with a piece of the +// last line, read here TANT[IS OPER]IBVS; editions differ by a letter at +// either end of the gap (TAN[TIS OPE]RIBVS in one). +const TEXT = + "SENATVS·POPVLVSQVE·ROMANVS·IMP·CAESARI·DIVI·NERVAE·F·NERVAE·TRAIANO·AVG·GERM·DACICO·PONTIF·MAXIMO·TRIB·POT·XVII·IMP·VI·COS·VI·P·P·" + + "AD·DECLARANDVM·QVANTAE·ALTITVDINIS·MONS·ET·LOCVS·TANT[IS·OPER]IBVS·SIT·EGESTVS"; + +// The brush: weight 1 for an upright, about 0.95 down-right, 0.13 +// down-left, 0 across. +const NIB = (-20 * PI) / 180; +const brush = (dx, dy) => cl((abs(sin(atan2(dy, dx) - NIB)) - 0.34) / 0.6, 0, 1); + +// Each glyph as segments in LEDs: x0, y0, x1, y1, half width. +function build(parts) { + const seg = []; + const push = (x0, y0, x1, y1, wt) => { + const w = wt == null ? brush(x1 - x0, y1 - y0) : wt; + seg.push(x0 * CAP, y0 * CAP, x1 * CAP, y1 * CAP, (HAIR + (THICK - HAIR) * w) / 2); + }; + for (const p of parts) { + if (p[0] === "l") push(p[1], p[2], p[3], p[4], p[5]); + else if (p[0] === "d") seg.push(p[1] * CAP, p[2] * CAP, p[1] * CAP, p[2] * CAP, 0.7); + else { + const [, cx, cy, rx, ry, a0, a1, wt] = p, n = max(3, round(abs(a1 - a0) / 20)); + for (let i = 0; i < n; i++) { + const t0 = ((a0 + ((a1 - a0) * i) / n) * PI) / 180, t1 = ((a0 + ((a1 - a0) * (i + 1)) / n) * PI) / 180; + push(cx + rx * cos(t0), cy + ry * sin(t0), cx + rx * cos(t1), cy + ry * sin(t1), wt); + } + } + } + return new Float32Array(seg); +} + +// Lay the text out along the band: each letter's start, width, segments and +// whether it is lost. A wider gap closes the round before it starts again. +function layout() { + const shapes = {}; + for (const ch in GLYPHS) shapes[ch] = build(GLYPHS[ch][1]); + const letters = []; + let x = 0, lost = false; + for (const ch of TEXT) { + if (ch === "[" || ch === "]") { + lost = ch === "["; + continue; + } + const w = GLYPHS[ch][0] * CAP; + letters.push({ x, w, seg: shapes[ch], lost }); + x += w + GAP; + } + const len = x + 14; + // at[i]: the first letter whose right edge passes arc i, for a quick find. + const at = new Int16Array(floor(len) + 1); + for (let i = 0, k = 0; i <= floor(len); i++) { + while (k < letters.length && letters[k].x + letters[k].w < i) k++; + at[i] = min(k, letters.length - 1); + } + // The lost stretch, a little wider than its letters. + let lost0 = Infinity, lost1 = -Infinity; + for (const g of letters) if (g.lost) (lost0 = min(lost0, g.x - 1.2)), (lost1 = max(lost1, g.x + g.w + 1.2)); + return { letters, len, at, lost0, lost1 }; +} + +// Marble: a vein field on the stone, 160 columns round by 256 rows, +// periodic both ways, from wrapped value noise and a sine through it. +const TW = 160, TH = 256; +function marble() { + const tex = new Float32Array(TW * TH); + const oct = [[10, 16, 1], [20, 32, 0.5], [40, 64, 0.25]].map(([nx, ny, a]) => { + const g = new Float32Array(nx * ny); + for (let i = 0; i < g.length; i++) g[i] = Math.random(); + return { nx, ny, a, g }; + }); + const sm = (u) => u * u * (3 - 2 * u); + for (let y = 0; y < TH; y++) + for (let x = 0; x < TW; x++) { + let n = 0; + for (const o of oct) { + const fx = (x / TW) * o.nx, fy = (y / TH) * o.ny, ix = floor(fx), iy = floor(fy), ux = sm(fx - ix), uy = sm(fy - iy); + const a = o.g[(iy % o.ny) * o.nx + (ix % o.nx)], b = o.g[(iy % o.ny) * o.nx + ((ix + 1) % o.nx)]; + const c = o.g[((iy + 1) % o.ny) * o.nx + (ix % o.nx)], d = o.g[((iy + 1) % o.ny) * o.nx + ((ix + 1) % o.nx)]; + n += o.a * (a + (b - a) * ux + (c - a + (d - c - b + a) * ux) * uy); + } + const m = abs(sin(TAU * (2 * x / TW + y / TH) + 5 * n)); + tex[y * TW + x] = (1 - m) ** 6 * 0.55 + n * 0.12; + } + return tex; +} + +export function setup(params) { + const L = layout(); + params.s = { L, tex: marble(), phi: 0, shift: 0, band: 0, climb: 0, paint: 0, turn: 2, turns: 5, light: 0.06, rough: new Float32Array(64) }; + for (let i = 0; i < 64; i++) params.s.rough[i] = Math.random(); + return params.s; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const s = params.s, k = input.knobValues || [0, 2, 5, 0.06]; + dt = cl(dt || 0, 0, 0.1); + s.paint = cl(k[0] || 0, 0, 1); + s.turn = cl(k[1] == null ? 2 : k[1], 0.2, 3); + s.turns = cl(k[2] == null ? 5 : k[2], 5, 9); + s.light = cl(k[3] == null ? 0.06 : k[3], 0, 1); + // 6 LEDs a second of arc at the front for each unit of Turn. The turn + // slides the text along the band; the view climbs FOLLOW of the band's + // rise, so the band itself creeps up the rest, a barber pole slowed down. + const dphi = (6 * s.turn * dt) / R, P = H / s.turns; + s.phi = (s.phi + dphi) % TAU; + s.shift = (s.shift + R * dphi) % s.L.len; + s.band += ((1 - FOLLOW) * P * dphi) / TAU; + s.climb = (s.climb + (FOLLOW * P * dphi) / TAU) % TH; +} + +// Coverage of the letters at one LED: (u, v) its place on the band in LEDs, +// sq the squeeze cos(beta). Returns coverage and the outward direction of +// the nearest stroke edge (for the walls of the cut) in out. +function ink(L, u, v, sq, out) { + const { letters, at } = L; + const pad = 2 / sq + 1; + let best = 0, nx = 0, ny = 0; + const i0 = at[cl(floor(u - pad), 0, at.length - 1)], i1 = at[cl(floor(u + pad), 0, at.length - 1)]; + for (let i = i0; i <= i1; i++) { + const g = letters[i], seg = g.seg, lu = u - g.x, lv = v; + for (let j = 0; j < seg.length; j += 5) { + const ax = seg[j], ay = seg[j + 1], bx = seg[j + 2] - ax, by = seg[j + 3] - ay, hw = seg[j + 4]; + const bb = bx * bx + by * by, t = bb > 0 ? cl(((lu - ax) * bx + (lv - ay) * by) / bb, 0, 1) : 0; + let dx = lu - ax - t * bx, dy = lv - ay - t * by; + const d = sqrt(dx * dx + dy * dy); + if (d - hw > pad) continue; + if (d > 1e-4) (dx /= d), (dy /= d); + else (dx = 0), (dy = 0); + // The gradient of the distance in LEDs on the panel: across the + // squeeze an arc step is sq LEDs wide. + const gx = dx / sq, gl = sqrt(gx * gx + dy * dy) || 1; + const c = min(0.5 - (d - hw) / gl, (2 * hw) / gl, 1); + if (c > best) (best = c), (nx = dx), (ny = dy), (out[2] = g.lost ? 1 : 0), (out[3] = d / hw); + } + } + out[0] = nx; + out[1] = ny; + return best; +} + +export function draw(display, params) { + const s = params.s; + if (!s) return; + const L = s.L, tex = s.tex, P = H / s.turns, phi = s.phi; + const al = ((-65 + 130 * s.light) * PI) / 180, ce = cos(ELEV), se = sin(ELEV); + // The light and the half vector for a little sheen. + const lx = sin(al) * ce, lz = cos(al) * ce, hx = lx, hy = -se, hz = lz + 1, hl = sqrt(hx * hx + hy * hy + hz * hz); + const paint = hsv(0.02 + s.paint, 0.86, 1); + const out = [0, 0, 0, 0]; + const climb = s.climb, band = s.band, shift = s.shift; + for (let x = 0; x < W; x++) { + const sx = (x + 0.5 - CX) / R, edge = cl(R - abs(x + 0.5 - CX) + 0.5, 0, 1); + const beta = asin(cl(sx, -1, 1)), sq = max(0.05, cos(beta)); + const nX = sin(beta), nZ = cos(beta); + const lam = max(0, nX * lx + nZ * lz), spec = max(0, (nX * hx + nZ * hz) / hl) ** 30; + const shade = 0.13 + 0.92 * lam; + // The lamp's pull along the surface, for the walls of the cuts. + const tu = sin(al - beta) * ce, tv = -se; + const us = frac((beta + phi) / TAU) * TW; + const qb = (P * beta) / TAU + band; + for (let y = 0; y < H; y++) { + const sb = 14 + (y / H) * 8; + if (edge <= 0) { + display.setPixel(x, y, 5, 7, round(sb)); + continue; + } + // The stone under this LED. + const ys = y - climb, vy = ((floor(ys) % TH) + TH) % TH; + const vein = tex[vy * TW + (floor(us) % TW)]; + let r = 236 - 70 * vein, gr = 230 - 66 * vein, b = 218 - 50 * vein; + // Which turn of the band, and where in it. + const q = y + 0.5 - Y0 + qb, m = floor(q / P), v = q - m * P; + let k = shade, lr = 0, lg = 0, lb = 0, c = 0; + if (v < BAND) { + // The band is a shallow sunk ribbon: shadow under its top edge, a + // lit lip at its foot. + k *= v < 1 ? 0.5 + 0.2 * (1 - se) : v >= BAND - 1 ? 1.18 : 0.86; + let u = R * (beta - TAU * m) + shift; + u = ((u % L.len) + L.len) % L.len; + // The broken patch where the piece is lost: rough, darker stone. + if (u > L.lost0 && u < L.lost1) k *= 0.5 + 0.22 * s.rough[(floor(u * 0.7) + 7 * floor(v * 0.6)) & 63]; + c = ink(L, u, v - TOP, sq, out); + if (c > 0) { + // A wall of the cut facing the lamp is lit, the one facing away + // dark; the stroke's middle is the floor of the V. A lost letter + // is only the restorer's pale outline on the broken patch. + const kk = shade * (1 - 0.55 * (out[0] * tu + out[1] * tv) * cl(out[3], 0, 1)); + if (out[2]) (lr = r * 1.05 * shade), (lg = gr * 1.05 * shade), (lb = b * 1.08 * shade), (c *= 0.75); + else (lr = 255 * paint[0] * kk), (lg = 255 * paint[1] * kk), (lb = 255 * paint[2] * kk); + } + } + r = r * k + 120 * spec; + gr = gr * k + 118 * spec; + b = b * k + 112 * spec; + if (c > 0) (r += (lr - r) * c), (gr += (lg - gr) * c), (b += (lb - b) * c); + const sky = 1 - edge; + display.setPixel(x, y, round(cl(r * edge + 5 * sky, 0, 255)), round(cl(gr * edge + 7 * sky, 0, 255)), round(cl(b * edge + sb * sky, 0, 255))); + } + } +} From d64d7c66b5a062f17d064c59a19a0a7298298bb1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 23:57:00 +0000 Subject: [PATCH 04/15] =?UTF-8?q?incoming:=20Lambda=20Diagrams=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4Jkt9PaeQhb1WkHMuJUh --- .../pattern-study/patterns/lambda-diagrams.js | 282 ++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 incoming/pattern-study/patterns/lambda-diagrams.js diff --git a/incoming/pattern-study/patterns/lambda-diagrams.js b/incoming/pattern-study/patterns/lambda-diagrams.js new file mode 100644 index 000000000..0b7ea7fd8 --- /dev/null +++ b/incoming/pattern-study/patterns/lambda-diagrams.js @@ -0,0 +1,282 @@ +// ===== Patternflow pattern ===== +// Title: Lambda Diagrams +// Family: line +// Idea: Church-numeral arithmetic, SUCC 2, PLUS 2 3, TIMES 2 3, EXP 2 3, TIMES 4 4 and EXP 2 4, are drawn as Tromp's lambda diagrams, every abstraction a horizontal bar, every variable a vertical line hanging from the bar that binds it and every application a link joining the leftmost lines of function and argument, and they are reduced one normal-order beta step at a time, the redex's bar with its variables lighting warm white and its argument gold before the picture cross-fades to the next term laid out afresh to fit the panel, until the normal form, a staircase comb with one tooth for each count, holds while a pulse runs down it and the next expression begins. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: lambda calculus and Church numerals (Alonzo Church, An Unsolvable Problem of Elementary Number Theory, 1936), de Bruijn indices (Nicolaas Govert de Bruijn, 1972), lambda diagrams (John Tromp), pattern round 74 +// Round: 74 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Expression=0..6, Glow=0..1 +// +// Knob 1: Hue. Turns the palette round the wheel. At 0 the outermost bar and its variables are cyan and each deeper binder steps 0.09 round the wheel through blue and violet to magenta, links are pale, and a redex lights warm white and gold; at 0.5 the binders run red, orange, yellow and green and the redex lights pale blue and blue. +// Knob 2: Speed. At 2 a step takes 1.25 s: 0.25 s showing the term, 0.55 s lighting the redex and 0.45 s cross-fading to the result. The step times scale as 2 / Speed, so at 0.2 a step takes 12.5 s and at 3 it takes 0.83 s; the 2.6 s hold on the normal form stays as it is. +// Knob 3: Expression. 0 (the opening) runs the six expressions in turn; 1 to 6 repeats one: SUCC 2 (3 steps to 3), PLUS 2 3 (6 steps to 5), TIMES 2 3 (7 steps to 6), EXP 2 3 (16 steps to 8), TIMES 4 4 (11 steps to 16) and EXP 2 4 (32 steps to 16), whose widest term is 27 variables across, drawn at 2 LEDs a column. +// Knob 4: Glow. A halo of the lines blurred over 5 x 5 LEDs is added on top, 0.35 times at 0, 0.48 at the opening 0.06 and 2.5 times at 1, where the lines swim in their own colour and the 1-LED lines of the big terms, a pitch of 2 apart, run together into bands. + +// A lambda term is variables, abstractions (lambda x. body) and +// applications (f a). Church (1936) wrote the number n as the term that +// applies f n times, lambda f. lambda x. f (f ... (f x)), and arithmetic as terms +// that take numerals apart and put them together: PLUS m n f x = m f (n f x). +// A beta step replaces (lambda x. body) a by the body with a in place of x; +// normal order always takes the leftmost, outermost such redex, and reaches +// the normal form whenever there is one. +// +// Terms are kept with de Bruijn indices: a variable is the number of +// binders between it and its own, so no name ever needs renaming. +// +// Tromp's diagram lays a term out on a grid. Each variable takes a column +// and is a vertical line from its binder's bar down to where it is used. An +// abstraction is a bar on its own row across its body's columns. An +// application puts function and argument side by side and joins their +// leftmost lines one row below the deeper of the two, the function's line +// carrying on down. The numeral n becomes two bars over a staircase of n +// links: a comb. + +const W = 64, H = 128; +const { floor, round, min, max, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const frac = (v) => v - floor(v); +const smooth = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +function hsv(h, s, v) { + 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); + 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]; +} + +// ---- Terms: { t: 0, i } a variable with its de Bruijn index (0 = the +// nearest binder), { t: 1, b } an abstraction, { t: 2, f, a } an application. +const Var = (i) => ({ t: 0, i }), Lam = (b) => ({ t: 1, b }), App = (f, a) => ({ t: 2, f, a }); +const ap = (...xs) => xs.reduce((f, a) => App(f, a)); +function num(n) { + let b = Var(0); + for (let k = 0; k < n; k++) b = App(Var(1), b); + return Lam(Lam(b)); +} +const SUCC = Lam(Lam(Lam(App(Var(1), ap(Var(2), Var(1), Var(0)))))); // n f x. f (n f x) +const PLUS = Lam(Lam(Lam(Lam(ap(Var(3), Var(1), ap(Var(2), Var(1), Var(0))))))); // m n f x. m f (n f x) +const TIMES = Lam(Lam(Lam(App(Var(2), App(Var(1), Var(0)))))); // m n f. m (n f) +const EXP = Lam(Lam(App(Var(0), Var(1)))); // m n. n m, which is m to the n +const EXPRS = [ + ap(SUCC, num(2)), ap(PLUS, num(2), num(3)), ap(TIMES, num(2), num(3)), + ap(EXP, num(2), num(3)), ap(TIMES, num(4), num(4)), ap(EXP, num(2), num(4)), +]; + +function shift(t, d, c) { + return t.t === 0 ? (t.i >= c ? Var(t.i + d) : t) : t.t === 1 ? Lam(shift(t.b, d, c + 1)) : App(shift(t.f, d, c), shift(t.a, d, c)); +} +function subst(t, j, s) { + return t.t === 0 ? (t.i === j ? s : t) : t.t === 1 ? Lam(subst(t.b, j + 1, shift(s, 1, 0))) : App(subst(t.f, j, s), subst(t.a, j, s)); +} +// One normal-order step, or null at the normal form. +function step(t) { + if (t.t === 2) { + if (t.f.t === 1) return shift(subst(t.f.b, 0, shift(t.a, 1, 0)), -1, 0); + const f = step(t.f); + if (f) return App(f, t.a); + const a = step(t.a); + return a ? App(t.f, a) : null; + } + if (t.t === 1) { + const b = step(t.b); + return b ? Lam(b) : null; + } + return null; +} +const nodes = (t) => (t.t === 0 ? 1 : t.t === 1 ? 1 + nodes(t.b) : 1 + nodes(t.f) + nodes(t.a)); + +// ---- Layout in grid units. Segments: k 0 a bar (row r, columns c0..c1), +// 1 a variable (column c, rows r0..r1), 2 a link (row r, columns c0..c1). +// d is the depth of the binder a bar or variable belongs to. role marks the +// first redex met in the walk, which is the normal-order one: 1 its bar and +// the variables it binds, 2 its argument, 3 its link. +function lay(t, c0, r0, env, ctx, redexLam) { + if (t.t === 0) { + const b = env[env.length - 1 - t.i] || { row: 0, d: 0, role: 0 }; + const seg = { k: 1, c: c0, r0: b.row, r1: r0, d: b.d, role: ctx.role || b.role }; + ctx.segs.push(seg); + return { w: 1, h: 0, left: seg }; + } + if (t.t === 1) { + const bar = { k: 0, c0, c1: c0, r: r0, d: env.length, role: ctx.role || (redexLam ? 1 : 0) }; + ctx.segs.push(bar); + env.push({ row: r0, d: env.length, role: redexLam ? 1 : 0 }); + const B = lay(t.b, c0, r0 + 1, env, ctx, false); + env.pop(); + bar.c1 = c0 + B.w - 1; + return { w: B.w, h: B.h + 1, left: B.left }; + } + const redex = t.f.t === 1 && !ctx.found; + if (redex) ctx.found = true; + const F = lay(t.f, c0, r0, env, ctx, redex); + const outer = ctx.role; + if (redex) ctx.role = 2; + const A = lay(t.a, c0 + F.w, r0, env, ctx, false); + ctx.role = outer; + const h = max(F.h, A.h) + 1, row = r0 + h - 1; + F.left.r1 = row; + A.left.r1 = row; + ctx.segs.push({ k: 2, c0: F.left.c, c1: A.left.c, r: row, d: 0, role: redex ? 3 : outer }); + return { w: F.w + A.w, h, left: F.left }; +} + +// The term in LED rectangles: the widest column pitch from 8 down to 2 that +// fits 62 LEDs, a row pitch up to twice that (the panel is tall) and at most +// 16 that fits 124, lines 2 LEDs thick from a pitch of 5. A bar reaches past +// its outer columns by e, which leaves at least one dark LED between two +// bars side by side on a row: none at pitch 2 and 3, 2 at pitch 7 and 8. +// null if the term cannot fit at 2 x 2. +function diagram(t) { + const ctx = { segs: [], role: 0, found: false }; + const R = lay(t, 0, 0, [], ctx, false); + R.left.r1 = R.h; // the root's line runs on one row: the term's output + const w = R.w, h = R.h; + let gx = 8; + const ext = (g) => floor((g - (g >= 5 ? 2 : 1) - 1) / 2); + const span = (g) => (w - 1) * g + 2 * ext(g) + (g >= 5 ? 2 : 1); + while (gx > 2 && span(gx) > 62) gx--; + if (span(gx) > 62) return null; + const th = gx >= 5 ? 2 : 1, e = ext(gx); + let gy = min(16, 2 * gx); + while (gy > 2 && h * gy + th > 124) gy--; + if (h * gy + th > 124) return null; + const ox = round((W - span(gx)) / 2) + e, oy = round((H - h * gy - th) / 2); + const X = (c) => ox + c * gx, Y = (r) => oy + r * gy; + const rects = ctx.segs.map((s) => + s.k === 0 ? [X(s.c0) - e, Y(s.r), X(s.c1) + e + th - 1, Y(s.r) + th - 1, s] + : s.k === 1 ? [X(s.c), Y(s.r0), X(s.c) + th - 1, Y(s.r1) + th - 1, s] + : [X(s.c0), Y(s.r), X(s.c1) + th - 1, Y(s.r) + th - 1, s]); + return { rects, redex: ctx.found, top: oy, bottom: Y(h) + th }; +} + +// Every term of one reduction, laid out, with a cap on steps and size so a +// term that blows up is cut off rather than drawn. +const MAX_STEPS = 60, MAX_NODES = 600; +function reduce(t0) { + const out = []; + for (let t = t0, n = 0; t && n <= MAX_STEPS; t = step(t), n++) { + const dg = nodes(t) <= MAX_NODES ? diagram(t) : null; + if (!dg) return null; + out.push(dg); + if (!dg.redex) return out; + } + return null; +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, glow: 0.06, pick: -1, ex: -1, terms: null, k: 0, u: 0, phase: 0, pt: 0, fade: 1, + a: new Float32Array(W * H * 3), b: new Float32Array(W * H * 3), out: new Float32Array(W * H * 3), tmp: new Float32Array(W * H * 3), + }; + return params.s; +} + +function start(S, ex) { + for (let n = 0; n < EXPRS.length; n++, ex = (ex + 1) % EXPRS.length) { + const terms = reduce(EXPRS[ex]); + if (terms) return void Object.assign(S, { ex, terms, k: 0, phase: 0, pt: 0 }); + } +} + +// Phases at Speed 2: 0 show the term 0.25 s, 1 light the redex 0.55 s, 2 +// cross-fade 0.45 s; at the normal form 3 hold 2.6 s and 4 fade out 0.5 s. +const T_SHOW = 0.25, T_HI = 0.55, T_MORPH = 0.45, T_HOLD = 2.6, T_OUT = 0.5; + +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); + if (!S.terms) start(S, 0); + S.hue = k[0] || 0; + S.speed = cl(k[1] == null ? 2 : k[1], 0.2, 3); + S.glow = cl(k[3] == null ? 0.06 : k[3], 0, 1); + const pick = round(cl(k[2] || 0, 0, 6)); + if (pick !== S.pick) (S.pick = pick), start(S, pick ? pick - 1 : 0), (S.fade = 0); + const sc = 2 / S.speed; + S.pt += d; + S.fade = min(1, S.fade + d / 0.4); + const last = S.k >= S.terms.length - 1; + if (S.phase === 0 && S.pt >= T_SHOW * sc) (S.phase = last ? 3 : 1), (S.pt = 0); + else if (S.phase === 1 && S.pt >= T_HI * sc) (S.phase = 2), (S.pt = 0); + else if (S.phase === 2 && S.pt >= T_MORPH * sc) (S.k++, (S.phase = 0), (S.pt = 0)); + else if (S.phase === 3 && S.pt >= T_HOLD) (S.phase = 4), (S.pt = 0); + else if (S.phase === 4 && S.pt >= T_OUT * min(sc, 1)) { + start(S, S.pick ? S.pick - 1 : (S.ex + 1) % EXPRS.length); + S.fade = 0; + } +} + +// Paint one diagram into buf at weight w; hl lights its redex, pulse (a row) +// runs a bright band down a held normal form. +function paint(S, buf, dg, w, hl, pulse) { + const base = 0.52 + S.hue; + const roleCol = [null, hsv(base + 0.6, 0.3, 1), hsv(base + 0.62, 0.9, 1), [1, 1, 1]]; + const link = hsv(base + 0.25, 0.15, 0.7); + for (const [x0, y0, x1, y1, s] of dg.rects) { + let c = s.k === 2 ? link : hsv(base + 0.09 * (s.d % 5), 0.78, 1); + if (hl > 0) { + const r = roleCol[s.role]; + c = r ? [c[0] + (r[0] - c[0]) * hl, c[1] + (r[1] - c[1]) * hl, c[2] + (r[2] - c[2]) * hl] : [c[0] * (1 - 0.7 * hl), c[1] * (1 - 0.7 * hl), c[2] * (1 - 0.7 * hl)]; + } + for (let y = max(0, y0); y <= min(H - 1, y1); y++) { + const p = pulse == null ? w : w * (0.55 + 0.75 * exp(-(((y - pulse) / 7) ** 2))); + for (let x = max(0, x0); x <= min(W - 1, x1); x++) { + const j = (y * W + x) * 3; + for (let q = 0; q < 3; q++) if (c[q] * p > buf[j + q]) buf[j + q] = c[q] * p; + } + } + } +} + +// A 5-wide box blur across and then down. +function blur(src, dst, tmp) { + for (let y = 0; y < H; y++) + for (let c = 0; c < 3; c++) { + let s = 0; + for (let x = -2; x < W + 2; x++) { + if (x + 2 < W) s += src[(y * W + x + 2) * 3 + c]; + if (x - 3 >= 0) s -= src[(y * W + x - 3) * 3 + c]; + if (x >= 0 && x < W) tmp[(y * W + x) * 3 + c] = s / 5; + } + } + for (let x = 0; x < W; x++) + for (let c = 0; c < 3; c++) { + let s = 0; + for (let y = -2; y < H + 2; y++) { + if (y + 2 < H) s += tmp[((y + 2) * W + x) * 3 + c]; + if (y - 3 >= 0) s -= tmp[((y - 3) * W + x) * 3 + c]; + if (y >= 0 && y < H) dst[(y * W + x) * 3 + c] = s / 5; + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.terms) return; + const sc = 2 / S.speed, A = S.a, B = S.b, O = S.out; + A.fill(0); + B.fill(0); + const cur = S.terms[S.k], fade = smooth(S.fade); + if (S.phase === 0) paint(S, A, cur, fade, 0, null); + else if (S.phase === 1) paint(S, A, cur, fade, smooth(S.pt / (0.35 * T_HI * sc)), null); + else if (S.phase === 2) { + const u = smooth(S.pt / (T_MORPH * sc)); + paint(S, A, cur, 1 - u, 1, null); + paint(S, B, S.terms[S.k + 1], u, 0, null); + } else { + // The pulse runs from the top bar to the end of the output line in 1.3 s, twice. + const pulse = cur.top - 10 + ((S.pt % 1.3) / 1.3) * (cur.bottom - cur.top + 20); + const out = S.phase === 4 ? 1 - smooth(S.pt / (T_OUT * min(sc, 1))) : 1; + paint(S, A, cur, out, 0, S.phase === 3 && S.pt < 2.6 ? pulse : null); + } + for (let i = 0; i < A.length; i++) A[i] += B[i]; + blur(A, O, S.tmp); + const g = 0.35 + 2.15 * S.glow; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const j = (y * W + x) * 3; + display.setPixel(x, y, round(cl((A[j] + g * O[j]) * 255, 0, 255)), round(cl((A[j + 1] + g * O[j + 1]) * 255, 0, 255)), round(cl((A[j + 2] + g * O[j + 2]) * 255, 0, 255))); + } +} From 8176a5595997cad11a82e0d4c35df44b6faa4fa3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 23:58:52 +0000 Subject: [PATCH 05/15] =?UTF-8?q?incoming:=20Eye=20Chart=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4Jkt9PaeQhb1WkHMuJUh --- incoming/pattern-study/patterns/eye-chart.js | 393 +++++++++++++++++++ 1 file changed, 393 insertions(+) create mode 100644 incoming/pattern-study/patterns/eye-chart.js diff --git a/incoming/pattern-study/patterns/eye-chart.js b/incoming/pattern-study/patterns/eye-chart.js new file mode 100644 index 000000000..5c38450c6 --- /dev/null +++ b/incoming/pattern-study/patterns/eye-chart.js @@ -0,0 +1,393 @@ +// ===== Patternflow pattern ===== +// Title: Eye Chart +// Family: type +// Idea: A back-lit letter chart stands the full height of the panel, eight rows of Sloan letters from one letter 25 LEDs tall down to five letters 5 LEDs tall with one LED to a stroke, each row 1.26 times the one below as on a logMAR chart, and lens after lens wipes across it as in a phoropter, blurring the chart by a disc as wide as the defocus that lens leaves, while a pointer reads down the rows letter by letter, marking blue the letters a noisy reader still tells apart from the ten letters blurred the same way and red the ones it takes for another, and stops at the row it fails, until the last lens of the series leaves the chart sharp, the black occluder sweeps across and a new chart goes up. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the Snellen chart (Herman Snellen, Optotypi ad visum determinandum, 1862), the ten Sloan letters (Louise Sloan, 1959) and the logMAR chart's rows a tenth of a log unit apart (Ian Bailey and Jan Lovie, 1976), the letters drawn for this pattern on Sloan's 5 by 5 grid, pattern round 74 +// Round: 74 +// =============================== +// @matrix 64x128 +// @knobs Error=0.5..2.5, Speed=0.2..3, Glare=0..1, Duochrome=0..1 +// +// Knob 1: Error. The eye's myopia, 0.5 to 2.5 diopters. One LED is one minute of arc and the pupil 3 mm, so each diopter left uncorrected blurs the chart by a disc 5.2 LEDs in radius. A series of lenses closes in on the error: at 0.5 they are 0, 0.25, 0.5, 0.75 and 0.5 diopters, blur radii 2.6, 1.3, 0, 1.3 and 0, and the bare eye reads down to the 10 or 7.9 LED row; at 2.5 the bare eye sees a disc 12.9 LEDs in radius and fails even the top letter, and the series takes 7 lenses. +// Knob 2: Speed. At 2 a lens wipes in over 0.45 s, the reader looks for 0.35 s, spends 0.18 s on a letter and holds its result 0.8 s (2.2 s after the last lens), so a lens takes 3 to 4 s and a series of five about 22 s. At 0.2 all of it is ten times slower, at 3 two thirds as long. +// Knob 3: Glare. A veil of light over the chart: at 0 the letters are black, at 1 they keep 35 percent of their contrast, and the reader sees that too. Through the right lens it still gets nine letters in ten on the 5 LED row, but a quarter diopter off it stops at the 7.9 LED row where it read to the bottom, and half a diopter off at the 12.6 or 15.8 row instead of the 10. +// Knob 4: Duochrome. Up from 0.1 the light box turns red on the left half and green on the right, fully at 1. The eye focuses red behind green, half a diopter apart, so the red half is blurred as by a quarter diopter less myopia and the green as by a quarter more: red sharper while the lens is too weak, green sharper when it is too strong, both equally soft at the right one. +// Buttons: K1 tap starts a new series on a new chart. + +// A letter is legible when what the eye sees of it is closer to that letter +// than to any of the other nine. The reader here does that literally: it +// blurs the patch of chart around the letter with the same disc, blurs each +// of the ten Sloan letters alone at the same size and place, and takes the +// one with the smallest difference, summed over the LEDs with exponent 4 +// (Minkowski pooling, as models of threshold vision add up small +// differences), after adding noise to each distance. A sharp C and O differ +// by one LED at the bottom row; blurred by a disc of radius 1.3 that LED is +// spread over eight, and the reader starts to confuse them. +// +// The eye is myopic and does not accommodate, so a lens a quarter diopter +// too strong blurs it as much as one a quarter too weak. + +const W = 64, H = 128; +const { floor, ceil, round, min, max, abs, sqrt, pow, log, cos, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const smooth = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +// Defocus blur: a disc of angular diameter pupil x diopters radians. With a +// 3 mm pupil and one LED to a minute of arc, 5.16 LEDs of radius a diopter. +const KAPPA = 0.003 * (180 / PI) * 60 * 0.5; +const NOISE = 0.2; +// Timings at Speed 2. +const T_FLIP = 0.45, T_LOOK = 0.35, T_LETTER = 0.18, T_HOLD = 0.8, T_LAST = 2.2; + +// ---- The ten Sloan letters. At the bottom row, 5 LEDs tall, each is a 5x5 +// bitmap, one LED to a stroke (16 is the left column, rows from the top). +// The C is the Landolt ring with its gap to the right, one stroke wide, so C +// and O differ by one LED there. +const SLOAN = "CDHKNORSVZ"; +const BITS = [ + 14, 17, 16, 17, 14, + 30, 17, 17, 17, 30, + 17, 17, 31, 17, 17, + 17, 18, 28, 18, 17, + 17, 25, 21, 19, 17, + 14, 17, 17, 17, 14, + 30, 17, 30, 18, 17, + 15, 16, 14, 1, 30, + 17, 17, 17, 10, 4, + 31, 2, 4, 8, 31, +]; +// Bigger rows draw the same letters from shapes on the 5 by 5 grid, one unit +// to a stroke: boxes, elliptical rings and straight bars. +const box = (u, v, x0, y0, x1, y1) => u >= x0 && u <= x1 && v >= y0 && v <= y1; +function ring(u, v, cx, cy, ao, bo, ai, bi) { + const x = u - cx, y = v - cy; + return (x * x) / (ao * ao) + (y * y) / (bo * bo) <= 1 && (x * x) / (ai * ai) + (y * y) / (bi * bi) >= 1; +} +function bar(u, v, ax, ay, bx, by, hw) { + const dx = bx - ax, dy = by - ay, t = cl(((u - ax) * dx + (v - ay) * dy) / (dx * dx + dy * dy), 0, 1); + const ex = u - ax - t * dx, ey = v - ay - t * dy; + return ex * ex + ey * ey <= hw * hw; +} +const O_ = (u, v) => ring(u, v, 2.5, 2.5, 2.5, 2.5, 1.5, 1.5); +function inside(c, u, v) { + switch (c) { + case 0: return O_(u, v) && !(u > 2.5 && abs(v - 2.5) < 0.5); + case 1: return box(u, v, 0, 0, 1, 5) || (u <= 2.5 ? box(u, v, 0, 0, 2.5, 1) || box(u, v, 0, 4, 2.5, 5) : O_(u, v)); + case 2: return box(u, v, 0, 0, 1, 5) || box(u, v, 4, 0, 5, 5) || box(u, v, 0, 2, 5, 3); + case 3: return box(u, v, 0, 0, 1, 5) || bar(u, v, 1, 3.2, 5, -0.4, 0.56) || bar(u, v, 2.4, 2.2, 5, 5.4, 0.56); + case 4: return box(u, v, 0, 0, 1, 5) || box(u, v, 4, 0, 5, 5) || bar(u, v, 0.6, 0, 4.4, 5, 0.6); + case 5: return O_(u, v); + case 6: return box(u, v, 0, 0, 1, 5) || (u <= 2.5 ? box(u, v, 0, 0, 2.5, 1) || box(u, v, 0, 2, 2.5, 3) : ring(u, v, 2.5, 1.5, 2.5, 1.5, 1.5, 0.5)) || bar(u, v, 2.6, 2.8, 4.6, 5.3, 0.56); + case 7: return (ring(u, v, 2.5, 1.5, 2.5, 1.5, 1.5, 0.5) && !(u > 2.5 && v > 1.5)) || (ring(u, v, 2.5, 3.5, 2.5, 1.5, 1.5, 0.5) && !(u < 2.5 && v < 3.5)); + case 8: return bar(u, v, 0.5, -1, 2.5, 5, 0.6) || bar(u, v, 4.5, -1, 2.5, 5, 0.6); + default: return box(u, v, 0, 0, 5, 1) || box(u, v, 0, 4, 5, 5) || bar(u, v, 4.3, 1, 0.7, 4, 0.6); + } +} +// Ink coverage of letter c, h LEDs tall, its corner at (x0, y0), into buf. +function stamp(buf, bw, bh, c, x0, y0, h) { + if (h === 5) { + for (let r = 0; r < 5; r++) + for (let q = 0; q < 5; q++) { + const x = x0 + q, y = y0 + r; + if ((BITS[c * 5 + r] >> (4 - q)) & 1 && x >= 0 && x < bw && y >= 0 && y < bh) buf[y * bw + x] = 1; + } + return; + } + const k = 5 / h; + for (let y = max(0, floor(y0)); y < min(bh, ceil(y0 + h)); y++) + for (let x = max(0, floor(x0)); x < min(bw, ceil(x0 + h)); x++) { + let n = 0; + for (let j = 0; j < 4; j++) { + const v = (y + (j + 0.5) / 4 - y0) * k; + if (v < 0 || v > 5) continue; + for (let i = 0; i < 4; i++) { + const u = (x + (i + 0.5) / 4 - x0) * k; + if (u >= 0 && u <= 5 && inside(c, u, v)) n++; + } + } + if (n) buf[y * bw + x] = max(buf[y * bw + x], n / 16); + } +} + +// ---- The chart: eight rows, logMAR 0.7 at the top to 0.0 at the bottom, +// each 10^0.1 = 1.259 times the one below: 25.1, 19.9, 15.8, 12.6, 10, 7.9, +// 6.3 and 5 LEDs. Letters stand one letter width apart as Bailey and Lovie +// spaced them, so a row holds as many as fit, 1 at the top and 5 below 7 +// LEDs. Rows sit about 3 LEDs apart with the pointer's line in the gap. +const ROWS = (() => { + const hs = []; + for (let k = 7; k >= 0; k--) { + const h = 5 * pow(10, k / 10); + hs.push(abs(h - round(h)) < 0.06 ? round(h) : h); + } + const gap = (125 - hs.reduce((a, b) => a + b, 0)) / 7, rows = []; + let y = 0; + for (let i = 0; i < 8; i++) { + const h = hs[i], top = round(y), n = min(5, floor((62 / h + 1) / 2)), left = (W - (2 * n - 1) * h) / 2, xs = []; + for (let j = 0; j < n; j++) xs.push(round(left + 2 * j * h)); + rows.push({ h, top, n, xs, w: max(1, round(h)), mark: round(top + h + gap / 2 - 0.5) }); + y += h + gap; + } + return rows; +})(); + +// ---- Disc blur. Each row of the disc is a span of the image row, summed +// from prefix sums at fractional ends, so the disc grows smoothly with its +// radius; rows near the rim come in with partial weight. Off the panel is +// no ink. +function kernel(r) { + const R = ceil(r + 0.5), dy = [], hw = [], wt = []; + let norm = 0; + for (let d = -R; d <= R; d++) { + const w = d === 0 ? 1 : cl(r - abs(d) + 0.5, 0, 1); + if (w <= 0) continue; + const h = sqrt(max(0, r * r - d * d)); + dy.push(d), hw.push(h), wt.push(w); + norm += w * (2 * h + 1); + } + return { dy, hw, wt, inv: 1 / norm, R }; +} +function prefix(src, w, h, pre) { + for (let y = 0; y < h; y++) { + const o = y * (w + 1); + let a = 0; + pre[o] = 0; + for (let x = 0; x < w; x++) pre[o + x + 1] = a += src[y * w + x]; + } +} +// Ink of row y from 0 to t. +function upto(src, pre, w, y, t) { + if (t <= 0) return 0; + if (t >= w) return pre[y * (w + 1) + w]; + const i = t | 0; + return pre[y * (w + 1) + i] + (t - i) * src[y * w + i]; +} +function blurAt(src, pre, w, h, x, y, K) { + let s = 0; + for (let k = 0; k < K.dy.length; k++) { + const yy = y + K.dy[k]; + if (yy < 0 || yy >= h) continue; + s += K.wt[k] * (upto(src, pre, w, yy, x + 1 + K.hw[k]) - upto(src, pre, w, yy, x - K.hw[k])); + } + return s * K.inv; +} + +function gauss() { + return sqrt(-2 * log(1 - Math.random())) * cos(2 * PI * Math.random()); +} + +// A new chart: each row draws its letters without repeating one. +function newChart(s) { + s.chart = ROWS.map((row) => { + const pool = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + for (let i = 9; i > 0; i--) { + const j = floor(Math.random() * (i + 1)); + [pool[i], pool[j]] = [pool[j], pool[i]]; + } + return pool.slice(0, row.n); + }); + s.ink.fill(0); + ROWS.forEach((row, i) => row.xs.forEach((x, j) => stamp(s.ink, W, H, s.chart[i][j], x, row.top, row.h))); + prefix(s.ink, W, H, s.pre); + s.tkey = -1; +} + +// The lenses a series tries for an error E, in quarter diopters: none, half +// of E, three quarters, then up a quarter at a time to E, one too strong, +// and back to E. +function plan(E) { + const q = (v) => round(v * 4) / 4, out = [0]; + const push = (v) => out[out.length - 1] !== v && out.push(v); + push(q(E / 2)); + push(q((3 * E) / 4)); + for (let v = out[out.length - 1] + 0.25; v < q(E) - 1e-6; v += 0.25) push(v); + push(q(E)); + push(q(E) + 0.25); + push(q(E)); + return out; +} + +// A new series starts behind the occluder, the phoropter's black disc: a +// new chart goes up and the first lens, none, wipes in from the dark. +function newSeries(s) { + newChart(s); + s.Eplan = s.E; + s.lenses = plan(s.E); + s.li = 0; + s.L = 0; + s.best = -1; + clearMarks(s); + s.dark = true; + s.phase = "flip"; + s.pt = 0; +} +function clearMarks(s) { + s.marks = ROWS.map((row) => new Array(row.n).fill(0)); + s.ri = -1; + s.lj = -1; +} + +// The blur radius a lens of power P leaves, on the red (-1), plain (0) or +// green (+1) side. +const radius = (s, P, side) => KAPPA * abs(s.E - P + side * 0.25 * s.u); + +// The reader's answer for letter j of row ri. It looks at the letter's own +// cell: its box, a LED above and below (the rows are 3 LEDs apart) and half +// a letter to each side, where its neighbours' blur can reach in and crowd it. +function read(s, ri, j) { + const row = ROWS[ri], h = row.h, x0 = row.xs[j], y0 = row.top; + const f = cl((32 - x0) / h, 0, 1); + const r = f * radius(s, s.L, -1) + (1 - f) * radius(s, s.L, 1); + const K = kernel(r), M = K.R + 1, n = ceil(h) + 2 * M, T = s.tpl; + const ex = min(M, ceil(h / 2)), wx = M - ex, wy = M - 1, ww = ceil(h) + 2 * ex, wh = ceil(h) + 2; + const key = ri * 100000 + round(r * 200); + if (s.tkey !== key) { + s.tkey = key; + for (let c = 0; c < 10; c++) { + s.ink2.fill(0, 0, n * n); + stamp(s.ink2, n, n, c, M, M, h); + prefix(s.ink2, n, n, s.pre2); + for (let y = 0; y < wh; y++) for (let x = 0; x < ww; x++) T[c][y * ww + x] = blurAt(s.ink2, s.pre2, n, n, wx + x, wy + y, K); + } + } + const P = s.patch; + for (let y = 0; y < wh; y++) for (let x = 0; x < ww; x++) P[y * ww + x] = blurAt(s.ink, s.pre, W, H, x0 - M + wx + x, y0 - M + wy + y, K); + let best = 0, bs = 1e9; + for (let c = 0; c < 10; c++) { + let d4 = 0; + for (let i = 0; i < ww * wh; i++) { + const e = s.c * (P[i] - T[c][i]), e2 = e * e; + d4 += e2 * e2; + } + const score = sqrt(sqrt(d4)) + NOISE * gauss(); + if (score < bs) (bs = score), (best = c); + } + return best; +} + +export function setup(params) { + const s = (params.s = { + E: 0.5, c: 1, u: 0, L: 0, Lold: 0, pt: 0, phase: "flip", dark: true, btn: false, + ink: new Float32Array(W * H), pre: new Float32Array(H * (W + 1)), + ink2: new Float32Array(64 * 64), pre2: new Float32Array(64 * 65), patch: new Float32Array(64 * 64), + tpl: Array.from({ length: 10 }, () => new Float32Array(64 * 64)), + rgb: new Float32Array(W * H * 3), colK: new Array(W), colDark: new Uint8Array(W), started: false, + }); + return s; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const s = params.s, k = input.knobValues || [0.5, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + s.E = cl(k[0] == null ? 0.5 : k[0], 0.5, 2.5); + const sc = 2 / cl(k[1] == null ? 2 : k[1], 0.2, 3); + s.c = 1 - 0.65 * cl(k[2] || 0, 0, 1); + s.u = smooth((cl(k[3] == null ? 0.06 : k[3], 0, 1) - 0.1) / 0.9); + const btn = !!(input.btnPressed && input.btnPressed[0]); + // A new error or a tap closes the occluder for a new series. + if (!s.started) (s.started = true), newSeries(s); + else if (((btn && !s.btn) || abs(s.E - s.Eplan) > 0.1) && s.phase !== "close") (s.phase = "close"), (s.pt = 0); + s.btn = btn; + s.pt += d; + if (s.phase === "close") { + if (s.pt >= T_FLIP * sc) newSeries(s); + } else if (s.phase === "flip") { + if (s.pt >= T_FLIP * sc) (s.phase = "look"), (s.pt = 0), (s.Lold = s.L), (s.dark = false); + } else if (s.phase === "look") { + if (s.pt >= T_LOOK * sc) { + s.phase = "read"; + s.pt = 0; + s.ri = s.li === 0 ? 0 : max(0, min(s.best, 6) - 1); + s.lj = 0; + s.marks[s.ri][0] = read(s, s.ri, 0) === s.chart[s.ri][0] ? 1 : 2; + s.best = -1; + } + } else if (s.phase === "read") { + if (s.pt >= T_LETTER * sc) { + s.pt = 0; + const row = ROWS[s.ri]; + if (++s.lj >= row.n) { + // More than half right passes the row. + const right = s.marks[s.ri].filter((m) => m === 1).length; + if (right * 2 > row.n) s.best = s.ri; + if (right * 2 > row.n && s.ri < 7) (s.ri++), (s.lj = 0); + else return void ((s.phase = "hold"), (s.lj = -1)); + } + s.marks[s.ri][s.lj] = read(s, s.ri, s.lj) === s.chart[s.ri][s.lj] ? 1 : 2; + } + } else if (s.pt >= (s.li === s.lenses.length - 1 ? T_LAST : T_HOLD) * sc) { + if (s.li === s.lenses.length - 1) return void ((s.phase = "close"), (s.pt = 0)); + s.li++; + s.Lold = s.L; + s.L = s.lenses[s.li]; + const keep = s.best; + clearMarks(s); + s.best = keep; + s.phase = "flip"; + s.pt = 0; + } + s.sc = sc; +} + +export function draw(display, params) { + const s = params.s; + if (!s || !s.started) return; + const sc = s.sc || 1; + // A lens wipes in from the left: columns it has passed see the new one, + // the rest the old lens, or the dark of the occluder. + const moving = s.phase === "flip" || s.phase === "close"; + const wipe = moving ? -6 + 76 * smooth(s.pt / (T_FLIP * sc)) : 999; + const Ks = [[kernel(radius(s, s.Lold, -1)), kernel(radius(s, s.Lold, 1))], [kernel(radius(s, s.L, -1)), kernel(radius(s, s.L, 1))]]; + const red = [255, 36, 18], green = [40, 255, 70], white = [255, 246, 232], u = s.u, c = s.c, rgb = s.rgb; + for (let x = 0; x < W; x++) { + const passed = x < wipe; + s.colDark[x] = s.phase === "close" ? +passed : s.phase === "flip" && s.dark ? +!passed : 0; + s.colK[x] = Ks[passed || s.phase === "close" ? 1 : 0][x < 32 ? 0 : 1]; + } + for (let y = 0; y < H; y++) { + const vy = (y - 63.5) / 64; + for (let x = 0; x < W; x++) { + if (s.colDark[x]) { + rgb.fill(0, (y * W + x) * 3, (y * W + x) * 3 + 3); + continue; + } + const b = blurAt(s.ink, s.pre, W, H, x, y, s.colK[x]); + const vx = (x - 31.5) / 32, lit = 0.8 * (1 - 0.13 * vx * vx - 0.1 * vy * vy) * (1 - c * b); + const tint = x < 32 ? red : green, j = (y * W + x) * 3; + let dark = 1; + if (moving) dark = 1 - 0.6 * exp(-(((x - wipe) / 2.2) ** 2)); + for (let q = 0; q < 3; q++) rgb[j + q] = (white[q] + (tint[q] - white[q]) * u) * lit * dark; + } + } + // The pointer's line: blue under a letter read right, red under a + // mistake, amber under the letter being read. + ROWS.forEach((row, i) => { + row.xs.forEach((x0, j) => { + const m = s.marks[i][j], now = s.phase === "read" && i === s.ri && j === s.lj; + if (!m && !now) return; + const col = now && s.pt < (T_LETTER * sc) / 2 ? [255, 150, 0] : m === 1 ? [20, 70, 255] : [235, 20, 40]; + for (let x = x0; x < min(W, x0 + row.w); x++) { + if (s.colDark[x]) continue; + const o = (row.mark * W + x) * 3; + rgb[o] = col[0], rgb[o + 1] = col[1], rgb[o + 2] = col[2]; + } + }); + }); + // The row the eye reads to through this lens, pointed at from both sides. + if (s.best >= 0) { + const row = ROWS[s.best], m = round(row.top + row.h / 2 - 0.5); + for (const [dx, dy] of [[0, 0], [1, 0], [2, 0], [0, -1], [1, -1], [0, 1], [1, 1], [0, -2], [0, 2]]) + for (const x of [dx, W - 1 - dx]) { + if (s.colDark[x]) continue; + const o = ((m + dy) * W + x) * 3; + rgb[o] = 20, rgb[o + 1] = 70, rgb[o + 2] = 255; + } + } + for (let i = 0; i < W * H; i++) { + const j = i * 3; + display.setPixel(i % W, (i / W) | 0, round(cl(rgb[j], 0, 255)), round(cl(rgb[j + 1], 0, 255)), round(cl(rgb[j + 2], 0, 255))); + } +} From f58c27f8e6088775acdfba1d746c0cc1a0e82bea Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:02:05 +0000 Subject: [PATCH 06/15] =?UTF-8?q?incoming:=20Tally=20Count=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4Jkt9PaeQhb1WkHMuJUh --- .../pattern-study/patterns/tally-count.js | 365 ++++++++++++++++++ 1 file changed, 365 insertions(+) create mode 100644 incoming/pattern-study/patterns/tally-count.js diff --git a/incoming/pattern-study/patterns/tally-count.js b/incoming/pattern-study/patterns/tally-count.js new file mode 100644 index 000000000..c7d8edc88 --- /dev/null +++ b/incoming/pattern-study/patterns/tally-count.js @@ -0,0 +1,365 @@ +// ===== Patternflow pattern ===== +// Title: Tally Count +// Family: type +// Idea: Four candidates numbered 1 to 4 head four columns of a chalkboard, ballots drawn from a sealed box are read out one at a time and each adds one chalk stroke to its candidate's column, written in the order of the character 正 so that every five votes close one 正 and the stacks of them race down the tall panel like bars, a column flashes when its candidate takes the lead, a thin trace along the bottom follows the lead of the final winner over the runner-up through the count as in Bertrand's ballot problem, and at the end the winner's column glows, the trace says whether the winner led all the way, and the board is wiped for the next count. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: tally marks in the character 正, five strokes to a mark, as vote counters in Korea, China and Japan keep them; the ballot problem (William Allen Whitworth, 1878; Joseph Bertrand, 1887); 5 by 7 figures from Photon Disk (pattern round 70); pattern round 74 +// Round: 74 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Closeness=0..1, Size=7..15 +// +// Knob 1: Hue. Turns the four chalk colours round the wheel together. At 0 candidate 1 writes in light cyan, 2 in lilac, 3 in salmon and 4 in light green, a quarter turn apart at 60% saturation; at 0.5 they are salmon, light green, light cyan and lilac. +// Knob 2: Speed. A full count takes 10 s at 2, 100 s at 0.2 and 6.7 s at 3, whatever the number of ballots, and a stroke is written in 0.18 s at 2. The winner then glows for 3 s and the eraser takes 1.2 s, both scaled by 2 / Speed within 1.5 to 6 s and 0.6 to 2.4 s. +// Knob 3: Closeness. How close the hidden shares are. Each count draws four shares from a Dirichlet(2, 2, 2, 2) and mixes them toward 25% each by this much. At 0 the winner takes 34 to 58% of the ballots (median 44%), the lead changes hands about 3 times and the winner leads all the way in 23% of counts; at 0.5 it takes 33% and leads all the way in 12%; at 1 all four shares are 25%, the columns finish within a few marks of each other, the lead changes about 12 times at Size 7 and the winner leads all the way in 2% of counts. A change of more than 0.08 starts a new count. +// Knob 4: Size. The height of a 正 in LEDs, 7 to 15 in odd steps. At 7 two marks share a row of the 16 LED column, 12 rows deep, so a column holds 120 votes and a count reads 265 ballots at Closeness 0 and 455 at 1; at 9 one mark a row, 9 rows, 45 votes and about 100 ballots; at 15 five rows of marks 13 wide with 2 LED strokes, 25 votes and about 55 ballots. The winner's column always ends within a few votes of full. A new size starts a new count. +// Buttons: K1 tap wipes the board and starts a new count. + +// The ballot problem: if A gets p votes and B gets q < p, and every order of +// reading them is equally likely, the chance that A is strictly ahead of B +// after every ballot is (p - q) / (p + q). Whitworth stated it in 1878 and +// Bertrand published it in 1887 (Comptes Rendus 105). Here the four +// candidates' votes are all drawn before the count starts, a sealed box, and +// then read in a uniformly shuffled order, so for the two who finish first +// and second the theorem holds as stated: ballots for the other two only +// stretch the trace sideways. The trace is the lead of the final winner over +// the final runner-up after each ballot; the winner led all the way when it +// stays above the zero line from that pair's first ballot on. +// +// The five strokes of 正 in their written order: the top bar, the centre +// stem, the short bar right of the stem at half height, the short stem at +// the lower left, the long bottom bar. One vote is one stroke. + +const W = 64, H = 128, NC = 4, CW = 16; +// The tally runs from row TOP to BOT; the trace of the lead sits on row +// ZERO and swings AMP rows either way. +const TOP = 16, BOT = 113, ZERO = 121, AMP = 5; +const { floor, round, min, max, abs, sqrt, exp, log, sin } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const T_COUNT = 10, T_STROKE = 0.18, T_END = 3, T_WIPE = 1.2, HUE0 = 0.55; + +const ZHENG = [ + [0.15, 0, 0.85, 0], + [0.5, 0, 0.5, 1], + [0.5, 0.5, 0.85, 0.5], + [0.15, 0.5, 0.15, 1], + [0, 1, 1, 1], +]; + +// The figures 1 to 4 of Photon Disk's 5 x 7 face (copied from autogram.js), +// rows from the top. +const FIG = [ + "..#.. .##.. ..#.. ..#.. ..#.. ..#.. .###.", + ".###. #...# ....# ...#. ..#.. .#... #####", + "##### ...#. ..#.. ...#. ....# #...# .###.", + "...#. ..##. .#.#. #..#. ##### ...#. ...#.", +].map((f) => f.replace(/ /g, "")); + +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; +} + +// A shares vector from a Dirichlet(2, 2, 2, 2): each a sum of two +// exponentials, normalised. +const expo = () => -log(1 - Math.random() * 0.999999); +function shares(close) { + const d = []; + let s = 0; + for (let c = 0; c < NC; c++) s += d[c] = expo() + expo(); + for (let c = 0; c < NC; c++) d[c] = (1 - close) * (d[c] / s) + close / NC; + return d; +} + +// The layout of one size: marks per row, rows, and the cell pitch. +function layout(size) { + const g = size <= 7 ? 1 : 2, per = size <= 7 ? 2 : 1, pitch = size + g; + const rows = floor((BOT - TOP + g) / pitch); + // A mark is as wide as it is tall up to 13 LEDs, which leaves at least + // 3 LEDs between neighbouring columns. + const mw = min(size, 13), wid = per * mw + (per - 1) * g; + return { size, mw, g, per, pitch, rows, cap: rows * per * 5, ox: floor((CW - wid) / 2), sw: size <= 9 ? 1 : size <= 13 ? 1.5 : 2 }; +} + +// A new count: draw the shares, the votes, a uniformly shuffled order, and +// the lead path of the final first over the final second. +function newCount(S, prefill) { + const L = (S.L = layout(S.size)); + let tally; + for (let tries = 0; tries < 30; tries++) { + S.share = shares(S.close); + const top = max(...S.share), n = round((L.cap * 0.96) / top); + tally = [0, 0, 0, 0]; + for (let i = 0; i < n; i++) { + let u = Math.random(), c = 0; + while (c < NC - 1 && u >= S.share[c]) u -= S.share[c++]; + tally[c]++; + } + for (let c = 0; c < NC; c++) tally[c] = min(tally[c], L.cap); + const sorted = tally.slice().sort((a, b) => b - a); + if (sorted[0] > sorted[1]) break; + } + const seq = []; + for (let c = 0; c < NC; c++) for (let i = 0; i < tally[c]; i++) seq.push(c); + for (let i = seq.length - 1; i > 0; i--) { + const j = floor(Math.random() * (i + 1)); + [seq[i], seq[j]] = [seq[j], seq[i]]; + } + let win = 0; + for (let c = 1; c < NC; c++) if (tally[c] > tally[win]) win = c; + let run = -1; + for (let c = 0; c < NC; c++) if (c !== win && (run < 0 || tally[c] > tally[run])) run = c; + const lead = new Int16Array(seq.length); + let d = 0, ok = true, seen = false, peak = 1; + for (let i = 0; i < seq.length; i++) { + if (seq[i] === win) (d++), (seen = true); + else if (seq[i] === run) (d--), (seen = true); + lead[i] = d; + if (seen && d <= 0) ok = false; + peak = max(peak, abs(d)); + } + Object.assign(S, { seq, final: tally, win, run, lead, ok, peak, n: seq.length, read: 0, next: 0, phase: 0, pt: 0, leader: -1 }); + S.p = tally[win]; + S.q = tally[run]; + S.tally = [0, 0, 0, 0]; + S.active = []; + S.flash.fill(0); + S.lit.fill(0); + S.chalk.fill(0); + if (prefill) { + const k = floor(S.n * prefill); + for (let i = 0; i < k; i++) commit(S, true); + S.read = k; + } +} + +// Where stroke j of candidate c goes, in LED coordinates of pixel centres. +function strokeAt(S, c, j, out) { + const L = S.L, m = floor(j / 5), k = j % 5, r = floor(m / L.per), col = m % L.per; + const x0 = c * CW + L.ox + col * (L.mw + L.g) + 0.5, y0 = TOP + r * L.pitch + 0.5, ex = L.mw - 1, ey = L.size - 1, z = ZHENG[k]; + out[0] = x0 + round(z[0] * ex); + out[1] = y0 + round(z[1] * ey); + out[2] = x0 + round(z[2] * ex); + out[3] = y0 + round(z[3] * ey); + return out; +} + +// Read the next ballot: one stroke for its candidate, and a flash when the +// lead changes hands. +function commit(S, silent) { + const c = S.seq[S.next], j = S.tally[c]++; + const st = strokeAt(S, c, j, [0, 0, 0, 0]); + if (silent) bake(S, st, c, 1); + else S.active.push({ st, c, t: 0 }); + let best = 0, ties = 0; + for (let k = 1; k < NC; k++) if (S.tally[k] > S.tally[best]) best = k; + for (let k = 0; k < NC; k++) if (S.tally[k] === S.tally[best]) ties++; + // A change of leader flashes the new leader's column once it has a + // whole 正; the swaps among the first few ballots would only flicker. + if (ties === 1 && best !== S.leader) { + if (S.leader >= 0 && !silent && S.tally[best] >= 5) S.flash[best] = 1; + S.leader = best; + } + if (!silent) S.lit[c] = min(1, S.lit[c] + 0.35); + S.next++; +} + +// Chalk: a line of width sw through pixel centres, its grain fixed per LED. +function chalkLine(buf, st, u, sw, col, grain, k0) { + const x0 = st[0], y0 = st[1], x1 = x0 + (st[2] - x0) * u, y1 = y0 + (st[3] - y0) * u; + const hw = sw / 2, dx = x1 - x0, dy = y1 - y0, l2 = dx * dx + dy * dy; + const ax = max(0, floor(min(x0, x1) - hw - 1)), bx = min(W - 1, floor(max(x0, x1) + hw + 1)); + const ay = max(0, floor(min(y0, y1) - hw - 1)), by = min(H - 1, floor(max(y0, y1) + hw + 1)); + for (let y = ay; y <= by; y++) + for (let x = ax; x <= bx; x++) { + const px = x + 0.5 - x0, py = y + 0.5 - y0, t = l2 > 0 ? cl((px * dx + py * dy) / l2, 0, 1) : 0; + const d = sqrt((px - t * dx) ** 2 + (py - t * dy) ** 2), cov = cl(hw + 0.5 - d, 0, 1); + if (cov <= 0) continue; + const i = y * W + x, a = cov * grain[i] * k0, kk = i * 3; + for (let q = 0; q < 3; q++) buf[kk + q] = max(buf[kk + q], col[q] * a); + } +} +function bake(S, st, c, u) { + chalkLine(S.chalk, st, u, S.L.sw, S.col[c], S.grain, 1); +} +// A finished stroke throws a little dust round itself, which settles in +// about a second: the front where a column is being written hazes over. +function puff(S, st, c) { + const ax = max(0, floor(min(st[0], st[2]) - 4)), bx = min(W - 1, floor(max(st[0], st[2]) + 4)); + const ay = max(0, floor(min(st[1], st[3]) - 4)), by = min(H - 1, floor(max(st[1], st[3]) + 4)); + const dx = st[2] - st[0], dy = st[3] - st[1], l2 = dx * dx + dy * dy || 1; + for (let y = ay; y <= by; y++) + for (let x = ax; x <= bx; x++) { + const px = x + 0.5 - st[0], py = y + 0.5 - st[1], t = cl((px * dx + py * dy) / l2, 0, 1); + const d2 = (px - t * dx) ** 2 + (py - t * dy) ** 2, i = y * W + x; + S.dust[i] = min(0.5, S.dust[i] + 0.09 * exp(-d2 / 5) * S.grain[i]); + S.dustC[i] = c; + } +} + +export function setup(params) { + const grain = new Float32Array(W * H); + for (let i = 0; i < grain.length; i++) grain[i] = 0.74 + 0.26 * Math.random(); + params.s = { + hue: 0, speed: 2, close: 0, size: 7, started: false, btn: false, grain, + chalk: new Float32Array(W * H * 3), out: new Float32Array(W * H * 3), dust: new Float32Array(W * H), dustC: new Uint8Array(W * H), + flash: new Float32Array(NC), lit: new Float32Array(NC), col: [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], + wipeY: -1, t: 0, + }; + return params.s; +} + +function paint(S) { + for (let c = 0; c < NC; c++) hsv(S.hue + HUE0 + c * 0.25, 0.6, 1, S.col[c]); +} + +// Phases: 0 counting, 1 the winner glows, 2 the eraser comes down. +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 7], d = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + S.speed = cl(k[1] == null ? 2 : k[1], 0.2, 3); + const close = cl(k[2] || 0, 0, 1), size = 7 + 2 * round((cl(k[3] == null ? 7 : k[3], 7, 15) - 7) / 2); + const btn = !!(input.btnPressed && input.btnPressed[0]); + paint(S); + if (!S.started || size !== S.size || abs(close - S.close) > 0.08) { + // The Lab opens on a count 40% of the way through. + S.close = close; + S.size = size; + newCount(S, S.started ? 0.12 : 0.4); + S.started = true; + } else if (btn && !S.btn) (S.phase = 2), (S.pt = 0), (S.wipeY = 0); + S.btn = btn; + const sc = 2 / S.speed; + S.t += d; + S.pt += d; + for (let c = 0; c < NC; c++) (S.flash[c] *= exp(-d / 0.45)), (S.lit[c] *= exp(-d / 0.25)); + const settle = exp(-d / 0.9); + for (let i = 0; i < W * H; i++) S.dust[i] *= settle; + if (S.phase === 0) { + const rate = S.n / (T_COUNT * sc); + S.read = min(S.n, S.read + d * rate); + while (S.next < S.n && S.next < S.read) commit(S, false); + const ds = T_STROKE * sc; + for (const a of S.active) a.t += d / ds; + S.active = S.active.filter((a) => (a.t >= 1 ? (bake(S, a.st, a.c, 1), puff(S, a.st, a.c), false) : true)); + if (S.next >= S.n && !S.active.length) (S.phase = 1), (S.pt = 0); + } else if (S.phase === 1) { + if (S.pt >= cl(T_END * sc, 1.5, 6)) (S.phase = 2), (S.pt = 0), (S.wipeY = 0); + } else { + // The eraser: what it passes keeps a smear of dust that fades. + const dur = cl(T_WIPE * sc, 0.6, 2.4), y1 = min(H, (S.pt / dur) * (H + 12)); + const ch = S.chalk; + for (let y = 0; y < min(H, floor(y1)); y++) { + const f = y >= S.wipeY ? 0.16 : exp(-d / 0.5); + for (let x = 0; x < W; x++) { + const kk = (y * W + x) * 3; + ch[kk] *= f; + ch[kk + 1] *= f; + ch[kk + 2] *= f; + } + } + S.wipeY = max(S.wipeY, floor(y1)); + if (S.pt >= dur + 0.5) newCount(S, 0); + } +} + +function fill(o, x0, y0, x1, y1, c, a) { + for (let y = max(0, y0); y <= min(H - 1, y1); y++) + for (let x = max(0, x0); x <= min(W - 1, x1); x++) { + const kk = (y * W + x) * 3; + for (let q = 0; q < 3; q++) o[kk + q] = max(o[kk + q], c[q] * a); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const o = S.out, ch = S.chalk, col = S.col; + const wipe = S.phase === 2, glow = S.phase === 1 ? 0.5 + 0.5 * sin(S.pt * 5) : 0; + // The board, with a faint tint behind a column that just took the lead or won. + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const kk = (y * W + x) * 3, c = floor(x / CW); + const inside = y >= TOP - 2 && y <= BOT; + let tint = inside ? 0.2 * S.flash[c] : 0; + if (S.phase === 1 && c === S.win && inside) tint = max(tint, 0.1 + 0.1 * glow); + const du = S.dust[y * W + x], dc = col[S.dustC[y * W + x]]; + o[kk] = 0.02 + col[c][0] * tint + dc[0] * du; + o[kk + 1] = 0.055 + col[c][1] * tint + dc[1] * du; + o[kk + 2] = 0.045 + col[c][2] * tint + dc[2] * du; + } + // Chalk on the board; the winner's marks brighten while it glows. + const boost = S.phase === 1 ? 1 + 0.25 * glow : 1; + for (let i = 0; i < W * H; i++) { + const kk = i * 3, b = S.phase === 1 && floor((i % W) / CW) === S.win ? boost : 1; + o[kk] = max(o[kk], ch[kk] * b); + o[kk + 1] = max(o[kk + 1], ch[kk + 1] * b); + o[kk + 2] = max(o[kk + 2], ch[kk + 2] * b); + } + // Strokes being written: the line so far, its tip a little brighter. + const tip = [0, 0, 0]; + for (const a of S.active) { + const u = cl(a.t, 0, 1); + chalkLine(o, a.st, u, S.L.sw, col[a.c], S.grain, 1); + for (let q = 0; q < 3; q++) tip[q] = 0.5 + 0.5 * col[a.c][q]; + const st = a.st, tx = st[0] + (st[2] - st[0]) * u, ty = st[1] + (st[3] - st[1]) * u; + chalkLine(o, [tx, ty, tx, ty], 1, S.L.sw + 0.4, tip, S.grain, 1); + } + // The candidates' numbers, 기호 1 to 4, in boxes that light as a ballot is read. + for (let c = 0; c < NC; c++) { + const x0 = c * CW + 2, x1 = c * CW + 13, cc = col[c]; + const lit = S.lit[c], won = S.phase >= 1 && c === S.win ? 1 : 0; + fill(o, x0, 1, x1, 13, cc, 0.14 + 0.5 * lit + 0.25 * won * (S.phase === 1 ? glow : 1)); + for (let x = x0; x <= x1; x++) fill(o, x, 1, x, 1, cc, 0.7), fill(o, x, 13, x, 13, cc, 0.7); + fill(o, x0, 1, x0, 13, cc, 0.7); + fill(o, x1, 1, x1, 13, cc, 0.7); + const f = FIG[c]; + for (let r = 0; r < 7; r++) + for (let q = 0; q < 5; q++) if (f[r * 5 + q] === "#") fill(o, c * CW + 5 + q, 4 + r, c * CW + 5 + q, 4 + r, [1, 1, 1], 0.9 + 0.1 * lit); + // Led all the way: a second frame round the winner's box. + if (S.phase === 1 && c === S.win && S.ok && glow > 0.3) { + for (let x = x0 - 1; x <= x1 + 1; x++) fill(o, x, 0, x, 0, [1, 1, 1], 0.9), fill(o, x, 14, x, 14, [1, 1, 1], 0.9); + fill(o, x0 - 1, 0, x0 - 1, 14, [1, 1, 1], 0.9); + fill(o, x1 + 1, 0, x1 + 1, 14, [1, 1, 1], 0.9); + } + } + // The ballot path: zero line dotted, the winner's lead above, the + // runner-up's below, one column per ballot read. + if (!wipe || S.pt < 0.3) { + for (let x = 0; x < W; x += 2) fill(o, x, ZERO, x, ZERO, [0.5, 0.5, 0.5], 0.42); + const n = min(S.next, S.n), cw = col[S.win], cr = col[S.run], sy = AMP / S.peak; + let py = ZERO; + for (let i = 0; i < n; i++) { + const x = floor((i * W) / S.n), y = round(ZERO - S.lead[i] * sy); + const c = S.lead[i] > 0 ? cw : S.lead[i] < 0 ? cr : [1, 1, 1]; + const a = S.phase === 1 ? (S.ok ? 0.75 + 0.25 * glow : 0.75) : 0.75; + for (let yy = min(py, y); yy <= max(py, y); yy++) fill(o, x, yy, x, yy, c, a); + py = y; + } + // Not led all the way: where the runner-up drew level or went ahead. + if (S.phase === 1 && !S.ok) { + let seen = false, was = false; + for (let i = 0; i < n; i++) { + if (S.seq[i] === S.win || S.seq[i] === S.run) seen = true; + const down = seen && S.lead[i] <= 0; + if (down && was === false) { + const x = floor((i * W) / S.n); + fill(o, x, ZERO - 2, x, ZERO + 2, [1, 1, 1], 0.4 + 0.6 * glow); + } + if (seen) was = down; + } + } + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const kk = (y * W + x) * 3; + display.setPixel(x, y, round(cl(o[kk], 0, 1) * 255), round(cl(o[kk + 1], 0, 1) * 255), round(cl(o[kk + 2], 0, 1) * 255)); + } +} From 517975f641a899469a2b2585f7519cf8641b336e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:02:24 +0000 Subject: [PATCH 07/15] =?UTF-8?q?incoming:=20Flag=20Hoist=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4Jkt9PaeQhb1WkHMuJUh --- incoming/pattern-study/patterns/flag-hoist.js | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 incoming/pattern-study/patterns/flag-hoist.js diff --git a/incoming/pattern-study/patterns/flag-hoist.js b/incoming/pattern-study/patterns/flag-hoist.js new file mode 100644 index 000000000..a6490bcb4 --- /dev/null +++ b/incoming/pattern-study/patterns/flag-hoist.js @@ -0,0 +1,338 @@ +// ===== Patternflow pattern ===== +// Title: Flag Hoist +// Family: type +// Idea: Words are spelled in the letter flags of the International Code of Signals, bent on one under another to a halyard that runs down the tall panel by its left edge, and each hoist is hauled up hand over hand into a night sky, flies there with every flag fluttering in a wave that grows towards its fly and turns its folds light and dark, shows the plain letter beside each flag while it is close up, and is hauled down for the next word. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the letter flags of the International Code of Signals, which began as the British Board of Trade's Commercial Code of Signals (1857), its 26 letter flags as revised in 1931 and 1965; before it Frederick Marryat's Code of Signals for the Merchant Service (1817); 5 by 7 capitals from Photon Disk (pattern round 70); pattern round 74 +// Round: 74 +// =============================== +// @matrix 64x128 +// @knobs Wind=3..8, Speed=0.2..3, Letters=3..6, Sky=0..1 +// +// Knob 1: Wind. The wind in Beaufort force. At the opening 3, the gentle breeze in which light flags stand out, a fold runs down each flag about once a second, its steepest side turned 29 degrees, and the fly lifts and falls about 2 LEDs; at 8, a gale, the folds come 2.7 times a second at half the length, turned up to 45 degrees, the fly whips about 6 LEDs and the cloth bunches 7 percent shorter. +// Knob 2: Speed. At 2 a hoist takes 2.6 s to go up in six hauls, flies close up for 4.5 s, takes 2.2 s to come down, and the next follows 0.4 s later, 9.7 s a word. Every time scales as 2 / Speed: 97 s a word at 0.2, 6.5 s at 3. +// Knob 3: Letters. How many flags a hoist carries, one word of that length. At 3 (KEG, RIG, DRY, BAY, OAR, SKY) the flags are 31 LEDs deep and 40 long and the hoist reaches row 100; at 4 (QUAY, GOLD, DECK, BUOY and four more) 25 by 34; at 5 (OCEAN, YACHT, RIDGE and three more) 20 by 28; at 6 (ANCHOR, BRIDGE, VOYAGE and three more) 16 by 23, the hoist reaching row 110 to 113. +// Knob 4: Sky. 0 is a moonless night, near black with a few faint stars; 1 is the blue hour, a deep blue sky going violet and then amber towards the foot of the panel. The opening 0.06 is night. +// Buttons: K1 tap hauls the hoist down at once and sends up the next word. + +// A ship carries one flag for each letter, so a word that needs one letter +// twice would take a substitute pennant in the second place. The words here +// never repeat a letter. A hoist is read from the top down once it is +// close up, hauled right up to the block, and that is when the plain +// letters show. +// +// A flag in the wind is a sheet on which waves run from the hoist, where it +// is held, to the fly, where it is free, growing as they go. The sheet's +// folds stand out of the panel: each column of the flag tilts by the slope +// of the fold there, which shortens it on the panel and turns it towards or +// away from a light on the left. The folds also lift the fly a +// little, so the top and bottom edges ripple. + +const W = 64, H = 128; +const { floor, round, min, max, abs, sqrt, sin, cos, atan, 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)); +const TAU = 2 * PI; + +// The halyard's column and where the flags' hoist edges start. +const HX = 3, X0 = 4, TOP = 2, GAP = 3; +// Cloth colours, chosen to read on LEDs: white, red, blue, yellow, black. +// Black cloth stays just above dark so the flag keeps its shape at night. +const CLOTH = [ + [228, 226, 214], + [255, 8, 22], + [0, 40, 255], + [255, 196, 0], + [36, 34, 42], +]; +const Wt = 0, R = 1, B = 2, Y = 3, K = 4; +// Swallowtailed flags, A and B: the notch reaches this far into the fly. +const NOTCH = 0.24; + +// Each flag as a function of u along the fly (0 at the hoist), v down the +// hoist (0 at the top), and the flag's length and depth in LEDs for the +// shapes that are square or round on the cloth. +function distLine(px, py, ax, ay, bx, by) { + const dx = bx - ax, dy = by - ay; + return abs((px - ax) * dy - (py - ay) * dx) / sqrt(dx * dx + dy * dy); +} +const FLAGS = { + A: (u) => (u < 0.5 ? Wt : B), + B: () => R, + C: (u, v) => [B, Wt, R, Wt, B][min(4, floor(v * 5))], + D: (u, v) => (v < 0.25 || v >= 0.75 ? Y : B), + E: (u, v) => (v < 0.5 ? B : R), + F: (u, v) => (abs(u - 0.5) + abs(v - 0.5) < 0.5 ? R : Wt), + G: (u) => (floor(u * 6) % 2 ? B : Y), + H: (u) => (u < 0.5 ? Wt : R), + I: (u, v, l, d) => (((u - 0.5) * l) ** 2 + ((v - 0.5) * d) ** 2 < (0.26 * d) ** 2 ? K : Y), + J: (u, v) => (v < 0.25 || v >= 0.75 ? B : Wt), + K: (u) => (u < 0.5 ? Y : B), + L: (u, v) => ((u < 0.5) === (v < 0.5) ? Y : K), + M: (u, v, l, d) => (min(distLine(u * l, v * d, 0, 0, l, d), distLine(u * l, v * d, 0, d, l, 0)) < 0.1 * d ? Wt : B), + N: (u, v) => ((floor(u * 4) + floor(v * 4)) % 2 ? Wt : B), + O: (u, v) => (v < u ? R : Y), + P: (u, v, l, d) => (abs(u - 0.5) * l < d / 6 && abs(v - 0.5) * d < d / 6 ? Wt : B), + Q: () => Y, + R: (u, v, l, d) => (abs(u - 0.5) * l < 0.1 * d || abs(v - 0.5) < 0.1 ? Y : R), + S: (u, v, l, d) => (abs(u - 0.5) * l < d / 6 && abs(v - 0.5) * d < d / 6 ? B : Wt), + T: (u) => [R, Wt, B][min(2, floor(u * 3))], + U: (u, v) => ((u < 0.5) === (v < 0.5) ? R : Wt), + V: (u, v, l, d) => (min(distLine(u * l, v * d, 0, 0, l, d), distLine(u * l, v * d, 0, d, l, 0)) < 0.1 * d ? R : Wt), + W: (u, v, l, d) => { + const e = min(u * l, (1 - u) * l, v * d, (1 - v) * d); + return e < d / 6 ? B : e < d / 3 ? Wt : R; + }, + X: (u, v, l, d) => (abs(u - 0.5) * l < 0.1 * d || abs(v - 0.5) < 0.1 ? B : Wt), + Y: (u, v, l, d) => (floor(((u * l + v * d) / (l + d)) * 10) % 2 ? R : Y), + Z: (u, v) => (u < v && u < 1 - v ? K : v < u && v < 1 - u ? Y : u > v && u > 1 - v ? B : R), +}; + +// Words of three to six letters with no letter twice. +const WORDS = { + 3: ["KEG", "RIG", "DRY", "BAY", "OAR", "SKY"], + 4: ["QUAY", "GOLD", "DECK", "BUOY", "YARD", "BRIG", "TIDE", "WIND"], + 5: ["OCEAN", "YACHT", "RIDGE", "BADGE", "BOARD", "DRIFT"], + 6: ["ANCHOR", "BRIDGE", "VOYAGE", "YONDER", "GOLDEN", "BYGONE"], +}; +// Flag depth and length for each word length: the hoist runs from row 2 to +// row 100 (three flags) or 110 to 113 (more), with 3 LEDs of halyard +// between flags. +const SIZE = { 3: [31, 40], 4: [25, 34], 5: [20, 28], 6: [16, 23] }; + +// 5 x 7 capitals from Photon Disk (pattern round 70), rows from the top. +const GLYPHS = { + 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: "##### ....# ...#. ..#.. .#... #.... #####", +}; +const FONT = {}; +for (const k in GLYPHS) FONT[k] = GLYPHS[k].replace(/ /g, ""); + +// Steps along the cloth when a flag's folds are worked out. +const STEPS = 96; +// The light comes from the left and the front. +const LX = -0.6, LZ = 0.8; + +// A few faint stars, fixed for the run. +const STARS = []; +for (let i = 0; i < 26; i++) STARS.push([floor(Math.random() * W), floor(Math.random() * H), Math.random() * TAU, 0.5 + Math.random() * 0.5]); + +export function setup(params) { + params.s = { + t: 0, phase: -1, pt: 0, word: "", n: 3, next: { 3: 0, 4: 0, 5: 0, 6: 0 }, wind: 3, speed: 2, sky: 0.06, btn: false, + y: H, show: 0, flut: 0, acc: new Float32Array(W * H * 3), + cu: new Float32Array(W), cdy: new Float32Array(W), csh: new Float32Array(W), cdu: new Float32Array(W), + xs: new Float32Array(STEPS + 1), ys: new Float32Array(STEPS + 1), ss: new Float32Array(STEPS + 1), + }; + return params.s; +} + +// Phase 0 up, 1 close up, 2 down, 3 the pause before the next word. +const DUR = [2.6, 4.5, 2.2, 0.4]; +const HAULS = 6; + +function hoistLength(S) { + const [d] = SIZE[S.n]; + return S.n * d + (S.n - 1) * GAP; +} +function nextWord(S, n) { + S.n = n; + const list = WORDS[n]; + S.word = list[S.next[n] % list.length]; + S.next[n]++; + S.phase = 0; + S.pt = 0; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [3, 2, 3, 0.06], d = cl(dt || 0, 0, 0.1); + S.wind = cl(k[0], 3, 8); + S.speed = cl(k[1], 0.2, 3); + const n = round(cl(k[2], 3, 6)); + S.sky = cl(k[3], 0, 1); + const btn = !!(input.btnPressed && input.btnPressed[0]); + if (S.phase < 0) nextWord(S, n); + if (btn && !S.btn && S.phase < 2) (S.phase = 2), (S.pt = 0), (S.from = S.y); + S.btn = btn; + S.t += d; + // The flutter runs on its own clock so a change of wind never jumps it. + S.flut += d * (0.95 + 0.35 * (S.wind - 3)); + const sc = 2 / S.speed; + S.pt += d; + const len = hoistLength(S); + if (S.phase === 0) { + // Hand over hand: the haul surges and slackens six times on the way. + const p = cl(S.pt / (DUR[0] * sc), 0, 1), q = p - (0.55 * sin(TAU * HAULS * p)) / (TAU * HAULS); + S.y = H + 2 + (TOP - H - 2) * smooth(q); + if (p >= 1) (S.phase = 1), (S.pt = 0); + } else if (S.phase === 1) { + S.y = TOP; + if (S.pt >= DUR[1] * sc) (S.phase = 2), (S.pt = 0), (S.from = TOP); + } else if (S.phase === 2) { + const p = cl(S.pt / (DUR[2] * sc), 0, 1), from = S.from ?? TOP; + S.y = from + (H + 2 - from) * (p * p * (1.6 - 0.6 * p)); + if (p >= 1) (S.phase = 3), (S.pt = 0); + } else { + S.y = H + len + 2; + if (S.pt >= DUR[3] * sc) nextWord(S, n); + } + // The letters fade in once the hoist is close up and out as it starts down. + const want = S.phase === 1 ? 1 : 0; + S.show += (want - S.show) * min(1, d * (want ? 3 : 6)); +} + +// One flag: its columns first (where each u lands, how far its fold lifts +// it, how it is lit), then every LED in its box, four samples each. +function flag(S, acc, ch, top, d, l, ph) { + const wind = S.wind - 3, t = S.flut * TAU; + // The steepest fold, as dz/dx: 0.55 (29 degrees) in a gentle breeze, 1 + // (45 degrees) in a gale, the folds getting shorter as the wind rises. + const lam = 0.95 - 0.09 * wind, amp = ((0.55 + 0.09 * wind) * lam) / TAU, lift = 1.5 + 0.6 * wind; + const cu = S.cu, cdy = S.cdy, csh = S.csh, cdu = S.cdu, xs = S.xs, ys = S.ys, ss = S.ss, N = STEPS; + // March along the cloth in small steps, adding up how far along the panel + // each step goes (its length times the cosine of its tilt). + let x = 0; + for (let i = 0; i <= N; i++) { + const u = i / N, g = u ** 0.9, th = (TAU * u) / lam - t + ph; + // The slope of the fold, dz/dx, which tilts the column. + const slope = amp * g * (TAU / lam) * cos(th) + amp * 0.9 * max(u, 0.01) ** -0.1 * sin(th); + const tilt = atan(slope); + if (i > 0) x += (l / N) * cos(tilt); + xs[i] = x; + ys[i] = lift * g * sin(th - 0.9) + 0.4 * lift * u * u * sin(2 * th + 0.5); + // Lambert light on the tilted column, never black. + ss[i] = 0.3 + 0.78 * max(0, -sin(tilt) * LX + cos(tilt) * LZ); + } + const end = xs[N]; + // Each LED column the flag reaches: the u at its centre by walking the + // table, and how much u one LED spans there. + let j = 0; + const cols = min(W - X0, floor(end) + 1); + for (let col = 0; col < cols; col++) { + const xc = col + 0.5; + while (j < N - 1 && xs[j + 1] < xc) j++; + const f = cl((xc - xs[j]) / max(1e-6, xs[j + 1] - xs[j]), 0, 1); + cu[col] = (j + f) / N; + cdy[col] = ys[j] + (ys[j + 1] - ys[j]) * f; + csh[col] = ss[j] + (ss[j + 1] - ss[j]) * f; + cdu[col] = 1 / (N * max(1e-6, xs[j + 1] - xs[j])); + } + const fn = FLAGS[ch], notch = ch === "A" || ch === "B"; + const y0 = max(0, floor(top - 8)), y1 = min(H - 1, floor(top + d + 8)); + for (let c = 0; c < cols; c++) { + const px = X0 + c, u = cu[c], du = cdu[c], dy = cdy[c], sh = csh[c]; + // The fly edge, soft. + const aFly = cl(end - c, 0, 1); + for (let py = y0; py <= y1; py++) { + const yy = py + 0.5 - top - dy; + const aEdge = cl(min(yy, d - yy) + 0.5, 0, 1); + if (aEdge <= 0) continue; + // Four samples a quarter LED from the centre. A sample in the + // swallowtail's notch is sky, so it only thins the LED. + let r = 0, g = 0, b = 0, hits = 0; + for (let s = 0; s < 4; s++) { + const su = cl(u + ((s & 1) - 0.5) * 0.5 * du, 0, 0.9999), sv = cl((yy + ((s >> 1) - 0.5) * 0.5) / d, 0, 0.9999); + if (notch && su > 1 - NOTCH * (1 - abs(2 * sv - 1))) continue; + const cc = CLOTH[fn(su, sv, l, d)]; + r += cc[0]; + g += cc[1]; + b += cc[2]; + hits++; + } + if (!hits) continue; + const a = (aEdge * aFly * hits) / 4, kk = (py * W + px) * 3, m = sh / 255 / hits; + acc[kk] += (r * m - acc[kk]) * a; + acc[kk + 1] += (g * m - acc[kk + 1]) * a; + acc[kk + 2] += (b * m - acc[kk + 2]) * a; + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const acc = S.acc, sky = S.sky, sk = sky ** 1.5, t = S.t; + // The sky: night is near black with a blue breath at the top; the blue + // hour runs deep blue to violet to amber at the foot. + for (let y = 0; y < H; y++) { + const v = y / (H - 1); + const nr = 0, ng = 2 + 4 * (1 - v), nb = 5 + 12 * (1 - v); + const hr = v < 0.6 ? 20 + 60 * (v / 0.6) : 80 + 120 * ((v - 0.6) / 0.4); + const hg = v < 0.6 ? 34 + 6 * (v / 0.6) : 40 + 50 * ((v - 0.6) / 0.4); + const hb = v < 0.6 ? 120 - 20 * (v / 0.6) : 100 - 70 * ((v - 0.6) / 0.4); + const r = (nr + (hr - nr) * sk) / 255, g = (ng + (hg - ng) * sk) / 255, b = (nb + (hb - nb) * sk) / 255; + for (let x = 0; x < W; x++) { + const k = (y * W + x) * 3; + acc[k] = r; + acc[k + 1] = g; + acc[k + 2] = b; + } + } + const starA = 1 - smooth(sky * 1.6); + for (const [x, y, p, m] of STARS) { + const k = (y * W + x) * 3, v = starA * m * (0.13 + 0.07 * sin(t * 1.7 + p)); + acc[k] = max(acc[k], v); + acc[k + 1] = max(acc[k + 1], v); + acc[k + 2] = max(acc[k + 2], v * 1.1); + } + // The halyard: a laid rope whose lay marks travel with the hoist. + const off = S.y; + for (let y = 0; y < H; y++) { + const lay = (((floor(y - off) % 3) + 3) % 3) === 0 ? 0.62 : 0.42; + const k = (y * W + HX) * 3; + acc[k] = 0.92 * lay; + acc[k + 1] = 0.84 * lay; + acc[k + 2] = 0.68 * lay; + } + const [d, l] = SIZE[S.n]; + const word = S.word; + for (let i = 0; i < word.length; i++) { + const top = S.y + i * (d + GAP); + if (top > H + 8 || top + d < -8) continue; + flag(S, acc, word[i], top, d, l, i * 1.7); + // The toggle and the snap that join each flag to the halyard. + for (const yy of [top, top + d - 1]) { + const iy = round(yy); + if (iy >= 0 && iy < H) for (let x = HX; x <= X0; x++) { + const k = (iy * W + x) * 3; + acc[k] = 0.75; + acc[k + 1] = 0.72; + acc[k + 2] = 0.66; + } + } + } + // The plain letters, two LEDs a dot, 9 LEDs clear of the fly. + if (S.show > 0.01) { + const a = smooth(S.show), lx = X0 + l + 9; + for (let i = 0; i < word.length; i++) { + const g = FONT[word[i]], top = round(S.y + i * (d + GAP) + d / 2 - 7); + for (let gy = 0; gy < 7; gy++) + for (let gx = 0; gx < 5; gx++) { + if (g[gy * 5 + gx] !== "#") continue; + for (let q = 0; q < 4; q++) { + const x = lx + gx * 2 + (q & 1), y = top + gy * 2 + (q >> 1); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const k = (y * W + x) * 3; + acc[k] += (0.94 - acc[k]) * a; + acc[k + 1] += (0.92 - acc[k + 1]) * a; + acc[k + 2] += (0.86 - acc[k + 2]) * a; + } + } + } + } + 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 69015c83c5623ec160c8994d674de191d40b6545 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:02:24 +0000 Subject: [PATCH 08/15] =?UTF-8?q?incoming:=20Brick=20Factory=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4Jkt9PaeQhb1WkHMuJUh --- .../pattern-study/patterns/brick-factory.js | 352 ++++++++++++++++++ 1 file changed, 352 insertions(+) create mode 100644 incoming/pattern-study/patterns/brick-factory.js diff --git a/incoming/pattern-study/patterns/brick-factory.js b/incoming/pattern-study/patterns/brick-factory.js new file mode 100644 index 000000000..b0514cb5f --- /dev/null +++ b/incoming/pattern-study/patterns/brick-factory.js @@ -0,0 +1,352 @@ +// ===== Patternflow pattern ===== +// Title: Brick Factory +// Family: geometry +// Idea: The complete bipartite graph K(m,n) is drawn with straight edges, every crossing of two edges lit as a spark: its vertices are thrown about the panel and wander so crossings flare up and go out all over it, then the drawing relaxes into Zarankiewicz's, m vertices split about the middle of a horizontal line and n of a vertical one, where the live count at the foot of the panel falls to the number Z(m,n) beside it and holds there before the vertices are thrown again. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Turán's brick factory problem (Pál Turán, met in a labour camp's brick yard in 1944, published in his A note of welcome, Journal of Graph Theory, 1977), the drawing and the number Z(m,n) of Kazimierz Zarankiewicz (1954), shown to be the crossing number when min(m,n) <= 6 by Daniel Kleitman (1970); 5 by 7 figures from Photon Disk (pattern round 70); pattern round 74 +// Round: 74 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, m=4..6, n=6..9 +// +// Knob 1: Hue. Turns the edge colours round the wheel. Each of the m vertices on the horizontal line gives its edges one colour, the m colours spread over two thirds of the wheel, blues lifted towards white so they read beside gold: at the opening 0 and m = 4 teal, blue, rose and gold, at 0.5 red, lime, sea green and azure. The sparks stay white-gold. +// Knob 2: Speed. At 2 the vertices fly to new places in 1.2 s, wander for 2.6 s, relax into Zarankiewicz's drawing in 3 s and hold it 2.6 s, 9.4 s a round. Every time scales as 2 / Speed: 94 s a round at 0.2, 6.3 s at 3. +// Knob 3: m. The vertices on the horizontal line, the smaller side: 4 at the opening, split 2 and 2 about the middle, 5 split 3 and 2, 6 split 3 and 3. +// Knob 4: n. The vertices on the vertical line: 6 at the opening, split 3 above the middle and 3 below, up to 9, split 5 and 4. Z(m,n) runs from 12 for the opening K(4,6), which wanders through 22 to 84 crossings, to 96 for K(6,9), which wanders through 180 to 380. +// Buttons: K1 tap throws the vertices again at once. + +// K(m,n) has m vertices on one side and n on the other and an edge from +// every vertex of one side to every vertex of the other. In 1944 Turán +// asked how few crossings the rail tracks between m kilns and n storage +// yards could have. Zarankiewicz's drawing puts one side on the x axis and +// the other on the y axis, each split as evenly as it goes about the origin. +// Two edges cross only when they lie in the same quadrant and their ends +// are in opposite orders, so the count is the sum over the quadrants of +// C(p,2) C(q,2), which comes to +// Z(m,n) = floor(m/2) floor((m-1)/2) floor(n/2) floor((n-1)/2). +// Kleitman proved no drawing does better when the smaller side is 6 or +// less. That none does for any m and n is Zarankiewicz's conjecture, still +// open. +// +// Every frame tests each pair of edges that share no vertex, up to 1080 +// pairs for K(6,9), and remembers which pairs crossed, so a crossing that +// appears or goes out can flare where it happened. + +const W = 64, H = 128; +const { floor, round, min, max, abs, sqrt, sin, 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)); +const TAU = 2 * PI; +// The middle of the drawing, and the box the vertices are thrown into. +const OX = 31.5, OY = 58, BX0 = 4, BX1 = 59.5, BY0 = 4, BY1 = 112; +// Fly, wander, relax, hold, in seconds at Speed 2. +const DUR = [1.2, 2.6, 3, 2.6]; +const MAXE = 54, FLARE = 0.55; + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// 5 x 7 figures and Z from Photon Disk (pattern round 70), rows from the +// top. A 3 x 5 Z is a 2 whose middle bar has shrunk to one LED, and the +// two could not be told apart on the panel. +const GLYPH = { + 0: ".###. #...# #..## #.#.# ##..# #...# .###.", 1: "..#.. .##.. ..#.. ..#.. ..#.. ..#.. .###.", 2: ".###. #...# ....# ...#. ..#.. .#... #####", + 3: "##### ...#. ..#.. ...#. ....# #...# .###.", 4: "...#. ..##. .#.#. #..#. ##### ...#. ...#.", 5: "##### #.... ####. ....# ....# #...# .###.", + 6: "..##. .#... #.... ####. #...# #...# .###.", 7: "##### ....# ...#. ..#.. .#... .#... .#...", 8: ".###. #...# #...# .###. #...# #...# .###.", + 9: ".###. #...# #...# .#### ....# ...#. .##..", Z: "##### ....# ...#. ..#.. .#... #.... #####", +}; +for (const k in GLYPH) GLYPH[k] = GLYPH[k].replace(/ /g, ""); + +const zar = (m, n) => floor(m / 2) * floor((m - 1) / 2) * floor(n / 2) * floor((n - 1) / 2); + +// Zarankiewicz's places: the m vertices on the horizontal line, more of them +// on the left, and the n on the vertical line, more of them above. +function targets(S) { + const { m, n } = S, pl = Math.ceil(m / 2), pr = m - pl, qu = Math.ceil(n / 2), qd = n - qu; + const hs = 27.5 / pl, vs = (OY - 6) / qu; + S.tA = []; + for (let i = pl; i >= 1; i--) S.tA.push([OX - hs * i, OY]); + for (let i = 1; i <= pr; i++) S.tA.push([OX + hs * i, OY]); + S.tB = []; + for (let j = qu; j >= 1; j--) S.tB.push([OX, OY - vs * j]); + for (let j = 1; j <= qd; j++) S.tB.push([OX, OY + vs * j]); +} + +// New places at random: the box is cut into 3 by 5 cells, at most one +// vertex to a cell (m + n is 15 at most), so the throw spreads over the +// whole panel; each vertex gets its own slow wander. The vertical side +// draws its cells first, so a change of n moves the whole throw. +function throwVertices(S) { + const all = [...S.B, ...S.A], cells = [...Array(15).keys()]; + for (let i = cells.length - 1; i > 0; i--) { + const j = floor(Math.random() * (i + 1)); + [cells[i], cells[j]] = [cells[j], cells[i]]; + } + const cw = (BX1 - BX0) / 3, ch = (BY1 - BY0) / 5; + all.forEach((v, i) => { + const c = cells[i], x = BX0 + ((c % 3) + 0.15 + 0.7 * Math.random()) * cw, y = BY0 + (floor(c / 3) + 0.15 + 0.7 * Math.random()) * ch; + v.fx = v.x; + v.fy = v.y; + v.rx = x; + v.ry = y; + v.p1 = Math.random() * TAU; + v.p2 = Math.random() * TAU; + v.f1 = 0.25 + Math.random() * 0.3; + v.f2 = 0.2 + Math.random() * 0.3; + v.delay = Math.random() * 0.35; + }); + S.phase = 0; + S.pt = 0; +} + +// Each side goes to its places in the order it already has: the vertices of +// the horizontal side sorted left to right, the vertical side top to bottom, +// so the paths into the drawing cross as little as they can. +function assign(S) { + const a = S.A.map((v, i) => i).sort((i, j) => S.A[i].x - S.A[j].x); + a.forEach((i, k) => ((S.A[i].tx = S.tA[k][0]), (S.A[i].ty = S.tA[k][1]))); + const b = S.B.map((v, i) => i).sort((i, j) => S.B[i].y - S.B[j].y); + b.forEach((i, k) => ((S.B[i].tx = S.tB[k][0]), (S.B[i].ty = S.tB[k][1]))); +} + +function build(S, m, n) { + S.m = m; + S.n = n; + targets(S); + const mk = () => ({ x: OX, y: OY, fx: OX, fy: OY, rx: OX, ry: OY, tx: OX, ty: OY, p1: 0, p2: 0, f1: 0, f2: 0, delay: 0, sx: OX, sy: OY }); + S.A = Array.from({ length: m }, mk); + S.B = Array.from({ length: n }, mk); + throwVertices(S); + // The first frame lands them straight on their thrown places. + for (const v of [...S.A, ...S.B]) (v.x = v.fx = v.rx), (v.y = v.fy = v.ry); + S.phase = 1; + S.pt = 0; + S.was.fill(0); + S.quiet = true; + S.flares.length = 0; +} + +export function setup(params) { + params.s = { + t: 0, m: 0, n: 0, hue: 0, speed: 2, phase: 1, pt: 0, btn: false, A: [], B: [], tA: [], tB: [], + was: new Uint8Array(MAXE * MAXE), lastX: new Float32Array(MAXE * MAXE), lastY: new Float32Array(MAXE * MAXE), + cx: new Float32Array(1200), cy: new Float32Array(1200), count: 0, bump: 0, flares: [], quiet: true, + acc: new Float32Array(W * H * 3), spark: new Float32Array(W * H), col: [0, 0, 0], + }; + return params.s; +} + +// Where each vertex is in the current phase: flying out along a smooth path, +// wandering about its thrown place, or easing into Zarankiewicz's place. +function place(S, sc) { + const ph = S.phase, u = S.pt / (DUR[ph] * sc), t = S.t; + for (const v of [...S.A, ...S.B]) { + const wx = 5 * sin(t * v.f1 * TAU * 0.5 + v.p1) + 2 * sin(t * v.f2 * TAU + v.p2); + const wy = 7 * sin(t * v.f2 * TAU * 0.5 + v.p2) + 2.5 * sin(t * v.f1 * TAU + v.p1); + if (ph === 0) { + // Each vertex sets off up to 0.17 of the flight late; its wander + // grows in as it lands. + const e = smooth(cl((u - v.delay * 0.5) / (1 - 0.175), 0, 1)), k = smooth(e); + v.x = v.fx + (v.rx + wx * k - v.fx) * e; + v.y = v.fy + (v.ry + wy * k - v.fy) * e; + } else if (ph === 1) { + v.x = v.rx + wx; + v.y = v.ry + wy; + v.sx = v.x; + v.sy = v.y; + } else if (ph === 2) { + const e = smooth(cl((u - v.delay) / (1 - 0.35), 0, 1)); + v.x = v.sx + (v.tx - v.sx) * e; + v.y = v.sy + (v.ty - v.sy) * e; + } else { + v.x = v.tx; + v.y = v.ty; + } + v.x = cl(v.x, BX0, BX1); + v.y = cl(v.y, BY0, BY1); + } +} + +// Every pair of edges a_i b_j and a_k b_l with i != k and j != l; a pair +// that shares a vertex never crosses in a straight drawing. Each end of one +// edge is tested for the side of the other it lies on; the edges cross when +// both pairs of ends lie on opposite sides. Where a pair last crossed is +// kept, so a crossing that goes out flares where it was. +function crossings(S) { + const { A, B, m, n, was, lastX, lastY, cx, cy } = S; + let c = 0; + for (let i = 0; i < m; i++) + for (let j = 0; j < n; j++) { + const ax = A[i].x, ay = A[i].y, ex = B[j].x - ax, ey = B[j].y - ay, e1 = i * n + j; + for (let k = i + 1; k < m; k++) + for (let l = 0; l < n; l++) { + if (l === j) continue; + const kx = A[k].x, ky = A[k].y, fx = B[l].x - kx, fy = B[l].y - ky; + const d1 = ex * (ky - ay) - ey * (kx - ax), d2 = ex * (B[l].y - ay) - ey * (B[l].x - ax); + const d3 = fx * (ay - ky) - fy * (ax - kx), d4 = fx * (B[j].y - ky) - fy * (B[j].x - kx); + const e2 = k * n + l, key = e1 * MAXE + e2; + const hit = d1 * d2 < 0 && d3 * d4 < 0; + if (hit) { + const s = d3 / (d3 - d4), x = ax + s * ex, y = ay + s * ey; + cx[c] = x; + cy[c] = y; + c++; + lastX[key] = x; + lastY[key] = y; + } + if (hit !== !!was[key] && !S.quiet && S.flares.length < 400) S.flares.push({ x: lastX[key], y: lastY[key], age: 0, on: hit }); + was[key] = hit ? 1 : 0; + } + } + S.quiet = false; + return c; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 4, 6], d = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + S.speed = cl(k[1], 0.2, 3); + const m = round(cl(k[2], 4, 6)), n = round(cl(k[3], 6, 9)), btn = !!(input.btnPressed && input.btnPressed[0]); + if (m !== S.m || n !== S.n) build(S, m, n); + if (btn && !S.btn) throwVertices(S); + S.btn = btn; + const sc = 2 / S.speed; + S.t += d; + S.pt += d; + if (S.pt >= DUR[S.phase] * sc) { + S.pt = 0; + if (S.phase === 3) throwVertices(S); + else { + S.phase++; + if (S.phase === 2) assign(S); + } + } + place(S, sc); + const c = crossings(S); + if (c !== S.count) S.bump = 1; + S.count = c; + S.bump = max(0, S.bump - d / 0.35); + for (const f of S.flares) f.age += d / sc; + S.flares = S.flares.filter((f) => f.age < FLARE); +} + +// An antialiased line, added in: along the long axis each step lights the +// LEDs across it by a tent 1.3 LEDs either side of the true line, measured +// square to the line, so steep and shallow lines weigh the same. +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, lim = st ? H - 1 : W - 1, k = 1 / sqrt(1 + s * s); + for (let x = max(0, round(x0)); x <= min(lim, round(x1)); x++) { + const y = y0 + (x - x0) * s; + for (let iy = floor(y - 1.8); iy <= floor(y + 1.8); iy++) { + const e = 1 - (abs(iy + 0.5 - y) * k) / 1.3; + if (e > 0) add(acc, st ? iy : x, st ? x : iy, c, w * e * k * 1.3); + } + } +} +function add(acc, x, y, c, w) { + if (x < 0 || x >= W || y < 0 || y >= H || w <= 0) return; + const k = (y * W + x) * 3; + acc[k] += c[0] * w; + acc[k + 1] += c[1] * w; + acc[k + 2] += c[2] * w; +} +// A soft round light of radius r about x, y. +function glow(acc, x, y, r, c, w) { + for (let py = floor(y - r); py <= floor(y + r); py++) + for (let px = floor(x - r); px <= floor(x + r); px++) { + const dd = sqrt((px + 0.5 - x) ** 2 + (py + 0.5 - y) ** 2) / r; + if (dd < 1) add(acc, px, py, c, w * (1 - dd) * (1 - dd)); + } +} +function ring(acc, x, y, r, c, w) { + for (let py = floor(y - r - 1); py <= floor(y + r + 1); py++) + for (let px = floor(x - r - 1); px <= floor(x + r + 1); px++) { + const e = abs(sqrt((px + 0.5 - x) ** 2 + (py + 0.5 - y) ** 2) - r); + if (e < 1) add(acc, px, py, c, w * (1 - e)); + } +} +function text(acc, str, x, y, c, w) { + for (const ch of str) { + const g = GLYPH[ch]; + if (g) for (let i = 0; i < 35; i++) if (g[i] === "#") add(acc, x + (i % 5), y + floor(i / 5), c, w); + x += 6; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.A.length) return; + const acc = S.acc, col = S.col, { A, B, m, n } = S; + acc.fill(0); + // The edges, coloured by their end on the horizontal side. + const pal = []; + for (let i = 0; i < m; i++) { + const c = [0, 0, 0]; + hsv(S.hue + 0.47 + (0.66 * i) / max(1, m - 1), 0.78, 1, c); + // Blue and violet are dim on the panel next to gold and teal: lift + // them towards white until their luminance is at least 0.45. + const lum = 0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2], up = max(0, 0.45 - lum) * 0.7; + for (let q = 0; q < 3; q++) c[q] += (1 - c[q]) * up; + pal.push(c); + } + for (let i = 0; i < m; i++) for (let j = 0; j < n; j++) line(acc, A[i].x, A[i].y, B[j].x, B[j].y, pal[i], 0.5); + // Sparks at the crossings, kept on a layer of their own where the + // brightest wins, so a knot of crossings reads as many points and not as + // one white blot. Then the flares of those that just came or went. + const hot = [1, 0.86, 0.55], white = [1, 1, 1], sp = S.spark; + sp.fill(0); + for (let c = 0; c < S.count; c++) { + const x = S.cx[c], y = S.cy[c]; + for (let py = floor(y - 1.5); py <= floor(y + 1.5); py++) + for (let px = floor(x - 1.5); px <= floor(x + 1.5); px++) { + if (px < 0 || px >= W || py < 0 || py >= H) continue; + const dd = sqrt((px + 0.5 - x) ** 2 + (py + 0.5 - y) ** 2), v = dd < 0.75 ? 1 : 0.45 * max(0, 1 - (dd - 0.75) / 1.3); + if (v > sp[py * W + px]) sp[py * W + px] = v; + } + } + for (let p = 0; p < W * H; p++) { + const v = sp[p]; + if (!v) continue; + const k = p * 3, wv = v > 0.9 ? 1 : 0; + acc[k] += hot[0] * v + 0.2 * wv; + acc[k + 1] += hot[1] * v + 0.3 * wv; + acc[k + 2] += hot[2] * v + 0.45 * wv; + } + for (const f of S.flares) { + const u = f.age / FLARE, fade = (1 - u) * (1 - u); + if (f.on) ring(acc, f.x, f.y, 1 + 5 * u, hot, 1.1 * fade); + else glow(acc, f.x, f.y, 2 + 3 * u, hot, 1.1 * fade); + } + // The vertices: the horizontal side large in its colour, the vertical + // side small and white. + for (let i = 0; i < m; i++) { + glow(acc, A[i].x, A[i].y, 3.2, pal[i], 1.6); + glow(acc, A[i].x, A[i].y, 1.6, white, 0.9); + } + for (const v of B) glow(acc, v.x, v.y, 2.4, white, 1.3); + // The count at the foot, left, against Z(m,n), right. The count flashes + // when it changes and turns gold when it is down to Z. + const z = zar(m, n), at = S.count === z; + hsv(S.hue + 0.47, 0.25, 1, col); + const cc = at ? hot : col; + text(acc, String(S.count), 1, 119, cc, 0.75 + 0.25 * S.bump); + const zs = String(z); + text(acc, zs, 64 - 6 * zs.length, 119, at ? hot : col, at ? 0.95 : 0.6); + text(acc, "Z", 64 - 6 * zs.length - 9, 119, col, 0.3); + 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 24fb289516009f0b00500d1910ed482fedc23e4c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:04:17 +0000 Subject: [PATCH 09/15] =?UTF-8?q?incoming:=20Flag=20Hoist=20=EC=9D=98=20J?= =?UTF-8?q?=20=EA=B8=B0=EB=A5=BC=20=EA=B0=99=EC=9D=80=20=EC=84=B8=20?= =?UTF-8?q?=EB=9D=A0=EB=A1=9C=20=EA=B3=A0=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4Jkt9PaeQhb1WkHMuJUh --- incoming/pattern-study/patterns/flag-hoist.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/incoming/pattern-study/patterns/flag-hoist.js b/incoming/pattern-study/patterns/flag-hoist.js index a6490bcb4..85efb1e9a 100644 --- a/incoming/pattern-study/patterns/flag-hoist.js +++ b/incoming/pattern-study/patterns/flag-hoist.js @@ -68,7 +68,7 @@ const FLAGS = { G: (u) => (floor(u * 6) % 2 ? B : Y), H: (u) => (u < 0.5 ? Wt : R), I: (u, v, l, d) => (((u - 0.5) * l) ** 2 + ((v - 0.5) * d) ** 2 < (0.26 * d) ** 2 ? K : Y), - J: (u, v) => (v < 0.25 || v >= 0.75 ? B : Wt), + J: (u, v) => (v < 1 / 3 || v >= 2 / 3 ? B : Wt), K: (u) => (u < 0.5 ? Y : B), L: (u, v) => ((u < 0.5) === (v < 0.5) ? Y : K), M: (u, v, l, d) => (min(distLine(u * l, v * d, 0, 0, l, d), distLine(u * l, v * d, 0, d, l, 0)) < 0.1 * d ? Wt : B), From 2568e3ad5a9119988b712c7bd534a2537b008bfd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:06:40 +0000 Subject: [PATCH 10/15] =?UTF-8?q?incoming:=20Promotion=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4Jkt9PaeQhb1WkHMuJUh --- incoming/pattern-study/patterns/promotion.js | 292 +++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 incoming/pattern-study/patterns/promotion.js diff --git a/incoming/pattern-study/patterns/promotion.js b/incoming/pattern-study/patterns/promotion.js new file mode 100644 index 000000000..25770c8db --- /dev/null +++ b/incoming/pattern-study/patterns/promotion.js @@ -0,0 +1,292 @@ +// ===== Patternflow pattern ===== +// Title: Promotion +// Family: algo +// Idea: A random standard Young tableau of 8 by 16 cells fills the panel, its 128 entries rising along every row and down every column, each cell coloured by its entry and the gutters lit where the entries pass a quarter, a half and three quarters of 128 so three nested staircases show, and Schützenberger's promotion runs on it again and again: the 1 in the top-left corner lifts out as a white hole, the hole slides by jeu de taquin to the bottom-right corner, each step pulling in the smaller of its right and lower neighbours behind a short bright tail, every entry drops by one so the bands creep outward while the hole's path pushes them back, 128 goes into the corner, and a bar along the foot counts the promotions until, after exactly 128 of them, every cell is back where it started and the tableau flashes home. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: promotion of standard Young tableaux (Marcel-Paul Schützenberger, 1972) by jeu de taquin (Schützenberger, 1977), whose order on an a by b rectangle is exactly ab (Mark Haiman, 1992), the start of Brendon Rhoades's cyclic sieving for promotion (2010); the tableau drawn uniformly by the hook walk of Curtis Greene, Albert Nijenhuis and Herbert Wilf (1979); 3 by 5 digits copied from hydra-game.js (round 72); pattern round 74 +// Round: 74 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Size=-1..2, Levels=0..1 +// +// Knob 1: Hue. Turns the ramp round the wheel: at 0 the small entries in the top-left corner are azure, the middle ones violet and magenta, the large ones at the bottom-right orange and yellow; at 0.5 the ramp runs from red-orange through yellow and green to blue. +// Knob 2: Speed. At 2 the 1 lifts out in 0.25 s, each slide of the hole takes 0.08 s on 8 LED cells (the glide is 100 LEDs a second at every size), 128 goes in over 0.25 s: a promotion of the 8 by 16 tableau takes 2.26 s and the way home 4.8 minutes. Every time scales as 2 / Speed: at 3 the way home takes 3.3 minutes, at 0.2 a promotion takes 23 s. +// Knob 3: Size. The cell size, 8 LEDs at the opening 0: 8 by 16 cells, N = 128, 22 slides a promotion. At -1 cells of 4 LEDs, 16 by 32, N = 512, 46 slides and 20 minutes home; at 1 cells of 16 LEDs, 4 by 8, N = 32, 10 slides, 67 s home, each entry written in the cell; at 2 cells of 32 LEDs, 2 by 4, N = 8, 4 slides, 14 s home, the digits twice the size, and some of its 14 tableaux come home after 2 or 4 promotions, as cyclic sieving counts. A new size draws a new tableau. +// Knob 4: Levels. How many bands the ramp is cut into: 4 at the opening 0.06, so three staircases, the edges of the Young diagrams of entries up to N/4, N/2 and 3N/4, stand in the lit gutters and the cells brighten and dim once a band; 8 at 0.5 (every 16 entries on 8 by 16); 16 at 1, where the 15 staircases nest close as contour lines of the tableau. +// Buttons: K1 tap draws a new random tableau and starts the count again. + +// A standard Young tableau of a rectangular shape holds 1 to N once each, +// increasing along rows and down columns. Promotion: take out the 1 at +// the corner, and while the hole has a neighbour to its right or below, +// move the smaller of the two into it; the hole ends at the far corner. +// Take 1 from every entry and write N there. Haiman (1992) proved that on +// an a by b rectangle doing this N = ab times gives back the tableau you +// started from, for every tableau, though a single promotion moves only +// one staircase of cells. The bar along the foot counts to N; a white dot +// marks each cell that holds what it held at the start, and at N they all +// light at once. A tableau can come home sooner, after a divisor of N, and +// Rhoades (2010) counted how many do; on 8 by 16 a random one almost never. +// +// The cells holding 1 to m always form a Young diagram, a staircase from +// the top-right edge to the bottom-left; the gutters on its edge are lit +// for every m that is a multiple of N / Levels. A promotion turns the +// diagram of 1 to m + 1 into that of 1 to m by taking off the corner +// where the hole left it, so each staircase moves by at most a cell a +// promotion: the falling entries push it out and the path pulls it in. +// +// The start is uniform among all tableaux of the shape: the hook walk +// places N, N-1, ... 1 in turn, each by starting on a random free cell +// and jumping to a random cell of its hook, to the right or below, until +// it stands on a corner. + +const W = 64, H = 128; +const { floor, round, min, max, abs, exp, sin, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const frac = (v) => v - floor(v); +const ease = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); +function hsv(h, s, v, o, k) { + 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); + o[k] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + o[k + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + o[k + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// 3 by 5 digits, copied from hydra-game.js (round 70), rows top to bottom. +const DIG = ["111101101101111", "010110010010111", "111001111100111", "111001011001111", "101101111001001", + "111100111001111", "111100111101111", "111001010010010", "111101111101111", "111101111001111"]; + +// The hook walk (Greene, Nijenhuis and Wilf, 1979) on a rows of a cells. +function randomTableau(a, b) { + const N = a * b, T = new Int16Array(N), len = new Int16Array(b).fill(a); + for (let n = N; n >= 1; n--) { + // A uniformly random cell of the shape still free. + let pick = floor(Math.random() * n), r = 0; + while (pick >= len[r]) pick -= len[r++]; + let c = pick; + for (;;) { + const arm = len[r] - c - 1; + let leg = 0; + while (r + leg + 1 < b && len[r + leg + 1] > c) leg++; + if (arm + leg === 0) break; + const m = floor(Math.random() * (arm + leg)); + if (m < arm) c += 1 + m; + else r += 1 + m - arm; + } + T[r * a + c] = n; + len[r]--; + } + return T; +} + +// One promotion of T in place; returns the hole's path as cell indices. +function promote(T, a, b) { + const N = a * b, path = [0]; + let r = 0, c = 0; + for (;;) { + const right = c + 1 < a ? T[r * a + c + 1] : 1e9, down = r + 1 < b ? T[(r + 1) * a + c] : 1e9; + if (right === 1e9 && down === 1e9) break; + if (right < down) T[r * a + c] = right, c++; + else T[r * a + c] = down, r++; + path.push(r * a + c); + } + for (let i = 0; i < N; i++) T[i]--; + T[r * a + c] = N; + return path; +} + +function newTableau(s) { + const cell = 8 * 2 ** s.size, a = W / cell, b = H / cell; + Object.assign(s, { cell, a, b, N: a * b, k: 0, phase: 0, pt: 0, j: 0, u: 0 }); + s.T = randomTableau(a, b); + s.T0 = s.T.slice(); + s.home = new Uint8Array(s.N).fill(1); + s.eh = new Float32Array((a - 1) * b); + s.ev = new Float32Array(a * (b - 1)); + s.col = new Float32Array(s.N * 3); + startPromotion(s); +} + +// Work out the promotion ahead, then let the animation walk through it. +function startPromotion(s) { + s.next = s.T.slice(); + s.path = promote(s.next, s.a, s.b); + s.Wk = s.T.slice(); + s.phase = 0; + s.pt = 0; + s.j = 0; +} + +export function setup(params) { + const s = (params.s = { size: 0, hue: 0, speed: 2, levels: 4, flash: 0, btn: false }); + newTableau(s); + return s; +} + +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.speed = cl(k[1] == null ? 2 : k[1], 0.2, 3); + const size = round(cl(k[2] || 0, -1, 2)), btn = !!(input.btnPressed && input.btnPressed[0]); + if (size !== s.size || (btn && !s.btn)) (s.size = size), newTableau(s); + s.btn = btn; + // Levels: 4 bands of N / 4 entries at the opening, 16 at 1. + s.levels = round(4 * 2 ** (2 * cl(k[3] == null ? 0.06 : k[3], 0, 1))); + const sc = 2 / s.speed, tRem = 0.25 * sc, tStep = 0.01 * s.cell * sc, tIns = 0.25 * sc; + const L = s.path.length - 1, total = tRem + L * tStep + tIns; + // The hole's staircase fades in 0.3 s at Speed 2, a comet's tail. + const fade = exp(-dt / (0.3 * sc)); + for (let i = 0; i < s.eh.length; i++) s.eh[i] *= fade; + for (let i = 0; i < s.ev.length; i++) s.ev[i] *= fade; + s.flash = max(0, s.flash - dt / 1.4); + s.pt += dt; + // Phases: 0 the 1 lifts out, 1 the hole slides, 2 N goes in. + for (let guard = 0; guard < 64; guard++) { + if (s.phase === 0 && s.pt >= tRem) (s.phase = 1), (s.pt -= tRem), (s.Wk[0] = -1); + else if (s.phase === 1 && s.pt >= tStep && s.j < L) { + const from = s.path[s.j + 1], to = s.path[s.j]; + s.Wk[to] = s.Wk[from]; + s.Wk[from] = -1; + // Light the edge the hole crossed. + const a = s.a, lo = min(from, to); + if (abs(from - to) === 1) s.eh[floor(lo / a) * (a - 1) + (lo % a)] = 1; + else s.ev[lo] = 1; + s.j++; + s.pt -= tStep; + } else if (s.phase === 1 && s.j >= L) s.phase = 2; + else if (s.phase === 2 && s.pt >= tIns) { + s.pt -= tIns; + s.T = s.next; + s.k = (s.k + 1) % s.N; + let all = 1; + for (let i = 0; i < s.N; i++) (s.home[i] = s.T[i] === s.T0[i] ? 1 : 0), (all &= s.home[i]); + if (all) s.flash = 1; + startPromotion(s); + } else break; + } + const done = s.phase === 0 ? s.pt : s.phase === 1 ? tRem + s.j * tStep + min(s.pt, tStep) : tRem + L * tStep + s.pt; + s.u = cl(done / total, 0, 1); + s.f = s.phase === 0 ? s.pt / tRem : s.phase === 1 ? s.pt / tStep : s.pt / tIns; +} + +// The ramp: the hue runs 0.62 of the wheel from entry 1 to N, and the +// brightness waves once over each band of N / Levels entries, at its +// middle where the bands meet. +function colourOf(s, x, out, k) { + const N = s.N, t = (x - 1) / (N - 1); + hsv(0.55 + s.hue + 0.62 * t, 0.8, 0.84 + 0.14 * sin((PI * (x - 1) * s.levels) / N), out, k); +} + +export function draw(display, params) { + const s = params.s; + if (!s) return; + const { a, cell, N, Wk, col, path } = s, c1 = cell - 1, u = s.u; + const hole = [1, 0.97, 0.9]; + // Every cell's colour; the entries fall by u on the way to the next + // tableau, so the bands flow outward while a promotion runs. + const band = s.band && s.band.length === N ? s.band : (s.band = new Int16Array(N)), bw = N / s.levels; + for (let i = 0; i < N; i++) { + if (Wk[i] < 0) (col[i * 3] = hole[0]), (col[i * 3 + 1] = hole[1]), (col[i * 3 + 2] = hole[2]), (band[i] = -1); + else colourOf(s, Wk[i] - u, col, i * 3), (band[i] = floor((Wk[i] - u - 1 + 1e-6) / bw)); + } + const f = ease(s.f); + // The 1 lifting out turns white; N coming in comes out of white. + if (s.phase === 0) for (let q = 0; q < 3; q++) col[q] += (hole[q] - col[q]) * f; + const corner = N - 1, ins = [0, 0, 0]; + if (s.phase === 2) { + colourOf(s, N + 1 - u, ins, 0); + for (let q = 0; q < 3; q++) col[corner * 3 + q] = hole[q] + (ins[q] - hole[q]) * f; + } + // The gliding cell: during a slide it leaves path[j+1] (now drawn as the + // hole) and moves over the hole at path[j]. + let gx = -99, gy = -99, gc = null, gv = 0; + if (s.phase === 1 && s.j < path.length - 1) { + const from = path[s.j + 1], to = path[s.j]; + gc = [0, 0, 0]; + colourOf(s, Wk[from] - u, gc, 0); + gv = Wk[from]; + for (let q = 0; q < 3; q++) col[from * 3 + q] = hole[q]; + gx = ((from % a) + ((to % a) - (from % a)) * f) * cell; + gy = (floor(from / a) + (floor(to / a) - floor(from / a)) * f) * cell; + } + const fl = s.flash * 0.55, px = s.px || (s.px = new Float32Array(W * H * 3)); + for (let y = 0; y < H; y++) { + const row = floor(y / cell), iy = y - row * cell; + for (let x = 0; x < W; x++) { + const cc = floor(x / cell), ix = x - cc * cell, i = row * a + cc; + let r = 0.025, g = 0.025, b = 0.05; + if (ix < c1 && iy < c1) (r = col[i * 3]), (g = col[i * 3 + 1]), (b = col[i * 3 + 2]); + else { + // A gutter on a band's edge is lit: the cells with entries up to a + // multiple of N / a form a Young diagram, and these are its edges. + const me = band[i], rt = ix === c1 && cc + 1 < a ? band[i + 1] : me, dn = iy === c1 && row + 1 < s.b ? band[i + a] : me; + const dg = ix === c1 && iy === c1 && cc + 1 < a && row + 1 < s.b ? band[i + a + 1] : me; + if (me >= 0 && ((rt >= 0 && rt !== me) || (dn >= 0 && dn !== me) || (dg >= 0 && dg !== me))) (r = 0.62), (g = 0.6), (b = 0.66); + } + // The gliding block, with soft edges where it sits between LEDs. + if (gc) { + const ox = cl(min(x + 1 - gx, gx + c1 - x), 0, 1), oy = cl(min(y + 1 - gy, gy + c1 - y), 0, 1), w = ox * oy; + if (w > 0) (r += (gc[0] - r) * w), (g += (gc[1] - g) * w), (b += (gc[2] - b) * w); + } + if (fl > 0) (r += (1 - r) * fl), (g += (1 - g) * fl), (b += (1 - b) * fl); + const k = (y * W + x) * 3; + px[k] = r; + px[k + 1] = g; + px[k + 2] = b; + } + } + const put = (x, y, r, g, b, w) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const k = (y * W + x) * 3; + px[k] += (r - px[k]) * w; + px[k + 1] += (g - px[k + 1]) * w; + px[k + 2] += (b - px[k + 2]) * w; + }; + // The staircases the hole has left, through the cells' middles. + const lw = cell <= 4 ? 1 : cell <= 8 ? 2 : 3, off = round((c1 - lw) / 2), tc = [1, 0.93, 0.78], ta = 0.9; + for (let rr = 0; rr < s.b; rr++) + for (let cc = 0; cc < a; cc++) { + const x0 = cc * cell + off, y0 = rr * cell + off; + const eh = cc < a - 1 ? s.eh[rr * (a - 1) + cc] : 0, ev = rr < s.b - 1 ? s.ev[rr * a + cc] : 0; + if (eh > 0.01) for (let x = x0; x < x0 + cell + lw; x++) for (let d = 0; d < lw; d++) put(x, y0 + d, tc[0], tc[1], tc[2], ta * eh); + if (ev > 0.01) for (let y = y0; y < y0 + cell + lw; y++) for (let d = 0; d < lw; d++) put(x0 + d, y, tc[0], tc[1], tc[2], ta * ev); + } + // Entries written in the cells when they are 16 LEDs or more. + if (cell >= 16) { + const sz = cell >= 32 ? 2 : 1; + const write = (v, x0, y0) => { + const str = String(v), wdt = str.length * 4 * sz - sz; + let x = round(x0 + (c1 - wdt) / 2); + const y = round(y0 + (c1 - 5 * sz) / 2); + for (const ch of str) { + const bits = DIG[ch.charCodeAt(0) - 48]; + for (let p = 0; p < 15; p++) + if (bits[p] === "1") + for (let dy = 0; dy < sz; dy++) for (let dx = 0; dx < sz; dx++) put(x + (p % 3) * sz + dx, y + floor(p / 3) * sz + dy, 0.03, 0.02, 0.06, 0.88); + x += 4 * sz; + } + }; + for (let i = 0; i < N; i++) if (Wk[i] > 0 && !(gc && i === path[s.j + 1])) write(Wk[i], (i % a) * cell, floor(i / a) * cell); + if (gc) write(gv, gx, gy); + } + // A white dot in each cell that holds what it held at the start. + const dsz = cell <= 4 ? 1 : 2; + for (let i = 0; i < N; i++) + if (s.home[i] && Wk[i] > 0) { + const x0 = (i % a) * cell + c1 - dsz - (cell > 4 ? 1 : 0), y0 = floor(i / a) * cell + (cell > 4 ? 1 : 0); + for (let dy = 0; dy < dsz; dy++) for (let dx = 0; dx < dsz; dx++) put(x0 + dx, y0 + dy, 1, 1, 1, 0.95); + } + // The count: a bar along the bottom gutter, k + u of N, with a tick at + // each band. + const bar = ((s.k + u) / N) * W; + for (let x = 0; x < W; x++) { + const w = cl(bar - x, 0, 1), tick = ((x + 1) % (W / a) === 0) ? 0.25 : 0; + put(x, H - 1, 1, 0.85, 0.55, max(w * 0.9, tick)); + } + for (let i = 0; i < W * H; i++) + display.setPixel(i % W, floor(i / W), round(cl(px[i * 3], 0, 1) * 255), round(cl(px[i * 3 + 1], 0, 1) * 255), round(cl(px[i * 3 + 2], 0, 1) * 255)); +} From 4911a467232743b595bd110ba98dda8a123ab039 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:13:00 +0000 Subject: [PATCH 11/15] =?UTF-8?q?incoming:=20Reed-Solomon=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4Jkt9PaeQhb1WkHMuJUh --- .../pattern-study/patterns/reed-solomon.js | 511 ++++++++++++++++++ 1 file changed, 511 insertions(+) create mode 100644 incoming/pattern-study/patterns/reed-solomon.js diff --git a/incoming/pattern-study/patterns/reed-solomon.js b/incoming/pattern-study/patterns/reed-solomon.js new file mode 100644 index 000000000..3ce5e790a --- /dev/null +++ b/incoming/pattern-study/patterns/reed-solomon.js @@ -0,0 +1,511 @@ +// ===== Patternflow pattern ===== +// Title: Reed-Solomon +// Family: data +// Idea: A word becomes a real version 1 QR code, 21 modules of 3 LEDs, its codewords standing above as columns of bits, data and error correction in two colours, the bits snaking into the symbol from the bottom right and the mask swept over them; then a scratch, a blot or a bite spoils part of it, the decoder lights the syndromes and finds the bad codewords by Berlekamp-Massey, Chien search and Forney's formula, and if they are at most half the error correction codewords the spoiled modules flash back, otherwise the count turns red. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Reed-Solomon codes (Irving S. Reed and Gustave Solomon, 1960) in a version 1 QR code (Masahiro Hara, Denso Wave, 1994), decoded with the Berlekamp-Massey algorithm (Elwyn Berlekamp, 1968; James Massey, 1969), Chien search (Robert Chien, 1964) and Forney's formula (G. David Forney, 1965), pattern round 74 +// Round: 74 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Damage=8..60, Data=9..19 +// +// Knob 1: Hue. Turns data, error correction and damage round the wheel together, a quarter turn or more apart: cyan, violet and red at 0; red, yellow-green and cyan-blue at 0.5. Finders stay warm white, format bits pale gold. +// Knob 2: Speed. At 2 a word takes 10.9 s: set and computed 1.2 s, placed 1.8 s, masked 0.6 s, held 0.6 s, spoiled 0.5 s, read 0.7 s, errors found 0.8 s, repaired 1 s, held 3.2 s, cleared 0.5 s. The first word opens with its bits placed, just before the mask. At 0.2 each step takes ten times as long, at 3 two thirds. +// Knob 3: Damage. The spoiled area, 8 to 60 square modules, times a random 0.7 to 1.5 a word. A blot makes modules dark, a bite out of an edge and a scratch 1.2 modules wide make them light. At 8 a hurt mostly spoils 0 to 4 codewords and H, Q and M mend 996 in 1000. At 60 a blot 4.4 modules in radius spoils 2 to 11: H mends 74 percent, M 47, L 19. A scratch spoils only 2 to 7, crossing codewords 2 modules wide. +// Knob 4: Data. How many of the 26 codewords carry the word, and so the level: 9 is H (17 error correction codewords, mends 8), 13 is Q (13, mends 6), 16 is M (10, mends 5), 19 is L (7, mends 3). A syndrome cell for each error correction codeword, a budget slot for each one it mends. A new level rebuilds the symbol at once. +// Buttons: K1 tap spoils the finished symbol again with the next hurt. + +// A version 1 QR code holds 26 codewords of 8 bits. The first k carry the +// word in alphanumeric mode, padded with 236 and 17; the other n = 26 - k are +// the remainder of that polynomial times x^n divided by +// g(x) = (x - a^0)...(x - a^(n-1)) over GF(256), a = 2, built on 0x11D. So +// what is read, taken at a^0 ... a^(n-1), gives n syndromes that are all zero +// unless something changed. From them Berlekamp-Massey finds the shortest +// error locator, Chien search tries every position for its roots and +// Forney's formula gives the value to XOR back: up to n / 2 codewords. The +// QR standard trusts fewer at version 1 (2, 4, 6, 8 for L, M, Q, H) and +// keeps the rest against misreading; their budget slots are dimmer. +// +// The decoder reads the symbol as a scanner does once it has found it: the +// two copies of the 15 format bits, the nearest valid one (within 3 bits), +// the mask it names, then the 208 data modules in zig-zag order. + +const W = 64, H = 128, N = 21, SX = 1, SY = 38, CY = 11, TY = 1; +const { floor, round, min, max, abs, sqrt, sin, cos, atan2, exp, PI, hypot } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ease = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- GF(256) on 0x11D, a = 2. +const EXP = new Uint8Array(512), LOG = new Uint8Array(256); +for (let i = 0, x = 1; i < 255; i++) { + EXP[i] = EXP[i + 255] = x; + LOG[x] = i; + x <<= 1; + if (x & 256) x ^= 0x11d; +} +const mul = (a, b) => (a && b ? EXP[LOG[a] + LOG[b]] : 0); +const div = (a, b) => (a ? EXP[LOG[a] + 255 - LOG[b]] : 0); + +// Remainder of data(x) x^n mod g(x), highest power first. +function rsRemainder(data, n) { + let g = [1]; + for (let i = 0; i < n; i++) { + const h = new Array(g.length + 1).fill(0); + for (let j = 0; j < g.length; j++) (h[j] ^= g[j]), (h[j + 1] ^= mul(g[j], EXP[i])); + g = h; + } + const rem = new Array(n).fill(0); + for (const d of data) { + const f = d ^ rem.shift(); + rem.push(0); + for (let i = 0; i < n; i++) rem[i] ^= mul(g[i + 1], f); + } + return rem; +} +// The received polynomial at a^0 ... a^(n-1). +const syndromes = (cw, n) => Array.from({ length: n }, (_, j) => cw.reduce((v, c) => mul(v, EXP[j]) ^ c, 0)); +// Berlekamp-Massey: the shortest LFSR (error locator, lowest power first) +// that generates the syndromes. +function berlekamp(S) { + let C = [1], B = [1], L = 0, m = 1, b = 1; + for (let k = 0; k < S.length; k++) { + let d = S[k]; + for (let i = 1; i <= L; i++) d ^= mul(C[i] || 0, S[k - i]); + if (!d) { m++; continue; } + const T = C.slice(), coef = div(d, b); + for (let i = 0; i < B.length; i++) C[i + m] = (C[i + m] || 0) ^ mul(coef, B[i]); + if (2 * L <= k) (L = k + 1 - L), (B = T), (b = d), (m = 1); + else m++; + } + return { C, L }; +} +// Codeword i is the coefficient of x^(25 - i). Returns the syndromes, the +// codewords found wrong, the mended codewords and whether it worked. +function rsDecode(cw, n) { + const S = syndromes(cw, n), out = { S, errs: [], fixed: cw.slice(), ok: true }; + if (S.every((v) => !v)) return out; + const { C, L } = berlekamp(S); + out.ok = false; + if (L > n >> 1) return out; + // Chien search: position p is wrong where the locator vanishes at a^-p. + const ps = []; + for (let p = 0; p < 26; p++) { + let v = 0; + for (let i = 0; i < C.length; i++) v ^= mul(C[i], EXP[(255 - ((p * i) % 255)) % 255]); + if (!v) ps.push(p); + } + out.errs = ps.map((p) => 25 - p); + if (ps.length !== L) return out; + // Forney: e = X Omega(1/X) / Lambda'(1/X), Omega = S Lambda mod x^n. + const Om = new Array(n).fill(0); + for (let i = 0; i < n; i++) for (let j = 0; j <= i && j < C.length; j++) Om[i] ^= mul(C[j], S[i - j]); + for (const p of ps) { + const xi = EXP[(255 - p) % 255]; + let om = 0, dl = 0; + for (let i = n - 1; i >= 0; i--) om = mul(om, xi) ^ Om[i]; + for (let i = 1; i < C.length; i += 2) dl ^= mul(C[i], EXP[(LOG[xi] * (i - 1)) % 255]); + if (!dl) return out; + out.fixed[25 - p] ^= mul(EXP[p], div(om, dl)); + } + out.ok = syndromes(out.fixed, n).every((v) => !v); + return out; +} + +// ---- The message, in alphanumeric mode. +const ALNUM = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"; +const WORDS = ["REED", "SOLOMON", "GALOIS", "SYNDROME", "LIGHT", "KNOB", "PATTERN", "ERASURE"]; +function encode(word, k) { + const bits = [], put = (v, n) => { for (let i = n - 1; i >= 0; i--) bits.push((v >> i) & 1); }; + put(2, 4); + put(word.length, 9); + for (let i = 0; i + 1 < word.length; i += 2) put(45 * ALNUM.indexOf(word[i]) + ALNUM.indexOf(word[i + 1]), 11); + if (word.length % 2) put(ALNUM.indexOf(word[word.length - 1]), 6); + for (let i = 0; i < 4 && bits.length < 8 * k; i++) bits.push(0); + while (bits.length % 8) bits.push(0); + const out = []; + for (let i = 0; i < bits.length; i += 8) out.push(bits.slice(i, i + 8).reduce((a, b) => (a << 1) | b, 0)); + for (let pad = 0xec; out.length < k; pad ^= 0xec ^ 0x11) out.push(pad); + return out; +} +// What the data codewords say, read leniently: a wrong mode or length +// gives question marks, a pair past 44 x 45 + 44 two of them. +function parse(cw, k, len) { + let pos = 0; + const take = (n) => { + let v = 0; + for (let i = 0; i < n; i++, pos++) v = (v << 1) | (pos < 8 * k ? (cw[pos >> 3] >> (7 - (pos & 7))) & 1 : 0); + return v; + }; + const mode = take(4), count = take(9); + if (mode !== 2 || count > 25) return "?".repeat(len); + let s = ""; + for (let i = 0; i < count && pos < 8 * k; i += 2) { + if (i + 1 === count) s += ALNUM[take(6)] || "?"; + else { + const v = take(11); + s += v < 2025 ? ALNUM[floor(v / 45)] + ALNUM[v % 45] : "??"; + } + } + return s; +} + +// ---- The symbol. Levels in the order of the Data knob, with the two bits +// the format info gives each. +const LEVELS = [{ k: 9, bits: 2 }, { k: 13, bits: 3 }, { k: 16, bits: 0 }, { k: 19, bits: 1 }]; +const STD = [8, 6, 4, 2]; +const FN = new Uint8Array(N * N), BASE = new Uint8Array(N * N); +const setF = (x, y, v) => ((FN[y * N + x] = 1), (BASE[y * N + x] = v)); +for (let i = 0; i < N; i++) setF(6, i, +(i % 2 === 0)), setF(i, 6, +(i % 2 === 0)); +for (const [cx, cy] of [[3, 3], [N - 4, 3], [3, N - 4]]) + for (let dy = -4; dy <= 4; dy++) + for (let dx = -4; dx <= 4; dx++) { + const x = cx + dx, y = cy + dy, d = max(abs(dx), abs(dy)); + if (x >= 0 && x < N && y >= 0 && y < N) setF(x, y, +(d !== 2 && d !== 4)); + } +// The 15 format bits, bit i at FMT[0][i] beside the top left finder and +// again at FMT[1][i] split under the top right and beside the bottom left. +const FMT = [[], []]; +for (let i = 0; i <= 5; i++) FMT[0].push([8, i]); +FMT[0].push([8, 7], [8, 8], [7, 8]); +for (let i = 9; i < 15; i++) FMT[0].push([14 - i, 8]); +for (let i = 0; i < 8; i++) FMT[1].push([N - 1 - i, 8]); +for (let i = 8; i < 15; i++) FMT[1].push([8, N - 15 + i]); +const ISFMT = new Uint8Array(N * N); +for (const [x, y] of [...FMT[0], ...FMT[1]]) setF(x, y, 0), (ISFMT[y * N + x] = 1); +setF(8, N - 8, 1); +function formatBits(level, mask) { + const data = (LEVELS[level].bits << 3) | mask; + let rem = data; + for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >> 9) * 0x537); + return ((data << 10) | rem) ^ 0x5412; +} +// The data modules in placement order: two-module columns from the right, +// up then down, skipping the timing column. +const ORDER = []; +for (let right = N - 1; right >= 1; right -= 2) { + if (right === 6) right = 5; + for (let vert = 0; vert < N; vert++) + for (let j = 0; j < 2; j++) { + const x = right - j, y = ((right + 1) & 2) === 0 ? N - 1 - vert : vert; + if (!FN[y * N + x]) ORDER.push(y * N + x); + } +} +// Which bit of the 208 each module carries (-1 for function modules). +const BITOF = new Int16Array(N * N).fill(-1); +ORDER.forEach((p, i) => (BITOF[p] = i)); +// The eight masks: a module flips where its expression is 0. +function maskAt(m, p) { + const x = p % N, y = floor(p / N), a = (x * y) % 2, b = (x * y) % 3; + return +([(x + y) % 2, y % 2, x % 3, (x + y) % 3, (floor(x / 3) + floor(y / 2)) % 2, a + b, (a + b) % 2, (((x + y) % 2) + b) % 2][m] === 0); +} + +function compose(cw, level, m) { + const mat = BASE.slice(), f = formatBits(level, m); + ORDER.forEach((p, i) => (mat[p] = ((cw[i >> 3] >> (7 - (i & 7))) & 1) ^ maskAt(m, p))); + for (let i = 0; i < 15; i++) for (const c of FMT) mat[c[i][1] * N + c[i][0]] = (f >> i) & 1; + return mat; +} +// The standard's four penalties: runs of five or more, 2x2 blocks, finder +// look-alikes with four light modules on a side, and imbalance of dark. +function penalty(mat) { + let p = 0, dark = 0; + for (let pass = 0; pass < 2; pass++) + for (let a = 0; a < N; a++) { + const at = (b) => (b < 0 || b >= N ? 0 : pass ? mat[b * N + a] : mat[a * N + b]); + let run = 1; + for (let b = 1; b <= N; b++) { + if (b < N && at(b) === at(b - 1)) run++; + else (p += run >= 5 ? run - 2 : 0), (run = 1); + } + // Runs of exactly 1, 1, 3, 1, 1; off the symbol counts as light. + for (let b = 0; b + 6 < N; b++) + if (!at(b - 1) && at(b) && !at(b + 1) && at(b + 2) && at(b + 3) && at(b + 4) && !at(b + 5) && at(b + 6) && !at(b + 7)) { + if (!at(b - 2) && !at(b - 3) && !at(b - 4)) p += 40; + if (!at(b + 8) && !at(b + 9) && !at(b + 10)) p += 40; + } + } + for (let y = 0; y < N; y++) + for (let x = 0; x < N; x++) { + const v = mat[y * N + x]; + dark += v; + if (x < N - 1 && y < N - 1 && v === mat[y * N + x + 1] && v === mat[(y + 1) * N + x] && v === mat[(y + 1) * N + x + 1]) p += 3; + } + return p + 10 * (Math.ceil(abs(dark * 20 - N * N * 10) / (N * N)) - 1); +} +function build(word, level) { + const k = LEVELS[level].k, data = encode(word, k), cw = data.concat(rsRemainder(data, 26 - k)); + let best = 0, bp = 1e9; + for (let m = 0; m < 8; m++) { + const p = penalty(compose(cw, level, m)); + if (p < bp) (bp = p), (best = m); + } + return { word, level, k, n: 26 - k, cw, mask: best, mat: compose(cw, level, best) }; +} +// Read a symbol as a scanner does once it has found it. +function readSymbol(mat) { + let f = null, fd = 99; + for (const c of FMT) { + let v = 0; + for (let i = 0; i < 15; i++) v |= mat[c[i][1] * N + c[i][0]] << i; + for (let g = 0; g < 32; g++) { + let d = 0; + for (let x = v ^ formatBits(g >> 3, g & 7); x; x &= x - 1) d++; + if (d < fd) (fd = d), (f = { l: g >> 3, m: g & 7 }); + } + } + if (fd > 3) return null; + const cw = new Array(26).fill(0); + ORDER.forEach((p, i) => (cw[i >> 3] |= (mat[p] ^ maskAt(f.m, p)) << (7 - (i & 7)))); + return { level: f.l, mask: f.m, cw }; +} + +// ---- 5x7 capitals, digits and the alphanumeric mode's signs, drawn for +// this pattern: seven base-32 digits a glyph, rows from the top, 16 the left +// column. +const FONT_CH = ALNUM + "?"; +const FONT = + "ehjlphe4c4444eeh1248vv2421he26aiv22vgu11he68guhhev124888ehhehheehhf12cehhvhhhuhhuhhu" + + "ehggghesihhhisvgguggvvggugggehgnhhfhhhvhhhe44444e72222ichikokihggggggvhrllhhhhhpljhh" + + "ehhhhheuhhugggehhhliduhhukihfgge11uv444444hhhhhhehhhhha4hhhlllahha4ahhhhha444v1248gv" + + "00000004fke5u4op248j304lel40044v440000v00000000cc01248g00cc0cc0eh12404"; + +// ---- Layout, top to bottom: the word, marks over the codewords found +// wrong, the codeword columns, the symbol, the syndromes, the budget of +// codewords the level mends, the count. +const MARK_Y = 9, SYN_Y = 105, SLOT_Y = 112, CNT_Y = 118; +const colX = (i, k) => 5 + 2 * i + (i >= k ? 3 : 0); +const PHASES = ["write", "place", "mask", "hold0", "damage", "synd", "locate", "repair", "hold", "clear"]; +const DUR = [1.2, 1.8, 0.6, 0.6, 0.5, 0.7, 0.8, 1.0, 3.2, 0.5]; +const FUNC = [235, 212, 175], FMTC = [255, 238, 150]; + +// ---- Hurts, in modules: a scratch 1.2 wide and at most 30 long through +// the middle, a blot with a wobbling rim, a bite centred just off an edge. +function newHurt(s) { + const kind = s.kinds++ % 3, A = s.area * (0.7 + 0.8 * Math.random()), R = Math.random; + if (kind === 0) { + const len = min(A / 1.2, 30), th = R() * PI, cx = 6 + 9 * R(), cy = 6 + 9 * R(), dx = (cos(th) * len) / 2, dy = (sin(th) * len) / 2; + return { kind, ax: cx - dx, ay: cy - dy, bx: cx + dx, by: cy + dy, dark: 0 }; + } + if (kind === 1) return { kind, cx: 3 + 15 * R(), cy: 3 + 15 * R(), r: sqrt(A / PI), ph: R() * 7, dark: 1 }; + const u = 4 + 13 * R(), side = floor(R() * 4); + const [cx, cy] = [[u, -0.3], [N + 0.3, u], [u, N + 0.3], [-0.3, u]][side]; + return { kind, cx, cy, r: sqrt((2 * A) / PI), ph: R() * 7, dark: 0 }; +} +function inHurt(h, u, v, g) { + if (!h || g <= 0) return false; + if (h.kind === 0) { + const bx = (h.bx - h.ax) * g, by = (h.by - h.ay) * g; + const t = cl(((u - h.ax) * bx + (v - h.ay) * by) / (bx * bx + by * by + 1e-9), 0, 1); + return hypot(u - h.ax - t * bx, v - h.ay - t * by) <= 0.6; + } + const dx = u - h.cx, dy = v - h.cy, a = atan2(dy, dx); + const rim = h.kind === 1 ? 1 + 0.06 * sin(2 * a + 3 * h.ph) + 0.07 * sin(5 * a + h.ph) + 0.05 * sin(8 * a + 2 * h.ph) : 0.9 + 0.14 * abs(sin(5 * a + h.ph)); + return hypot(dx, dy) <= h.r * rim * g; +} +// The symbol with the hurt grown to g, and the codewords read from it with +// the true mask, for the columns, the word and the count. +function spoil(s, g) { + const sym = s.sym; + for (let p = 0; p < N * N; p++) s.dmg[p] = inHurt(s.hurt, (p % N) + 0.5, floor(p / N) + 0.5, g) ? s.hurt.dark : sym.mat[p]; + s.seen.fill(0); + ORDER.forEach((p, i) => (s.seen[i >> 3] |= (s.dmg[p] ^ maskAt(sym.mask, p)) << (7 - (i & 7)))); + s.hit = s.seen.reduce((a, v, i) => a + (v !== sym.cw[i]), 0); +} + +function newWord(s, level, phase) { + const sym = build(WORDS[s.wi % WORDS.length], level); + Object.assign(s, { sym, dmg: sym.mat.slice(), seen: sym.cw.slice(), hurt: null, dec: null, hit: 0, ph: PHASES.indexOf(phase), pt: 0 }); +} + +export function setup(params) { + params.s = { wi: 0, kinds: 1, area: 8, hue: 0, sc: 1, level: -1, btn: false, rgb: new Float32Array(W * H * 3) }; + return params.s; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const s = params.s, k = input.knobValues || [0, 2, 8, 9], d = cl(dt || 0, 0, 0.1); + s.hue = k[0] || 0; + s.sc = 2 / cl(k[1] == null ? 2 : k[1], 0.2, 3); + s.area = cl(k[2] == null ? 8 : k[2], 8, 60); + // Data 9, 13, 16, 19: the nearest level. + const data = cl(k[3] == null ? 9 : k[3], 9, 19); + let level = 0; + LEVELS.forEach((L, i) => abs(L.k - data) < abs(LEVELS[level].k - data) && (level = i)); + // The first word, and a new level, start with the bits in place and the + // mask about to be swept over them. + if (level !== s.level) (s.level = level), newWord(s, level, "mask"); + const btn = !!(input.btnPressed && input.btnPressed[0]); + if (btn && !s.btn && s.ph >= 3) Object.assign(s, { dmg: s.sym.mat.slice(), seen: s.sym.cw.slice(), dec: null, ph: 4, pt: 0, hurt: newHurt(s) }); + s.btn = btn; + s.pt += d; + while (s.pt >= DUR[s.ph] * s.sc) { + s.pt -= DUR[s.ph] * s.sc; + if (++s.ph === PHASES.length) { + s.wi++; + newWord(s, s.level, "write"); + } + const ph = PHASES[s.ph]; + if (ph === "damage") s.hurt = newHurt(s); + if (ph === "synd") { + spoil(s, 1); + // The decoder reads the spoiled symbol on its own. + const r = readSymbol(s.dmg); + if (!r) s.dec = { fmt: false, ok: false, good: false, S: [], errs: [] }; + else { + const dec = rsDecode(r.cw, 26 - LEVELS[r.level].k); + dec.fmt = true; + dec.good = dec.ok && r.level === s.sym.level && dec.fixed.every((v, i) => v === s.sym.cw[i]); + s.dec = dec; + } + } + } + if (PHASES[s.ph] === "damage") spoil(s, ease(s.pt / (DUR[s.ph] * s.sc))); +} + +function put(rgb, x, y, c) { + const o = (y * W + x) * 3; + if (x >= 0 && x < W) (rgb[o] = c[0]), (rgb[o + 1] = c[1]), (rgb[o + 2] = c[2]); +} +function glyph(rgb, ch, x0, y0, col) { + const g = FONT_CH.indexOf(ch); + for (let r = 0; r < 7 && g >= 0; r++) for (let c = 0; c < 5; c++) if ((parseInt(FONT[g * 7 + r], 32) >> (4 - c)) & 1) put(rgb, x0 + c, y0 + r, col); +} +function fill(rgb, x0, y0, w, h, col) { + for (let y = y0; y < y0 + h; y++) for (let x = x0; x < x0 + w; x++) put(rgb, x, y, col); +} +const sc3 = (c, f) => [c[0] * f, c[1] * f, c[2] * f]; +const mix = (a, b, f) => [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f, a[2] + (b[2] - a[2]) * f]; + +export function draw(display, params, time) { + const s = params.s; + if (!s || !s.sym) return; + const rgb = s.rgb, sym = s.sym, K = sym.k, n = sym.n, t = n >> 1; + const u = s.pt / (DUR[s.ph] * s.sc), at = s.ph, T = time || 0; + // Data, error correction and damage stay a quarter of the wheel or more + // apart whatever the hue: cyan, violet and red at 0. + const DATA = hsv(0.5 + s.hue, 0.75, 255), ECC = hsv(0.75 + s.hue, 0.6, 255), RED = hsv(0.02 + s.hue, 0.88, 255), WHITE = [255, 255, 255]; + const good = s.dec && s.dec.good, fail = s.dec && !s.dec.good && at >= 6; + rgb.fill(0); + + // The word: typed in while it is encoded, then read from what the + // codewords say, red where that is wrong. + const word = sym.word; + let shown = word; + if (at >= 4 && !(at >= 7 && good && (at > 7 || u > 0.5))) shown = parse(s.seen, K, word.length).slice(0, 10); + const tx = round((W - (6 * shown.length - 1)) / 2); + for (let i = 0; i < shown.length; i++) { + if (at === 0 && u < ((i + 1) / word.length) * 0.33) break; + // Once the decoder gives up, none of it can be trusted. + glyph(rgb, shown[i], tx + 6 * i, TY, shown[i] === word[i] && !fail ? [235, 235, 225] : RED); + } + + // The codeword columns, 8 bits each, data then error correction. + const flashFix = at === 7 && good ? exp(-(((u - 0.45) / 0.15) ** 2)) : 0; + for (let i = 0; i < 26; i++) { + if (at === 0 && u < (i < K ? 0.33 + (0.34 * i) / K : 0.67 + (0.33 * (i - K)) / n)) continue; + const x = colX(i, K), base = i < K ? DATA : ECC; + const v = at >= 4 && !(at >= 7 && good && u > 0.45) && !(at > 7 && good) ? s.seen[i] : sym.cw[i]; + for (let b = 0; b < 8; b++) { + const bit = (v >> (7 - b)) & 1, wrong = bit !== ((sym.cw[i] >> (7 - b)) & 1); + let col = wrong ? sc3(RED, bit ? 1 : 0.3) : sc3(base, bit ? 1 : 0.16); + if (at === 1) { + const head = (u * 208) | 0, idx = i * 8 + b; + if (idx < head && idx > head - 6) col = mix(col, WHITE, 1 - (head - idx) / 6); + } + if (flashFix && ((s.seen[i] ^ sym.cw[i]) >> (7 - b)) & 1) col = mix(col, WHITE, flashFix); + fill(rgb, x, CY + 3 * b, 1, 2, col); + } + } + // Marks over the codewords the decoder finds. + if (s.dec && at >= 6 && at <= 8) { + const errs = s.dec.errs; + errs.forEach((i, j) => { + if (at === 6 && u < (j + 1) / (errs.length + 1)) return; + fill(rgb, colX(i, K), MARK_Y, 1, 1, fail ? RED : [255, 210, 60]); + }); + } + + // The symbol, 3 LEDs to a module. + const fade = at === 9 ? 1 - u : 1, fnIn = at === 0 ? 0 : at === 1 ? cl(u * 5, 0, 1) : 1; + const hurtG = at === 4 ? ease(u) : at >= 5 ? 1 : 0; + const heal = good ? (at === 7 ? ease((u - 0.2) / 0.5) : at > 7 ? 1 : 0) : 0; + const pulse = fail ? 0.75 + 0.25 * sin(T * 6) : 1; + for (let my = 0; my < N; my++) + for (let mx = 0; mx < N; mx++) { + const p = my * N + mx, bi = BITOF[p], isF = FN[p]; + let v = sym.mat[p], col; + if (isF) { + if (fnIn <= 0) continue; + const fmt = ISFMT[p]; + // The format bits arrive with the mask they name. + if (fmt && at < 2) continue; + if (fmt && at === 2 && mx + my > u * 42) continue; + col = sc3(fmt ? FMTC : FUNC, fnIn); + } else { + const ci = bi >> 3; + col = ci < K ? DATA : ECC; + if (at === 0) continue; + if (at === 1) { + const head = (u * 208) | 0; + if (bi >= head) continue; + v = (sym.cw[ci] >> (7 - (bi & 7))) & 1; + if (bi > head - 6) col = mix(col, WHITE, 1 - (head - bi) / 6), (v = 1); + } else if (at === 2) { + const front = u * 42 - (mx + my); + if (front < 0) v = (sym.cw[ci] >> (7 - (bi & 7))) & 1; + else if (maskAt(sym.mask, p) && front < 3) col = mix(col, WHITE, 1 - front / 3), (v = 1); + } + } + if (!v) col = [0, 0, 0]; + const wrongMod = !isF && s.dmg[p] !== sym.mat[p]; + for (let dy = 0; dy < 3; dy++) + for (let dx = 0; dx < 3; dx++) { + const x = SX + 3 * mx + dx, y = SY + 3 * my + dy; + let c = col; + if (hurtG > 0 && inHurt(s.hurt, (x - SX + 0.5) / 3, (y - SY + 0.5) / 3, hurtG)) { + const hc = sc3(RED, (s.hurt.dark ? 1 : 0.38) * pulse); + c = mix(hc, col, heal); + } + put(rgb, x, y, wrongMod && flashFix ? mix(c, WHITE, flashFix) : c); + } + } + + // Syndromes: one cell each, dark when zero. + const cw = min(7, floor(62 / n)), sx = round((W - cw * n + 1) / 2); + for (let j = 0; j < n; j++) { + let col = [26, 26, 32]; + const S = s.dec && s.dec.fmt ? s.dec.S[j] || 0 : 0; + const on = s.dec && at >= 5 && at <= 8 && !(at === 5 && u < j / n) && !(good && (at > 7 || (at === 7 && u > 0.35 + (0.5 * j) / n))); + if (on && s.dec.fmt && S) col = hsv(S / 255, 0.55, 255); + else if (on && !s.dec.fmt) col = sc3(RED, 0.4 + 0.3 * sin(T * 6)); + fill(rgb, sx + cw * j, SYN_Y, cw - 1, 4, col); + } + // The budget: one slot for each codeword the code can mend; the slots + // the standard holds back against misreading are dimmer. + const sw = t > 6 ? 5 : 7, bx = round((W - (sw + 2) * t + 2) / 2), found = s.dec ? s.dec.errs.length : 0; + for (let j = 0; j < t; j++) { + let col = j < STD[sym.level] ? [44, 44, 52] : [34, 16, 40]; + if (s.dec && at >= 6 && at <= 8) { + if (fail) col = sc3(RED, pulse); + else if (j < found && !(at === 6 && u < (j + 1) / (found + 1))) col = [255, 210, 60]; + } + fill(rgb, bx + (sw + 2) * j, SLOT_Y, sw, 3, col); + } + // The count: codewords spoiled over codewords the code can mend. + const cnt = `${at >= 4 && at <= 8 ? s.hit : 0}/${t}`, cx = round((W - (6 * cnt.length - 1)) / 2); + const ccol = at < 4 || at > 8 || !s.hit ? [120, 120, 120] : s.hit > t || fail ? RED : good && at >= 7 ? [120, 255, 140] : [235, 235, 225]; + for (let i = 0; i < cnt.length; i++) glyph(rgb, cnt[i], cx + 6 * i, CNT_Y, ccol); + + for (let i = 0; i < W * H; i++) { + const j = i * 3; + display.setPixel(i % W, (i / W) | 0, round(cl(rgb[j] * fade, 0, 255)), round(cl(rgb[j + 1] * fade, 0, 255)), round(cl(rgb[j + 2] * fade, 0, 255))); + } +} From 21577b7ec22c672cbba8cbee269cde9ee788d867 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:15:16 +0000 Subject: [PATCH 12/15] =?UTF-8?q?incoming:=20Rational=20Tangles=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4Jkt9PaeQhb1WkHMuJUh --- .../patterns/rational-tangles.js | 500 ++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 incoming/pattern-study/patterns/rational-tangles.js diff --git a/incoming/pattern-study/patterns/rational-tangles.js b/incoming/pattern-study/patterns/rational-tangles.js new file mode 100644 index 000000000..9f32fa061 --- /dev/null +++ b/incoming/pattern-study/patterns/rational-tangles.js @@ -0,0 +1,500 @@ +// ===== Patternflow pattern ===== +// Title: Rational Tangles +// Family: topology +// Idea: Two coloured ropes hang down the tall panel as a four-strand plat, capped at the top and loose at the bottom, and Conway's tangle dance plays on them: a twist T swaps the two middle ends at the foot so that the ropes really wind one more crossing in (x to x + 1), a rotate R carries all four ends one place round (x to -1/x), the fraction p/q in small figures changes with each call, and after a random string of calls the bottom ends close up into the two-bridge knot or link N(p/q), named by Schubert's classification, open again, and Conway's rule, rotate when positive and twist when negative, untangles the ropes back to 0. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: rational tangles and their fractions (John H. Conway, "An enumeration of knots and links, and some of their algebraic properties", 1970) and the square dance of twists and rotations he used to show them; classification of two-bridge knots and links (Horst Schubert, "Knoten mit zwei Brücken", 1956); 3 by 5 capitals from Typewriter (pattern round 18) and 3 by 5 figures from Josephus (pattern round 68); pattern round 74 +// Round: 74 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Depth=6..14, Rope=-1..1 +// +// Knob 1: Hue. Turns both ropes round the wheel together. At 0 rope A, the one capped on the left at [0], is cyan and rope B amber; at 0.5 A is red and B azure. Each rope keeps its colour through every call. A closed knot is one loop, so one rope takes the other's colour while the ends join; a closed link keeps both. The fraction is white, the calls grey, the undoing calls tinted like rope A, the name amber. +// Knob 2: Speed. At 2 a call takes 0.75 s and a pause of 0.25 s, the ropes settle into the simplest picture in 0.45 s after one call in five, the ends close in 0.8 s, the knot holds for 2.6 s and opens in 0.8 s, and the dance waits 1.2 s at 0, so a dance lasts 21 s on average at Depth 6 and 36 s at 14. Everything scales as 2 / Speed: at 0.2 a call takes 7.5 s, at 3 half a second. +// Knob 3: Depth. How many calls build the tangle, 6 to 14, drawn at random, T 65 times in 100, never R twice running and never R at 0, which would give 1/0, where a twist changes nothing. A dance is kept when it ends at p/q with |p| at least 3, holds at least 1 + floor((Depth - 6) / 4) calls of R (1 up to 9, 3 at 14) and never needs more than 12 crossings. At 6 the closure is a trefoil about two times in three and a figure-eight otherwise; at 10 it ranges over 4/1, 7/4, 10/7, 8/3, 13/8 and some thirty more. A crossing is 88 rows over the most crossings the dance will need, 16 at most: 16 at Depth 6, a median 14.7 at 8 and 10 and 12.6 at 12 and 14, 8.8 at the least. Conway's rule then undoes the tangle in 8 calls on average at Depth 6 and 14 at 14. +// Knob 4: Rope. The rope's width, 3 x 2^Rope LEDs: 1.5 at -1, 3.1 at the opening 0.06 and 6 at 1, where the ropes nearly fill the 14 LEDs between neighbouring strands. The gap left where a rope passes under grows with it. +// Buttons: K1 tap starts a new dance from 0. + +// A rational tangle is two strings in a ball with their four ends on the +// boundary, NW, NE, SE and SW. Conway gave each one a fraction: [0] is two +// arcs joining NW to NE and SW to SE, twisting the two right ends adds 1, +// and turning the ball a quarter turn sends x to -1/x. Every rational tangle +// is reached from [0] by twists and turns, and two are the same tangle +// exactly when their fractions agree (Conway 1970; full proofs by Jay +// Goldman and Louis Kauffman, 1997, and Kauffman and Sofia Lambropoulou, +// 2004). Conway showed it with four people holding two ropes: on "twist" +// the two on the right swap places, one passing under the other's rope; on +// "rotate" all four step one place round; and to untangle any tangle, rotate +// when its fraction is positive and twist when it is negative. +// Closing NW to NE and SW to SE, the numerator closure, gives the +// two-bridge link b(p, q); Schubert showed in 1956 that b(p, q) and b(p', q') +// are the same unoriented link exactly when p = p' and q' = q or q q' = 1 +// mod p, and allowing mirror images, q' = -q or q q' = -1 as well. +// +// The panel draws the tangle as a plat: four lanes across the panel, ends 1 +// to 4 at the foot standing for NW, NE, SE and SW. The top is closed by caps, +// 1-2 and 3-4 for [0], 2-3 inside 1-4 for [1/0]. Below come crossings, each +// between lanes 2 and 3 (a horizontal twist, H) or lanes 1 and 2 (a vertical +// twist, V, the twist of NW and NE). A T adds an H crossing at the foot, or +// takes one away if the last crossing there was the opposite hand. An R +// moves every end one place round the boundary: a crossing of lanes 2 and 3 +// becomes one of lanes 3 and 4, which on a rational tangle is the same as +// one of lanes 1 and 2 (a flype), one of lanes 1 and 2 becomes one of 2 and +// 3, every crossing keeps its hand and the caps swap. Worked through the +// fractions that is x to -1/x. The picture is then the alternating one of +// the regular continued fraction, all crossings of one sign alternating over +// and under. A call that leaves a picture of another form, mostly a T just +// after an R, is followed by the ropes settling into this one, their four +// ends held still. Which hand counts as +1 is a convention; +// the other choice would show every fraction as its negative, the mirror. + +const W = 64, H = 128; +const { floor, round, min, max, abs, sqrt, sin, cos, sign, 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)); +const gcd = (a, b) => { + (a = abs(a)), (b = abs(b)); + while (b) [a, b] = [b, a % b]; + return a; +}; +// The lanes' centres, the top of the crossings, the foot, the most crossings. +const X = [10.5, 24.5, 38.5, 52.5], YS = 28, YE = 116, MAXC = 12; +// Seconds at Speed 2. +const T_MOVE = 0.75, T_SETTLE = 0.45, T_PAUSE = 0.25, T_CLOSE = 0.8, T_HOLD = 2.6, T_REST = 1.2; + +// ---- Fractions: p / q with q >= 0, 1/0 for infinity. +function norm(p, q) { + if (q < 0 || (q === 0 && p < 0)) (p = -p), (q = -q); + const g = gcd(p, q) || 1; + return [p / g, q / g]; +} +const twist = ([p, q]) => (q === 0 ? [1, 0] : norm(p + q, q)); +const turn = ([p, q]) => norm(-q, p); + +// ---- Diagrams: caps 0 or 1, and crossings from the top, each { p, o }: +// p 1 between lanes 2 and 3, p 0 between lanes 1 and 2; o +1 when the +// strand going down to the right passes over, -1 when it passes under. +// The regular continued fraction, peeled from the foot: an H run of the +// whole part while |x| >= 1, else a V run of the whole part of 1/x. +function canon(p, q) { + const runs = []; + let caps; + for (;;) { + if (q === 0) { + caps = 1; + break; + } + if (p === 0) { + caps = 0; + break; + } + if (abs(p) >= q) { + const a = sign(p) * floor(abs(p) / q); + p -= a * q; + runs.push([1, a]); + } else { + const b = sign(p) * floor(q / abs(p)); + q -= b * p; + runs.push([0, b]); + } + } + const steps = []; + for (let i = runs.length - 1; i >= 0; i--) { + const [t, a] = runs[i]; + for (let k = 0; k < abs(a); k++) steps.push({ p: t, o: t ? sign(a) : -sign(a) }); + } + return { caps, steps }; +} +// The literal moves on a picture. +function litT(D) { + const st = D.steps.slice(), l = st[st.length - 1]; + if (l && l.p === 1 && l.o === -1) st.pop(); + else st.push({ p: 1, o: 1 }); + return { caps: D.caps, steps: st }; +} +const litR = (D) => ({ caps: D.caps ^ 1, steps: D.steps.map((s) => ({ p: s.p ^ 1, o: s.o })) }); +const same = (A, B) => A.caps === B.caps && A.steps.length === B.steps.length && A.steps.every((s, i) => s.p === B.steps[i].p && s.o === B.steps[i].o); +// Which rope (0 or 1) runs in each lane below the last crossing. +function ropesAtFoot(D) { + const r = D.caps ? [1, 0, 0, 1] : [0, 0, 1, 1]; + for (const s of D.steps) [r[s.p], r[s.p + 1]] = [r[s.p + 1], r[s.p]]; + return r; +} + +// ---- Names of N(p/q), keyed by p and the least of q, -q, 1/q and -1/q +// mod p, with Rolfsen's table numbers; "^2_1" is superscript 2, subscript 1. +const NAMES = { + "2/1": "HOPF LINK", "3/1": "TREFOIL 3_1", "4/1": "SOLOMON 4^2_1", "5/1": "CINQUEFOIL 5_1", "5/2": "FIGURE-8 4_1", + "6/1": "LINK 6^2_1", "7/1": "KNOT 7_1", "7/2": "KNOT 5_2", "8/3": "WHITEHEAD 5^2_1", + "9/1": "KNOT 9_1", "9/2": "STEVEDORE 6_1", "10/3": "LINK 6^2_2", "11/2": "KNOT 7_2", "11/3": "KNOT 6_2", + "12/5": "LINK 6^2_3", "13/3": "KNOT 7_3", "13/5": "KNOT 6_3", "15/4": "KNOT 7_4", "17/5": "KNOT 7_5", + "19/7": "KNOT 7_6", "21/8": "KNOT 7_7", +}; +function inverse(q, p) { + for (let k = 1; k < p; k++) if ((q * k) % p === 1) return k; + return 0; +} +function nameOf([p, q]) { + p = abs(p); + if (p === 0) return "UNLINK"; + if (p === 1) return "UNKNOT 0_1"; + const r = ((q % p) + p) % p, iv = inverse(r, p), c = min(r, p - r, iv, p - iv); + const key = p + "/" + c; + if (NAMES[key]) return NAMES[key]; + // Unnamed here: the crossing number, the sum of the continued fraction. + let a = p, b = c, n = 0; + while (b) (n += floor(a / b)), ([a, b] = [b, a % b]); + return n + "-CROSSING " + (p % 2 ? "KNOT" : "LINK"); +} + +// ---- 3 x 5 capitals from Typewriter (copied from autogram.js), 3 x 5 +// figures from Josephus (copied from josephus.js), and a slash and a +// lemniscate drawn here. +const G35 = { + A: "010101111101101", C: "011100100100011", D: "110101101101110", E: "111100110100111", F: "111100110100100", + G: "011100101101011", H: "101101111101101", I: "111010010010111", 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", "-": "000000111000000", "/": "001001010100100", +}; +const FIG = [0b111101101101111, 0b010110010010111, 0b111001111100111, 0b111001111001111, 0b101101111001001, 0b111100111001111, 0b111100111101111, 0b111001001001001, 0b111101111101111, 0b111101111001111]; +const LEMNI = ".#.#." + "#.#.#" + ".#.#."; + +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 dance: the calls that build it and Conway's calls that undo it, +// each with the picture before it (from), the literal picture after it (L) +// and the alternating one (C). Ropes are labelled from the caps down, so a +// new picture can swap the labels; f says which label rope A has, chosen so +// that each rope keeps its colour: an R carries the end in lane i to lane +// i + 1, and while the ropes settle no end moves. +function step(m, x, D, f) { + const nx = m === "T" ? twist(x) : turn(x), L = m === "T" ? litT(D) : litR(D), C = canon(nx[0], nx[1]); + const a = ropesAtFoot(D), b = ropesAtFoot(L), c = ropesAtFoot(C); + const fL = m === "R" ? b[1] ^ a[0] ^ f : f, fC = c[0] ^ b[0] ^ fL; + return { m, x: nx, L, C, from: D, f0: f, fL, fC }; +} +function plan(depth) { + let best = null; + for (let tries = 0; tries < 60; tries++) { + let x = [0, 1], D = canon(0, 1), last = "", peak = 0, f = 0; + const build = []; + for (let i = 0; i < depth; i++) { + // A turn of [0] would give 1/0, where a twist changes nothing and only + // a second turn, undoing the first, could follow. + let m = Math.random() < 0.65 ? "T" : "R"; + if (x[0] === 0 || last === "R") m = "T"; + const c = step(m, x, D, f); + build.push(c); + peak = max(peak, c.L.steps.length, c.C.steps.length); + (x = c.x), (D = c.C), (f = c.fC), (last = m); + } + const undo = []; + for (let k = 0; k < 60 && !(x[0] === 0 && x[1] === 1); k++) { + const c = step(x[1] === 0 || x[0] > 0 ? "R" : "T", x, D, f); + undo.push(c); + peak = max(peak, c.L.steps.length, c.C.steps.length); + (x = c.x), (D = c.C), (f = c.fC); + } + const end = build[build.length - 1].x, turns = build.filter((c) => c.m === "R").length; + const good = abs(end[0]) >= 3 && turns >= 1 + floor((depth - 6) / 4) && peak <= MAXC; + best = { build, undo, peak, end }; + if (good) break; + } + return best; +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, depth: 6, width: 3.1, btn: false, started: false, + acc: new Float32Array(W * H * 3), buf: new Float32Array(W * H * 3), + }; + return params.s; +} + +function newDance(S) { + const P = (S.plan = plan(S.depth)); + S.h = cl(88 / max(3, P.peak), 6, 16); + S.calls = [...P.build.map((c) => ({ ...c, undo: false })), { close: true }, ...P.undo.map((c) => ({ ...c, undo: true }))]; + S.k = -1; + S.pt = 0; + S.x = [0, 1]; + S.D = canon(0, 1); + S.f = 0; + S.said = ""; + S.undoSaid = ""; + S.stage = "rest0"; +} + +// Stages: a call (move, then settle), the closing, the hold, the opening, +// the rest at 0. The stage clock is in seconds at Speed 2. +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 6, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + S.speed = cl(k[1] == null ? 2 : k[1], 0.2, 3); + const depth = round(cl(k[2] == null ? 6 : k[2], 6, 14)), btn = !!(input.btnPressed && input.btnPressed[0]); + S.width = 3 * 2 ** cl(k[3] == null ? 0.06 : k[3], -1, 1); + if (!S.started || depth !== S.depth || (btn && !S.btn)) { + S.depth = depth; + S.started = true; + newDance(S); + } + S.btn = btn; + S.pt += (d * S.speed) / 2; + for (let guard = 0; guard < 4; guard++) { + const c = S.calls[S.k]; + if (S.stage === "rest0") { + if (S.pt < (S.k < 0 ? 0.6 : T_REST)) break; + if (S.k >= 0) return void newDance(S); + next(S); + } else if (S.stage === "move") { + if (S.pt < T_MOVE) break; + S.pt -= T_MOVE; + S.x = c.x; + S.D = c.L; + S.f = c.fL; + S.stage = same(c.L, c.C) ? "pause" : "settle"; + } else if (S.stage === "settle") { + if (S.pt < T_SETTLE) break; + S.pt -= T_SETTLE; + S.D = c.C; + S.f = c.fC; + S.stage = "pause"; + } else if (S.stage === "pause") { + if (S.pt < T_PAUSE) break; + S.pt -= T_PAUSE; + next(S); + } else if (S.stage === "close") { + if (S.pt < T_CLOSE) break; + (S.pt -= T_CLOSE), (S.stage = "hold"); + } else if (S.stage === "hold") { + if (S.pt < T_HOLD) break; + (S.pt -= T_HOLD), (S.stage = "open"); + } else if (S.stage === "open") { + if (S.pt < T_CLOSE) break; + S.pt -= T_CLOSE; + next(S); + } else break; + } +} + +function next(S) { + S.k++; + const c = S.calls[S.k]; + if (!c) return void ((S.stage = "rest0"), (S.pt = 0)); + if (c.close) return void (S.stage = "close"); + S.stage = "move"; + if (c.undo) S.undoSaid += c.m; + else S.said += c.m; +} + +// ---- Drawing. Ropes are drawn row by row: in a crossing the two strands +// follow x = mid -+ d cos(theta t) down its height, so a whole crossing is +// a half turn of the pair, and a twist that is still under way is the same +// picture with theta short of pi, its foot still turning. + +function putRope(acc, x, y, xc, hw, col, lv, cut) { + const off = x + 0.5 - xc, cov = cl(hw + 0.5 - abs(off), 0, 1) * lv * cut; + if (cov <= 0) return; + // Round: darker towards the edges; laid: a slanting strand every 3 rows. + const u = off / max(hw, 0.6), lay = 0.82 + 0.18 * cos(2 * PI * ((y + 0.5) / 3.2 + 0.35 * u)); + const v = (1 - 0.45 * u * u) * lay, k = (y * W + x) * 3; + for (let q = 0; q < 3; q++) acc[k + q] = acc[k + q] * (1 - cov) + col[q] * v * cov; +} +function strandRow(acc, y, xc, slope, w, col, lv, mask) { + const hw = (w / 2) * sqrt(1 + slope * slope); + for (let x = max(0, floor(xc - hw - 1)); x <= min(W - 1, floor(xc + hw + 1)); x++) { + let cut = 1; + if (mask) cut = cl(abs(x + 0.5 - mask[0]) - mask[1] - mask[2] + 0.5, 0, 1); + putRope(acc, x, y, xc, hw, col, lv, cut); + } +} +// A capsule chain for the caps and cups: a half ellipse about (cx, cy) +// bulging up (dir -1) or down (dir +1), drawn from angle a0 to a1. +function arc(acc, cx, cy, rx, ry, dir, a0, a1, w, colA, colB) { + if (a1 <= a0) return; + const n = 20, px = [], py = []; + for (let i = 0; i <= n; i++) { + const th = a0 + ((a1 - a0) * i) / n; + px.push(cx - rx * cos(th)); + py.push(cy + dir * ry * sin(th)); + } + const hw = w / 2, x0 = max(0, floor(min(...px) - hw - 1)), x1 = min(W - 1, floor(max(...px) + hw + 1)); + const y0 = max(0, floor(min(...py) - hw - 1)), y1 = min(H - 1, floor(max(...py) + hw + 1)); + const col = [0, 0, 0]; + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + let dm = 1e9, tm = 0; + for (let i = 0; i < n; i++) { + const ax = px[i], ay = py[i], bx = px[i + 1] - ax, by = py[i + 1] - ay, l2 = bx * bx + by * by || 1; + const t = cl(((x + 0.5 - ax) * bx + (y + 0.5 - ay) * by) / l2, 0, 1), dd = (x + 0.5 - ax - t * bx) ** 2 + (y + 0.5 - ay - t * by) ** 2; + if (dd < dm) (dm = dd), (tm = (i + t) / n); + } + const dist = sqrt(dm), cov = cl(hw + 0.5 - dist, 0, 1); + if (cov <= 0) continue; + const g = (a0 + (a1 - a0) * tm) / PI; + for (let q = 0; q < 3; q++) col[q] = colA[q] + (colB[q] - colA[q]) * g; + const u = dist / max(hw, 0.6), v = (1 - 0.45 * u * u) * (0.82 + 0.18 * cos(2 * PI * (tm * 9 + 0.35 * u))), k = (y * W + x) * 3; + for (let q = 0; q < 3; q++) acc[k + q] = acc[k + q] * (1 - cov) + col[q] * v * cov; + } +} + +// One picture into acc: D the diagram, dx its sideways shift, +// tw the share of a half turn the last crossing has made (1 when done), +// cup how far the foot has closed, cols the two ropes' colours by lane. +function picture(S, acc, D, dx, tw, cup, cols) { + const w = S.width, h = S.h, n = D.steps.length, gap = 0.9 + 0.25 * w; + const lane = X.map((v) => v + dx); + const caps = D.caps; + // Caps. + if (caps === 0) { + arc(acc, (lane[0] + lane[1]) / 2, YS, 7, 9, -1, 0, PI, w, cols[0], cols[0]); + arc(acc, (lane[2] + lane[3]) / 2, YS, 7, 9, -1, 0, PI, w, cols[1], cols[1]); + } else { + arc(acc, (lane[0] + lane[3]) / 2, YS, 21, 11, -1, 0, PI, w, cols[1], cols[1]); + arc(acc, (lane[1] + lane[2]) / 2, YS, 7, 5.5, -1, 0, PI, w, cols[0], cols[0]); + } + // Rope per lane, going down. + const rope = caps ? [1, 0, 0, 1] : [0, 0, 1, 1]; + let y0 = YS; + const xs = lane.slice(); + for (let i = 0; i < n; i++) { + const s = D.steps[i], last = i === n - 1, th = last ? PI * tw : PI, hh = last ? h * max(tw, 0.001) : h; + const p = s.p, mid = (xs[p] + xs[p + 1]) / 2, half = (xs[p + 1] - xs[p]) / 2; + const ya = floor(y0), yb = floor(y0 + hh); + for (let y = max(0, ya); y < min(H, yb); y++) { + const t = cl((y + 0.5 - y0) / hh, 0, 1), c = cos(th * t), sn = sin(th * t), sl = (half * th * sn) / hh; + for (let L = 0; L < 4; L++) if (L !== p && L !== p + 1) strandRow(acc, y, xs[L], 0, w, cols[rope[L]], 1, null); + const xl = mid - half * c, xr = mid + half * c, hwo = (w / 2) * sqrt(1 + sl * sl); + // The strand from the left lane is in front when o is +1. + const lf = s.o > 0; + const [xu, su, cu, xo, so, co] = lf ? [xr, -sl, cols[rope[p + 1]], xl, sl, cols[rope[p]]] : [xl, sl, cols[rope[p]], xr, -sl, cols[rope[p + 1]]]; + const near = abs(xl - xr) < 2 * hwo + 2 * gap + 2; + strandRow(acc, y, xu, su, w, cu, near ? 0.85 : 1, near ? [xo, hwo, gap] : null); + strandRow(acc, y, xo, so, w, co, 1, null); + } + // After the crossing the lanes carry each other's ropes, and an + // unfinished twist leaves its two ends partway round. + const ce = cos(th); + xs[p] = mid - half * ce; + xs[p + 1] = mid + half * ce; + if (th > PI / 2) [rope[p], rope[p + 1]] = [rope[p + 1], rope[p]]; + if (th > PI / 2) [xs[p], xs[p + 1]] = [xs[p + 1], xs[p]]; + y0 += hh; + } + // Below the crossings straight down to the foot, then the cups. + const yf = cup > 0 ? YE : H; + for (let y = max(0, floor(y0)); y < min(H, yf); y++) for (let L = 0; L < 4; L++) strandRow(acc, y, xs[L], 0, w, cols[rope[L]], 1, null); + if (cup > 0) { + const e = smooth(cup), ytail = YE + (H - YE) * (1 - e); + for (let y = YE; y < min(H, ytail); y++) for (let L = 0; L < 4; L++) strandRow(acc, y, xs[L], 0, w, cols[rope[L]], 1, null); + for (const [l0, l1] of [[0, 1], [2, 3]]) { + const cx = (xs[l0] + xs[l1]) / 2, rx = (xs[l1] - xs[l0]) / 2, ca = cols[rope[l0]], cb = cols[rope[l1]]; + arc(acc, cx, YE, rx, 8, 1, 0, (PI / 2) * e, w, ca, cb); + arc(acc, cx, YE, rx, 8, 1, PI - (PI / 2) * e, PI, w, ca, cb); + } + } +} + +// ---- Text. +function glyph(acc, ch, x, y, col) { + let bits = null; + if (ch >= "0" && ch <= "9") { + const f = FIG[ch.charCodeAt(0) - 48]; + bits = []; + for (let i = 14; i >= 0; i--) bits.push((f >> i) & 1); + } else if (G35[ch]) bits = [...G35[ch]].map(Number); + if (ch === "~") { + for (let i = 0; i < 15; i++) if (LEMNI[i] === "#") dotPx(acc, x + (i % 5), y + 1 + floor(i / 5), col); + return 5; + } + if (bits) for (let i = 0; i < 15; i++) if (bits[i]) dotPx(acc, x + (i % 3), y + floor(i / 3), col); + return 3; +} +function dotPx(acc, x, y, col) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const k = (y * W + x) * 3; + for (let q = 0; q < 3; q++) acc[k + q] = col[q]; +} +// "_" before a figure lowers it three rows, "^" raises it two; a lowered +// figure right after a raised one stands under it. +function text(acc, str, cx, y, col) { + let wd = 0; + for (let i = 0; i < str.length; i++) { + const ch = str[i]; + if (ch === "_" || ch === "^") continue; + if (str[i - 1] === "_" && str[i - 3] === "^") continue; + wd += (ch === "~" ? 5 : 3) + 1; + } + let x = round(cx - (wd - 1) / 2); + for (let i = 0; i < str.length; i++) { + const ch = str[i]; + if (ch === "_" || ch === "^") continue; + const dy = str[i - 1] === "_" ? 3 : str[i - 1] === "^" ? -2 : 0; + if (str[i - 1] === "_" && str[i - 3] === "^") x -= 4; + x += (ch === " " ? 3 : glyph(acc, ch, x, y + dy, col)) + 1; + } +} +const fracText = ([p, q]) => (q === 0 ? "~" : (p < 0 ? "-" : "") + abs(p) + "/" + q); + +export function draw(display, params) { + const S = params.s; + if (!S || !S.plan) return; + const acc = S.acc, buf = S.buf; + acc.fill(0); + const A = hsv(S.hue + 0.5, 0.85, 1), B = hsv(S.hue + 0.11, 0.85, 1); + const c = S.calls[S.k], AB = [A, B], BA = [B, A]; + let cols = S.f ? BA : AB; + const stage = S.stage; + if (stage === "close" || stage === "hold" || stage === "open") { + // A knot is one loop: one rope takes the other's colour as the ends join. + const D = S.D, r = ropesAtFoot(D), knot = r[0] !== r[1]; + const e = stage === "close" ? smooth(S.pt / T_CLOSE) : stage === "hold" ? 1 : 1 - smooth(S.pt / T_CLOSE); + if (knot) cols = [cols[0], cols[1].map((v, i) => v + (cols[0][i] - v) * e)]; + picture(S, acc, D, 0, 1, e, cols); + } else if (stage === "move" && c && c.m === "T") { + // A twist: the literal picture after it, its last crossing turning in, + // or the picture before it with its last crossing turning out. + const u = smooth(S.pt / T_MOVE); + cols = c.fL ? BA : AB; + if (c.L.steps.length > c.from.steps.length) picture(S, acc, c.L, 0, u, 0, cols); + else picture(S, acc, c.from, 0, 1 - u, 0, cols); + } else if (stage === "move" && c && c.m === "R") { + // A rotation: every end steps one lane round; the old picture slides + // off to the right as the new one comes in from the left. + const u = smooth(S.pt / T_MOVE); + picture(S, acc, c.from, 14 * u, 1, 0, c.f0 ? BA : AB); + buf.fill(0); + picture(S, buf, c.L, -14 * (1 - u), 1, 0, c.fL ? BA : AB); + for (let i = 0; i < acc.length; i++) acc[i] += (buf[i] - acc[i]) * u; + } else if (stage === "settle") { + const u = smooth(S.pt / T_SETTLE); + picture(S, acc, c.L, 0, 1, 0, c.fL ? BA : AB); + buf.fill(0); + picture(S, buf, c.C, 0, 1, 0, c.fC ? BA : AB); + for (let i = 0; i < acc.length; i++) acc[i] += (buf[i] - acc[i]) * u; + } else picture(S, acc, S.D, 0, 1, 0, cols); + // The fraction on the first line; the calls, or the name, on the second. + const white = [0.92, 0.92, 0.88]; + const fx = stage === "move" ? (S.pt / T_MOVE > 0.5 ? c.x : S.x) : S.x; + text(acc, fracText(fx), 32, 1, white); + if (stage === "close" || stage === "hold" || stage === "open") { + const e = stage === "close" ? S.pt / T_CLOSE : stage === "open" ? 1 - S.pt / T_CLOSE : 1; + if (e > 0.5) text(acc, nameOf(S.x), 32, 8, [1, 0.85, 0.55]); + } else { + const said = S.undoSaid ? S.undoSaid.slice(-15) : S.said; + text(acc, said, 32, 8, S.undoSaid ? A.map((v) => 0.35 + 0.6 * v) : [0.75, 0.75, 0.75]); + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const k = (y * W + x) * 3; + display.setPixel(x, y, round(cl(acc[k], 0, 1) * 255), round(cl(acc[k + 1], 0, 1) * 255), round(cl(acc[k + 2], 0, 1) * 255)); + } +} From 890b1a09f56af5a0137ba956a383d95a57b9bb85 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:17:41 +0000 Subject: [PATCH 13/15] =?UTF-8?q?incoming:=20Promotion=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=EC=9D=98=20Hydra=20Game=20=ED=8C=90=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=EB=A5=BC=20=EA=B3=A0=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4Jkt9PaeQhb1WkHMuJUh --- incoming/pattern-study/patterns/promotion.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/incoming/pattern-study/patterns/promotion.js b/incoming/pattern-study/patterns/promotion.js index 25770c8db..9709bd877 100644 --- a/incoming/pattern-study/patterns/promotion.js +++ b/incoming/pattern-study/patterns/promotion.js @@ -54,7 +54,7 @@ function hsv(h, s, v, o, k) { o[k + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; } -// 3 by 5 digits, copied from hydra-game.js (round 70), rows top to bottom. +// 3 by 5 digits, copied from hydra-game.js (round 72), rows top to bottom. const DIG = ["111101101101111", "010110010010111", "111001111100111", "111001011001111", "101101111001001", "111100111001111", "111100111101111", "111001010010010", "111101111101111", "111101111001111"]; From 137712752ee51bcad95e0545bd68a4b9e9f7fd36 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:23:25 +0000 Subject: [PATCH 14/15] =?UTF-8?q?incoming:=20README=20=EC=97=90=2074?= =?UTF-8?q?=ED=8C=90=20=EC=97=B4=20=EA=B0=9C=EC=9D=98=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=20=EB=85=B8=ED=8A=B8=EB=A5=BC=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4Jkt9PaeQhb1WkHMuJUh --- incoming/pattern-study/README.md | 101 ++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 3 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index eec0e99fe..70d94fe42 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 638개를 쉰여섯 갈래로 나눠 만들었어요. 1판에서 열세 갈래로 23개를 만들고, 2판에서 1판에 없던 아홉 갈래로 16개를, 3판에서 하나를, 4판에서 새 갈래인 직물로 하나를 더했어요. 5판은 새 패턴 없이 1판의 Euclid Pulse 를 다시 짰고, 6판은 세운 패널의 128줄을 위아래로 다 쓰는 패턴 하나를 더했어요. 7판은 또렷한 모양이 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 8판은 새 갈래인 기계로 하나를 더했어요. 9판은 새 패턴 없이 1판의 Sprout 을 다시 짰어요. 10판은 세운 패널을 떨어지는 블록의 우물로 쓰는 패턴 하나를 더했어요. 11판은 여러 색이 꽤 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 12판은 새 갈래인 천체로 하나를 더했어요. 13판은 새 패턴 없이 1판의 Glyph Rain 을 다시 짰어요. 14판은 절차가 바뀌기 전에 띄운 바퀴라서, 세운 패널의 높이를 다 떨어지는 폭포 하나를 더했어요. 15판은 바뀐 절차의 첫 판이라 열 갈래에 새 패턴을 하나씩, 모두 열 개를 더했어요. 도시와 음악 갈래는 그때 새로 열었어요. 16판도 열 갈래에 하나씩 열 개를 더했고, 바다와 곤충 갈래를 새로 열었어요. 17판도 열 갈래에 하나씩 열 개를 더했고, 전자 회로와 장난감 갈래를 새로 열었어요. 18판도 열 갈래에 하나씩 열 개를 더했고, 유리와 광물·결정, 놀이공원 갈래를 새로 열었어요. 19판도 열 갈래에 하나씩 열 개를 더했고, 부엌과 버섯·균사 갈래를 새로 열었어요. 20판도 열 갈래에 하나씩 열 개를 더했고, 새와 화학·실험 갈래를 새로 열었어요. 21판도 열 갈래에 하나씩 열 개를 더했고, 종이와 운동·경기 갈래를 새로 열었어요. 22판도 열 갈래에 하나씩 열 개를 더했고, 몸과 비행 갈래를 새로 열었어요. 23판도 열 갈래에 하나씩 열 개를 더했고, 무대·공연과 짐승 갈래를 새로 열었어요. 24판도 열 갈래에 하나씩 열 개를 더했고, 공사·건설과 도자기 갈래를 새로 열었어요. 25판도 열 갈래에 하나씩 열 개를 더했고, 농사·밭과 화산 갈래를 새로 열었어요. 26판도 열 갈래에 하나씩 열 개를 더했고, 얼음·극지와 미생물 갈래를 새로 열었어요. 27판도 열 갈래에 하나씩 열 개를 더했고, 철도와 필름·영화 갈래를 새로 열었어요. 28판은 주제 표가 수학 쪽으로 바뀐 첫 판이라 이름 있는 수학 대상 열 개를 열 갈래에 하나씩 더했고, 수론 갈래를 새로 열었어요. 29판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 첫 판이라, 이름 있는 수학 대상 여섯과 화장품 매장 벽에 틀 질감 넷을 더했고, 위상·매듭과 화장품 팝업 갈래를 새로 열었어요. 30판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 매장의 LED 벽에 틀 질감 넷을 화장품 팝업 갈래에 더했어요. 31판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 열지 않았어요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽의 질감 넷을 화장품 팝업 갈래에 더했어요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 41판은 주제 표가 수학·추상 다섯과 할로윈 다섯으로 바뀐 첫 판이라, 수학 대상 다섯을 다섯 갈래에 하나씩 더하고 할로윈 갈래를 새로 열어 10월 31일에 틀 패턴 다섯을 넣었어요. 42판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 41판이 연 할로윈 갈래에 더했어요. 43판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 44판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 45판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 열 개 아래인 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 46판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 47판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 48판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 49판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 50판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 51판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 52판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 53판은 할로윈 몫이 쉬게 된 첫 판이라, 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 54판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 55판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 56판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 57판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 58판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 59판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 60판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 61판도 수학 대상 열 개를 패턴이 적은 갈래부터 열 갈래에 하나씩 더했어요. 62판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 63판은 64판과 같이 주제 표에 「타이포」가 들어온 첫 판이라, 글자가 들어간 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 64판은 주제 표가 바뀌어 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 네 갈래에 더했어요. 65판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 66판도 같은 표로 돌아서 글자 패턴 넷을 글자 갈래에, 하나를 데이터 그림 갈래에 더하고, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 67판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 68판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 69판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 70판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 71판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 72판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 73판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 648개를 쉰여섯 갈래로 나눠 만들었어요. 1판에서 열세 갈래로 23개를 만들고, 2판에서 1판에 없던 아홉 갈래로 16개를, 3판에서 하나를, 4판에서 새 갈래인 직물로 하나를 더했어요. 5판은 새 패턴 없이 1판의 Euclid Pulse 를 다시 짰고, 6판은 세운 패널의 128줄을 위아래로 다 쓰는 패턴 하나를 더했어요. 7판은 또렷한 모양이 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 8판은 새 갈래인 기계로 하나를 더했어요. 9판은 새 패턴 없이 1판의 Sprout 을 다시 짰어요. 10판은 세운 패널을 떨어지는 블록의 우물로 쓰는 패턴 하나를 더했어요. 11판은 여러 색이 꽤 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 12판은 새 갈래인 천체로 하나를 더했어요. 13판은 새 패턴 없이 1판의 Glyph Rain 을 다시 짰어요. 14판은 절차가 바뀌기 전에 띄운 바퀴라서, 세운 패널의 높이를 다 떨어지는 폭포 하나를 더했어요. 15판은 바뀐 절차의 첫 판이라 열 갈래에 새 패턴을 하나씩, 모두 열 개를 더했어요. 도시와 음악 갈래는 그때 새로 열었어요. 16판도 열 갈래에 하나씩 열 개를 더했고, 바다와 곤충 갈래를 새로 열었어요. 17판도 열 갈래에 하나씩 열 개를 더했고, 전자 회로와 장난감 갈래를 새로 열었어요. 18판도 열 갈래에 하나씩 열 개를 더했고, 유리와 광물·결정, 놀이공원 갈래를 새로 열었어요. 19판도 열 갈래에 하나씩 열 개를 더했고, 부엌과 버섯·균사 갈래를 새로 열었어요. 20판도 열 갈래에 하나씩 열 개를 더했고, 새와 화학·실험 갈래를 새로 열었어요. 21판도 열 갈래에 하나씩 열 개를 더했고, 종이와 운동·경기 갈래를 새로 열었어요. 22판도 열 갈래에 하나씩 열 개를 더했고, 몸과 비행 갈래를 새로 열었어요. 23판도 열 갈래에 하나씩 열 개를 더했고, 무대·공연과 짐승 갈래를 새로 열었어요. 24판도 열 갈래에 하나씩 열 개를 더했고, 공사·건설과 도자기 갈래를 새로 열었어요. 25판도 열 갈래에 하나씩 열 개를 더했고, 농사·밭과 화산 갈래를 새로 열었어요. 26판도 열 갈래에 하나씩 열 개를 더했고, 얼음·극지와 미생물 갈래를 새로 열었어요. 27판도 열 갈래에 하나씩 열 개를 더했고, 철도와 필름·영화 갈래를 새로 열었어요. 28판은 주제 표가 수학 쪽으로 바뀐 첫 판이라 이름 있는 수학 대상 열 개를 열 갈래에 하나씩 더했고, 수론 갈래를 새로 열었어요. 29판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 첫 판이라, 이름 있는 수학 대상 여섯과 화장품 매장 벽에 틀 질감 넷을 더했고, 위상·매듭과 화장품 팝업 갈래를 새로 열었어요. 30판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 매장의 LED 벽에 틀 질감 넷을 화장품 팝업 갈래에 더했어요. 31판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 열지 않았어요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽의 질감 넷을 화장품 팝업 갈래에 더했어요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 41판은 주제 표가 수학·추상 다섯과 할로윈 다섯으로 바뀐 첫 판이라, 수학 대상 다섯을 다섯 갈래에 하나씩 더하고 할로윈 갈래를 새로 열어 10월 31일에 틀 패턴 다섯을 넣었어요. 42판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 41판이 연 할로윈 갈래에 더했어요. 43판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 44판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 45판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 열 개 아래인 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 46판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 47판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 48판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 49판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 50판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 51판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 52판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 53판은 할로윈 몫이 쉬게 된 첫 판이라, 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 54판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 55판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 56판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 57판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 58판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 59판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 60판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 61판도 수학 대상 열 개를 패턴이 적은 갈래부터 열 갈래에 하나씩 더했어요. 62판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 63판은 64판과 같이 주제 표에 「타이포」가 들어온 첫 판이라, 글자가 들어간 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 64판은 주제 표가 바뀌어 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 네 갈래에 더했어요. 65판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 66판도 같은 표로 돌아서 글자 패턴 넷을 글자 갈래에, 하나를 데이터 그림 갈래에 더하고, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 67판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 68판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 69판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 70판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 71판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 72판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 73판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 74판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. [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판에서 세로로 다시 짰어요. @@ -1005,6 +1005,21 @@ | [Steiner Deltoid](patterns/steiner-deltoid.js) | 선·궤적 | 외접원을 도는 점의 심슨 선이 패널 높이를 가로지르고, 그 자취가 쌓여 삼각형 세 변에 붙은 세 뿔 델토이드가 떠올라요. | | [Voss Dice](patterns/voss-dice.js) | 노이즈·흐름 | 이진 카운터의 비트가 바뀐 주사위만 다시 굴려 1/f 잡음을 만들고, 그 합을 백색 잡음과 갈색 잡음 사이에 나란히 흘려 내려요. | +### 74판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Flag Hoist](patterns/flag-hoist.js) | 글자 | 국제 신호서의 글자 깃발로 쓴 낱말이 왼쪽 가장자리의 깃줄에 한 장씩 아래로 매달려 밤하늘로 끌려 올라가고, 바람에 펄럭이는 동안 깃발 옆에 그 글자가 섰다가 내려가면 다음 낱말이 올라가요. | +| [Autostereogram](patterns/autostereogram.js) | 글자 | 무작위 색 점에 세 글자 낱말을 깊이로만 숨긴 단일 화면 입체 그림이고, 띠가 내려가며 LED 마다 한 주기 왼쪽 LED 와 견주면 낱말이 흰 점으로 드러나요. | +| [Tally Count](patterns/tally-count.js) | 글자 | 기호 1~4 아래 칠판 칸 넷에 표를 하나씩 읽을 때마다 분필 한 획이 正 의 획순대로 그어져, 다섯 표마다 正 하나가 닫히며 칸이 막대처럼 아래로 겨루고, 끝나면 1등 칸이 빛나고 맨 아래 선이 1등이 처음부터 끝까지 앞섰는지 말해요. | +| [Trajan Helix](patterns/trajan-helix.js) | 글자 | 대리석 기둥이 천천히 돌고 발치에서 꼭대기까지 감긴 나선 띠에 트라야누스 기둥 받침의 비문이 빨간 로마 대문자로 새겨져 있어서, 앞에서 제 너비로 읽히던 글자가 가장자리에서 좁아지다 뒤로 넘어가며 글줄이 띠를 따라 끝없이 흘러요. | +| [Eye Chart](patterns/eye-chart.js) | 글자 | 뒤에서 비춘 시력표의 슬론 글자 여덟 줄이 렌즈가 바뀔 때마다 흐려졌다 맑아지고, 지시봉이 글자를 하나씩 읽다가 더는 못 읽는 줄에서 멈춰요. | +| [Rational Tangles](patterns/rational-tangles.js) | 위상·매듭 | 두 가닥 밧줄이 위는 덮개로 닫히고 아래는 풀린 4가닥 plat 으로 세로로 걸려, 콘웨이의 엉킴 춤에서 T 마다 가운데 두 끝이 실제로 한 번 꼬이고 R 마다 네 끝이 한 칸씩 돌며 분수 p/q 가 바뀌고, 끝을 닫아 2다리 매듭의 이름을 단 뒤 다시 풀어 0 으로 돌아가요. | +| [Lambda Diagrams](patterns/lambda-diagrams.js) | 선·궤적 | 처치 수 셈이 트롬프의 람다 그림으로 서서 정규 차례 베타 줄이기를 한 걸음씩 밟고, 끝에서 셈한 수만큼 이가 난 빗이 돼요. | +| [Promotion](patterns/promotion.js) | 알고리즘 | 8×16 칸 표준 영 타블로에서 1 이 빠진 흰 구멍이 오른쪽과 아래 이웃 가운데 작은 수를 끌어당기며 오른쪽 아래 모서리로 미끄러지고 그 자리에 128 이 들어가기를 되풀이해서, 128번째에 모든 칸이 처음 수로 돌아와 번쩍여요. | +| [Reed-Solomon](patterns/reed-solomon.js) | 데이터 그림 | 낱말이 진짜 버전 1 QR 부호로 짜인 뒤 모듈이 긁히거나 얼룩지면, 디코더가 신드롬으로 틀린 코드워드를 찾아 고칠 수 있는 만큼이면 되돌리고 넘치면 빨갛게 남겨요. | +| [Brick Factory](patterns/brick-factory.js) | 기하·대칭 | 완전 이분 그래프 K(m,n) 의 꼭짓점이 패널에 흩어져 떠도는 동안 변의 교차점마다 불꽃이 켜졌다 꺼지고, 그림이 자란키에비치의 가로세로 축 그림으로 풀리면 아래 세는 수가 Z(m,n) 까지 내려가 금빛으로 서요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -4135,6 +4150,70 @@ Lab 에서 해 보기: Text 레이어에 Silkscreen 으로 「20」을 size 8, s 주제 5 「수학 · 추상」으로, 주사위 몇 개로 1/f 잡음을 만드는 방법을 주사위째 보여 주려 했어요. 마틴 가드너(Martin Gardner)는 1978년 4월 Scientific American 의 Mathematical Games 칼럼 「White and brown music, fractal curves and one-over-f fluctuations」에서 리처드 F. 보스(Richard F. Voss)의 방법을 소개했어요. 주사위 하나하나를 이진 카운터의 비트 하나로 두고, 한 걸음마다 비트가 바뀐 주사위만 다시 굴리면 주사위 k 는 2^k 걸음마다 굴러요. 그러면 합이 매번 다 굴리는 백색 잡음과 주사위로 걷는 갈색 잡음 사이의 1/f 잡음이 돼요. 패턴은 위쪽에 주사위 n 개를 7×7 칸 얼굴로 그려요. 넷이면 14 LED 이고 다섯부터 7 LED 예요. 가장 빠른 주사위 0 을 오른쪽에 둬서 아래 비트 등이 걸음 수를 이진수로 읽히게 했고, 다시 굴린 주사위는 분홍으로 번쩍였다가 0.25초에 식어요. 그 아래 20 LED 너비 세 칸에 왼쪽부터 백색, 분홍, 갈색 합이 한 걸음에 2줄씩 위에서 아래로 흘러요. 백색은 주사위 n 개를 매 걸음 다 굴린 합이고, 갈색은 주사위 하나의 눈에서 3.5 를 뺀 만큼 걸어 창 끝에서 되접혀요. 세 칸 모두 평균 3.5n 위아래로 표준편차의 2.6 배를 둔 같은 창에 그려서, 주사위 넷이면 4~24 가운데 5.1~22.9 를 써요. Speed 2 에서 1초에 6 걸음이고, 첫 화면부터 패널이 차도록 140 걸음을 미리 굴려 둬요. 넷째 노브 Hold 는 이웃한 주사위가 얼굴을 쥐는 시간의 비 r 이에요. 주사위 k 는 floor(걸음 / r^k) 가 바뀔 때 구르고, r 이 2 이면 보스의 카운터 그대로예요. Lab 의 처음 값 0.06 이 정확히 2 가 되게 맞췄어요. r 을 1 로 내리면 가운데 칸이 왼쪽처럼 백색이 되고, 3.8 로 올리면 느린 주사위가 수백 걸음을 버텨 가운데 줄이 좀처럼 자리를 옮기지 않아요. 40만 걸음을 돌려 세어 보니 주사위 넷일 때 한 걸음에 평균 1.875 개가 구르고, 분홍 합은 한 걸음에 2.56 움직여 백색의 3.87 보다 작아요. 잰 값은 움직임 0.959, 촘촘함 0.083, 가장자리 0.759, 복잡도 0.184, 채움 0.294, 색 0.125, 모양 바뀜 0.074 이고, thirds 는 37/24/26, 노브는 0.05, 0.10, 0.14, 0.10 이에요. 처음 잰 판에서 바꾼 것은 주사위 차례와 주석뿐이라 잰 값이 그대로예요. 새로 본 것은 첫 화면의 주사위 넷이 8걸음마다, Speed 2 에서 1.33초마다 한꺼번에 번쩍인다는 거예요. 카운터가 0111 에서 1000 으로 넘어가는 걸음이라 넷이 다 구르고, 그 걸음의 분홍 합은 평균 3.87 을 뛰어 백색이 매 걸음 뛰는 폭과 같아요. 주사위를 여덟로 늘리면 백색의 한 걸음은 5.46 으로 커지는데 분홍은 2.61 로 거의 그대로라서, 가운데 칸만 차분해져요. +## 74판 + +74판은 73판(#336)과 같이 돌았어요. 앞 바퀴와 같이 도는 바퀴의 PR 74개에는 OWNER, MEMBER, COLLABORATOR 가 쓴 글 가운데 일꾼 글이 아닌 것이 하나도 없어서 따를 글이 없었어요. 73판이 PR 본문에 먼저 밝힌 열 개와 지도 칸 둘을 읽고 겹치지 않게 골랐어요. 처음에는 5×7 픽셀 글자를 EPX 로 두 배씩 키워 한 글자를 네 크기로 쌓으려 했는데, 2판의 Big Type 이 이미 글꼴을 Scale2x 로 키워 그려서 Eye Chart 로 바꿨어요. 타이포 다섯은 「타이포 실험」의 출발점을 나눠 맡았어요. Flag Hoist 와 Trajan Helix 는 세로 패널다운 글이고, Autostereogram 은 글자와 빛, Tally Count 는 숫자와 기호 놀이예요. Eye Chart 는 출발점 밖에서 글자가 언제 읽히지 않게 되는지를 골랐어요. 다섯 다 글자 꼴을 코드에 적었고, Flag Hoist 와 Tally Count 는 70판 Photon Disk 의 5×7 글자를 옮겨 썼어요. 수학 다섯은 위상·매듭, 선·궤적, 알고리즘, 데이터 그림, 기하·대칭 갈래에 하나씩 섰어요. + +### Flag Hoist + +주제 7 「타이포」의 「세로 패널다운 글」로, 깃발 한 장이 글자 하나인 낱말을 위에서 아래로 읽는 그림이에요. 주제 1 로 채움 × 색 2,4 칸을 겨눴고, 주제 4 「세로 64×128 에서 잘 읽히는 것」도 맡았어요. 국제 신호서(International Code of Signals)는 영국 상무원(British Board of Trade)이 1857년에 낸 「Commercial Code of Signals」에서 시작했고, 지금 쓰는 26장의 글자 깃발은 1931년과 1965년 개정을 거친 것이에요. 그 앞에는 프레더릭 메리엇(Frederick Marryat)이 1817년에 낸 상선 신호서가 있었어요. 배에는 글자마다 깃발이 한 장씩이라, 같은 글자가 두 번 나오는 낱말은 둘째 자리에 대체 깃발을 달아야 해요. 그래서 낱말 스물여섯 개는 모두 같은 글자가 겹치지 않아요. 패널에서는 3째 열에 깃줄가 위아래로 지나가고, 깃발은 4째 열에서 오른쪽으로 날려요. Letters 3 에서 깃발은 31×40 LED 이고 3 LED 씩 떨어져 2째 줄부터 100째 줄까지 매달려요. 6 에서는 16×23 LED 여섯 장이 112째 줄까지 내려와요. 한 낱말은 손으로 여섯 번 당겨 2.6초에 올라가고, 4.5초 걸려 있다가 2.2초에 내려가요. 다음 낱말까지 9.7초예요. 깃발이 꼭대기에 닿아 있는 동안만 Photon Disk 의 5×7 대문자를 두 배로 키운 글자가 깃발 끝에서 9 LED 떨어진 자리에 서요. 펄럭임은 깃대 쪽에서 깃발 끝으로 가며 커지는 물결이에요. 깃발을 96 걸음으로 나눠 걸음마다 접힌 기울기만큼 패널 위 길이를 줄이고, 왼쪽 빛에 대한 램버트 밝기로 칠해요. 접힌 면이 가장 가파른 데는 풍력 3 에서 29°, 8 에서 45° 예요. 무늬는 LED 마다 네 표본을 칠해서 M 과 V 의 대각 십자나 I 의 검은 원이 펄럭이는 천 위에서도 계단 없이 보여요. A 와 B 는 깃발 끝을 길이의 24% 깊이로 제비꼬리처럼 팠어요. 여는 값에서 켜진 몫이 0.493, 색이 0.577 이라 2,4 칸에 서고, thirds 는 56/62/28 이에요. 첫 판은 깃발을 34×44 로 그리고 흰 바탕의 S 가 첫 깃발인 SKY 로 열어서 켜진 몫 0.657, 색 0.519 로 3,3 칸에 섰어요. 깃발을 31×40 으로 줄이자 켜진 몫이 0.483 으로 내려왔지만 색은 0.508 이었어요. 천의 색을 LED 의 원색에 가깝게 파랑 (0, 40, 255), 노랑 (255, 196, 0), 빨강 (255, 8, 22) 로 바꾸고 노랑과 파랑이 둘 다 든 KEG 로 열자 0.577 이 됐어요. 첫 판의 강풍은 접힌 면이 75° 까지 서서 깃발이 제 길이의 40% 로 오그라들었어요. 기울기를 45° 에서 멈추자 7% 짧아지는 데 그쳤어요. 검은 천은 (36, 34, 42) 로 두어서 밤에도 L 과 Z 의 검은 칸이 깃발 꼴을 지켜요. + +Lab 에서 해 보기: Text 레이어에 FAIR WIND 를 Silkscreen 으로 size 8, 색 #f2e6c8, blend screen, 효과 Twinkle, x 32, y 116 가운데 맞춤으로 두면 Letters 3 에서 깃발 아래 비어 있는 101~127째 줄에 배웅 인사가 반짝이고, 깃발이 오르내릴 때는 그 위에 엷게 얹혀요. + +기능 감: 패턴 코드가 받는 글 칸이요. Lab 에서 낱말을 치면 그 낱말이 깃발로 오르게 하고 싶었는데, 패턴은 노브 넷과 단추만 받아서 낱말 스물여섯 개를 코드에 적었어요. + +### Autostereogram + +주제 7 「타이포」의 「글자와 빛」 쪽 실험으로, 글자에 윤곽이 하나도 없이 두 눈 사이의 어긋남으로만 서는 그림이에요. 주제 1 로 촘촘함 × 복잡도 3,3 칸도 겨눴어요. 벨라 율레스(Béla Julesz)는 1960년 『Bell System Technical Journal』에 컴퓨터로 만든 무작위 점 입체 쌍을 냈어요. 두 장 다 점뿐인데, 한 장에서 네모 하나를 옆으로 밀어 두면 두 눈으로 볼 때 그 네모가 떠올라요. 크리스토퍼 타일러(Christopher Tyler)는 1979년에 모린 클라크(Maureen Clarke)와 두 장을 한 장에 담았고, 1990년 SPIE 의 Stereoscopic Displays and Applications 학회에 그 그림을 냈어요. 되풀이 무늬를 눈을 풀어 보면 벽지가 앞뒤로 떠 보이는 것은 데이비드 브루스터(David Brewster)가 1844년에 적은 벽지 효과예요. 패널에서는 줄마다 왼쪽 11 LED 가 무작위 점이고, 그 오른쪽 LED 는 11 LED 왼쪽의 점을 옮겨 적어요. 글자 안에서만 9 LED 왼쪽을 옮겨서, 눈을 11 LED 만큼 벌려 보면 글자가 바탕보다 앞으로 떠요. 점은 52% 가 청록, 하늘, 보라, 엷은 금빛 넷 가운데 하나이고 나머지는 짙은 남색 바탕이에요. 낱말은 SEE, EYE, AHA, DOT, HID 같은 세 글자 여덟이에요. 7×7 칸에 그린 굵은 대문자를 칸 하나 6×5.43 LED 로 키워 42×38 LED 글자 셋을 2, 45, 88째 줄에 세로로 쌓았어요. 칸을 쌍선형으로 읽어 절반에서 잘라서 모서리가 둥글고 A 와 Y 의 계단이 비탈이 돼요. 그냥 봐서는 점뿐이라, 24줄 높이의 띠가 1초에 40줄씩 내려가며 LED 마다 11 LED 왼쪽 LED 와 견줘요. 바탕은 바로 그 LED 를 옮겼으니 늘 같아서 어둡고, 글자 안은 9 LED 왼쪽을 옮겼으니 두 점이 달리 뽑힌 70% 의 LED 가 하얗게 켜져요. 띠가 지나간 자리는 0.9초에 걸쳐 식어서, 낱말이 위에서부터 한 글자씩 섰다가 사라져요. 한 낱말은 7.3초이고, 새 점이 0.5초 동안 위에서 아래로 쓸려 내려오며 다음 낱말이 들어와요. 점을 옮겨 적으니 deflate 가 반복을 다 찾아서 처음에는 복잡도가 0.456 이었어요. 그래서 LED 마다 세 채널에 제 몫의 결을 얹었어요. 결은 옮겨 적지 않아서 같은 점의 사본끼리도 바이트가 달라요. ±12% 결에서는 복잡도가 2.411 로 칸 위로 넘쳤고, ±2% 에서 1.635, ±3% 에서 1.869 였어요. ±2.5% 에서 1.782 로 칸 가운데에 섰고, 촘촘함은 결 없이 0.233, 지금 0.235 예요. 이만한 결은 눈으로 가려 볼 수 없어요. 눈에는 점이 처음부터 잡음으로 보였으니, 결이 바꾼 것은 그림보다 deflate 가 반복을 찾는 길이에요. 처음 Reveal 은 띠의 높이를 18 LED 에서 128 LED 까지 바꿨는데 노브 반응이 0.04 였어요. 0.1 을 넘으면 비교가 패널 전체에 그 세기로 깔리게 바꾸자 0.26 이 됐고, 1 에서는 점이 다 지워진 채 SEE 가 검은 바탕에 흰 점으로 또렷하게 서요. 바탕 색까지 돌리던 Hue 는 0.5 에서 바탕이 어두운 올리브라 갈색으로 보여서, 바탕을 남색에 묶었어요. 지금 값은 움직임 0.163, 가장자리 0.765, 채움 0.941, 색 0.332 이고 노브 반응은 0.17, 0.16, 0.19, 0.26 이에요. + +Lab 에서 해 보기: Text 레이어에 ? 한 자를 Silkscreen 으로 size 8, 색 #fff0c8, blend add, 효과 twinkle 로 x 5, y 64 가운데 맞춤으로 두면 글자가 오지 않는 왼쪽 무작위 띠(0~10열) 가운데에서 물음표가 깜박여서, 눈을 풀어 보라는 표시가 돼요. + +기능 감: Text 레이어가 쓴 글자를 패턴 코드가 마스크로 받아 읽는 길이요. 이 패턴은 Text 레이어의 낱말을 깊이 지도로 쓰면 아무 낱말이나 숨길 수 있는데, 지금은 패턴이 레이어를 읽지 못해서 열한 글자를 7×7 칸으로 손수 그렸어요. + +### Tally Count + +주제 7 「타이포」의 「숫자와 기호 놀이」예요. 한국 개표소에서 표를 세는 正 자를 셈 표시로 썼어요. 한 표가 한 획이고, 위 가로획, 가운데 세로획, 세로획 오른쪽의 짧은 가로획, 왼쪽 아래 짧은 세로획, 긴 밑획 차례로 그어서 다섯 표에 正 하나가 닫혀요. 앞 판의 Eight Principles of Yong 과 Wongoji 는 글자를 획순대로 쓰고, 여기서는 획 하나가 표 하나라서 글자가 셈이 돼요. 패널은 16 LED 칸 넷으로 나뉘고, 칸마다 1~13째 줄에 12×13 상자와 Photon Disk 의 5×7 숫자로 기호 1~4 가 서요. 그 아래 16~112째 줄에 正 이 쌓여요. Size 7 에서는 1 LED 획의 7×7 正 둘이 한 줄을 나눠 써서 칸 하나에 12줄, 120표가 들어가고, 한 번 셀 때 표가 265장쯤이에요. 넷의 몫은 셀 때마다 Dirichlet(2, 2, 2, 2)에서 새로 뽑고, 표는 다 뽑아 둔 뒤 고르게 섞어서 하나씩 읽어요. 봉인한 투표함을 여는 셈이라, 1등과 2등 사이에는 조제프 베르트랑(Joseph Bertrand)이 1887년에 낸 투표 문제가 그대로 맞아요. 1등이 p 표, 2등이 q 표면, 세는 내내 1등이 앞설 확률이 (p−q)/(p+q) 예요. 윌리엄 앨런 휘트워스(William Allen Whitworth)가 1878년에 먼저 적었어요. 121째 줄의 점선 위아래로 1등이 2등보다 몇 표 앞섰는지가 한 줄로 그려지고, 끝나서 1등 칸이 3초 빛날 때 끝까지 앞섰으면 기호 상자에 흰 테가 둘리고, 아니면 2등이 따라잡거나 앞선 자리에 흰 눈금이 서요. Size 9 에서 4,000번 세어 보니 끝까지 앞선 몫이 0.124, (p−q)/(p+q) 의 평균이 0.125 였어요. Closeness 0 에서는 1등이 표의 44% 를 가져가도 끝까지 앞서는 일은 23% 뿐이고, 1 에서는 2% 예요. 속도 2 에서 한 번 세는 데 10초, 한 획은 0.18초이고, Lab 은 40% 를 센 칠판에서 열려요. 첫 판은 넷의 몫을 Dirichlet(1)에서 뽑아 한 후보가 70% 를 가져가고 두 칸이 거의 비었고, 한 번 세는 데 18초가 걸려 thirds 가 30/6/2, Speed 반응이 0.01 이었어요. Dirichlet(2)로 바꾸고 40% 에서 열고 세는 시간을 10초로 줄이자 thirds 가 41/22/11 이 됐어요. 막 그은 획 둘레에 분필 가루가 1초쯤 떠 있게 하고 기호 상자가 그 후보의 표가 나오는 빠르기만큼 밝아지게 해서 Speed 반응이 0.040 이 됐어요. Size 15 에서 15 LED 너비의 正 은 옆 칸 것과 1 LED 사이로 붙어 밑획이 패널을 가로지르는 한 줄로 읽혔어요. 正 의 너비를 13 LED 로 묶어 칸 사이에 3 LED 를 남겼어요. + +Lab 에서 해 보기: Text 레이어에 正 한 자를 Serif 로 size 110, 색 #0c1e18, blend screen, 효과 없이 x 32, y 66 가운데 맞춤으로 두면 칠판 뒤에 큰 正 이 엷게 깔려 무엇으로 세는지 멀리서도 보여요. Serif 에 한자가 없으면 브라우저가 대신 고른 글꼴로 그려져요. + +기능 감: Text 레이어가 글자를 획순대로 그어 가는 효과요. 지금 Text 레이어의 효과에는 획을 긋는 것이 없어서, 正 의 다섯 획을 차례로 긋는 데 획 좌표를 패턴 코드에 손으로 적었어요. + +### Trajan Helix + +주제 7 「타이포」의 「세로 패널다운 글」로, 한 글줄이 패널 높이를 나선으로 감아 올라가요. 주제 4 「세로 64×128 에서 잘 읽히는 것」도 맡았어요. 트라야누스 기둥은 서기 113년에 봉헌됐고 다마스쿠스의 아폴로도로스(Apollodorus of Damascus)가 지었다고 전해요. 기둥에는 다키아 전쟁의 부조가 띠로 감겨 있는데, 이 패턴은 그 띠에 그림 대신 받침의 비문(CIL VI 960)을 실었어요. 이 비문의 글자가 로마 대문자의 본보기가 됐고, 에드워드 카티치(Edward Catich)는 『The Origin of the Serif』(1968)에서 이 글자들이 납작한 붓으로 먼저 쓰이고 그다음에 새겨졌다고 보았어요. 획의 굵기는 그 붓을 따라 매겼어요. 붓의 날을 수평에서 20도 세우면 세로획과 오른쪽 아래로 긋는 획은 1.8 LED 로 굵고, 가로획과 왼쪽 아래로 긋는 획은 0.85 LED 로 가늘어요. N 의 세로획만 돌에서처럼 손으로 가늘게 했어요. 대문자는 비문에 나오는 열아홉 자만 직선과 타원 호로 그렸고 키는 8 LED 예요. 칠은 플리니우스가 『박물지』 33권 122절에 비문 글자에 쓴다고 적은 미니움(minium)의 빨강이에요. 기둥은 반지름 25 LED 의 원통을 멀리서 본 꼴이라 양옆에 하늘이 7 LED 씩 남고, 띠는 12줄에 위쪽 그늘 한 줄과 아래쪽 밝은 턱 한 줄이 있어요. 획까지의 거리를 돌 위에서 재고 cos β 만큼 좁혀 LED 로 바꿔서, 가장자리로 갈수록 글자가 매끈하게 가늘어져요. 글자 172자와 가운뎃점 34개가 띠 위에서 1,358 LED 를 차지하고, Turn 2 에서 기둥 앞면이 1초에 12 LED 움직여 한 바퀴에 13.1초, 비문 전체는 1분 53초에 지나가요. 처음에는 기둥이 도는 만큼 시선이 띠를 따라 꼭 한 피치씩 올라가게 해서 띠는 패널에 멈춰 서고 글자만 흘렀어요. 그 판은 모양 바뀜이 0.008 로 축 끝 0.0082 아래였어요. 시선을 반 피치만 올리자 띠가 이발소 기둥처럼 천천히 기어오르고 0.011 이 됐어요. 기둥이 돈다는 것도 그 오름이 먼저 알려 줘요. 마지막 줄의 깨진 자리는 TANT[IS OPER]IBVS 로 읽고, 거칠고 어두운 돌 위에 복원한 글자를 흰 윤곽으로만 남겼어요. 판본마다 괄호가 한 글자씩 달라서 TAN[TIS OPE]RIBVS 로 적은 곳도 있어요. thirds 는 75/76/76 이고 켜진 몫은 0.760 이에요. 노브 반응은 0.05, 0.06, 0.09, 0.35 예요. Paint 는 처음 0.044 였고 굵은 획을 1.6 에서 1.8 LED 로 올리자 0.050 이 됐어요. + +Lab 에서 해 보기: Text 레이어에 SPQR 을 Newsreader 로 size 16, 색 #c0392b, blend multiply, 효과 없이 x 32, y 64 가운데 맞춤으로 두면 평평한 빨간 글자가 기둥 한가운데 붙고, 그 밑으로 나선의 글자가 가장자리에서 좁아지며 지나가서 평면에 찍은 글자와 원통에 새긴 글자가 나란히 보여요. + +기능 감: Text 레이어를 원통에 감는 변형이요. 반지름과 돌림 각 두 값만 있어도 Newsreader 같은 Lab 글꼴로 글자가 가장자리에서 좁아지며 도는 것을 해 볼 수 있는데, 이번에는 대문자 열아홉 자를 획으로 손수 그렸어요. + +### Eye Chart + +주제 7 「타이포」에서 「타이포 실험」의 출발점 밖에 있는 「읽힘」을 골라 글자가 언제 읽히지 않게 되는지를 그렸고, 128줄을 다 써서 주제 4 「세로 64×128 에서 잘 읽히는 것」도 맡았어요. 70판 Quousque Tandem 이 크기를 줄여 글자를 회색 결로 지웠다면, 여기서는 크기는 그대로 두고 흐림이 글자를 지워요. 헤르만 스넬렌(Herman Snellen)은 1862년 『Optotypi ad visum determinandum』에서 5분각 높이에 획이 1분각인 글자를 정상 시력의 기준으로 삼았어요. 루이즈 슬론(Louise Sloan)은 1959년 C D H K N O R S V Z 열 글자를 5×5 격자에 획 굵기 1/5 로 정했고, 이언 베일리(Ian Bailey)와 잰 러비(Jan Lovie)는 1976년 줄마다 0.1 log 단위, 곧 1.26배씩 크기를 바꾸고 글자 사이를 글자 너비만큼 띄우는 차트를 냈어요. 패널에서는 1 LED 가 1분각이라 맨 아래 5 LED 줄이 획 하나에 LED 하나인 6/6 줄이고, 맨 위 25 LED 한 자가 logMAR 0.7 이에요. 여덟 줄에 글자가 1, 2, 2, 2, 3, 4, 5, 5 자이고, 5 LED 줄만 5×5 비트맵이며 나머지는 같은 격자의 상자와 고리, 막대를 4×4 부표본으로 그렸어요. 동공을 3 mm 로 두면 1 디옵터의 흐림이 반지름 5.16 LED 의 원판이에요. 줄마다 누적합을 두고 원판의 줄을 소수 끝까지 더해서 반지름이 매끈하게 바뀌어요. 여는 값의 눈은 0.5 디옵터 근시이고, 렌즈가 0, 0.25, 0.5, 0.75, 0.5 디옵터로 바뀌며 흐림이 2.6, 1.3, 0, 1.3, 0 LED 가 돼요. 렌즈는 포롭터처럼 왼쪽에서 밀려 들어오고, 한 차례가 끝나면 검은 가림판이 지나가며 새 차트가 서요. 읽는 이는 글자 칸을 같은 원판으로 흐린 것과 열 글자를 홀로 흐린 본을 견줘, 차이를 4제곱으로 더하고 잡음 0.2 를 얹어 가장 가까운 글자를 대요. 따로 재 보니 반지름 1.3 에서 5 LED 줄은 글자의 72% 를 맞혔고, 2.6 에서는 29% 로 떨어질 때 10 LED 줄은 87% 였어요. 반 넘게 맞힌 줄만 읽은 것으로 쳐서 맨눈은 10 LED 나 7.9 LED 줄에서 멈춰요. 첫 판의 읽는 이는 흐린 렌즈에서 맨 위 S 마저 틀렸어요. 견주는 칸이 반지름만큼 아래로 늘어나 다음 줄 O 와 D 의 흐리지 않은 잉크까지 담았기 때문이에요. 칸을 글자 상자와 위아래 1 LED, 옆으로 글자 반 너비로 줄이자 S 는 늘 맞고, 이웃 글자의 번짐만 칸에 스며 붐빔으로 남았어요. Speed 반응도 처음에는 0.02 였어요. 렌즈끼리는 작은 줄 둘레만 달라서였고, 차례 첫머리에 가림판을 넣자 0.23 이 됐어요. Duochrome 을 올리면 왼쪽 반이 빨강, 오른쪽 반이 초록이 되고 두 반이 0.25 디옵터씩 어긋나게 흐려져서, 렌즈가 모자라면 빨강 쪽이, 넘치면 초록 쪽이 또렷해요. 잰 값은 움직임 0.067, 채움 0.958, 색 0.112 이고 `thirds` 는 91/96/99, 노브는 0.10, 0.23, 0.10, 0.32 예요. + +Lab 에서 해 보기: Text 레이어에 E 한 자를 Newsreader 로 size 24, 색 #000000, blend multiply, 효과 없이 x 9, y 13 가운데 맞춤으로 두면 맨 윗줄 S 왼쪽에 스넬렌식 세리프 E 가 서요. 렌즈가 바뀌어도 이 E 만 또렷해서 렌즈 앞에 붙은 글자처럼 보여요. + +기능 감: 레이어를 노브로 흐리는 효과요. 원판 흐림을 손으로 짰는데, Lab 에 레이어 흐림이 있으면 Text 레이어 글자도 이 패턴의 렌즈와 같은 반지름으로 흐려서 견줄 수 있어요. + +### Rational Tangles + +주제 5 「수학 · 추상」으로 위상·매듭 갈래에 세웠어요. 존 콘웨이(John Conway)는 「An enumeration of knots and links」(1970)에서 공 안의 두 가닥에 분수를 붙였어요. 북서와 북동, 남서와 남동을 잇는 두 호가 [0] 이고, 오른쪽 두 끝을 한 번 꼬면 x 가 x + 1 이 되고, 공을 4분의 1 바퀴 돌리면 −1/x 가 돼요. 분수가 같으면 같은 엉킴이에요. 콘웨이는 이것을 네 사람이 밧줄 두 개를 쥐는 춤으로 보였고, 분수가 양수면 돌고 음수면 꼬라는 규칙만으로 어떤 엉킴이든 0 으로 풀려요. 끝을 위끼리, 아래끼리 이으면 2다리 매듭이나 고리 N(p/q) 가 되고, 호르스트 슈베르트(Horst Schubert)는 1956년에 b(p, q) 와 b(p', q') 가 같은 것은 p = p' 이고 q' ≡ q^{±1} (mod p) 일 때뿐이라고 보였고, 거울상을 같은 것으로 치면 q' ≡ ±q^{±1} 이에요. 패널에서는 네 가닥이 14 LED 간격으로 10.5, 24.5, 38.5, 52.5 열에 서고, 아래 끝 1~4 가 북서, 북동, 남동, 남서예요. 위는 [0] 이면 1-2 와 3-4 덮개, 1/0 이면 2-3 을 1-4 가 감싸는 덮개로 닫혀요. 교차는 2-3 사이(가로 꼬임)나 1-2 사이(세로 꼬임)에 서고, 한 교차가 x = 가운데 ∓ 7 cos(πt) 의 반 바퀴라서 아직 덜 돈 꼬임은 θ 가 π 에 못 미친 같은 그림이에요. T 는 아래에 2-3 교차를 실제로 꼬아 넣거나 반대 손의 교차를 풀고, R 은 그림이 오른쪽으로 한 가닥 미끄러지며 새 그림이 왼쪽에서 들어와요. 그 뒤 그림이 정칙 연분수의 교대 그림과 다르면 네 끝은 그대로 둔 채 0.45초 동안 그 그림으로 가라앉고, 다섯 번에 한 번쯤 그래요. 춤을 다 지으면 아래 끝이 8줄 컵으로 닫혀 2.6초 동안 이름이 서요. 7교차까지의 매듭 열넷과 9₁, 6교차까지의 두 성분 고리 여섯은 Rolfsen 번호로, 나머지는 연분수 항의 합인 교차 수로 「9-CROSSING KNOT」처럼 적어요. 3×5 대문자는 Typewriter, 숫자는 Josephus 에서 옮겼어요. 따로 짠 셈으로 T 와 R 을 1~14번 아무렇게나 부르기를 20,000번 해 보니, 글자 그대로 그린 그림과 교대 그림이 다 따라간 분수와 같았고, 매듭과 고리는 |p| 의 홀짝과 맞았어요. 여는 Depth 6 에서 춤은 평균 21초이고, 닫은 것은 세 번에 두 번 세잎 매듭, 나머지는 8자 매듭이에요. 첫 판은 Depth 를 5 에서 열었는데, 0 에서 돌지 않고 두 번 잇달아 돌지 않으며 R 이 하나는 있고 |p| ≥ 3 인 다섯 걸음은 TTRTT 하나뿐이라 춤이 늘 3/2 세잎 매듭이었어요. 6..14 로 옮기자 6 에서 끝 분수가 다섯, 10 에서 서른셋이 됐어요. 덮개에서 밧줄 번호를 매기니 가라앉을 때와 R 뒤에 두 밧줄의 색이 서로 바뀌었어요. 아래 끝에서 어느 밧줄인지 따라가게 고치자 5,433번의 걸음에서 색이 한 번도 바뀌지 않았어요. thirds 는 18/22/20 이라 128줄을 다 써요. + +### Lambda Diagrams + +주제 5 「수학 · 추상」으로 선 갈래에 세웠어요. 알론조 처치(Alonzo Church)는 「An Unsolvable Problem of Elementary Number Theory」(1936)에서 람다 계산을 쓰고, 수 n 을 f 를 n 번 거듭하는 항 λf.λx.f(…(f x)) 로 적었어요. 처치 수예요. 셈도 항이라서 PLUS m n f x = m f (n f x) 이고, TIMES m n f = m (n f), EXP m n = n m 이에요. 베타 한 걸음은 (λx.몸) a 를 a 를 넣은 몸으로 바꾸고, 정규 차례는 늘 가장 왼쪽 바깥의 것을 줄여서 정규형이 있으면 반드시 닿아요. 존 트롬프(John Tromp)의 람다 그림에서 추상은 가로 막대, 변수는 저를 묶은 막대에서 내려오는 세로선이에요. 적용은 함수와 인자의 가장 왼쪽 세로선을 잇는 가로 고리이고, 함수 쪽 선은 고리 아래로 계속 내려가요. 트롬프가 이 그림을 처음 낸 해는 확인하지 못해서 적지 않았어요. 패턴 안에서는 항을 드브라위언 지표(Nicolaas Govert de Bruijn, 1972)로 들고 있어서 이름을 바꿔 쓸 일이 없어요. 여섯 식은 SUCC 2, PLUS 2 3, TIMES 2 3, EXP 2 3, TIMES 4 4, EXP 2 4 이고, 3, 6, 7, 16, 11, 32 걸음 만에 3, 5, 6, 8, 16, 16 의 빗이 돼요. 항마다 다시 배치해서 열 간격은 62 LED 에 드는 가장 넓은 값을 8 에서 2 사이로 고르고, 줄 간격은 세운 패널을 따라 그 두 배, 많아야 16 까지 써요. 여는 SUCC 2 는 7열 7줄을 8×16 간격으로 그려 54×114 LED 이고, EXP 2 4 의 가장 넓은 항은 변수 27개라 2 LED 간격에 1 LED 선이에요. 막대와 그 변수는 묶는 깊이마다 색상환을 0.09 씩 돌아 청록에서 파랑과 보라를 지나 자홍으로 가고, 고리는 엷은 빛이에요. 한 걸음은 Speed 2 에서 1.25초예요. 항을 0.25초 보이고, 0.55초 동안 줄일 자리의 막대와 그 변수를 따뜻한 흰빛으로, 인자를 금빛으로 밝히며 나머지를 30% 로 낮춘 뒤, 0.45초에 다음 항으로 겹쳐 넘어가요. 정규형에서는 2.6초를 멈추고 빛 한 줄기가 1.3초씩 두 번 빗을 타고 내려가요. 걸음이 60 을 넘거나 마디가 600 을 넘거나 2×2 간격에도 패널에 들지 않으면 그 식을 건너뛰는데, 여섯 식은 다 그 안에서 끝나요. 처음 판은 막대를 바깥 열보다 간격의 반만큼 내밀었어요. 그러자 SUCC 의 둘째 막대와 2 의 둘째 막대가 한 줄에서 맞닿아 두 항을 묶는 추상 하나로 읽혔어요. 내미는 길이를 (간격 − 선 굵기 − 1)/2 로 줄여 옆 막대와 사이에 어두운 LED 가 하나 이상 남게 했고, 간격 2 와 3 에서는 막대가 바깥 열에서 끝나요. 줄 간격을 12 까지만 둔 판은 그림이 패널 가운데 위에 몰려 `thirds` 가 13/25/3 이었어요. 16 까지 늘리자 19/23/6 이 되고 채움이 0.140 에서 0.166 으로 올랐어요. 지금 값은 움직임 0.165, 촘촘함 0.032, 색 0.165, 모양 바뀜 0.160 이고 노브 반응은 0.07, 0.18, 0.21, 0.11 이에요. + +### Promotion + +주제 5 「수학 · 추상」으로 알고리즘 갈래에 세우고, 주제 4 「세로 64×128 에서 잘 읽히는 것」도 맡았어요. 45판 Plancherel Growth 가 영 도표를 칸 하나씩 키웠다면, 여기서는 다 찬 직사각형 표 안에서 수가 돌아요. 표준 영 타블로는 1 부터 N 까지를 한 번씩 담고 줄을 따라서도 열을 따라서도 커지는 표예요. 마르셀폴 쉬첸베르거(Marcel-Paul Schützenberger)의 프로모션(Discrete Mathematics, 1972)은 왼쪽 위의 1 을 빼고, 빈 칸에 오른쪽과 아래 이웃 가운데 작은 수를 옮기기를 구멍이 바깥 모서리에 닿을 때까지 되풀이해요. 이 미끄러뜨리기가 쉬첸베르거가 1977년에 jeu de taquin 이라 부른 것이에요. 그다음 모든 수에서 1 을 빼고 비운 모서리에 N 을 적어요. 마크 헤이먼(Mark Haiman)은 1992년에 a×b 직사각형에서 이것을 N = ab 번 하면 어떤 표든 처음으로 돌아온다는 것을 보였어요. 브렌던 로즈(Brendon Rhoades)는 2010년에 프로모션을 d 번 해서 제자리인 표의 수를 q-갈고리 공식이 1 의 거듭제곱근에서 센다는 순환 체(cyclic sieving)를 보였어요. 처음 표는 커티스 그린(Curtis Greene)과 앨버트 나이엔하위스(Albert Nijenhuis), 허버트 윌프(Herbert Wilf)의 갈고리 걷기(1979)로 고르게 뽑아요. 3×2 모양의 표 다섯 가지를 5만 번 뽑아 보니 저마다 9,925~10,081번 나왔어요. 패널에는 8 LED 칸 8×16 개가 꼭 맞고, 칸 색은 수를 따라 하늘빛에서 보라와 자홍을 지나 주황과 노랑으로 가요. 수가 32, 64, 96 을 넘는 칸 사이의 홈이 밝게 켜져서, 1 부터 m 까지 담은 칸이 늘 영 도표를 이루는 것이 계단 세 줄로 보여요. 구멍은 0.25초에 하얗게 빠져 칸마다 0.08초에 미끄러지고 뒤로 짧은 꼬리를 끌어요. 22번 미끄러지면 오른쪽 아래에 128 이 들어가서 한 번에 2.26초, 집에 돌아오기까지 4.8분이에요. 아래 홈 한 줄이 그 횟수를 세는 막대이고, 처음과 같은 수를 든 칸에는 흰 점이 서요. 계단은 프로모션 한 번에 많아야 한 칸 움직여요. 모든 수가 1 씩 줄어 바깥으로 밀리고, 구멍이 지난 자리는 큰 수를 받아 안으로 당겨지기 때문이에요. 따로 짠 셈으로 2×4, 4×8, 8×16, 16×32 에서 세 표씩 돌려 보니 4×8 부터는 모두 꼭 N 번째에 처음 돌아왔고, 2×4 는 셋 가운데 하나가 2번, 둘이 4번 만에 돌아왔어요. 패턴은 모든 칸이 같아지는 순간마다 번쩍여서, 그런 표는 막대가 차기 전에 번쩍여요. 넷째 노브는 처음에 꼬리 길이(1~40초)였는데 반응이 0.010 이었어요. 꼬리를 칸 너비까지 넓히자 0.049 가 됐지만, 40초 끝에서는 길 열여덟 개가 겹쳐 표 전체가 허옇게 덮였어요. 띠 수를 4 에서 16 까지 바꾸는 Levels 로 바꾸자 0.11 이 됐어요. thirds 는 81/84/79 이고 노브 반응은 0.42, 0.06, 0.30, 0.11 이에요. + +### Reed-Solomon + +주제 5 「수학 · 추상」으로 리드-솔로몬 부호를 데이터 그림 갈래에 세웠어요. `thirds` 가 23/46/29 라 주제 4 의 문턱도 넘어요. 어빙 리드(Irving S. Reed)와 구스타브 솔로몬(Gustave Solomon)은 1960년 「Polynomial Codes over Certain Finite Fields」에서 메시지를 유한체 위 다항식으로 보고 그 값을 더 적어 두는 부호를 냈어요. 덴소 웨이브의 하라 마사히로(原昌宏)가 1994년에 만든 QR 부호가 이 부호를 GF(256)(원시 다항식 0x11D)에서 써요. 버전 1 은 21×21 모듈에 코드워드 26개이고, 나머지 n 개가 (x − α⁰)…(x − αⁿ⁻¹) 로 나눈 나머지라서 읽은 값을 그 근에 넣은 신드롬은 아무것도 바뀌지 않으면 다 0 이에요. 엘윈 벌캠프(Elwyn Berlekamp, 1968)와 제임스 매시(James Massey, 1969)의 알고리즘이 신드롬에서 가장 짧은 오류 위치 다항식을 찾고, 로버트 치엔(Robert Chien, 1964)의 탐색이 자리마다 근을 시험하고, 데이비드 포니(G. David Forney, 1965)의 식이 고칠 값을 줘요. 패널에서는 모듈 하나가 3×3 LED 라 부호가 1~63째 열, 38~100째 줄을 차지해요. 맨 위에 5×7 글자로 낱말이, 그 아래 11~33째 줄에 코드워드 26개가 8비트 세로줄로 서고 데이터는 청록, 오류 정정은 보라예요. 모듈도 제가 실은 코드워드의 색이라 데이터가 오른쪽 아래부터 차는 것이 보여요. 비트는 두 모듈 너비로 오르내리며 오른쪽 아래부터 뱀처럼 들어가고, 마스크가 대각선으로 쓸고 지나가요. 그다음 얼룩이나 이 빠짐, 긁힘이 차례로 와서 모듈을 어둡게 하거나 밝게 해요. 디코더는 스캐너처럼 형식 비트 두 벌에서 가장 가까운 유효 값을 골라 마스크를 벗기고, 105째 줄의 신드롬 칸이 켜지고, 찾은 코드워드 위에 노란 점이 찍히고, 고친 모듈이 하얗게 번쩍이며 돌아와요. 고칠 수 있는 수를 넘으면 맨 아래 「7/5」 같은 셈이 빨개지고 부호는 망가진 채 남아요. 여는 값은 H 수준(데이터 9, 오류 정정 17, 8개까지 고침)이에요. 만든 부호는 python qrcode 8.2 의 것과 낱말 여덟, 수준 넷, 마스크 여덟의 256개가 모듈 하나 다르지 않았고, zxing-cpp 3.1.1 이 256개를 다 읽었어요. 처음에는 고른 마스크가 Nayuki 의 qrcodegen 과 32개 가운데 5개에서 달랐어요. 파인더를 닮은 1:1:3:1:1 벌점을 셀 때 바깥 어두운 칸이 한 칸보다 긴 것까지 셌기 때문이고, 칸 길이를 꼭 맞게 따지자 32개가 다 같아졌어요. 긁힘은 생각보다 싸게 먹혔어요. 너비 1.2 모듈, 길이 30 모듈로 가로질러도 코드워드 2~7개만 망가뜨리는데, 코드워드가 2×4 모듈 덩이라 긁힘이 덩마다 한두 모듈만 스치고 밝은 모듈은 바뀌지 않아서예요. 넓이 60 의 얼룩은 2~11개를 망가뜨려 H 가 74%, M 이 47%, L 이 19% 를 고쳐요. 첫 단어를 다 지은 부호에서 열었을 때는 Speed 반응이 0.02 였고, 마스크를 쓸기 직전에서 열자 0.09 가 됐어요. Hue 0.5 에서 데이터가 빨개져 상처와 헷갈려서 상처 색도 색상을 따라 돌게 했어요. 잰 값은 움직임 0.055, 가장자리 0.919, 복잡도 0.080, 채움 0.330, 색 0.263 이고 노브는 0.09, 0.09, 0.05, 0.13 이에요. + +### Brick Factory + +주제 5 「수학 · 추상」으로 그래프 그리기의 교차수를 기하 갈래에 세웠어요. 투란 팔(Pál Turán)은 1944년에 부다페스트 근처 벽돌 공장에서 강제 노동을 하며 가마에서 저장소로 수레를 밀었는데, 레일이 엇갈리는 자리마다 수레가 덜컹이고 벽돌이 떨어졌어요. 가마 m 곳과 저장소 n 곳을 모두 잇는 레일을 엇갈림이 가장 적게 놓는 문제가 거기서 나왔고, 투란은 이것을 「A note of welcome」(Journal of Graph Theory, 1977)에 적었어요. 카지미에시 자란키에비치(Kazimierz Zarankiewicz)는 1954년에 한쪽 꼭짓점을 가로축에, 다른 쪽을 세로축에 원점 둘레로 고르게 나눠 놓는 그림을 냈어요. 같은 사분면에 있고 끝의 차례가 엇갈린 두 변만 만나서, 교차는 Z(m,n) = ⌊m/2⌋⌊(m−1)/2⌋⌊n/2⌋⌊(n−1)/2⌋ 개예요. 그 논문은 이것이 가장 적다고 증명했다고 했지만 틈이 있었어요. 대니얼 클라이트먼(Daniel Kleitman)이 1970년에 작은 쪽이 6 이하일 때를 증명했고, 나머지는 자란키에비치 추측으로 남아 있어요. 패널에서는 원점이 (31.5, 58) 이고 여는 K(4,6) 의 가로 꼭짓점 넷이 4~59째 열에, 세로 꼭짓점 여섯이 6~110째 줄에 서요. 변은 가로쪽 끝의 색을 받아 m 가지 색이고, 꼭짓점은 3×5 칸에 한 칸씩 던져진 뒤 가로 7, 세로 9.5 LED 안에서 떠돌아요. 프레임마다 꼭짓점을 나눠 갖지 않는 변 쌍을 모두 시험해서, K(6,9) 에서는 1,080쌍이에요. 떠도는 동안 K(4,6) 은 22~84개, K(6,9) 는 180~380개가 엇갈려요. 각 쌍이 지난 프레임에 만났는지 기억해서, 새로 생긴 교차는 6 LED 까지 퍼지는 고리로, 사라진 교차는 그 자리에서 번지는 빛으로 0.55초 동안 번쩍여요. 꼭짓점은 Speed 2 에서 1.2초에 날아가 2.6초 떠돌고, 3초 동안 저마다 조금씩 늦게 출발해 제자리로 풀리고, 2.6초 머물러요. 한 바퀴가 9.4초예요. 가로쪽은 왼쪽에서 오른쪽, 세로쪽은 위에서 아래 차례를 지킨 채 자리로 가서 가는 길이 덜 엇갈려요. m 4~6, n 6~9 의 열두 경우 모두 머무는 동안 센 수가 Z(m,n) 과 같았어요. 첫 판은 m 을 3..6, n 을 4..9 로 두어 K(3,4) 에서 열렸어요. 변 12개에 머무는 교차가 둘뿐이라 켜진 몫이 0.092, Hue 반응이 0.02 였어요. m 4..6, n 6..9 로 바꿔 K(4,6) 에서 열자 켜진 몫이 0.230, Hue 반응이 0.06 이 됐어요. 세는 수는 처음에 3×5 숫자로 적었는데, Z 가 가운데 가로획이 점 하나로 줄어든 2 라서 「Z12」가 「212」로 읽혔어요. Photon Disk 의 5×7 숫자와 Z 로 바꾸고 Z 는 흐리게 두었어요. 교차가 몰린 자리는 불꽃을 더해 그리면 흰 덩어리가 돼서, 불꽃을 따로 한 겹에 두고 밝은 쪽만 남겨 점으로 읽히게 했어요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -4177,7 +4256,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 25일 `dev`(`f72355d`)의 328개에 43판부터 73판까지의 삼백열 개를 더한 638개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 25일 `dev`(`f72355d`)의 328개에 43판부터 74판까지의 삼백스무 개를 더한 648개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -4281,6 +4360,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 73판은 72판(#335)의 열 개까지 든 628개에 빈 칸도 셋 이하인 칸도 없어서, 넷뿐인 칸 스물셋 가운데 72판이 고른 움직임 × 색 1,4 와 가장자리 × 색 1,4 를 빼고 촘촘함 × 채움 4,1 과 움직임 × 모양 바뀜 1,3 을 겨눴어요. 촘촘함 × 채움 4,1 은 이웃 차이 평균이 0.297 을 넘으면서 24/255 넘게 켜진 LED 가 0.302 아래인 칸이에요. 71판이 다음 감으로 적은 대로 어두운 바탕에 외딴 밝은 점을 찍는 길이에요. 켜진 LED 마다 네 이웃이 꺼져 있으면 촘촘함은 그 LED 의 몫의 두 배에 밝기를 곱한 값이라, Damask 는 5매 위수자의 묶는 점이 가로세로로 닿지 않는 것을 그대로 썼어요. 바탕의 20% 가 외딴 날실 점이고 낱말은 천의 10.4% 라서 촘촘함 0.324, 채움 0.263 이에요. 움직임 × 모양 바뀜 1,3 은 1초에 바뀌는 양이 0.061 아래이면서 모양 바뀜이 0.211~0.328 인 칸이라, 2~8초에 바뀌는 LED 가 한 번씩만 한쪽으로 바뀌어야 해요. Dürer's Alphabet 은 펜 끝과 컴퍼스 바늘 말고는 움직이는 것을 두지 않고, 잉크가 글자 몸을 채우고 종이가 밝아지는 차례를 재는 창 안에 넣어 움직임 0.048, 모양 바뀜 0.247 이에요. 6초 동안 바뀐 양을 다 더해도 모양 바뀜의 1.17 배예요. 둘 다 처음 판은 칸 밖이었어요. Damask 는 움직임 0.017 로 움직임 축의 아래 끝 0.019 를 내려서 천 밑에 낮은 물결을 흘렸고, Dürer's Alphabet 은 모양 바뀜 0.179 로 칸 아래에 서서 여는 자리를 옮겼어요. 열 개 다 일곱 축의 끝 안에 있어서 칸 경계는 60판 그대로예요. 72판까지 든 628개에 73판 열 개를 더한 638개에서 지도 21장은 다 `none` 이고, 겨눈 두 칸은 둘 다 다섯이 됐어요. 넷뿐인 칸은 스물셋에서 스물하나로 줄었어요. +74판은 72판까지 든 628개에 같이 돈 73판(#336)의 열 개를 더해 세어도 빈 칸도, 둘이나 셋뿐인 칸도 없었어요. 73판이 고른 촘촘함 × 채움 4,1 과 움직임 × 모양 바뀜 1,3 은 그 열 개로 다섯이 됐어요. 넷뿐인 칸 스물하나 가운데 채움 × 색 2,4 와 촘촘함 × 복잡도 3,3 을 겨눴어요. 채움 × 색 2,4 는 24/255 넘게 켜진 LED 가 0.302~0.558 이면서 색이 0.5445 를 넘는 칸이에요. Flag Hoist 는 원색 천의 깃발을 밤하늘에 매달아 켜진 몫 0.493, 색 0.577 이에요. 첫 판은 깃발을 34×44 로 그리고 흰 바탕의 S 가 든 SKY 로 열어서 0.657 과 0.519 로 3,3 칸에 섰어요. 깃발을 31×40 으로 줄이자 켜진 몫은 들어왔지만 색이 0.508 이었고, 천을 LED 원색에 가깝게 두고 노랑과 파랑이 둘 다 든 KEG 로 열자 칸에 섰어요. 촘촘함 × 복잡도 3,3 은 이웃 차이 평균이 0.189~0.297 이면서 복잡도가 1.393~2.172 인 칸이에요. Autostereogram 은 점을 11 LED 왼쪽에서 옮겨 적는 그림이라 deflate 가 그 반복을 다 찾아서 첫 판의 복잡도가 0.456 이었어요. LED 마다 옮겨 적지 않는 ±2.5% 결을 세 채널에 얹자 촘촘함 0.235, 복잡도 1.782 가 됐어요. ±12% 에서는 2.411 로 칸 위로 넘쳤어요. 열 개를 다 넣은 648개에서 두 칸은 다섯이 됐고 넷뿐인 칸은 열아홉이에요. 열 개 다 일곱 축의 끝 안에 있어서 칸 경계는 그대로예요. + ### 아직 없는 갈래 `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 가 섰어요. @@ -4438,9 +4519,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 73판에서는 Abracadabra 와 Airy Beam 이 128줄을 써요. Abracadabra 는 열한 줄을 128/11 = 11.64 줄 간격으로 세워 1행부터 126행까지 쓰는데, 줄이 한 글자씩 짧아지는 계단이라 아래로 갈수록 켜진 몫이 줄어 thirds 가 28/18/11 이에요. 처음에는 빠진 글자가 다 부서진 뒤 아주 꺼져서 맨 아래 3분의 1 이 8 이었고, 32/255 의 그을음을 남기자 11 이 됐어요. Airy Beam 은 128줄을 ξ 0~8 로 잡아 빔이 내려가며 휘고, 아래로 갈수록 곁 로브가 주 로브 왼쪽을 채워서 52/64/84 예요. Dürer's Alphabet 도 64줄 정사각형 두 장을 위아래로 쌓아 67/64/62 예요. Topologist's Sine Curve 는 x 를 세로에 정비례로 세워 흔들림이 아래 극한 선분 쪽에 몰려서 11/33/75 이고, 주제 4 로는 세지 않았어요. +74판에서는 Trajan Helix 와 Promotion 이 128줄을 써요. Trajan Helix 는 반지름 25 LED 의 기둥이 패널 높이를 다 채워 thirds 가 75/76/76 이고, 대리석을 빼고 빨간 글자만 세도 8/8/10 이에요. Promotion 은 8 LED 칸 8×16 개의 표가 패널을 꼭 채워 81/84/79 예요. 나머지 여덟 가운데 Autostereogram 은 94/93/93, Eye Chart 는 91/96/99 로 점과 차트가 패널을 다 덮어요. Flag Hoist 는 56/62/28, Reed-Solomon 은 23/46/29, Rational Tangles 는 18/22/20, Brick Factory 는 19/36/13, Tally Count 는 41/22/11 이에요. Lambda Diagrams 는 19/23/6 으로 아래 3분의 1이 가장 얕아요. 줄 간격을 12 까지만 둔 첫 판은 그림이 위에 몰려 13/25/3 이었고, 16 까지 늘려서 문턱을 넘었어요. + ### 낯선 수학 -주제 5 로 쓴 수학과 다음에 해 볼 수학이에요. 28판이 처음 열 개를 썼어요. 슈람-뢰브너 진화(Loewner Trace), 포드 원(Ford Circles), 털 난 공 정리(Hairy Ball), 아즈텍 다이아몬드와 북극권(Aztec Diamond), 부니모비치 경기장(Stadium Billiard), 라우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 다면체와 E8(Gosset Polytope), 콘웨이의 보고 말하기 수열(Look and Say), 위토프 게임의 그런디 값(Wythoff Nim), 쌍곡 평면의 띠 모형(Hyperbolic Band)이에요. 29판은 여섯을 더했어요. 클라인의 j 불변량(J-Invariant), 호프 올뭉치(Hopf Fibration), 다이슨 브라운 운동(Dyson Gas), 가케야 바늘 문제의 페론 나무(Perron Tree), 임계선 위의 리만 제타 함수(Zeta Loops), 길버트-섀넌-리즈 리플과 베이어-디아코니스의 일곱 번(Seven Shuffles)이에요. 30판도 여섯을 더했어요. 세 간격 정리(Three Gaps), 호프스태터 나비와 TKNN 정수(Hofstadter Butterfly), 노이만의 외부 당구(Outer Billiards), 볼드의 탄도 퇴적과 KPZ 앞면(Ballistic Deposition), 아르놀트의 미앤더(Meander), 브로드벤트와 해머슬리의 스미기(Percolation)예요. 31판도 여섯을 더했어요. 마스킷 조각의 클라인 군 극한 집합(Indra's Pearls), 리틀우드 다항식의 근(Littlewood Roots), 고든의 가우스 해자(Gaussian Moat), 아르놀트의 고양이 사상(Cat Map), 자부스키와 크루스칼의 KdV 솔리톤(KdV Solitons), 보이 곡면(Boy's Surface)이에요. ROUND.md 의 「낯선 수학 예」 여덟 가운데 쓴 것은 슈람-뢰브너 진화와 아즈텍 다이아몬드, 모듈러 형식, 호프 올뭉치, 세 간격 정리, 외부 당구 여섯이었고, 31판이 리틀우드 다항식의 근과 클라인 군의 극한 집합을 써서 여덟을 다 썼어요. 32판도 여섯을 더했어요. 탤벗 효과와 양자 카펫(Talbot Carpet), 길브레스 추측의 표(Gilbreath Table), 원 사상의 아르놀트 혀(Arnold Tongues), 콘웨이와 라딘의 핀휠 타일링(Pinwheel Tiling), 윌킨슨과 윌렘센의 침입 스미기(Invasion Percolation), 울람의 문제와 해머슬리 과정(Hammersley Lines)이에요. 33판도 여섯을 더했어요. 에인절, 홀로이드, 로믹, 비라그의 무작위 정렬 네트워크(Sorting Network), 베르트랑의 역설(Bertrand Paradox), 펜로즈의 비출 수 없는 방(Penrose Room), 당들랭 구(Dandelin Spheres), 테이트의 보로메오 고리(Borromean Rings), 골드바흐 혜성(Goldbach Comet)이에요. 34판도 여섯을 더했어요. 톰슨 문제와 오일러가 남기는 다섯 이웃 열둘(Thomson Problem), 켈빈의 배 물결(Kelvin Wake), 요네야마가 낸 와다의 호수(Lakes of Wada), 샤이데거의 강 그물(Scheidegger Rivers), 모저의 소파 문제와 해머슬리의 소파(Moving Sofa), 크누스의 춤추는 링크로 푼 펜토미노 덮기(Pentomino Cover)예요. 35판도 여섯을 더했어요. 뢸로의 폭이 일정한 곡선(Constant Width), 렌니의 주차 문제(Rényi Parking), 카리의 비주기 왕 타일(Kari-Culik Tiles), 콜라코스키 수열(Kolakoski), 쿠라모토와 바톡토흐의 키메라 상태(Chimera State), 주콥스키 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했어요. 그레이슨의 정리가 말하는 곡선 단축 흐름(Curve Shortening), 헤우드의 일곱 색 토러스 지도(Heawood Torus), 슈바르츠 랜턴(Schwarz Lantern), 윌브러햄과 깁스의 현상(Gibbs Phenomenon), 타원 곡선의 현과 접선(Elliptic Chords), 지먼의 파국 기계(Catastrophe Machine)예요. 37판도 여섯을 더했어요. 포천의 쓸기 선 알고리즘(Fortune's Sweep), 앙투안의 목걸이(Antoine's Necklace), 카르만 소용돌이 길의 안정성(Kármán Street), 단단한 원판의 헥사틱 상(Hexatic Phase), 존슨-멜 쪽매와 아브라미 법칙(Johnson-Mehl), 쿠라모토-시바신스키 방정식(Kuramoto-Sivashinsky)이에요. 38판도 여섯을 더했어요. 앤더슨 국소화(Anderson Localization), 무어가 찾고 Chenciner 와 Montgomery 가 증명한 세 몸 문제의 8자 해(Figure-Eight Orbit), 예센과 빈트너, 에르되시의 베르누이 합성곱(Bernoulli Convolutions), 브라우어르의 고정점 정리를 증명하는 슈페르너 보조정리(Sperner's Lemma), 에겐베르거와 폴리아의 항아리(Pólya Urn), 뇌르고르의 무한 수열(Infinity Series)이에요. 39판도 여섯을 더했어요. 고든, 웹, 월퍼트의 등스펙트럼 북(Isospectral Drums), 폰 망골트의 꼴로 쓴 리만의 명시 공식(Explicit Formula), 요한 베르누이의 최단 강하선(Brachistochrone), 슈타이너의 포리즘(Steiner Chain), 모런 집단으로 그린 킹먼의 합류(Kingman Coalescent), 보르수크-울람 정리(Borsuk-Ulam)예요. 40판도 여섯을 더했어요. 앨더스의 브라운 연속 랜덤 나무(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 함수(Hydrogen Orbital), 젤도비치 근사의 팬케이크(Zeldovich Pancakes), 망델브로가 내다보고 롤러, 슈람, 베르너가 증명한 브라운 운동의 바깥 경계(Brownian Frontier), 복소 긴즈부르크-란다우 방정식의 결함 난류(Defect Turbulence), 루퍼트 왕자의 정육면체(Prince Rupert's Cube)예요. 41판은 다섯을 더했어요. 홀과 왜건의 현수선 길과 정다각형 바퀴(Square Wheels), 퐁슬레의 포리즘(Poncelet Porism), 슐레플리의 4차원 정다포체를 자른 단면(Polytope Slices), 클림켁과 티먼, 마그누손의 사이트스왑(Siteswap), Dudeney 의 잡화상 퍼즐과 월리스-보여이-게르비엔 정리(Hinged Dissection)예요. 42판은 다섯을 더했어요. 바른스도르프 규칙으로 도는 기사 여행(Knight's Tour), 페르미와 파스타, 울람, 칭구의 되돌아옴(FPUT Recurrence), 포슬리에-립킨 반전기(Peaucellier Linkage), 몰리의 삼등분선 정리(Morley Triangle), 랄과 마드라스, 소칼의 피벗 알고리즘으로 돌린 자기 회피 걸음(Pivot Walk)이에요. 43판은 다섯을 더했어요. 월리스 워커의 칼레이도사이클(Kaleidocycle), 쌍곡형에서 록소드롬형으로 감기는 뫼비우스 변환의 흐름(Möbius Loxodrome), 터트의 용수철 정리(Tutte Embedding), 디펀트와 크래비츠의 고리 정리를 품은 히토메자시 사시코(Hitomezashi), 룽게 현상과 체비쇼프 점(Runge Phenomenon)이에요. 44판은 다섯을 더했어요. 마크 카츠의 고리 모형(Kac Ring), 스톤과 튜키의 햄 샌드위치 정리(Ham Sandwich), 브란트의 불가리아 솔리테어(Bulgarian Solitaire), 엘스켄스와 프리시의 탄도 소멸(Ballistic Annihilation), 페레그린의 솔리톤(Peregrine Soliton)이에요. 45판은 다섯을 더했어요. 로건-셰프와 베르시크-케로프의 극한 곡선으로 다가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도브루신 경계 조건을 건 임계 이징 모형의 SLE(3) 경계(Ising Interface), 프리예제프와 동료들의 오일러 걷개로 쌓는 로터 라우터 응집과 레빈-페레스의 스매시 합(Rotor-Router), 폴리아의 과수원 문제(Pólya's Orchard), 넬슨의 평면 색칠 문제와 아이즈벨의 일곱 색(Hadwiger-Nelson)이에요. 46판은 다섯을 더했어요. 브릴루앙의 구역(Brillouin Zones), 모스 이론과 레브의 그래프(Reeb Graph), 로렌츠 기체와 시나이의 당구(Lorentz Gas), 리언 글래스의 점무늬(Glass Patterns), 벤치와 수테라, 불피아니의 확률 공명(Stochastic Resonance)이에요. 47판은 다섯을 더했어요. 오키프와 홍, 스트로가츠의 스워멀레이터(Swarmalators), 빌스의 외로운 달리기 선수 추측(Lonely Runner), 샤츠의 올로이드와 그 전개도(Oloid), 쾨베의 원 채우기 정리와 콜린스-스티븐슨의 반복(Koebe Packing), 콘웨이의 병사와 황금비 무게(Conway's Soldiers)예요. 48판은 다섯을 더했어요. 오하라의 매듭 뫼비우스 에너지(Möbius Energy), 체비쇼프의 편향과 루빈스타인-사르낙의 밀도(Chebyshev's Bias), 섀든과 레키엔, 마스든의 이중 소용돌이 능선(Double Gyre), 아자리와 프로스트의 깜빡이는 래칫(Flashing Ratchet), 망델브로의 프랙탈 스미기(Fractal Percolation)예요. 49판은 다섯을 더했어요. 뒤러의 전개 문제와 셰퍼드의 추측(Dürer's Unfolding), 베리와 골드버그가 재규격화한 세타 합의 꼬부랑 곡선(Curlicue), 오일러 방정식과 푸앵소의 폴호드로 본 테니스 라켓 정리(Tennis Racket), 켐프의 사슬과 네 색 정리(Kempe Chains), 퇴플리츠의 내접 정사각형 문제와 그린-로브의 직사각형(Square Peg)이에요. 50판은 다섯을 더했어요. 크레이치넌과 배철러의 2차원 난류가 거꾸로 가는 캐스케이드와 맥윌리엄스의 소용돌이(Inverse Cascade), 사포발과 로소, 구예의 확산 앞줄(Diffusion Front), 필리와 마르케티, 케이츠와 타이외르의 운동성 상분리(Motility Clusters), 박과 스네펜의 진화 모형(Bak-Sneppen), 리세의 영원한 가속(Risset Rhythm)이에요. 51판은 다섯을 더했어요. 스미스와 마이어스, 캐플런, 굿먼-스트라우스의 모자 타일과 H/T/P/F 메타타일 치환(Hat Monotile), 반도와 동료들의 최적 속도 모형과 스기야마의 원형 트랙 실험(Phantom Jam), 사토와 테이트의 법칙과 타원곡선의 a_p(Sato-Tate), 칼라비와 야우의 공간에서 핸슨이 잘라 그린 5차 페르마 곡선(Calabi-Yau Quintic), 에델스브루너와 레처, 조모로디언의 지속 호몰로지와 바코드(Persistence Barcode)예요. 52판은 다섯을 더했어요. 슈람과 셰필드가 등고선을 SLE(4) 로 보인 2차원 가우스 자유장(Gaussian Free Field), 당튀와 마즈무다르, 베링거의 광탄성 힘 사슬(Force Chains), 앤스컴의 사중주와 마테이카, 피츠모리스의 같은 통계 담금질(Anscombe's Quartet), 드 브라윈 수열과 BEST 정리(de Bruijn Sequence), 라인스의 베타 평면 띠 제트(Zonal Jets)예요. 53판은 열을 더했어요. 듀크, 가르시아, 루츠가 그린 가우스 주기(Gaussian Periods), 부르가 보인 현수면과 나선면의 휨(Catenoid-Helicoid), 디랙의 벨트 묘기(Belt Trick), 헬름홀츠와 러브의 연기 고리 개구리뜀(Leapfrogging Vortices), 브룩스, 스미스, 스톤, 텃의 정사각형 분할(Squared Rectangle), 포드와 풀커슨의 최대 흐름 최소 절단(Max-Flow Min-Cut), 코스타스 배열(Costas Array), 바라니, 비어든, 칸의 무게중심 분할(Barycentric Subdivision), 무코파디아야와 크네저의 네 꼭짓점 정리(Four-Vertex Theorem), 오일러의 엘라스티카(Euler's Elastica)예요. 54판은 열을 더했어요. 베리의 무작위 파동과 보고몰니, 슈미트가 스미기로 센 마디 영역(Nodal Domains), 피어험버트가 사인 흐름에서 찾은 이상 고유 모드(Strange Eigenmode), 피어시 적분과 베리, 업스틸의 첨점 회절 파국(Pearcey Cusp), 나겔과 바이스의 STIT 모자이크(STIT Tessellation), 레비의 아크사인 법칙(Arcsine Law), 스퇴르메르가 다룬 쌍극자 장 속 대전 입자(Størmer Trap), 게일과 섀플리의 지연 수락(Stable Marriage), 스메일의 말굽(Smale Horseshoe), 울람 수열과 슈타이너베르거가 찾은 숨은 진동수(Ulam Sequence), 위어와 펠런의 거품과 켈빈의 깎은 팔면체(Weaire-Phelan)예요. 55판은 열을 더했어요. 밀러와 셰필드의 허수 기하(Imaginary Geometry), 티소의 지시 타원과 메르카토르 투영(Tissot's Indicatrix), 민코프스키의 물음표 함수(Question Mark Function), 버로스와 휠러의 변환(Burrows-Wheeler), 뷔퐁의 바늘과 바르비에의 국수(Buffon's Noodle), 부자의 리듬 타일링 캐넌(Vuza Canon), 하인과 내시의 헥스와 게일의 헥스 정리(Hex Theorem), 8자 고리의 범피복과 자유군의 케일리 그래프(Universal Cover), 오브친니코프와 젤도비치, 투생과 윌첵의 확산 제한 소멸(Toussaint-Wilczek), 헬름홀츠와 켈빈의 와류 시트와 크라스니의 와동 덩이(Vortex Sheet)예요. 56판은 열을 더했어요. 마테롱의 죽은 잎과 켄들, 퇴네스가 아래에서 본 완전 표본(Dead Leaves), 골롬의 트로미노 깔기(Golomb Trominoes), 윌슨의 고리 지운 걸음과 롤러, 슈람, 베르너의 균등 생성나무 페아노 곡선(Tree Peano Curve), 블로흐와 제너의 진동과 워니어의 사다리(Bloch Oscillations), 앤드루스의 곡선과 아시모프의 그랜드 투어(Andrews Curves), 지니브르와 기르코의 원 법칙과 좀머스, 크리산티, 솜폴린스키, 스타인의 타원 법칙(Elliptic Law), 에르되시의 불일치 문제와 보웨인, 최, 쿤스의 3진 수열(Erdős Discrepancy), 망델브로의 곱셈 폭포와 카안, 페이리에르의 문턱(Multiplicative Cascade), 코르테, 체이킨, 골럽, 파인의 무작위 조직화(Random Organization), 칼루가레아누, 화이트, 풀러의 꼬임과 뒤틀림(Twist and Writhe)이에요. 57판도 열을 더했어요. 블룸의 중심축과 들불 변환(Medial Axis), 스메일과 윌리엄스의 솔레노이드(Solenoid), 솔로몬의 무작위 환경 속 걸음과 시나이의 걸음(Sinai's Walk), 테렐과 펜로즈의 회전(Terrell Rotation), 갈페린의 π 당구(Galperin Billiard), 페르마의 두 제곱수 정리와 자기에의 한 문장 증명(Zagier's Windmills), 스토크스 표류와 게르스트너의 트로코이드 파도(Stokes Drift), 라인하르트부터 맨, 매클라우드맨, 폰 데라우까지의 볼록 오각형 타일링(Pentagon Tilings), 페로와 하위헌스의 트랙트릭스와 자전거 바퀴 자국(Bicycle Tracks), 오스트발트 숙성과 LSW 이론(Ostwald Ripening)이에요. 58판도 열을 더했어요. 야블란과 게르데스의 거울 곡선(Mirror Curves), 가운트와 피셔, 백스터의 딱딱한 정사각형 격자 기체(Hard Squares), 다니엘 베르누이의 매달린 사슬과 베셀 함수 J0(Hanging Chain), 레일리와 테일러의 불안정성(Rayleigh-Taylor), 터키의 반평면 깊이와 라도의 중심점 정리(Tukey Depth), 롤러와 베르너의 브라운 고리 수프(Loop Soup), 카돔체프-페트비아시빌리 방정식과 코다마의 선 솔리톤 웹(KP Web), 크누스가 이름 붙인 베케트-그레이 코드(Beckett-Gray), 파스칼의 신비 육각형과 슈타이너, 커크먼의 점(Hexagrammum Mysticum), 드 스밋과 렌스트라의 판화 화랑 사상(Print Gallery)이에요. 59판도 열을 더했어요. 르장드르와 가우스의 이차 상호 법칙(Quadratic Reciprocity), 위그너 함수와 유르케, 스톨러의 켜 고양이(Wigner Cat), 아비단과 샤미르의 심 카빙(Seam Carving), 슐리히팅과 노르트마르크, 에거스의 알갱이 기체 속 맥스웰의 도깨비(Granular Demon), 클렙시의 대각 3차 곡면과 케일리, 새먼의 직선 27개(Clebsch Surface), 보데르베르크의 나선 타일링(Voderberg Tiling), 자이페르트의 곡면(Seifert Surface), 호크스의 자기 흥분 점과정(Hawkes Process), 코호넨의 자기조직 지도(Kohonen Map), 레일리의 위상자 걸음과 굿맨의 스페클 통계(Laser Speckle)예요. 60판도 열을 더했어요. 마살리아가 짚은 RANDU 의 평면(RANDU Planes), 오일러의 36 장교와 보스, 슈리칸데, 파커의 10차 그레코-라틴 방진(Euler's Officers), 펜로즈와 카터의 등각 콤팩트화 도형(Penrose Diagram), 러더퍼드 산란의 포물선 그림자(Rutherford Shadow), 오일러 나선과 프레넬 적분, 코르뉴의 회절 셈(Cornu Spiral), 리처드슨의 디바이더와 망델브로의 해안선 차원(Richardson Dividers), 에농, 홀, 페익스의 1차원 자기중력 면 모형과 린든-벨의 격렬한 이완(Phase Spiral), 에르되시와 레니의 거대 성분과 아클리옵타스, 드수자, 스펜서의 폭발적 스미기(Giant Component), 허프와 두다, 하트의 직선 찾기(Hough Transform), 조르당의 곡선 정리와 심랏의 교차 세기(Jordan Curve)예요. 61판도 열을 더했어요. 세스나와 다섯 사람의 영온 무작위장 이징 눈사태(Crackling Noise), 몽주의 흙 옮기기와 매캔의 옮김 보간(Monge Transport), 파울의 덫과 마티외 방정식(Paul Trap), 그린힐의 제 무게 기둥(Greenhill Column), 흐바탈의 미술관 정리와 피스크의 세 색 증명(Art Gallery), 홀디치의 고리(Holditch Ring), 톰슨의 꺾인 역선과 헤비사이드의 움직이는 전하(Thomson Kink), 슈나이더와 바이스의 쌍성 렌즈(Binary Lens), 테일러의 되섞기(Taylor Unmixing), 브누아, 칼로, 디에네르 부부의 카나르 폭발(Canard Explosion)이에요. 62판도 열을 더했어요. 아브리코소프와 클라이너, 로스, 오틀러의 소용돌이 격자(Abrikosov Lattice), 울리크니의 청색 잡음과 빈자리와 뭉치 방법(Void and Cluster), 파인먼과 힙스의 체커보드(Feynman Checkerboard), 라도의 바쁜 비버와 마크센, 분트로크의 다섯 상태 챔피언(Busy Beaver), 폭스와 커시너, 비치의 평평한 곡면 위 직선 흐름(Veech Octagon), 리만의 미분 불가능 함수와 거버, 다위스테르마트의 복소 곡선(Riemann's Function), 헤슈와 킨즐레의 한 타일 형(Heesch Tiles), 차크라보르티와 헤이스, 보고시안의 마당 장터 모형(Yard-Sale Model), 베르그렌의 피타고라스 세 수 나무(Berggren Tree), 더브라윈과 람, 렁의 단위근 합과 밀른의 완전히 균형 잡힌 리듬(Balanced Rhythms)이에요. 63판은 다섯을 더했어요. 모팻의 모서리 소용돌이와 다네다의 사진(Moffatt Eddies), 맥스웰의 물고기 눈 렌즈(Maxwell's Fish-Eye), 이지케비치의 스파이크 신경망(Izhikevich Network), 애들러와 하소퍼, 우슬리의 들뜸 집합 오일러 지표(Excursion Sets), 심슨과 블라이스의 역설(Simpson's Paradox)이에요. 64판은 다섯을 더했어요. 암만과 베인커르의 팔각 타일링을 더 브라윈의 다중 격자로 지은 것(Ammann-Beenker), 겔판트, 카프라노프, 젤레빈스키의 아메바와 포르스베리, 파사레, 치흐의 구멍(Polynomial Amoeba), 키블과 주렉의 결함 얼기와 피시먼, 데 키아라, 칼라르코, 모리지의 지그재그 전이(Kibble-Zurek Ions), 야코비의 적분과 힐의 영속도 곡선(Hill's Regions), 스티븐슨과 카피차의 거꾸로 선 진자와 아치슨, 멀린의 인도 밧줄 묘기(Indian Rope Trick)예요. 65판은 다섯을 더했어요. 폰 노이만과 멀린스의 거품 법칙(Foam Coarsening), 베드퍼드와 맥멀런의 자기 아핀 카펫(McMullen Carpet), 클라인의 돛과 라그랑주의 주기 연분수(Klein's Sails), 야코비의 측지선과 샬의 공초점 곡면(Jacobi Geodesics), 하다마르와 리브친스키의 물방울과 코르테스의 정규화 스토크슬렛(Sedimenting Cloud)이에요. 67판은 다섯을 더했어요. 라돈의 변환과 라마찬드란, 락슈미나라야난의 램프 필터(Radon Transform), 아르틴의 브레이드 군과 폭스, 노이워스의 배치 공간(Artin Braids), 월시의 함수계와 실베스터의 행렬, 페일리의 차례(Walsh Functions), 아레프의 깜박이는 소용돌이(Blinking Vortex), 뒤팽의 사이클라이드(Dupin Cyclide)예요. 68판은 다섯을 더했어요. 들로네의 빈 외접원과 로슨의 뒤집기(Delaunay Flips), 모네로-뒤멘의 피보나치 단어 프랙탈(Fibonacci Word), 로즈의 검출 기준(Rose Criterion), 요세푸스 문제와 바셰의 셈 놀이(Josephus Problem), 레비치비타의 평행 이동과 푸코의 진자(Parallel Transport)예요. 69판은 다섯을 더했어요. 랭퍼드의 짝과 데이비스의 존재 조건, 스콜렘의 수열(Langford Pairs), 아하로노프와 봄의 효과와 베리 등의 파면 전위(Aharonov-Bohm), 페니의 놀이와 콘웨이의 선행수(Penney's Game), 휘트니와 그라우스타인의 회전수 정리(Whitney-Graustein), 슈타이너의 대칭화와 그로스의 수렴(Steiner Symmetrization)이에요. 70판은 다섯을 더했어요. 아우렌하머의 파워 다이어그램과 기타가와, 메리고, 티베르의 뉴턴 수송(Laguerre Cells), 배빗과 루윈의 헥사코드 정리(Hexachordal Theorem), 프렌켈과 콘토로바의 사슬(Frenkel-Kontorova), 에스하위스, 판 데르 벨레, 판 데르 메이르, 로서의 알갱이 라이덴프로스트 효과(Granular Leidenfrost), 카안의 가우스 곱셈 카오스(Multiplicative Chaos)예요. 71판은 다섯을 더했어요. 페니의 −1+i 진법과 데이비스, 크누스의 쌍둥이 용(Twindragon), 노바키, 한체, 벤트의 평탄 3차원 다양체와 콘웨이, 로세티의 이름(Platycosms), 크누스의 미니맥스 마스터마인드(Mastermind), 스넬슨의 텐세그리티 프리즘(Tensegrity), 아폴로니오스의 접촉 문제와 비에트의 풀이(Problem of Apollonius)예요. 72판은 다섯을 더했어요. 루퍼트의 들로네 다듬기(Ruppert Refinement), 라그랑주와 가우스의 격자 기저 줄이기(Lattice Reduction), 커비와 패리스의 히드라(Hydra Game), 프랭크의 액정 전위와 교차 편광 무늬(Schlieren Texture), 콘웨이와 콕서터의 정수 띠(Conway-Coxeter Frieze)예요. 73판은 다섯을 더했어요. 베리와 발라즈의 에어리 파속과 시빌로글루, 크리스토도울리데스의 유한 에너지 빔(Airy Beam), 스틴과 시배크가 반례로 실은 위상수학자의 사인 곡선(Topologist's Sine Curve), 에렌페스트 부부의 항아리와 카츠의 귀환 시간(Ehrenfest Urn), 월리스의 심슨 선과 슈타이너의 델토이드(Steiner Deltoid), 가드너가 소개한 보스의 1/f 주사위(Voss Dice)예요. +주제 5 로 쓴 수학과 다음에 해 볼 수학이에요. 28판이 처음 열 개를 썼어요. 슈람-뢰브너 진화(Loewner Trace), 포드 원(Ford Circles), 털 난 공 정리(Hairy Ball), 아즈텍 다이아몬드와 북극권(Aztec Diamond), 부니모비치 경기장(Stadium Billiard), 라우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 다면체와 E8(Gosset Polytope), 콘웨이의 보고 말하기 수열(Look and Say), 위토프 게임의 그런디 값(Wythoff Nim), 쌍곡 평면의 띠 모형(Hyperbolic Band)이에요. 29판은 여섯을 더했어요. 클라인의 j 불변량(J-Invariant), 호프 올뭉치(Hopf Fibration), 다이슨 브라운 운동(Dyson Gas), 가케야 바늘 문제의 페론 나무(Perron Tree), 임계선 위의 리만 제타 함수(Zeta Loops), 길버트-섀넌-리즈 리플과 베이어-디아코니스의 일곱 번(Seven Shuffles)이에요. 30판도 여섯을 더했어요. 세 간격 정리(Three Gaps), 호프스태터 나비와 TKNN 정수(Hofstadter Butterfly), 노이만의 외부 당구(Outer Billiards), 볼드의 탄도 퇴적과 KPZ 앞면(Ballistic Deposition), 아르놀트의 미앤더(Meander), 브로드벤트와 해머슬리의 스미기(Percolation)예요. 31판도 여섯을 더했어요. 마스킷 조각의 클라인 군 극한 집합(Indra's Pearls), 리틀우드 다항식의 근(Littlewood Roots), 고든의 가우스 해자(Gaussian Moat), 아르놀트의 고양이 사상(Cat Map), 자부스키와 크루스칼의 KdV 솔리톤(KdV Solitons), 보이 곡면(Boy's Surface)이에요. ROUND.md 의 「낯선 수학 예」 여덟 가운데 쓴 것은 슈람-뢰브너 진화와 아즈텍 다이아몬드, 모듈러 형식, 호프 올뭉치, 세 간격 정리, 외부 당구 여섯이었고, 31판이 리틀우드 다항식의 근과 클라인 군의 극한 집합을 써서 여덟을 다 썼어요. 32판도 여섯을 더했어요. 탤벗 효과와 양자 카펫(Talbot Carpet), 길브레스 추측의 표(Gilbreath Table), 원 사상의 아르놀트 혀(Arnold Tongues), 콘웨이와 라딘의 핀휠 타일링(Pinwheel Tiling), 윌킨슨과 윌렘센의 침입 스미기(Invasion Percolation), 울람의 문제와 해머슬리 과정(Hammersley Lines)이에요. 33판도 여섯을 더했어요. 에인절, 홀로이드, 로믹, 비라그의 무작위 정렬 네트워크(Sorting Network), 베르트랑의 역설(Bertrand Paradox), 펜로즈의 비출 수 없는 방(Penrose Room), 당들랭 구(Dandelin Spheres), 테이트의 보로메오 고리(Borromean Rings), 골드바흐 혜성(Goldbach Comet)이에요. 34판도 여섯을 더했어요. 톰슨 문제와 오일러가 남기는 다섯 이웃 열둘(Thomson Problem), 켈빈의 배 물결(Kelvin Wake), 요네야마가 낸 와다의 호수(Lakes of Wada), 샤이데거의 강 그물(Scheidegger Rivers), 모저의 소파 문제와 해머슬리의 소파(Moving Sofa), 크누스의 춤추는 링크로 푼 펜토미노 덮기(Pentomino Cover)예요. 35판도 여섯을 더했어요. 뢸로의 폭이 일정한 곡선(Constant Width), 렌니의 주차 문제(Rényi Parking), 카리의 비주기 왕 타일(Kari-Culik Tiles), 콜라코스키 수열(Kolakoski), 쿠라모토와 바톡토흐의 키메라 상태(Chimera State), 주콥스키 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했어요. 그레이슨의 정리가 말하는 곡선 단축 흐름(Curve Shortening), 헤우드의 일곱 색 토러스 지도(Heawood Torus), 슈바르츠 랜턴(Schwarz Lantern), 윌브러햄과 깁스의 현상(Gibbs Phenomenon), 타원 곡선의 현과 접선(Elliptic Chords), 지먼의 파국 기계(Catastrophe Machine)예요. 37판도 여섯을 더했어요. 포천의 쓸기 선 알고리즘(Fortune's Sweep), 앙투안의 목걸이(Antoine's Necklace), 카르만 소용돌이 길의 안정성(Kármán Street), 단단한 원판의 헥사틱 상(Hexatic Phase), 존슨-멜 쪽매와 아브라미 법칙(Johnson-Mehl), 쿠라모토-시바신스키 방정식(Kuramoto-Sivashinsky)이에요. 38판도 여섯을 더했어요. 앤더슨 국소화(Anderson Localization), 무어가 찾고 Chenciner 와 Montgomery 가 증명한 세 몸 문제의 8자 해(Figure-Eight Orbit), 예센과 빈트너, 에르되시의 베르누이 합성곱(Bernoulli Convolutions), 브라우어르의 고정점 정리를 증명하는 슈페르너 보조정리(Sperner's Lemma), 에겐베르거와 폴리아의 항아리(Pólya Urn), 뇌르고르의 무한 수열(Infinity Series)이에요. 39판도 여섯을 더했어요. 고든, 웹, 월퍼트의 등스펙트럼 북(Isospectral Drums), 폰 망골트의 꼴로 쓴 리만의 명시 공식(Explicit Formula), 요한 베르누이의 최단 강하선(Brachistochrone), 슈타이너의 포리즘(Steiner Chain), 모런 집단으로 그린 킹먼의 합류(Kingman Coalescent), 보르수크-울람 정리(Borsuk-Ulam)예요. 40판도 여섯을 더했어요. 앨더스의 브라운 연속 랜덤 나무(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 함수(Hydrogen Orbital), 젤도비치 근사의 팬케이크(Zeldovich Pancakes), 망델브로가 내다보고 롤러, 슈람, 베르너가 증명한 브라운 운동의 바깥 경계(Brownian Frontier), 복소 긴즈부르크-란다우 방정식의 결함 난류(Defect Turbulence), 루퍼트 왕자의 정육면체(Prince Rupert's Cube)예요. 41판은 다섯을 더했어요. 홀과 왜건의 현수선 길과 정다각형 바퀴(Square Wheels), 퐁슬레의 포리즘(Poncelet Porism), 슐레플리의 4차원 정다포체를 자른 단면(Polytope Slices), 클림켁과 티먼, 마그누손의 사이트스왑(Siteswap), Dudeney 의 잡화상 퍼즐과 월리스-보여이-게르비엔 정리(Hinged Dissection)예요. 42판은 다섯을 더했어요. 바른스도르프 규칙으로 도는 기사 여행(Knight's Tour), 페르미와 파스타, 울람, 칭구의 되돌아옴(FPUT Recurrence), 포슬리에-립킨 반전기(Peaucellier Linkage), 몰리의 삼등분선 정리(Morley Triangle), 랄과 마드라스, 소칼의 피벗 알고리즘으로 돌린 자기 회피 걸음(Pivot Walk)이에요. 43판은 다섯을 더했어요. 월리스 워커의 칼레이도사이클(Kaleidocycle), 쌍곡형에서 록소드롬형으로 감기는 뫼비우스 변환의 흐름(Möbius Loxodrome), 터트의 용수철 정리(Tutte Embedding), 디펀트와 크래비츠의 고리 정리를 품은 히토메자시 사시코(Hitomezashi), 룽게 현상과 체비쇼프 점(Runge Phenomenon)이에요. 44판은 다섯을 더했어요. 마크 카츠의 고리 모형(Kac Ring), 스톤과 튜키의 햄 샌드위치 정리(Ham Sandwich), 브란트의 불가리아 솔리테어(Bulgarian Solitaire), 엘스켄스와 프리시의 탄도 소멸(Ballistic Annihilation), 페레그린의 솔리톤(Peregrine Soliton)이에요. 45판은 다섯을 더했어요. 로건-셰프와 베르시크-케로프의 극한 곡선으로 다가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도브루신 경계 조건을 건 임계 이징 모형의 SLE(3) 경계(Ising Interface), 프리예제프와 동료들의 오일러 걷개로 쌓는 로터 라우터 응집과 레빈-페레스의 스매시 합(Rotor-Router), 폴리아의 과수원 문제(Pólya's Orchard), 넬슨의 평면 색칠 문제와 아이즈벨의 일곱 색(Hadwiger-Nelson)이에요. 46판은 다섯을 더했어요. 브릴루앙의 구역(Brillouin Zones), 모스 이론과 레브의 그래프(Reeb Graph), 로렌츠 기체와 시나이의 당구(Lorentz Gas), 리언 글래스의 점무늬(Glass Patterns), 벤치와 수테라, 불피아니의 확률 공명(Stochastic Resonance)이에요. 47판은 다섯을 더했어요. 오키프와 홍, 스트로가츠의 스워멀레이터(Swarmalators), 빌스의 외로운 달리기 선수 추측(Lonely Runner), 샤츠의 올로이드와 그 전개도(Oloid), 쾨베의 원 채우기 정리와 콜린스-스티븐슨의 반복(Koebe Packing), 콘웨이의 병사와 황금비 무게(Conway's Soldiers)예요. 48판은 다섯을 더했어요. 오하라의 매듭 뫼비우스 에너지(Möbius Energy), 체비쇼프의 편향과 루빈스타인-사르낙의 밀도(Chebyshev's Bias), 섀든과 레키엔, 마스든의 이중 소용돌이 능선(Double Gyre), 아자리와 프로스트의 깜빡이는 래칫(Flashing Ratchet), 망델브로의 프랙탈 스미기(Fractal Percolation)예요. 49판은 다섯을 더했어요. 뒤러의 전개 문제와 셰퍼드의 추측(Dürer's Unfolding), 베리와 골드버그가 재규격화한 세타 합의 꼬부랑 곡선(Curlicue), 오일러 방정식과 푸앵소의 폴호드로 본 테니스 라켓 정리(Tennis Racket), 켐프의 사슬과 네 색 정리(Kempe Chains), 퇴플리츠의 내접 정사각형 문제와 그린-로브의 직사각형(Square Peg)이에요. 50판은 다섯을 더했어요. 크레이치넌과 배철러의 2차원 난류가 거꾸로 가는 캐스케이드와 맥윌리엄스의 소용돌이(Inverse Cascade), 사포발과 로소, 구예의 확산 앞줄(Diffusion Front), 필리와 마르케티, 케이츠와 타이외르의 운동성 상분리(Motility Clusters), 박과 스네펜의 진화 모형(Bak-Sneppen), 리세의 영원한 가속(Risset Rhythm)이에요. 51판은 다섯을 더했어요. 스미스와 마이어스, 캐플런, 굿먼-스트라우스의 모자 타일과 H/T/P/F 메타타일 치환(Hat Monotile), 반도와 동료들의 최적 속도 모형과 스기야마의 원형 트랙 실험(Phantom Jam), 사토와 테이트의 법칙과 타원곡선의 a_p(Sato-Tate), 칼라비와 야우의 공간에서 핸슨이 잘라 그린 5차 페르마 곡선(Calabi-Yau Quintic), 에델스브루너와 레처, 조모로디언의 지속 호몰로지와 바코드(Persistence Barcode)예요. 52판은 다섯을 더했어요. 슈람과 셰필드가 등고선을 SLE(4) 로 보인 2차원 가우스 자유장(Gaussian Free Field), 당튀와 마즈무다르, 베링거의 광탄성 힘 사슬(Force Chains), 앤스컴의 사중주와 마테이카, 피츠모리스의 같은 통계 담금질(Anscombe's Quartet), 드 브라윈 수열과 BEST 정리(de Bruijn Sequence), 라인스의 베타 평면 띠 제트(Zonal Jets)예요. 53판은 열을 더했어요. 듀크, 가르시아, 루츠가 그린 가우스 주기(Gaussian Periods), 부르가 보인 현수면과 나선면의 휨(Catenoid-Helicoid), 디랙의 벨트 묘기(Belt Trick), 헬름홀츠와 러브의 연기 고리 개구리뜀(Leapfrogging Vortices), 브룩스, 스미스, 스톤, 텃의 정사각형 분할(Squared Rectangle), 포드와 풀커슨의 최대 흐름 최소 절단(Max-Flow Min-Cut), 코스타스 배열(Costas Array), 바라니, 비어든, 칸의 무게중심 분할(Barycentric Subdivision), 무코파디아야와 크네저의 네 꼭짓점 정리(Four-Vertex Theorem), 오일러의 엘라스티카(Euler's Elastica)예요. 54판은 열을 더했어요. 베리의 무작위 파동과 보고몰니, 슈미트가 스미기로 센 마디 영역(Nodal Domains), 피어험버트가 사인 흐름에서 찾은 이상 고유 모드(Strange Eigenmode), 피어시 적분과 베리, 업스틸의 첨점 회절 파국(Pearcey Cusp), 나겔과 바이스의 STIT 모자이크(STIT Tessellation), 레비의 아크사인 법칙(Arcsine Law), 스퇴르메르가 다룬 쌍극자 장 속 대전 입자(Størmer Trap), 게일과 섀플리의 지연 수락(Stable Marriage), 스메일의 말굽(Smale Horseshoe), 울람 수열과 슈타이너베르거가 찾은 숨은 진동수(Ulam Sequence), 위어와 펠런의 거품과 켈빈의 깎은 팔면체(Weaire-Phelan)예요. 55판은 열을 더했어요. 밀러와 셰필드의 허수 기하(Imaginary Geometry), 티소의 지시 타원과 메르카토르 투영(Tissot's Indicatrix), 민코프스키의 물음표 함수(Question Mark Function), 버로스와 휠러의 변환(Burrows-Wheeler), 뷔퐁의 바늘과 바르비에의 국수(Buffon's Noodle), 부자의 리듬 타일링 캐넌(Vuza Canon), 하인과 내시의 헥스와 게일의 헥스 정리(Hex Theorem), 8자 고리의 범피복과 자유군의 케일리 그래프(Universal Cover), 오브친니코프와 젤도비치, 투생과 윌첵의 확산 제한 소멸(Toussaint-Wilczek), 헬름홀츠와 켈빈의 와류 시트와 크라스니의 와동 덩이(Vortex Sheet)예요. 56판은 열을 더했어요. 마테롱의 죽은 잎과 켄들, 퇴네스가 아래에서 본 완전 표본(Dead Leaves), 골롬의 트로미노 깔기(Golomb Trominoes), 윌슨의 고리 지운 걸음과 롤러, 슈람, 베르너의 균등 생성나무 페아노 곡선(Tree Peano Curve), 블로흐와 제너의 진동과 워니어의 사다리(Bloch Oscillations), 앤드루스의 곡선과 아시모프의 그랜드 투어(Andrews Curves), 지니브르와 기르코의 원 법칙과 좀머스, 크리산티, 솜폴린스키, 스타인의 타원 법칙(Elliptic Law), 에르되시의 불일치 문제와 보웨인, 최, 쿤스의 3진 수열(Erdős Discrepancy), 망델브로의 곱셈 폭포와 카안, 페이리에르의 문턱(Multiplicative Cascade), 코르테, 체이킨, 골럽, 파인의 무작위 조직화(Random Organization), 칼루가레아누, 화이트, 풀러의 꼬임과 뒤틀림(Twist and Writhe)이에요. 57판도 열을 더했어요. 블룸의 중심축과 들불 변환(Medial Axis), 스메일과 윌리엄스의 솔레노이드(Solenoid), 솔로몬의 무작위 환경 속 걸음과 시나이의 걸음(Sinai's Walk), 테렐과 펜로즈의 회전(Terrell Rotation), 갈페린의 π 당구(Galperin Billiard), 페르마의 두 제곱수 정리와 자기에의 한 문장 증명(Zagier's Windmills), 스토크스 표류와 게르스트너의 트로코이드 파도(Stokes Drift), 라인하르트부터 맨, 매클라우드맨, 폰 데라우까지의 볼록 오각형 타일링(Pentagon Tilings), 페로와 하위헌스의 트랙트릭스와 자전거 바퀴 자국(Bicycle Tracks), 오스트발트 숙성과 LSW 이론(Ostwald Ripening)이에요. 58판도 열을 더했어요. 야블란과 게르데스의 거울 곡선(Mirror Curves), 가운트와 피셔, 백스터의 딱딱한 정사각형 격자 기체(Hard Squares), 다니엘 베르누이의 매달린 사슬과 베셀 함수 J0(Hanging Chain), 레일리와 테일러의 불안정성(Rayleigh-Taylor), 터키의 반평면 깊이와 라도의 중심점 정리(Tukey Depth), 롤러와 베르너의 브라운 고리 수프(Loop Soup), 카돔체프-페트비아시빌리 방정식과 코다마의 선 솔리톤 웹(KP Web), 크누스가 이름 붙인 베케트-그레이 코드(Beckett-Gray), 파스칼의 신비 육각형과 슈타이너, 커크먼의 점(Hexagrammum Mysticum), 드 스밋과 렌스트라의 판화 화랑 사상(Print Gallery)이에요. 59판도 열을 더했어요. 르장드르와 가우스의 이차 상호 법칙(Quadratic Reciprocity), 위그너 함수와 유르케, 스톨러의 켜 고양이(Wigner Cat), 아비단과 샤미르의 심 카빙(Seam Carving), 슐리히팅과 노르트마르크, 에거스의 알갱이 기체 속 맥스웰의 도깨비(Granular Demon), 클렙시의 대각 3차 곡면과 케일리, 새먼의 직선 27개(Clebsch Surface), 보데르베르크의 나선 타일링(Voderberg Tiling), 자이페르트의 곡면(Seifert Surface), 호크스의 자기 흥분 점과정(Hawkes Process), 코호넨의 자기조직 지도(Kohonen Map), 레일리의 위상자 걸음과 굿맨의 스페클 통계(Laser Speckle)예요. 60판도 열을 더했어요. 마살리아가 짚은 RANDU 의 평면(RANDU Planes), 오일러의 36 장교와 보스, 슈리칸데, 파커의 10차 그레코-라틴 방진(Euler's Officers), 펜로즈와 카터의 등각 콤팩트화 도형(Penrose Diagram), 러더퍼드 산란의 포물선 그림자(Rutherford Shadow), 오일러 나선과 프레넬 적분, 코르뉴의 회절 셈(Cornu Spiral), 리처드슨의 디바이더와 망델브로의 해안선 차원(Richardson Dividers), 에농, 홀, 페익스의 1차원 자기중력 면 모형과 린든-벨의 격렬한 이완(Phase Spiral), 에르되시와 레니의 거대 성분과 아클리옵타스, 드수자, 스펜서의 폭발적 스미기(Giant Component), 허프와 두다, 하트의 직선 찾기(Hough Transform), 조르당의 곡선 정리와 심랏의 교차 세기(Jordan Curve)예요. 61판도 열을 더했어요. 세스나와 다섯 사람의 영온 무작위장 이징 눈사태(Crackling Noise), 몽주의 흙 옮기기와 매캔의 옮김 보간(Monge Transport), 파울의 덫과 마티외 방정식(Paul Trap), 그린힐의 제 무게 기둥(Greenhill Column), 흐바탈의 미술관 정리와 피스크의 세 색 증명(Art Gallery), 홀디치의 고리(Holditch Ring), 톰슨의 꺾인 역선과 헤비사이드의 움직이는 전하(Thomson Kink), 슈나이더와 바이스의 쌍성 렌즈(Binary Lens), 테일러의 되섞기(Taylor Unmixing), 브누아, 칼로, 디에네르 부부의 카나르 폭발(Canard Explosion)이에요. 62판도 열을 더했어요. 아브리코소프와 클라이너, 로스, 오틀러의 소용돌이 격자(Abrikosov Lattice), 울리크니의 청색 잡음과 빈자리와 뭉치 방법(Void and Cluster), 파인먼과 힙스의 체커보드(Feynman Checkerboard), 라도의 바쁜 비버와 마크센, 분트로크의 다섯 상태 챔피언(Busy Beaver), 폭스와 커시너, 비치의 평평한 곡면 위 직선 흐름(Veech Octagon), 리만의 미분 불가능 함수와 거버, 다위스테르마트의 복소 곡선(Riemann's Function), 헤슈와 킨즐레의 한 타일 형(Heesch Tiles), 차크라보르티와 헤이스, 보고시안의 마당 장터 모형(Yard-Sale Model), 베르그렌의 피타고라스 세 수 나무(Berggren Tree), 더브라윈과 람, 렁의 단위근 합과 밀른의 완전히 균형 잡힌 리듬(Balanced Rhythms)이에요. 63판은 다섯을 더했어요. 모팻의 모서리 소용돌이와 다네다의 사진(Moffatt Eddies), 맥스웰의 물고기 눈 렌즈(Maxwell's Fish-Eye), 이지케비치의 스파이크 신경망(Izhikevich Network), 애들러와 하소퍼, 우슬리의 들뜸 집합 오일러 지표(Excursion Sets), 심슨과 블라이스의 역설(Simpson's Paradox)이에요. 64판은 다섯을 더했어요. 암만과 베인커르의 팔각 타일링을 더 브라윈의 다중 격자로 지은 것(Ammann-Beenker), 겔판트, 카프라노프, 젤레빈스키의 아메바와 포르스베리, 파사레, 치흐의 구멍(Polynomial Amoeba), 키블과 주렉의 결함 얼기와 피시먼, 데 키아라, 칼라르코, 모리지의 지그재그 전이(Kibble-Zurek Ions), 야코비의 적분과 힐의 영속도 곡선(Hill's Regions), 스티븐슨과 카피차의 거꾸로 선 진자와 아치슨, 멀린의 인도 밧줄 묘기(Indian Rope Trick)예요. 65판은 다섯을 더했어요. 폰 노이만과 멀린스의 거품 법칙(Foam Coarsening), 베드퍼드와 맥멀런의 자기 아핀 카펫(McMullen Carpet), 클라인의 돛과 라그랑주의 주기 연분수(Klein's Sails), 야코비의 측지선과 샬의 공초점 곡면(Jacobi Geodesics), 하다마르와 리브친스키의 물방울과 코르테스의 정규화 스토크슬렛(Sedimenting Cloud)이에요. 67판은 다섯을 더했어요. 라돈의 변환과 라마찬드란, 락슈미나라야난의 램프 필터(Radon Transform), 아르틴의 브레이드 군과 폭스, 노이워스의 배치 공간(Artin Braids), 월시의 함수계와 실베스터의 행렬, 페일리의 차례(Walsh Functions), 아레프의 깜박이는 소용돌이(Blinking Vortex), 뒤팽의 사이클라이드(Dupin Cyclide)예요. 68판은 다섯을 더했어요. 들로네의 빈 외접원과 로슨의 뒤집기(Delaunay Flips), 모네로-뒤멘의 피보나치 단어 프랙탈(Fibonacci Word), 로즈의 검출 기준(Rose Criterion), 요세푸스 문제와 바셰의 셈 놀이(Josephus Problem), 레비치비타의 평행 이동과 푸코의 진자(Parallel Transport)예요. 69판은 다섯을 더했어요. 랭퍼드의 짝과 데이비스의 존재 조건, 스콜렘의 수열(Langford Pairs), 아하로노프와 봄의 효과와 베리 등의 파면 전위(Aharonov-Bohm), 페니의 놀이와 콘웨이의 선행수(Penney's Game), 휘트니와 그라우스타인의 회전수 정리(Whitney-Graustein), 슈타이너의 대칭화와 그로스의 수렴(Steiner Symmetrization)이에요. 70판은 다섯을 더했어요. 아우렌하머의 파워 다이어그램과 기타가와, 메리고, 티베르의 뉴턴 수송(Laguerre Cells), 배빗과 루윈의 헥사코드 정리(Hexachordal Theorem), 프렌켈과 콘토로바의 사슬(Frenkel-Kontorova), 에스하위스, 판 데르 벨레, 판 데르 메이르, 로서의 알갱이 라이덴프로스트 효과(Granular Leidenfrost), 카안의 가우스 곱셈 카오스(Multiplicative Chaos)예요. 71판은 다섯을 더했어요. 페니의 −1+i 진법과 데이비스, 크누스의 쌍둥이 용(Twindragon), 노바키, 한체, 벤트의 평탄 3차원 다양체와 콘웨이, 로세티의 이름(Platycosms), 크누스의 미니맥스 마스터마인드(Mastermind), 스넬슨의 텐세그리티 프리즘(Tensegrity), 아폴로니오스의 접촉 문제와 비에트의 풀이(Problem of Apollonius)예요. 72판은 다섯을 더했어요. 루퍼트의 들로네 다듬기(Ruppert Refinement), 라그랑주와 가우스의 격자 기저 줄이기(Lattice Reduction), 커비와 패리스의 히드라(Hydra Game), 프랭크의 액정 전위와 교차 편광 무늬(Schlieren Texture), 콘웨이와 콕서터의 정수 띠(Conway-Coxeter Frieze)예요. 73판은 다섯을 더했어요. 베리와 발라즈의 에어리 파속과 시빌로글루, 크리스토도울리데스의 유한 에너지 빔(Airy Beam), 스틴과 시배크가 반례로 실은 위상수학자의 사인 곡선(Topologist's Sine Curve), 에렌페스트 부부의 항아리와 카츠의 귀환 시간(Ehrenfest Urn), 월리스의 심슨 선과 슈타이너의 델토이드(Steiner Deltoid), 가드너가 소개한 보스의 1/f 주사위(Voss Dice)예요. 74판은 다섯을 더했어요. 콘웨이의 유리 엉킴과 슈베르트의 2다리 매듭(Rational Tangles), 처치의 람다 계산과 트롬프의 람다 그림(Lambda Diagrams), 쉬첸베르거의 프로모션과 헤이먼의 주기(Promotion), 리드와 솔로몬의 부호를 쓰는 QR 부호(Reed-Solomon), 투란의 벽돌 공장 문제와 자란키에비치의 그림(Brick Factory)이에요. 목록 밖에서 다음에 해 볼 것이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -4536,6 +4619,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 73판에서 본 것이에요. 이번 다섯은 수학이 정한 몫이 그림의 밝기를 정했어요. Topologist's Sine Curve 의 띠가 가장자리에서 밝은 것은 sin(1/x) 가 x 의 몫으로 ±1 근처에 오래 머물러서예요. 처음에는 곡선의 길이도 그렇다고 짐작했는데, 셈해 보니 길이로 세면 띠가 고르게 차서 두 셈을 넷째 노브로 섞게 했어요. Voss Dice 는 주사위를 넷에서 여덟로 늘려도 분홍 합의 한 걸음이 2.56 에서 2.61 로 거의 그대로인데, 백색의 한 걸음은 3.87 에서 5.46 으로 커져요. 그래서 주사위가 많을수록 가운데 칸만 차분해 보여요. 기다림은 기댓값처럼 고르게 오지 않았어요. Ehrenfest Urn 의 공 8 개는 600초에 기대 23.4 번 가운데 20 번 모두 위로 돌아왔는데, 63.0초와 63.2초처럼 두 걸음 사이로 붙어 오고 47초를 비우기도 했어요. 정리가 못 박는 수는 이번에도 셈으로 먼저 지켜봤어요. Steiner Deltoid 는 무작위 삼각형 200 개에서 세 발이 이루는 넓이가 1e-15 아래였고, 감싸는 곡선이 구점원 중심에서 R/2 와 3R/2 사이에 들었어요. Airy Beam 은 FFT 로 지은 맨 윗줄이 Ai 의 멱급수와 1e−5 안에서 맞았고, 구슬이 가린 로브는 아래에서 자라도 자유 빔 세기의 70% 를 넘지 못했어요. +74판에서 본 것이에요. 셈이 정확한 대상은 이번에도 따로 짠 셈으로 먼저 맞춰 봤어요. Reed-Solomon 은 낱말 여덟, 수준 넷, 마스크 여덟로 지은 부호 256개가 python qrcode 8.2 의 것과 모듈 하나 다르지 않았고 zxing-cpp 가 다 읽었어요. 고칠 수 있는 만큼의 무작위 오류 2,227벌은 벌캠프-매시와 포니의 식이 다 고쳤어요. Rational Tangles 는 T 와 R 을 아무렇게나 부르기를 20,000번 해서 그린 그림이 따라간 분수와 같은지 봤어요. Promotion 은 4×8, 8×16, 16×32 에서 꼭 N 번째에 처음 표로 돌아왔고, Brick Factory 는 m 4~6, n 6~9 의 열두 경우 모두 머무는 그림의 교차 수가 Z(m,n) 이었어요. 패널에 드는 판을 고르는 것도 일이었어요. Lambda Diagrams 의 여섯 식은 3~32 걸음에 끝나고 2 LED 간격에 드는 것만 골랐어요. 8×16 표의 프로모션은 집에 오기까지 4.8분이라, Size 노브의 4×8 과 2×4 가 돌아오는 것을 67초와 14초에 보여요. 2×4 에서는 세 표 가운데 하나가 2번 만에 돌아와서, 로즈의 순환 체가 세는 짧은 궤도가 패널에서 번쩍였어요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. @@ -4618,6 +4703,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 목록 밖에서 73판 뒤에 해 볼 것이에요. 위상수학자의 사인 곡선의 오른쪽 끝을 호로 극한 선분에 이어 닫으면 바르샤바 원이 돼요. 원처럼 한 바퀴 도는 꼴인데 그 위의 닫힌 경로는 다 한 점으로 줄어들어서, Topologist's Sine Curve 의 띠 셈과 구슬에 호 하나만 더하면 돼요. 닫는 호의 끝 (1, sin 1) 이 패널에 들도록 X 를 1 까지 넓혀야 해요. 패트릭 모란(Patrick Moran)의 과정(1958)은 걸음마다 공 하나가 사라지고 다른 공의 빛깔을 베껴 새로 나서, Ehrenfest Urn 처럼 수가 한 걸음에 많아야 하나씩 움직이는데도 0 이나 N 에 붙으면 돌아오지 않아요. 같은 두 방과 띠로 두 사슬을 나란히 세울 수 있어요. 카를 빌헬름 포이어바흐(Karl Wilhelm Feuerbach)의 정리(1822)는 구점원이 내접원과 방접원 셋에 늘 닿는다는 것이라, Steiner Deltoid 의 바뀌는 삼각형 위에서 닿는 점 넷이 구점원을 미끄러지는 것을 그릴 수 있어요. 니콜라오스 에프레미디스(Nikolaos Efremidis)와 크리스토도울리데스의 자동 집속 빔(2010)은 마주 보는 에어리 빔 둘이라, Airy Beam 의 표를 거울로 하나 더 읽으면 줄곧 어둡던 가운데가 아래쪽 한 줄에서 갑자기 밝아져요. 브누아 망델브로(Benoit Mandelbrot)와 존 밴 네스(John Van Ness)의 분수 브라운 운동(1968)은 허스트 지수 H 하나로 거칠기가 이어서 바뀌어서, Voss Dice 의 갈색 칸을 노브 하나로 거칠게도 매끄럽게도 할 수 있어요. +목록 밖에서 74판 뒤에 해 볼 것이에요. 쉬첸베르거의 진화(evacuation)를 Promotion 의 표에 나란히 돌리면, 두 번 하면 제자리인 뒤집기와 N 번 해야 제자리인 돌림이 한 표에서 맞물려요. QR 부호의 얼룩 자리를 디코더에 지움(erasure)으로 알려 주면 같은 오류 정정 코드워드로 두 배를 고쳐서, Reed-Solomon 의 부호 둘을 위아래로 세워 지움을 아는 디코더와 모르는 디코더를 견줄 수 있어요. 프랭크 해러리(Frank Harary)와 앤서니 힐(Anthony Hill)이 1963년에 낸 완전 그래프 K_n 의 교차수 추측은 Brick Factory 의 교차 세기를 그대로 쓰고 꼭짓점만 두 동심원에 나눠 놓으면 돼요. 아직 답을 모르는 K(7,11) 에서 떠도는 그림이 Z 에 얼마나 다가가는지도 볼 만해요. (λx.x x)(λx.x x) 처럼 정규형이 없는 항은 Lambda Diagrams 의 걸음 캡에 닿을 때까지 같은 그림을 되풀이해요. Rational Tangles 의 가라앉기는 아직 두 그림을 겹쳐 바꾸는데, 플라이프(flype)로 교차를 실제로 옮기면 움직임이 돼요. + ### 타이포 주제 7 로 해 본 글자 실험과 다음에 해 볼 것이에요. 63판과 64판이 같이 돌며 다섯씩 처음 만들었어요. 63판의 다섯은 한글 자모가 날아와 모아쓰기로 잠기는 조립(Jamo Assembly), 놀턴과 하먼의 기호 모자이크(Type Mosaic), 호프스태터의 트립렛 그림자(Trip-let), 카프리카의 계산 종이(Kaprekar's Routine), 캐럴의 낱말 사다리(Word Ladder)예요. @@ -4702,6 +4789,14 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 73판 뒤에 해 볼 것이에요. 바빌로니아의 60진 쐐기 숫자는 세운 쐐기 아홉 다음 열이 갈고리 하나가 되고 갈고리 여섯이 윗자리 쐐기 하나가 돼서, Maya Numerals 의 받아 올림을 그대로 쓸 수 있어요. 고대 바빌로니아 숫자에는 처음에 0 기호가 없어서 빈 자리를 어떻게 보일지가 숙제예요. 한글 한 자를 마스터 넷으로 그려 Multiple Master 처럼 섞으면 굵기 축에서 ㅁ 과 ㅇ 의 속공간이 어느 차례로 닫히는지 보여요. Dürer's Alphabet 의 정사각형과 컴퍼스로 ㅇ 과 ㅎ 의 원, ㄱ 과 ㄴ 의 꺾임을 긋고 모아쓴 한 글자의 몸에 잉크를 흘리면 한글 짓기가 돼요. 레온 글래스(Leon Glass)의 짝 점 무늬(1969)는 점마다 짝 점을 조금 돌린 자리에 찍는데, 낱말 안에서만 돌리면 점 하나하나에는 무늬가 없고 짝끼리의 결이 글자로 읽혀요. Glass Patterns 의 셈으로 Damask 와 다른 길의 숨은 글을 짤 수 있어요. +74판의 다섯은 국제 신호서의 글자 깃발을 깃줄에 올려 읽는 낱말(Flag Hoist), 무작위 점에 깊이로만 숨긴 낱말(Autostereogram), 개표소의 正 자로 세는 표와 베르트랑의 투표 문제(Tally Count), 트라야누스 기둥의 비문을 나선 띠에 감은 글줄(Trajan Helix), 슬론 글자를 흐림 렌즈로 읽는 시력표(Eye Chart)예요. + +74판에서 본 것이에요. 다섯이 글자가 패널에서 무엇으로 읽히는지를 저마다 갈랐어요. Autostereogram 의 낱말에는 윤곽이 없어서, 한 주기 왼쪽과 견주는 띠가 지나갈 때만 흰 점으로 서요. Flag Hoist 의 깃발은 글자를 색 무늬로 바꾸고, 깃발이 꼭대기에 닿아 있는 동안만 옆에 그 글자가 서서 읽는 법을 알려 줘요. Trajan Helix 의 8 LED 대문자는 기둥 앞에서 제 너비로 읽히다 가장자리에서 cos β 로 좁아지는데, 획까지의 거리를 돌 위에서 재야 좁아지는 글자가 매끈했어요. Eye Chart 는 1 LED 를 1분각으로 두어서 맨 아래 5 LED 줄이 이 패널의 6/6 줄이에요. 반지름 1.3 LED 의 흐림에서 그 줄은 72% 가 읽히고, 2.6 에서는 29% 로 떨어졌어요. Tally Count 는 획 하나가 표 하나라 글자가 셈이 돼요. 4,000번 센 칠판에서 1등이 끝까지 앞선 몫이 0.124 이고, 베르트랑의 (p−q)/(p+q) 의 평균은 0.125 였어요. + +기능 감은 둘이 글자를 코드 밖에서 받는 길을, 셋이 Text 레이어의 효과를 바랐어요. Flag Hoist 는 Lab 에서 친 낱말이 깃발로 오르기를, Autostereogram 은 Text 레이어의 글자를 깊이 지도로 받기를 바랐어요. Trajan Helix 는 Text 레이어를 원통에 감는 변형을, Eye Chart 는 레이어를 노브로 흐리는 효과를, Tally Count 는 글자를 획순대로 그어 가는 효과를 적었어요. + +74판 뒤에 해 볼 것이에요. Eye Chart 의 원판 흐림과 읽는 이를 한글 음절에 걸면, 받침 있는 글자가 몇 LED 에서 읽히지 않게 되는지 슬론 글자와 나란히 잴 수 있어요. 수기 신호(semaphore)는 두 팔의 각도로 글자를 세워서, Flag Hoist 의 깃줄 자리에 두 막대만 세우면 낱말이 위에서 아래로 읽혀요. 네 줄을 긋고 다섯째 획을 비스듬히 긋는 묶음을 Tally Count 의 正 옆에 세우면, 다섯 표를 닫는 획순이 셈 표시마다 어떻게 다른지 한 판에서 보여요. Autostereogram 의 깊이를 두세 단계로 나누면 한글 음절의 획을 층층이 숨길 수 있어요. + ### 화장품 팝업 주제 6 으로 쓴 질감과 다음에 해 볼 질감이에요. 29판이 처음 넷을 썼어요. 팔레트 나이프로 펴 바르는 크림(Cream Swirl), 구슬이 뜬 젤과 빛 그물(Serum Gel), 쏟아져 쌓이는 글리터(Glitter Fall), 퍼프가 눌렀다 든 압축 파우더(Loose Powder)예요. 넷 다 K1 이 색상환 한 바퀴라서, 색을 돌리면 주황과 노랑이 갈색이나 올리브로 보이는 자리가 있었어요. Serum Gel 은 그 둘레에서만 밝기를 0.62 와 0.3 에서 0.92 와 0.68 로 올렸어요. 밝은 바탕은 반짝임을 지웠어요. 200 위에 55 를 더한 반짝임은 거의 안 보여서 Glitter Fall 은 어두운 바탕에 섰고, 흰 바탕과 금색 한 줄은 Loose Powder 의 아래 띠로만 썼어요. 크림과 젤은 가장자리 0.028 과 0.079 로 연구에서 가장 번지는 그림이에요. 그 둘이 가장자리 축의 아래 끝을 옮겨서 지도에 새 빈 칸이 났어요. 크림은 빛만 움직여서는 움직임이 0.019 에 머물렀어요. 결이 깊어야 빛이 돌 때 바뀌는 LED 가 생겨요. 비누막이 터질 때 구멍 가장자리가 테일러와 컬릭의 속도(1959, 1960)로 고르게 벌어지며 물방울 테를 남기는 것은 Soap Film 의 끝을 늘인 그림이에요. 클렌징 오일이 물을 만나 가장자리부터 우윳빛으로 유화되는 것은 Blotting Paper 처럼 LED 마다 한 번만 바뀌는 셈으로 그릴 만해요. From 9793928f1237744ac20dee49e6f21f32577ff5e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 00:57:11 +0000 Subject: [PATCH 15/15] =?UTF-8?q?incoming:=2074=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_01Bc4Jkt9PaeQhb1WkHMuJUh --- 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 0e07d9feb..669307817 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

패턴 연구

- - + +