From 677aa52f7be45baec61c4f27e0f486a09b16aff3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 13:13:48 +0000 Subject: [PATCH 1/8] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=20=EC=97=B0=EA=B5=AC=2061=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_01JJpBXX9YfaGEvtuHofsFxv From 0932acb0ae43b85563a01d3bbd25111a1d9fe6b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 14:06:07 +0000 Subject: [PATCH 2/8] pattern-study: Crackling Noise, Monge Transport for round 61 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JJpBXX9YfaGEvtuHofsFxv --- .../pattern-study/patterns/crackling-noise.js | 269 ++++++++++ .../pattern-study/patterns/monge-transport.js | 481 ++++++++++++++++++ 2 files changed, 750 insertions(+) create mode 100644 incoming/pattern-study/patterns/crackling-noise.js create mode 100644 incoming/pattern-study/patterns/monge-transport.js diff --git a/incoming/pattern-study/patterns/crackling-noise.js b/incoming/pattern-study/patterns/crackling-noise.js new file mode 100644 index 000000000..edf2a0704 --- /dev/null +++ b/incoming/pattern-study/patterns/crackling-noise.js @@ -0,0 +1,269 @@ +// ===== Patternflow pattern ===== +// Title: Crackling Noise +// Family: noise +// Idea: A random-field Ising magnet at zero temperature is swept from all down to all up, one avalanche at a time, and each avalanche of flipped spins crawls out across the dark panel in its own colour, the colour drifting along the sweep, until the panel is a patchwork of clusters of every size; then the field is swept back down, the avalanches go dim in another patchwork, and a new disorder is dealt. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Barkhausen noise (Heinrich Barkhausen, 1919), hysteresis and avalanches in the zero-temperature random-field Ising model (James Sethna, Karin Dahmen, Sivan Kartha, James Krumhansl, Bruce Roberts and Joel Shore, 1993), crackling noise (James Sethna, Karin Dahmen and Christopher Myers, 2001), the critical disorder of the two-dimensional model (Djordje Spasojevic, Sanja Janicevic and Milan Knezevic, 2011), pattern round 61 +// Round: 61 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Disorder=1.05..2.6, Grain=0..1.6 +// +// Knob 1: Hue. Turns the whole sweep round the wheel. At 0 the first avalanches are teal and the colour drifts through blue and violet to pink by the last; at 0.5 it starts magenta, reaches red a quarter of the way through and goes on from green to cyan. The dim colours of the way back skip orange, which reads brown. +// Knob 2: Speed. At 2 the panel flips 474 spins a second, so an avalanche of 1000 spins takes 2.1 seconds to crawl out and a whole sweep up takes 17 seconds (the first opens 30% done and ends at 12); 0.2 is ten times slower, 3 half as fast again. The way down runs twice as fast, and the holds of 1.5 s at the top and 1 s at the bottom scale with the knob. +// Knob 3: Disorder. R, the width of the Gaussian random field against a coupling of 1 to each of 4 neighbours. At 0.85 and below one avalanche snaps nine tenths of the 64 x 128 panel (the critical width on an endless lattice is 0.54), so the range starts above it. At 1.05 a sweep is about 130 avalanches, the biggest 1000 to 2000 spins and 16 of 100 or more; at 1.6 about 830 with the biggest near 170; at 2.6 about 2400, four in ten of them a lone spin and none over 50. +// Knob 4: Grain. The correlation length of the field: white noise blurred by a Gaussian of 0 to 1.6 LEDs and scaled back to width R. Below 0.25 the field is uncorrelated, the classic model, and the clusters have ragged edges; at 0.8 they are rounded blobs and there are 2.5 times as many; at 1.6 the field is smooth enough that a flat edge of flipped spins holds still, so the avalanches grow as rectangles nested inside each other. + +const W = 64, H = 128, N = W * H; +const { floor, min, abs, sqrt, log, exp, cos, ceil, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// Spins flipped per second at Speed 2, and the share of the first sweep +// already done when the pattern opens. +const RATE = 474, OPEN = 0.3; +// Seconds held at the top and at the bottom of the loop, at Speed 2. +const HOLD_UP = 1.5, HOLD_DOWN = 1; +// The hue of a flip is set by how far through the sweep its avalanche started. +const SPAN = 0.62; +// Skips orange through lime, which read brown and olive when dim. +const wheel = (h) => 0.28 + 0.74 * (h - floor(h)); + +function hsv(h, s, v, out, 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); + out[o] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + out[o + 1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + out[o + 2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; +} + +function gauss() { + let u = 0; + while (u === 0) u = Math.random(); + return sqrt(-2 * log(u)) * cos(2 * PI * Math.random()); +} + +function makeSweep() { + return { + seq: new Int16Array(N), // sites in the order they flip + pos: new Int16Array(N), // where each site is in seq + av: new Int32Array(N), // the avalanche each site flipped in + start: new Int32Array(N + 1), // where each avalanche starts in seq + n: 0, // avalanches + jit: new Float32Array(N), // per avalanche: hue jitter and saturation + sat: new Float32Array(N), + col: new Float32Array(N * 3), + }; +} + +export function setup(params) { + params.s = { + R: -1, grain: -1, hue: -1, phase: 0, cur: 0, wait: 0, started: false, + white: new Float64Array(N), h: new Float64Array(N), tmp: new Float64Array(N), + key: new Float64Array(N), fl: new Uint8Array(N), + hk: new Float64Array(6 * N), hs: new Int32Array(6 * N), + up: makeSweep(), dn: makeSweep(), + bg: new Float32Array(N * 3), + }; +} + +function deal(S) { + for (let i = 0; i < N; i++) S.white[i] = gauss(); + for (const sw of [S.up, S.dn]) + for (let a = 0; a < N; a++) { + sw.jit[a] = (Math.random() - 0.5) * 0.08; + sw.sat[a] = 0.5 + 0.45 * Math.random(); + } +} + +// The frozen field: the white noise, blurred on the torus by a Gaussian of +// radius grain, then shifted to mean 0 and scaled to standard deviation R. +function makeField(S) { + const { white, h, tmp } = S, sg = S.grain; + if (sg < 0.25) h.set(white); + else { + const r = ceil(2.5 * sg), k = new Float64Array(2 * r + 1); + for (let d = -r; d <= r; d++) k[d + r] = exp((-d * d) / (2 * sg * sg)); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let a = 0; + for (let d = -r; d <= r; d++) a += k[d + r] * white[y * W + ((x + d + 4 * W) % W)]; + tmp[y * W + x] = a; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let a = 0; + for (let d = -r; d <= r; d++) a += k[d + r] * tmp[((y + d + 4 * H) % H) * W + x]; + h[y * W + x] = a; + } + } + let m = 0, v = 0; + for (let i = 0; i < N; i++) m += h[i]; + m /= N; + for (let i = 0; i < N; i++) v += (h[i] -= m) * h[i]; + const f = S.R / sqrt(v / N); + for (let i = 0; i < N; i++) h[i] *= f; +} + +// One branch of the hysteresis loop. Spin i, still down, flips up once +// h_i + H + J (up neighbours - down neighbours) > 0 with J = 1, so its +// key h_i + 2 (up neighbours) - 4 says the field H at which it goes. The +// sweep raises H just to the largest key left, flips that spin, and lets +// the avalanche run at that H, breadth first, each flip adding 2 to the key +// of its neighbours. The way back down is the same sweep for the field -h, +// since flipping every spin and both fields maps one branch onto the other. +// Keys only grow, so a heap with stale entries skipped does the search. +function sweep(S, sw, sign) { + const { key, fl, hk, hs, h } = S, seq = sw.seq; + let hn = 0; + const push = (k, s) => { + let i = hn++; + while (i > 0) { + const p = (i - 1) >> 1; + if (hk[p] >= k) break; + hk[i] = hk[p]; + hs[i] = hs[p]; + i = p; + } + hk[i] = k; + hs[i] = s; + }; + const pop = () => { + const k = hk[--hn], s = hs[hn]; + let i = 0; + for (;;) { + let c = 2 * i + 1; + if (c >= hn) break; + if (c + 1 < hn && hk[c + 1] > hk[c]) c++; + if (hk[c] <= k) break; + hk[i] = hk[c]; + hs[i] = hs[c]; + i = c; + } + hk[i] = k; + hs[i] = s; + }; + fl.fill(0); + for (let i = 0; i < N; i++) push((key[i] = sign * h[i] - 4), i); + let pos = 0, n = 0; + while (pos < N) { + let i, k; + do { + i = hs[0]; + k = hk[0]; + pop(); + } while (fl[i] || k !== key[i]); + const field = -k; + sw.start[n] = pos; + fl[i] = 1; + seq[pos++] = i; + for (let q = pos - 1; q < pos; q++) { + const s = seq[q], x = s % W, y = (s - x) / W; + for (let d = 0; d < 4; d++) { + const j = d === 0 ? y * W + ((x + 1) % W) : d === 1 ? y * W + ((x + W - 1) % W) : d === 2 ? ((y + 1) % H) * W + x : ((y + H - 1) % H) * W + x; + if (fl[j]) continue; + key[j] += 2; + if (key[j] + field > 0) { + fl[j] = 1; + seq[pos++] = j; + } else push(key[j], j); + } + } + n++; + } + sw.start[n] = N; + sw.n = n; + for (let a = 0; a < n; a++) for (let p = sw.start[a]; p < sw.start[a + 1]; p++) { + sw.av[seq[p]] = a; + sw.pos[seq[p]] = p; + } +} + +// Colours per avalanche. Up: by where it started in the sweep, a little +// jitter so neighbours from the same moment part, and brightness by size, +// 232 for a lone spin up to 255 from 400 spins. Down: the same drift, dim. +function paint(S) { + for (const [sw, up] of [[S.up, true], [S.dn, false]]) { + for (let a = 0; a < sw.n; a++) { + const m = sw.start[a] / N, size = sw.start[a + 1] - sw.start[a]; + if (up) hsv(wheel(0.27 + S.hue + SPAN * m + sw.jit[a]), sw.sat[a], 0.91 + 0.09 * min(1, log(size) / log(400)), sw.col, a * 3); + else hsv(wheel(0.6 + S.hue + 0.4 * m + 2 * sw.jit[a]), sw.sat[a] + 0.05, 0.12 + 0.24 * (sw.sat[a] - 0.5), sw.col, a * 3); + } + } +} + +function rebuild(S) { + makeField(S); + sweep(S, S.up, 1); + sweep(S, S.dn, -1); + paint(S); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 1.05, 0]; + const d = cl(dt || 0, 0, 0.1), speed = cl(kv[1], 0.2, 3) / 2; + const R = cl(kv[2], 1.05, 2.6), grain = cl(kv[3], 0, 1.6), hue = cl(kv[0], 0, 1); + if (!S.started) { + deal(S); + S.started = true; + // The first sweep opens part done, so the patchwork is already there. + S.cur = OPEN * N; + } + if (abs(R - S.R) > 1e-4 || abs(grain - S.grain) > 1e-3) { + // A new disorder width or grain reshapes the same noise and the + // sweep stays as far through as it was. + S.R = R; + S.grain = grain; + S.hue = hue; + rebuild(S); + } else if (abs(hue - S.hue) > 1e-4) { + S.hue = hue; + paint(S); + } + // 0 up, 1 held up, 2 down, 3 held down. + if (S.phase === 0 || S.phase === 2) { + S.cur += RATE * (S.phase === 2 ? 2 : 1) * speed * d; + if (S.cur >= N) { + S.cur = N; + S.phase++; + S.wait = S.phase === 1 ? HOLD_UP : HOLD_DOWN; + } + } else { + S.wait -= speed * d; + if (S.wait <= 0) { + if (S.phase === 3) { + // The dim patchwork of the way down stays under the next sweep. + for (let i = 0; i < N; i++) { + const o = S.dn.av[i] * 3; + S.bg[i * 3] = S.dn.col[o]; + S.bg[i * 3 + 1] = S.dn.col[o + 1]; + S.bg[i * 3 + 2] = S.dn.col[o + 2]; + } + deal(S); + rebuild(S); + } + S.phase = (S.phase + 1) % 4; + S.cur = 0; + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || S.R < 0) return; + const c = floor(S.cur), ph = S.phase, up = S.up, dn = S.dn; + for (let i = 0; i < N; i++) { + let col = up.col, o = up.av[i] * 3; + if (ph === 0) { + if (up.pos[i] >= c) { + col = S.bg; + o = i * 3; + } + } else if (ph === 3 || (ph === 2 && dn.pos[i] < c)) { + col = dn.col; + o = dn.av[i] * 3; + } + display.setPixel(i % W, (i / W) | 0, col[o] | 0, col[o + 1] | 0, col[o + 2] | 0); + } +} diff --git a/incoming/pattern-study/patterns/monge-transport.js b/incoming/pattern-study/patterns/monge-transport.js new file mode 100644 index 000000000..d8068271a --- /dev/null +++ b/incoming/pattern-study/patterns/monge-transport.js @@ -0,0 +1,481 @@ +// ===== Patternflow pattern ===== +// Title: Monge Transport +// Family: algo +// Idea: Particles filling one abstract shape are paired at random with places in another shape in the other half of the panel, the pairing is untangled by swapping the targets of any two particles whose swap shortens the total of |x - y|^p, so the dim target dots sort into the colours of the particles bound for them and the sample rays stop crossing, and then every particle slides along its ray at once and the pile lands as the new shape with its colour wheel rearranged by the map. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the problem of deblais et remblais and its rays that never cross (Gaspard Monge, 1781), the relaxed problem (Leonid Kantorovich, 1942), the quadratic cost and its monotone map (Yann Brenier, 1987), displacement interpolation (Robert McCann, 1997), pattern round 61 +// Round: 61 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Particles=150..400, Exponent=1..2 +// +// Knob 1: Hue. Turns the colour wheel each pile carries. A particle's hue is its angle round the middle of the shape it starts in and its saturation its distance out, pale at the middle. At 0 the opening lobes run from green and yellow on the left to pink and violet on the right; at 0.5 the wheel is turned half round, blue and violet on the left, yellow and green on the right. +// Knob 2: Speed. At 2 a round takes 7.5 seconds: 3.5 s of untangling, 2.6 s of travel and 1.4 s at rest while the colours settle into the new shape's own wheel. The opening round untangles in 1.8 s, so the first pile leaves at 1.8 s and lands at 4.4 s. 0.2 is ten times slower and 3 half as fast again. +// Knob 3: Particles. 150 to 400 in steps of 25, on a jittered grid that fills 1500 LEDs of each shape, the grid 3.2 LEDs apart at 150 and 1.9 at 400. A random pairing takes about 5 swaps a particle to untangle, 720 at 150 and 1800 at 400. +// Knob 4: Exponent. p in the cost |x - y|^p. At 1, Monge's own cost, every crossing pair of rays is a swap that shortens the total, so when the untangling ends no two rays cross; many plans cost the same there, and the pile keeps left and right but lands with top and bottom shuffled. Any p above 1, even 1.02, also sorts top to top, and the pile slides over keeping its wheel while about one pair of rays in 60 crosses at a shallow angle. At 2, Brenier's quadratic cost, one pair in 35 crosses, but no two particles are ever in the same place at the same moment. + +const W = 64, H = 128; +const { floor, min, max, abs, sqrt, atan2, cos, sin, pow, round, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// LEDs each shape fills, and seconds of each stage at Speed 2. +// The opening round untangles in T_OPEN, so its pile is moving by 2 seconds. +const AREA = 1500, T_U = 3.5, T_M = 2.6, T_R = 1.4, T_OPEN = 1.8; +// Rays drawn through the untangling and the travel, from particles spread +// out over the source. +const WITNESS = 8; +const MAXN = 400; +// Pair checks of planning a frame. +const BUDGET = 50000; + +// Filled shapes in a box from -1 to 1 across the half panel. +const SHAPES = [ + (u, v) => u * u + v * v < 0.85, // disk + (u, v) => { + const r = u * u + v * v; + return r > 0.2 && r < 0.85; + }, // annulus + (u, v) => (u + 0.45) * (u + 0.45) + v * v < 0.27 || (u - 0.45) * (u - 0.45) + v * v < 0.27, // two lobes + (u, v) => abs(v) < 0.92 && abs(u) > 0.2 && abs(u) < 0.7, // bar pair + (u, v) => v < 0.75 && abs(u) < (0.95 * (v + 0.92)) / 1.67, // triangle + (u, v) => (abs(u) < 0.3 && abs(v) < 0.95) || (abs(v) < 0.3 && abs(u) < 0.95), // cross + (u, v) => u * u + v * v < 0.92 && (u - 0.5) * (u - 0.5) + v * v > 0.4, // crescent + (u, v) => abs(u) + abs(v) < 0.95, // diamond + (u, v) => { + const m = max(abs(u), abs(v)); + return m > 0.42 && m < 0.92; + }, // square ring + (u, v) => sqrt(u * u + v * v) < 0.66 + 0.3 * cos(3 * atan2(v, u)), // trefoil +]; + +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; + return out; +} + +export function setup(params) { + params.s = { + n: 0, p: -1, g: 3, half: 0, src: 2, dst: 4, rot: 0, dRot: 0, phase: 0, t: 0, tu: T_U, tm: T_M, base: 0, job: 0, turn: false, + X: new Float64Array(2 * MAXN), Y: new Float64Array(2 * MAXN), P: new Float64Array(2 * MAXN), F: new Float64Array(2 * MAXN), + hue: new Float64Array(MAXN), sat: new Float64Array(MAXN), hue0: new Float64Array(MAXN), sat0: new Float64Array(MAXN), + hueTo: new Float64Array(MAXN), satTo: new Float64Array(MAXN), + sig: new Int32Array(MAXN), inv: new Int32Array(MAXN), work: new Int32Array(MAXN), ord: new Int32Array(MAXN), + C: new Float64Array(MAXN * MAXN), + sw: new Int16Array(2 * 60 * MAXN), cross: new Uint8Array(60 * MAXN), gain: new Float32Array(60 * MAXN), K: 0, k: 0, + wit: new Int32Array(WITNESS), flash: [], rgb: new Float32Array(W * H * 3), pc: new Float32Array(W * H * 3), pa: new Float32Array(W * H), c: [0, 0, 0], + }; +} + +// N points of shape kind in the given half: a jittered grid of spacing +// sqrt(AREA / N), and the shape scaled about the middle of the half until +// N grid points fall inside it. Extra points from the last step of the +// scale are dropped at random. +function sample(S, kind, half, rot, out) { + const N = S.n, inside = SHAPES[kind], cr = cos(rot), sr = sin(rot); + let g = sqrt(AREA / N); + for (let tries = 0; tries < 12; tries++, g *= 0.96) { + const m = Math.ceil(64 / g) + 2, pts = []; + for (let i = 0; i < m; i++) + for (let j = 0; j < m; j++) pts.push((i - (m - 1) / 2 + (Math.random() - 0.5) * 0.12) * g, (j - (m - 1) / 2 + (Math.random() - 0.5) * 0.12) * g); + // A point must also stay in its half, whatever the turn of the shape. + const inAt = (k, s) => { + if (abs(pts[k]) > 30.6 || abs(pts[k + 1]) > 30.6) return false; + const x = pts[k] / (31 * s), y = pts[k + 1] / (31 * s); + return inside(x * cr + y * sr, y * cr - x * sr); + }; + const count = (s) => { + let c = 0; + for (let k = 0; k < pts.length; k += 2) if (inAt(k, s)) c++; + return c; + }; + if (count(1.25) < N) continue; + let lo = 0.2, hi = 1.25; + for (let it = 0; it < 16; it++) { + const s = (lo + hi) / 2; + if (count(s) >= N) hi = s; + else lo = s; + } + const keep = []; + for (let k = 0; k < pts.length; k += 2) if (inAt(k, hi)) keep.push(k); + while (keep.length > N) keep.splice(floor(Math.random() * keep.length), 1); + for (let i = 0; i < N; i++) { + out[2 * i] = 32 + pts[keep[i]]; + out[2 * i + 1] = 32 + 64 * half + pts[keep[i] + 1]; + } + S.g = g; + return; + } +} + +// Each particle's colour from where it sits: hue by angle round the +// centre of the pile, saturation by distance out. +function wheelOf(S, pos, hue, sat) { + const N = S.n; + let cx = 0, cy = 0, rm = 0; + for (let i = 0; i < N; i++) { + cx += pos[2 * i] / N; + cy += pos[2 * i + 1] / N; + } + for (let i = 0; i < N; i++) rm = max(rm, Math.hypot(pos[2 * i] - cx, pos[2 * i + 1] - cy)); + for (let i = 0; i < N; i++) { + const dx = pos[2 * i] - cx, dy = pos[2 * i + 1] - cy; + hue[i] = atan2(-dx, -dy) / (2 * PI) + 0.5; + sat[i] = 0.25 + 0.75 * pow(Math.hypot(dx, dy) / max(1, rm), 0.8); + } +} + +// Untangling. From the pairing now in sig, try pairs of particles and +// swap the targets of i and j whenever |x_i - y_b|^p + |x_j - y_a|^p is +// shorter than |x_i - y_a|^p + |x_j - y_b|^p. At p = 1 two crossing rays +// always qualify, by the triangle inequality at the point where they +// cross, so the plan that comes out has none. The work runs ahead of what +// is shown, a budget of pair checks a frame, and the swaps it finds are +// played back over the stage. +function startPlan(S) { + S.work.set(S.sig.subarray(0, S.n)); + S.K = 0; + S.k = 0; + S.job = 1; + S.jr = 0; + S.jpass = 0; + S.rng = (Math.random() * 2147483647) | 1; +} + +function record(S, i, j, a, b, d) { + if (S.K < S.cross.length) { + S.sw[2 * S.K] = i; + S.sw[2 * S.K + 1] = j; + S.gain[S.K] = d; + S.cross[S.K] = crosses(S.X, S.Y, i, a, j, b) ? 1 : 0; + S.K++; + } + S.work[i] = b; + S.work[j] = a; +} + +function newPass(S) { + const ord = S.ord; + for (let i = 0; i < S.n; i++) ord[i] = i; + for (let i = S.n - 1; i > 0; i--) { + const j = floor(Math.random() * (i + 1)), t = ord[i]; + ord[i] = ord[j]; + ord[j] = t; + } + S.jx = 0; + S.jany = false; +} + +function work(S, budget) { + const N = S.n, C = S.C, X = S.X, Y = S.Y, w = S.work, ord = S.ord, h = S.p / 2; + while (budget > 0 && S.job) { + if (S.job === 1) { + // The costs, a row a step. + const i = S.jr; + for (let j = 0; j < N; j++) { + const dx = X[2 * i] - Y[2 * j], dy = X[2 * i + 1] - Y[2 * j + 1]; + C[i * N + j] = pow(dx * dx + dy * dy, h); + } + budget -= N; + if (++S.jr === N) { + S.job = 2; + S.jr = N * N; + } + } else if (S.job === 2) { + // N^2 pairs at random from a xorshift stream, so the swaps play back + // spread over the whole pile. + let t = min(S.jr, budget), r = S.rng; + S.jr -= t; + budget -= t; + for (; t > 0; t--) { + r ^= r << 13; + r ^= r >>> 17; + r ^= r << 5; + const i = ((r >>> 16) * N) >>> 16, j = ((r & 0xffff) * N) >>> 16; + if (i === j) continue; + const a = w[i], b = w[j], d = C[i * N + a] + C[j * N + b] - C[i * N + b] - C[j * N + a]; + if (d > 1e-9) record(S, i, j, a, b, d); + } + S.rng = r; + if (!S.jr) { + S.job = 3; + newPass(S); + } + } else { + // Then whole passes over every pair, in a fresh order each time, + // until one finds nothing. + const x = S.jx, i = ord[x]; + for (let y = x + 1; y < N; y++) { + const j = ord[y], a = w[i], b = w[j], d = C[i * N + a] + C[j * N + b] - C[i * N + b] - C[j * N + a]; + if (d > 1e-9) { + record(S, i, j, a, b, d); + S.jany = true; + } + } + budget -= N - x; + if (++S.jx >= N - 1) { + if (!S.jany || ++S.jpass >= 40) S.job = 0; + else newPass(S); + } + } + } +} + +function crosses(X, Y, i, a, j, b) { + const ax = X[2 * i], ay = X[2 * i + 1], bx = Y[2 * a], by = Y[2 * a + 1]; + const cx = X[2 * j], cy = X[2 * j + 1], dx = Y[2 * b], dy = Y[2 * b + 1]; + const o = (px, py, qx, qy, rx, ry) => (qx - px) * (ry - py) - (qy - py) * (rx - px); + return o(ax, ay, bx, by, cx, cy) * o(ax, ay, bx, by, dx, dy) < 0 && o(cx, cy, dx, dy, ax, ay) * o(cx, cy, dx, dy, bx, by) < 0; +} + +function shuffle(S) { + const N = S.n, sig = S.sig; + for (let i = 0; i < N; i++) sig[i] = i; + for (let i = N - 1; i > 0; i--) { + const j = floor(Math.random() * (i + 1)), t = sig[i]; + sig[i] = sig[j]; + sig[j] = t; + } + for (let i = 0; i < N; i++) S.inv[sig[i]] = i; +} + +// Rays to draw all the time: particles spread over the source, each the +// one farthest from those already picked. +function pickWitnesses(S) { + const N = S.n, X = S.X, d = new Float64Array(N).fill(1e9); + let pick = floor(Math.random() * N); + for (let w = 0; w < WITNESS; w++) { + S.wit[w] = pick; + let best = 0; + for (let i = 0; i < N; i++) { + d[i] = min(d[i], Math.hypot(X[2 * i] - X[2 * pick], X[2 * i + 1] - X[2 * pick + 1])); + if (d[i] > d[best]) best = i; + } + pick = best; + } +} + +// A new round: the pile sits in S.X, the next shape is dealt in the other half. +function deal(S, fresh) { + const N = S.n; + if (fresh) { + sample(S, S.src, S.half, S.rot, S.X); + wheelOf(S, S.X, S.hue, S.sat); + } + sample(S, S.dst, 1 - S.half, S.dRot, S.Y); + S.P.set(S.X.subarray(0, 2 * N)); + shuffle(S); + pickWitnesses(S); + startPlan(S); + S.phase = 0; + S.t = 0; + S.tu = T_U; + S.base = 0; + S.flash = []; +} + +const ease = (u) => 1 - pow(1 - cl(u, 0, 1), 2.2); + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 150, 1]; + const d = cl(dt || 0, 0, 0.1) * (cl(kv[1], 0.2, 3) / 2); + S.hueK = cl(kv[0], 0, 1); + const n = cl(round(cl(kv[2], 150, 400) / 25) * 25, 150, 400), p = cl(kv[3], 1, 2); + if (n !== S.n) { + // A new count deals the same two shapes again, with the short + // untangling of the opening. + S.n = n; + S.p = p; + deal(S, true); + S.tu = T_OPEN; + } else if (abs(p - S.p) > 1e-4) { + S.p = p; + if (S.phase === 0) { + // Plan again from the pairing played so far and play the new plan + // over what is left of the stage. + startPlan(S); + S.base = ease(S.t / S.tu); + } else if (S.phase === 1) { + // Mid flight: plan from where the particles are now, and turn them + // towards their new targets once the plan is done. + S.X.set(S.P.subarray(0, 2 * S.n)); + startPlan(S); + S.turn = true; + } + } + S.t += d; + for (const f of S.flash) f.life -= d; + S.flash = S.flash.filter((f) => f.life > 0); + const N = S.n; + if (S.job) work(S, S.phase === 0 && S.t >= S.tu ? 1e9 : BUDGET); + if (S.phase === 0) { + // Until the work is done its length is a guess, and nothing it has + // not found yet is played. + const e = ease(S.t / S.tu), base = S.base || 0, total = S.job ? S.K * 1.15 : S.K; + const want = min(S.K, S.t >= S.tu ? S.K : floor((total * (e - base)) / max(1e-6, 1 - base))); + const k0 = S.k; + applyTo(S, want); + // Flash the swap of this frame that took the most off the total, + // a crossing one if there is one. + let best = -1; + for (let k = k0; k < S.k; k++) if (best < 0 || S.cross[k] > S.cross[best] || (S.cross[k] === S.cross[best] && S.gain[k] > S.gain[best])) best = k; + if (best >= 0 && S.flash.length < 3) S.flash.push({ i: S.sw[2 * best], j: S.sw[2 * best + 1], life: 0.2 }); + if (S.t >= S.tu) { + S.phase = 1; + S.t = 0; + S.tm = T_M; + S.turn = false; + S.F.set(S.X.subarray(0, 2 * N)); + } + } else if (S.phase === 1 && S.turn && !S.job) { + applyTo(S, S.K); + S.F.set(S.P.subarray(0, 2 * N)); + S.tm = max(0.8, S.tm - S.t); + S.t = 0; + S.turn = false; + } + if (S.phase === 1) { + const u = cl(S.t / S.tm, 0, 1), e = u * u * (3 - 2 * u); + for (let i = 0; i < N; i++) { + const a = S.sig[i]; + S.P[2 * i] = S.F[2 * i] + (S.Y[2 * a] - S.F[2 * i]) * e; + S.P[2 * i + 1] = S.F[2 * i + 1] + (S.Y[2 * a + 1] - S.F[2 * i + 1]) * e; + } + if (S.t >= S.tm) { + S.phase = 2; + S.t = 0; + // The pile now sits in the new shape; its colours drift to that + // shape's own wheel while it rests. + for (let i = 0; i < N; i++) { + S.X[2 * i] = S.P[2 * i]; + S.X[2 * i + 1] = S.P[2 * i + 1]; + } + wheelOf(S, S.X, S.hueTo, S.satTo); + S.hue0.set(S.hue.subarray(0, N)); + S.sat0.set(S.sat.subarray(0, N)); + } + } else if (S.phase === 2) { + const u = cl(S.t / (T_R * 0.8), 0, 1), e = u * u * (3 - 2 * u); + for (let i = 0; i < N; i++) { + const dh = S.hueTo[i] - S.hue0[i] - round(S.hueTo[i] - S.hue0[i]); + S.hue[i] = S.hue0[i] + dh * e; + S.sat[i] = S.sat0[i] + (S.satTo[i] - S.sat0[i]) * e; + } + if (S.t >= T_R) { + // The pile becomes the source; a new shape in the half it left. + S.half = 1 - S.half; + S.src = S.dst; + S.rot = S.dRot; + let k = floor(Math.random() * (SHAPES.length - 1)); + if (k >= S.src) k++; + S.dst = k; + S.dRot = floor(Math.random() * 8) * (PI / 4); + S.base = 0; + deal(S, false); + } + } +} + +function applyTo(S, want) { + for (; S.k < want; S.k++) { + const i = S.sw[2 * S.k], j = S.sw[2 * S.k + 1], a = S.sig[i], b = S.sig[j]; + S.sig[i] = b; + S.sig[j] = a; + S.inv[b] = i; + S.inv[a] = j; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.n) return; + const rgb = S.rgb, N = S.n, c = S.c, hk = 0.55 + S.hueK; + rgb.fill(0); + const add = (x, y, r, g, b) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + rgb[o] += r; + rgb[o + 1] += g; + rgb[o + 2] += b; + }; + const line = (x0, y0, x1, y1, r, g, b) => { + const n = Math.ceil(max(abs(x1 - x0), abs(y1 - y0))); + if (n < 1) return; + const steep = abs(y1 - y0) > abs(x1 - x0); + for (let s = 0; s <= n; s++) { + const f = s / n, x = x0 + (x1 - x0) * f - 0.5, y = y0 + (y1 - y0) * f - 0.5; + if (steep) { + const xi = floor(x), fx = x - xi, yi = round(y); + add(xi, yi, r * (1 - fx), g * (1 - fx), b * (1 - fx)); + add(xi + 1, yi, r * fx, g * fx, b * fx); + } else { + const yi = floor(y), fy = y - yi, xi = round(x); + add(xi, yi, r * (1 - fy), g * (1 - fy), b * (1 - fy)); + add(xi, yi + 1, r * fy, g * fy, b * fy); + } + } + }; + // The targets not yet reached: one dim dot each, in the colour of the + // particle meant for it. + if (S.phase < 2) + for (let j = 0; j < N; j++) { + const i = S.inv[j]; + hsv(S.hue[i] + hk, S.sat[i], 0.55, c); + add(floor(S.Y[2 * j]), floor(S.Y[2 * j + 1]), c[0], c[1], c[2]); + } + // The sample rays, from where each particle is to where it is going. + if (S.phase < 2) + for (let w = 0; w < WITNESS; w++) { + const i = S.wit[w], a = S.sig[i]; + hsv(S.hue[i] + hk, S.sat[i] * 0.7, 0.26, c); + line(S.P[2 * i], S.P[2 * i + 1], S.Y[2 * a], S.Y[2 * a + 1], c[0], c[1], c[2]); + } + for (const f of S.flash) { + const v = 0.75 * (f.life / 0.2); + for (const i of [f.i, f.j]) { + const a = S.sig[i]; + line(S.P[2 * i], S.P[2 * i + 1], S.Y[2 * a], S.Y[2 * a + 1], v, v, v); + add(floor(S.Y[2 * a]), floor(S.Y[2 * a + 1]), v, v, v); + } + } + // The particles: squares a little wider than the grid. Their colour and + // cover add up apart from the rest, so two half-covered LEDs on a seam + // make a whole one, and then lie over the dots and rays. + const q = (S.g * 1.14) / 2, pc = S.pc, pa = S.pa; + pc.fill(0); + pa.fill(0); + for (let i = 0; i < N; i++) { + hsv(S.hue[i] + hk, S.sat[i], 1, c); + const px = S.P[2 * i], py = S.P[2 * i + 1]; + for (let y = floor(py - q); y < py + q; y++) { + if (y < 0 || y >= H) continue; + const cy = min(y + 1, py + q) - max(y, py - q); + if (cy <= 0) continue; + for (let x = floor(px - q); x < px + q; x++) { + if (x < 0 || x >= W) continue; + const a = cy * (min(x + 1, px + q) - max(x, px - q)); + if (a <= 0) continue; + const o = (y * W + x) * 3; + pa[y * W + x] += a; + pc[o] += c[0] * a; + pc[o + 1] += c[1] * a; + pc[o + 2] += c[2] * a; + } + } + } + for (let p = 0, o = 0; p < W * H; p++, o += 3) { + const a = pa[p]; + if (a <= 0) continue; + const k = 1 / max(1, a), b = 1 - min(1, a); + rgb[o] = rgb[o] * b + pc[o] * k; + rgb[o + 1] = rgb[o + 1] * b + pc[o + 1] * k; + rgb[o + 2] = rgb[o + 2] * b + pc[o + 2] * k; + } + for (let p = 0, o = 0; p < W * H; p++, o += 3) + display.setPixel(p % W, (p / W) | 0, min(255, rgb[o] * 255) | 0, min(255, rgb[o + 1] * 255) | 0, min(255, rgb[o + 2] * 255) | 0); +} From 77acb6c2a1246e650a858959410990cdc5f811ab Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 14:11:36 +0000 Subject: [PATCH 3/8] pattern-study: Taylor Unmixing, Canard Explosion for round 61 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JJpBXX9YfaGEvtuHofsFxv --- .../patterns/canard-explosion.js | 445 ++++++++++++++++++ .../pattern-study/patterns/taylor-unmixing.js | 253 ++++++++++ 2 files changed, 698 insertions(+) create mode 100644 incoming/pattern-study/patterns/canard-explosion.js create mode 100644 incoming/pattern-study/patterns/taylor-unmixing.js diff --git a/incoming/pattern-study/patterns/canard-explosion.js b/incoming/pattern-study/patterns/canard-explosion.js new file mode 100644 index 000000000..5602541f2 --- /dev/null +++ b/incoming/pattern-study/patterns/canard-explosion.js @@ -0,0 +1,445 @@ +// ===== Patternflow pattern ===== +// Title: Canard Explosion +// Family: rhythm +// Idea: The van der Pol oscillator walks through its family of limit cycles from a tiny Hopf cycle round the fold of the cubic, through ducks that ride the repelling middle branch further and further before jumping back, to ducks with a head and the full relaxation loop, each cycle left in the phase plane as a fading band so the family stands as nested loops, while x(t) runs down the lower panel and turns from small ripples into wide square spikes. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: relaxation oscillations (Balthasar van der Pol, 1926), the Lienard plane (Alfred-Marie Lienard, 1928), canard cycles and the canard explosion (Eric Benoit, Jean-Louis Callot, Francine Diener and Marc Diener, 1981), the explosion by geometric singular perturbation (Martin Krupa and Peter Szmolyan, 2001), pattern round 61 +// Round: 61 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Epsilon=0.04..0.2, Pace=0.6..3 +// +// Knob 1: Hue. Turns the family's colours round the wheel. At 0 the smallest cycles are teal, the ducks azure to violet and the relaxation loop magenta, over attracting branches drawn green and a dashed repelling branch drawn rose; at 0.5 the family runs from violet through red to lime and cyan. The wheel skips orange, which reads brown when dim. +// Knob 2: Speed. At 2 the oscillator runs 2.5 units of its slow time a second: a Hopf cycle goes round in 0.5 s, the relaxation loop in 1.6 s and the longest duck in 2 s, and the trace below moves 15 rows a second. At 0.2 everything runs ten times slower, at 3 half as fast again. +// Knob 3: Epsilon. The ratio of the fast time to the slow, 0.04 to 0.2, in eps x' = y - (x^3 / 3 - x), y' = a - x. At 0.04 the ducks are sharp: every cycle from the duck that turns back at x = 0 to the one whose head reaches x = -1.47 lies in an interval of a 1.8e-11 wide at a = 0.9948377923, the jumps are flat and the corners square. At 0.1 the same stretch of the family is 1.3e-4 wide and the loops round off; at 0.2 it is 0.023 wide, the ducks are fat and the relaxation loop reaches only x = -1.52. The family is found again by bisection a quarter second after the knob stops, 5 to 20 ms of work. +// Knob 4: Pace. How fast the walk goes through the family of 18 cycles (17 at eps = 0.2), 0.6 to 3 cycles a second at Speed 2, out and back with a pause at each end. The parameter a changes only as the point crosses x = a going right, so each cycle is run whole. A ghost fades by e in 4 seconds. At 0.6 a duck runs one or two loops before the next takes over and five or six earlier cycles still show; at 3 the walk moves on several cycles each loop and goes from the Hopf cycle to the relaxation loop in 6 seconds. + +const W = 64, H = 128, PH = 76, T0 = 79, TH = H - T0; +// The phase plane: x from -XR to XR across, y from YT at the top to -YT. +// Both are fitted to the family of each eps: the loops overshoot the folds +// before they jump, up to y = 1.02 at eps = 0.04 and 1.57 at 0.2, so the +// plane runs to 1.08 and 1.66. use(S) sets them from the state before any +// drawing. +let XR = 2.2, YT = 1.1; +const use = (S) => { + XR = S.xr; + YT = S.yt; +}; +const PX = (x) => ((x + XR) * W) / (2 * XR), PY = (y) => ((YT - y) * PH) / (2 * YT); +const { floor, ceil, min, max, abs, exp, sqrt, round } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// Slow time a second at Speed 2 is 2 x RATE; trace rows per unit of slow time. +const RATE = 1.25, TR = 6; +// Every bisection starts here on the right attracting branch. From x = 1.95 +// the branch pulls the point onto the slow manifold by e^(-2.75 / eps) +// before the fold, below rounding error at eps = 0.04, so the canard that +// follows depends on a alone. +const X0 = 1.95; +const HOLD = 1.5, GHOST = 4; +const wheel = (h) => 0.28 + 0.74 * (h - floor(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; + return out; +} + +// One RK4 step of eps x' = y - (x^3/3 - x), y' = a - x. The live point and +// the bisection use this same step at the same h, so the cycle the +// bisection found is the cycle the point settles on. +let RX = 0, RY = 0; +function rk4(x, y, a, ie, h) { + const k1x = (y - x * (x * x / 3 - 1)) * ie, k1y = a - x; + let xx = x + 0.5 * h * k1x, yy = y + 0.5 * h * k1y; + const k2x = (yy - xx * (xx * xx / 3 - 1)) * ie, k2y = a - xx; + xx = x + 0.5 * h * k2x; + yy = y + 0.5 * h * k2y; + const k3x = (yy - xx * (xx * xx / 3 - 1)) * ie, k3y = a - xx; + xx = x + h * k3x; + yy = y + h * k3y; + const k4x = (yy - xx * (xx * xx / 3 - 1)) * ie, k4y = a - xx; + RX = x + (h / 6) * (k1x + 2 * k2x + 2 * k3x + k4x); + RY = y + (h / 6) * (k1y + 2 * k2y + 2 * k3y + k4y); +} + +// How far left the cycle reaches, from one pass: down the right branch, +// round the fold, along the middle branch and off. x_min runs from near 1 +// for the smallest cycles through -1 at the maximal canard to about -1.9 +// for the relaxation loop, so it orders the whole family. +function firstPass(a, ie, h) { + let x = X0, y = X0 * (X0 * X0 / 3 - 1), xmin = 9, below = false; + const M = ceil(14 / h); + for (let i = 0; i < M; i++) { + rk4(x, y, a, ie, h); + const px = x; + x = RX; + y = RY; + if (x < xmin) xmin = x; + if (x < a) below = true; + else if (below && px < a) return xmin; + } + return xmin; +} + +// Run until the loops agree, then keep one loop, crossing to crossing of +// x = a going right. +function settle(a, ie, h, most) { + let x = X0, y = X0 * (X0 * X0 / 3 - 1), xmin = 9, prev = 9, below = false, loops = 0, rec = null, n = 0; + const M = ceil(400 / h); + for (let i = 0; i < M; i++) { + rk4(x, y, a, ie, h); + const px = x; + x = RX; + y = RY; + if (rec) { + if (n < rec.length / 2) { + rec[2 * n] = x; + rec[2 * n + 1] = y; + n++; + } + } + if (x < xmin) xmin = x; + if (x < a) below = true; + else if (below && px < a) { + if (rec) return { a, pts: rec, n, xmin, period: n * h }; + loops++; + if ((loops >= 2 && abs(xmin - prev) < 3e-4) || loops >= most) rec = new Float32Array(2 * ceil(12 / h)); + prev = xmin; + xmin = 9; + below = false; + } + } + return { a, pts: rec || new Float32Array(2), n, xmin: prev, period: n * h }; +} + +// Max-blend an antialiased segment into a PH-row raster. +function seg(buf, x0, y0, x1, y1, v, rows) { + const n = max(1, ceil(max(abs(x1 - x0), abs(y1 - y0)) * 2)); + for (let s = 0; s <= n; s++) { + const f = s / n, x = x0 + (x1 - x0) * f - 0.5, y = y0 + (y1 - y0) * f - 0.5; + const xi = floor(x), yi = floor(y), fx = x - xi, fy = y - yi; + for (let k = 0; k < 4; k++) { + const xx = xi + (k & 1), yy = yi + (k >> 1); + if (xx < 0 || xx >= W || yy < 0 || yy >= rows) continue; + const w = v * ((k & 1) ? fx : 1 - fx) * ((k >> 1) ? fy : 1 - fy) * 1.6; + const o = yy * W + xx; + if (w > buf[o]) buf[o] = min(1, w); + } + } +} + +function rasterize(m) { + const stroke = new Float32Array(W * PH), inside = new Uint8Array(W * PH), xs = []; + for (let r = 0; r < PH; r++) xs.push([]); + const P = m.pts, n = m.n; + for (let i = 0; i < n; i++) { + const j = (i + 1) % n, x0 = PX(P[2 * i]), y0 = PY(P[2 * i + 1]), x1 = PX(P[2 * j]), y1 = PY(P[2 * j + 1]); + seg(stroke, x0, y0, x1, y1, 1, PH); + // Scanline crossings at row centres, for the even-odd fill. + const lo = min(y0, y1), hi = max(y0, y1); + for (let r = ceil(lo - 0.5); r + 0.5 < hi; r++) { + if (r < 0 || r >= PH) continue; + const yc = r + 0.5; + if (yc < lo || yc >= hi) continue; + xs[r].push(x0 + ((x1 - x0) * (yc - y0)) / (y1 - y0)); + } + } + for (let r = 0; r < PH; r++) { + const c = xs[r].sort((p, q) => p - q); + for (let k = 0; k + 1 < c.length; k += 2) + for (let x = max(0, ceil(c[k] - 0.5)); x < W && x + 0.5 < c[k + 1]; x++) inside[r * W + x] = 1; + } + m.stroke = stroke; + m.inside = inside; +} + +// The family for one eps: three Hopf cycles at a = 1 - f dc, where dc = +// eps / 8 + 3 eps^2 / 32 is the canard point to second order; fourteen +// ducks found by bisection on x_min, nine headless and five with a head; +// and the relaxation loop at a = 1 - 3 dc. +function family(S, eps) { + const h = eps / 8, ie = 1 / eps, dc = eps / 8 + (3 * eps * eps) / 32; + const samples = []; + const meas = (a) => { + const v = firstPass(a, ie, h); + samples.push(a, v); + return v; + }; + const aLo = 1 - 3 * dc, aHi = 1 - 0.5 * dc; + const xR = meas(aLo), xH = meas(aHi); + const as = [1 - 0.15 * dc, 1 - 0.4 * dc, 1 - 0.65 * dc]; + const targets = [0.55, 0.38, 0.2, 0, -0.2, -0.4, -0.6, -0.78, -0.92]; + for (let j = 1; j <= 5; j++) targets.push(-1 + ((xR + 1) * j) / 5.6); + for (const T of targets) { + if (!(T < xH && T > xR)) continue; + // The tightest bracket any earlier bisection left. + let lo = aLo, hi = aHi; + for (let i = 0; i < samples.length; i += 2) { + const a = samples[i], v = samples[i + 1]; + if (v < T && a > lo) lo = a; + if (v >= T && a < hi) hi = a; + } + for (let it = 0; it < 64; it++) { + const mid = 0.5 * (lo + hi); + if (mid <= lo || mid >= hi) break; + const v = meas(mid); + if (v < T) lo = mid; + else hi = mid; + if (abs(v - T) < 0.004) { + hi = mid; + break; + } + } + as.push(hi); + } + as.push(aLo); + const fam = []; + let ex = 0, ey = 0; + for (let i = 0; i < as.length; i++) { + const m = settle(as[i], ie, h, i < 3 ? 80 : 4); + if (m.n < 8) continue; + m.hopf = i < 3; + for (let j = 0; j < 2 * m.n; j += 2) { + ex = max(ex, abs(m.pts[j])); + ey = max(ey, abs(m.pts[j + 1])); + } + fam.push(m); + } + S.xr = max(2.2, 1.03 * ex); + S.yt = 1.06 * ey; + use(S); + for (const m of fam) rasterize(m); + cubicRasters(S); + fam.sort((p, q) => q.xmin - p.xmin); + // Each pixel takes the smallest cycle round it, so nested cycles leave + // nested bands. + const band = new Int8Array(W * PH).fill(-1); + for (let k = fam.length - 1; k >= 0; k--) for (let p = 0; p < W * PH; p++) if (fam[k].inside[p]) band[p] = k; + S.eps = eps; + S.h = h; + S.fam = fam; + S.band = band; + S.ghost = new Float32Array(fam.length); + S.kc = cl(S.kc, 0, fam.length - 1); + S.x = fam[S.kc].pts[0]; + S.y = fam[S.kc].pts[1]; +} + +function cubicRasters(S) { + // The critical manifold y = x^3/3 - x: outer branches attract, the + // middle one repels. + const att = new Float32Array(W * PH), rep = new Float32Array(W * PH), side = new Uint8Array(W * PH); + const f = (x) => x * (x * x / 3 - 1); + for (let i = 0; i < 400; i++) { + const x0 = -XR + (2 * XR * i) / 400, x1 = -XR + (2 * XR * (i + 1)) / 400; + const mid = abs(0.5 * (x0 + x1)) < 1; + // The repelling branch is dashed. + if (mid && floor(i / 6) % 2) continue; + seg(mid ? rep : att, PX(x0), PY(f(x0)), PX(x1), PY(f(x1)), 1, PH); + } + for (let r = 0; r < PH; r++) + for (let c = 0; c < W; c++) { + const x = ((c + 0.5) * 2 * XR) / W - XR, y = YT - ((r + 0.5) * 2 * YT) / PH; + side[r * W + c] = y > f(x) ? 1 : 0; + } + S.att = att; + S.rep = rep; + S.side = side; +} + +export function setup(params) { + params.s = { + eps: 0, h: 0, fam: [], band: null, ghost: null, kc: 0, walk: 0, dir: 1, + x: X0, y: X0 * (X0 * X0 / 3 - 1), acc: 0, rowT: 0, cmin: 9, cmax: -9, + tmin: new Float32Array(TH).fill(NaN), tmax: new Float32Array(TH), tk: new Int8Array(TH), ta: new Float32Array(TH), head: 0, + tail: new Float32Array(512), ti: 0, tn: 0, + xr: 2.2, yt: 1.1, wantEps: 0, still: 0, hue: 0, tl: new Float32Array(W * PH), rgb: new Float32Array(W * H * 3), c: [0, 0, 0], c2: [0, 0, 0], + }; +} + +function pushRow(S) { + S.head = (S.head + TH - 1) % TH; + S.tmin[S.head] = S.cmin; + S.tmax[S.head] = S.cmax; + S.tk[S.head] = S.kc; + S.ta[S.head] = S.fam[S.kc].a; + S.cmin = S.cmax = S.x; +} + +function sim(S, d, speed, pace) { + const N = S.fam.length; + S.walk += S.dir * pace * d * speed * 0.5; + if (S.walk > N - 1 + HOLD) { + S.walk = N - 1 + HOLD; + S.dir = -1; + } else if (S.walk < -HOLD) { + S.walk = -HOLD; + S.dir = 1; + } + const fade = exp(-(d * speed * 0.5) / GHOST); + for (let k = 0; k < N; k++) S.ghost[k] *= fade; + const h = S.h, ie = 1 / S.eps; + S.acc += d * speed * RATE; + let a = S.fam[S.kc].a; + while (S.acc >= h) { + S.acc -= h; + const px = S.x; + rk4(S.x, S.y, a, ie, h); + S.x = RX; + S.y = RY; + if (px < a && S.x >= a && S.y > a * (a * a / 3 - 1)) { + // A loop is done: the next cycle of the walk takes over here. + const was = S.fam[S.kc]; + S.kc = cl(round(S.walk), 0, N - 1); + const m = S.fam[S.kc]; + a = m.a; + // Out of a Hopf cycle the point sits by the weakly unstable focus and + // would take a dozen loops to spiral out to a duck, so it is set on + // the new cycle where that cycle crosses x = a. + if (was.hopf && !m.hopf) { + S.x = m.pts[0]; + S.y = m.pts[1]; + } + } + S.ghost[S.kc] = 1; + S.tail[S.ti] = S.x; + S.tail[S.ti + 1] = S.y; + S.ti = (S.ti + 2) % S.tail.length; + S.tn = min(S.tn + 1, S.tail.length / 2); + if (S.x < S.cmin) S.cmin = S.x; + if (S.x > S.cmax) S.cmax = S.x; + S.rowT += h * TR; + if (S.rowT >= 1) { + S.rowT -= 1; + pushRow(S); + } + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0.04, 0.6], d = cl(dt || 0, 0, 0.1); + use(S); + S.hue = cl(kv[0], 0, 1); + const speed = cl(kv[1], 0.2, 3), eps = cl(kv[2], 0.04, 0.2), pace = cl(kv[3], 0.6, 3); + if (!S.fam.length) { + family(S, eps); + // Open going up the walk at the duck that turns back near x = -0.5, + // with up to eight seconds already run to get there, so the ghosts and + // the trace are there from the first frame. At the opening Pace the + // first duck with a head comes 6 seconds in. + const N = S.fam.length, at = 0.5 * (N - 1), pre = min(8, (at + HOLD) / pace); + S.walk = at - pace * pre; + S.kc = cl(round(S.walk), 0, N - 1); + const m = S.fam[S.kc]; + S.x = m.pts[0]; + S.y = m.pts[1]; + S.cmin = S.cmax = S.x; + for (let i = 0; i < round(pre * 30); i++) sim(S, 1 / 30, 2, pace); + S.wantEps = eps; + return; + } + // A new eps is taken once the knob has rested for a quarter second. + if (abs(eps - S.wantEps) > 1e-6) { + S.wantEps = eps; + S.still = 0; + } else S.still += d; + if (abs(S.wantEps - S.eps) > 1e-6 && S.still > 0.25) family(S, S.wantEps); + sim(S, d, speed, pace); +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.fam.length) return; + use(S); + const rgb = S.rgb, fam = S.fam, N = fam.length, c = S.c, c2 = S.c2, hue = S.hue; + rgb.fill(0); + const add = (o, col, v) => { + rgb[o] += col[0] * v; + rgb[o + 1] += col[1] * v; + rgb[o + 2] += col[2] * v; + }; + const colOf = (k, s, v, out) => hsv(wheel(0.22 + (0.58 * k) / max(1, N - 1) + hue), s, v, out); + // The two sides of the cubic: above it the fast flow runs right, below it left. + const up = hsv(wheel(0.25 + hue), 0.45, 0.1, [0, 0, 0]), dn = hsv(wheel(0.62 + hue), 0.45, 0.1, [0, 0, 0]); + const cols = []; + for (let k = 0; k < N; k++) cols.push(colOf(k, 0.75, 1, [0, 0, 0])); + for (let p = 0; p < W * PH; p++) { + const o = p * 3; + add(o, S.side[p] ? up : dn, 1); + const b = S.band[p]; + if (b >= 0) add(o, cols[b], 0.42 * S.ghost[b]); + } + // Ghost outlines. + for (let k = 0; k < N; k++) { + const g = S.ghost[k]; + if (g < 0.02 || k === S.kc) continue; + const st = fam[k].stroke, col = cols[k]; + for (let p = 0; p < W * PH; p++) if (st[p] > 0) add(p * 3, col, st[p] * g * 0.6); + } + const att = hsv(wheel(0.07 + hue), 0.6, 0.42, [0, 0, 0]), rep = hsv(wheel(0.97 + hue), 0.6, 0.5, [0, 0, 0]); + for (let p = 0; p < W * PH; p++) { + if (S.att[p] > 0) add(p * 3, att, S.att[p]); + if (S.rep[p] > 0) add(p * 3, rep, S.rep[p]); + } + // The cycle being run. + colOf(S.kc, 0.35, 1, c); + const st = fam[S.kc].stroke; + for (let p = 0; p < W * PH; p++) if (st[p] > 0) add(p * 3, c, st[p] * 0.75); + // The equilibrium at (a, f(a)), the centre the Hopf cycle grew from. + const a = fam[S.kc].a, ex = PX(a), ey = PY(a * (a * a / 3 - 1)); + for (const [dx, dy, v] of [[0, 0, 0.7], [1, 0, 0.3], [-1, 0, 0.3], [0, 1, 0.3], [0, -1, 0.3]]) { + const xx = floor(ex) + dx, yy = floor(ey) + dy; + if (xx >= 0 && xx < W && yy >= 0 && yy < PH) add((yy * W + xx) * 3, rep, v * 2); + } + // The point and its last 0.3 units of slow time. + const tl = S.tl.fill(0), L = min(S.tn - 1, ceil(0.3 / S.h)), TL = S.tail.length; + for (let i = 0; i < L; i++) { + const j0 = (S.ti - 2 * (i + 1) + 2 * TL) % TL, j1 = (S.ti - 2 * (i + 2) + 2 * TL) % TL; + seg(tl, PX(S.tail[j0]), PY(S.tail[j0 + 1]), PX(S.tail[j1]), PY(S.tail[j1 + 1]), 1 - i / L, PH); + } + for (let p = 0; p < W * PH; p++) if (tl[p] > 0) add(p * 3, c, tl[p] * 0.9); + const qx = PX(S.x), qy = PY(S.y); + for (let yy = floor(qy - 3); yy <= qy + 3; yy++) + for (let xx = floor(qx - 3); xx <= qx + 3; xx++) { + if (xx < 0 || xx >= W || yy < 0 || yy >= PH) continue; + const dx = xx + 0.5 - qx, dy = yy + 0.5 - qy, r2 = dx * dx + dy * dy; + add((yy * W + xx) * 3, [1, 1, 1], 1.6 * exp(-r2 / 2.2)); + } + // The axis between: ticks at x = -2, -1, 0, 1, 2 and a mark at x = a. + const ax = hsv(wheel(0.5 + hue), 0.2, 1, c2); + for (let x = 0; x < W; x++) add(((PH + 1) * W + x) * 3, ax, 0.12); + for (let t = -2; t <= 2; t++) { + const xx = floor(PX(t)); + for (let yy = PH; yy <= PH + 2; yy++) if (xx >= 0 && xx < W) add((yy * W + xx) * 3, ax, 0.35); + } + for (let yy = PH; yy <= PH + 2; yy++) add((yy * W + cl(floor(ex), 0, W - 1)) * 3, rep, 0.8); + // x(t) running down, newest row on top, filled from the equilibrium + // x = a out to the trace. + for (let r = 0; r < TH; r++) { + const i = (S.head + r) % TH, lo = S.tmin[i], hi = S.tmax[i]; + if (lo !== lo) continue; + const k = S.tk[i], col = cols[min(k, N - 1)], e = PX(S.ta[i]), x0 = PX(lo), x1 = PX(hi), y = T0 + r; + const age = 1 - (0.55 * r) / TH; + // A faint rule across the trace where the next cycle of the family took over. + const j = (i + 1) % TH; + if (r < TH - 1 && S.tmin[j] === S.tmin[j] && S.tk[j] !== k) for (let x = 0; x < W; x++) add((y * W + x) * 3, col, 0.13 * age); + const fl = min(e, x0), fr = max(e, x1); + for (let x = max(0, floor(fl)); x < W && x < fr; x++) { + const cx = x + 0.5; + // Coverage of the pixel by the bright stroke [x0 - 0.5, x1 + 0.5]. + const sc = cl(min(cx + 0.5, x1 + 1) - max(cx - 0.5, x0 - 1), 0, 1); + const fc = cl(min(cx + 0.5, fr) - max(cx - 0.5, fl), 0, 1); + // The fill brightens from the equilibrium out to the trace. + const q = abs(cx - e) / max(1, abs((cx < e ? x0 : x1) - e)); + add((y * W + x) * 3, col, age * ((0.2 + 0.36 * min(1, q)) * fc + 0.9 * sc)); + } + } + for (let p = 0, o = 0; p < W * H; p++, o += 3) + display.setPixel(p % W, (p / W) | 0, min(255, rgb[o] * 255) | 0, min(255, rgb[o + 1] * 255) | 0, min(255, rgb[o + 2] * 255) | 0); +} diff --git a/incoming/pattern-study/patterns/taylor-unmixing.js b/incoming/pattern-study/patterns/taylor-unmixing.js new file mode 100644 index 000000000..a0028d4ab --- /dev/null +++ b/incoming/pattern-study/patterns/taylor-unmixing.js @@ -0,0 +1,253 @@ +// ===== Patternflow pattern ===== +// Title: Taylor Unmixing +// Family: sim +// Idea: Round drops of dye in a very viscous fluid between two walls are sheared by plane Couette flow into thin streaks that wrap round the panel many times and lay down a fine slanted lattice of colours, then the walls run back the same distance and the drops come back where they started, sharp when there is no diffusion and as fainter vertical smears when there is. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: plane Couette flow (Maurice Couette, 1890), shear dispersion (G. I. Taylor, 1953), kinematic reversibility of Stokes flow shown with dye in glycerine (G. I. Taylor, film "Low Reynolds Number Flows", 1966), pattern round 61 +// Round: 61 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Turns=2..8, Diffusion=0..1 +// +// Knob 1: Hue. Moves the seven dyes round the wheel together. At 0 they are lime, green, cyan, azure, blue, violet and pink; the seven sit evenly round the wheel, so every 1/7 of the knob the same set comes back with the colours handed on, and in between one dye passes through red. The wheel skips orange and yellow, which read brown and olive where a streak thins out. +// Knob 2: Speed. At 2 one cycle takes 10.4 seconds: 0.8 s at rest, 3.4 s of shearing, 0.6 s held, 3.4 s back, 1.4 s with the drops returned and 0.8 s while they fade and fresh drops take their place. At 0.2 it takes 104 s, at 3 it takes 6.9 s. Diffusion runs on the same clock, so Speed does not change how blurred the drops come back. +// Knob 3: Turns. How far each wall travels before it turns back, in panel heights: 2 to 8, so 256 to 1024 rows, the two walls going opposite ways. At 2 the shear across the 60 columns of fluid reaches 8.5, a drop 29 columns wide becomes a streak 247 rows long that wraps the panel almost twice, and its lines lie 15 columns apart; at 5 the shear reaches 21 and the lines 6 apart; at 8 it reaches 34, a drop wraps the panel 7.7 times and its lines lie 3.8 columns apart. The travel follows the knob over about half a second. +// Knob 4: Diffusion. The dye's diffusivity D = 0.6 k^3 LEDs squared per second. At 0 the drops return exactly, and at the opening 0.06 (D = 0.00013) they look the same. At 0.5 (D = 0.075) a drop comes back 1.4 times as tall at 2 turns and 4.6 times as tall at 8; at 1 (D = 0.6) 2.8 and 10 times as tall, with a third and a tenth of its brightness, and at 8 turns the dye has mixed into soft columns. The streaks blur first: at 8 turns a streak is 0.2 LED thick and diffusion crosses that almost at once, which is why a little D spoils the return. + +const W = 64, H = 128, FL = 2, FR = 62, WF = FR - FL; +const { floor, ceil, min, max, abs, sqrt, exp, cos, 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)); +// The phases of one cycle, in seconds at Speed 2. +const T_REST = 0.8, T_FWD = 3.4, T_HOLD = 0.6, T_BACK = 3.4, T_BACKREST = 1.4, T_SWAP = 0.8; +const T_CYCLE = T_REST + T_FWD + T_HOLD + T_BACK + T_BACKREST + T_SWAP; +// The film opens in the middle of the forward stroke, so the first second is +// already streaks and the drops come back at 4.9 s. +const T_OPEN = T_REST + 0.75 * T_FWD; +const NB = 7; +// Each pixel is read as a small Gaussian of this variance, so a streak +// thinner than an LED dims in proportion instead of flickering. +const PIX = 0.1; +// Dye hues skip orange to lime: wheel(h) runs lime, green, cyan, blue, violet, pink, red. +const wheel = (h) => 0.28 + 0.74 * (h - floor(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; + return out; +} + +// Wall travel S at cycle time tau, as a fraction of the full travel. +function travel(tau) { + if (tau < T_REST) return 0; + tau -= T_REST; + if (tau < T_FWD) return (1 - cos((PI * tau) / T_FWD)) / 2; + tau -= T_FWD; + if (tau < T_HOLD) return 1; + tau -= T_HOLD; + if (tau < T_BACK) return (1 + cos((PI * tau) / T_BACK)) / 2; + return 0; +} + +// A fresh set of drops: seven rows down the panel, each in one of three +// lanes across it, no two neighbours in the same lane. +function drops() { + const set = [], hues = [0, 1, 2, 3, 4, 5, 6]; + for (let i = NB - 1; i > 0; i--) { + const j = floor(Math.random() * (i + 1)), t = hues[i]; + hues[i] = hues[j]; + hues[j] = t; + } + const lanes = [13, 30, 47]; + let lane = floor(Math.random() * 3); + const y0 = Math.random() * H; + for (let i = 0; i < NB; i++) { + lane = (lane + 1 + floor(Math.random() * 2)) % 3; + const s = 5 + Math.random() * 1.6; + set.push({ + x: lanes[lane] + (Math.random() - 0.5) * 5, + y: y0 + (i * H) / NB + (Math.random() - 0.5) * 4, + s0: s * s, sxx: s * s, sxy: 0, syy: s * s, hue: hues[i] / NB, + }); + } + return set; +} + +// One step of the covariance under a shear g = dGamma over a time h with +// diffusivity D. For a shear rate constant over the step this is exact: +// Sigma' = F Sigma F^T + 2 D integral of F(s) F(s)^T, F = [[1, 0], [g, 1]]. +function shearStep(b, g, h, D) { + const sxx = b.sxx, sxy = b.sxy, syy = b.syy; + b.syy = syy + 2 * g * sxy + g * g * sxx + 2 * D * (h + (g * g * h) / 3); + b.sxy = sxy + g * sxx + D * g * h; + b.sxx = sxx + 2 * D * h; +} + +export function setup(params) { + params.s = { + tau: -1, smax: 0, S: 0, Sprev: 0, set: drops(), old: null, + rgb: new Float32Array(W * H * 3), c: [0, 0, 0], hue: 0, + }; +} + +function advance(S, dtau, D) { + // Small steps, so the drops keep up with the walls at full speed. + const n = max(1, ceil(dtau / 0.02)); + for (let k = 0; k < n; k++) { + const t0 = S.tau, t1 = S.tau + dtau / n; + const cyc = floor(t1 / T_CYCLE) > floor(t0 / T_CYCLE); + const s1 = (cyc ? 0 : travel(t1 - floor(t1 / T_CYCLE) * T_CYCLE)) * S.smax; + // The strain is taken from the travel actually made, so the total comes + // back to exactly 0 with the walls even if Turns moved on the way. + const g = (2 * (s1 - S.S)) / WF, h = t1 - t0; + for (const b of S.set) shearStep(b, g, h, D); + if (S.old) for (const b of S.old) shearStep(b, 0, h, D); + S.tau = t1; + S.S = s1; + // The swap: the returned drops fade out while new ones are laid down. + const ph = S.tau - floor(S.tau / T_CYCLE) * T_CYCLE; + if (!S.old && ph >= T_CYCLE - T_SWAP) { + S.old = S.set; + S.set = drops(); + } + if (cyc) S.old = null; + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 2, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = cl(kv[0], 0, 1); + const speed = cl(kv[1], 0.2, 3), turns = cl(kv[2], 2, 8), k4 = cl(kv[3], 0, 1); + const D = 0.6 * k4 * k4 * k4; + S.Sprev = S.S; + if (S.tau < 0) { + // First frame: run the cycle from its start to the opening point, so + // the streaks carry the diffusion they would have gathered. + S.smax = turns * H; + S.tau = 0; + advance(S, T_OPEN, D); + S.Sprev = S.S; + return; + } + // The travel follows the knob over about half a second, so the walls + // never jump. The drops still come home, because S returns to 0. + S.smax += (turns * H - S.smax) * min(1, d * 2.5); + advance(S, (d * speed) / 2, D); +} + +function drawSet(S, set, fade) { + const rgb = S.rgb, c = S.c; + for (const b of set) { + const cx = b.x, cy = b.y + (2 * b.x / WF - 1) * S.S; + const sxx = b.sxx + PIX, sxy = b.sxy, syy = b.syy + PIX; + const det = sxx * syy - sxy * sxy, det0 = (b.s0 + PIX) * (b.s0 + PIX); + // Dye is conserved: the peak falls as the patch spreads. + const amp = sqrt(det0 / det) * fade; + const v = det / sxx, sv = sqrt(v), slope = sxy / sxx; + const R = min(H * 2, ceil(4.2 * sv)); + hsv(wheel(b.hue + S.hue), 0.85, 1, c); + for (let col = FL; col < FR; col++) { + const dx = col - FL + 0.5 - cx, wx = amp * exp((-0.5 * dx * dx) / sxx); + if (wx < 0.004) continue; + let mu = cy + slope * dx - 0.5; + mu -= floor(mu / H) * H; + const r0 = floor(mu) - R; + // Gaussian down the column by recurrence: g(j+1) = g(j) e1, e1 *= e2. + let dy = r0 - mu, g = wx * exp((-0.5 * dy * dy) / v), e1 = exp(-(2 * dy + 1) / (2 * v)); + const e2 = exp(-1 / v); + for (let r = r0; r <= r0 + 2 * R + 1; r++) { + if (g > 0.003) { + const o = ((((r % H) + H) % H) * W + col) * 3; + rgb[o] += c[0] * g; + rgb[o + 1] += c[1] * g; + rgb[o + 2] += c[2] * g; + } + g *= e1; + e1 *= e2; + } + } + } +} + +function rings(S, set, w) { + if (w < 0.01) return; + const rgb = S.rgb, c = S.c; + for (const b of set) { + const r = 2.3 * sqrt(b.s0); + hsv(wheel(b.hue + S.hue), 0.6, 1, c); + for (let yy = floor(b.y - r - 2); yy <= b.y + r + 2; yy++) + for (let xx = max(FL, floor(FL + b.x - r - 2)); xx <= min(FR - 1, FL + b.x + r + 2); xx++) { + const dx = xx - FL + 0.5 - b.x, dy = yy + 0.5 - b.y, e = abs(sqrt(dx * dx + dy * dy) - r); + if (e >= 0.9) continue; + const v = 0.16 * w * (1 - e / 0.9), o = ((((yy % H) + H) % H) * W + xx) * 3; + rgb[o] += c[0] * v; + rgb[o + 1] += c[1] * v; + rgb[o + 2] += c[2] * v; + } + } +} + +// Wall ticks: one every 8 rows, a longer brighter one every 32, carried by +// the wall and smeared over the frame's travel. +function tick(y) { + const m = y - floor(y / 32) * 32, u = m - floor(m / 8) * 8; + const big = m < 8; + return cl(1 - abs(u - (big ? 1.5 : 1)) / (big ? 1.8 : 1.1), 0, 1) * (big ? 1 : 0.6); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const rgb = S.rgb; + // The dark fluid. + for (let p = 0, o = 0; p < W * H; p++, o += 3) { + rgb[o] = 0.008; + rgb[o + 1] = 0.015; + rgb[o + 2] = 0.034; + } + const ph = S.tau - floor(S.tau / T_CYCLE) * T_CYCLE; + // The swap: the returned drops fade out, then the fresh ones fade in. + const u = S.old ? cl((ph - (T_CYCLE - T_SWAP)) / T_SWAP, 0, 1) : 1; + const fo = 1 - sm(u / 0.5), fn = sm((u - 0.4) / 0.6); + if (S.old) drawSet(S, S.old, fo); + drawSet(S, S.set, fn); + // Where each drop started, as a faint ring at 2.3 sigma, the edge the + // drop shows at. It comes up over the last 40% of the way back and goes + // in the first 30% of the next stroke. + let ring = 0; + if (ph < T_REST) ring = 1; + else if (ph < T_REST + 0.3 * T_FWD) ring = 1 - (ph - T_REST) / (0.3 * T_FWD); + else if (ph > T_REST + T_FWD + T_HOLD + 0.6 * T_BACK) ring = min(1, (ph - (T_REST + T_FWD + T_HOLD + 0.6 * T_BACK)) / (0.4 * T_BACK)); + if (S.old) rings(S, S.old, ring * fo); + rings(S, S.set, ring * fn); + // The walls: left moves by -S, right by +S. + const wc = hsv(0.6 + S.hue * 0.2, 0.2, 1, [0, 0, 0]); + const ds = S.S - S.Sprev, n = min(40, max(1, ceil(abs(ds) * 1.5))); + for (let r = 0; r < H; r++) { + let tl = 0, tr = 0; + for (let k = 0; k < n; k++) { + const s = S.Sprev + (ds * (k + 0.5)) / n; + tl += tick(r + 0.5 + s); + tr += tick(r + 0.5 - s); + } + tl /= n; + tr /= n; + for (const [x, t] of [[0, tl], [1, tl], [62, tr], [63, tr]]) { + const o = (r * W + x) * 3, v = 0.16 + 0.95 * t; + rgb[o] = wc[0] * v; + rgb[o + 1] = wc[1] * v; + rgb[o + 2] = wc[2] * v; + } + } + for (let p = 0, o = 0; p < W * H; p++, o += 3) { + const x = p % W, y = (p / W) | 0; + if (x < FL || x >= FR) display.setPixel(x, y, min(255, rgb[o] * 255) | 0, min(255, rgb[o + 1] * 255) | 0, min(255, rgb[o + 2] * 255) | 0); + else display.setPixel(x, y, (255 * (1 - exp(-2.2 * rgb[o]))) | 0, (255 * (1 - exp(-2.2 * rgb[o + 1]))) | 0, (255 * (1 - exp(-2.2 * rgb[o + 2]))) | 0); + } +} From c8b784106fd2db3e24ec2e45a0c8e2ed6bd9a357 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 14:12:07 +0000 Subject: [PATCH 4/8] pattern-study: Thomson Kink, Binary Lens for round 61 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JJpBXX9YfaGEvtuHofsFxv --- .../pattern-study/patterns/binary-lens.js | 322 ++++++++++++++++++ .../pattern-study/patterns/thomson-kink.js | 297 ++++++++++++++++ 2 files changed, 619 insertions(+) create mode 100644 incoming/pattern-study/patterns/binary-lens.js create mode 100644 incoming/pattern-study/patterns/thomson-kink.js diff --git a/incoming/pattern-study/patterns/binary-lens.js b/incoming/pattern-study/patterns/binary-lens.js new file mode 100644 index 000000000..2ad1e8758 --- /dev/null +++ b/incoming/pattern-study/patterns/binary-lens.js @@ -0,0 +1,322 @@ +// ===== Patternflow pattern ===== +// Title: Binary Lens +// Family: space +// Idea: Two point masses on a tilted circular orbit lens the sky behind them, and the panel is their magnification map in the source plane by inverse ray shooting, dim blue with contours of equal magnification and bright caustic folds that meet in cusps and change, as the pair swings from upright to lying across, from two astroids to one six-cusp loop to a small central caustic with two triangles, while a star drifts over the map and its light curve scrolls along the bottom with a spike at every fold it crosses. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: gravitational lensing by two point masses (Peter Schneider and Alfred Weiss, 1986), inverse ray shooting (Joachim Wambsganss, Peter Schneider and Bohdan Paczynski, 1986), the three caustic topologies of the binary lens (Hans Erdl and Peter Schneider, 1993), pattern round 61 +// Round: 61 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Mass ratio=0.2..1, Orbit=15..60 +// +// Knob 1: Hue. Turns the colours round the wheel together. At 0 the map is deep blue where nothing is magnified, violet inside the caustics and nearly white on the folds, with teal contours and a green star and light curve; at 0.25 the map is magenta with green folds, at 0.5 green, at 0.75 teal with blue folds. The wheel runs from green to magenta and skips red, orange and lime, which read maroon, brown and olive when dim. +// Knob 2: Speed. 2 is real time: the star crosses the map at 1.2 Einstein radii, 34 LEDs, a second, the light curve holds the last 6.4 s, and at the bottom of Orbit the pair goes round in 24 s; 0.2 is ten times slower and 3 half as fast again. +// Knob 3: Mass ratio. q = m2 / m1 from 0.2 to 1, the masses adding to 1. At 0.2 the masses are 0.83 and 0.17 and the caustics are lopsided: the loop is a bent triangle, the lighter mass has the smaller astroid in the wide pair, and the topology changes at separations 0.74 and 1.82. At 1 the pair is equal, the caustics are symmetric and the changes come at 0.71 and 2, as Erdl and Schneider found. +// Knob 4: Orbit. How fast the pair goes round its circle, 15 to 60 degrees a second: once in 24 s at 15, once in 6 s at 60. The circle has radius 2.3 and is tilted 76 degrees, so the separation on the sky runs from 2.3 with the pair upright, two astroids one above the other, to 0.54 lying across, a small central caustic with two triangles above and below; with equal masses the pair spends 34% of its turn wide, 53% in the six-cusp loop and 13% close, swinging fast through the close part. At 15 the opening shows the loop, the close pair at about 2 s and the wide pair from 6 s. + +const W = 64, H = 128; +const { floor, min, max, abs, sqrt, sin, cos, log, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The map: rows 0 to 99, 28 LEDs to the Einstein radius of the total mass, +// centred on the centre of mass. The light curve fills rows 102 to 127. +const MH = 100, PX = 28, MX = 32, MY = 50, CY0 = 102, CH = 26; +// Rays: an image-plane grid 2 to the LED each way, so a patch with no +// lensing gets 4 rays per LED. Each ray is split over the four LEDs round +// where it lands. With a whole number of rays to the LED that split sums +// to the same on every LED, so the grid leaves no moire where the lensing +// is weak: a first try that dropped each ray into one cell of a grid twice +// as fine, at 2.3 rays to the LED, drew dark arcs 5 LEDs apart all over +// the dim parts. The grid spreads past the map by a margin that holds the +// images of its edge: a source 1.1 Einstein radii out has its bright image +// 0.6 further out, and a mass 1.9 out holds images 0.4 beyond itself. +const SUB = 2, HX = 1.8, HY = 2.35, STEP = 1 / (PX * SUB); +const NX = Math.ceil((2 * HX) / STEP), NY = Math.ceil((2 * HY) / STEP); +// The counts carry a border one LED wide, so the edge LEDs get their whole +// share. +const PW = W + 2, PH = MH + 2; +// The orbit: a circle of radius A Einstein radii seen tilted, cos i = CI, +// with its long axis along the panel's long axis. The projected +// separation runs from A = 2.3 when the pair stands upright (two astroids +// one above the other) to A CI = 0.54 when it lies across (the two small +// triangles then sit above and below the central caustic). Both fit the +// tall map, 1.8 from the centre to its top: the wide pair's astroids reach +// 1.7 at q = 0.2 and the close pair's triangles 1.5 at q = 1. +const A = 2.3, CI = 0.235; +// The light curve: one column every 0.1 s, log magnification from 1 to 16. +const COL_DT = 0.1, MU_TOP = 16; +// Contours of the map, a factor 1.12 apart. +const CSTEP = log(1.12); +// The star crosses the map at 1.2 Einstein radii a second. Its +// magnification is the rays landing on a soft disk 2 LEDs in radius. +const VSRC = 1.2, RSRC = 2 / PX; +// The star's trail on the map: its last 10 places, 1 s. +const TRAIL = 10; + +function hsv(h, s, v, out, 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); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; + return out; +} +// The knob turns hues on a wheel from green through blue and violet to +// magenta, which skips red, orange and lime: dim, they read maroon, brown +// and olive. +const wheel = (h) => 0.3 + 0.56 * (h - floor(h)); + +export function setup(params) { + params.s = { + t: 0, ready: false, drawn: false, phi: 0, q: 0.2, om: 15, hue: 0, half: 0, + // Two half-grids of ray counts, one shot each frame, and the star's + // count from each. + cnt: [new Float32Array(PW * PH), new Float32Array(PW * PH)], star: [1, 1], + mu: new Float32Array(W * MH), lm: new Float32Array(W * MH), tmp: new Float32Array(W * MH), c3: [0, 0, 0], + lut: new Float32Array(256 * 3), + curve: new Float32Array(W), colSum: 0, colN: 0, colT: 0, tx: new Float32Array(TRAIL), ty: new Float32Array(TRAIL), + sx: 0, sy: 0, vx: 0, vy: 0, + rgb: new Float32Array(W * H * 3), c: [0, 0, 0], + }; +} + +// A new crossing: in at a random point of the map's edge, aimed at a +// point within 0.7 Einstein radii of the centre. +function newTrack(S) { + const ex = MX / PX, ey = MY / PX, side = floor(Math.random() * 4); + let x, y; + if (side < 2) { + x = (Math.random() * 2 - 1) * ex; + y = side ? ey : -ey; + } else { + y = (Math.random() * 2 - 1) * ey; + x = side === 2 ? ex : -ex; + } + const ax = (Math.random() * 2 - 1) * 0.7, ay = (Math.random() * 2 - 1) * 0.7; + const dx = ax - x, dy = ay - y, l = sqrt(dx * dx + dy * dy); + S.sx = x; + S.sy = y; + S.vx = (dx / l) * VSRC; + S.vy = (dy / l) * VSRC; +} + +// The masses: the centre of mass at the origin, m1 + m2 = 1. +function lenses(S) { + const m1 = 1 / (1 + S.q), m2 = S.q / (1 + S.q), dx = A * CI * sin(S.phi), dy = A * cos(S.phi); + return { m1, m2, x1: -dx * m2, y1: -dy * m2, x2: dx * m1, y2: dy * m1 }; +} + +// Shoot every stride-th row of the ray grid starting at row off, through +// the lens equation zeta = z - sum m_i / conj(z - z_i), which in real +// numbers is zeta = z - sum m_i (z - z_i) / |z - z_i|^2. Each ray is split +// over four LEDs of cnt (if given) and adds a weight (1 - r^2 / R^2)^2 to +// the star if it lands within R of it; the return value is the star's +// magnification. +function shoot(S, L, cnt, off, stride) { + const { m1, m2, x1, y1, x2, y2 } = L, sx = S.sx, sy = S.sy, r2 = RSRC * RSRC, ir2 = 1 / r2; + const ox = MX + 0.5, oy = MY + 0.5; + let star = 0; + for (let j = off; j < NY; j += stride) { + const y = -HY + (j + 0.5) * STEP, dy1 = y - y1, dy2 = y - y2, e1 = dy1 * dy1, e2 = dy2 * dy2; + let x = -HX + 0.5 * STEP; + for (let i = 0; i < NX; i++, x += STEP) { + const dx1 = x - x1, dx2 = x - x2; + const k1 = m1 / (dx1 * dx1 + e1), k2 = m2 / (dx2 * dx2 + e2); + const zx = x - k1 * dx1 - k2 * dx2, zy = y - k1 * dy1 - k2 * dy2; + const ux = zx - sx, uy = zy - sy, u2 = ux * ux + uy * uy; + if (u2 < r2) { + const w = 1 - u2 * ir2; + star += w * w; + } + if (!cnt) continue; + const px = zx * PX + ox, py = zy * PX + oy; + // Written this way round so a ray that hits a mass dead on (NaN) is + // dropped too. + if (!(px >= 0 && py >= 0 && px < PW - 1 && py < PH - 1)) continue; + const ix = px | 0, iy = py | 0, fx = px - ix, fy = py - iy, o = iy * PW + ix, gx = 1 - fx; + cnt[o] += gx * (1 - fy); + cnt[o + 1] += fx * (1 - fy); + cnt[o + PW] += gx * fy; + cnt[o + PW + 1] += fx * fy; + } + } + // Rays per unit area of the image plane, for the rows shot, against the + // soft disk's area pi R^2 / 3. + return star / ((PI * r2) / 3 / (STEP * STEP * stride)); +} + +// A box 5 LEDs wide, across and then down, in place. +function blur(a, t) { + for (let y = 0; y < MH; y++) { + const r = y * W; + for (let x = 0; x < W; x++) { + let s = 0, n = 0; + for (let k = max(0, x - 2); k <= min(W - 1, x + 2); k++, n++) s += a[r + k]; + t[r + x] = s / n; + } + } + for (let x = 0; x < W; x++) + for (let y = 0; y < MH; y++) { + let s = 0, n = 0; + for (let k = max(0, y - 2); k <= min(MH - 1, y + 2); k++, n++) s += t[k * W + x]; + a[y * W + x] = s / n; + } +} + +function step(S, dt) { + S.t += dt; + // A circular orbit keeps an even pace in its own plane; seen tilted, the + // pair swings fast across the panel when close and slowly when upright. + S.phi += ((S.om * PI) / 180) * dt; + S.sx += S.vx * dt; + S.sy += S.vy * dt; + if (abs(S.sx) > MX / PX + 0.1 || abs(S.sy) > MY / PX + 0.1) newTrack(S); +} + +function pushColumn(S, mu) { + S.curve.copyWithin(0, 1); + S.curve[W - 1] = mu; + S.tx.copyWithin(0, 1); + S.ty.copyWithin(0, 1); + S.tx[TRAIL - 1] = S.sx; + S.ty[TRAIL - 1] = S.sy; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0.2, 15], dd = cl(dt || 0, 0, 0.1); + S.hue = cl(kv[0], 0, 1); + const speed = cl(kv[1], 0.2, 3); + S.q = cl(kv[2], 0.2, 1); + S.om = cl(kv[3], 15, 60); + if (!S.ready) { + // The first frame runs the 6.4 s the light curve holds, with every + // fourth row of the grid and only the star's count. + S.ready = true; + S.t = -W * COL_DT; + // At t = 0 the orbit is 60 degrees on from upright, so the opening + // seconds go from the six-cusp loop to the close pair lying across, at + // 2 s with the Orbit knob at the bottom, and back out. + S.phi = ((60 - S.om * W * COL_DT) * PI) / 180; + newTrack(S); + for (let k = 0; k < W; k++) { + step(S, COL_DT); + pushColumn(S, shoot(S, lenses(S), null, k % 4, 4)); + } + } + const h = (dd * speed) / 2; + step(S, h); + S.colT += h; +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.ready) return; + const L = lenses(S), rgb = S.rgb, hue = S.hue; + // This frame's half of the grid; the other half is the last frame's. + for (let k = 0; k < (S.drawn ? 1 : 2); k++) { + const f = S.cnt[S.half]; + f.fill(0); + S.star[S.half] = shoot(S, L, f, S.half, 2); + S.half ^= 1; + } + S.drawn = true; + const muStar = (S.star[0] + S.star[1]) / 2; + S.colSum += muStar; + S.colN++; + while (S.colT >= COL_DT) { + pushColumn(S, S.colSum / S.colN); + S.colT -= COL_DT; + S.colSum = muStar; + S.colN = 1; + } + // Magnification: rays per LED over the rays per LED with no lens. + const c0 = S.cnt[0], c1 = S.cnt[1], mu = S.mu, norm = 1 / (SUB * SUB); + for (let y = 0; y < MH; y++) + for (let x = 0, o = (y + 1) * PW + 1; x < W; x++, o++) mu[y * W + x] = (c0[o] + c1[o]) * norm; + // Colours by log magnification: deep blue at 1, violet inside the + // caustics where there are five images and the magnification is 3 or + // more, nearly white on the folds. + const lut = S.lut; + for (let i = 0; i < 256; i++) { + const l = i / 255; + hsv(wheel(0.56 + hue + 0.3 * l), 0.9 - 0.7 * l * l, 0.13 + 0.87 * l ** 0.8, lut, i * 3); + } + const kl = 255 / log(MU_TOP), lm = S.lm; + for (let p = 0; p < W * MH; p++) { + const m = mu[p], i = m > 1 ? min(255, (log(m) * kl) | 0) : 0, o = p * 3, q = i * 3; + lm[p] = log(max(m, 0.05)) / CSTEP; + rgb[o] = lut[q]; + rgb[o + 1] = lut[q + 1]; + rgb[o + 2] = lut[q + 2]; + } + // Contours of equal magnification outside the caustics, every 12%, so the + // dim part of the map shows its slope: wide loops round the pair far out, + // closing in and bending where the caustics pull them. + // The contours are read from the map smoothed twice by a box 5 LEDs + // wide: raw, the few rays to an LED make them speckle. + blur(lm, S.tmp); + blur(lm, S.tmp); + const cc = hsv(wheel(0.4 + hue), 0.6, 1, S.c3, 0), top = log(2.6) / CSTEP; + for (let y = 1; y < MH - 1; y++) + for (let x = 1; x < W - 1; x++) { + const p = y * W + x, f = lm[p]; + if (f > top || f < 0.7) continue; + const gx = (lm[p + 1] - lm[p - 1]) / 2, gy = (lm[p + W] - lm[p - W]) / 2, g = sqrt(gx * gx + gy * gy); + if (g < 1e-3) continue; + const e = abs(f - Math.round(f)) / g, v = 1 - e / 0.7; + if (v <= 0) continue; + const k = v * 0.2 * min(1, (top - f) / 1.5), o = p * 3; + rgb[o] += cc[0] * k; + rgb[o + 1] += cc[1] * k; + rgb[o + 2] += cc[2] * k; + } + rgb.fill(0, W * MH * 3); + const add = (x, y, c, v) => { + if (x < 0 || x >= W || y < 0 || y >= H || v <= 0) return; + const o = (y * W + x) * 3; + rgb[o] += c[0] * v; + rgb[o + 1] += c[1] * v; + rgb[o + 2] += c[2] * v; + }; + const dot = (cx, cy, rad, c, pk) => { + for (let y = floor(cy - rad); y <= cy + rad; y++) + for (let x = floor(cx - rad); x <= cx + rad; x++) { + const dx = x + 0.5 - cx, dy = y + 0.5 - cy, q = (dx * dx + dy * dy) / (rad * rad); + if (q < 1 && y < MH) add(x, y, c, pk * (1 - q) * (1 - q)); + } + }; + // The two masses, small and grey, the heavier one larger. + const g = [0.7, 0.7, 0.7]; + dot(L.x1 * PX + MX, L.y1 * PX + MY, 1.2 + 0.8 * L.m1, g, 0.45); + dot(L.x2 * PX + MX, L.y2 * PX + MY, 1.2 + 0.8 * L.m2, g, 0.45); + // The star and its trail. + const sc = hsv(wheel(0.18 + hue), 0.55, 1, S.c, 0); + for (let k = 1; k < TRAIL; k++) { + const ax = S.tx[k - 1], ay = S.ty[k - 1], bx = k < TRAIL - 1 ? S.tx[k] : S.sx, by = k < TRAIL - 1 ? S.ty[k] : S.sy; + if ((bx - ax) ** 2 + (by - ay) ** 2 > 0.09) continue; + for (let s = 0; s < 4; s++) { + const f = s / 4, x = (ax + (bx - ax) * f) * PX + MX, y = (ay + (by - ay) * f) * PX + MY; + dot(x, y, 1.2, sc, 0.22 * (k / TRAIL) ** 2); + } + } + dot(S.sx * PX + MX, S.sy * PX + MY, 2.4, sc, 1.6); + // The light curve: log magnification, a filled column per 0.1 s, the + // newest on the right. + const lt = log(MU_TOP); + for (let x = 0; x < W; x++) { + const v = cl(log(max(1, S.curve[x])) / lt, 0, 1) * (CH - 1) + 1; + for (let r = 0; r < CH; r++) { + const cover = cl(v - r, 0, 1); + if (cover <= 0) break; + const top = v - r < 1.5; + add(x, CY0 + CH - 1 - r, sc, cover * (top ? 0.95 : 0.16 + 0.22 * (r / CH))); + } + } + for (let p = 0, o = 0; p < W * H; p++, o += 3) { + const pk = max(1, rgb[o], rgb[o + 1], rgb[o + 2]), k = 255 / pk; + display.setPixel(p % W, (p / W) | 0, (rgb[o] * k) | 0, (rgb[o + 1] * k) | 0, (rgb[o + 2] * k) | 0); + } +} diff --git a/incoming/pattern-study/patterns/thomson-kink.js b/incoming/pattern-study/patterns/thomson-kink.js new file mode 100644 index 000000000..87aaa41a6 --- /dev/null +++ b/incoming/pattern-study/patterns/thomson-kink.js @@ -0,0 +1,297 @@ +// ===== Patternflow pattern ===== +// Title: Thomson Kink +// Family: wave +// Idea: A charge runs up and down the middle of the panel and keeps reversing or stopping dead, every change sends out a sphere of news at the speed of light, and each field line is a chain of straight spokes, each aimed at where the charge would now be had it kept the velocity it had between two changes, joined by bright sideways kinks across the expanding shells. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: field of a uniformly moving charge (Oliver Heaviside, 1888), radiation as a kink in the field lines (J. J. Thomson, 1904), Purcell's drawing of the kink (Edward Purcell, 1965), pattern round 61 +// Round: 61 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Lines=12..32, Velocity=0.3..0.9 +// +// Knob 1: Hue. Turns the colours round the wheel together. At 0 the regions where the news says the charge is moving down are blue, moving up green and standing still near black, the shells and kinks rose and the spokes white; at 0.25 the regions are violet and teal with aqua kinks, at 0.5 green and blue with cyan-blue kinks. The wheel runs from green to magenta and skips red, orange and lime, which read maroon, brown and olive when dim. +// Knob 2: Speed. 2 is real time with light at 36 LEDs a second, so a shell born in the middle reaches the top and bottom rows in 1.8 s while the charge changes its mind every 0.32 to 0.6 s, and four or five shells are out at once; 0.2 is ten times slower and 3 half as fast again. +// Knob 3: Lines. 12 to 32 field lines, an even number, spaced evenly in the angle they would have round a charge at rest: every 30 degrees at 12, every 15 at 24, every 11 at 32. Each keeps that label in every region, so it stays one line through all the kinks. +// Knob 4: Velocity. The charge's speed as a fraction of light, 0.3 to 0.9. At 0.3 the charge moves 11 LEDs a second, and a reversal puts the two pieces of the 75 degree line 18 LEDs apart along a shell 30 LEDs out; at 0.6 the kink of the 75 degree line runs 72 degrees round its shell and the shells read as rings; at 0.9 the charge moves 32 LEDs a second, gamma is 2.3, the spokes crowd towards the horizontal, each shell is a bright ring of lines running along it and the shells pile up in front of the charge. A new speed starts at the charge's next change and spreads out with the news. + +const W = 64, H = 128; +const { floor, min, max, abs, sqrt, sin, cos, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// Light in LEDs per second of the simulation, the axis the charge runs +// on, and how far it may stray from the middle row. +const C = 36, XC = 32, YC = 64, ROAM = 36; +// The kink shell's thickness in LEDs: the change takes 2.2 / 36 = 0.06 s. +const SHELL = 2.2; +// A shell is forgotten once it is wider than this; no LED is that far from +// any place the charge has been. +const RFORGET = 125; +// Seconds between changes, and the share of changes that stop the charge. +const GAP0 = 0.32, GAP1 = 0.6, STOP = 0.3; + +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; + return out; +} +// The knob turns hues on a wheel from green through blue and violet to +// magenta, which skips red, orange and lime: dim, they read maroon, brown +// and olive. +const wheel = (h) => 0.3 + 0.56 * (h - floor(h)); + +export function setup(params) { + params.s = { + T: 0, ready: false, beta: 0.3, hue: 0, n: 12, + // The changes, oldest first: time, the row where it happened, and the + // velocity after it, in LEDs a second (positive is down the panel). + ch: [], u0: 0, next: 0, + acc: new Float32Array(W * H * 3), + col: [[0, 0, 0], [0, 0, 0], [0, 0, 0]], kc: [0, 0, 0], lc: [0, 0, 0], sc: [0, 0, 0], c: [0, 0, 0], + sh: [], + }; +} + +// Where the charge is at time t, from the last change before it. +function posAt(S, t) { + const ch = S.ch; + for (let i = ch.length - 1; i >= 0; i--) if (ch[i].t <= t) return ch[i].p + ch[i].u * (t - ch[i].t); + return YC + S.u0 * (t - (ch.length ? ch[0].t : t)); +} +const velAt = (S) => (S.ch.length ? S.ch[S.ch.length - 1].u : S.u0); + +// The charge changes its mind at time t: it reverses, or stops dead, or +// sets off again, now with the knob's speed. A change that would carry it +// past the end of its run turns it round instead. +function change(S, t) { + const y = posAt(S, t), u = velAt(S), v = S.beta * C; + const gap = GAP0 + (GAP1 - GAP0) * Math.random(); + let nu; + if (u !== 0) nu = Math.random() < STOP ? 0 : -Math.sign(u) * v; + else nu = (Math.random() < 0.7 ? (y < YC ? 1 : -1) : y < YC ? -1 : 1) * v; + if (abs(y + nu * gap - YC) > ROAM) nu = y + nu * gap > YC ? -v : v; + if (u === 0 && nu === 0) nu = v; + S.ch.push({ t, p: y, u: nu }); + S.next = t + gap; +} + +function advance(S, T) { + while (S.next <= T) change(S, S.next); + // Forget the shells that have left the panel, and with them the + // velocity the charge had before them. + while (S.ch.length > 1 && C * (T - S.ch[0].t) > RFORGET) S.u0 = S.ch.shift().u; + S.T = T; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 12, 0.3], d = cl(dt || 0, 0, 0.1); + S.hue = cl(kv[0], 0, 1); + const speed = cl(kv[1], 0.2, 3); + S.n = 2 * Math.round(cl(kv[2], 12, 32) / 2); + S.beta = cl(kv[3], 0.3, 0.9); + if (!S.ready) { + // The first frame starts the charge 4 s in the past, so the panel + // opens full of shells. + S.ready = true; + S.u0 = S.beta * C; + S.ch = []; + S.next = -4; + advance(S, 0); + } + advance(S, S.T + (d * speed) / 2); +} + +// The far end of a ray from s (inside the sphere) along unit d where it +// leaves the sphere of centre (XC, cy) and radius r, as a distance. +function exitRay(sx, sy, dx, dy, cy, r) { + const ox = sx - XC, oy = sy - cy, b = ox * dx + oy * dy, c = ox * ox + oy * oy - r * r; + return -b + sqrt(max(0, b * b - c)); +} + +// Antialiased line, added into the buffer. Each column (or row, for a +// steep line) gets one unit of ink split between the two LEDs the line +// passes between, scaled by the length of line it holds, so a polyline is +// even along its whole length and its joints do not flare. +function line(acc, x0, y0, x1, y1, c, a) { + // Clip to the panel with a one-LED margin. + let t0 = 0, t1 = 1; + const dx = x1 - x0, dy = y1 - y0; + const clip = (p, q) => { + if (p === 0) return q >= 0; + const r = q / p; + if (p < 0) { + if (r > t1) return false; + if (r > t0) t0 = r; + } else { + if (r < t0) return false; + if (r < t1) t1 = r; + } + return true; + }; + if (!clip(-dx, x0 + 1) || !clip(dx, W + 1 - x0) || !clip(-dy, y0 + 1) || !clip(dy, H + 1 - y0)) return; + const ax = x0 + dx * t0, ay = y0 + dy * t0, bx = x0 + dx * t1, by = y0 + dy * t1; + const steep = abs(by - ay) > abs(bx - ax); + let u0 = steep ? ay : ax, v0 = steep ? ax : ay, u1 = steep ? by : bx, v1 = steep ? bx : by; + if (u1 < u0) { + let t = u0; + u0 = u1; + u1 = t; + t = v0; + v0 = v1; + v1 = t; + } + const du = u1 - u0; + if (du < 1e-6) return; + const g = (v1 - v0) / du, k = a * sqrt(1 + g * g); + for (let ui = floor(u0); ui <= floor(u1); ui++) { + const cover = min(u1, ui + 1) - max(u0, ui); + if (cover <= 0) continue; + const um = (max(u0, ui) + min(u1, ui + 1)) / 2; + const vv = v0 + g * (um - u0) - 0.5, vi = floor(vv), f = vv - vi; + for (let s = 0; s < 2; s++) { + const p = vi + s, w = (s ? f : 1 - f) * cover * k; + const x = steep ? p : ui, y = steep ? ui : p; + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const o = (y * W + x) * 3; + acc[o] += c[0] * w; + acc[o + 1] += c[1] * w; + acc[o + 2] += c[2] * w; + } + } +} + +// The kink across a shell centred on row cy, from the inner face to the +// outer one. In a thin shell the line runs along the shell, so it is drawn +// as a spiral piece round the shell's centre: angle and radius both go +// linearly from one end to the other. A straight chord would cut through +// the inside once the kink spans a wide angle, as it does above 0.5 c. It +// is drawn twice, 0.9 LED apart, so it reads two LEDs wide. +function arc(acc, cy, x0, y0, x1, y1, c, a) { + const r0 = sqrt((x0 - XC) ** 2 + (y0 - cy) ** 2), r1 = sqrt((x1 - XC) ** 2 + (y1 - cy) ** 2); + const f0 = Math.atan2(x0 - XC, y0 - cy), f1 = Math.atan2(x1 - XC, y1 - cy); + const steps = 1 + floor(max(abs(f1 - f0) * max(r0, r1), abs(r1 - r0)) / 1.5); + for (let side = -1; side <= 1; side += 2) { + let px = 0, py = 0; + for (let s = 0; s <= steps; s++) { + const f = f0 + ((f1 - f0) * s) / steps, r = r0 + ((r1 - r0) * s) / steps + side * 0.45; + const qx = XC + r * sin(f), qy = cy + r * cos(f); + if (s) line(acc, px, py, qx, qy, c, a * 0.7); + px = qx; + py = qy; + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.ready) return; + const acc = S.acc, T = S.T, ch = S.ch, m = ch.length, hue = S.hue; + acc.fill(0); + // The shells, newest last: centre row, radius, thickness, and for the + // region just inside each one the velocity and where the charge would + // now be had it kept that velocity. + const sh = S.sh; + sh.length = 0; + for (let i = 0; i < m; i++) { + const r = C * (T - ch[i].t), b = abs(ch[i].u) / C, ub = i ? ch[i - 1].u : S.u0; + sh.push({ + cy: ch[i].p, r, w: min(SHELL, 1.2 * r * (1 - b)), u: ch[i].u, s: ch[i].p + ch[i].u * (T - ch[i].t), + jump: abs(ch[i].u - ub) / (2 * C), + }); + } + // Region i lies inside shell i and outside shell i + 1; region -1 lies + // outside every shell and holds the velocity before the oldest change. + const sOut = m ? ch[0].p + S.u0 * (T - ch[0].t) : posAt(S, T); + // Tints: news of moving down, moving up, standing. + const tint = S.col; + hsv(wheel(0.59 + hue), 0.9, 0.25, tint[0]); + hsv(wheel(0.14 + hue), 0.85, 0.2, tint[1]); + hsv(wheel(0.36 + hue), 0.4, 0.09, tint[2]); + const glow = hsv(wheel(0.99 + hue), 0.65, 1, S.sc); + for (let y = 0; y < H; y++) { + const py = y + 0.5; + for (let x = 0; x < W; x++) { + const px = x + 0.5 - XC, o = (y * W + x) * 3; + let reg = -1, g = 0; + for (let i = 0; i < m; i++) { + const e = sh[i], dy = py - e.cy, dd = sqrt(px * px + dy * dy); + if (dd < e.r) reg = i; + // A faint ring on every shell, brightest at right angles to the + // motion: the radiated energy goes as sin^2. + const q = (dd - e.r) / 1.6; + if (q > -3 && q < 3 && dd > 0.1) { + const sn = px / dd; + g += e.jump * (0.12 + 0.88 * sn * sn) * exp(-q * q) * (40 / (40 + e.r)); + } + } + const u = reg < 0 ? S.u0 : sh[reg].u, tc = u > 0 ? tint[0] : u < 0 ? tint[1] : tint[2]; + acc[o] = tc[0] + glow[0] * g * 0.5; + acc[o + 1] = tc[1] + glow[1] * g * 0.5; + acc[o + 2] = tc[2] + glow[2] * g * 0.5; + } + } + // The field lines. Line j has the angle theta it would have round a + // charge at rest. In a region whose charge moves at beta it leaves that + // charge's extrapolated place at theta' with tan theta' = gamma tan theta + // (Heaviside's field, squeezed towards the plane across the motion). + const lc = hsv(wheel(0.45 + hue), 0.12, 1, S.lc), kc = hsv(wheel(0.99 + hue), 0.45, 1, S.kc); + const n = S.n; + const dirs = new Float32Array(2 * (m + 1)); + for (let j = 0; j < n; j++) { + const th = ((j + 0.5) * 2 * PI) / n, st = sin(th), ct = cos(th); + // Directions per region; slot m holds region -1. + for (let i = -1; i < m; i++) { + const u = i < 0 ? S.u0 : sh[i].u, b = abs(u) / C, gm = 1 / sqrt(1 - b * b); + const ex = gm * st, ey = ct, l = sqrt(ex * ex + ey * ey), k = i < 0 ? 2 * m : 2 * i; + dirs[k] = ex / l; + dirs[k + 1] = ey / l; + } + // From the charge out: the newest region first. + let ax = XC, ay = m ? sh[m - 1].s : sOut; + const kink = 0.25 + 0.95 * abs(st); + for (let i = m - 1; i >= -1; i--) { + const k = i < 0 ? 2 * m : 2 * i, dx = dirs[k], dy = dirs[k + 1]; + const sx = XC, sy = i < 0 ? sOut : sh[i].s; + // Where this region's spoke starts: at the charge, or where its ray + // leaves the outer face of the shell inside it. + let t0 = 0; + if (i < m - 1) t0 = exitRay(sx, sy, dx, dy, sh[i + 1].cy, sh[i + 1].r + sh[i + 1].w / 2); + const bx0 = sx + dx * t0, by0 = sy + dy * t0; + if (i < m - 1) arc(acc, sh[i + 1].cy, ax, ay, bx0, by0, kc, kink); + // Where it ends: the inner face of its own shell, or far off. + let t1 = i < 0 ? 400 : exitRay(sx, sy, dx, dy, sh[i].cy, sh[i].r - sh[i].w / 2); + if (t1 < t0) t1 = t0; + const ex = sx + dx * t1, ey = sy + dy * t1; + line(acc, bx0, by0, ex, ey, lc, 0.46); + ax = ex; + ay = ey; + if (ex < -2 || ex > W + 2 || ey < -2 || ey > H + 2) { + // Off the panel; the rest of the chain could still swing back in + // through a kink, so keep going only while it is near. + if (ex < -40 || ex > W + 40 || ey < -40 || ey > H + 40) break; + } + } + } + // The charge itself. + const qy = m ? sh[m - 1].s : sOut, cc = hsv(wheel(0.45 + hue), 0.15, 1, S.c); + for (let y = floor(qy - 4); y <= qy + 4; y++) + for (let x = XC - 5; x < XC + 5; x++) { + if (y < 0 || y >= H) continue; + const dx = x + 0.5 - XC, dy = y + 0.5 - qy, d2 = dx * dx + dy * dy, v = 1.3 * exp(-d2 / 1.6) + 0.25 * exp(-d2 / 7); + const o = (y * W + x) * 3; + acc[o] += cc[0] * v; + acc[o + 1] += cc[1] * v; + acc[o + 2] += cc[2] * v; + } + // Where lines pile up, scale the LED down as a whole instead of clipping + // each channel, so a crowded kink stays rose and does not wash to white. + for (let p = 0, o = 0; p < W * H; p++, o += 3) { + const pk = max(1, acc[o], acc[o + 1], acc[o + 2]), k = 255 / pk; + display.setPixel(p % W, (p / W) | 0, (acc[o] * k) | 0, (acc[o + 1] * k) | 0, (acc[o + 2] * k) | 0); + } +} From 2a0562665dce96c653f467579cd1fb2d951dbfd2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 14:20:13 +0000 Subject: [PATCH 5/8] pattern-study: Art Gallery, Holditch Ring for round 61 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JJpBXX9YfaGEvtuHofsFxv --- .../pattern-study/patterns/art-gallery.js | 418 ++++++++++++++++++ .../pattern-study/patterns/holditch-ring.js | 356 +++++++++++++++ 2 files changed, 774 insertions(+) create mode 100644 incoming/pattern-study/patterns/art-gallery.js create mode 100644 incoming/pattern-study/patterns/holditch-ring.js diff --git a/incoming/pattern-study/patterns/art-gallery.js b/incoming/pattern-study/patterns/art-gallery.js new file mode 100644 index 000000000..32ba2a434 --- /dev/null +++ b/incoming/pattern-study/patterns/art-gallery.js @@ -0,0 +1,418 @@ +// ===== Patternflow pattern ===== +// Title: Art Gallery +// Family: geometry +// Idea: A tall random gallery is drawn corner by corner, cut into triangles by clipping its ears one at a time, its corners given three colours so that every triangle has one of each, and then guards step out on the corners of the rarest colour and their light spreads over every LED they can see until the whole gallery is lit, which is Steve Fisk's proof that n/3 guards always suffice. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: art gallery problem (Victor Klee, 1973), art gallery theorem and the comb that needs every guard (Václav Chvátal, 1975), two ears theorem (Gary Meisters, 1975), three-colouring proof (Steve Fisk, 1978), pattern round 61 +// Round: 61 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Corners=12..30, Depth=0.6..0.95 +// +// Knob 1: Hue. Turns the three corner colours round together, on a wheel that skips orange and yellow. At 0 they are green, azure and pink; at 0.17 cyan, violet and red; at 0.5 violet, red and cyan. The guards' light is a pale version of whichever colour is rarest. +// Knob 2: Speed. At 2 the outline takes 1 s, the ears 2.2 s, the colours 2 s and the guards 0.8 s; the light takes 2.4 s and 0.6 s more for every guard after the first; then the gallery stays lit for 2.4 s and fades in 0.8 s. That is 13.4 s for a gallery with 4 guards and 17 s for one with 10. At 0.2 it is ten times as long, at 3 two thirds as long. +// Knob 3: Corners. n, 12 to 30. At 12 the gallery has 10 triangles, 9 diagonals and at most 4 guards; at 21 it has 19, 18 and 7; at 30 it has 28, 27 and 10. A new value starts a new gallery once the knob has rested for 0.35 s. +// Knob 4: Depth. How far the inner corners cut in from the edge toward the spine down the middle, 0.7 to 1 times this. At 0.6 they reach 42 to 60% of the way and one guard sees 90% of a 12-corner gallery on average, 68% of a 30-corner one; at 0.95 they reach 67 to 95%, and one guard sees 74% and 41%. Every third gallery is Chvátal's comb, n/3 teeth whose tips no point sees two of, so it needs all n/3 guards and the colouring gives it that many; Depth narrows its teeth from 68% to 54% of their spacing. + +const W = 64, H = 128; +const { floor, ceil, min, max, abs, sqrt, PI, exp, acos } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ss = (a, b, x) => { + const t = cl((x - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; + +// The acts of one gallery, in seconds at Speed 2, and when each one ends. +// The light act lasts as long as its guards need: they switch on 0.6 s +// apart and each front runs out at 60 LEDs a second. +const T_OUT = 1, T_EAR = 2.2, T_COL = 2, T_GRD = 0.8, T_HOLD = 2.4, T_FADE = 0.8, GAP = 0.6, FRONT = 60; +const A1 = T_OUT, A2 = A1 + T_EAR, A3 = A2 + T_COL, A4 = A3 + T_GRD; +// Rows of visibility worked out per frame. A gallery of 30 corners with +// 10 guards costs up to 2 ms a frame this way for the first 32 frames, +// and the light needs it 6 seconds in. +const VIS_ROWS = 4; + +// Hues on a wheel that skips orange through yellow, which read brown and +// olive when dim. +const wheel = (h) => 0.28 + 0.74 * (h - floor(h)); +const cross = (a, b, c) => (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]); + +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; + return out; +} + +// A star around a vertical spine from row 32 to row 96. Each corner sits on +// the ray from the spine to an even step round the edge of the panel, and +// the rays never cross, so the polygon is simple whatever the radii. An +// outer corner lies near the edge, an inner one depth x (0.7..1) of the way +// in toward the spine. +function star(n, depth) { + const X0 = 1.2, X1 = 62.8, Y0 = 1.2, Y1 = 126.8, CX = 32, S0 = 32, S1 = 96; + const a = X1 - X0, b = Y1 - Y0, per = 2 * (a + b); + const off = Math.random() * per, P = []; + let run = 1; + for (let i = 0; i < n; i++) { + let s = (off + ((i + 0.5 + (Math.random() - 0.5) * 0.5) * per) / n) % per, qx, qy; + if (s < a) (qx = X0 + s), (qy = Y0); + else if ((s -= a) < b) (qx = X1), (qy = Y0 + s); + else if ((s -= b) < a) (qx = X1 - s), (qy = Y1); + else (qx = X0), (qy = Y1 - (s - a)); + // An inner corner never follows an inner one and always follows two + // outer ones; after one outer corner it comes with chance 0.6. + const inner = run > 0 && i < n - 1 && (run > 1 || Math.random() < 0.6); + run = inner ? 0 : run + 1; + const oy = cl(qy, S0, S1), r = inner ? 1 - depth * (0.7 + 0.3 * Math.random()) : 0.92 + 0.08 * Math.random(); + P.push([CX + (qx - CX) * r, oy + (qy - oy) * r]); + } + return P; +} + +// Chvátal's comb: a bar down one side and k = n / 3 teeth across the panel, +// each tooth a triangle whose tip only a point inside that tooth's own cone +// can see. A tooth w wide at its base and 48 long, in front of a bar 12 +// deep, has a cone 1.25 w wide at the back wall, so teeth narrower than 0.8 +// of their spacing keep the cones apart. Corners: two at the back, k tips +// and two at every valley, 3k in all, and any left over go on the back wall. +function comb(n, depth) { + const k = floor(n / 3), extra = n - 3 * k, yT = 1.5, yB = 126.5, xb = 2.2, xs = 14, xt = 62.3; + const s = (yB - yT) / k, w = s * (0.92 - 0.4 * depth), P = [[xb, yT]]; + for (let e = 1; e <= extra; e++) P.push([xb - 1.2, yT + ((yB - yT) * e) / (extra + 1)]); + P.push([xb, yB]); + for (let j = 0; j < k; j++) { + const yc = yB - (j + 0.5) * s; + P.push([xt, yc]); + if (j < k - 1) P.push([xs, yc - w / 2], [xs, yc - s + w / 2]); + } + const fx = Math.random() < 0.5, fy = Math.random() < 0.5; + return P.map(([x, y]) => [fx ? W - x : x, fy ? H - y : y]); +} + +function segDist(p, a, b) { + const dx = b[0] - a[0], dy = b[1] - a[1], t = cl(((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / (dx * dx + dy * dy), 0, 1); + return Math.hypot(p[0] - a[0] - t * dx, p[1] - a[1] - t * dy); +} + +// Every corner at least 2 LEDs from every edge that does not end at it, so +// no part of the gallery is thinner than the panel can show. +function roomy(P) { + const n = P.length; + for (let i = 0; i < n; i++) + for (let j = 0; j < n; j++) { + if (j === i || (j + 1) % n === i) continue; + if (segDist(P[i], P[j], P[(j + 1) % n]) < 2) return false; + } + return true; +} + +function angleAt(a, b, c) { + const ux = a[0] - b[0], uy = a[1] - b[1], vx = c[0] - b[0], vy = c[1] - b[1]; + return acos(cl((ux * vx + uy * vy) / (Math.hypot(ux, uy) * Math.hypot(vx, vy) + 1e-9), -1, 1)); +} + +// Ear clipping. Meisters: a polygon with more than three corners has two +// ears, corners whose triangle with their neighbours lies inside it, so +// clipping one always leaves a smaller polygon to clip. Of the ears on +// offer this takes the one with the widest smallest angle, which keeps the +// triangles fat enough to read on 64 columns. +function triangulate(P, sgn) { + const idx = P.map((_, i) => i), tris = []; + while (idx.length > 3) { + let best = 0, bq = -1; + const m = idx.length; + for (let t = 0; t < m; t++) { + const a = idx[(t + m - 1) % m], b = idx[t], c = idx[(t + 1) % m]; + if (cross(P[a], P[b], P[c]) * sgn <= 1e-9) continue; + let ok = true; + for (const v of idx) { + if (v === a || v === b || v === c) continue; + const p = P[v]; + if (cross(P[a], P[b], p) * sgn >= -1e-9 && cross(P[b], P[c], p) * sgn >= -1e-9 && cross(P[c], P[a], p) * sgn >= -1e-9) { + ok = false; + break; + } + } + if (!ok) continue; + const q = min(angleAt(P[c], P[a], P[b]), angleAt(P[a], P[b], P[c]), angleAt(P[b], P[c], P[a])); + if (q > bq) (bq = q), (best = t); + } + tris.push([idx[(best + m - 1) % m], idx[best], idx[(best + 1) % m]]); + idx.splice(best, 1); + } + tris.push([idx[0], idx[1], idx[2]]); + return tris; +} + +function newGallery(S) { + const n = S.nWant, depth = S.dWant, isComb = S.count % 3 === 2; + let P = null; + for (let tries = 0; tries < 40 && !P; tries++) { + const Q = isComb ? comb(n, depth) : star(n, depth * (1 - tries * 0.012)); + if (isComb || roomy(Q)) P = Q; + } + let area = 0; + for (let i = 0; i < n; i++) area += cross([0, 0], P[i], P[(i + 1) % n]); + const sgn = area > 0 ? 1 : -1, tris = triangulate(P, sgn), T = tris.length; + // Fisk: walk the dual tree from the triangle nearest the middle. Its three + // corners take the three colours; every triangle after it shares a + // diagonal, so two corners already have colours and the third takes the + // colour left over. + let start = 0, bd = 1e9; + tris.forEach((t, i) => { + const d = Math.hypot((P[t[0]][0] + P[t[1]][0] + P[t[2]][0]) / 3 - 32, (P[t[0]][1] + P[t[1]][1] + P[t[2]][1]) / 3 - 64); + if (d < bd) (bd = d), (start = i); + }); + const col = new Int8Array(n).fill(-1), step = new Int16Array(n), done = new Int16Array(T), seen = new Uint8Array(T), queue = [start]; + let k = 0; + seen[start] = 1; + while (queue.length) { + const ti = queue.shift(), t = tris[ti]; + if (ti === start) for (let c = 0; c < 3; c++) (col[t[c]] = c), (step[t[c]] = k++); + else + for (const v of t) + if (col[v] < 0) { + let used = 0; + for (const u of t) if (col[u] >= 0) used += col[u]; + col[v] = 3 - used; + step[v] = k++; + } + done[ti] = max(step[t[0]], step[t[1]], step[t[2]]); + for (let j = 0; j < T; j++) { + if (seen[j]) continue; + let shared = 0; + for (const v of tris[j]) if (v === t[0] || v === t[1] || v === t[2]) shared++; + if (shared === 2) (seen[j] = 1), queue.push(j); + } + } + const cnt = [0, 0, 0]; + for (let i = 0; i < n; i++) cnt[col[i]]++; + let rare = 0; + for (let c = 1; c < 3; c++) if (cnt[c] < cnt[rare]) rare = c; + const guards = []; + for (let i = 0; i < n; i++) if (col[i] === rare) guards.push(i); + // Which triangle holds each LED centre, and where in it. + const tri = S.tri.fill(-1), bary = S.bary, vis = S.vis.fill(0); + tris.forEach((t, ti) => { + const a = P[t[0]], b = P[t[1]], c = P[t[2]], d = cross(a, b, c); + const x0 = max(0, floor(min(a[0], b[0], c[0]))), x1 = min(W - 1, ceil(max(a[0], b[0], c[0]))); + const y0 = max(0, floor(min(a[1], b[1], c[1]))), y1 = min(H - 1, ceil(max(a[1], b[1], c[1]))); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const p = [x + 0.5, y + 0.5], l0 = cross(b, c, p) / d, l1 = cross(c, a, p) / d, l2 = 1 - l0 - l1, o = y * W + x; + if (l0 < -1e-6 || l1 < -1e-6 || l2 < -1e-6 || tri[o] >= 0) continue; + tri[o] = ti; + // How much of each corner's colour this LED takes: e^(9 l) for + // barycentric l, so each corner's colour holds the third of the + // triangle nearest it and they blend where the thirds meet. + const e0 = exp(9 * l0), e1 = exp(9 * l1), e2 = exp(9 * l2), es = e0 + e1 + e2; + bary[o * 3] = e0 / es; + bary[o * 3 + 1] = e1 / es; + bary[o * 3 + 2] = e2 / es; + } + }); + let nIn = 0; + for (let o = 0; o < W * H; o++) if (tri[o] >= 0) S.ins[nIn++] = o; + S.nIn = nIn; + const A5 = A4 + (guards.length - 1) * GAP + 2.4; + Object.assign(S, { P, n, tris, col, step, done, guards, sgn, t: 0, visRow: 0, isComb, dirty: -1, nNow: n, dNow: depth, A5, A6: A5 + T_HOLD, A7: A5 + T_HOLD + T_FADE }); + S.count++; +} + +// A guard at corner G sees the LED centre p when p is in the gallery and +// the segment from G to p crosses no edge. On its own this test left 3 of +// 2.5 million LEDs dark over 630 galleries, centres lying on a line +// through a corner, which the triangle below takes care of. Edges ending at G cannot cross +// it; if the segment leaves through G's own corner it has to come back in +// across some other edge, so those are the only ones to test. +function visRows(S, rows) { + const { P, n, guards, tri, vis } = S; + for (let r = 0; r < rows && S.visRow < H; r++, S.visRow++) { + const y = S.visRow + 0.5; + for (let x = 0; x < W; x++) { + const o = S.visRow * W + x; + if (tri[o] < 0) continue; + const px = x + 0.5, own = S.tris[tri[o]]; + for (let g = 0; g < guards.length; g++) { + // Fisk: every triangle has one corner of each colour, so one guard + // stands on a corner of the triangle this LED is in, and a triangle + // is convex, so that guard sees it without a test. + if (own.indexOf(guards[g]) >= 0) vis[o] |= 1 << g; + if (vis[o] & (1 << g)) continue; + const gi = guards[g], G = P[gi]; + let clear = true; + for (let j = 0; j < n && clear; j++) { + const j1 = (j + 1) % n; + if (j === gi || j1 === gi) continue; + const A = P[j], B = P[j1]; + const d1 = (B[0] - A[0]) * (G[1] - A[1]) - (B[1] - A[1]) * (G[0] - A[0]); + const d2 = (B[0] - A[0]) * (y - A[1]) - (B[1] - A[1]) * (px - A[0]); + if (d1 * d2 >= 0) continue; + const d3 = (px - G[0]) * (A[1] - G[1]) - (y - G[1]) * (A[0] - G[0]); + const d4 = (px - G[0]) * (B[1] - G[1]) - (y - G[1]) * (B[0] - G[0]); + if (d3 * d4 < 0) clear = false; + } + if (clear) vis[o] |= 1 << g; + } + } + } +} + +export function setup(params) { + params.s = { + count: 0, nWant: 12, dWant: 0.6, tri: new Int16Array(W * H), bary: new Float32Array(W * H * 3), vis: new Uint16Array(W * H), ins: new Int32Array(W * H), nIn: 0, gx: new Float32Array(16), gy: new Float32Array(16), gr: new Float32Array(16), dc: [0, 0, 0], ec: [0, 0, 0], + buf: new Float32Array(W * H * 3), lifts: new Float32Array(32), tc: new Float32Array(32 * 9), hue: 0, C: [[0, 0, 0], [0, 0, 0], [0, 0, 0]], L: [0, 0, 0], c: [0, 0, 0], P: null, + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 12, 0.6], d = cl(dt || 0, 0, 0.1); + S.hue = cl(kv[0], 0, 1); + S.nWant = round(cl(kv[2], 12, 30)); + S.dWant = cl(kv[3], 0.6, 0.95); + if (!S.P) newGallery(S); + // A turned knob starts a new gallery once it has rested for 0.35 s. + if (S.nWant !== S.nNow || abs(S.dWant - S.dNow) > 0.02) { + if (S.dirty < 0) S.dirty = 0; + if ((S.dirty += d) > 0.35) S.count--, newGallery(S); + } else S.dirty = -1; + S.t += (d * cl(kv[1], 0.2, 3)) / 2; + if (S.visRow < H) visRows(S, VIS_ROWS); + if (S.t >= S.A7) newGallery(S); +} + +function round(v) { + return floor(v + 0.5); +} + +// Xiaolin Wu's line, mixed over what is there, so a grey diagonal is a +// bright seam on the dark gallery and a dark one on the lit gallery. +function line(B, x0, y0, x1, y1, c, a) { + const steep = abs(y1 - y0) > abs(x1 - x0); + let t; + if (steep) (t = x0), (x0 = y0), (y0 = t), (t = x1), (x1 = y1), (y1 = t); + if (x0 > x1) (t = x0), (x0 = x1), (x1 = t), (t = y0), (y0 = y1), (y1 = t); + const k = (y1 - y0) / (x1 - x0 || 1e-9); + for (let u = ceil(x0 - 0.5); u <= floor(x1 - 0.5); u++) { + const v = y0 + k * (u + 0.5 - x0) - 0.5, vi = floor(v), f = v - vi; + plot(B, steep ? vi : u, steep ? u : vi, c, a * (1 - f)); + plot(B, steep ? vi + 1 : u, steep ? u : vi + 1, c, a * f); + } +} + +function plot(B, x, y, c, a) { + if (x < 0 || x >= W || y < 0 || y >= H || a <= 0) return; + const o = (y * W + x) * 3; + a = min(1, a); + B[o] += (c[0] - B[o]) * a; + B[o + 1] += (c[1] - B[o + 1]) * a; + B[o + 2] += (c[2] - B[o + 2]) * a; +} + +function dot(B, x, y, r, c, a) { + for (let yy = floor(y - r); yy <= y + r; yy++) + for (let xx = floor(x - r); xx <= x + r; xx++) { + const q = Math.hypot(xx + 0.5 - x, yy + 0.5 - y) / r; + if (q < 1) plot(B, xx, yy, c, a * min(1, 1.6 * (1 - q))); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.P) return; + const { P, n, tris, col, step, done, guards, tri, bary, vis, buf: B, C, L } = S, t = S.t, nt = tris.length; + const earAt = (k) => A1 + (T_EAR * (k + 0.5)) / nt, colAt = (s) => A2 + (T_COL * (s + 0.5)) / n; + for (let c = 0; c < 3; c++) hsv(wheel(0.1 + S.hue + c / 3), 1, 1, C[c]); + const rare = col[guards[0]], G = C[rare]; + for (let c = 0; c < 3; c++) L[c] = 0.35 * G[c] + 0.65; + const fade = 1 - ss(S.A6, S.A7, t), base = hsv(wheel(0.46 + S.hue), 0.45, 0.12 * ss(0.3, A1, t), S.c); + // The light: guard k's front is FRONT x (t - A4 - k GAP) LEDs out. + const gx = S.gx, gy = S.gy, gr = S.gr; + for (let k = 0; k < guards.length; k++) (gx[k] = P[guards[k]][0]), (gy[k] = P[guards[k]][1]), (gr[k] = FRONT * (t - A4 - k * GAP)); + // Per triangle: the ear flashes as it is clipped and stays a shade + // lighter after; once its three corners have colours it takes a soft + // tint from them, each corner's colour strongest near that corner. + const lifts = S.lifts, tc = S.tc; + for (let i = 0; i < nt; i++) { + const ea = t - earAt(i), ta = 0.3 * ss(colAt(done[i]), colAt(done[i]) + 0.4, t); + lifts[i] = ea > 0 ? 1.7 + 2.5 * exp(-ea / 0.3) : 1; + for (let k = 0; k < 9; k++) tc[i * 9 + k] = C[col[tris[i][(k / 3) | 0]]][k % 3] * ta; + } + // What no guard sees yet sinks into shadow. + const sh = 1 - 0.4 * ss(A4, A4 + 1, t), ng = guards.length, lightOn = t > A4; + B.fill(0); + for (let j = 0; j < S.nIn; j++) { + const o = S.ins[j], x = o % W, y = (o / W) | 0, ti = tri[o]; + const lift = lifts[ti], w0 = bary[o * 3], w1 = bary[o * 3 + 1], w2 = bary[o * 3 + 2], q = ti * 9; + let r = base[0] * lift + w0 * tc[q] + w1 * tc[q + 3] + w2 * tc[q + 6]; + let g = base[1] * lift + w0 * tc[q + 1] + w1 * tc[q + 4] + w2 * tc[q + 7]; + let b = base[2] * lift + w0 * tc[q + 2] + w1 * tc[q + 5] + w2 * tc[q + 8]; + if (lightOn) { + let lit = 0, rim = 0; + const m = vis[o]; + for (let k = 0; k < ng; k++) { + if (!(m & (1 << k)) || gr[k] <= 0) continue; + const ddx = x + 0.5 - gx[k], ddy = y + 0.5 - gy[k], d2 = ddx * ddx + ddy * ddy, R = gr[k]; + if (R > 8 && d2 < (R - 8) * (R - 8)) { + lit++; + continue; + } + const e = R - sqrt(d2); + if (e > -3) (lit += cl(e / 4, 0, 1)), (rim += exp(-e * e / 8)); + } + if (lit > 0 || rim > 0) { + // A lit LED shows its colours at 1.2 times their strength where + // one guard sees it, 1.5 where two do and up to 1.8 where many + // do, with a little of the guards' light. + const l = 1 - Math.pow(0.5, lit), gain = 0.6 + 1.2 * l, v = 0.32 * l + 0.3 * min(1, rim); + (r = r * gain + L[0] * v), (g = g * gain + L[1] * v), (b = b * gain + L[2] * v); + } else (r *= sh), (g *= sh), (b *= sh); + } + B[o * 3] = r; + B[o * 3 + 1] = g; + B[o * 3 + 2] = b; + } + // Diagonals, each drawn from one end to the other as its ear goes. + const dc = hsv(wheel(0.37 + S.hue), 0.15, 0.62, S.dc); + for (let k = 0; k < nt - 1; k++) { + const u = cl((t - earAt(k) + 0.06) / 0.2, 0, 1); + if (u <= 0) continue; + const a = P[tris[k][0]], c = P[tris[k][2]]; + line(B, a[0], a[1], a[0] + (c[0] - a[0]) * u, a[1] + (c[1] - a[1]) * u, dc, 1); + } + // The outline, drawn round from corner 0 during the first act. + const ec = hsv(wheel(0.37 + S.hue), 0.3, 1, S.ec), drawn = (t / T_OUT) * n; + for (let i = 0; i < n; i++) { + const u = cl(drawn - i, 0, 1); + if (u <= 0) break; + const a = P[i], b = P[(i + 1) % n]; + line(B, a[0], a[1], a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u, ec, 1); + } + // Corners: white until coloured, then their colour; the guards' corners + // grow a ring once the colouring is done and the others step back. + const white = [1, 1, 1]; + const gOn = ss(A3, A3 + 0.5, t), pulse = 0.75 + 0.25 * Math.sin(t * 6); + for (let i = 0; i < n; i++) { + if (drawn < i) break; + const cs = ss(colAt(step[i]) - 0.05, colAt(step[i]) + 0.15, t), isG = col[i] === rare; + const cc = cs > 0 ? C[col[i]] : white, back = isG ? 1 : 1 - 0.5 * gOn; + dot(B, P[i][0], P[i][1], 1.3 + 0.6 * cs, cc, back); + if (isG && gOn > 0) { + const rr = 2.4 + 1.6 * gOn; + for (let k = 0; k < 28; k++) { + const an = (k / 28) * 2 * PI; + plot(B, floor(P[i][0] + rr * Math.cos(an)), floor(P[i][1] + rr * Math.sin(an)), G, gOn * pulse); + } + dot(B, P[i][0], P[i][1], 1.2, white, gOn); + } + } + const f255 = fade * 255; + for (let y = 0, o = 0; y < H; y++) + for (let x = 0; x < W; x++, o += 3) display.setPixel(x, y, min(255, B[o] * f255) | 0, min(255, B[o + 1] * f255) | 0, min(255, B[o + 2] * f255) | 0); +} diff --git a/incoming/pattern-study/patterns/holditch-ring.js b/incoming/pattern-study/patterns/holditch-ring.js new file mode 100644 index 000000000..2122f9dcd --- /dev/null +++ b/incoming/pattern-study/patterns/holditch-ring.js @@ -0,0 +1,356 @@ +// ===== Patternflow pattern ===== +// Title: Holditch Ring +// Family: line +// Idea: A chord of fixed length slides round a tall convex oval with both ends on it, the point that splits it into a and b leaves a gold trace, the ring between the oval and the trace fills behind the a-part, and an ellipse with semi-axes a and b in the middle fills with the same area of light as it goes, so both close together when the lap ends, and the next lap runs on a new oval with a different ring and the same ellipse. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Holditch's theorem (Hamnet Holditch, 1858), support functions and Minkowski sums of convex bodies (Hermann Minkowski, 1903), pattern round 61 +// Round: 61 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Split=0.25..0.5, Chord=36..44 +// +// Knob 1: Hue. Turns the colours round together. At 0 the oval is pale cyan, the trace gold and the ring and the ellipse violet; at 0.33 the oval is pink, the trace teal and the fills green; at 0.67 pale lime, violet and azure. +// Knob 2: Speed. At 2 a lap takes 6 seconds, then the full ring and ellipse pulse together for 1.2 s and the oval morphs into the next of six in 1.8 s, 9 seconds in all; at 0.2 the cycle takes 90 seconds, at 3 six. +// Knob 3: Split. a / (a + b), 0.25 to 0.5. With the opening 36 LED chord, at 0.25 a is 9 and b 27, the ring holds pi x 9 x 27 = 763 square LEDs and the ellipse is 18 wide and 54 tall; at 0.5 a and b are both 18, the ring holds 1018 and the ellipse is a circle 36 across. +// Knob 4: Chord. a + b, 36 to 44 LEDs. At a split of 0.25 the ring and the ellipse hold 763 square LEDs at 36 and 1140 at 44, and at 0.5 up to 1521. At 46 the first point ahead would jump 14 LEDs across the pointed end of the egg, and the chord would no longer slide. + +const W = 64, H = 128, CX = 32, CY = 64; +const { floor, ceil, min, max, abs, sqrt, PI, cos, sin, atan2, hypot } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ss = (a, b, x) => { + const t = cl((x - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; +// Points on the oval, taken at K even steps of the outward normal, and +// chord positions per lap. +const K = 240, N = 240; +// One cycle at Speed 2, in seconds. +const LAP = 6, HOLD = 1.2, MORPH = 1.8; + +// The ovals, each a Minkowski sum: its support function is the sum of its +// parts', so a blend of two ovals taken normal by normal is convex all the +// way through. Parts: [0, A, B] an ellipse, [1, w, h] a rectangle, +// [2, r] a disc, [3, points] a polygon. In turn: an ellipse, a rounded +// rectangle, an egg, a stadium, a shield and a lozenge, each 58 x 122 LEDs, +// with ey where the ellipse sits best inside its trace. A tall rounded +// triangle was tried and left out: at its point a 40 LED chord already +// jumps 39 LEDs from one side to the other. +const OVALS = [ + { p: [[0, 29, 61]], ey: 64 }, + { p: [[1, 17, 49], [2, 12]], ey: 64 }, + { p: [[0, 25, 52], [3, [[0, -9], [4, 9], [-4, 9]]]], ey: 70 }, + { p: [[1, 0, 32], [2, 29]], ey: 64 }, + { p: [[3, [[-12, -44], [12, -44], [12, 28], [0, 44], [-12, 28]]], [2, 17]], ey: 60 }, + { p: [[3, [[0, -40], [9, -20], [9, 20], [0, 40], [-9, 20], [-9, -20]]], [0, 20, 21]], ey: 64 }, +]; + +// The point of an oval whose outward normal is (c, s). +function support(oval, c, s, out) { + let x = 0, y = 0; + for (const q of oval.p) { + if (q[0] === 0) { + const h = hypot(q[1] * c, q[2] * s); + (x += (q[1] * q[1] * c) / h), (y += (q[2] * q[2] * s) / h); + } else if (q[0] === 1) (x += c < 0 ? -q[1] : q[1]), (y += s < 0 ? -q[2] : q[2]); + else if (q[0] === 2) (x += q[1] * c), (y += q[1] * s); + else { + let best = -1e9, bx = 0, by = 0; + for (const v of q[1]) { + const d = v[0] * c + v[1] * s; + if (d > best) (best = d), (bx = v[0]), (by = v[1]); + } + (x += bx), (y += by); + } + } + out[0] = x; + out[1] = y; +} + +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; + return out; +} +// Soft fills take hues from a wheel that skips orange through yellow, +// which read brown and olive when dim. +const wheel = (h) => 0.28 + 0.74 * (h - floor(h)); + +export function setup(params) { + params.s = { + f: 0, t: 0, ox: new Float32Array(K), oy: new Float32Array(K), arc: new Float64Array(K + 1), + sx: new Float32Array((N + 1) * 4), ang: new Float64Array(N + 1), area: new Float64Array(N + 1), + key: "", ring: new Uint8Array(W * H), tn: new Uint8Array(H), tx: new Float32Array(H * 8), xl: new Float32Array(H), xr: new Float32Array(H), buf: new Float32Array(W * H * 3), + c: [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], tmp: [0, 0], tmq: [0, 0], + }; +} + +// The oval at morph m between oval f and the next one. +function buildOval(S, m) { + const A = OVALS[S.f % OVALS.length], B = OVALS[(S.f + 1) % OVALS.length], p = S.tmp, q = S.tmq; + for (let i = 0; i < K; i++) { + const psi = ((i + 0.5) / K) * TAU, c = cos(psi), s = sin(psi); + support(A, c, s, p); + support(B, c, s, q); + S.ox[i] = CX + p[0] + (q[0] - p[0]) * m; + S.oy[i] = CY + p[1] + (q[1] - p[1]) * m; + } + S.arc[0] = 0; + for (let i = 1; i <= K; i++) S.arc[i] = S.arc[i - 1] + hypot(S.ox[i % K] - S.ox[i - 1], S.oy[i % K] - S.oy[i - 1]); + // Start each lap at the top of the oval, normal straight up. + S.u0 = S.arc[(3 * K) / 4]; + S.ey = A.ey + (B.ey - A.ey) * m; +} + +// The point of the oval at arc length u, and the edge it lies on. +function at(S, u, out) { + const per = S.arc[K]; + u = ((u % per) + per) % per; + let lo = 0, hi = K; + while (hi - lo > 1) { + const md = (lo + hi) >> 1; + if (S.arc[md] <= u) lo = md; + else hi = md; + } + const f = (u - S.arc[lo]) / (S.arc[lo + 1] - S.arc[lo] || 1), j = (lo + 1) % K; + out[0] = S.ox[lo] + (S.ox[j] - S.ox[lo]) * f; + out[1] = S.oy[lo] + (S.oy[j] - S.oy[lo]) * f; + out[2] = lo; +} + +// The other end of the chord: walking on from P, the first point of the +// oval at distance L, solved exactly on the edge where it lies. +function far(S, px, py, edge, L, out) { + for (let k = 0; k < K; k++) { + const i = (edge + k) % K, j = (i + 1) % K; + const ax = k ? S.ox[i] : px, ay = k ? S.oy[i] : py, bx = S.ox[j], by = S.oy[j]; + if (hypot(bx - px, by - py) < L) continue; + const dx = bx - ax, dy = by - ay, fx = ax - px, fy = ay - py; + const a = dx * dx + dy * dy, b = 2 * (fx * dx + fy * dy), c = fx * fx + fy * fy - L * L; + const t = (-b + sqrt(max(0, b * b - 4 * a * c))) / (2 * a); + out[0] = ax + dx * t; + out[1] = ay + dy * t; + return; + } +} + +// N + 1 chord positions round the lap: P, the split point D, the chord's +// angle unwrapped, and the area the a-part has swept so far, quad by quad. +function sampleLap(S, sp, L) { + const p = [0, 0, 0], q = [0, 0], per = S.arc[K], X = S.sx; + let prev = 0; + for (let k = 0; k <= N; k++) { + at(S, S.u0 + (k / N) * per, p); + far(S, p[0], p[1], p[2], L, q); + X[k * 4] = p[0]; + X[k * 4 + 1] = p[1]; + X[k * 4 + 2] = p[0] + (q[0] - p[0]) * sp; + X[k * 4 + 3] = p[1] + (q[1] - p[1]) * sp; + let an = atan2(q[1] - p[1], q[0] - p[0]); + if (k) { + while (an - prev > PI) an -= TAU; + while (an - prev < -PI) an += TAU; + } + S.ang[k] = prev = an; + S.area[k] = k ? S.area[k - 1] + quadArea(X, k - 1) : 0; + } + S.dir = S.ang[N] > S.ang[0] ? 1 : -1; + // The last quad meets the first again, so the a-part has swept the ring + // once: the total is pi a b to a few parts in ten thousand. + S.total = abs(S.area[N]); +} + +function quadArea(X, k) { + const a = k * 4, b = a + 4; + const px = [X[a], X[a + 2], X[b + 2], X[b]], py = [X[a + 1], X[a + 3], X[b + 3], X[b + 1]]; + let s = 0; + for (let i = 0; i < 4; i++) s += px[i] * py[(i + 1) % 4] - px[(i + 1) % 4] * py[i]; + return s / 2; +} + +// Marks the LED centres inside triangle a b c with v. +function tri(M, ax, ay, bx, by, cx, cy, v) { + const d = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax); + if (abs(d) < 1e-9) return; + const x0 = max(0, floor(min(ax, bx, cx))), x1 = min(W - 1, ceil(max(ax, bx, cx))); + const y0 = max(0, floor(min(ay, by, cy))), y1 = min(H - 1, ceil(max(ay, by, cy))); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const px = x + 0.5, py = y + 0.5; + const l0 = ((cx - bx) * (py - by) - (cy - by) * (px - bx)) / d, l1 = ((ax - cx) * (py - cy) - (ay - cy) * (px - cx)) / d; + if (l0 >= -1e-6 && l1 >= -1e-6 && 1 - l0 - l1 >= -1e-6) { + const o = y * W + x; + if (M[o] < v) M[o] = v; + } + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0.25, 36], d = cl(dt || 0, 0, 0.1); + S.hue = cl(kv[0], 0, 1); + S.sp = cl(kv[2], 0.25, 0.5); + S.L = cl(kv[3], 36, 44); + S.t += (d * cl(kv[1], 0.2, 3)) / 2; + if (S.t >= LAP + HOLD + MORPH) (S.t -= LAP + HOLD + MORPH), S.f++; + const m = ss(LAP + HOLD, LAP + HOLD + MORPH, S.t); + // The lap is worked out again only when the oval or a knob has moved. + const key = S.f + "," + m.toFixed(4) + "," + S.sp.toFixed(4) + "," + S.L.toFixed(3); + if (key !== S.key) { + S.key = key; + buildOval(S, m); + sampleLap(S, S.sp, S.L); + } +} + +// Xiaolin Wu's line, mixed over what is there. +function line(B, x0, y0, x1, y1, c, a) { + const steep = abs(y1 - y0) > abs(x1 - x0); + let t; + if (steep) (t = x0), (x0 = y0), (y0 = t), (t = x1), (x1 = y1), (y1 = t); + if (x0 > x1) (t = x0), (x0 = x1), (x1 = t), (t = y0), (y0 = y1), (y1 = t); + const k = (y1 - y0) / (x1 - x0 || 1e-9); + for (let u = ceil(x0 - 0.5); u <= floor(x1 - 0.5); u++) { + const v = y0 + k * (u + 0.5 - x0) - 0.5, vi = floor(v), f = v - vi; + plot(B, steep ? vi : u, steep ? u : vi, c, a * (1 - f)); + plot(B, steep ? vi + 1 : u, steep ? u : vi + 1, c, a * f); + } +} + +function plot(B, x, y, c, a) { + if (x < 0 || x >= W || y < 0 || y >= H || a <= 0) return; + const o = (y * W + x) * 3; + a = min(1, a); + B[o] += (c[0] - B[o]) * a; + B[o + 1] += (c[1] - B[o + 1]) * a; + B[o + 2] += (c[2] - B[o + 2]) * a; +} + +function dot(B, x, y, r, c, a) { + for (let yy = floor(y - r); yy <= y + r; yy++) + for (let xx = floor(x - r); xx <= x + r; xx++) { + const q = hypot(xx + 0.5 - x, yy + 0.5 - y) / r; + if (q < 1) plot(B, xx, yy, c, a * min(1, 1.6 * (1 - q))); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.key) return; + const B = S.buf, X = S.sx, M = S.ring, t = S.t, [cBase, cOval, cFill, cTrace, cA, cB] = S.c, h = S.hue; + hsv(wheel(0.43 + h), 0.25, 0.11, cBase); + hsv(0.53 + h, 0.45, 1, cOval); + hsv(wheel(0.73 + h), 0.7, 0.6, cFill); + hsv(0.13 + h, 0.65, 1, cTrace); + hsv(wheel(0.73 + h), 0.35, 1, cA); + hsv(0.53 + h, 0.1, 0.9, cB); + // How far round the lap the chord is, and how bright the fills are: they + // pulse together in the hold and sink back to a ghost at the end of the + // morph, ready for the next lap to fill them again. + const lap = cl(t / LAP, 0, 1), kf = lap * N, k = min(N - 1, floor(kf)), fr = kf - k; + const full = t >= LAP, pulse = full ? 1 + 0.25 * sin((PI * (t - LAP)) / HOLD) * (t < LAP + HOLD ? 1 : 0) : 1; + const sink = ss(LAP + HOLD + MORPH - 0.5, LAP + HOLD + MORPH, t), lit = (full ? 1 - sink * 0.65 : 1) * pulse, ghost = 0.35; + // The chord now, exactly, between sample k and k + 1. + const p = [0, 0, 0], q = [0, 0]; + at(S, S.u0 + lap * S.arc[K], p); + far(S, p[0], p[1], p[2], S.L, q); + const sp = S.sp, dx = p[0] + (q[0] - p[0]) * sp, dy = p[1] + (q[1] - p[1]) * sp; + // The ring is what lies inside the oval and outside the trace. M marks + // the LEDs the a-part has swept so far, quad by quad. With a < b the + // a-part can dip a little inside the trace and come back out; that part + // counts once forward and once back, so it is no part of the ring. + M.fill(0); + for (let i = 0; i < (full ? N : k); i++) { + const a = i * 4, b = a + 4; + tri(M, X[a], X[a + 1], X[a + 2], X[a + 3], X[b + 2], X[b + 3], 2); + tri(M, X[a], X[a + 1], X[b + 2], X[b + 3], X[b], X[b + 1], 2); + } + if (!full) { + const a = k * 4; + tri(M, X[a], X[a + 1], X[a + 2], X[a + 3], dx, dy, 2); + tri(M, X[a], X[a + 1], dx, dy, p[0], p[1], 2); + } + // The ellipse fills as a sector with the area the ring has so far: + // parametric angle 2 A / (a b), which comes to 2 pi as the lap closes. + const ea = sp * S.L, eb = S.L - ea, ecx = CX, ecy = S.ey; + const swept = full ? S.total : abs(S.area[k] + (S.area[k + 1] - S.area[k]) * fr); + const phi = full ? TAU : min(TAU, (2 * swept) / (ea * eb)), a0 = S.ang[0], dir = S.dir; + // Row by row, where the oval starts and ends. + const xl = S.xl.fill(1e9), xr = S.xr.fill(-1e9); + for (let i = 0; i < K; i++) { + const j = (i + 1) % K, y0 = S.oy[i], y1 = S.oy[j]; + for (let y = ceil(min(y0, y1) - 0.5); y <= floor(max(y0, y1) - 0.5); y++) { + if (y < 0 || y >= H || y0 === y1) continue; + const x = S.ox[i] + ((S.ox[j] - S.ox[i]) * (y + 0.5 - y0)) / (y1 - y0); + if (x < xl[y]) xl[y] = x; + if (x > xr[y]) xr[y] = x; + } + } + // Where the whole trace crosses each row, for an inside test by parity. + const tn = S.tn.fill(0), tx = S.tx; + for (let i = 0; i < N; i++) { + const x0 = X[i * 4 + 2], y0 = X[i * 4 + 3], x1 = X[i * 4 + 6], y1 = X[i * 4 + 7]; + for (let y = max(0, ceil(min(y0, y1) - 0.5)); y <= min(H - 1, floor(max(y0, y1) - 0.5)); y++) { + const yc = y + 0.5; + if (yc >= y0 === yc >= y1 || tn[y] >= 8) continue; + tx[y * 8 + tn[y]++] = x0 + ((x1 - x0) * (yc - y0)) / (y1 - y0); + } + } + B.fill(0); + for (let y = 0, o = 0; y < H; y++) + for (let x = 0; x < W; x++, o++) { + const cov = cl(x + 1 - xl[y], 0, 1) * cl(xr[y] - x, 0, 1); + if (cov <= 0) continue; + let r = cBase[0], g = cBase[1], b = cBase[2], f = 0, inT = 0; + for (let c = 0; c < tn[y]; c++) if (tx[y * 8 + c] < x + 0.5) inT ^= 1; + if (!inT) f = M[o] ? lit : ghost; + const u = (x + 0.5 - ecx) / ea, v = (y + 0.5 - ecy) / eb; + if (u * u + v * v <= 1) { + const beta = atan2(v, u); + f = (((dir * (beta - a0)) % TAU) + TAU) % TAU <= phi ? lit : ghost; + } + (r += cFill[0] * f), (g += cFill[1] * f), (b += cFill[2] * f); + B[o * 3] = r * cov; + B[o * 3 + 1] = g * cov; + B[o * 3 + 2] = b * cov; + } + // The ellipse's rim, faint, and its hand at the parametric angle reached. + for (let i = 0; i < 96; i++) { + const a1 = (i / 96) * TAU, a2 = ((i + 1) / 96) * TAU; + line(B, ecx + ea * cos(a1), ecy + eb * sin(a1), ecx + ea * cos(a2), ecy + eb * sin(a2), cA, 0.45); + } + const ha = a0 + dir * phi, hx = ecx + ea * cos(ha), hy = ecy + eb * sin(ha); + line(B, ecx, ecy, hx, hy, cA, 0.9); + // Every eighth chord of the lap so far, faint, and a tail of the last 36 + // behind the chord, fading. + for (let i = 0; i < (full ? N : k); i++) { + let al = i % 8 ? 0 : 0.16; + if (!full && k - i < 36 && i % 3 === 0) al = max(al, 0.45 * (1 - (k - i) / 36)); + if (al <= 0) continue; + const a = i * 4, px = X[a], py = X[a + 1]; + line(B, px, py, px + (X[a + 2] - px) / sp, py + (X[a + 3] - py) / sp, cB, al); + } + // The oval. + for (let i = 0; i < K; i++) { + const j = (i + 1) % K; + line(B, S.ox[i], S.oy[i], S.ox[j], S.oy[j], cOval, 0.95); + } + // The trace of the split point, as far as the chord has gone. + const kk = full ? N : k; + for (let i = 0; i < kk; i++) line(B, X[i * 4 + 2], X[i * 4 + 3], X[i * 4 + 6], X[i * 4 + 7], cTrace, 1); + if (!full) line(B, X[k * 4 + 2], X[k * 4 + 3], dx, dy, cTrace, 1); + // The chord: the a-part in the fill's colour, the b-part white. + line(B, p[0], p[1], dx, dy, cA, 1); + line(B, dx, dy, q[0], q[1], cB, 1); + dot(B, p[0], p[1], 1.6, cB, 1); + dot(B, q[0], q[1], 1.6, cB, 1); + dot(B, dx, dy, 2, cTrace, 1); + dot(B, hx, hy, 1.8, cTrace, 1); + for (let i = 0, o = 0; i < W * H; i++, o += 3) + display.setPixel(i % W, (i / W) | 0, min(255, B[o] * 255) | 0, min(255, B[o + 1] * 255) | 0, min(255, B[o + 2] * 255) | 0); +} From 49db94a4e9f55d3a23497d8675342efc2df62b4c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 14:20:17 +0000 Subject: [PATCH 6/8] pattern-study: Paul Trap, Greenhill Column for round 61 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JJpBXX9YfaGEvtuHofsFxv --- .../patterns/greenhill-column.js | 271 ++++++++++++++++ incoming/pattern-study/patterns/paul-trap.js | 303 ++++++++++++++++++ 2 files changed, 574 insertions(+) create mode 100644 incoming/pattern-study/patterns/greenhill-column.js create mode 100644 incoming/pattern-study/patterns/paul-trap.js diff --git a/incoming/pattern-study/patterns/greenhill-column.js b/incoming/pattern-study/patterns/greenhill-column.js new file mode 100644 index 000000000..9ccc7c94e --- /dev/null +++ b/incoming/pattern-study/patterns/greenhill-column.js @@ -0,0 +1,271 @@ +// ===== Patternflow pattern ===== +// Title: Greenhill Column +// Family: physics +// Idea: Eight elastic columns of different stiffness stand clamped along the bottom row and grow upward at a steady rate, each straight until it passes its own critical height, marked by a white tick, where it buckles under its own weight into the heavy elastica, solved every frame by shooting on the tip angle, and swings over until its tip points below the horizontal, when it shrinks back, straightens and grows again, so the stiffest one in the middle stands tallest. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: buckling of a heavy column under its own weight (Leonhard Euler, 1778; Alfred George Greenhill, 1881), Euler's flagpole with a weight on top (Leonhard Euler, 1744), pattern round 61 +// Round: 61 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Spread=3..64, Top weight=0..40 +// +// Knob 1: Hue. At 0 the columns run from azure for the softest through blue and violet to rose for the stiffest, dim while straight and bright where they bend hardest; the knob turns them all round the wheel together, skipping orange, yellow and lime, which read brown and olive when dim. +// Knob 2: Speed. At 2 the columns grow 8 LEDs a second and shrink back at 16. The stiffest takes 1 to 3 s to grow from where it last shrank back to its tick, 2 s more to fall over and about 2 s to shrink and straighten. At 0.2 they grow 0.8 LEDs a second, at 3 12. +// Knob 3: Spread. The stiffest column's EI over the softest's, 3 to 64, in equal ratios between. Greenhill's height goes as the cube root of EI, so at 3 the eight columns buckle at 83 to 120 LEDs, at 16 at 48 to 120 and at 64 at 30 to 120. +// Knob 4: Top weight. A weight P on every tip, 0 to 40 LEDs' worth of column. Near 0 it is Greenhill's column, buckling at 83 to 120 LEDs at Spread 3; at 20 they buckle at 67 to 103, at 40 at 56 to 90. Euler's flagpole, the top weight alone, buckles where P = pi^2 EI / (4 L^2), 67 to 117 LEDs at 40, and the softer a column the closer it comes: at Spread 64 and 40 the softest buckles at 14 LEDs against the flagpole's 15. + +const W = 64, H = 128, NC = 8, NS = 24; +const { floor, min, max, abs, sqrt, sin, cos, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// Lengths in LEDs, weight per LED of column Q = 1. Greenhill's column +// buckles once q L^3 / EI passes 7.837, where (2/3) sqrt(q L^3 / EI) reaches +// 1.866, the first zero of the Bessel function J_-1/3 in his solution; +// Euler had tried the same problem in 1778. The stiffest column has EI = +// 120^3 / 7.837, so with no top weight it buckles at 120 LEDs, and the +// others step down in EI by equal ratios to 1 / Spread of it. +const Q = 1, LTOP = 120, GREENHILL = 7.837; +// The tip angle at which a column gives up and shrinks back: 100 degrees, +// reached at 1.14 times the critical height with no top weight. +const DROOP = (100 * PI) / 180; +// Where each column stands, left to right: its rank in stiffness, 0 the +// softest, so the stiffest stand in the middle. +const RANK = [0, 2, 4, 6, 7, 5, 3, 1]; +const BASEY = H - 0.5; +// The pen: a core 2.6 LEDs wide at full strength and a glow out to 4.5, +// tabulated by squared distance. +const R2 = 4.5 * 4.5, PEN_STEP = 12, PEN = new Float32Array(Math.ceil(R2 * PEN_STEP) + 1); +for (let i = 0; i < PEN.length; i++) { + const dd = sqrt(i / PEN_STEP); + PEN[i] = dd <= 1.3 ? 1 : 0.7 * exp(-((dd - 1.3) * (dd - 1.3)) / 4.5); +} + +// The heavy elastica: EI theta''(s) = -(q (L - s) + P) sin theta, with s +// measured up the column. Scaled to s in [0, 1] it is theta'' = -(lam (1 - +// s) + mu) sin theta, lam = q L^3 / EI and mu = P L^2 / EI. This integrates +// it by RK4 in NS steps from the free top, where theta = alpha and theta' = 0 +// (no moment), down to the clamp, and returns theta there, which must be 0. +// If th is given it gets theta and theta' at the points s = i / NS. +function shoot(alpha, lam, mu, th, dth) { + let t = alpha, w = 0, s = 1; + const h = -1 / NS; + if (th) { + th[NS] = t; + dth[NS] = 0; + } + for (let i = NS - 1; i >= 0; i--) { + const a1 = -(lam * (1 - s) + mu), a2 = -(lam * (1 - s - h / 2) + mu), a3 = -(lam * (1 - s - h) + mu); + const k1t = w, k1w = a1 * sin(t); + const k2t = w + (h / 2) * k1w, k2w = a2 * sin(t + (h / 2) * k1t); + const k3t = w + (h / 2) * k2w, k3w = a2 * sin(t + (h / 2) * k2t); + const k4t = w + h * k3w, k4w = a3 * sin(t + h * k3t); + t += (h / 6) * (k1t + 2 * k2t + 2 * k3t + k4t); + w += (h / 6) * (k1w + 2 * k2w + 2 * k3w + k4w); + s += h; + if (th) { + th[i] = t; + dth[i] = w; + } + } + return t; +} + +// The linear problem u'' = -(lam (1 - s) + mu) u with u(1) = 1, u'(1) = 0: +// the column is critical where u(0) first reaches 0. +function linear(lam, mu) { + let u = 1, w = 0, s = 1; + const h = -1 / NS; + for (let i = 0; i < NS; i++) { + const a1 = -(lam * (1 - s) + mu), a2 = -(lam * (1 - s - h / 2) + mu), a3 = -(lam * (1 - s - h) + mu); + const k1u = w, k1w = a1 * u; + const k2u = w + (h / 2) * k1w, k2w = a2 * (u + (h / 2) * k1u); + const k3u = w + (h / 2) * k2w, k3w = a2 * (u + (h / 2) * k2u); + const k4u = w + h * k3w, k4w = a3 * (u + h * k3u); + u += (h / 6) * (k1u + 2 * k2u + 2 * k3u + k4u); + w += (h / 6) * (k1w + 2 * k2w + 2 * k3w + k4w); + s += h; + } + return u; +} + +// The critical height of a column of stiffness EI under top weight P, by +// bisection on L between 0 and its Greenhill height. +function critical(EI, P) { + let lo = 0, hi = Math.cbrt((GREENHILL * EI) / Q) * 1.0001; + for (let it = 0; it < 28; it++) { + const L = (lo + hi) / 2; + if (linear((Q * L * L * L) / EI, (P * L * L) / EI) > 0) lo = L; + else hi = L; + } + return (lo + hi) / 2; +} + +// The tip angle of the buckled column, by bisection, starting from a +// bracket round the last answer and falling back to a scan down from pi. +// The bifurcation is supercritical, so the answer grows from 0 at the +// critical height: 41 degrees at 1.02 times it, 88 at 1.10. +function tipAngle(lam, mu, prev, ratio) { + let lo = -1, hi = -1; + if (prev > 0.02) { + const a = max(1e-4, prev - 0.12), b = min(PI - 1e-4, prev + 0.12); + if (shoot(a, lam, mu) < 0 && shoot(b, lam, mu) > 0) { + lo = a; + hi = b; + } + } + if (lo < 0) { + if (shoot(1e-4, lam, mu) > 0 && ratio < 1.5) return 0; + for (let k = 47; k >= 1; k--) { + const a = (k * PI) / 48; + if (shoot(a, lam, mu) < 0) { + lo = a; + hi = ((k + 1) * PI) / 48 - 1e-4; + break; + } + } + if (lo < 0) return 0; + } + for (let it = 0; it < 20; it++) { + const m = (lo + hi) / 2; + if (shoot(m, lam, mu) < 0) lo = m; + else hi = m; + } + return (lo + hi) / 2; +} + +// Hue h in turns, with orange, yellow and lime (0.03 to 0.25) skipped, +// since they read brown and olive when dim. +function hsv(h, s, v, out) { + h = (h - floor(h)) * 0.78; + if (h >= 0.03) h += 0.22; + 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; + return out; +} + +export function setup(params) { + const cols = []; + for (let k = 0; k < NC; k++) + cols.push({ x: 3.5 + 8 * k, rank: RANK[k], EI: 1, Lc: 60, L: 0, grow: 1, side: 1, back: 0.8, alpha: 0, th: new Float64Array(NS + 1), dth: new Float64Array(NS + 1) }); + params.s = { cols, spread: -1, P: -1, hue: 0, started: false, rgb: new Float32Array(W * H * 3), cb: new Float32Array(W * H), touched: new Int32Array(W * H), c: [0, 0, 0] }; + return params.s; +} + +function pickSide(c) { + c.side = Math.random() < 0.5 ? -1 : 1; + c.back = 0.8 + 0.12 * Math.random(); +} + +export function update(dt, input, params) { + const S = params.s || setup(params); + const kv = input.knobValues || [0, 2, 3, 0], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0] || 0; + const speed = cl(kv[1], 0.2, 3), spread = cl(kv[2], 3, 64), P = cl(kv[3], 0, 40); + if (spread !== S.spread || P !== S.P) { + S.spread = spread; + S.P = P; + for (const c of S.cols) { + c.EI = ((Q * LTOP ** 3) / GREENHILL) * spread ** (-(NC - 1 - c.rank) / (NC - 1)); + c.Lc = critical(c.EI, P); + } + } + if (!S.started) { + // Pre-roll: every column starts somewhere in its cycle, from its + // shrink-back height to 1.11 times its critical height, 80% of them + // growing. + S.started = true; + for (const c of S.cols) { + pickSide(c); + c.L = c.Lc * (c.back + (1.11 - c.back) * Math.random()); + c.grow = Math.random() < 0.8 ? 1 : -1; + } + } + const rate = 4 * speed * d; + for (const c of S.cols) { + // Shrinking runs at twice the growth rate, and faster when a knob has + // left a column more than 1.2 times its new critical height. + c.L = max(4, c.grow > 0 ? c.L + rate : c.L - max(2 * rate, 3 * d * (c.L - 1.2 * c.Lc))); + const lam = (Q * c.L * c.L * c.L) / c.EI, mu = (S.P * c.L * c.L) / c.EI; + c.alpha = c.L > c.Lc ? tipAngle(lam, mu, c.alpha, c.L / c.Lc) : 0; + if (c.grow > 0 && c.alpha > DROOP) c.grow = -1; + else if (c.grow < 0 && c.L < c.back * c.Lc) { + c.grow = 1; + pickSide(c); + } + if (c.alpha > 0) shoot(c.alpha, lam, mu, c.th, c.dth); + else { + c.th.fill(0); + c.dth.fill(0); + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const rgb = S.rgb, cb = S.cb, touched = S.touched, col = S.c; + rgb.fill(0); + for (const c of S.cols) { + let nt = 0; + const seg = c.L / NS; + let x = c.x, y = BASEY; + for (let i = 0; i < NS; i++) { + const t0 = c.th[i], t1 = c.th[i + 1], tm = (t0 + t1) / 2; + const nx = x + c.side * sin(tm) * seg, ny = y - cos(tm) * seg; + const steps = max(1, Math.ceil(seg)); + for (let k = 0; k < steps; k++) { + const f = (k + 0.5) / steps, px = x + (nx - x) * f, py = y + (ny - y) * f; + // Bending stress: the curvature times the length, 0 on a straight + // column and about 4 at the clamp when the tip is horizontal. + const kap = abs(c.dth[i] + (c.dth[i + 1] - c.dth[i]) * f); + const b = 0.42 + 0.58 * min(1, kap / 4); + const x0 = max(0, floor(px - 4.5)), x1 = min(W - 1, Math.ceil(px + 4.5)); + const y0 = max(0, floor(py - 4.5)), y1 = min(H - 1, Math.ceil(py + 4.5)); + for (let Y = y0; Y <= y1; Y++) { + const dy = Y + 0.5 - py; + for (let X = x0; X <= x1; X++) { + const dx = X + 0.5 - px, d2 = dx * dx + dy * dy; + if (d2 >= R2) continue; + const v = b * PEN[(d2 * PEN_STEP) | 0], p = Y * W + X; + if (cb[p] === 0) touched[nt++] = p; + if (v > cb[p]) cb[p] = v; + } + } + } + x = nx; + y = ny; + } + hsv(0.48 + S.hue + 0.065 * c.rank, 0.78, 1, col); + for (let k = 0; k < nt; k++) { + const p = touched[k], v = cb[p], o = p * 3; + // Hot spots of stress go toward white. + const wv = max(0, v - 0.75) * 1.6; + const r = v * col[0] + wv, g = v * col[1] + wv, bl = v * col[2] + wv; + if (r > rgb[o]) rgb[o] = r; + if (g > rgb[o + 1]) rgb[o + 1] = g; + if (bl > rgb[o + 2]) rgb[o + 2] = bl; + cb[p] = 0; + } + } + // The ticks: a white bar 7 LEDs wide at each column's critical height, + // dimmer over the three LEDs where a straight column passes through it. + for (const c of S.cols) { + const ty = floor(BASEY - c.Lc); + if (ty < 0 || ty >= H) continue; + const cx = floor(c.x); + for (let X = cx - 3; X <= cx + 3; X++) { + if (X < 0 || X >= W) continue; + const o = (ty * W + X) * 3, v = abs(X - cx) <= 1 ? 0.55 : 0.85; + rgb[o] = max(rgb[o], v); + rgb[o + 1] = max(rgb[o + 1], v); + rgb[o + 2] = max(rgb[o + 2], v); + } + } + for (let p = 0, o = 0; p < W * H; p++, o += 3) + display.setPixel(p % W, (p / W) | 0, min(255, rgb[o] * 255) | 0, min(255, rgb[o + 1] * 255) | 0, min(255, rgb[o + 2] * 255) | 0); +} diff --git a/incoming/pattern-study/patterns/paul-trap.js b/incoming/pattern-study/patterns/paul-trap.js new file mode 100644 index 000000000..815dca2b0 --- /dev/null +++ b/incoming/pattern-study/patterns/paul-trap.js @@ -0,0 +1,303 @@ +// ===== Patternflow pattern ===== +// Title: Paul Trap +// Family: particle +// Idea: Ions in a linear Paul trap whose axis runs the length of the panel repel each other into a Coulomb crystal, a zigzag when the RF is weak, a string of round dots when it is strong and a twisted three-dimensional crystal when more ions crowd in, each ion off the RF null smeared across the axis by its micromotion as a camera sees it, until past q = 0.908 the Mathieu equation has no stable solution and the ions boil out sideways while new ones load at the ends. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Mathieu equation (Émile Mathieu, 1868), the Paul trap (Wolfgang Paul, 1953), structural transitions of ion Coulomb crystals (Daniel Dubin, 1993; John Schiffer, 1993), the critical ratio of the zigzag (Daniel Enzer and others, 2000), pattern round 61 +// Round: 61 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Ions=16..40, RF q=0.3..0.95 +// +// Knob 1: Hue. Turns the fluorescence round the wheel. At 0 the ions are azure with pale cores, the near side of a 3D crystal cyan and the far side blue; at 0.25 violet, at 0.5 rose, at 0.75 green, then cyan back to azure. Orange and yellow are skipped, since they read brown and olive when dim. +// Knob 2: Speed. At 2 the simulation runs 38 RF periods a second: the oven fills an empty trap with 16 ions in half a second, they settle into a crystal by 1 s, and a knock from the background gas every 4 s or so melts part of it for a moment. At 0.2 ten times slower, so the first seconds show the load; at 3 half as fast again. +// Knob 3: Ions. 16 to 40 ions, two of them dark impurities that leave gaps. The end caps are set so that a string of that many ions would be 122 rows long. At q = 0.3, 16 ions make a flat zigzag and 20 a zigzag that twists toward the camera; from 28 up the crystal is a spindle of shells, 32 LEDs across and 79 rows tall at 40 ions, with the outer ions drawn as dashes by their micromotion. Enzer's ratio puts the string at q = 0.54 for 16 ions, 0.72 for 20 and 0.86 for 24, but above about 0.7 a crystal with ions off the axis heats itself in the RF and melts in patches, so from 24 up it never lines up cleanly. +// Knob 4: RF q. The Mathieu q of the drive, 0.3 to 0.95. At 0.3 sixteen ions make a zigzag 10 LEDs across; it narrows as q rises and lines up into a string of round dots between 0.50 and 0.54, which holds to 0.9 with a shiver near the end. At a = 0 nothing is stable past 0.908; the small static field that lays the zigzag flat splits that edge, so past 0.903 the ions fly out toward the camera and past 0.915 sideways, at 0.95 swinging 1.8 times wider every RF period, and the new ions the oven loads at the ends race inward and burst into smears. Turned back, the trap refills and cools into a crystal again, and a quick turn from string to zigzag can freeze in a kink where the zigzag changes sides. + +const W = 64, H = 128, CX = 31.5, CY = 63.5, NMAX = 40; +const { floor, min, max, abs, sqrt, exp, cos, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// Units: lengths in LEDs, time tau = Omega t / 2, mass 1. In tau the radial +// equation is the Mathieu equation x'' + (a - 2 q cos 2 tau) x = F, and one +// RF period is pi. The step is a fortieth of it; velocity Verlet at that +// step puts the edge of stability at q = 0.9074 instead of 0.9081. With a +// twentieth the edge was 0.905 and a string of 16 ions melted at q = 0.9. +// RATE is tau per second at Speed 1. +const DT = PI / 40, RATE = 60; +// Half the length of a string of N ions in a harmonic well, for a Coulomb +// constant and an axial frequency of 1, from a relaxation run at N = 12, 16 +// ... 48. The axial frequency for N ions is then chosen so that the string +// would reach SPAN LEDs either side of the centre. +const HALF = [3.2187, 3.8175, 4.3281, 4.7778, 5.1825, 5.5522, 5.8939, 6.2125, 6.5141, 6.7941]; +const SPAN = 61; +// The Coulomb constant, in LED^3 / tau^2. With it the axial frequency for +// 16 ions is 0.050, and Enzer's critical ratio of radial to axial frequency, +// 0.73 N^0.86 = 7.9, asks for a radial frequency of 0.40, which the RF gives +// at q = 0.54. The simulated crystal straightens between 0.50 and 0.54. +const KC = 10.2; +// A small static quadrupole, a = -0.007 across the panel and +0.007 toward +// the camera, makes the picture plane the weaker radial direction, so the +// zigzag lies flat across the panel. At 0.005 it twisted out of the plane. +const AS = 0.007; +// Doppler cooling damps every bright ion's velocity at GAMMA per tau, and +// the recoil of the scattered photons heats it toward TEMP. The dark ions +// scatter nothing and are cooled only through their neighbours. +const GAMMA = 0.05, TEMP = 0.0012; +// Background gas: one ion takes a hard knock every COLL tau on average, 4.2 s +// at Speed 2, and the crystal melts round it and freezes again. An ion +// RLOST from the axis has reached an electrode. While the trap holds fewer +// ions than it should, the oven loads one every LOAD tau, at alternate ends. +const COLL = 500, KICK = 5, RLOST = 38, LOAD = 4; + +// A gamma of 0.75 on the way out lifts the faint smears of hot ions. +const GAMMA_LUT = new Uint8Array(1024); +for (let i = 0; i < 1024; i++) GAMMA_LUT[i] = Math.round(255 * (i / 1023) ** 0.75); + +function halfOf(n) { + const f = cl((n - 12) / 4, 0, HALF.length - 1.001), i = floor(f); + return HALF[i] + (HALF[i + 1] - HALF[i]) * (f - i); +} + +// The fluorescence colour, hue h in turns, with orange and yellow (0.03 +// to 0.2) skipped since they read brown and olive when dim. +function hsv(h, s, v, out) { + h = (h - floor(h)) * 0.83; + if (h >= 0.03) h += 0.17; + 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; + return out; +} + +const gauss = () => { + let u = 1 - Math.random(); + return sqrt(-2 * Math.log(u)) * cos(2 * PI * Math.random()); +}; + +export function setup(params) { + const S = (params.s = { + n: 0, target: 16, q: 0.3, tau: 0, wz: 0.047, ws: 0.047, nextColl: COLL, loadAt: 0, end: 1, + x: new Float64Array(NMAX), y: new Float64Array(NMAX), z: new Float64Array(NMAX), + vx: new Float64Array(NMAX), vy: new Float64Array(NMAX), vz: new Float64Array(NMAX), + ax: new Float64Array(NMAX), ay: new Float64Array(NMAX), az: new Float64Array(NMAX), + dark: new Uint8Array(NMAX), + // Per frame sums of each ion's position, the camera's exposure. + sx: new Float64Array(NMAX), sxx: new Float64Array(NMAX), sy: new Float64Array(NMAX), syy: new Float64Array(NMAX), + sz: new Float64Array(NMAX), sn: new Float64Array(NMAX), + rgb: new Float32Array(W * H * 3), gx: new Float32Array(W), hx: new Float32Array(W), gy: new Float32Array(H), hy: new Float32Array(H), + hue: 0, c: [0, 0, 0], w: [0, 0, 0], started: false, loaded: 0, darkSet: false, + }); + return S; +} + +function axialFor(n) { + return sqrt(KC) * (halfOf(n) / SPAN) ** 1.5; +} + +function addIon(S, y, hot, dark) { + const i = S.n++; + S.x[i] = gauss() * hot; + S.z[i] = gauss() * hot; + S.y[i] = y; + const v = hot * 0.06; + S.vx[i] = gauss() * v; + S.vz[i] = gauss() * v; + S.vy[i] = gauss() * v; + S.dark[i] = dark ? 1 : 0; + S.sx[i] = S.sxx[i] = S.sy[i] = S.syy[i] = S.sz[i] = S.sn[i] = 0; + return i; +} + +function removeIon(S, i) { + const j = --S.n; + if (i === j) return; + for (const a of [S.x, S.y, S.z, S.vx, S.vy, S.vz, S.ax, S.ay, S.az, S.dark, S.sx, S.sxx, S.sy, S.syy, S.sz, S.sn]) a[i] = a[j]; +} + +function darkCount(S) { + let d = 0; + for (let i = 0; i < S.n; i++) d += S.dark[i]; + return d; +} + +// The acceleration of every ion at phase tau. +function forces(S, tau) { + const n = S.n, x = S.x, y = S.y, z = S.z, ax = S.ax, ay = S.ay, az = S.az; + const rf = 2 * S.q * cos(2 * tau), w2 = S.wz * S.wz; + const kx = -w2 / 2 - AS - rf, kz = -w2 / 2 + AS + rf; + for (let i = 0; i < n; i++) { + ax[i] = -kx * x[i]; + az[i] = -kz * z[i]; + ay[i] = -w2 * y[i]; + } + for (let i = 0; i < n; i++) { + const xi = x[i], yi = y[i], zi = z[i]; + let fx = 0, fy = 0, fz = 0; + for (let j = i + 1; j < n; j++) { + const dx = xi - x[j], dy = yi - y[j], dz = zi - z[j]; + const r2 = dx * dx + dy * dy + dz * dz + 0.05, f = KC / (r2 * sqrt(r2)); + fx += f * dx; + fy += f * dy; + fz += f * dz; + ax[j] -= f * dx; + ay[j] -= f * dy; + az[j] -= f * dz; + } + ax[i] += fx; + ay[i] += fy; + az[i] += fz; + } +} + +function step(S) { + const n = S.n, h = DT; + for (let i = 0; i < n; i++) { + S.vx[i] += 0.5 * h * S.ax[i]; + S.vy[i] += 0.5 * h * S.ay[i]; + S.vz[i] += 0.5 * h * S.az[i]; + S.x[i] += h * S.vx[i]; + S.y[i] += h * S.vy[i]; + S.z[i] += h * S.vz[i]; + } + S.tau += h; + forces(S, S.tau); + const kick = sqrt(2 * GAMMA * TEMP * h); + for (let i = 0; i < n; i++) { + S.vx[i] += 0.5 * h * S.ax[i]; + S.vy[i] += 0.5 * h * S.ay[i]; + S.vz[i] += 0.5 * h * S.az[i]; + if (S.dark[i]) continue; + // Doppler cooling and the recoil of the scattered photons. + const g = 1 - GAMMA * h; + S.vx[i] = S.vx[i] * g + kick * (Math.random() - 0.5) * 3.46; + S.vy[i] = S.vy[i] * g + kick * (Math.random() - 0.5) * 3.46; + S.vz[i] = S.vz[i] * g + kick * (Math.random() - 0.5) * 3.46; + } + for (let i = 0; i < S.n; i++) { + S.sx[i] += S.x[i]; + S.sxx[i] += S.x[i] * S.x[i]; + S.sy[i] += S.y[i]; + S.syy[i] += S.y[i] * S.y[i]; + S.sz[i] += S.z[i]; + S.sn[i]++; + } +} + +export function update(dt, input, params) { + const S = params.s || setup(params); + const kv = input.knobValues || [0, 2, 16, 0.3], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0] || 0; + const speed = cl(kv[1], 0.2, 3); + S.target = cl(Math.round(kv[2]), 16, NMAX); + S.q = cl(kv[3], 0.3, 0.95); + if (!S.started) { + // The trap opens empty and the oven fills it from the ends. + S.started = true; + S.wz = S.ws = axialFor(S.target); + } + // The end caps follow the ion count with a time constant of 40 tau. + S.ws = axialFor(S.target); + for (let i = 0; i < S.n; i++) S.sx[i] = S.sxx[i] = S.sy[i] = S.syy[i] = S.sz[i] = S.sn[i] = 0; + const steps = min(120, Math.round((d * speed * RATE) / DT)); + for (let k = 0; k < steps; k++) { + S.wz += (S.ws - S.wz) * 0.002; + step(S); + // Losses: an ion that has reached an electrode or left past an end cap. + for (let i = S.n - 1; i >= 0; i--) + if (S.x[i] * S.x[i] + S.z[i] * S.z[i] > RLOST * RLOST || abs(S.y[i]) > 90 || !(abs(S.x[i]) < 1e6)) { + removeIon(S, i); + if (S.loadAt < S.tau) S.loadAt = S.tau + LOAD; + } + if (S.n > S.target && S.tau > S.loadAt) { + // Fewer ions wanted: the end cap opens for the outermost one. + let o = 0; + for (let i = 1; i < S.n; i++) if (abs(S.y[i]) > abs(S.y[o])) o = i; + removeIon(S, o); + S.loadAt = S.tau + LOAD * 0.5; + } else if (S.n < S.target && S.tau > S.loadAt) { + // An atom is ionised on the axis at one end and comes in at 0.6 LEDs + // per tau. After a loss a new ion is dark one time in ten while the + // crystal holds fewer than two. + S.end = -S.end; + const dk = S.loaded >= S.target && darkCount(S) < 2 && Math.random() < 0.1; + S.loaded++; + const i = addIon(S, S.end * (SPAN * 0.95), 0.1, dk); + S.vy[i] = -S.end * 0.6; + S.loadAt = S.tau + LOAD; + } + // When the first load is in, the two ions nearest two fifths of the way + // up and two fifths of the way down go dark, as after a reaction with the + // background gas, so the gaps open inside the crystal. + if (!S.darkSet && S.n === S.target) { + S.darkSet = true; + for (const yd of [-0.4 * SPAN, 0.4 * SPAN]) { + let b = 0; + for (let i = 1; i < S.n; i++) if (abs(S.y[i] - yd) < abs(S.y[b] - yd)) b = i; + S.dark[b] = 1; + } + } + if (S.tau > S.nextColl) { + S.nextColl = S.tau - COLL * Math.log(1 - Math.random()); + if (S.n) { + const i = floor(Math.random() * S.n); + const ux = gauss(), uy = gauss(), uz = gauss(), u = sqrt(ux * ux + uy * uy + uz * uz) || 1; + S.vx[i] += (KICK * ux) / u; + S.vy[i] += (KICK * uy) / u; + S.vz[i] += (KICK * uz) / u; + } + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const rgb = S.rgb, gx = S.gx, hx = S.hx, gy = S.gy, hy = S.hy, c = S.c, w = S.w, q = S.q; + // Each frame keeps 40% of the last one, an exposure a little longer than a frame. + for (let o = 0; o < rgb.length; o++) rgb[o] *= 0.4; + for (let i = 0; i < S.n; i++) { + if (S.dark[i] || !S.sn[i]) continue; + const n = S.sn[i], mx = S.sx[i] / n, my = S.sy[i] / n, mz = S.sz[i] / n; + // The spread over the exposure: secular motion as measured, and at + // least the micromotion an ion at mx has, amplitude q mx / 2. + const mm = (q * mx) / 2; + const vxx = max(S.sxx[i] / n - mx * mx, (mm * mm) / 2), vyy = max(0, S.syy[i] / n - my * my); + // Depth: toward the camera is brighter, bigger and paler. + const dz = cl(mz / 10, -1, 1); + const s0 = 0.8 + 0.18 * dz, sh = 3.3 + 0.5 * dz; + const sx = sqrt(s0 * s0 + vxx), sy = sqrt(s0 * s0 + vyy), tx = sqrt(sh * sh + vxx), ty = sqrt(sh * sh + vyy); + const spread = (s0 * s0) / (sx * sy); + const lum = (0.72 + 0.28 * dz) * spread ** 0.25; + hsv(0.46 + S.hue - 0.05 * dz, 0.85, 1, c); + hsv(0.46 + S.hue - 0.05 * dz, 0.3 - 0.15 * dz, 1, w); + const px = mx + CX, py = my + CY; + const x0 = max(0, floor(px - 3 * tx)), x1 = min(W - 1, Math.ceil(px + 3 * tx)); + const y0 = max(0, floor(py - 3 * ty)), y1 = min(H - 1, Math.ceil(py + 3 * ty)); + if (x0 > x1 || y0 > y1) continue; + for (let X = x0; X <= x1; X++) { + const e = X - px; + gx[X] = exp((-e * e) / (2 * sx * sx)); + hx[X] = exp((-e * e) / (2 * tx * tx)); + } + for (let Y = y0; Y <= y1; Y++) { + const e = Y - py; + gy[Y] = exp((-e * e) / (2 * sy * sy)); + hy[Y] = exp((-e * e) / (2 * ty * ty)); + } + const A = 0.75 * lum, B = 0.21 * lum; + for (let Y = y0; Y <= y1; Y++) + for (let X = x0; X <= x1; X++) { + const core = A * gx[X] * gy[Y], halo = B * hx[X] * hy[Y], o = (Y * W + X) * 3; + rgb[o] += core * w[0] + halo * c[0]; + rgb[o + 1] += core * w[1] + halo * c[1]; + rgb[o + 2] += core * w[2] + halo * c[2]; + } + } + for (let p = 0, o = 0; p < W * H; p++, o += 3) + display.setPixel(p % W, (p / W) | 0, GAMMA_LUT[min(1023, (rgb[o] * 1023) | 0)], GAMMA_LUT[min(1023, (rgb[o + 1] * 1023) | 0)], GAMMA_LUT[min(1023, (rgb[o + 2] * 1023) | 0)]); +} From 7c2f46ca7d8cfba29f855a703c28dccbc6c84bf6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 14:27:56 +0000 Subject: [PATCH 7/8] pattern-study: README notes for round 61 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JJpBXX9YfaGEvtuHofsFxv --- incoming/pattern-study/README.md | 70 ++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 2c1fa2b38..b00f5e516 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 팹턮 연구 2026-10 -새 팹턮 498개륌 쉰여섯 갈래로 나눠 만듀었얎요. 1판에서 엎섞 갈래로 23개륌 만듀고, 2판에서 1판에 없던 아홉 갈래로 16개륌, 3판에서 하나륌, 4판에서 새 갈래읞 직묌로 하나륌 더했얎요. 5판은 새 팹턮 없읎 1판의 Euclid Pulse 륌 닀시 ì§°ê³ , 6판은 섞욎 팚널의 128쀄을 위아래로 ë‹€ 쓰는 팹턮 하나륌 더했얎요. 7판은 또렷한 몚양읎 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 8판은 새 갈래읞 Ʞ계로 하나륌 더했얎요. 9판은 새 팹턮 없읎 1판의 Sprout 을 닀시 ì§°ì–Žìš”. 10판은 섞욎 팚널을 떚얎지는 랔록의 우묌로 쓰는 팹턮 하나륌 더했얎요. 11판은 여러 색읎 ꜀ 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 12판은 새 갈래읞 천첎로 하나륌 더했얎요. 13판은 새 팹턮 없읎 1판의 Glyph Rain 을 닀시 ì§°ì–Žìš”. 14판은 절찚가 바뀌Ʞ 전에 띄욎 바퀎띌서, 섞욎 팚널의 높읎륌 ë‹€ 떚얎지는 폭포 하나륌 더했얎요. 15판은 바뀐 절찚의 첫 판읎띌 ì—Ž 갈래에 새 팚턎을 하나씩, 몚두 ì—Ž 개륌 더했얎요. 도시와 음악 갈래는 귞때 새로 엎었얎요. 16판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 바닀와 곀충 갈래륌 새로 엎었얎요. 17판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 전자 회로와 장난감 갈래륌 새로 엎었얎요. 18판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 유늬와 ꎑ묌·결정, 놀읎공원 갈래륌 새로 엎었얎요. 19판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 부엌곌 버섯·균사 갈래륌 새로 엎었얎요. 20판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 새와 화학·싀험 갈래륌 새로 엎었얎요. 21판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 종읎와 욎동·겜Ʞ 갈래륌 새로 엎었얎요. 22판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 몞곌 비행 갈래륌 새로 엎었얎요. 23판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 묎대·공연곌 짐승 갈래륌 새로 엎었얎요. 24판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 공사·걎섀곌 도자Ʞ 갈래륌 새로 엎었얎요. 25판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 농사·밭곌 화산 갈래륌 새로 엎었얎요. 26판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 얌음·극지와 믞생묌 갈래륌 새로 엎었얎요. 27판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 철도와 필늄·영화 갈래륌 새로 엎었얎요. 28판은 죌제 표가 수학 쪜윌로 바뀐 첫 판읎띌 읎늄 있는 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했고, 수론 갈래륌 새로 엎었얎요. 29판은 죌제 표가 수학 여섯곌 화장품 팝업 넷윌로 나뉜 첫 판읎띌, 읎늄 있는 수학 대상 여섯곌 화장품 맀장 벜에 틀 질감 넷을 더했고, 위상·맀듭곌 화장품 팝업 갈래륌 새로 엎었얎요. 30판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 맀장의 LED 벜에 틀 질감 넷을 화장품 팝업 갈래에 더했얎요. 31판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 ì—Žì§€ 않았얎요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜의 질감 넷을 화장품 팝업 갈래에 더했얎요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 41판은 죌제 표가 수학·추상 닀섯곌 할로윈 닀섯윌로 바뀐 첫 판읎띌, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고 할로윈 갈래륌 새로 ì—Žì–Ž 10월 31음에 틀 팹턮 닀섯을 넣었얎요. 42판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 41판읎 연 할로윈 갈래에 더했얎요. 43판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 44판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 45판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 ì—Ž 개 아래읞 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 46판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 47판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 48판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 49판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 50판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 적은 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 51판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 52판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 적은 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 53판은 할로윈 몫읎 쉬게 된 첫 판읎띌, 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 54판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 ì—Ž 갈래에 하나씩 더했얎요. 55판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 56판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 57판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 ì—Ž 갈래에 하나씩 더했얎요. 58판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 ì—Ž 갈래에 하나씩 더했얎요. 59판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. [index.html](index.html) 을 람띌우저로 엎멎 전부 한 장에서 돌아가요. ë§š 위 지도에서 점을 누륎멎 ê·ž 팚턎윌로 가고, 칎드의 슬띌읎더가 녾뾌 넷읎에요. 마음에 드는 것은 「윔드 복사」로 가젞가 Pattern Lab 의 CODE 칞에 붙여 넣윌멎 같은 귞늌읎 나와요. 페읎지는 파음 하나띌서 바깥에서 받아 였는 것읎 없얎요. +새 팹턮 508개륌 쉰여섯 갈래로 나눠 만듀었얎요. 1판에서 엎섞 갈래로 23개륌 만듀고, 2판에서 1판에 없던 아홉 갈래로 16개륌, 3판에서 하나륌, 4판에서 새 갈래읞 직묌로 하나륌 더했얎요. 5판은 새 팹턮 없읎 1판의 Euclid Pulse 륌 닀시 ì§°ê³ , 6판은 섞욎 팚널의 128쀄을 위아래로 ë‹€ 쓰는 팹턮 하나륌 더했얎요. 7판은 또렷한 몚양읎 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 8판은 새 갈래읞 Ʞ계로 하나륌 더했얎요. 9판은 새 팹턮 없읎 1판의 Sprout 을 닀시 ì§°ì–Žìš”. 10판은 섞욎 팚널을 떚얎지는 랔록의 우묌로 쓰는 팹턮 하나륌 더했얎요. 11판은 여러 색읎 ꜀ 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 12판은 새 갈래읞 천첎로 하나륌 더했얎요. 13판은 새 팹턮 없읎 1판의 Glyph Rain 을 닀시 ì§°ì–Žìš”. 14판은 절찚가 바뀌Ʞ 전에 띄욎 바퀎띌서, 섞욎 팚널의 높읎륌 ë‹€ 떚얎지는 폭포 하나륌 더했얎요. 15판은 바뀐 절찚의 첫 판읎띌 ì—Ž 갈래에 새 팚턎을 하나씩, 몚두 ì—Ž 개륌 더했얎요. 도시와 음악 갈래는 귞때 새로 엎었얎요. 16판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 바닀와 곀충 갈래륌 새로 엎었얎요. 17판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 전자 회로와 장난감 갈래륌 새로 엎었얎요. 18판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 유늬와 ꎑ묌·결정, 놀읎공원 갈래륌 새로 엎었얎요. 19판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 부엌곌 버섯·균사 갈래륌 새로 엎었얎요. 20판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 새와 화학·싀험 갈래륌 새로 엎었얎요. 21판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 종읎와 욎동·겜Ʞ 갈래륌 새로 엎었얎요. 22판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 몞곌 비행 갈래륌 새로 엎었얎요. 23판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 묎대·공연곌 짐승 갈래륌 새로 엎었얎요. 24판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 공사·걎섀곌 도자Ʞ 갈래륌 새로 엎었얎요. 25판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 농사·밭곌 화산 갈래륌 새로 엎었얎요. 26판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 얌음·극지와 믞생묌 갈래륌 새로 엎었얎요. 27판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 철도와 필늄·영화 갈래륌 새로 엎었얎요. 28판은 죌제 표가 수학 쪜윌로 바뀐 첫 판읎띌 읎늄 있는 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했고, 수론 갈래륌 새로 엎었얎요. 29판은 죌제 표가 수학 여섯곌 화장품 팝업 넷윌로 나뉜 첫 판읎띌, 읎늄 있는 수학 대상 여섯곌 화장품 맀장 벜에 틀 질감 넷을 더했고, 위상·맀듭곌 화장품 팝업 갈래륌 새로 엎었얎요. 30판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 맀장의 LED 벜에 틀 질감 넷을 화장품 팝업 갈래에 더했얎요. 31판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 ì—Žì§€ 않았얎요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜의 질감 넷을 화장품 팝업 갈래에 더했얎요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 41판은 죌제 표가 수학·추상 닀섯곌 할로윈 닀섯윌로 바뀐 첫 판읎띌, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고 할로윈 갈래륌 새로 ì—Žì–Ž 10월 31음에 틀 팹턮 닀섯을 넣었얎요. 42판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 41판읎 연 할로윈 갈래에 더했얎요. 43판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 44판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 45판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 ì—Ž 개 아래읞 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 46판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 47판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 48판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 49판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 50판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 적은 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 51판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 52판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 적은 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 53판은 할로윈 몫읎 쉬게 된 첫 판읎띌, 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 54판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 ì—Ž 갈래에 하나씩 더했얎요. 55판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 56판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 57판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 ì—Ž 갈래에 하나씩 더했얎요. 58판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 ì—Ž 갈래에 하나씩 더했얎요. 59판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 61판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 갈래부터 ì—Ž 갈래에 하나씩 더했얎요. [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판에서 섞로로 닀시 ì§°ì–Žìš”. @@ -795,6 +795,21 @@ | [Kohonen Map](patterns/kohonen-map.js) | 데읎터 귞늌 | 묎작위로 엉킚 12×24 격자가 점 하나가 올 때마닀 귞쪜윌로 끌렀가며 하늘색에서 자홍윌로 묌든 천처럌 고늬, 쎈승달, S 위에 펎젞요. Lattice 녞람륌 끝까지 돌늬멎 288마디 사슬읎 고늬륌 읎늬저늬 ì ‘ì–Ž 메워요. | | [Laser Speckle](patterns/laser-speckle.js) | 녞읎슈·흐늄 | 묎작위 위상을 채욎 동공의 푞늬에 변환읎 끓는 얌룩읎 되고, 밝Ʞ는 위상자 합의 크Ʞ, 색은 ê·ž 위상읎띌 얎두욎 점마닀 색읎 한 바퀮 돌아요. 위쪜 찜의 동공읎 원판, 고늬, 바늘구멍 둘로 바뀌멎 알갱읎 ꌎ도 바뀌고, 아래 막대가 e^-I 곡선을 따띌가요. | +### 61판 + +| 팹턮 | 갈래 | 한 쀄 | +| --- | --- | --- | +| [Crackling Noise](patterns/crackling-noise.js) | 녞읎슈·흐늄 | 영옚 묎작위장 읎징 자석을 아래에서 위로 쓞얎 올늬멎 눈사태 하나하나가 제 색윌로 êž°ì–Ž 나가서, 판읎 크고 작은 덩얎늬의 조각볎가 돌요. ë‹€ 뒀집히멎 거꟞로 쓞얎 낎렀 얎둑한 조각볎륌 낚Ʞ고 새 묎질서로 닀시 핎요. | +| [Monge Transport](patterns/monge-transport.js) | 알고늬슘 | 한쪜 반의 도형에 몚읞 입자 더믞가 닀륞 반 도형의 자늬와 아묎렇게나 짝지얎졌닀가, 거늬 합읎 짧아지는 쌍마닀 목적지륌 맞바꿔 엇갈늌읎 풀며 ë’€ 한꺌번에 ꎑ선을 따띌 믞끄러젞 새 도형윌로 낎렀앉아요. 입자 색은 처음 도형에서의 각도띌서 도착한 색 바퀎가 옮김대로 닀시 놓여요. | +| [Paul Trap](patterns/paul-trap.js) | 입자 | 섞로축을 따띌 늘얎선 읎옚 엎여섯 개가 RF q 0.3 에서 지귞재귞로 서고, q 륌 올늬멎 둥귌 점 한 쀄로 펎졌닀가 0.908 을 넘윌멎 옆윌로 번지며 덫 밖윌로 날아가요. 빛나지 않는 불순묌 읎옚 둘읎 결정에 얎두욎 틈을 낎요. | +| [Greenhill Column](patterns/greenhill-column.js) | 묌늬·슀프링 | 바닥에 박힌 êž°ë‘¥ 여덟 개가 자띌닀가 흰 눈ꞈ윌로 찍은 제 임계 높읎륌 넘윌멎 제 묎게에 꺟여 옆윌로 눕고, 끝읎 수평 아래로 낎렀가멎 쀄얎듀었닀가 닀시 음얎서요. | +| [Art Gallery](patterns/art-gallery.js) | Ʞ하·대칭 | í‚€ 큰 닀각형의 귀륌 하나씩 잘띌 삌각형윌로 나누고 ꌭ짓점을 섞 색윌로 칠하멎, 가장 적은 색의 ꌭ짓점에 선 겜비원듀의 빛읎 가러늬륌 빈틈없읎 밝혀요. 섞 번에 한 번은 읎빚마닀 겜비원읎 하나씩 있얎알 하는 흐바탈의 빗읎에요. | +| [Holditch Ring](patterns/holditch-ring.js) | 선·궀적 | Ꞟ읎가 정핎진 현읎 í‚€ 큰 볌록 곡선을 따띌 한 바퀮 믞끄러지는 동안, 현을 a 와 b 로 나누는 점읎 ꞈ빛 자췚륌 낚Ʞ고 곡선곌 자췚 사읎의 고늬와 가욎데의 반축 a, b 타원읎 같은 넓읎만큌 핚께 찚올띌요. 바퀎마닀 곡선읎 달걀, 겜Ʞ장, 방팚 ꌎ로 바뀌얎 고늬 몚양은 달띌젞도 타원은 귞대로예요. | +| [Thomson Kink](patterns/thomson-kink.js) | 묌결·간섭 | 가욎데 쀄을 였륎낎늬던 전하가 거꟞로 돌거나 멈출 때마닀 빛의 속도로 퍌지는 껍질읎 생Ʞ고, 껍질 사읎마닀 귞때의 속도로 지ꞈ 있을 자늬에서 뻗은 곧은 역선읎 분홍 꺟임윌로 읎얎젞요. | +| [Binary Lens](patterns/binary-lens.js) | 공간·원귌 | 서로 도는 두 질량읎 만드는 확대 지도에서 밝은 컀슀틱읎 런족점에서 만나며 별 몚양 둘곌 육각 고늬, 가욎데 작은 것곌 섞몚 둘 사읎륌 였가요. 지도륌 걎너는 별의 밝Ʞ가 아래 띠에 흘러가며 컀슀틱을 넘을 때마닀 솟아요. | +| [Taylor Unmixing](patterns/taylor-unmixing.js) | 시뮬레읎션 | 끈적한 유첎에 떚얎뜚늰 엌료 음곱 방욞읎 쿠에튞 흐늄에 비슀듬한 가는 쀄로 늘얎나 팚널을 몇 바퀮 감았닀가, 벜읎 되돌아가멎 처음 자늬의 고늬 안윌로 둥Ꞁ게 돌아와요. 확산을 올늬멎 위아래로 번진 흐늰 얌룩윌로 돌아와요. | +| [Canard Explosion](patterns/canard-explosion.js) | 늬듬·펄슀 | 반 데륎 폎 진동자가 작은 혾프 고늬에서 반발 가지륌 타는 뚞늬 없는 였늬와 뚞늬 달늰 였늬륌 지나 완화 진동까지 찚례로 걞얎가고, 지나옚 고늬가 색 띠로 겹겹읎 낚아요. 아래에서는 x(t) 가 흘러낎늬며 잔묌결읎 ꞎ 톱니로 바뀌얎요. | + ## 1판 갈래마닀 녾뾌 넷의 자늬는 갈래가 달띌도 거의 같아요. K1 은 색읎고 K2 는 빠륎Ʞ예요. K3 은 몚양을 바꟞고, K4 는 ê·ž 갈래만의 손잡읎예요. 귞래서 처음 볎는 팹턮도 K1 곌 K2 는 손읎 뚌저 알아요. @@ -3089,6 +3104,50 @@ E8 의 ê·Œ 240개륌 고셋의 4₂₁ 닀멎첎 ꌭ짓점윌로 ê·žë žì–Žìš”. 죌제 5 로 헀륎베륎튞 자읎페륎튞(1934)의 알고늬슘을 위상·맀듭 갈래에 섞웠얎요. 방향을 당 맀듭 귞늌의 교찚륌 방향대로 풀얎 자읎페륎튞 원윌로 나누고, 원마닀 원판을 채욎 ë’€ 교찚마닀 반 바퀮 ꌬ읞 띠로 읎윌멎 겜계가 ê·ž 맀듭읞 곡멎읎 되고, ê·ž 종수는 (c − s + 1)/2 예요. 팚널은 2가닥읎나 3가닥 닫힌 땋임윌로 섞로로 귞늰 맀듭 여덟 개(3_1, 4_1, 5_1, 5_2, 6_2, 6_3, 7_1, 8_19)륌 맀듭, 풀Ʞ, 원, 원판, 띠, ë‹€ 된 곡멎 찚례로 볎여 죌고, 원판은 깊읎 22 LED 씩 ë–šì–Žì ž 쌓여서 화멎읎 돌멎 옆윌로 벌얎젞요. 원판읎 볎여 죌는 멎은 파랑에서 볎띌, 닀륞 멎은 ꞈ색읎띌 띠마닀 반 ꌬ임을 지나며 ꞈ색읎 파랑윌로 바뀌얎요. 팚턎의 맀듭 짓Ʞ로 여덟 맀듭을 ë‹€ 셌더니 원 수가 가닥 수와 같고 였음러 지표가 s − c, 겜계가 하나였얎요. 축소 뷰띌우 표현윌로 따로 셈한 알렉산더 닀항식도 읎늄 붙은 맀듭곌 맞았얎요. 뜻밖읎었던 것은 5_2 하나만 읎 땋임에서 종수 2가 나와 싀제 종수 1볎닀 크닀는 점읎에요. 닀륞 땋임은 생성원마닀 한 부혞로만 나였는 동질 땋임읎띌 슀톚링슀(1978)에 따띌 가장 작은 종수가 나였지만, 5_2 의 σ1³σ2σ1⁻¹σ2 는 σ1 읎 두 부혞로 나와요. 읎 맀듭은 알고늬슘읎 가장 작은 곡멎을 죌지 않는 예로 목록에 낚게얎요. Ʞ욞Ʞ륌 80도로 킀우멎 원판읎 얇은 판처럌 서고 띠가 ê·ž 사읎에 닀늬처럌 걞렀서 짜임읎 더 잘 읜혀요. `thirds` 64/67/64, 녾뾌 0.05, 0.21, 0.22, 0.09 예요. +## 61판 + +61판도 ì—Ž 개 몚두 죌제 5 「수학 · 추상」읎에요. 같읎 돈 60판(#274)읎 고륞 갈래와 수학, 지도 칞 둘(움직임 × 채움 4,1 곌 움직임 × 색 1,4)을 ê·ž PR 볞묞에서 읜고 플했고, 앞 바퀮 PR 61개에는 댓Ꞁ도 늬뷰도 없얎서 따륌 Ꞁ읎 없었얎요. dev 의 498개에 60판의 ì—Ž 개륌 얹얎 섞멎 추상 갈래 가욎데 녞읎슈·흐늄곌 입자, 늬듬·펄슀가 엎음곱윌로 가장 적고 나뚞지는 엎여덟읎에요. ê·ž 셋에 하나씩, 60판읎 비쌜 간 Ʞ하·대칭곌 묌결·간섭에 하나씩 넣고, 낚은 닀섯은 귞늌읎 맞는 알고늬슘, 묌늬·슀프링, 선·궀적, 공간·원귌, 시뮬레읎션에 넣었얎요. 죌제 1 은 498개에 둘뿐읞 칞읎 없얎서, 셋뿐읞 칞 슀묌닀섯 가욎데 60판읎 고륎지 않은 움직임 × 몚양 바뀜 1,4 에 Crackling Noise 륌, 채움 × 몚양 바뀜 1,4 에 Monge Transport 륌 섞웠얎요. 죌제 4 는 Paul Trap 곌 Greenhill Column 읎에요. ì—Ž 개 ë‹€ 「흔한 것」에도 「낯선 수학 예」에도 없고, Greenhill Column 은 58판읎 「목록 밖에서 핮 볌 것」에 적은 귞늰힐의 Ʞ둥읎에요. 처음 고륞 ì—Ž 개 가욎데 둘은 ì§“êž° 전에 갈았얎요. 고늬 진동자의 ꌬ읞 상태(대니얌 와음늬, 슀티랐 슀튞로가잠, 믞셞 거번, 2006)는 35판 Chimera State 와 같은 고늬 위 위상 진동자였고, 토러슀륌 ë‹€ 덮는 묎작위 걞음의 늊은 점(아믞륎 뎀볎와 섞 사람, 2006)은 40판 Brownian Frontier 처럌 걞음읎 지나간 LED 륌 쌜는 귞늌읎었얎요. 윘웚읎의 지형(1997)도 생각했는데, 위 반평멎에 귞늬멎 28판 Ford Circles 와 29판 J-Invariant 의 칞 귞늌곌 같아젞서 접었얎요. ì—Ž 개는 음Ꟍ 닀섯읎 레포 밖 worktree 에서 둘씩 짓고, 접쎉 시튞(처음 녾뾌, 녞람마닀 닀섯 자늬)로 하나씩 볞 ë’€ 합쳀얎요. 합치멎서 고친 팚턎은 없얎요. + +### Crackling Noise + +죌제 5 와 1 로 제임슀 섞슀나와 닀섯 사람(1993)의 영옚 묎작위장 읎징 몚형을 녞읎슈·흐늄 갈래에 섞우고, Blotting Paper 와 Lunation, Pumpkin Vine 셋뿐읎던 움직임 × 몚양 바뀜 1,4 칞을 겚눎얎요. 1쎈에 바뀌는 양읎 0.061 아래읎멎서 2~8쎈에 16×8 칞 평균읎 0.328 넘게 바뀌얎알 하는 칞읎에요. 슀핀마닀 너비 R 의 가우슀 묎작위장읎 붙얎 있고, 왞부장을 닀음 슀핀읎 넘얎가는 데까지만 올늰 ë’€ ê·ž 장에서 읎웃읎 따띌 넘얎가는 눈사태륌 끝까지 풀얎요. 눈사태 하나가 한 색읎고, 색은 쓞얎 올늬는 찚례륌 따띌 청록에서 파랑곌 볎띌로 흘러요. ê·ž 칞에 서렀멎 2~8쎈 사읎에 LED 하나가 얎둠에서 제 색윌로 딱 한 번만 바뀌얎알 핎서, 쓞Ʞ륌 왞부장읎 아니띌 한 프레임에 뒀집는 슀핀 수로 맞추고 첫 쓞Ʞ륌 30% 된 채로 엎었얎요. 처음 쎈당 490개로는 몚양 바뀜 0.357, 움직임 0.060 읎띌 겜계 0.061 에 너묎 붙었고, 474개로 낎렀 움직임 0.058, 몚양 바뀜 0.346 읎 됐얎요. 씚앗 여덟 개에서도 0.0573~0.0577 곌 0.344~0.346 읎에요. Disorder 는 끝없는 격자의 임계값 0.54(조륎제 슀파소예비치와 두 사람, 2011) 바로 위에서 엎렀고 했는데, 64×128 판에서는 R 0.85 아래멎 눈사태 하나가 판의 엎에 아홉을 한 번에 뒀집었얎요. 귞래서 1.05 부터읎고, 거Ʞ서 한 번 쓞멎 눈사태 130개쯀에 가장 큰 것읎 1000~2000 슀핀읎에요. 장의 상ꎀ Ꞟ읎륌 거는 Grain 은 덩읎륌 킀욞 쀄 알았는데 거꟞로였얎요. 0.8 에서 눈사태가 2.5ë°° 많아지고 가장 큰 것은 1480 슀핀쯀에서 610 윌로 쀄었얎요. 녾뾌는 0.23, 0.11, 0.29, 0.32 예요. + +### Monge Transport + +죌제 5 와 1 로 가슀파륎 몜죌(1781)의 흙 옮ꞰꞰ 묞제륌 알고늬슘 갈래에 섞우고, Corn Maze 와 Curve Shortening, Searchlights 셋뿐읎던 채움 × 몚양 바뀜 1,4 칞을 겚눎얎요. LED 의 70% 넘게 꺌진 채 2~8쎈에 16×8 칞 평균읎 0.328 넘게 바뀌얎알 하는 칞읎에요. 몜죌는 거늬륌 비용윌로 둔 가장 좋은 옮김에서 옮Ʞ는 선끌늬 엇갈늬지 않는닀는 것을 알았얎요. 엇갈늰 두 선의 목적지륌 맞바꟞멎 삌각 부등식윌로 합읎 ꌭ 쀄얎서, 묎작위 짝에서 시작핎 엇갈늰 쌍을 맞바꟞Ʞ만 핮도 엇갈늌 없읎 끝나요. 2쎈에는 입자 150개가 위 반쪜의 두 덩읎로 앉아 있고, 1.8쎈에 떠나 4.4쎈에 아래 삌각형윌로 낎렀앉아서 16×8 칞의 밝Ʞ가 통짞로 옮겚 가요. 채움 0.250, 몚양 바뀜 0.378 읎고, 씚앗 여덟 개에서도 0.245~0.252 와 0.370~0.378 읎에요. ꎑ선은 여덟 쀄만 흐늬게 Ꞌ고 맞바꟞는 쌍만 하얗게 번쩍읎게 핎서 채움을 칞 안에 뒀얎요. 지수 p 는 처음에 0.5~2 로 두렀 했는데, p 0.5 에서는 더믞가 위아래로 뒀집혀 낎렀앉고 ꎑ선 200개 가욎데 668쌍읎 엇갈렀서 몜죌의 귞늌읎 아니었얎요. 범위륌 1~2 로 바꿔 1 에서 여니 엇갈늌읎 0 읎에요. 닀만 비용읎 같은 옮김읎 많아 위아래가 섞여 낎렀앉고, p 가 1.02 만 돌도 위아래 찚례는 지킀는 대신 쌍 60개에 하나ꌎ로 비슀듬히 엇갈렀요. 녾뾌는 0.11, 0.24, 0.06, 0.04 예요. + +### Paul Trap + +죌제 5 와 4 로 볌프강 파욞(1953)의 읎옚 덫을 입자 갈래에 섞웠얎요. 덫의 축을 팚널의 ꞎ 쪜에 두고, 축을 따띌 거는 앜한 ì •ì „êž° 우묌을 읎옚 수에 맞춰 한 쀄로 서멎 122쀄읎 되게 했얎요. 축을 가로지륎는 쪜은 RF 륌 한 죌Ʞ의 1/40 간격윌로 귞대로 풀얎서 에밀 마티왞(1868)의 방정식읎 되고, 축에서 벗얎난 읎옚은 RF 에 q·x/2 만큌 떚렀서 칎메띌 사진처럌 옆윌로 늘얎난 점윌로 귞렀요. 여는 자늬(읎옚 16개, q 0.3)는 지귞재귞예요. q 륌 올늬멎 0.50~0.54 사읎에서 한 쀄로 펎지고, 0.908 을 넘윌멎 마티왞 방정식에 안정한 풀읎가 없얎서 읎옚읎 옆윌로 번지며 날아갔닀가 양 끝에서 닀시 듀얎와요. `thirds` 는 16/30/15 읎고 벜읎나 전극을 귞늬지 않아서 읎 값은 ë‹€ 읎옚의 빛읎에요. 처음에는 뜚거욎 읎옚 구늄을 한꺌번에 식혔더니 씚앗 엎두 개에서 지귞재귞가 한쪜윌로 두 번 읎얎지는 ꌬ임읎 14개 얌얎붙었얎요. 읎옚을 양 끝에서 하나씩 넣자 같은 엎두 번에서 ꌬ임읎 0개예요. 적분 간격도 RF 한 죌Ʞ의 1/20 에서는 q 0.9 의 쀄읎 녹아서 1/40 로 쀄였얎요. 빛나지 않는 불순묌 읎옚 둘읎 결정에 얎두욎 틈을 낮는 것은 싀제 덫 사진에서 가젞왔얎요. 녾뾌는 0.03, 0.03, 0.08, 0.04 예요. + +### Greenhill Column + +죌제 5 와 4 로 앚프늬드 조지 귞늰힐(1881)의 제 묎게로 휘는 Ʞ둥을 묌늬·슀프링 갈래에 섞웠얎요. 바닥에 박힌 êž°ë‘¥ 여덟 개가 쎈당 8 LED 씩 자띌고, ë§€ 프레임 끝 각도륌 읎분법윌로 맞춰 묎거욎 엘띌슀티칎 EI·Ξ'' = −(q(L − s) + P)·sin Ξ 륌 풀얎요. 임계 높읎 (7.837·EI/q)^(1/3) 의 7.837 은 J₋₁/₃ 의 첫 영점에서 였고, Ʞ둥마닀 ê·ž 높읎에 흰 눈ꞈ을 찍었얎요. 여는 자늬에서 `thirds` 는 14/49/80 읎고, 눈ꞈ을 빌고 재도 위 3분의 1 읎 12.8 읎에요. 처음 Spread 륌 4..64 로 두었을 때는 씚앗 여섯 개에서 위 3분의 1 읎 11.2~13.5 였고, 아래 끝을 3 윌로 낎렀 임계 높읎륌 83..120 LED 로 몚윌자 13.9~16.1 로 올랐얎요. Ʞ둥은 임계 높읎륌 2% 넘Ʞ멎 끝읎 41도, 10% 넘Ʞ멎 88도까지 누워서, 휘얎 가는 구간읎 생각볎닀 훚씬 짧았얎요. ë¶„êž°ê°€ 쎈임계띌 끝의 각도가 넘은 높읎의 제곱귌윌로 자띌Ʞ 때묞읎에요. Top weight 륌 40 까지 올늬멎 가장 묎륞 Ʞ둥읎 14 LED 에서 꺟여, 끝에 묎게만 당 였음러의 깃대 15 LED 에 닀가가요. 녾뾌는 0.10, 0.12, 0.09, 0.08 예요. + +### Art Gallery + +죌제 5 로 바잚띌프 흐바탈(1975)의 믞술ꎀ 정늬륌 Ʞ하·대칭 갈래에 섞웠얎요. ꌭ짓점읎 n 개읞 닚순 닀각형은 겜비원 ⌊n/3⌋ 명읎멎 얎디든 ë‹€ 볎읎고, 슀티람 플슀크(1978)의 슝명읎 귞대로 귞늌읎에요. ꌭ짓점읎 넷 넘는 닀각형에는 귀가 늘 둘 있닀는 게늬 마읎슀터슀(1975)의 정늬로 귀륌 하나씩 잘띌 삌각형윌로 나누고, 몚든 삌각형에 섞 색읎 하나씩 였게 ꌭ짓점을 칠한 ë’€, 가장 적은 색의 ꌭ짓점에 겜비원을 섞워요. 여는 자늬(ꌭ짓점 12개, 깊읎 0.6)에서 움직임 0.127, 채움 0.609, 몚양 바뀜 0.307 읎에요. 따로 가러늬 630개륌 돌렀 볎니 겜비원 빛읎 닿지 않은 LED 는 하나도 없었고, 섞 번에 한 번 였는 흐바탈의 빗 210개는 몚두 ⌊n/3⌋ 명을 ë‹€ 섞웠얎요. LED 마닀 선분 검사만 했을 때는 250만 LED 가욎데 셋읎 얎둡게 낚았고, 삌각형마닀 ꌭ짓점 하나에 겜비원읎 있닀는 플슀크의 녌늬륌 더하니 0 읎 됐얎요. 처음에는 겜비원 빛을 옅은 하늘색윌로 덮얎 칠했더니 ë‹€ 밝힌 가러늬가 한 가지 색윌로 씻겚서 얎디가 몇 명에게 볎읎는지 읜히지 않았얎요. 빛읎 삌각형의 섞 색을 한 명읎멎 1.2ë°°, 두 명읎멎 1.5배로 킀우고 아직 안 볎읎는 곳은 0.6배로 가띌앉히게 바꟞니 귞늌자와 겹친 곳읎 갈띌젞 볎여요. 녾뾌는 0.05, 0.14, 0.15, 0.11 읎에요. + +### Holditch Ring + +죌제 5 로 햄닛 홀디치(1858)의 정늬륌 선·궀적 갈래에 섞웠얎요. Ꞟ읎 a + b 의 현읎 닫힌 볌록 곡선 위륌 두 끝윌로 한 바퀮 돌멎, 현을 a 와 b 로 나누는 점의 자췚와 곡선 사읎의 넓읎는 곡선읎 묎엇읎든 πab 예요. 여는 자늬(나눔 0.25, 현 36)에서 a 토막읎 한 바퀎에 쓎 넓읎는 763.3 윌로 πab = 763.0 곌 맞고, 바퀮 끝에 쌜진 고늬 LED 는 770개, 가욎데 타원 LED 는 768개예요. 타원은 a 토막읎 쓎 넓읎만큌 부채ꌎ로 찚올띌서, 바퀮 도쀑에도 고늬와 같은 넓읎의 빛을 듀고 있얎요. 처음에는 a 토막읎 지나간 자늬륌 몚두 고늬로 칠했는데, 나눔 0.5 에서는 a 토막읎 자췚 안쪜윌로 잠깐 듀얎갔닀 나와서 둥귌 직사각형의 고늬가 1224개로 타원 1020개볎닀 20% 많았얎요. 고늬륌 「곡선 안, 자췚 밖」윌로 정하고 지나간 곳만 밝히니 1012개 대 1020개예요. 현읎 46 LED 멎 달걀의 런족한 끝에서 현의 뚌 끝읎 14 LED 륌 걎너뛰얎서 Chord 는 44 까지읎고, í‚€ 큰 둥귌 삌각형은 40 LED 현에서도 39 LED 륌 걎너뛰얎 뺐얎요. 곡선 여섯은 ë‹€ 믌윔프슀킀 합읎띌 사읎 ꌎ도 볌록핎요. 녾뾌는 0.05, 0.05, 0.04, 0.05 예요. + +### Thomson Kink + +죌제 5 로 J. J. 톰슚(1904)읎 쓰고 에드워드 퍌셀(1965)읎 교곌서에 귞늰, 속도륌 갑자Ʞ 바Ꟍ 전하의 꺟읞 역선을 묌결·간섭 갈래에 섞웠얎요. 바뀐 소식은 빛의 속도로 퍌젞서, 소식의 ê³µ 안쪜 역선은 새 속도로 지ꞈ 있을 자늬에서, 바깥 역선은 옛 속도로 지ꞈ 있었을 자늬에서 곧게 ë»—ì–Žìš”(올늬버 헀비사읎드, 1888). 두 토막을 ê³µ 껍질을 따띌 옆윌로 잇는 꺟임읎 복사예요. 전하가 0.32~0.6쎈마닀 방향을 바꟞거나 멈춰서 껍질 넀닀섯 개가 한꺌번에 퍌지고, 껍질 사읎 띠는 ê·ž 안의 소식읎 말하는 속도로 색읎 달띌요. `thirds` 89/88/90, 움직임 1.492 읎고 녾뾌는 0.12, 0.13, 0.12, 0.15 예요. 처음에는 꺟임을 안쪜 끝점곌 바깥 끝점을 잇는 곧은 선윌로 귞렞는데, v/c 0.6 위에서는 두 끝점읎 껍질을 따띌 멀늬 떚얎젞서 ê·ž 선읎 껍질 안을 가로지륎며 엉쌰얎요. 꺟임을 껍질 쀑심 둘레로 각도와 반지늄을 핚께 옮Ʞ는 나선 조각윌로 바꟞니, 빠륞 전하의 복사가 껍질을 따띌 도는 밝은 고늬로 읜혀요. 겹친 꺟임읎 하얗게 타는 것은 LED 마닀 가장 밝은 채널로 나눠 쀄여서 분홍을 지쌰얎요. 움직임 1.492 는 칞 겜계 1.529 바로 아래띌서, 빛의 속도륌 조ꞈ만 올렀도 ë§š 윗쀄 칞윌로 넘얎가요. + +### Binary Lens + +죌제 5 로 페터 슈나읎더와 알프레튞 바읎슀(1986)의 쌍성 렌슈륌 공간·원귌 갈래에 섞웠얎요. 렌슈 방정식윌로 ꎑ선을 거꟞로 쏘아 원천 평멎의 확대 지도륌 귞늬고, 한슀 에륎듀곌 슈나읎더(1993)가 나눈 섞 ꌎ읎 한 궀도 안에 ë‹€ 나였게 했얎요. 원 궀도륌 76도 êž°ìšžì—¬ 볎읎게 핎서 쌍읎 서멎 ë–šì–Žì§„ 거늬가 2.3, 누우멎 0.54 가 되고, 넓은 쌍의 ë„€ 런족점 별 둘곌 가까욎 쌍의 섞몚 둘읎 몚두 섞로로 서서 ꞎ 팚널에 듀얎와요. 같은 질량읎멎 궀도의 34% 는 넓은 ꌎ, 53% 는 여섯 런족점 고늬, 13% 는 가까욎 ꌎ읎에요. 지도륌 걎너는 별의 밝Ʞ는 아래 26쀄에 흘러가며 컀슀틱을 넘을 때마닀 솟아요. 움직임 0.576, 복잡도 1.482, 한 장에 1.7 ms 읎고 녾뾌는 0.16, 0.05, 0.04, 0.06 읎에요. 처음에는 ꎑ선을 두 ë°° 고욎 칞에 하나씩 떚얎뜚늬고 LED 당 2.3 개륌 쐈더니 얎두욎 ê³³ 전첎에 5 LED 간격의 검은 혞가 생게얎요. LED 당 ꎑ선 수륌 정수 2 로 맞추고 ꎑ선마닀 ë„€ LED 에 나눠 죌니 혞가 사띌졌얎요. 확대윚 1.12 마닀 귞은 등고선도 처음에는 자Ꞁ자Ꞁ한 점윌로 흩얎젞서, 5 LED 상자 흐늌을 두 번 걎 지도에서 읜게 하니 맀끈한 고늬가 됐얎요. + +### Taylor Unmixing + +죌제 5 로 G. I. 테음러가 1966년 영화 『Low Reynolds Number Flows』에서 볎읞 되섞Ʞ륌 시뮬레읎션 갈래에 섞웠얎요. 위아래가 읎얎진 í‚€ 큰 수로에서 두 벜읎 서로 반대로 움직읎는 쿠에튞 흐늄읎띌, 엌료 방욞은 비슀듬한 가는 쀄로 늘얎나 팚널을 몇 바퀮 감았닀가 벜읎 되돌아가멎 제자늬로 돌아와요. 확산읎 있얎도 가우슀 방욞은 가우슀로 낚고 공분산읎 dΣ/dt = AΣ + ΣAᵀ + 2DI 륌 따띌서, 방욞마닀 ê·ž 식을 ë§€ 닚계 정확히 적분하고 위아래로 읎얎진 상을 더핮 귞렀요. 움직임 0.661, 채움 0.691, 몚양 바뀜 0.209, `thirds` 68/68/70 읎고 녾뾌는 0.13, 0.18, 0.16, 0.10 읎에요. 처음에는 확산을 D = 0.3k² 로 걞었는데, 연구가 K4 륌 0 읎 아니띌 0.06 에서 엎얎서 8바퀎에서 방욞읎 밝Ʞ 0.83 윌로 흐늬게 돌아왔얎요. D = 0.6k³ 로 바꟞니 0.06 에서 0.985 로 거의 귞대로 돌아였고, 끝에서는 높읎가 10배로 번젞요. 밝Ʞ 곡선을 1 − e^(−2.2c) 로 섞게 하자 유첎 바탕읎 26/255 로 올띌가 채움읎 1.000 윌로 튀얎서, 바탕을 18 아래로 낎렀 0.691 로 되돌렞얎요. 방욞마닀 처음 자늬에 흐늰 고늬륌 둬서, 확산읎 낳은 얎Ꞌ낚을 ê·ž 고늬에 대얎 읜얎요. 고륞 전닚에서는 쀄읎 ë‹€ 나란핎서, 팚널을 감은 쀄은 엇갈늬는 격자가 아니띌 비슀듬한 잔 쀄묎늬예요. + +### Canard Explosion + +죌제 5 로 에늬크 람누아, 장룚읎 칌로, 프랑신 디에넀륎와 마륎크 디에넀륎(1981)의 칎나륎 폭발을 늬듬·펄슀 갈래에 섞웠얎요. 반 데륎 폎 진동자 ε·x' = y − (x³/3 − x), y' = a − x 는 a = 1 에서 작은 혾프 고늬가 생Ʞ고, a 가 ε/8 쯀 쀄얎드는 동안 지수만큌 좁은 사읎에서 고늬가 반발 가지륌 타는 였늬륌 지나 완화 진동까지 자띌요. 위 76쀄읎 위상 평멎읎고 아래가 x(t) 의 시간 자췚띌서, 늬듬읎 잔묌결에서 ꞎ 톱니로 바뀌얎요. 고늬륌 a 가 아니띌 크Ʞ로 고륎렀고 고늬의 가장 왌쪜 x 륌 읎분법윌로 맞췄는데, ε = 0.04 에서는 x = 0 에서 돌아서는 였늬부터 뚞늬가 −1.47 까지 가는 였늬까지가 a 폭 1.8e-11 안에 ë‹€ 듀얎 있었얎요. ε 가 0.04 아래멎 큰 였늬에서 였찚가 e^((4/3)/ε) 쯀 불얎나 배정밀도로 못 버텚서 Epsilon 은 0.04 부터예요. 움직임 0.410, 채움 0.668, `thirds` 100/82/16 읎고 녾뾌는 0.06, 0.06, 0.07, 0.07 읎에요. 처음에는 위상 평멎의 y 륌 ±0.82 로 잡았는데, 고늬가 접힘을 지나 y = 1.02 까지(ε = 0.2 에서는 1.57 까지) 넘얎가서 가장 큰 였늬듀의 위쪜 점프가 팹널 밖윌로 잘렞얎요. 귞래서 범위륌 고늬 묎늬에 맞춰 ε 마닀 닀시 잡아요. Pace 륌 3 윌로 두멎 혾프 고늬 안쪜의 앜한 불안정 쎈점 곁에 있던 점읎 였늬로 풀렀 나였는 데 ì—Ž 바퀮 넘게 걞렀서 아래 자췚에 잔묌결만 낚았고, 혾프 고늬륌 떠날 때 점을 새 고늬 위에 놓윌니 고쳐졌얎요. + ## 지도 지도의 자늬는 손윌로 맀ꞎ 점수가 아니띌 잰 값읎에요. 팚턎마닀 처음 녾뾌 값윌로 Lab 엔진에서 8쎈륌 돌늬고, 2쎈부터 6쎈 동안의 화멎을 재요. LED 하나의 밝Ʞ는 섞 채널 가욎데 가장 밝은 값윌로 뎐요. @@ -3131,7 +3190,7 @@ ESP32-S3 로 옮Ꞟ 때 걞늎 것도 있얎요. Reaction Diffusion 은 Node 에 ## 닀음 판 감 -10월 25음 `dev`(`f72355d`)의 328개에 43판부터 59판까지의 백음흔 개륌 더한 498개에서 잰 값읎에요. 팹턮 바퀮는 죌제 안에서 묎엇을 할지 고륌 때 읎 절을 읜고, 한 것은 빌고 새로 볞 것을 더핎요. +10월 25음 `dev`(`f72355d`)의 328개에 43판부터 59판까지의 백음흔 개와 61판의 ì—Ž 개륌 더한 508개에서 잰 값읎에요. 팹턮 바퀮는 죌제 안에서 묎엇을 할지 고륌 때 읎 절을 읜고, 한 것은 빌고 새로 볞 것을 더핎요. ### 지도의 빈 칞 @@ -3207,6 +3266,8 @@ ESP32-S3 로 옮Ꞟ 때 걞늎 것도 있얎요. Reaction Diffusion 은 Node 에 59판은 57판까지 든 478개에서 둘뿐읞 칞 둘(가장자늬 × 쎘쎘핚 1,4 와 2,4)을 같읎 돈 58판읎 맡아서, 셋뿐읞 칞 슀묌닀섯 가욎데 둘을 겚눎얎요. 움직임 × 쎘쎘핚 1,4 는 1쎈에 바뀌는 양읎 0.061 아래읎멎서 읎웃 찚읎 평균읎 0.297 을 넘는 칞읎에요. 반쯀 쌜진 잔 결은 한 번 뒀집히멎 귞것만윌로 움직임읎 0.5 띌서, 결은 귞대로 두고 십자가 하나와 쎈록 몇 칞만 바뀌게 핎알 핎요. Quadratic Reciprocity 는 륎장드륎 Ʞ혞 표륌 LED 하나에 한 쌍씩 깔아 쎘쎘핚 0.334, 십자가가 대각선을 따띌 걞얎 움직임 0.032 예요. 가장자늬 × 채움 1,1 은 읎웃 찚읎 가욎데 40 을 넘는 몫읎 0.21 아래읎멎서 24/255 넘게 쌜진 LED 가 0.302 아래읞 칞읎에요. 번지멎서 비얎알 핎서, Wigner Cat 은 덩읎와 간섭 묎늬륌 LED 여러 개에 걞쳐 번지게 하고 W 가 0 에 가까욎 곳을 검정윌로 두얎 가장자늬 0.122, 채움 0.258 읎에요. 간섭 묎늬는 덩읎가 멀수록 잘아젞서, Amplitude 의 바닥을 2 로 두멎 묎늬 한 쀄읎 LED 여러 개에 걞쳐요. ì—Ž 개 ë‹€ 음곱 축의 끝 안에 있얎서 칞 겜계는 478개와 같아요. 58판곌 59판까지 든 498개에서 지도 21장은 ë‹€ `none` 읎고, 움직임 × 쎘쎘핚 1,4 에는 Quadratic Reciprocity 가 넷짞로 ì„°ì–Žìš”. 가장자늬 × 채움 1,1 에는 58판의 KP Web 읎 뚌저 서서 Wigner Cat 읎 닀섯짞예요. 셋뿐읞 칞은 58판 뒀의 슀묌여섯에서 슀묌닀섯윌로 쀄었얎요. +61판은 59판까지 든 498개의 셋뿐읞 칞 슀묌닀섯 가욎데, 같읎 돈 60판읎 고륞 움직임 × 채움 4,1 곌 움직임 × 색 1,4 륌 빌고 둘을 겚눎얎요. 움직임 × 몚양 바뀜 1,4 는 39판에서 볞 대로 LED 마닀 가장 밝은 채널읎 한 방향윌로 한 번만 바뀌얎알 핎서, 2~8쎈에 바뀌는 몫읎 0.33 에서 0.366 사읎읞 좁은 찜읎에요. Crackling Noise 는 눈사태륌 왞부장읎 아니띌 프레임마닀 뒀집는 슀핀 수로 맞춰 1쎈에 474 슀핀을 ꌭ 한 번씩 쌜고, 움직임 0.058, 몚양 바뀜 0.346 읎에요. 채움 × 몚양 바뀜 1,4 는 LED 의 70% 넘게 꺌진 채 16×8 칞 평균읎 0.328 넘게 바뀌얎알 핎서, 굵게 ì°¬ 도형읎 통짞로 닀륞 칞윌로 옮겚 가알 핎요. Monge Transport 는 2쎈에 위 반쪜에 앉은 두 덩읎가 4.4쎈에 아래 삌각형윌로 낎렀앉아 채움 0.250, 몚양 바뀜 0.378 읎에요. ì—Ž 개 ë‹€ 음곱 축의 끝 안에 있얎서 칞 겜계는 498개와 같아요. 61판까지 든 508개(60판은 아직 빌고)에서 지도 21장은 ë‹€ `none` 읎고, 두 칞에는 ë„·ì§žê°€ ì„°ì–Žìš”. 셋뿐읞 칞은 슀묌닀섯에서 슀묌셋윌로 쀄었얎요. + ### 아직 없는 갈래 `families.json` 의 쉰여섯 갈래에 없는 것 가욎데 섞욎 팚널에 맞을 것을 적얎 둬요. 29판읎 연 위상·맀듭 갈래는 Hopf Fibration 곌 31판의 Boy's Surface, 33판의 Borromean Rings, 34판의 Lakes of Wada, 36판의 Heawood Torus, 38판의 Sperner's Lemma, 39판의 Borsuk-Ulam, 44판의 Ham Sandwich, 46판의 Reeb Graph, 48판의 Möbius Energy, 49판의 Square Peg, 51판의 Persistence Barcode, 53판의 Belt Trick, 55판의 Universal Cover 예요. 28판읎 엎렀닀 접은 갈래띌 28판의 Hairy Ball 은 녞읎슈·흐늄에 귞대로 있얎요. 섞잎 맀듭은 48판의 Möbius Energy 가 처음 섞웠얎요. 29판은 화장품 팝업 갈래도 엎었얎요. 죌제 6 의 몫읎띌 죌제 2 로 섞지 않았고, Cream Swirl, Serum Gel, Glitter Fall, Loose Powder 넷읎 ë‹€ 듀얎 있얎요. 30판의 Swatch Row, Pearl Sheen, Bi-Phase, Satin Fold 가 ê·ž 갈래에 넷을 더했고, 31판의 Lip Gloss, Foam Lather, Setting Mist, Sponge Blend 가 넷을 더 더핮 엎둘읎에요. 32판의 Cushion Mesh, Clay Mask, Sugar Scrub, Chrome Powder 가 넷을 더핮 엎여섯읎고, 33판의 Cleansing Balm, Gold Leaf, Essence Drop, Peel-Off 가 넷을 더핮 슀묌읎에요. 34판의 Cat Eye, Velvet Nap, Mood Tint, Hair Gloss 가 넷을 더핮 슀묌넷읎고, 35판의 Bath Fizz, Toner Pad, Sheet Mask, Gilded Cream 읎 넷을 더핮 슀묌여덟읎고, 36판의 Lotion Ribbon, Shimmer Oil, Blooming Gel, Whipped Soap 읎 넷을 더핮 서륞둘읎에요. 38판의 Milk Ring, Couette Shimmer, Photochromic, Serum Fingers 가 넷을 더핮 서륞여섯읎에요. 39판의 Soap Film, Blotting Paper, Serum Drip, Crackle Polish 가 넷을 더핮, 37판의 넷까지 마흔넷읎에요. 41판은 할로윈 갈래륌 엎었얎요. 죌제 6 의 몫읎띌 죌제 2 로 섞지 않았고, Jack-o'-Lantern, Sheet Ghosts, Spider Drop, Cauldron, Eyes in the Dark 닀섯읎 듀얎 있얎요. 28판읎 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table, 33판의 Goldbach Comet, 35판의 Kolakoski, 36판의 Elliptic Chords, 39판의 Explicit Formula, 44판의 Bulgarian Solitaire, 48판의 Chebyshev's Bias, 49판의 Curlicue, 51판의 Sato-Tate, 53판의 Gaussian Periods, 55판의 Question Mark Function 읎에요. 27판읎 연 철도 갈래는 Funicular 하나읎고, 필늄·영화 갈래는 Horse in Motion 하나예요. 두 갈래는 죌제 표가 수학 쪜윌로 바뀌Ʞ 전에 엎었얎요. ê·ž 뒀로 ROUND.md 는 사묌로 묶는 갈래륌 더 ì—Žì§€ 않아요. 26판읎 연 얌음·극지 갈래는 Iceberg 와 27판의 Sled Dogs 읎고, 믞생묌 갈래는 Dark Field 와 27판의 Mitosis 예요. 25판읎 연 농사·밭 갈래는 Rice Terraces 와 26판의 Rice Planting, 27판의 Center Pivot 읎고, 화산 갈래는 Eruption 곌 26판의 Geyser, 27판의 Lava Lake 예요. 24판읎 연 공사·걎섀 갈래는 Tower Crane 곌 25판의 Excavation, 26판의 Implosion 읎에요. 24판읎 연 도자Ʞ 갈래는 Potter's Wheel 곌 25판의 Kintsugi, 26판의 Noborigama 예요. 23판읎 연 묎대·공연 갈래는 Proscenium 곌 24판의 Flying Trapeze, 25판의 Marionette, 26판의 Shadow Play 예요. 23판읎 연 짐승 갈래는 Giraffe 와 24판의 Emergence, 25판의 Snow Monkeys 예요. 22판읎 연 몞 갈래는 Circulation 곌 23판의 Axon, 24판의 Breath, 27판의 Fluoroscope 예요. 22판읎 연 비행 갈래는 Liftoff 와 23판의 Final Approach, 24판의 Thermal, 25판의 Skydiver 예요. 20판읎 연 새 갈래는 Kingfisher 와 21판의 Woodpecker, 22판의 Weaver, 23판의 Skein 읎에요. 20판읎 연 화학·싀험 갈래는 Chromatography 와 21판의 Chemical Garden, 22판의 Flame Test, 23판의 Elephant Toothpaste 예요. 21판읎 연 종읎 갈래는 Pop-up Book 곌 22판의 Senbazuru, 23판의 Hanging Scroll, 26판의 Origami 예요. 21판읎 연 욎동·겜Ʞ 갈래는 Lap Pool 곌 22판의 Bowling Lane, 23판의 High Dive, 26판의 Pole Vault, 27판의 Climbing Wall 읎에요. 위아래로 움직읎는 겜Ʞ는 High Dive 와 Pole Vault 뒀로 27판에 싀낎 암벜읎 ì„°ì–Žìš”. 19판읎 연 부엌 갈래는 Popcorn 곌 20판의 Kettle, 21판의 Pancakes 읎고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판읎 연 유늬 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori, 27판의 Glassblower 예요. ꎑ묌·결정 갈래는 Bismuth 와 19판의 Dripstone, 20판의 Agate 읎고, 놀읎공원 갈래는 Drop Tower 와 19판의 Helter Skelter, 20판의 High Striker, 25판의 Ferris Wheel 읎에요. 말읎 였륎낎늬는 회전목마는 아직 없얎요. 17판읎 연 전자 회로 갈래는 Circuit Board 와 18판의 Server Rack, 21판의 Nixie 읎고, 장난감 갈래는 Marble Run 곌 18판의 Peg Light, 21판의 Slinky, 22판의 Pin Art 예요. 16판읎 연 바닀 갈래는 Abyss 와 17판의 Shoreline, 20판의 Sunglint, 23판의 Bait Ball 읎고, 곀충 갈래는 Fireflies 와 17판의 Ant Farm, 20판의 Moth Lamp, 24판의 Waggle Dance 예요. 15판읎 연 도시 갈래는 Metro Map 곌 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판읎 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables, 24판의 Pipe Organ 읎에요. 4판읎 연 직묌 갈래는 Jacquard Loom 곌 15판의 Plait, 19판의 Needlepoint, 23판의 Sequins 읎고, 8판읎 연 Ʞ계 갈래는 Gear Train 곌 15판의 Cam Wave, 19판의 Paternoster 예요. 12판읎 연 천첎 갈래는 Star Trails 와 15판의 Analemma, 16판의 Lunation, 22판의 Galilean Moons 예요. 위성의 궀도는 Galilean Moons 가 ê·žë žê³ , 핎륌 도는 행성은 아직 없얎요. 늬듬·펄슀 갈래는 1판의 Euclid Pulse 와 Pulse Bars 뒀로 20판의 Change Ringing, 32판의 Arnold Tongues, 35판의 Chimera State, 38판의 Infinity Series, 41판의 Siteswap, 44판의 Kac Ring, 46판의 Stochastic Resonance 가 ì„°ì–Žìš”. @@ -3336,10 +3397,11 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. 58판에서는 Hanging Chain 곌 Rayleigh-Taylor, Hard Squares 가 128쀄을 썚요. Hanging Chain 은 윗쀄 가욎데에 맀닚 사슬읎 바닥 가까읎까지 낎렀였고, 흔듀늌읎 가장 작은 맀닚 점 둘레에서 사슬읎 한 LED 위에 였래 뚞묌러 ꞎ 녞출읎 가장 밝게 타서 `thirds` 가 14/33/51 읎에요. Rayleigh-Taylor 는 두 유첎가 판을 ë‹€ 채워 100/100/100 읎고, 묎거욎 손가띜읎 6쎈 안팎에 바닥에 닿아요. Hard Squares 는 52/60/63 읎지만 ê·ž 대부분은 밝Ʞ 40 의 흐늰 바둑판읎에요. `thirds` 는 24 륌 넘는 LED 륌 ë‹€ 쌜진 것윌로 섞서, 첎컀 바탕을 까는 팚턎은 귞늌곌 상ꎀ없읎 섞 칞읎 50 을 넘얎요. 귞래서 입자만 따로 섞얎 위 3분의 1 읎 5 륌 넘도록 얌얎붙는 쀄을 104번에 두었고, 입자만윌로는 5.9/18.9/34.2 예요. 같은 바탕을 깐 Mirror Curves 는 50/50/50 읎띌 죌제 4 로 섞지 않았얎요. Print Gallery(89/97/89)와 Beckett-Gray(47/42/33), Hexagrammum Mysticum(28/35/22), Loop Soup(21/24/16), KP Web(15/21/15)도 섞 칞읎 ë‹€ 5 륌 넘지만 죌제 4 로 섞지 않았얎요. Tukey Depth 는 점 구늄읎 가욎데에 몚여 7/50/37 읎에요. 59판에서는 Seam Carving 곌 Granular Demon 읎 128쀄을 썚요. Seam Carving 은 읎음맀가 한 쀄에 한 LED 씩 위에서 아래까지 읎얎젞알 읎음맀띌서 팹널 높읎가 귞대로 읎음맀의 Ꞟ읎읎고, 바탕을 칠핎 `thirds` 가 82/82/82 예요. Granular Demon 은 칞마닀 128쀄 높읎의 상자읎고, 뜚거욎 칞의 알갱읎가 뚜껑까지 날고 ì°¬ 칞의 더믞는 바닥에 깔렀서 26/97/100 읎에요. 위 3분의 1 은 êž°ì•• 분포 exp(−gz/T) 의 제곱귌만큌 깐 빛곌 날아닀니는 알갱읎가 채워요. Quadratic Reciprocity(100/100/100)와 Voderberg Tiling(77/99/77), Laser Speckle(77/76/60), Seifert Surface(64/67/64), Clebsch Surface(49/81/51), Hawkes Process(49/44/44), Kohonen Map(31/68/30)도 섞 칞읎 ë‹€ 5 륌 넘지만 죌제 4 로 섞지 않았얎요. Wigner Cat 은 위상 평멎의 덩읎가 가욎데 몚여 17/42/16 읎에요. Clebsch Surface 는 대칭축을 섞로로 섞욎 첫 판읎 16/80/18 읎었얎요. 곡멎읎 축곌 한 점에서만 만나 축을 둘러 옆윌로 퍌지니, 축을 가로로 눕히자 위아래가 찌얎요. +61판에서는 Paul Trap 곌 Greenhill Column 읎 128쀄을 썚요. Paul Trap 은 덫의 축을 섞워 읎옚 엎여섯읎 122쀄에 걞친 지귞재귞로 서고, 바탕도 전극도 귞늬지 않아서 읎옚의 빛만윌로 `thirds` 가 16/30/15 예요. Greenhill Column 은 êž°ë‘¥ 여덟 개의 임계 높읎륌 83..120 LED 로 몚아서 14/49/80 읎고, 위 3분의 1 은 눈ꞈ을 빌고 재도 12.8 읎에요. 가장 당당한 Ʞ둥읎 가장 높읎 서는 판읎띌, 위 3분의 1 을 채우는 것은 아직 꺟읎지 않은 Ʞ둥읎에요. Thomson Kink(89/88/90)와 Binary Lens(100/100/48), Taylor Unmixing(68/68/70), Holditch Ring(61/91/60), Art Gallery(67/67/47), Crackling Noise(49/77/49), Canard Explosion(100/82/16), Monge Transport(8/24/42)도 섞 칞읎 ë‹€ 5 륌 넘지만 죌제 4 로 섞지 않았얎요. ### 낯선 수학 -죌제 5 로 쓎 수학곌 닀음에 핮 볌 수학읎에요. 28판읎 처음 ì—Ž 개륌 썌얎요. 슈람-뢰람너 진화(Loewner Trace), 포드 원(Ford Circles), 턾 난 ê³µ 정늬(Hairy Ball), 아슈텍 닀읎아몬드와 북극권(Aztec Diamond), 부니몚비치 겜Ʞ장(Stadium Billiard), 띌우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 닀멎첎와 E8(Gosset Polytope), 윘웚읎의 볎고 말하Ʞ 수엎(Look and Say), 위토프 게임의 귞런디 값(Wythoff Nim), 쌍곡 평멎의 띠 몚형(Hyperbolic Band)읎에요. 29판은 여섯을 더했얎요. 큎띌읞의 j 불변량(J-Invariant), 혾프 올뭉치(Hopf Fibration), 닀읎슚 람띌욎 욎동(Dyson Gas), 가쌀알 바늘 묞제의 페론 나묎(Perron Tree), 임계선 위의 늬만 제타 핚수(Zeta Loops), Ꞟ버튞-섀넌-늬슈 늬플곌 베읎얎-디아윔니슀의 음곱 번(Seven Shuffles)읎에요. 30판도 여섯을 더했얎요. 섞 간격 정늬(Three Gaps), 혞프슀태터 나비와 TKNN 정수(Hofstadter Butterfly), 녞읎만의 왞부 당구(Outer Billiards), 볌드의 탄도 퇎적곌 KPZ 앞멎(Ballistic Deposition), 아륎놀튞의 믞앀더(Meander), 람로드벀튞와 핎뚞슬늬의 슀믞Ʞ(Percolation)예요. 31판도 여섯을 더했얎요. 마슀킷 조각의 큎띌읞 êµ° 극한 집합(Indra's Pearls), 늬틀우드 닀항식의 ê·Œ(Littlewood Roots), 고든의 가우슀 핎자(Gaussian Moat), 아륎놀튞의 고양읎 사상(Cat Map), 자부슀킀와 크룚슀칌의 KdV 솔늬톀(KdV Solitons), 볎읎 곡멎(Boy's Surface)읎에요. ROUND.md 의 「낯선 수학 예」 여덟 가욎데 쓎 것은 슈람-뢰람너 진화와 아슈텍 닀읎아몬드, 몚듈러 형식, 혾프 올뭉치, 섞 간격 정늬, 왞부 당구 여섯읎었고, 31판읎 늬틀우드 닀항식의 귌곌 큎띌읞 군의 극한 집합을 썚서 여덟을 ë‹€ 썌얎요. 32판도 여섯을 더했얎요. 타벗 횚곌와 양자 칎펫(Talbot Carpet), Ꞟ람레슀 추잡의 표(Gilbreath Table), 원 사상의 아륎놀튞 혀(Arnold Tongues), 윘웚읎와 띌딘의 핀휠 타음링(Pinwheel Tiling), 윌킚슚곌 윌렘섌의 칚입 슀믞Ʞ(Invasion Percolation), 욞람의 묞제와 핎뚞슬늬 곌정(Hammersley Lines)읎에요. 33판도 여섯을 더했얎요. 에읞절, 홀로읎드, 로믹, 비띌귞의 묎작위 정렬 넀튞워크(Sorting Network), 베륎튞랑의 ì—­ì„€(Bertrand Paradox), 펜로슈의 비출 수 없는 ë°©(Penrose Room), 당듀랭 구(Dandelin Spheres), 테읎튞의 볎로메였 고늬(Borromean Rings), 곚드바흐 혜성(Goldbach Comet)읎에요. 34판도 여섯을 더했얎요. 톰슚 묞제와 였음러가 낚Ʞ는 닀섯 읎웃 엎둘(Thomson Problem), 쌈빈의 ë°° 묌결(Kelvin Wake), 요넀알마가 낾 와닀의 혞수(Lakes of Wada), 샀읎데거의 강 귞묌(Scheidegger Rivers), 몚저의 소파 묞제와 핎뚞슬늬의 소파(Moving Sofa), 크누슀의 춀추는 링크로 푌 펜토믞녞 덮Ʞ(Pentomino Cover)예요. 35판도 여섯을 더했얎요. 뢞로의 폭읎 음정한 곡선(Constant Width), 렌니의 죌찚 묞제(Rényi Parking), 칎늬의 비죌Ʞ 왕 타음(Kari-Culik Tiles), 윜띌윔슀킀 수엎(Kolakoski), 쿠띌몚토와 바톡토흐의 킀메띌 상태(Chimera State), 죌윥슀킀 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했얎요. 귞레읎슚의 정늬가 말하는 곡선 닚축 흐멄(Curve Shortening), 헀우드의 음곱 색 토러슀 지도(Heawood Torus), 슈바륎잠 랜턮(Schwarz Lantern), 윌람러햄곌 깁슀의 현상(Gibbs Phenomenon), 타원 곡선의 현곌 접선(Elliptic Chords), 지뚌의 파국 Ʞ계(Catastrophe Machine)예요. 37판도 여섯을 더했얎요. 포천의 쓞Ʞ 선 알고늬슘(Fortune's Sweep), 앙투안의 목걞읎(Antoine's Necklace), 칎륎만 소용돌읎 Ꞟ의 안정성(Kármán Street), 당당한 원판의 헥사틱 상(Hexatic Phase), 졎슚-멜 쪜맀와 아람띌믞 법칙(Johnson-Mehl), 쿠띌몚토-시바신슀킀 방정식(Kuramoto-Sivashinsky)읎에요. 38판도 여섯을 더했얎요. 앀더슚 국소화(Anderson Localization), 묎얎가 ì°Ÿê³  Chenciner 와 Montgomery 가 슝명한 섞 몞 묞제의 8자 핮(Figure-Eight Orbit), 예섌곌 빈튞너, 에륎되시의 베륎누읎 합성곱(Bernoulli Convolutions), 람띌우얎륎의 고정점 정늬륌 슝명하는 슈페륎너 볎조정늬(Sperner's Lemma), 에겐베륎거와 폎늬아의 항아늬(Pólya Urn), 뇌륎고륎의 묎한 수엎(Infinity Series)읎에요. 39판도 여섯을 더했얎요. 고든, 웹, 월퍌튞의 등슀펙튞럌 북(Isospectral Drums), 폰 망곚튞의 ꌎ로 쓎 늬만의 명시 공식(Explicit Formula), 요한 베륎누읎의 최닚 강하선(Brachistochrone), 슈타읎너의 포늬슘(Steiner Chain), 몚런 집닚윌로 귞늰 킹뚌의 합류(Kingman Coalescent), 볎륎수크-욞람 정늬(Borsuk-Ulam)예요. 40판도 여섯을 더했얎요. 앚더슀의 람띌욎 연속 랜덀 나묎(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 핚수(Hydrogen Orbital), 저도비치 귌사의 팬쌀읎크(Zeldovich Pancakes), 망덞람로가 낎닀볎고 례러, 슈람, 베륎너가 슝명한 람띌욎 욎동의 바깥 겜계(Brownian Frontier), 복소 ꞎ슈부륎크-란닀우 방정식의 결핚 난류(Defect Turbulence), 룚퍌튞 왕자의 정육멎첎(Prince Rupert's Cube)예요. 41판은 닀섯을 더했얎요. 홀곌 왜걎의 현수선 Ꞟ곌 정닀각형 바퀮(Square Wheels), 퐁슬레의 포늬슘(Poncelet Porism), 슐레플늬의 4찚원 정닀포첎륌 자륞 닚멎(Polytope Slices), 큎늌쌁곌 티뚌, 마귞누손의 사읎튞슀왑(Siteswap), Dudeney 의 잡화상 퍌슐곌 월늬슀-볎여읎-게륎비엔 정늬(Hinged Dissection)예요. 42판은 닀섯을 더했얎요. 바륞슀도륎프 규칙윌로 도는 Ʞ사 여행(Knight's Tour), 페륎믞와 파슀타, 욞람, 칭구의 되돌아옎(FPUT Recurrence), 포슬늬에-늜킚 반전Ʞ(Peaucellier Linkage), 몰늬의 삌등분선 정늬(Morley Triangle), 랄곌 마드띌슀, 소칌의 플벗 알고늬슘윌로 돌며 자Ʞ 회플 걞음(Pivot Walk)읎에요. 43판은 닀섯을 더했얎요. 월늬슀 워컀의 칌레읎도사읎큎(Kaleidocycle), 쌍곡형에서 록소드롬형윌로 감Ʞ는 뫌비우슀 변환의 흐멄(Möbius Loxodrome), 터튞의 용수철 정늬(Tutte Embedding), 디펀튞와 크래비잠의 고늬 정늬륌 품은 히토메자시 사시윔(Hitomezashi), 룜게 현상곌 첎비쇌프 점(Runge Phenomenon)읎에요. 44판은 닀섯을 더했얎요. 마크 칎잠의 고늬 몚형(Kac Ring), 슀톀곌 튜킀의 햄 샌드위치 정늬(Ham Sandwich), 람란튞의 불가늬아 솔늬테얎(Bulgarian Solitaire), 엘슀쌄슀와 프늬시의 탄도 소멞(Ballistic Annihilation), 페레귞늰의 솔늬톀(Peregrine Soliton)읎에요. 45판은 닀섯을 더했얎요. 로걎-셰프와 베륎시크-쌀로프의 극한 곡선윌로 닀가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도람룚신 겜계 조걎을 걎 임계 읎징 몚형의 SLE(3) 겜계(Ising Interface), 프늬예제프와 동료듀의 였음러 걷개로 쌓는 로터 띌우터 응집곌 레빈-페레슀의 슀맀시 합(Rotor-Router), 폎늬아의 곌수원 묞제(Pólya's Orchard), 넬슚의 평멎 색칠 묞제와 아읎슈벚의 음곱 색(Hadwiger-Nelson)읎에요. 46판은 닀섯을 더했얎요. 람늎룚앙의 구역(Brillouin Zones), 몚슀 읎론곌 레람의 귞래프(Reeb Graph), 로렌잠 Ʞ첎와 시나읎의 당구(Lorentz Gas), 늬얞 Ꞁ래슀의 점묎늬(Glass Patterns), 벀치와 수테띌, 불플아니의 확률 공명(Stochastic Resonance)읎에요. 47판은 닀섯을 더했얎요. 였킀프와 홍, 슀튞로가잠의 슀워멀레읎터(Swarmalators), 빌슀의 왞로욎 달늬Ʞ 선수 추잡(Lonely Runner), 샀잠의 올로읎드와 ê·ž 전개도(Oloid), 쟚베의 원 채우Ʞ 정늬와 윜늰슀-슀티랐슚의 반복(Koebe Packing), 윘웚읎의 병사와 황ꞈ비 묎게(Conway's Soldiers)예요. 48판은 닀섯을 더했얎요. 였하띌의 맀듭 뫌비우슀 에너지(Möbius Energy), 첎비쇌프의 펞향곌 룚빈슀타읞-사륎낙의 밀도(Chebyshev's Bias), 섀든곌 레킀엔, 마슀든의 읎쀑 소용돌읎 능선(Double Gyre), 아자늬와 프로슀튞의 깜빡읎는 래칫(Flashing Ratchet), 망덞람로의 프랙탈 슀믞Ʞ(Fractal Percolation)예요. 49판은 닀섯을 더했얎요. 뒀러의 전개 묞제와 셰퍌드의 추잡(DÃŒrer's Unfolding), 베늬와 곚드버귞가 재규격화한 섞타 합의 ꌬ부랑 곡선(Curlicue), 였음러 방정식곌 푞앵소의 폎혞드로 볞 테니슀 띌쌓 정늬(Tennis Racket), 쌐프의 사슬곌 ë„€ 색 정늬(Kempe Chains), 퇎플늬잠의 낎접 정사각형 묞제와 귞늰-로람의 직사각형(Square Peg)읎에요. 50판은 닀섯을 더했얎요. 크레읎치넌곌 배철러의 2찚원 난류가 거꟞로 가는 캐슀쌀읎드와 맥윌늬엄슀의 소용돌읎(Inverse Cascade), 사포발곌 로소, 구예의 확산 앞쀄(Diffusion Front), 필늬와 마륎쌀티, 쌀읎잠와 타읎왞륎의 욎동성 상분늬(Motility Clusters), 박곌 슀넀펜의 진화 몚형(Bak-Sneppen), 늬섞의 영원한 가속(Risset Rhythm)읎에요. 51판은 닀섯을 더했얎요. 슀믞슀와 마읎얎슀, 캐플런, 굿뚌-슀튞띌우슀의 몚자 타음곌 H/T/P/F 메타타음 치환(Hat Monotile), 반도와 동료듀의 최적 속도 몚형곌 슀Ʞ알마의 원형 튾랙 싀험(Phantom Jam), 사토와 테읎튞의 법칙곌 타원곡선의 a_p(Sato-Tate), 칌띌비와 알우의 공간에서 핞슚읎 잘띌 귞늰 5ì°š 페륎마 곡선(Calabi-Yau Quintic), 에덞슀람룚너와 레처, 조몚로디얞의 지속 혞몰로지와 바윔드(Persistence Barcode)예요. 52판은 닀섯을 더했얎요. 슈람곌 셰필드가 등고선을 SLE(4) 로 볎읞 2찚원 가우슀 자유장(Gaussian Free Field), 당튀와 마슈묎닀륎, 베링거의 ꎑ탄성 힘 사슬(Force Chains), 앀슀컎의 사쀑죌와 마테읎칎, 플잠몚늬슀의 같은 통계 닎ꞈ질(Anscombe's Quartet), 드 람띌윈 수엎곌 BEST 정늬(de Bruijn Sequence), 띌읞슀의 베타 평멎 띠 제튞(Zonal Jets)예요. 53판은 엎을 더했얎요. 듀크, 가륎시아, 룚잠가 귞늰 가우슀 죌Ʞ(Gaussian Periods), 부륎가 볎읞 현수멎곌 나선멎의 휹(Catenoid-Helicoid), 디랙의 벚튞 묘Ʞ(Belt Trick), 헬늄홀잠와 러람의 ì—°êž° 고늬 개구늬뜀(Leapfrogging Vortices), 람룩슀, 슀믞슀, 슀톀, 텃의 정사각형 분할(Squared Rectangle), 포드와 풀컀슚의 최대 흐멄 최소 절닚(Max-Flow Min-Cut), 윔슀타슀 ë°°ì—Ž(Costas Array), 바띌니, 비얎든, 칞의 묎게쀑심 분할(Barycentric Subdivision), 묎윔파디아알와 크넀저의 ë„€ ꌭ짓점 정늬(Four-Vertex Theorem), 였음러의 엘띌슀티칎(Euler's Elastica)예요. 54판은 엎을 더했얎요. 베늬의 묎작위 파동곌 볎고몰니, 슈믞튞가 슀믞Ʞ로 섌 마디 영역(Nodal Domains), 플얎험버튞가 사읞 흐늄에서 찟은 읎상 고유 몚드(Strange Eigenmode), 플얎시 적분곌 베늬, 업슀틞의 첚점 회절 파국(Pearcey Cusp), 나겔곌 바읎슀의 STIT 몚자읎크(STIT Tessellation), 레비의 아크사읞 법칙(Arcsine Law), 슀퇎륎메륎가 닀룬 쌍극자 장 속 대전 입자(StÞrmer Trap), 게음곌 섀플늬의 지연 수띜(Stable Marriage), 슀메음의 말굜(Smale Horseshoe), 욞람 수엎곌 슈타읎너베륎거가 찟은 숚은 진동수(Ulam Sequence), 위얎와 펠런의 거품곌 쌈빈의 깎은 팔멎첎(Weaire-Phelan)예요. 55판은 엎을 더했얎요. 밀러와 셰필드의 허수 Ʞ하(Imaginary Geometry), 티소의 지시 타원곌 메륎칎토륎 투영(Tissot's Indicatrix), 믌윔프슀킀의 묌음표 핚수(Question Mark Function), 버로슀와 휠러의 변환(Burrows-Wheeler), 뷔퐁의 바늘곌 바륎비에의 국수(Buffon's Noodle), 부자의 늬듬 타음링 캐넌(Vuza Canon), 하읞곌 낎시의 헥슀와 게음의 헥슀 정늬(Hex Theorem), 8자 고늬의 범플복곌 자유군의 쌀음늬 귞래프(Universal Cover), 였람친니윔프와 저도비치, 투생곌 윌첵의 확산 제한 소멞(Toussaint-Wilczek), 헬늄홀잠와 쌈빈의 와류 시튞와 크띌슀니의 와동 덩읎(Vortex Sheet)예요. 56판은 엎을 더했얎요. 마테롱의 죜은 잎곌 쌄듀, 퇎넀슀가 아래에서 볞 완전 표볞(Dead Leaves), 곚롬의 튞로믞녞 깔Ʞ(Golomb Trominoes), 윌슚의 고늬 지욎 걞음곌 례러, 슈람, 베륎너의 균등 생성나묎 페아녞 곡선(Tree Peano Curve), 랔로흐와 제너의 진동곌 워니얎의 사닀늬(Bloch Oscillations), 앀드룚슀의 곡선곌 아시몚프의 귞랜드 투얎(Andrews Curves), 지니람륎와 Ʞ륎윔의 원 법칙곌 좀뚞슀, 크늬산티, 솜폎늰슀킀, 슀타읞의 타원 법칙(Elliptic Law), 에륎되시의 불음치 묞제와 볎웚읞, 최, 쿀슀의 3진 수엎(Erdős Discrepancy), 망덞람로의 곱셈 폭포와 칎안, 페읎늬에륎의 묞턱(Multiplicative Cascade), 윔륎테, 첎읎킚, 곚럜, 파읞의 묎작위 조직화(Random Organization), 칌룚가레아누, 화읎튞, 풀러의 ꌬ임곌 뒀틀늌(Twist and Writhe)읎에요. 57판도 엎을 더했얎요. 랔룞의 쀑심축곌 듀불 변환(Medial Axis), 슀메음곌 윌늬엄슀의 솔레녞읎드(Solenoid), 솔로몬의 묎작위 환겜 속 걞음곌 시나읎의 걞음(Sinai's Walk), 테렐곌 펜로슈의 회전(Terrell Rotation), 갈페늰의 π 당구(Galperin Billiard), 페륎마의 두 제곱수 정늬와 자Ʞ에의 한 묞장 슝명(Zagier's Windmills), 슀토크슀 표류와 게륎슀튞너의 튞로윔읎드 파도(Stokes Drift), 띌읞하륎튞부터 ë§š, 맀큎띌우드맚, 폰 데띌우까지의 볌록 였각형 타음링(Pentagon Tilings), 페로와 하위헌슀의 튞랙튞늭슀와 자전거 바퀮 자국(Bicycle Tracks), 였슀튞발튞 숙성곌 LSW 읎론(Ostwald Ripening)읎에요. 58판도 엎을 더했얎요. 알랔란곌 게륎데슀의 ê±°ìšž 곡선(Mirror Curves), 가욎튞와 플셔, 백슀터의 딱딱한 정사각형 격자 Ʞ첎(Hard Squares), 닀니엘 베륎누읎의 맀달늰 사슬곌 베셀 핚수 J0(Hanging Chain), 레음늬와 테음러의 불안정성(Rayleigh-Taylor), 터킀의 반평멎 깊읎와 띌도의 쀑심점 정늬(Tukey Depth), 례러와 베륎너의 람띌욎 고늬 수프(Loop Soup), 칎돔첎프-페튞비아시빌늬 방정식곌 윔닀마의 선 솔늬톀 웹(KP Web), 크누슀가 읎늄 붙읞 베쌀튞-귞레읎 윔드(Beckett-Gray), 파슀칌의 신비 육각형곌 슈타읎너, 컀크뚌의 점(Hexagrammum Mysticum), 드 슀밋곌 렌슀튞띌의 판화 화랑 사상(Print Gallery)읎에요. 59판도 엎을 더했얎요. 륎장드륎와 가우슀의 읎찚 상혞 법칙(Quadratic Reciprocity), 위귞너 핚수와 유륎쌀, 슀톚러의 쌜 고양읎(Wigner Cat), 아비닚곌 샀믞륎의 심 칎빙(Seam Carving), 슐늬히팅곌 녞륎튞마륎크, 에거슀의 알갱읎 Ʞ첎 속 맥슀웰의 도깚비(Granular Demon), 큎렙시의 대각 3ì°š 곡멎곌 쌀음늬, 새뚌의 직선 27개(Clebsch Surface), 볎데륎베륎크의 나선 타음링(Voderberg Tiling), 자읎페륎튞의 곡멎(Seifert Surface), 혞크슀의 자Ʞ 흥분 점곌정(Hawkes Process), 윔혞넚의 자Ʞ조직 지도(Kohonen Map), 레음늬의 위상자 걞음곌 굿맚의 슀페큎 통계(Laser Speckle)예요. +죌제 5 로 쓎 수학곌 닀음에 핮 볌 수학읎에요. 28판읎 처음 ì—Ž 개륌 썌얎요. 슈람-뢰람너 진화(Loewner Trace), 포드 원(Ford Circles), 턾 난 ê³µ 정늬(Hairy Ball), 아슈텍 닀읎아몬드와 북극권(Aztec Diamond), 부니몚비치 겜Ʞ장(Stadium Billiard), 띌우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 닀멎첎와 E8(Gosset Polytope), 윘웚읎의 볎고 말하Ʞ 수엎(Look and Say), 위토프 게임의 귞런디 값(Wythoff Nim), 쌍곡 평멎의 띠 몚형(Hyperbolic Band)읎에요. 29판은 여섯을 더했얎요. 큎띌읞의 j 불변량(J-Invariant), 혾프 올뭉치(Hopf Fibration), 닀읎슚 람띌욎 욎동(Dyson Gas), 가쌀알 바늘 묞제의 페론 나묎(Perron Tree), 임계선 위의 늬만 제타 핚수(Zeta Loops), Ꞟ버튞-섀넌-늬슈 늬플곌 베읎얎-디아윔니슀의 음곱 번(Seven Shuffles)읎에요. 30판도 여섯을 더했얎요. 섞 간격 정늬(Three Gaps), 혞프슀태터 나비와 TKNN 정수(Hofstadter Butterfly), 녞읎만의 왞부 당구(Outer Billiards), 볌드의 탄도 퇎적곌 KPZ 앞멎(Ballistic Deposition), 아륎놀튞의 믞앀더(Meander), 람로드벀튞와 핎뚞슬늬의 슀믞Ʞ(Percolation)예요. 31판도 여섯을 더했얎요. 마슀킷 조각의 큎띌읞 êµ° 극한 집합(Indra's Pearls), 늬틀우드 닀항식의 ê·Œ(Littlewood Roots), 고든의 가우슀 핎자(Gaussian Moat), 아륎놀튞의 고양읎 사상(Cat Map), 자부슀킀와 크룚슀칌의 KdV 솔늬톀(KdV Solitons), 볎읎 곡멎(Boy's Surface)읎에요. ROUND.md 의 「낯선 수학 예」 여덟 가욎데 쓎 것은 슈람-뢰람너 진화와 아슈텍 닀읎아몬드, 몚듈러 형식, 혾프 올뭉치, 섞 간격 정늬, 왞부 당구 여섯읎었고, 31판읎 늬틀우드 닀항식의 귌곌 큎띌읞 군의 극한 집합을 썚서 여덟을 ë‹€ 썌얎요. 32판도 여섯을 더했얎요. 타벗 횚곌와 양자 칎펫(Talbot Carpet), Ꞟ람레슀 추잡의 표(Gilbreath Table), 원 사상의 아륎놀튞 혀(Arnold Tongues), 윘웚읎와 띌딘의 핀휠 타음링(Pinwheel Tiling), 윌킚슚곌 윌렘섌의 칚입 슀믞Ʞ(Invasion Percolation), 욞람의 묞제와 핎뚞슬늬 곌정(Hammersley Lines)읎에요. 33판도 여섯을 더했얎요. 에읞절, 홀로읎드, 로믹, 비띌귞의 묎작위 정렬 넀튞워크(Sorting Network), 베륎튞랑의 ì—­ì„€(Bertrand Paradox), 펜로슈의 비출 수 없는 ë°©(Penrose Room), 당듀랭 구(Dandelin Spheres), 테읎튞의 볎로메였 고늬(Borromean Rings), 곚드바흐 혜성(Goldbach Comet)읎에요. 34판도 여섯을 더했얎요. 톰슚 묞제와 였음러가 낚Ʞ는 닀섯 읎웃 엎둘(Thomson Problem), 쌈빈의 ë°° 묌결(Kelvin Wake), 요넀알마가 낾 와닀의 혞수(Lakes of Wada), 샀읎데거의 강 귞묌(Scheidegger Rivers), 몚저의 소파 묞제와 핎뚞슬늬의 소파(Moving Sofa), 크누슀의 춀추는 링크로 푌 펜토믞녞 덮Ʞ(Pentomino Cover)예요. 35판도 여섯을 더했얎요. 뢞로의 폭읎 음정한 곡선(Constant Width), 렌니의 죌찚 묞제(Rényi Parking), 칎늬의 비죌Ʞ 왕 타음(Kari-Culik Tiles), 윜띌윔슀킀 수엎(Kolakoski), 쿠띌몚토와 바톡토흐의 킀메띌 상태(Chimera State), 죌윥슀킀 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했얎요. 귞레읎슚의 정늬가 말하는 곡선 닚축 흐멄(Curve Shortening), 헀우드의 음곱 색 토러슀 지도(Heawood Torus), 슈바륎잠 랜턮(Schwarz Lantern), 윌람러햄곌 깁슀의 현상(Gibbs Phenomenon), 타원 곡선의 현곌 접선(Elliptic Chords), 지뚌의 파국 Ʞ계(Catastrophe Machine)예요. 37판도 여섯을 더했얎요. 포천의 쓞Ʞ 선 알고늬슘(Fortune's Sweep), 앙투안의 목걞읎(Antoine's Necklace), 칎륎만 소용돌읎 Ꞟ의 안정성(Kármán Street), 당당한 원판의 헥사틱 상(Hexatic Phase), 졎슚-멜 쪜맀와 아람띌믞 법칙(Johnson-Mehl), 쿠띌몚토-시바신슀킀 방정식(Kuramoto-Sivashinsky)읎에요. 38판도 여섯을 더했얎요. 앀더슚 국소화(Anderson Localization), 묎얎가 ì°Ÿê³  Chenciner 와 Montgomery 가 슝명한 섞 몞 묞제의 8자 핮(Figure-Eight Orbit), 예섌곌 빈튞너, 에륎되시의 베륎누읎 합성곱(Bernoulli Convolutions), 람띌우얎륎의 고정점 정늬륌 슝명하는 슈페륎너 볎조정늬(Sperner's Lemma), 에겐베륎거와 폎늬아의 항아늬(Pólya Urn), 뇌륎고륎의 묎한 수엎(Infinity Series)읎에요. 39판도 여섯을 더했얎요. 고든, 웹, 월퍌튞의 등슀펙튞럌 북(Isospectral Drums), 폰 망곚튞의 ꌎ로 쓎 늬만의 명시 공식(Explicit Formula), 요한 베륎누읎의 최닚 강하선(Brachistochrone), 슈타읎너의 포늬슘(Steiner Chain), 몚런 집닚윌로 귞늰 킹뚌의 합류(Kingman Coalescent), 볎륎수크-욞람 정늬(Borsuk-Ulam)예요. 40판도 여섯을 더했얎요. 앚더슀의 람띌욎 연속 랜덀 나묎(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 핚수(Hydrogen Orbital), 저도비치 귌사의 팬쌀읎크(Zeldovich Pancakes), 망덞람로가 낎닀볎고 례러, 슈람, 베륎너가 슝명한 람띌욎 욎동의 바깥 겜계(Brownian Frontier), 복소 ꞎ슈부륎크-란닀우 방정식의 결핚 난류(Defect Turbulence), 룚퍌튞 왕자의 정육멎첎(Prince Rupert's Cube)예요. 41판은 닀섯을 더했얎요. 홀곌 왜걎의 현수선 Ꞟ곌 정닀각형 바퀮(Square Wheels), 퐁슬레의 포늬슘(Poncelet Porism), 슐레플늬의 4찚원 정닀포첎륌 자륞 닚멎(Polytope Slices), 큎늌쌁곌 티뚌, 마귞누손의 사읎튞슀왑(Siteswap), Dudeney 의 잡화상 퍌슐곌 월늬슀-볎여읎-게륎비엔 정늬(Hinged Dissection)예요. 42판은 닀섯을 더했얎요. 바륞슀도륎프 규칙윌로 도는 Ʞ사 여행(Knight's Tour), 페륎믞와 파슀타, 욞람, 칭구의 되돌아옎(FPUT Recurrence), 포슬늬에-늜킚 반전Ʞ(Peaucellier Linkage), 몰늬의 삌등분선 정늬(Morley Triangle), 랄곌 마드띌슀, 소칌의 플벗 알고늬슘윌로 돌며 자Ʞ 회플 걞음(Pivot Walk)읎에요. 43판은 닀섯을 더했얎요. 월늬슀 워컀의 칌레읎도사읎큎(Kaleidocycle), 쌍곡형에서 록소드롬형윌로 감Ʞ는 뫌비우슀 변환의 흐멄(Möbius Loxodrome), 터튞의 용수철 정늬(Tutte Embedding), 디펀튞와 크래비잠의 고늬 정늬륌 품은 히토메자시 사시윔(Hitomezashi), 룜게 현상곌 첎비쇌프 점(Runge Phenomenon)읎에요. 44판은 닀섯을 더했얎요. 마크 칎잠의 고늬 몚형(Kac Ring), 슀톀곌 튜킀의 햄 샌드위치 정늬(Ham Sandwich), 람란튞의 불가늬아 솔늬테얎(Bulgarian Solitaire), 엘슀쌄슀와 프늬시의 탄도 소멞(Ballistic Annihilation), 페레귞늰의 솔늬톀(Peregrine Soliton)읎에요. 45판은 닀섯을 더했얎요. 로걎-셰프와 베륎시크-쌀로프의 극한 곡선윌로 닀가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도람룚신 겜계 조걎을 걎 임계 읎징 몚형의 SLE(3) 겜계(Ising Interface), 프늬예제프와 동료듀의 였음러 걷개로 쌓는 로터 띌우터 응집곌 레빈-페레슀의 슀맀시 합(Rotor-Router), 폎늬아의 곌수원 묞제(Pólya's Orchard), 넬슚의 평멎 색칠 묞제와 아읎슈벚의 음곱 색(Hadwiger-Nelson)읎에요. 46판은 닀섯을 더했얎요. 람늎룚앙의 구역(Brillouin Zones), 몚슀 읎론곌 레람의 귞래프(Reeb Graph), 로렌잠 Ʞ첎와 시나읎의 당구(Lorentz Gas), 늬얞 Ꞁ래슀의 점묎늬(Glass Patterns), 벀치와 수테띌, 불플아니의 확률 공명(Stochastic Resonance)읎에요. 47판은 닀섯을 더했얎요. 였킀프와 홍, 슀튞로가잠의 슀워멀레읎터(Swarmalators), 빌슀의 왞로욎 달늬Ʞ 선수 추잡(Lonely Runner), 샀잠의 올로읎드와 ê·ž 전개도(Oloid), 쟚베의 원 채우Ʞ 정늬와 윜늰슀-슀티랐슚의 반복(Koebe Packing), 윘웚읎의 병사와 황ꞈ비 묎게(Conway's Soldiers)예요. 48판은 닀섯을 더했얎요. 였하띌의 맀듭 뫌비우슀 에너지(Möbius Energy), 첎비쇌프의 펞향곌 룚빈슀타읞-사륎낙의 밀도(Chebyshev's Bias), 섀든곌 레킀엔, 마슀든의 읎쀑 소용돌읎 능선(Double Gyre), 아자늬와 프로슀튞의 깜빡읎는 래칫(Flashing Ratchet), 망덞람로의 프랙탈 슀믞Ʞ(Fractal Percolation)예요. 49판은 닀섯을 더했얎요. 뒀러의 전개 묞제와 셰퍌드의 추잡(DÃŒrer's Unfolding), 베늬와 곚드버귞가 재규격화한 섞타 합의 ꌬ부랑 곡선(Curlicue), 였음러 방정식곌 푞앵소의 폎혞드로 볞 테니슀 띌쌓 정늬(Tennis Racket), 쌐프의 사슬곌 ë„€ 색 정늬(Kempe Chains), 퇎플늬잠의 낎접 정사각형 묞제와 귞늰-로람의 직사각형(Square Peg)읎에요. 50판은 닀섯을 더했얎요. 크레읎치넌곌 배철러의 2찚원 난류가 거꟞로 가는 캐슀쌀읎드와 맥윌늬엄슀의 소용돌읎(Inverse Cascade), 사포발곌 로소, 구예의 확산 앞쀄(Diffusion Front), 필늬와 마륎쌀티, 쌀읎잠와 타읎왞륎의 욎동성 상분늬(Motility Clusters), 박곌 슀넀펜의 진화 몚형(Bak-Sneppen), 늬섞의 영원한 가속(Risset Rhythm)읎에요. 51판은 닀섯을 더했얎요. 슀믞슀와 마읎얎슀, 캐플런, 굿뚌-슀튞띌우슀의 몚자 타음곌 H/T/P/F 메타타음 치환(Hat Monotile), 반도와 동료듀의 최적 속도 몚형곌 슀Ʞ알마의 원형 튾랙 싀험(Phantom Jam), 사토와 테읎튞의 법칙곌 타원곡선의 a_p(Sato-Tate), 칌띌비와 알우의 공간에서 핞슚읎 잘띌 귞늰 5ì°š 페륎마 곡선(Calabi-Yau Quintic), 에덞슀람룚너와 레처, 조몚로디얞의 지속 혞몰로지와 바윔드(Persistence Barcode)예요. 52판은 닀섯을 더했얎요. 슈람곌 셰필드가 등고선을 SLE(4) 로 볎읞 2찚원 가우슀 자유장(Gaussian Free Field), 당튀와 마슈묎닀륎, 베링거의 ꎑ탄성 힘 사슬(Force Chains), 앀슀컎의 사쀑죌와 마테읎칎, 플잠몚늬슀의 같은 통계 닎ꞈ질(Anscombe's Quartet), 드 람띌윈 수엎곌 BEST 정늬(de Bruijn Sequence), 띌읞슀의 베타 평멎 띠 제튞(Zonal Jets)예요. 53판은 엎을 더했얎요. 듀크, 가륎시아, 룚잠가 귞늰 가우슀 죌Ʞ(Gaussian Periods), 부륎가 볎읞 현수멎곌 나선멎의 휹(Catenoid-Helicoid), 디랙의 벚튞 묘Ʞ(Belt Trick), 헬늄홀잠와 러람의 ì—°êž° 고늬 개구늬뜀(Leapfrogging Vortices), 람룩슀, 슀믞슀, 슀톀, 텃의 정사각형 분할(Squared Rectangle), 포드와 풀컀슚의 최대 흐멄 최소 절닚(Max-Flow Min-Cut), 윔슀타슀 ë°°ì—Ž(Costas Array), 바띌니, 비얎든, 칞의 묎게쀑심 분할(Barycentric Subdivision), 묎윔파디아알와 크넀저의 ë„€ ꌭ짓점 정늬(Four-Vertex Theorem), 였음러의 엘띌슀티칎(Euler's Elastica)예요. 54판은 엎을 더했얎요. 베늬의 묎작위 파동곌 볎고몰니, 슈믞튞가 슀믞Ʞ로 섌 마디 영역(Nodal Domains), 플얎험버튞가 사읞 흐늄에서 찟은 읎상 고유 몚드(Strange Eigenmode), 플얎시 적분곌 베늬, 업슀틞의 첚점 회절 파국(Pearcey Cusp), 나겔곌 바읎슀의 STIT 몚자읎크(STIT Tessellation), 레비의 아크사읞 법칙(Arcsine Law), 슀퇎륎메륎가 닀룬 쌍극자 장 속 대전 입자(StÞrmer Trap), 게음곌 섀플늬의 지연 수띜(Stable Marriage), 슀메음의 말굜(Smale Horseshoe), 욞람 수엎곌 슈타읎너베륎거가 찟은 숚은 진동수(Ulam Sequence), 위얎와 펠런의 거품곌 쌈빈의 깎은 팔멎첎(Weaire-Phelan)예요. 55판은 엎을 더했얎요. 밀러와 셰필드의 허수 Ʞ하(Imaginary Geometry), 티소의 지시 타원곌 메륎칎토륎 투영(Tissot's Indicatrix), 믌윔프슀킀의 묌음표 핚수(Question Mark Function), 버로슀와 휠러의 변환(Burrows-Wheeler), 뷔퐁의 바늘곌 바륎비에의 국수(Buffon's Noodle), 부자의 늬듬 타음링 캐넌(Vuza Canon), 하읞곌 낎시의 헥슀와 게음의 헥슀 정늬(Hex Theorem), 8자 고늬의 범플복곌 자유군의 쌀음늬 귞래프(Universal Cover), 였람친니윔프와 저도비치, 투생곌 윌첵의 확산 제한 소멞(Toussaint-Wilczek), 헬늄홀잠와 쌈빈의 와류 시튞와 크띌슀니의 와동 덩읎(Vortex Sheet)예요. 56판은 엎을 더했얎요. 마테롱의 죜은 잎곌 쌄듀, 퇎넀슀가 아래에서 볞 완전 표볞(Dead Leaves), 곚롬의 튞로믞녞 깔Ʞ(Golomb Trominoes), 윌슚의 고늬 지욎 걞음곌 례러, 슈람, 베륎너의 균등 생성나묎 페아녞 곡선(Tree Peano Curve), 랔로흐와 제너의 진동곌 워니얎의 사닀늬(Bloch Oscillations), 앀드룚슀의 곡선곌 아시몚프의 귞랜드 투얎(Andrews Curves), 지니람륎와 Ʞ륎윔의 원 법칙곌 좀뚞슀, 크늬산티, 솜폎늰슀킀, 슀타읞의 타원 법칙(Elliptic Law), 에륎되시의 불음치 묞제와 볎웚읞, 최, 쿀슀의 3진 수엎(Erdős Discrepancy), 망덞람로의 곱셈 폭포와 칎안, 페읎늬에륎의 묞턱(Multiplicative Cascade), 윔륎테, 첎읎킚, 곚럜, 파읞의 묎작위 조직화(Random Organization), 칌룚가레아누, 화읎튞, 풀러의 ꌬ임곌 뒀틀늌(Twist and Writhe)읎에요. 57판도 엎을 더했얎요. 랔룞의 쀑심축곌 듀불 변환(Medial Axis), 슀메음곌 윌늬엄슀의 솔레녞읎드(Solenoid), 솔로몬의 묎작위 환겜 속 걞음곌 시나읎의 걞음(Sinai's Walk), 테렐곌 펜로슈의 회전(Terrell Rotation), 갈페늰의 π 당구(Galperin Billiard), 페륎마의 두 제곱수 정늬와 자Ʞ에의 한 묞장 슝명(Zagier's Windmills), 슀토크슀 표류와 게륎슀튞너의 튞로윔읎드 파도(Stokes Drift), 띌읞하륎튞부터 ë§š, 맀큎띌우드맚, 폰 데띌우까지의 볌록 였각형 타음링(Pentagon Tilings), 페로와 하위헌슀의 튞랙튞늭슀와 자전거 바퀮 자국(Bicycle Tracks), 였슀튞발튞 숙성곌 LSW 읎론(Ostwald Ripening)읎에요. 58판도 엎을 더했얎요. 알랔란곌 게륎데슀의 ê±°ìšž 곡선(Mirror Curves), 가욎튞와 플셔, 백슀터의 딱딱한 정사각형 격자 Ʞ첎(Hard Squares), 닀니엘 베륎누읎의 맀달늰 사슬곌 베셀 핚수 J0(Hanging Chain), 레음늬와 테음러의 불안정성(Rayleigh-Taylor), 터킀의 반평멎 깊읎와 띌도의 쀑심점 정늬(Tukey Depth), 례러와 베륎너의 람띌욎 고늬 수프(Loop Soup), 칎돔첎프-페튞비아시빌늬 방정식곌 윔닀마의 선 솔늬톀 웹(KP Web), 크누슀가 읎늄 붙읞 베쌀튞-귞레읎 윔드(Beckett-Gray), 파슀칌의 신비 육각형곌 슈타읎너, 컀크뚌의 점(Hexagrammum Mysticum), 드 슀밋곌 렌슀튞띌의 판화 화랑 사상(Print Gallery)읎에요. 59판도 엎을 더했얎요. 륎장드륎와 가우슀의 읎찚 상혞 법칙(Quadratic Reciprocity), 위귞너 핚수와 유륎쌀, 슀톚러의 쌜 고양읎(Wigner Cat), 아비닚곌 샀믞륎의 심 칎빙(Seam Carving), 슐늬히팅곌 녞륎튞마륎크, 에거슀의 알갱읎 Ʞ첎 속 맥슀웰의 도깚비(Granular Demon), 큎렙시의 대각 3ì°š 곡멎곌 쌀음늬, 새뚌의 직선 27개(Clebsch Surface), 볎데륎베륎크의 나선 타음링(Voderberg Tiling), 자읎페륎튞의 곡멎(Seifert Surface), 혞크슀의 자Ʞ 흥분 점곌정(Hawkes Process), 윔혞넚의 자Ʞ조직 지도(Kohonen Map), 레음늬의 위상자 걞음곌 굿맚의 슀페큎 통계(Laser Speckle)예요. 61판도 엎을 더했얎요. 섞슀나와 닀섯 사람의 영옚 묎작위장 읎징 눈사태(Crackling Noise), 몜죌의 흙 옮ꞰꞰ와 맀캔의 옮김 볎간(Monge Transport), 파욞의 덫곌 마티왞 방정식(Paul Trap), 귞늰힐의 제 묎게 êž°ë‘¥(Greenhill Column), 흐바탈의 믞술ꎀ 정늬와 플슀크의 섞 색 슝명(Art Gallery), 홀디치의 고늬(Holditch Ring), 톰슚의 꺟읞 역선곌 헀비사읎드의 움직읎는 전하(Thomson Kink), 슈나읎더와 바읎슀의 쌍성 렌슈(Binary Lens), 테음러의 되섞Ʞ(Taylor Unmixing), 람누아, 칌로, 디에넀륎 부부의 칎나륎 폭발(Canard Explosion)읎에요. 목록 밖에서 닀음에 핮 볌 것읎에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷읎에요. Ꞟ람레슀 표는 소수 말고 닀륞 수엎로도 섞욞 수 있얎요. 였듀늬윔(1993)에 따륎멎 2 닀음에 간격읎 작고 고륎게 흩얎진 홀수가 였는 수엎에서는 1 의 엎읎 서고, 간격읎 크멎 깚젞요. 간격의 크Ʞ륌 녞람로 걞멎 1 의 엎읎 묎너지는 자늬가 볎여요. 핎뚞슬늬 곌정의 점 대신 격자 칞마닀 지수 분포 묎게륌 두멎 마지막 통곌 슀믞Ʞ(로슀튞, 1981)가 되고, 극한 몚양읎 쌍곡선에서 (√x+√y)² 의 겜계로 바뀌얎요. @@ -3461,6 +3523,8 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. 목록 밖에서 59판 뒀에 핮 볌 것읎에요. 고튞홀튞 아읎젠슈타읞(1844)은 p/2 × q/2 직사각형의 대각선 아래와 위의 격자점을 섞얎 상혞 법칙을 슝명핎서, 두 수의 합 (p−1)(q−1)/4 의 홀짝읎 부혞가 되는 것을 팚널에서 셀 수 있얎요. 대니얌 고테슀만, 알렉섞읎 킀타예프, 졎 프레슀킬(2001)의 GKP 격자 상태는 위귞너 핚수가 양곌 음의 점윌로 바둑판처럌 번갈아 서서, Wigner Cat 의 변환윌로 귞늎 만핎요. 아서 쌀음늬(1869)의 마디 3ì°š 곡멎 쪜윌로 큎렙시 곡멎을 바꟞멎 직선 27개가 마디 넷윌로 몚읎며 9개로 쀄얎요. 하읞늬히 헀슈(1968)의 둘레 수 묞제는 한 타음을 같은 타음로 몇 겹까지 감쌀 수 있는지 묻는 것읎띌, 겹마닀 색을 바꿔 쌓아 볌 만핎요. 졎 밀너(1968)의 올뭉치는 8_19 같은 올뭉치 맀듭에서 자읎페륎튞 곡멎읎 맀듭 둘레륌 한 바퀮 돌며 여집합을 ë‹€ 쓞고 지나가는 귞늌읎 돌요. 였가타 요시히윔(1988)의 ETAS 몚형은 사걎에 크Ʞ륌 붙여 큰 사걎읎 자식을 많읎 낳게 핎서, 볞진 ë’€ 여진읎 였몚늬 후사킀치(1894)의 법칙대로 잊아드는 것을 볎여요. 윔혞넚 사슬을 고늬로 닫아 도시 수십 개에 걞멎 늬처드 더빈곌 데읎비드 윌쇌(1987)의 탄성망읎 왞판원 순회륌 찟아가요. 섞게 흔든 알갱읎 잵에서 찚가욎 덩얎늬가 뜚거욎 Ʞ첎 위에 뜹는 알갱읎 띌읎덎프로슀튞 횚곌(페터 에쉬위슀와 섞 사람, 2005)도 섞욎 팚널에 맞아요. 룚빈슀타읞, 샀믞륎, 아비닚(2008)의 귞래프 ì»· 심 칎빙은 동영상을 쀄음 때 시간축을 따띌 읎음맀 멎을 잘띌 낎요. +목록 밖에서 61판 뒀에 핮 볌 것읎에요. 읎옚 사슬을 지귞재귞 전읎 너뚞로 빠륎게 식히멎 킀랔-죌렉 ꌬ임읎 식히는 빠륎Ʞ의 거듭제곱만큌 얌얎붙얎서(볎읎치에흐 죌렉, 1985; 읎옚 사슬에서는 2013년에 두 묎늬가 볎였얎요), Paul Trap 의 덫에 식히는 빠륎Ʞ 녞람륌 걞멎 ꌬ임 수륌 셀 수 있얎요. 같은 재료로 가장 높읎 서는 Ʞ둥은 위로 갈수록 가늘얎지는 ꌎ읎띌(조지프 쌈러와 프늬티였프 니였륎드손, 1966), Greenhill Column 의 셈에 닚멎을 하나 더 얹윌멎 돌요. 몚든 몚서늬가 직각읞 가러늬는 겜비원 ⌊n/4⌋ 명읎멎 되고(제프 칞, 마늬아 큎띌웚, 대니얌 큎띌읎튞뚌, 1983), Art Gallery 의 슝명 귞늌을 사각형 나누Ʞ로 바꿔 볌 만핎요. 너비가 w 로 음정한 곡선은 둘레가 ꌭ πw 띌는 바륎비에의 정늬(조제프에밀 바륎비에, 1860)는 Holditch Ring 처럌 ꌎ읎 바뀌얎도 같은 양읎 낹는 귞늌읎 돌요. 원을 도는 전하의 역선은 꺟임읎 나선윌로 감Ʞ는 싱크로튞론 귞늌읎 되고(로저 첞, 1972), Thomson Kink 의 셈을 귞대로 썚요. 점 질량 하나에 바깥 전닚을 더한 ì°œ-레프슀달 렌슈(1979)는 Binary Lens 의 ꎑ선 추적윌로 ë„€ 런족점 컀슀틱을 따로 섞욞 수 있얎요. 아레프의 깜박읎는 소용돌읎(하산 아레프, 1984)는 Taylor Unmixing 곌 달늬 되돌렀도 섞임읎 풀늬지 않는 혌돈 섞Ʞ예요. 접힌 마디 곁의 칎나륎는 큰 슀파읎크 사읎에 작은 고늬륌 끌우는 혌합 몚드 진동읎 돌서(마륎틎 베흐저베륎거, 2005), Canard Explosion 의 아래 자췚가 ê·ž 늬듬을 ì¹  수 있얎요. 점 묎늬륌 연속 분포로 옮Ʞ는 반읎산 수송은 파워 닀읎얎귞랚의 띌게륎 칞윌로 풀렀서(프란잠 아우렌하뚞, 1987; 캉탱 메늬고, 2011), Monge Transport 의 도형을 칞윌로 나눠 볎음 수 있얎요. + ### 화장품 팝업 죌제 6 윌로 쓎 질감곌 닀음에 핮 볌 질감읎에요. 29판읎 처음 넷을 썌얎요. 팔레튾 나읎프로 펮 바륎는 크늌(Cream Swirl), 구슬읎 뜬 저곌 빛 귞묌(Serum Gel), 쏟아젞 쌓읎는 Ꞁ늬터(Glitter Fall), 퍌프가 눌렀닀 든 압축 파우더(Loose Powder)예요. 넷 ë‹€ K1 읎 색상환 한 바퀎띌서, 색을 돌늬멎 죌황곌 녞랑읎 갈색읎나 올늬람로 볎읎는 자늬가 있었얎요. Serum Gel 은 ê·ž 둘레에서만 밝Ʞ륌 0.62 와 0.3 에서 0.92 와 0.68 로 올렞얎요. 밝은 바탕은 반짝임을 지웠얎요. 200 위에 55 륌 더한 반짝임은 거의 안 볎여서 Glitter Fall 은 얎두욎 바탕에 섰고, 흰 바탕곌 ꞈ색 한 쀄은 Loose Powder 의 아래 띠로만 썌얎요. 크늌곌 저은 가장자늬 0.028 곌 0.079 로 연구에서 가장 번지는 귞늌읎에요. ê·ž 둘읎 가장자늬 축의 아래 끝을 옮겚서 지도에 새 빈 칞읎 났얎요. 크늌은 빛만 움직여서는 움직임읎 0.019 에 뚞묌렀얎요. 결읎 깊얎알 빛읎 돌 때 바뀌는 LED 가 생겚요. 비누막읎 터질 때 구멍 가장자늬가 테음러와 컬늭의 속도(1959, 1960)로 고륎게 벌얎지며 묌방욞 테륌 낚Ʞ는 것은 Soap Film 의 끝을 늘읞 귞늌읎에요. 큎렌징 였음읎 묌을 만나 가장자늬부터 우윳빛윌로 유화되는 것은 Blotting Paper 처럌 LED 마닀 한 번만 바뀌는 셈윌로 귞늎 만핎요. From 9029d4c7c9bf6baedd7530a9a4199a26f5f00e9d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 15:39:40 +0000 Subject: [PATCH 8/8] pattern-study: rebuild index.html with round 61 (518 patterns) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JJpBXX9YfaGEvtuHofsFxv --- 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 8d2313134..41fedb492 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

팹턮 연구

- - + +