From f80c28177d560c5daf2927f5189aba9c650225b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 02:10:10 +0000 Subject: [PATCH 01/20] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=2054=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_01CtQeRUJHSr3xVvLYqdqFXC From e59260a06142656184e12376fea940d176d7e807 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 02:45:53 +0000 Subject: [PATCH 02/20] =?UTF-8?q?54=ED=8C=90:=20stormer-trap=20=EC=A7=93?= =?UTF-8?q?=EA=B8=B0?= 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_01CtQeRUJHSr3xVvLYqdqFXC --- .../pattern-study/patterns/stormer-trap.js | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 incoming/pattern-study/patterns/stormer-trap.js diff --git a/incoming/pattern-study/patterns/stormer-trap.js b/incoming/pattern-study/patterns/stormer-trap.js new file mode 100644 index 000000000..f99505958 --- /dev/null +++ b/incoming/pattern-study/patterns/stormer-trap.js @@ -0,0 +1,249 @@ +// ===== Patternflow pattern ===== +// Title: Størmer Trap +// Family: particle +// Idea: Charged particles in the field of a magnetic dipole lying across the panel are integrated in three dimensions: each one spirals along a faint field-line loop that stands up to the top or the bottom of the panel and turns back at a mirror point near the centre, while the whole belt drifts slowly round the dipole's axis, positive charges one way and negative ones the other; the few inside the loss cone reach the small sphere at the centre and flash where they land. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Størmer's problem, charged particles in a dipole field (Carl Størmer, 1907) +// Round: 54 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Energy=0..1, Pitch=0..90 +// +// Knob 1: Hue. Turns both charges round the colour wheel together. At 0 the positive charges are gold and the negative ones sky blue, at 0.5 blue and orange; the field lines stay a faint violet. +// Knob 2: Speed. At 2 a particle covers 100 LEDs a second and one on an outer shell goes from one mirror point to the other and back in two to three seconds, while the belt takes about 100 s to drift once round. At 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Energy. The particles' momentum against the field, given as the outermost shell's distance from the centre in Størmer lengths, from 0.19 at 0 to 1 at 1. At 0 a gyration at the top of an outer loop is 4.5 LEDs across and the magnetic moment holds to a fifth of a percent. Størmer showed that a particle on the equator nearer than sqrt(2) - 1 = 0.414 of his length can never leave, whatever its direction, and the outermost shell passes that at 0.47. Around 0.5 the gyrations swell to 24 LEDs and the moment breaks; past it the orbits begin to leave the panel, two a second at 0.75 and at 1 nearly all of them within a second or two. +// Knob 4: Pitch. The smallest angle, in degrees, between a particle's velocity and the field where it crosses the equator; each particle starts between it and 60% of the way to 90. At 0 most dive deep toward the sphere, and the few inside the loss cone (1.9 degrees on the outermost shell, 4.9 on the innermost) land on it within one bounce. At 90 they all stay on the equator, a tall thin ellipse in the tilted view, and only gyrate and drift. + +const W = 64, H = 128, N = W * H, CX = 31.5, CY = 63.5; +const { floor, min, max, abs, sqrt, sin, cos, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// NP particles on shells LMIN to LMAX LEDs from the centre at the equator, +// round a sphere of radius RS. RATE is the simulated time per second at +// Speed 1; a particle moves one LED per unit of it. +const NP = 48, RS = 8, LMIN = 34, LMAX = 62, RATE = 50; +// The view is turned TILT radians about the vertical, so a ring round the +// dipole's axis shows as a narrow ellipse instead of a line. +const TILT = 0.21, CT = cos(TILT), ST = sin(TILT); +// Largest gyration angle and largest distance one Boris step may take. +const DPHI = 0.3, DMAX = 0.5; + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + o[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + o[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; + return o; +} + +// The dipole points along +x, across the panel; y is up and z comes out of +// the panel toward the viewer. B = k (3 x r / r^5 - x_hat / r^3), with k the +// dipole moment times the charge-to-mass ratio, so that a charge s = +1 or -1 +// feels dv/dt = s v x B. +function field(x, y, z, k, o) { + const r2 = x * x + y * y + z * z, i3 = k / (r2 * sqrt(r2)), f = (3 * x) / r2; + o[0] = i3 * (f * x - 1); + o[1] = i3 * f * y; + o[2] = i3 * f * z; +} + +// The latitude where a particle of equatorial pitch angle a mirrors: there +// the field is 1 / sin^2(a) times its equatorial value, and along a dipole +// field line B / B_eq = sqrt(1 + 3 sin^2 l) / cos^6 l. +function mirrorLat(s2) { + let lo = 0, hi = PI / 2 - 1e-6; + for (let i = 0; i < 40; i++) { + const m = (lo + hi) / 2, c = cos(m), sn = sin(m), g = (c * c * c * c * c * c) / sqrt(1 + 3 * sn * sn); + if (g > s2) lo = m; + else hi = m; + } + return lo; +} + +// Equatorial distance from the centre in Størmer lengths for the outermost +// shell, from the Energy knob. The Størmer length is sqrt(k / v) with v = 1. +const sOf = (u) => 0.19 * (1 / 0.19) ** u; + +export function setup(params) { + const F = (n) => new Float64Array(n); + const S = (params.s = { + hue: 0, speed: 2, en: 0, pitch: 0, k: 1, cst: 1, + x: F(NP), y: F(NP), z: F(NP), vx: F(NP), vy: F(NP), vz: F(NP), sg: new Int8Array(NP), tc: F(NP), rh: F(NP), L: F(NP), a: F(NP), want: new Uint8Array(NP), + tp: new Float32Array(N), tn: new Float32Array(N), fx: new Float32Array(N), fl: new Float32Array(N), b: F(3), c: [0, 0, 0], d: [0, 0, 0], + lost: 0, esc: 0, started: false, + }); + // The faint field lines in the plane of the panel: x = L cos^2 l sin l and + // y = +-L cos^3 l, from the sphere on one side round to the sphere on the + // other, for six shells. + for (const L of [16, 26, 36, 46, 56, 66]) { + const lh = Math.acos(sqrt(RS / L)); + for (let t = -lh; t <= lh; t += 0.002) { + const c = cos(t), x = CT * L * c * c * sin(t), y = L * c * c * c; + splat(S.fl, CX + x, CY - y, 1); + splat(S.fl, CX + x, CY + y, 1); + } + } + let m = 0; + for (let i = 0; i < N; i++) m = max(m, S.fl[i]); + for (let i = 0; i < N; i++) S.fl[i] = min(1, S.fl[i] / (0.55 * m)); +} + +function splat(A, px, py, w) { + const x0 = floor(px), y0 = floor(py), fx = px - x0, fy = py - y0; + if (x0 >= 0 && x0 < W && y0 >= 0 && y0 < H) A[y0 * W + x0] += w * (1 - fx) * (1 - fy); + if (x0 + 1 >= 0 && x0 + 1 < W && y0 >= 0 && y0 < H) A[y0 * W + x0 + 1] += w * fx * (1 - fy); + if (x0 >= 0 && x0 < W && y0 + 1 >= 0 && y0 + 1 < H) A[(y0 + 1) * W + x0] += w * (1 - fx) * fy; + if (x0 + 1 >= 0 && x0 + 1 < W && y0 + 1 >= 0 && y0 + 1 < H) A[(y0 + 1) * W + x0 + 1] += w * fx * fy; +} + +// Puts particle i on a shell L at a random point of its bounce: its guiding +// centre goes on the field line at a latitude inside its mirror latitude, +// with the pitch angle there that keeps the magnetic moment +// sin^2(a) / B the same as at the equator, then the particle is set off by +// its gyration radius in a random gyration phase. +function inject(S, i) { + const R = Math.random; + const sg = i & 1 ? -1 : 1, L = LMIN + (LMAX - LMIN) * R(); + const a = ((S.pitch + 0.6 * (90 - S.pitch) * R()) * PI) / 180, s2 = sin(a) ** 2; + // A guiding centre whose mirror point lies inside the sphere starts above + // the sphere: it is in the loss cone and will land on its way down. + const lm = min(mirrorLat(s2), 0.97 * Math.acos(sqrt(RS / L))), l0 = lm * sin(2 * PI * R()); + const c = cos(l0), r = L * c * c, ph = 2 * PI * R(), rho = r * c; + const gx = r * sin(l0), gy = rho * cos(ph), gz = rho * sin(ph); + const b = S.b; + field(gx, gy, gz, S.k, b); + const bm = sqrt(b[0] * b[0] + b[1] * b[1] + b[2] * b[2]), hx = b[0] / bm, hy = b[1] / bm, hz = b[2] / bm; + const sl = min(1, (s2 * bm * L * L * L) / S.k), vp = sqrt(sl), va = sqrt(1 - sl) * (R() < 0.5 ? -1 : 1); + // Two unit vectors across the field line. + let ex = -hy, ey = hx, ez = 0, n = sqrt(ex * ex + ey * ey); + if (n < 1e-6) (ex = 0), (ey = -hz), (ez = hy), (n = sqrt(ey * ey + ez * ez)); + ex /= n; + ey /= n; + ez /= n; + const fx = hy * ez - hz * ey, fy = hz * ex - hx * ez, fz = hx * ey - hy * ex, ps = 2 * PI * R(), cp = cos(ps), sp = sin(ps); + const ux = vp * (cp * ex + sp * fx), uy = vp * (cp * ey + sp * fy), uz = vp * (cp * ez + sp * fz); + S.vx[i] = va * hx + ux; + S.vy[i] = va * hy + uy; + S.vz[i] = va * hz + uz; + // The gyration radius vector is s (b_hat x v_perp) / B. + S.x[i] = gx + (sg * (hy * uz - hz * uy)) / bm; + S.y[i] = gy + (sg * (hz * ux - hx * uz)) / bm; + S.z[i] = gz + (sg * (hx * uy - hy * ux)) / bm; + S.sg[i] = sg; + S.L[i] = L; + S.a[i] = a; + S.want[i] = 0; + S.tc[i] = 0; + field(S.x[i], S.y[i], S.z[i], S.k, b); + S.rh[i] = max(sqrt(b[0] * b[0] + b[1] * b[1] + b[2] * b[2]) / DPHI, 1 / DMAX); +} + +// Advances particle i by T of simulated time with the Boris pusher, which +// turns the velocity about the local field by exactly the angle the field +// gives and so keeps the speed fixed to rounding. Steps are short enough for +// about 0.3 rad of gyration and half an LED of travel. A step length read +// straight off the field would break the pusher's symmetry in time, and the +// magnetic moment then drifted by a quarter in 20 s; so the step density +// follows Hairer and Söderlind's reversible rule, the mean of the densities +// either side of a position equal to the one the field there asks for. The +// time a particle runs past its frame is taken off its next one. Every step +// leaves its light on the panel, weighted by the time it took and by depth. +function push(S, i, T) { + const b = S.b, k = S.k, sg = S.sg[i], A = sg > 0 ? S.tp : S.tn, esc = max(1.6 * LMAX, S.cst); + let x = S.x[i], y = S.y[i], z = S.z[i], vx = S.vx[i], vy = S.vy[i], vz = S.vz[i], rh = S.rh[i], n = 0; + T += S.tc[i]; + while (T > 0 && n++ < 6000) { + field(x, y, z, k, b); + const bm = sqrt(b[0] * b[0] + b[1] * b[1] + b[2] * b[2]), g = max(bm / DPHI, 1 / DMAX); + let rn = 2 * g - rh; + if (rn < 0.5 * g) rn = g; + const h = 1 / rn, hm = 0.5 * (h + 1 / rh); + rh = rn; + const tx = 0.5 * sg * hm * b[0], ty = 0.5 * sg * hm * b[1], tz = 0.5 * sg * hm * b[2], q = 2 / (1 + tx * tx + ty * ty + tz * tz); + const wx = vx + vy * tz - vz * ty, wy = vy + vz * tx - vx * tz, wz = vz + vx * ty - vy * tx; + vx += q * (wy * tz - wz * ty); + vy += q * (wz * tx - wx * tz); + vz += q * (wx * ty - wy * tx); + x += vx * h; + y += vy * h; + z += vz * h; + T -= h; + const r2 = x * x + y * y + z * z; + const u = CT * x - ST * z, dz = ST * x + CT * z; + if (r2 < RS * RS) { + // Into the sphere: a flash where it lands, dimmer on the far side, + // and a new particle. + const fw = dz > -2 ? 1 : 0.4; + splat(S.fx, CX + u, CY - y, 4 * fw); + for (let j = 0; j < 4; j++) splat(S.fx, CX + u + (j < 2 ? 2 * j - 1 : 0), CY - y + (j < 2 ? 0 : 2 * j - 5), 1.5 * fw); + S.lost++; + inject(S, i); + return; + } + if (r2 > esc * esc) { + S.esc++; + inject(S, i); + return; + } + if (dz < 0 && u * u + y * y < RS * RS) continue; + splat(A, CX + u, CY - y, h * (0.45 + 0.55 * cl(0.5 + dz / (1.6 * LMAX), 0, 1))); + } + S.tc[i] = min(T, 0); + S.rh[i] = rh; + S.x[i] = x; + S.y[i] = y; + S.z[i] = z; + S.vx[i] = vx; + S.vy[i] = vy; + S.vz[i] = vz; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0]; + S.speed = cl(kv[1], 0.2, 3); + const en = cl(kv[2], 0, 1), pitch = cl(kv[3], 0, 90); + // Energy: the field strength k sets the Størmer length sqrt(k) against + // which the outermost shell sits at sOf(Energy). Changing it changes every + // gyration at once, as a change of energy would. + S.en = en; + S.cst = LMAX / sOf(en); + S.k = S.cst * S.cst; + if (!S.started || abs(pitch - S.pitch) > 0.5) { + S.pitch = pitch; + if (!S.started) for (let i = 0; i < NP; i++) inject(S, i); + else S.want.fill(1); + S.started = true; + } + // A new pitch reaches the particles four a frame, each put back on its + // shell with the new pitch angle. + for (let i = 0, m = 0; i < NP && m < 4; i++) if (S.want[i]) inject(S, i), m++; + const T = d * S.speed * RATE, f = exp(-d / 0.32), g = exp(-d / 0.25); + const tp = S.tp, tn = S.tn, fx = S.fx; + for (let i = 0; i < N; i++) (tp[i] *= f), (tn[i] *= f), (fx[i] *= g); + for (let i = 0; i < NP; i++) push(S, i, T); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const c = S.c, e = S.d, cp = hsv(0.13 + S.hue, 0.75, 1, [0, 0, 0]), cn = hsv(0.56 + S.hue, 0.8, 1, [0, 0, 0]); + hsv(0.69 + S.hue * 0.3, 0.55, 1, e); + const tp = S.tp, tn = S.tn, fx = S.fx, fl = S.fl; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, dx = x - CX, dy = y - CY, r2 = dx * dx + dy * dy; + const a = 1 - exp(-0.9 * tp[p]), b = 1 - exp(-0.9 * tn[p]), w = max(0, a + b - 1.1) * 0.6 + 0.9 * (1 - exp(-fx[p])); + let r = a * cp[0] + b * cn[0], gg = a * cp[1] + b * cn[1], bb = a * cp[2] + b * cn[2]; + // Field lines underneath, and the sphere as a dark disc with a dim rim. + const fv = r2 < RS * RS ? 0 : 0.16 * fl[p] * (1 - min(1, a + b)); + const rim = abs(sqrt(r2) - RS + 0.3) < 0.7 ? 0.14 : 0; + r += fv * e[0] + rim + w; + gg += fv * e[1] + rim + w; + bb += fv * e[2] + rim + w; + display.setPixel(x, y, min(255, r * 255) | 0, min(255, gg * 255) | 0, min(255, bb * 255) | 0); + } +} From 0c9c1bdc433102622fd837a84cd4adf21bfdf23d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 02:48:11 +0000 Subject: [PATCH 03/20] =?UTF-8?q?54=ED=8C=90:=20nodal-domains=20=EC=A7=93?= =?UTF-8?q?=EA=B8=B0?= 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_01CtQeRUJHSr3xVvLYqdqFXC --- .../pattern-study/patterns/nodal-domains.js | 309 ++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 incoming/pattern-study/patterns/nodal-domains.js diff --git a/incoming/pattern-study/patterns/nodal-domains.js b/incoming/pattern-study/patterns/nodal-domains.js new file mode 100644 index 000000000..e3d0ba606 --- /dev/null +++ b/incoming/pattern-study/patterns/nodal-domains.js @@ -0,0 +1,309 @@ +// ===== Patternflow pattern ===== +// Title: Nodal Domains +// Family: noise +// Idea: Berry's random wave, 40 plane waves of one wavelength in random directions each drifting at its own frequency, cut at a level into its nodal domains, each domain one flat colour that stays with it from frame to frame and runs from rose to gold above the level and from violet to cyan below as the domain grows, the largest one pale, while a white spark marks each saddle where two domains just merged or one just split. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: random wave model (Michael Berry, 1977), with the percolation picture of nodal domains (Eugene Bogomolny and Charles Schmit, 2002) +// Round: 54 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Wavenumber=0..1, Level=-1..1 +// +// Knob 1: Hue. Turns every colour around the wheel together. At 0 the domains above the level run from rose through orange to gold and those below from blue violet to cyan, with the largest domain cream or sky blue; at 0.25 above is lime to green and below pink to violet, at 0.5 the two sides trade warm and cool. +// Knob 2: Speed. Each wave turns its phase at its own rate, drawn from a normal law with a spread of 0.3 radians a second per unit: 0.6 at the opening 2, where a domain changes shape within a second or two and the panel sees about 55 merges and as many splits a second; 0.06 at 0.2, and 0.9 at 3. +// Knob 3: Wavenumber. The wavelength of all 40 waves, 8 LEDs at 0 and 5.5 at 1, zooming about the middle of the panel. Bogomolny and Schmit's count for an area the size of the panel goes from 25 domains to 53; the panel shows about 54 and 95, because its edges cut domains into pieces. +// Knob 4: Level. Where psi is cut, in units of its standard deviation; it opens at 0.06, next to the critical level 0, where domains of both signs can reach across the panel. At 1 only 16% of the panel lies above the level, the warm side breaks into about 100 islands and one cool domain holds 85% of the panel; at -1 the same with the sides swapped. + +const W = 64, H = 128, N = W * H, J = 40, MAXID = 4096; +const { floor, abs, sqrt, sin, cos, log, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// Bogomolny and Schmit: nodal domains per unit of k^2 / (4 pi) of area, +// which is the number of eigenvalues below k^2 per unit area (Weyl). +const BS = (3 * sqrt(3) - 5) / PI; + +// Colour follows a domain's size. Above the level a domain runs from rose +// when it is a single lobe of a few LEDs through orange to gold when it is +// as big as 1,500 LEDs; below the level from blue violet through blue to +// cyan. Each domain keeps a small hue offset of its own, and the largest +// domain on the panel is drawn in a light tint, cream above the level and +// sky blue below. +const RAMP = [[0.97, 0.8, 1, 0.13, 0.95, 1], [0.7, 0.75, 0.95, 0.5, 0.9, 0.95]]; +const PALE = [[0.57, 0.5, 1], [0.14, 0.38, 1]]; +const A0 = log(8), A1 = log(1500); + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + o[1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + o[2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; + return o; +} + +const gauss = () => sqrt(-2 * log(1 - Math.random())) * cos(2 * PI * Math.random()); + +export function setup(params) { + const S = (params.s = { + hue: 0, speed: 2, wn: 0, lev: 0.06, k: -1, nc: 0, count: 0, bulk: 0, + th: new Float64Array(J), ph: new Float64Array(J), om: new Float64Array(J), + AR: new Float64Array(W * J), AI: new Float64Array(W * J), CR: new Float64Array(J), CI: new Float64Array(J), SR: new Float64Array(J), SI: new Float64Array(J), + V: new Float64Array(N), sg: new Uint8Array(N), par: new Int32Array(N), comp: new Int32Array(N), + area: new Int32Array(N), csign: new Uint8Array(N), cid: new Int32Array(N), + id: new Int32Array(N).fill(-1), fl: new Float32Array(N), dec: new Uint8Array(N), dec0: new Uint8Array(N), + idSign: new Uint8Array(MAXID), idHue: new Float64Array(MAXID), idLA: new Float64Array(MAXID), bigId: -1, used: new Uint8Array(MAXID), taken: new Uint8Array(MAXID), + free: [], pairs: new Map(), c: [0, 0, 0], + }); + for (let i = MAXID - 1; i >= 0; i--) S.free.push(i); + // Wave directions spread over half a turn, one in each of J equal slots at + // a random place in it; phases uniform; each wave its own frequency. + for (let j = 0; j < J; j++) { + S.th[j] = (PI * (j + Math.random())) / J; + S.ph[j] = 2 * PI * Math.random(); + S.om[j] = gauss(); + } +} + +function find(par, p) { + while (par[p] !== p) p = par[p] = par[par[p]]; + return p; +} +function union(par, a, b) { + a = find(par, a); + b = find(par, b); + if (a < b) par[b] = a; + else if (b < a) par[a] = b; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0]; + S.speed = cl(kv[1], 0.2, 3); + S.wn = cl(kv[2], 0, 1); + S.lev = cl(kv[3], -1, 1); + // Wavelength in LEDs: 8 at 0, 5.5 at 1. + const lam = 8 / (1 + 0.4545 * S.wn), k = (2 * PI) / lam; + const AR = S.AR, AI = S.AI, CR = S.CR, CI = S.CI, SR = S.SR, SI = S.SI, th = S.th, ph = S.ph, om = S.om; + if (k !== S.k) { + S.k = k; + for (let j = 0; j < J; j++) { + SR[j] = cos(k * sin(th[j])); + SI[j] = sin(k * sin(th[j])); + for (let x = 0; x < W; x++) { + const a = k * cos(th[j]) * (x - (W - 1) / 2); + AR[x * J + j] = cos(a); + AI[x * J + j] = sin(a); + } + } + } + // psi = sum_j a cos(k (x cos th + y sin th) + ph), the real part of a + // column factor times a row factor; a = sqrt(2 / J) gives variance 1. The + // row factor starts at the top row and turns by k sin th from row to row. + const amp = sqrt(2 / J), V = S.V; + for (let j = 0; j < J; j++) { + ph[j] += d * S.speed * 0.3 * om[j]; + const b = (-k * sin(th[j]) * (H - 1)) / 2 + ph[j]; + CR[j] = amp * cos(b); + CI[j] = amp * sin(b); + } + for (let y = 0; y < H; y++) { + if (y) + for (let j = 0; j < J; j++) { + const r = CR[j] * SR[j] - CI[j] * SI[j]; + CI[j] = CR[j] * SI[j] + CI[j] * SR[j]; + CR[j] = r; + } + // The column factor's real part is even about the middle column and its + // imaginary part odd, so one pair of sums serves a column and its mirror. + const r = y * W; + for (let x = 0; x < W / 2; x++) { + const o = x * J; + let e = 0, q = 0; + for (let j = 0; j < J; j++) { + e += AR[o + j] * CR[j]; + q += AI[o + j] * CI[j]; + } + V[r + x] = e - q; + V[r + W - 1 - x] = e + q; + } + } + label(S); + match(S, d); + // What Bogomolny and Schmit expect for an area of the panel's size, far + // from any edge. + S.bulk = (BS * N * k * k) / (4 * PI); +} + +// Components of {psi > c} and of {psi < c}: 4-neighbours of one sign join, +// and in a 2 by 2 cell whose corners alternate in sign the value of the +// bilinear surface at its saddle decides which diagonal pair is joined (the +// asymptotic decider), so these are the domains of the bilinear surface +// through the LEDs. +function label(S) { + const V = S.V, sg = S.sg, par = S.par, c = S.lev, dec = S.dec0; + // dec keeps which diagonal each cell joined, 1 or 2, for the last two + // frames, so match can tell where a saddle crossed the level. + S.dec0 = S.dec; + S.dec = dec; + dec.fill(0); + for (let p = 0; p < N; p++) { + sg[p] = V[p] > c ? 1 : 0; + par[p] = p; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, s = sg[p]; + if (x < W - 1 && sg[p + 1] === s) union(par, p, p + 1); + if (y < H - 1) { + if (sg[p + W] === s) union(par, p, p + W); + const q = p + W + 1; + if (x < W - 1 && sg[q] === s && sg[p + 1] !== s && sg[p + W] !== s) { + const a = V[p] - c, b = V[p + 1] - c, e = V[p + W] - c, f = V[q] - c; + const sad = (a * f - b * e) / (a + f - b - e); + if (sad > 0 === (s === 1)) union(par, p, q), (dec[p] = 1); + else union(par, p + 1, p + W), (dec[p] = 2); + } + } + } + const comp = S.comp, area = S.area, csign = S.csign; + let nc = 0; + // Each root is the lowest LED of its domain, so it comes first. + for (let p = 0; p < N; p++) { + const r = find(par, p); + if (r === p) { + comp[p] = nc; + area[nc] = 0; + csign[nc++] = sg[p]; + } else comp[p] = comp[r]; + area[comp[p]]++; + } + S.nc = S.count = nc; + let big = 0; + for (let i = 1; i < nc; i++) if (area[i] > area[big]) big = i; + S.big = big; +} + +// Each domain takes over the colour of the old domain of its sign that it +// overlaps most, largest overlaps first. So a domain that splits keeps its +// colour on the larger piece, and of two that merge the one with the larger +// overlap keeps its colour. +function match(S, d) { + const comp = S.comp, id = S.id, sg = S.sg, idSign = S.idSign, pairs = S.pairs, nc = S.nc, cid = S.cid, taken = S.taken, used = S.used; + const fl = S.fl, decay = exp(-4 * d); + for (let p = 0; p < N; p++) fl[p] *= decay; + if (S.k === S.k0) saddles(S); + S.k0 = S.k; + pairs.clear(); + let run = 0, key = -1; + for (let p = 0; p <= N; p++) { + const o = p < N ? id[p] : -1; + const kk = o >= 0 && idSign[o] === sg[p] ? comp[p] * MAXID + o : -1; + if (kk !== key || p === N) { + if (key >= 0) pairs.set(key, (pairs.get(key) || 0) + run); + key = kk; + run = 0; + } + run++; + } + const list = [...pairs].sort((a, b) => b[1] - a[1]); + cid.fill(-1, 0, nc); + taken.fill(0); + for (const [kk] of list) { + const ci = floor(kk / MAXID), o = kk % MAXID; + if (cid[ci] < 0 && !taken[o]) (cid[ci] = o), (taken[o] = 1); + } + for (let o = 0; o < MAXID; o++) if (used[o] && !taken[o]) (used[o] = 0), S.free.push(o); + for (let i = 0; i < nc; i++) + if (cid[i] < 0 && S.free.length) { + const o = S.free.pop(); + used[o] = 1; + cid[i] = o; + idSign[o] = S.csign[i]; + S.idHue[o] = 0.05 * (Math.random() - 0.5); + S.idLA[o] = log(S.area[i]); + } + for (let p = 0; p < N; p++) id[p] = cid[comp[p]]; + // The size a colour shows eases toward the true size in about a quarter + // of a second, so a big domain that touches a neighbour at one saddle and + // lets go again does not flicker. The pale mark stays on the domain that + // has it until another one is a quarter bigger. + const e = 1 - exp(-d / 0.25), area = S.area; + let bi = -1, top = 0; + for (let i = 0; i < nc; i++) { + const o = cid[i]; + S.idLA[o] += (log(area[i]) - S.idLA[o]) * e; + if (o === S.bigId) bi = i; + if (area[i] > top) top = area[i]; + } + if (bi < 0 || area[bi] * 1.25 < top) S.bigId = cid[S.big]; +} + +// A merge or a split happens where a saddle of psi crosses the level: an LED +// changes sign, or a cell's saddle value does. Around each such place, two +// LEDs of one sign that were in different domains last frame and are in one +// now mark a merge, and two that were in one and are now apart mark a split. +// The 3 by 3 LEDs around it flash white. +function saddles(S) { + const id = S.id, sg = S.sg, comp = S.comp, idSign = S.idSign, dec = S.dec, dec0 = S.dec0, fl = S.fl; + let n = 0; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, o = id[p]; + if (!((o >= 0 && idSign[o] !== sg[p]) || (dec[p] && dec0[p] && dec[p] !== dec0[p]))) continue; + const x0 = x > 0 ? x - 1 : 0, x1 = x < W - 1 ? x + 1 : x, y0 = y > 0 ? y - 1 : 0, y1 = y < H - 1 ? y + 1 : y; + let hit = false; + for (let q = 0; q < 9 && !hit; q++) { + const a = x - 1 + (q % 3), b = y - 1 + floor(q / 3); + if (a < x0 || a > x1 || b < y0 || b > y1) continue; + const u = b * W + a, ou = id[u]; + if (ou < 0 || idSign[ou] !== sg[u]) continue; + for (let r = q + 1; r < 9; r++) { + const a2 = x - 1 + (r % 3), b2 = y - 1 + floor(r / 3); + if (a2 < x0 || a2 > x1 || b2 < y0 || b2 > y1) continue; + const v = b2 * W + a2, ov = id[v]; + if (ov < 0 || sg[v] !== sg[u] || idSign[ov] !== sg[v]) continue; + if ((ou !== ov) === (comp[u] === comp[v])) { + hit = true; + break; + } + } + } + if (hit) { + if (fl[p] < 1) n++; + for (let b = y0; b <= y1; b++) for (let a = x0; a <= x1; a++) fl[b * W + a] = 1; + } + } + S.events = n; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const V = S.V, id = S.id, comp = S.comp, fl = S.fl, c = S.c, lev = S.lev, nc = S.nc; + const R = S.R || (S.R = new Float32Array(N * 3)); + for (let i = 0; i < nc; i++) { + const o = S.cid[i], s = S.idSign[o]; + if (S.cid[i] === S.bigId) hsv(PALE[s][0] + S.hue, PALE[s][1], PALE[s][2], c); + else { + const u = cl((S.idLA[o] - A0) / (A1 - A0), 0, 1), P = RAMP[s ? 0 : 1]; + hsv(P[0] + (P[3] - P[0] + (s ? 1 : 0)) * u + S.idHue[o] + S.hue, P[1] + (P[4] - P[1]) * u, P[2] + (P[5] - P[2]) * u, c); + } + R[i * 3] = c[0]; + R[i * 3 + 1] = c[1]; + R[i * 3 + 2] = c[2]; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, v = V[p] - lev; + // Distance to the level line in LEDs, the value over the gradient; the + // line is drawn dark, about one LED wide. + const gx = ((x < W - 1 ? V[p + 1] : V[p]) - (x > 0 ? V[p - 1] : V[p])) / (x > 0 && x < W - 1 ? 2 : 1); + const gy = ((y < H - 1 ? V[p + W] : V[p]) - (y > 0 ? V[p - W] : V[p])) / (y > 0 && y < H - 1 ? 2 : 1); + const f = cl((abs(v) / (sqrt(gx * gx + gy * gy) + 1e-6) - 0.2) / 0.6, 0, 1); + // A spark lights the saddle white, the line through it included. + const q = comp[p] * 3, w = 0.85 * fl[p], g = f * (1 - w), h = 255 * w; + display.setPixel(x, y, (R[q] * g + h) | 0, (R[q + 1] * g + h) | 0, (R[q + 2] * g + h) | 0); + } +} From 39e3f8f9037e45fd156712b186143cbd4303d293 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 02:49:29 +0000 Subject: [PATCH 04/20] =?UTF-8?q?54=ED=8C=90:=20pearcey-cusp=20=EC=A7=93?= =?UTF-8?q?=EA=B8=B0?= 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_01CtQeRUJHSr3xVvLYqdqFXC --- .../pattern-study/patterns/pearcey-cusp.js | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 incoming/pattern-study/patterns/pearcey-cusp.js diff --git a/incoming/pattern-study/patterns/pearcey-cusp.js b/incoming/pattern-study/patterns/pearcey-cusp.js new file mode 100644 index 000000000..2d5004502 --- /dev/null +++ b/incoming/pattern-study/patterns/pearcey-cusp.js @@ -0,0 +1,184 @@ +// ===== Patternflow pattern ===== +// Title: Pearcey Cusp +// Family: wave +// Idea: The Pearcey integral, the wave field at the tip of a cusp caustic, computed at every LED: from a bright focus near the top its intensity opens downward into a lattice of bright spots whose wavefronts flow down the panel, and each exact zero of the field, a phase vortex, carries a mark coloured by the way its phase turns. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Pearcey integral (Trevor Pearcey, 1946), the cusp diffraction catastrophe (Michael Berry and Colin Upstill, 1980) +// Round: 54 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Wavenumber=-1..1, Tilt=-1..1 +// +// Knob 1: Hue. The colour of the light. At 0 it is green inside the ray caustic, where three rays cross, and teal outside it, where one ray passes; a zero is marked pink when the phase climbs clockwise round it and amber when it climbs anticlockwise. At 0.25 the light is violet and at 0.5 rose; 0.75 turns it gold. +// Knob 2: Speed. How fast the phase turns: 0.4 turns a second for each unit, 0.8 at the opening 2. The crests run down the panel and wheel anticlockwise round a pink zero, clockwise round an amber one. +// Knob 3: Wavenumber. The wavenumber k in octaves around the opening view, with the ray caustic held still: at 0 the panel spans x from -12 to 12 and y from 2 to -10 and holds 76 zeros; at -1 it is 0.59 of that across and 0.71 down, 18 zeros; at 1 it is 1.68 across and 1.41 down, about 285 zeros, packed as close as the LEDs can mark them. +// Knob 4: Tilt. Adds a t^3 term to the phase, up to 1.5 t^3 at either end. A shift of t removes it again, so the field is the same Pearcey field on sheared axes: the cusp's axis leans 21 LEDs left at 1 or right at -1 by the bottom row, and its tip rises 9 rows. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, sqrt, sin, cos, exp, round, atan2, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The opening view in Pearcey units: x across, y down the panel. +const XH = 12, YT = 2, YB = -10; + +// Quadrature nodes. P(x, y) is the integral over real t of +// exp(i (t^4 + y t^2 + x t)). Cauchy lets the path leave the real axis, and +// on t(s) = s + i K s^3 / (s^2 + C^2) the integrand is real-axis-like near +// the stationary points and dies as exp(-s^4) far out. The integrand is +// analytic along the path, so the trapezoid rule converges geometrically: +// 169 nodes agree with an 80-digit sum on the path t = s e^(i pi/8) to +// 3e-10 of the field's size at 40 LEDs, tilted views among them. +const HS = 0.06, CC = 5, KK = 0.5, SM = 5, J = 2 * Math.ceil(SM / HS) + 1; +const SR = new Float64Array(J), SI = new Float64Array(J), WR = new Float64Array(J), WI = new Float64Array(J); +for (let j = 0; j < J; j++) { + const s = (j - (J - 1) / 2) * HS, d = s * s + CC * CC; + SR[j] = s; + SI[j] = (KK * s * s * s) / d; + // dt = t'(s) ds + WR[j] = HS; + WI[j] = (HS * KK * s * s * (s * s + 3 * CC * CC)) / (d * d); +} + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + o[1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + o[2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; + return o; +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, kn: NaN, tl: NaN, ph: 0, + re: new Float64Array(N), im: new Float64Array(N), amp: new Float32Array(N), ur: new Float32Array(N), ui: new Float32Array(N), + zs: new Int8Array(N), nz: 0, rays: new Float32Array(N), xs: new Float64Array(W), ys: new Float64Array(H), + Er: new Float64Array(W * J), Ei: new Float64Array(W * J), Br: new Float64Array(J), Bi: new Float64Array(J), c: [0, 0, 0], + }; +} + +// The whole field for wavenumber knob kn and tilt knob tl. Scaling the +// wavenumber k by 2^kn scales x by k^(3/4) and y by k^(1/2), which keeps the +// ray caustic 8 y^3 + 27 x^2 = 0 at the same LEDs. The cubic coefficient +// scales by k^(1/4), which keeps the lean the same. +function field(S, kn, tl) { + const s = 2 ** (kn / 2), sx = s ** 1.5, a = 1.5 * tl * sqrt(s), q = a / 4; + const { re, im, xs, ys, Er, Ei, Br, Bi } = S; + for (let m = 0; m < W; m++) xs[m] = sx * XH * ((2 * m + 1) / W - 1); + for (let r = 0; r < H; r++) ys[r] = s * (YT + ((YB - YT) * (r + 0.5)) / H); + // The path shifted left by a/4 is the one that suits the t^3 term, since + // t = u - a/4 turns t^4 + a t^3 into u^4 plus lower powers. + for (let m = 0; m < W; m++) + for (let j = 0; j < J; j++) { + const x = xs[m], e = exp(-x * SI[j]), ang = x * (SR[j] - q); + Er[m * J + j] = e * cos(ang); + Ei[m * J + j] = e * sin(ang); + } + for (let r = 0; r < H; r++) { + const y = ys[r]; + for (let j = 0; j < J; j++) { + const tr = SR[j] - q, ti = SI[j]; + const t2r = tr * tr - ti * ti, t2i = 2 * tr * ti, t3r = t2r * tr - t2i * ti, t3i = t2r * ti + t2i * tr; + const pr = t2r * t2r - t2i * t2i + a * t3r + y * t2r, pi = 2 * t2r * t2i + a * t3i + y * t2i, g = exp(-pi); + const fr = g * cos(pr), fi = g * sin(pr); + Br[j] = fr * WR[j] - fi * WI[j]; + Bi[j] = fr * WI[j] + fi * WR[j]; + } + for (let m = 0; m < W; m++) { + let ar = 0, ai = 0; + for (let j = 0, o = m * J; j < J; j++, o++) { + const er = Er[o], ei = Ei[o], br = Br[j], bi = Bi[j]; + ar += br * er - bi * ei; + ai += br * ei + bi * er; + } + re[r * W + m] = ar; + im[r * W + m] = ai; + } + } + // Brightness: the amplitude |P| over 2.2, nearly linear, which is the + // square root of the intensity: the dim fan outside the cusp still reads + // while the focus, |P|^2 = 6.9 at most, does not swamp the lattice. + const { amp, ur, ui, zs, rays } = S; + for (let p = 0; p < N; p++) { + const A = sqrt(re[p] * re[p] + im[p] * im[p]); + amp[p] = min(1, (A / 2.2) ** 0.95); + ur[p] = A > 0 ? re[p] / A : 0; + ui[p] = A > 0 ? im[p] / A : 0; + } + // The ray picture, for the tint: on the sheared axes u = x - a y / 2 + + // a^3 / 8, v = y - 3 a^2 / 8 the caustic is 8 v^3 + 27 u^2 = 0. Inside it + // three rays cross each point, outside one. rays runs from 0 outside to 1 + // inside across about two LEDs, by the caustic's value over its gradient. + const ex = (2 * sx * XH) / W, ey = (s * (YT - YB)) / H; + for (let r = 0; r < H; r++) + for (let m = 0; m < W; m++) { + const y = ys[r], u = xs[m] - (a * y) / 2 + (a * a * a) / 8, v = y - (3 * a * a) / 8; + const f = 8 * v * v * v + 27 * u * u, gx = 54 * u * ex, gy = 24 * v * v * ey; + rays[r * W + m] = cl(0.5 - f / sqrt(gx * gx + gy * gy + 1e-9) / 2, 0, 1); + } + // Zeros: the phase winds by a whole turn round a square of four LEDs. + // The sign of the turn is the vortex's charge; the LED of the four with + // the least amplitude carries the mark. + zs.fill(0); + let nz = 0; + const wrap = (d) => d - 2 * PI * round(d / (2 * PI)); + for (let r = 0; r < H - 1; r++) + for (let m = 0; m < W - 1; m++) { + const p = r * W + m, c4 = [p, p + 1, p + W + 1, p + W]; + let w = 0; + for (let k = 0; k < 4; k++) { + const u = c4[k], v = c4[(k + 1) & 3]; + w += wrap(atan2(im[v], re[v]) - atan2(im[u], re[u])); + } + const n = round(w / (2 * PI)); + if (n) { + let b = c4[0]; + for (let k = 1; k < 4; k++) if (amp[c4[k]] < amp[b]) b = c4[k]; + zs[b] = n > 0 ? 1 : -1; + nz++; + } + } + S.nz = nz; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + const kn = cl(k[2], -1, 1), tl = cl(k[3], -1, 1); + if (abs(kn - S.kn) > 1e-4 || abs(tl - S.tl) > 1e-4 || S.kn !== S.kn) { + S.kn = kn; + S.tl = tl; + field(S, kn, tl); + } + // A monochromatic wave keeps its intensity and turns its phase. P obeys + // dP/dy = -i d2P/dx2, the paraxial wave equation with y as minus the + // distance travelled, so the light runs down the panel; drawing the real + // part of P exp(i w t) moves each crest toward lower phase, down with it. + S.ph += d * S.speed * 0.4 * 2 * PI; + if (S.ph > 2 * PI) S.ph -= 2 * PI; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { amp, ur, ui, zs, rays, c } = S, cr = cos(S.ph), ci = sin(S.ph), h0 = 0.47 + S.hue; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, a = amp[p]; + if (zs[p]) { + // A vortex: pink when the phase climbs going clockwise round the + // square on the panel (right, down, left, up), amber the other way. + hsv(zs[p] > 0 ? h0 + 0.48 : h0 + 0.67, 0.75, 0.95, c); + } else { + // The real part of the turning field over its amplitude: 1 on a + // crest. Crests brighten and whiten, troughs dim. + const w = 0.5 + 0.5 * (ur[p] * cr - ui[p] * ci), w2 = w * w; + hsv(h0 + 0.12 * (1 - rays[p]) - 0.1 * w, 0.85 - 0.5 * a * w2, a * (0.4 + 0.6 * w2), c); + } + display.setPixel(x, y, c[0] | 0, c[1] | 0, c[2] | 0); + } +} From 011b9837fdf0d2fd620fc955ee10f5aa268df93f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 02:56:22 +0000 Subject: [PATCH 05/20] =?UTF-8?q?54=ED=8C=90:=20weaire-phelan=20=EC=A7=93?= =?UTF-8?q?=EA=B8=B0?= 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_01CtQeRUJHSr3xVvLYqdqFXC --- .../pattern-study/patterns/stormer-trap.js | 6 +- .../pattern-study/patterns/weaire-phelan.js | 294 ++++++++++++++++++ 2 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 incoming/pattern-study/patterns/weaire-phelan.js diff --git a/incoming/pattern-study/patterns/stormer-trap.js b/incoming/pattern-study/patterns/stormer-trap.js index f99505958..30bd0ebb7 100644 --- a/incoming/pattern-study/patterns/stormer-trap.js +++ b/incoming/pattern-study/patterns/stormer-trap.js @@ -173,8 +173,8 @@ function push(S, i, T) { const r2 = x * x + y * y + z * z; const u = CT * x - ST * z, dz = ST * x + CT * z; if (r2 < RS * RS) { - // Into the sphere: a flash where it lands, dimmer on the far side, - // and a new particle. + // Into the sphere: a flash where it lands (dimmer on the far side), + // then a new particle. const fw = dz > -2 ? 1 : 0.4; splat(S.fx, CX + u, CY - y, 4 * fw); for (let j = 0; j < 4; j++) splat(S.fx, CX + u + (j < 2 ? 2 * j - 1 : 0), CY - y + (j < 2 ? 0 : 2 * j - 5), 1.5 * fw); @@ -182,6 +182,8 @@ function push(S, i, T) { inject(S, i); return; } + // Past the Størmer length, or past 1.6 times the outermost shell if that + // is further, the particle has left the trap; a new one takes its place. if (r2 > esc * esc) { S.esc++; inject(S, i); diff --git a/incoming/pattern-study/patterns/weaire-phelan.js b/incoming/pattern-study/patterns/weaire-phelan.js new file mode 100644 index 000000000..b80b9676a --- /dev/null +++ b/incoming/pattern-study/patterns/weaire-phelan.js @@ -0,0 +1,294 @@ +// ===== Patternflow pattern ===== +// Title: Weaire-Phelan +// Family: space +// Idea: A tall column of 48 foam cells turns slowly in depth, each the Voronoi cell of a site of the A15 lattice, so that together they are the flat-faced Weaire-Phelan foam of green-teal pentagonal dodecahedra and fourteen-faced cells with two gold hexagons each, and a knob lowers a line above which Kelvin's truncated octahedra of the same volume, with violet squares, take their place. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Weaire-Phelan structure (Denis Weaire and Robert Phelan, 1993), against Kelvin's truncated octahedra (William Thomson, Lord Kelvin, 1887) +// Round: 54 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Kelvin=0..1, Gap=0..1 +// +// Knob 1: Hue. Turns every face colour round the wheel together. At 0 the pentagons of the fourteen-faced cells are blue-teal, those of the dodecahedra a greener teal, the hexagons gold and Kelvin's squares violet; at 0.5 the pentagons turn red and the hexagons blue. +// Knob 2: Speed. How fast the column turns about its upright axis: at 2 once in 16 s, at 0.2 once in two and a half minutes, at 3 once in 10.5 s. +// Knob 3: Kelvin. Lowers a line through the column. Below it the cells are Weaire and Phelan's, above it Kelvin's truncated octahedra with six squares and eight hexagons, each of 2,744 cubic LEDs, the mean of WP's two sizes (2,680 and 2,765). At 0 the column is all WP, at 0.5 its top half is Kelvin's, at 1 all of it. With flat faces the walls come to 5.2950 per cell of unit volume for WP and 5.3147 for Kelvin, 0.37% more. +// Knob 4: Gap. Shrinks every cell about its own site, to half its size at 1, so the foam opens up. At 0 the walls meet and at 0.06 they part by a hairline; at 0.5 the inner stack of fourteen-faced cells shows through the gaps, and at 1 each cell stands apart and its faces can be counted. + +const W = 64, H = 128, N = W * H, CX = 31.5, CY = 63.5; +const { floor, min, max, abs, sin, cos, hypot } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + o[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + o[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; + return o; +} + +// The sites of one unit cube. A15 (the beta-tungsten structure): its Voronoi +// cells are the flat-faced Weaire-Phelan foam. BCC: its Voronoi cells are +// Kelvin's truncated octahedra. +const A15 = [[0, 0, 0], [0.5, 0.5, 0.5], [0.25, 0.5, 0], [0.75, 0.5, 0], [0, 0.25, 0.5], [0, 0.75, 0.5], [0.5, 0, 0.25], [0.5, 0, 0.75]]; +const BCC = [[0, 0, 0], [0.5, 0.5, 0.5]]; +// The WP cube edge in LEDs. Kelvin's cube holds two cells to WP's eight, so +// for the same volume per cell its edge is the WP edge over the cube root of 4. +const AW = 28, AK = AW / Math.cbrt(4); + +const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; +const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +const crs = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; + +// The Voronoi cell of site i, relative to the site: a cube two cube edges +// wide, cut by the bisecting plane of every site of the periodic lattice +// within 1.5 cube edges, nearest first. Each cut keeps the part of every face +// on the site's side and closes the hole with a new face, its corners sorted +// by angle round their mean. +function voronoi(sites, i) { + const c = sites[i], E = 1e-10; + let F = []; + for (let ax = 0; ax < 3; ax++) + for (const sg of [-1, 1]) { + const u = (ax + 1) % 3, v = (ax + 2) % 3, q = []; + for (const [a, b] of [[-1, -1], [1, -1], [1, 1], [-1, 1]]) { + const p = [0, 0, 0]; + p[ax] = sg; + p[u] = a * sg; + p[v] = b; + q.push(p); + } + F.push(q); + } + const nb = []; + for (const s of sites) + for (let x = -2; x <= 2; x++) + for (let y = -2; y <= 2; y++) + for (let z = -2; z <= 2; z++) { + const d = [s[0] + x - c[0], s[1] + y - c[1], s[2] + z - c[2]], r2 = dot(d, d); + if (r2 > E && r2 < 2.25) nb.push([d, r2]); + } + nb.sort((a, b) => a[1] - b[1]); + // A plane further than the farthest corner cannot cut, nor can any after it. + let far = 3; + for (const [d, r2] of nb) { + if (r2 > 4 * far) break; + const side = (p) => dot(p, d) - r2 / 2, out = [], cut = []; + for (const f of F) { + const q = []; + for (let m = 0; m < f.length; m++) { + const A = f[m], B = f[(m + 1) % f.length], sa = side(A), sb = side(B); + if (sa <= E) q.push(A); + if (abs(sa) <= E) cut.push(A); + else if ((sa < 0) !== (sb < 0) && abs(sb) > E) { + const t = sa / (sa - sb), X = [A[0] + t * (B[0] - A[0]), A[1] + t * (B[1] - A[1]), A[2] + t * (B[2] - A[2])]; + q.push(X); + cut.push(X); + } + } + if (q.length >= 3) out.push(q); + } + if (cut.length >= 3) { + const m = [0, 1, 2].map((k) => cut.reduce((s, p) => s + p[k], 0) / cut.length), e1 = sub(cut[0], m), e2 = crs(d, e1), pts = []; + for (const p of cut) if (!pts.some((o) => hypot(...sub(o[0], p)) < 1e-7)) pts.push([p, Math.atan2(dot(sub(p, m), e2), dot(sub(p, m), e1))]); + pts.sort((a, b) => a[1] - b[1]); + if (pts.length >= 3) out.push(pts.map((o) => o[0])); + } + F = out; + far = 0; + for (const f of F) for (const p of f) far = max(far, dot(p, p)); + } + // Corners shared between faces, and each face as a list of corner indices + // with its outward unit normal. The cell's volume is summed as pyramids on + // its faces with their apex at the site. + const V = [], faces = []; + const idx = (p) => { + for (let k = 0; k < V.length; k++) if (hypot(...sub(V[k], p)) < 1e-7) return k; + V.push(p); + return V.length - 1; + }; + let vol = 0, area = 0; + for (const f of F) { + const ix = []; + for (const p of f) { + const k = idx(p); + if (ix[ix.length - 1] !== k && ix[0] !== k) ix.push(k); + } + let n = [0, 0, 0]; + for (let m = 1; m + 1 < ix.length; m++) n = n.map((v, t) => v + crs(sub(V[ix[m]], V[ix[0]]), sub(V[ix[m + 1]], V[ix[0]]))[t] / 2); + const ar = hypot(...n); + if (ix.length < 3 || ar < 1e-9) continue; + n = n.map((v) => v / ar); + if (dot(n, V[ix[0]]) < 0) ix.reverse(), (n = n.map((v) => -v)); + faces.push({ ix, n, nv: ix.length }); + area += ar; + vol += (ar * dot(n, V[ix[0]])) / 3; + } + // Each edge once, with the two faces that meet there. + const edges = [], seen = new Map(); + faces.forEach((f, j) => { + for (let m = 0; m < f.nv; m++) { + const a = f.ix[m], b = f.ix[(m + 1) % f.nv], key = min(a, b) * 1000 + max(a, b); + if (seen.has(key)) edges[seen.get(key) + 3] = j; + else seen.set(key, edges.length), edges.push(a, b, j, j); + } + }); + return { V, faces, edges, vol, area }; +} + +export function setup(params) { + const S = (params.s = { hue: 0, speed: 2, kel: 0, gap: 0.06, th: 0.6, inst: [], cells: [], check: null }); + // The base cells, eight of WP and two of Kelvin. S.check keeps what the + // theorem is about: each cell's volume and face sizes, and the mean surface + // of a cell scaled to unit volume, the number Kelvin's problem is quoted in + // (each wall then counts once for each of the two cells it parts). + const base = [A15, BCC].map((sites) => sites.map((_, i) => voronoi(sites, i))); + const ck = (S.check = {}); + base.forEach((cells, k) => { + const v = cells.reduce((s, c) => s + c.vol, 0), a = cells.reduce((s, c) => s + c.area, 0), n = cells.length; + ck[k ? "kelvin" : "wp"] = { volumes: cells.map((c) => c.vol), faces: cells.map((c) => c.faces.map((f) => f.nv)), total: v, areaPerUnitVolume: a / n / (v / n) ** (2 / 3) }; + }); + // The column: every site within a radius of a vertical axis and a height + // of the middle, for each lattice. WP's axis runs up a stack of + // fourteen-faced cells joined by their hexagons; Kelvin's up a line of sites. + const pick = (sites, cells, a, ax, az, R, kind) => { + for (let i = 0; i < sites.length; i++) + for (let x = -3; x <= 3; x++) + for (let y = -6; y <= 6; y++) + for (let z = -3; z <= 3; z++) { + const s = [sites[i][0] + x - ax, sites[i][1] + y - 0.5, sites[i][2] + z - az]; + if (hypot(s[0], s[2]) > R || abs(s[1]) * a > 48) continue; + const n = cells[i].V.length; + S.inst.push({ c: cells[i], kind, x: s[0] * a, y: s[1] * a, z: s[2] * a, a, on: false, fr: new Uint8Array(cells[i].faces.length), sx: new Float64Array(n), sy: new Float64Array(n), sz: new Float64Array(n) }); + } + }; + pick(A15, base[0], AW, 0, 0.5, 0.57, 0); + pick(BCC, base[1], AK, 0.25, 0.25, 0.8, 1); + S.PX = new Float64Array(16); + S.PY = new Float64Array(16); + S.PZ = new Float64Array(16); + S.zb = new Float32Array(N); + S.rgb = new Float32Array(N * 3); + S.col = [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.kel = cl(k[2], 0, 1); + S.gap = cl(k[3], 0, 1); + S.th += d * S.speed * 0.2; +} + +// The view: turned th about the vertical, then tipped EL toward the viewer +// so the tops of the cells show, with a little perspective from DCAM LEDs. +// A key light from the upper left and a weaker fill from the right. +const EL = 0.28, CE = cos(EL), SE = sin(EL), DCAM = 220; +// Light kept in the shade and overall gain for each face colour (see draw); +// hexagons keep more so their gold never sinks to brown. +const AMB = [0.14, 0.42, 0.22, 0.14], GAIN = [0.95, 1.05, 1, 0.95]; + +// Fills a convex polygon of n screen corners row by row, the depth taken +// from the plane through three of them; nearer pixels win. +function poly(S, X, Y, Z, n, r, g, b) { + let y0 = 1e9, y1 = -1e9; + for (let m = 0; m < n; m++) (y0 = min(y0, Y[m])), (y1 = max(y1, Y[m])); + const ux = X[1] - X[0], uy = Y[1] - Y[0], uz = Z[1] - Z[0], vx = X[2] - X[0], vy = Y[2] - Y[0], vz = Z[2] - Z[0], dn = ux * vy - uy * vx; + if (abs(dn) < 1e-6) return; + const zx = (uz * vy - uy * vz) / dn, zy = (ux * vz - uz * vx) / dn, zb = S.zb, rgb = S.rgb; + for (let y = max(0, Math.ceil(y0)), ye = min(H - 1, floor(y1)); y <= ye; y++) { + let xl = 1e9, xr = -1e9; + for (let m = 0; m < n; m++) { + const k = m + 1 < n ? m + 1 : 0, ya = Y[m], yb = Y[k]; + if ((ya <= y && yb >= y) || (yb <= y && ya >= y)) { + const x = ya === yb ? X[m] : X[m] + ((y - ya) * (X[k] - X[m])) / (yb - ya); + xl = min(xl, x); + xr = max(xr, x); + if (ya === yb) xr = max(xr, X[k]), (xl = min(xl, X[k])); + } + } + for (let x = max(0, Math.ceil(xl)), xe = min(W - 1, floor(xr)); x <= xe; x++) { + const p = y * W + x, z = Z[0] + zx * (x - X[0]) + zy * (y - Y[0]); + if (z <= zb[p]) continue; + // Darker with depth, pixel by pixel, so a face turning away fades + // along its length. + const f = cl(0.72 + z / 55, 0.3, 1); + zb[p] = z; + rgb[p * 3] = r * f; + rgb[p * 3 + 1] = g * f; + rgb[p * 3 + 2] = b * f; + } + } +} + +function line(S, ax, ay, az, bx, by, bz, lv) { + const n = Math.ceil(max(abs(bx - ax), abs(by - ay))) + 1, zb = S.zb, rgb = S.rgb; + for (let i = 0; i <= n; i++) { + const t = i / n, x = Math.round(ax + t * (bx - ax)), y = Math.round(ay + t * (by - ay)); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const p = y * W + x; + if (az + t * (bz - az) < zb[p] - 1.5) continue; + rgb[p * 3] = max(rgb[p * 3], lv); + rgb[p * 3 + 1] = max(rgb[p * 3 + 1], lv); + rgb[p * 3 + 2] = max(rgb[p * 3 + 2], lv); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const zb = S.zb, rgb = S.rgb, PX = S.PX, PY = S.PY, PZ = S.PZ; + zb.fill(-1e9); + rgb.fill(0); + const ct = cos(S.th), st = sin(S.th), sc = 1 - 0.5 * S.gap; + // Face colours: pentagons of the fourteen-faced cells, hexagons, squares + // and the pentagons of the dodecahedra, a little greener. + const col = S.col; + hsv(0.53 + S.hue, 0.75, 1, col[0]); + hsv(0.14 + S.hue, 0.6, 1, col[1]); + hsv(0.8 + S.hue, 0.6, 1, col[2]); + hsv(0.45 + S.hue, 0.65, 1, col[3]); + // The cut between the two structures, from above the top (all WP) at + // Kelvin 0 to below the bottom (all Kelvin) at 1. + const yc = 64 - 128 * S.kel, half = 8; + for (const it of S.inst) { + it.on = it.kind === 0 ? it.y < yc - half : it.y > yc + half; + if (!it.on) continue; + const c = it.c, V = c.V, a = it.a * sc, sx = it.sx, sy = it.sy, sz = it.sz, fr = it.fr; + for (let k = 0; k < V.length; k++) { + const x = it.x + a * V[k][0], y = it.y + a * V[k][1], z = it.z + a * V[k][2]; + const xr = ct * x + st * z, zr = -st * x + ct * z, yr = CE * y - SE * zr, zz = SE * y + CE * zr, f = DCAM / (DCAM - zz); + sx[k] = CX + xr * f; + sy[k] = CY - yr * f; + sz[k] = zz; + } + c.faces.forEach((fc, j) => { + const n = fc.n, nx = ct * n[0] + st * n[2], nzr = -st * n[0] + ct * n[2], ny = CE * n[1] - SE * nzr, nz = SE * n[1] + CE * nzr; + // Back faces away (seen from the camera, DCAM in front); the rest lit + // from the upper left and darker with depth. + const i0 = fc.ix[0], px = (sx[i0] - CX) * (DCAM - sz[i0]) / DCAM, py = (CY - sy[i0]) * (DCAM - sz[i0]) / DCAM; + fr[j] = nx * -px + ny * -py + nz * (DCAM - sz[i0]) > 0 ? 1 : 0; + if (!fr[j]) return; + const t = fc.nv === 5 ? (c.faces.length === 12 ? 3 : 0) : fc.nv === 6 ? 1 : 2; + const lam = max(0, -0.45 * nx + 0.6 * ny + 0.66 * nz), fill = max(0, 0.6 * nx - 0.1 * ny + 0.79 * nz); + const sh = min(1, AMB[t] + (1 - AMB[t]) * (0.8 * lam * lam + 0.3 * fill)) * GAIN[t], cc = col[t]; + for (let m = 0; m < fc.nv; m++) (PX[m] = sx[fc.ix[m]]), (PY[m] = sy[fc.ix[m]]), (PZ[m] = sz[fc.ix[m]]); + poly(S, PX, PY, PZ, fc.nv, cc[0] * sh, cc[1] * sh, cc[2] * sh); + }); + } + // Edges of the faces turned to the viewer, white, dimmer further back. + for (const it of S.inst) { + if (!it.on) continue; + const e = it.c.edges, sx = it.sx, sy = it.sy, sz = it.sz, fr = it.fr; + for (let m = 0; m < e.length; m += 4) { + if (!fr[e[m + 2]] && !fr[e[m + 3]]) continue; + const a = e[m], b = e[m + 1]; + line(S, sx[a], sy[a], sz[a], sx[b], sy[b], sz[b], cl(0.62 + (sz[a] + sz[b]) / 100, 0.35, 0.95)); + } + } + for (let p = 0; p < N; p++) display.setPixel(p % W, (p / W) | 0, min(255, rgb[p * 3] * 255) | 0, min(255, rgb[p * 3 + 1] * 255) | 0, min(255, rgb[p * 3 + 2] * 255) | 0); +} From 2bbede17dbc8bd7199290548f5a09c6b8128a167 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 02:57:25 +0000 Subject: [PATCH 06/20] =?UTF-8?q?54=ED=8C=90:=20arcsine-law=20=EC=A7=93?= =?UTF-8?q?=EA=B8=B0?= 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_01CtQeRUJHSr3xVvLYqdqFXC --- .../pattern-study/patterns/arcsine-law.js | 310 ++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 incoming/pattern-study/patterns/arcsine-law.js diff --git a/incoming/pattern-study/patterns/arcsine-law.js b/incoming/pattern-study/patterns/arcsine-law.js new file mode 100644 index 000000000..2939d51f9 --- /dev/null +++ b/incoming/pattern-study/patterns/arcsine-law.js @@ -0,0 +1,310 @@ +// ===== Patternflow pattern ===== +// Title: Arcsine Law +// Family: data +// Idea: 48 fair random walks come down the panel a row a step, each LED warm while its walk is above zero and cool below, and most columns turn out long stretches of one colour; after 126 steps the warm cells rise to the top of each column and the columns sort themselves by how many there are, so the boundary between the colours draws the arcsine quantile curve, while a histogram beside them gathers every walk so far into Lévy's U-shaped law, its exact values marked by white dots. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: arcsine laws (Paul Lévy, 1939) +// Round: 54 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Statistic=0..2, Drift=-1..1 +// +// Knob 1: Hue. Turns every colour around the wheel together. At 0 a walk above zero is amber and below zero azure, pale near zero and deeper the further out it is, with lavender histogram bars; at 0.25 green above and magenta below, at 0.5 the two sides trade colours. +// Knob 2: Speed. 16 rows a second per unit: 32 at the opening 2, so a batch of 126 steps comes down in 3.9 s, settles and sorts in 1.4 s and holds for 1.4 s; at 0.2 a batch takes 39 s to come down, at 3 it takes 2.6 s. Walks that are not drawn feed the histogram at 120 a second per unit. +// Knob 3: Statistic. Which of Lévy's three the columns sort by and the histogram counts. At 0 the time above zero: the warm cells rise to the top of their column. At 1 the last zero: the cells before each walk's last return to zero are dimmed, leaving a bright tail of one colour below it. At 2 the first time of the maximum: the cells after each walk's highest point so far are dimmed. All three histograms settle on the same U. +// Knob 4: Drift. A step goes up with chance (1 + mu) / 2. mu is 0 within 0.08 of the middle, so the opening 0.06 is the fair walk, then grows as the square to 0.014 at 0.3, 0.052 at 0.5 and 0.25 at either end, where nearly every column floods one colour. Any drift breaks the law: at 0.5 the histogram of time above zero lies 0.19 from it in Kolmogorov distance, while the last zero, which only asks whether the walk comes back, lies 0.047 from it. + +const W = 64, H = 128; +// 48 walks of 126 steps, one a column, step t drawn in row t - 1; then a +// dark column and the histogram, 15 columns wide, its bins two rows tall on +// the same time axis as the walks. +const NW = 48, N = 126, HX = 49, HL = 15, NB = 64; +const { floor, round, min, max, abs, sqrt, sin, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ease = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + o[1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + o[2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; + return o; +} + +// The exact laws for N = 126 steps, bin k of the histogram. The time spent +// above zero (counting a step as above when either end of it is) and the +// last zero both take the value 2k with probability u(2k) u(N - 2k), where +// u(2m) = C(2m, m) / 4^m is the chance of being at zero at time 2m (Chung +// and Feller, Sparre Andersen). The first time of the maximum is k with +// probability a(k) b(N - k): a(k) the chance a walk stays above zero for k +// steps, b(m) the chance it stays at or below zero for m, both by counting +// paths. Lévy's arcsine law is the limit of all three. +const U = new Float64Array(N + 1); +U[0] = 1; +for (let m = 1; 2 * m <= N; m++) U[2 * m] = (U[2 * m - 2] * (2 * m - 1)) / (2 * m); +function stay(strict) { + // out[k]: chance that S_1..S_k all stay above zero (strict) or at or + // below zero, by the distribution of the walk killed when it leaves. + const out = new Float64Array(N + 1), off = N + 1; + let p = new Float64Array(2 * N + 3), q = new Float64Array(2 * N + 3); + p[off] = 1; + out[0] = 1; + for (let k = 1; k <= N; k++) { + q.fill(0); + let tot = 0; + for (let x = 1; x < 2 * N + 2; x++) { + const v = 0.5 * (p[x - 1] + p[x + 1]); + const y = x - off; + if (strict ? y > 0 : y <= 0) (q[x] = v), (tot += v); + } + [p, q] = [q, p]; + out[k] = tot; + } + return out; +} +const PB = [new Float64Array(NB), new Float64Array(NB), new Float64Array(NB)]; +{ + const a = stay(true), b = stay(false); + for (let k = 0; k < NB; k++) PB[0][k] = PB[1][k] = U[2 * k] * U[N - 2 * k]; + for (let t = 0; t <= N; t++) PB[2][t >> 1] += a[t] * b[N - t]; +} +// The histogram's scale puts the third bin from either end at full length, +// so the two end bins, 2.6 and 1.3 times longer, run off the band. +const PFULL = PB[0][2]; + +export function setup(params) { + params.s = { + hue: 0, speed: 2, stat: 0, mu: 0, init: false, + // The batch on show: positions S[i * (N + 1) + t]. + S: new Int16Array(NW * (N + 1)), row: 0, phase: 0, clock: 0, + pos: new Int16Array(NW), lz: new Int16Array(NW), tm: new Int16Array(NW), top: new Int16Array(NW), + // Sorting: each walk's target column and each cell's target row. + X: new Int16Array(NW), R: new Int16Array(NW * N), sortStat: -1, + // The last sorted picture, shown dimmed below the new batch's front. + prev: new Float32Array(NW * N * 3), cell: new Float32Array(NW * N * 3), + hist: [new Float64Array(NB), new Float64Array(NB), new Float64Array(NB)], walks: 0, histMu: 0, + fb: new Float32Array(W * H * 3), c: [0, 0, 0], + }; +} + +// One walk's three statistics from its positions w[0..N]. +function stats(w, o, out) { + let pos = 0, lz = 0, tm = 0, top = 0; + for (let t = 1; t <= N; t++) { + const a = w[o + t - 1], b = w[o + t]; + if (a + b > 0) pos++; + if (b === 0) lz = t; + if (b > top) (top = b), (tm = t); + } + out[0] = pos; + out[1] = lz; + out[2] = tm; +} + +const tmpW = new Int16Array(N + 1), tmpS = [0, 0, 0]; +function hiddenWalk(S) { + const p = (1 + S.mu) / 2; + tmpW[0] = 0; + for (let t = 1; t <= N; t++) tmpW[t] = tmpW[t - 1] + (Math.random() < p ? 1 : -1); + stats(tmpW, 0, tmpS); + addStats(S, tmpS); +} +function addStats(S, st) { + S.hist[0][st[0] >> 1]++; + S.hist[1][st[1] >> 1]++; + S.hist[2][st[2] >> 1]++; + S.walks++; +} + +function stepRow(S) { + const t = S.row + 1, p = (1 + S.mu) / 2, A = S.S; + for (let i = 0; i < NW; i++) { + const o = i * (N + 1), b = (A[o + t] = A[o + t - 1] + (Math.random() < p ? 1 : -1)), a = A[o + t - 1]; + if (a + b > 0) S.pos[i]++; + if (b === 0) S.lz[i] = t; + if (b > S.top[i]) (S.top[i] = b), (S.tm[i] = t); + } + S.row = t; +} + +function newBatch(S) { + S.S.fill(0); + S.pos.fill(0); + S.lz.fill(0); + S.tm.fill(0); + S.top.fill(0); + S.row = 0; + S.phase = 0; + S.clock = 0; +} + +// The statistic each walk is sorted by, and where each cell goes: for the +// time above zero the cells above zero rise to the top of their column in +// their order and the rest sink below them; for the other two the cells +// stay in their rows. +function plan(S) { + const st = S.stat === 0 ? S.pos : S.stat === 1 ? S.lz : S.tm, A = S.S; + const idx = [...Array(NW).keys()].sort((a, b) => st[a] - st[b] || A[a * (N + 1) + N] - A[b * (N + 1) + N] || a - b); + idx.forEach((w, r) => (S.X[w] = r)); + for (let i = 0; i < NW; i++) { + const o = i * (N + 1); + let up = 0, down = S.pos[i]; + for (let t = 1; t <= N; t++) { + const above = A[o + t - 1] + A[o + t] > 0; + S.R[i * N + t - 1] = S.stat === 0 ? (above ? up++ : down++) : t - 1; + } + } + S.sortStat = S.stat; +} + +// A cell's colour: warm above zero and cool below, paler near zero and +// deeper the further the walk is out in units of sqrt(t); dimmed before +// the last zero or after the maximum when those are the statistic. +function shade(S, i, t, out) { + const o = i * (N + 1), a = S.S[o + t - 1], b = S.S[o + t], m = min(1, abs(a + b) / (2.2 * sqrt(t))); + if (a + b > 0) hsv(0.11 + S.hue, 0.3 + 0.7 * m, 1, out); + else hsv(0.6 + S.hue, 0.3 + 0.65 * m, 1, out); + let dim = false; + if (S.stat === 1) dim = t <= S.lz[i]; + else if (S.stat === 2) dim = t > S.tm[i]; + if (dim) { + const g = (out[0] + out[1] + out[2]) / 3; + for (let c = 0; c < 3; c++) out[c] = 0.2 * (0.4 * out[c] + 0.6 * g); + } + return out; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0]; + S.speed = cl(kv[1], 0.2, 3); + S.stat = cl(round(kv[2]), 0, 2); + // Drift: none within 0.08 of the middle, then growing as the square to + // 0.25 at either end; the chance of a step up is (1 + mu) / 2. + const k4 = cl(kv[3], -1, 1), e = max(0, (abs(k4) - 0.08) / 0.92); + S.mu = (k4 < 0 ? -1 : 1) * 0.25 * e * e; + if (S.mu !== S.histMu) { + for (const h of S.hist) h.fill(0); + S.walks = 0; + S.histMu = S.mu; + } + if (!S.init) { + // A first batch, run and sorted at once, is the picture the panel + // opens on while the next one comes down over it. + S.init = true; + newBatch(S); + while (S.row < N) stepRow(S); + plan(S); + keep(S, 0.75); + newBatch(S); + } + // Walks nobody sees feed the histogram, 8 a frame at Speed 2. + S.hidden = (S.hidden || 0) + (d * 30 * 8 * S.speed) / 2; + while (S.hidden >= 1) hiddenWalk(S), S.hidden--; + const rate = 16 * S.speed, slow = sqrt(2 / S.speed); + S.clock += d; + if (S.phase === 0) { + const want = min(N, floor(S.clock * rate)); + while (S.row < want) stepRow(S); + if (S.row >= N) { + for (let i = 0; i < NW; i++) addStats(S, [S.pos[i], S.lz[i], S.tm[i]]); + plan(S); + S.phase = 1; + S.clock = 0; + } + } else { + if (S.stat !== S.sortStat) plan(S), (S.clock = 0), (S.phase = 1); + if (S.phase === 1 && S.clock > 1.4 * slow) (S.phase = 2), (S.clock = 0); + if (S.phase === 2 && S.clock > 1.4 * slow) keep(S, 0.75), newBatch(S); + } +} + +// Store the sorted picture of the batch, dimmed by f, as the backdrop. +function keep(S, f) { + const P = S.prev, c = S.c; + P.fill(0); + for (let i = 0; i < NW; i++) + for (let t = 1; t <= N; t++) { + shade(S, i, t, c); + const q = (S.R[i * N + t - 1] * NW + S.X[i]) * 3; + P[q] = c[0] * f; + P[q + 1] = c[1] * f; + P[q + 2] = c[2] * f; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.init) return; + const fb = S.fb, c = S.c, P = S.prev; + fb.fill(0); + if (S.phase === 0) { + // The old picture below the front on every other LED, the new walks + // above it. + for (let r = S.row; r < N; r++) + for (let x = (r + 1) & 1; x < NW; x += 2) { + const q = (r * W + x) * 3, s = (r * NW + x) * 3; + fb[q] = P[s]; + fb[q + 1] = P[s + 1]; + fb[q + 2] = P[s + 2]; + } + for (let i = 0; i < NW; i++) + for (let t = 1; t <= S.row; t++) { + shade(S, i, t, c); + const q = ((t - 1) * W + i) * 3; + fb[q] = c[0]; + fb[q + 1] = c[1]; + fb[q + 2] = c[2]; + } + } else { + // Settle first, then slide the columns into their order. + const u = S.phase === 2 ? 1 : S.clock / (1.4 * sqrt(2 / S.speed)); + const ey = ease(u * 2), ex = ease(u * 2 - 1); + for (let i = 0; i < NW; i++) { + const x = round(i + (S.X[i] - i) * ex); + for (let t = 1; t <= N; t++) { + const y = round(t - 1 + (S.R[i * N + t - 1] - (t - 1)) * ey); + shade(S, i, t, c); + const q = (y * W + x) * 3; + fb[q] = c[0]; + fb[q + 1] = c[1]; + fb[q + 2] = c[2]; + } + } + } + // Once the columns stand in order, a dot in each marks where Lévy's law + // puts that rank: column r of 48 at N sin^2(pi (r + 1/2) / 96), the + // arcsine quantile. The boundary of the batch should hug it. + if (S.phase === 2) { + const v = 0.85 * min(1, S.clock / 0.4); + for (let r = 0; r < NW; r++) { + const sn = sin((PI * (r + 0.5)) / (2 * NW)), y = min(N - 1, floor(N * sn * sn)), q = (y * W + r) * 3; + for (let j = 0; j < 3; j++) fb[q + j] += (255 - fb[q + j]) * v; + } + } + // The histogram of the chosen statistic over every walk so far, and the + // exact law as a dot at the length each bar should reach. + const h = S.hist[S.stat], pb = PB[S.stat], n = max(1, S.walks), sc = HL / PFULL; + for (let k = 0; k < NB; k++) { + const len = (h[k] / n) * sc, ex = pb[k] * sc; + for (let j = 0; j < HL; j++) { + const f = cl(len - j, 0, 1), y0 = 2 * k; + let r = 0, g = 0, b = 0; + // The bar, the shortfall from the law dimmer, and the law's dot. + const sf = j >= len && j < ex - 0.5 ? 0.3 : 0; + if (f > 0 || sf) hsv(0.8 + S.hue, 0.35, f > 0 ? 0.3 + 0.6 * f : sf, c), (r = c[0]), (g = c[1]), (b = c[2]); + if (abs(j + 0.5 - ex) < 0.5) (r = 255), (g = 255), (b = 255); + for (let y = y0; y < y0 + 2; y++) { + const q = (y * W + HX + j) * 3; + fb[q] = r; + fb[q + 1] = g; + fb[q + 2] = b; + } + } + } + for (let p = 0; p < W * H; p++) display.setPixel(p % W, (p / W) | 0, fb[p * 3] | 0, fb[p * 3 + 1] | 0, fb[p * 3 + 2] | 0); +} From 8da1f4583b02ae6448fea57970498dd74158de19 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 02:57:52 +0000 Subject: [PATCH 07/20] =?UTF-8?q?54=ED=8C=90:=20smale-horseshoe=20?= =?UTF-8?q?=EC=A7=93=EA=B8=B0?= 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_01CtQeRUJHSr3xVvLYqdqFXC --- .../pattern-study/patterns/smale-horseshoe.js | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 incoming/pattern-study/patterns/smale-horseshoe.js diff --git a/incoming/pattern-study/patterns/smale-horseshoe.js b/incoming/pattern-study/patterns/smale-horseshoe.js new file mode 100644 index 000000000..ce1b8fd30 --- /dev/null +++ b/incoming/pattern-study/patterns/smale-horseshoe.js @@ -0,0 +1,232 @@ +// ===== Patternflow pattern ===== +// Title: Smale Horseshoe +// Family: fractal +// Idea: Smale's horseshoe map squeezes the square in the middle of the panel sideways and stretches it up past the top, then bends it back down over itself, fold after fold; the panel holds the folded ribbon of the fifth image, 32 strands about an LED wide that cross the square gold where the ribbon runs up and azure where it runs down, turning in nested arches above and nested U's below; where they cross the eight horizontal strips of points that stay in the square for three more folds they turn white, the invariant Cantor set on which the map is the shift on two symbols. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Smale's horseshoe (Stephen Smale, 1965) +// Round: 54 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Strands=-1..1, Squeeze=0..1 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the rising strands are gold and the falling ones azure, with violet stable strands and white crossings; at 0.5 the rising strands turn blue and the falling ones orange. +// Knob 2: Speed. One fold a cycle. At 2 a cycle takes 3 s and holds still for the first half; the squeeze and stretch take 0.45 s of the rest and the bend 1.05 s. At 0.2 a cycle takes 30 s, at 3 two seconds. +// Knob 3: Strands. Which Cantor family shows. At 0 both: the images of the panel as vertical strands and the points that stay in the square for three more folds as faint violet bands, white where they cross. At -1 only the images. Towards +1 the images fade and the violet bands brighten to full, the stable set alone. +// Knob 4: Squeeze. The factor lambda by which a fold narrows the square, 0.445 at 0 down to 0.285 at 1 (0.435 at the opening), with the stretch mu = 2 + (pi/2)(1 - lambda), 2.89 at the opening. The level drawn keeps the finest strands near an LED wide: the fifth image's 32 strands up to 0.15 and the fourth's 16 up to 0.66; from there on the third's 8, with gaps that widen as lambda falls. + +const W = 64, H = 128; +const { floor, min, max, abs, sqrt, sin, cos, atan2, log, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ss = (t) => t * t * (3 - 2 * t); + +// The domain D is the panel: x from 0 to 64, Y from 0 at the bottom row to +// 128. The square Q runs from Y = C to QT; the cap A lies below it and the +// cap B above. +const C = 32, QT = 96, HQ = 64; +// A cycle: held still for HOLD of it, then the homotopy, its first T1 the +// straight squeeze and stretch and the rest the bend. +const HOLD = 0.5, T1 = 0.3; + +// f lays D along a U-shaped centre line, squeezed by lambda across it and +// stretched along it by mu through Q. The left leg runs up the left edge of +// Q and the right leg down the right edge; between them the bend of radius R +// turns over in B. A goes to the left foot below Q, shifted sideways only, +// and B to the right foot, turned over. So f(D) lies in D and meets Q in two +// vertical strips, each lambda wide; the horizontal strip of Q between the +// two that feed the legs goes round the bend. +function geom(S, lam) { + S.lam = lam; + S.w = 32 * lam; + S.R = 32 - S.w; + // The bend runs as fast as the legs: pi R = mu (HQ - 2 HQ / mu). + S.mu = 2 + (PI * S.R) / HQ; + S.len = 2 * C + HQ * S.mu; + // The level drawn: the finest vertical strands near an LED wide, the + // finest horizontal ones near an LED tall. + S.nf = cl(floor(log(0.85 / 64) / log(lam)), 2, 6); + S.nb = cl(floor(log(0.9 / HQ) / log(1 / S.mu)), 2, 6); +} + +// From arc length along the centre line back to the height Y in D. +const isig = (S, s) => (s < C ? s : s <= C + HQ * S.mu ? C + (s - C) / S.mu : QT + s - C - HQ * S.mu); + +// The preimage under f of a point of f(D), into P. P[2] is 1 when that +// point's piece of the strand runs the other way up from its preimage: the +// right leg and foot, and the half of the bend past the top. False when the +// point is not in f(D). +function finv(S, X, Y, P) { + const lam = S.lam, w2 = 2 * S.w; + if (Y <= QT) { + if (X <= w2) { + P[0] = X / lam; + P[1] = Y < C ? Y : C + (Y - C) / S.mu; + P[2] = 0; + return true; + } + if (X >= 64 - w2) { + P[0] = (64 - X) / lam; + P[1] = Y < C ? QT + C - Y : QT - (Y - C) / S.mu; + P[2] = 1; + return true; + } + return false; + } + const dx = X - 32, dy = Y - QT, r = sqrt(dx * dx + dy * dy); + if (abs(r - S.R) > S.w) return false; + const th = atan2(dy, dx); + P[0] = 32 + (S.R - r) / lam; + P[1] = C + (HQ + S.R * (PI - th)) / S.mu; + P[2] = th < PI / 2 ? 1 : 0; + return true; +} + +// How many times a point can be pulled back by f and stay in D, up to nf: +// a point of f^d(D) outside f^(d+1)(D). In Q this is the depth of the gap +// of the Cantor set it sits in. The parity of the turns over in the first +// nf - 1 pulls goes to S.o1 and in all nf to S.o2: whether the strand of +// f^nf(D) through the point runs up (0) or down (1). Strands next to each +// other always run opposite ways. +function fdepth(S, X, Y, P) { + let k = 0, o = 0; + P[0] = X; + P[1] = Y; + S.o1 = 0; + while (k < S.nf && finv(S, P[0], P[1], P)) { + o ^= P[2]; + k++; + if (k === S.nf - 1) S.o1 = o; + } + S.o2 = o; + return k; +} + +// How many times f can push a point forward and keep it in Q, up to nb + 1. +// It depends on the height alone: the horizontal strips. +function bdepth(S, Y) { + let k = 0; + const mu = S.mu, h = HQ / mu; + while (k <= S.nb && Y >= C && Y <= QT) { + k++; + const q = Y - C; + if (q < h) Y = C + mu * q; + else if (q > HQ - h) Y = C + mu * (HQ - q); + else return k; + } + return k; +} + +// The homotopy from the identity at tau = 0 to f at tau = 1: the panel +// squeezed toward its left edge and stretched straight up along the centre +// line, then the part above Q bent over, the bend's angle growing to pi. +// The preimage of (X, Y) at tau goes into P, with P[2] set where the bend +// has turned the strand downward; false outside the image. +function minv(S, tau, X, Y, P) { + P[2] = 0; + if (tau < T1) { + const a = ss(tau / T1), sx = 1 - a + a * S.lam, sq = 1 - a + a * S.mu; + const gt = QT * (1 - a) + a * (C + HQ * S.mu); + const x = X / sx; + const y = Y < C ? Y : Y <= gt ? C + (Y - C) / sq : QT + Y - gt; + if (x > 64 || y > 128) return false; + P[0] = x; + P[1] = y; + return true; + } + const phi = PI * ss((tau - T1) / (1 - T1)), lam = S.lam, w = S.w, R = S.R; + let s = -1, u = 0; + if (Y <= QT) { + if (X <= 2 * w) (s = Y), (u = (X - w) / lam); + } else { + const dx = X - 32, dy = Y - QT, r = sqrt(dx * dx + dy * dy), th = atan2(dy, dx); + if (th >= PI - phi && abs(r - R) <= w) { + s = QT + R * (PI - th); + u = (R - r) / lam; + P[2] = th < PI / 2 ? 1 : 0; + } + } + if (s < 0) { + // Past the bend the strip runs straight on, along its tangent. + const te = PI - phi, ct = cos(te), st = sin(te); + const px = X - 32 - R * ct, py = Y - QT - R * st; + const sl = px * st - py * ct, off = -px * ct - py * st; + if (sl >= 0 && abs(off) <= w) { + s = QT + R * phi + sl; + u = off / lam; + P[2] = te < PI / 2 ? 1 : 0; + } + } + if (s < 0 || s > S.len) return false; + P[0] = 32 + u; + P[1] = isig(S, s); + return true; +} + +export function setup(params) { + // The first second shows the folded ribbon still; the first stretch + // starts at 1.5 s. + params.s = { ph: 0.98, hue: 0, strands: 0, lam: 0, nf: 5, nb: 4, o1: 0, o2: 0, c: [0, 0, 0], P: [0, 0, 0], pal: new Float64Array(15) }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.strands = cl(k[2], -1, 1); + const lam = 0.445 - 0.16 * cl(k[3], 0, 1); + if (lam !== S.lam) geom(S, lam); + S.ph += (d * cl(k[1], 0.2, 3)) / 6; + S.ph -= floor(S.ph); +} + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + o[1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + o[2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; + return o; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const tau = S.ph < HOLD ? 0 : (S.ph - HOLD) / (1 - HOLD); + const P = S.P, c = S.c, pal = S.pal, h = S.hue, nf = S.nf, nb = S.nb; + const wf = min(1, 1 - S.strands), wb = min(1, 1 + S.strands), vb = 0.3 + 0.7 * max(0, S.strands); + // Rising strands gold and falling strands azure; where a stable strand + // crosses them, near white. The stable strands alone are violet. + const cols = [0.12, 0.85, 1, 0.58, 0.75, 0.8, 0.12, 0.12, 1, 0.58, 0.3, 0.9, 0.78, 0.65, vb]; + for (let n = 0; n < 15; n += 3) { + hsv(h + cols[n], cols[n + 1], cols[n + 2], c); + pal[n] = c[0]; + pal[n + 1] = c[1]; + pal[n + 2] = c[2]; + } + for (let j = 0; j < H; j++) { + const Y = 127.5 - j; + for (let i = 0; i < W; i++) { + // The drawn depths move with the ribbon and climb by tau through the + // fold, so the picture at the end of a cycle is the one it began with: + // f maps the strands of each level onto those of the next. + let v = tau - 1, e = 0, dn = 0; + if (minv(S, tau, i + 0.5, Y, P)) { + const fl = P[2]; + e = max(0, bdepth(S, P[1]) - tau); + v = fdepth(S, P[0], P[1], P) + tau; + // Which way the strand runs, by the turns of the last nf pulls, + // handing over to the last nf - 1 as the fold adds one more. + dn = (1 - tau) * (S.o2 ^ fl) + tau * (S.o1 ^ fl); + } else if (tau > 0.6 && i + 0.5 > 2 * S.w && i + 0.5 < 64 - 2 * S.w) { + // In the gap the ribbon leaves between its legs, the stable strips + // of Q come back while the bend closes. + e = nb - 1 + ((tau - 0.6) / 0.4) * (bdepth(S, Y) - nb + 1); + } + const F = wf * cl(v - (nf - 1), 0, 1), B = wb * cl(e - (nb - 1), 0, 1); + const up = 1 - dn, a = F * (1 - B), b = F * B, g = (1 - F) * B; + for (let q = 0; q < 3; q++) c[q] = a * (up * pal[q] + dn * pal[3 + q]) + b * (up * pal[6 + q] + dn * pal[9 + q]) + g * pal[12 + q]; + display.setPixel(i, j, min(255, c[0]) | 0, min(255, c[1]) | 0, min(255, c[2]) | 0); + } + } +} From ded9be62d0e921f7b4a8caea3adaa384bca3a357 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 02:58:27 +0000 Subject: [PATCH 08/20] =?UTF-8?q?54=ED=8C=90:=20smale-horseshoe=20?= =?UTF-8?q?=EB=8B=A4=EB=93=AC=EA=B8=B0?= 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_01CtQeRUJHSr3xVvLYqdqFXC --- .../pattern-study/patterns/smale-horseshoe.js | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/incoming/pattern-study/patterns/smale-horseshoe.js b/incoming/pattern-study/patterns/smale-horseshoe.js index ce1b8fd30..0d5e90d8d 100644 --- a/incoming/pattern-study/patterns/smale-horseshoe.js +++ b/incoming/pattern-study/patterns/smale-horseshoe.js @@ -120,41 +120,55 @@ function bdepth(S, Y) { // The homotopy from the identity at tau = 0 to f at tau = 1: the panel // squeezed toward its left edge and stretched straight up along the centre // line, then the part above Q bent over, the bend's angle growing to pi. -// The preimage of (X, Y) at tau goes into P, with P[2] set where the bend -// has turned the strand downward; false outside the image. -function minv(S, tau, X, Y, P) { - P[2] = 0; +// phase() fixes the shape for one tau; minv() takes a point back through it +// into P, with P[2] set where the bend has turned the strand downward, and +// is false outside the image. +function phase(S, tau) { + S.tau = tau; if (tau < T1) { - const a = ss(tau / T1), sx = 1 - a + a * S.lam, sq = 1 - a + a * S.mu; - const gt = QT * (1 - a) + a * (C + HQ * S.mu); - const x = X / sx; - const y = Y < C ? Y : Y <= gt ? C + (Y - C) / sq : QT + Y - gt; + const a = ss(tau / T1); + S.sx = 1 - a + a * S.lam; + S.sq = 1 - a + a * S.mu; + S.gt = QT * (1 - a) + a * (C + HQ * S.mu); + } else { + S.phi = PI * ss((tau - T1) / (1 - T1)); + S.ct = cos(PI - S.phi); + S.st = sin(PI - S.phi); + } +} + +function minv(S, X, Y, P) { + P[2] = 0; + if (S.tau < T1) { + const x = X / S.sx, y = Y < C ? Y : Y <= S.gt ? C + (Y - C) / S.sq : QT + Y - S.gt; if (x > 64 || y > 128) return false; P[0] = x; P[1] = y; return true; } - const phi = PI * ss((tau - T1) / (1 - T1)), lam = S.lam, w = S.w, R = S.R; + const phi = S.phi, lam = S.lam, w = S.w, R = S.R; let s = -1, u = 0; if (Y <= QT) { if (X <= 2 * w) (s = Y), (u = (X - w) / lam); } else { - const dx = X - 32, dy = Y - QT, r = sqrt(dx * dx + dy * dy), th = atan2(dy, dx); - if (th >= PI - phi && abs(r - R) <= w) { - s = QT + R * (PI - th); - u = (R - r) / lam; - P[2] = th < PI / 2 ? 1 : 0; + const dx = X - 32, dy = Y - QT, r = sqrt(dx * dx + dy * dy); + if (abs(r - R) <= w) { + const th = atan2(dy, dx); + if (th >= PI - phi) { + s = QT + R * (PI - th); + u = (R - r) / lam; + P[2] = th < PI / 2 ? 1 : 0; + } } } if (s < 0) { // Past the bend the strip runs straight on, along its tangent. - const te = PI - phi, ct = cos(te), st = sin(te); - const px = X - 32 - R * ct, py = Y - QT - R * st; + const ct = S.ct, st = S.st, px = X - 32 - R * ct, py = Y - QT - R * st; const sl = px * st - py * ct, off = -px * ct - py * st; if (sl >= 0 && abs(off) <= w) { s = QT + R * phi + sl; u = off / lam; - P[2] = te < PI / 2 ? 1 : 0; + P[2] = phi > PI / 2 ? 1 : 0; } } if (s < 0 || s > S.len) return false; @@ -166,7 +180,7 @@ function minv(S, tau, X, Y, P) { export function setup(params) { // The first second shows the folded ribbon still; the first stretch // starts at 1.5 s. - params.s = { ph: 0.98, hue: 0, strands: 0, lam: 0, nf: 5, nb: 4, o1: 0, o2: 0, c: [0, 0, 0], P: [0, 0, 0], pal: new Float64Array(15) }; + params.s = { ph: 0.98, hue: 0, strands: 0, lam: 0, nf: 5, nb: 4, tau: 0, sx: 1, sq: 1, gt: QT, phi: 0, ct: -1, st: 0, o1: 0, o2: 0, c: [0, 0, 0], P: [0, 0, 0], pal: new Float64Array(15) }; } export function update(dt, input, params) { @@ -193,6 +207,7 @@ export function draw(display, params) { const S = params.s; if (!S) return; const tau = S.ph < HOLD ? 0 : (S.ph - HOLD) / (1 - HOLD); + phase(S, tau); const P = S.P, c = S.c, pal = S.pal, h = S.hue, nf = S.nf, nb = S.nb; const wf = min(1, 1 - S.strands), wb = min(1, 1 + S.strands), vb = 0.3 + 0.7 * max(0, S.strands); // Rising strands gold and falling strands azure; where a stable strand @@ -211,7 +226,7 @@ export function draw(display, params) { // fold, so the picture at the end of a cycle is the one it began with: // f maps the strands of each level onto those of the next. let v = tau - 1, e = 0, dn = 0; - if (minv(S, tau, i + 0.5, Y, P)) { + if (minv(S, i + 0.5, Y, P)) { const fl = P[2]; e = max(0, bdepth(S, P[1]) - tau); v = fdepth(S, P[0], P[1], P) + tau; From 0b574e29c847fb7558ef51d0dcacb152a62278a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 02:59:15 +0000 Subject: [PATCH 09/20] =?UTF-8?q?54=ED=8C=90:=20stable-marriage=20?= =?UTF-8?q?=EC=A7=93=EA=B8=B0?= 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_01CtQeRUJHSr3xVvLYqdqFXC --- .../pattern-study/patterns/stable-marriage.js | 326 ++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 incoming/pattern-study/patterns/stable-marriage.js diff --git a/incoming/pattern-study/patterns/stable-marriage.js b/incoming/pattern-study/patterns/stable-marriage.js new file mode 100644 index 000000000..0b9b40783 --- /dev/null +++ b/incoming/pattern-study/patterns/stable-marriage.js @@ -0,0 +1,326 @@ +// ===== Patternflow pattern ===== +// Title: Stable Marriage +// Family: algo +// Idea: Gale and Shapley's deferred acceptance between 16 people along the top edge and 16 along the bottom, every proposal a line that runs the panel's full height; a proposal held stays lit and one turned away flashes and fades, and when a receiver trades up the line it held snaps in two. Once the top has its stable matching the bottom proposes on the same lists, and then the pairs where the two stable matchings differ swap back and forth. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: deferred acceptance (David Gale and Lloyd Shapley, 1962) +// Round: 54 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Agreement=0..1, Choosy=0..1 +// +// Knob 1: Hue. Each end of a line, and the tile of each person, is coloured by the rank that person gives the partner: at 0 a first choice is green and the last pink, with blue at about the fifth. Turning it moves the whole scale round the colour wheel. +// Knob 2: Speed. Rounds a second: 2 at the opening 2. In a round every free proposer sends one proposal; a round of eight or more takes a whole round's time, a round of one, as in the chains of rejections that end a run, 0.35 of it. A proposal takes 0.6 of its round to cross. +// Knob 3: Agreement. How much everyone shares one ranking of the other side, left best. At 0 each list is an independent shuffle and the lines cross every which way: a run takes 47 proposals on average, close to n ln n = 44, and in 9 runs of 10 the two sides' matchings differ. At 1 everyone agrees: all 16 first proposals land on the bottom left, the run takes 136 proposals in 16 rounds, and it ends in 16 parallel lines, the one stable matching. +// Knob 4: Choosy. The share of the other side each person refuses outright, from none at 0 to three quarters at 1, where 7.5 of the 16 on each side stay single on average. A single person's tile is dim grey, and the same people are single in both stable matchings. + +const W = 64, H = 128, n = 16, SP = W / n, Y0 = 4, Y1 = 123; +const { floor, min, max, abs, sqrt, cos, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const XP = (i) => SP * i + (SP >> 1); +// Rank given to someone a person will not take. +const NO = 999; +// The phases of a cycle. The top proposes and its matching holds; the +// bottom proposes on the same lists and its matching holds; then the two are +// compared, and the panel fades to new lists. +const RUN_A = 0, HOLD_A = 1, RUN_B = 2, HOLD_B = 3, COMPARE = 4, FADE = 5; +// Lengths in rounds; FLY is when the proposals of a round arrive. +const T_HOLD_A = 3, T_HOLD_B = 2, T_CMP = 8, T_FADE = 1.5, FLY = 0.6, T_GONE = 0.8; + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + o[1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + o[2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; + return o; +} + +export function setup(params) { + params.s = { + t: 0, hue: 0, speed: 2, agree: -1, choosy: -1, phase: RUN_A, pt: 0, rt: 0, decided: true, + // Lists: lt[i * n + k] is the k-th choice of top person i, lb the same + // for the bottom; rkt and rkb hold the rank each gives, NO if refused. + lt: new Int8Array(n * n), lb: new Int8Array(n * n), nt: new Int8Array(n), nb: new Int8Array(n), + rkt: new Int16Array(n * n), rkb: new Int16Array(n * n), + nx: new Int8Array(n), hold: new Int8Array(n), hb: new Float64Array(n), rl: 1, + ma: new Int8Array(n), mb: new Int8Array(n), props: 0, propsA: 0, propsB: 0, rounds: 0, + fx: [], + buf: new Float32Array(W * H * 3), c: [0, 0, 0], c2: [0, 0, 0], + }; +} + +// Preferences: top person i values bottom person j at +// A (1 - j / (n - 1)) + (1 - A) u with u uniform, and the bottom the same way, +// so at A = 1 everyone ranks the other side left to right. Each pair is +// refused by each side with chance c. +function newPrefs(S) { + const A = S.agree, c = 0.75 * S.choosy, u = new Float64Array(n), idx = []; + for (let side = 0; side < 2; side++) { + const L = side ? S.lb : S.lt, R = side ? S.rkb : S.rkt, len = side ? S.nb : S.nt; + for (let i = 0; i < n; i++) { + idx.length = 0; + for (let j = 0; j < n; j++) { + u[j] = A * (1 - j / (n - 1)) + (1 - A) * Math.random(); + R[i * n + j] = NO; + if (Math.random() >= c) idx.push(j); + } + idx.sort((a, b) => u[b] - u[a]); + len[i] = idx.length; + for (let k = 0; k < idx.length; k++) { + L[i * n + k] = idx[k]; + R[i * n + idx[k]] = k + 1; + } + } + } +} + +// Ranks seen from each end of the pair (top a, bottom b). +const rankTop = (S, a, b) => S.rkt[a * n + b]; +const rankBot = (S, a, b) => S.rkb[b * n + a]; + +function startRun(S, side) { + S.side = side; + S.nx.fill(0); + S.hold.fill(-1); + S.props = 0; + S.rounds = 0; + S.rt = S.t - 1; + S.rl = 1; + S.decided = true; + S.fx = S.fx.filter((e) => e.k >= 2); +} + +// The proposals of one round leave together: every free proposer with +// someone left on the list sends one. Returns how many left. +function sendRound(S) { + const side = S.side, L = side ? S.lb : S.lt, len = side ? S.nb : S.nt, held = new Uint8Array(n); + for (let r = 0; r < n; r++) if (S.hold[r] >= 0) held[S.hold[r]] = 1; + let sent = 0; + for (let p = 0; p < n; p++) { + if (held[p] || S.nx[p] >= len[p]) continue; + const r = L[p * n + S.nx[p]++]; + S.fx.push({ k: side, a: side ? r : p, b: side ? p : r, t0: S.t }); + sent++; + } + S.props += sent; + if (sent) S.rounds++; + return sent; +} + +// The receivers answer: each keeps the best of what it holds and what just +// arrived, among those it will take, and turns the rest away. +function decide(S) { + const side = S.side, fly = [], keep = []; + for (const e of S.fx) (e.k === side ? fly : keep).push(e); + const best = new Int8Array(n).fill(-1), bestRank = new Int16Array(n).fill(NO); + for (let r = 0; r < n; r++) { + const h = S.hold[r]; + if (h >= 0) { + best[r] = h; + bestRank[r] = side ? rankTop(S, r, h) : rankBot(S, h, r); + } + } + for (const e of fly) { + const p = side ? e.b : e.a, r = side ? e.a : e.b, rk = side ? rankTop(S, r, p) : rankBot(S, p, r); + if (rk < bestRank[r]) (best[r] = p), (bestRank[r] = rk); + } + for (const e of fly) { + const p = side ? e.b : e.a, r = side ? e.a : e.b; + if (best[r] === p && S.hold[r] !== p) continue; + keep.push({ k: 2, a: e.a, b: e.b, t0: S.t }); + } + for (let r = 0; r < n; r++) + if (best[r] !== S.hold[r]) { + const o = S.hold[r]; + if (o >= 0) keep.push({ k: 3, a: side ? r : o, b: side ? o : r, t0: S.t }); + S.hold[r] = best[r]; + S.hb[r] = S.t; + } + S.fx = keep; +} + +// The run's matching as top person to bottom person, -1 for single. +function matching(S, m) { + m.fill(-1); + for (let r = 0; r < n; r++) { + const p = S.hold[r]; + if (p >= 0) S.side ? (m[r] = p) : (m[p] = r); + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + const ag = cl(k[2], 0, 1), ch = cl(k[3], 0, 1); + if (abs(ag - S.agree) > 0.02 || abs(ch - S.choosy) > 0.02) { + // New lists from the start whenever Agreement or Choosy moves. + S.agree = ag; + S.choosy = ch; + newPrefs(S); + S.fx = []; + S.phase = RUN_A; + S.pt = S.t; + startRun(S, 0); + } + S.t += d * S.speed; + const T = S.t, el = T - S.pt; + if (S.phase === RUN_A || S.phase === RUN_B) { + if (!S.decided && T - S.rt >= FLY * S.rl) { + decide(S); + S.decided = true; + } + if (S.decided && T - S.rt >= S.rl - 1e-9) { + S.rt = T; + const sent = sendRound(S); + // A round of one proposal, the chains of rejections that end a run, + // takes 0.35 of the time of a round of eight or more. + S.rl = 0.35 + 0.65 * min(1, (sent - 1) / 7); + for (const e of S.fx) if (e.k < 2 && e.t0 === T) e.fl = FLY * S.rl; + if (sent) S.decided = false; + else { + // Nobody free has anyone left to ask: the matching is stable. + if (S.phase === RUN_A) matching(S, S.ma), (S.propsA = S.props), (S.phase = HOLD_A); + else matching(S, S.mb), (S.propsB = S.props), (S.phase = HOLD_B); + S.pt = T; + } + } + } else if (S.phase === HOLD_A && el >= T_HOLD_A) { + S.phase = RUN_B; + S.pt = T; + startRun(S, 1); + } else if (S.phase === HOLD_B && el >= T_HOLD_B) { + S.phase = COMPARE; + S.pt = T; + } else if (S.phase === COMPARE && el >= T_CMP) { + S.phase = FADE; + S.pt = T; + } else if (S.phase === FADE && el >= T_FADE) { + newPrefs(S); + S.fx = []; + S.phase = RUN_A; + S.pt = T; + startRun(S, 0); + } + S.fx = S.fx.filter((e) => e.k < 2 || T - e.t0 < T_GONE); +} + +// Rank to colour: green for a first choice through blue to pink for a last. +function rankCol(S, rank, o) { + const q = rank >= NO ? 1 : (rank - 1) / (n - 1); + return hsv(0.3 + S.hue + 0.6 * sqrt(q), 0.85, 1, o); +} + +// Where lines cross, each channel keeps the brighter of the two. +function add(buf, x, y, r, g, b) { + if (x < 0 || x >= W) return; + const o = (y * W + x) * 3; + buf[o] = max(buf[o], r); + buf[o + 1] = max(buf[o + 1], g); + buf[o + 2] = max(buf[o + 2], b); +} + +// The line from top person a to bottom person b over rows ya..yb, its +// colour running from ca at the top end to cb at the bottom, times v. Each +// row lights the LEDs within LW / 2 of the line, the edge ones in part. +const LW = 1.5; +function line(buf, a, b, ya, yb, ca, cb, v, white) { + const xa = XP(a), xb = XP(b), y0 = max(Y0, Math.ceil(ya)), y1 = min(Y1, floor(yb)); + for (let y = y0; y <= y1; y++) { + const f = (y - Y0) / (Y1 - Y0), x = xa + (xb - xa) * f; + const r = ((ca[0] + (cb[0] - ca[0]) * f) * (1 - white) + 255 * white) * v, g = ((ca[1] + (cb[1] - ca[1]) * f) * (1 - white) + 255 * white) * v, bl = ((ca[2] + (cb[2] - ca[2]) * f) * (1 - white) + 255 * white) * v; + for (let xi = floor(x - LW / 2); xi <= x + LW / 2; xi++) { + const k = cl(LW / 2 + 0.5 - abs(xi - x), 0, 1); + if (k > 0) add(buf, xi, y, r * k, g * k, bl * k); + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const buf = S.buf, c = S.c, c2 = S.c2, T = S.t, el = T - S.pt, ph = S.phase; + buf.fill(0); + const fade = ph === FADE ? max(0, 1 - el / T_FADE) : 1; + const pair = (a, b, v, white) => { + rankCol(S, rankTop(S, a, b), c); + rankCol(S, rankBot(S, a, b), c2); + line(buf, a, b, Y0, Y1, c, c2, v, white); + }; + // The first stable matching stays as dim ghosts while the bottom proposes. + if (ph === RUN_B || ph === HOLD_B) for (let a = 0; a < n; a++) if (S.ma[a] >= 0) pair(a, S.ma[a], 0.25, 0.3); + if (ph <= HOLD_B) { + // Held proposals; one just taken glows white for half a round. + for (let r = 0; r < n; r++) { + const p = S.hold[r]; + if (p < 0) continue; + const a = S.side ? r : p, b = S.side ? p : r, g = max(0, 1 - (T - S.hb[r]) * 2); + pair(a, b, 0.85 + 0.15 * g, 0.5 * g); + } + } else { + // Compare: pairs in both matchings stay; the others swap between the + // top's best matching and the bottom's best, a full swing in 4 rounds. + const w = 0.5 + 0.5 * cos((PI * el) / 2); + for (let a = 0; a < n; a++) { + const x = S.ma[a], y = S.mb[a]; + if (x >= 0 && x === y) pair(a, x, 0.45 * fade, 0); + else { + if (x >= 0) pair(a, x, w * fade, 0.25 * w); + if (y >= 0) pair(a, y, (1 - w) * fade, 0.25 * (1 - w)); + } + } + } + for (const e of S.fx) { + const age = T - e.t0; + if (e.k < 2) { + // In flight: the proposer's end coloured by the rank it gives, a white + // head running across the panel. + const fr = min(1, age / e.fl), down = e.k === 0; + rankCol(S, down ? rankTop(S, e.a, e.b) : rankBot(S, e.a, e.b), c); + const yh = down ? Y0 + (Y1 - Y0) * fr : Y1 - (Y1 - Y0) * fr; + line(buf, e.a, e.b, down ? Y0 : yh, down ? yh : Y1, c, c, 0.55, 0.15); + line(buf, e.a, e.b, yh - 3, yh + 3, c, c, 0.9, 0.8); + } else if (e.k === 2) { + // Turned away: a flash that fades. + const q = 1 - age / T_GONE; + rankCol(S, rankTop(S, e.a, e.b), c); + rankCol(S, rankBot(S, e.a, e.b), c2); + line(buf, e.a, e.b, Y0, Y1, c, c2, 0.8 * q * q, 0.6 * q); + } else { + // Snapped: the line breaks at its middle and each half pulls back to + // its own person. + const q = age / T_GONE, mid = (Y0 + Y1) / 2, l = (mid - Y0) * (1 - q); + rankCol(S, rankTop(S, e.a, e.b), c); + rankCol(S, rankBot(S, e.a, e.b), c2); + line(buf, e.a, e.b, Y0, Y0 + l - 1, c, c2, 0.9 * (1 - q), 0.3); + line(buf, e.a, e.b, Y1 - l + 1, Y1, c, c2, 0.9 * (1 - q), 0.3); + } + } + // People: a tile of three by three LEDs at each end, in the colour of the + // rank of what they hold now, dim grey while single. + const mt = new Int8Array(n).fill(-1), mbm = new Int8Array(n).fill(-1); + if (ph <= HOLD_B) { + for (let r = 0; r < n; r++) { + const p = S.hold[r]; + if (p < 0) continue; + S.side ? ((mt[r] = p), (mbm[p] = r)) : ((mt[p] = r), (mbm[r] = p)); + } + } else { + const m = ph === COMPARE && cos((PI * el) / 2) < 0 ? S.mb : S.ma; + for (let a = 0; a < n; a++) if (m[a] >= 0) (mt[a] = m[a]), (mbm[m[a]] = a); + } + for (let i = 0; i < n; i++) { + for (const top of [1, 0]) { + const o = top ? mt[i] : mbm[i]; + if (o >= 0) rankCol(S, top ? rankTop(S, i, o) : rankBot(S, o, i), c); + else c[0] = c[1] = c[2] = 60; + const y0 = top ? 0 : H - 3; + for (let y = y0; y < y0 + 3; y++) for (let x = XP(i) - 1; x <= XP(i) + 1; x++) add(buf, x, y, c[0] * fade, c[1] * fade, c[2] * fade); + } + } + for (let p = 0, o = 0; p < W * H; p++, o += 3) display.setPixel(p % W, (p / W) | 0, min(255, buf[o]) | 0, min(255, buf[o + 1]) | 0, min(255, buf[o + 2]) | 0); +} From 765a7599bdae4d8640d47a1ee04f3a5b092d8e8b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 03:00:05 +0000 Subject: [PATCH 10/20] =?UTF-8?q?54=ED=8C=90:=20arcsine-law=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EB=8B=A4=EB=93=AC=EA=B8=B0?= 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_01CtQeRUJHSr3xVvLYqdqFXC --- incoming/pattern-study/patterns/arcsine-law.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/incoming/pattern-study/patterns/arcsine-law.js b/incoming/pattern-study/patterns/arcsine-law.js index 2939d51f9..b96a793bd 100644 --- a/incoming/pattern-study/patterns/arcsine-law.js +++ b/incoming/pattern-study/patterns/arcsine-law.js @@ -37,11 +37,12 @@ function hsv(h, s, v, o) { // The exact laws for N = 126 steps, bin k of the histogram. The time spent // above zero (counting a step as above when either end of it is) and the // last zero both take the value 2k with probability u(2k) u(N - 2k), where -// u(2m) = C(2m, m) / 4^m is the chance of being at zero at time 2m (Chung -// and Feller, Sparre Andersen). The first time of the maximum is k with -// probability a(k) b(N - k): a(k) the chance a walk stays above zero for k -// steps, b(m) the chance it stays at or below zero for m, both by counting -// paths. Lévy's arcsine law is the limit of all three. +// u(2m) = C(2m, m) / 4^m is the chance of being at zero at time 2m: the +// discrete arcsine law of Feller's textbook (1950). The first time of the +// maximum is k with probability a(k) b(N - k): a(k) the chance a walk stays +// above zero for k steps, b(m) the chance it stays at or below zero for m, +// both by following the walk step by step. Lévy's arcsine law is the limit +// of all three. const U = new Float64Array(N + 1); U[0] = 1; for (let m = 1; 2 * m <= N; m++) U[2 * m] = (U[2 * m - 2] * (2 * m - 1)) / (2 * m); @@ -72,7 +73,7 @@ const PB = [new Float64Array(NB), new Float64Array(NB), new Float64Array(NB)]; for (let t = 0; t <= N; t++) PB[2][t >> 1] += a[t] * b[N - t]; } // The histogram's scale puts the third bin from either end at full length, -// so the two end bins, 2.6 and 1.3 times longer, run off the band. +// so the bins at the very ends run off the band. const PFULL = PB[0][2]; export function setup(params) { @@ -83,8 +84,9 @@ export function setup(params) { pos: new Int16Array(NW), lz: new Int16Array(NW), tm: new Int16Array(NW), top: new Int16Array(NW), // Sorting: each walk's target column and each cell's target row. X: new Int16Array(NW), R: new Int16Array(NW * N), sortStat: -1, - // The last sorted picture, shown dimmed below the new batch's front. - prev: new Float32Array(NW * N * 3), cell: new Float32Array(NW * N * 3), + // The last sorted picture, shown on every other LED below the new + // batch's front. + prev: new Float32Array(NW * N * 3), hist: [new Float64Array(NB), new Float64Array(NB), new Float64Array(NB)], walks: 0, histMu: 0, fb: new Float32Array(W * H * 3), c: [0, 0, 0], }; From a99863b56ee874b715add7dfc13d0d516f7d3598 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 03:00:20 +0000 Subject: [PATCH 11/20] =?UTF-8?q?54=ED=8C=90:=20strange-eigenmode=20?= =?UTF-8?q?=EC=A7=93=EA=B8=B0?= 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_01CtQeRUJHSr3xVvLYqdqFXC --- .../patterns/strange-eigenmode.js | 278 ++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 incoming/pattern-study/patterns/strange-eigenmode.js diff --git a/incoming/pattern-study/patterns/strange-eigenmode.js b/incoming/pattern-study/patterns/strange-eigenmode.js new file mode 100644 index 000000000..bc38784c0 --- /dev/null +++ b/incoming/pattern-study/patterns/strange-eigenmode.js @@ -0,0 +1,278 @@ +// ===== Patternflow pattern ===== +// Title: Strange Eigenmode +// Family: sim +// Idea: A dye on a torus stirred by Pierrehumbert's alternating sine flow, the rows sliding sideways for half a period and the columns sliding up and down for the other half, with a little diffusion, and rescaled every frame so it never fades: the striated picture is the flow's slowest decaying Floquet mode, the strange eigenmode, so it comes back in exactly the same shape every period while its true contrast falls to 39% each time. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: strange eigenmodes (Raymond Pierrehumbert, 1994), in his alternating sine flow +// Round: 54 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Stir=-1..1, Diffusion=0..1 +// +// Knob 1: Hue. The colour of the dye. At 0 the positive half runs from violet where it is faint to pale aqua where it is strongest, and the negative half shows as dim indigo level lines; 0.25 turns it crimson to lilac, 0.5 olive through amber to rose, 0.75 teal through green to pale gold, and 1 is back at violet and aqua. +// Knob 2: Speed. One period of the flow takes 6 / Speed seconds: 3 s at the opening 2, each half 1.5 s with the slide easing in and out and resting a fifth of a second at either end. At 0.2 a period takes 30 s and at 3 it takes 2 s. The dye takes the same path at any speed. +// Knob 3: Stir. The amplitude A of both sines in units of the torus, which the panel draws 64 LEDs wide and 128 tall, so a row slides up to 64 A LEDs and a column up to 128 A. At 0, A = 1.2 (77 and 154 LEDs): global chaos, the leading Floquet multiplier 0.39 well ahead of the next, 0.28, so one shape returns every period at 39% of its contrast, a decay rate of 0.95 a period. At -1, A = 0.45: islands of regular flow keep their dye, which leaves only by diffusion, and the pattern fades only 10% a period. At -0.5 (A = 0.82) and +0.5 (A = 1.4) the leading multipliers are a complex pair, 137 and 136 degrees round, and the pattern swings between shapes without repeating. At +1 (A = 1.6) the leading multiplier is negative, -0.41: the pattern returns as its own negative every period, bright where it was dark. +// Knob 4: Diffusion. The diffusivity, 4 A times 30 to the power of the knob, in LED^2 a period: 5.9 at the opening 0.06, where the stripes are 2.9 LEDs across (rms over rms gradient). At 0.5 it is 26 (6 LEDs). At 1 it is 144 (12 LEDs): a few broad bands are left, and the decay rate rises to 1.6 a period. Any thinner than at 0 and the stripes would be finer than the LEDs. +// Buttons: K1 tap pours fresh dye, a few random waves, which the flow stirs on screen until, after about a dozen periods, the same mode is back. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, sqrt, sin, cos, exp, log, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + o[1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + o[2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; + return o; +} + +// Radix-2 FFT of n points. The forward transform takes its input in +// bit-reversed order and the inverse leaves its output so, which the loads +// and stores in shear do for free. Twiddles of the stage with half width h +// sit at h - 1 .. 2h - 2. +function plan(n) { + const bits = Math.round(log(n) / log(2)), rev = new Uint16Array(n), wr = new Float64Array(n), wi = new Float64Array(n); + for (let i = 0; i < n; i++) { + let r = 0; + for (let b = 0; b < bits; b++) r |= ((i >> b) & 1) << (bits - 1 - b); + rev[i] = r; + } + for (let h = 1; h < n; h <<= 1) for (let j = 0; j < h; j++) (wr[h - 1 + j] = cos((PI * j) / h)), (wi[h - 1 + j] = -sin((PI * j) / h)); + return { n, rev, wr, wi, re: new Float64Array(n), im: new Float64Array(n), d: new Float64Array(n) }; +} +function fwd(P) { + const n = P.n, re = P.re, im = P.im, wr = P.wr, wi = P.wi; + for (let h = 1; h < n; h <<= 1) + for (let j = 0, m = h << 1; j < h; j++) { + const cr = wr[h - 1 + j], ci = wi[h - 1 + j]; + for (let a = j; a < n; a += m) { + const b = a + h, tr = re[b] * cr - im[b] * ci, ti = re[b] * ci + im[b] * cr; + re[b] = re[a] - tr, im[b] = im[a] - ti; + re[a] += tr, im[a] += ti; + } + } +} +function inv(P) { + const n = P.n, re = P.re, im = P.im, wr = P.wr, wi = P.wi; + for (let h = n >> 1; h >= 1; h >>= 1) + for (let j = 0, m = h << 1; j < h; j++) { + const cr = wr[h - 1 + j], ci = -wi[h - 1 + j]; + for (let a = j; a < n; a += m) { + const b = a + h, xr = re[a] - re[b], xi = im[a] - im[b]; + re[a] += re[b], im[a] += im[b]; + re[b] = xr * cr - xi * ci, im[b] = xr * ci + xi * cr; + } + } +} +const PX = plan(W), PY = plan(H); + +// Every line (a row when rows, else a column) moves along itself by sh[line] +// LEDs, exactly, as a phase turn of each Fourier mode, and each mode is +// damped by P.d, the diffusion along that axis over the step (with the 1/n +// of the inverse folded in). Two real lines ride one complex FFT and are +// parted again in Fourier space. The Nyquist mode is dropped. +function shear(F, rows, sh) { + const P = rows ? PX : PY, n = P.n, lines = rows ? H : W, ls = rows ? W : 1, es = rows ? 1 : W; + const re = P.re, im = P.im, d = P.d, rev = P.rev, hn = n >> 1; + for (let l = 0; l < lines; l += 2) { + const o1 = l * ls, o2 = o1 + ls; + for (let i = 0, q = 0; i < n; i++, q += es) { + const r = rev[i]; + re[r] = F[o1 + q], im[r] = F[o2 + q]; + } + fwd(P); + const t1 = (-2 * PI * sh[l]) / n, t2 = (-2 * PI * sh[l + 1]) / n; + const c1 = cos(t1), s1 = sin(t1), c2 = cos(t2), s2 = sin(t2); + let e1r = 1, e1i = 0, e2r = 1, e2i = 0; + for (let k = 1; k < hn; k++) { + let q = e1r * c1 - e1i * s1; + e1i = e1r * s1 + e1i * c1, e1r = q; + q = e2r * c2 - e2i * s2; + e2i = e2r * s2 + e2i * c2, e2r = q; + const m = n - k, zr = re[k], zi = im[k], wr = re[m], wi = im[m], dk = 0.5 * d[k]; + const ar = zr + wr, ai = zi - wi, br = zi + wi, bi = wr - zr; + const a2r = (ar * e1r - ai * e1i) * dk, a2i = (ar * e1i + ai * e1r) * dk; + const b2r = (br * e2r - bi * e2i) * dk, b2i = (br * e2i + bi * e2r) * dk; + re[k] = a2r - b2i, im[k] = a2i + b2r; + re[m] = a2r + b2i, im[m] = b2r - a2i; + } + re[0] *= d[0], im[0] *= d[0]; + re[hn] = im[hn] = 0; + inv(P); + for (let i = 0, q = 0; i < n; i++, q += es) { + const r = rev[i]; + F[o1 + q] = re[r], F[o2 + q] = im[r]; + } + } +} + +// Diffusion along one axis for tau periods at kappa LED^2 a period, as the +// damping of each Fourier mode. +function damp(P, kappa, tau) { + const n = P.n; + for (let k = 0; k < n; k++) { + const w = (2 * PI * min(k, n - k)) / n; + P.d[k] = exp(-kappa * w * w * tau) / n; + } +} + +// Diffusion across the lines while the variance v is small: three taps. +const TMP = new Float64Array(N); +function blur(F, rows, v) { + const a = v / 2, b = 1 - v; + TMP.set(F); + if (rows) + for (let y = 0; y < H; y++) { + const o = y * W, u = ((y + H - 1) % H) * W, w = ((y + 1) % H) * W; + for (let x = 0; x < W; x++) F[o + x] = b * TMP[o + x] + a * (TMP[u + x] + TMP[w + x]); + } + else + for (let y = 0; y < H; y++) { + const o = y * W; + for (let x = 0; x < W; x++) F[o + x] = b * TMP[o + x] + a * (TMP[o + ((x + 63) & 63)] + TMP[o + ((x + 1) & 63)]); + } +} + +// Remove the mean and scale to unit rms; returns the log of the rms before. +function norm(F) { + let m = 0; + for (let i = 0; i < N; i++) m += F[i]; + m /= N; + let q = 0; + for (let i = 0; i < N; i++) q += (F[i] -= m) * F[i]; + const r = sqrt(q / N) || 1e-30, g = 1 / r; + for (let i = 0; i < N; i++) F[i] *= g; + return log(r); +} + +// The torus is one unit wide and one tall, drawn 64 LEDs wide and 128 +// tall, so a shift of A moves a row by 64 A LEDs and a column by 128 A. The +// phases of the two sines are fixed: the flow is periodic in time. +const P1 = 0.3, P2 = 1.9, SX = new Float64Array(H), SY = new Float64Array(W), ZERO = new Float64Array(H); +for (let y = 0; y < H; y++) SX[y] = W * sin((2 * PI * y) / H + P1); +for (let x = 0; x < W; x++) SY[x] = H * sin((2 * PI * x) / W + P2); + +function fresh(S) { + const F = S.F; + // A few random large waves to start from; the eigenmode forgets them. + F.fill(0); + for (let j = 0; j < 6; j++) { + const kx = 1 + floor(Math.random() * 3), ky = 1 + floor(Math.random() * 5), a = Math.random() * 2 * PI, b = Math.random() * 2 * PI; + for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) F[y * W + x] += sin((2 * PI * kx * x) / W + a) * sin((2 * PI * ky * y) / H + b); + } + norm(F); + S.ph = 0, S.logsum = 0; +} + +export function setup(params) { + const S = (params.s = { + hue: 0, speed: 2, A: 1.2, kap: 6, ph: 0, logsum: 0, gamma: 0, warm: 10, + F: new Float64Array(N), sh: new Float64Array(H), c: [0, 0, 0], + }); + fresh(S); +} + +// One step of the flow: a share f of the half period's shear along one +// axis, with tau periods of diffusion along both. The step is cut so that +// no piece tilts a line by more than one LED for each LED across, and the +// diffusion across the lines runs after each piece, as three taps or, when +// that much would be too wide for three, as damping in Fourier space. The +// log of what norm took away adds up over a period to minus its decay rate. +function step(S, rows, f, tau) { + const tilt = abs(f) * S.A * (rows ? (2 * PI * W) / H : (2 * PI * H) / W), n = max(1, Math.ceil(tilt)); + const prof = rows ? SX : SY, sh = S.sh, len = rows ? H : W, v = (2 * S.kap * tau) / n; + for (let i = 0; i < len; i++) sh[i] = (f / n) * S.A * prof[i]; + damp(rows ? PX : PY, S.kap, tau / n); + if (v > 0.8) damp(rows ? PY : PX, S.kap, tau / n); + for (let i = 0; i < n; i++) { + shear(S.F, rows, sh); + if (v > 0.8) shear(S.F, !rows, ZERO); + else blur(S.F, rows, v); + } + S.logsum += norm(S.F); +} + +// Within each half period the shear starts and stops smoothly and rests a +// little at either end. +const ease = (u) => { + const t = cl((u - 0.15) / 0.7, 0, 1); + return t * t * (3 - 2 * t); +}; + +// Advance the flow by dp of a period from phase ph. +function advance(S, dp) { + while (dp > 1e-9) { + const inX = S.ph < 0.5, end = inX ? 0.5 : 1, st = min(dp, end - S.ph); + const u0 = 2 * S.ph - (inX ? 0 : 1), f = ease(u0 + 2 * st) - ease(u0); + step(S, inX, f, st); + S.ph += st; + dp -= st; + if (S.ph >= 1 - 1e-9) { + S.ph = 0; + S.gamma = -S.logsum; + S.logsum = 0; + } + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + const st = cl(k[2], -1, 1); + S.A = 1.2 + st * (st < 0 ? 0.75 : 0.4); + S.kap = 4 * S.A * 30 ** cl(k[3], 0, 1); + // K1 tap: fresh dye, stirred from here on screen. + if (input.btnPressed && input.btnPressed[0]) fresh(S), (S.warm = 0); + if (S.warm > 0) { + // At the start the flow runs ahead unseen, steady shears whole periods + // at a time, two periods before the first frame and one more in each + // of the next eight, so the panel opens on the mode itself. + const n = S.warm === 10 ? 2 : 1; + for (let i = 0; i < 2 * n; i++) step(S, i % 2 === 0, 1, 0.5); + S.warm -= n; + S.logsum = 0; + if (!S.warm) { + // The equation is linear, so the mode and its negative are the same + // mode. Draw it with its long tail on the bright side. + let m3 = 0; + for (let i = 0; i < N; i++) m3 += S.F[i] * S.F[i] * S.F[i]; + if (m3 < 0) for (let i = 0; i < N; i++) S.F[i] = -S.F[i]; + } + } + advance(S, (d * S.speed) / 6); +} + +// A triangle wave of period 1, 0 at whole numbers and 1 halfway. +const tri = (u) => 1 - abs(2 * (u - floor(u)) - 1); + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const F = S.F, c = S.c, h0 = 0.45 + S.hue, V0 = -0.05; + for (let y = 0; y < H; y++) { + const o = y * W, u = ((y + H - 1) % H) * W, w = ((y + 1) % H) * W; + for (let x = 0; x < W; x++) { + const p = o + x, v = F[p]; + // Relief: the field as a height lit from the upper left. + const gx = F[o + ((x + 1) & 63)] - F[o + ((x + 63) & 63)], gy = F[w + x] - F[u + x]; + const g = -(gx + gy) * 0.6, sh = g / (1 + abs(g)); + // Above a cut just under zero, about half the panel, the dye lights + // from violet to pale aqua; level lines every 0.35 shade it a little. + if (v > V0) { + const t = 1 - exp(-(v - V0) / 0.9), b = tri(v / 0.35); + hsv(h0 + 0.3 - 0.3 * t + 0.06 * sh - 0.03 * b, 0.9 - 0.5 * t - 0.15 * sh, 0.11 + (0.06 + 0.83 * t) * (0.78 + 0.3 * sh) * (0.9 + 0.1 * b), c); + } else { + // Below the cut, the level lines of the mode every 0.15, dim. + const t = exp((v - V0) / 1.2), b = tri(v / 0.15); + hsv(h0 + 0.32 - 0.12 * t + 0.05 * sh, 0.55 + 0.3 * b, min(0.09, (0.02 + 0.045 * t + 0.03 * b) * (1 + 0.3 * sh)), c); + } + display.setPixel(x, y, cl(c[0], 0, 255) | 0, cl(c[1], 0, 255) | 0, cl(c[2], 0, 255) | 0); + } + } +} From 6fc4c49da76f27c514b4b9d3fb1fb9559b1c2c9b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 03:10:58 +0000 Subject: [PATCH 12/20] =?UTF-8?q?54=ED=8C=90:=20stit-tessellation=20?= =?UTF-8?q?=EC=A7=93=EA=B8=B0?= 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_01CtQeRUJHSr3xVvLYqdqFXC --- .../patterns/stit-tessellation.js | 255 ++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 incoming/pattern-study/patterns/stit-tessellation.js diff --git a/incoming/pattern-study/patterns/stit-tessellation.js b/incoming/pattern-study/patterns/stit-tessellation.js new file mode 100644 index 000000000..a6cc27291 --- /dev/null +++ b/incoming/pattern-study/patterns/stit-tessellation.js @@ -0,0 +1,255 @@ +// ===== Patternflow pattern ===== +// Title: STIT Tessellation +// Family: cells +// Idea: The panel starts as one cell, and as in Nagel and Weiss's STIT process every cell waits an exponential time at a rate of its perimeter over pi before a random line among those that hit it cuts it in two, so each new segment flashes white and ends on older ones in T junctions, never crossing them; once the cells reach their density the view dives toward a slowly wandering point while the cutting runs on for just the time that restores the same law, since the tessellation at time t scaled by t is the one at time 1, and cells that leave the panel are dropped. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: STIT tessellations (Werner Nagel and Viola Weiss, 2005) +// Round: 54 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Axes=0..1, Cells=0..1 +// +// Knob 1: Hue. Turns the palette round the wheel. A cut gives its two halves the parent's colour stepped 0.05 to 0.1 round the wheel in opposite directions, one lighter and one darker, kept within 0.34 of the centre, so the oldest lines part families of colour. At 0 the centre is blue and the cells run from lime through cyan and violet to rose; at 0.5 the centre is orange. +// Knob 2: Speed. At 2 the first cells reach their density in 1.5 s; then the view closes in by e^0.24 a second, twice as close every 2.9 s, and about 18 cuts land a second. At 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Axes. The share of the cutting lines that run along the two axes instead of in a uniform direction. At 0 the isotropic STIT; at 1 every line is horizontal or vertical and the cells are rectangles, the Mondrian process (Daniel Roy and Yee Whye Teh, 2008). Between the two a mix, still a STIT. +// Knob 4: Cells. The scale of the dive, which keeps the panel looking at the process at time 1/kappa in LED units, 1 + 384/(pi kappa) + 8192/(pi kappa^2) cells on average: 42 at 0, 46 at the opening, 89 at 0.5, 194 at 1. +// Buttons: K1 tap starts over from one cell: the dive pauses while the STIT grows again from time zero. + +const W = 64, H = 128; +const { floor, min, max, abs, sqrt, sin, cos, exp, log, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// Edge times: the panel's border is never drawn. +const BORDER = -1e9; + +// A cell is a convex polygon in LED units: corners x, y and for each edge, +// from corner i to i + 1, the time it was cut. clk is the exponential mass +// left on its clock; hv and lv its colour. +function props(c) { + const x = c.x, y = c.y, n = x.length; + let a = 0, p = 0, x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9; + for (let i = 0; i < n; i++) { + const j = i + 1 === n ? 0 : i + 1; + a += x[i] * y[j] - x[j] * y[i]; + p += sqrt((x[j] - x[i]) ** 2 + (y[j] - y[i]) ** 2); + if (x[i] < x0) x0 = x[i]; + if (x[i] > x1) x1 = x[i]; + if (y[i] < y0) y0 = y[i]; + if (y[i] > y1) y1 = y[i]; + } + c.area = abs(a) / 2; + c.per = p; + c.x0 = x0; + c.x1 = x1; + c.y0 = y0; + c.y1 = y1; + return c; +} + +const windowCell = () => props({ x: [0, W, W, 0], y: [0, 0, H, H], t: [BORDER, BORDER, BORDER, BORDER], clk: 0, hv: 0, lv: 0 }); + +// The part of cell c where nx x + ny y <= p, or null. The cut edge carries +// time tc; edges cut short keep their own. +function half(c, nx, ny, p, tc) { + const x = c.x, y = c.y, t = c.t, n = x.length, ox = [], oy = [], ot = []; + for (let i = 0; i < n; i++) { + const j = i + 1 === n ? 0 : i + 1; + const si = nx * x[i] + ny * y[i] - p, sj = nx * x[j] + ny * y[j] - p; + if (si <= 0) { + ox.push(x[i]); + oy.push(y[i]); + ot.push(t[i]); + } + if ((si < 0 && sj > 0) || (si > 0 && sj < 0)) { + const f = si / (si - sj); + ox.push(x[i] + f * (x[j] - x[i])); + oy.push(y[i] + f * (y[j] - y[i])); + ot.push(si < 0 ? tc : t[i]); + } + } + if (ox.length < 3) return null; + const o = props({ x: ox, y: oy, t: ot, clk: c.clk, hv: c.hv, lv: c.lv }); + return o.area > 1e-6 ? o : null; +} + +// The rate at which a cell is cut, the measure of the lines that hit it: +// lines in a uniform direction with weight 1 - m give perimeter / pi by +// Cauchy's formula, lines along the two axes with weight m give half the +// width plus half the height. +const rate = (c, m) => ((1 - m) * c.per) / PI + (m * (c.x1 - c.x0 + c.y1 - c.y0)) / 2; + +// A line from that measure among the lines that hit c, as nx x + ny y = p. +function cutLine(c, m, L) { + const bw = c.x1 - c.x0, bh = c.y1 - c.y0; + if (Math.random() * rate(c, m) < (m * (bw + bh)) / 2) { + if (Math.random() * (bw + bh) < bw) (L[0] = 1), (L[1] = 0), (L[2] = c.x0 + Math.random() * bw); + else (L[0] = 0), (L[1] = 1), (L[2] = c.y0 + Math.random() * bh); + return; + } + // Uniform direction, uniform offset across the cell's bounding circle, + // kept when the line meets the cell. + const cx = (c.x0 + c.x1) / 2, cy = (c.y0 + c.y1) / 2, r = sqrt(bw * bw + bh * bh) / 2; + for (;;) { + const th = Math.random() * PI, nx = cos(th), ny = sin(th), p = nx * cx + ny * cy + (2 * Math.random() - 1) * r; + let lo = 1e9, hi = -1e9; + for (let i = 0; i < c.x.length; i++) { + const s = nx * c.x[i] + ny * c.y[i]; + if (s < lo) lo = s; + if (s > hi) hi = s; + } + if (p > lo && p < hi) { + L[0] = nx; + L[1] = ny; + L[2] = p; + return; + } + } +} + +const clock = () => -log(1 - Math.random()); +// Folds v back into -r..r. +const refl = (v, r) => (v > r ? 2 * r - v : v < -r ? -2 * r - v : v); + +function split(S, k, tc) { + const c = S.cells[k], L = S.L; + cutLine(c, S.m, L); + const a = half(c, L[0], L[1], L[2], tc), b = half(c, -L[0], -L[1], -L[2], tc); + if (!a || !b) return; + a.clk = clock(); + b.clk = clock(); + // The two halves take their parent's colour, one a step one way round + // the wheel and lighter or darker, the other the opposite step. + const dh = (Math.random() < 0.5 ? 1 : -1) * (0.05 + 0.05 * Math.random()), dl = Math.random() < 0.5 ? 0.1 : -0.1; + a.hv = refl(c.hv + dh, 0.34); + b.hv = refl(c.hv - dh, 0.34); + a.lv = refl(c.lv + dl, 0.2); + b.lv = refl(c.lv - dl, 0.2); + S.cells[k] = a; + S.cells.push(b); + S.splits++; +} + +export function setup(params) { + params.s = { time: 0, hue: 0, m: 0, kap: 9, T: 0, cells: [windowCell()], splits: 0, L: [0, 0, 0], c: [0, 0, 0], zx: W / 2, zy: H / 2, ph: 0, seen: new Uint8Array(W * H) }; + params.s.cells[0].clk = clock(); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.m = cl(k[2], 0, 1); + S.kap = 9.6 * 0.4167 ** cl(k[3], 0, 1); + if (input.btnPressed && input.btnPressed[0]) { + S.cells = [windowCell()]; + S.cells[0].clk = clock(); + S.T = 0; + } + const tau = d * cl(k[1], 0.2, 3); + S.time += d; + S.ph += tau; + // In LED units the panel shows the process at time 1/kappa. From one + // cell it first runs up to that time, in 1.5 s at Speed 2. Then the dive: + // the view closes in by z on a point that wanders slowly, and the process + // runs on for the time that brings it back to the same law, because Y(t) + // scaled by t has the law of Y(1). STIT's restriction to a window is a + // STIT in that window, so the cells can be clipped to the panel and the + // ones that leave it dropped. + let z = 1, dlt; + if (S.T < 1 / S.kap) { + dlt = min(1 / S.kap - S.T, tau / (3 * S.kap)); + S.T += dlt; + } else { + z = exp(0.12 * tau); + dlt = (1 - 1 / z) / S.kap; + } + S.zx = W / 2 + 14 * sin(0.21 * S.ph); + S.zy = H / 2 + 34 * sin(0.13 * S.ph + 1); + const cells = S.cells, out = []; + for (let i = 0; i < cells.length; i++) { + let c = cells[i]; + if (z !== 1) { + for (let v = 0; v < c.x.length; v++) { + c.x[v] = S.zx + z * (c.x[v] - S.zx); + c.y[v] = S.zy + z * (c.y[v] - S.zy); + } + props(c); + if (c.x1 <= 0 || c.x0 >= W || c.y1 <= 0 || c.y0 >= H) continue; + if (c.x0 < 0) c = half(c, -1, 0, 0, BORDER); + if (c && c.x1 > W) c = half(c, 1, 0, W, BORDER); + if (c && c.y0 < 0) c = half(c, 0, -1, 0, BORDER); + if (c && c.y1 > H) c = half(c, 0, 1, H, BORDER); + } + if (c) out.push(c); + } + S.cells = out; + // Each cell's clock runs at its rate; when it runs out the cell is cut. + const n = out.length; + for (let i = 0; i < n; i++) { + const c = out[i]; + c.clk -= rate(c, S.m) * dlt; + if (c.clk <= 0) split(S, i, S.time); + } +} + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + o[1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + o[2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; + return o; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const seen = S.seen, c = S.c, now = S.time, ex = [], ey = [], ed = [], et = []; + seen.fill(0); + for (const cell of S.cells) { + hsv(S.hue + 0.62 + cell.hv, 0.7, 0.78 + cell.lv, c); + const r = c[0], g = c[1], b = c[2]; + // The drawn edges: unit normal, offset and the time of the cut. + const x = cell.x, y = cell.y, t = cell.t, n = x.length; + ex.length = ey.length = ed.length = et.length = 0; + for (let i = 0; i < n; i++) { + if (t[i] === BORDER) continue; + const j = i + 1 === n ? 0 : i + 1, dx = x[j] - x[i], dy = y[j] - y[i], l = sqrt(dx * dx + dy * dy) || 1; + ex.push(-dy / l); + ey.push(dx / l); + ed.push((-dy * x[i] + dx * y[i]) / l); + et.push(t[i]); + } + // Fill the LEDs whose centres the cell holds, row by row, with a dark + // line where a centre lies within 0.62 LED of an edge, flashing white + // for half a second after the cut. + const j0 = max(0, Math.ceil(cell.y0 - 0.5)), j1 = min(H - 1, floor(cell.y1 - 0.5)); + for (let row = j0; row <= j1; row++) { + const yc = row + 0.5; + let xl = 1e9, xr = -1e9; + for (let i = 0; i < n; i++) { + const j = i + 1 === n ? 0 : i + 1; + if ((y[i] - yc) * (y[j] - yc) > 0 || y[i] === y[j]) continue; + const xx = x[i] + ((yc - y[i]) / (y[j] - y[i])) * (x[j] - x[i]); + if (xx < xl) xl = xx; + if (xx > xr) xr = xx; + } + const i0 = max(0, Math.ceil(xl - 0.5)), i1 = min(W - 1, Math.ceil(xr - 0.5) - 1); + for (let col = i0; col <= i1; col++) { + const p = row * W + col; + if (seen[p]) continue; + seen[p] = 1; + const xc = col + 0.5; + let dm = 9, tm = BORDER; + for (let e = 0; e < ex.length; e++) { + const dd = abs(ex[e] * xc + ey[e] * yc - ed[e]); + if (dd < dm) (dm = dd), (tm = et[e]); + } + const w = cl((0.62 - dm) / 0.3, 0, 1), f = exp(-(now - tm) / 0.5) * 255; + display.setPixel(col, row, (r + w * (f - r)) | 0, (g + w * (f - g)) | 0, (b + w * (f - b)) | 0); + } + } + } + for (let p = 0; p < W * H; p++) if (!seen[p]) display.setPixel(p % W, (p / W) | 0, 0, 0, 0); +} From 22da0f7879e96412ed80474fb901befcab99e016 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 03:15:51 +0000 Subject: [PATCH 13/20] =?UTF-8?q?54=ED=8C=90:=20ulam-sequence=20=EC=A7=93?= =?UTF-8?q?=EA=B8=B0?= 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_01CtQeRUJHSr3xVvLYqdqFXC --- .../pattern-study/patterns/ulam-sequence.js | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 incoming/pattern-study/patterns/ulam-sequence.js diff --git a/incoming/pattern-study/patterns/ulam-sequence.js b/incoming/pattern-study/patterns/ulam-sequence.js new file mode 100644 index 000000000..b8fe4d4a6 --- /dev/null +++ b/incoming/pattern-study/patterns/ulam-sequence.js @@ -0,0 +1,222 @@ +// ===== Patternflow pattern ===== +// Title: Ulam Sequence +// Family: number +// Idea: The Ulam sequence counted live, each new term the least number that is a sum of two earlier terms in exactly one way, wound round a circle by the hidden frequency alpha that the code finds for itself: every term sits at the angle alpha times the term, across the panel, and lower the larger it is, so the terms fill only the half of the circle where cos(alpha a) < 0, in two humps, and leave the other half empty but for 2, 3, 47 and 69, while each new one drops into the pile below. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Ulam sequence (Stanisław Ulam, 1964), with the hidden frequency Stefan Steinerberger found (2017) +// Round: 54 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Seed=0..5, Detune=-1..1 +// +// Knob 1: Hue. The colour of the terms, with the ones off the pile in a hue a fifth of the wheel back: gold with pink at 0, green with yellow at 0.25, blue with sea green at 0.5, magenta with blue at 0.75. +// Knob 2: Speed. New terms a second, 40 for each unit: 80 at the opening 2. At 0.2 there are 8 a second, at 3 there are 120. Each new term falls from the foot of the coil into its column of the pile in 0.4 s. After 16,000 terms (the last of U(1,2) is 213,514) the count rests 4 s and starts again from the first 600. +// Knob 3: Seed. Which sequence: U(1,2) at 0, U(1,3) at 1, U(1,4) at 2, U(2,3) at 3, U(3,4) at 4 and U(2,5) at 5. The code finds each frequency itself as the alpha in (0, pi] where |sum of exp(i alpha a)| over the terms is largest, and finds it again each time the count doubles: 2.5714 for U(1,2) (Steinerberger gives 2.5714474995), 2.8335 for U(1,3), 0.5060 for U(1,4), 1.1650 for U(2,3), 2.2090 for U(3,4). U(2,5) has just two even terms, 2 and 12 (James Schmerl and Eugene Spiegel, 1994), so its frequency is pi and its pile is one spike. The terms off the pile are 2, 3, 47 and 69 for U(1,2), 4 and 98 for U(1,3), 1 and 10 for U(1,4), 5 for U(2,3), 3 and 11 for U(3,4). +// Knob 4: Detune. Moves alpha off the found value: not at all within 0.1 of the middle (it opens at 0.06), then as the square of the rest, to 0.002 either way at the ends. The error in the angle grows with the term, so at 0.5 (0.0004 off) the band leans more and more towards the foot of the coil and winds into stripes that cross the empty half in pink; at 1 the pile is flat. + +const W = 64, H = 128; +const { floor, min, max, abs, sqrt, sin, cos, exp, log, PI, round } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + o[1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + o[2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; + return o; +} + +// The seeds K3 steps through. +const SEEDS = [[1, 2], [1, 3], [1, 4], [2, 3], [3, 4], [2, 5]]; +// Terms kept at most, and the counting array: C[m] is how many ways m is a +// sum of two distinct terms so far, stopped at 2. +const NMAX = 16000, LIM = 1 << 19, HEAD = 600; +// Rows of the coil (the number line wound on the circle) and of the pile. +const CH = 86, PB = 34; + +function start(S, seed) { + const [p, q] = SEEDS[seed]; + S.seed = seed; + S.C.fill(0); + S.T[0] = p, S.T[1] = q, S.n = 2; + S.C[p + q] = 1; + S.alpha = 0, S.fitN = 0, S.acc = 0, S.full = 0; + S.born.fill(-9); + while (S.n < HEAD) next(S); + fit(S); +} + +// The next Ulam term: the least m above the last term that is a sum of two +// distinct earlier terms in exactly one way. Then its sums with every +// earlier term are counted. +function next(S) { + const T = S.T, C = S.C, n = S.n; + let m = T[n - 1] + 1; + while (m < LIM >> 1 && C[m] !== 1) m++; + if (m >= LIM >> 1 || n >= NMAX) return false; + T[n] = m; + for (let j = 0; j < n; j++) { + const s = m + T[j]; + if (s < LIM && C[s] < 2) C[s]++; + } + S.n = n + 1; + return true; +} + +// |sum of exp(i alpha a)|^2 over the first n terms. +function power(T, n, a) { + let re = 0, im = 0; + for (let j = 0; j < n; j++) (re += cos(a * T[j])), (im += sin(a * T[j])); + return re * re + im * im; +} + +// The same at K points a0, a0 + step, ..., each term's phase turned on by a +// fixed step from the last; returns the best point. +const CR = new Float64Array(NMAX), CI = new Float64Array(NMAX), WR = new Float64Array(NMAX), WI = new Float64Array(NMAX); +function scan(T, n, a0, step, K) { + for (let j = 0; j < n; j++) (CR[j] = cos(a0 * T[j])), (CI[j] = sin(a0 * T[j])), (WR[j] = cos(step * T[j])), (WI[j] = sin(step * T[j])); + let best = -1, at = a0; + for (let k = 0; k <= K; k++) { + let re = 0, im = 0; + for (let j = 0; j < n; j++) { + re += CR[j], im += CI[j]; + const r = CR[j] * WR[j] - CI[j] * WI[j]; + CI[j] = CR[j] * WI[j] + CI[j] * WR[j], CR[j] = r; + } + const p = re * re + im * im; + if (p > best) (best = p), (at = a0 + k * step); + } + return at; +} + +// The hidden frequency: the alpha in (0, pi] where the terms' Fourier sum +// is largest. The first time, a scan of the first 200 terms from 0.01 to pi +// in steps of a fifth of the peak's width; each time the count doubles, a +// scan of three old peak widths round the last value with all the terms. +// Then the top of a parabola through the best point and its neighbours, +// twice, the second time five times closer. +function fit(S) { + const T = S.T, n = S.n; + let at, h; + if (!S.alpha) { + h = (0.2 * TAU) / T[199]; + at = scan(T, 200, 0.01, h, Math.ceil((PI - 0.01) / h)); + } else { + h = (0.2 * TAU) / T[n - 1]; + const w = (1.5 * TAU) / T[S.fitN - 1]; + at = scan(T, n, S.alpha - w, h, Math.ceil((2 * w) / h)); + } + for (let i = 0; i < 2; i++, h /= 5) { + const f0 = power(T, n, at - h), f1 = power(T, n, at), f2 = power(T, n, at + h), q = f0 - 2 * f1 + f2; + if (q < 0) at += (cl((0.5 * (f0 - f2)) / q, -1, 1)) * h; + } + S.alpha = at; + S.fitN = n; +} + +export function setup(params) { + const S = (params.s = { + hue: 0, speed: 2, seed: 0, det: 0, alpha: 0, fitN: 0, acc: 0, n: 0, full: 0, t: 0, + T: new Float64Array(NMAX), C: new Uint8Array(LIM), born: new Float64Array(NMAX), + G: new Float64Array(W * CH), R: new Float64Array(CH), P: new Float64Array(W), c: [0, 0, 0], + }); + start(S, 0); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + const seed = cl(round(k[2]), 0, SEEDS.length - 1); + if (seed !== S.seed) start(S, seed); + // Detune: nothing within 0.1 of the middle, then up to 0.002 either way. + const u = cl(abs(k[3]) - 0.1, 0, 0.9) / 0.9; + S.det = (k[3] < 0 ? -1 : 1) * 0.002 * u * u; + S.t += d; + // New terms, 40 a second for each unit of Speed. + S.acc += 40 * S.speed * d; + while (S.acc >= 1) { + S.acc -= 1; + if (!next(S)) { + S.full += d; + break; + } + S.born[S.n - 1] = S.t; + } + if (S.n >= 2 * S.fitN) fit(S); + if (S.full > 4) start(S, S.seed); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const T = S.T, n = S.n, G = S.G, R = S.R, P = S.P, c = S.c, al = S.alpha + S.det; + G.fill(0), R.fill(0), P.fill(0); + // Rows of the coil go as log(a + 16), from a = 1 at the top to the + // largest term at the bottom; columns are alpha a modulo 2 pi. + const l0 = log(17), sc = (CH - 1) / (log(T[n - 1] + 16) - l0); + let pm = 1; + for (let j = 0; j < n; j++) { + const a = T[j], x = floor(((al * a) % TAU) * (W / TAU)) & 63, y = (log(a + 16) - l0) * sc, y0 = floor(y), f = y - y0; + G[y0 * W + x] += 1 - f, R[y0] += 1 - f; + if (y0 + 1 < CH) (G[(y0 + 1) * W + x] += f), (R[y0 + 1] += f); + if (++P[x] > pm) pm = P[x]; + } + const h0 = 0.12 + S.hue; + // A cell's light goes with its count against the row's share of terms + // for each of the 24 columns the band spans, so sparse rows near the top + // show single terms and dense rows the band's shape. + for (let y = 0; y < CH; y++) { + const q = 1.2 / max(0.4, R[y] / 24); + for (let x = 0; x < W; x++) { + const b = 1 - exp(-G[y * W + x] * q); + hsv(h0 + 0.05 * b, 0.75 - 0.3 * b, b, c); + display.setPixel(x, y, c[0] | 0, c[1] | 0, c[2] | 0); + } + } + // The terms with cos(alpha a) >= 0, on the empty half of the circle, in + // pink with a dim cross round each. + for (let j = 0; j < n; j++) { + const a = T[j]; + if (cos(al * a) < 0) continue; + const x = floor(((al * a) % TAU) * (W / TAU)) & 63, y = min(CH - 1, round((log(a + 16) - l0) * sc)); + hsv(h0 + 0.8, 0.7, 0.45, c); + for (const [dx, dy] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) { + const yy = y + dy; + if (yy >= 0 && yy < CH) display.setPixel((x + dx) & 63, yy, c[0] | 0, c[1] | 0, c[2] | 0); + } + hsv(h0 + 0.8, 0.5, 1, c); + display.setPixel(x, y, c[0] | 0, c[1] | 0, c[2] | 0); + } + // The pile: how many terms in each 64th of the circle, on a bottom row + // that marks the half where cos(alpha a) < 0 apart from the other half. + for (let x = 0; x < W; x++) { + const hgt = (P[x] / pm) * PB; + for (let z = 0; z < PB; z++) { + const b = cl(hgt - z, 0, 1); + if (b <= 0) break; + hsv(h0 + 0.03 * (z / PB), 0.85 - 0.4 * (z / PB), b * (0.62 + 0.38 * (z / PB)), c); + display.setPixel(x, H - 2 - z, c[0] | 0, c[1] | 0, c[2] | 0); + } + if (x >= 16 && x < 48) hsv(h0 + 0.5, 0.6, 0.3, c); + else hsv(h0 + 0.8, 0.6, 0.3, c); + display.setPixel(x, H - 1, c[0] | 0, c[1] | 0, c[2] | 0); + } + // Each new term falls from the bottom of the coil into its column of + // the pile in 0.4 s. + for (let j = n - 1; j >= 2 && S.t - S.born[j] < 0.4; j--) { + const a = T[j], x = floor(((al * a) % TAU) * (W / TAU)) & 63, u = (S.t - S.born[j]) / 0.4; + const top = H - 2 - (P[x] / pm) * PB, y = CH - 1 + (top - CH + 1) * u * u; + for (let i = 0; i < 3; i++) { + const yy = round(y) - i; + if (yy < CH || yy >= H) continue; + hsv(h0 + 0.02, 0.35 + 0.2 * i, 1 - 0.3 * i, c); + display.setPixel(x, yy, c[0] | 0, c[1] | 0, c[2] | 0); + } + } +} From 13ddef71a5a3e824d40286e14516f0bea5f7a0a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 03:16:43 +0000 Subject: [PATCH 14/20] =?UTF-8?q?54=ED=8C=90:=20strange-eigenmode=20?= =?UTF-8?q?=EB=85=B8=EB=B8=8C=20=EA=B8=80=20=EB=8B=A4=EB=93=AC=EA=B8=B0?= 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_01CtQeRUJHSr3xVvLYqdqFXC --- incoming/pattern-study/patterns/strange-eigenmode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/incoming/pattern-study/patterns/strange-eigenmode.js b/incoming/pattern-study/patterns/strange-eigenmode.js index bc38784c0..0702b07c8 100644 --- a/incoming/pattern-study/patterns/strange-eigenmode.js +++ b/incoming/pattern-study/patterns/strange-eigenmode.js @@ -12,7 +12,7 @@ // @knobs Hue=0..1, Speed=0.2..3, Stir=-1..1, Diffusion=0..1 // // Knob 1: Hue. The colour of the dye. At 0 the positive half runs from violet where it is faint to pale aqua where it is strongest, and the negative half shows as dim indigo level lines; 0.25 turns it crimson to lilac, 0.5 olive through amber to rose, 0.75 teal through green to pale gold, and 1 is back at violet and aqua. -// Knob 2: Speed. One period of the flow takes 6 / Speed seconds: 3 s at the opening 2, each half 1.5 s with the slide easing in and out and resting a fifth of a second at either end. At 0.2 a period takes 30 s and at 3 it takes 2 s. The dye takes the same path at any speed. +// Knob 2: Speed. One period of the flow takes 6 / Speed seconds: 3 s at the opening 2, each half 1.5 s with the slide easing in and out and resting a fifth of a second at either end. At 0.2 a period takes 30 s and at 3 it takes 2 s; Speed only changes how fast the same flow plays. // Knob 3: Stir. The amplitude A of both sines in units of the torus, which the panel draws 64 LEDs wide and 128 tall, so a row slides up to 64 A LEDs and a column up to 128 A. At 0, A = 1.2 (77 and 154 LEDs): global chaos, the leading Floquet multiplier 0.39 well ahead of the next, 0.28, so one shape returns every period at 39% of its contrast, a decay rate of 0.95 a period. At -1, A = 0.45: islands of regular flow keep their dye, which leaves only by diffusion, and the pattern fades only 10% a period. At -0.5 (A = 0.82) and +0.5 (A = 1.4) the leading multipliers are a complex pair, 137 and 136 degrees round, and the pattern swings between shapes without repeating. At +1 (A = 1.6) the leading multiplier is negative, -0.41: the pattern returns as its own negative every period, bright where it was dark. // Knob 4: Diffusion. The diffusivity, 4 A times 30 to the power of the knob, in LED^2 a period: 5.9 at the opening 0.06, where the stripes are 2.9 LEDs across (rms over rms gradient). At 0.5 it is 26 (6 LEDs). At 1 it is 144 (12 LEDs): a few broad bands are left, and the decay rate rises to 1.6 a period. Any thinner than at 0 and the stripes would be finer than the LEDs. // Buttons: K1 tap pours fresh dye, a few random waves, which the flow stirs on screen until, after about a dozen periods, the same mode is back. From 748662d363a4ba366f114bf7bd0f29295a36a85a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 03:24:36 +0000 Subject: [PATCH 15/20] =?UTF-8?q?54=ED=8C=90:=20arcsine-law=20=EB=8F=99?= =?UTF-8?q?=EC=A0=84=20=EA=B2=B0=EC=9D=84=20=EB=B0=9D=EA=B8=B0=EB=A1=9C=20?= =?UTF-8?q?=EA=B7=B8=EB=A0=A4=20=EC=B4=98=EC=B4=98=ED=95=A8=20=C3=97=20?= =?UTF-8?q?=EB=AA=A8=EC=96=91=20=EB=B0=94=EB=80=9C=204,3=20=EC=97=90=20?= =?UTF-8?q?=EC=84=B8=EC=9A=B0=EA=B8=B0?= 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_01CtQeRUJHSr3xVvLYqdqFXC --- .../pattern-study/patterns/arcsine-law.js | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/incoming/pattern-study/patterns/arcsine-law.js b/incoming/pattern-study/patterns/arcsine-law.js index b96a793bd..65b44355c 100644 --- a/incoming/pattern-study/patterns/arcsine-law.js +++ b/incoming/pattern-study/patterns/arcsine-law.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Arcsine Law // Family: data -// Idea: 48 fair random walks come down the panel a row a step, each LED warm while its walk is above zero and cool below, and most columns turn out long stretches of one colour; after 126 steps the warm cells rise to the top of each column and the columns sort themselves by how many there are, so the boundary between the colours draws the arcsine quantile curve, while a histogram beside them gathers every walk so far into Lévy's U-shaped law, its exact values marked by white dots. +// Idea: 48 fair random walks come down the panel a row a step, each LED one toss of the coin, bright for a step up and dark for a step down, coloured warm while its walk is above zero and cool below, so the tosses look like noise while most columns run in long stretches of one colour; after 126 steps the warm cells rise to the top of each column and the columns sort themselves by how many there are, so the boundary between the colours draws the arcsine quantile curve, while a histogram beside them gathers every walk so far into Lévy's U-shaped law, its exact values marked by white dots. // Author: patternflow-lab // Date: 2026-10-26 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -11,10 +11,10 @@ // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Statistic=0..2, Drift=-1..1 // -// Knob 1: Hue. Turns every colour around the wheel together. At 0 a walk above zero is amber and below zero azure, pale near zero and deeper the further out it is, with lavender histogram bars; at 0.25 green above and magenta below, at 0.5 the two sides trade colours. -// Knob 2: Speed. 16 rows a second per unit: 32 at the opening 2, so a batch of 126 steps comes down in 3.9 s, settles and sorts in 1.4 s and holds for 1.4 s; at 0.2 a batch takes 39 s to come down, at 3 it takes 2.6 s. Walks that are not drawn feed the histogram at 120 a second per unit. +// Knob 1: Hue. Turns every colour around the wheel together. At 0 a walk above zero is amber and below zero azure, pale near zero and deeper the further out it is, a step down drawn at a third of the brightness of a step up, with lavender histogram bars; at 0.25 green above and magenta below, at 0.5 the two sides trade colours. +// Knob 2: Speed. 20 rows a second per unit: 40 at the opening 2, so a batch of 126 steps comes down in 3.2 s, settles and sorts in 1.4 s and holds for 1.4 s; at 0.2 a batch takes 32 s to come down, at 3 it takes 2.1 s. Walks that are not drawn feed the histogram at 120 a second per unit. // Knob 3: Statistic. Which of Lévy's three the columns sort by and the histogram counts. At 0 the time above zero: the warm cells rise to the top of their column. At 1 the last zero: the cells before each walk's last return to zero are dimmed, leaving a bright tail of one colour below it. At 2 the first time of the maximum: the cells after each walk's highest point so far are dimmed. All three histograms settle on the same U. -// Knob 4: Drift. A step goes up with chance (1 + mu) / 2. mu is 0 within 0.08 of the middle, so the opening 0.06 is the fair walk, then grows as the square to 0.014 at 0.3, 0.052 at 0.5 and 0.25 at either end, where nearly every column floods one colour. Any drift breaks the law: at 0.5 the histogram of time above zero lies 0.19 from it in Kolmogorov distance, while the last zero, which only asks whether the walk comes back, lies 0.047 from it. +// Knob 4: Drift. A step goes up with chance (1 + mu) / 2. mu is 0 within 0.08 of the middle, so the opening 0.06 is the fair walk, then grows as the square to 0.014 at 0.3, 0.052 at 0.5 and 0.25 at either end, where nearly every column floods one colour. Any drift breaks the law: at 0.5 the histogram of time above zero lies 0.19 from it in Kolmogorov distance, while the last zero, which only asks whether the walk comes back, lies 0.051 from it. const W = 64, H = 128; // 48 walks of 126 steps, one a column, step t drawn in row t - 1; then a @@ -162,13 +162,16 @@ function plan(S) { S.sortStat = S.stat; } -// A cell's colour: warm above zero and cool below, paler near zero and -// deeper the further the walk is out in units of sqrt(t); dimmed before -// the last zero or after the maximum when those are the statistic. +// A cell is one toss of the coin: bright when the step went up, dark when +// it went down, so the tosses read as noise. Its hue is the sign of the +// running total, warm above zero and cool below, paler near zero and +// deeper the further the walk is out in units of sqrt(t), and that hue +// runs in long stretches. Cells are dimmed before the last zero or after +// the maximum when those are the statistic. function shade(S, i, t, out) { - const o = i * (N + 1), a = S.S[o + t - 1], b = S.S[o + t], m = min(1, abs(a + b) / (2.2 * sqrt(t))); - if (a + b > 0) hsv(0.11 + S.hue, 0.3 + 0.7 * m, 1, out); - else hsv(0.6 + S.hue, 0.3 + 0.65 * m, 1, out); + const o = i * (N + 1), a = S.S[o + t - 1], b = S.S[o + t], m = min(1, abs(a + b) / (2.2 * sqrt(t))), up = b > a; + if (a + b > 0) hsv(0.11 + S.hue, 0.3 + 0.7 * m, up ? 1 : 0.3, out); + else hsv(0.6 + S.hue, 0.3 + 0.65 * m, up ? 0.85 : 0.25, out); let dim = false; if (S.stat === 1) dim = t <= S.lz[i]; else if (S.stat === 2) dim = t > S.tm[i]; @@ -195,19 +198,15 @@ export function update(dt, input, params) { S.histMu = S.mu; } if (!S.init) { - // A first batch, run and sorted at once, is the picture the panel - // opens on while the next one comes down over it. + // The first batch comes down a dark panel; each later one comes down + // over the sorted picture of the one before. S.init = true; newBatch(S); - while (S.row < N) stepRow(S); - plan(S); - keep(S, 0.75); - newBatch(S); } // Walks nobody sees feed the histogram, 8 a frame at Speed 2. S.hidden = (S.hidden || 0) + (d * 30 * 8 * S.speed) / 2; while (S.hidden >= 1) hiddenWalk(S), S.hidden--; - const rate = 16 * S.speed, slow = sqrt(2 / S.speed); + const rate = 20 * S.speed, slow = sqrt(2 / S.speed); S.clock += d; if (S.phase === 0) { const want = min(N, floor(S.clock * rate)); @@ -221,21 +220,21 @@ export function update(dt, input, params) { } else { if (S.stat !== S.sortStat) plan(S), (S.clock = 0), (S.phase = 1); if (S.phase === 1 && S.clock > 1.4 * slow) (S.phase = 2), (S.clock = 0); - if (S.phase === 2 && S.clock > 1.4 * slow) keep(S, 0.75), newBatch(S); + if (S.phase === 2 && S.clock > 1.4 * slow) keep(S), newBatch(S); } } -// Store the sorted picture of the batch, dimmed by f, as the backdrop. -function keep(S, f) { +// Store the sorted picture of the batch as the backdrop. +function keep(S) { const P = S.prev, c = S.c; P.fill(0); for (let i = 0; i < NW; i++) for (let t = 1; t <= N; t++) { shade(S, i, t, c); const q = (S.R[i * N + t - 1] * NW + S.X[i]) * 3; - P[q] = c[0] * f; - P[q + 1] = c[1] * f; - P[q + 2] = c[2] * f; + P[q] = c[0]; + P[q + 1] = c[1]; + P[q + 2] = c[2]; } } From 010716bc56894d4142822a0246184888d76cf7e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 03:29:16 +0000 Subject: [PATCH 16/20] =?UTF-8?q?54=ED=8C=90:=20README=20=EC=97=B0?= =?UTF-8?q?=EA=B5=AC=20=EB=85=B8=ED=8A=B8,=20Ulam=20Sequence=20=EC=9D=98?= =?UTF-8?q?=20Idea=20=EC=A4=84=20=EC=85=8B=20=EB=82=98=EC=97=B4=20?= =?UTF-8?q?=EA=B3=A0=EC=B9=98=EA=B8=B0?= 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_01CtQeRUJHSr3xVvLYqdqFXC --- incoming/pattern-study/README.md | 75 ++++++++++++++++++- .../pattern-study/patterns/ulam-sequence.js | 2 +- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 0250d1c5b..b231044b2 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 428개를 쉰여섯 갈래로 나눠 만들었어요. 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판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 438개를 쉰여섯 갈래로 나눠 만들었어요. 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판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 54판은 할로윈 몫이 쉬어서 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. [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판에서 세로로 다시 짰어요. @@ -690,6 +690,21 @@ | [Lost Head](patterns/lost-head.js) | 할로윈 | 달 뜬 묘지에서 목 없는 해골이 묘비 위 머리를 하나씩 써 보며 너무 크거나 작은 머리, 모자 쓴 머리, 무는 머리와 호박을 으쓱하며 내려놓다가, 제 머리가 맞으면 눈구멍이 독한 초록으로 켜지고 춤을 춰요. 그러다 머리가 떨어져 굴러가요. K3 은 틀린 머리 수, K4 는 크기가 틀린 정도예요. | | [Creaking Door](patterns/creaking-door.js) | 할로윈 | 보라 복도 끝의 문이 다섯 번 덜컹이며 삐걱 열리면 촛불 빛이 쐐기로 넓어지며 바닥과 왼쪽 벽을 타고, 그 빛 속으로 손가락이 긴 손 그림자가 기어 나오거나 뾰족 모자의 그림자가 지나가요. 문이 쾅 닫히면 판이 흔들리고 먼지가 떨어지고, 문 밑 빛줄기를 두 발이 지나가요. K3 은 빛이 닿는 거리, K4 는 공기 속 먼지이고, K1 을 누르면 문이 닫히거나 열려요. | +### 54판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Nodal Domains](patterns/nodal-domains.js) | 노이즈·흐름 | 같은 파장의 평면파 40개를 무작위 방향으로 겹친 베리의 무작위 파동을 0 근처에서 잘라, 부호가 같은 이웃끼리 이어진 마디 영역마다 납작한 한 색을 칠해요. 0 위의 영역은 작을 때 장미색이다가 커지면 주황을 지나 금색이 되고 0 아래는 파랑 보라에서 청록으로 가며, 가장 큰 영역은 옅은 하늘색으로 패널을 가로지르고, 파동마다 위상이 제 빠르기로 돌아서 안장점에서 영역이 이어지고 끊길 때마다 흰 불꽃이 튀어요. K3 은 파장, K4 는 자르는 높이예요. | +| [Strange Eigenmode](patterns/strange-eigenmode.js) | 시뮬레이션 | 한 변 1 인 토러스에 푼 물감을 반 주기는 행이 옆으로, 반 주기는 열이 위아래로 미끄러지는 피어험버트의 사인 흐름이 줄무늬로 늘이고 접는데, 매 프레임 밝기를 되돌려 놓아서 보라에서 옅은 청록으로 빛나는 줄무늬가 3초마다 똑같은 모양으로 돌아오는 동안 참 세기는 주기마다 39% 로 줄어요. K3 은 젓는 세기, K4 는 확산이고, K1 을 누르면 새 물감을 부어 다시 저어요. | +| [Pearcey Cusp](patterns/pearcey-cusp.js) | 물결·간섭 | LED 마다 피어시 적분을 셈한 파동장이 패널 위쪽의 밝은 초점에서 아래로 첨점 모양으로 열리고, 그 안에 밝은 점 격자와 정확한 영점 76개가 서는데, 영점은 위상이 감기는 쪽에 따라 분홍과 호박색 점으로 찍히고 물마루는 빛이 가는 대로 아래로 흘러요. K3 은 파수를 올려 무늬를 광선의 첨점 곡선 쪽으로 촘촘하게 하고, K4 는 t³ 항으로 첨점을 옆으로 기울여요. | +| [STIT Tessellation](patterns/stit-tessellation.js) | 보로노이·셀 | 패널이 한 칸에서 시작해, 칸마다 둘레에 비례하는 빠르기로 기다리다 그 칸을 지나는 무작위 직선에 둘로 잘리고, 새 선분은 하얗게 번쩍이며 옛 선분에 T 자로 닿을 뿐 가로지르지 않아요. 칸이 제 밀도에 이르면 화면이 떠도는 한 점으로 다이빙하고, 잘림이 같은 법칙을 되찾을 만큼만 이어져서 칸은 끝없이 다가오며 갈라져요. K3 은 축을 따르는 선의 몫이라 1 이면 몬드리안 과정이고, K4 는 패널에 서는 칸의 수예요. | +| [Arcsine Law](patterns/arcsine-law.js) | 데이터 그림 | 공정한 동전 던지기 걸음 48개가 열마다 한 걸음에 한 줄씩 내려오며, 위로 간 걸음은 밝고 아래로 간 걸음은 어두워서 동전은 잡음처럼 흩어지는데, 0 위면 호박색이고 0 아래면 하늘색인 색은 대부분의 열에서 한 가지로 길게 이어져요. 126걸음이 끝나면 열마다 호박색 칸이 위로 떠올라 모이고 열이 그 수대로 줄을 서서 두 색의 경계가 아크사인 분위 곡선을 그리고, 옆의 히스토그램은 지금까지의 모든 걸음을 모아 레비의 U 자 법칙으로 다가가요. K3 은 줄 세우는 통계를 고르고, K4 는 걸음의 치우침이에요. | +| [Størmer Trap](patterns/stormer-trap.js) | 입자 | 패널을 가로질러 눕힌 자기 쌍극자의 장에서 대전 입자 48개가 위아래 끝까지 서는 희미한 자기력선 고리를 감아 돌며 가운데 가까운 거울점에서 되튀고, 양전하는 한쪽으로 음전하는 반대쪽으로 축을 돌며 천천히 떠돌다가 손실 원뿔에 든 몇 개만 가운데 작은 구에 닿아 번쩍이는데, K3 은 에너지를, K4 는 적도에서의 피치각을 정해요. | +| [Stable Marriage](patterns/stable-marriage.js) | 알고리즘 | 위 16명과 아래 16명 사이의 지연 수락에서 청혼마다 128줄을 가로지르는 선이 날아가 받아들여지면 남고 밀려나면 가운데서 끊기며, 위쪽 안정 매칭이 서면 아래쪽이 같은 목록으로 청혼해서 선 끝의 순위 색이 뒤집히고 두 안정 매칭이 다른 쌍만 번갈아 바뀌는데, K3 은 모두가 한 순위를 나누는 정도, K4 는 아예 받지 않는 상대의 몫이에요. | +| [Smale Horseshoe](patterns/smale-horseshoe.js) | 프랙탈 | 가운데 정사각형을 옆으로 누르고 위로 늘인 띠를 꺾어 다시 덮는 스메일 말굽이 한 번씩 접힐 때마다, 위로 오르는 금빛 가닥과 내려가는 하늘색 가닥 32개가 정사각형을 세로로 가로지르고 위에서는 겹친 아치로, 아래에서는 겹친 U 자로 돌아요. 가로 띠와 겹치는 곳이 하얗게 빛나는 불변 칸토어 집합이고, K3 은 세로 가닥과 가로 띠 가운데 무엇을 보일지, K4 는 한 번 접을 때 누르는 비율 λ 예요. | +| [Ulam Sequence](patterns/ulam-sequence.js) | 수론 | 울람 수열을 1초에 80 항씩 세면서 코드가 스스로 찾은 숨은 진동수 α 로 각 항을 원에 감아 가로로 펼치면, 항들은 cos(αa) < 0 인 절반에만 두 봉우리로 쌓이고 다른 절반은 분홍으로 표시한 2, 3, 47, 69 넷만 남긴 채 비어요. 새 항은 아래 더미로 떨어지고, K3 은 씨앗을 바꾸고 K4 는 α 를 비틀어 띠를 감아 버려요. | +| [Weaire-Phelan](patterns/weaire-phelan.js) | 공간·원근 | A15 격자의 보로노이 칸 48개로 쌓은 키 큰 거품 기둥이 천천히 돌며, 청록 오각형만 두른 십이면체와 금색 육각형 둘을 낀 십사면체가 흰 모서리 아래 맞물린 위어-펠런 구조를 보여 주는데, K3 을 올리면 기둥 위쪽부터 같은 부피의 켈빈 깎은 팔면체가 보라 정사각형을 달고 들어서고 K4 는 칸을 제 자리 쪽으로 줄여 틈을 벌려요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -2676,6 +2691,50 @@ E8 의 근 240개를 고셋의 4₂₁ 다면체 꼭짓점으로 그렸어요. 주제 6 으로 어두운 복도 끝에서 삐걱 열리는 문을 그렸어요. 패널의 LED 8,192개마다 눈에서 광선을 쏘아 너비 1.2 m, 높이 2.4 m 인 복도와 3 m 앞 벽의 문을 찾고, 빛을 받는 점마다 촛불까지 그은 선이 문틀 안을 지나는지, 돌아간 문짝에 막히는지 따져요. 문은 0.9×2.0 m 라 30×66 LED 로 서요. 촛불은 문 가운데보다 0.75 m 오른쪽, 문틀에서 1.4 m 안쪽 모퉁이에 있어 눈에는 보이지 않아요. 문이 경첩을 축으로 방 안쪽으로 다섯 번 덜컹이며 54~69도까지 열리는 동안, 빛은 오른쪽 문설주에서 가는 줄로 나와 왼쪽 아래로 넓어지는 쐐기가 되고 왼쪽 벽을 타고 올라가요. 처음 값에서 따뜻한 빛을 받은 LED 는 0.04초에 274개, 2초에 1,930개, 4초에 4,567개이고, 문이 닫히면 문 밑 틈의 137개만 남아요. 빛 속으로 손가락이 긴 손의 그림자가 문턱에서 눈 쪽으로 기어 나와 손가락을 오므렸다 물러나고, 다음 판에는 뾰족 모자를 쓴 누군가의 그림자가 걸어서 쐐기를 가로질러요. 한 판의 6.2~7초에 문이 0.14초 만에 쾅 닫히면 판이 0.5초 동안 3 LED 까지 흔들리고 상인방에서 먼지 28개가 떨어져요. 어둠 속에서는 문 밑 빛줄기를 두 발이 오른쪽에서 왼쪽으로 지나가고, 잠시 뒤 열쇠 구멍의 빛이 0.5초 꺼져요. 첫 판 뒤로는 열 판에 셋쯤 문이 절반만 열리다 일찍 닫혀요. 촛불 빛은 0.9 m 에서 절반이 되게 했어요. 문 앞 바닥이 촛불에서 1.6 m 넘게 떨어져 있어서 쐐기 끝의 판자는 짙은 빨강으로 남고, 쐐기 밖의 복도는 보라로 남아요. 빛의 색은 약할 때 짙은 빨강에서 셀 때 옅은 금빛으로 오르는 표라 갈색으로 가라앉지 않아요. 빛이 오르는 만큼 보라 바탕을 두 배로 빼서 복숭앗빛도 나지 않아요. 첫 판은 안개 세기가 0.05 + 0.55×Haze 라 처음 값에서 복도 위쪽이 다 붉게 떴고, 0.02 + 0.3×Haze² 로 낮추자 쐐기 밖이 보라로 돌아왔어요. 손 그림자는 처음에 촛불이 보는 문틀 평면에 그렸어요. 촛불이 0.75 m 오른쪽에 있어 빛이 눈 쪽으로 1 m 올 때마다 0.54 m 왼쪽으로 기울어서, 손가락이 대각선 줄무늬로 늘어졌어요. 그림자를 바닥 판자 위의 모양으로 그리고 1 m 에 0.3 m 만 기울이자 손으로 읽혔어요. 안개 속의 그림자도 그 바닥 모양에서 셈해서 바닥과 어긋나지 않아요. 사람 그림자는 몸통 위에 어깨와 머리를 얹으니 십자가나 열쇠 구멍처럼 보여서 뾰족 모자를 씌웠어요. 쾅 닫힌 뒤 문이 0.05 rad 로 튀면 그 틈의 빛이 바닥에 점선으로 찍혀서, 튐을 0.025 rad 로 줄이고 0.03 rad 아래로 열린 문은 닫힌 것으로 셌어요. 문 밑 빛은 처음에 0.1 m 만 퍼져 발 그림자가 보이지 않아서 0.3 m 로 넓혔어요. 처음 잰 값은 움직임 0.304, 복잡도 0.373, drift 0.210 이었고, 흔들림을 0.2초 2.4 LED 에서 0.5초 3 LED 로 늘리자 움직임이 0.327 이 됐어요. 이 패널에서 새로 본 것은 광선으로 셈한 빛이 쐐기 모양을 저절로 만든다는 것이에요. 문짝이 50도쯤 열리면 쐐기가 문틀 너비를 다 채우는데, 바로 그 각도를 넘으면서 문짝 앞면이 촛불을 받아 밝아지기 시작해요. 둘 다 촛불이 문짝 평면 너머를 보게 되는 순간이라서 같은 각도예요. 잰 값은 움직임 0.327, 복잡도 0.379, 채움 0.956, drift 0.212 이고, `thirds` 는 97/97/92 예요. +## 54판 + +54판은 할로윈 몫이 쉰 첫 판이라 열 개 모두 주제 5 「수학 · 추상」이에요. 같이 돈 53판(#264)이 고른 가우스 주기와 정사각형 분할, 디랙의 벨트 묘기 같은 수학과 지도 칸 둘을 피해서 골랐고, 앞 바퀴 PR 들에 따를 글은 없었어요. 52판까지 세면 추상 갈래 가운데 노이즈·흐름과 보로노이·셀, 시뮬레이션이 열하나로 가장 적었고 데이터 그림과 입자가 열둘이었어요. 53판까지 넣어 세면 열하나에서 열셋인 갈래가 열둘이라, 위상·매듭과 리듬·펄스를 뺀 열 갈래에 하나씩 넣었어요. 새 갈래는 열지 않았어요. 열 개 다 「흔한 것」에도 「낯선 수학 예」에도 없고, 다 새로 찾았어요. 주제 1 은 Strange Eigenmode 가 Frost 와 Smalti 뿐이던 복잡도 × 채움 4,2 에, Arcsine Law 가 Hitomezashi 와 Sequins 뿐이던 촘촘함 × 모양 바뀜 4,3 에 섰어요. 처음에는 Smale Horseshoe 가 촘촘함 × 모양 바뀜 4,4 를 겨눴는데 촘촘함 0.216 에 그쳐서, 모양 바뀜이 이미 그 띠에 있던 Arcsine Law 를 4,3 으로 옮겼어요. 주제 4 는 Størmer Trap 과 Stable Marriage, Arcsine Law 가 맡았어요. Ulam Sequence 의 모양 바뀜 0.016 이 그 축의 아래 끝을 0.021 에서 내렸고, 나머지 아홉은 어느 축의 끝도 넘지 않았어요. 복잡도는 0.121 에서 2.254, 움직임은 0.187 에서 1.910 이에요. 열 개는 `shelf.json` 의 어느 묶음에도 넣지 않아서 페이지 맨 위 「아직 안 추린 것」에 서요. + +### Nodal Domains + +주제 5 로 무작위 파동의 마디 영역을 세웠어요. 마이클 베리는 1977년에 혼돈 당구대의 높은 고유함수가 파수 k 가 같은 평면파를 무작위 방향과 위상으로 겹친 것처럼 보인다고 했어요. 그 함수가 양수인 곳끼리, 음수인 곳끼리 이어진 조각이 마디 영역이에요. 에브게니 보고몰니와 샤를 슈미트는 2002년에 이 영역들을 임계 결합 스미기로 보고, 넓이 k²/4π 마다 (3√3 − 5)/π = 0.0624 개의 영역이 있다고 내다봤어요. 패턴은 LED 하나를 한 점으로 두고, 반 바퀴를 40칸으로 나눠 칸마다 한 방향을 뽑은 평면파 40개를 더해요. 파동마다 위상이 표준편차 0.6 rad/s 의 정규 분포에서 뽑은 제 빠르기로 돌아서, 장은 어느 순간에도 베리의 장이고 되풀이하지 않아요. 영역은 같은 부호의 네 이웃을 잇고, 2×2 칸의 네 모서리 부호가 엇갈리면 쌍선형 곡면의 안장 값으로 어느 대각선을 이을지 정해요. 그래서 LED 사이의 이어짐이 곡면의 이어짐과 같아요. 영역의 색은 지난 프레임과 겹친 LED 수로 물려받아서, 갈라지면 큰 쪽이 색을 지키고 합쳐지면 더 많이 겹친 쪽의 색이 남아요. 첫 판은 영역마다 무작위 색을 줘서 어두운 주황이 갈색으로 보였고, 움직임 1.091 에 4.2 ms 였어요. K3 끝의 파장 3.5 LED 에서는 마디선이 영역을 다 먹었어요. 그래서 파장을 8 에서 5.5 LED 까지로 줄이고, 색을 영역 크기에 걸었어요. 처음에는 패널을 가로지르는 영역을 다 옅게 칠했는데, 64×128 에서는 두 부호가 다 가로질러서 화면이 바랬어요. 가장 큰 영역 하나만 옅게 칠하고, 다른 영역이 4분의 1 더 커져야 표시를 넘겨주게 했어요. 색이 바뀐 LED 를 통째로 번쩍이게 했더니 프레임마다 336개, 패널의 4분의 1이 늘 번쩍였어요. 안장 둘레 3×3 만 번쩍이게 바꾸자 26개가 됐어요. 위상 빠르기를 절반으로 줄이자 움직임이 0.369 가 됐고, 가운데 열에 대한 대칭으로 합을 절반으로 줄이고 줄마다 삼각함수 대신 한 번 돌려서 2.6 ms 가 됐어요. 확인한 것이에요. 높이 0 에서 패널은 1,800 프레임 평균 53.8 개 영역을 셌고, 보고몰니-슈미트가 같은 넓이에 주는 25.1 의 2.14 배예요. 테두리가 영역을 잘라 조각을 늘리기 때문이에요. 같은 코드를 64×128 에서 512×1024 까지 상자 넷에 옮겨 52.7, 153.1, 504.6, 1,783.9 개를 세고 넓이와 둘레로 맞추자, 넓이 k²/4π 마다 0.0604 개로 예측의 0.968 배가 나왔어요. 둘레 1 LED 마다 0.075 개가 더 붙어요. 높이 1 에서는 패널의 15.3% 가 그 위에 있어서 정규 분포의 15.9% 와 맞았어요. 속도 2 에서 패널은 1초에 합침 54번과 갈라짐 55번을 겪어요. 잰 값은 움직임 0.369, 촘촘함 0.289, 가장자리 0.861, 복잡도 1.315 이고 채움은 0.879 예요. 노브 넷은 0.47, 0.34, 0.40, 0.48 이에요. + +### Strange Eigenmode + +주제 5 와 1 로 이상 고유 모드(strange eigenmode)를 세웠어요. 레이먼드 피어험버트는 1994년 Chaos, Solitons & Fractals 4 에서, 주기적으로 젓는 혼돈 흐름에 확산이 조금 있으면 물감의 세기는 정해진 지수 비율로 줄지만 줄무늬 모양은 한 꼴로 굳어 주기마다 다시 나타난다는 것을 보였어요. 한 주기의 이류 확산 사상에서 가장 느리게 사라지는 플로케 모드예요. 흐름은 그의 사인 흐름이라, 반 주기는 행마다 x 를 A sin(2πy + p1) 만큼 밀고 나머지 반 주기는 열마다 y 를 A sin(2πx + p2) 만큼 밀어요. 패턴은 한 변 1 인 토러스를 가로 64, 세로 128 LED 로 그려요. 줄 하나를 미는 일은 그 줄의 푸리에 모드마다 위상을 돌리는 것이라 FFT 로 정확히 하고, 매 프레임 평균을 빼고 실효값 1 로 되돌려서 화면은 바래지 않아요. 처음에는 반 주기를 한 번에 밀었더니 이웃한 열이 13 LED 씩 어긋나 줄무늬가 점선으로 끊겼어요. 한 조각에서 줄이 LED 하나에 하나 넘게 기울지 않게 걸음을 자르고 조각마다 가로지르는 확산을 넣자 끊김이 사라졌어요. 확산 33A LED² 에서는 덩어리뿐이라 복잡도 1.510, 채움 0.459 였어요. 4A 까지 내려도 나이퀴스트의 4분의 3 위로 가는 분산이 0.18% 라서 거기를 바닥으로 뒀어요. 겨눈 칸은 Frost 와 Smalti 뿐이던 복잡도 × 채움 4,2 예요. 음수를 패널에서 보이지 않는 23 아래의 남색 등고선으로 칠하고 양수를 보라에서 옅은 청록으로 칠하자, 씨앗에 따라 복잡도는 1.99 에서 2.18, 채움은 0.41 에서 0.57 로 흔들렸어요. 모드의 왜도가 ±1.4~2.7 이라 부호가 바뀌면 밝은 쪽 몫이 달라졌고, A 1.0 에서는 맨 앞 두 승수가 0.465 와 0.444 로 붙어 있어 8초 안에 한 모양으로 모이지 않았어요. 부분 공간 반복으로 A 를 훑어 둘째 승수가 0.28 로 떨어진 A 1.2 를 K3 0 에 두고, 처음 열 주기를 미리 돌리고, 왜도가 양이 되게 부호를 정했어요. 복잡도는 2.194 를 지나 2.219 가 됐어요. K3 을 돌리면 이 흐름의 플로케 구조가 그대로 보여요. A 0.45 에서는 규칙 섬이 물감을 붙잡아 한 주기에 10% 만 바래요. A 0.82 와 1.4 에서는 맨 앞 승수가 137도와 136도 돈 복소쌍이라 모양이 되풀이되지 않아요. A 1.6 에서는 승수가 -0.41 이라 주기마다 밝은 곳과 어두운 곳이 뒤집혀 돌아와요. 확인한 것이에요. 여는 노브에서 같은 위상의 프레임을 주기마다 견주니 1 빼기 상관이 첫 주기 9.3e-4 에서 아홉째 주기 9.3e-7 로 줄었어요. 넷째 주기부터는 주기마다 0.44~0.55 배라서 둘째와 첫째 승수 비의 제곱 0.52 와 같은 크기예요. 정규화가 덜어 낸 로그를 한 주기 더하면 감쇠율이 0.951 이고, 같은 사상을 부분 공간 반복으로 푼 첫 승수 0.389 에서 나오는 0.945 와 맞아요. 화면은 그대로지만 참 세기는 3초마다 0.39 배가 되어 1분이면 5.5e-9 배예요. 잰 값은 복잡도 2.219, 채움 0.512 이고 움직임은 1.628 이에요. + +### Pearcey Cusp + +주제 5 로 피어시 적분을 세웠어요. 트레버 피어시가 1946년에 초곡선(caustic)의 첨점 근처 전자기장을 셈해 표로 낸 적분 P(x, y) = ∫exp(i(t⁴ + y t² + x t))dt 예요. 광선만 따지면 빛은 8y³ + 27x² = 0 의 첨점 곡선에 모여 세기가 무한대가 되는데, 파동으로 셈하면 그 곡선 안쪽에 밝은 점이 격자로 늘어선 간섭무늬가 생겨요. 마이클 베리와 콜린 업스틸은 1980년에 이것을 에어리 함수 다음의 둘째 회절 파국으로 묶었어요. 그 무늬의 어두운 점은 세기가 정확히 0 이고 위상이 한 바퀴 도는 소용돌이라는 것이 1979년에 베리가 존 나이, 프랜시스 라이트와 낸 논문에 나와요. 패턴은 LED 마다 이 적분을 셈해요. 적분 길을 실수축에서 t(s) = s + 0.5i·s³/(s² + 25) 로 들어 올리면 먼 곳에서 exp(−s⁴) 로 꺼져서, 사다리꼴 규칙 169점이 기하급수로 맞아요. 행마다 t⁴ + y t² 의 몫과 열마다 exp(ixt) 의 몫을 따로 두어 복소 곱셈 138만 번에 한 장을 짓고, 노브가 움직일 때만 11~16 ms 를 써요. 프레임마다는 위상만 돌려서 1 ms 안이에요. 처음에는 140자리 십진수로 멱급수를 더해 맞춰 보려 했는데, |x| 가 30 을 넘자 항이 10의 130제곱까지 커져서 급수 쪽이 틀렸어요. 그래서 mpmath 80자리로 t = s·e^(iπ/8) 길의 합을 따로 셈해 LED 40곳과 견줬고, K4 로 기울인 곳까지 상대 오차가 2.9e-10 안이었어요. 영점은 LED 네 개가 이루는 칸마다 위상이 감기는 수로 찾아요. 처음 노브에서 +38 과 −38 로 76개가 나왔고, 4배 촘촘한 격자에서는 같은 범위에 78개라 한 쌍이 LED 한 칸 안에 숨어 있었어요. K3 이 1 이면 282개 가운데 264개를 찾아요. 첫 판은 움직임 0.454, 복잡도 2.074, 채움 0.825, 색 0.165 였어요. 청록 위에 파랑으로 찍은 음의 영점은 안 보였고, 물마루는 첨점 끝 쪽으로 거슬러 올라갔어요. P 는 dP/dy = −i d²P/dx² 를 따르는 근축 파동이고 y 는 나아간 거리의 반대라서, 그리는 것을 P·exp(iωt) 의 실수부로 바꿔 물마루가 아래로 흐르게 했어요. 밝기를 (|P|/2.4)^0.85 에서 (|P|/2.2)^0.85 로 올리고 물마루 사이의 골도 0.3 에서 0.4 로 밝히자 채움이 0.959 가 되어 패널 전체가 초록이었고 첨점이 묻혔어요. 지수를 0.95 로 올려 흐린 곳을 누르고, 광선 셋이 지나는 첨점 곡선 안쪽은 초록, 하나만 지나는 바깥은 청록으로 나누자 채움 0.911, 색 0.188 이 됐어요. K4 의 기울기는 a·t³ 항이에요. t = u − a/4 로 옮기면 사라지는 항이라 그림은 비스듬히 민 축 위의 같은 피어시 장이고, 적분 길도 a/4 만큼 옮겨 셈해요. 잰 값은 움직임 0.384, 촘촘함 0.053, 복잡도 2.254, 모양 바뀜 0.131 이고, 노브 넷은 0.16, 0.10, 0.12, 0.12 예요. + +### STIT Tessellation + +주제 5 로 STIT 모자이크를 세웠어요. 베르너 나겔과 비올라 바이스가 2005년 「Crack STIT tessellations」(Advances in Applied Probability 37)에서 낸 과정이에요. 창 하나에서 시작해 칸마다 지수 분포 시간을 기다리는데, 그 빠르기는 칸을 지나는 직선의 측도이고 방향이 고르면 코시 공식으로 둘레를 π 로 나눈 값이에요. 시간이 다 되면 그 직선들 가운데 하나가 칸을 둘로 자르고, 두 칸은 저마다 따로 이어 가요. 새 선분은 늘 옛 선분에서 끝나서 꼭짓점이 다 T 자이고, 직선이 모든 것을 가로지르는 푸아송 직선 모자이크의 X 자가 없어요. 이름의 STable under ITeration 은 모자이크의 칸마다 모자이크를 다시 넣어도 법칙이 그대로라는 뜻이고, 그 덕에 시간 t 의 모자이크를 t 배 하면 시간 1 의 모자이크와 같아요. 패턴은 칸을 볼록 다각형으로 두고 변마다 잘린 시각을 적어 둬요. 자를 직선은 방향과 칸을 감싼 원 위의 위치를 고르게 뽑아 칸에 닿을 때만 받아요. 패널은 한 칸으로 시작해 Speed 2 에서 1.5초 만에 LED 단위 시간 1/κ 의 밀도에 이르고, 그다음은 천천히 떠도는 점으로 다이빙해요. 한 프레임에 화면을 z 배 키우면 과정을 시간 (1 − 1/z)/κ 만큼 더 돌려 같은 법칙으로 돌아오고, 창 안의 STIT 는 그 창만의 STIT 라서 칸을 패널 테두리로 잘라 내고 나간 칸은 버려요. 선은 LED 중심이 변에서 0.62 안이면 검게, 잘린 지 0.5초 동안은 하얗게 그려요. 첫 판은 시작할 때 시간 1/κ 의 모자이크를 한꺼번에 짓고 칸을 넓이의 로그로 칠했어요. 이웃한 칸은 크기가 비슷해서 색도 비슷했고, 다이빙으로 커지는 칸이 다 초록과 파랑으로 몰렸어요. 색이 0.546 이었고, 금빛에서 보라로 가는 넓이 띠로 바꾸자 0.439 였어요. 그래서 잘린 두 칸이 부모 색에서 색상환을 0.05~0.1 씩 반대로 돌고 하나는 밝게 하나는 어둡게 물려받게 했어요. 가장 오래된 선이 색의 집안을 가르고, 색은 0.494 가 됐어요. 패널이 한 칸에서 자라나는 처음 1.5초도 넣었어요. 다이빙은 초당 0.18 에서 0.24 로 빨라져 2.9초마다 두 배로 다가가요. 확인한 것이에요. 시간 0.05, 0.1, 0.2 에서 1,000번씩 지어 칸 13.52, 39.88, 129.48 개를 얻었고, 1 + t·둘레/π + t²·넓이/π 의 13.63, 39.30, 129.75 와 표준오차 안팎에서 맞았어요. 안쪽 꼭짓점은 T 가 12.9, 53.1, 207.9 개이고 X 는 0 이었어요. 같은 측도의 푸아송 직선은 2,000번에 칸 39.65, 130.69 개, X 26.4, 105.1 개예요. 축을 따르는 선만 쓰면 10.97, 31.27, 102.09 개로 1 + t(64 + 128)/2 + t²·넓이/4 의 10.92, 31.08, 102.12 와 맞았어요. 칸의 꼭짓점 수를 Miles-Lantuéjoul 가중으로 평균하니 3.994 로 이론의 4 예요. 정적으로 지은 모자이크에서 16줄 띠마다 선분 길이가 110.6~113.0 으로 기대값 112.4 와 맞아, 테두리 탓의 치우침이 없어요. 여는 노브로 190초씩 여섯 번 다이빙하니 패널의 칸이 평균 46.70 개로 창 공식의 45.85 에 가까웠고, 초당 18.1 번 잘렸고 X 는 한 번도 없었어요. 잰 값은 채움 0.964, 색 0.494, 움직임 0.778, 모양 바뀜 0.134 이고 `thirds` 는 96/97/95 예요. + +### Arcsine Law + +주제 5 와 4 로 아크사인 법칙을 세웠고, 주제 1 로 촘촘함 × 모양 바뀜 4,3 을 겨눴어요. 폴 레비는 1939년에 브라운 운동이 0 위에서 보내는 시간의 몫이 밀도 1/(π√(x(1−x))) 를 따른다고 보였어요. 이 밀도는 1/2 에서 가장 낮고 0 과 1 쪽으로 치솟아서, 동전을 오래 던지면 한쪽이 거의 내내 앞서는 일이 반반으로 갈리는 일보다 흔해요. 마지막으로 0 을 지난 시각과 최댓값에 닿은 시각도 같은 법칙을 따르고, 걸음 수가 유한할 때의 정확한 꼴은 펠러의 교과서(1950)에 있어요. 패턴은 걸음 48개를 열마다 하나씩 두고, 속도 2 에서 1초에 40줄씩 한 걸음에 한 줄을 내려서 126걸음이 패널 높이를 다 써요. LED 하나가 동전 한 번이라 위로 간 걸음은 밝고 아래로 간 걸음은 3분의 1 밝기예요. 색은 그때까지 합의 부호라서 0 위면 호박색, 아래면 하늘색이에요. 동전은 잡음처럼 흩어지는데 색은 열마다 길게 한 가지로 이어져요. 126걸음이 끝나면 호박색 칸이 열 위쪽으로 떠오르고 열이 그 수대로 줄을 서서, 두 색의 경계가 이 판의 분위 곡선이 되고 그 위에 레비 법칙의 분위를 흰 점으로 찍어요. 오른쪽 15열은 같은 시간축의 히스토그램이고, 그리지 않는 걸음을 프레임마다 8개 더해서 1분이면 1만 4천 걸음 넘게 쌓여요. 첫 판은 촘촘함 0.192, 모양 바뀜 0.296 이었어요. 4,3 칸에 서려면 촘촘함이 0.297 을 넘어야 했는데, 호박색과 하늘색의 가장 밝은 채널이 다 255 라서 0 을 사이에 둔 이웃 열 사이에 밝기 차이가 거의 없었어요. 하늘색 쪽을 어둡게 하고 지난 판의 바둑판을 다 밝히자 0.217 과 0.235 였어요. 다 내려온 걸음만 있는 화면이 0.16, 줄 선 화면이 0.06 이었어요. 동전 결을 밝기로 넣자 줄 선 화면도 0.27 이 되어 촘촘함이 0.316 이 됐지만, 화면 평균 밝기가 바둑판과 가까워져서 모양 바뀜이 0.182 로 떨어졌어요. 하늘색 쪽을 더 어둡게 하거나 바둑판을 넷에 하나로 성기게 해도 두 값이 함께 넘지 않았고, 0.260 과 0.212 가 가장 가까웠어요. 첫 판을 어두운 패널 위로 내리고 1초에 40줄로 빠르게 하자 0.309 와 0.232 가 됐어요. 그 뒤 10~16초 창에서는 촘촘함 0.317, 모양 바뀜 0.174 예요. 확인한 것이에요. 공정한 걸음 148,848 개의 히스토그램을 126걸음의 정확한 법칙과 견주자, 0 위의 시간은 콜모고로프 거리 0.0028, 마지막 0 은 0.0014 였어요. 최댓값의 시각은 0.0023 이고, 모두 5% 문턱 0.0036 아래예요. 레비의 연속 법칙과는 0.015 에서 0.036 떨어지는데, 정확한 법칙 자체가 126걸음에서 그만큼 떨어져 있어요. K4 를 0.5 로 올려 치우침 0.052 를 걸자 0 위의 시간은 0.19 벗어났고 마지막 0 은 0.051 만 벗어났어요. 잰 값은 움직임 1.079, 촘촘함 0.309, 복잡도 1.082, 모양 바뀜 0.232 이고 `thirds` 는 83/72/63 이에요. 노브 넷은 0.26, 0.29, 0.12, 0.30 이에요. + +### Størmer Trap + +주제 5 와 4 로 카를 스퇴르메르가 1907년에 다룬 쌍극자 장 속 대전 입자의 문제를 세웠어요. 장이 축을 두고 돌려도 같아서 방위각 방향의 정준 운동량이 보존되고, 스퇴르메르는 이 하나로 입자가 들어갈 수 없는 금지 영역을 그렸어요. 적도에서 √2 − 1 = 0.414 스퇴르메르 길이보다 안쪽에 있는 입자는 어느 쪽으로 출발해도 빠져나가지 못해요. 갇힌 입자는 자기력선을 감아 돌며 장이 센 쪽의 두 거울점 사이를 오가요. 그러는 동안 축을 따라 천천히 떠도는데, 양전하와 음전하가 서로 반대로 돌아요. 패턴은 입자 48개를 3차원에서 보리스 방법으로 적분해요. 쌍극자는 패널을 가로질러 눕혔어요. 자기력선 r = L cos²λ 는 축을 가로질러 2L 너비에 0.77L 높이라서, 축을 세우면 64 LED 너비 안의 덫이 가운데 24줄만 써요. 눕히자 고리가 위아래 끝까지 서서 첫 판의 `thirds` 가 19/41/22 였어요. 입자는 적도 거리 34~62 LED 의 껍질에 놓고, 자기 모멘트가 적도와 같아지는 피치각으로 바운스의 아무 자리에서 출발시켜요. 첫 판은 걸음 길이를 그 자리의 장에서 바로 읽었는데, 피치각 60도에서 자기 모멘트가 20초에 26~31% 흘렀고 방위각 운동량도 12% 흔들렸어요. 걸음 밀도를 하이러와 쇠더린드의 가역 규칙으로 고르자 0.4~0.7% 와 0.3% 로 줄었어요. 바깥 껍질의 회전 반경이 1.6 LED 일 때는 나선이 보이지 않아서, 처음 에너지를 바깥 껍질 0.16 스퇴르메르 길이에서 0.19 로 올렸어요. 이제 고리 꼭대기에서 회전이 4.5 LED 폭으로 꿈틀거려요. 시점을 0.21 라디안 돌려 적도 고리가 선 대신 좁은 타원으로 보이고, K4 를 2~90도에서 0~90도로 바꿔 처음 화면에서 입자 대부분이 구 가까이까지 깊이 되튀어요. 확인한 것이에요. 처음 노브로 30초를 돌리니 속력 제곱은 5e-14 안에서 그대로였고, 방위각 정준 운동량은 자기 항 k/L 에 견주어 입자마다 가장 크게 흔들린 폭이 중앙값 0.24%, 가장 큰 것 0.65% 였어요. 거울점에서 잰 자기 모멘트 v²/B_m 은 안쪽 껍질에서 0.1%, 바깥 껍질에서 0.2% 의 표준편차로 지켜졌어요. K3 을 0.25 로 올리면 바깥 껍질의 표준편차가 3.5% 가 되고, 0.5 에서는 23~56% 로 깨져요. 스퇴르메르의 한계 0.414 는 K3 0.47 에서 바깥 껍질을 지나요. 0.5 까지는 30초 동안 빠져나간 입자가 없었어요. 0.75 에서는 71개, 1 에서는 1,060개가 빠져나갔어요. 손실 원뿔(바깥 껍질 1.9도, 안쪽 4.9도) 안에서 출발한 넷은 넷 다 구에 닿았고, 밖에서 출발한 49개 가운데 닿은 것은 원뿔 가장자리의 하나뿐이에요. 양전하는 초당 0.059, 음전하는 −0.057 라디안으로 하나도 빠짐없이 제 방향으로 떠돌아서 한 바퀴에 106초쯤 걸려요. 잰 값은 움직임 0.352, 복잡도 1.420, 채움 0.292, `thirds` 21/40/26 이고, 노브는 0.05, 0.09, 0.09, 0.09 예요. + +### Stable Marriage + +주제 5 와 4 로 게일과 섀플리의 지연 수락을 세웠어요. 데이비드 게일과 로이드 섀플리가 1962년에 낸 알고리즘이에요. 청혼하는 쪽이 목록 순서대로 청혼하고, 받는 쪽은 지금까지 온 것 가운데 가장 나은 하나만 쥐고 나머지는 돌려보내요. 끝나면 언제나 안정 매칭이 서요. 서로를 지금 짝보다 더 바라는 쌍이 없다는 뜻이에요. 게다가 청혼한 쪽은 모두 안정 매칭에서 얻을 수 있는 가장 나은 짝을 얻고, 받는 쪽은 모두 가장 나쁜 짝을 얻어요. 패턴은 위 가장자리에 16명, 아래에 16명을 4 LED 간격으로 세우고, 청혼 하나를 128줄을 가로지르는 선으로 그려요. 한 라운드에 짝 없는 사람이 모두 한 번씩 청혼해요. 받아들여진 선은 남고 거절된 선은 번쩍이며 사라져요. 받는 쪽이 더 나은 상대로 갈아타면 쥐고 있던 선이 가운데서 끊겨 두 사람 쪽으로 감겨 들어가요. 선의 위 끝은 위 사람이, 아래 끝은 아래 사람이 상대에게 매긴 순위의 색이라 첫 선택은 초록, 마지막은 분홍이에요. 위쪽 안정 매칭이 서면 같은 목록으로 아래쪽이 청혼하고, 그동안 첫 매칭은 흐린 선으로 남아요. 위가 청혼할 때 초록이던 위 끝이 아래가 청혼하면 파랑과 분홍으로 바뀌고 아래 끝이 초록이 돼서, 한쪽의 최선이 다른 쪽의 최악이라는 정리가 색으로 보여요. 마지막 4초는 두 매칭이 다른 쌍만 번갈아 바뀌어요. 첫 판은 32명씩이었고 움직임 0.173, 촘촘함 0.203, 채움 0.563, 색 0.362 였어요. 그런데 라운드로 도는 지연 수락은 첫 몇 라운드 뒤에 청혼 하나가 한 사람을 밀어내는 사슬로 끝나서, 32명이면 2,000번 평균 43라운드, 가장 긴 것은 161라운드였어요. 처음 노브에서 한 번 도는 데 12초에서 29초가 걸렸어요. 청혼이 하나뿐인 라운드를 여덟 이상인 라운드의 0.35 길이로 줄이고 16명으로 내리자 한 번이 2~13초가 됐고, 선 하나하나가 읽혀요. 채움은 0.566 에서 0.371 로 내려갔고, 선 폭을 1.5 LED 로 넓히고 가장자리에 3×3 LED 사람 칸을 두자 0.442 가 됐어요. 정리는 패턴을 그대로 돌려 확인했어요. Agreement 다섯 값과 Choosy 네 값에서 8,000번을 돌려 안정 매칭 16,000개를 모으니 막는 쌍은 하나도 없었어요. 위가 청혼한 매칭보다 아래가 청혼한 매칭에서 더 나은 짝을 얻은 위 사람도, 그 거꾸로인 아래 사람도 없었어요. Choosy 를 올려 혼자 남는 사람이 생겨도 두 매칭에서 혼자인 사람은 늘 같았어요. 앨빈 로스가 1986년에 낸 시골 병원 정리예요. 목록이 다 무작위인 5,000번에서 청혼 수는 평균 46.07 로, 크누스의 n ln n = 44.4 와 윌슨의 상한 n H_n = 54.1 사이에 들어요. 모두가 같은 순위를 쓰면 136 = 16·17/2 번이고 안정 매칭은 하나뿐이에요. `thirds` 는 45/40/46 이고, 잰 값은 움직임 0.187, 복잡도 1.441, 색 0.315, 모양 바뀜 0.173, 노브 넷은 0.15, 0.15, 0.18, 0.17 이에요. + +### Smale Horseshoe + +주제 5 로 스메일의 말굽을 세우고, 주제 1 의 촘촘함 × 모양 바뀜 4,4 도 겨눴어요. 스티븐 스메일이 1965년 「Diffeomorphisms with many periodic points」에서 낸 사상으로, 정사각형을 한쪽으로 누르고 다른 쪽으로 늘인 다음 U 자로 꺾어 제 위에 다시 얹어요. 정사각형과 그 상은 띠 두 개에서 만나고, n 번 접은 상은 너비 합이 (2λ)^n 인 세로 띠 2^n 개, 거꾸로 접은 상은 가로 띠 2^n 개예요. 둘이 겹치는 곳에 남는 칸토어 먼지 위에서 사상은 두 글자의 자리옮김과 같아요. 패턴은 패널 전체를 정의역으로 두고 가운데 64×64 를 정사각형으로 써요. 왼쪽 다리는 왼쪽 가장자리를 오르고 오른쪽 다리는 오른쪽 가장자리를 내려오며, 둘 사이를 반지름 18 의 아치가 위 덮개에서 이어요. 아래 덮개는 왼발로, 위 덮개는 뒤집혀 오른발로 가요. λ 는 0.435 이고 늘이는 비율 μ = 2 + (π/2)(1 − λ) = 2.89 는 아치가 다리와 같은 빠르기로 돌게 고른 값이에요. LED 마다 역사상을 다섯 번까지 걸어 칸토어 틈의 깊이를 읽고, 오른쪽 다리와 아치 오른쪽 절반을 지난 횟수의 홀짝으로 그 가닥이 오르는지 내리는지를 정해요. 첫 판은 깊이를 한 색의 밝기로만 칠해 촘촘함 0.144, 모양 바뀜 0.304 였어요. 겨눈 칸은 Ham Sandwich 와 Pin Art 뿐인 촘촘함 × 모양 바뀜 4,4 였어요. 가닥을 오르내림으로 금빛과 하늘색으로 나누자 0.168, 반 바퀴를 멈춰 두자 0.181, 발을 아래 덮개까지 내리자 0.196, 색을 RGB 로 섞어 초록 얼룩을 없애자 0.210 이 됐어요. 하늘색을 0.62 에서 0.8 로 밝히고, 가로 띠는 다리 사이 틈에서만 되살아나게 하자 촘촘함 0.216, 모양 바뀜 0.338 이 됐어요. 멈춘 프레임은 0.262 인데 누르는 동안은 0.09~0.15 로 떨어져요. 정사각형 안의 가로 걸음은 평균 0.43 이지만 세로 걸음은 0.03~0.05 예요. λ 를 하나로 정하면 가닥과 틈이 한 LED 씩 번갈아 설 수 없고, 교차점을 가장 밝게 두면 밝기 폭을 두 방향이 나눠 써서 0.25 쯤이 끝이에요. 0.297 을 넘기려면 교차점을 어둡게 한 바둑판이 되어야 해서 불변 집합이 숨어요. 그래서 4,4 도 4,3 도 아닌 3,4 에 섰어요. 확인한 것이에요. 가운데 줄을 64만 점으로 훑으니 n = 1~6 에서 세로 띠가 2, 4, 8, 16, 32, 64 개였고, 너비 합 55.731, 48.531, 42.261, 36.800, 32.046, 27.906 LED 가 64(2λ)^n 과 소수 셋째 자리까지 같았어요. 이웃한 띠는 늘 반대로 달렸어요. f^n 의 고정점은 2^n 개였고 |f^n(z) − z| 는 3.5×10⁻¹⁰ 아래예요. 한 번 접은 끝 프레임과 다음 처음 프레임은 30 넘게 다른 LED 가 0개예요. 잰 값은 움직임 1.910, 촘촘함 0.216, 모양 바뀜 0.338 이고 `thirds` 는 47/51/43 이에요. + +### Ulam Sequence + +주제 5 로 울람 수열과 그 숨은 진동수를 세웠어요. 스타니스와프 울람이 1964년에 낸 수열이에요. 1 과 2 에서 시작해서, 앞선 서로 다른 두 항의 합으로 꼭 한 가지로만 쓰이는 가장 작은 수를 다음 항으로 붙여요. 1, 2, 3, 4, 6, 8, 11, 13, 16, 18, 26 으로 가고, 16,000 항까지 간격은 평균 13.3 이지만 규칙은 보이지 않아요. 슈테판 슈타이너베르거는 2017년 Experimental Mathematics 에서, α ≈ 2.5714474995 로 각 항을 원에 감으면 2, 3, 47, 69 말고는 모두 cos(αa) < 0 인 절반에 든다는 것을 찾았어요. 왜 그런지는 아직 아무도 몰라요. 패턴은 항을 직접 세요. 가짓수를 2 에서 멈추는 배열에 새 항과 앞선 모든 항의 합을 더하고, 가짓수가 1 인 가장 작은 수를 다음 항으로 골라요. 첫 프레임에 600 항을 세고, 그 뒤로 1초에 80 항씩 16,000 항까지 가요. α 도 코드가 찾아요. 처음 200 항으로 0.01 에서 π 까지 봉우리 폭의 5분의 1 간격으로 |Σ e^{iαa}| 를 훑고, 항 수가 두 배가 될 때마다 모든 항으로 그 둘레를 다시 훑어 포물선 꼭짓점을 잡아요. 가로는 αa 를 2π 로 나눈 나머지라 가운데가 π 이고, 세로는 log(a + 16) 이라 위에는 작은 항이 하나씩, 아래로는 큰 항이 띠로 모여요. 너비가 2π/α, 곧 정수 2.44 개인 격자에 수직선을 감은 셈이에요. 아래 34 줄은 64 칸 히스토그램 더미예요. 첫 판은 항이 늘며 그림이 천천히 줄어들 뿐이라 움직임이 0.046 이었어요. 새 항마다 코일 아래에서 제 칸 더미 위로 0.4초에 떨어지는 불똥을 더하자 0.201 이 됐어요. 더미는 π 바로 위가 움푹한 두 봉우리예요. K3 으로 고르는 U(1,3) 도 두 봉우리이고 U(1,4) 는 평평한 고원이에요. U(2,3) 은 빗살처럼 칸마다 높낮이가 갈려요. U(2,5) 는 짝수 항이 2 와 12 둘뿐이라는 제임스 슈멀과 유진 스피걸의 정리(1994) 대로 α 가 π 로 나오고 더미가 기둥 하나예요. K4 로 α 를 0.0004 만 비틀어도 각도 오차가 항의 크기만큼 자라서, 띠가 아래로 갈수록 기울다 감겨 분홍 절반을 가로질러요. 0.002 에서는 더미가 평평해져요. 확인한 것이에요. U(1,2) 를 16,000 항, 마지막 항 213,514 까지 세고 코드가 9,600 항으로 찾은 α 2.5714479 로 cos(αa) ≥ 0 인 항을 세니 2, 3, 47, 69 넷뿐이었어요. 슈타이너베르거의 값으로 세어도 같은 넷이고, 나머지 가운데 가장 큰 코사인은 -0.035 예요. 3,000 항으로 찾은 α 는 그의 값과 8.6e-7, 16,000 항으로 찾은 α 는 2.2e-7 차이예요. 다른 씨앗에서 절반을 벗어난 항은 U(1,3) 이 4 와 98, U(1,4) 가 1 과 10, U(2,3) 이 5 하나, U(3,4) 가 3 과 11 이에요. 잰 값은 움직임 0.201, 복잡도 0.384, 채움 0.152 예요. + +### Weaire-Phelan + +주제 5 로 켈빈의 문제를 세웠어요. 같은 부피의 칸으로 공간을 나눌 때 벽의 넓이가 가장 작은 꼴을 묻는 문제예요. 윌리엄 톰슨(켈빈 경)은 1887년에 깎은 팔면체를 살짝 굽힌 칸을 내놓았고, 106년 뒤 데니스 위어와 로버트 펠런이 두 가지 칸으로 그보다 0.3% 작은 거품을 찾았어요. 단위 정육면체마다 오각형만으로 된 불규칙한 십이면체 둘과, 육각형 둘에 오각형 열둘을 두른 십사면체 여섯이 들어요. 면을 평평하게 펴면 A15 격자의 보로노이 분할이에요. 패턴은 칸을 정말로 보로노이로 셈해요. 자리마다 정육면체 하나를 두고 주기 격자의 이웃 자리와의 수직이등분면으로 가까운 것부터 잘라 내고, 남은 꼭짓점보다 먼 평면이 오면 멈춰요. 그러면 A15 의 여덟 칸과 BCC 의 두 칸, 곧 켈빈의 칸이 나와요. 한 변 28 LED 의 정육면체로, 십사면체가 육각형으로 이어 쌓인 세로 줄을 축으로 삼아 0.57 변 안의 자리를 높이 ±48 LED 까지 모아 십이면체 14개와 십사면체 34개의 기둥을 세웠어요. 기둥은 위에서 16도 내려다보며 돌고, 앞을 향한 면만 깊이 버퍼로 칠한 뒤 흰 모서리를 그어요. K3 은 기둥을 가르는 선을 내려서 그 위를 같은 부피의 켈빈 칸으로 바꾸고, K4 는 칸을 제 자리 쪽으로 반까지 줄여 안쪽 칸이 보이게 해요. 첫 판은 움직임 0.750, 가장자리 0.879, 복잡도 0.470, 채움 0.693 이었고 한 프레임이 3.1 ms 였어요. 기둥 바닥이 패널 끝에 잘렸고, 그늘진 육각형은 갈색으로 보였어요. 삼각형마다 무게 중심 좌표를 푸는 대신 볼록 다각형을 줄마다 채우고 앞면에 닿은 모서리만 그리자 1.1 ms 가 됐어요. 변을 29 에서 28 LED 로, 높이를 ±58 에서 ±48 LED 로 줄여 기둥이 10~119 줄에 서요. 육각형은 그늘에서도 빛의 42% 를 남겨 금색을 지켜요. 면을 한 색으로 칠하니 복잡도가 0.295 까지 내려가서, LED 마다 깊이로 어둡게 하자 0.521 이 됐어요. 십이면체의 오각형을 초록 쪽으로 옮겨 두 칸이 갈려 보여요. 보로노이 셈은 먼 평면에서 멈추게 해 첫 프레임이 76 ms 에서 33 ms 로 줄었어요. 확인한 것이에요. 평평한 A15 칸의 부피는 정육면체의 정확히 125/1024 와 129/1024 라서 십사면체가 3.2% 커요. 여덟을 더하면 1 이 1e-12 안에서 맞고, BCC 의 두 칸은 512/1024 씩이에요. 십이면체는 오각형 12개, 십사면체는 오각형 12개에 육각형 2개예요. 켈빈 칸은 정사각형 6개에 육각형 8개이고, 칸마다 꼭짓점 − 모서리 + 면 = 2 예요. 부피 1 의 칸 하나가 두른 넓이는 A15 가 5.2950, 켈빈이 5.3147 이라 A15 가 0.371% 작아요. 면을 굽혀 이완하면 위어-펠런이 5.288, 켈빈이 5.306 으로 알려져 있어요. 켈빈 칸의 부피는 2,744 LED³ 로 A15 의 두 칸 2,680 과 2,765 의 평균이에요. 잰 값은 움직임 0.597, 가장자리 0.847, 복잡도 0.544, 채움 0.582 이고, 노브는 0.12, 0.20, 0.21, 0.22 예요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -2718,7 +2777,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 25일 `dev`(`f72355d`)의 328개에 43판부터 51판까지의 아흔 개와 52판의 열 개를 더한 428개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 25일 `dev`(`f72355d`)의 328개에 43판부터 52판까지의 백 개와 54판의 열 개를 더한 438개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -2768,6 +2827,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 52판의 열 개도 어느 축의 끝을 넘지 않아서 칸 경계는 30판 그대로이고, 52판까지 든 428개에서도 지도 21장이 다 `none` 이에요. 주제 1 은 50판까지의 408개에 남은 둘뿐인 칸 열다섯 가운데, 같이 돈 51판이 겨눈 촘촘함 × 색 4,4 와 가장자리 × 촘촘함 1,2 를 빼고 복잡도 × 모양 바뀜 4,4 와 가장자리 × 복잡도 3,4 를 겨눴어요. 51판까지 든 418개에서 열셋이던 그런 칸이 428개에서 열하나예요. 복잡도 × 모양 바뀜 4,4 는 한 화면이 deflate 로 LED 당 2.173 바이트를 넘으면서 2~8초에 4×16 칸 평균이 0.331 넘게 바뀌어야 하는 칸이에요. Gaussian Free Field 의 첫 판은 모드를 오른슈타인-울렌벡 과정으로만 굴려서 움직임 4.226, 복잡도 1.262, 모양 바뀜 0.195 였어요. 모양 바뀜은 자유장 자신이 막았어요. 자유장은 규모가 두 배가 될 때마다 분산을 같은 몫씩 더 가져서 4×16 칸 안에서도 부호가 칸 평균만큼 흔들리고, 흐림을 1.1 LED 에서 3.9 LED 로 넓혀도 모양 바뀜은 0.239 에 그쳤어요. 땅을 가장 밝은 채널이 늘 가득인 색으로 칠하고 바다를 밝기 0.15 로 내려 부호가 칸의 밝기를 정하게 하고, 가장 큰 모드가 제 값을 잊는 시간을 2.4초에서 0.7초로 줄이자 0.375 가 됐어요. 그때 복잡도가 2.141 이라, 해안선과 등고선을 음영으로 몇 단계씩 흔들어 칸 안에 들였어요. 마지막 값은 복잡도 2.256, 모양 바뀜 0.369 이고, 다른 씨앗 다섯에서도 복잡도 2.225~2.269, 모양 바뀜 0.360~0.404 라 같은 칸이에요. 가장자리 × 복잡도 3,4 는 이웃 차이 가운데 40/255 를 넘는 몫이 0.50~0.79 이면서 복잡도가 2.173 을 넘어야 하는 칸이에요. Force Chains 는 원판 사이의 검은 틈이 날카로운 차이를 맡고, 백색광 등색선의 섞인 색이 복잡도를 맡아요. 프린지 눈금을 바로잡은 판은 가장자리 0.591, 복잡도 1.784 였어요. 프린지를 늘리면 복잡도는 오르지만 원판이 거의 다 밝아져 사슬이 묻혔어요. LED 하나를 네 점에서 재 평균하자 같은 씨앗에서 복잡도가 2.105 에서 2.466 으로 올랐어요. 무늬는 부드러워졌는데 원판 테두리마다 색이 섞여 deflate 가 되풀이를 덜 찾았어요. 마지막 값은 가장자리 0.585, 복잡도 2.405 이고, 다른 씨앗 둘에서 복잡도가 2.31~2.41 이에요. 남은 열하나 가운데 여덟은 촘촘함 축이 든 칸이고, 셋은 복잡도가 2.173 을 넘어야 하는 칸이에요. +54판에서는 Ulam Sequence 의 모양 바뀜 0.016 이 축의 아래 끝을 0.021 에서 내려서, 모양 바뀜 칸의 경계가 0.103, 0.217, 0.331 에서 0.099, 0.215, 0.330 으로 옮겨졌어요. 다른 축의 경계는 30판 그대로이고, 빈 칸은 생기지 않아서 54판까지 든 438개에서도 지도 21장이 다 `none` 이에요. 주제 1 은 52판까지의 428개에 남은 둘뿐인 칸 열하나 가운데, 같이 돈 53판이 겨눈 움직임 × 촘촘함 4,4 와 복잡도 × 채움 4,1 을 빼고 복잡도 × 채움 4,2 와 촘촘함 × 모양 바뀜 4,4 를 겨눴어요. 복잡도 × 채움 4,2 는 한 화면이 deflate 로 LED 당 2.173 바이트를 넘으면서 LED 의 30~56% 만 켜져야 하는 칸이에요. Strange Eigenmode 는 확산이 클 때 덩어리뿐이라 복잡도 1.510, 채움 0.459 였어요. 확산을 줄이자 1.918 과 0.600 이 됐고, 음수 쪽을 패널에서 보이지 않는 23 아래 남색으로 두어 채움을 반쯤으로 묶었어요. 그다음 걸린 것은 고유 모드의 부호였어요. 모드는 부호가 정해지지 않고 왜도가 ±1.4~2.7 이라, 씨앗이 고른 부호에 따라 채움이 0.41 에서 0.57 까지 흔들렸어요. 왜도가 양이 되게 부호를 정하고 맨 앞 두 승수가 떨어진 A 1.2 에서 열자 복잡도 2.219, 채움 0.512 로 칸 안에 섰어요. 촘촘함 × 모양 바뀜 4,4 는 이웃 차이의 평균이 0.297 을 넘으면서 2~8초에 4×16 칸 평균이 0.330 넘게 바뀌어야 하는 칸이에요. Smale Horseshoe 는 모양 바뀜 0.338 로 띠 안에 들었지만 촘촘함이 0.144 에서 0.216 까지만 올랐어요. 멈춘 프레임은 0.262 인데 누르는 동안은 0.09~0.15 로 떨어졌고, λ 를 하나로 정한 칸토어 띠는 가닥과 틈이 한 LED 씩 번갈아 서지 못해요. 그래서 모양 바뀜이 이미 0.296 으로 4,3 의 띠에 있던 Arcsine Law 를 Hitomezashi 와 Sequins 뿐이던 촘촘함 × 모양 바뀜 4,3 으로 옮겼어요. 호박색과 하늘색은 가장 밝은 채널이 다 255 라서, 0 을 사이에 둔 이웃 열 사이에 밝기 차이가 거의 없었어요. 하늘색 쪽을 어둡게 하자 촘촘함이 0.192 에서 0.217 이 됐고, 동전 한 번 한 번을 밝기로 넣자 0.316 이 됐어요. 그런데 그때 모양 바뀜이 0.182 로 떨어졌어요. 화면 평균 밝기가 아래에 깔린 지난 판과 가까워졌기 때문이에요. 첫 판을 어두운 패널 위로 내리고 걸음을 1초에 40줄로 빠르게 하자 촘촘함 0.309, 모양 바뀜 0.232 가 됐어요. 그 뒤 판들이 도는 10~16초 창에서는 모양 바뀜이 0.174 라서, 이 칸은 첫 판이 어두운 패널을 내려오는 처음 8초의 자리예요. 52판까지의 428개에서 열하나이던 둘뿐인 칸이 438개에서 아홉이에요. 남은 아홉 가운데 일곱은 촘촘함 축이 든 칸이고, 둘(가장자리 × 복잡도 4,4 와 복잡도 × 채움 4,1)은 복잡도가 2.173 을 넘어야 하는 칸이에요. + 23판부터 28판까지는 `*` 가 붙은 칸이 없었어요. 22판까지 남은 두 칸, 촘촘함 × 모양 바뀜 3,4 와 4,3 에 23판의 Bait Ball 과 Sequins 가 섰어요. 하나 남은 빈 칸이던 복잡도 × 채움 4,3 에는 24판의 Waggle Dance 가 섰어요. 복잡도가 2.173 을 넘고 채움이 0.558~0.815 인 자리예요. 복잡도 축 끝의 Contour Map(2.735)을 빼고 세면 차는 칸이라 `*` 가 붙지 않았고, 그 아래 복잡도 2.4~2.6 에 선 Woodpecker, Dripstone, Agate, Guilloché 는 채움이 다 0.93 을 넘었어요. 이제 지도 21장의 칸이 다 차서 `--gaps` 는 모든 줄에 `none` 을 찍어요. 주제 1 은 축 끝의 패턴 하나가 축을 늘려 새 빈 칸이 날 때까지 고를 칸이 없어요. Waggle Dance 에서 어려운 쪽은 채움이었어요. 벌집은 꺼진 LED 가 거의 없는 그림이라, 빈 방 바닥과 봉한 방 사이 이음매를 24 아래로 눌러 채움 0.732 를 냈어요. 복잡도는 켜진 LED 의 세 채널을 따로 흔드는 낟알로 쉽게 올라서, 첫 판은 오히려 2.766 으로 축 끝을 넘었어요. 25판도 기준 148개의 21장이 다 `none` 이라 주제 1 은 겨눌 칸이 없었어요. 그 대신 패턴이 하나만 선 칸을 셌어요. 148개에서 49칸이었고, 남은 자리로 짠 Penrose 가 그 가운데 여섯 칸에 둘째로 서서 158개에서는 43칸이에요. Penrose 가 겨눈 칸은 촘촘함 × 색 3,4 예요. 촘촘함 0.195~0.299 에 색 0.509 위인 자리이고, 4판의 Jacquard Loom 혼자 서 있었어요. Penrose 는 촘촘함 0.264, 색 0.583 으로 거기 섰어요. 같은 값으로 움직임 × 촘촘함 4,3 과 촘촘함 × 채움 3,3 의 Moire Rings, 촘촘함 × 복잡도 3,2 의 Peg Light, 복잡도 × 색 2,4 의 Gear Train, 채움 × 색 3,4 의 Marbling 곁에도 섰어요. 1 LED 어두운 선으로 나눈 작은 타일에 선명한 색을 여럿 두면 촘촘함과 색이 함께 높은 자리에 닿아요. 26판도 기준 158개의 21장이 다 `none` 이었어요. 패턴이 하나뿐인 칸 43 가운데 넷을 겨눴어요. Dark Field 는 검은 바탕 위의 흩은 빛으로 채움 0.263 에 촘촘함 0.222, 복잡도 2.081 이라, Foxfire 혼자이던 복잡도 × 채움 3,1 과 Peg Light 혼자이던 촘촘함 × 채움 3,1 에 씨앗 여덟이 다 들어요. Origami 는 어두운 탁자 위 종이 한 장이라 채움 0.264 이고, 2초의 정사각형이 8초에 좁은 학 기본형이 되어 모양 바뀜 0.255 로 Domino Run 혼자이던 채움 × 모양 바뀜 1,3 에 서요. Lichtenberg 는 Rabbit Ears 혼자이던 복잡도 × 모양 바뀜 4,3 에 씨앗 여덟 가운데 일곱이 들어요. 네 칸 말고는 바뀐 칸이 없어서, 168개에서 패턴이 하나뿐인 칸은 39칸이에요. 27판도 기준 168개의 21장이 다 `none` 이었고, 39칸 가운데 둘을 겨눴어요. Mitosis 는 채움 0.263, 색 0.540 으로 Neon Alley 곁의 채움 × 색 1,4 에 서요. 판의 4분의 1만 켜는 그림에서 색 0.5 를 넘기려면 켜진 LED 가 거의 다 순색 최대 밝기여야 해서, 형광의 번짐은 셈에 들지 않는 21/255 아래로 눌렀어요. Lava Lake 는 복잡도 2.222, 채움 0.271 로 Moth Lamp 곁의 복잡도 × 채움 4,1 에 서요. 복잡도는 프레임 바이트 값의 엔트로피를 거의 그대로 따라서, 어두운 지각의 결을 채널마다 0~24 전부로 넓히자 1.866 이 2.010 이 됐어요. 붉은 용암은 켜진 LED 하나에 24 를 넘는 바이트가 빨강 하나뿐이라, 이음매를 늘리기보다 주황 쪽으로 데우는 편이 채움을 덜 쓰고 복잡도를 벌었어요. 색 축의 아래 끝은 Cloud Chamber 의 0.052, 모양 바뀜 축의 아래 끝은 Climbing Wall 의 0.021 로 옮겨졌지만 혼자 서는 칸은 그대로라서, 178개에서 패턴이 하나뿐인 칸은 37칸이에요. 28판도 기준 178개의 21장이 다 `none` 이었고, 37칸 가운데 둘을 겨눴어요. Aztec Diamond 는 촘촘함 0.350, 색 0.640 으로 Millefiori 곁의 촘촘함 × 색 4,4 에 서요. 촘촘함은 가장 밝은 채널로 재서 네 색 도미노가 맞닿아도 턱이 없었고, LED 에 묶은 체커판이 그 턱을 줬어요. Stadium Billiard 는 움직임 2.362, 채움 0.186 으로 Warp 곁의 움직임 × 채움 4,1 에 서요. 공 이백 개가 1초에 125 LED 를 가면서 짧은 자취만 남겨요. 겨누지 않은 다섯 칸에도 둘째가 들어서 188개에서 패턴이 하나뿐인 칸은 30칸이에요. 29판도 기준 188개의 21장이 다 `none` 이었고, 30칸 가운데 둘을 겨눴어요. 28판이 모듈러 형식의 등고선으로 겨눠 보라던 가장자리 × 움직임 1,4(Mandelbrot 혼자)는 J-Invariant 가 움직임 0.372 로 모자랐고, Hopf Fibration 이 가장자리 0.207, 움직임 1.607 로 섰어요. 원 일흔 개가 저마다 1초에 몇 LED 씩 쓸고 가서 움직임이 높고, 원마다 옅은 빛 번짐을 둬서 가장자리가 낮아요. J-Invariant 는 복잡도 2.693, 색 0.400 으로 Spectrogram 곁의 복잡도 × 색 4,3 에 서요. 같은 판의 Cream Swirl(0.028)과 Serum Gel(0.079)이 가장자리 축의 아래 끝을 Fire 의 0.120 에서 내려서, 가장자리 칸 경계가 0.305, 0.560, 0.815 에서 0.232, 0.514, 0.796 으로 옮겨졌어요. 그래서 198개에서는 가장자리 × 촘촘함 1,2 가 23판 뒤로 처음 `*` 가 붙은 빈 칸이에요. 가장자리 0.232 아래에 촘촘함 0.091~0.195 인 자리라, 번지는 바탕 위에 잔 결이 고르게 깔린 그림이 들어설 수 있어요. 같은 지도의 1,3 과 1,4, 가장자리 × 채움 1,1 도 비었지만 끝의 패턴 하나를 빼면 차서 `*` 가 없어요. 198개에서 패턴이 하나뿐인 칸은 24칸이에요. 촘촘함이 0.299 를 넘는 패턴이 20판에서 셋 늘었고, 셋 다 그 값을 다른 길로 냈어요. Agate 는 1 LED 띠를 벽까지의 거리로 그어서, 축에 나란한 띠는 이웃 쌍의 절반에서만 차이가 나고 대각선 띠는 70% 쯤에서 나요. 방향이 고르게 섞이면 띠 너비 0.85 LED 언저리가 가장 높았어요. Millefiori 는 촘촘함을 가장 밝은 채널로 재서 코발트와 카나리아 노랑이 맞닿아도 턱이 0 이었어요. 고리마다 밝은 유리와 그 유리를 어둡게 뽑은 유리를 번갈아 넣어야 했어요. Sunglint 는 반짝임을 물결 띠마다 체커판의 한쪽 칸에만 둬서 반짝임끼리 닿지 않게 했어요. 셋 다 채움이 0.85 를 넘어서, 촘촘함 × 채움 4,4 는 꺼진 LED 없이도 닿는 칸이었어요. 21판의 Sandpile 이 일곱째예요. 칸 하나가 LED 하나이고 알 셋 옆의 알 둘이 가장 흔한 이웃 쌍이라, 밝기를 0, 2, 1, 3 차례로 올려 더미 안의 촘촘함이 0.40 언저리예요. 판의 75% 는 덮여야 0.3 을 넘어요. 22판의 Pin Art 와 Night Flight 가 여덟째와 아홉째예요. Pin Art 는 핀과 틈의 체커 결이라 이웃 쌍이 모두 핀과 틈의 쌍이고, Night Flight 는 Pancakes 의 체커 결에 1 LED 가로등을 LED 의 12.7% 만큼 얹어서 가장자리 0.258 에 촘촘함 0.328 이에요. 촘촘함과 가장자리는 가장 밝은 채널로 재서, 색을 섞는 자리마다 섞은 뒤 가장 밝은 채널을 다시 맞춰야 했어요. 땅과 강물을 섞은 자리에서 가장 밝은 채널이 0.68 까지 내려가 체커 결의 37 이 41~60 으로 넘어갔고, 그대로 둔 판은 가장자리가 0.05 높았어요. 23판의 Sequins 가 열째예요. 체커 결 없이, 가로 5 LED 와 세로 3 LED 간격으로 엇갈려 단 스팽글의 빛점과 그림자 줄로 0.327 을 냈어요. 촘촘함은 무늬의 모양보다 턱의 크기를 따라가서, 스팽글 몸통을 0.8 에서 0.6 으로 어둡게 하자 은빛 면이 0.339 에서 0.311 로 내려갔어요. 26판의 Lichtenberg 는 0.2 를 넘긴 판을 버렸어요. 판 어디서나 돋는 1 LED 잔가지로 0.248 을 냈더니 검은 직교 그물이 나무판을 덮어 미로로 보였어요. 잔가지를 탄 줄기에만 달아 빈 나무를 42% 남기자 0.130 이었어요. 촘촘함은 판 전체 이웃 쌍의 평균이라, 빈 나무가 넓게 남는 그림은 줄기 둘레가 아무리 잘아도 평균이 낮아요. 같은 판의 Dark Field 는 검은 바탕에 세균 몇백과 밝은 테두리를 판 곳곳에 흩어 0.222 를 냈어요. @@ -2830,6 +2891,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 52판의 열 개는 가장 낮은 노브가 Pumpkin Drop 의 K1 Flame 로 네 자리로 0.0342 이고, Specimen Shelf 의 K4 Knock 0.0383 과 Anscombe's Quartet 의 K3 Heat 0.0387 이 그다음이에요. 노브 마흔 개가 다 0.034 를 넘어요. 이번에도 이야기가 있는 그림은 노브를 재는 2~3초 창이 정했어요. Pumpkin Drop 의 첫 판은 호박이 꼭대기에 매달린 채 시작해 1.2초에 놓여서, 그 창에는 바닥의 조각만 남아 Flame 이 0.0071, 그때 터지는 세기이던 K4 가 0.0119 였어요. 첫 바퀴를 땅에서 감아올리는 데서 시작해 창 안에 불 켠 호박이 매달려 있게 하고, 촛불이 하늘에 던지는 빛을 두 배쯤 세게 해 e 분의 1 거리를 9 LED 에서 16 LED 로 넓히자 Flame 이 0.034 가 됐어요. K4 는 호박 크기로 바꿔 0.088 이에요. 한 번 일어나는 일에 건 노브는 그 일이 창에 들지 않으면 0 에 가까웠어요. Specimen Shelf 의 Knock 은 두드림이 2.4초에 한 번 들어와 0.06 이었다가 출렁임이 식는 동안 0.02 로 내려가서, 노브 끝의 첫 두드림을 1.5초로 당기고 출렁임을 두 배 남짓 세게 해 0.04 가 됐어요. Anscombe's Quartet 의 Heat 는 첫 모양이 창에 막 흐르기 시작해서 0.02 였고, 사중주와 접기를 줄여 모양을 창 앞으로 당기고 뜨거운 걸음을 2.5배 길게 해 0.04 가 됐어요. 같은 그림의 Hue 는 색이 점에만 실려 0.01 로 바닥 아래였어요. 회귀선 위와 아래를 레벨 23 의 옅은 청록과 보라로 나눠 칠하자 0.05 를 넘었어요. 얇은 것만 물들이는 노브는 이번에도 약해서, Banshee 의 Wind 는 머리와 옷자락만 날려 0.05 였고 센 바람에 여인을 바람 쪽으로 8 LED 옮기자 0.066 이 됐어요. +54판의 열 개는 가장 낮은 노브가 Ulam Sequence 의 K1 Hue 와 K2 Speed, Størmer Trap 의 K1 Hue 로 두 자리로 0.05 이고, 노브 마흔 개가 다 0.05 를 넘어요. 이번 판의 노브는 다 화면에 늘 있는 것을 바꿔서, 52판처럼 한 번 일어나는 일에 걸린 노브가 없었어요. 가는 선과 점만 켜진 그림은 색 노브가 약했어요. Størmer Trap 과 Ulam Sequence 는 채움이 0.292 와 0.152 라 색이 실린 LED 가 적어요. Ulam Sequence 의 첫 판은 항이 늘며 그림이 천천히 줄어들 뿐이라 움직임이 0.046 이었고, 새 항마다 제 칸 더미로 0.4초에 떨어지는 불똥을 더하자 0.201 이 되면서 Speed 도 창 안에서 보였어요. 노브 하나에 정리의 갈래를 건 판이 많았어요. Strange Eigenmode 의 K3 Stir 는 규칙 섬과 되풀이되지 않는 복소쌍, 주기마다 뒤집히는 모드를 차례로 지나요. Arcsine Law 의 K3 은 레비의 세 통계를, Ulam Sequence 의 K3 은 씨앗 여섯을 골라요. 문턱을 가운데 둔 노브에는 Lab 기본 0.06 이 문턱을 비켜 가게 죽은 띠를 뒀어요. Arcsine Law 의 Drift 는 ±0.08, Ulam Sequence 의 Detune 은 ±0.1 안에서 0 이에요. + Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. ### 세로 64×128 @@ -2882,9 +2945,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 52판에서는 Force Chains 와 Pumpkin Drop 이 128줄을 써요. Force Chains 는 지름 6.3 과 8.1 LED 의 원판 175개가 패널보다 양옆으로 2 LED 넓은 상자를 바닥부터 꼭대기까지 채워서 `thirds` 가 62/65/73 이에요. 맨 위의 피스톤이 누르는 동안에는 사슬이 피스톤 바로 아래까지 서서 위 칸도 비지 않아요. Pumpkin Drop 은 호박이 꼭대기 도르래까지 감겨 올라가 81줄을 1.04초에 떨어지고 바닥에서 터져서, 한 바퀴 10초 동안 호박이 지나가지 않는 줄이 없어요. 하늘을 맨 위에서도 밝기 0.3 으로 두어 `thirds` 는 96/97/98 이고, 검은 도르래 틀도 그 하늘 앞이라 읽혀요. Zonal Jets(100/100/100)와 Gaussian Free Field(96/96/96), Creaking Door(97/97/92), Lost Head(85/87/96), Specimen Shelf(70/90/88), Banshee(41/98/77)도 128줄을 다 쓰지만 주제 4 로 세지 않았어요. Zonal Jets 는 제트 띠가 128줄에 쌓이지만 바닥 밝기 0.25 가 모든 LED 를 켜서 `thirds` 가 띠에 대해 말해 주는 것이 없어요. Anscombe's Quartet 은 점구름이 회귀선 가운데에 모여 13/37/9 이고, de Bruijn Sequence 는 어두운 판에 타일과 실만 켜져 32/27/40 이에요. +54판에서는 Størmer Trap 과 Stable Marriage, Arcsine Law 가 128줄을 써요. Størmer Trap 은 쌍극자를 패널을 가로질러 눕혔어요. 자기력선 r = L cos²λ 는 축을 가로질러 2L 너비에 0.77L 높이라서, 축을 세우면 64 LED 너비 안의 덫이 가운데 24줄만 써요. 눕히자 고리가 위아래 끝까지 서서 `thirds` 가 21/40/26 이에요. Stable Marriage 는 청혼 하나하나가 위 가장자리의 16명에서 아래 가장자리의 16명까지 128줄을 가로지르는 선이라 45/40/46 이에요. Arcsine Law 는 한 걸음이 한 줄이라 126걸음이 패널 높이를 다 써서 83/72/63 이에요. Pearcey Cusp(97/84/91)와 STIT Tessellation(96/97/95), Nodal Domains(88/87/87)도 128줄을 다 쓰지만 주제 4 로 세지 않았어요. 셋 다 판 전체를 덮는 장이나 모자이크라 `thirds` 가 높이에 대해 말해 주는 것이 없어요. Ulam Sequence 는 위쪽에 작은 항 몇 개만 서서 5/20/20 이에요. + ### 낯선 수학 -주제 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)예요. +주제 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)예요. 54판은 열을 더했어요. 베리의 무작위 파동과 보고몰니, 슈미트가 스미기로 센 마디 영역(Nodal Domains), 피어험버트가 사인 흐름에서 찾은 이상 고유 모드(Strange Eigenmode), 피어시 적분과 베리, 업스틸의 첨점 회절 파국(Pearcey Cusp), 나겔과 바이스의 STIT 모자이크(STIT Tessellation), 레비의 아크사인 법칙(Arcsine Law), 스퇴르메르가 다룬 쌍극자 장 속 대전 입자(Størmer Trap), 게일과 섀플리의 지연 수락(Stable Marriage), 스메일의 말굽(Smale Horseshoe), 울람 수열과 슈타이너베르거가 찾은 숨은 진동수(Ulam Sequence), 위어와 펠런의 거품과 켈빈의 깎은 팔면체(Weaire-Phelan)예요. 목록 밖에서 다음에 해 볼 것이에요. 민코프스키의 물음표 함수(1904)는 페리 나무를 이진 나무로 펴는 계단이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -2938,6 +3003,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 52판에서 본 것이에요. 수학이 말하는 수를 이번에도 패턴의 셈으로 재 봤어요. Gaussian Free Field 는 장을 120초 굴려 패널 평균 분산이 0.6271 로 이론 값 0.6290 과 맞았어요. 같은 짓기를 16×32 에서 128×256 까지 상자 넷에 옮겨 K3 1 의 해안선 길이를 세니, 상자가 두 배가 될 때마다 1.44, 1.46, 1.46 제곱으로 늘어 SLE(4) 의 차원 1.5 에 다가갔어요. Anscombe's Quartet 은 세 노브 자리에서 120초씩 돌린 프레임 9,221장에서 다섯 통계가 한 장도 빠짐없이 그대로였어요. de Bruijn Sequence 는 규칙과 차수마다 창이 다 서로 다르고, 시프트 레지스터가 000000 을 한 번도 만들지 않는 것을 셌어요. 정리가 그림을 막기도 했어요. 상관 0.82 는 축을 어떻게 늘려도 공분산 타원을 3.1 대 1 보다 둥글게 만들지 못해서, Anscombe's Quartet 의 원과 별은 점의 24% 를 회귀선 양 끝의 덩어리 둘로 보내고서야 둥글게 섰어요. 앤스컴의 셋째와 넷째 자료에서 바깥 점 하나가 하는 일이에요. 시간으로 거친 확률 과정은 패널에서 잡음이 됐어요. Gaussian Free Field 의 모드를 오른슈타인-울렌벡 과정 그대로 굴리면 가장 큰 모드도 프레임마다 진폭의 17% 를 새로 받아 판 전체가 끓었고, 모드마다 지수 필터로 한 번 더 걸러서야 해안선이 흘렀어요. 점성으로 잘라 둔 파수 바로 아래가 이번에도 그림을 망쳤어요. Zonal Jets 는 초점성 12 에서 그 껍질의 엔스트로피가 강제 껍질의 다섯 배까지 튀어 세 LED 길이의 잔물결 조각이 났고, 80 으로 올리자 만분의 1 아래로 내려갔어요. 50판 Inverse Cascade 가 베타 평면을 노브에서 뺀 빗살도 이것이었어요. 무늬가 흐름을 거슬러 가는 것도 보였어요. Zonal Jets 의 흐름선 굽이는 1초에 20 LED 씩 서쪽으로 미끄러지는데, 같은 줄의 물은 3.3 LED 씩 동쪽으로 흘러요. 로스비 파의 서쪽 표류예요. 빛을 물리대로 셈하면 색이 저절로 늘었어요. Force Chains 는 400~700 nm 의 31 파장을 CIE 등색 함수로 더해 등색선을 칠하자 복잡도가 2.0 을 넘었고, LED 하나를 네 점에서 재 평균하자 2.4 가 됐어요. +54판에서 본 것이에요. 이번에도 정리가 말하는 수를 패턴의 셈으로 재 봤어요. Nodal Domains 는 같은 코드를 상자 넷에 옮겨 넓이와 둘레로 맞추자 보고몰니-슈미트 상수의 0.968 배가 나왔고, Ulam Sequence 는 16,000 항 가운데 원의 다른 절반에 든 항이 2, 3, 47, 69 넷뿐이었어요. Stable Marriage 는 안정 매칭 16,000개에서 막는 쌍이 하나도 없었고, Weaire-Phelan 의 평평한 칸은 부피 1 에서 넓이가 켈빈 칸보다 0.371% 작았어요. 패널 크기가 정리의 수를 바꾸기도 했어요. Nodal Domains 의 영역은 64×128 에서 예측의 2.14 배라, 테두리가 영역을 잘라 늘린 조각을 둘레 항으로 빼야 상수가 나왔어요. Pearcey Cusp 는 영점 쌍이 LED 한 칸 안에 같이 들면 위상 감김이 서로 지워져서, 4배 촘촘한 격자에서 78개인 영점을 76개만 찾아요. 눈에 보이는 크기와 수학이 맞는 범위가 한 자리에서만 만나기도 했어요. Størmer Trap 의 회전 나선은 회전 반경이 2 LED 는 돼야 보이는데, 그 크기가 자기 모멘트가 깨지기 시작하는 자리라 단열 영역과 눈에 보이는 나선이 경계에서만 만나요. Smale Horseshoe 는 λ 를 하나로 정한 칸토어 띠의 가장 가는 틈이 가닥의 (1 − 2λ)/λ 배라서, 가닥을 한 LED 에 맞추면 틈은 0.3 LED 로 사라졌어요. 이웃한 가닥이 그레이 부호 차례로 늘 반대로 달리는 것을 색으로 칠하자 틈 대신 오르내림이 가닥을 갈랐어요. 고유 모드에는 부호가 없어서, Strange Eigenmode 는 같은 모드를 칠해도 씨앗이 고른 부호에 따라 채움이 0.41 에서 0.57 까지 갈렸어요. 왜도로 부호를 정하고 맨 앞 두 승수가 떨어진 자리에서 열고서야 잰 값이 씨앗을 타지 않았어요. 작은 어긋남이 큰 수에서 먼저 보였어요. Ulam Sequence 의 α 를 0.0004 만 비틀어도 위쪽의 작은 항은 제자리에 있고 아래쪽 큰 항부터 띠가 기울어 감겼어요. Arcsine Law 에 작은 치우침을 걸면 0 위의 시간과 최댓값의 시각이 먼저 무너지고 마지막 0 은 덜 움직였어요. 치우침 0.014 에서 앞의 둘은 법칙에서 0.051 벗어났고 마지막 0 은 0.011 이었어요. 마지막 0 은 걸음이 어느 쪽에 있는지가 아니라 돌아오는지만 묻기 때문이라고 봐요. 창 안의 무작위 과정은 창 밖을 몰라도 됐어요. STIT Tessellation 은 다이빙하며 칸을 패널 테두리로 잘라 버려도 칸 수가 창 공식대로 46 개 안팎에 머물렀어요. 넓이의 정리는 넓이가 아니라 꼴로 보였어요. Weaire-Phelan 의 0.37% 는 패널에서 보이지 않고, 한 변 28 LED 에서 8 LED 쯤인 오각형과 기둥 옆면에 줄지어 선 금색 육각형이 두 거품을 갈라요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 뷔퐁의 바늘(1777)은 Bertrand Paradox 와 같은 꼴로, 금을 넘는 바늘의 몫이 2/π 로 모이는 막대와 함께 세울 수 있어요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. @@ -2978,6 +3045,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 목록 밖에서 52판 뒤에 해 볼 것이에요. 같은 가우스 자유장 위에 각도마다 흐름선을 그으면 한 점에서 부채처럼 갈라지는 SLE(κ) 곡선들이 서요. 제이슨 밀러와 스콧 셰필드의 허수 기하(2016)예요. 균열 끝의 응력장(조지 어윈, 1957)을 Force Chains 의 백색광 등색선 표로 칠하면, 균열이 자라는 동안 그 끝에 나비 모양 무늬가 따라가요. 마테이카와 피츠모리스는 같은 논문에서 다섯 수 요약이 같은 상자 그림 옆에서 점들이 한 덩어리에서 두 덩어리로 갈라졌다 모이는 것도 보였어요. 드 브라윈 토러스는 4×4 판 위에 모든 2×2 무늬가 꼭 한 번씩 나오는 2차원 짝이라, 패널을 그 판으로 덮고 창을 상하좌우로 굴려 볼 만해요. 베타 평면에서 마찰을 더 낮추고 잠재 소용돌이도 q + βy 의 등고선으로 칠하면, 동쪽 제트가 섞임을 막는 벽이 되는 PV 계단(데이비드 드리첼, 마이클 매킨타이어, 2008)이 보여요. +목록 밖에서 54판 뒤에 해 볼 것이에요. 보고몰니와 슈미트는 2007년에 뒤브르트랑과 함께 무작위 파동의 마디선이 SLE(6) 를 닮았다는 것을 수치로 보였어요. 패널을 가로지르는 마디선 하나를 따라가며 그 차원 7/4 를 재 볼 만해요. 확산을 0 으로 보낼 때 이상 고유 모드의 승수가 가는 곳은 폴리콧-뤼엘 공명(마크 폴리콧 1985, 다비드 뤼엘 1986)이라, 섭동한 고양이 사상 같은 단순한 사상에서 셈해 그릴 수 있어요. 피어시 다음의 회절 파국인 제비꼬리는 지수에 t⁵ 이 드는 적분이라 Pearcey Cusp 의 휜 적분 길을 그대로 써요. STIT 를 3차원 상자에서 돌려 평면으로 자른 단면도 평면 STIT 라서, 볼록 다면체로 갈라지는 3차원 STIT 의 단면을 천천히 밀어 올리는 그림이 될 만해요. 끝이 0 으로 묶인 브라운 다리에서는 0 위의 시간이 아크사인이 아니라 균등 분포를 따르고, 이산판은 카이 라이 정과 윌리엄 펠러의 정리(1949)예요. 묶은 걸음과 풀린 걸음을 나란히 세울 수 있어요. 토카막 장에서 갇힌 입자가 바나나 꼴로 오가는 바나나 궤도는 Størmer Trap 의 보리스 적분을 그대로 써요. 같은 목록의 안정 매칭 전체가 이루는 분배 격자는 회전을 하나씩 밟아 가는 그림이 돼요(로버트 어빙과 폴 리더, 1986). 굵은 고리를 길이로 두 배 늘여 제 안에 두 번 감아 넣는 스메일-윌리엄스 솔레노이드(스메일 1967, 윌리엄스 1967)는 단면이 칸토어 집합인 끌개라, 세운 패널에 고리를 위아래로 길게 늘여 그릴 만해요. 합이 꼭 s 가지인 항을 고르는 크노의 s-가법 수열(1972)에도 같은 숨은 진동수가 있는지 세어 볼 수 있어요. 프랑크-카스퍼 상의 다른 거품인 C15 라베스 상은 십이면체와 십육면체로 지어서 Weaire-Phelan 의 셈으로 넓이를 견줄 수 있어요. + ### 화장품 팝업 주제 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 마다 한 번만 바뀌는 셈으로 그릴 만해요. diff --git a/incoming/pattern-study/patterns/ulam-sequence.js b/incoming/pattern-study/patterns/ulam-sequence.js index b8fe4d4a6..30564dd6c 100644 --- a/incoming/pattern-study/patterns/ulam-sequence.js +++ b/incoming/pattern-study/patterns/ulam-sequence.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Ulam Sequence // Family: number -// Idea: The Ulam sequence counted live, each new term the least number that is a sum of two earlier terms in exactly one way, wound round a circle by the hidden frequency alpha that the code finds for itself: every term sits at the angle alpha times the term, across the panel, and lower the larger it is, so the terms fill only the half of the circle where cos(alpha a) < 0, in two humps, and leave the other half empty but for 2, 3, 47 and 69, while each new one drops into the pile below. +// Idea: The Ulam sequence counted live, each new term the least number that is a sum of two earlier terms in exactly one way, wound round a circle by the hidden frequency alpha that the code finds for itself: every term sits across the panel at the angle alpha times the term and lower down the larger it is, so the terms fill only the half of the circle where cos(alpha a) < 0, in two humps, and leave the other half empty but for 2, 3, 47 and 69, while each new one drops into the pile below. // Author: patternflow-lab // Date: 2026-10-26 // SPDX-License-Identifier: CC-BY-SA-4.0 From 55aa6afde222c241a4b021f09b3d15b86485dcc9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 04:42:53 +0000 Subject: [PATCH 17/20] =?UTF-8?q?54=ED=8C=90:=20index.html=20=EC=9D=84=204?= =?UTF-8?q?38=EA=B0=9C=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20=EC=A7=93=EA=B3=A0?= =?UTF-8?q?=20=EB=85=B8=EB=B8=8C=20=EB=AC=B8=EB=8B=A8=EC=97=90=20=EB=84=A4?= =?UTF-8?q?=20=EC=9E=90=EB=A6=AC=20=EA=B0=92?= 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_01CtQeRUJHSr3xVvLYqdqFXC --- incoming/pattern-study/README.md | 2 +- incoming/pattern-study/index.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index b231044b2..61132f435 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -2891,7 +2891,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 52판의 열 개는 가장 낮은 노브가 Pumpkin Drop 의 K1 Flame 로 네 자리로 0.0342 이고, Specimen Shelf 의 K4 Knock 0.0383 과 Anscombe's Quartet 의 K3 Heat 0.0387 이 그다음이에요. 노브 마흔 개가 다 0.034 를 넘어요. 이번에도 이야기가 있는 그림은 노브를 재는 2~3초 창이 정했어요. Pumpkin Drop 의 첫 판은 호박이 꼭대기에 매달린 채 시작해 1.2초에 놓여서, 그 창에는 바닥의 조각만 남아 Flame 이 0.0071, 그때 터지는 세기이던 K4 가 0.0119 였어요. 첫 바퀴를 땅에서 감아올리는 데서 시작해 창 안에 불 켠 호박이 매달려 있게 하고, 촛불이 하늘에 던지는 빛을 두 배쯤 세게 해 e 분의 1 거리를 9 LED 에서 16 LED 로 넓히자 Flame 이 0.034 가 됐어요. K4 는 호박 크기로 바꿔 0.088 이에요. 한 번 일어나는 일에 건 노브는 그 일이 창에 들지 않으면 0 에 가까웠어요. Specimen Shelf 의 Knock 은 두드림이 2.4초에 한 번 들어와 0.06 이었다가 출렁임이 식는 동안 0.02 로 내려가서, 노브 끝의 첫 두드림을 1.5초로 당기고 출렁임을 두 배 남짓 세게 해 0.04 가 됐어요. Anscombe's Quartet 의 Heat 는 첫 모양이 창에 막 흐르기 시작해서 0.02 였고, 사중주와 접기를 줄여 모양을 창 앞으로 당기고 뜨거운 걸음을 2.5배 길게 해 0.04 가 됐어요. 같은 그림의 Hue 는 색이 점에만 실려 0.01 로 바닥 아래였어요. 회귀선 위와 아래를 레벨 23 의 옅은 청록과 보라로 나눠 칠하자 0.05 를 넘었어요. 얇은 것만 물들이는 노브는 이번에도 약해서, Banshee 의 Wind 는 머리와 옷자락만 날려 0.05 였고 센 바람에 여인을 바람 쪽으로 8 LED 옮기자 0.066 이 됐어요. -54판의 열 개는 가장 낮은 노브가 Ulam Sequence 의 K1 Hue 와 K2 Speed, Størmer Trap 의 K1 Hue 로 두 자리로 0.05 이고, 노브 마흔 개가 다 0.05 를 넘어요. 이번 판의 노브는 다 화면에 늘 있는 것을 바꿔서, 52판처럼 한 번 일어나는 일에 걸린 노브가 없었어요. 가는 선과 점만 켜진 그림은 색 노브가 약했어요. Størmer Trap 과 Ulam Sequence 는 채움이 0.292 와 0.152 라 색이 실린 LED 가 적어요. Ulam Sequence 의 첫 판은 항이 늘며 그림이 천천히 줄어들 뿐이라 움직임이 0.046 이었고, 새 항마다 제 칸 더미로 0.4초에 떨어지는 불똥을 더하자 0.201 이 되면서 Speed 도 창 안에서 보였어요. 노브 하나에 정리의 갈래를 건 판이 많았어요. Strange Eigenmode 의 K3 Stir 는 규칙 섬과 되풀이되지 않는 복소쌍, 주기마다 뒤집히는 모드를 차례로 지나요. Arcsine Law 의 K3 은 레비의 세 통계를, Ulam Sequence 의 K3 은 씨앗 여섯을 골라요. 문턱을 가운데 둔 노브에는 Lab 기본 0.06 이 문턱을 비켜 가게 죽은 띠를 뒀어요. Arcsine Law 의 Drift 는 ±0.08, Ulam Sequence 의 Detune 은 ±0.1 안에서 0 이에요. +54판의 열 개는 가장 낮은 노브가 Størmer Trap 의 K1 Hue 로 네 자리로 0.0516 이고, Ulam Sequence 의 K2 Speed 0.0534 와 K1 Hue 0.0546 이 그다음이에요. 노브 마흔 개가 다 0.05 를 넘어요. 이번 판의 노브는 다 화면에 늘 있는 것을 바꿔서, 52판처럼 한 번 일어나는 일에 걸린 노브가 없었어요. 가는 선과 점만 켜진 그림은 색 노브가 약했어요. Størmer Trap 과 Ulam Sequence 는 채움이 0.292 와 0.152 라 색이 실린 LED 가 적어요. Ulam Sequence 의 첫 판은 항이 늘며 그림이 천천히 줄어들 뿐이라 움직임이 0.046 이었고, 새 항마다 제 칸 더미로 0.4초에 떨어지는 불똥을 더하자 0.201 이 되면서 Speed 도 창 안에서 보였어요. 노브 하나에 정리의 갈래를 건 판이 많았어요. Strange Eigenmode 의 K3 Stir 는 규칙 섬과 되풀이되지 않는 복소쌍, 주기마다 뒤집히는 모드를 차례로 지나요. Arcsine Law 의 K3 은 레비의 세 통계를, Ulam Sequence 의 K3 은 씨앗 여섯을 골라요. 문턱을 가운데 둔 노브에는 Lab 기본 0.06 이 문턱을 비켜 가게 죽은 띠를 뒀어요. Arcsine Law 의 Drift 는 ±0.08, Ulam Sequence 의 Detune 은 ±0.1 안에서 0 이에요. Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index 82e749540..791f786ef 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

패턴 연구

- - + + - + + - + +