From b0c22129ad734c5f5e2bdd47077ccca80a2ea604 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 15:28:38 +0000 Subject: [PATCH 1/9] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=20=EC=97=B0=EA=B5=AC=2034=ED=8C=90=20(pfpat=2032?= =?UTF-8?q?=EB=B0=94=ED=80=B4)?= 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_011BuccZdVDjYHZnwdNh4X1F From 392d61fceebf0ee4fe6e87b65144a3cf0bfda59c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 16:11:31 +0000 Subject: [PATCH 2/9] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=203?= =?UTF-8?q?4=ED=8C=90:=20Scheidegger=20Rivers,=20Hair=20Gloss?= 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_011BuccZdVDjYHZnwdNh4X1F --- incoming/pattern-study/patterns/hair-gloss.js | 215 +++++++++++++ .../patterns/scheidegger-rivers.js | 302 ++++++++++++++++++ 2 files changed, 517 insertions(+) create mode 100644 incoming/pattern-study/patterns/hair-gloss.js create mode 100644 incoming/pattern-study/patterns/scheidegger-rivers.js diff --git a/incoming/pattern-study/patterns/hair-gloss.js b/incoming/pattern-study/patterns/hair-gloss.js new file mode 100644 index 000000000..5f46f6c8a --- /dev/null +++ b/incoming/pattern-study/patterns/hair-gloss.js @@ -0,0 +1,215 @@ +// ===== Patternflow pattern ===== +// Title: Hair Gloss +// Family: cosmetic +// Idea: Long glossy hair hangs the full height of the panel, 230 strands swaying in a light breeze that runs down them, with a white shine band across it where the strands lie square to the half vector and a weaker band in the hair's own colour below it, both sliding up and down as the hair sways; every 9 seconds a comb runs down through it, the strands part round its teeth and lie smoother and shinier behind. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 34, after the shine band of glossy hair in haircare displays; the highlights are a fibre highlight (James Kajiya and Timothy Kay, 1989), the white one shifted toward the root and a tinted one toward the tip (Stephen Marschner and others, 2003) +// Round: 34 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Wave=0..1, Shine=-1..1 +// +// Knob 1: Hue. The hair colour, once round the wheel, while the shine band stays near white and the band below it takes the hair's colour. At 0 violet, at 0.1 orchid, at 0.3 copper, at 0.33 caramel, at 0.36 honey blonde (from 0.25 to 0.47 the hair is lifted so it never reads muddy), at 0.6 green, at 0.7 teal, at 0.85 blue. +// Knob 2: Speed. At 2 the breeze runs a wave down the hair every 4.6 s, swinging the ends up to 4 LEDs, the shine band rises and falls between rows 26 and 60 every 6.5 s, and a comb comes every 9 s and takes 2.4 s to pass; at 0.2 all of it ten times slower, at 3 one and a half times faster. +// Knob 3: Wave. At 0 the hair is straight and carries one band. Toward 1 it waves, 44 rows a wave and up to 3 LEDs to each side, and each wave catches its own highlight: two bands at 0.5, three or four at 1. +// Knob 4: Shine. How glossy the finish is. At 0 the band is about 9 rows tall; at -1 it is 17 rows and dimmer, with a soft sheen over 50 rows round it, like matte hair; at 1 it is 4 rows and brighter, its edges ragged strand by strand, and the coloured band stands apart below it. +// Buttons: K1 tap sends the comb through now. + +const W = 64, H = 128, N = 230, Y0 = -2, ROWS = 160; +const { floor, min, max, sqrt, sin, cos, exp, log, round, random, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => { + const t = cl((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + const rgb = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + out[0] = rgb[0]; + out[1] = rgb[1]; + out[2] = rgb[2]; +} + +// Light from above, a little left and in front; the eye straight on. y runs +// down and z comes out of the panel. +const norm3 = (x, y, z) => { + const l = sqrt(x * x + y * y + z * z); + return [x / l, y / l, z / l]; +}; +const [LX, LY, LZ] = norm3(-0.15, -0.55, 0.82); +const [HX, HY, HZ] = norm3(LX, LY, LZ + 1); +// The sheet of hair leans toward the eye by tz per row. The primary highlight +// sits where a strand's tangent is square to the half vector, tz about 0.3. +const TZ0 = -HY / HZ; + +// A smooth wobble along one strand: three sines at random pitch. +function wobble(out, o) { + const f = [0, 0, 0].map(() => (1 / 30 + random() / 40) * 2 * PI), ph = [0, 0, 0].map(() => random() * 2 * PI); + for (let r = 0; r < ROWS; r++) out[o + r] = (sin(f[0] * r + ph[0]) + 0.6 * sin(0.5 * f[1] * r + ph[1]) + 0.4 * sin(0.3 * f[2] * r + ph[2])) / 1.4; +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, wave: 0, shine: 0, begun: false, t: 0, + comb: -10, combing: false, combWait: 1.2, teeth: 0, + X0: new Float32Array(N), B: new Float32Array(N), SL: new Float32Array(N), D: new Float32Array(N), END: new Float32Array(N), TO: new Float32Array(N), + NX: new Float32Array(N * ROWS), NT: new Float32Array(N * ROWS), + fz: new Float32Array(ROWS).fill(1), part: new Float32Array(ROWS), + sx: new Float32Array(ROWS), tz: new Float32Array(ROWS), ws: new Float32Array(ROWS), wc: new Float32Array(ROWS), + acc: new Float32Array(W * H * 3), wt: new Float32Array(W * H), c: [0, 0, 0], body: [0, 0, 0], tint: [0, 0, 0], + }; + const S = params.s; + for (let i = 0; i < N; i++) { + S.X0[i] = -7 + (78 * (i + random())) / N; + S.B[i] = 0.28 + 0.72 * random() ** 0.6; + S.TO[i] = (random() - 0.5) * 0.12; + S.SL[i] = (random() - 0.5) * 0.025; + S.D[i] = (random() - 0.5) * 0.6; + S.END[i] = 112 + 40 * random(); + wobble(S.NX, i * ROWS); + wobble(S.NT, i * ROWS); + } +} + +function step(S, d) { + const sp = S.speed / 2, ds = d * sp; + S.t += ds; + // The comb: every 9 s at Speed 2 it runs down from above the panel to past + // the ends in 2.4 s. Hair it has passed lies smooth and loosens again over + // about 6 s; the partings behind its teeth close over about 1.2 s. + const was = S.comb; + if (!S.combing) { + S.combWait -= ds; + if (S.combWait <= 0) startComb(S); + } else { + S.comb += (ds * 170) / 2.4; + if (S.comb > 160) { + S.combing = false; + S.combWait = 6.6; + } + } + for (let r = 0; r < ROWS; r++) { + const y = r + Y0; + S.fz[r] += (1 - S.fz[r]) * (1 - exp(-ds / 6)); + S.part[r] *= exp(-ds / 1.2); + // The teeth bite over the 8 rows behind the comb's edge. + if (S.combing && y < S.comb) { + const a = sm(0, 8, S.comb - y); + S.fz[r] = min(S.fz[r], 1 - 0.55 * a); + S.part[r] = max(S.part[r], a * (y >= was - 9 ? 1 : 0)); + } + } +} + +function startComb(S) { + S.combing = true; + S.comb = -10; + S.teeth = random() * 7; +} + +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.wave = cl(k[2], 0, 1); + S.shine = cl(k[3], -1, 1); + if (!S.begun) { + // Open with the band a third of the way down and rising. + S.t = 1.5; + S.begun = true; + } + if (input.btnPressed && input.btnPressed[0] && !S.combing) startComb(S); + step(S, d); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { acc, wt, sx, tz, ws, wc, fz, part, NX, NT, X0, B, SL, D, END, c, body, tint } = S; + acc.fill(0); + wt.fill(0); + const t = S.t, wv = S.wave; + // Per row: the breeze, a wave running down the hair that grows toward the + // ends; the lean of the sheet, which puts the shine band where tz meets + // TZ0 and slides it up and down as the hair sways; and the waves of wavy + // hair, sideways and in depth a quarter turn apart. + const yb = 46 + 13 * sin(2 * PI * (t / 6.5)) + 4 * sin(2 * PI * (t / 2.9) + 1); + for (let r = 0; r < ROWS; r++) { + const y = r + Y0, reach = ((y + 30) / 160) ** 1.6; + sx[r] = 3.2 * reach * sin(2 * PI * (y / 120 - t / 4.6)) + 1.1 * reach * sin(2 * PI * (y / 45 - t / 2.3) + 2); + const a = (2 * PI * y) / 44; + tz[r] = TZ0 + 0.016 * (1 - 0.6 * wv) * (yb - y) + 0.3 * wv * sin(a + 0.6 * sin(2 * PI * (t / 7.3))); + ws[r] = 3 * wv * cos(a); + wc[r] = 3 * wv * -sin(a); + } + const sh = S.shine, p1 = 0.5 * 320 * 4 ** sh, k1 = 0.9 + 0.4 * sh, p2 = 0.4 * p1, k2 = 0.45 + 0.15 * sh, k0 = 0.24 * (1 - sh); + const teeth = S.teeth; + for (let i = 0; i < N; i++) { + const o = i * ROWS, cd = cos(D[i]), sd = sin(D[i]), end = END[i], b = B[i], to = S.TO[i], x0 = X0[i], sl = SL[i]; + let px = 0; + const last = min(H - 1, floor(end)); + for (let r = 0; r <= last - Y0; r++) { + const y = r + Y0, f = fz[r]; + let x = x0 + sl * y + sx[r] + ws[r] * cd + wc[r] * sd + 0.4 * f * NX[o + r]; + // Behind the comb the strands part round its teeth, 7 LEDs apart: each + // gap of 7 closes to 5.7, leaving a dark parting 1.3 LEDs wide. + const m = part[r]; + if (m > 0.01) { + const u = (x - teeth) / 7, e = u - floor(u) - 0.5; + x -= m * 1.3 * e; + } + const tx = x - px; + px = x; + if (y < 0) continue; + const z = tz[r] + to * f + 0.06 * f * NT[o + r]; + const inv = 1 / sqrt(tx * tx + 1 + z * z); + const th = (tx * HX + HY + z * HZ) * inv; + // The primary highlight is shifted toward the root, the secondary, + // tinted by the hair, toward the tip. + const t1 = th - 0.05, t2 = th + 0.11; + const smooth = 1 + 0.45 * (1 - f); + const l1 = log(max(1e-6, 1 - t1 * t1)); + // A sharp band and, wider and weaker, the sheen round it that a duller + // finish spreads out. + const s1 = smooth * (k1 * exp(p1 * l1) + k0 * exp(5 * l1)), s2 = smooth * k2 * exp(p2 * log(max(1e-6, 1 - t2 * t2))); + const lam = (0.55 * z + 0.82) / sqrt(1 + z * z), dif = b * (0.3 + 0.7 * max(0, lam)); + const tip = cl((end - y) / 7, 0, 1), w0 = tip * (0.4 + 0.6 * tip); + const xi = floor(x), fr = x - xi, q = y * W + xi; + if (xi >= 0 && xi < W) { + const w = (1 - fr) * w0; + acc[q * 3] += dif * w; + acc[q * 3 + 1] += s2 * b * w; + acc[q * 3 + 2] += s1 * (0.35 + 0.65 * b) * w; + wt[q] += w; + } + if (xi + 1 >= 0 && xi + 1 < W) { + const w = fr * w0; + acc[q * 3 + 3] += dif * w; + acc[q * 3 + 4] += s2 * b * w; + acc[q * 3 + 5] += s1 * (0.35 + 0.65 * b) * w; + wt[q + 1] += w; + } + } + } + // Each LED: the mean of the strands over it, darker where they are fewer + // than 2.4. Channel 0 holds the body, 1 the tinted secondary, 2 the white + // primary. + // Orange and yellow read brown and olive at mid level on the panel, so + // round hue 0.12 the hair is lifted toward copper and honey blonde. + const h = S.hue + 0.76, hh = h - floor(h), warm = max(0, 1 - Math.abs(hh - 0.12) / 0.11); + hsv(h, 0.78 - 0.18 * warm, 1 + 0.6 * warm, body); + hsv(h, 0.8 - 0.2 * warm, 1, tint); + const RHO = (N / 78) * 0.8; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const q = y * W + x, n = max(wt[q], RHO); + const dv = acc[q * 3] / n, s2 = acc[q * 3 + 1] / n, s1 = acc[q * 3 + 2] / n; + for (let ch = 0; ch < 3; ch++) c[ch] = 0.52 * dv * body[ch] + 0.9 * s2 * tint[ch] + s1 * (0.8 + 0.2 * tint[ch]); + display.setPixel(x, y, round(255 * (1 - exp(-1.4 * c[0]))), round(255 * (1 - exp(-1.4 * c[1]))), round(255 * (1 - exp(-1.4 * c[2])))); + } +} diff --git a/incoming/pattern-study/patterns/scheidegger-rivers.js b/incoming/pattern-study/patterns/scheidegger-rivers.js new file mode 100644 index 000000000..83bd86dcc --- /dev/null +++ b/incoming/pattern-study/patterns/scheidegger-rivers.js @@ -0,0 +1,302 @@ +// ===== Patternflow pattern ===== +// Title: Scheidegger Rivers +// Family: line +// Idea: Scheidegger's random river network on the checkerboard of LEDs, every site draining to the LED down-left or down-right of it, each stream lit by the log of the water through it and coloured by the outlet it reaches, so brooks at the top merge into trunks that thicken down the panel; raindrops run down the rivers, and every so often a site turns the other way and a river is captured, its old bed draining and its new one filling row by row. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Scheidegger's river network model (Adrian Scheidegger, 1967), pattern round 34; its rivers are coalescing random walks, whose scaling limit is the Brownian web (Balint Toth and Wendelin Werner, 1998) +// Round: 34 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Bias=-1..1, Capture=0..1 +// +// Knob 1: Hue. Turns the rivers round the wheel. Each large basin holds one of four hues 0.1 apart: at 0 green, cyan, blue and violet, at 0.25 blue-violet to rose, at 0.5 pink, red, orange and amber, at 0.75 yellow to teal. Brooks are saturated and the big trunks go pale. +// Knob 2: Speed. At 2 raindrops run 16 rows a second (a drop from the top reaches the bottom in 8 s), 14 land each second, and a rerouted river drains and refills 40 rows a second; at 0.2 all of it a tenth as fast, at 3 one and a half times. Captures come in step with it. +// Knob 3: Bias. The chance a site drains down-right, 0.5 + 0.48 x Bias. At 0 even, and the trunks wander straight down; at 0.5 it is 0.74 and every river leans right; at -1 or 1 it is 0.02 or 0.98, so the rivers are straight diagonals that merge only where one site in 50 turns the other way. +// Knob 4: Capture. How often sites change their way down. At 0.06 and Speed 2, 9 sites a second at random and 1.2 a second on large rivers, picked by the flux through them where the other way runs at least 10 rows apart; at 0 the network holds still, at 1 150 and 20 a second and the basins churn. +// Buttons: K1 tap is a burst of rain: 260 drops land on the top 24 rows at once and run together down the network, tracing the tree into its trunks. + +const W = 64, H = 128, SW = W / 2, NS = SW * H; +const { floor, min, max, log, exp, 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); + const rgb = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + out[0] = rgb[0]; + out[1] = rgb[1]; + out[2] = rgb[2]; +} + +// Site s sits in row y = s / 32 at x = 2 (s % 32) + (y & 1): the LEDs with +// x + y even. Its two neighbours below are x - 1 and x + 1 in row y + 1, +// wrapping round the sides. +const SX = new Int16Array(NS), SY = new Int16Array(NS); +const DL = new Int32Array(NS), DR = new Int32Array(NS), UL = new Int32Array(NS), UR = new Int32Array(NS); +const siteAt = (x, y) => y * SW + (((x % W) + W) % W >> 1); +for (let s = 0; s < NS; s++) { + const y = floor(s / SW), x = 2 * (s % SW) + (y & 1); + SX[s] = x; + SY[s] = y; + DL[s] = y < H - 1 ? siteAt(x - 1, y + 1) : -1; + DR[s] = y < H - 1 ? siteAt(x + 1, y + 1) : -1; + UL[s] = y > 0 ? siteAt(x - 1, y - 1) : -1; + UR[s] = y > 0 ? siteAt(x + 1, y - 1) : -1; +} +const FMAX = 1500, LFMAX = log(FMAX); + +export function setup(params) { + params.s = { + hue: 0, speed: 2, bias: 0, cap: 0.06, warm: false, sweep: 0, flipAcc: 0, bigAcc: 0, rainAcc: 0, + u: new Float32Array(NS), dn: new Int32Array(NS), F: new Float32Array(NS), Q: new Float32Array(NS), + out: new Int16Array(NS), prev: new Int16Array(NS), ov: new Int32Array(SW * SW), col: new Int8Array(SW).fill(-1), + col2: new Int8Array(SW), ho: new Float32Array(NS), cum: new Float64Array(NS), + drops: [], Lv: new Float32Array(W * H), Hu: new Float32Array(W * H), Sa: new Float32Array(W * H), + Wd: new Float32Array(W * H), Dr: new Float32Array(W * H), c: [0, 0, 0], + }; + const S = params.s; + for (let s = 0; s < NS; s++) S.u[s] = random(); +} + +// Each site drains right when its own random number is under 0.5 + 0.48 Bias. +function route(S) { + const p = 0.5 + 0.48 * S.bias; + for (let s = 0; s < NS; s++) S.dn[s] = DL[s] < 0 ? -1 : S.u[s] < p ? DR[s] : DL[s]; +} + +// The exact flux, row by row from the top: 1 for the site itself plus all +// that drains into it. +function flux(S) { + const F = S.F, dn = S.dn; + F.fill(1); + for (let s = 0; s < NS - SW; s++) F[dn[s]] += F[s]; + // Which outlet in the bottom row each site reaches, from the bottom up. + for (let s = NS - 1; s >= 0; s--) S.out[s] = dn[s] < 0 ? s - (NS - SW) : S.out[dn[s]]; +} + +// The water on show moves one row per sweep: a rerouted river drains and the +// new one fills from the site that changed downwards. +function sweep(S) { + const Q = S.Q, dn = S.dn; + for (let s = NS - 1; s >= SW; s--) { + let q = 1; + const a = UL[s], b = UR[s]; + if (dn[a] === s) q += Q[a]; + if (dn[b] === s) q += Q[b]; + Q[s] = q; + } +} + +// Basin colours. A basin draining at least AMIN sites through its outlet +// holds one of four hue steps; after the network changes, each new basin +// takes the colour of the old basin it shares most sites with, and a basin +// with none to take gets the step farthest from those in use. +const STEPS = [-0.1, 0, 0.1, 0.2], AMIN = 120; +function basins(S) { + const ov = S.ov, F = S.F, B = NS - SW; + ov.fill(0); + for (let s = 0; s < NS; s++) ov[S.prev[s] * SW + S.out[s]]++; + const pairs = []; + for (let i = 0; i < SW; i++) + if (S.col[i] >= 0) for (let j = 0; j < SW; j++) if (F[B + j] >= AMIN && ov[i * SW + j] > 0) pairs.push([ov[i * SW + j], i, j]); + pairs.sort((a, b) => b[0] - a[0]); + const col = S.col2.fill(-1), taken = new Uint8Array(SW); + for (const [, i, j] of pairs) + if (!taken[i] && col[j] < 0) { + taken[i] = 1; + col[j] = S.col[i]; + } + for (let j = 0; j < SW; j++) + if (F[B + j] >= AMIN && col[j] < 0) { + let best = 0, far = -1; + for (let k = 0; k < STEPS.length; k++) { + let near = 9; + for (let m = 0; m < SW; m++) if (col[m] >= 0) near = min(near, Math.abs(STEPS[k] - STEPS[col[m]])); + if (near > far) (far = near), (best = k); + } + col[j] = best; + } + S.col.set(col); + S.prev.set(S.out); +} + +// How many rows the two paths out of site s, one through each neighbour +// below, run apart before they join (or reach the bottom). +function apart(S, s) { + let a = DL[s], b = DR[s], n = 0; + while (a >= 0 && a !== b) { + a = S.dn[a]; + b = S.dn[b]; + n++; + } + return n; +} + +// A capture: site s turns to its other neighbour, with the Metropolis chance +// that keeps every site draining right with chance p in the long run. +function flip(S, s) { + const p = 0.5 + 0.48 * S.bias, right = S.u[s] < p; + const want = right ? 1 - p : p; + if (random() * max(p, 1 - p) > want) return false; + S.u[s] = right ? p + (1 - p) * random() : p * random(); + S.dn[s] = right ? DL[s] : DR[s]; + return true; +} + +function step(S, d) { + const sp = S.speed / 2; + route(S); + flux(S); + // Small captures land on any site; large ones on a site picked in + // proportion to the flux through it, among those whose other way down + // runs at least 10 rows apart from the river it leaves. + S.flipAcc += d * sp * 150 * S.cap; + S.bigAcc += d * sp * 20 * S.cap; + let changed = false; + while (S.flipAcc >= 1) { + S.flipAcc -= 1; + changed = flip(S, floor(random() * (NS - SW))) || changed; + } + if (S.bigAcc >= 1) { + const cum = S.cum; + let acc = 0; + for (let s = 0; s < NS; s++) cum[s] = acc += S.F[s]; + while (S.bigAcc >= 1) { + S.bigAcc -= 1; + for (let tries = 0; tries < 8; tries++) { + const r = random() * acc; + let lo = 0, hi = NS - 1; + while (lo < hi) { + const m = (lo + hi) >> 1; + if (cum[m] < r) lo = m + 1; + else hi = m; + } + if (lo >= NS - SW || apart(S, lo) < 10) continue; + changed = flip(S, lo) || changed; + break; + } + } + } + if (changed) flux(S); + S.sweep += d * sp * 40; + while (S.sweep >= 1) { + S.sweep -= 1; + sweep(S); + } + const k = 1 - exp(-d * sp * 3); + basins(S); + for (let s = 0; s < NS; s++) { + const c = S.col[S.out[s]]; + if (c >= 0) S.ho[s] += (STEPS[c] - S.ho[s]) * k; + } + + // Rain: drops land on random sites and run down the rivers, two LED steps + // a row (across, then down). + S.rainAcc += d * sp * 14; + while (S.rainAcc >= 1) { + S.rainAcc -= 1; + addDrop(S, floor(random() * (NS - 8 * SW))); + } + const v = d * sp * 32; + const drops = S.drops; + for (let i = drops.length - 1; i >= 0; i--) { + const D = drops[i]; + D.pos += v; + while (D.pos >= 1) { + D.pos -= 1; + if (D.mid < 0) { + const t = S.dn[D.s]; + if (t < 0) { + D.gone = true; + break; + } + D.mid = SY[D.s] * W + SX[t]; + D.s = t; + D.trail.unshift(D.mid); + } else { + D.mid = -1; + D.trail.unshift(SY[D.s] * W + SX[D.s]); + } + if (D.trail.length > 8) D.trail.pop(); + } + if (D.gone) drops.splice(i, 1); + } +} + +function addDrop(S, s) { + S.drops.push({ s, mid: -1, pos: random(), trail: [SY[s] * W + SX[s]], gone: false }); +} + +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.bias = cl(k[2], -1, 1); + S.cap = cl(k[3], 0, 1); + if (!S.warm) { + route(S); + flux(S); + S.Q.set(S.F); + S.prev.set(S.out); + basins(S); + for (let s = 0; s < NS; s++) S.ho[s] = S.col[S.out[s]] >= 0 ? STEPS[S.col[S.out[s]]] : 0; + const sp = S.speed; + S.speed = 2; + for (let i = 0; i < 120; i++) step(S, 1 / 30); + S.speed = sp; + S.warm = true; + } + if (input.btnPressed && input.btnPressed[0]) for (let i = 0; i < 260; i++) addDrop(S, floor(random() * 24 * SW)); + step(S, d); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { Lv, Hu, Sa, Wd, Dr, Q } = S; + Lv.fill(0); + Wd.fill(0); + Dr.fill(0); + // Each site lights its own LED and the LED beside it on the side it drains + // to, which is the LED above the site it drains into: a river is a stair of + // LEDs, across then down, and two streams meeting make a T. + for (let s = 0; s < NS; s++) { + const g = cl(log(Q[s]) / LFMAX, 0, 1), L = 30 + 225 * g ** 1.35; + const p = SY[s] * W + SX[s], h = S.hue + 0.52 + S.ho[s], sat = 0.85 - 0.45 * g; + const wd = 1 + 1.8 * cl((log(Q[s]) - log(30)) / (LFMAX - log(30)), 0, 1); + if (L > Lv[p]) (Lv[p] = L), (Hu[p] = h), (Sa[p] = sat), (Wd[p] = wd); + const t = S.dn[s]; + if (t >= 0) { + const m = SY[s] * W + SX[t]; + if (L > Lv[m]) (Lv[m] = L), (Hu[m] = h), (Sa[m] = sat), (Wd[m] = wd); + } + } + for (const D of S.drops) + D.trail.forEach((p, i) => { + Dr[p] = max(Dr[p], i < 2 ? 1 : 0.85 ** (i - 1)); + }); + const out = S.c; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + let L = Lv[p], h = Hu[p], sa = Sa[p]; + // Wide rivers spill onto the LEDs beside them. + for (const dx of [-2, -1, 1, 2]) { + const q = y * W + ((x + dx + W) % W), w = Wd[q] - Math.abs(dx); + if (w > 0) { + const v = Lv[q] * min(1, w) * (Math.abs(dx) === 1 ? 0.8 : 0.55); + if (v > L) (L = v), (h = Hu[q]), (sa = Sa[q]); + } + } + const dr = Dr[p]; + if (dr > 0) { + L = L + (255 - L) * dr; + sa *= 1 - 0.85 * dr; + } + hsv(h, sa, 1, out); + display.setPixel(x, y, round(out[0] * L), round(out[1] * L), round(out[2] * L)); + } +} From 4de8fe51a50e3055e37ad929031de0e292dd4547 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 16:11:36 +0000 Subject: [PATCH 3/9] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=203?= =?UTF-8?q?4=ED=8C=90:=20Kelvin=20Wake,=20Velvet=20Nap?= 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_011BuccZdVDjYHZnwdNh4X1F --- .../pattern-study/patterns/kelvin-wake.js | 264 ++++++++++++++++++ incoming/pattern-study/patterns/velvet-nap.js | 240 ++++++++++++++++ 2 files changed, 504 insertions(+) create mode 100644 incoming/pattern-study/patterns/kelvin-wake.js create mode 100644 incoming/pattern-study/patterns/velvet-nap.js diff --git a/incoming/pattern-study/patterns/kelvin-wake.js b/incoming/pattern-study/patterns/kelvin-wake.js new file mode 100644 index 000000000..32a16a2d1 --- /dev/null +++ b/incoming/pattern-study/patterns/kelvin-wake.js @@ -0,0 +1,264 @@ +// ===== Patternflow pattern ===== +// Title: Kelvin Wake +// Family: wave +// Idea: A small patch of foam climbs the panel over still water and leaves Kelvin's ship wave behind it: curved transverse crests across its track and short divergent waves slanting out along the two arms of a wedge of half-angle 19.47 degrees, brightest where the two systems meet in cusps along the wedge line, and the wedge stays the same however fast the source goes. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Kelvin ship-wave pattern (William Thomson, Lord Kelvin, 1887), pattern round 34 +// Round: 34 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Velocity=1..1.7, Patch=0..1 +// +// Knob 1: Hue. The colour of the water, once round the wheel. At 0 azure, at 0.15 indigo, at 0.3 magenta, at 0.45 crimson, at 0.5 orange, at 0.6 gold, at 0.75 green, at 0.9 jade. From orange to lime the calm water is lifted from 0.34 to 0.74 of full brightness and its shadowed faces lean to crimson, so it never reads brown or olive. +// Knob 2: Speed. The clock. At 2 a source at Velocity 1 climbs 30 LEDs a second, crosses the panel in 4.3 seconds and the next one comes up from the bottom 6.3 seconds after it; at 0.2 everything runs ten times slower, at 3 half as fast again. +// Knob 3: Velocity. The source's speed U. The wavelength of the transverse waves goes as U squared, 12 LEDs at 1, 22 at 1.35 and 35 at 1.7, and the source climbs from 30 to 51 LEDs a second (at Speed 2). The wedge does not change: its cusp lines meet the panel's sides 90 LEDs behind the source at every setting. With the patch the same size, a faster source makes more of the short divergent waves, until at 1.7 they outshine the transverse crests. +// Knob 4: Patch. The size a of the Gaussian pressure patch that drives the wake, 1.2 LEDs at 0 to 6 at 1, and the foam over it grows from 4 to 8 LEDs across. A small patch makes short divergent waves and sharp cusps along the wedge lines; a large one smooths them away and leaves only the transverse crests, by 0.25 at Velocity 1 and by about 0.6 at Velocity 1.7. +// Buttons: K1 tap sends the next source up from the bottom at once (if the last one is at least 24 LEDs up the panel), so two wakes overlap and add. + +const W = 64, H = 128, YC = 32, DX = 0.5, N = 256; +// The wake is drawn to 150 LEDs behind its source and fades over the last +// 45; sources come 190 LEDs apart, so 40 LEDs of still water separate them. +const LT = 150, FADE = 45, GAP = 190, ROWS = Math.ceil(LT / DX) + 2; +const { floor, sqrt, sin, cos, exp, abs, round, min, max, pow, random, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => { + const t = cl((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + const rgb = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + out[0] = rgb[0]; + out[1] = rgb[1]; + out[2] = rgb[2]; +} + +// In-place radix-2 FFT with the + sign. +function fft(re, im) { + const n = re.length; + for (let i = 1, j = 0; i < n; i++) { + let bit = n >> 1; + for (; j & bit; bit >>= 1) j ^= bit; + j ^= bit; + if (i < j) { + let t = re[i]; + re[i] = re[j]; + re[j] = t; + t = im[i]; + im[i] = im[j]; + im[j] = t; + } + } + for (let len = 2; len <= n; len <<= 1) { + const ang = (2 * PI) / len, wr = cos(ang), wi = sin(ang), h = len >> 1; + for (let i = 0; i < n; i += len) { + let cr = 1, ci = 0; + for (let k = 0; k < h; k++) { + const a = i + k, b = a + h; + const xr = re[b] * cr - im[b] * ci, xi = re[b] * ci + im[b] * cr; + re[b] = re[a] - xr; + im[b] = im[a] - xi; + re[a] += xr; + im[a] += xi; + const nr = cr * wr - ci * wi; + ci = cr * wi + ci * wr; + cr = nr; + } + } + } +} + +// The steady wake in the source's frame, x LEDs behind it and y LEDs to the +// side, from the classical integral over wave directions theta: +// zeta(x, y) = Im int sec^3(theta) exp(-(k a)^2 / 4) exp(i k (x cos theta + y sin theta)) dtheta, +// k = k0 sec^2(theta), k0 = g / U^2 = 2 pi / lambda. The imaginary part is +// the sine form, which is zero at x = 0, so the wake starts from nothing at +// the source. Changing variable to the cross-track wavenumber +// ky = k0 sec(theta) tan(theta) makes each row x one Fourier sum over ky: +// sec^2(theta) = (1 + sqrt(1 + 4 (ky / k0)^2)) / 2, the along-track +// wavenumber is k0 sec(theta), and sec^3 dtheta = sec^2 / (k0 (2 sec^2 - 1)) dky. +// Only the slopes are drawn, so a row is two 256-point FFTs across y: the +// same sum times i kx and times i ky. 256 LEDs across hold the wedge out to +// x = 150, where it is 106 wide, so the FFT's wrap never reaches the panel. +// Fitting the brightest crest beside the axis, row by row out to x = 320, as +// s x - c x^(1/3) (the Airy fringe sits inside the caustic by c x^(1/3)) puts +// the cusp line at 19.25 to 19.46 degrees over the whole Velocity range; +// Kelvin's angle is arcsin(1/3) = 19.47. +function build(T, lambda, a) { + const k0 = (2 * PI) / lambda; + const { ky, kx, B, r1, i1, r2, i2, ZX, ZY } = T; + for (let j = 0; j < N; j++) { + const q = (2 * PI * (j < N / 2 ? j : j - N)) / N, u = q / k0; + const s2 = (1 + sqrt(1 + 4 * u * u)) / 2, k = k0 * s2, aq = abs(q); + // A soft roll-off before the LED grid's Nyquist limit. + const win = aq < 0.6 * PI ? 1 : cos(((aq - 0.6 * PI) / (0.4 * PI)) * 0.5 * PI) ** 2; + B[j] = (s2 / (k0 * (2 * s2 - 1))) * exp(-(((k * a) / 2) ** 2)) * win; + ky[j] = q; + kx[j] = k0 * sqrt(s2); + } + for (let r = 0; r < ROWS; r++) { + const x = r * DX; + for (let j = 0; j < N; j++) { + // Columns sit at y = m + 0.5, so the track runs between the two middle columns. + const ph = kx[j] * x + ky[j] * 0.5, c = B[j] * cos(ph), s = B[j] * sin(ph); + r1[j] = -kx[j] * s; + i1[j] = kx[j] * c; + r2[j] = -ky[j] * s; + i2[j] = ky[j] * c; + } + fft(r1, i1); + fft(r2, i2); + // The far end of the wake fades out: a source that has run for a finite + // time has a wake of finite length. + const f = 1 - sm(LT - FADE, LT, x); + for (let m = 0; m < YC; m++) { + ZX[r * YC + m] = i1[m] * f; + ZY[r * YC + m] = i2[m] * f; + } + } + // One scale for the shading: the 99th percentile of the lit slope over + // the stretch that fits on the panel, both sides of the track. + const vals = T.vals; + let n = 0; + for (let r = floor(4 / DX); r < floor(128 / DX); r += 2) + for (let m = 0; m < YC; m++) { + const gx = ZX[r * YC + m], gy = ZY[r * YC + m]; + vals[n++] = abs(gy * T.lx + gx * T.ly); + vals[n++] = abs(-gy * T.lx + gx * T.ly); + } + const v = vals.subarray(0, n).sort(); + T.scale = 1 / (v[floor(n * 0.99)] || 1); +} + +export function setup(params) { + // The light comes from the upper left, low over the water. + const lx = -0.45, ly = -0.89; + params.s = { + hue: 0, speed: 2, vel: 1, patch: 0.06, warm: false, key: "", foamT: 0, + src: [], foam: [], cache: new Map(), c: [0, 0, 0], + work: { + ky: new Float64Array(N), kx: new Float64Array(N), B: new Float64Array(N), + r1: new Float64Array(N), i1: new Float64Array(N), r2: new Float64Array(N), i2: new Float64Array(N), + }, + lx, ly, GX: new Float32Array(W * H), GY: new Float32Array(W * H), F: new Float32Array(W * H), tab: null, + }; +} + +function table(S) { + // Tables are cached by the quantised knob pair: Velocity in steps of 0.02, + // Patch in steps of 0.02. + const qv = round(S.vel * 50) / 50, qp = round(S.patch * 50) / 50, key = qv + "," + qp; + if (key === S.key) return; + S.key = key; + let T = S.cache.get(key); + if (!T) { + T = { + ...S.work, lx: S.lx, ly: S.ly, scale: 1, vals: new Float32Array(2 * YC * 128), + ZX: new Float32Array(ROWS * YC), ZY: new Float32Array(ROWS * YC), + }; + build(T, 12 * qv * qv, 1.2 + 4.8 * qp); + if (S.cache.size > 6) S.cache.delete(S.cache.keys().next().value); + S.cache.set(key, T); + } + S.tab = T; +} + +// Rows climbed per second. +const rate = (S) => 15 * S.vel * S.speed; + +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.vel = cl(k[2], 1, 1.7); + S.patch = cl(k[3], 0, 1); + table(S); + const v = rate(S); + if (!S.warm) { + // Open so that 2.4 seconds in, a source is 40 LEDs below the top with the + // one before it already gone past. + const y0 = 40 + 2.4 * v; + S.src = [y0 - GAP, y0]; + S.warm = true; + } + for (let i = 0; i < S.src.length; i++) S.src[i] -= v * d; + while (S.src.length && S.src[0] < -LT - 4) S.src.shift(); + const last = S.src[S.src.length - 1]; + if (last === undefined || last < H + 6 - GAP) S.src.push(H + 6); + else if (input.btnPressed && input.btnPressed[0] && last < H - 24) S.src.push(H + 6); + // Foam shed by each source stays where it fell on the still water and + // fades in about 0.4 seconds. + const rf = 1.5 + 0.35 * (1.2 + 4.8 * S.patch); + S.foamT += d * 60 * S.speed / 2; + for (const f of S.foam) f.life -= (d * S.speed) / 2 / 0.4; + S.foam = S.foam.filter((f) => f.life > 0); + while (S.foamT >= 1) { + S.foamT -= 1; + for (const ys of S.src) + if (ys > -4 && ys < H + 4) { + const ang = random() * 2 * PI, r = rf * sqrt(random()); + S.foam.push({ x: 31.5 + r * cos(ang), y: ys + 0.6 * r * sin(ang) + 1, life: 0.6 + 0.4 * random() }); + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.tab) return; + const T = S.tab, { ZX, ZY } = T, GX = S.GX, GY = S.GY, F = S.F, c = S.c; + GX.fill(0); + GY.fill(0); + F.fill(0); + for (const f of S.foam) { + const x = round(f.x), y = round(f.y); + if (x >= 0 && x < W && y >= 0 && y < H) F[y * W + x] = max(F[y * W + x], min(1, f.life) * 0.85); + } + // Each source lays its wake, row by row, from the table: x behind it, + // linear between table rows; the cross-track slope is odd in y. + for (const ys of S.src) + for (let y = max(0, Math.ceil(ys)); y < H; y++) { + const fx = (y - ys) / DX; + if (fx >= ROWS - 1) break; + const r = floor(fx), w = fx - r, o0 = r * YC, o1 = o0 + YC; + for (let m = 0; m < YC; m++) { + const gx = ZX[o0 + m] * (1 - w) + ZX[o1 + m] * w, gy = ZY[o0 + m] * (1 - w) + ZY[o1 + m] * w; + GY[y * W + 32 + m] += gx; + GX[y * W + 32 + m] += gy; + GY[y * W + 31 - m] += gx; + GX[y * W + 31 - m] -= gy; + } + } + const h0 = S.hue + 0.55, hh = h0 - floor(h0); + // Amber, gold and lime water read brown or olive at the calm level, so + // there the water is lifted to 0.74 and its shadow faces lean to crimson. + const warm = sm(0.0, 0.05, hh) * (1 - sm(0.22, 0.32, hh)); + const base = 0.34 + 0.4 * warm, sat0 = 0.82 + 0.1 * warm; + const rf = 1.5 + 0.35 * (1.2 + 4.8 * S.patch); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + // Lit where the surface tilts towards the light. + const sh = -(GX[p] * S.lx + GY[p] * S.ly) * T.scale; + const up = sh > 0 ? min(1.4, sh) : 0, dn = sh < 0 ? min(1, -sh) : 0; + let v = base + (1 - base) * pow(min(1, up), 0.75) - (base - 0.08 - 0.12 * warm) * pow(dn, 0.7); + let s = sat0 * (1 - 0.7 * sm(0.55, 1.25, up)); + let hue = h0 + 0.03 * up - (0.02 + 0.08 * warm) * dn; + // The foam patch over each source. + let foam = F[p]; + for (const ys of S.src) { + const dx = x - 31.5, dy = (y - ys) * 1.4, r2 = dx * dx + dy * dy; + if (r2 < (rf + 1.5) * (rf + 1.5)) foam = max(foam, 1 - sm(rf - 1, rf + 1.5, sqrt(r2))); + } + if (foam > 0) { + v = v + (1 - v) * foam; + s *= 1 - foam; + } + hsv(hue, s, v, c); + display.setPixel(x, y, round(c[0] * 255), round(c[1] * 255), round(c[2] * 255)); + } +} diff --git a/incoming/pattern-study/patterns/velvet-nap.js b/incoming/pattern-study/patterns/velvet-nap.js new file mode 100644 index 000000000..75beeb9c9 --- /dev/null +++ b/incoming/pattern-study/patterns/velvet-nap.js @@ -0,0 +1,240 @@ +// ===== Patternflow pattern ===== +// Title: Velvet Nap +// Family: cosmetic +// Idea: The whole panel is matte velvet under a low light from the left that sways slowly, and two unseen fingertips wander over it in slow strokes, brushing the pile their own way: trails turn dark and rich where the pile is pushed towards the light and pale and silvery where it lies away from it, and cross and overwrite each other like writing on suede, while every few seconds a palm strokes one band from top to bottom so its pile lies one way again. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 34, after the matte velvet of flocked compacts and velvet lip tints +// Round: 34 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Width=0..1, Pile=-1..1 +// +// Knob 1: Hue. The dye of the velvet, once round the wheel, the sheen a pale tint of it. At 0 emerald, at 0.2 sapphire, at 0.35 violet, at 0.5 berry, at 0.6 crimson, at 0.68 orange, at 0.76 yellow, at 0.84 lime. From orange to lime the body is lifted towards 0.8 of full brightness and its darker faces lean to ruby, so it never reads rust, mustard or olive. +// Knob 2: Speed. At 2 the fingertips travel 26 LEDs a second in strokes of 1.2 to 3 seconds, lifting for 0.2 to 0.5 seconds between them; a palm strokes down a band every 7 seconds and takes 1.6 seconds over it, and the light sways across and back once every 12 seconds. At 0.2 all of it runs ten times slower, at 3 half as fast again. +// Knob 3: Width. The fingertip: a trail about 4 LEDs wide at 0, 7 at 0.5 and 10 at 1, so the writing goes from a fine hand to broad smears that cover most of the panel. +// Knob 4: Pile. The length of the nap. At -1 a short suede nap whose fibres lean only 36 degrees when brushed, with a broad weak sheen, so the trails are faint; at 0 they lean 54 degrees; at 1 a long plush pile leans 72 degrees, the dark trails go deepest and the silvery ones carry a tight sheen that glints with the fibre grain. +// Buttons: K1 tap lays a palm on the whole panel at once: in about half a second every trail is smoothed away and the pile lies straight down again. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, sqrt, sin, cos, atan2, round, pow, random, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => { + const t = cl((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + const rgb = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + out[0] = rgb[0]; + out[1] = rgb[1]; + out[2] = rgb[2]; +} + +// The pile of each LED is a vector in the plane of the cloth: its direction +// is where the fibres lean, its length how far they lean, 0 upright and 1 as +// far over as the pile goes. A palm lays it straight down the panel. +const PALM = 0.85; +// The light comes from the left and a little below, 22 degrees above the +// cloth, and sways 29 degrees either way every 12 seconds. +const ELEV = (22 * PI) / 180, LAZ = PI - 0.36, SWAY = 0.5; + +export function setup(params) { + const S = (params.s = { + hue: 0, speed: 2, width: 0, pile: 0.06, warm: false, clock: 0, palmT: 0, palmY: -99, palmX: 32, flat: 0, + qx: new Float32Array(N), qy: new Float32Array(N), + ga: new Float32Array(N), gl: new Float32Array(N), gb: new Float32Array(N), + fingers: [], c: [0, 0, 0], d: [0, 0, 0], + }); + for (let p = 0; p < N; p++) { + S.qy[p] = PALM; + // The fibre grain: each LED's tuft leans a little off the brushed way, + // a little more or less, and holds a little more or less dye. + S.ga[p] = (random() - 0.5) * 0.3; + S.gl[p] = 1 + (random() - 0.5) * 0.16; + S.gb[p] = 1 + (random() - 0.5) * 0.05; + } + for (let i = 0; i < 2; i++) S.fingers.push(newFinger(i)); +} + +// The two fingers start one in each half of the panel. +function newFinger(i) { + return { + x: 10 + random() * 44, y: (i + 0.15 + random() * 0.7) * 64, hd: random() * 2 * PI, turn: 0, + down: true, t: random() * 1.5, len: 1.2 + random() * 1.8, + }; +} + +// One finger, one step: it curves as it goes and steers away from the edges. +// A stroke lasts 1.2 to 3 seconds, then the finger lifts for 0.2 to 0.5 +// seconds and comes down somewhere else. +function moveFinger(S, f, ds) { + f.t += ds; + if (!f.down) { + if (f.t >= f.len) { + f.down = true; + f.t = 0; + f.len = 1.2 + random() * 1.8; + // It comes down where the velvet is least written: the best of four + // tries, scored by how far the pile round each is from the palm's. + let best = 1e9; + for (let k = 0; k < 4; k++) { + const x = 6 + random() * (W - 13), y = 6 + random() * (H - 13); + let sc = 0; + for (let v = -9; v <= 9; v += 3) + for (let u = -9; u <= 9; u += 3) { + const p = cl(round(y + v), 0, H - 1) * W + cl(round(x + u), 0, W - 1); + sc += Math.abs(S.qx[p]) + Math.abs(S.qy[p] - PALM); + } + if (sc < best) (best = sc), (f.x = x), (f.y = y); + } + f.hd = random() * 2 * PI; + f.turn = (random() - 0.5) * 3; + } + return false; + } + if (f.t >= f.len) { + f.down = false; + f.t = 0; + f.len = 0.2 + random() * 0.3; + return false; + } + f.turn += (random() - 0.5) * 14 * ds - f.turn * 0.8 * ds; + f.turn = cl(f.turn, -2.6, 2.6); + // Steer back towards the middle near an edge. + const mx = 10, cx = f.x < mx ? 1 : f.x > W - 1 - mx ? -1 : 0, cy = f.y < mx ? 1 : f.y > H - 1 - mx ? -1 : 0; + if (cx || cy) { + const want = atan2(cy || (32 - f.y) * 0.01, cx || (32 - f.x) * 0.01); + let dh = want - f.hd; + dh -= 2 * PI * Math.round(dh / (2 * PI)); + f.turn += dh * 4 * ds; + } + f.hd += f.turn * ds; + return true; +} + +// Brush the pile under the fingertip towards the way it moves. +function brush(S, x, y, dx, dy, r) { + const r2 = (r + 1) * (r + 1); + for (let yy = max(0, floor(y - r - 1)); yy <= min(H - 1, y + r + 1); yy++) + for (let xx = max(0, floor(x - r - 1)); xx <= min(W - 1, x + r + 1); xx++) { + const ex = xx - x, ey = yy - y, e2 = ex * ex + ey * ey; + if (e2 > r2) continue; + const w = 0.55 * (1 - sm(0.45 * r, r + 0.6, sqrt(e2))), p = yy * W + xx; + S.qx[p] += (dx - S.qx[p]) * w; + S.qy[p] += (dy - S.qy[p]) * w; + } +} + +function step(S, d) { + const sp = S.speed / 2, ds = d * sp, r = 2.2 + 3.6 * S.width, v = 26; + S.clock += ds; + for (const f of S.fingers) { + if (!moveFinger(S, f, ds)) continue; + const dx = cos(f.hd), dy = sin(f.hd), dist = v * ds, n = Math.max(1, Math.ceil(dist / 0.7)); + for (let i = 0; i < n; i++) { + f.x += (dx * dist) / n; + f.y += (dy * dist) / n; + brush(S, f.x, f.y, dx, dy, r); + } + } + // Every 7 seconds a palm strokes down one band 22 LEDs wide, from the + // top edge to the bottom in 1.6 seconds, and lays its pile straight down. + S.palmT += ds; + if (S.palmT >= 7) { + S.palmT -= 7; + S.palmY = -14; + S.palmX = 11 + random() * 42; + } + if (S.palmY > -50 && S.palmY < H + 14) { + const y0 = S.palmY; + S.palmY += (156 / 1.6) * ds; + const ym = (y0 + S.palmY) / 2; + for (let y = max(0, floor(y0 - 12)); y <= min(H - 1, S.palmY + 12); y++) { + const wy = 1 - sm(4, 12, Math.abs(y - ym)); + if (wy <= 0) continue; + for (let x = max(0, floor(S.palmX - 13)); x <= min(W - 1, S.palmX + 13); x++) { + const w = 0.5 * wy * (1 - sm(7, 12, Math.abs(x - S.palmX))), p = y * W + x; + S.qx[p] += (0 - S.qx[p]) * w; + S.qy[p] += (PALM - S.qy[p]) * w; + } + } + } + if (S.flat > 0) { + S.flat -= ds; + for (let p = 0; p < N; p++) { + S.qx[p] += (0 - S.qx[p]) * 0.2; + S.qy[p] += (PALM - S.qy[p]) * 0.2; + } + } +} + +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.width = cl(k[2], 0, 1); + S.pile = cl(k[3], -1, 1); + if (!S.warm) { + // Open on velvet that has been written on for fourteen seconds, with the + // next palm due 2.5 seconds in. + const sp = S.speed; + S.speed = 2; + for (let i = 0; i < 420; i++) step(S, 1 / 30); + S.speed = sp; + S.palmT = 4.5; + S.palmY = -99; + S.warm = true; + } + if (input.btnPressed && input.btnPressed[0]) S.flat = 0.5; + step(S, d); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const c = S.c, dcol = S.d; + // Pile length: a short nap leans 36 degrees when brushed and its sheen is + // broad; a long plush one leans 72 degrees with a tight bright sheen. + const u = (S.pile + 1) / 2, bmax = ((36 + 36 * u) * PI) / 180, pex = 3 + 4 * u; + const az = LAZ + SWAY * sin((2 * PI * S.clock) / 12); + const lx = cos(az) * cos(ELEV), ly = sin(az) * cos(ELEV), lz = sin(ELEV); + const h0 = S.hue + 0.42, hh = h0 - floor(h0); + // Orange, gold and chartreuse velvet read rust, mustard or olive at the + // body's level, so there the body is lifted towards 0.8 and its darker + // faces lean to ruby. + const warm = sm(0.0, 0.05, hh) * (1 - sm(0.24, 0.34, hh)); + // The sheen is a pale tint of the dye. + hsv(h0, 0.18, 1, dcol); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + const qx = S.qx[p], qy = S.qy[p]; + let m = sqrt(qx * qx + qy * qy); + const ang = (m > 1e-4 ? atan2(qy, qx) : 0) + S.ga[p]; + m = min(1, m * S.gl[p]); + // The fibre axis t: leaning by beta towards the pile's way. + const be = m * bmax, sb = sin(be), cb = cos(be); + const tx = sb * cos(ang), ty = sb * sin(ang), tz = cb; + const tl = tx * lx + ty * ly + tz * lz, sl = sqrt(max(0, 1 - tl * tl)); + // Kajiya-Kay for a fibre: the lit side goes with the sine to the + // light, and a tuft seen end-on shows less of its side. The sheen is + // the reflection cone round the fibre crossing the eye straight ahead. + const diff = sl * (0.35 + 0.65 * sb); + const spec = pow(max(0, sl * sb - tl * cb), pex); + const body = (0.22 + 0.78 * diff) * S.gb[p]; + const rich = 1 - diff; + hsv(h0 - (0.012 + 0.06 * warm) * rich, 0.78 + 0.2 * rich - 0.25 * spec, 1, c); + const vb = body * (0.8 + 0.2 * warm) + 0.5 * warm * (1 - body), vs = 0.55 * spec; + display.setPixel( + x, y, + round(255 * min(1, c[0] * vb + dcol[0] * vs)), + round(255 * min(1, c[1] * vb + dcol[1] * vs)), + round(255 * min(1, c[2] * vb + dcol[2] * vs)), + ); + } +} From 4791c52aa36355bbcb97f84c11480ec501c711e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 16:13:20 +0000 Subject: [PATCH 4/9] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=203?= =?UTF-8?q?4=ED=8C=90:=20Lakes=20of=20Wada,=20Mood=20Tint?= 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_011BuccZdVDjYHZnwdNh4X1F --- .../pattern-study/patterns/lakes-of-wada.js | 530 ++++++++++++++++++ incoming/pattern-study/patterns/mood-tint.js | 185 ++++++ 2 files changed, 715 insertions(+) create mode 100644 incoming/pattern-study/patterns/lakes-of-wada.js create mode 100644 incoming/pattern-study/patterns/mood-tint.js diff --git a/incoming/pattern-study/patterns/lakes-of-wada.js b/incoming/pattern-study/patterns/lakes-of-wada.js new file mode 100644 index 000000000..86be63360 --- /dev/null +++ b/incoming/pattern-study/patterns/lakes-of-wada.js @@ -0,0 +1,530 @@ +// ===== Patternflow pattern ===== +// Title: Lakes of Wada +// Family: topology +// Idea: An island holds two lakes, and lake one, lake two and the sea take turns digging one-LED canals into the dry land until none of it lies farther than 32, then 16, 8, 4, 2 and 1 LEDs from the digging water, no water ever touching another, so the canals interleave until nearly every LED of land left is a one-LED wall between two waters; the finished labyrinth stands three seconds, the sea floods it and a new island rises. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Lakes of Wada (Kunizō Yoneyama, 1917, after Takeo Wada), pattern round 34 +// Round: 34 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Lakes=3..5, Wander=0..1 +// +// Knob 1: Hue. Turns the waters round the wheel together, each the same step from the next. At 0 the sea is azure, lake one pink and lake two lime; at 0.17 the sea is violet, lake one orange and lake two green; at 0.33 the three colours have moved on by one water and the sea is pink. +// Knob 2: Speed. At 2 three canal heads dig 160 LEDs a second between them, a fresh island is dug in about 15 seconds, the finished labyrinth stands 3 seconds, the flood takes 1.2 and the new island rises in 1; the panel opens on an island 6 seconds into its digging. At 0.2 they dig 16 a second and an island takes two and a half minutes; at 3, 240 a second and about 10 seconds. +// Knob 3: Lakes. How many waters share the shore, the sea counted: at 3 two lakes and the sea, a third of the wheel apart; at 4 three lakes, a quarter apart; at 5 four lakes, a fifth apart. Turning it floods the island and raises a new one with that many waters. +// Knob 4: Wander. How a canal finds the far land. At 0 it takes the shortest way through the land, ties broken toward the target, so canals run straight or in even steps; at 0.06 almost the same; at 1 its heading drifts up to 37 degrees with every LED and the canals wind, so the walls between them become a maze of bends. +// Buttons: K1 tap floods the island now and raises a new one. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, sqrt, sin, cos, atan2, exp, round, random, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => { + const t = cl((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + const rgb = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + out[0] = rgb[0]; + out[1] = rgb[1]; + out[2] = rgb[2]; +} + +const LAND = -1, BIG = 30000; +// Canal heads digging at once for the water whose turn it is. +const TIPS = 3; +// The first turn's distance; every later turn halves it, down to 1. +const D0 = 32; +// LEDs dug a second for each unit of Speed. +const RATE = 80; +// Distance searches a frame may run, so a late turn full of short canals +// spreads over frames instead of stalling one. +const SEARCHES = 14; +// LEDs a turn digs before it starts no new canal, a third of it once the +// distance is 4 or less, so the water whose turn comes at a fine distance +// cannot take the whole island. +const TURN_CAP = 160; +// Hue of the sea at Hue 0; the lakes follow round the wheel. +const H0 = 0.56; +const NB = [1, -1, W, -W], ANG = [0, PI, PI / 2, -PI / 2]; +const wrap = (a) => a - 2 * PI * floor((a + PI) / (2 * PI)); + +export function setup(params) { + params.s = { + hue: 0, speed: 2, n: 3, wander: 0.06, warm: false, clock: 0, acc: 0, + phase: "dig", pt: 0, stage: 0, turnDug: 0, roundDug: 0, roundTurns: 0, stageSteps: 0, searches: 0, rr: 0, + wat: new Int8Array(N), born: new Float32Array(N), give: new Uint8Array(N), + D: new Int16Array(N), C: new Int16Array(N), cw: -1, clr: 2, F: new Float32Array(N), FD: new Int16Array(N), fdMax: 1, fMax: 1, + Q: new Int32Array(N), tips: [], c: [0, 0, 0], next: null, + }; + for (let k = 0; k < TIPS; k++) params.s.tips.push({ on: false, sleep: false, pos: 0, tgt: 0, T: new Int16Array(N), steps: 0, cap: 0, h: 0 }); +} + +// ---------- the island ---------- + +// A smooth random field on a coarse lattice, eased between lattice points. +function field(gx, gy) { + const g = new Float32Array((gx + 1) * (gy + 1)); + for (let i = 0; i < g.length; i++) g[i] = random() * 2 - 1; + const out = new Float32Array(N); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const u = (x / (W - 1)) * gx, v = (y / (H - 1)) * gy; + const i = min(gx - 1, floor(u)), j = min(gy - 1, floor(v)); + const fu = sm(0, 1, u - i), fv = sm(0, 1, v - j); + const a = g[j * (gx + 1) + i], b = g[j * (gx + 1) + i + 1], c = g[(j + 1) * (gx + 1) + i], d = g[(j + 1) * (gx + 1) + i + 1]; + out[y * W + x] = (a + (b - a) * fu) * (1 - fv) + (c + (d - c) * fu) * fv; + } + return out; +} + +// A new island with n - 1 lakes: water 0 is the sea, 1..n-1 the lakes. +// Every water is kept at least two LEDs from every other, so no two touch +// even at a corner. +function makeIsland(n) { + const wat = new Int8Array(N), f = new Float32Array(N), Q = new Int32Array(N); + const a = field(5, 9), b = field(11, 21); + const rx = 29.5 + random() * 2, ry = 59 + random() * 3, cx = 31.5 + (random() - 0.5) * 3, cy = 63.5 + (random() - 0.5) * 6; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, dx = (x - cx) / rx, dy = (y - cy) / ry; + // The coast bends away from the panel's rim instead of running along it. + const rim = min(x, W - 1 - x, y, H - 1 - y); + f[p] = 0.6 * (1 - dx * dx - dy * dy) + 0.32 * a[p] + 0.12 * b[p] - 0.5 * (1 - sm(1.5, 6, rim)); + const edge = rim < 2; + wat[p] = f[p] > 0 && !edge ? LAND : 0; + } + // Keep the largest piece of land, and fill any pond the field left in it. + const lab = new Int32Array(N).fill(-1); + let best = -1, bestSize = 0; + for (let s = 0; s < N; s++) { + if (wat[s] !== LAND || lab[s] >= 0) continue; + let qh = 0, qt = 0; + Q[qt++] = s; + lab[s] = s; + while (qh < qt) { + const p = Q[qh++], x = p % W; + for (let k = 0; k < 4; k++) { + const q = p + NB[k]; + if ((k === 0 && x === W - 1) || (k === 1 && x === 0) || q < 0 || q >= N) continue; + if (wat[q] === LAND && lab[q] < 0) (lab[q] = s), (Q[qt++] = q); + } + } + if (qt > bestSize) (bestSize = qt), (best = s); + } + for (let p = 0; p < N; p++) if (wat[p] === LAND && lab[p] !== best) wat[p] = 0; + const sea = new Uint8Array(N); + let qh = 0, qt = 0; + for (let p = 0; p < N; p++) { + const x = p % W, y = (p / W) | 0; + if ((x === 0 || y === 0 || x === W - 1 || y === H - 1) && wat[p] !== LAND) (sea[p] = 1), (Q[qt++] = p); + } + while (qh < qt) { + const p = Q[qh++], x = p % W; + for (let k = 0; k < 4; k++) { + const q = p + NB[k]; + if ((k === 0 && x === W - 1) || (k === 1 && x === 0) || q < 0 || q >= N) continue; + if (!sea[q] && wat[q] !== LAND) (sea[q] = 1), (Q[qt++] = q); + } + } + for (let p = 0; p < N; p++) if (!sea[p]) wat[p] = LAND; + // Distance from the coast, through the land. + const cd = new Int16Array(N).fill(BIG); + qh = qt = 0; + for (let p = 0; p < N; p++) if (wat[p] === 0) (cd[p] = 0), (Q[qt++] = p); + while (qh < qt) { + const p = Q[qh++], x = p % W; + for (let k = 0; k < 4; k++) { + const q = p + NB[k]; + if ((k === 0 && x === W - 1) || (k === 1 && x === 0) || q < 0 || q >= N) continue; + if (wat[q] === LAND && cd[q] === BIG) (cd[q] = cd[p] + 1), (Q[qt++] = q); + } + } + // The lakes, spread over the island: each the best of 60 tries, as far as + // it can be from the lakes already placed. + const lakes = []; + for (let w = 1; w < n; w++) { + const r = 4 + random() * 2; + let bx = -1, by = -1, bs = -1; + for (let k = 0; k < 60; k++) { + const p = floor(random() * N); + if (wat[p] !== LAND || cd[p] < r + 9) continue; + const x = p % W, y = (p / W) | 0; + let s = 200 + random(); + for (const l of lakes) s = min(s, sqrt((x - l.x) ** 2 + (y - l.y) ** 2) - l.r - r); + if (s > bs) (bs = s), (bx = x), (by = y); + } + if (bx < 0) continue; + lakes.push({ x: bx, y: by, r }); + const ph = random() * 6.28; + for (let y = max(2, floor(by - r - 2)); y <= min(H - 3, by + r + 2); y++) + for (let x = max(2, floor(bx - r - 2)); x <= min(W - 3, bx + r + 2); x++) { + const p = y * W + x, dx = x - bx, dy = y - by; + if (wat[p] !== LAND || cd[p] < 3) continue; + const th = atan2(dy, dx); + if (sqrt(dx * dx + dy * dy) > r * (1 + 0.12 * sin(2 * th + ph) + 0.1 * sin(3 * th + 2 * ph))) continue; + let ok = true; + for (let v = -2; v <= 2 && ok; v++) for (let u = -2; u <= 2; u++) if (wat[p + v * W + u] > 0 && wat[p + v * W + u] !== w) ok = false; + if (ok) wat[p] = w; + } + } + let fMax = 0; + for (let p = 0; p < N; p++) fMax = max(fMax, f[p]); + return { wat, f, fMax }; +} + +function begin(S, isl) { + S.wat.set(isl.wat); + S.F.set(isl.f); + S.fMax = isl.fMax; + S.born.fill(-99); + S.give.fill(0); + S.stage = 0; + S.cw = -1; + S.turnDug = 0; + S.roundDug = 0; + S.roundTurns = 0; + S.stageSteps = 0; + for (const t of S.tips) (t.on = false), (t.sleep = false); +} + +// ---------- digging ---------- + +const turnWater = (S) => (S.stage + 1) % S.n; +const turnDist = (S) => max(1, floor(D0 / 2 ** S.stage)); +// How far a canal keeps from the other waters: a quarter of the turn's +// distance, and never closer than one LED of land between. A long early +// canal that ran up to another water's shore would close the way past it. +const turnClear = (S) => max(2, floor(turnDist(S) / 4)); + +// Chebyshev distance from every cell to the nearest water other than w, +// into S.C. The others hold still during w's turn, so once a turn is enough. +function clearance(S, w) { + const { wat, C, Q } = S; + C.fill(BIG); + let qh = 0, qt = 0; + for (let p = 0; p < N; p++) if (wat[p] !== LAND && wat[p] !== w) (C[p] = 0), (Q[qt++] = p); + while (qh < qt) { + const p = Q[qh++], x = p % W, y = (p / W) | 0; + for (let dy = -1; dy <= 1; dy++) + for (let dx = -1; dx <= 1; dx++) { + const u = x + dx, v = y + dy; + if (u < 0 || u >= W || v < 0 || v >= H) continue; + const q = v * W + u; + if (C[q] === BIG) (C[q] = C[p] + 1), (Q[qt++] = q); + } + } + S.cw = w; +} + +// Land a canal of w may take at all: inside the panel's rim and clear of +// the other waters. +const free = (S, c) => S.wat[c] === LAND && S.C[c] >= S.clr && c % W > 0 && c % W < W - 1 && c >= W && c < N - W; + +// May water w dig land cell c, coming from its own cell t? Of c's eight +// neighbours only t and the two cells beside t may be water, so a canal +// never runs alongside itself or another canal of its own water. +function canDig(S, c, t, w) { + if (!free(S, c)) return false; + const wat = S.wat, cx = c % W, cy = (c / W) | 0, ex = (t % W) - cx, ey = ((t / W) | 0) - cy; + for (let dy = -1; dy <= 1; dy++) + for (let dx = -1; dx <= 1; dx++) { + if ((!dx && !dy) || wat[c + dy * W + dx] === LAND) continue; + // t itself, or a cell beside t across the canal's line. + if ((ex !== 0 && dx === ex) || (ey !== 0 && dy === ey)) continue; + return false; + } + return true; +} + +// Land with no water at all among its eight neighbours: a canal can pass. +function open(S, c) { + if (!free(S, c)) return false; + const wat = S.wat; + for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) if (wat[c + dy * W + dx] !== LAND) return false; + return true; +} + +// Distance through the land from water w (and from the targets the other +// heads are already digging toward), into S.D. +function distances(S, w, mine) { + const { wat, D, Q } = S; + D.fill(BIG); + let qh = 0, qt = 0; + for (let p = 0; p < N; p++) if (wat[p] === w) (D[p] = 0), (Q[qt++] = p); + for (const t of S.tips) if (t !== mine && t.on && D[t.tgt] === BIG) (D[t.tgt] = 0), (Q[qt++] = t.tgt); + while (qh < qt) { + const p = Q[qh++], x = p % W; + for (let k = 0; k < 4; k++) { + const q = p + NB[k]; + if ((k === 0 && x === W - 1) || (k === 1 && x === 0) || q < 0 || q >= N) continue; + if (wat[q] === LAND && D[q] === BIG) (D[q] = D[p] + 1), (Q[qt++] = q); + } + } +} + +function dig(S, c, w) { + S.wat[c] = w; + S.born[c] = S.clock; + S.turnDug++; +} + +function finish(S, tip) { + tip.on = false; + S.give[tip.tgt] = 1; + for (const t of S.tips) t.sleep = false; +} + +// A new canal for this head: the land farthest from the water whose turn it +// is, and the nearest place on that water's shore to set out from. +// Returns 1 if it dug, 0 if this turn has nothing left for it, -1 to retry. +function startCanal(S, tip) { + const w = turnWater(S), d = turnDist(S), { D, give, Q } = S; + S.searches++; + if (S.cw !== w) clearance(S, w); + S.clr = turnClear(S); + distances(S, w, tip); + let m = d + 1, cnt = 0, X = -1; + for (let p = 0; p < N; p++) { + const v = D[p]; + if (v < m || v === BIG || give[p] || !free(S, p)) continue; + if (v > m) (m = v), (cnt = 0); + if (random() * ++cnt < 1) X = p; + } + if (X < 0) return 0; + // Distance to X through land a canal can cross; the first cells reached + // that w can dig from its shore are where the canal may start. + const T = tip.T; + T.fill(BIG); + let qh = 0, qt = 0, start = -1, from = -1, tmin = BIG, seen = 0; + T[X] = 0; + Q[qt++] = X; + const tryStart = (c) => { + for (let k = 0; k < 4; k++) { + const t = c + NB[k]; + if (S.wat[t] === w && canDig(S, c, t, w) && random() * ++seen < 1) (start = c), (from = t), (tmin = T[c]); + } + }; + tryStart(X); + while (qh < qt && T[Q[qh]] <= tmin) { + const p = Q[qh++], x = p % W; + for (let k = 0; k < 4; k++) { + const q = p + NB[k]; + if ((k === 0 && x === W - 1) || (k === 1 && x === 0) || T[q] !== BIG) continue; + if (open(S, q)) { + T[q] = T[p] + 1; + Q[qt++] = q; + } else if (S.wat[q] === LAND) { + T[q] = T[p] + 1; + if (T[q] <= tmin) tryStart(q); + } + } + } + if (start < 0) { + // No way in from w's shore: none of this land counts for the turn. + for (let i = 0; i < qt; i++) give[Q[i]] = 1; + give[X] = 1; + return -1; + } + // Finish the map past the start, so a wandering head still knows its way. + while (qh < qt) { + const p = Q[qh++], x = p % W; + for (let k = 0; k < 4; k++) { + const q = p + NB[k]; + if ((k === 0 && x === W - 1) || (k === 1 && x === 0) || T[q] !== BIG || !open(S, q)) continue; + T[q] = T[p] + 1; + Q[qt++] = q; + } + } + dig(S, start, w); + Object.assign(tip, { on: true, pos: start, tgt: X, steps: 1, cap: 2 * T[start] + 12 }); + tip.h = atan2(((start / W) | 0) - ((from / W) | 0), (start % W) - (from % W)); + if (T[start] === 0) finish(S, tip); + return 1; +} + +// One LED more for an active head: toward X along the land map, with the +// heading drifting as Wander allows. +function extend(S, tip) { + const w = turnWater(S), t = tip.pos, T = tip.T, u = S.wander; + const beta = 3.2 - 2.4 * u, kap = 0.5 + 2.0 * u, noise = 0.12 + 1.1 * u; + let best = -1, bs = -1e9; + for (let k = 0; k < 4; k++) { + const c = t + NB[k]; + if (!canDig(S, c, t, w)) continue; + const dT = T[c] === BIG ? 3 : T[c] - T[t]; + const s = -beta * dT + kap * cos(ANG[k] - tip.h) + noise * random(); + if (s > bs) (bs = s), (best = k); + } + if (best < 0) { + finish(S, tip); + return false; + } + const c = t + NB[best]; + dig(S, c, w); + tip.pos = c; + tip.steps++; + const tx = tip.tgt % W, ty = (tip.tgt / W) | 0, cx = c % W, cy = (c / W) | 0; + tip.h += 0.25 * wrap(ANG[best] - tip.h) + 0.12 * (1 - u) * wrap(atan2(ty - cy, tx - cx) - tip.h) + (random() - 0.5) * 1.3 * u; + if (T[c] === 0 || tip.steps >= tip.cap) finish(S, tip); + return true; +} + +function nextTurn(S) { + for (const t of S.tips) (t.on = false), (t.sleep = false); + S.give.fill(0); + S.cw = -1; + // A whole round of turns at distance 1 that found almost nothing to dig: + // the labyrinth is finished. + if (turnDist(S) === 1) { + S.roundDug += S.turnDug; + if (++S.roundTurns >= S.n) { + if (S.roundDug < 4) { + S.phase = "hold"; + S.pt = 0; + } + S.roundDug = 0; + S.roundTurns = 0; + } + } + S.stage++; + S.turnDug = 0; + S.stageSteps = 0; +} + +// One step of digging: one LED for one head, round robin. Once the turn has +// dug its share no new canal starts, but the heads finish the ones they are +// on, so a long canal into the far land is not left half dug. +function step(S, limited) { + if (++S.stageSteps > 1400) { + nextTurn(S); + return; + } + const full = S.turnDug >= (turnDist(S) > 4 ? TURN_CAP : TURN_CAP / 3); + for (let tries = 0; tries < TIPS * 2; tries++) { + const tip = S.tips[(S.rr = (S.rr + 1) % TIPS)]; + if (tip.on) { + if (extend(S, tip)) return; + continue; + } + if (tip.sleep || full) continue; + if (limited && S.searches >= SEARCHES) return; + const r = startCanal(S, tip); + if (r === 1) return; + if (r === 0) tip.sleep = true; + } + if (S.tips.every((t) => !t.on && (t.sleep || full))) nextTurn(S); +} + +// ---------- flood ---------- + +// Distance from the sea through everything else, for the flood front. +function floodMap(S) { + const { wat, FD, Q } = S; + FD.fill(BIG); + let qh = 0, qt = 0; + for (let p = 0; p < N; p++) if (wat[p] === 0) (FD[p] = 0), (Q[qt++] = p); + let m = 1; + while (qh < qt) { + const p = Q[qh++], x = p % W; + for (let k = 0; k < 4; k++) { + const q = p + NB[k]; + if ((k === 0 && x === W - 1) || (k === 1 && x === 0) || q < 0 || q >= N || FD[q] !== BIG) continue; + FD[q] = FD[p] + 1; + m = max(m, FD[q]); + Q[qt++] = q; + } + } + S.fdMax = m; +} + +function startFlood(S) { + floodMap(S); + for (const t of S.tips) t.on = false; + S.phase = "flood"; + S.pt = 0; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 3, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.wander = cl(k[3], 0, 1); + const n = round(cl(k[2], 3, 5)); + if (!S.warm) { + S.n = n; + begin(S, makeIsland(n)); + // Open on an island the waters have been digging for six seconds at + // this Speed: at 2, the first two turns done and the sea into its own. + const pre = round(6 * S.speed * RATE); + for (let i = 0; i < pre; i++) step(S, false); + S.clock = 10; + S.warm = true; + } else if (n !== S.n && S.phase !== "flood" && S.phase !== "rise") { + S.next = n; + startFlood(S); + } + if (input.btnPressed && input.btnPressed[0] && S.phase !== "flood" && S.phase !== "rise") startFlood(S); + const ds = (d * S.speed) / 2; + S.clock += ds; + S.pt += ds; + if (S.phase === "dig") { + S.acc += d * S.speed * RATE; + S.searches = 0; + let steps = 0; + while (S.acc >= 1 && steps < 200 && S.phase === "dig") { + step(S, true); + S.acc -= 1; + steps++; + if (S.searches >= SEARCHES) { + S.acc = min(S.acc, 2); + break; + } + } + } else if (S.phase === "hold" && S.pt > 3) startFlood(S); + else if (S.phase === "flood" && S.pt > 1.2) { + if (S.next) (S.n = S.next), (S.next = null); + begin(S, makeIsland(S.n)); + S.phase = "rise"; + S.pt = 0; + } else if (S.phase === "rise" && S.pt > 1) { + S.phase = "dig"; + S.pt = 0; + S.acc = 0; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { wat, born, c, n } = S; + const front = S.phase === "flood" ? S.fdMax * sm(0, 1.1, S.pt) : -1; + const level = S.phase === "rise" ? S.fMax * (1 - sm(0, 0.95, S.pt)) : -1; + const tipAt = new Set(); + for (const t of S.tips) if (t.on) tipAt.add(t.pos); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + let w = wat[p], glow = 0; + if (front >= 0 && S.FD[p] <= front) { + w = 0; + if (S.FD[p] > 0) glow = 0.7 * sm(4, 0, front - S.FD[p]); + } else if (level >= 0 && S.F[p] <= level) { + w = 0; + glow = 0.6 * sm(0.06, 0, level - S.F[p]) * (S.F[p] > 0 ? 1 : 0); + } + if (w === LAND) { + display.setPixel(x, y, 14, 12, 10); + continue; + } + if (front < 0 && level < 0) glow = tipAt.has(p) ? 1 : exp(-(S.clock - born[p]) / 0.22); + hsv(S.hue + H0 + w / n, 1, 1, c); + const v = 255, s = 1 - 0.85 * glow; + display.setPixel(x, y, round(v * (c[0] * s + 1 - s)), round(v * (c[1] * s + 1 - s)), round(v * (c[2] * s + 1 - s))); + } +} diff --git a/incoming/pattern-study/patterns/mood-tint.js b/incoming/pattern-study/patterns/mood-tint.js new file mode 100644 index 000000000..162cd7944 --- /dev/null +++ b/incoming/pattern-study/patterns/mood-tint.js @@ -0,0 +1,185 @@ +// ===== Patternflow pattern ===== +// Title: Mood Tint +// Family: cosmetic +// Idea: A thermochromic coat over the whole panel, deep violet while cool, that unseen fingertips press here and there: each press leaves a warm oval with faint fingerprint ridges, the heat spreads and drains back into the room, and the colour flips to pale orchid wherever the surface is above one temperature, so the prints bloom with crisp edges, lose their ridges first, merge with their neighbours like soft blobs and shrink back into the deep colour. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 34, after thermochromic lip tints and mood nail polish +// Round: 34 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Warmth=-1..1, Blend=0..1 +// +// Knob 1: Hue. The coat's colour once round the wheel; the warm shade is a pale tint 0.08 further round. At 0 deep violet turning pale orchid; at 0.24 wine red turning peach; at 0.3 orange turning cream; at 0.4 yellow turning pale lemon; at 0.5 green turning mint; at 0.75 deep teal turning pale sky; at 0.9 indigo turning lavender. Round orange, yellow and lime the cool coat stays bright, 0.78 of full instead of 0.44, so it never reads brown or olive. +// Knob 2: Speed. At 2 a group of one to three fingertips lands every 0.28 seconds on average and each is held 0.45 to 0.75 seconds; a lone print's ridges fade within about 0.4 seconds of lifting and the print is gone 1.7 seconds after. At 0.2 all of it runs ten times slower, at 3 one and a half times faster. +// Knob 3: Warmth. How warm the fingertips are over the room, the coat turning at 0.4. At 0 they are 1.0 and a lone print turns about 100 LEDs pale; at -1 they are 0.63, only the pressed ridges cross the turning point and each print is a broken whorl of about 60 LEDs gone within a second; at 1 they are 1.6, the prints whiten, spread to about 115 LEDs, last 3.5 seconds and run together. +// Knob 4: Blend. How far either side of the turning point the colour change is spread. At 0 it is 0.012 of a fingertip's warmth, so the edge of a print is one LED sharp; at 0.06 almost the same; at 0.5 it is 0.16 and the edges go soft; at 1 it is 0.61 and the heat shows as soft glows with the fresh ridges standing out white, like a mood ring. +// Buttons: K1 tap presses a whole palm somewhere on the panel for 1.1 seconds: a warm oval 34 by 46 LEDs with soft crease lines across it. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, sqrt, sin, cos, atan2, exp, round, random, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => { + const t = cl((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + const rgb = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + out[0] = rgb[0]; + out[1] = rgb[1]; + out[2] = rgb[2]; +} + +// Temperatures are in units of a fingertip at Warmth 0 above the room: the +// room is 0, a fingertip 1. The coat turns at THETA. +const THETA = 0.4; +// Heat diffusion in LED^2 a second and Newton cooling a second, at Speed 2. +const ALPHA = 1.1, COOL = 0.32; +// How fast the coat under a pressed fingertip takes its warmth, a second. +const GRIP = 7; +// Hue of the cool coat at Hue 0: violet. The warm coat is a paler, pinker shade. +const H0 = 0.76; +// The colour table spans temperatures -0.2 to 1.8. +const LUT = 512, T0 = -0.2, T1 = 1.8; + +export function setup(params) { + params.s = { + hue: 0, speed: 2, warmth: 0, blend: 0.06, warm: false, clock: 0, nextPress: 0, + T: new Float32Array(N), U: new Float32Array(N), room: new Float32Array(N), + presses: [], lut: new Uint8Array(LUT * 3), lutKey: "", c: [0, 0, 0], c2: [0, 0, 0], + }; + const S = params.s; + // The room is not quite even: a soft field of +-0.05 that only the cool + // shade shows. + const g = []; + for (let i = 0; i < 5 * 9; i++) g.push(random() * 2 - 1); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const u = (x / W) * 4, v = (y / H) * 8, i = floor(u), j = floor(v), fu = sm(0, 1, u - i), fv = sm(0, 1, v - j); + const a = g[j * 5 + i], b = g[j * 5 + i + 1], c = g[(j + 1) * 5 + i], d = g[(j + 1) * 5 + i + 1]; + S.room[y * W + x] = 0.05 * ((a + (b - a) * fu) * (1 - fv) + (c + (d - c) * fu) * fv); + } +} + +// A fingertip pad: an oval about 11 by 16 LEDs, tilted, with whorl ridges +// 2.3 LEDs apart round a core a little below its middle. +function fingertip(x, y) { + return { + x, y, rx: 5 + random() * 1.5, ry: 7 + random() * 1.8, a: (random() - 0.5) * 1.1, + core: 1 + random() * 2, ph: random() * 6.28, lam: 2.8, palm: false, + t: 0, dur: 0.45 + random() * 0.3, + }; +} + +// The coat's colour at each temperature, rebuilt when a knob moves it. +function buildLut(S) { + const key = S.hue.toFixed(3) + S.blend.toFixed(3); + if (key === S.lutKey) return; + S.lutKey = key; + const h = S.hue + H0, hh = h - floor(h), c = S.c, c2 = S.c2; + // Orange and yellow go brown or olive when deep, so round them the cool + // shade stays bright. + const lift = sm(0.0, 0.06, hh) * (1 - sm(0.24, 0.33, hh)); + const width = 0.012 + 0.6 * S.blend * S.blend; + for (let i = 0; i < LUT; i++) { + const T = T0 + ((T1 - T0) * i) / (LUT - 1); + const m = sm(THETA - width, THETA + width, T); + // Cool: deeper the colder it is, a little lighter just short of turning. + const vc = 0.44 + 0.34 * lift + 0.12 * sm(-0.1, THETA, T); + hsv(h, 0.9 - 0.1 * lift, vc, c); + // Warm: pale, and paler the hotter, so a ridge pressed hot is a whiter line. + const hot = sm(THETA, THETA + 0.7, T); + hsv(h + 0.08, (0.5 - 0.42 * hot) * (1 - 0.5 * lift), 1, c2); + for (let k = 0; k < 3; k++) S.lut[i * 3 + k] = round(255 * (c[k] + (c2[k] - c[k]) * m)); + } +} + +// One moment of the coat: presses warm it, heat spreads and drains away. +function step(S, dt) { + const { T, U, room } = S; + const Tf = 1.6 ** S.warmth; + // Fingertips arrive in little groups of one to three, about every 0.42 s. + S.nextPress -= dt; + if (S.nextPress <= 0) { + S.nextPress = 0.1 + random() * 0.35; + const n = random() < 0.55 ? 1 : random() < 0.7 ? 2 : 3; + const x0 = 6 + random() * (W - 12), y0 = 8 + random() * (H - 16); + for (let i = 0; i < n; i++) S.presses.push(fingertip(x0 + (i - (n - 1) / 2) * 12 + (random() - 0.5) * 3, y0 + (random() - 0.5) * 8)); + } + for (const P of S.presses) { + P.t += dt; + const env = sm(0, 0.12, P.t) * (1 - sm(P.dur - 0.1, P.dur, P.t)); + if (env <= 0) continue; + const ca = cos(P.a), sa = sin(P.a), R = max(P.rx, P.ry) + 2; + const k = 1 - exp(-GRIP * dt * env); + for (let y = max(0, floor(P.y - R)); y <= min(H - 1, P.y + R); y++) + for (let x = max(0, floor(P.x - R)); x <= min(W - 1, P.x + R); x++) { + const dx = x - P.x, dy = y - P.y, u = dx * ca + dy * sa, v = -dx * sa + dy * ca; + const rho = sqrt((u / P.rx) ** 2 + (v / P.ry) ** 2); + if (rho >= 1) continue; + let ridge; + if (P.palm) ridge = 0.5 + 0.5 * cos(u * 0.35 + 2.2 * sin(v * 0.11 + P.ph)) * cos(v * 0.5 + sin(u * 0.2)); + else { + const vv = v - P.core, rw = sqrt(u * u + vv * vv * 0.7) + 0.8 * sin(atan2(vv, u) + P.ph); + ridge = 0.5 + 0.5 * cos((2 * PI * rw) / P.lam); + } + const s = (1 - sm(0.72, 1, rho)) * (0.58 + 0.42 * ridge); + const p = y * W + x, target = Tf * s; + if (target > T[p]) T[p] += (target - T[p]) * k; + } + } + S.presses = S.presses.filter((P) => P.t < P.dur); + // Heat equation on the panel, its edges insulated, and Newton cooling + // toward the room. + const a = ALPHA * dt, c = COOL * dt; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, t = T[p]; + const l = x > 0 ? T[p - 1] : t, r = x < W - 1 ? T[p + 1] : t, u = y > 0 ? T[p - W] : t, d = y < H - 1 ? T[p + W] : t; + U[p] = t + a * (l + r + u + d - 4 * t) - c * (t - room[p]); + } + T.set(U); +} + +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.warmth = cl(k[2], -1, 1); + S.blend = cl(k[3], 0, 1); + if (!S.warm) { + // Open on a coat that has been pressed for four seconds. + for (let i = 0; i < 120; i++) step(S, 1 / 30); + S.warm = true; + } + if (input.btnPressed && input.btnPressed[0]) { + const P = fingertip(10 + random() * (W - 20), 22 + random() * (H - 44)); + Object.assign(P, { rx: 17, ry: 23, a: (random() - 0.5) * 0.5, palm: true, dur: 1.1 }); + S.presses.push(P); + } + // Time on the coat runs at Speed / 2; steps stay under 0.07 s so the heat + // equation holds steady. + let left = (d * S.speed) / 2; + while (left > 1e-6) { + const h = min(left, 0.07); + step(S, h); + left -= h; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + buildLut(S); + const { T, lut } = S; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = round(((cl(T[y * W + x], T0, T1) - T0) / (T1 - T0)) * (LUT - 1)) * 3; + display.setPixel(x, y, lut[i], lut[i + 1], lut[i + 2]); + } +} From e40858a5cb6f40a6bfbfa45e3877c03c42ccb831 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 16:17:26 +0000 Subject: [PATCH 5/9] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=203?= =?UTF-8?q?4=ED=8C=90:=20Thomson=20Problem,=20Cat=20Eye?= 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_011BuccZdVDjYHZnwdNh4X1F --- incoming/pattern-study/patterns/cat-eye.js | 185 ++++++++ .../pattern-study/patterns/thomson-problem.js | 422 ++++++++++++++++++ 2 files changed, 607 insertions(+) create mode 100644 incoming/pattern-study/patterns/cat-eye.js create mode 100644 incoming/pattern-study/patterns/thomson-problem.js diff --git a/incoming/pattern-study/patterns/cat-eye.js b/incoming/pattern-study/patterns/cat-eye.js new file mode 100644 index 000000000..41053ba1b --- /dev/null +++ b/incoming/pattern-study/patterns/cat-eye.js @@ -0,0 +1,185 @@ +// ===== Patternflow pattern ===== +// Title: Cat Eye +// Family: cosmetic +// Idea: A deep jelly polish full of magnetic shimmer flakes over an unseen gliding magnet: the flakes turn to follow its field, lie flat where the field runs along the surface and gather the light into a sharp bright band with a velvet falloff that trails the magnet as they rotate, and stand dark where the field dives into the surface. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 34, after magnetic cat-eye gel nail polish +// Round: 34 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Magnet=-1..1, Depth=0..1 +// +// Knob 1: Hue. The polish, once round the wheel. At 0 a deep violet jelly with a lilac-white band; at 0.2 berry pink, at 0.6 emerald, at 0.8 teal, at 0.9 sapphire. From 0.25 to 0.49, copper through gold to chartreuse, the jelly sinks toward black, a gold cat eye over a black base coat, and the band's glow turns to champagne. +// Knob 2: Speed. At 2 the magnet glides 15 LEDs either side of the middle and back every 7 seconds, 16 LEDs up and down every 11 and tilts 17 degrees either way every 17; flakes in its field line up in 0.15 seconds and tumble back over 1.8 seconds once it has passed. At 0.2 all of it ten times slower, at 3 one and a half times faster. +// Knob 3: Magnet. At 0 one bar magnet 76 LEDs long under the polish, one straight line. Toward -1 a second bar, poles reversed, comes in beside the first and splits the line in two, about 22 LEDs apart at -1. Toward 1 the second bar lies across the first and the line branches into a star. +// Knob 4: Depth. How far under the surface the magnet runs, 10 LEDs at 0 to 40 at 1. At 0.06 the band with its velvet is about 11 LEDs wide round a core of two; at 0.5 about 21, at 1 a soft glow about 30 LEDs wide. +// Buttons: K1 tap pulls the magnet away to four times its depth in 0.3 seconds and holds it there for 2.5: the band dissolves and the flakes tumble back into a scattered shimmer, then the magnet comes back in 0.5 seconds and the line forms again. + +const W = 64, H = 128, N = W * H; +// The magnet is a flat bar 76 LEDs long, magnetised across its width, lying +// along the panel under the polish: nine dipoles in a row. +const BAR = 76, PTS = 9; +const { floor, min, max, sqrt, sin, cos, exp, atan2, 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); + const rgb = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + out[0] = rgb[0]; + out[1] = rgb[1]; + out[2] = rgb[2]; +} + +function gauss() { + const u = 1 - random(), v = random(); + return sqrt(-2 * Math.log(u)) * cos(2 * PI * v); +} + +export function setup(params) { + const S = { + hue: 0, speed: 2, mag: 0, depth: 0.06, t: 0, warm: false, pull: 0, pullT: -1, + A: new Float32Array(N), TH: new Float32Array(N), JIT: new Float32Array(N), TEX: new Float32Array(N), SZ: new Float32Array(N), + R0: new Float32Array(N), RW: new Float32Array(N), RP: new Float32Array(N), + dip: new Float64Array(6 * 2 * PTS), nd: 0, ref: 1, + }; + for (let p = 0; p < N; p++) { + S.JIT[p] = 0.1 * gauss(); + S.TEX[p] = 0.8 + 0.4 * random(); + S.SZ[p] = 0.3 + 1.2 * random() ** 2; + S.R0[p] = (random() - 0.5) * PI; + S.RW[p] = 0.3 + 0.9 * random(); + S.RP[p] = 2 * PI * random(); + } + params.s = S; +} + +// A second bar has strength |Magnet|. Below 0 it lies beside the first, 0.9 +// of the depth away and poles reversed, for a double line; above 0 it lies +// across the first, for a star. + +function magnets(S) { + const sp = S.t, d = 10 + 30 * S.depth; + const cx = 32 + 15 * sin((2 * PI * sp) / 7), cy = 64 + 16 * sin((2 * PI * sp) / 11 + 1); + const ang = 0.3 * sin((2 * PI * sp) / 17); + // Along the bar (down the panel) and across it. + const lx = -sin(ang), ly = cos(ang), mx = cos(ang), my = sin(ang); + const k = S.mag, away = 1 + 3 * S.pull, dip = S.dip; + S.nd = 0; + const bar = (x, y, ax, ay, ex, ey, st) => { + for (let i = 0; i < PTS; i++) { + const u = (i / (PTS - 1) - 0.5) * BAR, o = 6 * S.nd++; + dip[o] = x + u * ax; + dip[o + 1] = y + u * ay; + dip[o + 2] = d * away; + dip[o + 3] = ex; + dip[o + 4] = ey; + dip[o + 5] = st / PTS; + } + }; + if (k < 0) { + const s = 0.9 * d * -k; + bar(cx - s * mx, cy - s * my, lx, ly, mx, my, 1); + bar(cx + s * mx, cy + s * my, lx, ly, -mx, -my, -k); + } else { + bar(cx, cy, lx, ly, mx, my, 1); + if (k > 0) bar(cx, cy, mx, my, -lx, -ly, k); + } + // The field at the surface right over the middle of one bar at this depth + // is the scale the flakes answer to. + let ref = 0; + for (let i = 0; i < PTS; i++) { + const u = (i / (PTS - 1) - 0.5) * BAR, r2 = u * u + d * d, ir = 1 / sqrt(r2); + ref += (ir * ir * ir) / PTS; + } + S.ref = ref; +} + +function step(S, dt) { + const sp = S.speed / 2, ds = dt * sp; + S.t += ds; + if (S.pullT >= 0) { + // Away in 0.3 s, held 2.5 s, back in 0.5 s. + S.pullT += ds; + const u = S.pullT; + S.pull = u < 0.3 ? u / 0.3 : u < 2.8 ? 1 : u < 3.3 ? 1 - (u - 2.8) / 0.5 : 0; + if (u >= 3.3) S.pullT = -1; + } + magnets(S); + const { A, TH, dip } = S, iref = 1 / S.ref, nd = S.nd; + const kOn = 1 - exp(-ds / 0.15), kOff = 1 - exp(-ds / 1.8); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let bx = 0, by = 0, bz = 0; + for (let o = 0; o < 6 * nd; o += 6) { + const ex = dip[o + 3], ey = dip[o + 4], st = dip[o + 5]; + const rx = x + 0.5 - dip[o], ry = y + 0.5 - dip[o + 1], rz = dip[o + 2]; + const r2 = rx * rx + ry * ry + rz * rz, ir = 1 / sqrt(r2), i3 = st * ir * ir * ir, md = 3 * (ex * rx + ey * ry) * ir * ir; + bx += i3 * (md * rx - ex); + by += i3 * (md * ry - ey); + bz += i3 * md * rz; + } + const bh = sqrt(bx * bx + by * by), b = sqrt(bh * bh + bz * bz) * iref; + const p = y * W + x, at = 1 - exp(-b / 0.25); + A[p] += (at - A[p]) * (at > A[p] ? kOn : kOff); + // Tilt of the field from the surface, -pi/2..pi/2; a flake's axis is a + // line, so tilts half a turn apart are the same. + const tt = atan2(bz, bh); + let dlt = tt - TH[p]; + if (dlt > PI / 2) dlt -= PI; + else if (dlt < -PI / 2) dlt += PI; + TH[p] += dlt * (1 - exp(-ds * (1 + 10 * at) / 0.6)); + if (TH[p] > PI / 2) TH[p] -= PI; + else if (TH[p] < -PI / 2) TH[p] += PI; + } +} + +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.mag = cl(k[2], -1, 1); + S.depth = cl(k[3], 0, 1); + if (!S.warm) { + // Open on flakes that have followed the magnet for two seconds. + const sp = S.speed; + S.speed = 2; + for (let i = 0; i < 60; i++) step(S, 1 / 30); + S.speed = sp; + S.warm = true; + } + if (input.btnPressed && input.btnPressed[0] && S.pullT < 0) S.pullT = 0; + step(S, d); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { A, TH, JIT, TEX, SZ, R0, RW, RP } = S, h = S.hue + 0.74, t = S.t; + // Orange and yellow jelly at level 40 reads brown on the panel. Round + // there (hue 0.0 to 0.22) the base sinks toward black, as a gold cat eye + // over a black base coat, and the band's glow turns paler and brighter. + const hh = h - floor(h), wy = max(0, 1 - ((hh - 0.11) / 0.12) ** 2); + const body = [0, 0, 0], glint = [0, 0, 0], vel = [0, 0, 0]; + hsv(h, 0.95 - 0.2 * wy, 1 - 0.7 * wy, body); + hsv(h + 0.02, 0.25 - 0.1 * wy, 1, glint); + hsv(h + 0.01, 0.6 - 0.25 * wy, 1 + 0.4 * wy, vel); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, a = A[p], th = TH[p], s2 = sin(th) * sin(th); + // Where the flake points: along the field as far as it is aligned, + // otherwise its own slow tumble. + const free = 6 * (R0[p] + 0.7 * sin(RW[p] * t + RP[p])); + const phi = a * (th + JIT[p]) + (1 - a) * free; + const g = exp(-(phi * phi) / (2 * 0.12 * 0.12)); + const v = a * a * exp(-(th * th) / (2 * 0.5 * 0.5)); + const lb = 40 * TEX[p] * (1 - 0.5 * a * s2), lg = 255 * g * (a + 0.3 * (1 - a) * SZ[p]), lv = 150 * v; + const r = body[0] * lb + glint[0] * lg + vel[0] * lv; + const gg = body[1] * lb + glint[1] * lg + vel[1] * lv; + const bb = body[2] * lb + glint[2] * lg + vel[2] * lv; + display.setPixel(x, y, round(min(255, r)), round(min(255, gg)), round(min(255, bb))); + } +} diff --git a/incoming/pattern-study/patterns/thomson-problem.js b/incoming/pattern-study/patterns/thomson-problem.js new file mode 100644 index 000000000..245756f43 --- /dev/null +++ b/incoming/pattern-study/patterns/thomson-problem.js @@ -0,0 +1,422 @@ +// ===== Patternflow pattern ===== +// Title: Thomson Problem +// Family: physics +// Idea: Equal point charges on a slowly turning sphere push each other into an almost triangular lattice, and the charges with five neighbours instead of six, which Euler's formula keeps at exactly twelve more than those with seven, glow gold among the pale-centred blue ones. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Thomson problem (J. J. Thomson, 1904), with the scars of Bowick, Nelson and Travesset (2000), pattern round 34 +// Round: 34 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Charges=100..480, Heat=0..1 +// +// Knob 1: Hue. Turns the sphere once round the wheel. At 0 the sixfold charges are sky blue with pale centres, the fivefold gold and the sevenfold magenta; at 0.25 they are violet, green and orange, at 0.5 orange, blue and green, at 0.75 green, magenta and blue. +// Knob 2: Speed. How fast the sphere turns and the charges move. At 2 the sphere turns 0.2 radians a second, once in 31 seconds, and the charges take 60 descent steps a second, so a scattered sphere is a lattice again within half a second; at 0.2 a turn takes over five minutes and the lattice five seconds; at 3 a turn takes 21 seconds. +// Knob 3: Charges. 100 to 480 charges. At 100, 11 LEDs apart, the opening holds exactly twelve fivefold charges and no sevenfold one, 0.02 above the known minimum energy 4448.351; at 200 two 5-7 pairs are left over, at 300 four; at 480, 5 LEDs apart, the twelve have grown into short scars, gold-magenta-gold chains, 25 fivefold and 13 sevenfold charges in all. Turning it drops new charges at random places or takes the last ones away, and the lattice heals round them. +// Knob 4: Heat. A random kick at each step, up to 0.075 of the lattice spacing at 1. At 0.06 the lattice holds still; at 0.5 one to three 5-7 pairs flicker in and out every few seconds; at 1 four to twelve pairs come and go at once, with a fourfold charge now and then, and the fivefold charges wander. +// Buttons: K1 tap scatters every charge at random over the sphere, and they anneal back into a lattice in front of you. + +const W = 64, H = 128, CX = 31.5, CY = 63.5, RS = 29; +const NMIN = 100, NMAX = 480; +// Sixfold, fivefold and sevenfold charges: hue offset, saturation at the +// rim and at the centre, and peak level. +const TYPES = [ + [0, 1, 0.4, 215], + [0.58, 1, 0.45, 250], + [0.3, 1, 0.4, 240], +]; +// The glow's sigma in LEDs, or a fifth of the lattice spacing where that is less. +const SIG = 2.15; +const { floor, min, max, abs, sqrt, sin, cos, exp, log, round, random, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// Lattice spacing on the unit sphere: each charge holds (sqrt3/2) a^2 of the +// 4 pi of surface. +const spacing = (n) => sqrt((8 * PI) / (sqrt(3) * n)); + +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); + const rgb = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + out[0] = rgb[0]; + out[1] = rgb[1]; + out[2] = rgb[2]; +} + +function gauss() { + const u = 1 - random(), v = random(); + return sqrt(-2 * log(u)) * cos(2 * PI * v); +} + +function scatter(X, from, to) { + for (let i = from; i < to; i++) { + const z = 2 * random() - 1, f = 2 * PI * random(), r = sqrt(1 - z * z); + X[3 * i] = r * cos(f); + X[3 * i + 1] = r * sin(f); + X[3 * i + 2] = z; + } +} + +// One step of projected gradient descent on E = sum 1/r_ij, with momentum +// beta. The step is 0.15 a^3; plain descent at 0.25 a^3 rang and never +// settled. No charge moves more than a fifth of the spacing in one step, so a +// fresh scatter, where two charges can sit almost on top of each other, stays +// calm. Heat adds a random kick of up to 0.075 a per step. +function relax(S, kick, beta) { + const { X, F, M } = S, n = S.n; + F.fill(0, 0, 3 * n); + for (let i = 0; i < n; i++) { + const xi = X[3 * i], yi = X[3 * i + 1], zi = X[3 * i + 2]; + let fx = 0, fy = 0, fz = 0; + for (let j = i + 1; j < n; j++) { + const dx = xi - X[3 * j], dy = yi - X[3 * j + 1], dz = zi - X[3 * j + 2]; + const r2 = dx * dx + dy * dy + dz * dz + 1e-9, q = 1 / (r2 * sqrt(r2)); + fx += dx * q; + fy += dy * q; + fz += dz * q; + F[3 * j] -= dx * q; + F[3 * j + 1] -= dy * q; + F[3 * j + 2] -= dz * q; + } + F[3 * i] += fx; + F[3 * i + 1] += fy; + F[3 * i + 2] += fz; + } + const a = spacing(n), eta = 0.15 * a * a * a, cap = 0.2 * a, k = kick * a; + for (let i = 0; i < n; i++) { + const o = 3 * i, x = X[o], y = X[o + 1], z = X[o + 2]; + let fx = F[o], fy = F[o + 1], fz = F[o + 2]; + const d = fx * x + fy * y + fz * z; + fx = beta * M[o] + eta * (fx - d * x); + fy = beta * M[o + 1] + eta * (fy - d * y); + fz = beta * M[o + 2] + eta * (fz - d * z); + const e = fx * x + fy * y + fz * z; + fx -= e * x; + fy -= e * y; + fz -= e * z; + const m = sqrt(fx * fx + fy * fy + fz * fz); + if (m > cap) { + fx *= cap / m; + fy *= cap / m; + fz *= cap / m; + } + M[o] = fx; + M[o + 1] = fy; + M[o + 2] = fz; + if (k > 0) { + fx += k * gauss(); + fy += k * gauss(); + fz += k * gauss(); + } + const nx = x + fx, ny = y + fy, nz = z + fz, l = 1 / sqrt(nx * nx + ny * ny + nz * nz); + X[o] = nx * l; + X[o + 1] = ny * l; + X[o + 2] = nz * l; + } +} + +function energy(X, n) { + let e = 0; + for (let i = 0; i < n; i++) + for (let j = i + 1; j < n; j++) { + const dx = X[3 * i] - X[3 * j], dy = X[3 * i + 1] - X[3 * j + 1], dz = X[3 * i + 2] - X[3 * j + 2]; + e += 1 / sqrt(dx * dx + dy * dy + dz * dz); + } + return e; +} + +// The Delaunay triangulation of points on a sphere is their convex hull. +// Incremental hull: each new point removes the faces it sees and joins the +// horizon around them to itself. E maps a directed edge u->v to its face. +function hull(S) { + const P = S.U, n = S.n, { fa, fb, fc, fx, fy, fz, fd, vis, E } = S; + let nf = 0; + const add = (a, b, c) => { + const ax = P[3 * a], ay = P[3 * a + 1], az = P[3 * a + 2]; + const ux = P[3 * b] - ax, uy = P[3 * b + 1] - ay, uz = P[3 * b + 2] - az; + const vx = P[3 * c] - ax, vy = P[3 * c + 1] - ay, vz = P[3 * c + 2] - az; + const x = uy * vz - uz * vy, y = uz * vx - ux * vz, z = ux * vy - uy * vx; + fa[nf] = a; + fb[nf] = b; + fc[nf] = c; + fx[nf] = x; + fy[nf] = y; + fz[nf] = z; + fd[nf] = x * ax + y * ay + z * az; + vis[nf] = 0; + E[a * n + b] = nf; + E[b * n + c] = nf; + E[c * n + a] = nf; + return nf++; + }; + // The fourth point of the first tetrahedron is the one farthest from the + // plane of the first three. + let k = 3, best = -1; + for (let j = 3; j < n; j++) { + const v = abs(orient(P, 0, 1, 2, j)); + if (v > best) (best = v), (k = j); + } + let live = []; + if (orient(P, 0, 1, 2, k) < 0) live.push(add(0, 1, 2), add(1, 0, k), add(2, 1, k), add(0, 2, k)); + else live.push(add(0, 2, 1), add(0, 1, k), add(1, 2, k), add(2, 0, k)); + const seen = [], rim = []; + for (let p = 3; p < n; p++) { + if (p === k) continue; + const px = P[3 * p], py = P[3 * p + 1], pz = P[3 * p + 2]; + seen.length = 0; + for (let q = 0; q < live.length; q++) { + const f = live[q]; + if (fx[f] * px + fy[f] * py + fz[f] * pz - fd[f] > 1e-13) { + vis[f] = 1; + seen.push(f); + } + } + if (!seen.length) continue; + rim.length = 0; + for (const f of seen) { + const a = fa[f], b = fb[f], c = fc[f]; + if (!vis[E[b * n + a]]) rim.push(a, b); + if (!vis[E[c * n + b]]) rim.push(b, c); + if (!vis[E[a * n + c]]) rim.push(c, a); + } + const next = []; + for (const f of live) if (!vis[f]) next.push(f); + for (let r = 0; r < rim.length; r += 2) next.push(add(rim[r], rim[r + 1], p)); + live = next; + } + // Keep the faces as the triangulation the flips look after from here on. + S.nt = live.length; + for (let t = 0; t < live.length; t++) { + const f = live[t]; + S.ta[t] = fa[f]; + S.tb[t] = fb[f]; + S.tc[t] = fc[f]; + } + for (let t = 0; t < S.nt; t++) { + const a = S.ta[t], b = S.tb[t], c = S.tc[t]; + E[a * n + b] = t; + E[b * n + c] = t; + E[c * n + a] = t; + } +} + +// (d - a) . ((b - a) x (c - a)): positive when d lies outside the plane of the +// triangle abc, which on the sphere means inside its circumcircle. +function orient(P, a, b, c, d) { + const ax = P[3 * a], ay = P[3 * a + 1], az = P[3 * a + 2]; + const ux = P[3 * b] - ax, uy = P[3 * b + 1] - ay, uz = P[3 * b + 2] - az; + const vx = P[3 * c] - ax, vy = P[3 * c + 1] - ay, vz = P[3 * c + 2] - az; + const wx = P[3 * d] - ax, wy = P[3 * d + 1] - ay, wz = P[3 * d + 2] - az; + return wx * (uy * vz - uz * vy) + wy * (uz * vx - ux * vz) + wz * (ux * vy - uy * vx); +} + +// Between hulls the triangulation follows the charges by edge flips. An edge +// flips only once the far point is inside the circumcircle by 2% of what a +// perfect lattice holds it outside, so a nearly square quad does not toggle +// a 5-7 pair from frame to frame. A face turned inside out asks for a new hull. +function maintain(S) { + const P = S.U, n = S.n, { ta, tb, tc, E } = S; + for (let t = 0; t < S.nt; t++) { + const a = ta[t], b = tb[t], c = tc[t]; + const ux = P[3 * b] - P[3 * a], uy = P[3 * b + 1] - P[3 * a + 1], uz = P[3 * b + 2] - P[3 * a + 2]; + const vx = P[3 * c] - P[3 * a], vy = P[3 * c + 1] - P[3 * a + 1], vz = P[3 * c + 2] - P[3 * a + 2]; + const d = (uy * vz - uz * vy) * P[3 * a] + (uz * vx - ux * vz) * P[3 * a + 1] + (ux * vy - uy * vx) * P[3 * a + 2]; + if (d <= 0) return hull(S); + } + const a = spacing(n), thr = 0.02 * 0.433 * a * a * a * a; + for (let pass = 0; pass < 8; pass++) { + let flips = 0; + for (let t = 0; t < S.nt; t++) + for (let e = 0; e < 3; e++) { + const u = e === 0 ? ta[t] : e === 1 ? tb[t] : tc[t]; + const v = e === 0 ? tb[t] : e === 1 ? tc[t] : ta[t]; + const w = e === 0 ? tc[t] : e === 1 ? ta[t] : tb[t]; + if (u > v) continue; + const s = E[v * n + u]; + const x = ta[s] !== u && ta[s] !== v ? ta[s] : tb[s] !== u && tb[s] !== v ? tb[s] : tc[s]; + if (orient(P, u, v, w, x) > thr) { + ta[t] = w; + tb[t] = u; + tc[t] = x; + ta[s] = x; + tb[s] = v; + tc[s] = w; + E[w * n + u] = t; + E[u * n + x] = t; + E[x * n + w] = t; + E[x * n + v] = s; + E[v * n + w] = s; + E[w * n + x] = s; + flips++; + break; + } + } + if (!flips) break; + } +} + +// U is the display position: the charge's position smoothed over about seven +// steps and put back on the sphere, so heat shows as a wobble and not a buzz, +// and the lattice is read from where the charges sit, not where they shiver. +function smooth(S, k) { + const { X, V, U } = S; + for (let i = 0; i < S.n; i++) { + const o = 3 * i; + V[o] += k * (X[o] - V[o]); + V[o + 1] += k * (X[o + 1] - V[o + 1]); + V[o + 2] += k * (X[o + 2] - V[o + 2]); + const l = 1 / sqrt(V[o] * V[o] + V[o + 1] * V[o + 1] + V[o + 2] * V[o + 2]); + U[o] = V[o] * l; + U[o + 1] = V[o + 1] * l; + U[o + 2] = V[o + 2] * l; + } +} + +function degrees(S) { + const { deg, ta, tb, tc } = S; + deg.fill(0, 0, S.n); + for (let t = 0; t < S.nt; t++) { + deg[ta[t]]++; + deg[tb[t]]++; + deg[tc[t]]++; + } +} + +// The opening: a few scatters, each run down 400 steps with heavy momentum +// (0.9), and the one with the lowest energy kept. At 100 charges the best of +// five opens 0.02 above the known minimum 4448.3506, with just the twelve. +function settle(S) { + const n = S.n, tries = n <= 160 ? 5 : n <= 300 ? 3 : 1, steps = n <= 300 ? 400 : 300; + const keep = new Float64Array(3 * n); + let best = Infinity; + for (let r = 0; r < tries; r++) { + scatter(S.X, 0, n); + S.M.fill(0); + for (let i = 0; i < steps; i++) relax(S, 0, 0.9); + const e = energy(S.X, n); + if (e < best) { + best = e; + keep.set(S.X.subarray(0, 3 * n)); + } + } + S.X.set(keep); + S.V.set(keep); + S.U.set(keep); + S.M.fill(0); + hull(S); + degrees(S); + for (let i = 0; i < n; i++) { + S.f5[i] = S.deg[i] < 6 ? 1 : 0; + S.f7[i] = S.deg[i] > 6 ? 1 : 0; + } +} + +function resize(S, n) { + const old = S.n; + S.n = n; + if (n > old) { + scatter(S.X, old, n); + for (let i = 3 * old; i < 3 * n; i++) { + S.V[i] = S.U[i] = S.X[i]; + S.M[i] = 0; + } + for (let i = old; i < n; i++) S.f5[i] = S.f7[i] = 0; + } + hull(S); +} + +export function setup(params) { + const F = 16 * NMAX + 64; + params.s = { + n: 0, hue: 0, speed: 2, heat: 0.06, turn: 0, acc: 0, warm: false, + X: new Float64Array(3 * NMAX), F: new Float64Array(3 * NMAX), M: new Float64Array(3 * NMAX), V: new Float64Array(3 * NMAX), U: new Float64Array(3 * NMAX), + fa: new Int32Array(F), fb: new Int32Array(F), fc: new Int32Array(F), fx: new Float64Array(F), fy: new Float64Array(F), fz: new Float64Array(F), fd: new Float64Array(F), vis: new Uint8Array(F), + E: new Int32Array(NMAX * NMAX), ta: new Int32Array(2 * NMAX), tb: new Int32Array(2 * NMAX), tc: new Int32Array(2 * NMAX), nt: 0, + deg: new Int32Array(NMAX), f5: new Float32Array(NMAX), f7: new Float32Array(NMAX), + R: new Float32Array(W * H), G: new Float32Array(W * H), B: new Float32Array(W * H), c: [0, 0, 0, 0, 0, 0], + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, NMIN, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.heat = cl(k[3], 0, 1); + const n = round(cl(k[2], NMIN, NMAX)), kick = 0.075 * S.heat; + if (!S.warm) { + S.n = n; + settle(S); + S.warm = true; + } else if (n !== S.n) resize(S, n); + if (input.btnPressed && input.btnPressed[0]) { + scatter(S.X, 0, S.n); + S.M.fill(0); + S.V.set(S.X.subarray(0, 3 * S.n)); + S.U.set(S.X.subarray(0, 3 * S.n)); + hull(S); + } + // 30 steps a second for each unit of Speed, at most six a frame. + S.acc += d * S.speed * 30; + let steps = min(6, floor(S.acc)); + S.acc -= floor(S.acc); + for (; steps > 0; steps--) { + relax(S, kick, 0.5); + smooth(S, 0.15); + } + maintain(S); + degrees(S); + const ease = 1 - exp(-d / 0.08); + for (let i = 0; i < S.n; i++) { + S.f5[i] += ease * ((S.deg[i] < 6 ? 1 : 0) - S.f5[i]); + S.f7[i] += ease * ((S.deg[i] > 6 ? 1 : 0) - S.f7[i]); + } + S.turn += d * S.speed * 0.1; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { U, R, G, B, c, f5, f7 } = S, n = S.n; + R.fill(0); + G.fill(0); + B.fill(0); + // The sphere turns about an axis leaning 20 degrees toward the viewer. + const ct = cos(S.turn), st = sin(S.turn), cx = cos(0.35), sx = sin(0.35); + const h = S.hue + 0.55; + // Each charge is one soft gaussian glow. Its colour runs from nearly white + // at the centre to the full hue at the rim: hsv(hue, s, 1) is white less s + // times (1 - the pure hue), and s falls with the square of the glow. + const T = TYPES.map(([dh, sr, sc, lv]) => { + const q = [0, 0, 0]; + hsv(h + dh, 1, 1, q); + return [1 - q[0], 1 - q[1], 1 - q[2], sr, sr - sc, lv]; + }); + const sp = spacing(n) * RS, sig = min(SIG, 0.2 * sp); + for (let i = 0; i < n; i++) { + const x0 = U[3 * i], y0 = U[3 * i + 1], z0 = U[3 * i + 2]; + const x1 = ct * x0 + st * z0, z1 = -st * x0 + ct * z0; + const y = cx * y0 - sx * z1, z = sx * y0 + cx * z1, x = x1; + if (z < -0.02) continue; + const t = cl((z + 0.02) / 0.37, 0, 1), w = t * t * (3 - 2 * t); + const a5 = f5[i], a7 = f7[i], a6 = 1 - a5 - a7; + for (let j = 0; j < 6; j++) c[j] = T[0][j] * a6 + T[1][j] * a5 + T[2][j] * a7; + const L = c[5] * w * (0.55 + 0.45 * z), s = sig * (1 + 0.25 * (a5 + a7)); + // Foreshortened toward the rim: narrower across the radius. + const px = CX + RS * x, py = CY - RS * y, rr = sqrt(x * x + y * y) || 1, rx = x / rr, ry = -y / rr; + const sr = s * (0.45 + 0.55 * z), ir = 1 / (2 * sr * sr), it = 1 / (2 * s * s), ext = 3 * s; + for (let yy = max(0, floor(py - ext)); yy <= min(H - 1, py + ext); yy++) + for (let xx = max(0, floor(px - ext)); xx <= min(W - 1, px + ext); xx++) { + const dx = xx - px, dy = yy - py, dr = dx * rx + dy * ry, dq = -dx * ry + dy * rx; + const g = exp(-dr * dr * ir - dq * dq * it), v = L * g, sat = v * (c[3] - c[4] * g * g * g), p = yy * W + xx; + R[p] += v - sat * c[0]; + G[p] += v - sat * c[1]; + B[p] += v - sat * c[2]; + } + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + display.setPixel(x, y, round(min(255, R[p])), round(min(255, G[p])), round(min(255, B[p]))); + } +} From 83f5468d8b90899fcaf1cbcfe5c62c08147a9fd9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 16:27:35 +0000 Subject: [PATCH 6/9] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=203?= =?UTF-8?q?4=ED=8C=90:=20Moving=20Sofa,=20Pentomino=20Cover?= 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_011BuccZdVDjYHZnwdNh4X1F --- .../pattern-study/patterns/moving-sofa.js | 261 ++++++++++++++ .../pattern-study/patterns/pentomino-cover.js | 332 ++++++++++++++++++ 2 files changed, 593 insertions(+) create mode 100644 incoming/pattern-study/patterns/moving-sofa.js create mode 100644 incoming/pattern-study/patterns/pentomino-cover.js diff --git a/incoming/pattern-study/patterns/moving-sofa.js b/incoming/pattern-study/patterns/moving-sofa.js new file mode 100644 index 000000000..3ab976fd9 --- /dev/null +++ b/incoming/pattern-study/patterns/moving-sofa.js @@ -0,0 +1,261 @@ +// ===== Patternflow pattern ===== +// Title: Moving Sofa +// Family: geometry +// Idea: Hammersley's sofa, two quarter discs set apart by a block with a half disc cut from its inner side, is carried up the right leg of a hallway fourteen LEDs wide, round the corner with the inner corner riding inside its hole, across the top and down the left leg, and in every turn a faint strip trailing behind it is shaved off by the walls until only the sofa is left. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: moving sofa problem (Leo Moser, 1966) and Hammersley's sofa (John Hammersley, 1968), pattern round 34 +// Round: 34 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Hole=-1..1, Trail=0..1 +// +// Knob 1: Hue. Turns every colour once round the wheel. At 0 the sofa is teal with a gold rim on its hole and the walls are lavender; at 0.25 the sofa is violet with a green rim, at 0.5 brick red with a blue rim, at 0.75 green with a pink rim. +// Knob 2: Speed. At 2 a sofa comes up the right leg at 34 LEDs a second, takes 2.4 seconds over each corner and 1.4 seconds across the top, and a new one sets off every 6.2 seconds; at 0.2 a corner takes 24 seconds, at 3 it takes 1.6 and a sofa sets off every 4.1 seconds. +// Knob 3: Hole. The radius r of the hole, in hallway widths. At 0 it is Hammersley's 2/pi = 0.637, the largest sofa of this shape, area pi/2 + 2/pi = 2.2074 widths squared and 46 LEDs long; at -1 r = 0 and the sofa is a half disc of area pi/2 = 1.571 that pivots on the inner corner; at 1 r = 1, area 2, and the two quarter discs meet at a single point above the hole. +// Knob 4: Trail. How long the floor keeps a glow where the sofas have passed, a fade time of 0.25 seconds at 0, 0.36 at 0.06, 2.5 at 0.5 and 7.8 at 1 (at Speed 2). Turned up, the whole hallway glows except the two outer corners, which no sofa ever reaches. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, sqrt, sin, cos, abs, round, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (t) => { + t = cl(t, 0, 1); + return t * t * (3 - 2 * t); +}; + +// One corner of the hallway, in widths: the arm the sofa comes along is +// 0 <= wy <= 1, wx <= 1, the arm it leaves by 0 <= wx <= 1, wy <= 1, the +// inner corner the origin and the outer corner (1, 1). On the panel the right +// corner has wx running up the right leg and wy across, X = XC + U wy and +// Y = YC - U wx; the left corner is its mirror image, X -> 64 - X. +const U = 14, XC = 49, YC = 15; + +// Hole 0 is r = 2/pi, where the area pi/2 + 2r - pi r^2 / 2 is largest. +// Gerver's sofa of 1992, area 2.2195, is larger still and is not drawn here. +const R_OPT = 2 / PI; +const holeRadius = (k) => (k < 0 ? R_OPT * (1 + k) : R_OPT + (1 - R_OPT) * k); + +// Signed distance, in widths, to Hammersley's sofa with hole radius r: the +// points with y >= 0 within 1 of the segment from (-r, 0) to (r, 0), less +// the half disc of radius r on the origin. Negative inside. Below the flat +// side it is the distance to the two flat pieces, so a sharp end of the hole +// touching a wall does not bleed into the wall's LED. +function sdf(x, y, r) { + const ax = abs(x); + if (y < 0) { + const ex = ax < r ? r - ax : ax > r + 1 ? ax - r - 1 : 0; + return sqrt(ex * ex + y * y); + } + const dx = ax > r ? ax - r : 0; + return max(-y, sqrt(dx * dx + y * y) - 1, r - sqrt(x * x + y * y)); +} +// Whether the sofa's nearest edge to (x, y) is the hole's semicircle. +const notch = (x, y, r) => r > 0.02 && x * x + y * y < (r + 0.12) * (r + 0.12) && abs(x) < r + 0.04; + +// How far (x, y), in the sofa's frame, lies inside every position the +// hallway has taken in a turn so far, th of the way round, starting from the +// strip behind the sofa's front: negative once some wall has passed through +// it. The outer wall a = 1 has kept (x - r, y) within 1 of the origin in the +// directions 0..th, the wall b = 1 has done the same for (x + r, y) in the +// directions 90..90+th degrees, and the inner corner has cut from the hole +// the part below the chord from (-r, 0) at angle th. At th = 90 degrees +// this is the sofa. +function block(x, y, th, r) { + const ct = cos(th), st = sin(th); + let s = min(y, 1 - y, r + 1 - x); + const vx = x - r, vy = y; + if (vy >= 0 && vx * st - vy * ct >= 0) s = min(s, 1 - sqrt(vx * vx + vy * vy)); + else s = min(s, 1 - vx, 1 - vx * ct - vy * st); + const wx = x + r, wy = y; + if (wx <= 0 && wy * st + wx * ct >= 0) s = min(s, 1 - sqrt(wx * wx + wy * wy)); + else s = min(s, 1 - wy, 1 + wx * st - wy * ct); + return min(s, max(sqrt(x * x + y * y) - r, -wx * st + wy * ct)); +} + +// The motion, seen from the sofa: the hallway is turned through th and its +// inner corner sits at c. While th runs from 0 to 90 degrees c runs round +// the hole's semicircle, c = r (cos 2th, sin 2th). By Thales the inner walls +// then pass through the hole's two ends, and the outer walls stay tangent to +// the two quarter discs, so the sofa touches both outer walls all the way +// round. Phases, in seconds at Speed 2: up the right leg, round the right +// corner, across the top, round the left corner (the right one mirrored and +// run backwards), down the left leg. +const TA = 4.4, TB = 2.4, TC = 1.4; +const T1 = TA, T2 = T1 + TB, T3 = T2 + TC, T4 = T3 + TB, LIFE = T4 + TA; +// A new sofa sets off as the last one starts round the right corner, so it +// reaches that corner as the last one leaves the left. +const PERIOD = TB + TC + TB; +// Up the leg from below the panel, and the run across the top. +const DA = 9.2, EC = (XC - (64 - XC)) / U; +const KE = 0.3, VE = 1 / (1 - KE / 2); +// Constant speed, then a stop over the last KE of the way. +function easeOut(t) { + t = cl(t, 0, 1); + if (t <= 1 - KE) return VE * t; + const s = t - (1 - KE); + return VE * (1 - KE + s - (s * s) / (2 * KE)); +} +function turn(th, r, o) { + o.th = th; + o.cx = r * cos(2 * th); + o.cy = r * sin(2 * th); +} +// Pose at phase ph: o.m is 1 when it is given in the left corner's mirrored +// frame. +function pose(ph, r, o) { + o.m = 0; + o.cy = 0; + if (ph < T1) { + o.th = 0; + o.cx = r + DA * (1 - easeOut(ph / TA)); + } else if (ph < T2) turn((PI / 2) * sm((ph - T1) / TB), r, o); + else if (ph < T3) { + o.th = PI / 2; + o.cx = -r - (EC - 2 * r) * sm((ph - T2) / TC); + } else { + o.m = 1; + if (ph < T4) turn((PI / 2) * (1 - sm((ph - T3) / TB)), r, o); + else { + o.th = 0; + o.cx = r + DA * (1 - easeOut(1 - (ph - T4) / TA)); + } + } + return o; +} + +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); + const rgb = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + out[0] = rgb[0]; + out[1] = rgb[1]; + out[2] = rgb[2]; +} + +// Calls f(X, Y, sx, sy) for every LED near the sofa at pose P, with (sx, sy) +// the LED centre in the sofa's frame. +function eachLed(P, r, f, xa = -r - 1, xb = r + 1) { + const ct = cos(P.th), st = sin(P.th); + let x0 = 99, x1 = -99, y0 = 999, y1 = -999; + for (const [sx, sy] of [[xa, 0], [xb, 0], [xa, 1], [xb, 1]]) { + const ex = sx - P.cx, ey = sy - P.cy, wx = ex * ct + ey * st, wy = -ex * st + ey * ct; + const X = XC + U * wy, Y = YC - U * wx; + x0 = min(x0, X), x1 = max(x1, X), y0 = min(y0, Y), y1 = max(y1, Y); + } + x0 = max(0, floor(x0) - 1), x1 = min(W - 1, floor(x1) + 1), y0 = max(0, floor(y0) - 1), y1 = min(H - 1, floor(y1) + 1); + for (let Y = y0; Y <= y1; Y++) + for (let Xm = x0; Xm <= x1; Xm++) { + const wx = (YC - (Y + 0.5)) / U, wy = (Xm + 0.5 - XC) / U; + f(P.m ? W - 1 - Xm : Xm, Y, P.cx + wx * ct - wy * st, P.cy + wx * st + wy * ct); + } +} + +export function setup(params) { + params.s = { + clock: 0, hue: 0, speed: 2, hole: 0, trail: 0.06, r: R_OPT, warm: false, + wake: new Float32Array(N), buf: new Float32Array(N * 3), + pose: { th: 0, cx: 0, cy: 0, m: 0 }, c1: [0, 0, 0], c2: [0, 0, 0], c3: [0, 0, 0], + }; +} + +// Advance the clock by dc and let the floor's glow where the sofas have been +// fade. +function step(S, dc) { + S.clock += dc; + const keep = exp(-dc / (0.25 + 1.5 * S.trail + 6 * S.trail * S.trail)), r = S.r, wake = S.wake; + for (let p = 0; p < N; p++) wake[p] *= keep; + const first = floor((S.clock - LIFE) / PERIOD) + 1, last = floor(S.clock / PERIOD); + for (let n = first; n <= last; n++) { + const ph = S.clock - n * PERIOD; + if (ph < 0 || ph > LIFE) continue; + eachLed(pose(ph, r, S.pose), r, (X, Y, sx, sy) => { + const cov = cl(0.5 - sdf(sx, sy, r) * U, 0, 1), p = Y * W + X; + if (cov > wake[p]) wake[p] = cov; + }); + } +} + +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.hole = cl(k[2], -1, 1); + S.trail = cl(k[3], 0, 1); + S.r = holeRadius(S.hole); + if (!S.warm) { + // Open with a sofa 1.6 s short of the right corner and four seconds of + // memory already laid down behind it. + S.clock = T1 - 1.6 - 4; + for (let i = 0; i < 60; i++) step(S, 4 / 60); + S.warm = true; + } + step(S, (d * S.speed) / 2); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { buf, c1, c2, c3, wake } = S, r = S.r, h = S.hue + 0.52; + buf.fill(0); + const put = (p, c, v) => { + buf[p * 3] = max(buf[p * 3], c[0] * v); + buf[p * 3 + 1] = max(buf[p * 3 + 1], c[1] * v); + buf[p * 3 + 2] = max(buf[p * 3 + 2], c[2] * v); + }; + const set = (p, c, v, al) => { + buf[p * 3] += (c[0] * v - buf[p * 3]) * al; + buf[p * 3 + 1] += (c[1] * v - buf[p * 3 + 1]) * al; + buf[p * 3 + 2] += (c[2] * v - buf[p * 3 + 2]) * al; + }; + + // Walls one LED outside the hallway, the solid between the legs hatched + // as on a floor plan, and the floor's glow where the sofas have passed. + hsv(h + 0.2, 0.45, 1, c1); + for (let Y = 0; Y < H; Y++) put(Y * W, c1, 70), put(Y * W + 63, c1, 70); + for (let X = 0; X < W; X++) put(X, c1, 70); + for (let Y = 15; Y < H; Y++) put(Y * W + 15, c1, 70), put(Y * W + 48, c1, 70); + for (let X = 15; X <= 48; X++) put(15 * W + X, c1, 70); + for (let Y = 16; Y < H; Y++) for (let X = 16; X < 48; X++) if ((X + Y) % 5 === 0) put(Y * W + X, c1, 40); + hsv(h + 0.05, 0.6, 1, c2); + for (let p = 0; p < N; p++) if (wake[p] > 0.01) put(p, c2, 100 * wake[p]); + + const first = floor((S.clock - LIFE) / PERIOD) + 1, last = floor(S.clock / PERIOD); + for (let n = first; n <= last; n++) { + const ph = S.clock - n * PERIOD; + if (ph < 0 || ph > LIFE) continue; + const P = pose(ph, r, S.pose); + // In a turn, the rest of the strip behind the sofa that every position + // of the hallway so far still holds: the walls shave it down to the sofa. + const u = ph >= T1 && ph < T2 ? (ph - T1) / TB : ph >= T3 && ph < T4 ? (ph - T3) / TB : -1; + if (u >= 0) { + const th = (PI / 2) * sm(u), al = sm(u / 0.12), back = min(r + 1 / max(sin(th), 1e-3), 12); + hsv(h, 0.45, 1, c1); + eachLed(P, r, (X, Y, sx, sy) => { + // Each corner's own hallway runs on for ever; the panel's other + // corner stops the strip. + if (X < 1 || X > 62 || Y < 1 || (Y > 14 && X > 14 && X < 49)) return; + const x = P.m ? -sx : sx, ex = cl(0.5 + block(x, sy, th, r) * U, 0, 1) - cl(0.5 - sdf(sx, sy, r) * U, 0, 1); + if (ex > 0) put(Y * W + X, c1, 80 * ex * al); + }, P.m ? -r - 1 : -back, P.m ? back : r + 1); + } + hsv(h, 0.85, 1, c1); + hsv(h, 0.3, 1, c2); + hsv(h + 0.62, 0.55, 1, c3); + eachLed(P, r, (X, Y, sx, sy) => { + const dl = sdf(sx, sy, r) * U, cov = cl(0.5 - dl, 0, 1); + if (cov <= 0) return; + const p = Y * W + X; + set(p, c1, 150, cov); + put(p, notch(sx, sy, r) ? c3 : c2, 255 * cl(1.6 + dl, 0, 1) * cov); + }); + } + // The inner corners. + hsv(h + 0.62, 0.3, 1, c3); + put(15 * W + 48, c3, 255); + put(15 * W + 15, c3, 255); + + for (let p = 0; p < N; p++) + display.setPixel(p % W, (p / W) | 0, round(min(255, buf[p * 3])), round(min(255, buf[p * 3 + 1])), round(min(255, buf[p * 3 + 2]))); +} diff --git a/incoming/pattern-study/patterns/pentomino-cover.js b/incoming/pattern-study/patterns/pentomino-cover.js new file mode 100644 index 000000000..da144f1fc --- /dev/null +++ b/incoming/pattern-study/patterns/pentomino-cover.js @@ -0,0 +1,332 @@ +// ===== Patternflow pattern ===== +// Title: Pentomino Cover +// Family: algo +// Idea: A rectangle of sixty cells stands on the panel and Knuth's dancing links fill it with the twelve pentominoes as an exact cover, always placing a piece on the cell with the fewest choices left and taking pieces back out as it backs up, so the board flickers with pieces dropping in and leaving their outlines as they go, and each full cover holds for two seconds under a sweep of light and adds a tick below the board. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Dancing Links and Algorithm X (Donald Knuth, 2000) on Solomon Golomb's pentominoes (1953), pattern round 34 +// Round: 34 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Board=0..3, Shuffle=0..1 +// +// Knob 1: Hue. Turns the twelve piece colours, 30 degrees apart, together round the wheel. At 0 the F is sky blue, the I blue, the X green and the U red-orange; at 0.5 each piece takes the colour the piece six letters on had. +// Knob 2: Speed. Steps of the search a second, one step being a column chosen, a row tried or a step back: 5 at 0.2, where each piece can be followed in and out; 394 at 2, where the 6 by 10 search averages 1532 steps, about 4 seconds, from one cover to the next; 4457 at 3. +// Knob 3: Board. 0 is 6 by 10 cells of 10 LEDs, with 2339 covers; 1 is 5 by 12 of 10 LEDs, 1010 covers; 2 is 4 by 15 of 8 LEDs, 368 covers; 3 is 3 by 20 of 6 LEDs, where the whole search, 33,545 steps, finds only 2 covers before it starts again. Covers are counted once for each set of rotations and mirror images. +// Knob 4: Shuffle. How far the rows are shuffled when a search starts: at 0 every column tries its placements in the fixed order F to Z, top left first; at 0.06 a row moves by up to 12% of the list; at 1 the order is random and the same board gives other covers first. Turning it by more than 0.04 starts a new search. +// Buttons: K1 tap starts a new search in a new shuffled order. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, round, abs, pow, exp, random } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The twelve pentominoes, F I L N P T U V W X Y Z. +const PIECES = [ + [[1, 0], [2, 0], [0, 1], [1, 1], [1, 2]], + [[0, 0], [1, 0], [2, 0], [3, 0], [4, 0]], + [[0, 0], [0, 1], [1, 1], [2, 1], [3, 1]], + [[0, 0], [1, 0], [1, 1], [2, 1], [3, 1]], + [[0, 0], [1, 0], [0, 1], [1, 1], [0, 2]], + [[0, 0], [1, 0], [2, 0], [1, 1], [1, 2]], + [[0, 0], [2, 0], [0, 1], [1, 1], [2, 1]], + [[0, 0], [0, 1], [0, 2], [1, 2], [2, 2]], + [[0, 0], [0, 1], [1, 1], [1, 2], [2, 2]], + [[1, 0], [0, 1], [1, 1], [2, 1], [1, 2]], + [[1, 0], [0, 1], [1, 1], [2, 1], [3, 1]], + [[0, 0], [1, 0], [1, 1], [1, 2], [2, 2]], +]; +const P_ = 4, X_ = 9; +// Boards, columns by rows, and the LEDs a cell takes. +const BOARDS = [[6, 10, 10], [5, 12, 10], [4, 15, 8], [3, 20, 6]]; + +// Every distinct orientation of a piece: the four turns first, then the +// four turns of its mirror image, so flip marks the mirrored ones. +function orientations(cells) { + const out = [], seen = new Set(); + for (let f = 0; f < 2; f++) + for (let q = 0; q < 4; q++) { + let c = cells.map(([x, y]) => (f ? [-x, y] : [x, y])); + for (let k = 0; k < q; k++) c = c.map(([x, y]) => [-y, x]); + const mx = min(...c.map((p) => p[0])), my = min(...c.map((p) => p[1])); + c = c.map(([x, y]) => [x - mx, y - my]).sort((a, b) => a[1] - b[1] || a[0] - b[0]); + const key = c.join(";"); + if (!seen.has(key)) seen.add(key), out.push({ cells: c, flip: f }); + } + return out; +} +const ORIENT = PIECES.map(orientations); + +// The exact cover matrix: one column per piece, one per cell, and one +// secondary column that keeps the count to solutions up to symmetry. The X +// may only sit in the board's first quarter; when it sits on a mirror line +// of the board the P may not be mirrored, and the secondary column holds +// those two choices apart. +function placements(bw, bh) { + const rows = []; + PIECES.forEach((_, pi) => { + for (const o of ORIENT[pi]) { + const ow = max(...o.cells.map((p) => p[0])) + 1, oh = max(...o.cells.map((p) => p[1])) + 1; + for (let y = 0; y + oh <= bh; y++) + for (let x = 0; x + ow <= bw; x++) { + let sec = pi === P_ && o.flip === 1; + if (pi === X_) { + const cx = x + 1, cy = y + 1; + if (2 * cx > bw - 1 || 2 * cy > bh - 1) continue; + sec = (bw % 2 === 1 && 2 * cx === bw - 1) || (bh % 2 === 1 && 2 * cy === bh - 1); + } + rows.push({ piece: pi, cells: o.cells.map(([a, b]) => (y + b) * bw + x + a), sec }); + } + } + }); + return rows; +} + +const MAXN = 16000; +function newSearch(S, again) { + const [bw, bh] = BOARDS[S.board]; + const rows = placements(bw, bh); + // Shuffle moves each row by up to twice that share of the list. A search + // started again keeps the order it had. + if (!again) S.jit = rows.map(() => random()); + const key = rows.map((_, i) => i / rows.length + 2 * S.shuffle * S.jit[i]); + const order = rows.map((_, i) => i).sort((a, b) => key[a] - key[b]); + const NC = 12 + bw * bh, SEC = NC + 1; + const { L, R, U, D, C, RW, SZ } = S; + for (let c = 0; c <= NC; c++) (L[c] = c - 1), (R[c] = c + 1), (U[c] = D[c] = C[c] = c), (SZ[c] = 0); + L[0] = NC; + R[NC] = 0; + L[SEC] = R[SEC] = U[SEC] = D[SEC] = C[SEC] = SEC; + let k = SEC + 1; + S.rows = order.map((i) => rows[i]); + S.rows.forEach((r, ri) => { + const cols = [1 + r.piece, ...r.cells.map((c) => 13 + c)]; + if (r.sec) cols.push(SEC); + const first = k; + for (const c of cols) { + C[k] = c; + RW[k] = ri; + U[k] = U[c]; + D[k] = c; + D[U[c]] = k; + U[c] = k; + SZ[c]++; + L[k] = k - 1; + R[k] = k + 1; + k++; + } + L[first] = k - 1; + R[k - 1] = first; + }); + S.bw = bw; + S.bh = bh; + S.cs = BOARDS[S.board][2]; + S.k = 0; + S.mode = 0; + S.done = false; + S.found = 0; + S.hold = 0; + S.cell.fill(-1); + S.age.fill(9); + S.ghost.fill(-1); + S.gAge.fill(9); + S.builtBoard = S.board; + S.builtShuffle = S.shuffle; +} + +function cover(S, c) { + const { L, R, U, D, C, SZ } = S; + L[R[c]] = L[c]; + R[L[c]] = R[c]; + for (let i = D[c]; i !== c; i = D[i]) + for (let j = R[i]; j !== i; j = R[j]) (U[D[j]] = U[j]), (D[U[j]] = D[j]), SZ[C[j]]--; +} +function uncover(S, c) { + const { L, R, U, D, C, SZ } = S; + for (let i = U[c]; i !== c; i = U[i]) + for (let j = L[i]; j !== i; j = L[j]) SZ[C[j]]++, (U[D[j]] = j), (D[U[j]] = j); + L[R[c]] = c; + R[L[c]] = c; +} +function place(S, node, on) { + const r = S.rows[S.RW[node]]; + for (const c of r.cells) { + if (on) (S.cell[c] = r.piece), (S.age[c] = 0); + else (S.ghost[c] = S.cell[c]), (S.gAge[c] = 0), (S.cell[c] = -1); + } +} + +// Algorithm X, one node of the search tree a call: enter a level and cover +// the column with the fewest rows left, try its next row, or step back. +// Returns true when the board is full. +function searchStep(S) { + const { L, R, D, C, SZ, col, row } = S; + if (S.mode === 0) { + if (R[0] === 0) { + S.mode = 2; + return true; + } + let c = R[0], best = 1e9; + for (let j = R[0]; j !== 0; j = R[j]) if (SZ[j] < best) (best = SZ[j]), (c = j); + cover(S, c); + col[S.k] = c; + row[S.k] = D[c]; + S.mode = 1; + } else if (S.mode === 1) { + const r = row[S.k]; + if (r === col[S.k]) { + uncover(S, col[S.k]); + S.mode = 2; + return false; + } + for (let j = R[r]; j !== r; j = R[j]) cover(S, C[j]); + place(S, r, true); + S.k++; + S.mode = 0; + } else { + if (S.k === 0) { + S.done = true; + return false; + } + S.k--; + const r = row[S.k]; + place(S, r, false); + for (let j = L[r]; j !== r; j = L[j]) uncover(S, C[j]); + row[S.k] = D[r]; + S.mode = 1; + } + return false; +} + +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); + const rgb = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + out[0] = rgb[0]; + out[1] = rgb[1]; + out[2] = rgb[2]; +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, board: 0, shuffle: 0.06, warm: false, acc: 0, sweep: 0, + L: new Int32Array(MAXN), R: new Int32Array(MAXN), U: new Int32Array(MAXN), D: new Int32Array(MAXN), + C: new Int32Array(MAXN), RW: new Int32Array(MAXN), SZ: new Int32Array(80), col: new Int32Array(16), row: new Int32Array(16), + cell: new Int8Array(60), age: new Float32Array(60), ghost: new Int8Array(60), gAge: new Float32Array(60), + key: new Int8Array(60), pc: new Int8Array(N), c: [0, 0, 0], + }; +} + +// A full cover holds for HOLD seconds; the search takes rate(Speed) steps a +// second. +const HOLD = 2; +const rate = (sp) => 5 * pow(2, 3.5 * (sp - 0.2)); + +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.board = cl(round(k[2]), 0, 3); + S.shuffle = cl(k[3], 0, 1); + const pressed = input.btnPressed && input.btnPressed[0]; + if (!S.warm || pressed || S.board !== S.builtBoard || abs(S.shuffle - S.builtShuffle) > 0.04) { + newSearch(S); + if (!S.warm) { + // Open about 1100 steps short of the first full board, so at Speed 2 + // the first solution comes near 2.8 s. + let n = 0; + while (!searchStep(S) && !S.done) n++; + newSearch(S, true); + for (let i = 0; i < n - 1100; i++) searchStep(S); + S.age.fill(9); + S.ghost.fill(-1); + S.warm = true; + } + } + for (let c = 0; c < 60; c++) (S.age[c] += d), (S.gAge[c] += d); + if (S.hold > 0) { + S.hold = max(0, S.hold - d); + S.sweep += d / HOLD; + return; + } + if (S.done) { + newSearch(S); + return; + } + S.acc += d * rate(S.speed); + while (S.acc >= 1) { + S.acc--; + if (searchStep(S)) { + S.found++; + S.hold = HOLD; + S.sweep = 0; + S.acc = 0; + break; + } + if (S.done) break; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { bw, bh, cs, cell, age, ghost, gAge, key, pc, c } = S, base = S.hue + 0.55; + const BW = bw * cs + 1, BH = bh * cs + 1, ox = floor((W - BW) / 2), oy = floor((H - BH) / 2); + for (let p = 0; p < N; p++) display.setPixel(p % W, (p / W) | 0, 0, 0, 0); + // Each cell shows its piece, or for 0.18 s after the search takes a + // piece out, that piece's outline fading. + for (let i = 0; i < bw * bh; i++) key[i] = cell[i] >= 0 ? cell[i] : ghost[i] >= 0 && gAge[i] < 0.18 ? 12 + ghost[i] : -1; + const at = (x, y) => (x < 0 || y < 0 || x >= bw || y >= bh ? -2 : key[y * bw + x]); + // A cell's own LEDs show its piece, a grid line between two cells of one + // piece that piece, any other grid line the dark seam. + pc.fill(-1); + for (let Y = 0; Y < BH; Y++) + for (let X = 0; X < BW; X++) { + const gx = X % cs === 0, gy = Y % cs === 0, cx = floor(X / cs), cy = floor(Y / cs); + let q; + if (!gx && !gy) q = at(cx, cy); + else if (gx && !gy) q = at(cx - 1, cy) === at(cx, cy) ? at(cx, cy) : -1; + else if (!gx && gy) q = at(cx, cy - 1) === at(cx, cy) ? at(cx, cy) : -1; + else { + const a = at(cx - 1, cy - 1); + q = a === at(cx, cy - 1) && a === at(cx - 1, cy) && a === at(cx, cy) ? a : -1; + } + pc[(oy + Y) * W + ox + X] = q; + } + // A light sweeps across the board while a solution holds. + const band = S.hold > 0 ? -0.25 + 1.5 * cl(S.sweep, 0, 1) : -9; + for (let Y = 0; Y < BH; Y++) + for (let X = 0; X < BW; X++) { + const p = (oy + Y) * W + ox + X, q = pc[p]; + const cx = min(bw - 1, floor(X / cs)), cy = min(bh - 1, floor(Y / cs)), ci = cy * bw + cx; + const g = (X / BW + Y / BH) / 2 - band, lit = S.hold > 0 ? exp(-g * g * 90) : 0; + if (q >= 0 && q < 12) { + // A slight bevel: lighter next to a seam above or to the left, + // darker next to one below or to the right. + const up = pc[p - W] < 0 || pc[p - W] >= 12, lf = pc[p - 1] < 0 || pc[p - 1] >= 12; + const dn = pc[p + W] < 0 || pc[p + W] >= 12, rt = pc[p + 1] < 0 || pc[p + 1] >= 12; + const fresh = cl(1 - age[ci] / 0.15, 0, 1); + const v = min(1, 0.82 + (up || lf ? 0.18 : 0) - (dn || rt ? 0.2 : 0) + 0.15 * fresh + 0.3 * lit); + hsv(base + q / 12, (0.72 - 0.3 * fresh) * (1 - 0.7 * lit), 1, c); + display.setPixel(ox + X, oy + Y, round(c[0] * 255 * v), round(c[1] * 255 * v), round(c[2] * 255 * v)); + } else if (q >= 12) { + // A piece just taken out leaves its outline for a moment. + if (pc[p - W] === q && pc[p + W] === q && pc[p - 1] === q && pc[p + 1] === q) continue; + const f = cl(1 - gAge[ci] / 0.18, 0, 1); + hsv(base + (q - 12) / 12, 0.6, 1, c); + display.setPixel(ox + X, oy + Y, round(c[0] * 150 * f), round(c[1] * 150 * f), round(c[2] * 150 * f)); + } else if (X === 0 || Y === 0 || X === BW - 1 || Y === BH - 1) display.setPixel(ox + X, oy + Y, 44, 44, 54); + else if (X % cs === 0 && Y % cs === 0) display.setPixel(ox + X, oy + Y, 40, 40, 48); + } + // One tick under the board for each solution this search has found, 31 + // to a row; a full row turns dim and the next starts over it. + const ty = oy + BH + (oy > 6 ? 2 : 0), full = floor(S.found / 31), part = S.found % 31; + for (let i = 0; i < 31; i++) { + const on = i < part, x = 1 + 2 * i; + if (!on && full === 0) continue; + hsv(base + 0.1 * (on ? full : full - 1), on ? 0.25 : 0.5, 1, c); + const v = on ? (i === part - 1 && S.hold > 0 ? 255 : 190) : 70; + for (let y = ty; y < min(H, ty + 2); y++) display.setPixel(x, y, round(c[0] * v), round(c[1] * v), round(c[2] * v)); + } +} From 0d359c6e68e91407622e0e24adcf1339cb7b86f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 16:49:59 +0000 Subject: [PATCH 7/9] =?UTF-8?q?=EC=99=80=EB=8B=A4=EC=9D=98=20=ED=98=B8?= =?UTF-8?q?=EC=88=98=EA=B0=80=20=EC=A7=80=EB=8F=84=EC=9D=98=20=EC=B6=95=20?= =?UTF-8?q?=EB=81=9D=EC=9D=84=20=EB=84=98=EC=A7=80=20=EC=95=8A=EA=B2=8C=20?= =?UTF-8?q?=EB=B9=A8=EB=A6=AC=20=ED=8C=8C=EA=B3=A0=20=EC=83=89=EC=9D=84=20?= =?UTF-8?q?=EC=A1=B0=EA=B8=88=20=EB=88=84=EB=A5=B8=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 색 0.723 과 움직임 0.017 이 축의 끝을 넘어 지도 여섯 칸을 비웠다. RATE 80→110, 미리 파는 시간 6→3초, 채도 0.88 로 색 0.633, 움직임 0.023. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BuccZdVDjYHZnwdNh4X1F --- .../pattern-study/patterns/lakes-of-wada.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/incoming/pattern-study/patterns/lakes-of-wada.js b/incoming/pattern-study/patterns/lakes-of-wada.js index 86be63360..d02bf3f40 100644 --- a/incoming/pattern-study/patterns/lakes-of-wada.js +++ b/incoming/pattern-study/patterns/lakes-of-wada.js @@ -12,7 +12,7 @@ // @knobs Hue=0..1, Speed=0.2..3, Lakes=3..5, Wander=0..1 // // Knob 1: Hue. Turns the waters round the wheel together, each the same step from the next. At 0 the sea is azure, lake one pink and lake two lime; at 0.17 the sea is violet, lake one orange and lake two green; at 0.33 the three colours have moved on by one water and the sea is pink. -// Knob 2: Speed. At 2 three canal heads dig 160 LEDs a second between them, a fresh island is dug in about 15 seconds, the finished labyrinth stands 3 seconds, the flood takes 1.2 and the new island rises in 1; the panel opens on an island 6 seconds into its digging. At 0.2 they dig 16 a second and an island takes two and a half minutes; at 3, 240 a second and about 10 seconds. +// Knob 2: Speed. At 2 three canal heads dig 220 LEDs a second between them, a fresh island is dug in about 11 seconds, the finished labyrinth stands 3 seconds, the flood takes 1.2 and the new island rises in 1; the panel opens on an island 3 seconds into its digging. At 0.2 they dig 22 a second and an island takes nearly two minutes; at 3, 330 a second and about 8 seconds. // Knob 3: Lakes. How many waters share the shore, the sea counted: at 3 two lakes and the sea, a third of the wheel apart; at 4 three lakes, a quarter apart; at 5 four lakes, a fifth apart. Turning it floods the island and raises a new one with that many waters. // Knob 4: Wander. How a canal finds the far land. At 0 it takes the shortest way through the land, ties broken toward the target, so canals run straight or in even steps; at 0.06 almost the same; at 1 its heading drifts up to 37 degrees with every LED and the canals wind, so the walls between them become a maze of bends. // Buttons: K1 tap floods the island now and raises a new one. @@ -40,7 +40,7 @@ const TIPS = 3; // The first turn's distance; every later turn halves it, down to 1. const D0 = 32; // LEDs dug a second for each unit of Speed. -const RATE = 80; +const RATE = 110; // Distance searches a frame may run, so a late turn full of short canals // spreads over frames instead of stalling one. const SEARCHES = 14; @@ -50,6 +50,11 @@ const SEARCHES = 14; const TURN_CAP = 160; // Hue of the sea at Hue 0; the lakes follow round the wheel. const H0 = 0.56; +// Seconds of digging before the first frame, and the waters' saturation. +// Six seconds at a RATE of 80 and full colour put the study's colour at 0.723 +// and its motion at 0.017, both past the ends of their map axes, which +// emptied six cells of the map. +const PRE = 3, SAT = 0.88; const NB = [1, -1, W, -W], ANG = [0, PI, PI / 2, -PI / 2]; const wrap = (a) => a - 2 * PI * floor((a + PI) / (2 * PI)); @@ -459,9 +464,9 @@ export function update(dt, input, params) { if (!S.warm) { S.n = n; begin(S, makeIsland(n)); - // Open on an island the waters have been digging for six seconds at - // this Speed: at 2, the first two turns done and the sea into its own. - const pre = round(6 * S.speed * RATE); + // Open on an island the waters have been digging for PRE seconds at + // this Speed. + const pre = round(PRE * S.speed * RATE); for (let i = 0; i < pre; i++) step(S, false); S.clock = 10; S.warm = true; @@ -523,7 +528,7 @@ export function draw(display, params) { continue; } if (front < 0 && level < 0) glow = tipAt.has(p) ? 1 : exp(-(S.clock - born[p]) / 0.22); - hsv(S.hue + H0 + w / n, 1, 1, c); + hsv(S.hue + H0 + w / n, SAT, 1, c); const v = 255, s = 1 - 0.85 * glow; display.setPixel(x, y, round(v * (c[0] * s + 1 - s)), round(v * (c[1] * s + 1 - s)), round(v * (c[2] * s + 1 - s))); } From 49303964796d1c092a91189d0135a168f8e1bc35 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 16:55:09 +0000 Subject: [PATCH 8/9] =?UTF-8?q?=EC=97=B0=EA=B5=AC=20=EB=85=B8=ED=8A=B8?= =?UTF-8?q?=EC=97=90=2034=ED=8C=90=20=EC=97=B4=20=EA=B0=9C=EC=9D=98=20?= =?UTF-8?q?=ED=91=9C=EC=99=80=20=EC=A0=88,=20=EB=8B=A4=EC=9D=8C=20?= =?UTF-8?q?=ED=8C=90=20=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_011BuccZdVDjYHZnwdNh4X1F --- incoming/pattern-study/README.md | 81 ++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 5 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index a72dca02a..5ee6e3843 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 238개를 쉰다섯 갈래로 나눠 만들었어요. 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판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 248개를 쉰다섯 갈래로 나눠 만들었어요. 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판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. [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판에서 세로로 다시 짰어요. @@ -399,6 +399,21 @@ | [Penrose Room](patterns/penrose-room.js) | 물리·스프링 | 거울로 된 펜로즈의 방에서 빛 한 점이 떠돌고, 수백 개의 광선이 반사하며 복도와 위아래 반타원을 밝히는 동안 양옆 방은 캄캄해요. 빛이 반타원을 지나 옆방으로 건너가면 밝은 곳과 어두운 곳이 뒤바뀌어요. K3 은 복도 벽을 초점에서 4 LED 까지 옮겨 어둠에 빛이 새게 하고, K1 을 누르면 빛이 바로 건너가요. | | [Sorting Network](patterns/sorting-network.js) | 선·궤적 | 실 열여섯 가닥이 이웃끼리 한 쌍씩 자리를 바꿔 128줄 만에 순서가 거꾸로 되고, 다음 네트워크가 이어받아 실마다 패널을 사인 곡선으로 가로질러요. 균일한 무작위 정렬 네트워크라 바꾼 자리를 찍은 점이 가운데로 반원 꼴로 몰려요. K3 은 실을 32가닥까지 늘리고, K1 을 누르면 실 하나만 8초 동안 밝혀 그 곡선을 보여 줘요. | +### 34판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Cat Eye](patterns/cat-eye.js) | 화장품 팝업 | 보랏빛 젤리 폴리시 밑으로 보이지 않는 막대 자석이 미끄러지면, 자성 반짝이가 자기장을 따라 돌아 자기장이 표면과 나란한 자리에 날카로운 빛줄기 하나가 서고 그 양옆은 곧추선 반짝이로 짙어져요. 자석이 지나간 자리의 반짝이는 1.8초에 걸쳐 흩어진 은하 같은 반짝임으로 돌아가요. K3 은 막대를 둘로 늘려 줄 두 개나 별 모양을 세우고, K1 을 누르면 자석이 멀어졌다 돌아와요. | +| [Hair Gloss](patterns/hair-gloss.js) | 화장품 팝업 | 판 전체에 윤기 나는 긴 머리카락 230가닥이 위에서 끝까지 늘어져 산들바람에 흔들려요. 가닥이 빛과 눈의 반벡터에 직각인 줄에 흰 광택 띠가 서고 그 아래로 머리색의 둘째 띠가 따라와 머리가 흔들릴 때마다 오르내리며, 9초마다 보이지 않는 빗이 내려가 빗살 자리를 가르고 지나간 자리를 더 매끄럽고 반짝이게 해요. | +| [Kelvin Wake](patterns/kelvin-wake.js) | 물결·간섭 | 고요한 물 위로 흰 거품 한 점이 아래에서 위로 올라가고, 그 뒤로 켈빈의 배 물결이 펼쳐져요. 물길을 가로지르는 굽은 가로 물마루와 비스듬한 짧은 발산 물결이 반각 19.47° 쐐기의 두 팔에서 만나 가장 밝은 첨점 줄을 이루고, 쐐기는 원천이 얼마나 빠르든 원천 뒤 90 LED 에서 패널 양옆에 닿아요. K3 은 원천을 빠르게 해 물결을 길게 늘이지만 쐐기의 각은 그대로예요. | +| [Lakes of Wada](patterns/lakes-of-wada.js) | 위상·매듭 | 섬 안의 두 호수와 바다가 차례로 1 LED 운하를 파요. 마른 땅이 파는 물에서 32, 16, 8, … LED 안에 들 때까지 차례마다 거리를 반으로 줄여 파고 서로 다른 물은 끝내 닿지 않아서, 다 판 섬은 하늘색 바다와 분홍, 연두 호수 사이에 가는 땅 벽만 남은 미로가 돼요. K3 은 호수를 다섯까지 늘리고, K1 을 누르면 섬이 잠기고 새 섬이 솟아요. | +| [Mood Tint](patterns/mood-tint.js) | 화장품 팝업 | 판 전체가 열에 색이 바뀌는 짙은 보라 코팅이에요. 보이지 않는 손끝이 누른 자리마다 지문 결이 든 옅은 난초색 타원이 피고, 열이 퍼지며 결부터 흐려지고 이웃 자국과 엉겨 붙었다가 다시 보라 속으로 줄어들어요. K1 을 누르면 손바닥 하나가 1.1초 동안 눌러요. | +| [Moving Sofa](patterns/moving-sofa.js) | 기하·대칭 | 폭 14 LED 의 ㄇ자 복도를 해머슬리 소파가 오른쪽 다리로 올라와 모퉁이를 돌고, 위를 건너 왼쪽 다리로 내려가요. 모퉁이마다 안쪽 모서리가 소파의 금빛 반원 홈 안을 지나가고, 소파 뒤에 옅게 붙어 온 띠가 벽에 깎여 나가 소파 모양만 남아요. K3 은 홈의 반지름을 0 의 반원판에서 넓이 2 인 소파까지 바꿔요. | +| [Pentomino Cover](patterns/pentomino-cover.js) | 알고리즘 | 6×10 직사각형을 크누스의 춤추는 링크가 열두 펜토미노로 채워요. 고를 수가 가장 적은 칸부터 조각을 놓고 막히면 빼서 아래쪽 조각들이 쉴 새 없이 들고 나며 윤곽만 잠깐 남기고, 다 덮이면 2초 동안 빛이 비스듬히 쓸고 판 아래에 눈금이 하나 늘어요. K3 은 판을 5×12, 4×15, 해가 둘뿐인 3×20 으로 바꿔요. | +| [Scheidegger Rivers](patterns/scheidegger-rivers.js) | 선·궤적 | 체크무늬로 놓인 LED 마다 물을 왼쪽 아래나 오른쪽 아래로 무작위로 보내는 샤이데거의 강 그물이에요. 모인 물의 로그로 밝혀서 위의 실개천이 아래로 갈수록 굵은 줄기로 합쳐지고, 바닥의 출구마다 다른 색으로 물든 강을 따라 빗방울이 흘러내리며, 가끔 한 자리가 방향을 바꾸면 큰 강이 옆 유역으로 넘어가 옛 물길이 마르고 새 물길이 줄마다 차올라요. | +| [Thomson Problem](patterns/thomson-problem.js) | 물리·스프링 | 천천히 도는 구 위에서 같은 전하 100개가 서로 밀어 거의 삼각형인 격자로 앉아요. 이웃이 여섯인 전하는 가운데가 옅은 하늘색으로, 오일러 공식이 늘 열둘을 남기는 다섯 이웃 전하는 금색으로 빛나고, 일곱 이웃 전하가 생기면 자홍색이에요. K3 을 480 까지 올리면 열둘이 5-7-5 흉터로 자라고, K1 을 누르면 전하가 흩어졌다가 다시 격자로 앉아요. | +| [Velvet Nap](patterns/velvet-nap.js) | 화장품 팝업 | 판 전체가 에메랄드 무광 벨벳이고, 보이지 않는 손끝 둘이 천천히 글씨를 쓰듯 결을 쓸어요. 빛 쪽으로 밀린 결은 짙고 깊은 색으로, 빛을 따라 누운 결은 옅은 은빛으로 남아 서로 겹쳐 덮이고, 7초마다 손바닥이 22 LED 너비 띠를 위에서 아래로 쓸어 결을 한 방향으로 눕혀요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -1549,6 +1564,50 @@ E8 의 근 240개를 고셋의 4₂₁ 다면체 꼭짓점으로 그렸어요. 주제 6 으로 필오프 젤 마스크가 한 장으로 벗겨지는 것을 그렸어요. 막은 떨어지는 줄에서 원통으로 말리는데, 바깥으로 나온 면이 막의 안쪽이라 색이 옅고 위쪽에 흰 빛 줄이 서요. 벗기는 선에는 막이 1초쯤 붙잡혀 2.5~5.5줄 늘어났다가 0.2초 만에 곧은 자리를 지나치며 풀리는 곳을 더해서, 자로 그은 선이 되지 않아요. 움직임과 모양 바뀜은 가장 밝은 채널만 읽어서 막을 제 색의 0.6 밝기로 깊게 두고 살결은 0.8 쯤으로 밝혔고, 움직임 0.232, 모양 바뀜 0.226 이 나왔어요. 그 깊이에서는 K1 0.30 의 주황이 황갈색으로, 0.45 의 연두가 올리브로 보여서 빨강부터 연두까지는 막을 살결보다 밝게 올리고 컬 밑 그림자도 반으로 줄였어요. 처음 판은 젖은 새 막의 두께 잡음이 빛 띠를 벌레처럼 비틀어서, 잡음을 18에서 4로 줄이고 앞머리 뒤로 잦아드는 물결 줄을 따로 그렸어요. +## 34판 + +34판도 주제 5 「낯선 수학」 여섯과 주제 6 「화장품 브랜드 팝업」 넷이에요. 같이 돈 33판(#226)이 정렬 네트워크와 베르트랑의 역설, 클렌징 밤 같은 것을 먼저 골라서, 이 판은 그와 겹치지 않게 골랐어요. 수학은 Thomson Problem, Kelvin Wake, Lakes of Wada, Scheidegger Rivers, Moving Sofa, Pentomino Cover 이고, 화장품은 Cat Eye, Velvet Nap, Mood Tint, Hair Gloss 예요. 수학 여섯은 다 「다음 판 감」의 목록 밖에서 찾았고, 있는 갈래에 들어가서 주제 2 로 연 갈래는 없어요. Lakes of Wada 가 위상·매듭 갈래의 넷째예요. 화장품 넷 가운데 자석 젤 네일은 「다음에 해 볼 질감」에 적혀 있던 감이에요. 주제 1 은 228개의 지도 21장이 다 `none` 이라 30판과 33판처럼 패턴이 하나뿐인 칸을 겨눴어요. Thomson Problem 이 Littlewood Roots 혼자이던 가장자리 × 채움 1,1 에 둘째로 섰어요. Lakes of Wada 는 Penrose 혼자이던 촘촘함 × 색 3,4 를 겨눴는데, 33판을 합치고 보니 Bertrand Paradox 가 먼저 둘째로 서 있어서 셋째가 됐어요. 패턴이 하나뿐인 칸은 33판까지의 238개에서 24 였고 248개에서 23 이에요. 처음 합친 248개에서는 Lakes of Wada 가 색과 움직임 두 축의 끝을 넘어 지도 여섯 칸이 비었어요. 그 패턴을 축 안으로 들이자 칸 경계가 30판 그대로 돌아왔고 지도 21장이 다 `none` 이에요. 주제 4 는 Kelvin Wake 와 Scheidegger Rivers 예요. Kelvin Wake 는 원천 뒤로 쐐기가 128줄을 다 덮어 `thirds` 가 99/99/99 이고, Scheidegger Rivers 는 위의 실개천부터 바닥 줄기까지 그물이 판을 다 써서 90/92/92 예요. Hair Gloss 도 머리가 128줄을 다 내려와서 주제 4 와 6 을 같이 채워요. 다섯 일꾼이 둘씩 짰고, 본체가 연구의 Lab 하네스와 각 slug 의 씨앗으로 다시 그려 보고 합쳤어요. + +### Thomson Problem + +주제 1 과 5 로 톰슨 문제(1904)를 세웠어요. 전하 N 개를 Σ1/r 이 줄도록 구 위에서 밀고 볼록 껍질로 이웃 수를 세면, Σ(6 − z) 는 어느 프레임에서나 12 였어요. 겨눈 칸은 Littlewood Roots 혼자 서 있던 가장자리 × 채움 1,1 이에요. 구 가장자리 쪽 점을 0.55 까지 어둡게 하자 가장자리가 0.193 에서 0.113 으로 내려가서, 그 여유로 점의 봉우리를 185 에서 215 로 올렸어요. 옅은 하늘색(채도 0.3)으로 칠한 첫 판은 점 사이가 어두워 패널 평균 밝기가 낮은 탓에 Hue 반응이 0.02 였고, 채도를 점 가운데만 0.4 로 빼고 둘레는 1 로 두자 0.053 이 됐어요. 경사 하강만으로 데운 첫 판은 100개에서 5-7 쌍 하나가 남은 4448.44 에 섰고, 운동량 0.9 로 다섯 번 풀어 가장 낮은 것을 고르자 알려진 최솟값 4448.35 보다 0.02 위에서 다섯 이웃 열둘만 남았어요. 볼록 껍질의 모서리는 거의 정사각형인 사변형에서 프레임마다 뒤집혀 5-7 쌍을 깜박이게 해서, 2% 의 여유를 두고서야 뒤집게 했어요. 마지막 값은 가장자리 0.165, 채움 0.259 예요. + +### Kelvin Wake + +주제 4 와 5 로 켈빈의 배 물결(1887)을 세웠어요. 파향 θ 에 대한 적분을 가로 파수 ky = k0 secθ tanθ 로 바꾸면 줄마다 y 방향 256점 FFT 하나가 돼서, 기울기 표 300줄을 8~45 ms 에 다시 지어요. 표 위에서 물마루 줄을 s x − c x^(1/3) 로 맞추면 Velocity 범위 전체에서 19.25~19.46° 가 나와요. 가장 밝은 물마루만 줄마다 이으면 17.2~18.3° 로 안쪽에 서서, 에어리 무늬의 안쪽 밀림을 빼야 켈빈의 19.47° 가 보였어요. 물을 다 켜서 `thirds` 는 99/99/99 이고, 2~8초에 밝기가 24/255 넘게 오르내린 LED 는 세 3분의 1 모두 100% 예요. 처음에는 파장을 10 LED 로 열었더니 가로 물결만 보였어요. 12 LED 로 늘이자 발산 물결이 왼쪽 팔에 깃털처럼 섰고, 원천을 빠르게 하면 발산 물결이 가로 물마루보다 밝아져요. 빛이 한쪽에서 와서 왼쪽 첨점 줄이 밝고 오른쪽은 옅어요. 왼쪽 팔의 물결은 빛을 마주 보고 오른쪽 팔의 물결은 빛에 거의 옆을 보여서예요. 노브 반응은 셋이 0.05 언저리였는데, 기울기 눈금을 99.5 에서 99 백분위로 내리자 0.06 이 됐어요. 주황과 노랑 물은 고요한 밝기 0.34 에서 갈색과 올리브였고, 0.74 로 올리고 그늘진 면을 진홍 쪽으로 기울이자 제 색이 됐어요. + +### Lakes of Wada + +주제 1 과 5 로 요네야마가 1917년에 와다의 생각이라며 낸 와다의 호수를 위상·매듭 갈래의 넷째로 세웠어요. 처음 판은 서로 다른 물 사이에 땅 한 LED 만 남기게 했더니, 거리 32 차례에 호수 1 의 운하가 호수 2 를 둘러싸서 호수 2 는 제 구석만 팠어요. 운하 하나가 두 물 사이를 지나가려면 땅이 3 LED 있어야 해서, 차례마다 거리의 4분의 1 만큼 다른 물에서 떨어져 파게 하자 세 물이 섞였어요. 이 간격은 다음 차례 간격의 두 배여야 해서 6 에서 멈춘 판은 호수 2 가 다시 갇혔고, 거리의 2분의 1 로 둔 판은 첫 차례가 간격 16 에 걸려 한 칸도 못 팠어요. 촘촘함은 물 210, 땅 26 인 처음 판이 0.131 이었고, 물을 255, 땅을 14 로 두고 섬을 키우자 0.212, Speed 대로 6초를 미리 파고 열자 0.228 이 됐어요. 그 판은 색 0.723 과 움직임 0.017 이 두 축의 끝을 넘어서, 1초에 파는 LED 를 Speed 1 에 80 에서 110 으로 늘리고 미리 파는 시간을 3초로, 물의 채도를 0.88 로 낮췄어요. 마지막 값은 촘촘함 0.227, 색 0.633, 움직임 0.023 이에요. 다 판 섬 다섯에서 땅은 다 물에 닿지만 양쪽에 물이 있는 1 LED 벽은 42~48% 이고, 나머지는 서로 다른 물 사이에 남은 2 LED 벽이라 어느 물이 파도 제 운하 옆을 나란히 파게 되어 더 가늘어지지 않아요. + +### Scheidegger Rivers + +주제 4 와 5 로 샤이데거의 강 그물 모형(1967)을 세웠어요. 체크무늬 4,096 자리가 저마다 물을 왼쪽 아래나 오른쪽 아래로 보내고 모인 물의 로그로 밝혀서, 가장 작은 실개천도 30/255 에 서고 `thirds` 가 90/92/92 예요. 강은 합쳐지기만 하는 무작위 걸음이라 넓이 A 인 유역의 강 길이가 A^(2/3) 쯤인데, 이 판 크기의 그물 스무 개에서 잰 지수는 0.61 이었어요. 처음에는 아무 자리나 방향을 바꾸게 했더니 3.5초 동안 그물이 거의 그대로여서 움직임 0.062, 모양 바뀜 0.021, Speed 노브 반응 0.03 이었어요. 물이 많이 지나는 자리 가운데 다른 길이 10줄 넘게 떨어져 가는 곳을 골라 바꾸게 하자 움직임과 모양 바뀜이 0.085 와 0.043 이 됐고, 빗방울을 1초에 10개에서 14개로 늘려 Speed 반응이 0.06 이 됐어요. 유역 색을 출구 자리의 사인으로 칠했더니 x 와 32−x 에 선 두 출구가 같은 색이어서, 바뀐 유역이 옛 유역 가운데 가장 많이 겹치는 것의 색을 물려받게 했어요. Bias 를 정확히 ±1 로 두면 대각선마다 물이 같아 판이 고른 빛 한 장이 되어서, 끝에서도 50자리에 하나는 반대로 꺾이게 했어요. + +### Moving Sofa + +주제 5 로 모저의 소파 문제(1966)와 해머슬리의 소파(1968)를 세웠어요. 소파의 틀에서 보면 복도가 90° 도는 동안 안쪽 모서리가 홈의 반원 위 c = r(cos 2θ, sin 2θ) 를 지나고, 탈레스 정리로 안쪽 벽이 홈의 두 끝을 늘 지나며 바깥 벽은 두 4분원에 닿은 채 돌아요. 소파를 0.005 폭 간격으로 찍어 길 전체를 0.01초씩 따라가 보니 벽 밖으로 나간 거리가 r 다섯 값에서 다 0.0000 LED 였고, 2% 큰 소파는 0.457 LED, 모서리를 2θ 대신 θ 로 돌린 길은 8.9 LED 가 나갔어요. 처음 판은 ㄱ자 모퉁이 하나 옆에 소파의 틀을 그린 작은 창을 두고 K4 로 지난 각도의 복도 윤곽을 그렸는데, 1 LED 선이라 노브 반응이 0.0119 로 검사에 걸렸고 윤곽마다 옅은 바닥을 겹쳐도 0.04 였어요. 창을 빼고 복도를 ㄇ자로 바꿔 모퉁이를 둘로 늘린 뒤, 도는 동안 지나온 복도 자리의 교집합을 닫힌 꼴로 셈해 소파 뒤의 옅은 띠로 그렸고, 벽이 그 띠를 깎아 90° 에서 소파만 남아요. 홈 끝처럼 뾰족한 모서리는 거리 함수를 max 로 합치면 거리를 짧게 잡아 벽 LED 에 소파 빛이 0.15 만큼 묻었고, 평평한 쪽 아래에서 참 거리를 쓰자 25초 동안 벽 LED 가 한 번도 바뀌지 않았어요. 움직임은 0.308, 가장자리는 0.274 예요. + +### Pentomino Cover + +주제 5 로 크누스의 춤추는 링크와 알고리즘 X(2000)를 골롬의 펜토미노(1953)에 걸었어요. 열이 조각 12개와 칸 60개인 정확 덮개 행렬을 타입 배열의 이중 연결 리스트로 짓고, 재귀 대신 한 걸음씩 나아가는 반복으로 풀어서 프레임마다 Speed 가 정한 걸음만 가요. 대칭으로 같은 해를 거듭 세지 않게 X 를 판의 첫 4분의 1에만 두고, X 가 판의 거울선 위에 서면 P 를 뒤집지 못하게 보조 열 하나로 묶었어요. 끝까지 돈 탐색이 6×10 에서 2339, 5×12 에서 1010, 4×15 에서 368, 3×20 에서 2 를 찾아 알려진 수와 맞았고, 행 순서를 다 섞어도 걸음 수는 6×10 의 3,583,904 와 3×20 의 33,545 그대로였어요. 처음 판은 빠진 조각을 칸마다 흐린 색으로 0.25초 남겼더니 노랑과 연두 조각이 어두워지며 올리브와 갈색 얼룩이 됐고, 0.18초 동안 윤곽만 남기자 되돌아가는 자리가 철사 그림처럼 읽혔어요. 첫 덮개를 2.8초에 맞추려고 처음 update 에서 첫 덮개까지 한 번 돌고 1100 걸음 앞에서 다시 시작했는데, 다시 짓는 판이 행을 새로 섞어서 3×20 의 첫 덮개가 6초에 왔고 섞은 순서를 지키자 맞았어요. 움직임 1.828 은 움직임 축의 위 칸이고, 조각 사이 1 LED 이음매가 가장자리를 0.941 로 올렸으며, 노브 넷은 0.21~0.31 이에요. + +### Cat Eye + +주제 6 으로 「다음에 해 볼 질감」에 적혀 있던 자석 젤 네일의 캣아이를 그렸어요. LED 마다 자기장이 표면과 이루는 기울기 atan(Bz/|Bh|) 를 셈하고, 반짝이는 그 기울기를 0.15초에 따라 돌다가 자기장이 떠나면 1.8초에 걸쳐 흩어져요. 쌍극자 하나로 짠 첫 판은 빛줄기가 30 LED 남짓한 토막이었고, 너비 방향으로 자화된 76 LED 막대를 쌍극자 아홉 개로 놓자 줄이 세로 90 LED 쯤을 지나갔어요. K1 의 0.3~0.42 에서는 밝기 40 의 젤리가 갈색과 올리브로 보였어요. 젤리를 12 까지 낮춰도 띠 둘레의 은은한 빛이 판 전체에 회갈색 안개로 남아서, 그 빛을 정렬 정도 a 대신 a² 에 비례시켜 띠 곁으로 모으자 검은 베이스 위의 금색 캣아이가 됐어요. 띠 너비는 자석 깊이를 따라가서 깊이 11.8 LED 에서 11 LED, 40 LED 에서 30 LED 쯤이에요. 노브 반응은 0.19, 0.14, 0.19, 0.18 이에요. + +### Velvet Nap + +주제 6 으로 벨벳 위에 손끝으로 쓴 글씨를 그렸어요. LED 마다 결의 방향과 누운 정도를 벡터로 두고, 털 한 올을 카지야-케이 식으로 비춰서 밝기가 빛과 결 사이의 각에서 나오게 했어요. 31판에서 본 대로 색만 바뀌면 움직임에 잡히지 않아서, 빛 쪽으로 밀린 자국은 밝기 87 언저리, 몸통은 180, 은빛 자국은 255 까지 가게 했어요. 처음에는 손바닥이 판 전체를 쓸었더니 5초 만에 글씨가 다 지워졌어요. 세로 띠 하나만 쓸고 손끝이 가장 덜 쓴 자리에 내려앉게 하자 판 전체에 글씨가 남았어요. 빛을 멈추면 움직임이 0.016, 모양 바뀜이 0.041 이에요. 빛이 12초에 한 번 ±29° 흔들리게 하자 0.050 과 0.160 이 되고 K2 반응도 0.054 에서 0.09 로 올랐어요. 흔들림을 ±23°, 10초로 두면 모양 바뀜이 0.106 이라 칸 경계 0.103 에 붙어서 ±29°, 12초를 골랐어요. 주황에서 연두는 몸통 밝기 0.6 언저리에서 녹슨 색과 겨자색, 올리브로 보였고, 0.8 쪽으로 올리고 어두운 면을 루비 쪽으로 기울이자 주황과 노랑으로 읽혔어요. + +### Mood Tint + +주제 6 으로 손끝의 열에 색이 바뀌는 코팅을 그렸어요. 온도는 2차원 열 방정식과 뉴턴 냉각으로 풀고, 색은 문턱 0.4 언저리 0.014 폭에서만 바뀌어 자국 가장자리가 1 LED 로 날카로워요. 열 방정식은 가는 무늬부터 지워서 2.8 LED 간격의 지문 결은 0.18초마다 e 분의 1 로 줄고, 결이 손을 뗀 뒤 0.4초 안에 흐려지는 것은 따로 만든 것이 아니라 풀이가 그래요. 문턱이 날카로워서 누르는 순간 결이 먼저 문턱을 넘어 자국이 깨진 소용돌이로 나타났다가 채워지고, Warmth -1 에서는 손끝이 0.63 이라 그 조각만 남아요. 짙은 보라를 밝기 0.44 로 두니 색상 0.03~0.33 의 주황에서 연두까지가 갈색과 올리브로 보여서, 그 사이에서는 0.78 로 올렸어요. 누름을 0.42초에서 0.28초마다로 늘리고 식는 빠르기를 1초에 0.42 에서 0.32 로 줄인 판에서 움직임이 0.042 에서 0.055 가 됐어요. Blend 반응은 판 대부분이 방 온도라 0.02 였고, 가장 넓은 폭을 0.32 에서 0.61 로 넓히자 0.09 가 됐어요. + +### Hair Gloss + +주제 4 와 6 으로 헤어 광고의 광택 띠를 머리결 질감으로만 그렸어요. 가닥마다 접선과 반벡터의 각으로 카지야-케이 광을 매기고, 흰 광택은 뿌리 쪽으로, 머리색으로 물든 둘째 광택은 끝 쪽으로 밀었어요. 첫 판은 360가닥이 9~23줄 주기로 1.3 LED 씩 옆으로 떨려서 머리결이 아니라 점박이 잡음이었고 한 프레임이 12.4 ms 였는데, 230가닥으로 줄이고 떨림을 0.4 LED, 30~70줄 주기로 늘이자 세로결이 읽히고 5.6 ms 가 됐어요. Shine 노브가 띠의 날카로움만 바꿀 때는 반응이 0.06 이었고, 둔한 결일수록 띠 둘레에 넓은 윤을 펴게 하자 0.17 이 됐어요. 주황과 노랑 쪽은 이번에도 갈색과 올리브로 가서 K1 0.36 의 꿀색 금발이 카키로 보였고, 색상 0.12 둘레에서 밝기를 1.6배까지 올리고 채도를 0.18 내렸어요. 머리가 판을 다 채워 `thirds` 가 99/99/94 라서 2~8초 동안 밝기가 24/255 넘게 오르내린 LED 로 다시 세었더니 97.8/100/100% 였고, 빗을 끄면 위 3분의 1이 80.0% 로 내려가요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -1591,7 +1650,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 24일 `dev`(`07b2766`)의 228개에 33판의 열 개를 더한 238개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 24일 `dev`(`e4204d2`)의 238개에 34판의 열 개를 더한 248개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -1605,6 +1664,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 33판의 열 개도 어느 축의 끝을 넘지 않아서 칸 경계는 30판 그대로이고, 238개에서도 지도 21장이 다 `none` 이에요. 그래서 주제 1 은 30판처럼 패턴이 하나뿐인 칸을 겨눴고, 228개에서 27 이던 그런 칸이 24 가 됐어요. 채움 × 색 1,4 는 LED 의 70% 넘게 꺼진 채 색이 0.544 를 넘어야 하는 칸이에요. 색은 꺼진 LED 까지 세는 셈이라 반쯤 밝은 LED 가 값을 깎았어요. Bertrand Paradox 는 켜진 LED 를 다 255 로 두고 바래는 현을 LED 하나씩 끄자 0.537 에서 0.576 이 됐고, R − G 가 반대인 자홍과 초록이 그 값을 냈어요. 가장자리 × 모양 바뀜 1,4 는 번진 그림이 2~6초 사이에 블록 평균으로 0.331 넘게 바뀌어야 해요. Penrose Room 은 밝은 곳과 어두운 곳이 뒤바뀌는 순간을 그 창 안에 두고, 켜진 LED 의 평균을 92 에서 128 로 올려 모양 바뀜을 0.270 에서 0.365 로 밀었어요. 가장자리는 0.044 에서 0.075 로만 올랐어요. +34판에서는 Lakes of Wada 의 첫 판이 두 축의 끝을 넘었어요. 색 0.723 이 Outer Billiards 의 0.675 를, 움직임 0.017 이 Star Trails 의 0.019 를 넘어서, 248개의 `--gaps` 가 가장자리 × 색 1,4 와 움직임 × 모양 바뀜 1,4, 복잡도 × 색 4,4, 채움 × 색 1,4, 색 × 모양 바뀜 4,3 과 4,4 를 찍었어요. 여섯 다 `*` 가 없는 칸이라, 축을 넘은 패턴을 안으로 들이는 것이 길이었어요. 1 LED 운하는 땅 14 에서 물 255 로만 바뀌고, 막 판 자리가 흰빛에서 물빛으로 식어도 가장 밝은 채널은 255 그대로예요. 그래서 움직임은 1초에 파는 LED 수에 묶여 Speed 2 에서 0.0185 가 끝이었어요. 파는 빠르기를 1.4배로 올리고 채도를 0.88 로 누르자 0.023 과 0.633 이 되어 21장이 다시 다 `none` 이에요. 모양 바뀜도 블록 평균이 한쪽으로만 바뀌어 움직임의 여섯 배를 넘지 못해요. Balloon Box 혼자인 색 × 모양 바뀜 4,3 에 서려면 0.217 이 필요했는데, 그러려면 섬 하나를 6초 만에 다 파야 해서 겨누지 않았어요. Thomson Problem 은 가장자리 × 채움 1,1 에 Littlewood Roots 의 둘째로 섰어요. 판이 거의 어두운 점 그림은 노브 반응이 화면 평균 밝기 언저리에서 멈춰서, 채움 0.302 와 가장자리 0.210 아래를 지키며 노브 넷을 0.05 위로 올리려면 점의 봉우리와 번짐 너비를 맞바꿔야 했어요. 패턴이 하나뿐인 칸은 238개에서 24 이던 것이 248개에서 23 이에요. + 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 를 냈어요. @@ -1619,7 +1680,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ### 아직 없는 갈래 -`families.json` 의 쉰다섯 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 29판이 연 위상·매듭 갈래는 Hopf Fibration 과 31판의 Boy's Surface, 33판의 Borromean Rings 예요. 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 가 넷을 더해 스물이에요. 28판이 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table, 33판의 Goldbach Comet 이에요. 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 이 셋째예요. +`families.json` 의 쉰다섯 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 29판이 연 위상·매듭 갈래는 Hopf Fibration 과 31판의 Boy's Surface, 33판의 Borromean Rings, 34판의 Lakes of Wada 예요. 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 가 넷을 더해 스물넷이에요. 28판이 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table, 33판의 Goldbach Comet 이에요. 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 이 셋째예요. ### 멋진 것 더 멋지게 @@ -1631,6 +1692,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 33판의 열 개는 가장 낮은 노브가 Essence Drop 의 K2 Speed 와 K4 Size 로 둘 다 0.03 이에요. 웅덩이와 바탕이 다 켜진 그림에서 방울 하나가 바꾸는 LED 가 적어서, 웅덩이 속을 천천히 지나는 빛줄기를 더하고서야 Speed 가 0.02 에서 0.03 이 됐어요. Gold Leaf 는 조각이 작을 때 K3 Flakes 와 K4 Size 가 0.02 였고, 가장 가까운 조각을 지름 12 LED 로 키우자 둘 다 0.09 가 됐어요. Cleansing Balm 의 K4 Grain 도 결의 진폭을 0.09 에서 0.21 까지 넓혀 0.02 에서 0.04 가 됐어요. +34판의 열 개는 가장 낮은 노브가 Thomson Problem 의 K3 Charges 와 K4 Heat 로 둘 다 0.05 예요. 판이 거의 어두운 점 그림이라 비교한 두 화면에 같은 점이 하나도 없어도 반응이 화면 평균 밝기 언저리에서 멈췄어요. Moving Sofa 의 K1 Hue 는 표가 0.05 로 찍었지만 네 자리로 재면 0.0463 이었고, 마지막 판은 0.0600 이에요. 표는 노브 반응을 두 자리로 반올림해서 약한 노브를 가려요. Mood Tint 의 K4 Blend 는 판 대부분이 방 온도라 0.02 였고, 문턱의 가장 넓은 폭을 0.32 에서 0.61 로 넓히자 0.09 가 됐어요. 빠르기 노브는 미리 돌리는 처음 시간도 Speed 대로 두어야 반응이 났어요. Lakes of Wada 는 처음 섬을 Speed 와 상관없이 파 둔 판이 0.04 였고, 그 시간을 Speed 대로 파게 하자 0.12 가 됐고, 마지막 판은 0.11 이에요. + Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. ### 세로 64×128 @@ -1647,9 +1710,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 33판에서는 Sorting Network 가 한 네트워크의 바꿈 120번을 128줄에 담아 51/51/50 이고, Bertrand Paradox 는 원 셋을 세 칸에 하나씩 엇갈려 놓아 24/28/25 예요. Goldbach Comet 은 줄이 log₂ n 이라 네 옥타브가 패널에 들어 53/55/48 이고, Penrose Room 은 반타원 둘과 복도가 패널 전체라 68/61/72 예요. 3차원 대상은 이번에도 세로를 따로 골라야 했어요. Borromean Rings 의 황금 직사각형 셋은 꼭짓점이 정이십면체라 어느 쪽에서 봐도 둥글어서, 그대로는 128줄 가운데 50줄쯤만 켰어요. 세로로 2.3배 늘여도 고리가 걸리는 부등식은 그대로라 늘이자 23/40/12 가 됐어요. Dandelin Spheres 는 원뿔의 꼭짓점이 아래에 있어 95/71/15 예요. +34판에서는 Kelvin Wake 가 쐐기의 반각 19.47° 로 세로를 썼어요. 쐐기가 원천 뒤 90 LED 에서 패널 양옆에 닿아서, 원천이 판을 아래에서 위로 건너는 동안 물결이 128줄을 다 덮어요. 물을 다 켜서 `thirds` 는 99/99/99 이고, 2~8초에 밝기가 24/255 넘게 오르내린 LED 로 세어도 3분의 1마다 100% 예요. 2초 화면과 8초 화면만 견주면 0/16/30 이었어요. 물결 한 주기 6.3초가 창 안에 들어서 두 화면의 물결이 거의 같은 자리에 와요. 바뀐 LED 는 오르내린 폭으로 세야 해요. Scheidegger Rivers 는 가장 작은 실개천도 30/255 에 세워서 90/92/92 이고, 위 3분의 1은 거의 다 실개천이에요. Hair Gloss 는 머리가 판을 다 채워 99/99/94 이고, 오르내린 LED 로 세면 97.8/100/100% 예요. 빗을 끄면 위 3분의 1이 80.0% 로 내려가요. Moving Sofa 는 ㄇ자 복도의 다리 둘을 세로로 세웠어요. 다리 사이 벽의 빗금 40/255 가 셈에 들어서 45/29/28 이에요. + ### 낯선 수학 -주제 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)이에요. +주제 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)예요. 목록 밖에서 다음에 해 볼 것이에요. 콜라코스키 수열(1965)은 제 길이 부호를 읽으면 제가 되는 1 과 2 의 줄이라 리듬·펄스에 맞아요. 영 도형의 플랑셰렐 성장은 상자가 하나씩 붙으며 로건-셰프와 베르시크-케로프의 극한 곡선(1977)으로 다가가요. 아즈텍 다이아몬드의 북극권과 같은 극한 모양이라 같은 판에 두지 않아요. 슈타이너 사슬은 반전 하나로 고리의 원들이 미끄러지는 포리즘이고, 민코프스키의 물음표 함수(1904)는 페리 나무를 이진 나무로 펴는 계단이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 렌니의 주차 문제(1958)는 줄마다 길이 1 인 차가 아무 데나 서다가 빈자리가 다 1 보다 짧아지면 멈추는데, 128줄이 다 멈추면 채운 몫이 렌니 상수 0.7476 언저리에 모여요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -1665,10 +1730,14 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 33판에서 본 것이에요. 분포가 정리의 주인공인 대상은 고르게 뽑는 셈이 그림을 정했어요. 정렬 네트워크는 이웃 쌍을 아무렇게나 바꾸면 사인 곡선이 나오지 않아서, 훅 걷기로 영 타블로를 고르게 뽑아 에델만-그린 대응으로 옮겼어요. 베르트랑의 역설은 그 뽑는 법 셋이 그대로 원 셋이에요. 예측이 매끈한 값을 줄 때는 나누는 값을 정확히 맞춰야 줄기가 섰어요. Goldbach Comet 은 n/ln²n 으로 나누면 줄기가 4 LED 기울었고, 적분 꼴로 나누자 곧게 섰어요. 정리가 어둠을 말하는 대상은 그 어둠이 정말 0 인지 셈으로 먼저 봤어요. Penrose Room 은 광선 2만 개를 40번씩 튕겨 초점 밖으로 나간 광선이 하나도 없는 것을 본 뒤에 노출을 걸었어요. 3차원 대상은 수학이 허락하는 만큼 늘여 패널에 맞췄고, Borromean Rings 는 세로로 2.3배 늘여도 같은 고리예요. +34판에서 본 것이에요. 정리가 수를 못 박는 대상은 그 수를 셈으로 지켜봤어요. Thomson Problem 은 볼록 껍질로 센 Σ(6 − z) 가 노브 넷을 다 돌리고 단추를 두 번 누른 600 프레임 동안 늘 12 였고, Pentomino Cover 는 끝까지 돈 탐색이 네 판에서 2339, 1010, 368, 2 를 찾았어요. 모양이 주인공인 대상은 그 모양이 맞는지 재는 셈을 그림보다 먼저 짰어요. Moving Sofa 는 소파가 벽 밖으로 0.0000 LED 나가는 것과 2% 큰 소파가 0.457 LED 나가는 것을 본 뒤에 그렸어요. Kelvin Wake 는 가장 밝은 물마루를 이은 각이 17.2~18.3° 로 안쪽에 서서, 에어리 무늬의 안쪽 밀림을 빼고서야 19.47° 가 나왔어요. 극한을 말하는 대상은 이번에도 LED 한 칸에서 멈췄어요. Lakes of Wada 는 땅이 다 물에 닿아도 양쪽에 물이 있는 1 LED 벽이 42~48% 였어요. Scheidegger Rivers 의 A^(2/3) 은 64×128 그물 스무 개에서 지수 0.61 로 나왔어요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 모자 타일(스미스, 마이어스, 캐플런, 굿먼-스트라우스, 2023)은 한 가지 타일로만 평면을 비주기로 덮고, 뒤집어 놓인 타일이 판에 드문드문 흩어져요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 뷔퐁의 바늘(1777)은 Bertrand Paradox 와 같은 꼴로, 금을 넘는 바늘의 몫이 2/π 로 모이는 막대와 함께 세울 수 있어요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. +목록 밖에서 34판 뒤에 해 볼 것이에요. 타메스 문제(피터 타메스, 1930)는 구 위에 같은 원 N 개를 겹치지 않게 가장 크게 놓는 문제라, 톰슨 문제와 달리 가장 가까운 거리만 보고 어디에도 닿지 않고 덜그럭거리는 원이 생겨요. 앙투안의 목걸이(루이 앙투안, 1921)는 고리 사슬의 고리마다 더 작은 사슬을 끼우기를 되풀이한 칸토어 집합이에요. 점이 다 흩어져 있어도 바깥에 건 실이 빠지지 않아서, 세운 패널에 긴 사슬로 두고 단계마다 고리 속 사슬을 드러낼 수 있어요. 로드리게스-이투르베와 리날도의 최적 하천망(1992)은 같은 격자에서 물길마다 Q^0.5 를 더한 에너지가 가장 작은 그물을 담금질로 찾아서, 샤이데거의 무작위 그물보다 덜 구불거리는 나무가 돼요. 로믹의 양손잡이 소파(댄 로믹, 2018)는 왼쪽 모퉁이와 오른쪽 모퉁이를 다 돌 수 있어서, 지그재그 복도를 128줄 쌓고 소파 하나가 끝까지 오르게 할 수 있어요. 데이나 스콧(1958)이 처음 컴퓨터로 푼 8×8 판의 가운데 2×2 구멍 문제는 펜토미노 덮기가 65가지이고, 칸 8 LED 면 판이 64×64 로 패널 너비에 꼭 맞아요. 표면장력이 있는 물결은 원천이 최소 위상속도 23 cm/s 아래로 느려지면 항적이 사라져요(라파엘과 드 젠, 1996). Kelvin Wake 의 FFT 표에 분산식 하나만 바꾸면 그 문턱을 노브에 걸 수 있어요. + ### 화장품 팝업 주제 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 가 생겨요. @@ -1681,7 +1750,9 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 33판이 넷을 더 썼어요. 셔벗 밤이 녹아 오일이 되는 클렌징 밤(Cleansing Balm), 떠서 뒤집히는 금박 조각(Gold Leaf), 에센스에 떨어지는 방울과 왕관(Essence Drop), 한 장으로 말려 벗겨지는 젤 마스크(Peel-Off)예요. 앞의 둘은 「다음에 해 볼 질감」에 적혀 있던 감이에요. 주황과 노랑 쪽의 갈색과 올리브는 넷 다에서 다시 왔어요. 짙게 둔 바탕이나 그림자가 문제였고, 넷 다 그 색상 범위에서만 바탕을 밝히고 채도를 내렸어요. Essence Drop 은 수면 위 바탕이 124, 100, 80 이던 것을 200 위로 올렸고, Gold Leaf 는 세럼을 거의 검게 두고 금박의 짙은 면을 0.58 로 올렸어요. 무광과 유광의 차이는 밝기로 옮겨야 잡혔어요. Cleansing Balm 은 오일을 밤보다 어둡게, Peel-Off 는 막을 살결보다 어둡게 두어서 움직임과 모양 바뀜이 그 차이를 읽어요. -다음에 해 볼 질감이에요. 세팅 미스트가 유리판에 맺혀 굵어지고 흘러내리는 것은 Lip Gloss 와 Setting Mist 사이라 같은 판에 두지 않아요. 매니큐어 붓이 남긴 줄이 스스로 펴지는 것은 Cream Swirl 과 가까워서 같은 판에 두지 않아요. 자석 젤 네일은 보이지 않는 자석을 따라 반짝이 띠가 모였다 흩어지는 그림이에요. 젖은 립글로스나 탑코트가 굳는 것은 Clay Mask 처럼 광이 먼저, 색이 나중에 빠지는 두 문턱으로 그릴 만해요. 립밤이 체온에 녹아 번지거나 고체 향수가 문질러 펴지는 것은 Cleansing Balm 의 녹는 경계를 가로로 돌린 그림이에요. 흰 크림에 금박이 박혀 크림과 함께 천천히 접히는 것은 떠 있는 Gold Leaf 와 다른 그림이에요. 크림 한 방울이 젤 위에 떨어져 천천히 잠기는 것은 Essence Drop 의 거리장을 그대로 써요. 시트 마스크가 굴곡을 따라 붙으며 공기 방울이 밀려 빠지는 것은 Peel-Off 를 거꾸로 돌린 그림이에요. +34판이 넷을 더 썼어요. 자석을 따라 모이는 자석 젤 네일의 빛줄기(Cat Eye), 손끝이 결을 쓸어 남긴 벨벳의 글씨(Velvet Nap), 손끝의 열에 색이 바뀌는 감온 틴트(Mood Tint), 흔들리는 머리결의 광택 띠(Hair Gloss)예요. 자석 젤 네일은 「다음에 해 볼 질감」에 적혀 있던 감이에요. 빛을 털 한 올이나 반짝이 한 조각의 기울기로 셈하자 그 질감의 빛으로 읽혔어요. Velvet Nap 과 Hair Gloss 는 둘 다 카지야-케이 식으로 결과 빛 사이의 각에서 밝기를 냈고, Cat Eye 는 자기장이 표면과 이루는 기울기를 따라 도는 반짝이로 빛줄기를 세웠어요. 주황과 노랑 쪽의 갈색과 올리브는 넷 다에서 다시 왔어요. 셋은 그 색상 범위에서만 바탕을 밝혔고, Cat Eye 는 거꾸로 젤리를 12 까지 낮추고 띠 곁의 빛을 정렬 정도의 제곱으로 모아 검은 베이스 위의 금색 줄로 읽히게 했어요. Mood Tint 는 색이 바뀌는 문턱 폭을 0.014 로 좁혀서, 열이 번지는 그림인데도 자국 가장자리가 1 LED 로 날카로워요. + +다음에 해 볼 질감이에요. 세팅 미스트가 유리판에 맺혀 굵어지고 흘러내리는 것은 Lip Gloss 와 Setting Mist 사이라 같은 판에 두지 않아요. 매니큐어 붓이 남긴 줄이 스스로 펴지는 것은 Cream Swirl 과 가까워서 같은 판에 두지 않아요. 젖은 립글로스나 탑코트가 굳는 것은 Clay Mask 처럼 광이 먼저, 색이 나중에 빠지는 두 문턱으로 그릴 만해요. 립밤이 체온에 녹아 번지거나 고체 향수가 문질러 펴지는 것은 Cleansing Balm 의 녹는 경계를 가로로 돌린 그림이에요. 흰 크림에 금박이 박혀 크림과 함께 천천히 접히는 것은 떠 있는 Gold Leaf 와 다른 그림이에요. 크림 한 방울이 젤 위에 떨어져 천천히 잠기는 것은 Essence Drop 의 거리장을 그대로 써요. 시트 마스크가 굴곡을 따라 붙으며 공기 방울이 밀려 빠지는 것은 Peel-Off 를 거꾸로 돌린 그림이에요. 크러시드 벨벳처럼 결이 조각마다 다른 방향으로 눌린 천을 보이지 않는 손이 쓰다듬어 조각이 하나씩 같은 방향으로 눕는 것은 Velvet Nap 의 결 벡터를 그대로 써요. 빛에 짙어지는 포토크로믹 네일은 보이지 않는 UV 손전등이 지나간 자리만 짙어졌다가 몇 초에 걸쳐 옅어져서, 바탕이 옅고 흔적이 짙은 Mood Tint 의 거꾸로예요. 헤어 오일 한 방울이 가닥을 따라 끝으로 미끄러지며 지나간 가닥만 한 톤 짙어지고 윤이 오래 남는 것은 Hair Gloss 의 가닥 위에 그릴 수 있어요. ## 다시 돌리기 From a0803614028b61df6201c4e22abe01da13f10df3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 17:11:38 +0000 Subject: [PATCH 9/9] =?UTF-8?q?=EC=97=B0=EA=B5=AC=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=2034=ED=8C=90=20=EC=97=B4=20=EA=B0=9C?= =?UTF-8?q?=EB=A5=BC=20=EB=84=A3=EC=96=B4=20=EB=8B=A4=EC=8B=9C=20=EC=A7=93?= =?UTF-8?q?=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_011BuccZdVDjYHZnwdNh4X1F --- incoming/pattern-study/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index b8c8832a2..517197eed 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -141,7 +141,7 @@

패턴 연구

- +