From cbfb5ae05160976b90b601feebd03e794e678d61 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 20:10:30 +0000 Subject: [PATCH 01/13] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=2038=ED=8C=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013eaaLKBuXCHpZzU9162GoH From 1698a2dbac9d1dcee8dcdf9292193a9a8b675bbb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 20:31:04 +0000 Subject: [PATCH 02/13] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2038=ED=8C=90:=20Anderson=20Localization=20=EC=9D=84=20?= =?UTF-8?q?=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/anderson-localization.js | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 incoming/pattern-study/patterns/anderson-localization.js diff --git a/incoming/pattern-study/patterns/anderson-localization.js b/incoming/pattern-study/patterns/anderson-localization.js new file mode 100644 index 000000000..45e5fe9c9 --- /dev/null +++ b/incoming/pattern-study/patterns/anderson-localization.js @@ -0,0 +1,262 @@ +// ===== Patternflow pattern ===== +// Title: Anderson Localization +// Family: wave +// Idea: A quantum particle starts on one site of a chain whose site energies are random, shown as faint stripes behind, and its probability is drawn with the sites across the panel and time running down it: the cone it opens stops growing about 13 sites either side and stands there as a speckled column for the rest of the run, while the dotted edges of the cone a clean chain would make run on to the sides. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Anderson localization (Philip W. Anderson, 1958), pattern round 38 +// Round: 38 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Disorder=4..12, Samples=1..16 +// +// Knob 1: Hue. Turns the wave round the wheel; the old run below the head and the stripes turn with it. At 0 tails violet, body azure, core pale cyan; at 0.1 blue, at 0.2 violet, at 0.35 pink, at 0.42 red, at 0.5 peach, at 0.58 butter, at 0.66 lime, at 0.85 mint. From 0.42 to 0.68 the tails turn toward rose instead of yellow and the body is paled and lifted up to 1.4 times, so nothing reads brown or olive. +// Knob 2: Speed. How fast time is written down the panel. At 2 twenty rows a second, so a run of 128 rows (t = 0 to 32 in units of the hopping) takes 6.4 s and the finished run holds 1.5 s; at 0.2 64 s, at 3 4.3 s. +// Knob 3: Disorder. The width W of the random site energies, uniform in [-W/2, W/2]. At 4 the localization length at the band centre is about 105/W^2 = 6.6 sites: the lit column, where the probability is over 1/1000, is 13 LEDs wide at row 8, 19 at row 16, 25 at row 32 and stops at 27 to 28 from row 48 on, where a clean cone would be 49 wide and growing. At 8 it stops at 12 LEDs by row 16; at 12 the particle hardly leaves its site and the column is 8 LEDs wide from row 8. +// Knob 4: Samples. How many disorder samples run side by side from the same site, their probabilities averaged. At 1 the speckle of one sample, braided fringes and dark holes where its localized states beat; from about 8 the holes fill in, and at 16 the column is a smooth bell that falls off exponentially on both sides and stops widening. +// Buttons: K1 tap launches now, on a new sample of energies and a new site. + +const W = 64, H = 128, NS = 192, OFF = 64, MAXS = 16; +// Time per row: 0.25 in units of the hopping, so 128 rows hold t = 0 to 32 +// and a clean cone, whose edge runs 2 sites per unit, reaches 32 sites out at +// row 64. RK4 steps of 0.025, ten to a row. +const TROW = 0.25, STEPS = 10, DT = TROW / STEPS; +// Rows per second at Speed 1. +const RATE = 10; +// Brightness from the probability on a log scale: 1e-3 is dark, 0.4 full. +const LMIN = -3, LSPAN = 2.6; +// Opening hue: azure. +const H0 = 0.56; +const { floor, min, max, abs, log10, round, random } = 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) { + params.s = { + hue: 0, speed: 2, dis: 3, ns: 1, begun: false, + // u: each sample's site energies over W, uniform in [-1/2, 1/2]. + u: new Float64Array(MAXS * NS), re: new Float64Array(MAXS * NS), im: new Float64Array(MAXS * NS), done: 0, + ya: new Float64Array(NS), yb: new Float64Array(NS), ka: new Float64Array(NS), kb: new Float64Array(NS), + sa: new Float64Array(NS), sb: new Float64Array(NS), + n0: 96, t: 0, row: 0, prog: 0, hold: 0, norm: 1, + rho: new Float32Array(W * H), vt: new Float32Array(W * H), cone: new Uint8Array(W * H), dim: new Float32Array(H), + lo: [0, 0, 0], mid: [0, 0, 0], hi: [0, 0, 0], pos: [0, 0, 0], neg: [0, 0, 0], + }; +} + +// dy/dt for y = re + i im under i dy/dt = H y, H y_n = -(y_n+1 + y_n-1) + V_n y_n, +// with hard walls past both ends of the chain: d re = H im, d im = -H re. +function deriv(a, b, u, o, Wd, ka, kb) { + for (let n = 0; n < NS; n++) { + const V = Wd * u[o + n]; + let ha = V * a[n], hb = V * b[n]; + if (n > 0) { + ha -= a[n - 1]; + hb -= b[n - 1]; + } + if (n < NS - 1) { + ha -= a[n + 1]; + hb -= b[n + 1]; + } + ka[n] = hb; + kb[n] = -ha; + } +} + +// One RK4 step of one sample. +function rk4(S, k, h) { + const o = k * NS, { re, im, u, ya, yb, ka, kb, sa, sb } = S, Wd = S.dis; + for (let n = 0; n < NS; n++) { + ya[n] = re[o + n]; + yb[n] = im[o + n]; + } + // k1 + const a0 = re.subarray(o, o + NS), b0 = im.subarray(o, o + NS); + deriv(a0, b0, u, o, Wd, ka, kb); + for (let n = 0; n < NS; n++) { + sa[n] = ka[n]; + sb[n] = kb[n]; + ya[n] = a0[n] + 0.5 * h * ka[n]; + yb[n] = b0[n] + 0.5 * h * kb[n]; + } + // k2 + deriv(ya, yb, u, o, Wd, ka, kb); + for (let n = 0; n < NS; n++) { + sa[n] += 2 * ka[n]; + sb[n] += 2 * kb[n]; + ya[n] = a0[n] + 0.5 * h * ka[n]; + yb[n] = b0[n] + 0.5 * h * kb[n]; + } + // k3 + deriv(ya, yb, u, o, Wd, ka, kb); + for (let n = 0; n < NS; n++) { + sa[n] += 2 * ka[n]; + sb[n] += 2 * kb[n]; + ya[n] = a0[n] + h * ka[n]; + yb[n] = b0[n] + h * kb[n]; + } + // k4 + deriv(ya, yb, u, o, Wd, ka, kb); + for (let n = 0; n < NS; n++) { + a0[n] += (h / 6) * (sa[n] + ka[n]); + b0[n] += (h / 6) * (sb[n] + kb[n]); + } +} + +// Put sample k on the launch site and run it up to the time of the run. +function start(S, k) { + const o = k * NS; + S.re.fill(0, o, o + NS); + S.im.fill(0, o, o + NS); + S.re[o + S.n0] = 1; + const steps = round(S.t / DT); + for (let i = 0; i < steps; i++) rk4(S, k, DT); +} + +// A new run: fresh energies for every sample and a new site, picked so the +// start lies in the middle 40 columns. +function launch(S) { + for (let i = 0; i < MAXS * NS; i++) S.u[i] = random() - 0.5; + S.n0 = OFF + 12 + floor(random() * 40); + S.t = 0; + S.row = 0; + S.prog = 0; + S.hold = 0; + for (let k = 0; k < S.ns; k++) start(S, k); + S.done = S.ns; + // What is left of the run before stays at 40%. + S.dim.fill(0.4); +} + +// Write the row for the present time, the samples' probability averaged. +function writeRow(S) { + const r = S.row, { re, im, rho, vt, cone, u } = S, ns = S.ns, c0 = S.n0 - OFF, edge = 2 * S.t; + let norm = 0; + for (let n = 0; n < NS; n++) norm += re[n] * re[n] + im[n] * im[n]; + S.norm = norm; + for (let x = 0; x < W; x++) { + const n = OFF + x; + let s = 0; + for (let k = 0; k < ns; k++) { + const i = k * NS + n; + s += re[i] * re[i] + im[i] * im[i]; + } + const p = r * W + x; + rho[p] = s / ns; + vt[p] = u[n]; + // The clean chain's light cone, n0 +- 2t, dotted every other row. + const d = abs(abs(x - c0) - edge); + cone[p] = r % 2 === 0 && d < 0.5 && r > 0 ? 1 : 0; + } + S.dim[r] = 1; + S.row++; +} + +// Run the present time forward to row r and write each row on the way. +function advanceTo(S, target) { + while (S.row <= target && S.row < H) { + if (S.row > 0) { + for (let i = 0; i < STEPS; i++) for (let k = 0; k < S.ns; k++) rk4(S, k, DT); + S.t = S.row * TROW; + } + writeRow(S); + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 4, 1], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.dis = cl(k[2], 4, 12); + S.ns = round(cl(k[3], 1, MAXS)); + if (!S.begun) { + // Open on a whole run already written, and a new one starting over it. + launch(S); + advanceTo(S, H - 1); + launch(S); + S.begun = true; + } + // Samples turned up in the middle of a run catch up to its time. + while (S.done < S.ns) start(S, S.done++); + if (S.done > S.ns) S.done = S.ns; + if (input.btnPressed && input.btnPressed[0]) launch(S); + if (S.row >= H) { + S.hold += d * S.speed; + // Hold the finished run 1.5 s at Speed 2. + if (S.hold > 3) launch(S); + return; + } + S.prog += d * S.speed * RATE; + advanceTo(S, floor(S.prog)); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { rho, vt, cone, dim, lo, mid, hi, pos, neg } = S; + const h = S.hue + H0; + // Orange to yellow on the panel goes brown or olive when dim. Round there + // the tails turn toward rose instead of toward yellow, the body is paled + // to peach or butter, and everything is lifted. + const hh = h - floor(h), warm = cl(1 - abs(hh - 0.11) / 0.13, 0, 1); + hsv(h + 0.09 - 0.19 * warm, 1 - 0.2 * warm, 1, lo); + hsv(h, 0.85 - 0.4 * warm, 1, mid); + hsv(h - 0.04, 0.18, 1, hi); + hsv(h + 0.45, 0.7, 1, pos); + hsv(h + 0.3, 0.7, 1, neg); + const vlift = 1 + 0.4 * warm, head = S.row, mixOld = 0.5 + 0.4 * warm; + for (let y = 0; y < H; y++) { + const old = dim[y] < 1, dm = old ? dim[y] + 0.2 * warm : 1; + // Two rows of dark under the head, between the new run and the old. + const gap = old && y >= head && y < head + 2; + for (let x = 0; x < W; x++) { + const p = y * W + x; + let r = 0, g = 0, b = 0; + if (!gap) { + const L = cl((log10(rho[p] + 1e-12) - LMIN) / LSPAN, 0, 1); + if (L > 0) { + const v = min(1, (0.1 + 0.9 * L) * vlift); + let c0 = lo, c1 = mid, f = L * 2; + if (L > 0.5) { + c0 = mid; + c1 = hi; + f = L * 2 - 1; + } + r = (c0[0] + (c1[0] - c0[0]) * f) * v; + g = (c0[1] + (c1[1] - c0[1]) * f) * v; + b = (c0[2] + (c1[2] - c0[2]) * f) * v; + // The run before is drawn half in the tails' colour, so it sits + // back and dims to violet or rose, not to grey or brown. + if (old) { + r += (lo[0] * v - r) * mixOld; + g += (lo[1] * v - g) * mixOld; + b += (lo[2] * v - b) * mixOld; + } + } + // The site energies, faint: high sites one tint, low sites another. + const e = vt[p], c = e > 0 ? pos : neg, a = 0.16 * abs(e) * 2; + r = max(r, c[0] * a); + g = max(g, c[1] * a); + b = max(b, c[2] * a); + if (cone[p]) { + r = max(r, 0.32); + g = max(g, 0.32); + b = max(b, 0.32); + } + r *= dm; + g *= dm; + b *= dm; + } + display.setPixel(x, y, round(255 * min(1, r)), round(255 * min(1, g)), round(255 * min(1, b))); + } + } +} From 1a7f0b5cd6a50693eaf87d38a2e324ff20c1ead8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 20:32:42 +0000 Subject: [PATCH 03/13] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2038=ED=8C=90:=20P=C3=B3lya=20Urn=20=EC=9D=84=20=EB=8D=94?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- incoming/pattern-study/patterns/polya-urn.js | 227 +++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 incoming/pattern-study/patterns/polya-urn.js diff --git a/incoming/pattern-study/patterns/polya-urn.js b/incoming/pattern-study/patterns/polya-urn.js new file mode 100644 index 000000000..0530592ed --- /dev/null +++ b/incoming/pattern-study/patterns/polya-urn.js @@ -0,0 +1,227 @@ +// ===== Patternflow pattern ===== +// Title: Pólya Urn +// Family: data +// Idea: Sixty-four Pólya urns run side by side, one per column, each drawing a ball, putting it back and adding another of the same colour, with the draws running down the panel on a logarithmic clock from none at the top to 2000 at the bottom, so every column swings wildly in the top rows and then freezes into its own mix of the two colours, a barcode whose limits are spread evenly from one colour to the other, and when the run is done the columns sort themselves by where they froze and show that spread as a straight ramp. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Pólya's urn (Florian Eggenberger and George Pólya, 1923), pattern round 38 +// Round: 38 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Balls=1..8, Added=1..8 +// +// Knob 1: Hue. Turns the two colours round the wheel together, a third of the wheel apart. At 0 teal and violet, mixing to lavender blue; at 0.25 violet and orange, mixing to rose; at 0.5 red and lime, mixing to butter yellow; at 0.75 lime and sky blue, mixing to sea green. Every mix is lifted halfway back to full brightness, and one that lands on orange or yellow is lifted and paled further, so nothing goes brown or olive. +// Knob 2: Speed. How fast the run comes down the panel: at 2 one run fills the 128 rows in 8 seconds, at 0.2 in 80, at 3 in 5.3. The clock is the same at every speed, row r holding draw 2001^(r/127) - 1, so the first draw lands on row 12, the tenth on row 40, the hundredth on row 77, and the last rows go by 116 draws at a time. A finished run holds 1.2 seconds, sorts in about 2.5 and shows its ramp for 3.2 before the next run comes down over it. +// Knob 3: Balls. The balls of each colour in every urn at the start, 1 to 8, one ball added per draw at Added 1. At 1 the limits are uniform: the bottom row holds every mix from one colour to the other in equal measure and the sorted ramp runs evenly from violet to teal. At 3 they are Beta(3, 3), 0.5 +- 0.19; at 8 Beta(8, 8), 0.5 +- 0.12: every column settles near the even mix, the barcode goes to one soft lavender and the ramp is flat with short ends. +// Knob 4: Added. Balls of the drawn colour added after each draw, 1 to 8. The limit is Beta(Balls/Added, Balls/Added), so at Balls 1 and Added 8 it is Beta(1/8, 1/8): most urns lock onto one colour within the first ten draws and the barcode goes nearly two-tone, the sorted ramp a step. Balls and Added equal is the same urn as one and one. + +const W = 64, H = 128; +// The clock: row r is draw n(r) = N0 ((1 + NMAX / N0)^(r / 127) - 1), 0 at +// the top and NMAX at the bottom: the first draw lands on row 12, the tenth +// on row 40, the hundredth on row 77. +const NMAX = 2000, N0 = 1; +// One run fills the panel in FILL / Speed seconds; the finished run waits +// HOLD1 seconds, sorts at SORTS transposition rounds a second, then shows the +// ramp for HOLD2 seconds before the next run comes down over it. +const FILL = 16, HOLD1 = 1.2, SORTS = 26, HOLD2 = 3.2; +const { floor, min, max, abs, round, random, pow } = 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]; +} + +// Draw count at each row boundary. +const NAT = new Float64Array(H); +for (let r = 0; r < H; r++) NAT[r] = round(N0 * (pow(1 + NMAX / N0, r / (H - 1)) - 1)); + +export function setup(params) { + params.s = { + init: false, phase: 0, t: 0, front: 0, rounds: 0, still: false, + balls: 1, added: 1, hue: 0, speed: 2, + // Each urn: balls of the first colour, all balls, draws made. + A: new Float64Array(W), T: new Float64Array(W), n: new Float64Array(W), + // Share of the first colour at each row, this run and the run before. + cur: new Float32Array(W * H), old: new Float32Array(W * H), + ca: [0, 0, 0], cb: [0, 0, 0], + }; +} + +function startRun(S) { + for (let i = 0; i < W; i++) { + S.A[i] = S.balls; + S.T[i] = 2 * S.balls; + S.n[i] = 0; + } + S.front = 0; + S.t = 0; + S.phase = 0; + S.rounds = 0; + S.still = false; +} + +// Run every urn on to row r's draw count and write the shares of the rows it +// passes; the row the front stands in shows the share as it is now. +function advance(S, rf) { + const { A, T, n, cur } = S, c = S.added; + const r1 = min(H - 1, floor(rf)); + for (let i = 0; i < W; i++) { + let a = A[i], t = T[i], k = n[i]; + for (let r = S.front; r <= r1; r++) { + const goal = NAT[r]; + while (k < goal) { + if (random() * t < a) a += c; + t += c; + k++; + } + cur[r * W + i] = a / t; + } + // Draws inside the row the front is crossing, toward the next row. + if (r1 < H - 1) { + const goal = NAT[r1] + (NAT[r1 + 1] - NAT[r1]) * (rf - r1); + while (k < goal) { + if (random() * t < a) a += c; + t += c; + k++; + } + cur[(r1 + 1) * W + i] = a / t; + } + A[i] = a; + T[i] = t; + n[i] = k; + } + S.front = r1 + 1; +} + +// One round of odd-even transposition sort on the columns, by the bottom +// row's share; whole columns move, their history with them. +function sortRound(S) { + const { cur } = S, base = (H - 1) * W, odd = S.rounds & 1; + let moved = false; + for (let i = odd; i + 1 < W; i += 2) { + if (cur[base + i] > cur[base + i + 1]) { + for (let r = 0; r < H; r++) { + const p = r * W + i, v = cur[p]; + cur[p] = cur[p + 1]; + cur[p + 1] = v; + } + moved = true; + } + } + S.rounds++; + return moved; +} + +function firstRun(S) { + // The panel opens on a finished run, so the first run comes down over it. + startRun(S); + advance(S, H - 1); + while (sortRound(S) || sortRound(S)); + S.old.set(S.cur); + startRun(S); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 1, 1], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + const balls = round(cl(k[2], 1, 8)), added = round(cl(k[3], 1, 8)); + // A change of Balls or Added starts a fresh run under the new rule. + if (S.init && (balls !== S.balls || added !== S.added)) { + S.balls = balls; + S.added = added; + // What is on the panel now becomes the run the new one comes down over. + if (S.phase === 0) S.old.set(S.cur.subarray(0, S.front * W)); + else S.old.set(S.cur); + startRun(S); + } + S.balls = balls; + S.added = added; + if (!S.init) { + firstRun(S); + S.init = true; + } + S.t += d; + if (S.phase === 0) { + const rf = min(H - 1, (S.t * S.speed * (H - 1)) / FILL); + advance(S, rf); + if (rf >= H - 1) { + S.phase = 1; + S.t = 0; + } + } else if (S.phase === 1) { + if (S.t > HOLD1) { + S.phase = 2; + S.t = 0; + } + } else if (S.phase === 2) { + // Sorting rounds at SORTS a second; two rounds in a row that move + // nothing mean the columns are in order. + while (S.t > 1 / SORTS) { + S.t -= 1 / SORTS; + const moved = sortRound(S); + if (!moved && S.still) { + S.phase = 3; + S.t = 0; + break; + } + S.still = !moved; + } + } else if (S.t > HOLD2) { + S.old.set(S.cur); + startRun(S); + } +} + +function colours(S) { + // The first colour, and the second a third of the wheel on; orange and + // yellow are paled and lifted. + for (const [h, out] of [[S.hue + 0.47, S.ca], [S.hue + 0.8, S.cb]]) { + const hh = h - floor(h), warm = cl(1 - abs(hh - 0.12) / 0.1, 0, 1); + hsv(h, 0.78 - 0.3 * warm, 1, out); + if (warm > 0) { + const lift = 1 + 0.25 * warm; + out[0] = min(1, out[0] * lift); + out[1] = min(1, out[1] * lift); + out[2] = min(1, out[2] * lift); + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.init) return; + colours(S); + const { ca, cb, cur, old } = S; + const fill = S.phase === 0, fr = S.front; + for (let y = 0; y < H; y++) { + const live = !fill || y < fr; + const atFront = fill && y === fr - 1; + for (let x = 0; x < W; x++) { + const p = y * W + x, s = live ? cur[p] : old[p]; + // The mix of the two colours by the share of the first, lifted halfway + // back to full brightness, so two colours either side of yellow mix to + // yellow and not olive. A mix in orange or yellow is lifted and paled + // further, a quarter of the way to white. The run before waits under + // the front at 70%, sorted, so the new barcode reads against its ramp. + let r = s * ca[0] + (1 - s) * cb[0], g = s * ca[1] + (1 - s) * cb[1], b = s * ca[2] + (1 - s) * cb[2]; + const m = max(r, g, b), warm = cl((min(r, g) - b) / m, 0, 1); + const k = ((live ? 0.95 : 0.7) + (live ? 0.05 : 0.3) * warm) * (0.5 + 0.5 / m), pale = 0.25 * warm; + r = r * k + (1 - r * k) * pale; + g = g * k + (1 - g * k) * pale; + b = b * k + (1 - b * k) * pale; + if (atFront) { + r = r * 0.5 + 0.5; + g = g * 0.5 + 0.5; + b = b * 0.5 + 0.5; + } + display.setPixel(x, y, round(255 * min(1, r)), round(255 * min(1, g)), round(255 * min(1, b))); + } + } +} From fe43d5058010ca81fa33ec158c6f6240561d0dba Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 20:32:55 +0000 Subject: [PATCH 04/13] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2038=ED=8C=90:=20Sperner's=20Lemma=20=EB=A5=BC=20=EB=8D=94?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../pattern-study/patterns/sperner-lemma.js | 388 ++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 incoming/pattern-study/patterns/sperner-lemma.js diff --git a/incoming/pattern-study/patterns/sperner-lemma.js b/incoming/pattern-study/patterns/sperner-lemma.js new file mode 100644 index 000000000..6c1b3966e --- /dev/null +++ b/incoming/pattern-study/patterns/sperner-lemma.js @@ -0,0 +1,388 @@ +// ===== Patternflow pattern ===== +// Title: Sperner's Lemma +// Family: topology +// Idea: A tall triangle cut into small triangular rooms whose corners are labelled 1, 2 or 3 (blue, red, green) by which coordinate a slowly moving map of the triangle takes down the most, so each corner keeps its own label and each side only its two; a white walk enters through a door in the base (an edge labelled 1-2), leaves every room by its other door and always ends in a rainbow room that carries all three labels and sits on the map's fixed point. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Sperner's lemma (Emanuel Sperner, 1928) and the Brouwer fixed-point theorem it proves (L. E. J. Brouwer, 1911), pattern round 38 +// Round: 38 +// =============================== +// @matrix 64x128 +// @knobs Mesh=12..28, Speed=0.2..3, Swirl=0..1, Ripple=0..1 +// +// Knob 1: Mesh. Rooms along each side: 12 at the start (144 rooms), 20 at the middle (400), 28 at the top (784). The finer the mesh, the smaller the rainbow room, the closer it sits on the fixed point and the more the walk wanders; at 28 the territories' borders are nearly smooth curves. +// Knob 2: Speed. At 2 the push of the map leans up to 60 degrees either way and back every 21 seconds, so the three territories rock round the fixed point, the fixed point goes round its loop in 27 seconds, and a bright head runs the walk at 36 LEDs a second. At 0.2 all of it ten times slower, at 3 one and a half times faster. +// Knob 3: Swirl. Twists the map round its fixed point more the further out it reaches. At 0 the three territories are wedges meeting at the fixed point; at 0.5 their borders curl about half a turn round it; at 1 most of a turn, and the walk from the base runs along the bottom and round before it climbs in. +// Knob 4: Ripple. Adds a wavy push to the map, which gives it more fixed points. Up to 0.06 none: one fixed point and one rainbow room. At 0.25 three to seven rainbow rooms, at 1 five to eleven, always an odd number; islands of one label float in another, the walk from the base reaches one rainbow room and dim walks join the others two by two. + +const W = 64, H = 128, NMAX = 28, ST = NMAX + 1; +const { floor, min, max, abs, sqrt, sin, cos, atan2, round, hypot, PI } = Math; +const TAU = 2 * PI, R3 = sqrt(3); +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); +}; + +// The big triangle on the panel, pixel centres at x + 0.5: corner 1 bottom +// left, corner 2 bottom right, corner 3 the apex. The base is the 1-2 side. +const P1x = 0.5, P1y = 127.2, P2x = 63.5, P2y = 127.2, P3x = 32, P3y = 0.8; +const E2x = P2x - P1x, E2y = P2y - P1y, E3x = P3x - P1x, E3y = P3y - P1y, DET = E2x * E3y - E3x * E2y; +// Length of the gradient of each barycentric coordinate, in units per LED: +// the distance from a pixel to a mesh line is its offset over this. +const G2 = hypot(E3y, E3x) / abs(DET), G3 = hypot(E2y, E2x) / abs(DET); +const G1 = hypot(E3y - E2y, E2x - E3x) / abs(DET); + +// The map f is defined on an equilateral triangle, corners E1 = (-1, 0), +// E2 = (1, 0), E3 = (0, sqrt 3): f(q) = q + d(q), pulled back into the +// triangle by the nearest point, so f is continuous and maps the triangle +// into itself. d turns round the fixed point c by an angle that grows by +// SWIRL radians per unit of distance at Swirl 1, and is STEP long from R0 +// out, shrinking to nothing at c. +const STEP = 0.12, R0 = 0.35, SWIRL = 3.4; +// The ripple: two waves added to d, up to RIP times STEP. +const RIP = 3.5; + +// Label colours, the territories at FILL of full: 1 azure, 2 rose, 3 green. +const COL = [ + [0.12, 0.42, 1], + [1, 0.18, 0.3], + [0.2, 0.95, 0.28], +]; +const FILL = 0.7; + +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) { + params.s = { + n: 0, speed: 2, swirl: 0, ripple: 0, t: 0, head: 0, begun: false, + lab: new Int8Array(ST * ST), cx: 0, cy: 0, turn: 0, + walks: [], main: null, rainbow: [], seen: new Uint8Array(2 * NMAX * NMAX), + line: new Float32Array(W * H), room: new Int32Array(W * H), R: new Float32Array(W * H), G: new Float32Array(W * H), B: new Float32Array(W * H), + y: [0, 0, 0], hc: [0, 0, 0], + }; +} + +// Vertex (b, c) of an n-mesh has barycentric coordinates (n - b - c, b, c) / n. +const vid = (b, c) => c * ST + b; + +// Rooms: up room (i, j) has corners (i, j), (i + 1, j), (i, j + 1); down room +// (i, j) has corners (i + 1, j), (i, j + 1), (i + 1, j + 1). +const rid = (i, j, down) => 2 * (j * NMAX + i) + down; +function corners(r) { + const down = r & 1, k = r >> 1, i = k % NMAX, j = (k - i) / NMAX; + return down ? [vid(i + 1, j), vid(i, j + 1), vid(i + 1, j + 1)] : [vid(i, j), vid(i + 1, j), vid(i, j + 1)]; +} +// The three walls of a room, each as its two corners and the room behind it +// (-1 for the base, -2 for the other two sides). +function walls(r, n) { + const down = r & 1, k = r >> 1, i = k % NMAX, j = (k - i) / NMAX; + if (down) + return [ + [vid(i + 1, j), vid(i, j + 1), rid(i, j, 0)], + [vid(i, j + 1), vid(i + 1, j + 1), rid(i, j + 1, 0)], + [vid(i + 1, j), vid(i + 1, j + 1), rid(i + 1, j, 0)], + ]; + return [ + [vid(i, j), vid(i + 1, j), j > 0 ? rid(i, j - 1, 1) : -1], + [vid(i, j), vid(i, j + 1), i > 0 ? rid(i - 1, j, 1) : -2], + [vid(i + 1, j), vid(i, j + 1), i + j < n - 1 ? rid(i, j, 1) : -2], + ]; +} + +// The map at barycentric x, written into y as barycentric coordinates. +// Near the sides the turn and the ripple fade out (over the band where the +// smallest coordinate is under EDGE), so there d points at c and q + d stays +// inside: f has no fixed point on the sides, only where d is zero inside. +const EDGE = 0.16; +function f(S, x1, x2, x3, y) { + const mb = min(x1, x2, x3), qx = x2 - x1, qy = R3 * x3, inner = sm(0, EDGE, mb); + const dx = S.cx - qx, dy = S.cy - qy, r = hypot(dx, dy); + let gx = 0, gy = 0; + if (r > 1e-12) { + const psi = (S.turn + S.swirl * SWIRL * r) * inner, m = (STEP * min(r, R0)) / R0 / r, cs = cos(psi), sn = sin(psi); + gx = (dx * cs - dy * sn) * m; + gy = (dx * sn + dy * cs) * m; + } + if (S.ripple > 0) { + const a = S.ripple ** 0.6 * RIP * STEP * sm(0, EDGE / 2, mb), ph = S.t * 0.21; + gx += a * sin(5.3 * qx + 3.1 * qy + ph); + gy += a * sin(-4.4 * qx + 4.9 * qy + 1.9 - 0.8 * ph); + } + let px = qx + gx, py = qy + gy; + // Back into the triangle by the nearest point. + let z3 = py / R3, z2 = (1 - z3 + px) / 2, z1 = (1 - z3 - px) / 2; + if (z1 < 0 || z2 < 0 || z3 < 0) { + let best = Infinity, bx = 0, by = 0; + const E = [-1, 0, 1, 0, 0, R3]; + for (let e = 0; e < 3; e++) { + const ax = E[2 * e], ay = E[2 * e + 1], ex = E[(2 * e + 2) % 6] - ax, ey = E[(2 * e + 3) % 6] - ay; + const t = cl(((px - ax) * ex + (py - ay) * ey) / (ex * ex + ey * ey), 0, 1), sx = ax + t * ex, sy = ay + t * ey; + const d2 = (px - sx) ** 2 + (py - sy) ** 2; + if (d2 < best) { + best = d2; + bx = sx; + by = sy; + } + } + px = bx; + py = by; + z3 = max(0, py / R3); + z2 = max(0, (1 - z3 + px) / 2); + z1 = max(0, 1 - z2 - z3); + } + y[0] = z1; + y[1] = z2; + y[2] = z3; +} + +// Label every vertex: of the coordinates that are not zero, the one f takes +// down the most. Such a one always exists because both sum to 1, and a +// coordinate that is zero is never chosen, so each corner gets its own label +// and each side only its two corners' labels: a Sperner labelling. +function label(S) { + const n = S.n, y = S.y; + for (let c = 0; c <= n; c++) + for (let b = 0; b + c <= n; b++) { + const a = n - b - c, x = [a / n, b / n, c / n]; + f(S, x[0], x[1], x[2], y); + let best = -Infinity, L = 0; + for (let i = 0; i < 3; i++) + if (x[i] > 0 && x[i] - y[i] > best + 1e-12) { + best = x[i] - y[i]; + L = i; + } + S.lab[vid(b, c)] = L; + } +} + +const isDoor = (S, va, vb) => S.lab[va] + S.lab[vb] === 1; +function rainbow(S, r) { + const [a, b, c] = corners(r), L = S.lab; + return L[a] !== L[b] && L[b] !== L[c] && L[a] !== L[c]; +} + +// Walk from room r, having come in through the wall behind which lies +// `from`: leave by the other door each time. Ends in a rainbow room (end = +// room) or out through a door in the base (end = -1). +function walk(S, r, from, pts) { + const n = S.n; + for (let guard = 0; guard < 2 * n * n + 2; guard++) { + S.seen[r] = 1; + const cs = corners(r); + pts.push(centroid(n, cs)); + if (rainbow(S, r)) return r; + const ws = walls(r, n); + let out = null; + for (const w of ws) if (isDoor(S, w[0], w[1]) && w[2] !== from) out = w; + if (!out) return r; + pts.push(mid(n, out[0], out[1])); + if (out[2] < 0) return -1; + from = r; + r = out[2]; + } + return -1; +} + +// Panel position of vertex v of an n-mesh. +function vpos(n, v) { + const b = v % ST, c = (v - b) / ST; + return [P1x + (E2x * b + E3x * c) / n, P1y + (E2y * b + E3y * c) / n]; +} +function centroid(n, cs) { + const a = vpos(n, cs[0]), b = vpos(n, cs[1]), c = vpos(n, cs[2]); + return [(a[0] + b[0] + c[0]) / 3, (a[1] + b[1] + c[1]) / 3]; +} +function mid(n, va, vb) { + const a = vpos(n, va), b = vpos(n, vb); + return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]; +} + +// All the door walks: from each door in the base, then from each rainbow +// room no walk has reached yet. The main walk is the leftmost one from the +// base that ends in a rainbow room; there is always one, because the base +// holds an odd number of doors and walks from the base pair them up two by +// two except where they end in a rainbow room. +function walkAll(S) { + const n = S.n; + S.seen.fill(0); + S.walks = []; + S.main = null; + S.rainbow = []; + for (let i = 0; i < n; i++) { + const r = rid(i, 0, 0); + if (S.seen[r] || !isDoor(S, vid(i, 0), vid(i + 1, 0))) continue; + const pts = [mid(n, vid(i, 0), vid(i + 1, 0))], end = walk(S, r, -1, pts); + const w = { pts, end, base: true }; + S.walks.push(w); + if (end >= 0 && !S.main) S.main = w; + } + for (let j = 0; j < n; j++) + for (let i = 0; i + j < n; i++) + for (let down = 0; down < 2; down++) { + if (down && i + j > n - 2) continue; + const r = rid(i, j, down); + if (!rainbow(S, r)) continue; + S.rainbow.push(r); + if (S.seen[r]) continue; + // Its one door, and on from the room behind it. + S.seen[r] = 1; + const pts = [centroid(n, corners(r))]; + let end = r; + for (const w of walls(r, n)) + if (isDoor(S, w[0], w[1])) { + pts.push(mid(n, w[0], w[1])); + end = w[2] >= 0 ? walk(S, w[2], r, pts) : -1; + } + S.walks.push({ pts, end, base: false }); + } +} + +function lengthOf(pts) { + let L = 0; + for (let k = 1; k < pts.length; k++) L += hypot(pts[k][0] - pts[k - 1][0], pts[k][1] - pts[k - 1][1]); + return L; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [12, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.n = round(cl(k[0], 12, NMAX)); + S.speed = cl(k[1], 0.2, 3); + S.swirl = cl(k[2], 0, 1); + // Up to 0.06 no ripple, so the opening has one fixed point. + S.ripple = max(0, cl(k[3], 0, 1) - 0.06) / 0.94; + if (!S.begun) { + // Open some way into the run, further in the faster it goes, so Speed + // shows from the first second. + S.t = 4 * S.speed; + S.begun = true; + } + S.t += (d * S.speed) / 2; + S.head += d * S.speed * 18; + // The fixed point of the map without ripple wanders inside the triangle, + // its barycentric coordinates never under 0.12, round its loop in 27 + // seconds at Speed 2. + const t = S.t, rr = 0.13 + 0.08 * sin(0.37 * t + 0.6), ph = 0.23 * t + 1.1; + const c1 = 1 / 3 + rr * cos(ph), c2 = 1 / 3 + rr * cos(ph - TAU / 3), c3 = 1 - c1 - c2; + S.cx = c2 - c1; + S.cy = R3 * c3; + // The push leans up to 60 degrees either side of straight at c, back and + // forth every 21 seconds at Speed 2. + S.turn = 1.05 * sin(0.3 * t); + label(S); + walkAll(S); +} + +// Light the line buffer along a polyline, with brightness fn(s) at arc +// length s. +function stroke(S, pts, fn) { + const L = S.line; + let s = 0; + for (let k = 1; k < pts.length; k++) { + const ax = pts[k - 1][0], ay = pts[k - 1][1], bx = pts[k][0], by = pts[k][1]; + const ex = bx - ax, ey = by - ay, len = hypot(ex, ey); + if (len < 1e-6) continue; + const x0 = max(0, floor(min(ax, bx) - 1.5)), x1 = min(W - 1, floor(max(ax, bx) + 1.5)); + const y0 = max(0, floor(min(ay, by) - 1.5)), y1 = min(H - 1, floor(max(ay, by) + 1.5)); + 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 = cl((px * ex + py * ey) / (len * len), 0, 1); + const dd = hypot(px - u * ex, py - u * ey), a = cl(1.25 - dd, 0, 1); + if (a <= 0) continue; + const v = a * fn(s + u * len), p = y * W + x; + if (v > L[p]) L[p] = v; + } + s += len; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.n) return; + const { n, lab, R, G, B, line, hc, room } = S; + line.fill(0); + const isMainEnd = (r) => S.main && S.main.end === r; + const rb = new Set(S.rainbow); + // The territories, the mesh and the rainbow rooms. + const lw = 0.5 + 0.3 * (NMAX - n) / 16; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, X = x + 0.5 - P1x, Y = y + 0.5 - P1y; + const l2 = (X * E3y - Y * E3x) / DET, l3 = (E2x * Y - E2y * X) / DET, l1 = 1 - l2 - l3; + if (l1 < -0.004 || l2 < -0.004 || l3 < -0.004) { + R[p] = G[p] = B[p] = 0; + room[p] = -1; + continue; + } + const u = cl(l2 * n, 0, n - 1e-6), v = cl(l3 * n, 0, n - 1e-6); + let i = floor(u), j = floor(v); + if (i + j > n - 1) j = n - 1 - i; + const fu = u - i, fv = v - j, down = fu + fv > 1 ? 1 : 0; + const r = rid(i, j, down), cs = corners(r); + const w = down ? [fu + fv - 1, 1 - fu, 1 - fv] : [1 - fu - fv, fu, fv]; + // Each corner's share of the room: the part nearer it than the others, + // so every vertex sits in a cell of its own label's colour. + let m = 0; + if (w[1] > w[m]) m = 1; + if (w[2] > w[m]) m = 2; + const c = COL[lab[cs[m]]]; + // Mesh lines: dark walls, about one LED wide. + const a1 = u - i, b1 = v - j, w1 = u + v - floor(u + v); + const dm = min(min(a1, 1 - a1) / (n * G2), min(b1, 1 - b1) / (n * G3), min(w1, 1 - w1) / (n * G1)); + const wl = 1 - sm(0.2, 0.75, dm); + room[p] = r; + if (rb.has(r)) { + // A rainbow room: a spinning wheel of every hue inside white walls. + const ctr = centroid(n, cs), a = atan2(y + 0.5 - ctr[1], x + 0.5 - ctr[0]) / TAU; + hsv(a + S.t * 0.6, 0.85, 1, hc); + const k = isMainEnd(r) ? 0.9 : 0.6; + R[p] = 255 * (hc[0] + (k - hc[0]) * wl); + G[p] = 255 * (hc[1] + (k - hc[1]) * wl); + B[p] = 255 * (hc[2] + (k - hc[2]) * wl); + continue; + } + const wall = 1 - lw * wl; + R[p] = c[0] * FILL * wall * 255; + G[p] = c[1] * FILL * wall * 255; + B[p] = c[2] * FILL * wall * 255; + } + // The other walks dim; the main one brighter, with a head running from + // the base door into the rainbow room and a pause there before it runs again. + // Each walk is drawn from door to door and stops at the wall of a rainbow + // room, so the room's own colours stay clear. + const trim = (w) => w.pts.slice(w.base ? 0 : 1, w.end >= 0 ? w.pts.length - 1 : w.pts.length); + for (const w of S.walks) if (w !== S.main) stroke(S, trim(w), () => 0.42); + if (S.main) { + const pts = trim(S.main), L = lengthOf(pts), cyc = L + 30, hs = S.head % cyc; + stroke(S, pts, (s) => { + const back = hs - s; + return back < 0 ? 0.62 : 0.62 + 0.38 * Math.exp(-back / 9); + }); + // A halo round the room the walk ends in, flaring as the head arrives. + const ctr = centroid(n, corners(S.main.end)), fl = hs > L ? Math.exp(-(hs - L) / 12) : 0; + const rad = 5 + 3 * fl + 60 / n, amp = 0.3 + 0.5 * fl; + for (let y = max(0, floor(ctr[1] - rad)); y <= min(H - 1, floor(ctr[1] + rad)); y++) + for (let x = max(0, floor(ctr[0] - rad)); x <= min(W - 1, floor(ctr[0] + rad)); x++) { + const dd = hypot(x + 0.5 - ctr[0], (y + 0.5 - ctr[1]) * 0.6) / rad; + if (dd >= 1) continue; + const g = amp * (1 - dd) * (1 - dd), p = y * W + x; + if (room[p] < 0 || rb.has(room[p])) continue; + R[p] += (255 - R[p]) * g; + G[p] += (255 - G[p]) * g; + B[p] += (255 - B[p]) * g; + } + } + for (let p = 0; p < W * H; p++) { + const a = line[p]; + const r = R[p] * (1 - a) + 255 * a, g = G[p] * (1 - a) + 255 * a, b = B[p] * (1 - a) + 255 * a; + display.setPixel(p % W, (p / W) | 0, round(min(255, r)), round(min(255, g)), round(min(255, b))); + } +} From 73507e7ac84313a557d148c6a96267af072bf04e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 20:40:46 +0000 Subject: [PATCH 05/13] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2038=ED=8C=90:=20P=C3=B3lya=20Urn,=20Infinity=20Series=20?= =?UTF-8?q?=EB=A5=BC=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../pattern-study/patterns/infinity-series.js | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 incoming/pattern-study/patterns/infinity-series.js diff --git a/incoming/pattern-study/patterns/infinity-series.js b/incoming/pattern-study/patterns/infinity-series.js new file mode 100644 index 000000000..c2dc7ca24 --- /dev/null +++ b/incoming/pattern-study/patterns/infinity-series.js @@ -0,0 +1,189 @@ +// ===== Patternflow pattern ===== +// Title: Infinity Series +// Family: rhythm +// Idea: Per Nørgård's infinity series falls down four piano-roll lanes onto a strike line, the same melody in each at one, two, four and eight notes to the beat, and on every beat the four lanes land together: the four-to-the-beat lane on the very note the one-to-the-beat lane plays, the two- and eight-to-the-beat lanes on its mirror image, because every fourth note of the series plays the series again and every second note plays it upside down. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Per Nørgård's infinity series (1959), pattern round 38 +// Round: 38 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Beat=8..40, Start=0..1 +// +// Knob 1: Hue. Turns the two colours of the notes on the beat round the wheel together: the lanes of 1 and 4, whose notes on the beat are the melody, in one; the lanes of 2 and 8, whose notes on the beat are the melody upside down, in the other. At 0 azure and rose; at 0.25 magenta and butter yellow; at 0.5 orange and mint; at 0.75 green and violet. The notes between the beats are the lane's colour paled 55% toward white. +// Knob 2: Speed. The tempo, 60 times Speed beats a minute: 120 at 2, so the lane of 8 plays 16 notes a second and the panel scrolls 16 rows a second at Beat 8; 12 at 0.2, 180 at 3. After 512 beats the roll goes back to Start. +// Knob 3: Beat. Rows to a beat, 8 to 40 in steps of 8. At 8 thirteen beats stand above the strike line, lane 1's notes are 7 rows long and the lane of 8's one row; at 16 six and a half beats; at 40 two and a half, lane 1's notes 39 rows long and the lane of 8's five. +// Knob 4: Start. Where in the series the roll starts. Up to 0.06 at note 0, so it opens on 0, 1, -1, 2, 1, 0, -2, 3; above that at beat 16 to beat 496 in steps of 16. Further in, the melody wanders further from the middle: at most one note in 30 lies past seven steps, and it is drawn dimmer on the lane's edge. + +const W = 64, H = 128; +// Lanes: four, 16 columns each; pitches -7..7 on columns 0..14 of the lane, +// pitch 0 on column 7, column 15 dark between lanes. +const LANES = 4, LW = 16, PMAX = 7, RATE = [1, 2, 4, 8]; +// The strike line: notes fall onto row SY and fade out below it. +const SY = 104; +// Brightness of the held note on the beat behind the faster lanes. +const HOLD = 0.34; +const NA = 1 << 16; +const { floor, min, max, abs, round, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The series: a(0) = 0, a(2n) = -a(n), a(2n + 1) = a(n) + 1. +const A = new Int8Array(NA); +for (let n = 1; n < NA; n++) A[n] = n & 1 ? A[n >> 1] + 1 : -A[n >> 1]; +const ser = (n) => A[((n % NA) + NA) % NA]; + +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) { + params.s = { + init: false, beat: 0, start: 0, speed: 2, rpb: 8, hue: 0, + R: new Float32Array(W * H), G: new Float32Array(W * H), B: new Float32Array(W * H), + up: [0, 0, 0], dn: [0, 0, 0], upOff: [0, 0, 0], dnOff: [0, 0, 0], upHold: [0, 0, 0], dnHold: [0, 0, 0], warm: [0, 0], + }; +} + +function startBeat(k) { + // Up to 0.06 note 0; above, beat 16 to 496 in steps of 16. + const u = cl(k, 0, 1); + return u <= 0.06 ? 0 : 16 * max(1, round(((u - 0.06) / 0.94) * 31)); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 8, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.rpb = 8 * cl(round(cl(k[2], 8, 40) / 8), 1, 5); + const st = startBeat(k[3]); + // The roll opens with the start beat 1.5 beats above the strike line; a + // new Start jumps there. + if (!S.init || st !== S.start) { + S.start = st; + S.beat = st - 1.5; + S.init = true; + } + S.beat += d * S.speed; + // After 512 beats the roll goes back to its start, so the fastest lane + // reads no further than note 8 x 1008 = 8064, and no more than one note in + // 30 lies past seven steps from the middle. + if (S.beat > S.start + 512) S.beat -= 512 + 1.5; +} + +// Adds light to one LED; the dark column between lanes stays dark. +function add(S, x, y, c, v) { + if (x < 0 || x >= W || y < 0 || y >= H || v <= 0 || (x & 15) === 15) return; + const p = y * W + x; + S.R[p] += c[0] * v; + S.G[p] += c[1] * v; + S.B[p] += c[2] * v; +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.init) return; + const { R, G, B, up, dn, upOff, dnOff, upHold, dnHold, warm } = S; + R.fill(0); + G.fill(0); + B.fill(0); + // The melody as it is, the melody upside down, and the notes between beats. + hsv(S.hue + 0.57, 0.8, 1, up); + hsv(S.hue + 0.93, 0.62, 1, dn); + // How far each colour is into orange or yellow, 0 to 1. Those are lifted + // toward white so they read amber or butter, not brown or olive, most of + // all in the dim held notes. + [up, dn].forEach((c, i) => { + const w = cl(2.5 * (min(c[0], c[1]) - c[2]), 0, 1); + warm[i] = w; + for (let j = 0; j < 3; j++) c[j] = c[j] + (1 - c[j]) * 0.3 * w; + }); + for (let j = 0; j < 3; j++) { + // The notes between the beats: the lane's colour taken 55% toward white. + upOff[j] = 0.55 + 0.45 * up[j]; + dnOff[j] = 0.55 + 0.45 * dn[j]; + // The held notes: the colour itself, or up to 45% toward white and 1.6 + // times as bright in orange and yellow. + upHold[j] = (up[j] + (1 - up[j]) * 0.45 * warm[0]) * HOLD * (1 + 0.6 * warm[0]); + dnHold[j] = (dn[j] + (1 - dn[j]) * 0.45 * warm[1]) * HOLD * (1 + 0.6 * warm[1]); + } + const T = S.beat, rpb = S.rpb, white = [1, 1, 1]; + + // Guides: each lane's pitch 0, the mirror line, and a line on every beat. + const grey = [0.55, 0.6, 0.75]; + for (let l = 0; l < LANES; l++) for (let y = 0; y < H; y++) add(S, l * LW + PMAX, y, grey, 0.1); + const b0 = floor(T - (H - SY) / rpb) - 1, b1 = floor(T + SY / rpb) + 1; + for (let b = b0; b <= b1; b++) { + const y = round(SY - (b - T) * rpb); + for (let x = 0; x < W; x++) if ((x & 15) !== 15) add(S, x, y, grey, 0.13); + } + + for (let l = 0; l < LANES; l++) { + const s = RATE[l], x0 = l * LW, len = rpb / s; + // Notes on the beat in this lane play +a(m) (rates 1, 4) or -a(m) (2, 8). + const onC = l & 1 ? dn : up, offC = l & 1 ? dnOff : upOff, holdC = l & 1 ? dnHold : upHold, lw = warm[l & 1]; + // Behind the lane's own notes, each note on the beat is held dim for the + // whole beat: the series read at the lane's step, which lands on lane + // 1's note in the lanes of 4 and on its mirror image in the lanes of 2 + // and 8. + if (s > 1) + for (let m = max(0, b0); m <= b1; m++) { + const a = ser(s * m), px = x0 + PMAX + cl(a, -PMAX, PMAX), yb = SY - (m - T) * rpb; + const ye = round(yb); + for (let y = ye - rpb + 1; y < ye; y++) { + if (y < 0 || y >= H) continue; + const fade = y > SY ? 1 - (0.85 * (y - SY)) / (H - SY) : 1; + add(S, px, y, holdC, fade); + add(S, px - 1, y, holdC, 0.75 * fade); + add(S, px + 1, y, holdC, 0.75 * fade); + } + } + const n0 = floor((T - (H - SY) / rpb) * s) - 1, n1 = floor((T + SY / rpb) * s) + 1; + for (let n = max(0, n0); n <= n1; n++) { + const a = ser(n), px = x0 + PMAX + cl(a, -PMAX, PMAX), on = n % s === 0; + // Rows the note covers: its onset at the bottom, the next onset at the top. + const yb = SY - (n / s - T) * rpb, yt = yb - len; + const gap = len >= 3 ? 1 : 0; + const c = on ? onC : offC, core = on ? 0.95 : 0.42 * (1 + 0.4 * lw), side = on ? 0.7 : 0.2 * (1 + 0.4 * lw); + const clip = abs(a) > PMAX ? 0.55 : 1; + // How long since this note struck, in beats: a flash that dies in about + // a sixth of a beat, longer for a downbeat. + const age = T - n / s, flash = age >= 0 && age < 1 ? exp(-age * (on ? 9 : 16)) : 0; + // Rows are rounded at both ends, so a note of one row never drops out. + const ye = round(yb); + for (let y = round(yt) + gap; y < ye; y++) { + if (y < 0 || y >= H) continue; + // Below the strike line the note has sounded and fades out. + const fade = y > SY ? 1 - (0.85 * (y - SY)) / (H - SY) : 1; + const lit = (y >= SY - 2 ? 1 + 1.6 * flash : 1) * fade * clip; + add(S, px, y, c, core * lit); + add(S, px - 1, y, c, side * lit); + add(S, px + 1, y, c, side * lit); + if (y >= SY - 2 && flash > 0) add(S, px, y, white, 0.5 * flash * fade); + } + // The struck key on the strike line, and its glow on either side. + if (flash > 0) { + for (let dx = -2; dx <= 2; dx++) { + const g = (on ? 1 : 0.5) * flash * (dx === 0 ? 1 : abs(dx) === 1 ? 0.55 : 0.2); + add(S, px + dx, SY, white, 0.5 * g); + add(S, px + dx, SY, c, 0.8 * g); + add(S, px + dx, SY + 1, c, 0.35 * g); + } + } + } + } + + // The strike line itself, a dim rule across each lane. + for (let x = 0; x < W; x++) if ((x & 15) !== 15) add(S, x, SY, grey, 0.16); + + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + display.setPixel(x, y, round(255 * min(1, R[p])), round(255 * min(1, G[p])), round(255 * min(1, B[p]))); + } +} From 5884bf8940bb1b188d12fc9621ccaea5ea5a73ba Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 20:41:42 +0000 Subject: [PATCH 06/13] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2038=ED=8C=90:=20Bernoulli=20Convolutions=20=EB=A5=BC=20?= =?UTF-8?q?=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/bernoulli-convolutions.js | 312 ++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 incoming/pattern-study/patterns/bernoulli-convolutions.js diff --git a/incoming/pattern-study/patterns/bernoulli-convolutions.js b/incoming/pattern-study/patterns/bernoulli-convolutions.js new file mode 100644 index 000000000..dbd7ed9a4 --- /dev/null +++ b/incoming/pattern-study/patterns/bernoulli-convolutions.js @@ -0,0 +1,312 @@ +// ===== Patternflow pattern ===== +// Title: Bernoulli Convolutions +// Family: fractal +// Idea: Each of the 128 rows is the distribution of the random sum ±1 ±λ ±λ² ±… for its own λ, spread across the panel over the sum's whole range, from Cantor dust at λ = 0.32 at the top through a flat row near 1/2 to a smooth bell at 0.89 at the bottom; the sum is built up a term at a time, each term splitting every point of a row in two, and on the 14 rows where λ is the reciprocal of a Pisot number, the golden ratio's 0.618 among them, the splits keep landing on points already there, so those rows burn white-gold, brighter with every term, and brightest at the plastic number's 0.755 where each point holds 36 sign choices on average after 13 terms. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Bernoulli convolutions (Børge Jessen and Aurel Wintner, 1935; Paul Erdős, 1939; Boris Solomyak, 1995), pattern round 38 +// Round: 38 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Bias=0.5..0.95, Zoom=0..1 +// +// Knob 1: Hue. Turns the rows round the wheel; the Pisot seams stay white-gold, and go nearly white when the rows are warm. At 0 thin parts of a row are violet and thick parts azure; at 0.25 rose and orchid, at 0.5 butter and peach, at 0.75 teal and green. Orange and yellow are lifted and paled so they do not go brown or olive. +// Knob 2: Speed. At 2 a new term comes in every second, each split taking the first two thirds of it: the climb from 6 terms to 13 takes 7 seconds, the 13-term picture holds 2.5, and the terms are taken away again three a second. At 0.2 all of it ten times slower, at 3 one and a half times faster. The pattern opens on the 13-term picture held for 1.2 seconds at any speed. +// Knob 3: Bias. The chance that a sign is plus. At 0.5 a fair coin and every row symmetric; at 0.7 the mass leans right and the bells slide toward the right edge; at 0.95 the bottom bells are narrow bright lumps against the right edge and the dust at the top is a few hot points. The short white ticks at both edges mark λ = p^p (1-p)^(1-p): every row above them is singular, because the signs carry too little entropy to fill the interval. They sit at λ = 0.5 for a fair coin and move down to 0.82 at 0.95. +// Knob 4: Zoom. The range of λ down the rows. At 0 from 0.30 to 0.90, with 14 Pisot seams between rows 46 and 96; at 1 from 0.56 to 0.78, where 11 of them spread over the whole panel and the rows between are 0.0017 apart. +// Buttons: K1 tap takes the terms away down to none, one point in the middle of every row, and builds the sum up again from its first term. + +const W = 64, H = 128, M = 512, F = M / W; +// Terms: the cycle falls to NLO and climbs to NHI; stages 0..NHI are kept. +const NLO = 6, NHI = 13; +// Seconds at Speed 2: hold at NHI, per term falling, rest at NLO, per term +// climbing. The first hold, the picture the pattern opens on, lasts OPEN +// seconds at any speed. +const HOLD = 2.5, TF = 0.33, REST = 0.6, TC = 1.0, OPEN = 1.2; +// Opening hue: violet and azure, not red. +const H0 = 0.62; +// Pisot numbers in (1, 2), each the largest root of its polynomial (highest +// power first). Their reciprocals are the lambdas where Erdos (1939) showed +// the measure singular: the sums land on each other again and again. +const PISOT = [ + [1, 0, -1, -1], // 1.3247, the plastic number + [1, -1, 0, 0, -1], // 1.3803 + [1, -1, -1, 1, 0, -1], // 1.4433 + [1, -1, 0, -1], // 1.4656 + [1, -1, -1, 0, 1, 0, -1], // 1.5016 + [1, 0, -1, -1, -1, -1], // 1.5342 + [1, -1, -1, 0, 0, 1, 0, -1], // 1.5452 + [1, -1, 0, -1, 0, -1], // 1.5701 + [1, -1, -1], // 1.6180, the golden ratio + [1, -1, -1, 0, 0, -1], // 1.7049 + [1, -2, 1, -1], // 1.7549 + [1, -1, -1, -1], // 1.8393, the tribonacci constant + [1, -2, 0, 1, -1], // 1.8668 + [1, -1, -1, -1, -1], // 1.9276 +]; +const { floor, min, abs, exp, log2, round } = 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, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = [v, q, p, p, t, v][i]; + out[o + 1] = [t, v, v, q, p, p][i]; + out[o + 2] = [p, p, t, v, v, q][i]; +} + +// Newton from 2 down to the largest real root. +function rootOf(c) { + let x = 2; + for (let it = 0; it < 60; it++) { + let f = 0, d = 0; + for (let k = 0; k < c.length; k++) { + d = d * x + f; + f = f * x + c[k]; + } + x -= f / d; + } + return x; +} +const LAMP = PISOT.map((c) => 1 / rootOf(c)); + +export function setup(params) { + params.s = { + hue: 0, speed: 2, bias: 0.5, zoom: 0.06, built: false, bBias: -1, bZoom: -1, since: 0, + lam: new Float64Array(H), pis: new Int8Array(H).fill(-1), + h: new Float64Array(M), h2: new Float64Array(M + 1), + // Per stage, row and column: the mass of the row in the column, and + // the log2 of how many sign choices share a point there on average. + I: new Float32Array((NHI + 1) * H * W), G: new Float32Array((NHI + 1) * H * W), + phase: 0, pt: 0, lo: NLO, x: NHI, open: OPEN, + lut: new Float32Array(256 * 3), row: new Float32Array(W), gold: [1, 0.86, 0.56], + }; +} + +// The lambdas of the rows, the nearest row to each Pisot reciprocal moved +// onto it exactly. Every other row's lambda is a binary fraction, and for +// those no two sign choices ever give the same sum: the highest power that +// differs leaves an odd numerator no lower power can cancel. +function rows(S) { + const lo = 0.3 + 0.26 * S.zoom, hi = 0.9 - 0.12 * S.zoom, dl = (hi - lo) / (H - 1); + S.lo0 = lo; + S.hi0 = hi; + S.pis.fill(-1); + for (let r = 0; r < H; r++) S.lam[r] = lo + dl * r; + LAMP.forEach((l, k) => { + const r = round((l - lo) / dl); + if (r >= 0 && r < H && abs(S.lam[r] - l) <= dl / 2 + 1e-12) { + S.lam[r] = l; + S.pis[r] = k; + } + }); +} + +// Every stage of every row. The fine histogram (M bins over the row's +// range, u = x (1 - lambda) in [-1, 1]) starts as a point at 0 and is +// pushed through nu -> p S+ nu + (1 - p) S- nu, S+- u = lambda u +- (1 - lambda), +// once per term. On a Pisot row the sums are also kept exactly, as sorted +// points with their mass, and points closer than 1e-9 are one point. +function build(S) { + rows(S); + const p = S.bias, q = 1 - p, { h, h2, I, G } = S, c0 = M / 2 - 0.5; + G.fill(0); + for (let r = 0; r < H; r++) { + const lam = S.lam[r], sh = (1 - lam) * (M / 2); + h.fill(0); + h[M / 2 - 1] = 0.5; + h[M / 2] = 0.5; + for (let n = 0; n <= NHI; n++) { + const o = (n * H + r) * W; + for (let c = 0; c < W; c++) { + let m = 0; + for (let i = c * F; i < c * F + F; i++) m += h[i]; + I[o + c] = m; + } + if (n === NHI) break; + h2.fill(0); + for (let i = 0; i < M; i++) { + const v = h[i]; + if (v === 0) continue; + const base = c0 + lam * (i - c0); + for (let sgn = 0; sgn < 2; sgn++) { + const f = base + (sgn ? sh : -sh), w = v * (sgn ? p : q), j = floor(f), fr = f - j; + if (j >= 0 && j < M) h2[j] += w * (1 - fr); + if (j + 1 >= 0 && j + 1 < M) h2[j + 1] += w * fr; + } + } + for (let i = 0; i < M; i++) h[i] = h2[i]; + } + if (S.pis[r] >= 0) exact(S, r, p); + } + S.built = true; + S.bBias = S.bias; + S.bZoom = S.zoom; + S.since = 0; +} + +// The exact sums on a Pisot row, term by term in the order they shrink: +// each point x splits into x + lambda^n (mass p) and x - lambda^n (mass 1 - p). +// m is a point's mass; w the sum of its sign choices' squared masses, so +// m^2 / w is how many choices it holds, weighted, and 1 when it holds one. +function exact(S, r, p) { + const lam = S.lam[r], q = 1 - p, G = S.G, k1 = 1 - lam; + let X = new Float64Array([0]), Mm = new Float64Array([1]), Q = new Float64Array([1]); + const sm2 = new Float64Array(W), sq = new Float64Array(W); + for (let n = 1; n <= NHI; n++) { + const a = lam ** (n - 1), L = X.length; + const X2 = new Float64Array(2 * L), M2 = new Float64Array(2 * L), Q2 = new Float64Array(2 * L); + let i = 0, j = 0, k = -1; + while (i < L || j < L) { + // Merge the two shifted copies, both already sorted. + let x, m, w; + if (j >= L || (i < L && X[i] - a <= X[j] + a)) { + x = X[i] - a; + m = Mm[i] * q; + w = Q[i] * q * q; + i++; + } else { + x = X[j] + a; + m = Mm[j] * p; + w = Q[j] * p * p; + j++; + } + if (k >= 0 && x - X2[k] < 1e-9) { + M2[k] += m; + Q2[k] += w; + } else { + k++; + X2[k] = x; + M2[k] = m; + Q2[k] = w; + } + } + X = X2.subarray(0, k + 1); + Mm = M2.subarray(0, k + 1); + Q = Q2.subarray(0, k + 1); + sm2.fill(0); + sq.fill(0); + for (let t = 0; t <= k; t++) { + const c = cl(floor((X[t] * k1 + 1) * (W / 2)), 0, W - 1); + sm2[c] += Mm[t] * Mm[t]; + sq[c] += Q[t]; + } + const o = (n * H + r) * W; + for (let c = 0; c < W; c++) G[o + c] = sq[c] > 0 ? log2(sm2[c] / sq[c]) : 0; + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0.5, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.bias = cl(k[2], 0.5, 0.95); + S.zoom = cl(k[3], 0, 1); + S.since += d; + // Rebuilding takes a few milliseconds: at most ten times a second while a knob turns. + if (!S.built || ((abs(S.bias - S.bBias) > 1e-4 || abs(S.zoom - S.bZoom) > 1e-4) && S.since > 0.1)) build(S); + if (input.btnPressed && input.btnPressed[0]) { + S.lo = 0; + if (S.phase !== 1) { + S.phase = 1; + S.pt = (NHI - S.x) * TF; + } + } + // The term count x runs hold, fall, rest, climb. + if (S.open > 0) { + // The opening hold, in plain seconds; it ends straight into the fall. + S.open -= d; + if (S.open <= 0) S.pt = HOLD; + } else S.pt += (d * S.speed) / 2; + const span = NHI - S.lo; + if (S.phase === 0 && S.pt >= HOLD) { + S.phase = 1; + S.pt -= HOLD; + } + if (S.phase === 1 && S.pt >= span * TF) { + S.phase = 2; + S.pt -= span * TF; + } + if (S.phase === 2 && S.pt >= REST) { + S.phase = 3; + S.pt -= REST; + } + if (S.phase === 3 && S.pt >= span * TC) { + S.phase = 0; + S.pt -= span * TC; + S.lo = NLO; + } + if (S.phase === 0) S.x = NHI; + else if (S.phase === 1) { + const u = S.pt / TF, j = floor(u); + S.x = NHI - j - sm(0, 1, u - j); + } else if (S.phase === 2) S.x = S.lo; + else { + const u = S.pt / TC, j = floor(u); + S.x = S.lo + j + sm(0, 0.65, u - j); + } + S.x = cl(S.x, 0, NHI); +} + +// Density levels: violet where a row is thin, azure where it is thick. A +// hue that comes round to orange or yellow is paled and lifted, so thin +// parts read peach or butter and not brown or olive. +function palette(S) { + const lut = S.lut; + for (let i = 0; i < 256; i++) { + const L = i / 255, h = S.hue + H0 + 0.16 * (1 - L) - 0.06, hh = h - floor(h); + const warm = cl(1 - abs(hh - 0.13) / 0.13, 0, 1); + hsv(h, 0.9 - 0.45 * L - 0.3 * warm, min(1, 0.92 * L ** (0.7 - 0.2 * warm) * (1 + 0.35 * warm)), lut, i * 3); + } + // The seams: white-gold, nearly white where the rows themselves are warm. + const hh = S.hue + H0 - 0.06 - floor(S.hue + H0 - 0.06), warm = cl(1 - abs(hh - 0.13) / 0.2, 0, 1); + S.gold[0] = 1; + S.gold[1] = 0.86 + 0.12 * warm; + S.gold[2] = 0.56 + 0.36 * warm; +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.built) return; + palette(S); + const { I, G, lut, row } = S, p = S.bias, q = 1 - p; + const n = min(NHI - 1, floor(S.x)), s = S.x - n; + // The new term splits every point by +- (1 - lambda) lambda^n of the range, + // and in the last third of the split the exact next stage takes over. + const wNext = sm(0.55, 1, s); + const gold = S.gold; + // The entropy bound p^p (1 - p)^(1 - p), as a row. + const lstar = p ** p * q ** q, rstar = round(((lstar - S.lo0) / (S.hi0 - S.lo0)) * (H - 1)); + for (let r = 0; r < H; r++) { + const lam = S.lam[r], dsh = s * (1 - lam) * lam ** n * (W / 2); + const o0 = (n * H + r) * W, o1 = o0 + H * W; + for (let c = 0; c < W; c++) { + // p of the row moved right by dsh, 1 - p moved left. + const a = c - dsh, b = c + dsh, ia = floor(a), ib = floor(b), fa = a - ia, fb = b - ib; + const va = (ia >= 0 && ia < W ? I[o0 + ia] * (1 - fa) : 0) + (ia + 1 >= 0 && ia + 1 < W ? I[o0 + ia + 1] * fa : 0); + const vb = (ib >= 0 && ib < W ? I[o0 + ib] * (1 - fb) : 0) + (ib + 1 >= 0 && ib + 1 < W ? I[o0 + ib + 1] * fb : 0); + row[c] = (p * va + q * vb) * (1 - wNext) + I[o1 + c] * wNext; + } + const seam = S.pis[r] >= 0; + for (let c = 0; c < W; c++) { + // 64 times the mass is 1 where the row is flat; 1 - e^(-rho / 1.2) + // keeps thin rows readable and the dust and the bells from burning out. + const L = 1 - exp((-W * row[c]) / 1.2), li = round(L * 255) * 3; + let R = lut[li], Gc = lut[li + 1], B = lut[li + 2]; + if (seam) { + const g = G[o0 + c] * (1 - s) + G[o1 + c] * s, k = sm(0.2, 5.5, g) * (0.35 + 0.65 * sm(0, 0.5, L)); + R += (gold[0] - R) * k; + Gc += (gold[1] - Gc) * k; + B += (gold[2] - B) * k; + } + if (r === rstar && (c < 2 || c > W - 3)) R = Gc = B = 0.85; + display.setPixel(c, r, round(255 * min(1, R)), round(255 * min(1, Gc)), round(255 * min(1, B))); + } + } +} From 5833345ac764936b09de07bf7f4d040540f13beb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 20:42:17 +0000 Subject: [PATCH 07/13] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2038=ED=8C=90:=20Anderson=20Localization,=20Milk=20Ring=20?= =?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/anderson-localization.js | 6 +- incoming/pattern-study/patterns/milk-ring.js | 269 ++++++++++++++++++ 2 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 incoming/pattern-study/patterns/milk-ring.js diff --git a/incoming/pattern-study/patterns/anderson-localization.js b/incoming/pattern-study/patterns/anderson-localization.js index 45e5fe9c9..aa676039f 100644 --- a/incoming/pattern-study/patterns/anderson-localization.js +++ b/incoming/pattern-study/patterns/anderson-localization.js @@ -14,7 +14,7 @@ // Knob 1: Hue. Turns the wave round the wheel; the old run below the head and the stripes turn with it. At 0 tails violet, body azure, core pale cyan; at 0.1 blue, at 0.2 violet, at 0.35 pink, at 0.42 red, at 0.5 peach, at 0.58 butter, at 0.66 lime, at 0.85 mint. From 0.42 to 0.68 the tails turn toward rose instead of yellow and the body is paled and lifted up to 1.4 times, so nothing reads brown or olive. // Knob 2: Speed. How fast time is written down the panel. At 2 twenty rows a second, so a run of 128 rows (t = 0 to 32 in units of the hopping) takes 6.4 s and the finished run holds 1.5 s; at 0.2 64 s, at 3 4.3 s. // Knob 3: Disorder. The width W of the random site energies, uniform in [-W/2, W/2]. At 4 the localization length at the band centre is about 105/W^2 = 6.6 sites: the lit column, where the probability is over 1/1000, is 13 LEDs wide at row 8, 19 at row 16, 25 at row 32 and stops at 27 to 28 from row 48 on, where a clean cone would be 49 wide and growing. At 8 it stops at 12 LEDs by row 16; at 12 the particle hardly leaves its site and the column is 8 LEDs wide from row 8. -// Knob 4: Samples. How many disorder samples run side by side from the same site, their probabilities averaged. At 1 the speckle of one sample, braided fringes and dark holes where its localized states beat; from about 8 the holes fill in, and at 16 the column is a smooth bell that falls off exponentially on both sides and stops widening. +// Knob 4: Samples. How many disorder samples run side by side from the same site, their probabilities averaged. At 1 the speckle of one sample, braided fringes and dark holes where its localized states beat; from about 8 the holes fill in, and at 16 the column is a smooth bell that falls off exponentially on both sides and stops widening. The stripes behind show the first sample's energies at 1/Samples of their strength, so they fade as the average takes over. // Buttons: K1 tap launches now, on a new sample of energies and a new site. const W = 64, H = 128, NS = 192, OFF = 64, MAXS = 16; @@ -151,7 +151,9 @@ function writeRow(S) { } const p = r * W + x; rho[p] = s / ns; - vt[p] = u[n]; + // The stripes show the first sample's energies, fainter as more samples + // are averaged, since the average has no one landscape. + vt[p] = u[n] / ns; // The clean chain's light cone, n0 +- 2t, dotted every other row. const d = abs(abs(x - c0) - edge); cone[p] = r % 2 === 0 && d < 0.5 && r > 0 ? 1 : 0; diff --git a/incoming/pattern-study/patterns/milk-ring.js b/incoming/pattern-study/patterns/milk-ring.js new file mode 100644 index 000000000..a8c54aed2 --- /dev/null +++ b/incoming/pattern-study/patterns/milk-ring.js @@ -0,0 +1,269 @@ +// ===== Patternflow pattern ===== +// Title: Milk Ring +// Family: cosmetic +// Idea: Drops of milky cream fall one at a time through the surface of a tall glass of tinted serum, each rolls up into a vortex ring with a bright rim that sinks, slows and swells until its rim breaks into a few smaller drops, and each of those rolls up into a smaller ring and sinks further, so a branching tree of rings hangs down the panel on thin cream threads. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 38, after drops of cream let fall into clear serum, the vortex rings they form and the cascade in which each ring breaks into smaller rings (J. J. Thomson and H. F. Newall, 1885) +// Round: 38 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Drops=3..6, Gel=1..5 +// +// Knob 1: Hue. Turns the serum round the whole wheel; the cream stays milk white, tinted with the serum where it is thin. At 0 rose, at 0.05 crimson, 0.1 coral, 0.15 apricot, 0.2 butter, 0.25 pale lime, 0.35 green, 0.5 teal, 0.6 sky blue, 0.75 indigo, 0.85 violet. From 0.03 to 0.37 the bottom of the glass is lifted up to 2.1 times and paled a little, so orange and yellow serum never reads brown or olive. +// Knob 2: Speed. At 2 a drop falls every 7 s; its ring reaches its first break 3.2 s after it goes in, the second 2.8 s later, and the last rings melt into the serum 4 to 7 s after that, so two trees always share the glass. At 0.2 all of it ten times slower, at 3 one and a half times faster. +// Knob 3: Drops. How many drops each ring breaks into, spaced evenly round its rim and thrown a little outward. At 3 a tree of 1, 3 and 9 rings, at most 13 in the glass; at 6 1, 6 and 36, up to 43, and the bottom third fills with small rings. At Gel 1 the first ring swells from 2.4 to 9 LEDs across its radius, an ellipse 18 wide and 7 tall, and breaks about row 47; its rings swell to 4.6 and break about row 76; the last swell to 2.6 and are gone by about row 117. +// Knob 4: Gel. How thick the serum is. At 1 the threads fade with a time constant of 4 s. Turned up, everything runs slower, 2.6 times at 5; the rings stay smaller, 0.72 of their size at 5, so the tree is narrower; and the threads hold longer, about 50 s at 5, so the threads of the last three drops hang together as a curtain. +// Buttons: K1 tap lets a drop fall now; the next comes 7 s after it at Speed 2. + +const W = 64, H = 128, N = W * H; +// The serum's surface, rows above it are the air the drop falls through. +const SURF = 5; +// Seen from a little above: a ring of radius R is an ellipse R wide and +// R * TILT tall, its near half lower on the panel. +const TILT = 0.4; +// Per generation: starting radius, the radius it swells to, starting sink +// speed in LEDs a second, how long it sinks before its rim breaks. +const GEN = [ + { r0: 2.4, r1: 10, v0: 24, brk: 3.2 }, + { r0: 1.4, r1: 5.2, v0: 18, brk: 2.8 }, + { r0: 0.9, r1: 2.8, v0: 14, brk: 99 }, +]; +// Every sink speed falls toward VS with time constant TV; radii swell with TR. +const VS = 3.2, TV = 1.3, TR = 1.6; +// Seconds between drops at Speed 2 and Gel 1, and the run before the first frame. +const PERIOD = 7, PREROLL = 9; +// Opening hue: rose. +const H0 = 0.93; +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]; +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, n: 3, gel: 1, begun: false, t: 0, next: 0, tk: 1, + rings: [], falling: [], ripples: [], + D: new Float32Array(N), F: new Float32Array(N), T: new Float32Array(N), G: new Float32Array(N), tmp: new Float32Array(N), + deep: [0, 0, 0], light: [0, 0, 0], cream: [0, 0, 0], + }; +} + +// Soft dot of cream into a field, a Gaussian of width s. +function splat(A, x, y, s, w) { + const r = Math.ceil(2.2 * s), xi = round(x), yi = round(y), k = -0.5 / (s * s); + for (let j = max(0, yi - r); j <= min(H - 1, yi + r); j++) { + const dy = j - y, ey = dy * dy; + for (let i = max(0, xi - r); i <= min(W - 1, xi + r); i++) { + const dx = i - x; + A[j * W + i] += w * exp((dx * dx + ey) * k); + } + } +} + +// A thin thread point, bilinear into the trail. +function dab(T, x, y, w) { + const i = floor(x), j = floor(y), fx = x - i, fy = y - j; + for (let b = 0; b < 2; b++) + for (let a = 0; a < 2; a++) { + const xx = i + a, yy = j + b; + if (xx < 0 || yy < 0 || xx >= W || yy >= H) continue; + T[yy * W + xx] += w * (a ? fx : 1 - fx) * (b ? fy : 1 - fy); + } +} + +function newRing(S, g, x, y, z, vx, vz) { + const G0 = GEN[g]; + S.rings.push({ g, x, y, z, vx, vz, age: 0, R: G0.r0, amp: 1, broken: false, spin: random() * 2 * PI, v0: G0.v0 * (0.85 + 0.3 * random()) }); +} + +function drop(S) { + S.falling.push({ x: 18 + random() * 28, y: -4, z: (random() - 0.5) * 6 }); +} + +// Sim time, in seconds at Speed 2 and Gel 1; the gel stretches it. +function step(S, dt) { + // A thicker gel slows everything, keeps the rings smaller and holds the + // threads longer. + const ts = S.gel ** 0.6, d = dt / ts, rs = S.gel ** -0.2; + S.t += d; + if (S.t >= S.next) { + drop(S); + S.next = S.t + PERIOD; + } + // The drop falls through the air and goes in at the surface. + for (let i = S.falling.length - 1; i >= 0; i--) { + const f = S.falling[i]; + f.y += 34 * d; + if (f.y >= SURF + 1) { + newRing(S, 0, f.x, SURF + 2.5, f.z, 0, 0); + S.ripples.push({ x: f.x, age: 0 }); + S.falling.splice(i, 1); + } + } + for (const rp of S.ripples) rp.age += d; + S.ripples = S.ripples.filter((rp) => rp.age < 3); + const T = S.T, born = []; + for (const r of S.rings) { + const G0 = GEN[r.g]; + r.age += d; + r.R = (G0.r0 + (G0.r1 - G0.r0) * (1 - exp(-r.age / TR))) * rs; + const vy = r.v0 * exp(-r.age / TV) + VS, k = exp(-d / 0.8); + const x0 = r.x, y0 = r.y + r.z * TILT; + r.y += vy * d; + r.x += r.vx * d; + r.z += r.vz * d; + r.vx *= k; + r.vz *= k; + // The thread: the ring leaves cream behind it, the same for every LED + // of the way it goes. + const y1 = r.y + r.z * TILT, len = sqrt((r.x - x0) ** 2 + (y1 - y0) ** 2); + dab(T, r.x, y1, (r.g === 0 ? 0.75 : 0.55) * len * r.amp); + if (!r.broken && r.age > G0.brk) { + // The rim breaks into n drops round the ring, each thrown a little + // outward and down, and the ring itself is spent into them. + r.broken = true; + const n = S.n, ph = r.spin; + for (let j = 0; j < n; j++) { + const a = ph + (2 * PI * j) / n, c = cos(a), s = sin(a); + born.push([r.g + 1, r.x + r.R * c, r.y + 0.5, r.z + r.R * s, 7 * c, 7 * s]); + } + } + if (r.broken) r.amp -= d / 0.7; + // The last generation dissolves into the serum after 4 s. + if (r.g === GEN.length - 1 && r.age > 4) r.amp -= d / 3; + } + for (const b of born) newRing(S, b[0], b[1], b[2], b[3], b[4], b[5]); + S.rings = S.rings.filter((r) => r.amp > 0.01 && r.y < H + 12); + // Threads fade, the fade gathered up and laid on every few frames. + S.tk *= exp(-d / (4 * S.gel)); + if (S.tk < 0.98) { + for (let p = 0; p < N; p++) T[p] *= S.tk; + S.tk = 1; + } +} + +// Draw every ring into the cream field. +function render(S) { + const D = S.D, F = S.F; + D.fill(0); + F.fill(0); + for (const r of S.rings) { + const form = cl(r.age / 0.6, 0, 1), R = r.R, amp = r.amp; + const cy = r.y + r.z * TILT; + // A fresh drop is a round blob that opens into the ring. + if (form < 1) splat(D, r.x, cy, 0.55 * R + 0.5, 1.3 * (1 - form) * amp); + // The ring: its core is an eighth of its radius thick, at least 0.55 + // LED, laid as dots along it so that a line of them peaks at the weight. + const core = max(0.55, 0.12 * R), m = max(10, round(2 * PI * R * 1.6)), ds = (2 * PI * R) / m; + for (let j = 0; j < m; j++) { + const a = (2 * PI * j) / m, c = cos(a), s = sin(a); + // The near half (s > 0) lower on the panel and brighter than the far + // half seen through the ring. + const w = (0.85 + 0.45 * s) * form * amp * (ds / (core * 2.5066)); + splat(D, r.x + R * c, cy + R * s * TILT, core, w); + } + // The bell of milky serum the ring carries above it. + splat(F, r.x, cy - 0.45 * R, 0.6 * R + 0.5, 0.4 * form * amp); + } + for (const f of S.falling) { + splat(D, f.x, f.y, 1.1, 1.4); + splat(D, f.x, f.y - 1.6, 0.7, 0.5); + } +} + +// Box blur of radius r, horizontal then vertical, A into B. +function blur(A, B, tmp, r) { + const k = 1 / (2 * r + 1); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let s = 0; + for (let i = -r; i <= r; i++) s += A[y * W + cl(x + i, 0, W - 1)]; + tmp[y * W + x] = s * k; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let s = 0; + for (let i = -r; i <= r; i++) s += tmp[cl(y + i, 0, H - 1) * W + x]; + B[y * W + x] = s * k; + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 3, 1], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.n = round(cl(k[2], 3, 6)); + S.gel = cl(k[3], 1, 5); + if (!S.begun) { + // The gel stretches the run before as it stretches everything. + const n = round(PREROLL * 30 * S.gel ** 0.6); + for (let i = 0; i < n; i++) step(S, 1 / 30); + S.begun = true; + } + if (input.btnPressed && input.btnPressed[0]) { + drop(S); + S.next = S.t + PERIOD; + } + step(S, (d * S.speed) / 2); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + render(S); + const { D, F, T, G, tmp, deep, light, cream } = S; + blur(D, G, tmp, 2); + const h = S.hue + H0, hh = h - floor(h); + // Orange, yellow and lime serum goes brown or olive when deep: there it + // is lifted, so the bottom of the glass is as bright as the top elsewhere. + const warm = cl(1.3 - abs(hh - 0.13) / 0.13, 0, 1); + hsv(h, 0.7 - 0.2 * warm, 0.3 + 0.34 * warm, deep); + hsv(h + 0.03, 0.5 - 0.1 * warm, 0.62 + 0.14 * warm, light); + hsv(h + 0.02, 0.4, 1, cream); + const t = S.t; + for (let y = 0; y < H; y++) { + const u = y / (H - 1), lg = (1 - u) ** 1.6; + for (let x = 0; x < W; x++) { + const p = y * W + x, vig = 0.8 + 0.2 * sin((PI * (x + 0.5)) / W); + // The serum: light from above, and a slow shimmer of caustics. + const sh = 0.07 * sin(x * 0.31 + y * 0.07 + t * 0.6 + 1.7 * sin(x * 0.11 - y * 0.05 - t * 0.4)) * lg; + let r = deep[0] + (light[0] - deep[0]) * lg, g = deep[1] + (light[1] - deep[1]) * lg, b = deep[2] + (light[2] - deep[2]) * lg; + r *= (1 + sh) * vig; + g *= (1 + sh) * vig; + b *= (1 + sh) * vig; + if (y < SURF) { + r *= 0.25; + g *= 0.25; + b *= 0.25; + } + // Cream: the rings, a soft glow round them, the bell and the threads, + // tinted where it is thin and white where it is thick. + const C = D[p] + 0.5 * G[p] + F[p] + T[p]; + const c = 1 - exp(-1.2 * C), wh = c * sqrt(c); + const cr = cream[0] + (1 - cream[0]) * wh, cg = cream[1] + (1 - cream[1]) * wh, cb = cream[2] + (1 - cream[2]) * wh; + r = r * (1 - 0.85 * c) + cr * c; + g = g * (1 - 0.85 * c) + cg * c; + b = b * (1 - 0.85 * c) + cb * c; + display.setPixel(x, y, round(255 * min(1, r)), round(255 * min(1, g)), round(255 * min(1, b))); + } + } + // The surface: a bright line, rippled where a drop went in. + for (let x = 0; x < W; x++) { + let off = 0; + for (const rp of S.ripples) { + const dx = abs(x - rp.x), front = rp.age * 14; + off += 1.2 * exp(-rp.age / 0.8) * sin(dx * 0.9 - rp.age * 12) * exp(-((dx - front) ** 2) / 30); + } + const y = cl(round(SURF + off), 0, H - 1); + display.setPixel(x, y, round(255 * min(1, light[0] * 0.5 + 0.5)), round(255 * min(1, light[1] * 0.5 + 0.5)), round(255 * min(1, light[2] * 0.5 + 0.5))); + } +} From b89eace0bb10011f6bc14ae529cc5e1f8db5e735 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 20:47:01 +0000 Subject: [PATCH 08/13] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2038=ED=8C=90:=20Figure-Eight=20Orbit,=20Couette=20Shimmer=20?= =?UTF-8?q?=EB=A5=BC=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../pattern-study/patterns/couette-shimmer.js | 258 +++++++++++++ .../patterns/figure-eight-orbit.js | 344 ++++++++++++++++++ 2 files changed, 602 insertions(+) create mode 100644 incoming/pattern-study/patterns/couette-shimmer.js create mode 100644 incoming/pattern-study/patterns/figure-eight-orbit.js diff --git a/incoming/pattern-study/patterns/couette-shimmer.js b/incoming/pattern-study/patterns/couette-shimmer.js new file mode 100644 index 000000000..312f89f0d --- /dev/null +++ b/incoming/pattern-study/patterns/couette-shimmer.js @@ -0,0 +1,258 @@ +// ===== Patternflow pattern ===== +// Title: Couette Shimmer +// Family: cosmetic +// Idea: A pearly mica serum fills the gap between a turning inner cylinder and a still outer one, seen from the side, and as the inner one speeds up the smooth sideways streaks of Couette flow break into Taylor vortices, stacked rolls turning in alternate senses whose aligned mica shows as bright and dark bands up the whole panel, which then go wavy and send their waves round the cylinder, while single flakes glint white as they tumble. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 38, after shimmer serum stirred between two cylinders as rheoscopic fluid shows it (Paul Matisse's Kalliroscope), with the Taylor vortices of Taylor-Couette flow (G. I. Taylor, 1923) +// Round: 38 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Spin=0.5..4, Rolls=5..14, Counter=0..1 +// +// Knob 1: Hue. Turns the serum round the whole wheel; its pearl leans 0.12 on round the wheel and the highlight 0.1 back. At 0 lilac with pink pearl; at 0.1 orchid, 0.2 rose, 0.3 coral, 0.4 gold, 0.5 lime, 0.6 mint, 0.7 aqua, 0.8 sky blue, 0.9 periwinkle. From red to lime the dark rolls stay bright and only deepen in colour, so nothing reads brown or olive. +// Knob 2: Spin. How fast the inner cylinder turns, 1 being the critical speed. From 0.5 to 1 plain Couette flow: the serum only runs round, and the panel is a sheen of sideways streaks moving at 5 LEDs a second per unit of Spin. Past 1 Taylor vortices: level bands, a pair of rolls 25.6 rows tall at the opening Rolls, at full contrast by 1.25, standing still while the streaks run through them and flow up one roll and down the next. From 1.2 the rolls go wavy, six waves round the cylinder, two and a half across the panel, travelling round at 0.34 of the inner cylinder's rate: at 2, the opening, a crest crosses the middle every 4.9 seconds. From 2.6 a slower second wave swells and shrinks the first, and from 3 the rolls fill with turbulence and their edges fray while the bands hold. In the laboratory those last two come at about 10 and 20 times the critical speed; the knob draws them in. +// Knob 3: Rolls. How many pairs of rolls stand up the panel, as a narrower gap between the cylinders gives more. At 5 each roll is 12.8 rows tall; at 9.5 6.7; at 14 4.6, a fine ribbing. +// Knob 4: Counter. How fast the outer cylinder turns the other way. Up to 0.15 nothing changes. From 0.15 to 0.6 the rolls lean into a spiral vortex that runs down the panel as it turns, 1.2 pairs of fall across the panel at 0.6, and the waves die away; from 0.6 a second spiral of the other hand runs up through it, and at 1 the two cross in a lattice of diamonds that slides sideways: interpenetrating spirals, as in Andereck, Liu and Swinney's map of the regimes (1986). + +const W = 64, H = 128, N = W * H; +const { floor, min, max, sqrt, sin, cos, asin, exp, abs, round, random, PI } = Math; +const TAU = 2 * PI; +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 cylinder seen from the side: column x looks at angle TH[x] round it, +// the front half from -1.29 to 1.29 radians, so a thing going round slows +// and narrows toward the panel's edges. ARC is the distance round the +// surface in LEDs, the cylinder 32 LEDs in radius. +const TH = new Float32Array(W), ARC = new Float32Array(W), SHADE = new Float32Array(W), SPEC = new Float32Array(W); +for (let x = 0; x < W; x++) { + const xn = (x + 0.5) / 32 - 1, th = asin(0.96 * xn); + TH[x] = th; + ARC[x] = th * 32; + // Light from the front and a little to the left: the serum dims toward + // the cylinder's edges, and a soft upright highlight sits left of centre. + SHADE[x] = 0.62 + 0.38 * cos(th + 0.2); + SPEC[x] = exp(-(((th + 0.42) / 0.32) ** 2)); +} + +// What the aligned mica reflects across one pair of rolls, phase 0 to 2 pi: +// the outflow boundary at 0, where the serum leaves the inner cylinder and +// spreads against the outer one; the inflow boundary at pi. In one roll +// the serum at the outer wall runs up, in the next down, so the flakes there +// lean one way and then the other and the light catches one roll and not +// the next. A thin dark line marks the outflow, where the flakes stand on +// edge, and a thin bright one the inflow. +const NR = 512, REFL = new Float32Array(NR); +for (let i = 0; i < NR; i++) { + const ph = (i / NR) * TAU, s = sin(ph); + const d0 = min(ph, TAU - ph), dp = abs(ph - PI); + REFL[i] = cl(0.5 + 0.34 * (s >= 0 ? s ** 0.6 : -((-s) ** 0.6)) - 0.42 * exp(-((d0 / 0.3) ** 2)) + 0.3 * exp(-((dp / 0.22) ** 2)), 0, 1); +} +const refl = (ph) => { + const u = (ph / TAU) % 1; + return REFL[((u < 0 ? u + 1 : u) * NR) | 0]; +}; + +// Streak texture: random values on a 64 by 128 grid, read stretched 6 times +// sideways, so the flakes the flow has lined up show as long streaks. +const NU = 64, NV = 128; + +export function setup(params) { + const noise = new Float32Array(NU * NV); + for (let i = 0; i < noise.length; i++) noise[i] = random(); + const rate = new Float32Array(H); + for (let y = 0; y < H; y++) rate[y] = 0.85 + 0.3 * random(); + const glints = []; + for (let i = 0; i < 220; i++) glints.push({ a: random() * 2.6 - 1.3, y: random() * H, ph: random() * TAU, w: 0.6 + 0.8 * random() }); + params.s = { + hue: 0, spin: 2, rolls: 5, counter: 0, warm: 0, t: 0, ang: 0, wave: 0, spiral: 0, su: 0, begun: false, + noise, rate, glints, glow: new Float32Array(N), CA: new Float32Array(W), CB: new Float32Array(W), RA: new Float32Array(H), RB: new Float32Array(H), col: [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], + }; +} + +function nz(S, u, v) { + const n = S.noise, fu = floor(u), fv = floor(v), au = u - fu, av = v - fv; + const i0 = ((fu % NU) + NU) % NU, i1 = (i0 + 1) % NU, j0 = ((fv % NV) + NV) % NV, j1 = (j0 + 1) % NV; + const a = n[j0 * NU + i0] + (n[j0 * NU + i1] - n[j0 * NU + i0]) * au; + const b = n[j1 * NU + i0] + (n[j1 * NU + i1] - n[j1 * NU + i0]) * au; + return a + (b - a) * av; +} + +// The regimes Spin walks through, as weights 0..1. +function regimes(s) { + return { + // Taylor vortices from the critical speed 1, full by 1.25. + rolls: sqrt(sm(1, 1.25, s)), + // Waves on the rolls from 1.2, full by 1.8; quieter again toward 4. + waves: sqrt(sm(1.2, 1.8, s)) * (1 - 0.6 * sm(3, 4, s)), + // A second, slower wave that modulates the first, from 2.6. + modulate: sm(2.6, 3.4, s), + // Turbulence inside the rolls from 3. + turb: sm(3, 4, s), + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 5, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.spin = cl(k[1], 0.5, 4); + S.rolls = cl(k[2], 5, 14); + S.counter = cl(k[3], 0, 1); + step(S, d); + if (!S.begun) { + // Open as if the serum had been turning a while. + for (let i = 0; i < 90; i++) step(S, 1 / 30); + S.begun = true; + } +} + +function step(S, d) { + const s = S.spin; + S.t += d; + // The streaks run round at 5 LEDs a second per unit of Spin; the inner + // cylinder turns at twice that, 0.31 radians a second per unit; the + // waves go round at 0.34 of the inner cylinder's rate, as Coles measured. + S.su += d * 5 * s; + S.ang += d * 0.3125 * s; + S.wave += d * 0.34 * 0.3125 * s; + S.spiral += d * 0.3125 * s * 0.5; + // Glints ride the flow: round with the streaks, and up or down the roll + // they are in toward its inflow boundary, where they dive and come back. + const R = regimes(s), kk = (TAU * S.rolls) / H; + for (const g of S.glints) { + g.a += (d * 5 * s) / 32; + if (g.a > 1.3) g.a -= 2.6; + const ph = kk * (g.y - 63.5); + g.y += d * R.rolls * 1.2 * s * sin(ph); + if (g.y < 0) g.y += H; + if (g.y >= H) g.y -= H; + // At the inflow boundary the serum turns in toward the inner cylinder + // and the flake goes with it; another comes out somewhere else. + if (R.rolls > 0.3 && cos(ph) < -0.93) { + g.y = random() * H; + g.a = random() * 2.6 - 1.3; + } + // Tumbling: a flake turns over faster in faster shear. + g.ph += d * g.w * (1.2 + 1.6 * s); + } +} + +function palette(S) { + const h = S.hue + 0.74, hh = h - floor(h); + // From red through orange and yellow to lime a deep tone reads brown or + // olive on the panel, so there the rolls keep their brightness and differ + // by saturation instead: full from 0.03 to 0.23 round the wheel, none + // beyond 0.96 and 0.3. + const dw = abs(((hh - 0.13 + 1.5) % 1) - 0.5), warm = 1 - sm(0.1, 0.17, dw); + const c = S.col; + // Deep, body, pearl and the highlight that leans round the wheel. + hsv(h - 0.02 * warm, 0.82 + 0.06 * warm, 0.3 + 0.55 * warm, c[0]); + hsv(h + 0.01, 0.6 - 0.15 * warm, 0.72 + 0.26 * warm, c[1]); + hsv(h + 0.12, 0.26 - 0.08 * warm, 1, c[2]); + hsv(h - 0.1, 0.12, 1, c[3]); + S.warm = warm; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + palette(S); + const R = regimes(S.spin), kk = (TAU * S.rolls) / H, lam = H / S.rolls; + // Counter-rotating outer cylinder: from 0.15 the rolls lean into a spiral + // that runs down the panel as it turns; from 0.6 a second spiral of the + // other hand, turning the same way and so running up, comes in across it. + const cr = S.counter, sp1 = 3 * sm(0.15, 0.6, cr), sp2 = sm(0.6, 1, cr); + const wv = R.waves * (1 - sm(0.15, 0.45, cr)); + const m = 6, aw = 0.13 * lam * kk; + const c = S.col, glow = S.glow, { CA, CB, RA, RB } = S; + // The rolls are not all equally strong: two slow waves of strength, one + // drifting round with the serum, cross each other over the panel. + for (let x = 0; x < W; x++) { + CA[x] = sin(0.09 * ARC[x] - 0.8 * S.ang); + CB[x] = cos(0.21 * ARC[x] + 0.5 * S.ang); + } + for (let y = 0; y < H; y++) { + RA[y] = cos(0.045 * y + 0.13 * S.t); + RB[y] = sin(0.083 * y - 0.21 * S.t); + } + glow.fill(0); + // Glints: a flake flashes as it turns face on to the light. + for (const g of S.glints) { + const cw = cos(g.ph); + if (cw < 0.85) continue; + const xs = (sin(g.a) / 0.96 + 1) * 32 - 0.5, xi = round(xs), yi = round(g.y) % H; + if (xi < 0 || xi >= W) continue; + const b = ((cw - 0.85) / 0.15) ** 2 * 1.3; + glow[yi * W + xi] += b; + if (xi > 0) glow[yi * W + xi - 1] += b * 0.3; + if (xi < W - 1) glow[yi * W + xi + 1] += b * 0.3; + if (yi > 0) glow[(yi - 1) * W + xi] += b * 0.3; + if (yi < H - 1) glow[(yi + 1) * W + xi] += b * 0.3; + } + // The streak texture flows along each roll toward its inflow boundary; + // two copies, three seconds apart, take turns so it never smears. + const TL = 3, t0 = S.t % TL, t1 = (S.t + TL / 2) % TL, w0 = 1 - abs((2 * t0) / TL - 1); + const vz = R.rolls * 1.2 * S.spin; + const turbU = 1 + 2 * R.turb; + for (let x = 0; x < W; x++) { + const th = TH[x], arc = ARC[x]; + // Axial displacement of the rolls here: the travelling wave, and in the + // modulated regime a slower second wave riding on it. + const wph = m * (th - S.wave); + const wave = aw * wv * sin(wph) * (1 + 0.5 * R.modulate * sin(2 * th - 0.4 * S.wave * m)); + const spiral1 = sp1 * (th - S.spiral), spiral2 = 3 * sp2 * (th - S.spiral); + for (let y = 0; y < H; y++) { + const p = y * W + x; + let ph = kk * (y - 63.5) + wave; + if (R.turb > 0) ph += R.turb * 1.4 * (nz(S, arc / 5 - S.su * 0.4, y * 0.35 + S.t * 0.7) - 0.5); + let rf = refl(ph + spiral1); + if (sp2 > 0) rf = rf * (1 - 0.5 * sp2) + refl(ph - spiral2) * 0.5 * sp2; + const rb = 0.5 + R.rolls * (rf - 0.5) * (1 + 0.16 * CA[x] * RA[y] + 0.1 * CB[x] * RB[y]); + // Streaks: the texture moved round with the serum and along the roll. + const az = sin(ph + spiral1) * vz, su = S.su * S.rate[y]; + const a0 = nz(S, ((arc - su) / 10) * turbU, (y - az * t0) * 0.75), a1 = nz(S, ((arc - su) / 10) * turbU + 17, (y - az * t1) * 0.75); + const tex = a0 * w0 + a1 * (1 - w0) - 0.5; + const sh = SHADE[x], spc = SPEC[x]; + // Light: the serum's body by the cylinder's shading and the rolls, + // the streaks over it, the highlight strongest on the rolls that + // catch the light, and the glints. + const L = sh * (0.18 + 0.72 * rb) + (0.4 - 0.1 * R.rolls) * tex * sh; + const hi = spc * (0.15 + 0.85 * rb * rb) * 0.75 + glow[p]; + // Deep to body to pearl by L; the highlight toward white. + let r, g, b; + if (L < 0.5) { + const f = cl(L / 0.5, 0, 1); + r = c[0][0] + (c[1][0] - c[0][0]) * f; + g = c[0][1] + (c[1][1] - c[0][1]) * f; + b = c[0][2] + (c[1][2] - c[0][2]) * f; + } else { + const f = cl((L - 0.5) / 0.45, 0, 1); + r = c[1][0] + (c[2][0] - c[1][0]) * f; + g = c[1][1] + (c[2][1] - c[1][1]) * f; + b = c[1][2] + (c[2][2] - c[1][2]) * f; + } + const lo = 1 - (1 - cl(L / 0.2, 0, 1)) * (1 - 0.7 * S.warm), hm = min(1, hi); + r *= lo; + g *= lo; + b *= lo; + r += (c[3][0] - r) * hm; + g += (c[3][1] - g) * hm; + b += (c[3][2] - b) * hm; + display.setPixel(x, y, round(255 * cl(r, 0, 1)), round(255 * cl(g, 0, 1)), round(255 * cl(b, 0, 1))); + } + } +} diff --git a/incoming/pattern-study/patterns/figure-eight-orbit.js b/incoming/pattern-study/patterns/figure-eight-orbit.js new file mode 100644 index 000000000..0f4f58419 --- /dev/null +++ b/incoming/pattern-study/patterns/figure-eight-orbit.js @@ -0,0 +1,344 @@ +// ===== Patternflow pattern ===== +// Title: Figure-Eight Orbit +// Family: physics +// Idea: Three equal masses under Newton's gravity chase one another round one figure eight standing the height of the panel, each a third of a period behind the next, integrated step by step from Chenciner and Montgomery's starting values with a faint line of the exact eight under them, and a kick to their starting velocities leaves the eight wobbling or breaks it into a tight binary and a body flung off the panel. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: figure-eight solution of the three-body problem (Cris Moore, 1993; Alain Chenciner and Richard Montgomery, 2000), pattern round 38 +// Round: 38 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Trail=0.33..2, Kick=0..1 +// +// Knob 1: Hue. Turns the three bodies round the wheel together, a third of the wheel apart. At 0 azure, rose and lime; at 0.25 magenta, butter yellow and cyan; at 0.5 peach, sea green and violet; at 0.75 green, blue and red. A body that comes round to orange or yellow is paled and its tail kept near full brightness, so it never reads brown or olive. +// Knob 2: Speed. At 2 one period of the eight, 6.33 units of the orbit's own time, takes 6.3 seconds, and a body runs up to 67 LEDs a second through the crossing and slows to 25 at the far ends; at 0.2 a period takes 63 seconds, at 3 it takes 4.2. +// Knob 3: Trail. How long each tail is, in periods; a tail is 8 LEDs wide at its body and narrows to nothing at its end. At 0.33 each tail ends just at the next body, so the eight is three comets in three colours; from 1 the whole eight is drawn, each stretch in the colour of the body that passed it last; with a kick the older passes show under the newest, a band of eights 2 periods deep at 2. +// Knob 4: Kick. Pushes bodies 1 and 2 apart along the line between them at the start, then scales every velocity back to the old kinetic energy, so energy, momentum and angular momentum stay those of the eight. Up to 0.06 no kick: the exact eight, its energy within 1e-8 of the start for 200 periods. At 0.25 a push of 0.07, 5% of the fastest speed, and the eight wobbles; at 0.5 (0.18) it swings wide but holds; from about 0.65 (0.25) it holds a few periods and then two bodies pair off and the third is slung away; at 1 (0.45) a body leaves the panel in the first tenth of a period. The push keeps the symmetry of the starting line-up, which is why the eight survives so much of it. The three start again after a body has been off the panel 3 seconds, and 0.4 seconds after the knob comes to rest. +// Buttons: K1 tap nudges the running orbit: two bodies pushed apart by 0.05, or by the Kick if that is more, a different pair on each tap. Away from the starting line-up the eight is far less forgiving: one tap leaves it wobbling for a minute or more before it breaks, three taps a second apart break it in about 20 seconds. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, sqrt, abs, hypot, round, cbrt, exp } = 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]; +} + +// Chenciner and Montgomery's starting values, G = m = 1: body 3 at the +// crossing, bodies 1 and 2 opposite each other, both moving at half body 3's +// velocity the other way. The period is 6.32591398. +const X1 = 0.97000436, Y1 = -0.24308753, VX3 = -0.93240737, VY3 = -0.86473146, PERIOD = 6.32591398; +// Orbit to panel: the eight's long axis down the panel, SC LEDs to the unit. +// It reaches 1.081 along that axis and 0.358 across, so 115 by 38 LEDs. +const SC = 53, CX = 31.5, CY = 63.5; +// Under RS the pull is that of a uniform ball, so a close pass of a binary +// stays finite. The eight never comes closer than 0.69, so it never feels it. +const RS = 0.05; +// Step in orbit time: Yoshida's fourth-order leapfrog at H0, shortened by +// (r / 0.5)^1.5 when two bodies are closer than 0.5. +const H0 = 1 / 128; +const YW1 = 1 / (2 - cbrt(2)), YW0 = -cbrt(2) * YW1; +const YC = [YW1 / 2, (YW0 + YW1) / 2, (YW0 + YW1) / 2, YW1 / 2], YD = [YW1, YW0, YW1]; +// Tail half-width at the head, in LEDs, and how far a body glows. +const HW = 3.6, GLOW = 6; +// Opening hues: azure, rose, lime. +const HUE0 = 0.58; +// Longest tail, in periods, and so the history the pattern keeps. +const LMAX = 2; + +export function setup(params) { + params.s = { + hue: 0, speed: 2, trail: 0.33, kick: 0, kickSet: -1, kickWait: 0, begun: false, + p: new Float64Array(6), v: new Float64Array(6), a: new Float64Array(6), + clock: 0, carry: 0, out: 0, fade: 1, phase: 0, nudges: 0, + // Per body and LED: when the body last passed (orbit time) and how close. + tv: new Float32Array(3 * N).fill(-1e9), td: new Float32Array(3 * N).fill(99), + lx: new Float32Array(3), ly: new Float32Array(3), warm: [0, 0, 0], + ref: new Float32Array(N), col: [[0, 0, 0], [0, 0, 0], [0, 0, 0]], + }; + drawReference(params.s); +} + +// Pull on each body; under RS the force grows linearly from the centre. +function accel(p, a) { + a.fill(0); + for (let i = 0; i < 3; i++) + for (let j = i + 1; j < 3; j++) { + const dx = p[2 * j] - p[2 * i], dy = p[2 * j + 1] - p[2 * i + 1], r = sqrt(dx * dx + dy * dy), q = max(r, RS); + const f = 1 / (q * q * q); + a[2 * i] += dx * f; + a[2 * i + 1] += dy * f; + a[2 * j] -= dx * f; + a[2 * j + 1] -= dy * f; + } +} + +function closest(p) { + let m = 1e9; + for (let i = 0; i < 3; i++) + for (let j = i + 1; j < 3; j++) m = min(m, hypot(p[2 * j] - p[2 * i], p[2 * j + 1] - p[2 * i + 1])); + return m; +} + +// One step of Yoshida's fourth-order symplectic integrator: three leapfrogs +// of h w1, h w0, h w1, with w0 negative. +function step(p, v, a, h) { + for (let s = 0; s < 3; s++) { + for (let i = 0; i < 6; i++) p[i] += YC[s] * h * v[i]; + accel(p, a); + for (let i = 0; i < 6; i++) v[i] += YD[s] * h * a[i]; + } + for (let i = 0; i < 6; i++) p[i] += YC[3] * h * v[i]; +} + +const stepLength = (p) => H0 * min(1, (max(closest(p), RS) / 0.5) ** 1.5); + +// The kick: bodies i and j pushed apart along the line between them, each +// by eps, and then every velocity scaled back to the kinetic energy before +// the push. Positions do not move, so the energy stays what it was; the +// momentum stays zero and the angular momentum stays zero too, as for the +// eight itself, so only the shape of the motion changes. +function push(S, i, j, eps) { + const { p, v } = S, dx = p[2 * i] - p[2 * j], dy = p[2 * i + 1] - p[2 * j + 1], r = hypot(dx, dy) || 1; + let k0 = 0, k1 = 0; + for (let n = 0; n < 6; n++) k0 += v[n] * v[n]; + v[2 * i] += (eps * dx) / r; + v[2 * i + 1] += (eps * dy) / r; + v[2 * j] -= (eps * dx) / r; + v[2 * j + 1] -= (eps * dy) / r; + for (let n = 0; n < 6; n++) k1 += v[n] * v[n]; + const f = sqrt(k0 / k1); + for (let n = 0; n < 6; n++) v[n] *= f; +} + +// Kick 0..1 to eps: nothing up to 0.06, then up to 0.45. +const epsOf = (k) => 0.45 * (max(0, k - 0.06) / 0.94) ** 1.2; + +function initial(S) { + const { p, v } = S; + p.set([X1, Y1, -X1, -Y1, 0, 0]); + v.set([-VX3 / 2, -VY3 / 2, -VX3 / 2, -VY3 / 2, VX3, VY3]); + push(S, 0, 1, epsOf(S.kick)); + S.kickSet = S.kick; +} + +const toCol = (y) => CX + y * SC, toRow = (x) => CY - x * SC; + +// A tail at an LED: age a in orbit time, distance d in LEDs, tail length L. +// It narrows from HW at the head to nothing at its end, and dims from full +// at the head to 0.6 a third of a period behind it and 0.55 after that. +function tailCover(a, d, L) { + if (a < 0 || a >= L) return 0; + return cl((HW + 0.5) * (1 - a / L) ** 1.5 - d + 0.5, 0, 1); +} +const tailBright = (a) => 0.55 + 0.45 * exp(-a / (0.15 * PERIOD)); +const tailValue = (a, d, L) => tailCover(a, d, L) * tailBright(a); + +// Mark the LEDs round body k at orbit time t. Within one pass the closest +// approach is kept; a later pass takes the LED when it would light it more. +function deposit(S, k, t) { + const bx = toCol(S.p[2 * k + 1]), by = toRow(S.p[2 * k]), R = HW + 1.5, L = LMAX * PERIOD; + if (bx < -R || bx > W + R || by < -R || by > H + R) return; + const o = k * N, tv = S.tv, td = S.td; + for (let y = max(0, floor(by - R)); y <= min(H - 1, floor(by + R)); y++) + for (let x = max(0, floor(bx - R)); x <= min(W - 1, floor(bx + R)); x++) { + const d = hypot(x + 0.5 - bx, y + 0.5 - by); + if (d > R) continue; + const q = o + y * W + x, t0 = tv[q]; + if (abs(t - t0) < 0.3) { + if (d < td[q]) { + tv[q] = t; + td[q] = d; + } + continue; + } + const ref = max(t, t0), vNew = tailValue(ref - t, d, L), vOld = tailValue(ref - t0, td[q], L); + if (vNew > vOld || (vNew === vOld && t > t0)) { + tv[q] = t; + td[q] = d; + } + } +} + +// Deposit body k if it has moved half an LED since it last did. +function trace(S, t) { + for (let k = 0; k < 3; k++) { + const bx = toCol(S.p[2 * k + 1]), by = toRow(S.p[2 * k]); + if (hypot(bx - S.lx[k], by - S.ly[k]) < 0.5) continue; + S.lx[k] = bx; + S.ly[k] = by; + deposit(S, k, t); + } +} + +const farthest = (p) => max(hypot(p[0], p[1]), hypot(p[2], p[3]), hypot(p[4], p[5])); + +// Start (or start again): the starting line-up with the current kick, and +// its own past drawn behind it by running the orbit backward for up to LMAX +// periods, or until a body would come in from off the panel. +function restart(S) { + S.tv.fill(-1e9); + S.td.fill(99); + S.clock = 0; + S.carry = 0; + S.out = 0; + initial(S); + const { p, v, a } = S; + const p0 = Float64Array.from(p), v0 = Float64Array.from(v); + S.lx.fill(-99); + S.ly.fill(-99); + let t = 0; + while (t > -LMAX * PERIOD && farthest(p) < 1.4) { + trace(S, t); + const h = stepLength(p); + step(p, v, a, -h); + t -= h; + } + p.set(p0); + v.set(v0); + S.lx.fill(-99); + S.ly.fill(-99); + trace(S, 0); +} + +// Whole steps only, the remainder carried to the next frame, so at the +// opening the step never changes and the integrator stays symplectic. +function advance(S, dt) { + const { p, v, a } = S; + S.carry += dt; + for (let h = stepLength(p); S.carry >= h; h = stepLength(p)) { + step(p, v, a, h); + S.carry -= h; + S.clock += h; + trace(S, S.clock); + } +} + +// The exact eight, sampled over one period, as a faint line under the tails. +function drawReference(S) { + const p = Float64Array.from([X1, Y1, -X1, -Y1, 0, 0]); + const v = Float64Array.from([-VX3 / 2, -VY3 / 2, -VX3 / 2, -VY3 / 2, VX3, VY3]), a = new Float64Array(6); + const n = 1600, h = PERIOD / n, ref = S.ref; + for (let s = 0; s < n; s++) { + const bx = toCol(p[1]), by = toRow(p[0]); + for (let y = max(0, floor(by - 1)); y <= min(H - 1, floor(by + 1)); y++) + for (let x = max(0, floor(bx - 1)); x <= min(W - 1, floor(bx + 1)); x++) { + const c = cl(1.1 - hypot(x + 0.5 - bx, y + 0.5 - by), 0, 1); + if (c > ref[y * W + x]) ref[y * W + x] = c; + } + step(p, v, a, h); + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0.33, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.trail = cl(k[2], 0.33, 2); + S.kick = cl(k[3], 0, 1); + if (!S.begun) { + restart(S); + S.begun = true; + } + // A turned Kick starts the three again once the knob has rested 0.4 s, + // fading the old orbit out over 0.5 s and the new one in. + if (abs(S.kick - S.kickSet) > 0.004) { + S.kickWait += d; + if (S.kickWait > 0.4 && S.phase === 0) S.phase = -1; + } else S.kickWait = 0; + if (input.btnPressed && input.btnPressed[0] && S.phase === 0) { + const pair = S.nudges++ % 3; + push(S, pair, (pair + 1) % 3, max(0.05, epsOf(S.kick))); + } + // At Speed 2 the orbit's clock runs 1 unit a second. + advance(S, d * S.speed * 0.5); + // Off the panel: a body more than 1.35 from the centre. + if (farthest(S.p) > 1.35) S.out += d; + else S.out = max(0, S.out - d); + if ((S.out > 3 || farthest(S.p) > 8) && S.phase === 0) S.phase = -1; + if (S.phase < 0) { + S.fade -= d / 0.5; + if (S.fade <= 0) { + S.fade = 0; + restart(S); + S.kickWait = 0; + S.phase = 1; + } + } else if (S.phase > 0) { + S.fade += d / 0.5; + if (S.fade >= 1) { + S.fade = 1; + S.phase = 0; + } + } +} + +function palette(S) { + for (let k = 0; k < 3; k++) { + const h = S.hue + HUE0 + k / 3, hh = h - floor(h); + // Orange to yellow-green: paler and kept at full value, so a tail never + // reads brown or olive. + const warm = sm(0.02, 0.08, hh) * (1 - sm(0.2, 0.27, hh)); + hsv(h, 0.85 - 0.3 * warm, 1, S.col[k]); + S.warm[k] = warm; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + palette(S); + const { tv, td, ref, col, clock, fade } = S, L = S.trail * PERIOD; + const bx = [0, 0, 0], by = [0, 0, 0]; + for (let k = 0; k < 3; k++) { + bx[k] = toCol(S.p[2 * k + 1]); + by[k] = toRow(S.p[2 * k]); + } + const ord = [0, 1, 2], ag = [0, 0, 0]; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + // The faint reference eight, then the tails painted oldest first, so + // the newest pass lies on top. + let r = 0.13 * ref[p], g = 0.13 * ref[p], b = 0.16 * ref[p]; + for (let k = 0; k < 3; k++) ag[k] = clock - tv[k * N + p]; + ord[0] = 0; + ord[1] = 1; + ord[2] = 2; + if (ag[ord[0]] < ag[ord[1]]) [ord[0], ord[1]] = [ord[1], ord[0]]; + if (ag[ord[1]] < ag[ord[2]]) [ord[1], ord[2]] = [ord[2], ord[1]]; + if (ag[ord[0]] < ag[ord[1]]) [ord[0], ord[1]] = [ord[1], ord[0]]; + for (let n = 0; n < 3; n++) { + const k = ord[n], age = ag[k], d = td[k * N + p], al = tailCover(age, d, L) * fade; + if (al <= 0) continue; + const c = col[k], lv = 1 - (1 - tailBright(age)) * (1 - 0.8 * S.warm[k]), wh = 0.5 * (1 - age / L) ** 8 * cl(1 - d / HW, 0, 1); + r += ((c[0] + (1 - c[0]) * wh) * lv - r) * al; + g += ((c[1] + (1 - c[1]) * wh) * lv - g) * al; + b += ((c[2] + (1 - c[2]) * wh) * lv - b) * al; + } + // The bodies: a white core in a glow of the body's colour. + for (let k = 0; k < 3; k++) { + const dx = x + 0.5 - bx[k], dy = y + 0.5 - by[k], d2 = dx * dx + dy * dy; + if (d2 >= GLOW * GLOW) continue; + const dd = sqrt(d2), halo = 0.7 * fade * (1 - dd / GLOW) ** 1.5, core = sm(2.8, 1.4, dd) * fade, c = col[k]; + r = 1 - (1 - r) * (1 - c[0] * halo); + g = 1 - (1 - g) * (1 - c[1] * halo); + b = 1 - (1 - b) * (1 - c[2] * halo); + r += (1 - r) * core; + g += (1 - g) * core; + b += (1 - b) * core; + } + display.setPixel(x, y, round(255 * min(1, r)), round(255 * min(1, g)), round(255 * min(1, b))); + } +} + From 17077f7aec6fb46681c8e97e574c3532d0355b09 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 20:52:29 +0000 Subject: [PATCH 09/13] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2038=ED=8C=90:=20Bernoulli=20Convolutions,=20Photochromic=20?= =?UTF-8?q?=EB=A5=BC=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../pattern-study/patterns/photochromic.js | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 incoming/pattern-study/patterns/photochromic.js diff --git a/incoming/pattern-study/patterns/photochromic.js b/incoming/pattern-study/patterns/photochromic.js new file mode 100644 index 000000000..b348378a6 --- /dev/null +++ b/incoming/pattern-study/patterns/photochromic.js @@ -0,0 +1,203 @@ +// ===== Patternflow pattern ===== +// Title: Photochromic +// Family: cosmetic +// Idea: A pale shimmer polish fills the panel, every LED either bare polish or a mica flake that catches the light or turns away from it; an unseen UV torch, seen only by its faint violet spill, sweeps down or up the panel in a weaving stroke every 13 seconds, and wherever it passes the polish deepens within a quarter of a second to a dark jewel colour that dims and tints the flakes under it, then fades back to pale over the next few seconds, the first-lit parts first. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 38, after photochromic nail polish, pale in the shade, that deepens under UV light and fades back [Beer-Lambert law, August Beer, 1852] +// Round: 38 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Flakes=1..4, Fade=-1..1 +// +// Knob 1: Hue. Turns the polish round the whole wheel, pale and deep together. At 0 lavender deepening to dark violet; at 0.1 pink to plum; at 0.2 blush to raspberry; at 0.3 cream to tangerine; at 0.4 butter to marigold; at 0.5 pale lime to lime; at 0.6 mint to forest; at 0.7 aqua to deep teal; at 0.8 sky to navy; at 0.9 periwinkle to indigo. From 0.24 to 0.48, and partly from 0.12 to 0.6, where a dark shade would read brown or olive, the polish goes from cream to a bright full colour instead of darkening. +// Knob 2: Speed. At 2 a stroke starts every 13 seconds and takes 1.6 seconds to cross the 128 rows, weaving 20 LEDs either side of the middle about twice on the way; the dye under the middle of the torch is 90% switched in 0.23 seconds. At 0.2 all of it ten times slower, so the first stroke comes at 22 seconds; at 3 one and a half times faster, the first stroke at 1.5 seconds. +// Knob 3: Flakes. The size of the shimmer flakes. At 1 one LED, 38% of them catching the light and 22% turned away, a fine shimmer over every LED; at 2 flakes two LEDs across, 30% catching and 10% turned away, with bare polish between; at 3 small crosses and at 4 round flakes four LEDs across, a coarse glitter. +// Knob 4: Fade. How long the deep colour lasts once the torch has passed. At -1 it is half gone in 1.5 seconds and only a short band trails the torch; at 0.06 half gone in 4.8 seconds; at 1 in 13.5, so the panel is still half deep when the next stroke comes, and the stroke from before the start still shows on the opening picture. +// Buttons: K1 tap sends a stroke now, running the other way from the last one. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, sin, exp, round, random, LN10, PI } = Math; +const TAU = 2 * PI; +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]; +} + +// Seconds at Speed 2: a stroke every PERIOD, crossing in SWEEP. The first +// stroke of a fresh start begins at FIRST; the one before it, which ended +// 8.4 seconds before the start, is run through when the pattern opens. +const PERIOD = 13, SWEEP = 2.4, FIRST = 2.2; +// The torch: a soft spot of sigma SIG LEDs, weaving AMP LEDs either side of +// the middle; the dye switches at KON per second where it is brightest. +const SIG = 15, AMP = 20, KON = 10; +// Optical density of the switched dye, so the depth of colour goes as +// 1 - 10^(-OD a) (Beer-Lambert): it holds dark while most of the dye is +// still switched and lets go toward the end. +const OD = 2; +// The fade, from a = 0.95 to the a where the colour is half gone. +const HALF_A = 1.856; +// Opening hue: violet, not red. +const H0 = 0.76; + +export function setup(params) { + params.s = { + hue: 0, speed: 2, flakes: 1, fade: 0.06, t: 0, begun: false, k: 0, tx: -99, ty: -99, on: 0, + a: new Float32Array(N), g: new Float32Array(N), ph: new Float32Array(N), om: new Float32Array(N), + size: 0, pale: [0, 0, 0], deep: [0, 0, 0], tint: [0, 0, 0], pearl: [0, 0, 0], extra: -1, + }; +} + +// The flakes for a size, one glint per LED, every LED of a flake sharing +// it: 1 catches the light, -1 is turned away and shades the polish under +// it, 0 is bare polish. At size 1 every LED holds mica, 38% catching the +// light, 22% turned away; larger flakes are fewer, 30% and 10%. +function flakes(S, size) { + const { g, ph, om } = S; + const cw = Math.ceil(W / size), nc = cw * Math.ceil(H / size); + const cg = new Float32Array(nc), cp = new Float32Array(nc), co = new Float32Array(nc); + for (let c = 0; c < nc; c++) { + const u = random(); + if (size === 1) cg[c] = u < 0.4 ? 0 : u < 0.62 ? -1 : 1; + else cg[c] = u < 0.6 ? 0 : u < 0.7 ? -1 : 1; + cp[c] = random() * TAU; + co[c] = 0.8 + 0.7 * random(); + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const c = floor(y / size) * cw + floor(x / size), i = y * W + x; + // Flakes three or four across lose their corners. + const ex = x % size === 0 || x % size === size - 1, ey = y % size === 0 || y % size === size - 1; + g[i] = size > 2 && ex && ey ? 0 : cg[c]; + ph[i] = cp[c]; + om[i] = co[c]; + } + S.size = size; +} + +// Where stroke k has the torch at progress s (0..1): strokes alternate down +// and up the panel, weaving side to side, each a little different. +function torchAt(S, k, s) { + const down = (k & 1) === 0, w = 2.4 + 0.5 * ((k * 0.618 + 10) % 1), ph = k * 2.4; + S.ty = down ? -30 + s * (H + 60) : H + 30 - s * (H + 60); + S.tx = W / 2 + AMP * sin(TAU * w * s + ph); +} + +// Advance the dye by dt seconds of pattern time: switched by the torch at +// KON times its intensity, back by the thermal fade. +function dye(S, dt) { + const a = S.a, half = 4.5 * 3 ** S.fade, decay = exp((-HALF_A / half) * dt); + for (let i = 0; i < N; i++) a[i] *= decay; + if (!S.on) return; + const R = 3 * SIG, x0 = max(0, floor(S.tx - R)), x1 = min(W - 1, Math.ceil(S.tx + R)); + const y0 = max(0, floor(S.ty - R)), y1 = min(H - 1, Math.ceil(S.ty + R)); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const dx = x + 0.5 - S.tx, dy = y + 0.5 - S.ty, I = exp(-(dx * dx + dy * dy) / (2 * SIG * SIG)); + if (I < 0.01) continue; + const i = y * W + x; + a[i] += (1 - a[i]) * (1 - exp(-KON * I * dt)); + } +} + +// Run the clock from t0 to t1 in steps, strokes switching on and off. +function run(S, t0, t1, step) { + for (let t = t0; t < t1 - 1e-9; t += step) { + const d = min(step, t1 - t), tm = t + d; + const k = floor((tm - FIRST) / PERIOD), s = (tm - FIRST - k * PERIOD) / SWEEP; + S.on = 0; + if (s <= 1) { + S.on = 1; + S.k = k; + torchAt(S, k, s); + } + if (S.extra >= 0) { + const se = (tm - S.extra) / SWEEP; + if (se <= 1) { + S.on = 1; + torchAt(S, S.k + 1, se); + } else S.extra = -1; + } + dye(S, d); + } +} + +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); + S.flakes = cl(k[2], 1, 4); + S.fade = cl(k[3], -1, 1); + const size = round(S.flakes); + if (size !== S.size) flakes(S, size); + if (!S.begun) { + S.t = FIRST - PERIOD - 0.3; + run(S, S.t, 0, 0.1); + S.t = 0; + S.begun = true; + } + if (input.btnPressed && input.btnPressed[0] && S.extra < 0) S.extra = S.t; + const step = (d * S.speed) / 2; + if (step > 0) run(S, S.t, S.t + step, step); + S.t += step; +} + +function palette(S) { + const h = S.hue + H0, hh = h - floor(h); + // Red, orange, yellow and lime go brown or olive when dark: there the + // polish deepens to a bright full colour instead, from a cream. + const dw = min(abs(hh - 0.12), 1 - abs(hh - 0.12)), warm = 1 - sm(0.12, 0.24, dw); + hsv(h, 0.32 - 0.1 * warm, 0.52 + 0.04 * warm, S.pale); + hsv(h + 0.015 * (1 - warm), 0.92 + 0.06 * warm, 0.11 + 0.5 * warm, S.deep); + hsv(h, 0.6 - 0.1 * warm, 0.5 + 0.4 * warm, S.tint); + hsv(h, 0.12, 1, S.pearl); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + palette(S); + const { a, g, ph, om, pale, deep, tint, pearl } = S, t = S.t, k = -OD * LN10, norm = 1 - exp(k); + const sig2 = 2 * (SIG * 1.2) ** 2; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x; + // How deep the colour is: Beer-Lambert in the switched dye. + const D = (1 - exp(k * a[i])) / norm; + let r = pale[0] + (deep[0] - pale[0]) * D; + let gg = pale[1] + (deep[1] - pale[1]) * D; + let b = pale[2] + (deep[2] - pale[2]) * D; + // The flake: pearl white in the pale polish; under the dark dye the light + // crosses the dye twice on its way to the flake and back, so the + // flake goes dim and takes the colour. Now and then a flake turns half + // away from the room light. + const gl = g[i] * (sin(ph[i] + t * om[i]) > -0.8 ? 1 : 0.5); + if (gl < 0) { + r *= 1 + 0.3 * gl; + gg *= 1 + 0.3 * gl; + b *= 1 + 0.3 * gl; + } else if (gl > 0) { + r += (pearl[0] + (tint[0] - pearl[0]) * D - r) * gl; + gg += (pearl[1] + (tint[1] - pearl[1]) * D - gg) * gl; + b += (pearl[2] + (tint[2] - pearl[2]) * D - b) * gl; + } + // The torch's violet spill. + if (S.on) { + const dx = x + 0.5 - S.tx, dy = y + 0.5 - S.ty, v = 0.12 * exp(-(dx * dx + dy * dy) / sig2); + r += v * 0.45; + b += v; + } + display.setPixel(x, y, round(255 * min(1, r)), round(255 * min(1, gg)), round(255 * min(1, b))); + } +} From 88d9c8460db6e42dac2836781a37d208001bc6da Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 20:56:58 +0000 Subject: [PATCH 10/13] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2038=ED=8C=90:=20Serum=20Fingers=20=EB=A5=BC=20=EB=8D=94?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../pattern-study/patterns/serum-fingers.js | 343 ++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 incoming/pattern-study/patterns/serum-fingers.js diff --git a/incoming/pattern-study/patterns/serum-fingers.js b/incoming/pattern-study/patterns/serum-fingers.js new file mode 100644 index 000000000..eb8b039e4 --- /dev/null +++ b/incoming/pattern-study/patterns/serum-fingers.js @@ -0,0 +1,343 @@ +// ===== Patternflow pattern ===== +// Title: Serum Fingers +// Family: cosmetic +// Idea: A thin bright serum is pressed in through a slot along the bottom of a layer of thick pale cream between two glass plates and pushes up through it in rounded fingers that bulge, split and compete, two or three leaders racing for the top while the rest stall, each LED turning from cream to serum once as a finger reaches it; when a finger reaches the top the cream closes back over the fingers, thin ones first, and a new press begins. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 38, after a thin serum pressed into thick cream between two glass plates (Saffman-Taylor viscous fingering, Philip Saffman and Geoffrey Taylor, 1958, in a Hele-Shaw cell, Henry Selby Hele-Shaw, 1898) +// Round: 38 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Fingers=-1..1, Inlet=0..1 +// +// Knob 1: Hue. Turns the serum round the whole wheel, the cream a faint tint of the opposite hue. At 0 aqua serum on warm grey cream; at 0.1 azure, at 0.2 violet, at 0.3 orchid, at 0.4 rose, at 0.5 coral, at 0.6 orange, at 0.7 lime, at 0.8 green, at 0.9 jade, each on cream tinted the other way. Faint growth rings in the serum, paler bands between deeper ones 12% dimmer, show where the front stood after every 300 LEDs pressed in. +// Knob 2: Speed. At 2 the serum goes in at 580 LEDs of area a second and a finger reaches the top 9 to 11 seconds into a press; the cell then holds for 1.5 seconds and the cream closes back over 3. At 0.2 all of it ten times slower, the fingers only climbing; at 3 one and a half times faster. +// Knob 3: Fingers. Surface tension, 9 times higher at -1 than at 1. At -1 the lip of the slot carries 3 waves and three fat round fingers rise side by side and seldom split; at 0 six waves, of which two or three fingers win, bulge and split into lobes; at 1 twelve thin fingers, one of which takes off with ragged sides and reaches the top about 2 seconds sooner. The waves of the lip change at the next press. +// Knob 4: Inlet. Where the serum comes in. Up to 0.06 a slot along the whole bottom; toward 1 the slot narrows to a half disc 5 LEDs across at the bottom centre and the serum rises from it as one broad plume whose sides bulge into lobes. Takes effect at the next press. +// Buttons: K1 tap: the cream closes back now and a new press begins. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, sqrt, cos, exp, 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]; +} + +// Serum pressed in, in LEDs of area a second at Speed 2. +const Q0 = 580; +// A fresh run opens with the first PRE seconds of a press already in. +const PRE = 0.6; +// The cream gives way at a front LED as the pressure drop across it to the +// power ETA, times the roughness of the glass there, times 1 + SIG phi, where +// phi is how much more of the disc round it is serum than at a flat front: +// below 0 at a fingertip, which is held back, above 0 in a crevice, which is +// filled up to CAP times faster. That is the surface tension that keeps the +// fingers round. SIG is for Fingers 0. +const ETA = 0.7, SIG = 2, CAP = 1.6; +// Gauss-Seidel sweeps of the pressure each frame, over-relaxed. +const SWEEPS = 12, OMEGA = 1.9; +// The disc the curvature is read over: cells within RC of a front cell. A +// flat front fills PHI0 of it with serum. +const RC = 3.2; +const DISC = []; +for (let dy = -3; dy <= 3; dy++) for (let dx = -3; dx <= 3; dx++) if (dx * dx + dy * dy <= RC * RC) DISC.push(dx, dy); +let PHI0 = 0; +{ + let a = 0, b = 0; + for (let k = 0; k < DISC.length; k += 2) { + b++; + if (DISC[k + 1] < 0) a++; + } + PHI0 = a / b; +} +// Opening hue: aqua serum. +const H0 = 0.5; + +export function setup(params) { + params.s = { + hue: 0, speed: 2, fingers: 0, inlet: 0, sigma: SIG, begun: false, + st: new Uint8Array(N), fill: new Float32Array(N), P: new Float32Array(N), + eta: new Float32Array(N), phi: new Float32Array(N), band: new Float32Array(N), fill0: new Float32Array(N), dist: new Float32Array(N), + seed: new Uint8Array(N), tex: new Float32Array(N), front: new Int32Array(N), nf: 0, + vol: 0, mode: 0, timer: 0, maxd: 1, + A: new Float32Array(4 * N), T: new Float32Array(4 * N), ser: [0, 0, 0], pale: [0, 0, 0], cream: [0, 0, 0], + }; + const S = params.s; + // The cream's own static mottle, a few soft lumps. + const lumps = []; + for (let k = 0; k < 14; k++) lumps.push([random() * W, random() * H, 6 + random() * 14, random() - 0.5]); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let v = 0; + for (const [lx, ly, r, a] of lumps) v += a * exp(-((x - lx) ** 2 + (y - ly) ** 2) / (r * r)); + S.tex[y * W + x] = v; + } +} + +// A fresh press: the inlet filled, the rest cream, new roughness in the glass. +function fresh(S) { + const { st, fill, P, eta, band, seed } = S; + st.fill(0); + fill.fill(0); + seed.fill(0); + // The inlet: a slot along the whole bottom at Inlet 0, narrowing to a half + // disc 5 LEDs across at the bottom centre at 1, its lip rounded at each + // end. Along the slot the lip is waved, from 3 waves across the panel at + // Fingers -1 to 12 at 1, so the fingers start at the width they would grow. + const half = 32 - 30 * S.inlet, RE = 2.6, nb = round(6 * 2 ** S.fingers), ph = random() * 2 * PI, amps = []; + for (let k = 0; k < 16; k++) amps.push(2 + 4 * random()); + for (let y = H - 6; y < H; y++) + for (let x = 0; x < W; x++) { + const dx = max(0, abs(x + 0.5 - W / 2) - (half - RE)), up = H - (y + 0.5); + const wv = (2 * PI * nb * (x + 0.5)) / W + ph, amp = amps[floor(wv / (2 * PI) + 0.5) % amps.length]; + const lip = dx > 0 ? sqrt(max(0, RE * RE - dx * dx)) : 2 + amp * (0.5 + 0.5 * cos(wv)); + if (dx <= RE && up <= lip) { + const p = y * W + x; + st[p] = 1; + fill[p] = 1; + seed[p] = 1; + band[p] = 0; + } + } + // Roughness of the glass: two octaves of smooth noise, 8 and 4 LEDs + // across, so the front is nudged at the scale of a finger, not per LED. + const g8 = [], g4 = []; + for (let k = 0; k < 9 * 17; k++) g8.push(random() - 0.5); + for (let k = 0; k < 17 * 33; k++) g4.push(random() - 0.5); + const vn = (g, s, gw, x, y) => { + const u = x / s, v = y / s, i = floor(u), j = floor(v), fu = u - i, fv = v - j, a = j * gw + i; + return (g[a] * (1 - fu) + g[a + 1] * fu) * (1 - fv) + (g[a + gw] * (1 - fu) + g[a + gw + 1] * fu) * fv; + }; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + eta[p] = 1 + 0.5 * vn(g8, 8, 9, x, y) + 0.3 * vn(g4, 4, 17, x, y); + P[p] = st[p] ? 1 : y / H; + } + S.vol = 0; + S.mode = 0; + S.timer = 0; +} + +// The front: every cream LED next to the serum, and phi there. +function boundary(S) { + const { st, fill, front, phi } = S; + let nf = 0; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + if (st[p]) continue; + if ((x > 0 && st[p - 1]) || (x < W - 1 && st[p + 1]) || (y > 0 && st[p - W]) || (y < H - 1 && st[p + W])) { + let s = 0, c = 0; + for (let k = 0; k < DISC.length; k += 2) { + const xx = x + DISC[k], yy = y + DISC[k + 1]; + if (xx < 0 || xx >= W || yy >= H) continue; + c++; + if (yy < 0) continue; + const q = yy * W + xx; + s += st[q] ? 1 : fill[q]; + } + phi[p] = s / c - PHI0; + front[nf++] = p; + } + } + S.nf = nf; +} + +// Over-relaxed sweeps of Laplace's equation for the pressure in the cream: +// 1 in the serum, which is so thin it holds one pressure throughout, 0 at the +// top edge, where the cream leaves the cell, and no flow through the sides +// and the bottom. +function relax(S, sweeps) { + const { st, P } = S; + for (let it = 0; it < sweeps; it++) + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + if (st[p]) continue; + let s = 0, c = 0; + if (y === 0) c++; + else { + s += st[p - W] ? 1 : P[p - W]; + c++; + } + if (y < H - 1) { + s += st[p + W] ? 1 : P[p + W]; + c++; + } + if (x > 0) { + s += st[p - 1] ? 1 : P[p - 1]; + c++; + } + if (x < W - 1) { + s += st[p + 1] ? 1 : P[p + 1]; + c++; + } + P[p] += OMEGA * (s / c - P[p]); + } +} + +const speed = (S, p) => max(0, 1 - S.P[p]) ** ETA * S.eta[p] * cl(1 + S.sigma * S.phi[p], 0, CAP); + +// Press in vol LEDs of serum, shared among the front LEDs by how fast the +// cream gives way at each. +function grow(S, vol) { + const { st, fill, P, band, front, nf } = S; + let tot = 0; + for (let k = 0; k < nf; k++) { + const p = front[k]; + tot += speed(S, p); + } + if (tot <= 0) return; + for (let k = 0; k < nf; k++) { + const p = front[k], v = speed(S, p); + if (v <= 0) continue; + if (fill[p] === 0) band[p] = S.vol; + fill[p] += (vol * v) / tot; + if (fill[p] >= 1) { + fill[p] = 1; + st[p] = 1; + P[p] = 1; + // A finger at the top edge: the cell is full. + if (p < 2 * W) S.mode = 1; + } + } + S.vol += vol; +} + +// The cream closing back: every serum LED's distance to the cream, so the +// fingers thin from their sides and the trunks go last. +function startClear(S) { + const { st, fill, fill0, dist, seed } = S; + for (let p = 0; p < N; p++) { + fill0[p] = fill[p]; + dist[p] = fill[p] >= 1 && !seed[p] ? 1e9 : 0; + } + const D = 1, DD = 1.414; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + if (!dist[p]) continue; + let d = dist[p]; + if (x > 0) d = min(d, dist[p - 1] + D); + if (y > 0) { + d = min(d, dist[p - W] + D); + if (x > 0) d = min(d, dist[p - W - 1] + DD); + if (x < W - 1) d = min(d, dist[p - W + 1] + DD); + } + dist[p] = d; + } + let m = 1; + for (let y = H - 1; y >= 0; y--) + for (let x = W - 1; x >= 0; x--) { + const p = y * W + x; + if (!dist[p]) continue; + let d = dist[p]; + if (x < W - 1) d = min(d, dist[p + 1] + D); + if (y < H - 1) { + d = min(d, dist[p + W] + D); + if (x < W - 1) d = min(d, dist[p + W + 1] + DD); + if (x > 0) d = min(d, dist[p + W - 1] + DD); + } + dist[p] = d; + if (d < 1e8 && d > m) m = d; + } + S.maxd = m; + S.mode = 2; + S.timer = 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.fingers = cl(k[2], -1, 1); + S.inlet = max(0, cl(k[3], 0, 1) - 0.06) / 0.94; + const rate = S.speed / 2; + // Surface tension: 6 at Fingers -1, 2 at 0, 0.67 at 1. + S.sigma = SIG * 3 ** -S.fingers; + if (!S.begun) { + // Open with the first PRE seconds of a press already in, as at Speed 2. + fresh(S); + boundary(S); + relax(S, 200); + for (let i = 0; i < PRE * 30; i++) { + boundary(S); + relax(S, SWEEPS); + grow(S, Q0 / 30); + } + S.begun = true; + } + if (input.btnPressed && input.btnPressed[0] && S.mode < 2) startClear(S); + if (S.mode === 0 || S.mode === 1) { + boundary(S); + relax(S, SWEEPS); + if (S.mode === 0) grow(S, Q0 * rate * d); + else { + S.timer += d * rate; + if (S.timer > 1.5) startClear(S); + } + } else if (S.mode === 2) { + S.timer += d * rate; + const u = S.timer / 3, R = (S.maxd + 1.5) * u * u; + const { fill, fill0, dist, st, seed } = S; + for (let p = 0; p < N; p++) { + if (seed[p] || fill0[p] <= 0) continue; + const f = min(fill0[p], cl(dist[p] - R, 0, 1)); + fill[p] = f; + st[p] = f >= 1 ? 1 : 0; + } + if (u >= 1) { + fresh(S); + boundary(S); + relax(S, 60); + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.begun) return; + const { fill, band, tex, ser, pale, cream, A, T } = S; + const h = S.hue + H0; + hsv(h, 0.72, 1, ser); + hsv(h + 0.06, 0.3, 1, pale); + hsv(h + 0.5, 0.1, 0.28, cream); + // The serum's colour, times how full each LED is, and the fill itself, + // softened by a 3 by 3 blur so the fingers' edges are round on the LEDs. + // Each LED's serum colour is set once, by how much had been pressed in + // when the serum got there: faint growth rings. + for (let p = 0; p < N; p++) { + const f = fill[p], o = 4 * p; + if (f <= 0) { + A[o] = A[o + 1] = A[o + 2] = A[o + 3] = 0; + continue; + } + const ring = 0.5 + 0.5 * cos((band[p] / 300) * 2 * PI), kk = (0.88 + 0.12 * ring) * f; + A[o] = (ser[0] + (pale[0] - ser[0]) * ring * 0.5) * kk; + A[o + 1] = (ser[1] + (pale[1] - ser[1]) * ring * 0.5) * kk; + A[o + 2] = (ser[2] + (pale[2] - ser[2]) * ring * 0.5) * kk; + A[o + 3] = f; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const o = 4 * (y * W + x), l = x > 0 ? o - 4 : o, r = x < W - 1 ? o + 4 : o; + for (let c = 0; c < 4; c++) T[o + c] = 0.25 * A[l + c] + 0.5 * A[o + c] + 0.25 * A[r + c]; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, o = 4 * p, u = y > 0 ? o - 4 * W : o, d = y < H - 1 ? o + 4 * W : o; + const f = 0.25 * T[u + 3] + 0.5 * T[o + 3] + 0.25 * T[d + 3], t = (1 + tex[p] * 0.25) * (1 - f); + const r = cream[0] * t + 0.25 * T[u] + 0.5 * T[o] + 0.25 * T[d]; + const g = cream[1] * t + 0.25 * T[u + 1] + 0.5 * T[o + 1] + 0.25 * T[d + 1]; + const b = cream[2] * t + 0.25 * T[u + 2] + 0.5 * T[o + 2] + 0.25 * T[d + 2]; + display.setPixel(x, y, round(255 * min(1, r)), round(255 * min(1, g)), round(255 * min(1, b))); + } +} From fa906cbba7f944480cc4c985d37a904bf12fc046 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 21:02:41 +0000 Subject: [PATCH 11/13] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2038=ED=8C=90:=20Sperner's=20Lemma=20=EA=B0=80=20=EC=B6=95=20?= =?UTF-8?q?=EB=81=9D=20=EC=95=88=EC=9C=BC=EB=A1=9C=20=EB=93=A4=EC=96=B4?= =?UTF-8?q?=EC=98=A8=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/sperner-lemma.js | 54 ++++++++++++++----- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/incoming/pattern-study/patterns/sperner-lemma.js b/incoming/pattern-study/patterns/sperner-lemma.js index 6c1b3966e..55b332e42 100644 --- a/incoming/pattern-study/patterns/sperner-lemma.js +++ b/incoming/pattern-study/patterns/sperner-lemma.js @@ -12,7 +12,7 @@ // @knobs Mesh=12..28, Speed=0.2..3, Swirl=0..1, Ripple=0..1 // // Knob 1: Mesh. Rooms along each side: 12 at the start (144 rooms), 20 at the middle (400), 28 at the top (784). The finer the mesh, the smaller the rainbow room, the closer it sits on the fixed point and the more the walk wanders; at 28 the territories' borders are nearly smooth curves. -// Knob 2: Speed. At 2 the push of the map leans up to 60 degrees either way and back every 21 seconds, so the three territories rock round the fixed point, the fixed point goes round its loop in 27 seconds, and a bright head runs the walk at 36 LEDs a second. At 0.2 all of it ten times slower, at 3 one and a half times faster. +// Knob 2: Speed. At 2 the push of the map leans up to 60 degrees either way and back every 12.6 seconds, so the three territories rock round the fixed point and 9 to 16 corners change label every second at Mesh 12, the fixed point goes round its loop in 14 seconds, and a bright head with a glow 7 LEDs round runs the walk at 36 LEDs a second. At 0.2 all of it ten times slower, at 3 one and a half times faster. // Knob 3: Swirl. Twists the map round its fixed point more the further out it reaches. At 0 the three territories are wedges meeting at the fixed point; at 0.5 their borders curl about half a turn round it; at 1 most of a turn, and the walk from the base runs along the bottom and round before it climbs in. // Knob 4: Ripple. Adds a wavy push to the map, which gives it more fixed points. Up to 0.06 none: one fixed point and one rainbow room. At 0.25 three to seven rainbow rooms, at 1 five to eleven, always an odd number; islands of one label float in another, the walk from the base reaches one rainbow room and dim walks join the others two by two. @@ -41,16 +41,20 @@ const G1 = hypot(E3y - E2y, E2x - E3x) / abs(DET); // SWIRL radians per unit of distance at Swirl 1, and is STEP long from R0 // out, shrinking to nothing at c. const STEP = 0.12, R0 = 0.35, SWIRL = 3.4; +// Radius of the glow round the head of the walk, in LEDs. +const GLOW = 7; // The ripple: two waves added to d, up to RIP times STEP. const RIP = 3.5; -// Label colours, the territories at FILL of full: 1 azure, 2 rose, 3 green. +// Label colours: 1 azure, 2 rose, 3 green. Each at its own brightness +// (brightest channel 0.85, 0.62 and 0.4 of full), so a room that changes +// label changes in brightness too and not only in hue; green at 0.4 still +// looks about as bright to the eye as azure at 0.85. const COL = [ - [0.12, 0.42, 1], - [1, 0.18, 0.3], - [0.2, 0.95, 0.28], + [0.12 * 0.85, 0.42 * 0.85, 0.85], + [0.62, 0.18 * 0.62, 0.3 * 0.62], + [0.2 * 0.4 / 0.95, 0.4, 0.28 * 0.4 / 0.95], ]; -const FILL = 0.7; function hsv(h, s, v, out) { h = (h - floor(h)) * 6; @@ -268,15 +272,15 @@ export function update(dt, input, params) { S.t += (d * S.speed) / 2; S.head += d * S.speed * 18; // The fixed point of the map without ripple wanders inside the triangle, - // its barycentric coordinates never under 0.12, round its loop in 27 + // its barycentric coordinates never under 0.12, round its loop in 14 // seconds at Speed 2. - const t = S.t, rr = 0.13 + 0.08 * sin(0.37 * t + 0.6), ph = 0.23 * t + 1.1; + const t = S.t, rr = 0.13 + 0.08 * sin(0.6 * t + 0.6), ph = 0.45 * t + 1.1; const c1 = 1 / 3 + rr * cos(ph), c2 = 1 / 3 + rr * cos(ph - TAU / 3), c3 = 1 - c1 - c2; S.cx = c2 - c1; S.cy = R3 * c3; // The push leans up to 60 degrees either side of straight at c, back and - // forth every 21 seconds at Speed 2. - S.turn = 1.05 * sin(0.3 * t); + // forth every 12.6 seconds at Speed 2. + S.turn = 1.05 * sin(0.5 * t); label(S); walkAll(S); } @@ -350,9 +354,9 @@ export function draw(display, params) { continue; } const wall = 1 - lw * wl; - R[p] = c[0] * FILL * wall * 255; - G[p] = c[1] * FILL * wall * 255; - B[p] = c[2] * FILL * wall * 255; + R[p] = c[0] * wall * 255; + G[p] = c[1] * wall * 255; + B[p] = c[2] * wall * 255; } // The other walks dim; the main one brighter, with a head running from // the base door into the rainbow room and a pause there before it runs again. @@ -366,6 +370,30 @@ export function draw(display, params) { const back = hs - s; return back < 0 ? 0.62 : 0.62 + 0.38 * Math.exp(-back / 9); }); + // A soft glow rides with the head, GLOW LEDs round, so the run up the + // walk shows from across the room and not only on the thin line. + if (hs < L) { + let s = 0, hx = pts[0][0], hy = pts[0][1]; + for (let k = 1; k < pts.length; k++) { + const ex = pts[k][0] - pts[k - 1][0], ey = pts[k][1] - pts[k - 1][1], len = hypot(ex, ey); + if (s + len >= hs) { + const u = len > 0 ? (hs - s) / len : 0; + hx = pts[k - 1][0] + u * ex; + hy = pts[k - 1][1] + u * ey; + break; + } + s += len; + } + for (let y = max(0, floor(hy - GLOW)); y <= min(H - 1, floor(hy + GLOW)); y++) + for (let x = max(0, floor(hx - GLOW)); x <= min(W - 1, floor(hx + GLOW)); x++) { + const dd = hypot(x + 0.5 - hx, y + 0.5 - hy) / GLOW, p = y * W + x; + if (dd >= 1 || room[p] < 0 || rb.has(room[p])) continue; + const g = 0.7 * (1 - dd) * (1 - dd); + R[p] += (255 - R[p]) * g; + G[p] += (255 - G[p]) * g; + B[p] += (255 - B[p]) * g; + } + } // A halo round the room the walk ends in, flaring as the head arrives. const ctr = centroid(n, corners(S.main.end)), fl = hs > L ? Math.exp(-(hs - L) / 12) : 0; const rad = 5 + 3 * fl + 60 / n, amp = 0.3 + 0.5 * fl; From 40c7d6f0eabe74ad7d3155d57df5596ce87367c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 21:05:53 +0000 Subject: [PATCH 12/13] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2038=ED=8C=90:=20README=20=EC=97=90=2038=ED=8C=90=20=ED=91=9C?= =?UTF-8?q?=EC=99=80=20=EC=A0=88,=20=EB=8B=A4=EC=9D=8C=20=ED=8C=90=20?= =?UTF-8?q?=EA=B0=90=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 Claude-Session: https://claude.ai/code/session_013eaaLKBuXCHpZzU9162GoH --- 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 f83618d9a..cd8746a58 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 268개를 쉰다섯 갈래로 나눠 만들었어요. 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판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 278개를 쉰다섯 갈래로 나눠 만들었어요. 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판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. [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판에서 세로로 다시 짰어요. @@ -444,6 +444,21 @@ | [Shimmer Oil](patterns/shimmer-oil.js) | 화장품 팝업 | 보이지 않는 손끝이 마이카가 든 베리빛 바디 오일을 저으면, 납작한 조각들이 늘어난 자리에서는 일어서고 돌기만 하는 자리에서는 누운 채라 소용돌이 자체가 진주빛 실크 결로 보여요. K3 은 젓는 힘이라 1 에서는 소용돌이가 서로 말려 들며 결이 촘촘히 접혀요. K1 을 누르면 다른 손끝이 내려와 20 LED 쯤의 나선을 감아요. | | [Whipped Soap](patterns/whipped-soap.js) | 화장품 팝업 | 보이지 않는 별 깍지가 휘핑 바디 솝을 한 송이씩 짜는데, 한 바퀴가 앞 바퀴에 반쯤 얹히며 뾰족하게 솟아요. 쟁반이 두 칸 네 줄로 차면 긁개가 거품을 말아 아래로 밀어 내요. K3 은 깍지의 별 꼭짓점 수라서 굵은 골 두세 줄이 가는 골 네다섯 줄로 바뀌고, K1 을 누르면 쟁반을 바로 밀어요. | +### 38판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Anderson Localization](patterns/anderson-localization.js) | 물결·간섭 | 위치 에너지가 무작위로 놓인 사슬의 한 자리에 양자 입자를 놓고, 자리를 가로로, 시간을 세로로 그려요. 깨끗한 사슬이라면 점선을 따라 패널 끝까지 벌어졌을 원뿔이 27 LED 너비에서 멈추고, 얼룩진 기둥으로 끝까지 서 있어요. K3 은 무질서를 키워 기둥을 12 LED 까지 좁히고, K1 을 누르면 새 에너지와 새 자리에서 바로 다시 시작해요. | +| [Bernoulli Convolutions](patterns/bernoulli-convolutions.js) | 프랙탈 | 128줄 하나하나가 λ 하나에 대한 ±1 ±λ ±λ² … 의 분포예요. 위의 칸토어 먼지에서 아래의 매끈한 종까지 항이 하나 더해질 때마다 모든 점이 둘로 쪼개지고, λ 가 피소 수의 역수인 열네 줄에서는 합이 같은 점에 거듭 내려앉아 흰 금빛으로 타요. K3 은 동전을 한쪽으로 기울여 종을 오른쪽 벽으로 밀고, K1 을 누르면 항을 다 걷었다가 첫 항부터 다시 쌓아요. | +| [Couette Shimmer](patterns/couette-shimmer.js) | 화장품 팝업 | 두 원통 사이에 든 펄 세럼을 옆에서 봐요. 안쪽 원통이 빨라지면 옆으로 흐르던 결이 테일러 소용돌이의 밝고 어두운 띠로 128줄을 쌓고, 더 빨라지면 띠가 물결치며 원통을 돌아요. K3 은 띠를 다섯 쌍에서 열네 쌍으로 늘려 가는 골로 만들고, K4 는 바깥 원통을 거꾸로 돌려 띠를 나선으로 기울여요. | +| [Figure-Eight Orbit](patterns/figure-eight-orbit.js) | 물리·스프링 | 같은 질량 셋이 뉴턴 중력으로 패널 높이만 한 8자 하나를 3분의 1주기씩 떨어져 돌아요. 식으로 그린 곡선이 아니라 한 걸음씩 푼 궤도라서, K4 로 처음 속도를 밀면 8자가 흔들리다가 더 밀면 둘이 짝을 짓고 하나가 패널 밖으로 튕겨 나가요. K3 은 꼬리를 늘려 세 혜성을 겹친 8자 띠로 바꾸고, K1 을 누르면 도는 궤도를 살짝 밀어요. | +| [Infinity Series](patterns/infinity-series.js) | 리듬·펄스 | 페르 뇌르고르의 무한 수열이 네 줄 피아노 롤에 한 박에 1, 2, 4, 8음으로 떨어져 타격선에 닿아요. 박마다 네 줄이 함께 닿는데, 4음 줄은 1음 줄과 같은 음에, 2음과 8음 줄은 그 거울상에 닿아요. K3 은 한 박의 줄 수를 8 에서 40 으로 늘려 음을 길게 펴고, K4 는 수열의 더 먼 자리에서 시작해요. | +| [Milk Ring](patterns/milk-ring.js) | 화장품 팝업 | 우윳빛 크림 방울이 색을 띤 세럼에 떨어져 테두리가 밝은 소용돌이 고리가 돼요. 고리는 가라앉으며 부풀다 작은 방울 몇 개로 갈라지고, 그 방울이 다시 작은 고리가 되어 가는 크림 실에 매달린 나무처럼 패널 아래로 늘어져요. K3 은 한 고리가 갈라지는 방울 수를 셋에서 여섯으로 늘리고, K1 을 누르면 방울이 바로 떨어져요. | +| [Photochromic](patterns/photochromic.js) | 화장품 팝업 | LED 마다 맨 폴리시이거나 펄 조각인 옅은 매니큐어 위로 보이지 않는 UV 손전등이 구불구불 쓸고 지나가면, 지나간 자리가 순식간에 짙은 보석 색으로 바뀌었다가 몇 초에 걸쳐 다시 옅어져요. K3 은 펄 조각을 한 LED 에서 네 LED 너비로 키워 굵은 글리터로 바꾸고, K1 을 누르면 지난번과 반대쪽으로 한 번 더 쓸어요. | +| [Pólya Urn](patterns/polya-urn.js) | 데이터 그림 | 폴리아 항아리 64개가 한 열에 하나씩 나란히 공을 뽑아요. 로그 시계를 따라 위에서는 크게 흔들리다 아래로 가며 저마다 다른 섞임 비율로 굳고, 한 판이 끝나면 열들이 굳은 비율대로 줄을 서서 고르게 퍼진 극한을 곧은 그러데이션으로 보여 줘요. K3 은 처음 공을 늘려 열들을 가운데 섞임으로 모으고, K4 는 한 번에 더하는 공을 늘려 바코드를 두 색으로 갈라요. | +| [Serum Fingers](patterns/serum-fingers.js) | 화장품 팝업 | 유리판 두 장 사이 진한 크림에 묽은 세럼을 밑에서 밀어 넣으면, 둥근 손가락이 부풀고 갈라지며 위로 다투어 올라가요. LED 는 세럼이 닿을 때 한 번만 바뀌고, 한 손가락이 꼭대기에 닿으면 크림이 가는 손가락부터 다시 덮고 새로 밀어요. K3 은 표면 장력을 낮춰 굵은 손가락 셋을 가는 손가락 열둘로 바꾸고, K1 을 누르면 크림이 바로 다시 덮어요. | +| [Sperner's Lemma](patterns/sperner-lemma.js) | 위상·매듭 | 세운 삼각형을 작은 방으로 나누고, 꼭짓점마다 연속 사상이 가장 많이 줄이는 좌표의 번호를 붙여 파랑, 빨강, 초록 영토로 칠해요. 흰 걸음이 밑변의 1-2 문으로 들어와 방마다 다른 문으로 나가다가 세 번호를 다 가진 무지개 방에서 멈추고, 그 방은 사상의 고정점 위에 있어요. K3 은 사상을 고정점 둘레로 비틀어 영토 경계를 감고, K4 는 고정점을 늘려 무지개 방을 홀수 개로 늘려요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -1726,6 +1741,50 @@ E8 의 근 240개를 고셋의 4₂₁ 다면체 꼭짓점으로 그렸어요. 주제 6 을 겨눴어요. 별 깍지의 단면을 ρ(φ) = 1 − c + c(1 − |sin(nφ/2)|)² 로 두어 꼭짓점은 모나고 골은 둥글게 했고, 이 단면을 나선 길을 따라 높이장에 찍어요. 검산에서 n 이 5, 7, 10 일 때 꼭짓점과 골이 n 개씩 나왔어요. 한 바퀴는 깍지 너비만큼 앞서 깔린 거품 위에 얹혀서, Swirl 이 −1, 0, 1 일 때 가장 높은 점이 5.3, 11.7, 16.9 LED 로 솟아요. 처음엔 짜는 송이 하나만 바뀌어서 움직임이 0.012 로 축 끝 0.019 아래였어요. 빛을 7초마다 ±0.5 라디안 흔들고 그림자는 세 방위의 지평선 기울기를 미리 재 두니 0.031 이 됐고, 한 프레임은 1.6 ms 로 남았어요. 쟁반도 처음엔 모두 24/255 위라 채움이 1.000 이었고, 쟁반을 낮춰 송이 그림자가 꺼지게 하니 0.976 이 됐어요. 세 칸 여섯 줄로 짜 보니 송이 하나가 21 LED 라서 패널에서는 보풀 덩어리로만 보였어요. 두 칸 네 줄로 바꿔 한 칸을 32 LED 로 키우니 나선이 읽혔어요. 별 꼭짓점의 결을 길 방향 그대로 두면 1~2 LED 간격의 동심원이 되어 송이가 동전처럼 보였어요. 깍지를 길 1 LED 마다 0.11 라디안씩 돌리니 결이 바퀴를 비스듬히 건너면서 휘핑 거품으로 보였어요. 잰 값은 움직임 0.031, 가장자리 0.364, 채움 0.976, 모양 바뀜 0.070 이에요. +## 38판 + +38판도 주제 5 「낯선 수학」 여섯과 주제 6 「화장품 브랜드 팝업」 넷이에요. 같이 돈 37판(#231)이 포천의 쓸기 선과 앙투안의 목걸이, 태피 크림 같은 것을 먼저 골라서, 이 판은 그와 겹치지 않게 골랐어요. 수학은 Anderson Localization, Figure-Eight Orbit, Bernoulli Convolutions, Sperner's Lemma, Pólya Urn, Infinity Series 이고, 화장품은 Milk Ring, Couette Shimmer, Photochromic, Serum Fingers 예요. 수학 여섯은 37판이 쓴 여섯 갈래를 빼고 수학 패턴이 적은 갈래부터 하나씩 넣어서, 물결·간섭, 물리·스프링, 프랙탈, 위상·매듭, 데이터 그림, 리듬·펄스에 하나씩 들어갔어요. 여섯 다 「다음 판 감」의 목록 밖에서 찾았고, 주제 2 로 연 갈래는 없어요. Sperner's Lemma 가 위상·매듭 갈래의 여섯째예요. 화장품 넷은 다 「다음에 해 볼 질감」에 적혀 있던 감이에요. 젤에 떨어져 잠기는 크림 방울, 테일러-쿠에트 흐름에 넣은 쉬머, 포토크로믹 네일, 사프만-테일러 불안정이에요. 주제 1 은 268개의 지도 21장이 다 `none` 이라 30판부터처럼 패턴이 하나뿐인 칸을 겨눴고, 37판이 겨눈 가장자리 × 움직임 1,4 와 채움 × 모양 바뀜 2,4 는 피했어요. Serum Fingers 가 Day and Night 혼자이던 움직임 × 모양 바뀜 1,3 에, Photochromic 이 Bait Ball 혼자이던 촘촘함 × 모양 바뀜 3,4 에 섰어요. 패턴이 하나뿐인 칸은 36판까지의 268개에서 16 이었고 278개에서 14 예요. 열 개 다 어느 축의 끝도 넘지 않아서 칸 경계는 30판 그대로이고, 지도 21장이 다 `none` 이에요. 주제 4 는 Anderson Localization 과 Milk Ring 이에요. Anderson Localization 은 시간이 128줄을 내려가서 `thirds` 가 55/64/52 이고, 뒤의 위치 에너지 줄무늬를 빼고 재도 29/40/38 이에요. Milk Ring 은 세럼이 다 켜져 100/100/100 이라, 2~8초 사이 어느 때든 밝기가 24 넘게 바뀐 LED 를 세면 위, 가운데, 아래가 26/48/51 이에요. 다섯 일꾼이 둘씩 짰고, 본체가 연구의 Lab 하네스로 다시 재고 화면을 그려 보고 합쳤어요. + +### Anderson Localization + +주제 4 와 5 로 앤더슨 국소화를 세웠어요. 무작위 위치 에너지가 놓인 1차원 사슬의 한 자리에 입자를 놓고 확률을 시간을 따라 아래로 그려서, 원뿔이 열리다가 멈추는 것을 위에서 아래로 읽어요. 192자리 사슬을 RK4 로 풀고 가운데 64자리만 보여서 128줄 안에는 벽에서 되돌아온 파동이 들어오지 않고, 일곱 판 동안 노름은 1 에서 1.0e-4 안에 머물렀어요. 무질서가 없을 때는 t = 20 에서 |J_n(2t)|² 와 2e-8 안으로 같았어요. 잰 값은 움직임 0.071, 채움 0.574, `thirds` 55/64/52 이고, 줄무늬를 빼고 재도 29/40/38 이라 파동만으로 세 칸을 다 써요. 처음에는 Disorder 를 3 에서 열고 밝기를 1e-4 부터 로그로 올렸더니, 지수로 줄어드는 꼬리가 패널 폭을 거의 다 채워서 멈춘 것이 보이지 않았어요. 바닥을 1e-3 으로 올리고 Disorder 를 4 에서 열자 켜진 폭이 32줄째 25칸, 48줄째부터 27~28칸에 멈췄어요. 깨끗한 원뿔의 점선은 같은 48줄에서 49칸으로 계속 벌어져서, 둘이 갈라지는 자리가 국소화로 읽혀요. 표본 평균만 바꾸는 K4 는 0.03 언저리였고, 줄무늬가 표본 수에 반비례해 옅어지게 하자 0.05 가 됐어요. + +### Figure-Eight Orbit + +주제 5 로 세 몸 문제의 8자 해를 세웠어요. Chenciner 와 Montgomery 의 시작 값에서 세 물체를 요시다의 4차 도약 적분으로 한 걸음씩 풀어서, 여는 노브에서 에너지가 200주기 동안 시작 값에서 9.3e-9 안에 머물러요. 잰 값은 움직임 0.268, 채움 0.152, `thirds` 14/16/14 이고 노브 반응은 0.08, 0.09, 0.06, 0.10 이에요. 처음에는 꼬리 셋을 screen 으로 겹쳐서 Trail 2 에서 8자가 하얀 고리가 됐고, 오래된 꼬리부터 칠해 새 꼬리가 위에 오게 하자 세 색이 남았어요. 그러자 K3 반응이 0.04 로 떨어져서, 꼬리 머리의 반폭을 3.3 에서 3.6 LED 로 넓히고 끝으로 갈수록 더 빨리 가늘어지게 해 0.06 으로 올렸어요. Kick 은 처음에 물체 1 과 2 를 밀기만 해서 에너지가 올라갔고, 궤도가 1.39 까지 커져 Kick 0.5 에서도 3초 안에 화면을 벗어났어요. 민 뒤 속도를 원래 운동 에너지로 되돌리자 Kick 0.6 까지 71주기를 버텼어요. 도는 중에 미는 버튼은 0.05 로도 90초 안에 8자를 깨서, 시작 배치의 대칭을 지키는 밀기만 그렇게 너그럽다는 것이 보였어요. + +### Bernoulli Convolutions + +주제 5 로 베르누이 합성곱을 세웠어요. 줄마다 λ 하나를 맡아 ±1 ±λ ±λ² … 의 분포를 그 합이 닿는 범위 전체에 펴서, 위 0.32 의 칸토어 먼지에서 1/2 언저리의 평평한 줄을 지나 아래 0.89 의 종까지 이어져요. 잰 값은 움직임 0.105, 촘촘함 0.078, 색 0.238, 모양 바뀜 0.050 이고 `thirds` 는 68/96/77, 노브 반응은 0.31, 0.05, 0.31, 0.13 이에요. 처음에는 수렴한 밀도를 줄마다 1024칸으로 셈했는데 황금비 줄이 이웃 줄과 구별되지 않았어요. 황금비의 측도는 특이하지만 차원이 0.9957 이라, 4096칸으로 셈해도 거칠기가 0.106 대 0.072 만큼만 달랐어요. 그래서 피소 수의 역수 열넷에 가장 가까운 줄을 그 값으로 옮기고, 처음 n항의 2^n 개 합이 정확히 겹치는 수를 세어 빛으로 바꿨어요. 13항에서 한 점이 품은 부호 선택이 황금비 줄은 평균 10.5, 플라스틱 수 줄은 36 이고, λ 가 이진 소수인 나머지 줄은 늘 1 이라 이음매가 한 줄 두께로 또렷이 서요. 항을 일곱부터 쌓으면 모양 바뀜이 0.017 로 축 끝 0.021 아래라서 여섯 항부터 쌓아요. + +### Sperner's Lemma + +주제 5 로 슈페르너 보조정리를 위상·매듭 갈래의 여섯째로 세웠어요. 꼭짓점마다 사상이 가장 많이 줄이는 무게중심 좌표의 번호를 붙여서, 번호가 붙는 순간부터 슈페르너 꼴이에요. 처음에는 사상이 미는 방향을 통째로 돌렸는데, 미는 쪽이 90도를 넘어 바깥을 향하면 삼각형 안으로 되돌리는 최근접점 사영이 모서리와 변에 고정점을 만들었어요. 그래서 세 모서리에 늘 무지개 방이 켜지고 걸음이 꼭대기로 달아났어요. 가장 작은 무게중심 좌표가 0.16 아래인 띠에서 돌림과 물결을 0 으로 줄이자 고정점이 안쪽에 하나만 남고 무지개 방도 하나가 됐어요. 12칸 메시에서 무지개 방은 5×10 LED 쯤이라 둘레 빛이 덮으면 흰 점으로만 보여서, 방의 벽을 희게 긋고 둘레 빛은 방 밖에만 비춰요. 첫 판은 움직임 0.011, 모양 바뀜 0.015 로 두 축의 아래 끝을 넘었어요. 세 영토의 가장 밝은 채널이 다 178 쯤이라 방의 번호가 바뀌어도 잰 값에 남지 않았어요. 영토마다 밝기를 0.85, 0.62, 0.4 로 나누고 사상이 12.6초마다 흔들리게 하고 걸음 머리에 반지름 7 LED 의 빛을 달자 0.041 과 0.045 가 됐어요. 노브 60 자리의 7,740 프레임을 따로 확인했더니 번호가 슈페르너 꼴을 어긴 적이 없고, 무지개 방 수는 늘 홀수였고, 밑변에서 들어간 걸음은 늘 무지개 방에서 끝났어요. + +### Pólya Urn + +주제 5 로 폴리아 항아리를 세웠어요. 항아리 64개가 한 열씩 맡고 줄 r 은 2001^(r/127) − 1 번째 뽑기라서, 첫 뽑기가 12번째 줄, 열 번째가 40번째 줄, 백 번째가 77번째 줄에 와요. 잰 값은 움직임 0.039, 촘촘함 0.006, 채움 1.000, 색 0.429, 모양 바뀜 0.182 이고, `thirds` 는 100/100/100, 노브 반응은 0.35, 0.10, 0.06, 0.07 이에요. 처음 판은 5000번까지 뽑으며 20번째 줄에서 이미 일곱 번을 뽑아서, 흔들리는 부분이 맨 위 15줄에 눌리고 판 전체가 곧은 줄무늬로만 보였어요. 시계를 2000번까지로 줄이고 처음을 늦추니 위 3분의 1이 흔들림을 맡았어요. 극한이 고르게 퍼진다는 것은 열이 제자리에 있으면 보이지 않아서, 한 판이 끝나면 열들이 맨 아래 줄의 비율 차례로 2.5초 동안 자리를 바꿔 줄을 서요. 지난 판을 30% 로 어둡게 깔았을 때는 앞줄이 내려가며 모양 바뀜이 0.433 으로 축 끝 0.413 을 넘었는데, 줄 선 그러데이션을 70% 밝기로 깔자 0.182 로 내려오고 새 바코드가 매끈한 바탕 위에서 더 잘 읽혔어요. 섞인 색을 가장 밝은 쪽으로 반쯤 끌어올려 올리브를 피했더니 밝기가 거의 고르게 되어 촘촘함이 0.012 에서 0.006 으로 줄었어요. + +### Infinity Series + +주제 5 로 페르 뇌르고르의 무한 수열을 세웠어요. 네 줄은 같은 선율을 한 박에 1, 2, 4, 8음으로 치고, 박마다 4음 줄은 1음 줄과 같은 음에, 2음과 8음 줄은 거울상 음에 닿아서 a(2n) = −a(n) 과 a(4n) = a(n) 이 화면에 그대로 서요. 한 빠르기로 a(sn) 을 읽으면 1음 줄과 4음 줄이 그냥 같은 줄이라 왜 같은지 보이지 않아서, 줄마다 빠르기를 달리했어요. 잰 값은 움직임 1.250, 촘촘함 0.083, 채움 0.338, 모양 바뀜 0.058, `thirds` 33/33/33 이고 노브 반응은 0.07, 0.12, 0.11, 0.14 예요. 처음 판은 음을 1열 굵기로 그리고 박 사이 음을 0.38 밝기 회색으로 둬서 채움이 0.129, K1 반응이 0.03 이었어요. 4음과 8음 줄의 1행짜리 음은 1음 줄의 7행짜리 블록과 견줄 수가 없었어요. 음을 3열로 넓히고 빠른 줄마다 박 위의 음을 한 박 내내 흐리게 깔아 두니, 4음 줄에는 1음 줄과 같은 블록이, 2음과 8음 줄에는 뒤집힌 블록이 서서 한눈에 맞아떨어졌어요. 120 bpm 에 한 박 8행이면 한 프레임에 8/15행씩 내려가서 1행짜리 음이 반올림으로 0행이 되는 프레임마다 사라졌고, 위아래 끝을 다 반올림해서 고쳤어요. + +### Milk Ring + +주제 4 와 6 으로 젤에 떨어져 잠기는 크림 방울을 세웠고, K1 하나로 세럼 색이 색상환을 다 돌아요. 세럼이 어디서나 켜져 있어 `thirds` 는 100/100/100 이라 밝기가 24 넘게 바뀐 LED 의 몫을 따로 쟀어요. 2초와 8초 두 화면 사이로는 위, 가운데, 아래가 12/18/29 이고, 2~8초 사이 어느 때든 바뀐 것으로는 26/48/51 이에요. 첫 고리는 47줄쯤, 둘째 대는 76줄쯤에서 깨지고, 마지막 고리는 117줄쯤에서 세럼에 녹아 128줄을 다 써요. 처음에는 고리 심을 반지름의 4분의 1 두께로 그리고 크림을 진하게 쌓았더니, 하나하나가 하얀 알약처럼 꽉 차서 고리로 읽히지 않았어요. 심을 반지름의 8분의 1로 줄이고, 앞쪽 반을 뒤쪽보다 밝게 하고, 고리 높이를 폭의 0.4 로 해서 가운데로 세럼이 보이게 하자 18칸 너비, 7칸 높이의 납작한 타원으로 읽혔어요. 작은 방울을 테두리에서 1초에 7칸씩 바깥으로 던지자 실이 위의 고리로 휘어 올라가는 가지가 생겨 해파리 같은 나무가 됐어요. 노랑과 라임 세럼은 아래쪽이 올리브로 보여서, 색상 0.03~0.37 에서 아래쪽 밝기를 2.1배까지 올렸어요. 잰 값은 움직임 0.073, 촘촘함 0.021, 복잡도 1.560, 색 0.117, 모양 바뀜 0.053 이에요. + +### Couette Shimmer + +주제 6 으로 두 원통 사이에서 저은 쉬머 세럼을 세웠어요. K1 이 색상환을 다 돌고 라일락에서 열려요. 바탕이 다 켜져서 `thirds` 는 99/99/99 이고, 2초와 8초 사이에 밝기가 24 넘게 바뀐 LED 의 몫은 위, 가운데, 아래가 35/28/32 예요. 잰 값은 움직임 0.210, 복잡도 2.577, 색 0.150 이고 노브 반응은 0.32, 0.15, 0.17, 0.19 예요. 처음 그림은 물결 진폭이 띠 한 쌍 높이의 0.2 라서 지그재그 벽지처럼 읽혔어요. 0.13 으로 줄이고 띠마다 세기가 천천히 달라지게 하고 결 무늬가 각 소용돌이를 따라 유입 경계 쪽으로 흐르게 하자 유체로 읽혔어요. 반짝이는 운모 조각은 유입 경계에 모여 점선처럼 줄을 섰는데, 거기서 안쪽 원통으로 빨려 들어가 다른 곳에서 다시 나오게 해서 풀었어요. 실험실에서는 물결 띠가 임계 속도의 열 배쯤, 난류가 스무 배쯤에서 오는데, Spin 2.6 과 3 으로 당겨 노브 하나에 다 담았어요. Hue 0.3 언저리 주황에서는 어두운 띠가 황갈색으로 보여서, 빨강부터 라임까지는 어두운 띠도 밝기 0.85 를 지키고 채도로만 갈려요. + +### Photochromic + +주제 1 과 6 으로 포토크로믹 매니큐어를 세웠고, 겨눈 칸은 Bait Ball 혼자이던 촘촘함 × 모양 바뀜 3,4 예요. 촘촘함이 0.189~0.297 이면서 2~8초에 블록 평균이 0.331 넘게 바뀌어야 하는 칸이에요. 처음 판은 촘촘함 0.127, 모양 바뀜 0.130 이었고 복잡도가 2.762 로 축 끝 2.735 를 넘었어요. 안료를 연속값 결로 깔고 반짝임을 세제곱으로 세우니 압축은 안 되면서 이웃 차는 작았고, 시작 전 붓질의 자국이 Beer-Lambert 꼬리 때문에 2초 화면까지 남아 있었어요. LED 를 맨 폴리시 40%, 빛을 받는 조각 38%, 돌아앉아 밑을 0.7배로 가리는 조각 22% 로 나누고, 짙은 상태의 바탕을 0.11 로 낮추자 옅은 상태와 짙은 상태의 블록 평균 차가 0.45 쯤으로 벌어졌어요. 앞 붓질을 시작 8.4초 전으로 밀고 주기를 13초로 늘리고 빛을 넓혀 4.4초에 LED 의 80% 가 바뀌게 하니, 0.160/0.289 와 0.195/0.387 을 거쳐 촘촘함 0.210, 모양 바뀜 0.365 로 칸 안에 들었어요. 나머지는 움직임 0.131, 복잡도 1.470, 색 0.106 이고 노브 반응은 0.13, 0.30, 0.23, 0.19 예요. 주황과 노랑 쪽에서는 짙어지는 대신 크림색에서 밝은 원색으로 바뀌어서, 칸은 여는 보라 언저리에서만 지켜요. + +### Serum Fingers + +주제 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초 뒤에 시작해요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -1768,7 +1827,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 24일 `dev`(`c18bee4`)의 258개에 36판의 열 개를 더한 268개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 24일 `dev`(`3f5d8c6`)의 268개에 38판의 열 개를 더한 278개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -1788,6 +1847,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 36판의 열 개도 어느 축의 끝을 넘지 않아서 칸 경계는 30판 그대로이고, 268개에서도 지도 21장이 다 `none` 이에요. 주제 1 은 패턴이 하나뿐인 칸을 겨눠서, 35판까지의 258개에서 19 이던 그런 칸이 16 이 됐어요. 채움 × 모양 바뀜 1,4 는 LED 의 70% 넘게 꺼진 채 2~8초에 블록 평균이 0.331 넘게 바뀌어야 하는 칸이에요. Curve Shortening 은 곡선 단축 흐름에서 넓이가 흐름 시간 1 에 꼭 2π 씩 줄어드는 것을 썼어요. 2초에 패널의 36.5% 를 거의 꽉 찬 밝기로 채운 곡선이 7.5초쯤 점으로 사라지면, 채움은 시작의 절반쯤인 0.194 에 남고 모양 바뀜은 시작 넓이만큼인 0.375 가 돼요. 첫 판은 넓이 4209 로 모양 바뀜 0.487 이라 축 끝 0.413 을 넘었어요. Heawood Torus 가 처음 겨눈 색 × 모양 바뀜 4,3 은 같이 돈 35판의 Bath Fizz 가 먼저 채웠어요. 색은 꺼진 LED 까지 세서, 토러스가 패널을 43% 만 덮은 첫 판은 일곱 나라를 채도 끝까지 칠해도 0.416 이었어요. 관을 굵게 해 패널의 반을 덮고 그늘 쪽 밝기를 0.6 으로 올려 0.62 를 냈어요. 모양 바뀜은 2초 화면이 정면에 가까울수록 커서, 2초에 46도 돈 자리에서 0.275, 정면을 1초에 지나게 하자 0.378 이에요. 그 값으로 색 × 모양 바뀜 4,4(Marbling)와 채움 × 모양 바뀜 2,4(Bokeh)에 함께 섰어요. 회전하는 물체는 2초 화면의 자세 하나로 모양 바뀜을 0.1 넘게 옮겨서, 다른 값을 거의 건드리지 않고 칸을 고를 수 있어요. 남은 16칸 가운데 다섯은 모양 바뀜이 높은 촘촘함의 칸이에요. 촘촘함 × 모양 바뀜 2,4, 3,3, 3,4, 4,3, 4,4 의 Marbling, Flipdot, Bait Ball, Sequins, Pin Art 예요. +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 사이의 좁은 띠를 써야 해요. + 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 를 냈어요. @@ -1802,7 +1863,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ### 아직 없는 갈래 -`families.json` 의 쉰다섯 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 29판이 연 위상·매듭 갈래는 Hopf Fibration 과 31판의 Boy's Surface, 33판의 Borromean Rings, 34판의 Lakes of Wada, 36판의 Heawood Torus 예요. 28판이 열려다 접은 갈래라 28판의 Hairy Ball 은 노이즈·흐름에 그대로 있어요. 세잎 매듭처럼 이름 있는 매듭이 그 갈래의 여섯째로 맞아요. 29판은 화장품 팝업 갈래도 열었어요. 주제 6 의 몫이라 주제 2 로 세지 않았고, Cream Swirl, Serum Gel, Glitter Fall, Loose Powder 넷이 다 들어 있어요. 30판의 Swatch Row, Pearl Sheen, Bi-Phase, Satin Fold 가 그 갈래에 넷을 더했고, 31판의 Lip Gloss, Foam Lather, Setting Mist, Sponge Blend 가 넷을 더 더해 열둘이에요. 32판의 Cushion Mesh, Clay Mask, Sugar Scrub, Chrome Powder 가 넷을 더해 열여섯이고, 33판의 Cleansing Balm, Gold Leaf, Essence Drop, Peel-Off 가 넷을 더해 스물이에요. 34판의 Cat Eye, Velvet Nap, Mood Tint, Hair Gloss 가 넷을 더해 스물넷이고, 35판의 Bath Fizz, Toner Pad, Sheet Mask, Gilded Cream 이 넷을 더해 스물여덟이고, 36판의 Lotion Ribbon, Shimmer Oil, Blooming Gel, Whipped Soap 이 넷을 더해 서른둘이에요. 28판이 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table, 33판의 Goldbach Comet, 35판의 Kolakoski, 36판의 Elliptic Chords 예요. 27판이 연 철도 갈래는 Funicular 하나이고, 필름·영화 갈래는 Horse in Motion 하나예요. 두 갈래는 주제 표가 수학 쪽으로 바뀌기 전에 열었어요. 그 뒤로 ROUND.md 는 사물로 묶는 갈래를 더 열지 않아요. 26판이 연 얼음·극지 갈래는 Iceberg 와 27판의 Sled Dogs 이고, 미생물 갈래는 Dark Field 와 27판의 Mitosis 예요. 25판이 연 농사·밭 갈래는 Rice Terraces 와 26판의 Rice Planting, 27판의 Center Pivot 이고, 화산 갈래는 Eruption 과 26판의 Geyser, 27판의 Lava Lake 예요. 24판이 연 공사·건설 갈래는 Tower Crane 과 25판의 Excavation, 26판의 Implosion 이에요. 24판이 연 도자기 갈래는 Potter's Wheel 과 25판의 Kintsugi, 26판의 Noborigama 예요. 23판이 연 무대·공연 갈래는 Proscenium 과 24판의 Flying Trapeze, 25판의 Marionette, 26판의 Shadow Play 예요. 23판이 연 짐승 갈래는 Giraffe 와 24판의 Emergence, 25판의 Snow Monkeys 예요. 22판이 연 몸 갈래는 Circulation 과 23판의 Axon, 24판의 Breath, 27판의 Fluoroscope 예요. 22판이 연 비행 갈래는 Liftoff 와 23판의 Final Approach, 24판의 Thermal, 25판의 Skydiver 예요. 20판이 연 새 갈래는 Kingfisher 와 21판의 Woodpecker, 22판의 Weaver, 23판의 Skein 이에요. 20판이 연 화학·실험 갈래는 Chromatography 와 21판의 Chemical Garden, 22판의 Flame Test, 23판의 Elephant Toothpaste 예요. 21판이 연 종이 갈래는 Pop-up Book 과 22판의 Senbazuru, 23판의 Hanging Scroll, 26판의 Origami 예요. 21판이 연 운동·경기 갈래는 Lap Pool 과 22판의 Bowling Lane, 23판의 High Dive, 26판의 Pole Vault, 27판의 Climbing Wall 이에요. 위아래로 움직이는 경기는 High Dive 와 Pole Vault 뒤로 27판에 실내 암벽이 섰어요. 19판이 연 부엌 갈래는 Popcorn 과 20판의 Kettle, 21판의 Pancakes 이고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판이 연 유리 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori, 27판의 Glassblower 예요. 광물·결정 갈래는 Bismuth 와 19판의 Dripstone, 20판의 Agate 이고, 놀이공원 갈래는 Drop Tower 와 19판의 Helter Skelter, 20판의 High Striker, 25판의 Ferris Wheel 이에요. 말이 오르내리는 회전목마는 아직 없어요. 17판이 연 전자 회로 갈래는 Circuit Board 와 18판의 Server Rack, 21판의 Nixie 이고, 장난감 갈래는 Marble Run 과 18판의 Peg Light, 21판의 Slinky, 22판의 Pin Art 예요. 16판이 연 바다 갈래는 Abyss 와 17판의 Shoreline, 20판의 Sunglint, 23판의 Bait Ball 이고, 곤충 갈래는 Fireflies 와 17판의 Ant Farm, 20판의 Moth Lamp, 24판의 Waggle Dance 예요. 15판이 연 도시 갈래는 Metro Map 과 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판이 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables, 24판의 Pipe Organ 이에요. 4판이 연 직물 갈래는 Jacquard Loom 과 15판의 Plait, 19판의 Needlepoint, 23판의 Sequins 이고, 8판이 연 기계 갈래는 Gear Train 과 15판의 Cam Wave, 19판의 Paternoster 예요. 12판이 연 천체 갈래는 Star Trails 와 15판의 Analemma, 16판의 Lunation, 22판의 Galilean Moons 예요. 위성의 궤도는 Galilean Moons 가 그렸고, 해를 도는 행성은 아직 없어요. 리듬·펄스 갈래는 1판의 Euclid Pulse 와 Pulse Bars 뒤로 20판의 Change Ringing, 32판의 Arnold Tongues, 35판의 Chimera State 가 섰어요. +`families.json` 의 쉰다섯 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 29판이 연 위상·매듭 갈래는 Hopf Fibration 과 31판의 Boy's Surface, 33판의 Borromean Rings, 34판의 Lakes of Wada, 36판의 Heawood Torus, 38판의 Sperner's Lemma 예요. 28판이 열려다 접은 갈래라 28판의 Hairy Ball 은 노이즈·흐름에 그대로 있어요. 세잎 매듭처럼 이름 있는 매듭이 그 갈래의 일곱째로 맞아요. 29판은 화장품 팝업 갈래도 열었어요. 주제 6 의 몫이라 주제 2 로 세지 않았고, Cream Swirl, Serum Gel, Glitter Fall, Loose Powder 넷이 다 들어 있어요. 30판의 Swatch Row, Pearl Sheen, Bi-Phase, Satin Fold 가 그 갈래에 넷을 더했고, 31판의 Lip Gloss, Foam Lather, Setting Mist, Sponge Blend 가 넷을 더 더해 열둘이에요. 32판의 Cushion Mesh, Clay Mask, Sugar Scrub, Chrome Powder 가 넷을 더해 열여섯이고, 33판의 Cleansing Balm, Gold Leaf, Essence Drop, Peel-Off 가 넷을 더해 스물이에요. 34판의 Cat Eye, Velvet Nap, Mood Tint, Hair Gloss 가 넷을 더해 스물넷이고, 35판의 Bath Fizz, Toner Pad, Sheet Mask, Gilded Cream 이 넷을 더해 스물여덟이고, 36판의 Lotion Ribbon, Shimmer Oil, Blooming Gel, Whipped Soap 이 넷을 더해 서른둘이에요. 38판의 Milk Ring, Couette Shimmer, Photochromic, Serum Fingers 가 넷을 더해 서른여섯이에요. 28판이 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table, 33판의 Goldbach Comet, 35판의 Kolakoski, 36판의 Elliptic Chords 예요. 27판이 연 철도 갈래는 Funicular 하나이고, 필름·영화 갈래는 Horse in Motion 하나예요. 두 갈래는 주제 표가 수학 쪽으로 바뀌기 전에 열었어요. 그 뒤로 ROUND.md 는 사물로 묶는 갈래를 더 열지 않아요. 26판이 연 얼음·극지 갈래는 Iceberg 와 27판의 Sled Dogs 이고, 미생물 갈래는 Dark Field 와 27판의 Mitosis 예요. 25판이 연 농사·밭 갈래는 Rice Terraces 와 26판의 Rice Planting, 27판의 Center Pivot 이고, 화산 갈래는 Eruption 과 26판의 Geyser, 27판의 Lava Lake 예요. 24판이 연 공사·건설 갈래는 Tower Crane 과 25판의 Excavation, 26판의 Implosion 이에요. 24판이 연 도자기 갈래는 Potter's Wheel 과 25판의 Kintsugi, 26판의 Noborigama 예요. 23판이 연 무대·공연 갈래는 Proscenium 과 24판의 Flying Trapeze, 25판의 Marionette, 26판의 Shadow Play 예요. 23판이 연 짐승 갈래는 Giraffe 와 24판의 Emergence, 25판의 Snow Monkeys 예요. 22판이 연 몸 갈래는 Circulation 과 23판의 Axon, 24판의 Breath, 27판의 Fluoroscope 예요. 22판이 연 비행 갈래는 Liftoff 와 23판의 Final Approach, 24판의 Thermal, 25판의 Skydiver 예요. 20판이 연 새 갈래는 Kingfisher 와 21판의 Woodpecker, 22판의 Weaver, 23판의 Skein 이에요. 20판이 연 화학·실험 갈래는 Chromatography 와 21판의 Chemical Garden, 22판의 Flame Test, 23판의 Elephant Toothpaste 예요. 21판이 연 종이 갈래는 Pop-up Book 과 22판의 Senbazuru, 23판의 Hanging Scroll, 26판의 Origami 예요. 21판이 연 운동·경기 갈래는 Lap Pool 과 22판의 Bowling Lane, 23판의 High Dive, 26판의 Pole Vault, 27판의 Climbing Wall 이에요. 위아래로 움직이는 경기는 High Dive 와 Pole Vault 뒤로 27판에 실내 암벽이 섰어요. 19판이 연 부엌 갈래는 Popcorn 과 20판의 Kettle, 21판의 Pancakes 이고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판이 연 유리 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori, 27판의 Glassblower 예요. 광물·결정 갈래는 Bismuth 와 19판의 Dripstone, 20판의 Agate 이고, 놀이공원 갈래는 Drop Tower 와 19판의 Helter Skelter, 20판의 High Striker, 25판의 Ferris Wheel 이에요. 말이 오르내리는 회전목마는 아직 없어요. 17판이 연 전자 회로 갈래는 Circuit Board 와 18판의 Server Rack, 21판의 Nixie 이고, 장난감 갈래는 Marble Run 과 18판의 Peg Light, 21판의 Slinky, 22판의 Pin Art 예요. 16판이 연 바다 갈래는 Abyss 와 17판의 Shoreline, 20판의 Sunglint, 23판의 Bait Ball 이고, 곤충 갈래는 Fireflies 와 17판의 Ant Farm, 20판의 Moth Lamp, 24판의 Waggle Dance 예요. 15판이 연 도시 갈래는 Metro Map 과 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판이 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables, 24판의 Pipe Organ 이에요. 4판이 연 직물 갈래는 Jacquard Loom 과 15판의 Plait, 19판의 Needlepoint, 23판의 Sequins 이고, 8판이 연 기계 갈래는 Gear Train 과 15판의 Cam Wave, 19판의 Paternoster 예요. 12판이 연 천체 갈래는 Star Trails 와 15판의 Analemma, 16판의 Lunation, 22판의 Galilean Moons 예요. 위성의 궤도는 Galilean Moons 가 그렸고, 해를 도는 행성은 아직 없어요. 리듬·펄스 갈래는 1판의 Euclid Pulse 와 Pulse Bars 뒤로 20판의 Change Ringing, 32판의 Arnold Tongues, 35판의 Chimera State, 38판의 Infinity Series 가 섰어요. ### 멋진 것 더 멋지게 @@ -1842,9 +1903,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 36판에서는 Schwarz Lantern 이 원기둥을 114 LED 높이로 세워 69/80/71 이에요. 띠가 3 LED 아래로 얇아지면 접힌 면이 가로 줄무늬로 뭉개져서, 띠를 60개까지 늘리던 첫 판을 40개에서 멈췄어요. Gibbs Phenomenon 은 줄이 부분합의 항 수라 71/91/99 예요. 항 수를 줄 번호의 제곱으로 두었을 때는 3항 아래가 위 4분의 1을 차지해서, 25줄마다 두 배가 되는 로그 눈금으로 바꿨어요. Lotion Ribbon 은 위 3분의 1에 줄기만 있어서 줄기가 그 17% 쯤을 켜고, 나머지는 벽의 빛이라 87/100/100 이에요. Curve Shortening 은 곡선 하나가 판 가운데에 서서 14/31/11 이고, Catastrophe Machine 은 고정점과 원판, 마름모를 위에서 아래로 놓았는데도 선 그림이라 10/30/17 이에요. +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 예요. + ### 낯선 수학 -주제 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)예요. +주제 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)예요. 38판도 여섯을 더했어요. 앤더슨 국소화(Anderson Localization), 무어가 찾고 Chenciner 와 Montgomery 가 증명한 세 몸 문제의 8자 해(Figure-Eight Orbit), 예센과 빈트너, 에르되시의 베르누이 합성곱(Bernoulli Convolutions), 브라우어르의 고정점 정리를 증명하는 슈페르너 보조정리(Sperner's Lemma), 에겐베르거와 폴리아의 항아리(Pólya Urn), 뇌르고르의 무한 수열(Infinity Series)이에요. 목록 밖에서 다음에 해 볼 것이에요. 영 도형의 플랑셰렐 성장은 상자가 하나씩 붙으며 로건-셰프와 베르시크-케로프의 극한 곡선(1977)으로 다가가요. 아즈텍 다이아몬드의 북극권과 같은 극한 모양이라 같은 판에 두지 않아요. 슈타이너 사슬은 반전 하나로 고리의 원들이 미끄러지는 포리즘이고, 민코프스키의 물음표 함수(1904)는 페리 나무를 이진 나무로 펴는 계단이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -1866,6 +1929,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 36판에서 본 것이에요. 정리가 수를 못 박는 대상은 이번에도 그 수를 셈으로 지켜봤어요. Curve Shortening 의 넓이는 흐름 시간 1 에 6.27 씩 줄어 2π 보다 0.2% 작았고, 걸음을 2.5배 줄이자 6.281 이 돼서 차이가 걸음 크기에서 온 것을 알았어요. Gibbs Phenomenon 의 맨 아래 줄은 1.1790 까지 올라 1.178980 에 닿았고, Heawood Torus 는 한 바퀴 도는 동안 LED 위에 나타난 경계로 21쌍을 다 셌어요. 교과서의 치수가 패널에 맞지 않기도 했어요. 지먼의 기계를 흔히 쓰는 고정점 거리 2 로 두면 마름모의 위 뾰족점이 원판 안에 가려져서, 2.5 로 옮겨서야 네 뾰족점이 다 보였어요. 극한을 말하는 대상은 이번에도 LED 한 칸에서 멈췄어요. Schwarz Lantern 의 넓이는 띠 40개에서 원기둥의 1.88배까지만 오르고, Gibbs Phenomenon 의 맨 아래 뿔은 점프에서 반 칸 떨어져서 부표본의 최댓값을 써야 남았어요. 유한체 위의 직선은 x 를 하나씩 늘려 그리면 흩뿌린 모래였고, 라그랑주 축소로 찾은 가장 짧은 격자 벡터로 걸어야 판을 감아 도는 줄로 읽혔어요. 회전체 토러스에 그린 헤우드의 지도는 둘레가 관보다 2.6배 길어서 육각형이 비스듬히 감긴 띠로 늘어나요. +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초 안에 깨졌어요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 모자 타일(스미스, 마이어스, 캐플런, 굿먼-스트라우스, 2023)은 한 가지 타일로만 평면을 비주기로 덮고, 뒤집어 놓인 타일이 판에 드문드문 흩어져요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 뷔퐁의 바늘(1777)은 Bertrand Paradox 와 같은 꼴로, 금을 넘는 바늘의 몫이 2/π 로 모이는 막대와 함께 세울 수 있어요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. @@ -1876,6 +1941,8 @@ 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 의 마름모 자리에 뾰족점 둘과 자기 교차 하나를 가진 경계가 생겼다 사라져요. +목록 밖에서 38판 뒤에 해 볼 것이에요. 오브리-앙드레 모형(세르주 오브리, 길 앙드레, 1980)은 Anderson Localization 의 무작위 에너지를 무리수 주기의 코사인으로 바꾼 사슬이라, 세기 2 에서 모든 상태가 한꺼번에 퍼짐에서 갇힘으로 넘어가요. 같은 시공간 그림에 그 문턱을 노브로 걸 수 있어요. 수바코프와 드미트라시노비치(2013)가 찾은 세 몸의 주기 궤도 열세 갈래는 나비와 나방, 요구르트 같은 이름이 붙은 꼴이라 Figure-Eight Orbit 의 적분을 그대로 써요. 게일(1979)이 보인 헥스 정리는 칸이 다 찬 헥스 판에는 꼭 한쪽이 이기는 길이 있다는 것으로, 슈페르너 보조정리처럼 브라우어르의 고정점 정리와 같은 힘이에요. 마름모 판을 세워 두 색이 차오르며 이긴 길이 켜지는 그림이 돼요. 쉬츠와 트림퍼의 코끼리 걸음(2004)은 지난 걸음 가운데 하나를 골라 따르는 기억 걸음이라, 따르는 확률 3/4 에서 퍼짐이 바뀌어요. Pólya Urn 처럼 64열을 나란히 둘 수 있어요. + ### 화장품 팝업 주제 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 가 생겨요. @@ -1894,7 +1961,9 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 36판이 넷을 더 썼어요. 쏟아져 똬리를 트는 로션 줄기(Lotion Ribbon), 마이카가 흐름을 따라 서는 쉬머 오일(Shimmer Oil), 젖은 베이스에 번지는 블루밍 젤 네일(Blooming Gel), 별 깍지로 짠 휘핑 바디 솝(Whipped Soap)이에요. 넷 다 「다음에 해 볼 질감」 밖에서 골랐어요. 질감마다 그 물리를 셈으로 따르자 사물로 읽혔어요. Lotion Ribbon 의 줄기는 질량 흐름을 지키며 가늘어지고 리브의 법칙(2004)으로 감기고, Shimmer Oil 의 조각은 제퍼리의 방정식(1922)으로 돌고, Blooming Gel 의 젤은 다공질 매질 방정식으로 번져요. 가장 밝은 채널만 읽는 잣대가 이번에도 질감을 놓쳤어요. Blooming Gel 의 첫 판은 옅은 바탕에 밝은 파란 틴트를 얹어 움직임 0.011, 촘촘함 0.003 으로 두 축의 아래 끝을 넘었고, 틴트를 명도 0.58 로 짙게 하자 0.037 과 0.014 가 됐어요. Shimmer Oil 은 조각마다 방향을 따로 주면 1 LED 안에서 결이 후추처럼 깨져서, LED 마다 방향의 퍼짐을 행렬 하나로 들고 다녀야 실크로 읽혔어요. Whipped Soap 은 세 칸 여섯 줄로 짜면 송이 하나가 21 LED 라 보풀 덩어리였고, 두 칸 네 줄로 키워서야 나선이 보였어요. 주황과 노랑 쪽의 갈색과 올리브는 넷 다에서 다시 왔어요. Lotion Ribbon 과 Whipped Soap 은 그 색상 범위에서 바탕을 거의 검게 내리고 크림을 옅게 했고, Shimmer Oil 은 오일을 밝기 0.82 로 아예 빛나게 두었어요. Lotion Ribbon 과 Whipped Soap 은 첫 판의 채움이 축 끝 1.000 이었어요. 위 두 모서리와 송이 그림자 아래 쟁반을 꺼서 0.958 과 0.976 으로 내렸어요. -다음에 해 볼 질감이에요. 세팅 미스트가 유리판에 맺혀 굵어지고 흘러내리는 것은 Lip Gloss 와 Setting Mist 사이라 같은 판에 두지 않아요. 매니큐어 붓이 남긴 줄이 스스로 펴지는 것은 Cream Swirl 과 가까워서 같은 판에 두지 않아요. 젖은 립글로스나 탑코트가 굳는 것은 Clay Mask 처럼 광이 먼저, 색이 나중에 빠지는 두 문턱으로 그릴 만해요. 립밤이 체온에 녹아 번지거나 고체 향수가 문질러 펴지는 것은 Cleansing Balm 의 녹는 경계를 가로로 돌린 그림이에요. 크림 한 방울이 젤 위에 떨어져 천천히 잠기는 것은 Essence Drop 의 거리장을 그대로 써요. 크러시드 벨벳처럼 결이 조각마다 다른 방향으로 눌린 천을 보이지 않는 손이 쓰다듬어 조각이 하나씩 같은 방향으로 눕는 것은 Velvet Nap 의 결 벡터를 그대로 써요. 빛에 짙어지는 포토크로믹 네일은 보이지 않는 UV 손전등이 지나간 자리만 짙어졌다가 몇 초에 걸쳐 옅어져서, 바탕이 옅고 흔적이 짙은 Mood Tint 의 거꾸로예요. 헤어 오일 한 방울이 가닥을 따라 끝으로 미끄러지며 지나간 가닥만 한 톤 짙어지고 윤이 오래 남는 것은 Hair Gloss 의 가닥 위에 그릴 수 있어요. 물 위에 매니큐어 고리를 떨어뜨려 겹치고 바늘로 끄는 워터 마블 네일은 Bath Fizz 의 유체를 그대로 써요. 기름종이가 피지를 빨아들인 자리만 반투명해지는 것은 Toner Pad 의 길이장으로 그릴 만해요. 하이드로겔 마스크처럼 두껍고 맑은 젤 시트가 붙는 것은 Sheet Mask 의 젖는 선을 그대로 써요. 두 색 크림을 꼬아 늘이는 마블링은 Gilded Cream 의 접기 대신 핀 셋이 감아 도는 태피 당김으로 그릴 만해요. 움직이는 판 위로 떨어지는 점성 줄기는 판이 빨라질수록 똬리가 구불거리는 선으로, 다시 8자 매듭으로 바뀌어요. 치우-웹스터와 리스터의 유체 재봉틀(2006)로 컨베이어 위에 깔리는 로션 띠를 그릴 만해요. Shimmer Oil 의 마이카 장을 두 원통 사이의 테일러-쿠에트 흐름(1923)에 넣으면 세운 패널에 가로 띠 소용돌이가 층층이 쌓여요. 묽은 층이 젖은 바탕으로 밀고 들어가며 손가락처럼 갈라지는 사프만-테일러 불안정(1958)은 마블 네일의 깃털 무늬예요. 위에서 본 비누 거품은 변이 n 개인 방이 (n − 6) 에 비례해 커지거나 줄어드는 폰 노이만의 법칙(1952)으로 굵어져서, Foam Lather 와 다른 질감이 돼요. +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 의 결 벡터를 그대로 써요. 헤어 오일 한 방울이 가닥을 따라 끝으로 미끄러지며 지나간 가닥만 한 톤 짙어지고 윤이 오래 남는 것은 Hair Gloss 의 가닥 위에 그릴 수 있어요. 물 위에 매니큐어 고리를 떨어뜨려 겹치고 바늘로 끄는 워터 마블 네일은 Bath Fizz 의 유체를 그대로 써요. 기름종이가 피지를 빨아들인 자리만 반투명해지는 것은 Toner Pad 의 길이장으로 그릴 만해요. 하이드로겔 마스크처럼 두껍고 맑은 젤 시트가 붙는 것은 Sheet Mask 의 젖는 선을 그대로 써요. 두 색 크림을 꼬아 늘이는 마블링은 Gilded Cream 의 접기 대신 핀 셋이 감아 도는 태피 당김으로 그릴 만해요. 움직이는 판 위로 떨어지는 점성 줄기는 판이 빨라질수록 똬리가 구불거리는 선으로, 다시 8자 매듭으로 바뀌어요. 치우-웹스터와 리스터의 유체 재봉틀(2006)로 컨베이어 위에 깔리는 로션 띠를 그릴 만해요. 위에서 본 비누 거품은 변이 n 개인 방이 (n − 6) 에 비례해 커지거나 줄어드는 폰 노이만의 법칙(1952)으로 굵어져서, Foam Lather 와 다른 질감이 돼요. 세럼이 스포이트에서 실로 늘어지다 구슬로 끊기는 레일리-플라토 불안정(조제프 플라토, 1873; 레일리 경, 1878)은 위에서 아래로 늘어지는 실과 구슬 줄이라 세운 패널에 맞아요. 토너나 향수 잔 벽을 타고 오르내리는 와인의 눈물(제임스 톰슨, 1855; 카를로 마랑고니, 1865)도 기어오르는 막과 흘러내리는 줄기로 128줄을 써요. 두 유리판 사이의 크림을 떼어 낼 때 공기가 가운데로 파고들며 나뭇가지처럼 갈라지는 것은 Serum Fingers 의 성장 법칙을 바깥에서 안으로 돌려 그릴 만해요. ## 다시 돌리기 From 8b68adb788dbd4e804643fb7eb800772d1d86fc8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 21:24:03 +0000 Subject: [PATCH 13/13] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2038=ED=8C=90:=20index.html=20=EC=9D=84=20278=EA=B0=9C?= =?UTF-8?q?=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20=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_013eaaLKBuXCHpZzU9162GoH --- 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 cd8746a58..384c8835d 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1881,6 +1881,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 36판의 열 개는 가장 낮은 노브가 Elliptic Chords 의 K4 Trail 로 네 자리로 0.0281 이에요. 2~3초 창은 꼬리가 쌓이기 전이라, 꼬리 길이를 바꾸는 노브는 첫 화면에 쌓인 이력이 없을 때 0.008 이었어요. 한 바퀴의 80% 가 켜진 채 열리게 하자 0.028 이 됐어요. Lotion Ribbon 의 K2 Speed 와 Blooming Gel 의 K4 Rim 도 0.03 이에요. Catastrophe Machine 은 가는 선만 그린 판에서 노브 넷이 0.02~0.03 이었고, 원판에 함께 도는 부채꼴 여섯 칸을 칠하자 0.04~0.05 가 됐어요. 선 그림은 노브가 바꾸는 LED 가 적어서, 함께 움직이는 면 하나를 두면 노브 넷이 다 올라요. +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 으로 올렸어요. + Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. ### 세로 64×128 diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index 34123964e..64efbfbd5 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -141,7 +141,7 @@

패턴 연구

- +