From 4bd29ec62ea89a397d58842e4f825078a4a90b8d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 07:32:53 +0000 Subject: [PATCH 01/10] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=2057=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_01XoJSXx5srrfeJ2DPWFv6K5 From f9d058c4eda08c32f42430716a4943d536bac8b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 08:31:52 +0000 Subject: [PATCH 02/10] =?UTF-8?q?57=ED=8C=90:=20Terrell=20Rotation=20?= =?UTF-8?q?=EC=99=80=20Stokes=20Drift=20=EB=A5=BC=20=EB=8D=94=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XoJSXx5srrfeJ2DPWFv6K5 --- .../pattern-study/patterns/stokes-drift.js | 315 ++++++++++++++++++ .../patterns/terrell-rotation.js | 166 +++++++++ 2 files changed, 481 insertions(+) create mode 100644 incoming/pattern-study/patterns/stokes-drift.js create mode 100644 incoming/pattern-study/patterns/terrell-rotation.js diff --git a/incoming/pattern-study/patterns/stokes-drift.js b/incoming/pattern-study/patterns/stokes-drift.js new file mode 100644 index 000000000..9eb01aa6d --- /dev/null +++ b/incoming/pattern-study/patterns/stokes-drift.js @@ -0,0 +1,315 @@ +// ===== Patternflow pattern ===== +// Title: Stokes Drift +// Family: wave +// Idea: A deep-water wave runs to the right across a cross-section of water 109 LEDs deep, and the particles stepped through its velocity field each trace a little orbit that falls short of closing, so the dye lines dropped straight down lean forward at the top along the curve e^(2kz) while the dots far below hardly stir. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Stokes drift (George Gabriel Stokes, 1847), against the trochoidal wave (Franz Gerstner, 1802), pattern round 57 +// Round: 57 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Steepness=0.15..0.4, Gerstner=0..1 +// +// Knob 1: Hue. Turns the four dye lines round the wheel together, magenta, gold, mint and azure at 0 from the left, and the dots' pale tint with them, pale green at 0; the surface stays white. +// Knob 2: Speed. The wave's period, 2.6 s divided by the knob: 1.3 s at 2, 13 s at 0.2 and 0.87 s at 3. The wavelength stays 256 LEDs, four panel widths, so the panel shows a quarter of a wave. +// Knob 3: Steepness. ak, the wave's amplitude times its wavenumber. At 0.15 the surface rises and falls 6.1 LEDs either side of its mean and the water at the top creeps 5.8 LEDs a period; at 0.25 10.2 LEDs and 16 a period; at 0.4 16.3 LEDs and 41 a period. The drift halves every 14.1 LEDs of depth, the orbits every 28.2. New dye drops when the top of the old has run 48 LEDs or after 12 periods. +// Knob 4: Gerstner. Where each particle reads the wave's velocity. At 0 it reads it where it is, Airy's linear wave in Euler's view, and the orbits leave the drift omega k a^2 e^(2kz) behind; at 1 it reads it at the centre of its own orbit, which is Gerstner's exact trochoidal wave, whose orbits close and the dye lines only sway. In between the drift is close to 1 minus the knob times the full one: 0.94 at the opening 0.06, 0.52 at 0.5. + +const W = 64, H = 128; +const { floor, min, max, abs, exp, sin, cos, PI, round } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// Wavelength and domain: the water is periodic over one wavelength, 256 +// LEDs, and the panel shows the first 64 of it. +const L = 256, K = (2 * PI) / L; +// The mean surface sits on row 18; depth runs down to row 127, 109 LEDs. +const SURF = 18, DEEP = H - 1 - SURF; +// Material: a dot every 8 LEDs across and down, four dye lines of 220 +// points packed towards the top, and a chain of 256 surface particles. +const DX = 8, DZ = 8, NLINE = 4, NPT = 220, NSURF = 256; +const LINE_X = [8, 24, 40, 56]; +const HUES = [0.88, 0.13, 0.42, 0.6]; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// A set of particles: position (x, z), z up from the mean surface, x not +// wrapped so a particle's whole run can be read; and (rx, rz), the centre of +// its orbit as the linear wave would have it. Dots and the surface are +// placed by their centres and seated on their orbits. +function pack(n) { + return { n, x: new Float64Array(n), z: new Float64Array(n), rx: new Float64Array(n), rz: new Float64Array(n) }; +} +function place(P, i, x, z) { + P.x[i] = P.rx[i] = x; + P.z[i] = P.rz[i] = z; +} + +export function setup(params) { + const nd = (L / DX) * floor(DEEP / DZ + 1); + const s = { + ph: 0, hue: 0, speed: 2, ak: 0.15, akSeat: -1, g: 0.06, age: 0, fade: 0, + dots: pack(nd), lines: pack(NLINE * NPT), old: pack(NLINE * NPT), surf: pack(NSURF), + trail: new Float32Array(W * H), rgb: new Float32Array(W * H * 3), c: new Float32Array(3), + }; + let i = 0; + for (let z = -4; z >= -DEEP; z -= DZ) for (let x = 4; x < L; x += DX) place(s.dots, i++, x, z); + s.dots.n = i; + for (let j = 0; j < NSURF; j++) place(s.surf, j, j * (L / NSURF), 0); + seat(s); + dye(s); + params.s = s; +} + +// Fresh dye lines, dropped straight down from the surface chain to the bottom, +// their points packed towards the top where the water moves most. Each +// point's orbit centre is found from where it is. +function dye(s) { + const a = s.ak / K; + for (let l = 0; l < NLINE; l++) { + const x0 = LINE_X[l], top = level(s.surf, x0); + for (let k = 0; k < NPT; k++) { + const i = l * NPT + k, z0 = top + (-DEEP - top) * (k / (NPT - 1)) ** 1.6; + s.lines.x[i] = s.lines.rx[i] = x0; + s.lines.z[i] = s.lines.rz[i] = z0; + centre(s.lines, i, a, s.ph, 12); + } + } + s.age = 0; +} + +// The height of the surface chain above x: the pair of neighbours that +// straddles x, wrapped onto one wavelength. +function level(S, x) { + for (let j = 0; j < S.n; j++) { + const k = j + 1 < S.n ? j + 1 : 0; + const x1 = S.x[j] - floor(S.x[j] / L) * L; + let x2 = S.x[k] - floor(S.x[k] / L) * L; + if (x2 < x1 - L / 2) x2 += L; + for (const xx of [x, x + L]) + if (xx >= x1 && xx < x2) return S.z[j] + ((S.z[k] - S.z[j]) * (xx - x1)) / (x2 - x1); + } + return 0; +} + +// The surface is a material line, and at steep waves its particles bunch +// at some crests and leave gaps elsewhere. When a gap opens past 1.2 LEDs +// the chain is laid out again at even steps along the same curve, each new +// point with its own orbit centre. +function even(s) { + const S = s.surf, n = S.n, a = s.ak / K, cum = new Float64Array(n + 1); + let gap = 0; + for (let j = 0; j < n; j++) { + const k = j + 1 < n ? j + 1 : 0, dx = S.x[k] + (k ? 0 : L) - S.x[j], d = Math.hypot(dx, S.z[k] - S.z[j]); + cum[j + 1] = cum[j] + d; + gap = max(gap, d); + } + if (gap < 1.2) return; + const ox = Float64Array.from(S.x), oz = Float64Array.from(S.z), orx = Float64Array.from(S.rx), orz = Float64Array.from(S.rz); + for (let m = 0, j = 0; m < n; m++) { + const want = (cum[n] * m) / n; + while (cum[j + 1] < want) j++; + const k = j + 1 < n ? j + 1 : 0, u = (want - cum[j]) / max(1e-9, cum[j + 1] - cum[j]), w = k ? 0 : L; + S.x[m] = ox[j] + (ox[k] + w - ox[j]) * u; + S.z[m] = oz[j] + (oz[k] - oz[j]) * u; + S.rx[m] = orx[j] + (orx[k] + w - orx[j]) * u; + S.rz[m] = orz[j] + (orz[k] - orz[j]) * u; + centre(S, m, a, s.ph, 6); + } +} + +// The orbit centre r of a particle at p: p = r + xi(r), where the linear +// wave moves a particle by xi = a e^(k rz) (-sin, cos)(k rx - phase); found +// by turns of the fixed point from the last centre. +function centre(P, i, a, ph, turns) { + const x = P.x[i], z = P.z[i]; + let rx = P.rx[i], rz = P.rz[i]; + for (let it = 0; it < turns; it++) { + const e = a * exp(K * rz), th = K * rx - ph; + rx = x + e * sin(th); + rz = z - e * cos(th); + } + P.rx[i] = rx; + P.rz[i] = rz; +} + +// Every particle put back on its orbit for steepness ak, keeping the orbit's +// centre, and the surface chain's centres on the mean surface. After a turn +// of Steepness the old orbits would leave dots above the new troughs, and a +// particle above the water rides the linear wave's crest away. +function seat(s) { + const a = s.ak / K; + for (const P of [s.dots, s.lines, s.old, s.surf]) + for (let i = 0; i < P.n; i++) { + if (P === s.surf) P.rz[i] = 0; + const rz = min(0, P.rz[i]), e = a * exp(K * rz), th = K * P.rx[i] - s.ph; + P.rz[i] = rz; + P.x[i] = P.rx[i] - e * sin(th); + P.z[i] = rz + e * cos(th); + } + s.akSeat = s.ak; +} + +// One step of every particle in a set from phase ph by dph (radians of the +// wave, so dt = dph / omega). The velocity of the linear deep-water wave +// eta = a cos(kx - wt) is a w e^(kz) (cos, sin)(kx - wt); per radian of +// phase, which is how it is stepped here, the w drops out. Airy's particles +// read it where they are; Gerstner's read it at their orbit's centre. The +// knob g reads it at centre + (1 - g)(where they are - centre). Only the +// Airy reading feels the wave's gradient across the orbit, and that is all +// the drift there is: nothing pushes a particle forward by hand. +function step(P, ph, dph, ak, g) { + const a = ak / K, h = dph, f = 1 - g; + for (let i = 0; i < P.n; i++) { + centre(P, i, a, ph, 2); + const x = P.x[i], z = P.z[i], rx = P.rx[i], rz = P.rz[i]; + // Classical fourth-order Runge-Kutta with the centre held for the step. + let k1x, k1z, k2x, k2z, k3x, k3z, k4x, k4z, px, pz, e, th; + px = rx + f * (x - rx); + pz = rz + f * (z - rz); + e = a * exp(K * pz); + th = K * px - ph; + k1x = e * cos(th); + k1z = e * sin(th); + px = rx + f * (x + 0.5 * h * k1x - rx); + pz = rz + f * (z + 0.5 * h * k1z - rz); + e = a * exp(K * pz); + th = K * px - ph - 0.5 * h; + k2x = e * cos(th); + k2z = e * sin(th); + px = rx + f * (x + 0.5 * h * k2x - rx); + pz = rz + f * (z + 0.5 * h * k2z - rz); + e = a * exp(K * pz); + k3x = e * cos(K * px - ph - 0.5 * h); + k3z = e * sin(K * px - ph - 0.5 * h); + px = rx + f * (x + h * k3x - rx); + pz = rz + f * (z + h * k3z - rz); + e = a * exp(K * pz); + th = K * px - ph - h; + k4x = e * cos(th); + k4z = e * sin(th); + P.x[i] = x + (h / 6) * (k1x + 2 * k2x + 2 * k3x + k4x); + P.z[i] = z + (h / 6) * (k1z + 2 * k2z + 2 * k3z + k4z); + // A particle thrown over the crests is put back on its orbit. + if (!(P.z[i] < 1.6 * a)) { + const c = min(0, rz), e2 = a * exp(K * c); + P.x[i] = rx - e2 * sin(K * rx - ph - h); + P.z[i] = c + e2 * cos(K * rx - ph - h); + } + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const s = params.s, kv = input.knobValues || [0, 2, 0.15, 0.06], d = cl(dt || 0, 0, 0.1); + s.hue = cl(kv[0], 0, 1); + s.speed = cl(kv[1], 0.2, 3); + s.ak = cl(kv[2], 0.15, 0.4); + s.g = cl(kv[3], 0, 1); + if (s.ak !== s.akSeat) seat(s); + // Phase advance this frame, in steps of at most 0.1 rad. + const dph = ((2 * PI) / 2.6) * s.speed * d, n = Math.ceil(dph / 0.1); + for (let k = 0; k < n; k++) { + for (const P of [s.dots, s.lines, s.surf]) step(P, s.ph, dph / n, s.ak, s.g); + if (s.fade > 0) step(s.old, s.ph, dph / n, s.ak, s.g); + s.ph += dph / n; + } + // The surface chain's orbit centres are eased back onto the mean surface, + // a twentieth a frame. Left alone, at Steepness past 0.33 particles that + // started at a crest and at a trough settle at slightly different mean + // heights, drift at different speeds and fold the line within a minute. + const S = s.surf, a = s.ak / K; + for (let j = 0; j < S.n; j++) { + centre(S, j, a, s.ph, 2); + const dz = 0.05 * S.rz[j]; + S.rz[j] -= dz; + S.z[j] -= dz; + } + even(s); + s.age += dph; + s.fade = max(0, s.fade - d * 1.2); + // New dye when the top of the lines has run 48 LEDs ahead, or after 12 + // periods; the old lines fade out over a second. + const run = s.lines.x[0] - LINE_X[0]; + if (run > 48 || s.age > 12 * 2 * PI) { + const t = s.old; + s.old = s.lines; + s.lines = t; + s.fade = 1; + dye(s); + } +} + +export function draw(display, params) { + const s = params.s; + if (!s) return; + const T = s.trail, B = s.rgb, c = s.c; + // The dots leave a short trail, so each traces its orbit. + for (let i = 0; i < W * H; i++) T[i] *= 0.9; + B.fill(0); + const wrap = (x) => x - floor(x / L) * L; + const splat = (buf, x, y, v) => { + const x0 = floor(x - 0.5), y0 = floor(y - 0.5), fx = x - 0.5 - x0, fy = y - 0.5 - y0; + for (let dy = 0; dy < 2; dy++) + for (let dx = 0; dx < 2; dx++) { + const X = x0 + dx, Y = y0 + dy; + if (X < 0 || X >= W || Y < 0 || Y >= H) continue; + const wgt = (dx ? fx : 1 - fx) * (dy ? fy : 1 - fy); + buf[Y * W + X] += v * wgt; + } + }; + const D = s.dots; + for (let i = 0; i < D.n; i++) { + let x = wrap(D.x[i]); + if (x > L - 1) x -= L; + if (x < W + 1) splat(T, x, SURF - D.z[i], 0.3); + } + // A line through a chain of points, drawn as samples every half LED, each + // lighting its four nearest LEDs by how near it falls. + const chain = (P, from, to, r, g, b, closed) => { + for (let i = from; i < to - (closed ? 0 : 1); i++) { + const j = i + 1 < to ? i + 1 : from; + const x1 = wrap(P.x[i]); + let x2 = wrap(P.x[j]); + if (x2 - x1 > L / 2) x2 -= L; + else if (x1 - x2 > L / 2) x2 += L; + const y1 = SURF - P.z[i], y2 = SURF - P.z[j]; + const m = max(1, Math.ceil(2 * max(abs(x2 - x1), abs(y2 - y1)))); + for (let k = 0; k < m; k++) { + const u = k / m, y = y1 + (y2 - y1) * u; + let xx = x1 + (x2 - x1) * u; + if (xx > W + 1) xx -= L; + if (xx < -1 || xx > W + 1) continue; + const x0 = floor(xx - 0.5), y0 = floor(y - 0.5), fx = xx - 0.5 - x0, fy = y - 0.5 - y0; + for (let dy = 0; dy < 2; dy++) + for (let dx = 0; dx < 2; dx++) { + const X = x0 + dx, Y = y0 + dy; + if (X < 0 || X >= W || Y < 0 || Y >= H) continue; + const wgt = min(1, 2 * (dx ? fx : 1 - fx) * (dy ? fy : 1 - fy)), o = (Y * W + X) * 3; + B[o] = max(B[o], r * wgt); + B[o + 1] = max(B[o + 1], g * wgt); + B[o + 2] = max(B[o + 2], b * wgt); + } + } + } + }; + for (let l = 0; l < NLINE; l++) { + hsv(HUES[l] + s.hue, 0.75, 1, c, 0); + chain(s.lines, l * NPT, (l + 1) * NPT, c[0], c[1], c[2], false); + if (s.fade > 0) chain(s.old, l * NPT, (l + 1) * NPT, c[0] * s.fade * 0.6, c[1] * s.fade * 0.6, c[2] * s.fade * 0.6, false); + } + chain(s.surf, 0, NSURF, 0.85, 0.88, 0.95, true); + // The dots in a pale tint half the wheel from the first dye line. + hsv(HUES[0] + 0.5 + s.hue, 0.55, 0.62, c, 0); + const dr = c[0], dg = c[1], db = c[2]; + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const o = p * 3, t = min(0.5, T[p]); + display.setPixel(x, y, round(min(1, B[o] + t * dr) * 255), round(min(1, B[o + 1] + t * dg) * 255), round(min(1, B[o + 2] + t * db) * 255)); + } +} diff --git a/incoming/pattern-study/patterns/terrell-rotation.js b/incoming/pattern-study/patterns/terrell-rotation.js new file mode 100644 index 000000000..c4aea4477 --- /dev/null +++ b/incoming/pattern-study/patterns/terrell-rotation.js @@ -0,0 +1,166 @@ +// ===== Patternflow pattern ===== +// Title: Terrell Rotation +// Family: space +// Idea: A sheet of cubes and spheres slides up past a pinhole camera at a share of light's speed, and each LED shows the light that left the sheet earlier and reaches the camera now, so the cubes look turned to show their undersides instead of squashed, the spheres keep round outlines while their nets swing round, and the colours shift towards blue below, where the light left parts coming on. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Terrell-Penrose rotation (James Terrell, 1959; Roger Penrose, 1959), after Anton Lampa (1924), pattern round 57 +// Round: 57 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Beta=0..0.95, Distance=2..8 +// +// Knob 1: Hue. The sheet's colour at rest, azure at 0, turned round the wheel. The Doppler shift moves each LED along the wheel from there by 0.12 times the log of the Doppler factor: towards violet where the light left a part coming towards the camera, towards green where it left a part going away. +// Knob 2: Speed. How fast the sheet slides on the panel, not its speed against light's: 0.45 times the knob in cells of the sheet's own length a second, 0.9 at 2, so a new row comes in about every 1.1 s whatever Beta is. +// Knob 3: Beta. The sheet's speed as a share of light's. At 0 the camera sees plain perspective. At 0.3 a cube seen straight out from the camera looks turned 17.5 degrees, at 0.6 36.9 and at 0.9 64.2, the arcsine of Beta, showing its trailing underside; the spheres stay round. The rows bunch up towards the top, and straight out the light arrives with the Doppler factor 1/gamma, 0.44 at 0.9. +// Knob 4: Distance. The camera's distance from the sheet, 2 to 8 cells, with the lens zoomed to keep three columns across the panel: it sees 54 degrees above and below the middle at 2, where the near rows bend, and 19 degrees at 8, where every cube is turned by nearly the same angle. + +const W = 64, H = 128; +const { floor, min, max, abs, sqrt, acos, atan2, PI, round } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// The sheet in its own rest frame, in cells: a cube or a sphere on every +// cell of a square grid in the plane z = D, chequered, cubes 0.5 on a side +// and spheres 0.68 across, so neither touches a neighbour. +const HC = 0.25, RS = 0.34, HM = 0.34; +// The net on each sphere: its pole looks back at the camera along -z, +// parallels every 30 degrees and six meridians. +const DPAR = PI / 6, DMER = PI / 3; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +export function setup(params) { + params.s = { + phi: 0, hue: 0, speed: 2, beta: 0, dist: 2, key: "", + // Per LED: the ray in the sheet's frame (ex, ey, ez) and the Doppler + // factor of light arriving along it. + ray: new Float32Array(W * H * 4), rgb: new Float32Array(3), + }; +} + +// One pixel is one direction at the camera. Light reaching the camera from +// camera-frame direction d (unit, y up the panel) left the sheet along the +// sheet-frame direction (dx, gamma (dy + beta), dz) / (gamma (1 + beta dy)): +// aberration, the whole of the effect. Emission point and arrival meet on a +// straight line in the sheet's frame, so a ray traced there from the +// camera's present place finds what the LED shows. 1 / (gamma (1 + beta dy)) +// is the Doppler factor. Rays only change with Beta and the lens. +function aim(s) { + const b = s.beta, g = 1 / sqrt(1 - b * b), F = 23 * s.dist, R = s.ray; + for (let y = 0, o = 0; y < H; y++) + for (let x = 0; x < W; x++, o += 4) { + const X = (x + 0.5 - 32) / F, A = (64 - y - 0.5) / F, n = 1 / sqrt(X * X + A * A + 1); + const dy = A * n, k = 1 / (g * (1 + b * dy)); + R[o] = X * n * k; + R[o + 1] = g * (dy + b) * k; + R[o + 2] = n * k; + R[o + 3] = k; + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const s = params.s, kv = input.knobValues || [0, 2, 0, 2], d = cl(dt || 0, 0, 0.1); + s.hue = cl(kv[0], 0, 1); + s.speed = cl(kv[1], 0.2, 3); + s.beta = cl(kv[2], 0, 0.95); + s.dist = cl(kv[3], 2, 8); + // phi is how far the camera has gone through the sheet, in the sheet's + // own cells. + s.phi += d * 0.45 * s.speed; +} + +export function draw(display, params) { + const s = params.s; + if (!s) return; + const key = s.beta.toFixed(4) + "/" + s.dist.toFixed(3); + if (key !== s.key) aim(s), (s.key = key); + const R = s.ray, D = s.dist, F = 23 * D, phi = s.phi % 2, out = s.rgb, h0 = 0.56 + s.hue; + const z0 = D - HM, z1 = D + HM; + + for (let y = 0, o = 0; y < H; y++) + for (let x = 0; x < W; x++, o += 4) { + const ex = R[o], ey = R[o + 1], ez = R[o + 2], dop = R[o + 3]; + // The slab the sheet fills, and the cells the ray crosses inside it. + const t0 = z0 / ez, t1 = z1 / ez; + const ya = -phi + t0 * ey, yb = -phi + t1 * ey, xa = t0 * ex, xb = t1 * ex; + const jlo = Math.ceil(min(ya, yb) - HM), jhi = floor(max(ya, yb) + HM); + const clo = max(-2, Math.ceil(min(xa, xb) - HM)), chi = min(2, floor(max(xa, xb) + HM)); + let best = 1e9, kind = 0, bj = 0, bc = 0, axis = 0; + for (let j = jlo; j <= jhi; j++) + for (let c = clo; c <= chi; c++) { + // Local ray origin: the camera relative to this cell's centre. + const ox = -c, oy = -phi - j, oz = -D; + if (((j + c) & 1) === 0) { + // A cube, by slabs. + let tn = -1e9, tf = 1e9, ax = 0; + const ix = 1 / ex, iy = 1 / ey; + let a = (-HC - ox) * ix, b2 = (HC - ox) * ix; + if (a > b2) [a, b2] = [b2, a]; + if (a > tn) (tn = a), (ax = 0); + if (b2 < tf) tf = b2; + a = (-HC - oy) * iy; + b2 = (HC - oy) * iy; + if (a > b2) [a, b2] = [b2, a]; + if (a > tn) (tn = a), (ax = 1); + if (b2 < tf) tf = b2; + a = (-HC - oz) / ez; + b2 = (HC - oz) / ez; + if (a > tn) (tn = a), (ax = 2); + if (b2 < tf) tf = b2; + if (tn <= tf && tn > 0 && tn < best) (best = tn), (kind = 1), (bj = j), (bc = c), (axis = ax); + } else { + const b2 = ox * ex + oy * ey + oz * ez, cc = ox * ox + oy * oy + oz * oz - RS * RS, disc = b2 * b2 - cc; + if (disc <= 0) continue; + const t = -b2 - sqrt(disc); + if (t > 0 && t < best) (best = t), (kind = 2), (bj = j), (bc = c); + } + } + if (!kind) { + display.setPixel(x, y, 0, 0, 0); + continue; + } + const px = best * ex - bc, py = best * ey - phi - bj, pz = best * ez - D; + // The size of one LED on the surface, in cells: Z / F across, and a + // Doppler factor shorter along the motion, then stretched where the + // surface is seen edge on. + const foot = (best * ez) / (F * sqrt(dop)); + let fill, line; + if (kind === 1) { + const ea = axis === 0 ? abs(ex) : axis === 1 ? abs(ey) : ez; + const u = axis === 0 ? max(abs(py), abs(pz)) : axis === 1 ? max(abs(px), abs(pz)) : max(abs(px), abs(py)); + const L = (foot * ez) / max(0.22, ea); + line = cl(1.15 - (HC - u) / L, 0, 1); + // The face towards the camera's side is brightest, the rows' tops and + // bottoms next, the sides dimmest. + fill = axis === 2 ? 0.17 : axis === 1 ? 0.12 : 0.085; + } else { + const mx = px / RS, my = py / RS, mz = pz / RS; + const facing = -(mx * ex + my * ey + mz * ez); + const th = acos(cl(-mz, -1, 1)), ps = atan2(my, mx); + const dp = RS * abs(th - round(th / DPAR) * DPAR), dm = RS * Math.sin(th) * abs(ps - round(ps / DMER) * DMER); + const L = foot / max(0.25, facing); + line = max(cl(1.1 - dp / L, 0, 1), cl(1.1 - dm / L, 0, 1) * cl(th * 3, 0, 1)); + // The pole itself, and the rim where the ray grazes the outline. + if (th < 0.2) line = 1; + const rim = (1 - facing) * (1 - facing); + line = max(line, rim * rim * 1.4); + fill = 0.06 + 0.1 * facing; + } + // Doppler: hue along the wheel by 0.12 times the log of the factor, blue + // for light from parts coming on, held short of yellow, which reads olive + // when dim; and brightness by a softened searchlight, the + // factor to the power 0.6 where the true intensity goes as its cube. + const ld = Math.log(dop); + const bright = cl(Math.exp(0.6 * ld), 0.5, 1.35); + const v = min(1, (fill + (0.95 - fill) * line) * bright); + hsv(h0 + cl(0.12 * ld, -0.24, 0.16), 0.85 - 0.4 * line, v, out, 0); + display.setPixel(x, y, round(out[0] * 255), round(out[1] * 255), round(out[2] * 255)); + } +} From 93f51eff68c65c97cdeebfb7c79b59806930af1b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 08:34:57 +0000 Subject: [PATCH 03/10] =?UTF-8?q?57=ED=8C=90:=20Terrell=20Rotation=20?= =?UTF-8?q?=EC=9D=B4=20=EB=B9=9B=EC=9D=98=20=EC=A0=88=EB=B0=98=20=EB=B9=A0?= =?UTF-8?q?=EB=A5=B4=EA=B8=B0=EC=97=90=EC=84=9C=20=EC=97=B4=EB=A6=B0?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XoJSXx5srrfeJ2DPWFv6K5 --- incoming/pattern-study/patterns/terrell-rotation.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/incoming/pattern-study/patterns/terrell-rotation.js b/incoming/pattern-study/patterns/terrell-rotation.js index c4aea4477..a681db7b8 100644 --- a/incoming/pattern-study/patterns/terrell-rotation.js +++ b/incoming/pattern-study/patterns/terrell-rotation.js @@ -9,11 +9,11 @@ // Round: 57 // =============================== // @matrix 64x128 -// @knobs Hue=0..1, Speed=0.2..3, Beta=0..0.95, Distance=2..8 +// @knobs Hue=0..1, Speed=0.2..3, Beta=0.5..0.95, Distance=2..8 // // Knob 1: Hue. The sheet's colour at rest, azure at 0, turned round the wheel. The Doppler shift moves each LED along the wheel from there by 0.12 times the log of the Doppler factor: towards violet where the light left a part coming towards the camera, towards green where it left a part going away. // Knob 2: Speed. How fast the sheet slides on the panel, not its speed against light's: 0.45 times the knob in cells of the sheet's own length a second, 0.9 at 2, so a new row comes in about every 1.1 s whatever Beta is. -// Knob 3: Beta. The sheet's speed as a share of light's. At 0 the camera sees plain perspective. At 0.3 a cube seen straight out from the camera looks turned 17.5 degrees, at 0.6 36.9 and at 0.9 64.2, the arcsine of Beta, showing its trailing underside; the spheres stay round. The rows bunch up towards the top, and straight out the light arrives with the Doppler factor 1/gamma, 0.44 at 0.9. +// Knob 3: Beta. The sheet's speed as a share of light's, from 0.5, where a cube seen straight out from the camera looks turned 30 degrees, through 36.9 at 0.6 to 64.2 at 0.9 and 71.8 at 0.95, the arcsine of Beta, showing its trailing underside; the spheres stay round. The rows bunch up towards the top, and straight out the light arrives with the Doppler factor 1/gamma, 0.44 at 0.9. // Knob 4: Distance. The camera's distance from the sheet, 2 to 8 cells, with the lens zoomed to keep three columns across the panel: it sees 54 degrees above and below the middle at 2, where the near rows bend, and 19 degrees at 8, where every cube is turned by nearly the same angle. const W = 64, H = 128; @@ -69,7 +69,7 @@ export function update(dt, input, params) { const s = params.s, kv = input.knobValues || [0, 2, 0, 2], d = cl(dt || 0, 0, 0.1); s.hue = cl(kv[0], 0, 1); s.speed = cl(kv[1], 0.2, 3); - s.beta = cl(kv[2], 0, 0.95); + s.beta = cl(kv[2], 0.5, 0.95); s.dist = cl(kv[3], 2, 8); // phi is how far the camera has gone through the sheet, in the sheet's // own cells. From 504e4a162433ce752dd07311a7a85007aefd5d1c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 08:35:56 +0000 Subject: [PATCH 04/10] =?UTF-8?q?57=ED=8C=90:=20Sinai's=20Walk=20=EC=99=80?= =?UTF-8?q?=20Galperin=20Billiard=20=EB=A5=BC=20=EB=8D=94=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XoJSXx5srrfeJ2DPWFv6K5 --- .../patterns/galperin-billiard.js | 331 ++++++++++++++++++ incoming/pattern-study/patterns/sinai-walk.js | 325 +++++++++++++++++ 2 files changed, 656 insertions(+) create mode 100644 incoming/pattern-study/patterns/galperin-billiard.js create mode 100644 incoming/pattern-study/patterns/sinai-walk.js diff --git a/incoming/pattern-study/patterns/galperin-billiard.js b/incoming/pattern-study/patterns/galperin-billiard.js new file mode 100644 index 000000000..ea1b73afc --- /dev/null +++ b/incoming/pattern-study/patterns/galperin-billiard.js @@ -0,0 +1,331 @@ +// ===== Patternflow pattern ===== +// Title: Galperin Billiard +// Family: physics +// Idea: A heavy block slides down a rail onto a light one above a wall at the bottom, and every collision, block on block or light block on wall, lights one dot of a tally that groups itself in tens, hundreds and thousands, so with masses 100^k apart it reads 3, 31, 314 or 3141; above it the two velocities, scaled by the square roots of the masses, are a point on the energy circle that zigzags from the circle's left end to its right, one chord per collision, sweeping the disc until it lands in the wedge of angle arctan(sqrt(m/M)) where no collision is left. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Gregory Galperin, Playing pool with π (2003), pattern round 57 +// Round: 57 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Digits=0..3, Warp=0..8 +// +// Knob 1: Hue. Turns the colours round the wheel together. At 0 the heavy block is violet and the light one cyan, the swept disc runs violet through blue to green from left to right, and the tally is cyan, its thousands at k = 3 cyan, violet, green and rose; at 0.5 the heavy block is green, the light one pink and the disc runs green through rose to violet. The wheel skips orange to lime, and the unswept disc stays slate blue. +// Knob 2: Speed. 2 is real time away from the burst, the heavy block coming down at 60 rows a second; 0.2 is ten times slower and 3 half as fast again. At 2, with Warp at its opening 0.06, the blocks meet 1.4 to 1.7 seconds into a run, the heavy block is off the top about 4 seconds in, and the tally holds 1.8 seconds before the next run. +// Knob 3: Digits. k from 0 to 3: M = 100^k m, so 1, 100, 10,000 or 1,000,000 times the light block, and the collisions number 3, 31, 314 and 3141, the count of steps of theta = arctan(sqrt(m/M)), 45, 5.71, 0.573 or 0.0573 degrees, that fit below pi. The heavy block is 4 + 6k rows tall. The tally's dots are 15, 4, 1 and 1 LEDs wide at a pitch of 16, 5, 2 and 1. At k = 3 the middle 90% of the collisions fall within 3.4 ms and the closest two are 0.27 microseconds apart. +// Knob 4: Warp. Seconds of slow motion added to the burst at Speed 2, from 0 to 8, shared evenly so each collision gets Warp / n of them: at the opening 0.06 the burst runs at nearly real time and the middle 90% of k = 3's collisions pass within two frames; at 8 they come 393 a second, and k = 1's 31 come four a second. + +const W = 64, H = 128; +const { floor, ceil, min, max, abs, sqrt, exp, atan, atan2, cos, sin, round, PI, hypot } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// The rail: the wall's top edge is row 125's top; z counts rows up from it. +// The light block starts Z1 rows up: at 16 the last collision of k = 2, +// the latest of the four, still happens on the panel, the heavy block 98 +// rows up. +const FLOOR = 125, V0 = 60, HL = 4, Z1 = 16; +const HOLD = 1.8, FADE = 0.6; +// The energy circle and the tally beside the rail. +const CX = 37.5, CY = 25.5, R = 24, TX = 13, TB = 124; +// The tally's thousands at k = 3, as hues at knob 0 (they go through the +// wheel below): cyan, violet, green, rose. +const BAND = [0.5, 0.75, 0.36, 0.92]; + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; + return out; +} +// The knob turns hues on a wheel that skips orange through yellow to lime, +// which read brown and olive when dim. Hues here are given as they look at +// knob 0, from 0.28 round to 1.02. +const tone = (h, k) => 0.28 + 0.74 * (((h - 0.28) / 0.74 + k) % 1); + +// The collisions, event by event and in closed form: the blocks move at +// constant speed between events, so each next event time is a gap divided +// by a closing speed. Velocities are positive up, away from the wall. The +// light block (mass m) sits at z1, HL rows tall; the heavy one (mass M = +// 100^k m) has its lower face at z2. Block on block, momentum and energy +// give v' = ((m - M) v + 2 M V) / (m + M) and V' = ((M - m) V + 2 m v) / +// (m + M), here with r = m / M; at the wall v' = -v. It stops when the +// light block moves up no faster than the heavy one: 0 <= v <= V. +function collide(k, hh) { + const r = 1 / 100 ** k; + let v = 0, V = -V0, z1 = Z1, z2 = FLOOR - hh, t = 0; + const T = [0], A = [z1], B = [z2], VL = [v], VH = [V], kind = [0]; + for (let guard = 0; guard < 400000; guard++) { + const tb = V < v ? max(0, z2 - z1 - HL) / (v - V) : Infinity; + const tw = v < 0 ? max(0, z1) / -v : Infinity; + if (tb === Infinity && tw === Infinity) break; + if (tb <= tw) { + t += tb; + z1 += v * tb; + z2 = z1 + HL; + const nv = ((r - 1) * v + 2 * V) / (1 + r), nV = ((1 - r) * V + 2 * r * v) / (1 + r); + v = nv; + V = nV; + kind.push(1); + } else { + t += tw; + z1 = 0; + z2 += V * tw; + v = -v; + kind.push(2); + } + T.push(t), A.push(z1), B.push(z2), VL.push(v), VH.push(V); + } + return { T, A, B, VL, VH, kind, n: T.length - 1, r }; +} + +// The slow motion. Display time s = tau + b N(tau), where N climbs by one +// across each collision, linearly between the midpoints of the gaps around +// it, and b = Warp / n. Far from the burst s runs with tau; inside it each +// collision gets b seconds however close the next one is. Both directions +// are exact on the piecewise linear knots. +function warp(E, b) { + const T = E.T, n = E.n, m = new Float64Array(n + 1), sk = new Float64Array(n + 1); + m[0] = T[1] - (T[2] - T[1]) / 2; + for (let j = 1; j < n; j++) m[j] = (T[j] + T[j + 1]) / 2; + m[n] = T[n] + (T[n] - T[n - 1]) / 2; + for (let j = 0; j <= n; j++) sk[j] = m[j] + b * j; + E.m = m; + E.sk = sk; + E.b = b; +} +function tauOf(E, s) { + const { m, sk, n } = E; + if (s <= sk[0]) return s; + if (s >= sk[n]) return s - E.b * n; + let lo = 0, hi = n; + while (hi - lo > 1) { + const mid = (lo + hi) >> 1; + if (sk[mid] <= s) lo = mid; + else hi = mid; + } + const span = sk[lo + 1] - sk[lo]; + return span > 0 ? m[lo] + ((s - sk[lo]) * (m[lo + 1] - m[lo])) / span : m[lo]; +} +function sOf(E, tau) { + const { m, n } = E; + if (tau <= m[0]) return tau; + if (tau >= m[n]) return tau + E.b * n; + let lo = 0, hi = n; + while (hi - lo > 1) { + const mid = (lo + hi) >> 1; + if (m[mid] <= tau) lo = mid; + else hi = mid; + } + const span = m[lo + 1] - m[lo]; + return tau + E.b * (lo + (span > 0 ? (tau - m[lo]) / span : 0)); +} +// Collisions done by time tau. +function doneBy(E, tau) { + let lo = 0, hi = E.n + 1; + while (hi - lo > 1) { + const mid = (lo + hi) >> 1; + if (E.T[mid] <= tau) lo = mid; + else hi = mid; + } + return lo; +} + +// Where collision i's dot sits in the tally: hundreds are squares of ten +// rows of ten, filled from the bottom; squares run five to a row at pitch 1 +// and two at pitch 2 (k = 0 and 1 never fill one), and at pitch 1 each two +// rows of squares, a thousand, stand apart from the next by a row. +function tallyAt(k, i) { + const p = [16, 5, 2, 1][k], side = 10 * p + (p > 1 ? p : 0), per = max(1, floor((W - TX + (p > 1 ? p : 0)) / side)); + const q = floor(i / 100), rr = i % 100, sr = floor(q / per), sc = q % per; + const x = TX + sc * side + (rr % 10) * p; + const y = TB - sr * 10 * p - (p === 1 ? floor(sr / 2) : sr * p) - floor(rr / 10) * p; + return [x, y, p > 1 ? p - 1 : 1]; +} + +export function setup(params) { + params.s = { k: -1, warpS: -1, s: 0, phase: 0, pt: 0, hue: 0, rgb: new Float32Array(W * H * 3), E: null, c: [0, 0, 0], c2: [0, 0, 0] }; +} + +// The run ends once the heavy block is off the top and the last collision +// half a second gone. +const endOf = (E) => max(sOf(E, E.T[E.n] + (H + 4 - E.B[E.n]) / max(1, E.VH[E.n])), E.sAt[E.n] + 0.5); + +function start(S, k, wsec) { + S.k = k; + S.hh = HL + 6 * k; + S.E = collide(k, S.hh); + S.warpS = wsec; + warp(S.E, wsec / S.E.n); + const E = S.E; + // Points on the energy circle: (sqrt(M) V, sqrt(m) v) / (sqrt(M) V0). + const q = sqrt(E.r); + E.px = E.VH.map((V) => V / V0); + E.py = E.VL.map((v) => (q * v) / V0); + E.theta = atan(q); + E.sAt = E.T.map((t) => sOf(E, t)); + E.sEnd = endOf(E); + S.s = 0; + S.phase = 0; + S.pt = 0; +} + +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 = cl(kv[0], 0, 1); + const speed = cl(kv[1], 0.2, 3), k = round(cl(kv[2], 0, 3)), wsec = cl(kv[3], 0, 8); + if (k !== S.k) start(S, k, wsec); + else if (abs(wsec - S.warpS) > 1e-3) { + // A new warp keeps the moment on screen where it is. + const E = S.E, tau = tauOf(E, S.s); + S.warpS = wsec; + warp(E, wsec / E.n); + E.sAt = E.T.map((t) => sOf(E, t)); + E.sEnd = endOf(E); + S.s = sOf(E, tau); + } + const g = (d * speed) / 2; + if (S.phase === 0) { + S.s += g; + if (S.s >= S.E.sEnd + HOLD) (S.phase = 1), (S.pt = 0); + } else { + S.pt += g; + if (S.pt >= FADE) start(S, k, wsec); + } +} + +// A point laid over its four nearest LEDs by how close it falls. +function splat(rgb, x, y, c, a) { + const fx = x - 0.5, fy = y - 0.5, ix = floor(fx), iy = floor(fy), tx = fx - ix, ty = fy - iy; + for (let dy = 0; dy < 2; dy++) { + const yy = iy + dy; + if (yy < 0 || yy >= H) continue; + const wy = dy ? ty : 1 - ty; + for (let dx = 0; dx < 2; dx++) { + const xx = ix + dx; + if (xx < 0 || xx >= W) continue; + const w = (dx ? tx : 1 - tx) * wy * a, o = (yy * W + xx) * 3; + rgb[o] = max(rgb[o], c[0] * w); + rgb[o + 1] = max(rgb[o + 1], c[1] * w); + rgb[o + 2] = max(rgb[o + 2], c[2] * w); + } + } +} +function line(rgb, x0, y0, x1, y1, c, a) { + const len = hypot(x1 - x0, y1 - y0), m = max(1, ceil(len * 2)); + for (let e = 0; e <= m; e++) splat(rgb, x0 + ((x1 - x0) * e) / m, y0 + ((y1 - y0) * e) / m, c, a * 1.6); +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.E) return; + const E = S.E, rgb = S.rgb, th = E.theta, hue = S.hue, c = S.c, c2 = S.c2; + const s = min(S.s, E.sEnd + HOLD), tau = tauOf(E, s), done = doneBy(E, tau); + const fade = S.phase === 1 ? 1 - S.pt / FADE : min(1, s / 0.3); + rgb.fill(0); + const add = (x, y, col, a) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + rgb[o] += col[0] * a; + rgb[o + 1] += col[1] * a; + rgb[o + 2] += col[2] * a; + }; + const since = (j) => s - E.sAt[j]; + + // The energy circle. Inside it, the part the chords have swept: the + // polygon of every point so far, which is the disc on the start's side + // of the latest chord, less the thin caps outside each chord joining two + // points on the same side. + const cx = E.px, cy = E.py, T2 = floor(done / 2), B2 = ceil(done / 2); + const ti = 2 * T2, bi = done > 0 ? 2 * B2 - 1 : 0; + const ex = cx[bi] - cx[ti], ey = cy[bi] - cy[ti]; + const ref = ex * (cy[0] - cy[ti]) - ey * (cx[0] - cx[ti]); + const cth = cos(th); + // The unswept disc and the rim stay slate blue whatever the hue: dim + // and turned to red they read brown. + const disc = [0.45, 0.6, 1]; + const swept = c2; + for (let y = floor(CY - R - 1); y <= ceil(CY + R + 1); y++) + for (let x = floor(CX - R - 1); x <= ceil(CX + R + 1); x++) { + const ux = (x + 0.5 - CX) / R, uy = (CY - y - 0.5) / R, rr = hypot(ux, uy); + if (rr > 1 + 1.5 / R) continue; + // The rim, a pixel and a half wide. + const rim = cl(1 - abs(rr - 1) * R / 0.9, 0, 1); + if (rim > 0) add(x, y, disc, 0.4 * rim); + if (rr >= 1) continue; + let a = 0.09; + if (done >= 2 && (ex * (uy - cy[ti]) - ey * (ux - cx[ti])) * ref > 0) { + const phi = atan2(uy, ux); + let ok = true; + if (phi >= 0) { + const j = floor((PI - phi) / (2 * th)); + if (j < T2) ok = ux * cos(PI - th * (2 * j + 1)) + uy * sin(PI - th * (2 * j + 1)) <= cth; + } else { + const j = floor((phi + PI) / (2 * th)); + if (j < B2) ok = ux * cos(-PI + th * (2 * j + 1)) + uy * sin(-PI + th * (2 * j + 1)) <= cth; + } + if (ok) a = 0.55; + } + if (a > 0.1) hsv(tone(0.76 - 0.2 * (ux + 1), hue), 0.7, 1, swept); + add(x, y, a > 0.1 ? swept : disc, a); + } + // The wedge 0 <= angle <= theta, where v <= V and no collision is left. + const goal = [1, 0.95, 0.8]; + line(rgb, CX, CY, CX + R, CY, goal, 0.35); + line(rgb, CX, CY, CX + R * cos(th), CY - R * sin(th), goal, 0.35); + // The chords: every one so far, faint (the last 400 at k = 3, the older + // ones lie under the swept disc), and the newest bright. + const chord = [0.85, 0.95, 1], keep = [0.6, 0.45, 0.16, 0.12][S.k]; + for (let j = max(1, done - 400); j <= done; j++) { + const age = since(j), a = keep + 0.7 * exp(-age / 0.25); + line(rgb, CX + R * cx[j - 1], CY - R * cy[j - 1], CX + R * cx[j], CY - R * cy[j], chord, min(1, a)); + } + // The state point. + const sx = CX + R * cx[done], sy = CY - R * cy[done]; + for (const [dx, dy] of [[0, 0], [-0.7, 0], [0.7, 0], [0, -0.7], [0, 0.7]]) splat(rgb, sx + dx, sy + dy, [1, 1, 1], 1); + + // The tally, one dot per collision; the newest flash white. + for (let i = 0; i < done; i++) { + const [x, y, w] = tallyAt(S.k, i), q = floor(i / 100); + const band = S.k === 3 ? BAND[floor(i / 1000)] : BAND[0]; + hsv(tone(band, hue), 0.75, q % 2 ? 0.62 : 0.9, c); + const f = 0.9 * exp(-since(i + 1) / 0.3); + for (let e = 0; e < 3; e++) c2[e] = c[e] + (1 - c[e]) * f; + for (let yy = 0; yy < w; yy++) for (let xx = 0; xx < w; xx++) add(x + xx, y - yy, c2, 1); + } + + // The rail, the wall and the blocks. + const rail = hsv(tone(0.62, hue), 0.35, 1, [0, 0, 0]); + for (let y = 0; y < FLOOR; y++) add(5, y, rail, 0.16); + // The last wall hit and the last block hit, for a flash. + let lastW = -1, lastB = -1; + for (let j = done; j >= 1 && (lastW < 0 || lastB < 0) && j > done - 3; j--) { + if (E.kind[j] === 2 && lastW < 0) lastW = j; + if (E.kind[j] === 1 && lastB < 0) lastB = j; + } + const wf = lastW > 0 ? exp(-since(lastW) / 0.15) : 0; + const wall = hsv(tone(0.3, hue), 0.25, 1, [0, 0, 0]); + for (let y = FLOOR; y < H; y++) for (let x = 0; x < 11; x++) add(x, y, wall, 0.55 + 0.45 * wf); + const at = (j0, z0, v0) => z0 + v0 * (tau - E.T[j0]); + const z1 = at(done, E.A[done], E.VL[done]), z2 = at(done, E.B[done], E.VH[done]); + const light = hsv(tone(0.5, hue), 0.7, 1, [0, 0, 0]); + const heavy = hsv(tone(0.78, hue), 0.7, 1, [0, 0, 0]); + const block = (za, zb, x0, x1, col, a) => { + const ya = FLOOR - zb, yb = FLOOR - za; + for (let y = max(0, floor(ya)); y <= min(H - 1, floor(yb)); y++) { + const cov = min(y + 1, yb) - max(y, ya); + if (cov > 0) for (let x = x0; x <= x1; x++) add(x, y, col, a * cov); + } + }; + const bf = lastB > 0 ? exp(-since(lastB) / 0.15) : 0; + block(z1, z1 + HL, 2, 8, light, 0.9); + block(z2, z2 + S.hh, S.k ? 0 : 2, S.k ? 10 : 8, heavy, 0.85); + // The face where they meet glows on each hit. + if (bf > 0.02) block(z2 - 0.5, z2 + 0.5, S.k ? 0 : 2, S.k ? 10 : 8, [1, 1, 1], 0.8 * bf); + + for (let y = 0, o = 0; y < H; y++) + for (let x = 0; x < W; x++, o += 3) + display.setPixel(x, y, (255 * cl(rgb[o] * fade, 0, 1)) | 0, (255 * cl(rgb[o + 1] * fade, 0, 1)) | 0, (255 * cl(rgb[o + 2] * fade, 0, 1)) | 0); +} diff --git a/incoming/pattern-study/patterns/sinai-walk.js b/incoming/pattern-study/patterns/sinai-walk.js new file mode 100644 index 000000000..e7156476c --- /dev/null +++ b/incoming/pattern-study/patterns/sinai-walk.js @@ -0,0 +1,325 @@ +// ===== Patternflow pattern ===== +// Title: Sinai's Walk +// Family: noise +// Idea: A random potential runs down the panel as a jagged ridge, one site to a row and each site with its own coin for a step up or down; 3,000 walkers start on one row of this one landscape and run on a clock of log t, so a pool fills every valley to depth log t and rises steadily while the walkers glow in the pool that holds them, stay there until it spills over a ridge into a deeper valley, then pour over together, until they gather in the deepest valley on the panel and a fresh landscape is drawn. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: random walk in a random environment (Fred Solomon, 1975), Sinai's walk and its (log t)^2 law (Yakov Sinai, 1982), the walkers' localization in one valley (Andrei Golosov, 1984), the transient walk X_t ~ t^kappa (Harry Kesten, M. V. Kozlov and Frank Spitzer, 1975), the valleys of depth h as h-extrema (Jacques Neveu and Jim Pitman, 1989), pattern round 57 +// Round: 57 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Bias=0..1, Disorder=1.2..2.4 +// +// Knob 1: Hue. Turns the colours round the wheel together. At 0 each valley's ground has its own shade between azure and violet, the valley holding the walkers a fifth brighter, the pools are cyan with a bright surface line and the walkers pale gold; at 0.5 the ground runs rose to magenta, the pools are pink and the walkers pale blue. The wheel skips orange to lime, which reads brown on the dim ground. +// Knob 2: Speed. How fast log t runs: 1 a second at 2, so a walker has taken 19 steps after 3 seconds, 2,980 after 8 and 162,750 after 12; 0.1 a second at 0.2 and 1.5 at 3. A run ends 4.5 units of log t after the walkers' valley first reaches the deepest point of the panel, at log t 9.7 for the median landscape at the opening Disorder, then holds 1.8 seconds. +// Knob 3: Bias. 0 is Sinai's recurrent walk: log rho = log((1 - omega)/omega) has mean 0, the walkers start on row 64 and the potential wanders with no slope. Above 0 the mean of log rho is -Bias x Disorder^2 / 2, which makes E[rho^kappa] = 1 at kappa = Bias exactly; the ridge leans, the start moves up to row 4 by Bias 0.25, and the walkers go down the panel a trap at a time as Kesten, Kozlov and Spitzer's X_t ~ t^kappa. At Bias 0.5 and Disorder 1.2 the median walker on an open line is 100 sites out after 10,000 steps and 4,420 after 10 million. +// Knob 4: Disorder. sigma, the spread of log rho at each site, 1.2 to 2.4. The ridge draws V at 2.6 columns a unit, narrower when it would pass column 52. At 1.2 the median landscape spans 19 units and the walkers' valley reaches its deepest point at log t 5.2; at 2.4 it spans 38 and reaches it at 10.3, and one landscape in seven has its walkers' valley still short of the deepest point when the run stops at the cap, log t 19 (180 million steps). + +const W = 64, H = 128, L = 128, NW = 3000; +const { floor, ceil, min, max, abs, sqrt, exp, log, cos, sin, round, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// log t a second at Speed 2, the longest run, and the hold and fade after it. +const RATE = 1, H_CAP = 19, HOLD = 1.8, FADE = 0.7; +// The ridge puts the lowest V at column X0 and draws V at K_PX columns a +// unit, so a pool rises K_PX columns for each unit of log t; a landscape +// that would run past column X0 + SPAN is drawn narrower to fit. +const X0 = 2, SPAN = 50, K_PX = 2.6; +// Base hues on the knob's wheel: ground, pools, walkers. +const HUE_GROUND = 0.46, HUE_POOL = 0.3, HUE_WALK = 0.13; + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; + return out; +} +// The wheel skips orange through yellow to lime, which reads brown and olive when dim. +const wheel = (h) => 0.28 + 0.74 * (h - floor(h)); + +function normals(n) { + const z = new Float64Array(n); + for (let i = 0; i < n; i += 2) { + const r = sqrt(-2 * log(1 - Math.random())), a = 2 * PI * Math.random(); + z[i] = r * cos(a); + if (i + 1 < n) z[i + 1] = r * sin(a); + } + return z; +} + +// The environment. Site x sends a walker down the panel, to x + 1, with +// chance omega_x and up with 1 - omega_x. log rho_x = log((1 - omega_x) / +// omega_x) = sigma Z_x - mu with Z_x standard normal, so E[rho^k] = +// exp(-mu k + sigma^2 k^2 / 2), which is 1 at k = 2 mu / sigma^2: mu = +// kappa sigma^2 / 2 puts the Kesten-Kozlov-Spitzer exponent on the knob. +// V(x) = sum of log rho up to x is the potential; the walk's resting +// measure is about exp(-V), so walkers gather where V is low. +function environment(Z, sigma, kappa) { + const mu = (kappa * sigma * sigma) / 2, n = Z.length; + const omega = new Float64Array(n), V = new Float64Array(n); + for (let x = 0; x < n; x++) { + const lr = sigma * Z[x] - mu; + omega[x] = 1 / (1 + exp(lr)); + if (x > 0) V[x] = V[x - 1] + lr; + } + return { omega, V }; +} + +// One step of the walk as an L x L matrix; the end rows hold the walker +// where a step would leave the panel. +function transition(omega) { + const P = new Float64Array(L * L); + for (let x = 0; x < L; x++) { + P[x * L + min(L - 1, x + 1)] += omega[x]; + P[x * L + max(0, x - 1)] += 1 - omega[x]; + } + return P; +} +// P squared, inside the band where P can be non-zero. Products under 1e-13 +// are dropped: a row loses less than 1e-10 of its weight. +function square(P, bw) { + const R = new Float64Array(L * L); + for (let i = 0; i < L; i++) { + const oi = i * L; + for (let m = max(0, i - bw), m1 = min(L - 1, i + bw); m <= m1; m++) { + const p = P[oi + m]; + if (p < 1e-13) continue; + const om = m * L; + for (let j = max(0, m - bw), j1 = min(L - 1, m + bw); j <= j1; j++) R[oi + j] += p * P[om + j]; + } + } + return R; +} +function cdf(P) { + const C = new Float32Array(L * L); + for (let i = 0, o = 0; i < L; i++) { + let s = 0; + for (let j = 0; j < L; j++, o++) C[o] = s += P[o]; + } + return C; +} + +// The bottoms of the valleys of depth h: b is one when, walking away from +// it either way, V climbs h above V(b), or meets a wall, before it dips +// below V(b). These are Neveu and Pitman's h-minima; between two of them V +// rises at least h above both. +function bottoms(V, h, out) { + let n = 0; + for (let b = 0; b < L; b++) { + const vb = V[b]; + if ((b > 0 && V[b - 1] < vb) || (b < L - 1 && V[b + 1] < vb)) continue; + let ok = true; + for (let x = b - 1; x >= 0; x--) { + if (V[x] < vb) { ok = false; break; } + if (V[x] >= vb + h) break; + } + if (ok) + for (let x = b + 1; x < L; x++) { + if (V[x] < vb) { ok = false; break; } + if (V[x] >= vb + h) break; + } + if (ok) out[n++] = b; + } + return n; +} +// The bottom of the valley of depth h that holds site s: Sinai's b(log t), +// where his theorem puts the walker at time t. The valley's walls are the +// highest points between its bottom and the next bottoms either side. +function valleyOf(V, h, s, buf) { + const n = bottoms(V, h, buf); + for (let i = 0; i < n; i++) { + let wall = L; + if (i < n - 1) { + wall = buf[i]; + for (let x = buf[i]; x <= buf[i + 1]; x++) if (V[x] > V[wall]) wall = x; + } + if (s <= wall) return buf[i]; + } + return buf[n - 1]; +} + +export function setup(params) { + params.s = { + hue: 0, kappa: -1, sigma: -1, Z: null, buf: new Int16Array(L), buf2: new Int16Array(L), + pos: new Int16Array(NW), u: new Float32Array(NW), + rgb: new Float32Array(W * H * 3), pool: new Float32Array(L), mine: new Uint8Array(L), owner: new Int16Array(L), c: [0, 0, 0], + age: 0, + }; + for (let j = 0; j < NW; j++) params.s.u[j] = Math.random(); +} + +function build(S, kappa, sigma, fresh) { + if (fresh || !S.Z) S.Z = normals(L); + S.kappa = kappa; + S.sigma = sigma; + const { omega, V } = environment(S.Z, sigma, kappa); + S.V = V; + let lo = 0, hi = 0; + for (let x = 1; x < L; x++) { + if (V[x] < V[lo]) lo = x; + if (V[x] > V[hi]) hi = x; + } + S.vmin = V[lo]; + S.scale = min(K_PX, SPAN / max(1e-6, V[hi] - V[lo])); + S.P = transition(omega); + S.bw = 1; + S.C = [cdf(S.P)]; + // Unbiased, the walkers start on the middle row; with a bias, near the top. + S.s0 = round(64 - 60 * min(1, 4 * kappa)); + S.pos.fill(S.s0); + // The depth at which the start's valley first reaches the deepest point + // of the panel. The walkers cross later than that: an escape over a + // barrier of height H takes e^H times a factor from the widths of the + // valley and the barrier, and half of them are across 3.2 units of log t + // after it in the median landscape at sigma 1.2, 1.9 at 2.4. The run + // goes on 4.5 units, a factor of 90 in time. + let a = 0, b = V[hi] - V[lo] + 1; + for (let k = 0; k < 24; k++) { + const m = (a + b) / 2; + if (valleyOf(V, m, S.s0, S.buf) === lo) b = m; + else a = m; + } + S.hEnd = min(H_CAP, max(5, b + 4.5)); + S.h = 0; + S.t = 0; + S.phase = 0; + S.pt = 0; + S.age = 0; +} + +// Every walker takes 2^k steps at once, drawn from row pos of P^(2^k). +function jump(S, k) { + const C = S.C[k], pos = S.pos; + for (let j = 0; j < NW; j++) { + const o = pos[j] * L, u = Math.random() * C[o + L - 1]; + let lo = 0, hi = L - 1; + while (lo < hi) { + const mid = (lo + hi) >> 1; + if (C[o + mid] > u) hi = mid; + else lo = mid + 1; + } + pos[j] = lo; + } +} + +// Bring the walkers to time floor(e^h) - 1 in jumps of powers of two, +// squaring the matrix once more when a longer jump is due. +function advance(S) { + let rem = floor(exp(S.h) - 1) - S.t, squared = 0; + for (let n = 0; n < 6 && rem >= 1; n++) { + let k = floor(Math.log2(rem)); + while (k > S.C.length - 1 && S.C.length < 31 && squared < (S.bw < 32 ? 4 : 1)) { + S.P = square(S.P, S.bw); + S.bw = min(L, 2 * S.bw); + S.C.push(cdf(S.P)); + squared++; + } + k = min(k, S.C.length - 1); + jump(S, k); + S.t += 2 ** k; + rem -= 2 ** k; + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 1.2], d = cl(dt || 0, 0, 0.1); + S.hue = cl(kv[0], 0, 1); + const speed = cl(kv[1], 0.2, 3), kappa = cl(kv[2], 0, 1), sigma = cl(kv[3], 1.2, 2.4); + if (abs(kappa - S.kappa) > 1e-4 || abs(sigma - S.sigma) > 1e-4) build(S, kappa, sigma, false); + const g = (d * speed) / 2; + S.age += g; + if (S.phase === 0) { + S.h = min(S.hEnd, S.h + g * RATE); + advance(S); + if (S.h >= S.hEnd) (S.phase = 1), (S.pt = 0); + } else { + // Settled: the pools stop rising, and each frame every walker takes + // the longest jump made so far, so they keep moving inside their valley. + S.pt += g; + jump(S, S.C.length - 1); + if (S.phase === 1 && S.pt >= HOLD) (S.phase = 2), (S.pt = 0); + else if (S.phase === 2 && S.pt >= FADE) build(S, kappa, sigma, true); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.V) return; + const V = S.V, rgb = S.rgb, pool = S.pool, mine = S.mine, buf = S.buf; + const px = (v) => X0 + (v - S.vmin) * S.scale; + const fade = S.phase === 2 ? 1 - S.pt / FADE : min(1, S.age / 0.5); + rgb.fill(0); + // The pools: each valley of depth h filled from its bottom to V(bottom) + // + h, spreading along the rows while V stays under that level. + mine.fill(0); + const n = bottoms(V, S.h, buf), b0 = valleyOf(V, S.h, S.s0, S.buf2); + // Each valley's ground takes a tint of its own from its bottom, from the + // bottom to the top of its walls, so as log t grows the valleys merge + // and the deeper one's tint spreads over the shallower. + const owner = S.owner; + for (let i = 0, from = 0; i < n; i++) { + const b = buf[i], lv = V[b] + S.h; + let lo = b, hi = b, wall = L - 1; + while (lo > 0 && V[lo - 1] <= lv) lo--; + while (hi < L - 1 && V[hi + 1] <= lv) hi++; + for (let y = lo; y <= hi; y++) (pool[y] = lv), (mine[y] = b === b0 ? 2 : 1); + if (i < n - 1) { + wall = b; + for (let x = b; x <= buf[i + 1]; x++) if (V[x] > V[wall]) wall = x; + } + for (let y = from; y <= wall; y++) owner[y] = b; + from = wall + 1; + } + const ground = S.c; + const ridge = hsv(wheel(HUE_GROUND + S.hue - 0.04), 0.3, 1, [0, 0, 0]); + const water = hsv(wheel(HUE_POOL + S.hue), 0.85, 1, [0, 0, 0]); + const walk = hsv(HUE_WALK + S.hue, 0.5, 1, [0, 0, 0]); + const add = (x, y, col, a) => { + if (x < 0 || x >= W || a <= 0) return; + const o = (y * W + x) * 3; + rgb[o] += col[0] * a; + rgb[o + 1] += col[1] * a; + rgb[o + 2] += col[2] * a; + }; + for (let y = 0; y < H; y++) { + const r = px(V[y]), ri = floor(r), fr = r - ri; + // Ground: lit most near the ridge, fading in to a floor that still + // reads. The valley's tint folds back at the ends of the wheel rather + // than wrapping round, so no valley jumps from rose to lime beside its + // neighbour. + const b = owner[y], base = HUE_GROUND + S.hue; + let tp = base - floor(base) + 0.24 * (((b * 0.618034) % 1) - 0.5); + tp = tp < 0 ? -tp : tp > 0.999 ? 1.998 - tp : tp; + hsv(wheel(tp), 0.75, b === b0 ? 1.2 : 1, ground); + for (let x = 0; x <= ri && x < W; x++) { + const a = (0.15 + 0.3 * exp(-(r - x - 0.5) / 6)) * (x === ri ? fr : 1); + add(x, y, ground, a); + } + // The ridge, joined to the row above so the profile stays one line. + const ra = y > 0 ? px(V[y - 1]) : r; + for (let x = floor(min(r, ra)); x <= floor(max(r, ra)); x++) add(x, y, ridge, x === ri ? 0.85 : 0.55); + if (mine[y]) { + const p = min(W - 0.5, px(pool[y])), m = mine[y] === 2; + for (let x = ri + 1; x < p; x++) add(x, y, water, m ? 0.3 : 0.17); + add(floor(p), y, water, m ? 0.75 : 0.4); + } + } + // The walkers: each one a dot in its row, at its own fixed share of the + // way across the pool and dimmer the further out, so a crowded row glows + // across its whole width. A walker outside every pool, still on its way + // out of a valley Sinai's clock has already drained, gets five columns. + for (let j = 0; j < NW; j++) { + const y = S.pos[j], r = px(V[y]); + const w = mine[y] ? max(2, min(W - 1, px(pool[y])) - r) : 5; + add(floor(r + 1 + S.u[j] * w), y, walk, 0.14 - 0.07 * S.u[j]); + } + // Sinai's b(log t), white at the right edge; the start row, dim. + for (let x = 60; x < W; x++) add(x, S.s0, ridge, 0.3); + for (let x = 58; x < W; x++) add(x, b0, [1, 1, 1], 0.9); + for (let y = 0, o = 0; y < H; y++) + for (let x = 0; x < W; x++, o += 3) + display.setPixel(x, y, (255 * cl(rgb[o] * fade, 0, 1)) | 0, (255 * cl(rgb[o + 1] * fade, 0, 1)) | 0, (255 * cl(rgb[o + 2] * fade, 0, 1)) | 0); +} From 65c9f3c678d7f63ae2b6b75a87e269dc82555bd2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 08:40:27 +0000 Subject: [PATCH 05/10] =?UTF-8?q?57=ED=8C=90:=20Bicycle=20Tracks=20?= =?UTF-8?q?=EC=99=80=20Ostwald=20Ripening=20=EC=9D=84=20=EB=8D=94=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XoJSXx5srrfeJ2DPWFv6K5 --- .../pattern-study/patterns/bicycle-tracks.js | 360 ++++++++++++++++++ .../patterns/ostwald-ripening.js | 323 ++++++++++++++++ 2 files changed, 683 insertions(+) create mode 100644 incoming/pattern-study/patterns/bicycle-tracks.js create mode 100644 incoming/pattern-study/patterns/ostwald-ripening.js diff --git a/incoming/pattern-study/patterns/bicycle-tracks.js b/incoming/pattern-study/patterns/bicycle-tracks.js new file mode 100644 index 000000000..c5ddbae39 --- /dev/null +++ b/incoming/pattern-study/patterns/bicycle-tracks.js @@ -0,0 +1,360 @@ +// ===== Patternflow pattern ===== +// Title: Bicycle Tracks +// Family: line +// Idea: A front point weaves up the tall panel with a rear point a fixed wheelbase behind it that always rolls straight at it, so the rear track cuts inside every turn of the front track, and a comb of segments from the rear track forward to the front one, each one wheelbase long and tangent to the rear track, fills the lunes between them and shows which way the bicycle went; every 34 seconds or so the rider rides two laps of a circle, the rear spirals onto the smaller circle the bicycle's monodromy holds, and it leaves straight up along a tractrix. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the tractrix (Claude Perrault's question, 1670; Christiaan Huygens, 1692) as the track of a bicycle's rear wheel, the tangent rule of "Which Way Did the Bicycle Go?" (Joseph Konhauser, Dan Velleman and Stan Wagon, 1996), and bicycle monodromy as a Möbius map (Robert Foote, 1998; Mark Levi and Serge Tabachnikov, 2009), pattern round 57 +// Round: 57 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Wheelbase=16..24, Bar=50..70 +// +// Knob 1: Hue. Turns both tracks round the wheel together. At 0 the front track is rose and the rear track cyan, with each tangent segment fading from cyan at its rear end to rose at its front end; at 0.25 the front is green and the rear blue, at 0.5 azure and magenta, at 0.75 violet and green. The wheel skips orange and yellow, which read brown and olive on the dim segments. +// Knob 2: Speed. The front point rides 13 LEDs a second times the knob: 26 at 2, where a weave of 37 to 50 rows passes in 2.2 to 3.7 seconds; 2.6 at 0.2 and 39 at 3. After 560 LEDs of weaving, 21.5 seconds at 2, the rider straightens up near one side and rides its loop, 10 to 15 seconds at 2. +// Knob 3: Wheelbase. The fixed distance L between the two points, 16 to 24 LEDs, and the length of every tangent segment. The loop's radius is 1.3 L + 1 up to 28: at 16 the front circles on 21.8 and the rear settles on 14.8, at 24 the front on 28 and the rear on 14.4. A lune is widest where the bar is held, at L tan(bar / 2): 7.5 LEDs at 16 and 11.2 at 24, with the bar at 50 degrees. +// Knob 4: Bar. The angle from the frame the rider holds the bar at through each turn, 50 to 70 degrees. While it is held the front runs on a circle of radius L / sin(bar) and the rear inside it on L / tan(bar), 1.31 L and 0.84 L at 50 degrees, 1.06 L and 0.36 L at 70. A weave (two turns) takes 45 rows at 50 degrees and 23 at 70, with L at 16 (medians over 400 seconds). The rear never rolls slower than cos 70 = 0.34 of the front's speed, so it never stops or backs up. + +// The bicycle: F is the front point, R the rear, L = |F - R|. The rear +// wheel cannot slide sideways, so R always moves along the frame, towards F, +// at the speed of F's motion along the frame: +// dR/ds = ((F - R) . T) (F - R) / L^2, +// with T the front's unit velocity and s its arc. That keeps |F - R| = L +// (the derivative of |D|^2 is 2 (D . T)(1 - |D|^2 / L^2)), so the segment +// from R to F is the tangent to the rear track and meets the front track +// exactly L ahead. Perrault asked in 1670 what curve a watch traces when +// its chain is pulled along a straight line; that curve, the tractrix, is +// this rear track when the front rides a straight line, and Huygens named it +// in 1692. Konhauser, Velleman and Wagon turned the tangent rule round: of +// two tracks, the one whose forward tangents always meet the other at the +// same distance is the rear one, so the tracks say which way the bicycle +// went. When the front rides a closed loop, where the rear ends up after a +// lap is a Möbius map of where it started (Foote, 1998). For a circle of +// radius r > L it has two fixed points; riding forward one attracts, the +// rear circling on radius sqrt(r^2 - L^2) with the frame tangent to it, and +// the rear comes in by a factor exp(-2 pi sqrt(r^2 - L^2) / L) each lap. +// Levi and Tabachnikov (2009) proved Menzin's conjecture of 1906, that the +// same happens on any convex loop holding more area than pi L^2. + +const W = 64, H = 128; +const { floor, min, max, abs, sqrt, sin, cos, atan, atan2, ceil, hypot, round, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const wrap = (a) => a - TAU * round(a / TAU); +// Integration step and record spacing, in LEDs of the front's arc, and a +// tangent segment every COMB records. +const STEP = 0.2, SPACE = 0.5, COMB = 8, RING = 3000; +// While riding the rear sits L + TOP rows from the top, so the front is +// never above row TOP. +const TOP = 8; +// Arc before the opening, before the first loop, and between loops. +const PRE = 320, FIRST = 330, RIDE = 560; +// Loops: two laps and a bit of a circle of radius 1.3 L + 1, at least +// L + 4 and at most RMAX, kept MARGIN inside the panel's sides. +const LAPS = 2, RMAX = 28, MARGIN = 3; +// The rider weaves along a guide line that wanders across the panel, 140 +// rows a wave and 15 LEDs either side: the bar is held at the knob's angle +// to one side until the frame has swung SWING radians past the line to the +// guide, then thrown to the other side over RELAX LEDs. SWING itself +// swells and fades by 35% once every 390 LEDs. +const G_AMP = 15, G_K = TAU / 140, SWING = 0.72, MOD_K = TAU / 390, RELAX = 3; +// Hues at knob 0 before the wheel below bends them: rose and cyan. +const HUE_F = 0.865, HUE_R = 0.297, GLOW = 1; +const wheel = (h) => 0.28 + 0.74 * (h - floor(h)); + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +export function setup(params) { + const R = Math.random; + params.s = { + ready: false, hue: 0, v: 26, L: 16, Lk: 16, bar: 0.87, side: 1, run: 0, + // Guide phases, and the phase of the swing's swell. + g0: R() * TAU, g1: R() * TAU, mph: R() * TAU, + // y: front x, front Y (up), the bar's angle from the frame, rear x, rear Y. + y: new Float64Array(5), t: new Float64Array(5), k: [0, 1, 2, 3].map(() => new Float64Array(5)), + arc: 0, next: 0, sig: 0, + mode: 0, turn: 1, r: 0, cx: 0, cy: 0, b0: 0, len: 0, due: 1e9, wait: 0, fade: 0, edge: 32, blend: 0, + hx: new Float64Array(RING), hy: new Float64Array(RING), gx: new Float64Array(RING), gy: new Float64Array(RING), n: 0, + buf: new Float32Array(W * H * 3), glow: new Float32Array(W * H * 3), tmp2: new Float32Array(W * H * 3), + cf: new Float32Array(3), cr: new Float32Array(3), + }; +} + +const guide = (s, Y) => 32 + G_AMP * (0.7 * sin(G_K * Y + s.g0) + 0.3 * sin(1.73 * G_K * Y + s.g1)); + +// Derivatives along the front's arc. On a loop the front is a given circle +// and only the rear moves by the equation; riding, the front goes where the +// bar points, the bar follows the rider, and the rear by the same equation. +function deriv(s, y, ds, out) { + const L = s.L; + let tx, ty; + if (s.mode === 1) { + const b = s.b0 - (s.turn * (s.sig + ds)) / s.r; + y[0] = s.cx + s.r * cos(b); + y[1] = s.cy + s.r * sin(b); + tx = s.turn * sin(b); + ty = -s.turn * cos(b); + out[0] = out[1] = out[2] = 0; + } else if (s.mode === 2) { + // Off a loop the front rides straight up while the rear swings in + // behind it on a tractrix. + tx = out[2] = out[0] = 0; + ty = out[1] = 1; + } else { + const phi = atan2(y[0] - y[3], y[1] - y[4]), th = phi + y[2]; + tx = sin(th); + ty = cos(th); + // While a loop is due the rider stops weaving and steers by pure + // pursuit for a line near the panel's edge, where the loop starts. + const f = s.fade, ay = y[4] + 1.5 * L + 10, ax = (1 - f) * guide(s, ay) + f * s.edge; + const a = wrap(atan2(ax - y[3], ay - y[4]) - phi); + const nav = cl(atan((2 * L * sin(a)) / hypot(ax - y[3], ay - y[4])), -0.6, 0.6); + const want = (1 - f) * s.side * s.bar + f * nav; + out[0] = tx; + out[1] = ty; + out[2] = (want - y[2]) / RELAX; + } + const dx = y[0] - y[3], dy = y[1] - y[4], p = (dx * tx + dy * ty) / (L * L); + out[3] = p * dx; + out[4] = p * dy; +} + +// One classical Runge-Kutta step of length h along the front's arc. +function rk4(s, h) { + const y = s.y, t = s.t, [a, b, c, d] = s.k; + deriv(s, y, 0, a); + for (let i = 0; i < 5; i++) t[i] = y[i] + 0.5 * h * a[i]; + deriv(s, t, h / 2, b); + for (let i = 0; i < 5; i++) t[i] = y[i] + 0.5 * h * b[i]; + deriv(s, t, h / 2, c); + for (let i = 0; i < 5; i++) t[i] = y[i] + h * c[i]; + deriv(s, t, h, d); + for (let i = 0; i < 5; i++) y[i] += (h / 6) * (a[i] + 2 * b[i] + 2 * c[i] + d[i]); + // On a loop the front is placed on its circle exactly. + if (s.mode === 1) (s.sig += h), deriv(s, y, 0, a); + s.mph = wrap(s.mph + MOD_K * h); + s.arc += h; +} + +const loopR = (L) => min(RMAX, max(L + 4, 1.3 * L + 1)); + +// A loop starts on a circle through the front, tangent to it, that fits +// across the panel; it waits for one of radius loopR, easing by a LED for +// every 40 it waits, but never under L + 3. +function tryLoop(s) { + const y = s.y, th = atan2(y[0] - y[3], y[1] - y[4]) + y[2], c = cos(th), x = y[0], lo = MARGIN, hi = W - MARGIN; + // Turning right the centre is r to the right of the heading, at x + r c. + const right = min((x - lo) / (1 - c + 1e-9), 1 + c > 1e-9 ? (hi - x) / (1 + c) : 1e9); + const left = min(1 + c > 1e-9 ? (x - lo) / (1 + c) : 1e9, (hi - x) / (1 - c + 1e-9)); + const want = loopR(s.L), turn = right >= left ? 1 : -1, r = min(want, max(right, left)); + if (r < max(s.L + 3, want - s.wait / 40)) return; + s.cx = x + turn * r * c; + s.cy = y[1] - turn * r * sin(th); + s.b0 = atan2(y[1] - s.cy, x - s.cx); + s.turn = turn; + s.r = r; + s.sig = 0; + // Whole laps and then on to where the front heads straight up, at the + // circle's side. + const extra = turn > 0 ? -th : th; + s.len = r * (LAPS * TAU + extra - TAU * floor(extra / TAU)); + s.mode = 1; +} + +// Ride arc more LEDs of the front's track, recording both points every +// SPACE LEDs. +function advance(s, arc) { + const end = s.arc + arc, y = s.y; + while (s.arc < end - 1e-9) { + let h = min(STEP, end - s.arc, s.next - s.arc); + if (s.mode === 1) h = min(h, s.len - s.sig); + if (h > 1e-12) rk4(s, h); + if (s.mode === 1) { + if (s.sig >= s.len - 1e-9) (s.mode = 2), (s.run = 0); + } else if (s.mode === 2) { + // Straight on until the frame points within 0.2 radians of up; then + // the bar takes over from there, so the track has no corner. + const phi = atan2(y[0] - y[3], y[1] - y[4]); + s.run += h; + if (abs(phi) < 0.2 || s.run > 4 * s.L) (y[2] = -phi), (s.mode = 0), (s.due = RIDE), (s.wait = 0), (s.side = phi < 0 ? 1 : -1); + } else { + // The weave: throw the bar over once the frame has swung far enough + // past the line to the guide, or the front comes within 6 LEDs of a + // side of the panel. + const phi = atan2(y[0] - y[3], y[1] - y[4]), ay = y[4] + 1.5 * s.L + 10; + const a = wrap(atan2(guide(s, ay) - y[3], ay - y[4]) - phi), sw = SWING * (1 + 0.35 * sin(s.mph)); + if (s.side > 0 && (a < -sw || y[0] > W - 6)) s.side = -1; + else if (s.side < 0 && (a > sw || y[0] < 6)) s.side = 1; + if (s.due <= 0) { + if (s.wait === 0) s.edge = y[0] < 32 ? 32 - loopR(s.L) + 1 : 32 + loopR(s.L) - 1; + s.wait += h; + s.fade = min(1, s.fade + h / 40); + tryLoop(s); + } else (s.due -= h), (s.fade = max(0, s.fade - h / 40)); + } + if (s.arc >= s.next - 1e-9) { + const i = s.n % RING; + s.hx[i] = y[0]; + s.hy[i] = y[1]; + s.gx[i] = y[3]; + s.gy[i] = y[4]; + s.n++; + s.next += SPACE; + } + } +} + +function start(s) { + const y = s.y; + y[0] = y[3] = guide(s, 0); + y[1] = s.L; + y[2] = 0; + y[4] = 0; + s.arc = s.next = 0; + s.due = 1e9; + advance(s, PRE); + s.due = FIRST; + s.ready = true; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const s = params.s, kv = input.knobValues || [0, 2, 16, 50], d = cl(dt || 0, 0, 0.1), y = s.y; + s.hue = cl(kv[0], 0, 1); + s.v = 13 * cl(kv[1], 0.2, 3); + s.bar = (cl(kv[3], 50, 70) * PI) / 180; + // On a loop the wheelbase stays under the loop's radius until it ends. + s.Lk = cl(kv[2], 16, 24); + const L = s.mode === 1 ? min(s.Lk, s.r - 1) : s.Lk; + if (L !== s.L) { + // A new wheelbase: the rear slides along the frame to the new distance. + const dx = y[3] - y[0], dy = y[4] - y[1], m = hypot(dx, dy) || 1; + y[3] = y[0] + (dx * L) / m; + y[4] = y[1] + (dy * L) / m; + s.L = L; + } + if (!s.ready) start(s); + advance(s, s.v * d); + // The view eases onto a loop and back off it over 0.8 seconds. + s.blend = cl(s.blend + (s.mode === 1 ? d : -d) / 0.8, 0, 1); + // Keep the numbers small on a long run. + if (y[4] > 1e5) { + const k = 1e5, m = min(s.n, RING); + y[1] -= k; + y[4] -= k; + s.cy -= k; + s.g0 = wrap(s.g0 + G_K * k); + s.g1 = wrap(s.g1 + 1.73 * G_K * k); + for (let i = 0; i < m; i++) (s.hy[i] -= k), (s.gy[i] -= k); + } +} + +// A box blur of radius 2 along rows (step 1, n of them, len long, stride +// W) or columns (step W, n = W, len = H, stride 1), three channels. +function box(src, dst, step, len, n, stride) { + for (let r = 0; r < n; r++) + for (let e = 0; e < 3; e++) { + const base = r * stride * 3 + e, st = step * 3; + let acc = 0; + for (let j = 0; j < 2 && j < len; j++) acc += src[base + j * st]; + for (let j = 0; j < len; j++) { + if (j + 2 < len) acc += src[base + (j + 2) * st]; + if (j - 3 >= 0) acc -= src[base + (j - 3) * st]; + dst[base + j * st] = acc / 5; + } + } +} + +// A round dot 2.8 LEDs across with a soft edge, kept by the brighter of +// what is there and what lands. +function dot(B, x, y, c, k) { + const x0 = floor(x - 1.4), y0 = floor(y - 1.4); + for (let yy = y0; yy <= y0 + 3; yy++) { + if (yy < 0 || yy >= H) continue; + for (let xx = x0; xx <= x0 + 3; xx++) { + if (xx < 0 || xx >= W) continue; + const u = xx + 0.5 - x, v = yy + 0.5 - y, w = k * cl(1.4 - sqrt(u * u + v * v), 0, 1); + if (w <= 0) continue; + const o = (yy * W + xx) * 3; + for (let e = 0; e < 3; e++) if (c[e] * w > B[o + e]) B[o + e] = c[e] * w; + } + } +} + +// A thin line from a to b, its colour fading from ca to cb, each sample +// laid over its four nearest LEDs. +function line(B, ax, ay, bx, by, ca, cb, k) { + const m = ceil(hypot(bx - ax, by - ay) * 2) || 1; + for (let j = 0; j <= m; j++) { + const u = j / m, x = ax + (bx - ax) * u - 0.5, y = ay + (by - ay) * u - 0.5; + const ix = floor(x), iy = floor(y), tx = x - ix, ty = y - iy; + for (let dy = 0; dy < 2; dy++) { + const yy = iy + dy; + if (yy < 0 || yy >= H) continue; + const wy = dy ? ty : 1 - ty; + for (let dx = 0; dx < 2; dx++) { + const xx = ix + dx; + if (xx < 0 || xx >= W) continue; + const w = min(1, (dx ? tx : 1 - tx) * wy * 2.6) * k, o = (yy * W + xx) * 3; + for (let e = 0; e < 3; e++) { + const v = (ca[e] + (cb[e] - ca[e]) * u) * w; + if (v > B[o + e]) B[o + e] = v; + } + } + } + } +} + +export function draw(display, params) { + const s = params.s; + if (!s || !s.ready) return; + const B = s.buf, G = s.glow, T = s.tmp2, cf = s.cf, cr = s.cr, y = s.y; + B.fill(0); + hsv(wheel(HUE_F + s.hue), 0.9, 1, cf, 0); + hsv(wheel(HUE_R + s.hue), 0.94, 1, cr, 0); + const e = s.blend * s.blend * (3 - 2 * s.blend); + const cam = (1 - e) * (y[4] + s.L + TOP) + e * (s.cy + s.r + 6); + const m = min(s.n, RING), first = s.n - m; + // The two tracks, then a glow round them: the tracks blurred by two + // passes of a 5-LED box each way and added at GLOW. + for (let j = first; j < s.n; j++) { + const i = j % RING, rb = cam - s.gy[i], rf = cam - s.hy[i]; + if (rb > -3 && rb < H + 3) dot(B, s.gx[i], rb, cr, 1); + if (rf > -3 && rf < H + 3) dot(B, s.hx[i], rf, cf, 1); + } + box(B, T, 1, W, H, W); + box(T, G, 1, W, H, W); + box(G, T, W, H, W, 1); + box(T, G, W, H, W, 1); + for (let o = 0; o < W * H * 3; o++) B[o] += GLOW * G[o]; + // The comb over it: the frame every 4 LEDs of the ride, from the rear + // point to the front point, one wheelbase long and tangent to the rear + // track, fading from 0.7 near the top to 0.45 at the bottom. + for (let j = first; j < s.n; j++) { + if (j % COMB) continue; + const i = j % RING, ra = cam - s.gy[i], fa = cam - s.hy[i]; + if ((ra < -2 && fa < -2) || (ra > H + 2 && fa > H + 2)) continue; + line(B, s.gx[i], ra, s.hx[i], fa, cr, cf, 0.7 - 0.25 * cl((ra + fa) / (2 * H), 0, 1)); + } + // On a loop, a dotted ring where the rear settles. + if (e > 0.01 && s.r > s.L) { + const rho = sqrt(s.r * s.r - s.L * s.L), n = floor((TAU * rho) / 3), wh = [0.8, 0.8, 0.85]; + for (let j = 0; j < n; j++) { + const a = (j / n) * TAU; + dot(B, s.cx + rho * cos(a), cam - (s.cy + rho * sin(a)), wh, 0.3 * e); + } + } + // The frame now, white-cored, and the two points. + const wf = [cf[0] * 0.5 + 0.5, cf[1] * 0.5 + 0.5, cf[2] * 0.5 + 0.5], wr = [cr[0] * 0.5 + 0.5, cr[1] * 0.5 + 0.5, cr[2] * 0.5 + 0.5]; + line(B, y[3], cam - y[4], y[0], cam - y[1], wr, wf, 0.9); + dot(B, y[3], cam - y[4], wr, 1.2); + dot(B, y[0], cam - y[1], wf, 1.2); + for (let p = 0, o = 0; p < W * H; p++, o += 3) display.setPixel(p % W, (p / W) | 0, min(255, (B[o] * 255) | 0), min(255, (B[o + 1] * 255) | 0), min(255, (B[o + 2] * 255) | 0)); +} diff --git a/incoming/pattern-study/patterns/ostwald-ripening.js b/incoming/pattern-study/patterns/ostwald-ripening.js new file mode 100644 index 000000000..5fee49bde --- /dev/null +++ b/incoming/pattern-study/patterns/ostwald-ripening.js @@ -0,0 +1,323 @@ +// ===== Patternflow pattern ===== +// Title: Ostwald Ripening +// Family: sim +// Idea: A few hundred droplets sit still in a slightly supersaturated solution and trade matter through it: every drop smaller than the mean radius is magenta and shrinks ever faster until it blinks out, every larger one is green and swells, the solution glows faint magenta round the small drops and green round the large ones while specks of solute drift from the one to the other, and when fewer than a fifth are left a new population condenses in the gaps. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Ostwald ripening (Wilhelm Ostwald, 1896) and the Lifshitz-Slyozov-Wagner theory of coarsening (Ilya Lifshitz and Vitaly Slyozov, 1961; Carl Wagner, 1961), pattern round 57 +// Round: 57 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Drops=200..500, Spread=0..1 +// +// Knob 1: Hue. Turns the colours round the wheel together. At 0 growing drops and the poorer solution round them are green, shrinking drops and the richer solution magenta, and drops within 30% of the mean radius pale towards white; at 0.25 growing is azure and shrinking green, at 0.5 violet and cyan, at 0.75 rose and blue. The wheel skips orange and yellow, which read brown in the faint field. +// Knob 2: Speed. At 2 the kinetics run one time unit a second, K = 0.42 r0^3 for a population that starts at radius r0, and a population ripens down to 18% of its drops in 13 to 19 seconds and is renewed over 1.6 seconds; at 0.2 a population lasts about 3 minutes, at 3 about 12 seconds. +// Knob 3: Drops. How many droplets a new population starts with, 200 to 500, covering 17% of the panel: at 200 they start at radius 1.49 LEDs and the 37 left at the renewal average 2.4 (the largest 3.8); at 500 they start at 0.94 and the 91 left average 1.5 (the largest 2.4). +// Knob 4: Spread. How unequal the starting radii are: r0 (1 + s g), with g a sum of three uniform numbers scaled to deviation 1, s from 0.12 at 0 to 0.45 at 1 (0.14 at the opening), and the radius kept between 0.35 r0 and 2.2 r0. At 0.14 a population of 200 keeps 136 drops after 3 seconds; at 0.45 only 93, as the smallest go at once. + +// Each drop exchanges matter with a mean field. By the Gibbs-Thomson effect +// the solution at a drop's surface is richer the smaller the drop, +// c(r) = c_inf (1 + a / r), and in the quasi-static diffusion field round it +// the excess over the mean c falls off as r / d. A drop gains volume at the +// rate 4 pi D r (c - c(r)), which in reduced units is +// d(r^3)/dt = 3 K (r / r_c - 1), dr/dt = (K / r)(1 / r_c - 1 / r), +// where r_c is the radius in balance with the field. The total volume is +// fixed, so the sum of the rates is zero and r_c is exactly the mean radius. +// Lifshitz and Slyozov, and Wagner, showed that then r_c^3 grows as +// (4/9) K t and the distribution of r / tends to one shape, with its +// peak near 1.13 and nothing past 3/2. Run with a million drops from this +// pattern's own start, ripen() below gave ^3 growing at 0.446 K over the +// time the count fell from 50,000 to 10,000 (R^2 0.9999), with the scaled +// sizes peaking near 1.13 and none past 1.35. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, sqrt, sin, cos, round, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (u) => u * u * (3 - 2 * u); +// Capacity, the share of the panel a new population covers, the rate (K = +// RATE r0^3, with time in seconds at Speed 2), when to renew (the share of +// drops left) and how long the renewal takes, in seconds at Speed 2. +const CAP = 620, AREA = 0.17, RATE = 0.42, END = 0.18, RENEW = 1.6; +// Solute specks: how many at most, how many a second leave the shrinking +// drops, and how far the field reaches round a drop. +const MOTES = 200, EMIT = 110, REACH = 8; +// Hues at knob 0 before the wheel bends them: green and magenta. +const HUE_G = 0.135, HUE_S = 0.81; +const wheel = (h) => 0.28 + 0.74 * (h - floor(h)); + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +export function setup(params) { + params.s = { + ready: false, hue: 0, speed: 2, n0: 200, spread: 0.14, + // Drops: centre, volume r^3, radius, how far a new drop has condensed + // (0..1), and whether it belongs to the population a renewal dissolves. + n: 0, x: new Float32Array(CAP), y: new Float32Array(CAP), v: new Float64Array(CAP), r: new Float32Array(CAP), + grow: new Float32Array(CAP), old: new Uint8Array(CAP), + K: 1, rc: 1, renew: -1, merges: 0, + // Specks of solute. + m: 0, mx: new Float32Array(MOTES), my: new Float32Array(MOTES), ml: new Float32Array(MOTES), emit: 0, + // Pops where drops vanish: x, y, age. + pops: [], + field: new Float32Array(N), owner: new Int16Array(N), buf: new Float32Array(N * 3), + cg: new Float32Array(3), cs: new Float32Array(3), tmp: new Float32Array(3), + }; +} + +// The kinetics, one step of the mean-field equation on the volumes v[0..n): +// r_c is the mean radius, and a drop whose volume reaches zero is gone +// (its slot is set to -1). Returns r_c. +function ripen(v, n, dt, K) { + let sr = 0, live = 0; + for (let i = 0; i < n; i++) if (v[i] > 0) (sr += Math.cbrt(v[i])), live++; + if (!live) return 0; + const rc = sr / live; + for (let i = 0; i < n; i++) { + if (v[i] <= 0) continue; + v[i] += 3 * K * (Math.cbrt(v[i]) / rc - 1) * dt; + if (v[i] <= 0) v[i] = -1; + } + return rc; +} + +// A new population: radii r0 (1 + spread g) with g roughly normal, placed +// at random with a gap of a LED from every drop already there. +function populate(s) { + const n0 = s.n0, r0 = sqrt((AREA * N) / (PI * n0)); + s.K = RATE * r0 * r0 * r0; + const first = s.n; + for (let k = 0; k < n0 && s.n < CAP; k++) { + const g = (Math.random() + Math.random() + Math.random() - 1.5) * 2; + const r = r0 * cl(1 + s.spread * g, 0.35, 2.2); + for (let tries = 0; tries < 40; tries++) { + const x = r + Math.random() * (W - 2 * r), y = r + Math.random() * (H - 2 * r); + let ok = true; + for (let j = 0; j < s.n && ok; j++) { + const dx = s.x[j] - x, dy = s.y[j] - y, need = s.r[j] + r + 1; + if (dx * dx + dy * dy < need * need) ok = false; + } + if (!ok) continue; + const i = s.n++; + s.x[i] = x; + s.y[i] = y; + s.v[i] = r * r * r; + s.r[i] = r; + s.grow[i] = first ? 0 : 1; + s.old[i] = 0; + break; + } + } +} + +function remove(s, i) { + const j = --s.n; + s.x[i] = s.x[j]; + s.y[i] = s.y[j]; + s.v[i] = s.v[j]; + s.r[i] = s.r[j]; + s.grow[i] = s.grow[j]; + s.old[i] = s.old[j]; +} + +// Two drops that touch run together, keeping their volume. +function merge(s) { + for (let i = 0; i < s.n; i++) + for (let j = i + 1; j < s.n; j++) { + const dx = s.x[j] - s.x[i], dy = s.y[j] - s.y[i], need = s.r[i] + s.r[j] + 0.3; + if (dx * dx > need * need || dx * dx + dy * dy >= need * need || s.old[i] || s.old[j]) continue; + const vi = s.v[i], vj = s.v[j], v = vi + vj; + s.x[i] = (s.x[i] * vi + s.x[j] * vj) / v; + s.y[i] = (s.y[i] * vi + s.y[j] * vj) / v; + s.v[i] = v; + s.r[i] = Math.cbrt(v); + remove(s, j); + s.merges++; + j = i; + } +} + +// The field round the drops: each adds (1 - r / r_c) / d outside itself, +// the excess of a drop's surface over the mean carried out as 1 / d, faded +// to nothing REACH LEDs past its edge. Positive near small drops, negative +// near large ones. owner marks the LEDs inside each drop. +function field(s) { + const F = s.field, O = s.owner, rc = s.rc; + F.fill(0); + O.fill(-1); + for (let i = 0; i < s.n; i++) { + const r = s.r[i] * (s.old[i] ? 1 : sm(s.grow[i])), cx = s.x[i], cy = s.y[i]; + if (r <= 0.05) continue; + const e = s.old[i] ? 0 : 1 - s.r[i] / rc, R = r + REACH; + const x0 = max(0, floor(cx - R)), x1 = min(W - 1, floor(cx + R)), y0 = max(0, floor(cy - R)), y1 = min(H - 1, floor(cy + R)); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const dx = x + 0.5 - cx, dy = y + 0.5 - cy, d = sqrt(dx * dx + dy * dy), p = y * W + x; + if (d < r) { + O[p] = i; + continue; + } + if (d >= R) continue; + const t = 1 - (d - r) / REACH; + F[p] += (e / d) * t * t; + } + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const s = params.s, kv = input.knobValues || [0, 2, 200, 0.06], d = cl(dt || 0, 0, 0.1); + s.hue = cl(kv[0], 0, 1); + s.speed = cl(kv[1], 0.2, 3); + s.n0 = round(cl(kv[2], 200, 500)); + s.spread = 0.12 + 0.33 * cl(kv[3], 0, 1); + if (!s.ready) { + populate(s); + s.ready = true; + } + const g = d * s.speed; + if (s.renew >= 0) { + // A renewal: the old drops dissolve while the new ones condense. + s.renew += g / 2 / RENEW; + let sr = 0, k = 0; + for (let i = 0; i < s.n; i++) if (!s.old[i]) (s.grow[i] = min(1, s.renew)), (sr += s.r[i]), k++; + // The new drops are measured against their own mean from the start. + s.rc = sr / max(1, k); + if (s.renew >= 1) { + for (let i = s.n - 1; i >= 0; i--) if (s.old[i]) remove(s, i); + s.renew = -1; + } + } else { + // Ripening, in steps of at most 1/60 of a second. + const steps = Math.ceil(g / 2 / (1 / 60)) || 1, h = g / 2 / steps; + for (let k = 0; k < steps; k++) s.rc = ripen(s.v, s.n, h, s.K) || s.rc; + for (let i = s.n - 1; i >= 0; i--) { + if (s.v[i] < 0) { + s.pops.push([s.x[i], s.y[i], 0]); + remove(s, i); + } else s.r[i] = Math.cbrt(s.v[i]); + } + merge(s); + if (s.n <= max(8, s.n0 * END)) { + for (let i = 0; i < s.n; i++) s.old[i] = 1; + populate(s); + s.renew = 0; + } + } + if (s.renew >= 0) { + // The dissolving drops shrink as the renewal runs. + const f = 1 - sm(min(1, s.renew)); + for (let i = 0; i < s.n; i++) if (s.old[i]) s.r[i] = Math.cbrt(s.v[i]) * f; + } + field(s); + motes(s, d * s.speed / 2); + for (const p of s.pops) p[2] += d * s.speed / 2; + s.pops = s.pops.filter((p) => p[2] < 0.4); +} + +// Specks of solute leave the shrinking drops and wander down the field to +// the growing ones, which take them in. +function motes(s, g) { + const F = s.field, O = s.owner; + s.emit += EMIT * g; + while (s.emit >= 1 && s.renew < 0) { + s.emit -= 1; + if (s.m >= MOTES) continue; + // Pick a shrinking drop, the more often the faster it loses volume. + for (let tries = 0; tries < 12; tries++) { + const i = floor(Math.random() * s.n), out = 1 - s.r[i] / s.rc; + if (out <= 0 || Math.random() > out * 2) continue; + const a = Math.random() * TAU, k = s.m++; + s.mx[k] = s.x[i] + (s.r[i] + 0.7) * cos(a); + s.my[k] = s.y[i] + (s.r[i] + 0.7) * sin(a); + s.ml[k] = 0; + break; + } + } + if (s.renew >= 0) s.emit = 0; + for (let k = s.m - 1; k >= 0; k--) { + let x = s.mx[k], y = s.my[k]; + const xi = cl(floor(x), 1, W - 2), yi = cl(floor(y), 1, H - 2), p = yi * W + xi; + const gx = (F[p + 1] - F[p - 1]) / 2, gy = (F[p + W] - F[p - W]) / 2; + // Down the gradient, at most a LED a frame, and a random walk. + const sp = sqrt(gx * gx + gy * gy) + 1e-9, v = min(30, 600 * sp) * g; + x += (-gx / sp) * v + (Math.random() - 0.5) * 3 * sqrt(g); + y += (-gy / sp) * v + (Math.random() - 0.5) * 3 * sqrt(g); + s.ml[k] += g; + const q = floor(y) * W + floor(x), o = x >= 0 && x < W && y >= 0 && y < H ? O[q] : -2; + if (o === -2 || s.ml[k] > 2.5 || (o >= 0 && s.r[o] > s.rc)) { + const j = --s.m; + s.mx[k] = s.mx[j]; + s.my[k] = s.my[j]; + s.ml[k] = s.ml[j]; + continue; + } + s.mx[k] = x; + s.my[k] = y; + } +} + +function add(B, x, y, c, k) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + B[o] = min(1, B[o] + c[0] * k); + B[o + 1] = min(1, B[o + 1] + c[1] * k); + B[o + 2] = min(1, B[o + 2] + c[2] * k); +} + +export function draw(display, params) { + const s = params.s; + if (!s || !s.ready) return; + const B = s.buf, F = s.field, cg = s.cg, cs = s.cs, c = s.tmp; + hsv(wheel(HUE_G + s.hue), 0.85, 1, cg, 0); + hsv(wheel(HUE_S + s.hue), 0.8, 1, cs, 0); + // The field: magenta where the solution is richer than the mean, green + // where it is poorer, faint. + for (let p = 0, o = 0; p < N; p++, o += 3) { + const f = F[p], a = min(0.45, abs(f) * 3), col = f > 0 ? cs : cg; + B[o] = col[0] * a; + B[o + 1] = col[1] * a; + B[o + 2] = col[2] * a; + } + // The specks, pale in the colour of the field they cross. + const wh = [0.85, 0.85, 0.9]; + for (let k = 0; k < s.m; k++) { + const life = s.ml[k], a = 0.55 * min(1, life * 8) * min(1, (2.5 - life) * 2); + const x = floor(s.mx[k]), y = floor(s.my[k]), col = F[cl(y, 0, H - 1) * W + cl(x, 0, W - 1)] > 0 ? cs : cg; + for (let e = 0; e < 3; e++) c[e] = 0.5 * (wh[e] + col[e]); + add(B, x, y, c, a); + } + // The drops: pale when near the mean radius, green the more they exceed + // it, magenta the more they fall short. + for (let i = 0; i < s.n; i++) { + const r = s.r[i] * (s.old[i] ? 1 : sm(s.grow[i])), cx = s.x[i], cy = s.y[i]; + if (r <= 0.05) continue; + // A drop of the old population dissolving in a renewal is pale. + const q = s.old[i] ? 0 : s.r[i] / s.rc - 1, t = min(1, abs(q) / 0.3), col = q > 0 ? cg : cs; + for (let e = 0; e < 3; e++) c[e] = 0.9 * (wh[e] * (1 - t) + col[e] * t); + const x0 = max(0, floor(cx - r - 1)), x1 = min(W - 1, floor(cx + r + 1)), y0 = max(0, floor(cy - r - 1)), y1 = min(H - 1, floor(cy + r + 1)); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const dx = x + 0.5 - cx, dy = y + 0.5 - cy, a = cl(r + 0.5 - sqrt(dx * dx + dy * dy), 0, 1); + if (a <= 0) continue; + const o = (y * W + x) * 3; + for (let e = 0; e < 3; e++) B[o + e] = B[o + e] * (1 - a) + c[e] * a; + } + } + // A drop that vanishes leaves a ring that opens and fades. + for (const [px, py, age] of s.pops) { + const u = age / 0.4, rr = 0.6 + 2.6 * u, k = 0.8 * (1 - u), m = Math.ceil(TAU * rr * 1.5); + for (let j = 0; j < m; j++) { + const a = (j / m) * TAU; + add(B, floor(px + rr * cos(a)), floor(py + rr * sin(a)), cs, k / 2); + } + } + for (let p = 0, o = 0; p < N; p++, o += 3) display.setPixel(p % W, (p / W) | 0, (B[o] * 255) | 0, (B[o + 1] * 255) | 0, (B[o + 2] * 255) | 0); +} From 10569c466cb2b9ff39548f61a33451c5b3c81303 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 08:41:41 +0000 Subject: [PATCH 06/10] =?UTF-8?q?57=ED=8C=90:=20Galperin=20Billiard=20?= =?UTF-8?q?=EA=B0=80=2031=EB=B2=88=20=EB=B6=80=EB=94=AA=ED=9E=88=EB=8A=94?= =?UTF-8?q?=20=EC=A7=88=EB=9F=89=EB=B9=84=20100=20=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=97=B4=EB=A6=B0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XoJSXx5srrfeJ2DPWFv6K5 --- incoming/pattern-study/patterns/galperin-billiard.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/incoming/pattern-study/patterns/galperin-billiard.js b/incoming/pattern-study/patterns/galperin-billiard.js index ea1b73afc..0cf0d7bde 100644 --- a/incoming/pattern-study/patterns/galperin-billiard.js +++ b/incoming/pattern-study/patterns/galperin-billiard.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Galperin Billiard // Family: physics -// Idea: A heavy block slides down a rail onto a light one above a wall at the bottom, and every collision, block on block or light block on wall, lights one dot of a tally that groups itself in tens, hundreds and thousands, so with masses 100^k apart it reads 3, 31, 314 or 3141; above it the two velocities, scaled by the square roots of the masses, are a point on the energy circle that zigzags from the circle's left end to its right, one chord per collision, sweeping the disc until it lands in the wedge of angle arctan(sqrt(m/M)) where no collision is left. +// Idea: A heavy block slides down a rail onto a light one above a wall at the bottom, and every collision, block on block or light block on wall, lights one dot of a tally that groups itself in tens, hundreds and thousands, so with masses 100, 10,000 or 1,000,000 times apart it reads 31, 314 or 3141; above it the two velocities, scaled by the square roots of the masses, are a point on the energy circle that zigzags from the circle's left end to its right, one chord per collision, sweeping the disc until it lands in the wedge of angle arctan(sqrt(m/M)) where no collision is left. // Author: patternflow-lab // Date: 2026-10-26 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -9,11 +9,11 @@ // Round: 57 // =============================== // @matrix 64x128 -// @knobs Hue=0..1, Speed=0.2..3, Digits=0..3, Warp=0..8 +// @knobs Hue=0..1, Speed=0.2..3, Digits=1..3, Warp=0..8 // // Knob 1: Hue. Turns the colours round the wheel together. At 0 the heavy block is violet and the light one cyan, the swept disc runs violet through blue to green from left to right, and the tally is cyan, its thousands at k = 3 cyan, violet, green and rose; at 0.5 the heavy block is green, the light one pink and the disc runs green through rose to violet. The wheel skips orange to lime, and the unswept disc stays slate blue. // Knob 2: Speed. 2 is real time away from the burst, the heavy block coming down at 60 rows a second; 0.2 is ten times slower and 3 half as fast again. At 2, with Warp at its opening 0.06, the blocks meet 1.4 to 1.7 seconds into a run, the heavy block is off the top about 4 seconds in, and the tally holds 1.8 seconds before the next run. -// Knob 3: Digits. k from 0 to 3: M = 100^k m, so 1, 100, 10,000 or 1,000,000 times the light block, and the collisions number 3, 31, 314 and 3141, the count of steps of theta = arctan(sqrt(m/M)), 45, 5.71, 0.573 or 0.0573 degrees, that fit below pi. The heavy block is 4 + 6k rows tall. The tally's dots are 15, 4, 1 and 1 LEDs wide at a pitch of 16, 5, 2 and 1. At k = 3 the middle 90% of the collisions fall within 3.4 ms and the closest two are 0.27 microseconds apart. +// Knob 3: Digits. k from 1 to 3: M = 100^k m, so 100, 10,000 or 1,000,000 times the light block, and the collisions number 31, 314 and 3141, the count of steps of theta = arctan(sqrt(m/M)), 5.71, 0.573 or 0.0573 degrees, that fit below pi (equal blocks, k = 0, would give 3). The heavy block is 4 + 6k rows tall. The tally's dots are 4, 1 and 1 LEDs wide at a pitch of 5, 2 and 1. At k = 3 the middle 90% of the collisions fall within 3.4 ms and the closest two are 0.27 microseconds apart. // Knob 4: Warp. Seconds of slow motion added to the burst at Speed 2, from 0 to 8, shared evenly so each collision gets Warp / n of them: at the opening 0.06 the burst runs at nearly real time and the middle 90% of k = 3's collisions pass within two frames; at 8 they come 393 a second, and k = 1's 31 come four a second. const W = 64, H = 128; @@ -175,7 +175,7 @@ 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 = cl(kv[0], 0, 1); - const speed = cl(kv[1], 0.2, 3), k = round(cl(kv[2], 0, 3)), wsec = cl(kv[3], 0, 8); + const speed = cl(kv[1], 0.2, 3), k = round(cl(kv[2], 1, 3)), wsec = cl(kv[3], 0, 8); if (k !== S.k) start(S, k, wsec); else if (abs(wsec - S.warpS) > 1e-3) { // A new warp keeps the moment on screen where it is. From 61ae5818c5bb42ddfacd4b3a9a57def247969d42 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 08:48:27 +0000 Subject: [PATCH 07/10] =?UTF-8?q?57=ED=8C=90:=20Solenoid=20=EC=99=80=20Pen?= =?UTF-8?q?tagon=20Tilings=20=EB=A5=BC=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XoJSXx5srrfeJ2DPWFv6K5 --- .../patterns/pentagon-tilings.js | 276 ++++++++++++++++++ incoming/pattern-study/patterns/solenoid.js | 214 ++++++++++++++ 2 files changed, 490 insertions(+) create mode 100644 incoming/pattern-study/patterns/pentagon-tilings.js create mode 100644 incoming/pattern-study/patterns/solenoid.js diff --git a/incoming/pattern-study/patterns/pentagon-tilings.js b/incoming/pattern-study/patterns/pentagon-tilings.js new file mode 100644 index 000000000..a5cc9b5d2 --- /dev/null +++ b/incoming/pattern-study/patterns/pentagon-tilings.js @@ -0,0 +1,276 @@ +// ===== Patternflow pattern ===== +// Title: Pentagon Tilings +// Family: cells +// Idea: The plane tiled by copies of one convex pentagon with thin dark grout, each tile coloured by which way it is turned and whether it is flipped, drifting slowly up the panel and to the left; the knob steps through five of the fifteen kinds, from Reinhardt's type 1, whose pentagon keeps changing shape while it tiles, through the Cairo tiling and Reinhardt's six-fold type 5 to Marjorie Rice's type 13 and the type 15 found in 2015, each new tiling replacing the old one tile by tile. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: tilings by one convex pentagon: types 1, 4 and 5 (Karl Reinhardt, 1918), type 13 (Marjorie Rice, 1976-77), type 15 (Casey Mann, Jennifer McLoud-Mann and David Von Derau, 2015), the list of fifteen shown complete (Michaël Rao, 2017), pattern round 57 +// Round: 57 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Type=0..4, Size=0..1 +// +// Knob 1: Hue. Turns every colour round the wheel together. A tile's colour says how it is turned: at 0 an unturned tile is cyan and the hue moves half way round the wheel as the turn goes from 0 to 360 degrees, so the Cairo tiling's four turns are cyan, azure, violet and magenta; a flipped tile takes a paler tint a little further on. At 0.5 the unturned tiles are red. +// Knob 2: Speed. The tiles drift up and to the left at 1.5 x Speed LEDs a second, 3 at the opening, and type 1 runs round its loop of shapes in 32 / Speed seconds, 16 s at the opening; 0.2 is ten times slower, 3 half as fast again. +// Knob 3: Type. 0 is type 1 (B + C = 180, two tiles a period), its pentagon morphing with B from 70 to 110 degrees; 1 the Cairo tiling, type 4 (b = c, d = e, B = D = 90, four tiles round each right-angled corner); 2 type 5 (a = b, d = e, A = 60, D = 120, six round A); 3 Rice's type 13 (d = 2a = 2e, B = E = 90, 2A + D = 360, eight tiles a period); 4 type 15 (a = c = e, b = 2a, angles 150, 60, 135, 105 and 90, twelve tiles a period). A change sweeps down the panel in 1.1 s, the new tiles taking over one at a time. +// Knob 4: Size. Every tiling is scaled so a tile covers the area of a square 12 LEDs on a side at 0 (13 at the opening) up to 28 at 1: about 50 tiles on the panel at the opening and 10 at the end. + +const W = 64, H = 128; +const { floor, min, max, cos, sin, atan2, hypot, sqrt, PI, round } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const D2R = PI / 180; + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// An isometry [c, s, m, tx, ty] sends p to R(th) diag(1, m) p + t. +const ap = (g, p) => { + const y = g[2] * p[1]; + return [g[0] * p[0] - g[1] * y + g[3], g[1] * p[0] + g[0] * y + g[4]]; +}; +const rot = (th, q) => { + const c = cos(th), s = sin(th); + return [c, s, 1, q[0] - c * q[0] + s * q[1], q[1] - s * q[0] - c * q[1]]; +}; + +// The pentagon from its angles A..E (degrees) and sides, side a = EA, +// b = AB, c = BC, d = CD, e = DE: walk from A along AB and turn by +// 180 - angle at each corner. sides(x, y) gives the five sides with two of +// them unknown; the walk must close, which fixes x and y. +function pentagon(ang, sides) { + const walk = (sd) => { + const v = [[0, 0]]; + let dir = 0, x = 0, y = 0; + for (let k = 1; k <= 5; k++) { + x += sd[k % 5] * cos(dir); + y += sd[k % 5] * sin(dir); + if (k < 5) v.push([x, y]), (dir += (180 - ang[k]) * D2R); + } + return { v, end: [x, y] }; + }; + const g0 = walk(sides(0, 0)).end, gx = walk(sides(1, 0)).end, gy = walk(sides(0, 1)).end; + const a = [gx[0] - g0[0], gx[1] - g0[1]], b = [gy[0] - g0[0], gy[1] - g0[1]], det = a[0] * b[1] - b[0] * a[1]; + return walk(sides((-g0[0] * b[1] + g0[1] * b[0]) / det, (-a[0] * g0[1] + a[1] * g0[0]) / det)).v; +} + +// A copy glued to tile g: the copy's corner b on g's corner a, the copy's +// edge b -> bi along g's edge a -> ai, the copy mirrored when m = -1. +function glue(v, g, a, ai, b, bi, m) { + const V = ap(g, v[a]), U = ap(g, v[ai]); + const th = atan2(U[1] - V[1], U[0] - V[0]) - atan2(m * (v[bi][1] - v[b][1]), v[bi][0] - v[b][0]); + const c = cos(th), s = sin(th), lx = v[b][0], ly = m * v[b][1]; + return [c, s, m, V[0] - (c * lx - s * ly), V[1] - (s * lx + c * ly)]; +} +// A tiling from gluings: G builds the tiles of one period from tile 0, and +// each row of L glues a copy that is a translate of tile k, giving one of +// the two periods. +function glued(v, G, L) { + const isos = [[1, 0, 1, 0, 0]]; + for (const [f, a, ai, b, bi, m] of G) isos.push(glue(v, isos[f], a, ai, b, bi, m)); + const T = L.map(([f, a, ai, b, bi, m, k]) => { + const g = glue(v, isos[f], a, ai, b, bi, m); + return [g[3] - isos[k][3], g[4] - isos[k][4]]; + }); + return { v, isos, T }; +} + +// Type 1, B + C = 180: AB and DC parallel. B = (0, 0), C = (1, 0), the +// sides AB and DC leave at angle be with lengths b and d, and E sits over +// AD at base angles chosen to keep every corner under 165 degrees. The +// shape runs round a loop as w goes from 0 to 2 pi; every member tiles by +// the copy turned half round about the midpoint of BC, the two making a +// hexagon D E A D' E' A' with opposite sides parallel, which tiles by +// translation. +function type1(w) { + const be = (90 + 20 * sin(w)) * D2R, b = 1 + 0.3 * sin(2 * w + 1), d = 1 + 0.3 * sin(w + 2); + const u = [cos(be), sin(be)]; + const A = [b * u[0], b * u[1]], B = [0, 0], C = [1, 0], D = [1 + d * u[0], d * u[1]]; + const corner = (o, p, q) => { + const x = [p[0] - o[0], p[1] - o[1]], y = [q[0] - o[0], q[1] - o[1]]; + return Math.acos((x[0] * y[0] + x[1] * y[1]) / hypot(x[0], x[1]) / hypot(y[0], y[1])); + }; + const gA = (165 * D2R - corner(A, B, D)) * (0.5 + 0.12 * sin(3 * w)); + const gD = (165 * D2R - corner(D, A, C)) * (0.5 - 0.12 * sin(3 * w + 0.7)); + const dir = atan2(D[1] - A[1], D[0] - A[0]); + const r1 = [cos(dir + gA), sin(dir + gA)], r2 = [cos(dir + PI - gD), sin(dir + PI - gD)]; + const t = ((D[0] - A[0]) * r2[1] - (D[1] - A[1]) * r2[0]) / (r1[0] * r2[1] - r1[1] * r2[0]); + const E = [A[0] + t * r1[0], A[1] + t * r1[1]]; + const v = [A, B, C, D, E]; + const r = rot(PI, [0.5, 0]), E2 = ap(r, E), A2 = ap(r, A); + return { v, isos: [[1, 0, 1, 0, 0], r], T: [[D[0] - E2[0], D[1] - E2[1]], [E[0] - A2[0], E[1] - A2[1]]] }; +} + +// Type 4, b = c, d = e, B = D = 90: the Cairo pentagon (angles 120, 90, +// 120, 90, 120, four sides 1 and a = sqrt 3 - 1). Quarter turns about B and +// about D generate it: four tiles round B, and R_D R_B^-1 a translation. +function type4() { + const v = pentagon([120, 90, 120, 90, 120], (x, y) => [y, 1, 1, x, x]), B = v[1], D = v[3]; + const dx = D[0] - B[0], dy = D[1] - B[1]; + return { v, isos: [0, 1, 2, 3].map((k) => rot((k * PI) / 2, B)), T: [[dx + dy, dy - dx], [dx - dy, dx + dy]] }; +} + +// Type 5, a = b, d = e, A = 60, D = 120 (here B = 140, C = 100, E = 120): +// sixth turns about A, third turns about D. +function type5() { + const v = pentagon([60, 140, 100, 120, 120], (x, y) => [1, 1, y, x, x]), D = v[3]; + const c = cos((2 * PI) / 3), s = sin((2 * PI) / 3); + const T1 = [D[0] - (c * D[0] - s * D[1]), D[1] - (s * D[0] + c * D[1])]; + const c6 = cos(PI / 3), s6 = sin(PI / 3); + return { v, isos: [0, 1, 2, 3, 4, 5].map((k) => rot((k * PI) / 3, [0, 0])), T: [T1, [c6 * T1[0] - s6 * T1[1], s6 * T1[0] + c6 * T1[1]]] }; +} + +// Type 13, d = 2a = 2e, B = E = 90, 2A + D = 360 (here A = C = 112, D = +// 136): eight tiles a period, in two orbits of the symmetry group. +function type13() { + const v = pentagon([112, 90, 112, 136, 90], (x, y) => [1, x, y, 2, 1]); + return glued(v, [[0, 2, 3, 3, 2, 1], [0, 3, 4, 2, 3, -1], [0, 0, 4, 3, 4, 1], [0, 1, 2, 1, 2, -1], [1, 0, 4, 3, 4, 1], [2, 3, 4, 0, 4, -1], [3, 1, 2, 1, 2, -1]], [[0, 0, 1, 0, 1, -1, 2], [4, 3, 4, 0, 4, -1, 7]]); +} + +// Type 15, a = c = e = 1, b = 2, d = sqrt 2 / (sqrt 3 - 1), A = 150, B = 60, +// C = 135, D = 105, E = 90: twelve tiles a period, in three orbits of the +// symmetry group. +function type15() { + // The walk is solved for d and e; e comes out 1. + const v = pentagon([150, 60, 135, 105, 90], (x, y) => [1, 2, 1, x, y]); + return glued(v, [[0, 0, 4, 1, 2, 1], [0, 3, 4, 1, 2, -1], [0, 1, 2, 2, 1, 1], [1, 0, 1, 3, 4, -1], [1, 0, 4, 3, 4, 1], [3, 3, 4, 1, 2, -1], [3, 0, 4, 1, 2, 1], [4, 1, 2, 0, 4, -1], [6, 3, 4, 0, 4, -1], [7, 0, 4, 3, 4, 1], [8, 2, 3, 3, 2, -1]], [[0, 0, 1, 0, 1, -1, 2], [9, 1, 2, 0, 4, -1, 11]]); +} + +const GRID = 20; + +// Lookup for one tiling: every tile of a period and its translates by up +// to two periods, as five edge lines each, filed in a GRID x GRID grid over +// the period cell in lattice coordinates. A point is found by reducing it +// into the cell and testing the few tiles filed in its grid square. +function prepare(t) { + const { v, isos, T } = t; + const det = T[0][0] * T[1][1] - T[0][1] * T[1][0]; + let area = 0; + for (let k = 0; k < 5; k++) area += v[k][0] * v[(k + 1) % 5][1] - v[(k + 1) % 5][0] * v[k][1]; + // Aspect: the tile's turn, as a fraction of a whole turn, and its hand. + const asp = isos.map((g) => [((atan2(g[1], g[0]) / (2 * PI) + 1.0001) % 1), g[2]]); + const lines = [], cells = []; + for (let g = 0; g < GRID * GRID; g++) cells.push([]); + isos.forEach((g, k) => { + let p = v.map((q) => ap(g, q)); + if (g[2] < 0) p = p.reverse(); + for (let i = -2; i <= 2; i++) + for (let j = -2; j <= 2; j++) { + const ox = i * T[0][0] + j * T[1][0], oy = i * T[0][1] + j * T[1][1]; + const q = p.map((r) => [r[0] + ox, r[1] + oy]); + let a0 = 9, a1 = -9, b0 = 9, b1 = -9, cx = 0, cy = 0; + for (const [x, y] of q) { + const a = (x * T[1][1] - y * T[1][0]) / det, b = (T[0][0] * y - T[0][1] * x) / det; + (a0 = min(a0, a)), (a1 = max(a1, a)), (b0 = min(b0, b)), (b1 = max(b1, b)), (cx += x / 5), (cy += y / 5); + } + if (a1 < 0 || a0 > 1 || b1 < 0 || b0 > 1) continue; + const id = lines.length; + const L = [k, cx, cy]; + for (let e = 0; e < 5; e++) { + const P = q[e], Q = q[(e + 1) % 5], len = hypot(Q[0] - P[0], Q[1] - P[1]); + // inside when nx x + ny y >= c, the value the distance to the edge + const nx = -(Q[1] - P[1]) / len, ny = (Q[0] - P[0]) / len; + L.push(nx, ny, nx * P[0] + ny * P[1]); + } + lines.push(L); + for (let ga = max(0, floor(a0 * GRID)); ga <= min(GRID - 1, floor(a1 * GRID)); ga++) + for (let gb = max(0, floor(b0 * GRID)); gb <= min(GRID - 1, floor(b1 * GRID)); gb++) cells[ga * GRID + gb].push(id); + } + }); + return { T, det, area: area / 2, asp, lines, cells, n: isos.length }; +} + +const BUILD = [null, type4, type5, type13, type15]; + +export function setup(params) { + params.s = { + hue: 0, speed: 2, size: 12, type: 0, prev: -1, wipe: 1, w: 0, + // Where the middle of the panel sits in each tiling, in its own lattice + // coordinates, so the changing shapes of type 1 stay put round it. + pan: BUILD.map(() => [0.3, 0.6]), + tilings: BUILD.map((f) => (f ? prepare(f()) : null)), + rgb: new Float32Array(3), hit: new Float64Array(4), + }; +} + +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 = cl(kv[0], 0, 1); + s.speed = cl(kv[1], 0.2, 3); + s.size = 12 + 16 * cl(kv[3], 0, 1); + const ty = round(cl(kv[2], 0, 4)); + if (ty !== s.type) (s.prev = s.type), (s.type = ty), (s.wipe = 0); + s.wipe = min(1, s.wipe + d * 0.9); + // Type 1 runs round its loop of shapes once in 32 / Speed seconds. + s.w += d * s.speed * ((2 * PI) / 32); + s.tilings[0] = prepare(type1(s.w)); + // The tiles drift up and to the left at 1.5 LEDs a second per unit of + // Speed. + s.tilings.forEach((t, i) => { + const sc = s.size / sqrt(t.area), vx = (d * s.speed * 1.2) / sc, vy = (-d * s.speed * 0.9) / sc, P = s.pan[i], T = t.T; + P[0] = (P[0] + (vx * T[1][1] - vy * T[1][0]) / t.det + 1) % 1; + P[1] = (P[1] + (T[0][0] * vy - T[0][1] * vx) / t.det + 1) % 1; + }); +} + +// The tile under tiling point (x, y): its period index and aspect, the +// distance to its nearest edge, and its centre. +function locate(t, x, y, out) { + const { T, det, cells, lines } = t; + const a = (x * T[1][1] - y * T[1][0]) / det, b = (T[0][0] * y - T[0][1] * x) / det; + const ia = floor(a), ib = floor(b); + const rx = x - ia * T[0][0] - ib * T[1][0], ry = y - ia * T[0][1] - ib * T[1][1]; + const list = cells[min(GRID - 1, floor((a - ia) * GRID)) * GRID + min(GRID - 1, floor((b - ib) * GRID))]; + let best = -1, bd = -1e9; + for (let n = 0; n < list.length; n++) { + const L = lines[list[n]]; + let dm = 1e9; + for (let e = 3; e < 18; e += 3) dm = min(dm, L[e] * rx + L[e + 1] * ry - L[e + 2]); + if (dm > bd) (bd = dm), (best = list[n]); + } + const L = lines[best]; + out[0] = L[0]; + out[1] = bd; + out[2] = L[1] + ia * T[0][0] + ib * T[1][0]; + out[3] = L[2] + ia * T[0][1] + ib * T[1][1]; +} + +export function draw(display, params) { + const s = params.s; + if (!s) return; + const { rgb, hit } = s; + const cur = s.tilings[s.type], old = s.prev >= 0 && s.wipe < 1 ? s.tilings[s.prev] : null; + // The front of a change of type runs down the panel on a slant; a tile of + // the new tiling shows once its centre is above the front. + const front = -20 + s.wipe * 190; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let t = cur, ti = s.type; + for (let pass = 0; pass < 2; pass++) { + // Each tiling is scaled so a tile covers size^2 LEDs. + const sc = s.size / sqrt(t.area), P = s.pan[ti], T = t.T; + const ox = P[0] * T[0][0] + P[1] * T[1][0], oy = P[0] * T[0][1] + P[1] * T[1][1]; + locate(t, (x - 31.5) / sc + ox, (63.5 - y) / sc + oy, hit); + if (old && t === cur) { + const cx = 31.5 + (hit[2] - ox) * sc, cy = 63.5 - (hit[3] - oy) * sc; + if (cy + 0.35 * cx > front) { + (t = old), (ti = s.prev); + continue; + } + } + const [turn, hand] = t.asp[hit[0]]; + hsv(0.47 + s.hue + 0.5 * turn + (hand > 0 ? 0 : 0.05), hand > 0 ? 0.62 : 0.36, hand > 0 ? 0.95 : 0.85, rgb); + // Grout: the LED darkens within about half an LED of an edge. + const g = cl((hit[1] * sc - 0.3) / 0.7, 0, 1); + const k = g * (0.8 + 0.2 * cl(hit[1] * sc / 4, 0, 1)); + display.setPixel(x, y, (255 * rgb[0] * k) | 0, (255 * rgb[1] * k) | 0, (255 * rgb[2] * k) | 0); + break; + } + } +} diff --git a/incoming/pattern-study/patterns/solenoid.js b/incoming/pattern-study/patterns/solenoid.js new file mode 100644 index 000000000..4c50a32a8 --- /dev/null +++ b/incoming/pattern-study/patterns/solenoid.js @@ -0,0 +1,214 @@ +// ===== Patternflow pattern ===== +// Title: Solenoid +// Family: fractal +// Idea: A solid torus stands on the panel as a dim glass ring and turns about its upright axis every 12 seconds, and inside it runs one closed strand that the Smale-Williams map has wound 16 times round, two bundles of eight that each split in two and again down to pairs, so any slice across the tube cuts it in a Cantor set; each strand wears a dark collar that cuts the strands passing behind it. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Smale-Williams solenoid (Stephen Smale, 1967; Robert F. Williams, 1967), the dyadic solenoid (Leopold Vietoris, 1927; David van Dantzig, 1930), pattern round 57 +// Round: 57 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Level=-3..3, Lambda=0..1 +// +// Knob 1: Hue. Turns the strand colours round the wheel together. At 0 they run from green through cyan to violet-blue: the colour follows the turning of the first term e^(is/2)/2, so where the two bundles of f(T) are green and violet, half a circuit on they are both cyan, and a whole circuit on they have swapped, because the two bundles are one tube going round twice. At 0.5 the same runs from magenta through red to yellow. The body of T stays slate. +// Knob 2: Speed. The ring turns once in 24 / Speed seconds: 12 s at 2, so it goes from face-on (two tall arcs of cable at the sides, black in the hole) to edge-on (one column of cable down the middle) in 3 s. 120 s at 0.2, 8 s at 3. +// Knob 3: Level. The level k is 4 plus the knob, 1 to 7: f^k(T) is one tube wound 2^k times round, so 2 strands at -3 (a double helix), 16 at 0 and 128 at 3. Between whole numbers the newest term of z grows in, so turning the knob up splits every strand in two inside its own tube; the picture follows the knob within a second. +// Knob 4: Lambda. The contraction of f(th, z) = (2 th, lambda z + e^(i th)/2), 0.49 at 0 down to 0.22 at 1 (0.474 at the opening). The Cantor set in a slice has dimension log 2 / log(1/lambda), 0.97 down to 0.46, and the two halves of a finest bundle sit lambda^(k-1) R apart: at level 4, 1.6 LEDs at the opening and 0.16 at 0.22, where each pair fuses into one line. + +const W = 64, H = 128, N = W * H; +const { floor, ceil, min, max, cos, sin, sqrt, PI, round, abs } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The solid torus T = S1 x D2. Its centre line is a stadium in the plane +// of the panel: straight sides of half length HS joined by half circles of +// radius A, run at constant speed. The disc has radius R. +const A = 24, HS = 30, R = 15; +const PER = 4 * HS + TAU * A; +// The disc coordinate z is read in a frame that turns TWIST times round +// the tube per circuit: another way of putting S1 x D2 into space, with +// the same map in (th, z). In space it winds the strands round the tube +// like a cable, so they cross the panel slanted instead of running in +// parallel lines up its sides. +// +// The map is f(th, z) = (2 th, lambda z + e^(i th)/2). It sends the unit +// disc bundle into itself (|f| <= lambda + 1/2 < 1), and the slice of f(T) +// over 2 th is two discs of radius lambda round +-e^(i th)/2, apart while +// lambda < 1/2, so f is one to one. f^k(T) is a tube of radius lambda^k +// round the curve z(s) = 1/2 sum_{m=1..k} lambda^(m-1) e^(i s / 2^m) over +// th = s mod 2 pi, which closes after s = 2 pi 2^k: 2^k turns. +const TWIST = 10; +// Steps of the circle per turn of a strand (under 0.9 LED a step), and the +// dark collar: a strand blacks out what lies more than COLLAR LEDs behind +// it, one LED round, so a strand passing in front reads as in front. +const NT = 1000, COLLAR = 3; +// Brightness of the body of T, the glass the strands lie in. Over black +// the collars would vanish; over the body every strand shows its edge. +const BODY = 0.55; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// Centre point and outward normal at torus angle th. +function centre(th, o, out) { + let u = (th / TAU) * PER; + if (u < 2 * HS) return (out[o] = A), (out[o + 1] = -HS + u), (out[o + 2] = 1), (out[o + 3] = 0); + u -= 2 * HS; + if (u < PI * A) { + const al = u / A; + return (out[o] = A * cos(al)), (out[o + 1] = HS + A * sin(al)), (out[o + 2] = cos(al)), (out[o + 3] = sin(al)); + } + u -= PI * A; + if (u < 2 * HS) return (out[o] = -A), (out[o + 1] = HS - u), (out[o + 2] = -1), (out[o + 3] = 0); + u -= 2 * HS; + const al = PI + u / A; + return (out[o] = A * cos(al)), (out[o + 1] = -HS + A * sin(al)), (out[o + 2] = cos(al)), (out[o + 3] = sin(al)); +} + +export function setup(params) { + // Centre line, normal and the turning frame, per step of th. + const C = new Float64Array(NT * 6); + for (let j = 0; j < NT; j++) { + centre((TAU * j) / NT, j * 6, C); + C[j * 6 + 4] = cos((TWIST * TAU * j) / NT); + C[j * 6 + 5] = sin((TWIST * TAU * j) / NT); + } + params.s = { + C, hue: 0, speed: 2, level: 4, shown: 4, lam: 0.47, phi: -PI / 3, + zb: new Float32Array(N), hb: new Float32Array(N), vb: new Float32Array(N), rgb: new Float32Array(3), + er: new Float64Array(10), ei: new Float64Array(10), sr: new Float64Array(10), si: new Float64Array(10), + }; +} + +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 = cl(kv[0], 0, 1); + s.speed = cl(kv[1], 0.2, 3); + s.level = 4 + cl(kv[2], -3, 3); + s.lam = 0.49 - 0.27 * cl(kv[3], 0, 1); + // The level eases to the knob, so strands split in two or merge. The + // view opens 60 degrees short of face-on and turns about the upright. + s.shown += (s.level - s.shown) * min(1, d * 3); + s.phi += d * s.speed * (TAU / 24); +} + +export function draw(display, params) { + const s = params.s; + if (!s) return; + const { C, zb, hb, vb, rgb, er, ei, sr, si } = s; + zb.fill(-1e9); + // Per LED: the depth that holds it, and what it shows: a strand's hue, + // or -1 a collar, -2 the body, -3 nothing. + hb.fill(-3); + const L = s.shown, k = floor(L + 1e-6), w = L - k, K = w > 1e-3 ? k + 1 : k, M = 1 << K, lam = s.lam; + const cp = cos(s.phi), sp = sin(s.phi), F = 220, Dm = A + R, hue0 = 0.5 + s.hue; + + // The body of T: every LED within R of the projected centre line, since + // the shadow of a tube is the union of the shadows of its spheres. + for (let j = 0; j < NT; j += 15) { + const o = j * 6, x = C[o], y = C[o + 1]; + const Z = -x * sp, f = F / (F - Z), qx = 31.5 + x * cp * f, qy = 63.5 - y * f, rr = R * f; + const y0 = max(0, ceil(qy - rr)), y1 = min(H - 1, floor(qy + rr)); + for (let yy = y0; yy <= y1; yy++) { + const hw = sqrt(max(0, rr * rr - (yy - qy) * (yy - qy))); + const x0 = max(0, ceil(qx - hw)), x1 = min(W - 1, floor(qx + hw)); + for (let xx = x0, p = yy * W + x0; xx <= x1; xx++, p++) (zb[p] = -1e8), (hb[p] = -2); + } + } + + // z(s) = 1/2 sum_{m=1..K} lam^(m-1) e^(i s / 2^m); the last term grows in + // while the level rises from K - 1 to K. + const amp = []; + for (let m = 1, a = 0.5; m <= K; m++, a *= lam) amp.push(m === K && w > 1e-3 ? a * w : a); + // From 64 strands on, every second or fourth step: the strands of a + // bundle lie closer than an LED there and fill each other's gaps. + const step = K > 5 ? 1 << (K - 5) : 1, ds = (TAU * step) / NT; + for (let m = 0; m < K; m++) (sr[m] = cos(ds / (2 << m))), (si[m] = sin(ds / (2 << m))); + + const write = (p, Z, h, v) => { + const zh = Z - COLLAR, px = p % W; + if (px > 0 && zh > zb[p - 1]) (zb[p - 1] = zh), (hb[p - 1] = -1); + if (px < W - 1 && zh > zb[p + 1]) (zb[p + 1] = zh), (hb[p + 1] = -1); + if (p >= W && zh > zb[p - W]) (zb[p - W] = zh), (hb[p - W] = -1); + if (p < N - W && zh > zb[p + W]) (zb[p + W] = zh), (hb[p + W] = -1); + if (Z <= zb[p]) return; + zb[p] = Z; + hb[p] = h; + vb[p] = v; + }; + + // The strand of level K is one closed curve, s from 0 to 2 pi 2^K, over + // the angle th = s mod 2 pi. It is plotted as a thin 8-connected line: a + // pixel that only turns the corner between two diagonal neighbours is + // dropped. + let ap = -1, aZ = 0, ah = 0, av = 0, bp = -1, bZ = 0, bh = 0, bv = 0; + const total = NT * M; + for (let i = 0; i < total; i += step) { + const j = i % NT; + if (j === 0) { + // Exact phases at the start of every turn, so rounding never builds up. + const sv = (TAU * i) / NT; + for (let m = 0; m < K; m++) (er[m] = cos(sv / (2 << m))), (ei[m] = sin(sv / (2 << m))); + } + let zr = 0, zi = 0; + for (let m = 0; m < K; m++) { + zr += amp[m] * er[m]; + zi += amp[m] * ei[m]; + const r2 = er[m] * sr[m] - ei[m] * si[m]; + ei[m] = er[m] * si[m] + ei[m] * sr[m]; + er[m] = r2; + } + const o = j * 6, cq = C[o + 4], sq = C[o + 5]; + const ur = zr * cq - zi * sq, ui = zr * sq + zi * cq; + const x = C[o] + R * ur * C[o + 2], y = C[o + 1] + R * ur * C[o + 3], z = R * ui; + const X = x * cp + z * sp, Z = -x * sp + z * cp; + const f = F / (F - Z); + const px = round(31.5 + X * f), py = round(63.5 - y * f); + if (px < 0 || px >= W || py < 0 || py >= H) { + if (ap >= 0) write(ap, aZ, ah, av); + if (bp >= 0) write(bp, bZ, bh, bv); + ap = bp = -1; + continue; + } + const p = py * W + px; + if (p === bp) { + if (Z > bZ) bZ = Z; + continue; + } + const sv = (TAU * i) / NT; + const h = hue0 + 0.16 * cos(sv / 2) + 0.07 * cos(sv / 4); + const v = cl((Z + Dm) / (2 * Dm), 0, 1); + if (ap >= 0 && bp >= 0) { + const ax = ap % W, ay = (ap / W) | 0, bx = bp % W, by = (bp / W) | 0; + if (abs(ax - px) === 1 && abs(ay - py) === 1 && abs(bx - ax) + abs(by - ay) === 1 && abs(bx - px) + abs(by - py) === 1) { + (bp = p), (bZ = Z), (bh = h), (bv = v); + continue; + } + } + if (ap >= 0) write(ap, aZ, ah, av); + (ap = bp), (aZ = bZ), (ah = bh), (av = bv); + (bp = p), (bZ = Z), (bh = h), (bv = v); + } + if (ap >= 0) write(ap, aZ, ah, av); + if (bp >= 0) write(bp, bZ, bh, bv); + + const body = [0, 0, 0]; + hsv(0.68, 0.4, BODY, body, 0); + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const h = hb[p]; + if (h >= 0) { + // Near strands pale, far ones deep, all at full level. + hsv(h, 0.95 - 0.5 * vb[p], 1, rgb, 0); + display.setPixel(x, y, (255 * rgb[0]) | 0, (255 * rgb[1]) | 0, (255 * rgb[2]) | 0); + } else if (h === -2) display.setPixel(x, y, (255 * body[0]) | 0, (255 * body[1]) | 0, (255 * body[2]) | 0); + else display.setPixel(x, y, 0, 0, 0); + } +} From 27fb2aebe6f87af376137e4807adf48a19804488 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 08:48:52 +0000 Subject: [PATCH 08/10] =?UTF-8?q?57=ED=8C=90:=20Medial=20Axis=20=EC=99=80?= =?UTF-8?q?=20Zagier's=20Windmills=20=EB=A5=BC=20=EB=8D=94=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XoJSXx5srrfeJ2DPWFv6K5 --- .../pattern-study/patterns/medial-axis.js | 319 ++++++++++++++++++ .../pattern-study/patterns/zagier-windmill.js | 277 +++++++++++++++ 2 files changed, 596 insertions(+) create mode 100644 incoming/pattern-study/patterns/medial-axis.js create mode 100644 incoming/pattern-study/patterns/zagier-windmill.js diff --git a/incoming/pattern-study/patterns/medial-axis.js b/incoming/pattern-study/patterns/medial-axis.js new file mode 100644 index 000000000..ddbdd1f25 --- /dev/null +++ b/incoming/pattern-study/patterns/medial-axis.js @@ -0,0 +1,319 @@ +// ===== Patternflow pattern ===== +// Title: Medial Axis +// Family: algo +// Idea: A fire lit along a closed curve that fills the panel burns inward and outward at one speed, its fronts drawn as staggered dashes of the distance to the curve, and every LED takes the colour of the point of the curve nearest it, so the medial axis shows as a pale seam where that point jumps across and the fronts fold into corners: a tall tree that grows a long new branch from every bump a fraction of an LED high. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: medial axis and the grassfire transform (Harry Blum, 1967), pattern round 57 +// Round: 57 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Bumps=0..1, Width=0..1 +// +// Knob 1: Hue. Turns the colours round the wheel together. At 0 an LED is teal where its nearest point is on the right of the curve, blue where it is at the top or bottom and magenta where it is on the left, in 32 steps round the curve; outside the curve the same hues go pale. At 0.5 the right is magenta, the top and bottom yellow and the left teal. +// Knob 2: Speed. The fronts move 1.5 times the knob in LEDs a second: 3 at 2, so a front lit on the curve reaches the deepest point, about 29 LEDs in, after 9.5 seconds; 0.3 at 0.2 and 4.5 at 3. At 2 the blob's five harmonics drift 0.07 to 0.13 radians a second and the bumps travel round the curve 1 to 3.6 LEDs a second; both scale with the knob. +// Knob 3: Bumps. Adds ripples of 11, 14, 17, 21, 25 and 29 waves round the curve, 1.6 down to 0.7 LEDs high, one more each sixth of the knob. At 0 the axis is a trunk with a fork, 3.5 ends inside the curve on average. Ripples a fifth of an LED high (0.02) give it 8 ends, half an LED (0.05) 15 and the whole first ripple (0.17) 26, each new branch running from its ripple deep into the curve to the trunk; past that the count stays at 21 to 24 while the branches crowd and the pale seams grow from 5% of the inside to 16%. +// Knob 4: Width. The distance between fronts, 6.2 + 8 x knob LEDs: 6.7 at the opening 0.06, 14.2 at 1. The dashes stay about 12 LEDs long along the curve, so wide fronts break into squarer blocks. + +const W = 64, H = 128, NP = W * H; +const { floor, min, max, abs, sqrt, cos, sin, PI, round } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// Vertices of the curve. +const N = 384; +// The blob: harmonic k of the radius, its amplitude and its drift in +// radians a second at Speed 2. +const LOW = [[2, 0.09, 0.13], [3, 0.08, -0.09], [4, 0.07, 0.07], [5, 0.05, -0.11], [6, 0.035, 0.1]]; +// The bumps, in LEDs along the outward ray, k waves round the curve. Bumps +// brings them in one after another. +const BUMPS = [[11, 1.6, 0.21], [14, 1.3, -0.17], [17, 1.1, 0.13], [21, 0.9, -0.23], [25, 0.8, 0.19], [29, 0.7, -0.15]]; +// The margin left between the blob and the panel's edges. +const M = 2; +// Length of a dash along the curve, in LEDs. +const DASH = 12; +// The sawtooth's height: each front is 255 on its bright side. +const A = 255; + +// Vertex angles spaced evenly by arc length round a 29 x 61 ellipse. +const TH = new Float32Array(N), CO = new Float32Array(N), SI = new Float32Array(N); +{ + const K = 4096, acc = new Float64Array(K + 1); + for (let k = 0; k < K; k++) { + const a = (k + 0.5) / K * TAU; + acc[k + 1] = acc[k] + Math.hypot(29 * sin(a), 61 * cos(a)); + } + for (let i = 0, k = 0; i < N; i++) { + const target = (i / N) * acc[K]; + while (acc[k + 1] < target) k++; + TH[i] = ((k + (target - acc[k]) / (acc[k + 1] - acc[k])) / K) * TAU; + CO[i] = cos(TH[i]); + SI[i] = sin(TH[i]); + } +} + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +export function setup(params) { + params.s = { + clock: 0, ph: 0, hue: 0, speed: 2, bumps: 0, width: 5, + px: new Float32Array(N), py: new Float32Array(N), + // Per segment: start, direction, 1 / length squared, length, arc start. + ax: new Float32Array(N), ay: new Float32Array(N), dx: new Float32Array(N), dy: new Float32Array(N), + il: new Float32Array(N), len: new Float32Array(N), arc: new Float32Array(N), per: 1, + F: new Int16Array(NP), D2: new Float32Array(NP), T: new Float32Array(NP), U: new Float32Array(NP), + G: new Float32Array(NP), IN: new Uint8Array(NP), + DIS: new Float32Array(NP), SH: new Float32Array(NP), HT: new Float32Array(NP), + CR: new Float32Array(NP), CG: new Float32Array(NP), CB: new Float32Array(NP), frame: 0, ready: false, + xs: new Float32Array(H * 32), xc: new Int32Array(H), + pal: new Float32Array(256 * 6), palHue: -1, + }; +} + +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 = cl(kv[0], 0, 1); + s.speed = cl(kv[1], 0.2, 3); + s.bumps = cl(kv[2], 0, 1); + s.width = 6.2 + 8 * cl(kv[3], 0, 1); + s.clock += d * s.speed / 2; + // The fronts move 1.5 x Speed LEDs a second. + s.ph += (d * 1.5 * s.speed) / s.width; + s.ph -= floor(s.ph); +} + +// The curve at this moment: the blob scaled to the panel less the margin, +// then the bumps pushed out along the ray from its centre. +function curve(s) { + const { px, py } = s, t = s.clock; + let x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9; + for (let i = 0; i < N; i++) { + let r = 1; + for (const [k, a, w] of LOW) r += a * cos(k * TH[i] + w * t + k); + px[i] = r * CO[i]; + py[i] = r * SI[i]; + x0 = min(x0, px[i]); x1 = max(x1, px[i]); y0 = min(y0, py[i]); y1 = max(y1, py[i]); + } + const sx = (W - 1 - 2 * M) / (x1 - x0), sy = (H - 1 - 2 * M) / (y1 - y0), cx = M - x0 * sx, cy = M - y0 * sy; + const nb = s.bumps * BUMPS.length; + for (let i = 0; i < N; i++) { + const X = px[i] * sx, Y = py[i] * sy, rho = sqrt(X * X + Y * Y); + let b = 0; + for (let j = 0; j < BUMPS.length && j < nb; j++) { + const [k, a, w] = BUMPS[j]; + b += min(1, nb - j) * a * cos((k * TAU * i) / N + w * t * 4 + j * 2.1); + } + const f = 1 + b / rho; + px[i] = cx + X * f; + py[i] = cy + Y * f; + } + let arc = 0; + for (let i = 0; i < N; i++) { + const j = i + 1 === N ? 0 : i + 1, dx = px[j] - px[i], dy = py[j] - py[i], l2 = dx * dx + dy * dy; + s.ax[i] = px[i]; s.ay[i] = py[i]; s.dx[i] = dx; s.dy[i] = dy; + s.il[i] = l2 > 0 ? 1 / l2 : 0; + s.len[i] = sqrt(l2); + s.arc[i] = arc; + arc += s.len[i]; + } + s.per = arc; +} + +// Inside or out, row by row: where the curve's edges cross each row's +// centre line, sorted, filled between pairs. +function fill(s) { + const { xs, xc, IN, px, py } = s; + xc.fill(0); + IN.fill(0); + for (let i = 0; i < N; i++) { + const j = i + 1 === N ? 0 : i + 1, ya = py[i], yb = py[j]; + if (ya === yb) continue; + const lo = min(ya, yb), hi = max(ya, yb); + for (let y = max(0, Math.ceil(lo)); y < hi && y < H; y++) { + if (xc[y] < 32) xs[y * 32 + xc[y]++] = px[i] + ((y - ya) * (px[j] - px[i])) / (yb - ya); + } + } + for (let y = 0; y < H; y++) { + const n = xc[y], o = y * 32; + for (let a = 1; a < n; a++) { + const v = xs[o + a]; + let b = a - 1; + while (b >= 0 && xs[o + b] > v) (xs[o + b + 1] = xs[o + b]), b--; + xs[o + b + 1] = v; + } + for (let a = 0; a + 1 < n; a += 2) { + for (let x = max(0, Math.ceil(xs[o + a])); x < W && x < xs[o + a + 1]; x++) IN[y * W + x] = 1; + } + } +} + +// The feature transform: for every LED the nearest segment of the curve, +// the squared distance to it and where on it the nearest point lies. The +// arrays sit in module variables so the two small functions below inline. +let AX, AY, DX, DY, IL, FF, DD, TT, tt = 0; +function sd(i, x, y) { + const ex = x - AX[i], ey = y - AY[i]; + let t = (ex * DX[i] + ey * DY[i]) * IL[i]; + t = t < 0 ? 0 : t > 1 ? 1 : t; + tt = t; + const fx = ex - t * DX[i], fy = ey - t * DY[i]; + return fx * fx + fy * fy; +} +// Try segment c for LED p, then walk along the curve while it gets nearer. +// A segment within two of the one held is skipped: the one held is already +// the nearest of its neighbours. +function relax(p, x, y, c) { + if (c < 0) return; + const e = FF[p]; + if (e >= 0) { + const k = c > e ? c - e : e - c; + if (k <= 2 || k >= N - 2) return; + } + let bd = sd(c, x, y); + if (bd >= DD[p]) return; + let bt = tt; + for (;;) { + const j = c + 1 === N ? 0 : c + 1, d = sd(j, x, y); + if (d < bd) (bd = d), (bt = tt), (c = j); + else break; + } + for (;;) { + const j = c === 0 ? N - 1 : c - 1, d = sd(j, x, y); + if (d < bd) (bd = d), (bt = tt), (c = j); + else break; + } + FF[p] = c; + DD[p] = bd; + TT[p] = bt; +} +function transform(s) { + (AX = s.ax), (AY = s.ay), (DX = s.dx), (DY = s.dy), (IL = s.il), (FF = s.F), (DD = s.D2), (TT = s.T); + const F = FF, D2 = DD; + F.fill(-1); + D2.fill(1e9); + // Seeds: the LED nearest each vertex takes that vertex's segment, walked + // to the nearest segment around it. + for (let i = 0; i < N; i++) { + const x = round(AX[i]), y = round(AY[i]); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + relax(y * W + x, x, y, i); + } + // Two raster passes carry the nearest segment from neighbour to neighbour. + for (let y = 0; y < H; y++) { + for (let x = 0, p = y * W; x < W; x++, p++) { + if (x > 0) relax(p, x, y, F[p - 1]); + if (y > 0) { + relax(p, x, y, F[p - W]); + if (x > 0) relax(p, x, y, F[p - W - 1]); + if (x < W - 1) relax(p, x, y, F[p - W + 1]); + } + } + for (let x = W - 2, p = y * W + x; x >= 0; x--, p--) relax(p, x, y, F[p + 1]); + } + for (let y = H - 1; y >= 0; y--) { + for (let x = W - 1, p = y * W + x; x >= 0; x--, p--) { + if (x < W - 1) relax(p, x, y, F[p + 1]); + if (y < H - 1) { + relax(p, x, y, F[p + W]); + if (x < W - 1) relax(p, x, y, F[p + W + 1]); + if (x > 0) relax(p, x, y, F[p + W - 1]); + } + } + for (let x = 1, p = y * W + 1; x < W; x++, p++) relax(p, x, y, F[p - 1]); + } +} + +// Everything that depends on the curve and not on the phase of the fire: +// the distance of each LED, its dash, its screen offset and its colour. +// +// The dashes and the screen are there for the study map. Bands of the +// distance rise along the normal and stay flat along the front, so where +// the curve runs up the panel only the steps along a row change. With every +// rise kept under 40 levels, the mean step between neighbours stayed at +// 0.151 of full scale with bands alone, and no height or width of band took +// it past 0.16. The half-band jumps between dashes and the screen's steps +// along the fronts give the columns their share too: 0.203. +function field(s) { + curve(s); + fill(s); + transform(s); + const { F, D2, T, U, G, IN, arc, len, pal, ax, ay, dx, dy, DIS, SH, HT, CR, CG, CB } = s, per = s.per, half = per / 2; + for (let p = 0; p < NP; p++) U[p] = arc[F[p]] + T[p] * len[F[p]]; + // How far apart along the curve the nearest points of two neighbours + // lie: a few LEDs off the medial axis, up to half the curve across it. + G.fill(0); + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + if (x < W - 1) { + let g = abs(U[p] - U[p + 1]); + if (g > half) g = per - g; + if (g > G[p]) G[p] = g; + if (g > G[p + 1]) G[p + 1] = g; + } + if (y < H - 1) { + let g = abs(U[p] - U[p + W]); + if (g > half) g = per - g; + if (g > G[p]) G[p] = g; + if (g > G[p + W]) G[p + W] = g; + } + } + if (s.palHue !== s.hue) { + // Round the curve the hue runs from teal on the right through blue at + // the top and bottom to magenta on the left; outside it the same hues + // go pale. + for (let i = 0; i < 256; i++) { + const tri = 1 - abs(1 - 2 * (i / 256)); + hsv(0.42 + s.hue + 0.5 * tri, 0.85, 1, pal, i * 6); + hsv(0.42 + s.hue + 0.5 * tri, 0.38, 1, pal, i * 6 + 3); + } + s.palHue = s.hue; + } + const rise = A / s.width, nd = 2 * max(2, round(per / (2 * DASH))), dl = per / nd; + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const i = F[p], d = sqrt(D2[p]), u = U[p]; + DIS[p] = d; + SH[p] = 0.5 * (floor(u / dl) & 1); + // The screen: rows, or columns where the normal is nearer vertical, + // alternate up and down by as much as the step to the neighbour across + // leaves under 38 levels. It needs the direction to the nearest point, + // or across the segment for an LED on the curve. + let gx, gy, gn; + if (d > 0.3) (gx = abs(x - ax[i] - T[p] * dx[i])), (gy = abs(y - ay[i] - T[p] * dy[i])), (gn = d); + else (gx = abs(dy[i])), (gy = abs(dx[i])), (gn = len[i] || 1); + const row = gx >= gy, del = (38 - (rise * (row ? gy : gx)) / gn) / 2; + HT[p] = del > 0 ? ((row ? y : x) & 1 ? del : -del) : 0; + // The hue steps 32 times round the curve. + const o = ((floor((u / per) * 32) & 31) << 3) * 6 + (IN[p] ? 0 : 3); + const wh = cl((G[p] - 3) / 9, 0, 1) * 0.8; + CR[p] = pal[o] + (1 - pal[o]) * wh; + CG[p] = pal[o + 1] + (1 - pal[o + 1]) * wh; + CB[p] = pal[o + 2] + (1 - pal[o + 2]) * wh; + } +} + +export function draw(display, params) { + const s = params.s; + if (!s) return; + // The curve moves a small fraction of an LED a frame, so its field is + // worked out every other frame; the fire moves every frame. + if (!s.ready || (s.frame++ & 1) === 0) field(s), (s.ready = true); + const { DIS, SH, HT, CR, CG, CB } = s, w = s.width, ph = s.ph; + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + // A sawtooth of the distance: each front bright on its trailing side, + // dark ahead of it, the brightness fading back towards the curve. + // Every other dash runs half a band behind. + const f = DIS[p] / w - ph + SH[p]; + let lv = A * (f - floor(f)) + HT[p]; + lv = lv < 0 ? 0 : lv > 255 ? 255 : lv; + display.setPixel(x, y, round(CR[p] * lv), round(CG[p] * lv), round(CB[p] * lv)); + } +} diff --git a/incoming/pattern-study/patterns/zagier-windmill.js b/incoming/pattern-study/patterns/zagier-windmill.js new file mode 100644 index 000000000..2ff60918c --- /dev/null +++ b/incoming/pattern-study/patterns/zagier-windmill.js @@ -0,0 +1,277 @@ +// ===== Patternflow pattern ===== +// Title: Zagier's Windmills +// Family: number +// Idea: A prime p one more than a multiple of four is drawn as a windmill of p cells, a central square with four rectangular sails, and Zagier's involution and the swap of the sails take turns redrawing it: Zagier's map keeps the windmill's outline and cuts a new central square from it, the swap turns every sail on its side, and the walk from Zagier's one fixed point ends at a windmill with square sails that slide together into the two squares whose areas add up to p, while below the plane of sail sizes y and z plots every windmill of p on its hyperbola and the zigzag path the walk took. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Fermat's theorem on sums of two squares (Pierre de Fermat, 1640; Leonhard Euler, 1749), Zagier's one-sentence proof (Don Zagier, 1990, after Roger Heath-Brown, 1984), pattern round 57 +// Round: 57 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Prime=1..13, Zoom=0..1 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the central square is pale gold and the sails on its top, right, bottom and left are green, cyan, blue and violet; in the plot the hyperbolas and Zagier's steps are gold and the swaps pale blue. At 0.5 the square is pale blue and the sails pink, red, orange and lime. +// Knob 2: Speed. At 2 each step of the walk takes 1 second and holds half a second; the first windmill grows in 0.8 s and the end, a pause, the slide into two squares, the show and the fade, takes 4.4 s. So 13 takes 8.2 s, 97 with its walk of 26 steps 44 s, and the 14 primes 5.1 minutes. 0.2 is ten times slower, 3 half as fast again. +// Knob 3: Prime. Which prime the cycle opens with: 1 is 13, 2 is 17, and so on to 13, which is 113. After 113 the cycle comes round to 5, whose windmill (1, 1, 1) is a plus that both maps leave alone, then 13 again. The walks are 2, 4, 4, 6, 10, 6, 10, 20, 20, 26, 10, 16 and 22 steps long, and for each of these primes the walk passes through every windmill of p, 3 for 13 and 23 for 113. +// Knob 4: Zoom. 1 to 2.5 times the size that fits the windmill into the top 64 rows, which is 8 LEDs a cell for small windmills and 1 LED for the plus of 113, 57 cells across; at most 14 LEDs a cell. Turned up, the central square and the roots of the sails fill the top, where Zagier's map makes its cut, and the long sails run off the edges. + +const W = 64, H = 128; +const { floor, min, max, abs, cos, sin, atan2, round, log, PI, SQRT2 } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ease = (t) => (t <= 0 ? 0 : t >= 1 ? 1 : t * t * (3 - 2 * t)); +// The primes one more than a multiple of four whose first windmill, a plus +// sign (p + 1) / 2 cells across, fits the 64 columns. +const PR = [5, 13, 17, 29, 37, 41, 53, 61, 73, 89, 97, 101, 109, 113]; +// Seconds at Speed 2: the first windmill grows, each step of the walk +// moves and holds, then the pause, the slide into two squares, the show +// and the fade. +const GROW = 0.8, STEP = 1.0, HOLD = 0.5, PAUSE = 0.4, SLIDE = 1.4, SHOW = 2.0, FADE = 0.6; +// The plot of sail sizes: y across from column 3, z up from row 126. +const PX = 3, PY = 126, PS = 58; + +// Zagier's involution on x^2 + 4yz = p, and which of its three cases ran. +function zag(x, y, z) { + if (x < y - z) return [x + 2 * z, z, y - x - z, 1]; + if (x < 2 * y) return [2 * y - x, y, x - y + z, 2]; + return [x - 2 * y, x - y + z, y, 3]; +} + +// The walk for p: from Zagier's fixed point (1, 1, (p - 1) / 4), swap and +// Zagier's map in turn until the swap would fix the windmill (y = z). Each +// windmill keeps a turning sense: Zagier's middle case keeps the outline of +// the mirror image, so it flips the sense, and the outline then stays put. +function plan(p) { + const k = (p - 1) / 4, path = [[1, 1, k, 1]]; + for (let t = path[0]; t[1] !== t[2]; ) { + t = [t[0], t[2], t[1], t[3]]; + path.push(t); + if (t[1] === t[2]) break; + const z = zag(t[0], t[1], t[2]); + t = [z[0], z[1], z[2], z[3] === 2 ? -t[3] : t[3]]; + path.push(t); + } + const S = []; + for (let x = 1; x * x < p; x += 2) { + const m = (p - x * x) / 4; + for (let y = 1; y <= m; y++) if (m % y === 0) S.push([x, y, m / y]); + } + const n = path.length - 1, end = GROW + n * (STEP + HOLD); + const lm = []; + for (let x = 1; x * x < p; x += 2) lm.push(log((p - x * x) / 4)); + return { p, k, path, S, n, lm, end, total: end + PAUSE + SLIDE + SHOW + FADE }; +} + +// The five pieces of a windmill, each as a rectangle (centre u, centre v, +// half width, half height, turn) in cells about the centre of its square: +// the square, then the sails on its top, right, bottom and left. Each sail +// is y cells along its side, starting from a corner, and z cells out. A +// turning sense of -1 mirrors the windmill. +function mill(x, y, z, h, out) { + const c = x / 2; + const R = [[-c, c, -c, c], [-c, -c + y, c, c + z], [c, c + z, c - y, c], [c - y, c, -c - z, -c], [-c - z, -c, -c, -c + y]]; + for (let i = 0; i < 5; i++) { + const r = R[i], o = i * 5; + out[o] = (h * (r[0] + r[1])) / 2; + out[o + 1] = (r[2] + r[3]) / 2; + out[o + 2] = (r[1] - r[0]) / 2; + out[o + 3] = (r[3] - r[2]) / 2; + out[o + 4] = 0; + } + return out; +} +// The windmill with square sails as the squares x^2 and (2y)^2, side by +// side and one cell apart, its four sails the quarters of the second. +function squares(x, y, out) { + const c = x / 2, L = -c - y, B = -c - floor((max(x, 2 * y) - x) / 2), R = L + x + 1; + const Q = [[L + c, B + c, c], [R + y / 2, B + 1.5 * y, y / 2], [R + 1.5 * y, B + 1.5 * y, y / 2], [R + 1.5 * y, B + y / 2, y / 2], [R + y / 2, B + y / 2, y / 2]]; + for (let i = 0; i < 5; i++) { + const o = i * 5; + (out[o] = Q[i][0]), (out[o + 1] = Q[i][1]), (out[o + 2] = out[o + 3] = Q[i][2]), (out[o + 4] = 0); + } + return out; +} +const span = (t) => max(t[0] + 2 * t[2], 2 * t[1] - t[0]); + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +export function setup(params) { + params.s = { + t: 0, clock: 0, idx: -1, first: -1, hue: -1, rate: 1, zoom: 1, P: null, + ra: new Float32Array(25), rb: new Float32Array(25), pal: new Float32Array(24), + buf: new Float32Array(W * H * 3), + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const s = params.s, kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + const first = round(cl(kv[2], 1, 13)); + s.rate = cl(kv[1], 0.2, 3) / 2; + s.zoom = 1 + 1.5 * cl(kv[3], 0, 1); + const hue = cl(kv[0], 0, 1); + if (hue !== s.hue) { + // The square pale gold, the sails green, cyan, blue and violet. + s.hue = hue; + hsv(0.13 + hue, 0.3, 1, s.pal, 0); + [0.36, 0.5, 0.63, 0.79].forEach((h, i) => hsv(h + hue, 0.78, 1, s.pal, 3 + i * 3)); + hsv(0.6 + hue, 0.25, 1, s.pal, 15); + hsv(0.12 + hue, 0.55, 1, s.pal, 18); + } + if (first !== s.first) (s.first = first), (s.idx = first), (s.P = plan(PR[first])), (s.t = 0); + s.t += d * s.rate; + s.clock += d * s.rate; + if (s.t >= s.P.total) (s.t -= s.P.total), (s.idx = (s.idx + 1) % PR.length), (s.P = plan(PR[s.idx])); +} + +export function draw(display, params) { + const s = params.s; + if (!s || !s.P) return; + const P = s.P, B = s.buf, pal = s.pal, t = s.t; + B.fill(0); + const add = (x, y, r, g, b) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + B[o] = max(B[o], r); + B[o + 1] = max(B[o + 1], g); + B[o + 2] = max(B[o + 2], b); + }; + // Where the walk is: step i moving with progress e, or the end. + let i = 0, e = 0, grow = 1, slide = 0, fade = 1; + if (t < GROW) grow = ease(t / GROW); + else if (t < P.end) { + const u = (t - GROW) / (STEP + HOLD); + i = floor(u) + 1; + e = ease(((u - floor(u)) * (STEP + HOLD)) / STEP); + } else { + i = P.n; + slide = ease((t - P.end - PAUSE) / SLIDE); + fade = 1 - ease((t - P.end - PAUSE - SLIDE - SHOW) / FADE); + } + const a = P.path[max(0, i - 1)], b = P.path[i]; + const ra = s.ra, rb = s.rb; + // The pieces this frame: ra, and for a step of Zagier's map the new + // pieces rb, cut in as a square that spreads out from the centre. + let cut = -1, fit; + if (t < GROW) mill(1, 1, P.k * grow, 1, ra), (fit = span(b)); + else if (t < P.end && i % 2 === 1) { + // The swap: every sail turns a quarter about its middle, the way the + // windmill turns, while its middle slides to where the turned sail + // belongs: y cells along and z out become z along and y out. + mill(a[0], a[1], a[2], a[3], ra); + mill(b[0], b[1], b[2], b[3], rb); + for (let j = 5; j < 25; j += 5) { + ra[j] += (rb[j] - ra[j]) * e; + ra[j + 1] += (rb[j + 1] - ra[j + 1]) * e; + ra[j + 4] = (e * PI * a[3]) / 2; + } + fit = span(a) + (span(b) - span(a)) * e; + } else if (t < P.end) { + mill(a[0], a[1], a[2], a[3], ra); + mill(b[0], b[1], b[2], b[3], rb); + cut = e * (span(a) / 2 + 1); + fit = span(a) + (span(b) - span(a)) * e; + } else { + mill(b[0], b[1], b[2], b[3], ra); + squares(b[0], b[1], rb); + for (let j = 0; j < 25; j++) ra[j] += (rb[j] - ra[j]) * slide; + fit = span(b) + (b[0] + 1 + 2 * b[1] - span(b)) * slide; + } + const cs = min(14, min(8, 58 / fit) * s.zoom), light = s.clock * 1.6 * b[3]; + // Which piece an LED falls in, leaving its place in that piece in lu, lv + // (cells from the piece's corner) for the ruling of the cells. + let lu = 0, lv = 0; + const piece = (R, U, V) => { + for (let j = 0; j < 5; j++) { + const o = j * 5, an = R[o + 4]; + let du = U - R[o], dv = V - R[o + 1]; + if (an !== 0) { + const c = cos(an), sn = sin(an), t0 = c * du + sn * dv; + (dv = c * dv - sn * du), (du = t0); + } + if (abs(du) < R[o + 2] && abs(dv) < R[o + 3]) return (lu = du + R[o + 2]), (lv = dv + R[o + 3]), j; + } + return -1; + }; + // The top: the windmill on squared paper, one ruled line a cell, or + // every 2 or 4 cells when cells are small. + const step = cs >= 3 ? 1 : cs >= 1.5 ? 2 : 4; + for (let y = 0; y < 64; y++) + for (let x = 0; x < W; x++) { + const U = (x + 0.5 - 31.5) / cs, V = (31.5 - (y + 0.5)) / cs; + const j = cut >= 0 && max(abs(U), abs(V)) < cut ? piece(rb, U, V) : piece(ra, U, V); + if (j < 0) { + const gu = U - 0.5, gv = V - 0.5, nu = round(gu / step) * step, nv = round(gv / step) * step; + if (abs(gu - nu) * cs < 0.5 || abs(gv - nv) * cs < 0.5) add(x, y, (0.05 + 0.13 * pal[12]) * fade, (0.05 + 0.13 * pal[13]) * fade, (0.05 + 0.13 * pal[14]) * fade); + continue; + } + // A light turns round the windmill the way its sails turn. + let k = (0.8 + 0.2 * cos(atan2(V, U) - light)) * fade; + if (cs >= 3.5 && ((lu - floor(lu)) * cs < 1 || (lv - floor(lv)) * cs < 1)) k *= 0.62; + // The front of the new central square as it spreads. + if (cut >= 0 && abs(max(abs(U), abs(V)) - cut) * cs < 0.8) k = 1; + add(x, y, pal[j * 3] * k, pal[j * 3 + 1] * k, pal[j * 3 + 2] * k); + } + + // The bottom: the plane of sail sizes, log y across and log z up. Every + // windmill of p lies on the hyperbola yz = (p - x^2) / 4 of its square's + // side x, which these axes draw as a straight line; the swap keeps x, so + // it slides a windmill along its own line, mirrored in the diagonal + // y = z, and Zagier's map jumps from line to line. + const lo = log(0.7), sc = PS / (log(P.k / 0.7) - lo), f2 = fade * min(1, t / GROW + 0.2); + const cur = t < P.end && e < 0.5 ? a : b, R2 = SQRT2; + for (let y = PY - PS; y < PY; y++) + for (let x = PX; x < PX + PS; x++) { + const lY = lo + (x + 0.5 - PX) / sc, lZ = lo + (PY - (y + 0.5)) / sc, q = lY + lZ; + let band = 0, hc = 0; + for (let j = 0; j < P.lm.length; j++) { + if (q <= P.lm[j]) band = j + 1; + if ((abs(q - P.lm[j]) * sc) / R2 < 0.6) hc = 2 * j + 1; + } + // Below the outer line the bands between the lines, tinted with the + // sail colours, rippling along the lines in towards the diagonal. + if (band) { + const c = band & 1 ? 9 : 12, k = (0.15 + 0.06 * cos(abs(lY - lZ) * sc * 0.55 + s.clock * 3)) * f2; + add(x, y, pal[c] * k, pal[c + 1] * k, pal[c + 2] * k); + } + if ((abs(lY - lZ) * sc) / R2 < 0.6) add(x, y, 0.3 * f2, 0.3 * f2, 0.34 * f2); + if (hc) { + const k = (hc === cur[0] ? 0.8 : 0.34) * f2; + add(x, y, pal[18] * k, pal[19] * k, pal[20] * k); + } + } + // The walk so far, segment by segment, the swaps in pale blue and + // Zagier's steps in gold, then the windmills of p as dots: grey until the + // walk reaches them, gold after, the one shown above white. + const at = (tr) => [PX + (log(tr[1]) - lo) * sc, PY - (log(tr[2]) - lo) * sc]; + const segs = t < GROW ? 0 : t < P.end ? i : P.n; + for (let j = 1; j <= segs; j++) { + const [x0, y0] = at(P.path[j - 1]), [x1, y1] = at(P.path[j]); + const reach = j === i && t < P.end ? e : 1, n = max(1, round(max(abs(x1 - x0), abs(y1 - y0)) * reach)); + const c = j % 2 ? 15 : 18, k = 0.9 * fade; + for (let q = 0; q <= n; q++) { + const u = (q / n) * reach; + add(floor(x0 + (x1 - x0) * u), floor(y0 + (y1 - y0) * u), pal[c] * k, pal[c + 1] * k, pal[c + 2] * k); + } + } + const rad = cl(sc * 0.12, 0.8, 3), pulse = 0.75 + 0.25 * cos(s.clock * 7); + for (const tr of P.S) { + const [cx, cy] = at(tr); + let seen = false; + for (let q = 0; q <= segs; q++) if (P.path[q][0] === tr[0] && P.path[q][1] === tr[1] && P.path[q][2] === tr[2]) seen = true; + const now = tr[0] === cur[0] && tr[1] === cur[1] && tr[2] === cur[2]; + const k = (now ? pulse : seen ? 0.9 : 0.45) * f2, r = now ? 1 : seen ? pal[18] : 0.8, g = now ? 1 : seen ? pal[19] : 0.8, bl = now ? 1 : seen ? pal[20] : 0.85; + for (let yy = floor(cy - rad); yy <= cy + rad; yy++) + for (let xx = floor(cx - rad); xx <= cx + rad; xx++) + if ((xx + 0.5 - cx) ** 2 + (yy + 0.5 - cy) ** 2 <= rad * rad + 0.3) add(xx, yy, r * k, g * k, bl * k); + } + + for (let y = 0, o = 0; y < H; y++) + for (let x = 0; x < W; x++, o += 3) display.setPixel(x, y, min(255, round(B[o] * 255)), min(255, round(B[o + 1] * 255)), min(255, round(B[o + 2] * 255))); +} From d0b4bc157ecf080af9dbeaa9a6f4b118a927e1a3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 08:58:41 +0000 Subject: [PATCH 09/10] =?UTF-8?q?57=ED=8C=90:=20README=20=EC=97=90=20?= =?UTF-8?q?=ED=8C=90=20=ED=91=9C=EC=99=80=20=ED=8C=90=20=EC=A0=88,=20?= =?UTF-8?q?=EB=8B=A4=EC=9D=8C=20=ED=8C=90=20=EA=B0=90=EC=9D=84=20=EB=8D=94?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XoJSXx5srrfeJ2DPWFv6K5 --- incoming/pattern-study/README.md | 77 ++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index e5488e99d..e331c597c 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 468개를 쉰여섯 갈래로 나눠 만들었어요. 1판에서 열세 갈래로 23개를 만들고, 2판에서 1판에 없던 아홉 갈래로 16개를, 3판에서 하나를, 4판에서 새 갈래인 직물로 하나를 더했어요. 5판은 새 패턴 없이 1판의 Euclid Pulse 를 다시 짰고, 6판은 세운 패널의 128줄을 위아래로 다 쓰는 패턴 하나를 더했어요. 7판은 또렷한 모양이 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 8판은 새 갈래인 기계로 하나를 더했어요. 9판은 새 패턴 없이 1판의 Sprout 을 다시 짰어요. 10판은 세운 패널을 떨어지는 블록의 우물로 쓰는 패턴 하나를 더했어요. 11판은 여러 색이 꽤 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 12판은 새 갈래인 천체로 하나를 더했어요. 13판은 새 패턴 없이 1판의 Glyph Rain 을 다시 짰어요. 14판은 절차가 바뀌기 전에 띄운 바퀴라서, 세운 패널의 높이를 다 떨어지는 폭포 하나를 더했어요. 15판은 바뀐 절차의 첫 판이라 열 갈래에 새 패턴을 하나씩, 모두 열 개를 더했어요. 도시와 음악 갈래는 그때 새로 열었어요. 16판도 열 갈래에 하나씩 열 개를 더했고, 바다와 곤충 갈래를 새로 열었어요. 17판도 열 갈래에 하나씩 열 개를 더했고, 전자 회로와 장난감 갈래를 새로 열었어요. 18판도 열 갈래에 하나씩 열 개를 더했고, 유리와 광물·결정, 놀이공원 갈래를 새로 열었어요. 19판도 열 갈래에 하나씩 열 개를 더했고, 부엌과 버섯·균사 갈래를 새로 열었어요. 20판도 열 갈래에 하나씩 열 개를 더했고, 새와 화학·실험 갈래를 새로 열었어요. 21판도 열 갈래에 하나씩 열 개를 더했고, 종이와 운동·경기 갈래를 새로 열었어요. 22판도 열 갈래에 하나씩 열 개를 더했고, 몸과 비행 갈래를 새로 열었어요. 23판도 열 갈래에 하나씩 열 개를 더했고, 무대·공연과 짐승 갈래를 새로 열었어요. 24판도 열 갈래에 하나씩 열 개를 더했고, 공사·건설과 도자기 갈래를 새로 열었어요. 25판도 열 갈래에 하나씩 열 개를 더했고, 농사·밭과 화산 갈래를 새로 열었어요. 26판도 열 갈래에 하나씩 열 개를 더했고, 얼음·극지와 미생물 갈래를 새로 열었어요. 27판도 열 갈래에 하나씩 열 개를 더했고, 철도와 필름·영화 갈래를 새로 열었어요. 28판은 주제 표가 수학 쪽으로 바뀐 첫 판이라 이름 있는 수학 대상 열 개를 열 갈래에 하나씩 더했고, 수론 갈래를 새로 열었어요. 29판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 첫 판이라, 이름 있는 수학 대상 여섯과 화장품 매장 벽에 틀 질감 넷을 더했고, 위상·매듭과 화장품 팝업 갈래를 새로 열었어요. 30판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 매장의 LED 벽에 틀 질감 넷을 화장품 팝업 갈래에 더했어요. 31판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 열지 않았어요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽의 질감 넷을 화장품 팝업 갈래에 더했어요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 41판은 주제 표가 수학·추상 다섯과 할로윈 다섯으로 바뀐 첫 판이라, 수학 대상 다섯을 다섯 갈래에 하나씩 더하고 할로윈 갈래를 새로 열어 10월 31일에 틀 패턴 다섯을 넣었어요. 42판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 41판이 연 할로윈 갈래에 더했어요. 43판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 44판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 45판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 열 개 아래인 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 46판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 47판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 48판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 49판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 50판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 51판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 52판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 53판은 할로윈 몫이 쉬게 된 첫 판이라, 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 54판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 55판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 56판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 478개를 쉰여섯 갈래로 나눠 만들었어요. 1판에서 열세 갈래로 23개를 만들고, 2판에서 1판에 없던 아홉 갈래로 16개를, 3판에서 하나를, 4판에서 새 갈래인 직물로 하나를 더했어요. 5판은 새 패턴 없이 1판의 Euclid Pulse 를 다시 짰고, 6판은 세운 패널의 128줄을 위아래로 다 쓰는 패턴 하나를 더했어요. 7판은 또렷한 모양이 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 8판은 새 갈래인 기계로 하나를 더했어요. 9판은 새 패턴 없이 1판의 Sprout 을 다시 짰어요. 10판은 세운 패널을 떨어지는 블록의 우물로 쓰는 패턴 하나를 더했어요. 11판은 여러 색이 꽤 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 12판은 새 갈래인 천체로 하나를 더했어요. 13판은 새 패턴 없이 1판의 Glyph Rain 을 다시 짰어요. 14판은 절차가 바뀌기 전에 띄운 바퀴라서, 세운 패널의 높이를 다 떨어지는 폭포 하나를 더했어요. 15판은 바뀐 절차의 첫 판이라 열 갈래에 새 패턴을 하나씩, 모두 열 개를 더했어요. 도시와 음악 갈래는 그때 새로 열었어요. 16판도 열 갈래에 하나씩 열 개를 더했고, 바다와 곤충 갈래를 새로 열었어요. 17판도 열 갈래에 하나씩 열 개를 더했고, 전자 회로와 장난감 갈래를 새로 열었어요. 18판도 열 갈래에 하나씩 열 개를 더했고, 유리와 광물·결정, 놀이공원 갈래를 새로 열었어요. 19판도 열 갈래에 하나씩 열 개를 더했고, 부엌과 버섯·균사 갈래를 새로 열었어요. 20판도 열 갈래에 하나씩 열 개를 더했고, 새와 화학·실험 갈래를 새로 열었어요. 21판도 열 갈래에 하나씩 열 개를 더했고, 종이와 운동·경기 갈래를 새로 열었어요. 22판도 열 갈래에 하나씩 열 개를 더했고, 몸과 비행 갈래를 새로 열었어요. 23판도 열 갈래에 하나씩 열 개를 더했고, 무대·공연과 짐승 갈래를 새로 열었어요. 24판도 열 갈래에 하나씩 열 개를 더했고, 공사·건설과 도자기 갈래를 새로 열었어요. 25판도 열 갈래에 하나씩 열 개를 더했고, 농사·밭과 화산 갈래를 새로 열었어요. 26판도 열 갈래에 하나씩 열 개를 더했고, 얼음·극지와 미생물 갈래를 새로 열었어요. 27판도 열 갈래에 하나씩 열 개를 더했고, 철도와 필름·영화 갈래를 새로 열었어요. 28판은 주제 표가 수학 쪽으로 바뀐 첫 판이라 이름 있는 수학 대상 열 개를 열 갈래에 하나씩 더했고, 수론 갈래를 새로 열었어요. 29판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 첫 판이라, 이름 있는 수학 대상 여섯과 화장품 매장 벽에 틀 질감 넷을 더했고, 위상·매듭과 화장품 팝업 갈래를 새로 열었어요. 30판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 매장의 LED 벽에 틀 질감 넷을 화장품 팝업 갈래에 더했어요. 31판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 열지 않았어요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽의 질감 넷을 화장품 팝업 갈래에 더했어요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 41판은 주제 표가 수학·추상 다섯과 할로윈 다섯으로 바뀐 첫 판이라, 수학 대상 다섯을 다섯 갈래에 하나씩 더하고 할로윈 갈래를 새로 열어 10월 31일에 틀 패턴 다섯을 넣었어요. 42판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 41판이 연 할로윈 갈래에 더했어요. 43판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 44판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 45판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 열 개 아래인 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 46판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 47판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 48판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 49판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 50판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 51판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 52판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 53판은 할로윈 몫이 쉬게 된 첫 판이라, 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 54판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 55판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 56판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 57판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. [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판에서 세로로 다시 짰어요. @@ -750,6 +750,21 @@ | [Random Organization](patterns/random-organization.js) | 시뮬레이션 | 원판 83개가 오른쪽은 아래로, 왼쪽은 위로 밀렸다 돌아오고, 그동안 서로 닿은 원판만 금색으로 빛났다가 주기 끝에 조금씩 튕겨 나가요. 1분쯤 지나면 아무도 닿지 않는 자리를 찾아 청록으로 멈춰요. | | [Twist and Writhe](patterns/twist-writhe.js) | 위상·매듭 | 세 번 꼬인 고리 리본이 납작한 원에서 8자를 지나 키 큰 초나선으로 감기는 동안, 아래 막대에서 꼬임수 Tw 가 줄어든 만큼 뒤틀림수 Wr 가 늘어 둘의 합은 늘 같은 정수 눈금에 멈춰요. | +### 57판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Medial Axis](patterns/medial-axis.js) | 알고리즘 | 닫힌 곡선을 따라 붙인 불이 안팎으로 같은 빠르기로 번져서, 거리의 톱니 띠가 반 띠씩 엇갈린 토막이 되어 곡선 안쪽과 바깥쪽으로 흘러가요. LED 마다 가장 가까운 곡선 위 점의 자리로 색을 칠해서, 그 점이 건너뛰는 중심축이 두 색 사이의 솔기로 서요. 처음에는 줄기에 갈래 하나가 붙은 키 큰 나무예요. K3 은 곡선에 물결을 하나씩 더하는데 5분의 1 LED 높이의 혹에서도 깊숙한 새 가지가 자라고, K4 는 앞선 사이 거리(6.7~14.2 LED)예요. | +| [Solenoid](patterns/solenoid.js) | 프랙탈 | 흐린 유리 같은 솔리드 토러스가 세로로 서서 12초에 한 바퀴 돌고, 그 안에서 닫힌 줄 한 가닥이 16번 감겨 두 다발, 네 다발, 여덟 쌍으로 갈라져요. 줄마다 검은 테가 있어서 앞을 지나는 줄이 뒤의 줄을 끊어요. K3 은 단계 k 를 1 에서 7 까지 바꿔 줄을 2가닥에서 128가닥까지 둘로 가르고, K4 는 축소 비율 λ 를 0.49 에서 0.22 까지 줄여 쌍을 한 줄로 붙여요. | +| [Sinai's Walk](patterns/sinai-walk.js) | 노이즈·흐름 | 무작위 퍼텐셜이 들쭉날쭉한 능선으로 패널을 내려가는 지형에서, 줄마다 제 동전을 가진 자리를 걷개 3,000개가 log t 로 가는 시계를 따라 걸어요. 골짜기마다 log t 깊이까지 웅덩이가 차오르고, 걷개는 저를 담은 웅덩이 바닥에서 빛나다가 웅덩이가 능선을 넘쳐 더 깊은 골짜기와 합쳐지면 다 같이 넘어가요. K3 은 치우침이라 0 이면 시나이의 되돌아오는 걸음이고 올리면 지형이 기울어 걷개가 t^κ 로 덫을 하나씩 거쳐 내려가며, K4 는 자리마다 log ρ 의 퍼짐 σ(1.2~2.4)예요. | +| [Stokes Drift](patterns/stokes-drift.js) | 물결·간섭 | 깊이 109 LED 의 물 단면에 파장 256 LED 의 깊은 물 파도가 오른쪽으로 지나가고, 선형 파도의 속도장을 따라 적분한 입자마다 작은 궤도가 꼭 닫히지 않아서 수면에서 곧게 내린 물감 선 네 개가 윗부분부터 앞으로 기울어요. 기운 모양은 e^(2kz) 곡선이라 14 LED 내려갈 때마다 반으로 줄고, 아래쪽 점들은 거의 제자리에 있어요. K3 은 가파름 ak(0.15~0.4)이고, K4 는 입자가 속도를 읽는 자리라 0 은 제 자리에서 읽는 에어리 파도, 1 은 궤도 중심에서 읽는 게르스트너의 트로코이드 파도여서 궤도가 닫히고 물감 선이 흔들리기만 해요. | +| [Bicycle Tracks](patterns/bicycle-tracks.js) | 선·궤적 | 앞바퀴 점이 세운 패널을 굽이치며 오르고 한 축거 뒤의 뒷바퀴 점은 늘 앞바퀴를 향해서만 굴러서, 뒷바퀴 자취가 굽이마다 안쪽을 질러가요. 두 자취 사이 초승달에는 뒷바퀴 자취의 접선이 꼭 한 축거 앞에서 앞바퀴 자취에 닿는 선분이 빗살처럼 서서, 자전거가 어느 쪽으로 갔는지 알려 줘요. 34초쯤마다 앞바퀴가 원을 두 바퀴 돌면 뒷바퀴는 반지름 √(r²−L²) 의 원으로 감겨 들고, 빠져나올 때는 곧게 나가는 앞바퀴 뒤로 트랙트릭스를 그려요. K3 은 축거(16~24 LED), K4 는 굽이마다 핸들을 꺾어 두는 각도(50~70도)예요. | +| [Terrell Rotation](patterns/terrell-rotation.js) | 공간·원근 | 정육면체와 위경선 그물을 두른 구가 바둑판으로 놓인 판이 바늘구멍 카메라 앞을 빛의 절반 넘는 빠르기로 위로 미끄러지고, LED 마다 판을 먼저 떠나 지금 카메라에 닿는 빛이 그리는 모습을 보여 줘요. 정육면체는 납작해지지 않고 아랫면을 보이며 돌아 보이고, 구는 테두리가 둥근 채로 그물의 극만 위로 돌아가요. 빛이 다가오는 쪽에서 떠난 아래는 보라 쪽으로, 멀어지는 쪽에서 떠난 위는 초록 쪽으로 옮겨 가요. K3 은 빛에 대한 속도 β(0.5~0.95)라 카메라 바로 앞의 정육면체가 0.5 에서 30도, 0.9 에서 64도 돌아 보이고, K4 는 카메라와 판 사이 거리(2~8칸)라 가까우면 시야가 위아래 54도라 가까운 줄의 테가 휘고 멀면 19도라 모든 정육면체가 거의 같은 각도 arcsin β 로 돌아 보여요. | +| [Galperin Billiard](patterns/galperin-billiard.js) | 물리·스프링 | 무거운 블록이 레일을 내려와 바닥 벽 위의 가벼운 블록에 부딪히고, 블록끼리든 벽이든 부딪힐 때마다 셈판에 점이 하나씩 켜져서 질량비 100, 10,000, 1,000,000 에서 31, 314, 3141 로 읽혀요. 위의 에너지 원에서는 두 속도가 한 점이 되어 부딪힐 때마다 현을 하나씩 그으며 원의 왼쪽 끝에서 오른쪽 끝까지 지그재그로 원판을 쓸고, 각 arctan √(m/M) 의 쐐기에 들어가면 멈춰요. K3 은 자릿수 k(1~3), K4 는 몰린 부딪힘을 늘려 보여 주는 슬로 모션 초(0~8)예요. | +| [Zagier's Windmills](patterns/zagier-windmill.js) | 수론 | 소수 p 가 p 칸짜리 풍차로 서요. 가운데 정사각형 둘레에 직사각형 날개 넷이 돌아가며 붙고, 자기에의 사상은 바깥선을 그대로 둔 채 가운데 정사각형을 새로 잘라 내고, 날개 바꾸기는 날개마다 4분의 1 바퀴 돌려 눕혀요. 두 사상을 번갈아 밟은 걸음은 날개가 정사각형인 풍차에서 멈추고, 그 풍차가 넓이를 더하면 p 인 두 정사각형으로 미끄러져 모이며, 아래 로그 눈금의 (y, z) 평면에는 p 의 풍차가 모두 제 쌍곡선 위의 점으로 찍히고 걸어온 지그재그 길이 남아요. K3 은 13부터 113까지 순환을 여는 소수이고, K4 는 풍차를 1~2.5배로 키워요. | +| [Pentagon Tilings](patterns/pentagon-tilings.js) | 보로노이·셀 | 볼록 오각형 하나의 복사본이 얇은 검은 줄눈을 두고 평면을 덮고, 타일마다 돌아간 각도와 뒤집힘에 따라 색이 달라요. K3 은 라인하르트의 1형, 카이로 타일링(4형), 5형, 마저리 라이스의 13형, 2015년의 15형을 차례로 고르고, 바뀔 때 새 타일이 한 장씩 들어와요. 1형에서는 오각형 모양이 32/Speed 초에 한 바퀴 바뀌어도 타일링이 그대로 이어지고, K4 는 타일 한 장의 넓이를 12×12 에서 28×28 LED 까지 키워요. | +| [Ostwald Ripening](patterns/ostwald-ripening.js) | 시뮬레이션 | 수백 개의 방울이 묽은 용액에 멈춰 선 채 물질을 주고받아서, 평균 반지름보다 작은 방울은 자홍으로 점점 빨리 줄다가 꺼지고 큰 방울은 초록으로 부풀어요. 용액은 작은 방울 둘레에서 옅은 자홍으로, 큰 방울 둘레에서 옅은 초록으로 빛나고, 그 사이를 용질 알갱이가 작은 방울에서 큰 방울로 흘러가요. 방울이 처음의 18% 아래로 줄면 빈자리에 새 무리가 맺혀요. K3 은 새 무리의 방울 수(200~500), K4 는 처음 반지름이 고르지 않은 정도예요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -2912,6 +2927,50 @@ E8 의 근 240개를 고셋의 4₂₁ 다면체 꼭짓점으로 그렸어요. 주제 5 로 칼루가레아누-화이트-풀러 정리를 위상·매듭 갈래에 세웠어요. 닫힌 리본의 두 모서리가 얽힌 횟수 Lk 는 리본이 중심선을 감아 도는 꼬임수 Tw 와 중심선 자체의 뒤틀림수 Wr 의 합이에요(게오르게 칼루가레아누 1959, 제임스 화이트 1969, 브록 풀러 1971). 패널에서는 Lk 번 꼬인 원 리본이 돌면서 8자를 지나 106 LED 높이의 초나선으로 감겼다가 풀려요. 리본 앞면은 하늘색, 뒷면은 보라, 두 모서리는 분홍과 연두예요. 맨 아래 막대는 Wr 를 0 에서부터, Tw 를 Wr 가 끝난 자리에서부터 그려서 늘 Lk 눈금에서 멈춰요. Wr 는 매 프레임 144 점 다각형에 가우스 이중 적분을 클레닌과 랑고프스키(2000)의 닫힌 꼴로 돌려 구하고, Tw 는 지어진 리본에서 각을 하나씩 재요. Lk 3 부터 6 까지 Coil 3, 4.5, 6 으로 한 바퀴씩 돌려 보니, 두 모서리의 가우스 연결 적분은 늘 정확히 Lk 였고 Tw + Wr 는 Lk 에서 0.005 안에 있었어요. 중심선을 늘이는 첫 식은 감을수록 키가 줄어서 초나선이 60 LED 에 그쳤고, 반 바퀴당 Wr 도 0.39 였어요. 가닥이 축과 약 42도를 이루게 늘이자 106 LED 로 섰고, 반 바퀴당 Wr 도 0.74 가 됐어요. 반지름 45 원은 정면으로는 패널에 안 들어가서, 납작할 때는 시점이 비스듬한 채로 천천히 돌고 감겼을 때 빨리 돌아요. 처음 색은 앞면과 한쪽 모서리가 둘 다 초록빛이라 섞여서 앞면을 하늘색으로 옮겼어요. +## 57판 + +57판도 열 개 모두 주제 5 「수학 · 추상」이에요. 같이 돈 56판(#267)이 고른 갈래와 수학, 지도 칸 둘을 그 PR 본문에서 읽고 피했고, 앞 바퀴 PR 58개에는 댓글도 리뷰도 없어서 따를 글이 없었어요. dev 의 458개에 56판 열 개를 이 판에서 재서 더한 468개로 세면, 추상 갈래 가운데 노이즈·흐름이 열넷으로 가장 적고 기하·대칭을 뺀 나머지 열셋이 열다섯이에요. 노이즈·흐름에 하나, 56판이 비켜 간 공간·원근과 물리·스프링, 수론, 알고리즘에 하나씩 넣고, 남은 다섯은 프랙탈과 물결·간섭, 선·궤적, 보로노이·셀, 시뮬레이션에 하나씩 넣었어요. 주제 1 은 그 468개에서 패턴이 둘뿐인 칸 넷 가운데 가장자리 × 촘촘함 1,3 에 Medial Axis 를, 촘촘함 × 모양 바뀜 4,4 에 Solenoid 를 세웠고, 주제 4 는 Sinai's Walk 와 Stokes Drift, Bicycle Tracks 예요. 열 개 다 「흔한 것」에도 「낯선 수학 예」에도 없고, Solenoid 하나가 54판이 「목록 밖에서 해 볼 것」에 적어 둔 감이에요. Solenoid 는 끌개지만 미분방정식을 적분한 점 구름이 아니라 관 속에 관을 감아 넣는 짓기를 그대로 그려서, 「흔한 것」의 끌개 점 구름과 다르다고 봤어요. 열 개는 일꾼 다섯이 레포 밖 worktree 에서 둘씩 짓고, 접촉 시트(처음 노브, 노브마다 다섯 자리)로 하나씩 본 뒤 합쳤어요. 합치면서 Terrell Rotation 과 Galperin Billiard 는 K3 범위의 바닥을 올렸어요. 둘 다 처음 판은 효과가 없는 끝에서 열렸어요. + +### Medial Axis + +주제 5 와 1 로 해리 블럼(1967)의 중심축과 들불 변환을 알고리즘 갈래에 세우고, Möbius Loxodrome 과 Talbot Carpet 둘뿐이던 가장자리 × 촘촘함 1,3 칸을 겨눴어요. 가장자리가 0.21 아래이면서 촘촘함이 0.189 에서 0.297 사이인 칸이에요. LED 마다 384 꼭짓점 곡선에서 가장 가까운 선분을 이웃에게서 넘겨받고 곡선을 따라 더 가까운 쪽으로 걸어서 찾는데, 노브 열 조합의 819,200 LED 를 전수 계산과 맞춰 보니 1e-4 LED 넘게 다른 것이 56개, 가장 큰 차이가 0.015 LED 였어요. 처음 판은 거리의 톱니 띠만 그려서 촘촘함이 0.151 이었어요. 띠는 법선 쪽으로만 오르고 앞선을 따라서는 평평해서, 곡선이 세로로 선 곳에서는 행 쪽 이웃만 달라져요. 오르막을 40 아래로 두면 띠의 높이와 너비를 어떻게 골라도 0.16 을 넘지 못했어요. 앞선을 곡선을 따라 12 LED 토막으로 끊어 하나 건너 하나를 반 띠 늦추고, 앞선을 따라 한 줄씩 오르내리는 38단 안의 결을 깔자 촘촘함 0.203, 가장자리 0.149 로 그 칸에 섰어요. 색을 곡선 위 자리 256단으로 칠한 판은 복잡도가 2.837 로 축 끝 2.735 를 넘어서, 32단으로 줄여 2.545 가 됐어요. 이 패널에서 새로 본 것은 중심축이 얼마나 쉽게 가지를 치는지예요. 5분의 1 LED 높이의 물결 하나로 곡선 안쪽 중심축의 끝이 평균 3.5개에서 8.2개로, 반 LED 높이에서 15개로 늘고, 가지는 혹 바로 안쪽에서 시작해 곡선 한가운데 줄기까지 이어져요. 곡선이 프레임마다 LED 의 몇 분의 1 만 움직여서 거리 계산은 두 프레임에 한 번 하고, 한 프레임 1.7 ms, 노브 0.28, 0.32, 0.24, 0.24 예요. + +### Solenoid + +주제 5 와 1 로 스메일-윌리엄스 솔레노이드(스티븐 스메일 1967, 로버트 윌리엄스 1967)를 프랙탈 갈래에 세우고, Ham Sandwich 와 Pin Art 둘뿐이던 촘촘함 × 모양 바뀜 4,4 칸을 겨눴어요. 솔리드 토러스를 길이로 두 배 늘여 가늘게 한 뒤 제 안에 두 번 감아 넣는 사상 f(θ, z) = (2θ, λz + ½e^{iθ}) 의 끌개가 레오폴트 피에토리스(1927)와 다비트 판 단치히(1930)의 이진 솔레노이드예요. 패턴은 궤도를 적분하지 않고 k 단계 관의 가운데 줄 z(s) = ½ Σ λ^{m−1} e^{is/2^m} 을 그려요. 패턴의 셈으로 k 1 에서 7 까지 줄은 꼭 2^k 바퀴 뒤에 2.9e-14 안에서 닫혔고, 그 전에는 λ^{k−1} 보다 가까워지지 않았어요. 단면의 상자 세기 차원은 λ 0.474 에서 0.929 로 log 2 / log(1/λ) = 0.928 과 맞았어요. 처음 판은 검은 바탕에 꼬이지 않은 줄 여덟 가닥이라 촘촘함 0.091, 모양 바뀜 0.231 이었어요. 줄이 패널 양옆을 나란히 오르는 세로줄이라 LED 사이 단차가 옆으로만 생겼어요. 원판 좌표를 관 둘레로 열 번 도는 틀에서 읽어 줄을 비스듬히 눕히고 모서리로만 닿는 LED 를 빼서 대각선 줄을 한 LED 두께로 만들자 0.17 을 넘었고, 줄마다 3 LED 뒤를 지우는 검은 테를 두르고 토러스 몸통을 0.55 밝기의 유리로 칠하자 0.320 이 됐어요. 모양 바뀜 0.356 은 토러스가 정면에서 옆면까지 3초 만에 도는 데서 나와요. 정면에서는 양옆 두 기둥 사이에 검은 구멍이 있고 옆면에서는 가운데 한 기둥만 남아서, 16×8 칸의 대부분이 켜짐과 꺼짐을 바꿔요. 여는 λ 는 1/4 이 아니라 0.474 예요. 1/4 에서는 잔 단계가 LED 하나 아래로 떨어져 촘촘함 0.239 로 칸 밖이에요. 이 패널에서 새로 본 것은 색이에요. 첫 항의 위상을 따라 칠한 두 다발이 한 둘레를 돌고 나면 서로 색을 바꿔서, 한 관이 두 번 감겼다는 것이 그 자리에서 읽혀요. 노브는 0.18, 0.34, 0.23, 0.20 이에요. + +### Sinai's Walk + +주제 5 와 4 로 프레드 솔로몬(1975)의 무작위 환경 속 걸음과 야코프 시나이(1982)의 걸음을 노이즈·흐름 갈래에 세웠어요. 128줄이 그대로 128자리이고, 걷개 3,000개는 한 걸음씩 옮기지 않고 전이 행렬을 거듭 제곱한 P^(2^k) 의 행에서 한 번에 뽑아서, log t 가 19 인 1억 8천만 걸음까지 한 프레임 1 ms 안팎으로 가요. 그렇게 뽑은 걷개 20만 개의 분포는 정확한 법칙과 총변동 거리 0.0016(t = 1,000)과 0.0024(t = 10,000)로 맞았어요. 4만 자리 직선에서 환경 300개로 센 |X_t| 의 중앙값을 (log t)² 로 나누면 t = 10^3 에서 10^7 까지 0.25, 0.26, 0.30, 0.38, 0.35 로 해리 케스텐(1986)의 극한값 0.41 아래에 머물렀지만, √t 로 나누면 0.38 에서 0.029 로 떨어졌어요. 치우친 걸음의 지수는 κ 0.25, 0.5, 0.75 에서 0.39, 0.56, 0.74 로 나와서, κ 가 작을수록 10^7 걸음에서도 아직 멀어요. 첫 판은 능선을 늘 패널 폭에 맞춰 그렸는데, V 가 브라운 운동처럼 크기만 바뀌어서 σ 를 돌려도 같은 그림이었고 K4 반응이 0.0076 으로 실패했어요. V 한 단위를 2.6 열로 고정하자 σ 가 지형을 넓히며 K4 가 0.09 가 됐고, 웅덩이도 log t 한 단위에 2.6 열씩 같은 빠르기로 차올라요. 이 패널에서 새로 본 것은 걷개가 시나이의 바닥을 늦게 따라간다는 것이에요. 탈출 시간에 골짜기와 장벽의 너비에서 오는 곱수가 붙어서, 걷개의 절반이 가장 깊은 골짜기로 넘어가는 때가 예측보다 log t 로 σ 1.2 에서 3.2, 2.4 에서 1.9 늦었어요. 그래서 σ 를 1.2 부터 두고 한 판을 예측보다 4.5 더 돌려요. 골짜기마다 바닥에 따라 땅의 색을 달리하자 골짜기들이 log t 에 따라 합쳐지는 것이 보였고, `thirds` 42/36/37, 노브 0.06, 0.05, 0.10, 0.09 예요. + +### Stokes Drift + +주제 5 와 4 로 조지 가브리엘 스토크스(1847)의 표류를 물결·간섭 갈래에 세웠어요. 선형 깊은 물 파도의 속도장에서 입자를 4차 룽게-쿠타로 적분할 뿐 앞으로 미는 항은 없는데, 패턴의 update 를 그대로 40주기 돌려 점들의 평균 속도를 재 보니 ak 0.15 에서 깊이 4, 20, 44, 76 LED 의 값이 이론 ωka²e^(2kz) 의 1.019, 1.009, 1.003, 1.0005 배였어요. 0.4 에서는 수면 가까이 1.166 배로 벗어나고 깊이 76 에서 1.004 배로 돌아와서, 차이가 (ak e^(kz))² 차수로 줄어요. K4 는 속도를 입자 자리 대신 궤도 중심에서 읽게 하는데, 1 에서는 같은 적분기로 한 주기에 0.0000001 LED 도 움직이지 않는 프란츠 게르스트너(1802)의 닫힌 궤도가 되고 0.5 에서는 표류가 0.50~0.53 배였어요. 첫 판은 ak 0.4 에서 골 위에 놓인 점이 선형 파도의 마루를 타고 날아가 측정이 멈춰서, 점을 궤도 중심에서 놓고 노브가 바뀌면 궤도에 다시 앉혀요. 그다음 K4 반응이 0.03 이라 파장을 192 에서 256 LED 로 늘리고 가파름의 바닥을 0.15 로 올렸어요. 표류는 (ak)² 로 가서 0.05 에서는 0.4 의 64분의 1 이라, 바닥에서 여는 Lab 이 기울지 않는 물감 선으로 열릴 판이었어요. 노브는 0.06, 0.11, 0.13, 0.06 이고 움직임 0.973, 채움 0.188, `thirds` 15/21/19, 한 프레임 1.3 ms 예요. 이 패널에서 새로 본 것은 수면이라는 물질선이 가만두면 접힌다는 점이에요. ak 0.33 위에서는 마루와 골에서 출발한 수면 입자의 평균 높이가 조금씩 달라져 다른 빠르기로 흘러가서 50초 안에 선이 겹쳤고, 수면 입자의 궤도 중심만 프레임마다 20분의 1씩 평균 수면으로 당기자 120초 동안 한 번도 접히지 않았어요. + +### Bicycle Tracks + +주제 5 와 4 로 자전거 바퀴 자국을 선·궤적 갈래에 세웠어요. 뒷바퀴는 옆으로 미끄러지지 않아서 늘 앞바퀴 쪽으로만 가고, 그래서 뒷바퀴 자취의 접선은 언제나 한 축거 L 앞에서 앞바퀴 자취와 만나요. 앞바퀴가 곧게 갈 때의 뒷바퀴 자취가 클로드 페로(1670)가 묻고 크리스티안 하위헌스(1692)가 이름 붙인 트랙트릭스이고, 조지프 콘하우저와 댄 벨먼, 스탠 웨건(1996)은 이 접선 규칙으로 두 자국만 보고 자전거가 간 방향을 가렸어요. 두 점을 RK4 로 함께 풀었더니 한 시간 동안 |F − R| 이 L 에서 3.0×10^-7 안에 머물렀고, 뒷바퀴 점마다 접선을 L 만큼 뻗은 끝이 앞바퀴 자취에서 많아야 0.013 LED 떨어졌어요. 거꾸로 앞바퀴 자취의 접선을 뻗으면 평균 10.8 LED 빗나가요. 원을 도는 동안 뒷바퀴와 √(r² − L²) 의 차이는 한 바퀴마다 0.00305 배로 줄었고, 로버트 푸트(1998)의 뫼비우스 사상에서 나오는 exp(−2π√(r² − L²)/L) 는 0.00298 이에요. 첫 판은 앞바퀴가 곡률 제한을 받으며 느리게 굽어서 두 자취가 1.4 LED 만 벌어졌고, 채움 0.062, `thirds` 5/6/6, 노브 0.01, 0.04, 0.01, 0.06 이었어요. 핸들을 50~70도로 꺾어 둔 채 굽이를 돌게 하자 두 자취가 동심원 호를 따라 L tan(각/2), 16 과 50도에서 7.5 LED 만큼 벌어지고 그 초승달을 빗살이 채워서, 채움 0.234, `thirds` 19/25/24, 노브 0.06, 0.12, 0.13, 0.14 가 됐어요. 원에서 나온 뒷바퀴가 63도 기운 틀을 끌고 나와 L 24 에서 앞바퀴가 x = −5 까지 패널 밖으로 나간 것은, 앞바퀴를 곧게 올려 보내 뒷바퀴가 트랙트릭스로 따라 들게 해서 고쳤어요. 이 패널에서는 두 자취보다 초승달 안에서 부채꼴로 펴졌다 오므라드는 빗살이 먼저 눈에 들어와서, 앞뒤를 가리는 규칙이 그대로 그림으로 읽혀요. + +### Terrell Rotation + +주제 5 로 테렐-펜로즈 회전을 공간·원근 갈래에 세웠어요. 1959년에 제임스 테렐과 로저 펜로즈가 따로 보였고 안톤 람파가 1924년에 먼저 적은 결과로, 빛에 가깝게 지나가는 물체를 찍으면 로런츠 수축 대신 회전이 찍혀요. 패턴은 LED 마다 카메라 쪽 방향을 광행차 식 하나로 판의 정지계 방향으로 바꾸고, 정지계에서 카메라가 지금 있는 자리부터 광선을 추적해요. 지연 시간의 이차방정식을 카메라 계에서 따로 풀어 맞춰 보니 광선 표가 0.000016 LED 안에서 맞았어요. 멀리 둔 작은 정육면체는 β 0.3, 0.6, 0.9 에서 17.46, 36.87, 64.16도 돌아 보여 arcsin β 와 0.01도 안에서 같았고, 반지름 0.34 의 구는 겉면의 점 40만 개로 짚은 테두리의 가로세로 비가 1.0001~1.0007 이었어요. 첫 판은 거리 1.2 에서 시야가 위아래 67도라 줄마다 길게 늘어나고 채움이 0.755 였는데, 거리를 2~8 로 옮기고 물체를 줄이고 면을 어둡게 해서 0.527 이 됐어요. 그 판은 K3 이 β 0 에서 열려 잰 값이 그냥 원근의 그림이었어서, 합치면서 범위를 0.5~0.95 로 올려 정육면체가 30도 돌아 보이는 자리에서 열리게 했어요. β 0 과 나란히 견주는 자리는 없어졌어요. 움직임 2.709, 색 0.198, `thirds` 58/51/44, 한 프레임 1.4 ms, 노브 0.12, 0.22, 0.23, 0.23 이에요. 이 패널에서 새로 본 것은 β 0.9 에서 패널에 보이는 판이 거의 다 이미 카메라를 지나간 줄이라는 점이에요. 바로 옆을 지나는 줄은 아래 가장자리보다 더 아래인 64도에 보이고, 패널에는 앞으로 멀어지는 줄이 위로 갈수록 촘촘히 쌓이며 초록으로 어두워져요. + +### Galperin Billiard + +주제 5 로 그레고리 갈페린(2003)의 「π 로 치는 당구」를 물리·스프링 갈래에 세웠어요. 패턴의 부딪힘 셈은 블록이 다음에 만날 시각을 간격 나누기 다가오는 속도로 바로 풀어서 시간 걸음의 오차가 없고, k = 0 에서 5 까지 3, 31, 314, 3141, 31415, 314159 번 부딪힌 뒤 늘 0 ≤ v < V 로 끝났으며 에너지는 5.3e-12 안에서 지켜졌어요. k = 3 에서는 3141번의 한가운데 90% 가 3.4 ms 안에 몰리고 가장 가까운 두 번은 0.27 마이크로초 떨어져서, 화면 시간을 τ + b·N(τ) 로 두어 부딪힘마다 Warp/n 초씩 나눠 주는 늘이기를 썼어요. 질량의 제곱근으로 잰 속도 평면에서 블록끼리의 현은 모두 기울기 −10^k 로 나란하고 벽의 현은 모두 세로라서, 현들은 별이 아니라 원을 왼쪽에서 오른쪽으로 쓰는 빗살 지그재그가 돼요. 첫 판은 노브가 0.02, 0.03, 0.08, 0.02 였어요. 여는 자리가 세 번만 부딪히는 k = 0 이고, 원판이 어둡고 셈판 점이 작아서 2~3초 사이에 바뀌는 LED 가 적었어요. Warp 를 0~8 로 바꿔 0.06 에서 거의 제 시간으로 열고 쓸린 원판을 0.55 밝기의 보라에서 초록으로 가는 색으로 칠하자 노브가 살아났고, 합치면서 K3 의 범위를 1~3 으로 올려 31번 부딪히는 질량비 100 에서 열리게 했어요. 셈판은 천을 색 띠 하나로, 백을 체커 무늬 정사각형으로 묶어서 k = 3 에서 띠 셋, 정사각형 하나, 줄 넷, 점 하나로 3141 이 읽혀요. 움직임 0.151, 모양 바뀜 0.236, 노브 0.07, 0.14, 0.23, 0.15 예요. + +### Zagier's Windmills + +주제 5 로 피에르 드 페르마(1640)의 두 제곱수 정리를 레온하르트 오일러(1749)의 증명 대신 돈 자기에(1990)의 한 문장 증명으로 수론 갈래에 세웠어요. 자기에의 증명은 로저 히스브라운(1984)의 셈을 한 문장으로 줄인 거예요. 10^5 아래의 4로 나눠 1 남는 소수 4,783개 모두에서 자기에의 사상은 S 위의 대합이고 고정점은 (1, 1, (p − 1)/4) 하나뿐이었으며, |S| 는 다 홀수였고 걸음은 다 y = z 에서 끝나 p = x² + (2y)² 를 줬어요. 가장 긴 걸음은 p = 95,881 의 3,768 걸음이고, 4로 나눠 3 남는 소수 4,808개는 x² + 4yz 가 늘 4로 나눠 1 남아서 S 가 비고 고정점도 없어요. 그리다가 새로 안 것은 바깥선이에요. 날개를 늘 같은 쪽으로 돌려 붙이면 자기에의 첫째와 셋째 경우는 바깥선을 칸까지 그대로 두지만 가운데 경우는 거울에 비친 바깥선을 줘서, 패턴은 가운데 경우마다 풍차의 도는 방향을 뒤집어요. 3,000 아래 소수 211개의 걸음에 든 자기에 걸음 10,196개가 그렇게 다 바깥선을 지켰고, 1,928개가 방향을 뒤집었어요. 패널의 13~113 은 걸음이 S 를 다 지나서 아래 평면의 점이 끝내 모두 금색이 되고, 다 지나지 못하는 첫 소수는 29개 가운데 15개만 지나는 229 예요. 5 에서 열던 첫 판은 두 사상이 다 그대로 두는 십자 (1, 1, 1) 때문에 잰 시간 대부분이 정사각형 1 과 4 였고 노브 반응이 0.03, 0.05, 0.10, 0.09 였는데, 13 에서 열자 0.08, 0.09, 0.11, 0.11 이 됐고 움직임은 0.108, 채움은 0.393 이에요. (y, z) 평면은 보통 눈금에서 113 의 점이 다 두 축에 붙어서 로그 눈금으로 바꿨고, 그러자 쌍곡선이 평행한 직선이 되어 날개 바꾸기는 제 직선을 따라 미끄러지고 자기에의 사상은 직선 사이를 건너뛰어요. + +### Pentagon Tilings + +주제 5 로 볼록 오각형 하나로 평면을 덮는 열다섯 형 가운데 다섯을 보로노이·셀 갈래에 세웠어요. 카를 라인하르트(1918)의 1형과 4형(카이로 타일링), 5형은 회전 생성원으로 바로 짓고, 마저리 라이스의 13형과 케이시 맨, 제니퍼 매클라우드맨, 데이비드 폰 데라우(2015)의 15형은 오각형 하나에서 패치를 키우는 되돌이 탐색으로 찾은 뒤 붙임 목록으로 적었어요. 15형 탐색은 2,013번 만에 362장짜리 패치에 닿았고, 거기서 읽은 주기 한 칸에 12장이 들어 있었어요. 13형은 주기 한 칸에 8장이에요. 이 둘의 배열은 펴낸 그림과 맞춰 보지 못했고, 덮임과 오각형 조건만 셈으로 지켜봤어요. 검사는 64×128 LED 를 LED 하나에 8×8 표본으로 52만 4,288 점을 찍어 했는데, 다섯 형 모두 빈 점과 두 번 덮인 점이 0 이었고 모든 꼭짓점의 각의 합이 360도에서 1e-13도 안으로 맞았어요. 타일마다 본 오각형과 변은 2.0e-15, 각은 4.4e-15 라디안 안에서 같았어요. 잰 값은 촘촘함 0.143, 색 0.454, 모양 바뀜 0.123 이고, 노브는 0.37, 0.27, 0.32, 0.29 예요. 처음 잰 색이 0.590 이라 축 끝 0.675 에 가까워서 채도를 0.78 에서 0.62 로, 뒤집힌 타일은 0.36 으로 낮췄어요. 이 패널에서 새로 본 것은 1형이에요. 오각형의 각 B 가 70도에서 110도까지 흔들려도 B + C = 180 만 지키면 두 장이 육각형을 이뤄 틈 없이 깔리는 것이 눈으로 보여요. + +### Ostwald Ripening + +주제 5 로 빌헬름 오스트발트(1896)의 숙성과 일리야 리프시츠와 비탈리 슬료조프(1961), 카를 바그너(1961)의 LSW 이론을 시뮬레이션 갈래에 세웠어요. 깁스-톰슨 효과로 작은 방울의 표면 농도가 더 높아서 d(r³)/dt = 3K(r/r_c − 1) 이 되고, 전체 부피가 그대로라 r_c 는 꼭 평균 반지름이에요. 패턴의 ripen() 을 그대로 써서 방울 백만 개를 여는 자리의 퍼짐 0.14 로 풀었더니, 개수가 5만에서 1만으로 주는 동안 ⟨r⟩³ 이 직선(R² 0.99988)으로 자랐고 기울기 0.4455K 가 LSW 의 4/9 = 0.4444K 와 0.25% 안에서 맞았어요. 그때 r/⟨r⟩ 의 분포는 1.125 언저리에서 가장 높고 가장 큰 값이 1.348 이라 1.5 를 넘는 방울이 하나도 없었으며, LSW 곡선과의 L1 거리는 0.053 이었어요. 패널 위의 200개 무리는 아직 과도기라 기울기가 0.57K, R² 가 0.958 이에요. 첫 판은 장이 너무 옅고 용질 알갱이가 제자리에서 흔들려 먼지처럼 보였는데, 장의 이득을 0.9 에서 3 으로, 닿는 거리를 6 에서 8 LED 로 늘리고 알갱이가 기울기를 따라 흐르게 하자 채움이 0.280 에서 0.519 로, K1 반응이 0.07 에서 0.12 로 올랐어요. 개수가 1/t 로 줄어 한 무리의 마지막 10초가 멈춘 듯 보여서, 속도 상수를 0.3 에서 0.42 r0³ 로 올리고 18% 에서 새로 맺히게 해 한 무리가 30초에서 15~17초로 짧아졌고, 채움은 0.460 이 됐어요. 이 패널에서는 평균 반지름에 가까운 흰 방울이 초록에서 자홍으로 넘어가는 것이 자주 보이는데, r_c 가 방울보다 빨리 자라서 자라던 방울도 언젠가는 줄어드는 쪽으로 넘어가기 때문이에요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -2954,7 +3013,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 25일 `dev`(`f72355d`)의 328개에 43판부터 56판까지의 백마흔 개를 더한 468개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 25일 `dev`(`f72355d`)의 328개에 43판부터 57판까지의 백쉰 개를 더한 478개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -3024,6 +3083,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 56판은 54판까지 든 458개에 남은 둘뿐인 칸 여섯 가운데, 같이 돈 54판이 겨눈 복잡도 × 채움 4,2 와 촘촘함 × 모양 바뀜 4,3 을 빼고 가장자리 × 복잡도 4,4 와 움직임 × 촘촘함 1,4 를 겨눴어요. 가장자리 × 복잡도 4,4 는 이웃 차이 가운데 40 을 넘는 몫이 0.79 를 넘으면서 한 화면이 deflate 로 LED 당 2.17 바이트 넘게 눌리는 칸이에요. 가장자리는 가장 밝은 채널로만 재고 복잡도는 세 채널을 다 눌러서, Dead Leaves 는 잎의 밝기를 45 씩 떨어진 네 단으로만 두고 색상은 잎마다 조금씩 흩었어요. 가장자리 1.000, 복잡도 2.337 예요. 움직임 × 촘촘함 1,4 는 1초에 바뀌는 밝기가 0.061 아래로 고요하면서 이웃 차이 평균이 0.297 을 넘는 칸이에요. Golomb Trominoes 는 LED 하나를 한 칸으로 깔고 빈칸만 걸려서, 한 걸음에 다시 깔리는 트로미노가 하나에서 63개뿐이에요. 움직임 0.023, 촘촘함 0.355 이에요. 두 패턴의 첫 판은 축의 끝을 넓혔어요. Golomb Trominoes 는 빈칸이 1초에 10 LED 를 걸어 움직임이 0.014 로 그 축의 아래 끝 0.019 보다 낮았고, 모양 바뀜도 0.005 였어요. Dead Leaves 는 다음 표본이 시작될 때 앞 표본 전체가 밝기 60 으로 가라앉아 모양 바뀜이 0.454 로 위 끝 0.413 을 넘었어요. 둘이 같이 들어가자 움직임 칸의 경계가 0.061, 0.305, 1.529 에서 0.048, 0.262, 1.434 로, 모양 바뀜 칸의 경계가 0.099, 0.214, 0.330 에서 0.099, 0.229, 0.360 으로 옮겨져서, 움직임 × 모양 바뀜 1,4 가 비고 촘촘함 × 모양 바뀜 4,4 에는 Ham Sandwich 하나만 남을 판이었어요. 빈칸을 1초에 18 LED 로 걷게 하고 앞 표본은 한 단만 낮춰 잿빛으로 비치게 하자, 움직임 0.023, 모양 바뀜 0.196 가 돼서 움직임 축과 모양 바뀜 축의 위 끝은 그대로였어요. Golomb Trominoes 의 모양 바뀜 0.008 만 그 축의 아래 끝을 0.016 에서 내려서, 모양 바뀜 칸의 경계가 0.099, 0.214, 0.330 에서 0.093, 0.211, 0.328 로 옮겨졌어요. 하나뿐이거나 빈 칸은 생기지 않았어요. 56판까지 든 468개에서 지도 21장은 다 `none`이고, 겨눈 두 칸에는 셋째가 섰어요. 둘뿐인 칸은 가장자리 × 촘촘함 1,3 과 1,4, 2,4, 촘촘함 × 모양 바뀜 4,4 넷이 남아요. +57판은 56판까지 든 468개에 남은 둘뿐인 칸 넷 가운데 가장자리 × 촘촘함 1,3 과 촘촘함 × 모양 바뀜 4,4 를 겨눴어요. 가장자리 × 촘촘함 1,3 은 이웃 차이 가운데 40 을 넘는 몫이 0.21 아래이면서 이웃 차이 평균이 0.189 를 넘는 칸이에요. 오르막이 40 아래인 톱니 띠를 깔면 날카로운 차이는 띠의 끝마다 하나뿐이지만, 띠가 법선 쪽으로만 올라서 앞선을 따라 선 이웃은 같은 값이에요. 그래서 Medial Axis 의 띠만으로는 촘촘함이 0.16 을 넘지 못했고, 앞선을 12 LED 토막으로 끊어 하나 건너 하나를 반 띠 늦추고 한 줄씩 오르내리는 38단 안의 결을 깔아 0.203, 가장자리 0.149 가 됐어요. 색을 256단으로 칠하자 복잡도가 2.837 로 그 축의 위 끝 2.735 를 넘어서, 32단으로 줄여 2.545 로 지켰어요. 촘촘함 × 모양 바뀜 4,4 는 이웃 차이 평균이 0.297 을 넘으면서 16×8 칸으로 뭉갠 화면이 첫 화면에서 0.328 넘게 멀어지는 칸이에요. 잔 결이 판의 반쯤을 덮고 그 결이 6초 안에 크게 자리를 옮겨야 해요. Solenoid 는 LED 한 줄 두께의 비스듬한 줄과 검은 테로 결을 만들고, 토러스가 정면에서 옆면까지 3초 만에 돌아 양옆 두 기둥과 가운데 한 기둥이 켜짐을 바꿔서 촘촘함 0.320, 모양 바뀜 0.356 이에요. 처음 판은 꼬이지 않은 세로줄이라 단차가 옆으로만 생겨서 촘촘함이 0.091 이었어요. 열 개 다 일곱 축의 끝 안에 있어서 칸 경계는 468개와 같아요. 57판까지 든 478개에서 지도 21장은 다 `none` 이고, 겨눈 두 칸에는 셋째가 섰어요. 둘뿐인 칸은 가장자리 × 촘촘함 1,4 와 2,4 둘이 남아요. + ### 아직 없는 갈래 `families.json` 의 쉰여섯 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 29판이 연 위상·매듭 갈래는 Hopf Fibration 과 31판의 Boy's Surface, 33판의 Borromean Rings, 34판의 Lakes of Wada, 36판의 Heawood Torus, 38판의 Sperner's Lemma, 39판의 Borsuk-Ulam, 44판의 Ham Sandwich, 46판의 Reeb Graph, 48판의 Möbius Energy, 49판의 Square Peg, 51판의 Persistence Barcode, 53판의 Belt Trick, 55판의 Universal Cover 예요. 28판이 열려다 접은 갈래라 28판의 Hairy Ball 은 노이즈·흐름에 그대로 있어요. 세잎 매듭은 48판의 Möbius Energy 가 처음 세웠어요. 29판은 화장품 팝업 갈래도 열었어요. 주제 6 의 몫이라 주제 2 로 세지 않았고, Cream Swirl, Serum Gel, Glitter Fall, Loose Powder 넷이 다 들어 있어요. 30판의 Swatch Row, Pearl Sheen, Bi-Phase, Satin Fold 가 그 갈래에 넷을 더했고, 31판의 Lip Gloss, Foam Lather, Setting Mist, Sponge Blend 가 넷을 더 더해 열둘이에요. 32판의 Cushion Mesh, Clay Mask, Sugar Scrub, Chrome Powder 가 넷을 더해 열여섯이고, 33판의 Cleansing Balm, Gold Leaf, Essence Drop, Peel-Off 가 넷을 더해 스물이에요. 34판의 Cat Eye, Velvet Nap, Mood Tint, Hair Gloss 가 넷을 더해 스물넷이고, 35판의 Bath Fizz, Toner Pad, Sheet Mask, Gilded Cream 이 넷을 더해 스물여덟이고, 36판의 Lotion Ribbon, Shimmer Oil, Blooming Gel, Whipped Soap 이 넷을 더해 서른둘이에요. 38판의 Milk Ring, Couette Shimmer, Photochromic, Serum Fingers 가 넷을 더해 서른여섯이에요. 39판의 Soap Film, Blotting Paper, Serum Drip, Crackle Polish 가 넷을 더해, 37판의 넷까지 마흔넷이에요. 41판은 할로윈 갈래를 열었어요. 주제 6 의 몫이라 주제 2 로 세지 않았고, Jack-o'-Lantern, Sheet Ghosts, Spider Drop, Cauldron, Eyes in the Dark 다섯이 들어 있어요. 28판이 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table, 33판의 Goldbach Comet, 35판의 Kolakoski, 36판의 Elliptic Chords, 39판의 Explicit Formula, 44판의 Bulgarian Solitaire, 48판의 Chebyshev's Bias, 49판의 Curlicue, 51판의 Sato-Tate, 53판의 Gaussian Periods, 55판의 Question Mark Function 이에요. 27판이 연 철도 갈래는 Funicular 하나이고, 필름·영화 갈래는 Horse in Motion 하나예요. 두 갈래는 주제 표가 수학 쪽으로 바뀌기 전에 열었어요. 그 뒤로 ROUND.md 는 사물로 묶는 갈래를 더 열지 않아요. 26판이 연 얼음·극지 갈래는 Iceberg 와 27판의 Sled Dogs 이고, 미생물 갈래는 Dark Field 와 27판의 Mitosis 예요. 25판이 연 농사·밭 갈래는 Rice Terraces 와 26판의 Rice Planting, 27판의 Center Pivot 이고, 화산 갈래는 Eruption 과 26판의 Geyser, 27판의 Lava Lake 예요. 24판이 연 공사·건설 갈래는 Tower Crane 과 25판의 Excavation, 26판의 Implosion 이에요. 24판이 연 도자기 갈래는 Potter's Wheel 과 25판의 Kintsugi, 26판의 Noborigama 예요. 23판이 연 무대·공연 갈래는 Proscenium 과 24판의 Flying Trapeze, 25판의 Marionette, 26판의 Shadow Play 예요. 23판이 연 짐승 갈래는 Giraffe 와 24판의 Emergence, 25판의 Snow Monkeys 예요. 22판이 연 몸 갈래는 Circulation 과 23판의 Axon, 24판의 Breath, 27판의 Fluoroscope 예요. 22판이 연 비행 갈래는 Liftoff 와 23판의 Final Approach, 24판의 Thermal, 25판의 Skydiver 예요. 20판이 연 새 갈래는 Kingfisher 와 21판의 Woodpecker, 22판의 Weaver, 23판의 Skein 이에요. 20판이 연 화학·실험 갈래는 Chromatography 와 21판의 Chemical Garden, 22판의 Flame Test, 23판의 Elephant Toothpaste 예요. 21판이 연 종이 갈래는 Pop-up Book 과 22판의 Senbazuru, 23판의 Hanging Scroll, 26판의 Origami 예요. 21판이 연 운동·경기 갈래는 Lap Pool 과 22판의 Bowling Lane, 23판의 High Dive, 26판의 Pole Vault, 27판의 Climbing Wall 이에요. 위아래로 움직이는 경기는 High Dive 와 Pole Vault 뒤로 27판에 실내 암벽이 섰어요. 19판이 연 부엌 갈래는 Popcorn 과 20판의 Kettle, 21판의 Pancakes 이고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판이 연 유리 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori, 27판의 Glassblower 예요. 광물·결정 갈래는 Bismuth 와 19판의 Dripstone, 20판의 Agate 이고, 놀이공원 갈래는 Drop Tower 와 19판의 Helter Skelter, 20판의 High Striker, 25판의 Ferris Wheel 이에요. 말이 오르내리는 회전목마는 아직 없어요. 17판이 연 전자 회로 갈래는 Circuit Board 와 18판의 Server Rack, 21판의 Nixie 이고, 장난감 갈래는 Marble Run 과 18판의 Peg Light, 21판의 Slinky, 22판의 Pin Art 예요. 16판이 연 바다 갈래는 Abyss 와 17판의 Shoreline, 20판의 Sunglint, 23판의 Bait Ball 이고, 곤충 갈래는 Fireflies 와 17판의 Ant Farm, 20판의 Moth Lamp, 24판의 Waggle Dance 예요. 15판이 연 도시 갈래는 Metro Map 과 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판이 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables, 24판의 Pipe Organ 이에요. 4판이 연 직물 갈래는 Jacquard Loom 과 15판의 Plait, 19판의 Needlepoint, 23판의 Sequins 이고, 8판이 연 기계 갈래는 Gear Train 과 15판의 Cam Wave, 19판의 Paternoster 예요. 12판이 연 천체 갈래는 Star Trails 와 15판의 Analemma, 16판의 Lunation, 22판의 Galilean Moons 예요. 위성의 궤도는 Galilean Moons 가 그렸고, 해를 도는 행성은 아직 없어요. 리듬·펄스 갈래는 1판의 Euclid Pulse 와 Pulse Bars 뒤로 20판의 Change Ringing, 32판의 Arnold Tongues, 35판의 Chimera State, 38판의 Infinity Series, 41판의 Siteswap, 44판의 Kac Ring, 46판의 Stochastic Resonance 가 섰어요. @@ -3084,6 +3145,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 56판의 열 개는 가장 낮은 노브가 Golomb Trominoes 의 K2 Speed 로 네 자리로 0.0515 이고, Elliptic Law 의 K1 Hue 0.0525 와 Twist and Writhe 의 K1 Hue 0.0582, K3 Lk 0.0593 이 그다음이에요. 노브 마흔 개가 다 0.03 을 넘어요. 고요한 그림은 노브가 화면에 늘 있는 것을 바꿔야 창 안에서 보였어요. Golomb Trominoes 의 깔기는 빈칸이 어디로 걸어도 판마다 190 칸 넘게 달라지지 않아서, Speed 노브는 처음에 0.010 이었어요. 바탕을 빈칸의 껍질로 칠해 빈칸이 큰 블록의 경계를 넘을 때 판 넓이의 색이 바뀌게 하자 0.05 가 됐어요. 가는 점과 선만 켜지는 그림은 이번에도 색 노브가 약했어요. Elliptic Law 는 채움이 0.317 이고 고윳값의 꼬리가 어두워서 Hue 가 0.0525 예요. 여러 노브가 정리의 다른 자리를 골라요. Erdős Discrepancy 의 K4 는 보웨인, 최, 쿤스 수열의 소수 3, 5, 7, 11, 13 을, Multiplicative Cascade 의 K4 는 형제 무게의 상관을 골라요. Twist and Writhe 의 K3 은 정수 Lk 라서 노브가 다섯 자리에서만 그림을 바꿔요. +57판의 열 개는 가장 낮은 노브가 Sinai's Walk 의 K2 Speed 로 네 자리로 0.0539 이고, 같은 패턴의 K1 Hue 0.0590 과 Stokes Drift 의 K1 Hue 0.0601, Bicycle Tracks 의 K1 Hue 0.0617 이 그다음이에요. 노브 마흔 개가 다 0.05 를 넘어요. 이번에는 여는 자리가 정리의 시시한 끝에 놓인 패턴이 넷이었어요. Lab 은 범위가 0 을 품는 K3 을 0 에서 여는데, Terrell Rotation 의 β 0 은 그냥 원근이고 Galperin Billiard 의 k 0 은 세 번만 부딪히는 같은 질량이에요. 둘 다 범위의 바닥을 효과가 보이는 0.5 와 1 로 올렸어요. Zagier's Windmills 는 p = 5 의 풍차 (1, 1, 1) 를 두 사상이 다 그대로 두어서 13 에서 열고, Stokes Drift 는 표류가 (ak)² 로 가서 가파름의 바닥을 0.15 로 두었어요. 크기만 바뀌는 그림은 노브가 안 보였어요. Sinai's Walk 의 퍼텐셜은 브라운 운동처럼 σ 를 바꾸면 크기만 바뀌어서, 능선을 패널 폭에 맞춰 그린 첫 판은 K4 가 0.0076 이었고 한 단위를 2.6 열로 고정하자 0.09 가 됐어요. + ### 세로 64×128 2~8초 동안 켜진 LED 가 5% 아래인 3분의 1이 있는 패턴이 넷이에요. `study.ts` 표의 `thirds` 칸이 이 값이에요. Jelly 4.5% 와 Pulse Bars 2.6% 는 위 3분의 1이 비고, Sparks 2.0% 와 Dragon Fold 4.9% 는 아래 3분의 1이 비어요. Dragon Fold 는 문턱 바로 아래예요. 6판의 Jacob's Ladder 는 위, 가운데, 아래가 27.0%, 21.4%, 35.1% 예요. 햇빛층에서 심해까지 내려가는 바다는 16판의 Abyss 가 했고, 덩굴은 15판의 Ivy 가 했어요. 어두운 판 위의 가는 빛 하나는 128줄을 다 지나가도 3분의 1마다 10% 를 넘기 어려워요. 6판의 처음 아크는 가운데 3분의 1을 7.4% 켰어요. 9판의 Sprout 은 접시꽃 둘이 위 3분의 1을 8.1% 켜요. 이렇게 철이 도는 패턴은 2~8초가 어느 철에 걸리느냐에 따라 값이 몇 배씩 달라서, 씨앗 여럿으로 재 봐야 해요. 테두리를 그리는 패턴은 테두리를 빼고 재요. 10판의 Tetromino Well 은 양쪽 벽 2 LED 만으로 3분의 1마다 6.3% 를 켜서, 벽을 넣어 재면 위 3분의 1이 늘 문턱을 넘어요. 바탕을 다 그리는 패턴은 `thirds` 칸이 3분의 1마다 90 을 넘어요. 14판의 Waterfall 은 98/93/97 이에요. 그런 패턴은 2~8초 동안 밝기가 24/255 넘게 바뀌는 LED 로 세어야 그림이 128줄을 쓰는지 보여요. 그렇게 세면 Waterfall 의 위 3분의 1은 25.9% 예요. 15판의 Ivy 는 벽을 그려 `thirds` 가 84/89/82 이고, 바뀐 LED 로 세면 48.7%, 70.6%, 29.2% 예요. 가만히 있는 가는 선은 켜져 있어도 바뀌지 않아요. 15판의 Incense 는 곧은 연기 실이 가운데 3분의 1을 지나도 바뀐 LED 가 3.7% 였고, 소용돌이가 그 3분의 1 안에서 시작되게 하자 15.5% 가 됐어요. 16판의 Abyss 는 물을 다 칠해서 `thirds` 가 100/80/22 이고, 바뀐 LED 로 세면 84.5%, 59.3%, 36.0% 예요. 한밤층은 아귀와 불꽃, 잠수정 등빛뿐이라 아래 3분의 1이 가장 낮아요. 16판의 Harp 는 액자를 빼고 줄만 재면 48/53/12 이고, 바뀐 LED 는 아래 3분의 1이 9.8% 예요. 거기는 받침과 공명통 발이고, 줄은 300 LED 남짓한 삼각형에만 있어요. 17판의 Marble Run 은 레일과 리프트가 서 있어서 `thirds` 가 39/30/68 이고, 바뀐 LED 는 40.7%, 54.3%, 59.7% 예요. 17판의 Ant Farm 은 모래가 바탕이라 `thirds` 가 100/97/96 이고, 바뀐 LED 는 6.9%, 34.5%, 53.4% 예요. 위 3분의 1은 모래 윗면과 그 위의 어두운 유리라서 개미가 드나드는 입구와 더미만 바뀌어요. 18판의 Server Rack 은 앞판을 13~23 으로 24 아래에 두어서, `thirds` 63/63/61 이 다 드라이브와 코드와 LED 의 빛이에요. 바뀐 LED 셈은 2~8초 가운데 한 프레임만 24 를 넘어도 그 LED 를 세서, 무작위 반짝임이 값을 부풀려요. 18판의 Pint 는 잔 기포 반짝임을 켜면 50.3%, 60.2%, 40.0% 이고 끄면 50.2%, 46.8%, 26.3% 예요. 빛의 흔들림도 그래요. Longcase Clock 은 촛불일 때 41.8%, 45.6%, 45.3% 이고 흔들림 없는 달빛에서는 2.6%, 4.2%, 5.9% 예요. 자라는 그림은 미리 돌리기를 시간 대신 상태로 멈추면 씨앗마다 같은 자리에서 열려요. 18판의 Bismuth 는 무리 꼭대기가 20번 줄에 닿을 때 멈춰요. 14번 줄까지 기다리면 2초에 이미 다 자라 있어서 모양 바뀜이 0.027 로 떨어졌어요. 19판의 Dripstone 은 잘린 암반이 4/255 쯤이라 위아래가 검게 보여 `thirds` 가 68/99/77 이었고, 암반에 제 빛을 주자 92/98/99 가 됐어요. Foxfire 는 땅을 80번 줄에 두었을 때 위 3분의 1이 5% 였고, 64번 줄로 올리고 그루터기를 세우자 16% 가 됐어요. 쌓이는 그림은 2~8초에 쌓이는 자리가 보이게 열어야 해요. Popcorn 은 더미가 높게 시작하면 아래 3분의 1의 바뀐 LED 가 4.4% 였고, 낮게 시작해 자라게 하자 27.9% 가 됐어요. 넓은 면을 물들이는 조명은 반짝임 없이도 바뀐 LED 를 올려요. Dripstone 은 세 조명이 15초에 한 바퀴 번갈아 켜져서 57.6%, 84.9%, 79.2% 예요. 20판에서는 한 번 일어나는 일이 2~8초 창에 드는지가 값을 갈랐어요. High Striker 는 7.3초에 종이 한 번 울려서 위 3분의 1의 바뀐 LED 가 6.5% 에서 95.6% 가 됐고, Kettle 은 7.1초에 스위치가 꺼지는 것이 창에 들어 모양 바뀜이 0.065 에서 0.168 이 됐어요. 위로 오르는 앞선은 모양 바뀜을 적게 내요. Chromatography 는 6초 동안 앞선이 26줄을 올라도 블록이 16줄 높이라 0.040 이었어요. 21판의 세로 일곱은 Nixie 를 빼고 방이나 바탕을 다 그려서 `thirds` 가 3분의 1마다 70 을 넘었어요. 그런 그림은 바탕을 지우거나 멈추고 한 번 더 재야 주인공이 128줄을 쓰는지 보여요. Chemical Garden 은 유리와 방을 검게 지우자 정원만의 위 3분의 1이 6.6% 였고, 씨앗 열여섯 가운데 하나는 3.2% 였어요. Lap Pool 은 바닥 햇빛 무늬의 시간을 멈추자 바뀐 LED 가 3분의 1마다 18.7~52.1% 였어요. Woodpecker 는 새를 빼고 재도 바뀐 LED 가 57.3/35.1/50.0% 라서, 그 값은 거의 다 잎 사이로 움직이는 햇빛 반점에서 와요. 조금 위에서 본 장면은 세로를 싸게 얻어요. Pop-up Book 은 누운 페이지가 깊이의 5분의 1 줄만 써서, 책을 아래 34줄에 두고도 선 카드가 위 3분의 1까지 닿아요. 닉시관 하나는 지름이 높이의 절반쯤이라 Nixie 의 IN-18 은 54 LED 너비에 109줄로 서요.Slinky 는 계단과 벽을 빼고 스프링과 그림자만 세면 켜진 LED 가 0.4/2.6/3.0% 이고, 스프링이 켠 계단등까지 세면 20.9/16.5/8.6% 예요. 22판의 세로 다섯 가운데 넷은 바탕을 다 칠해서 `thirds` 가 말해 주는 것이 적었어요. 바탕을 검게 지운 사본으로 다시 재면 Liftoff 는 로켓만의 위 3분의 1이 켜진 LED 3.7%, 바뀐 LED 8.2% 라서, 그 3분의 1의 변화는 대부분 카메라가 고개를 들며 어두워지는 하늘에서 와요. Bowling Lane 은 공을 던지지 않게 하면 바뀐 LED 가 5.2/9.5/6.6% 로 떨어져서, 그 값은 거의 다 공과 그 빛이에요. Circulation 은 바탕을 21 아래로 내려 몸만 켜진 LED 로 세게 해서 `thirds` 가 34/50/26 이에요. 남은 자리로 짠 Flame Test 는 실험대가 어두워 아래 3분의 1의 바뀐 LED 가 4.6% 였어요. 불꽃빛의 몫을 0.42 에서 0.52 로 올려도 1.3 포인트만 올라서, 어두운 바닥은 빛을 비추는 것보다 2~8초 창 안에 그 자리로 가는 움직임이 있어야 바뀐 LED 가 올라요. 23판의 세로 일곱 가운데 Proscenium 은 금박 아치가 세 3분의 1을 다 지나가서, 아치를 검게 바꿔 다시 재면 `thirds` 가 94/97/69 에서 63/85/65 가 돼요. Final Approach 는 위 3분의 1을 밤하늘이 채워서, 도시 빛을 받은 천정을 33/255 로 두어 문턱 24 를 넘겼어요. 가는 선 하나가 지나가는 3분의 1은 둘레를 같이 밝혀야 해요. Axon 의 가운데 3분의 1은 축삭 한 줄이 지나가는 자리라서, 수초의 빛과 비스듬히 지나가는 이웃 축삭 둘을 두어 바뀐 LED 가 23.6~35.5% 가 됐어요. 한 번 일어나는 일은 2~8초 창에 들게 짰어요. Hanging Scroll 은 첫 그림이 걸려 있는 시간을 2.2초로 줄여 4.8~6.8초에 말려 올라가게 했고, Giraffe 는 첫 물 마시기를 3.4초에 두었어요. Elephant Toothpaste 의 아래 3분의 1은 바뀐 LED 가 9.7% 로 일곱 가운데 가장 낮아요. 기둥이 6.8초에 꺾이기 전까지는 실린더와 실험대가 서 있기만 해서예요. 24판의 세로 일곱 가운데 다섯은 바탕을 다 칠해서 바뀐 LED 로 봤어요. 23판이 물은 어두운 하늘의 떼는 Emergence 가 답했어요. 하늘을 지우고 박쥐만 세면 3분의 1마다 13.4~13.6, 9.9~10.2, 5.1~5.3% 가 켜져서, 기둥이 좁은 아래 3분의 1은 문턱을 겨우 넘어요. 2~8초 동안 바뀐 LED 로 세면 아래 3분의 1도 23.5~25.0% 예요. 자라는 것의 키가 위 3분의 1을 정해요. Potter's Wheel 은 그릇을 64~90줄로 빚을 때 씨앗 넷 가운데 둘이 위 3분의 1의 바뀐 LED 0.6% 와 0.0% 였고, 76~92줄로 키우자 열여섯 모두 15.2% 를 넘었어요. 바탕의 움직임을 멈추고 다시 재면 주인공의 몫이 보여요. Tower Crane 은 구름을 멈춰도 바뀐 LED 가 45.2/54.1/36.3% 이고, Pipe Organ 은 장미창 빛을 꺼도 3분의 1마다 18.2% 를 넘어요. Flying Trapeze 는 `thirds` 가 81/35/34 로 일곱 가운데 가장 고르지 않지만, 팔로 스폿 빛줄기가 연무 속을 쓸어서 바뀐 LED 는 93.9/87.9/57.5% 예요. 25판의 세로 여섯 가운데 넷은 바탕을 다 칠해서 `thirds` 가 3분의 1마다 97 을 넘었고, 바탕을 멈추거나 지운 사본으로 다시 쟀어요. Eruption 은 하늘의 불빛과 부푸는 지층을 멈춰도 바뀐 LED 가 56.4~67.4/39.1~55.5/15.7~17.7% 이고, 아래 3분의 1의 값은 마그마 방의 대류와 화도를 오르는 가스 덩어리에서 와요. 빨강이 255 에 붙은 불빛은 가장 밝은 채널로 재면 결이 보이지 않아요. 플리니식에서 마그마 방이 밝기 끝에 붙자 아래 3분의 1의 바뀐 LED 가 5.3% 였고, 방의 온도가 기둥의 맥박을 따라 0.16 오르내리게 하자 17.7% 가 됐어요. Excavation 은 구름을 멈추면 24.3/12.5/15.3%, Skydiver 는 구름과 그림자를 멈추면 26.7/66.2/23.7% 예요. Rice Terraces 는 하늘 36줄을 빼도 위 3분의 1의 바뀐 LED 가 12.5% 라서, 맨 위 논들의 구름 반사가 그 3분의 1을 움직여요. 가는 선도 쓸고 지나가면 바뀐 LED 를 많이 켜요. Marionette 는 무대를 검게 지우면 인형과 실, 조종대가 켜는 LED 가 20.5/18.1/10.4% 인데 바뀐 LED 는 67.3/76.8/64.2% 예요. 조종대가 기울 때마다 실 일곱이 옆으로 쓸려서, 15판 Incense 의 곧은 연기 실이 3.7% 였던 것과 반대예요. 물에 비친 그림은 아래 3분의 1을 싸게 채워요. Ferris Wheel 은 위 장면을 0.72 로 눌러 41줄 호수에 비춰서 아래 3분의 1의 바뀐 LED 가 80~81% 예요. 이야기가 있는 데이터 그림은 여는 해가 세로를 정해요. Population Pyramid 는 1820년에서 열 때 막대가 0~78살에만 있어서, `thirds` 13/20/40 의 위 3분의 1이 연도와 라벨뿐이었어요. 라벨과 눈금을 지운 사본의 위 3분의 1은 1975년에서 열면 5.6%, 1985년 6.2%, 1995년 7.3%, 2000년 7.8% 여서 1995년에서 열어요. 26판의 세로 여섯 가운데 다섯은 바탕을 다 칠해서 `thirds` 가 3분의 1마다 79 이상이었고, 바탕을 지운 사본으로 다시 쟀어요. 단면을 물높이나 땅에서 자르면 주인공이 세 3분의 1에 다 걸쳐요. Iceberg 는 물높이를 30번 줄에 두어 빙산의 9분의 8이 물 아래에 서서, 하늘과 물을 지워도 빙산만으로 켜진 LED 가 30/67/30% 예요. Geyser 는 땅속 화도와 물방이 아래 3분의 1을 맡아서, 하늘과 땅을 지운 사본의 아래 3분의 1이 13.6% 켜지고 14.2~14.7% 바뀌어요. 가마 불꽃은 Eruption 의 마그마 방과 같은 문턱에 걸렸어요. Noborigama 의 흰 열 칸은 빨강이 255 에 붙고 일렁임이 온도로만 갈 때 2~8초 동안 바뀐 LED 가 0.8% 와 4.2% 였어요. 빨강을 232 에서 멈추고 밝기를 0.72~1 배로 흔들자 96.9% 와 97.0% 가 됐어요. 가장 밝은 채널이 끝에 붙은 불빛은 색만 흔들어서는 바뀐 LED 로 잡히지 않아요. 위에서 내려다본 판은 3분의 1마다 맡을 것을 따로 두어야 해요. Rice Planting 은 심은 모가 위, 일꾼 줄이 가운데, 모판과 못자리가 아래를 맡아서 물을 지운 사본의 켜진 LED 가 15/16~18/40% 예요. 못자리까지 지우면 아래 3분의 1이 3.9~5.0% 라서, 논둑 길에 모판 둘을 놓아 5.7~6.7% 로 올렸어요. 한 번 일어나는 일은 이번에도 2~8초 창에 두었어요. Implosion 은 2.4초에 발파해 7초쯤 먼지가 꼭대기에 닿고, Pole Vault 는 1.97초에 달려 나가 5.14초에 가로대 위 정점을 찍어요. 가는 선이 쓸고 지나가는 그림은 켜진 LED 가 적어도 바뀐 LED 가 많아요. Pole Vault 는 경기장을 지우면 선수와 장대가 위 3분의 1을 1.4~1.7% 만 켜지만, 2~8초 동안 바뀐 LED 는 22.8~23.2% 이고 장대가 휘며 쓰는 가운데 3분의 1은 92.8% 예요. 27판의 Funicular 는 그 물음을 케이블로 다시 쟀어요. 바탕을 지우고 케이블만 남기면 2~8초 동안 바뀐 LED 가 2.7%, 3.3%, 0.3% 이고, 객차와 함께 두면 케이블이 보태는 몫은 위 3분의 1의 0.1%p 예요. 제자리에서 길이 방향으로 꼬임만 미끄러지는 선은 제 밑의 LED 만 바꿔요. 가는 선은 패널을 가로질러 쓸고 가야 바뀐 LED 를 싸게 얻어요. Climbing Wall 은 홀드를 높이 전체에 고르게 박아도 사람과 로프만 재면 켜진 LED 가 2.1%, 4.1%, 6.1% 로 Pole Vault 와 같은 자리였어요. 키 20줄 남짓한 사람 하나는 어느 3분의 1에 있든 그 칸을 몇 % 밖에 켜지 못하고, 그 사람을 따라 오르는 스폿 빛이 바뀐 LED 를 22% 언저리로 올려요. Glassblower 는 덩어리가 105번 줄까지만 내려와 아래 3분의 1이 1.1% 였고, 잘린 조각이 떨어져 부서지는 컬릿 더미를 바닥에 깔자 41% 가 됐어요. 떨어지는 것이 닿을 바닥을 그리는 것이 아래 3분의 1을 채우는 가장 싼 길이었어요. @@ -3142,9 +3205,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 56판에서는 Tree Peano Curve 와 Bloch Oscillations, Andrews Curves, Elliptic Law, Erdős Discrepancy 가 128줄을 써요. Tree Peano Curve 는 곡선이 LED 를 다 지나서 `thirds` 가 98/99/99 예요. 메우는 순서가 아래에서 위로 가서, 세운 패널이 곡선의 시간축이 돼요. Bloch Oscillations 는 시간이 한 줄에 0.75 씩 쌓여 렌즈 둘 반이 패널에 서고 59/58/58 예요. Andrews Curves 는 t 가 −π 에서 π 까지 128줄을 내려가서 45/51/44 이고, Erdős Discrepancy 는 한 걸음이 한 줄이라 84/96/92 예요. Elliptic Law 는 실수축을 세로로 세워 τ 1/3 의 타원이 반축 비 2 로 패널에 꼭 맞아서 26/41/26 예요. 타원은 위아래 끝이 좁아서, 꼬리를 두 배로 늘이기 전에는 21/30/21 이었어요. Twist and Writhe 의 초나선은 106 LED 로 서지만 리본만 켜져서 16/17/10 예요. +57판에서는 Sinai's Walk 와 Stokes Drift, Bicycle Tracks 가 128줄을 써요. Sinai's Walk 는 한 줄이 한 자리라 퍼텐셜의 능선이 패널을 위아래로 다 내려가고, 땅을 칠해 `thirds` 가 42/36/37 이에요. Stokes Drift 는 수면을 18번 줄에 두고 109 LED 깊이까지 물을 그려서, 아래쪽 점은 거의 움직이지 않아도 점과 물감 선이 켜져 15/21/19 예요. Bicycle Tracks 는 앞바퀴가 패널을 굽이치며 올라서 두 자취와 빗살이 위아래를 다 채우고 19/25/24 예요. 두 선만 그린 첫 판은 자취가 1.4 LED 만 벌어져 5/6/6 이었어요. Medial Axis(90/90/90)와 Pentagon Tilings(88/88/88), Terrell Rotation(58/51/44), Ostwald Ripening(50/50/37), Solenoid(49/50/48), Zagier's Windmills(47/20/49)도 세 칸이 다 5 를 넘지만 주제 4 로 세지 않았어요. Galperin Billiard 는 에너지 원과 셈판 사이가 비어 50/7/16 이에요. + ### 낯선 수학 -주제 5 로 쓴 수학과 다음에 해 볼 수학이에요. 28판이 처음 열 개를 썼어요. 슈람-뢰브너 진화(Loewner Trace), 포드 원(Ford Circles), 털 난 공 정리(Hairy Ball), 아즈텍 다이아몬드와 북극권(Aztec Diamond), 부니모비치 경기장(Stadium Billiard), 라우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 다면체와 E8(Gosset Polytope), 콘웨이의 보고 말하기 수열(Look and Say), 위토프 게임의 그런디 값(Wythoff Nim), 쌍곡 평면의 띠 모형(Hyperbolic Band)이에요. 29판은 여섯을 더했어요. 클라인의 j 불변량(J-Invariant), 호프 올뭉치(Hopf Fibration), 다이슨 브라운 운동(Dyson Gas), 가케야 바늘 문제의 페론 나무(Perron Tree), 임계선 위의 리만 제타 함수(Zeta Loops), 길버트-섀넌-리즈 리플과 베이어-디아코니스의 일곱 번(Seven Shuffles)이에요. 30판도 여섯을 더했어요. 세 간격 정리(Three Gaps), 호프스태터 나비와 TKNN 정수(Hofstadter Butterfly), 노이만의 외부 당구(Outer Billiards), 볼드의 탄도 퇴적과 KPZ 앞면(Ballistic Deposition), 아르놀트의 미앤더(Meander), 브로드벤트와 해머슬리의 스미기(Percolation)예요. 31판도 여섯을 더했어요. 마스킷 조각의 클라인 군 극한 집합(Indra's Pearls), 리틀우드 다항식의 근(Littlewood Roots), 고든의 가우스 해자(Gaussian Moat), 아르놀트의 고양이 사상(Cat Map), 자부스키와 크루스칼의 KdV 솔리톤(KdV Solitons), 보이 곡면(Boy's Surface)이에요. ROUND.md 의 「낯선 수학 예」 여덟 가운데 쓴 것은 슈람-뢰브너 진화와 아즈텍 다이아몬드, 모듈러 형식, 호프 올뭉치, 세 간격 정리, 외부 당구 여섯이었고, 31판이 리틀우드 다항식의 근과 클라인 군의 극한 집합을 써서 여덟을 다 썼어요. 32판도 여섯을 더했어요. 탤벗 효과와 양자 카펫(Talbot Carpet), 길브레스 추측의 표(Gilbreath Table), 원 사상의 아르놀트 혀(Arnold Tongues), 콘웨이와 라딘의 핀휠 타일링(Pinwheel Tiling), 윌킨슨과 윌렘센의 침입 스미기(Invasion Percolation), 울람의 문제와 해머슬리 과정(Hammersley Lines)이에요. 33판도 여섯을 더했어요. 에인절, 홀로이드, 로믹, 비라그의 무작위 정렬 네트워크(Sorting Network), 베르트랑의 역설(Bertrand Paradox), 펜로즈의 비출 수 없는 방(Penrose Room), 당들랭 구(Dandelin Spheres), 테이트의 보로메오 고리(Borromean Rings), 골드바흐 혜성(Goldbach Comet)이에요. 34판도 여섯을 더했어요. 톰슨 문제와 오일러가 남기는 다섯 이웃 열둘(Thomson Problem), 켈빈의 배 물결(Kelvin Wake), 요네야마가 낸 와다의 호수(Lakes of Wada), 샤이데거의 강 그물(Scheidegger Rivers), 모저의 소파 문제와 해머슬리의 소파(Moving Sofa), 크누스의 춤추는 링크로 푼 펜토미노 덮기(Pentomino Cover)예요. 35판도 여섯을 더했어요. 뢸로의 폭이 일정한 곡선(Constant Width), 렌니의 주차 문제(Rényi Parking), 카리의 비주기 왕 타일(Kari-Culik Tiles), 콜라코스키 수열(Kolakoski), 쿠라모토와 바톡토흐의 키메라 상태(Chimera State), 주콥스키 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했어요. 그레이슨의 정리가 말하는 곡선 단축 흐름(Curve Shortening), 헤우드의 일곱 색 토러스 지도(Heawood Torus), 슈바르츠 랜턴(Schwarz Lantern), 윌브러햄과 깁스의 현상(Gibbs Phenomenon), 타원 곡선의 현과 접선(Elliptic Chords), 지먼의 파국 기계(Catastrophe Machine)예요. 37판도 여섯을 더했어요. 포천의 쓸기 선 알고리즘(Fortune's Sweep), 앙투안의 목걸이(Antoine's Necklace), 카르만 소용돌이 길의 안정성(Kármán Street), 단단한 원판의 헥사틱 상(Hexatic Phase), 존슨-멜 쪽매와 아브라미 법칙(Johnson-Mehl), 쿠라모토-시바신스키 방정식(Kuramoto-Sivashinsky)이에요. 38판도 여섯을 더했어요. 앤더슨 국소화(Anderson Localization), 무어가 찾고 Chenciner 와 Montgomery 가 증명한 세 몸 문제의 8자 해(Figure-Eight Orbit), 예센과 빈트너, 에르되시의 베르누이 합성곱(Bernoulli Convolutions), 브라우어르의 고정점 정리를 증명하는 슈페르너 보조정리(Sperner's Lemma), 에겐베르거와 폴리아의 항아리(Pólya Urn), 뇌르고르의 무한 수열(Infinity Series)이에요. 39판도 여섯을 더했어요. 고든, 웹, 월퍼트의 등스펙트럼 북(Isospectral Drums), 폰 망골트의 꼴로 쓴 리만의 명시 공식(Explicit Formula), 요한 베르누이의 최단 강하선(Brachistochrone), 슈타이너의 포리즘(Steiner Chain), 모런 집단으로 그린 킹먼의 합류(Kingman Coalescent), 보르수크-울람 정리(Borsuk-Ulam)예요. 40판도 여섯을 더했어요. 앨더스의 브라운 연속 랜덤 나무(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 함수(Hydrogen Orbital), 젤도비치 근사의 팬케이크(Zeldovich Pancakes), 망델브로가 내다보고 롤러, 슈람, 베르너가 증명한 브라운 운동의 바깥 경계(Brownian Frontier), 복소 긴즈부르크-란다우 방정식의 결함 난류(Defect Turbulence), 루퍼트 왕자의 정육면체(Prince Rupert's Cube)예요. 41판은 다섯을 더했어요. 홀과 왜건의 현수선 길과 정다각형 바퀴(Square Wheels), 퐁슬레의 포리즘(Poncelet Porism), 슐레플리의 4차원 정다포체를 자른 단면(Polytope Slices), 클림켁과 티먼, 마그누손의 사이트스왑(Siteswap), Dudeney 의 잡화상 퍼즐과 월리스-보여이-게르비엔 정리(Hinged Dissection)예요. 42판은 다섯을 더했어요. 바른스도르프 규칙으로 도는 기사 여행(Knight's Tour), 페르미와 파스타, 울람, 칭구의 되돌아옴(FPUT Recurrence), 포슬리에-립킨 반전기(Peaucellier Linkage), 몰리의 삼등분선 정리(Morley Triangle), 랄과 마드라스, 소칼의 피벗 알고리즘으로 돌린 자기 회피 걸음(Pivot Walk)이에요. 43판은 다섯을 더했어요. 월리스 워커의 칼레이도사이클(Kaleidocycle), 쌍곡형에서 록소드롬형으로 감기는 뫼비우스 변환의 흐름(Möbius Loxodrome), 터트의 용수철 정리(Tutte Embedding), 디펀트와 크래비츠의 고리 정리를 품은 히토메자시 사시코(Hitomezashi), 룽게 현상과 체비쇼프 점(Runge Phenomenon)이에요. 44판은 다섯을 더했어요. 마크 카츠의 고리 모형(Kac Ring), 스톤과 튜키의 햄 샌드위치 정리(Ham Sandwich), 브란트의 불가리아 솔리테어(Bulgarian Solitaire), 엘스켄스와 프리시의 탄도 소멸(Ballistic Annihilation), 페레그린의 솔리톤(Peregrine Soliton)이에요. 45판은 다섯을 더했어요. 로건-셰프와 베르시크-케로프의 극한 곡선으로 다가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도브루신 경계 조건을 건 임계 이징 모형의 SLE(3) 경계(Ising Interface), 프리예제프와 동료들의 오일러 걷개로 쌓는 로터 라우터 응집과 레빈-페레스의 스매시 합(Rotor-Router), 폴리아의 과수원 문제(Pólya's Orchard), 넬슨의 평면 색칠 문제와 아이즈벨의 일곱 색(Hadwiger-Nelson)이에요. 46판은 다섯을 더했어요. 브릴루앙의 구역(Brillouin Zones), 모스 이론과 레브의 그래프(Reeb Graph), 로렌츠 기체와 시나이의 당구(Lorentz Gas), 리언 글래스의 점무늬(Glass Patterns), 벤치와 수테라, 불피아니의 확률 공명(Stochastic Resonance)이에요. 47판은 다섯을 더했어요. 오키프와 홍, 스트로가츠의 스워멀레이터(Swarmalators), 빌스의 외로운 달리기 선수 추측(Lonely Runner), 샤츠의 올로이드와 그 전개도(Oloid), 쾨베의 원 채우기 정리와 콜린스-스티븐슨의 반복(Koebe Packing), 콘웨이의 병사와 황금비 무게(Conway's Soldiers)예요. 48판은 다섯을 더했어요. 오하라의 매듭 뫼비우스 에너지(Möbius Energy), 체비쇼프의 편향과 루빈스타인-사르낙의 밀도(Chebyshev's Bias), 섀든과 레키엔, 마스든의 이중 소용돌이 능선(Double Gyre), 아자리와 프로스트의 깜빡이는 래칫(Flashing Ratchet), 망델브로의 프랙탈 스미기(Fractal Percolation)예요. 49판은 다섯을 더했어요. 뒤러의 전개 문제와 셰퍼드의 추측(Dürer's Unfolding), 베리와 골드버그가 재규격화한 세타 합의 꼬부랑 곡선(Curlicue), 오일러 방정식과 푸앵소의 폴호드로 본 테니스 라켓 정리(Tennis Racket), 켐프의 사슬과 네 색 정리(Kempe Chains), 퇴플리츠의 내접 정사각형 문제와 그린-로브의 직사각형(Square Peg)이에요. 50판은 다섯을 더했어요. 크레이치넌과 배철러의 2차원 난류가 거꾸로 가는 캐스케이드와 맥윌리엄스의 소용돌이(Inverse Cascade), 사포발과 로소, 구예의 확산 앞줄(Diffusion Front), 필리와 마르케티, 케이츠와 타이외르의 운동성 상분리(Motility Clusters), 박과 스네펜의 진화 모형(Bak-Sneppen), 리세의 영원한 가속(Risset Rhythm)이에요. 51판은 다섯을 더했어요. 스미스와 마이어스, 캐플런, 굿먼-스트라우스의 모자 타일과 H/T/P/F 메타타일 치환(Hat Monotile), 반도와 동료들의 최적 속도 모형과 스기야마의 원형 트랙 실험(Phantom Jam), 사토와 테이트의 법칙과 타원곡선의 a_p(Sato-Tate), 칼라비와 야우의 공간에서 핸슨이 잘라 그린 5차 페르마 곡선(Calabi-Yau Quintic), 에델스브루너와 레처, 조모로디언의 지속 호몰로지와 바코드(Persistence Barcode)예요. 52판은 다섯을 더했어요. 슈람과 셰필드가 등고선을 SLE(4) 로 보인 2차원 가우스 자유장(Gaussian Free Field), 당튀와 마즈무다르, 베링거의 광탄성 힘 사슬(Force Chains), 앤스컴의 사중주와 마테이카, 피츠모리스의 같은 통계 담금질(Anscombe's Quartet), 드 브라윈 수열과 BEST 정리(de Bruijn Sequence), 라인스의 베타 평면 띠 제트(Zonal Jets)예요. 53판은 열을 더했어요. 듀크, 가르시아, 루츠가 그린 가우스 주기(Gaussian Periods), 부르가 보인 현수면과 나선면의 휨(Catenoid-Helicoid), 디랙의 벨트 묘기(Belt Trick), 헬름홀츠와 러브의 연기 고리 개구리뜀(Leapfrogging Vortices), 브룩스, 스미스, 스톤, 텃의 정사각형 분할(Squared Rectangle), 포드와 풀커슨의 최대 흐름 최소 절단(Max-Flow Min-Cut), 코스타스 배열(Costas Array), 바라니, 비어든, 칸의 무게중심 분할(Barycentric Subdivision), 무코파디아야와 크네저의 네 꼭짓점 정리(Four-Vertex Theorem), 오일러의 엘라스티카(Euler's Elastica)예요. 54판은 열을 더했어요. 베리의 무작위 파동과 보고몰니, 슈미트가 스미기로 센 마디 영역(Nodal Domains), 피어험버트가 사인 흐름에서 찾은 이상 고유 모드(Strange Eigenmode), 피어시 적분과 베리, 업스틸의 첨점 회절 파국(Pearcey Cusp), 나겔과 바이스의 STIT 모자이크(STIT Tessellation), 레비의 아크사인 법칙(Arcsine Law), 스퇴르메르가 다룬 쌍극자 장 속 대전 입자(Størmer Trap), 게일과 섀플리의 지연 수락(Stable Marriage), 스메일의 말굽(Smale Horseshoe), 울람 수열과 슈타이너베르거가 찾은 숨은 진동수(Ulam Sequence), 위어와 펠런의 거품과 켈빈의 깎은 팔면체(Weaire-Phelan)예요. 55판은 열을 더했어요. 밀러와 셰필드의 허수 기하(Imaginary Geometry), 티소의 지시 타원과 메르카토르 투영(Tissot's Indicatrix), 민코프스키의 물음표 함수(Question Mark Function), 버로스와 휠러의 변환(Burrows-Wheeler), 뷔퐁의 바늘과 바르비에의 국수(Buffon's Noodle), 부자의 리듬 타일링 캐넌(Vuza Canon), 하인과 내시의 헥스와 게일의 헥스 정리(Hex Theorem), 8자 고리의 범피복과 자유군의 케일리 그래프(Universal Cover), 오브친니코프와 젤도비치, 투생과 윌첵의 확산 제한 소멸(Toussaint-Wilczek), 헬름홀츠와 켈빈의 와류 시트와 크라스니의 와동 덩이(Vortex Sheet)예요. 56판은 열을 더했어요. 마테롱의 죽은 잎과 켄들, 퇴네스가 아래에서 본 완전 표본(Dead Leaves), 골롬의 트로미노 깔기(Golomb Trominoes), 윌슨의 고리 지운 걸음과 롤러, 슈람, 베르너의 균등 생성나무 페아노 곡선(Tree Peano Curve), 블로흐와 제너의 진동과 워니어의 사다리(Bloch Oscillations), 앤드루스의 곡선과 아시모프의 그랜드 투어(Andrews Curves), 지니브르와 기르코의 원 법칙과 좀머스, 크리산티, 솜폴린스키, 스타인의 타원 법칙(Elliptic Law), 에르되시의 불일치 문제와 보웨인, 최, 쿤스의 3진 수열(Erdős Discrepancy), 망델브로의 곱셈 폭포와 카안, 페이리에르의 문턱(Multiplicative Cascade), 코르테, 체이킨, 골럽, 파인의 무작위 조직화(Random Organization), 칼루가레아누, 화이트, 풀러의 꼬임과 뒤틀림(Twist and Writhe)이에요. +주제 5 로 쓴 수학과 다음에 해 볼 수학이에요. 28판이 처음 열 개를 썼어요. 슈람-뢰브너 진화(Loewner Trace), 포드 원(Ford Circles), 털 난 공 정리(Hairy Ball), 아즈텍 다이아몬드와 북극권(Aztec Diamond), 부니모비치 경기장(Stadium Billiard), 라우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 다면체와 E8(Gosset Polytope), 콘웨이의 보고 말하기 수열(Look and Say), 위토프 게임의 그런디 값(Wythoff Nim), 쌍곡 평면의 띠 모형(Hyperbolic Band)이에요. 29판은 여섯을 더했어요. 클라인의 j 불변량(J-Invariant), 호프 올뭉치(Hopf Fibration), 다이슨 브라운 운동(Dyson Gas), 가케야 바늘 문제의 페론 나무(Perron Tree), 임계선 위의 리만 제타 함수(Zeta Loops), 길버트-섀넌-리즈 리플과 베이어-디아코니스의 일곱 번(Seven Shuffles)이에요. 30판도 여섯을 더했어요. 세 간격 정리(Three Gaps), 호프스태터 나비와 TKNN 정수(Hofstadter Butterfly), 노이만의 외부 당구(Outer Billiards), 볼드의 탄도 퇴적과 KPZ 앞면(Ballistic Deposition), 아르놀트의 미앤더(Meander), 브로드벤트와 해머슬리의 스미기(Percolation)예요. 31판도 여섯을 더했어요. 마스킷 조각의 클라인 군 극한 집합(Indra's Pearls), 리틀우드 다항식의 근(Littlewood Roots), 고든의 가우스 해자(Gaussian Moat), 아르놀트의 고양이 사상(Cat Map), 자부스키와 크루스칼의 KdV 솔리톤(KdV Solitons), 보이 곡면(Boy's Surface)이에요. ROUND.md 의 「낯선 수학 예」 여덟 가운데 쓴 것은 슈람-뢰브너 진화와 아즈텍 다이아몬드, 모듈러 형식, 호프 올뭉치, 세 간격 정리, 외부 당구 여섯이었고, 31판이 리틀우드 다항식의 근과 클라인 군의 극한 집합을 써서 여덟을 다 썼어요. 32판도 여섯을 더했어요. 탤벗 효과와 양자 카펫(Talbot Carpet), 길브레스 추측의 표(Gilbreath Table), 원 사상의 아르놀트 혀(Arnold Tongues), 콘웨이와 라딘의 핀휠 타일링(Pinwheel Tiling), 윌킨슨과 윌렘센의 침입 스미기(Invasion Percolation), 울람의 문제와 해머슬리 과정(Hammersley Lines)이에요. 33판도 여섯을 더했어요. 에인절, 홀로이드, 로믹, 비라그의 무작위 정렬 네트워크(Sorting Network), 베르트랑의 역설(Bertrand Paradox), 펜로즈의 비출 수 없는 방(Penrose Room), 당들랭 구(Dandelin Spheres), 테이트의 보로메오 고리(Borromean Rings), 골드바흐 혜성(Goldbach Comet)이에요. 34판도 여섯을 더했어요. 톰슨 문제와 오일러가 남기는 다섯 이웃 열둘(Thomson Problem), 켈빈의 배 물결(Kelvin Wake), 요네야마가 낸 와다의 호수(Lakes of Wada), 샤이데거의 강 그물(Scheidegger Rivers), 모저의 소파 문제와 해머슬리의 소파(Moving Sofa), 크누스의 춤추는 링크로 푼 펜토미노 덮기(Pentomino Cover)예요. 35판도 여섯을 더했어요. 뢸로의 폭이 일정한 곡선(Constant Width), 렌니의 주차 문제(Rényi Parking), 카리의 비주기 왕 타일(Kari-Culik Tiles), 콜라코스키 수열(Kolakoski), 쿠라모토와 바톡토흐의 키메라 상태(Chimera State), 주콥스키 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했어요. 그레이슨의 정리가 말하는 곡선 단축 흐름(Curve Shortening), 헤우드의 일곱 색 토러스 지도(Heawood Torus), 슈바르츠 랜턴(Schwarz Lantern), 윌브러햄과 깁스의 현상(Gibbs Phenomenon), 타원 곡선의 현과 접선(Elliptic Chords), 지먼의 파국 기계(Catastrophe Machine)예요. 37판도 여섯을 더했어요. 포천의 쓸기 선 알고리즘(Fortune's Sweep), 앙투안의 목걸이(Antoine's Necklace), 카르만 소용돌이 길의 안정성(Kármán Street), 단단한 원판의 헥사틱 상(Hexatic Phase), 존슨-멜 쪽매와 아브라미 법칙(Johnson-Mehl), 쿠라모토-시바신스키 방정식(Kuramoto-Sivashinsky)이에요. 38판도 여섯을 더했어요. 앤더슨 국소화(Anderson Localization), 무어가 찾고 Chenciner 와 Montgomery 가 증명한 세 몸 문제의 8자 해(Figure-Eight Orbit), 예센과 빈트너, 에르되시의 베르누이 합성곱(Bernoulli Convolutions), 브라우어르의 고정점 정리를 증명하는 슈페르너 보조정리(Sperner's Lemma), 에겐베르거와 폴리아의 항아리(Pólya Urn), 뇌르고르의 무한 수열(Infinity Series)이에요. 39판도 여섯을 더했어요. 고든, 웹, 월퍼트의 등스펙트럼 북(Isospectral Drums), 폰 망골트의 꼴로 쓴 리만의 명시 공식(Explicit Formula), 요한 베르누이의 최단 강하선(Brachistochrone), 슈타이너의 포리즘(Steiner Chain), 모런 집단으로 그린 킹먼의 합류(Kingman Coalescent), 보르수크-울람 정리(Borsuk-Ulam)예요. 40판도 여섯을 더했어요. 앨더스의 브라운 연속 랜덤 나무(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 함수(Hydrogen Orbital), 젤도비치 근사의 팬케이크(Zeldovich Pancakes), 망델브로가 내다보고 롤러, 슈람, 베르너가 증명한 브라운 운동의 바깥 경계(Brownian Frontier), 복소 긴즈부르크-란다우 방정식의 결함 난류(Defect Turbulence), 루퍼트 왕자의 정육면체(Prince Rupert's Cube)예요. 41판은 다섯을 더했어요. 홀과 왜건의 현수선 길과 정다각형 바퀴(Square Wheels), 퐁슬레의 포리즘(Poncelet Porism), 슐레플리의 4차원 정다포체를 자른 단면(Polytope Slices), 클림켁과 티먼, 마그누손의 사이트스왑(Siteswap), Dudeney 의 잡화상 퍼즐과 월리스-보여이-게르비엔 정리(Hinged Dissection)예요. 42판은 다섯을 더했어요. 바른스도르프 규칙으로 도는 기사 여행(Knight's Tour), 페르미와 파스타, 울람, 칭구의 되돌아옴(FPUT Recurrence), 포슬리에-립킨 반전기(Peaucellier Linkage), 몰리의 삼등분선 정리(Morley Triangle), 랄과 마드라스, 소칼의 피벗 알고리즘으로 돌린 자기 회피 걸음(Pivot Walk)이에요. 43판은 다섯을 더했어요. 월리스 워커의 칼레이도사이클(Kaleidocycle), 쌍곡형에서 록소드롬형으로 감기는 뫼비우스 변환의 흐름(Möbius Loxodrome), 터트의 용수철 정리(Tutte Embedding), 디펀트와 크래비츠의 고리 정리를 품은 히토메자시 사시코(Hitomezashi), 룽게 현상과 체비쇼프 점(Runge Phenomenon)이에요. 44판은 다섯을 더했어요. 마크 카츠의 고리 모형(Kac Ring), 스톤과 튜키의 햄 샌드위치 정리(Ham Sandwich), 브란트의 불가리아 솔리테어(Bulgarian Solitaire), 엘스켄스와 프리시의 탄도 소멸(Ballistic Annihilation), 페레그린의 솔리톤(Peregrine Soliton)이에요. 45판은 다섯을 더했어요. 로건-셰프와 베르시크-케로프의 극한 곡선으로 다가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도브루신 경계 조건을 건 임계 이징 모형의 SLE(3) 경계(Ising Interface), 프리예제프와 동료들의 오일러 걷개로 쌓는 로터 라우터 응집과 레빈-페레스의 스매시 합(Rotor-Router), 폴리아의 과수원 문제(Pólya's Orchard), 넬슨의 평면 색칠 문제와 아이즈벨의 일곱 색(Hadwiger-Nelson)이에요. 46판은 다섯을 더했어요. 브릴루앙의 구역(Brillouin Zones), 모스 이론과 레브의 그래프(Reeb Graph), 로렌츠 기체와 시나이의 당구(Lorentz Gas), 리언 글래스의 점무늬(Glass Patterns), 벤치와 수테라, 불피아니의 확률 공명(Stochastic Resonance)이에요. 47판은 다섯을 더했어요. 오키프와 홍, 스트로가츠의 스워멀레이터(Swarmalators), 빌스의 외로운 달리기 선수 추측(Lonely Runner), 샤츠의 올로이드와 그 전개도(Oloid), 쾨베의 원 채우기 정리와 콜린스-스티븐슨의 반복(Koebe Packing), 콘웨이의 병사와 황금비 무게(Conway's Soldiers)예요. 48판은 다섯을 더했어요. 오하라의 매듭 뫼비우스 에너지(Möbius Energy), 체비쇼프의 편향과 루빈스타인-사르낙의 밀도(Chebyshev's Bias), 섀든과 레키엔, 마스든의 이중 소용돌이 능선(Double Gyre), 아자리와 프로스트의 깜빡이는 래칫(Flashing Ratchet), 망델브로의 프랙탈 스미기(Fractal Percolation)예요. 49판은 다섯을 더했어요. 뒤러의 전개 문제와 셰퍼드의 추측(Dürer's Unfolding), 베리와 골드버그가 재규격화한 세타 합의 꼬부랑 곡선(Curlicue), 오일러 방정식과 푸앵소의 폴호드로 본 테니스 라켓 정리(Tennis Racket), 켐프의 사슬과 네 색 정리(Kempe Chains), 퇴플리츠의 내접 정사각형 문제와 그린-로브의 직사각형(Square Peg)이에요. 50판은 다섯을 더했어요. 크레이치넌과 배철러의 2차원 난류가 거꾸로 가는 캐스케이드와 맥윌리엄스의 소용돌이(Inverse Cascade), 사포발과 로소, 구예의 확산 앞줄(Diffusion Front), 필리와 마르케티, 케이츠와 타이외르의 운동성 상분리(Motility Clusters), 박과 스네펜의 진화 모형(Bak-Sneppen), 리세의 영원한 가속(Risset Rhythm)이에요. 51판은 다섯을 더했어요. 스미스와 마이어스, 캐플런, 굿먼-스트라우스의 모자 타일과 H/T/P/F 메타타일 치환(Hat Monotile), 반도와 동료들의 최적 속도 모형과 스기야마의 원형 트랙 실험(Phantom Jam), 사토와 테이트의 법칙과 타원곡선의 a_p(Sato-Tate), 칼라비와 야우의 공간에서 핸슨이 잘라 그린 5차 페르마 곡선(Calabi-Yau Quintic), 에델스브루너와 레처, 조모로디언의 지속 호몰로지와 바코드(Persistence Barcode)예요. 52판은 다섯을 더했어요. 슈람과 셰필드가 등고선을 SLE(4) 로 보인 2차원 가우스 자유장(Gaussian Free Field), 당튀와 마즈무다르, 베링거의 광탄성 힘 사슬(Force Chains), 앤스컴의 사중주와 마테이카, 피츠모리스의 같은 통계 담금질(Anscombe's Quartet), 드 브라윈 수열과 BEST 정리(de Bruijn Sequence), 라인스의 베타 평면 띠 제트(Zonal Jets)예요. 53판은 열을 더했어요. 듀크, 가르시아, 루츠가 그린 가우스 주기(Gaussian Periods), 부르가 보인 현수면과 나선면의 휨(Catenoid-Helicoid), 디랙의 벨트 묘기(Belt Trick), 헬름홀츠와 러브의 연기 고리 개구리뜀(Leapfrogging Vortices), 브룩스, 스미스, 스톤, 텃의 정사각형 분할(Squared Rectangle), 포드와 풀커슨의 최대 흐름 최소 절단(Max-Flow Min-Cut), 코스타스 배열(Costas Array), 바라니, 비어든, 칸의 무게중심 분할(Barycentric Subdivision), 무코파디아야와 크네저의 네 꼭짓점 정리(Four-Vertex Theorem), 오일러의 엘라스티카(Euler's Elastica)예요. 54판은 열을 더했어요. 베리의 무작위 파동과 보고몰니, 슈미트가 스미기로 센 마디 영역(Nodal Domains), 피어험버트가 사인 흐름에서 찾은 이상 고유 모드(Strange Eigenmode), 피어시 적분과 베리, 업스틸의 첨점 회절 파국(Pearcey Cusp), 나겔과 바이스의 STIT 모자이크(STIT Tessellation), 레비의 아크사인 법칙(Arcsine Law), 스퇴르메르가 다룬 쌍극자 장 속 대전 입자(Størmer Trap), 게일과 섀플리의 지연 수락(Stable Marriage), 스메일의 말굽(Smale Horseshoe), 울람 수열과 슈타이너베르거가 찾은 숨은 진동수(Ulam Sequence), 위어와 펠런의 거품과 켈빈의 깎은 팔면체(Weaire-Phelan)예요. 55판은 열을 더했어요. 밀러와 셰필드의 허수 기하(Imaginary Geometry), 티소의 지시 타원과 메르카토르 투영(Tissot's Indicatrix), 민코프스키의 물음표 함수(Question Mark Function), 버로스와 휠러의 변환(Burrows-Wheeler), 뷔퐁의 바늘과 바르비에의 국수(Buffon's Noodle), 부자의 리듬 타일링 캐넌(Vuza Canon), 하인과 내시의 헥스와 게일의 헥스 정리(Hex Theorem), 8자 고리의 범피복과 자유군의 케일리 그래프(Universal Cover), 오브친니코프와 젤도비치, 투생과 윌첵의 확산 제한 소멸(Toussaint-Wilczek), 헬름홀츠와 켈빈의 와류 시트와 크라스니의 와동 덩이(Vortex Sheet)예요. 56판은 열을 더했어요. 마테롱의 죽은 잎과 켄들, 퇴네스가 아래에서 본 완전 표본(Dead Leaves), 골롬의 트로미노 깔기(Golomb Trominoes), 윌슨의 고리 지운 걸음과 롤러, 슈람, 베르너의 균등 생성나무 페아노 곡선(Tree Peano Curve), 블로흐와 제너의 진동과 워니어의 사다리(Bloch Oscillations), 앤드루스의 곡선과 아시모프의 그랜드 투어(Andrews Curves), 지니브르와 기르코의 원 법칙과 좀머스, 크리산티, 솜폴린스키, 스타인의 타원 법칙(Elliptic Law), 에르되시의 불일치 문제와 보웨인, 최, 쿤스의 3진 수열(Erdős Discrepancy), 망델브로의 곱셈 폭포와 카안, 페이리에르의 문턱(Multiplicative Cascade), 코르테, 체이킨, 골럽, 파인의 무작위 조직화(Random Organization), 칼루가레아누, 화이트, 풀러의 꼬임과 뒤틀림(Twist and Writhe)이에요. 57판도 열을 더했어요. 블룸의 중심축과 들불 변환(Medial Axis), 스메일과 윌리엄스의 솔레노이드(Solenoid), 솔로몬의 무작위 환경 속 걸음과 시나이의 걸음(Sinai's Walk), 테렐과 펜로즈의 회전(Terrell Rotation), 갈페린의 π 당구(Galperin Billiard), 페르마의 두 제곱수 정리와 자기에의 한 문장 증명(Zagier's Windmills), 스토크스 표류와 게르스트너의 트로코이드 파도(Stokes Drift), 라인하르트부터 맨, 매클라우드맨, 폰 데라우까지의 볼록 오각형 타일링(Pentagon Tilings), 페로와 하위헌스의 트랙트릭스와 자전거 바퀴 자국(Bicycle Tracks), 오스트발트 숙성과 LSW 이론(Ostwald Ripening)이에요. 목록 밖에서 다음에 해 볼 것이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -3206,6 +3271,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 56판에서 본 것이에요. 정리가 못 박는 수는 이번에도 패턴의 셈으로 지켜봤어요. Tree Peano Curve 는 나무 290그루에서 늘 LED 8192개를 한 번씩 지났고, Twist and Writhe 는 Lk 넷과 Coil 셋의 조합마다 두 모서리의 연결 적분이 정확히 Lk 였어요. Erdős Discrepancy 는 3진 수열의 합이 m = 600,000 까지 log3 m + 1 을 넘지 않았고, Bloch Oscillations 는 베셀 함수의 닫힌 꼴과 3×10⁻⁸ 안에서 맞았어요. Andrews Curves 는 그랜드 투어가 1분 도는 동안 곡선 사이 거리가 소수 셋째 자리까지 그대로였어요. 극한을 말하는 정리는 유한한 판에서 무른 문턱이 됐어요. Multiplicative Cascade 는 열세 번 나눈 판이 τ = 1 에서 갑자기 꺼지지 않고 Spread 를 따라 서서히 어두워졌고, Random Organization 의 원판 83개는 Strain 3 에서 판마다 멈추기도 하고 6,000주기를 넘기도 했어요. Elliptic Law 의 실수 고윳값 7.6 개도 큰 N 의 √(2N/π) 보다 τ 만큼 많아요. 재는 값이 밝기만 보는 것을 쓴 그림도 있어요. 가장자리는 가장 밝은 채널의 계단만 세서, Dead Leaves 는 밝기를 네 단에 묶고 색상만 흩어 가장자리 1 과 높은 복잡도를 같이 얻었어요. 고요한 패턴은 축의 아래 끝을 넓히기 쉬웠어요. Golomb Trominoes 의 첫 판은 움직임 0.014 로 그 축의 끝을 옮겨서 다른 칸 하나를 비울 뻔했어요. +57판에서 본 것이에요. 정리가 못 박는 수는 이번에도 패턴의 셈으로 지켜봤어요. Galperin Billiard 는 k = 5 까지 314159 번을 셌고, Zagier's Windmills 는 10^5 아래 소수 4,783개의 걸음이 다 p = x² + (2y)² 에서 끝났어요. Pentagon Tilings 는 다섯 형 모두 52만 4,288 표본에서 빈 점도 겹친 점도 없었고, Terrell Rotation 의 정육면체는 arcsin β 와 0.01도 안에서 돌아 보였어요. Bicycle Tracks 의 뒷바퀴는 원으로 감겨 드는 빠르기가 푸트의 뫼비우스 사상이 주는 0.00298 과 0.00305 로 맞았어요. 극한을 말하는 법칙은 패널의 크기에서 아직 오는 중이었어요. Sinai's Walk 의 |X_t|/(log t)² 는 10^7 걸음에서도 케스텐의 0.41 아래였고, 걷개는 시나이의 바닥보다 log t 로 2~3 늦게 따라갔어요. Ostwald Ripening 은 방울 백만 개에서 LSW 의 4/9 와 0.25% 안에서 맞았지만 패널의 200개는 0.57 이에요. 정리가 그리는 모양이 생각과 다르기도 했어요. Galperin Billiard 의 속도 평면에서 현은 별을 그리지 않고 모두 나란해서 원을 빗살로 쓸었고, Zagier's Windmills 의 가운데 경우는 바깥선을 거울에 비춰서 풍차의 도는 방향을 뒤집어야 칸까지 같은 바깥선이 됐어요. 작은 흔들림이 큰 구조를 바꾸는 것도 LED 한 칸 아래에서 보였어요. Medial Axis 는 5분의 1 LED 높이의 물결에 중심축의 끝이 3.5개에서 8.2개로 늘었어요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. @@ -3248,12 +3315,14 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 목록 밖에서 53판 뒤에 해 볼 것이에요. 극소곡면의 연관족은 엔네퍼 면과 셰르크 면에도 있어서, Catenoid-Helicoid 의 체커와 법선 셈으로 다른 극소곡면의 휨을 그릴 수 있어요. 벨트 묘기의 H(1, t) 를 3차원 구면의 입체 사영으로 옆에 찍으면 360° 에 −1 에 닿았다 720° 에 1 로 돌아오는 점이 두 겹 덮개를 바로 보여요. Leapfrogging Vortices 의 거울을 풀면 너비 비 0.382 아래에서 개구리뜀이 스스로 대칭을 깨고 흩어지는 애치슨(2000)의 불안정을 노브 하나로 보일 수 있어요. 정사각형 분할은 쌍대 그물을 돌리면 90도 돌아간 분할이 나와서, 두 그물을 반씩 섞어 직사각형이 옆으로 눕는 장면이 돼요. 최소 절단은 평면 그물에서 쌍대 그래프의 최단 경로라, 다익스트라 물결이 왼쪽 벽에서 오른쪽 벽으로 번지며 웅덩이 테두리와 같은 선을 긋는 것을 보일 수 있어요. 골롬 자는 코스타스 배열의 성질을 1차원에 둔 것이고, 차수 32 와 33 은 코스타스 배열이 하나도 알려지지 않은 가장 작은 차수예요. 무게중심 분할에서 둥근 자식을 고르는 확률의 문턱은 2/9 와 1/3 사이에 있어서, 그 사이를 잘게 재면 납작해지는 걷기와 둥글게 머무는 걷기가 갈리는 자리를 찾을 수 있어요. 분리선 바깥에서 넘어 도는 진자는 곡률 부호가 바뀌지 않는 비굴곡 엘라스티카라서, 노브로 분리선을 건너면 오일러의 1744년 그림의 아홉 갈래가 한 판에 이어져요. 공간 곡선의 네 꼭짓점 정리(세디흐, 1992)도 있어요. -목록 밖에서 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 의 셈으로 넓이를 견줄 수 있어요. +목록 밖에서 54판 뒤에 해 볼 것이에요. 보고몰니와 슈미트는 2007년에 뒤브르트랑과 함께 무작위 파동의 마디선이 SLE(6) 를 닮았다는 것을 수치로 보였어요. 패널을 가로지르는 마디선 하나를 따라가며 그 차원 7/4 를 재 볼 만해요. 확산을 0 으로 보낼 때 이상 고유 모드의 승수가 가는 곳은 폴리콧-뤼엘 공명(마크 폴리콧 1985, 다비드 뤼엘 1986)이라, 섭동한 고양이 사상 같은 단순한 사상에서 셈해 그릴 수 있어요. 피어시 다음의 회절 파국인 제비꼬리는 지수에 t⁵ 이 드는 적분이라 Pearcey Cusp 의 휜 적분 길을 그대로 써요. STIT 를 3차원 상자에서 돌려 평면으로 자른 단면도 평면 STIT 라서, 볼록 다면체로 갈라지는 3차원 STIT 의 단면을 천천히 밀어 올리는 그림이 될 만해요. 끝이 0 으로 묶인 브라운 다리에서는 0 위의 시간이 아크사인이 아니라 균등 분포를 따르고, 이산판은 카이 라이 정과 윌리엄 펠러의 정리(1949)예요. 묶은 걸음과 풀린 걸음을 나란히 세울 수 있어요. 토카막 장에서 갇힌 입자가 바나나 꼴로 오가는 바나나 궤도는 Størmer Trap 의 보리스 적분을 그대로 써요. 같은 목록의 안정 매칭 전체가 이루는 분배 격자는 회전을 하나씩 밟아 가는 그림이 돼요(로버트 어빙과 폴 리더, 1986). 합이 꼭 s 가지인 항을 고르는 크노의 s-가법 수열(1972)에도 같은 숨은 진동수가 있는지 세어 볼 수 있어요. 프랑크-카스퍼 상의 다른 거품인 C15 라베스 상은 십이면체와 십육면체로 지어서 Weaire-Phelan 의 셈으로 넓이를 견줄 수 있어요. 목록 밖에서 55판 뒤에 해 볼 것이에요. 같은 각도의 흐름선을 판 여러 점에서 띄우면 만나는 대로 합쳐져 나무가 되고, 그 나무를 따라 도는 곡선이 판을 다 메우는 SLE(16/κ) 예요. 밀러와 셰필드의 『Imaginary geometry IV』(2017)에 있어요. 같은 원들을 원통 대신 원뿔과 방위 투영에 얹으면 왜곡이 한 원이나 한 점 둘레로 모여서, 람베르트가 1772년에 낸 정각 원뿔 투영과 방위 등적 투영을 Tissot's Indicatrix 의 K4 로 오가게 해 볼 만해요. 콘웨이의 상자 함수(1976)는 물음표 함수를 뒤집은 꼴이라 같은 계단을 눕혀 실수 전체로 늘려 볼 수 있고, 슈테른의 이원자 수열(모리츠 슈테른, 1858)도 128줄을 채우는 산맥이 될 만해요. 천과 폭스, 린던의 린던 분해(1958)는 글을 사전순으로 커지지 않는 린던 단어들로 자르는 것이라, 조각마다 색이 다른 띠로 그려 볼 만해요. 크로프턴 공식(모건 크로프턴, 1868)은 Buffon's Noodle 과 같은 셈으로, 무작위 직선이 곡선을 건너는 수로 곡선의 길이를 재요. 리듬 타일링의 다른 끝은 푸글레드의 스펙트럼 추측(벤트 푸글레드, 1974)이고, 타오(2004)가 5차원에서 반례를 찾았어요. 삼각 격자 스미기에서 사각형을 가로지를 확률은 카디의 공식(존 카디, 1992)을 따르고 스미르노프(2001)가 증명해서, Hex Theorem 의 판 모양을 바꾸며 건넌 횟수를 세면 공식의 곡선에 다가가요. 자유군의 부분군은 8자 고리의 유한 피복이고, 스톨링스(1983)의 접기가 생성 낱말의 꽃잎 고리에서 같은 글자의 변을 하나씩 접어 그 피복을 만들어요. A + B → 0 에 입자를 끊임없이 고르게 넣어도 정상 상태에서 영역이 갈라져서(린덴버그, 웨스트, 코펠만, 1988), 영역이 패널 크기까지 퍼진 채 흔들리는 판을 세울 수 있어요. 한 주기에 두 파장을 넣고 반 파장 섭동을 더하면 이웃 나선이 서로 돌다 하나로 합치는 짝짓기(위넌트와 브라우언드, 1974)가 나와요. 목록 밖에서 56판 뒤에 해 볼 것이에요. 잎을 반투명하게 겹치는 투명 죽은 잎 모형(브뤼노 갈른, 얀 구소, 2012)은 잎이 투명해질수록 가우스 장으로 다가가서, Dead Leaves 의 셈에 노브 하나로 두 그림 사이를 오갈 수 있어요. 골롬의 렙타일(1964)로 L 트로미노를 같은 꼴 넷으로 거듭 나누면 의자 타일링이 돼요. 같은 생성나무를 해럴드 템펄리(1974)의 전단사로 도미노 타일링으로 옮겨 그려 볼 만해요. 힘을 교류로 흔들면 세기와 진동수의 비가 J_0 의 영점에 놓일 때 묶음이 퍼지지 않고 제자리에 묶여요. 데이비드 던랩과 V. M. 켄크리(1986)의 동적 국소화예요. 알프레드 인셀버그(1985)의 평행 좌표는 다차원 점을 세로축 여러 개를 잇는 꺾은선으로 그리고, 한 직선 위의 점들은 이웃한 두 축 사이에서 한 점을 지나요. 하타노-넬슨 모형(하타노 나오미치, 데이비드 넬슨, 1996)은 한쪽으로 기운 뜀을 넣은 무질서 사슬이라, 고윳값이 실수축에서 떨어져 나와 고리를 그려요. 리우빌 함수의 합이 2 이상에서 늘 0 이하라는 포여의 추측(조지 포여, 1919)은 C. 브라이언 해즐그로브(1958)가 깼고, 처음 양수가 되는 n = 906,150,257 까지 가는 길을 그려 볼 만해요. 이진 칸 없이 로그 상관 가우스장의 지수로 같은 다중 프랙탈을 만드는 장피에르 카안(1985)의 가우스 곱셈 혼돈도 있어요. 무작위 조직화의 임계점에서는 입자 배치가 초균일해져서(대니얼 헥스너, 도브 레빈, 2015), 큰 상자의 밀도 요동으로 그릴 수 있어요. 미첼 불안정성(존 헨리 미첼, 1889)은 꼬인 탄성 고리가 꼬임이 문턱을 넘을 때까지 납작하게 버티다가 한순간 평면 밖으로 휘는 일이에요. +목록 밖에서 57판 뒤에 해 볼 것이에요. 블룸의 중심축 대신 다각형의 변을 같은 빠르기로 평행하게 밀어 들이는 직선 골격(오스빈 아이히홀처, 프란츠 아우렌하머와 두 사람, 1995)은 포물선 조각 없이 선분만으로 된 나무를 남겨요. θ 를 세 배로 늘리는 3진 솔레노이드는 단면이 세 갈래 칸토어 집합이 되고, 구멍 셋 뚫린 원판 위의 플리킨 끌개(1974)도 같은 관 그림으로 그릴 수 있어요. 장필리프 부쇼(1992)의 덫 모형은 자리마다 무작위 깊이의 덫에서 걷개가 지수 분포 시간만큼 머물다 떠나서, 오래 기다린 걷개일수록 더 오래 머무는 노화가 패널에서 보일 거예요. 갈페린의 두 블록은 꼭지각 arctan √(m/M) 의 쐐기 당구와 같아서, 쐐기를 거울로 펼쳐 공의 길이 곧은 선 하나가 되는 것을 보여 줄 수 있어요. 존 콘웨이의 지형(1997)으로 x² + y² 의 값을 평면을 나누는 나무 위에 펼치면, 두 제곱수의 합으로 쓰이는 수들이 나무를 따라 자라는 것이 보여요. 갑자기 멈춘 전하의 전기력선이 빛의 속도로 퍼지는 껍질에서 꺾이는 J. J. 톰슨(1904)의 그림도 빛의 지연이 주인공이라 세운 패널에 맞고, 에드워드 퍼셀(1965)이 교과서에서 복사를 이 꺾임으로 설명했어요. 스토크스 표류가 바람이 만든 전단과 만나면 물속에 나란한 회전 띠가 서는 랭뮤어 순환(어빙 랭뮤어, 1938; 크레이크와 레이보비치의 식, 1976)을 단면으로 그려 볼 수 있어요. 라이스의 9형도 매개변수 하나로 모양이 바뀌어 1형처럼 모프로 보여 줄 수 있어요. 앞바퀴가 볼록한 닫힌 고리를 돌 때 그 넓이가 πL² 보다 크면 뒷바퀴를 끌어들이는 고리가 생긴다는 멘진의 추측(1906)은 마크 레비와 세르게이 타바치니코프(2009)가 증명해서, 타원 고리의 넓이를 노브에 걸면 뒷바퀴가 감겨 들다 문턱 아래에서 떠도는 것으로 바뀌는 순간이 보일 거예요. LSW 의 극한 분포가 처음 분포의 꼬리 모양에 따라 달라진다는 바르바라 니트하머와 로버트 페고(1999)의 결과는, 처음 반지름 분포의 꼬리를 노브에 걸어 늦은 분포가 LSW 곡선에서 벗어나는 것으로 보여 줄 수 있어요. + ### 화장품 팝업 주제 6 으로 쓴 질감과 다음에 해 볼 질감이에요. 29판이 처음 넷을 썼어요. 팔레트 나이프로 펴 바르는 크림(Cream Swirl), 구슬이 뜬 젤과 빛 그물(Serum Gel), 쏟아져 쌓이는 글리터(Glitter Fall), 퍼프가 눌렀다 든 압축 파우더(Loose Powder)예요. 넷 다 K1 이 색상환 한 바퀴라서, 색을 돌리면 주황과 노랑이 갈색이나 올리브로 보이는 자리가 있었어요. Serum Gel 은 그 둘레에서만 밝기를 0.62 와 0.3 에서 0.92 와 0.68 로 올렸어요. 밝은 바탕은 반짝임을 지웠어요. 200 위에 55 를 더한 반짝임은 거의 안 보여서 Glitter Fall 은 어두운 바탕에 섰고, 흰 바탕과 금색 한 줄은 Loose Powder 의 아래 띠로만 썼어요. 크림과 젤은 가장자리 0.028 과 0.079 로 연구에서 가장 번지는 그림이에요. 그 둘이 가장자리 축의 아래 끝을 옮겨서 지도에 새 빈 칸이 났어요. 크림은 빛만 움직여서는 움직임이 0.019 에 머물렀어요. 결이 깊어야 빛이 돌 때 바뀌는 LED 가 생겨요. 비누막이 터질 때 구멍 가장자리가 테일러와 컬릭의 속도(1959, 1960)로 고르게 벌어지며 물방울 테를 남기는 것은 Soap Film 의 끝을 늘인 그림이에요. 클렌징 오일이 물을 만나 가장자리부터 우윳빛으로 유화되는 것은 Blotting Paper 처럼 LED 마다 한 번만 바뀌는 셈으로 그릴 만해요. From 095f7b11c9fc8f43decab68820f1ef3067a252fc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 09:25:58 +0000 Subject: [PATCH 10/10] =?UTF-8?q?57=ED=8C=90:=20index.html=20=EC=9D=84=204?= =?UTF-8?q?78=EA=B0=9C=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20=EC=A7=93=EB=8A=94?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XoJSXx5srrfeJ2DPWFv6K5 --- incoming/pattern-study/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index 6a466d5fd..e0af6dfe4 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

패턴 연구

- - + +