From d633b122855fc000d528db5c477571d4a9e72ee5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 22:22:17 +0000 Subject: [PATCH 01/16] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=EC=97=B0=EA=B5=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=2040?= =?UTF-8?q?=ED=8C=90=20=ED=8C=A8=ED=84=B4=EC=9D=B4=20=EC=84=A0=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_01CLauBYuJBXD9L9NRJjxCqK From cb3016fbf540c5ab5e9d45c4e6b886ec829cf996 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 22:52:15 +0000 Subject: [PATCH 02/16] =?UTF-8?q?Hydrogen=20Orbital=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9D=84=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 --- .../patterns/hydrogen-orbital.js | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 incoming/pattern-study/patterns/hydrogen-orbital.js diff --git a/incoming/pattern-study/patterns/hydrogen-orbital.js b/incoming/pattern-study/patterns/hydrogen-orbital.js new file mode 100644 index 000000000..4f7dd0491 --- /dev/null +++ b/incoming/pattern-study/patterns/hydrogen-orbital.js @@ -0,0 +1,262 @@ +// ===== Patternflow pattern ===== +// Title: Hydrogen Orbital +// Family: physics +// Idea: Thousands of points drawn at random from the probability density of one hydrogen eigenstate stand as a cloud with its z axis up the panel and turn slowly about it, each point one LED that never touches another lit LED, coloured by the sign of the real wavefunction so the nodal planes, cones and spheres stay dark between the lobes. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: hydrogen atom eigenfunctions (Erwin Schrödinger, 1926), pattern round 40 +// Round: 40 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Orbital=1..12, Slab=0..1 +// +// Knob 1: Hue. Turns the two sign colours round the wheel together, always opposite each other. At 0 the positive lobes are amber and the negative ones azure. +// Knob 2: Speed. How fast the cloud turns about its vertical axis: at 2 once in 16 s, at 0.2 once in 160 s, at 3 once in 11 s. +// Knob 3: Orbital. Walks twelve real orbitals (n, l, m): 5g xz3 (5,4,1), 3d z2 (3,2,0), 4f xz2 (4,3,1), 5g z4 (5,4,0), 4d z2 (4,2,0), 6g z4 (6,4,0), 4f z3 (4,3,0), 6h xz4 (6,5,1), 5f z3 (5,3,0), 5p z (5,1,0), 7i z6 (7,6,0) and 6f xz2 (6,3,1). The ones with m = 0 look the same from every side, so only their points stream round as they turn; the xz ones open into a flower of 2l lobes when their xz plane faces us and close into a stack of lobes and rings a quarter turn later. +// Knob 4: Slab. How deep a slab of the cloud is kept, through the nucleus and facing us, so the near side of a ring does not hide the lobes inside it. For 5g xz3 the cloud is 107 LEDs deep: at 0 a slice 9 LEDs thick, where each of the n - l - 1 radial nodes shows as a dark ring and each nodal cone or plane as dark lines through the centre; at 0.06 a slab 33 LEDs thick; at 1 the whole cloud. + +const W = 64, H = 128; +// n, l, m of each orbital; m > 0 takes cos(m phi), m < 0 sin(|m| phi). +const ORB = [ + [5, 4, 1], [3, 2, 0], [4, 3, 1], [5, 4, 0], [4, 2, 0], [6, 4, 0], + [4, 3, 0], [6, 5, 1], [5, 3, 0], [5, 1, 0], [7, 6, 0], [6, 3, 1], +]; +const MAXP = 16000, NR = 2048, NU = 1024; +// The share of LEDs the cloud keeps lit: the number of points follows it. +const LIT = 0.2; +// The points fill the boundary surface that holds this share of the +// probability; the faint halo outside it is left out. +const KEEP = 0.97; +// Opening hue of the positive lobes: amber. +const H0 = 0.09; +const { floor, min, max, abs, sqrt, exp, sin, cos, round, random, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +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] = [v, q, p, p, t, v][i]; + out[1] = [t, v, v, q, p, p][i]; + out[2] = [p, p, t, v, v, q][i]; +} + +// Generalized Laguerre polynomial L_k^a(x) by its three-term recurrence. +function laguerre(k, a, x) { + if (k === 0) return 1; + let l0 = 1, l1 = 1 + a - x; + for (let j = 1; j < k; j++) { + const l2 = ((2 * j + 1 + a - x) * l1 - (j + a) * l0) / (j + 1); + l0 = l1; + l1 = l2; + } + return l1; +} + +// Associated Legendre function P_l^m(u), m >= 0, by the recurrence in l. +function legendre(l, m, u) { + let pmm = 1; + const s = sqrt(max(0, 1 - u * u)); + for (let j = 1; j <= m; j++) pmm *= -(2 * j - 1) * s; + if (l === m) return pmm; + let p1 = u * (2 * m + 1) * pmm; + if (l === m + 1) return p1; + let p0 = pmm; + for (let k = m + 2; k <= l; k++) { + const p2 = ((2 * k - 1) * u * p1 - (k + m - 1) * p0) / (k - m); + p0 = p1; + p1 = p2; + } + return p1; +} + +// The radial part R_nl up to a constant: rho^l e^(-rho/2) L_(n-l-1)^(2l+1)(rho), +// rho = 2r/n, with r in Bohr radii. +function radial(n, l, r) { + const rho = (2 * r) / n; + return rho ** l * exp(-rho / 2) * laguerre(n - l - 1, 2 * l + 1, rho); +} + +// Invert a cumulative table of N bins on [a, b] at a uniform draw q. +function invert(c, N, a, b, q) { + let lo = 0, hi = N; + while (hi - lo > 1) { + const mid = (lo + hi) >> 1; + if (c[mid] <= q) lo = mid; + else hi = mid; + } + const f = (q - c[lo]) / max(1e-12, c[hi] - c[lo]); + return a + ((lo + f) / N) * (b - a); +} + +// One point of |psi|^2, in Bohr radii: out = x, y, z, sign of psi, |psi|^2. +// |psi|^2 r^2 dr du dphi splits into r^2 R^2 in r, P_l^m(u)^2 in u = cos(theta) +// and cos^2(m phi) in phi, so each is drawn on its own. +function draw1(O, out) { + const { n, l, m } = O; + const r = invert(O.rc, NR, 0, O.rmax, random()), u = invert(O.uc, NU, -1, 1, random()); + let ph = random() * 2 * PI, tr = 1; + if (m !== 0) { + for (;;) { + tr = m > 0 ? cos(m * ph) : sin(-m * ph); + if (random() < tr * tr) break; + ph = random() * 2 * PI; + } + } + const s = sqrt(max(0, 1 - u * u)), psi = radial(n, l, r) * legendre(l, abs(m), u) * tr; + out[0] = r * s * cos(ph); + out[1] = r * s * sin(ph); + out[2] = r * u; + out[3] = psi >= 0 ? 1 : -1; + out[4] = psi * psi; +} + +// Tables of the cumulative distribution in r and in u, the boundary surface, +// and the scale that stands the cloud on the panel. +function build(o) { + const [n, l, m] = ORB[o]; + const rmax = 2.5 * n * n + 8 * n + 10, rc = new Float64Array(NR + 1), uc = new Float64Array(NU + 1); + for (let i = 0; i < NR; i++) { + const r = ((i + 0.5) / NR) * rmax, R = radial(n, l, r); + rc[i + 1] = rc[i] + r * r * R * R; + } + for (let i = 0; i < NU; i++) { + const P = legendre(l, abs(m), -1 + (2 * (i + 0.5)) / NU); + uc[i + 1] = uc[i] + P * P; + } + for (let i = 1; i <= NR; i++) rc[i] /= rc[NR]; + for (let i = 1; i <= NU; i++) uc[i] /= uc[NU]; + const O = { n, l, m, rmax, rc, uc, scale: 1, depth: 1, c: 0 }; + // Points come with density |psi|^2, so the share of them above a level c + // is the probability inside the surface |psi|^2 = c. + const p = [0, 0, 0, 0, 0], d2 = new Float64Array(8000), zs = [], hs = [], rs = []; + for (let i = 0; i < d2.length; i++) { + draw1(O, p); + d2[i] = p[4]; + } + d2.sort(); + O.c = d2[floor((1 - KEEP) * d2.length)]; + for (let i = 0; i < 3000; i++) { + do draw1(O, p); + while (p[4] < O.c); + zs.push(abs(p[2])); + hs.push(sqrt(p[0] * p[0] + p[1] * p[1])); + rs.push(sqrt(p[0] * p[0] + p[1] * p[1] + p[2] * p[2])); + } + // 199 points in 200 within 62 LEDs up or down, and within 48 either side: + // a wide orbital runs a little past the edges rather than shrink. + const q = (a) => a.sort((x, y) => x - y)[floor(0.995 * a.length)]; + O.scale = min(62 / q(zs), 48 / q(hs)); + O.depth = q(rs); + return O; +} + +export function setup(params) { + params.s = { + o: -1, O: null, np: 5000, ang: -0.6, ca: 1, sa: 0, slab: 1, begun: false, + x: new Float32Array(MAXP), y: new Float32Array(MAXP), z: new Float32Array(MAXP), sg: new Int8Array(MAXP), life: new Float32Array(MAXP), + sx: new Float32Array(MAXP), dz: new Float32Array(MAXP), + zb: new Float32Array(W * H), site: new Int8Array(W * H), tmp: [0, 0, 0, 0, 0], + pos: [0, 0, 0], neg: [0, 0, 0], hue: 0, speed: 2, lit: 0, + }; +} + +// The view, turned by ang about z: screen x, and depth toward us. +function project(S, i) { + const x = S.x[i], y = S.y[i]; + S.sx[i] = x * S.ca - y * S.sa; + S.dz[i] = -(x * S.sa + y * S.ca); +} + +// A fresh point inside the boundary surface, drawn again until it lies in the slab. +function renew(S, i) { + const t = S.tmp, O = S.O; + for (let k = 0; k < 80; k++) { + draw1(O, t); + if (t[4] < O.c) continue; + S.x[i] = t[0]; + S.y[i] = t[1]; + S.z[i] = t[2]; + S.sg[i] = t[3]; + project(S, i); + if (abs(S.dz[i]) < S.slab) break; + } + S.life[i] = 1.5 + 3 * random(); +} + +// Each point to the nearest LED with x + y even, so no two lit LEDs are side +// by side; where several land, the nearest to us shows. +function sites(S) { + const { zb, site, sx, dz, z, sg, O } = S, np = round(S.np), k = O.scale; + zb.fill(-1e9); + site.fill(0); + let lit = 0; + for (let i = 0; i < np; i++) { + const fx = 32 + sx[i] * k, fy = 64 - z[i] * k; + let ix = round(fx), iy = round(fy); + if ((ix + iy) & 1) { + const ex = fx - ix, ey = fy - iy; + if (abs(ex) > abs(ey)) ix += ex > 0 ? 1 : -1; + else iy += ey > 0 ? 1 : -1; + } + if (ix < 0 || iy < 0 || ix >= W || iy >= H) continue; + const p = iy * W + ix; + if (!site[p]) lit++; + if (dz[i] > zb[p]) { + zb[p] = dz[i]; + site[p] = sg[i]; + } + } + S.lit = lit; + // The number of points follows the share of lit LEDs. + S.np = cl(S.np * ((LIT * W * H) / max(1, lit)) ** 0.4, 400, MAXP); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 1, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + const o = round(cl(k[2], 1, ORB.length)) - 1; + if (o !== S.o) { + S.o = o; + S.O = build(o); + S.begun = false; + } + S.slab = S.O.depth * (0.08 + 0.92 * sqrt(cl(k[3], 0, 1))); + S.ang += (d * S.speed * PI) / 16; + S.ca = cos(S.ang); + S.sa = sin(S.ang); + if (!S.begun) { + for (let i = 0; i < MAXP; i++) renew(S, i); + for (let j = 0; j < 8; j++) sites(S); + S.begun = true; + } + for (let i = 0; i < MAXP; i++) { + S.life[i] -= d; + project(S, i); + if (S.life[i] < 0 || abs(S.dz[i]) >= S.slab) renew(S, i); + } + sites(S); +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.O) return; + const { zb, site, pos, neg } = S; + const h = S.hue + H0; + hsv(h, 0.72, 1, pos); + hsv(h + 0.5, 0.72, 1, neg); + const D = S.slab; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, s = site[p]; + if (!s) { + display.setPixel(x, y, 0, 0, 0); + continue; + } + // Nearer points brighter, the back of the slab at 0.7. + const v = 0.7 + 0.3 * cl((zb[p] / D + 1) / 2, 0, 1), c = s > 0 ? pos : neg; + display.setPixel(x, y, round(255 * c[0] * v), round(255 * c[1] * v), round(255 * c[2] * v)); + } +} From 384864d33396d058d0ed0dddbd7c10f407c15670 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 23:06:32 +0000 Subject: [PATCH 03/16] =?UTF-8?q?Continuum=20Random=20Tree=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=EC=9D=84=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 --- .../patterns/continuum-random-tree.js | 496 ++++++++++++++++++ 1 file changed, 496 insertions(+) create mode 100644 incoming/pattern-study/patterns/continuum-random-tree.js diff --git a/incoming/pattern-study/patterns/continuum-random-tree.js b/incoming/pattern-study/patterns/continuum-random-tree.js new file mode 100644 index 000000000..0bdb4c843 --- /dev/null +++ b/incoming/pattern-study/patterns/continuum-random-tree.js @@ -0,0 +1,496 @@ +// ===== Patternflow pattern ===== +// Title: Continuum Random Tree +// Family: fractal +// Idea: Aldous's Brownian continuum random tree stands on its root at the bottom of the panel, coded by a normalised Brownian excursion and grown by line-breaking one uniform leaf at a time, a white glow climbing each new branch; once grown, the view dives into one of its subtrees, which stretched by Brownian scaling is again a continuum random tree, and the growth goes on inside it. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Brownian continuum random tree (David Aldous, 1991), pattern round 40 +// Round: 40 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Dive=0..1, Cut=0..1 +// +// Knob 1: Hue. Turns the tree round the wheel: at 0 azure crowns on pale stems with green and violet limbs either side, at 0.25 magenta, 0.5 apricot, 0.75 green. The subtrees above the cut always spread over half the wheel from left to right. +// Knob 2: Speed. At 2 the tree grows 1.6 units of length a second, 60 to 120 rows of branch depending on how tall the tree stands, the dive takes 3.2 s and the two trees cross-fade for 0.7 s, so a dive comes every 9 to 10 s at Dive 0. At 0.2 all of it ten times slower, at 3 half as fast again. +// Knob 3: Dive. The mass of the subtree the view dives into: 0.3 of the tree at 0, 0.13 at 0.5, 0.06 at 1. Its time is stretched by one over the mass (3.3 to 17 times) and its heights by one over the square root (1.8 to 4 times), Brownian scaling, and what fills the panel is again a tree of mass 1. Deeper dives keep fewer leaves, so the new tree starts sparser and grows longer before the next dive. +// Knob 4: Cut. A level at 20% of the height at 0 and 90% at 1 (24% at the start). Every subtree above it gets its own hue, and below it the tree is pale. Turned up, the one crown splits into more and smaller trees, each of them a continuum random tree in its own right. +// Buttons: K1 tap dives now, once the tree holds 30 leaves. + +const W = 64, H = 128, N = 8192, NL = 256; +// The root sits on row 126.5 and the highest point of the tree 122 rows above. +const ROWS = 122, Y0 = 126.5; +// Layout: a branch stands over the middle of its run, followed from the run +// below by EASE of the way, at most SLOPE LEDs sideways per level. +const SLOPE = 0.5, EASE = 0.12; +// The spanned tree grows LEND long (in units of the tree, where the height +// of a uniform point is Rayleigh), VGROW a second at Speed 2; then the view +// dives into a subtree for DIVE seconds and the two trees cross-fade for FADE. +const LEND = 24, VGROW = 1.6, DIVE = 3.2, FADE = 0.7; +// Opening hue: azure. +const H0 = 0.58; +const { floor, ceil, min, max, abs, sqrt, log, exp, cos, round, random, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ease = (u) => u * u * (3 - 2 * u); + +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); + if (i === 0) (out[0] = v), (out[1] = t), (out[2] = p); + else if (i === 1) (out[0] = q), (out[1] = v), (out[2] = p); + else if (i === 2) (out[0] = p), (out[1] = v), (out[2] = t); + else if (i === 3) (out[0] = p), (out[1] = q), (out[2] = v); + else if (i === 4) (out[0] = t), (out[1] = p), (out[2] = v); + else (out[0] = v), (out[1] = p), (out[2] = q); +} + +function gauss() { + let u = 0; + while (u === 0) u = random(); + return sqrt(-2 * log(u)) * cos(2 * PI * random()); +} + +// A normalised Brownian excursion e on N steps, kept as heights 2e: the +// Vervaat transform of a Gaussian bridge (cut the bridge at its minimum and +// swap the two pieces), scaled by 2 / sqrt(N). Height 2e(t) is the distance +// from the root to the point of the tree at time t. +function excursion(S) { + const E = new Float32Array(N + 1), Z = S.tmp; + let w = 0; + Z[0] = 0; + for (let k = 1; k <= N; k++) Z[k] = w += gauss(); + let m = 0; + for (let k = 0; k <= N; k++) { + Z[k] -= (k / N) * w; + if (k < N && Z[k] < Z[m]) m = k; + } + const s = 2 / sqrt(N); + for (let j = 0; j <= N; j++) E[j] = (Z[(m + j) % N] - Z[m]) * s; + E[0] = E[N] = 0; + return E; +} + +// The subtree over run [a, b] at height hs, as an excursion of its own: +// shifted down by hs, stretched in time by N / n and in height by +// sqrt(N / n), Brownian scaling, so that it is again a normalised one. The +// samples in between get the Brownian bridges they lacked. +function subExcursion(S, T, g) { + const n = g.b - g.a + 2, k = sqrt(N / n), E = T.E, F = new Float32Array(N + 1), Z = S.tmp; + const C = (q) => (q <= 0 || q >= n ? 0 : (E[g.a - 1 + q] - g.hs) * k); + Z[0] = 0; + for (let j = 1; j <= N; j++) Z[j] = Z[j - 1] + (2 * gauss()) / sqrt(N); + const Zat = (q) => { + const u = (q * N) / n, j = min(N - 1, floor(u)), f = u - j; + return Z[j] * (1 - f) + Z[j + 1] * f; + }; + let q0 = -1, za = 0, zb = 0; + for (let j = 0; j <= N; j++) { + const u = (j * n) / N, q = min(n - 1, floor(u)), f = u - q; + if (q !== q0) { + za = Zat(q); + zb = Zat(q + 1); + q0 = q; + } + F[j] = abs(C(q) * (1 - f) + C(q + 1) * f + Z[j] - za * (1 - f) - zb * f); + } + F[0] = F[N] = 0; + return F; +} + +// The runs of {E >= level} at each of NL levels. Each run is a vertex of the +// tree at that height, and its samples are the part of the tree above it: +// start a, end b, highest point top, parent run par one level down, and fx, +// where the branch is drawn. The follow keeps the order of the runs, so no +// two subtrees cross. +// Built a few levels a frame while the view dives, n at a time. +const RA = new Int32Array(N + 2), RB = new Int32Array(N + 2), RT = new Float32Array(N + 2); +function build(T, count) { + const E = T.E, out = T.lv, cs = (SLOPE * N) / W, end = min(NL, out.length + count); + for (let s = out.length; s < end; s++) { + const h = s * T.dh, a = RA, b = RB, top = RT; + let start = -1, mx = 0, n = 0; + for (let j = 0; j <= N; j++) { + const v = E[j]; + if (v >= h) { + if (start < 0) { + start = j; + mx = v; + } else if (v > mx) mx = v; + } else if (start >= 0) { + a[n] = start; + b[n] = j - 1; + top[n++] = mx; + start = -1; + } + } + if (start >= 0) { + a[n] = start; + b[n] = N; + top[n++] = mx; + } + const fx = new Float32Array(n), par = new Int32Array(n); + if (s === 0) for (let q = 0; q < n; q++) fx[q] = (a[q] + b[q]) / 2; + else { + const P = out[s - 1]; + let p = 0; + for (let q = 0; q < n; q++) { + while (P.b[p] < a[q]) p++; + par[q] = p; + fx[q] = P.fx[p] + cl(EASE * ((a[q] + b[q]) / 2 - P.fx[p]), -cs, cs); + } + } + out.push({ a: a.slice(0, n), b: b.slice(0, n), top: top.slice(0, n), fx, par }); + } +} + +// Index of the run at level s that holds sample i (or -1). +function runOf(T, s, i) { + const L = T.lv[s], a = L.a; + let lo = 0, hi = a.length - 1, f = -1; + while (lo <= hi) { + const m = (lo + hi) >> 1; + if (a[m] <= i) { + f = m; + lo = m + 1; + } else hi = m - 1; + } + return f >= 0 && L.b[f] >= i ? f : -1; +} + +function tree(E, n) { + let mh = 0; + for (let j = 0; j <= N; j++) if (E[j] > mh) mh = E[j]; + const T = { E, maxH: mh, dh: mh / (NL - 1), lv: [], leaves: [], sorted: [], used: new Uint8Array(N + 1), L: 0, cur: null, prog: 0 }; + build(T, n); + return T; +} + +// Checked against the theorem: over 12,000 uniform points the distance to +// the root 2e(U) exceeds h with probability within 0.01 of exp(-h^2/2) at +// every h from 0.5 to 3 (Rayleigh, mean 1.245 against 1.253), and so does a +// subtree after the dive's stretching. The spanned tree of 40 leaves is 8.44 +// long on average; adding the grid's missed minima, 2 x 0.583 / sqrt(N) a +// branch, gives 8.95 against line-breaking's E[C_40] = 8.92. +// +// Aldous's line-breaking, read off the excursion: a new uniform leaf i +// joins the tree spanned by the root and the leaves so far at the higher of +// the two minima between i and its sampled neighbours, and the branch from +// there up to i is the next segment. Its length is the next gap of the +// line-breaking clock. +function addLeaf(T, i) { + if (i === undefined) { + i = 1 + floor(random() * (N - 1)); + while (T.used[i]) i = 1 + floor(random() * (N - 1)); + } + if (T.used[i]) return null; + T.used[i] = 1; + const so = T.sorted, E = T.E; + let lo = 0, hi = so.length; + while (lo < hi) { + const m = (lo + hi) >> 1; + if (so[m] < i) lo = m + 1; + else hi = m; + } + let b = 0; + if (lo > 0) { + let mn = Infinity; + for (let j = so[lo - 1]; j <= i; j++) if (E[j] < mn) mn = E[j]; + b = max(b, mn); + } + if (lo < so.length) { + let mn = Infinity; + for (let j = i; j <= so[lo]; j++) if (E[j] < mn) mn = E[j]; + b = max(b, mn); + } + so.splice(lo, 0, i); + const h = E[i], sb = floor(b / T.dh), st = min(NL - 1, floor(h / T.dh)); + // The path from the branch point up to i: at each level the run that + // holds i, where it is drawn and the mass of the tree above it. + const n = st - sb + 1, xs = new Float32Array(n + 1), ms = new Float32Array(n); + for (let s = sb; s <= st; s++) { + const q = runOf(T, s, i), L = T.lv[s]; + xs[s - sb] = L.fx[q]; + ms[s - sb] = (L.b[q] - L.a[q] + 1) / N; + } + const cs = (SLOPE * N) / W; + xs[n] = xs[n - 1] + cl(EASE * (i - xs[n - 1]), -cs, cs); + const leaf = { i, b, h, sb, st, xs, ms }; + T.leaves.push(leaf); + T.L += h - b; + return leaf; +} + +// Grow the tree by length d: the current branch climbs from its branch +// point, and when it reaches its leaf the next leaf is drawn. +function grow(T, d) { + while (d > 0) { + if (!T.cur) { + T.cur = addLeaf(T); + T.prog = 0; + T.L -= T.cur.h - T.cur.b; + } + const left = T.cur.h - T.cur.b - T.prog; + if (d < left) { + T.prog += d; + T.L += d; + return; + } + T.L += left; + d -= left; + T.cur = null; + } +} + +// The subtree to dive into: a run that starts just above a branch point +// (its parent holds at least a third more) with a mass near the target and +// at least four spanned leaves; of those, the one with the largest share of +// its spanned leaves in the upper half of its height, so that the tree it +// opens into keeps a crown on the tall panel. +function pickDive(T, mt, tol) { + let best = null, score = -Infinity; + const so = T.sorted, E = T.E; + for (let s = 4; s < NL - 8; s++) { + const L = T.lv[s], P = T.lv[s - 1], hs = s * T.dh; + for (let q = 0; q < L.a.length; q++) { + const m = (L.b[q] - L.a[q] + 2) / N, p = L.par[q]; + if (abs(log(m / mt)) > tol || (P.b[p] - P.a[p] + 2) / N < m * 1.33) continue; + let lo = 0, hi = so.length; + while (lo < hi) { + const k = (lo + hi) >> 1; + if (so[k] < L.a[q]) lo = k + 1; + else hi = k; + } + let n = 0, up = 0; + const mid = hs + 0.5 * (L.top[q] - hs); + for (let k = lo; k < so.length && so[k] <= L.b[q]; k++) { + n++; + if (E[so[k]] > mid) up++; + } + if (n < 4) continue; + const sc = up / n + 0.002 * n - 0.3 * abs(log(m / mt)); + if (sc > score) { + score = sc; + best = { a: L.a[q], b: L.b[q], s, hs, top: L.top[q], m }; + } + } + } + return best; +} + +// The view of tree T, or of T diving into g at progress e. +function viewOf(T, g, e) { + if (!g || e <= 0) return { x0: 0, x1: N, hb: 0, ht: T.maxH }; + const w0 = N, w1 = g.xb - g.xa, w = w0 ** (1 - e) * w1 ** e, fx = (w0 - w) / (w0 - w1); + const h0 = T.maxH, h1 = g.top - g.hs, hh = h0 ** (1 - e) * h1 ** e, fy = (h0 - hh) / (h0 - h1); + return { x0: fx * g.xa, x1: N + fx * (g.xb - N), hb: fy * g.hs, ht: T.maxH + fy * (g.top - T.maxH) }; +} + +export function setup(params) { + params.s = { + tmp: new Float64Array(N + 1), T: null, next: null, old: null, oldView: null, begun: false, t: 0, + phase: 0, u: 0, g: null, fade: 1, + hue: 0, speed: 2, dive: 0, cut: 0.06, + R: new Float32Array(W * H), G: new Float32Array(W * H), B: new Float32Array(W * H), + gR: new Float32Array(W * H), gG: new Float32Array(W * H), gB: new Float32Array(W * H), tmpb: new Float32Array(W * H), + c: [0, 0, 0], bud: [0, 0, 0], + }; +} + +function startDive(S) { + const mt = 0.3 * 0.2 ** S.dive; + const g = pickDive(S.T, mt, 0.6) || pickDive(S.T, mt, 1.5); + if (!g) { + S.T = tree(excursion(S), NL); + return; + } + // Frame the subtree as it is drawn: from its root up, the leftmost and + // rightmost of its branches, and a little room on either side. + const T = S.T, q = runOf(T, g.s, g.a); + let xa = T.lv[g.s].fx[q], xb = xa; + for (const leaf of T.leaves) + if (leaf.i >= g.a && leaf.i <= g.b) + for (let k = max(0, g.s - leaf.sb); k < leaf.xs.length; k++) { + xa = min(xa, leaf.xs[k]); + xb = max(xb, leaf.xs[k]); + } + const pad = 0.08 * (xb - xa) + 0.004 * N; + g.xa = xa - pad; + g.xb = xb + pad; + S.g = g; + S.next = null; + S.phase = 1; + S.u = 0; +} + +function endDive(S) { + const T = S.T, g = S.g, n = g.b - g.a + 2, T2 = S.next || tree(subExcursion(S, T, g), 0); + build(T2, NL); + // The spanned leaves inside the subtree stay: their reduced tree is a + // stage of line-breaking for the new tree. + for (const i of T.sorted) if (i >= g.a && i <= g.b) addLeaf(T2, cl(round(((i - g.a + 1) * N) / n), 1, N - 1)); + S.old = T; + S.oldView = viewOf(T, g, 1); + S.fade = 0; + S.T = T2; + S.g = S.next = null; + S.phase = 0; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.dive = cl(k[2], 0, 1); + S.cut = cl(k[3], 0, 1); + const r = (d * S.speed) / 2; + if (!S.begun) { + // Of four excursions, the one with most of its time in the upper half + // of its height, so the first tree has a crown. + let best = null, sc = -1; + for (let k = 0; k < 4; k++) { + const E = excursion(S); + let mx = 0, up = 0; + for (let j = 0; j <= N; j++) mx = max(mx, E[j]); + for (let j = 0; j <= N; j++) if (E[j] > 0.5 * mx) up++; + if (up > sc) { + sc = up; + best = E; + } + } + S.T = tree(best, NL); + // Open a second before the first dive. + grow(S.T, LEND - VGROW); + S.begun = true; + } + S.t += r; + if (S.phase === 0) { + grow(S.T, r * VGROW); + if (S.T.L >= LEND || (input.btnPressed && input.btnPressed[0] && S.T.leaves.length >= 30)) startDive(S); + } else { + S.u += r / DIVE; + // The tree the subtree opens into, made a few levels a frame meanwhile. + if (!S.next) S.next = tree(subExcursion(S, S.T, S.g), 0); + else build(S.next, 24); + if (S.u >= 1) endDive(S); + } + if (S.old) { + S.fade += r / FADE; + if (S.fade >= 1) S.old = null; + } +} + +// A round-capped line of radius w from (ax, ay) to (bx, by), max-blended. +function capsule(S, ax, ay, bx, by, w, r, g, b) { + const R = S.R, G = S.G, B = S.B; + const x0 = max(0, floor(min(ax, bx) - w - 1)), x1 = min(W - 1, ceil(max(ax, bx) + w)); + const y0 = max(0, floor(min(ay, by) - w - 1)), y1 = min(H - 1, ceil(max(ay, by) + w)); + if (x0 > x1 || y0 > y1) return; + const ex = bx - ax, ey = by - ay, ll = ex * ex + ey * ey; + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const px = x + 0.5 - ax, py = y + 0.5 - ay, u = ll > 0 ? cl((px * ex + py * ey) / ll, 0, 1) : 0; + const dd = sqrt((px - u * ex) ** 2 + (py - u * ey) ** 2), a = cl(w + 0.5 - dd, 0, 1); + if (a <= 0) continue; + const p = y * W + x; + if (a * r > R[p]) R[p] = a * r; + if (a * g > G[p]) G[p] = a * g; + if (a * b > B[p]) B[p] = a * b; + } +} + +// Draw the spanned tree of T in view V at weight wt. While diving, what is +// not in the subtree g dims by `dim`. +function drawTree(S, T, V, wt, g, dim) { + const sx = W / (V.x1 - V.x0), sy = ROWS / (V.ht - V.hb), vm = (V.x1 - V.x0) / N, c = S.c, bud = S.bud; + const px = (j) => (j - V.x0) * sx, py = (h) => Y0 - (h - V.hb) * sy; + const hue = S.hue + H0; + // The cut: each subtree above this height (a share of the view) gets its + // own hue by where it stands, half the wheel from left to right; below the + // cut the tree is pale. + const sc = cl(round((V.hb + (0.2 + 0.7 * S.cut) * (V.ht - V.hb)) / T.dh), 0, NL - 1), C = T.lv[sc]; + const vc = (V.x0 + V.x1) / 2, vw = V.x1 - V.x0, seen = new Set(); + for (const leaf of T.leaves) { + const grown = leaf === T.cur ? leaf.b + T.prog : leaf.h; + let off = 0; + if (leaf.st >= sc) { + const q = runOf(T, sc, leaf.i); + if (q >= 0) off = cl((C.a[q] + C.b[q] - 2 * vc) / vw, -1, 1) * 0.25; + } + const inside = !g || (leaf.i >= g.a && leaf.i <= g.b); + for (let s = leaf.sb; s <= leaf.st; s++) { + const h0 = s * T.dh; + if (h0 > grown) break; + const k = s - leaf.sb; + let h1 = s < leaf.st ? (s + 1) * T.dh : leaf.h, x0 = leaf.xs[k], x1 = leaf.xs[k + 1]; + if (h1 > grown) { + x1 = x0 + ((x1 - x0) * (grown - h0)) / (h1 - h0); + h1 = grown; + } + const ax = px(x0), bx = px(x1), ay = py(h0), by = py(h1); + if ((ax < -4 && bx < -4) || (ax > W + 4 && bx > W + 4) || (ay < -4 && by < -4) || ay > H + 4) continue; + const u = cl((h1 - V.hb) / (V.ht - V.hb), 0, 1), m = leaf.ms[k] / vm; + if (s >= sc) hsv(hue + off - 0.06 * u, 0.85 - 0.35 * u, 0.5 + 0.5 * u, c); + else hsv(hue, 0.3, 0.27 + 0.2 * u, c); + let v = wt * (!inside || s < (g ? g.s : 0) ? 1 - dim : 1); + // The newest 0.12 of the growing branch burns white. + if (leaf === T.cur) { + const hot = cl(1 - (grown - h1) / 0.12, 0, 1); + c[0] += (1 - c[0]) * hot; + c[1] += (1 - c[1]) * hot; + c[2] += (1 - c[2]) * hot; + } + capsule(S, ax, ay, bx, by, 0.45 + 1.4 * min(1, m) ** 0.35, c[0] * v, c[1] * v, c[2] * v); + } + const tx = px(leaf.xs[leaf.xs.length - 1]), ty = py(grown); + if (leaf === T.cur) capsule(S, tx, ty, tx, ty, 1.5, wt, wt, wt); + else if (inside || !g) { + hsv(hue + off, 0.25, wt, bud); + capsule(S, tx, ty, tx, ty, 0.6, bud[0], bud[1], bud[2]); + } + // A bead where the branch crosses the cut: the root of its subtree. + if (leaf.sb <= sc && leaf.st >= sc && grown >= sc * T.dh) { + const q = runOf(T, sc, leaf.i); + if (q >= 0 && !seen.has(q)) { + seen.add(q); + hsv(hue + off, 0.2, wt, bud); + const bx = px(C.fx[q]), by = py(sc * T.dh); + capsule(S, bx, by, bx, by, 1.1, bud[0], bud[1], bud[2]); + } + } + } +} + +// Box blur of radius r, A into O. +function blur(A, O, tmp, r) { + const k = 1 / (2 * r + 1); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let v = 0; + for (let i = -r; i <= r; i++) v += A[y * W + cl(x + i, 0, W - 1)]; + tmp[y * W + x] = v * k; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let v = 0; + for (let i = -r; i <= r; i++) v += tmp[cl(y + i, 0, H - 1) * W + x]; + O[y * W + x] = v * k; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.T) return; + const { R, G, B, gR, gG, gB } = S; + R.fill(0); + G.fill(0); + B.fill(0); + if (S.old) drawTree(S, S.old, S.oldView, sqrt(1 - ease(S.fade)), null, 0); + const e = S.phase === 1 ? ease(S.u) : 0; + drawTree(S, S.T, viewOf(S.T, S.g, e), S.old ? sqrt(ease(S.fade)) : 1, S.g, 0.8 * e); + blur(R, gR, S.tmpb, 3); + blur(G, gG, S.tmpb, 3); + blur(B, gB, S.tmpb, 3); + for (let p = 0; p < W * H; p++) + display.setPixel(p % W, (p / W) | 0, round(255 * min(1, R[p] + 1.1 * gR[p])), round(255 * min(1, G[p] + 1.1 * gG[p])), round(255 * min(1, B[p] + 1.1 * gB[p]))); +} From aac6465023549d9e23d3bcfb044b65617c993f63 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 23:10:22 +0000 Subject: [PATCH 04/16] =?UTF-8?q?Lamellar=20Cream=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9D=84=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 --- .../pattern-study/patterns/lamellar-cream.js | 341 ++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 incoming/pattern-study/patterns/lamellar-cream.js diff --git a/incoming/pattern-study/patterns/lamellar-cream.js b/incoming/pattern-study/patterns/lamellar-cream.js new file mode 100644 index 000000000..85b0cac9a --- /dev/null +++ b/incoming/pattern-study/patterns/lamellar-cream.js @@ -0,0 +1,341 @@ +// ===== Patternflow pattern ===== +// Title: Lamellar Cream +// Family: cosmetic +// Idea: A thin layer of cream between crossed polarizers under a microscope: on a black field its lamellar droplets drift up the panel as the cream is spread, pack, now and then run together, and each shows a dark Maltese cross along the polarizer axes with Michel-Lévy interference colours in its four bright quadrants that run in rings on the bigger drops. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Maltese cross and Michel-Lévy interference colours (Auguste Michel-Lévy, 1888) of the lamellar liquid-crystal droplets in creams (Stig Friberg, 1969), pattern round 40 +// Round: 40 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Size=0..1, Turn=0..1 +// +// Knob 1: Hue. Turns the interference colours round the wheel and gives the white of the small drops a pale tint of the same hue, so the first-order colour most drops show leads. At 0 champagne and gold with violet rings on the biggest drops; at 0.25 green, at 0.5 blue, at 0.75 pink. +// Knob 2: Speed. How fast the cream is spread. At 2 the drops in the middle columns rise about 3.5 LEDs a second and those by the sides about 2, so a drop crosses the panel in 40 to 70 s, and drops pressed together run into one about every 2 s; at 0.2 ten times slower and almost none merge; at 3 about one merge a second. +// Knob 3: Size. Scales every drop, which swells or shrinks over about 2 s; drops crowded out drift out of focus and new ones drift in where there is room. At 0 radii from 3 to 10 LEDs, more small than large. The retardation peaks at 56 nm per LED of radius, so most drops stay in the first order (grey, white, straw, gold) and only those over 9 LEDs reach the first-order red and the violet and blue rings past it. At 1 radii from 5.7 to 19, peaking near 1070 nm, and the big drops carry second-order blue, green, yellow and red rings. +// Knob 4: Turn. How fast the crossed polarizers turn together. The crosses turn with them, not with the drops: at 0 they stand upright as a +; at 0.06 the pair turns once in 200 s, so each cross comes back to itself every 50 s; at 1 once in 12 s. + +const W = 64, H = 128, N = W * H; +// Birefringence in nm of retardation per LED of path: a drop of radius R +// peaks at 1.122 * KR * R, 0.652 R out from its centre. +const KR = 50; +// The colour table: retardation 0 to GMAX nm in NT steps. +const GMAX = 2400, NT = 1024; +// Hue of the first-order straw yellow, which leads the palette at Hue 0. +const H0 = 0.12; +// Share of the panel the drops cover. +const FILL = 0.72; +const { floor, min, max, sqrt, exp, sin, cos, atan, round, random, PI, hypot } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +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] = [v, q, p, p, t, v][i]; + out[1] = [t, v, v, q, p, p][i]; + out[2] = [p, p, t, v, v, q][i]; +} + +// The Michel-Lévy colours: white light through a plate of retardation G +// between crossed polarizers keeps sin^2(pi G / lambda) of each wavelength. +// CIE 1931 matching functions in the multi-lobe fit of Wyman, Sloan and +// Shirley (2013), to linear sRGB, balanced so that full transmission is white. +function lobe(l, mu, s1, s2) { + const s = l < mu ? s1 : s2; + return exp(-0.5 * ((l - mu) / s) ** 2); +} +function michelLevy() { + const ws = []; + for (let l = 380; l <= 720; l += 5) { + const X = 1.056 * lobe(l, 599.8, 37.9, 31) + 0.362 * lobe(l, 442, 16, 26.7) - 0.065 * lobe(l, 501.1, 20.4, 26.2); + const Y = 0.821 * lobe(l, 568.8, 46.9, 40.5) + 0.286 * lobe(l, 530.9, 16.3, 31.1); + const Z = 1.217 * lobe(l, 437, 11.8, 36) + 0.681 * lobe(l, 459, 26, 13.8); + ws.push([l, 3.2406 * X - 1.5372 * Y - 0.4986 * Z, -0.9689 * X + 1.8758 * Y + 0.0415 * Z, 0.0557 * X - 0.204 * Y + 1.057 * Z]); + } + const full = [0, 0, 0]; + for (const w of ws) for (let c = 0; c < 3; c++) full[c] += w[c + 1]; + const T = new Float32Array(NT * 3); + for (let i = 0; i < NT; i++) { + const G = (i / (NT - 1)) * GMAX, s = [0, 0, 0]; + for (const w of ws) { + const t = sin((PI * G) / w[0]) ** 2; + for (let c = 0; c < 3; c++) s[c] += t * w[c + 1]; + } + for (let c = 0; c < 3; c++) T[i * 3 + c] = max(0, s[c] / full[c]); + // Past the first-order white the dark bands between colours (the + // first-order red and violet, near 550 nm) are lifted toward a floor of + // luminance 0.4, so the rings read as jewels and not as mud. + const lum = 0.3 * T[i * 3] + 0.59 * T[i * 3 + 1] + 0.11 * T[i * 3 + 2]; + if (G > 300 && lum < 0.4) { + const f = min(3, (0.4 / max(lum, 0.02)) ** 0.7); + for (let c = 0; c < 3; c++) T[i * 3 + c] *= f; + } + } + return T; +} + +// The table turned round the wheel by angle a: the chroma of each colour +// rotated in YIQ, its luminance kept. +function turnTable(S, a) { + const { ml, tab, tint } = S, ca = cos(a), sa = sin(a), c = [0, 0, 0]; + for (let i = 0; i < NT; i++) { + const r = ml[i * 3], g = ml[i * 3 + 1], b = ml[i * 3 + 2]; + const y = 0.299 * r + 0.587 * g + 0.114 * b, I0 = 0.596 * r - 0.274 * g - 0.322 * b, Q0 = 0.211 * r - 0.523 * g + 0.312 * b; + const I = I0 * ca - Q0 * sa, Q = I0 * sa + Q0 * ca; + c[0] = max(0, y + 0.956 * I + 0.621 * Q); + c[1] = max(0, y - 0.272 * I - 0.647 * Q); + c[2] = max(0, y - 1.106 * I + 1.703 * Q); + // An orange band turned to yellow keeps the orange's luminance and + // reads olive on the panel: yellows and limes are lifted and paled. + const v = max(c[0], c[1], c[2]), mn = min(c[0], c[1], c[2]); + if (v > 0.05 && v - mn > 0.05) { + const d = v - mn; + let h = v === c[0] ? (c[1] - c[2]) / d : v === c[1] ? 2 + (c[2] - c[0]) / d : 4 + (c[0] - c[1]) / d; + h = (h / 6 + 1) % 1; + const warm = cl(1 - Math.abs(h - 0.16) / 0.12, 0, 1); + if (warm > 0) hsv(h, (d / v) * (1 - 0.3 * warm), min(1, v * (1 + 0.7 * warm)), c); + } + // The greys and whites of the first order take a pale tint of the lead hue. + for (let k = 0; k < 3; k++) tab[i * 3 + k] = c[k] * tint[k]; + } +} + +export function setup(params) { + const ml = michelLevy(); + params.s = { + drops: [], next: null, g: 1, t: 0, pol: 0, hue: -1, speed: 2, size: 0, turn: 0.06, begun: false, + ml, tab: new Float32Array(NT * 3), tint: [1, 1, 1], own: new Int16Array(N), best: new Float32Array(N), gam: new Uint8Array(1024), + }; + // Light to LED level, v^0.6. + for (let i = 0; i < 1024; i++) params.s.gam[i] = round(255 * (i / 1023) ** 0.6); +} + +// A new drop, more small than large: p(r) ~ r^-1.5 for r from 3 to 10, +// times the Size scale. v3 is r^3, the volume merges keep. +function newDrop(S, x, y) { + const r = (3 ** -0.5 + random() * (10 ** -0.5 - 3 ** -0.5)) ** -2; + return { x, y, R: r * S.g, v3: r * r * r, m: null, into: null, mt: 0, dead: false, fade: false, a: 1 }; +} + +// Share of the panel covered by drops that are not leaving the focus, each +// counted by the part of its square that lies on the panel. +function cover(S) { + let a = 0; + for (const d of S.drops) { + if (d.fade) continue; + const R = d.R, fx = max(0, min(W, d.x + R) - max(0, d.x - R)) / (2 * R), fy = max(0, min(H, d.y + R) - max(0, d.y - R)) / (2 * R); + a += PI * R * R * fx * fy; + } + return a / N; +} + +// Whether a drop of radius R at x, y overlaps no other. +function free(S, x, y, R) { + for (const o of S.drops) if (hypot(o.x - x, o.y - y) < o.R + R) return false; + return true; +} + +// Packing: overlapping drops push each other apart, the small one more. +// With merge on, drops that stay pressed together may run into one. +function pack(S, dt, merge) { + const ds = S.drops, k = min(1, 6 * dt); + for (let i = 0; i < ds.length; i++) + for (let j = i + 1; j < ds.length; j++) { + const a = ds[i], b = ds[j]; + if (a.into === b || b.into === a) continue; + const dx = b.x - a.x, dy = b.y - a.y, d = hypot(dx, dy) + 1e-6, gap = a.R + b.R + 0.2 - d; + if (gap <= 0) continue; + const push = min(gap, 2) * 0.5 * k, wa = (b.R * b.R) / (a.R * a.R + b.R * b.R); + a.x -= (dx / d) * push * wa; + a.y -= (dy / d) * push * wa; + b.x += (dx / d) * push * (1 - wa); + b.y += (dy / d) * push * (1 - wa); + if (merge && gap > 0.4 && a.y < H - a.R && b.y < H - b.R && !a.m && !b.m && !a.into && !b.into && !a.fade && !b.fade && random() < 0.05 * S.speed * dt) { + const big = a.R >= b.R ? a : b, sm = big === a ? b : a; + if (big.v3 + sm.v3 < 1500) { + sm.into = big; + sm.mt = 0; + sm.v0 = sm.v3; + big.m = sm; + big.b0 = big.v3; + } + } + } +} + +// Share of the band from 12 rows above the bottom to 24 below it that the +// drops whose centres lie in it would cover. +function band(S) { + let a = 0; + for (const d of S.drops) if (d.y > H - 12 && d.y < H + 24) a += PI * d.R * d.R; + return a / (36 * W); +} + +// Speed of the flow across the panel, 1 in the middle column. +function flow(x) { + return 0.55 + 0.45 * sin((PI * cl(x, 0, W)) / W); +} + +function step(S, dt) { + const V = 1.5 * S.speed, rm = 10 * S.g; + S.t += dt; + // The Size scale eases to the knob over about two seconds. + S.g += (1 + 0.9 * S.size - S.g) * min(1, dt / 2); + for (const d of S.drops) { + // Drops come into focus over a second and leave it over 0.6 s. + d.a = d.fade ? d.a - dt / 0.6 : min(1, d.a + dt); + d.R = Math.cbrt(max(0, d.v3)) * S.g; + } + // The flow: up the panel, fastest in the middle columns and for the + // smaller drops, so drops slide past each other and meet. + for (const d of S.drops) { + d.y -= V * flow(d.x) * (1.25 - (0.25 * d.R) / rm) * dt; + d.x += 0.25 * V * sin(d.y * 0.05 + S.t * 0.07) * dt; + } + pack(S, dt, true); + // Merging: the small drop runs into the big one and gives it its volume + // over 1.2 s at Speed 1. + for (const d of S.drops) { + if (!d.into) continue; + const big = d.into; + d.mt += dt * (0.6 + 0.4 * S.speed); + const f = cl(d.mt / 1.2, 0, 1), e = f * f * (3 - 2 * f), g = min(1, 3 * dt * (0.5 + e)); + d.x += (big.x - d.x) * g; + d.y += (big.y - d.y) * g; + d.v3 = d.v0 * (1 - e); + big.v3 = big.b0 + d.v0 * e; + d.R = Math.cbrt(max(0, d.v3)) * S.g; + big.R = Math.cbrt(big.v3) * S.g; + if (f >= 1) { + big.m = null; + d.dead = true; + } + } + for (const d of S.drops) d.x = cl(d.x, -d.R * 0.5, W + d.R * 0.5); + S.drops = S.drops.filter((d) => !d.dead && d.y > -d.R - 2 && d.R > 0.6 && d.a > 0); + // The layer is thicker than the focus: when the drops have swelled past + // what the panel holds the smallest drifts out of focus, and when they + // have shrunk new ones drift in where there is room. + const c = cover(S); + if (c > 1.15 * FILL) { + let sm = null; + for (const d of S.drops) if (!d.fade && !d.m && !d.into && d.y < H && (!sm || d.R < sm.R)) sm = d; + if (sm) sm.fade = true; + } else if (c < 0.85 * FILL) { + for (let tries = 0; tries < 6; tries++) { + const d = newDrop(S, random() * W, random() * H); + if (free(S, d.x, d.y, d.R)) { + d.a = 0; + S.drops.push(d); + break; + } + } + } + // New cream comes in just below the panel while the band there is short + // of drops, more of it where the flow is faster so the packing stays + // even. The next drop keeps its size until a gap opens for it, so the big + // ones are not crowded out by the small. + if (!S.next) S.next = newDrop(S, 0, 0); + for (let tries = 0; tries < 12 && band(S) < FILL; tries++) { + const d = S.next; + d.R = Math.cbrt(d.v3) * S.g; + d.x = random() * W; + while (random() > flow(d.x)) d.x = random() * W; + d.y = H + d.R + 1 + random() * 4; + if (free(S, d.x, d.y, d.R)) { + S.drops.push(d); + S.next = newDrop(S, 0, 0); + } + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + if (k[0] !== S.hue) { + S.hue = k[0]; + hsv(H0 + S.hue, 0.3, 1, S.tint); + // Turning the chroma from I toward Q runs the hue backward, so the table + // turns by minus the knob. + turnTable(S, -2 * PI * S.hue); + } + S.speed = cl(k[1], 0.2, 3); + S.size = cl(k[2], 0, 1); + S.turn = cl(k[3], 0, 1); + if (!S.begun) { + S.g = 1 + 0.9 * S.size; + // Fill the panel, the big drops first, each where it overlaps nothing, + // then let them settle before any flow. + const rs = []; + for (let a = 0; a < FILL * N * 1.1; ) { + const d = newDrop(S, 0, 0); + rs.push(d); + a += PI * d.R * d.R; + } + rs.sort((p, q) => q.R - p.R); + for (const d of rs) + for (let t = 0; t < 300; t++) { + d.x = random() * W; + d.y = random() * H; + if (S.drops.every((o) => hypot(o.x - d.x, o.y - d.y) >= o.R + d.R - 0.5)) { + S.drops.push(d); + break; + } + } + for (let i = 0; i < 60; i++) pack(S, 1 / 30, false); + S.begun = true; + } + step(S, d); + // The polarizer pair: one turn in 12 s at Turn 1. + S.pol += ((d * S.turn * 2 * PI) / 12); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { own, best, tab, drops, gam } = S; + own.fill(-1); + best.fill(1); + // Each LED belongs to the drop it lies deepest in. + for (let i = 0; i < drops.length; i++) { + const d = drops[i], R = d.R; + if (R < 0.5) continue; + const k = 1 / (R * R); + for (let y = max(0, floor(d.y - R)); y <= min(H - 1, floor(d.y + R)); y++) { + const ey = (y + 0.5 - d.y) ** 2; + for (let x = max(0, floor(d.x - R)); x <= min(W - 1, floor(d.x + R)); x++) { + const s = ((x + 0.5 - d.x) ** 2 + ey) * k, p = y * W + x; + if (s < best[p]) { + best[p] = s; + own[p] = i; + } + } + } + } + const c2 = cos(2 * S.pol), s2 = sin(2 * S.pol); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, i = own[p]; + if (i < 0) { + display.setPixel(x, y, 0, 0, 0); + continue; + } + const d = drops[i], dx = x + 0.5 - d.x, dy = y + 0.5 - d.y, r2 = dx * dx + dy * dy + 1e-6, rho = sqrt(r2); + // Retardation of a sphere whose optic axis points out from its centre: + // along a ray at distance rho the birefringence goes as sin^2 of the + // angle to the radius, and integrates to 2 rho atan(L / rho). + const L = sqrt(max(0, d.R * d.R - r2)), G = KR * 2 * rho * atan(L / rho); + // Extinction where the radius lies along either polarizer: sin^2 2(alpha - P). + const sn = ((2 * dx * dy) / r2) * c2 - ((dx * dx - dy * dy) / r2) * s2, I = sn * sn; + const ti = min(NT - 1, round((G / GMAX) * (NT - 1))) * 3; + const v = I * d.a; + let r = tab[ti] * v, g = tab[ti + 1] * v, b = tab[ti + 2] * v; + // Dim colours go grey rather than brown or olive. + const lum = 0.3 * r + 0.59 * g + 0.11 * b, sat = 0.25 + 0.75 * v; + r = lum + (r - lum) * sat; + g = lum + (g - lum) * sat; + b = lum + (b - lum) * sat; + display.setPixel(x, y, gam[round(1023 * cl(r, 0, 1))], gam[round(1023 * cl(g, 0, 1))], gam[round(1023 * cl(b, 0, 1))]); + } +} From b632d74c72748096517e7f1fc7ecfe8f65743cd0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 23:16:26 +0000 Subject: [PATCH 05/16] =?UTF-8?q?Zeldovich=20Pancakes=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9D=84=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 --- .../patterns/zeldovich-pancakes.js | 511 ++++++++++++++++++ 1 file changed, 511 insertions(+) create mode 100644 incoming/pattern-study/patterns/zeldovich-pancakes.js diff --git a/incoming/pattern-study/patterns/zeldovich-pancakes.js b/incoming/pattern-study/patterns/zeldovich-pancakes.js new file mode 100644 index 000000000..06eb7734e --- /dev/null +++ b/incoming/pattern-study/patterns/zeldovich-pancakes.js @@ -0,0 +1,511 @@ +// ===== Patternflow pattern ===== +// Title: Zeldovich Pancakes +// Family: space +// Idea: A sheet of 32768 particles is moved by the Zeldovich approximation through a periodic box the size of the panel, and their density, counted per LED, grows from a smooth fog into thin pancakes that light up where the larger eigenvalue of the deformation tensor reaches 1/D, then into the walls, filaments and knots of a cosmic web, before a new random potential fades in. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Zeldovich approximation (Yakov Zeldovich, 1970), pattern round 40 +// Round: 40 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Scale=1.8..3.4, Cross=0..1 +// +// Knob 1: Hue. Turns both palettes round the wheel together, the free sheet and the matter the walls have swallowed. At 0 an indigo fog with walls rose to peach and knots near white; at 0.1 magenta and peach, 0.2 crimson and cream, 0.3 red and pale lime, 0.4 dark grey-green and green, 0.5 green and cyan, 0.6 teal and sky blue, 0.7 teal and violet, 0.8 blue and pink, 0.9 indigo and peach. Where a palette lands on orange, yellow or lime its dim end leans to crimson or to green and its middle is lifted toward cream, so nothing reads brown or olive. +// Knob 2: Speed. At 2 a new realisation goes from fog to its first caustic in 2.0 s, spends 4.3 s on pancakes (D from 1 to 1.6), reaches D = 2.4 at 10.5 s and D = 3.8 at 15.9 s, and the next one fades in over 1.6 s. At 0.2 all of it ten times slower, at 3 one and a half times faster. +// Knob 3: Scale. The smoothing length of the random potential, 1.8 to 3.4 LEDs. At 1.8 the web at D = 3.2 holds about 42 voids on the panel, at 2.6 about 26 and at 3.4 about 18, and the pancakes are born fewer and longer. Waves longer than 48 LEDs are tapered off at every setting. +// Knob 4: Cross. How far two streams of particles that meet pass through each other. At 0 they stick, the zero-viscosity limit of the adhesion model, and every wall stays one LED thin while its mass drains into the knots; at 1 they pass straight through, the plain Zeldovich approximation, and past D = 2 the knots puff up into wide diamonds of crossing streams. It opens at 0.06, 94 percent sticky. By D = 3.8 about half of the particles have been swallowed by a wall. +// Buttons: K1 tap starts a new realisation now; the old web stands still and fades out over 1.6 s at Speed 2. + +const W = 64, H = 128, NL = W * H; +// The particle lattice: two by two particles to an LED, 128 by 256. +const NX = 128, NY = 256, NP = NX * NY, HQ = 0.5; +const { floor, min, max, sqrt, exp, log, sin, cos, round, random, PI, abs } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +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] = [v, q, p, p, t, v][i]; + out[1] = [t, v, v, q, p, p][i]; + out[2] = [p, p, t, v, v, q][i]; +} + +// In-place radix-2 FFT of one line, sign +1 or -1. +function fft(re, im, n, sgn) { + for (let i = 1, j = 0; i < n; i++) { + let bit = n >> 1; + for (; j & bit; bit >>= 1) j ^= bit; + j ^= bit; + if (i < j) { + let t = re[i]; + re[i] = re[j]; + re[j] = t; + t = im[i]; + im[i] = im[j]; + im[j] = t; + } + } + for (let len = 2; len <= n; len <<= 1) { + const ang = (sgn * 2 * PI) / len, wr = cos(ang), wi = sin(ang), h = len >> 1; + for (let i = 0; i < n; i += len) { + let cr = 1, ci = 0; + for (let k = 0; k < h; k++) { + const a = i + k, b = a + h; + const xr = re[b] * cr - im[b] * ci, xi = re[b] * ci + im[b] * cr; + re[b] = re[a] - xr; + im[b] = im[a] - xi; + re[a] += xr; + im[a] += xi; + const nr = cr * wr - ci * wi; + ci = cr * wi + ci * wr; + cr = nr; + } + } + } +} + +// 2D FFT of an NX by NY field, rows then columns. +function fft2(re, im, sgn, T) { + const { lr, li, cr, ci } = T; + for (let y = 0; y < NY; y++) { + const o = y * NX; + for (let x = 0; x < NX; x++) { + lr[x] = re[o + x]; + li[x] = im[o + x]; + } + fft(lr, li, NX, sgn); + for (let x = 0; x < NX; x++) { + re[o + x] = lr[x]; + im[o + x] = li[x]; + } + } + for (let x = 0; x < NX; x++) { + for (let y = 0; y < NY; y++) { + cr[y] = re[y * NX + x]; + ci[y] = im[y * NX + x]; + } + fft(cr, ci, NY, sgn); + for (let y = 0; y < NY; y++) { + re[y * NX + x] = cr[y]; + im[y * NX + x] = ci[y]; + } + } +} + +// A new realisation: white noise, its transform kept so a new Scale only +// filters it again. +function noise(R) { + const { nr, ni } = R; + for (let p = 0; p < NP; p += 2) { + const u = 1 - random(), v = random(), m = sqrt(-2 * log(u)); + nr[p] = m * cos(2 * PI * v); + nr[p + 1] = m * sin(2 * PI * v); + } + ni.fill(0); + fft2(nr, ni, -1, R.T); +} + +// The potential phi has power k^(-4) exp(-(k s)^2), so the linear density +// D lap(phi) is white noise smoothed over s LEDs. Waves +// longer than about 48 LEDs are tapered off: the box has only a few of them, +// all lined up with its sides, and one alone lays a ruled wall across the +// panel. Its gradient, the deformation tensor d2 phi / dq2 and phi itself +// come back from three inverse transforms, and all of it is scaled so the +// largest eigenvalue lambda1 over the lattice is 1: the first caustic is +// due at D = 1. On three seeds the first cell of the lattice turned over at +// D = 1.002 to 1.013, and before that the counted density matched +// 1 / ((1 - D lambda1)(1 - D lambda2)) per LED to a median 1 to 2.6 percent. +const KLO = (2 * PI) / 48; +function amp(k2, s) { + return (k2 / sqrt(k2 * k2 + KLO ** 4)) * k2 ** -1 * exp(-0.5 * k2 * s * s); +} + +function filter(R, s) { + const { nr, ni, ar, ai, br, bi, cr, ci, gx, gy, T } = R; + for (let j = 0; j < NY; j++) { + const ky = ((2 * PI) / H) * (j < NY / 2 ? j : j - NY); + for (let i = 0; i < NX; i++) { + const kx = ((2 * PI) / W) * (i < NX / 2 ? i : i - NX), p = j * NX + i, k2 = kx * kx + ky * ky; + if (k2 === 0 || i === NX / 2 || j === NY / 2) { + ar[p] = ai[p] = br[p] = bi[p] = cr[p] = ci[p] = 0; + continue; + } + const w = amp(k2, s), Ar = nr[p] * w, Ai = ni[p] * w; + // gx + i gy + ar[p] = -kx * Ai - ky * Ar; + ai[p] = kx * Ar - ky * Ai; + // phi_xx + i phi_yy + br[p] = -kx * kx * Ar + ky * ky * Ai; + bi[p] = -kx * kx * Ai - ky * ky * Ar; + // phi_xy + i phi + cr[p] = -kx * ky * Ar - Ai; + ci[p] = -kx * ky * Ai + Ar; + } + } + fft2(ar, ai, 1, T); + fft2(br, bi, 1, T); + fft2(cr, ci, 1, T); + let l1 = 0; + for (let p = 0; p < NP; p++) { + const a = br[p], b = bi[p], c = cr[p], m = 0.5 * (a + b) + sqrt(0.25 * (a - b) * (a - b) + c * c); + if (m > l1) l1 = m; + } + R.l1raw = l1; + const k = 1 / l1; + for (let p = 0; p < NP; p++) { + gx[p] = ar[p] * k; + gy[p] = ai[p] * k; + R.phi[p] = ci[p] * k; + const a = br[p], b = bi[p], c = cr[p]; + R.l1[p] = (0.5 * (a + b) + sqrt(0.25 * (a - b) * (a - b) + c * c)) * k; + } + R.scale = s; + R.Dh = R.D1 = -1; +} + +function realisation() { + const R = { + nr: new Float64Array(NP), ni: new Float64Array(NP), + ar: new Float64Array(NP), ai: new Float64Array(NP), br: new Float64Array(NP), bi: new Float64Array(NP), + cr: new Float64Array(NP), ci: new Float64Array(NP), + gx: new Float32Array(NP), gy: new Float32Array(NP), phi: new Float32Array(NP), + // Where each particle would be in the sticky limit, less where it is. + dx: new Float32Array(NP), dy: new Float32Array(NP), psi: new Float64Array(NP), Dh: -1, D1: -1, + X: new Float32Array(NP), Y: new Float32Array(NP), wk: new Float32Array(NP), l1: new Float32Array(NP), + T: { lr: new Float64Array(NX), li: new Float64Array(NX), cr: new Float64Array(NY), ci: new Float64Array(NY) }, + l1raw: 1, scale: 0, + }; + noise(R); + return R; +} + +// The sticky limit is the adhesion model (Gurbatov, Saichev and Shandarin, +// 1989): Burgers flow with the viscosity taken to zero. A particle sits at +// the gradient of the convex hull of G(q) = |q|^2 / 2 - D phi(q); where G is +// its own hull the particle is where Zeldovich put it, and where the hull +// bridges over G the particle has been swallowed by a wall or a knot. The +// hull is two Legendre transforms, each two passes of Felzenszwalb and +// Huttenlocher's lower envelope of parabolas: +// Phi(p) = min_q |p - q|^2 / 2 - D phi(q), Psi(q) = min_p |q - p|^2 / 2 - Phi(p), +// and the hull is |q|^2 / 2 - Psi(q). Sites q one an LED, MQ LEDs of the +// periodic box copied round it; the p grid two an LED, MP LEDs round the +// box, wide enough for the largest move, D = 3.8 times 5.3 LEDs at Scale 3.4. +const MQ = 30, SX = W + 2 * MQ, SY = H + 2 * MQ, MP = 20, PD = 2, PX = PD * (W + 2 * MP), PY = PD * (H + 2 * MP); +const NE = max(SY, PY) + 2; +const A = { + zsx: new Float64Array(SX), zsy: new Float64Array(SY), zpx: new Float64Array(PX), zpy: new Float64Array(PY), + zlx: new Float64Array(NX), zly: new Float64Array(NY), + T1: new Float64Array(SY * PX), PH: new Float64Array(PY * PX), T2: new Float64Array(PY * NX), + f: new Float64Array(NE), o: new Float64Array(NE), V: new Int32Array(NE), Zb: new Float64Array(NE + 1), +}; +for (let m = 0; m < SX; m++) A.zsx[m] = m - MQ + 0.25; +for (let m = 0; m < SY; m++) A.zsy[m] = m - MQ + 0.25; +for (let k = 0; k < PX; k++) A.zpx[k] = (k + 0.5) / PD - MP; +for (let k = 0; k < PY; k++) A.zpy[k] = (k + 0.5) / PD - MP; +for (let i = 0; i < NX; i++) A.zlx[i] = (i + 0.5) * HQ; +for (let j = 0; j < NY; j++) A.zly[j] = (j + 0.5) * HQ; + +// Lower envelope of (x - z[i])^2 / 2 + f[i] over n sites, read at m +// queries xq, both in increasing order. +function envelope(z, f, n, xq, m, out) { + const { V, Zb } = A; + let k = 0; + V[0] = 0; + Zb[0] = -Infinity; + Zb[1] = Infinity; + for (let i = 1; i < n; i++) { + const ai = f[i] + 0.5 * z[i] * z[i]; + let s = 0; + for (;;) { + const v = V[k]; + s = (ai - f[v] - 0.5 * z[v] * z[v]) / (z[i] - z[v]); + if (s <= Zb[k]) k--; + else break; + } + k++; + V[k] = i; + Zb[k] = s; + Zb[k + 1] = Infinity; + } + k = 0; + for (let j = 0; j < m; j++) { + const x = xq[j]; + while (Zb[k + 1] < x) k++; + const v = V[k], d = x - z[v]; + out[j] = 0.5 * d * d + f[v]; + } +} + +// The hull is split over two frames: Phi at growth D on one, Psi and the +// particles' moves on the next, so they lag the growth by two frames at +// most, a twentieth of an LED. +function hullPhi(R, D) { + const { zsx, zsy, zpx, zpy, T1, PH, f, o } = A; + const phi = R.phi; + // Along x for each row of sites, then along y for each column of p. + for (let n = 0; n < SY; n++) { + const jy = 2 * ((((n - MQ) % H) + H) % H); + for (let m = 0; m < SX; m++) f[m] = -D * phi[jy * NX + 2 * ((((m - MQ) % W) + W) % W)]; + envelope(zsx, f, SX, zpx, PX, o); + T1.set(o.subarray(0, PX), n * PX); + } + for (let k = 0; k < PX; k++) { + for (let n = 0; n < SY; n++) f[n] = T1[n * PX + k]; + envelope(zsy, f, SY, zpy, PY, o); + for (let j = 0; j < PY; j++) PH[j * PX + k] = o[j]; + } + R.D1 = D; +} + +function hullPsi(R) { + const { zpx, zpy, zlx, zly, PH, T2, f, o } = A; + const { phi, gx, gy, dx, dy, psi, wk } = R, D = R.D1; + // Along x for each row of p, then along y, read at the lattice. + for (let j = 0; j < PY; j++) { + for (let k = 0; k < PX; k++) f[k] = -PH[j * PX + k]; + envelope(zpx, f, PX, zlx, NX, o); + T2.set(o.subarray(0, NX), j * NX); + } + for (let i = 0; i < NX; i++) { + for (let j = 0; j < PY; j++) f[j] = T2[j * NX + i]; + envelope(zpy, f, PY, zly, NY, o); + for (let j = 0; j < NY; j++) psi[j * NX + i] = o[j]; + } + // Psi - |q|^2 / 2 is periodic like phi, so the hull's gradient q - grad Psi + // comes from central differences round the box; on a knot's flat facet it + // is the same point for every particle on the facet, on a wall it runs + // along the wall. Where the hull touches G (to within GAP0, the slack of + // the grids) the particle is free and stays where Zeldovich put it. + for (let j = 0; j < NY; j++) { + const jn = ((j + 1) % NY) * NX, js = ((j + NY - 1) % NY) * NX; + for (let i = 0; i < NX; i++) { + const p = j * NX + i, w = cl((psi[p] - D * phi[p] - GAP0) / GAP0, 0, 1); + wk[p] = w; + if (w === 0) { + dx[p] = dy[p] = 0; + continue; + } + const ie = (i + 1) % NX, iw = (i + NX - 1) % NX; + dx[p] = w * (D * gx[p] - (psi[j * NX + ie] - psi[j * NX + iw]) / (2 * HQ)); + dy[p] = w * (D * gy[p] - (psi[jn + i] - psi[js + i]) / (2 * HQ)); + } + } + R.Dh = D; + R.D1 = -1; +} +const GAP0 = 0.05; + +function tick(R, D) { + if (R.Dh < 0) { + hullPhi(R, D); + hullPsi(R); + } else if (R.D1 < 0) hullPhi(R, D); + else hullPsi(R); +} + +// One point into the LEDs by cloud in cell, weight w into rho and the +// share k of it that a wall has swallowed into rs as well. +function cic(rho, rs, x, y, w, k) { + x -= W * floor(x / W); + y -= H * floor(y / H); + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + const ix1 = ix + 1 === W ? 0 : ix + 1, o0 = iy * W, o1 = iy + 1 === H ? 0 : o0 + W; + const a = w * (1 - fy), b = w * fy, a0 = a * (1 - fx), a1 = a * fx, b0 = b * (1 - fx), b1 = b * fx; + rho[o0 + ix] += a0; + rho[o0 + ix1] += a1; + rho[o1 + ix] += b0; + rho[o1 + ix1] += b1; + if (k > 0) { + rs[o0 + ix] += k * a0; + rs[o0 + ix1] += k * a1; + rs[o1 + ix] += k * b0; + rs[o1 + ix1] += k * b1; + } +} + +// How much of a particle a wall has swallowed: all of it once it has passed +// through its own caustic, D lambda1(q) > 1, ramped in over the first tenth; +// in the sticky limit also its neighbours that the hull bridges over. +const eaten = (dl, w) => { + const e = dl <= 1 ? 0 : dl >= 1.1 ? 1 : (dl - 1) * 10; + return e > w ? e : w; +}; + +// Moves every particle to x = q - D grad phi(q), pulled the sticky share of +// the way to where the sticky limit puts it, and counts it into the LEDs, a +// quarter of a particle each so the untouched sheet is density 1. Where the +// sheet is stretched so that neighbours land more than SPREAD apart (the +// voids, and walls draining into their knots), a particle is laid as 2 to 4 +// points across the parallelogram its own cell of the lattice went to; +// otherwise those stretches show as a ladder of dots. +const SPREAD = 0.9, S1 = SPREAD * SPREAD, S2 = 4 * S1, S3 = 9 * S1; +function deposit(R, D, st, rho, rs, wgt) { + const { gx, gy, dx, dy, X, Y, wk, l1 } = R; + const hull = R.Dh >= 0; + if (!hull) st = 0; + for (let j = 0; j < NY; j++) { + const qy = (j + 0.5) * HQ - 0.5; + for (let i = 0; i < NX; i++) { + const p = j * NX + i; + X[p] = (i + 0.5) * HQ - 0.5 - D * gx[p] + st * dx[p]; + Y[p] = qy - D * gy[p] + st * dy[p]; + } + } + for (let j = 0; j < NY; j++) { + const on = (j === NY - 1 ? 0 : j + 1) * NX, os = (j === 0 ? NY - 1 : j - 1) * NX, yn = j === NY - 1 ? H : 0, ys = j === 0 ? H : 0; + for (let i = 0; i < NX; i++) { + const p = j * NX + i, e = j * NX + (i === NX - 1 ? 0 : i + 1), w = j * NX + (i === 0 ? NX - 1 : i - 1); + // The cell's edges, half the central differences. + const ax = 0.5 * (X[e] - X[w] + (i === NX - 1 || i === 0 ? W : 0)), ay = 0.5 * (Y[e] - Y[w]); + const bx = 0.5 * (X[on + i] - X[os + i]), by = 0.5 * (Y[on + i] - Y[os + i] + yn + ys); + const la = ax * ax + ay * ay, lb = bx * bx + by * by; + if (la < S1 && lb < S1) { + cic(rho, rs, X[p], Y[p], wgt, eaten(D * l1[p], hull ? wk[p] : 0)); + continue; + } + const k = la < S1 ? 1 : la < S2 ? 2 : la < S3 ? 3 : 4, l = lb < S1 ? 1 : lb < S2 ? 2 : lb < S3 ? 3 : 4, ww = wgt / (k * l), sw = eaten(D * l1[p], hull ? wk[p] : 0); + for (let b = 0; b < l; b++) { + const v = (b + 0.5) / l - 0.5; + for (let a = 0; a < k; a++) { + const u = (a + 0.5) / k - 0.5; + cic(rho, rs, X[p] + u * ax + v * bx, Y[p] + u * ay + v * by, ww, sw); + } + } + } + } +} + +// Thin fog and voids, where both an LED and its neighbours hold under FOG, +// are smoothed by a 1 2 1 blur: there the lattice of particles beats with +// the LEDs in a faint cross-hatch. Walls and knots are left sharp. +const FOG = 1.6; +function smooth(rho, t1, t2) { + for (let y = 0; y < H; y++) { + const o = y * W; + for (let x = 0; x < W; x++) t1[o + x] = 0.25 * (rho[o + (x === 0 ? W - 1 : x - 1)] + rho[o + (x === W - 1 ? 0 : x + 1)]) + 0.5 * rho[o + x]; + } + for (let y = 0; y < H; y++) { + const on = (y === H - 1 ? 0 : y + 1) * W, os = (y === 0 ? H - 1 : y - 1) * W, o = y * W; + for (let x = 0; x < W; x++) t2[o + x] = 0.25 * (t1[on + x] + t1[os + x]) + 0.5 * t1[o + x]; + } + for (let p = 0; p < NL; p++) { + const r = rho[p], b = t2[p], m = r > b ? r : b; + if (m < FOG) rho[p] = b; + else if (m < 2 * FOG) rho[p] = r + (b - r) * (2 - m / FOG); + } +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, sticky: 0.94, + D: DSTART, cur: null, old: null, fade: 0, Dold: 0, + rho: new Float32Array(NL), rs: new Float32Array(NL), t1: new Float32Array(NL), t2: new Float32Array(NL), + lut: new Uint8Array(256 * 3), lutS: new Uint8Array(256 * 3), lutHue: -1, c: [0, 0, 0], + }; +} + +// Growth: D climbs RATE a second at Speed 2 times pace(D), from D0 to DMAX, +// where the next realisation fades in over FADE seconds and the old web +// stands still as it fades. It hurries through the fog, slows to half +// speed while the pancakes are born (D 1 to 1.6) and quickens again as the +// web drains into its knots. The first run opens at DSTART, the web grown. +const RATE = 0.2, D0 = 0.5, DMAX = 3.8, DSTART = 2.6, FADE = 1.6; +const sm = (a, b, v) => { + const t = cl((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; +const pace = (D) => 0.7 + 0.9 * (1 - sm(0.7, 1, D)) + 0.6 * sm(1.6, 2.4, D); +// Opening hue: the fog indigo, the swallowed walls rose to peach. +const H0 = 0.68; + +// Two palettes from the log density, LUTs rebuilt when Hue turns: one for +// the free sheet, which runs up 0.3 of the wheel from the voids to the +// knots while it pales, and one a third of the way round, paler and +// brighter, for the matter a wall has swallowed, so a pancake shows the +// moment it forms. Orange, yellow and lime go brown or olive when dim, so +// where a palette lands there it leans to crimson (from orange) or to green +// (from yellow) in the dark, and is lifted toward cream at mid brightness. +function entry(lut, k, h, s, v, c) { + h -= floor(h); + const warm = cl(h / 0.04, 0, 1) * cl((0.3 - h) / 0.06, 0, 1), dim = warm * cl((0.62 - v) / 0.3, 0, 1), mid = warm * cl((v - 0.25) / 0.2, 0, 1); + const toRed = cl((0.125 - h) / 0.04, 0, 1), toGreen = cl((h - 0.125) / 0.04, 0, 1); + h += dim * (toGreen * (0.32 - h) - toRed * (h + 0.03)); + s *= 1 - 0.7 * dim * (1 - toRed - toGreen); + v += mid * 0.6 * (1 - v); + s *= 1 - 0.55 * mid; + hsv(h, s, v, c); + lut[k * 3] = round(255 * c[0]); + lut[k * 3 + 1] = round(255 * c[1]); + lut[k * 3 + 2] = round(255 * c[2]); +} + +function buildLut(S) { + const c = S.c, h0 = S.hue + H0; + for (let k = 0; k < 256; k++) { + const t = k / 255; + entry(S.lut, k, h0 + 0.3 * t, 0.92 - 0.8 * t * t, 0.03 + 0.97 * t ** 1.25, c); + entry(S.lutS, k, h0 + 0.22 + 0.14 * t, 0.78 - 0.5 * t * t, 0.04 + 0.96 * t ** 1.3, c); + } + S.lutHue = S.hue; +} + +function fresh(S, sc) { + S.old = S.cur; + S.Dold = S.D; + S.cur = realisation(); + filter(S.cur, sc); + S.D = D0; + S.fade = 0; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 1.8, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + const sc = round(cl(k[2], 1.8, 3.4) * 10) / 10; + S.sticky = 1 - cl(k[3], 0, 1); + if (!S.cur) S.cur = realisation(); + if (S.cur.scale !== sc) filter(S.cur, sc); + if (S.old && S.old.scale !== sc) filter(S.old, sc); + S.D += ((RATE * S.speed) / 2) * pace(S.D) * d; + if (S.old) { + S.fade += (d * S.speed) / 2 / FADE; + if (S.fade >= 1) S.old = null; + } + if (!S.old && (S.D >= DMAX || (input.btnPressed && input.btnPressed[0]))) fresh(S, sc); + tick(S.cur, S.D); + // The old web stands still while it fades, so its hull is done once. + if (S.old && S.old.Dh !== S.Dold) { + hullPhi(S.old, S.Dold); + hullPsi(S.old); + S.cur.D1 = -1; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.cur) return; + if (S.lutHue !== S.hue) buildLut(S); + const { rho, rs, t1, lut, lutS } = S; + rho.fill(0); + rs.fill(0); + if (S.old) { + // The old web fades as the new one comes in. + const f = S.fade * S.fade * (3 - 2 * S.fade); + deposit(S.cur, S.D, S.sticky, rho, rs, 0.25 * f); + deposit(S.old, S.Dold, S.sticky, rho, rs, 0.25 * (1 - f)); + } else deposit(S.cur, S.D, S.sticky, rho, rs, 0.25); + // The swallowed share of each LED, before the fog is smoothed. + for (let p = 0; p < NL; p++) rs[p] = rho[p] > 0 ? min(1, (1.4 * rs[p]) / rho[p]) : 0; + smooth(rho, t1, S.t2); + // Brightness from the log of the density: 0.25 dark, 1 a dim fog, 16 full. + const lo = log(0.25), sp = 1 / (log(16) - lo); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, r = rho[p], a = rs[p], b = 1 - a; + const i = r > 0.25 ? round(min(1, (log(r) - lo) * sp) * 255) * 3 : 0; + display.setPixel(x, y, round(b * lut[i] + a * lutS[i]), round(b * lut[i + 1] + a * lutS[i + 1]), round(b * lut[i + 2] + a * lutS[i + 2])); + } +} From 19c855a5dbe97467b3184780d832ce14b61a2faa Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 23:17:02 +0000 Subject: [PATCH 06/16] =?UTF-8?q?Defect=20Turbulence=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9D=84=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 --- .../patterns/defect-turbulence.js | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 incoming/pattern-study/patterns/defect-turbulence.js diff --git a/incoming/pattern-study/patterns/defect-turbulence.js b/incoming/pattern-study/patterns/defect-turbulence.js new file mode 100644 index 000000000..cd17c01f6 --- /dev/null +++ b/incoming/pattern-study/patterns/defect-turbulence.js @@ -0,0 +1,214 @@ +// ===== Patternflow pattern ===== +// Title: Defect Turbulence +// Family: wave +// Idea: The complex Ginzburg-Landau equation runs on the panel and is drawn as its wavefronts, the zero lines of its imaginary part in azure and of its real part in rose, with a small + or - where they cross at a phase defect: from a random start the defects are born and annihilate in pairs and drag spirals about until the spirals win and freeze into cells, and turned past the Benjamin-Feir line the turbulence never settles. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: complex Ginzburg-Landau equation (Vitaly Ginzburg, Lev Landau, 1950); defect turbulence (Pierre Coullet, Lydia Gil, Jean Lega, 1989), pattern round 40 +// Round: 40 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Stability=-0.8..0.6, Strobe=0..1 +// +// Knob 1: Hue. Turns both line colours round the wheel together. At 0 azure and rose, at 0.25 violet and yellow, at 0.5 orange and spring green, at 0.75 green and blue. The lines are drawn at full brightness on black, so no colour goes brown or olive. +// Knob 2: Speed. How fast the equation runs. At 2 four units of its time a second, so the fronts of a spiral run out from its core at about 3 LEDs a second and a line passes any LED 1.3 times a second; at 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Stability. The number 1 + bc, with b = 2 held and c = (Stability - 1) / 2. Above 0 plane waves are stable; below it they are Benjamin-Feir unstable. Counted over three minutes from the random start: at 0.6 and 0.3 the field coarsens into 7 and 6 pairs of spirals within 20 s and they freeze into cells with shock lines between them; at 0, on the line, the turbulence of the start lasts about 40 s and then the spirals win and 5 pairs are left; at -0.2 it never settles and holds 11 to 37 pairs, at -0.4 13 to 38, at -0.8 19 to 52. +// Knob 4: Strobe. The lines are the zero lines of A times a turning phase. At 0 the phase turns so that the field beats at 0.56 radians per unit of time and the fronts run outward from every core; at 1 it turns with the field's own frequency, measured as it runs, so the arms of frozen spirals stand still and only the defects and the turbulence move. +// Buttons: K1 tap stirs a round patch 20 LEDs across its radius back to small random noise. It breaks into defect pairs at once; at the opening the spirals round it close it again in about ten seconds. + +const W = 64, H = 128; +// The equation is solved on a grid twice as fine as the LEDs, GW x GH cells +// of HS units each, so one LED is 2 HS units across. +const GW = 128, GH = 256, GN = GW * GH, HS = 0.8; +// Linear dispersion b. A lone spiral at b = 2, c = -0.5 settles to +// wavenumber 0.42 at both 0.6 and 0.4 units a cell, so 0.8 a cell is close +// to the continuum; its waves are 9 LEDs long on the panel. +const B = 2; +// Explicit Euler, at 0.75 of the 9-point Laplacian's limit 0.375 h^2 / (1 + b^2). +const DT = (0.75 * 0.375 * HS * HS) / (1 + B * B); +// Equation time per second at Speed 1, the beat the wavefronts are drawn at, +// and the run before the first frame. +const RATE = 2, BEAT = 0.56, PREROLL = 20; +// Line colours (azure, and rose 0.37 round the wheel from it), and the +// lines' half width in LEDs and falloff. +const H0 = 0.55, HM = 0.37, LW = 1.1, LP = 0.8; +const { floor, min, max, abs, sqrt, atan2, round, random, cos, sin, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +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] = [v, q, p, p, t, v][i]; + out[1] = [t, v, v, q, p, p][i]; + out[2] = [p, p, t, v, v, q][i]; +} + +export function setup(params) { + const S = { + ar: new Float64Array(GN), ai: new Float64Array(GN), br: new Float64Array(GN), bi: new Float64Array(GN), + fr: new Float64Array(W * H), fi: new Float64Array(W * H), + def: [], np: 0, nm: 0, t: 0, phi: 0, ws: 0, begun: false, c: -0.5, hue: 0, speed: 2, strobe: 0.06, + pc: [0, 0, 0], mc: [0, 0, 0], + }; + // A smooth random start: a few long waves in each part. + const modes = []; + for (let m = 0; m < 24; m++) modes.push([floor(random() * 5) - 2, floor(random() * 7) - 3, random() * 2 * PI, m & 1]); + for (let y = 0; y < GH; y++) + for (let x = 0; x < GW; x++) { + let r = 0, i = 0; + for (const [kx, ky, ph, im] of modes) { + const v = cos((2 * PI * kx * x) / GW + (2 * PI * ky * y) / GH + ph); + if (im) i += v; + else r += v; + } + S.ar[y * GW + x] = 0.15 * r; + S.ai[y * GW + x] = 0.15 * i; + } + params.s = S; +} + +// One Euler step of A_t = A + (1 + ib) lap A - (1 + ic)|A|^2 A, periodic edges. +function step(S) { + const { ar, ai, br, bi } = S, c = S.c, k = 1 / (6 * HS * HS); + let wsum = 0, asum = 0; + for (let y = 0; y < GH; y++) { + const yu = ((y + GH - 1) % GH) * GW, yd = ((y + 1) % GH) * GW, y0 = y * GW; + for (let x = 0; x < GW; x++) { + const xl = x === 0 ? GW - 1 : x - 1, xr = x === GW - 1 ? 0 : x + 1, p = y0 + x; + const lr = (4 * (ar[yu + x] + ar[yd + x] + ar[y0 + xl] + ar[y0 + xr]) + ar[yu + xl] + ar[yu + xr] + ar[yd + xl] + ar[yd + xr] - 20 * ar[p]) * k; + const li = (4 * (ai[yu + x] + ai[yd + x] + ai[y0 + xl] + ai[y0 + xr]) + ai[yu + xl] + ai[yu + xr] + ai[yd + xl] + ai[yd + xr] - 20 * ai[p]) * k; + const r = ar[p], i = ai[p], m = r * r + i * i; + const dr = r + lr - B * li - m * (r - c * i), di = i + li + B * lr - m * (i + c * r); + br[p] = r + DT * dr; + bi[p] = i + DT * di; + // Im(conj(A) A_t), for the field's own frequency. + wsum += r * di - i * dr; + asum += m; + } + } + S.ar = br; + S.br = ar; + S.ai = bi; + S.bi = ai; + S.t += DT; + // A ~ exp(-i w t) gives Im(conj(A) A_t) = -w |A|^2. + const w = -wsum / max(1e-9, asum); + S.ws += (w - S.ws) * min(1, DT / 4); +} + +// Phase defects: the winding of the phase round each plaquette of four +// cells. A plaquette can wind only if both the real and the imaginary part +// change sign round it, so the phase is looked at only there. On a periodic +// grid every edge is walked once each way, so the windings sum to zero and +// there are as many + as -: at six settings of Stability, 3 minutes each, +// not one of 32,400 frames had a difference. +function defects(S) { + const { ar, ai } = S, TAU = 2 * PI; + const wr = (d) => (d > PI ? d - TAU : d < -PI ? d + TAU : d); + S.def.length = 0; + let np = 0, nm = 0; + for (let y = 0; y < GH; y++) { + const y1 = ((y + 1) % GH) * GW, y0 = y * GW; + for (let x = 0; x < GW; x++) { + const x1 = x === GW - 1 ? 0 : x + 1, pa = y0 + x, pb = y0 + x1, pc = y1 + x1, pd = y1 + x; + const sr = (ar[pa] > 0) + (ar[pb] > 0) + (ar[pc] > 0) + (ar[pd] > 0); + if (sr === 0 || sr === 4) continue; + const si = (ai[pa] > 0) + (ai[pb] > 0) + (ai[pc] > 0) + (ai[pd] > 0); + if (si === 0 || si === 4) continue; + const a = atan2(ai[pa], ar[pa]), b = atan2(ai[pb], ar[pb]), c = atan2(ai[pc], ar[pc]), d = atan2(ai[pd], ar[pd]); + const n = round((wr(b - a) + wr(c - b) + wr(d - c) + wr(a - d)) / TAU); + if (n) { + S.def.push(x / 2, y / 2, n); + if (n > 0) np += n; + else nm -= n; + } + } + } + S.np = np; + S.nm = nm; +} + +// Stir a round patch, 20 LEDs across its radius, back to a small random +// field. Its zero lines cross at random, so it breaks into defect pairs. +function stir(S) { + const cx = floor(random() * GW), cy = floor(random() * GH), R = 40, modes = []; + for (let m = 0; m < 12; m++) modes.push([(random() - 0.5) * 0.9, (random() - 0.5) * 0.9, random() * 2 * PI, m & 1]); + const { ar, ai } = S; + for (let j = -R; j <= R; j++) + for (let i = -R; i <= R; i++) { + const r = sqrt(i * i + j * j); + if (r > R) continue; + let nr = 0, ni = 0; + for (const [kx, ky, ph, im] of modes) { + const v = cos(kx * i + ky * j + ph); + if (im) ni += v; + else nr += v; + } + const w = min(1, (R - r) / 8), p = ((cy + j + GH) % GH) * GW + ((cx + i + GW) % GW); + ar[p] += (0.12 * nr - ar[p]) * w; + ai[p] += (0.12 * ni - ai[p]) * w; + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + // The knob is 1 + bc, so c follows from it. + S.c = (cl(k[2], -0.8, 0.6) - 1) / B; + S.strobe = cl(k[3], 0, 1); + if (!S.begun) { + for (let n = 0; n < PREROLL / DT; n++) step(S); + S.begun = true; + } + if (input.btnPressed && input.btnPressed[0]) stir(S); + const target = S.t + d * S.speed * RATE; + while (S.t < target) { + step(S); + // The drawing's clock: the field seen at the beat, slowing to the + // field's own frequency as Strobe goes to 1. + S.phi += (S.ws - BEAT * (1 - S.strobe)) * DT; + } + defects(S); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { ar, ai, pc, mc, fr, fi } = S, cp = cos(S.phi), sp = sin(S.phi); + hsv(S.hue + H0, 0.8, 1, pc); + hsv(S.hue + H0 + HM, 0.65, 1, mc); + // The field turned by the drawing's clock, averaged over the 2 x 2 cells of each LED. + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = 2 * y * GW + 2 * x, q = p + GW; + const r = ar[p] + ar[p + 1] + ar[q] + ar[q + 1], i = ai[p] + ai[p + 1] + ai[q] + ai[q + 1]; + fr[y * W + x] = 0.25 * (r * cp - i * sp); + fi[y * W + x] = 0.25 * (r * sp + i * cp); + } + for (let y = 0; y < H; y++) { + const yu = ((y + H - 1) % H) * W, yd = ((y + 1) % H) * W, y0 = y * W; + for (let x = 0; x < W; x++) { + const xl = x === 0 ? W - 1 : x - 1, xr = x === W - 1 ? 0 : x + 1, p = y0 + x; + // Distance in LEDs to the zero line of each part, |f| / |grad f|. + const rx = fr[y0 + xr] - fr[y0 + xl], ry = fr[yd + x] - fr[yu + x]; + const ix = fi[y0 + xr] - fi[y0 + xl], iy = fi[yd + x] - fi[yu + x]; + const dr = (2 * abs(fr[p])) / sqrt(rx * rx + ry * ry + 1e-6), di = (2 * abs(fi[p])) / sqrt(ix * ix + iy * iy + 1e-6); + const lr = dr < LW ? (1 - dr / LW) ** LP : 0, li = di < LW ? (1 - di / LW) ** LP : 0; + // Im = 0 (crests and troughs) in the first colour, Re = 0 in the second. + const v = li > lr ? li : lr, col = li > lr ? pc : mc; + display.setPixel(x, y, round(255 * col[0] * v), round(255 * col[1] * v), round(255 * col[2] * v)); + } + } + const D = S.def; + for (let j = 0; j < D.length; j += 3) { + const x = round(D[j]), y = round(D[j + 1]), n = D[j + 2]; + // A small + for a +1 defect and a - for a -1. + for (let e = -1; e <= 1; e++) { + display.setPixel((x + e + W) % W, y % H, 255, 255, 255); + if (n > 0 && e) display.setPixel(x % W, (y + e + H) % H, 255, 255, 255); + } + } +} From 282dde560871fd4182c8970da111cf3f4133bc83 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 23:26:06 +0000 Subject: [PATCH 07/16] =?UTF-8?q?Jelly=20Layers=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9D=84=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 --- .../pattern-study/patterns/jelly-layers.js | 243 ++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 incoming/pattern-study/patterns/jelly-layers.js diff --git a/incoming/pattern-study/patterns/jelly-layers.js b/incoming/pattern-study/patterns/jelly-layers.js new file mode 100644 index 000000000..ab1073c51 --- /dev/null +++ b/incoming/pattern-study/patterns/jelly-layers.js @@ -0,0 +1,243 @@ +// ===== Patternflow pattern ===== +// Title: Jelly Layers +// Family: cosmetic +// Idea: Sheer jelly polish is brushed down a milky white base in overlapping strokes, each a thin translucent coat with brush lines along it and a bead of polish along its edges; where coats overlap the colour deepens by multiplying, two coats the square of one, the brush lines level out over a few seconds, a gloss window and a passing band of shine sit on top, and every third pass a remover pad wipes the deepest band back to a faint stain. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Beer-Lambert absorption (Pierre Bouguer, 1729; August Beer, 1852), pattern round 40 +// Round: 40 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Density=0.35..1.2, Matte=0..1 +// +// Knob 1: Hue. The polish, once round the wheel. At 0 raspberry pink, at 0.1 cherry, at 0.2 tangerine, at 0.25 yellow, at 0.35 lime, at 0.5 jade, at 0.6 teal, at 0.75 cobalt, at 0.85 violet. From 0.12 (scarlet) to 0.32 (yellow-green) the grey part of the absorption is taken away, so a deep stack stays a bright colour and never goes brown or olive. +// Knob 2: Speed. At 2 the brush or the pad sets off every 1.6 s, two strokes and then a wipe, and crosses the panel in about 1.3 s; brush lines level with a time constant of 2.5 s. At 0.2 everything is ten times slower, at 3 half as fast again. +// Knob 3: Density. The optical depth of one coat. For the opening pink one coat passes 66% of the green, two coats 44% and three 29%; at 1.2 one coat passes 24% and two 6%, and the overlaps go to deep raspberry. +// Knob 4: Matte. At 0 a high gloss: a two-pane window reflected in the polish, bent where the coats change thickness, a band of shine running down every 5 s, and a bright rim on the bead edges that face the light. Turned up the reflections spread and fade, and at 1 a matte top coat leaves only a pale frosted veil over the colour. +// Buttons: K1 tap sends the next stroke or wipe at once. + +const W = 64, H = 128, N = W * H; +// Live strokes kept apart while their brush marks level; older coats are +// baked into one field. +const SLOTS = 4; +// Seconds between passes (a stroke or a wipe) at Speed 2, brush and pad speed +// in LEDs a second, how long brush lines take to level, the age at which a +// coat is baked, the run before the first frame, and the shine's period. +const PER = 1.6, BRUSH = 120, LEVEL = 2.5, BAKE = 7, PREROLL = 9, SWEEP = 5; +// The bare base, a warm milky white; the grey part of the absorption; opening hue. +const BASE = [0.93, 0.91, 0.88], GREY = 0.18, H0 = 0.9; +const { floor, min, max, abs, sqrt, sin, cos, exp, round, random, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => { + const t = cl((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; + +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] = [v, q, p, p, t, v][i]; + out[1] = [t, v, v, q, p, p][i]; + out[2] = [p, p, t, v, v, q][i]; +} + +export function setup(params) { + const S = { + t: 0, next: 0, n: 0, begun: false, hue: 0, speed: 2, dens: 0.35, matte: 0.06, + St: new Float32Array(N), T: new Float32Array(N), Tb: new Float32Array(N), tmp: new Float32Array(N), strokes: [], wipes: [], col: [0, 0, 0], + }; + for (let k = 0; k < SLOTS; k++) S.strokes.push({ live: false, cov: new Float32Array(N), rid: new Float32Array(N) }); + params.s = S; +} + +// A gently bowed path down or up the panel at a small slant, through x = xm +// at mid height; s runs along it from where it enters, u across it. +function path(xm, ang, down, bow) { + const ux = sin(ang) * (down ? 1 : -1), uy = cos(ang) * (down ? 1 : -1); + const back = 70 / cos(ang); + return { ux, uy, x0: xm - ux * back, y0: H / 2 - uy * back, len: 2 * back, bow }; +} +const across = (P, dx, dy, s) => dx * P.uy - dy * P.ux - P.bow * sin((PI * s) / P.len); + +// How much polish lies along the column at x. +function column(S, x) { + let sum = 0; + for (let y = 0; y < H; y += 2) sum += S.T[y * W + x]; + return sum; +} + +function newStroke(S) { + let st = S.strokes.find((q) => !q.live); + if (!st) { + // Every slot busy: bake the oldest now. + st = S.strokes.reduce((a, b) => (a.age > b.age ? a : b)); + bake(S, st); + } + // The barest of three places. + let xm = 0, least = 1e9; + for (let k = 0; k < 3; k++) { + const x = 6 + floor(random() * 52), c = column(S, x); + if (c < least) { + least = c; + xm = x; + } + } + const ang = (random() - 0.5) * 0.5; + Object.assign(st, path(xm, ang, random() < 0.6, (random() - 0.5) * 10), { + live: true, age: 0, sb: 0, hw: 7 + random() * 5, p1: random() * 2 * PI, p2: random() * 2 * PI, p3: random() * 2 * PI, + }); + st.cov.fill(0); + st.rid.fill(0); +} + +function bake(S, st) { + const St = S.St, { cov, rid } = st; + for (let p = 0; p < N; p++) if (cov[p] > 0) St[p] += cov[p] * (1 + 0.06 * rid[p]); + st.live = false; +} + +// Lay the coat the brush has reached this step. +function paint(st, d) { + const s0 = st.sb; + st.sb = min(st.len + 20, st.sb + BRUSH * d); + const { ux, uy, x0, y0, hw, cov, rid, len } = st, R = 0.7 * hw; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const dx = x - x0, dy = y - y0, s = dx * ux + dy * uy; + // Only the strip the brush front has touched, including its rounded front. + if (s > st.sb || s < s0 - R - 1) continue; + const u = across(st, dx, dy, s); + if (abs(u) > hw + 1) continue; + const e = st.sb - s, hwe = hw * sqrt(min(1, e / R)), q = abs(u) / max(hwe, 0.01); + if (q >= 1) continue; + // A flat coat with a soft edge, a bead of polish along each edge, and + // more polish where the stroke starts than where it ends. + const body = 1 - sm(0.78, 1, q), bead = 0.5 * exp(-(((q - 0.87) / 0.07) ** 2)); + const fp = (body + bead) * (1.2 - 0.4 * (s / len)); + const p = y * W + x; + if (fp > cov[p]) { + cov[p] = fp; + // Brush marks: lines along the stroke, a few LEDs apart. + rid[p] = 0.6 * sin((2 * PI * u) / 3.4 + st.p1 + 0.5 * sin(s * 0.05 + st.p3)) + 0.4 * sin((2 * PI * u) / 5.3 + st.p2); + } + } +} + +// A remover pad along a path takes 94% of every coat under it, in streaks. +function wipe(S, w, d) { + const s0 = w.sb; + w.sb += BRUSH * d; + const { ux, uy, x0, y0, hw } = w, R = 0.8 * hw, fields = [S.St]; + for (const st of S.strokes) if (st.live) fields.push(st.cov); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const dx = x - x0, dy = y - y0, s = dx * ux + dy * uy; + if (s > w.sb || s < s0 - R) continue; + const u = across(w, dx, dy, s), q = abs(u) / hw; + if (q >= 1) continue; + // The pad's front is round: an LED is wiped once, when the front reaches it. + const lag = R * q * q; + if (w.sb - s < lag || s0 - s >= lag) continue; + const m = (1 - sm(0.7, 1, q)) * (0.94 + 0.06 * sin((2 * PI * u) / 2.7 + w.p1)), k = 1 - 0.94 * m, p = y * W + x; + for (const F of fields) F[p] *= k; + } +} + +function step(S, d) { + S.t += d; + if (S.t >= S.next) { + S.next = S.t + PER; + // Two strokes, then one wipe where the polish is deepest. + if (S.n % 3 === 2) { + let best = -1, bx = 32; + for (let x = 8; x < 56; x += 2) { + const c = column(S, x); + if (c > best) { + best = c; + bx = x; + } + } + const ang = (random() - 0.5) * 0.3; + S.wipes.push(Object.assign(path(bx, ang, random() < 0.5, (random() - 0.5) * 6), { sb: 0, hw: 15, p1: random() * 2 * PI })); + } else newStroke(S); + S.n++; + } + for (const st of S.strokes) + if (st.live) { + st.age += d; + if (st.sb < st.len + 20) paint(st, d); + else if (st.age > BAKE) bake(S, st); + } + for (const w of S.wipes) wipe(S, w, d); + S.wipes = S.wipes.filter((w) => w.sb < w.len + 20); + // The total thickness, in coats, with the brush marks of the live coats + // levelling out as they age. + const T = S.T; + T.set(S.St); + for (const st of S.strokes) + if (st.live) { + const a = 0.3 * exp(-st.age / LEVEL) + 0.06, { cov, rid } = st; + for (let p = 0; p < N; p++) if (cov[p] > 0) T[p] += cov[p] * (1 + a * rid[p]); + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0.35, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.dens = cl(k[2], 0.35, 1.2); + S.matte = cl(k[3], 0, 1); + if (!S.begun) { + for (let i = 0; i < PREROLL * 30; i++) step(S, 1 / 30); + S.begun = true; + } + // K1 sends the next stroke or wipe now. + if (input.btnPressed && input.btnPressed[0]) S.next = S.t; + step(S, (d * S.speed) / 2); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { T, Tb, tmp, col } = S, h = S.hue + H0, hh = h - floor(h); + hsv(h, 1, 1, col); + // Orange, yellow and lime would go brown or olive where the coats stack + // deep, so there the grey part of the absorption is taken away. + const warm = 1 - sm(0.1, 0.18, min(abs(hh - 0.12), 1 - abs(hh - 0.12))); + const g = GREY * (1 - warm), D = S.dens; + // Absorbance per coat in each channel: what the hue does not pass, plus grey. + const ar = D * (1 - col[0] + g), ag = D * (1 - col[1] + g), ab = D * (1 - col[2] + g); + // The surface for the gloss: the thickness smoothed over 3 x 3 LEDs. + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) tmp[y * W + x] = (T[y * W + max(0, x - 1)] + T[y * W + x] + T[y * W + min(W - 1, x + 1)]) / 3; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) Tb[y * W + x] = (tmp[max(0, y - 1) * W + x] + tmp[y * W + x] + tmp[min(H - 1, y + 1) * W + x]) / 3; + const mt = S.matte, xl = 22 + 11 * sin(S.t * 0.35), gl = 1 - 0.9 * mt, soft = 0.7 + 4 * mt; + // Every SWEEP seconds a light passes down the panel on a slant, and its + // reflection runs down the coats as a band of shine. + const ph = (S.t % SWEEP) / 2.2, yb = -30 + ph * 190; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, tau = T[p]; + // Beer-Lambert: transmittance multiplies, so n coats are the nth power of one. + let r = BASE[0] * exp(-ar * tau), gg = BASE[1] * exp(-ag * tau), b = BASE[2] * exp(-ab * tau); + // Gloss: a tall window, two panes, reflected in the polish and bent by its slope. + const gx = Tb[y * W + min(W - 1, x + 1)] - Tb[y * W + max(0, x - 1)], coat = sm(0.05, 0.35, tau); + const gy = Tb[min(H - 1, y + 1) * W + x] - Tb[max(0, y - 1) * W + x]; + const xs = x + 3 * gx - xl; + const pane = sm(-2.2 - soft, -2.2 + soft, xs) * (1 - sm(2.2 - soft, 2.2 + soft, xs)) + 0.5 * sm(4.6 - soft, 4.6 + soft, xs) * (1 - sm(6 - soft, 6 + soft, xs)); + const ys = y + 0.35 * x + 4 * gy - yb, band = ph < 1 ? exp(-((ys / (3 + 6 * mt)) ** 2)) : 0; + // The bead along a stroke's edge catches the room light from the upper + // left as a thin bright rim on the side that faces it. + const rim = sm(0.06, 0.2, 0.3 * gx + 0.4 * gy); + const spec = coat * gl * (0.9 * pane + 0.75 * band + 0.55 * rim); + // A matte top coat scatters a pale veil over the colour. + const veil = coat * 0.32 * mt; + r += (1 - r) * min(1, spec + veil); + gg += (1 - gg) * min(1, spec + veil); + b += (1 - b) * min(1, spec + veil); + display.setPixel(x, y, round(255 * r), round(255 * gg), round(255 * b)); + } +} From 072a4152fcdd4672accd542b3a03ea031337e4a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 23:28:09 +0000 Subject: [PATCH 08/16] =?UTF-8?q?Perfume=20Tears=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9D=84=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 --- .../pattern-study/patterns/perfume-tears.js | 387 ++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 incoming/pattern-study/patterns/perfume-tears.js diff --git a/incoming/pattern-study/patterns/perfume-tears.js b/incoming/pattern-study/patterns/perfume-tears.js new file mode 100644 index 000000000..d2be63600 --- /dev/null +++ b/incoming/pattern-study/patterns/perfume-tears.js @@ -0,0 +1,387 @@ +// ===== Patternflow pattern ===== +// Title: Perfume Tears +// Family: cosmetic +// Idea: The inside wall of a glass of tinted perfume up close: a thin film climbs from the pool at the bottom, pulled by the Marangoni stress of the evaporating alcohol, gathers into a glossy rim where the stress can no longer carry it, and the rim swells at a Rayleigh-Plateau spacing into tears that slide back down on bright legs into the pool. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: tears of wine (James Thomson, 1855; Carlo Marangoni, 1865), pattern round 40 +// Round: 40 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Rim=0..1, Warmth=0..1 +// +// Knob 1: Hue. The liquid's tint, once round the wheel: at 0 rose violet, 0.1 rose, 0.2 apricot, 0.3 butter, 0.45 green, 0.65 aqua, 0.75 sky blue, 0.9 indigo. From apricot to lime the film and the pool are lifted and paled, so they never read brown or olive. Thin film is dim, thick liquid bright. +// Knob 2: Speed. At 2 the film climbs from the pool at 10.5 rows a second, slowing to 6.9 under the rim as it thickens; about one tear a second lets go, and a tear of radius 3.4 LEDs slides down at 35 rows a second. At 0.2 all of it ten times slower, at 3 half as fast again. +// Knob 3: Rim. How thick the rim grows before it sheds: a critical radius of 2.6 LEDs at 0 and 3.9 at 1. The tears follow the rim's mean radius r at 9.02 r apart: five across the panel at 0 (13 LEDs apart), four at 0.5 and three at 1, while the tears' radius grows from 3.4 to 5.1 LEDs and they come three times less often and fall faster. +// Knob 4: Warmth. How fast the alcohol evaporates. Turned up, the Marangoni stress grows by up to 60%, the film carries 19 instead of 10.5 units of liquid a second and climbs at 19 rows a second, and the rim stands 102 rows above the pool instead of 84, so the dry glass above shrinks from 27 rows to 9 and almost twice as many tears fall. +// Buttons: K1 tap swirls the glass: the pool sloshes up and leaves a coat as thick as the stress can still lift (nearly twice the film at the bottom), the tears and legs washed off. The thin film climbs back up under it in scalloped arches, most of the way in 15 s, while the coat runs into the rim, which lets its first tear go about a second after the tap. + +const W = 64, H = 128; +// The pool's surface: rows from YP down are the pool, the film climbs the +// wall above it, NZ cells of one LED, cell 0 just above the surface. +const YP = 112, NZ = YP; +// Film flux up the wall, per unit width: q = A(z) h^2 - B h^3, the +// Marangoni stress A(z) (it falls with height as the alcohol is spent) +// against gravity B. h is in units of the film's thickness at the pool. +const B = 5, A0 = 15, H0F = 1; +// The rim: volume per column into radius in LEDs, r = sqrt(2 V KR / pi). +const KR = 0.06; +// Rayleigh-Plateau: a cylinder of radius r breaks fastest at 9.02 r. +const RP = 9.02; +// Growth rate of the fastest mode on the rim, per second. +const SIG = 1.4; +// Drops: radius from volume, speed from radius, the leg they leave. +const KD = 0.062, KU = 3.5, UF = 5, HLEG = 2, CLEG = 2; +// A drop is a cap on the wall: its radius goes as the cube root of its volume. +const rdOf = (v) => Math.cbrt(v * KD); +// Legs drain under gravity alone, flux BL L^3 / 3 down the wall, and soak +// back into the climbing film with time constant TL. +const BL = 3, TL = 1.3; +// Climbing waves on the film, per second. +const WF = 1.3; +// Opening tint: rose violet. +const HUE0 = 0.86; +const { floor, ceil, min, max, abs, sqrt, exp, sin, cos, round, random, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +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); + if (i === 0) (out[0] = v), (out[1] = t), (out[2] = p); + else if (i === 1) (out[0] = q), (out[1] = v), (out[2] = p); + else if (i === 2) (out[0] = p), (out[1] = v), (out[2] = t); + else if (i === 3) (out[0] = p), (out[1] = q), (out[2] = v); + else if (i === 4) (out[0] = t), (out[1] = p), (out[2] = v); + else (out[0] = v), (out[1] = p), (out[2] = q); +} + +const q = (a, h) => a * h * h - B * h * h * h; +// Godunov flux across a face with hl below and hr above: the least flux +// between them if hl <= hr, the most if hl > hr. q has its one maximum +// for h > 0 at 2a / 3B. +function god(a, hl, hr) { + if (hl <= hr) return min(q(a, hl), q(a, hr)); + const hm = (2 * a) / (3 * B); + return hm > hr && hm < hl ? q(a, hm) : max(q(a, hl), q(a, hr)); +} + +// The rim stands where the stress can no longer carry the film's flux: +// the largest flux A^3 4 / 27 B^2 falls to q0. +function balance(S) { + const a0 = A0 * (1 + 0.6 * S.warm), q0 = q(a0, H0F), ar = Math.cbrt((27 * B * B * q0) / 4); + S.a0 = a0; + S.q0 = q0; + S.zr = 84 + 18 * S.warm; + // A(z) = a0 (1 - z / Z0), reaching ar at the rim. + S.Z0 = S.zr / (1 - ar / a0); +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, rim: 0, warm: 0.06, begun: false, t: 0, + a0: A0, q0: 10, zr: 90, Z0: 400, + h: new Float32Array(W * NZ), L: new Float32Array(W * NZ), F: new Float32Array(NZ + 1), V: new Float32Array(W), r: new Float32Array(W), cool: new Float32Array(W), + re: new Float32Array(W), im: new Float32Array(W), drops: [], rip: [], sw: 0, + T: new Float32Array(W * H), c: [0, 0, 0], rbar: 2, tau: new Float32Array(NZ), lam: new Float32Array(NZ), hm: new Float32Array(NZ), + }; +} + +// Steady film below the rim: the thin root of q(A(z), h) = q0. +function steady(S, z) { + const a = S.a0 * (1 - z / S.Z0); + let h = H0F; + for (let k = 0; k < 30; k++) { + const f = q(a, h) - S.q0, d = 2 * a * h - 3 * B * h * h; + if (abs(d) < 1e-6) break; + h = cl(h - f / d, 0.2, (2 * a) / (3 * B)); + } + return h; +} + +function init(S) { + const zi = floor(S.zr); + for (let z = 0; z < NZ; z++) { + const hs = z < zi ? steady(S, z) : 0; + for (let x = 0; x < W; x++) S.h[z * W + x] = hs; + } + const rc = rimCrit(S), Vc = (PI * rc * rc) / (2 * KR); + for (let x = 0; x < W; x++) S.V[x] = Vc * (0.7 + 0.25 * random()); +} + +const rimCrit = (S) => 2.6 + 1.3 * S.rim; + +// One step of the film, ds seconds: Godunov fluxes up each column, the +// rim taking what reaches it, and a little sideways spreading. +function film(S, ds) { + const { h, F, V } = S, zi = floor(S.zr), HM = S.hm; + for (let z = 0; z < zi; z++) HM[z] = (2 * S.a0 * (1 - z / S.Z0)) / (3 * B); + for (let x = 0; x < W; x++) { + F[0] = god(S.a0, H0F, h[x]); + for (let z = 1; z < zi; z++) F[z] = god(S.a0 * (1 - z / S.Z0), h[(z - 1) * W + x], h[z * W + x]); + const top = h[(zi - 1) * W + x], fr = q(S.a0 * (1 - zi / S.Z0), top); + F[zi] = fr > 0 ? fr : 0; + for (let z = 0; z < zi; z++) { + const p = z * W + x; + h[p] = max(0, h[p] + ds * (F[z] - F[z + 1])); + } + // Film thicker than the most the stress can lift there (h > 2A / 3B) + // drains into the rim over a few seconds. Without it a swirled coat + // would settle on the slow branch of q and hang below the rim for good. + for (let z = 0; z < zi; z++) { + const p = z * W + x, ex = (h[p] - HM[z]) * min(1, (z === zi - 1 ? 6 : 0.4) * ds); + if (ex > 0) { + h[p] -= ex; + V[x] += ex; + } + } + // What reaches the rim varies slowly along it. + V[x] += ds * F[zi] * (1 + 0.35 * sin(0.29 * x + 0.7 * S.t) * sin(0.11 * x - 0.23 * S.t + 1)); + } + // Sideways: the film spreads, legs widen. + const D = 1.5 * ds; + for (let z = 0; z < zi; z++) { + const o = z * W; + let prev = h[o + W - 1], first = h[o]; + for (let x = 0; x < W; x++) { + const c = h[o + x], nx = x === W - 1 ? first : h[o + x + 1]; + h[o + x] = c + D * (prev + nx - 2 * c); + prev = c; + } + } +} + +// Legs: each column drains downward, the flux from the cell above. +function legs(S, ds) { + const L = S.L, k = (BL / 3) * ds, f = exp(-ds / TL); + for (let x = 0; x < W; x++) { + let inflow = 0; + for (let z = NZ - 1; z >= 0; z--) { + const p = z * W + x, l = L[p], out = min(l, k * l * l * l); + L[p] = (l - out + inflow) * f; + inflow = out; + } + } +} + +// The rim's sideways instability, mode by mode: sigma(k) = al k^2 - be k^4, +// fastest at k* = 2 pi / (9.02 r), growing at SIG there. +function rimStep(S, ds) { + const { V, re, im, r } = S; + let mean = 0; + for (let x = 0; x < W; x++) mean += V[x]; + mean /= W; + const rbar = sqrt((2 * mean * KR) / PI); + S.rbar = rbar; + const ks = (2 * PI) / max(4, RP * rbar), al = (2 * SIG) / (ks * ks), be = al / (2 * ks * ks); + for (let n = 0; n < W; n++) { + let a = 0, b = 0; + for (let x = 0; x < W; x++) { + const ph = (2 * PI * n * x) / W; + a += V[x] * cos(ph); + b -= V[x] * sin(ph); + } + const k = (2 * PI * (n <= W / 2 ? n : n - W)) / W, g = n === 0 ? 1 : exp((al * k * k - be * k * k * k * k) * ds); + re[n] = a * g; + im[n] = b * g; + } + let after = 0; + for (let x = 0; x < W; x++) { + let a = 0; + for (let n = 0; n < W; n++) { + const ph = (2 * PI * n * x) / W; + a += re[n] * cos(ph) - im[n] * sin(ph); + } + V[x] = max(0, a / W + 0.02 * (random() - 0.5) * mean * ds * 30); + after += V[x]; + } + // Troughs cut off at zero would make liquid: scale back to the volume. + const k = after > 0 ? (mean * W) / after : 1; + for (let x = 0; x < W; x++) { + V[x] *= k; + r[x] = sqrt((2 * V[x] * KR) / PI); + } +} + +// The thickest place on the rim, once past the critical radius, lets go a +// tear: the swelling over one wavelength runs into a drop. +function shed(S) { + const { V, r, cool } = S, rc = rimCrit(S); + let xb = -1; + for (let x = 0; x < W; x++) if (r[x] >= rc && cool[x] <= 0 && (xb < 0 || r[x] > r[xb])) xb = x; + if (xb < 0) return; + const lam = RP * S.rbar, half = max(3, lam / 2), keep = (PI * (0.45 * rc) ** 2) / (2 * KR); + let v = 0; + for (let d = -floor(half); d <= floor(half); d++) { + const x = (xb + d + W) % W, w = cos((PI * d) / (2 * half)) ** 2, take = max(0, V[x] - keep) * w; + V[x] -= take; + v += take; + cool[x] = max(cool[x], 0.6 * w); + } + S.drops.push({ x: xb + 0.5, z: S.zr - r[xb], v, age: 0, ph: random() * 6.3 }); +} + +function drops(S, ds) { + const L = S.L, zi = floor(S.zr); + for (const d of S.drops) { + d.age += ds; + const rd = rdOf(d.v), ut = min(90, KU * rd * rd - UF), u = max(0, ut) * (1 - exp(-d.age / 0.5)); + const z0 = d.z; + d.z -= u * ds; + d.x += 1.6 * sin(d.ph + d.age * 1.7) * ds; + // The leg: liquid left behind at HLEG, a third as wide as the drop. + const sg = 0.32 * rd + 0.25; + for (let z = max(0, floor(d.z + rd)); z <= min(zi - 1, floor(z0 + rd)); z++) + for (let dx = -3; dx <= 3; dx++) { + const x = floor(d.x) + dx, xx = (x + W) % W, g = exp(-((x + 0.5 - d.x) ** 2) / (2 * sg * sg)), p = z * W + xx; + if (L[p] < HLEG * g) L[p] = HLEG * g; + } + d.v -= CLEG * rd * (z0 - d.z); + // Too small to slide, it stops and sinks into its leg. + if (ut < 4 && d.age > 1) d.v = 0; + if (d.z <= 0) { + S.rip.push({ x: d.x, age: 0, a: min(1.5, rd / 2) }); + d.v = 0; + } + } + S.drops = S.drops.filter((d) => d.v > 2); + for (const rp of S.rip) rp.age += ds; + S.rip = S.rip.filter((rp) => rp.age < 2.5); +} + +function step(S, ds) { + S.t += ds; + const n = max(1, Math.ceil(ds / 0.008)), sub = ds / n; + for (let k = 0; k < n; k++) { + film(S, sub); + legs(S, sub); + } + rimStep(S, ds); + for (let x = 0; x < W; x++) S.cool[x] -= ds; + shed(S); + drops(S, ds); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.rim = cl(k[2], 0, 1); + S.warm = cl(k[3], 0, 1); + balance(S); + if (!S.begun) { + init(S); + for (let i = 0; i < 150; i++) step(S, 1 / 30); + S.begun = true; + } + if (input.btnPressed && input.btnPressed[0]) { + // Swirl: a fresh coat over the whole film, the tears and legs washed off. + const zi = floor(S.zr); + const ph = random() * 6.3; + // As thick as the stress can still lift at each height, a little less + // here and there: thicker and it would hang as the slow branch. + for (let z = 0; z < zi; z++) { + const hm = (2 * S.a0 * (1 - z / S.Z0)) / (3 * B); + for (let x = 0; x < W; x++) S.h[z * W + x] = max(S.h[z * W + x], hm * (0.9 + 0.05 * sin(0.3 * x + ph) + 0.03 * sin(0.71 * x - ph))); + } + S.L.fill(0); + S.drops = []; + S.sw = 1; + } + S.sw = max(0, S.sw - (d * S.speed) / 2 / 1.5); + step(S, (d * S.speed) / 2); +} + +// Kinematic waves on the climbing film travel up at c = dq/dh. Each row's +// arrival time from the pool, and how many rows one wave spans there. +function waves(S) { + const tau = S.tau, lam = S.lam; + let t = 0; + for (let z = 0; z < NZ; z++) { + const a = S.a0 * (1 - z / S.Z0), hs = steady(S, z), c = max(0.5, 2 * a * hs - 3 * B * hs * hs); + tau[z] = t; + lam[z] = c / WF; + t += 1 / c; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { h, r, T, c, L, tau, lam } = S, zi = floor(S.zr); + T.fill(0); + waves(S); + // Thickness on the wall: the film, the legs, the rim's half-cylinder + // hanging from the line it climbs to, the drops as caps. + for (let y = 0; y < YP; y++) { + const z = YP - 1 - y; + for (let x = 0; x < W; x++) T[y * W + x] = (z < zi ? h[z * W + x] : 0) + L[z * W + x]; + } + for (let x = 0; x < W; x++) { + const rr = r[x], zc = S.zr - 1.5 * rr; + for (let y = max(0, floor(YP - 1 - S.zr)); y <= min(YP - 1, YP - 1 - floor(zc - 1.5 * rr - 1)); y++) { + const dz = (YP - 1 - y + 0.5 - zc) / 1.5, t2 = rr * rr - dz * dz; + if (t2 > 0) T[y * W + x] = max(T[y * W + x], 1.2 + 1.4 * sqrt(t2)); + } + } + for (const d of S.drops) { + const rd = rdOf(d.v), yc = YP - 1 - d.z + 0.5; + // Never above the line the rim hangs from. + for (let y = max(ceil(YP - 1 - S.zr), floor(yc - 2.4 * rd - 1)); y <= min(YP - 1, floor(yc + rd + 1)); y++) + for (let x = floor(d.x - rd - 1); x <= floor(d.x + rd + 1); x++) { + // Round in front, drawn out behind into the leg. + const dx = x + 0.5 - d.x, dy = y + 0.5 - yc, ey = dy < 0 ? dy / 2.4 : dy, t2 = rd * rd - dx * dx * (dy < 0 ? 1 - dy / (2.4 * rd) : 1) ** 2 - ey * ey; + if (t2 <= 0) continue; + const p = y * W + ((x + W) % W); + T[p] = max(T[p], 1.6 + 1.5 * sqrt(t2)); + } + } + const hue = S.hue + HUE0, hh = hue - floor(hue); + // Orange and yellow read brown when dim: there the liquid is lifted and + // paled. + const warm = cl(1.4 - abs(hh - 0.12) / 0.11, 0, 1); + // Key light from the upper left and a soft one from below right, seen + // straight on: half vectors. + const H1 = half(-0.45, -0.6, 0.66), H2 = half(0.35, 0.75, 0.56); + for (let y = 0; y < YP; y++) { + const z = YP - 1 - y, wv = z < zi ? cl((lam[z] - 2) / 4, 0, 1) : 0; + for (let x = 0; x < W; x++) { + const p = y * W + x, t = T[p]; + const gx = (T[y * W + ((x + 1) % W)] - T[y * W + ((x + W - 1) % W)]) / 2; + const gy = ((y < YP - 1 ? T[p + W] : t) - (y > 0 ? T[p - W] : t)) / 2; + const nn = sqrt(gx * gx + gy * gy + 1); + const n1 = (-gx * H1[0] - gy * H1[1] + H1[2]) / nn, n2 = (-gx * H2[0] - gy * H2[1] + H2[2]) / nn; + const thick = min(1, t / 2.2), spec = thick * (0.95 * max(0, n1) ** 36 + 0.28 * max(0, n2) ** 6); + // The film's climbing waves. + const sh = wv > 0 ? 1 + 0.09 * wv * sin(2 * PI * WF * (S.t - tau[z]) + 1.3 * sin(0.31 * x + 0.07 * z + 0.2 * S.t) + 0.8 * sin(0.13 * x - 0.11 * z)) : 1; + const body = 1 - exp(-t / 2.4), v = 0.04 + (1.05 + 0.1 * warm) * body ** 1.9 * sh; + hsv(hue, 0.82 - 0.4 * body - 0.25 * warm, v, c); + display.setPixel(x, y, round(255 * min(1, c[0] + spec)), round(255 * min(1, c[1] + spec)), round(255 * min(1, c[2] + spec))); + } + } + // The pool: liquid seen through the glass, a bright meniscus, ripples + // where tears come in. + for (let x = 0; x < W; x++) { + let off = 0, glow = 0; + for (const rp of S.rip) { + const dx = abs(x + 0.5 - rp.x), fr = rp.age * 18; + const e = exp(-((dx - fr) ** 2) / 12) * exp(-rp.age / 0.7) * rp.a; + off += e * sin(dx * 1.1 - rp.age * 9); + glow += e; + } + // A swirl throws the surface up the wall and lets it fall back. + off -= 4 * S.sw * (0.7 + 0.3 * sin(0.21 * x + 9 * S.sw)); + for (let y = YP; y < H; y++) { + const u = (y - YP) / (H - YP), dy = y - YP - off, men = exp(-(dy * dy) / 1.5); + const lit = 0.24 * exp(-max(0, dy) / 4); + hsv(hue + 0.02, 0.72 - 0.25 * men - 0.25 * warm, 0.34 + 0.2 * warm - 0.12 * u + lit + 0.35 * men + 0.25 * glow * men, c); + display.setPixel(x, y, round(255 * min(1, c[0] + 0.3 * men)), round(255 * min(1, c[1] + 0.3 * men)), round(255 * min(1, c[2] + 0.3 * men))); + } + } +} + +function half(x, y, z) { + const n = sqrt(x * x + y * y + (z + 1) ** 2); + return [x / n, y / n, (z + 1) / n]; +} From acb8367ffd5a0b6465d6d6720fa2d45329bb65f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 23:28:55 +0000 Subject: [PATCH 09/16] =?UTF-8?q?Continuum=20Random=20Tree=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=EC=9D=84=20=EA=B3=A0=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../pattern-study/patterns/continuum-random-tree.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/incoming/pattern-study/patterns/continuum-random-tree.js b/incoming/pattern-study/patterns/continuum-random-tree.js index 0bdb4c843..d4c9208ec 100644 --- a/incoming/pattern-study/patterns/continuum-random-tree.js +++ b/incoming/pattern-study/patterns/continuum-random-tree.js @@ -11,10 +11,10 @@ // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Dive=0..1, Cut=0..1 // -// Knob 1: Hue. Turns the tree round the wheel: at 0 azure crowns on pale stems with green and violet limbs either side, at 0.25 magenta, 0.5 apricot, 0.75 green. The subtrees above the cut always spread over half the wheel from left to right. +// Knob 1: Hue. Turns the tree round the wheel: at 0 azure crowns on dim grey stems with green and violet limbs either side, at 0.25 magenta, 0.5 apricot, 0.75 green. The subtrees above the cut always spread over half the wheel from left to right. // Knob 2: Speed. At 2 the tree grows 1.6 units of length a second, 60 to 120 rows of branch depending on how tall the tree stands, the dive takes 3.2 s and the two trees cross-fade for 0.7 s, so a dive comes every 9 to 10 s at Dive 0. At 0.2 all of it ten times slower, at 3 half as fast again. // Knob 3: Dive. The mass of the subtree the view dives into: 0.3 of the tree at 0, 0.13 at 0.5, 0.06 at 1. Its time is stretched by one over the mass (3.3 to 17 times) and its heights by one over the square root (1.8 to 4 times), Brownian scaling, and what fills the panel is again a tree of mass 1. Deeper dives keep fewer leaves, so the new tree starts sparser and grows longer before the next dive. -// Knob 4: Cut. A level at 20% of the height at 0 and 90% at 1 (24% at the start). Every subtree above it gets its own hue, and below it the tree is pale. Turned up, the one crown splits into more and smaller trees, each of them a continuum random tree in its own right. +// Knob 4: Cut. A level at 20% of the height at 0 and 90% at 1 (24% at the start). Every subtree above it gets its own hue and a white bead where its branch crosses the level, and below it the tree is dim and grey. Turned up, the one crown splits into more and smaller trees, each of them a continuum random tree in its own right. // Buttons: K1 tap dives now, once the tree holds 30 leaves. const W = 64, H = 128, N = 8192, NL = 256; @@ -101,8 +101,8 @@ function subExcursion(S, T, g) { // tree at that height, and its samples are the part of the tree above it: // start a, end b, highest point top, parent run par one level down, and fx, // where the branch is drawn. The follow keeps the order of the runs, so no -// two subtrees cross. -// Built a few levels a frame while the view dives, n at a time. +// two subtrees cross. Built count levels at a time, a few a frame while the +// view dives. const RA = new Int32Array(N + 2), RB = new Int32Array(N + 2), RT = new Float32Array(N + 2); function build(T, count) { const E = T.E, out = T.lv, cs = (SLOPE * N) / W, end = min(NL, out.length + count); @@ -359,7 +359,7 @@ export function update(dt, input, params) { } } S.T = tree(best, NL); - // Open a second before the first dive. + // Open a second before the first dive at Speed 2. grow(S.T, LEND - VGROW); S.begun = true; } From ff32cfde99efda8a8a50c527d41f5493f93c0c4b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 23:30:24 +0000 Subject: [PATCH 10/16] =?UTF-8?q?Continuum=20Random=20Tree=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EA=B0=92=EC=9D=84=20=EA=B3=A0=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../pattern-study/patterns/continuum-random-tree.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/incoming/pattern-study/patterns/continuum-random-tree.js b/incoming/pattern-study/patterns/continuum-random-tree.js index d4c9208ec..52312ebc3 100644 --- a/incoming/pattern-study/patterns/continuum-random-tree.js +++ b/incoming/pattern-study/patterns/continuum-random-tree.js @@ -165,12 +165,13 @@ function tree(E, n) { return T; } -// Checked against the theorem: over 12,000 uniform points the distance to -// the root 2e(U) exceeds h with probability within 0.01 of exp(-h^2/2) at -// every h from 0.5 to 3 (Rayleigh, mean 1.245 against 1.253), and so does a -// subtree after the dive's stretching. The spanned tree of 40 leaves is 8.44 -// long on average; adding the grid's missed minima, 2 x 0.583 / sqrt(N) a -// branch, gives 8.95 against line-breaking's E[C_40] = 8.92. +// Checked against the theorem: over 16,000 uniform points of 2,000 trees +// the distance to the root 2e(U) exceeds h with probability within 0.008 of +// exp(-h^2/2) for h from 0.5 to 3 (Rayleigh), the mean 1.241 against 1.253, +// short by about the 0.013 the grid misses at the minimum; subtrees after +// the dive's stretching stay within 0.012. The spanned tree of 40 leaves is +// 8.40 long on average; adding the grid's missed minima, 2 x 0.583 / sqrt(N) +// a branch, gives 8.92 against line-breaking's E[C_40] = 8.916. // // Aldous's line-breaking, read off the excursion: a new uniform leaf i // joins the tree spanned by the root and the leaves so far at the higher of From e0813eca7c32d7bb3eecefaaf43193df4c3fcaa8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 23:30:48 +0000 Subject: [PATCH 11/16] =?UTF-8?q?Brownian=20Frontier=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9D=84=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 --- .../patterns/brownian-frontier.js | 306 ++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 incoming/pattern-study/patterns/brownian-frontier.js diff --git a/incoming/pattern-study/patterns/brownian-frontier.js b/incoming/pattern-study/patterns/brownian-frontier.js new file mode 100644 index 000000000..b69af65a1 --- /dev/null +++ b/incoming/pattern-study/patterns/brownian-frontier.js @@ -0,0 +1,306 @@ +// ===== Patternflow pattern ===== +// Title: Brownian Frontier +// Family: line +// Idea: A fine random walk scribbles outward on a lattice; the sites it has visited stay dim blue, brighter where it has stood many times, the pockets it has closed off glow faint indigo, and its frontier, the visited sites that touch the unbounded outside, is a bright gold crinkled curve that the walk keeps eating, leaving a brief orange ring where it was, and redrawing further out, while the view pulls back so the path always fills the panel and the frontier looks just as crinkled at every scale. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: frontier of planar Brownian motion, dimension 4/3 (conjectured by Benoit Mandelbrot, 1982; proved by Gregory Lawler, Oded Schramm and Wendelin Werner, 2001), pattern round 40 +// Round: 40 +// =============================== +// @matrix 64x128 +// @knobs Trail=0..1, Speed=0.2..3, Fine=1..3, Rings=0..1 +// +// Knob 1: Trail. How long a visited site stays lit cyan after the walk last stood on it, as a time constant. At 0 0.7 seconds, a short bright tangle behind the head; at 0.5 4.4 seconds; at 1 28 seconds, so the whole scribble is lit and shades from old to new. +// Knob 2: Speed. At 2 the walk takes 500 steps a second while one LED holds one site, and that times the square of the sites an LED holds as the view pulls back, so the head crosses the panel at the same pace at every scale; a path lasts about 20 seconds. At 0.2 all of it ten times slower, at 3 one and a half times faster. +// Knob 3: Fine. The fewest lattice sites across one LED. At 1 the walk starts one site an LED and the view pulls back to about four and a half by the time the path nears the lattice's edge; at 2 it starts two sites an LED; at 3 three, and the view pulls back only to six. A new value starts a new path once the knob rests. +// Knob 4: Rings. Every 0.3 seconds the frontier is stamped, and a stamped site the walk has since shut in glows orange, fading with a time constant of 0.6 seconds at 0, 0.7 at 0.06 and 15 at 1. Above 0.06 the scribble and its pockets dim too, to a tenth at 1, so the old frontiers stand inside the path as nested orange lines on black. +// Buttons: K1 tap starts a new path. + +const W = 64, H = 128; +// The lattice, at most six sites an LED; each path starts in the middle and +// ends when it comes within two sites of the lattice's edge. +const NX = 384, NY = 768, N = NX * NY, CX = 192, CY = 384; +// Steps a second at Speed 2 while one LED holds one site. +const RATE = 500; +// Seconds of the hold at the end of a path and of the fade of the old path +// behind a new one; a new path is first shown once its box is OPEN LEDs +// across. A snapshot of the frontier is kept every RING seconds. +const HOLD = 1.6, GHOST = 2.4, OPEN = 50, RING = 0.3; +const { floor, min, max, abs, exp, round, random, sqrt, log } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// Colours, 0..255: visited sites (dim and bright end), closed-off pockets +// (the bounded parts of the complement), the frontier, the trail, rings. +const VIS0 = [28, 52, 110], VIS1 = [64, 112, 180], LAKE = [30, 20, 76], FRONT = [255, 226, 146], TRAIL = [150, 232, 255], RINGC = [255, 118, 36]; + +export function setup(params) { + params.s = { + vis: new Uint8Array(N), ext: new Uint8Array(N), front: new Uint8Array(N), hits: new Uint8Array(N), + lastT: new Float32Array(N), ring: new Float32Array(N), stack: new Int32Array(N), + mapX: new Int16Array(NX), mapY: new Int16Array(NY), cntX: new Int16Array(W), cntY: new Int16Array(H), + cV: new Float32Array(W * H), cL: new Float32Array(W * H), cH: new Float32Array(W * H), aF: new Uint8Array(W * H), + aT: new Float32Array(W * H), aR: new Float32Array(W * H), rgb: new Float32Array(W * H * 3), + ghost: new Float32Array(W * H * 3), ghostT: 99, + fine: -1, vs: 1, vx: CX, vy: CY, + x: CX, y: CY, x0: CX, x1: CX, y0: CY, y1: CY, + now: 0, acc: 0, done: false, holdT: 0, steps: 0, F: 0, V: 0, snap: 0, + trail: 0.7, rings: 0.7, dim: 1, kf: -1, wait: 0, + }; +} + +// The view: sites an LED and the lattice point at the panel's middle. It +// eases toward the path's box with a margin, never pulls in again during a +// path, and always keeps the whole path on the panel. +function frame(S, w) { + const bw = S.x1 - S.x0 + 1, bh = S.y1 - S.y0 + 1, mx = (S.x0 + S.x1 + 1) / 2, my = (S.y0 + S.y1 + 1) / 2; + const a = w > 0 ? min(1, w * 2.5) : 1; + S.vx += (mx - S.vx) * a; + S.vy += (my - S.vy) * a; + const want = max(S.fine, (bw * 1.06) / W, (bh * 1.06) / H); + S.vs = max(S.vs, S.vs + (want - S.vs) * a); + // Hard bound: every visited site inside the panel with one LED to spare. + const need = max((S.vx - S.x0 + 0.5) / (W / 2 - 1), (S.x1 + 0.5 - S.vx) / (W / 2 - 1), (S.vy - S.y0 + 0.5) / (H / 2 - 1), (S.y1 + 0.5 - S.vy) / (H / 2 - 1)); + S.vs = max(S.vs, need); +} + +// A new path: the old picture is kept to fade out behind it, and the new one +// runs until its box is OPEN LEDs across before it is shown. +function restart(S, keep) { + if (keep) { + S.ghost.set(S.rgb); + S.ghostT = 0; + } + S.vis.fill(0); + S.front.fill(0); + S.hits.fill(0); + S.ring.fill(-1e9); + S.x = S.x0 = S.x1 = CX; + S.y = S.y0 = S.y1 = CY; + S.vis[CY * NX + CX] = 1; + S.vx = CX; + S.vy = CY; + S.vs = S.fine; + S.done = false; + S.holdT = 0; + S.steps = 0; + S.acc = 0; + S.snap = S.now; + for (let i = 0; i < 1800 && !S.done && max(S.x1 - S.x0, S.y1 - S.y0) < OPEN * S.vs; i++) advance(S, 1 / 30); +} + +// n steps of the simple random walk, until the path comes within two sites +// of the lattice's edge. +function walk(S, n) { + const { vis, lastT, hits } = S, now = S.now; + let { x, y, x0, x1, y0, y1 } = S; + for (let i = 0; i < n; i++) { + const r = (random() * 4) | 0; + if (r === 0) x++; + else if (r === 1) x--; + else if (r === 2) y++; + else y--; + const p = y * NX + x; + vis[p] = 1; + lastT[p] = now; + if (hits[p] < 255) hits[p]++; + S.steps++; + if (x < x0) x0 = x; + else if (x > x1) x1 = x; + if (y < y0) y0 = y; + else if (y > y1) y1 = y; + if (x < 3 || x > NX - 4 || y < 3 || y > NY - 4) { + S.done = true; + break; + } + } + Object.assign(S, { x, y, x0, x1, y0, y1 }); +} + +// The unbounded component of the complement: a 4-connected flood from the +// border of the path's box, one site wider than the path all round. A visited +// site with an outside site next to it is on the frontier. The outside only +// shrinks as the path grows, so a site that leaves the frontier never comes +// back to it. Every RING seconds the frontier is stamped into ring, so the +// sites of each past frontier remember when it was there. +function frontier(S) { + const { vis, ext, front, ring, stack } = S; + const x0 = S.x0 - 1, x1 = S.x1 + 1, y0 = S.y0 - 1, y1 = S.y1 + 1; + for (let y = y0; y <= y1; y++) ext.fill(0, y * NX + x0, y * NX + x1 + 1); + // A guard ring just outside the box, marked so the flood stops at it. + ext.fill(1, (y0 - 1) * NX + x0 - 1, (y0 - 1) * NX + x1 + 2); + ext.fill(1, (y1 + 1) * NX + x0 - 1, (y1 + 1) * NX + x1 + 2); + for (let y = y0; y <= y1; y++) ext[y * NX + x0 - 1] = ext[y * NX + x1 + 1] = 1; + let sp = 0; + for (let x = x0; x <= x1; x++) { + ext[y0 * NX + x] = ext[y1 * NX + x] = 1; + stack[sp++] = y0 * NX + x; + stack[sp++] = y1 * NX + x; + } + for (let y = y0 + 1; y < y1; y++) { + ext[y * NX + x0] = ext[y * NX + x1] = 1; + stack[sp++] = y * NX + x0; + stack[sp++] = y * NX + x1; + } + while (sp) { + const p = stack[--sp]; + let q = p - 1; + if (!vis[q] && !ext[q]) (ext[q] = 1), (stack[sp++] = q); + q = p + 1; + if (!vis[q] && !ext[q]) (ext[q] = 1), (stack[sp++] = q); + q = p - NX; + if (!vis[q] && !ext[q]) (ext[q] = 1), (stack[sp++] = q); + q = p + NX; + if (!vis[q] && !ext[q]) (ext[q] = 1), (stack[sp++] = q); + } + let F = 0, V = 0; + const stamp = S.now >= S.snap; + if (stamp) S.snap += RING; + for (let y = y0 + 1; y < y1; y++) + for (let p = y * NX + x0 + 1, e = y * NX + x1; p < e; p++) { + if (!vis[p]) continue; + V++; + const f = ext[p - 1] | ext[p + 1] | ext[p - NX] | ext[p + NX]; + if (f) { + F++; + if (stamp) ring[p] = S.now; + } + front[p] = f; + } + S.F = F; + S.V = V; +} + +// One frame of w seconds (at Speed 2): the walk, the frontier, the view. +function advance(S, w) { + S.now += w; + if (!S.done) { + S.acc += RATE * w * S.vs * S.vs; + const n = floor(S.acc); + S.acc -= n; + walk(S, n); + } + frontier(S); + frame(S, w); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 1, 0.06], d = cl(dt || 0, 0, 0.1); + S.trail = 0.7 * 40 ** cl(k[0], 0, 1); + const speed = cl(k[1], 0.2, 3), fine = cl(k[2], 1, 3); + S.rings = 0.6 * 25 ** cl(k[3], 0, 1); + // Above 0.06 the scribble and the pockets also dim, to 0.1 at 1, so the + // rings show as lines on black. + S.dim = 1 - (0.9 * max(0, cl(k[3], 0, 1) - 0.06)) / 0.94; + const w = (d * speed) / 2; + // A new Fine starts a new path once the knob has rested a quarter second. + let fresh = S.fine < 0 || (input.btnPressed && input.btnPressed[0]); + if (!fresh && abs(fine - S.fine) > 0.02) { + if (fine !== S.kf) (S.kf = fine), (S.wait = 0.25); + else fresh = (S.wait -= d) <= 0; + } + if (fresh) { + const keep = S.fine > 0; + S.fine = fine; + restart(S, keep); + } + S.ghostT += w; + if (S.done && (S.holdT += w) > HOLD) restart(S, true); + else advance(S, w); +} + +export function draw(display, params) { + const S = params.s; + if (!S || S.fine < 0) return; + const { vis, ext, front, lastT, ring, hits, mapX, mapY, cntX, cntY, cV, cL, cH, aF, aT, aR, rgb, ghost } = S; + // Which LED each lattice column and row falls in. + const s = S.vs; + cntX.fill(0); + cntY.fill(0); + for (let i = 0; i < NX; i++) { + const lx = floor((i + 0.5 - S.vx) / s + W / 2); + mapX[i] = lx >= 0 && lx < W ? lx : -1; + if (mapX[i] >= 0) cntX[lx]++; + } + for (let j = 0; j < NY; j++) { + const ly = floor((j + 0.5 - S.vy) / s + H / 2); + mapY[j] = ly >= 0 && ly < H ? ly : -1; + if (mapY[j] >= 0) cntY[ly]++; + } + cV.fill(0); + cL.fill(0); + cH.fill(0); + aF.fill(0); + aT.fill(0); + aR.fill(0); + const now = S.now, it = 1 / S.trail, ir = 1 / S.rings, dim = S.dim; + for (let y = S.y0; y <= S.y1; y++) { + const ly = mapY[y]; + if (ly < 0) continue; + const row = ly * W; + for (let x = S.x0, p = y * NX + x; x <= S.x1; x++, p++) { + const lx = mapX[x]; + if (lx < 0) continue; + const q = row + lx; + if (vis[p]) { + cV[q]++; + cH[q] += hits[p]; + const t = exp((lastT[p] - now) * it); + if (t > aT[q]) aT[q] = t; + if (front[p]) aF[q] = 1; + else { + const e = exp((ring[p] - now) * ir); + if (e > aR[q]) aR[q] = e; + } + } else if (!ext[p]) cL[q]++; + } + } + const hx = mapX[S.x], hy = mapY[S.y], head = !S.done && hx >= 0 && hy >= 0 ? hy * W + hx : -1; + const gk = S.ghostT < GHOST ? 1 - S.ghostT / GHOST : 0, g2 = gk * gk; + for (let ly = 0, q = 0; ly < H; ly++) + for (let lx = 0; lx < W; lx++, q++) { + const tot = cntX[lx] * cntY[ly] || 1, v = cV[q] / tot, l = cL[q] / tot; + const ld = l * dim; + let r = LAKE[0] * ld, g = LAKE[1] * ld, b = LAKE[2] * ld; + if (v > 0) { + // How often the walk stood here: its local time, on a log scale. + // Sparse scribble in a coarse LED is lifted so it does not vanish. + const m = cl(log(cH[q] / cV[q]) / log(40), 0, 1), vv = (0.4 + 0.6 * sqrt(v)) * dim; + r += (VIS0[0] + (VIS1[0] - VIS0[0]) * m) * vv; + g += (VIS0[1] + (VIS1[1] - VIS0[1]) * m) * vv; + b += (VIS0[2] + (VIS1[2] - VIS0[2]) * m) * vv; + } + const t = aT[q] * (0.3 + 0.7 * sqrt(v)); + if (t > 0.004) { + const a = min(1, t * 1.1); + r += (TRAIL[0] - r) * a; + g += (TRAIL[1] - g) * a; + b += (TRAIL[2] - b) * a; + } + const e = aR[q]; + if (e > 0.004) { + // A fresh ring is orange; it turns rose and dims as it ages. + const a = min(1, e * 1.5), o = 1 - e; + r += (RINGC[0] - 40 * o - r) * a; + g += (RINGC[1] - 80 * o - g) * a; + b += (RINGC[2] + 70 * o - b) * a; + } + if (aF[q]) { + r = FRONT[0]; + g = FRONT[1]; + b = FRONT[2]; + } + if (q === head) r = g = b = 255; + const o = q * 3; + rgb[o] = r; + rgb[o + 1] = g; + rgb[o + 2] = b; + if (g2 > 0) { + r = max(r, ghost[o] * g2); + g = max(g, ghost[o + 1] * g2); + b = max(b, ghost[o + 2] * g2); + } + display.setPixel(lx, ly, round(min(255, r)), round(min(255, g)), round(min(255, b))); + } +} From 12362d007cc970c9d322eebe00277c2363085420 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 23:30:48 +0000 Subject: [PATCH 12/16] =?UTF-8?q?Prince=20Rupert's=20Cube=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=EC=9D=84=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 --- .../patterns/prince-ruperts-cube.js | 328 ++++++++++++++++++ 1 file changed, 328 insertions(+) create mode 100644 incoming/pattern-study/patterns/prince-ruperts-cube.js diff --git a/incoming/pattern-study/patterns/prince-ruperts-cube.js b/incoming/pattern-study/patterns/prince-ruperts-cube.js new file mode 100644 index 000000000..1fd617f81 --- /dev/null +++ b/incoming/pattern-study/patterns/prince-ruperts-cube.js @@ -0,0 +1,328 @@ +// ===== Patternflow pattern ===== +// Title: Prince Rupert's Cube +// Family: geometry +// Idea: A unit cube turns slowly with a square tunnel bored straight through it along Nieuwland's direction (2, 2, -1), and a second cube 3√2/4 ≈ 1.06066 times larger rises from below the panel, slides up through the tunnel with its faces flush against the walls and comes out of the top mouth, every solid ray-cast and lit so the faces and the tunnel walls read; bored along any other direction, or made any larger, the passing cube jams against the unit cube and the contact glows. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Prince Rupert's cube (Prince Rupert of the Rhine, 1693; Pieter Nieuwland, 1816), pattern round 40 +// Round: 40 +// =============================== +// @matrix 64x128 +// @knobs Fit=-1..1, Speed=0.2..3, Tilt=0..1, Bore=0..1 +// +// Knob 1: Fit. The side of the passing cube. At -1 it is 1, the same as the cube it goes through, Rupert's wager; at 0 it is 3√2/4 = 1.06066, Nieuwland's limit, and the tunnel's four corners just touch four edges of the unit cube; above 0 it grows to 1.1213 at 1, the tunnel cannot widen any further without cutting the cube apart, and the cube stops where it first touches, knocks and glows red-orange. +// Knob 2: Speed. At 2 a passage takes 8 seconds and the cube turns once round the tunnel in 25. At 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Tilt. How far the view looks down the tunnel. At 0 from 30 degrees above its side, so the top mouth shows as a slanted square; at 1 from 80 degrees, nearly straight down it, so the cube's outline is almost the hexagon of its shadow along the tunnel and the square hole is seen end on. +// Knob 4: Bore. Turns the tunnel away from Nieuwland's direction toward the cube's long diagonal (1, 1, -1), 15.8 degrees off, and bores it as wide as that direction allows. Up to 0.06 Nieuwland's direction; at 0.5 the widest tunnel is 1.0546; at 1 the long diagonal, where the cube's shadow is a regular hexagon and the widest square in it is √6 - √2 = 1.03528, so a unit cube still passes and wins Rupert's wager, but Nieuwland's cube jams. + +const W = 64, H = 128; +const { floor, min, max, abs, sqrt, sin, cos, round, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const S2 = sqrt(2), SMAX = (3 * S2) / 4; +const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; +const unit = (a) => { + const l = sqrt(dot(a, a)); + return a.map((v) => v / l); +}; +// Nieuwland's tunnel: its axis D0, and E1, E2 along the sides of its square. +// His square has corners (3/4, 0, 0), (0, 3/4, 0), (1/4, 1, 1) and +// (1, 1/4, 1) on the cube [0, 1]^3; here the cube is centred on the origin. +const D0 = [2 / 3, 2 / 3, -1 / 3], E10 = [-1 / S2, 1 / S2, 0], E20 = [1 / (3 * S2), 1 / (3 * S2), 4 / (3 * S2)]; +// The long diagonal Bore turns toward, and the angle to it. +const DG = unit([1, 1, -1]), TURN = Math.acos(dot(D0, DG)); +// LEDs per unit length, and seconds of a passage at Speed 2. +const K = 34, PASS = 8; +// Materials: the unit cube's faces, the tunnel walls, the passing cube, the +// glow where it jams, the unit cube's edges. +const FACE = [236, 196, 140], WALL = [250, 104, 150], MOVE = [52, 196, 214], HOT = [255, 70, 30], WIRE = [70, 62, 48]; + +export function setup(params) { + params.s = { + t: 0, spin: 0.6, fit: -9, bore: -9, s: SMAX, T: SMAX, tilt: 0, uc: 99, u: 0, blocked: 0, + D: D0, E1: E10, E2: E20, Tmax: SMAX, + col: new Float32Array(W * H * 3), id: new Int8Array(W * H), + }; +} + +// The widest centred square in the unit cube's shadow along d, its sides +// along cos(psi) e1 + sin(psi) e2 and the turn of that by 90 degrees. The +// shadow is the hexagon |q . n_i| <= h_i, n_i = d x (axis i) normalised and +// h_i = (|n_i,x| + |n_i,y| + |n_i,z|) / 2; a square of half side h with +// sides along u and w fits when h (|n_i . u| + |n_i . w|) <= h_i for all i. +// A centred square is enough: the shadow is centrally symmetric. +function halfSide(d, e1, e2, psi) { + const c = cos(psi), s = sin(psi), u = [0, 1, 2].map((m) => c * e1[m] + s * e2[m]), w = cross(d, u); + let h = 1e9; + for (const ax of [[1, 0, 0], [0, 1, 0], [0, 0, 1]]) { + const n = cross(d, ax), l = sqrt(dot(n, n)); + if (l < 1e-9) continue; + const hi = (abs(n[0]) + abs(n[1]) + abs(n[2])) / (2 * l); + h = min(h, (hi * l) / (abs(dot(n, u)) + abs(dot(n, w)))); + } + return [h, u, w]; +} + +// The tunnel for Bore b: its axis turned by b * TURN toward the long diagonal +// (its square's frame carried along by the same rotation), and the square +// turned in its plane to the widest that fits, followed step by step from +// Nieuwland's so it never jumps. +function bore(S, b) { + S.bore = b; + if (b <= 0) { + Object.assign(S, { D: D0, E1: E10, E2: E20, Tmax: SMAX }); + return; + } + const k = unit(cross(D0, DG)); + const rot = (v, a) => { + const c = cos(a), s = sin(a), kv = cross(k, v), kd = dot(k, v); + return [0, 1, 2].map((m) => v[m] * c + kv[m] * s + k[m] * kd * (1 - c)); + }; + let psi = 0, best = null; + for (let i = 1; i <= 24; i++) { + const a = (b * TURN * i) / 24, d = rot(D0, a), e1 = rot(E10, a), e2 = rot(E20, a); + best = null; + for (let j = -60; j <= 60; j++) { + const r = halfSide(d, e1, e2, psi + (j * PI) / 3600); + if (!best || r[0] > best[0]) best = [r[0], r[1], r[2], psi + (j * PI) / 3600, d]; + } + psi = best[3]; + } + Object.assign(S, { D: best[4], E1: best[1], E2: best[2], Tmax: 2 * best[0] }); +} + +// The convex solid A ∩ B(u) has as its corners the points where three of +// the twelve face planes (n . x = h) of the two cubes meet inside both. B(u) +// is the passing cube of side s centred u along the axis. It stays inside +// the tunnel of side T exactly when all those corners do, since it is convex +// and so is the tunnel: otherwise it cuts into the unit cube, and jams. +const PN = new Float64Array(36), PH = new Float64Array(12); +function jams(S, u) { + const ax = [[1, 0, 0], [0, 1, 0], [0, 0, 1], S.E1, S.E2, S.D]; + for (let i = 0; i < 12; i++) { + const j = i >> 1, sg = i & 1 ? -1 : 1; + for (let m = 0; m < 3; m++) PN[i * 3 + m] = ax[j][m] * sg; + PH[i] = j < 3 ? 0.5 : S.s / 2 + (j === 5 ? sg * u : 0); + } + const h = S.T / 2 + 1e-7, E1 = S.E1, E2 = S.E2; + for (let i = 0; i < 12; i++) + for (let j = i + 1; j < 12; j++) + for (let k = j + 1; k < 12; k++) { + const a0 = PN[i * 3], a1 = PN[i * 3 + 1], a2 = PN[i * 3 + 2]; + const b0 = PN[j * 3], b1 = PN[j * 3 + 1], b2 = PN[j * 3 + 2]; + const c0 = PN[k * 3], c1 = PN[k * 3 + 1], c2 = PN[k * 3 + 2]; + const x0 = b1 * c2 - b2 * c1, x1 = b2 * c0 - b0 * c2, x2 = b0 * c1 - b1 * c0; + const det = a0 * x0 + a1 * x1 + a2 * x2; + if (abs(det) < 1e-9) continue; + const y0 = c1 * a2 - c2 * a1, y1 = c2 * a0 - c0 * a2, y2 = c0 * a1 - c1 * a0; + const z0 = a1 * b2 - a2 * b1, z1 = a2 * b0 - a0 * b2, z2 = a0 * b1 - a1 * b0; + const p0 = (PH[i] * x0 + PH[j] * y0 + PH[k] * z0) / det; + const p1 = (PH[i] * x1 + PH[j] * y1 + PH[k] * z1) / det; + const p2 = (PH[i] * x2 + PH[j] * y2 + PH[k] * z2) / det; + let inside = true; + for (let q = 0; q < 12 && inside; q++) inside = PN[q * 3] * p0 + PN[q * 3 + 1] * p1 + PN[q * 3 + 2] * p2 <= PH[q] + 1e-9; + if (!inside) continue; + if (abs(p0 * E1[0] + p1 * E1[1] + p2 * E1[2]) > h || abs(p0 * E2[0] + p1 * E2[1] + p2 * E2[2]) > h) return true; + } + return false; +} + +// Where along the axis the passing cube first touches, rising from below: +// bisection between far below (clear) and the middle (jammed). +function contact(S) { + if (!jams(S, 0)) return 99; + let lo = -2, hi = 0; + for (let i = 0; i < 28; i++) { + const m = (lo + hi) / 2; + if (jams(S, m)) hi = m; + else lo = m; + } + return lo; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + const fit = cl(k[0], -1, 1), speed = cl(k[1], 0.2, 3), b = max(0, cl(k[3], 0, 1) - 0.06) / 0.94; + S.tilt = ((30 + 50 * cl(k[2], 0, 1)) * PI) / 180; + if (fit !== S.fit || b !== S.bore) { + if (b !== S.bore) bore(S, b); + S.fit = fit; + S.s = SMAX + (SMAX - 1) * fit; + // The tunnel is bored to fit the passing cube, as wide as its direction + // allows and no wider. + S.T = min(S.s, S.Tmax); + S.uc = contact(S); + } + const w = (d * speed) / 2; + S.t += w; + // Open with the passing cube about to enter, so it is halfway through the + // tunnel 2.5 seconds in. + const f = S.t / PASS + 0.22, x = 2 * (f - floor(f)) - 1; + // Slow through the tunnel, fast out of view at both ends: far enough along + // the axis to leave the panel at any tilt. + const U0 = 3.1 / cos(S.tilt), u = U0 * (0.3 * x + 0.7 * x * x * x); + S.blocked = u > S.uc ? min(1, (u - S.uc) * 3) : 0; + // Jammed, it knocks against the edges it cannot pass. + S.u = u > S.uc ? S.uc - 0.025 * abs(sin(S.t * 11)) * S.blocked : u; + S.spin = 0.6 + (S.t * 2 * PI) / 25; +} + +// The six directions the faces of all three solids are normal to: the cube's +// x, y and z, the tunnel's E1 and E2, and the axis D. +const AX = [[1, 0, 0], [0, 1, 0], [0, 0, 1], E10, E20, D0]; +// Per frame, for each direction j: the ray's position along it is +// cx[j] X + cy[j] Y + c0[j] + t vj[j] for the panel point (X, Y). +const cx = new Float64Array(6), cy = new Float64Array(6), c0 = new Float64Array(6), iv = new Float64Array(6), vj = new Float64Array(6); +// Shaded colour of each face, by solid (0 cube, 1 tunnel wall, 2 passing +// cube), direction and side. +const FC = new Float32Array(3 * 12 * 3); + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { col, id, E1, E2, D } = S; + AX[3] = E1; + AX[4] = E2; + AX[5] = D; + // The camera: it turns round the tunnel's axis and looks down at it from + // the tilt above its side. Up on the panel is the axis, leaned back. + const cs = cos(S.spin), sn = sin(S.spin), ct = cos(S.tilt), st = sin(S.tilt); + const Wv = [0, 1, 2].map((m) => cs * E1[m] + sn * E2[m]); + const E = [0, 1, 2].map((m) => ct * Wv[m] + st * D[m]); + const U = [0, 1, 2].map((m) => -st * Wv[m] + ct * D[m]); + const R = [D[1] * Wv[2] - D[2] * Wv[1], D[2] * Wv[0] - D[0] * Wv[2], D[0] * Wv[1] - D[1] * Wv[0]]; + // A small lift so the cube sits a little above the middle of the panel. + const lift = 0.12; + for (let j = 0; j < 6; j++) { + cx[j] = dot(R, AX[j]); + cy[j] = dot(U, AX[j]); + c0[j] = 4 * dot(E, AX[j]); + vj[j] = -dot(E, AX[j]); + iv[j] = 1 / (abs(vj[j]) < 1e-9 ? 1e-9 : vj[j]); + } + // Light from the upper left, in front, and a weaker fill from the right, + // both fixed to the camera. In an orthographic view under a distant light + // every face has one colour, so each is shaded once a frame. + const L = [0, 1, 2].map((m) => -0.45 * R[m] + 0.62 * U[m] + 0.64 * E[m]); + const F = [0, 1, 2].map((m) => 0.78 * R[m] - 0.2 * U[m] + 0.59 * E[m]); + const Hh = [0, 1, 2].map((m) => L[m] + E[m]), hl = sqrt(dot(Hh, Hh)); + const hot = S.blocked * (0.75 + 0.25 * sin(S.t * 9)); + for (let k = 0; k < 3; k++) { + let base = k === 0 ? FACE : k === 1 ? WALL : MOVE; + if (k === 2 && hot > 0) base = base.map((c, m) => c + (HOT[m] - c) * hot * 0.8); + // A jammed cube glows of itself as well as by the light. + const glow = k === 2 ? hot * 0.45 : 0; + for (let j = 0; j < 6; j++) + for (let sg = 0; sg < 2; sg++) { + const n = AX[j].map((q) => (sg ? -q : q)); + const lam = max(0, dot(n, L)), sp = max(0, dot(n, Hh) / hl) ** 24; + const a = 0.14 + 0.72 * lam + 0.3 * max(0, dot(n, F)); + for (let m = 0; m < 3; m++) FC[(k * 12 + j * 2 + sg) * 3 + m] = base[m] * (a + glow) + 120 * sp; + } + } + // The face a ray enters through along direction j faces back at it. + const side = (j) => (vj[j] > 0 ? 1 : 0); + const hA = 0.5, hT = S.T / 2, hB = S.s / 2, u = S.u; + const rgb = [0, 0, 0]; + for (let py = 0; py < H; py++) + for (let px = 0; px < W; px++) { + rgb[0] = rgb[1] = rgb[2] = 0; + let face = 0; + for (let sy = 0; sy < 2; sy++) + for (let sx = 0; sx < 2; sx++) { + const X = (px + 0.25 + 0.5 * sx - W / 2) / K, Y = (H / 2 - (py + 0.25 + 0.5 * sy)) / K + lift; + // The unit cube. + let a0 = -1e9, a1 = 1e9, ia = 0; + for (let j = 0; j < 3; j++) { + const o = cx[j] * X + cy[j] * Y + c0[j]; + const p = (-hA - o) * iv[j], q = (hA - o) * iv[j], lo = p < q ? p : q, hi = p < q ? q : p; + if (lo > a0) (a0 = lo), (ia = j); + if (hi < a1) a1 = hi; + } + // The tunnel, unbounded along D. + let p0 = -1e9, p1 = 1e9, ip = 3; + const o3 = cx[3] * X + cy[3] * Y + c0[3], o4 = cx[4] * X + cy[4] * Y + c0[4]; + for (let j = 3; j < 5; j++) { + const o = j === 3 ? o3 : o4; + const p = (-hT - o) * iv[j], q = (hT - o) * iv[j], lo = p < q ? p : q, hi = p < q ? q : p; + if (lo > p0) p0 = lo; + if (hi < p1) (p1 = hi), (ip = j); + } + // The cube less the tunnel: its first solid point along the ray. + let tA = 1e9, fA = -1; + if (a0 < a1) { + if (p0 >= p1 || p1 <= a0 || p0 >= a1 || a0 < p0) (tA = a0), (fA = ia * 2 + side(ia)); + else if (p1 < a1) (tA = p1), (fA = 12 + ip * 2 + side(ip)); + } + // The passing cube. + let b0 = -1e9, b1 = 1e9, ib = 3; + for (let j = 3; j < 6; j++) { + const o = j === 3 ? o3 : j === 4 ? o4 : cx[5] * X + cy[5] * Y + c0[5] - u; + const p = (-hB - o) * iv[j], q = (hB - o) * iv[j], lo = p < q ? p : q, hi = p < q ? q : p; + if (lo > b0) (b0 = lo), (ib = j); + if (hi < b1) b1 = hi; + } + const tB = b0 < b1 ? b0 : 1e9, fB = 24 + ib * 2 + side(ib); + let f = -1; + if (tB < 1e9 && tB <= tA + 1e-9) f = fB; + else if (fA >= 0) f = fA; + if (f < 0) continue; + let r = FC[f * 3], g = FC[f * 3 + 1], b = FC[f * 3 + 2]; + if (f === fA && hot > 0) { + // The rim the passing cube jams against glows. + const t = tA, x3 = abs(o3 + t * vj[3]), x4 = abs(o4 + t * vj[4]), z = cx[5] * X + cy[5] * Y + c0[5] + t * vj[5] - (u + hB); + if (max(x3, x4) < hB + 0.05 && abs(z) < 0.08) { + r += (HOT[0] - r) * hot; + g += (HOT[1] - g) * hot; + b += (HOT[2] - b) * hot; + } + } + rgb[0] += r * 0.25; + rgb[1] += g * 0.25; + rgb[2] += b * 0.25; + if (sx === 0 && sy === 0) face = f + 1; + } + const p = py * W + px; + col[p * 3] = rgb[0]; + col[p * 3 + 1] = rgb[1]; + col[p * 3 + 2] = rgb[2]; + id[p] = face; + } + // The unit cube's twelve edges as faint lines over everything, so its + // whole outline shows where the tunnel has cut its faces away. + const P = (x) => [W / 2 + dot(x, R) * K, H / 2 - (dot(x, U) - lift) * K]; + for (let a = 0; a < 8; a++) + for (let b = 0; b < 3; b++) { + if (a & (1 << b)) continue; + const pa = P([(a & 1) - 0.5, ((a >> 1) & 1) - 0.5, ((a >> 2) & 1) - 0.5]), c = a | (1 << b); + const pb = P([(c & 1) - 0.5, ((c >> 1) & 1) - 0.5, ((c >> 2) & 1) - 0.5]); + line(col, pa, pb, WIRE); + } + for (let py = 0; py < H; py++) + for (let px = 0; px < W; px++) { + const p = py * W + px, f = id[p]; + // A crease between two faces or two solids is darkened a little. + let k = 1; + if (f && ((px > 0 && id[p - 1] !== f) || (py > 0 && id[p - W] !== f))) k = 0.72; + display.setPixel(px, py, round(min(255, col[p * 3] * k)), round(min(255, col[p * 3 + 1] * k)), round(min(255, col[p * 3 + 2] * k))); + } +} + +// Add a soft line one LED wide into the colour buffer. +function line(col, a, b, c) { + const ex = b[0] - a[0], ey = b[1] - a[1], L2 = ex * ex + ey * ey || 1; + const x0 = max(0, floor(min(a[0], b[0]) - 1)), x1 = min(W - 1, floor(max(a[0], b[0]) + 1)); + const y0 = max(0, floor(min(a[1], b[1]) - 1)), y1 = min(H - 1, floor(max(a[1], b[1]) + 1)); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const px = x + 0.5 - a[0], py = y + 0.5 - a[1], u = cl((px * ex + py * ey) / L2, 0, 1); + const dd = sqrt((px - u * ex) ** 2 + (py - u * ey) ** 2), k = cl(1 - dd / 0.9, 0, 1); + if (k <= 0) continue; + const p = (y * W + x) * 3; + col[p] += c[0] * k; + col[p + 1] += c[1] * k; + col[p + 2] += c[2] * k; + } +} From 3404c2053540477f34d0a3e0ac4578d7bc0f777a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 23:31:35 +0000 Subject: [PATCH 13/16] =?UTF-8?q?Lotion=20Stitch=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9D=84=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 --- .../pattern-study/patterns/lotion-stitch.js | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 incoming/pattern-study/patterns/lotion-stitch.js diff --git a/incoming/pattern-study/patterns/lotion-stitch.js b/incoming/pattern-study/patterns/lotion-stitch.js new file mode 100644 index 000000000..d7c8403b7 --- /dev/null +++ b/incoming/pattern-study/patterns/lotion-stitch.js @@ -0,0 +1,292 @@ +// ===== Patternflow pattern ===== +// Title: Lotion Stitch +// Family: cosmetic +// Idea: Seen from above, a glossy rope of lotion falls onto a belt that carries it down the panel, and the belt's speed decides what it stitches: translated coils, alternating loops, W-shaped cusps, meanders or a straight line, each repeating at a pitch of belt speed over the frequency at which the landing point turns. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: fluid mechanical sewing machine (Sunny Chiu-Webster and John Lister, 2006), pattern round 40 +// Round: 40 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Belt=0.2..1.2, Thick=0..1 +// +// Knob 1: Hue. The lotion's colour, once round the wheel, on a near black belt. At 0 lilac, 0.1 pink, 0.2 coral, 0.3 peach, 0.35 butter, 0.4 lemon, 0.45 pale lime, 0.5 light green, 0.6 mint, 0.7 aqua, 0.8 sky blue, 0.9 periwinkle. From orange to lime the side of the rope away from the light is lifted to 0.72 and paled, and orange's leans to rose, so nothing reads brown or olive. +// Knob 2: Speed. The clock. At 2 the thread lands at V = 36 LEDs a second; at Belt 0.2 the belt runs 7.2 LEDs a second, the landing point turns 0.51 times a second and a stitch takes 15 s to ride from the landing to the bottom edge. At 0.2 all of it ten times slower, at 3 one and a half times faster. +// Knob 3: Belt. Belt speed U over the speed U_c = V at which the thread lands, which is the steady coil's radius times its coiling frequency. Below 0.42 translated coils, pitch 14.2 LEDs at 0.2 and 29.3 at 0.4; from 0.42 to 0.79 alternating loops, large at first and shrinking as the belt speeds up; near 0.8 the turns sharpen into cusps, the W shapes; from 0.82 to 1 meanders, their swing growing from nothing as the belt slows below U_c; at 1 and over the belt drags a straight line, thinner by the square root of U / V. +// Knob 4: Thick. A thicker lotion coils wider and slower: the coil radius goes from 11 to 14 LEDs and the coiling frequency from 0.51 to 0.41 turns a second, the fresh rope from 4 to 7 LEDs wide. U_c does not change, so neither do the boundaries on Belt. Laid lotion slumps, widening by a third and flattening over about 4 s. + +const W = 64, H = 128, N = W * H; +// The thread falls on the axis at (XL, YL) and lands at speed V, in LEDs a +// second at Speed 2. +const XL = 31.5, YL = 17, V = 36; +// Samples laid per second of the model's time, and the ring that holds them. +const LAY = 120, CAP = 4096; +const { floor, ceil, min, max, sqrt, exp, sin, cos, round, abs, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => { + const t = cl((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; + +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] = [v, q, p, p, t, v][i]; + out[1] = [t, v, v, q, p, p][i]; + out[2] = [p, p, t, v, v, q][i]; +} + +// The kinematic model. The contact point hangs rho from the axis at angle a +// from straight downstream: (x, y) = rho (sin a, cos a). The thread lands +// without slipping, so the curve laid on the belt grows by V a second; that +// fixes how far the contact must swing, or how fast it must turn, for a +// belt speed U = e V below U_c = V. +// e >= 1 the belt drags the thread straight, laying at U, not V +// e < 1 the contact swings like a pendulum, a = A sin p, p turning at +// w = Om (0.6 + 0.4 sm(0.4, 0.9, e)), from the coiling frequency +// Om = V / R at onset down to 0.6 Om; rho = R (0.65 + 0.35 +// sm(0.55, ECOIL, e)). A grows from 0 as e falls below 1: +// meanders down to e = 0.815, then the turns sharpen to cusps +// (W shapes) until the trace first runs back along the belt at +// e = 0.79, and below that the swing, 104 degrees and wider, +// lays alternating loops that grow as the belt slows +// e < ECOIL it goes over the top: translated coiling at radius R, a +// turning at the rate the laid length sets, within 5 percent of Om +// Every pattern repeats on the belt once a turn, so its pitch is U 2 pi / w, +// belt speed over the turning frequency; measured on the laid samples it +// matched to 0.01 LED at every belt speed from 0.2 to 0.95. +const ECOIL = 0.42; + +// Mean laid speed on the belt over one turn, swinging (amp, w) at hang rho, +// or with amp < 0 turning steadily at w. +function laid(rho, amp, w, U) { + let s = 0; + for (let k = 0; k < 96; k++) { + const p = ((k + 0.5) / 96) * 2 * PI; + const a = amp < 0 ? p : amp * sin(p), ad = amp < 0 ? w : amp * w * cos(p); + const vx = rho * ad * cos(a), vy = -rho * ad * sin(a) - U; + s += sqrt(vx * vx + vy * vy); + } + return s / 96; +} + +// The regime for belt speed e and coil radius R: swing amplitude A (0 for +// the straight line, -1 for coiling), rate w and hang rho. +function regime(e, R) { + const Om = V / R, U = e * V; + if (e >= 1) return { A: 0, w: Om, rho: R }; + if (e < ECOIL) { + let lo = 0.2 * Om, hi = 3 * Om; + for (let it = 0; it < 40; it++) { + const m = 0.5 * (lo + hi); + if (laid(R, -1, m, U) < V) lo = m; + else hi = m; + } + return { A: -1, w: 0.5 * (lo + hi), rho: R }; + } + const w = Om * (0.6 + 0.4 * sm(0.4, 0.9, e)), rho = R * (0.65 + 0.35 * sm(0.55, ECOIL, e)); + let lo = 0, hi = 2 * PI; + for (let it = 0; it < 40; it++) { + const m = 0.5 * (lo + hi); + if (laid(rho, m, w, U) < V) lo = m; + else hi = m; + } + return { A: 0.5 * (lo + hi), w, rho }; +} + +// Coil radius R and thread half-width from Thick. +const radius = (k) => 11 + 3 * k; +const half = (k) => 2 + 1.5 * k; + +export function setup(params) { + params.s = { + hue: 0, speed: 2, e: 0.2, thick: 0.06, begun: false, + p: 0, a: 0, ad: 0, B: 0, acc: 0, n: 0, key: "", reg: null, rot: false, amp: 0, w: 1, rho: 10, cx: XL, cy: YL, + sx: new Float32Array(CAP), sy: new Float32Array(CAP), sw: new Float32Array(CAP), + own: new Int32Array(N), z: new Float32Array(N), nx: new Float32Array(N), ny: new Float32Array(N), occ: new Float32Array(N), + c: [0, 0, 0], lot: new Float32Array(64 * 3), lotHue: -1, + }; +} + +// The model advances in steps of 1 / LAY of a second, and lays a sample of +// thread at the contact after each. +function step(S, dt) { + S.acc += dt * LAY; + while (S.acc >= 1) { + S.acc -= 1; + advance(S, 1 / LAY); + } +} + +function advance(S, dt) { + const R = radius(S.thick), U = S.e * V; + const key = S.e.toFixed(3) + "," + S.thick.toFixed(3); + if (key !== S.key) { + S.key = key; + S.reg = regime(S.e, R); + } + const G = S.reg, g = 1 - exp(-dt / 0.5); + S.w += (G.w - S.w) * g; + S.rho += (G.rho - S.rho) * g; + if (G.A < 0) { + // Over the top: the angle runs on from wherever the swing left it. + S.rot = true; + S.ad = S.w; + S.a += S.w * dt; + } else { + if (S.rot) { + // Back from coiling: the swing starts again from straight downstream. + S.rot = false; + S.amp = 0; + S.p = 0; + S.a -= 2 * PI * round(S.a / (2 * PI)); + } + S.amp += (G.A - S.amp) * g; + S.p += S.w * dt; + // Ease from wherever the angle was, so a change of regime does not jump. + const prev = S.a; + S.a += (S.amp * sin(S.p) - S.a) * min(1, dt / 0.15); + S.ad = (S.a - prev) / dt; + } + S.B += U * dt; + S.cx = XL + S.rho * sin(S.a); + S.cy = YL + S.rho * cos(S.a); + // The same flux spread over more length lies thinner: in the straight + // line the belt stretches it by sqrt(U / V). + const vx = S.rho * S.ad * cos(S.a), vy = -S.rho * S.ad * sin(S.a) - U; + const vl = max(0.6 * V, sqrt(vx * vx + vy * vy)), i = S.n % CAP; + S.sx[i] = S.cx; + S.sy[i] = S.cy - S.B; + S.sw[i] = sqrt(V / vl); + S.n++; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0.2, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.e = cl(k[2], 0.2, 1.2); + S.thick = cl(k[3], 0, 1); + if (!S.begun) { + // Run the belt until the whole panel holds laid thread. + S.reg = regime(S.e, radius(S.thick)); + S.key = S.e.toFixed(3) + "," + S.thick.toFixed(3); + S.amp = max(0, S.reg.A); + S.w = S.reg.w; + S.rho = S.reg.rho; + const T = (H - YL + 16) / (S.e * V); + step(S, T); + S.begun = true; + } + step(S, (d * S.speed) / 2); +} + +// Light from the upper left and a little in front; the viewer straight above. +const LX = -0.45, LY = -0.55, LZ = 0.7; +const LN = sqrt(LX * LX + LY * LY + LZ * LZ), Lx = LX / LN, Ly = LY / LN, Lz = LZ / LN; +const HN = sqrt(Lx * Lx + Ly * Ly + (Lz + 1) ** 2), Hx = Lx / HN, Hy = Ly / HN, Hz = (Lz + 1) / HN; +// Opening hue: lilac. +const H0 = 0.78; + +// The lotion's colour by how much light a point of the rope faces, a LUT +// rebuilt when Hue turns. Lotion scatters light inside, so even the side +// away from the light keeps half its brightness. Orange and yellow still go +// brown or olive there: across that part of the wheel the shaded side is +// lifted to 0.72 and paled, and orange's leans to rose. +function lotion(S) { + const c = S.c; + let h = S.hue + H0; + h -= floor(h); + const warm = cl(h / 0.04, 0, 1) * cl((0.3 - h) / 0.06, 0, 1), red = cl((0.12 - h) / 0.06, 0, 1); + for (let k = 0; k < 64; k++) { + const q = k / 63, lo = 1 - q; + const v = 0.5 + 0.22 * warm + (0.5 - 0.22 * warm) * q; + hsv(h - 0.06 * warm * red * lo, (0.5 - 0.15 * q) * (1 - 0.3 * warm * lo), v, c); + S.lot[k * 3] = c[0]; + S.lot[k * 3 + 1] = c[1]; + S.lot[k * 3 + 2] = c[2]; + } + S.lotHue = S.hue; +} + +// One capsule of thread, a to b with half-width r, into the layer buffers. +// A strand laid more than K samples later lies on top and shades the edge +// of what it crosses; within K it is the same strand and the ridge wins. +// fl < 1 flattens the rope's cross-section, so its sides tilt less. +function capsule(S, n, K, ax, ay, bx, by, r, fl) { + const { own, z, nx, ny, occ } = S; + const ex = bx - ax, ey = by - ay, l2 = ex * ex + ey * ey || 1e-9, rr = r + 1.3, R2 = rr * rr, r2 = r * r; + const x0 = max(0, floor(min(ax, bx) - rr)), x1 = min(W - 1, ceil(max(ax, bx) + rr)); + const y0 = max(0, floor(min(ay, by) - rr)), y1 = min(H - 1, ceil(max(ay, by) + rr)); + for (let py = y0; py <= y1; py++) + for (let px = x0; px <= x1; px++) { + let u = ((px - ax) * ex + (py - ay) * ey) / l2; + u = u < 0 ? 0 : u > 1 ? 1 : u; + const dx = px - ax - u * ex, dy = py - ay - u * ey, d2 = dx * dx + dy * dy; + if (d2 >= R2) continue; + const p = py * W + px, o = own[p]; + if (d2 < r2) { + const zz = sqrt(1 - d2 / r2); + if (o < n - K || zz > z[p]) { + if (o < n - K) occ[p] = 0; + own[p] = n; + z[p] = zz; + nx[p] = (fl * dx) / r; + ny[p] = (fl * dy) / r; + } + } else if (o < n - K) { + const a = 1 - (sqrt(d2) - r) / 1.3; + if (a > occ[p]) occ[p] = a; + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + if (S.lotHue !== S.hue) lotion(S); + const { own, z, nx, ny, occ, sx, sy, sw } = S; + own.fill(-1); + occ.fill(0); + const r0 = half(S.thick), K = ceil((5 * r0 * LAY) / V) + 2; + const first = max(1, S.n - CAP + 1); + for (let n = first; n < S.n; n++) { + const i = n % CAP, j = (n - 1) % CAP, y = sy[i] + S.B; + if (y < -8 || y > H + 8) continue; + // Laid lotion slumps: over about four seconds the rope widens by a third + // and flattens, so older stitching is softer and less glossy. + const sl = 1 - exp(-(S.n - n) / (4 * LAY)); + capsule(S, n, K, sx[j], sy[j] + S.B, sx[i], y, r0 * sw[i] * (1 + 0.33 * sl), 1 - 0.45 * sl); + } + + const lot = S.lot; + for (let py = 0; py < H; py++) + for (let px = 0; px < W; px++) { + const p = py * W + px; + let r, g, b; + if (own[p] >= 0) { + const zz = z[p], nn = 1 / sqrt(nx[p] * nx[p] + ny[p] * ny[p] + zz * zz), ax = nx[p] * nn, ay = ny[p] * nn, az = zz * nn; + const dif = max(0, ax * Lx + ay * Ly + az * Lz), sp = max(0, ax * Hx + ay * Hy + az * Hz) ** 28; + const q = round(cl(0.15 + 0.85 * dif, 0, 1) * 63) * 3, sh = 1 - 0.5 * occ[p]; + r = (lot[q] * sh + 0.85 * sp) * 255; + g = (lot[q + 1] * sh + 0.85 * sp) * 255; + b = (lot[q + 2] * sh + 0.85 * sp) * 255; + } else { + // The belt: a dark cloth with faint ribs that ride down with it. + const rib = 0.5 + 0.5 * sin(((py - S.B) * 2 * PI) / 5); + const v = (12 + 8 * rib * rib) * (1 - 0.6 * occ[p]); + r = v; + g = v; + b = v * 1.25; + } + // Where the thread lands it is fresh and wet: a glint just to the + // light side of the contact. + const gx = px - S.cx + 0.5, gy = py - S.cy + 0.6, gl = 0.95 * exp(-(gx * gx + gy * gy) / (0.55 * r0 * r0)); + r += (255 - r) * gl; + g += (255 - g) * gl; + b += (255 - b) * gl; + display.setPixel(px, py, round(min(255, r)), round(min(255, g)), round(min(255, b))); + } +} From 1e9574c21215efa19f5d215243acefe424feeee2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 23:38:34 +0000 Subject: [PATCH 14/16] =?UTF-8?q?40=ED=8C=90=20=EC=97=B4=20=EA=B0=9C?= =?UTF-8?q?=EB=A5=BC=20README=20=EC=97=B0=EA=B5=AC=20=EB=85=B8=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20=EC=A0=81=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CLauBYuJBXD9L9NRJjxCqK --- incoming/pattern-study/README.md | 79 ++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 917cd6ce3..e4d3cbcc3 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 288개를 쉰다섯 갈래로 나눠 만들었어요. 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판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 298개를 쉰다섯 갈래로 나눠 만들었어요. 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판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. [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판에서 세로로 다시 짰어요. @@ -474,6 +474,21 @@ | [Serum Fingers](patterns/serum-fingers.js) | 화장품 팝업 | 유리판 두 장 사이 진한 크림에 묽은 세럼을 밑에서 밀어 넣으면, 둥근 손가락이 부풀고 갈라지며 위로 다투어 올라가요. LED 는 세럼이 닿을 때 한 번만 바뀌고, 한 손가락이 꼭대기에 닿으면 크림이 가는 손가락부터 다시 덮고 새로 밀어요. K3 은 표면 장력을 낮춰 굵은 손가락 셋을 가는 손가락 열둘로 바꾸고, K1 을 누르면 크림이 바로 다시 덮어요. | | [Sperner's Lemma](patterns/sperner-lemma.js) | 위상·매듭 | 세운 삼각형을 작은 방으로 나누고, 꼭짓점마다 연속 사상이 가장 많이 줄이는 좌표의 번호를 붙여 파랑, 빨강, 초록 영토로 칠해요. 흰 걸음이 밑변의 1-2 문으로 들어와 방마다 다른 문으로 나가다가 세 번호를 다 가진 무지개 방에서 멈추고, 그 방은 사상의 고정점 위에 있어요. K3 은 사상을 고정점 둘레로 비틀어 영토 경계를 감고, K4 는 고정점을 늘려 무지개 방을 홀수 개로 늘려요. | +### 40판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Brownian Frontier](patterns/brownian-frontier.js) | 선·궤적 | 단순 무작위 걸음이 지나간 자리를 어두운 파랑으로, 바깥에 닿은 경계를 금빛 선으로 그려요. 걸음이 밖으로 나갔다 돌아와 경계를 먹을 때마다 주황 고리가 남고, 화면은 길을 따라 물러나 늘 패널을 채워요. K4 는 지난 경계를 겹겹의 주황 등고선으로 오래 남기고, K1 을 누르면 새 길이 시작돼요. | +| [Continuum Random Tree](patterns/continuum-random-tree.js) | 프랙탈 | 정규화한 브라운 엑스커션으로 짠 앨더스의 연속 랜덤 나무가 바닥의 뿌리에서 선 끊기로 잎 하나씩 자라고, 새 가지마다 흰 빛이 타고 올라가요. 다 자라면 시야가 가지 하나로 뛰어들고, 브라운 크기로 늘린 그 가지가 다시 온전한 나무가 되어 계속 자라요. K3 은 뛰어들 가지의 질량을 0.3 에서 0.06 으로 줄이고, 누르면 바로 뛰어들어요. | +| [Defect Turbulence](patterns/defect-turbulence.js) | 물결·간섭 | 복소 긴즈부르크-란다우 방정식을 패널에서 풀어, 허수부와 실수부가 0 인 선을 두 색으로 긋고 둘이 만나는 위상 결함마다 + 와 − 를 찍어요. 처음의 결함 난류에서 쌍이 생기고 사라지며 나선을 끌고 다니다가 나선이 이겨 얼어붙은 칸이 되고, K3 을 Benjamin-Feir 선 아래로 돌리면 난류가 끝내 가라앉지 않아요. K4 는 그리는 시계를 나선의 진동수에 맞춰 팔을 멈춰 세우고, K1 을 누르면 한 자리를 휘저어 결함 쌍을 새로 터뜨려요. | +| [Hydrogen Orbital](patterns/hydrogen-orbital.js) | 물리·스프링 | 수소 원자 고유 상태의 확률 밀도에서 뽑은 점 수천 개가 z 축을 세운 구름으로 16초에 한 바퀴 돌아요. 점은 서로 닿지 않는 LED 하나씩이고 파동 함수의 부호로 두 색을 칠해 마디 평면과 원뿔이 검게 남아요. K3 은 5g xz³ 에서 6f xz² 까지 열두 궤도를 고르고, K4 를 0 으로 내리면 9 LED 두께의 단면이 되어 지름 마디가 검은 고리로 보여요. | +| [Jelly Layers](patterns/jelly-layers.js) | 화장품 팝업 | 투명한 젤리 매니큐어를 흰 바탕에 붓으로 한 겹씩 겹쳐 발라, 겹친 곳은 투과율이 곱해져 짙어지고 붓결은 몇 초 만에 평평해져요. 두 번 바르면 한 번은 패드가 가장 짙은 띠를 닦아 내고, 광택 창과 빛 띠는 늘 맨 위에 떠요. K3 은 한 겹의 짙기를 옅은 분홍에서 깊은 라즈베리까지 올리고, K4 는 광택을 무광 베일로 바꾸고, K1 을 누르면 다음 붓질이나 닦기가 바로 와요. | +| [Lamellar Cream](patterns/lamellar-cream.js) | 화장품 팝업 | 크림을 직교 편광 현미경으로 본 그림이에요. 검은 바탕에서 라멜라 방울이 패널 위로 흐르며 맞붙고 가끔 합쳐지고, 방울마다 편광자 축을 따른 검은 몰타 십자와 간섭색을 띠며 큰 방울은 색 고리를 둘러요. K3 은 방울을 키워 2차의 파랑과 초록 고리를 꺼내고, K4 는 편광자 쌍을 돌려 십자를 방울이 아니라 편광자를 따라 돌게 해요. | +| [Lotion Stitch](patterns/lotion-stitch.js) | 화장품 팝업 | 위에서 내려다본 벨트에 윤기 나는 로션 줄기가 떨어지고, 벨트가 놓인 땀을 아래로 실어 날라 128줄에 최근 바느질이 쌓여요. K3 으로 벨트를 빠르게 하면 옮겨지는 코일이 엇갈린 고리와 W 모양 꺾임과 사행을 거쳐 곧은 줄이 되고, K4 는 로션을 굵게 해서 코일을 넓고 느리게 감아요. | +| [Perfume Tears](patterns/perfume-tears.js) | 화장품 팝업 | 향수 잔 안쪽 벽을 가까이서 봐요. 알코올이 날아가며 생긴 마랑고니 응력이 바닥의 액체를 얇은 막으로 끌어올리고, 막이 모인 윤기 나는 테가 반지름의 9배 간격으로 부풀어 다리를 끄는 눈물로 미끄러져 내려요. K3 은 테를 굵게 해 눈물을 다섯 줄에서 세 줄의 큰 방울로 바꾸고, 누르면 잔을 휘저어 새 막을 입혀요. | +| [Prince Rupert's Cube](patterns/prince-ruperts-cube.js) | 기하·대칭 | 가운데가 정사각형으로 뚫린 정육면체가 천천히 돌고, 그보다 1.06066 배 큰 정육면체가 밑에서 올라와 그 굴을 벽에 딱 붙어 지나가요. K1 을 0 위로 돌리면 지나가던 정육면체가 굴 어귀에 걸려 붉게 빛나며 두드리고, K4 는 굴의 방향을 긴 대각선 쪽으로 돌려 굴을 √6 − √2 까지 좁혀요. | +| [Zeldovich Pancakes](patterns/zeldovich-pancakes.js) | 공간·원근 | 입자 3만 2768개가 젤도비치 근사로 움직여, 고른 안개에서 얇은 팬케이크가 켜지고 벽과 실과 매듭으로 된 우주 거대 구조가 돼요. 벽에 삼켜진 물질은 장밋빛이라 첫 팬케이크가 D = 1/max λ1 에서 켜지는 것이 보이고, K4 를 올리면 만난 흐름이 붙지 않고 지나가 매듭이 마름모로 부풀어요. K1 을 누르면 새 무작위 퍼텐셜이 바로 스며 들어요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -1844,6 +1859,50 @@ E8 의 근 240개를 고셋의 4₂₁ 다면체 꼭짓점으로 그렸어요. 주제 1 과 6 으로 사프만-테일러의 손가락을 세웠고, 겨눈 칸은 Day and Night 혼자이던 움직임 × 모양 바뀜 1,3 이에요. 움직임 0.061 아래에 모양 바뀜 0.217~0.331 인 칸이에요. 처음 판은 바닥 가운데 한 점에서 밀어 넣었는데 표면 장력이 세서 둥근 산 하나만 자랐어요(움직임 0.031, 모양 바뀜 0.186). 장력을 줄이자 가는 손가락 하나가 8초 전에 꼭대기에 닿아 지우기가 잰 구간 안에 들어왔어요(모양 바뀜 0.084). 장력을 압력장에 넣는 셈은 퍼져서, 곡률을 성장 속도에 곱하는 꼴로 바꾸고 바닥 전체를 틈으로 바꾸자 둥근 손가락 여럿이 다투며 올라갔어요. LED 마다 한 번 오르기만 하면 모양 바뀜은 침범한 넓이 곱하기 밝기 차이이고, 움직임은 그 6분의 1 이에요. 2초에서 8초 사이에 오른 합이 0.251, 내린 합이 0.0000 이었어요. 그래서 모양은 두고 밀어 넣는 양을 1초에 580 LED 로 정하고, 크림을 밝기 71 의 회색으로 낮춰 세럼과 0.7 쯤 벌려서 움직임 0.042, 모양 바뀜 0.251 로 칸 가운데에 맞췄어요. 씨앗 없이 12번 돌려도 손가락이 꼭대기에 닿는 때는 8.8초에서 11.3초여서 지우기는 늘 10초 뒤에 시작해요. +## 40판 + +40판도 주제 5 「낯선 수학」 여섯과 주제 6 「화장품 브랜드 팝업」 넷이에요. 같이 돈 39판(#233)이 등스펙트럼 북과 슈타이너 사슬, 비누 막 같은 것을 먼저 골라서, 이 판은 그와 겹치지 않게 골랐어요. 수학은 Continuum Random Tree, Hydrogen Orbital, Zeldovich Pancakes, Brownian Frontier, Defect Turbulence, Prince Rupert's Cube 이고, 화장품은 Perfume Tears, Lotion Stitch, Jelly Layers, Lamellar Cream 이에요. 수학 여섯은 프랙탈, 물리·스프링, 공간·원근, 선·궤적, 물결·간섭, 기하·대칭에 하나씩 들어갔고, 주제 2 로 연 갈래는 없어요. 연속 랜덤 나무와 긴즈부르크-란다우 방정식의 결함 난류는 「다음 판 감」에 적혀 있던 감이고, 나머지 넷은 목록 밖에서 찾았어요. 화장품 넷 가운데 와인의 눈물과 유체 재봉틀, 젤리 네일 겹 바르기는 「다음에 해 볼 질감」에 적혀 있던 감이고, 라멜라 크림의 몰타 십자는 목록 밖이에요. 주제 1 은 288개의 지도 21장이 다 `none` 이라 30판부터처럼 패턴이 하나뿐인 칸을 겨눴고, 39판이 겨눈 움직임 × 모양 바뀜 1,4 와 복잡도 × 모양 바뀜 4,4 는 피했어요. Hydrogen Orbital 이 One Bit 혼자이던 촘촘함 × 채움 4,1 에, Defect Turbulence 가 Rule Scroll 혼자이던 움직임 × 촘촘함 4,4 에 섰어요. 패턴이 하나뿐인 칸은 288개에서 12 였고 298개에서 10 이에요. 열 개 다 어느 축의 끝도 넘지 않아서 칸 경계는 30판 그대로이고, 지도 21장이 다 `none` 이에요. 주제 4 는 Continuum Random Tree 와 Perfume Tears 예요. Continuum Random Tree 는 바닥의 뿌리에서 나무가 128줄을 올라가 `thirds` 가 24/60/27 이에요. Perfume Tears 는 막이 어디서나 켜져 37/100/100 이라, 2~8초 사이 어느 때든 밝기가 24 넘게 바뀐 LED 를 따로 세면 위, 가운데, 아래가 26/71/35 예요. 다섯 일꾼이 둘씩 짰고, 본체가 연구의 Lab 하네스로 다시 재고 화면을 그려 보고 합쳤어요. + +### Continuum Random Tree + +주제 4 와 5 로 앨더스의 브라운 연속 랜덤 나무를 세웠어요. 정규화한 브라운 엑스커션 2e 를 8,192걸음으로 만들고, 균일한 잎을 하나씩 뽑아 그 엑스커션 위에서 앨더스의 선 끊기를 해 뿌리부터 가지를 키워요. 나무 2,000그루의 균일한 점 16,000개에서 뿌리까지 거리가 h 를 넘을 몫은 h = 0.5~3 내내 레일리 꼬리 exp(−h²/2) 와 0.008 안으로 맞았고, 잎 40개로 펼친 나무 길이 8.40 에 격자가 놓친 최솟값 0.52 를 더하면 선 끊기의 E[C_40] = 8.916 과 같은 8.92 예요. 처음 판은 가지를 엑스커션 구간 가운데에 바로 세워서 가지점마다 가로 막대가 생겨 회로도처럼 읽혔어요. 한 단 아래에서 따라오며 옆으로 레벨마다 0.5 LED 까지만 움직이게 하자 갈래가 Y 로 열렸고, 이 따라가기는 구간 순서를 지켜서 가지끼리 건너지 않아요. 나무 꼭대기는 원래 성겨서 처음 `thirds` 가 8/52/22 였는데, 빛 번짐을 넓히고 굵기를 질량의 0.35 제곱으로 주고 엑스커션 넷 가운데 위 절반에 시간을 가장 많이 쓴 것으로 열자 24/60/27 이 됐어요. 다 자라면 시야가 질량 0.3 짜리 가지로 뛰어들고, 시간을 1/m, 높이를 1/√m 로 늘리고 사이를 브라운 다리로 채운 그 가지도 레일리 꼬리와 0.012 안으로 맞는 온전한 나무예요. 처음에는 키가 가장 큰 가지로 뛰어들어 등뼈에 잔가지만 붙은 생선뼈가 나왔고, 깊이 뛰어들면 시야가 구간을 겨누는 사이 그려진 가지가 화면 밖에 남아 초록 선 하나만 보였어요. 위 절반에 잎이 많은 가지를 고르고 그려진 가지의 폭으로 시야를 잡아서 둘 다 풀었어요. 잰 값은 움직임 0.377, 촘촘함 0.054, 복잡도 1.351, 채움 0.376, 색 0.200, 모양 바뀜 0.245 이고 노브 반응은 0.10, 0.21, 0.23, 0.05 예요. + +### Hydrogen Orbital + +주제 5 와 1 로 수소 원자의 고유 함수를 세웠어요. r 과 cos θ 는 누적 분포를 뒤집고 φ 는 기각으로 |ψ|² 에서 점을 뽑아, 파동 함수의 부호를 두 색으로 칠하고 z 축을 세운 채 16초에 한 바퀴 돌려요. 패턴 안의 표본기로 궤도마다 40만 점을 뽑자 r 의 평균이 열두 궤도 모두 a0(3n² − l(l+1))/2 와 1.4σ 안에서 같았고, 5g xz³ 는 27.496 대 27.5 였어요. 점은 x + y 가 짝수인 LED 에만 찍어서 켜진 LED 끼리 옆으로 붙지 않아요. 그래서 촘촘함 0.355, 채움 0.200 으로 One Bit 혼자이던 촘촘함 × 채움 4,1 에 섰어요. 처음에는 구름 전체를 14도 위에서 내려다봤더니, 고리의 앞쪽이 엽을 덮어 마디 원뿔과 평면이 어둡게 남지 않았고 촘촘함도 0.306 이었어요. 확률의 97% 를 품은 면 안만 남기고, 시선 쪽 두께를 구름의 3분의 1로 자르고, 기울기를 0 으로 하자 xy 마디 평면이 어두운 한 줄로, 원뿔이 엽 사이의 검은 V 로 섰어요. m = 0 인 5g z⁴ 로 열었을 때는 어느 쪽에서 봐도 같은 모양이라 도는 것이 보이지 않았어요. 5g xz³ 로 열어 꽃잎 여덟 장이 4초 만에 엽과 고리의 탑으로 닫히게 하자 모양 바뀜도 0.032 에서 0.104 가 됐어요. K4 를 0 으로 내리면 9 LED 두께의 단면이 되어 5p z 의 지름 마디 셋이 검은 고리 셋으로 보여요. + +### Zeldovich Pancakes + +주제 5 로 젤도비치 근사의 팬케이크를 세웠어요. 128×256 격자의 입자 32768개를 가우스 무작위 퍼텐셜의 기울기만큼 x = q − D∇φ(q) 로 옮기고, LED 마다 세어 로그 밝기로 그려요. 변형 텐서의 큰 고유값 λ1 의 최댓값이 1 이 되게 φ 를 맞춰서 첫 꺾임은 D = 1 에 와야 하고, 씨앗 셋에서 입자 격자의 첫 칸이 뒤집힌 것은 D = 1.002~1.013 이었어요. 그 전에는 LED 마다 센 밀도가 1/((1 − Dλ1)(1 − Dλ2)) 와 중앙값 0.9~2.6% 안으로 맞고 상관은 0.992 를 넘었어요. 처음 판은 지수 −1 스펙트럼이라 상자에서 가장 긴 파동 하나가 패널을 가로지르는 곧은 벽을 깔았고, 밀도를 흰 잡음으로 바꾸고 48 LED 보다 긴 파동을 줄이자 그 벽이 그물의 한 가닥으로 돌아왔어요. 접착 모형의 끈적한 극한을 K4 로 걸려고 |q|²/2 − Dφ 의 볼록 껍질을 르장드르 변환 두 번으로 구했는데, 노브를 재는 2~3초의 D 1.5~1.7 에서는 반응이 0.0014 였어요. 성장을 D 3.8 까지 늘리고 첫 판을 D 2.6 에서 열자 0.04 가 됐고, 여는 화면이 거의 끈적한 그물이 되게 K4 를 Cross 로 뒤집었어요. 늘어난 벽은 입자가 2 LED 씩 벌어져 사다리 점선으로 보여서, 늘어난 입자만 2~4점으로 나눠 찍었어요. LED 한 칸에서는 막 생긴 꺾임이 그 밑의 능선보다 조금만 밝아서 팬케이크가 안개에 묻혔고, 제 꺾임을 지난 입자(Dλ1 > 1)를 장밋빛으로 칠하자 첫 팬케이크가 실처럼 켜졌어요. 잰 값은 움직임 0.054, 촘촘함 0.052, 복잡도 1.591, 채움 0.734, 색 0.152, 모양 바뀜 0.106, 5.3 ms 이고 노브 반응은 0.11, 0.05, 0.10, 0.04 예요. + +### Brownian Frontier + +주제 5 로 평면 브라운 운동의 바깥 경계를 세웠어요. 격자 위 단순 무작위 걸음이 지나간 자리를 어두운 파랑으로 두고, 길을 감싼 상자의 테두리에서 4방향으로 채워 들어가 그 바깥에 닿은 자리만 금빛으로 그려요. 길 107개를 재 보니 상자가 64칸에서 256칸으로 커지는 동안 경계 칸이 290에서 1,793으로 늘어 기울기가 1.325였고, 4/3 은 1.333이에요. 따로 돌린 걸음 300개는 1.341이었고, 지나간 자리는 같은 셈으로 1.877이에요. 길이 끝날 때 패널의 금빛 LED 를 1, 2, 4, 8 LED 상자로 세면 차례로 1.24, 1.34, 1.37 이라, 셈을 패널 위에서 해도 4/3 언저리에 와요. 처음 판은 LED 하나에 한 칸으로 걷다가 벽에 닿으면 두 배씩 물러났는데, 8초에 35 LED 너비의 꽉 찬 섬이라 채움이 0.089 로 축 끝 0.117 아래였어요. 화면이 길의 상자를 따라 천천히 물러나 늘 패널 너비를 채우게 하자 채움이 0.193 이 됐어요. 먹힌 경계를 프레임마다 주황으로 남겼더니 오래 남길수록 안쪽이 분홍 점으로 덮였어요. 거의 모든 자리가 한 번은 경계였기 때문이고, 0.3초마다 경계를 찍어 그 선만 남기자 겹겹의 등고선이 됐어요. K4 반응은 처음 0.004 언저리였고, 등고선을 오래 남길수록 안쪽을 어둡게 하고 처음 화면을 키우자 0.036 이 됐어요. 잰 값은 움직임 0.365, 채움 0.193, 색 0.155, 모양 바뀜 0.128 이에요. + +### Defect Turbulence + +주제 1 과 5 로 복소 긴즈부르크-란다우 방정식의 결함 난류를 세웠고, 겨눈 칸은 Rule Scroll 혼자이던 움직임 × 촘촘함 4,4 예요. 방정식은 LED 한 칸에 두 칸씩 128×256 격자에서 풀고, 위상은 색상환에 얹지 않고 허수부가 0 인 선을 하늘색, 실수부가 0 인 선을 장미색으로 검은 바탕에 그어요. 두 선이 만나는 곳이 위상 결함이라, 위상이 한 바퀴 감기는 칸마다 작은 + 나 − 를 찍었어요. 주기 경계라 감김의 합은 늘 0 이어야 하는데, Stability 여섯 자리에서 3분씩 32,400 프레임을 세는 동안 + 와 − 의 수가 다른 프레임은 하나도 없었고 쌍 수는 5 에서 52 사이였어요. 처음 판은 64×128 격자에서 실수부를 두 색 띠로 칠해 촘촘함이 0.160 이었고, 띠 안에 허수부 0 선을 넣어도 위상이 평평한 곳이 큰 덩어리로 번져 플라즈마처럼 읽혔어요. 선 너비를 위상이 아니라 |f|/|∇f| 로 LED 거리에 맞추자 덩어리가 사라졌어요. LED 한 칸에 방정식 단위를 더 넣어 무늬를 잘게 하면 칸 하나가 1.4 단위부터 나선의 파수가 0.39 에서 0.31 로 떨어져서, 칸을 0.8 단위로 두고 격자를 두 배로 늘렸더니 촘촘함 0.348, 움직임 2.801 로 칸 안에 들었어요. b = 2 에서 Benjamin-Feir 선인 여는 자리는 처음 40초쯤 난류가 일다가 나선이 이겨 다섯 쌍의 얼어붙은 칸이 되고, −0.2 아래로는 끝내 가라앉지 않아요. 얼어붙은 칸에서 잰 진동수 −0.052 는 분산 관계 c + (b − c)q² 가 주는 −0.047 과 맞았어요. + +### Prince Rupert's Cube + +주제 5 로 루퍼트 왕자의 정육면체를 세웠어요. 한 변 1 인 정육면체에 Nieuwland 의 방향 (2, 2, −1) 로 정사각 굴을 뚫고, 1.06066 배 큰 정육면체가 그 굴을 지나가는 것을 광선 투사로 그려요. 정육면체를 그 방향으로 비춘 그림자는 꼭짓점이 (±0.7071, ±0.4714), (0, ±0.7071) 인 육각형이고, 그 안에 드는 가장 큰 정사각형은 이분법으로 1.060660172 라 3√2/4 와 같았어요. 정사각형을 돌리거나 옮겨도, 방향을 17도 안에서 바꿔도 이보다 크지 않았어요. 벽 두께는 굴 한 변이 1 일 때 가장 얇은 곳이 0.038, 1.06 일 때 0.00042 이고, 3√2/4 에서는 네 귀퉁이에서 0 이 되어 굴의 귀퉁이가 정육면체의 네 모서리에 닿아요. 변 가운데 벽은 그때도 0.168 과 0.177 이에요. 처음 판은 남은 벽이 얇아서 황갈색 조각 몇 개가 흩어진 것처럼 보였고 정육면체로 읽히지 않았어요. 열두 모서리를 옅은 선으로 늘 그리고 면 사이 경계를 조금 어둡게 하자 큰 구멍이 뚫린 정육면체로 읽혔어요. 투명도를 바꾸던 K4 는 반응이 0.01 이라, 굴의 방향을 긴 대각선 쪽으로 15.8도까지 돌리는 Bore 로 바꿨어요. 그 끝에서 가장 넓은 굴은 √6 − √2 = 1.03528 이라 Nieuwland 의 정육면체가 걸리고, K4 반응은 0.15 가 됐어요. 잰 값은 움직임 0.225, 채움 0.278, 색 0.173, 모양 바뀜 0.197 이고, 한 프레임이 7.9 ms 에서 2.3 ms 로 줄었어요. + +### Perfume Tears + +주제 4 와 6 으로 와인의 눈물을 향수 잔 안쪽 벽으로 옮겼고, K1 하나로 액체 색이 색상환을 다 돌아요. 막은 q = A(z)h² − Bh³ 의 보존 법칙을 고두노프 플럭스로 풀고, 알코올이 날아가며 마랑고니 응력 A 가 위로 갈수록 줄어서 최대 플럭스 4A³/27B² 가 들어오는 플럭스와 같아지는 높이에 테가 서요. 마른 벽을 오르는 막의 앞은 충격파 속도 q0/h(z) 로 가서 바닥에서 1초에 10.5줄, 테 밑에서 6.9줄이고, 84줄 높이에 9.25초에 닿아 적분한 예측 9.26초와 맞았어요. 테의 가로 불안정은 가장 빠른 파장을 평균 반지름의 9.02배로 두었고, 2분 동안 눈물이 난 자리의 간격이 반지름 1.46 에서 예측 13.2 에 12.9, 1.83 에서 16.5 에 16.1, 2.12 에서 19.1 에 21.0 LED 로 나왔어요. 처음 판은 다리 액체를 막 두께에 그대로 더해서 다리가 두꺼운 커튼으로 합쳐 내려왔고, 반지름 1 언저리의 방울이 거의 멈춘 채 10초 만에 48개까지 쌓였어요. 다리를 중력으로만 흐르는 따로 된 층으로 떼고, 방울 반지름을 부피의 세제곱근으로 바꾸고, 테 부피를 0에서 자를 때 생기던 액체를 되돌리자 1초에 1.1개 꼴로 반지름 3.4 의 눈물이 떨어졌어요. K1 버튼으로 잔을 휘저으면 처음에는 두꺼운 막이 응력과 무게가 맞는 h = A/B 에 걸려 눈물이 다시 나지 않았어요. 들어 올릴 수 있는 것보다 두꺼운 막이 테로 흘러들게 하자 얇은 막이 아치 모양으로 다시 올라와 15초쯤 만에 처음 모습이 돼요. 막이 어디서나 켜져 `thirds` 는 37/100/100 이고, 2~8초 사이 어느 때든 밝기가 24 넘게 바뀐 LED 는 위, 가운데, 아래가 26/71/35 예요. 잰 값은 움직임 0.147, 촘촘함 0.026, 복잡도 1.232, 채움 0.789, 색 0.132, 모양 바뀜 0.044 이고 노브 반응은 0.13, 0.05, 0.05, 0.11 이에요. + +### Lotion Stitch + +주제 6 으로 Chiu-Webster 와 Lister(2006)의 유체 재봉틀을 위에서 내려다본 로션으로 세웠어요. 실이 미끄러지지 않고 떨어지면 벨트 위에 놓인 곡선은 1초에 V 만큼 자라야 해서, 이 조건 하나로 닿는 점이 얼마나 흔들리거나 돌아야 하는지가 정해지는 운동학 모형을 만들었어요. 처음에는 닿는 점이 1:2 리사주를 그리게 했는데 고리 대신 곧은 지그재그 끝에 작은 매듭만 달렸어요. 곡률을 닿는 점의 위치에 거는 기하 모형도 해 봤는데, 선형 법칙으로는 코일이 안정하지 않고 흩어진 무늬만 나왔어요. 닿는 점을 축에서 R 떨어진 진자로 두자 작게 흔들리면 사행, 104도를 넘으면 엇갈린 고리, 꼭대기를 넘어가면 옮겨지는 코일이 한 노브 위에 차례로 섰어요. 경계는 벨트 속도를 U_c = RΩ 로 나눈 값으로 0.42, 0.79, 0.815, 1 이고, 1 위에서는 벨트가 실을 곧게 끌며 √(U/V) 만큼 가늘게 펴요. 처음 간격 검사는 한 프레임에 놓인 표본 넷이 같은 자리에 쌓여 틀렸고, 모형을 1/120초씩 나눠 진행하자 한 바퀴 간격이 벨트 속도 0.2~0.95 모두에서 U/f 와 0.01 LED 안으로 맞았어요. 여는 자리에서는 7.2 LED/s 를 0.507 turn/s 로 나눈 14.19 LED 예요. 표본마다 공을 찍던 첫 렌더링은 하이라이트가 표본 간격으로 끊겨 점선처럼 보여서, 이웃 표본을 잇는 캡슐로 바꿨어요. K1 은 라일락에서 열고, 주황에서 라임까지는 그늘진 쪽을 0.72 로 올리고 옅게 해서 갈색과 올리브를 피했어요. 잰 값은 움직임 0.482, 촘촘함 0.059, 복잡도 0.836, 채움 0.332, 색 0.143 이고, 모양 바뀜은 0.025 로 아래 끝 0.021 에 붙어 있다가 놓인 로션이 4초에 걸쳐 퍼지며 납작해지게 하자 0.031 로 올라왔어요. + +### Jelly Layers + +주제 6 으로 투명한 젤리 매니큐어를 겹겹이 바르는 그림을 세웠고, K1 하나로 색상환을 다 돌아요. 붓이 1.6초마다 패널을 한 번씩 지나가며 가장자리에 매니큐어가 맺힌 얇은 한 겹을 남기고, 두 번 바르면 한 번은 지우개 패드가 가장 짙은 띠를 6% 의 얼룩만 남기고 닦아 내요. 겹친 곳은 Beer-Lambert 대로 투과율이 곱해져서, LED 값으로 읽어도 한 겹이 초록을 0.664 통과시키는 자리에서 두 겹은 0.440 이었고 0.664 의 제곱 0.441 과 8비트 반올림 안에서 같았어요. 처음 판은 움직임 0.008, 가장자리 0.000 으로 축 끝에 섰어요. 흰 바탕 위 옅은 분홍은 빨강을 거의 다 통과시켜서, 가장 밝은 채널로 재는 잰 값에는 칠한 것이 거의 남지 않았어요. 바탕을 0.93 으로 낮추고 흡수에 회색 몫을 조금 넣고, 광택이 창 반사와 5초마다 내려가는 빛 띠로 코팅 위에서 움직이게 하자 움직임 0.053, 모양 바뀜 0.038 이 됐어요. 회색 몫을 0.35 로 넣었을 때는 분홍이 탁한 모브로 보여서 0.18 로 줄였어요. 주홍에서 연두까지는 회색 몫을 빼서 깊게 쌓여도 갈색이나 올리브로 가지 않는데, 처음에 그 구간을 노랑 둘레로만 잡았더니 Hue 0.13 의 주홍이 벽돌색이 되어 구간을 넓혔어요. 나머지는 촘촘함 0.008, 채움 1.000, 색 0.191 이고 노브 반응은 0.17, 0.07, 0.15, 0.06 이에요. + +### Lamellar Cream + +주제 6 으로 크림의 라멜라 액정 방울을 직교 편광 현미경으로 본 그림을 세웠고, K1 하나로 간섭색이 색상환을 다 돌아요. 방울마다 광축이 중심에서 바깥을 향한다고 보고 위상 지연을 2Δn ρ arctan(√(R²−ρ²)/ρ) 로 셈해서, 수치 적분과 여섯 자리까지 맞았고 0.652R 에서 1.122ΔnR 로 가장 커요. 그 지연을 빛의 파장마다 sin²(πΓ/λ) 로 거른 미셸-레비 색표로 칠하고 sin²2(α − P) 로 어둡게 해서 몰타 십자가 생겨요. 그린 화면에서 큰 방울의 가장 어두운 방위를 재니 편광자가 18도일 때 15~19도, 27도일 때 26~29도라 십자는 방울이 아니라 편광자를 따라 돌아요. 처음 판은 색표를 YIQ 에서 돌리기만 해서, 가장 많은 작은 방울의 흰색이 그대로라 K1 반응이 0.04 였고 회색 클로버가 성기게 흩어져 보였어요. 지연을 LED 당 75 nm 로 키웠을 때는 중간 방울이 다 2차의 파랑과 청록이 되어 이끄는 색이 묻혔어요. 1차의 흰색에 고른 색상의 옅은 빛을 얹고 지연을 50 nm 로 내리자 대부분이 흰색과 금색에 머물고 반지름 9 LED 넘는 방울만 보라 고리를 둘러, K1 반응이 0.09 가 됐어요. 아래에서 들어오는 방울은 시도마다 크기를 새로 뽑아서 작은 방울만 틈에 들어가 중앙값 반지름이 1분 만에 3.4 LED 로 줄었고, 다음 방울의 크기를 틈이 날 때까지 지키자 5~6 LED 에 머물렀어요. K3 을 올리면 방울이 2초에 걸쳐 부푸는데, 처음에는 넘친 넓이를 지워지는 방울까지 세어서 판이 10초 넘게 검게 비었어요. 지워지는 방울을 빼고 패널 안의 넓이만 세자 방울이 초점 밖으로 하나씩 빠지며 맞춰졌어요. 잰 값은 움직임 0.582, 촘촘함 0.178, 채움 0.592, 색 0.193, 모양 바뀜 0.144 이고 노브 반응은 0.09, 0.35, 0.33, 0.27 이에요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -1886,7 +1945,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 24일 `dev`(`3f5d8c6`)의 268개에 38판의 열 개를 더한 278개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 24일 `dev`(`902f5ea`)의 288개에 40판의 열 개를 더한 298개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -1908,6 +1967,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 38판의 열 개도 어느 축의 끝을 넘지 않아서 칸 경계는 30판 그대로이고, 278개에서도 지도 21장이 다 `none` 이에요. 주제 1 은 패턴이 하나뿐인 칸을 겨눠서, 36판까지의 268개에서 16 이던 그런 칸이 14 가 됐어요. 움직임 × 모양 바뀜 1,3 은 1초에 바뀌는 빛이 0.061 아래인데 2~8초에 블록 평균이 0.217 넘게 바뀌어야 하는 칸이에요. 블록 평균이 한쪽으로만 바뀌면 모양 바뀜은 6초 동안 바뀐 양이라 움직임의 여섯 배가 끝이고, 그 끝에 닿으려면 LED 마다 한 번만, 깜박임 없이 바뀌어야 해요. Serum Fingers 는 세럼이 닿는 LED 를 한 번만 밝혀서 2~8초에 오른 합이 0.251, 내린 합이 0.0000 이었고, 움직임 0.042 가 모양 바뀜의 6분의 1 이에요. 그러면 칸은 모양이 아니라 1초에 밀어 넣는 넓이와 밝기 차이로 골라져요. 촘촘함 × 모양 바뀜 3,4 는 한 LED 결이 판의 반 넘게 한꺼번에 어두워져야 하는 칸이에요. Photochromic 은 LED 마다 펄 조각이나 맨 폴리시를 두어 촘촘함 0.210 을 내고, 짙어진 바탕을 0.11 까지 내려 모양 바뀜 0.365 를 냈어요. 가장 밝은 채널로 재서, 채도만 오르는 짙어짐은 모양 바뀜에 잡히지 않았어요. 남은 14칸 가운데 넷은 여전히 모양 바뀜이 높은 촘촘함의 칸이에요. 촘촘함 × 모양 바뀜 2,4, 3,3, 4,3, 4,4 의 Marbling, Flipdot, Sequins, Pin Art 예요. 움직임 × 모양 바뀜 1,4 의 Lunation 곁에 서려면 모양 바뀜이 0.331 을 넘어야 해서, 한 번씩만 바뀌는 그림이라도 움직임 0.055 에서 0.061 사이의 좁은 띠를 써야 해요. +40판의 열 개도 어느 축의 끝을 넘지 않아서 칸 경계는 30판 그대로이고, 298개에서도 지도 21장이 다 `none` 이에요. 주제 1 은 패턴이 하나뿐인 칸을 겨눠서, 38판까지의 288개에서 12 이던 그런 칸이 10 이 됐어요. 촘촘함 × 채움 4,1 은 LED 의 70% 넘게 꺼진 채 이웃 사이 차이가 평균 0.297 을 넘어야 하는 칸이에요. 켜진 LED 끼리 옆으로 닿지 않으면 촘촘함은 2 × 채움 × 평균 밝기라서, Hydrogen Orbital 은 점을 x + y 가 짝수인 LED 에만 다 밝게 찍어 채움 0.200 에 촘촘함 0.355 를 냈어요. 움직임 × 촘촘함 4,4 는 1초에 바뀌는 빛이 1.49 를 넘으면서 촘촘함이 0.297 을 넘어야 하는 칸이에요. 0 에서 255 까지 오르내리는 주기 P LED 의 띠는 그 방향으로 촘촘함이 2/P 쯤이고, LED 하나가 f Hz 로 오르내리면 움직임은 2f 쯤이에요. Defect Turbulence 는 처음에 실수부를 두 색 띠로 칠해 촘촘함이 0.160 이었고, 검은 바탕에 영선 둘을 LED 너비로 긋고 격자를 두 배로 늘려 촘촘함 0.348, 움직임 2.801 이 됐어요. 남은 하나뿐인 칸은 가장자리 × 촘촘함 1,2 와 1,3, 1,4, 움직임 × 촘촘함 1,4, 움직임 × 모양 바뀜 1,4, 촘촘함 × 모양 바뀜 2,4 와 3,3, 4,3, 4,4, 복잡도 × 모양 바뀜 4,4 예요. 39판이 겨눈 두 칸이 그 가운데 있어요. + 23판부터 28판까지는 `*` 가 붙은 칸이 없었어요. 22판까지 남은 두 칸, 촘촘함 × 모양 바뀜 3,4 와 4,3 에 23판의 Bait Ball 과 Sequins 가 섰어요. 하나 남은 빈 칸이던 복잡도 × 채움 4,3 에는 24판의 Waggle Dance 가 섰어요. 복잡도가 2.173 을 넘고 채움이 0.558~0.815 인 자리예요. 복잡도 축 끝의 Contour Map(2.735)을 빼고 세면 차는 칸이라 `*` 가 붙지 않았고, 그 아래 복잡도 2.4~2.6 에 선 Woodpecker, Dripstone, Agate, Guilloché 는 채움이 다 0.93 을 넘었어요. 이제 지도 21장의 칸이 다 차서 `--gaps` 는 모든 줄에 `none` 을 찍어요. 주제 1 은 축 끝의 패턴 하나가 축을 늘려 새 빈 칸이 날 때까지 고를 칸이 없어요. Waggle Dance 에서 어려운 쪽은 채움이었어요. 벌집은 꺼진 LED 가 거의 없는 그림이라, 빈 방 바닥과 봉한 방 사이 이음매를 24 아래로 눌러 채움 0.732 를 냈어요. 복잡도는 켜진 LED 의 세 채널을 따로 흔드는 낟알로 쉽게 올라서, 첫 판은 오히려 2.766 으로 축 끝을 넘었어요. 25판도 기준 148개의 21장이 다 `none` 이라 주제 1 은 겨눌 칸이 없었어요. 그 대신 패턴이 하나만 선 칸을 셌어요. 148개에서 49칸이었고, 남은 자리로 짠 Penrose 가 그 가운데 여섯 칸에 둘째로 서서 158개에서는 43칸이에요. Penrose 가 겨눈 칸은 촘촘함 × 색 3,4 예요. 촘촘함 0.195~0.299 에 색 0.509 위인 자리이고, 4판의 Jacquard Loom 혼자 서 있었어요. Penrose 는 촘촘함 0.264, 색 0.583 으로 거기 섰어요. 같은 값으로 움직임 × 촘촘함 4,3 과 촘촘함 × 채움 3,3 의 Moire Rings, 촘촘함 × 복잡도 3,2 의 Peg Light, 복잡도 × 색 2,4 의 Gear Train, 채움 × 색 3,4 의 Marbling 곁에도 섰어요. 1 LED 어두운 선으로 나눈 작은 타일에 선명한 색을 여럿 두면 촘촘함과 색이 함께 높은 자리에 닿아요. 26판도 기준 158개의 21장이 다 `none` 이었어요. 패턴이 하나뿐인 칸 43 가운데 넷을 겨눴어요. Dark Field 는 검은 바탕 위의 흩은 빛으로 채움 0.263 에 촘촘함 0.222, 복잡도 2.081 이라, Foxfire 혼자이던 복잡도 × 채움 3,1 과 Peg Light 혼자이던 촘촘함 × 채움 3,1 에 씨앗 여덟이 다 들어요. Origami 는 어두운 탁자 위 종이 한 장이라 채움 0.264 이고, 2초의 정사각형이 8초에 좁은 학 기본형이 되어 모양 바뀜 0.255 로 Domino Run 혼자이던 채움 × 모양 바뀜 1,3 에 서요. Lichtenberg 는 Rabbit Ears 혼자이던 복잡도 × 모양 바뀜 4,3 에 씨앗 여덟 가운데 일곱이 들어요. 네 칸 말고는 바뀐 칸이 없어서, 168개에서 패턴이 하나뿐인 칸은 39칸이에요. 27판도 기준 168개의 21장이 다 `none` 이었고, 39칸 가운데 둘을 겨눴어요. Mitosis 는 채움 0.263, 색 0.540 으로 Neon Alley 곁의 채움 × 색 1,4 에 서요. 판의 4분의 1만 켜는 그림에서 색 0.5 를 넘기려면 켜진 LED 가 거의 다 순색 최대 밝기여야 해서, 형광의 번짐은 셈에 들지 않는 21/255 아래로 눌렀어요. Lava Lake 는 복잡도 2.222, 채움 0.271 로 Moth Lamp 곁의 복잡도 × 채움 4,1 에 서요. 복잡도는 프레임 바이트 값의 엔트로피를 거의 그대로 따라서, 어두운 지각의 결을 채널마다 0~24 전부로 넓히자 1.866 이 2.010 이 됐어요. 붉은 용암은 켜진 LED 하나에 24 를 넘는 바이트가 빨강 하나뿐이라, 이음매를 늘리기보다 주황 쪽으로 데우는 편이 채움을 덜 쓰고 복잡도를 벌었어요. 색 축의 아래 끝은 Cloud Chamber 의 0.052, 모양 바뀜 축의 아래 끝은 Climbing Wall 의 0.021 로 옮겨졌지만 혼자 서는 칸은 그대로라서, 178개에서 패턴이 하나뿐인 칸은 37칸이에요. 28판도 기준 178개의 21장이 다 `none` 이었고, 37칸 가운데 둘을 겨눴어요. Aztec Diamond 는 촘촘함 0.350, 색 0.640 으로 Millefiori 곁의 촘촘함 × 색 4,4 에 서요. 촘촘함은 가장 밝은 채널로 재서 네 색 도미노가 맞닿아도 턱이 없었고, LED 에 묶은 체커판이 그 턱을 줬어요. Stadium Billiard 는 움직임 2.362, 채움 0.186 으로 Warp 곁의 움직임 × 채움 4,1 에 서요. 공 이백 개가 1초에 125 LED 를 가면서 짧은 자취만 남겨요. 겨누지 않은 다섯 칸에도 둘째가 들어서 188개에서 패턴이 하나뿐인 칸은 30칸이에요. 29판도 기준 188개의 21장이 다 `none` 이었고, 30칸 가운데 둘을 겨눴어요. 28판이 모듈러 형식의 등고선으로 겨눠 보라던 가장자리 × 움직임 1,4(Mandelbrot 혼자)는 J-Invariant 가 움직임 0.372 로 모자랐고, Hopf Fibration 이 가장자리 0.207, 움직임 1.607 로 섰어요. 원 일흔 개가 저마다 1초에 몇 LED 씩 쓸고 가서 움직임이 높고, 원마다 옅은 빛 번짐을 둬서 가장자리가 낮아요. J-Invariant 는 복잡도 2.693, 색 0.400 으로 Spectrogram 곁의 복잡도 × 색 4,3 에 서요. 같은 판의 Cream Swirl(0.028)과 Serum Gel(0.079)이 가장자리 축의 아래 끝을 Fire 의 0.120 에서 내려서, 가장자리 칸 경계가 0.305, 0.560, 0.815 에서 0.232, 0.514, 0.796 으로 옮겨졌어요. 그래서 198개에서는 가장자리 × 촘촘함 1,2 가 23판 뒤로 처음 `*` 가 붙은 빈 칸이에요. 가장자리 0.232 아래에 촘촘함 0.091~0.195 인 자리라, 번지는 바탕 위에 잔 결이 고르게 깔린 그림이 들어설 수 있어요. 같은 지도의 1,3 과 1,4, 가장자리 × 채움 1,1 도 비었지만 끝의 패턴 하나를 빼면 차서 `*` 가 없어요. 198개에서 패턴이 하나뿐인 칸은 24칸이에요. 촘촘함이 0.299 를 넘는 패턴이 20판에서 셋 늘었고, 셋 다 그 값을 다른 길로 냈어요. Agate 는 1 LED 띠를 벽까지의 거리로 그어서, 축에 나란한 띠는 이웃 쌍의 절반에서만 차이가 나고 대각선 띠는 70% 쯤에서 나요. 방향이 고르게 섞이면 띠 너비 0.85 LED 언저리가 가장 높았어요. Millefiori 는 촘촘함을 가장 밝은 채널로 재서 코발트와 카나리아 노랑이 맞닿아도 턱이 0 이었어요. 고리마다 밝은 유리와 그 유리를 어둡게 뽑은 유리를 번갈아 넣어야 했어요. Sunglint 는 반짝임을 물결 띠마다 체커판의 한쪽 칸에만 둬서 반짝임끼리 닿지 않게 했어요. 셋 다 채움이 0.85 를 넘어서, 촘촘함 × 채움 4,4 는 꺼진 LED 없이도 닿는 칸이었어요. 21판의 Sandpile 이 일곱째예요. 칸 하나가 LED 하나이고 알 셋 옆의 알 둘이 가장 흔한 이웃 쌍이라, 밝기를 0, 2, 1, 3 차례로 올려 더미 안의 촘촘함이 0.40 언저리예요. 판의 75% 는 덮여야 0.3 을 넘어요. 22판의 Pin Art 와 Night Flight 가 여덟째와 아홉째예요. Pin Art 는 핀과 틈의 체커 결이라 이웃 쌍이 모두 핀과 틈의 쌍이고, Night Flight 는 Pancakes 의 체커 결에 1 LED 가로등을 LED 의 12.7% 만큼 얹어서 가장자리 0.258 에 촘촘함 0.328 이에요. 촘촘함과 가장자리는 가장 밝은 채널로 재서, 색을 섞는 자리마다 섞은 뒤 가장 밝은 채널을 다시 맞춰야 했어요. 땅과 강물을 섞은 자리에서 가장 밝은 채널이 0.68 까지 내려가 체커 결의 37 이 41~60 으로 넘어갔고, 그대로 둔 판은 가장자리가 0.05 높았어요. 23판의 Sequins 가 열째예요. 체커 결 없이, 가로 5 LED 와 세로 3 LED 간격으로 엇갈려 단 스팽글의 빛점과 그림자 줄로 0.327 을 냈어요. 촘촘함은 무늬의 모양보다 턱의 크기를 따라가서, 스팽글 몸통을 0.8 에서 0.6 으로 어둡게 하자 은빛 면이 0.339 에서 0.311 로 내려갔어요. 26판의 Lichtenberg 는 0.2 를 넘긴 판을 버렸어요. 판 어디서나 돋는 1 LED 잔가지로 0.248 을 냈더니 검은 직교 그물이 나무판을 덮어 미로로 보였어요. 잔가지를 탄 줄기에만 달아 빈 나무를 42% 남기자 0.130 이었어요. 촘촘함은 판 전체 이웃 쌍의 평균이라, 빈 나무가 넓게 남는 그림은 줄기 둘레가 아무리 잘아도 평균이 낮아요. 같은 판의 Dark Field 는 검은 바탕에 세균 몇백과 밝은 테두리를 판 곳곳에 흩어 0.222 를 냈어요. @@ -1966,9 +2027,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 38판에서는 Anderson Localization 이 시간을 128줄 아래로 내려 55/64/52 이에요. 뒤의 위치 에너지 줄무늬를 빼고 재도 29/40/38 이라 파동만으로 세 칸을 다 써요. 로그 밝기의 바닥을 1e-4 에 두었을 때는 꼬리가 판 폭을 다 채워 멈춘 원뿔이 보이지 않았고, 1e-3 으로 올리자 27 LED 너비에서 멈춘 기둥이 섰어요. Milk Ring 은 세럼이 다 켜져 100/100/100 이라 바뀐 LED 로 셌어요. 2초와 8초 두 화면 사이로는 12/18/29 이고, 2~8초 어느 때든 바뀐 것으로는 26/48/51 이에요. 첫 고리가 47줄, 둘째 대가 76줄에서 깨지고 마지막 고리가 117줄에서 녹아서, 패널 높이가 그대로 갈라지는 차례예요. Couette Shimmer 도 99/99/99 이고 바뀐 LED 로는 35/28/32 예요. 띠가 층층이 쌓이는 테일러 소용돌이는 세운 원통을 옆에서 본 그림이라 세운 패널에 맞았어요. Pólya Urn 은 줄이 로그 시계의 뽑기 차례라 100/100/100 이고, Bernoulli Convolutions 는 줄이 λ 라 68/96/77 이에요. Infinity Series 는 네 줄이 위에서 타격선까지 떨어져 33/33/33 이에요. Sperner's Lemma 는 꼭짓점이 위에 선 삼각형이라 12/42/82 로 위가 좁고, Figure-Eight Orbit 은 8자가 패널 높이로 서도 선 그림이라 14/16/14 예요. +40판에서는 Continuum Random Tree 가 바닥의 뿌리에서 128줄을 올라가는 나무라 `thirds` 가 24/60/27 이에요. 나무 꼭대기는 가지가 성겨서 처음에는 8/52/22 였고, 빛 번짐을 넓히고 가지 굵기를 질량의 0.35 제곱으로 주고 엑스커션 넷 가운데 위가 가장 무거운 것으로 열어 위 3분의 1을 24 로 올렸어요. 2~8초 사이 어느 때든 밝기가 24 넘게 바뀐 LED 로 세면 56/99/100 이라, 가지가 자라고 시야가 뛰어드는 동안 판 거의 전체가 바뀌어요. Perfume Tears 는 잔 벽의 막이 어디서나 켜지고 테 위의 마른 유리만 어두워서 37/100/100 이에요. 같은 셈으로는 26/71/35 라, 오르는 막과 테에서 떨어지는 눈물이 세 칸을 다 써요. + ### 낯선 수학 -주제 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)이에요. +주제 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)이에요. 40판도 여섯을 더했어요. 앨더스의 브라운 연속 랜덤 나무(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 함수(Hydrogen Orbital), 젤도비치 근사의 팬케이크(Zeldovich Pancakes), 망델브로가 내다보고 롤러, 슈람, 베르너가 증명한 브라운 운동의 바깥 경계(Brownian Frontier), 복소 긴즈부르크-란다우 방정식의 결함 난류(Defect Turbulence), 루퍼트 왕자의 정육면체(Prince Rupert's Cube)예요. 목록 밖에서 다음에 해 볼 것이에요. 영 도형의 플랑셰렐 성장은 상자가 하나씩 붙으며 로건-셰프와 베르시크-케로프의 극한 곡선(1977)으로 다가가요. 아즈텍 다이아몬드의 북극권과 같은 극한 모양이라 같은 판에 두지 않아요. 슈타이너 사슬은 반전 하나로 고리의 원들이 미끄러지는 포리즘이고, 민코프스키의 물음표 함수(1904)는 페리 나무를 이진 나무로 펴는 계단이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -1994,6 +2057,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 38판에서 본 것이에요. 정리가 수를 못 박는 대상은 이번에도 셈으로 지켜봤어요. Sperner's Lemma 는 노브 60 자리의 7,740 프레임에서 무지개 방이 늘 홀수였고, Anderson Localization 의 노름은 1 에서 1.0e-4 안에, Figure-Eight Orbit 의 에너지는 200주기 동안 9.3e-9 안에 머물렀어요. 특이한 측도가 패널에서는 매끈해 보였어요. 황금비의 베르누이 합성곱은 차원이 0.9957 이라 64칸에서 이웃 줄과 같아 보였고, 처음 몇 항의 합이 정확히 겹치는 수를 세서야 피소 줄이 섰어요. 자기 닮음은 같은 크기로 그려야 읽혔어요. Infinity Series 는 네 번째 음마다 읽은 선율을 1행짜리 점으로 두었을 때 원래 선율과 견줄 수 없었고, 한 박 내내 깔아 같은 블록으로 세우자 맞아떨어졌어요. 극한의 분포는 줄을 세워야 보였어요. Pólya Urn 의 64열은 저마다 다른 비율로 굳는데, 그 비율이 고르게 퍼졌다는 것은 열들이 차례로 선 뒤에야 곧은 그러데이션으로 보여요. 증명의 걸음이 그림이 되기도 했어요. 슈페르너의 문 걷기는 방마다 다른 문으로 나가기만 해서, 걸음 하나가 고정점을 찾아가는 길로 읽혀요. 대칭을 지키는 흔들기는 너그러웠어요. 8자 해는 시작 배치의 대칭을 지키며 민 Kick 0.6 에서도 71주기를 버텼지만, 도는 중에 0.05 로 민 것은 90초 안에 깨졌어요. +40판에서 본 것이에요. 정리가 수를 못 박는 대상은 이번에도 셈으로 먼저 지켜봤어요. Defect Turbulence 는 주기 경계라 감김의 합이 0 이어야 해서, 32,400 프레임 동안 + 결함과 − 결함의 수가 다른 프레임이 하나도 없는 것을 본 뒤에 그렸어요. Hydrogen Orbital 은 r 의 평균이 열두 궤도 모두 a0(3n² − l(l+1))/2 와 1.4σ 안에 들었고, Prince Rupert's Cube 의 굴은 그림자 육각형 안의 가장 큰 정사각형을 이분법으로 찾아 1.060660172, 곧 3√2/4 였어요. 극한을 말하는 대상은 이번에도 패널 크기에서 멈췄어요. Brownian Frontier 의 바깥 경계는 상자 64칸에서 256칸 사이 기울기가 1.325 로 4/3 언저리에 왔지만, 지나간 자리는 넓이가 D²/log D 로 자라서 2 가 아니라 1.877 이었어요. 칠하는 법이 수학을 숨기기도 드러내기도 했어요. 결함 난류는 위상을 색상환에 얹지 않고 실수부와 허수부의 영선만 그어서 결함이 두 선의 만남으로 읽혔고, 선 너비를 위상이 아니라 |f|/|∇f| 로 LED 거리에 맞춰야 평평한 곳이 덩어리로 번지지 않았어요. Zeldovich Pancakes 는 LED 한 칸에 모아 세면 막 생긴 꺾임이 그 밑의 능선보다 조금만 밝아서, 제 꺾임을 지난 물질을 장밋빛으로 칠하자 첫 팬케이크가 보였어요. 3차원 대상은 이번에도 잘라야 읽혔어요. Hydrogen Orbital 은 시선 쪽 두께를 구름의 3분의 1로 잘라서야 마디 원뿔이 엽 사이의 검은 V 로 섰고, Prince Rupert's Cube 는 얇게 남은 벽이 흩어진 조각으로 보여서 정육면체의 열두 모서리를 옅은 선으로 늘 그었어요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 모자 타일(스미스, 마이어스, 캐플런, 굿먼-스트라우스, 2023)은 한 가지 타일로만 평면을 비주기로 덮고, 뒤집어 놓인 타일이 판에 드문드문 흩어져요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 뷔퐁의 바늘(1777)은 Bertrand Paradox 와 같은 꼴로, 금을 넘는 바늘의 몫이 2/π 로 모이는 막대와 함께 세울 수 있어요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. @@ -2004,10 +2069,12 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 목록 밖에서 36판 뒤에 해 볼 것이에요. 아브레슈와 랑거(1986)가 분류한 꽃잎 모양 곡선은 제 몸을 가로지르는데도 곡선 단축 흐름 아래서 모양을 지킨 채 크기만 줄어요. Curve Shortening 의 흐름을 그대로 써서, 원 말고도 그렇게 사라지는 곡선을 보여 줄 수 있어요. 실라시 다면체(실라시 러요시, 1977)는 일곱 면이 다 서로 맞닿은 토러스 꼴 다면체라 Heawood Torus 의 지도를 평평한 면 일곱 장으로 접은 꼴이에요. 원통을 위아래로 누르면 생기는 요시무라 좌굴 무늬(요시무라 요시마루, 1951)는 Schwarz Lantern 과 같은 마름모라 같은 판에 두지 않아요. 룽게 현상(카를 룽게, 1901)은 Gibbs Phenomenon 의 줄과 항 수 배치를 그대로 써서, 등간격 점의 보간 다항식이 차수가 오를수록 양 끝에서 터지고 체비쇼프 점에서는 가라앉는 것을 그릴 수 있어요. 사토-테이트 추측(사토 미키오, 존 테이트, 1960년대, 2011년 증명)은 Elliptic Chords 의 곡선을 소수 p 마다 세어 p + 1 − #E(F_p) 를 2√p 로 나눈 값이 반원 분포로 쌓이는 막대예요. 르네 톰의 일곱 파국 가운데 제비꼬리는 조절 변수가 셋이라, 하나를 노브에 걸면 Catastrophe Machine 의 마름모 자리에 뾰족점 둘과 자기 교차 하나를 가진 경계가 생겼다 사라져요. -목록 밖에서 37판 뒤에 해 볼 것이에요. 로터 라우터 응집(제임스 프롭, 2001)은 원점에서 떠난 알갱이가 칸마다 돌아가는 화살표를 따라 걷다 빈 칸에 앉는데, 쌓인 덩이가 거의 완전한 원판이고 그 안의 화살표가 프랙탈 모자이크를 이뤄요(레빈과 페레스, 2009). 걷는 한 점이 랭턴의 개미와 같은 꼴이라 「흔한 것」에 걸리는지 먼저 따져야 해요. 단단한 정사각형은 원판과 달리 테트라틱 상을 지나 녹아서(보이치에호프스키와 프렌켈, 2004), Hexatic Phase 의 색을 격자 회전 90도 한 바퀴로 바꾸면 같은 그림으로 견줄 수 있어요. 결정마다 자라는 빠르기를 다르게 하면 Johnson-Mehl 의 경계가 아폴로니우스 원이 되는 곱셈 가중 보로노이가 돼요. 복소 긴즈부르크-란다우 방정식(1950)의 결함 혼돈은 Kuramoto-Sivashinsky 를 2차원으로 넓힌 그림이라, 위상이 한 바퀴 도는 점 결함이 생기고 사라지며 나선을 끌고 다녀요. 브라운 연속 랜덤 나무(데이비드 올더스, 1991)는 브라운 들뜸의 높이를 나무의 높이로 읽어서, 뿌리를 패널 바닥에 두면 128줄을 다 쓰는 나무가 돼요. +목록 밖에서 37판 뒤에 해 볼 것이에요. 로터 라우터 응집(제임스 프롭, 2001)은 원점에서 떠난 알갱이가 칸마다 돌아가는 화살표를 따라 걷다 빈 칸에 앉는데, 쌓인 덩이가 거의 완전한 원판이고 그 안의 화살표가 프랙탈 모자이크를 이뤄요(레빈과 페레스, 2009). 걷는 한 점이 랭턴의 개미와 같은 꼴이라 「흔한 것」에 걸리는지 먼저 따져야 해요. 단단한 정사각형은 원판과 달리 테트라틱 상을 지나 녹아서(보이치에호프스키와 프렌켈, 2004), Hexatic Phase 의 색을 격자 회전 90도 한 바퀴로 바꾸면 같은 그림으로 견줄 수 있어요. 결정마다 자라는 빠르기를 다르게 하면 Johnson-Mehl 의 경계가 아폴로니우스 원이 되는 곱셈 가중 보로노이가 돼요. 목록 밖에서 38판 뒤에 해 볼 것이에요. 오브리-앙드레 모형(세르주 오브리, 길 앙드레, 1980)은 Anderson Localization 의 무작위 에너지를 무리수 주기의 코사인으로 바꾼 사슬이라, 세기 2 에서 모든 상태가 한꺼번에 퍼짐에서 갇힘으로 넘어가요. 같은 시공간 그림에 그 문턱을 노브로 걸 수 있어요. 수바코프와 드미트라시노비치(2013)가 찾은 세 몸의 주기 궤도 열세 갈래는 나비와 나방, 요구르트 같은 이름이 붙은 꼴이라 Figure-Eight Orbit 의 적분을 그대로 써요. 게일(1979)이 보인 헥스 정리는 칸이 다 찬 헥스 판에는 꼭 한쪽이 이기는 길이 있다는 것으로, 슈페르너 보조정리처럼 브라우어르의 고정점 정리와 같은 힘이에요. 마름모 판을 세워 두 색이 차오르며 이긴 길이 켜지는 그림이 돼요. 쉬츠와 트림퍼의 코끼리 걸음(2004)은 지난 걸음 가운데 하나를 골라 따르는 기억 걸음이라, 따르는 확률 3/4 에서 퍼짐이 바뀌어요. Pólya Urn 처럼 64열을 나란히 둘 수 있어요. +목록 밖에서 40판 뒤에 해 볼 것이에요. 브라운 맵(장프랑수아 르 갈, 그레고리 미에르몽, 2013)은 같은 엑스커션에 브라운 뱀을 얹어 연속 랜덤 나무를 구면으로 접은 것이라 Continuum Random Tree 의 다음 걸음이고, 가지점마다 가지가 끝없이 나는 안정 레비 나무(토마 뒤케느, 르 갈, 2002)도 같은 틀로 그려요. 브라운 경로의 자르는 점은 차원 3/4, 개척점은 7/4 로, 바깥 경계의 4/3 과 함께 롤러, 슈람, 베르너(2001)가 같은 교차 지수로 셈했어요. Brownian Frontier 의 걸음에서 둘을 따로 칠하면 세 차원을 한 패널에서 견줄 수 있어요. 포물 좌표로 푼 수소의 슈타르크 상태(파울 엡스타인, 1926)는 같은 n 의 궤도를 섞어 전기장 쪽으로 기운 엽을 만들어서, 위아래가 다른 구름으로 세운 패널에 서요. 정사면체와 정팔면체도 제 몸을 지나간다는 것은 크리스토프 스크리바(1968)가 보였고, 제 몸을 지나가지 못하는 첫 볼록 다면체는 2025년에 슈타이닝거와 유르케비치가 찾았어요. 접착 모형(구르바토프, 사이체프, 샨다린, 1989)을 점성이 남은 버거스 흐름 그대로 호프-콜 변환(1950, 1951)으로 풀면 Zeldovich Pancakes 의 벽 두께를 노브에 걸 수 있어요. + ### 화장품 팝업 주제 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 가 생겨요. @@ -2030,7 +2097,9 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 38판이 넷을 더 썼어요. 젤에 떨어져 고리로 말리고 작은 고리로 갈라지며 잠기는 크림 방울(Milk Ring), 두 원통 사이 테일러 소용돌이에 든 쉬머 세럼(Couette Shimmer), UV 손전등이 지나간 자리만 짙어지는 포토크로믹 네일(Photochromic), 진한 크림에 밀려 들어가 손가락으로 갈라지는 세럼(Serum Fingers)이에요. 넷 다 「다음에 해 볼 질감」에 적혀 있던 감이에요. 이번에도 물리를 따르자 사물로 읽혔어요. Milk Ring 은 톰슨과 뉴얼(1885)이 본 고리의 갈라짐을 대마다 따르고, Serum Fingers 는 압력장을 풀어 압력 기울기가 가장 큰 앞부터 밀어요. 가장 밝은 채널만 읽는 잣대는 이번에는 칸을 겨누는 손잡이가 됐어요. Photochromic 은 채도만 오르면 모양 바뀜이 그대로라 짙은 상태의 바탕을 0.11 까지 내렸고, Serum Fingers 는 크림을 밝기 71 의 회색으로 두어 세럼과 0.7 쯤 벌렸어요. 주황과 노랑 쪽의 갈색과 올리브는 넷 다에서 다시 왔어요. Milk Ring 은 그 범위에서 유리 아래쪽을 2.1배까지 밝혔고, Couette Shimmer 는 어두운 띠도 밝기 0.85 를 지키고 채도로만 갈랐고, Photochromic 은 짙어지는 대신 크림색에서 밝은 원색으로 바뀌어요. Milk Ring 은 고리 심을 반지름의 4분의 1 로 두꺼이 그렸을 때 하얀 알약으로 보였고, 8분의 1 로 줄이고 가운데로 세럼이 비치게 하자 고리로 읽혔어요. -다음에 해 볼 질감이에요. 세팅 미스트가 유리판에 맺혀 굵어지고 흘러내리는 것은 Lip Gloss 와 Setting Mist 사이라 같은 판에 두지 않아요. 매니큐어 붓이 남긴 줄이 스스로 펴지는 것은 Cream Swirl 과 가까워서 같은 판에 두지 않아요. 젖은 립글로스나 탑코트가 굳는 것은 Clay Mask 처럼 광이 먼저, 색이 나중에 빠지는 두 문턱으로 그릴 만해요. 립밤이 체온에 녹아 번지거나 고체 향수가 문질러 펴지는 것은 Cleansing Balm 의 녹는 경계를 가로로 돌린 그림이에요. 크러시드 벨벳처럼 결이 조각마다 다른 방향으로 눌린 천을 보이지 않는 손이 쓰다듬어 조각이 하나씩 같은 방향으로 눕는 것은 Velvet Nap 의 결 벡터를 그대로 써요. 물 위에 매니큐어 고리를 떨어뜨려 겹치고 바늘로 끄는 워터 마블 네일은 Bath Fizz 의 유체를 그대로 써요. 기름종이가 피지를 빨아들인 자리만 반투명해지는 것은 Toner Pad 의 길이장으로 그릴 만해요. 하이드로겔 마스크처럼 두껍고 맑은 젤 시트가 붙는 것은 Sheet Mask 의 젖는 선을 그대로 써요. 움직이는 판 위로 떨어지는 점성 줄기는 판이 빨라질수록 똬리가 구불거리는 선으로, 다시 8자 매듭으로 바뀌어요. 치우-웹스터와 리스터의 유체 재봉틀(2006)로 컨베이어 위에 깔리는 로션 띠를 그릴 만해요. 위에서 본 비누 거품은 변이 n 개인 방이 (n − 6) 에 비례해 커지거나 줄어드는 폰 노이만의 법칙(1952)으로 굵어져서, Foam Lather 와 다른 질감이 돼요. 크림 하이라이터를 손끝 열로 녹여 펴는 것은 Embossed Glow 의 엠보싱을 Cleansing Balm 의 녹는 경계로 무너뜨리는 그림이에요. 반투명한 젤리 네일을 여러 번 겹쳐 바르면 겹친 곳만 짙어져서, Taffy Cream 의 크림 대신 붓 자국이 남는 얇은 층을 쌓을 만해요. 오일 방울이 머리결 대신 속눈썹처럼 짧고 굽은 털 다발을 타고 끝에 맺히는 것은 Hair Oil 의 방울 셈을 그대로 써요. 세럼이 스포이트에서 실로 늘어지다 구슬로 끊기는 레일리-플라토 불안정(조제프 플라토, 1873; 레일리 경, 1878)은 위에서 아래로 늘어지는 실과 구슬 줄이라 세운 패널에 맞아요. 토너나 향수 잔 벽을 타고 오르내리는 와인의 눈물(제임스 톰슨, 1855; 카를로 마랑고니, 1865)도 기어오르는 막과 흘러내리는 줄기로 128줄을 써요. 두 유리판 사이의 크림을 떼어 낼 때 공기가 가운데로 파고들며 나뭇가지처럼 갈라지는 것은 Serum Fingers 의 성장 법칙을 바깥에서 안으로 돌려 그릴 만해요. +40판이 넷을 더 썼어요. 향수 잔 벽을 기어오르다 떨어지는 와인의 눈물(Perfume Tears), 움직이는 벨트에 떨어져 코일과 고리와 사행을 놓는 로션의 유체 재봉틀(Lotion Stitch), 겹친 곳만 짙어지는 젤리 네일(Jelly Layers), 직교 편광으로 본 라멜라 크림 방울의 몰타 십자(Lamellar Cream)예요. 앞의 셋은 「다음에 해 볼 질감」에 적혀 있던 감이에요. 이번에도 물리를 따르자 질감으로 읽혔어요. Perfume Tears 의 테는 마랑고니 응력이 나를 수 있는 가장 큰 플럭스 4A³/27B² 가 들어오는 플럭스와 같아지는 높이에 서고, Lotion Stitch 의 무늬는 실이 미끄러지지 않는다는 조건 하나로 코일에서 곧은 줄까지 차례로 바뀌어요. 가장 밝은 채널만 읽는 잣대는 이번에도 옅은 색을 놓쳤어요. Jelly Layers 의 첫 판은 흰 바탕 위 옅은 분홍이 빨강을 거의 다 통과시켜 움직임 0.008, 가장자리 0.000 이었고, 바탕을 0.93 으로 낮추고 흡수에 회색 몫 0.18 을 넣자 0.053 이 됐어요. 색상환 노브가 바꿀 것이 없는 흰색도 있었어요. Lamellar Cream 은 가장 많은 작은 방울이 1차의 흰색이라 K1 이 0.04 였고, 그 흰색에 고른 색상을 옅게 얹자 0.09 가 됐어요. 주황과 노랑 쪽의 갈색과 올리브는 넷 다에서 다시 왔어요. Jelly Layers 는 주홍에서 연두까지 회색 몫을 뺐고, Lotion Stitch 는 그늘진 쪽을 0.72 로 올리고 옅게 했고, Perfume Tears 와 Lamellar Cream 은 노랑과 라임을 밝히고 옅게 했어요. + +다음에 해 볼 질감이에요. 세팅 미스트가 유리판에 맺혀 굵어지고 흘러내리는 것은 Lip Gloss 와 Setting Mist 사이라 같은 판에 두지 않아요. 매니큐어 붓이 남긴 줄이 스스로 펴지는 것은 Cream Swirl 과 가까워서 같은 판에 두지 않아요. 젖은 립글로스나 탑코트가 굳는 것은 Clay Mask 처럼 광이 먼저, 색이 나중에 빠지는 두 문턱으로 그릴 만해요. 립밤이 체온에 녹아 번지거나 고체 향수가 문질러 펴지는 것은 Cleansing Balm 의 녹는 경계를 가로로 돌린 그림이에요. 크러시드 벨벳처럼 결이 조각마다 다른 방향으로 눌린 천을 보이지 않는 손이 쓰다듬어 조각이 하나씩 같은 방향으로 눕는 것은 Velvet Nap 의 결 벡터를 그대로 써요. 물 위에 매니큐어 고리를 떨어뜨려 겹치고 바늘로 끄는 워터 마블 네일은 Bath Fizz 의 유체를 그대로 써요. 기름종이가 피지를 빨아들인 자리만 반투명해지는 것은 Toner Pad 의 길이장으로 그릴 만해요. 하이드로겔 마스크처럼 두껍고 맑은 젤 시트가 붙는 것은 Sheet Mask 의 젖는 선을 그대로 써요. 위에서 본 비누 거품은 변이 n 개인 방이 (n − 6) 에 비례해 커지거나 줄어드는 폰 노이만의 법칙(1952)으로 굵어져서, Foam Lather 와 다른 질감이 돼요. 크림 하이라이터를 손끝 열로 녹여 펴는 것은 Embossed Glow 의 엠보싱을 Cleansing Balm 의 녹는 경계로 무너뜨리는 그림이에요. 오일 방울이 머리결 대신 속눈썹처럼 짧고 굽은 털 다발을 타고 끝에 맺히는 것은 Hair Oil 의 방울 셈을 그대로 써요. 세럼이 스포이트에서 실로 늘어지다 구슬로 끊기는 레일리-플라토 불안정(조제프 플라토, 1873; 레일리 경, 1878)은 위에서 아래로 늘어지는 실과 구슬 줄이라 세운 패널에 맞아요. 두 유리판 사이의 크림을 떼어 낼 때 공기가 가운데로 파고들며 나뭇가지처럼 갈라지는 것은 Serum Fingers 의 성장 법칙을 바깥에서 안으로 돌려 그릴 만해요. 매니큐어가 마르며 안료가 가장자리로 몰려 고리가 지는 커피 고리 효과(로버트 디건 외, 1997)는 Jelly Layers 다음의 질감이에요. 브런, 오두아, 리브, 이브스, 리스터(2015)의 기하 모형은 닿는 점 위치에 맞춘 곡률 함수 하나로 이중 사행과 8자까지 내서, Lotion Stitch 의 운동학 모형이 못 낸 무늬를 그려요. 꿀이나 시럽이 접시에 떨어져 겹겹이 접히는 점성 접힘(스코로보가티와 마하데반, 2000)도 감이에요. 알코올 섞인 물방울이 기름 위에서 터지며 가는 손가락을 뿜는 마랑고니 터짐(카이저 외, 2017)은 Perfume Tears 의 응력의 다른 얼굴이에요. 스멕틱 액정을 직교 편광으로 본 부채꼴 초점 원뿔 결(조르주 프리델, 1922)은 검은 쌍곡선과 타원이 얽힌 결이라, Lamellar Cream 의 몰타 십자 다음 감이에요. ## 다시 돌리기 From 1caa8d2add955f3a77e85f07f15f3fe46c9a0c59 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 23:54:46 +0000 Subject: [PATCH 15/16] =?UTF-8?q?40=ED=8C=90=20=EC=97=B4=20=EA=B0=9C?= =?UTF-8?q?=EB=A1=9C=20index.html=20=EC=9D=84=20=EB=8B=A4=EC=8B=9C=20?= =?UTF-8?q?=EC=A7=93=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CLauBYuJBXD9L9NRJjxCqK --- incoming/pattern-study/README.md | 2 ++ incoming/pattern-study/index.html | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index e4d3cbcc3..e2ed09bac 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -2003,6 +2003,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 38판의 열 개는 가장 낮은 노브가 Serum Fingers 의 K3 Fingers 로 네 자리로 0.0454 이고, Anderson Localization 의 K3 Disorder 0.0464 와 K4 Samples 0.0484 가 그다음이에요. 둘 다 자라는 그림이라 2~3초 창에는 손가락이 판 아래쪽에만 있고 원뿔도 다 열리기 전이어서, 노브가 바꿀 넓이가 적어요. 한 그림의 얼룩을 평균해 매끈하게만 하는 Samples 는 0.03 언저리였고, 뒤의 줄무늬가 표본 수에 반비례해 옅어지게 하자 0.05 가 됐어요. Figure-Eight Orbit 은 꼬리를 오래된 것부터 칠해 세 색을 살리자 K3 Trail 이 0.04 로 떨어져서, 꼬리 머리를 넓히고 끝을 빨리 가늘게 해 0.06 으로 올렸어요. +40판의 열 개는 가장 낮은 노브가 Brownian Frontier 의 K4 Rings 로 네 자리로 0.0359 이고, Zeldovich Pancakes 의 K4 Cross 0.0369 가 그다음이에요. Brownian Frontier 의 Rings 는 첫 판에서 0.0038 로 문턱 아래였어요. 지난 경계를 오래 남길수록 안쪽 낙서를 어둡게 하고 처음 화면을 키우자 0.036 이 됐는데, 같은 코드를 다른 씨앗으로 재면 0.098 이었어요. 무작위 그림은 씨앗 하나로 잰 반응이 그만큼 흔들려요. Zeldovich Pancakes 의 Cross 는 접착 모형의 끈적한 극한과 그냥 젤도비치 사이를 오가는데, 2~3초 창이 D 1.5~1.7 이던 첫 판에서는 두 극한이 거의 같은 그림이라 0.0014 였어요. 첫 판을 D 2.6 에서 열어 매듭이 부푸는 자리를 창 안에 넣고서야 0.04 가 됐어요. + Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. ### 세로 64×128 diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index 5a75e8563..7dfc16544 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -141,7 +141,7 @@

패턴 연구

- + +