From 3f908e29e4f42fe47ecfb28dc053cf8c52a4b8f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 19:43:05 +0000 Subject: [PATCH 01/14] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=2072=ED=8C=90=20(pfpat?= =?UTF-8?q?=2070=EB=B0=94=ED=80=B4)?= 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_01Bc4JkrPaZ8GQQiA22PWEvR From dd113ec598a4cd6bc9f799bed77da3fb74ef6cc3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 20:18:47 +0000 Subject: [PATCH 02/14] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2072=ED=8C=90:=20Optical=20Kerning?= 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_01Bc4JkrPaZ8GQQiA22PWEvR --- .../pattern-study/patterns/optical-kerning.js | 260 ++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 incoming/pattern-study/patterns/optical-kerning.js diff --git a/incoming/pattern-study/patterns/optical-kerning.js b/incoming/pattern-study/patterns/optical-kerning.js new file mode 100644 index 000000000..a319eb953 --- /dev/null +++ b/incoming/pattern-study/patterns/optical-kerning.js @@ -0,0 +1,260 @@ +// ===== Patternflow pattern ===== +// Title: Optical Kerning +// Family: type +// Idea: Five words whose letters have open and slanted sides, AVOW, LYNX, TOY, WAVE and FLY among them, stand in capitals 19 LEDs tall down the panel, the white between each pair of neighbours glows in a colour that says how much of it there is, and after each word is set with the same 2 LED space between every pair of letter boxes, so that A and V gape while H and I sit tight, its letters slide sideways until every gap holds the same area and the glows of the word turn one colour, a wave of new words running down the panel every few seconds. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 72, after spacing letters by a formula rather than by eye as in David Kindersley's Optical Letter Spacing for New Printing Systems (1976 edition) and the spacing method in Walter Tracy's Letters of Credit (1986) +// Round: 72 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Tracking=-1..1, Depth=-1..1 +// +// Knob 1: Hue. Turns the glow palette round the wheel. At 0 a gap 1.5 LEDs or less across on average glows violet-blue, 3.6 cyan, 6 yellow-green and 7.5 or more amber; the letters stay warm white. +// Knob 2: Speed. At 2 a word is set, shown for 1.15 s at box spacing, slides into its spacing in 1.5 s, holds 3 s and goes, 6.35 s in all, and each line starts 0.45 s after the one above; at 0.2 a word takes 64 s, at 3 it takes 4.2 s. +// Knob 3: Tracking. The area every gap is spaced to, given as its width averaged over the 19 rows: 3.6 LEDs at 0, where every gap of all ten words meets it, H and I stand 3.6 apart and A tucks 0.3 under V; 2.4 at -1, where seven pairs, Y N, F L and T O among them, reach the 1 LED that letters may come to and stay a little wider than the rest; 5.4 at 1, AVOW then 57 LEDs wide. The settled glows are blue at -1, cyan at 0 and green at 1. +// Knob 4: Depth. How far into an open side the white is counted, measured in from the letter's outermost point: 3.2 LEDs at the opening 0.06. At -1 only 1 LED counts, every pair is spaced almost by its boxes and A V stays 1.9 apart, as wide as N X. At 1 the 8 LEDs counted reach right into the throats of A, V, L and T: A goes 0.6 under V and the L's foot 3.4 under the Y's arm, and still L Y, L T and F L cannot shed enough white without touching, so their glows stay yellow-green while the rest settle cyan. + +// The white between two letters is counted row by row through the cap +// height: the horizontal distance between their facing contours, except +// that on each side the distance is counted only Depth LEDs in from that +// letter's outermost point. Without that limit the open sides of A, L, T, V +// and Y would count all their white and pull their neighbours in until they +// touched. Every pair is then moved until its area meets the Tracking +// target, never closer than 1 LED in any row. +// +// Pairs whose slants run parallel, V Y or A X, cannot meet any target this +// way: the tips touch before the throats close. The words leave them out. + +const W = 64, H = 128, CAP = 19, HW = 1.25, LINES = 5, PITCH = 25, TOP0 = 4, BOX = 2, CLEAR = 1; +const { floor, round, min, max, abs, sqrt, exp, 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]; +} + +// ---- The capitals, drawn for this pattern in a box 19 LEDs tall: "r" is a +// filled rectangle x0, y0, x1, y1; "s" a stroke 2.5 LEDs wide along a +// segment, cut flat at the top and bottom of the box; "o" an elliptical +// ring 2.5 LEDs wide with its centre and the radii of its middle line. +const GLYPHS = { + A: [["s", 6, -1, 1.2, 19.6], ["s", 6, -1, 10.8, 19.6], ["r", 2.4, 12, 9.6, 14.5]], + V: [["s", 1.2, -0.6, 6, 20], ["s", 10.8, -0.6, 6, 20]], + O: [["o", 6, 9.5, 4.75, 8.25]], + W: [["s", 1.1, -0.5, 4.3, 19.6], ["s", 4.3, 19.6, 7.5, 3], ["s", 7.5, 3, 10.7, 19.6], ["s", 10.7, 19.6, 13.9, -0.5]], + L: [["r", 0, 0, 2.5, 19], ["r", 0, 16.5, 10, 19]], + Y: [["s", 1.2, -0.6, 6, 10], ["s", 10.8, -0.6, 6, 10], ["r", 4.75, 9, 7.25, 19]], + N: [["r", 0, 0, 2.5, 19], ["r", 9.5, 0, 12, 19], ["s", 1.4, 0.4, 10.6, 18.6]], + X: [["s", 1.2, -0.6, 10.8, 19.6], ["s", 10.8, -0.6, 1.2, 19.6]], + T: [["r", 0, 0, 12, 2.5], ["r", 4.75, 0, 7.25, 19]], + E: [["r", 0, 0, 2.5, 19], ["r", 0, 0, 10, 2.5], ["r", 0, 8.25, 9, 10.75], ["r", 0, 16.5, 10, 19]], + F: [["r", 0, 0, 2.5, 19], ["r", 0, 0, 10, 2.5], ["r", 0, 8.25, 9, 10.75]], + I: [["r", 0, 0, 2.5, 19]], + H: [["r", 0, 0, 2.5, 19], ["r", 8.5, 0, 11, 19], ["r", 0, 8.25, 11, 10.75]], +}; +// Line k takes words k and k + 5 in turn. +const WORDS = ["AVOW", "LYNX", "TOY", "WAVE", "FLY", "YAWL", "HILT", "VOLT", "AWAY", "OVAL"]; + +// Signed distance from a point in the box to the letter's ink (negative inside). +function dist(parts, x, y) { + let d = 1e9; + for (const p of parts) { + let e; + if (p[0] === "r") { + const dx = max(p[1] - x, x - p[3]), dy = max(p[2] - y, y - p[4]); + e = dx > 0 && dy > 0 ? hypot(dx, dy) : max(dx, dy); + } else if (p[0] === "s") { + const ax = p[1], ay = p[2], bx = p[3] - ax, by = p[4] - ay; + const t = cl(((x - ax) * bx + (y - ay) * by) / (bx * bx + by * by), 0, 1); + e = max(hypot(x - ax - t * bx, y - ay - t * by) - HW, -y, y - CAP); + } else { + const qx = (x - p[1]) / p[3], qy = (y - p[2]) / p[4], k0 = hypot(qx, qy), k1 = hypot(qx / p[3], qy / p[4]); + e = abs((k0 * (k0 - 1)) / max(k1, 1e-6)) - HW; + } + if (e < d) d = e; + } + return d; +} + +// Each letter's contour, row by row, measured once: where the ink starts +// and ends in the row's middle, and from those its outermost points. +function measure(ch) { + const parts = GLYPHS[ch], lf = new Float32Array(CAP), rt = new Float32Array(CAP); + for (let r = 0; r < CAP; r++) { + let a = 99, b = -99; + for (let x = -3; x <= 17; x += 0.02) if (dist(parts, x, r + 0.5) <= 0) { if (a === 99) a = x; b = x; } + lf[r] = a; + rt[r] = b; + } + let L = 99, R = -99; + for (let r = 0; r < CAP; r++) (L = min(L, lf[r])), (R = max(R, rt[r])); + const ld = new Float32Array(CAP), rd = new Float32Array(CAP); + for (let r = 0; r < CAP; r++) (ld[r] = lf[r] - L), (rd[r] = R - rt[r]); + return { parts, L, w: R - L, ld, rd }; +} + +// The white counted between a left letter and a right letter whose +// outermost points stand g apart, and the g that gives a wanted area. +function area(A, B, g, D) { + let s = 0; + for (let r = 0; r < CAP; r++) s += max(0, g + min(A.rd[r], D) + min(B.ld[r], D)); + return s; +} +function solve(A, B, want, D) { + let lo = -12; + for (let r = 0; r < CAP; r++) lo = max(lo, CLEAR - A.rd[r] - B.ld[r]); + if (area(A, B, lo, D) >= want) return lo; + let hi = 30; + for (let i = 0; i < 28; i++) { + const m = (lo + hi) / 2; + if (area(A, B, m, D) < want) lo = m; + else hi = m; + } + return (lo + hi) / 2; +} + +function setWord(s, k, word) { + const ln = s.lines[k]; + ln.word = word; + ln.g = []; + ln.letters = [...word].map((ch) => s.m[ch]); + for (let i = 0; i + 1 < ln.letters.length; i++) ln.g.push(BOX); +} + +export function setup(params) { + const s = (params.s = {}); + s.m = {}; + for (const ch in GLYPHS) s.m[ch] = measure(ch); + s.lines = []; + for (let k = 0; k < LINES; k++) { + s.lines.push({ tau: 0.35 - 0.45 * k, n: 0 }); + setWord(s, k, WORDS[k]); + } + s.glow = new Float32Array(W * H * 3); + s.tmp = new Float32Array(W * H * 3); + s.ink = new Float32Array(W * H); + s.D = 3.18; + s.want = 3.6 * CAP; + s.hue = 0; +} + +// Phases of a line, in seconds at Speed 2. +const T_IN = 0.35, T_SLIDE = 1.5, T_HOLD = 3.0, T_OUT = 6.0, T_END = 6.35; + +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; + const rate = cl(k[1] == null ? 2 : k[1], 0.2, 3) / 2; + const tr = cl(k[2] || 0, -1, 1), dp = cl(k[3] == null ? 0.06 : k[3], -1, 1); + s.want = (3.6 + (tr < 0 ? 1.2 : 1.8) * tr) * CAP; + s.D = 3 * exp(dp * (dp < 0 ? 1.0986 : 0.9808)); + const follow = 1 - exp(-8 * dt); + for (let li = 0; li < LINES; li++) { + const ln = s.lines[li]; + ln.tau += dt * rate; + if (ln.tau >= T_END) { + ln.tau -= T_END; + ln.n++; + setWord(s, li, WORDS[(li + LINES * ln.n) % WORDS.length]); + } + const e = ease((ln.tau - T_SLIDE) / (T_HOLD - T_SLIDE)); + for (let i = 0; i < ln.g.length; i++) { + const target = BOX + (solve(ln.letters[i], ln.letters[i + 1], s.want, s.D) - BOX) * e; + ln.g[i] += (target - ln.g[i]) * (ln.tau < T_HOLD ? 1 : follow); + } + } +} + +export function draw(display, params) { + const s = params.s; + if (!s) return; + const glow = s.glow, ink = s.ink, D = s.D; + glow.fill(0); + ink.fill(0); + const marks = []; + for (let li = 0; li < LINES; li++) { + const ln = s.lines[li], top = TOP0 + li * PITCH, L = ln.letters; + const fade = ln.tau < 0 ? 1 : ln.tau < T_IN ? ln.tau / T_IN : ln.tau > T_OUT ? 1 - (ln.tau - T_OUT) / (T_END - T_OUT) : 1; + let total = 0; + for (let i = 0; i < L.length; i++) total += L[i].w + (i < ln.g.length ? ln.g[i] : 0); + let x = 32 - total / 2; + const xs = []; + for (let i = 0; i < L.length; i++) { + xs.push(x); + x += L[i].w + (i < ln.g.length ? ln.g[i] : 0); + } + // Letters. + for (let i = 0; i < L.length; i++) { + const g = L[i], ox = xs[i] - g.L; + const x0 = max(0, floor(xs[i] - 1)), x1 = min(W - 1, floor(xs[i] + g.w + 1)); + for (let y = 0; y < CAP; y++) + for (let px = x0; px <= x1; px++) { + const c = cl(0.5 - dist(g.parts, px + 0.5 - ox, y + 0.5), 0, 1) * fade; + const j = (top + y) * W + px; + if (c > ink[j]) ink[j] = c; + } + } + // The counted white between each pair, and its colour. + for (let i = 0; i < ln.g.length; i++) { + const A = L[i], B = L[i + 1], a = xs[i] + A.w, b = xs[i + 1]; + const ar = area(A, B, b - a, D), mean = ar / CAP; + const sv = cl((mean - 1.5) / 6, 0, 1); + const col = hsv(0.7 + s.hue - 0.58 * sv, 0.9, 0.62 + 0.3 * sv); + const lv = fade; + for (let r = 0; r < CAP; r++) { + const u0 = a - min(A.rd[r], D), u1 = b + min(B.ld[r], D); + if (u1 <= u0) continue; + for (let px = max(0, floor(u0)); px <= min(W - 1, floor(u1)); px++) { + const c = (min(px + 1, u1) - max(px, u0)) * lv; + if (c <= 0) continue; + const j = ((top + r) * W + px) * 3; + glow[j] += col[0] * c; + glow[j + 1] += col[1] * c; + glow[j + 2] += col[2] * c; + } + } + // Under each gap a bar as long as the gap's mean width: its area + // laid out along one row. + let cx = 0, n = 0; + for (let r = 0; r < CAP; r++) { + const u0 = a - min(A.rd[r], D), u1 = b + min(B.ld[r], D); + if (u1 > u0) (cx += (u0 + u1) / 2), n++; + } + marks.push([n ? cx / n : (a + b) / 2, top + CAP + 1, mean, col, fade]); + } + } + // Soften the glow: a 1-2-1 blur across and down. + const t = s.tmp; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) + for (let c = 0; c < 3; c++) { + const j = (y * W + x) * 3 + c; + t[j] = (2 * glow[j] + (x > 0 ? glow[j - 3] : 0) + (x < W - 1 ? glow[j + 3] : 0)) / 4; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) + for (let c = 0; c < 3; c++) { + const j = (y * W + x) * 3 + c; + glow[j] = (2 * t[j] + (y > 0 ? t[j - W * 3] : 0) + (y < H - 1 ? t[j + W * 3] : 0)) / 4; + } + for (const [cx, y, len, col, f] of marks) { + const u0 = cx - len / 2, u1 = cx + len / 2; + for (let px = max(0, floor(u0)); px <= min(W - 1, floor(u1)); px++) { + const c = cl(min(px + 1, u1) - max(px, u0), 0, 1) * 0.9 * f; + const j = (y * W + px) * 3; + for (let q = 0; q < 3; q++) glow[j + q] = max(glow[j + q], col[q] * c); + } + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x, j = i * 3, a = ink[i]; + const r = glow[j] * 255 * (1 - a) + 242 * a, g = glow[j + 1] * 255 * (1 - a) + 234 * a, b = glow[j + 2] * 255 * (1 - a) + 218 * a; + display.setPixel(x, y, round(cl(r + 3, 0, 255)), round(cl(g + 3, 0, 255)), round(cl(b + 6, 0, 255))); + } +} From 907147c2cb7c17cd09e467e6cc4067fd1b532960 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 20:26:49 +0000 Subject: [PATCH 03/14] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2072=ED=8C=90:=20Schlieren=20Texture?= 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_01Bc4JkrPaZ8GQQiA22PWEvR --- .../patterns/schlieren-texture.js | 272 ++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 incoming/pattern-study/patterns/schlieren-texture.js diff --git a/incoming/pattern-study/patterns/schlieren-texture.js b/incoming/pattern-study/patterns/schlieren-texture.js new file mode 100644 index 000000000..e83a1cd47 --- /dev/null +++ b/incoming/pattern-study/patterns/schlieren-texture.js @@ -0,0 +1,272 @@ +// ===== Patternflow pattern ===== +// Title: Schlieren Texture +// Family: physics +// Idea: A nematic liquid crystal film between crossed polarizers, its director a line field relaxing by Ginzburg-Landau dynamics, so that disclinations of strength +1/2 and -1/2 drift together and annihilate while new pairs are let in, each half-strength core sitting on two dark brushes and each whole-strength core on four, the brushes swinging round the cores as the polarizers turn, over Michel-Levy interference colours that band in the second order down the wedge of the film and step down through the first order into every melted core. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: disinclination lines and the elastic theory of nematic liquid crystals (F. C. Frank, "On the theory of liquid crystals", 1958), Ginzburg-Landau relaxation of a complex order parameter (Vitaly Ginzburg and Lev Landau, 1950), interference colour chart (Auguste Michel-Levy, 1888), pattern round 72 +// Round: 72 +// =============================== +// @matrix 64x128 +// @knobs Order=-1..1, Speed=0.2..3, Defects=10..30, Uncross=0..1 +// +// Knob 1: Order. The film's retardation at its mean thickness, 850 + 650 Order nm. At the opening 0 it is 850 nm, and the wedge spreads it from about 620 nm at the top of the panel to 1080 nm at the bottom, the whole second order in bands: blue, cyan, green, yellow, orange, red and magenta. At -1 it is 200 nm, first-order grey and white; at -0.5, 525 nm, first-order orange and the violet of the sensitive tint; at 1, 1500 nm, the greens and pinks of the third order. +// Knob 2: Speed. The polarizers turn at 0.03 Speed radians a second, 0.06 at the opening 2, and the brushes of a half-strength core turn twice as fast, a +1/2 core's with the polarizers and a -1/2 core's against them; crossed polarizers look the same after a quarter turn, 26 s at Speed 2, 17 s at 3. The film relaxes 10 Speed time units a second: at Speed 2 a lone pair 20 LEDs apart meets in 2.4 s and one 30 LEDs apart in 10 s. +// Knob 3: Defects. The film opens with this many cores and keeps at least this many: when annihilation leaves fewer, a new pair is let in 26 to 34 LEDs apart, at the best of 14 spots away from the cores already there, at most one every 0.8 s, and the panel dissolves to it over 0.7 s. One pair in five is a +1 and a -1 with four brushes each, which come apart into half-strength cores within a few seconds. 10 at the opening, 30 at the top. +// Knob 4: Uncross. Turns the analyzer away from crossed by 90 Uncross degrees: at 0 the polarizers are crossed and the brushes black, at the opening 0.06 they are 5.4 degrees off, at 0.5 the colours have paled and the brushes gone, and at 1 the polarizers are parallel, the brushes white and the colours turned to their pale complements. + +// The director n is a line field: n and -n are the same, so its angle theta +// counts only modulo pi, and a loop round a core may turn it by half a turn, +// a disclination of strength 1/2. The film is held as the complex order +// parameter psi = S e^(2 i theta), whose phase counts modulo 2 pi: a +// half-strength core is a single winding of psi, a whole-strength core a +// double one, which splits because it costs twice the energy of two singles. +// psi relaxes by d psi / dt = lap psi + a psi (1 - |psi|^2) with a = 0.15: +// the order S is below 0.5 over 4 LEDs across a core and below 0.9 over 9. +// Opposite cores attract and annihilate as the texture coarsens. + +const W = 64, H = 128, N = W * H; +const { floor, round, min, max, abs, sqrt, sin, cos, atan2, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const TAU = 2 * PI; +const FADE = 0.7, GAIN = 1.4, AGL = 0.15, XI = 1 / sqrt(AGL); + +// ---- Michel-Levy colours: white light through a plate of retardation G +// between crossed polarizers transmits sin^2(pi G / lambda) of each +// wavelength. Summed against the CIE 1931 matching functions (the +// three-lobe fit of Wyman, Sloan and Shirley) from 400 to 700 nm and taken +// to sRGB, each channel divided by white's, then saturated SAT times over +// for the panel, kept in linear light. One entry each 2 nm up to 2400 nm. +// (Matching functions: Chris Wyman, Peter-Pike Sloan and Peter Shirley, +// 2013.) ---- +const SAT = 3, GMAX = 2400, GSTEP = 2, GN = GMAX / GSTEP + 1, ML = new Float32Array(GN * 3); +{ + const g = (l, m, a, b) => exp(-0.5 * ((l - m) / (l < m ? a : b)) ** 2); + const L = [], C = []; + for (let l = 400; l <= 700; l += 10) { + L.push(l); + const x = 1.056 * g(l, 599.8, 37.9, 31) + 0.362 * g(l, 442, 16, 26.7) - 0.065 * g(l, 501.1, 20.4, 26.2); + const y = 0.821 * g(l, 568.8, 46.9, 40.5) + 0.286 * g(l, 530.9, 16.3, 31.1); + const z = 1.217 * g(l, 437, 11.8, 36) + 0.681 * g(l, 459, 26, 13.8); + C.push([3.2406 * x - 1.5372 * y - 0.4986 * z, -0.9689 * x + 1.8758 * y + 0.0415 * z, 0.0557 * x - 0.204 * y + 1.057 * z]); + } + const wh = [0, 0, 0]; + for (const c of C) for (let k = 0; k < 3; k++) wh[k] += c[k]; + const enc = (v) => (v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055); + const dec = (v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4); + for (let i = 0; i < GN; i++) { + const G = i * GSTEP, c = [0, 0, 0]; + for (let j = 0; j < L.length; j++) { + const s = sin((PI * G) / L[j]) ** 2; + for (let k = 0; k < 3; k++) c[k] += C[j][k] * s; + } + for (let k = 0; k < 3; k++) c[k] = enc(cl(c[k] / wh[k], 0, 1)); + // Saturation (max - min) / max times SAT, hue and the brightest channel kept. + const mx = max(c[0], c[1], c[2]), mn = min(c[0], c[1], c[2]), sa = mx > 0 ? (mx - mn) / mx : 0, k2 = sa > 0 ? min(1, sa * SAT) / sa : 1; + for (let k = 0; k < 3; k++) ML[i * 3 + k] = dec(mx - (mx - c[k]) * k2); + } +} +// Back to sRGB for the panel, 1024 steps of linear light. +const ENC = new Uint8Array(1025); +for (let i = 0; i <= 1024; i++) { + const v = i / 1024; + ENC[i] = round(255 * (v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055)); +} + +// The film's thickness, 1 at its mean: a wedge from 0.725 at the top to +// 1.275 at the bottom and two shallow waves of 0.05 and 0.04 across it, so +// the colours lie in wavy bands. +const D = new Float32Array(N); +for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) + D[p] = 1 + 0.55 * ((y - 63.5) / 127) + 0.05 * sin(0.11 * x + 0.035 * y + 1) + 0.04 * sin(0.05 * y - 0.07 * x + 2); + +// A core of winding w (+-1 or +-2) at (cx, cy), multiplied into psi: its +// phase, and its order melted to the profile of a relaxed core. +function core(S, cx, cy, w) { + const { U, V } = S, n = abs(w); + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const dx = x - cx, dy = y - cy, r2 = dx * dx + dy * dy; + const r = sqrt(r2) || 1e-6; + let c = dx / r, s = (w < 0 ? -dy : dy) / r; + if (n === 2) (s = 2 * c * s), (c = 2 * c * c - 1); + // GL core profile, about r / sqrt(r^2 + 2 xi^2), n times over. + const f = (r / sqrt(r2 + 2 * XI * XI)) ** n; + const u = U[p], v = V[p]; + U[p] = (u * c - v * s) * f; + V[p] = (u * s + v * c) * f; + } +} + +// Where the cores are: winding of psi round each 2 by 2 square. +function census(S) { + const { U, V, A } = S; + for (let p = 0; p < N; p++) A[p] = atan2(V[p], U[p]); + const wrap = (d) => (d > PI ? d - TAU : d < -PI ? d + TAU : d); + const list = []; + let count = 0; + for (let y = 0; y < H - 1; y++) + for (let x = 0; x < W - 1; x++) { + const p = y * W + x, a = A[p], b = A[p + 1], c = A[p + W + 1], d = A[p + W]; + const w = round((wrap(b - a) + wrap(c - b) + wrap(d - c) + wrap(a - d)) / TAU); + if (w) { + list.push(x + 0.5, y + 0.5); + count += abs(w); + } + } + S.cores = list; + S.count = count; +} + +// A spot for a pair: of 14 tries, the one furthest from the cores there. +function spot(S, sep) { + let best = null, bd = -1; + for (let t = 0; t < 14; t++) { + const an = Math.random() * PI, hx = 0.5 * sep * cos(an), hy = 0.5 * sep * sin(an); + const cx = 8 + abs(hx) + Math.random() * (48 - 2 * abs(hx)), cy = 8 + abs(hy) + Math.random() * (112 - 2 * abs(hy)); + let d = 1e9; + for (let i = 0; i < S.cores.length; i += 2) + for (const e of [-1, 1]) d = min(d, (S.cores[i] - cx - e * hx) ** 2 + (S.cores[i + 1] - cy - e * hy) ** 2); + if (d > bd) (bd = d), (best = [cx - hx, cy - hy, cx + hx, cy + hy]); + } + return best; +} + +function pair(S) { + const w = Math.random() < 0.2 ? 2 : 1, q = spot(S, 26 + Math.random() * 8); + core(S, q[0], q[1], w); + core(S, q[2], q[3], -w); +} + +// Relax psi by time t in Euler steps of at most 0.2 (stable below 0.25), +// walls with no flux through them. +function relax(S, t) { + const n = Math.ceil(t / 0.2 - 1e-9); + if (n <= 0) return; + const h = t / n; + let { U, V, U2, V2 } = S; + for (let k = 0; k < n; k++) { + for (let y = 0, p = 0; y < H; y++) { + const up = y > 0 ? -W : 0, dn = y < H - 1 ? W : 0; + for (let x = 0; x < W; x++, p++) { + const lf = x > 0 ? -1 : 0, rt = x < W - 1 ? 1 : 0, u = U[p], v = V[p]; + const lu = U[p + lf] + U[p + rt] + U[p + up] + U[p + dn] - 4 * u; + const lv = V[p + lf] + V[p + rt] + V[p + up] + V[p + dn] - 4 * v; + const g = AGL * (1 - u * u - v * v); + U2[p] = u + h * (lu + g * u); + V2[p] = v + h * (lv + g * v); + } + } + const tu = U, tv = V; + (U = U2), (V = V2), (U2 = tu), (V2 = tv); + } + S.U = U; + S.V = V; + S.U2 = U2; + S.V2 = V2; +} + +// The opening film: n cores of alternating sign, no two closer than the +// spacing allows, on a gently twisted background, relaxed 8 time units. +function seed(S, n) { + const { U, V } = S; + const k1 = Math.random() * TAU, k2 = Math.random() * TAU; + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const ph = 0.15 * sin(0.045 * x + 0.03 * y + k1) + 0.1 * sin(0.02 * x - 0.04 * y + k2); + U[p] = cos(ph); + V[p] = sin(ph); + } + const pts = []; + let dmin = 0.75 * sqrt(N / n), tries = 0; + while (pts.length < 2 * n) { + const x = 6 + Math.random() * 52, y = 6 + Math.random() * 116; + if (++tries > 400) (dmin *= 0.9), (tries = 0); + let ok = true; + for (let i = 0; i < pts.length && ok; i += 2) ok = (pts[i] - x) ** 2 + (pts[i + 1] - y) ** 2 >= dmin * dmin; + if (ok) pts.push(x, y); + } + for (let i = 0; i < n; i++) core(S, pts[2 * i], pts[2 * i + 1], i & 1 ? -1 : 1); + relax(S, 8); + census(S); +} + +export function setup(params) { + params.s = { + U: new Float32Array(N), V: new Float32Array(N), U2: new Float32Array(N), V2: new Float32Array(N), A: new Float32Array(N), + LAST: new Uint8Array(N * 3), OLD: new Uint8Array(N * 3), fade: 9, + order: 0, speed: 2, want: 10, uncross: 0.06, phi: 0.3, t: 0, frame: 0, lastPair: -9, cores: [], count: 0, seeded: false, + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 10, 0.06], d = cl(dt || 0, 0, 0.1); + S.order = cl(k[0], -1, 1); + S.speed = cl(k[1], 0.2, 3); + S.want = round(cl(k[2], 10, 30)); + S.uncross = cl(k[3], 0, 1); + if (!S.seeded) { + seed(S, S.want + (S.want & 1)); + S.seeded = true; + } + S.t += d; + S.fade += d; + S.phi += d * 0.03 * S.speed; + relax(S, d * 10 * S.speed); + if (++S.frame % 6 === 0) census(S); + if (S.count < S.want && S.t - S.lastPair > 0.8) { + // The panel dissolves from the last frame to the film with the new + // pair over FADE seconds. + pair(S); + census(S); + S.lastPair = S.t; + S.OLD.set(S.LAST); + S.fade = 0; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.seeded) { + for (let p = 0; p < N; p++) display.setPixel(p & 63, p >> 6, 0, 0, 0); + return; + } + const { U, V, OLD, LAST } = S, G0 = 850 + 650 * S.order, w0 = min(1, S.fade / FADE), w = w0 * w0 * (3 - 2 * w0); + // Polarizer at phi, analyzer at phi + 90 (1 - Uncross) degrees. A plate + // with its axis at theta passes cos^2(A - P) - sin 2(theta - P) + // sin 2(theta - A) times sin^2(pi G / lambda) of the light: with the + // polarizers crossed, sin^2 2(theta - P) of the colour, black where the + // director lies along either polarizer. Mixed in linear light, then + // encoded for the panel. + const P = S.phi, A = P + (PI / 2) * (1 - S.uncross); + const c2p = cos(2 * P), s2p = sin(2 * P), c2a = cos(2 * A), s2a = sin(2 * A), base = cos(A - P) ** 2; + for (let p = 0; p < N; p++) { + const u = U[p], v = V[p], r = sqrt(u * u + v * v), Sq = min(1, r), iv = r > 1e-9 ? 1 / r : 0; + const c = u * iv, s = v * iv; // cos and sin of 2 theta + const kP = s * c2p - c * s2p, kA = s * c2a - c * s2a; + // Retardation d S: toward a core the colours step down through the + // orders to first-order grey, and the light fades as S^3 into it. + const G = G0 * D[p] * Sq; + const gi = min(GN - 2, G / GSTEP), i0 = floor(gi), f = gi - i0, o = i0 * 3; + const m = Sq * Sq * Sq * 1024 * GAIN, t = -kP * kA; + const R = (base + t * (ML[o] + f * (ML[o + 3] - ML[o]))) * m; + const Gc = (base + t * (ML[o + 1] + f * (ML[o + 4] - ML[o + 1]))) * m; + const B = (base + t * (ML[o + 2] + f * (ML[o + 5] - ML[o + 2]))) * m; + // Exposed GAIN times over: a channel past full scales the three down + // together, so the hue holds. + const q = max(1024, R, Gc, B) / 1024, o3 = p * 3; + let r8 = ENC[cl((R / q) | 0, 0, 1024)], g8 = ENC[cl((Gc / q) | 0, 0, 1024)], b8 = ENC[cl((B / q) | 0, 0, 1024)]; + if (w < 1) { + r8 = round(OLD[o3] + (r8 - OLD[o3]) * w); + g8 = round(OLD[o3 + 1] + (g8 - OLD[o3 + 1]) * w); + b8 = round(OLD[o3 + 2] + (b8 - OLD[o3 + 2]) * w); + } + LAST[o3] = r8; + LAST[o3 + 1] = g8; + LAST[o3 + 2] = b8; + display.setPixel(p & 63, p >> 6, r8, g8, b8); + } +} From 93f89ef2fbc8453f5d804fa70b62f47baf63e7f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 20:28:18 +0000 Subject: [PATCH 04/14] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2072=ED=8C=90:=20Optical=20Kerning=20=EA=B3=BC=20Spray=20Stenc?= =?UTF-8?q?il?= 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_01Bc4JkrPaZ8GQQiA22PWEvR --- .../pattern-study/patterns/optical-kerning.js | 11 +- .../pattern-study/patterns/spray-stencil.js | 310 ++++++++++++++++++ 2 files changed, 316 insertions(+), 5 deletions(-) create mode 100644 incoming/pattern-study/patterns/spray-stencil.js diff --git a/incoming/pattern-study/patterns/optical-kerning.js b/incoming/pattern-study/patterns/optical-kerning.js index a319eb953..fae38759b 100644 --- a/incoming/pattern-study/patterns/optical-kerning.js +++ b/incoming/pattern-study/patterns/optical-kerning.js @@ -1,11 +1,11 @@ // ===== Patternflow pattern ===== // Title: Optical Kerning // Family: type -// Idea: Five words whose letters have open and slanted sides, AVOW, LYNX, TOY, WAVE and FLY among them, stand in capitals 19 LEDs tall down the panel, the white between each pair of neighbours glows in a colour that says how much of it there is, and after each word is set with the same 2 LED space between every pair of letter boxes, so that A and V gape while H and I sit tight, its letters slide sideways until every gap holds the same area and the glows of the word turn one colour, a wave of new words running down the panel every few seconds. +// Idea: Five words whose letters have open and slanted sides, AVOW, LYNX, TOY, WAVE and FLY among them, stand in capitals 19 LEDs tall down the panel, the white between each pair of neighbours glows in a colour that says how much of it there is, and after each word is set with the same 2 LED space between every pair of letter boxes, so that A and V gape while H and I sit tight, its letters slide sideways until every gap holds the same area and the glows of the word turn one colour, a wave of new words running down the panel every 6.35 s. // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: original, pattern round 72, after spacing letters by a formula rather than by eye as in David Kindersley's Optical Letter Spacing for New Printing Systems (1976 edition) and the spacing method in Walter Tracy's Letters of Credit (1986) +// Lineage: original, pattern round 72, after letter spacing worked out by formula in David Kindersley's Optical Letter Spacing for New Printing Systems (1976 edition) and by method in Walter Tracy's Letters of Credit (1986) // Round: 72 // =============================== // @matrix 64x128 @@ -14,7 +14,7 @@ // Knob 1: Hue. Turns the glow palette round the wheel. At 0 a gap 1.5 LEDs or less across on average glows violet-blue, 3.6 cyan, 6 yellow-green and 7.5 or more amber; the letters stay warm white. // Knob 2: Speed. At 2 a word is set, shown for 1.15 s at box spacing, slides into its spacing in 1.5 s, holds 3 s and goes, 6.35 s in all, and each line starts 0.45 s after the one above; at 0.2 a word takes 64 s, at 3 it takes 4.2 s. // Knob 3: Tracking. The area every gap is spaced to, given as its width averaged over the 19 rows: 3.6 LEDs at 0, where every gap of all ten words meets it, H and I stand 3.6 apart and A tucks 0.3 under V; 2.4 at -1, where seven pairs, Y N, F L and T O among them, reach the 1 LED that letters may come to and stay a little wider than the rest; 5.4 at 1, AVOW then 57 LEDs wide. The settled glows are blue at -1, cyan at 0 and green at 1. -// Knob 4: Depth. How far into an open side the white is counted, measured in from the letter's outermost point: 3.2 LEDs at the opening 0.06. At -1 only 1 LED counts, every pair is spaced almost by its boxes and A V stays 1.9 apart, as wide as N X. At 1 the 8 LEDs counted reach right into the throats of A, V, L and T: A goes 0.6 under V and the L's foot 3.4 under the Y's arm, and still L Y, L T and F L cannot shed enough white without touching, so their glows stay yellow-green while the rest settle cyan. +// Knob 4: Depth. How far into an open side the white is counted, measured in from the letter's outermost point: 3.2 LEDs at the opening 0.06. At -1 only 1 LED counts, every pair is spaced almost by its boxes and A V stands 1.9 apart instead of tucking under. At 1 the 8 LEDs counted reach right into the throats of A, V, L and T: A goes 0.6 under V and the L's foot 3.4 under the Y's arm, and still L Y, L T and F L cannot shed enough white without touching, so their glows stay yellow-green while the rest settle cyan. // The white between two letters is counted row by row through the cap // height: the horizontal distance between their facing contours, except @@ -28,7 +28,7 @@ // way: the tips touch before the throats close. The words leave them out. const W = 64, H = 128, CAP = 19, HW = 1.25, LINES = 5, PITCH = 25, TOP0 = 4, BOX = 2, CLEAR = 1; -const { floor, round, min, max, abs, sqrt, exp, hypot } = Math; +const { floor, round, min, max, abs, exp, 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) { @@ -180,7 +180,8 @@ export function draw(display, params) { const marks = []; for (let li = 0; li < LINES; li++) { const ln = s.lines[li], top = TOP0 + li * PITCH, L = ln.letters; - const fade = ln.tau < 0 ? 1 : ln.tau < T_IN ? ln.tau / T_IN : ln.tau > T_OUT ? 1 - (ln.tau - T_OUT) / (T_END - T_OUT) : 1; + // The first words are on the panel from the start; later ones fade in. + const fade = ln.n === 0 && ln.tau < T_IN ? 1 : ln.tau < T_IN ? ln.tau / T_IN : ln.tau > T_OUT ? 1 - (ln.tau - T_OUT) / (T_END - T_OUT) : 1; let total = 0; for (let i = 0; i < L.length; i++) total += L[i].w + (i < ln.g.length ? ln.g[i] : 0); let x = 32 - total / 2; diff --git a/incoming/pattern-study/patterns/spray-stencil.js b/incoming/pattern-study/patterns/spray-stencil.js new file mode 100644 index 000000000..f9090105d --- /dev/null +++ b/incoming/pattern-study/patterns/spray-stencil.js @@ -0,0 +1,310 @@ +// ===== Patternflow pattern ===== +// Title: Spray Stencil +// Family: type +// Idea: A grey metal stencil plate cut with the four syllables of 취급주의 (handle with care) one above the other lies on the side of a kraft cardboard crate, a spray nozzle sweeps down it in a zigzag and builds the paint out of soft particles, and when the plate slides off the letters stand on the crate with their bridges left as gaps, the counters of ㅇ, ㅁ and ㅂ held on by them; then the crate moves on and a fresh one comes under the next plate, 화기엄금 or 파손주의. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 72, after the stencilled handling marks on shipping crates, the syllables drawn for this pattern as a stencil face +// Round: 72 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Plate=0..3, Overspray=0..1 +// +// Knob 1: Hue. The paint: azure at 0, then violet, magenta, red at 0.4, orange, yellow and green round the wheel. +// Knob 2: Speed. At 2 a crate takes 8.4 s: the plate slides on in 0.6 s, the nozzle sweeps it in 4 s, the plate slides off in 0.8 s, the letters hold 2.2 s and the crate moves on in 0.8 s. At 0.2 a crate takes 84 s, at 3 it takes 5.6 s. +// Knob 3: Plate. 0 the three plates take turns, 취급주의, 화기엄금 (no open flame), 파손주의 (fragile); 1, 2 and 3 keep one of them. A new choice brings a fresh crate and plate at once. +// Knob 4: Overspray. The nozzle's distance from the plate. At the opening 0.06 the droplets land with a standard deviation of 4 LEDs across the sweep and 3.2 down it, one in ten of them a finer mist spread 1.7 times wider, and the letters print crisp. At 1 the spread is 9.6 LEDs, nearly two in five are mist spread 3.2 times wider and dusting cuts the nozzle has not reached, and paint creeps under the plate's edges: of what lands on the plate 1 LED from a cut, 27 percent reaches the crate beneath, 3 LEDs out 13 percent, so the letters come up soft and haloed. + +const W = 64, H = 128, N = W * H; +const { floor, round, min, max, abs, sqrt, exp, log, cos, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ease = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- The stencil face. Each syllable is drawn in a box 52 LEDs wide and +// 28 tall from cut-outs: [0, x0, y0, x1, y1] a rectangle, [1, xa, ya, xb, yb, +// w] a slanted stroke w LEDs thick with flat ends, [2, cx, cy, rx, ry, t] an +// elliptical ring t thick; and [3, x0, y0, x1, y1] a bridge, plate left +// standing across a cut. Horizontal strokes are 3 to 4 LEDs, upright ones 5. +// Every ring is bridged top and bottom, and the corners of ㄱ, ㄴ, ㅁ, ㅂ and +// ㅍ, the peaks of ㅅ, ㅈ and ㅊ and the arms of ㅏ and ㅓ are cut apart by +// 2 LEDs of plate, so no part of the plate is ringed by a cut and the inside +// of ㅇ, ㅎ, ㅁ, ㅂ and ㅍ stays held to the rest: filled from its corner, LED +// to LED across edges, each plate reaches all of its 5855 to 6056 LEDs. The +// joins of ㅜ and ㅗ ring nothing and stay whole. +const SYL = { + 취: [[0, 17, 0, 25, 3], [0, 5, 3, 37, 6.5], [1, 21, 8.5, 7, 16.5, 4.2], [1, 21, 8.5, 35, 16.5, 4.2], [3, 20, 8, 22, 17], + [0, 1, 18.5, 40, 22], [0, 18, 22, 23, 28], [0, 44, 0, 49, 28]], + 급: [[0, 6, 0, 37, 3.5], [0, 39, 0, 44, 8.5], [0, 0, 10.5, 52, 13.5], + [0, 9, 15.5, 14, 28], [0, 38, 15.5, 43, 28], [0, 16, 20, 36, 23], [0, 16, 25, 36, 28]], + 주: [[0, 7, 0, 45, 4], [1, 26, 6, 10, 15.5, 4.5], [1, 26, 6, 42, 15.5, 4.5], [3, 25, 5, 27, 16], + [0, 0, 17.5, 52, 21], [0, 23.5, 21, 28.5, 28]], + 의: [[2, 20, 9, 14, 8.5, 4.5], [3, 19, 0, 21, 5.5], [3, 19, 12.5, 21, 18], [0, 1, 21, 40, 25], [0, 44, 0, 49, 28]], + 화: [[0, 14, 0, 22, 2.5], [0, 5, 2.5, 31, 5.5], [2, 18, 11.5, 10, 4.8, 3.3], [3, 17, 6, 19, 10.5], [3, 17, 12.5, 19, 17], + [0, 15.5, 18, 20.5, 22.5], [0, 0, 22.5, 37, 26], [0, 41, 0, 46, 28], [0, 48, 11.5, 52, 15.5]], + 기: [[0, 4, 3, 25, 7], [0, 27, 3, 32, 24], [0, 41, 0, 46, 28]], + 엄: [[2, 15, 7.5, 12, 7.5, 4.5], [3, 14, 0, 16, 5], [3, 14, 10, 16, 15], [0, 29, 5.5, 35, 9.5], [0, 37, 0, 42, 15.5], + [0, 8, 17.5, 13, 28], [0, 37, 17.5, 42, 28], [0, 15, 17.5, 35, 21], [0, 15, 24.5, 35, 28]], + 금: [[0, 6, 0, 37, 3.5], [0, 39, 0, 44, 8.5], [0, 0, 10.5, 52, 13.5], + [0, 9, 15.5, 14, 28], [0, 38, 15.5, 43, 28], [0, 16, 15.5, 36, 19], [0, 16, 24.5, 36, 28]], + 파: [[0, 2, 1, 33, 5], [0, 2, 23, 33, 27], [1, 11, 7, 9, 21, 4.2], [1, 22, 7, 24, 21, 4.2], [0, 38, 0, 43, 28], [0, 45, 11.5, 51, 15.5]], + 손: [[1, 26, 0, 11, 9.5, 4.2], [1, 26, 0, 41, 9.5, 4.2], [3, 25, 0, 27, 10], [0, 23.5, 9.5, 28.5, 14], [0, 0, 13.5, 52, 17], + [0, 8, 19, 13, 28], [0, 15, 24.5, 46, 28]], +}; +const PLATES = [["취", "급", "주", "의"], ["화", "기", "엄", "금"], ["파", "손", "주", "의"]]; +const SX = 6, SY = [3, 34, 65, 96]; + +function inPrim(p, x, y) { + if (p[0] === 0 || p[0] === 3) return x >= p[1] && x < p[3] && y >= p[2] && y < p[4]; + if (p[0] === 1) { + if (y < p[2] || y >= p[4]) return false; + const dx = p[3] - p[1], dy = p[4] - p[2], c = p[1] + (dx * (y - p[2])) / dy; + return abs(x - c) < (p[5] / 2) * (sqrt(dx * dx + dy * dy) / dy); + } + const qx = (x - p[1]) / p[3], qy = (y - p[2]) / p[4], qi = (x - p[1]) / (p[3] - p[5]), qj = (y - p[2]) / (p[4] - p[5]); + return qx * qx + qy * qy < 1 && qi * qi + qj * qj >= 1; +} + +// The plate: how much of each LED is cut away (0 to 1, from 4 by 4 samples), +// and for the plate around the cuts, how far it is to the nearest cut. +function makePlate(k) { + const hole = new Float32Array(N), far = new Float32Array(N).fill(99); + PLATES[k].forEach((ch, r) => { + const prims = SYL[ch], cuts = prims.filter((p) => p[0] !== 3), bridges = prims.filter((p) => p[0] === 3); + for (let y = 0; y < 28; y++) + for (let x = -1; x < 53; x++) { + let c = 0; + for (let j = 0; j < 4; j++) + for (let i = 0; i < 4; i++) { + const u = x + (i + 0.5) / 4, v = y + (j + 0.5) / 4; + if (cuts.some((p) => inPrim(p, u, v)) && !bridges.some((p) => inPrim(p, u, v))) c++; + } + const px = x + SX, py = y + SY[r]; + if (c && px >= 0 && px < W) hole[py * W + px] = c / 16; + } + }); + for (let i = 0; i < N; i++) if (hole[i] >= 0.5) far[i] = 0; + // Two chamfer passes give the distance to the nearest cut. + const step = (i, j, d) => { if (far[j] + d < far[i]) far[i] = far[j] + d; }; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x; + if (x > 0) step(i, i - 1, 1); + if (y > 0) step(i, i - W, 1), x > 0 && step(i, i - W - 1, 1.41), x < W - 1 && step(i, i - W + 1, 1.41); + } + for (let y = H - 1; y >= 0; y--) + for (let x = W - 1; x >= 0; x--) { + const i = y * W + x; + if (x < W - 1) step(i, i + 1, 1); + if (y < H - 1) step(i, i + W, 1), x > 0 && step(i, i + W - 1, 1.41), x < W - 1 && step(i, i + W + 1, 1.41); + } + return { hole, far, paint: new Float32Array(N) }; +} + +// ---- Surfaces: value noise from a hash. +const hash = (a, b, c) => { + let h = (a * 374761393 + b * 668265263 + c * 1013904223) | 0; + h = Math.imul(h ^ (h >>> 13), 1274126177); + h = Math.imul(h ^ (h >>> 16), 2246822519); + return ((h ^ (h >>> 13)) >>> 0) / 4294967296; +}; +function vn(x, y, k) { + const x0 = floor(x), y0 = floor(y), fx = x - x0, fy = y - y0; + const sx = fx * fx * (3 - 2 * fx), sy = fy * fy * (3 - 2 * fy); + const a = hash(x0, y0, k), b = hash(x0 + 1, y0, k), c = hash(x0, y0 + 1, k), d = hash(x0 + 1, y0 + 1, k); + return a + (b - a) * sx + (c - a) * sy + (a - b - c + d) * sx * sy; +} +// Kraft board: the flutes of the corrugation run up the side 4 LEDs apart, +// with fibre speckle and a tint that differs a little from crate to crate. +function makeCrate(seed) { + const t = 0.93 + 0.14 * hash(seed, 7, 1), warm = 0.04 * (hash(seed, 9, 2) - 0.5); + const lum = new Float32Array(N); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) + lum[y * W + x] = t * (0.92 + 0.07 * vn(x / 9, y / 13, seed) + 0.05 * (hash(x, y, seed) - 0.5) + 0.035 * cos((2 * PI * x) / 4)); + return { lum, warm, paint: new Float32Array(N) }; +} + +function gauss() { + return sqrt(-2 * log(1 - Math.random())) * cos(2 * PI * Math.random()); +} + +// Phases of a crate, in seconds at Speed 2. +const T_ON = 0.6, T_SPRAY = 4.0, T_OFF = 0.8, T_HOLD = 2.2, T_MOVE = 0.8; +const P_SPRAY = T_ON, P_OFF = P_SPRAY + T_SPRAY, P_HOLD = P_OFF + T_OFF, P_MOVE = P_HOLD + T_HOLD, P_END = P_MOVE + T_MOVE; +const DROPS = 26000; // droplets in one pass of the nozzle +const PRE = 0.35; + +export function setup(params) { + const s = (params.s = {}); + s.plates = [0, 1, 2].map(makePlate); + s.seed = 1; + s.crate = makeCrate(s.seed); + s.next = makeCrate(s.seed + 1); + s.mode = -1; + s.which = 0; + // The panel opens on a plate the nozzle is already a third of the way down. + s.t = P_SPRAY + PRE * T_SPRAY; + s.nx = -9; + s.ny = -9; + s.fresh = []; + s.hue = 0; + s.ov = 0.06; + s.buf = new Float32Array(N * 3); +} + +function freshCrate(s) { + s.seed++; + s.crate = s.next; + s.next = makeCrate(s.seed + 1); +} + +function spray(s, a, b) { + // The nozzle runs 13 sweeps across while it moves steadily down, from + // 6 LEDs above the panel to 6 below, so its path is a zigzag. + const ov = s.ov, sig = 3.6 + 6 * ov, plate = s.plates[s.which], hole = plate.hole, far = plate.far; + const pos = (p) => { + const q = p * 13, k = floor(q), f = q - k; + return [k % 2 ? 72 - 80 * f : -8 + 80 * f, -6 + 140 * p]; + }; + const n = round(DROPS * (b - a)); + const ss = 0.5 + 0.6 * ov, reach = ss > 0.8 ? 2 : 1, bleed = 0.4 * ov * ov, bl = 0.4 + 2.2 * ov; + s.fresh.length = 0; + for (let i = 0; i < n; i++) { + const [px, py] = pos(a + ((b - a) * (i + Math.random())) / n); + const mist = Math.random() < 0.08 + 0.3 * ov, r = mist ? sig * (1.6 + 1.6 * ov) : sig, m = mist ? 0.5 : 1; + const x = px + gauss() * r, y = py + gauss() * r * 0.8; + if (x < -2 || x > W + 1 || y < -2 || y > H + 1) continue; + if (i % 2 === 0 && s.fresh.length < 240) s.fresh.push(x, y); + const x0 = floor(x), y0 = floor(y); + for (let yy = y0 - reach; yy <= y0 + reach; yy++) { + if (yy < 0 || yy >= H) continue; + for (let xx = x0 - reach; xx <= x0 + reach; xx++) { + if (xx < 0 || xx >= W) continue; + const dx = xx + 0.5 - x, dy = yy + 0.5 - y, w = (m * exp(-(dx * dx + dy * dy) / (2 * ss * ss))) / (2 * PI * ss * ss); + const j = yy * W + xx, c = hole[j]; + s.crate.paint[j] += w * (c + (1 - c) * bleed * exp(-far[j] / bl)); + plate.paint[j] += w * (1 - c); + } + } + } +} + +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; + const rate = cl(k[1] == null ? 2 : k[1], 0.2, 3) / 2; + s.ov = cl(k[3] == null ? 0.06 : k[3], 0, 1); + const mode = round(cl(k[2] || 0, 0, 3)); + if (mode !== s.mode) { + // A new choice: fresh crate, and the plate slides on. + const first = s.mode < 0; + s.mode = mode; + s.which = mode ? mode - 1 : 0; + if (first) spray(s, 0, PRE); + else { + freshCrate(s); + s.t = 0; + } + } + const t0 = s.t; + s.t += dt * rate; + s.fresh.length = 0; + if (s.t > P_SPRAY && t0 < P_OFF) { + const a = cl((t0 - P_SPRAY) / T_SPRAY, 0, 1), b = cl((s.t - P_SPRAY) / T_SPRAY, 0, 1); + if (b > a) spray(s, a, b); + const q = b * 13, kk = floor(q), f = q - kk; + s.nx = kk % 2 ? 72 - 80 * f : -8 + 80 * f; + s.ny = -6 + 140 * b; + } + if (s.t >= P_END) { + s.t -= P_END; + freshCrate(s); + if (!s.mode) s.which = (s.which + 1) % 3; + // The plate is wiped between crates, but not clean. + const pp = s.plates[s.which].paint; + for (let i = 0; i < N; i++) pp[i] *= 0.35; + } +} + +export function draw(display, params) { + const s = params.s; + if (!s) return; + const t = s.t, plate = s.plates[s.which], hole = plate.hole, pp = plate.paint, buf = s.buf; + const paint = hsv(0.6 + s.hue, 0.88, 1); + // Plate offset: slides on from the right, off to the right. + const off = t < P_SPRAY ? round(68 * (1 - ease(t / T_ON))) : t < P_OFF ? 0 : round(68 * ease((t - P_OFF) / T_OFF)); + const lift = t >= P_OFF && t < P_HOLD ? 1 : 0; + // Crate move: the painted crate slides down and out, the fresh one follows. + const drop = t >= P_MOVE ? round(H * ease((t - P_MOVE) / T_MOVE)) : 0; + const kraft = [176, 121, 66]; + for (let y = 0; y < H; y++) { + const sy = y - drop, old = sy >= 0, cr = old ? s.crate : s.next, row = old ? sy : sy + H; + const seam = drop && (sy === 0 || sy === -1) ? 0.45 : 1; + for (let x = 0; x < W; x++) { + const i = row * W + x, l = cr.lum[i] * seam; + let r = kraft[0] * l * (1 + cr.warm), g = kraft[1] * l, b = kraft[2] * l * (1 - cr.warm); + const a = 1 - exp(-1.4 * cr.paint[i]); + r += (paint[0] * 250 - r) * a; + g += (paint[1] * 250 - g) * a; + b += (paint[2] * 250 - b) * a; + // The plate over the crate, shifted right by off. + const px = x - off; + if (px >= 0 && px < W && !drop) { + const j = y * W + px, c = hole[j]; + // The cut's upper left edge throws a one-LED shadow into the hole. + const sh = c > 0 && ((px > 0 && hole[j - 1] < 0.5) || (y > 0 && hole[j - W] < 0.5)) ? 0.55 : 1; + const brush = 0.94 + 0.08 * hash(0, y, 5) + 0.03 * (hash(px, y, 3) - 0.5); + let mr = 100 * brush, mg = 100 * brush, mb = 98 * brush; + const pa = (1 - exp(-0.6 * pp[j])) * 0.45; + mr += (paint[0] * 150 - mr) * pa; + mg += (paint[1] * 150 - mg) * pa; + mb += (paint[2] * 150 - mb) * pa; + r = r * sh * c + mr * (1 - c); + g = g * sh * c + mg * (1 - c); + b = b * sh * c + mb * (1 - c); + } else if (lift && px >= -3 && px < 0) { + // The lifted plate's shadow just left of its edge. + const d = 0.55 + 0.15 * (3 + px); + r *= d; + g *= d; + b *= d; + } + const o = (y * W + x) * 3; + buf[o] = r; + buf[o + 1] = g; + buf[o + 2] = b; + } + } + // The nozzle: a cloud of paint in the air over the sweep, and the droplets + // landing this frame. + if (t > P_SPRAY && t < P_OFF) { + const rad = (3.6 + 6 * s.ov) * 1.3, amp = 0.26 + 0.34 * s.ov; + for (let y = max(0, floor(s.ny - 2 * rad)); y < min(H, s.ny + 2 * rad); y++) + for (let x = max(0, floor(s.nx - 2 * rad)); x < min(W, s.nx + 2 * rad); x++) { + const dx = x + 0.5 - s.nx, dy = y + 0.5 - s.ny, d2 = dx * dx + dy * dy; + const e = min(0.85, amp * exp(-d2 / (rad * rad)) + 0.8 * exp(-d2 / 1.6)); + const o = (y * W + x) * 3; + for (let q = 0; q < 3; q++) buf[o + q] += (min(255, paint[q] * 255 + 140 * e) - buf[o + q]) * e; + } + const f = s.fresh; + for (let i = 0; i < f.length; i += 2) { + const x = floor(f[i]), y = floor(f[i + 1]); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const o = (y * W + x) * 3; + for (let q = 0; q < 3; q++) buf[o + q] += (min(255, paint[q] * 255 + 110) - buf[o + q]) * 0.8; + } + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const o = (y * W + x) * 3; + display.setPixel(x, y, round(cl(buf[o], 0, 255)), round(cl(buf[o + 1], 0, 255)), round(cl(buf[o + 2], 0, 255))); + } +} From a7ead237db5e9cb302106b65d283f55e1c067b34 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 20:28:46 +0000 Subject: [PATCH 05/14] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2072=ED=8C=90:=20Clothesline=20=EA=B3=BC=20Hydra=20Game?= 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_01Bc4JkrPaZ8GQQiA22PWEvR --- .../pattern-study/patterns/clothesline.js | 269 ++++++++++ incoming/pattern-study/patterns/hydra-game.js | 458 ++++++++++++++++++ 2 files changed, 727 insertions(+) create mode 100644 incoming/pattern-study/patterns/clothesline.js create mode 100644 incoming/pattern-study/patterns/hydra-game.js diff --git a/incoming/pattern-study/patterns/clothesline.js b/incoming/pattern-study/patterns/clothesline.js new file mode 100644 index 000000000..9b248aa88 --- /dev/null +++ b/incoming/pattern-study/patterns/clothesline.js @@ -0,0 +1,269 @@ +// ===== Patternflow pattern ===== +// Title: Clothesline +// Family: type +// Idea: Four ropes sag from wall to wall down the tall panel with ten letter cards pegged on them, 바람이 / 분다 / 살아야 / 겠다, and gusts cross the panel from the left so the cards swing on their pegs one after another like pendulums, twist on the line, the ropes lift and bob under them, and every card falls back to hanging straight once the gust has gone. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 72; the text is the Korean of «Le vent se lève, il faut tenter de vivre» from Paul Valéry's Le Cimetière marin (1920); the wind's mean speed from the Beaufort scale's v = 0.836 B^(3/2) m/s; the Hangul cards drawn as strokes for this pattern +// Round: 72 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Wind=2..7, Gust=0..1 +// +// Knob 1: Hue. The card colour, with the ink always cream white: at 0 the cards are teal, at 0.25 blue violet, at 0.5 rose, at 0.75 olive gold, each card a little off its neighbours. +// Knob 2: Speed. The time scale. At 2 a card left to itself swings once a second and a gust takes 1.6 s to cross the panel in a light breeze; at 1 everything is half as fast, at 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Wind. The mean wind on the Beaufort scale. At the opening 2, a light breeze of 2.4 m/s, the cards lean 7 degrees and each gust swings them to about 25; at 4, 6.7 m/s, they lean 45 degrees; at 7, a near gale of 15.5 m/s, they fly almost flat and the ropes pull nearly straight. +// Knob 4: Gust. Turbulence on top of the gusts: at 0 only the gust fronts that cross every 1.5 to 3 s, at the opening 0.06 a little flutter, at 1 gusts half as strong again and twice as often, eddies that throw neighbouring cards opposite ways, and the cards twist round their pegs until some show their blank backs. + +const W = 64, H = 128; +const { floor, round, min, max, abs, sin, cos, exp, sqrt, tanh, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- The cards' syllables, drawn as strokes in an 11 by 13 box, one unit +// an LED: polylines, and ellipses [cx, cy, rx, ry] for the ㅇ. Each vowel +// stands at column 8, each initial in columns 0 to 5 beside it. +const A_ = [[[8, 0], [8, 12]], [[8, 6], [10, 6]]]; +const O_ = { e: [2.5, 6, 2.5, 3.6] }; +const GLYPHS = { + 바: [[[0, 1], [0, 11]], [[5, 1], [5, 11]], [[0, 6], [5, 6]], [[0, 11], [5, 11]], ...A_], + 람: [[[0, 0], [5, 0], [5, 2], [0, 2], [0, 4], [5, 4]], [[8, 0], [8, 6]], [[8, 3], [10, 3]], [[1, 8], [9, 8], [9, 12], [1, 12], [1, 8]]], + 이: [O_, [[8, 0], [8, 12]]], + 분: [[[1, 0], [1, 4]], [[9, 0], [9, 4]], [[1, 2], [9, 2]], [[1, 4], [9, 4]], [[0, 6.5], [10, 6.5]], [[5, 6.5], [5, 8.5]], [[1, 10], [1, 12], [9, 12]]], + 다: [[[5, 1], [0, 1], [0, 11], [5, 11]], ...A_], + 살: [[[2.6, 0], [0, 5]], [[2.6, 0.5], [5.2, 5]], [[8, 0], [8, 6]], [[8, 3], [10, 3]], [[1, 8], [9, 8], [9, 10], [1, 10], [1, 12], [9, 12]]], + 아: [O_, ...A_], + 야: [O_, [[8, 0], [8, 12]], [[8, 4], [10, 4]], [[8, 8], [10, 8]]], + 겠: [[[0, 0], [3.5, 0], [3.5, 5]], [[5, 3], [7, 3]], [[7, 0], [7, 6.5]], [[10, 0], [10, 7]], [[2.5, 8.5], [0.5, 12]], [[2.5, 8.5], [4.5, 12]], [[7.5, 8.5], [5.5, 12]], [[7.5, 8.5], [9.5, 12]]], +}; + +// Each glyph's distance field, 4 samples an LED over x -2..12 and y -2..14, +// so a card turned to any angle samples its strokes smoothly. +const FS = 4, FX0 = -2, FY0 = -2, FW = 14 * FS + 1, FH = 16 * FS + 1; +function segDist(px, py, ax, ay, bx, by) { + const dx = bx - ax, dy = by - ay, l = dx * dx + dy * dy, t = l ? cl(((px - ax) * dx + (py - ay) * dy) / l, 0, 1) : 0; + const ex = px - ax - t * dx, ey = py - ay - t * dy; + return sqrt(ex * ex + ey * ey); +} +const FIELD = {}; +for (const key in GLYPHS) { + const F = new Float32Array(FW * FH); + for (let j = 0; j < FH; j++) + for (let i = 0; i < FW; i++) { + const x = FX0 + i / FS, y = FY0 + j / FS; + let d = 99; + for (const s of GLYPHS[key]) { + if (s.e) { + const [cx, cy, rx, ry] = s.e, ux = (x - cx) / rx, uy = (y - cy) / ry, r = sqrt(ux * ux + uy * uy); + // |r - 1| over the gradient of r: the distance to the ellipse near it. + const gx = ux / rx, gy = uy / ry, gl = sqrt(gx * gx + gy * gy); + d = min(d, gl > 1e-6 ? (abs(r - 1) * r) / gl : min(rx, ry)); + } else for (let k = 1; k < s.length; k++) d = min(d, segDist(x, y, s[k - 1][0], s[k - 1][1], s[k][0], s[k][1])); + } + F[j * FW + i] = d; + } + FIELD[key] = F; +} +function fieldAt(F, gx, gy) { + const fx = (gx - FX0) * FS, fy = (gy - FY0) * FS; + if (fx < 0 || fy < 0 || fx >= FW - 1 || fy >= FH - 1) return 99; + const i = floor(fx), j = floor(fy), u = fx - i, v = fy - j, o = j * FW + i; + return (F[o] * (1 - u) + F[o + 1] * u) * (1 - v) + (F[o + FW] * (1 - u) + F[o + FW + 1] * u) * v; +} + +// ---- The line. Four ropes 32 rows apart, anchored at both walls with a +// 5-LED sag; the cards 16 by 18, hung from a peg at the middle of the top. +const ROPES = [ + { y: 5, cards: "바람이" }, + { y: 37, cards: "분다" }, + { y: 69, cards: "살아야" }, + { y: 101, cards: "겠다" }, +]; +const SAG = 5, CW = 8, CT = 1, CB = 19, GX = 5, GY = 4.5; +// A card about its peg: centre of mass 10 LEDs down, J = I / m in LED^2. +const L = (CT + CB) / 2, J = (2 * CW) ** 2 / 12 + (CB - CT) ** 2 / 12 + L * L; +// A swing of 1 s at small angles: g l / J = (2 pi)^2. +const G = (4 * PI * PI * J) / L; +// Drag: tan(lean) = 0.022 v^2 for v in m/s, and 10 LED/s to a m/s. +const Q = 10, KD = (0.022 * G) / (Q * Q); + +function ropeY(r, x) { + const f1 = sin((PI * x) / 63), f2 = sin((2 * PI * x) / 63); + return r.y + (SAG - r.b1) * f1 - r.b2 * f2; +} + +export function setup(params) { + const ropes = ROPES.map((R, ri) => { + const n = [...R.cards].length, cards = []; + [...R.cards].forEach((ch, i) => { + const x = 32 + (i - (n - 1) / 2) * 20 + (Math.random() - 0.5) * 2; + cards.push({ ch, x, th: (Math.random() - 0.5) * 0.1, w: 0, ph: 0, pw: 0, dh: (Math.random() - 0.5) * 0.08, ay: 0 }); + }); + return { y: R.y, cards, b1: 0, v1: 0, a1: 0, b2: 0, v2: 0, a2: 0, ri }; + }); + params.s = { t: 0, ropes, gusts: [], next: 0.3, ph: [0, 1, 2, 3, 4, 5].map(() => Math.random() * 2 * PI), warm: false, hue: 0, speed: 2, wind: 2, gust: 0.06, F: new Float32Array(W * H * 3) }; +} + +// The wind across the panel, LED/s rightward, at sim time t. +function windAt(S, x, y) { + const V = S.V, t = S.t; + let g = 0; + for (const q of S.gusts) { + const u = (x - q.x + 0.18 * (y - 64)) / q.s, v = (y - q.yc) / 90; + g += q.a * exp(-u * u - v * v); + } + const P = S.ph, k = S.gust; + // Turbulence: four drifting waves of different sizes. + const tu = + 0.45 * sin(0.21 * (x - 0.8 * V * t) + 0.05 * y + P[0]) + + 0.3 * sin(0.37 * (x - 0.8 * V * t) - 0.11 * y + 2.3 * t + P[1]) + + 0.25 * sin(0.6 * x - 0.17 * y - 3.7 * t + P[2]) + + 0.2 * sin(0.09 * y + 1.3 * t + P[3]); + return V * (1 + g + 1.6 * k * tu); +} + +function step(S, h) { + S.t += h; + const V = S.V, k = S.gust; + // Gust fronts: born left of the panel every 1.5 to 3 s (twice as often at + // Gust 1), crossing at 0.9 V + 20 LED/s. + S.next -= h; + if (S.next <= 0) { + S.next = (1.5 + Math.random() * 1.5) / (1 + k); + S.gusts.push({ x: -25, s: 8 + Math.random() * 5, a: (0.7 + Math.random() * 0.6) * (1 + 0.5 * k), yc: Math.random() * 128, c: 0.9 * V + 20 }); + } + for (const q of S.gusts) q.x += q.c * h; + S.gusts = S.gusts.filter((q) => q.x < 110); + for (const r of S.ropes) { + // The wind on its cards lifts the rope (the first mode) and tips it + // toward the side the gust has reached (the second); 0.9 and 1.6 Hz. + let l1 = 0, l2 = 0; + for (const c of r.cards) { + const py = ropeY(r, c.x), w = windAt(S, c.x, py + L); + c.wv = w; + c.wd = windAt(S, c.x + 6, py + L) - windAt(S, c.x - 6, py + L); + const f = (w * w) / (Q * Q) / 100; + l1 += f * sin((PI * c.x) / 63); + l2 += f * sin((2 * PI * c.x) / 63); + } + const w1 = 2 * PI * 0.9, w2 = 2 * PI * 1.6; + r.a1 = -w1 * w1 * r.b1 - 0.5 * w1 * r.v1 + w1 * w1 * 4 * tanh(l1); + r.a2 = -w2 * w2 * r.b2 - 0.5 * w2 * r.v2 + w2 * w2 * 1.5 * tanh(l2 - 0.3 * l1); + r.v1 += r.a1 * h; + r.v2 += r.a2 * h; + r.b1 = min(SAG * 0.85, r.b1 + r.v1 * h); + r.b2 += r.v2 * h; + for (const c of r.cards) { + // The peg's own acceleration, down, changes the gravity the card feels. + const ay = -r.a1 * sin((PI * c.x) / 63) - r.a2 * sin((2 * PI * c.x) / 63); + const ct = cos(c.th), st = sin(c.th), rel = c.wv - 0.5 * L * ct * c.w, F = KD * rel * abs(rel); + const acc = ((-(G - ay) * st + F * ct) * L) / J - 0.3 * c.w; + c.w += acc * h; + c.th += c.w * h; + // Twist about the peg: a gust front reaching one edge of the card + // before the other turns it, and a spring brings it back. + const target = cl(((0.5 * c.wd) / max(V, 1)) * (0.3 + 1.6 * k) * (0.7 + 0.08 * S.B) + c.dh, -1.5, 1.5); + c.pw += (16 * (target - c.ph) - 1.4 * c.pw) * h; + c.ph += c.pw * h; + } + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kn = input.knobValues || [0, 2, 2, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kn[0] || 0; + S.speed = cl(kn[1], 0.2, 3); + S.B = cl(kn[2], 2, 7); + S.gust = cl(kn[3], 0, 1); + // Beaufort: v = 0.836 B^1.5 m/s. + S.V = 0.836 * S.B ** 1.5 * Q; + let T = d * (S.speed / 2); + // Start with the line already in the wind, 2.5 s in. + if (!S.warm) (S.warm = true), (T += 2.5); + const n = max(1, Math.ceil(T / (1 / 240))); + for (let i = 0; i < n; i++) step(S, T / n); +} + +// ---- Drawing. +const BG0 = [5, 7, 16], BG1 = [2, 3, 8]; +function blend(F, x, y, a, r, g, b) { + if (x < 0 || x >= W || y < 0 || y >= H || a <= 0) return; + const o = (y * W + x) * 3; + F[o] += (r - F[o]) * a; + F[o + 1] += (g - F[o + 1]) * a; + F[o + 2] += (b - F[o + 2]) * a; +} + +function drawCard(S, F, r, c, ci) { + const px = c.x, py = ropeY(r, c.x), th = c.th, ct = cos(th), st = sin(th); + const cp = cos(c.ph), sx = max(0.06, abs(cp)), back = cp < 0; + // The knob runs the cards from sea green through azure and violet to rose, + // past the oranges, which a dim card turns brown. + const hue = 0.42 + 0.56 * S.hue + ((ci * 0.37) % 1 - 0.5) * 0.07; + const card = hsv(hue, 0.78, back ? 0.3 : 0.5), ink = [1, 0.96, 0.86]; + // Light from the upper left: a card turned away from it dims. + const lit = 0.55 + 0.45 * sx + 0.12 * st; + const Fg = FIELD[c.ch]; + // Bounding box of the turned card and its peg. + let x0 = 99, x1 = -99, y0 = 99, y1 = -99; + for (const [u, v] of [[-CW, -3], [CW, -3], [-CW, CB], [CW, CB]]) { + const X = px + u * sx * ct + v * st, Y = py - u * sx * st + v * ct; + x0 = min(x0, X); + x1 = max(x1, X); + y0 = min(y0, Y); + y1 = max(y1, Y); + } + for (let Y = max(0, floor(y0) - 1); Y <= min(H - 1, Math.ceil(y1) + 1); Y++) + for (let X = max(0, floor(x0) - 1); X <= min(W - 1, Math.ceil(x1) + 1); X++) { + const dx = X - px, dy = Y - py, us = dx * ct - dy * st, v = dx * st + dy * ct; + // Coverage of the card, its edges antialiased in LEDs on the panel. + const cov = cl(CW * sx - abs(us) + 0.5, 0, 1) * cl(min(v - CT, CB - v) + 0.5, 0, 1); + if (cov > 0) { + const u = (back ? -us : us) / sx; + const dd = fieldAt(Fg, u + GX, v - GY); + // Ink: strokes 1.3 LEDs wide, faint and mirrored through the paper + // from behind. + const ik = cl(0.65 + 0.5 - dd * (0.6 + 0.4 * sx), 0, 1) * (back ? 0.18 : 1); + const shade = lit * (1 - 0.18 * max(0, (v - 12) / 7)); + blend(F, X, Y, cov, (card[0] + (ink[0] - card[0]) * ik) * shade, (card[1] + (ink[1] - card[1]) * ik) * shade, (card[2] + (ink[2] - card[2]) * ik) * shade); + } + // The peg: a pale wooden clip 2.4 LEDs wide over the rope. + const pc = cl(1.2 - abs(us) + 0.5, 0, 1) * cl(min(v + 2.6, 3 - v) + 0.5, 0, 1); + if (pc > 0) blend(F, X, Y, pc, 0.82, 0.72, 0.55); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const F = S.F; + for (let y = 0, o = 0; y < H; y++) { + const f = y / (H - 1); + for (let x = 0; x < W; x++, o += 3) { + F[o] = (BG0[0] + (BG1[0] - BG0[0]) * f) / 255; + F[o + 1] = (BG0[1] + (BG1[1] - BG0[1]) * f) / 255; + F[o + 2] = (BG0[2] + (BG1[2] - BG0[2]) * f) / 255; + } + } + let ci = 0; + for (const r of S.ropes) { + // The rope, one LED thick, shared between the two rows it falls across. + for (let x = 0; x < W; x++) { + const y = ropeY(r, x), iy = floor(y), f = y - iy; + blend(F, x, iy, 1 - f, 0.55, 0.5, 0.42); + blend(F, x, iy + 1, f, 0.55, 0.5, 0.42); + } + // Hooks in the walls. + blend(F, 0, r.y - 1, 1, 0.7, 0.7, 0.72); + blend(F, 63, r.y - 1, 1, 0.7, 0.7, 0.72); + for (const c of r.cards) drawCard(S, F, r, c, ci++); + } + for (let y = 0, o = 0; y < H; y++) + for (let x = 0; x < W; x++, o += 3) display.setPixel(x, y, round(cl(F[o], 0, 1) * 255), round(cl(F[o + 1], 0, 1) * 255), round(cl(F[o + 2], 0, 1) * 255)); +} diff --git a/incoming/pattern-study/patterns/hydra-game.js b/incoming/pattern-study/patterns/hydra-game.js new file mode 100644 index 000000000..1b03436bd --- /dev/null +++ b/incoming/pattern-study/patterns/hydra-game.js @@ -0,0 +1,458 @@ +// ===== Patternflow pattern ===== +// Title: Hydra Game +// Family: algo +// Idea: Hercules fights Kirby and Paris's hydra, a tree rooted at the bottom of the panel with glowing heads on curved necks: at step n he cuts one head, and unless it grew straight from the root the node two levels down grows n copies of the stump's branch, which sprout from it in gold, so the hydra swells into a dense fan of hundreds or thousands of heads while the ordinal at the top, the sum of omega to the power of each branch's ordinal, falls at every cut, and the cuts come faster and faster until the last head drops and the root stands alone. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the hydra game (Laurie Kirby and Jeff Paris, Accessible independence results for Peano arithmetic, Bulletin of the London Mathematical Society, 1982), the ordinal of a tree in Cantor normal form (Georg Cantor), pattern round 72 +// Round: 72 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Hydra=1..5, Strategy=0..3 +// +// Knob 1: Hue. Turns the hydra round the wheel. At 0 the necks are green and the heads pale mint, new growth sprouts gold and the ordinal's omegas are green; at 0.25 the necks are sky blue, at 0.5 violet, at 0.75 rose red. +// Knob 2: Speed. Cuts a second, 0.9 Speed (1 + n / 8) at step n, so the cuts speed up as the hydra grows: at the opening 2 the first cut comes 1.6 s after the hydra has grown, step 40 at about 8 s, step 1149 at 23 s and step 5629 at 30 s; at 0.2 everything takes ten times as long. +// Knob 3: Hydra. Five starting hydras, by whole steps. 1: omega^2 + omega + 1, a root with a two-headed neck, a one-headed neck and a head, 7 nodes; cut from the right it dies at step 1149 with 576 nodes at its biggest. 2: omega^omega, a single neck of three, dead at step 37 from either side, 20 nodes at most. 3: omega^2 + omega 2, dead at step 445 from either side, 224 nodes. 4: omega^2 + omega + 2, dead at step 5629 from the right, 2816 nodes. 5: omega^2 + omega 2 + 2, dead at step 2302 from either side, 1153 nodes. +// Knob 4: Strategy. Which head Hercules cuts, by whole steps: 0 the rightmost, 1 the leftmost, 2 a random branch at every fork, 3 the end of the longest neck, the rightmost of the deepest. The order changes the length of the fight a hundredfold: hydra 1 dies at step 1149 cut from the right, at 30 from the left and at 23 from the deepest end, and 2000 random hunts took from 17 to 441 steps. A hunt whose regrowth would pass 8000 nodes is called off; in 2000 random hunts on hydra 5 that happened twice. +// Buttons: K1 tap grows the hydra again from its root. + +// Kirby and Paris (1982): a hydra is a finite rooted tree and its heads are +// the leaves. At step n Hercules cuts a head. If its neck grows from the +// root, the head is simply gone; otherwise the node below the stump grows n +// copies of the stump's branch, head removed. Every way of choosing heads +// kills every hydra in finitely many steps, and Peano arithmetic cannot +// prove it. The proof gives each node the ordinal o(v) = the sum of +// omega^o(c) over its children c, written in Cantor normal form with the +// biggest terms first: a cut lowers one term and adds n smaller ones, so +// the root's ordinal falls at every step, and ordinals below epsilon_0 cannot +// fall forever. Arithmetic counts only up to omega^omega^... of finite +// height, short of epsilon_0, so it cannot see the end. +// +// The panel can draw a few thousand heads, which leaves the hydras below +// omega^3 and the single neck of three: the rightmost cut on omega^2 started +// at step m ends near (m + 4) 2^(m + 2), and an omega^2 met at step 40 +// would need some 10^13 steps. + +const W = 64, H = 128, N = W * H; +const { floor, round, min, max, abs, sin, cos, exp, sqrt, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// The root's children as brackets, each pair a node, an empty pair a head. +const HYDRAS = ["(()())(())()", "((()))", "(())(()())(())", "(()())(())()()", "()(())(()())(())()"]; +const CAP = 8000; +const ROOT_X = 31.5, ROOT_Y = 124, TOP_Y = 24; +const HUE0 = 0.36; + +function mk(p, x, y, t) { + return { p, c: [], x, y, tx: x, ty: y, dx: 0, b: t, ph: Math.random() * 2 * PI, k: 1, h: 0 }; +} +function clone(v, p, x, y, t) { + const u = mk(p, x, y, t); + for (const c of v.c) u.c.push(clone(c, u, x, y, t)); + return u; +} +function size(v) { + let s = 1; + for (const c of v.c) s += size(c); + return s; +} +function height(v) { + let h = 0; + for (const c of v.c) h = max(h, height(c) + 1); + return (v.h = h); +} + +// ---- Ordinals below epsilon_0: a list of [exponent, coefficient] terms, +// exponents falling, each exponent an ordinal of the same kind; [] is 0. +function cmpO(a, b) { + for (let i = 0; i < a.length && i < b.length; i++) { + const c = cmpO(a[i][0], b[i][0]); + if (c) return c; + if (a[i][1] !== b[i][1]) return a[i][1] - b[i][1]; + } + return a.length - b.length; +} +function ord(v) { + if (!v.c.length) return []; + const ex = v.c.map(ord).sort((a, b) => cmpO(b, a)), out = []; + for (const e of ex) { + const l = out[out.length - 1]; + if (l && !cmpO(l[0], e)) l[1]++; + else out.push([e, 1]); + } + return out; +} + +// ---- The game. +function restart(S, hy) { + S.hy = hy; + const root = mk(null, ROOT_X, ROOT_Y, S.t), st = [root]; + for (const ch of HYDRAS[hy]) { + if (ch === "(") { + const v = mk(st[st.length - 1], ROOT_X, ROOT_Y, S.t); + st[st.length - 1].c.push(v); + st.push(v); + } else st.pop(); + } + S.root = root; + S.nodes = size(root); + // Levels share the 100 rows above the root: 50 apart for a hydra two + // necks high, 33 for three. + S.sp = min(50, floor((ROOT_Y - TOP_Y) / height(root))); + S.n = 0; + S.acc = 0; + S.state = 0; + S.st = 0; + S.grow = 1; + S.dirty = true; + S.fx = []; + aim(S); +} + +// The head Hercules cuts next, as the branch index taken at each fork. +function aim(S) { + const path = []; + let v = S.root; + if (S.strat === 3) height(S.root); + while (v.c.length) { + const n = v.c.length; + let i = n - 1; + if (S.strat === 1) i = 0; + else if (S.strat === 2) i = floor(Math.random() * n); + else if (S.strat === 3) for (const want = v.h - 1; i > 0 && v.c[i].h !== want; i--); + path.push(i); + v = v.c[i]; + } + S.path = path; + S.aim = path.length ? v : null; +} + +function chop(S) { + const path = S.path; + let g = null, p = null, v = S.root; + for (let d = 0; d < path.length; d++) (g = p), (p = v), (v = v.c[path[d]]); + const n = ++S.n; + p.c.splice(path[path.length - 1], 1); + S.nodes--; + S.dirty = true; + if (S.fx.length < 28) S.fx.push({ cx: v.x + v.dx, cy: v.y, x: v.x + v.dx, y: v.y, vx: (Math.random() - 0.5) * 14, vy: -14, a: 0 }); + if (g) { + const s = size(p); + if (S.nodes + n * s > CAP) { + S.state = 2; + S.st = 0; + return; + } + const at = path[path.length - 2] + 1, copies = []; + for (let j = 0; j < n; j++) copies.push(clone(p, g, g.x + g.dx, g.y, S.t)); + g.c = g.c.slice(0, at).concat(copies, g.c.slice(at)); + S.nodes += n * s; + } + if (!S.root.c.length) { + S.state = 1; + S.st = 0; + S.aim = null; + return; + } + aim(S); +} + +// Heads under each node, then a span of the 60 columns for each node in +// proportion to its heads, levels S.sp apart up from the root. +// A node with a head of its own keeps at least 4 heads' width, so a few +// branches stay apart beside a fan of hundreds. +function count(v) { + if (!v.c.length) return (v.k = v.w = 1); + let s = 0, w = 0; + for (const c of v.c) (s += count(c)), (w += c.w); + v.w = max(4, w); + return (v.k = s); +} +function place(S, v, a, b, d) { + v.tx = (a + b) / 2; + v.ty = ROOT_Y - d * S.sp; + let x = a, sw = 0; + for (const c of v.c) sw += c.w; + for (const c of v.c) { + const w = ((b - a) * c.w) / sw; + place(S, c, x, x + w, d + 1); + x += w; + } +} +function ease(S, v, e, d) { + v.x += (v.tx - v.x) * e; + v.y += (v.ty - v.y) * e; + // The necks writhe: a head sways up to 0.7 LED a level. + v.dx = d ? sin(S.t * 1.7 + v.ph) * 0.7 * d : 0; + for (const c of v.c) ease(S, c, e, d + 1); +} + +export function setup(params) { + params.s = { t: 0, hue: 0, speed: 2, strat: 0, hy: -1, A: new Float32Array(N * 3), O: new Uint8ClampedArray(N * 3), ordv: [], fx: [] }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 1, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + S.speed = cl(k[1], 0.2, 3); + const hy = cl(round(k[2]), 1, 5) - 1, strat = cl(round(k[3]), 0, 3); + S.t += d; + if (hy !== S.hy || (input.btnPressed && input.btnPressed[0])) restart(S, hy); + if (strat !== S.strat) { + S.strat = strat; + if (S.state === 0) aim(S); + } + if (S.state === 0) { + if (S.grow > 0) S.grow -= d; + else { + S.acc += d * S.speed * 0.9 * (1 + S.n / 8); + for (let m = 0; S.acc >= 1 && S.state === 0; m++) { + if (m === 600) { + S.acc = 0; + break; + } + chop(S); + S.acc -= 1; + } + } + } else { + S.st += d; + if (S.st > (S.state === 1 ? 3.5 : 2)) restart(S, S.hy); + } + count(S.root); + place(S, S.root, 1.5, 61.5, 0); + ease(S, S.root, 1 - exp(-d * 7), 0); + if (S.dirty) { + S.ordv = ord(S.root); + S.heads = S.root.c.length ? S.root.k : 0; + S.dirty = false; + } + for (const f of S.fx) { + f.a += d; + f.vy += 70 * d; + f.x += f.vx * d; + f.y += f.vy * d; + } + S.fx = S.fx.filter((f) => f.a < 1.6 && f.y < H + 2); +} + +// ---- Drawing into a light buffer, then 1 - exp(-light) per channel. +function splat(A, x, y, r, g, b) { + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + for (let j = 0; j < 2; j++) { + const yy = iy + j; + if (yy < 0 || yy >= H) continue; + const wy = j ? fy : 1 - fy; + for (let i = 0; i < 2; i++) { + const xx = ix + i; + if (xx < 0 || xx >= W) continue; + const w = wy * (i ? fx : 1 - fx), o = (yy * W + xx) * 3; + A[o] += r * w; + A[o + 1] += g * w; + A[o + 2] += b * w; + } + } +} +// A neck: it leaves its node straight up and bends over to the child, +// 2.8 LEDs thick at the node and 1.4 at the head, in a faint glow, and a +// wave of up to 1.1 LEDs runs up it at phase ph. +function neck(A, x0, y0, x1, y1, col, w, thick, ph) { + const ky = y0 + (y1 - y0) * 0.6, n = 2 + floor(max(abs(x1 - x0), abs(y1 - y0)) * 1.25); + const r = col[0] * w, g = col[1] * w, b = col[2] * w; + for (let i = 1; i < n; i++) { + const t = i / n, u = 1 - t, y = u * u * y0 + 2 * u * t * ky + t * t * y1; + let x = u * u * x0 + 2 * u * t * x0 + t * t * x1; + if (!thick) { + splat(A, x, y, r, g, b); + continue; + } + x += 1.1 * sin(PI * t) * sin(ph - 5 * t); + // Across the neck: the normal to the curve's tangent. + let tx = 2 * t * (x1 - x0), ty = 2 * u * (ky - y0) + 2 * t * (y1 - ky); + const l = sqrt(tx * tx + ty * ty) || 1, half = (2.8 - 1.4 * t) / 2; + tx /= l; + ty /= l; + for (let j = -1; j <= 1; j++) { + const q = j ? 0.45 : 0.8; + splat(A, x - ty * half * j, y + tx * half * j, r * q, g * q, b * q); + } + // and its glow, 1.5 LEDs further out each side. + for (let j = -1; j <= 1; j += 2) splat(A, x - ty * (half + 1.5) * j, y + tx * (half + 1.5) * j, r * 0.16, g * 0.16, b * 0.16); + } +} +function headDot(A, x, y, col, w) { + splat(A, x, y - 0.5, col[0] * w * 1.6, col[1] * w * 1.6, col[2] * w * 1.6); + const q = w * 0.45; + for (const [i, j] of [[-1.2, 0], [1.2, 0], [0, -1.6], [-0.9, -1.2], [0.9, -1.2]]) splat(A, x + i, y + j, col[0] * q, col[1] * q, col[2] * q); +} +// The neck colour, gold for a branch just grown and back to the hydra's in +// a second. +const MIX = [0, 0, 0]; +function tint(C, age) { + const f = age < 1 ? (1 - age) * (1 - age) : 0; + for (let i = 0; i < 3; i++) MIX[i] = C.neck[i] + (C.gold[i] - C.neck[i]) * f; + return MIX; +} +const BN = new Float32Array(64), BX = new Float32Array(64), BY = new Float32Array(64), BA = new Float32Array(64), BL = new Float32Array(64); +function drawNode(S, A, C, v) { + const n = v.c.length; + if (!n) return; + const px = v.x + v.dx, py = v.y; + if (n <= 48) { + for (const c of v.c) { + const cx = c.x + c.dx; + neck(A, px, py, cx, c.y, tint(C, S.t - c.b), 0.85, true, S.t * 2.3 + c.ph); + if (c.c.length) { + // A joint where the neck meets the next ones. + splat(A, cx, c.y, C.neck[0] * 0.7, C.neck[1] * 0.7, C.neck[2] * 0.7); + drawNode(S, A, C, c); + } else headDot(A, cx, c.y, C.head, 1); + } + return; + } + // Hundreds of necks from one node: one neck a column, as bright as the + // square root of the necks it stands for. + BN.fill(0); + BX.fill(0); + BY.fill(0); + BA.fill(0); + BL.fill(0); + for (const c of v.c) { + const cx = c.x + c.dx, j = cl(round(cx), 0, 63); + BN[j]++; + BX[j] += cx; + BY[j] += c.y; + BA[j] += S.t - c.b; + if (c.c.length) drawNode(S, A, C, c); + else BL[j]++; + } + for (let j = 0; j < 64; j++) { + if (!BN[j]) continue; + const cx = BX[j] / BN[j], cy = BY[j] / BN[j], w = sqrt(BN[j]); + neck(A, px, py, cx, cy, tint(C, BA[j] / BN[j]), 0.55 * w, false, 0); + if (BL[j]) headDot(A, cx, cy, C.head, sqrt(BL[j])); + } +} + +// ---- The 3 by 5 face for the counters, and a 5-wide omega. +const G = { + 0: "111101101101111", 1: "010110010010111", 2: "111001111100111", 3: "111001011001111", 4: "101101111001001", + 5: "111100111001111", 6: "111100111101111", 7: "111001010010010", 8: "111101111101111", 9: "111101111001111", + n: "000110101101101", "=": "000111000111000", "+": "000010111010000", D: "110101101101110", E: "111100110100111", + A: "010101111101101", M: "101111111101101", X: "101101010101101", +}; +// rows 00000 10001 10101 10101 01010 +const OMEGA = "0000010001101011010101010"; +function put(O, x, y, r, g, b) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + O[o] = r; + O[o + 1] = g; + O[o + 2] = b; +} +function glyph(O, ch, x, y, col) { + const g = G[ch]; + for (let i = 0; i < 15; i++) if (g[i] === "1") put(O, x + (i % 3), y + floor(i / 3), col[0], col[1], col[2]); + return x + 4; +} +function text(O, s, x, y, col) { + for (const ch of s) x = glyph(O, ch, x, y, col); + return x; +} +function omega(O, x, y, col) { + for (let i = 0; i < 25; i++) if (OMEGA[i] === "1") put(O, x + (i % 5), y + floor(i / 5), col[0], col[1], col[2]); + return x + 6; +} +// An ordinal in Cantor normal form, exponents raised 4 rows; returns the +// end column, or 99 when the next term would pass the edge. +function ordText(O, o, x, y, C) { + if (!o.length) return text(O, "0", x, y, C.txt); + for (let i = 0; i < o.length; i++) { + const [e, c] = o[i]; + if (i) { + if (x > 54) return 99; + x = glyph(O, "+", x, y, C.dim); + } + if (!e.length) { + x = text(O, String(c), x, y, C.txt); + continue; + } + x = omega(O, x, y, C.om); + if (!(e.length === 1 && !e[0][0].length && e[0][1] === 1)) x = ordText(O, e, x, y - 4, C); + if (c > 1) { + put(O, x, y + 2, C.dim[0], C.dim[1], C.dim[2]); + x = text(O, String(c), x + 2, y, C.txt); + } + } + return x; +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.root) return; + const A = S.A, O = S.O, h = S.hue + HUE0; + const C = { + neck: hsv(h, 0.85, 0.9), + head: hsv(h + 0.04, 0.3, 1), + gold: hsv(h - 0.22, 0.55, 1.1), + txt: [200, 200, 196], + dim: [120, 120, 120], + om: hsv(h, 0.6, 1).map((c) => round(c * 255)), + }; + A.fill(0); + // The ground the root stands on. + for (let x = 0; x < W; x++) { + const o = (127 * W + x) * 3, f = 0.1 * exp(-(((x - ROOT_X) / 22) ** 2)); + A[o] += C.neck[0] * f; + A[o + 1] += C.neck[1] * f; + A[o + 2] += C.neck[2] * f; + } + // Called off for size: the hydra fades out. + const fade = S.state === 2 ? max(0, 1 - S.st / 1.5) : 1; + drawNode(S, A, C, S.root); + // The root, pulsing while it stands alone. + const rw = S.state === 1 ? 1.4 + 0.8 * sin(S.st * 5) : 1; + // Dead: a ring goes out from the root, 30 LEDs in 1.5 s. + if (S.state === 1 && S.st < 1.5) { + const r = 3 + S.st * 20, q = 1.6 * (1 - S.st / 1.5); + for (let i = 0; i < 80; i++) { + const a = (i / 80) * PI; + splat(A, S.root.x + r * cos(a), ROOT_Y - r * sin(a), C.head[0] * q, C.head[1] * q, C.head[2] * q); + } + } + for (let j = -1; j <= 1; j++) for (let i = -1; i <= 1; i++) splat(A, S.root.x + i, ROOT_Y + j, C.neck[0] * rw * (i || j ? 0.5 : 1.2), C.neck[1] * rw * (i || j ? 0.5 : 1.2), C.neck[2] * rw * (i || j ? 0.5 : 1.2)); + // The next head, ringed while the cuts are slow enough to follow. + if (S.aim && S.state === 0 && S.grow <= 0 && S.speed * 0.9 * (1 + S.n / 8) < 6) { + const a = S.aim, ax = a.x + a.dx, w = 0.35 + 0.25 * sin(S.t * 14); + for (let i = 0; i < 12; i++) splat(A, ax + 2.7 * cos((i * PI) / 6), a.y + 2.7 * sin((i * PI) / 6), w, w, w * 0.9); + } + // Each cut: a white flash where the neck was, and the head falling. + for (const f of S.fx) { + if (f.a < 0.3) { + const q = (1 - f.a / 0.3) * 1.4, r = 1 + f.a * 9; + for (let i = 0; i < 10; i++) splat(A, f.cx + r * cos(i * 0.63), f.cy + r * sin(i * 0.63), q * 0.5, q * 0.5, q * 0.42); + } + const q = max(0, 1 - f.a / 1.6) * 1.3; + splat(A, f.x, f.y, C.head[0] * q, C.head[1] * q, C.head[2] * q); + } + for (let p = 0; p < N * 3; p++) O[p] = 255 * fade * (1 - exp(-1.25 * A[p])); + // Step and heads, the ordinal under them. + let x = text(O, "n=", 1, 1, C.dim); + text(O, String(S.n), x, 1, C.txt); + const right = S.state === 1 ? "DEAD" : S.state === 2 ? "MAX" : String(S.heads); + x = 64 - right.length * 4; + if (S.state === 0) { + const hc = C.head.map((c) => round(c * 230)); + for (const [i, j] of [[0, 0], [1, 0], [0, 1], [1, 1]]) put(O, x - 4 + i, 2 + j, hc[0], hc[1], hc[2]); + } + text(O, right, x, 1, C.txt); + if (ordText(O, S.ordv, 1, 11, C) === 99) for (let i = 0; i < 3; i++) put(O, 58 + i * 2, 15, C.dim[0], C.dim[1], C.dim[2]); + for (let p = 0, y = 0; y < H; y++) for (let xx = 0; xx < W; xx++, p += 3) display.setPixel(xx, y, O[p], O[p + 1], O[p + 2]); +} From d6eebcfb237fa5505a454b29d410cf66f8d58127 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 20:32:34 +0000 Subject: [PATCH 06/14] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2072=ED=8C=90:=20Ruppert=20Refinement=20=EA=B3=BC=20Lattice=20?= =?UTF-8?q?Reduction?= 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_01Bc4JkrPaZ8GQQiA22PWEvR --- .../patterns/lattice-reduction.js | 262 +++++++++ .../patterns/ruppert-refinement.js | 553 ++++++++++++++++++ 2 files changed, 815 insertions(+) create mode 100644 incoming/pattern-study/patterns/lattice-reduction.js create mode 100644 incoming/pattern-study/patterns/ruppert-refinement.js diff --git a/incoming/pattern-study/patterns/lattice-reduction.js b/incoming/pattern-study/patterns/lattice-reduction.js new file mode 100644 index 000000000..6de50146b --- /dev/null +++ b/incoming/pattern-study/patterns/lattice-reduction.js @@ -0,0 +1,262 @@ +// ===== Patternflow pattern ===== +// Title: Lattice Reduction +// Family: geometry +// Idea: The points of a plane lattice stand still as white dots over the whole panel while the plane is tiled by the parallelogram of its current basis, which starts as long thin slivers, and Lagrange-Gauss reduction takes the basis apart one step at a time: b2 shears along b1 by the whole number of b1 nearest its projection, the tiles sliding until their corners land back on dots, the arrows trade colours when b2 has become the shorter and the two swap names, and when the projection is half of b1 or less the tiles are compact, flash, hold, and a new lattice starts skewed. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: reduction of binary quadratic forms, the two-dimensional lattice basis reduction (Joseph-Louis Lagrange, 1773; Carl Friedrich Gauss, Disquisitiones Arithmeticae, 1801), whose heir in any dimension is the LLL algorithm (Arjen Lenstra, Hendrik Lenstra and László Lovász, 1982), pattern round 72 +// Round: 72 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Skew=3..7, Spacing=11..22 +// +// Knob 1: Hue. Turns the tiles and the arrows round the wheel together. At 0 the tiles are teal, blue and violet, the cell at the centre green, b1 amber and b2 pink; at 0.5 the tiles go red, ochre and lime, the centre cell magenta, b1 blue and b2 spring green. +// Knob 2: Speed. At 2 a shear takes 0.55 s, a swap 0.4 s and a pause of 0.2 s follows each, so a basis from 3 shears is reduced in 2.2 to 4.2 s and a new lattice comes every 6 to 8 s. Every time but the 2.6 s hold and the 0.5 s cross-fade scales as 2 / Speed: at 0.2 a shear takes 5.5 s, at 3 0.37 s. +// Knob 3: Skew. How many shears, b2 += b1 and b1 += b2 by turns, each with the sign that lengthens, turn the reduced basis into the starting one. At 3 the longer vector is 3.5 to 4.9 spacings and the tiles 2.3 to 3.2 LEDs wide; at 5 about 10 spacings and one LED wide; at 7 up to 25 spacings, where a cap stops the word, and the tiles under a LED, a moire until the first shear. Gauss's steps take 2 shears at 3 and 4 at 7, each by up to 3 times b1, with one to four swaps between. +// Knob 4: Spacing. The square root of one cell's area, 11 to 22 LEDs: 68 dots at 11, 17 at 22. The lattices come square, hexagonal, rectangular (sides 1.6 to 1) and random (60 to 90 degrees, sides up to 1.7 to 1) in turn, all of that area and turned at random. +// Buttons: K1 tap starts a new lattice. + +// A lattice is every whole combination of two vectors. Many bases give the +// same lattice: b1, b2 and b1, b2 - q b1 for any whole q, or the two swapped. +// Lagrange (1773) and Gauss (1801) reduced a basis by alternating the two: +// keep b1 the shorter, subtract from b2 the multiple of b1 nearest its +// projection, and stop when that multiple is 0. Then |b1| <= |b2| and +// |b1 . b2| <= |b1|^2 / 2, so the angle between them is 60 to 120 degrees +// and b1 is a shortest vector of the lattice. + +const W = 64, H = 128; +const { floor, round, min, max, abs, sqrt, sin, cos, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const frac = (v) => v - floor(v); +const smooth = (u) => u * u * (3 - 2 * u); +// The lattice point everything turns about, at the panel's middle. +const OX = 31.5, OY = 63.5; +const HOLD = 2.6, FADE = 0.5, LOOK = 0.8, CAP = 26; + +function hsv(h, s, v, out) { + h = frac(h) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// The basis is R U: R the reduced basis the lattice was made from, U whole +// numbers with determinant +-1, kept exactly so the basis never drifts off +// the lattice. +function basis(S) { + const R = S.R, U = S.U, b = S.b; + b[0] = R[0] * U[0] + R[2] * U[2]; + b[1] = R[1] * U[0] + R[3] * U[2]; + b[2] = R[0] * U[1] + R[2] * U[3]; + b[3] = R[1] * U[1] + R[3] * U[3]; +} +const len2 = (x, y) => x * x + y * y; + +function newLattice(S) { + // The last frame stays to cross-fade from, black at the very start. + S.old.set(S.acc); + S.xf = 1; + const kind = S.kind++ % 4, a = S.spacing, th = Math.random() * PI; + let u, v, ang; + if (kind === 0) (u = a), (v = a), (ang = PI / 2); + else if (kind === 1) (u = v = a * sqrt(2 / sqrt(3))), (ang = PI / 3); + else if (kind === 2) (u = a / sqrt(1.6)), (v = a * sqrt(1.6)), (ang = PI / 2); + else { + ang = PI / 3 + Math.random() * (PI / 6); + const r = 1 + Math.random() * 0.7; + u = a / sqrt(r * sin(ang)); + v = u * r; + } + const R = S.R; + R[0] = u * cos(th); + R[1] = u * sin(th); + R[2] = v * cos(th + ang); + R[3] = v * sin(th + ang); + // The word: shears of b2 along b1 and of b1 along b2 by turns, from a + // random first one, each with the sign that lengthens the vector it + // moves, stopping early if the longer vector would pass CAP spacings. + const U = S.U; + U.splice(0, 4, 1, 0, 0, 1); + basis(S); + for (let k = 0, w = Math.random() < 0.5 ? 1 : 0; k < S.skew; k++, w ^= 1) { + const b = S.b, d = b[0] * b[2] + b[1] * b[3], sg = d > 1e-9 ? 1 : d < -1e-9 ? -1 : Math.random() < 0.5 ? 1 : -1; + const V = U.slice(); + if (w) (V[1] += sg * V[0]), (V[3] += sg * V[2]); + else (V[0] += sg * V[1]), (V[2] += sg * V[3]); + const nx = w ? R[0] * V[1] + R[2] * V[3] : R[0] * V[0] + R[2] * V[2], ny = w ? R[1] * V[1] + R[3] * V[3] : R[1] * V[0] + R[3] * V[2]; + if (len2(nx, ny) > (CAP * a) ** 2) break; + U.splice(0, 4, ...V); + basis(S); + } + Object.assign(S, { phase: 0, pt: 0, op: null, steps: 0, ca: 1, cb: 2 }); +} + +// The next operation: a swap when b2 is the shorter, else the shear by q, +// else none (reduced). +function nextOp(S) { + const b = S.b, l1 = len2(b[0], b[1]), l2 = len2(b[2], b[3]); + if (l2 < l1 * (1 - 1e-9)) return { swap: true }; + // A projection of half of b1 or less is already reduced; testing that + // and not q = 0 keeps a tie, as in the hexagonal lattice, from flipping + // between q = 1 and q = -1 for ever. + const mu = (b[0] * b[2] + b[1] * b[3]) / l1; + return abs(mu) > 0.5 + 1e-7 ? { q: round(mu) } : null; +} + +export function setup(params) { + params.s = { + t: 0, hue: 0, speed: 2, skew: 3, spacing: 11, kind: 0, phase: 0, pt: 0, op: null, steps: 0, flash: 0, btn: false, + R: [9, 0, 0, 9], U: [1, 0, 0, 1], b: [9, 0, 0, 9], ca: 1, cb: 2, c: [0, 0, 0], + tile: [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], acc: new Float32Array(W * H * 3), old: new Float32Array(W * H * 3), xf: 1, + }; + return params.s; +} + +// Phases: 0 cross-fading in and showing the skewed basis, 1 reducing, 2 +// the reduced basis held, then the next lattice. +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 3, 11], d = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + S.speed = cl(k[1], 0.2, 3); + const skew = round(cl(k[2], 3, 7)), spacing = cl(k[3], 11, 22), btn = !!(input.btnPressed && input.btnPressed[0]); + if (skew !== S.skew || abs(spacing - S.spacing) > 0.05 || !S.started || (btn && !S.btn)) + (S.skew = skew), (S.spacing = spacing), (S.started = true), newLattice(S); + S.btn = btn; + const sc = 2 / S.speed; + S.t += d; + S.pt += d; + S.flash = max(0, S.flash - d / 0.7); + S.xf = max(0, S.xf - d / FADE); + if (S.phase === 0) { + if (S.pt >= FADE + LOOK * sc) (S.phase = 1), (S.pt = 0), (S.op = nextOp(S)); + } else if (S.phase === 1) { + if (!S.op) return void ((S.phase = 2), (S.pt = 0), (S.flash = 1)); + const dur = (S.op.swap ? 0.4 : 0.55) * sc; + if (S.pt >= dur + 0.2 * sc) { + const U = S.U; + if (S.op.swap) { + [U[0], U[1]] = [U[1], U[0]]; + [U[2], U[3]] = [U[3], U[2]]; + [S.ca, S.cb] = [S.cb, S.ca]; + } else (U[1] -= S.op.q * U[0]), (U[3] -= S.op.q * U[2]); + basis(S); + S.steps++; + S.pt = 0; + S.op = nextOp(S); + } + } else if (S.pt >= HOLD) newLattice(S); +} + +function put(acc, x, y, c, w) { + if (x < 0 || x >= W || y < 0 || y >= H || w <= 0) return; + const k = (y * W + x) * 3; + w = min(1, w); + for (let i = 0; i < 3; i++) acc[k + i] += (c[i] - acc[k + i]) * w; +} +function line(acc, x0, y0, x1, y1, c, w) { + const st = abs(x1 - x0) < abs(y1 - y0); + if (st) [x0, y0, x1, y1] = [y0, x0, y1, x1]; + if (x1 < x0) [x0, y0, x1, y1] = [x1, y1, x0, y0]; + const s = x1 > x0 ? (y1 - y0) / (x1 - x0) : 0; + for (let x = max(0, round(x0)); x <= min(st ? H - 1 : W - 1, round(x1)); x++) { + const y = y0 + (x - x0) * s, iy = floor(y), f = y - iy; + if (st) put(acc, iy, x, c, w * (1 - f)), put(acc, iy + 1, x, c, w * f); + else put(acc, x, iy, c, w * (1 - f)), put(acc, x, iy + 1, c, w * f); + } +} +// An arrow from the origin along x, y: a shaft 1.5 LEDs wide and a head 6 +// long, in colour c over a dark rim 1 LED wide that keeps it off the tiles. +function arrow(acc, x, y, c) { + const l = sqrt(x * x + y * y); + if (l < 1) return; + const ux = x / l, uy = y / l, hl = min(6, l * 0.5), hw = hl * 0.45, sl = l - hl, k = hl / sqrt(hl * hl + hw * hw); + const x0 = max(0, floor(min(OX, OX + x) - 4)), x1 = min(W - 1, floor(max(OX, OX + x) + 5)); + const y0 = max(0, floor(min(OY, OY + y) - 4)), y1 = min(H - 1, floor(max(OY, OY + y) + 5)); + for (let py = y0; py <= y1; py++) + for (let px = x0; px <= x1; px++) { + const dx = px - OX, dy = py - OY, al = dx * ux + dy * uy, ac = abs(-dx * uy + dy * ux); + const ds = (al < 0 ? sqrt(al * al + ac * ac) : al > sl ? sqrt((al - sl) ** 2 + ac * ac) : ac) - 0.75; + const dh = max(sl - al, al - l, (ac - (hw * (l - al)) / hl) * k); + const dd = min(ds, dh); + if (dd > 1.6) continue; + const kk = (py * W + px) * 3, w = cl(0.5 - dd, 0, 1), m = 1 - 0.6 * cl(1.6 - dd, 0, 1); + for (let i = 0; i < 3; i++) acc[kk + i] = acc[kk + i] * m * (1 - w) + c[i] * w; + } +} +function dot(acc, x, y, c, w) { + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + put(acc, ix, iy, c, w * min(1, 1.7 * (1 - fx) * (1 - fy))); + put(acc, ix + 1, iy, c, w * min(1, 1.7 * fx * (1 - fy))); + put(acc, ix, iy + 1, c, w * min(1, 1.7 * (1 - fx) * fy)); + put(acc, ix + 1, iy + 1, c, w * min(1, 1.7 * fx * fy)); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const acc = S.acc, b = S.b, c = S.c, tile = S.tile; + // The basis on screen: during a shear b2 slides along b1 by q b1. + let b1x = b[0], b1y = b[1], b2x = b[2], b2y = b[3], mix = 0; + if (S.phase === 1 && S.op) { + const sc = 2 / S.speed, u = smooth(cl(S.pt / ((S.op.swap ? 0.4 : 0.55) * sc), 0, 1)); + if (S.op.swap) mix = u; + else (b2x -= u * S.op.q * b1x), (b2y -= u * S.op.q * b1y); + } + const det = b1x * b2y - b1y * b2x, l1 = sqrt(len2(b1x, b1y)), l2 = sqrt(len2(b2x, b2y)); + // Tile heights across each family of edges, and how much edge they carry. + const h1 = abs(det) / l2, h2 = abs(det) / l1, e1 = cl((h1 - 1.2) / 2, 0, 1), e2 = cl((h2 - 1.2) / 2, 0, 1); + const fl = S.flash * S.flash; + for (let k = 0; k < 3; k++) hsv(S.hue + [0.5, 0.6, 0.73][k], 0.72 - 0.2 * fl, [0.56, 0.66, 0.76][k] + 0.12 * fl, tile[k]); + hsv(S.hue + 0.38, 0.8 - 0.3 * fl, 0.85 + 0.15 * fl, tile[3]); + // Each LED's place in the tiling: s along b1, t along b2. Tiles are + // coloured by (ca i + cb j) mod 3, which keeps every tile's colour when + // the arrows swap and never gives two tiles that share an edge one colour. + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const dx = x - OX, dy = y - OY, s = (dx * b2y - dy * b2x) / det, t = (b1x * dy - b1y * dx) / det; + const i = floor(s), j = floor(t), fs = s - i, ft = t - j; + const col = i === 0 && j === 0 ? tile[3] : tile[(((S.ca * i + S.cb * j) % 3) + 3) % 3]; + const ed = max(e1 * cl(1 - (min(fs, 1 - fs) * h1) / 0.7, 0, 1), e2 * cl(1 - (min(ft, 1 - ft) * h2) / 0.7, 0, 1)); + const m = 1 - 0.7 * ed, k = (y * W + x) * 3; + acc[k] = col[0] * m; + acc[k + 1] = col[1] * m; + acc[k + 2] = col[2] * m; + } + // The lattice points, from the reduced basis R: they never move. + const R = S.R, rd = R[0] * R[3] - R[1] * R[2], white = [1, 1, 1]; + let span = 0; + for (const [x, y] of [[-OX, -OY], [W - OX, -OY], [-OX, H - OY], [W - OX, H - OY]]) { + span = max(span, abs((x * R[3] - y * R[2]) / rd) + 1, abs((R[0] * y - R[1] * x) / rd) + 1); + } + span = min(60, ceilInt(span)); + for (let m = -span; m <= span; m++) + for (let n = -span; n <= span; n++) { + const x = OX + m * R[0] + n * R[2], y = OY + m * R[1] + n * R[3]; + if (x > -1 && x < W && y > -1 && y < H) dot(acc, x, y, white, 0.95); + } + // The arrows; during a swap their colours cross over. + const a1 = [0, 0, 0], a2 = [0, 0, 0]; + hsv(S.hue + 0.1, 0.85, 1, a1); + hsv(S.hue + 0.92, 0.6, 1, a2); + const ma = [0, 1, 2].map((k) => a1[k] + (a2[k] - a1[k]) * mix), mb = [0, 1, 2].map((k) => a2[k] + (a1[k] - a2[k]) * mix); + // During a shear, the path of b2's tip along b1, through the dots. + if (S.phase === 1 && S.op && !S.op.swap) { + const c2 = mb.map((v) => v * 0.8); + line(acc, OX + b[2], OY + b[3], OX + b2x, OY + b2y, c2, 0.8); + } + arrow(acc, b2x, b2y, mb); + arrow(acc, b1x, b1y, ma); + dot(acc, OX, OY, white, 1); + const xf = smooth(S.xf), old = S.old; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const k = (y * W + x) * 3; + for (let i = 0; i < 3; i++) acc[k + i] = min(1, acc[k + i]); + const r = acc[k] + (old[k] - acc[k]) * xf, gr = acc[k + 1] + (old[k + 1] - acc[k + 1]) * xf, bl = acc[k + 2] + (old[k + 2] - acc[k + 2]) * xf; + display.setPixel(x, y, (255 * r) | 0, (255 * gr) | 0, (255 * bl) | 0); + } +} +const ceilInt = (v) => -floor(-v); diff --git a/incoming/pattern-study/patterns/ruppert-refinement.js b/incoming/pattern-study/patterns/ruppert-refinement.js new file mode 100644 index 000000000..3cc6b0357 --- /dev/null +++ b/incoming/pattern-study/patterns/ruppert-refinement.js @@ -0,0 +1,553 @@ +// ===== Patternflow pattern ===== +// Title: Ruppert Refinement +// Family: cells +// Idea: A polygon with notches and holes starts as the constrained Delaunay triangulation of its corners, a panel of magenta needles, and Ruppert's refinement mends it one point at a time: an encroached boundary segment is split at its midpoint, else the worst triangle shows its circumcircle and a point goes in at the centre, or the segment that centre would encroach is split instead, Lawson flips restore the Delaunay mesh, and each triangle wears the colour of its smallest angle until the domain is green, teal and blue. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Delaunay refinement (Jim Ruppert, "A Delaunay refinement algorithm for quality 2-dimensional mesh generation", J. Algorithms, 1995), Lawson flips (Charles L. Lawson, 1977), size limit as in Triangle (Jonathan Shewchuk, 1996), pattern round 72 +// Round: 72 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Bound=20..33, Domain=0..5 +// +// Knob 1: Hue. Turns the angle colours round the wheel. At 0 needles are magenta, triangles halfway to the bound red, those just under it amber, and triangles past it green, then teal, then blue at 60 degrees; at 0.5 needles go green and the finished mesh magenta, red and orange. +// Knob 2: Speed. Steps a second: 0.8 times Speed at first, and 1 + n / 24 times that after n new points, since later steps are smaller. At 2 the first steps come 1.6 a second and the opening domain is finished after 15 s; at 0.2 one step every 6 s, each circle in plain view. +// Knob 3: Bound. The smallest angle a triangle may keep, 20 to 33 degrees. Triangles below it are coloured magenta to amber and get refined. Ruppert proved that the refinement stops for bounds up to 20.7 degrees when no two segments meet at less than 90, as in all six domains here; past 20.7 nothing is proved and above about 30 some inputs never finish, but these six do: 35 to 54 new points at 20, 58 to 166 at 33. Turning it up on a finished mesh starts the refinement again; turning it down only recolours it. +// Knob 4: Domain. Which of six domains comes first: a slab with crenels top and bottom, a slab with slots and a diamond hole, an octagon with three small diamond holes at each end, a slab with notches 40 LEDs deep, a slab with a slot 63 LEDs tall, and a staircase. Each finished mesh holds 4 s and hands on to the next domain. +// Buttons: K1 tap starts the next domain. + +// Ruppert (1995): start from a constrained Delaunay triangulation of the +// input segments. While some segment has a vertex inside its diametral +// circle (the circle it is a diameter of), split it at its midpoint. +// Otherwise take a triangle whose smallest angle is below the bound and add +// its circumcentre, unless that centre would encroach a segment: then split +// the segment and leave the centre out. Each new point is joined to the +// triangle (or the two triangles) it falls in and Lawson flips restore the +// Delaunay property. The boundary sides are the segments and never flip. +// The worst triangle goes first. Once none is below the bound, a triangle +// whose circumcircle is wider than 22 LEDs is split the same way, a size +// limit like Triangle's, so the open middle ends as a mesh too. + +const W = 64, H = 128; +const { floor, ceil, min, max, abs, sqrt, atan2, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const frac = (v) => v - floor(v); +const DEG = PI / 180; + +const NMAX = 700, TMAX = 2 * NMAX + 16; +// Triangles with a circumradius under 1.1 LEDs are left alone; no domain +// gets there even at 33 degrees. +const RMIN = 1.1, RMAX = 11, INTRO = 0.7, HOLD = 4, FADE = 0.6; +const FV = 0.84, FS = 0.88; + +// The domains in LEDs, outer ring first, then the holes. Every corner is 90 +// degrees or more. Most corners sit near the ends, so the first +// triangulation is needles from end to end. +const rect = (x0, y0, x1, y1) => [x0, y0, x1, y0, x1, y1, x0, y1]; +const diamond = (x, y, r) => [x, y - r, x + r, y, x, y + r, x - r, y]; +// A rectangle 3..60 by 3..124 with notches d deep cut into its top edge at +// the x spans in top and into its bottom edge at those in bot. +function notched(top, bot, d) { + const out = [3, 3]; + for (let k = 0; k < top.length; k += 2) out.push(top[k], 3, top[k], 3 + d, top[k + 1], 3 + d, top[k + 1], 3); + out.push(60, 3, 60, 124); + for (let k = bot.length - 2; k >= 0; k -= 2) out.push(bot[k + 1], 124, bot[k + 1], 124 - d, bot[k], 124 - d, bot[k], 124); + out.push(3, 124); + return out; +} +// Stairs: five steps 11 or 12 LEDs wide and 4 high down along the top and +// five up along the bottom. +const STEPS = [3, 14, 26, 37, 49, 60]; +function stairs() { + const out = [3, 3]; + for (let k = 0; k < 5; k++) out.push(STEPS[k + 1], 3 + 4 * k, STEPS[k + 1], 7 + 4 * k); + out.length -= 2; + out.push(60, 124); + for (let k = 0; k < 5; k++) out.push(STEPS[4 - k], 124 - 4 * k, STEPS[4 - k], 120 - 4 * k); + out.length -= 2; + return out; +} +const DOMAINS = [ + [notched([9, 16, 23, 30, 37, 44, 51, 56], [7, 13, 20, 27, 34, 41, 48, 55], 6)], + [notched([14, 19, 29, 34, 44, 49], [21, 26, 37, 42], 16), diamond(31.5, 32, 5)], + [[17, 3, 46, 3, 60, 17, 60, 110, 46, 124, 17, 124, 3, 110, 3, 17], diamond(19, 17, 3.5), diamond(31.5, 17, 3.5), diamond(44, 17, 3.5), diamond(19, 110, 3.5), diamond(31.5, 110, 3.5), diamond(44, 110, 3.5)], + [notched([10, 18, 45, 53], [22, 41], 40), rect(29, 16, 34, 28)], + [notched([20, 43], [8, 16, 47, 55], 8), rect(28, 32, 35, 95)], + [stairs()], +]; + +function hsv(h, s, v, out) { + h = frac(h) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} +// The colour of a smallest angle m: below the bound from magenta through red +// to amber, from the bound to 60 degrees green through teal to blue. +function angleHue(S, m) { + if (m < S.bound) return S.hue + 0.84 + 0.27 * (m / S.bound); + return S.hue + 0.31 + 0.31 * cl((m - S.bound) / (PI / 3 - S.bound), 0, 1); +} + +// ---- Predicates. Every stored triangle has orient > 0. ---- +const orient = (P, a, b, c) => + (P[2 * b] - P[2 * a]) * (P[2 * c + 1] - P[2 * a + 1]) - (P[2 * b + 1] - P[2 * a + 1]) * (P[2 * c] - P[2 * a]); +function incircle(P, a, b, c, d) { + const dx = P[2 * d], dy = P[2 * d + 1]; + const ax = P[2 * a] - dx, ay = P[2 * a + 1] - dy, bx = P[2 * b] - dx, by = P[2 * b + 1] - dy; + const cx = P[2 * c] - dx, cy = P[2 * c + 1] - dy; + const a2 = ax * ax + ay * ay, b2 = bx * bx + by * by, c2 = cx * cx + cy * cy; + return ax * (by * c2 - b2 * cy) - ay * (bx * c2 - b2 * cx) + a2 * (bx * cy - by * cx); +} +function angle(P, a, b, c) { + const ux = P[2 * b] - P[2 * a], uy = P[2 * b + 1] - P[2 * a + 1], vx = P[2 * c] - P[2 * a], vy = P[2 * c + 1] - P[2 * a + 1]; + return atan2(abs(ux * vy - uy * vx), ux * vx + uy * vy); +} +function minAngle(S, t) { + const P = S.P, V = S.V, a = V[3 * t], b = V[3 * t + 1], c = V[3 * t + 2]; + return min(angle(P, a, b, c), angle(P, b, c, a), angle(P, c, a, b)); +} +function circum(P, a, b, c, out) { + const ax = P[2 * a], ay = P[2 * a + 1]; + const bx = P[2 * b] - ax, by = P[2 * b + 1] - ay, cx = P[2 * c] - ax, cy = P[2 * c + 1] - ay; + const d = 2 * (bx * cy - by * cx), b2 = bx * bx + by * by, c2 = cx * cx + cy * cy; + const ux = (cy * b2 - by * c2) / d, uy = (bx * c2 - cx * b2) / d; + out[0] = ax + ux; + out[1] = ay + uy; + out[2] = sqrt(ux * ux + uy * uy); +} + +// ---- The starting triangulation: the holes bridged into the outer ring, +// ears clipped, then Lawson flips to the constrained Delaunay one. ---- +function addPoint(S, x, y) { + const i = S.np++; + S.P[2 * i] = x; + S.P[2 * i + 1] = y; + S.pb[i] = S.t; + return i; +} +function build(S, d) { + const P = S.P, rings = [], edges = []; + S.np = 0; + for (const R of DOMAINS[d]) { + let ids = [], area = 0; + for (let k = 0; k < R.length; k += 2) ids.push(addPoint(S, R[k], R[k + 1])); + for (let k = 0, n = R.length; k < n; k += 2) area += R[k] * R[(k + 3) % n] - R[(k + 2) % n] * R[k + 1]; + if ((area > 0) !== !rings.length) ids = ids.reverse(); + rings.push(ids); + for (let k = 0; k < ids.length; k++) edges.push(ids[k], ids[(k + 1) % ids.length]); + } + S.n0 = S.np; + let L = rings[0]; + const holes = rings.slice(1).map((h) => { + let m = 0; + for (let k = 1; k < h.length; k++) if (P[2 * h[k]] > P[2 * h[m]]) m = k; + return { h, m }; + }); + holes.sort((p, q) => P[2 * q.h[q.m]] - P[2 * p.h[p.m]]); + for (const { h, m } of holes) { + const v = h[m]; + let best = -1, bd = 1e9; + for (let k = 0; k < L.length; k++) { + const o = L[k], dd = (P[2 * o] - P[2 * v]) ** 2 + (P[2 * o + 1] - P[2 * v + 1]) ** 2; + if (dd >= bd) continue; + let ok = true; + for (let j = 0; j < edges.length && ok; j += 2) { + const p = edges[j], q = edges[j + 1]; + if (p === o || q === o || p === v || q === v) continue; + ok = !(orient(P, o, v, p) * orient(P, o, v, q) < 0 && orient(P, p, q, o) * orient(P, p, q, v) < 0); + } + if (ok) (best = k), (bd = dd); + } + const ring = h.slice(m).concat(h.slice(0, m + 1)); + L = L.slice(0, best + 1).concat(ring, L.slice(best)); + } + const tris = []; + while (L.length > 3) { + const n = L.length; + let found = -1; + for (let pass = 0; pass < 2 && found < 0; pass++) + for (let i = 0; i < n && found < 0; i++) { + const a = L[(i + n - 1) % n], v = L[i], b = L[(i + 1) % n]; + if (orient(P, a, v, b) <= 1e-9) continue; + const e = pass ? 1e-9 : -1e-9; + let ok = true; + for (let j = 0; j < n && ok; j++) { + const p = L[j]; + if (p === a || p === v || p === b) continue; + ok = !(orient(P, a, v, p) > e && orient(P, v, b, p) > e && orient(P, b, a, p) > e); + } + if (ok) found = i; + } + if (found < 0) break; + tris.push(L[(found + n - 1) % n], L[found], L[(found + 1) % n]); + L.splice(found, 1); + } + if (L.length === 3) tris.push(...L); + link(S, tris); + for (let pass = 0, any = true; any && pass < 100; pass++) { + any = false; + for (let e = 0; e < 3 * S.T; e++) if (S.Nb[e] > ((e / 3) | 0) && illegal(S, e)) flip(S, e), (any = true); + } +} +// Nb[3t + i] is the triangle across t's side opposite its vertex i, -1 on +// the boundary: the boundary sides are exactly the segments. +function link(S, tris) { + const V = S.V, Nb = S.Nb, T = (S.T = tris.length / 3), seen = new Map(); + for (let i = 0; i < tris.length; i++) V[i] = tris[i]; + Nb.fill(-1); + S.tb.fill(-9); + for (let e = 0; e < 3 * T; e++) { + const t = (e / 3) | 0, b = V[3 * t + ((e + 1) % 3)], c = V[3 * t + ((e + 2) % 3)]; + const key = min(b, c) * 4096 + max(b, c), o = seen.get(key); + if (o === undefined) seen.set(key, e); + else (Nb[e] = (o / 3) | 0), (Nb[o] = t); + } +} + +// ---- Edges and flips. Edge e = 3t + i is t's side opposite vertex i. ---- +function quad(S, e, q) { + const V = S.V, t = (e / 3) | 0, i = e % 3, u = S.Nb[e]; + if (u < 0) return false; + let j = 0; + while (S.Nb[3 * u + j] !== t) j++; + q.splice(0, 6, V[e], V[3 * t + ((i + 1) % 3)], V[3 * t + ((i + 2) % 3)], V[3 * u + j], u, j); + return true; +} +function illegal(S, e) { + const q = S.q, P = S.P; + if (!quad(S, e, q)) return false; + const [a, b, c, d] = q; + return incircle(P, a, b, c, d) > 1e-6 && orient(P, a, b, d) > 0 && orient(P, d, c, a) > 0; +} +// t = a b c and u = d c b become t = a b d and u = d c a. +function flip(S, e) { + const V = S.V, Nb = S.Nb, q = S.q; + quad(S, e, q); + const [a, b, c, d, u, j] = q, t = (e / 3) | 0, i = e % 3; + const nca = Nb[3 * t + ((i + 1) % 3)], nab = Nb[3 * t + ((i + 2) % 3)]; + const nbd = Nb[3 * u + ((j + 1) % 3)], ndc = Nb[3 * u + ((j + 2) % 3)]; + V.set([a, b, d], 3 * t); + Nb.set([nbd, u, nab], 3 * t); + V.set([d, c, a], 3 * u); + Nb.set([nca, t, ndc], 3 * u); + for (let k = 0; k < 3; k++) { + if (nbd >= 0 && Nb[3 * nbd + k] === u) Nb[3 * nbd + k] = t; + if (nca >= 0 && Nb[3 * nca + k] === t) Nb[3 * nca + k] = u; + } + S.tb[t] = S.tb[u] = S.t; +} +const repoint = (S, n, from, to) => { + for (let k = 0; n >= 0 && k < 3; k++) if (S.Nb[3 * n + k] === from) S.Nb[3 * n + k] = to; +}; +function setTri(S, t, a, b, c, na, nb, nc) { + S.V.set([a, b, c], 3 * t); + S.Nb.set([na, nb, nc], 3 * t); + S.tb[t] = S.t; +} +// Lawson: flip the side facing p while it is illegal, then the two sides +// facing p of the triangles the flip made. +function legalize(S, p, stack) { + for (let guard = 0; stack.length && guard < 4000; guard++) { + const t = stack.pop(); + let i = 0; + while (i < 3 && S.V[3 * t + i] !== p) i++; + if (i === 3) continue; + const u = S.Nb[3 * t + i]; + if (u >= 0 && illegal(S, 3 * t + i)) flip(S, 3 * t + i), stack.push(t, u); + } +} +// p inside t = a b c: three triangles round p. +function insertIn(S, t, p) { + const V = S.V, Nb = S.Nb, a = V[3 * t], b = V[3 * t + 1], c = V[3 * t + 2]; + const n0 = Nb[3 * t], n1 = Nb[3 * t + 1], n2 = Nb[3 * t + 2], t2 = S.T, t3 = S.T + 1; + S.T += 2; + setTri(S, t, a, b, p, t2, t3, n2); + setTri(S, t2, b, c, p, t3, t, n0); + setTri(S, t3, c, a, p, t, t2, n1); + repoint(S, n0, t, t2); + repoint(S, n1, t, t3); + legalize(S, p, [t, t2, t3]); +} +// p on side e of t (opposite a, from b to c). On a segment t splits in two; +// inside, the triangle across splits too. +function insertOn(S, e, p) { + const V = S.V, Nb = S.Nb, t = (e / 3) | 0, i = e % 3; + const a = V[e], b = V[3 * t + ((i + 1) % 3)], c = V[3 * t + ((i + 2) % 3)]; + const nca = Nb[3 * t + ((i + 1) % 3)], nab = Nb[3 * t + ((i + 2) % 3)], u = Nb[e], t2 = S.T++; + if (u < 0) { + setTri(S, t, a, b, p, -1, t2, nab); + setTri(S, t2, a, p, c, -1, nca, t); + repoint(S, nca, t, t2); + return legalize(S, p, [t, t2]); + } + let j = 0; + while (Nb[3 * u + j] !== t) j++; + const d = V[3 * u + j], nbd = Nb[3 * u + ((j + 1) % 3)], ndc = Nb[3 * u + ((j + 2) % 3)], u2 = S.T++; + setTri(S, t, a, b, p, u2, t2, nab); + setTri(S, t2, a, p, c, u, nca, t); + setTri(S, u, d, c, p, t2, u2, ndc); + setTri(S, u2, d, p, b, t, nbd, u); + repoint(S, nca, t, t2); + repoint(S, nbd, u, u2); + legalize(S, p, [t, t2, u, u2]); +} + +// ---- Ruppert's tests. ---- +const segEnds = (S, e) => [S.V[3 * ((e / 3) | 0) + ((e + 1) % 3)], S.V[3 * ((e / 3) | 0) + ((e + 2) % 3)]]; +// The longest segment with a point from p0 up to p1 inside its diametral +// circle, or -1. Segments under 2.2 LEDs are not split again. +function encroached(S, p0, p1) { + const P = S.P; + let best = -1, bl = 0; + for (let e = 0; e < 3 * S.T; e++) { + if (S.Nb[e] >= 0) continue; + const [a, b] = segEnds(S, e), l = (P[2 * a] - P[2 * b]) ** 2 + (P[2 * a + 1] - P[2 * b + 1]) ** 2; + if (l < 4 * RMIN * RMIN || l <= bl) continue; + for (let p = p0; p < p1; p++) { + const v = (P[2 * p] - P[2 * a]) * (P[2 * p] - P[2 * b]) + (P[2 * p + 1] - P[2 * a + 1]) * (P[2 * p + 1] - P[2 * b + 1]); + if (v < -1e-7 && p !== a && p !== b) (bl = l), (best = e); + } + } + return best; +} +// The triangle with the smallest angle below the bound, or when there is +// none the one with the widest circle past RMAX, or -1. +function worst(S) { + let best = -1, bm = S.bound, br = RMAX; + for (let t = 0; t < S.T; t++) { + const m = minAngle(S, t); + if (m >= bm) continue; + circum(S.P, S.V[3 * t], S.V[3 * t + 1], S.V[3 * t + 2], S.cc); + if (S.cc[2] >= RMIN) (best = t), (bm = m); + } + for (let t = 0; t < S.T && best < 0; t++) { + circum(S.P, S.V[3 * t], S.V[3 * t + 1], S.V[3 * t + 2], S.cc); + if (S.cc[2] > br) (best = t), (br = S.cc[2]); + } + return best; +} +// Walk from t toward point p: the triangle that holds it as { t }, the side +// it lies on as { e }, or the segment in the way as { seg }. +function locate(S, t, p) { + const P = S.P, V = S.V; + for (let g = 0; g < 4 * S.T + 8; g++) { + let next = -1, on = -1; + for (let i = 0; i < 3 && next < 0; i++) { + const b = V[3 * t + ((i + 1) % 3)], c = V[3 * t + ((i + 2) % 3)]; + const o = orient(P, b, c, p) / sqrt((P[2 * b] - P[2 * c]) ** 2 + (P[2 * b + 1] - P[2 * c + 1]) ** 2); + if (o < -1e-7) next = i; + else if (o < 1e-7) on = i; + } + const e = 3 * t + (next < 0 ? on : next); + if (next < 0 && on < 0) return { t }; + if (S.Nb[e] < 0) return { seg: e }; + if (next < 0) return { e }; + t = S.Nb[e]; + } + return null; +} + +// The next step, worked out before it is shown: { split } with the segment, +// and the rejected centre when there is one, or { t } and its centre. +function plan(S) { + const P = S.P, V = S.V, cc = S.cc; + if (S.np >= NMAX - 2) return null; + const seg = encroached(S, 0, S.np); + if (seg >= 0) return splitPlan(S, seg, null); + const t = worst(S); + if (t < 0) return null; + circum(P, V[3 * t], V[3 * t + 1], V[3 * t + 2], cc); + const tri = [V[3 * t], V[3 * t + 1], V[3 * t + 2]], at = { t, tri, cx: cc[0], cy: cc[1], r: cc[2] }; + // The centre waits in the next free slot while it is tested. + P[2 * S.np] = cc[0]; + P[2 * S.np + 1] = cc[1]; + const by = encroached(S, S.np, S.np + 1); + if (by >= 0) return splitPlan(S, by, at); + const where = locate(S, t, S.np); + if (!where) return null; + if (where.seg !== undefined) return splitPlan(S, where.seg, at); + return Object.assign(at, { where }); +} +function splitPlan(S, e, from) { + const P = S.P, [a, b] = segEnds(S, e); + const mx = (P[2 * a] + P[2 * b]) / 2, my = (P[2 * a + 1] + P[2 * b + 1]) / 2; + return { split: e, a, b, mx, my, sr: sqrt((P[2 * a] - P[2 * b]) ** 2 + (P[2 * a + 1] - P[2 * b + 1]) ** 2) / 2, from }; +} +function execute(S, pl) { + if (pl.split !== undefined) return insertOn(S, pl.split, addPoint(S, pl.mx, pl.my)); + const p = addPoint(S, pl.cx, pl.cy); + if (pl.where.t !== undefined) insertIn(S, pl.where.t, p); + else insertOn(S, pl.where.e, p); +} + +function restart(S, d) { + S.dom = d; + build(S, d); + Object.assign(S, { phase: 0, pt: 0, u: 0, pl: null }); +} + +export function setup(params) { + params.s = { + t: 0, hue: 0, speed: 2, bound: 20 * DEG, dom: -1, k4: -1, btn: false, n0: 0, np: 0, T: 0, + P: new Float64Array(2 * NMAX), pb: new Float32Array(NMAX), + V: new Int32Array(3 * TMAX), Nb: new Int32Array(3 * TMAX), tb: new Float32Array(TMAX), + phase: 0, pt: 0, u: 0, pl: null, q: [0, 0, 0, 0, 0, 0], cc: [0, 0, 0], + acc: new Float32Array(W * H * 3), c: [0, 0, 0], + }; + return params.s; +} + +// Phases: 0 the corners' triangulation fades in, 1 refining, 2 holding the +// finished mesh, 3 fading out to the next domain. +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 20, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + S.speed = cl(k[1], 0.2, 3); + const bound = cl(k[2], 20, 33) * DEG, k4 = floor(cl(k[3], 0, 5.999)); + const btn = !!(input.btnPressed && input.btnPressed[0]); + if (k4 !== S.k4) (S.k4 = k4), restart(S, k4); + else if (btn && !S.btn) restart(S, (S.dom + 1) % DOMAINS.length); + S.btn = btn; + if (abs(bound - S.bound) > 1e-6) { + S.bound = bound; + if (S.phase === 1 && S.u < 0.5) S.pl = plan(S); + if (S.phase === 2) (S.phase = 1), (S.u = 0), (S.pl = plan(S)); + } + S.t += d; + S.pt += d; + if (S.phase === 0) { + if (S.pt >= INTRO) (S.phase = 1), (S.pt = 0), (S.u = 0), (S.pl = plan(S)); + return; + } + if (S.phase === 2) { + if (S.pt >= HOLD) (S.phase = 3), (S.pt = 0); + return; + } + if (S.phase === 3) { + if (S.pt >= FADE) restart(S, (S.dom + 1) % DOMAINS.length); + return; + } + const rate = 0.8 * S.speed * (1 + (S.np - S.n0) / 24); + let left = d * rate; + while (left > 0 && S.phase === 1) { + if (!S.pl) { + (S.phase = 2), (S.pt = 0); + break; + } + const step = min(left, 1 - S.u); + if (S.u < 0.5 && S.u + step >= 0.5) execute(S, S.pl); + S.u += step; + left -= step; + if (S.u >= 1 - 1e-9) (S.u = 0), (S.pl = plan(S)); + } +} + +// ---- Drawing into acc, a float RGB buffer. ---- +function fill(S, t, c) { + const P = S.P, V = S.V, acc = S.acc, xs = [0, 0, 0], ys = [0, 0, 0]; + for (let k = 0; k < 3; k++) (xs[k] = P[2 * V[3 * t + k]]), (ys[k] = P[2 * V[3 * t + k] + 1]); + for (let y = max(0, ceil(min(ys[0], ys[1], ys[2]))); y <= min(H - 1, floor(max(ys[0], ys[1], ys[2]))); y++) { + let lo = W, hi = -1; + for (let e = 0; e < 3; e++) { + const ya = ys[e], yb = ys[(e + 1) % 3]; + if ((ya - y) * (yb - y) > 0 || ya === yb) continue; + const x = xs[e] + ((y - ya) * (xs[(e + 1) % 3] - xs[e])) / (yb - ya); + (lo = min(lo, x)), (hi = max(hi, x)); + } + for (let x = max(0, ceil(lo - 0.5)); x <= min(W - 1, floor(hi + 0.5)); x++) acc.set(c, (y * W + x) * 3); + } +} +// Mode 0 darkens what is there by w, mode 1 moves it toward c by w. +function put(acc, x, y, c, w, mode) { + if (x < 0 || x >= W || y < 0 || y >= H || w <= 0) return; + const k = (y * W + x) * 3; + w = min(1, w); + for (let i = 0; i < 3; i++) acc[k + i] = mode ? acc[k + i] + (c[i] - acc[k + i]) * w : acc[k + i] * (1 - c[i] * w); +} +function line(acc, x0, y0, x1, y1, c, w, mode) { + const st = abs(x1 - x0) < abs(y1 - y0); + if (st) [x0, y0, x1, y1] = [y0, x0, y1, x1]; + if (x1 < x0) [x0, y0, x1, y1] = [x1, y1, x0, y0]; + const s = x1 > x0 ? (y1 - y0) / (x1 - x0) : 0; + for (let x = max(0, ceil(x0)); x <= min(st ? H - 1 : W - 1, floor(x1)); x++) { + const y = y0 + (x - x0) * s, iy = floor(y), f = y - iy; + if (st) put(acc, iy, x, c, w * (1 - f), mode), put(acc, iy + 1, x, c, w * f, mode); + else put(acc, x, iy, c, w * (1 - f), mode), put(acc, x, iy + 1, c, w * f, mode); + } +} +// A circle: by rows where it runs steep, by columns where it runs flat. +function ring(acc, cx, cy, r, c, w) { + if (!(r > 0.5 && r < 1e4)) return; + const h = r * 0.7072; + for (let ax = 0; ax < 2; ax++) { + const a0 = ax ? cx : cy, b0 = ax ? cy : cx, n = ax ? W : H; + for (let u = max(0, ceil(a0 - h)); u <= min(n - 1, floor(a0 + h)); u++) { + const dv = sqrt(max(0, r * r - (u - a0) ** 2)); + for (const v of [b0 - dv, b0 + dv]) { + const iv = floor(v), f = v - iv; + if (ax) put(acc, u, iv, c, w * (1 - f), 1), put(acc, u, iv + 1, c, w * f, 1); + else put(acc, iv, u, c, w * (1 - f), 1), put(acc, iv + 1, u, c, w * f, 1); + } + } + } +} +function dot(acc, x, y, c, w) { + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + for (let k = 0; k < 4; k++) + put(acc, ix + (k & 1), iy + (k >> 1), c, w * min(1, 1.6 * (k & 1 ? fx : 1 - fx) * (k >> 1 ? fy : 1 - fy)), 1); +} +// The line from point a to point b. +const edge = (S, a, b, c, w, mode) => line(S.acc, S.P[2 * a], S.P[2 * a + 1], S.P[2 * b], S.P[2 * b + 1], c, w, mode); + +export function draw(display, params) { + const S = params.s; + if (!S || !S.T) return; + const P = S.P, V = S.V, Nb = S.Nb, acc = S.acc, c = S.c, pl = S.pl; + const white = [1, 1, 1], dark = [0.62, 0.62, 0.62]; + acc.fill(0); + // Fills by smallest angle, all at one brightness; a triangle just made + // starts pale and takes its colour over 0.4 s. + for (let t = 0; t < S.T; t++) { + const age = S.t - S.tb[t]; + hsv(angleHue(S, minAngle(S, t)), FS * (1 - 0.5 * exp(-age / 0.3)), FV, c); + fill(S, t, c); + } + // Inner edges darken what they cross; the segments are pale lines. + for (let e = 0; e < 3 * S.T; e++) { + const t = (e / 3) | 0, u = Nb[e]; + if (u >= 0 && u < t) continue; + const a = V[3 * t + ((e + 1) % 3)], b = V[3 * t + ((e + 2) % 3)]; + edge(S, a, b, u >= 0 ? dark : white, u >= 0 ? 1 : 0.85, u < 0); + } + // The step under way: before its midpoint the circle grows bright, after + // it the new point is in and the circle fades. + if (S.phase === 1 && pl) { + const g = S.u < 0.5 ? 0.25 + 1.5 * S.u : 1 - 1.6 * (S.u - 0.5); + const from = pl.split !== undefined ? pl.from : pl; + if (from) { + for (let k = 0; k < 3 && S.u < 0.5; k++) edge(S, from.tri[k], from.tri[(k + 1) % 3], white, g, 1); + ring(acc, from.cx, from.cy, from.r, white, 0.8 * g); + if (S.u < 0.5) dot(acc, from.cx, from.cy, pl.split !== undefined ? dark : white, g); + } + if (pl.split !== undefined) { + hsv(S.hue + 0.11, 0.5, 1, c); + ring(acc, pl.mx, pl.my, pl.sr, c, g); + if (S.u < 0.5) dot(acc, pl.mx, pl.my, white, g); + } + } + // New points shine white for a moment. + for (let i = S.n0; i < S.np; i++) { + const w = exp(-(S.t - S.pb[i]) / 0.5); + if (w > 0.02) dot(acc, P[2 * i], P[2 * i + 1], white, w); + } + const g = S.phase === 0 ? min(1, S.pt / 0.5) : S.phase === 3 ? max(0, 1 - S.pt / FADE) : 1; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const k = (y * W + x) * 3; + display.setPixel(x, y, (255 * g * min(1, acc[k])) | 0, (255 * g * min(1, acc[k + 1])) | 0, (255 * g * min(1, acc[k + 2])) | 0); + } +} From 2568319b668d0f6c75fc8a137d2969d3a0d3ac48 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 20:33:03 +0000 Subject: [PATCH 07/14] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2072=ED=8C=90:=20Biliteral=20Cipher=20=EA=B3=BC=20Autogram?= 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_01Bc4JkrPaZ8GQQiA22PWEvR --- incoming/pattern-study/patterns/autogram.js | 364 ++++++++++++++++++ .../patterns/biliteral-cipher.js | 308 +++++++++++++++ 2 files changed, 672 insertions(+) create mode 100644 incoming/pattern-study/patterns/autogram.js create mode 100644 incoming/pattern-study/patterns/biliteral-cipher.js diff --git a/incoming/pattern-study/patterns/autogram.js b/incoming/pattern-study/patterns/autogram.js new file mode 100644 index 000000000..28e60df4d --- /dev/null +++ b/incoming/pattern-study/patterns/autogram.js @@ -0,0 +1,364 @@ +// ===== Patternflow pattern ===== +// Title: Autogram +// Family: type +// Idea: A sentence that counts its own letters fills the panel in 3 by 5 capitals, and a checker goes through it letter by letter, lighting every occurrence of that letter in reading order while a tally climbs towards the number the sentence claims, so the claim word and the bar under the text turn true when the tally reaches it; with Doubt turned up some claims are changed first, the check finds them false, and the sentence is rewritten again and again with the numbers it actually has, faster and faster, until it either comes true or is caught going round a loop of hundreds of sentences. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: autograms, named by Lee Sallows, the first of his printed in Douglas Hofstadter's Metamagical Themas column in Scientific American, January 1982; the three sentences here were found for this pattern; 3 by 5 capitals from Typewriter (pattern round 18) and 5 by 7 figures from Photon Disk (pattern round 70); pattern round 72 +// Round: 72 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Sentence=0..2, Doubt=0..1 +// +// Knob 1: Hue. Moves the two colours together along the part of the wheel away from red, which is kept for a false claim: at 0 the counted letters are cyan and the claims amber, at 0.5 blue-violet and green, at 1 magenta and sea green; the sentence and the dark ground take a tint of the same two. +// Knob 2: Speed. At 2 the sentence is written in 1.8 s, 125 characters a second, and the checker takes 0.035 s an occurrence and 0.4 s a letter, so THIS SENTENCE HAS, 152 letters of 19 kinds, is checked in 13 s and held 3 s when true; the first rewrite takes 1.5 s and each after it 0.8 times as long, down to 240 a second. At 0.2 everything is ten times slower, at 3 half as fast again. +// Knob 3: Sentence. Which autogram comes first; the others follow in turn: 0 THIS SENTENCE HAS, 1 THIS PANEL SHOWS, 2 THESE LIGHTS SPELL. Turning it starts the new one at once. +// Knob 4: Doubt. How many claims are changed, by 1 to 3, before a sentence is checked: none below 0.125, so the opening 0.06 shows the sentence as found, one at 0.25, two at 0.5, three at 0.75 and four at 1. A changed claim changes the letters of its own number word, so more claims than that come out false. + +// Each sentence lists, for every letter it uses, how many times that letter +// appears in it, counting letters only, the S of each 'S included. The +// three were found by a search for this pattern that starts from random +// claims, writes the sentence, counts it and copies some of the counts back +// into the claims, with restarts, until a sentence comes out true of itself; +// setup counts each one again and stops if a claim is wrong. +// +// Copying every count back at once, which is what the rewrite here does, +// almost never finds one. In 1200 tries from the first sentence with one +// to four claims moved by 1 to 3, the rewriting was caught in a loop of +// 453 sentences 1042 times, of 397 sentences 109 times and of 127 +// sentences 46 times, half of them within 760 rewrites and all within 1320, +// and came true 3 times: twice as the sentence it started from, once as an +// autogram nobody had written. From the second sentence the loops were 257, +// 75, 306, 172, 21 and 12 sentences long (all within 2220 rewrites), from +// the third 415, 94, 28 and 35. The chase gives up at 3000. + +const W = 64, H = 128; +const { floor, min, max, round, abs, sin, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +const SENTENCES = [ + "THIS SENTENCE HAS THREE A'S, TWO C'S, TWO D'S, TWENTY-NINE E'S, TWO F'S, THREE G'S, NINE H'S, EIGHT I'S, ONE L, SIXTEEN N'S, SEVEN O'S, SIX R'S, TWENTY-SEVEN S'S, EIGHTEEN T'S, TWO U'S, FOUR V'S, SEVEN W'S, THREE X'S AND THREE Y'S.", + "THIS PANEL SHOWS THREE A'S, TWO D'S, TWENTY-THREE E'S, TWO F'S, TWO G'S, NINE H'S, SIX I'S, THREE L'S, ELEVEN N'S, EIGHT O'S, TWO P'S, SEVEN R'S, TWENTY-SIX S'S, TWENTY T'S, TWO U'S, THREE V'S, TEN W'S, THREE X'S AND FOUR Y'S.", + "THESE LIGHTS SPELL TWO A'S, TWO D'S, TWENTY-FIVE E'S, THREE F'S, TWO G'S, SEVEN H'S, SIX I'S, FOUR L'S, TEN N'S, SEVEN O'S, TWO P'S, SIX R'S, THIRTY S'S, SEVENTEEN T'S, TWO U'S, SEVEN V'S, SEVEN W'S, THREE X'S AND THREE Y'S.", +]; +const ONES = ["ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE", "TEN", "ELEVEN", "TWELVE", "THIRTEEN", "FOURTEEN", "FIFTEEN", "SIXTEEN", "SEVENTEEN", "EIGHTEEN", "NINETEEN"]; +const TENS = ["", "", "TWENTY", "THIRTY", "FORTY", "FIFTY", "SIXTY", "SEVENTY", "EIGHTY", "NINETY"]; +const word = (n) => (n < 20 ? ONES[n] : TENS[floor(n / 10)] + (n % 10 ? "-" + ONES[n % 10] : "")); +const NUMBER = {}; +for (let n = 0; n < 100; n++) NUMBER[word(n)] = n; +const AZ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + +// ---- The sentence from its opening words and its claims, with where each +// claim sits: the number word, the letter and its 'S. +function build(pre, L, c) { + let text = pre + " "; + const item = []; + L.forEach((l, i) => { + if (i === L.length - 1) text += "AND "; + const a = text.length; + text += word(c[i]) + " " + l + (c[i] !== 1 ? "'S" : ""); + item.push([a, text.length]); + text += i === L.length - 1 ? "." : i === L.length - 2 ? " " : ", "; + }); + return { text, item }; +} +function tally(text, L) { + const n = new Array(26).fill(0); + for (let i = 0; i < text.length; i++) { + const k = text.charCodeAt(i) - 65; + if (k >= 0 && k < 26) n[k]++; + } + return L.map((l) => n[l.charCodeAt(0) - 65]); +} +// Parse a sentence back into opening words and claims, then check it. +const BOOK = SENTENCES.map((s, si) => { + const re = /([A-Z]+(?:-[A-Z]+)?) ([A-Z])(?:'S)?(?=[,.]| AND)/g; + const L = [], c = []; + let m, first = -1; + while ((m = re.exec(s))) { + if (!(m[1] in NUMBER)) continue; + if (first < 0) first = m.index; + L.push(m[2]), c.push(NUMBER[m[1]]); + } + const pre = s.slice(0, first - 1), b = build(pre, L, c), n = tally(s, L); + if (b.text !== s) throw new Error(`sentence ${si} does not rebuild`); + for (let i = 0; i < L.length; i++) if (n[i] !== c[i]) throw new Error(`sentence ${si} has ${n[i]} ${L[i]}, not ${c[i]}`); + for (const ch of AZ) if (s.includes(ch) && !L.includes(ch)) throw new Error(`sentence ${si} does not list ${ch}`); + return { pre, L, c }; +}); + +// ---- Faces. 3 x 5 capitals from Typewriter, one bit per LED, rows from +// the top; 5 x 7 capitals and figures from Photon Disk, and three signs. +const G35 = { + A: "010101111101101", B: "110101110101110", C: "011100100100011", D: "110101101101110", + E: "111100110100111", F: "111100110100100", G: "011100101101011", H: "101101111101101", + I: "111010010010111", J: "001001001101010", K: "101101110101101", L: "100100100100111", + M: "101111111101101", N: "110101101101101", O: "010101101101010", P: "110101110100100", + Q: "010101101111011", R: "110101110101101", S: "011100010001110", T: "111010010010010", + U: "101101101101011", V: "101101101010010", W: "101101111111101", X: "101101010101101", + Y: "101101010010010", Z: "111001010100111", ".": "000000000000010", ",": "000000000010100", + "-": "000000111000000", "'": "010010000000000", +}; +const G57 = { + A: ".###. #...# #...# ##### #...# #...# #...#", C: ".###. #...# #.... #.... #.... #...# .###.", D: "####. #...# #...# #...# #...# #...# ####.", + E: "##### #.... #.... ####. #.... #.... #####", F: "##### #.... #.... ####. #.... #.... #....", G: ".###. #...# #.... #.### #...# #...# .####", + H: "#...# #...# #...# ##### #...# #...# #...#", I: ".###. ..#.. ..#.. ..#.. ..#.. ..#.. .###.", L: "#.... #.... #.... #.... #.... #.... #####", + N: "#...# #...# ##..# #.#.# #..## #...# #...#", O: ".###. #...# #...# #...# #...# #...# .###.", P: "####. #...# #...# ####. #.... #.... #....", + R: "####. #...# #...# ####. #.#.. #..#. #...#", S: ".#### #.... #.... .###. ....# ....# ####.", T: "##### ..#.. ..#.. ..#.. ..#.. ..#.. ..#..", + U: "#...# #...# #...# #...# #...# #...# .###.", V: "#...# #...# #...# #...# #...# .#.#. ..#..", W: "#...# #...# #...# #.#.# #.#.# #.#.# .#.#.", + X: "#...# #...# .#.#. ..#.. .#.#. #...# #...#", Y: "#...# #...# .#.#. ..#.. ..#.. ..#.. ..#..", 0: ".###. #...# #..## #.#.# ##..# #...# .###.", + 1: "..#.. .##.. ..#.. ..#.. ..#.. ..#.. .###.", 2: ".###. #...# ....# ...#. ..#.. .#... #####", 3: "##### ...#. ..#.. ...#. ....# #...# .###.", + 4: "...#. ..##. .#.#. #..#. ##### ...#. ...#.", 5: "##### #.... ####. ....# ....# #...# .###.", 6: "..##. .#... #.... ####. #...# #...# .###.", + 7: "##### ....# ...#. ..#.. .#... .#... .#...", 8: ".###. #...# #...# .###. #...# #...# .###.", 9: ".###. #...# #...# .#### ....# ...#. .##..", + "<": "...#. ..#.. .#... #.... .#... ..#.. ...#.", ">": ".#... ..#.. ...#. ....# ...#. ..#.. .#...", "=": "..... ..... ##### ..... ##### ..... .....", +}; +for (const k in G57) G57[k] = G57[k].replace(/ /g, ""); + +const blend = (c1, c2, u) => [c1[0] + (c2[0] - c1[0]) * u, c1[1] + (c2[1] - c1[1]) * u, c1[2] + (c2[2] - c1[2]) * u]; +const T_TYPE = 0.008, T_IN = 0.1, T_OCC = 0.035, T_V = 0.3, T_TRUE = 3, T_LOOP = 4, T_R0 = 1.5, T_RMIN = 1 / 240, MAXR = 3000; + +function wrap(s, text) { + // Break at spaces, or after a hyphen; 16 letters of 4 LEDs a line. + const pos = new Int16Array(text.length * 2).fill(-1); + let col = 0, line = 0, i = 0; + while (i < text.length) { + let j = i; + while (j < text.length && text[j] !== " " && !(j > i && text[j - 1] === "-")) j++; + if (col > 0 && col + (j - i) > 16) (line++), (col = 0); + for (let k = i; k < j; k++) (pos[k * 2] = col++), (pos[k * 2 + 1] = line); + if (text[j] === " ") (col++), j++; + i = j; + } + s.pos = pos; + s.lines = line + 1; +} + +function setText(s, c) { + const b = BOOK[s.si], { text, item } = build(b.pre, b.L, c); + s.claim = c.slice(); + s.text = text; + s.item = item; + s.count = tally(text, b.L); + s.itemOf = new Int8Array(text.length).fill(-1); + s.litAt = new Float32Array(text.length).fill(-9); + item.forEach(([a, e], i) => { + for (let k = a; k < e; k++) s.itemOf[k] = i; + }); + wrap(s, text); +} + +function start(s, si, m) { + s.si = si; + s.m = m; + const b = BOOK[si], c = b.c.slice(); + const vol = []; + b.L.forEach((l, i) => "EFGHILNORSTUVWXY".includes(l) && vol.push(i)); + for (let k = 0; k < m && vol.length; k++) { + const i = vol.splice(floor(Math.random() * vol.length), 1)[0]; + let d = 0; + while (!d || c[i] + d < 1) d = (Math.random() < 0.5 ? -1 : 1) * (1 + floor(Math.random() * 3)); + c[i] += d; + } + setText(s, c); + s.changedAt = new Float32Array(s.text.length).fill(-9); + s.phase = 0; // 0 check, 1 true, 2 chase, 3 loop + s.t = 0; + s.li = 0; + s.sub = -1; // -1 writing, 0 intro, 1 count, 2 verdict + s.typed = 0; + s.k = 0; + s.done = new Int16Array(b.L.length).fill(-1); + s.occ = occ(s, 0); + s.round = 0; + s.seen = null; + s.born = s.clock; +} +function occ(s, li) { + const l = BOOK[s.si].L[li], o = []; + for (let i = 0; i < s.text.length; i++) if (s.text[i] === l) o.push(i); + return o; +} +const keyOf = (c) => c.join(","); + +export function setup(params) { + const s = (params.s = {}); + s.clock = 0; + s.kS = -1; + s.kM = -1; + s.fb = new Uint8ClampedArray(W * H * 3); + start(s, 0, 0); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const s = params.s, kn = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + s.hue = kn[0] || 0; + const rate = cl(kn[1] == null ? 2 : kn[1], 0.2, 3) / 2; + const si = round(cl(kn[2] || 0, 0, 2)), m = round(cl(kn[3] == null ? 0.06 : kn[3], 0, 1) * 4); + if (si !== s.kS || m !== s.kM) { + s.kS = si; + s.kM = m; + start(s, si, m); + } + s.clock += dt; + s.t += dt * rate; + const L = BOOK[s.si].L; + for (let guard = 0; guard < 24; guard++) { + if (s.phase === 0) { + if (s.sub === -1) { + const n = min(s.text.length - s.typed, floor(s.t / T_TYPE)); + s.typed += n; + s.t -= n * T_TYPE; + if (s.typed < s.text.length) break; + s.sub = 0; + } else if (s.sub === 0) { + if (s.t < T_IN) break; + s.t -= T_IN; + s.sub = 1; + } else if (s.sub === 1) { + if (s.k >= s.occ.length) { + s.sub = 2; + s.done[s.li] = s.k; + continue; + } + if (s.t < T_OCC) break; + s.t -= T_OCC; + s.litAt[s.occ[s.k++]] = s.clock; + } else { + if (s.t < T_V) break; + s.t -= T_V; + s.li++; + if (s.li >= L.length) { + s.t = 0; + if (s.done.every((n, i) => n === s.claim[i])) s.phase = 1; + else (s.phase = 2), (s.seen = new Map([[keyOf(s.claim), 0]])), (s.round = 0); + } else (s.sub = 0), (s.k = 0), (s.occ = occ(s, s.li)); + } + } else if (s.phase === 1 || s.phase === 3) { + if (s.t < (s.phase === 1 ? T_TRUE : T_LOOP)) break; + start(s, (s.si + 1) % 3, s.m); + } else { + const d = max(T_RMIN, T_R0 * Math.pow(0.8, s.round)); + if (s.t < d) break; + s.t -= d; + // Rewrite every claim with the count it has. + const prev = s.claim, c = s.count.slice(); + setText(s, c); + s.round++; + s.changedAt = new Float32Array(s.text.length).fill(-9); + s.item.forEach(([a, e], i) => { + if (c[i] !== prev[i]) for (let k = a; k < e; k++) s.changedAt[k] = s.clock; + }); + const key = keyOf(c); + if (s.count.every((n, i) => n === c[i])) (s.phase = 1), (s.t = 0), (s.verdict = "TRUE"); + else if (s.seen.has(key)) (s.phase = 3), (s.t = 0), (s.period = s.round - s.seen.get(key)); + else if (s.round >= MAXR) (s.phase = 3), (s.t = 0), (s.period = 0); + else s.seen.set(key, s.round); + } + } +} + +export function draw(display, params) { + const s = params.s; + if (!s) return; + const fb = s.fb, b = BOOK[s.si], L = b.L, nL = L.length; + // Both colours stay clear of red, which marks a false claim. + const hk = 0.11 + 0.34 * cl(s.hue, 0, 1), hc = hk + 0.41; + const CNT = hsv(hc, 0.8, 255), CLM = hsv(hk, 0.85, 255), TXT = hsv(hk, 0.3, 155), TONE = blend(TXT, CNT, 0.42), RED = [255, 34, 30], WHT = [255, 255, 255]; + const BG = hsv(hc, 0.7, 10); + for (let i = 0; i < W * H; i++) (fb[i * 3] = BG[0]), (fb[i * 3 + 1] = BG[1]), (fb[i * 3 + 2] = BG[2]); + const put = (x, y, c, a) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const i = (y * W + x) * 3; + fb[i] = c[0] * a; + fb[i + 1] = c[1] * a; + fb[i + 2] = c[2] * a; + }; + const fadeIn = cl((s.clock - s.born) / 0.4, 0, 1); + const pitch = s.lines * 6 <= 102 ? 6 : floor(102 / s.lines); + const checking = s.phase === 0, writing = checking && s.sub === -1, cur = checking && !writing ? s.li : -1; + // Which claims are known false. + const bad = new Uint8Array(nL); + for (let i = 0; i < nL; i++) { + if (checking) bad[i] = s.done[i] >= 0 && (i < s.li || s.sub === 2) && s.done[i] !== s.claim[i] ? 1 : 0; + else bad[i] = s.count[i] !== s.claim[i] ? 1 : 0; + } + const sweep = s.phase === 1 ? s.t * 60 : -99; + // ---- The sentence. + const shown = writing ? s.typed : s.text.length; + for (let ci = 0; ci < shown; ci++) { + const ch = s.text[ci], g = G35[ch]; + if (!g) continue; + const X = s.pos[ci * 2] * 4, Y = s.pos[ci * 2 + 1] * pitch; + const it = s.itemOf[ci], lit = s.litAt[ci]; + let col = lit > -9 ? TONE : TXT; + if (it >= 0 && bad[it]) col = RED; + if (it === cur && cur >= 0) col = CLM; + if (checking && ch === L[cur] && lit > -9) { + const age = s.clock - lit; + col = blend(CNT, WHT, cl(1 - age / 0.15, 0, 1) * 0.8); + if (s.sub === 2) col = blend(col, TONE, cl(s.t / T_V, 0, 1)); + } + if (writing && shown - ci < 6) col = blend(col, WHT, 1 - (shown - ci) / 6); + const ch2 = s.clock - s.changedAt[ci]; + if (ch2 < 0.35) col = blend(col, WHT, 1 - ch2 / 0.35); + if (sweep > -99) { + const d = abs(Y - sweep + 20); + if (d < 20) col = blend(col, CNT, (1 - d / 20) * 0.85); + } + for (let y = 0; y < 5; y++) + for (let x = 0; x < 3; x++) if (g.charCodeAt(y * 3 + x) === 49) put(X + x, Y + y, col, fadeIn); + } + // ---- The tally line: letter, count, sign, claim; or the round; or the + // verdict. + let line = []; + if (writing) line = []; + else if (checking) { + const n = s.sub === 2 ? s.done[cur] : s.k, c = s.claim[cur]; + const sign = n < c ? "<" : n > c ? ">" : "="; + const sc = s.sub === 2 ? (n === c ? CNT : RED) : n > c ? RED : WHT; + line = [[L[cur], TXT], [" ", TXT], ...[...String(n).padStart(2, " ")].map((d) => [d, CNT]), [sign, sc], ...[...String(c)].map((d) => [d, CLM])]; + } else if (s.phase === 2) line = s.round ? [..."ROUND "].map((d) => [d, TXT]).concat([...String(s.round)].map((d) => [d, CLM])) : [..."FALSE"].map((d) => [d, RED]); + else if (s.phase === 1) line = [..."TRUE"].map((d) => [d, CNT]); + else if (s.period) line = [..."LOOP "].map((d) => [d, RED]).concat([...String(s.period)].map((d) => [d, CLM])); + else line = [..."ROUND "].map((d) => [d, TXT]).concat([...String(s.round)].map((d) => [d, RED])); + const x0 = floor((W - (line.length * 6 - 1)) / 2); + line.forEach(([d, col], k) => { + const g = G57[d]; + if (!g) return; + for (let y = 0; y < 7; y++) for (let x = 0; x < 5; x++) if (g[y * 5 + x] === "#") put(x0 + k * 6 + x, 104 + y, col, fadeIn); + }); + // ---- The bars: a dim column as tall as each claim, a bright one as tall + // as the count so far, half an LED a letter. + const bx = floor((W - (nL * 3 - 1)) / 2); + for (let i = 0; i < nL; i++) { + const c = s.claim[i]; + let n = s.count[i]; + if (checking) n = i < cur ? s.done[i] : i === cur ? (s.sub === 2 ? s.done[i] : s.k) : 0; + const hi = i === cur ? 1 : 0.75; + const fill = bad[i] || (checking && i === cur && n > c) ? RED : CNT; + if (writing && s.item[i][1] > s.typed) continue; + const top = Math.ceil(c / 2) - 1; + for (let r = 0; r < 16; r++) { + const nn = cl(n / 2 - r, 0, 1); + let col = null, a = 0; + if (r === top) (col = n === c ? blend(CNT, WHT, 0.5) : n > c ? RED : CLM), (a = i === cur ? 1 : 0.8); + else if (nn > 0) (col = fill), (a = (0.35 + 0.65 * nn) * hi); + else if (r < top) (col = CLM), (a = i === cur ? 0.16 : 0.09); + if (col) for (let x = 0; x < 2; x++) put(bx + i * 3 + x, 127 - r, col, a * fadeIn); + } + if (c > 32 || n > 32) for (let x = 0; x < 2; x++) put(bx + i * 3 + x, 111, RED, fadeIn); + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = (y * W + x) * 3; + display.setPixel(x, y, fb[i] | 0, fb[i + 1] | 0, fb[i + 2] | 0); + } +} diff --git a/incoming/pattern-study/patterns/biliteral-cipher.js b/incoming/pattern-study/patterns/biliteral-cipher.js new file mode 100644 index 000000000..bc38c996d --- /dev/null +++ b/incoming/pattern-study/patterns/biliteral-cipher.js @@ -0,0 +1,308 @@ +// ===== Patternflow pattern ===== +// Title: Biliteral Cipher +// Family: type +// Idea: A cover sentence is set in capitals ten LEDs wide, five to a line, each one in fount a or fount b, two founts that differ by a slant and a little weight the eye passes over; a reading light then goes along the letters one by one, writes a small a or b under each, and every five of those spell one letter of Bacon's 24-letter alphabet, set below in light until the hidden word is complete, holds, and the next sentence is typed. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Francis Bacon's biliteral cipher in De Augmentis Scientiarum (1623), with his own example, FUGE hidden in MANERE TE VOLO DONEC VENERO; the 5 by 7 capitals of Photon Disk (pattern round 70) doubled; pattern round 72 +// Round: 72 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Message=0..4, Tell=0..1 +// +// Knob 1: Hue. The colour of the reading light, the a and b marks and the hidden word, cyan at 0, violet at 0.25, red at 0.5, yellow-green at 0.75; the cover letters take a pale tint from the opposite side of the wheel, ivory at 0. +// Knob 2: Speed. At 2 a letter is typed every 0.07 s and read every 0.3 s, a hidden letter is set in 0.45 s and the finished word holds 3 s, so Bacon's sentence takes 13 s from first letter to the next sentence. At 0.2 everything is ten times slower, at 3 half as fast again. +// Knob 3: Message. Which sentence is set first; the rest follow in turn. 0 Bacon's MANERE TE VOLO DONEC VENERO hiding FUGE, 1 THE SEA IS CALM AND STILL TODAY hiding STORM, 2 SAY NOTHING TO ANYONE YET hiding TELL, 3 WE ARE ALL QUITE WELL HERE hiding HELP, 4 BURN THIS LETTER AT ONCE hiding KEEP. Turning it sets the new sentence at once. +// Knob 4: Tell. How far fount b strays from fount a. At 0 they are the same LEDs and only the reading light knows; at the opening 0.06 a b letter leans 0.22 LED over its 14 rows and is 0.12 LED heavier, a change of a few levels on its edge LEDs; at 0.5 it leans 1.8 LEDs, is 1 LED heavier and the b letters read as a faint italic; at 1 it leans 3.6 LEDs and is 2 LEDs heavier, a bold italic, and the hidden word can be read off the page without the light. + +// Bacon's cipher writes each letter of the hidden word as five symbols, +// a or b, A = aaaaa through Z = babbb, with I and J sharing abaaa and U and +// V sharing baabb, and prints a cover text of at least five times as many +// letters in two founts: each cover letter in fount a stands for a, each in +// fount b for b. Letters past the last full group are nulls, set in either +// fount and not read; here they are drawn a little dimmer. Bacon's own +// example hides FUGE (flee) in MANERE TE VOLO DONEC VENERO (stay until I +// come): 23 letters, 20 read, ERO left over. +// +// The cover letters here are the 5 by 7 capitals of Photon Disk drawn at +// two LEDs a cell, 10 wide and 14 tall with 3 LEDs between them, so five +// fill 62 of the 64 columns: one group of five to a line. Fount b is the +// same bitmap sheared and thickened, sampled 4 x 4 times a LED, so at a +// small Tell it differs from fount a only in how bright its edge LEDs are. +// A dot halfway up the gap marks the end of a word, as Roman inscriptions +// did, since a line of five has no room for word spaces. + +const W = 64, H = 128; +const { floor, min, max, round, abs, sin, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- 5 x 7 capitals from Photon Disk, rows from the top. +const G57 = { + 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 BITS = {}; +for (const k in G57) { + const s = G57[k].replace(/ /g, ""), b = new Uint8Array(35); + for (let i = 0; i < 35; i++) b[i] = s[i] === "#" ? 1 : 0; + BITS[k] = b; +} +// The marks, a 3 by 5 a and b drawn for this pattern. +const MARK = ["....###.##.#.##".replace(/\./g, "0").replace(/#/g, "1"), "#..#..##.#.###.".replace(/\./g, "0").replace(/#/g, "1")]; +// Bacon's alphabet of 24: J reads as I, V as U. +const ABC = "ABCDEFGHIKLMNOPQRSTUWXYZ"; +const code = (ch) => ABC.indexOf(ch === "J" ? "I" : ch === "V" ? "U" : ch); + +const MESSAGES = [ + ["MANERE TE VOLO DONEC VENERO", "FUGE"], + ["THE SEA IS CALM AND STILL TODAY", "STORM"], + ["SAY NOTHING TO ANYONE YET", "TELL"], + ["WE ARE ALL QUITE WELL HERE", "HELP"], + ["BURN THIS LETTER AT ONCE", "KEEP"], +]; + +// ---- Layout: a letter is 10 x 14 LEDs in a cell 13 wide; a line is 22 +// rows, the letter, a blank row, the 5-row mark and two blank rows. +const CX = (c) => 1 + 13 * c, CY = (r) => 1 + 22 * r, HY = 112, RULE = 110; +const GW = 16, GOX = 3; // glyph raster: columns -3..12 of the cell + +// Coverage of one glyph in fount b at a given tell, 16 x 14 LEDs. Shear s +// leans the top s/2 right and the foot s/2 left; weight w widens every +// stroke by w, half to each side. +function raster(ch, t) { + const bm = BITS[ch], out = new Float32Array(GW * 14); + const s = 3.6 * t, w = 2 * t; + const on = (x, y) => { + const c = floor(x / 2), r = floor(y / 2); + return c >= 0 && c < 5 && r >= 0 && r < 7 && bm[r * 5 + c] === 1; + }; + for (let y = 0; y < 14; y++) + for (let gx = 0; gx < GW; gx++) { + let n = 0; + for (let j = 0; j < 4; j++) { + const Y = y + (j + 0.5) / 4, sh = s * (0.5 - Y / 14); + for (let i = 0; i < 4; i++) { + const x = gx - GOX + (i + 0.5) / 4 - sh; + if (on(x, Y) || (w > 0 && (on(x - w / 2, Y) || on(x + w / 2, Y)))) n++; + } + } + out[y * GW + gx] = n / 16; + } + return out; +} + +function prepare(s, mi) { + const [cover, hidden] = MESSAGES[mi]; + const letters = [], dots = []; + for (const ch of cover) { + if (ch === " ") { + if (letters.length % 5) dots.push(letters.length - 1); + } else letters.push(ch); + } + const n = min(hidden.length, floor(letters.length / 5), 5); + const fount = letters.map(() => (Math.random() < 0.5 ? 1 : 0)); + for (let i = 0; i < n; i++) { + const v = code(hidden[i]); + for (let k = 0; k < 5; k++) fount[i * 5 + k] = (v >> (4 - k)) & 1; + } + // Read it back the way a decipherer would, so a wrong table cannot pass. + for (let i = 0; i < n; i++) { + let v = 0; + for (let k = 0; k < 5; k++) v = v * 2 + fount[i * 5 + k]; + if (ABC[v] !== hidden[i]) throw new Error(`message ${mi} letter ${i} reads ${ABC[v]}`); + } + s.mi = mi; + s.letters = letters.slice(0, 25); + s.fount = fount; + s.dots = dots; + s.hidden = hidden.slice(0, n); + s.n = n; + s.phase = 0; // 0 type, 1 read, 2 set, 3 hold, 4 clear + s.t = 0; + s.typed = 0; + s.read = 0; + s.set = 0; + s.born = letters.map(() => -1); + s.markAt = letters.map(() => -1); + s.setAt = [-1, -1, -1, -1, -1]; +} + +export function setup(params) { + const s = (params.s = {}); + s.clock = 0; + s.knobMsg = -1; + s.cache = {}; + s.tellKey = -1; + s.fb = new Uint8ClampedArray(W * H * 3); + prepare(s, 0); +} + +const T_TYPE = 0.07, T_READ = 0.3, T_SET = 0.45, T_HOLD = 3, T_CLEAR = 0.6; + +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.tell = cl(k[3] == null ? 0.06 : k[3], 0, 1); + const rate = cl(k[1] == null ? 2 : k[1], 0.2, 3) / 2; + const mi = round(cl(k[2] || 0, 0, 4)); + if (mi !== s.knobMsg) { + s.knobMsg = mi; + prepare(s, mi); + } + s.clock += dt; + s.t += dt * rate; + const L = s.letters.length, R = s.n * 5; + for (let guard = 0; guard < 40; guard++) { + if (s.phase === 0) { + if (s.t < T_TYPE) break; + s.t -= T_TYPE; + s.born[s.typed++] = s.clock; + if (s.typed >= L) (s.phase = 1), (s.t = -0.25); + } else if (s.phase === 1) { + if (s.t < T_READ) break; + s.t -= T_READ; + if (s.markAt[s.read] < 0) s.markAt[s.read] = s.clock; + s.read++; + if (s.read % 5 === 0) (s.phase = 2), (s.setAt[s.read / 5 - 1] = s.clock); + } else if (s.phase === 2) { + if (s.t < T_SET) break; + s.t -= T_SET; + s.set++; + s.phase = s.read >= R ? 3 : 1; + } else if (s.phase === 3) { + if (s.t < T_HOLD) break; + s.t -= T_HOLD; + s.phase = 4; + } else { + if (s.t < T_CLEAR) break; + prepare(s, (s.mi + 1) % MESSAGES.length); + } + } + // The mark under a letter is written 60% of the way through its reading. + if (s.phase === 1 && s.read < R && s.t > 0.6 * T_READ && s.markAt[s.read] < 0) s.markAt[s.read] = s.clock; +} + +function glyphOf(s, ch, b) { + const key = b ? round(s.tell * 400) : 0; + if (b && key !== s.tellKey) (s.cache = {}), (s.tellKey = key); + const id = ch + (b ? "b" : "a"); + if (!s.cache[id]) s.cache[id] = raster(ch, b ? key / 400 : 0); + return s.cache[id]; +} + +export function draw(display, params) { + const s = params.s; + if (!s) return; + const fb = s.fb; + const ha = 0.5 + s.hue; + const acc = hsv(ha, 0.85, 255), ink = hsv(ha + 0.6, 0.24, 225), bg = hsv(ha, 0.6, 7); + for (let i = 0; i < W * H; i++) (fb[i * 3] = bg[0]), (fb[i * 3 + 1] = bg[1]), (fb[i * 3 + 2] = bg[2]); + const add = (x, y, c, a) => { + if (x < 0 || x >= W || y < 0 || y >= H || a <= 0) return; + const i = (y * W + x) * 3; + fb[i] = max(fb[i], c[0] * a); + fb[i + 1] = max(fb[i + 1], c[1] * a); + fb[i + 2] = max(fb[i + 2], c[2] * a); + }; + const mix = (x, y, c, a) => { + if (x < 0 || x >= W || y < 0 || y >= H || a <= 0) return; + const i = (y * W + x) * 3; + fb[i] += (c[0] - fb[i]) * a; + fb[i + 1] += (c[1] - fb[i + 1]) * a; + fb[i + 2] += (c[2] - fb[i + 2]) * a; + }; + const fade = s.phase === 4 ? 1 - cl(s.t / T_CLEAR, 0, 1) : 1; + const R = s.n * 5; + const reading = s.phase === 1 && s.t >= 0 && s.read < R ? s.read : -1; + const p = reading >= 0 ? cl(s.t / T_READ, 0, 1) : 0; + + // The reading light: a dim box behind the letter and a bright line + // crossing it left to right. + if (reading >= 0) { + const x0 = CX(reading % 5), y0 = CY(floor(reading / 5)); + const env = sin(PI * cl(p * 1.15, 0, 1)) * 0.22 + 0.06; + for (let y = y0 - 1; y <= y0 + 14; y++) for (let x = x0 - 2; x <= x0 + 11; x++) add(x, y, acc, env); + } + // Cover letters. + for (let j = 0; j < s.typed; j++) { + const c = j % 5, r = floor(j / 5), x0 = CX(c), y0 = CY(r); + const g = glyphOf(s, s.letters[j], s.fount[j]); + const age = s.clock - s.born[j], flash = cl(1 - age / 0.25, 0, 1); + let col = ink, lv = fade; + if (j >= R) lv *= 0.62; + // A group glows as its letter is set below; in the hold the b letters + // keep a tint. + const sa = j < R ? s.setAt[floor(j / 5)] : -1; + let u = sa >= 0 ? 0.4 * cl(1 - (s.clock - sa) / 0.6, 0, 1) : 0; + if (s.phase === 3 && s.fount[j] && j < R) u = max(u, 0.3); + if (u > 0) col = [ink[0] + (acc[0] - ink[0]) * u, ink[1] + (acc[1] - ink[1]) * u, ink[2] + (acc[2] - ink[2]) * u]; + const scan = j === reading ? x0 - 1 + p * 12 : -99; + for (let y = 0; y < 14; y++) + for (let gx = 0; gx < GW; gx++) { + const v = g[y * GW + gx]; + if (v <= 0) continue; + const X = x0 + gx - GOX, Y = y0 + y; + mix(X, Y, col, v * lv); + if (flash > 0) mix(X, Y, [255, 255, 255], v * flash * 0.8); + const d = abs(X - scan); + if (d < 1.5) mix(X, Y, [acc[0] * 0.5 + 127, acc[1] * 0.5 + 127, acc[2] * 0.5 + 127], v * (1 - d / 1.5)); + } + } + if (reading >= 0) { + const x = round(CX(reading % 5) - 1 + p * 12), y0 = CY(floor(reading / 5)); + for (let y = y0 - 1; y <= y0 + 14; y++) add(x, y, acc, 0.55); + } + // Word dots in the gaps. + for (const j of s.dots) + if (j + 1 < s.typed && j % 5 < 4) { + const x = CX(j % 5) + 11, y = CY(floor(j / 5)) + 6; + add(x, y, ink, 0.5 * fade), add(x, y + 1, ink, 0.5 * fade); + } + // The a and b marks. + for (let j = 0; j < R; j++) { + if (s.markAt[j] < 0) continue; + const age = s.clock - s.markAt[j], m = MARK[s.fount[j]], x0 = CX(j % 5) + 3, y0 = CY(floor(j / 5)) + 15; + const grp = floor(j / 5), sa = s.setAt[grp]; + let lv = cl(age / 0.12, 0, 1) * 0.78; + if (sa >= 0) lv = 0.78 + 0.22 * cl(1 - abs(s.clock - sa - 0.2) / 0.3, 0, 1); + for (let y = 0; y < 5; y++) for (let x = 0; x < 3; x++) if (m[y * 3 + x] === "1") add(x0 + x, y0 + y, acc, lv * fade); + } + // The rule over the hidden word, and the word. + for (let x = 2; x < W - 2; x++) add(x, RULE, acc, 0.13 * fade); + for (let i = 0; i < s.n; i++) { + if (s.setAt[i] < 0) continue; + const age = s.clock - s.setAt[i], g = glyphOf(s, s.hidden[i], 0), x0 = CX(i); + const grow = cl(age / 0.3, 0, 1), flash = cl(1 - age / 0.5, 0, 1); + for (let y = 0; y < 14; y++) { + if (y > grow * 14 + 0.5) continue; + for (let gx = 0; gx < GW; gx++) { + const v = g[y * GW + gx]; + if (v <= 0) continue; + mix(x0 + gx - GOX, HY + y, acc, v * fade); + if (flash > 0) mix(x0 + gx - GOX, HY + y, [255, 255, 255], v * flash * 0.7 * fade); + } + } + // A soft glow under a freshly set letter. + if (flash > 0) for (let y = HY - 1; y <= HY + 14; y++) for (let x = x0 - 1; x <= x0 + 10; x++) add(x, y, acc, 0.12 * flash * fade); + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = (y * W + x) * 3; + display.setPixel(x, y, fb[i] | 0, fb[i + 1] | 0, fb[i + 2] | 0); + } +} From a9b0f0a4ef8859c5350011d0d851be755b13c844 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 20:33:22 +0000 Subject: [PATCH 08/14] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2072=ED=8C=90:=20Ruppert=20Refinement=20=EA=B3=BC=20Lattice=20?= =?UTF-8?q?Reduction?= 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_01Bc4JkrPaZ8GQQiA22PWEvR --- incoming/pattern-study/patterns/lattice-reduction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/incoming/pattern-study/patterns/lattice-reduction.js b/incoming/pattern-study/patterns/lattice-reduction.js index 6de50146b..8faf9f817 100644 --- a/incoming/pattern-study/patterns/lattice-reduction.js +++ b/incoming/pattern-study/patterns/lattice-reduction.js @@ -13,7 +13,7 @@ // // Knob 1: Hue. Turns the tiles and the arrows round the wheel together. At 0 the tiles are teal, blue and violet, the cell at the centre green, b1 amber and b2 pink; at 0.5 the tiles go red, ochre and lime, the centre cell magenta, b1 blue and b2 spring green. // Knob 2: Speed. At 2 a shear takes 0.55 s, a swap 0.4 s and a pause of 0.2 s follows each, so a basis from 3 shears is reduced in 2.2 to 4.2 s and a new lattice comes every 6 to 8 s. Every time but the 2.6 s hold and the 0.5 s cross-fade scales as 2 / Speed: at 0.2 a shear takes 5.5 s, at 3 0.37 s. -// Knob 3: Skew. How many shears, b2 += b1 and b1 += b2 by turns, each with the sign that lengthens, turn the reduced basis into the starting one. At 3 the longer vector is 3.5 to 4.9 spacings and the tiles 2.3 to 3.2 LEDs wide; at 5 about 10 spacings and one LED wide; at 7 up to 25 spacings, where a cap stops the word, and the tiles under a LED, a moire until the first shear. Gauss's steps take 2 shears at 3 and 4 at 7, each by up to 3 times b1, with one to four swaps between. +// Knob 3: Skew. How many shears, b2 += b1 and b1 += b2 by turns, each with the sign that lengthens, turn the reduced basis into the starting one. At the opening spacing: at 3 the longer vector is 3.5 to 4.9 spacings and the tiles 2.3 to 3.2 LEDs wide; at 5 about 10 spacings and one LED wide; at 7 up to 25 spacings, where a cap stops the word, and the tiles under a LED, a moire until the first shear. Gauss takes 2 or 3 shears at 3 and 4 at 7, each by 1 to 3 times b1, with a swap between most of them. // Knob 4: Spacing. The square root of one cell's area, 11 to 22 LEDs: 68 dots at 11, 17 at 22. The lattices come square, hexagonal, rectangular (sides 1.6 to 1) and random (60 to 90 degrees, sides up to 1.7 to 1) in turn, all of that area and turned at random. // Buttons: K1 tap starts a new lattice. From 4b8c0d5ad27a120e8634c9838af5ea6a22375cfd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 20:34:08 +0000 Subject: [PATCH 09/14] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2072=ED=8C=90:=20Clothesline=20=EA=B3=BC=20Hydra=20Game?= 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_01Bc4JkrPaZ8GQQiA22PWEvR --- .../pattern-study/patterns/clothesline.js | 38 ++++++++------ incoming/pattern-study/patterns/hydra-game.js | 50 +++++++++---------- 2 files changed, 48 insertions(+), 40 deletions(-) diff --git a/incoming/pattern-study/patterns/clothesline.js b/incoming/pattern-study/patterns/clothesline.js index 9b248aa88..27f881442 100644 --- a/incoming/pattern-study/patterns/clothesline.js +++ b/incoming/pattern-study/patterns/clothesline.js @@ -1,20 +1,21 @@ // ===== Patternflow pattern ===== // Title: Clothesline // Family: type -// Idea: Four ropes sag from wall to wall down the tall panel with ten letter cards pegged on them, 바람이 / 분다 / 살아야 / 겠다, and gusts cross the panel from the left so the cards swing on their pegs one after another like pendulums, twist on the line, the ropes lift and bob under them, and every card falls back to hanging straight once the gust has gone. +// Idea: Four ropes sag from wall to wall down the tall panel with ten letter cards pegged on them, 바람이 / 분다 / 살아야 / 겠다, and gusts cross the panel from the left so the cards swing on their pegs one after another like pendulums and twist on the line, the ropes lift and bob under them, and every card falls back to hanging straight once the gust has gone. // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: original, pattern round 72; the text is the Korean of «Le vent se lève, il faut tenter de vivre» from Paul Valéry's Le Cimetière marin (1920); the wind's mean speed from the Beaufort scale's v = 0.836 B^(3/2) m/s; the Hangul cards drawn as strokes for this pattern +// Lineage: original, pattern round 72; the text is «Le vent se lève, il faut tenter de vivre» from Paul Valéry's Le Cimetière marin (1920) in the Korean it is usually given; the mean wind from the Beaufort scale (Francis Beaufort, 1805) by v = 0.836 B^(3/2) m/s; the Hangul syllables drawn as strokes for this pattern // Round: 72 // =============================== // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Wind=2..7, Gust=0..1 // -// Knob 1: Hue. The card colour, with the ink always cream white: at 0 the cards are teal, at 0.25 blue violet, at 0.5 rose, at 0.75 olive gold, each card a little off its neighbours. -// Knob 2: Speed. The time scale. At 2 a card left to itself swings once a second and a gust takes 1.6 s to cross the panel in a light breeze; at 1 everything is half as fast, at 0.2 ten times slower, at 3 one and a half times faster. -// Knob 3: Wind. The mean wind on the Beaufort scale. At the opening 2, a light breeze of 2.4 m/s, the cards lean 7 degrees and each gust swings them to about 25; at 4, 6.7 m/s, they lean 45 degrees; at 7, a near gale of 15.5 m/s, they fly almost flat and the ropes pull nearly straight. -// Knob 4: Gust. Turbulence on top of the gusts: at 0 only the gust fronts that cross every 1.5 to 3 s, at the opening 0.06 a little flutter, at 1 gusts half as strong again and twice as often, eddies that throw neighbouring cards opposite ways, and the cards twist round their pegs until some show their blank backs. +// Knob 1: Hue. The card colour, the ink always cream: sea green at 0, azure at 0.25, blue violet at 0.5, magenta at 0.75 and rose red at 1, each card a little off its neighbours. The wheel skips orange, which on a dim card reads brown. +// Knob 2: Speed. The time scale. At 2 a card left to itself swings once a second and a gust crosses the 64 columns in 1.6 s in a light breeze; at 1 everything is half as fast, at 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Wind. The mean wind in Beaufort force. At the opening 2, a light breeze of 2.4 m/s, the cards lean 7 degrees and the gusts swing them to 20 or 35 and back past straight; at 4, 6.7 m/s, they lean 45 degrees; at 7, a near gale of 15.5 m/s, they fly 79 degrees out, almost flat, and the ropes pull from a 5-LED sag to 1. +// Knob 4: Gust. Turbulence on top of the gust fronts. At 0 only the fronts, one every 1.5 to 3 s; at the opening 0.06 a little flutter; at 1 the fronts come twice as often and half as strong again, eddies throw neighbouring cards opposite ways and the cards twist round their pegs until some turn their backs, where the ink shows through mirrored. +// Buttons: K1 tap sends a strong gust across the whole line. const W = 64, H = 128; const { floor, round, min, max, abs, sin, cos, exp, sqrt, tanh, PI } = Math; @@ -92,21 +93,27 @@ const G = (4 * PI * PI * J) / L; // Drag: tan(lean) = 0.022 v^2 for v in m/s, and 10 LED/s to a m/s. const Q = 10, KD = (0.022 * G) / (Q * Q); +// The rope hangs in a catenary, y = c cosh(x / c) about its lowest point: +// a 5-LED sag over the 63 columns between the hooks makes c = 99, a pull +// along the rope equal to the weight of 99 LEDs of it. A gust lifting it +// shrinks the sag in the same shape, and tips it in a sine of the span. +const CC = 99, CH = Math.cosh(31.5 / CC) - 1; +const cat = (x) => (Math.cosh(31.5 / CC) - Math.cosh((x - 31.5) / CC)) / CH; +const tip = (x) => sin((2 * PI * x) / 63); function ropeY(r, x) { - const f1 = sin((PI * x) / 63), f2 = sin((2 * PI * x) / 63); - return r.y + (SAG - r.b1) * f1 - r.b2 * f2; + return r.y + (SAG - r.b1) * cat(x) - r.b2 * tip(x); } export function setup(params) { - const ropes = ROPES.map((R, ri) => { + const ropes = ROPES.map((R) => { const n = [...R.cards].length, cards = []; [...R.cards].forEach((ch, i) => { const x = 32 + (i - (n - 1) / 2) * 20 + (Math.random() - 0.5) * 2; - cards.push({ ch, x, th: (Math.random() - 0.5) * 0.1, w: 0, ph: 0, pw: 0, dh: (Math.random() - 0.5) * 0.08, ay: 0 }); + cards.push({ ch, x, th: (Math.random() - 0.5) * 0.1, w: 0, ph: 0, pw: 0, dh: (Math.random() - 0.5) * 0.08 }); }); - return { y: R.y, cards, b1: 0, v1: 0, a1: 0, b2: 0, v2: 0, a2: 0, ri }; + return { y: R.y, cards, b1: 0, v1: 0, a1: 0, b2: 0, v2: 0, a2: 0 }; }); - params.s = { t: 0, ropes, gusts: [], next: 0.3, ph: [0, 1, 2, 3, 4, 5].map(() => Math.random() * 2 * PI), warm: false, hue: 0, speed: 2, wind: 2, gust: 0.06, F: new Float32Array(W * H * 3) }; + params.s = { t: 0, ropes, gusts: [], next: 0.3, ph: [0, 1, 2, 3, 4, 5].map(() => Math.random() * 2 * PI), warm: false, hue: 0, speed: 2, B: 2, V: 24, gust: 0.06, F: new Float32Array(W * H * 3) }; } // The wind across the panel, LED/s rightward, at sim time t. @@ -148,8 +155,8 @@ function step(S, h) { c.wv = w; c.wd = windAt(S, c.x + 6, py + L) - windAt(S, c.x - 6, py + L); const f = (w * w) / (Q * Q) / 100; - l1 += f * sin((PI * c.x) / 63); - l2 += f * sin((2 * PI * c.x) / 63); + l1 += f * cat(c.x); + l2 += f * tip(c.x); } const w1 = 2 * PI * 0.9, w2 = 2 * PI * 1.6; r.a1 = -w1 * w1 * r.b1 - 0.5 * w1 * r.v1 + w1 * w1 * 4 * tanh(l1); @@ -160,7 +167,7 @@ function step(S, h) { r.b2 += r.v2 * h; for (const c of r.cards) { // The peg's own acceleration, down, changes the gravity the card feels. - const ay = -r.a1 * sin((PI * c.x) / 63) - r.a2 * sin((2 * PI * c.x) / 63); + const ay = -r.a1 * cat(c.x) - r.a2 * tip(c.x); const ct = cos(c.th), st = sin(c.th), rel = c.wv - 0.5 * L * ct * c.w, F = KD * rel * abs(rel); const acc = ((-(G - ay) * st + F * ct) * L) / J - 0.3 * c.w; c.w += acc * h; @@ -184,6 +191,7 @@ export function update(dt, input, params) { // Beaufort: v = 0.836 B^1.5 m/s. S.V = 0.836 * S.B ** 1.5 * Q; let T = d * (S.speed / 2); + if (input.btnPressed && input.btnPressed[0]) S.gusts.push({ x: -25, s: 12, a: 2.4, yc: 64, c: 0.9 * S.V + 20 }); // Start with the line already in the wind, 2.5 s in. if (!S.warm) (S.warm = true), (T += 2.5); const n = max(1, Math.ceil(T / (1 / 240))); diff --git a/incoming/pattern-study/patterns/hydra-game.js b/incoming/pattern-study/patterns/hydra-game.js index 1b03436bd..d5102501d 100644 --- a/incoming/pattern-study/patterns/hydra-game.js +++ b/incoming/pattern-study/patterns/hydra-game.js @@ -1,38 +1,38 @@ // ===== Patternflow pattern ===== // Title: Hydra Game // Family: algo -// Idea: Hercules fights Kirby and Paris's hydra, a tree rooted at the bottom of the panel with glowing heads on curved necks: at step n he cuts one head, and unless it grew straight from the root the node two levels down grows n copies of the stump's branch, which sprout from it in gold, so the hydra swells into a dense fan of hundreds or thousands of heads while the ordinal at the top, the sum of omega to the power of each branch's ordinal, falls at every cut, and the cuts come faster and faster until the last head drops and the root stands alone. +// Idea: Hercules fights Kirby and Paris's hydra, a tree rooted at the bottom of the panel with glowing heads on writhing necks: at step n he cuts one head, and unless its neck grows from the root, the node one level further down grows n copies of the branch the head was cut from, which sprout from it in gold, so the hydra swells into a dense fan of hundreds or thousands of heads while its ordinal at the top, the sum of omega to the power of each branch's ordinal, falls at every cut, and the cuts come faster and faster until the last head drops and the root stands alone. // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: the hydra game (Laurie Kirby and Jeff Paris, Accessible independence results for Peano arithmetic, Bulletin of the London Mathematical Society, 1982), the ordinal of a tree in Cantor normal form (Georg Cantor), pattern round 72 +// Lineage: the hydra game (Laurie Kirby and Jeff Paris, Accessible independence results for Peano arithmetic, Bulletin of the London Mathematical Society, 1982), a tree's ordinal in Cantor normal form (Georg Cantor), pattern round 72 // Round: 72 // =============================== // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Hydra=1..5, Strategy=0..3 // -// Knob 1: Hue. Turns the hydra round the wheel. At 0 the necks are green and the heads pale mint, new growth sprouts gold and the ordinal's omegas are green; at 0.25 the necks are sky blue, at 0.5 violet, at 0.75 rose red. -// Knob 2: Speed. Cuts a second, 0.9 Speed (1 + n / 8) at step n, so the cuts speed up as the hydra grows: at the opening 2 the first cut comes 1.6 s after the hydra has grown, step 40 at about 8 s, step 1149 at 23 s and step 5629 at 30 s; at 0.2 everything takes ten times as long. -// Knob 3: Hydra. Five starting hydras, by whole steps. 1: omega^2 + omega + 1, a root with a two-headed neck, a one-headed neck and a head, 7 nodes; cut from the right it dies at step 1149 with 576 nodes at its biggest. 2: omega^omega, a single neck of three, dead at step 37 from either side, 20 nodes at most. 3: omega^2 + omega 2, dead at step 445 from either side, 224 nodes. 4: omega^2 + omega + 2, dead at step 5629 from the right, 2816 nodes. 5: omega^2 + omega 2 + 2, dead at step 2302 from either side, 1153 nodes. -// Knob 4: Strategy. Which head Hercules cuts, by whole steps: 0 the rightmost, 1 the leftmost, 2 a random branch at every fork, 3 the end of the longest neck, the rightmost of the deepest. The order changes the length of the fight a hundredfold: hydra 1 dies at step 1149 cut from the right, at 30 from the left and at 23 from the deepest end, and 2000 random hunts took from 17 to 441 steps. A hunt whose regrowth would pass 8000 nodes is called off; in 2000 random hunts on hydra 5 that happened twice. +// Knob 1: Hue. Turns the hydra round the wheel. At 0 the necks are green, the heads pale mint and new growth gold; at 0.25 the necks are azure and new growth green, at 0.5 magenta with sky-blue growth, at 0.75 amber with pink growth. +// Knob 2: Speed. Cuts a second, 0.9 Speed (1 + n / 8) at step n, so the cuts come faster as the hydra grows. At the opening 2 the hydra grows out of its root in the first second, the first cut falls at 1.6 s, step 40 at 9.3 s, and hydra 1 dies at step 1149 at 23.5 s; at 3 that death comes at 16 s, at 0.2 at 225 s. The dead root waits 3.5 s and the hydra grows again. +// Knob 3: Hydra. Five starting hydras, by whole steps, with their deaths under the rightmost cut. 1: ω²+ω+1, a two-headed neck, a one-headed neck and a head on the root, 7 nodes; dead at step 1149, 576 nodes at its biggest. 2: ω^ω, one neck of three, dead at step 37 with 20 nodes at most. 3: ω²+ω·2, dead at step 445, 224 nodes. 4: ω²+ω+2, dead at step 5629, 2816 nodes. 5: ω²+ω·2+2, 10 nodes, dead at step 2302, 1153 nodes. Levels stand 50 LEDs apart for the hydras two necks high and 33 for the neck of three. +// Knob 4: Strategy. Which head Hercules cuts, by whole steps: 0 the rightmost head, 1 the leftmost, 2 a random branch at every fork, 3 the deepest head, the rightmost of them. The order changes the length of the fight up to 234 times: hydra 4 dies at step 5629 cut from the right and at 24 from the deepest end; hydra 1 at 1149 from the right, 30 from the left and 23 from the deepest end, and 2000 random hunts on it took 17 to 441 steps. Hydras 2, 3 and 5 are symmetric enough that the leftmost cut takes as long as the rightmost. A hunt whose regrowth would pass 8000 nodes is called off and shown as MAX; in 2000 random hunts on hydra 5 that happened twice. // Buttons: K1 tap grows the hydra again from its root. // Kirby and Paris (1982): a hydra is a finite rooted tree and its heads are // the leaves. At step n Hercules cuts a head. If its neck grows from the -// root, the head is simply gone; otherwise the node below the stump grows n -// copies of the stump's branch, head removed. Every way of choosing heads -// kills every hydra in finitely many steps, and Peano arithmetic cannot -// prove it. The proof gives each node the ordinal o(v) = the sum of -// omega^o(c) over its children c, written in Cantor normal form with the -// biggest terms first: a cut lowers one term and adds n smaller ones, so -// the root's ordinal falls at every step, and ordinals below epsilon_0 cannot -// fall forever. Arithmetic counts only up to omega^omega^... of finite -// height, short of epsilon_0, so it cannot see the end. +// root, the head is simply gone; otherwise the node below that neck's own +// node grows n copies of the branch the head was cut from. Every way of +// choosing heads kills every hydra in finitely many steps, and Peano +// arithmetic cannot prove it. The proof gives each node the ordinal o(v), +// the sum of omega^o(c) over its children c in Cantor normal form, biggest +// terms first. A cut lowers one term and adds n smaller ones, so the root's +// ordinal falls at every step, and ordinals below epsilon_0 cannot fall +// forever; the hydra needs all of them, more than Peano arithmetic reaches. // // The panel can draw a few thousand heads, which leaves the hydras below -// omega^3 and the single neck of three: the rightmost cut on omega^2 started -// at step m ends near (m + 4) 2^(m + 2), and an omega^2 met at step 40 -// would need some 10^13 steps. +// omega^3 and the single neck of three. The rightmost cut on omega^2 with +// nothing beside it, first met at step m, ends at step (m + 3) 2^(m + 1) - 3: +// 13 for m = 1, 1149 for m = 6. Two such necks, omega^2 2, die at step +// 557053 with 278528 nodes at their biggest. const W = 64, H = 128, N = W * H; const { floor, round, min, max, abs, sin, cos, exp, sqrt, PI } = Math; @@ -164,9 +164,9 @@ function chop(S) { } // Heads under each node, then a span of the 60 columns for each node in -// proportion to its heads, levels S.sp apart up from the root. -// A node with a head of its own keeps at least 4 heads' width, so a few -// branches stay apart beside a fan of hundreds. +// proportion to its heads, levels S.sp apart up from the root. A node that +// still carries necks keeps at least 4 heads' width, so a few branches stay +// apart beside a fan of hundreds. function count(v) { if (!v.c.length) return (v.k = v.w = 1); let s = 0, w = 0; @@ -284,7 +284,7 @@ function neck(A, x0, y0, x1, y1, col, w, thick, ph) { splat(A, x - ty * half * j, y + tx * half * j, r * q, g * q, b * q); } // and its glow, 1.5 LEDs further out each side. - for (let j = -1; j <= 1; j += 2) splat(A, x - ty * (half + 1.5) * j, y + tx * (half + 1.5) * j, r * 0.16, g * 0.16, b * 0.16); + for (let j = -1; j <= 1; j += 2) splat(A, x - ty * (half + 1.5) * j, y + tx * (half + 1.5) * j, r * 0.22, g * 0.22, b * 0.22); } } function headDot(A, x, y, col, w) { @@ -400,10 +400,10 @@ export function draw(display, params) { const A = S.A, O = S.O, h = S.hue + HUE0; const C = { neck: hsv(h, 0.85, 0.9), - head: hsv(h + 0.04, 0.3, 1), + head: hsv(h + 0.04, 0.42, 1), gold: hsv(h - 0.22, 0.55, 1.1), - txt: [200, 200, 196], - dim: [120, 120, 120], + txt: hsv(h + 0.04, 0.18, 0.85).map((c) => round(c * 255)), + dim: hsv(h, 0.3, 0.55).map((c) => round(c * 255)), om: hsv(h, 0.6, 1).map((c) => round(c * 255)), }; A.fill(0); From e315ab9e892f9779a829161d717e326121ab3bf5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 20:35:10 +0000 Subject: [PATCH 10/14] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2072=ED=8C=90:=20Biliteral=20Cipher=20=EA=B3=BC=20Autogram?= 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_01Bc4JkrPaZ8GQQiA22PWEvR --- incoming/pattern-study/patterns/autogram.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/incoming/pattern-study/patterns/autogram.js b/incoming/pattern-study/patterns/autogram.js index 28e60df4d..fa087f7c1 100644 --- a/incoming/pattern-study/patterns/autogram.js +++ b/incoming/pattern-study/patterns/autogram.js @@ -242,6 +242,17 @@ export function update(dt, input, params) { } else (s.sub = 0), (s.k = 0), (s.occ = occ(s, s.li)); } } else if (s.phase === 1 || s.phase === 3) { + // Caught in a loop, the rewriting goes on round it, 8 a second. + if (s.phase === 3 && s.period && s.t >= s.next) { + const prev = s.claim, c = s.count.slice(); + setText(s, c); + s.changedAt = new Float32Array(s.text.length).fill(-9); + s.item.forEach(([a, e], i) => { + if (c[i] !== prev[i]) for (let k = a; k < e; k++) s.changedAt[k] = s.clock; + }); + s.next += 0.125; + continue; + } if (s.t < (s.phase === 1 ? T_TRUE : T_LOOP)) break; start(s, (s.si + 1) % 3, s.m); } else { @@ -258,7 +269,7 @@ export function update(dt, input, params) { }); const key = keyOf(c); if (s.count.every((n, i) => n === c[i])) (s.phase = 1), (s.t = 0), (s.verdict = "TRUE"); - else if (s.seen.has(key)) (s.phase = 3), (s.t = 0), (s.period = s.round - s.seen.get(key)); + else if (s.seen.has(key)) (s.phase = 3), (s.t = 0), (s.next = 0.125), (s.period = s.round - s.seen.get(key)); else if (s.round >= MAXR) (s.phase = 3), (s.t = 0), (s.period = 0); else s.seen.set(key, s.round); } From e96ec9ca068f98fc80e2248fb8ad7ab432831538 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 20:36:21 +0000 Subject: [PATCH 11/14] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2072=ED=8C=90:=20Schlieren=20Texture=20=EA=B3=BC=20Conway-Coxe?= =?UTF-8?q?ter=20Frieze?= 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_01Bc4JkrPaZ8GQQiA22PWEvR --- .../pattern-study/patterns/coxeter-frieze.js | 346 ++++++++++++++++++ .../patterns/schlieren-texture.js | 19 +- 2 files changed, 357 insertions(+), 8 deletions(-) create mode 100644 incoming/pattern-study/patterns/coxeter-frieze.js diff --git a/incoming/pattern-study/patterns/coxeter-frieze.js b/incoming/pattern-study/patterns/coxeter-frieze.js new file mode 100644 index 000000000..9000fe090 --- /dev/null +++ b/incoming/pattern-study/patterns/coxeter-frieze.js @@ -0,0 +1,346 @@ +// ===== Patternflow pattern ===== +// Title: Conway-Coxeter Frieze +// Family: number +// Idea: A Conway-Coxeter frieze stands up the tall panel, its rows of whole numbers turned into columns between two columns of 1s and offset half a step into a lattice of diamonds, every diamond keeping ad - bc = 1 as the frieze drifts down past its glide; every 6 s the frieze dims, the triangulated polygon it comes from lights up with each vertex's count of triangles, one diagonal flips, and the frieze comes back recomputed with the numbers that changed flashing white. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: frieze patterns (H. S. M. Coxeter, "Frieze patterns", 1971), triangulated polygons and frieze patterns (John Conway and H. S. M. Coxeter, 1973), pattern round 72 +// Round: 72 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Sides=7..11, Chords=0..1 +// +// Knob 1: Hue. Turns the numbers' colours round the wheel. At 0 the 2s are green, 3s teal, 4s cyan, 5s azure, 7s and 8s blue, 13s magenta, 21s red and 34s gold, each number on a diamond of its own colour at a fifth of the brightness; the 1s are white, the two border columns grey, and the flip is drawn in amber. At 0.5 the 2s are purple and the 8s yellow-green. +// Knob 2: Speed. The frieze drifts down 1.5 Speed LEDs a second, 3 at the opening 2, so the heptagon's frieze, which repeats every 84 LEDs and mirrors itself left to right every 42, comes round in 28 s. A diagonal flips every 12 / Speed seconds: every 6 s at Speed 2, first at 4.2 s, every 60 s at 0.2 and 4 s at 3. +// Knob 3: Sides. The polygon's n, 7 to 11, and the frieze's n - 1 columns. At the opening 7 there are 6 columns and the 42 triangulations of the heptagon give numbers up to 8; the largest number over all triangulations is a Fibonacci number, 13 for the octagon's 132, then 21, 34 and 55 for 11 sides and 4862 triangulations. Columns that can hold two digits are 7 LEDs wide, the rest 3. A new n starts from a fan and 4n random flips. +// Knob 4: Chords. Every number in the frieze is m(i, j) for the chord from vertex i to vertex j. At 0 the numbers are coloured by value; at 1 by the direction of their chord, so parallel chords share a colour, and the numbers standing at one height are one family of parallel chords. Between the two the colours blend; at the opening 0.06 they are nearly all by value. + +// Coxeter (1971): a frieze is rows of numbers between a row of 0s and 1s +// above and of 1s and 0s below, each row offset half a step, where every +// diamond b over a, d over c holds ad - bc = 1. Conway and Coxeter (1973): +// the friezes of positive whole numbers with n - 1 rows between the 0s are +// the triangulations of an n-gon, the row after the 1s counting the +// triangles at each vertex, its quiddity. The entries are m(i, j) for +// vertices i and j, with m(i, i) = 0, m(i, i + 1) = 1 and +// m(i, j + 1) = a_j m(i, j) - m(i, j - 1); m(i, j) = 1 exactly where i j is +// a side or a diagonal of the triangulation, and m(i, j) = m(j, i + n), the +// glide. Both were checked against every triangulation from 5 to 11 sides. + +const W = 64, H = 128; +const { floor, round, min, max, abs, sqrt, cos, sin, log2, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const STEP = 12; // LEDs between one entry and the next down a column +const CX = 31.5, CY = 63.5, RX = 24.5, RY = 50; // the polygon, an ellipse's n-gon + +// 3 x 5 digits, one octal digit a row from the top, 4 = left column. +const FONT = [0o75557, 0o26227, 0o71747, 0o71717, 0o55711, 0o74717, 0o74757, 0o71122, 0o75757, 0o75717]; + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// ---- The triangulation: an n by n table of sides and diagonals. ---- +const linked = (S, i, j) => S.adj[i * S.n + j] === 1; +function link(S, i, j, v) { + S.adj[i * S.n + j] = v; + S.adj[j * S.n + i] = v; +} +function diagonals(S) { + const n = S.n, out = []; + for (let i = 0; i < n; i++) for (let j = i + 2; j < n; j++) if (!(i === 0 && j === n - 1) && linked(S, i, j)) out.push(i, j); + return out; +} +// The two triangles on diagonal i j: the vertices joined to both, one on +// each side. In a triangulated convex polygon every three mutually joined +// vertices bound a triangle. +function apexes(S, i, j) { + const out = []; + for (let k = 0; k < S.n; k++) if (k !== i && k !== j && linked(S, i, k) && linked(S, j, k)) out.push(k); + return out; +} +function flip(S, i, j) { + const [k, l] = apexes(S, i, j); + link(S, i, j, 0); + link(S, k, l, 1); + return [k, l]; +} + +// ---- The frieze from the triangulation, checked. ---- +function build(S) { + const n = S.n, a = new Int32Array(n), M = []; + for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) a[i] += S.adj[i * n + j]; + for (let i = 0; i < n; i++) a[i] -= 1; // triangles at i: its sides and diagonals less one + for (let i = 0; i < n; i++) { + const r = new Int32Array(n + 1); + r[1] = 1; + for (let k = 1; k < n; k++) r[k + 1] = a[(i + k) % n] * r[k] - r[k - 1]; + M.push(r); + } + // Every diamond: a = m(i, i + k) over d = m(i + 1, i + 1 + k), with + // b = m(i + 1, i + k) and c = m(i, i + k + 1) beside them; the 1s and 0s + // closing every column; every entry between them positive. + let ok = true, checked = 0; + for (let i = 0; i < n; i++) { + const R = M[i], Q = M[(i + 1) % n]; + if (R[n - 1] !== 1 || R[n] !== 0) ok = false; + for (let k = 1; k <= n - 2; k++, checked++) if (R[k] * Q[k] - Q[k - 1] * R[k + 1] !== 1) ok = false; + for (let k = 1; k < n; k++) if (R[k] < 1) ok = false; + } + return { a, M, ok, checked }; +} + +// A random triangulation: a fan from vertex 0, then 4n random flips. +function fresh(S, n) { + S.n = n; + S.adj = new Uint8Array(n * n); + for (let i = 0; i < n; i++) link(S, i, (i + 1) % n, 1); + for (let j = 2; j < n - 1; j++) link(S, 0, j, 1); + for (let t = 0; t < 4 * n; t++) { + const d = diagonals(S), q = 2 * floor((Math.random() * d.length) / 2); + flip(S, d[q], d[q + 1]); + } + const f = build(S); + S.a = f.a; + S.M = f.M; + S.checked = f.checked; + S.flash = new Float32Array(n * (n + 1)).fill(-99); + S.old = null; + S.neu = null; + S.pick = null; + // Fixed column widths: 3 LEDs, or 7 where two digits can come, columns + // 3 to n - 3 from the octagon on; the spare LEDs shared out as gaps. + const wd = []; + let used = 0; + for (let k = 1; k < n; k++) { + const w = n >= 8 && k >= 3 && k <= n - 3 ? 7 : 3; + wd.push(w); + used += w; + } + const gap = (W - used) / (n - 1); + S.colX = []; + let x = gap / 2; + for (let k = 0; k < n - 1; k++) { + S.colX.push(x + wd[k] / 2); + x += wd[k] + gap; + } + // For each LED column, the frieze columns either side of it (1 to n - 1). + S.near = new Int8Array(2 * W); + for (let X = 0; X < W; X++) { + let k = 1; + while (k < n - 1 && S.colX[k] <= X) k++; + S.near[2 * X] = X < S.colX[0] ? 1 : k; + S.near[2 * X + 1] = X < S.colX[0] ? 1 : min(k + 1, n - 1); + } + // Colour and flash of the 14 entries in view down each column. + S.EC = []; + for (let k = 0; k < n; k++) S.EC.push(new Float32Array(14 * 5)); + S.I0 = new Int32Array(n); +} + +export function setup(params) { + params.s = { hue: 0, speed: 2, n: 0, chords: 0.06, t: 0, scroll: 0, cyc: 0.5, pick: null, flips: 0, bad: 0, buf: new Float32Array(W * H * 3) }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 7, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.chords = cl(k[3], 0, 1); + const n = round(cl(k[2], 7, 11)); + if (n !== S.n) fresh(S, n); + S.t += d; + S.scroll += d * 1.5 * S.speed; + // The flip cycle, 12 / Speed seconds: the polygon fades in over the first + // 8 per cent, the diagonal flips at 20, the polygon fades out from 32 to 40 + // and the changed numbers flash as the frieze comes back. + S.cyc += (d * S.speed) / 12; + if (S.cyc >= 1) { + // A new cycle picks the diagonal to flip, not the one the last flip + // made, which would only flip back. + S.cyc -= 1; + const dg = diagonals(S); + let q = 2 * floor((Math.random() * dg.length) / 2); + if (S.neu && dg.length > 2 && dg[q] === S.neu[0] && dg[q + 1] === S.neu[1]) q = (q + 2) % dg.length; + S.pick = [dg[q], dg[q + 1]]; + } + if (S.pick && S.cyc >= 0.2) { + const [i, j] = S.pick, kl = flip(S, i, j), f = build(S); + S.pick = null; + if (f.ok) { + // The changed entries flash from the moment the frieze comes back. + const at = S.t + ((0.32 - S.cyc) * 12) / S.speed; + for (let r = 0; r < n; r++) for (let c = 1; c < n; c++) if (f.M[r][c] !== S.M[r][c]) S.flash[r * (n + 1) + c] = at; + S.a = f.a; + S.M = f.M; + S.old = [i, j]; + S.neu = [min(kl[0], kl[1]), max(kl[0], kl[1])]; + S.flips++; + } else { + // Never seen: a frieze that fails a diamond keeps the old triangulation. + link(S, kl[0], kl[1], 0); + link(S, i, j, 1); + S.bad++; + } + S.checked = f.checked; + } +} + +// Additive soft line into the buffer, a splat every quarter LED. +function line(buf, x0, y0, x1, y1, r, g, b) { + const len = sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2), steps = max(1, round(len * 4)); + for (let s = 0; s <= steps; s++) { + const t = s / steps, x = x0 + (x1 - x0) * t, y = y0 + (y1 - y0) * t, ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + for (let dy = 0; dy < 2; dy++) + for (let dx = 0; dx < 2; dx++) { + const X = ix + dx, Y = iy + dy; + if (X < 0 || Y < 0 || X >= W || Y >= H) continue; + const w = (dx ? fx : 1 - fx) * (dy ? fy : 1 - fy) * 0.25, o = (Y * W + X) * 3; + buf[o] = max(buf[o], min(1, buf[o] + r * w)); + buf[o + 1] = max(buf[o + 1], min(1, buf[o + 1] + g * w)); + buf[o + 2] = max(buf[o + 2], min(1, buf[o + 2] + b * w)); + } + } +} + +// One 3 x 5 digit with its top left at (x, y). +function digit(buf, d, x, y, r, g, b) { + const f = FONT[d]; + for (let row = 0; row < 5; row++) { + const Y = y + row; + if (Y < 0 || Y >= H) continue; + const bits = (f >> (3 * (4 - row))) & 7; + for (let c = 0; c < 3; c++) + if (bits & (4 >> c)) { + const X = x + c; + if (X < 0 || X >= W) continue; + const o = (Y * W + X) * 3; + buf[o] = r; + buf[o + 1] = g; + buf[o + 2] = b; + } + } +} +// A number 1 to 99 centred on (cx, cy). +function number(buf, v, cx, cy, r, g, b) { + const y = round(cy) - 2; + if (v < 10) digit(buf, v, round(cx - 1.5), y, r, g, b); + else { + const x = round(cx - 3.5); + digit(buf, floor(v / 10) % 10, x, y, r, g, b); + digit(buf, v % 10, x + 4, y, r, g, b); + } +} + +const vx = (S, v) => CX + RX * cos((2 * PI * v) / S.n - PI / 2); +const vy = (S, v) => CY + RY * sin((2 * PI * v) / S.n - PI / 2); + +export function draw(display, params) { + const S = params.s; + if (!S || !S.n) { + for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) display.setPixel(x, y, 0, 0, 0); + return; + } + const { buf, n, M, colX, EC, I0 } = S, c = S.cyc, tmp = [0, 0, 0], acc = [0, 0, 0]; + // How far the polygon has come in: 0 on the frieze alone, 1 at full. + const sm = (e) => e * e * (3 - 2 * e); + const ov = c < 0.08 ? sm(c / 0.08) : c < 0.32 ? 1 : c < 0.4 ? sm(1 - (c - 0.32) / 0.08) : 0, fr = 1 - 0.85 * ov; + const hue0 = S.hue + 0.12; + hsv(hue0, 0.75, 1, acc); + + // ---- Each entry's colour. Column k holds m(i, i + k) at height + // i + k / 2 steps. By value: 1s white, the border's dimmer, the rest round + // the wheel by log2 of the value. By chord: hue from (i + j) mod n. ---- + for (let k = 1; k < n; k++) { + const i0 = (I0[k] = Math.ceil((-STEP - S.scroll) / STEP - k / 2)), e = EC[k]; + for (let q = 0; q < 14; q++) { + const i = i0 + q, r = ((i % n) + n) % n, v = M[r][k]; + const edge = k === 1 || k === n - 1; + if (v === 1) tmp[0] = tmp[1] = tmp[2] = edge ? 0.6 : 1; + else hsv(hue0 + 0.2 * log2(v), 0.8, 1, tmp); + if (S.chords > 0) { + const t = tmp.slice(); + hsv(hue0 + ((2 * r + k) % n) / n, 0.75, 1, tmp); + for (let z = 0; z < 3; z++) tmp[z] = t[z] + (tmp[z] - t[z]) * S.chords; + } + // A changed number flashes white for 0.4 s and fades back over 1.2 s. + const age = S.t - S.flash[r * (n + 1) + k]; + const wf = age >= 0 && age < 1.6 ? (age < 0.4 ? 1 : 1 - (age - 0.4) / 1.2) : 0; + for (let z = 0; z < 3; z++) e[q * 4 + z] = tmp[z] + (1 - tmp[z]) * wf; + e[q * 4 + 3] = wf; + // How bright its diamond is: 0.2 of the colour, 0.1 for a 1 inside + // and 0.05 on the border. + e[56 + q] = v > 1 ? 0.2 : edge ? 0.083 : 0.1; + } + } + + // ---- Tiles: every LED belongs to the nearest entry in a diamond metric, + // so each number sits on a diamond of its colour at 0.2, and LEDs on a + // boundary between two diamonds stay dark. ---- + for (let y = 0, o = 0; y < H; y++) + for (let x = 0; x < W; x++, o += 3) { + const a = S.near[2 * x], b = S.near[2 * x + 1], h = b > a ? (colX[b - 1] - colX[a - 1]) / 2 : 6; + let d1 = 9, d2 = 9, kb = 0, qb = 0; + for (const k of a === b ? [a] : [a, b]) { + const u = (y - S.scroll) / STEP - k / 2, i = round(u), dd = abs(x - colX[k - 1]) / h + (abs(u - i) * STEP) / (STEP / 2); + if (dd < d1) (d2 = d1), (d1 = dd), (kb = k), (qb = i - I0[k]); + else if (dd < d2) d2 = dd; + } + if (d2 - d1 < 0.16 || qb < 0 || qb > 13) { + buf[o] = buf[o + 1] = buf[o + 2] = 0; + continue; + } + const e = EC[kb], g = (e[56 + qb] + 0.3 * e[qb * 4 + 3]) * fr; + buf[o] = e[qb * 4] * g; + buf[o + 1] = e[qb * 4 + 1] * g; + buf[o + 2] = e[qb * 4 + 2] * g; + } + + // ---- The numbers. ---- + for (let k = 1; k < n; k++) { + const e = EC[k]; + for (let q = 0; q < 14; q++) { + const i = I0[k] + q, r = ((i % n) + n) % n, y = (i + k / 2) * STEP + S.scroll, lv = fr + (1 - fr) * e[q * 4 + 3]; + number(buf, M[r][k], colX[k - 1], y, e[q * 4] * lv, e[q * 4 + 1] * lv, e[q * 4 + 2] * lv); + } + } + + // ---- In the flip, the polygon over the dimmed frieze: sides grey, + // diagonals white. The chosen diagonal pulses in the accent colour from + // 0.08 of the cycle, fades from 0.2 to 0.26, and the new one grows from + // one end to the other from 0.2 to 0.28. Each vertex carries its count of + // triangles, the quiddity, the four the flip changes in the accent. ---- + if (ov > 0) { + for (let v = 0; v < n; v++) line(buf, vx(S, v), vy(S, v), vx(S, (v + 1) % n), vy(S, (v + 1) % n), 0.6 * ov, 0.6 * ov, 0.6 * ov); + const hot = c < 0.2 ? S.pick : null, grow = c >= 0.2 && c < 0.28 ? S.neu : null; + for (let i = 0; i < n; i++) + for (let j = i + 2; j < n; j++) { + if ((i === 0 && j === n - 1) || !linked(S, i, j)) continue; + if (hot && hot[0] === i && hot[1] === j) { + const e = (c < 0.08 ? 1 : 0.6 + 0.4 * cos((c - 0.08) * 160)) * ov; + line(buf, vx(S, i), vy(S, i), vx(S, j), vy(S, j), acc[0] * e, acc[1] * e, acc[2] * e); + } else if (grow && grow[0] === i && grow[1] === j) { + const g = (c - 0.2) / 0.08, x0 = vx(S, i), y0 = vy(S, i); + line(buf, x0, y0, x0 + (vx(S, j) - x0) * g, y0 + (vy(S, j) - y0) * g, acc[0] * ov, acc[1] * ov, acc[2] * ov); + } else line(buf, vx(S, i), vy(S, i), vx(S, j), vy(S, j), ov, ov, ov); + } + if (S.old && c >= 0.2 && c < 0.26) { + const e = (1 - (c - 0.2) / 0.06) * ov, [i, j] = S.old; + line(buf, vx(S, i), vy(S, i), vx(S, j), vy(S, j), acc[0] * e, acc[1] * e, acc[2] * e); + } + for (let v = 0; v < n; v++) { + const ang = (2 * PI * v) / n - PI / 2, lx = CX + (RX + 6) * cos(ang), ly = CY + (RY + 7) * sin(ang); + const touched = S.old && c >= 0.2 && c < 0.4 && (v === S.old[0] || v === S.old[1] || v === S.neu[0] || v === S.neu[1]); + if (touched) number(buf, S.a[v], lx, ly, acc[0] * ov, acc[1] * ov, acc[2] * ov); + else number(buf, S.a[v], lx, ly, ov, ov, ov); + } + } + + for (let y = 0, o = 0; y < H; y++) + for (let x = 0; x < W; x++, o += 3) display.setPixel(x, y, round(buf[o] * 255), round(buf[o + 1] * 255), round(buf[o + 2] * 255)); +} diff --git a/incoming/pattern-study/patterns/schlieren-texture.js b/incoming/pattern-study/patterns/schlieren-texture.js index e83a1cd47..ab6ca1efc 100644 --- a/incoming/pattern-study/patterns/schlieren-texture.js +++ b/incoming/pattern-study/patterns/schlieren-texture.js @@ -12,8 +12,8 @@ // @knobs Order=-1..1, Speed=0.2..3, Defects=10..30, Uncross=0..1 // // Knob 1: Order. The film's retardation at its mean thickness, 850 + 650 Order nm. At the opening 0 it is 850 nm, and the wedge spreads it from about 620 nm at the top of the panel to 1080 nm at the bottom, the whole second order in bands: blue, cyan, green, yellow, orange, red and magenta. At -1 it is 200 nm, first-order grey and white; at -0.5, 525 nm, first-order orange and the violet of the sensitive tint; at 1, 1500 nm, the greens and pinks of the third order. -// Knob 2: Speed. The polarizers turn at 0.03 Speed radians a second, 0.06 at the opening 2, and the brushes of a half-strength core turn twice as fast, a +1/2 core's with the polarizers and a -1/2 core's against them; crossed polarizers look the same after a quarter turn, 26 s at Speed 2, 17 s at 3. The film relaxes 10 Speed time units a second: at Speed 2 a lone pair 20 LEDs apart meets in 2.4 s and one 30 LEDs apart in 10 s. -// Knob 3: Defects. The film opens with this many cores and keeps at least this many: when annihilation leaves fewer, a new pair is let in 26 to 34 LEDs apart, at the best of 14 spots away from the cores already there, at most one every 0.8 s, and the panel dissolves to it over 0.7 s. One pair in five is a +1 and a -1 with four brushes each, which come apart into half-strength cores within a few seconds. 10 at the opening, 30 at the top. +// Knob 2: Speed. The polarizers turn at 0.03 Speed radians a second, 0.06 at the opening 2, and the brushes of a half-strength core turn twice as fast, a +1/2 core's with the polarizers and a -1/2 core's against them; crossed polarizers look the same after a quarter turn, 26 s at Speed 2, 17 s at 3. The film relaxes 10 Speed time units a second: at Speed 2 a lone pair of opposite cores 20 LEDs apart meets in about 3 s, 30 LEDs apart in about 11 s, 40 apart in 23 s. +// Knob 3: Defects. The film opens with this many cores and keeps at least this many: when annihilation leaves fewer, new pairs are let in, 26 to 34 LEDs apart at the best of 14 spots away from the cores already there, up to three pairs at once and at most every 1.5 s, and the panel dissolves to them over 0.5 s. One pair in five is a +1 and a -1 with four brushes each, which come apart into half-strength cores within a few seconds. 10 at the opening, 30 at the top. // Knob 4: Uncross. Turns the analyzer away from crossed by 90 Uncross degrees: at 0 the polarizers are crossed and the brushes black, at the opening 0.06 they are 5.4 degrees off, at 0.5 the colours have paled and the brushes gone, and at 1 the polarizers are parallel, the brushes white and the colours turned to their pale complements. // The director n is a line field: n and -n are the same, so its angle theta @@ -30,7 +30,7 @@ const W = 64, H = 128, N = W * H; const { floor, round, min, max, abs, sqrt, sin, cos, atan2, exp, PI } = Math; const cl = (v, a, b) => (v < a ? a : v > b ? b : v); const TAU = 2 * PI; -const FADE = 0.7, GAIN = 1.4, AGL = 0.15, XI = 1 / sqrt(AGL); +const FADE = 0.5, GAIN = 1.4, AGL = 0.15, XI = 1 / sqrt(AGL); // ---- Michel-Levy colours: white light through a plate of retardation G // between crossed polarizers transmits sin^2(pi G / lambda) of each @@ -217,11 +217,14 @@ export function update(dt, input, params) { S.phi += d * 0.03 * S.speed; relax(S, d * 10 * S.speed); if (++S.frame % 6 === 0) census(S); - if (S.count < S.want && S.t - S.lastPair > 0.8) { - // The panel dissolves from the last frame to the film with the new - // pair over FADE seconds. - pair(S); - census(S); + if (S.count < S.want && S.t - S.lastPair > 1.5) { + // Enough pairs at once to make up the count, three at most, and the + // panel dissolves from the last frame to the film with them over FADE + // seconds. + for (let m = min(3, Math.ceil((S.want - S.count) / 2)); m > 0; m--) { + pair(S); + census(S); + } S.lastPair = S.t; S.OLD.set(S.LAST); S.fade = 0; From af71852a289796593d9c1858a155c06d679330bf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 20:36:56 +0000 Subject: [PATCH 12/14] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2072=ED=8C=90:=20Schlieren=20Texture=20=EA=B3=BC=20Conway-Coxe?= =?UTF-8?q?ter=20Frieze?= 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_01Bc4JkrPaZ8GQQiA22PWEvR --- incoming/pattern-study/patterns/schlieren-texture.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/incoming/pattern-study/patterns/schlieren-texture.js b/incoming/pattern-study/patterns/schlieren-texture.js index ab6ca1efc..85dd361e7 100644 --- a/incoming/pattern-study/patterns/schlieren-texture.js +++ b/incoming/pattern-study/patterns/schlieren-texture.js @@ -35,11 +35,10 @@ const FADE = 0.5, GAIN = 1.4, AGL = 0.15, XI = 1 / sqrt(AGL); // ---- Michel-Levy colours: white light through a plate of retardation G // between crossed polarizers transmits sin^2(pi G / lambda) of each // wavelength. Summed against the CIE 1931 matching functions (the -// three-lobe fit of Wyman, Sloan and Shirley) from 400 to 700 nm and taken -// to sRGB, each channel divided by white's, then saturated SAT times over -// for the panel, kept in linear light. One entry each 2 nm up to 2400 nm. -// (Matching functions: Chris Wyman, Peter-Pike Sloan and Peter Shirley, -// 2013.) ---- +// multi-lobe fit of Chris Wyman, Peter-Pike Sloan and Peter Shirley, 2013) +// from 400 to 700 nm and taken to sRGB, each channel divided by white's, +// then saturated SAT times over for the panel and kept in linear light. One +// entry each 2 nm up to 2400 nm. ---- const SAT = 3, GMAX = 2400, GSTEP = 2, GN = GMAX / GSTEP + 1, ML = new Float32Array(GN * 3); { const g = (l, m, a, b) => exp(-0.5 * ((l - m) / (l < m ? a : b)) ** 2); From ced2cf93fc3a1d9b488129cffa7a8ef8b48d6b33 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 20:45:11 +0000 Subject: [PATCH 13/14] =?UTF-8?q?72=ED=8C=90=20=EC=97=B0=EA=B5=AC=20?= =?UTF-8?q?=EB=85=B8=ED=8A=B8=EB=A5=BC=20README=20=EC=97=90=20=EC=A0=81?= =?UTF-8?q?=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Bc4JkrPaZ8GQQiA22PWEvR --- incoming/pattern-study/README.md | 105 +++++++++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 5 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 7a9918be3..a8c2e5c26 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 608개를 쉰여섯 갈래로 나눠 만들었어요. 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판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 618개를 쉰여섯 갈래로 나눠 만들었어요. 1판에서 열세 갈래로 23개를 만들고, 2판에서 1판에 없던 아홉 갈래로 16개를, 3판에서 하나를, 4판에서 새 갈래인 직물로 하나를 더했어요. 5판은 새 패턴 없이 1판의 Euclid Pulse 를 다시 짰고, 6판은 세운 패널의 128줄을 위아래로 다 쓰는 패턴 하나를 더했어요. 7판은 또렷한 모양이 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 8판은 새 갈래인 기계로 하나를 더했어요. 9판은 새 패턴 없이 1판의 Sprout 을 다시 짰어요. 10판은 세운 패널을 떨어지는 블록의 우물로 쓰는 패턴 하나를 더했어요. 11판은 여러 색이 꽤 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 12판은 새 갈래인 천체로 하나를 더했어요. 13판은 새 패턴 없이 1판의 Glyph Rain 을 다시 짰어요. 14판은 절차가 바뀌기 전에 띄운 바퀴라서, 세운 패널의 높이를 다 떨어지는 폭포 하나를 더했어요. 15판은 바뀐 절차의 첫 판이라 열 갈래에 새 패턴을 하나씩, 모두 열 개를 더했어요. 도시와 음악 갈래는 그때 새로 열었어요. 16판도 열 갈래에 하나씩 열 개를 더했고, 바다와 곤충 갈래를 새로 열었어요. 17판도 열 갈래에 하나씩 열 개를 더했고, 전자 회로와 장난감 갈래를 새로 열었어요. 18판도 열 갈래에 하나씩 열 개를 더했고, 유리와 광물·결정, 놀이공원 갈래를 새로 열었어요. 19판도 열 갈래에 하나씩 열 개를 더했고, 부엌과 버섯·균사 갈래를 새로 열었어요. 20판도 열 갈래에 하나씩 열 개를 더했고, 새와 화학·실험 갈래를 새로 열었어요. 21판도 열 갈래에 하나씩 열 개를 더했고, 종이와 운동·경기 갈래를 새로 열었어요. 22판도 열 갈래에 하나씩 열 개를 더했고, 몸과 비행 갈래를 새로 열었어요. 23판도 열 갈래에 하나씩 열 개를 더했고, 무대·공연과 짐승 갈래를 새로 열었어요. 24판도 열 갈래에 하나씩 열 개를 더했고, 공사·건설과 도자기 갈래를 새로 열었어요. 25판도 열 갈래에 하나씩 열 개를 더했고, 농사·밭과 화산 갈래를 새로 열었어요. 26판도 열 갈래에 하나씩 열 개를 더했고, 얼음·극지와 미생물 갈래를 새로 열었어요. 27판도 열 갈래에 하나씩 열 개를 더했고, 철도와 필름·영화 갈래를 새로 열었어요. 28판은 주제 표가 수학 쪽으로 바뀐 첫 판이라 이름 있는 수학 대상 열 개를 열 갈래에 하나씩 더했고, 수론 갈래를 새로 열었어요. 29판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 첫 판이라, 이름 있는 수학 대상 여섯과 화장품 매장 벽에 틀 질감 넷을 더했고, 위상·매듭과 화장품 팝업 갈래를 새로 열었어요. 30판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 매장의 LED 벽에 틀 질감 넷을 화장품 팝업 갈래에 더했어요. 31판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 열지 않았어요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽의 질감 넷을 화장품 팝업 갈래에 더했어요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 41판은 주제 표가 수학·추상 다섯과 할로윈 다섯으로 바뀐 첫 판이라, 수학 대상 다섯을 다섯 갈래에 하나씩 더하고 할로윈 갈래를 새로 열어 10월 31일에 틀 패턴 다섯을 넣었어요. 42판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 41판이 연 할로윈 갈래에 더했어요. 43판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 44판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 45판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 열 개 아래인 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 46판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 47판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 48판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 49판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 50판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 51판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 52판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 53판은 할로윈 몫이 쉬게 된 첫 판이라, 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 54판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 55판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 56판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 57판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 58판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 59판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 60판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 61판도 수학 대상 열 개를 패턴이 적은 갈래부터 열 갈래에 하나씩 더했어요. 62판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 63판은 64판과 같이 주제 표에 「타이포」가 들어온 첫 판이라, 글자가 들어간 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 64판은 주제 표가 바뀌어 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 네 갈래에 더했어요. 65판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 66판도 같은 표로 돌아서 글자 패턴 넷을 글자 갈래에, 하나를 데이터 그림 갈래에 더하고, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 67판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 68판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 69판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 70판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 72판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. [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판에서 세로로 다시 짰어요. @@ -960,6 +960,21 @@ | [Granular Leidenfrost](patterns/granular-leidenfrost.js) | 입자 | 바닥이 쳐 올리는 상자에서 320알이 차가운 결정 덩어리로 뭉쳐 뜨거운 알갱이 기체 위에 뜨고, 흔들기를 줄이면 바닥에 내려앉고 키우면 뚜껑에 눌리며, 알을 덜 비탄성으로 하면 결정이 녹아요. | | [Multiplicative Chaos](patterns/multiplicative-chaos.js) | 노이즈·흐름 | 로그 상관 가우스장을 깊은 파랑 부조로 깔고 그 지수로 만든 카안의 측도를 금빛으로 띄우면, γ 가 오를수록 질량이 장의 가장 높은 몇 LED 로 모여 흰 점이 돼요. | +### 72판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Biliteral Cipher](patterns/biliteral-cipher.js) | 글자 | 다섯 자씩 한 줄로 친 겉글을 읽는 빛이 글자마다 a 활자인지 b 활자인지 적고, 다섯이 차면 베이컨의 표대로 숨은 글자 하나가 아래 줄에 서요. | +| [Optical Kerning](patterns/optical-kerning.js) | 글자 | 낱말 다섯이 상자 사이를 같게 띄워 섰다가 글자 사이 흰 자리가 다 같은 넓이가 될 때까지 옆으로 미끄러지고, 틈마다 넓이를 말하던 빛이 한 색으로 모여요. | +| [Spray Stencil](patterns/spray-stencil.js) | 글자 | 「취급주의」를 오린 철판 위로 분무기가 지그재그로 내려가며 물감을 뿌리고, 판이 걷히면 다리 자리를 틈으로 남긴 글자가 골판지 상자에 패널 높이로 서요. | +| [Clothesline](patterns/clothesline.js) | 글자 | 줄 넷에 집게로 꽂은 「바람이 분다 살아야겠다」 카드가 왼쪽에서 건너오는 돌풍에 차례로 흔들리고 비틀렸다가 곧은 자리를 지나 되흔들려요. | +| [Autogram](patterns/autogram.js) | 글자 | 제 글자 수를 늘어놓은 영어 문장을 글자마다 세어 주장과 맞춰 보고, 주장을 흔들면 틀린 문장을 센 수로 거듭 다시 쓰다 고리에 갇혀요. | +| [Ruppert Refinement](patterns/ruppert-refinement.js) | 보로노이·셀 | 자홍 바늘로 가득한 다각형 삼각 분할에 외심과 선분의 가운데 점을 하나씩 꽂아, 가장 작은 각이 기준을 넘을 때까지 바늘이 초록과 파랑 삼각형으로 바뀌어요. | +| [Lattice Reduction](patterns/lattice-reduction.js) | 기하·대칭 | 흰 격자점은 그대로 선 채 가늘고 긴 평행사변형 타일이 b1 을 따라 미끄러지고 두 화살이 자리를 바꾸며, 짧고 곧은 기저의 타일로 줄어들어요. | +| [Hydra Game](patterns/hydra-game.js) | 알고리즘 | 머리를 자를 때마다 한 칸 아래 마디에서 가지가 n 벌 돋아 히드라가 부채처럼 커져도, 위에 적힌 순서수는 매번 줄고 끝내 뿌리만 남아요. | +| [Schlieren Texture](patterns/schlieren-texture.js) | 물리·스프링 | 교차한 편광판 사이 네마틱 막에서 ±1/2 전위마다 검은 붓자국 둘이 나와 편광판이 돌면 서로 반대로 돌고, 반대 부호 전위가 만나 사라지는 동안 둘째 차수 간섭색이 띠를 이뤄요. | +| [Conway-Coxeter Frieze](patterns/coxeter-frieze.js) | 수론 | 칠각형의 삼각 분할이 정하는 정수 띠가 세로로 서서 모든 마름모가 ad − bc = 1 을 지키며 내려가고, 대각선 하나가 뒤집힐 때마다 바뀐 수가 하얗게 번쩍여요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -3898,6 +3913,70 @@ Lab 에서 해 보기: Text 레이어에 Silkscreen 으로 QRN 을 써요. size 주제 5 「수학 · 추상」으로 장피에르 카안(Jean-Pierre Kahane)의 가우스 곱셈 카오스를 노이즈 갈래에 세우고, 주제 1 로 Agate, Percolation, Sunglint 셋뿐이던 촘촘함 × 복잡도 4,4 칸을 겨눴어요. 촘촘함이 0.297, 복잡도가 2.172 를 넘는 칸이에요. 56판 Multiplicative Cascade 는 패널을 열세 번 반으로 나눈 만델브로의 이산 캐스케이드였고, 그 README 가 다음으로 카안의 연속판을 짚었어요. 카안은 「Sur le chaos multiplicatif」(Ann. Sci. Math. Québec 9, 1985)에서 거리 r 의 공분산이 log(1/r) 처럼 가는 가우스장 X 로 측도 exp(γX − γ²E[X²]/2)dx 를 만들었어요. 공분산을 그렇게 맞추면 임계값은 γ = √(2d) 이고 평면에서는 2 예요. 측도는 X 가 γ log(1/ε) 언저리인 γ-두꺼운 점 위에 얹히고, 그 점들의 차원 2 − γ²/2 는 γ 가 2 로 가면 0 이 돼요. 이 둘은 레미 로드(Rémi Rhodes)와 뱅상 바르가스(Vincent Vargas)의 개관(Probability Surveys 11, 2014)이 정리한 대로 적었어요. 패널의 X 는 64×128 토러스에서 격자 라플라시안의 고유 모드 8,191 개로 짰어요. 모드의 표준편차를 √(2π/(λN)) 로 두어 공분산이 log(1/r) 꼴이 되게 했고, 재 보니 C(r) − C(2r) 가 r = 1, 2, 4 LED 에서 0.71, 0.71, 0.68 로 log 2 = 0.69 언저리였어요. 모드는 저마다 시간에 매끈하게 만든 오른슈타인-울렌벡 과정으로 떠돌아서, Speed 2 에서 128줄 긴 파동은 8초, 가장 잘은 파동은 1.6초에 제 값을 잊어요. 여는 자리의 장은 LED 의 4분의 1 까지 내려가요. LED 아래 두 옥타브는 분산 1.39 의 정규값을 LED 마다 따로 뽑아 넣어서, 장 전체의 분산 6.37 가운데 그만큼이 LED 마다 독립이에요. 장은 왼쪽 위에서 빛을 받은 깊은 파랑 부조로 그렸어요. 측도는 각 LED 가 프레임 질량에서 차지하는 몫으로 그려서, 고른 몫의 0.14 배부터 장밋빛으로 켜지고 주황과 금빛을 지나 33배에서 흰빛이 돼요. 장 스무 개를 평균하니 γ = 1 에서 LED 의 3분의 1 이 빛나고, 밝은 1% 가 질량의 52% 를 갖고 78 LED 가 절반을 가져요. 1.6 에서는 9% 가 빛나고 6 LED 가 절반을 갖고, 2.4 에서는 한두 LED 가 절반을 가져요. Cutoff 는 장이 내려가는 가장 잘은 크기를 4분의 1 LED 에서 12.5 LED 까지 올려요. 한 LED 에서는 금빛 레이스가 부드러운 구름이 되고, 12.5 에서는 LED 95% 가 흐린 녹슨 빨강으로 고르게 빛나요. 첫 판은 LED 아래 옥타브 없이 장을 부조로 그리고 금빛을 반투명하게 얹었는데, 촘촘함 0.237, 복잡도 2.692 였어요. 격자 장은 이웃 LED 끼리 상관이 0.685 라, 밝기 하나로 칠하면 문턱 하나로 둘로 가르는 가장 거친 칠도 촘촘함이 (1/π)arccos(0.685) = 0.26 이에요. LED 아래 두 옥타브를 넣어 상관을 0.54 로 낮추고 부조의 밝기를 0.02 에서 1 까지 다 쓰자 촘촘함이 0.346 이 됐어요. 양 끝에서 잘리는 LED 가 늘면서 복잡도는 칸 위 끝 2.7352 에 붙어 있던 2.692 에서 2.359 로 내려왔어요. 잰 값은 움직임 0.249, 촘촘함 0.346, 가장자리 0.787, 복잡도 2.359, 채움 0.753, 색 0.431, 모양 바뀜 0.070 이고, 노브는 0.31, 0.21, 0.09, 0.25 예요. 새로 본 것은 8,192 LED 위에서는 질량이 γ = 2 보다 한참 앞에서 얼어붙는다는 거예요. 질량이 앉은 높이는 γ = 1 에서 0.93 E[X²] 로 γE[X²] 를 따르다가 1.3 에서 1.12, 1.6 에서 1.27 로 처지고, 장의 최댓값 1.47 E[X²] 에 가서 붙어요. 격자가 커지면 최댓값은 2 E[X²] 로 가지만, 이 패널에서는 1.47 이 끝이에요. +## 72판 + +72판은 71판(#334)과 같이 돌았어요. 앞 바퀴와 같이 도는 바퀴의 PR 72개에는 OWNER, MEMBER, COLLABORATOR 가 쓴 글 가운데 일꾼 글이 아닌 것이 하나도 없어서 따를 글이 없었어요. 71판이 PR 본문에 먼저 밝힌 열 개와 지도 칸 둘을 읽고 겹치지 않게 골랐어요. 처음에는 낱말의 글자가 자리를 옮겨 다른 낱말이 되는 애너그램을 세로로 세우려 했는데, 71판의 Sator Square 가 25자를 PATERNOSTER 십자로 다시 모아서 같은 수법이라 Spray Stencil 로 바꿨어요. 타이포 다섯은 「타이포 실험」의 출발점을 하나씩 맡았어요. Biliteral Cipher 는 글자로 짠 무늬, Optical Kerning 은 글자와 빛, Spray Stencil 은 글자가 만들어지는 과정, Clothesline 은 글자의 몸, Autogram 은 숫자와 기호 놀이예요. 셋이 글자 꼴을 새로 그렸고, Biliteral Cipher 는 Photon Disk 의 5×7 대문자를, Autogram 은 Typewriter 의 3×5 대문자와 Photon Disk 의 5×7 숫자를 옮겨 썼어요. 한글은 Spray Stencil 과 Clothesline 이 열아홉 음절을 따로 그렸어요. 수학 다섯은 70판까지 든 608개와 71판의 열 개에 없던 대상이고, Ruppert Refinement 만 「다음 판 감」에 적혀 있던 것이에요. 「낯선 수학 예」에서는 하나도 고르지 않았어요. 주제 1 은 넷뿐인 칸 가운데 움직임 × 색 1,4 를 Ruppert Refinement 로, 가장자리 × 색 1,4 를 Schlieren Texture 로 겨눴고, 주제 4 는 Spray Stencil 과 Conway-Coxeter Frieze 가 맡았어요. + +### Biliteral Cipher + +주제 7 「타이포」의 「글자로 짠 무늬」로, 글의 뜻이 아니라 글자를 찍은 활자가 말을 싣는 그림이에요. 프랜시스 베이컨(Francis Bacon)이 『De Augmentis Scientiarum』(1623)에 적은 두 글자 암호는 겉글을 a 와 b 두 벌 활자로 짜요. 다섯 자마다 a 와 b 의 차례가 숨은 글자 하나이고, 표는 A = aaaaa 에서 Z = babbb 까지 24자이며 I 와 J, U 와 V 가 한 칸을 나눠요. 베이컨이 든 보기는 「Manere te volo donec venero(내가 올 때까지 머물러라)」에 FUGE(달아나라)를 숨긴 것이에요. 패널에서는 Photon Disk 의 5×7 대문자를 두 배로 키운 10×14 글자가 한 줄에 다섯씩 22줄 간격으로 서서, 한 줄이 꼭 한 묶음이에요. 64열 가운데 62열을 써서 낱말 사이를 띄울 자리가 없어요. 그래서 3 LED 틈 가운데에 1×2 점을 찍어 로마 비문의 가운뎃점처럼 낱말을 갈랐어요. 읽는 빛이 글자를 하나씩 지나며 그 아래에 3×5 소문자 a 나 b 를 적고, 다섯이 차면 그 줄이 잠깐 빛나고 숨은 글자가 112~125째 줄에 위에서부터 자라요. 묶음을 채우지 못한 끝 글자는 흐리게 두고 읽지 않아서, 23자인 베이컨의 글은 ERO 가 남아요. b 활자는 같은 비트맵을 기울이고 굵힌 것이고, Tell 이 그 거리를 정해요. 여는 값 0.06 에서는 14줄에 0.22 LED 기울고 0.12 LED 굵어서 가장자리 LED 몇 개가 몇 단계 밝아질 뿐이라, 8배로 키운 그림에서야 얼룩으로 보여요. 1 에서는 3.6 LED 기운 굵은 이탤릭이라 빛 없이도 숨은 낱말이 읽혀요. 베이컨 말고 넷은 이 패턴에서 지은 글이에요. THE SEA IS CALM AND STILL TODAY 가 STORM 을, SAY NOTHING TO ANYONE YET 이 TELL 을, WE ARE ALL QUITE WELL HERE 가 HELP 를, BURN THIS LETTER AT ONCE 가 KEEP 을 품어요. 패턴은 켤 때 다섯 글을 다시 풀어 보고, 숨은 낱말과 맞지 않으면 멈춰요. Tell 은 노브 넷 가운데 반응이 가장 작았어요. 기울기 2.6 LED, 굵기 1.1 LED 의 첫 판은 0.030 이었고 굵기를 2 LED 까지 올리자 0.042 가 됐어요. 그 끝에서는 W, R, M 의 좁은 속이 조금 메워져요. + +Lab 에서 해 보기: Text 레이어에 b 한 자를 Newsreader 로 size 110, 색 #102038, blend screen, 효과 없이 x 32, y 56 가운데 맞춤으로 두면 겉글 뒤에 둘째 활자가 물자국처럼 엷게 깔려요. 패널은 1~108째 줄과 112~125째 줄이 차 있어서 또렷한 낱말을 얹을 빈자리는 없어요. + +기능 감: Lab 미리보기를 방 건너편에서 보는 크기로 흐리거나 줄여 보는 단추요. Tell 0.06 에서 「얼핏 봐서는 모른다」는 것을 4~8배로 키운 PNG 로만 가늠했어요. + +### Optical Kerning + +주제 7 「타이포」의 「글자와 빛」이에요. 64판 Counterforms 가 글자 속 빈 자리를 켰고, 그 뒤 「다음 판 감」이 글자 사이 흰 자리를 짚어 둔 것을 받았어요. 높이 19 LED, 획 2.5 LED 의 대문자로 짠 낱말 다섯이 25줄 간격으로 4째 줄부터 122째 줄까지 서요. 낱말은 AVOW, LYNX, TOY, WAVE, FLY, YAWL, HILT, VOLT, AWAY, OVAL 열 가운데 다섯이 패널에 있어요. 낱말은 먼저 글자 상자 사이를 2 LED 씩 똑같이 띄워 서고, 그러면 틈의 넓이가 쌍마다 달라요. L Y 는 137, L T 는 140, A V 는 112 LED² 이고 H I 와 I L 은 38 이에요. 1.15초 뒤 글자가 옆으로 미끄러져 모든 틈이 68 LED², 곧 19줄 평균 너비 3.6 LED 가 돼요. 틈은 제 넓이를 말하는 색으로 빛나서 좁으면 보랏빛 파랑, 넓으면 호박색이고, 다 맞춰지면 한 낱말의 빛이 모두 청록으로 모여요. 줄이 0.45초씩 늦게 시작해 다시 띄우기가 물결처럼 패널을 내려가요. 틈 아래 작은 막대는 틈의 평균 너비라서 맞춰진 낱말에서는 길이가 같아요. 넓이는 대문자 높이 안에서 줄마다 마주 보는 두 윤곽 사이 거리를 세는데, 열린 쪽은 글자의 가장 바깥 점에서 Depth 만큼만 들어가서 세요. A, V, L, T 의 열린 목을 끝까지 세면 그 쌍은 겹쳐야만 목표에 닿으니, 넓이로 간격을 맞추는 도구들이 고르는 값이 이 깊이예요. 빛은 센 흰 자리만 덮어서 Depth 를 돌리면 목 안에서 빛이 잘리는 자리가 옮겨요. 여는 3.2 LED 에서는 열 낱말의 모든 쌍이 목표에 닿아요. 첫 판은 4.7 LED 에서 열려 여덟 쌍이 닿지 못해서 다시 매겼어요. 1 의 8 LED 에서는 A 가 V 아래로 0.6, L 의 발이 Y 의 팔 아래로 3.4 LED 들어가도 L Y, L T, F L 은 맞닿지 않고는 흰 자리를 다 덜어 내지 못해 연두로 남아요. 비스듬한 획이 나란히 달리는 V Y 와 A X, 위 가로대가 둘인 F T 는 끝이 먼저 닿아 목표에 닿을 수 없어서, WAVY 와 TAXI, WAFT 는 낱말에서 뺐어요. 데이비드 킨더슬리(David Kindersley)는 『Optical Letter Spacing for New Printing Systems』(1976 판)에서 간격을 식으로 정했고 월터 트레이시(Walter Tracy)는 『Letters of Credit』(1986)에 간격 맞추는 법을 적었어요. 두 방법이 넓이에 기댄 것인지는 확인하지 못해서 Lineage 에 곁으로만 적었어요. + +Lab 에서 해 보기: Text 레이어에 AVOW 를 Inter 로 size 22, 색 #ff3c78, blend screen, 효과 없이 x 32, y 14 가운데 맞춤으로 두면 Inter 의 커닝 표가 띄운 AVOW 가 첫 줄에 겹쳐서, 넓이로 띄운 간격과 어디서 갈리는지 보여요. + +기능 감: Text 레이어에 자간 값이 없어요. 노브로 자간을 바꾸는 Text 레이어가 있으면 이 패턴의 Tracking 과 나란히 견줄 수 있어요. + +### Spray Stencil + +주제 7 「타이포」의 「글자가 만들어지는 과정」과 「세로 패널다운 글」이고, 주제 4 「세로 64×128 에서 잘 읽히는 것」도 맡았어요. 상자에 뿌려 찍는 「취급주의」를 패널 높이로 세웠어요. 스텐실은 판 한 장이라 글자 속 닫힌 자리를 그대로 오리면 그 조각이 떨어져 나가요. 그래서 ㅇ 의 고리는 위아래를 끊고, ㄱ ㄴ ㅁ ㅂ ㅍ 의 모서리와 ㅅ ㅈ ㅊ 의 꼭대기, ㅏ ㅓ 의 팔은 2 LED 너비의 판을 남겨 끊었어요. ㅜ 와 ㅗ 의 이음은 아무것도 두르지 않아서 이어 두었어요. 열 음절(취급주의, 화기엄금, 파손주의)은 이 패턴에서 직사각형과 두께 4.2~4.5 LED 의 사선, 타원 고리로 그렸고 옮겨 쓴 글꼴은 없어요. 음절은 52×28 상자에 가로획 3~4 LED, 세로획 5 LED 이고, 넷이 3, 34, 65, 96째 줄에서 시작해 3째 줄부터 123째 줄까지 서요. 판의 남은 자리를 네 방향 flood fill 로 세어 보니 세 판 다 한 조각이었어요. 첫 판은 「화」의 ㅎ 고리 안에 섬이 하나 남았는데, 다리가 고리를 다 건너지 못해서였어요. 회색 금속 판이 크라프트 골판지 상자에 놓이면 분무기가 판을 13번 지그재그로 쓸어 내려가요. 한 판에 물방울 26,000개가 가우스 꼴로 떨어지고, 판 위에는 45% 까지만 어둡게 쌓여서 오린 자리가 읽혀요. 판이 오른쪽으로 걷히면 하늘빛 글자가 다리 자리를 빈 틈으로 남긴 채 상자에 서고, 상자가 아래로 빠지면 새 상자와 다음 판이 들어와요. 판은 묵은 물감의 35% 를 들고 다음 상자로 가요. 크라프트는 (176, 121, 66) 이라 진흙이 아니라 골판지로 읽혀요. Overspray 1 에서는 퍼짐이 9.6 LED 이고, 오린 자리에서 1 LED 떨어진 판 위에 내린 물감의 27% 가 판 밑으로 스며 글자에 무리가 져요. 첫 판의 사선은 줄마다 가로 너비로 재서 ㅅ ㅈ ㅊ 의 다리가 2 LED 쯤으로 가늘었고, 참 획 두께로 고쳤어요. 물감을 판 위에도 다 쌓은 판은 판 전체가 파래져 오린 자리가 묻혔어요. + +Lab 에서 해 보기: Text 레이어에 FRAGILE 을 Silkscreen 으로 size 8, 색 #1a1a1a, blend multiply, x 32, y 64 가운데 맞춤으로 두면 칠한 글자 위에 검은 고무 도장이 하나 더 찍힌 것처럼 보여요. + +기능 감: 패턴끼리 같이 쓰는 큰 한글 획 글꼴이요. 자모를 직사각형, 사선, 고리로 적은 표가 있으면 좋겠어요. 펌웨어의 Matrix Fonts 에는 한글이 없고, 있는 9×11 조합 글꼴은 28줄 음절에 작아서 열 음절의 좌표를 손으로 적었어요. + +### Clothesline + +주제 7 「타이포」의 「글자의 몸」으로, 글자가 중력과 바람을 받는 그림이에요. 64판 Letter Sand 와 69판 Letter Melt 는 글자가 무너지거나 녹고, 여기서는 글자 꼴은 그대로인 채 몸이 통째로 흔들려요. 5, 37, 69, 101째 줄에 맨 줄 넷이 벽에서 벽으로 5 LED 처진 현수선(c = 99 LED)으로 걸리고, 16×18 LED 카드 열 장이 20 LED 간격으로 집게에 꽂혀 「바람이 / 분다 / 살아야 / 겠다」를 읽어요. 폴 발레리(Paul Valéry)의 「해변의 묘지(Le Cimetière marin)」(1920)에 나오는 「Le vent se lève, il faut tenter de vivre」를 흔히 옮기는 말이에요. 카드는 위 가운데 집게에 매달린 단단한 진자이고, 돌풍의 앞선이 왼쪽에서 들어와 패널을 건너서 한 줄의 카드가 0.3초쯤 사이를 두고 차례로 흔들려요. 바람은 보퍼트 풍력 계급(Francis Beaufort, 1805)으로 매겨 평균 풍속이 0.836 B^1.5 m/s 예요. 여는 풍력 2 의 2.4 m/s 에서 카드가 7도 기울고, 돌풍에 20~35도까지 갔다가 곧게 선 자리를 조금 지나 돌아와요. 풍력 7 에서는 79도로 거의 눕고 줄의 처짐이 1 LED 로 당겨져요. 돌아간 글자는 LED 마다 카드 좌표로 되돌려 미리 구운 거리장(LED 당 4표본)을 읽어서, 획 1.3 LED 의 글자가 기울어도 매끈하게 읽혀요. 카드가 90도 넘게 비틀리면 어두운 뒷면에 잉크가 18% 로 뒤집혀 비쳐요. 아홉 음절(바 람 이 분 다 살 아 야 겠)은 획으로 그렸고 ㅇ 은 타원이에요. 처음에는 프랑스어 LE VENT 와 SE LEVE 도 줄에 걸려 했는데, 한국어만 줄 넷에 3, 2, 3, 2장으로 걸자 카드를 16 LED 로 키울 수 있어 더 잘 읽혔어요. 첫 판은 돌풍이 약하고 흔들림을 세게 눌러 카드가 20도 언저리에서 멈췄고 움직임이 0.118 이었어요. 돌풍을 키우고 카드가 제 흔들림에서 받는 끌림을 반으로 줄이자 0.275 가 되고, 카드가 곧은 자리를 지나 되흔들려요. 색 노브는 바다빛 초록에서 장밋빛 빨강까지만 돌아요. 온 바퀴를 돌리면 0.6 언저리에서 카드가 갈색이 됐어요. + +Lab 에서 해 보기: Text 레이어에 LE VENT 를 Silkscreen 8, 크림색 #f2e6c8, normal, 효과 없이 x 32, y 33 가운데 맞춤으로 두면 첫 줄 카드 아래 8줄 틈에 들어가요. 같은 꼴로 SE LEVE 를 y 65 에 하나 더 두면 프랑스어 원문이 한국어 카드 사이에 끼어요. + +기능 감: Text 레이어의 「코드로 복사」가 비트맵만 내줘서, 돌고 기우는 글자를 만들려고 한글 아홉 자를 획으로 손수 그렸어요. 글자를 거리장이나 획 목록으로 내보내는 길이 있으면 좋겠어요. + +### Autogram + +주제 7 「타이포」의 「숫자와 기호 놀이」로, 문장이 제 글자를 세는 그림이에요. 리 샐로스(Lee Sallows)가 이름 붙인 오토그램은 제가 쓴 글자마다 몇 번 나오는지 늘어놓아 그 말이 참인 문장이에요. 샐로스의 첫 오토그램은 더글러스 호프스태터(Douglas Hofstadter)가 『Scientific American』에 쓰던 「Metamagical Themas」 1982년 1월 호에 실렸어요. 63판 Kaprekar's Routine 은 숫자를 되풀이해 6174 에 닿고, 여기서는 말이 제 말의 글자를 세요. 세 문장은 이 패턴을 위해 찾았어요. 아무 수에서 문장을 쓰고 세어, 틀린 주장마다 0.35 의 확률로 센 수를 옮겨 적고 5,000걸음마다 다시 시작하는 Node 탐색이에요. 첫째는 THIS SENTENCE HAS THREE A'S, TWO C'S, TWO D'S, TWENTY-NINE E'S 로 시작해 THREE X'S AND THREE Y'S 로 끝나는 글자 152개, 열아홉 가지의 문장이에요. 나머지 둘은 THIS PANEL SHOWS(146개)와 THESE LIGHTS SPELL(145개)로 시작해요. ONLY A FOOL WOULD CHECK THAT THIS LINE HAS 로 시작하는 문장은 400만 걸음 동안 하나도 나오지 않았어요. 세 문장은 따로 짠 셈으로 맞춰 봤고, 패턴도 켤 때 문장을 주장에서 다시 지어 글자까지 같은지, 주장이 다 참인지 세어 보고 틀리면 멈춰요. 패널에서는 Typewriter 의 3×5 대문자로 한 줄 16자, 6줄 간격으로 15~17줄이 서요. 검사는 알파벳 차례로 글자 하나씩 가요. 그 글자의 주장이 호박색이 되고, 글자가 읽는 차례로 하나씩 청록으로 켜지며, Photon Disk 의 5×7 숫자로 쓴 셈 줄이 「E 17<29」 꼴로 센 수와 주장을 견주다 다 세면 = 가 돼요. 아래 막대 열아홉은 센 수가 주장 눈금까지 오르는 것이에요. 다 맞으면 TRUE 가 서요. Doubt 를 올리면 주장 몇 개를 1~3 바꾼 문장을 검사해서 틀린 주장이 빨갛게 남고, 그다음 센 수로 문장을 다시 써요. 수 낱말이 바뀌면 다른 글자의 수가 바뀌어 또 틀려요. 다시 쓰기는 1.5초에서 시작해 한 번에 0.8배씩 빨라져 1초에 240번까지 가고, 지난 주장을 다 기억해 같은 것이 다시 나오면 LOOP 와 고리 길이를 적어요. 주장을 1~4개 흔들어 1,200번씩 해 보니 THIS SENTENCE HAS 는 1,042번이 길이 453 의 고리에 갇혔고, 참에 닿은 것은 세 번이었어요. 둘은 처음 문장으로, 하나는 아직 적지 않은 다른 오토그램으로 갔어요. 갇히기까지는 길어야 1,315번이었어요. 처음에는 다시 쓰기를 고르게 느리게 두었는데, 흔든 문장 800개 가운데 60번 안에 되풀이에 닿은 것이 없어서 쫓기가 빨라지게 바꿨어요. + +Lab 에서 해 보기: Text 레이어에 ? 한 자를 Newsreader 로 size 100, 색 #1a0808, blend screen, 효과 없이 x 32, y 50 가운데 맞춤으로 두면 문장 뒤에 엷은 물음표가 깔려요. Doubt 를 올려 틀린 주장이 빨갛게 남을 때 어울려요. + +기능 감: 패턴 코드가 import 없이 같이 쓰는 픽셀 글꼴 표(3×5 와 5×7)요. Biliteral Cipher 와 이 패턴이 Typewriter 와 Photon Disk 의 글꼴 표를 손으로 옮겨 실었어요. + +### Ruppert Refinement + +주제 5 「수학 · 추상」이고, 주제 1 로 넷뿐이던 움직임 × 색 1,4 칸을 겨눴어요. 68판 「다음 판 감」이 Delaunay Flips 다음으로 적어 둔 것을 받았어요. 짐 루퍼트(Jim Ruppert)의 들로네 다듬기(J. Algorithms, 1995)는 경계 선분을 지키는 제약 들로네 삼각 분할에서 시작해요. 어떤 점이 선분의 지름 원 안에 들면 그 선분을 가운데에서 나누고, 아니면 가장 작은 각이 기준보다 작은 삼각형의 외심에 점을 꽂아요. 그 외심이 선분을 침범하면 꽂지 않고 선분을 나눠요. 선분끼리 90도보다 좁게 만나지 않으면 기준 20.7° 까지는 반드시 끝난다는 것이 루퍼트의 증명이에요. 패널의 영역은 여섯이에요. 위아래에 성가퀴를 낸 판, 홈과 마름모 구멍을 낸 판, 끝마다 작은 마름모 구멍 셋을 낸 팔각형, 40 LED 깊이 홈을 낸 판, 63 LED 높이 홈을 낸 판, 계단이에요. 여는 영역은 꼭짓점 36개에 삼각형 34개이고 가장 작은 각이 2.0° 라서, 판 끝에서 끝으로 달리는 자홍 바늘로 차 있어요. 걸음마다 침범된 선분은 엷은 호박색 지름 원으로, 고칠 삼각형은 흰 테와 외접원으로 보이고, 점이 들어가면 로슨 뒤집기가 다시 들로네로 맞춰요. 기준 20° 에서 여는 영역은 새 점 40개(가운데 점 18, 외심 22)와 삼각형 96개로 15.4초에 끝나요. 여섯 영역 모두 20, 26, 33° 에서 끝난 메시의 가장 작은 각이 기준 이상이었고, 삼각형 넓이의 합이 다각형 넓이와 늘 같았어요. 루퍼트의 걸음만으로는 메시가 거칠었어요. 첫 판은 삼각형 20~40개로 4초 만에 끝나서, 바늘이 다 사라진 뒤에는 외접원 반지름이 11 LED 를 넘는 삼각형도 나누게 했어요. 조너선 슈척(Jonathan Shewchuk)의 Triangle(1996)이 넓이 상한을 두는 것과 같은 길이에요. 바늘이 늘 먼저라 2~8초는 바늘을 지우는 시간이에요. 처음 그린 영역들은 꼭짓점이 옆으로 흩어져 첫 삼각 분할부터 초록이 많았어요. 꼭짓점을 패널 위아래로 몰자 바늘에서 시작했어요. + +### Lattice Reduction + +주제 5 「수학 · 추상」으로 2차원 격자 기저 줄이기를 기하 갈래에 세웠어요. 격자는 두 벡터의 정수 결합 전부이고, 같은 격자를 내는 기저는 끝없이 많아요. 조제프루이 라그랑주(Joseph-Louis Lagrange)가 1773년에 이차 형식을 줄이며 쓰고 가우스가 『Disquisitiones Arithmeticae』(1801)에서 정리한 걸음은 b2 에서 b1 을 q = round(⟨b1,b2⟩/|b1|²) 번 빼고, b2 가 b1 보다 짧으면 둘을 바꾸는 것을 |μ| ≤ 1/2 이 될 때까지 되풀이해요. 정수의 유클리드 호제법을 벡터로 한 셈이고, 어느 차원에서나 하는 길이 아르연 렌스트라와 헨드릭 렌스트라, 로바스 라슬로(Arjen Lenstra, Hendrik Lenstra, László Lovász)의 LLL(1982)이에요. 패널에서 격자점은 흰 점으로 서서 움직이지 않아요. 판은 지금 기저의 기본 평행사변형으로 깔리고, 타일은 (ca·i + cb·j) mod 3 으로 칠해서 변을 맞댄 둘이 늘 달라요. 시작 기저는 줄인 기저에 늘이는 쪽 부호의 밀기를 번갈아 걸어 만들어서 타일이 가늘고 긴 조각이에요. 밀기 한 번은 0.55초 동안 타일이 b1 을 따라 미끄러지다 모서리가 다시 점 위에 내려앉는 것으로 보이고, 바꾸기는 0.4초에 두 화살의 색이 엇갈려요. 여는 값에서는 밀기 2~3번, 바꾸기 1~3번으로 2.2~4.2초에 끝나고, 짧고 곧은 기저의 타일이 은은히 빛나며 2.6초 선 뒤 다음 격자로 넘어가요. 격자는 정사각, 육각, 1.6:1 직사각, 아무 꼴을 차례로 같은 넓이에 아무 방향으로 놓아요. Skew 7 에서는 긴 벡터가 간격의 25배까지 가서 타일이 1 LED 보다 가늘고, 첫 밀기 전까지는 모아레예요. 육각 격자에서 μ 가 꼭 ±0.5 이면 q 가 1 과 −1 을 오가며 끝나지 않아서, 멈춤 조건을 q = 0 에서 |μ| ≤ 0.5 로 바꿨어요. 격자 사이를 검정으로 넘기던 판은 모양 바뀜이 0.461 로 축 끝 0.413 을 넘어서, 앞 화면에서 바로 겹쳐 넘기게 하자 0.199 가 됐어요. + +### Hydra Game + +주제 5 「수학 · 추상」으로 알고리즘 갈래에 세웠어요. 로리 커비(Laurie Kirby)와 제프 패리스(Jeff Paris)의 히드라(「Accessible independence results for Peano arithmetic」, Bulletin of the London Mathematical Society, 1982)는 뿌리 있는 나무예요. 헤라클레스가 n 째 걸음에 머리 하나를 자르면, 그 목이 뿌리에서 났으면 그냥 없어지고, 아니면 한 칸 아래 마디에서 잘린 가지가 n 벌 새로 돋아요. 어떻게 잘라도 히드라는 끝내 죽는데, 페아노 산술로는 그것을 증명할 수 없어요. 증명은 나무에 칸토어 표준형 순서수를 매겨요. 마디의 순서수는 자식들 순서수의 ω 거듭제곱을 더한 것이고, 머리를 자를 때마다 나무가 아무리 커져도 이 수는 줄어요. 패널에서는 뿌리가 바닥 가운데에 있고 목이 위로 자라요. 목은 마디에서 2.8 LED, 머리에서 1.4 LED 로 가늘어지고, 자르면 하얗게 번쩍이며 머리가 떨어지고, 새 가지는 금빛으로 돋아 부채처럼 퍼져요. 맨 위에 걸음 n 과 머리 수가, 그 아래에 순서수가 서요. ω 는 5칸 너비로 그리고 지수는 4줄 올렸어요. 자르는 빠르기는 1초에 0.9·Speed·(1 + n/8) 번이라 걸음이 늘수록 빨라져요. 여는 히드라 ω²+ω+1 은 마디 7개에서 시작해, 오른쪽부터 자르면 576개까지 컸다가 1,149째 걸음, 23.5초에 죽어요. 뿌리만 남으면 고리가 퍼지고 DEAD 가 서요. 왼쪽부터 자르면 30걸음, 가장 깊은 머리부터 자르면 23걸음이라 순서가 싸움의 길이를 50배까지 바꿔요. 화면 안에서 죽는 히드라를 찾으려고 따로 시뮬레이션을 돌리니, 네 순서 다 마디 8,000개 아래에서 죽는 것은 목이 둘 높이까지이거나 셋을 이은 한 줄(ω^ω)뿐이었어요. ω^ω+1 을 오른쪽부터 자르면 28,798째 걸음에 마디가 20,000개를 넘어요. 그래서 마디 사이를 50 LED 로 띄웠어요. 오른쪽부터 자르는 ω² 이 m 째 걸음에 홀로 남으면 (m+3)·2^(m+1) − 3 째 걸음에 죽는 것을 일곱 경우로 맞춰 봤어요. ω²·2 는 557,053째 걸음에 마디 278,528개로 죽어요. 첫 판은 목이 1 LED 라 켜진 몫이 0.13 이고 노브 반응이 0.01~0.03 이어서, 목을 굵게 하고 빛을 둘렀어요. + +### Schlieren Texture + +주제 5 「수학 · 추상」으로 물리 갈래에 세우고, 주제 1 로 넷뿐이던 가장자리 × 색 1,4 칸을 겨눴어요. 네마틱 액정의 분자는 머리와 꼬리가 같아서, 그 방향은 θ 와 θ+π 가 같은 선장이에요. 그래서 둘레를 한 바퀴 돌 때 방향이 반 바퀴만 도는 점 결함, ±1/2 전위가 설 수 있어요. 찰스 프랭크(Charles Frank)가 「On the theory of liquid crystals」(Discussions of the Faraday Society 25, 1958)의 탄성 이론에서 이 결함을 다뤘어요. 교차한 편광판 사이에서는 방향이 편광판과 나란하거나 직각인 곳이 어두워서, ±1/2 전위마다 검은 붓자국이 둘, ±1 전위마다 넷이 나가요. 패널에서는 ψ = S·e^{2iθ} 를 64×128 격자 전체에서 긴즈부르크-란다우 이완(1950)으로 풀어서, ψ 가 한 번 감기는 자리가 ±1/2 전위예요. 반대 부호 전위가 서로 끌려 만나 사라지며 결이 굵어지고, 전위가 모자라면 26~34 LED 떨어진 쌍을 0.5초에 녹여 넣어요. 다섯 쌍에 하나는 ±1 쌍이라 붓자국 넷으로 났다가 둘씩 갈라져요. 여는 화면에는 전위가 열 개 있고, 따로 놓은 한 쌍은 20 LED 떨어져 있으면 3초쯤, 40 LED 면 23초쯤에 만나요. 색은 셋업에서 지은 미셸레비 표예요. 지연 Γ 마다 sin²(πΓ/λ) 를 400~700 nm 에 걸쳐 CIE 1931 등색 함수(Wyman, Sloan, Shirley 의 2013 맞춤)로 더해 sRGB 로 바꿨어요. 오귀스트 미셸레비(Auguste Michel-Lévy)가 1888년에 낸 간섭색 표와 같은 셈이에요. Γ = Γ0·d·S 이고 두께 d 에 쐐기를 두어, 위 620 nm 에서 아래 1080 nm 까지 둘째 차수의 파랑, 청록, 초록, 노랑, 주황, 빨강, 자홍이 띠를 이뤄요. 핵에서는 S 가 0 으로 내려가 Γ 도 줄어서, 어두운 점 둘레에 낮은 차수의 색 고리가 서요. S 가 0.5 아래인 자리는 핵을 가로질러 4 LED 예요. 편광판은 1초에 0.03·Speed 라디안 돌아서 여는 값에서 26초에 4분의 1 바퀴를 돌고, 돌면 +1/2 와 −1/2 의 붓자국이 서로 반대로 돌아요. 미셸레비 색을 그대로 둔 첫 판은 색이 0.37 이었어요. 색상과 가장 밝은 채널은 두고 채도를 세 배로 올린 뒤, 한 채널이 넘치면 세 채널을 같이 낮추는 1.4배 노출을 걸자 0.63 언저리가 됐어요. 빛을 선형 공간에서 섞고 sRGB 로 바꾸자 붓자국이 가늘고 검어졌어요. 전위 여섯으로 열 때는 씨앗마다 색이 0.55~0.72 로 갈렸어요. 방향이 고른 넓은 영역이 편광판과 나란해지면 통째로 어두워져서였고, 열 개로 열자 0.56~0.64 로 좁혀졌어요. + +### Conway-Coxeter Frieze + +주제 5 「수학 · 추상」이고 주제 4 「세로 64×128 에서 잘 읽히는 것」도 맡았어요. 콕서터(H. S. M. Coxeter)의 「Frieze patterns」(Acta Arithmetica, 1971)는 위아래를 1 의 줄로 두른 양의 정수 띠에서 모든 마름모 b / a d / c 가 ad − bc = 1 을 지키는 것이에요. 존 콘웨이(John Conway)와 콕서터는 「Triangulated polygons and frieze patterns」(Mathematical Gazette, 1973)에서 차수 n 의 정수 띠가 n각형의 삼각 분할과 꼭 하나씩 맞는다는 것을 보였어요. 첫 줄은 꼭짓점마다 닿은 삼각형의 수이고, 띠는 주기 n 의 미끄럼 대칭을 가져요. 패널에서는 띠를 세워서 n−1 줄이 열이 되고, 양 끝 열이 1 이에요. 한 열에서 수는 12 LED 간격이고 이웃 열은 6 LED 어긋나 마름모 격자가 돼요. 수는 이 패턴에서 그린 3×5 숫자이고, 저마다 제 색의 마름모 타일 위에 앉아요. 색은 수의 log₂ 로 매겨서 2 는 초록, 3 은 청록, 5 는 하늘, 8 은 파랑, 13 은 자홍, 21 은 빨강, 34 는 금빛이에요. 띠는 1초에 1.5·Speed LED 씩 내려가고, 칠각형의 띠는 84 LED 마다 되풀이되고 42 LED 마다 좌우가 뒤집혀요. 여는 값에서 6초마다 띠가 어두워지고, 삼각 분할된 n각형이 키 큰 타원 위에 꼭짓점마다 닿은 삼각형 수를 달고 켜져요. 대각선 하나가 호박색으로 깜박이다 사라지고 새 대각선이 건너가면, 바뀐 꼭짓점 넷의 수가 호박색이 되고 다시 계산한 띠가 바뀐 수를 하얗게 번쩍이며 돌아와요. 패턴은 다시 계산할 때마다 마름모 n(n−2) 개를 다 맞춰 보고, 하나라도 어긋나면 뒤집기를 되돌려요. n = 7~11 에서 뒤집기를 375번씩 해 마름모 13,125~37,125개가 하나도 어긋나지 않았고, 칠각형은 삼각 분할 42가지를 다 지나갔어요. 따로 짠 셈으로 n = 5~12 의 삼각 분할을 모두 돌려 보니 수가 1 인 자리는 꼭 삼각 분할의 변과 대각선이었고, 가장 큰 수는 n = 7~12 에서 8, 13, 21, 34, 55, 89 로 피보나치 수였어요. Sides 는 처음에 5..10 으로 하려 했어요. 오각형의 띠는 다 한 띠를 민 것이라 뒤집기가 밀기일 뿐이고 육각형은 열이 가늘어서 7..11 로 했고, 열둘이면 두 자리 수가 64 LED 에 들어가지 않아요. 첫 판은 검은 바탕에 수만 두어 켜진 몫이 0.123 으로 축 끝 0.1168 에 붙었고, Hue 와 Chords 반응이 0.03 이었어요. 마름모 타일을 깔자 켜진 몫이 0.488, 두 노브가 0.08 과 0.07 이 됐어요. Chords 1 에서는 색을 대각선 방향 (i+j) mod n 으로 칠하는데, 같은 높이의 수가 나란한 대각선 한 무리라서 띠에 가로 줄이 서요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -3940,7 +4019,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 25일 `dev`(`f72355d`)의 328개에 43판부터 70판까지의 이백여든 개를 더한 608개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 25일 `dev`(`f72355d`)의 328개에 43판부터 70판까지와 72판의 이백아흔 개를 더한 618개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -4038,6 +4117,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 70판은 68판까지 든 588개에 같이 돈 69판(#332)의 열 개를 더해 세어도 빈 칸도 둘뿐인 칸도 없었어요. 69판이 고른 가장자리 × 복잡도 4,4 와 가장자리 × 촘촘함 2,4 는 그 열 개로 넷이 되어, 셋뿐인 칸은 가장자리 × 촘촘함 1,3 과 1,4, 촘촘함 × 복잡도 4,4 셋이었고, 그 가운데 가장자리 × 촘촘함 1,3 과 촘촘함 × 복잡도 4,4 를 겨눴어요. 가장자리 × 촘촘함 1,3 은 가장자리가 0.210 아래이면서 촘촘함이 0.189~0.297 인 칸이에요. 이웃 쌍의 80% 쯤이 35 언저리 계단이고 17% 쯤이 140 을 넘는 계단이어야 해요. Quousque Tandem 은 1 LED 아래 크기의 글자를 덮인 넓이로 그린 회색 결로 그 칸을 노렸는데, 넓이대로 그린 회색은 계단 10~25 의 거의 평평한 면이라 첫 판이 촘촘함 0.059, 가장자리 0.254 였어요. 종이에 ±19.5 의 한 LED 바둑판 결을 깔고, 크기를 등비로 줄여 대문자 1.5 LED 를 넘는 줄이 패널의 94% 를 덮게 하자 촘촘함 0.215, 가장자리 0.173 이 됐어요. 칸을 세운 것은 작은 글자가 아니라 종이 결과 드문 큰 가장자리였어요. 촘촘함 × 복잡도 4,4 는 촘촘함이 0.297, 복잡도가 2.172 를 넘는 칸이에요. 이웃 LED 차이가 평균 76 을 넘으면서 deflate 가 줄이지 못해야 해요. Multiplicative Chaos 의 첫 판은 로그 상관장을 LED 하나에 밝기 하나로 칠해 촘촘함 0.237, 복잡도 2.692 였어요. 이웃 LED 끼리 상관이 0.685 라, 문턱 하나로 둘로 가르는 가장 거친 칠도 촘촘함이 0.26 을 넘지 못해요. LED 아래 두 옥타브를 LED 마다 따로 넣자 0.346 과 2.359 가 됐어요. 복잡도는 축 끝 2.735 바로 아래에 있다가 칸 안으로 내려왔어요. 열 개 다 일곱 축의 끝 안에 있어서 칸 경계는 60판 그대로예요. 69판까지 든 598개에 70판 열 개를 더한 608개에서 지도 21장은 다 `none` 이고, 겨눈 두 칸은 둘 다 넷이 됐어요. 셋뿐인 칸은 가장자리 × 촘촘함 1,4 하나가 남았어요. +72판은 70판까지 든 608개에 같이 돈 71판(#334)의 열 개를 더해 세어도 빈 칸도, 둘이나 셋뿐인 칸도 없었어요. 70판 뒤에 셋뿐이던 가장자리 × 촘촘함 1,4 는 71판의 Twindragon 이 넷째가 됐어요. 넷뿐인 칸 스물여섯 가운데 71판이 고른 가장자리 × 촘촘함 1,4 와 움직임 × 촘촘함 1,3 을 빼고 움직임 × 색 1,4 와 가장자리 × 색 1,4 를 겨눴어요. 움직임 × 색 1,4 는 1초에 바뀌는 밝기가 0.061 아래이면서 색이 0.5445 를 넘는 칸이에요. 움직임은 가장 밝은 채널로 재서, 밝기를 두고 색상만 바꾸면 값이 들지 않아요. Ruppert Refinement 는 삼각형을 가장 작은 각에 따라 자홍에서 파랑까지 칠하되 모두 V 0.84, 채도 0.88 로 두고, 새 삼각형도 가장 밝은 채널은 그대로인 채 엷게 났다 짙어지게 해서 움직임 0.030, 색 0.582 예요. 가장자리 × 색 1,4 는 40 을 넘는 계단의 몫이 0.210 아래이면서 색이 0.5445 를 넘는 칸이에요. Schlieren Texture 는 핵을 4 LED 에 걸쳐 녹이고 붓자국을 선형 공간에서 섞어 모든 비탈을 무르게 두고, 미셸레비 색의 채도를 세 배로 올려 가장자리 0.060, 색 0.629 예요. 미셸레비 색을 그대로 둔 첫 판은 색이 0.37 이었어요. Schlieren Texture 는 겨누지 않은 복잡도 × 색 4,4 와 색 × 모양 바뀜 4,3 에도 다섯째로 서서, 71판이 올린 열 개와 72판까지 든 628개에서 넷뿐인 칸은 스물둘이에요. 열 개 다 일곱 축의 끝 안에 있어서 칸 경계는 70판과 같아요. + ### 아직 없는 갈래 `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 가 섰어요. @@ -4189,9 +4270,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 70판에서는 Eight Principles of Yong 과 Granular Leidenfrost 가 128줄을 써요. Eight Principles of Yong 은 永 이 4째 줄에서 116째 줄까지 서고 쪽빛 종이까지 켜져서 thirds 가 99/99/99 예요. 종이를 검게 두고 먹과 이름만 재면 20.8/28.6/22.7 이에요. Granular Leidenfrost 는 지름 2.5 LED 알 300개일 때 뜬 덩어리와 그 아래 기체가 다 바닥 48줄 안에 들어가 위 80줄이 비었어요. 알을 지름 4 LED 로 키워 상자를 32알 높이로 줄이자 같은 상태가 패널 높이를 다 써서 32/84/75 가 됐어요. 나머지 여덟 가운데 Quousque Tandem 은 글줄이 패널을 다 채워 91/92/96 이고, Laguerre Cells 와 Hellschreiber 는 바탕이 다 켜져 100/100/100 이에요. Frenkel-Kontorova 는 위 113줄의 시공간 기록이 49/49/49 이고, Hexachordal Theorem 은 위아래 고리와 가운데 막대가 17/24/18 이에요. +72판에서는 Spray Stencil 과 Conway-Coxeter Frieze 가 128줄을 써요. Spray Stencil 은 「취급주의」 네 음절이 3째 줄부터 123째 줄까지 서요. 골판지 상자까지 켜져서 thirds 가 100/100/100 이고, 오린 자리만 세면 세 판이 위, 가운데, 아래에서 28/35/22, 26/22/30, 24/34/22 % 예요. Conway-Coxeter Frieze 는 수의 띠가 패널을 위아래로 다 채우고 마름모 타일이 깔려 48/50/47 이에요. 첫 판은 검은 바탕에 수만 두어 켜진 몫이 12.3% 였어요. 나머지 여덟 가운데 Lattice Reduction 은 타일이 판을 다 덮어 100/99/99, Schlieren Texture 는 96/97/97 이고, Clothesline 은 줄 넷이 5, 37, 69, 101째 줄에 걸려 43/48/37 이에요. Autogram 은 문장이 15~17줄이라 아래 3분의 1이 9% 예요. + ### 낯선 수학 -주제 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)예요. +주제 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)예요. 72판은 다섯을 더했어요. 루퍼트의 들로네 다듬기(Ruppert Refinement), 라그랑주와 가우스의 격자 기저 줄이기(Lattice Reduction), 커비와 패리스의 히드라(Hydra Game), 프랭크의 액정 전위와 교차 편광 무늬(Schlieren Texture), 콘웨이와 콕서터의 정수 띠(Conway-Coxeter Frieze)예요. 목록 밖에서 다음에 해 볼 것이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -4281,6 +4364,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 70판에서 본 것이에요. 이번 다섯은 다 「다음 판 감」이 앞 판의 파일을 짚어 적어 둔 것이라 앞 판 코드를 이어 쓴 것이 많았는데, 이어 쓴 셈이 그대로 맞지는 않았어요. Frenkel-Kontorova 는 64판 Kibble-Zurek Ions 의 감쇠 사슬을 썼어요. 미는 힘을 바닥 깊이 K 에 비례하게 주면 Substrate 노브는 꼬임의 폭만 바꾸고, 힘을 K 와 떼어 놓아야 깊은 바닥에서 꼬임이 갇히는 것이 보였어요. 표시하지 않은 세계선은 꼬임이 지나가도 곧은 줄로 남아서, 공 둘을 크림색으로 칠하고서야 한 우물씩 비켜 가는 계단이 보였어요. Laguerre Cells 는 질량을 맞추고 나면 점 24 개 가운데 11 개가 남의 칸에 앉아요. 점을 흰 255 로 찍으면 눈이 점을 칸의 중심으로 읽어서 150 으로 낮췄어요. Hexachordal Theorem 은 12박에서 여섯을 고르는 924 가지 가운데 552 가지가 나머지 여섯과 돌리거나 뒤집어도 겹치지 않는데 막대가 같았고, 16박에서 여덟을 고르면 12,870 가지 가운데 10,768 가지였어요. Granular Leidenfrost 는 뚜껑 있는 상자에서 아무리 세게 흔들어도 기체가 되지 않고 결정이 뚜껑에 눌려서, 기체 쪽 끝을 반발 계수 노브로 옮겼어요. Multiplicative Chaos 는 8,192 LED 위에서 질량이 γ = 2 보다 한참 앞인 1.3 언저리에서 장의 가장 높은 LED 로 얼어붙었어요. 격자가 커지면 장의 최댓값은 2 E[X²] 로 가지만, 이 패널에서는 1.47 E[X²] 가 끝이에요. +72판에서 본 것이에요. 이번에도 셈이 정확한 대상은 패턴 안에서 먼저 맞춰 봤어요. Conway-Coxeter Frieze 는 다시 계산할 때마다 마름모를 다 맞춰 보고, Ruppert Refinement 는 여섯 영역과 세 기준에서 끝난 메시의 가장 작은 각과 넓이 합을 맞췄어요. 끝난다는 것이 정리인 대상은 패널 안에서 끝나는 판을 고르는 것이 일이었어요. Hydra Game 은 네 순서 다 마디 8,000개 안에서 죽는 히드라가 목 둘 높이까지뿐이라 히드라 다섯을 그 안에서 골랐어요. Ruppert Refinement 는 기준이 20.7° 를 넘으면 증명이 없는데, 여섯 영역은 33° 까지 다 끝났어요. 증명이 서는 걸음만으로는 그림이 모자라기도 했어요. 루퍼트의 걸음만으로는 메시가 4초에 거칠게 끝나서 크기 상한을 더했고, Lattice Reduction 의 멈춤 조건 q = 0 은 육각 격자에서 끝나지 않았어요. 셈보다 색이 칸을 정한 것도 둘이에요. 가장 밝은 채널로 재는 지도에서는 같은 밝기로 색상만 바꾸면 움직임이 들지 않아 Ruppert Refinement 가 고요한 칸에 섰고, Schlieren Texture 는 셈한 간섭색이 엷어 채도를 올려야 했어요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. @@ -4351,12 +4436,14 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 목록 밖에서 67판 뒤에 해 볼 것이에요. 스테판 카치마르츠(Stefan Kaczmarz)의 반복법(1937)은 사이노그램을 한 줄씩 풀어 가는 재구성이라, Radon Transform 의 걸러 되비추기 옆에 세우면 투영이 적을 때 별 자국 대신 어떤 얼룩이 남는지 견줄 수 있어요. 같은 브레이드를 다르게 적은 두 낱말은 F. A. 가사이드(F. A. Garside, 1969)의 정규형에서 한 줄로 모여서, Artin Braids 의 시공간 그림 둘을 나란히 두고 모이는 것을 볼 만해요. 알프레드 하르(Alfréd Haar, 1910)의 함수계는 Walsh 함수와 같은 ±1 값이지만 받침이 좁아서, 같은 그림을 되살리면 계수가 가장자리 둘레에 모여요. Blinking Vortex 가 LED 마다 끌고 온 야코비안의 큰 특이값은 유한 시간 랴푸노프 지수라, 조지 할러(George Haller)와 위안궈청(Guocheng Yuan)의 라그랑주 응집 구조(2000)를 그 위에 바로 그릴 수 있어요. 원환면의 비스듬한 빌라르소 원(이봉 빌라르소, 1848)도 반전 뒤에 원으로 남으니, Dupin Cyclide 의 한 점을 지나는 원 넷을 같이 그려 볼 만해요. -목록 밖에서 68판 뒤에 해 볼 것이에요. 짐 루퍼트의 들로네 다듬기(Jim Ruppert, 1995)는 가장 작은 각이 기준보다 작은 삼각형의 외접원 중심에 점을 더하고 로슨 뒤집기로 다시 들로네를 맞춰요. 모든 각이 20.7° 를 넘을 때까지 자홍 바늘이 하나씩 사라지는 그림이라, Delaunay Flips 의 뒤집기와 외접원, 각 색칠을 그대로 써요. 스투름 단어(Marston Morse, Gustav Hedlund, 1940)를 기울기 α 노브로 바꿔 가며 같은 홀짝 규칙으로 걸어 볼 만해요. α = 1/φ² 에서 Fibonacci Word 의 곡선이 되고, 그 파일의 걷기와 닮음 맞춤, 확대를 그대로 써요. 캠벨-롭슨 대비 감도 도표(Fergus Campbell, John Robson, 1968)는 Rose Criterion 의 광자 셈과 k 등고선 코드를 그대로 써요. 원판 대신 오른쪽으로 갈수록 촘촘해지고 위로 갈수록 대비가 낮아지는 사인 줄무늬를 두면, 낟알 속에서 보이는 경계가 선량에 따라 움직여요. 럭키 넘버 체(Verna Gardiner, Roger Lazarus, Nicholas Metropolis, Stanislaw Ulam, 1956)는 남은 수가 다음에 지울 간격을 정하는 요세푸스식 제거라서, Josephus Problem 의 표 코드로 체를 한 번 거를 때마다 한 줄씩 쌓을 수 있어요. 광섬유를 나선으로 감으면 접선이 방향의 구 위에 원을 그리고, 편광면이 그 원이 두른 입체각만큼 돌아요. 아키라 도미타(Akira Tomita)와 레이먼드 치아오(Raymond Chiao)가 1986년에 잰 마이클 베리(Michael Berry, 1984)의 기하 위상이에요. 이 파일의 구와 carry 함수, 넓이 다이얼을 그대로 쓰고 아래 절반에 감긴 섬유를 세우면 돼요. +목록 밖에서 68판 뒤에 해 볼 것이에요. 스투름 단어(Marston Morse, Gustav Hedlund, 1940)를 기울기 α 노브로 바꿔 가며 같은 홀짝 규칙으로 걸어 볼 만해요. α = 1/φ² 에서 Fibonacci Word 의 곡선이 되고, 그 파일의 걷기와 닮음 맞춤, 확대를 그대로 써요. 캠벨-롭슨 대비 감도 도표(Fergus Campbell, John Robson, 1968)는 Rose Criterion 의 광자 셈과 k 등고선 코드를 그대로 써요. 원판 대신 오른쪽으로 갈수록 촘촘해지고 위로 갈수록 대비가 낮아지는 사인 줄무늬를 두면, 낟알 속에서 보이는 경계가 선량에 따라 움직여요. 럭키 넘버 체(Verna Gardiner, Roger Lazarus, Nicholas Metropolis, Stanislaw Ulam, 1956)는 남은 수가 다음에 지울 간격을 정하는 요세푸스식 제거라서, Josephus Problem 의 표 코드로 체를 한 번 거를 때마다 한 줄씩 쌓을 수 있어요. 광섬유를 나선으로 감으면 접선이 방향의 구 위에 원을 그리고, 편광면이 그 원이 두른 입체각만큼 돌아요. 아키라 도미타(Akira Tomita)와 레이먼드 치아오(Raymond Chiao)가 1986년에 잰 마이클 베리(Michael Berry, 1984)의 기하 위상이에요. 이 파일의 구와 carry 함수, 넓이 다이얼을 그대로 쓰고 아래 절반에 감긴 섬유를 세우면 돼요. 목록 밖에서 69판 뒤에 해 볼 것이에요. 토노무라 아키라(Akira Tonomura) 등이 1986년에 초전도체로 감싼 고리 자석으로 찍은 전자 홀로그램처럼, 고리 안과 밖을 지난 간섭 줄무늬가 반 칸 어긋나는 그림을 Aharonov-Bohm 의 급수로 그려 볼 만해요. 자속관 둘을 반대 자속으로 나란히 두면 두 그림자 선 사이의 띠만 밀린 채 내려와요. 페니의 놀이를 네 번 줄로 넓히면 (x2 의 반대) x1 x2 x3 규칙이 열여섯 가운데 여섯에서 최선이 아니에요. HHTH 에 규칙이 고른 THHT 는 7:5 로 이기지만 HHHT 는 2:1 로 이겨서, 그 어긋남을 노브 하나로 넘겨 보는 판이 돼요. 휘트니가 같은 논문에 적은 공식, 곧 교차점마다 매긴 부호의 합에 ±1 을 더하면 회전수가 된다는 것을 Whitney-Graustein 의 곡선 위에 교차점 표시로 보여 줄 만하고, 블라디미르 아르놀트(Vladimir Arnold)가 1994년에 낸 평면 곡선의 불변량 J+, J−, St 는 곡선이 제 몸에 접하거나 삼중점을 지날 때만 바뀌어서 그 순간을 셀 수 있어요. 한 직선에 대해 거울쌍 두 점 가운데 한쪽만 옮기는 극 대칭화(polarization)는 Steiner Symmetrization 의 빗금 틀에서 현이 미끄러지는 대신 조각이 축 건너편으로 뒤집혀 건너가요. 같은 색 블록을 셋씩 쌓아 이웃한 둘 사이마다 k 블록이 끼게 하는 랭퍼드 문제의 확장은 Langford Pairs 의 탑에 그대로 서고, 도널드 크누스(Donald Knuth)의 춤추는 연결(2000)로 선택지가 가장 적은 칸부터 고르는 탐색을 옆에 두면 두 탐색 나무의 크기를 나란히 잴 수 있어요. 목록 밖에서 70판 뒤에 해 볼 것이에요. 질량을 맞춘 뒤 점을 칸의 질량 중심으로 옮기기를 되풀이하면 용량 제약 점 분포(Michael Balzer, Thomas Schlömer, Oliver Deussen, 2009)가 되고, 이것을 파워 다이어그램 위의 최적 수송으로 다시 쓴 것이 페르난두 지 고이스(Fernando de Goes)와 동료들의 블루 노이즈(2012)예요. Laguerre Cells 의 질량 배분과 뉴턴 걸음에 점을 옮기는 걸음 하나만 더하면 돼요. 포트의 4-Z15 {0,1,4,6} 와 4-Z29 {0,1,3,7} 은 12박에 온셋 넷으로 거리 1~6 이 한 번씩 나오는데, 서로 돌리거나 뒤집은 것이 아니에요. 패터슨(A. L. Patterson)이 1944년 결정학에서 본 호모메트릭 집합을 12박 고리에서 보는 셈이라, Hexachordal Theorem 의 아래 고리에 나머지 대신 Z 짝을 두면 돼요. 세르주 오브리(Serge Aubry)의 해석성 깨짐 전이(1983)는 평균 간격이 황금비인 사슬이 K = 0.9716 을 넘으면 바닥에 갇히는 일이에요. Frenkel-Kontorova 의 고리를 공 34개와 우물 21개처럼 피보나치 수 둘로 짜면, Drive 를 0 가까이 둔 채 K 를 넘길 때 기록이 기울다가 서요. 에스하위스와 동료들은 라이덴프로스트 상태를 더 세게 흔들면 알갱이 대류가 생긴다고 2010년에 보였어요(Physical Review Letters). Granular Leidenfrost 의 뚜껑을 패널 밖으로 올리면 결정이 대류 고리 둘로 갈라지는지 볼 수 있어요. 베르트랑 뒤플랑티에(Bertrand Duplantier)와 스콧 셰필드(Scott Sheffield)는 KPZ 관계(2011)를 세려고 측도가 문턱 아래가 될 때까지 판을 이분 정사각형으로 쪼갰어요. Multiplicative Chaos 의 질량 몫으로 그렇게 쪼개면 두꺼운 점 둘레만 잘게 갈라진 격자가 나와요. +목록 밖에서 72판 뒤에 해 볼 것이에요. 알퍼 윙외르(Alper Üngör, 2004)의 오프센터는 바늘 삼각형의 외심 대신 짧은 변 쪽으로 당긴 점을 꽂아 같은 기준에 점을 덜 써서, Ruppert Refinement 의 여섯 영역에 두 규칙을 나란히 돌려 점 수를 견줄 수 있어요. 기저의 비 τ = b2/b1 을 위 반평면의 점으로 보면 가우스의 걸음은 τ → τ − q 와 τ → −1/τ 라서, Lattice Reduction 의 한 판을 J-Invariant 의 반평면 위에서 SL(2,Z) 기본 영역으로 들어가는 길로 그릴 수 있어요. 커비와 패리스의 같은 논문은 구드스타인 수열(Reuben Goodstein, 1944)도 페아노 산술로 증명할 수 없음을 보였어요. 수를 유전적 b 진법으로 적고 밑을 하나 올린 뒤 1 을 빼기를 되풀이하는 수열이라, Hydra Game 의 순서수 글자로 수가 크게 부풀다 0 에 닿는 것을 적을 수 있어요. 미세소관으로 짠 능동 네마틱(Tim Sanchez 등, Nature, 2012)에서는 +1/2 전위가 혜성처럼 제 머리 쪽으로 헤엄쳐서, Schlieren Texture 의 이완에 능동 응력 한 항을 더하면 전위가 끝없이 나고 사라져요. 콕서터의 정수 띠는 A형 클러스터 대수의 교환 관계라서(Philippe Caldero, Frédéric Chapoton, 2006), 수 대신 대각선 이름을 두면 Conway-Coxeter Frieze 의 뒤집기마다 바뀌는 분수식을 적을 수 있어요. + ### 타이포 주제 7 로 해 본 글자 실험과 다음에 해 볼 것이에요. 63판과 64판이 같이 돌며 다섯씩 처음 만들었어요. 63판의 다섯은 한글 자모가 날아와 모아쓰기로 잠기는 조립(Jamo Assembly), 놀턴과 하먼의 기호 모자이크(Type Mosaic), 호프스태터의 트립렛 그림자(Trip-let), 카프리카의 계산 종이(Kaprekar's Routine), 캐럴의 낱말 사다리(Word Ladder)예요. @@ -4369,7 +4456,7 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 주제 7 「타이포」로 해 볼 것이에요. 64판이 처음 다섯을 만들었고, 다섯 다 글꼴을 쓸 수 없어서 글자를 코드에 그렸어요. 다섯이 「기능 감」에 적은 것도 같은 길이었어요. 패턴 코드가 Text 레이어의 글이나 그 글자 비트맵, 글자마다 폭을 담은 글꼴 표를 받는 길이에요. 그 길이 있으면 Broad Nib 의 펜이 손으로 친 낱말을 쓰고, Wongoji 가 「진달래꽃」이든 아무 한글이든 원고지에 써요. 한글 픽셀 글꼴은 Wongoji 가 9×11 로 지은 조합 글꼴(자모 그림 391개)이 출발점이 될 수 있어요. -같은 펜 모델로 한글을 써 볼 만해요. Broad Nib 의 펜이 쓸고 지나가는 셈에 자모 획을 궁체 붓 각도로 적으면, 초성, 중성, 종성 차례로 펜이 한 칸에 모여요. 카운터 다음은 글자 사이의 흰 자리예요. Counterforms 의 flood fill 에 글자마다 볼록 껍질을 더하면 이웃한 두 글자 사이를 따로 떼어 빛나게 할 수 있고, 노브로 자간을 좁히면 빛이 줄어요. 한글 양쪽 맞춤도 남았어요. 한글 조판은 낱말 사이보다 글자 사이를 고르게 늘리는 일이 많아서, Rivers of White 의 문단을 한글로 짜면 강 대신 세로로 곧게 선 글자 열이 생기는지 볼 수 있어요. +같은 펜 모델로 한글을 써 볼 만해요. Broad Nib 의 펜이 쓸고 지나가는 셈에 자모 획을 궁체 붓 각도로 적으면, 초성, 중성, 종성 차례로 펜이 한 칸에 모여요. 한글 양쪽 맞춤도 남았어요. 한글 조판은 낱말 사이보다 글자 사이를 고르게 늘리는 일이 많아서, Rivers of White 의 문단을 한글로 짜면 강 대신 세로로 곧게 선 글자 열이 생기는지 볼 수 있어요. 65판이 다섯을 더 썼어요. 1 을 소수로 나누는 세로셈(Long Division), 스콧 킴의 반 바퀴 앰비그램을 한글로 옮긴 것(Inversions), 한 시점에서만 맞춰지는 착시 원근 낱말(Anamorphosis), 해례본 제자해의 가획 계보(Hunminjeongeum), 캐럴의 꼬리 시(Mouse's Tale)예요. 다섯이 이번에도 다 글꼴을 코드에 그렸어요. 돌려 읽기는 한글이 라틴 글자보다 잘 맞았어요. 가로 모음을 가진 받침 글자는 반 바퀴에 초성과 받침이 자리를 바꿔서 곰국과 논문, 공공과 운운이 짝인데, 받침 상자를 초성 상자와 같은 크기로 칸 가운데에 대칭으로 두지 않으면 켜진 LED 2,617개 가운데 612개가 갈렸어요. 착시 원근의 낱말은 읽히는 각도가 좁았어요. 시점에서 6.6도 비끼면 획이 두 겹으로 겨우 읽히고 21도면 무슨 낱말인지 알 수 없어요. 꼬리 시는 글자가 2×3 으로 줄면 읽히지 않는데도 솟는 글자와 처지는 글자가 LED 하나씩 남아 낱말마다 다른 무늬가 됐어요. @@ -4417,6 +4504,14 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 70판 뒤에 해 볼 것이에요. 매튜 카터가 1996년 마이크로소프트에 그린 Verdana 는 종이 대신 화면 픽셀에 맞춘 글자라, Ink Traps 의 볼록 조각 글자와 4×4 표본 래스터를 작은 크기로 돌리면 픽셀 격자에 맞춘 획과 맞추지 않은 획을 위아래 견본으로 견줄 수 있어요. 왕희지의 「난정서(蘭亭序)」(353년)에는 之 자가 스무 번 나오고 하나도 같게 쓰지 않았다고 전해요. Eight Principles of Yong 의 붓과 붓털 문턱, 마디 시간표로 之 스무 벌을 세로로 쌓을 수 있어요. 루돌프 헬의 디지셋(Digiset, 1965)은 원판 대신 음극선관이 글자를 주사선으로 그려 필름에 찍었어요. Photon Disk 의 필름 띠와 섬광 자리에 글자를 한 줄씩 긋는 빔을 두면 돼요. 피에르시몽 푸르니에(Pierre-Simon Fournier)의 『Manuel typographique』(1764)는 활자 크기를 포인트로 맞춘 표를 실었어요. Quousque Tandem 의 넓이 합 표로 등비 대신 그 표의 크기들을 짜면 크기 사이가 고르지 않은 견본이 서요. 새뮤얼 모스(Samuel Morse)의 1844년 워싱턴–볼티모어 선은 기록기가 점과 선을 종이 테이프에 눌러 찍었어요. Hellschreiber 의 띠와 양식지, 잡음을 그대로 쓰면 같은 메시지가 점과 선으로 찍힌 띠가 헬의 글자 띠와 나란히 붙어요. 이번에 골랐다가 짜지 않은 것도 있어요. 어도비의 멀티플 마스터(Adobe Multiple Master, 1991)는 굵기와 너비 같은 축의 끝에 마스터 글꼴을 두고 그 사이를 보간해서, 노브 둘을 두 축에 걸면 한 글자가 설계 공간을 미끄러지는 「노브와 글」이 돼요. +72판의 다섯은 베이컨이 두 벌 활자로 짠 겉글에서 숨은 글자를 읽는 두 글자 암호(Biliteral Cipher), 글자 사이 흰 자리가 같은 넓이가 될 때까지 글자를 미끄러뜨리는 간격 맞추기(Optical Kerning), 다리를 남긴 「취급주의」 판에 물감을 뿌려 찍는 스텐실(Spray Stencil), 줄에 꽂은 「바람이 분다」 카드가 돌풍에 흔들리는 빨랫줄(Clothesline), 제 글자 수를 세는 샐로스의 오토그램(Autogram)이에요. + +72판에서 본 것이에요. 글자를 무엇으로 적느냐가 할 수 있는 움직임을 정했어요. Clothesline 은 획을 거리장으로 구워서 돌고 비틀린 카드의 글자가 매끈했고, Biliteral Cipher 는 같은 비트맵을 4×4 표본으로 기울이고 굵혀서 둘째 활자를 노브 하나로 이었어요. 스텐실은 글자 꼴에 판이 한 조각이어야 한다는 조건을 더했어요. 한글에서 닫힌 자리는 ㅇ, ㅁ, ㅂ, ㅍ 와 ㅎ 의 고리라서 Spray Stencil 의 열 음절에 둔 다리는 거의 다 이 다섯에 놓였어요. 넓이로 띄우는 간격은 낱말을 가렸어요. Optical Kerning 에서 비스듬한 획이 나란히 달리는 V Y 와 A X 는 끝이 먼저 닿아 어떤 깊이에서도 목표 넓이에 닿지 못해서, WAVY 와 TAXI 를 뺐어요. 스스로를 세는 문장은 틀에 따라 찾기가 갈렸어요. Autogram 의 탐색은 THIS SENTENCE HAS 로 시작하는 문장은 금방 찾았지만 ONLY A FOOL WOULD CHECK THAT THIS LINE HAS 로 시작하는 문장은 400만 걸음에도 찾지 못했어요. 한번 흔든 오토그램은 다시 세기만으로는 거의 돌아오지 않고 길이 수백의 고리에 갇혔어요. + +기능 감은 이번에도 글자를 코드 밖에서 받는 길과 Lab 의 글 도구로 모였어요. Spray Stencil 과 Autogram 은 패턴끼리 같이 쓰는 글꼴 표를 바랐어요. 하나는 직사각형, 사선, 고리로 적은 큰 한글 획 글꼴이고, 하나는 3×5 와 5×7 픽셀 글꼴이에요. Clothesline 은 Text 레이어가 글자를 거리장이나 획 목록으로 내주기를, Optical Kerning 은 Text 레이어에 노브로 바꾸는 자간 값을 바랐어요. Biliteral Cipher 는 다른 것을 바랐어요. Lab 미리보기를 방 건너편에서 보는 크기로 흐리게 보는 단추가 있으면 「얼핏 봐서는 모른다」를 PNG 를 키우지 않고 가늠할 수 있어요. + +72판 뒤에 해 볼 것이에요. 한글 기본 자모는 자음 열넷과 모음 열로 스물넷이라, 베이컨의 24자 표에 그대로 맞아요. Biliteral Cipher 의 겉글을 명조와 고딕 두 벌의 한글로 짜면 다섯 음절마다 자모 하나를 숨길 수 있어요. 한글 오토그램도 해 볼 만해요. 「이 문장에는 ㄱ 이 다섯 개…」처럼 자모를 세면 수 낱말(하나, 둘, 셋 또는 일, 이, 삼)의 자모가 영어 수 낱말과 전혀 다르게 섞여서, Autogram 의 탐색이 찾는 틀이 달라질 거예요. Optical Kerning 의 넓이 규칙을 한글 음절 사이에 걸면 받침 없는 「가」와 받침 있는 「갈」처럼 아래가 비는 쌍이 생겨서, 줄마다 세는 깊이가 받침 자리에서 어떻게 갈리는지 볼 수 있어요. + ### 화장품 팝업 주제 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 b3df85620c77cd079d6268f4df147ca8e493077f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 22:34:59 +0000 Subject: [PATCH 14/14] =?UTF-8?q?index.html=20=EC=9D=84=20628=EA=B0=9C?= =?UTF-8?q?=EB=A1=9C=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_01Bc4JkrPaZ8GQQiA22PWEvR --- 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 5aee56976..1ba59ae43 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

패턴 연구

- - + +