From 9056e8c81e732d417e8c8581258ff14b28129580 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 23:16:38 +0000 Subject: [PATCH 01/10] =?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=2052=ED=8C=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LXsMxtcgxEaHMjeqmCbsjv From 2411f587349cca9e5c0912720725b7d04a4905b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 00:07:02 +0000 Subject: [PATCH 02/10] =?UTF-8?q?52=ED=8C=90:=20de=20Bruijn=20Sequence=20?= =?UTF-8?q?=EC=99=80=20Lost=20Head?= 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_01LXsMxtcgxEaHMjeqmCbsjv --- .../patterns/de-bruijn-sequence.js | 249 +++++++++ incoming/pattern-study/patterns/lost-head.js | 473 ++++++++++++++++++ 2 files changed, 722 insertions(+) create mode 100644 incoming/pattern-study/patterns/de-bruijn-sequence.js create mode 100644 incoming/pattern-study/patterns/lost-head.js diff --git a/incoming/pattern-study/patterns/de-bruijn-sequence.js b/incoming/pattern-study/patterns/de-bruijn-sequence.js new file mode 100644 index 000000000..fd4ba9f74 --- /dev/null +++ b/incoming/pattern-study/patterns/de-bruijn-sequence.js @@ -0,0 +1,249 @@ +// ===== Patternflow pattern ===== +// Title: de Bruijn Sequence +// Family: rhythm +// Idea: A binary de Bruijn sequence plays as a rhythm, a hit for each 1 and a rest for each 0, and each beat its window of the last k bits lights the one tile of a board of all 2^k words that it spells, so the board fills exactly once a cycle while the jumps from tile to tile trace a Hamiltonian cycle and the same words light one by one as the threads of the order k-1 de Bruijn graph below the tape. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: de Bruijn sequence (Nicolaas Govert de Bruijn, 1946) +// Round: 52 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Rule=0..2, Zoom=0..3 +// +// Knob 1: Hue. Each word takes its colour from when it comes in the cycle, a sweep of three tenths of the wheel from the first word to the last: cyan through blue to violet at 0, violet to magenta and red at 0.25, red through yellow to green at 0.5, lime to cyan at 0.75. +// Knob 2: Speed. Three bits a second for each unit: 6 at the opening 2, so the 64 words of order 6 take 10.7 s; 0.6 a second at 0.2 and 9 at 3. +// Knob 3: Rule. Which sequence plays. At 0 a new one every cycle, drawn uniformly from all 67,108,864 of order 6 (2,048 of order 5, 16 of order 4, 2 of order 3): a random spanning tree of the order k-1 graph by Wilson's loop-erased walks, then the Eulerian circuit that leaves each vertex by its tree edge last (the BEST theorem). At 1 Martin's prefer-one rule (1934), the same every cycle, which runs down the right column and along the bottom row early on. At 2 a maximal shift register, x^k + x + 1 (x^5 + x^2 + 1 at order 5), which never makes the all-zero word: its 2^k - 1 beats, 63 at order 6, leave the top left tile dark and the first slot of the tape empty. +// Knob 4: Zoom. The order k, 6 minus the knob rounded: 6 at 0 is 64 words on 8 by 8 tiles 8 LEDs apart, with 1 LED a bit on the tape; 5 at 1 gives 32 tiles 8 by 16; 4 at 2 gives 16 tiles of 16 by 16; 3 at 3 gives 8 tall tiles, and there Martin's rule plays 01110100, the light and heavy syllables of ya mā tā rā ja bhā na sa, the Sanskrit prosody mnemonic yamātārājabhānasalagāḥ. +// Buttons: K1 tap ends the cycle now: the board flashes and fades and a new sequence starts from the all-zero word. + +const W = 64, H = 128, N = W * H; +const { floor, round, min, max, abs, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const hsv = (h, s, v, o) => [5, 3, 1].forEach((n, c) => { + const k = (n + (h - floor(h)) * 6) % 6; + o[c] = 255 * v * (1 - s * max(0, min(k, 4 - k, 1))); +}); + +// The board fills the top 64 rows; the tape and the loom share the rest. +const BY = 0, BH = 64, TY = 68, TH = 6, LY0 = 82, LY1 = 125; + +// A uniformly random binary de Bruijn sequence of order k, by the BEST +// theorem: a random spanning arborescence of the order k-1 graph into the +// all-zero vertex (Wilson's loop-erased walks), then the Eulerian circuit +// that leaves every other vertex by its tree edge last. +function randomSeq(k) { + const M = 1 << (k - 1), m = M - 1, inT = new Uint8Array(M), nb = new Uint8Array(M); + inT[0] = 1; + for (let v = 1; v < M; v++) { + let u = v; + while (!inT[u]) (nb[u] = Math.random() < 0.5 ? 0 : 1), (u = ((u << 1) | nb[u]) & m); + for (u = v; !inT[u]; u = ((u << 1) | nb[u]) & m) inT[u] = 1; + } + const used = new Uint8Array(M), s = new Uint8Array(2 * M); + for (let i = 0, v = 0; i < 2 * M; i++) { + const b = v === 0 ? used[v] : used[v] ? nb[v] : 1 - nb[v]; + used[v]++; + s[i] = b; + v = ((v << 1) | b) & m; + } + return s; +} +// Martin's rule (1934): from k zeros, append a 1 whenever the new word is +// new, else a 0. +function martin(k) { + const L = 1 << k, mask = L - 1, seen = new Uint8Array(L), s = new Uint8Array(L); + seen[0] = 1; + for (let i = 1, w = 0; i < L; i++) { + const b = seen[((w << 1) | 1) & mask] ? 0 : 1; + w = ((w << 1) | b) & mask; + seen[w] = 1; + s[i] = b; + } + return s; +} +// A maximal-length shift register, x^k + x^a + 1: every word but the +// all-zero one, so slot 0 of the tape stays empty. +const TAP = [0, 0, 0, 1, 1, 2, 1]; +function lfsr(k) { + const L = 1 << k, s = new Uint8Array(L), r = new Uint8Array(L + k); + r[k - 1] = 1; + for (let i = k; i < L + k; i++) r[i] = r[i - k] ^ r[i - k + TAP[k]]; + // Turn it so the run of k-1 zeros comes last; the missing zero is slot 0. + let z = 0; + for (let i = 0; i < L - 1; i++) { + let ok = r[i] === 1; + for (let j = 1; j < k && ok; j++) ok = r[(i - j + L - 1) % (L - 1)] === 0; + if (ok) z = i; + } + for (let i = 1; i < L; i++) s[i] = r[(z + i - 1) % (L - 1)]; + return s; +} + +export function setup(params) { + params.s = { + pos: 0, k: 0, rule: -1, hue: 0, start: 0, seq: null, word: null, old: null, oldAge: 9, + buf: new Float32Array(N * 3), c: [0, 0, 0], + }; +} + +function newCycle(S) { + const k = S.k; + S.seq = S.rule === 0 ? randomSeq(k) : S.rule === 1 ? martin(k) : lfsr(k); + const L = 1 << k, w = new Int16Array(L); + for (let i = 0; i < L; i++) { + let v = 0; + for (let j = k - 1; j >= 0; j--) v = (v << 1) | S.seq[(i - j + L) % L]; + w[i] = v; + } + S.word = w; + S.start = S.rule === 2 ? 1 : 0; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kn = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = cl(kn[0], 0, 1); + const rule = round(cl(kn[2], 0, 2)), k = 6 - round(cl(kn[3], 0, 3)); + if (k !== S.k || rule !== S.rule) { + S.k = k; + S.rule = rule; + newCycle(S); + S.pos = S.start; + S.old = null; + } + // Bits a second: three for each unit of Speed. + const L = 1 << S.k, rate = 3 * cl(kn[1], 0.2, 3); + S.pos += d * rate; + if (input.btnPressed && input.btnPressed[0]) S.pos = L; + if (S.pos >= L) { + // The cycle is complete: keep the full board to fade, draw the next. + S.old = { word: S.word, start: S.start }; + S.oldAge = 0; + newCycle(S); + S.pos = S.start + (S.pos - floor(S.pos)); + } + if (S.old) S.oldAge += d * rate; +} + +// Cell of word w on the board: columns and rows of a 64x64 square. +function cell(k, w) { + const cols = 1 << ((k + 1) >> 1), rows = 1 << (k >> 1), cw = W / cols, ch = BH / rows; + return [(w % cols) * cw, BY + floor(w / cols) * ch, cw, ch]; +} +function add(buf, x, y, r, g, b) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + buf[o] += r; + buf[o + 1] += g; + buf[o + 2] += b; +} +function line(buf, x0, y0, x1, y1, c, a) { + const n = max(abs(x1 - x0), abs(y1 - y0), 1); + for (let i = 0; i <= n; i++) add(buf, round(x0 + ((x1 - x0) * i) / n), round(y0 + ((y1 - y0) * i) / n), c[0] * a, c[1] * a, c[2] * a); +} +function rect(buf, x0, y0, w, h, r, g, b) { + for (let y = y0; y < y0 + h; y++) for (let x = x0; x < x0 + w; x++) add(buf, x, y, r, g, b); +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.seq) return; + const buf = S.buf, k = S.k, L = 1 << k, M = L >> 1, i = floor(S.pos), f = S.pos - i, c = S.c; + buf.fill(0); + // Colour of the n-th word of the cycle: a sweep along the wheel. + const col = (n, out) => hsv(S.hue + 0.5 + (0.3 * n) / L, 0.7, 1, out); + const hit = S.seq[i], beat = exp(-f * 5); + const ctr = (w) => { + const [x, y, cw, ch] = cell(k, w); + return [round(x + cw / 2 - 0.5), round(y + ch / 2 - 0.5)]; + }; + // The path so far, under the tiles: it shows only in the gaps. + for (let j = S.start + 1; j <= i - 6; j++) { + col(j, c); + line(buf, ...ctr(S.word[j - 1]), ...ctr(S.word[j]), c, 0.22); + } + // The board: every word a tile, dim until its turn. + const step = new Int16Array(L).fill(-1), ostep = new Int16Array(L).fill(-1), O = S.old, fade = O ? max(0, 1 - S.oldAge / 4) : 0; + for (let j = S.start; j <= i; j++) step[S.word[j]] = j; + if (fade > 0) for (let j = O.start; j < L; j++) ostep[O.word[j]] = j; + for (let w = 0; w < L; w++) { + const [x, y, cw, ch] = cell(k, w), j = step[w]; + if (j < 0) { + // Not yet this cycle: the last cycle's full board fading out, after a + // flash on the downbeat that completed it. + let r = 14, g = 16, b = 22; + if (fade > 0 && ostep[w] >= 0) { + col(ostep[w], c); + const a = 0.5 * fade, e = 160 * max(0, 1 - S.oldAge * 1.5); + (r = max(r, c[0] * a + e)), (g = max(g, c[1] * a + e)), (b = max(b, c[2] * a + e)); + } + fill(buf, x + 1, y + 1, cw - 2, ch - 2, r, g, b); + continue; + } + col(j, c); + const now = j === i, a = now ? (hit ? 1 : 0.75) : 0.5, g = now ? (hit ? 220 : 70) * beat : 0; + fill(buf, x + 1, y + 1, cw - 2, ch - 2, c[0] * a + g, c[1] * a + g, c[2] * a + g); + // A hit rings the tile's edge. + if (now && hit) { + const r = (c[0] * 0.6 + 40) * beat, gg = (c[1] * 0.6 + 40) * beat, bb = (c[2] * 0.6 + 40) * beat; + rect(buf, x + 1, y, cw - 2, 1, r, gg, bb); + rect(buf, x + 1, y + ch - 1, cw - 2, 1, r, gg, bb); + rect(buf, x, y + 1, 1, ch - 2, r, gg, bb); + rect(buf, x + cw - 1, y + 1, 1, ch - 2, r, gg, bb); + } + } + // The last jumps on top, fading with age. + for (let j = max(S.start + 1, i - 6); j <= i; j++) { + col(j, c); + line(buf, ...ctr(S.word[j - 1]), ...ctr(S.word[j]), c, 0.22 + 0.7 * exp(-(i - j + f) / 2)); + } + // The tape: the whole cycle, one slot per bit, the window bracketed. + const sw = W / L, bw = max(1, floor(sw)); + for (let j = S.start; j < L; j++) { + const played = j <= i, x = floor(j * sw); + col(j, c); + const a = S.seq[j] ? (played ? 0.95 : 0.3) : played ? 0.1 : 0.04; + rect(buf, x, TY, bw, TH, c[0] * a, c[1] * a, c[2] * a); + } + for (let q = 0; q < k; q++) { + const j = (i - q + L) % L, x = floor(j * sw), g = 150 + 100 * beat * hit; + rect(buf, x, TY - 2, bw, 1, g, g, g); + rect(buf, x, TY + TH + 1, bw, 1, g, g, g); + if (q === 0 && hit) rect(buf, x, TY, bw, TH, 160 * beat, 160 * beat, 160 * beat); + } + // The loom: each word is a thread from its first k-1 bits, along the top, + // to its last k-1 bits along the bottom: the order k-1 de Bruijn graph. + const nx = (v) => (v + 0.5) * (W / M) - 0.5, th = max(1, round(12 / M)); + if (fade > 0) + for (let j = O.start; j < L; j++) { + col(j, c); + thread(buf, nx(O.word[j] >> 1), nx(O.word[j] & (M - 1)), c, 0.12 * fade, th); + } + for (let j = S.start; j <= i; j++) { + const w = S.word[j], age = i - j + f; + col(j, c); + thread(buf, nx(w >> 1), nx(w & (M - 1)), c, j === i ? (hit ? 1 : 0.6) : 0.12 + 0.5 * exp(-age / 2.5), th); + } + for (let v = 0; v < M; v++) { + rect(buf, round(nx(v)), LY0 - 2, 1, 1, 90, 90, 100); + rect(buf, round(nx(v)), LY1 + 2, 1, 1, 90, 90, 100); + } + for (let p = 0, o = 0; p < N; p++, o += 3) display.setPixel(p % W, (p / W) | 0, min(255, buf[o]) & 252, min(255, buf[o + 1]) & 252, min(255, buf[o + 2]) & 252); +} +function fill(buf, x0, y0, w, h, r, g, b) { + for (let y = y0; y < y0 + h; y++) + for (let x = x0, o = (y * W + x) * 3; x < x0 + w; x++, o += 3) (buf[o] = r), (buf[o + 1] = g), (buf[o + 2] = b); +} +// A thread of the loom: an S from top to bottom, eased at both ends, wider +// when there are fewer of them. +function thread(buf, x0, x1, c, a, th) { + const n = max(LY1 - LY0, abs(x1 - x0) * 2); + let px = -1, py = -1; + for (let s = 0; s <= n; s++) { + const t = s / n, e = t * t * (3 - 2 * t), x = round(x0 + (x1 - x0) * e - (th - 1) / 2), y = round(LY0 + (LY1 - LY0) * t); + if (x === px && y === py) continue; + for (let q = 0; q < th; q++) add(buf, x + q, y, c[0] * a, c[1] * a, c[2] * a); + (px = x), (py = y); + } +} diff --git a/incoming/pattern-study/patterns/lost-head.js b/incoming/pattern-study/patterns/lost-head.js new file mode 100644 index 000000000..ee07224ba --- /dev/null +++ b/incoming/pattern-study/patterns/lost-head.js @@ -0,0 +1,473 @@ +// ===== Patternflow pattern ===== +// Title: Lost Head +// Family: halloween +// Idea: A headless skeleton scuttles along moonlit tombstones trying on the skulls that sit on them and puts each wrong one back with a shrug, until the right one clicks on and its sockets light toxic green; it dances a jig, then the head falls off and rolls away. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 52, built on Skull Swap, pattern round 50 +// Round: 52 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Tries=1..5, Size=0..1 +// +// Knob 1: Hue. The night sky and the shaded side of the bones, round the cold side of the wheel from violet at 0 to teal at 1. The moon and the halo stay. +// Knob 2: Speed. One beat a second for each unit. At the opening 2 the big skull is on the neck at 0.9 s and the right one clicks on at 7 s, after a 2 s walk; it rolls away at 10.8 s. Ten times slower at 0.2. +// Knob 3: Tries. Wrong skulls before the right one, 1 to 5, in turn from too small, witch's hat, biter, pumpkin, too big. The first round ends on the big one, so at 1 it opens on the big one and at 5 on the small one. +// Knob 4: Size. How wrong the wrong sizes are: the big skull is 1.1 times the right one at 0 and twice it at 1, sliding down over the hips; the small one goes from 0.8 to 0.4. + +const W = 64, H = 128, N = W * H; +const { floor, ceil, round, min, max, abs, sqrt, sin, cos, atan2, acos, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const mix = (a, b, t) => a + (b - a) * t; +const ease = (v) => (v <= 0 ? 0 : v >= 1 ? 1 : v * v * (3 - 2 * v)); +// Up from a over ra beats, down from c over rc. +const win = (b, a, ra, c, rc) => ease((b - a) / ra) * (1 - ease((b - c) / rc)); +const blend = (D, o, w) => Object.keys(o).forEach((key) => (D[key] = mix(D[key], o[key], w))); +const hsv = (h, s, o) => [5, 3, 1].forEach((n, c) => { + const k = (n + h * 6) % 6; + o[c] = 255 * (1 - s * max(0, min(k, 4 - k, 1))); +}); + +// --- Skull Swap's bones, as distances in their own frames, y down. +const seg = (x, y, ax, ay, bx, by) => { + const dx = bx - ax, dy = by - ay, t = cl(((x - ax) * dx + (y - ay) * dy) / (dx * dx + dy * dy), 0, 1); + return sqrt((x - ax - dx * t) ** 2 + (y - ay - dy * t) ** 2); +}; +const ell = (x, y, cx, cy, rx, ry) => (sqrt(((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2) - 1) * min(rx, ry); +const box = (x, y, cx, cy, hx, hy, r) => { + const qx = abs(x - cx) - hx + r, qy = abs(y - cy) - hy + r; + return sqrt(max(qx, 0) ** 2 + max(qy, 0) ** 2) + min(max(qx, qy), 0) - r; +}; +function long(x, y, L, w, r) { + const e = L / 2 - r, q = 0.6 * r, ry = r * 0.9; + return min(seg(x, y, 0, -e, 0, e) - w, ell(x, y, -q, -e, r, ry), ell(x, y, q, -e, r, ry), ell(x, y, -q, e, r, ry), ell(x, y, q, e, r, ry)); +} +// 0 long bone, 1 paired bones, 2 cage, 3 lumbar, 4 pelvis, 5 skull, 6 jaw, +// 7 hand, 8 foot, 9 hat. +const RIBS = [[6.4, 2.4], [7.9, 6.6], [8.8, 10.8], [8.8, 15], [7.8, 19]]; +function shape(k, x, y, sg) { + switch (k) { + case 0: + return long(x, y, sg, 0.95 + 0.015 * sg, 1.5); + case 1: { + const e = sg / 2 - 1.1; + return min(seg(x, y, -1, -e, -1, e) - 0.8, seg(x, y, 1.1, -e, 1.1, e) - 0.6, box(x, y, 0, -e - 0.2, 2.3, 1.1, 1), box(x, y, 0, e + 0.2, 2.1, 1.1, 1)); + } + case 2: { + let d = min(box(x, y, 0, -3.4, 1.5, 0.75, 0.5), box(x, y, 0, -1.4, 1.6, 0.75, 0.5)); + d = min(d, seg(x, y, -1.4, 0.8, -9.6, -0.2) - 0.8, seg(x, y, 1.4, 0.8, 9.6, -0.2) - 0.8, seg(x, y, 0, 1, 0, 14) - 1.2); + for (let v = 15.5; v < 24; v += 2.7) d = min(d, box(x, y, 0, v, 1.6, 0.9, 0.5)); + for (const [rx, ry] of RIBS) { + const c = 3.6 / (rx * rx), ax = min(abs(x), rx), f = ry + c * ax * ax; + d = min(d, abs(x) <= rx ? abs(y - f) / sqrt(1 + (2 * c * ax) ** 2) - 0.7 : sqrt((abs(x) - rx) ** 2 + (y - f) ** 2) - 0.7); + } + return d; + } + case 3: { + let d = 9; + for (let v = -4; v <= 4.1; v += 2.7) d = min(d, box(x, y, 0, v, 1.9, 0.95, 0.5), box(x, y, 0, v, 3.1, 0.4, 0.3)); + return d; + } + case 4: { + let d = max(abs(x) - (2.5 - 0.28 * (y + 5.5)), abs(y + 2.6) - 3); + for (let s = -1; s <= 1; s += 2) { + const dx = x - s * 5.3, dy = y + 3.6, c = 0.8, n = 0.6 * s; + d = min(d, ell(dx * c + dy * n, -dx * n + dy * c, 0, 0, 4.1, 2.9), abs(ell(x, y, s * 3.4, 2.4, 2.4, 1.9)) - 0.75, ell(x, y, s * 6.5, 0, 1.5, 1.5)); + } + return d; + } + case 5: + return min(ell(x, y, 0, -10.6, 7, 7.3), ell(x, y, 0, -6.8, 6.4, 2.3), box(x, y, 0, -3.2, 4.4, 2.6, 1.4)); + case 6: { + const hw = 4.3 - 0.4 * max(0, y); + return min(box(x, y, 0, 1.5, 4.3, 1.6, 0.8), max(abs(x) - hw, abs(y - 1.6) - 1.8)); + } + case 7: { + let d = ell(x, y, 0, 1.4, 1.8, 1.4); + for (let i = 0; i < 4; i++) d = min(d, seg(x, y, -1.2 + 0.8 * i, 2.2, -1.8 + 1.2 * i, 5.4) - 0.5); + return d; + } + case 8: + return min(ell(x, y, sg * 0.8, 1.6, 3.1, 1.4), ell(x, y, 0, 0.4, 1.4, 1)); + default: { + const t = cl((-17.4 - y) / 10, 0, 1); + return min(ell(x, y, 0.4, -17, 8.4, 1.4), seg(x, y, 0, -17.4, 2.6, -27.4) - mix(4.6, 0.7, t), seg(x, y, 2.6, -27.4, 6, -28.6) - 0.7); + } + } +} +// 0 none, 1 dark, 2 socket, 3 shade, 4 pupil, 5 hat band. +function hole(k, x, y) { + if (k === 5) { + if (((abs(x) - 2.9) / 2.2) ** 2 + ((y + 7.9) / 2.5) ** 2 < 1) return ((abs(x) - 2.9) / 1.15) ** 2 + ((y + 7.7) / 1.3) ** 2 < 1 ? 4 : 2; + const ny = y + 5.4; + if (ny >= 0 && ny < 1.7 && abs(x) < 0.3 + 0.5 * ny) return 1; + if (y > -2 && y < -0.5 && abs(x * 0.8) < 3.4) return (x + 4) / 1.45 - floor((x + 4) / 1.45) < 0.3 || y > -0.9 ? 1 : 0; + } else if (k === 6 && y < 1.2 && y > -0.2 && abs(x) < 3.8) return (x + 4) / 1.45 - floor((x + 4) / 1.45) < 0.3 ? 1 : 0; + else if (k === 4 && ((abs(x) - 4.6) / 2.3) ** 2 + ((y + 4.4 + 0.3 * abs(x)) / 1.3) ** 2 < 1) return 3; + else if (k === 9 && y > -19.4 && y < -17.6 && abs(x) < 5.2) return 5; + return 0; +} + +const FEMUR = 22, TIBIA = 21, ARM = 15, FORE = 13, HIP = 6.5, FLOOR = 120, HIPY = FLOOR - 3 - FEMUR - TIBIA; +const two = (p) => [p, p]; +const PIECES = [[8, -1, -3.9, 3.9, -1, 3.2], [8, 1, -3.9, 3.9, -1, 3.2], ...two([1, TIBIA, -2.4, 2.4, -11, 11]), ...two([0, FEMUR, -2.6, 2.6, -11.4, 11.4]), [4, 0, -11, 11, -9, 5.2], [3, 0, -3.6, 3.6, -5.3, 5.3], [2, 0, -10.6, 10.6, -4.5, 22.6], ...two([0, ARM, -2.6, 2.6, -7.9, 7.9]), ...two([1, FORE, -2.4, 2.4, -7, 7]), ...two([7, 0, -2.7, 2.7, -0.3, 6.1])]; +const SKULL = [5, 0, -7.2, 7.2, -18.1, 0.6], JAW = [6, 0, -4.4, 4.4, -0.3, 3.5], CAP = [9, 0, -8.6, 7, -29.6, -15.4]; + +const at = (K, i, x, y, a) => { + K.x[i] = K.cx + K.sc * x; + K.y[i] = FLOOR + K.sc * (y - FLOOR); + K.a[i] = a; +}; +const ang = (dx, dy) => atan2(-dx, dy); +const bone = (K, i, ax, ay, bx, by) => at(K, i, (ax + bx) / 2, (ay + by) / 2, ang(bx - ax, by - ay)); +const rot = (a, x, y) => [x * cos(a) - y * sin(a), x * sin(a) + y * cos(a)]; +function limb(K, ax, ay, tx, ty, L1, L2, i1, i2, px, py) { + const dx = tx - ax, dy = ty - ay, d = cl(sqrt(dx * dx + dy * dy), 2, L1 + L2 - 0.01), b = atan2(dy, dx); + const al = acos(cl((L1 * L1 + d * d - L2 * L2) / (2 * L1 * d), -1, 1)); + let kx = ax + L1 * cos(b + al), ky = ay + L1 * sin(b + al); + const mx = ax + L1 * cos(b - al), my = ay + L1 * sin(b - al); + if ((mx - px) ** 2 + (my - py) ** 2 < (kx - px) ** 2 + (ky - py) ** 2) (kx = mx), (ky = my); + const n = sqrt((tx - kx) ** 2 + (ty - ky) ** 2) || 1, u = (tx - kx) / n, v = (ty - ky) / n; + bone(K, i1, ax, ay, kx, ky); + bone(K, i2, kx, ky, kx + L2 * u, ky + L2 * v); + return [kx + L2 * u, ky + L2 * v, u, v]; +} +function torso(D, K) { + const rx = D.sway, ry = HIPY + D.bob, t = D.tilt, l = D.lean; + at(K, 6, rx, ry, t); + for (const s of [-1, 1]) { + const [hx, hy] = rot(t, s * HIP, 0), f = s < 0 ? D.fl : D.fr, ax = rx + hx, ay = ry + hy; + const [ex, ey] = limb(K, ax, ay, s * (HIP + f[1]), FLOOR - 3 - f[0], FEMUR, TIBIA, 4 + (s > 0), 2 + (s > 0), ax + s * 60, ay + 20); + at(K, s > 0 ? 1 : 0, ex, ey, 0); + } + const [b0, b1] = rot(t, 0, -5), sx = rx + b0, sy = ry + b1, [c0, c1] = rot(l, 0, -10); + bone(K, 7, sx + c0, sy + c1, sx, sy); + const [d0, d1] = rot(l, 0, -24), tx = sx + c0 + d0, ty = sy + c1 + d1 - D.shr * 0.4; + at(K, 8, tx, ty, l); + const [n0, n1] = rot(l, 0, -4.4); + Object.assign(K, { tx, ty, nx: K.cx + K.sc * (tx + n0), ny: FLOOR + K.sc * (ty + n1 - FLOOR), na: l + D.head }); +} +function arms(D, K) { + const l = D.lean; + for (const s of [-1, 1]) { + const [o0, o1] = rot(l, s * 10, 0.6 - D.shr), ax = K.tx + o0, ay = K.ty + o1, a1 = s < 0 ? D.al : D.ar, a2 = s < 0 ? D.bl : D.br; + const [u0, u1] = rot(l, s * sin(a1), cos(a1)), ex = ax + ARM * u0, ey = ay + ARM * u1; + const [v0, v1] = rot(l, s * sin(a1 + a2), cos(a1 + a2)); + let wx = ex + FORE * v0, wy = ey + FORE * v1; + const h = K.hold[s > 0 ? 1 : 0]; + if (h[0] > 0) (wx = mix(wx, (h[1] - K.cx) / K.sc, h[0])), (wy = mix(wy, FLOOR + (h[2] - FLOOR) / K.sc, h[0])); + const [hx, hy, u, v] = limb(K, ax, ay, wx, wy, ARM, FORE, 9 + (s > 0), 11 + (s > 0), ex, ey); + at(K, 13 + (s > 0), hx, hy, ang(u, v)); + } +} +const REST = { sway: 0, bob: 0, tilt: 0, lean: 0, head: 0, jaw: 0, shr: 0, al: 0.1, bl: 0.06, ar: 0.1, br: 0.06, fl: [0, 0], fr: [0, 0] }; + +// --- the story. The camera follows the body at BX; the stone tried stands +// at SX, its top at ST, the next GAP further on. +const SC = 0.85, BX = 20, SX = 40, ST = 82, SHW = 8, GAP = 48, WALK = 4, GROUND = 117, HS = GAP / SC / 8; +const BIG = 0, SMALL = 1, HAT = 2, BITER = 3, PUMPKIN = 4, RIGHT = 5; +const ORDER = [SMALL, HAT, BITER, PUMPKIN, BIG]; +const DARK = [34, 20, 50], TOXIC = [96, 255, 64]; +const EYE = [DARK, DARK, [255, 132, 8], [200, 70, 255], DARK, DARK]; +// Beats a wrong one stays on the neck, and when the shrug starts. +const GAG = [3.6, 3.6, 3.8, 0, 3.2, 0]; +const shrugAt = (k) => (k === RIGHT ? 10.6 : k === BITER ? 4.4 : 3.4 + GAG[k]); + +export function setup(params) { + params.s = { + hue: -1, t: 0, b: 0, ph: 0, cam: 0, c0: 0, stones: [], queue: [], + light: [0, 0, 0], bg: new Float32Array(N * 3), buf: new Float32Array(N * 3), + K: { cx: BX, sc: SC, x: new Float64Array(15), y: new Float64Array(15), a: new Float64Array(15), D: { ...REST }, hold: [] }, + }; +} +function stone(S, j) { + if (j < 0) return { j }; + if (!S.queue.length) { + for (let i = 0; i < S.tries; i++) S.queue.push(ORDER[(S.g + i) % 5]); + S.g += S.tries; + S.queue.push(RIGHT); + } + const kind = S.queue.shift(), sc = kind === BIG ? 1.1 + 0.9 * S.size : kind === SMALL ? 0.8 - 0.4 * S.size : kind === HAT ? 0.95 : 1; + return { j, kind, Hd: { kind, sc, eye: EYE[kind].slice(), hd: 0, jaw: 0, glow: 0 } }; +} +// A skull on its stone; where a hand takes it, on top (s 0) or at a side. +const sit = (S, st, Hd) => Object.assign(Hd, { x: SX + st.j * GAP - S.cam, y: ST - (Hd.kind === PUMPKIN ? 1 : 3.5 * Hd.sc), a: 0, z: 0 }); +const grip = (Hd, s) => { + const [p, q] = Hd.kind === PUMPKIN ? [0, -18] : rot(Hd.a, s * 8.6 * Hd.sc, (s ? -6 : -23) * Hd.sc); + return [Hd.x + p, Hd.y + q]; +}; +const arc = (Hd, ax, ay, a0, bx, by, a1, f, h, z = 1) => Object.assign(Hd, { x: mix(ax, bx, f), y: mix(ay, by, f) - 4 * h * f * (1 - f), a: mix(a0, a1, f), z }); +// A sideways scuttle: one foot stays put as the other swings on ahead. +function gait(D, u) { + const p = u / (2 * HS) - floor(u / (2 * HS)), q = (p % 0.5) * 2, w = q * HS, lift = 6 * sin(PI * q), o = sin(TAU * p); + D.fl = [p < 0.5 ? 0 : lift, p < 0.5 ? w : HS - w]; + D.fr = [p < 0.5 ? lift : 0, p < 0.5 ? w : HS - w]; + Object.assign(D, { bob: 2 - 1.6 * sin(PI * q), sway: (p < 0.5 ? -1.2 : 1.2) * sin(PI * q), al: 0.45 + 0.25 * o, ar: 0.45 - 0.25 * o, bl: 0.5, br: 0.5 }); +} + +// The body for beat b of a try: crouch, rise with the skull, the gag, crouch +// to put it back, shrug. +function pose(D, k, b) { + const E = 1.8 + GAG[k], q = b - 1.8, big = k === BIG, w = sin(PI * b), o = sin(TAU * b); + let c = b < 0.8 ? ease(b / 0.8) : 1 - ease(k === BITER ? (b - 0.95) / 0.3 : b - 0.8); + if (k < BITER || k === PUMPKIN) c = max(c, b < E + 1.2 ? ease((b - E - 0.4) / 0.6) : 1 - ease((b - E - 1.2) / 0.5)); + D.bob += (big ? 7 : 3) * c; + D.lean += (big ? 0.2 : 0.12) * c; + // A hand on the hip while the other carries. + if (k > SMALL && k !== BITER) blend(D, { al: 0.5, bl: -1.6 }, 1 - ease((b - (k === RIGHT ? 2 : E + 1.8)) / 0.4)); + if (big) { + // Blind: groping, staggering. + const g = win(q, 0.3, 0.4, GAG[k] - 0.5, 0.4); + blend(D, { al: 1.3 + 0.3 * o, ar: 1.3 - 0.3 * o, bl: 0.4, br: 0.4, sway: 3 * w, lean: D.lean + 0.1 * w }, g); + D[w > 0 ? "fr" : "fl"] = [5 * abs(w) * g, 2 * abs(w) * g]; + } + // The hips work against the small one's wobble. + if (k === SMALL && b < E) D.sway = -3 * sin(TAU * 1.25 * q) * cl(q / 1.5, 0, 1); + if (k === BITER) { + // Bitten: jump back, the free arm flapping. + const g = win(b, 1, 0.2, 3.4, 0.4); + D.lean -= 0.12 * g; + D.bob -= 3 * g * abs(sin(TAU * 1.25 * b)); + blend(D, { al: 1.3 + 0.5 * sin(TAU * 2 * b), bl: 0.6 }, g); + } + if (k === RIGHT) { + // The jig: kicks to each side, hands on the hips. + const g = win(q, 0.5, 0.4, 6.4, 0.3), j = sin(PI * (b - floor(b))) ** 2 * g, s = floor(b) & 1 ? 1 : -1; + blend(D, { sway: -2 * s * j, bob: -2 * j, head: 0.2 * s * j, al: 0.5, ar: 0.5, bl: -1.6, br: -1.6 }, g); + D[s < 0 ? "fl" : "fr"] = [12 * j, 7 * j]; + blend(D, { al: 2.6, ar: 2.6, bl: 0.3, br: 0.3 }, win(b, 8.4, 0.15, 9, 0.4)); + } + const s0 = shrugAt(k); + blend(D, { shr: 3.5, al: 0.3, ar: 0.3, bl: 2.5, br: 2.5 }, win(b, s0, 0.3, s0 + 0.9, 0.3)); +} + +// The skull being tried and the hands on it. +function skull(S, K, st, b) { + const Hd = st.Hd, k = st.kind, E = 1.8 + GAG[k], q = b - 1.8, R = (s, w, x, y) => (K.hold[s] = [w, x, y]); + // Patting the neck where it was. + const pat = k === RIGHT ? win(b, 9.2, 0.3, 10.4, 0.3) : 0; + if (pat > 0) for (const s of [0, 1]) R(s, pat, K.nx + (2 * s - 1) * 5, K.ny - 3 + 3 * sin(TAU * 2 * b + s * PI)); + if (!Hd) return; + // A hand on top; both at the sides of the big one. + const take = (w, two = k === BIG) => (two ? [0, 1].forEach((s) => R(s, w, ...grip(Hd, 2 * s - 1))) : R(1, w, ...grip(Hd, 0))); + sit(S, st, Hd); + const sx = Hd.x, sy = Hd.y; + if (k === BITER) { + Hd.jaw = b > 0.8 && b < 1.05 ? 1.3 : 0.5 * max(0, sin(TAU * 3 * b)); + if (b < 1.05) take(ease(b / 0.6)); + else if (b < 3.3) { + // Clamped on the hand, shaken high. + Object.assign(Hd, { x: K.x[14], y: K.y[14] + 2, a: 0.35 * sin(TAU * 2.5 * b), jaw: 0, z: 2, fx: K.x[14] }); + R(1, 1, BX + 13 + 5 * sin(TAU * 2.2 * b), 34 + 4 * sin(TAU * 4.4 * b)); + } else if (b < 4.1) { + // Flung back, turning once. + arc(Hd, Hd.fx ?? sx, 36, 0, sx, sy, -TAU, ease((b - 3.3) / 0.8), 8, 2); + R(1, 1 - ease((b - 3.3) / 0.5), SX - 6, 60); + } + return; + } + if (b < 0.8) return take(ease(b / 0.6)); + if (b < 1.8) return arc(Hd, sx, sy, 0, K.nx, K.ny, K.na, ease(b - 0.8), 7), take(1); + const gag = (t) => { + Object.assign(Hd, { x: K.nx, y: K.ny, a: K.na, z: 1 }); + if (k === BIG) { + // It drops down over the ribs. + const [p, r] = rot(K.na, 0, (6 + 18 * Hd.sc) * ease(t / 0.4) - 3 * sin(PI * cl((t - 0.4) / 0.3, 0, 1))); + (Hd.x += p), (Hd.y += r); + } else if (k === SMALL) { + // Wobbling ever wider, then tipping off. + const tip = ease((t - GAG[k] + 0.7) / 0.5); + Hd.a += 0.5 * sin(TAU * 1.25 * t) * cl(t / 1.5, 0, 1) * (1 - tip) + 1.4 * tip; + (Hd.x += 5 * tip), (Hd.y += 3 * tip); + } else if (k === HAT) Hd.hd = win(t, 0.3, 0.2, 1, 0.3) + (t > 2.1 && t < GAG[k] ? ease((t - 2.1) / 0.2) : 0); + }; + if (k === RIGHT) { + gag(min(q, 6.6)); + Hd.glow = ease(q / 0.4); + Hd.flash = q < 0.5 ? 1 - q / 0.5 : 0; + for (let c = 0; c < 3; c++) Hd.eye[c] = mix(DARK[c], TOXIC[c], Hd.glow); + Hd.jaw = K.D.jaw; + if (q < 0.3) take(1 - ease(q / 0.3)); + if (b > 8.4) { + // Off it goes. + const f = min(1, b - 8.4); + arc(Hd, K.nx, K.ny, K.na, 3, GROUND - 3.5, -TAU, f, 14, 2); + if (f >= 1) (S.lost = { Hd, wx: 3 + S.cam, t: 0 }), (st.Hd = null); + } + return; + } + if (b < E) { + gag(q); + if (k === PUMPKIN) R(1, win(q, 0.6, 0.3, 2.4, 0.3), Hd.x + 10 + 2.5 * abs(sin(TAU * 1.5 * q)), Hd.y - 7); + else if (k === HAT) R(1, win(q, 0.8, 0.2, 1.6, 0.3), Hd.x + 6.5 * Hd.sc, Hd.y + (10 * Hd.hd - 17) * Hd.sc); + else if (k === SMALL) { + // Hands up ready; the right one catches it. + for (const s of [0, 1]) R(s, ease((q - 0.3) / 0.4), K.nx + (2 * s - 1) * 11, K.ny - 8); + if (b > E - 0.5) R(1, 1, ...grip(Hd, 1)); + } else if (q < 0.3) take(1 - ease(q / 0.3)); + return; + } + gag(E - 1.801); + if (b < E + 0.4) return take(ease((b - E) / 0.4)); + if (b < E + 1.4) return arc(Hd, Hd.x, Hd.y, Hd.a, sx, sy, 0, ease(b - E - 0.4), 6), (Hd.hd = 0), take(1); + sit(S, st, Hd); + take(1 - ease((b - E - 1.4) / 0.4)); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kn = input.knobValues || [0, 2, 1, 0.06], d = cl(dt || 0, 0, 0.1), hue = cl(kn[0], 0, 1); + if (hue !== S.hue) { + hsv(0.78 - 0.3 * (S.hue = hue), 0.62, S.light); + background(S); + } + (S.size = cl(kn[3], 0, 1)), (S.tries = round(cl(kn[2], 1, 5))); + if (!S.stones.length) (S.g = 5 - S.tries), (S.stones = [-1, 0, 1].map((j) => stone(S, j))); + const db = d * cl(kn[1], 0.2, 3); + (S.b += db), (S.t += d); + let st = S.stones[1]; + if (!S.ph && S.b >= shrugAt(st.kind) + 1.2) (S.b -= shrugAt(st.kind) + 1.2), (S.ph = 1), (S.c0 = S.cam); + if (S.ph && S.b >= WALK) { + S.b -= WALK; + S.ph = 0; + S.cam = S.c0 + GAP; + S.stones = [S.stones[1], S.stones[2], stone(S, S.stones[2].j + 1)]; + st = S.stones[1]; + } + const K = S.K, D = Object.assign(K.D, REST, { fl: [0, 0], fr: [0, 0] }); + K.hold = [[0], [0]]; + if (S.ph) { + // Walking on: the camera glides one stone further. + S.cam = S.c0 + GAP * ease(S.b / WALK); + gait(D, (S.cam - S.c0) / SC); + } else pose(D, st.kind, S.b); + torso(D, K); + for (const x of S.stones) if (x.Hd && (x !== st || S.ph)) sit(S, x, x.Hd); + if (!S.ph) skull(S, K, st, S.b); + arms(D, K); + if (S.lost) { + // Rolling away about the middle of the cranium. + const L = S.lost, x = L.wx - S.cam - 9 * (L.t += db), a = -L.t, [p, r] = rot(a, 0, 10.6); + Object.assign(L.Hd, { x: x + p, y: GROUND - 14.1 + r, a, z: 2 }); + if (x < -24) S.lost = null; + } +} + +// --- the graveyard, laid out per sky colour: the stepped sky and the moon +// over a dark hill. +const MX = 46, MY = 20, MR = 12; +const put = (buf, o, r, g, b) => ((buf[o] = r), (buf[o + 1] = g), (buf[o + 2] = b)); +function background(S) { + const L = S.light; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const q = sqrt((x + 0.5 - MX) ** 2 + (y + 0.5 - MY) ** 2), v = floor((0.14 + 0.5 * (y / GROUND) ** 1.6) * 20) / 20, e = q < MR + 4 ? 60 : q < MR + 9 ? 26 : 0; + const sp = q > MR - 1.5 || (x - MX + 3) ** 2 + (y - MY + 2) ** 2 < 10 ? 24 : 0; + if (q < MR) put(S.bg, (y * W + x) * 3, 255 - sp, 240 - sp, 204 - sp); + else if (y > 100 + 4 * sin(x * 0.09) + 2.5 * sin(x * 0.23 + 1.3)) put(S.bg, (y * W + x) * 3, L[0] * 0.16 + 8, L[1] * 0.16 + 6, L[2] * 0.16 + 12); + else put(S.bg, (y * W + x) * 3, L[0] * v + e, L[1] * v + e * 0.93, L[2] * v + e * 0.83); + } +} +// A tall headstone with a cut cross, rimmed by the moon. +const inStone = (dx, y) => abs(dx) < SHW && y < GROUND && (y >= ST + SHW || dx * dx + (y - ST - SHW) ** 2 < SHW * SHW); +function headstone(buf, cx) { + for (let y = ST - 1; y < GROUND; y++) + for (let x = max(0, floor(cx - SHW)); x < min(W, ceil(cx + SHW)); x++) { + const dx = x + 0.5 - cx, yy = y + 0.5; + if (!inStone(dx, yy)) continue; + const cut = (abs(dx) < 1.1 && y > ST + 5 && y < ST + 19) || (abs(dx) < 4.2 && y >= ST + 9 && y < ST + 11); + const v = !inStone(dx, yy - 1) || !inStone(dx + 1, yy) ? 1.6 : cut ? 0.45 : dx < 3 - SHW ? 0.75 : 1; + put(buf, (y * W + x) * 3, 92 * v, 94 * v, 112 * v); + } +} +// Bone in three tones, the darkest tinted by the sky; the hat violet. +const BONE = [[255, 244, 220], [212, 202, 180], [96, 88, 84]]; +const HATC = [[196, 120, 255], [140, 64, 210], [70, 26, 112]]; +const FIELD = new Float32Array(48 * 48); +function piece(S, pc, cx, cy, a, sc, Hd) { + const [k, sg, x0, x1, y0, y1] = pc, c = cos(a), s = sin(a); + let xa = 1e9, xb = -1e9, ya = 1e9, yb = -1e9; + for (const [u, v] of [[x0, y0], [x1, y0], [x0, y1], [x1, y1]]) { + const wx = cx + (u * c - v * s) * sc, wy = cy + (u * s + v * c) * sc; + (xa = min(xa, wx)), (xb = max(xb, wx)), (ya = min(ya, wy)), (yb = max(yb, wy)); + } + xa = max(0, floor(xa) - 1); + xb = min(W - 1, ceil(xb) + 1); + ya = max(0, floor(ya) - 1); + yb = min(H - 1, ceil(yb) + 1); + if (xb < xa || yb < ya || xb - xa >= 48 || yb - ya >= 48) return; + const buf = S.buf, L = S.light, z = 1 / sc; + for (let y = ya; y <= yb; y++) + for (let x = xa; x <= xb; x++) { + const dx = x + 0.5 - cx, dy = y + 0.5 - cy; + FIELD[(y - ya) * 48 + x - xa] = shape(k, (dx * c + dy * s) * z, (-dx * s + dy * c) * z, sg) * sc; + } + for (let y = ya; y <= yb; y++) + for (let x = xa; x <= xb; x++) { + const f = (y - ya) * 48 + x - xa, d = FIELD[f], o = (y * W + x) * 3; + if (d > 0.1) { + if (d < 1.1) for (let ch = 0; ch < 3; ch++) buf[o + ch] *= 0.3; + continue; + } + const dx = x + 0.5 - cx, dy = y + 0.5 - cy, hl = hole(k, (dx * c + dy * s) * z, (-dx * s + dy * c) * z); + if (hl && hl !== 3) { + const e = hl === 1 ? [2, 0, 4] : hl === 5 ? [245, 134, 2] : Hd.eye, g = hl === 2 ? 0.34 : 1; + put(buf, o, e[0] * g + 10, e[1] * g + 6, e[2] * g + 14); + continue; + } + const up = y > ya ? FIELD[f - 48] : d + 1, dn = y < yb ? FIELD[f + 48] : d + 1, n = (up - dn) / (abs(up - dn) + abs((x > xa ? FIELD[f - 1] : d) - (x < xb ? FIELD[f + 1] : d)) + 0.01); + const t = hl === 3 || n <= -0.3 ? 2 : n > 0.4 || (d > -0.6 && up > 0.1) ? 0 : 1, C = k === 9 ? HATC : BONE, w = t === 2 && k !== 9 ? 0.4 : 0; + for (let ch = 0; ch < 3; ch++) buf[o + ch] = C[t][ch] + L[ch] * w; + } +} +// A jack-o'-lantern, the face flickering in three steps. +function pumpkin(S, Hd) { + const buf = S.buf, cx = Hd.x, cy = Hd.y - 6, v = 0.6 + 0.2 * sin(S.t * 9) + 0.2 * sin(S.t * 23.7), fl = v > 0.82 ? 1 : v > 0.66 ? 0.84 : 0.7; + for (let y = max(0, floor(cy - 11)); y < min(H, cy + 9); y++) + for (let x = max(0, floor(cx - 11)); x < min(W, cx + 11); x++) { + const dx = x + 0.5 - cx, dy = y + 0.5 - cy, o = (y * W + x) * 3, e = (dx / 9) ** 2 + (dy / 7) ** 2; + if (e < 1) { + const rib = abs(abs(dx) - 4.2) < 0.6 && e > 0.25, eye = dy > -4.2 && dy < -0.8 && abs(abs(dx) - 3.6) < 1.9 + (dy + 4.2) * 0.5, grin = dy > 1.1 && dy < 3.9 - abs(dx) * 0.15 && abs(dx) < 6 && !(abs(dx) % 3 < 0.8 && dy < 2.2); + if (eye || grin) put(buf, o, 255, 196 * fl + 30, 50 * fl); + else put(buf, o, rib ? 220 : 255, rib ? 84 : 118, rib ? 0 : 10); + } else if (e < 1.35 && dy > -8) put(buf, o, buf[o] * 0.3, buf[o + 1] * 0.3, buf[o + 2] * 0.3); + if (abs(dx + 0.3) < 1.2 && dy > -9.6 && dy < -6.2) put(buf, o, 72, 150, 40); + } +} +function head(S, Hd) { + if (Hd.kind === PUMPKIN) return pumpkin(S, Hd); + // The right one's toxic halo, white on the click. + const [p, q] = rot(Hd.a, 0, -8), cx = Hd.x + p, cy = Hd.y + q, f = Hd.flash || 0; + for (let y = max(0, floor(cy - 15)); y < min(H, cy + 15) && Hd.glow > 0; y++) + for (let x = max(0, floor(cx - 15)); x < min(W, cx + 15); x++) { + const v = floor(max(0, 1 - sqrt((x + 0.5 - cx) ** 2 + (y + 0.5 - cy) ** 2) / 15) * 4) / 4; + for (let ch = 0, o = (y * W + x) * 3; ch < 3; ch++) S.buf[o + ch] += (TOXIC[ch] * Hd.glow * 0.8 + 255 * f) * v; + } + piece(S, SKULL, Hd.x, Hd.y, Hd.a, Hd.sc, Hd); + const [j0, j1] = rot(Hd.a, 0, (2.4 * Hd.jaw - 0.7) * Hd.sc), [h0, h1] = rot(Hd.a, 0, 10 * Hd.hd * Hd.sc); + piece(S, JAW, Hd.x + j0, Hd.y + j1, Hd.a, Hd.sc, Hd); + if (Hd.kind === HAT) piece(S, CAP, Hd.x + h0, Hd.y + h1, Hd.a, Hd.sc, Hd); +} + +export function draw(display, params) { + const S = params.s; + if (!S || S.hue < 0 || !S.stones.length) return; + const buf = S.buf, K = S.K, cam = S.cam; + buf.set(S.bg); + // The ground, a lit edge and specks sliding with the camera. + for (let p = GROUND * W; p < N; p++) { + const y = (p / W) | 0, e = y === GROUND ? 30 : (floor((p % W) + cam) * 7 + y * y * 3) % 11 ? 0 : 12; + put(buf, p * 3, 40 + e, 26 + e * 0.8, 48 + e); + } + const heads = (z) => [...S.stones.map((st) => st.Hd), S.lost?.Hd].forEach((Hd) => Hd && Hd.z === z && head(S, Hd)); + // The stones stand in front of the path; the hands in front of what they hold. + for (let i = 0; i < 13; i++) piece(S, PIECES[i], K.x[i], K.y[i], K.a[i], K.sc, null); + for (const st of S.stones) headstone(buf, SX + st.j * GAP - cam); + heads(0); + heads(1); + for (let i = 13; i < 15; i++) piece(S, PIECES[i], K.x[i], K.y[i], K.a[i], K.sc, null); + heads(2); + for (let p = 0, o = 0; p < N; p++, o += 3) display.setPixel(p % W, (p / W) | 0, min(255, buf[o]) & 252, min(255, buf[o + 1]) & 252, min(255, buf[o + 2]) & 252); +} From ccc5ea82afc893190b9ae81fae12b91d15a9a5e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 00:09:17 +0000 Subject: [PATCH 03/10] =?UTF-8?q?52=ED=8C=90:=20Gaussian=20Free=20Field=20?= =?UTF-8?q?=EC=99=80=20Pumpkin=20Drop?= 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_01LXsMxtcgxEaHMjeqmCbsjv --- .../patterns/gaussian-free-field.js | 250 +++++++++ .../pattern-study/patterns/pumpkin-drop.js | 516 ++++++++++++++++++ 2 files changed, 766 insertions(+) create mode 100644 incoming/pattern-study/patterns/gaussian-free-field.js create mode 100644 incoming/pattern-study/patterns/pumpkin-drop.js diff --git a/incoming/pattern-study/patterns/gaussian-free-field.js b/incoming/pattern-study/patterns/gaussian-free-field.js new file mode 100644 index 000000000..3ffcca548 --- /dev/null +++ b/incoming/pattern-study/patterns/gaussian-free-field.js @@ -0,0 +1,250 @@ +// ===== Patternflow pattern ===== +// Title: Gaussian Free Field +// Family: noise +// Idea: The discrete Gaussian free field on the panel's 64 by 128 lattice, an amber land above zero and a dark violet sea below, its zero level lines drawn as white coastlines that close into nested loops, while every sine mode of the field wanders on its own and the whole map redraws itself within a second or two; with the boundary knob up, one white line climbs all 128 rows, the SLE(4) curve. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: discrete Gaussian free field, whose contour lines converge to SLE(4) (Oded Schramm and Scott Sheffield, 2009), pattern round 52 +// Round: 52 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Boundary=0..1, Grain=0..1 +// +// Knob 1: Hue. The colour of the land. At 0 it rises from orange through amber to gold, a step at every height of 0.63 (the height gap, see Boundary); at 0.25 green, at 0.5 sky blue, at 0.75 pink, and back to orange at 1. The sea stays violet, leaning bluer or redder, and the coastlines stay white. +// Knob 2: Speed. Each of the 8,192 sine modes wanders as its own Ornstein-Uhlenbeck process, smoothed in time, so at every instant the field has the free field's law. At 2 the largest mode forgets itself in 0.7 s and the finest, two LEDs from crest to crest, in 0.14 s; at 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Boundary. What lies outside the frame. At 0 the field is zero all round, and the coastlines are closed loops, nested, which tend to CLE(4). Turned up, the frame is held at +h on its left half and -h on its right, up to h = sqrt(pi/8) = 0.63 at 1: then exactly one coastline runs from the middle of the bottom edge to the middle of the top, the chordal SLE(4), and from 0.33 on it is drawn two LEDs wide while the other coastlines dim to pale amber. +// Knob 4: Grain. The width of the Gaussian blur under the coastlines and bands, from 1.4 LEDs at 0 to 5 at 1, where the land breaks into a few smooth blobs; it opens at 1.6. The shading always shows the lattice field itself, lit from the upper left. + +const W = 64, H = 128, N = W * H, HW = W / 2, HH = H / 2; +const { floor, min, abs, sqrt, sin, cos, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// The height gap: with -LAM on one boundary arc and +LAM on the other the +// zero line between them is SLE(4), for a field whose covariance is the +// inverse of the lattice Laplacian. +const LAM = sqrt(PI / 8); + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + o[1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + o[2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; + return o; +} + +// Sine modes, orthonormal: SX[j * W + x] is mode j at column x and, the +// matrix being symmetric, also mode x at column j. LA is the eigenvalue of +// minus the lattice Laplacian for mode (j, k), stored at j * H + k. +const SX = new Float64Array(W * W), SY = new Float64Array(H * H), LA = new Float64Array(N); +for (let j = 0; j < W; j++) for (let x = 0; x < W; x++) SX[j * W + x] = sqrt(2 / (W + 1)) * sin((PI * (j + 1) * (x + 1)) / (W + 1)); +for (let k = 0; k < H; k++) for (let y = 0; y < H; y++) SY[k * H + y] = sqrt(2 / (H + 1)) * sin((PI * (k + 1) * (y + 1)) / (H + 1)); +for (let j = 0; j < W; j++) for (let k = 0; k < H; k++) LA[j * H + k] = 4 - 2 * cos((PI * (j + 1)) / (W + 1)) - 2 * cos((PI * (k + 1)) / (H + 1)); +const LMIN = LA[0]; +// The field is drawn with a blur of a third of an LED, which leaves the lattice +// as it is: AMP is each mode's standard deviation, 1 / sqrt(l), times the +// square root of 1.5 that the filter in update takes away. TAU is each +// mode's relaxation time as a share of the largest mode's. +const C0 = 0.05, AMP = new Float64Array(N), TAU = new Float64Array(N); +for (let i = 0; i < N; i++) { + AMP[i] = (sqrt(1.5) * exp(-C0 * LA[i])) / sqrt(LA[i]); + TAU[i] = (LMIN / LA[i]) ** 0.2; +} + +// Normal deviates from a table of 4096 quantiles of the unit normal, read +// off its distribution function summed in steps of a thousandth and scaled +// to unit variance: one draw of Math.random each. +const GT = new Float64Array(4096); +{ + let c = 0, i = 0, v = 0; + for (let z = -6; z < 6 && i < 4096; z += 0.001) { + const c2 = c + (exp(-((z + 0.0005) ** 2) / 2) * 0.001) / sqrt(2 * PI); + while (i < 4096 && (i + 0.5) / 4096 <= c2) GT[i++] = z + (0.001 * ((i + 0.5) / 4096 - c)) / (c2 - c); + c = c2; + } + for (i = 0; i < 4096; i++) v += GT[i] * GT[i]; + v = sqrt(v / 4096); + for (i = 0; i < 4096; i++) GT[i] /= v; +} +const gauss = () => GT[(Math.random() * 4096) | 0]; + +// Modes B (j, k at B[j * H + k]) to the field F[y * W + x], one axis at a +// time. Mode k is even or odd about the middle row as k + 1 is odd or even, +// so each sum serves a row and its mirror; the same for columns. +function synth(B, T, F) { + for (let j = 0; j < W; j++) { + const b = j * H; + for (let y = 0; y < HH; y++) { + const r = y * H; + let e = 0, o = 0; + for (let k = 0; k < H; k += 2) { + e += B[b + k] * SY[r + k]; + o += B[b + k + 1] * SY[r + k + 1]; + } + T[y * W + j] = e + o; + T[(H - 1 - y) * W + j] = e - o; + } + } + for (let y = 0; y < H; y++) { + const t = y * W; + for (let x = 0; x < HW; x++) { + const r = x * W; + let e = 0, o = 0; + for (let j = 0; j < W; j += 2) { + e += SX[r + j] * T[t + j]; + o += SX[r + j + 1] * T[t + j + 1]; + } + F[t + x] = e + o; + F[t + W - 1 - x] = e - o; + } + } +} + +export function setup(params) { + const S = (params.s = { + hue: 0, speed: 2, bnd: 0, grain: 0.06, qd: -1, + z: new Float64Array(N), a: new Float64Array(N), Q: new Float64Array(N), R: new Float64Array(N), Z: new Float64Array(N), + B: new Float64Array(N), T: new Float64Array(N), F: new Float64Array(N), U: new Float64Array(N), G: new Float64Array(N), V: new Float64Array(N), + K: new Float64Array(24), P: new Float64Array(H + 40), side: new Int8Array(N), stack: new Int32Array(N), chord: new Uint8Array(N), c: [0, 0, 0], + }); + for (let i = 0; i < N; i++) S.z[i] = S.a[i] = gauss(); + // The harmonic part for the Dobrushin boundary: +1 outside the left half + // of the frame and -1 outside the right half. Minus its Laplacian inside + // is what the edge LEDs see beyond the frame, so it is that source divided + // by each mode's eigenvalue. The sine matrices are symmetric, so synth run + // on the source laid out by column gives its modes laid out by row. + const f = S.B, m = S.F; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let s = 0; + if (x === 0) s += 1; + if (x === W - 1) s -= 1; + if (y === 0 || y === H - 1) s += x < HW ? 1 : -1; + f[x * H + y] = s; + } + synth(f, S.T, m); + for (let j = 0; j < W; j++) for (let k = 0; k < H; k++) f[j * H + k] = m[k * W + j] / LA[j * H + k]; + synth(f, S.T, S.U); +} + +// The pixels joined to the left arc through LEDs above zero (side 1), and +// those joined to the right arc through the rest (side 2): the zero line +// between the two is the chordal one, from the middle of the bottom edge to +// the middle of the top. +function sides(S) { + const V = S.V, sd = S.side, st = S.stack; + sd.fill(0); + let n = 0; + const seed = (p, s) => { + if (!sd[p] && (s === 2 || V[p] > 0)) (sd[p] = s), (st[n++] = p); + }; + const fill = (s) => { + while (n) { + const p = st[--n], x = p % W; + if (x > 0) seed(p - 1, s); + if (x < W - 1) seed(p + 1, s); + if (p >= W) seed(p - W, s); + if (p < N - W) seed(p + W, s); + } + }; + for (let y = 0; y < H; y++) seed(y * W, 1); + for (let x = 0; x < HW; x++) seed(x, 1), seed(N - W + x, 1); + fill(1); + for (let y = 0; y < H; y++) seed(y * W + W - 1, 2); + for (let x = HW; x < W; x++) seed(x, 2), seed(N - W + x, 2); + fill(2); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.bnd = cl(k[2], 0, 1); + S.grain = cl(k[3], 0, 1); + // Each mode is an Ornstein-Uhlenbeck process seen through an exponential + // filter of half its time constant, which smooths its path in time; the + // filter takes its variance to 1 / 1.5, which AMP gives back, so at every + // instant the modes are independent normals and the field is the free + // field. The largest mode forgets itself in about 0.7 s at Speed 2. + const z = S.z, a = S.a, B = S.B, Q = S.Q, R = S.R, Z = S.Z; + const qd = (d * S.speed) / (0.7 * 2); + if (qd !== S.qd) { + S.qd = qd; + for (let i = 0; i < N; i++) { + const q = exp(-qd / TAU[i]); + Q[i] = q; + Z[i] = sqrt(1 - q * q); + R[i] = q * q; + } + } + for (let i = 0; i < N; i++) { + z[i] = z[i] * Q[i] + Z[i] * gauss(); + a[i] = a[i] * R[i] + z[i] * (1 - R[i]); + B[i] = a[i] * AMP[i]; + } + synth(B, S.T, S.F); + // The field the level lines are drawn on: the same field blurred by a + // Gaussian of Grain's width, zero outside the panel as the boundary is. + const sg = 1.4 + 3.6 * S.grain, rad = min(20, Math.ceil(2.5 * sg)), K = S.K; + let ks = 0; + for (let i = 0; i <= rad; i++) ks += (K[i] = exp((-i * i) / (2 * sg * sg))) * (i ? 2 : 1); + for (let i = 0; i <= rad; i++) K[i] /= ks; + const F = S.F, G = S.G, V = S.V, U = S.U, P = S.P, g = S.bnd * LAM; + // Rows, then columns, each copied into a strip with 20 zeros either side. + for (let y = 0; y < H; y++) { + for (let x = 0; x < W; x++) P[20 + x] = F[y * W + x]; + for (let x = 0; x < W; x++) { + const c = 20 + x; + let s = K[0] * P[c]; + for (let i = 1; i <= rad; i++) s += K[i] * (P[c - i] + P[c + i]); + G[y * W + x] = s; + } + } + for (let x = 0; x < W; x++) { + for (let y = 0; y < H; y++) P[20 + y] = G[y * W + x]; + for (let y = 0; y < H; y++) { + const c = 20 + y, p = y * W + x; + let s = K[0] * P[c]; + for (let i = 1; i <= rad; i++) s += K[i] * (P[c - i] + P[c + i]); + V[p] = (s + g * U[p]) / LAM; + } + } + if (S.bnd > 0) sides(S); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const V = S.V, F = S.F, sd = S.side, c = S.c, hp = 0.07 + S.hue, hn = 0.7 + 0.06 * sin(2 * PI * S.hue); + const ch = S.bnd > 0 ? min(1, 3 * S.bnd) : 0; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, v = V[p], L = Math.round(v), e = v - L, ae = abs(e); + // On a level line: a neighbour lies across the nearest level and is no + // nearer to it. The chord is drawn two LEDs wide, on both sides. + let z = 0, q, n = -1; + if (x > 0 && ((q = V[p - 1] - L), q * e < 0) && abs(q) >= ae) n = p - 1; + else if (x < W - 1 && ((q = V[p + 1] - L), q * e < 0) && abs(q) >= ae) n = p + 1; + else if (y > 0 && ((q = V[p - W] - L), q * e < 0) && abs(q) >= ae) n = p - W; + else if (y < H - 1 && ((q = V[p + W] - L), q * e < 0) && abs(q) >= ae) n = p + W; + if (n >= 0) z = L === 0 ? 2 : 1; + if (ch) { + const o = 3 - sd[p]; + if (o < 3 && ((x > 0 && sd[p - 1] === o) || (x < W - 1 && sd[p + 1] === o) || (y > 0 && sd[p - W] === o) || (y < H - 1 && sd[p + W] === o))) z = 3; + } + // Relief: the fine field lit from the upper left. + const u = ((x > 0 ? F[p - 1] : 0) - (x < W - 1 ? F[p + 1] : 0) + (y > 0 ? F[p - W] : 0) - (y < H - 1 ? F[p + W] : 0)) * 0.8, sh = (0.35 * u) / (1 + abs(u)); + const t = 1 - exp(-abs(v) / 1.5); + if (z === 3) hsv(hp + 0.1 + 0.03 * sh, 0.04 + 0.1 * abs(sh), 1, c); + else if (z === 2) hsv(hp + 0.05 + 0.04 * sh, 0.05 + 0.12 * abs(sh) + 0.35 * ch, 1 - 0.35 * ch, c); + else if (v > 0) { + // Above zero the brightest channel stays at full. Each band of + // height LAM steps the hue from amber toward gold and pales it; the + // relief turns hue and saturation a little inside the band. + const b = min(L - (e < 0 ? 1 : 0), 3); + hsv(hp + 0.04 * b + 0.02 * (v - b) + 0.05 * sh - (z ? 0.025 : 0), 0.8 - 0.1 * b + 0.5 * sh, z ? 0.86 + 0.3 * sh : 1, c); + } else hsv(hn + 0.08 * t + 0.12 * sh, 0.85 - 0.3 * t, z ? 0.3 * (1 + sh) : (0.16 - 0.04 * t) * (1 + 1.8 * sh), c); + display.setPixel(x, y, min(255, c[0]) | 0, min(255, c[1]) | 0, min(255, c[2]) | 0); + } +} diff --git a/incoming/pattern-study/patterns/pumpkin-drop.js b/incoming/pattern-study/patterns/pumpkin-drop.js new file mode 100644 index 000000000..24e9f123a --- /dev/null +++ b/incoming/pattern-study/patterns/pumpkin-drop.js @@ -0,0 +1,516 @@ +// ===== Patternflow pattern ===== +// Title: Pumpkin Drop +// Family: halloween +// Idea: A lit jack-o'-lantern is wound up a wooden hoist under a cloudy purple night, swings at the top, is let go and falls all 128 rows with its flame streaking upward, then bursts on the ground, flinging shards and seeds over a splash of pulp while the candle tumbles out and sputters into a puff of smoke; the next pumpkin spins in from the right, lights up and goes up. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 52 +// Round: 52 +// =============================== +// @matrix 64x128 +// @knobs Flame=0..1, Speed=0.2..3, Wind=0..1, Size=0..1 +// +// Knob 1: Flame. The colour of the candle and of what it lights: the carved face, the halo on the sky and the hoist, the streak as it falls and the flash of the burst. Gold at 0, toxic green at 0.25, blue at 0.5, magenta at 0.75. The pumpkin's skin stays orange. +// Knob 2: Speed. At 2 a round takes about 10 s. The pumpkin is wound up from the ground in 2.2 s and swings for 1.6 s; the fall of 81 rows takes 1.04 s and ends at 156 LEDs a second; the burst lasts 2.4 s, the candle going out 1.3 to 1.6 s after the impact; the hook comes down in 0.9 s and the next pumpkin takes 1.8 s to come in and light. At 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Wind. How far the pumpkin swings at the top and how fast the clouds and the smoke drift right. At 0 nearly still air: a swing of 5 degrees either way and clouds at 2 LEDs a second. At 1 it swings 50 degrees and is let go at the end of a long arc, left and right in turn, while the top band of cloud runs at 18 LEDs a second. A big pumpkin swings less, so it stays on the panel. +// Knob 4: Size. The pumpkin, 16 LEDs wide at 0 and 32 at 1, about 17 at the opening. A bigger one throws more light, farther, and bursts into more pieces: 25 shards at the opening and 48 at 1. +// Buttons: K1 tap lets the hanging pumpkin go at once. + +const W = 64, H = 128, N = W * H; +const { floor, ceil, round, min, max, abs, sqrt, sin, cos, exp, asin, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (v) => (v <= 0 ? 0 : v >= 1 ? 1 : v * v * (3 - 2 * v)); +const rnd = (a, b) => a + (b - a) * Math.random(); +const hash = (i) => { + let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; +const vn = (x, sd) => { + const i = floor(x), f = x - i, u = f * f * (3 - 2 * f); + return (hash(i * 7919 + sd) * (1 - u) + hash((i + 1) * 7919 + sd) * u) * 2 - 1; +}; +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + o[1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + o[2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; + return o; +} + +// The ground's top row; the hoist's post, beam and pulley; the moon. +const GY = 119, PX = 38, PY = 13, MX = 52, MY = 34, MR = 7, CB = [20, 40, 62]; +// The rope's length from the pulley to the hook when the pumpkin hangs at +// the top; the pumpkin's middle hangs its half height and 2 LEDs below. +const HOOK = 9; +// Seconds of each part at Speed 2. +const T_HANG = 1.6, T_SMASH = 2.4, T_LOWER = 0.9, T_ROLL = 1.3, T_LIGHT = 0.5, T_HOIST = 2.2, SWING = 1.6; +const G = 150; +// Light round a point, by squared distance in LEDs. +const RAD = new Float32Array(20480); +for (let i = 0; i < RAD.length; i++) RAD[i] = exp(-sqrt(i) / 16); +// Kinds of LED in the still picture and how much lantern light each takes. +const SKY = 0, WOOD = 1, EARTH = 2, MOON = 3, TAKE = [0.5, 0.75, 1, 0.1]; + +export function setup(params) { + params.s = { + t: 0, gh: 0.11, speed: 2, wind: 0, size: 0.06, cx: [0, 31, 67], rx: 9, ry: 7, + ph: "hoist", e: 0, side: 1, lc: 0, th: 0, x: PX, y: 0, vy: 0, lit: 1, roll: 0, + hook: { l: 0, a: 0, w: 0 }, + shards: [], seeds: [], smoke: [], splat: null, candle: null, flash: 0, fl: 1, fade: 1, + bg: new Float32Array(N * 3), kind: new Uint8Array(N), buf: new Float32Array(N * 3), c: [0, 0, 0], g: [0, 0, 0], + stars: [], + }; + scene(params.s); +} + +// The still picture: a violet sky paling to mauve at the horizon with the +// moon in it, and the black hoist with a moonlit edge standing on the earth. +function scene(S) { + const bg = S.bg, kd = S.kind, c = S.c; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, o = p * 3, u = y / GY; + hsv(0.74 + 0.08 * u, 0.72 - 0.12 * u, 0.3 + 0.2 * u * u, c); + const dm = sqrt((x - MX) ** 2 + (y - MY) ** 2); + const halo = 0.5 * exp(-max(0, dm - MR) / 7); + bg[o] = c[0] + 70 * halo; + bg[o + 1] = c[1] + 60 * halo; + bg[o + 2] = c[2] + 70 * halo; + if (dm < MR + 0.5) { + // The moon, two grey seas on it set on a slant. + const sea = exp(-((x - MX + 2) ** 2 + (y - MY + 2) ** 2) / 5) + 0.8 * exp(-((x - MX - 3) ** 2 + (y - MY - 3) ** 2) / 4); + const a = cl(MR + 0.5 - dm, 0, 1), m = 1 - 0.18 * sea; + bg[o] += (238 * m - bg[o]) * a; + bg[o + 1] += (228 * m - bg[o + 1]) * a; + bg[o + 2] += (196 * m - bg[o + 2]) * a; + kd[p] = MOON; + } + if (y >= GY) { + // Earth, a little lighter in the top rows, with a speckle. + const k = 1 - (y - GY) * 0.05, n = 0.85 + 0.3 * ((x * 37 + y * 91) % 7) / 7; + bg[o] = 52 * k * n; + bg[o + 1] = 30 * k * n; + bg[o + 2] = 60 * k * n; + kd[p] = EARTH; + } + } + // Grass tufts standing on the ground line, black against the sky. + for (let x = 0; x < W; x++) { + const hgt = 1 + ((x * 7 + 3) % 5 === 0 ? 2 : (x * 13) % 3 === 0 ? 1 : 0); + for (let j = 1; j <= hgt; j++) wood(S, x, GY - j, 0.4); + } + // The post and the beam with its brace, each with a moonlit edge. + for (let y = 8; y < GY; y++) for (let x = 4; x <= 6; x++) wood(S, x, y, x === 6 ? 1 : 0); + for (let x = 2; x <= 44; x++) for (let y = 8; y <= 10; y++) wood(S, x, y, y === 8 ? 1 : 0); + for (let i = 0; i <= 16; i++) for (let j = 0; j < 2; j++) wood(S, 6 + i, 25 - i + j, j); + // The rope down the post to the winch. + for (let y = 11; y < 97; y++) rope(bg, 8, y, 0.8); + for (let i = 0; i < 26; i++) S.stars.push({ x: floor(rnd(0, W)), y: floor(rnd(0, 70)), ph: rnd(0, 7), f: rnd(1, 3), b: rnd(0.4, 1) }); +} + +function wood(S, x, y, edge) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const p = y * W + x, o = p * 3; + S.bg[o] = 16 + 44 * edge; + S.bg[o + 1] = 8 + 30 * edge; + S.bg[o + 2] = 24 + 54 * edge; + S.kind[p] = WOOD; +} + +function rope(buf, x, y, a) { + x = round(x); + y = round(y); + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + buf[o] += (176 - buf[o]) * a; + buf[o + 1] += (140 - buf[o + 1]) * a; + buf[o + 2] += (96 - buf[o + 2]) * a; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.gh = 0.11 + cl(k[0], 0, 1); + S.speed = cl(k[1], 0.2, 3); + S.wind = cl(k[2], 0, 1); + S.size = cl(k[3], 0, 1); + // A pie pumpkin 16 LEDs wide at 0, 32 at 1. + S.rx = 8 + 8 * S.size; + S.ry = 0.78 * S.rx; + const ds = (d * S.speed) / 2, hk = S.hook, RY = S.ry; + const TOP = HOOK + 2 + RY, LOW = GY - RY - 1 - PY; + // The swing, kept small enough that a big pumpkin stays on the panel. + const A = min(0.08 + 0.8 * S.wind, asin(min(1, (W - S.rx - 2 - PX) / TOP))); + S.t += ds; + S.e += ds; + // Three bands of cloud drift right, the lower ones slower. + for (let i = 0; i < 3; i++) S.cx[i] += (2 + 16 * S.wind) * (1 - 0.25 * i) * ds; + // The candle flickers on three beats. + S.fl = 0.8 + 0.1 * sin(S.t * 23) + 0.06 * sin(S.t * 41 + 1) + 0.05 * sin(S.t * 7.3); + if (input.btnPressed && input.btnPressed[0] && S.ph === "hang") S.e = T_HANG; + if (S.ph === "hoist" || S.ph === "hang") { + // A pendulum from the pulley. While it is wound up the swing builds, and + // it is let go at the end of its swing, where it stands still. + const p = S.ph === "hoist" ? sm(S.e / T_HOIST) : 1; + const tt = S.ph === "hoist" ? S.e - T_HOIST : S.e; + S.lc = S.ph === "hoist" ? LOW + (TOP - LOW) * p : TOP; + S.th = S.side * A * p * cos((2 * PI * tt) / SWING); + S.x = PX + S.lc * sin(S.th); + S.y = PY + S.lc * cos(S.th); + hk.l = S.lc - RY - 2; + hk.a = S.th; + if (S.ph === "hoist" && S.e >= T_HOIST) (S.ph = "hang"), (S.e = 0); + else if (S.ph === "hang" && S.e >= T_HANG) { + S.ph = "fall"; + S.e = 0; + S.vy = 0; + hk.w = 0; + } + } else if (S.ph === "fall") { + S.vy += G * ds; + S.y += S.vy * ds; + S.th *= exp(-1.5 * ds); + if (S.y + RY >= GY - 0.5) smash(S); + } else if (S.ph === "smash") { + if (S.e >= T_SMASH) (S.ph = "lower"), (S.e = 0); + } else if (S.ph === "lower") { + // The hook is let down to the ground while the mess fades. + const p = sm(S.e / T_LOWER); + hk.l = HOOK + (LOW - TOP) * p; + S.fade = 1 - p; + if (S.e >= T_LOWER) { + S.ph = "roll"; + S.e = 0; + S.shards.length = S.seeds.length = 0; + S.splat = null; + S.fade = 1; + S.lit = 0; + } + } else if (S.ph === "roll") { + // The next pumpkin spins in from the right and turns its face to us. + const p = S.e / T_ROLL, q = 1 - (1 - p) * (1 - p); + S.x = PX + 36 * (1 - q); + S.y = PY + LOW; + S.roll = (S.x - PX) / S.rx; + S.th = 0; + if (S.e >= T_ROLL) (S.ph = "light"), (S.e = 0), (S.roll = 0); + } else if (S.ph === "light") { + S.lit = S.e < 0.12 ? 1.6 * (S.e / 0.12) : 1 + 0.6 * exp(-(S.e - 0.12) * 8); + if (S.e >= T_LIGHT) { + S.ph = "hoist"; + S.e = 0; + S.lit = 1; + S.side = -S.side; + } + } + // The empty hook swings back under the pulley. + if (S.ph !== "hoist" && S.ph !== "hang") { + hk.w += (-30 * hk.a - 2.2 * hk.w) * ds; + hk.a += hk.w * ds; + } + physics(S, ds); +} + +// The pumpkin hits the ground and everything in it is thrown out at once. +function smash(S) { + const RX = S.rx, RY = S.ry, b = S.size; + S.ph = "smash"; + S.e = 0; + S.y = GY - RY - 0.5; + S.flash = 1; + const n = round(3 * RX), x0 = cl(S.x, 6, W - 7); + S.x = x0; + for (let i = 0; i < n; i++) { + const a = rnd(-PI + 0.25, -0.25), sp = rnd(40, 95) * (0.9 + 0.4 * b), r = rnd(1.3, 3) * RX / 9; + S.shards.push({ x: x0 + rnd(-RX, RX) * 0.8, y: S.y + rnd(-RY, RY) * 0.6, vx: cos(a) * sp * 0.8, vy: sin(a) * sp, a: rnd(0, 7), w: rnd(-12, 12), r, k: Math.random() < 0.3 ? 1 : 0, rest: 0 }); + } + for (let i = 0; i < 14 + 26 * b; i++) { + const a = rnd(-PI * 0.95, -PI * 0.05), sp = rnd(30, 85) * (0.8 + 0.5 * b); + S.seeds.push({ x: x0 + rnd(-4, 4), y: S.y, vx: cos(a) * sp, vy: sin(a) * sp, rest: 0, p: Math.random() < 0.35 }); + } + S.splat = { x: x0, w: 0, s: 6 + 8 * b + 4 * Math.random() }; + const dir = Math.random() < 0.5 ? -1 : 1; + S.candle = { x: x0, y: S.y, vx: dir * rnd(16, 26), vy: -rnd(55, 70), a: 0, w: dir * rnd(8, 14), f: 1, out: rnd(1.3, 1.6), t: 0, puff: false }; +} + +function physics(S, ds) { + S.flash *= exp(-ds * 9); + for (const s of S.shards) { + if (s.rest) continue; + s.vy += G * ds; + s.x += s.vx * ds; + s.y += s.vy * ds; + s.a += s.w * ds; + if (s.x < 0.5 || s.x > W - 1.5) (s.vx = -0.5 * s.vx), (s.x = cl(s.x, 0.5, W - 1.5)); + if (s.y > GY - 0.5 && s.vy > 0) { + s.y = GY - 0.5; + s.vy *= -0.42; + s.vx *= 0.55; + s.w *= 0.5; + if (abs(s.vy) < 12) (s.rest = 1), (s.y = GY - 0.5 + rnd(0, 1.2)); + } + } + for (const s of S.seeds) { + if (s.rest) continue; + s.vy += G * ds; + s.x += s.vx * ds; + s.y += s.vy * ds; + if (s.y > GY && s.vy > 0) { + s.y = GY; + s.vy *= -0.45; + s.vx *= 0.6; + if (abs(s.vy) < 10) (s.rest = 1), (s.y = GY + floor(rnd(0, 3))); + } + } + if (S.splat) S.splat.w = min(1, S.splat.w + ds * 6); + const cd = S.candle; + if (cd) { + cd.t += ds; + cd.vy += G * ds; + cd.x += cd.vx * ds; + cd.y += cd.vy * ds; + cd.a += cd.w * ds; + if (cd.x < 2 || cd.x > W - 3) (cd.vx = -0.6 * cd.vx), (cd.x = cl(cd.x, 2, W - 3)); + if (cd.y > GY - 1 && cd.vy > 0) { + // On the ground it lies down and rolls to a stop. + cd.y = GY - 1; + cd.vy *= -0.3; + cd.vx *= 0.7; + cd.w = 0; + cd.a += (PI / 2 - (cd.a % PI)) * 0.4; + if (abs(cd.vy) < 8) cd.vy = 0; + } + if (cd.vy === 0) cd.vx *= exp(-4 * ds); + // It sputters: the flame gutters and goes out. + const left = cd.out - cd.t; + cd.f = left > 0 ? cl(left / cd.out + 0.3, 0, 1) * (left < 0.6 ? (sin(cd.t * 61) > 0.1 ? 1 : 0.25) : 1) : 0; + if (left <= 0 && !cd.puff) { + cd.puff = true; + const w = wick(cd); + for (let i = 0; i < 22; i++) S.smoke.push({ x: w[0] + rnd(-0.5, 0.5), y: w[1] - rnd(0, 2), vx: rnd(-5, 5), vy: -rnd(10, 26), r: rnd(0.8, 1.4), t: 0, life: rnd(1.8, 2.8) }); + } + if (S.ph === "roll") S.candle = null; + } + let n = 0; + for (const m of S.smoke) { + m.t += ds; + m.x += (m.vx + 3 * sin(m.t * 3 + m.life * 5) + 12 * S.wind * min(1, m.t)) * ds; + m.y += m.vy * ds; + m.vy *= exp(-0.6 * ds); + m.r = min(3.6, m.r + ds * 1.6); + if (m.t < m.life) S.smoke[n++] = m; + } + S.smoke.length = n; +} + +// Where the candle's wick is: the top of the stick, which tumbles. +function wick(cd) { + return [cd.x + 2.5 * sin(cd.a), cd.y - 2.5 * cos(cd.a)]; +} + +// Add a colour at a pixel by alpha. +function put(buf, x, y, r, g, b, a) { + x = round(x); + y = round(y); + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + buf[o] += (r - buf[o]) * a; + buf[o + 1] += (g - buf[o + 1]) * a; + buf[o + 2] += (b - buf[o + 2]) * a; +} + +// One LED of the pumpkin at local (u, v) from its middle, turned by its +// spin: false outside it. Ribs are meridians, so they crowd at the edges; +// the face is painted on the front and turns away as it spins. +function pumpkinAt(S, u, v, out) { + const RX = S.rx, RY = S.ry, k = 9 / RX, vv = v / RY; + if (vv < -1 || vv > 1) return false; + const half = RX * sqrt(1 - vv * vv) * (vv < 0 ? 1 : 1 - 0.08 * vv); + if (abs(u) > half + 0.35) return false; + // A dip at the top round the stem. + if (vv < -0.8 && abs(u) < 2 && v < -RY + 0.9) return false; + const n = cl(u / max(half, 0.5), -1, 1), lon = asin(n) - S.roll; + const rib = abs(((lon / 0.628 + 50.5) % 1) - 0.5) < 0.13; + const nz = sqrt(max(0, 1 - n * n)) * sqrt(max(0, 1 - vv * vv)); + const lightv = 0.82 + 0.18 * cl(0.3 * n - 0.5 * vv + 0.5 * nz, 0, 1); + const glow = S.lit * S.fl; + let hole = false; + if (cos(lon) > 0.2) hole = face(9 * sin(lon), v * k); + if (hole) { + if (glow > 0.05) { + hsv(S.gh + 0.03, 0.62 - 0.4 * cl(glow - 0.7, 0, 1), min(1, 0.5 + 0.5 * glow), out); + } else (out[0] = 46), (out[1] = 16), (out[2] = 6); + return true; + } + // Skin: bright saturated orange, the ribs a redder line. + hsv(rib ? 0.05 : 0.07, 1, lightv * (rib ? 0.86 : 0.97), out); + return true; +} + +// The carving, in LEDs from the middle of a pumpkin 18 wide: triangle eyes +// over a small nose, then a grin with two teeth above and one below. +function face(u, v) { + const au = abs(u); + if (v >= -4 && v <= -1.5) { + const w = (v + 4.3) * 0.75; + if (abs(au - 4.2) < w) return true; + } + if (v >= -0.5 && v <= 0.6 && au < 0.6 + (v + 0.5) * 0.9) return true; + if (v > 1.5 && v < 4.5) { + const top = 2.5 - 0.035 * au * au, bot = 4.2 - 0.05 * au * au; + if (au < 6.4 && v > top && v < bot) { + if (v < top + 1.2 && (abs(u + 2.5) < 0.6 || abs(u - 2.5) < 0.6)) return false; + if (v > bot - 1.1 && au < 0.6) return false; + return true; + } + } + return false; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const bg = S.bg, buf = S.buf, kd = S.kind, gc = hsv(S.gh, 0.85, 1, S.g), c = S.c, hk = S.hook; + // Lantern light: from the pumpkin while it is whole and lit, then from the + // candle out on the ground; the burst adds a flash. + const whole = S.ph !== "smash" && S.ph !== "lower"; + const L1 = whole ? (0.9 + 0.5 * S.size) * S.lit * S.fl : 0, q1 = (8.5 / S.rx) ** 2, cd = S.candle, RX = S.rx, RY = S.ry; + const L2 = cd ? 0.7 * cd.f * S.fl : 0, wk = cd ? wick(cd) : [0, 0]; + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const o = p * 3; + let l = 0; + if (L1 > 0) l += L1 * RAD[min(20479, round(((x - S.x) ** 2 + (y - S.y) ** 2) * q1))]; + if (L2 > 0) l += L2 * RAD[min(20479, round((x - wk[0]) ** 2 + (y - wk[1]) ** 2 * 1.5))]; + if (S.flash > 0.01) l += 1.6 * S.flash * RAD[min(20479, round(((x - S.x) ** 2 + (y - GY) ** 2) * 0.4))]; + const t = l * TAKE[kd[p]]; + let r = bg[o], g = bg[o + 1], b = bg[o + 2]; + if (y > 6 && y < 78 && kd[p] !== WOOD) { + // Cloud: wisps of value noise in three wavy bands, lilac grey. + let a = 0; + for (let i = 0; i < 3; i++) { + const dy = (y - CB[i] - 3 * vn((x - S.cx[i]) * 0.03, i * 31)) / (3.5 + 1.5 * i); + if (dy * dy > 4) continue; + const u = x - S.cx[i], n = vn(u * 0.045 + y * 0.02, i * 101) + 0.6 * vn(u * 0.12 - y * 0.05, i * 101 + 7) + 0.3 * vn(u * 0.25 + y * 0.1, i * 101 + 13); + a += cl((n - 0.05) * 2.2, 0, 1) * exp(-dy * dy) * 0.75; + } + if (a > 0) { + a = min(0.8, a); + r += (142 - r) * a; + g += (116 - g) * a; + b += (178 - b) * a; + } + } + buf[o] = r + gc[0] * t; + buf[o + 1] = g + gc[1] * t; + buf[o + 2] = b + gc[2] * t; + } + // Stars twinkle. + for (const s of S.stars) { + const b = s.b * (0.55 + 0.45 * sin(S.t * s.f + s.ph)); + put(buf, s.x, s.y, 235, 220, 255, 0.85 * b); + } + // The winch on the post turns as the rope runs. + const wa = hk.l / 3; + for (let i = 0; i < 24; i++) { + const a = (i / 24) * 2 * PI; + put(buf, 9 + 3.5 * cos(a), 100 + 3.5 * sin(a), 120, 90, 60, 1); + } + for (let i = 0; i < 4; i++) { + const a = wa + (i * PI) / 2; + for (let r = 0; r <= 3; r++) put(buf, 9 + r * cos(a), 100 + r * sin(a), 150, 112, 70, 1); + } + // The pulley under the beam. + for (let i = 0; i < 16; i++) { + const a = (i / 16) * 2 * PI; + put(buf, PX + 2.2 * cos(a), PY + 2.2 * sin(a), 120, 110, 130, 1); + } + put(buf, PX, PY, 200, 190, 210, 1); + // The rope and the hook: on the pumpkin while it hangs, empty after. + const ha = hk.a, hl = hk.l; + for (let i = 2; i <= hl; i += 0.7) rope(buf, PX + i * sin(ha), PY + i * cos(ha), 0.9); + const hx = PX + hl * sin(ha), hy = PY + hl * cos(ha); + put(buf, hx, hy + 1, 170, 170, 190, 1); + put(buf, hx + 1, hy + 2, 170, 170, 190, 1); + put(buf, hx, hy + 2.6, 140, 140, 160, 1); + // The pumpkin, turned by its swing. + if (whole) { + const cs = cos(S.th), sn = sin(S.th); + // The flame streaks up out of it as it falls. + if (S.ph === "fall") { + const len = min(20, S.vy / 6); + for (let j = 0; j < len; j++) { + const f = 1 - j / len; + hsv(S.gh + 0.04 - 0.08 * (1 - f), 0.1 + 0.8 * (1 - f), 1, c); + put(buf, S.x, S.y - RY - 1 - j, c[0], c[1], c[2], S.fl * min(1, 1.5 * f)); + hsv(S.gh - 0.03, 0.85, 1, c); + for (let i = -1; i <= 1; i += 2) if (j < len * 0.7) put(buf, S.x + i, S.y - RY - 1 - j, c[0], c[1], c[2], 0.75 * f * S.fl); + } + } + for (let dy = -ceil(RY) - 3; dy <= RY + 1; dy++) + for (let dx = -ceil(RX) - 2; dx <= RX + 2; dx++) { + const u = dx * cs + dy * sn, v = -dx * sn + dy * cs; + if (pumpkinAt(S, u, v, c)) put(buf, S.x + dx, S.y + dy, c[0], c[1], c[2], 1); + else if (v < -RY + 1 && v > -RY - 2.5 && abs(u) < 1.1) put(buf, S.x + dx, S.y + dy, 70, 120, 40, 1); + } + } + // The burst: pulp splashed on the ground, with the shards and seeds over it. + const fa = S.fade; + if (S.splat) { + const sp = S.splat; + for (let i = -sp.s - 3; i <= sp.s + 3; i++) { + const w = sp.s * sp.w, r = abs(i) / max(1, w), j = (i * 7919 + 13) % 5; + if (r > 1) continue; + const hgt = 1 + (r < 0.6 ? 1 : 0) + (j === 0 ? 1 : 0); + for (let k = 0; k < hgt; k++) put(buf, sp.x + i, GY - k + (k ? 0 : 0), 255, 150 + 20 * j, 30, fa * (1 - 0.3 * k)); + } + } + for (const s of S.shards) { + const ca = cos(s.a), sa = sin(s.a), r = s.r; + for (let dy = -3; dy <= 3; dy++) + for (let dx = -3; dx <= 3; dx++) { + // A rough triangle of skin, or of yellow flesh. + const u = dx * ca + dy * sa, v = -dx * sa + dy * ca; + if (v < -r * 0.5 || v > r * 0.6 || abs(u) > r - (v + r * 0.5) * 0.9) continue; + if (s.k) put(buf, s.x + dx, s.y + dy, 255, 196, 60, fa); + else put(buf, s.x + dx, s.y + dy, 255, v < 0 ? 112 : 96, 0, fa); + } + } + for (const s of S.seeds) { + put(buf, s.x, s.y, 250, 238, 190, fa); + if (s.p && !s.rest) put(buf, s.x - s.vx * 0.02, s.y - s.vy * 0.02, 255, 170, 40, fa * 0.8); + } + if (cd) { + // The candle, a stub of wax two LEDs thick, and its flame, which stands + // up whichever way the stub lies. + const ca = cos(cd.a), sa = sin(cd.a); + for (let j = -2; j <= 2; j += 0.5) + for (let i = 0; i <= 1; i++) put(buf, cd.x + j * sa + i * ca, cd.y - j * ca + i * sa, 240, 232, 212, fa); + if (cd.f > 0.02) { + const w = wick(cd), fl = cd.f * S.fl; + hsv(S.gh + 0.04, 0.15, 1, c); + put(buf, w[0], w[1] - 1, c[0], c[1], c[2], min(1, fl * 1.4)); + put(buf, w[0] + 1, w[1] - 1, c[0], c[1], c[2], min(1, fl * 0.8)); + hsv(S.gh, 0.8, 1, c); + put(buf, w[0], w[1] - 2, c[0], c[1], c[2], min(1, fl * 1.2)); + put(buf, w[0] + 1, w[1] - 2, c[0], c[1], c[2], fl * 0.6); + if (fl > 0.5) put(buf, w[0] + 0.5 + 0.6 * sin(S.t * 13), w[1] - 3, c[0], c[1], c[2], fl - 0.3); + } + } + for (const m of S.smoke) { + const a = 0.5 * (1 - m.t / m.life) * min(1, m.t * 8); + const r = m.r; + for (let dy = -4; dy <= 4; dy++) + for (let dx = -4; dx <= 4; dx++) { + const q = dx * dx + dy * dy; + if (q <= r * r + 0.3) put(buf, m.x + dx, m.y + dy, 180, 170, 196, a * (1 - q / (r * r + 1.5))); + } + } + // Out in steps of four. + for (let y = 0, o = 0; y < H; y++) + for (let x = 0; x < W; x++, o += 3) + display.setPixel(x, y, min(255, ((buf[o] + 2) >> 2) << 2), min(255, ((buf[o + 1] + 2) >> 2) << 2), min(255, ((buf[o + 2] + 2) >> 2) << 2)); +} From 6289b6b8f7bbf199e6e9d577a622264170ee4876 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 00:09:45 +0000 Subject: [PATCH 04/10] =?UTF-8?q?52=ED=8C=90:=20Anscombe's=20Quartet=20?= =?UTF-8?q?=EC=99=80=20Specimen=20Shelf?= 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_01LXsMxtcgxEaHMjeqmCbsjv --- .../patterns/anscombes-quartet.js | 487 +++++++++++++++++ .../pattern-study/patterns/specimen-shelf.js | 488 ++++++++++++++++++ 2 files changed, 975 insertions(+) create mode 100644 incoming/pattern-study/patterns/anscombes-quartet.js create mode 100644 incoming/pattern-study/patterns/specimen-shelf.js diff --git a/incoming/pattern-study/patterns/anscombes-quartet.js b/incoming/pattern-study/patterns/anscombes-quartet.js new file mode 100644 index 000000000..384d43883 --- /dev/null +++ b/incoming/pattern-study/patterns/anscombes-quartet.js @@ -0,0 +1,487 @@ +// ===== Patternflow pattern ===== +// Title: Anscombe's Quartet +// Family: data +// Idea: Anscombe's four scatter plots of eleven points stand stacked under one shared regression line, then fold into a single tall plot where their points split into a cloud of 264 that anneals from shape to shape while all five of its statistics stay the same to two decimals, so the line and the crosshair never move. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Anscombe's quartet (Francis Anscombe, 1973); same-stats annealing (Justin Matejka, George Fitzmaurice, 2017) +// Round: 52 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Heat=0..1, Residuals=0..1 +// +// Knob 1: Hue. Turns the four sets' colours round the wheel together: at 0 set I is sea green, II blue, III pink and IV yellow, and the cloud keeps each point's colour. The faint washes above and below the line stay on the cool side, teal over violet at 0 and magenta over green at 1. +// Knob 2: Speed. At 2 the quartet stands 1 s and folds in 0.8 s, each of the eight shapes takes 3.6 s (the cloud settles in about 1.5 s, and the round ones turn a quarter radian a second), the cloud walks home in 3 s and the quartet stands 4 s: one round is 37.4 s. The annealer tries 20,000 nudges a second through the first second of a shape and 100,000 a second from the second second on. At 0.2 everything runs ten times slower, at 3 half as fast again. +// Knob 3: Heat. The annealing temperature: the chance that a nudge taking a point away from its place is kept anyway, still only when the statistics hold. 0.2% at 0, with 5% that cools over the first second of each shape; 60% at 1, where the nudges are also 2.5 times longer and the shapes fray into a haze round their outlines while the numbers stay put. +// Knob 4: Residuals. A stick from each point straight up or down to the line, in the point's colour, brightest at the point. Too faint to see below about 0.1; at 1 the sticks fill the space between the cloud and the line, and their squares add up to about the same total in every shape, since r and the spread of y are held. + +const W = 64, H = 128; +const { floor, round, min, max, abs, sqrt, exp, cos, sin, hypot, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (v) => (v <= 0 ? 0 : v >= 1 ? 1 : v * v * (3 - 2 * 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]; +} + +// Anscombe's published sets (1973). The first three share x. +const AX = [10, 8, 13, 9, 11, 14, 6, 4, 12, 7, 5]; +const SETS = [ + [AX, [8.04, 6.95, 7.58, 8.81, 8.33, 9.96, 7.24, 4.26, 10.84, 4.82, 5.68]], + [AX, [9.14, 8.14, 8.74, 8.77, 9.26, 8.1, 6.13, 3.1, 9.13, 7.26, 4.74]], + [AX, [7.46, 6.77, 12.74, 7.11, 7.81, 8.84, 6.08, 5.39, 8.15, 6.42, 5.73]], + [[8, 8, 8, 8, 8, 8, 8, 19, 8, 8, 8], [6.58, 5.76, 7.71, 8.84, 8.47, 7.04, 5.25, 12.5, 5.56, 7.91, 6.89]], +]; +// What all four share: means 9 and 7.50, deviations 3.32 and 2.03, +// correlation 0.816, and the line y = 3 + 0.5 x. +const MX = 9, MY = 7.5, SDX = sqrt(11), SDY = 2.0316, RHO = 0.8164; + +// Two views of the data. The quartet: four plots 32 rows tall, x from 3 to +// 20 across the panel. The tall plot: one plot over all 128 rows, scaled so +// that the outliers at x 19 and y 12.74 still land on the panel. +const QKX = 3.47, QX0 = 2 + 6 * QKX, QKY = 2.64; +const qY0 = (k) => 32 * k + 30 - (MY - 2.5) * QKY; +const KX = 3.4, KY = 12, X0 = 27, Y0 = 67; + +// Each of the 44 points splits into C copies for the cloud. +const C = 6, NP = 44, N = NP * C; + +// The cloud's shapes, drawn in a design square and then fitted so that their +// own means and covariance are the cloud's: a shape can only be one the +// statistics allow. Lines stay upright ("L") or level ("U") under the fit. +function seg(o, a, b, c, d) { + const n = max(2, round(hypot(c - a, d - b) / 0.02)); + for (let i = 0; i <= n; i++) o.push([a + ((c - a) * i) / n, b + ((d - b) * i) / n]); +} +function circ(o, r) { + const n = max(8, round((2 * PI * r) / 0.02)); + for (let i = 0; i < n; i++) o.push([r * cos((2 * PI * i) / n), r * sin((2 * PI * i) / n)]); +} +function dot(o, a, b, n) { + for (let i = 0; i < n; i++) o.push([a + 0.03 * cos(i), b + 0.03 * sin(i)]); +} +function star(o) { + const p = []; + for (let i = 0; i < 10; i++) { + const a = -PI / 2 + (i * PI) / 5, r = i % 2 ? 0.45 : 1.1; + p.push([r * cos(a), r * sin(a)]); + } + for (let i = 0; i < 10; i++) seg(o, p[i][0], p[i][1], p[(i + 1) % 10][0], p[(i + 1) % 10][1]); +} +function lattice(o) { + for (let b = -1.2; b <= 1.21; b += 0.4) for (let a = -1.2; a <= 1.21; a += 0.4) if (a * a + b * b < 1.5) dot(o, a, b, 12); +} +// "R" shapes are round and carry no correlation of their own: they keep +// their roundness on the panel and the statistics put a clump of points out +// at each end of the line to carry r = 0.82, as Anscombe's outliers do. +// "L" shapes are fitted whole: upright lines stay upright. "U": level stays level. +const SHAPES = [ + ["R", circ1], + ["L", (o) => { for (const v of [-1.2, -0.4, 0.4, 1.2]) seg(o, -1.6, v, 1.6, v); }], + ["R", star], + ["L", (o) => { for (const u of [-1.5, -0.75, 0, 0.75, 1.5]) seg(o, u, -1.3, u, 1.3); }], + ["R", (o) => (circ(o, 1), circ(o, 0.5))], + ["U", (o) => { for (const v of [-1.5, -0.75, 0, 0.75, 1.5]) seg(o, -1.3, v, 1.3, v); }], + ["R", lattice], + ["R", (o) => (seg(o, -1, -1, 1, 1), seg(o, -1, 1, 1, -1))], +]; +function circ1(o) { + circ(o, 1); +} + +// Fit points to the cloud's means and covariance (sx, sy, sxy in LEDs). +function fit(pts, ref, mode, sx, sy, sxy) { + let ma = 0, mb = 0, aa = 0, bb = 0, ab = 0; + for (const p of ref) (ma += p[0]), (mb += p[1]); + ma /= ref.length; + mb /= ref.length; + for (const p of ref) { + const a = p[0] - ma, b = p[1] - mb; + aa += a * a; + bb += b * b; + ab += a * b; + } + aa /= ref.length; + bb /= ref.length; + ab /= ref.length; + let M; + if (mode !== "U") { + // T times the inverse of S, both lower triangular: x depends on x alone. + const s11 = sqrt(aa), s21 = ab / s11, s22 = sqrt(bb - s21 * s21); + const t11 = sx, t21 = sxy / sx, t22 = sqrt(sy * sy - t21 * t21); + M = [t11 / s11, 0, t21 / s11 - (t22 * s21) / (s11 * s22), t22 / s22]; + } else { + // Both upper triangular: y depends on y alone, so level lines stay level. + const s22 = sqrt(bb), s12 = ab / s22, s11 = sqrt(aa - s12 * s12); + const t22 = sy, t12 = sxy / sy, t11 = sqrt(sx * sx - t12 * t12); + M = [t11 / s11, t12 / s22 - (t11 * s12) / (s11 * s22), 0, t22 / s22]; + } + return pts.map((p) => { + const a = p[0] - ma, b = p[1] - mb; + return [X0 + M[0] * a + M[1] * b, Y0 + M[2] * a + M[3] * b]; + }); +} + +// Distance from each LED centre to the nearest point of a shape, filled a +// few rows at a time so that no frame stalls on it. +function fillRows(F, rows) { + const { f, xs, ys } = F, n = xs.length, end = min(H, F.row + rows); + for (let y = F.row; y < end; y++) + for (let x = 0; x < W; x++) { + let best = 1e9; + const px = x + 0.5, py = y + 0.5; + for (let i = 0; i < n; i++) { + const dx = xs[i] - px, dy = ys[i] - py, d = dx * dx + dy * dy; + if (d < best) best = d; + } + f[y * W + x] = sqrt(best); + } + F.row = end; +} +function look(f, x, y) { + const fx = cl(x - 0.5, 0, W - 1.001), fy = cl(y - 0.5, 0, H - 1.001); + const ix = floor(fx), iy = floor(fy), u = fx - ix, v = fy - iy, i = iy * W + ix; + return (f[i] * (1 - u) + f[i + 1] * u) * (1 - v) + (f[i + W] * (1 - u) + f[i + W + 1] * u) * v; +} + +export function setup(params) { + // The cloud: copy c of point j of set k, spread round the means by a hair + // so that its deviations divided by n - 1 are Anscombe's again. + let sxx = 0; + for (const [xs] of SETS) for (const x of xs) sxx += (x - MX) ** 2; + const grow = sqrt(((N - 1) * 11) / (C * sxx)); + const hx = new Float64Array(N), hy = new Float64Array(N), set = new Uint8Array(N), idx = new Uint8Array(N); + for (let n = 0; n < N; n++) { + const k = floor(n / (11 * C)), j = floor(n / C) % 11; + set[n] = k; + idx[n] = j; + const [xs, ys] = SETS[k]; + hx[n] = X0 + KX * (xs[j] - MX) * grow; + hy[n] = Y0 - KY * (ys[j] - MY) * grow; + } + params.s = { + t: 0, hue: 0, speed: 2, heat: 0, res: 0.06, fade: 0, + hx, hy, set, idx, x: Float64Array.from(hx), y: Float64Array.from(hy), d: new Float64Array(N), + fields: [], pts: [], cores: [], cur: -1, bx: null, by: null, core: new Uint8Array(N), tx: new Float64Array(N), ty: new Float64Array(N), lock: null, stats: [0, 0, 0, 0, 0], + rgb: new Float32Array(W * H * 3), trail: new Float32Array(W * H * 3), + }; +} + +// Pixel sums of the cloud, and its statistics in Anscombe's units, rounded +// to two decimals as whole hundredths. +function sums(S) { + let a = 0, b = 0, aa = 0, bb = 0, ab = 0; + for (let n = 0; n < N; n++) { + const x = S.x[n] - X0, y = S.y[n] - Y0; + a += x; + b += y; + aa += x * x; + bb += y * y; + ab += x * y; + } + return [a, b, aa, bb, ab]; +} +function stats(a, b, aa, bb, ab, out) { + const vx = (aa - (a * a) / N) / (N - 1), vy = (bb - (b * b) / N) / (N - 1), c = (ab - (a * b) / N) / (N - 1); + out[0] = round(100 * (MX + a / N / KX)); + out[1] = round(100 * (MY - b / N / KY)); + out[2] = round((100 * sqrt(vx)) / KX); + out[3] = round((100 * sqrt(vy)) / KY); + out[4] = round((-100 * c) / sqrt(vx * vy)); + return out; +} + +function shapeField(S, k, rows = H) { + if (!S.fields[k]) { + // The shape's points carry the cloud's spread divided by n, so a cloud + // lying on it has Anscombe's spread divided by n - 1. Pixel rows run + // down, so the covariance turns negative. + const f = sqrt((N - 1) / N), sx = KX * SDX * f, sy = KY * SDY * f, sxy = -RHO * sx * sy; + const [mode, build] = SHAPES[k]; + let raw = []; + build(raw); + const coreLen = raw.length; + if (mode === "R") { + // A fraction 2w of the points sits in the two clumps at (+-cx, -+cy): + // 2w cx cy carries the covariance, and what the clumps leave of the + // x and y spread, equal, is the round core's. + const w2 = 0.24, A = sx * sx, D = sy * sy, B = -sxy / w2, E = (D - A) / w2; + const cx = sqrt((-E + sqrt(E * E + 4 * B * B)) / 2), cy = B / cx; + let v = 0; + for (const p of raw) v += p[0] * p[0] + p[1] * p[1]; + const lam = sqrt((A - w2 * cx * cx) / (1 - w2) / (v / raw.length / 2)); + raw = raw.map((p) => [lam * p[0], lam * p[1]]); + const m = round(((raw.length * w2) / (1 - w2)) / 2); + for (const sg of [1, -1]) + for (let i = 0; i < m; i++) { + const a = i * 2.4, r = 2.2 * sqrt((i + 0.5) / m); + raw.push([sg * cx + r * cos(a), -sg * cy + r * sin(a)]); + } + } + // One place on the shape for each point, picked evenly along it. The fit + // is taken from these places, so a cloud sitting on them has the + // statistics exactly. + const spots = [], core = new Uint8Array(N); + for (let n = 0; n < N; n++) { + const i = floor(((n + 0.5) * raw.length) / N); + spots.push(raw[i]); + core[n] = mode === "R" && i < coreLen ? 1 : 0; + } + const pts = fit(raw, spots, mode, sx, sy, sxy); + S.pts[k] = fit(spots, spots, mode, sx, sy, sxy); + S.cores[k] = core; + const half = pts.filter((_, i) => i % 2 === 0); + S.fields[k] = { f: new Float32Array(W * H), xs: Float32Array.from(half, (p) => p[0]), ys: Float32Array.from(half, (p) => p[1]), row: 0 }; + } + const F = S.fields[k]; + if (F.row < H) fillRows(F, rows); + return F.f; +} + +// Hand the places out: start in any order, then swap pairs while a swap +// shortens the paths. +function assign(S, pts, core) { + const tx = S.tx, ty = S.ty, X = S.x, Y = S.y; + for (let n = 0; n < N; n++) (tx[n] = pts[n][0]), (ty[n] = pts[n][1]); + const own = new Uint16Array(N).map((_, i) => i); + for (let t = 0; t < 40000; t++) { + const a = floor(Math.random() * N), b = floor(Math.random() * N), oa = own[a], ob = own[b]; + const xa = X[a], ya = Y[a], xb = X[b], yb = Y[b]; + const keep = (xa - tx[oa]) ** 2 + (ya - ty[oa]) ** 2 + (xb - tx[ob]) ** 2 + (yb - ty[ob]) ** 2; + const swap = (xa - tx[ob]) ** 2 + (ya - ty[ob]) ** 2 + (xb - tx[oa]) ** 2 + (yb - ty[oa]) ** 2; + if (swap < keep) (own[a] = ob), (own[b] = oa); + } + S.bx = Float64Array.from(own, (m) => tx[m]); + S.by = Float64Array.from(own, (m) => ty[m]); + S.core = Uint8Array.from(own, (m) => core[m]); + S.tx.set(S.bx); + S.ty.set(S.by); +} + +// Matejka and Fitzmaurice: nudge one point, keep the nudge only if all five +// statistics still round to the same two decimals, and prefer nudges that +// bring the point nearer the target. A hot step takes a worse nudge too. +// The target here is the shape's outline plus twice the distance to the +// point's own place on it: the outline alone left a fifth of the points +// stranded in a clump or on the wrong line, since only a hot step could +// carry them across a gap. +function anneal(S, tries, temp, f, rc, rs) { + let [a, b, aa, bb, ab] = sums(S); + const st = [0, 0, 0, 0, 0], L = S.lock, home = !f, step = 1.2 * (1 + 1.5 * S.heat); + const tx = home ? S.hx : S.tx, ty = home ? S.hy : S.ty, be = home ? 1 : BETA; + // A turning shape is looked up turned back, near its middle. + const at = (x, y) => { + const dx = x - X0, dy = y - Y0; + return rs && dx * dx + dy * dy < 900 ? look(f, X0 + rc * dx + rs * dy, Y0 - rs * dx + rc * dy) : look(f, x, y); + }; + const dist = (n, x, y) => (home ? 0 : at(x, y)) + be * sqrt((x - tx[n]) ** 2 + (y - ty[n]) ** 2); + for (let n = 0; n < N; n++) S.d[n] = dist(n, S.x[n], S.y[n]); + for (let t = 0; t < tries; t++) { + const n = floor(Math.random() * N); + const ox = S.x[n], oy = S.y[n]; + const nx = ox + (Math.random() + Math.random() - 1) * step, ny = oy + (Math.random() + Math.random() - 1) * step; + if (nx < 0 || nx >= W || ny < 0 || ny >= H) continue; + const nd = dist(n, nx, ny); + if (nd >= S.d[n] && Math.random() >= temp) continue; + const x0 = ox - X0, y0 = oy - Y0, x1 = nx - X0, y1 = ny - Y0; + const a2 = a - x0 + x1, b2 = b - y0 + y1, aa2 = aa - x0 * x0 + x1 * x1, bb2 = bb - y0 * y0 + y1 * y1, ab2 = ab - x0 * y0 + x1 * y1; + stats(a2, b2, aa2, bb2, ab2, st); + if (st[0] !== L[0] || st[1] !== L[1] || st[2] !== L[2] || st[3] !== L[3] || st[4] !== L[4]) continue; + S.x[n] = nx; + S.y[n] = ny; + S.d[n] = nd; + a = a2; + b = b2; + aa = aa2; + bb = bb2; + ab = ab2; + } +} + +// The story, in seconds at Speed 2: the quartet 1 s, folding into one plot +// 0.8 s, eight shapes of 3.6 s, back home 3 s, unfolding 0.8 s, the quartet +// 4 s. Nudges tried a second, the weight on a point's own place, the round +// shapes' turn in radians a second, and the heat that cools at each shape. +const TR = 20000, BETA = 2, SPIN = 0.25, TEMP0 = 0.05, TAU = 1; +const QFIRST = 1, FOLD = 0.8, SHAPE = 3.6, NSH = 8, BACK = 3, QHOLD = 4; +const CYCLE = FOLD + NSH * SHAPE + BACK + FOLD + QHOLD; +function phase(t) { + if (t < QFIRST) return { q: 1, next: 0 }; + let u = (t - QFIRST) % CYCLE; + const cyc = floor((t - QFIRST) / CYCLE); + if (u < FOLD) return { fold: sm(u / FOLD), next: (cyc * NSH) % SHAPES.length }; + u -= FOLD; + if (u < NSH * SHAPE) return { shape: (cyc * NSH + floor(u / SHAPE)) % SHAPES.length, into: u % SHAPE }; + u -= NSH * SHAPE; + const next = ((cyc + 1) * NSH) % SHAPES.length; + if (u < BACK) return { back: u, next }; + u -= BACK; + if (u < FOLD) return { fold: 1 - sm(u / FOLD), next }; + return { q: 1, next }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.heat = cl(k[2], 0, 1); + S.res = cl(k[3], 0, 1); + const g = (d * S.speed) / 2; + S.t += g; + S.fade = exp(-g * 9); + const P = (S.P = phase(S.t)); + if (!S.lock) S.lock = stats(...sums(S), [0, 0, 0, 0, 0]); + const into = P.shape !== undefined ? P.into : P.back !== undefined ? P.back : 0; + const rate = TR * (1 + 4 * sm(into - 1)); + const tries = min(13000, round(g * rate)); + // Work on the next shape's distances a few rows a frame. + shapeField(S, P.shape !== undefined ? (P.shape + 1) % SHAPES.length : P.next, 8); + if (P.shape !== undefined && P.shape !== S.cur) { + shapeField(S, P.shape); + assign(S, S.pts[P.shape], S.cores[P.shape]); + S.cur = P.shape; + } + if (P.shape === undefined) S.cur = -1; + if (P.shape !== undefined) { + // A round shape turns slowly about the means while it holds; turned + // whole, its spread and its zero correlation do not change. + const th = SPIN * P.into, rc = cos(th), rs = sin(th); + for (let n = 0; n < N; n++) + if (S.core[n]) { + const dx = S.bx[n] - X0, dy = S.by[n] - Y0; + S.tx[n] = X0 + rc * dx - rs * dy; + S.ty[n] = Y0 + rs * dx + rc * dy; + } + anneal(S, tries, 0.002 + 0.6 * S.heat + TEMP0 * exp(-P.into / TAU), shapeField(S, P.shape), rc, rs); + } + else if (P.back !== undefined) anneal(S, tries, 0.002, null); + else for (let n = 0; n < N; n++) (S.x[n] = S.hx[n]), (S.y[n] = S.hy[n]); + stats(...sums(S), S.stats); +} + +// Where set k's data lands at fold s (0 the quartet, 1 the tall plot), and +// the rows its plot may use. +function view(k, s) { + return { + x0: QX0 + (X0 - QX0) * s, kx: QKX + (KX - QKX) * s, + y0: qY0(k) + (Y0 - qY0(k)) * s, ky: QKY + (KY - QKY) * s, + top: 32 * k * (1 - s), bot: 32 * (k + 1) + (H - 32 * (k + 1)) * s, + }; +} + +// Digits three LEDs wide and five tall, a row of bits each, top first. +const DIG = [0x7b6f, 0x2c97, 0x73e7, 0x73cf, 0x5bc9, 0x79cf, 0x79ef, 0x7249, 0x7bef, 0x7bcf]; + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const P = S.P || { q: 1 }, rgb = S.rgb, tr = S.trail; + rgb.fill(0); + const add = (x, y, c, a) => { + x = floor(x); + y = floor(y); + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + rgb[o] = max(rgb[o], c[0] * a); + rgb[o + 1] = max(rgb[o + 1], c[1] * a); + rgb[o + 2] = max(rgb[o + 2], c[2] * a); + }; + const s = P.q ? 0 : P.fold !== undefined ? P.fold : 1; + const cols = [0, 1, 2, 3].map((k) => hsv(0.42 + S.hue + 0.25 * k, 0.6, 1)); + // The washes keep to the cool side of the wheel: dark yellow and orange + // read as olive and brown. + const upC = hsv(0.5 + 0.45 * S.hue, 0.8, 0.09), dnC = hsv(0.78 - 0.45 * S.hue, 0.7, 0.09); + const lineC = [1, 0.82, 0.55], meanC = [0.45, 0.5, 0.65], sdC = [0.22, 0.26, 0.4]; + // The fixed things: one set of them per plot while there are four. + const views = s >= 1 ? [view(0, 1)] : [0, 1, 2, 3].map((k) => view(k, s)); + views.forEach((v, k) => { + const inb = (y) => y >= v.top && y < v.bot; + // The line splits each plot in two: a faint wash above it, where the + // residuals are positive, and another below, a little stronger inside + // one deviation of the means. + const bx = v.kx * SDX, by = v.ky * SDY; + for (let x = 0; x < W; x++) { + const ly = v.y0 - v.ky * 0.5 * ((x + 0.5 - v.x0) / v.kx), inx = abs(x + 0.5 - v.x0) < bx; + for (let y = Math.ceil(v.top); y < v.bot; y++) add(x, y, y + 0.5 < ly ? upC : dnC, inx && abs(y + 0.5 - v.y0) < by ? 1.35 : 1); + } + if (s < 1 && k > 0) for (let x = 0; x < W; x++) add(x, v.top, sdC, 1 - s); + const sdx = v.kx * SDX, sdy = v.ky * SDY; + for (let y = Math.ceil(v.top); y < v.bot; y++) { + if (y % 4 < 2) add(v.x0, y, meanC, 1); + if (y % 3 === 0) (add(v.x0 - sdx, y, sdC, 1), add(v.x0 + sdx, y, sdC, 1)); + } + for (let x = 0; x < W; x++) { + if (inb(v.y0) && x % 4 < 2) add(x, v.y0, meanC, 1); + if (x % 3 === 0) for (const yy of [v.y0 - sdy, v.y0 + sdy]) if (inb(yy)) add(x, yy, sdC, 1); + } + }); + // The residuals: from each point straight down or up to the line. + const pos = new Float32Array(2 * N); + for (let n = 0; n < N; n++) { + const k = S.set[n]; + let px = S.x[n], py = S.y[n]; + if (s < 1) { + const v = view(k, s), [xs, ys] = SETS[k], j = S.idx[n]; + const hx = (S.hx[n] - X0) / KX + MX, hy = MY - (S.hy[n] - Y0) / KY; + const dx = xs[j] + (hx - xs[j]) * s, dy = ys[j] + (hy - ys[j]) * s; + px = v.x0 + v.kx * (dx - MX); + py = v.y0 - v.ky * (dy - MY); + } + pos[2 * n] = px; + pos[2 * n + 1] = py; + if (S.res > 0.02) { + const v = s >= 1 ? views[0] : views[k]; + const ly = v.y0 - v.ky * 0.5 * ((px - v.x0) / v.kx); + // Brightest at the point, under a third as bright where it meets the line. + const len = max(1, abs(py - ly)); + for (let y = floor(min(ly, py)); y <= floor(max(ly, py)); y++) add(px, y, cols[k], S.res * (0.3 + 0.7 * (1 - abs(y + 0.5 - py) / len))); + } + } + // The line y = 3 + 0.5 x, filled down each column so it stays unbroken. + views.forEach((v) => { + for (let x = 0; x < W; x++) { + const ya = v.y0 - v.ky * 0.5 * ((x - v.x0) / v.kx), yb = v.y0 - v.ky * 0.5 * ((x + 1 - v.x0) / v.kx); + for (let y = floor(min(ya, yb)); y <= floor(max(ya, yb)) && y < H; y++) if (y + 0.5 >= v.top && y + 0.5 < v.bot) add(x, y, lineC, 1); + } + }); + // The five numbers the cloud keeps, top left of the tall plot, each in the + // colour of the lines it sets. + if (s > 0.5) { + const a = sm((s - 0.5) * 2), vals = S.stats, cc = [meanC, meanC, sdC, sdC, lineC]; + for (let r = 0; r < 5; r++) { + const v = vals[r], ds = [floor(v / 100), floor(v / 10) % 10, v % 10]; + const c = r >= 2 && r < 4 ? [0.4, 0.46, 0.7] : cc[r]; + let x = 1; + for (let q = 0; q < 3; q++) { + for (let b = 0; b < 15; b++) if ((DIG[ds[q]] >> (14 - b)) & 1) add(x + (b % 3), 2 + 7 * r + floor(b / 3), c, a); + x += 4; + if (q === 0) (add(x, 6 + 7 * r, c, a), (x += 2)); + } + } + } + // The points, over everything, leaving a short trail as they move. + const fd = S.fade; + for (let i = 0; i < tr.length; i++) tr[i] *= fd; + for (let n = 0; n < N; n++) { + const c = cols[S.set[n]], px = pos[2 * n], py = pos[2 * n + 1]; + const big = s < 0.5 ? 2 : 1; + for (let dy = 0; dy < big; dy++) + for (let dx = 0; dx < big; dx++) { + const x = floor(px - (big - 1) / 2 + dx), y = floor(py - (big - 1) / 2 + dy); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const o = (y * W + x) * 3; + tr[o] = c[0]; + tr[o + 1] = c[1]; + tr[o + 2] = c[2]; + } + } + for (let i = 0; i < W * H; i++) { + const o = i * 3, t = max(tr[o], tr[o + 1], tr[o + 2]); + // A point covers what is under it; its fading trail only adds. + const r = t > 0.97 ? tr[o] : max(rgb[o], tr[o] * 0.6), g = t > 0.97 ? tr[o + 1] : max(rgb[o + 1], tr[o + 1] * 0.6), b = t > 0.97 ? tr[o + 2] : max(rgb[o + 2], tr[o + 2] * 0.6); + display.setPixel(i % W, floor(i / W), 4 * round(63.75 * cl(r, 0, 1)), 4 * round(63.75 * cl(g, 0, 1)), 4 * round(63.75 * cl(b, 0, 1))); + } +} diff --git a/incoming/pattern-study/patterns/specimen-shelf.js b/incoming/pattern-study/patterns/specimen-shelf.js new file mode 100644 index 000000000..faf0fe6c6 --- /dev/null +++ b/incoming/pattern-study/patterns/specimen-shelf.js @@ -0,0 +1,488 @@ +// ===== Patternflow pattern ===== +// Title: Specimen Shelf +// Family: halloween +// Idea: Three specimen jars on two shelves, toxic green and amber above and violet below, each hold a floating eyeball; one turns to stare out at you, the others glance at it and turn one after another, then all of them look away together, roll up in a blink and drift apart, while bubbles rise and a knock on a shelf sets the fluid sloshing and glowing. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 52 +// Round: 52 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Bubbles=0..1, Knock=0..1 +// +// Knob 1: Hue. Turns the three fluids round the wheel together, each iris with its fluid: at 0 toxic green with a magenta iris and amber with a blue iris on the top shelf, violet with a gold iris below. At 0.33 they are blue, green and amber, at 0.67 red, violet and teal. The whites, veins, nerves, lids, cork and shelves stay as they are. +// Knob 2: Speed. At 2 one round of the story takes 12 s: the first eye turns to stare 0.6 s in, the second glances at it at 1.3 s and stares at 1.8, the third glances at 2.1 and stares at 2.6; they stare until 4.4, look away together until 5.3, blink and drift from 5.7. Each round a different eye starts. A small bubble rises through its jar in about 3 s. At 0.2 everything runs ten times slower, at 3 half as fast again. +// Knob 3: Bubbles. At 0 four bubbles a second across the three jars, a third of them rings; at 0.5 about 18 a second; at 1 sixty a second, four in five of them rings up to 5 LEDs across in the small jars and 7 in the big one. +// Knob 4: Knock. How often something knocks a shelf, and how hard. At 0 the first knock comes 6.6 s in and then one every 12 s; at 0.06, 6.3 s in and every 11.4 s; at 1, 1.5 s in and every 1.8 s, twice as hard. Each knock hits the other shelf from the last: the board jolts 2 LEDs sideways, dust falls from under it or jumps off it, the fluid tilts 3 rows at the glass (6 at 1) and sloshes for about 2 s, glowing brighter while it settles, a burst of bubbles shakes loose, and every eye that is not staring turns toward the knock. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, round, sin, cos, sqrt, atan2, acos, exp, hypot, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const mix = (a, b, t) => a + (b - a) * t; +const q = (v, n) => round(v * n) / n; +const wrap = (a) => a - TAU * floor((a + PI) / TAU); +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), qq = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? qq : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? qq : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? qq : p]; +} + +// The two shelves: the top of each board and the row its front ends. +const SHELF = [[56, 61], [119, 127]]; +// The three jars. Rows: lid (or cork), neck, shoulder, body, rounded bottom, +// thick base. Half-widths inside the glass. surf is the fluid's resting +// row, ex/ey/er the eye's rest and radius, hue the fluid's colour, iris the +// eye's, lum how bright the fluid is away from the light. +const JARS = [ + { cx: 16, lid: [5, 10], lidW: 11.5, cork: 0, neck: [11, 13], neckW: 9, sh: [14, 18], w: 12, bot: [50, 54], base: 55, shelf: 0, + surf: 19, ex: 16, ey: 36, er: 7, hue: 0.29, iris: 0.83, irisS: 0.8, lum: 0.08 }, + { cx: 48, lid: [4, 9], lidW: 5.5, cork: 1, neck: [10, 17], neckW: 5, sh: [18, 28], w: 12.5, bot: [49, 54], base: 55, shelf: 0, + surf: 24, ex: 48, ey: 40, er: 7, hue: 0.09, iris: 0.6, irisS: 0.8, lum: 0.2 }, + { cx: 32, lid: [63, 69], lidW: 22.5, cork: 0, neck: [70, 72], neckW: 18, sh: [73, 79], w: 23, bot: [111, 117], base: 118, shelf: 1, + surf: 81, ex: 32, ey: 98, er: 11, hue: 0.78, iris: 0.12, irisS: 0.72, lum: 0.07, gold: 1 }, +]; +function halfIn(J, y) { + if (y < J.neck[0] || y >= J.bot[1]) return -1; + if (y < J.sh[0]) return J.neckW; + if (y < J.sh[1]) { + const u = (J.sh[1] - y) / (J.sh[1] - J.sh[0]); + return J.neckW + (J.w - J.neckW) * sqrt(1 - u * u); + } + if (y < J.bot[0]) return J.w; + const u = (y - J.bot[0]) / (J.bot[1] - J.bot[0]); + return J.w - (J.w * 0.25) * (1 - sqrt(1 - u * u)); +} + +// Veins on the white, laid out in the eye's own angles (th from the gaze +// axis, ph round it) so they turn with it; tabled once, ph against th. +const VEINS = [], VT = 65; +for (let k = 0; k < 7; k++) { + const ph0 = (k * TAU) / 7 + 0.35 * sin(k * 2.3), a = 0.22 + 0.08 * sin(k * 1.7), f = 3 + (k % 3), p = k * 1.3; + const tab = new Float32Array(VT); + for (let j = 0; j < VT; j++) { + const th = (j * PI) / (VT - 1); + tab[j] = ph0 + a * sin(f * th + p) + 0.1 * sin(9 * th + 2 * p); + } + VEINS.push({ tab, end: 0.8 + 0.1 * ((k * 5) % 3) }); +} +const IRIS = 0.62; +// The fluid's glow round an eye, falling off by e every 5 LEDs, tabled. +const EXP5 = Float32Array.from({ length: 64 }, (_, i) => exp(-i / 20)); + +export function setup(params) { + // Per LED: which jar (0 none, else 1 + index) and what part of it. + // 1 fluid side of the glass, 2 glass wall, 3 thick base, 4 lid, 5 cork, + // 6 shelf top, 7 shelf front, 0 the wall behind. + const reg = new Uint8Array(N), jar = new Uint8Array(N), glow = new Float32Array(N); + JARS.forEach((J, j) => { + J.hw = new Float32Array(H); + for (let y = 0; y < H; y++) J.hw[y] = halfIn(J, y); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x, dx = abs(x + 0.5 - J.cx), hw = J.hw[y]; + let r = 0; + if (y >= J.lid[0] && y <= J.lid[1]) { + const lw = J.cork ? J.lidW - 0.15 * (y - J.lid[0]) : y === J.lid[0] ? J.lidW - 1.5 : y === J.lid[0] + 1 ? J.lidW - 0.5 : J.lidW; + if (dx < lw) r = J.cork ? 5 : 4; + } else if (hw > 0 && dx < hw) r = 1; + else if (hw > 0 && dx < hw + 1.3) r = 2; + else if (y >= J.bot[1] && y <= J.base && dx < J.w * 0.8 + 1) r = 3; + if (r) (reg[i] = r), (jar[i] = j + 1); + // How much of this jar's glow reaches the wall here. + const top = J.lid[0], bot = J.base; + const ddx = max(0, dx - (hw > 0 ? hw + 1 : J.w)), ddy = y < top ? top - y : y > bot ? y - bot : 0; + const gw = exp(-hypot(ddx, ddy) / 5); + if (!r && !reg[i] && gw > glow[i]) (glow[i] = gw), (jar[i] = j + 1); + } + }); + SHELF.forEach(([a, b]) => { + for (let y = a; y <= b; y++) for (let x = 0; x < W; x++) reg[y * W + x] = y < a + 2 ? 6 : 7; + }); + params.s = { + t: 0, hue: 0, speed: 2, bub: 0, knockK: 0.06, + reg, jar, glow, + eyes: JARS.map((J, j) => ({ + yaw: 0.6 - 0.6 * j, pitch: 0.2, roll: 0, tyaw: 0.6, tpitch: 0.2, next: 0.5 + 0.7 * j, rate: 2, + stare: 0, dil: 0, ox: 0, oy: 0, vx: 0, vy: 0, ph: j * 2.1, + })), + slosh: JARS.map(() => ({ a: 0, v: 0, ag: 0 })), + bubbles: [], acc: 0, pops: [], dust: [], kSide: 1, cyc: -1, look: [0, 0], + knockAt: 0, kAge: 9, kShelf: 0, + rgb: new Float32Array(N * 3), nerve: new Float32Array(N), + }; +} + +// The story, in seconds at Speed 2, one round of 12 s: one eye turns to +// stare out at 0.6; the next glances at it at 1.3 and stares at 1.8; the +// last glances at 2.1 and stares at 2.6; at 4.4 all three look away to the +// same place, at 5.3 they roll up and back down together, a blink, and from +// 5.7 each drifts on its own. +const ROUND = 12; +function story(S, e, j, tc, order) { + const pos = order.indexOf(j), st = [0.6, 1.8, 2.6][pos], gl = [9, 1.3, 2.1][pos]; + if (tc >= 5.3 && tc < 5.7) return { yaw: 0, pitch: -1.75, rate: 16, stare: 0 }; + if (tc >= 4.4 && tc < 5.3) return { yaw: S.look[0], pitch: S.look[1], rate: 11, stare: 0 }; + if (tc >= st && tc < 4.4) return { yaw: 0, pitch: 0, rate: 6, stare: 1 }; + if (tc >= gl && tc < st) { + // Glance at whichever has already turned. + const L = JARS[order[0]], J = JARS[j]; + const dx = L.ex - J.ex, dy = L.ey - J.ey; + return { yaw: atan2(dx, 10), pitch: cl(atan2(dy, hypot(dx, 10)), -1, 1), rate: 9, stare: 0 }; + } + return null; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.bub = cl(k[2], 0, 1); + S.knockK = cl(k[3], 0, 1); + const g = (d * S.speed) / 2; + S.t += g; + const cyc = floor(S.t / ROUND), tc = S.t - cyc * ROUND; + if (cyc !== S.cyc) { + S.cyc = cyc; + // Who starts this round, and where all three look together. + const o = [[2, 0, 1], [1, 2, 0], [0, 1, 2]][cyc % 3]; + S.order = o; + const looks = [[-1.1, -0.5], [1.1, 0.3], [0.2, 0.9], [-0.9, 0.6]]; + S.look = looks[cyc % 4]; + } + // Knocks: the first 6.6 s in at 0, 1.5 s in at 1; then every 12 s at 0 + // and every 1.8 s at 1. Each hits the other shelf from the last. + S.knockAt += g; + const first = mix(6.6, 1.5, S.knockK), every = mix(12, 1.8, S.knockK); + if (S.knockAt >= (S.knocks ? every : first)) { + S.knockAt = 0; + S.knocks = (S.knocks || 0) + 1; + S.kAge = 0; + S.kShelf = S.knocks % 2; + const hard = 0.6 + 0.6 * S.knockK, side = S.knocks % 3 === 1 ? -1 : 1; + S.kSide = side; + JARS.forEach((J, j) => { + const near = J.shelf === S.kShelf ? 1 : 0.35; + S.slosh[j].v += side * hard * near * 20; + S.slosh[j].ag = min(1.5, S.slosh[j].ag + hard * near); + const e = S.eyes[j]; + e.vx += side * hard * near * 22; + e.vy -= hard * near * 16; + }); + // Dust shaken off the board: down from under the top shelf, up off the + // bottom one. + for (let n = 0; n < 14 + 10 * S.knockK; n++) { + const top = S.kShelf === 0; + S.dust.push({ x: Math.random() * W, y: top ? SHELF[0][1] + 1 : SHELF[1][0] - 1, vx: (Math.random() - 0.5) * 6, vy: top ? 2 + 6 * Math.random() : -8 - 10 * Math.random(), life: 1.2 + Math.random() }); + } + // A rush of bubbles shaken loose. + for (let n = 0; n < 10 + 14 * S.knockK; n++) spawn(S, floor(Math.random() * 3), true); + } + S.kAge += g; + for (const m of S.dust) { + m.vy += 14 * g; + m.x += m.vx * g; + m.y += m.vy * g; + m.life -= g; + } + S.dust = S.dust.filter((m) => m.life > 0 && m.y < H); + JARS.forEach((J, j) => { + // The fluid's tilt, a damped swing of about 1.4 a second. + const sl = S.slosh[j]; + sl.v += (-80 * sl.a - 2.2 * sl.v) * g; + sl.a += sl.v * g; + // Shaken, the fluid lights up, and settles back in about a second. + sl.ag *= exp(-g * 1.1); + const e = S.eyes[j]; + let tg = story(S, e, j, tc, S.order); + if (!tg && S.kAge < 1) { + // A knock: every idle eye looks round at it. + const up = JARS[j].shelf > S.kShelf ? -0.6 : JARS[j].shelf < S.kShelf ? 0.6 : 0.15; + tg = { yaw: S.kSide * 1, pitch: up, rate: 10, stare: 0 }; + e.next = min(e.next, 1.2); + } + if (!tg) { + // Drifting: somewhere new to look every 1.5 to 3.5 s. + e.next -= g; + if (e.next <= 0) { + e.next = 1.5 + 2 * Math.random(); + e.tyaw = (Math.random() * 2 - 1) * (Math.random() < 0.15 ? 1.9 : 0.9); + e.tpitch = (Math.random() * 2 - 1) * 0.6; + } + tg = { yaw: e.tyaw, pitch: e.tpitch, rate: 2, stare: 0 }; + } else (e.tyaw = tg.yaw), (e.tpitch = tg.pitch); + const r = min(1, g * tg.rate); + e.yaw += wrap(tg.yaw - e.yaw) * r; + e.pitch += (tg.pitch - e.pitch) * r; + e.roll = 0.3 * sin(S.t * 0.23 + j) + 0.12 * sin(S.t * 0.61 + 2 * j); + e.stare += (tg.stare - e.stare) * min(1, g * (tg.stare ? 4 : 2.5)); + e.dil += ((tg.stare && e.stare > 0.6 ? 1 : 0) - e.dil) * min(1, g * 2.4); + // A knock throws the eye; a spring brings it back. + e.vx += (-30 * e.ox - 3 * e.vx) * g; + e.vy += (-30 * e.oy - 3 * e.vy) * g; + e.ox += e.vx * g; + e.oy += e.vy * g; + }); + // Bubbles: a few a second from each jar at 0, a fizz at 1. + S.acc += g * mix(4, 60, S.bub * S.bub); + while (S.acc >= 1) { + S.acc -= 1; + spawn(S, floor(Math.random() * 3), false); + } + for (const b of S.bubbles) { + const J = JARS[b.j]; + b.v = min(b.v + g * 30, 7 + 6 * b.r); + b.y -= b.v * g; + b.x += 2.5 * sin(b.ph + b.y * 0.3) * g; + b.x = cl(b.x, J.cx - J.hw[cl(round(b.y), 0, H - 1)] + 1, J.cx + J.hw[cl(round(b.y), 0, H - 1)] - 1); + if (b.y < surfAt(S, b.j, b.x) + 0.5) { + b.dead = 1; + S.pops.push({ x: b.x, j: b.j, life: 0.25 }); + } + } + S.bubbles = S.bubbles.filter((b) => !b.dead); + for (const p of S.pops) p.life -= g; + S.pops = S.pops.filter((p) => p.life > 0); +} +function spawn(S, j, shaken) { + const J = JARS[j]; + const x = J.cx + (Math.random() * 2 - 1) * J.w * 0.7; + const y = shaken ? J.surf + 6 + Math.random() * (J.bot[0] - J.surf - 6) : J.bot[0] + 1; + const r = Math.random() < 0.3 + 0.5 * S.bub ? 1.1 + Math.random() * (0.5 + 1.2 * S.bub) * (J.er / 7) : 0.5; + S.bubbles.push({ j, x, y, z: Math.random() * 2 - 1, r, v: shaken ? 4 : 1, ph: TAU * Math.random() }); +} +// The fluid's surface row at x, tilted by the slosh and rippled. +function surfAt(S, j, x) { + const J = JARS[j], t = S.t, a = S.slosh[j].a; + return J.surf + (a * (x - J.cx)) / J.w * 2.2 + 0.5 * sin(0.4 * x + 1.9 * t + j) + 0.35 * sin(0.7 * x - 2.6 * t) * (1 + 2 * abs(a)); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { rgb, reg, jar, glow, nerve } = S, t = S.t; + const fls = JARS.map((J) => hsv(J.hue + S.hue, 0.92, 1)); + const irs = JARS.map((J) => hsv(J.iris + S.hue, J.irisS, 1)); + // Each eye this frame: where it sits and which way it faces. + const E = JARS.map((J, j) => { + const e = S.eyes[j], bob = 1 - 0.6 * e.stare, s2 = J.er / 11; + const ex = J.ex + (3 * sin(t * 0.45 + 1 + j * 2) * bob) * s2 + e.ox; + const ey = J.ey + (5 * sin(t * 0.9 + j * 1.7) + 1.2 * sin(t * 2.1 + j)) * bob * s2 + e.oy; + const er = J.er * (1 + 0.15 * e.stare); + const ca = cos(e.yaw), sa = sin(e.yaw), cb = cos(e.pitch), sb = sin(e.pitch); + const f = [sa * cb, sb, ca * cb]; + const r0 = [ca, 0, -sa], u0 = [-sa * sb, cb, -ca * sb]; + const cr = cos(e.roll), sr = sin(e.roll); + const rx = [r0[0] * cr + u0[0] * sr, r0[1] * cr + u0[1] * sr, r0[2] * cr + u0[2] * sr]; + const ux = [u0[0] * cr - r0[0] * sr, u0[1] * cr - r0[1] * sr, u0[2] * cr - r0[2] * sr]; + return { ex, ey, er, f, rx, ux, pupil: mix(0.36, 0.72, e.dil), front: f[2] < 0, stare: e.stare }; + }); + // The optic nerves: a pink cord from the back of each eye, hanging and + // waving in the fluid. + nerve.fill(0); + E.forEach((v, j) => { + let bx = -v.f[0], by = -v.f[1]; + const bl = hypot(bx, by); + let dx = bl > 0.05 ? bx / bl : 0, dy = bl > 0.05 ? by / bl : 1; + let x = v.ex + v.er * bx, y = v.ey + v.er * by; + const sc = v.er / 11; + for (let s = 0; s < 11; s++) { + const rad = (2.2 - 0.1 * s) * max(0.7, sc); + for (let yy = floor(y - rad); yy <= y + rad; yy++) + for (let xx = floor(x - rad); xx <= x + rad; xx++) { + if (xx < 0 || xx >= W || yy < 0 || yy >= H || jar[yy * W + xx] !== j + 1) continue; + const dd = sqrt((xx + 0.5 - x) ** 2 + (yy + 0.5 - y) ** 2); + if (dd < rad) nerve[yy * W + xx] = max(nerve[yy * W + xx], dd < rad - 1 ? 1 : 0.6); + } + const a = atan2(dy, dx), wave = 0.32 * sin(t * 1.6 - s * 0.45 + j); + const na = a + 0.12 * wrap(PI / 2 - a) + wave * 0.35; + dx = cos(na); + dy = sin(na); + x += 2 * sc * dx; + y += 2 * sc * dy; + } + }); + + // The light rippling through the fluid: three wavy bands, lit where their + // sum crosses zero. Each band's inner wave is worked out once a frame. + const cs = S.cs || (S.cs = new Float32Array(2 * W + 2 * H)); + for (let y = 0; y < H; y++) cs[y] = 0.9 * sin(y * 0.15 + t * 0.9); + for (let x = 0; x < W; x++) cs[H + x] = 0.9 * sin(x * 0.19 - t * 0.6); + for (let i = 0; i < W + H; i++) cs[H + W + i] = 0.7 * sin((i - H) * 0.12 + t * 0.9); + const sc = S.sc || (S.sc = new Float32Array(3 * W)); + for (let j = 0; j < 3; j++) for (let x = 0; x < W; x++) sc[j * W + x] = surfAt(S, j, x + 0.5); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x, rg = reg[i], jj = jar[i] - 1, px = x + 0.5, py = y + 0.5; + let r = 0.035, gg = 0.012, b = 0.06; + const J = JARS[jj], fl = fls[jj]; + if (rg === 1) { + const sy = sc[jj * W + x] - (abs(px - J.cx) > J.hw[y] - 1.5 ? 1 : 0); + if (py < sy - 0.5) { + r = 0.03 + 0.06 * fl[0]; + gg = 0.02 + 0.08 * fl[1]; + b = 0.06 + 0.06 * fl[2]; + } else { + const v = E[jj], cyl = 1 - ((px - J.cx) / J.hw[y]) ** 2; + const a1 = x * 0.27 + cs[y] + t * 0.75 + jj; + const a2 = y * 0.2 + cs[H + x] - t * 1.1; + const a3 = (x + y) * 0.15 + cs[H + W + x - y + H]; + const cv = abs(sin(a1) + sin(a2) + sin(a3)); + const caus = cv < 0.3 ? 1 - cv / 0.3 : 0; + const de = sqrt((px - v.ex) ** 2 + (py - v.ey) ** 2) - v.er; + const flare = 1 + 0.6 * v.stare + 1.4 * S.slosh[jj].ag; + let L = (J.lum + 0.2 * cyl * cyl) * flare + caus * (0.3 + 0.4 * cyl) + (de > 0 && de < 16 ? 0.1 * (0.6 + 1.6 * v.stare) * EXP5[floor(de * 4)] : 0); + if (py < sy + 0.5) L = 0.95; + L = q(L, 14); + r = fl[0] * L; + gg = fl[1] * L; + b = fl[2] * L; + } + } else if (rg === 2) { + r = 0.14 + 0.22 * fl[0]; + gg = 0.16 + 0.24 * fl[1]; + b = 0.2 + 0.22 * fl[2]; + } else if (rg === 3) { + r = fl[0] * 0.45; + gg = fl[1] * 0.45; + b = fl[2] * 0.45; + } else if (rg === 4) { + // Pewter lid, ridged, with a bright band. + const dx = abs(px - J.cx), rnd = 0.6 + 0.4 * sqrt(max(0, 1 - (dx / J.lidW) ** 2)); + const ridge = (y - J.lid[0]) % 2 === 0 ? 1.25 : 0.8; + const spec = abs(px - (J.cx - J.lidW * 0.5)) < 1.2 ? 2.6 : 1; + const vv = ridge * spec * rnd * (y === J.lid[1] ? 0.6 : 1); + r = 0.17 * vv; + gg = 0.13 * vv; + b = 0.25 * vv; + if (y === J.lid[0]) (r = 0.42), (gg = 0.36), (b = 0.52); + } else if (rg === 5) { + // The cork, grainy and lit from the left. + const dx = px - J.cx, grain = ((x * 7 + y * 13) % 5) / 5; + const vv = (0.75 - 0.05 * dx) * (0.85 + 0.25 * grain) * (y === J.lid[0] ? 1.2 : 1); + r = 0.62 * vv; + gg = 0.36 * vv; + b = 0.24 * vv; + } else if (rg === 6) { + // The shelf top: the jars' glow pools under them. + let pr = 0, pg = 0, pb = 0; + JARS.forEach((K, k) => { + if (SHELF[K.shelf][0] !== y && SHELF[K.shelf][0] + 1 !== y) return; + const pool = exp(-abs(px - K.cx) / (K.w * 0.7)), f2 = fls[k]; + pr += 0.45 * pool * f2[0]; + pg += 0.45 * pool * f2[1]; + pb += 0.45 * pool * f2[2]; + }); + r = 0.2 + pr; + gg = 0.1 + pg; + b = 0.28 + pb; + } else if (rg === 7) { + const edge = y === SHELF[0][0] + 2 || y === SHELF[1][0] + 2; + r = edge ? 0.26 : 0.1; + gg = edge ? 0.15 : 0.04; + b = edge ? 0.36 : 0.16; + } else if (jj >= 0) { + const gw = 0.1 * glow[i] * (1 + 0.8 * E[jj].stare); + r += gw * fl[0]; + gg += gw * fl[1]; + b += gw * fl[2]; + } + if (rg === 1) { + const v = E[jj], u = (px - v.ex) / v.er, w = (py - v.ey) / v.er, d2 = u * u + w * w, nv = nerve[i]; + if (nv && (v.front || d2 >= 1)) { + r = 0.9 * nv; + gg = 0.32 * nv; + b = 0.42 * nv; + } else if (d2 < 1) { + const f = v.f, rx = v.rx, ux = v.ux, iris = irs[jj]; + const nz = sqrt(1 - d2), cosA = u * f[0] + w * f[1] + nz * f[2]; + const th = acos(cl(cosA, -1, 1)), sh = 0.55 + 0.45 * nz; + if (th < IRIS * v.pupil) (r = 0), (gg = 0), (b = 0); + else if (th < IRIS) { + const tt = th / IRIS; + const ph = atan2(u * ux[0] + w * ux[1] + nz * ux[2], u * rx[0] + w * rx[1] + nz * rx[2]); + // Never below 0.6 of full, and the gold one near full, so that + // it stays gold and does not turn olive. + const streak = 0.8 + 0.2 * sin(ph * 13 + 3 * sin(ph * 4)); + let vv = tt > 0.86 ? 0.05 : streak * (tt < v.pupil + 0.12 ? 1 : 0.88) * (0.85 + 0.15 * sh); + if (J.gold && vv > 0.1) vv = 0.9 + 0.1 * streak; + r = iris[0] * vv; + gg = iris[1] * vv; + b = iris[2] * vv; + } else { + const rim = (1 - nz) * (1 - nz); + r = mix(1, fl[0], 0.7 * rim) * sh; + gg = mix(0.97, fl[1], 0.7 * rim) * sh; + b = mix(0.95, fl[2], 0.7 * rim) * sh; + const ph = atan2(u * ux[0] + w * ux[1] + nz * ux[2], u * rx[0] + w * rx[1] + nz * rx[2]); + const st = sin(th) * v.er, jt = round((th / PI) * (VT - 1)), wd = 0.4 + 0.3 * (th / PI); + for (const vn of VEINS) + if (th >= vn.end && abs(wrap(ph - vn.tab[jt])) * st < wd) { + r = 0.8 * sh; + gg = 0.02 * sh; + b = 0.06 * sh; + break; + } + } + if ((u + 0.38) ** 2 + (w + 0.42) ** 2 < 0.03) (r = 1), (gg = 1), (b = 1); + } + } + // The glass catches the light down its left side. + if ((rg === 1 || rg === 2) && abs(px - (J.cx - J.hw[y] - 0.2)) < 0.8 && y > J.sh[1] && y < J.bot[0]) { + r += 0.22; + gg += 0.22; + b += 0.22; + } + const o = i * 3; + rgb[o] = r; + rgb[o + 1] = gg; + rgb[o + 2] = b; + } + // Bubbles: a bright point, or a ring for the bigger ones; those behind the + // eye are hidden by it. + for (const bb of S.bubbles) { + const R = bb.r, v = E[bb.j], fl = fls[bb.j], x0 = floor(bb.x - R - 1), y0 = floor(bb.y - R - 1); + for (let y = y0; y <= y0 + 2 * R + 2; y++) + for (let x = x0; x <= x0 + 2 * R + 2; x++) { + if (x < 0 || x >= W || y < 0 || y >= H || reg[y * W + x] !== 1 || jar[y * W + x] !== bb.j + 1) continue; + if (bb.z < 0 && (x + 0.5 - v.ex) ** 2 + (y + 0.5 - v.ey) ** 2 < v.er * v.er) continue; + const dd = sqrt((x + 0.5 - bb.x) ** 2 + (y + 0.5 - bb.y) ** 2); + if (R < 1 ? dd < 0.75 : abs(dd - R) < 0.55) { + const o = (y * W + x) * 3; + rgb[o] = mix(fl[0], 1, 0.6); + rgb[o + 1] = mix(fl[1], 1, 0.6); + rgb[o + 2] = mix(fl[2], 1, 0.6); + } + } + } + // A popped bubble flicks two drops off the surface. + for (const p of S.pops) { + const fl = fls[p.j]; + for (const dx of [-1, 1]) { + const x = floor(p.x + dx * (1.5 - p.life * 2)), y = floor(surfAt(S, p.j, p.x) - 2 - (p.life < 0.12 ? 1 : 0)); + if (x < 0 || x >= W || y < 0 || y >= H || reg[y * W + x] !== 1) continue; + const o = (y * W + x) * 3; + rgb[o] = mix(fl[0], 1, 0.5); + rgb[o + 1] = mix(fl[1], 1, 0.5); + rgb[o + 2] = mix(fl[2], 1, 0.5); + } + } + for (const m of S.dust) { + const x = floor(m.x), y = floor(m.y); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const o = (y * W + x) * 3, a = 0.5 * min(1, m.life); + rgb[o] = max(rgb[o], 0.75 * a); + rgb[o + 1] = max(rgb[o + 1], 0.62 * a); + rgb[o + 2] = max(rgb[o + 2], 0.8 * a); + } + // A knock jolts its shelf sideways for a moment. + const jolt = S.kAge < 0.5 ? round(2.2 * sin(S.kAge * 40) * exp(-S.kAge / 0.18)) : 0; + const j0 = S.kShelf ? 62 : 0, j1 = S.kShelf ? H : 62; + for (let i = 0; i < N; i++) { + const x = i % W, y = floor(i / W); + const sx = jolt && y >= j0 && y < j1 ? cl(x - jolt, 0, W - 1) : x, o = (y * W + sx) * 3; + display.setPixel(x, y, 4 * round(63.75 * cl(rgb[o], 0, 1)), 4 * round(63.75 * cl(rgb[o + 1], 0, 1)), 4 * round(63.75 * cl(rgb[o + 2], 0, 1))); + } +} From 6b395b770e3f1418d792f8d1d588a5dabfefbdd8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 00:11:38 +0000 Subject: [PATCH 05/10] =?UTF-8?q?52=ED=8C=90:=20Force=20Chains=20=EC=99=80?= =?UTF-8?q?=20Banshee?= 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_01LXsMxtcgxEaHMjeqmCbsjv --- incoming/pattern-study/patterns/banshee.js | 573 ++++++++++++++++++ .../pattern-study/patterns/force-chains.js | 399 ++++++++++++ 2 files changed, 972 insertions(+) create mode 100644 incoming/pattern-study/patterns/banshee.js create mode 100644 incoming/pattern-study/patterns/force-chains.js diff --git a/incoming/pattern-study/patterns/banshee.js b/incoming/pattern-study/patterns/banshee.js new file mode 100644 index 000000000..ec0fb0746 --- /dev/null +++ b/incoming/pattern-study/patterns/banshee.js @@ -0,0 +1,573 @@ +// ===== Patternflow pattern ===== +// Title: Banshee +// Family: halloween +// Idea: A pale, see-through woman drifts in the night wind above a cottage with one lit window, her long hair and tattered robe streaming and whipping, and every 7 seconds she throws back her head and wails: rings of toxic green sound burst from her mouth and bend the night as they pass, the sky flashes green, the window panes rattle in their frame and the candle flame lies flat, the crows on the ridge scatter and the ground fog is blown back. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 52 +// Round: 52 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Wind=0..1, Fury=0..1 +// +// Knob 1: Hue. The colour of her light, of the rings and of the flash; the sky and the cottage with its window stay as they are. At 0 she is green-white and wails toxic green, at 0.25 blue-white with blue rings, at 0.5 lilac with magenta and at 0.75 peach with orange-red. +// Knob 2: Speed. At 2 she wails every 7 s, the first time at 1.2 s: her head goes back over 0.5 s and the wail lasts 1.8 s, a ring leaving her mouth every 0.2 s; 0.7 s later she is quiet again. The rings travel 60 LEDs a second and reach the window about 1.2 s after they leave her; the crows are back on the ridge 4.5 to 5.5 s after they flew. At 0.2 everything, her drift and the flutter of her hair too, is ten times slower, at 3 half as fast again. +// Knob 3: Wind. A wind from the left. At 0 a breeze of 4 LEDs a second, and her own drifting is what lifts her hair and hem; at 1 it blows 44 LEDs a second with gusts and her hair and robe stream out flat to the right. She hangs 8 LEDs further left against it, and the fog runs past four times as fast. +// Knob 4: Fury. How hard she wails. At 0.06 the rings are thin and the sky glows faintly green round her; at 1 the rings are nearly twice as bright and more than twice as wide, they bend the scene further, and the flash floods the whole sky. The crows fly off faster and the flame lies flatter; the fog is blown back harder too. + +const W = 64, H = 128, N = W * H; +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 sm = (v) => (v <= 0 ? 0 : v >= 1 ? 1 : v * v * (3 - 2 * v)); +const hash = (i) => { + let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; +const vn = (x, sd) => { + const i = floor(x), f = x - i, u = f * f * (3 - 2 * f); + return (hash(i * 7919 + sd) * (1 - u) + hash((i + 1) * 7919 + sd) * u) * 2 - 1; +}; +function hsv(h, s, v, out) { + for (let i = 0; i < 3; i++) { + const k = (5 - 2 * i + 6 * (h - floor(h))) % 6; + out[i] = v - v * s * max(0, min(k, 4 - k, 1)); + } + return out; +} +// Channels in steps of 4 of 255. +const q4 = (v) => (v <= 0 ? 0 : v >= 1 ? 252 : round(v * 63) * 4); + +// The roof's ridge and eaves, the ground, and where the fog starts. +const RIDGE = 70, EAVE = 86, GROUND = 119, FOG = 98; +// The window: its left edge, top and pane size; the candle's wick sits in +// the lower left pane. +const WX = 30, WY = 93, PS = 7, FX = 34.5, FY = 106; +// Hair: strands, nodes and segment length in LEDs. Robe and sleeve tatters +// the same. +const NH = 16, HN = 20, HL = 2.3, NR = 9, RN = 14, RL = 3.1, NT = 3, TN = 7, TL = 2.4; +// The wail at Speed 2, in seconds: one every WP, the first at W0; the head +// goes back over WA, she wails until WB and is quiet again by WC. +const WP = 7, W0 = 1.2, WA = 0.5, WB = 2.3, WC = 3.0; +// Sound rings: speed in LEDs a second and the gap between them. +const RV = 60, RI = 0.2; +// The crows' perches along the ridge. +const PERCH = [24, 32, 41, 49]; + +// Every chain in one set of arrays: hair first, then robe, then tatters. +const CH = []; +{ + let o = 0; + for (let i = 0; i < NH; i++) CH.push({ o, n: HN, L: HL * (0.85 + 0.3 * hash(i + 5)), g: 12, k: 0.07 + 0.1 * hash(i + 21), kind: 0, f: 0.7 + 0.6 * hash(i + 33) }), (o += HN); + for (let i = 0; i < NR; i++) CH.push({ o, n: RN - (hash(i + 40) * 4) | 0, L: RL, g: 40, k: 0.04, kind: 1, f: 1 }), (o += RN); + for (let i = 0; i < 2 * NT; i++) CH.push({ o, n: TN, L: TL * (0.8 + 0.4 * hash(i + 80)), g: 14, k: 0.1, kind: 2, f: 1 }), (o += TN); +} +const NN = NH * HN + NR * RN + 2 * NT * TN; + +export function setup(params) { + params.s = { + t: 0, hue: 0, speed: 2, wind: 0, fury: 0.06, gust: 0, + hx: 32, hy: 30, tilt: 0, mouth: 0, arms: 0, blast: 0, flash: 0, + X: new Float32Array(NN), Y: new Float32Array(NN), PX: new Float32Array(NN), PY: new Float32Array(NN), + rx: new Float32Array(NN), ry: new Float32Array(NN), init: false, + rings: [], wailN: -1, ringT: 0, + rattle: 0, bend: 0, gutter: 0, fogX: 32, fogR: 0, fogP: 0, + crows: PERCH.map((x, i) => ({ hx: x, hy: RIDGE, x, y: RIDGE, vx: 0, vy: 0, st: 0, t: 0, ph: i * 1.7, face: i & 1 ? 1 : -1 })), + bg: new Float32Array(N * 3), sky: new Float32Array(N), buf: new Float32Array(N * 3), + ga: new Float32Array(N), gh: new Float32Array(N), ri: new Float32Array(N), rs: new Float64Array(400), + gc: [0, 0, 0], wc: [0, 0, 0], + }; + scene(params.s); +} + +// The still picture: sky, far hills, the cottage and the ground. +function scene(S) { + const { bg, sky } = S; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + let r, g, b, s = 0; + // The roof: a hipped thatch from the left end to past the right edge. + const roofL = 14 + ((EAVE - y) / (EAVE - RIDGE)) * 8; + const hill = 84 + 3 * sin(x * 0.09 + 1) + 2 * sin(x * 0.23); + if (y >= RIDGE && y < EAVE && x >= roofL) { + const n = hash(x * 3 + y * 131) * 0.5 + 0.5 * (0.5 + 0.5 * sin(x * 1.3 + y * 0.6)); + r = 22 + 10 * n; + g = 14 + 6 * n; + b = 26 + 8 * n; + if (y === RIDGE) (r += 14), (g += 10), (b += 18); + } else if (y >= 60 && y < RIDGE && x >= 53 && x <= 58) { + // The chimney. + const n = hash(x + y * 17); + r = 24 + 8 * n; + g = 16 + 5 * n; + b = 26 + 6 * n; + } else if (y >= EAVE && y < GROUND && x >= 16) { + // Stone wall: courses of rough stones with dark mortar. + const row = floor((y - EAVE) / 4), off = row & 1 ? 3 : 0, col = floor((x + off) / 6); + const mort = (y - EAVE) % 4 === 3 || (x + off) % 6 === 5; + const n = hash(row * 97 + col * 13); + r = mort ? 8 : 26 + 14 * n; + g = mort ? 6 : 20 + 10 * n; + b = mort ? 12 : 30 + 12 * n; + if (x === 16) (r *= 0.6), (g *= 0.6), (b *= 0.6); + } else if (y >= GROUND || (y >= hill && (x < 16 || y >= EAVE))) { + // Moor: dark peat with tufts. + const n = hash(x * 7 + y * 61); + const d = y >= GROUND ? 1 : 0.7; + r = (10 + 8 * n) * d; + g = (12 + 9 * n) * d; + b = (16 + 6 * n) * d; + } else { + // Sky, near black overhead and violet towards the horizon, with stars. + const u = y / 84; + s = 1; + r = 4 + 54 * u * u; + g = 2 + 26 * u * u; + b = 12 + 80 * u * u; + if (y < 56 && hash(p * 3 + 1) > 0.985) { + const v = 50 + 70 * hash(p); + (r += v), (g += v), (b += v * 1.1); + } + } + bg[p * 3] = r / 255; + bg[p * 3 + 1] = g / 255; + bg[p * 3 + 2] = b / 255; + sky[p] = s; + } + // The window's frame, dark wood round the four panes. + for (let y = WY - 1; y <= WY + 2 * PS + 1; y++) + for (let x = WX - 1; x <= WX + 2 * PS + 1; x++) { + const p = (y * W + x) * 3; + bg[p] = 0.07; + bg[p + 1] = 0.04; + bg[p + 2] = 0.05; + } +} + +// Where each chain hangs from this step, from the head and the arms. +function roots(S) { + const { hx, hy, tilt, arms, rx, ry } = S; + for (let c = 0; c < CH.length; c++) { + const ch = CH[c]; + let x, y; + if (ch.kind === 0) { + // Hair across the crown, from one temple over to the other. + const a = PI * (1.06 + (0.88 * c) / (NH - 1)); + x = hx + 4.3 * cos(a); + y = hy - 0.6 - tilt + 5 * sin(a); + } else if (ch.kind === 1) { + // The robe across the shoulders. + const u = ((c - NH) / (NR - 1)) * 2 - 1; + x = hx + u * 5.6; + y = hy + 6.4 + 1.4 * u * u; + } else { + // Tatters along the forearms. + const j = c - NH - NR, side = j < NT ? -1 : 1, f = ((j % NT) + 1) / NT; + const [ex, ey, wx, wy] = arm(S, side); + x = ex + (wx - ex) * f; + y = ey + (wy - ey) * f; + } + rx[c] = x; + ry[c] = y; + } +} + +// An arm's elbow and wrist: hanging a little out at rest, flung up and out +// when she wails. +const ARM = [0, 0, 0, 0]; +function arm(S, side) { + const a = 0.32 + 0.1 * sin(S.t * 1.3 + side) + 2.1 * S.arms, b = a + 0.45 - 0.75 * S.arms; + const sx = S.hx + side * 5, sy = S.hy + 7.5; + ARM[0] = sx + side * 8 * sin(a); + ARM[1] = sy + 8 * cos(a); + ARM[2] = ARM[0] + side * 7 * sin(b); + ARM[3] = ARM[1] + 7 * cos(b); + return ARM; +} + +// One step of h for every chain: each node keeps its motion, is pulled +// towards the wind's speed and by its own gravity, and the blast of the wail +// pushes it away from her head; then each segment is set back to its length, +// root first. +function chains(S, h) { + const { X, Y, PX, PY, rx, ry, t, hx, hy } = S; + const wx0 = 4 + 40 * S.wind + S.gust, amp = 7 + 18 * S.wind, bl = S.blast * 45; + for (let c = 0; c < CH.length; c++) { + const { o, n, L, g, k, f: fq } = CH[c]; + X[o] = PX[o] = rx[c]; + Y[o] = PY[o] = ry[c]; + for (let i = 1; i < n; i++) { + const j = o + i, f = i / n, vx = X[j] - PX[j], vy = Y[j] - PY[j]; + let wx = wx0 + amp * f * sin(t * 2.9 * fq + i * 0.42 + c * 1.37), wy = amp * 0.8 * f * cos(t * 2.3 * fq + i * 0.5 + c * 0.9); + if (bl > 0) { + const dx = X[j] - hx, dy = Y[j] - hy, d = sqrt(dx * dx + dy * dy) + 2; + wx += bl * (0.5 + (0.6 * dx) / d); + wy += (bl * 0.3 * dy) / d; + } + PX[j] = X[j]; + PY[j] = Y[j]; + X[j] += vx * 0.98 + k * (wx * h - vx); + Y[j] += vy * 0.98 + k * (wy * h - vy) + g * h * h; + } + for (let i = 1; i < n; i++) { + const j = o + i, dx = X[j] - X[j - 1], dy = Y[j] - Y[j - 1], d = sqrt(dx * dx + dy * dy) || 1; + X[j] = X[j - 1] + (dx * L) / d; + Y[j] = Y[j - 1] + (dy * L) / d; + } + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = cl(k[0], 0, 1); + S.speed = cl(k[1], 0.2, 3); + S.wind = cl(k[2], 0, 1); + S.fury = cl(k[3], 0, 1); + const ds = (d * S.speed) / 2, F = 0.55 + 0.45 * S.fury; + S.t += ds; + const t = S.t; + // Where she is in her wail. + const nw = floor((t - W0) / WP), w = t >= W0 ? t - W0 - nw * WP : -1; + const up = w < 0 ? 0 : w < WA ? sm(w / WA) : w < WB ? 1 : w < WC ? 1 - sm((w - WB) / (WC - WB)) : 0; + S.tilt = up; + S.arms = up; + S.mouth = w < 0 ? 0.1 : w < WA ? 0.1 + 0.9 * sm((w - 0.15) / (WA - 0.15)) : w < WB ? 1 : w < WC ? 1 - 0.9 * sm((w - WB) / 0.4) : 0.1; + S.blast = w >= WA && w < WB ? F * (0.6 + 0.4 * sm((w - WA) / 0.3)) : 0; + // She drifts, bobbing, and rises when she wails. + S.hx = 27 - 8 * S.wind + 10 * sin(0.23 * t + 0.4) + 4 * sin(0.57 * t); + S.hy = 30 + 4 * sin(0.31 * t) + 1.5 * sin(1.3 * t) - 4 * up; + S.gust = vn(t * 0.7, 3) * (4 + 16 * S.wind); + // A new wail: rings start leaving her mouth. + if (w >= WA && w < WB) { + if (S.wailN !== nw) (S.wailN = nw), (S.ringT = 0), (S.fogX = S.hx), (S.fogR = 0); + S.ringT -= ds; + if (S.ringT <= 0) { + S.ringT += RI; + S.rings.push({ x: S.hx, y: S.hy + 2.5 - S.tilt, t0: t, a: F }); + } + } + S.flash = max(S.flash * exp(-6 * ds), (S.blast > 0) * (0.45 + 1.2 * S.fury) * (S.ringT > RI * 0.5 ? 1 : 0.7)); + S.rattle *= exp(-3 * ds); + S.fogP = max(0, S.fogP - 0.22 * ds); + const mx = S.hx, my = S.hy + 2.5; + for (let i = S.rings.length - 1; i >= 0; i--) { + const R = S.rings[i], r = (t - R.t0) * RV, r0 = r - ds * RV; + if (r > 140) { + S.rings.splice(i, 1); + continue; + } + // What the ring front crossed this step. + const dw = sqrt((R.x - 38.5) ** 2 + (R.y - 101) ** 2); + if (r0 < dw && r >= dw) S.rattle = max(S.rattle, R.a); + const fy = FOG + 8 - R.y; + if (r > fy) S.fogR = max(S.fogR, sqrt(r * r - fy * fy)), (S.fogP = max(S.fogP, R.a)); + for (const c of S.crows) { + const dc = sqrt((R.x - c.x) ** 2 + (R.y - c.y) ** 2); + if (c.st === 0 && r0 < dc && r >= dc) { + c.st = 1; + c.t = 0; + c.vx = (c.x < R.x ? -1 : 1) * (16 + 14 * hash(c.hx + nw * 7)) * (0.6 + 0.6 * R.a); + c.vy = -(24 + 12 * hash(c.hx * 3 + nw)); + } + } + } + // The candle flame lies over, away from her, while the wail lasts. + const flat = w >= WA && w < WB + 0.3 && S.rattle > 0.05 ? 1 : 0; + const lean = (FX > mx ? 1 : -1) * 1.45 * F; + S.bend += ((flat ? lean : 0.12 * vn(t * 3, 9)) - S.bend) * min(1, 9 * ds); + S.gutter += ((flat ? 0.55 * F : 0) - S.gutter) * min(1, 5 * ds); + // Crows: off the ridge, away and back. + for (const c of S.crows) { + c.ph += ds * (c.st ? 40 : 1.3); + c.t += ds; + if (c.st === 1) { + c.vy += 8 * ds; + c.vx *= 1 - 0.3 * ds; + if (c.t > 2.2) c.st = 2; + } else if (c.st === 2) { + const dx = c.hx - c.x, dy = c.hy - c.y, dd = sqrt(dx * dx + dy * dy) || 1; + const sp = min(28, 3 + dd * 2); + c.vx += ((dx / dd) * sp - c.vx) * min(1, 2.5 * ds); + c.vy += ((dy / dd) * sp - c.vy) * min(1, 2.5 * ds); + if (dd < 1.2) (c.st = 0), (c.x = c.hx), (c.y = c.hy), (c.vx = c.vy = 0), (c.face = c.hx < mx ? 1 : -1); + } + if (c.st) (c.x += c.vx * ds), (c.y += c.vy * ds), (c.face = c.vx < 0 ? -1 : 1); + } + // Hair and cloth, two steps a frame; the first frame lets them stream out. + if (!S.init) { + S.init = true; + roots(S); + for (let c = 0; c < CH.length; c++) + for (let i = 0; i < CH[c].n; i++) { + const j = CH[c].o + i; + S.X[j] = S.PX[j] = S.rx[c] + i * CH[c].L * 0.6; + S.Y[j] = S.PY[j] = S.ry[c] + i * CH[c].L * 0.8; + } + for (let i = 0; i < 90; i++) chains(S, 1 / 60); + } + roots(S); + chains(S, ds / 2); + chains(S, ds / 2); +} + +// Ghost light: an anti-aliased dot added into a buffer, a segment of them, +// and filled shapes kept at the higher of what is there and a. +function dot(g, x, y, a) { + x -= 0.5; + y -= 0.5; + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + for (let j = 0; j < 2; j++) + for (let i = 0; i < 2; i++) { + const X = ix + i, Y = iy + j; + if (X >= 0 && X < W && Y >= 0 && Y < H) g[Y * W + X] += a * (i ? fx : 1 - fx) * (j ? fy : 1 - fy); + } +} +function seg(g, x0, y0, x1, y1, a0, a1) { + const n = max(1, Math.ceil(sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2) * 1.6)); + for (let i = 0; i < n; i++) { + const f = i / n; + dot(g, x0 + (x1 - x0) * f, y0 + (y1 - y0) * f, (a0 + (a1 - a0) * f) * 0.62); + } +} +function tri(g, ax, ay, bx, by, cx, cy, a) { + const x0 = max(0, floor(min(ax, bx, cx))), x1 = min(W - 1, floor(max(ax, bx, cx))), y0 = max(0, floor(min(ay, by, cy))), y1 = min(H - 1, floor(max(ay, by, cy))); + const ar = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax); + if (abs(ar) < 1e-6) return; + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const px = x + 0.5, py = y + 0.5; + const u = ((bx - px) * (cy - py) - (by - py) * (cx - px)) / ar, v = ((cx - px) * (ay - py) - (cy - py) * (ax - px)) / ar; + if (u >= 0 && v >= 0 && u + v <= 1) { + const p = y * W + x; + if (g[p] < a) g[p] = a; + } + } +} +function ell(g, cx, cy, rx, ry, a) { + for (let y = max(0, floor(cy - ry)); y <= min(H - 1, floor(cy + ry)); y++) + for (let x = max(0, floor(cx - rx)); x <= min(W - 1, floor(cx + rx)); x++) { + const u = (x + 0.5 - cx) / rx, v = (y + 0.5 - cy) / ry, q = u * u + v * v; + if (q < 1) { + const p = y * W + x, b = a * min(1, (1 - q) * 3); + if (g[p] < b) g[p] = b; + } + } +} + +// Crow on a perch, facing left; flipped when it faces right. +const PERCHED = [".##....", "####...", ".#####.", "..#####", "...#.#."]; + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { bg, sky, buf, ga, gh, X, Y, gc, wc, t } = S; + const hue = 0.33 + S.hue; + hsv(hue, 0.85, 1, wc); + hsv(hue, 0.2, 1, gc); + const mx = S.hx, my = S.hy + 2.6 - 1.1 * S.tilt; + // Rings now: centre, radius and strength. + const rs = S.rs, ri = S.ri; + let nr = 0; + for (const R of S.rings) { + const r = (t - R.t0) * RV; + rs[nr * 4] = R.x; + rs[nr * 4 + 1] = R.y; + rs[nr * 4 + 2] = r; + rs[nr++ * 4 + 3] = R.a * (1 - r / 140); + } + ri.fill(0); + // The night, bent where a ring passes, and lit by the flash. + const fr = 40 + 40 * S.fury, rw = 0.9 + 1.4 * S.fury; + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + let sx = x, sy = y; + for (let k = 0; k < nr * 4; k += 4) { + const dx = x + 0.5 - rs[k], dy = y + 0.5 - rs[k + 1], d = sqrt(dx * dx + dy * dy) + 1e-3, e = d - rs[k + 2], a = rs[k + 3]; + if (e > -4 && e < 4) { + const o = 2.4 * a * (1 - abs(e) / 4) * (e < 0 ? 1 : -0.5); + sx += (dx / d) * o; + sy += (dy / d) * o; + if (e > -rw && e < rw) ri[p] += a * 0.75 * (1 - abs(e) / rw); + } + } + const q = (cl(round(sy), 0, H - 1) * W + cl(round(sx), 0, W - 1)) * 3; + const dm = sqrt((x - mx) ** 2 + (y - my) ** 2), fl = S.flash * (0.25 + 0.5 * sky[p]) * exp(-dm / fr); + buf[p * 3] = bg[q] + wc[0] * fl * 0.5; + buf[p * 3 + 1] = bg[q + 1] + wc[1] * fl * 0.5; + buf[p * 3 + 2] = bg[q + 2] + wc[2] * fl * 0.5; + } + win(S); + fog(S); + crows(S); + // The banshee. + ga.fill(0); + gh.fill(0); + for (let c = NH; c < NH + NR - 1; c++) { + const A = CH[c], B = CH[c + 1], n = min(A.n, B.n); + for (let i = 0; i < n - 1; i++) { + const a = 0.45 - (0.25 * i) / n, p = A.o + i, q = B.o + i; + tri(ga, X[p], Y[p], X[p + 1], Y[p + 1], X[q + 1], Y[q + 1], a); + tri(ga, X[p], Y[p], X[q + 1], Y[q + 1], X[q], Y[q], a); + } + } + for (let c = NH; c < CH.length; c++) { + const { o, n, kind } = CH[c]; + for (let i = 0; i < n - 1; i++) seg(gh, X[o + i], Y[o + i], X[o + i + 1], Y[o + i + 1], kind === 1 ? 0.25 : 0.5, kind === 1 ? 0.2 : 0.2); + } + for (const side of [-1, 1]) { + const [ex, ey, wx, wy] = arm(S, side), sx = S.hx + side * 5, sy = S.hy + 7.5; + seg(gh, sx, sy, ex, ey, 0.9, 0.8); + seg(gh, ex, ey, wx, wy, 0.8, 0.7); + ell(ga, wx, wy, 1.1, 1.1, 0.9); + } + seg(gh, S.hx, S.hy + 3, S.hx, S.hy + 7, 0.9, 0.9); + ell(ga, S.hx, S.hy, 3.7, 4.7 * (1 - 0.12 * S.tilt), 0.95); + for (let c = 0; c < NH; c++) { + const { o, n } = CH[c]; + for (let i = 0; i < n - 1; i++) seg(gh, X[o + i], Y[o + i], X[o + i + 1], Y[o + i + 1], 0.55 - (0.4 * i) / n, 0.55 - (0.4 * (i + 1)) / n); + } + for (let p = 0; p < N; p++) { + const a = min(1, ga[p] + gh[p]); + if (a <= 0) continue; + const k = 1 - 0.72 * a; + buf[p * 3] = buf[p * 3] * k + gc[0] * a * 0.92; + buf[p * 3 + 1] = buf[p * 3 + 1] * k + gc[1] * a * 0.92; + buf[p * 3 + 2] = buf[p * 3 + 2] * k + gc[2] * a * 0.92; + } + face(S, mx, my); + // The rings themselves, added on top. + for (let p = 0; p < N; p++) { + const v = ri[p]; + if (v > 0) (buf[p * 3] += wc[0] * v), (buf[p * 3 + 1] += wc[1] * v), (buf[p * 3 + 2] += wc[2] * v); + } + for (let p = 0; p < N; p++) display.setPixel(p % W, floor(p / W), q4(buf[3 * p]), q4(buf[3 * p + 1]), q4(buf[3 * p + 2])); +} + +// Eyes and the mouth: dark hollows with a glint, and a mouth that opens to a +// long dark O glowing inside while she wails. +function face(S, mx, my) { + const { buf, wc } = S; + const set = (x, y, r, g, b) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const p = (y * W + x) * 3; + buf[p] = r; + buf[p + 1] = g; + buf[p + 2] = b; + }; + const ey = floor(S.hy - 0.6 - 1.2 * S.tilt), gl = 0.3 + 0.7 * S.blast; + for (const side of [-1, 1]) { + const ex = floor(S.hx + side * 1.6); + set(ex, ey, wc[0] * gl, wc[1] * gl, wc[2] * gl); + set(ex, ey + 1, 0.04, 0.03, 0.08); + } + const rx = 0.5 + 1.1 * S.mouth, ry = 0.5 + 2.3 * S.mouth; + for (let y = floor(my - ry); y <= floor(my + ry); y++) + for (let x = floor(mx - rx); x <= floor(mx + rx); x++) { + const u = (x + 0.5 - mx) / rx, v = (y + 0.5 - my) / ry, q = u * u + v * v; + if (q < 1) { + const g = S.blast * (1 - q) * 0.9; + set(x, y, 0.03 + wc[0] * g, 0.02 + wc[1] * g, 0.06 + wc[2] * g); + } + } +} + +// The window: four panes lit warm by a candle on the sill, shaken loose in +// their frame by a passing ring, the flame bending over. +function win(S) { + const { buf, t } = S; + const fb = 1 - S.gutter, fl = 0.92 + 0.08 * vn(t * 9, 4); + const len = (4.2 + 0.6 * vn(t * 7, 2)) * (1 - 0.35 * S.gutter); + for (let pj = 0; pj < 2; pj++) + for (let pi = 0; pi < 2; pi++) { + const id = pj * 2 + pi, r = S.rattle; + const ox = r > 0.06 ? round(r * 1.4 * sin(t * 53 + id * 2.1)) : 0, oy = r > 0.06 ? round(r * 1.1 * cos(t * 47 + id * 1.3)) : 0; + const x0 = WX + pi * (PS + 1), y0 = WY + pj * (PS + 1); + for (let y = y0; y < y0 + PS; y++) + for (let x = x0; x < x0 + PS; x++) { + const p = (y * W + x) * 3, lx = x - ox, ly = y - oy; + if (lx < x0 || lx >= x0 + PS || ly < y0 || ly >= y0 + PS) { + buf[p] = 0.05; + buf[p + 1] = 0.03; + buf[p + 2] = 0.04; + continue; + } + const d = sqrt((lx + 0.5 - FX) ** 2 + (ly + 0.5 - FY + 2) ** 2), gw = exp(-d / 5) * fb * fl; + let R = 0.42 + 0.58 * gw, G = 0.2 + 0.52 * gw, B = 0.06 + 0.22 * gw; + // The candle, cream wax on the sill. + if (lx >= 34 && lx <= 35 && ly > FY && ly <= FY + 4) (R = 0.85), (G = 0.75), (B = 0.55); + buf[p] = R * fl; + buf[p + 1] = G * fl; + buf[p + 2] = B * fl; + } + } + // The flame: a tongue from the wick, bent by S.bend. + const r = S.rattle, ox = r > 0.06 ? round(r * 1.4 * sin(t * 53 + 4.2)) : 0, oy = r > 0.06 ? round(r * 1.1 * cos(t * 47 + 1.3)) : 0; + for (let i = 0; i <= 8; i++) { + const f = i / 8, a = S.bend * f, px = FX + ox + sin(a) * len * f, py = FY + oy - cos(a) * len * f, wd = 1.3 * (1 - f) + 0.3; + for (let y = floor(py - wd); y <= floor(py + wd); y++) + for (let x = floor(px - wd); x <= floor(px + wd); x++) { + if (x < WX + 0 || x >= WX + PS || y < WY + PS + 1 || y >= WY + 2 * PS + 1) continue; + if ((x + 0.5 - px) ** 2 + (y + 0.5 - py) ** 2 > wd * wd) continue; + const p = (y * W + x) * 3; + buf[p] = 1 * fb + 0.4 * (1 - fb); + buf[p + 1] = (0.95 - 0.45 * f) * fb + 0.2 * (1 - fb); + buf[p + 2] = (0.7 - 0.6 * f) * fb + 0.05; + } + } +} + +// Ground fog drifting with the wind, blown out in a ring under her by the +// wail and creeping back. +function fog(S) { + const { buf, t } = S; + const sp = 3 + 9 * S.wind; + for (let y = FOG; y < H; y++) { + const pr = sm((y - FOG) / 16); + for (let x = 0; x < W; x++) { + let d = pr * (0.62 + 0.24 * vn(x * 0.11 - t * sp * 0.1 + y * 0.37, 11) + 0.18 * vn(x * 0.23 - t * sp * 0.07 - y * 0.21, 12)); + if (S.fogP > 0) { + const dd = abs(x + 0.5 - S.fogX), e = (dd - S.fogR) / 3; + d *= dd < S.fogR ? 1 - S.fogP * 0.92 * sm((S.fogR - dd) / 5) : 1; + d += pr * S.fogP * 0.6 * exp(-e * e); + } + const a = cl(d * 0.62, 0, 0.85), p = (y * W + x) * 3; + buf[p] = buf[p] * (1 - a) + 0.5 * a; + buf[p + 1] = buf[p + 1] * (1 - a) + 0.45 * a; + buf[p + 2] = buf[p + 2] * (1 - a) + 0.62 * a; + } + } +} + +function crows(S) { + const { buf } = S; + const set = (x, y) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const p = (y * W + x) * 3; + buf[p] = 0.02; + buf[p + 1] = 0.01; + buf[p + 2] = 0.03; + }; + for (const c of S.crows) { + const x = round(c.x), y = round(c.y); + if (c.st === 0) { + for (let j = 0; j < 5; j++) + for (let i = 0; i < 7; i++) if (PERCHED[j][c.face < 0 ? i : 6 - i] === "#") set(x - 3 + i, y - 5 + j); + } else { + // In flight: a body, a head and two wings beating. + const f = sin(c.ph), tip = -3.2 * f, mid = -1.4 * f; + for (let i = -1; i <= 1; i++) set(x + i, y); + set(x + 2 * c.face, y - 1); + set(x + c.face, y - 1); + set(x - 2 * c.face, y + 1); + for (const s of [-1, 1]) { + set(x + s * 2, round(y + mid * 0.6)); + set(x + s * 3, round(y + mid)); + set(x + s * 4, round(y + (mid + tip) * 0.5)); + set(x + s * 5, round(y + tip)); + } + } + } +} diff --git a/incoming/pattern-study/patterns/force-chains.js b/incoming/pattern-study/patterns/force-chains.js new file mode 100644 index 000000000..4b9145e2d --- /dev/null +++ b/incoming/pattern-study/patterns/force-chains.js @@ -0,0 +1,399 @@ +// ===== Patternflow pattern ===== +// Title: Force Chains +// Family: particle +// Idea: About 175 soft discs fill a tall box seen in white light between crossed circular polarisers and are squeezed in turn by a piston on top and by the side walls, and only the discs that carry the load light up in isochromatic fringes, grey, white, yellow, orange, red, violet and blue, along branching chains that swing from upright to sideways and find new paths as the push moves, while the discs between them stay dark. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: photoelastic force chains (Pierre Dantu, 1957; Trushant Majmudar and Robert Behringer, 2005), pattern round 52 +// Round: 52 +// =============================== +// @matrix 64x128 +// @knobs Analyser=0..1, Speed=0.2..3, Load=-1..1, Grain=0..1 +// +// Knob 1: Analyser. Turns the analyser away from crossed, a quarter turn over the range. At 0 the field is dark: an unloaded disc is black and only the loaded ones light up; turning it shifts every fringe along the colour sequence, and at 1 the field is light: unloaded discs shine white and the chains show as dark and coloured bands. +// Knob 2: Speed. At 2 a round takes 6 s: the piston presses hardest at 0 s while the walls ease off, and at 3 s the walls squeeze hardest while the piston eases to a fifth. In each squeeze one wall pushes three times as hard as the other, the left in one round and the right in the next, so the pile leans one way and then the other and the whole sequence takes 12 s. At 0.2 it is ten times slower, at 3 half as fast again; the discs always move at their own pace. +// Knob 3: Load. The hardest push of the piston and of the walls, as a multiple of the weight of the pile spread over the floor: 1.5 at -1, 3 at 0 and 6 at 1. At -1 the chains are thin and grey-white with a little yellow; at 1 they thicken and the strongest discs low in the pile run through red and blue into the pale pinks and greens of high orders. +// Knob 4: Grain. The size of the discs: at 0.06 they are 6.3 and 8.1 LEDs across, 175 of them; at 0.5 8.5 and 11, 95 of them; at 1 12 and 15.6, 48 discs whose fringes show the whole pattern of a disc squeezed at its contacts. A new size pours a new pile. + +const W = 64, H = 128, NP = W * H; +const { floor, min, max, abs, sqrt, sin, cos, PI, pow, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// Gravity in LEDs a second squared, physics steps a frame, and the most +// contacts one disc keeps. +const G = 50, SUB = 16, MC = 8; +// Normal stiffness per unit of the weight pressure on the floor, tangential +// stiffness as a share of it, the friction coefficient, the share of critical +// damping a contact gets along its normal and the share of that along its +// tangent, and the drag every disc feels, per second. +const KS = 150, KT = 0.4, MU = 0.5, ZETA = 0.35, TD = 0.2, DRAG = 2.5; +// One round at Speed 2 in seconds. The piston and the two side walls are +// heavy plates pushed with set pressures, as shares of the Load: the piston +// from SY0 to SY1 and the walls from SX0 to SX1 and back. Each wall is NS +// plates stacked up its height, like a membrane, so the squeeze does not all +// land on the stiff bottom of the pile. A wall plate stops at WOUT LEDs +// outside the panel's edge and WIN inside it. One wall pushes 1 + LEAN of +// the set pressure and the other 1 - LEAN, the left in one round and the +// right in the next, and the pile leans. +const T_R = 6, SY0 = 1, SY1 = 0.2, SX0 = 0.2, SX1 = 1, NS = 8, WOUT = 2, WIN = 3, PD = 30, LEAN = 0.5; +// White-light fringe colours: retardation in nanometres per LUT step, the +// steps, and the wavelength the fringe order is counted at. +const LS = 3, LN = 1024, L0 = 560; +// Contact softening: the squared radius in LEDs over which a point load spreads. +const A2 = 0.25; +// Fringes made by the weight pressure on the floor, and the LUT's gamma. +const FR = 0.09, GAM = 1.0; + +// CIE 1931 colour matching, as the piecewise Gaussian fit of Wyman, Sloan +// and Shirley (2013). +const gp = (l, m, s1, s2) => { + const t = (l - m) / (l < m ? s1 : s2); + return exp(-0.5 * t * t); +}; +const XB = (l) => 1.056 * gp(l, 599.8, 37.9, 31) + 0.362 * gp(l, 442, 16, 26.7) - 0.065 * gp(l, 501.1, 20.4, 26.2); +const YB = (l) => 0.821 * gp(l, 568.8, 46.9, 40.5) + 0.286 * gp(l, 530.9, 16.3, 31.1); +const ZB = (l) => 1.217 * gp(l, 437, 11.8, 36) + 0.681 * gp(l, 459, 26, 13.8); + +// The colour seen through a circular polariscope at each retardation, for an +// analyser turned beta from crossed: each wavelength passes sin^2(pi R/l + beta). +function buildLut(S, beta) { + const lut = S.lut, ws = []; + for (let l = 400; l <= 700; l += 10) ws.push(l); + let wr = 0, wg = 0, wb = 0; + const rgb = (X, Y, Z) => [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]; + { + let X = 0, Y = 0, Z = 0; + for (const l of ws) (X += XB(l)), (Y += YB(l)), (Z += ZB(l)); + [wr, wg, wb] = rgb(X, Y, Z); + } + for (let i = 0; i < LN; i++) { + const R = i * LS; + let X = 0, Y = 0, Z = 0; + for (const l of ws) { + const s = sin((PI * R) / l + beta), I = s * s; + X += I * XB(l); + Y += I * YB(l); + Z += I * ZB(l); + } + const c = rgb(X, Y, Z); + lut[i * 3] = pow(cl(c[0] / wr, 0, 1), GAM) * 255; + lut[i * 3 + 1] = pow(cl(c[1] / wg, 0, 1), GAM) * 255; + lut[i * 3 + 2] = pow(cl(c[2] / wb, 0, 1), GAM) * 255; + } + S.beta = beta; +} + +const NMAX = 400, WALL = 1000; +export function setup(params) { + const F = (k) => new Float64Array(NMAX * k); + params.s = { + u: 0, speed: 2, load: 3, grain: -1, beta: -1, n: 0, rm: 3, gen: 0, + x: F(1), y: F(1), vx: F(1), vy: F(1), w: F(1), r: F(1), m: F(1), fx: F(1), fy: F(1), tq: F(1), + nb: new Int32Array(NMAX * MC).fill(-1), xi: F(MC), sg: new Int32Array(NMAX * MC), + cn: new Uint8Array(NMAX), cux: new Float32Array(NMAX * MC), cuy: new Float32Array(NMAX * MC), + cfx: new Float32Array(NMAX * MC), cfy: new Float32Array(NMAX * MC), + head: new Int32Array(4096), next: new Int32Array(NMAX), + wi: new Float64Array(2 * NS), wv: new Float64Array(2 * NS), wf: new Float64Array(2 * NS), lean: 0, yp: 0, vp: 0, mp: 1, fp: 0, fw: 0, + k: 1, c: 0, kt: 1, mg: 1, gw: 1, gh: 1, cs: 8, + lut: new Float32Array(LN * 3), buf: new Uint8Array(NP * 3), cc: new Float64Array(MC * 4), + }; +} + +// Pours a new pile: half small discs and half large ones, 1.3 times wider, so +// the packing stays disordered. The discs are dropped anywhere in the box at +// a fifth of their size and grown while overlapping pairs are pushed apart, +// then the pile settles under gravity and the piston. +function pour(S, grain) { + S.grain = grain; + const rm = 3.6 * pow(2, grain - 0.06), rs = rm * 0.87, rl = rm * 1.13; + S.rm = rm; + const bw = W + 2 * WOUT, area = 0.82 * bw * (H + 2), n = min(NMAX, Math.round(area / (PI * (rs * rs + rl * rl) * 0.5))); + const top = H - (H * 0.82) / 0.78; + S.n = n; + let mg = 0; + for (let i = 0; i < n; i++) { + const r = i & 1 ? rs : rl; + S.r[i] = r; + S.m[i] = r * r; + S.x[i] = -WOUT + r + Math.random() * (bw - 2 * r); + S.y[i] = top + r + Math.random() * (H - top - 2 * r); + S.vx[i] = S.vy[i] = S.w[i] = 0; + mg += r * r * G; + } + S.nb.fill(-1); + S.mg = mg; + S.cs = 2 * rl + 0.01; + S.gw = floor((W + 8) / S.cs) + 1; + S.gh = floor((H + 200) / S.cs) + 1; + for (let it = 0; it < 90; it++) grow(S, min(1, 0.2 + it / 50), top); + // Stiffness and damping scale with the weight pressure on the floor, so a + // contact squeezes the same share of a radius whatever the grain. + S.k = (KS * mg) / W; + S.kt = KT * S.k; + S.c = 2 * ZETA * sqrt(S.k * rm * rm * 0.5); + S.mp = (0.15 * mg) / G; + let ymin = H; + for (let i = 0; i < n; i++) ymin = min(ymin, S.y[i] - S.r[i]); + S.yp = ymin; + S.vp = 0; + S.wi.fill(-WOUT); + S.wv.fill(0); + press(S, 0); + for (let i = 0; i < 50 * SUB; i++) step(S, 1 / 30 / SUB, 8, false); +} + +// One pass of growing: radii at share s of their size, every overlapping +// pair pushed apart by its overlap, half each, and the walls pushing back. +function grow(S, s, top) { + const { n, x, y, r, head, next } = S; + const cs = S.cs, gw = S.gw, gh = S.gh, off = 200; + head.fill(-1, 0, gw * gh); + for (let i = 0; i < n; i++) { + const o = cl(floor((y[i] + off) / cs), 0, gh - 1) * gw + cl(floor((x[i] + 4) / cs), 0, gw - 1); + next[i] = head[o]; + head[o] = i; + } + for (let gy = 0; gy < gh; gy++) + for (let gx = 0; gx < gw; gx++) + for (let i = head[gy * gw + gx]; i >= 0; i = next[i]) + for (let dy = 0; dy <= 1; dy++) + for (let dx = -1; dx <= 1; dx++) { + if (dy === 0 && dx < 0) continue; + const hx = gx + dx, hy = gy + dy; + if (hx < 0 || hx >= gw || hy >= gh) continue; + for (let j = dy === 0 && dx === 0 ? next[i] : head[hy * gw + hx]; j >= 0; j = next[j]) { + const ddx = x[j] - x[i], ddy = y[j] - y[i], rr = (r[i] + r[j]) * s, d2 = ddx * ddx + ddy * ddy; + if (d2 >= rr * rr) continue; + const d = sqrt(d2) || 1e-6, p = (0.5 * (rr - d)) / d; + x[i] -= ddx * p; + y[i] -= ddy * p; + x[j] += ddx * p; + y[j] += ddy * p; + } + } + for (let i = 0; i < n; i++) { + const ri = r[i] * s; + x[i] = cl(x[i], ri - WOUT, W + WOUT - ri); + y[i] = cl(y[i], top + ri, H - ri); + } +} + +// The slot that holds the tangential spring between disc a and body id, made +// if there is none and room for one. +function slot(S, a, id) { + const nb = S.nb, b0 = a * MC; + let free = -1; + for (let o = b0; o < b0 + MC; o++) { + if (nb[o] === id) return o; + if (nb[o] < 0 && free < 0) free = o; + } + if (free >= 0) (nb[free] = id), (S.xi[free] = 0); + return free; +} + +// The tangential force on the second body of a contact that slid vt h along +// its tangent, from the spring held in slot o: it slips past MU times fn. +function fric(S, o, vt, fn, h, cd) { + if (o < 0) return 0; + let xi = S.xi[o] + vt * h, ft = -S.kt * xi - TD * cd * vt; + const lim = MU * fn; + if (ft > lim) (ft = lim), (xi = -lim / S.kt); + else if (ft < -lim) (ft = -lim), (xi = lim / S.kt); + S.xi[o] = xi; + S.sg[o] = S.gen; + return ft; +} + +// Keeps a contact for drawing: the direction from the disc's centre to it +// and the force on the disc there. +function keepC(S, i, ux, uy, fx, fy) { + if (S.cn[i] >= MC) return; + const o = i * MC + S.cn[i]++; + S.cux[o] = ux; + S.cuy[o] = uy; + S.cfx[o] = fx; + S.cfy[o] = fy; +} + +// One physics step of h seconds, after Cundall and Strack: touching discs +// push apart with a spring and a damper along the line between them and hold +// each other with a tangential spring that slips past Coulomb friction. The +// walls and the floor do the same. The piston is a heavy lid pushed down +// with force S.fp, and each wall plate is pushed in with S.fw. +function step(S, h, drag, keep) { + const { n, x, y, vx, vy, w, r, m, fx, fy, tq, head, next, nb, sg, k, c } = S; + const cs = S.cs, gw = S.gw, gh = S.gh, off = 200, gen = ++S.gen, cd0 = c / (S.rm * 0.7071); + head.fill(-1, 0, gw * gh); + for (let i = 0; i < n; i++) { + fx[i] = 0; + fy[i] = m[i] * G; + tq[i] = 0; + const o = cl(floor((y[i] + off) / cs), 0, gh - 1) * gw + cl(floor((x[i] + 4) / cs), 0, gw - 1); + next[i] = head[o]; + head[o] = i; + } + if (keep) S.cn.fill(0); + for (let gy = 0; gy < gh; gy++) + for (let gx = 0; gx < gw; gx++) + for (let i = head[gy * gw + gx]; i >= 0; i = next[i]) + for (let dy = 0; dy <= 1; dy++) + for (let dx = -1; dx <= 1; dx++) { + if (dy === 0 && dx < 0) continue; + const hx = gx + dx, hy = gy + dy; + if (hx < 0 || hx >= gw || hy >= gh) continue; + for (let j = dy === 0 && dx === 0 ? next[i] : head[hy * gw + hx]; j >= 0; j = next[j]) { + const a = i < j ? i : j, b = i < j ? j : i; + const ddx = x[b] - x[a], ddy = y[b] - y[a], rr = r[a] + r[b], d2 = ddx * ddx + ddy * ddy; + if (d2 >= rr * rr) continue; + const d = sqrt(d2) || 1e-6, nx = ddx / d, ny = ddy / d, ov = rr - d; + const rvx = vx[b] - vx[a], rvy = vy[b] - vy[a], vn = rvx * nx + rvy * ny; + const vt = -rvx * ny + rvy * nx - w[b] * r[b] - w[a] * r[a]; + const cd = cd0 * sqrt((m[a] * m[b]) / (m[a] + m[b])); + const fs = k * ov, fn = max(0, fs - cd * vn), ft = fric(S, slot(S, a, b), vt, fn, h, cd); + const Fx = fn * nx - ft * ny, Fy = fn * ny + ft * nx; + fx[b] += Fx; + fy[b] += Fy; + fx[a] -= Fx; + fy[a] -= Fy; + tq[a] -= r[a] * ft; + tq[b] -= r[b] * ft; + if (keep) { + const Kx = fs * nx - ft * ny, Ky = fs * ny + ft * nx; + keepC(S, a, nx, ny, -Kx, -Ky); + keepC(S, b, -nx, -ny, Kx, Ky); + } + } + } + // Walls: m is the direction from the disc to the wall, the wall moves at + // (wx, wy). + let up = 0; + const yp = S.yp, wi = S.wi, wv = S.wv, wf = S.wf; + wf.fill(0); + const wall = (i, id, ov, mx, my, wx, wy) => { + const ri = r[i], cd = cd0 * sqrt(m[i]), rvx = vx[i] - wx, rvy = vy[i] - wy; + const vn = rvx * mx + rvy * my, vt = -rvx * my + rvy * mx + w[i] * ri; + const fs = k * ov, fn = max(0, fs + cd * vn), ft = fric(S, slot(S, i, id), vt, fn, h, cd); + fx[i] += -fn * mx - ft * my; + fy[i] += -fn * my + ft * mx; + tq[i] += ri * ft; + if (keep) keepC(S, i, mx, my, -fs * mx - ft * my, -fs * my + ft * mx); + return fn; + }; + for (let i = 0; i < n; i++) { + const ri = r[i]; + const sl = cl(floor((y[i] * NS) / H), 0, NS - 1), sr = sl + NS; + let ov = ri - (x[i] - wi[sl]); + if (ov > 0) wf[sl] += wall(i, WALL, ov, -1, 0, wv[sl], 0); + ov = ri - (W - wi[sr] - x[i]); + if (ov > 0) wf[sr] += wall(i, WALL + 1, ov, 1, 0, -wv[sr], 0); + ov = ri - (H - y[i]); + if (ov > 0) wall(i, WALL + 2, ov, 0, 1, 0, 0); + ov = ri - (y[i] - yp); + if (ov > 0) up += wall(i, WALL + 3, ov, 0, -1, 0, S.vp); + } + // Springs whose contact broke this step are let go. + for (let o = 0, e = n * MC; o < e; o++) if (nb[o] >= 0 && sg[o] !== gen) nb[o] = -1; + const dmp = 1 - drag * h; + for (let i = 0; i < n; i++) { + vx[i] = (vx[i] + (fx[i] / m[i]) * h) * dmp; + vy[i] = (vy[i] + (fy[i] / m[i]) * h) * dmp; + w[i] = (w[i] + ((2 * tq[i]) / (m[i] * r[i] * r[i])) * h) * dmp; + x[i] += vx[i] * h; + y[i] += vy[i] * h; + } + const pd = 1 - PD * h; + S.vp = (S.vp + ((S.fp - up) / S.mp) * h) * pd; + S.yp += S.vp * h; + // The wall plates move inward at wv and halt at their stops. The top plate + // also holds the pile above the panel's top edge. + const mw = S.mp / NS; + for (let k = 0; k < 2 * NS; k++) { + const f = S.fw * (k < NS ? 1 + S.lean : 1 - S.lean) * (k % NS ? 1 : 1 - (yp * NS) / H); + wv[k] = (wv[k] + ((f - wf[k]) / mw) * h) * pd; + wi[k] += wv[k] * h; + if (wi[k] < -WOUT) (wi[k] = -WOUT), (wv[k] = max(0, wv[k])); + if (wi[k] > WIN) (wi[k] = WIN), (wv[k] = min(0, wv[k])); + } +} + +// The pushes at point q of the round, 0 with the piston pressing hardest and +// 1 with the walls pressing hardest. A pressure of 1 is the weight of the +// pile spread over the floor. +function press(S, q) { + const p = (S.load * S.mg) / W; + S.fp = p * (SY0 + (SY1 - SY0) * q) * (W - S.wi[0] - S.wi[NS]); + S.fw = (p * (SX0 + (SX1 - SX0) * q) * H) / NS; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + const beta = cl(kv[0], 0, 1) * PI * 0.5; + if (abs(beta - S.beta) > 0.004) buildLut(S, beta); + S.speed = cl(kv[1], 0.2, 3); + S.load = 3 * pow(2, cl(kv[2], -1, 1)); + const grain = cl(kv[3], 0, 1); + if (S.grain < 0 || abs(grain - S.grain) > 0.03) pour(S, grain), (S.u = 0); + const h = 1 / 30 / SUB, steps = max(1, Math.round(d * 30 * SUB)), du = (d * S.speed) / 2 / steps; + for (let i = 0; i < steps; i++) { + S.u = (S.u + du) % (2 * T_R); + // The round: the piston pressing hard with the walls easing off, then the + // walls squeezing while the piston eases, and back. The lean turns over + // once every two rounds. + press(S, 0.5 - 0.5 * cos((2 * PI * S.u) / T_R)); + S.lean = LEAN * sin((PI * S.u) / T_R); + step(S, h, DRAG, i === steps - 1); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { n, x, y, r, cn, cux, cuy, cfx, cfy, lut, buf, cc } = S; + buf.fill(0); + // LUT steps per unit of stress: the weight pressure on the floor makes FR + // of a fringe. + const ks = (L0 * W * FR) / (S.mg * LS); + for (let i = 0; i < n; i++) { + const R = r[i], cx = x[i], cy = y[i], nc = cn[i], R2 = R * R, Ri = (R - 0.7) * (R - 0.7); + for (let k = 0; k < nc; k++) { + const o = i * MC + k; + cc[k * 4] = R * cux[o]; + cc[k * 4 + 1] = R * cuy[o]; + cc[k * 4 + 2] = cfx[o]; + cc[k * 4 + 3] = cfy[o]; + } + const x0 = max(0, floor(cx - R)), x1 = min(W - 1, floor(cx + R)), y0 = max(0, floor(cy - R)), y1 = min(H - 1, floor(cy + R)); + for (let py = y0; py <= y1; py++) + for (let px = x0; px <= x1; px++) { + const ex0 = px + 0.5 - cx, ey0 = py + 0.5 - cy, q = ex0 * ex0 + ey0 * ey0; + if (q >= R2) continue; + let cr = 0, cg = 0, cb = 0; + for (let sub = 0; sub < 4; sub++) { + const ex = ex0 + (sub & 1 ? 0.25 : -0.25), ey = ey0 + (sub & 2 ? 0.25 : -0.25); + let sxx = 0, syy = 0, sxy = 0; + for (let k = 0; k < nc * 4; k += 4) { + const dx = ex - cc[k], dy = ey - cc[k + 1]; + const r2 = dx * dx + dy * dy + A2, wgt = (cc[k + 2] * dx + cc[k + 3] * dy) / (r2 * r2); + sxx += wgt * dx * dx; + syy += wgt * dy * dy; + sxy += wgt * dx * dy; + } + const a = sxx - syy, ds = sqrt(a * a + 4 * sxy * sxy) * (2 / PI); + const li = min(LN - 1, (ds * ks) | 0) * 3; + cr += lut[li]; + cg += lut[li + 1]; + cb += lut[li + 2]; + } + // Four samples a LED, and the rim darkened where the disc's edge bends + // the light away. + const f = (q > Ri ? 0.45 : 1) * 0.25, o = (py * W + px) * 3; + buf[o] = cr * f; + buf[o + 1] = cg * f; + buf[o + 2] = cb * f; + } + } + for (let p = 0, yy = 0; yy < H; yy++) + for (let xx = 0; xx < W; xx++, p += 3) display.setPixel(xx, yy, buf[p], buf[p + 1], buf[p + 2]); +} From 251c274639c1a7042f970a2375ec3f11e7b35103 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 00:19:31 +0000 Subject: [PATCH 06/10] =?UTF-8?q?52=ED=8C=90:=20Zonal=20Jets=20=EC=99=80?= =?UTF-8?q?=20Creaking=20Door?= 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_01LXsMxtcgxEaHMjeqmCbsjv --- .../pattern-study/patterns/creaking-door.js | 454 ++++++++++++++++++ incoming/pattern-study/patterns/zonal-jets.js | 364 ++++++++++++++ 2 files changed, 818 insertions(+) create mode 100644 incoming/pattern-study/patterns/creaking-door.js create mode 100644 incoming/pattern-study/patterns/zonal-jets.js diff --git a/incoming/pattern-study/patterns/creaking-door.js b/incoming/pattern-study/patterns/creaking-door.js new file mode 100644 index 000000000..32d7fbfbc --- /dev/null +++ b/incoming/pattern-study/patterns/creaking-door.js @@ -0,0 +1,454 @@ +// ===== Patternflow pattern ===== +// Title: Creaking Door +// Family: halloween +// Idea: A door at the end of a purple-dark hallway creaks open in lurches and candlelight from the room behind spills out as a widening wedge across the floorboards and up the left wall, dust glittering in it, while the shadow of a long-fingered hand, or of someone in a pointed hat, reaches across the light from somewhere out of sight; then the door slams with a jolt that shakes the panel and brings dust down from the lintel, and in the dark two feet cross the line of light under the door before it begins again. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 52 +// Round: 52 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Reach=0..1, Haze=0..1 +// +// Knob 1: Hue. The colour of the light in the room. At 0 a candle's, deep red where it is faint and pale gold where it is strong, orange between. At 0.33 toxic green turning towards cyan where it is strong, at 0.67 blue turning violet. The hallway's purple stays. +// Knob 2: Speed. At 2 a round lasts 10 to 11.5 seconds: the door lurches open over 3.4 to 4.8 s to between 54 and 69 degrees, the shadow comes 2.2 to 2.8 s in and stays 4 s, the door slams 6.2 to 7 s in, shut in 0.14 s, and the dark lasts 3.6 to 4.2 s. The first round starts with the door already ajar. At 1 everything takes twice as long, at 0.2 ten times. +// Knob 3: Reach. How far the candle's light carries before it falls to half: 0.9 m (30 LEDs across at the door) at 0, 2.3 m at 1. At 0 the boards near the eye stay deep red and the hallway outside the wedge stays purple; at 1 the wedge burns orange down to the bottom of the panel and the left wall glows gold. +// Knob 4: Haze. Dust in the air. At 0 the light shows only where it lands, with a few motes; at the opening 0.06 a faint glow over the wedge and 140 motes glittering in it; at 1 the beam stands in the air as a glowing wedge with the shadow cut through it. +// Buttons: K1 tap slams the door while it is opening or open; while it is shut, a tap opens it again. + +const W = 64, H = 128, N = W * H; +const { floor, round, min, max, abs, sqrt, sin, cos, exp, pow, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (v) => (v <= 0 ? 0 : v >= 1 ? 1 : v * v * (3 - 2 * v)); +const hash = (i) => { + let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; +const vn = (x, sd) => { + const i = floor(x), f = x - i, u = f * f * (3 - 2 * f); + return (hash(i * 7919 + sd) * (1 - u) + hash((i + 1) * 7919 + sd) * u) * 2 - 1; +}; + +// World in metres: x to the right, y up, z away from the eye. The eye stands +// EYE above the floor and ZC in front of the far wall (z = 0). F is the focal +// length in LEDs, and the horizon sits on row CY. +const EYE = 1.2, ZC = 3, F = 100, CX = 32, CY = 42; +// The hallway is 2 HW wide under a ceiling HC high. The doorway in the far +// wall is 2 DW wide and DH high inside a trim TRIM wide, with a gap GAP +// under the door. +const HW = 0.6, HC = 2.4, DW = 0.45, DH = 2, TRIM = 0.07, GAP = 0.02; +// The room behind is 2 RW wide, RD deep and RH high. The candle stands in it +// at (LX, LY, LZ), round the corner to the right where the eye cannot see it. +const RW = 1.4, RD = 2.6, RH = 2.5, LX = 0.75, LY = 0.85, LZ = 1.4; +// Light strength, and its strength on the room's own walls. +const LI = 8, RI = 2; +// Kinds of LED. +const CEIL = 0, WALL = 1, TRM = 2, LEFT = 3, RIGHT = 4, FLOOR = 5, DOOR = 6; +const NRM = [[0, -1, 0], [0, 0, -1], [0, 0, -1], [1, 0, 0], [-1, 0, 0], [0, 1, 0]]; + +function setup0(S) { + const g = (S.geo = { + kind: new Uint8Array(N), P: new Float32Array(N * 3), amb: new Float32Array(N * 3), alb: new Float32Array(N), + d: new Float32Array(N * 2), R: new Float32Array(N * 3), rn: new Float32Array(N * 3), ramb: new Float32Array(N * 3), tz: new Float32Array(N), + }); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, dx = (x + 0.5 - CX) / F, dy = -(y + 0.5 - CY) / F; + // The nearest of the hallway's six faces along the ray from the eye. + let t = ZC, k = WALL; + if (dy < 0 && -EYE / dy < t) (t = -EYE / dy), (k = FLOOR); + if (dy > 0 && (HC - EYE) / dy < t) (t = (HC - EYE) / dy), (k = CEIL); + if (dx < 0 && -HW / dx < t) (t = -HW / dx), (k = LEFT); + if (dx > 0 && HW / dx < t) (t = HW / dx), (k = RIGHT); + const X = dx * t, Y = EYE + dy * t, Z = -ZC + t; + if (k === WALL && abs(X) < DW && Y < DH) k = DOOR; + else if (k === WALL && abs(X) < DW + TRIM && Y < DH + TRIM) k = TRM; + g.kind[p] = k; + g.P[p * 3] = X; + g.P[p * 3 + 1] = Y; + g.P[p * 3 + 2] = Z; + g.d[p * 2] = dx; + g.d[p * 2 + 1] = dy; + g.tz[p] = Z; + let r = 0, gg = 0, b = 0, a = 1; + if (k === FLOOR) { + // Boards 0.12 wide running away from the eye. + const bi = floor((X + 3) / 0.12), u = (X + 3) / 0.12 - bi; + const seam = u < 0.14 ? 0.55 : 1, tone = 0.85 + 0.3 * hash(bi * 13); + r = 26 * seam * tone; + gg = 11 * seam * tone; + b = 30 * seam * tone; + a = 0.85 * seam * tone; + } else if (k === CEIL) (r = 14), (gg = 6), (b = 22), (a = 0.8); + else if (k === TRM) (r = 54), (gg = 30), (b = 58), (a = 1); + else { + // Wallpaper in stripes above the dado rail, panelling below it. + const u = k === WALL ? X : Z, st = 0.5 + 0.5 * cos(u * 2 * PI * 7), lo = Y < 0.9, rail = abs(Y - 0.9) < 0.03 || Y < 0.1; + const f = rail ? 1.12 : lo ? 0.8 : 0.85 + 0.25 * st; + r = 36 * f; + gg = 14 * f; + b = 50 * f; + a = f; + } + g.amb[p * 3] = r; + g.amb[p * 3 + 1] = gg; + g.amb[p * 3 + 2] = b; + g.alb[p] = a; + if (k === DOOR) { + // Where the ray lands in the room if the door is not in the way. + let tr = 1e9, kk = 0; + const c = [[0, -1, 0], [0, 1, 0], [-1, 0, 0], [1, 0, 0], [0, 0, -1]]; + const ts = [dy > 0 ? (RH - EYE) / dy : 1e9, dy < 0 ? -EYE / dy : 1e9, dx > 0 ? RW / dx : 1e9, dx < 0 ? -RW / dx : 1e9, ZC + RD]; + for (let i = 0; i < 5; i++) if (ts[i] > ZC && ts[i] < tr) (tr = ts[i]), (kk = i); + const RX = dx * tr, RY = EYE + dy * tr, RZ = -ZC + tr; + g.R[p * 3] = RX; + g.R[p * 3 + 1] = RY; + g.R[p * 3 + 2] = RZ; + g.rn[p * 3] = c[kk][0]; + g.rn[p * 3 + 1] = c[kk][1]; + g.rn[p * 3 + 2] = c[kk][2]; + // A picture on the back wall; plain plaster elsewhere. + const pic = kk === 4 && abs(RX + 0.1) < 0.35 && abs(RY - 1.45) < 0.3, fr = pic && (abs(RX + 0.1) > 0.29 || abs(RY - 1.45) > 0.24); + const f = fr ? 0.35 : pic ? 0.6 : kk === 1 ? 0.7 : 1; + g.ramb[p * 3] = 40 * f; + g.ramb[p * 3 + 1] = 18 * f; + g.ramb[p * 3 + 2] = 40 * f; + } + } +} + +// The door turns by th about its hinge at the left jamb, into the room. +// Does the segment from A to the candle cross it? +function doorBlocks(S, ax, ay, az) { + const s = S.sn, c = S.cs, na = s * (ax + DW) - c * az, nb = s * (LX + DW) - c * LZ; + if ((na > 0) === (nb > 0)) return false; + const t = na / (na - nb), X = ax + (LX - ax) * t, Y = ay + (LY - ay) * t, Z = az + (LZ - az) * t, u = (X + DW) * c + Z * s; + return u >= 0 && u <= 2 * DW && Y >= GAP && Y <= DH; +} + +// Light at P from the candle, 0 when something is in the way. Points in the +// hallway see the candle only through the doorway, and there the shadow +// mask (the hand, or the figure) can stand in it. A door ajar by less than +// 0.03 rad counts as shut: the slit would light a dotted line. +function vis(S, x, y, z) { + if (z < 0) { + if (S.th < 0.03) return 0; + const k = -z / (LZ - z), qx = x + (LX - x) * k, qy = y + (LY - y) * k; + if (abs(qx) >= DW || qy <= 0 || qy >= DH) return 0; + if (doorBlocks(S, x, y, z)) return 0; + return 1 - mask(S, qx, qy); + } + return doorBlocks(S, x, y, z) ? 0 : 1; +} + +// The shadow, kept as the shape it throws on the boards: a grid over the +// hallway floor. A ray from the candle through the doorway is in shadow when +// the floor it would reach is dark, so the haze shows the same shadow as the +// floor does. +const MS = 0.02, MW = 60, MH = 80; +function mask(S, qx, qy) { + if (qy >= LY * 0.99) return 0; + const m = LY / (LY - qy), i = floor((LX + (qx - LX) * m + HW) / MS), j = floor((LZ * (m - 1)) / MS); + return j < MH && i >= 0 && i < MW ? S.mk[j * MW + i] : 0; +} +// A round-ended stroke from (ax, az) to (bx, bz) on the floor, r wide. The +// shapes are drawn upright and sheared along the light, which crosses the +// boards leaning SH to the left for every metre it comes towards the eye. +const SH = 0.3; +function capsule(S, ax, az, bx, bz, r) { + (ax += SH * az), (bx += SH * bz); + const M = S.mk, x0 = max(0, floor((min(ax, bx) - r - 0.03 + HW) / MS)), x1 = min(MW - 1, floor((max(ax, bx) + r + 0.03 + HW) / MS)); + const y0 = max(0, floor((-max(az, bz) - r - 0.03) / MS)), y1 = min(MH - 1, floor((-min(az, bz) + r + 0.03) / MS)); + const ex = bx - ax, ey = bz - az, ee = ex * ex + ey * ey || 1; + for (let j = y0; j <= y1; j++) + for (let i = x0; i <= x1; i++) { + const px = (i + 0.5) * MS - HW, py = -(j + 0.5) * MS, t = cl(((px - ax) * ex + (py - az) * ey) / ee, 0, 1); + const d = sqrt((px - ax - ex * t) ** 2 + (py - az - ey * t) ** 2), v = cl((r - d) / 0.03 + 0.5, 0, 1); + if (v > M[j * MW + i]) M[j * MW + i] = v; + } +} +// A long-fingered hand lying in the light: the wrist at (wx, wz), the +// fingers reaching towards the eye, turned by a, curled by c. +function hand(S, wx, wz, a, c) { + const ca = cos(a), sa = sin(a), P = (u, v) => [wx + u * ca + v * sa, wz - v * ca + u * sa]; + capsule(S, ...P(0, -0.6), ...P(0, 0), 0.1); + capsule(S, ...P(-0.03, 0.03), ...P(0, 0.16), 0.15); + for (let f = 0; f < 4; f++) { + let [px, pz] = P(-0.15 + f * 0.1, 0.24), an = -PI / 2 + a + (f - 1.5) * 0.12; + const len = [0.19, 0.24, 0.23, 0.17][f], wig = 0.12 * sin(S.t * 5 + f * 1.7) * (1 - c); + for (let seg = 0; seg < 3; seg++) { + an += (c * (0.3 + 0.15 * seg) * (1.5 - f) * 0.5 + wig * (f < 2 ? 1 : -1)) * 0.7; + const nx = px + cos(an) * len * (1 - 0.45 * c), nz = pz + sin(an) * len * (1 - 0.45 * c); + capsule(S, px, pz, nx, nz, 0.045 - seg * 0.009); + (px = nx), (pz = nz); + } + } + const t0 = P(0.17, 0.05), t1 = P(0.31, 0.19); + capsule(S, ...t0, ...t1, 0.045); + capsule(S, ...t1, ...P(0.35, 0.33), 0.035); +} +// Someone in a pointed hat crossing the room between the candle and the +// door: a long shadow from the threshold, the legs walking, the hat nearest +// the eye. +function figure(S, x, walk) { + const l = 0.07 * sin(walk); + capsule(S, x - 0.05 - l, 0.15, x - 0.05, -0.36, 0.05); + capsule(S, x + 0.05 + l, 0.15, x + 0.05, -0.36, 0.05); + capsule(S, x, -0.38, x, -0.6, 0.13); + capsule(S, x - 0.06, -0.66, x + 0.06, -0.66, 0.11); + capsule(S, x, -0.76, x, -0.84, 0.085); + capsule(S, x - 0.17, -0.92, x + 0.17, -0.92, 0.035); + for (let i = 0; i < 6; i++) capsule(S, x - 0.006 * i * i, -0.95 - 0.06 * i, x - 0.006 * (i + 1) * (i + 1), -1.01 - 0.06 * i, 0.08 - 0.013 * i); +} + +// One round of the door. Times in seconds at Speed 2. +function plan(S, first) { + const r = Math.random; + // Now and then the door gets only half way before it slams. + const part = !first && r() < 0.3; + S.cy = { + t0: first ? 0.8 : 0, open0: 0.8, dur: part ? 2.4 : 3.4 + r() * 1.4, thMax: part ? 0.5 + r() * 0.15 : 0.95 + r() * 0.25, kind: S.round % 2, + steps: [0, 1, 2, 3, 4].map(() => r()).sort(), sh0: 2.2 + r() * 0.6, slam: part ? 4.6 + r() * 0.6 : 6.2 + r() * 0.8, + }; + S.cy.end = S.cy.slam + 3.6 + r() * 0.6; + S.ct = S.cy.t0; + S.round++; +} +// The angle as the door creaks open: five lurches with sticking between. +function creak(c, u) { + let a = 0; + for (let i = 0; i < 5; i++) a += sm((u - c.steps[i] * 0.85) / 0.12) * 0.2; + return 0.12 + (c.thMax - 0.12) * (0.25 * u + 0.75 * a); +} + +export function setup(params) { + const S = (params.s = { t: 0, ct: 0, round: 0, hue: 0, speed: 2, reach: 0, haze: 0.06, th: 0.12, sn: 0, cs: 1, slamT: -9, shake: [0, 0], flick: 1, D0: -1, feet: -1, peep: true, + mk: new Float32Array(MW * MH), lf: new Float32Array(N), buf: new Float32Array(N * 3), hz: new Float32Array(32 * 64), motes: [], falls: [] }); + setup0(S); + plan(S, true); + for (let i = 0; i < 140; i++) S.motes.push({ x: (Math.random() * 2 - 1) * HW, y: Math.random() * HC, z: -Math.random() * 1.6, ph: Math.random() * 6.3, f: 0.5 + Math.random() }); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + S.hue = cl(k[0], 0, 1); + S.speed = cl(k[1], 0.2, 3); + S.reach = cl(k[2], 0, 1); + S.haze = cl(k[3], 0, 1); + const ft = cl(dt || 0, 0, 0.1), st = (ft * S.speed) / 2, c = S.cy; + S.t += st; + S.ct += st; + if (input.btnPressed && input.btnPressed[0]) { + if (S.ct < c.slam && S.ct > c.open0) c.slam = S.ct; + else if (S.ct > c.slam + 0.3) S.ct = c.end + 1; + } + if (S.ct > c.end) { + plan(S, false); + if (input.btnPressed && input.btnPressed[0]) S.ct = S.cy.open0; + } + const ct = S.ct, cc = S.cy; + // The door's angle. + let th = 0; + if (ct < cc.open0) th = 0; + else if (ct < cc.slam) th = creak(cc, cl((ct - cc.open0) / cc.dur, 0, 1)); + else { + const e = ct - cc.slam, a0 = creak(cc, cl((cc.slam - cc.open0) / cc.dur, 0, 1)); + th = e < 0.14 ? a0 * (1 - (e / 0.14) ** 2) : 0.025 * exp(-(e - 0.14) * 9) * abs(sin((e - 0.14) * 30)); + if (e >= 0.14 && S.slamT < cc.slam) { + S.slamT = cc.slam; + for (let i = 0; i < 28; i++) S.falls.push({ x: (Math.random() * 2 - 1) * (DW + TRIM), y: DH + TRIM + Math.random() * 0.05, z: -0.01 - Math.random() * 0.1, v: -Math.random() * 2.5, m: 0.2 + Math.random() * 0.45, a: 1.3 + Math.random(), w: Math.random() * 6 }); + } + } + S.th = th; + S.sn = sin(th); + S.cs = cos(th); + // Shake after the slam. + const e = ct - cc.slam - 0.14; + if (e > 0 && e < 0.8) { + const A = 3 * exp(-e * 3.5); + S.shake = [round(A * sin(e * 83)), round(0.6 * A * sin(e * 61 + 1))]; + } else S.shake = [0, 0]; + // After the slam two feet cross the light under the door, right to left, + // and a while later the keyhole goes dark: an eye on the other side. + S.feet = e > 1 && e < 2.8 ? (e - 1) / 1.8 : -1; + S.peep = !(e > 3 && e < 3.5); + // Candle flicker. + S.flick = 1 + 0.13 * vn(S.t * 9, 3) + 0.06 * vn(S.t * 23, 5); + // The shadow. + S.mk.fill(0); + const sh = ct - cc.sh0; + if (sh > 0 && sh < 4) { + const u = sh / 4; + if (cc.kind === 0) { + const reach = sm(u / 0.4) - 1.5 * sm((u - 0.78) / 0.22); + hand(S, 0.05 + 0.08 * sin(sh * 0.9), 0.6 - 1.1 * reach, 0.12 * sin(sh * 1.3), 0.75 * sm((u - 0.45) / 0.2) - 0.75 * sm((u - 0.72) / 0.1)); + } else figure(S, 0.75 - 1.3 * u, sh * 7); + } + // Dust falling from the lintel after the slam. + for (const f of S.falls) { + f.v = min(f.m, f.v + 1.5 * st); + f.y -= max(0, f.v) * st; + f.x += 0.04 * sin(S.t * 3 + f.w) * st; + f.z -= 0.03 * st; + f.a -= st * 0.45; + } + S.falls = S.falls.filter((f) => f.y > 0 && f.a > 0); + for (const m of S.motes) { + m.x += 0.03 * vn(S.t * 0.5 + m.ph * 10, 1) * st; + m.y += (0.02 * vn(S.t * 0.4 + m.ph * 7, 2) - 0.01) * st; + if (m.y < 0) m.y += HC; + if (abs(m.x) > HW) m.x = -m.x * 0.98; + } +} + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + o[1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + o[2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; +} +// Warm light by amount: deep red when faint, orange, then pale gold, so it +// never sinks to brown. +const RAMP = 256, RMAX = 2; +function ramp(S) { + if (S.rh === S.hue) return; + S.rh = S.hue; + const T = (S.rt = S.rt || new Float32Array(RAMP * 3)), o = [0, 0, 0]; + for (let j = 0; j < RAMP; j++) { + const w = (j / (RAMP - 1)) * RMAX; + hsv(S.hue + 0.11 * min(1, w), 1 - 0.55 * cl(w - 0.9, 0, 1), pow(min(1, w * 1.6), 0.7), o); + T[j * 3] = o[0]; + T[j * 3 + 1] = o[1]; + T[j * 3 + 2] = o[2]; + } +} + +// 0 where a foot stands in the gap under the door, 1 elsewhere. +function feet(S, x, d) { + if (S.feet < 0) return 1; + const r = 0.05 + 0.3 * d; + const u = S.feet, step = u * 5, a = DW + 0.15 - (2 * DW + 0.3) * (floor(step) + sm((step % 1) * 2)) / 5; + const b = DW + 0.15 - (2 * DW + 0.3) * (floor(step + 0.5) + sm(((step + 0.5) % 1) * 2) - 0.5) / 5 + 0.13; + return abs(x - a) < r || abs(x - b) < r ? 0.08 : 1; +} + +// Candle light at a point facing (nx, ny, nz); nx = 2 for a mote or a speck +// of haze, which faces every way. +function lightAt(S, x, y, z, nx, ny, nz) { + const lx = LX - x, ly = LY - y, lz = LZ - z, d2 = lx * lx + ly * ly + lz * lz, d = sqrt(d2), lam = nx === 2 ? 1 : (nx * lx + ny * ly + nz * lz) / d; + if (lam <= 0) return 0; + return lam / (1 + d2 / (S.D * S.D)); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + ramp(S); + const g = S.geo, T = S.rt, buf = S.buf, lf = S.lf, I = LI * S.flick; + S.D = 0.9 * pow(2.6, S.reach); + if (S.D0 !== S.D) { + S.D0 = S.D; + for (let p = 0; p < N; p++) { + const k = g.kind[p]; + if (k === DOOR) continue; + const n = NRM[k]; + lf[p] = lightAt(S, g.P[p * 3], g.P[p * 3 + 1], g.P[p * 3 + 2], n[0], n[1], n[2]) * g.alb[p]; + } + } + const s = S.sn, c = S.cs, shut = S.th < 0.03; + // Haze: light scattered towards the eye along each ray, at half size. + const hz = S.hz, hk = 0.02 + 0.3 * S.haze * S.haze; + for (let j = 0; j < 64; j++) + for (let i = 0; i < 32; i++) { + const p = (j * 2) * W + i * 2, dx = g.d[p * 2], dy = g.d[p * 2 + 1], tz = min(0, g.tz[p]); + let a = 0; + if (!shut) { + const t0 = 1.2, t1 = tz + ZC, n = 8, h = (t1 - t0) / n; + for (let q = 0; q < n; q++) { + const t = t0 + (q + 0.5) * h, x = dx * t, y = EYE + dy * t, z = -ZC + t; + if (z > -1.8) a += vis(S, x, y, z) * lightAt(S, x, y, z, 2, 0, 0) * h; + } + } + hz[j * 32 + i] = a * hk; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, k = g.kind[p]; + let w = 0, ar = g.amb[p * 3], ag = g.amb[p * 3 + 1], ab = g.amb[p * 3 + 2]; + if (k === DOOR) { + const dx = g.d[p * 2], dy = g.d[p * 2 + 1], nC = s * DW + c * ZC, nd = s * dx - c, t = -nC / nd; + let hit = false; + if (t > ZC - 1e-6 && t < 1e8) { + const X = dx * t, Y = EYE + dy * t, Z = -ZC + t, u = (X + DW) * c + Z * s; + if (u >= 0 && u <= 2 * DW && Y >= GAP && Y <= DH && Z <= g.R[p * 3 + 2]) { + hit = true; + // The door's face: stiles, rails and four sunk panels. + const pu = u / (2 * DW), pv = Y / DH, inP = pu > 0.13 && pu < 0.87 && ((pv > 0.08 && pv < 0.43) || (pv > 0.52 && pv < 0.93)) && abs(pu - 0.5) > 0.04; + const edge = inP && (pu < 0.16 || pu > 0.84 || abs(pu - 0.5) < 0.07 || abs(pv - 0.08) < 0.02 || abs(pv - 0.43) < 0.02 || abs(pv - 0.52) < 0.02 || abs(pv - 0.93) < 0.02); + const knob = (pu - 0.88) ** 2 * 4 + (pv - 0.48) ** 2 < 0.0004, key = shut && S.peep && abs(pu - 0.88) < 0.012 && abs(pv - 0.44) < 0.012; + const f = knob ? 1.8 : edge ? 0.55 : inP ? 1.12 : 1; + ar = 42 * f; + ag = 20 * f; + ab = 38 * f; + w = lightAt(S, X, Y, Z, s, 0, -c) * 0.7 * f; + if (key) w = 0.8 * S.flick; + } + } + if (!hit) { + const R = g.R, X = R[p * 3], Y = R[p * 3 + 1], Z = R[p * 3 + 2]; + ar = g.ramb[p * 3]; + ag = g.ramb[p * 3 + 1]; + ab = g.ramb[p * 3 + 2]; + w = shut ? 0 : (doorBlocks(S, X, Y, Z) ? 0.08 : (lightAt(S, X, Y, Z, g.rn[p * 3], g.rn[p * 3 + 1], g.rn[p * 3 + 2]) * RI) / LI) * (ar / 40); + if (Y < GAP * 2 && shut) w = 0.9 * feet(S, X, 0); + } + w *= I; + } else if (lf[p] > 0) w = lf[p] * I * vis(S, g.P[p * 3], g.P[p * 3 + 1], g.P[p * 3 + 2]); + // Light leaking under a shut door onto the boards just in front. + if (k === FLOOR && shut && g.P[p * 3 + 2] > -0.3 && abs(g.P[p * 3]) < DW + 0.1) { + const z = g.P[p * 3 + 2], e = 1 + z / 0.3; + w += 0.55 * S.flick * e * e * cl((DW + 0.1 - abs(g.P[p * 3])) / 0.1, 0, 1) * feet(S, g.P[p * 3], -z); + } + // The haze, laid back over the full panel between its four nearest. + const hy = cl((y - 0.5) / 2, 0, 62.999), hx = cl((x - 0.5) / 2, 0, 30.999), iy = floor(hy), ix = floor(hx), fy = hy - iy, fx = hx - ix, q = iy * 32 + ix; + w += (hz[q] * (1 - fx) * (1 - fy) + hz[q + 1] * fx * (1 - fy) + hz[q + 32] * (1 - fx) * fy + hz[q + 33] * fx * fy) * I; + const j = 3 * min(RAMP - 1, round((w / RMAX) * (RAMP - 1))), keep = max(0, 1 - 2 * w); + buf[p * 3] = ar * keep + T[j]; + buf[p * 3 + 1] = ag * keep + T[j + 1]; + buf[p * 3 + 2] = ab * keep + T[j + 2]; + } + // Dust in the beam. + for (const m of shut ? [] : S.motes) { + const v = vis(S, m.x, m.y, m.z); + if (!v) continue; + const t = m.z + ZC, x = floor(CX + (F * m.x) / t), y = floor(CY - (F * (m.y - EYE)) / t); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const a = lightAt(S, m.x, m.y, m.z, 2, 0, 0) * I * (0.5 + 0.5 * sin(S.t * 6 * m.f + m.ph * 9)) * (0.5 + 2 * S.haze), p = (y * W + x) * 3; + buf[p] += 255 * a; + buf[p + 1] += 170 * a; + buf[p + 2] += 90 * a; + } + for (const f of S.falls) { + const t = f.z + ZC, x = floor(CX + (F * f.x) / t), y = floor(CY - (F * (f.y - EYE)) / t); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const p = (y * W + x) * 3, a = min(1, f.a) * (0.6 + 0.4 * sin(S.t * 7 + f.w * 5)); + buf[p] += 90 * a; + buf[p + 1] += 78 * a; + buf[p + 2] += 110 * a; + } + const [ox, oy] = S.shake; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const sx = x - ox, sy = y - oy; + if (sx < 0 || sx >= W || sy < 0 || sy >= H) { + display.setPixel(x, y, 0, 0, 0); + continue; + } + const p = (sy * W + sx) * 3; + display.setPixel(x, y, min(252, round(buf[p] / 4) * 4), min(252, round(buf[p + 1] / 4) * 4), min(252, round(buf[p + 2] / 4) * 4)); + } +} diff --git a/incoming/pattern-study/patterns/zonal-jets.js b/incoming/pattern-study/patterns/zonal-jets.js new file mode 100644 index 000000000..3444dce60 --- /dev/null +++ b/incoming/pattern-study/patterns/zonal-jets.js @@ -0,0 +1,364 @@ +// ===== Patternflow pattern ===== +// Title: Zonal Jets +// Family: wave +// Idea: Two-dimensional turbulence stirred in small eddies on a beta plane, solved on the panel's own 64x128 grid, gathers its energy into east-west jets stacked up the panel, eastward flow red to pale amber and westward violet to sky blue, while the stream lines drawn over them close into cat's eyes between the jets and slide west as Rossby waves, three times faster than the jets carry the water east; beta sets how many jets stand, through the Rhines scale. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: beta-plane zonal jets and the Rhines scale (Peter Rhines, 1975), Rossby waves (Carl-Gustaf Rossby, 1939), pattern round 52 +// Round: 52 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Beta=-1..1, Stir=0..1 +// +// Knob 1: Hue. Turns both colours round the wheel together. At 0 eastward flow runs from red through orange to pale amber and westward flow from violet through blue to pale sky blue, dark where the water is still. At 0.33 the eastward flow is green and the westward crimson to pink; at 0.67 blue and lime. +// Knob 2: Speed. Flow time per second. At 2 the jet cores carry the water east at about 7 LEDs a second, and the pattern of stream lines on them slides west at about 20. At 1 half as fast, at 0.2 a tenth. Above 2 a frame may take a third step; when the flow is too fast for the steps a frame may take, it runs slower instead. +// Knob 3: Beta. How fast the Coriolis parameter grows northward, 9 at 0, from 2.5 at -1 to 32 at 1. At -1 two eastward jets stand on the panel, now and then three, with big eddies between them; at 0 three or four; at 1 four to six narrow ones, and the stream lines slide west at about 48 LEDs a second. Turning it by more than 0.2 starts a fresh run at the new beta. +// Knob 4: Stir. How hard the small eddies that feed the jets are stirred, at a wavelength of 6.4 LEDs, by a factor of 12 from one end to the other. At 0 and at the opening 0.06 the jets run smooth between round eddies; at 1 the bands fill with grainy small eddies and the jets fray. +// Buttons: K1 tap starts a fresh run. + +const W = 64, H = 128; +const { floor, min, max, exp, sqrt, abs, cos, sin, PI, log2, pow, round } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The panel is a periodic box 2 pi wide and 4 pi tall, x to the east and y to +// the north (up the panel), solved pseudo-spectrally the way Inverse Cascade +// solves it, with Orszag's two-thirds rule and third-order Adams-Bashforth +// steps of changing length. The vorticity q obeys +// dq/dt + J(psi, q) + beta dpsi/dx = stirring - MU q - NU (k / kmax)^8 q +// The beta term is linear like the friction, so it goes into the integrating +// factor with them: each mode turns round by beta kx / k^2 per unit time, +// the westward drift of a Rossby wave. +// Flow time per panel second at Speed 2, and the Courant number each step +// keeps to (with |u| + |v|). At 1.0 the smallest modes flare up now and +// then; at 0.9 they do not. +const RATE = 0.7, CFL = 0.9; +// Hyperviscosity at the cut-off wavenumber, per unit time. At 12 the +// enstrophy piled up near the cut-off in bursts, patches of ripples three +// LEDs long; at 80 it drains away. +const NU = 80; +// White-in-time stirring on a ring of wavenumbers KF +- 1, a wavelength of +// 6.4 LEDs, injecting energy EPS per unit time at Stir 0.06; the knob scales +// it by STR per unit of knob. Linear drag MU. +const KF = 10, EPS = 0.003, STR = 12, MU = 0.003; +// beta at Beta 0, and the factor one unit of the knob multiplies it by. +const BM = 9, BR = 3.6; +// A run starts from noise round wavenumber K0 with energy E0 on a grid half +// as fine (32 x 64, stirred at KF0). It runs there for PRE units of flow +// time before the first frame and is then carried over mode for mode to the +// panel grid, so the jets already stand when the panel lights. +const KF0 = 7, K0 = 5, E0 = 1, PRE = 15; +// The colour is the zonal velocity u with only AL of its departure from the +// row's mean, so the eddies tint the bands without breaking them; UN of its +// largest size reaches full colour. +const AL = 0.4, UN = 0.7, LUT = 512; +// Stream lines: BANDS per largest size of psi, cut DEP deep with a cos^LP +// profile so they stay thin. +const BANDS = 4, DEP = 0.6, LP = 3; +// The hue of the strongest eastward flow at Hue 0 and how far the weak flow +// turns from it; the brightness floor and curve. +const HE = 0.12, HS = 0.15, V0 = 0.25, GAM = 0.5; +// Crossfade into a fresh run, panel seconds. +const FADE = 0.6; + +// ---- radix-2 FFT, in place on (re, im) at off, off + st, off + 2 st, ... +function plan(n) { + const lg = log2(n), rev = new Uint16Array(n), c = new Float64Array(n >> 1), s = new Float64Array(n >> 1); + for (let i = 0; i < n; i++) { + let r = 0; + for (let b = 0; b < lg; b++) r |= ((i >> b) & 1) << (lg - 1 - b); + rev[i] = r; + } + for (let i = 0; i < n >> 1; i++) (c[i] = cos((2 * PI * i) / n)), (s[i] = sin((2 * PI * i) / n)); + return { n, rev, c, s, br: new Float64Array(n), bi: new Float64Array(n) }; +} +function fft1(P, re, im, off, st, sign) { + const { n, rev, c, s, br, bi } = P; + for (let i = 0; i < n; i++) { + const j = off + rev[i] * st; + br[i] = re[j]; + bi[i] = im[j]; + } + for (let size = 2; size <= n; size <<= 1) { + const half = size >> 1, step = n / size; + for (let a0 = 0; a0 < n; a0 += size) + for (let k = 0, t = 0; k < half; k++, t += step) { + const wr = c[t], wi = sign * s[t], a = a0 + k, b = a + half; + const xr = br[b] * wr - bi[b] * wi, xi = br[b] * wi + bi[b] * wr; + br[b] = br[a] - xr; + bi[b] = bi[a] - xi; + br[a] += xr; + bi[a] += xi; + } + } + for (let i = 0; i < n; i++) (re[off + i * st] = br[i]), (im[off + i * st] = bi[i]); +} +// The y transforms run only on the columns that survive the cut. +function fft2(G, re, im, sign) { + const { w, h, cols, px, py } = G; + if (sign < 0) for (let y = 0; y < h; y++) fft1(px, re, im, y * w, 1, sign); + for (let t = 0; t < cols.length; t++) fft1(py, re, im, cols[t], w, sign); + if (sign > 0) for (let y = 0; y < h; y++) fft1(px, re, im, y * w, 1, sign); +} + +// One grid over the box, w x h, stirred at kf. +function grid(w, h, kf) { + const n = w * h, kxc = floor(w / 3), kyc = floor(h / 3), F = () => new Float64Array(n); + const G = { + w, h, n, dx: (2 * PI) / w, px: plan(w), py: plan(h), cols: [], + kx: F(), ky: F(), k2: F(), ik2: F(), lin: F(), neg: new Int32Array(n), idx: new Int32Array(n), + wr: F(), wi: F(), nr: F(), ni: F(), p1r: F(), p1i: F(), p2r: F(), p2i: F(), ar: F(), ai: F(), br: F(), bi: F(), ug: F(), + h1: 0, h2: 0, vmax: 0.4, vs: 0.4, um: 0, Ec: new Map(), + }; + for (let x = 0; x < w; x++) if ((x < w / 2 ? x : w - x) <= kxc) G.cols.push(x); + const keep = [], forced = []; + for (let y = 0; y < h; y++) + for (let x = 0; x < w; x++) { + const i = y * w + x, jx = x < w / 2 ? x : x - w, jy = y < h / 2 ? y : y - h, k2 = jx * jx + (jy * jy) / 4; + G.kx[i] = jx; + G.ky[i] = jy / 2; + G.k2[i] = k2; + G.ik2[i] = k2 > 0 ? 1 / k2 : 0; + G.neg[i] = ((h - y) % h) * w + ((w - x) % w); + // where the same mode sits on the panel grid + G.idx[i] = ((jy + H) % H) * W + ((jx + W) % W); + const q = k2 / (kxc * kxc); + G.lin[i] = q * q * q * q; + if (abs(jx) > kxc || abs(jy) > kyc) continue; + keep.push(i); + if (abs(sqrt(k2) - kf) <= 1 && (jx > 0 || (jx === 0 && jy > 0))) forced.push(i); + } + G.keep = Int32Array.from(keep); + G.forced = Int32Array.from(forced); + // A kick of +-A sqrt(h) to both parts of each forced pair adds 4 A^2 h / 3 + // to the sum of |q^|^2, an energy of EPS h at wavenumber kf. + G.famp = n * sqrt((3 * EPS * kf * kf) / (2 * forced.length)); + return G; +} + +// -div(u q) for the spectrum (sr, si), into (or, oi), dealiased. One inverse +// transform brings q and psi to the grid together as q + i psi, the velocity +// comes from fourth-order differences of psi, and one forward transform takes +// u q + i v q back. Leaves q, psi and u on the grid in br, bi, ug. +function tendency(G, sr, si, or, oi) { + const { w, h, n, kx, ky, ik2, keep, neg, ar, ai, br: om, bi: ps, ug } = G, nk = keep.length, mw = w - 1, mh = h - 1; + om.fill(0); + ps.fill(0); + for (let t = 0; t < nk; t++) { + const i = keep[t], r = sr[i] / n, m = si[i] / n, q = ik2[i]; + om[i] = r + q * m; + ps[i] = m - q * r; + } + fft2(G, om, ps, 1); + const c = 1 / (12 * G.dx); + let vmax = 0, um = 0; + for (let y = 0; y < h; y++) { + const o = y * w, u1 = ((y + 1) & mh) * w, d1 = ((y - 1) & mh) * w, u2 = ((y + 2) & mh) * w, d2 = ((y - 2) & mh) * w; + for (let x = 0; x < w; x++) { + const r1 = (x + 1) & mw, l1 = (x - 1) & mw, r2 = (x + 2) & mw, l2 = (x - 2) & mw; + const u = -c * (8 * (ps[u1 + x] - ps[d1 + x]) - (ps[u2 + x] - ps[d2 + x])); + const v = c * (8 * (ps[o + r1] - ps[o + l1]) - (ps[o + r2] - ps[o + l2])); + const q = om[o + x]; + ug[o + x] = u; + ar[o + x] = u * q; + ai[o + x] = v * q; + const sp = abs(u) + abs(v); + if (sp > vmax) vmax = sp; + } + } + fft2(G, ar, ai, -1); + for (let t = 0; t < nk; t++) { + const i = keep[t], j = neg[i], cr = ar[i], ci = ai[i], dr = ar[j], di = ai[j], a = kx[i], b = ky[i]; + or[i] = 0.5 * (a * (ci - di) - b * (cr - dr)); + oi[i] = -0.5 * (a * (cr + dr) + b * (ci + di)); + } + return vmax; +} + +// One AB3 step of length h with the linear terms in a complex integrating +// factor (kept for a few step lengths at a time), then the stirring kick. +function step(G, S, h) { + const { wr, wi, nr, ni, p1r, p1i, p2r, p2i, lin, kx, ik2, keep } = G, nk = keep.length; + G.vmax = tendency(G, wr, wi, nr, ni); + const key = h * 1e6 + S.beta; + let E = G.Ec.get(key); + if (!E) { + if (G.Ec.size > 8) G.Ec.clear(); + E = new Float64Array(2 * G.n); + for (let t = 0; t < nk; t++) { + const i = keep[t], d = exp(-h * (NU * lin[i] + MU)), th = h * S.beta * kx[i] * ik2[i]; + E[2 * i] = d * cos(th); + E[2 * i + 1] = d * sin(th); + } + G.Ec.set(key, E); + } + const h1 = G.h1, h2 = G.h2; + let c0 = h, c1 = 0, c2 = 0; + if (h1 && h2) { + const a = (h * h * h) / 3, b = (h * h) / 2; + c0 = (a + (2 * h1 + h2) * b + h1 * (h1 + h2) * h) / (h1 * (h1 + h2)); + c1 = -(a + (h1 + h2) * b) / (h1 * h2); + c2 = (a + h1 * b) / ((h1 + h2) * h2); + } else if (h1) (c0 = h * (1 + h / (2 * h1))), (c1 = -(h * h) / (2 * h1)); + for (let t = 0; t < nk; t++) { + const i = keep[t], er = E[2 * i], ei = E[2 * i + 1]; + const xr = wr[i] + c0 * nr[i] + c1 * p1r[i] + c2 * p2r[i], xi = wi[i] + c0 * ni[i] + c1 * p1i[i] + c2 * p2i[i]; + wr[i] = er * xr - ei * xi; + wi[i] = er * xi + ei * xr; + let ar = p1r[i], ai = p1i[i]; + p2r[i] = er * ar - ei * ai; + p2i[i] = er * ai + ei * ar; + ar = nr[i]; + ai = ni[i]; + p1r[i] = er * ar - ei * ai; + p1i[i] = er * ai + ei * ar; + } + const A = G.famp * sqrt(h * S.stir), F = G.forced, NG = G.neg; + for (let t = 0; t < F.length; t++) { + const i = F[t], j = NG[i], a = (Math.random() * 2 - 1) * A, b = (Math.random() * 2 - 1) * A; + wr[i] += a; + wi[i] += b; + wr[j] += a; + wi[j] -= b; + } + G.h2 = h1; + G.h1 = h; +} + +// The step: n steps from a ladder 16 to an octave, as long as the Courant +// number allows, so the integrating factors come back. +function ladder(G, adv, most) { + G.vs = max(G.vmax, G.vs * 0.98); + const hmax = (CFL * G.dx) / max(1e-6, G.vs), n = min(most, Math.ceil(adv / hmax)); + return [n, pow(2, floor(16 * log2(min(adv / n, hmax))) / 16)]; +} + +// A fresh run: noise round K0 on the coarse grid, spun up, carried over. +function start(S) { + const C = S.G0, G = S.G, { wr, wi, k2, keep, n } = C; + wr.fill(0); + wi.fill(0); + for (let i = 0; i < n; i++) wr[i] = Math.random() * 2 - 1; + fft2(C, wr, wi, -1); + const on = new Uint8Array(n); + for (let t = 0; t < keep.length; t++) on[keep[t]] = 1; + let e = 0; + for (let i = 0; i < n; i++) { + const q = k2[i] / (K0 * K0), a = on[i] && k2[i] > 0 ? q * exp(-q) : 0; + wr[i] *= a; + wi[i] *= a; + if (k2[i] > 0) e += (wr[i] * wr[i] + wi[i] * wi[i]) / k2[i]; + } + const g = sqrt(E0 / ((0.5 * e) / (n * n))); + for (let i = 0; i < n; i++) (wr[i] *= g), (wi[i] *= g); + C.h1 = C.h2 = 0; + for (let t = 0; t < PRE; ) { + const [, h] = ladder(C, 1, 1); + step(C, S, h); + t += h; + } + const s = G.n / n; + G.wr.fill(0); + G.wi.fill(0); + for (let t = 0; t < keep.length; t++) { + const i = keep[t], j = C.idx[i]; + G.wr[j] = wr[i] * s; + G.wi[j] = wi[i] * s; + } + G.h1 = G.h2 = 0; + G.vs = C.vs; + G.vmax = C.vmax; + tendency(G, G.wr, G.wi, G.nr, G.ni); + S.b0 = S.k3; + S.runs++; +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, k3: 0, b0: 0, beta: BM, stir: 1, runs: 0, fade: 0, usc: 0, psc: 0, palHue: -1, + G: grid(W, H, KF), G0: grid(W / 2, H / 2, KF0), zm: new Float64Array(H), + pal: new Float32Array(2 * LUT * 3), lines: new Float32Array(256), old: new Uint8Array(W * H * 3), cur: new Uint8Array(W * H * 3), + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, G = S.G, k = input.knobValues || [0, 2, 0, 0.06]; + S.hue = cl(k[0], 0, 1); + S.speed = cl(k[1], 0.2, 3); + S.k3 = cl(k[2], -1, 1); + S.beta = BM * pow(BR, S.k3); + S.stir = pow(STR, cl(k[3], 0, 1) - 0.06); + const ft = cl(dt || 0, 0, 0.1); + if (!S.runs) start(S); + else if ((input.btnPressed && input.btnPressed[0]) || abs(S.k3 - S.b0) > 0.2) { + S.old.set(S.cur); + S.fade = 1; + start(S); + } + S.fade = max(0, S.fade - ft / FADE); + const adv = (ft * S.speed * RATE) / 2; + if (adv <= 0) return; + const [n, h] = ladder(G, adv, S.speed > 2 ? 3 : 2); + for (let j = 0; j < n; j++) step(G, S, h); +} + +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; +} + +// Zonal velocity to colour: the upper half of the table is eastward flow, +// red through orange to pale amber, the lower half westward, violet through +// blue to pale sky blue, both dark where the flow is still. +function palette(S) { + if (S.palHue === S.hue) return; + S.palHue = S.hue; + const T = S.pal, c = [0, 0, 0]; + for (let j = 0; j < 2 * LUT; j++) { + const east = j >= LUT, a = east ? (j - LUT) / (LUT - 1) : (LUT - 1 - j) / (LUT - 1); + hsv(S.hue + (east ? HE : HE + 0.42) + (east ? -HS : HS) * pow(1 - a, 1.2), 1 - 0.5 * a * a * a, 1, c); + const v = 255 * (V0 + (1 - V0) * pow(a, GAM)); + T[j * 3] = v * c[0]; + T[j * 3 + 1] = v * c[1]; + T[j * 3 + 2] = v * c[2]; + } + for (let j = 0; j < 256; j++) S.lines[j] = 1 - DEP * pow(0.5 + 0.5 * cos((2 * PI * j) / 256), LP); +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.runs) return; + palette(S); + const G = S.G, { ug, bi: ps } = G, { pal: T, lines: L, zm, cur, old } = S; + let pm = 0, um = 0; + for (let y = 0; y < H; y++) { + let a = 0; + for (let x = 0, o = y * W; x < W; x++) { + a += ug[o + x]; + if (abs(ps[o + x]) > pm) pm = abs(ps[o + x]); + } + zm[y] = a / W; + } + // The scales follow the flow slowly, so a strong eddy does not flash. + S.psc = S.psc ? S.psc + (pm - S.psc) * 0.05 : pm; + if (!S.usc) for (let i = 0; i < G.n; i++) S.usc = max(S.usc, abs(zm[i >> 6] + AL * (ug[i] - zm[i >> 6]))); + const iu = (LUT - 1) / max(1e-6, S.usc * UN), ip = (256 * BANDS) / max(1e-6, S.psc), f = S.fade, g = 1 - f; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x, u = zm[y] + AL * (ug[i] - zm[y]), j = 3 * (LUT + cl(round(u * iu), -LUT, LUT - 1)), l = L[(ps[i] * ip + 1e6) & 255], o = 3 * ((H - 1 - y) * W + x); + if (abs(u) > um) um = abs(u); + cur[o] = T[j] * l; + cur[o + 1] = T[j + 1] * l; + cur[o + 2] = T[j + 2] * l; + } + S.usc = S.usc ? S.usc + (um - S.usc) * 0.05 : um; + for (let y = 0, o = 0; y < H; y++) + for (let x = 0; x < W; x++, o += 3) { + if (f > 0) display.setPixel(x, y, cur[o] * g + old[o] * f, cur[o + 1] * g + old[o + 1] * f, cur[o + 2] * g + old[o + 2] * f); + else display.setPixel(x, y, cur[o], cur[o + 1], cur[o + 2]); + } +} From 3e40550830e9f4704320aeb31ba28d2f30f7571a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 00:24:48 +0000 Subject: [PATCH 07/10] =?UTF-8?q?52=ED=8C=90:=20=ED=8C=A8=ED=84=B4=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EB=91=90=20=EC=A4=84=EC=9D=98=20=EC=85=8B?= =?UTF-8?q?=20=EB=82=98=EC=97=B4=EC=9D=84=20=EA=B3=A0=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LXsMxtcgxEaHMjeqmCbsjv --- incoming/pattern-study/patterns/banshee.js | 2 +- incoming/pattern-study/patterns/pumpkin-drop.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/patterns/banshee.js b/incoming/pattern-study/patterns/banshee.js index ec0fb0746..309c76c6a 100644 --- a/incoming/pattern-study/patterns/banshee.js +++ b/incoming/pattern-study/patterns/banshee.js @@ -236,7 +236,7 @@ export function update(dt, input, params) { S.arms = up; S.mouth = w < 0 ? 0.1 : w < WA ? 0.1 + 0.9 * sm((w - 0.15) / (WA - 0.15)) : w < WB ? 1 : w < WC ? 1 - 0.9 * sm((w - WB) / 0.4) : 0.1; S.blast = w >= WA && w < WB ? F * (0.6 + 0.4 * sm((w - WA) / 0.3)) : 0; - // She drifts, bobbing, and rises when she wails. + // She drifts and bobs. When she wails she rises. S.hx = 27 - 8 * S.wind + 10 * sin(0.23 * t + 0.4) + 4 * sin(0.57 * t); S.hy = 30 + 4 * sin(0.31 * t) + 1.5 * sin(1.3 * t) - 4 * up; S.gust = vn(t * 0.7, 3) * (4 + 16 * S.wind); diff --git a/incoming/pattern-study/patterns/pumpkin-drop.js b/incoming/pattern-study/patterns/pumpkin-drop.js index 24e9f123a..9957c6e13 100644 --- a/incoming/pattern-study/patterns/pumpkin-drop.js +++ b/incoming/pattern-study/patterns/pumpkin-drop.js @@ -14,7 +14,7 @@ // Knob 1: Flame. The colour of the candle and of what it lights: the carved face, the halo on the sky and the hoist, the streak as it falls and the flash of the burst. Gold at 0, toxic green at 0.25, blue at 0.5, magenta at 0.75. The pumpkin's skin stays orange. // Knob 2: Speed. At 2 a round takes about 10 s. The pumpkin is wound up from the ground in 2.2 s and swings for 1.6 s; the fall of 81 rows takes 1.04 s and ends at 156 LEDs a second; the burst lasts 2.4 s, the candle going out 1.3 to 1.6 s after the impact; the hook comes down in 0.9 s and the next pumpkin takes 1.8 s to come in and light. At 0.2 ten times slower, at 3 half as fast again. // Knob 3: Wind. How far the pumpkin swings at the top and how fast the clouds and the smoke drift right. At 0 nearly still air: a swing of 5 degrees either way and clouds at 2 LEDs a second. At 1 it swings 50 degrees and is let go at the end of a long arc, left and right in turn, while the top band of cloud runs at 18 LEDs a second. A big pumpkin swings less, so it stays on the panel. -// Knob 4: Size. The pumpkin, 16 LEDs wide at 0 and 32 at 1, about 17 at the opening. A bigger one throws more light, farther, and bursts into more pieces: 25 shards at the opening and 48 at 1. +// Knob 4: Size. The pumpkin, 16 LEDs wide at 0 and 32 at 1, about 17 at the opening. A bigger one throws its light farther and bursts into more pieces: 25 shards at the opening and 48 at 1. // Buttons: K1 tap lets the hanging pumpkin go at once. const W = 64, H = 128, N = W * H; From a96053e3595fc5ee963834d5ef7749029c14449e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 00:28:02 +0000 Subject: [PATCH 08/10] =?UTF-8?q?52=ED=8C=90:=20README=20=EC=97=90=20?= =?UTF-8?q?=EC=97=B4=20=EA=B0=9C=EC=9D=98=20=ED=91=9C=EC=99=80=20=EC=97=B0?= =?UTF-8?q?=EA=B5=AC=20=EB=85=B8=ED=8A=B8=EB=A5=BC=20=EB=8D=94=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LXsMxtcgxEaHMjeqmCbsjv --- incoming/pattern-study/README.md | 87 +++++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 6 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 598dae68e..27befecc1 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 408개를 쉰여섯 갈래로 나눠 만들었어요. 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판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 418개를 쉰여섯 갈래로 나눠 만들었어요. 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판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 52판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. [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판에서 세로로 다시 짰어요. @@ -660,6 +660,21 @@ | [Skull Swap](patterns/skull-swap.js) | 할로윈 | 달 뜬 묘지에서 춤추던 해골 둘이 제 머리를 들어 서로 던져 남의 목에 끼우고, 눈구멍을 돌려 마주 보며 어깨를 으쓱한 뒤 도로 던져요. K3 은 머리가 나는 높이와 도는 수, K4 는 바꾸기 사이에 추는 박자 수이고, K1 을 누르면 바로 던져요. | | [Paper Lantern Ghost](patterns/paper-lantern-ghost.js) | 할로윈 | 비 오는 보라 밤에 처마의 종이 등이 흔들리다 외눈을 뜨고, 이음매가 찢어져 붉은 혀를 늘어뜨리며 웃어요. 그 아래로 외눈 종이우산이 외다리로 웅덩이를 튀기며 뛰어다녀요. K3 은 비, K4 는 우산이 뛰는 높이예요. | +### 52판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Gaussian Free Field](patterns/gaussian-free-field.js) | 노이즈·흐름 | 패널의 격자점마다 가우스 무작위 높이를 둔 자유장이 0 위는 주황 땅, 0 아래는 보라 바다로 칠해지고, 흰 해안선이 섬 안의 호수와 호수 안의 섬으로 겹겹이 닫힌 고리가 되어 1~2초마다 지도를 새로 그려요. K3 을 올리면 테두리 왼쪽과 오른쪽의 높이가 갈려 바닥에서 꼭대기까지 오르는 해안선 하나, SLE(4) 곡선이 서고, K4 는 해안선 아래 흐림의 폭이에요. | +| [Force Chains](patterns/force-chains.js) | 입자 | 교차한 편광판 사이에 세운 원판 175개를 위의 피스톤과 양옆 벽이 번갈아 누르면, 하중을 지는 원판만 회색에서 흰색과 노랑을 지나 빨강과 파랑으로 물든 등색선을 띠고 갈라지는 사슬로 이어져서, 미는 쪽이 바뀔 때마다 위아래에서 옆으로 길을 바꿔요. K3 은 누르는 힘, K4 는 원판 크기예요. | +| [Anscombe's Quartet](patterns/anscombes-quartet.js) | 데이터 그림 | 회귀선이 같은 앤스컴의 산점도 넷이 쌓였다가 한 그림으로 펴지고, 점 264개가 다섯 통계를 소수 둘째 자리까지 지킨 채 원과 별, 과녁과 빗금으로 모양을 바꾸는 동안 회귀선과 십자선은 꿈쩍하지 않아요. K3 은 담금질 온도, K4 는 점에서 선까지 내린 잔차 막대예요. | +| [de Bruijn Sequence](patterns/de-bruijn-sequence.js) | 리듬·펄스 | 0과 1의 고리를 1 은 치고 0 은 쉬는 리듬으로 치면서, 박마다 마지막 여섯 비트 창이 가리키는 단어의 타일을 켜서 단어 64개의 판이 한 주기에 꼭 한 번 차고, 그 아래 띠와 베틀 실이 같은 단어를 하나씩 밝혀요. K3 은 수열을 고르는 규칙, K4 는 차수를 6 에서 3 까지 내리고, K1 을 누르면 주기를 끝내고 새 수열로 가요. | +| [Zonal Jets](patterns/zonal-jets.js) | 물결·간섭 | 북쪽으로 갈수록 코리올리 힘이 커지는 판에서 잔 소용돌이를 흔들면 에너지가 큰 규모로 올라가다 막혀, 동쪽으로 흐르는 붉은 띠와 서쪽으로 흐르는 푸른 띠가 패널 위아래로 줄지어 서요. 흐름선의 굽이와 띠 사이 소용돌이는 물이 흐르는 쪽을 거슬러 서쪽으로 미끄러져요. K3 은 띠의 수를 정하는 β, K4 는 휘젓는 세기이고, K1 을 누르면 새로 시작해요. | +| [Pumpkin Drop](patterns/pumpkin-drop.js) | 할로윈 | 구름 낀 보라 밤에 촛불 켠 호박 등이 도르래 틀 꼭대기까지 감겨 올라가 흔들리다 놓여 128줄을 떨어지고, 땅에서 터져 조각과 씨를 흩뿌려요. 초는 굴러 나가 연기를 피우며 꺼지고, 다음 호박이 들어와 불을 켜요. K3 은 바람, K4 는 호박 크기이고, K1 을 누르면 매달린 호박을 바로 놓아요. | +| [Banshee](patterns/banshee.js) | 할로윈 | 창 하나 밝힌 오두막 위 밤바람에 비치는 여인이 긴 머리와 해진 옷자락을 날리며 떠다니다 7초마다 고개를 젖혀 울면, 독한 초록 소리 고리가 밤을 휘며 퍼져서 하늘이 번쩍이고 유리창이 덜컹거리고 촛불이 눕고 까마귀가 흩어져요. K3 은 바람, K4 는 울음의 세기예요. | +| [Specimen Shelf](patterns/specimen-shelf.js) | 할로윈 | 두 선반의 표본 병 셋에 뜬 눈알 가운데 하나가 돌아 나를 보면, 다른 눈들이 그 눈을 흘끗 보고 차례로 따라 돌았다가 다 함께 딴 데를 보고 위로 굴러 깜빡여요. 선반을 두드리면 액체가 출렁이며 빛나요. K3 은 거품, K4 는 두드리는 잦기예요. | +| [Lost Head](patterns/lost-head.js) | 할로윈 | 달 뜬 묘지에서 목 없는 해골이 묘비 위 머리를 하나씩 써 보며 너무 크거나 작은 머리, 모자 쓴 머리, 무는 머리와 호박을 으쓱하며 내려놓다가, 제 머리가 맞으면 눈구멍이 독한 초록으로 켜지고 춤을 춰요. 그러다 머리가 떨어져 굴러가요. K3 은 틀린 머리 수, K4 는 크기가 틀린 정도예요. | +| [Creaking Door](patterns/creaking-door.js) | 할로윈 | 보라 복도 끝의 문이 다섯 번 덜컹이며 삐걱 열리면 촛불 빛이 쐐기로 넓어지며 바닥과 왼쪽 벽을 타고, 그 빛 속으로 손가락이 긴 손 그림자가 기어 나오거나 뾰족 모자의 그림자가 지나가요. 문이 쾅 닫히면 판이 흔들리고 먼지가 떨어지고, 문 밑 빛줄기를 두 발이 지나가요. K3 은 빛이 닿는 거리, K4 는 공기 속 먼지이고, K1 을 누르면 문이 닫히거나 열려요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -2558,6 +2573,50 @@ E8 의 근 240개를 고셋의 4₂₁ 다면체 꼭짓점으로 그렸어요. 주제 6 으로 일본 요괴 둘을 그렸어요. 조친오바케는 처마에 걸린 종이 등에 눈과 입이 생긴 것이고, 가사오바케는 외눈에 혀를 내민 종이우산이 외다리로 뛰는 것이에요. 둘 다 에도 시대 요괴 그림에 나오는 민간 요괴예요. 패널 위쪽에서는 너비 26 의 등이 처마 고리에 매달려 흔들리고, 9초마다 0.8초에 외눈이 가늘게 떠지고 1.2초에 가운데보다 6 LED 아래 이음매가 찢어져 입꼬리가 올라간 웃음이 돼요. 2.4초까지 길이 25 의 붉은 혀가 일곱 마디 사슬로 늘어져 흔들림을 따라 출렁이고, 등의 눈동자는 우산을 따라가요. 아래에서는 초록 종이우산이 0.6초마다 웅크렸다 뛰어 x 10 과 54 사이를 오가고, 내려앉을 때마다 웅덩이에 고리와 물방울을 튀겨요. 등빛은 비와 웅덩이에 떨어져요. 등에서 20 LED 안의 빗줄기는 등빛 0.85 를 더하고, 웅덩이에는 등이 거꾸로 일렁여요. 첫 판의 젖은 돌 반짝임은 대각선 무늬로 찍혀 주황 빗금으로 보여서, 등 아래 돌 이음 줄만 밝혔어요. 등 가까운 빗줄기는 라일락 바탕에 주황을 더하자 복숭앗빛이어서, 등빛을 받는 만큼 라일락을 빼자 주황이 됐어요. 그러자 K3 반응이 0.06 에서 0.04 로 내려가서, 장대비일수록 빗줄기를 3 LED 길게 하자 0.06 으로 돌아왔어요. 바람에 0.4 기울어 떨어지는 장대비는 내려오는 동안 63 LED 옆으로 가서, 위에서 판 너비만큼만 뿌리면 1.2초 뒤 왼쪽 아래가 비었어요. 바람이 셀수록 바람 위쪽으로 더 넓게 뿌려 모든 줄에 비가 닿게 했어요. 우산의 혀는 처음에 몸통 뒤에 그려져 테 아래 3 LED 만 보여서, 몸통 위로 올려 13.5 LED 로 늘였어요. 잰 값은 움직임 1.472, 채움 0.869, 색 0.271 이에요. +## 52판 + +52판은 주제 5 「수학 · 추상」 다섯과 주제 6 「할로윈」 다섯이에요. 같이 돈 51판(#262)이 고른 모자 타일과 사토-테이트 법칙, 흡혈귀 변신과 좀비 떼 같은 것을 피해서 골랐고, 앞 바퀴 PR 들에 따를 글은 없었어요. 수학은 Gaussian Free Field, Force Chains, Anscombe's Quartet, de Bruijn Sequence, Zonal Jets 이고 노이즈·흐름, 입자, 데이터 그림, 리듬·펄스, 물결·간섭에 하나씩 들어갔어요. 50판까지 넣어 세면 추상 갈래 가운데 노이즈·흐름이 열로 가장 적었고, 같은 열인 보로노이·셀과 시뮬레이션에는 51판이 하나씩 넣고 있었어요. 그다음으로 적은 열하나 가운데 데이터 그림과 입자, 리듬·펄스에 하나씩 넣고, 다섯째는 열둘인 물결·간섭에 넣었어요. 다섯 다 「흔한 것」에도 「낯선 수학 예」에도 없어요. Zonal Jets 는 이 절의 「목록 밖에서」 50판 뒤에 적힌 감이고, 나머지 넷은 새로 찾았어요. 할로윈은 Pumpkin Drop, Banshee, Specimen Shelf, Lost Head, Creaking Door 예요. 세 병의 눈알이 차례로 돌아보는 선반은 47판 뒤에, 목 없는 해골이 제 머리를 찾는 장면은 50판 뒤에 적혀 있던 감이고, 나머지 셋은 새로 찾았어요. 주제 1 은 50판까지의 408개에도 빈 칸과 하나뿐인 칸이 없어서, 둘뿐인 칸 열다섯 가운데 51판이 겨눈 칸과 겹치지 않는 복잡도 × 모양 바뀜 4,4 와 가장자리 × 복잡도 3,4 를 겨눴어요. Gaussian Free Field 가 Bread Mould 와 Soap Film 뿐이던 복잡도 × 모양 바뀜 4,4 에, Force Chains 가 Percolation 과 Guilloché 뿐이던 가장자리 × 복잡도 3,4 에 섰어요. 주제 4 는 Force Chains 와 Pumpkin Drop 이 맡았어요. 열 개는 어느 축의 끝도 넘지 않았고, 복잡도는 0.201 에서 2.564, 움직임은 0.118 에서 1.698 이에요. 열 개는 `shelf.json` 의 어느 묶음에도 넣지 않아서 페이지 맨 위 「아직 안 추린 것」에 서요. + +### Gaussian Free Field + +주제 5 와 1 로 2차원 가우스 자유장과 그 등고선을 세웠어요. 자유장은 점마다 가우스 분포를 따르는 무작위 높이이고, 두 점의 상관이 거리의 로그로만 줄어서 어느 규모에서 봐도 같은 거칠기예요. 오데드 슈람과 스콧 셰필드는 2009년에 격자 자유장의 등고선이 SLE(4) 로 간다는 것을 보였어요(Acta Mathematica 202). 패턴은 LED 8,192개를 격자점으로 두고, 테두리 밖을 0 으로 묶은 사인 모드 8,192개의 합으로 장을 지어요. 모드의 진폭은 정규 난수를 격자 라플라시안 고윳값의 제곱근으로 나눈 것이에요. 모드의 홀짝 대칭으로 한 줄과 그 거울 줄을 한 합에서 얻어서, 프레임당 곱셈 79만 번에 3 ms 안에 들어요. 모드는 저마다 오른슈타인-울렌벡 과정으로 떠돌아서 어느 순간에도 장의 분포가 자유장 그대로예요. Speed 2 에서 가장 큰 모드는 0.7초, 가장 잘은 모드는 0.14초에 제 값을 잊어요. 0 위는 주황 땅, 0 아래는 짙은 보라 바다이고, 0 등고선을 흰 해안선으로 그려요. 테두리가 0 이라 해안선은 닫힌 고리가 되어 섬 안에 호수, 호수 안에 섬으로 겹겹이 들어앉아요. 이 고리들의 극한이 CLE(4) 예요. 땅은 높이 0.63 마다 주황에서 호박색과 금빛으로 한 칸씩 오르고, 바다에는 같은 간격으로 옅은 등고선이 서요. K3 을 올리면 테두리 왼쪽 절반을 +0.63, 오른쪽 절반을 -0.63 으로 묶어요. 그러면 바닥 가운데에서 꼭대기 가운데까지 128줄을 오르는 해안선이 꼭 하나 생기고, 0.33 부터 그 선을 두 LED 굵기의 흰 선으로 그려요. 0.63 은 √(π/8) 이고, 슈람과 셰필드가 2013년 논문에서 이 선을 SLE(4) 로 만드는 연속 자유장의 높이 차로 적은 값이에요. 겨눈 칸은 Bread Mould 와 Soap Film 뿐이던 복잡도 × 모양 바뀜 4,4 예요. 첫 판은 격자 장을 그대로 칠하고 모드를 OU 로만 굴려서 움직임 4.226, 복잡도 1.262, 모양 바뀜 0.195 였어요. 해안선이 화면 가득 끓어 TV 잡음으로 보였어요. OU 의 경로는 시간으로 거칠어서, 가장 큰 모드도 한 프레임마다 진폭의 17% 를 새 난수로 받았어요. 모드마다 시간 상수의 절반짜리 지수 필터로 한 번 더 거르고 분산을 √1.5 로 되돌렸어요. 등고선은 흐린 장에서 긋고 음영은 거의 흐리지 않은 격자 장을 왼쪽 위에서 비춰 그리게 나누었더니, 움직임이 3.625 에서 0.723 으로, 복잡도가 1.388 에서 2.089 로 갔어요. 모양 바뀜은 그 뒤로도 0.19 에 머물렀어요. 자유장은 규모가 두 배가 될 때마다 분산을 같은 몫씩 더 가져서, 4×16 칸 안에서도 부호가 칸 평균만큼 흔들려요. 흐림을 1.1 LED 에서 3.9 LED 로 넓혀도 0.189 가 0.239 가 될 뿐이었어요. 땅은 가장 밝은 채널을 늘 가득 두고 높이를 색상과 채도로만 보였고, 바다는 밝기 0.15 로 내렸어요. 가장 큰 모드가 잊는 시간도 2.4초에서 0.7초로 줄이고 기본 흐림을 1.6 LED 로 넓히자, 모양 바뀜이 0.282 와 0.307, 0.330 을 지나 0.375 가 됐어요. 그때 복잡도는 2.141 이었어요. 해안선과 등고선을 한 색으로 칠하면 deflate 가 그 줄을 되풀이로 묶었고, 음영으로 몇 단계씩 흔들자 2.392 가 됐어요. 땅을 높이 띠마다 한 칸씩 오르는 계단 색으로 바꾸자 흰 해안선이 또렷해졌고, 복잡도는 2.256 에 섰어요. 확인한 것이에요. 패턴의 장을 그대로 120초 굴리니 패널 평균 분산이 0.6271 로 이론 값 0.6290 과 맞았고, 0 위의 몫은 0.512 였어요. 부호가 바뀐 LED 는 0.1초 뒤 10%, 1초 뒤 43%, 2초 뒤 48% 예요. 같은 짓기를 16×32 에서 128×256 까지 상자 넷에 옮겨 K3 1 의 해안선을 세니 평균 59.5, 161, 445, 1,222 변이었어요. 상자가 두 배가 될 때마다 1.44, 1.46, 1.46 제곱으로 늘어서 SLE(4) 의 차원 1 + 4/8 = 1.5 에 가까워요. 가운데 점의 분산은 상자가 두 배가 될 때마다 0.107~0.109 씩 늘어서 ln 2 / 2π = 0.110 과 맞아요. 잰 값은 복잡도 2.256, 모양 바뀜 0.369 이고, 움직임은 1.408 이에요. + +### Force Chains + +주제 5 와 1, 4 로 광탄성 원판의 힘 사슬을 세웠어요. 피에르 당튀가 1957년에 광탄성 알갱이를 쌓아 하중이 몇 줄기 사슬로만 흐르는 것을 보였고, 트루샨트 마즈무다르와 로버트 베링거가 2005년에 같은 원판의 무늬에서 접촉력을 하나하나 읽어 전단이 사슬을 한 방향으로 줄 세운다는 것을 보였어요. 패턴은 패널보다 양옆으로 2 LED 넓은 상자에 지름 6.3 과 8.1 LED 의 원판 175개를 반씩 넣어요. 지름을 1.3 배 다르게 해서 원판이 결정으로 줄 서지 않아요. 원판은 쿤달과 스트랙(1979)의 이산 요소법으로 한 프레임에 16걸음을 걸어요. 겹친 만큼 미는 감쇠 용수철에, 마찰 계수 0.5 를 넘으면 미끄러지는 접선 용수철을 더했어요. 위의 피스톤과 양옆 벽은 정해진 압력으로 미는 무거운 판이에요. Speed 2 에서 한 바퀴 6초 동안 피스톤은 하중의 1 배에서 0.2 배로 줄고, 벽은 0.2 배에서 1 배로 늘었다 돌아가요. 벽 하나가 다른 벽의 세 배로 밀어서 더미가 한 바퀴는 오른쪽으로, 다음 바퀴는 왼쪽으로 기울어요. 그림은 교차한 원편광판 사이에서 백색광으로 본 등색선이에요. 접촉마다 플라망(1892)의 점하중 응력장을 텐서로 더해 주응력 차를 구해요. 그 위상차에서 400~700 nm 의 31 파장이 sin² 만큼 지나간 빛을 CIE 등색 함수로 색으로 바꿔요. 하중이 작은 원판은 검고, 사슬은 회색, 흰색, 노랑, 주황, 빨강, 보라, 파랑 차례로 물들어요. 겨눈 칸은 Percolation 과 Guilloché 뿐이던 가장자리 × 복잡도 3,4 예요. 첫 판은 마찰 없는 원판을 5초마다 누르고 놓고 흔들었는데, 프린지 눈금을 잘못 잡아 모든 원판이 높은 차수의 청록으로 칠해졌어요. 가장자리 0.731, 복잡도 1.586 이었어요. 눈금을 바로잡아 바닥의 무게 압력이 프린지 0.08 이 되게 하자 사슬이 섰고, 가장자리 0.591, 복잡도 1.784 가 됐어요. 누른 프레임은 2.07 이었지만 놓고 흔드는 프레임이 1.3 으로 떨어졌어요. 그래서 늘 눌린 채로 미는 방향만 바뀌는 이축 압축으로 바꾸고 마찰을 넣었어요. 처음에는 접선 감쇠가 너무 세서 더미가 1초에 4~11 LED 로 끓었고 움직임이 2.810 이었어요. 접선 감쇠를 법선의 0.2 로 줄이자 가라앉았어요. 벽을 위치로 밀었더니 단단한 바닥이 힘을 다 받아서 아래 절반이 높은 차수의 색종이가 됐어요. 벽을 압력으로 밀고 한쪽 벽을 높이 16 LED 판 여덟 장으로 나눠 막처럼 밀게 하자 사슬이 판 전체에 고르게 섰어요. 그때 복잡도는 2.008 이었어요. 프린지를 늘리면 복잡도는 오르지만 원판이 거의 다 밝아져 사슬이 묻혔어요. LED 하나를 네 점에서 재 평균하자 같은 씨앗에서 복잡도가 2.105 에서 2.466 으로 올랐어요. 무늬는 부드러워졌는데 섞인 색이 많아져 압축이 덜 됐어요. 그 덕에 프린지를 0.09 로 내리고 감마를 1 로 두어 약한 원판을 다시 어둡게 했어요. 한쪽으로 기우는 힘을 넣자 움직임이 0.28 에서 0.54 가 됐어요. 패턴의 걸음 함수를 그대로 돌려 셌어요. 2~24초 동안 평균보다 센 접촉은 43~46% 이고, 평균의 두 배를 넘는 접촉은 8~11%, 세 배를 넘는 접촉은 1% 남짓이에요. 센 접촉의 방향을 힘으로 가중해 수직 성분의 제곱을 평균하면 피스톤이 가장 셀 때 0.62, 벽이 가장 셀 때 0.37 이에요. 고르면 0.5 라서 사슬이 위아래에서 옆으로 돌아가는 것이 숫자로도 보여요. 잰 값은 가장자리 0.585, 복잡도 2.405 예요. 움직임은 0.536 이고 `thirds` 는 62/65/73 이에요. + +### Anscombe's Quartet + +주제 5 로 앤스컴의 사중주를 세웠어요. 프랜시스 앤스컴이 1973년에 낸 점 11개짜리 자료 넷이에요. x 와 y 의 평균이 9 와 7.50, 표준편차가 3.32 와 2.03, 상관계수가 0.816, 회귀선이 y = 3 + 0.5x 로 소수 둘째 자리까지 같은데, 그림은 흩어진 구름과 포물선, 바깥 점 하나가 낀 직선, 한 줄로 선 세로줄로 서로 달라요. 패널은 먼저 64×32 그림 넷을 쌓아 앤스컴이 낸 값 그대로 2×2 LED 점을 찍고, 그림마다 같은 회귀선과 평균 십자선, 1 표준편차 점선을 그어요. 1초 뒤 네 그림이 0.8초 동안 128줄짜리 하나로 펴지고, 점 44개가 6개씩 갈라져 264개의 점구름이 돼요. 그다음은 저스틴 마테이카와 조지 피츠모리스가 2017년에 낸 같은 통계 담금질이에요. 점 하나를 조금 밀고, 다섯 통계가 소수 둘째 자리까지 그대로일 때만 그 밀기를 남기고, 목표 모양에 가까워지는 밀기를 골라요. 구름은 3.6초마다 원, 빗금, 별, 세로 막대, 과녁, 가로 계단, 점 격자, X 로 바뀌어요. 회귀선과 십자선은 움직이지 않고, 왼쪽 위의 숫자 9.00, 7.50, 3.32, 2.03, 0.82 도 그대로예요. 한 바퀴 37.4초 끝에 점들이 제 자료 자리로 돌아가고 그림 넷이 다시 서요. 처음에는 모양을 백색화한 좌표에 그려 통계에 맞췄어요. 상관 0.82 는 축을 어떻게 늘려도 공분산 타원을 3.1 대 1 보다 둥글게 만들지 못해서, 원은 3.9 대 1 로 기운 타원이 됐고 별은 읽히지 않았어요. 그래서 둥근 모양은 둥근 채로 두고, 점의 24% 를 회귀선 양 끝의 덩어리 둘로 보내 상관을 그 덩어리가 지게 했어요. 앤스컴의 셋째와 넷째 자료에서 바깥 점 하나가 하는 일이에요. 선으로 된 모양은 촐레스키 인수로 맞춰서 세로선은 세로로, 가로선은 가로로 남아요. 마테이카의 방식대로 가장 가까운 윤곽까지의 거리만 쓰자 세로 막대와 가로 계단에서 점의 19~24% 가 덩어리나 다른 줄에 걸려 남았어요. 뜨거운 걸음이 아니면 줄 사이 틈을 건너지 못해서예요. 점마다 윤곽 위의 제자리를 하나씩 나눠 주고 그 거리의 두 배를 더하자, 여덟 모양 모두 1.5초 안에 점의 95% 가 제자리에 닿았어요. 둥근 모양은 머무는 동안 1초에 0.25 라디안 돌아요. 통째로 돌면 퍼짐과 상관이 그대로라서예요. 처음 배율에서는 (13, 12.74) 와 (19, 12.5) 의 바깥 점 사본 12개가 패널 밖 (51, −10) 과 (79, −7) 에 떨어졌어요. 패널 밖으로 가는 밀기를 버리니 그 점들은 끝까지 움직이지 못했어요. x 1 이 4.5 LED, y 1 이 13.5 LED 이던 배율을 3.4 와 12 로 줄여 넷째 자료의 x 19 까지 패널에 들였어요. 첫 판의 잰 값은 움직임 0.265, 복잡도 0.208 이었고, 노브는 0.01, 0.16, 0.02, 0.02 로 Hue 가 바닥 0.012 아래였어요. 색이 점에만 실려서예요. 회귀선 위와 아래를 레벨 23 의 옅은 청록과 보라로 나눠 칠하자 Hue 가 0.07 이 됐어요. 그 바탕은 24 를 넘지 않아서 채움이 0.104 로 지금 가장 낮은 0.117 아래였어요. 평균에서 1 표준편차 안의 상자만 1.35배 밝히자 0.202 가 됐어요. Heat 는 첫 모양이 2~3초 창에 막 흐르기 시작해서 0.02 였어요. 사중주를 1.3초에서 1초로, 접기를 1초에서 0.8초로 줄이고 뜨거운 걸음을 2.5배 길게 하자 0.04 가 됐어요. 세 노브 자리에서 120초씩 돌린 점구름 프레임 9,221장에서 다섯 통계는 한 장도 빠짐없이 그대로였고, 정확한 값은 평균 x 가 8.9950~9.0050, r 이 0.8150~0.8250 으로 반올림 칸을 끝까지 다 써요. 점구름의 진짜 회귀선은 기울기 0.4965~0.5063, 절편 2.938~3.037 사이를 오가서, 그려 둔 선과는 패널 양 끝에서도 1 LED 안에서만 갈라져요. 잰 값은 움직임 0.174, 복잡도 0.205, 채움 0.202, 색 0.143 이고 노브는 0.05, 0.12, 0.04, 0.04 예요. + +### de Bruijn Sequence + +주제 5 로 드 브라윈 수열을 리듬으로 세웠어요. 길이 2^k 의 0과 1 고리에서 연속한 k 비트 창이 k 비트 단어 2^k 개를 빠짐없이 꼭 한 번씩 지나는 수열이에요. 니콜라스 호버르트 드 브라윈이 1946년에 그 개수를 셌고, 카미유 플라이 생트마리가 1894년에 먼저 센 것이 뒤에 알려졌어요. 산스크리트 운율의 외움말 yamātārājabhānasalagāḥ 가 가볍고 무거운 음절로 쓴 차수 3 의 수열이라 리듬 갈래에 넣었어요. 1 은 치고 0 은 쉬어요. 위 64줄은 차수 6 의 단어 64개를 8 LED 간격의 8×8 타일로 깐 판이에요. 박마다 창이 쓰는 단어의 타일이 켜진 채 남아서, 판은 한 주기에 꼭 한 번 차고 두 번 켜지는 타일이 없어요. 타일의 칠은 그 단어가 주기의 몇 번째인지를 따라 청록에서 보라로 가요. 친 박이면 타일이 희게 번쩍이고 테두리가 울려요. 타일에서 타일로 뛴 자리는 최근 여섯 번이 선으로 남아요. 그 앞의 길은 타일 밑에 깔려 2 LED 틈으로만 보여서, 해밀턴 순환이 점선처럼 쌓여요. 판 아래 6줄 띠는 한 주기 64 비트 전체를 LED 하나에 한 비트로 그리고, 흰 괄호가 창을 표시해요. 패널 가로 64 가 차수 6 의 주기 64 비트와 같아서 수열 전체가 띠 한 줄에 들어가요. 주기의 첫 5박 동안 괄호가 띠의 끝과 처음으로 갈라져서 수열이 고리라는 것이 보여요. 그 아래 44줄은 차수 5 그래프를 베틀처럼 그렸어요. 단어 하나는 앞 5 비트 꼭짓점에서 뒤 5 비트 꼭짓점으로 가는 실 한 가닥이라, 실 64가닥이 한 주기에 한 번씩 켜져요. 노브 3 이 0 이면 주기마다 새 수열을 골라요. 차수 5 그래프의 펼침 나무를 윌슨의 고리 지운 걸음으로 고르게 뽑고, 나무 가지로 마지막에 나가는 오일러 회로를 따라요. 그러면 BEST 정리대로 67,108,864개 가운데 하나가 고르게 나와요. 1 은 마틴의 1을 앞세우는 규칙(1934)이라 0111111 로 시작해서 오른쪽 열과 아랫줄이 먼저 차요. 2 는 x^6+x+1 시프트 레지스터의 m 수열이에요. 000000 이 없어서 63박 동안 왼쪽 위 타일 하나가 끝까지 어둡고 띠의 첫 칸이 비어요. 노브 4 는 차수를 6 에서 3 까지 내리고, 차수 3 에서 마틴 규칙은 01110100, 곧 ya mā tā rā ja bhā na sa 를 쳐요. 첫 판은 베틀 실을 곧은 선으로 그리고 지난 실을 0.3 으로 남겼는데, 반 주기만 지나도 아래 절반이 빗금으로 덮였어요. 실을 양 끝이 누운 S 자로 바꾸고 지난 실을 0.12 로 낮춰 최근 실만 밝히자 짜인 천처럼 읽혔어요. 주기가 끝나면 판을 바로 지우던 것은, 다 찬 판이 다음 첫 박에 번쩍이고 4박 동안 바래도록 바꿨어요. 움직임은 0.101 에서 0.118, 경계는 0.774 에서 0.543 이 됐어요. 잰 값은 움직임 0.118, 복잡도 0.261, 채움 0.337 이에요. + +### Zonal Jets + +주제 5 로 베타 평면의 띠 제트를 세웠어요. 피터 라인스가 1975년에 보인 현상이에요. 북쪽으로 갈수록 코리올리 매개변수가 β 씩 커지는 판에서는 2차원 난류가 에너지를 큰 규모로 올려 보내다가 라인스 규모 (U/β)^½ 에서 막혀, 그 에너지가 동서로 흐르는 제트로 줄지어 서요. 목성의 띠도 이렇게 설명해요. 패턴은 Inverse Cascade 의 의사 스펙트럼 풀이를 가져와 소용돌이도 방정식에 β ∂ψ/∂x 항과 선형 마찰, 파수 10 둘레(파장 6.4 LED)를 흔드는 백색 잡음 강제를 더했어요. β 항은 마찰처럼 선형이라 적분 인자에 함께 넣었고, 모드마다 단위 시간에 β kx/k² 씩 도는 복소 인자가 로스비 파의 서쪽 표류예요. 색은 행 평균의 동서 속도에 그 행에서 벗어난 몫의 0.4 를 더한 값이에요. 동쪽 흐름은 빨강에서 옅은 호박색으로, 서쪽 흐름은 보라에서 하늘색으로 칠하고, 흐름 함수의 등고선을 흐름선으로 얇게 그어요. 첫 판은 강제를 0.02, 마찰을 0.01 로 두고 패널 격자에서 20 단위를 미리 돌렸어요. 미리 돌리기에 0.8~1.2초가 걸렸고, 동서 모드가 가진 에너지 몫이 0.15~0.41 이라 β 를 올리면 빨강과 파랑이 조각보처럼 섞였어요. 50판 Inverse Cascade 는 넷째 노브로 베타 평면을 해 보고 제트 앞에 격자 크기의 빗살을 봤어요. 여기서도 초점성 12 에서는 자르는 파수 바로 아래 껍질의 엔스트로피가 강제 껍질의 다섯 배까지 튀어 세 LED 길이의 잔물결 조각이 났고, 80 으로 올리자 만분의 1 아래로 내려갔어요. 미리 돌리기는 32×64 격자로 옮겼어요. 15 단위를 돌린 뒤 모드째로 패널 격자에 옮기니 첫 프레임이 0.14~0.17초예요. 강제와 마찰을 0.003 으로 낮추자 동서 몫이 K3 -1, 0, 1 에서 0.43, 0.40, 0.21 이 되고 띠가 섰어요. 처음 잰 값은 움직임 2.241, 복잡도 2.278, 프레임당 4.1 ms 였어요. 쿠랑 수를 1.2 로 올려 보니 파수 16 위로 엔스트로피의 12% 가 몰리는 걸음이 나왔고, 1.0 에서 0.67%, 0.9 에서 0.04% 였어요. 0.9 로 두고 1초의 흐름 시간을 1.3 에서 0.7 로 줄이자 2.6 ms 가 됐어요. 넷째 노브는 처음에 마찰이었어요. 넓게 잡으면 2초 안에 작은 소용돌이 죽이 됐고 좁게 잡으면 3초 안에 달라지는 것이 없어서, 강제의 세기로 바꿨어요. 행 평균 속도가 양인 띠를 세면 2~8초에 K3 -1 에서 2.4개, 0 에서 3.4개, 1 에서 4.7개예요. 실효 속도 1.0 으로 라인스 식 2√(β/2U) 를 셈하면 2.2, 4.2, 8.2개라, β 가 작을 때는 맞고 클 때는 15 단위 안에 제트가 다 서지 못해요. 이 패널에서 새로 본 것은 흐름을 거스르는 무늬예요. 흐름선의 굽이와 제트 사이 고양이 눈은 1초에 K3 -1, 0, 1 에서 10, 20, 48 LED 씩 서쪽으로 미끄러지는데, 같은 행의 물은 4.2, 3.3, 2.3 LED 씩 동쪽으로 흘러요. 제트에서 가장 빠른 곳도 1초에 7 LED 쯤이에요. 잰 값은 움직임 1.698, 복잡도 2.564, 채움 1.000 이고, `thirds` 는 100/100/100 이에요. + +### Pumpkin Drop + +주제 6 과 4 로 도르래 틀에서 떨어지는 호박 등을 그렸어요. 보라 밤하늘 앞에 기둥과 들보가 검게 서고, 들보 끝 도르래에서 내려온 밧줄이 촛불 켠 호박을 땅에서 2.2초 동안 감아올려요. 그동안 기둥의 감개 바퀴가 밧줄을 따라 돌아요. 꼭대기에서 1.6초 그네를 탄 호박은 흔들림 끝에서 놓여 81줄을 1.04초에 떨어지고, 땅에 닿을 때는 1초에 156 LED 로 내려와요. 떨어지는 동안 꼭지 위로 불꽃이 빠르기에 비례해 20줄까지 늘어나요. 땅에 닿으면 촛불 빛깔의 번쩍임이 땅을 비추고, 껍질과 노란 속살 조각 25개가 26줄 높이까지 튀었다가 두어 번 더 튀고 내려앉아요. 씨 16개가 그 사이로 흩어지고 땅에는 터진 속이 번져요. 초는 옆으로 날아가 구르다가 1.3~1.6초 뒤 불꽃이 깜빡이며 꺼지고, 연기 한 줌이 피어올라요. 고리가 땅까지 내려오면 다음 호박이 오른쪽에서 돌며 들어와 얼굴을 이쪽으로 돌리고, 불이 붙어 번쩍인 뒤 올라가요. 한 바퀴는 10초쯤이에요. 첫 판은 호박이 꼭대기에 매달린 채 시작해 1.2초에 놓였고, K4 는 터지는 세기였어요. 노브는 시작 2~3초 창에서 재는데, 그 창에는 이미 바닥의 조각만 있어서 Flame 이 0.0071, Burst 가 0.0119 였어요. 첫 바퀴를 땅에서 감아올리는 데서 시작해 그 창 안에 불 켠 호박이 매달려 있게 했어요. 촛불이 하늘에 던지는 빛도 두 배쯤 세게 하고 e 분의 1 거리를 9 LED 에서 16 LED 로 넓히자 Flame 이 0.034 가 됐어요. K4 는 호박 크기로 바꿔 16 LED 에서 32 LED 까지 키우고, 큰 호박이 더 멀리 비추게 해서 0.088 이에요. 흔들림만 거는 K3 은 0.02 여서, 바람이 흔들림과 함께 구름 띠 셋을 1초에 2~18 LED 로 밀게 하자 0.042 가 됐어요. 처음 얼굴은 뚫린 자리 둘레까지 빛을 칠해서 주황 공에 노란 얼룩으로 보였어요. 둘레를 지우자 눈과 이가 읽혔어요. 조각은 처음에 14줄 높이까지만 튀어 바닥에 띠로 깔려서, 빠르기를 1초에 40~95 LED 로 올렸어요. 첫 판의 잰 값은 움직임 0.067, 복잡도 0.319 였어요. 하늘을 맨 위에서도 밝기 0.3 으로 두어 `thirds` 는 처음부터 94/96/97 이었고, 검은 틀도 그 밝은 하늘 앞이라 읽혀요. 잰 값은 움직임 0.152, 복잡도 0.542 이고, `thirds` 는 96/97/98 이에요. + +### Banshee + +주제 6 으로 아일랜드 민담의 밴시(bean sídhe)를 그렸어요. 집안에 죽음이 올 때 그 집 밖에서 운다는 창백한 여인의 넋이에요. 밤바람 속에 창 하나만 밝힌 오두막 위로 비치는 여인이 떠다니고, 긴 머리와 해진 옷자락이 바람에 날려요. 머리는 정수리를 가로질러 붙은 16 가닥이고, 가닥마다 마디 20개에 길이 40 LED 남짓이에요. 옷자락은 어깨에서 내려오는 9 가닥 사이를 반투명하게 메우고, 소매 끝에는 해진 천 여섯 가닥이 달려요. 마디는 바람의 빠르기 쪽으로 끌리면서 제 무게로 처지고, 그다음 뿌리부터 마디 길이를 되돌려 놓아요. 가닥마다 바람을 받는 정도를 0.07 에서 0.17 로, 흔들림의 빠르기를 0.7 배에서 1.3 배로 다르게 줘서 한 장으로 붙지 않고 갈래로 흩날려요. Speed 2 에서 7초마다, 처음은 1.2초에 머리를 0.5초 동안 젖히고 1.8초 동안 울어요. 그동안 입에서 0.2초마다 소리 고리가 나와 1초에 60 LED 로 퍼지고, 고리가 지나는 자리의 뒤 배경을 2.4 LED 까지 밀어 휘게 해요. 하늘은 독한 초록으로 번쩍여요. 고리가 1.2초쯤 뒤 창에 닿으면 유리판 넷이 틀 안에서 따로 덜컹거리고, 촛불은 그녀 반대쪽으로 누우며 어두워져요. 지붕 마루의 까마귀 넷은 고리가 닿는 순간 날아올랐다가 4.5~5.5초 뒤 제자리로 돌아와요. 땅 안개는 고리가 닿은 너비만큼 걷혔다가 1초에 0.22 씩 다시 차요. 첫 판은 그녀가 판 가운데보다 오른쪽에 떠서 바람에 날린 머리가 판 밖으로 나갔고, 옷자락이 깃발처럼 가로로 날렸어요. 우는 순간에는 머리 위로 터지는 바람에 머리카락이 위로 솟아 빗자루처럼 보였어요. 떠다니는 가운데를 32 에서 27 로 옮기고, 옷자락의 무게를 22 에서 40 으로 올리고 바람을 받는 정도를 0.07 에서 0.04 로 내렸어요. 터지는 바람은 머리에서 바깥으로만 밀지 않고 바람 부는 쪽으로 반을 보탰어요. 머리 가닥은 밝기 0.75 라 한 장의 천처럼 보여서 0.55 로 낮췄어요. 첫 판의 움직임은 0.660, 노브 반응은 0.08, 0.10, 0.05, 0.06 이었고 프레임당 4.4 ms 가 걸렸어요. 고리마다 모든 LED 의 거리를 두 번 재던 것을 한 번에 재서 배경을 휘게 하고 고리 빛도 더하게 바꿨더니 2.3~2.8 ms 가 됐어요. Fury 는 고리 밝기만 바꿔서 약했어요. 번쩍임을 0.45 + 1.2 × Fury 로 두고 고리 폭을 0.9 에서 2.3 까지 넓히자 0.06 에서 0.11 이 됐어요. Wind 는 머리와 옷자락이 얇아 0.05 였어요. 센 바람에 그녀를 바람 쪽으로 8 LED 옮기자 머리가 판 안에 남고 0.07 이 됐어요. 까마귀는 처음에 3.2초를 날아서, 다음 울음이 올 때까지 마루가 비어 있었어요. 나는 시간을 2.2초로 줄였어요. 검은 까마귀는 보라 지평선과 초록 번쩍임 앞에서만 읽혀서, 하늘을 지붕 마루 높이에서 가장 밝게 두었어요. 잰 값은 움직임 0.553, 채움 0.726 이에요. + +### Specimen Shelf + +주제 6 으로 표본 병 세 개가 놓인 선반을 그렸어요. README 에 남아 있던 감이고, 47판 Specimen Jar 의 병과 구 눈알 그리기를 가져와 세 병이 같은 코드를 써요. 위 선반에는 퓨터 뚜껑을 덮은 독한 초록 병과 코르크 마개의 호박색 플라스크가 안쪽 24 LED 폭으로 서고, 지름 14 LED 눈알이 하나씩 떠요. 아래 선반의 보라 병은 안쪽 46 LED 폭에 지름 22 LED 눈알이에요. 초록 병의 눈은 홍채가 자홍이고 호박색 병의 눈은 파랑이에요. 보라 병의 눈은 금빛이에요. 12초 한 바퀴에서 0.6초에 한 눈이 돌아 나를 똑바로 봐요. 다른 눈은 1.3초에 그 눈을 흘끗 보고 1.8초에 따라 돌고, 마지막 눈은 2.1초에 보고 2.6초에 돌아요. 4.4초까지 같이 보는 동안 동공이 홍채의 0.36 에서 0.72 로 열리고 액체가 1.6배 환해져요. 4.4초에 모두 한꺼번에 같은 곳으로 눈을 돌리고, 5.3초에 위로 1.75 라디안 굴렀다 내려와 깜빡임을 대신해요. 떠 있는 눈알에는 눈꺼풀이 없어서예요. 그 뒤로는 저마다 떠다니고, 바퀴마다 먼저 보는 눈이 바뀌어요. 처음 값에서 선반은 6.3초에 처음 두드려지고 그 뒤 11.4초마다 위와 아래가 번갈아 맞아요. 맞은 선반은 2 LED 옆으로 흔들리고 먼지가 떨어져요. 액체는 유리 벽에서 3줄 기울어 2초쯤 출렁이며 빛나요. 흔들린 거품이 한꺼번에 오르고, 응시하지 않던 눈은 모두 소리 난 쪽을 봐요. 첫 판에서는 왼쪽 위 병이 흰 쐐기로 칠해졌어요. 벽에 번지는 빛을 병마다 차례로 정하다가 뒤의 병이 앞 병의 안쪽 칸까지 제 것으로 적어서, 초록 액체를 호박색 병의 폭으로 그렸어요. 금빛 홍채는 색상 0.14, 채도 0.8 에 0.75~1 을 곱하니 카키색으로 보였어요. 색상 0.12, 채도 0.72 로 바꾸고 밝기를 0.9 아래로 내리지 않았어요. 첫 판의 잰 값은 움직임 0.233, 복잡도 0.506 이었고 노브는 0.15, 0.09, 0.03, 0.02 였어요. Knock 는 2~3초 창에 두드림이 2.4초에 한 번 들어와 0.06 이었다가 0.02 로 식었고, 창 앞쪽 12 프레임은 차이가 0 이었어요. 출렁임을 두 배 남짓 세게 하고 흔들린 액체가 빛나게 했어요. 노브 끝의 첫 두드림을 1.5초로 당기자 0.04 가 됐어요. Bubbles 는 1초에 3~30개에서 4~60개로 늘리고 고리를 키워 0.03 에서 0.06 이 됐어요. 그리기의 hypot 을 sqrt 로 바꾸자 프레임당 2.22 ms 가 1.54 ms 가 됐어요. 지름 14 LED 눈도 홍채와 핏줄, 분홍 시신경까지 그리면 눈알로 읽혀요. 위의 작은 눈이 아래 큰 눈을 흘끗 볼 때는 홍채가 오른쪽 아래로 쏠리는 것만으로 서로 보는 것이 읽혔어요. 잰 값은 움직임 0.288, 복잡도 0.511, 채움 0.831, 색 0.255 이고 노브는 0.15, 0.09, 0.06, 0.04 예요. + +### Lost Head + +주제 6 으로 묘비 위에 놓인 머리를 하나씩 써 보며 제 머리를 찾는 목 없는 해골을 그렸어요. 50판 뒤에 적혀 있던 감이고, Skull Swap 의 뼈 조각 그리기와 몸 짜기를 그대로 가져와 몸 하나를 0.85 배로 세웠어요. 해골은 x 20 에 서고 카메라가 따라가서, 높이 35줄의 묘비가 48 LED 간격으로 오른쪽에서 들어와요. 묘비 사이는 정면을 본 채 옆걸음으로 가요. 한 발은 땅에 붙어 몸과 함께 뒤로 밀리고 다른 발이 앞으로 건너가서 게걸음처럼 읽혀요. 틀린 머리는 다섯이에요. 큰 머리는 두 손으로 들어 얹으면 목을 지나 갈비뼈를 덮으며 미끄러져 내려가요. 해골은 커다란 머리에 다리가 달린 꼴로 팔을 벌리고 비틀거려요. 작은 머리는 0.78 배라 목 위에서 점점 크게 흔들리다 옆으로 떨어지고, 양옆에 들고 있던 손 가운데 오른손이 받아요. 마녀 모자를 쓴 머리는 모자가 눈구멍 위로 흘러내려요. 한 손으로 챙을 들어 올리면 주황 눈이 보였다가 다시 덮여요. 무는 머리는 집으려는 손을 물고 늘어지고, 해골은 그 손을 높이 들고 흔들다 묘비로 내던져요. 호박은 목에 얹고 옆을 두드려 봐요. 하나를 내려놓을 때마다 어깨를 으쓱해요. 맞는 머리는 얹는 순간 머리 둘레의 빛무리가 희게 번쩍이고, 눈구멍과 계단진 빛무리가 독한 초록으로 남아요. 3초 동안 엉덩이에 손을 얹고 양옆으로 발을 차다가 머리가 튀어 올라 왼쪽 풀밭에 떨어져 굴러 나가요. 해골은 빈 목을 두드리고 으쓱한 뒤 다음 묘비로 가요. 노브 3 은 맞는 머리 앞의 틀린 머리 수예요. 첫 판의 마지막을 큰 머리로 맞춰서 노브 값마다 여는 머리가 달라요. 처음에는 머리를 두 손으로 옆을 잡게 했더니, 오른쪽 묘비로 뻗는 왼팔이 갈비뼈를 가로질러 몸통이 엉킨 선이 됐어요. 큰 머리만 두 손으로 들고, 나머지는 오른손으로 정수리를 쥐고 왼손을 엉덩이에 얹었어요. 지나가는 묘비 위의 머리를 몸 뒤에 그렸더니 걷는 동안 갈비뼈 안에 머리가 든 것처럼 보였어요. 묘비를 길 앞에 세워 몸이 그 뒤로 지나가게 하자 다리가 묘비에 가려 앞뒤가 읽혔어요. 큰 머리를 17 LED 내리면 가슴만 덮여 머리 큰 해골로만 읽혀서, 정수리가 어깨 바로 아래에 오도록 6 + 18 × 크기 LED 내렸어요. 파일은 처음에 28.9 KB 였어요. 안개를 빼고 노브 4 를 크기로 바꾸고 달의 운석구를 테 하나와 바다 하나로 줄여 25,157 바이트로 맞췄어요. 처음 잰 판은 움직임 0.941 에 크기 노브 반응이 0.03 이었어요. 큰 머리가 1.2 에서 1.75 배 사이라 차이가 좁았어요. 1.1 에서 2 배로 넓히자 2 배에서는 머리가 엉덩이까지 덮고 반응이 0.05 가 됐어요. 잰 값은 움직임 0.945, 채움 0.898, 복잡도 0.201 이에요. + +### Creaking Door + +주제 6 으로 어두운 복도 끝에서 삐걱 열리는 문을 그렸어요. 패널의 LED 8,192개마다 눈에서 광선을 쏘아 너비 1.2 m, 높이 2.4 m 인 복도와 3 m 앞 벽의 문을 찾고, 빛을 받는 점마다 촛불까지 그은 선이 문틀 안을 지나는지, 돌아간 문짝에 막히는지 따져요. 문은 0.9×2.0 m 라 30×66 LED 로 서요. 촛불은 문 가운데보다 0.75 m 오른쪽, 문틀에서 1.4 m 안쪽 모퉁이에 있어 눈에는 보이지 않아요. 문이 경첩을 축으로 방 안쪽으로 다섯 번 덜컹이며 54~69도까지 열리는 동안, 빛은 오른쪽 문설주에서 가는 줄로 나와 왼쪽 아래로 넓어지는 쐐기가 되고 왼쪽 벽을 타고 올라가요. 처음 값에서 따뜻한 빛을 받은 LED 는 0.04초에 274개, 2초에 1,930개, 4초에 4,567개이고, 문이 닫히면 문 밑 틈의 137개만 남아요. 빛 속으로 손가락이 긴 손의 그림자가 문턱에서 눈 쪽으로 기어 나와 손가락을 오므렸다 물러나고, 다음 판에는 뾰족 모자를 쓴 누군가의 그림자가 걸어서 쐐기를 가로질러요. 한 판의 6.2~7초에 문이 0.14초 만에 쾅 닫히면 판이 0.5초 동안 3 LED 까지 흔들리고 상인방에서 먼지 28개가 떨어져요. 어둠 속에서는 문 밑 빛줄기를 두 발이 오른쪽에서 왼쪽으로 지나가고, 잠시 뒤 열쇠 구멍의 빛이 0.5초 꺼져요. 첫 판 뒤로는 열 판에 셋쯤 문이 절반만 열리다 일찍 닫혀요. 촛불 빛은 0.9 m 에서 절반이 되게 했어요. 문 앞 바닥이 촛불에서 1.6 m 넘게 떨어져 있어서 쐐기 끝의 판자는 짙은 빨강으로 남고, 쐐기 밖의 복도는 보라로 남아요. 빛의 색은 약할 때 짙은 빨강에서 셀 때 옅은 금빛으로 오르는 표라 갈색으로 가라앉지 않아요. 빛이 오르는 만큼 보라 바탕을 두 배로 빼서 복숭앗빛도 나지 않아요. 첫 판은 안개 세기가 0.05 + 0.55×Haze 라 처음 값에서 복도 위쪽이 다 붉게 떴고, 0.02 + 0.3×Haze² 로 낮추자 쐐기 밖이 보라로 돌아왔어요. 손 그림자는 처음에 촛불이 보는 문틀 평면에 그렸어요. 촛불이 0.75 m 오른쪽에 있어 빛이 눈 쪽으로 1 m 올 때마다 0.54 m 왼쪽으로 기울어서, 손가락이 대각선 줄무늬로 늘어졌어요. 그림자를 바닥 판자 위의 모양으로 그리고 1 m 에 0.3 m 만 기울이자 손으로 읽혔어요. 안개 속의 그림자도 그 바닥 모양에서 셈해서 바닥과 어긋나지 않아요. 사람 그림자는 몸통 위에 어깨와 머리를 얹으니 십자가나 열쇠 구멍처럼 보여서 뾰족 모자를 씌웠어요. 쾅 닫힌 뒤 문이 0.05 rad 로 튀면 그 틈의 빛이 바닥에 점선으로 찍혀서, 튐을 0.025 rad 로 줄이고 0.03 rad 아래로 열린 문은 닫힌 것으로 셌어요. 문 밑 빛은 처음에 0.1 m 만 퍼져 발 그림자가 보이지 않아서 0.3 m 로 넓혔어요. 처음 잰 값은 움직임 0.304, 복잡도 0.373, drift 0.210 이었고, 흔들림을 0.2초 2.4 LED 에서 0.5초 3 LED 로 늘리자 움직임이 0.327 이 됐어요. 이 패널에서 새로 본 것은 광선으로 셈한 빛이 쐐기 모양을 저절로 만든다는 것이에요. 문짝이 50도쯤 열리면 쐐기가 문틀 너비를 다 채우는데, 바로 그 각도를 넘으면서 문짝 앞면이 촛불을 받아 밝아지기 시작해요. 둘 다 촛불이 문짝 평면 너머를 보게 되는 순간이라서 같은 각도예요. 잰 값은 움직임 0.327, 복잡도 0.379, 채움 0.956, drift 0.212 이고, `thirds` 는 97/97/92 예요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -2600,7 +2659,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 25일 `dev`(`f72355d`)의 328개에 43판부터 50판까지의 여든 개를 더한 408개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 25일 `dev`(`f72355d`)의 328개에 43판부터 50판까지의 여든 개와 52판의 열 개를 더한 418개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -2646,6 +2705,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 50판의 열 개도 어느 축의 끝을 넘지 않아서 칸 경계는 30판 그대로이고, 50판까지 든 408개에서도 지도 21장이 다 `none` 이에요. 주제 1 은 49판까지의 398개에 남은 둘뿐인 칸 열일곱 가운데 움직임 × 복잡도 4,4 와 촘촘함 × 모양 바뀜 3,4 를 겨눠서, 그런 칸이 408개에서 열다섯이에요. 움직임 × 복잡도 4,4 는 1초에 바뀌는 빛이 1.49 를 넘으면서 한 화면이 deflate 로 LED 당 2.173 바이트를 넘어야 하는 칸이에요. 판 전체가 빨리 바뀌는 그림은 잡음이 되기 쉬운데, 2차원 난류는 판을 다 덮은 부드러운 장이 쉬지 않고 감겨서 두 값을 함께 넘었어요. Inverse Cascade 는 소용돌이도를 밝기로만 그릴 때 움직임 0.273, 복잡도 1.912 였어요. 등고선 띠를 넣고 흐름을 빠르게 하고 밝기 곡선을 낮추자 움직임이 1.897 이 됐지만 복잡도는 2.037 이었고, 흐름 함수로 색을 조금 비틀자 2.437 로 올랐어요. 복잡도는 이번에도 색이 몇 가지냐가 정했어요. 작은 규모에 힘을 계속 넣으면 움직임이 3.0 을 넘었지만 판이 잡음으로 읽혀서 버렸어요. 마지막 값은 움직임 1.648, 복잡도 2.340 이에요. 촘촘함 × 모양 바뀜 3,4 는 이웃 차이의 평균이 0.189~0.297 이면서 2~8초에 블록 평균이 0.331 넘게 바뀌어야 하는 칸이에요. 48판이 적은 대로 판이 크게 옮겨 다니는 그림에 잔 결을 얹는 쪽이 맞았어요. Diffusion Front 는 낱 LED 로 차고 비는 격자가 촘촘함을, 바닥부터 차올랐다 빠지는 덩어리가 모양 바뀜을 맡아요. √t 로 오르는 앞줄은 2초에서 8초 사이에 높이를 두 배밖에 늘리지 못해서 오름만으로는 모양 바뀜이 0.200 에 머물렀어요. 한 판을 6.3초로 줄여 오름과 빠짐이 다 잰 창에 들게 하자 0.373 이 됐고, 그러자 어두운 섬이 촘촘함을 0.178 로 깎아서 섬을 밝혀 0.217 로 되돌렸어요. 남은 열다섯 가운데 열은 촘촘함 축이 든 칸이고, 다섯은 복잡도가 2.173 을 넘어야 하는 칸이에요. +52판의 열 개도 어느 축의 끝을 넘지 않아서 칸 경계는 30판 그대로이고, 52판까지 든 418개에서도 지도 21장이 다 `none` 이에요. 주제 1 은 50판까지의 408개에 남은 둘뿐인 칸 열다섯 가운데 같이 돈 51판이 겨눈 촘촘함 × 색 4,4 와 가장자리 × 촘촘함 1,2 를 빼고 복잡도 × 모양 바뀜 4,4 와 가장자리 × 복잡도 3,4 를 겨눠서, 그런 칸이 418개에서 열셋이에요. 복잡도 × 모양 바뀜 4,4 는 한 화면이 deflate 로 LED 당 2.173 바이트를 넘으면서 2~8초에 4×16 칸 평균이 0.331 넘게 바뀌어야 하는 칸이에요. Gaussian Free Field 의 첫 판은 모드를 오른슈타인-울렌벡 과정으로만 굴려서 움직임 4.226, 복잡도 1.262, 모양 바뀜 0.195 였어요. 모양 바뀜은 자유장 자신이 막았어요. 자유장은 규모가 두 배가 될 때마다 분산을 같은 몫씩 더 가져서 4×16 칸 안에서도 부호가 칸 평균만큼 흔들리고, 흐림을 1.1 LED 에서 3.9 LED 로 넓혀도 모양 바뀜은 0.239 에 그쳤어요. 땅을 가장 밝은 채널이 늘 가득인 색으로 칠하고 바다를 밝기 0.15 로 내려 부호가 칸의 밝기를 정하게 하고, 가장 큰 모드가 제 값을 잊는 시간을 2.4초에서 0.7초로 줄이자 0.375 가 됐어요. 그때 복잡도가 2.141 이라, 해안선과 등고선을 음영으로 몇 단계씩 흔들어 칸 안에 들였어요. 마지막 값은 복잡도 2.256, 모양 바뀜 0.369 이고, 다른 씨앗 다섯에서도 복잡도 2.225~2.269, 모양 바뀜 0.360~0.404 라 같은 칸이에요. 가장자리 × 복잡도 3,4 는 이웃 차이 가운데 40/255 를 넘는 몫이 0.50~0.79 이면서 복잡도가 2.173 을 넘어야 하는 칸이에요. Force Chains 는 원판 사이의 검은 틈이 날카로운 차이를 맡고, 백색광 등색선의 섞인 색이 복잡도를 맡아요. 프린지 눈금을 바로잡은 판은 가장자리 0.591, 복잡도 1.784 였어요. 프린지를 늘리면 복잡도는 오르지만 원판이 거의 다 밝아져 사슬이 묻혔어요. LED 하나를 네 점에서 재 평균하자 같은 씨앗에서 복잡도가 2.105 에서 2.466 으로 올랐어요. 무늬는 부드러워졌는데 원판 테두리마다 색이 섞여 deflate 가 되풀이를 덜 찾았어요. 마지막 값은 가장자리 0.585, 복잡도 2.405 이고, 다른 씨앗 둘에서 복잡도가 2.31~2.41 이에요. 남은 열셋 가운데 열은 촘촘함 축이 든 칸이고, 셋은 복잡도가 2.173 을 넘어야 하는 칸이에요. + 23판부터 28판까지는 `*` 가 붙은 칸이 없었어요. 22판까지 남은 두 칸, 촘촘함 × 모양 바뀜 3,4 와 4,3 에 23판의 Bait Ball 과 Sequins 가 섰어요. 하나 남은 빈 칸이던 복잡도 × 채움 4,3 에는 24판의 Waggle Dance 가 섰어요. 복잡도가 2.173 을 넘고 채움이 0.558~0.815 인 자리예요. 복잡도 축 끝의 Contour Map(2.735)을 빼고 세면 차는 칸이라 `*` 가 붙지 않았고, 그 아래 복잡도 2.4~2.6 에 선 Woodpecker, Dripstone, Agate, Guilloché 는 채움이 다 0.93 을 넘었어요. 이제 지도 21장의 칸이 다 차서 `--gaps` 는 모든 줄에 `none` 을 찍어요. 주제 1 은 축 끝의 패턴 하나가 축을 늘려 새 빈 칸이 날 때까지 고를 칸이 없어요. Waggle Dance 에서 어려운 쪽은 채움이었어요. 벌집은 꺼진 LED 가 거의 없는 그림이라, 빈 방 바닥과 봉한 방 사이 이음매를 24 아래로 눌러 채움 0.732 를 냈어요. 복잡도는 켜진 LED 의 세 채널을 따로 흔드는 낟알로 쉽게 올라서, 첫 판은 오히려 2.766 으로 축 끝을 넘었어요. 25판도 기준 148개의 21장이 다 `none` 이라 주제 1 은 겨눌 칸이 없었어요. 그 대신 패턴이 하나만 선 칸을 셌어요. 148개에서 49칸이었고, 남은 자리로 짠 Penrose 가 그 가운데 여섯 칸에 둘째로 서서 158개에서는 43칸이에요. Penrose 가 겨눈 칸은 촘촘함 × 색 3,4 예요. 촘촘함 0.195~0.299 에 색 0.509 위인 자리이고, 4판의 Jacquard Loom 혼자 서 있었어요. Penrose 는 촘촘함 0.264, 색 0.583 으로 거기 섰어요. 같은 값으로 움직임 × 촘촘함 4,3 과 촘촘함 × 채움 3,3 의 Moire Rings, 촘촘함 × 복잡도 3,2 의 Peg Light, 복잡도 × 색 2,4 의 Gear Train, 채움 × 색 3,4 의 Marbling 곁에도 섰어요. 1 LED 어두운 선으로 나눈 작은 타일에 선명한 색을 여럿 두면 촘촘함과 색이 함께 높은 자리에 닿아요. 26판도 기준 158개의 21장이 다 `none` 이었어요. 패턴이 하나뿐인 칸 43 가운데 넷을 겨눴어요. Dark Field 는 검은 바탕 위의 흩은 빛으로 채움 0.263 에 촘촘함 0.222, 복잡도 2.081 이라, Foxfire 혼자이던 복잡도 × 채움 3,1 과 Peg Light 혼자이던 촘촘함 × 채움 3,1 에 씨앗 여덟이 다 들어요. Origami 는 어두운 탁자 위 종이 한 장이라 채움 0.264 이고, 2초의 정사각형이 8초에 좁은 학 기본형이 되어 모양 바뀜 0.255 로 Domino Run 혼자이던 채움 × 모양 바뀜 1,3 에 서요. Lichtenberg 는 Rabbit Ears 혼자이던 복잡도 × 모양 바뀜 4,3 에 씨앗 여덟 가운데 일곱이 들어요. 네 칸 말고는 바뀐 칸이 없어서, 168개에서 패턴이 하나뿐인 칸은 39칸이에요. 27판도 기준 168개의 21장이 다 `none` 이었고, 39칸 가운데 둘을 겨눴어요. Mitosis 는 채움 0.263, 색 0.540 으로 Neon Alley 곁의 채움 × 색 1,4 에 서요. 판의 4분의 1만 켜는 그림에서 색 0.5 를 넘기려면 켜진 LED 가 거의 다 순색 최대 밝기여야 해서, 형광의 번짐은 셈에 들지 않는 21/255 아래로 눌렀어요. Lava Lake 는 복잡도 2.222, 채움 0.271 로 Moth Lamp 곁의 복잡도 × 채움 4,1 에 서요. 복잡도는 프레임 바이트 값의 엔트로피를 거의 그대로 따라서, 어두운 지각의 결을 채널마다 0~24 전부로 넓히자 1.866 이 2.010 이 됐어요. 붉은 용암은 켜진 LED 하나에 24 를 넘는 바이트가 빨강 하나뿐이라, 이음매를 늘리기보다 주황 쪽으로 데우는 편이 채움을 덜 쓰고 복잡도를 벌었어요. 색 축의 아래 끝은 Cloud Chamber 의 0.052, 모양 바뀜 축의 아래 끝은 Climbing Wall 의 0.021 로 옮겨졌지만 혼자 서는 칸은 그대로라서, 178개에서 패턴이 하나뿐인 칸은 37칸이에요. 28판도 기준 178개의 21장이 다 `none` 이었고, 37칸 가운데 둘을 겨눴어요. Aztec Diamond 는 촘촘함 0.350, 색 0.640 으로 Millefiori 곁의 촘촘함 × 색 4,4 에 서요. 촘촘함은 가장 밝은 채널로 재서 네 색 도미노가 맞닿아도 턱이 없었고, LED 에 묶은 체커판이 그 턱을 줬어요. Stadium Billiard 는 움직임 2.362, 채움 0.186 으로 Warp 곁의 움직임 × 채움 4,1 에 서요. 공 이백 개가 1초에 125 LED 를 가면서 짧은 자취만 남겨요. 겨누지 않은 다섯 칸에도 둘째가 들어서 188개에서 패턴이 하나뿐인 칸은 30칸이에요. 29판도 기준 188개의 21장이 다 `none` 이었고, 30칸 가운데 둘을 겨눴어요. 28판이 모듈러 형식의 등고선으로 겨눠 보라던 가장자리 × 움직임 1,4(Mandelbrot 혼자)는 J-Invariant 가 움직임 0.372 로 모자랐고, Hopf Fibration 이 가장자리 0.207, 움직임 1.607 로 섰어요. 원 일흔 개가 저마다 1초에 몇 LED 씩 쓸고 가서 움직임이 높고, 원마다 옅은 빛 번짐을 둬서 가장자리가 낮아요. J-Invariant 는 복잡도 2.693, 색 0.400 으로 Spectrogram 곁의 복잡도 × 색 4,3 에 서요. 같은 판의 Cream Swirl(0.028)과 Serum Gel(0.079)이 가장자리 축의 아래 끝을 Fire 의 0.120 에서 내려서, 가장자리 칸 경계가 0.305, 0.560, 0.815 에서 0.232, 0.514, 0.796 으로 옮겨졌어요. 그래서 198개에서는 가장자리 × 촘촘함 1,2 가 23판 뒤로 처음 `*` 가 붙은 빈 칸이에요. 가장자리 0.232 아래에 촘촘함 0.091~0.195 인 자리라, 번지는 바탕 위에 잔 결이 고르게 깔린 그림이 들어설 수 있어요. 같은 지도의 1,3 과 1,4, 가장자리 × 채움 1,1 도 비었지만 끝의 패턴 하나를 빼면 차서 `*` 가 없어요. 198개에서 패턴이 하나뿐인 칸은 24칸이에요. 촘촘함이 0.299 를 넘는 패턴이 20판에서 셋 늘었고, 셋 다 그 값을 다른 길로 냈어요. Agate 는 1 LED 띠를 벽까지의 거리로 그어서, 축에 나란한 띠는 이웃 쌍의 절반에서만 차이가 나고 대각선 띠는 70% 쯤에서 나요. 방향이 고르게 섞이면 띠 너비 0.85 LED 언저리가 가장 높았어요. Millefiori 는 촘촘함을 가장 밝은 채널로 재서 코발트와 카나리아 노랑이 맞닿아도 턱이 0 이었어요. 고리마다 밝은 유리와 그 유리를 어둡게 뽑은 유리를 번갈아 넣어야 했어요. Sunglint 는 반짝임을 물결 띠마다 체커판의 한쪽 칸에만 둬서 반짝임끼리 닿지 않게 했어요. 셋 다 채움이 0.85 를 넘어서, 촘촘함 × 채움 4,4 는 꺼진 LED 없이도 닿는 칸이었어요. 21판의 Sandpile 이 일곱째예요. 칸 하나가 LED 하나이고 알 셋 옆의 알 둘이 가장 흔한 이웃 쌍이라, 밝기를 0, 2, 1, 3 차례로 올려 더미 안의 촘촘함이 0.40 언저리예요. 판의 75% 는 덮여야 0.3 을 넘어요. 22판의 Pin Art 와 Night Flight 가 여덟째와 아홉째예요. Pin Art 는 핀과 틈의 체커 결이라 이웃 쌍이 모두 핀과 틈의 쌍이고, Night Flight 는 Pancakes 의 체커 결에 1 LED 가로등을 LED 의 12.7% 만큼 얹어서 가장자리 0.258 에 촘촘함 0.328 이에요. 촘촘함과 가장자리는 가장 밝은 채널로 재서, 색을 섞는 자리마다 섞은 뒤 가장 밝은 채널을 다시 맞춰야 했어요. 땅과 강물을 섞은 자리에서 가장 밝은 채널이 0.68 까지 내려가 체커 결의 37 이 41~60 으로 넘어갔고, 그대로 둔 판은 가장자리가 0.05 높았어요. 23판의 Sequins 가 열째예요. 체커 결 없이, 가로 5 LED 와 세로 3 LED 간격으로 엇갈려 단 스팽글의 빛점과 그림자 줄로 0.327 을 냈어요. 촘촘함은 무늬의 모양보다 턱의 크기를 따라가서, 스팽글 몸통을 0.8 에서 0.6 으로 어둡게 하자 은빛 면이 0.339 에서 0.311 로 내려갔어요. 26판의 Lichtenberg 는 0.2 를 넘긴 판을 버렸어요. 판 어디서나 돋는 1 LED 잔가지로 0.248 을 냈더니 검은 직교 그물이 나무판을 덮어 미로로 보였어요. 잔가지를 탄 줄기에만 달아 빈 나무를 42% 남기자 0.130 이었어요. 촘촘함은 판 전체 이웃 쌍의 평균이라, 빈 나무가 넓게 남는 그림은 줄기 둘레가 아무리 잘아도 평균이 낮아요. 같은 판의 Dark Field 는 검은 바탕에 세균 몇백과 밝은 테두리를 판 곳곳에 흩어 0.222 를 냈어요. @@ -2704,6 +2765,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 50판의 열 개는 가장 낮은 노브가 In the Flash 의 K1 Hue 로 네 자리로 0.0339 이고, Old Well 의 K4 Fog 0.0422 와 In the Flash 의 K3 Rain 0.0431 이 그다음이에요. 노브 마흔 개가 다 0.033 을 넘어요. In the Flash 의 Hue 는 번쩍임의 색만 바꿔서, 노브를 재는 2~3초 창에 번쩍임이 든 동안만 셈에 들었어요. 첫 판은 0.0247 이었고, 번쩍임이 식는 시간을 0.13초에서 0.2초로 늘리고 빗줄기도 번쩍임 색으로 칠하고 채도를 0.3 에서 0.45 로 올려 0.0339 가 됐어요. Old Well 의 Fog 는 처음 0.03 이었고, 안개 세기를 0.12 + 0.3 × Fog 에서 0.16 + 0.42 × Fog 로 올렸어요. 이야기가 있는 그림은 첫 일을 그 창 앞으로 당겨야 셈에 들었어요. Murmuration 은 첫 부름을 1.4초에 두어 창에서 새가 모양으로 들어가는 중이라 Flock 이 0.0494 예요. Skull Swap 은 머리가 1.56초에 손을 떠나 2.12초에 남의 목에 내려앉고, Old Well 은 첫 두레박을 물 가까이에서 내려 0.5초에 닿게 했어요. Paper Lantern Ghost 의 Rain 은 등빛을 받는 빗줄기에서 라일락을 빼자 0.06 에서 0.04 로 내려가서, 장대비일수록 빗줄기를 3 LED 길게 해 0.0626 으로 되돌렸어요. +52판의 열 개는 가장 낮은 노브가 Pumpkin Drop 의 K1 Flame 로 네 자리로 0.0342 이고, Specimen Shelf 의 K4 Knock 0.0383 과 Anscombe's Quartet 의 K3 Heat 0.0387 이 그다음이에요. 노브 마흔 개가 다 0.034 를 넘어요. 이번에도 이야기가 있는 그림은 노브를 재는 2~3초 창이 정했어요. Pumpkin Drop 의 첫 판은 호박이 꼭대기에 매달린 채 시작해 1.2초에 놓여서, 그 창에는 바닥의 조각만 남아 Flame 이 0.0071, 그때 터지는 세기이던 K4 가 0.0119 였어요. 첫 바퀴를 땅에서 감아올리는 데서 시작해 창 안에 불 켠 호박이 매달려 있게 하고, 촛불이 하늘에 던지는 빛을 두 배쯤 세게 해 e 분의 1 거리를 9 LED 에서 16 LED 로 넓히자 Flame 이 0.034 가 됐어요. K4 는 호박 크기로 바꿔 0.088 이에요. 한 번 일어나는 일에 건 노브는 그 일이 창에 들지 않으면 0 에 가까웠어요. Specimen Shelf 의 Knock 은 두드림이 2.4초에 한 번 들어와 0.06 이었다가 출렁임이 식는 동안 0.02 로 내려가서, 노브 끝의 첫 두드림을 1.5초로 당기고 출렁임을 두 배 남짓 세게 해 0.04 가 됐어요. Anscombe's Quartet 의 Heat 는 첫 모양이 창에 막 흐르기 시작해서 0.02 였고, 사중주와 접기를 줄여 모양을 창 앞으로 당기고 뜨거운 걸음을 2.5배 길게 해 0.04 가 됐어요. 같은 그림의 Hue 는 색이 점에만 실려 0.01 로 바닥 아래였어요. 회귀선 위와 아래를 레벨 23 의 옅은 청록과 보라로 나눠 칠하자 0.05 를 넘었어요. 얇은 것만 물들이는 노브는 이번에도 약해서, Banshee 의 Wind 는 머리와 옷자락만 날려 0.05 였고 센 바람에 여인을 바람 쪽으로 8 LED 옮기자 0.066 이 됐어요. + Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. ### 세로 64×128 @@ -2752,9 +2815,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 50판에서는 Diffusion Front 와 Old Well 이 128줄을 써요. Diffusion Front 는 앞줄이 바닥에서 √t 로 올라 4.8초에 121줄에 닿아서 `thirds` 가 47/62/77 이에요. 이어지지 않은 섬을 끈 K4 0 에서도 15/47/73 이라, 세 칸을 덩어리와 앞줄이 채워요. Old Well 은 지붕부터 물까지 한 장면이라 96/80/63 이고, 돌벽과 뒷벽, 흙을 지우고 재도 90/14/21 이에요. 2~8초에 두레박이 46줄까지 올라오고, 한 바퀴 12.5초 동안에는 28줄에서 127줄까지 다녀요. Inverse Cascade(98/98/98)와 Paper Lantern Ghost(96/97/66), Murmuration(86/71/57), Skull Swap(84/80/82), Motility Clusters(67/78/71), Risset Rhythm(44/48/40), Bak-Sneppen(39/96/99)도 128줄을 다 쓰지만 주제 4 로 세지 않았어요. In the Flash 는 번개 사이가 어두워서 70/59/15 예요. +52판에서는 Force Chains 와 Pumpkin Drop 이 128줄을 써요. Force Chains 는 지름 6.3 과 8.1 LED 의 원판 175개가 패널보다 양옆으로 2 LED 넓은 상자를 바닥부터 꼭대기까지 채워서 `thirds` 가 62/65/73 이에요. 맨 위의 피스톤이 누르는 동안에는 사슬이 피스톤 바로 아래까지 서서 위 칸도 비지 않아요. Pumpkin Drop 은 호박이 꼭대기 도르래까지 감겨 올라가 81줄을 1.04초에 떨어지고 바닥에서 터져서, 한 바퀴 10초 동안 호박이 지나가지 않는 줄이 없어요. 하늘을 맨 위에서도 밝기 0.3 으로 두어 `thirds` 는 96/97/98 이고, 검은 도르래 틀도 그 하늘 앞이라 읽혀요. Zonal Jets(100/100/100)와 Gaussian Free Field(96/96/96), Creaking Door(97/97/92), Lost Head(85/87/96), Specimen Shelf(70/90/88), Banshee(41/98/77)도 128줄을 다 쓰지만 주제 4 로 세지 않았어요. Zonal Jets 는 제트 띠가 128줄에 쌓이지만 바닥 밝기 0.25 가 모든 LED 를 켜서 `thirds` 가 띠에 대해 말해 주는 것이 없어요. Anscombe's Quartet 은 점구름이 회귀선 가운데에 모여 13/37/9 이고, de Bruijn Sequence 는 어두운 판에 타일과 실만 켜져 32/27/40 이에요. + ### 낯선 수학 -주제 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)이에요. +주제 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)이에요. 52판은 다섯을 더했어요. 슈람과 셰필드가 등고선을 SLE(4) 로 보인 2차원 가우스 자유장(Gaussian Free Field), 당튀와 마즈무다르, 베링거의 광탄성 힘 사슬(Force Chains), 앤스컴의 사중주와 마테이카, 피츠모리스의 같은 통계 담금질(Anscombe's Quartet), 드 브라윈 수열과 BEST 정리(de Bruijn Sequence), 라인스의 베타 평면 띠 제트(Zonal Jets)예요. 목록 밖에서 다음에 해 볼 것이에요. 민코프스키의 물음표 함수(1904)는 페리 나무를 이진 나무로 펴는 계단이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -2804,6 +2869,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 50판에서 본 것이에요. 잰 창 2~8초가 수학의 시간표를 정했어요. Diffusion Front 의 앞줄은 √t 로 올라서 그 창에서 높이를 두 배밖에 늘리지 못해, 한 판을 6.3초로 줄여 오름과 빠짐을 창 안에 넣었어요. Bak-Sneppen 은 고르게 던진 막대에서 문턱까지 오르는 6,000걸음을 1.8초에 넣고, 걸음 수를 6000^u 로 늘려 틈 선이 고른 빠르기로 오르게 했어요. Motility Clusters 는 첫 화면 전에 4초를 미리 헤엄치게 해서 첫 프레임이 Node 에서 130 ms 쯤 걸려요. 작은 판은 문턱을 낮췄어요. 64종의 Bak-Sneppen 은 틈이 0.645 ± 0.014 에서 멈춰 무한한 고리의 0.667 에 닿지 않았고, 사태 크기의 기울기도 이론의 −1.07 보다 얕은 −0.84 였어요. 같은 규칙을 1,024종에서 세면 −0.92 예요. 물리를 그대로 적분하는 길은 이번에도 걸음이 막았어요. 무른 스프링의 원판은 Pe 320 에서도 뭉치지 않아서 겹친 만큼 떼어 놓는 과감쇠 극한으로 바꿨고, Inverse Cascade 는 AB2 가 터져서 걸음 길이가 바뀌어도 되는 AB3 로 바꿨어요. 주기가 정확한 착각은 픽셀까지 정확했어요. Risset Rhythm 은 4초 뒤의 프레임이 24,576 채널 값 가운데 하나도 다르지 않아요. +52판에서 본 것이에요. 수학이 말하는 수를 이번에도 패턴의 셈으로 재 봤어요. Gaussian Free Field 는 장을 120초 굴려 패널 평균 분산이 0.6271 로 이론 값 0.6290 과 맞았어요. 같은 짓기를 16×32 에서 128×256 까지 상자 넷에 옮겨 K3 1 의 해안선 길이를 세니, 상자가 두 배가 될 때마다 1.44, 1.46, 1.46 제곱으로 늘어 SLE(4) 의 차원 1.5 에 다가갔어요. Anscombe's Quartet 은 세 노브 자리에서 120초씩 돌린 프레임 9,221장에서 다섯 통계가 한 장도 빠짐없이 그대로였어요. de Bruijn Sequence 는 규칙과 차수마다 창이 다 서로 다르고, 시프트 레지스터가 000000 을 한 번도 만들지 않는 것을 셌어요. 정리가 그림을 막기도 했어요. 상관 0.82 는 축을 어떻게 늘려도 공분산 타원을 3.1 대 1 보다 둥글게 만들지 못해서, Anscombe's Quartet 의 원과 별은 점의 24% 를 회귀선 양 끝의 덩어리 둘로 보내고서야 둥글게 섰어요. 앤스컴의 셋째와 넷째 자료에서 바깥 점 하나가 하는 일이에요. 시간으로 거친 확률 과정은 패널에서 잡음이 됐어요. Gaussian Free Field 의 모드를 오른슈타인-울렌벡 과정 그대로 굴리면 가장 큰 모드도 프레임마다 진폭의 17% 를 새로 받아 판 전체가 끓었고, 모드마다 지수 필터로 한 번 더 걸러서야 해안선이 흘렀어요. 점성으로 잘라 둔 파수 바로 아래가 이번에도 그림을 망쳤어요. Zonal Jets 는 초점성 12 에서 그 껍질의 엔스트로피가 강제 껍질의 다섯 배까지 튀어 세 LED 길이의 잔물결 조각이 났고, 80 으로 올리자 만분의 1 아래로 내려갔어요. 50판 Inverse Cascade 가 베타 평면을 노브에서 뺀 빗살도 이것이었어요. 무늬가 흐름을 거슬러 가는 것도 보였어요. Zonal Jets 의 흐름선 굽이는 1초에 20 LED 씩 서쪽으로 미끄러지는데, 같은 줄의 물은 3.3 LED 씩 동쪽으로 흘러요. 로스비 파의 서쪽 표류예요. 빛을 물리대로 셈하면 색이 저절로 늘었어요. Force Chains 는 400~700 nm 의 31 파장을 CIE 등색 함수로 더해 등색선을 칠하자 복잡도가 2.0 을 넘었고, LED 하나를 네 점에서 재 평균하자 2.4 가 됐어요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 모자 타일(스미스, 마이어스, 캐플런, 굿먼-스트라우스, 2023)은 한 가지 타일로만 평면을 비주기로 덮고, 뒤집어 놓인 타일이 판에 드문드문 흩어져요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 뷔퐁의 바늘(1777)은 Bertrand Paradox 와 같은 꼴로, 금을 넘는 바늘의 몫이 2/π 로 모이는 막대와 함께 세울 수 있어요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. @@ -2838,7 +2905,9 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 목록 밖에서 49판 뒤에 해 볼 것이에요. 깎은 정이십면체는 무작위 모서리 전개 5,000개 가운데 마흔 몇 개가 겹쳐서, Dürer's Unfolding 의 셈으로 겹치는 그물과 겹치지 않는 그물을 나란히 펼치면 셰퍼드의 추측이 왜 아직 열려 있는지 보여요. 에너지를 조금씩 잃는 굳은 몸은 가장 큰 축으로 옮겨 가 돌아요. 1958년 익스플로러 1호가 휘는 안테나 때문에 가장 작은 축에서 가장 큰 축으로 돌아누웠어요. Tennis Racket 의 적분에 감쇠를 걸면 폴호드를 건너 큰 축으로 내려앉는 길을 그릴 수 있어요. 허버트 본(1977)은 곡선 위 점 쌍을 뫼비우스 띠로 놓아 모든 조르당 곡선이 직사각형을 품는다는 것을 보였어요. Square Peg 의 곡선에서 그 띠를 세워 그리면 띠가 제 몸을 지나는 자리가 직사각형이에요. -목록 밖에서 50판 뒤에 해 볼 것이에요. 라인스(1975)의 베타 평면에서는 거꾸로 가는 캐스케이드가 띠 모양 제트로 바뀌어요. Inverse Cascade 에서는 제트 앞에 격자 크기의 빗살이 나와 노브에서 뺐는데, 걸음을 줄이면 세로 128줄에 제트 대여섯 줄이 설 만해요. 지프와 사포발(1986)의 앞줄 걷기는 기울기 위에서 앞줄 하나만 따라 걸으며 문턱을 재요. Diffusion Front 의 규칙으로 그 걸음이 128줄을 오르내리는 것을 그릴 수 있어요. 헤엄치는 방향이 일정하게 도는 카이랄 활성 원판(벤노 리브헨, 데미안 레비스, 2017)은 도는 빠르기를 올리면 운동성 상분리가 작은 맴돌이 덩어리로 쪼개져서, Motility Clusters 의 셈에 노브 하나를 더하면 돼요. 박-스네펜의 가장 낮은 종 자리를 한 줄에 한 점씩 쌓아 내리면 사태의 시공간 그림이 프랙탈 계단으로 128줄을 채워요. 리세의 가속에 고른 펄스 대신 3+3+2 같은 박자 꼴을 실으면 같은 줌 안에서 박자 꼴이 끝없이 빨라져요. +목록 밖에서 50판 뒤에 해 볼 것이에요. 지프와 사포발(1986)의 앞줄 걷기는 기울기 위에서 앞줄 하나만 따라 걸으며 문턱을 재요. Diffusion Front 의 규칙으로 그 걸음이 128줄을 오르내리는 것을 그릴 수 있어요. 헤엄치는 방향이 일정하게 도는 카이랄 활성 원판(벤노 리브헨, 데미안 레비스, 2017)은 도는 빠르기를 올리면 운동성 상분리가 작은 맴돌이 덩어리로 쪼개져서, Motility Clusters 의 셈에 노브 하나를 더하면 돼요. 박-스네펜의 가장 낮은 종 자리를 한 줄에 한 점씩 쌓아 내리면 사태의 시공간 그림이 프랙탈 계단으로 128줄을 채워요. 리세의 가속에 고른 펄스 대신 3+3+2 같은 박자 꼴을 실으면 같은 줌 안에서 박자 꼴이 끝없이 빨라져요. + +목록 밖에서 52판 뒤에 해 볼 것이에요. 같은 가우스 자유장 위에 각도마다 흐름선을 그으면 한 점에서 부채처럼 갈라지는 SLE(κ) 곡선들이 서요. 제이슨 밀러와 스콧 셰필드의 허수 기하(2016)예요. 균열 끝의 응력장(조지 어윈, 1957)을 Force Chains 의 백색광 등색선 표로 칠하면, 균열이 자라는 동안 그 끝에 나비 모양 무늬가 따라가요. 마테이카와 피츠모리스는 같은 논문에서 다섯 수 요약이 같은 상자 그림 옆에서 점들이 한 덩어리에서 두 덩어리로 갈라졌다 모이는 것도 보였어요. 드 브라윈 토러스는 4×4 판 위에 모든 2×2 무늬가 꼭 한 번씩 나오는 2차원 짝이라, 패널을 그 판으로 덮고 창을 상하좌우로 굴려 볼 만해요. 베타 평면에서 마찰을 더 낮추고 잠재 소용돌이도 q + βy 의 등고선으로 칠하면, 동쪽 제트가 섞임을 막는 벽이 되는 PV 계단(데이비드 드리첼, 마이클 매킨타이어, 2008)이 보여요. ### 화장품 팝업 @@ -2906,7 +2975,7 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 47판에서 본 것이에요. 불이 켜진 구멍은 껍질보다 밝아야 구멍으로 읽혔어요. Pumpkin Carving 의 켜진 얼굴은 처음에 (197,121,10) 으로 껍질 (248,148,25) 보다 어두워서 무늬처럼 보였고, 구멍을 흰빛으로 올리고 껍질을 붉은 주황으로 내리자 얼굴이 섰어요. 노브를 재는 2~3초 창이 이야기의 처음을 정했어요. Web Tremor 는 나방이 처음 부딪히는 때를 1.1초로 당겨 그 창에 빛이 차게 하자 Hue 가 0.024 에서 0.072 로 올랐고, Pumpkin Carving 은 칼이 첫 입을 반쯤 돈 자리에서 시작해요. 얇은 것만 물들이는 색 노브는 이번에도 약했어요. Web Tremor 의 빛은 실 둘레로 번지게 하고서야 Hue 가 0.03 을 넘었어요. 어두운 노랑은 이번에도 올리브 갈색이 돼서, Specimen Jar 는 노란 홍채의 밝기를 0.6 아래로 내리지 않았어요. 매끈한 촛불 빛은 이번에도 복잡도를 올려서 Talking Board 는 1.02 에서 0.40 으로, Pumpkin Carving 은 1.018 에서 0.224 로 계단을 지어 내렸어요. -47판 뒤에 해 볼 것이에요. 선반 위에 병 셋을 두고 색이 다른 액체에 눈알을 하나씩 넣어, 한 눈이 응시하면 나머지도 차례로 돌아보게 하면 Specimen Jar 의 구 그리기를 그대로 써요. Web Tremor 의 그래프 파동을 덤불에 이어 걸린 거미줄 여러 장에 걸면, 빛이 한 그물에서 다른 그물로 건너가는 것을 보여 줄 수 있어요. +47판 뒤에 해 볼 것이에요. Web Tremor 의 그래프 파동을 덤불에 이어 걸린 거미줄 여러 장에 걸면, 빛이 한 그물에서 다른 그물로 건너가는 것을 보여 줄 수 있어요. 48판이 다섯을 더 그렸어요. 보름달 앞 마른 나무에서 내리꽂히는 원숭이올빼미(Barn Owl), 불붙은 호박을 던지는 머리 없는 기수(Headless Rider), 무너졌다 다시 맞춰지는 해골(Bone Pile), 손전등을 들고 사탕을 받으러 가는 아이들(Trick or Treat), 낫을 휘두르며 달 앞을 떠가는 사신(Grim Reaper)이에요. 움직임은 0.347 에서 0.759, 복잡도는 0.242 에서 0.575 예요. @@ -2924,7 +2993,13 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 50판에서 본 것이에요. 검은 것은 이번에도 밝은 것 앞에서만 읽혔어요. Murmuration 은 하늘을 맨 위까지 밝기 0.8 아래로 내리지 않았고, 떼가 왼쪽 나무 앞을 돌 때 새가 사라져서 떼의 길을 5 LED 올렸어요. 떼가 모양을 지을 때는 새 하나가 덮는 넓이가 그림을 정했어요. 0.72√(넓이/새 수) + 0.38 반지름으로는 해골이 2 LED 격자마다 분홍 점이 박힌 체였고, 0.9√(넓이/새 수) + 0.5 로 넓히자 검게 찼어요. 보이지 않는 동안 움직이는 것은 셈에 들지 않았어요. In the Flash 의 형체는 번쩍임 사이에만 다가와서, 움직임 0.551 은 판 전체를 바꾸는 번쩍임이 다 맡았어요. 번쩍임이 0.009 까지 식은 꼬리에서는 4 단위 반올림이 형체 자리만 한 계단 다르게 남겨 어둠에 형체가 비쳤고, 0.02 아래를 0 으로 잘라 지웠어요. 사물의 꼴은 이번에도 다른 것으로 읽혔어요. In the Flash 의 18 LED 짜리 먼 형체는 빗살로, 38×88 달걀꼴 머리는 외계인으로 보였어요. Old Well 의 달 바다 무늬 둘은 웃는 얼굴로, 사인 둘로 그린 물빛은 갈매기 무늬로 보였어요. 두 몸이 나란히 서기에는 64 너비가 좁았어요. Skull Swap 은 Bone Pile 의 몸을 0.8 배로 줄여도 두 팔을 들면 손이 판 밖 5.6 LED 까지 나가서, 갈비를 0.85 배로 좁히고 팔과 다리를 줄여 둘을 32 LED 씩에 넣었어요. 주황은 이번에도 섞이면 색을 잃었어요. Paper Lantern Ghost 의 등 가까운 빗줄기는 라일락 바탕에 주황을 더하자 복숭앗빛이어서, 등빛을 받는 만큼 라일락을 빼서야 주황으로 읽혔어요. -50판 뒤에 해 볼 것이에요. 까마귀 떼가 해골 대신 빗자루 탄 마녀가 되어 해 앞을 건너가게 하면 모양이 머무는 동안에도 판 위를 움직여요. 두레박 대신 등불을 줄에 매달아 우물에 내리면, 내려갈수록 벽에 비친 빛 동그라미가 작아지다가 물 위에서 무언가가 불을 꺼요. 처마 끝에 작은 등 넷이 매달려 하나씩 눈을 뜨고 웃음이 옆 등으로 옮아가는 장면은 Paper Lantern Ghost 의 등 그리기를 크기만 줄여 써요. 목 없는 해골 하나가 묘비 위에 놓인 머리들을 하나씩 써 보며 제 머리를 찾는 장면은 Skull Swap 의 머리 셈을 그대로 써요. +50판 뒤에 해 볼 것이에요. 까마귀 떼가 해골 대신 빗자루 탄 마녀가 되어 해 앞을 건너가게 하면 모양이 머무는 동안에도 판 위를 움직여요. 두레박 대신 등불을 줄에 매달아 우물에 내리면, 내려갈수록 벽에 비친 빛 동그라미가 작아지다가 물 위에서 무언가가 불을 꺼요. 처마 끝에 작은 등 넷이 매달려 하나씩 눈을 뜨고 웃음이 옆 등으로 옮아가는 장면은 Paper Lantern Ghost 의 등 그리기를 크기만 줄여 써요. + +52판이 다섯을 더 그렸어요. 도르래 틀에서 놓여 128줄을 떨어져 터지는 호박 등(Pumpkin Drop), 오두막 위에서 고개를 젖히고 초록 소리 고리를 내지르는 밴시(Banshee), 세 병 속 눈알이 하나씩 돌아보는 표본 선반(Specimen Shelf), 묘비 위 머리를 써 보며 제 머리를 찾는 목 없는 해골(Lost Head), 삐걱 열린 문틈으로 촛불 쐐기와 손 그림자가 기어 나오는 복도(Creaking Door)예요. 표본 선반은 47판 뒤에, 머리를 찾는 해골은 50판 뒤에 적혀 있던 감이에요. 움직임은 Pumpkin Drop 의 0.152 에서 Lost Head 의 0.945 까지, 복잡도는 Lost Head 의 0.201 에서 Banshee 의 0.745 까지예요. + +52판에서 본 것이에요. 빛을 더하지 않고 배경을 휘게 해도 소리가 보였어요. Banshee 의 소리 고리는 지나는 자리의 뒤 배경을 2.4 LED 까지 밀어서, 46판 Howling Wolf 의 고리처럼 이미 255 인 곳에 빛을 더해 셈에서 빠지는 일이 없었어요. 광선으로 셈한 빛은 모양을 저절로 만들었어요. Creaking Door 는 LED 마다 촛불까지 그은 선이 문틀과 문짝에 막히는지 따져서, 문이 50도쯤 열리면 쐐기가 문틀 너비를 다 채우고 같은 각도에서 문짝 앞면이 촛불을 받아 밝아지기 시작해요. 둘 다 촛불이 문짝 평면 너머를 보게 되는 순간이에요. 그림자는 물리대로 그리면 읽히지 않았어요. 빛이 눈 쪽으로 1 m 올 때마다 0.54 m 기울어 손가락이 대각선 줄무늬로 늘어져서, 그림자를 바닥 판자 위의 모양으로 그리고 0.3 m 만 기울였어요. 사물의 꼴은 이번에도 다른 것으로 읽혔어요. Creaking Door 의 사람 그림자는 몸통에 어깨와 머리를 얹으니 십자가나 열쇠 구멍이어서 뾰족 모자를 씌웠어요. Banshee 의 머리카락은 터지는 바람에 위로 솟아 빗자루였고, 밝기 0.75 의 가닥 열여섯은 한 장의 천이었어요. 터지는 바람에 바람 부는 쪽을 반 보태고 가닥을 0.55 로 낮추자 머리로 읽혔어요. Pumpkin Drop 의 첫 얼굴은 뚫린 자리 둘레까지 빛을 칠해 주황 공에 노란 얼룩이었고, Lost Head 는 지나가는 묘비 위 머리를 몸 뒤에 그리자 갈비뼈 안에 머리가 든 것처럼 보여서 묘비를 길 앞에 세웠어요. 어두운 노랑은 이번에도 색을 잃어서, Specimen Shelf 의 금빛 홍채는 밝기 0.75 까지 내려가자 카키색이었고 밝기를 0.9 아래로 내리지 않았어요. 주황 빛이 보라 바탕에 더해지면 이번에도 복숭앗빛이어서, Creaking Door 는 빛이 오르는 만큼 보라 바탕을 두 배로 뺐어요. 25 KB 가 그림을 깎기도 했어요. Lost Head 는 처음 28.9 KB 라 안개를 빼고 달의 운석구를 테 하나와 바다 하나로 줄였어요. + +52판 뒤에 해 볼 것이에요. 바닥의 투석기가 호박을 꼭대기까지 쏘아 올렸다 떨어뜨리면 오름과 떨어짐이 한 포물선에 담겨 128줄을 두 번 써요. 배경을 휘게 하는 Banshee 의 고리로는 물에 잠긴 종탑의 종이 울려 수면과 달이 일렁이는 장면을 그릴 만해요. 표본 병 속에 눈알 대신 뛰는 심장을 넣으면 선반을 두드릴 때마다 박동이 빨라져요. Lost Head 의 굴러간 머리를 다른 해골이 주워 쓰고 달아나면, 같은 몸 짜기로 머리 하나를 두고 쫓는 장면이 돼요. Creaking Door 의 광선 셈으로는 창밖 번개가 창틀과 나뭇가지 그림자를 방바닥에 던지는 장면을 그릴 수 있어요. ## 다시 돌리기 From b8de4be35c22bad36afee189fb38b741f2e232fc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 00:56:59 +0000 Subject: [PATCH 09/10] =?UTF-8?q?52=ED=8C=90:=20index.html=20=EC=9D=84=204?= =?UTF-8?q?18=EA=B0=9C=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20=EC=A7=93=EB=8A=94?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LXsMxtcgxEaHMjeqmCbsjv --- 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 ca8ec4775..3ee26ef86 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

패턴 연구

- - + + - + +