From dbfe07f22108edc64dc5ea2884e30dba6b8ef87b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 00:47:56 +0000 Subject: [PATCH 01/14] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=2053=ED=8C=90:=20?= =?UTF-8?q?=EC=88=98=ED=95=99=20=EB=8B=A4=EC=84=AF=EA=B3=BC=20=ED=95=A0?= =?UTF-8?q?=EB=A1=9C=EC=9C=88=20=EB=8B=A4=EC=84=AF?= 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_01Yb8D9UxrifXqrXju5CVbqY From dad291453d0f834cf6f065ce598d7e76ca27c41b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 01:36:59 +0000 Subject: [PATCH 02/14] =?UTF-8?q?53=ED=8C=90:=20Catenoid-Helicoid=20?= =?UTF-8?q?=EC=99=80=20House=20of=20Usher=20=ED=8C=A8=ED=84=B4=EC=9D=84=20?= =?UTF-8?q?=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Yb8D9UxrifXqrXju5CVbqY --- .../patterns/catenoid-helicoid.js | 227 +++++++++ .../pattern-study/patterns/house-of-usher.js | 458 ++++++++++++++++++ 2 files changed, 685 insertions(+) create mode 100644 incoming/pattern-study/patterns/catenoid-helicoid.js create mode 100644 incoming/pattern-study/patterns/house-of-usher.js diff --git a/incoming/pattern-study/patterns/catenoid-helicoid.js b/incoming/pattern-study/patterns/catenoid-helicoid.js new file mode 100644 index 000000000..b998c317c --- /dev/null +++ b/incoming/pattern-study/patterns/catenoid-helicoid.js @@ -0,0 +1,227 @@ +// ===== Patternflow pattern ===== +// Title: Catenoid-Helicoid +// Family: space +// Idea: A helicoid stands up the panel and turns about its axis while it unwinds into a catenoid and winds up again the other way, a checkerboard painted on it in (u, v) keeping every square the same size and shape the whole time, because the bending stretches nothing. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the catenoid (Leonhard Euler, 1744) bent into the helicoid (Jean Baptiste Meusnier, 1776) without stretching (Edmond Bour, 1862), pattern round 53 +// Round: 53 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Squares=12..36, Tilt=0..1 +// +// Knob 1: Hue. Turns both faces round the wheel together: at 0 the face that becomes the catenoid's inside is cyan and the other violet, and the dark squares of each sink toward indigo. The line v = 0 stays warm white. +// Knob 2: Speed. At 2 the surface takes 4 seconds to bend from the helicoid to the catenoid, lingering a little at each end, and turns once about its axis in 20 seconds; at 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Squares. How many checker squares go once round in u: 12 where it opens, where a square is about 9 LEDs on the line v = 0 and 16 at the free edge, and 36 at the top. The count is always even, so the checker closes up across the catenoid's seam. +// Knob 4: Tilt. How far we look down on the surface: 12 degrees at 0, 15 where it opens, 60 at 1, and the view nods 24 degrees further down as the catenoid forms so we see into its mouth. The scale is set once per tilt from the tallest moment of the bending. It never zooms while the surface bends, so the squares keep their size on the panel too. +// Buttons: K1 tap holds the bending where it is while the surface keeps turning; tap again to let it go on. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, round, sqrt, sin, cos, abs, pow, sinh, cosh, tanh, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The surface, for the bending angle th: +// x = cos th sinh v sin u + sin th cosh v cos u +// y = -cos th sinh v cos u + sin th cosh v sin u +// z = u cos th + v sin th +// th = 0 is the helicoid, th = pi/2 the catenoid. Its first fundamental +// form is cosh^2 v (du^2 + dv^2) for every th, so a square in (u, v) +// stays a square, and its normal (cos u / cosh v, sin u / cosh v, -tanh v) +// does not depend on th either. +const U = PI, V = 1.15; +const NU = 96, NV = 30, VU = NU + 1, VV = NV + 1, NPT = VU * VV; +const CAM = 12, CX = 31.5, CY = 63.5; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +export function setup(params) { + const F = (n) => new Float32Array(n); + const s = { + t: 0, b: 0, phi: 0.35, hold: false, hue: 0, squares: 12, tilt: 0.06, scaleTilt: -1, S: 18, + gu: F(NPT), gv: F(NPT), su: F(NPT), cu: F(NPT), sh: F(NPT), ch: F(NPT), + sx: F(NPT), sy: F(NPT), sd: F(NPT), + zb: F(N), pu: F(N), pv: F(N), side: new Int8Array(N), col: F(N * 3), + pal: F(12), palHue: -1, + }; + for (let j = 0; j < VV; j++) + for (let i = 0; i < VU; i++) { + const k = j * VU + i, u = -U + (2 * U * i) / NU, v = -V + (2 * V * j) / NV; + s.gu[k] = u; + s.gv[k] = v; + s.su[k] = sin(u); + s.cu[k] = cos(u); + s.sh[k] = sinh(v); + s.ch[k] = cosh(v); + } + params.s = s; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const s = params.s, k = input.knobValues || [0, 2, 12, 0.06], d = cl(dt || 0, 0, 0.1); + s.hue = cl(k[0], 0, 1); + const sp = cl(k[1], 0.2, 3); + s.squares = 2 * round(cl(k[2], 12, 36) / 2); + s.tilt = cl(k[3], 0, 1); + if (input.btnPressed && input.btnPressed[0]) s.hold = !s.hold; + s.t += d * sp; + s.phi += d * sp * 0.157; + if (!s.hold) s.b += d * sp * 0.125; +} + +// The bending angle: a quarter turn of th per unit of b, slowed near the +// helicoid and the catenoid so each end holds for a moment. +function bend(b) { + const n = floor(b), f = b - n; + return (PI / 2) * (n + f - (0.7 * sin(TAU * f)) / TAU); +} + +// The camera nods down as the catenoid forms, so we look into its mouth. +const look = (e0, st) => min(1.25, e0 + 0.42 * st * st); + +// One scale per tilt: the largest the surface ever gets on the panel over +// the whole bending and every turn, so the picture never zooms. +function fit(s, e0) { + let mu = 0, mx = 0; + for (let a = 0; a <= 24; a++) { + const th = (PI / 2) * (a / 24), ct = cos(th), st = sin(th), e = look(e0, st), ce = cos(e), se = sin(e); + for (let r = 0; r < 16; r++) { + const ph = (TAU * r) / 16, cp = cos(ph), spn = sin(ph); + for (let q = 0; q < 48; q++) { + let u, v; + if (q < 16) (u = q < 8 ? -U : U), (v = -V + (2 * V * (q % 8)) / 7); + else (u = -U + (2 * U * (q - 16)) / 31), (v = q & 1 ? V : -V); + const x = ct * sinh(v) * sin(u) + st * cosh(v) * cos(u), y = -ct * sinh(v) * cos(u) + st * cosh(v) * sin(u), z = u * ct + v * st; + const xr = x * cp - y * spn, yr = x * spn + y * cp; + const dep = yr * ce - z * se, up = yr * se + z * ce, f = CAM / (CAM + dep); + mu = max(mu, abs(up * f)); + mx = max(mx, abs(xr * f)); + } + } + } + s.S = min(65 / mu, 33.5 / mx); + s.scaleTilt = s.tilt; +} + +function palette(s) { + // Light and dark squares of the outer face, then of the inner face. + // A dark square is its light square at 0.4 over an indigo floor, so a + // warm face darkens toward plum rather than brown or olive. + hsv(s.hue + 0.52, 0.5, 1, s.pal, 0); + hsv(s.hue + 0.8, 0.45, 1, s.pal, 6); + for (const o of [0, 6]) { + s.pal[o + 3] = 0.4 * s.pal[o] + 0.1; + s.pal[o + 4] = 0.4 * s.pal[o + 1] + 0.03; + s.pal[o + 5] = 0.4 * s.pal[o + 2] + 0.28; + } + s.palHue = s.hue; +} + +export function draw(display, params) { + const s = params.s; + if (!s) return; + if (s.palHue !== s.hue) palette(s); + const e0 = ((12 + 48 * s.tilt) * PI) / 180; + if (s.scaleTilt !== s.tilt) fit(s, e0); + const th = bend(s.b), ct = cos(th), st = sin(th), cp = cos(s.phi), spn = sin(s.phi), S = s.S; + const e = look(e0, st), ce = cos(e), se = sin(e); + const { gu, gv, su, cu, sh, ch, sx, sy, sd, zb, pu, pv, side, col, pal } = s; + for (let k = 0; k < NPT; k++) { + const x = ct * sh[k] * su[k] + st * ch[k] * cu[k], y = -ct * sh[k] * cu[k] + st * ch[k] * su[k], z = gu[k] * ct + gv[k] * st; + const xr = x * cp - y * spn, yr = x * spn + y * cp; + const dep = yr * ce - z * se, up = yr * se + z * ce, f = (S * CAM) / (CAM + dep); + sx[k] = CX + f * xr; + sy[k] = CY - f * up; + sd[k] = dep; + } + zb.fill(1e9); + side.fill(0); + for (let j = 0; j < NV; j++) + for (let i = 0; i < NU; i++) { + const a = j * VU + i; + tri(s, a, a + 1, a + VU + 1); + tri(s, a, a + VU + 1, a + VU); + } + const n = s.squares, dl = TAU / n; + // Light from the upper left, in front: (screen x, depth, screen up). + const LX = -0.45, LD = -0.62, LU = 0.64, HD = LD - 1, HL = 1 / sqrt(LX * LX + LU * LU + HD * HD); + const lw = 0.62 / S; + for (let q = 0; q < N; q++) { + const o = q * 3, sd0 = side[q]; + if (!sd0) { + col[o] = col[o + 1] = col[o + 2] = 0; + continue; + } + const u = pu[q], v = pv[q], cv = cosh(v); + // The normal, the same for every th, turned with the surface. + const nx = cos(u) / cv, ny = sin(u) / cv, nz = -tanh(v); + let mx = nx * cp - ny * spn; + const nyr = nx * spn + ny * cp; + let md = nyr * ce - nz * se, mu = nyr * se + nz * ce; + if (md > 0) (mx = -mx), (md = -md), (mu = -mu); + const ld = max(0, mx * LX + md * LD + mu * LU), hh = max(0, (mx * LX + md * HD + mu * LU) * HL); + // Light falls off toward an indigo shade rather than toward black. + const spc = pow(hh, 24) * 0.45, lit = 0.2 + 0.8 * ld, sh0 = (1 - ld) * 0.16; + const par = (floor((u + U) / dl) + floor(v / dl)) & 1, c = (sd0 > 0 ? 0 : 6) + par * 3; + let r = pal[c] * lit + 0.3 * sh0 + spc, g = pal[c + 1] * lit + 0.1 * sh0 + spc, b = pal[c + 2] * lit + sh0 + spc; + // The line v = 0: the helicoid's axis and the catenoid's waist, the + // same length 2 pi all the way through the bending. + if (abs(v) < lw) (r = 1 * (0.45 + 0.55 * lit)), (g = 0.86 * (0.45 + 0.55 * lit)), (b = 0.62 * (0.45 + 0.55 * lit)); + col[o] = r; + col[o + 1] = g; + col[o + 2] = b; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const q = y * W + x, o = q * 3; + if (side[q]) { + // Where one sheet passes in front of another, or the ribbon turns + // over at a fold, a darker LED keeps the two apart. + let k = 255; + const rq = x < W - 1 ? q + 1 : q, dq = y < H - 1 ? q + W : q; + if ((side[rq] && (side[rq] !== side[q] || abs(zb[rq] - zb[q]) > 0.4)) || (side[dq] && (side[dq] !== side[q] || abs(zb[dq] - zb[q]) > 0.4))) k = 120; + display.setPixel(x, y, min(255, col[o] * k), min(255, col[o + 1] * k), min(255, col[o + 2] * k)); + continue; + } + // An empty LED on the outline takes a little of its lit neighbours. + let r = 0, g = 0, b = 0; + if (x > 0 && side[q - 1]) (r += col[o - 3]), (g += col[o - 2]), (b += col[o - 1]); + if (x < W - 1 && side[q + 1]) (r += col[o + 3]), (g += col[o + 4]), (b += col[o + 5]); + if (y > 0 && side[q - W]) (r += col[o - W * 3]), (g += col[o - W * 3 + 1]), (b += col[o - W * 3 + 2]); + if (y < H - 1 && side[q + W]) (r += col[o + W * 3]), (g += col[o + W * 3 + 1]), (b += col[o + W * 3 + 2]); + display.setPixel(x, y, r * 22, g * 22, b * 22); + } +} + +// One triangle of the (u, v) grid into the depth buffer. Its winding on +// the screen says which face of the surface we see there. +function tri(s, i0, i1, i2) { + const { sx, sy, sd, gu, gv, zb, pu, pv, side } = s; + const x0 = sx[i0], y0 = sy[i0], x1 = sx[i1], y1 = sy[i1], x2 = sx[i2], y2 = sy[i2]; + const den = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0); + if (abs(den) < 1e-9) return; + const inv = 1 / den, sg = den > 0 ? 1 : -1; + const X0 = max(0, Math.ceil(min(x0, x1, x2) - 0.5)), X1 = min(W - 1, floor(max(x0, x1, x2) - 0.5)); + const Y0 = max(0, Math.ceil(min(y0, y1, y2) - 0.5)), Y1 = min(H - 1, floor(max(y0, y1, y2) - 0.5)); + if (X0 > X1 || Y0 > Y1) return; + const z0 = sd[i0], dz1 = sd[i1] - z0, dz2 = sd[i2] - z0; + const u0 = gu[i0], du1 = gu[i1] - u0, du2 = gu[i2] - u0, v0 = gv[i0], dv1 = gv[i1] - v0, dv2 = gv[i2] - v0; + for (let y = Y0; y <= Y1; y++) { + const cy = y + 0.5 - y0; + for (let x = X0; x <= X1; x++) { + const cx = x + 0.5 - x0; + const a = (cx * (y2 - y0) - cy * (x2 - x0)) * inv, b = ((x1 - x0) * cy - (y1 - y0) * cx) * inv; + if (a < -1e-6 || b < -1e-6 || a + b > 1 + 1e-6) continue; + const dd = z0 + dz1 * a + dz2 * b, q = y * W + x; + if (dd < zb[q]) (zb[q] = dd), (pu[q] = u0 + du1 * a + du2 * b), (pv[q] = v0 + dv1 * a + dv2 * b), (side[q] = sg); + } + } +} diff --git a/incoming/pattern-study/patterns/house-of-usher.js b/incoming/pattern-study/patterns/house-of-usher.js new file mode 100644 index 000000000..e496b4dfb --- /dev/null +++ b/incoming/pattern-study/patterns/house-of-usher.js @@ -0,0 +1,458 @@ +// ===== Patternflow pattern ===== +// Title: House of Usher +// Family: halloween +// Idea: A grey mansion stands over a black tarn under racing storm cloud, a zigzag fissure from its roof to the water glowing slowly red until it opens on a blood-red full moon, the two halves rush apart and sink into the tarn in a burst of spray and rings, the moon shines over the closing water, and a leaden mist rises out of which the house stands again. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 53, after Edgar Allan Poe's The Fall of the House of Usher (1839) +// Round: 53 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Storm=-1..1, Mist=0..1 +// +// Knob 1: Hue. The storm sky and its reflection, kept on the cool side so the dim cloud never reads brown: violet at 0, indigo at 0.25, blue at 0.4, teal at 0.7, a sickly green at 1. The moon stays blood red and the one lit window stays green. +// Knob 2: Speed. At 2 one round of the story takes 17 seconds: the fissure glows for 6 and opens in 1.6, the halves sink for 3.2, the moon shines over the rings until the mist rises at 11.4, and the house is back by 15.2 and clear of the mist by 16.6. The first round opens 1.5 seconds in, so the first fall comes at 6.1. At 0.2 ten times slower, at 3 half as fast again; clouds, lightning and rain keep the same clock. +// Knob 3: Storm. At -1 still air: no rain, no lightning, slow cloud and a glassy tarn. At 0, where it opens, 92 drops of rain and a strike about every 5 seconds, the first at 2.4; at 1, 260 drops, a strike every 1.5 seconds and the tarn chopped by the wind into broken bars of reflection. +// Knob 4: Mist. The leaden vapour that hangs over the tarn and the foot of the house: a thin line at 0.06 where it opens, a bank that hides the lower windows at 0.5, and at 1 a fog over everything below the roofs. The mist that ends each fall rises on top of this. +// Buttons: K1 tap while the house stands opens the fissure now. + +const W = 64, H = 128, N = W * H, WL = 76, NU = W * WL; +const { floor, min, max, sqrt, abs, sin, cos, exp, round, 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); +}; +const mix = (a, b, t) => a + (b - a) * t; + +// The story, in seconds at Speed 2. +const T_WIDEN = 6, T_FALL = 7.6, T_GONE = 10.8, T_MIST = 11.4, T_BACK = 13.4, T_END = 17, T_START = 1.5; + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = [v, q, p, p, t, v][i]; + out[1] = [t, v, v, q, p, p][i]; + out[2] = [p, p, t, v, v, q][i]; +} + +// A fixed generator for the house, so its stones never change. +function prng(seed) { + let a = seed >>> 0; + return () => { + a = (a + 0x6d2b79f5) >>> 0; + let t = a; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +// Value noise on a 16 x 16 lattice that wraps. +const LAT = new Float32Array(256); +{ + const r = prng(11); + for (let i = 0; i < 256; i++) LAT[i] = r(); +} +function noise(x, y) { + const xi = floor(x), yi = floor(y), fx = x - xi, fy = y - yi; + const u = fx * fx * (3 - 2 * fx), v = fy * fy * (3 - 2 * fy); + const a = LAT[(yi & 15) * 16 + (xi & 15)], b = LAT[(yi & 15) * 16 + ((xi + 1) & 15)]; + const c = LAT[((yi + 1) & 15) * 16 + (xi & 15)], d = LAT[((yi + 1) & 15) * 16 + ((xi + 1) & 15)]; + return mix(mix(a, b, u), mix(c, d, u), v); +} + +// The fissure: a zigzag from the gable's peak down through the door to the water. +const FIS = [[32.5, 16], [30, 23], [35, 30], [29.5, 37], [34.5, 45], [29.5, 52], [34, 58], [31, 64], [33, 70], [32, 76]]; +function fisAt(y) { + if (y <= FIS[0][1]) return FIS[0][0]; + for (let i = 1; i < FIS.length; i++) if (y <= FIS[i][1]) return mix(FIS[i - 1][0], FIS[i][0], (y - FIS[i - 1][1]) / (FIS[i][1] - FIS[i - 1][1])); + return FIS[FIS.length - 1][0]; +} +// The same, looked up in quarter rows. +const FT = new Float32Array(WL * 4 + 1); +for (let i = 0; i <= WL * 4; i++) FT[i] = fisAt(i / 4); +const fisX = (y) => FT[cl(round(y * 4), 0, WL * 4)]; + +// Materials: 1 wall, 2 roof, 3 vacant window, 4 the lit window, 5 door. +function buildHouse(mat, shade, crack) { + const r = prng(5); + const win = (x, y, x0, y0, h) => x >= x0 && x < x0 + 3 && y >= y0 && y < y0 + h && (y > y0 || x === x0 + 1); + for (let y = 0; y < WL; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + let m = 0; + if (x >= 4 && x <= 59 && y >= 46) m = 1; + if (x >= 21 && x <= 43 && y >= 37) m = 1; + // Hipped roofs on the wings and a steep gable over the middle. + if (y >= 35 && y < 46 && x >= 4 + (46 - y) * 0.9 && x <= 21) m = 2; + if (y >= 35 && y < 46 && x <= 59 - (46 - y) * 0.9 && x >= 43) m = 2; + if (y >= 16 && y < 37 && abs(x - 32) <= (y - 16) * 0.62) m = 2; + // Chimneys with caps. + if ((x >= 9 && x <= 12) || (x >= 51 && x <= 54)) if (y >= 28 && y < 40) m = 1; + if ((x >= 8 && x <= 13) || (x >= 50 && x <= 55)) if (y >= 27 && y < 29) m = 2; + if (m === 1) { + // Vacant windows, pointed at the top: three to a row on each wing. + for (const x0 of [7, 12, 17, 45, 50, 55]) for (const y0 of [50, 61]) if (win(x, y, x0, y0, 6)) m = x0 === 50 && y0 === 50 ? 4 : 3; + for (const x0 of [24, 38]) if (win(x, y, x0, 41, 9)) m = 3; + // The door: a pointed arch. + if (abs(x - 32) <= 3.5 && y >= 61 + abs(x - 32) * 0.9) m = 5; + } + if (m === 2 && (x - 32) ** 2 + (y - 29) ** 2 <= 6.5) m = 3; + mat[p] = m; + // Stone courses, blocks of different age, and dark streaks of fungus + // hanging from the eaves. + let s = 0.9 + 0.2 * r() + 0.25 * (noise(x * 0.18 + 3, y * 0.18) - 0.5); + if (m === 1) { + if (y % 4 === 0) s *= 0.8; + else if ((x + ((y >> 2) & 1) * 3) % 6 === 0) s *= 0.87; + } + if (m === 2 && y & 1) s *= 0.85; + shade[p] = s; + } + for (let k = 0; k < 26; k++) { + const x = 5 + floor(r() * 54), y0 = x > 20 && x < 44 ? 37 : 46, len = 3 + floor(r() * 9); + for (let y = y0; y < y0 + len && y < WL; y++) if (mat[y * W + x] === 1) shade[y * W + x] *= 0.62 + (0.3 * (y - y0)) / len; + } + // Distance from each LED to the fissure, for its faint line and its glow. + crack.fill(9); + for (let i = 1; i < FIS.length; i++) { + const [ax, ay] = FIS[i - 1], [bx, by] = FIS[i]; + for (let s = 0; s <= 40; s++) { + const fx = mix(ax, bx, s / 40), fy = mix(ay, by, s / 40); + for (let y = max(0, floor(fy) - 4); y <= min(WL - 1, floor(fy) + 4); y++) + for (let x = max(0, floor(fx) - 4); x <= min(W - 1, floor(fx) + 4); x++) { + const d = sqrt((x + 0.5 - fx) ** 2 + (y + 0.5 - fy) ** 2), p = y * W + x; + if (d < crack[p]) crack[p] = d; + } + } + } +} + +export function setup(params) { + const F = (n) => new Float32Array(n); + const s = { + hue: 0, sp: 2, storm: 0, mist: 0.06, + t: 0, c: T_START, wind: 0, flashAt: -9, nextStrike: 2.4, bolt: [], + mat: new Uint8Array(NU), shade: F(NU), crack: F(NU), fb: F(N * 3), hm: new Uint8Array(NU), cf: F(NU + 2 * W), + drops: [], deb: [], spray: [], rings: [], fallAcc: 0, chop: 0.2, slant: 0.3, + halves: [{ dx: 0, dy: 0, a: 0, px: 4.5 }, { dx: 0, dy: 0, a: 0, px: 59.5 }], + sky0: F(3), sky1: F(3), + }; + buildHouse(s.mat, s.shade, s.crack); + for (let i = 0; i < 260; i++) s.drops.push(newDrop(Math.random() * H)); + params.s = s; +} + +function newDrop(y) { + return { x: Math.random() * (W + 30) - 8, y, v: 0.8 + 0.4 * Math.random(), land: WL + 1 + Math.random() * (H - WL - 2), dim: 0 }; +} + +// Lightning: a jagged channel from the top down behind the house. +function makeBolt(s) { + const pts = [[8 + Math.random() * 48, -1]]; + while (pts[pts.length - 1][1] < 44) { + const [x, y] = pts[pts.length - 1]; + pts.push([cl(x + (Math.random() - 0.5) * 9, 2, 61), y + 3 + Math.random() * 5]); + } + s.bolt = pts; +} + +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 = cl(k[0], 0, 1); + s.sp = cl(k[1], 0.2, 3); + s.storm = (cl(k[2], -1, 1) + 1) / 2; + s.mist = cl(k[3], 0, 1); + const ds = (d * s.sp) / 2, c0 = s.c; + s.t += ds; + s.c += ds; + if (input.btnPressed && input.btnPressed[0] && s.c < T_WIDEN) s.c = T_WIDEN; + if (s.c >= T_END) s.c -= T_END; + const c = s.c, st = s.storm; + // The whirlwind that comes as the fissure opens. + const whirl = sm(5.6, 6.6, c) * (1 - sm(9.5, 11.5, c)); + s.wind += ds * (0.35 + 1.4 * st) * (1 + 1.6 * whirl); + s.chop = 0.15 + 1.1 * st * st + 2.6 * sm(7.8, 8.6, c) * (1 - sm(10.6, 14, c)); + + if (s.t >= s.nextStrike) { + if (st > 0.04) { + s.flashAt = s.t; + makeBolt(s); + } + s.nextStrike = s.t + mix(8, 1.5, st) * (0.6 + 0.8 * Math.random()); + } + + // Rain falls past the house and lands on the tarn somewhere between the + // far shore and our feet, leaving a dimple. + const nd = round(260 * st ** 1.5), vy = 120 * (s.sp / 2) + 25, slant = 0.25 + 0.2 * st; + for (let i = 0; i < nd; i++) { + const r = s.drops[i]; + if (r.dim > 0) { + r.dim -= d; + if (r.dim <= 0) Object.assign(r, newDrop(-Math.random() * 30)); + continue; + } + r.y += r.v * vy * d; + r.x -= r.v * vy * slant * d; + if (r.y >= r.land) (r.y = r.land), (r.dim = 0.13); + } + s.slant = slant; + + // The two halves. Each turns about its outer foot, slides outward and + // sinks; the right one starts a quarter second later. + for (let h = 0; h < 2; h++) { + const f = s.halves[h], tf = c < T_GONE ? max(0, c - T_FALL - 0.25 * h) : 0, sg = h ? 1 : -1; + const shake = c > T_WIDEN && c < T_FALL + 0.6 ? 0.6 * sin(s.t * 55 + h * 2) * sm(T_WIDEN, T_FALL, c) : 0; + f.dx = sg * 8 * sm(0, 1.7, tf) + shake; + f.dy = 13 * tf * tf; + f.a = sg * 0.3 * sm(0.15, 2.3, tf); + f.px = h ? 59.5 : 4.5; + } + // Stones from the fissure, then spray where anything meets the water. + if (c > T_FALL && c < T_GONE - 0.6) { + s.fallAcc += ds * 34; + while (s.fallAcc >= 1 && s.deb.length < 90) { + s.fallAcc -= 1; + const h = Math.random() < 0.5 ? 0 : 1, f = s.halves[h], y = 18 + Math.random() * 56, x = fisX(y) + (h ? 2 : -2); + const [px, py] = place(f, x, y); + if (py < WL - 1) s.deb.push({ x: px, y: py, vx: (h ? 1 : -1) * (4 + 14 * Math.random()), vy: -6 * Math.random() }); + } + for (let h = 0; h < 2; h++) { + const f = s.halves[h], foot = place(f, h ? 40 : 24, 75); + if (Math.random() < ds * 10) splash(s, foot[0] + (Math.random() - 0.5) * 24, 2 + Math.random() * 3); + } + } + for (const b of s.deb) { + b.vy += 70 * ds; + b.x += b.vx * ds; + b.y += b.vy * ds; + if (b.y >= WL - 0.5) (b.dead = 1), splash(s, b.x, 1 + Math.random()); + } + s.deb = s.deb.filter((b) => !b.dead && b.x > -3 && b.x < W + 3); + for (const p of s.spray) { + p.vy += 75 * ds; + p.x += p.vx * ds; + p.y += p.vy * ds; + } + s.spray = s.spray.filter((p) => p.y < WL); + for (const r of s.rings) r.r += ds * 13; + s.rings = s.rings.filter((r) => r.r < 60); + if (c < c0) (s.deb.length = 0), (s.spray.length = 0); +} + +// Where house point (x, y) is now on the panel, for half f. +function place(f, x, y) { + const ca = cos(f.a), sa = sin(f.a), X = x - f.px, Y = y - 75; + return [f.px + ca * X - sa * Y + f.dx, 75 + sa * X + ca * Y + f.dy]; +} + +function splash(s, x, n) { + for (let i = 0; i < n && s.spray.length < 90; i++) s.spray.push({ x, y: WL - 1, vx: (Math.random() - 0.5) * 16, vy: -(10 + 24 * Math.random()) }); + if (s.rings.length < 14) s.rings.push({ x, r: 0.5, a: 0.5 + 0.1 * n }); +} + +export function draw(display, params) { + const s = params.s; + if (!s) return; + const { fb, mat, shade, crack, sky0, sky1, hm, cf } = s; + const c = s.c, t = s.t, st = s.storm, hue = 0.76 - 0.44 * s.hue; + hsv(hue, 0.62, 0.34, sky0); + hsv(hue + 0.03, 0.42, 0.66, sky1); + // The flash: two pulses, the second a little later and weaker. + const fa = t - s.flashAt, fl = fa < 0 || fa > 1 ? 0 : (exp(-fa * 10) + (fa > 0.14 ? 0.7 * exp(-(fa - 0.14) * 9) : 0)) * (0.5 + 0.5 * st); + // The moon: behind the house until the fissure opens, then full over the + // water until the mist takes it. + const moon = (0.28 + 0.72 * sm(5.4, 7.2, c)) * (1 - 0.72 * sm(12, 14.2, c)); + const glow = c < T_WIDEN ? 0.3 * sm(0.4, T_WIDEN, c) : c < T_GONE ? 0.3 + 0.7 * sm(T_WIDEN, 7.2, c) : 0; + const gap = c < T_GONE ? 3.4 * sm(T_WIDEN, T_FALL, c) + 2.5 * sm(T_FALL, 8.4, c) : 0; + const alpha = c < T_GONE ? 1 : sm(T_BACK, T_BACK + 1.8, c); + const MX = 32, MY = 51, MR = 14.5; + const w0 = s.wind, h0 = s.halves[0], h1 = s.halves[1]; + const c0a = cos(h0.a), s0a = sin(h0.a), c1a = cos(h1.a), s1a = sin(h1.a); + const lw = 0.72 + 0.28 * noise(t * 3.1, 5.5); + // The cloud field, two rows deeper than the sky so each LED can see + // whether cloud thins below it: there its underside catches the light. + // It is stretched sideways, so every other LED is the mean of its neighbours. + for (let y = 0; y < WL + 2; y++) { + const q = y * W; + for (let x = 0; x <= W; x += 2) cf[q + min(x, W - 1)] = noise(x * 0.045 + w0, y * 0.2 + 2) * 0.65 + noise(x * 0.1 + w0 * 1.7, y * 0.4) * 0.35; + for (let x = 1; x < W - 1; x += 2) cf[q + x] = (cf[q + x - 1] + cf[q + x + 1]) / 2; + } + + for (let y = 0; y < WL; y++) { + const gy = y / WL; + for (let x = 0; x < W; x++) { + const o = (y * W + x) * 3; + // Sky and racing cloud with faintly luminous undersides. + const n1 = cf[y * W + x], n2 = cf[(y + 2) * W + x]; + const dn = sm(0.4, 0.68, n1) * (1 - 0.6 * gy), under = cl((n1 - n2) * 7, 0, 1) * dn; + let r = mix(sky0[0], sky1[0], gy), g = mix(sky0[1], sky1[1], gy), b = mix(sky0[2], sky1[2], gy); + r = r * (1 - 0.42 * dn) + under * 0.42; + g = g * (1 - 0.42 * dn) + under * 0.36; + b = b * (1 - 0.42 * dn) + under * 0.5; + // The blood-red moon and its halo, dimmed where cloud crosses it. + const md = sqrt((x + 0.5 - MX) ** 2 + (y + 0.5 - MY) ** 2), mv = moon * (1 - 0.6 * dn); + if (md < MR) { + const e = md / MR, k = mv * (1 - 0.25 * e * e); + r = mix(r, 1, k); + g = mix(g, 0.22 - 0.14 * e, k); + b = mix(b, 0.12 - 0.06 * e, k); + } else if (md < MR + 30) { + const hk = mv * 0.55 * exp(-(md - MR) / 6); + r += hk * 0.85; + g += hk * 0.04; + b += hk * 0.22; + } + r += fl * 0.55; + g += fl * 0.5; + b += fl * 0.65; + // The house: either half, as it stands or as it falls. + hm[y * W + x] = 0; + if (alpha > 0) { + let m = 0, p = 0; + for (let h = 0; h < 2 && !m; h++) { + const f = h ? h1 : h0, ca = h ? c1a : c0a, sa = h ? s1a : s0a; + const X = x + 0.5 - f.px - f.dx, Y = y + 0.5 - 75 - f.dy; + const hx = f.px + ca * X + sa * Y, hy = 75 - sa * X + ca * Y; + const ix = floor(hx), iy = floor(hy); + if (ix < 0 || ix >= W || iy < 0 || iy >= WL) continue; + const q = iy * W + ix, fx = fisX(hy); + if (mat[q] && (h ? hx >= fx + gap / 2 : hx < fx - gap / 2)) (m = mat[q]), (p = q); + } + if (m) { + hm[y * W + x] = 1; + const sh = shade[p]; + let hr, hg, hb; + if (m === 1) (hr = 0.34 * sh), (hg = 0.32 * sh), (hb = 0.38 * sh); + else if (m === 2) (hr = 0.15 * sh), (hg = 0.14 * sh), (hb = 0.19 * sh); + else if (m === 4) (hr = 0.32 * lw), (hg = 1 * lw), (hb = 0.3 * lw); + else (hr = 0.02), (hg = 0.015), (hb = 0.035); + if (m <= 2) { + hr += fl * 0.4; + hg += fl * 0.38; + hb += fl * 0.5; + // The fissure: a darker line that glows red as the moon finds it, + // and the red it throws on the stone beside it. + const cd = crack[p]; + if (cd < 0.75 && gap < 0.5) (hr = mix(hr * 0.78, 0.85, glow)), (hg = mix(hg * 0.78, 0.06, glow)), (hb = mix(hb * 0.78, 0.12, glow)); + else if (cd < 4) { + const sp = glow * glow * (1 - cd / 4) * 0.55; + hr += sp; + hg += sp * 0.12; + hb += sp * 0.16; + } + } + r = mix(r, hr, alpha); + g = mix(g, hg, alpha); + b = mix(b, hb, alpha); + } + } + // White trunks of dead trees on the banks either side. + if ((x === 1 && y > 47) || (x === 2 && y > 52) || (x === 61 && y > 55) || (x === 62 && y > 60) || (x === 3 && y === 53) || (x === 4 && y === 51) || (x === 60 && y === 58)) (r = 0.55 + fl * 0.3), (g = 0.53 + fl * 0.3), (b = 0.6 + fl * 0.3); + fb[o] = r; + fb[o + 1] = g; + fb[o + 2] = b; + } + } + // The bolt, behind the house only where the sky shows. + if (fa >= 0 && fa < 0.2 && s.bolt.length) + for (let i = 1; i < s.bolt.length; i++) { + const [ax, ay] = s.bolt[i - 1], [bx, by] = s.bolt[i]; + for (let k = 0; k <= 8; k++) { + const x = floor(mix(ax, bx, k / 8)), y = floor(mix(ay, by, k / 8)); + if (x < 0 || x >= W || y < 0 || y >= WL) continue; + const o = (y * W + x) * 3; + if (!hm[y * W + x]) (fb[o] = 0.95), (fb[o + 1] = 0.9), (fb[o + 2] = 1); + } + } + // Falling stones and spray. + for (const b of s.deb) dot(fb, b.x, b.y, 0.42, 0.4, 0.46, 1); + for (const p of s.spray) dot(fb, p.x, p.y, 0.7, 0.68, 0.82, 0.8); + + // The tarn: the scene above, upside down, broken by the wind and by the + // fall, darker toward our feet. + const ch = s.chop, hr = sky1[0], hg = sky1[1], hb = sky1[2]; + for (let y = WL; y < H; y++) { + const dd = y - WL + 1, ps = 0.4 + dd / 28, rf = 0.52 - (0.24 * dd) / (H - WL); + for (let x = 0; x < W; x++) { + const ph = 0.55 * dd - 4.1 * t + 0.21 * x; + const xo = ch * ps * (sin(ph) + 0.6 * sin(0.23 * dd + 2.7 * t - 0.13 * x)); + const yo = ch * 0.6 * ps * sin(0.8 * dd - 3.3 * t + 0.1 * x); + const sx = cl(round(x + xo), 0, W - 1), sy = cl(round(WL - dd - yo), 0, WL - 1); + const i = (sy * W + sx) * 3, o = (y * W + x) * 3; + let r = fb[i] * rf * 0.85 + 0.01, g = fb[i + 1] * rf * 0.85 + 0.005, b = fb[i + 2] * rf + 0.02; + // Glints where the wavelets face the sky. + const gl = ch > 0.25 && cos(ph) > 0.6 ? cos(ph) * noise(x * 0.4 + t * 1.3, dd * 0.7 - t * 0.6) : 0; + if (gl > 0.6) { + const k = min(1, ch) * (gl - 0.6) * 1.2; + r += hr * k; + g += hg * k; + b += hb * k; + } + fb[o] = r; + fb[o + 1] = g; + fb[o + 2] = b; + } + } + // Rings where the house and its stones went in. + for (const rg of s.rings) { + const k = rg.a * 0.6 * (1 - rg.r / 60) * min(1, rg.r / 4); + for (let y = WL; y < H; y++) { + const dy = (y - WL + 0.5) / 0.5, wo = (rg.r + 0.9) ** 2 - dy * dy; + if (wo < 0) break; + const a = sqrt(max(0, (rg.r - 0.9) ** 2 - dy * dy)), b = sqrt(wo); + for (let x = max(0, Math.ceil(rg.x - b)); x <= min(W - 1, rg.x + b); x++) { + if (abs(x - rg.x) < a) continue; + const o = (y * W + x) * 3; + fb[o] += 0.34 * k; + fb[o + 1] += 0.3 * k; + fb[o + 2] += 0.42 * k; + } + } + } + // Mist: a leaden vapour over the water, rising thick after the fall. + const boost = sm(T_MIST, T_MIST + 1.8, c) * (1 - sm(14.8, 16.6, c)), den = s.mist * 0.75 + boost; + if (den > 0.01) { + const top = mix(64, 6, min(1, s.mist * 0.7 + boost)), wv = s.t * 0.2; + for (let y = 0; y < H; y++) { + const v = y < 74 ? sm(top, 74, y) : 1 - 0.55 * sm(74, H, y); + if (v <= 0) continue; + for (let x = 0; x < W; x++) { + const n = 0.6 * noise(x * 0.05 - wv, y * 0.16 + wv * 0.2) + 0.4 * noise(x * 0.12 - wv * 1.6, y * 0.3 + 7); + const m = min(0.88, den * v * (0.25 + sm(0.3, 0.75, n))); + const o = (y * W + x) * 3; + fb[o] = fb[o] * (1 - 0.45 * m) + 0.4 * m; + fb[o + 1] = fb[o + 1] * (1 - 0.45 * m) + 0.38 * m; + fb[o + 2] = fb[o + 2] * (1 - 0.45 * m) + 0.46 * m; + } + } + } + // Rain streaks, and dimples where drops strike the tarn. + const nd = round(260 * st ** 1.5); + for (let i = 0; i < nd; i++) { + const r = s.drops[i]; + if (r.dim > 0) { + const a = r.dim / 0.13; + dot(fb, r.x, r.y, 0.3 * a, 0.28 * a, 0.36 * a, 1, true); + dot(fb, r.x - 1, r.y, 0.15 * a, 0.14 * a, 0.18 * a, 1, true); + dot(fb, r.x + 1, r.y, 0.15 * a, 0.14 * a, 0.18 * a, 1, true); + continue; + } + for (let k = 0; k < 4; k++) dot(fb, r.x + k * s.slant, r.y - k, 0.2 * (1 - k / 4), 0.19 * (1 - k / 4), 0.25 * (1 - k / 4), 1, true); + } + for (let q = 0; q < N; q++) { + const o = q * 3; + display.setPixel(q % W, (q / W) | 0, min(255, fb[o] * 255), min(255, fb[o + 1] * 255), min(255, fb[o + 2] * 255)); + } +} + +function dot(fb, x, y, r, g, b, a, add) { + const ix = floor(x), iy = floor(y); + if (ix < 0 || ix >= W || iy < 0 || iy >= H) return; + const o = (iy * W + ix) * 3; + if (add) (fb[o] += r), (fb[o + 1] += g), (fb[o + 2] += b); + else (fb[o] = mix(fb[o], r, a)), (fb[o + 1] = mix(fb[o + 1], g, a)), (fb[o + 2] = mix(fb[o + 2], b, a)); +} From fce81243ef81d85e98c060fa44c9a6ad7ca98f91 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 01:49:42 +0000 Subject: [PATCH 03/14] =?UTF-8?q?53=ED=8C=90:=20Gaussian=20Periods=20?= =?UTF-8?q?=EC=99=80=20Medusa=20=ED=8C=A8=ED=84=B4=EC=9D=84=20=EB=8D=94?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Yb8D9UxrifXqrXju5CVbqY --- .../patterns/gaussian-periods.js | 187 ++++++++ incoming/pattern-study/patterns/medusa.js | 453 ++++++++++++++++++ 2 files changed, 640 insertions(+) create mode 100644 incoming/pattern-study/patterns/gaussian-periods.js create mode 100644 incoming/pattern-study/patterns/medusa.js diff --git a/incoming/pattern-study/patterns/gaussian-periods.js b/incoming/pattern-study/patterns/gaussian-periods.js new file mode 100644 index 000000000..f8df12fe7 --- /dev/null +++ b/incoming/pattern-study/patterns/gaussian-periods.js @@ -0,0 +1,187 @@ +// ===== Patternflow pattern ===== +// Title: Gaussian Periods +// Family: number +// Idea: Every residue k mod n lights one LED at its Gaussian period eta(k), a sum of d roots of unity, so eleven rotated deltoids stand as a sunburst of dots coloured by k mod 11; then every point slides to eta(u k), the burst comes apart, and the same set of points builds it again with the copies' colours traded. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Gaussian periods (Carl Friedrich Gauss, 1801), plotted as William Duke, Stephan Ramon Garcia and Bob Lutz plotted them (2015), pattern round 53 +// Round: 53 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Figure=0..7, Unit=0..2 +// +// Knob 1: Hue. Turns the colour classes round the wheel together. At 0 the class of k = 0 is cyan and the other classes step evenly round the wheel, ordered so that neighbouring spikes get neighbouring hues. +// Knob 2: Speed. At 2 the figure rests 1 second and a rebuild takes 1.75 seconds; at 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Figure. Eight pairs (n, omega): eleven rotated deltoids (n = 11 * 991), seven (7 * 1201), four (4 * 1201), a deltoid and its negative (2 * 2719), deltoids scaled by 2 cos(2 pi j / 13) (13 * 1021), the five-cusp star of the prime 12011, a nine-point star (9 * 1093) and the deltoid lace of 7^4. +// Knob 4: Unit. Which multiplier u the rebuilds use. At 0 a unit that carries points into other colour classes, at 1 one that keeps every class in place so each copy rebuilds itself, at 2 one that turns whole copies about the centre. A figure without copies uses u = -1 there, which folds it onto its mirror image. +// Buttons: K1 tap goes to the next figure. + +const W = 64, H = 128; +const { floor, round, min, max, cos, sin, abs, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// [n, omega, colour kind, m, g, q, x zoom, hue step, three units u]. +// Colour kind 0 is k mod m, kind 1 is k mod m up to sign, kind 2 is the class +// of k mod q among the units mod q, read off the m-th powers of the +// primitive root g, with k = 0 mod q a class of its own. Each class is a +// union of whole orbits k, k omega, k omega^2, ..., so every dot has one +// colour. The hue step orders the classes so that neighbouring spikes get +// neighbouring hues. The first unit carries points into other classes (on +// the hexagram none can, so it is a second unit of the next kind), the +// second keeps every class in place, the third turns whole copies about the +// centre (u = 1 mod n / m) or, on a figure without copies, is u = -1. +const FIG = [ + [10901, 9032, 0, 11, 0, 0, 1.2, 3, 882, 870, 992], // 11 * 991, omega = 1 mod 11: eleven rotated deltoids + [8407, 3032, 0, 7, 0, 0, 1.1, 6, 1363, 2689, 1202], // 7 * 1201: seven rotated deltoids + [4804, 4173, 0, 4, 0, 0, 1, 3, 679, 1921, 2403], // 4 * 1201: four rotated deltoids + [5438, 1453, 0, 2, 0, 0, 1, 1, 57, 819, 5437], // 2 * 2719: a deltoid and its negative + [13273, 3431, 1, 13, 0, 0, 1, 1, 87, 116, 13272], // 13 * 1021, omega = -1 mod 13: deltoids scaled by 2 cos(2 pi j / 13) + [12011, 2557, 2, 2, 2, 12011, 1, 1, 1819, 151, 12010], // prime, d = 5: the five-cusp star, by quadratic character + [9837, 151, 0, 3, 0, 0, 1, 1, 2960, 31, 3280], // 9 * 1093, omega of order 3 mod 9 too: a nine-point star + [2401, 1047, 2, 2, 3, 7, 1, 1, 38, 1086, 2400], // 7^4, d = 3: deltoid lace, by quadratic character of k mod 7 +]; + +function hsv(h, s, v) { + 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); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// Build one figure: the orbits of omega on Z/n, eta for every k, the colour +// of each orbit and the scale that fills the panel. +function build(idx) { + const [n, w, kind, m, g, q, zx, hm, u0, u1, u2] = FIG[idx]; + const pw = [1]; + for (let x = w % n; x !== 1; x = (x * w) % n) pw.push(x); + const d = pw.length; + const C = new Float64Array(n), S = new Float64Array(n); + for (let k = 0; k < n; k++) (C[k] = cos((TAU * k) / n)), (S[k] = sin((TAU * k) / n)); + const er = new Float64Array(n), ei = new Float64Array(n); + for (let k = 0; k < n; k++) { + let a = 0, b = 0; + for (let j = 0; j < d; j++) { + const r = (k * pw[j]) % n; + a += C[r]; + b += S[r]; + } + er[k] = a; + ei[k] = b; + } + let lg = null; + if (kind === 2) { + lg = new Int32Array(q); + for (let e = 0, x = 1; e < q - 1; e++, x = (x * g) % q) lg[x] = e % m; + } + const seen = new Uint8Array(n), rep = []; + for (let k = 0; k < n; k++) { + if (seen[k]) continue; + for (let j = 0; j < d; j++) seen[(k * pw[j]) % n] = 1; + rep.push(k); + } + const np = rep.length, ncls = kind === 0 ? m : kind === 1 ? (m >> 1) + 1 : m + 1; + const cls = new Int16Array(np), slot = new Float32Array(ncls); + for (let p = 0; p < np; p++) { + const k = rep[p], r = k % m; + cls[p] = kind === 0 ? r : kind === 1 ? min(r, m - r) : k % q === 0 ? m : lg[k % q]; + } + for (let c = 0; c < ncls; c++) slot[c] = ((hm * c) % ncls) / ncls; + let r0 = 1e9, r1 = -1e9, i1 = 0; + for (let k = 0; k < n; k++) (r0 = min(r0, er[k])), (r1 = max(r1, er[k])), (i1 = max(i1, abs(ei[k]))); + // The imaginary axis runs across and the real axis up, each scaled to the + // panel, so a round figure stands twice as tall as it is wide and complex + // conjugation, eta(-k), is the left-right mirror. The eleven and the seven + // deltoids are drawn 1.2 and 1.1 times wider and lose the tips of their + // side spikes. + const sx = (31.2 * zx) / i1, sy = 126.4 / (r1 - r0), cr = (r0 + r1) / 2; + return { + idx, n, d, pw, er, ei, sx, sy, cr, ncls, slot, units: [u0, u1, u2], + cls, cur: Int32Array.from(rep), np, a: new Int32Array(np * d), dl: new Int32Array(np * d), + }; +} + +export function setup(params) { + params.s = { f: build(0), fig: 0, knobFig: 0, t: 0, s: 0, moving: false, u: 0, hue: -1, palHue: -2, pal: [], grid: new Int16Array(W * H) }; +} + +const TRAVEL = 3.5, REST = 2; + +// A rebuild: root j of point k, e(k omega^j / n), turns along the shorter +// arc to e(u k omega^j / n), so the point ends exactly at eta(u k). +function startRebuild(s, u) { + const { n, d, pw, cur, a, dl, np } = s.f; + for (let p = 0; p < np; p++) { + const k = cur[p], v = (u * k) % n; + for (let j = 0; j < d; j++) { + const x = (k * pw[j]) % n; + let e = ((v * pw[j]) % n) - x; + if (e > n / 2) e -= n; + else if (e <= -n / 2) e += n; + a[p * d + j] = x; + dl[p * d + j] = e; + } + } + s.u = u; + s.moving = true; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const s = params.s, k = input.knobValues || [0, 2, 0, 0.06], step = cl(dt || 0, 0, 0.1); + const kf = cl(round(k[2]), 0, FIG.length - 1); + if (kf !== s.knobFig) (s.knobFig = kf), (s.fig = kf); + if (input.btnPressed && input.btnPressed[0]) s.fig = (s.fig + 1) % FIG.length; + if (s.fig !== s.f.idx) (s.f = build(s.fig)), (s.t = 0), (s.moving = false); + const f = s.f; + s.hue = cl(k[0], 0, 1); + s.t += step * cl(k[1], 0.2, 3); + if (!s.moving) { + s.s = 0; + if (s.t >= REST) (s.t -= REST), startRebuild(s, f.units[cl(round(k[3]), 0, 2)]); + } + if (s.moving) { + if (s.t >= TRAVEL) { + s.t -= TRAVEL; + s.moving = false; + s.s = 0; + for (let p = 0; p < f.np; p++) f.cur[p] = (s.u * f.cur[p]) % f.n; + } else { + const x = s.t / TRAVEL; + s.s = x * x * (3 - 2 * x); + } + } +} + +export function draw(display, params) { + const s = params.s; + if (!s) return; + const f = s.f, { n, d, er, ei, sx, sy, cr, cls, cur, a, dl, np, ncls, slot } = f; + if (s.palHue !== s.hue || s.pal.length !== ncls) { + s.pal = []; + for (let c = 0; c < ncls; c++) s.pal.push(hsv(s.hue + 0.5 + slot[c], 0.78, 255)); + s.palHue = s.hue; + } + const grid = s.grid; + grid.fill(-1); + const t = s.s, moving = s.moving && t > 0; + for (let p = 0; p < np; p++) { + let re = 0, im = 0; + if (!moving) (re = er[cur[p]]), (im = ei[cur[p]]); + else + for (let j = 0, o = p * d; j < d; j++, o++) { + const ang = (TAU * (a[o] + t * dl[o])) / n; + re += cos(ang); + im += sin(ang); + } + const x = floor(31.5 + im * sx + 0.5), y = floor(63.5 - (re - cr) * sy + 0.5); + if (x >= 0 && x < W && y >= 0 && y < H) grid[y * W + x] = cls[p]; + } + const pal = s.pal; + for (let y = 0, q = 0; y < H; y++) + for (let x = 0; x < W; x++, q++) { + const c = grid[q]; + if (c < 0) display.setPixel(x, y, 0, 0, 0); + else display.setPixel(x, y, pal[c][0], pal[c][1], pal[c][2]); + } +} diff --git a/incoming/pattern-study/patterns/medusa.js b/incoming/pattern-study/patterns/medusa.js new file mode 100644 index 000000000..b1c5102a1 --- /dev/null +++ b/incoming/pattern-study/patterns/medusa.js @@ -0,0 +1,453 @@ +// ===== Patternflow pattern ===== +// Title: Medusa +// Family: halloween +// Idea: A Gorgon's head stands against a purple night with an orange glow over her crown: a crown of green and violet snakes rises from her hair and hangs past her cheeks, writhing, flicking their tongues and striking, until her eyes open toxic green and a ring of their light runs out from her gaze, turning snakes, face and robe to grey stone that holds, cracks and falls away in flakes so the colour and the writhing come back. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 53, after the Gorgon Medusa of Greek myth +// Round: 53 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Snakes=9..17, Writhe=-1..1 +// +// Knob 1: Hue. Turns the snakes, her eyes and the ring of her gaze round the wheel together. At 0 two snakes in three are toxic green with a black zigzag, the third violet with an orange one, and her eyes and the ring are green; her face, robe and the night keep their colours. +// Knob 2: Speed. At 2 a wave runs up each snake at 13 to 23 LEDs a second, each snake strikes every 4 to 7 seconds and flicks its tongue every 1.6 to 2.6, and she looks 3.4 seconds in and then every 13.4: her eyes open in 0.35 seconds, the stone takes 1.7 to pass every corner, holds about 1.3, cracks over 0.8 and falls away over 1.8. At 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Snakes. How many snakes grow from her hair, 9 to 17. With 9 one hangs on each side and seven rise; from 13 two hang on each side. +// Knob 4: Writhe. How hard they writhe: at -1 the bends are a third as deep and slower, at 0.06 where it opens a little over the middle, at 1 they coil and knot. +// Buttons: K1 tap while the snakes writhe: she looks at you now and the stone wave starts. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, sqrt, sin, cos, hypot, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const lerp = (a, b, t) => a + (b - a) * t; +const ease = (t) => ((t = cl(t, 0, 1)), t * t * (3 - 2 * t)); +const hash = (i) => { + let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; +function hsv(h, s, v) { + 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); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// Her eyes sit at (CX -+ EDX, EY); the gaze runs out from between them. +const CX = 32, EY = 72, EDX = 6.5; +// Snakes: up to MS of them, each cut into stone segments SEG LEDs long. +const SEG = 5, MSEG = 16, MS = 17; +const SKIN = [178, 222, 166]; + +// Half width of the face at height y: a dome over the brow, a long jaw to +// a narrow chin at y = 96. +function faceHW(y) { + if (y < 70) return y < 55 ? 0 : 13.2 * sqrt(max(0, 1 - ((70 - y) / 15) ** 2)); + const t = (y - 70) / 26; + return t >= 1 ? 0 : 13.2 * sqrt(1 - t ** 2.5) * (1 - 0.25 * t); +} + +// The night behind her: distance from the glow over her crown, colour. +const GLOW = [[0, 255, 132, 34], [16, 236, 62, 92], [30, 120, 26, 128], [48, 40, 8, 62], [72, 12, 3, 22]]; +function glow(d) { + let i = 0; + while (i < GLOW.length - 2 && d > GLOW[i + 1][0]) i++; + const A = GLOW[i], B = GLOW[i + 1], u = cl((d - A[0]) / (B[0] - A[0]), 0, 1); + return [lerp(A[1], B[1], u), lerp(A[2], B[2], u), lerp(A[3], B[3], u)]; +} + +// The still layer: the night, her hair, face, neck and robe. fig marks the +// LEDs that turn to stone; cell and edge split them into flakes. +function still(S) { + const { bg, fig, col, stone, cell, edge, cells } = S; + for (let p = 0; p < N; p++) { + const x = p % W, y = floor(p / W), cx = x + 0.5, cy = y + 0.5, o = 3 * p; + const c = glow(hypot((cx - 32) * 1.15, cy - 44)); + bg[o] = c[0]; + bg[o + 1] = c[1]; + bg[o + 2] = c[2]; + let f = 0, r = 0, g = 0, b = 0; + const ax = abs(cx - CX), hw = faceHW(cy), u = (cx - CX) / max(hw, 1e-3); + const neckHW = 6.5 + max(0, cy - 98) * 0.5, shTop = 100 + 11 * max(0, (ax - 7) / 25) ** 1.2; + const hairE = ((cx - CX) / 17.5) ** 2 + ((cy - 66) / 13.5) ** 2; + if (ax < hw) { + // Skin: pale jade, lit from the upper left, the sides falling off. + f = 1; + let k = (0.5 + 0.5 * sqrt(max(0, 1 - u * u))) * (1 - 0.14 * u) * (cy < 70 ? 1.04 : 1); + [r, g, b] = SKIN; + for (const s of [-1, 1]) { + const ex = CX + s * EDX, e = ((cx - ex) / 4.8) ** 2 + ((cy - 71.6) / 3) ** 2; + // Sockets in lavender shadow. + if (e < 1) (k *= 0.6 + 0.4 * e), (r = lerp(150, r, e)), (g = lerp(110, g, e)), (b = lerp(170, b, e)); + // Brows: dark bars rising outward, the inner ends low. + const bx0 = CX + s * 2.4, by0 = 67.4, bx1 = CX + s * 10.6, by1 = 64.4; + const t = cl(((cx - bx0) * (bx1 - bx0) + (cy - by0) * (by1 - by0)) / ((bx1 - bx0) ** 2 + (by1 - by0) ** 2), 0, 1); + if (hypot(cx - lerp(bx0, bx1, t), cy - lerp(by0, by1, t)) < 0.8) (r = 52), (g = 30), (b = 58), (k = 1); + // The hollow under the cheekbone. + if (s * u > 0.5 && s * u < 0.86 && cy > 79 && cy < 87) k *= 0.82; + } + // Nose: a lit ridge, its shadow side, the shade under it, two nostrils. + if (x === 31 && y >= 71 && y <= 80) k *= 1.13; + if ((x === 33 || x === 34) && y >= 74 && y <= 81) k *= 0.74; + if (y === 83 && x >= 29 && x <= 35) k *= 0.72; + if (y === 82 && (x === 30 || x === 34)) (k = 0.45), (r = 120), (g = 80), (b = 110); + // Mouth: dark lips, the line between them and turned-down corners. + if (y === 87 && x >= 28 && x <= 36) (r = 132), (g = 50), (b = 122), (k = 1); + if (y === 88 && x >= 27 && x <= 37) (r = 46), (g = 10), (b = 44), (k = 1); + if (y === 89 && x >= 28 && x <= 36) (r = 176), (g = 78), (b = 160), (k = 1); + if (y === 89 && (x === 27 || x === 37)) (r = 46), (g = 10), (b = 44), (k = 1); + if (y === 90 && x >= 29 && x <= 35) k *= 0.8; + if (y >= 92 && y <= 93 && ax < 3) k *= 1.08; + // The jaw's edge darkens so the face stands off the neck. + if (abs(u) > 0.88 || faceHW(cy + 1.2) < ax) k *= 0.62; + (r *= k), (g *= k), (b *= k); + } else if (hairE < 1 && (cy < 74 || cy < 88 - ax * 0.3)) { + // The hair the snakes grow from: dark violet coils. + f = 1; + const coil = sin(cx * 1.3 + cy * 0.6) + sin(cy * 1.1 - cx * 0.7); + const k = 0.7 + 0.3 * (1 - hairE) + (coil > 1 ? 0.5 : 0); + (r = 34 * k), (g = (coil > 1.3 ? 52 : 12) * k), (b = 46 * k); + } else if (cy >= 89 && cy < shTop + 2 && ax < neckHW) { + // Neck, in the jaw's shadow at the top. + f = 1; + const v = (cx - CX) / neckHW; + let k = (0.45 + 0.4 * sqrt(max(0, 1 - v * v))) * (1 - 0.1 * v); + if (cy < 95) k *= 0.62; + (r = SKIN[0] * k), (g = SKIN[1] * k), (b = SKIN[2] * k); + } else if (cy >= shTop) { + f = 1; + // The robe: violet, its shoulders and collar rimmed, with folds. + let k = 1, rr = 92, gg = 30, bb = 124; + const dy = cy - 104; + for (const fx of [9.5 + dy * 0.35, 18 + dy * 0.55, 26 + dy * 0.75]) { + if (abs(ax - fx) < 0.6) k = 0.48; + else if (abs(ax - fx - 1) < 0.5) k = 1.4; + } + if (cy < shTop + 1.4) (rr = 180), (gg = 86), (bb = 204), (k = 1); + (r = rr * k), (g = gg * k), (b = bb * k); + // A clasp: a toxic green stone at the collar. + if (hypot(cx - CX, cy - 104) < 1.7) (r = 70), (g = 240), (b = 90); + } + fig[p] = f; + col[o] = min(255, r); + col[o + 1] = min(255, g); + col[o + 2] = min(255, b); + grey(stone, o, r, g, b, p); + } + // Flakes: cells round a jittered 6 LED grid; their borders are the cracks. + const G = 6, GW = 11, GH = 22, sx = [], sy = []; + for (let j = 0; j < GH; j++) + for (let i = 0; i < GW; i++) sx.push(i * G + 3 + (hash(j * 97 + i) - 0.5) * 4), sy.push(j * G + 3 + (hash(j * 89 + i + 5000) - 0.5) * 4); + for (let p = 0; p < N; p++) { + if (!fig[p]) continue; + const x = (p % W) + 0.5, y = floor(p / W) + 0.5, gi = floor(x / G), gj = floor(y / G); + let best = 1e9, id = 0; + for (let j = gj - 1; j <= gj + 1; j++) + for (let i = gi - 1; i <= gi + 1; i++) { + if (i < 0 || j < 0 || i >= GW || j >= GH) continue; + const d = (x - sx[j * GW + i]) ** 2 + (y - sy[j * GW + i]) ** 2; + if (d < best) (best = d), (id = j * GW + i); + } + cell[p] = id; + } + for (let p = 0; p < N; p++) { + if (!fig[p]) continue; + edge[p] = (p % W < W - 1 && fig[p + 1] && cell[p + 1] !== cell[p]) || (p + W < N && fig[p + W] && cell[p + W] !== cell[p]) ? 1 : 0; + (cells[cell[p]] || (cells[cell[p]] = [])).push(p); + } +} + +export function setup(params) { + const S = { + T: 0, c0: 0, first: true, hue: -1, palHue: -2, n: 9, amp: 1, R: -1, ph: -9, look: 0, lastT: 0, + bg: new Float32Array(N * 3), col: new Float32Array(N * 3), stone: new Float32Array(N * 3), buf: new Float32Array(N * 3), + fig: new Uint8Array(N), cell: new Int16Array(N), edge: new Uint8Array(N), cells: [], + cellState: new Uint8Array(242), cellCrack: new Float32Array(242), cellFlake: new Float32Array(242), + own: new Int8Array(N), dd: new Float32Array(N), vv: new Float32Array(N), ss: new Float32Array(N), + snakes: [], segStone: new Uint8Array(MS * MSEG), segCrack: new Float32Array(MS * MSEG), segFlake: new Float32Array(MS * MSEG), + parts: [], pal: [], eye: [0, 0, 0], + }; + still(S); + for (let i = 0; i < MS; i++) S.snakes.push({ tau: hash(i * 13 + 7) * 20, rate: 1, frozen: false, pts: new Float32Array(720), np: 0, L: 40, strike: 0, mouth: 0, tongue: 0 }); + params.s = S; +} + +// The story, in seconds at Speed 2 after her eyes start to open: the stone +// wave leaves her eyes at OPEN and takes WAVE to pass every corner, cracks +// run from CRACK - 0.8, flakes fall from FLAKE, her eyes close from BACK and +// at END the snakes writhe again until the next look. +const OPEN = 0.35, WAVE = 1.7, CRACK = 4.1, FLAKE = 4.2, BACK = 6.3, END = 6.6; +const gazeAt = (S) => (S.first ? 3.4 : 6.8); + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 9, 0.06]; + const step = cl(dt || 0, 0, 0.1) * cl(k[1], 0.2, 3) * 0.5; + S.hue = cl(k[0], 0, 1); + S.n = cl(Math.round(k[2]), 9, MS); + const wr = cl(k[3], -1, 1); + S.amp = 1 + 0.65 * wr; + S.T += step; + let g = S.T - S.c0 - gazeAt(S); + if (input.btnPressed && input.btnPressed[0] && g < 0) (S.c0 = S.T - gazeAt(S)), (g = 0); + if (g >= END) { + // A new cycle: the stone is gone and every snake moves. + S.c0 = S.T; + S.first = false; + S.cellState.fill(0); + S.segStone.fill(0); + for (const sn of S.snakes) sn.frozen = false; + g = -gazeAt(S); + } + S.ph = g; + // The wave's radius, from her eyes out past every corner. + S.R = g < OPEN ? -1 : g < WAVE + OPEN ? ((g - OPEN) / WAVE) * 125 : 200; + S.look = g < 0 ? 0 : g < OPEN ? g / OPEN : g < BACK ? 1 : 1 - (g - BACK) / (END - BACK); + for (let i = 0; i < MS; i++) { + const sn = S.snakes[i]; + // A snake stops when the wave reaches its root and moves again once its + // last flake is off. + if (sn.frozen && g > FLAKE) { + let left = 0; + for (let j = 0; j < MSEG; j++) if (S.segStone[i * MSEG + j] === 1) left++; + if (left === 0) sn.frozen = false; + } + sn.rate = sn.frozen ? 0 : min(1, sn.rate + step * 3); + sn.tau += step * sn.rate * (1 + 0.2 * wr); + } +} + +// One snake's centreline: its root on the crown, a heading that bends with +// a wave running from root to head, and the strike that throws the head +// out. The outermost snakes hang down past her cheeks and lift their heads +// beside her shoulders; the rest rise in a fan above her. +function body(S, i) { + const sn = S.snakes[i], n = S.n, r = (q) => hash(i * 31 + q); + const nh = n >= 13 ? 2 : 1, hang = i < nh || i >= n - nh, sd = i < n / 2 ? -1 : 1; + const t = sn.tau, amp = S.amp; + // Strikes every 4 to 7 seconds: out fast, back slower. + const per = 4 + 3 * r(1), sp = ((t + per * r(2)) % per) / 0.5; + const strike = sp < 1 ? (sp < 0.25 ? sp / 0.25 : 1 - ((sp - 0.25) / 0.75) ** 1.5) : 0; + sn.strike = strike; + sn.mouth = sp < 0.7 ? 1 : 0; + const tp = 1.6 + r(3), tf = ((t + tp * r(4)) % tp) / tp; + sn.tongue = tf < 0.12 ? sin((tf / 0.12) * PI * 2) : 0; + let phi, th, kap, kh, L0; + if (hang) { + const o = sd < 0 ? i : n - 1 - i; + phi = sd < 0 ? -PI + 0.12 + 0.3 * o : -0.12 - 0.3 * o; + th = sd < 0 ? PI - 0.8 - 0.25 * o : 0.8 + 0.25 * o; + kap = sd * (0.032 - 0.006 * o); + kh = -sd * 0.13; + L0 = 46 + 5 * r(5) - 6 * o; + } else { + const nr = n - 2 * nh, u = (i - nh + 0.5) / nr; + phi = -PI + 0.5 + u * (PI - 1); + th = lerp(phi, -PI / 2, 0.5); + kap = (u - 0.5) * 0.012; + kh = (u < 0.5 ? -1 : 1) * 0.05; + L0 = 64 - 52 * abs(u - 0.5) ** 1.5 - 5 * r(5); + } + th += 0.2 * sin(t * 0.5 + 9 * r(7)) * amp; + const L = L0 * (0.94 + 0.06 * sin(t * 0.9 + 6 * r(6))) + 12 * strike; + sn.L = L; + const lam = 20 + 6 * r(9), w = 4 + 1.5 * r(10), A = 1.05 * amp * (1 - 0.75 * strike); + let x = CX + 15.5 * cos(phi), y = 66 + 11 * sin(phi), m = 0; + const P = sn.pts; + for (let s = 0; s <= L && m < P.length; s += 0.5) { + const hth = th + A * (0.35 + (0.65 * s) / L) * sin((TAU * s) / lam - w * t + 7 * r(12)); + P[m++] = x; + P[m++] = y; + P[m++] = hth; + P[m++] = s; + th += 0.5 * (s > L - 12 ? kh * (1 - strike) : kap); + x += 0.5 * cos(hth); + y += 0.5 * sin(hth); + } + sn.np = m >> 2; +} + +// Radius of a snake at s: a taper to the neck, then the wide flat head. +function radius(s, L) { + const hs = (s - (L - 7)) / 7; + if (hs <= 0) return 2.5 - 1.15 * (s / (L - 7)); + return hs < 0.4 ? 1.35 + 1.25 * (hs / 0.4) : 2.6 - 1.6 * ((hs - 0.4) / 0.6) ** 1.5; +} + +// Stone grey for a lit colour, speckled per LED. +function grey(buf, o, r, g, b, p) { + const l = (0.35 * r + 0.5 * g + 0.15 * b) / 255, sp = (hash(p * 3 + 1) - 0.5) * 22; + buf[o] = 50 + 160 * l + sp; + buf[o + 1] = 48 + 156 * l + sp; + buf[o + 2] = 58 + 160 * l + sp; +} + +function put(buf, x, y, c) { + x = floor(x); + y = floor(y); + if (x >= 0 && x < W && y >= 0 && y < H) (buf[3 * (y * W + x)] = c[0]), (buf[3 * (y * W + x) + 1] = c[1]), (buf[3 * (y * W + x) + 2] = c[2]); +} + +const TONGUE = [255, 50, 40], CRACKC = [30, 28, 36]; + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { bg, col, stone, buf, fig, cell, edge, own, dd, vv, ss, snakes, segStone, segCrack, segFlake, cellState, cellCrack, cellFlake, cells, parts } = S; + const g = S.ph, R = S.R, n = S.n; + if (S.palHue !== S.hue) { + // Two kinds of snake: toxic green with a black zigzag, and violet with + // an orange one. + S.pal = []; + for (let i = 0; i < MS; i++) S.pal.push(i % 3 === 1 ? [hsv(S.hue + 0.78, 0.62, 235), hsv(0.07, 0.86, 255)] : [hsv(S.hue + 0.33, 0.76, 240), [10, 18, 12]]); + S.eye = hsv(S.hue + 0.36, 0.62, 255); + S.palHue = S.hue; + } + buf.set(bg); + own.fill(-1); + // Snakes: the middle ones first, so the outer ones lie over them. + const order = []; + for (let i = 0; i < n; i++) order.push(i); + order.sort((a, b) => abs(a - (n - 1) / 2) - abs(b - (n - 1) / 2)); + for (const i of order) { + body(S, i); + const sn = snakes[i], P = sn.pts, L = sn.L; + for (let q = 0; q < sn.np; q++) { + const px = P[q * 4], py = P[q * 4 + 1], th = P[q * 4 + 2], s = P[q * 4 + 3], rad = radius(s, L); + // The stone wave marks the segments it has passed. + if (R > 0 && hypot(px - CX, py - EY) < R) { + const j = min(MSEG - 1, floor(s / SEG)), z = i * MSEG + j; + if (!segStone[z]) (segStone[z] = 1), (segCrack[z] = CRACK - 0.8 + 0.8 * hash(z)), (segFlake[z] = FLAKE + 1.6 * (0.45 * hash(z + 99) + 0.55 * (1 - j / MSEG))); + if (q === 0) sn.frozen = true; + } + const tx = cos(th), ty = sin(th); + for (let yy = floor(py - rad); yy <= floor(py + rad); yy++) + for (let xx = floor(px - rad); xx <= floor(px + rad); xx++) { + if (xx < 0 || xx >= W || yy < 0 || yy >= H) continue; + const dx = xx + 0.5 - px, dy = yy + 0.5 - py, d = hypot(dx, dy) / rad; + if (d > 1) continue; + const p = yy * W + xx; + if (own[p] !== i || d < dd[p]) (own[p] = i), (dd[p] = d), (vv[p] = (tx * dy - ty * dx) / rad), (ss[p] = s); + } + } + } + // Shade them: round bodies with a dark rim, a zigzag down the back. + for (let p = 0; p < N; p++) { + const i = own[p]; + if (i < 0) continue; + const sn = snakes[i], s = ss[p], v = cl(vv[p], -1, 1), L = sn.L, o = 3 * p, head = s > L - 7; + const zz = 0.55 * (abs((((s + i * 2.3) / 3.2) % 2) - 1) * 2 - 1); + const c = !head && abs(v - zz) < 0.3 ? S.pal[i][1] : S.pal[i][0]; + let k = (0.42 + 0.58 * sqrt(1 - v * v)) * (1 - 0.18 * v); + if (abs(v) > 0.72) k *= 0.38; + if (head) k *= abs(v) < 0.5 ? 0.95 : 0.8; + let r = c[0] * k, gg = c[1] * k, b = c[2] * k; + if (head && sn.mouth && sn.strike > 0.15 && s > L - 3 && abs(v) < 0.5) (r = 120), (gg = 0), (b = 36); + const z = i * MSEG + min(MSEG - 1, floor(s / SEG)); + if (segStone[z] === 1) { + grey(buf, o, r, gg, b, p); + if (g > segCrack[z] && (s % SEG < 0.55 || abs(v - 0.3 * sin(s * 2 + z)) < 0.12)) buf.set(CRACKC, o); + } else (buf[o] = r), (buf[o + 1] = gg), (buf[o + 2] = b); + } + // Their eyes and tongues. + for (let i = 0; i < n; i++) { + const sn = snakes[i], P = sn.pts, q = sn.np - 1; + if (q < 8) continue; + const st = segStone[i * MSEG + min(MSEG - 1, floor(P[q * 4 + 3] / SEG))] === 1; + const hx = P[(q - 6) * 4], hy = P[(q - 6) * 4 + 1], th = P[(q - 6) * 4 + 2], nx = -sin(th), ny = cos(th); + for (const sd of [-1, 1]) { + const p = floor(hy + ny * sd * 1.4) * W + floor(hx + nx * sd * 1.4); + if (p >= 0 && p < N && own[p] === i) buf.set(st ? [70, 68, 76] : i % 3 === 1 ? [140, 255, 70] : [255, 196, 30], 3 * p); + } + if (!st && sn.tongue > 0 && !sn.frozen) { + const a = P[q * 4 + 2], tl = 1 + 3.2 * sn.tongue, tx = P[q * 4] + cos(a) * 0.5, ty = P[q * 4 + 1] + sin(a) * 0.5; + for (let u = 0.5; u <= tl; u += 0.5) put(buf, tx + cos(a) * u, ty + sin(a) * u, TONGUE); + for (const sd of [-1, 1]) put(buf, tx + cos(a) * tl + cos(a + sd * 0.9), ty + sin(a) * tl + sin(a + sd * 0.9), TONGUE); + } + } + // Her head and robe, and the stone over them. + for (let p = 0; p < N; p++) { + if (!fig[p]) continue; + const o = 3 * p, c = cell[p], dist = hypot((p % W) + 0.5 - CX, floor(p / W) + 0.5 - EY); + if (cellState[c] === 0 && R > 0 && dist < R) (cellState[c] = 1), (cellCrack[c] = CRACK - 0.8 + 0.8 * hash(c + 7)), (cellFlake[c] = FLAKE + 1.8 * (0.5 * hash(c + 77) + 0.5 * (1 - floor(p / W) / H))); + const src = cellState[c] === 1 && dist < R ? (edge[p] && g > cellCrack[c] ? null : stone) : col; + if (src) (buf[o] = src[o]), (buf[o + 1] = src[o + 1]), (buf[o + 2] = src[o + 2]); + else buf.set(CRACKC, o); + } + // Her eyes: shut lids, or open and glowing, or stone. + const look = ease(S.look), e = S.eye; + for (const sd of [-1, 1]) { + const ex = CX + sd * EDX; + const stoneEye = cellState[cell[EY * W + floor(ex)]] === 1 && g > OPEN + 0.25; + for (let y = 68; y <= 76; y++) + for (let x = floor(ex - 5); x <= floor(ex + 5); x++) { + const o = 3 * (y * W + x), dx = (x + 0.5 - ex) / 3.7, dy = y + 0.5 - 72; + const hh = 1.7 * look * sqrt(max(0, 1 - dx * dx)); + if (abs(dx) <= 1 && look < 0.15 && y === floor(72.6 + 0.9 * (1 - dx * dx))) buf.set([40, 26, 46], o); + else if (abs(dx) <= 1 && abs(dy) <= hh) { + const pupil = abs(x + 0.5 - ex) < 0.6; + buf.set(stoneEye ? (pupil ? [38, 36, 44] : [168, 166, 176]) : pupil ? [8, 30, 10] : e, o); + } else if (!stoneEye && look > 0) { + // A green glow round the open eye. + const gl = look * 0.5 * max(0, 1 - hypot((x + 0.5 - ex) / 6, (y + 0.5 - 72) / 3.5)); + (buf[o] += e[0] * gl), (buf[o + 1] += e[1] * gl), (buf[o + 2] += e[2] * gl); + } + } + } + // The wave itself: a ring of the eyes' green running out. + if (R > 0 && R < 130) + for (let p = 0; p < N; p++) { + const a = 0.7 * (1 - abs(hypot((p % W) + 0.5 - CX, floor(p / W) + 0.5 - EY) - R) / 3.2); + if (a > 0) (buf[3 * p] += e[0] * a), (buf[3 * p + 1] += e[1] * a), (buf[3 * p + 2] += e[2] * a); + } + // Flakes whose time has come break off and fall. + if (g > FLAKE) { + for (let c = 0; c < cells.length; c++) { + if (cellState[c] !== 1 || g < cellFlake[c] || !cells[c]) continue; + cellState[c] = 2; + spawn(S, cells[c], c); + } + for (let i = 0; i < n; i++) + for (let j = 0; j < MSEG; j++) { + const z = i * MSEG + j; + if (segStone[z] !== 1 || g < segFlake[z]) continue; + segStone[z] = 2; + const px = []; + for (let p = 0; p < N; p++) if (own[p] === i && min(MSEG - 1, floor(ss[p] / SEG)) === j) px.push(p); + spawn(S, px, z + 500); + } + } + const dt = max(0, S.T - S.lastT); + S.lastT = S.T; + for (let a = parts.length - 1; a >= 0; a--) { + const f = parts[a]; + f.vy += 90 * dt; + f.x += f.vx * dt; + f.y += f.vy * dt; + if (f.y > H + 8) { + parts.splice(a, 1); + continue; + } + for (let q = 0; q < f.dx.length; q++) { + const x = floor(f.x + f.dx[q]), y = floor(f.y + f.dy[q]); + if (x >= 0 && x < W && y >= 0 && y < H) { + const o = 3 * (y * W + x); + (buf[o] = f.c[3 * q]), (buf[o + 1] = f.c[3 * q + 1]), (buf[o + 2] = f.c[3 * q + 2]); + } + } + } + for (let p = 0, o = 0; p < N; p++, o += 3) display.setPixel(p % W, (p / W) | 0, min(255, max(0, buf[o])), min(255, max(0, buf[o + 1])), min(255, max(0, buf[o + 2]))); +} + +// A flake: the stone LEDs it covers, as they are now, falling away. +function spawn(S, px, seed) { + if (!px.length) return; + let mx = 0, my = 0; + for (const p of px) (mx += p % W), (my += floor(p / W)); + mx /= px.length; + my /= px.length; + const f = { x: mx, y: my, vx: (mx - 32) * 0.4 + (hash(seed + 3) - 0.5) * 10, vy: -6 - 10 * hash(seed + 5), dx: [], dy: [], c: [] }; + for (const p of px) f.dx.push((p % W) - mx), f.dy.push(floor(p / W) - my), f.c.push(S.buf[3 * p], S.buf[3 * p + 1], S.buf[3 * p + 2]); + S.parts.push(f); +} From 0e7a67a210d2bb9209d3aa33abe778b59721a187 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 01:49:45 +0000 Subject: [PATCH 04/14] =?UTF-8?q?53=ED=8C=90:=20Squared=20Rectangle=20?= =?UTF-8?q?=EA=B3=BC=20Haunted=20Carousel=20=ED=8C=A8=ED=84=B4=EC=9D=84=20?= =?UTF-8?q?=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Yb8D9UxrifXqrXju5CVbqY --- .../patterns/haunted-carousel.js | 340 ++++++++++++++++++ .../patterns/squared-rectangle.js | 317 ++++++++++++++++ 2 files changed, 657 insertions(+) create mode 100644 incoming/pattern-study/patterns/haunted-carousel.js create mode 100644 incoming/pattern-study/patterns/squared-rectangle.js diff --git a/incoming/pattern-study/patterns/haunted-carousel.js b/incoming/pattern-study/patterns/haunted-carousel.js new file mode 100644 index 000000000..8532519df --- /dev/null +++ b/incoming/pattern-study/patterns/haunted-carousel.js @@ -0,0 +1,340 @@ +// ===== Patternflow pattern ===== +// Title: Haunted Carousel +// Family: halloween +// Idea: A merry-go-round turns alone in an empty fairground at night, its striped canopy and ring of orange bulbs wheeling overhead while skeleton horses on brass poles rise and fall past the mirrored centre column, until now and then it lurches faster, the bulbs go toxic green and ghostly riders sit on the horses and stare out at us, then it slows and they fade. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 53, after a carousel in an empty fairground at night +// Round: 53 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Horses=5..8, Fog=-1..1 +// +// Knob 1: Hue. The night in the sky, the fog, the dark canopy stripes and the platform's panels: purple at 0, deep blue at 0.3, sea blue at 0.6, green at 1. The tops of the bright stripes go from pink at 0 to cyan at 1. The bulbs stay orange, green in a lurch, and the bones stay white. +// Knob 2: Speed. At 2 the carousel turns once in 15 seconds and each horse rises and falls three times a turn; the first lurch comes at 4 seconds and one every 17 after, two and a half times as fast for 3 seconds before it slows over 3 more. At 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Horses. How many horses ride the ring, 5 to 8, spaced evenly; at 8 they run nose to tail. +// Knob 4: Fog. Ground fog drifting round the platform: none at -1, a low bank over the ground at the opening 0.06, and at 1 a deep bank up to the horses' hooves. +// Buttons: K1 tap starts a lurch now. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, sqrt, sin, cos, exp, atan2, round, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (u) => ((u = cl(u, 0, 1)), u * u * (3 - 2 * u)); +const fr = (v) => v - floor(v); +const mix = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]; +const hash = (i) => { + let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; +function hsv(h, s, v) { + h = fr(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); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// The camera stands D0 from the carousel's axis at the height of the horses' +// backs, so a point (x, h, z) with z toward us lands at x 32 + F x / (D0 - z), +// y 64 - F h / (D0 - z). Lengths are in canopy radii. +const F = 150, D0 = 5; +const VB = 0.8, VT = 1, HA = 1.85, RC = 0.19, RP = 0.78, RF = 1.05, PT = -0.95, PG = -1.07; +// Surfaces: sky, cone, valance, ceiling, valance seen from inside, finial, +// column, platform top, platform side, ground. +const SKY = 0, CONE = 1, VAL = 2, CEIL = 3, VIN = 4, FIN = 5, COL = 6, TOPS = 7, SIDE = 8, GND = 9; +const proj = (x, h, z) => [32 + (F * x) / (D0 - z), 64 - (F * h) / (D0 - z), D0 - z]; + +// Every LED's first fixed surface along its view line, found once. +const KIND = new Uint8Array(N), DEP = new Float32Array(N).fill(1e9), ANG = new Float32Array(N), AUX = new Float32Array(N); +for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const a = (x + 0.5 - 32) / F, b = (64 - y - 0.5) / F, A2 = a * a + 1; + let best = 1e9, k = SKY, aux = 0; + const hit = (t, kk, v) => t > 0 && t < best && ((best = t), (k = kk), (aux = v)); + const cyl = (R) => { + const d = D0 * D0 - A2 * (D0 * D0 - R * R); + return d < 0 ? null : [(D0 - sqrt(d)) / A2, (D0 + sqrt(d)) / A2]; + }; + let c = cyl(1); + if (c) { + if (b * c[0] >= VB && b * c[0] <= VT) hit(c[0], VAL, b * c[0]); + if (b * c[1] >= VB && b * c[1] <= VT) hit(c[1], VIN, b * c[1]); + } + if (b > 0) { + const t = VT / b, r = Math.hypot(a * t, D0 - t); + if (r <= 1) hit(t, CEIL, r); + } + // The cone: radius (HA - h) / (HA - VT). + const kk = (HA - VT) ** 2, qa = A2 - (b * b) / kk, qb = -2 * D0 + (2 * HA * b) / kk, qc = D0 * D0 - (HA * HA) / kk; + const disc = qb * qb - 4 * qa * qc; + if (disc >= 0 && abs(qa) > 1e-9) + for (const t of [(-qb - sqrt(disc)) / (2 * qa), (-qb + sqrt(disc)) / (2 * qa)]) if (b * t >= VT && b * t <= HA) hit(t, CONE, b * t); + const hf = b * D0; + if (hf > HA - 0.05 && hf < 2.08 && abs(a * D0) < (hf > 1.98 ? 0.045 : 0.022)) hit(D0, FIN, hf); + c = cyl(RC); + if (c && b * c[0] >= PT && b * c[0] <= VT) hit(c[0], COL, b * c[0]); + if (b < 0) { + const t = PT / b; + if (Math.hypot(a * t, D0 - t) <= RF) hit(t, TOPS, Math.hypot(a * t, D0 - t)); + } + c = cyl(RF); + if (c && b * c[0] >= PG && b * c[0] <= PT) hit(c[0], SIDE, b * c[0]); + if (b < 0) hit(PG / b, GND, Math.hypot((a * PG) / b, D0 - PG / b)); + KIND[p] = k; + DEP[p] = best; + ANG[p] = atan2(a * best, D0 - best); + AUX[p] = aux; + } + +// A skeleton horse in its own frame: forward f, up u, out s, as bones +// [f, u, s, f, u, s, r, r]. Legs are added each frame as they gallop. +const SC = 1.45; +const BONES = [ + [-0.2, 0.03, 0, 0.12, 0.06, 0, 0.024, 0.022], + [0.12, 0.06, 0, 0.2, 0.19, 0, 0.028, 0.022], + [0.19, 0.22, 0, 0.33, 0.12, 0, 0.055, 0.03], + [0.19, 0.24, 0.02, 0.17, 0.31, 0.02, 0.016, 0.008], + [-0.17, 0.04, 0, -0.25, -0.05, 0, 0.032, 0.02], + [-0.07, -0.1, 0, 0.09, -0.09, 0, 0.012, 0.012], + [-0.25, 0.02, 0, -0.32, -0.03, 0, 0.013, 0.011], + [-0.32, -0.03, 0, -0.35, -0.15, 0, 0.011, 0.008], +]; +for (const f of [-0.08, -0.03, 0.02, 0.07]) for (const s of [-0.055, 0.055]) BONES.push([f, 0.04, s, f + 0.015, -0.09, s * 1.1, 0.012, 0.01]); +// A rider, sitting astride with both hands on the pole. +const RIDER = [ + [-0.02, 0.09, 0, 0.02, 0.33, 0, 0.075, 0.06], + [0.03, 0.3, 0.05, 0.01, 0.24, 0.02, 0.026, 0.02], + [0.03, 0.3, -0.05, 0.01, 0.24, -0.02, 0.026, 0.02], + [-0.02, 0.1, 0.06, 0.07, -0.07, 0.08, 0.036, 0.024], + [-0.02, 0.1, -0.06, 0.07, -0.07, -0.08, 0.036, 0.024], + [-0.02, 0.22, 0, -0.17, 0.27, 0, 0.045, 0.01], +]; + +export function setup(params) { + params.s = { + t: 0, th: 0.35, lurch: -99, next: 4, L: 0, flash: 0, hue: -1, speed: 2, horses: 5, fog: 0.06, + buf: new Float32Array(N * 3), zb: new Float32Array(N), bg: new Float32Array(N * 3), ga: new Float32Array(N), ge: new Uint8Array(N), + }; +} + +// The sky and the far fairground for a hue: a night deepening upward over a +// glow at the horizon, a moon, stars, and the black shapes of a big wheel +// and a tent that show between the horses. +function sky(S) { + const h = 0.76 - 0.36 * S.hue, bg = S.bg; + S.night = hsv(h, 0.75, 0.42); + S.fogc = hsv(h + 0.04, 0.3, 0.62); + S.dark = hsv(h, 0.8, 0.17); + S.top = hsv(h + 0.12, 0.88, 0.75); + S.panel = hsv(h + 0.02, 0.82, 0.36); + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const u = cl(y / 64, 0, 1), v = round((0.1 + 0.45 * u * u) * 24) / 24; + let c = hsv(h + 0.06 * u, 0.8 - 0.2 * u, v); + const dx = x + 0.5 - 11, dy = y + 0.5 - 13, dm = sqrt(dx * dx + dy * dy); + if (dm < 7.5) { + const cr = Math.hypot(dx + 2, dy - 1.5) < 2.2 || Math.hypot(dx - 2.5, dy + 2.5) < 1.4; + c = cr ? [0.78, 0.76, 0.66] : [0.98, 0.95, 0.8]; + } else if (dm < 10.5) c = mix(c, [0.8, 0.75, 0.9], dm < 9 ? 0.25 : 0.12); + else if (y < 50 && hash(p * 7 + 3) < 0.012) c = [0.75, 0.75, 0.85]; + // The big wheel on the right and a tent on the left, black. + const wx = x + 0.5 - 54, wy = y + 0.5 - 42, wr = Math.hypot(wx, wy), wa = atan2(wy, wx); + const wheel = (abs(wr - 17) < 0.7 || (wr < 17 && abs(fr((wa * 8) / TAU + 0.5) - 0.5) * wr < 0.45)) && y < 64; + const legs = y > 42 && y < 64 && abs(abs(x + 0.5 - 54) - (y - 42) * 0.35) < 0.6; + const tent = y > 45 && y < 64 && abs(x + 0.5 - 6) < (y - 45) * 0.75 + 0.5; + if (wheel || legs || tent) c = [0.03, 0.01, 0.05]; + bg.set(c, 3 * p); + } +} + +// Paint a tapered capsule between two projected points, keeping the nearest. +function capsule(S, A, B, ra, rb, rim, col, ghost) { + const { buf, zb, ga } = S; + const pa = (ra * F) / A[2], pb = (rb * F) / B[2], ex = B[0] - A[0], ey = B[1] - A[1], e2 = ex * ex + ey * ey || 1e-6, pad = max(pa, pb) + rim + 1; + for (let y = max(0, floor(min(A[1], B[1]) - pad)); y <= min(H - 1, floor(max(A[1], B[1]) + pad)); y++) + for (let x = max(0, floor(min(A[0], B[0]) - pad)); x <= min(W - 1, floor(max(A[0], B[0]) + pad)); x++) { + const qx = x + 0.5 - A[0], qy = y + 0.5 - A[1], u = cl((qx * ex + qy * ey) / e2, 0, 1), dx = qx - u * ex, dy = qy - u * ey; + const d = sqrt(dx * dx + dy * dy), pr = max(0.55, pa + (pb - pa) * u) + rim, p = y * W + x; + if (d >= pr) continue; + const w = ra + (rb - ra) * u, z = A[2] + (B[2] - A[2]) * u - (rim ? -0.04 : w * sqrt(1 - (d / pr) ** 2)); + if (z >= zb[p]) continue; + if (ghost) { + ga[p] = max(ga[p], ghost * (1 - 0.45 * (d / pr) ** 2)); + continue; + } + zb[p] = z; + const l = rim ? 1 : 0.78 + 0.22 * (1 - d / pr); + buf[3 * p] = col[0] * l; + buf[3 * p + 1] = col[1] * l; + buf[3 * p + 2] = col[2] * l; + } +} + +// One bulb: a hot core and a small halo, if nothing nearer hides it. +function bulb(S, P, col, b) { + const x = floor(P[0]), y = floor(P[1]), p = y * W + x; + if (x < 0 || x >= W || y < 0 || y >= H || P[2] > S.zb[p] + 0.03) return; + const { buf } = S; + for (let j = -2; j <= 2; j++) + for (let i = -2; i <= 2; i++) { + const xx = x + i, yy = y + j, d = i * i + j * j; + if (xx < 0 || xx >= W || yy < 0 || yy >= H || d > 5) continue; + const q = 3 * (yy * W + xx), g = d === 0 ? 1.2 * b : (d < 2 ? 0.32 : d < 3 ? 0.14 : 0.06) * b; + buf[q] += col[0] * g; + buf[q + 1] += col[1] * g; + buf[q + 2] += col[2] * g; + } + if (b > 0.5) buf.set(mix(col, [1, 1, 1], 0.5), 3 * p); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 5, 0.06], d = cl(dt || 0, 0, 0.1); + const hue = cl(k[0], 0, 1); + if (hue !== S.hue) (S.hue = hue), sky(S); + S.speed = cl(k[1], 0.2, 3); + S.horses = round(cl(k[2], 5, 8)); + S.fog = cl(k[3], -1, 1); + const ds = (d * S.speed) / 2; + S.t += ds; + if ((input.btnPressed && input.btnPressed[0] && S.t - S.lurch > 3.2) || S.t >= S.next) (S.lurch = S.t), (S.next = S.t + 17); + // The lurch: a jolt up to speed in 0.35 s, three seconds of green, three to slow. + const lt = S.t - S.lurch; + S.L = lt < 0.35 ? sm(lt / 0.35) : lt < 3.2 ? 1 : 1 - sm((lt - 3.2) / 3); + S.flash = lt < 0.4 ? 1 - lt / 0.4 : 0; + S.th += ds * 0.42 * (1 + 1.5 * S.L); +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.night) return; + const { buf, zb, bg, ga, ge, th, L } = S; + const bc = mix([1, 0.55, 0.12], [0.35, 1, 0.25], L), glowc = mix(bc, [1, 1, 1], S.flash * 0.6); + const night = S.night, dark = S.dark, t = S.t; + zb.set(DEP); + for (let p = 0; p < N; p++) { + const kd = KIND[p], a = ANG[p] - th, h = AUX[p], q = 3 * p; + let c; + if (kd === SKY) { + buf[q] = bg[q]; + buf[q + 1] = bg[q + 1]; + buf[q + 2] = bg[q + 2]; + continue; + } else if (kd === CONE) { + // Eighteen stripes, the bright ones lit by the bulbs below and going + // from the bulb colour at the rim to magenta and the night at the top. + const up = (h - VT) / (HA - VT), face = 0.7 + 0.3 * cos(ANG[p]); + c = floor(fr(a / TAU) * 18) % 2 ? mix(mix(bc, S.top, sm(up * 1.6)), night, sm(up * 1.3 - 0.3)) : mix(dark, [0.05, 0.01, 0.06], up); + c = c.map((v) => v * face); + } else if (kd === VAL) { + const tooth = 0.07 * (1 - abs(2 * fr((a * 36) / TAU) - 1)); + c = h > 0.975 || h < 0.812 ? [0.95, 0.7, 0.25] : h < VB + tooth ? mix(bc, [0.8, 0.2, 0.5], 0.3) : [0.12, 0.03, 0.15]; + } else if (kd === CEIL) c = fr((a * 12) / TAU) < 0.1 ? [0.55, 0.38, 0.12] : mix([0.2, 0.05, 0.18], bc, 0.25 * h); + else if (kd === VIN) c = h > 0.98 || h < 0.82 ? [0.45, 0.3, 0.1] : [0.07, 0.02, 0.09]; + else if (kd === FIN) c = [0.95, 0.75, 0.3]; + else if (kd === COL) { + // Eight panels round the column, mirrors between gilt frames; a mirror + // catches the bulbs as it turns toward us. + const f = fr((a * 8) / TAU), face = cos(ANG[p]); + c = h > 0.86 || h < -0.86 || f < 0.12 ? [0.85, 0.62, 0.2] : floor((a * 8) / TAU) % 2 ? mix([0.3, 0.26, 0.45], bc, 0.6 * max(0, face) ** 3 + 0.1) : [0.08, 0.12, 0.1]; + c = c.map((v) => v * (0.55 + 0.45 * max(0, face))); + } else if (kd === TOPS) c = mix(floor((a * 12) / TAU) % 2 ? [0.2, 0.14, 0.24] : [0.13, 0.08, 0.17], bc, 0.1 + 0.15 * h); + else if (kd === SIDE) { + const f = fr((a * 16) / TAU); + c = h > PT - 0.015 || h < PG + 0.015 ? [0.85, 0.6, 0.2] : f < 0.5 ? S.panel : [0.12, 0.03, 0.14]; + } else { + // Ground: dark earth, warm near the platform from the bulbs. + const g = round(6 * exp(-(h - RF) * 2.4)) / 6, far = cl((h - 4) / 20, 0, 1); + c = mix(mix([0.06, 0.04, 0.08], bc, 0.3 * g), bg.slice(q, q + 3), far); + } + buf[q] = c[0]; + buf[q + 1] = c[1]; + buf[q + 2] = c[2]; + } + // Poles, horses and riders, ring order does not matter with the depths. + const n = S.horses, legs = [], ghost = sm(L * 1.6 - 0.4); + ga.fill(0); + ge.fill(0); + for (let i = 0; i < n; i++) { + const ph = th + (TAU * i) / n, sp = sin(ph), cp = cos(ph), bob = 3 * th + i * PI; + const hb = -0.33 + 0.09 * sin(bob), sw = 0.035 * sin(bob + 0.8); + const at = (f, u, s) => proj(RP * sp + SC * (f * cp + s * sp), hb + SC * u, RP * cp + SC * (-f * sp + s * cp)); + // Brass pole, twisted: bright bands climb it as it turns. + const P0 = proj(RP * sp, PT, RP * cp), P1 = proj(RP * sp, VT, RP * cp), hw = max(0.5, (0.028 * F) / P0[2]), dim = 1 - (0.42 * (P0[2] - 4.2)) / 1.56; + for (let y = max(0, floor(P1[1])); y <= min(H - 1, floor(P0[1])); y++) + for (let x = max(0, round(P0[0] - hw)); x < min(W, round(P0[0] + hw)); x++) { + const p = y * W + x; + if (P0[2] >= zb[p]) continue; + zb[p] = P0[2]; + const l = dim * (0.72 + 0.28 * sin(y * 1.1 + th * 9 + x)); + buf.set([0.98 * l, 0.74 * l, 0.28 * l], 3 * p); + } + // The skeleton: a dark rim round every bone first, then the bones. + legs.length = 0; + for (const s of [-0.045, 0.045]) { + const g = s > 0 ? sw : -sw; + legs.push([0.1, -0.03, s, 0.18 + g, -0.13, s, 0.019, 0.017], [0.18 + g, -0.13, s, 0.13 + 1.5 * g, -0.24, s, 0.017, 0.015]); + legs.push([-0.19, -0.02, s, -0.25 - g, -0.14, s, 0.021, 0.017], [-0.25 - g, -0.14, s, -0.35 - 1.5 * g, -0.22, s, 0.017, 0.015]); + } + const bone = mix(mix([0.95, 0.92, 0.8], [0.55, 1, 0.5], 0.25 * L), night, 0.5 * (1 - dim)); + for (const rim of [0.9, 0]) + for (const B of [...BONES, ...legs]) capsule(S, at(B[0], B[1], B[2]), at(B[3], B[4], B[5]), SC * B[6], SC * B[7], rim, rim ? [0.02, 0, 0.03] : bone.map((v) => v * dim), 0); + // Hollow eye sockets, wherever the skull faces us. + for (const s of [-0.045, 0.045]) { + const E = at(0.235, 0.2, s), r = max(0.75, (0.026 * SC * F) / E[2]); + for (let y = floor(E[1] - r); y <= E[1] + r; y++) + for (let x = floor(E[0] - r); x <= E[0] + r; x++) { + const p = y * W + x; + if (x < 0 || x >= W || y < 0 || y >= H || Math.hypot(x + 0.5 - E[0], y + 0.5 - E[1]) > r || E[2] - 0.07 > zb[p]) continue; + buf.set([0.02, 0, 0.03], 3 * p); + } + } + if (ghost > 0.01) { + for (const B of RIDER) capsule(S, at(B[0], B[1], B[2]), at(B[3], B[4], B[5]), SC * B[6], SC * B[7], 0, null, ghost * dim); + const Hc = at(0.04, 0.44, 0), hr = (0.085 * SC * F) / Hc[2]; + capsule(S, Hc, Hc, 0.085 * SC, 0.085 * SC, 0, null, ghost * dim); + // The rider turns its face to us: two dark eyes. + for (const dx of [-0.42, 0.42]) { + const x = floor(Hc[0] + dx * hr); + for (let y = floor(Hc[1] - 0.35 * hr); y <= Hc[1] + 0.25 * hr; y++) if (x >= 0 && x < W && y >= 0 && y < H && Hc[2] < zb[y * W + x] + 0.1) ge[y * W + x] = 1; + } + } + } + // Bulbs: round the valance, under the canopy, on the column and round the + // platform's edge, each flickering on its own. + const lamp = (j, P) => { + let b = 0.82 + 0.18 * sin(t * (6 + (j % 5)) + j * 1.7); + if (hash(j * 131 + floor(t * 2.5)) < 0.05) b = 0.25; + b = min(1.4, b * (1 + S.flash)); + bulb(S, P, glowc, b); + }; + for (let j = 0; j < 28; j++) { + const a = th + (TAU * j) / 28; + lamp(j, proj(1.005 * sin(a), 0.9, 1.005 * cos(a))); + if (j < 16) lamp(j + 40, proj(0.62 * sin(a * 1.75), 0.995, 0.62 * cos(a * 1.75))); + if (j < 24) lamp(j + 80, proj(1.055 * sin(a * 1.1667), -1.01, 1.055 * cos(a * 1.1667))); + if (j < 14) lamp(j + 120, proj(0.2 * sin(a * 2), j % 2 ? 0.62 : -0.62, 0.2 * cos(a * 2))); + } + // Riders: pale green and half see-through, with dark eyes. + if (ghost > 0.01) + for (let p = 0; p < N; p++) { + const g = ga[p], q = 3 * p; + if (!g) continue; + const c = ge[p] ? [0, 0.06, 0.02] : [0.62, 1, 0.66]; + for (let i = 0; i < 3; i++) buf[q + i] = buf[q + i] * (1 - 0.75 * g) + c[i] * 0.75 * g; + } + // Ground fog, its top edge rolling sideways and wisps drifting through it. + const u = (S.fog + 1) / 2, top = 129 - 58 * u, dens = 0.7 * sm(u * 2.2), fc = mix(S.fogc, bc, 0.12); + for (let x = 0; x < W; x++) { + const yt = top + 3 * sin(x * 0.16 + t * 0.7) + 2 * sin(x * 0.37 - t * 1.1); + for (let y = max(0, floor(yt)); y < H; y++) { + const wisp = 0.62 + 0.38 * sin(x * 0.21 + y * 0.43 - t * 1.3) * sin(x * 0.13 - y * 0.19 + t * 0.8); + const a = round(dens * sm((y - yt) / 16) * wisp * 8) / 8, q = 3 * (y * W + x); + if (!a) continue; + for (let i = 0; i < 3; i++) buf[q + i] = buf[q + i] * (1 - a) + fc[i] * a; + } + } + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) display.setPixel(x, y, round(cl(buf[3 * p], 0, 1) * 255), round(cl(buf[3 * p + 1], 0, 1) * 255), round(cl(buf[3 * p + 2], 0, 1) * 255)); +} diff --git a/incoming/pattern-study/patterns/squared-rectangle.js b/incoming/pattern-study/patterns/squared-rectangle.js new file mode 100644 index 000000000..5c1f5934d --- /dev/null +++ b/incoming/pattern-study/patterns/squared-rectangle.js @@ -0,0 +1,317 @@ +// ===== Patternflow pattern ===== +// Title: Squared Rectangle +// Family: cells +// Idea: A network of resistors between a top and a bottom pole is drawn the way Brooks, Smith, Stone and Tutte drew it, one tile per wire with the voltage drop for its height and the current for its width, so the tiles always fill one tall rectangle exactly while the conductances wander and stretch them, and every few seconds the conductances all come back to 1 and the tiling snaps into squares, with a new network now and then giving a new squared rectangle. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: squared rectangles from electrical networks (R. L. Brooks, C. A. B. Smith, A. H. Stone and W. T. Tutte, 1940), after Zbigniew Moroń's first squared rectangle (1925), pattern round 53 +// Round: 53 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Tiles=13..20, Wander=-1..1 +// +// Knob 1: Hue. Turns the palette round the wheel. A tile's colour comes from its size against the biggest and smallest tiles shown: at 0 the biggest is teal, the middling ones blue, violet and pink, and the smallest orange. +// Knob 2: Speed. At 2 a cycle takes 8 seconds: the squares hold for 1.2, the conductances drift for 5.6, and the tiling snaps back into squares at the end of the eighth second with a flash on every seam. Every second cycle the network is rewired; at 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Tiles. How many wires the network has, so how many squares the snap shows, from 13 to 20. Rewiring keeps the count within one of the knob; at 16, 17 and 19 the first rectangle shown is perfect, every square a different size. +// Knob 4: Wander. How far each conductance drifts from 1: within a factor of 1.4 at -1, 2 at the opening 0.06, and 4.5 at 1. +// Buttons: K1 tap snaps the tiling into squares now. + +const W = 64, H = 128; +const { floor, round, min, max, abs, exp, sin, sqrt, log, atan2, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (u) => ((u = cl(u, 0, 1)), u * u * (3 - 2 * u)); + +// The network lives on a triangular lattice of 3 by 4 nodes, the top pole +// above it wired to the first row and the bottom pole below wired to the +// last. Positions only fix the order of the wires round each node, which +// is all the drawing needs: the plane picture says which faces lie on +// either side of a wire. +const CW = 3, RW = 4, NI = CW * RW, PS = NI, PT = NI + 1, NN = NI + 2; +const NX = [], NY = []; +for (let j = 0; j < RW; j++) for (let i = 0; i < CW; i++) NX.push(i + (j % 2) * 0.5), NY.push(-j * 0.866); +NX.push(1.25, 1.25); +NY.push(1.5, -(RW - 1) * 0.866 - 1.5); +const EU = [], EW = []; +const wire = (u, w) => (EU.push(u), EW.push(w)); +for (let j = 0; j < RW; j++) + for (let i = 0; i < CW; i++) { + const p = j * CW + i, k = j % 2 ? i + 1 : i - 1; + if (i + 1 < CW) wire(p, p + 1); + if (j + 1 < RW) { + wire(p, p + CW); + if (k >= 0 && k < CW) wire(p, (j + 1) * CW + k); + } + } +for (let i = 0; i < CW; i++) wire(PS, i), wire((RW - 1) * CW + i, PT); +// Wire NE is the battery, leaving the top pole upward and reaching the +// bottom pole from below, round the outside. +const NE = EU.length; + +// Networks, one walk per Tiles setting from 13 to 20. Each is a set of +// wires written as a bitmask in base 36, and two neighbours in a walk differ +// by one or two wires. They were picked so that at conductance 1 no wire +// carries zero current, the rectangle stands 1.75 to 2.35 times as tall as +// it is wide and the smallest square is at least 4.5 LEDs. Walks 16, 17 +// and 19 open on perfect squared rectangles of orders 16, 17 and 19. +const WALKS = [ + "46p4dk 47xze0 47y2js 47y3ag 46qjoo 46qiy0 46npt4 46qiw8 46p548 46p554", + "46mus8 46pniw 46qyxk 46qzbk 464le8 46o9cg 46r0wg 46pmfk 46k060 46lemw", + "46pmfs 46pmcc 46k1ng 46musc 47vsyg 46o9co 46qzbs 464leg 46pnx4 46q0d4", + "6c9fh7 69sxp7 69sxln 6l1h5n 6l1h5m 6l02oq 6l2vtm 6jwqsa 6m3el6 6m3ehn", + "8k66kn 8hoa87 8hpop3 7xqhxr 808ea7 6catzj 6casf3 6c9dy7 69rj6n 69sxr2", + "5iz8d7 6cy0ij 6d287v 6dj317 6m9q8r 6ma2or 6jtkwr 6jxsm3 6juzkr 26jdsr", + "3syppz 3t02pj 3t02lz 3szwan 3t01tr 3t03en 3syou7 47y2wv 4cxvlq 4cxvlp", + "8glqwb 8gg4mz 8gg5f7 8gg5ff 8gg5fc 8gg3y0 7mhbso 4am77c 4am9ko 4am9kc", +].map((s) => s.split(" ").map((w) => parseInt(w, 36))); +const has = (m, e) => floor(m / 2 ** e) % 2 === 1; +const apart = (a, b) => { + let d = 0; + for (let e = 0; e < NE; e++) d += has(a, e) !== has(b, e); + return d; +}; + +// The plane structure of a set of wires: which nodes take part, and for +// each wire the face on its left going from EU to EW (half 2e) and going +// back (half 2e + 1). Faces are traced by turning clockwise at each node. +function build(S, mask) { + const on = []; + for (let e = 0; e < NE; e++) if (has(mask, e)) on.push(e); + const adj = []; + for (let u = 0; u < NN; u++) adj.push([]); + for (const e of on) { + const u = EU[e], w = EW[e]; + adj[u].push([atan2(NY[w] - NY[u], NX[w] - NX[u]), e, w]); + adj[w].push([atan2(NY[u] - NY[w], NX[u] - NX[w]), e, u]); + } + adj[PS].push([PI / 2, NE, PT]); + adj[PT].push([-PI / 2, NE, PS]); + for (const l of adj) l.sort((p, q) => p[0] - q[0]); + const half = (e, from) => 2 * e + (e === NE ? (from === PS ? 0 : 1) : from === EU[e] ? 0 : 1); + const face = new Int32Array(2 * NE + 2).fill(-1); + let nf = 0; + for (let u0 = 0; u0 < NN; u0++) + for (const h0 of adj[u0]) { + if (face[half(h0[1], u0)] >= 0) continue; + let u = u0, h = h0; + while (face[half(h[1], u)] < 0) { + face[half(h[1], u)] = nf; + const l = adj[h[2]]; + let k = 0; + while (l[k][1] !== h[1]) k++; + u = h[2]; + h = l[(k + l.length - 1) % l.length]; + } + nf++; + } + // Unknown node potentials: the inner nodes some wire touches. + const idx = new Int32Array(NN).fill(-1); + let n = 0; + for (const e of on) for (const u of [EU[e], EW[e]]) if (u < NI && idx[u] < 0) idx[u] = n++; + // An order to fix the face potentials: across wire e the left face sits + // higher than the right by the current along it. + const known = new Uint8Array(nf), order = []; + known[face[2 * NE]] = 1; + for (let more = true; more; ) { + more = false; + for (const e of [...on, NE]) { + const a = face[2 * e], b = face[2 * e + 1]; + if (known[a] && !known[b]) order.push([e, a, b, -1]), (known[b] = 1), (more = true); + else if (known[b] && !known[a]) order.push([e, b, a, 1]), (known[a] = 1), (more = true); + } + } + Object.assign(S, { on, face, nf, idx, n, order, phi: new Float64Array(nf) }); +} + +// Kirchhoff: solve for the node potentials with the top pole at 1 and the +// bottom at 0, then lay every wire out as a tile. +function solve(S) { + const { on, idx, n, c, v, cur, face, order, phi, tiles } = S; + const A = S.A; + for (let i = 0; i < n; i++) A[i].fill(0, 0, n + 1); + for (const e of on) { + const g = c[e], u = EU[e], w = EW[e], iu = idx[u], iw = idx[w]; + if (iu >= 0) { + A[iu][iu] += g; + if (iw >= 0) A[iu][iw] -= g; + else if (w === PS) A[iu][n] += g; + } + if (iw >= 0) { + A[iw][iw] += g; + if (iu >= 0) A[iw][iu] -= g; + else if (u === PS) A[iw][n] += g; + } + } + for (let i = 0; i < n; i++) { + let p = i; + for (let k = i + 1; k < n; k++) if (abs(A[k][i]) > abs(A[p][i])) p = k; + const t = A[i]; + (A[i] = A[p]), (A[p] = t); + for (let k = i + 1; k < n; k++) { + const f = A[k][i] / A[i][i]; + if (f) for (let m = i; m <= n; m++) A[k][m] -= f * A[i][m]; + } + } + v.fill(0); + v[PS] = 1; + for (let i = n - 1; i >= 0; i--) { + let s = A[i][n]; + for (let m = i + 1; m < n; m++) s -= A[i][m] * S.x[m]; + S.x[i] = s / A[i][i]; + } + for (let u = 0; u < NI; u++) if (idx[u] >= 0) v[u] = S.x[idx[u]]; + let I = 0; + for (const e of on) { + cur[e] = c[e] * (v[EU[e]] - v[EW[e]]); + if (EU[e] === PS) I += cur[e]; + } + cur[NE] = -I; + phi[face[2 * NE]] = 0; + for (const [e, a, b, s] of order) phi[b] = phi[a] + s * cur[e]; + let lo = 0; + for (let f = 0; f < S.nf; f++) lo = min(lo, phi[f]); + S.I = I; + tiles.length = 0; + for (const e of on) { + const a = phi[face[2 * e]] - lo, b = phi[face[2 * e + 1]] - lo, p = v[EU[e]], q = v[EW[e]]; + tiles.push([e, min(a, b), max(a, b), 1 - max(p, q), 1 - min(p, q)]); + } +} + +// Story seconds at Speed 2: squares hold, drift rises, drift, snap. +const HOLD = 1.2, RISE = 2.4, SETTLE = 6.8, CYCLE = 8, REWIRE = 3.4; + +export function setup(params) { + const fq = [], ph = []; + for (let e = 0; e < NE; e++) fq.push(0.35 + 0.5 * Math.random(), 0.8 + 0.7 * Math.random()), ph.push(6.3 * Math.random(), 6.3 * Math.random()); + params.s = { + t: 0, tc: 0, cyc: 0, amp: 0, set0: SETTLE, setLen: CYCLE - SETTLE, setFrom: 1, flash: 1, + K: 0, walk: 0, step: 0, dir: 1, mask: 0, next: 0, ramp: -1, rate: 0, + hue: 0, speed: 2, wander: 0.06, fq, ph, drift: new Float64Array(NE + 1), c: new Float64Array(NE), + v: new Float64Array(NN), cur: new Float64Array(NE + 1), x: new Float64Array(NI), A: Array.from({ length: NI }, () => new Float64Array(NI + 1)), + tiles: [], I: 1, id: new Int16Array(W * H), stripe: new Float64Array(NE), + }; +} + +function rewire(S, to) { + S.next = to; + S.ramp = 0; + S.rate = 1 / 2.6; + build(S, S.mask | to); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 13, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = cl(k[0], 0, 1); + S.speed = cl(k[1], 0.2, 3); + S.wander = 0.7 * 2 ** (1.1 * cl(k[3], -1, 1)); + const K = round(cl(k[2], 13, 20)); + if (!S.K) { + S.K = K; + S.mask = WALKS[K - 13][0]; + build(S, S.mask); + } else if (K !== S.K && S.ramp < 0) { + // A new walk: go to its network nearest the one shown. + S.K = K; + const w = WALKS[K - 13]; + let best = 0; + for (let i = 1; i < w.length; i++) if (apart(w[i], S.mask) < apart(w[best], S.mask)) best = i; + (S.step = best), (S.dir = best === w.length - 1 ? -1 : 1); + if (w[best] !== S.mask) rewire(S, w[best]); + } + const ds = (d * S.speed) / 2; + S.t += ds; + S.flash *= exp(-d / 0.35); + const press = input.btnPressed && input.btnPressed[0]; + if (press && S.tc > 0.15 && S.tc < S.set0) { + // Snap now: settle from where the drift is in 0.4 seconds. + (S.setFrom = S.amp), (S.set0 = S.tc), (S.setLen = 0.4); + if (S.ramp >= 0) S.rate = max(S.rate, (1 - S.ramp) / 0.38); + } + S.tc += ds; + if (S.tc >= S.set0 + S.setLen) { + (S.tc = 0), S.cyc++, (S.flash = 1), (S.set0 = SETTLE), (S.setLen = CYCLE - SETTLE), (S.setFrom = 1); + } + const tc = S.tc; + S.amp = tc < HOLD ? 0 : tc < S.set0 ? (tc < RISE ? sm((tc - HOLD) / (RISE - HOLD)) : 1) : S.setFrom * (1 - ((tc - S.set0) / S.setLen) ** 2); + // Every second cycle, one step along the walk. + if (S.ramp < 0 && S.cyc % 2 === 1 && tc >= REWIRE && tc < REWIRE + 0.5 && S.set0 === SETTLE) { + const w = WALKS[S.K - 13]; + if (w.length > 1) { + if (S.step + S.dir < 0 || S.step + S.dir >= w.length) S.dir = -S.dir; + S.step += S.dir; + rewire(S, w[S.step]); + } + } + if (S.ramp >= 0) { + S.ramp += ds * S.rate; + if (S.ramp >= 1) { + S.mask = S.next; + S.ramp = -1; + build(S, S.mask); + } + } + // Conductances: each wire drifts as exp(a n(t)), n a slow wave in -1..1. + const r = S.ramp < 0 ? 1 : sm(S.ramp), a = S.amp * S.wander; + for (const e of S.on) { + const n = 0.6 * sin(S.fq[2 * e] * S.t + S.ph[2 * e]) + 0.4 * sin(S.fq[2 * e + 1] * S.t + S.ph[2 * e + 1]); + let g = a ? exp(a * n) : 1; + if (S.ramp >= 0) { + const was = has(S.mask, e), will = has(S.next, e); + if (was && !will) g *= 1 - r; + else if (will && !was) g *= r; + } + S.c[e] = max(g, 1e-9); + S.stripe[e] += ds * 9 * S.c[e]; + } + solve(S); +} + +function hsv(h, s, v) { + 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); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.tiles.length) return; + const { tiles, id, I } = S; + const sc = min(W / I, H), ox = (W - I * sc) / 2, oy = (H - sc) / 2; + id.fill(-1); + const col = [], top = [], q = []; + let qa = 1, qb = 0; + tiles.forEach((t, k) => { + const x0 = round(ox + t[1] * sc), x1 = round(ox + t[2] * sc), y0 = round(oy + t[3] * sc), y1 = round(oy + t[4] * sc); + for (let y = max(0, y0); y < min(H, y1); y++) for (let x = max(0, x0); x < min(W, x1); x++) id[y * W + x] = k; + // Size: the side of a square of the same area, against the width. + q.push(log(max(1e-6, sqrt((t[2] - t[1]) * (t[4] - t[3])) / I))); + if (x1 > x0 && y1 > y0) (qa = min(qa, q[k])), (qb = max(qb, q[k])); + top.push(y0 + S.stripe[t[0]]); + }); + // Colour by size between the smallest and the biggest tile shown now. + tiles.forEach((t, k) => { + const u = cl((q[k] - qa) / max(0.3, qb - qa), 0, 1); + col.push(hsv(0.52 + S.hue + 0.55 * (1 - u), 0.85 - 0.1 * u, 0.62 + 0.38 * (1 - u))); + }); + const fl = S.flash; + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const k = id[p]; + if (k < 0) { + display.setPixel(x, y, 0, 0, 0); + continue; + } + const c = col[k]; + // Seams on the left and top of each tile; the rectangle's own edge stays. + if ((x > 0 && id[p - 1] >= 0 && id[p - 1] !== k) || (y > 0 && id[p - W] >= 0 && id[p - W] !== k)) { + const b = 255 * fl; + display.setPixel(x, y, round(0.08 * 255 * c[0] * (1 - fl) + b * 0.9), round(0.08 * 255 * c[1] * (1 - fl) + b * 0.95), round(0.08 * 255 * c[2] * (1 - fl) + b)); + continue; + } + // Faint bands run down each tile at a speed set by its conductance. + const ph = (((y - top[k]) % 9) + 9) % 9, m = ph < 1 ? 0.8 : 1; + display.setPixel(x, y, round(255 * c[0] * m), round(255 * c[1] * m), round(255 * c[2] * m)); + } +} From c468eb3afc7a0136370e83b3ab4677ae5b592e05 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 01:52:01 +0000 Subject: [PATCH 05/14] =?UTF-8?q?53=ED=8C=90:=20Belt=20Trick=20=EA=B3=BC?= =?UTF-8?q?=20Rokurokubi=20=ED=8C=A8=ED=84=B4=EC=9D=84=20=EB=8D=94?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Yb8D9UxrifXqrXju5CVbqY --- incoming/pattern-study/patterns/belt-trick.js | 311 +++++++++++++++ incoming/pattern-study/patterns/rokurokubi.js | 360 ++++++++++++++++++ 2 files changed, 671 insertions(+) create mode 100644 incoming/pattern-study/patterns/belt-trick.js create mode 100644 incoming/pattern-study/patterns/rokurokubi.js diff --git a/incoming/pattern-study/patterns/belt-trick.js b/incoming/pattern-study/patterns/belt-trick.js new file mode 100644 index 000000000..6402da546 --- /dev/null +++ b/incoming/pattern-study/patterns/belt-trick.js @@ -0,0 +1,311 @@ +// ===== Patternflow pattern ===== +// Title: Belt Trick +// Family: topology +// Idea: A cube in the middle of the panel, tied by ribbons to bars along the top and bottom edges, turns about one axis without stopping: after one full turn the ribbons are wound round it and no moving of the ribbons alone can free them, after the second they have swung round the cube and hang straight again, and a ring behind it fills once a turn, violet in the first and green in the second, so the two turns can be told apart. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Dirac's string problem (Paul Dirac; M. H. A. Newman, 1942), the double cover of SO(3) by the unit quaternions, pattern round 53 +// Round: 53 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Ribbons=2..8, Tilt=0..1 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the ribbons are amber on the front and cyan on the back, the cube's faces blue, violet and pink, and the ring violet in the first turn and green in the second. +// Knob 2: Speed. At 2 the cube turns once in 4 seconds, so the ribbons come free every 8; at 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Ribbons. How many ribbons tie the cube, from 2, one belt up and one down, to 8, four each way. Fewer ribbons are wider. Neighbours also sit 4 apart in depth: over a whole loop no two ribbons come closer than 0.6 LEDs with 8 of them, 0.8 with 4 and 4.8 with 2. +// Knob 4: Tilt. The spin axis. At 0 it stands upright and the cube turns like a plate on a waiter's hand, and after one turn the ribbons lie wound round it in the plane of the panel; at 1 it points at us, the cube turns like a wheel, and after one turn the ribbons are twisted round each other like a rope. +// Buttons: K1 tap reverses the spin. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, ceil, sqrt, sin, cos, abs, atan2, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The cube's half side, the ball it turns in, the shell the ribbons move in, +// and the wall the ribbons are tied to, just past the panel's top and bottom. +const HC = 8, RIN = HC * sqrt(3) + 1.2, ROUT = 30.5, YW = 67; +// Rows of a ribbon inside the shell. +const NS = 110; +// Camera: turned a little to the right and looking a little down. +const DIST = 170, YAW = 0.42, PITCH = 0.16, CX = 31.5, CY = 63.5; +const CYW = cos(YAW), SYW = sin(YAW), CP = cos(PITCH), SP = sin(PITCH); + +function hsv(h, s, v) { + 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); + return [ + i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, + i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, + i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p, + ]; +} + +export function setup(params) { + const F = (n) => new Float32Array(n); + params.s = { + t: 0, dir: 1, hue: 0, sp: 2, n: 2, tilt: 0.06, + zb: F(N), id: new Int16Array(N), col: F(N * 3), + // Mapped ribbon vertices (world) and their screen places. + wx: F(2 * NS + 8), wy: F(2 * NS + 8), wz: F(2 * NS + 8), sx: F(2 * NS + 8), sy: F(2 * NS + 8), sz: F(2 * NS + 8), + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const s = params.s, k = input.knobValues || [0, 2, 2, 0.06], d = cl(dt || 0, 0, 0.1); + s.hue = cl(k[0], 0, 1); + s.sp = cl(k[1], 0.2, 3); + s.n = cl(Math.round(k[2]), 2, 8); + s.tilt = cl(k[3], 0, 1); + if (input.btnPressed && input.btnPressed[0]) s.dir = -s.dir; + // t runs 0..1 over the two turns, 720 degrees. + s.t += (s.dir * d * s.sp) / 16; + s.t -= floor(s.t); +} + +// The homotopy. q(t) = cos(2 pi t) + sin(2 pi t) a is the cube's turn by +// 720 degrees about the unit axis a, a loop in the unit quaternions. With +// b = s pi / 2 and v a unit vector at right angles to a, +// H(s, t) = (cos^2 b + sin^2 b cos 2 pi t) + (sin b sin 2 pi t) a + (cos b sin b (1 - cos 2 pi t)) v. +// For each s, t runs H round a circle in the 3-sphere that passes through 1 +// and touches the loop there; the circles shrink from the loop itself at +// s = 1 to the point 1 at s = 0. H has length 1, H(0, t) = 1, H(1, t) = q(t) +// and H(s, 0) = H(s, 1) = 1. At t = 1/2 it is a turn by 360 s degrees about +// v: the ribbons wound once round the cube. s is read off the distance from +// the cube's centre, 0 at the shell's outer sphere and 1 at its inner one, +// so every sphere round the cube turns as one piece and nothing in space +// ever passes through anything else. +function shellTurn(x, y, z, ax, ay, az, vx, vy, vz, C, S, out, o) { + const r = sqrt(x * x + y * y + z * z); + let u = cl((ROUT - r) / (ROUT - RIN), 0, 1); + u = u * u * (3 - 2 * u); + const b = (u * PI) / 2, cb = cos(b), sb = sin(b); + const w = cb * cb + sb * sb * C, p = sb * S, q = cb * sb * (1 - C); + rot(w, p * ax + q * vx, p * ay + q * vy, p * az + q * vz, x, y, z, out, o); +} + +// Turn (x, y, z) by the unit quaternion w + (i, j, k). +function rot(w, i, j, k, x, y, z, out, o) { + const tx = 2 * (j * z - k * y), ty = 2 * (k * x - i * z), tz = 2 * (i * y - j * x); + out[o] = x + w * tx + (j * tz - k * ty); + out[o + 1] = y + w * ty + (k * tx - i * tz); + out[o + 2] = z + w * tz + (i * ty - j * tx); +} + +// World to screen: the camera turns the world, then a soft perspective. +function cam(x, y, z, out, o) { + const x1 = x * CYW - z * SYW, z1 = x * SYW + z * CYW; + const y2 = y * CP - z1 * SP, z2 = y * SP + z1 * CP; + const f = DIST / (DIST - z2); + out[o] = CX + x1 * f; + out[o + 1] = CY - y2 * f; + out[o + 2] = DIST - z2; +} + +const tmp = new Float32Array(3), hc = new Float32Array(3 * (NS + 3)); +// How much a normal faces the eye. +const toEye = (x, y, z) => y * SP + (x * SYW + z * CYW) * CP; + +export function draw(display, params) { + const s = params.s; + if (!s) return; + const { zb, id, col, wx, wy, wz, sx, sy, sz } = s; + zb.fill(1e9); + id.fill(0); + const al = ((1 - s.tilt) * PI) / 2; + const ax = 0, ay = sin(al), az = cos(al), vx = 0, vy = cos(al), vz = -sin(al); + const C = cos(TAU * s.t), S = sin(TAU * s.t); + const L = [0.42, 0.55, 0.72]; + const h0 = s.hue; + const fr = hsv(h0 + 0.1, 0.85, 1), bk = hsv(h0 + 0.5, 0.75, 1); + const n = s.n, nt = ceil(n / 2); + let rib = 0; + for (let side = 0; side < 2; side++) { + const k = side ? n - nt : nt, sg = side ? -1 : 1; + const spc = (1.75 * HC) / k, wdt = min(8.5, 0.5 * spc), spw = 46 / k; + for (let r = 0; r < k; r++, rib++) { + // Neighbours sit 4 apart in depth as well, which keeps them about + // twice as far apart when they are wound round the cube. + const zs = k > 1 ? ((r + side) & 1 ? 2 : -2) : 0; + const off = r - (k - 1) / 2; + const Ax = off * spw, Ay = sg * YW, Bx = off * spc, By = sg * HC; + const dx = Bx - Ax, dy = By - Ay, len = sqrt(dx * dx + dy * dy); + // Across the ribbon, in the panel's plane. + const bx = (dy / len) * sg, by = (-dx / len) * sg; + // Where the centre line crosses the shell's spheres. + const uo = cross(Ax, Ay, dx, dy, sqrt((ROUT + 1) ** 2 - zs * zs)), ui = cross(Ax, Ay, dx, dy, sqrt((RIN - 0.5) ** 2 - zs * zs)); + const rows = NS + 3; + for (let j = 0; j < rows; j++) { + const u = j === 0 ? 0 : j === rows - 1 ? 1 : uo + ((ui - uo) * (j - 1)) / NS; + const cxp = Ax + dx * u, cyp = Ay + dy * u; + for (let e = 0; e < 2; e++) { + const h = (e ? 0.5 : -0.5) * wdt, vi = j * 2 + e; + shellTurn(cxp + bx * h, cyp + by * h, zs, ax, ay, az, vx, vy, vz, C, S, tmp, 0); + wx[vi] = tmp[0]; + wy[vi] = tmp[1]; + wz[vi] = tmp[2]; + cam(tmp[0], tmp[1], tmp[2], tmp, 0); + sx[vi] = tmp[0]; + sy[vi] = tmp[1]; + sz[vi] = tmp[2]; + } + } + for (let j = 0; j < rows - 1; j++) { + const a = j * 2, b = a + 1, c = a + 2, dd = a + 3; + // The face's normal in the world, for the light. + const ux = wx[c] - wx[a], uy = wy[c] - wy[a], uz = wz[c] - wz[a]; + const qx = wx[b] - wx[a], qy = wy[b] - wy[a], qz = wz[b] - wz[a]; + let nx = uy * qz - uz * qy, ny = uz * qx - ux * qz, nz = ux * qy - uy * qx; + const nl = 1 / (sqrt(nx * nx + ny * ny + nz * nz) + 1e-9); + nx *= nl; + ny *= nl; + nz *= nl; + // Which face is towards us: the sign of the quad's area on the screen. + const area = (sx[a] - sx[dd]) * (sy[b] - sy[c]) - (sx[b] - sx[c]) * (sy[a] - sy[dd]); + const front = area * sg < 0; + tri(s, a, c, b, rib + 1, nx, ny, nz, front ? fr : bk, L); + tri(s, b, c, dd, rib + 1, nx, ny, nz, front ? fr : bk, L); + hc[j * 3] = s.lr * 0.8; + hc[j * 3 + 1] = s.lg * 0.8; + hc[j * 3 + 2] = s.lb * 0.8; + } + // The hems, drawn as lines, so a ribbon seen edge on stays a line. + for (let j = 0; j < rows - 1; j++) + for (let e = 0; e < 2; e++) { + const a = j * 2 + e, b = a + 2; + line(s, sx[a], sy[a], sz[a], sx[b], sy[b], sz[b], rib + 1, hc[j * 3], hc[j * 3 + 1], hc[j * 3 + 2]); + } + } + } + cube(s, ax, ay, az, C, S, L); + shade(s, display); +} + +// The u where the line (Ax, Ay) + u (dx, dy) is at distance R from the centre. +function cross(Ax, Ay, dx, dy, R) { + const a = dx * dx + dy * dy, b = 2 * (Ax * dx + Ay * dy), c = Ax * Ax + Ay * Ay - R * R; + const disc = b * b - 4 * a * c; + if (disc < 0) return 1; + return cl((-b - sqrt(disc)) / (2 * a), 0, 1); +} + +function tri(s, i0, i1, i2, idv, nx, ny, nz, c, L) { + const { sx, sy, sz } = s; + const x0 = sx[i0], y0 = sy[i0], x1 = sx[i1], y1 = sy[i1], x2 = sx[i2], y2 = sy[i2]; + // Two-sided light: the normal is turned to face the eye. + const sgn = toEye(nx, ny, nz) >= 0 ? 1 : -1; + const ld = max(0, sgn * (nx * L[0] + ny * L[1] + nz * L[2])); + const lit = 0.6 + 0.4 * ld; + const r = c[0] * lit, g = c[1] * lit, b = c[2] * lit; + (s.lr = r), (s.lg = g), (s.lb = b); + fill(s, x0, y0, x1, y1, x2, y2, sz[i0], sz[i1], sz[i2], idv, r, g, b); +} + +function line(s, x0, y0, z0, x1, y1, z1, idv, r, g, b) { + const { zb, id, col } = s; + const n = ceil(max(abs(x1 - x0), abs(y1 - y0)) * 1.5) + 1; + for (let i = 0; i <= n; i++) { + const u = i / n, x = floor(x0 + (x1 - x0) * u), y = floor(y0 + (y1 - y0) * u); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const q = y * W + x, d = z0 + (z1 - z0) * u - 0.9; + if (d < zb[q]) { + zb[q] = d + 0.9; + id[q] = idv; + col[q * 3] = r; + col[q * 3 + 1] = g; + col[q * 3 + 2] = b; + } + } +} + +function fill(s, x0, y0, x1, y1, x2, y2, z0, z1, z2, idv, r, g, b) { + const { zb, id, col } = s; + const den = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0); + if (abs(den) < 1e-9) return; + const inv = 1 / den; + const X0 = max(0, ceil(min(x0, x1, x2) - 0.5)), X1 = min(W - 1, floor(max(x0, x1, x2) - 0.5)); + const Y0 = max(0, ceil(min(y0, y1, y2) - 0.5)), Y1 = min(H - 1, floor(max(y0, y1, y2) - 0.5)); + for (let y = Y0; y <= Y1; y++) { + const cy = y + 0.5 - y0; + for (let x = X0; x <= X1; x++) { + const cx = x + 0.5 - x0; + const a = (cx * (y2 - y0) - cy * (x2 - x0)) * inv, bb = ((x1 - x0) * cy - (y1 - y0) * cx) * inv; + if (a < -1e-6 || bb < -1e-6 || a + bb > 1 + 1e-6) continue; + const d = z0 + (z1 - z0) * a + (z2 - z0) * bb, q = y * W + x; + if (d < zb[q]) { + zb[q] = d; + id[q] = idv; + col[q * 3] = r; + col[q * 3 + 1] = g; + col[q * 3 + 2] = b; + } + } + } +} + +// The cube: six faces, each its own colour, turned by q(t). +const FACES = [ + [1, 0, 0, 0, 1, 0, 0, 0, 1], [-1, 0, 0, 0, 1, 0, 0, 0, -1], + [0, 1, 0, 0, 0, 1, 1, 0, 0], [0, -1, 0, 0, 0, -1, 1, 0, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 0], [0, 0, -1, -1, 0, 0, 0, 1, 0], +]; +const cv = new Float32Array(12), cn = new Float32Array(3); +function cube(s, ax, ay, az, C, S, L) { + const w = C, i = S * ax, j = S * ay, k = S * az; + for (let f = 0; f < 6; f++) { + const F = FACES[f]; + rot(w, i, j, k, F[0], F[1], F[2], cn, 0); + const vdot = toEye(cn[0], cn[1], cn[2]); + const ld = max(0, cn[0] * L[0] + cn[1] * L[1] + cn[2] * L[2]); + const c = hsv(s.hue + 0.62 + 0.13 * (f >> 1), f & 1 ? 0.4 : 0.8, 1); + const lit = 0.5 + 0.5 * ld; + for (let q = 0; q < 4; q++) { + const e = q === 0 || q === 3 ? -1 : 1, g = q < 2 ? -1 : 1; + const x = (F[0] + e * F[3] + g * F[6]) * HC, y = (F[1] + e * F[4] + g * F[7]) * HC, z = (F[2] + e * F[5] + g * F[8]) * HC; + rot(w, i, j, k, x, y, z, tmp, 0); + cam(tmp[0], tmp[1], tmp[2], cv, q * 3); + } + if (vdot < -0.05) continue; + const r = c[0] * lit, gg = c[1] * lit, b = c[2] * lit; + fill(s, cv[0], cv[1], cv[3], cv[4], cv[6], cv[7], cv[2], cv[5], cv[8], 20 + f, r, gg, b); + fill(s, cv[0], cv[1], cv[6], cv[7], cv[9], cv[10], cv[2], cv[8], cv[11], 20 + f, r, gg, b); + } +} + +function shade(s, display) { + const { id, col } = s; + const lap = s.t < 0.5 ? 0 : 1, prog = (s.t * 2) % 1; + const ca = hsv(s.hue + 0.78, 0.65, 0.7), cb = hsv(s.hue + 0.36, 0.75, 0.7); + const cur = lap ? cb : ca, old = lap ? ca : cb; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const q = y * W + x, o = q * 3, p = id[q]; + // The bars the ribbons are tied to, along the top and bottom edges, + // with a clamp where a ribbon goes under. + if (y < 2 || y > H - 3) { + const edge = y === 1 || y === H - 2, tied = edge && id[y === 1 ? q + W : q - W] > 0 && id[y === 1 ? q + W : q - W] < 20; + const v = tied ? 170 : edge ? 70 : 40; + display.setPixel(x, y, v * 0.85, v * 0.78, v); + continue; + } + if (p) { + // A seam where two ribbons or two faces meet. + const rp = x < W - 1 ? id[q + 1] : p, dp = y < H - 1 ? id[q + W] : p; + const k = (rp && rp !== p) || (dp && dp !== p) ? 150 : 255; + display.setPixel(x, y, min(255, col[o] * k), min(255, col[o + 1] * k), min(255, col[o + 2] * k)); + continue; + } + // The dial behind the cube: one colour fills it in the first turn, + // the other in the second. + const dx = x + 0.5 - CX, dy = y + 0.5 - CY, r = sqrt(dx * dx + dy * dy); + const ring = cl(1.6 - abs(r - ROUT - 0.6), 0, 1); + if (ring > 0) { + let a = atan2(dx, -dy) / TAU; + if (a < 0) a += 1; + const c = a < prog ? cur : old, m = a < prog ? 1 : 0.4; + const tip = abs(a - prog) < 0.012 || abs(a - prog) > 0.988 ? 1.6 : 1; + display.setPixel(x, y, c[0] * 255 * ring * m * tip, c[1] * 255 * ring * m * tip, c[2] * 255 * ring * m * tip); + } else display.setPixel(x, y, 4, 3, 10); + } +} diff --git a/incoming/pattern-study/patterns/rokurokubi.js b/incoming/pattern-study/patterns/rokurokubi.js new file mode 100644 index 000000000..089ec4667 --- /dev/null +++ b/incoming/pattern-study/patterns/rokurokubi.js @@ -0,0 +1,360 @@ +// ===== Patternflow pattern ===== +// Title: Rokurokubi +// Family: halloween +// Idea: Behind lit paper doors a woman sleeps by an andon lamp, and on the glowing paper her shadow's neck rises and snakes up the whole height of the room, the head wanders under the lintel and goes down to lick the lamp oil until its shadow swells over the doors and the flame flares green, then the neck draws back down to the pillow. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 53, after the rokurokubi of Japanese folklore +// Round: 53 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Reach=-1..1, Flicker=0..1 +// +// Knob 1: Hue. The lamp light on the paper: orange at 0, yellow at 0.1, green at 0.3, blue at 0.6, rose at 0.9. The night outside stays purple, the shadows black and the flare at the lick green. +// Knob 2: Speed. One clock for the story and the flame. At 2 a night takes 18 seconds: the neck rises from 0.6 s and reaches the lintel at 4.4, the head wanders under it until 8, is down at the lamp by 10 and licks the oil until 13, and is back on the pillow at 16; at 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Reach. How far the neck goes. At -1 the head climbs only halfway up the doors and at 0 to the lintel; turned up, the neck winds in more and wider coils on the way, 7 coils 5.5 LEDs out to each side at 0 and 10 coils 12.5 out at 1. +// Knob 4: Flicker. How much the flame gutters. At 0.06 it breathes, the paper dims by up to a seventh and the shadows tremble; at 1 a draught swings the flame up to 10 LEDs and the paper pulses down to 40%, so a shadow near the lamp leaps across the doors. +// Buttons: K1 tap: the head turns to us, comes up against the paper and tears a hole in it, and her white face peers through for three seconds while the story waits. The hole stays open until the next night. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, sqrt, sin, cos, abs, atan2, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +const mix = (a, b, t) => a + (b - a) * t; +function hash(i, j) { + let h = (Math.imul(i, 374761393) + Math.imul(j, 668265263)) | 0; + h = Math.imul(h ^ (h >>> 13), 1274126177); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +} +function hsv(h, s, v) { + 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); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// The paper: x 6..57, y 7..116, two doors meeting at x 31.5. +const PX0 = 6, PX1 = 57, PY0 = 7, PY1 = 116; +// The lamp's flame behind the paper, and the depth of the sleeper's plane +// as a share of the lamp's depth: a shadow is the shape blown up by +// 1 / (1 - depth) about the flame. +const LX = 13, LY = 99, DEP = 0.3; +// The story in seconds at Speed 2. +const T_RISE = 0.6, T_TOP = 4.4, T_WAND = 8, T_LAMP = 10, T_LICK = 13, T_BACK = 16, T_END = 18; +// The shoulders and the head on the pillow. +const SHX = 45, SHY = 108, HDX = 50, HDY = 106; +// The head's parts round the skull's middle, facing right: skull, nose, +// lips and chin, the knot, the front lock, the side lock, the tail of hair. +const HS = 1.6; +const HEAD = [[0, 0, 3.6, 4.2], [3.6, 0.4, 0.9, 0.8], [2.9, 2.6, 1.2, 1.1], [-0.8, -5.4, 4.4, 2.2], [2.2, -3.9, 1.9, 1.6], [-1.6, -0.8, 3, 3.3], [-4.6, 1, 2.2, 1.5]].map((a) => a.map((v) => v * HS)); +const PINS = [[-2.5, -6.8, -7.5, -9], [0.5, -7.2, 3.6, -10.4]].map((a) => a.map((v) => v * HS)); + +export function setup(params) { + params.s = { + t: 0, rt: 0, hue: 0, sp: 2, reach: 0, fl: 0.06, pathFor: 99, + px: new Float32Array(600), py: new Float32Array(600), np: 0, uTop: 0, uWand: 0, uLamp: 0, + M: new Float32Array(N), BG: new Float32Array(N * 3), PAP: new Uint8Array(N), + peep: -1, hole: null, holeLive: 0, fcs: -1, ph: [0, 1, 2, 3, 4].map(() => Math.random() * TAU), + }; + scenery(params.s); +} + +// What does not move: the night, the posts, the lintel and sill, the +// veranda, and which LEDs are paper. +function scenery(s) { + const B = s.BG; + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, o = i * 3, n = hash(x, y); + let c; + if (x >= PX0 && x <= PX1 && y >= PY0 && y <= PY1) { + s.PAP[i] = 1; + continue; + } + if (y < 4) { + // Sky under the eave, moonlit purple, brighter at the left. + const g = 0.55 + 0.25 * (1 - x / 63) - 0.1 * y; + c = [0.42 * g, 0.2 * g, 0.62 * g]; + if (y >= 2 && y - 2 > (x * 3) / 63) c = [0.05, 0.03, 0.07]; + } else if (y < PY0) c = y === 4 ? [0.3, 0.2, 0.36] : [0.17, 0.1, 0.16]; + else if (y <= PY1 + 3) { + if (x < 3 || x > 60) { + // Night beside the house: purple, with leaves of a garden shrub. + const g = 0.32 + 0.22 * (1 - y / 127) + 0.05 * n; + c = [0.36 * g, 0.16 * g, 0.55 * g]; + if (hash(floor(x / 2), floor(y / 3)) < 0.28) c = [0.07, 0.04, 0.1]; + } else if (y > PY1) c = y === PY1 + 1 ? [0.32, 0.21, 0.3] : [0.16, 0.09, 0.13]; + else c = x === 3 || x === 60 ? [0.34, 0.22, 0.4] : x === 4 || x === 59 ? [0.2, 0.12, 0.19] : [0.12, 0.07, 0.11]; + } else { + // Veranda boards, the warm spill near the doors, moonlight at the edge. + const v = (y - PY1 - 4) / 7, seam = (x + (y & 1) * 7) % 17 === 0 ? 0.6 : 1; + c = [mix(0.5, 0.2, v) * seam, mix(0.26, 0.11, v) * seam, mix(0.2, 0.3, v) * seam]; + } + (B[o] = c[0]), (B[o + 1] = c[1]), (B[o + 2] = c[2]); + } +} + +// The head's road for one night: up from the pillow in coils, along under +// the lintel, down the left side to the lamp. Sampled every 0.7 LED. +function road(s) { + const r = s.reach, sc = r < 0 ? 1 + 0.5 * r : 1, rp = max(0, r); + const Y = (y) => HDY - (HDY - y) * sc; + // Up the right door in coils: more of them and wider as Reach turns up. + const nz = 7 + Math.round(3 * rp), A = 5.5 + 7 * rp, wp = [[HDX, HDY]]; + for (let i = 1; i <= nz; i++) wp.push([45 + (i & 1 ? A : -A) * (i === 1 ? 0.6 : 1), Y(98 - (62 * (i - 1)) / nz)]); + const top = wp.length; + wp.push([43, Y(35)], [37, Y(30)], [31, Y(36)], [25, Y(30)], [18, Y(34)]); + const wand = wp.length - 1; + const dn = (x, y) => [16 + (x - 16) * (1 + 0.8 * rp), Y(y)]; + wp.push(dn(14, 44), dn(23, 55), dn(13, 65), dn(22, 74), dn(15, 80), [22, Y(86)]); + const mark = [top, wand, wp.length - 1]; + const P = []; + for (let i = 0; i < wp.length - 1; i++) { + const a = wp[max(0, i - 1)], b = wp[i], c = wp[i + 1], d = wp[min(wp.length - 1, i + 2)]; + for (let k = 0; k < 24; k++) { + const t = k / 24, t2 = t * t, t3 = t2 * t; + const f = (p0, p1, p2, p3) => 0.5 * (2 * p1 + (-p0 + p2) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t2 + (-p0 + 3 * p1 - 3 * p2 + p3) * t3); + P.push([cl(f(a[0], b[0], c[0], d[0]), PX0 + 5, PX1 - 4), cl(f(a[1], b[1], c[1], d[1]), PY0 + 22, PY1), i]); + } + } + P.push([...wp[wp.length - 1], wp.length - 1]); + // Even steps along the road. + let n = 0, acc = 0; + s.px[0] = P[0][0]; + s.py[0] = P[0][1]; + n = 1; + for (let i = 1; i < P.length && n < 599; i++) { + const dx = P[i][0] - P[i - 1][0], dy = P[i][1] - P[i - 1][1], l = sqrt(dx * dx + dy * dy); + acc += l; + while (acc >= 0.7 && n < 599) { + acc -= 0.7; + const u = 1 - acc / max(l, 1e-6); + s.px[n] = P[i - 1][0] + dx * u; + s.py[n] = P[i - 1][1] + dy * u; + n++; + } + for (let m = 0; m < 3; m++) if (P[i][2] === mark[m] && P[i - 1][2] !== mark[m]) s[["uTop", "uWand", "uLamp"][m]] = n - 1; + } + s.uLamp = n - 1; + s.np = n; + s.pathFor = r; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const s = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + s.hue = cl(k[0], 0, 1); + s.sp = cl(k[1], 0.2, 3); + s.reach = cl(k[2], -1, 1); + s.fl = cl(k[3], 0, 1); + if (abs(s.reach - s.pathFor) > 0.004) road(s); + // One clock for everything, the flame included. + s.rt += (d * s.sp) / 2; + if (input.btnPressed && input.btnPressed[0] && s.peep < 0) s.peep = 0; + if (s.peep >= 0) { + s.peep += (d * s.sp) / 2; + if (s.peep > 3.8) s.peep = -1; + } else { + s.t += (d * s.sp) / 2; + if (s.t >= T_END) (s.t -= T_END), (s.hole = null); + } +} + +// Where the head is on its road (in steps) at story time t. +function headStep(s, t) { + if (t < T_RISE || t >= T_BACK) return 0; + if (t < T_TOP) return s.uTop * (1 - (1 - (t - T_RISE) / (T_TOP - T_RISE)) ** 2); + if (t < T_WAND) return mix(s.uTop, s.uWand, sm(T_TOP, T_WAND, t) * 0.7 + 0.3 * (t - T_TOP) / (T_WAND - T_TOP)); + if (t < T_LAMP) return mix(s.uWand, s.uLamp, sm(T_WAND, T_LAMP, t)); + if (t < T_LICK) return s.uLamp; + return s.uLamp * (1 - sm(T_LICK, T_BACK, t)); +} + +// Shadow coverage: a capsule from (ax, ay) to (bx, by) of radius r and soft edge e. +function capsule(M, ax, ay, bx, by, r, e) { + const x0 = max(PX0, floor(min(ax, bx) - r - e)), x1 = min(PX1, floor(max(ax, bx) + r + e) + 1); + const y0 = max(PY0, floor(min(ay, by) - r - e)), y1 = min(PY1, floor(max(ay, by) + r + e) + 1); + const dx = bx - ax, dy = by - ay, ll = dx * dx + dy * dy + 1e-9; + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const qx = x + 0.5 - ax, qy = y + 0.5 - ay, u = cl((qx * dx + qy * dy) / ll, 0, 1); + const ex = qx - dx * u, ey = qy - dy * u, dd = sqrt(ex * ex + ey * ey) - r; + const c = cl(0.5 - dd / e, 0, 1), q = y * W + x; + if (c > M[q]) M[q] = c; + } +} +// An ellipse at (cx, cy), radii rx, ry, turned by angle a. +function blob(M, cx, cy, rx, ry, a, e) { + const R = max(rx, ry) + e, ca = cos(a), sa = sin(a), mr = min(rx, ry); + for (let y = max(PY0, floor(cy - R)); y <= min(PY1, floor(cy + R) + 1); y++) + for (let x = max(PX0, floor(cx - R)); x <= min(PX1, floor(cx + R) + 1); x++) { + const qx = x + 0.5 - cx, qy = y + 0.5 - cy, u = (qx * ca + qy * sa) / rx, v = (-qx * sa + qy * ca) / ry; + const dd = (sqrt(u * u + v * v) - 1) * mr, c = cl(0.5 - dd / e, 0, 1), q = y * W + x; + if (c > M[q]) M[q] = c; + } +} + +export function draw(display, params) { + const s = params.s; + if (!s) return; + const { M, BG, PAP, px, py } = s; + const t = s.t, rt = s.rt, ph = s.ph, f = s.fl; + // The flame: a breath, now and then a gutter, and at the lick a flare. + const lick = sm(T_LAMP - 0.4, T_LAMP + 0.6, t) * (1 - sm(T_LICK - 0.5, T_LICK + 0.3, t)); + const jit = 0.6 + 4.5 * f + 2.5 * lick; + const n1 = sin(9.1 * rt + ph[0]) * 0.6 + sin(15.7 * rt + ph[1]) * 0.4, n2 = sin(7.3 * rt + ph[2]) * 0.5 + sin(21.1 * rt + ph[3]) * 0.5; + const gust = max(0, sin(1.3 * rt + ph[4]) + sin(2.9 * rt) * 0.6) ** 3 * 0.3; + const fx = LX + n1 * jit * 0.5 + gust * 6 * f, fy = LY - 1.2 * abs(n2) * jit * 0.4; + const glow = max(0.4, 1 - (0.12 + 0.4 * f) * (0.5 + 0.5 * n2) - 1.4 * gust * f - 0.15 * lick * (0.5 + 0.5 * sin(rt * 23))); + const green = lick * (0.6 + 0.4 * sin(rt * 7)); + // The light's colour on the paper, near the lamp and far from it. + let near = hsv(0.105 + s.hue, 0.42, 1); + const far = hsv(0.07 + s.hue, 0.74, 1); + const tox = [0.45, 1, 0.12]; + near = near.map((v, i) => mix(v, tox[i], green)); + M.fill(0); + // Shadows. Shapes are laid out where their shadows fall with the flame at + // rest and at the sleeper's depth; proj finds the shadow for the flame as + // it is and the depth given, blown up about the flame by 1 / (1 - depth), + // and how much bigger that is than the layout. + const proj = (x, y, dep) => { + const k = (1 - DEP) / (1 - dep), k0 = 1 / (1 - dep); + const wx = LX + (x - LX) * (1 - DEP), wy = LY + (y - LY) * (1 - DEP); + return [fx + (wx - fx) * k0, fy + (wy - fy) * k0, k]; + }; + const pen = (k) => 0.75 + 0.8 * (k - 1) + 0.5 * f; + // The sleeper under the quilt, breathing. + const br = 0.5 + 0.5 * sin(rt * 1.6); + const P1 = proj(31, 111.5 - 0.5 * br, DEP), P2 = proj(41, 109, DEP), P3 = proj(22, 112, DEP), P4 = proj(32, 115.2, DEP); + blob(M, P1[0], P1[1], 14 * P1[2], 4.2 * P1[2], 0.04, pen(P1[2])); + blob(M, P2[0], P2[1], 6 * P2[2], 3.6 * P2[2], -0.2, pen(P2[2])); + blob(M, P3[0], P3[1], 6 * P3[2], 3 * P3[2], 0, pen(P3[2])); + capsule(M, P4[0] - 21, P4[1], P4[0] + 23, P4[1], 1.6, pen(P4[2])); + const pl = proj(HDX + 1, HDY + 3.2, DEP); + blob(M, pl[0], pl[1], 4 * pl[2], 1.6 * pl[2], 0, pen(pl[2])); + // The neck: the road from the shoulders to the head, with a slow wave + // running along it, deeper toward the head while it licks the lamp. + const peep = s.peep, pp = peep < 0 ? 0 : sm(0, 0.5, peep) * (1 - sm(3.3, 3.8, peep)); + const u = headStep(s, t), nu = floor(u), deepH = DEP + 0.35 * lick; + const amp = 1.5 * min(1, u / 40) * (1 - 0.3 * abs(s.reach)); + const sh0 = proj(SHX, SHY, DEP); + let hx = HDX, hy = HDY, ox = sh0[0], oy = sh0[1]; + for (let i = 0; i <= nu + 1 && i < s.np; i++) { + const j = min(i, u); + const i0 = floor(j), i1 = min(i0 + 1, s.np - 1), fr = j - i0; + let x = mix(px[i0], px[i1], fr), y = mix(py[i0], py[i1], fr); + const a = max(0, i0 - 2), b = min(s.np - 1, i0 + 2), tx = px[b] - px[a], ty = py[b] - py[a], tl = sqrt(tx * tx + ty * ty) + 1e-6; + const wv = amp * sin(j * 0.26 - rt * 3.4) * sin((PI * j) / max(u, 1)); + x += (-ty / tl) * wv; + y += (tx / tl) * wv; + const dep = mix(DEP, deepH, sm(u - 9, u, j)); + const p = proj(x, y, dep); + capsule(M, ox, oy, p[0], p[1], 1.8 * p[2], pen(p[2])); + (ox = p[0]), (oy = p[1]); + (hx = x), (hy = y); + if (j >= u) break; + } + // The head: a profile with the big shimada knot of hair on top, the side + // locks, the tail of hair behind and two pins. It lies face up on the + // pillow, turns (narrowing as it turns) to face the way it goes, bows to + // the lamp, and turns to us before it comes to the paper. + const lying = 1 - sm(0, 8, u); + // She faces into the room, looks back once along the lintel, and looks + // the way she goes while the neck draws back. + const back = (t > 5.9 && t < 6.8) || (t > T_LICK + 0.3 && t < T_BACK - 0.6); + const want = lying > 0.5 ? -1 : (back ? 1 : -1) * (1 - 0.65 * pp); + s.fcs += (want - s.fcs) * 0.12; + const fs = s.fcs < 0 ? min(-0.3, s.fcs) : max(0.3, s.fcs), sg = fs < 0 ? -1 : 1; + const tilt = lying * 1.57 + sg * (0.85 * lick + 0.1 * sin(rt * 1.3) * (1 - lying)); + const hk = proj(hx, hy, deepH), K = hk[2] * (1 - 0.15 * pp), e = pen(K) * (1 - 0.5 * pp), ct = cos(tilt), st = sin(tilt); + const at = (dx, dy) => ((dx = (dx + 0.8 * HS) * fs), (dy -= 4.6 * HS), [hk[0] + (dx * ct - dy * st) * K, hk[1] + (dx * st + dy * ct) * K]); + const faceOn = peep >= 0 ? sm(0.75, 1.0, peep) * (1 - sm(3.2, 3.6, peep)) : 0; + if (faceOn < 0.5) { + for (const [dx, dy, rx, ry] of HEAD) { + const c = at(dx, dy); + blob(M, c[0], c[1], rx * K * abs(fs), ry * K, tilt, e); + } + for (const [a1, b1, a2, b2] of PINS) { + const c1 = at(a1, b1), c2 = at(a2, b2); + capsule(M, c1[0], c1[1], c2[0], c2[1], 0.5 * K, e); + } + } + // The tongue, lapping at the oil. + if (lick > 0.05) { + const tl = lick * (3.5 + 2.5 * abs(sin(rt * 9))), c1 = at(3.2 * HS, 2.6 * HS), c2 = at(3.6 * HS + tl * 0.5, 2.6 * HS + tl); + capsule(M, c1[0], c1[1], c2[0], c2[1], 0.5 * K, e); + } + // The torn hole and her face in it. + const hc = at(0, 0); + if (peep >= 0.5 && !s.holeLive) (s.hole = [cl(hc[0], PX0 + 9, PX1 - 9), cl(hc[1], PY0 + 9, PY1 - 9), (Math.random() * 1000) | 0]), (s.holeLive = 1); + if (peep < 0) s.holeLive = 0; + const tear = s.hole ? (s.holeLive ? sm(0.5, 0.8, peep) : 1) : 0; + // Light the paper, lay the shadow and the grid of the doors over it. + const lw = 1 / (52 * 52); + for (let q = 0; q < N; q++) { + const x = q & 63, y = q >> 6; + if (!PAP[q]) { + const o = q * 3, sp = y > PY1 + 3 && x > 2 && x < 61 ? 0.25 * (1 - (y - PY1 - 4) / 8) * glow : 0; + display.setPixel(x, y, 255 * (BG[o] + sp * far[0]), 255 * (BG[o + 1] + sp * far[1]), 255 * (BG[o + 2] + sp * far[2])); + continue; + } + const dx = x + 0.5 - fx, dy = y + 0.5 - fy, r2 = (dx * dx + dy * dy) * lw; + // The andon's own paper glows through, a soft bright box round the flame. + const box = sm(7.5, 5, abs(x + 0.5 - LX)) * sm(14, 10, abs(y + 0.5 - LY + 2)); + const fall = 1 / (1 + r2) ** 1.5, w = cl(fall * 1.3 - 0.3, 0, 1); + let lv = (0.86 + 0.2 * fall) * glow + 0.3 * box * glow; + const fib = 0.97 + 0.03 * hash(x, y >> 1); + let r = mix(far[0], near[0], w) * lv * fib, g = mix(far[1], near[1], w) * lv * fib, b = mix(far[2], near[2], w) * lv * fib; + // The andon's frame and legs, close behind the paper, in soft shadow. + const ax = abs(x + 0.5 - LX), ay = y + 0.5 - LY; + if ((abs(ax - 6) < 0.6 && ay > -13 && ay < 16) || (ax < 6.5 && (abs(ay + 13) < 0.6 || abs(ay - 9) < 0.6))) (r *= 0.45), (g *= 0.4), (b *= 0.45); + const m = M[q]; + r = mix(r, 0.04, m * 0.94); + g = mix(g, 0.015, m * 0.94); + b = mix(b, 0.05, m * 0.94); + // The hole: the dim room behind, her face when she is at it. + if (tear > 0) { + const hx2 = x + 0.5 - s.hole[0], hy2 = y + 0.5 - s.hole[1], hr = sqrt(hx2 * hx2 + hy2 * hy2); + const ang = atan2(hy2, hx2), bin = ((ang / TAU + 1) * 13) % 13, bi = floor(bin); + const rag = mix(hash(bi, s.hole[2]), hash((bi + 1) % 13, s.hole[2]), bin - bi); + const R = tear * (6.5 + 3 * rag); + if (hr < R) { + // The room behind, lit by the lamp, nearer the lamp the brighter. + const room = (0.3 + 0.45 * w) * glow, fcol = faceAt(hx2, hy2, rt, glow); + r = mix(far[0] * room, fcol[0], faceOn); + g = mix(far[1] * room * 0.8, fcol[1], faceOn); + b = mix(far[2] * room, fcol[2], faceOn); + } else if (hr < R + 1.2) (r = min(1, r * 0.4 + 0.62)), (g = min(1, g * 0.4 + 0.57)), (b = min(1, b * 0.4 + 0.48)); + } + // The lattice of the doors: a frame round each door, bars across and up. + const xl = x - PX0, yl = y - PY0; + const frame = x <= PX0 + 1 || x >= PX1 - 1 || x === 31 || x === 32 || y <= PY0 + 1 || y >= PY1 - 1; + const bar = frame || (x < 31 ? (xl - 1) % 8 === 0 : (x - 33) % 8 === 7) || yl % 14 === 13; + if (bar) { + const k = frame ? 0.5 : 0.3; + r = mix(r, 0.2, 0.8) * k + 0.05; + g = mix(g, 0.11, 0.8) * k + 0.02; + b = mix(b, 0.17, 0.8) * k + 0.06; + } + display.setPixel(x, y, r * 255, g * 255, b * 255); + } +} + +// Her face pressed to the hole: white skin, black hair parted in the middle, +// two dark eyes with a glint that look at us and now and then aside, red +// lips. Returns the colour. +function faceAt(x, y, rt, glow) { + const lk = sin(rt * 0.9) > 0.6 ? 1 : 0; + if (y < -4.2 + 0.25 * abs(x) * (y < -1 ? 1 : 0) || abs(x) > 6.8 - max(0, y - 3) * 0.6) { + const part = abs(x) < 0.6 && y < -4 ? 0.25 : 0; + return [0.04 + part, 0.03 + part, 0.05 + part]; + } + const l = (0.82 + 0.12 * cl(-x / 6, -1, 1)) * (0.8 + 0.2 * glow); + let c = [l, l * 0.93, l * 0.86]; + for (const ex of [-2.8, 2.8]) { + const u = (x - ex) / 1.9, v = (y + 0.6) / 0.85; + if (u * u + v * v < 1) c = x - ex - lk * 0.8 > -0.5 && x - ex - lk * 0.8 < 0.5 && y < -0.6 ? [0.95, 1, 0.9] : [0.05, 0.02, 0.04]; + } + if (abs(x) < 1.4 && abs(y - 3.6) < 0.7) c = [0.75, 0.06, 0.1]; + return c; +} From 6cf743167016c977dbcb2f80e2e594061646e19d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 01:52:41 +0000 Subject: [PATCH 06/14] =?UTF-8?q?53=ED=8C=90:=20Leapfrogging=20Vortices=20?= =?UTF-8?q?=EC=99=80=20Baba=20Yaga's=20Hut=20=ED=8C=A8=ED=84=B4=EC=9D=84?= =?UTF-8?q?=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_01Yb8D9UxrifXqrXju5CVbqY --- .../pattern-study/patterns/baba-yaga-hut.js | 455 ++++++++++++++++++ .../patterns/leapfrogging-vortices.js | 317 ++++++++++++ 2 files changed, 772 insertions(+) create mode 100644 incoming/pattern-study/patterns/baba-yaga-hut.js create mode 100644 incoming/pattern-study/patterns/leapfrogging-vortices.js diff --git a/incoming/pattern-study/patterns/baba-yaga-hut.js b/incoming/pattern-study/patterns/baba-yaga-hut.js new file mode 100644 index 000000000..3ac22b231 --- /dev/null +++ b/incoming/pattern-study/patterns/baba-yaga-hut.js @@ -0,0 +1,455 @@ +// ===== Patternflow pattern ===== +// Title: Baba Yaga's Hut +// Family: halloween +// Idea: In a moonlit birch forest a log hut on giant chicken legs paces behind a fence of skulls with glowing green eyes, knees bending backward at every step, swaying, crouching and turning round, while Baba Yaga crosses the moon in her mortar, steering with the pestle and sweeping her trail away with a broom. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 53, after Baba Yaga's hut on chicken legs in Slavic folklore +// Round: 53 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Legs=0..1, Fog=0..1 +// +// Knob 1: Hue. The night: violet at 0, then indigo, blue and teal to a sickly green at 1. The windows stay orange, the eyes green and the birches pale. +// Knob 2: Speed. At 2 the hut walks a step every 0.7 seconds and goes once round its round of pacing, turning and crouching in 14 seconds, and Baba Yaga crosses the moon every 12; at 0.2 all of it ten times slower. +// Knob 3: Legs. How long the legs are: at 0 they hold the floor of the hut 38 LEDs off the ground, at 1 they lift it 62, so the roof stands in front of the moon. +// Knob 4: Fog. Thin wisps over the ground at 0.06; turned up, a moonlit bank rises to the hut's knees, then hides the legs, the eyes of the skulls glowing green through it. +// Buttons: K1 tap: the hut turns its front to us, kneels and opens its door on the fire inside, then gets up and goes on. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, sqrt, sin, cos, exp, round, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +const mix = (a, b, t) => a + (b - a) * t; +const hash = (i) => { + let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} +const NT = new Float32Array(4096).map((_, i) => hash(i * 7 + 3)); +function vn(x, y) { + const xi = floor(x), yi = floor(y), fx = x - xi, fy = y - yi, ux = fx * fx * (3 - 2 * fx), uy = fy * fy * (3 - 2 * fy); + const i0 = xi & 63, i1 = (xi + 1) & 63, j0 = (yi & 63) << 6, j1 = ((yi + 1) & 63) << 6, a = NT[j0 + i0], b = NT[j0 + i1], c = NT[j1 + i0]; + return a + (b - a) * ux + (c - a) * uy + (a - b - c + NT[j1 + i1]) * ux * uy; +} + +// The row the feet stand on; the moon; the hut's half width across its +// front and half depth along its sides, the wall and roof heights, the +// eaves' overhang and the hips either side of its middle. +const GY = 106, MX = 21, MY = 24, MR = 17; +const A = 12, B = 15, WALL = 20, ROOF = 12, OVER = 1.5, HIP = 5.5; +// Birch trunks: middle, width, how near (1 near and bright), lean per row. +const BIRCH = [[3, 5, 0.9, 0.01], [13, 2.5, 0.5, -0.02], [36, 2, 0.4, 0.015], [42, 3, 0.62, -0.01], [51, 2.5, 0.45, 0.02], [60, 6, 1, -0.012], [27, 3.5, 0.7, 0.008]]; +const SKULLS = [7, 24, 41, 58]; +const groundAt = (x) => 103 + 1.2 * sin(x * 0.21) + 0.8 * sin(x * 0.53 + 1); +// The round the hut walks, in seconds at Speed 2: [length, facing from, to, +// middle from, to, how hard it steps, crouch]. Facing 0 is its door to us, +// 90 walking right. +const SEG = [ + [2.2, 90, 90, 24, 40, 1, 0], + [1.4, 90, 0, 40, 40, 0.8, 0], + [3.2, 0, 0, 40, 40, 0, 1], + [1.4, 0, -90, 40, 40, 0.8, 0], + [2.2, -90, -90, 40, 24, 1, 0], + [2.4, -90, -270, 24, 24, 0.8, 0], + [1.2, 90, 90, 24, 24, 0, 0], +]; +const LOOP = SEG.reduce((s, g) => s + g[0], 0), STEP = 1.4; + +export function setup(params) { + params.s = { + hue: -1, legs: 0, fog: 0.06, t: 0, ct: 0, call: 0, + phi: 90, X: 24, c: 0, dr: 0, act: 1, v: 0, gp: 0, + puffs: [], dust: [], fly: null, nf: 0.8, nfl: 0, + bg: new Float32Array(N * 3), F: new Float32Array(N * 3), fogc: [0, 0, 0], + }; +} + +// Sky, moon, birches and ground: fixed until the Hue knob moves. +function background(S) { + const h = 0.76 - 0.4 * S.hue, top = [0, 0, 0], low = [0, 0, 0], moon = [0.96, 0.93, 0.82], bark = [0, 0, 0], bg = S.bg; + hsv(h, 0.8, 0.13, top, 0); + hsv(h + 0.03, 0.45, 0.52, low, 0); + hsv(h, 0.12, 1, bark, 0); + hsv(h + 0.02, 0.35, 0.62, S.fogc, 0); + for (let y = 0; y < H; y++) { + const t = sm(0, 102, y) ** 1.2; + for (let x = 0; x < W; x++) { + const o = 3 * (y * W + x), xx = x + 0.5, yy = y + 0.5; + let r = mix(top[0], low[0], t), g = mix(top[1], low[1], t), b = mix(top[2], low[2], t); + const d = Math.hypot(xx - MX, yy - MY); + if (d < MR) { + // The moon's seas, and a little darkening at the limb. + const sea = sm(0.52, 0.62, vn(xx * 0.32 + 5, yy * 0.32 + 9)) * 0.16 + sm(MR - 3, MR, d) * 0.1; + (r = moon[0] * (1 - sea)), (g = moon[1] * (1 - sea)), (b = moon[2] * (1 - sea * 0.7)); + } else { + const k = floor(9 * max(0, 1 - (d - MR) / 11) ** 2) / 9 * 0.22; + (r += moon[0] * k), (g += moon[1] * k), (b += moon[2] * k); + if (y < 80 && hash(y * W + x + 999) < 0.006) (r += 0.5), (g += 0.48), (b += 0.55); + } + const gl = groundAt(xx); + for (let k = 0; k < BIRCH.length; k++) { + const [cx, w, near, lean] = BIRCH[k], u = (xx - cx - lean * (GY - yy)) / (w / 2); + if (abs(u) > 1 || yy > gl + 1) continue; + // Bark: pale and round, dark lenticels, black patches near the foot. + const L = (0.78 - 0.28 * u - 0.22 * u * u) * near, hk = hash(k * 977 + y), mk = hk < 0.12 + 0.3 * sm(60, 100, yy) && abs(u - (hash(k * 31 + y) - 0.5)) < 0.3 + hk * 2; + const v = mk ? 0.1 + 0.05 * near : L; + (r = bark[0] * v), (g = bark[1] * v), (b = bark[2] * v); + } + if (yy > gl) { + // The forest floor, its edge catching the moon, and dead leaves. + const e = sm(gl + 2.5, gl, yy), q = hash(y * 131 + x); + (r = 0.06 + 0.12 * e + (q < 0.1 ? 0.1 : 0)), (g = 0.04 + 0.08 * e + (q < 0.1 ? 0.04 : 0)), (b = 0.07 + 0.12 * e); + const f = 1 - 0.4 * sm(105, 128, yy); + (r *= f), (g *= f), (b *= f); + } + (bg[o] = r), (bg[o + 1] = g), (bg[o + 2] = b); + } + } + S.hb = S.hue; +} + +let NX = 0, NY = 0; +// Distance to a capsule a-b of radius r0 at a and r1 at b; (NX, NY) the outward normal. +function cap(px, py, ax, ay, bx, by, r0, r1) { + const ex = bx - ax, ey = by - ay, qx = px - ax, qy = py - ay, u = cl((qx * ex + qy * ey) / (ex * ex + ey * ey + 1e-9), 0, 1), dx = qx - u * ex, dy = qy - u * ey, d = sqrt(dx * dx + dy * dy) + 1e-9; + (NX = dx / d), (NY = dy / d); + return d - mix(r0, r1, u); +} +function poly(px, py, v) { + let dd = 1e9, s = 1; + for (let i = 0, j = v.length - 2; i < v.length; j = i, i += 2) { + const ex = v[j] - v[i], ey = v[j + 1] - v[i + 1], wx = px - v[i], wy = py - v[i + 1], u = cl((wx * ex + wy * ey) / (ex * ex + ey * ey), 0, 1), bx = wx - ex * u, by = wy - ey * u; + dd = min(dd, bx * bx + by * by); + const c1 = py >= v[i + 1], c2 = py < v[j + 1], c3 = ex * wy > ey * wx; + if ((c1 && c2 && c3) || (!c1 && !c2 && !c3)) s = -s; + } + return s * sqrt(dd); +} +function put(F, x, y, r, g, b, a) { + if (x < 0 || x >= W || y < 0 || y >= H || a <= 0) return; + const o = 3 * (y * W + x); + a = min(1, a); + (F[o] += (r - F[o]) * a), (F[o + 1] += (g - F[o + 1]) * a), (F[o + 2] += (b - F[o + 2]) * a); +} + +function choreo(ct) { + let t = ct % LOOP, k = 0; + while (t > SEG[k][0]) (t -= SEG[k][0]), k++; + const [d, p0, p1, x0, x1, act, cr] = SEG[k], f = t / d; + return { phi: mix(p0, p1, sm(0, 1, f)), X: mix(x0, x1, f), act, cr: cr * sin(PI * f) ** 2, v: (x1 - x0) / d }; +} +const toward = (v, t, r) => (v < t ? min(t, v + r) : max(t, v - r)); + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = cl(k[0], 0, 1); + S.legs = cl(k[2], 0, 1); + S.fog = cl(k[3], 0, 1); + const ds = (d * cl(k[1], 0.2, 3)) / 2, first = S.t === 0; + S.t += ds; + if ((input.btnPressed || [])[0] && S.call <= 0) S.call = 7; + let T; + if (S.call > 0) { + // Turn the front to us, kneel, open the door; then get up and go on. + S.call -= ds; + const face = 360 * round(S.phi / 360), there = abs(S.phi - face) < 8; + T = { phi: face, X: S.X, act: there ? 0 : 0.8, cr: there && S.call > 1.6 ? 1 : 0, v: 0 }; + S.dr = toward(S.dr, S.c > 0.85 && S.call > 2.2 ? 1 : 0, ds / 0.7); + } else { + S.ct += ds; + T = choreo(S.ct); + S.dr = toward(S.dr, 0, ds / 0.7); + } + const tp = T.phi + 360 * round((S.phi - T.phi) / 360); + if (first) (S.phi = tp), (S.X = T.X); + S.phi = toward(S.phi, tp, 160 * ds); + S.X = toward(S.X, T.X, 12 * ds); + S.c = toward(S.c, T.cr, 1.6 * ds); + S.act = toward(S.act, T.act, 2.5 * ds); + S.v = toward(S.v, T.v, 20 * ds); + S.gp += ds / STEP; + // Smoke from the chimney, swelling and drifting downwind. + if (S.cx !== undefined && Math.random() < ds * 9) S.puffs.push({ x: S.cx + Math.random() - 0.5, y: S.cy, a: 0, s: Math.random() }); + for (const p of S.puffs) (p.a += ds), (p.y -= (5 + 2 * p.s) * ds), (p.x += (2.4 + 1.5 * sin(S.t * 0.4 + p.s * 3)) * ds); + S.puffs = S.puffs.filter((p) => p.a < 4.5 && p.y > -8); + // Baba Yaga's flights across the moon, one way and then the other. + if (!S.fly && (S.nf -= ds) <= 0) (S.fly = { dir: S.nfl % 2 ? -1 : 1, a: 0 }), S.nfl++; + const f = S.fly; + if (f) { + f.a += ds; + const x = f.dir > 0 ? -22 + 16 * f.a : W + 22 - 16 * f.a, y = MY + 1 + 2.5 * sin(f.a * 1.7) - 0.08 * f.dir * (x - MX); + (f.x = x), (f.y = y); + // The broom sweeps her track away in a little dust. + for (let e = ds * 30; e > 0; e--) if (e >= 1 || Math.random() < e) S.dust.push({ x: x - f.dir * 20 + 2 * Math.random(), y: y + 2 + 4 * Math.random(), vx: -f.dir * (2 + 4 * Math.random()), vy: 1 + 3 * Math.random(), a: 0 }); + if (f.a * 16 > W + 46) (S.fly = null), (S.nf = 12 - f.a); + } + for (const p of S.dust) (p.a += ds), (p.x += p.vx * ds), (p.y += p.vy * ds), (p.vx *= 1 - ds); + S.dust = S.dust.filter((p) => p.a < 1.6); +} + +// Leg i of the hut: joints in the panel, from the hip to the toes. +function leg(S, i, yHip, Hh, Lmax, cp, sp, out) { + const p = (S.gp + 0.5 * i) % 1, act = S.act, stride = abs(S.v) * 0.6 * STEP; + let f, lift; + if (p < 0.6) (f = stride * (0.5 - p / 0.6)), (lift = 0); + else { + const q = (p - 0.6) / 0.4; + (f = stride * (sm(0, 1, q) - 0.5)), (lift = (3 + 5 * S.legs) * act * sin(PI * q)); + } + const lat = i ? HIP : -HIP, L1 = 0.6 * Lmax, L2 = 0.68 * Lmax, dv = Hh - lift; + const D = Math.hypot(f, dv) + 1e-6, dd = cl(D, abs(L1 - L2) + 0.5, L1 + L2 - 0.3); + const a = (L1 * L1 - L2 * L2 + dd * dd) / (2 * dd), h = sqrt(max(0, L1 * L1 - a * a)), uf = f / D, uv = dv / D; + // The joint bends backward: it sits behind the line from hip to foot. + const jf = a * uf - h * uv, jv = a * uv + h * uf, X = (l, fw) => S.X + l * cp + fw * sp; + (out[0] = X(lat, 0)), (out[1] = yHip), (out[2] = X(lat, jf)), (out[3] = yHip + jv), (out[4] = X(lat, f)), (out[5] = yHip + dv); + out[6] = sm(0, 3, lift); + out[7] = lat * sp - f * cp; +} + +const TOES = [[-0.5, 0.87, 9], [0, 1, 10], [0.5, 0.87, 9], [0, -1, 4.5]]; +function drawLeg(S, F, P, cp, sp) { + const [hx, hy, jx, jy, fx, fy, curl] = P, tx = [], ty = [], jl = Math.hypot(jx - hx, jy - hy) + 1e-6; + for (const [l, fw, len] of TOES) { + const k = len * (1 - 0.45 * curl), dz = l * sp - fw * cp; + tx.push(fx + k * (l * cp + fw * sp)), ty.push(fy + (0.35 * -dz * k) / len + 3 * curl); + } + const x0 = floor(min(hx, jx, fx) - 11), x1 = floor(max(hx, jx, fx) + 11), y0 = floor(hy - 2), y1 = floor(max(fy, ...ty) + 2); + for (let y = max(0, y0); y <= min(H - 1, y1); y++) + for (let x = max(0, x0); x <= min(W - 1, x1); x++) { + const px = x + 0.5, py = y + 0.5; + // The thigh: a big feathered drumstick, scalloped down its edge. + const u = cl(((px - hx) * (jx - hx) + (py - hy) * (jy - hy)) / (jl * jl), 0, 1); + let best = cap(px, py, hx, hy, jx, jy, 5.2, 2.6) - 0.9 * max(0, sin(u * 16 + 1)), nx = NX, ny = NY, r, g, b; + const tip = best > -1.2 || hash(y * 64 + x) < 0.22; + (r = tip ? 0.29 : 0.14), (g = tip ? 0.21 : 0.095), (b = tip ? 0.17 : 0.085); + // The shank: scaly and yellowish, with a spur, and knobbly joints. + const e = min(cap(px, py, jx, jy, fx, fy, 2.2, 1.7), cap(px, py, mix(jx, fx, 0.55), mix(jy, fy, 0.55), mix(jx, fx, 0.55) + (jx - hx) * 0.12, mix(jy, fy, 0.55) + 1, 0.9, 0.4)); + if (e < best) { + const sc = (Math.hypot(px - jx, py - jy) * 0.7 + (px - fx) * 0.2) % 1 < 0.35 ? 0.75 : 1; + (best = e), (nx = NX), (ny = NY), (r = 0.4 * sc), (g = 0.28 * sc), (b = 0.07 * sc); + } + const kn = min(cap(px, py, jx, jy, jx, jy, 3, 3), cap(px, py, fx, fy, fx, fy, 2.1, 2.1)); + if (kn < best) (best = kn), (nx = NX), (ny = NY), (r = 0.3), (g = 0.2), (b = 0.08); + for (let t = 0; t < 4; t++) { + const c = cap(px, py, fx, fy, tx[t], ty[t], 1.4, 0.7); + if (c < best) { + (best = c), (nx = NX), (ny = NY); + const claw = Math.hypot(px - tx[t], py - ty[t]) < 2; + (r = claw ? 0.66 : 0.36), (g = claw ? 0.62 : 0.25), (b = claw ? 0.52 : 0.07); + } + } + if (best > 0.7) continue; + // Moonlight from the upper left catches the edges facing it. + const rim = max(0, -0.6 * nx - 0.8 * ny) * sm(-1.8, 0, best) * 0.4; + put(F, x, y, r + rim * 0.7, g + rim * 0.65, b + rim * 0.8, sm(0.7, -0.3, best)); + } +} + +// The hut: walls of logs, windows and door on the faces turned to us, the +// gable roof, its gable ends and the chimney. +function hut(S, F, ySill, cp, sp, sway, flick) { + const yE = ySill - WALL, yR = yE - ROOF, P = (l, z) => l * cp + z * sp; + const faces = []; + if (cp > 0.02) faces.push([-A, B, A, B, sp, 0]); + if (cp < -0.02) faces.push([A, -B, -A, -B, -sp, 1]); + if (sp < -0.02) faces.push([A, B, A, -B, cp, 2]); + if (sp > 0.02) faces.push([-A, -B, -A, B, -cp, 2]); + for (const fc of faces) (fc[6] = P(fc[0], fc[1])), (fc[7] = P(fc[2], fc[3])); + const eav = [P(-A - OVER, -B - OVER), P(A + OVER, -B - OVER), P(A + OVER, B + OVER), P(-A - OVER, B + OVER)], rid = [P(0, -B - OVER), P(0, B + OVER)]; + const eL = min(...eav), eR = max(...eav), rL = min(...rid), rR = max(...rid), cx = P(0, -6); + const gb = cp > 0.02 ? [P(-A, B), P(A, B), P(0, B)] : cp < -0.02 ? [P(A, -B), P(-A, -B), P(0, -B)] : null; + (S.cx = S.X + cx + sway * (ySill - yR + 6)), (S.cy = yR - 6); + for (let y = max(0, floor(yR - 7)); y <= min(H - 1, floor(ySill)); y++) { + const yy = y + 0.5, sh = S.X + sway * (ySill - yy); + for (let x = 0; x < W; x++) { + const xr = x + 0.5 - sh; + let col = 0, cr = 0, cg = 0, cb = 0; + if (yy > yE && yy <= ySill + 0.5) { + for (const [, , , , nx, kind, x0, x1] of faces) { + if (xr < x0 || xr >= x1) continue; + const u = (xr - x0) / (x1 - x0), wpx = x1 - x0, lv = 0.8 - 0.3 * nx, hv = ySill - yy, lg = hv % 3, row = floor(hv / 3); + // Logs: a lit top, a dark gap, and round log ends at the corners. + let r = 0.15, g = 0.095, b = 0.08; + if (lg > 2.2) (r = 0.3), (g = 0.21), (b = 0.17); + if (lg < 0.7) (r = 0.05), (g = 0.03), (b = 0.035); + const end = (u * wpx < 1.5 || (1 - u) * wpx < 1.5) && (row + (kind >> 1)) % 2 === 0; + if (end) (r = 0.36), (g = 0.26), (b = 0.19); + (r *= lv), (g *= lv), (b *= lv); + // Windows: carved frames round orange light and a dark cross; the door. + const w0 = kind ? 0.37 : 0.58, ww = (kind ? 0.26 : 0.28) * wpx, wu = (u - w0) / (ww / wpx), wy = (yy - yE - 5) / 6; + if (wu >= 0 && wu < 1 && wy >= 0 && wy < 1 && ww > 1.2) { + const fr = wu * ww < 1 || (1 - wu) * ww < 1 || wy < 0.17 || wy > 0.83; + const cross = abs(wu - 0.5) * ww < 0.5 || abs(wy - 0.5) < 0.09, gl = flick * (1 - 0.5 * ((wu - 0.5) ** 2 + (wy - 0.5) ** 2)); + if (fr) (r = 0.45), (g = 0.35), (b = 0.27); + else if (cross) (r = 0.16), (g = 0.07), (b = 0.03); + else (r = gl), (g = 0.6 * gl * gl), (b = 0.14 * gl * gl); + } + if (kind === 0 && u >= 0.12 && u < 0.46 && hv < 15) { + const du = (u - 0.14) / 0.3, open = du > 1 - S.dr && du < 1 && hv < 14; + if (open) { + // Firelight from the stove: yellow low in the doorway, deep orange above. + const gl = flick * (1 - 0.35 * (hv / 14)), k = gl * gl; + (r = gl), (g = 0.28 + 0.4 * k - 0.1 * (hv / 14)), (b = 0.04 + 0.16 * k * k); + } else if (du < 0 || du >= 1 || hv >= 14) (r = 0.46), (g = 0.36), (b = 0.27); + else { + const plank = ((du / max(0.05, 1 - S.dr)) * 4) % 1 < 0.2; + (r = plank ? 0.1 : 0.3), (g = plank ? 0.06 : 0.19), (b = plank ? 0.04 : 0.12); + if (abs(du - 0.8 * (1 - S.dr)) * 0.3 * wpx < 0.6 && abs(hv - 7) < 0.6) (r = 0.7), (g = 0.6), (b = 0.35); + } + } + (col = 1), (cr = r), (cg = g), (cb = b); + } + } + if (yy > yR && yy <= yE + 1.2) { + // The roof: hull of the eaves and the ridge; thatch in rows. + const f = (yy - yR) / (yE - yR), xl = mix(rL, eL, f), xrr = mix(rR, eR, f); + if (xr >= xl && xr < xrr) { + const q = hash(y * 71 + floor(xr * 2) + 5000), lv = 0.85 - 0.3 * ((xr - xl) / max(1, xrr - xl)); + // Straw in rows, combed down the slope, the ridge and eaves catching light. + let v = ((xr * 0.8 + (yy - yR) * 0.5) % 1.6 < 0.5 ? 0.12 : 0.24 + 0.08 * q) * ((yy - yR) % 4 < 0.9 ? 0.6 : 1); + if (yy > yE || yy < yR + 1) v = 0.36; + (col = 1), (cr = v * 1.0 * lv), (cg = v * 0.92 * lv), (cb = v * 0.78 * lv); + // A gable end turned to us: boards, and a round attic window. + if (gb) { + const gl = mix(gb[2], gb[0], f), gr = mix(gb[2], gb[1], f); + if (xr >= gl && xr < gr) { + const bd = ((xr - gb[0]) * 0.7) % 1 < 0.25 ? 0.33 : 1; + (cr = 0.21 * bd), (cg = 0.14 * bd), (cb = 0.1 * bd); + if (Math.hypot((xr - gb[2]) / max(0.2, abs(cp)), yy - yE + ROOF * 0.38) < 1.7) (cr = flick), (cg = 0.55 * flick), (cb = 0.12); + } + } + } + } + if (yy > yR - 6 && yy < yR + 1.5 && abs(xr - cx) < 1.6) (col = yy < yR - 5 ? 1.6 : 1), (cr = 0.2 * col), (cg = 0.12 * col), (cb = 0.1 * col); + if (col) put(F, x, y, cr, cg, cb, 1); + } + } +} + +// Baba Yaga in her mortar, facing +x with y down: mortar, hunched body, +// head in a kerchief, long nose, the pestle she steers with and the broom. +function yaga(S, F) { + const f = S.fly, sc = 1.3, w = sin(f.a * 7) * 1.2, pw = 0.55 + 0.35 * sin(f.a * 3.4), pc = cos(pw), ps = sin(pw); + const sh = [ + ["p", [-4.2, 0.6, 4.2, 0.6, 3, 6, 1.8, 6.6, -1.8, 6.6, -3, 6]], + ["c", -4.8, -0.2, 4.8, -0.2, 0.7, 0.7], + ["p", [-2.2, -0.5, -3.2, -3.5, -2.6, -6.5, -0.8, -8.6, 1.6, -9.4, 3, -8, 2.4, -5, 2.2, -0.5]], + ["c", 3, -10.6, 3, -10.6, 1.9, 1.9], + ["p", [1.6, -12.2, -2.2, -11.6 + w, 1.2, -9.8]], + ["p", [4.6, -11.2, 7.2, -9.2, 4.6, -9.7]], + ["c", 1.6, -7.2, 5.4, -6.6, 0.75, 0.75], + ["c", 5.4 - 2 * ps, -6.6 + 2 * pc, 5.4 + 8 * ps, -6.6 - 8 * pc, 0.6, 1.4], + ["c", -1.5, -6, -3.5, -3.5, 0.7, 0.7], + ["c", -3.5, -3.5, -12, 0, 0.5, 0.5], + ["p", [-11.5, -0.6, -18, -1.6 + w, -18.5, 3 + w, -12, 1.6]], + ]; + for (let y = max(0, floor(f.y - 21)); y < min(H, f.y + 11); y++) + for (let x = max(0, floor(f.x - 26)); x < min(W, f.x + 26); x++) { + const lx = ((x + 0.5 - f.x) / sc) * f.dir, ly = (y + 0.5 - f.y) / sc; + let d = 1e9; + for (const s of sh) d = min(d, s[0] === "c" ? cap(lx, ly, s[1], s[2], s[3], s[4], s[5], s[6]) : poly(lx, ly, s[1])); + d *= sc; + // The mortar's rim catches a thread of moonlight between it and her. + const rim = abs(ly + 0.45) < 0.32 && abs(lx) < 4.5 ? 0.22 : 0; + if (d < 0.6) put(F, x, y, 0.035 + rim, 0.02 + rim * 0.9, 0.05 + rim, sm(0.6, -0.4, d)); + } +} + +function skulls(S, F) { + const eg = [0.5, 1, 0.2]; + // The rail between the posts: bones lashed end to end. + for (let x = 0; x < W; x++) for (const y of [120, 121]) put(F, x, y, 0.36, 0.33, 0.28, y === 120 ? 0.9 : 0.6); + SKULLS.forEach((sx, k) => { + // Each pair of eyes flickers on its own, and now and then goes dim. + const fl = 0.6 + 0.4 * vn(S.t * 5 + k * 13, k * 7) - 0.45 * sm(0.72, 0.84, vn(S.t * 0.7 + k * 5, 30)); + for (let y = 106; y < H; y++) + for (let x = max(0, sx - 6); x < min(W, sx + 6); x++) { + const px = x + 0.5, py = y + 0.5, dx = abs(px - sx); + // A bone post with knobs, the skull's dome, cheekbones and jaw. + const post = min(cap(px, py, sx, 117, sx, 128, 1.5, 1.5), cap(px, py, sx, 124, sx, 124, 2.1, 2.1)); + const sk = min(cap(px, py, sx, 111.5, sx, 111.5, 4.4, 4.4), cap(px, py, sx - 2.3, 115.2, sx + 2.3, 115.2, 1.6, 1.6), cap(px, py, sx - 3.1, 113.4, sx + 3.1, 113.4, 1.2, 1.2)); + const d = min(post, sk), lv = 0.66 + (0.28 * (sx - px)) / 4; + const so = Math.hypot((dx - 1.85) / 1.25, (py - 112.2) / 1.15), eye = Math.hypot(dx - 1.85, py - 112.3); + if (d < 0.5) { + let r = 0.84 * lv, g = 0.8 * lv, b = 0.68 * lv; + if (sk < 0.5) { + // Dark sockets with a green coal in each, the nose and the teeth. + if (so < 1) (r *= 0.08), (g *= 0.08), (b *= 0.1); + if (eye < 0.85) (r = eg[0] * fl), (g = eg[1] * fl), (b = eg[2] * fl); + if (Math.hypot(px - sx, py - 114.3) < 0.8 || (abs(py - 116.1) < 0.5 && x % 2 === 0 && dx < 2.4)) (r *= 0.2), (g *= 0.2), (b *= 0.25); + } + put(F, x, y, r, g, b, sm(0.5, -0.5, d)); + } + const gl = 0.4 * fl * exp(-(eye * eye) / 3.5); + if (gl > 0.01) { + const o = 3 * (y * W + x); + (F[o] += eg[0] * gl), (F[o + 1] += eg[1] * gl), (F[o + 2] += eg[2] * gl); + } + } + }); +} + +export function draw(display, params) { + const S = params.s; + if (!S || S.t === 0) return; + if (S.hb !== S.hue) background(S); + const F = S.F, t = S.t; + F.set(S.bg); + const Lmax = 38 + 24 * S.legs, Hh = Lmax * (1 - 0.62 * S.c), bob = 1.1 * S.act * cos(4 * PI * S.gp), ySill = GY - Hh + bob; + const cp = cos((S.phi * PI) / 180), sp = sin((S.phi * PI) / 180), sway = 0.035 * S.act * sin(2 * PI * S.gp) + 0.02 * sin(t * 0.9) * (1 - S.act); + const flick = 0.88 + 0.12 * vn(t * 6, 3); + // Smoke, lit by the moon. + for (const p of S.puffs) { + const R = 1.6 + 1.6 * p.a, al = 0.65 * sm(0, 0.3, p.a) * (1 - p.a / 4.5); + for (let y = max(0, floor(p.y - R)); y < min(H, p.y + R); y++) + for (let x = max(0, floor(p.x - R)); x < min(W, p.x + R); x++) { + const q = 1 - ((x + 0.5 - p.x) ** 2 + (y + 0.5 - p.y) ** 2) / (R * R); + if (q > 0) put(F, x, y, 0.62, 0.6, 0.7, al * q); + } + } + if (S.fly) yaga(S, F); + for (const p of S.dust) put(F, floor(p.x), floor(p.y), 0.75, 0.72, 0.85, 0.8 * (1 - p.a / 1.6)); + // The hut's shadow on the ground, cast away from the moon. + for (let y = GY - 2; y < GY + 4; y++) + for (let x = 0; x < W; x++) { + const q = ((x + 0.5 - S.X - 7) / 17) ** 2 + ((y + 0.5 - GY - 0.5) / 2.6) ** 2; + if (q < 1 && y + 0.5 > groundAt(x + 0.5)) put(F, x, y, 0, 0, 0, 0.45 * (1 - q) * (1 - 0.5 * S.c)); + } + hut(S, F, ySill, cp, sp, sway, flick); + const L0 = [], L1 = []; + leg(S, 0, ySill, Hh, Lmax, cp, sp, L0); + leg(S, 1, ySill, Hh, Lmax, cp, sp, L1); + for (const P of L0[7] > L1[7] ? [L0, L1] : [L1, L0]) drawLeg(S, F, P, cp, sp); + // The open door throws firelight down onto the ground. + const dx = S.X - 5 * cp; + if (S.dr > 0.02) + for (let y = floor(ySill); y < min(H, GY + 6); y++) { + const hw = 4 + (9 * (y - ySill)) / max(4, GY - ySill); + for (let x = max(0, floor(dx - hw)); x < min(W, dx + hw); x++) { + const o = 3 * (y * W + x), a = 0.22 * S.dr * flick * (1 - abs(x + 0.5 - dx) / hw) * (y > groundAt(x) ? 1.6 : 0.6); + (F[o] += a), (F[o + 1] += 0.55 * a), (F[o + 2] += 0.15 * a); + } + } + skulls(S, F); + // Ground fog: wisps at 0.06, a bank up to the hut's knees and higher as Fog turns. + const fz = S.fog, top = GY - 72 * fz, fc = S.fogc; + for (let y = max(0, floor(top)); y < H; y++) { + const yy = y + 0.5, base = sm(top, top + 10 + 20 * fz, yy); + for (let x = 0; x < W; x++) { + const n = vn(x * 0.09 + t * 0.35, yy * 0.22 + t * 0.05) * 0.7 + vn(x * 0.21 - t * 0.6, yy * 0.4) * 0.3; + const a = base * sm(0.42 - 0.2 * fz, 0.8, n) * (0.35 + 0.5 * fz); + if (a > 0.01) put(F, x, y, fc[0], fc[1], fc[2], a); + } + } + for (let i = 0, o = 0; i < N; i++, o += 3) display.setPixel(i & 63, i >> 6, cl(F[o] * 255, 0, 255) | 0, cl(F[o + 1] * 255, 0, 255) | 0, cl(F[o + 2] * 255, 0, 255) | 0); +} diff --git a/incoming/pattern-study/patterns/leapfrogging-vortices.js b/incoming/pattern-study/patterns/leapfrogging-vortices.js new file mode 100644 index 000000000..e3e4bff74 --- /dev/null +++ b/incoming/pattern-study/patterns/leapfrogging-vortices.js @@ -0,0 +1,317 @@ +// ===== Patternflow pattern ===== +// Title: Leapfrogging Vortices +// Family: sim +// Idea: Two smoke rings travel up the panel one behind the other, seen in a slice through their axis as two pairs of dye spirals, and the rear ring keeps shrinking, darting through the front one and widening again while the front one swells, slows and falls behind. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: leapfrogging vortex rings (Hermann von Helmholtz, 1858), as two pairs of point vortices (Walter Gröbli, 1877; A. E. H. Love, 1894), pattern round 53 +// Round: 53 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Ratio=3..8, Dye=0..1 +// +// Knob 1: Hue. Turns both dyes round the wheel together: at 0 the ring that starts in front is emerald and the one behind it magenta. +// Knob 2: Speed. At 2 a whole leapfrog, each ring passing through the other once, takes 7.9 seconds at the start ratio and the pair climbs 67 LEDs in it; at 0.2 ten times slower. +// Knob 3: Ratio. How many times wider the big ring is than the small one at the moment the small one passes through it. At 3 the rings leapfrog every 7.9 seconds; at 5 every 41, drifting up to 53 LEDs apart between passes. Love's threshold is 1 / (3 - 2 sqrt 2) = 5.83. From about 5.6 the rings part further than the panel before they turn back, and above 5.83 the small ring darts through once and walks away for good; either way a fresh pair is launched. +// Knob 4: Dye. How much dye fills the rings' bubbles of water: at the start the spirals in the cores and a thin wake; turned up, two full smoke rings and a long braided wake. +// Buttons: K1 tap lays fresh dye on both rings. + +// Each ring is a pair of point vortices of circulation G in the plane +// through the axis, one on each side, spinning opposite ways. A ring is the +// same all round its axis, so only the right-hand vortex of each pair is +// integrated and its mirror image across the axis carries -G. The slice is +// four point vortices, and the motion is the one Gröbli and Love worked +// out: when the small pair passes through the big one, if it is wider than +// 3 - 2 sqrt 2 = 0.17157 of the big one the two keep leapfrogging, and if it +// is narrower they part. The threshold is where the energy of the four +// vortices at that moment equals the most that two pairs far apart can +// have with the same impulse, so above it they can never get far apart. +// +// Checked in a separate run (G = 1, the big pair's half width 1, both +// level at the start, RK4): at 0.17157 the pairs walked away, 2000 apart at +// t = 1.3e6; at 0.1716 they came level again at t = 38545, at 0.172 at 2433, +// at 0.175 at 297. Near the threshold that half period grows as +// 1 / (ratio - 0.17157). Integrating all four vortices with no mirror +// imposed and one nudged by 1e-9, the leapfrog broke its mirror symmetry at +// every ratio up to 0.38 (at 0.3 by t = 62) and held from 0.385, as Acheson +// (2000) found with 0.382. A ring that stays round cannot do that, hence the +// mirror. With this file's integrator over 600 seconds at Speed 2 the energy +// of the four vortices drifted by 2.6e-12 of itself and the impulse along the +// axis by 3e-14, at every ratio from 3 to 5.5. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, sqrt, sin, cos, exp, abs, log, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const rnd = () => Math.random(); +// The big ring's half width when the small one is level with it, in LEDs; +// circulation per vortex in LED^2 per second at Speed 2, divided by 2 pi; +// the panel row the rings' middle is held on; dye particles per ring +// (right half, drawn mirrored); the separation at which a pair is replaced. +const A = 22, GAM = 820, G = GAM / (2 * PI), ROW = 52, NPR = 2600, LIM = 104; +// The vortex step, and the longest dye step, in seconds. +const HV = 0.004, HD = 0.018; +// Dye brightness: density scale, and the share under which dye is only haze. +const S0 = 2.2, D0 = 0.06; +const JIT = new Uint8Array(N).map((_, p) => h32(p, 77) & 63); + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} +function h32(i, j) { + let h = (Math.imul(i, 374761393) + Math.imul(j, 668265263)) | 0; + h = Math.imul(h ^ (h >>> 13), 1274126177); + return (h ^ (h >>> 16)) >>> 0; +} + +// Velocity of the two right-hand vortices (x, y up) from the other three. +const K1 = new Float64Array(4), K2 = new Float64Array(4), K3 = new Float64Array(4), K4 = new Float64Array(4), TV = new Float64Array(4); +function vvel(V, o) { + for (let k = 0; k < 2; k++) { + const x = V[2 * k], y = V[2 * k + 1], j = 2 - 2 * k, xj = V[j], dy = y - V[j + 1]; + const dx = x - xj, r1 = dx * dx + dy * dy, ex = x + xj, r2 = ex * ex + dy * dy; + o[2 * k] = G * (dy / r1 - dy / r2); + o[2 * k + 1] = G * (0.5 / x - dx / r1 + ex / r2); + } +} +function vstep(V, h) { + vvel(V, K1); + for (let i = 0; i < 4; i++) TV[i] = V[i] + 0.5 * h * K1[i]; + vvel(TV, K2); + for (let i = 0; i < 4; i++) TV[i] = V[i] + 0.5 * h * K2[i]; + vvel(TV, K3); + for (let i = 0; i < 4; i++) TV[i] = V[i] + h * K3[i]; + vvel(TV, K4); + for (let i = 0; i < 4; i++) V[i] += (h / 6) * (K1[i] + 2 * K2[i] + 2 * K3[i] + K4[i]); +} + +export function setup(params) { + const F = (n) => new Float32Array(n); + params.s = { + V: new Float64Array(4), V0: new Float64Array(4), Vm: new Float64Array(4), + yc: 0, ratio: -1, want: 3, wait: 0, go: 0, gain: 1, + hue: -1, dye: 0.06, d2: 4, nact: NPR, + px: F(2 * NPR), py: F(2 * NPR), age: F(2 * NPR), life: F(2 * NPR), pt: [0, 0], pp: [0, 0], + M: F(2 * N), M2: F(2 * N), DA: F(N), DB: F(N), col: F(6), + }; +} + +// Dye. Each ring has NL particles that are laid as straight lines across +// its core, half of them every PULSE seconds, so each line lives two pulses +// and winds into a spiral, the inside turning faster than the outside. The +// rest fill the ring's bubble of water; what the rings shed of it trails +// behind as the wake, and comes back into a bubble when it leaves the panel. +const NL = 360, CL = NL / 2, PULSE = 1.3; +function line(S, k) { + const V = S.V, a = V[2 * k], R = 0.62 * a, th = rnd() * PI, c = cos(th), s = sin(th), par = (S.pp[k] ^= 1); + for (let m = 0; m < CL; m++) { + const i = k * NPR + 2 * m + par, r = R * (((m + 0.5) / CL) * 2 - 1); + S.px[i] = abs(a + r * c); + S.py[i] = V[2 * k + 1] + r * s; + S.age[i] = 0; + S.life[i] = 2 * PULSE; + } +} +function bubble(S, i, spread) { + const k = i < NPR ? 0 : 1, V = S.V, a = V[2 * k]; + let x, y; + do (x = rnd()), (y = 2 * rnd() - 1); + while (x * x + y * y > 1); + S.px[i] = 1.5 * a * x; + S.py[i] = V[2 * k + 1] + 1.8 * a * y; + S.age[i] = spread ? -0.5 * rnd() : 0; + S.life[i] = 9 + 12 * rnd(); +} + +// A fresh pair: level with the small ring inside the big one, then run +// backwards until the small one is furthest behind (or LIM / 2 behind, if +// they would part for good), so it shrinks and darts through first. +function launch(S, ratio) { + const V = S.V; + V[0] = A, V[1] = 0, V[2] = A / ratio, V[3] = 0; + let last = 0; + for (let t = 0; t < 60; t += HV * 4) { + vstep(V, -HV * 4); + const sep = abs(V[1] - V[3]); + if (sep < last || sep > LIM * 0.42) break; + last = sep; + } + const m = (V[1] + V[3]) / 2; + V[1] += S.yc - m, V[3] += S.yc - m; + S.ratio = ratio; + fresh(S); + // A second and a bit, so the first lines are already spirals. + for (let i = 0; i < 70; i++) advance(S, HD); +} + +function fresh(S) { + for (let k = 0; k < 2; k++) { + S.pt[k] = PULSE * (0.5 + 0.5 * k); + S.age.fill(9, k * NPR, k * NPR + NL); + line(S, k); + for (let i = k * NPR + NL; i < (k + 1) * NPR; i++) bubble(S, i, 1); + } +} + +// Velocity of the water at (x, y), each core a disc of radius sqrt(d2) +// turning as a solid body; outside it the same as a point vortex. +let U = 0, VY = 0; +function flow(x, y, V, d2) { + let u = 0, v = 0; + for (let k = 0; k < 4; k += 2) { + const xk = V[k], dy = y - V[k + 1], dx = x - xk, ex = x + xk; + let r = dx * dx + dy * dy; + if (r < d2) r = d2; + let q = ex * ex + dy * dy; + if (q < d2) q = d2; + u += dy / r - dy / q; + v += ex / q - dx / r; + } + U = G * u; + VY = G * v; +} + +function advance(S, h) { + const { V, V0, Vm, px, py, age, life, nact, d2 } = S; + V0.set(V); + const n = Math.max(1, Math.round(h / 2 / HV)), hv = h / 2 / n; + for (let i = 0; i < n; i++) vstep(V, hv); + Vm.set(V); + for (let i = 0; i < n; i++) vstep(V, hv); + for (let k = 0; k < 2; k++) if ((S.pt[k] -= h) <= 0) (S.pt[k] += PULSE), line(S, k); + for (let i = 0; i < 2 * NPR; i++) { + const j = i < NPR ? i : i - NPR; + if (j >= nact) continue; + if (j >= NL && ((age[i] += h) > life[i] || py[i] < S.yc + ROW - H - 3 || px[i] > 36)) { + bubble(S, i, 0); + continue; + } + if (j < NL) age[i] += h; + const x = px[i], y = py[i]; + flow(x, y, V0, d2); + const u1 = U, v1 = VY; + flow(x + 0.5 * h * u1, y + 0.5 * h * v1, Vm, d2); + const u2 = U, v2 = VY; + flow(x + 0.5 * h * u2, y + 0.5 * h * v2, Vm, d2); + const u3 = U, v3 = VY; + flow(x + h * u3, y + h * v3, V, d2); + px[i] = abs(x + (h / 6) * (u1 + 2 * u2 + 2 * u3 + U)); + py[i] = y + (h / 6) * (v1 + 2 * v2 + 2 * v3 + VY); + } +} + +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 = cl(k[0], 0, 1); + const sp = cl(k[1], 0.2, 3), want = cl(k[2], 3, 8); + S.dye = cl(k[3], 0, 1); + S.nact = NL + Math.round((NPR - NL) * (0.25 + 0.75 * S.dye)); + if (S.ratio < 0) launch(S, want), (S.want = want); + if (abs(want - S.want) > 0.01) (S.want = want), (S.wait = 0.3); + if (S.wait > 0 && (S.wait -= d) <= 0 && !S.go) S.go = 0.5; + const p = input.btnPressed || []; + if (p[0]) fresh(S); + // Rings that have parted beyond the panel make way for a fresh pair. + if (!S.go && abs(S.V[1] - S.V[3]) > LIM) S.go = 0.5; + if (S.go) { + S.gain = S.go / 0.5; + if ((S.go -= d) <= 0) (S.go = 0), launch(S, S.want), (S.gain = 0); + } else S.gain = min(1, S.gain + d / 0.5); + const h = (d * sp) / 2, n = Math.ceil(h / HD - 1e-9); + // Heights are kept within a few thousand LEDs of the start. + if (S.yc > 4096) { + (S.V[1] -= 4096), (S.V[3] -= 4096), (S.yc -= 4096); + for (let i = 0; i < 2 * NPR; i++) S.py[i] -= 4096; + for (let i = 1; i < 2 * N; i += 2) S.M[i] -= 4096; + } + const yc0 = S.yc; + if (!S.mset) { + S.mset = 1; + for (let j = 0; j < H; j++) for (let i = 0; i < W; i++) (S.M[2 * (j * W + i)] = i - 31.5), (S.M[2 * (j * W + i) + 1] = yc0 + ROW - j - 0.5); + } + for (let i = 0; i < n; i++) advance(S, h / n); + S.yc = (S.V[1] + S.V[3]) / 2; + water(S, h, yc0); +} + +// The water carries fine glitter that catches the light sheet, too faint to +// light an LED. Each LED keeps the place its water came from (M), carried +// back along the flow every frame, and shows the glitter of that place, so +// still water slides down past the rings and the water they carry turns. +function water(S, h, yc0) { + const { V, M, M2, d2 } = S; + for (let j = 0; j < H; j++) { + const y = S.yc + ROW - j - 0.5; + for (let i = 32; i < W; i++) { + const x = i - 31.5; + flow(x, y, V, d2); + for (let s = 0; s < 2; s++) { + const X = s ? -x : x, u = s ? -U : U, xb = X - h * u, yb = y - h * VY; + const fx = xb + 31.5, fy = yc0 + ROW - yb - 0.5, o = 2 * (j * W + (s ? 63 - i : i)); + if (fx < 0 || fx > W - 1 || fy < 0 || fy > H - 1) { + M2[o] = xb, M2[o + 1] = yb; + continue; + } + const ix = floor(fx), iy = floor(fy), ax = fx - ix, ay = fy - iy, q = 2 * (iy * W + ix), q2 = q + (ix < W - 1 ? 2 : 0), q3 = q + (iy < H - 1 ? 2 * W : 0), q4 = q3 + (ix < W - 1 ? 2 : 0); + for (let c = 0; c < 2; c++) M2[o + c] = (M[q + c] * (1 - ax) + M[q2 + c] * ax) * (1 - ay) + (M[q3 + c] * (1 - ax) + M[q4 + c] * ax) * ay; + } + } + } + S.M = M2, S.M2 = M; +} + +function splat(D, x, y, w) { + const fx = x - 0.5, fy = y - 0.5, ix = floor(fx), iy = floor(fy); + if (ix < -1 || ix >= W || iy < -1 || iy >= H) return; + const ax = fx - ix, ay = fy - iy; + if (iy >= 0) { + if (ix >= 0) D[iy * W + ix] += w * (1 - ax) * (1 - ay); + if (ix < W - 1) D[iy * W + ix + 1] += w * ax * (1 - ay); + } + if (iy < H - 1) { + if (ix >= 0) D[(iy + 1) * W + ix] += w * (1 - ax) * ay; + if (ix < W - 1) D[(iy + 1) * W + ix + 1] += w * ax * ay; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || S.ratio < 0) return; + const { px, py, age, life, DA, DB, col, M, nact } = S; + hsv(S.hue + 0.4, 0.55, 1, col, 0); + hsv(S.hue + 0.86, 0.5, 1, col, 3); + DA.fill(0), DB.fill(0); + for (let i = 0; i < 2 * NPR; i++) { + const j = i < NPR ? i : i - NPR; + if (j >= nact) continue; + const a = age[i], w = (j < NL ? min(1, a / 0.5, (life[i] - a) / 0.6) : 0.22 * min(1, a / 0.5, (life[i] - a) / 2)) * min(1, 0.5 + px[i]); + if (w <= 0) continue; + const y = ROW + S.yc - py[i], D = i < NPR ? DA : DB; + splat(D, 32 + px[i], y, w), splat(D, 32 - px[i], y, w); + } + const gain = S.gain, cA0 = col[0], cA1 = col[1], cA2 = col[2], cB0 = col[3], cB1 = col[4], cB2 = col[5]; + for (let p = 0; p < N; p++) { + // The glitter of the water this LED shows, never past 24, and a fixed + // flicker of a level or two per LED under it. + const g = h32(floor(M[2 * p] + 64), floor(M[2 * p + 1]) & 4095), q = JIT[p]; + let r = (((g & 31) * 22) >> 5) + (q & 3), gg = ((((g >> 5) & 31) * 22) >> 5) + ((q >> 2) & 3), b = ((((g >> 10) & 31) * 22) >> 5) + ((q >> 4) & 3); + const a = DA[p], c = DB[p], s = a + c; + if (s > 0.01) { + const D = gain * (1 - exp(-s / S0)), u = a / s, c0 = cB0 + (cA0 - cB0) * u, c1 = cB1 + (cA1 - cB1) * u, c2 = cB2 + (cA2 - cB2) * u; + if (D < D0) { + // Thin dye is a haze in the water that stays under the LEDs' threshold. + const t = (0.7 * D) / D0; + (r += (24 * c0 - r) * t), (gg += (24 * c1 - gg) * t), (b += (24 * c2 - b) * t); + } else { + const v = 28 + 227 * ((D - D0) / (1 - D0)) ** 0.45, e = 2.4; + (r = v * c0 + (r - 12) * e), (gg = v * c1 + (gg - 12) * e), (b = v * c2 + (b - 12) * e); + } + } + display.setPixel(p & 63, p >> 6, cl(r, 0, 255) | 0, cl(gg, 0, 255) | 0, cl(b, 0, 255) | 0); + } +} From 7e124c16984af051ab972561a10a57d119dd6f27 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 01:58:40 +0000 Subject: [PATCH 07/14] =?UTF-8?q?53=ED=8C=90:=20=ED=95=A0=EB=A1=9C?= =?UTF-8?q?=EC=9C=88=20=EB=AA=AB=EC=9D=84=20=EC=89=AC=EB=8A=94=20ROUND.md?= =?UTF-8?q?=20=EB=A5=BC=20=EB=94=B0=EB=9D=BC=20=ED=95=A0=EB=A1=9C=EC=9C=88?= =?UTF-8?q?=20=EB=8B=A4=EC=84=AF=EC=9D=84=20=EB=BA=80=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dev 의 c7ddad2 가 찜작자 10-26 「할로윈 Ʞ념 팚턎은 읎제 귞만」을 따띌 죌제 6 을 쉬게 했닀. House of Usher, Medusa, Haunted Carousel, Rokurokubi, Baba Yaga's Hut 은 읎 람랜치의 dad2914, fce8124, 0e7a67a, c468eb3, 6cf7431 에 낚는닀. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Yb8D9UxrifXqrXju5CVbqY --- .../pattern-study/patterns/baba-yaga-hut.js | 455 ----------------- .../patterns/haunted-carousel.js | 340 ------------- .../pattern-study/patterns/house-of-usher.js | 458 ------------------ incoming/pattern-study/patterns/medusa.js | 453 ----------------- incoming/pattern-study/patterns/rokurokubi.js | 360 -------------- 5 files changed, 2066 deletions(-) delete mode 100644 incoming/pattern-study/patterns/baba-yaga-hut.js delete mode 100644 incoming/pattern-study/patterns/haunted-carousel.js delete mode 100644 incoming/pattern-study/patterns/house-of-usher.js delete mode 100644 incoming/pattern-study/patterns/medusa.js delete mode 100644 incoming/pattern-study/patterns/rokurokubi.js diff --git a/incoming/pattern-study/patterns/baba-yaga-hut.js b/incoming/pattern-study/patterns/baba-yaga-hut.js deleted file mode 100644 index 3ac22b231..000000000 --- a/incoming/pattern-study/patterns/baba-yaga-hut.js +++ /dev/null @@ -1,455 +0,0 @@ -// ===== Patternflow pattern ===== -// Title: Baba Yaga's Hut -// Family: halloween -// Idea: In a moonlit birch forest a log hut on giant chicken legs paces behind a fence of skulls with glowing green eyes, knees bending backward at every step, swaying, crouching and turning round, while Baba Yaga crosses the moon in her mortar, steering with the pestle and sweeping her trail away with a broom. -// Author: patternflow-lab -// Date: 2026-10-26 -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: original, pattern round 53, after Baba Yaga's hut on chicken legs in Slavic folklore -// Round: 53 -// =============================== -// @matrix 64x128 -// @knobs Hue=0..1, Speed=0.2..3, Legs=0..1, Fog=0..1 -// -// Knob 1: Hue. The night: violet at 0, then indigo, blue and teal to a sickly green at 1. The windows stay orange, the eyes green and the birches pale. -// Knob 2: Speed. At 2 the hut walks a step every 0.7 seconds and goes once round its round of pacing, turning and crouching in 14 seconds, and Baba Yaga crosses the moon every 12; at 0.2 all of it ten times slower. -// Knob 3: Legs. How long the legs are: at 0 they hold the floor of the hut 38 LEDs off the ground, at 1 they lift it 62, so the roof stands in front of the moon. -// Knob 4: Fog. Thin wisps over the ground at 0.06; turned up, a moonlit bank rises to the hut's knees, then hides the legs, the eyes of the skulls glowing green through it. -// Buttons: K1 tap: the hut turns its front to us, kneels and opens its door on the fire inside, then gets up and goes on. - -const W = 64, H = 128, N = W * H; -const { floor, min, max, abs, sqrt, sin, cos, exp, round, PI } = Math; -const cl = (v, a, b) => (v < a ? a : v > b ? b : v); -const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); -const mix = (a, b, t) => a + (b - a) * t; -const hash = (i) => { - let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); - h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); - return ((h ^ (h >>> 16)) >>> 0) / 4294967296; -}; -function hsv(h, s, v, out, o) { - h = (h - floor(h)) * 6; - const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); - out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; - out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; - out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; -} -const NT = new Float32Array(4096).map((_, i) => hash(i * 7 + 3)); -function vn(x, y) { - const xi = floor(x), yi = floor(y), fx = x - xi, fy = y - yi, ux = fx * fx * (3 - 2 * fx), uy = fy * fy * (3 - 2 * fy); - const i0 = xi & 63, i1 = (xi + 1) & 63, j0 = (yi & 63) << 6, j1 = ((yi + 1) & 63) << 6, a = NT[j0 + i0], b = NT[j0 + i1], c = NT[j1 + i0]; - return a + (b - a) * ux + (c - a) * uy + (a - b - c + NT[j1 + i1]) * ux * uy; -} - -// The row the feet stand on; the moon; the hut's half width across its -// front and half depth along its sides, the wall and roof heights, the -// eaves' overhang and the hips either side of its middle. -const GY = 106, MX = 21, MY = 24, MR = 17; -const A = 12, B = 15, WALL = 20, ROOF = 12, OVER = 1.5, HIP = 5.5; -// Birch trunks: middle, width, how near (1 near and bright), lean per row. -const BIRCH = [[3, 5, 0.9, 0.01], [13, 2.5, 0.5, -0.02], [36, 2, 0.4, 0.015], [42, 3, 0.62, -0.01], [51, 2.5, 0.45, 0.02], [60, 6, 1, -0.012], [27, 3.5, 0.7, 0.008]]; -const SKULLS = [7, 24, 41, 58]; -const groundAt = (x) => 103 + 1.2 * sin(x * 0.21) + 0.8 * sin(x * 0.53 + 1); -// The round the hut walks, in seconds at Speed 2: [length, facing from, to, -// middle from, to, how hard it steps, crouch]. Facing 0 is its door to us, -// 90 walking right. -const SEG = [ - [2.2, 90, 90, 24, 40, 1, 0], - [1.4, 90, 0, 40, 40, 0.8, 0], - [3.2, 0, 0, 40, 40, 0, 1], - [1.4, 0, -90, 40, 40, 0.8, 0], - [2.2, -90, -90, 40, 24, 1, 0], - [2.4, -90, -270, 24, 24, 0.8, 0], - [1.2, 90, 90, 24, 24, 0, 0], -]; -const LOOP = SEG.reduce((s, g) => s + g[0], 0), STEP = 1.4; - -export function setup(params) { - params.s = { - hue: -1, legs: 0, fog: 0.06, t: 0, ct: 0, call: 0, - phi: 90, X: 24, c: 0, dr: 0, act: 1, v: 0, gp: 0, - puffs: [], dust: [], fly: null, nf: 0.8, nfl: 0, - bg: new Float32Array(N * 3), F: new Float32Array(N * 3), fogc: [0, 0, 0], - }; -} - -// Sky, moon, birches and ground: fixed until the Hue knob moves. -function background(S) { - const h = 0.76 - 0.4 * S.hue, top = [0, 0, 0], low = [0, 0, 0], moon = [0.96, 0.93, 0.82], bark = [0, 0, 0], bg = S.bg; - hsv(h, 0.8, 0.13, top, 0); - hsv(h + 0.03, 0.45, 0.52, low, 0); - hsv(h, 0.12, 1, bark, 0); - hsv(h + 0.02, 0.35, 0.62, S.fogc, 0); - for (let y = 0; y < H; y++) { - const t = sm(0, 102, y) ** 1.2; - for (let x = 0; x < W; x++) { - const o = 3 * (y * W + x), xx = x + 0.5, yy = y + 0.5; - let r = mix(top[0], low[0], t), g = mix(top[1], low[1], t), b = mix(top[2], low[2], t); - const d = Math.hypot(xx - MX, yy - MY); - if (d < MR) { - // The moon's seas, and a little darkening at the limb. - const sea = sm(0.52, 0.62, vn(xx * 0.32 + 5, yy * 0.32 + 9)) * 0.16 + sm(MR - 3, MR, d) * 0.1; - (r = moon[0] * (1 - sea)), (g = moon[1] * (1 - sea)), (b = moon[2] * (1 - sea * 0.7)); - } else { - const k = floor(9 * max(0, 1 - (d - MR) / 11) ** 2) / 9 * 0.22; - (r += moon[0] * k), (g += moon[1] * k), (b += moon[2] * k); - if (y < 80 && hash(y * W + x + 999) < 0.006) (r += 0.5), (g += 0.48), (b += 0.55); - } - const gl = groundAt(xx); - for (let k = 0; k < BIRCH.length; k++) { - const [cx, w, near, lean] = BIRCH[k], u = (xx - cx - lean * (GY - yy)) / (w / 2); - if (abs(u) > 1 || yy > gl + 1) continue; - // Bark: pale and round, dark lenticels, black patches near the foot. - const L = (0.78 - 0.28 * u - 0.22 * u * u) * near, hk = hash(k * 977 + y), mk = hk < 0.12 + 0.3 * sm(60, 100, yy) && abs(u - (hash(k * 31 + y) - 0.5)) < 0.3 + hk * 2; - const v = mk ? 0.1 + 0.05 * near : L; - (r = bark[0] * v), (g = bark[1] * v), (b = bark[2] * v); - } - if (yy > gl) { - // The forest floor, its edge catching the moon, and dead leaves. - const e = sm(gl + 2.5, gl, yy), q = hash(y * 131 + x); - (r = 0.06 + 0.12 * e + (q < 0.1 ? 0.1 : 0)), (g = 0.04 + 0.08 * e + (q < 0.1 ? 0.04 : 0)), (b = 0.07 + 0.12 * e); - const f = 1 - 0.4 * sm(105, 128, yy); - (r *= f), (g *= f), (b *= f); - } - (bg[o] = r), (bg[o + 1] = g), (bg[o + 2] = b); - } - } - S.hb = S.hue; -} - -let NX = 0, NY = 0; -// Distance to a capsule a-b of radius r0 at a and r1 at b; (NX, NY) the outward normal. -function cap(px, py, ax, ay, bx, by, r0, r1) { - const ex = bx - ax, ey = by - ay, qx = px - ax, qy = py - ay, u = cl((qx * ex + qy * ey) / (ex * ex + ey * ey + 1e-9), 0, 1), dx = qx - u * ex, dy = qy - u * ey, d = sqrt(dx * dx + dy * dy) + 1e-9; - (NX = dx / d), (NY = dy / d); - return d - mix(r0, r1, u); -} -function poly(px, py, v) { - let dd = 1e9, s = 1; - for (let i = 0, j = v.length - 2; i < v.length; j = i, i += 2) { - const ex = v[j] - v[i], ey = v[j + 1] - v[i + 1], wx = px - v[i], wy = py - v[i + 1], u = cl((wx * ex + wy * ey) / (ex * ex + ey * ey), 0, 1), bx = wx - ex * u, by = wy - ey * u; - dd = min(dd, bx * bx + by * by); - const c1 = py >= v[i + 1], c2 = py < v[j + 1], c3 = ex * wy > ey * wx; - if ((c1 && c2 && c3) || (!c1 && !c2 && !c3)) s = -s; - } - return s * sqrt(dd); -} -function put(F, x, y, r, g, b, a) { - if (x < 0 || x >= W || y < 0 || y >= H || a <= 0) return; - const o = 3 * (y * W + x); - a = min(1, a); - (F[o] += (r - F[o]) * a), (F[o + 1] += (g - F[o + 1]) * a), (F[o + 2] += (b - F[o + 2]) * a); -} - -function choreo(ct) { - let t = ct % LOOP, k = 0; - while (t > SEG[k][0]) (t -= SEG[k][0]), k++; - const [d, p0, p1, x0, x1, act, cr] = SEG[k], f = t / d; - return { phi: mix(p0, p1, sm(0, 1, f)), X: mix(x0, x1, f), act, cr: cr * sin(PI * f) ** 2, v: (x1 - x0) / d }; -} -const toward = (v, t, r) => (v < t ? min(t, v + r) : max(t, v - r)); - -export function update(dt, input, params) { - if (!params.s) setup(params); - const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); - S.hue = cl(k[0], 0, 1); - S.legs = cl(k[2], 0, 1); - S.fog = cl(k[3], 0, 1); - const ds = (d * cl(k[1], 0.2, 3)) / 2, first = S.t === 0; - S.t += ds; - if ((input.btnPressed || [])[0] && S.call <= 0) S.call = 7; - let T; - if (S.call > 0) { - // Turn the front to us, kneel, open the door; then get up and go on. - S.call -= ds; - const face = 360 * round(S.phi / 360), there = abs(S.phi - face) < 8; - T = { phi: face, X: S.X, act: there ? 0 : 0.8, cr: there && S.call > 1.6 ? 1 : 0, v: 0 }; - S.dr = toward(S.dr, S.c > 0.85 && S.call > 2.2 ? 1 : 0, ds / 0.7); - } else { - S.ct += ds; - T = choreo(S.ct); - S.dr = toward(S.dr, 0, ds / 0.7); - } - const tp = T.phi + 360 * round((S.phi - T.phi) / 360); - if (first) (S.phi = tp), (S.X = T.X); - S.phi = toward(S.phi, tp, 160 * ds); - S.X = toward(S.X, T.X, 12 * ds); - S.c = toward(S.c, T.cr, 1.6 * ds); - S.act = toward(S.act, T.act, 2.5 * ds); - S.v = toward(S.v, T.v, 20 * ds); - S.gp += ds / STEP; - // Smoke from the chimney, swelling and drifting downwind. - if (S.cx !== undefined && Math.random() < ds * 9) S.puffs.push({ x: S.cx + Math.random() - 0.5, y: S.cy, a: 0, s: Math.random() }); - for (const p of S.puffs) (p.a += ds), (p.y -= (5 + 2 * p.s) * ds), (p.x += (2.4 + 1.5 * sin(S.t * 0.4 + p.s * 3)) * ds); - S.puffs = S.puffs.filter((p) => p.a < 4.5 && p.y > -8); - // Baba Yaga's flights across the moon, one way and then the other. - if (!S.fly && (S.nf -= ds) <= 0) (S.fly = { dir: S.nfl % 2 ? -1 : 1, a: 0 }), S.nfl++; - const f = S.fly; - if (f) { - f.a += ds; - const x = f.dir > 0 ? -22 + 16 * f.a : W + 22 - 16 * f.a, y = MY + 1 + 2.5 * sin(f.a * 1.7) - 0.08 * f.dir * (x - MX); - (f.x = x), (f.y = y); - // The broom sweeps her track away in a little dust. - for (let e = ds * 30; e > 0; e--) if (e >= 1 || Math.random() < e) S.dust.push({ x: x - f.dir * 20 + 2 * Math.random(), y: y + 2 + 4 * Math.random(), vx: -f.dir * (2 + 4 * Math.random()), vy: 1 + 3 * Math.random(), a: 0 }); - if (f.a * 16 > W + 46) (S.fly = null), (S.nf = 12 - f.a); - } - for (const p of S.dust) (p.a += ds), (p.x += p.vx * ds), (p.y += p.vy * ds), (p.vx *= 1 - ds); - S.dust = S.dust.filter((p) => p.a < 1.6); -} - -// Leg i of the hut: joints in the panel, from the hip to the toes. -function leg(S, i, yHip, Hh, Lmax, cp, sp, out) { - const p = (S.gp + 0.5 * i) % 1, act = S.act, stride = abs(S.v) * 0.6 * STEP; - let f, lift; - if (p < 0.6) (f = stride * (0.5 - p / 0.6)), (lift = 0); - else { - const q = (p - 0.6) / 0.4; - (f = stride * (sm(0, 1, q) - 0.5)), (lift = (3 + 5 * S.legs) * act * sin(PI * q)); - } - const lat = i ? HIP : -HIP, L1 = 0.6 * Lmax, L2 = 0.68 * Lmax, dv = Hh - lift; - const D = Math.hypot(f, dv) + 1e-6, dd = cl(D, abs(L1 - L2) + 0.5, L1 + L2 - 0.3); - const a = (L1 * L1 - L2 * L2 + dd * dd) / (2 * dd), h = sqrt(max(0, L1 * L1 - a * a)), uf = f / D, uv = dv / D; - // The joint bends backward: it sits behind the line from hip to foot. - const jf = a * uf - h * uv, jv = a * uv + h * uf, X = (l, fw) => S.X + l * cp + fw * sp; - (out[0] = X(lat, 0)), (out[1] = yHip), (out[2] = X(lat, jf)), (out[3] = yHip + jv), (out[4] = X(lat, f)), (out[5] = yHip + dv); - out[6] = sm(0, 3, lift); - out[7] = lat * sp - f * cp; -} - -const TOES = [[-0.5, 0.87, 9], [0, 1, 10], [0.5, 0.87, 9], [0, -1, 4.5]]; -function drawLeg(S, F, P, cp, sp) { - const [hx, hy, jx, jy, fx, fy, curl] = P, tx = [], ty = [], jl = Math.hypot(jx - hx, jy - hy) + 1e-6; - for (const [l, fw, len] of TOES) { - const k = len * (1 - 0.45 * curl), dz = l * sp - fw * cp; - tx.push(fx + k * (l * cp + fw * sp)), ty.push(fy + (0.35 * -dz * k) / len + 3 * curl); - } - const x0 = floor(min(hx, jx, fx) - 11), x1 = floor(max(hx, jx, fx) + 11), y0 = floor(hy - 2), y1 = floor(max(fy, ...ty) + 2); - for (let y = max(0, y0); y <= min(H - 1, y1); y++) - for (let x = max(0, x0); x <= min(W - 1, x1); x++) { - const px = x + 0.5, py = y + 0.5; - // The thigh: a big feathered drumstick, scalloped down its edge. - const u = cl(((px - hx) * (jx - hx) + (py - hy) * (jy - hy)) / (jl * jl), 0, 1); - let best = cap(px, py, hx, hy, jx, jy, 5.2, 2.6) - 0.9 * max(0, sin(u * 16 + 1)), nx = NX, ny = NY, r, g, b; - const tip = best > -1.2 || hash(y * 64 + x) < 0.22; - (r = tip ? 0.29 : 0.14), (g = tip ? 0.21 : 0.095), (b = tip ? 0.17 : 0.085); - // The shank: scaly and yellowish, with a spur, and knobbly joints. - const e = min(cap(px, py, jx, jy, fx, fy, 2.2, 1.7), cap(px, py, mix(jx, fx, 0.55), mix(jy, fy, 0.55), mix(jx, fx, 0.55) + (jx - hx) * 0.12, mix(jy, fy, 0.55) + 1, 0.9, 0.4)); - if (e < best) { - const sc = (Math.hypot(px - jx, py - jy) * 0.7 + (px - fx) * 0.2) % 1 < 0.35 ? 0.75 : 1; - (best = e), (nx = NX), (ny = NY), (r = 0.4 * sc), (g = 0.28 * sc), (b = 0.07 * sc); - } - const kn = min(cap(px, py, jx, jy, jx, jy, 3, 3), cap(px, py, fx, fy, fx, fy, 2.1, 2.1)); - if (kn < best) (best = kn), (nx = NX), (ny = NY), (r = 0.3), (g = 0.2), (b = 0.08); - for (let t = 0; t < 4; t++) { - const c = cap(px, py, fx, fy, tx[t], ty[t], 1.4, 0.7); - if (c < best) { - (best = c), (nx = NX), (ny = NY); - const claw = Math.hypot(px - tx[t], py - ty[t]) < 2; - (r = claw ? 0.66 : 0.36), (g = claw ? 0.62 : 0.25), (b = claw ? 0.52 : 0.07); - } - } - if (best > 0.7) continue; - // Moonlight from the upper left catches the edges facing it. - const rim = max(0, -0.6 * nx - 0.8 * ny) * sm(-1.8, 0, best) * 0.4; - put(F, x, y, r + rim * 0.7, g + rim * 0.65, b + rim * 0.8, sm(0.7, -0.3, best)); - } -} - -// The hut: walls of logs, windows and door on the faces turned to us, the -// gable roof, its gable ends and the chimney. -function hut(S, F, ySill, cp, sp, sway, flick) { - const yE = ySill - WALL, yR = yE - ROOF, P = (l, z) => l * cp + z * sp; - const faces = []; - if (cp > 0.02) faces.push([-A, B, A, B, sp, 0]); - if (cp < -0.02) faces.push([A, -B, -A, -B, -sp, 1]); - if (sp < -0.02) faces.push([A, B, A, -B, cp, 2]); - if (sp > 0.02) faces.push([-A, -B, -A, B, -cp, 2]); - for (const fc of faces) (fc[6] = P(fc[0], fc[1])), (fc[7] = P(fc[2], fc[3])); - const eav = [P(-A - OVER, -B - OVER), P(A + OVER, -B - OVER), P(A + OVER, B + OVER), P(-A - OVER, B + OVER)], rid = [P(0, -B - OVER), P(0, B + OVER)]; - const eL = min(...eav), eR = max(...eav), rL = min(...rid), rR = max(...rid), cx = P(0, -6); - const gb = cp > 0.02 ? [P(-A, B), P(A, B), P(0, B)] : cp < -0.02 ? [P(A, -B), P(-A, -B), P(0, -B)] : null; - (S.cx = S.X + cx + sway * (ySill - yR + 6)), (S.cy = yR - 6); - for (let y = max(0, floor(yR - 7)); y <= min(H - 1, floor(ySill)); y++) { - const yy = y + 0.5, sh = S.X + sway * (ySill - yy); - for (let x = 0; x < W; x++) { - const xr = x + 0.5 - sh; - let col = 0, cr = 0, cg = 0, cb = 0; - if (yy > yE && yy <= ySill + 0.5) { - for (const [, , , , nx, kind, x0, x1] of faces) { - if (xr < x0 || xr >= x1) continue; - const u = (xr - x0) / (x1 - x0), wpx = x1 - x0, lv = 0.8 - 0.3 * nx, hv = ySill - yy, lg = hv % 3, row = floor(hv / 3); - // Logs: a lit top, a dark gap, and round log ends at the corners. - let r = 0.15, g = 0.095, b = 0.08; - if (lg > 2.2) (r = 0.3), (g = 0.21), (b = 0.17); - if (lg < 0.7) (r = 0.05), (g = 0.03), (b = 0.035); - const end = (u * wpx < 1.5 || (1 - u) * wpx < 1.5) && (row + (kind >> 1)) % 2 === 0; - if (end) (r = 0.36), (g = 0.26), (b = 0.19); - (r *= lv), (g *= lv), (b *= lv); - // Windows: carved frames round orange light and a dark cross; the door. - const w0 = kind ? 0.37 : 0.58, ww = (kind ? 0.26 : 0.28) * wpx, wu = (u - w0) / (ww / wpx), wy = (yy - yE - 5) / 6; - if (wu >= 0 && wu < 1 && wy >= 0 && wy < 1 && ww > 1.2) { - const fr = wu * ww < 1 || (1 - wu) * ww < 1 || wy < 0.17 || wy > 0.83; - const cross = abs(wu - 0.5) * ww < 0.5 || abs(wy - 0.5) < 0.09, gl = flick * (1 - 0.5 * ((wu - 0.5) ** 2 + (wy - 0.5) ** 2)); - if (fr) (r = 0.45), (g = 0.35), (b = 0.27); - else if (cross) (r = 0.16), (g = 0.07), (b = 0.03); - else (r = gl), (g = 0.6 * gl * gl), (b = 0.14 * gl * gl); - } - if (kind === 0 && u >= 0.12 && u < 0.46 && hv < 15) { - const du = (u - 0.14) / 0.3, open = du > 1 - S.dr && du < 1 && hv < 14; - if (open) { - // Firelight from the stove: yellow low in the doorway, deep orange above. - const gl = flick * (1 - 0.35 * (hv / 14)), k = gl * gl; - (r = gl), (g = 0.28 + 0.4 * k - 0.1 * (hv / 14)), (b = 0.04 + 0.16 * k * k); - } else if (du < 0 || du >= 1 || hv >= 14) (r = 0.46), (g = 0.36), (b = 0.27); - else { - const plank = ((du / max(0.05, 1 - S.dr)) * 4) % 1 < 0.2; - (r = plank ? 0.1 : 0.3), (g = plank ? 0.06 : 0.19), (b = plank ? 0.04 : 0.12); - if (abs(du - 0.8 * (1 - S.dr)) * 0.3 * wpx < 0.6 && abs(hv - 7) < 0.6) (r = 0.7), (g = 0.6), (b = 0.35); - } - } - (col = 1), (cr = r), (cg = g), (cb = b); - } - } - if (yy > yR && yy <= yE + 1.2) { - // The roof: hull of the eaves and the ridge; thatch in rows. - const f = (yy - yR) / (yE - yR), xl = mix(rL, eL, f), xrr = mix(rR, eR, f); - if (xr >= xl && xr < xrr) { - const q = hash(y * 71 + floor(xr * 2) + 5000), lv = 0.85 - 0.3 * ((xr - xl) / max(1, xrr - xl)); - // Straw in rows, combed down the slope, the ridge and eaves catching light. - let v = ((xr * 0.8 + (yy - yR) * 0.5) % 1.6 < 0.5 ? 0.12 : 0.24 + 0.08 * q) * ((yy - yR) % 4 < 0.9 ? 0.6 : 1); - if (yy > yE || yy < yR + 1) v = 0.36; - (col = 1), (cr = v * 1.0 * lv), (cg = v * 0.92 * lv), (cb = v * 0.78 * lv); - // A gable end turned to us: boards, and a round attic window. - if (gb) { - const gl = mix(gb[2], gb[0], f), gr = mix(gb[2], gb[1], f); - if (xr >= gl && xr < gr) { - const bd = ((xr - gb[0]) * 0.7) % 1 < 0.25 ? 0.33 : 1; - (cr = 0.21 * bd), (cg = 0.14 * bd), (cb = 0.1 * bd); - if (Math.hypot((xr - gb[2]) / max(0.2, abs(cp)), yy - yE + ROOF * 0.38) < 1.7) (cr = flick), (cg = 0.55 * flick), (cb = 0.12); - } - } - } - } - if (yy > yR - 6 && yy < yR + 1.5 && abs(xr - cx) < 1.6) (col = yy < yR - 5 ? 1.6 : 1), (cr = 0.2 * col), (cg = 0.12 * col), (cb = 0.1 * col); - if (col) put(F, x, y, cr, cg, cb, 1); - } - } -} - -// Baba Yaga in her mortar, facing +x with y down: mortar, hunched body, -// head in a kerchief, long nose, the pestle she steers with and the broom. -function yaga(S, F) { - const f = S.fly, sc = 1.3, w = sin(f.a * 7) * 1.2, pw = 0.55 + 0.35 * sin(f.a * 3.4), pc = cos(pw), ps = sin(pw); - const sh = [ - ["p", [-4.2, 0.6, 4.2, 0.6, 3, 6, 1.8, 6.6, -1.8, 6.6, -3, 6]], - ["c", -4.8, -0.2, 4.8, -0.2, 0.7, 0.7], - ["p", [-2.2, -0.5, -3.2, -3.5, -2.6, -6.5, -0.8, -8.6, 1.6, -9.4, 3, -8, 2.4, -5, 2.2, -0.5]], - ["c", 3, -10.6, 3, -10.6, 1.9, 1.9], - ["p", [1.6, -12.2, -2.2, -11.6 + w, 1.2, -9.8]], - ["p", [4.6, -11.2, 7.2, -9.2, 4.6, -9.7]], - ["c", 1.6, -7.2, 5.4, -6.6, 0.75, 0.75], - ["c", 5.4 - 2 * ps, -6.6 + 2 * pc, 5.4 + 8 * ps, -6.6 - 8 * pc, 0.6, 1.4], - ["c", -1.5, -6, -3.5, -3.5, 0.7, 0.7], - ["c", -3.5, -3.5, -12, 0, 0.5, 0.5], - ["p", [-11.5, -0.6, -18, -1.6 + w, -18.5, 3 + w, -12, 1.6]], - ]; - for (let y = max(0, floor(f.y - 21)); y < min(H, f.y + 11); y++) - for (let x = max(0, floor(f.x - 26)); x < min(W, f.x + 26); x++) { - const lx = ((x + 0.5 - f.x) / sc) * f.dir, ly = (y + 0.5 - f.y) / sc; - let d = 1e9; - for (const s of sh) d = min(d, s[0] === "c" ? cap(lx, ly, s[1], s[2], s[3], s[4], s[5], s[6]) : poly(lx, ly, s[1])); - d *= sc; - // The mortar's rim catches a thread of moonlight between it and her. - const rim = abs(ly + 0.45) < 0.32 && abs(lx) < 4.5 ? 0.22 : 0; - if (d < 0.6) put(F, x, y, 0.035 + rim, 0.02 + rim * 0.9, 0.05 + rim, sm(0.6, -0.4, d)); - } -} - -function skulls(S, F) { - const eg = [0.5, 1, 0.2]; - // The rail between the posts: bones lashed end to end. - for (let x = 0; x < W; x++) for (const y of [120, 121]) put(F, x, y, 0.36, 0.33, 0.28, y === 120 ? 0.9 : 0.6); - SKULLS.forEach((sx, k) => { - // Each pair of eyes flickers on its own, and now and then goes dim. - const fl = 0.6 + 0.4 * vn(S.t * 5 + k * 13, k * 7) - 0.45 * sm(0.72, 0.84, vn(S.t * 0.7 + k * 5, 30)); - for (let y = 106; y < H; y++) - for (let x = max(0, sx - 6); x < min(W, sx + 6); x++) { - const px = x + 0.5, py = y + 0.5, dx = abs(px - sx); - // A bone post with knobs, the skull's dome, cheekbones and jaw. - const post = min(cap(px, py, sx, 117, sx, 128, 1.5, 1.5), cap(px, py, sx, 124, sx, 124, 2.1, 2.1)); - const sk = min(cap(px, py, sx, 111.5, sx, 111.5, 4.4, 4.4), cap(px, py, sx - 2.3, 115.2, sx + 2.3, 115.2, 1.6, 1.6), cap(px, py, sx - 3.1, 113.4, sx + 3.1, 113.4, 1.2, 1.2)); - const d = min(post, sk), lv = 0.66 + (0.28 * (sx - px)) / 4; - const so = Math.hypot((dx - 1.85) / 1.25, (py - 112.2) / 1.15), eye = Math.hypot(dx - 1.85, py - 112.3); - if (d < 0.5) { - let r = 0.84 * lv, g = 0.8 * lv, b = 0.68 * lv; - if (sk < 0.5) { - // Dark sockets with a green coal in each, the nose and the teeth. - if (so < 1) (r *= 0.08), (g *= 0.08), (b *= 0.1); - if (eye < 0.85) (r = eg[0] * fl), (g = eg[1] * fl), (b = eg[2] * fl); - if (Math.hypot(px - sx, py - 114.3) < 0.8 || (abs(py - 116.1) < 0.5 && x % 2 === 0 && dx < 2.4)) (r *= 0.2), (g *= 0.2), (b *= 0.25); - } - put(F, x, y, r, g, b, sm(0.5, -0.5, d)); - } - const gl = 0.4 * fl * exp(-(eye * eye) / 3.5); - if (gl > 0.01) { - const o = 3 * (y * W + x); - (F[o] += eg[0] * gl), (F[o + 1] += eg[1] * gl), (F[o + 2] += eg[2] * gl); - } - } - }); -} - -export function draw(display, params) { - const S = params.s; - if (!S || S.t === 0) return; - if (S.hb !== S.hue) background(S); - const F = S.F, t = S.t; - F.set(S.bg); - const Lmax = 38 + 24 * S.legs, Hh = Lmax * (1 - 0.62 * S.c), bob = 1.1 * S.act * cos(4 * PI * S.gp), ySill = GY - Hh + bob; - const cp = cos((S.phi * PI) / 180), sp = sin((S.phi * PI) / 180), sway = 0.035 * S.act * sin(2 * PI * S.gp) + 0.02 * sin(t * 0.9) * (1 - S.act); - const flick = 0.88 + 0.12 * vn(t * 6, 3); - // Smoke, lit by the moon. - for (const p of S.puffs) { - const R = 1.6 + 1.6 * p.a, al = 0.65 * sm(0, 0.3, p.a) * (1 - p.a / 4.5); - for (let y = max(0, floor(p.y - R)); y < min(H, p.y + R); y++) - for (let x = max(0, floor(p.x - R)); x < min(W, p.x + R); x++) { - const q = 1 - ((x + 0.5 - p.x) ** 2 + (y + 0.5 - p.y) ** 2) / (R * R); - if (q > 0) put(F, x, y, 0.62, 0.6, 0.7, al * q); - } - } - if (S.fly) yaga(S, F); - for (const p of S.dust) put(F, floor(p.x), floor(p.y), 0.75, 0.72, 0.85, 0.8 * (1 - p.a / 1.6)); - // The hut's shadow on the ground, cast away from the moon. - for (let y = GY - 2; y < GY + 4; y++) - for (let x = 0; x < W; x++) { - const q = ((x + 0.5 - S.X - 7) / 17) ** 2 + ((y + 0.5 - GY - 0.5) / 2.6) ** 2; - if (q < 1 && y + 0.5 > groundAt(x + 0.5)) put(F, x, y, 0, 0, 0, 0.45 * (1 - q) * (1 - 0.5 * S.c)); - } - hut(S, F, ySill, cp, sp, sway, flick); - const L0 = [], L1 = []; - leg(S, 0, ySill, Hh, Lmax, cp, sp, L0); - leg(S, 1, ySill, Hh, Lmax, cp, sp, L1); - for (const P of L0[7] > L1[7] ? [L0, L1] : [L1, L0]) drawLeg(S, F, P, cp, sp); - // The open door throws firelight down onto the ground. - const dx = S.X - 5 * cp; - if (S.dr > 0.02) - for (let y = floor(ySill); y < min(H, GY + 6); y++) { - const hw = 4 + (9 * (y - ySill)) / max(4, GY - ySill); - for (let x = max(0, floor(dx - hw)); x < min(W, dx + hw); x++) { - const o = 3 * (y * W + x), a = 0.22 * S.dr * flick * (1 - abs(x + 0.5 - dx) / hw) * (y > groundAt(x) ? 1.6 : 0.6); - (F[o] += a), (F[o + 1] += 0.55 * a), (F[o + 2] += 0.15 * a); - } - } - skulls(S, F); - // Ground fog: wisps at 0.06, a bank up to the hut's knees and higher as Fog turns. - const fz = S.fog, top = GY - 72 * fz, fc = S.fogc; - for (let y = max(0, floor(top)); y < H; y++) { - const yy = y + 0.5, base = sm(top, top + 10 + 20 * fz, yy); - for (let x = 0; x < W; x++) { - const n = vn(x * 0.09 + t * 0.35, yy * 0.22 + t * 0.05) * 0.7 + vn(x * 0.21 - t * 0.6, yy * 0.4) * 0.3; - const a = base * sm(0.42 - 0.2 * fz, 0.8, n) * (0.35 + 0.5 * fz); - if (a > 0.01) put(F, x, y, fc[0], fc[1], fc[2], a); - } - } - for (let i = 0, o = 0; i < N; i++, o += 3) display.setPixel(i & 63, i >> 6, cl(F[o] * 255, 0, 255) | 0, cl(F[o + 1] * 255, 0, 255) | 0, cl(F[o + 2] * 255, 0, 255) | 0); -} diff --git a/incoming/pattern-study/patterns/haunted-carousel.js b/incoming/pattern-study/patterns/haunted-carousel.js deleted file mode 100644 index 8532519df..000000000 --- a/incoming/pattern-study/patterns/haunted-carousel.js +++ /dev/null @@ -1,340 +0,0 @@ -// ===== Patternflow pattern ===== -// Title: Haunted Carousel -// Family: halloween -// Idea: A merry-go-round turns alone in an empty fairground at night, its striped canopy and ring of orange bulbs wheeling overhead while skeleton horses on brass poles rise and fall past the mirrored centre column, until now and then it lurches faster, the bulbs go toxic green and ghostly riders sit on the horses and stare out at us, then it slows and they fade. -// Author: patternflow-lab -// Date: 2026-10-26 -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: original, pattern round 53, after a carousel in an empty fairground at night -// Round: 53 -// =============================== -// @matrix 64x128 -// @knobs Hue=0..1, Speed=0.2..3, Horses=5..8, Fog=-1..1 -// -// Knob 1: Hue. The night in the sky, the fog, the dark canopy stripes and the platform's panels: purple at 0, deep blue at 0.3, sea blue at 0.6, green at 1. The tops of the bright stripes go from pink at 0 to cyan at 1. The bulbs stay orange, green in a lurch, and the bones stay white. -// Knob 2: Speed. At 2 the carousel turns once in 15 seconds and each horse rises and falls three times a turn; the first lurch comes at 4 seconds and one every 17 after, two and a half times as fast for 3 seconds before it slows over 3 more. At 0.2 ten times slower, at 3 half as fast again. -// Knob 3: Horses. How many horses ride the ring, 5 to 8, spaced evenly; at 8 they run nose to tail. -// Knob 4: Fog. Ground fog drifting round the platform: none at -1, a low bank over the ground at the opening 0.06, and at 1 a deep bank up to the horses' hooves. -// Buttons: K1 tap starts a lurch now. - -const W = 64, H = 128, N = W * H; -const { floor, min, max, abs, sqrt, sin, cos, exp, atan2, round, PI } = Math; -const TAU = 2 * PI; -const cl = (v, a, b) => (v < a ? a : v > b ? b : v); -const sm = (u) => ((u = cl(u, 0, 1)), u * u * (3 - 2 * u)); -const fr = (v) => v - floor(v); -const mix = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]; -const hash = (i) => { - let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); - h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); - return ((h ^ (h >>> 16)) >>> 0) / 4294967296; -}; -function hsv(h, s, v) { - h = fr(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); - return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; -} - -// The camera stands D0 from the carousel's axis at the height of the horses' -// backs, so a point (x, h, z) with z toward us lands at x 32 + F x / (D0 - z), -// y 64 - F h / (D0 - z). Lengths are in canopy radii. -const F = 150, D0 = 5; -const VB = 0.8, VT = 1, HA = 1.85, RC = 0.19, RP = 0.78, RF = 1.05, PT = -0.95, PG = -1.07; -// Surfaces: sky, cone, valance, ceiling, valance seen from inside, finial, -// column, platform top, platform side, ground. -const SKY = 0, CONE = 1, VAL = 2, CEIL = 3, VIN = 4, FIN = 5, COL = 6, TOPS = 7, SIDE = 8, GND = 9; -const proj = (x, h, z) => [32 + (F * x) / (D0 - z), 64 - (F * h) / (D0 - z), D0 - z]; - -// Every LED's first fixed surface along its view line, found once. -const KIND = new Uint8Array(N), DEP = new Float32Array(N).fill(1e9), ANG = new Float32Array(N), AUX = new Float32Array(N); -for (let y = 0, p = 0; y < H; y++) - for (let x = 0; x < W; x++, p++) { - const a = (x + 0.5 - 32) / F, b = (64 - y - 0.5) / F, A2 = a * a + 1; - let best = 1e9, k = SKY, aux = 0; - const hit = (t, kk, v) => t > 0 && t < best && ((best = t), (k = kk), (aux = v)); - const cyl = (R) => { - const d = D0 * D0 - A2 * (D0 * D0 - R * R); - return d < 0 ? null : [(D0 - sqrt(d)) / A2, (D0 + sqrt(d)) / A2]; - }; - let c = cyl(1); - if (c) { - if (b * c[0] >= VB && b * c[0] <= VT) hit(c[0], VAL, b * c[0]); - if (b * c[1] >= VB && b * c[1] <= VT) hit(c[1], VIN, b * c[1]); - } - if (b > 0) { - const t = VT / b, r = Math.hypot(a * t, D0 - t); - if (r <= 1) hit(t, CEIL, r); - } - // The cone: radius (HA - h) / (HA - VT). - const kk = (HA - VT) ** 2, qa = A2 - (b * b) / kk, qb = -2 * D0 + (2 * HA * b) / kk, qc = D0 * D0 - (HA * HA) / kk; - const disc = qb * qb - 4 * qa * qc; - if (disc >= 0 && abs(qa) > 1e-9) - for (const t of [(-qb - sqrt(disc)) / (2 * qa), (-qb + sqrt(disc)) / (2 * qa)]) if (b * t >= VT && b * t <= HA) hit(t, CONE, b * t); - const hf = b * D0; - if (hf > HA - 0.05 && hf < 2.08 && abs(a * D0) < (hf > 1.98 ? 0.045 : 0.022)) hit(D0, FIN, hf); - c = cyl(RC); - if (c && b * c[0] >= PT && b * c[0] <= VT) hit(c[0], COL, b * c[0]); - if (b < 0) { - const t = PT / b; - if (Math.hypot(a * t, D0 - t) <= RF) hit(t, TOPS, Math.hypot(a * t, D0 - t)); - } - c = cyl(RF); - if (c && b * c[0] >= PG && b * c[0] <= PT) hit(c[0], SIDE, b * c[0]); - if (b < 0) hit(PG / b, GND, Math.hypot((a * PG) / b, D0 - PG / b)); - KIND[p] = k; - DEP[p] = best; - ANG[p] = atan2(a * best, D0 - best); - AUX[p] = aux; - } - -// A skeleton horse in its own frame: forward f, up u, out s, as bones -// [f, u, s, f, u, s, r, r]. Legs are added each frame as they gallop. -const SC = 1.45; -const BONES = [ - [-0.2, 0.03, 0, 0.12, 0.06, 0, 0.024, 0.022], - [0.12, 0.06, 0, 0.2, 0.19, 0, 0.028, 0.022], - [0.19, 0.22, 0, 0.33, 0.12, 0, 0.055, 0.03], - [0.19, 0.24, 0.02, 0.17, 0.31, 0.02, 0.016, 0.008], - [-0.17, 0.04, 0, -0.25, -0.05, 0, 0.032, 0.02], - [-0.07, -0.1, 0, 0.09, -0.09, 0, 0.012, 0.012], - [-0.25, 0.02, 0, -0.32, -0.03, 0, 0.013, 0.011], - [-0.32, -0.03, 0, -0.35, -0.15, 0, 0.011, 0.008], -]; -for (const f of [-0.08, -0.03, 0.02, 0.07]) for (const s of [-0.055, 0.055]) BONES.push([f, 0.04, s, f + 0.015, -0.09, s * 1.1, 0.012, 0.01]); -// A rider, sitting astride with both hands on the pole. -const RIDER = [ - [-0.02, 0.09, 0, 0.02, 0.33, 0, 0.075, 0.06], - [0.03, 0.3, 0.05, 0.01, 0.24, 0.02, 0.026, 0.02], - [0.03, 0.3, -0.05, 0.01, 0.24, -0.02, 0.026, 0.02], - [-0.02, 0.1, 0.06, 0.07, -0.07, 0.08, 0.036, 0.024], - [-0.02, 0.1, -0.06, 0.07, -0.07, -0.08, 0.036, 0.024], - [-0.02, 0.22, 0, -0.17, 0.27, 0, 0.045, 0.01], -]; - -export function setup(params) { - params.s = { - t: 0, th: 0.35, lurch: -99, next: 4, L: 0, flash: 0, hue: -1, speed: 2, horses: 5, fog: 0.06, - buf: new Float32Array(N * 3), zb: new Float32Array(N), bg: new Float32Array(N * 3), ga: new Float32Array(N), ge: new Uint8Array(N), - }; -} - -// The sky and the far fairground for a hue: a night deepening upward over a -// glow at the horizon, a moon, stars, and the black shapes of a big wheel -// and a tent that show between the horses. -function sky(S) { - const h = 0.76 - 0.36 * S.hue, bg = S.bg; - S.night = hsv(h, 0.75, 0.42); - S.fogc = hsv(h + 0.04, 0.3, 0.62); - S.dark = hsv(h, 0.8, 0.17); - S.top = hsv(h + 0.12, 0.88, 0.75); - S.panel = hsv(h + 0.02, 0.82, 0.36); - for (let y = 0, p = 0; y < H; y++) - for (let x = 0; x < W; x++, p++) { - const u = cl(y / 64, 0, 1), v = round((0.1 + 0.45 * u * u) * 24) / 24; - let c = hsv(h + 0.06 * u, 0.8 - 0.2 * u, v); - const dx = x + 0.5 - 11, dy = y + 0.5 - 13, dm = sqrt(dx * dx + dy * dy); - if (dm < 7.5) { - const cr = Math.hypot(dx + 2, dy - 1.5) < 2.2 || Math.hypot(dx - 2.5, dy + 2.5) < 1.4; - c = cr ? [0.78, 0.76, 0.66] : [0.98, 0.95, 0.8]; - } else if (dm < 10.5) c = mix(c, [0.8, 0.75, 0.9], dm < 9 ? 0.25 : 0.12); - else if (y < 50 && hash(p * 7 + 3) < 0.012) c = [0.75, 0.75, 0.85]; - // The big wheel on the right and a tent on the left, black. - const wx = x + 0.5 - 54, wy = y + 0.5 - 42, wr = Math.hypot(wx, wy), wa = atan2(wy, wx); - const wheel = (abs(wr - 17) < 0.7 || (wr < 17 && abs(fr((wa * 8) / TAU + 0.5) - 0.5) * wr < 0.45)) && y < 64; - const legs = y > 42 && y < 64 && abs(abs(x + 0.5 - 54) - (y - 42) * 0.35) < 0.6; - const tent = y > 45 && y < 64 && abs(x + 0.5 - 6) < (y - 45) * 0.75 + 0.5; - if (wheel || legs || tent) c = [0.03, 0.01, 0.05]; - bg.set(c, 3 * p); - } -} - -// Paint a tapered capsule between two projected points, keeping the nearest. -function capsule(S, A, B, ra, rb, rim, col, ghost) { - const { buf, zb, ga } = S; - const pa = (ra * F) / A[2], pb = (rb * F) / B[2], ex = B[0] - A[0], ey = B[1] - A[1], e2 = ex * ex + ey * ey || 1e-6, pad = max(pa, pb) + rim + 1; - for (let y = max(0, floor(min(A[1], B[1]) - pad)); y <= min(H - 1, floor(max(A[1], B[1]) + pad)); y++) - for (let x = max(0, floor(min(A[0], B[0]) - pad)); x <= min(W - 1, floor(max(A[0], B[0]) + pad)); x++) { - const qx = x + 0.5 - A[0], qy = y + 0.5 - A[1], u = cl((qx * ex + qy * ey) / e2, 0, 1), dx = qx - u * ex, dy = qy - u * ey; - const d = sqrt(dx * dx + dy * dy), pr = max(0.55, pa + (pb - pa) * u) + rim, p = y * W + x; - if (d >= pr) continue; - const w = ra + (rb - ra) * u, z = A[2] + (B[2] - A[2]) * u - (rim ? -0.04 : w * sqrt(1 - (d / pr) ** 2)); - if (z >= zb[p]) continue; - if (ghost) { - ga[p] = max(ga[p], ghost * (1 - 0.45 * (d / pr) ** 2)); - continue; - } - zb[p] = z; - const l = rim ? 1 : 0.78 + 0.22 * (1 - d / pr); - buf[3 * p] = col[0] * l; - buf[3 * p + 1] = col[1] * l; - buf[3 * p + 2] = col[2] * l; - } -} - -// One bulb: a hot core and a small halo, if nothing nearer hides it. -function bulb(S, P, col, b) { - const x = floor(P[0]), y = floor(P[1]), p = y * W + x; - if (x < 0 || x >= W || y < 0 || y >= H || P[2] > S.zb[p] + 0.03) return; - const { buf } = S; - for (let j = -2; j <= 2; j++) - for (let i = -2; i <= 2; i++) { - const xx = x + i, yy = y + j, d = i * i + j * j; - if (xx < 0 || xx >= W || yy < 0 || yy >= H || d > 5) continue; - const q = 3 * (yy * W + xx), g = d === 0 ? 1.2 * b : (d < 2 ? 0.32 : d < 3 ? 0.14 : 0.06) * b; - buf[q] += col[0] * g; - buf[q + 1] += col[1] * g; - buf[q + 2] += col[2] * g; - } - if (b > 0.5) buf.set(mix(col, [1, 1, 1], 0.5), 3 * p); -} - -export function update(dt, input, params) { - if (!params.s) setup(params); - const S = params.s, k = input.knobValues || [0, 2, 5, 0.06], d = cl(dt || 0, 0, 0.1); - const hue = cl(k[0], 0, 1); - if (hue !== S.hue) (S.hue = hue), sky(S); - S.speed = cl(k[1], 0.2, 3); - S.horses = round(cl(k[2], 5, 8)); - S.fog = cl(k[3], -1, 1); - const ds = (d * S.speed) / 2; - S.t += ds; - if ((input.btnPressed && input.btnPressed[0] && S.t - S.lurch > 3.2) || S.t >= S.next) (S.lurch = S.t), (S.next = S.t + 17); - // The lurch: a jolt up to speed in 0.35 s, three seconds of green, three to slow. - const lt = S.t - S.lurch; - S.L = lt < 0.35 ? sm(lt / 0.35) : lt < 3.2 ? 1 : 1 - sm((lt - 3.2) / 3); - S.flash = lt < 0.4 ? 1 - lt / 0.4 : 0; - S.th += ds * 0.42 * (1 + 1.5 * S.L); -} - -export function draw(display, params) { - const S = params.s; - if (!S || !S.night) return; - const { buf, zb, bg, ga, ge, th, L } = S; - const bc = mix([1, 0.55, 0.12], [0.35, 1, 0.25], L), glowc = mix(bc, [1, 1, 1], S.flash * 0.6); - const night = S.night, dark = S.dark, t = S.t; - zb.set(DEP); - for (let p = 0; p < N; p++) { - const kd = KIND[p], a = ANG[p] - th, h = AUX[p], q = 3 * p; - let c; - if (kd === SKY) { - buf[q] = bg[q]; - buf[q + 1] = bg[q + 1]; - buf[q + 2] = bg[q + 2]; - continue; - } else if (kd === CONE) { - // Eighteen stripes, the bright ones lit by the bulbs below and going - // from the bulb colour at the rim to magenta and the night at the top. - const up = (h - VT) / (HA - VT), face = 0.7 + 0.3 * cos(ANG[p]); - c = floor(fr(a / TAU) * 18) % 2 ? mix(mix(bc, S.top, sm(up * 1.6)), night, sm(up * 1.3 - 0.3)) : mix(dark, [0.05, 0.01, 0.06], up); - c = c.map((v) => v * face); - } else if (kd === VAL) { - const tooth = 0.07 * (1 - abs(2 * fr((a * 36) / TAU) - 1)); - c = h > 0.975 || h < 0.812 ? [0.95, 0.7, 0.25] : h < VB + tooth ? mix(bc, [0.8, 0.2, 0.5], 0.3) : [0.12, 0.03, 0.15]; - } else if (kd === CEIL) c = fr((a * 12) / TAU) < 0.1 ? [0.55, 0.38, 0.12] : mix([0.2, 0.05, 0.18], bc, 0.25 * h); - else if (kd === VIN) c = h > 0.98 || h < 0.82 ? [0.45, 0.3, 0.1] : [0.07, 0.02, 0.09]; - else if (kd === FIN) c = [0.95, 0.75, 0.3]; - else if (kd === COL) { - // Eight panels round the column, mirrors between gilt frames; a mirror - // catches the bulbs as it turns toward us. - const f = fr((a * 8) / TAU), face = cos(ANG[p]); - c = h > 0.86 || h < -0.86 || f < 0.12 ? [0.85, 0.62, 0.2] : floor((a * 8) / TAU) % 2 ? mix([0.3, 0.26, 0.45], bc, 0.6 * max(0, face) ** 3 + 0.1) : [0.08, 0.12, 0.1]; - c = c.map((v) => v * (0.55 + 0.45 * max(0, face))); - } else if (kd === TOPS) c = mix(floor((a * 12) / TAU) % 2 ? [0.2, 0.14, 0.24] : [0.13, 0.08, 0.17], bc, 0.1 + 0.15 * h); - else if (kd === SIDE) { - const f = fr((a * 16) / TAU); - c = h > PT - 0.015 || h < PG + 0.015 ? [0.85, 0.6, 0.2] : f < 0.5 ? S.panel : [0.12, 0.03, 0.14]; - } else { - // Ground: dark earth, warm near the platform from the bulbs. - const g = round(6 * exp(-(h - RF) * 2.4)) / 6, far = cl((h - 4) / 20, 0, 1); - c = mix(mix([0.06, 0.04, 0.08], bc, 0.3 * g), bg.slice(q, q + 3), far); - } - buf[q] = c[0]; - buf[q + 1] = c[1]; - buf[q + 2] = c[2]; - } - // Poles, horses and riders, ring order does not matter with the depths. - const n = S.horses, legs = [], ghost = sm(L * 1.6 - 0.4); - ga.fill(0); - ge.fill(0); - for (let i = 0; i < n; i++) { - const ph = th + (TAU * i) / n, sp = sin(ph), cp = cos(ph), bob = 3 * th + i * PI; - const hb = -0.33 + 0.09 * sin(bob), sw = 0.035 * sin(bob + 0.8); - const at = (f, u, s) => proj(RP * sp + SC * (f * cp + s * sp), hb + SC * u, RP * cp + SC * (-f * sp + s * cp)); - // Brass pole, twisted: bright bands climb it as it turns. - const P0 = proj(RP * sp, PT, RP * cp), P1 = proj(RP * sp, VT, RP * cp), hw = max(0.5, (0.028 * F) / P0[2]), dim = 1 - (0.42 * (P0[2] - 4.2)) / 1.56; - for (let y = max(0, floor(P1[1])); y <= min(H - 1, floor(P0[1])); y++) - for (let x = max(0, round(P0[0] - hw)); x < min(W, round(P0[0] + hw)); x++) { - const p = y * W + x; - if (P0[2] >= zb[p]) continue; - zb[p] = P0[2]; - const l = dim * (0.72 + 0.28 * sin(y * 1.1 + th * 9 + x)); - buf.set([0.98 * l, 0.74 * l, 0.28 * l], 3 * p); - } - // The skeleton: a dark rim round every bone first, then the bones. - legs.length = 0; - for (const s of [-0.045, 0.045]) { - const g = s > 0 ? sw : -sw; - legs.push([0.1, -0.03, s, 0.18 + g, -0.13, s, 0.019, 0.017], [0.18 + g, -0.13, s, 0.13 + 1.5 * g, -0.24, s, 0.017, 0.015]); - legs.push([-0.19, -0.02, s, -0.25 - g, -0.14, s, 0.021, 0.017], [-0.25 - g, -0.14, s, -0.35 - 1.5 * g, -0.22, s, 0.017, 0.015]); - } - const bone = mix(mix([0.95, 0.92, 0.8], [0.55, 1, 0.5], 0.25 * L), night, 0.5 * (1 - dim)); - for (const rim of [0.9, 0]) - for (const B of [...BONES, ...legs]) capsule(S, at(B[0], B[1], B[2]), at(B[3], B[4], B[5]), SC * B[6], SC * B[7], rim, rim ? [0.02, 0, 0.03] : bone.map((v) => v * dim), 0); - // Hollow eye sockets, wherever the skull faces us. - for (const s of [-0.045, 0.045]) { - const E = at(0.235, 0.2, s), r = max(0.75, (0.026 * SC * F) / E[2]); - for (let y = floor(E[1] - r); y <= E[1] + r; y++) - for (let x = floor(E[0] - r); x <= E[0] + r; x++) { - const p = y * W + x; - if (x < 0 || x >= W || y < 0 || y >= H || Math.hypot(x + 0.5 - E[0], y + 0.5 - E[1]) > r || E[2] - 0.07 > zb[p]) continue; - buf.set([0.02, 0, 0.03], 3 * p); - } - } - if (ghost > 0.01) { - for (const B of RIDER) capsule(S, at(B[0], B[1], B[2]), at(B[3], B[4], B[5]), SC * B[6], SC * B[7], 0, null, ghost * dim); - const Hc = at(0.04, 0.44, 0), hr = (0.085 * SC * F) / Hc[2]; - capsule(S, Hc, Hc, 0.085 * SC, 0.085 * SC, 0, null, ghost * dim); - // The rider turns its face to us: two dark eyes. - for (const dx of [-0.42, 0.42]) { - const x = floor(Hc[0] + dx * hr); - for (let y = floor(Hc[1] - 0.35 * hr); y <= Hc[1] + 0.25 * hr; y++) if (x >= 0 && x < W && y >= 0 && y < H && Hc[2] < zb[y * W + x] + 0.1) ge[y * W + x] = 1; - } - } - } - // Bulbs: round the valance, under the canopy, on the column and round the - // platform's edge, each flickering on its own. - const lamp = (j, P) => { - let b = 0.82 + 0.18 * sin(t * (6 + (j % 5)) + j * 1.7); - if (hash(j * 131 + floor(t * 2.5)) < 0.05) b = 0.25; - b = min(1.4, b * (1 + S.flash)); - bulb(S, P, glowc, b); - }; - for (let j = 0; j < 28; j++) { - const a = th + (TAU * j) / 28; - lamp(j, proj(1.005 * sin(a), 0.9, 1.005 * cos(a))); - if (j < 16) lamp(j + 40, proj(0.62 * sin(a * 1.75), 0.995, 0.62 * cos(a * 1.75))); - if (j < 24) lamp(j + 80, proj(1.055 * sin(a * 1.1667), -1.01, 1.055 * cos(a * 1.1667))); - if (j < 14) lamp(j + 120, proj(0.2 * sin(a * 2), j % 2 ? 0.62 : -0.62, 0.2 * cos(a * 2))); - } - // Riders: pale green and half see-through, with dark eyes. - if (ghost > 0.01) - for (let p = 0; p < N; p++) { - const g = ga[p], q = 3 * p; - if (!g) continue; - const c = ge[p] ? [0, 0.06, 0.02] : [0.62, 1, 0.66]; - for (let i = 0; i < 3; i++) buf[q + i] = buf[q + i] * (1 - 0.75 * g) + c[i] * 0.75 * g; - } - // Ground fog, its top edge rolling sideways and wisps drifting through it. - const u = (S.fog + 1) / 2, top = 129 - 58 * u, dens = 0.7 * sm(u * 2.2), fc = mix(S.fogc, bc, 0.12); - for (let x = 0; x < W; x++) { - const yt = top + 3 * sin(x * 0.16 + t * 0.7) + 2 * sin(x * 0.37 - t * 1.1); - for (let y = max(0, floor(yt)); y < H; y++) { - const wisp = 0.62 + 0.38 * sin(x * 0.21 + y * 0.43 - t * 1.3) * sin(x * 0.13 - y * 0.19 + t * 0.8); - const a = round(dens * sm((y - yt) / 16) * wisp * 8) / 8, q = 3 * (y * W + x); - if (!a) continue; - for (let i = 0; i < 3; i++) buf[q + i] = buf[q + i] * (1 - a) + fc[i] * a; - } - } - for (let y = 0, p = 0; y < H; y++) - for (let x = 0; x < W; x++, p++) display.setPixel(x, y, round(cl(buf[3 * p], 0, 1) * 255), round(cl(buf[3 * p + 1], 0, 1) * 255), round(cl(buf[3 * p + 2], 0, 1) * 255)); -} diff --git a/incoming/pattern-study/patterns/house-of-usher.js b/incoming/pattern-study/patterns/house-of-usher.js deleted file mode 100644 index e496b4dfb..000000000 --- a/incoming/pattern-study/patterns/house-of-usher.js +++ /dev/null @@ -1,458 +0,0 @@ -// ===== Patternflow pattern ===== -// Title: House of Usher -// Family: halloween -// Idea: A grey mansion stands over a black tarn under racing storm cloud, a zigzag fissure from its roof to the water glowing slowly red until it opens on a blood-red full moon, the two halves rush apart and sink into the tarn in a burst of spray and rings, the moon shines over the closing water, and a leaden mist rises out of which the house stands again. -// Author: patternflow-lab -// Date: 2026-10-26 -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: original, pattern round 53, after Edgar Allan Poe's The Fall of the House of Usher (1839) -// Round: 53 -// =============================== -// @matrix 64x128 -// @knobs Hue=0..1, Speed=0.2..3, Storm=-1..1, Mist=0..1 -// -// Knob 1: Hue. The storm sky and its reflection, kept on the cool side so the dim cloud never reads brown: violet at 0, indigo at 0.25, blue at 0.4, teal at 0.7, a sickly green at 1. The moon stays blood red and the one lit window stays green. -// Knob 2: Speed. At 2 one round of the story takes 17 seconds: the fissure glows for 6 and opens in 1.6, the halves sink for 3.2, the moon shines over the rings until the mist rises at 11.4, and the house is back by 15.2 and clear of the mist by 16.6. The first round opens 1.5 seconds in, so the first fall comes at 6.1. At 0.2 ten times slower, at 3 half as fast again; clouds, lightning and rain keep the same clock. -// Knob 3: Storm. At -1 still air: no rain, no lightning, slow cloud and a glassy tarn. At 0, where it opens, 92 drops of rain and a strike about every 5 seconds, the first at 2.4; at 1, 260 drops, a strike every 1.5 seconds and the tarn chopped by the wind into broken bars of reflection. -// Knob 4: Mist. The leaden vapour that hangs over the tarn and the foot of the house: a thin line at 0.06 where it opens, a bank that hides the lower windows at 0.5, and at 1 a fog over everything below the roofs. The mist that ends each fall rises on top of this. -// Buttons: K1 tap while the house stands opens the fissure now. - -const W = 64, H = 128, N = W * H, WL = 76, NU = W * WL; -const { floor, min, max, sqrt, abs, sin, cos, exp, round, 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); -}; -const mix = (a, b, t) => a + (b - a) * t; - -// The story, in seconds at Speed 2. -const T_WIDEN = 6, T_FALL = 7.6, T_GONE = 10.8, T_MIST = 11.4, T_BACK = 13.4, T_END = 17, T_START = 1.5; - -function hsv(h, s, v, out) { - h = (h - floor(h)) * 6; - const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); - out[0] = [v, q, p, p, t, v][i]; - out[1] = [t, v, v, q, p, p][i]; - out[2] = [p, p, t, v, v, q][i]; -} - -// A fixed generator for the house, so its stones never change. -function prng(seed) { - let a = seed >>> 0; - return () => { - a = (a + 0x6d2b79f5) >>> 0; - let t = a; - t = Math.imul(t ^ (t >>> 15), t | 1); - t ^= t + Math.imul(t ^ (t >>> 7), t | 61); - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; -} - -// Value noise on a 16 x 16 lattice that wraps. -const LAT = new Float32Array(256); -{ - const r = prng(11); - for (let i = 0; i < 256; i++) LAT[i] = r(); -} -function noise(x, y) { - const xi = floor(x), yi = floor(y), fx = x - xi, fy = y - yi; - const u = fx * fx * (3 - 2 * fx), v = fy * fy * (3 - 2 * fy); - const a = LAT[(yi & 15) * 16 + (xi & 15)], b = LAT[(yi & 15) * 16 + ((xi + 1) & 15)]; - const c = LAT[((yi + 1) & 15) * 16 + (xi & 15)], d = LAT[((yi + 1) & 15) * 16 + ((xi + 1) & 15)]; - return mix(mix(a, b, u), mix(c, d, u), v); -} - -// The fissure: a zigzag from the gable's peak down through the door to the water. -const FIS = [[32.5, 16], [30, 23], [35, 30], [29.5, 37], [34.5, 45], [29.5, 52], [34, 58], [31, 64], [33, 70], [32, 76]]; -function fisAt(y) { - if (y <= FIS[0][1]) return FIS[0][0]; - for (let i = 1; i < FIS.length; i++) if (y <= FIS[i][1]) return mix(FIS[i - 1][0], FIS[i][0], (y - FIS[i - 1][1]) / (FIS[i][1] - FIS[i - 1][1])); - return FIS[FIS.length - 1][0]; -} -// The same, looked up in quarter rows. -const FT = new Float32Array(WL * 4 + 1); -for (let i = 0; i <= WL * 4; i++) FT[i] = fisAt(i / 4); -const fisX = (y) => FT[cl(round(y * 4), 0, WL * 4)]; - -// Materials: 1 wall, 2 roof, 3 vacant window, 4 the lit window, 5 door. -function buildHouse(mat, shade, crack) { - const r = prng(5); - const win = (x, y, x0, y0, h) => x >= x0 && x < x0 + 3 && y >= y0 && y < y0 + h && (y > y0 || x === x0 + 1); - for (let y = 0; y < WL; y++) - for (let x = 0; x < W; x++) { - const p = y * W + x; - let m = 0; - if (x >= 4 && x <= 59 && y >= 46) m = 1; - if (x >= 21 && x <= 43 && y >= 37) m = 1; - // Hipped roofs on the wings and a steep gable over the middle. - if (y >= 35 && y < 46 && x >= 4 + (46 - y) * 0.9 && x <= 21) m = 2; - if (y >= 35 && y < 46 && x <= 59 - (46 - y) * 0.9 && x >= 43) m = 2; - if (y >= 16 && y < 37 && abs(x - 32) <= (y - 16) * 0.62) m = 2; - // Chimneys with caps. - if ((x >= 9 && x <= 12) || (x >= 51 && x <= 54)) if (y >= 28 && y < 40) m = 1; - if ((x >= 8 && x <= 13) || (x >= 50 && x <= 55)) if (y >= 27 && y < 29) m = 2; - if (m === 1) { - // Vacant windows, pointed at the top: three to a row on each wing. - for (const x0 of [7, 12, 17, 45, 50, 55]) for (const y0 of [50, 61]) if (win(x, y, x0, y0, 6)) m = x0 === 50 && y0 === 50 ? 4 : 3; - for (const x0 of [24, 38]) if (win(x, y, x0, 41, 9)) m = 3; - // The door: a pointed arch. - if (abs(x - 32) <= 3.5 && y >= 61 + abs(x - 32) * 0.9) m = 5; - } - if (m === 2 && (x - 32) ** 2 + (y - 29) ** 2 <= 6.5) m = 3; - mat[p] = m; - // Stone courses, blocks of different age, and dark streaks of fungus - // hanging from the eaves. - let s = 0.9 + 0.2 * r() + 0.25 * (noise(x * 0.18 + 3, y * 0.18) - 0.5); - if (m === 1) { - if (y % 4 === 0) s *= 0.8; - else if ((x + ((y >> 2) & 1) * 3) % 6 === 0) s *= 0.87; - } - if (m === 2 && y & 1) s *= 0.85; - shade[p] = s; - } - for (let k = 0; k < 26; k++) { - const x = 5 + floor(r() * 54), y0 = x > 20 && x < 44 ? 37 : 46, len = 3 + floor(r() * 9); - for (let y = y0; y < y0 + len && y < WL; y++) if (mat[y * W + x] === 1) shade[y * W + x] *= 0.62 + (0.3 * (y - y0)) / len; - } - // Distance from each LED to the fissure, for its faint line and its glow. - crack.fill(9); - for (let i = 1; i < FIS.length; i++) { - const [ax, ay] = FIS[i - 1], [bx, by] = FIS[i]; - for (let s = 0; s <= 40; s++) { - const fx = mix(ax, bx, s / 40), fy = mix(ay, by, s / 40); - for (let y = max(0, floor(fy) - 4); y <= min(WL - 1, floor(fy) + 4); y++) - for (let x = max(0, floor(fx) - 4); x <= min(W - 1, floor(fx) + 4); x++) { - const d = sqrt((x + 0.5 - fx) ** 2 + (y + 0.5 - fy) ** 2), p = y * W + x; - if (d < crack[p]) crack[p] = d; - } - } - } -} - -export function setup(params) { - const F = (n) => new Float32Array(n); - const s = { - hue: 0, sp: 2, storm: 0, mist: 0.06, - t: 0, c: T_START, wind: 0, flashAt: -9, nextStrike: 2.4, bolt: [], - mat: new Uint8Array(NU), shade: F(NU), crack: F(NU), fb: F(N * 3), hm: new Uint8Array(NU), cf: F(NU + 2 * W), - drops: [], deb: [], spray: [], rings: [], fallAcc: 0, chop: 0.2, slant: 0.3, - halves: [{ dx: 0, dy: 0, a: 0, px: 4.5 }, { dx: 0, dy: 0, a: 0, px: 59.5 }], - sky0: F(3), sky1: F(3), - }; - buildHouse(s.mat, s.shade, s.crack); - for (let i = 0; i < 260; i++) s.drops.push(newDrop(Math.random() * H)); - params.s = s; -} - -function newDrop(y) { - return { x: Math.random() * (W + 30) - 8, y, v: 0.8 + 0.4 * Math.random(), land: WL + 1 + Math.random() * (H - WL - 2), dim: 0 }; -} - -// Lightning: a jagged channel from the top down behind the house. -function makeBolt(s) { - const pts = [[8 + Math.random() * 48, -1]]; - while (pts[pts.length - 1][1] < 44) { - const [x, y] = pts[pts.length - 1]; - pts.push([cl(x + (Math.random() - 0.5) * 9, 2, 61), y + 3 + Math.random() * 5]); - } - s.bolt = pts; -} - -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 = cl(k[0], 0, 1); - s.sp = cl(k[1], 0.2, 3); - s.storm = (cl(k[2], -1, 1) + 1) / 2; - s.mist = cl(k[3], 0, 1); - const ds = (d * s.sp) / 2, c0 = s.c; - s.t += ds; - s.c += ds; - if (input.btnPressed && input.btnPressed[0] && s.c < T_WIDEN) s.c = T_WIDEN; - if (s.c >= T_END) s.c -= T_END; - const c = s.c, st = s.storm; - // The whirlwind that comes as the fissure opens. - const whirl = sm(5.6, 6.6, c) * (1 - sm(9.5, 11.5, c)); - s.wind += ds * (0.35 + 1.4 * st) * (1 + 1.6 * whirl); - s.chop = 0.15 + 1.1 * st * st + 2.6 * sm(7.8, 8.6, c) * (1 - sm(10.6, 14, c)); - - if (s.t >= s.nextStrike) { - if (st > 0.04) { - s.flashAt = s.t; - makeBolt(s); - } - s.nextStrike = s.t + mix(8, 1.5, st) * (0.6 + 0.8 * Math.random()); - } - - // Rain falls past the house and lands on the tarn somewhere between the - // far shore and our feet, leaving a dimple. - const nd = round(260 * st ** 1.5), vy = 120 * (s.sp / 2) + 25, slant = 0.25 + 0.2 * st; - for (let i = 0; i < nd; i++) { - const r = s.drops[i]; - if (r.dim > 0) { - r.dim -= d; - if (r.dim <= 0) Object.assign(r, newDrop(-Math.random() * 30)); - continue; - } - r.y += r.v * vy * d; - r.x -= r.v * vy * slant * d; - if (r.y >= r.land) (r.y = r.land), (r.dim = 0.13); - } - s.slant = slant; - - // The two halves. Each turns about its outer foot, slides outward and - // sinks; the right one starts a quarter second later. - for (let h = 0; h < 2; h++) { - const f = s.halves[h], tf = c < T_GONE ? max(0, c - T_FALL - 0.25 * h) : 0, sg = h ? 1 : -1; - const shake = c > T_WIDEN && c < T_FALL + 0.6 ? 0.6 * sin(s.t * 55 + h * 2) * sm(T_WIDEN, T_FALL, c) : 0; - f.dx = sg * 8 * sm(0, 1.7, tf) + shake; - f.dy = 13 * tf * tf; - f.a = sg * 0.3 * sm(0.15, 2.3, tf); - f.px = h ? 59.5 : 4.5; - } - // Stones from the fissure, then spray where anything meets the water. - if (c > T_FALL && c < T_GONE - 0.6) { - s.fallAcc += ds * 34; - while (s.fallAcc >= 1 && s.deb.length < 90) { - s.fallAcc -= 1; - const h = Math.random() < 0.5 ? 0 : 1, f = s.halves[h], y = 18 + Math.random() * 56, x = fisX(y) + (h ? 2 : -2); - const [px, py] = place(f, x, y); - if (py < WL - 1) s.deb.push({ x: px, y: py, vx: (h ? 1 : -1) * (4 + 14 * Math.random()), vy: -6 * Math.random() }); - } - for (let h = 0; h < 2; h++) { - const f = s.halves[h], foot = place(f, h ? 40 : 24, 75); - if (Math.random() < ds * 10) splash(s, foot[0] + (Math.random() - 0.5) * 24, 2 + Math.random() * 3); - } - } - for (const b of s.deb) { - b.vy += 70 * ds; - b.x += b.vx * ds; - b.y += b.vy * ds; - if (b.y >= WL - 0.5) (b.dead = 1), splash(s, b.x, 1 + Math.random()); - } - s.deb = s.deb.filter((b) => !b.dead && b.x > -3 && b.x < W + 3); - for (const p of s.spray) { - p.vy += 75 * ds; - p.x += p.vx * ds; - p.y += p.vy * ds; - } - s.spray = s.spray.filter((p) => p.y < WL); - for (const r of s.rings) r.r += ds * 13; - s.rings = s.rings.filter((r) => r.r < 60); - if (c < c0) (s.deb.length = 0), (s.spray.length = 0); -} - -// Where house point (x, y) is now on the panel, for half f. -function place(f, x, y) { - const ca = cos(f.a), sa = sin(f.a), X = x - f.px, Y = y - 75; - return [f.px + ca * X - sa * Y + f.dx, 75 + sa * X + ca * Y + f.dy]; -} - -function splash(s, x, n) { - for (let i = 0; i < n && s.spray.length < 90; i++) s.spray.push({ x, y: WL - 1, vx: (Math.random() - 0.5) * 16, vy: -(10 + 24 * Math.random()) }); - if (s.rings.length < 14) s.rings.push({ x, r: 0.5, a: 0.5 + 0.1 * n }); -} - -export function draw(display, params) { - const s = params.s; - if (!s) return; - const { fb, mat, shade, crack, sky0, sky1, hm, cf } = s; - const c = s.c, t = s.t, st = s.storm, hue = 0.76 - 0.44 * s.hue; - hsv(hue, 0.62, 0.34, sky0); - hsv(hue + 0.03, 0.42, 0.66, sky1); - // The flash: two pulses, the second a little later and weaker. - const fa = t - s.flashAt, fl = fa < 0 || fa > 1 ? 0 : (exp(-fa * 10) + (fa > 0.14 ? 0.7 * exp(-(fa - 0.14) * 9) : 0)) * (0.5 + 0.5 * st); - // The moon: behind the house until the fissure opens, then full over the - // water until the mist takes it. - const moon = (0.28 + 0.72 * sm(5.4, 7.2, c)) * (1 - 0.72 * sm(12, 14.2, c)); - const glow = c < T_WIDEN ? 0.3 * sm(0.4, T_WIDEN, c) : c < T_GONE ? 0.3 + 0.7 * sm(T_WIDEN, 7.2, c) : 0; - const gap = c < T_GONE ? 3.4 * sm(T_WIDEN, T_FALL, c) + 2.5 * sm(T_FALL, 8.4, c) : 0; - const alpha = c < T_GONE ? 1 : sm(T_BACK, T_BACK + 1.8, c); - const MX = 32, MY = 51, MR = 14.5; - const w0 = s.wind, h0 = s.halves[0], h1 = s.halves[1]; - const c0a = cos(h0.a), s0a = sin(h0.a), c1a = cos(h1.a), s1a = sin(h1.a); - const lw = 0.72 + 0.28 * noise(t * 3.1, 5.5); - // The cloud field, two rows deeper than the sky so each LED can see - // whether cloud thins below it: there its underside catches the light. - // It is stretched sideways, so every other LED is the mean of its neighbours. - for (let y = 0; y < WL + 2; y++) { - const q = y * W; - for (let x = 0; x <= W; x += 2) cf[q + min(x, W - 1)] = noise(x * 0.045 + w0, y * 0.2 + 2) * 0.65 + noise(x * 0.1 + w0 * 1.7, y * 0.4) * 0.35; - for (let x = 1; x < W - 1; x += 2) cf[q + x] = (cf[q + x - 1] + cf[q + x + 1]) / 2; - } - - for (let y = 0; y < WL; y++) { - const gy = y / WL; - for (let x = 0; x < W; x++) { - const o = (y * W + x) * 3; - // Sky and racing cloud with faintly luminous undersides. - const n1 = cf[y * W + x], n2 = cf[(y + 2) * W + x]; - const dn = sm(0.4, 0.68, n1) * (1 - 0.6 * gy), under = cl((n1 - n2) * 7, 0, 1) * dn; - let r = mix(sky0[0], sky1[0], gy), g = mix(sky0[1], sky1[1], gy), b = mix(sky0[2], sky1[2], gy); - r = r * (1 - 0.42 * dn) + under * 0.42; - g = g * (1 - 0.42 * dn) + under * 0.36; - b = b * (1 - 0.42 * dn) + under * 0.5; - // The blood-red moon and its halo, dimmed where cloud crosses it. - const md = sqrt((x + 0.5 - MX) ** 2 + (y + 0.5 - MY) ** 2), mv = moon * (1 - 0.6 * dn); - if (md < MR) { - const e = md / MR, k = mv * (1 - 0.25 * e * e); - r = mix(r, 1, k); - g = mix(g, 0.22 - 0.14 * e, k); - b = mix(b, 0.12 - 0.06 * e, k); - } else if (md < MR + 30) { - const hk = mv * 0.55 * exp(-(md - MR) / 6); - r += hk * 0.85; - g += hk * 0.04; - b += hk * 0.22; - } - r += fl * 0.55; - g += fl * 0.5; - b += fl * 0.65; - // The house: either half, as it stands or as it falls. - hm[y * W + x] = 0; - if (alpha > 0) { - let m = 0, p = 0; - for (let h = 0; h < 2 && !m; h++) { - const f = h ? h1 : h0, ca = h ? c1a : c0a, sa = h ? s1a : s0a; - const X = x + 0.5 - f.px - f.dx, Y = y + 0.5 - 75 - f.dy; - const hx = f.px + ca * X + sa * Y, hy = 75 - sa * X + ca * Y; - const ix = floor(hx), iy = floor(hy); - if (ix < 0 || ix >= W || iy < 0 || iy >= WL) continue; - const q = iy * W + ix, fx = fisX(hy); - if (mat[q] && (h ? hx >= fx + gap / 2 : hx < fx - gap / 2)) (m = mat[q]), (p = q); - } - if (m) { - hm[y * W + x] = 1; - const sh = shade[p]; - let hr, hg, hb; - if (m === 1) (hr = 0.34 * sh), (hg = 0.32 * sh), (hb = 0.38 * sh); - else if (m === 2) (hr = 0.15 * sh), (hg = 0.14 * sh), (hb = 0.19 * sh); - else if (m === 4) (hr = 0.32 * lw), (hg = 1 * lw), (hb = 0.3 * lw); - else (hr = 0.02), (hg = 0.015), (hb = 0.035); - if (m <= 2) { - hr += fl * 0.4; - hg += fl * 0.38; - hb += fl * 0.5; - // The fissure: a darker line that glows red as the moon finds it, - // and the red it throws on the stone beside it. - const cd = crack[p]; - if (cd < 0.75 && gap < 0.5) (hr = mix(hr * 0.78, 0.85, glow)), (hg = mix(hg * 0.78, 0.06, glow)), (hb = mix(hb * 0.78, 0.12, glow)); - else if (cd < 4) { - const sp = glow * glow * (1 - cd / 4) * 0.55; - hr += sp; - hg += sp * 0.12; - hb += sp * 0.16; - } - } - r = mix(r, hr, alpha); - g = mix(g, hg, alpha); - b = mix(b, hb, alpha); - } - } - // White trunks of dead trees on the banks either side. - if ((x === 1 && y > 47) || (x === 2 && y > 52) || (x === 61 && y > 55) || (x === 62 && y > 60) || (x === 3 && y === 53) || (x === 4 && y === 51) || (x === 60 && y === 58)) (r = 0.55 + fl * 0.3), (g = 0.53 + fl * 0.3), (b = 0.6 + fl * 0.3); - fb[o] = r; - fb[o + 1] = g; - fb[o + 2] = b; - } - } - // The bolt, behind the house only where the sky shows. - if (fa >= 0 && fa < 0.2 && s.bolt.length) - for (let i = 1; i < s.bolt.length; i++) { - const [ax, ay] = s.bolt[i - 1], [bx, by] = s.bolt[i]; - for (let k = 0; k <= 8; k++) { - const x = floor(mix(ax, bx, k / 8)), y = floor(mix(ay, by, k / 8)); - if (x < 0 || x >= W || y < 0 || y >= WL) continue; - const o = (y * W + x) * 3; - if (!hm[y * W + x]) (fb[o] = 0.95), (fb[o + 1] = 0.9), (fb[o + 2] = 1); - } - } - // Falling stones and spray. - for (const b of s.deb) dot(fb, b.x, b.y, 0.42, 0.4, 0.46, 1); - for (const p of s.spray) dot(fb, p.x, p.y, 0.7, 0.68, 0.82, 0.8); - - // The tarn: the scene above, upside down, broken by the wind and by the - // fall, darker toward our feet. - const ch = s.chop, hr = sky1[0], hg = sky1[1], hb = sky1[2]; - for (let y = WL; y < H; y++) { - const dd = y - WL + 1, ps = 0.4 + dd / 28, rf = 0.52 - (0.24 * dd) / (H - WL); - for (let x = 0; x < W; x++) { - const ph = 0.55 * dd - 4.1 * t + 0.21 * x; - const xo = ch * ps * (sin(ph) + 0.6 * sin(0.23 * dd + 2.7 * t - 0.13 * x)); - const yo = ch * 0.6 * ps * sin(0.8 * dd - 3.3 * t + 0.1 * x); - const sx = cl(round(x + xo), 0, W - 1), sy = cl(round(WL - dd - yo), 0, WL - 1); - const i = (sy * W + sx) * 3, o = (y * W + x) * 3; - let r = fb[i] * rf * 0.85 + 0.01, g = fb[i + 1] * rf * 0.85 + 0.005, b = fb[i + 2] * rf + 0.02; - // Glints where the wavelets face the sky. - const gl = ch > 0.25 && cos(ph) > 0.6 ? cos(ph) * noise(x * 0.4 + t * 1.3, dd * 0.7 - t * 0.6) : 0; - if (gl > 0.6) { - const k = min(1, ch) * (gl - 0.6) * 1.2; - r += hr * k; - g += hg * k; - b += hb * k; - } - fb[o] = r; - fb[o + 1] = g; - fb[o + 2] = b; - } - } - // Rings where the house and its stones went in. - for (const rg of s.rings) { - const k = rg.a * 0.6 * (1 - rg.r / 60) * min(1, rg.r / 4); - for (let y = WL; y < H; y++) { - const dy = (y - WL + 0.5) / 0.5, wo = (rg.r + 0.9) ** 2 - dy * dy; - if (wo < 0) break; - const a = sqrt(max(0, (rg.r - 0.9) ** 2 - dy * dy)), b = sqrt(wo); - for (let x = max(0, Math.ceil(rg.x - b)); x <= min(W - 1, rg.x + b); x++) { - if (abs(x - rg.x) < a) continue; - const o = (y * W + x) * 3; - fb[o] += 0.34 * k; - fb[o + 1] += 0.3 * k; - fb[o + 2] += 0.42 * k; - } - } - } - // Mist: a leaden vapour over the water, rising thick after the fall. - const boost = sm(T_MIST, T_MIST + 1.8, c) * (1 - sm(14.8, 16.6, c)), den = s.mist * 0.75 + boost; - if (den > 0.01) { - const top = mix(64, 6, min(1, s.mist * 0.7 + boost)), wv = s.t * 0.2; - for (let y = 0; y < H; y++) { - const v = y < 74 ? sm(top, 74, y) : 1 - 0.55 * sm(74, H, y); - if (v <= 0) continue; - for (let x = 0; x < W; x++) { - const n = 0.6 * noise(x * 0.05 - wv, y * 0.16 + wv * 0.2) + 0.4 * noise(x * 0.12 - wv * 1.6, y * 0.3 + 7); - const m = min(0.88, den * v * (0.25 + sm(0.3, 0.75, n))); - const o = (y * W + x) * 3; - fb[o] = fb[o] * (1 - 0.45 * m) + 0.4 * m; - fb[o + 1] = fb[o + 1] * (1 - 0.45 * m) + 0.38 * m; - fb[o + 2] = fb[o + 2] * (1 - 0.45 * m) + 0.46 * m; - } - } - } - // Rain streaks, and dimples where drops strike the tarn. - const nd = round(260 * st ** 1.5); - for (let i = 0; i < nd; i++) { - const r = s.drops[i]; - if (r.dim > 0) { - const a = r.dim / 0.13; - dot(fb, r.x, r.y, 0.3 * a, 0.28 * a, 0.36 * a, 1, true); - dot(fb, r.x - 1, r.y, 0.15 * a, 0.14 * a, 0.18 * a, 1, true); - dot(fb, r.x + 1, r.y, 0.15 * a, 0.14 * a, 0.18 * a, 1, true); - continue; - } - for (let k = 0; k < 4; k++) dot(fb, r.x + k * s.slant, r.y - k, 0.2 * (1 - k / 4), 0.19 * (1 - k / 4), 0.25 * (1 - k / 4), 1, true); - } - for (let q = 0; q < N; q++) { - const o = q * 3; - display.setPixel(q % W, (q / W) | 0, min(255, fb[o] * 255), min(255, fb[o + 1] * 255), min(255, fb[o + 2] * 255)); - } -} - -function dot(fb, x, y, r, g, b, a, add) { - const ix = floor(x), iy = floor(y); - if (ix < 0 || ix >= W || iy < 0 || iy >= H) return; - const o = (iy * W + ix) * 3; - if (add) (fb[o] += r), (fb[o + 1] += g), (fb[o + 2] += b); - else (fb[o] = mix(fb[o], r, a)), (fb[o + 1] = mix(fb[o + 1], g, a)), (fb[o + 2] = mix(fb[o + 2], b, a)); -} diff --git a/incoming/pattern-study/patterns/medusa.js b/incoming/pattern-study/patterns/medusa.js deleted file mode 100644 index b1c5102a1..000000000 --- a/incoming/pattern-study/patterns/medusa.js +++ /dev/null @@ -1,453 +0,0 @@ -// ===== Patternflow pattern ===== -// Title: Medusa -// Family: halloween -// Idea: A Gorgon's head stands against a purple night with an orange glow over her crown: a crown of green and violet snakes rises from her hair and hangs past her cheeks, writhing, flicking their tongues and striking, until her eyes open toxic green and a ring of their light runs out from her gaze, turning snakes, face and robe to grey stone that holds, cracks and falls away in flakes so the colour and the writhing come back. -// Author: patternflow-lab -// Date: 2026-10-26 -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: original, pattern round 53, after the Gorgon Medusa of Greek myth -// Round: 53 -// =============================== -// @matrix 64x128 -// @knobs Hue=0..1, Speed=0.2..3, Snakes=9..17, Writhe=-1..1 -// -// Knob 1: Hue. Turns the snakes, her eyes and the ring of her gaze round the wheel together. At 0 two snakes in three are toxic green with a black zigzag, the third violet with an orange one, and her eyes and the ring are green; her face, robe and the night keep their colours. -// Knob 2: Speed. At 2 a wave runs up each snake at 13 to 23 LEDs a second, each snake strikes every 4 to 7 seconds and flicks its tongue every 1.6 to 2.6, and she looks 3.4 seconds in and then every 13.4: her eyes open in 0.35 seconds, the stone takes 1.7 to pass every corner, holds about 1.3, cracks over 0.8 and falls away over 1.8. At 0.2 ten times slower, at 3 half as fast again. -// Knob 3: Snakes. How many snakes grow from her hair, 9 to 17. With 9 one hangs on each side and seven rise; from 13 two hang on each side. -// Knob 4: Writhe. How hard they writhe: at -1 the bends are a third as deep and slower, at 0.06 where it opens a little over the middle, at 1 they coil and knot. -// Buttons: K1 tap while the snakes writhe: she looks at you now and the stone wave starts. - -const W = 64, H = 128, N = W * H; -const { floor, min, max, abs, sqrt, sin, cos, hypot, PI } = Math; -const TAU = 2 * PI; -const cl = (v, a, b) => (v < a ? a : v > b ? b : v); -const lerp = (a, b, t) => a + (b - a) * t; -const ease = (t) => ((t = cl(t, 0, 1)), t * t * (3 - 2 * t)); -const hash = (i) => { - let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); - h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); - return ((h ^ (h >>> 16)) >>> 0) / 4294967296; -}; -function hsv(h, s, v) { - 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); - return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; -} - -// Her eyes sit at (CX -+ EDX, EY); the gaze runs out from between them. -const CX = 32, EY = 72, EDX = 6.5; -// Snakes: up to MS of them, each cut into stone segments SEG LEDs long. -const SEG = 5, MSEG = 16, MS = 17; -const SKIN = [178, 222, 166]; - -// Half width of the face at height y: a dome over the brow, a long jaw to -// a narrow chin at y = 96. -function faceHW(y) { - if (y < 70) return y < 55 ? 0 : 13.2 * sqrt(max(0, 1 - ((70 - y) / 15) ** 2)); - const t = (y - 70) / 26; - return t >= 1 ? 0 : 13.2 * sqrt(1 - t ** 2.5) * (1 - 0.25 * t); -} - -// The night behind her: distance from the glow over her crown, colour. -const GLOW = [[0, 255, 132, 34], [16, 236, 62, 92], [30, 120, 26, 128], [48, 40, 8, 62], [72, 12, 3, 22]]; -function glow(d) { - let i = 0; - while (i < GLOW.length - 2 && d > GLOW[i + 1][0]) i++; - const A = GLOW[i], B = GLOW[i + 1], u = cl((d - A[0]) / (B[0] - A[0]), 0, 1); - return [lerp(A[1], B[1], u), lerp(A[2], B[2], u), lerp(A[3], B[3], u)]; -} - -// The still layer: the night, her hair, face, neck and robe. fig marks the -// LEDs that turn to stone; cell and edge split them into flakes. -function still(S) { - const { bg, fig, col, stone, cell, edge, cells } = S; - for (let p = 0; p < N; p++) { - const x = p % W, y = floor(p / W), cx = x + 0.5, cy = y + 0.5, o = 3 * p; - const c = glow(hypot((cx - 32) * 1.15, cy - 44)); - bg[o] = c[0]; - bg[o + 1] = c[1]; - bg[o + 2] = c[2]; - let f = 0, r = 0, g = 0, b = 0; - const ax = abs(cx - CX), hw = faceHW(cy), u = (cx - CX) / max(hw, 1e-3); - const neckHW = 6.5 + max(0, cy - 98) * 0.5, shTop = 100 + 11 * max(0, (ax - 7) / 25) ** 1.2; - const hairE = ((cx - CX) / 17.5) ** 2 + ((cy - 66) / 13.5) ** 2; - if (ax < hw) { - // Skin: pale jade, lit from the upper left, the sides falling off. - f = 1; - let k = (0.5 + 0.5 * sqrt(max(0, 1 - u * u))) * (1 - 0.14 * u) * (cy < 70 ? 1.04 : 1); - [r, g, b] = SKIN; - for (const s of [-1, 1]) { - const ex = CX + s * EDX, e = ((cx - ex) / 4.8) ** 2 + ((cy - 71.6) / 3) ** 2; - // Sockets in lavender shadow. - if (e < 1) (k *= 0.6 + 0.4 * e), (r = lerp(150, r, e)), (g = lerp(110, g, e)), (b = lerp(170, b, e)); - // Brows: dark bars rising outward, the inner ends low. - const bx0 = CX + s * 2.4, by0 = 67.4, bx1 = CX + s * 10.6, by1 = 64.4; - const t = cl(((cx - bx0) * (bx1 - bx0) + (cy - by0) * (by1 - by0)) / ((bx1 - bx0) ** 2 + (by1 - by0) ** 2), 0, 1); - if (hypot(cx - lerp(bx0, bx1, t), cy - lerp(by0, by1, t)) < 0.8) (r = 52), (g = 30), (b = 58), (k = 1); - // The hollow under the cheekbone. - if (s * u > 0.5 && s * u < 0.86 && cy > 79 && cy < 87) k *= 0.82; - } - // Nose: a lit ridge, its shadow side, the shade under it, two nostrils. - if (x === 31 && y >= 71 && y <= 80) k *= 1.13; - if ((x === 33 || x === 34) && y >= 74 && y <= 81) k *= 0.74; - if (y === 83 && x >= 29 && x <= 35) k *= 0.72; - if (y === 82 && (x === 30 || x === 34)) (k = 0.45), (r = 120), (g = 80), (b = 110); - // Mouth: dark lips, the line between them and turned-down corners. - if (y === 87 && x >= 28 && x <= 36) (r = 132), (g = 50), (b = 122), (k = 1); - if (y === 88 && x >= 27 && x <= 37) (r = 46), (g = 10), (b = 44), (k = 1); - if (y === 89 && x >= 28 && x <= 36) (r = 176), (g = 78), (b = 160), (k = 1); - if (y === 89 && (x === 27 || x === 37)) (r = 46), (g = 10), (b = 44), (k = 1); - if (y === 90 && x >= 29 && x <= 35) k *= 0.8; - if (y >= 92 && y <= 93 && ax < 3) k *= 1.08; - // The jaw's edge darkens so the face stands off the neck. - if (abs(u) > 0.88 || faceHW(cy + 1.2) < ax) k *= 0.62; - (r *= k), (g *= k), (b *= k); - } else if (hairE < 1 && (cy < 74 || cy < 88 - ax * 0.3)) { - // The hair the snakes grow from: dark violet coils. - f = 1; - const coil = sin(cx * 1.3 + cy * 0.6) + sin(cy * 1.1 - cx * 0.7); - const k = 0.7 + 0.3 * (1 - hairE) + (coil > 1 ? 0.5 : 0); - (r = 34 * k), (g = (coil > 1.3 ? 52 : 12) * k), (b = 46 * k); - } else if (cy >= 89 && cy < shTop + 2 && ax < neckHW) { - // Neck, in the jaw's shadow at the top. - f = 1; - const v = (cx - CX) / neckHW; - let k = (0.45 + 0.4 * sqrt(max(0, 1 - v * v))) * (1 - 0.1 * v); - if (cy < 95) k *= 0.62; - (r = SKIN[0] * k), (g = SKIN[1] * k), (b = SKIN[2] * k); - } else if (cy >= shTop) { - f = 1; - // The robe: violet, its shoulders and collar rimmed, with folds. - let k = 1, rr = 92, gg = 30, bb = 124; - const dy = cy - 104; - for (const fx of [9.5 + dy * 0.35, 18 + dy * 0.55, 26 + dy * 0.75]) { - if (abs(ax - fx) < 0.6) k = 0.48; - else if (abs(ax - fx - 1) < 0.5) k = 1.4; - } - if (cy < shTop + 1.4) (rr = 180), (gg = 86), (bb = 204), (k = 1); - (r = rr * k), (g = gg * k), (b = bb * k); - // A clasp: a toxic green stone at the collar. - if (hypot(cx - CX, cy - 104) < 1.7) (r = 70), (g = 240), (b = 90); - } - fig[p] = f; - col[o] = min(255, r); - col[o + 1] = min(255, g); - col[o + 2] = min(255, b); - grey(stone, o, r, g, b, p); - } - // Flakes: cells round a jittered 6 LED grid; their borders are the cracks. - const G = 6, GW = 11, GH = 22, sx = [], sy = []; - for (let j = 0; j < GH; j++) - for (let i = 0; i < GW; i++) sx.push(i * G + 3 + (hash(j * 97 + i) - 0.5) * 4), sy.push(j * G + 3 + (hash(j * 89 + i + 5000) - 0.5) * 4); - for (let p = 0; p < N; p++) { - if (!fig[p]) continue; - const x = (p % W) + 0.5, y = floor(p / W) + 0.5, gi = floor(x / G), gj = floor(y / G); - let best = 1e9, id = 0; - for (let j = gj - 1; j <= gj + 1; j++) - for (let i = gi - 1; i <= gi + 1; i++) { - if (i < 0 || j < 0 || i >= GW || j >= GH) continue; - const d = (x - sx[j * GW + i]) ** 2 + (y - sy[j * GW + i]) ** 2; - if (d < best) (best = d), (id = j * GW + i); - } - cell[p] = id; - } - for (let p = 0; p < N; p++) { - if (!fig[p]) continue; - edge[p] = (p % W < W - 1 && fig[p + 1] && cell[p + 1] !== cell[p]) || (p + W < N && fig[p + W] && cell[p + W] !== cell[p]) ? 1 : 0; - (cells[cell[p]] || (cells[cell[p]] = [])).push(p); - } -} - -export function setup(params) { - const S = { - T: 0, c0: 0, first: true, hue: -1, palHue: -2, n: 9, amp: 1, R: -1, ph: -9, look: 0, lastT: 0, - bg: new Float32Array(N * 3), col: new Float32Array(N * 3), stone: new Float32Array(N * 3), buf: new Float32Array(N * 3), - fig: new Uint8Array(N), cell: new Int16Array(N), edge: new Uint8Array(N), cells: [], - cellState: new Uint8Array(242), cellCrack: new Float32Array(242), cellFlake: new Float32Array(242), - own: new Int8Array(N), dd: new Float32Array(N), vv: new Float32Array(N), ss: new Float32Array(N), - snakes: [], segStone: new Uint8Array(MS * MSEG), segCrack: new Float32Array(MS * MSEG), segFlake: new Float32Array(MS * MSEG), - parts: [], pal: [], eye: [0, 0, 0], - }; - still(S); - for (let i = 0; i < MS; i++) S.snakes.push({ tau: hash(i * 13 + 7) * 20, rate: 1, frozen: false, pts: new Float32Array(720), np: 0, L: 40, strike: 0, mouth: 0, tongue: 0 }); - params.s = S; -} - -// The story, in seconds at Speed 2 after her eyes start to open: the stone -// wave leaves her eyes at OPEN and takes WAVE to pass every corner, cracks -// run from CRACK - 0.8, flakes fall from FLAKE, her eyes close from BACK and -// at END the snakes writhe again until the next look. -const OPEN = 0.35, WAVE = 1.7, CRACK = 4.1, FLAKE = 4.2, BACK = 6.3, END = 6.6; -const gazeAt = (S) => (S.first ? 3.4 : 6.8); - -export function update(dt, input, params) { - if (!params.s) setup(params); - const S = params.s, k = input.knobValues || [0, 2, 9, 0.06]; - const step = cl(dt || 0, 0, 0.1) * cl(k[1], 0.2, 3) * 0.5; - S.hue = cl(k[0], 0, 1); - S.n = cl(Math.round(k[2]), 9, MS); - const wr = cl(k[3], -1, 1); - S.amp = 1 + 0.65 * wr; - S.T += step; - let g = S.T - S.c0 - gazeAt(S); - if (input.btnPressed && input.btnPressed[0] && g < 0) (S.c0 = S.T - gazeAt(S)), (g = 0); - if (g >= END) { - // A new cycle: the stone is gone and every snake moves. - S.c0 = S.T; - S.first = false; - S.cellState.fill(0); - S.segStone.fill(0); - for (const sn of S.snakes) sn.frozen = false; - g = -gazeAt(S); - } - S.ph = g; - // The wave's radius, from her eyes out past every corner. - S.R = g < OPEN ? -1 : g < WAVE + OPEN ? ((g - OPEN) / WAVE) * 125 : 200; - S.look = g < 0 ? 0 : g < OPEN ? g / OPEN : g < BACK ? 1 : 1 - (g - BACK) / (END - BACK); - for (let i = 0; i < MS; i++) { - const sn = S.snakes[i]; - // A snake stops when the wave reaches its root and moves again once its - // last flake is off. - if (sn.frozen && g > FLAKE) { - let left = 0; - for (let j = 0; j < MSEG; j++) if (S.segStone[i * MSEG + j] === 1) left++; - if (left === 0) sn.frozen = false; - } - sn.rate = sn.frozen ? 0 : min(1, sn.rate + step * 3); - sn.tau += step * sn.rate * (1 + 0.2 * wr); - } -} - -// One snake's centreline: its root on the crown, a heading that bends with -// a wave running from root to head, and the strike that throws the head -// out. The outermost snakes hang down past her cheeks and lift their heads -// beside her shoulders; the rest rise in a fan above her. -function body(S, i) { - const sn = S.snakes[i], n = S.n, r = (q) => hash(i * 31 + q); - const nh = n >= 13 ? 2 : 1, hang = i < nh || i >= n - nh, sd = i < n / 2 ? -1 : 1; - const t = sn.tau, amp = S.amp; - // Strikes every 4 to 7 seconds: out fast, back slower. - const per = 4 + 3 * r(1), sp = ((t + per * r(2)) % per) / 0.5; - const strike = sp < 1 ? (sp < 0.25 ? sp / 0.25 : 1 - ((sp - 0.25) / 0.75) ** 1.5) : 0; - sn.strike = strike; - sn.mouth = sp < 0.7 ? 1 : 0; - const tp = 1.6 + r(3), tf = ((t + tp * r(4)) % tp) / tp; - sn.tongue = tf < 0.12 ? sin((tf / 0.12) * PI * 2) : 0; - let phi, th, kap, kh, L0; - if (hang) { - const o = sd < 0 ? i : n - 1 - i; - phi = sd < 0 ? -PI + 0.12 + 0.3 * o : -0.12 - 0.3 * o; - th = sd < 0 ? PI - 0.8 - 0.25 * o : 0.8 + 0.25 * o; - kap = sd * (0.032 - 0.006 * o); - kh = -sd * 0.13; - L0 = 46 + 5 * r(5) - 6 * o; - } else { - const nr = n - 2 * nh, u = (i - nh + 0.5) / nr; - phi = -PI + 0.5 + u * (PI - 1); - th = lerp(phi, -PI / 2, 0.5); - kap = (u - 0.5) * 0.012; - kh = (u < 0.5 ? -1 : 1) * 0.05; - L0 = 64 - 52 * abs(u - 0.5) ** 1.5 - 5 * r(5); - } - th += 0.2 * sin(t * 0.5 + 9 * r(7)) * amp; - const L = L0 * (0.94 + 0.06 * sin(t * 0.9 + 6 * r(6))) + 12 * strike; - sn.L = L; - const lam = 20 + 6 * r(9), w = 4 + 1.5 * r(10), A = 1.05 * amp * (1 - 0.75 * strike); - let x = CX + 15.5 * cos(phi), y = 66 + 11 * sin(phi), m = 0; - const P = sn.pts; - for (let s = 0; s <= L && m < P.length; s += 0.5) { - const hth = th + A * (0.35 + (0.65 * s) / L) * sin((TAU * s) / lam - w * t + 7 * r(12)); - P[m++] = x; - P[m++] = y; - P[m++] = hth; - P[m++] = s; - th += 0.5 * (s > L - 12 ? kh * (1 - strike) : kap); - x += 0.5 * cos(hth); - y += 0.5 * sin(hth); - } - sn.np = m >> 2; -} - -// Radius of a snake at s: a taper to the neck, then the wide flat head. -function radius(s, L) { - const hs = (s - (L - 7)) / 7; - if (hs <= 0) return 2.5 - 1.15 * (s / (L - 7)); - return hs < 0.4 ? 1.35 + 1.25 * (hs / 0.4) : 2.6 - 1.6 * ((hs - 0.4) / 0.6) ** 1.5; -} - -// Stone grey for a lit colour, speckled per LED. -function grey(buf, o, r, g, b, p) { - const l = (0.35 * r + 0.5 * g + 0.15 * b) / 255, sp = (hash(p * 3 + 1) - 0.5) * 22; - buf[o] = 50 + 160 * l + sp; - buf[o + 1] = 48 + 156 * l + sp; - buf[o + 2] = 58 + 160 * l + sp; -} - -function put(buf, x, y, c) { - x = floor(x); - y = floor(y); - if (x >= 0 && x < W && y >= 0 && y < H) (buf[3 * (y * W + x)] = c[0]), (buf[3 * (y * W + x) + 1] = c[1]), (buf[3 * (y * W + x) + 2] = c[2]); -} - -const TONGUE = [255, 50, 40], CRACKC = [30, 28, 36]; - -export function draw(display, params) { - const S = params.s; - if (!S) return; - const { bg, col, stone, buf, fig, cell, edge, own, dd, vv, ss, snakes, segStone, segCrack, segFlake, cellState, cellCrack, cellFlake, cells, parts } = S; - const g = S.ph, R = S.R, n = S.n; - if (S.palHue !== S.hue) { - // Two kinds of snake: toxic green with a black zigzag, and violet with - // an orange one. - S.pal = []; - for (let i = 0; i < MS; i++) S.pal.push(i % 3 === 1 ? [hsv(S.hue + 0.78, 0.62, 235), hsv(0.07, 0.86, 255)] : [hsv(S.hue + 0.33, 0.76, 240), [10, 18, 12]]); - S.eye = hsv(S.hue + 0.36, 0.62, 255); - S.palHue = S.hue; - } - buf.set(bg); - own.fill(-1); - // Snakes: the middle ones first, so the outer ones lie over them. - const order = []; - for (let i = 0; i < n; i++) order.push(i); - order.sort((a, b) => abs(a - (n - 1) / 2) - abs(b - (n - 1) / 2)); - for (const i of order) { - body(S, i); - const sn = snakes[i], P = sn.pts, L = sn.L; - for (let q = 0; q < sn.np; q++) { - const px = P[q * 4], py = P[q * 4 + 1], th = P[q * 4 + 2], s = P[q * 4 + 3], rad = radius(s, L); - // The stone wave marks the segments it has passed. - if (R > 0 && hypot(px - CX, py - EY) < R) { - const j = min(MSEG - 1, floor(s / SEG)), z = i * MSEG + j; - if (!segStone[z]) (segStone[z] = 1), (segCrack[z] = CRACK - 0.8 + 0.8 * hash(z)), (segFlake[z] = FLAKE + 1.6 * (0.45 * hash(z + 99) + 0.55 * (1 - j / MSEG))); - if (q === 0) sn.frozen = true; - } - const tx = cos(th), ty = sin(th); - for (let yy = floor(py - rad); yy <= floor(py + rad); yy++) - for (let xx = floor(px - rad); xx <= floor(px + rad); xx++) { - if (xx < 0 || xx >= W || yy < 0 || yy >= H) continue; - const dx = xx + 0.5 - px, dy = yy + 0.5 - py, d = hypot(dx, dy) / rad; - if (d > 1) continue; - const p = yy * W + xx; - if (own[p] !== i || d < dd[p]) (own[p] = i), (dd[p] = d), (vv[p] = (tx * dy - ty * dx) / rad), (ss[p] = s); - } - } - } - // Shade them: round bodies with a dark rim, a zigzag down the back. - for (let p = 0; p < N; p++) { - const i = own[p]; - if (i < 0) continue; - const sn = snakes[i], s = ss[p], v = cl(vv[p], -1, 1), L = sn.L, o = 3 * p, head = s > L - 7; - const zz = 0.55 * (abs((((s + i * 2.3) / 3.2) % 2) - 1) * 2 - 1); - const c = !head && abs(v - zz) < 0.3 ? S.pal[i][1] : S.pal[i][0]; - let k = (0.42 + 0.58 * sqrt(1 - v * v)) * (1 - 0.18 * v); - if (abs(v) > 0.72) k *= 0.38; - if (head) k *= abs(v) < 0.5 ? 0.95 : 0.8; - let r = c[0] * k, gg = c[1] * k, b = c[2] * k; - if (head && sn.mouth && sn.strike > 0.15 && s > L - 3 && abs(v) < 0.5) (r = 120), (gg = 0), (b = 36); - const z = i * MSEG + min(MSEG - 1, floor(s / SEG)); - if (segStone[z] === 1) { - grey(buf, o, r, gg, b, p); - if (g > segCrack[z] && (s % SEG < 0.55 || abs(v - 0.3 * sin(s * 2 + z)) < 0.12)) buf.set(CRACKC, o); - } else (buf[o] = r), (buf[o + 1] = gg), (buf[o + 2] = b); - } - // Their eyes and tongues. - for (let i = 0; i < n; i++) { - const sn = snakes[i], P = sn.pts, q = sn.np - 1; - if (q < 8) continue; - const st = segStone[i * MSEG + min(MSEG - 1, floor(P[q * 4 + 3] / SEG))] === 1; - const hx = P[(q - 6) * 4], hy = P[(q - 6) * 4 + 1], th = P[(q - 6) * 4 + 2], nx = -sin(th), ny = cos(th); - for (const sd of [-1, 1]) { - const p = floor(hy + ny * sd * 1.4) * W + floor(hx + nx * sd * 1.4); - if (p >= 0 && p < N && own[p] === i) buf.set(st ? [70, 68, 76] : i % 3 === 1 ? [140, 255, 70] : [255, 196, 30], 3 * p); - } - if (!st && sn.tongue > 0 && !sn.frozen) { - const a = P[q * 4 + 2], tl = 1 + 3.2 * sn.tongue, tx = P[q * 4] + cos(a) * 0.5, ty = P[q * 4 + 1] + sin(a) * 0.5; - for (let u = 0.5; u <= tl; u += 0.5) put(buf, tx + cos(a) * u, ty + sin(a) * u, TONGUE); - for (const sd of [-1, 1]) put(buf, tx + cos(a) * tl + cos(a + sd * 0.9), ty + sin(a) * tl + sin(a + sd * 0.9), TONGUE); - } - } - // Her head and robe, and the stone over them. - for (let p = 0; p < N; p++) { - if (!fig[p]) continue; - const o = 3 * p, c = cell[p], dist = hypot((p % W) + 0.5 - CX, floor(p / W) + 0.5 - EY); - if (cellState[c] === 0 && R > 0 && dist < R) (cellState[c] = 1), (cellCrack[c] = CRACK - 0.8 + 0.8 * hash(c + 7)), (cellFlake[c] = FLAKE + 1.8 * (0.5 * hash(c + 77) + 0.5 * (1 - floor(p / W) / H))); - const src = cellState[c] === 1 && dist < R ? (edge[p] && g > cellCrack[c] ? null : stone) : col; - if (src) (buf[o] = src[o]), (buf[o + 1] = src[o + 1]), (buf[o + 2] = src[o + 2]); - else buf.set(CRACKC, o); - } - // Her eyes: shut lids, or open and glowing, or stone. - const look = ease(S.look), e = S.eye; - for (const sd of [-1, 1]) { - const ex = CX + sd * EDX; - const stoneEye = cellState[cell[EY * W + floor(ex)]] === 1 && g > OPEN + 0.25; - for (let y = 68; y <= 76; y++) - for (let x = floor(ex - 5); x <= floor(ex + 5); x++) { - const o = 3 * (y * W + x), dx = (x + 0.5 - ex) / 3.7, dy = y + 0.5 - 72; - const hh = 1.7 * look * sqrt(max(0, 1 - dx * dx)); - if (abs(dx) <= 1 && look < 0.15 && y === floor(72.6 + 0.9 * (1 - dx * dx))) buf.set([40, 26, 46], o); - else if (abs(dx) <= 1 && abs(dy) <= hh) { - const pupil = abs(x + 0.5 - ex) < 0.6; - buf.set(stoneEye ? (pupil ? [38, 36, 44] : [168, 166, 176]) : pupil ? [8, 30, 10] : e, o); - } else if (!stoneEye && look > 0) { - // A green glow round the open eye. - const gl = look * 0.5 * max(0, 1 - hypot((x + 0.5 - ex) / 6, (y + 0.5 - 72) / 3.5)); - (buf[o] += e[0] * gl), (buf[o + 1] += e[1] * gl), (buf[o + 2] += e[2] * gl); - } - } - } - // The wave itself: a ring of the eyes' green running out. - if (R > 0 && R < 130) - for (let p = 0; p < N; p++) { - const a = 0.7 * (1 - abs(hypot((p % W) + 0.5 - CX, floor(p / W) + 0.5 - EY) - R) / 3.2); - if (a > 0) (buf[3 * p] += e[0] * a), (buf[3 * p + 1] += e[1] * a), (buf[3 * p + 2] += e[2] * a); - } - // Flakes whose time has come break off and fall. - if (g > FLAKE) { - for (let c = 0; c < cells.length; c++) { - if (cellState[c] !== 1 || g < cellFlake[c] || !cells[c]) continue; - cellState[c] = 2; - spawn(S, cells[c], c); - } - for (let i = 0; i < n; i++) - for (let j = 0; j < MSEG; j++) { - const z = i * MSEG + j; - if (segStone[z] !== 1 || g < segFlake[z]) continue; - segStone[z] = 2; - const px = []; - for (let p = 0; p < N; p++) if (own[p] === i && min(MSEG - 1, floor(ss[p] / SEG)) === j) px.push(p); - spawn(S, px, z + 500); - } - } - const dt = max(0, S.T - S.lastT); - S.lastT = S.T; - for (let a = parts.length - 1; a >= 0; a--) { - const f = parts[a]; - f.vy += 90 * dt; - f.x += f.vx * dt; - f.y += f.vy * dt; - if (f.y > H + 8) { - parts.splice(a, 1); - continue; - } - for (let q = 0; q < f.dx.length; q++) { - const x = floor(f.x + f.dx[q]), y = floor(f.y + f.dy[q]); - if (x >= 0 && x < W && y >= 0 && y < H) { - const o = 3 * (y * W + x); - (buf[o] = f.c[3 * q]), (buf[o + 1] = f.c[3 * q + 1]), (buf[o + 2] = f.c[3 * q + 2]); - } - } - } - for (let p = 0, o = 0; p < N; p++, o += 3) display.setPixel(p % W, (p / W) | 0, min(255, max(0, buf[o])), min(255, max(0, buf[o + 1])), min(255, max(0, buf[o + 2]))); -} - -// A flake: the stone LEDs it covers, as they are now, falling away. -function spawn(S, px, seed) { - if (!px.length) return; - let mx = 0, my = 0; - for (const p of px) (mx += p % W), (my += floor(p / W)); - mx /= px.length; - my /= px.length; - const f = { x: mx, y: my, vx: (mx - 32) * 0.4 + (hash(seed + 3) - 0.5) * 10, vy: -6 - 10 * hash(seed + 5), dx: [], dy: [], c: [] }; - for (const p of px) f.dx.push((p % W) - mx), f.dy.push(floor(p / W) - my), f.c.push(S.buf[3 * p], S.buf[3 * p + 1], S.buf[3 * p + 2]); - S.parts.push(f); -} diff --git a/incoming/pattern-study/patterns/rokurokubi.js b/incoming/pattern-study/patterns/rokurokubi.js deleted file mode 100644 index 089ec4667..000000000 --- a/incoming/pattern-study/patterns/rokurokubi.js +++ /dev/null @@ -1,360 +0,0 @@ -// ===== Patternflow pattern ===== -// Title: Rokurokubi -// Family: halloween -// Idea: Behind lit paper doors a woman sleeps by an andon lamp, and on the glowing paper her shadow's neck rises and snakes up the whole height of the room, the head wanders under the lintel and goes down to lick the lamp oil until its shadow swells over the doors and the flame flares green, then the neck draws back down to the pillow. -// Author: patternflow-lab -// Date: 2026-10-26 -// SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: original, pattern round 53, after the rokurokubi of Japanese folklore -// Round: 53 -// =============================== -// @matrix 64x128 -// @knobs Hue=0..1, Speed=0.2..3, Reach=-1..1, Flicker=0..1 -// -// Knob 1: Hue. The lamp light on the paper: orange at 0, yellow at 0.1, green at 0.3, blue at 0.6, rose at 0.9. The night outside stays purple, the shadows black and the flare at the lick green. -// Knob 2: Speed. One clock for the story and the flame. At 2 a night takes 18 seconds: the neck rises from 0.6 s and reaches the lintel at 4.4, the head wanders under it until 8, is down at the lamp by 10 and licks the oil until 13, and is back on the pillow at 16; at 0.2 ten times slower, at 3 half as fast again. -// Knob 3: Reach. How far the neck goes. At -1 the head climbs only halfway up the doors and at 0 to the lintel; turned up, the neck winds in more and wider coils on the way, 7 coils 5.5 LEDs out to each side at 0 and 10 coils 12.5 out at 1. -// Knob 4: Flicker. How much the flame gutters. At 0.06 it breathes, the paper dims by up to a seventh and the shadows tremble; at 1 a draught swings the flame up to 10 LEDs and the paper pulses down to 40%, so a shadow near the lamp leaps across the doors. -// Buttons: K1 tap: the head turns to us, comes up against the paper and tears a hole in it, and her white face peers through for three seconds while the story waits. The hole stays open until the next night. - -const W = 64, H = 128, N = W * H; -const { min, max, floor, sqrt, sin, cos, abs, atan2, PI } = Math; -const TAU = 2 * PI; -const cl = (v, a, b) => (v < a ? a : v > b ? b : v); -const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); -const mix = (a, b, t) => a + (b - a) * t; -function hash(i, j) { - let h = (Math.imul(i, 374761393) + Math.imul(j, 668265263)) | 0; - h = Math.imul(h ^ (h >>> 13), 1274126177); - return ((h ^ (h >>> 16)) >>> 0) / 4294967296; -} -function hsv(h, s, v) { - 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); - return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; -} - -// The paper: x 6..57, y 7..116, two doors meeting at x 31.5. -const PX0 = 6, PX1 = 57, PY0 = 7, PY1 = 116; -// The lamp's flame behind the paper, and the depth of the sleeper's plane -// as a share of the lamp's depth: a shadow is the shape blown up by -// 1 / (1 - depth) about the flame. -const LX = 13, LY = 99, DEP = 0.3; -// The story in seconds at Speed 2. -const T_RISE = 0.6, T_TOP = 4.4, T_WAND = 8, T_LAMP = 10, T_LICK = 13, T_BACK = 16, T_END = 18; -// The shoulders and the head on the pillow. -const SHX = 45, SHY = 108, HDX = 50, HDY = 106; -// The head's parts round the skull's middle, facing right: skull, nose, -// lips and chin, the knot, the front lock, the side lock, the tail of hair. -const HS = 1.6; -const HEAD = [[0, 0, 3.6, 4.2], [3.6, 0.4, 0.9, 0.8], [2.9, 2.6, 1.2, 1.1], [-0.8, -5.4, 4.4, 2.2], [2.2, -3.9, 1.9, 1.6], [-1.6, -0.8, 3, 3.3], [-4.6, 1, 2.2, 1.5]].map((a) => a.map((v) => v * HS)); -const PINS = [[-2.5, -6.8, -7.5, -9], [0.5, -7.2, 3.6, -10.4]].map((a) => a.map((v) => v * HS)); - -export function setup(params) { - params.s = { - t: 0, rt: 0, hue: 0, sp: 2, reach: 0, fl: 0.06, pathFor: 99, - px: new Float32Array(600), py: new Float32Array(600), np: 0, uTop: 0, uWand: 0, uLamp: 0, - M: new Float32Array(N), BG: new Float32Array(N * 3), PAP: new Uint8Array(N), - peep: -1, hole: null, holeLive: 0, fcs: -1, ph: [0, 1, 2, 3, 4].map(() => Math.random() * TAU), - }; - scenery(params.s); -} - -// What does not move: the night, the posts, the lintel and sill, the -// veranda, and which LEDs are paper. -function scenery(s) { - const B = s.BG; - for (let i = 0; i < N; i++) { - const x = i & 63, y = i >> 6, o = i * 3, n = hash(x, y); - let c; - if (x >= PX0 && x <= PX1 && y >= PY0 && y <= PY1) { - s.PAP[i] = 1; - continue; - } - if (y < 4) { - // Sky under the eave, moonlit purple, brighter at the left. - const g = 0.55 + 0.25 * (1 - x / 63) - 0.1 * y; - c = [0.42 * g, 0.2 * g, 0.62 * g]; - if (y >= 2 && y - 2 > (x * 3) / 63) c = [0.05, 0.03, 0.07]; - } else if (y < PY0) c = y === 4 ? [0.3, 0.2, 0.36] : [0.17, 0.1, 0.16]; - else if (y <= PY1 + 3) { - if (x < 3 || x > 60) { - // Night beside the house: purple, with leaves of a garden shrub. - const g = 0.32 + 0.22 * (1 - y / 127) + 0.05 * n; - c = [0.36 * g, 0.16 * g, 0.55 * g]; - if (hash(floor(x / 2), floor(y / 3)) < 0.28) c = [0.07, 0.04, 0.1]; - } else if (y > PY1) c = y === PY1 + 1 ? [0.32, 0.21, 0.3] : [0.16, 0.09, 0.13]; - else c = x === 3 || x === 60 ? [0.34, 0.22, 0.4] : x === 4 || x === 59 ? [0.2, 0.12, 0.19] : [0.12, 0.07, 0.11]; - } else { - // Veranda boards, the warm spill near the doors, moonlight at the edge. - const v = (y - PY1 - 4) / 7, seam = (x + (y & 1) * 7) % 17 === 0 ? 0.6 : 1; - c = [mix(0.5, 0.2, v) * seam, mix(0.26, 0.11, v) * seam, mix(0.2, 0.3, v) * seam]; - } - (B[o] = c[0]), (B[o + 1] = c[1]), (B[o + 2] = c[2]); - } -} - -// The head's road for one night: up from the pillow in coils, along under -// the lintel, down the left side to the lamp. Sampled every 0.7 LED. -function road(s) { - const r = s.reach, sc = r < 0 ? 1 + 0.5 * r : 1, rp = max(0, r); - const Y = (y) => HDY - (HDY - y) * sc; - // Up the right door in coils: more of them and wider as Reach turns up. - const nz = 7 + Math.round(3 * rp), A = 5.5 + 7 * rp, wp = [[HDX, HDY]]; - for (let i = 1; i <= nz; i++) wp.push([45 + (i & 1 ? A : -A) * (i === 1 ? 0.6 : 1), Y(98 - (62 * (i - 1)) / nz)]); - const top = wp.length; - wp.push([43, Y(35)], [37, Y(30)], [31, Y(36)], [25, Y(30)], [18, Y(34)]); - const wand = wp.length - 1; - const dn = (x, y) => [16 + (x - 16) * (1 + 0.8 * rp), Y(y)]; - wp.push(dn(14, 44), dn(23, 55), dn(13, 65), dn(22, 74), dn(15, 80), [22, Y(86)]); - const mark = [top, wand, wp.length - 1]; - const P = []; - for (let i = 0; i < wp.length - 1; i++) { - const a = wp[max(0, i - 1)], b = wp[i], c = wp[i + 1], d = wp[min(wp.length - 1, i + 2)]; - for (let k = 0; k < 24; k++) { - const t = k / 24, t2 = t * t, t3 = t2 * t; - const f = (p0, p1, p2, p3) => 0.5 * (2 * p1 + (-p0 + p2) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t2 + (-p0 + 3 * p1 - 3 * p2 + p3) * t3); - P.push([cl(f(a[0], b[0], c[0], d[0]), PX0 + 5, PX1 - 4), cl(f(a[1], b[1], c[1], d[1]), PY0 + 22, PY1), i]); - } - } - P.push([...wp[wp.length - 1], wp.length - 1]); - // Even steps along the road. - let n = 0, acc = 0; - s.px[0] = P[0][0]; - s.py[0] = P[0][1]; - n = 1; - for (let i = 1; i < P.length && n < 599; i++) { - const dx = P[i][0] - P[i - 1][0], dy = P[i][1] - P[i - 1][1], l = sqrt(dx * dx + dy * dy); - acc += l; - while (acc >= 0.7 && n < 599) { - acc -= 0.7; - const u = 1 - acc / max(l, 1e-6); - s.px[n] = P[i - 1][0] + dx * u; - s.py[n] = P[i - 1][1] + dy * u; - n++; - } - for (let m = 0; m < 3; m++) if (P[i][2] === mark[m] && P[i - 1][2] !== mark[m]) s[["uTop", "uWand", "uLamp"][m]] = n - 1; - } - s.uLamp = n - 1; - s.np = n; - s.pathFor = r; -} - -export function update(dt, input, params) { - if (!params.s) setup(params); - const s = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); - s.hue = cl(k[0], 0, 1); - s.sp = cl(k[1], 0.2, 3); - s.reach = cl(k[2], -1, 1); - s.fl = cl(k[3], 0, 1); - if (abs(s.reach - s.pathFor) > 0.004) road(s); - // One clock for everything, the flame included. - s.rt += (d * s.sp) / 2; - if (input.btnPressed && input.btnPressed[0] && s.peep < 0) s.peep = 0; - if (s.peep >= 0) { - s.peep += (d * s.sp) / 2; - if (s.peep > 3.8) s.peep = -1; - } else { - s.t += (d * s.sp) / 2; - if (s.t >= T_END) (s.t -= T_END), (s.hole = null); - } -} - -// Where the head is on its road (in steps) at story time t. -function headStep(s, t) { - if (t < T_RISE || t >= T_BACK) return 0; - if (t < T_TOP) return s.uTop * (1 - (1 - (t - T_RISE) / (T_TOP - T_RISE)) ** 2); - if (t < T_WAND) return mix(s.uTop, s.uWand, sm(T_TOP, T_WAND, t) * 0.7 + 0.3 * (t - T_TOP) / (T_WAND - T_TOP)); - if (t < T_LAMP) return mix(s.uWand, s.uLamp, sm(T_WAND, T_LAMP, t)); - if (t < T_LICK) return s.uLamp; - return s.uLamp * (1 - sm(T_LICK, T_BACK, t)); -} - -// Shadow coverage: a capsule from (ax, ay) to (bx, by) of radius r and soft edge e. -function capsule(M, ax, ay, bx, by, r, e) { - const x0 = max(PX0, floor(min(ax, bx) - r - e)), x1 = min(PX1, floor(max(ax, bx) + r + e) + 1); - const y0 = max(PY0, floor(min(ay, by) - r - e)), y1 = min(PY1, floor(max(ay, by) + r + e) + 1); - const dx = bx - ax, dy = by - ay, ll = dx * dx + dy * dy + 1e-9; - for (let y = y0; y <= y1; y++) - for (let x = x0; x <= x1; x++) { - const qx = x + 0.5 - ax, qy = y + 0.5 - ay, u = cl((qx * dx + qy * dy) / ll, 0, 1); - const ex = qx - dx * u, ey = qy - dy * u, dd = sqrt(ex * ex + ey * ey) - r; - const c = cl(0.5 - dd / e, 0, 1), q = y * W + x; - if (c > M[q]) M[q] = c; - } -} -// An ellipse at (cx, cy), radii rx, ry, turned by angle a. -function blob(M, cx, cy, rx, ry, a, e) { - const R = max(rx, ry) + e, ca = cos(a), sa = sin(a), mr = min(rx, ry); - for (let y = max(PY0, floor(cy - R)); y <= min(PY1, floor(cy + R) + 1); y++) - for (let x = max(PX0, floor(cx - R)); x <= min(PX1, floor(cx + R) + 1); x++) { - const qx = x + 0.5 - cx, qy = y + 0.5 - cy, u = (qx * ca + qy * sa) / rx, v = (-qx * sa + qy * ca) / ry; - const dd = (sqrt(u * u + v * v) - 1) * mr, c = cl(0.5 - dd / e, 0, 1), q = y * W + x; - if (c > M[q]) M[q] = c; - } -} - -export function draw(display, params) { - const s = params.s; - if (!s) return; - const { M, BG, PAP, px, py } = s; - const t = s.t, rt = s.rt, ph = s.ph, f = s.fl; - // The flame: a breath, now and then a gutter, and at the lick a flare. - const lick = sm(T_LAMP - 0.4, T_LAMP + 0.6, t) * (1 - sm(T_LICK - 0.5, T_LICK + 0.3, t)); - const jit = 0.6 + 4.5 * f + 2.5 * lick; - const n1 = sin(9.1 * rt + ph[0]) * 0.6 + sin(15.7 * rt + ph[1]) * 0.4, n2 = sin(7.3 * rt + ph[2]) * 0.5 + sin(21.1 * rt + ph[3]) * 0.5; - const gust = max(0, sin(1.3 * rt + ph[4]) + sin(2.9 * rt) * 0.6) ** 3 * 0.3; - const fx = LX + n1 * jit * 0.5 + gust * 6 * f, fy = LY - 1.2 * abs(n2) * jit * 0.4; - const glow = max(0.4, 1 - (0.12 + 0.4 * f) * (0.5 + 0.5 * n2) - 1.4 * gust * f - 0.15 * lick * (0.5 + 0.5 * sin(rt * 23))); - const green = lick * (0.6 + 0.4 * sin(rt * 7)); - // The light's colour on the paper, near the lamp and far from it. - let near = hsv(0.105 + s.hue, 0.42, 1); - const far = hsv(0.07 + s.hue, 0.74, 1); - const tox = [0.45, 1, 0.12]; - near = near.map((v, i) => mix(v, tox[i], green)); - M.fill(0); - // Shadows. Shapes are laid out where their shadows fall with the flame at - // rest and at the sleeper's depth; proj finds the shadow for the flame as - // it is and the depth given, blown up about the flame by 1 / (1 - depth), - // and how much bigger that is than the layout. - const proj = (x, y, dep) => { - const k = (1 - DEP) / (1 - dep), k0 = 1 / (1 - dep); - const wx = LX + (x - LX) * (1 - DEP), wy = LY + (y - LY) * (1 - DEP); - return [fx + (wx - fx) * k0, fy + (wy - fy) * k0, k]; - }; - const pen = (k) => 0.75 + 0.8 * (k - 1) + 0.5 * f; - // The sleeper under the quilt, breathing. - const br = 0.5 + 0.5 * sin(rt * 1.6); - const P1 = proj(31, 111.5 - 0.5 * br, DEP), P2 = proj(41, 109, DEP), P3 = proj(22, 112, DEP), P4 = proj(32, 115.2, DEP); - blob(M, P1[0], P1[1], 14 * P1[2], 4.2 * P1[2], 0.04, pen(P1[2])); - blob(M, P2[0], P2[1], 6 * P2[2], 3.6 * P2[2], -0.2, pen(P2[2])); - blob(M, P3[0], P3[1], 6 * P3[2], 3 * P3[2], 0, pen(P3[2])); - capsule(M, P4[0] - 21, P4[1], P4[0] + 23, P4[1], 1.6, pen(P4[2])); - const pl = proj(HDX + 1, HDY + 3.2, DEP); - blob(M, pl[0], pl[1], 4 * pl[2], 1.6 * pl[2], 0, pen(pl[2])); - // The neck: the road from the shoulders to the head, with a slow wave - // running along it, deeper toward the head while it licks the lamp. - const peep = s.peep, pp = peep < 0 ? 0 : sm(0, 0.5, peep) * (1 - sm(3.3, 3.8, peep)); - const u = headStep(s, t), nu = floor(u), deepH = DEP + 0.35 * lick; - const amp = 1.5 * min(1, u / 40) * (1 - 0.3 * abs(s.reach)); - const sh0 = proj(SHX, SHY, DEP); - let hx = HDX, hy = HDY, ox = sh0[0], oy = sh0[1]; - for (let i = 0; i <= nu + 1 && i < s.np; i++) { - const j = min(i, u); - const i0 = floor(j), i1 = min(i0 + 1, s.np - 1), fr = j - i0; - let x = mix(px[i0], px[i1], fr), y = mix(py[i0], py[i1], fr); - const a = max(0, i0 - 2), b = min(s.np - 1, i0 + 2), tx = px[b] - px[a], ty = py[b] - py[a], tl = sqrt(tx * tx + ty * ty) + 1e-6; - const wv = amp * sin(j * 0.26 - rt * 3.4) * sin((PI * j) / max(u, 1)); - x += (-ty / tl) * wv; - y += (tx / tl) * wv; - const dep = mix(DEP, deepH, sm(u - 9, u, j)); - const p = proj(x, y, dep); - capsule(M, ox, oy, p[0], p[1], 1.8 * p[2], pen(p[2])); - (ox = p[0]), (oy = p[1]); - (hx = x), (hy = y); - if (j >= u) break; - } - // The head: a profile with the big shimada knot of hair on top, the side - // locks, the tail of hair behind and two pins. It lies face up on the - // pillow, turns (narrowing as it turns) to face the way it goes, bows to - // the lamp, and turns to us before it comes to the paper. - const lying = 1 - sm(0, 8, u); - // She faces into the room, looks back once along the lintel, and looks - // the way she goes while the neck draws back. - const back = (t > 5.9 && t < 6.8) || (t > T_LICK + 0.3 && t < T_BACK - 0.6); - const want = lying > 0.5 ? -1 : (back ? 1 : -1) * (1 - 0.65 * pp); - s.fcs += (want - s.fcs) * 0.12; - const fs = s.fcs < 0 ? min(-0.3, s.fcs) : max(0.3, s.fcs), sg = fs < 0 ? -1 : 1; - const tilt = lying * 1.57 + sg * (0.85 * lick + 0.1 * sin(rt * 1.3) * (1 - lying)); - const hk = proj(hx, hy, deepH), K = hk[2] * (1 - 0.15 * pp), e = pen(K) * (1 - 0.5 * pp), ct = cos(tilt), st = sin(tilt); - const at = (dx, dy) => ((dx = (dx + 0.8 * HS) * fs), (dy -= 4.6 * HS), [hk[0] + (dx * ct - dy * st) * K, hk[1] + (dx * st + dy * ct) * K]); - const faceOn = peep >= 0 ? sm(0.75, 1.0, peep) * (1 - sm(3.2, 3.6, peep)) : 0; - if (faceOn < 0.5) { - for (const [dx, dy, rx, ry] of HEAD) { - const c = at(dx, dy); - blob(M, c[0], c[1], rx * K * abs(fs), ry * K, tilt, e); - } - for (const [a1, b1, a2, b2] of PINS) { - const c1 = at(a1, b1), c2 = at(a2, b2); - capsule(M, c1[0], c1[1], c2[0], c2[1], 0.5 * K, e); - } - } - // The tongue, lapping at the oil. - if (lick > 0.05) { - const tl = lick * (3.5 + 2.5 * abs(sin(rt * 9))), c1 = at(3.2 * HS, 2.6 * HS), c2 = at(3.6 * HS + tl * 0.5, 2.6 * HS + tl); - capsule(M, c1[0], c1[1], c2[0], c2[1], 0.5 * K, e); - } - // The torn hole and her face in it. - const hc = at(0, 0); - if (peep >= 0.5 && !s.holeLive) (s.hole = [cl(hc[0], PX0 + 9, PX1 - 9), cl(hc[1], PY0 + 9, PY1 - 9), (Math.random() * 1000) | 0]), (s.holeLive = 1); - if (peep < 0) s.holeLive = 0; - const tear = s.hole ? (s.holeLive ? sm(0.5, 0.8, peep) : 1) : 0; - // Light the paper, lay the shadow and the grid of the doors over it. - const lw = 1 / (52 * 52); - for (let q = 0; q < N; q++) { - const x = q & 63, y = q >> 6; - if (!PAP[q]) { - const o = q * 3, sp = y > PY1 + 3 && x > 2 && x < 61 ? 0.25 * (1 - (y - PY1 - 4) / 8) * glow : 0; - display.setPixel(x, y, 255 * (BG[o] + sp * far[0]), 255 * (BG[o + 1] + sp * far[1]), 255 * (BG[o + 2] + sp * far[2])); - continue; - } - const dx = x + 0.5 - fx, dy = y + 0.5 - fy, r2 = (dx * dx + dy * dy) * lw; - // The andon's own paper glows through, a soft bright box round the flame. - const box = sm(7.5, 5, abs(x + 0.5 - LX)) * sm(14, 10, abs(y + 0.5 - LY + 2)); - const fall = 1 / (1 + r2) ** 1.5, w = cl(fall * 1.3 - 0.3, 0, 1); - let lv = (0.86 + 0.2 * fall) * glow + 0.3 * box * glow; - const fib = 0.97 + 0.03 * hash(x, y >> 1); - let r = mix(far[0], near[0], w) * lv * fib, g = mix(far[1], near[1], w) * lv * fib, b = mix(far[2], near[2], w) * lv * fib; - // The andon's frame and legs, close behind the paper, in soft shadow. - const ax = abs(x + 0.5 - LX), ay = y + 0.5 - LY; - if ((abs(ax - 6) < 0.6 && ay > -13 && ay < 16) || (ax < 6.5 && (abs(ay + 13) < 0.6 || abs(ay - 9) < 0.6))) (r *= 0.45), (g *= 0.4), (b *= 0.45); - const m = M[q]; - r = mix(r, 0.04, m * 0.94); - g = mix(g, 0.015, m * 0.94); - b = mix(b, 0.05, m * 0.94); - // The hole: the dim room behind, her face when she is at it. - if (tear > 0) { - const hx2 = x + 0.5 - s.hole[0], hy2 = y + 0.5 - s.hole[1], hr = sqrt(hx2 * hx2 + hy2 * hy2); - const ang = atan2(hy2, hx2), bin = ((ang / TAU + 1) * 13) % 13, bi = floor(bin); - const rag = mix(hash(bi, s.hole[2]), hash((bi + 1) % 13, s.hole[2]), bin - bi); - const R = tear * (6.5 + 3 * rag); - if (hr < R) { - // The room behind, lit by the lamp, nearer the lamp the brighter. - const room = (0.3 + 0.45 * w) * glow, fcol = faceAt(hx2, hy2, rt, glow); - r = mix(far[0] * room, fcol[0], faceOn); - g = mix(far[1] * room * 0.8, fcol[1], faceOn); - b = mix(far[2] * room, fcol[2], faceOn); - } else if (hr < R + 1.2) (r = min(1, r * 0.4 + 0.62)), (g = min(1, g * 0.4 + 0.57)), (b = min(1, b * 0.4 + 0.48)); - } - // The lattice of the doors: a frame round each door, bars across and up. - const xl = x - PX0, yl = y - PY0; - const frame = x <= PX0 + 1 || x >= PX1 - 1 || x === 31 || x === 32 || y <= PY0 + 1 || y >= PY1 - 1; - const bar = frame || (x < 31 ? (xl - 1) % 8 === 0 : (x - 33) % 8 === 7) || yl % 14 === 13; - if (bar) { - const k = frame ? 0.5 : 0.3; - r = mix(r, 0.2, 0.8) * k + 0.05; - g = mix(g, 0.11, 0.8) * k + 0.02; - b = mix(b, 0.17, 0.8) * k + 0.06; - } - display.setPixel(x, y, r * 255, g * 255, b * 255); - } -} - -// Her face pressed to the hole: white skin, black hair parted in the middle, -// two dark eyes with a glint that look at us and now and then aside, red -// lips. Returns the colour. -function faceAt(x, y, rt, glow) { - const lk = sin(rt * 0.9) > 0.6 ? 1 : 0; - if (y < -4.2 + 0.25 * abs(x) * (y < -1 ? 1 : 0) || abs(x) > 6.8 - max(0, y - 3) * 0.6) { - const part = abs(x) < 0.6 && y < -4 ? 0.25 : 0; - return [0.04 + part, 0.03 + part, 0.05 + part]; - } - const l = (0.82 + 0.12 * cl(-x / 6, -1, 1)) * (0.8 + 0.2 * glow); - let c = [l, l * 0.93, l * 0.86]; - for (const ex of [-2.8, 2.8]) { - const u = (x - ex) / 1.9, v = (y + 0.6) / 0.85; - if (u * u + v * v < 1) c = x - ex - lk * 0.8 > -0.5 && x - ex - lk * 0.8 < 0.5 && y < -0.6 ? [0.95, 1, 0.9] : [0.05, 0.02, 0.04]; - } - if (abs(x) < 1.4 && abs(y - 3.6) < 0.7) c = [0.75, 0.06, 0.1]; - return c; -} From 19e8f0dbcb462e2a8dd07ea7c31e645f17171b0f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 02:14:08 +0000 Subject: [PATCH 08/14] =?UTF-8?q?53=ED=8C=90:=20Max-Flow=20Min-Cut=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Yb8D9UxrifXqrXju5CVbqY --- .../patterns/max-flow-min-cut.js | 423 ++++++++++++++++++ 1 file changed, 423 insertions(+) create mode 100644 incoming/pattern-study/patterns/max-flow-min-cut.js diff --git a/incoming/pattern-study/patterns/max-flow-min-cut.js b/incoming/pattern-study/patterns/max-flow-min-cut.js new file mode 100644 index 000000000..b6c596e44 --- /dev/null +++ b/incoming/pattern-study/patterns/max-flow-min-cut.js @@ -0,0 +1,423 @@ +// ===== Patternflow pattern ===== +// Title: Max-Flow Min-Cut +// Family: algo +// Idea: A triangular network of pipes, the wide ones holding more, runs from a source bar along the bottom to a sink bar along the top; pulses climb it one shortest augmenting path at a time and fill the pipes cyan until the full ones turn gold, then the nodes the source can still reach fill in as a pool that stops at a jagged line, and the full pipes crossing that line flash while the sink bar splits into their capacities, which add up to exactly the flow that got through. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: max-flow min-cut theorem (Lester R. Ford Jr. and Delbert R. Fulkerson, 1956; Peter Elias, Amiel Feinstein and Claude Shannon, 1956), augmenting along shortest paths (Yefim Dinitz, 1970; Jack Edmonds and Richard Karp, 1972), pattern round 53 +// Round: 53 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Columns=7..13, Grain=0..1 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the empty pipes are steel blue, flow is cyan, a full pipe is gold and the cut flashes between gold and pale pink. The dim colours, the empty pipes and the pool, lose their saturation between red and lime so they never go brown or olive. +// Knob 2: Speed. At 2 a pulse climbs the panel in a second, the augmenting paths start one after another for five seconds, and a whole run from the first pulse to the next network takes eleven; at 0.2 ten times as long, at 3 two thirds as long. +// Knob 3: Columns. The network is 7 to 13 nodes wide in a triangular lattice: 15 rows, 98 nodes and 251 pipes at 7, and 28 rows, 350 nodes and 970 pipes at 13. Turning it builds a new network. +// Knob 4: Grain. How much of each pipe's capacity is its own random draw rather than the smooth terrain with its wavy band of thin pipes. Near 0 neighbouring pipes hold about the same and the cut follows the band; at 1 the capacities scatter pipe by pipe from 1 to 9, a quarter of them 9 and an eighth 1, and the cut wanders. Turning it drains the flow and runs the same network again on the new capacities. +// Buttons: K1 tap draws new capacities now. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, sqrt, sin, round, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// 1 at the middle of the arc of the wheel from red through orange and +// yellow to lime, 0 outside it. +const warm = (h) => ((h -= floor(h)), h < 0.3 ? sin((PI * h) / 0.3) : 0); +// Rows of nodes run from y = BOT up to y = TOP; the source bar is the two +// bottom rows of LEDs and the sink bar the two top rows. +const TOP = 6, BOT = 121, SRC_Y = 126.5, SINK_Y = 0.5; +// Pulse speed in LEDs per unit of time (Speed 2 runs two units a second). +const V = 70; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// The lattice: rows of C and C - 1 nodes in turn, each node tied to its +// neighbours in the row and to the two nearest in the rows above and below. +function build(s, C) { + const sx = W / C, R = max(4, round((BOT - TOP) / (sx * 0.87)) + 1), sy = (BOT - TOP) / (R - 1); + const X = [], Y = [], rows = []; + for (let r = 0; r < R; r++) { + const odd = r & 1, cnt = odd ? C - 1 : C, ids = []; + for (let i = 0; i < cnt; i++) { + let x = 31.5 + sx * (i - (cnt - 1) / 2), y = BOT - r * sy; + if (r > 0 && r < R - 1) y += (Math.random() - 0.5) * 0.34 * sy; + x += (Math.random() - 0.5) * 0.34 * sx; + ids.push(X.length); + X.push(cl(x, 0.5, 62.5)); + Y.push(y); + } + rows.push(ids); + } + const eu = [], ev = []; + for (let r = 0; r < R; r++) { + const a = rows[r]; + for (let i = 0; i + 1 < a.length; i++) eu.push(a[i]), ev.push(a[i + 1]); + if (r + 1 < R) { + const b = rows[r + 1]; + for (let i = 0; i < a.length; i++) + for (const j of r & 1 ? [i, i + 1] : [i - 1, i]) if (j >= 0 && j < b.length) eu.push(a[i]), ev.push(b[j]); + } + } + const n = X.length, m = eu.length; + const deg = new Int32Array(n + 1); + for (let k = 0; k < m; k++) deg[eu[k] + 1]++, deg[ev[k] + 1]++; + for (let i = 0; i < n; i++) deg[i + 1] += deg[i]; + const adjE = new Int32Array(2 * m), fill = deg.slice(0, n); + for (let k = 0; k < m; k++) (adjE[fill[eu[k]]++] = k), (adjE[fill[ev[k]]++] = k); + // Each pipe's samples, one per LED along its longer axis: the LED on that + // axis (sb), where the line crosses the other axis (sm), the step to the + // next LED across (ss), and the share of the way from eu to ev (tt). + const sb = [], sm = [], ss = [], tt = [], off = new Int32Array(m + 1); + for (let k = 0; k < m; k++) { + const x1 = X[eu[k]], y1 = Y[eu[k]], dx = X[ev[k]] - x1, dy = Y[ev[k]] - y1, flat = abs(dx) >= abs(dy); + const steps = max(2, Math.ceil(max(abs(dx), abs(dy)))); + for (let j = 1; j < steps; j++) { + const t = j / steps, px = x1 + dx * t, py = y1 + dy * t; + sb.push(flat ? round(px) : round(py) * W); + sm.push(flat ? py : px); + ss.push(flat ? W : 1); + tt.push(t); + } + off[k + 1] = sb.length; + } + // The node nearest each LED, for the pool the source can reach. + const near = new Int16Array(N); + for (let p = 0; p < N; p++) { + const px = p % W, py = (p / W) | 0; + let best = 1e9, bi = 0; + for (let i = 0; i < n; i++) { + const d = (X[i] - px) ** 2 + (Y[i] - py) ** 2; + if (d < best) (best = d), (bi = i); + } + near[p] = bi; + } + const isBot = new Uint8Array(n), isTop = new Uint8Array(n); + for (const i of rows[0]) isBot[i] = 1; + for (const i of rows[R - 1]) isTop[i] = 1; + Object.assign(s, { + C, n, m, X: Float32Array.from(X), Y: Float32Array.from(Y), eu: Int32Array.from(eu), ev: Int32Array.from(ev), + deg, adjE, sb: Int32Array.from(sb), sm: Float32Array.from(sm), ss: Int32Array.from(ss), tt: Float32Array.from(tt), off, near, + bottom: Int32Array.from(rows[0]), isBot, isTop, + g: new Float32Array(m), e: new Float32Array(m), cap: new Int32Array(m), flow: new Int32Array(m), dcap: new Float32Array(m), dcap0: new Float32Array(m), + dflow: new Float32Array(m), dflow0: new Float32Array(m), inS: new Uint8Array(n), level: new Int32Array(n), vS: new Uint8Array(n), vLevel: new Int32Array(n), + dist: new Int32Array(n), prevE: new Int32Array(n), queue: new Int32Array(n), cut: new Uint8Array(m), + }); +} + +// Fresh random capacities. Each pipe gets a share g from a smooth terrain +// of a few broad waves, cut by a wavy band of thin pipes somewhere across +// the middle and thickened near the two bars, and a draw of its own, e. +function drawField(s) { + const w = []; + for (let i = 0; i < 3; i++) w.push([TAU * Math.random(), (0.03 + 0.07 * Math.random()) * (Math.random() < 0.5 ? -1 : 1), 0.02 + 0.05 * Math.random()]); + const yc = 44 + 40 * Math.random(), A = 5 + 11 * Math.random(), fx = (TAU * (0.6 + Math.random())) / W, ph = TAU * Math.random(), bw = 7 + 5 * Math.random(); + for (let k = 0; k < s.m; k++) { + const x = (s.X[s.eu[k]] + s.X[s.ev[k]]) / 2, y = (s.Y[s.eu[k]] + s.Y[s.ev[k]]) / 2; + let f = 0; + for (const [a, ax, ay] of w) f += sin(a + ax * x + ay * y) / 3; + const band = Math.exp(-(((y - yc - A * sin(fx * x + ph)) / bw) ** 2)), bar = cl(1 - min(y - TOP, BOT - y) / 18, 0, 1); + s.g[k] = 0.6 + 0.3 * f - 0.5 * band + 0.3 * bar; + s.e[k] = Math.random() - 0.5; + } +} + +function capacities(s, grain) { + const a = 0.25 + 1.1 * grain; + for (let k = 0; k < s.m; k++) s.cap[k] = cl(round(1 + 8 * (s.g[k] + a * s.e[k])), 1, 9); +} + +// Residual capacity of pipe k leaving node u. +const resid = (s, k, u) => (s.eu[k] === u ? s.cap[k] - s.flow[k] : s.cap[k] + s.flow[k]); + +// Breadth-first search from every bottom node at once over pipes with room +// left. Returns the first top node reached, or -1 once the sink is cut off; +// dist then marks the nodes the source still reaches. +function bfs(s) { + const { dist, prevE, queue, bottom, adjE, deg, eu, ev, isTop } = s; + dist.fill(-1); + let h = 0, t = 0; + // The bottom nodes enter the queue in a new order each search. + for (const b of bottom) { + const j = floor(Math.random() * (t + 1)); + queue[t++] = queue[j]; + queue[j] = b; + } + for (let i = 0; i < t; i++) (dist[queue[i]] = 0), (prevE[queue[i]] = -1); + while (h < t) { + const u = queue[h++]; + if (isTop[u]) return u; + for (let j = deg[u]; j < deg[u + 1]; j++) { + const k = adjE[j], v = eu[k] === u ? ev[k] : eu[k]; + if (dist[v] < 0 && resid(s, k, u) > 0) (dist[v] = dist[u] + 1), (prevE[v] = k), (queue[t++] = v); + } + } + return -1; +} + +// Edmonds-Karp: augment along a shortest path until none is left, keeping +// every path for the pulses; then read the cut off the last search. +function solve(s) { + const { eu, ev, adjE, deg, flow } = s; + flow.fill(0); + // Shuffle each node's pipes so ties between shortest paths fall anywhere. + for (let u = 0; u < s.n; u++) + for (let j = deg[u + 1] - 1; j > deg[u]; j--) { + const r = deg[u] + floor(Math.random() * (j - deg[u] + 1)), x = adjE[j]; + (adjE[j] = adjE[r]), (adjE[r] = x); + } + s.paths = []; + s.F = 0; + for (let top; (top = bfs(s)) >= 0; ) { + const nodes = [top], ks = []; + let d = 1e9; + for (let v = top; s.prevE[v] >= 0; ) { + const k = s.prevE[v], u = eu[k] === v ? ev[k] : eu[k]; + d = min(d, resid(s, k, u)); + ks.push(k); + nodes.push(u); + v = u; + } + nodes.reverse(); + ks.reverse(); + const sg = new Int8Array(ks.length); + ks.forEach((k, i) => { + sg[i] = eu[k] === nodes[i] ? 1 : -1; + flow[k] += sg[i] * d; + }); + // The pulse's way: up from the source bar, along the path, into the sink bar. + const pts = [s.X[nodes[0]], SRC_Y]; + for (const v of nodes) pts.push(s.X[v], s.Y[v]); + pts.push(s.X[top], SINK_Y); + const cum = new Float32Array(pts.length / 2); + for (let i = 1; i < cum.length; i++) cum[i] = cum[i - 1] + Math.hypot(pts[2 * i] - pts[2 * i - 2], pts[2 * i + 1] - pts[2 * i - 1]); + s.paths.push({ pts, cum, ks, sg, d, j: 0 }); + s.F += d; + } + // The last search found no top node: what it reached is the source's side. + let lv = 0; + for (let i = 0; i < s.n; i++) (s.inS[i] = s.dist[i] >= 0 ? 1 : 0), (s.level[i] = s.dist[i]), (lv = max(lv, s.dist[i])); + s.maxLevel = lv; + s.cutCap = 0; + s.cutList = []; + for (let k = 0; k < s.m; k++) { + s.cut[k] = s.inS[eu[k]] !== s.inS[ev[k]] ? 1 : 0; + if (s.cut[k]) (s.cutCap += s.cap[k]), s.cutList.push(k); + } + // Left to right, for the pieces of the sink bar. + s.cutList.sort((a, b) => s.X[eu[a]] + s.X[ev[a]] - s.X[eu[b]] - s.X[ev[b]]); + const P = s.paths.length; + s.gap = cl(10 / max(1, P), 0.15, 0.9); + s.paths.forEach((p, i) => (p.t0 = i * s.gap)); + s.flowEnd = P ? (P - 1) * s.gap + s.paths[P - 1].cum[s.paths[P - 1].cum.length - 1] / V : 0; + s.arrived = 0; +} + +// A new run: capacities from the current field, the flow drained from what +// the panel shows over DRAIN units while the pipes take their new widths. +const DRAIN = 2, REVEAL = 2.5, HOLD = 5; +function restart(s, fresh) { + if (fresh) drawField(s); + capacities(s, s.k4); + solve(s); + s.dcap0.set(s.dcap); + s.dflow0.set(s.dflow); + s.glow0 = s.glow; + s.phase = 1; + s.pt = 0; +} + +export function setup(params) { + // The network itself is built on the first update, from the knobs. + params.s = { C: 0, t: 0, hue: 0, k4: 0.06, glow: 0, glow0: 0, phase: 2, pt: 0, rgb: new Float32Array(N * 3), col: new Float32Array(15) }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const s = params.s, k = input.knobValues || [0, 2, 7, 0.06], d = cl(dt || 0, 0, 0.1) * cl(k[1], 0.2, 3); + s.hue = cl(k[0], 0, 1); + const C = round(cl(k[2], 7, 13)), k4 = cl(k[3], 0, 1); + if (C !== s.C) { + build(s, C); + s.k4 = k4; + drawField(s); + capacities(s, s.k4); + solve(s); + s.dcap.set(s.cap); + s.glow = 0; + s.phase = 2; + s.pt = 0; + } else if (abs(k4 - s.k4) > 0.004) { + s.k4 = k4; + restart(s, false); + } + if (input.btnPressed && input.btnPressed[0]) restart(s, true); + s.t += d; + s.pt += d; + if (s.phase === 1) { + // Drain: flow and pool fade, capacities ease to the new ones. + const e = cl(s.pt / DRAIN, 0, 1), q = e * e * (3 - 2 * e); + for (let i = 0; i < s.m; i++) (s.dcap[i] = s.dcap0[i] + (s.cap[i] - s.dcap0[i]) * q), (s.dflow[i] = s.dflow0[i] * (1 - q)); + s.glow = s.glow0 * (1 - q); + if (e >= 1) (s.phase = 2), (s.pt = 0), s.dflow.fill(0), s.dcap.set(s.cap); + } else if (s.phase === 2) { + // Augment: as a pulse's head passes the end of a pipe, the pipe takes its flow. + for (const p of s.paths) { + const dist = (s.pt - p.t0) * V; + while (p.j < p.ks.length && dist >= p.cum[p.j + 2]) { + s.dflow[p.ks[p.j]] += p.sg[p.j] * p.d; + p.j++; + } + if (!p.done && dist >= p.cum[p.cum.length - 1]) (p.done = 1), (s.arrived += p.d); + } + // Every path is in: show the side the source still reaches, kept apart + // so the next run's search does not move the pool while it fades. + if (s.pt > s.flowEnd + 0.4) (s.phase = 3), (s.pt = 0), s.vS.set(s.inS), s.vLevel.set(s.level); + } else if (s.phase === 3) { + s.glow = 1; + if (s.pt > REVEAL + HOLD) restart(s, true); + } +} + +// Brightest-wins write, so pipes meeting at a node do not pile up. +function put(rgb, p, r, g, b) { + const o = p * 3; + if (r > rgb[o]) rgb[o] = r; + if (g > rgb[o + 1]) rgb[o + 1] = g; + if (b > rgb[o + 2]) rgb[o + 2] = b; +} +function add(rgb, x, y, r, g, b, w) { + const xi = round(x), yi = round(y); + if (xi < 0 || xi >= W || yi < 0 || yi >= H) return; + const o = (yi * W + xi) * 3; + rgb[o] += r * w; + rgb[o + 1] += g * w; + rgb[o + 2] += b * w; +} + +export function draw(display, params) { + const s = params.s; + if (!s || !s.C) return; + const { rgb, col, n, m, eu, ev, sb, sm, ss, tt, off, dcap, dflow, near, vS, vLevel } = s; + rgb.fill(0); + const hb = s.hue + 0.5; + // The dim colours lose their saturation near orange and yellow, which + // read brown and olive on the panel when dim. + hsv(hb + 0.08, 0.45 * (1 - 0.85 * warm(hb + 0.08)), 1, col, 0); // empty pipe + hsv(hb, 0.8, 1, col, 3); // flow + hsv(hb + 0.62, 0.78, 1, col, 6); // full pipe + hsv(hb + 0.4, 0.45, 1, col, 9); // cut flash + hsv(hb, 0.8 * (1 - 0.8 * warm(hb)), 1, col, 12); // pool + // The pool: LEDs whose nearest node the source still reaches, rising by + // search level, with its rim (the cut line, a path in the dual) brighter. + const reveal = s.phase === 3 ? (s.pt / REVEAL) * (s.maxLevel + 2) : s.phase === 1 ? 1e9 : -1; + if (s.glow > 0.01 && reveal >= 0) { + for (let p = 0; p < N; p++) { + const i = near[p]; + if (!vS[i]) continue; + const a = cl(reveal - vLevel[i], 0, 1) * s.glow; + if (a <= 0) continue; + const x = p % W, y = (p / W) | 0; + const rim = (y > 0 && !vS[near[p - W]]) || (x > 0 && !vS[near[p - 1]]) || (x < W - 1 && !vS[near[p + 1]]); + const v = (rim ? 110 : 34) * a; + put(rgb, p, col[12] * v, col[13] * v, col[14] * v); + } + } + const hold = s.phase === 3 ? cl((s.pt - REVEAL) / 0.4, 0, 1) : 0; + const flash = 0.5 + 0.5 * sin(s.t * 7), wf = warm(hb); + for (let k = 0; k < m; k++) { + const cap = max(dcap[k], 0.01), r = min(1, abs(dflow[k]) / cap), cf = cap / 9; + // A pipe's width and brightness both grow with its capacity. + let wd = 0.6 + cf; + const v0 = 0.1 + 0.36 * cf * sqrt(cf); + let cr = col[0] * v0, cg = col[1] * v0, cb = col[2] * v0; + if (r > 0.999) { + cr = col[6]; + cg = col[7]; + cb = col[8]; + // Once the cut is found, full pipes off it go back to the flow colour. + if (!s.cut[k] && hold > 0) { + cr += (col[3] * 0.8 - cr) * hold; + cg += (col[4] * 0.8 - cg) * hold; + cb += (col[5] * 0.8 - cb) * hold; + } else if (hold > 0) { + const f = hold * flash; + cr += (col[9] - cr) * f; + cg += (col[10] - cg) * f; + cb += (col[11] - cb) * f; + wd += hold * (0.6 + 0.6 * flash); + } + } else if (r > 0) { + // Part full: brighter with its share, and lifted near yellow. + const v = 0.45 + 0.4 * r + 0.25 * (1 - r) * wf, a = 0.4 + 0.6 * r; + cr += (col[3] * v - cr) * a; + cg += (col[4] * v - cg) * a; + cb += (col[5] * v - cb) * a; + } + // Flow runs along the pipe as faint ripples from the side it comes from. + const dir = dflow[k] > 0 ? 1 : -1, wave = r > 0 && r < 0.999 ? 0.25 : 0, hw = wd / 2; + const len = Math.hypot(s.X[ev[k]] - s.X[eu[k]], s.Y[ev[k]] - s.Y[eu[k]]); + for (let j = off[k]; j < off[k + 1]; j++) { + const w0 = (1 - wave + wave * sin(TAU * ((dir * tt[j] * len) / 5 - s.t * 0.9))) * 255; + const c = sm[j], top = ss[j] === W ? H : W; + for (let i = max(0, Math.ceil(c - hw - 0.5)); i <= min(top - 1, floor(c + hw + 0.5)); i++) { + const cover = min(i + 0.5, c + hw) - max(i - 0.5, c - hw); + if (cover > 0) put(rgb, sb[j] + i * ss[j], cr * cover * w0, cg * cover * w0, cb * cover * w0); + } + } + } + // Nodes. + for (let i = 0; i < n; i++) { + const p = round(s.Y[i]) * W + round(s.X[i]); + const v = vS[i] && s.glow > 0 && reveal > vLevel[i] ? 200 : 120; + put(rgb, p, v * 0.9, v * 0.95, v); + } + // Pulses: a white head and a tail in the flow colour. + if (s.phase === 2) + for (const p of s.paths) { + const dist = (s.pt - p.t0) * V, L = p.cum[p.cum.length - 1]; + if (dist < 0 || dist > L + 12) continue; + let seg = 0; + for (let j = 0; j < 14; j++) { + const dd = dist - j; + if (dd < 0) break; + if (dd > L) continue; + while (seg + 1 < p.cum.length - 1 && p.cum[seg + 1] < dd) seg++; + while (seg > 0 && p.cum[seg] > dd) seg--; + const f = (dd - p.cum[seg]) / max(1e-6, p.cum[seg + 1] - p.cum[seg]); + const x = p.pts[2 * seg] + (p.pts[2 * seg + 2] - p.pts[2 * seg]) * f, y = p.pts[2 * seg + 1] + (p.pts[2 * seg + 3] - p.pts[2 * seg + 1]) * f; + if (j === 0) { + add(rgb, x, y, 255, 255, 255, 1); + for (const [ox, oy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) add(rgb, x + ox, y + oy, col[3], col[4], col[5], 140); + } else { + const w = (1 - j / 14) ** 2 * 230; + add(rgb, x, y, col[3], col[4], col[5], w); + } + } + } + // Source bar along the bottom, sink bar along the top. The sink bar lights + // from the middle out as the flow arrives; once the cut is found it splits + // into one piece per cut pipe, as wide as its share of the flow, and the + // pieces fill the bar exactly. + for (let x = 0; x < W; x++) for (const y of [126, 127]) put(rgb, y * W + x, col[3] * 200, col[4] * 200, col[5] * 200); + const fill = s.phase === 2 ? s.arrived / max(1, s.F) : s.phase === 3 ? 1 : s.glow; + if (hold > 0) { + let x0 = 0; + s.cutList.forEach((k, i) => { + const x1 = x0 + (W * s.cap[k]) / s.F, c = i & 1 ? 6 : 9, a = i & 1 ? 0.9 : 0.55 + 0.45 * flash; + for (let x = round(x0); x < round(x1); x++) for (const y of [0, 1]) put(rgb, y * W + x, col[c] * 230 * a, col[c + 1] * 230 * a, col[c + 2] * 230 * a); + x0 = x1; + }); + } else + for (let x = 0; x < W; x++) { + const v = abs(x - 31.5) < fill * 32 ? 200 : 60; + for (const y of [0, 1]) put(rgb, y * W + x, col[3] * v, col[4] * v, col[5] * v); + } + for (let p = 0; p < N; p++) display.setPixel(p % W, (p / W) | 0, min(255, rgb[p * 3]), min(255, rgb[p * 3 + 1]), min(255, rgb[p * 3 + 2])); +} From 79240c44829ba3242be6eda69fe4ff4ce0fce72b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 02:15:57 +0000 Subject: [PATCH 09/14] =?UTF-8?q?53=ED=8C=90:=20Costas=20Array=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Yb8D9UxrifXqrXju5CVbqY --- .../pattern-study/patterns/costas-array.js | 398 ++++++++++++++++++ 1 file changed, 398 insertions(+) create mode 100644 incoming/pattern-study/patterns/costas-array.js diff --git a/incoming/pattern-study/patterns/costas-array.js b/incoming/pattern-study/patterns/costas-array.js new file mode 100644 index 000000000..4c39cedc7 --- /dev/null +++ b/incoming/pattern-study/patterns/costas-array.js @@ -0,0 +1,398 @@ +// ===== Patternflow pattern ===== +// Title: Costas Array +// Family: rhythm +// Idea: A Costas array plays as a frequency-hopping rhythm in the top half while a ghost copy of it spirals out through the shifts in time and pitch, flashing where it lands on a dot, and the bottom half fills in one cell per shift with how many dots met: noughts and ones all the way out, and one spike of N at the centre. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Costas arrays (John P. Costas, 1965, published 1984), with the Welch and Lempel-Golomb constructions (Solomon W. Golomb, 1984), pattern round 53 +// Round: 53 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Field=0..8, Rival=0..3 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the array's dots are sea green, the ghost copy rose, a single meeting in the map green and a crowd of them rose fading to white. +// Knob 2: Speed. At 2 the playhead strikes 5.5 columns a second at order 10 and 10.5 at order 30. The ghost takes 1.2 (2N - 1) steps a second and stays one step on an empty shift, four on a meeting and up to seven on a crowd, so the order 10 map is full in 15 seconds and the order 30 map in 43. 0.2 is ten times slower and 3 half as fast again. +// Knob 3: Field. The finite field the arrays are built over, one of 11, 13, 16, 17, 19, 23, 29, 31 and 32 elements. A prime p gives Welch arrays of order p - 1 and Lempel-Golomb arrays of order p - 2; 16 and 32 give Lempel-Golomb arrays of order 14 and 30. The field of 11 opens on the Welch array of 2^i mod 11. +// Knob 4: Rival. Swaps the array for a permutation that is not a Costas array and keeps the scan where it is. 0 is the Costas array and 1 a shuffle of the same order, whose map picks up a few twos and threes. 2 is the folded chirp i -> a*i mod N, a lattice of tall counts, and 3 the plain diagonal, one ridge that climbs to N - 1 beside the spike. +// Buttons: K1 tap moves on to the next array of the field: the next construction, primitive root and one of the eight symmetries of the square. The pattern also moves on by itself a few seconds after a map is full. + +const W = 64, H = 128; +const { floor, round, min, max, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const FIELDS = [11, 13, 16, 17, 19, 23, 29, 31, 32]; +// Hue of the array's dots at knob 0, and the ghost's hue past it. +const HUE0 = 0.42, GHOST = 0.5; +// Seconds the full map stays before the next array, and the extra time the +// ghost sits on the original at the start of a scan, both at Speed 2. +const HOLD = 5, REST = 0.9; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +const gcd = (a, b) => (b ? gcd(b, a % b) : a); + +// GF(q) for a prime q or q = 16, 32. The elements are 0..q-1; exp and log +// are taken to a generator g0, and the primitive elements are g0^e with e +// prime to q - 1, sorted by value so the field of 11 starts with 2. +function field(q) { + const two = (q & (q - 1)) === 0, poly = q === 16 ? 0x13 : 0x25; + const ex = new Int32Array(q - 1), lg = new Int32Array(q); + let g0 = 2; + for (;;) { + let v = 1, ok = true; + for (let i = 0; i < q - 1; i++) { + ex[i] = v; + if (i > 0 && v === 1) { ok = false; break; } + if (two) { v <<= 1; if (v & q) v ^= poly; } else v = (v * g0) % q; + } + if (ok && v === 1) break; + g0++; + } + for (let i = 0; i < q - 1; i++) lg[ex[i]] = i; + const prim = []; + for (let e = 1; e < q - 1; e++) if (gcd(e, q - 1) === 1) prim.push(ex[e]); + prim.sort((a, b) => a - b); + // 1 - a: a subtraction mod q, or in characteristic 2 an XOR. + const oneMinus = (a) => (two ? 1 ^ a : (1 - a + q) % q); + return { q, two, ex, lg, prim, oneMinus }; +} + +// Welch: column i, row g^(i + c) mod p minus 1, order p - 1. +function welch(F, g, c) { + const n = F.q - 1, e = F.lg[g], f = new Int32Array(n); + for (let i = 0; i < n; i++) f[i] = F.ex[(e * (i + c)) % n] - 1; + return f; +} + +// Lempel-Golomb: a dot at (i, j), 1 <= i, j <= q - 2, where a^i + b^j = 1. +// a = b is Lempel's symmetric array. +function lempelGolomb(F, a, b) { + const n = F.q - 2, m = F.q - 1, ea = F.lg[a], eb = F.lg[b], f = new Int32Array(n); + let inv = 1; + while ((eb * inv) % m !== 1) inv++; + for (let i = 1; i <= n; i++) { + const v = F.oneMinus(F.ex[(ea * i) % m]); + f[i - 1] = ((F.lg[v] * inv) % m) - 1; + } + return f; +} + +// The eight symmetries of the square: bit 0 transposes (the inverse +// permutation), bit 1 mirrors the columns, bit 2 the rows. +function symmetry(f, k) { + const n = f.length; + let g = Int32Array.from(f); + if (k & 1) for (let i = 0; i < n; i++) g[f[i]] = i; + if (k & 2) g = g.map((_, i, a) => a[n - 1 - i]); + if (k & 4) g = g.map((v) => n - 1 - v); + return g; +} + +// Array number k of a field: Welch and Lempel-Golomb in turn for a prime, +// Lempel-Golomb alone for 16 and 32, the roots and symmetry moving on. +function costas(F, k) { + const P = F.prim, R = P.length, h = floor(k / 2), sym = (3 * k) % 8; + let f; + if (F.two) f = lempelGolomb(F, P[k % R], P[(2 * k) % R]); + else if (k % 2 === 0) f = welch(F, P[h % R], (3 * h) % (F.q - 1)); + else f = lempelGolomb(F, P[h % R], P[(2 * h) % R]); + return symmetry(f, sym); +} + +// The coincidence count of every shift: C[dx, dy] is how many dots of the +// copy shifted by (dx, dy) land on a dot. Index (N-1-dy)*M + dx+N-1. +function counts(f) { + const n = f.length, M = 2 * n - 1, C = new Int16Array(M * M); + for (let i = 0; i < n; i++) + for (let j = 0; j < n; j++) C[(n - 1 - (f[j] - f[i])) * M + j - i + n - 1]++; + return C; +} + +// A small seeded generator, so a shuffled rival is the same each time. +function shuffle(n, seed) { + let a = seed >>> 0; + const rnd = () => { + a = (a + 0x6d2b79f5) >>> 0; + let t = Math.imul(a ^ (a >>> 15), a | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; + for (;;) { + const f = Int32Array.from({ length: n }, (_, i) => i); + for (let i = n - 1; i > 0; i--) { + const j = floor(rnd() * (i + 1)), t = f[i]; + f[i] = f[j]; + f[j] = t; + } + // A shuffle that came out a Costas array by chance is drawn again. + const C = counts(f), M = 2 * n - 1, mid = (n - 1) * M + n - 1; + let worst = 0; + for (let d = 0; d < C.length; d++) if (d !== mid) worst = max(worst, C[d]); + if (worst > 1) return f; + } +} + +function rival(f, kind, seed) { + const n = f.length; + if (kind === 1) return shuffle(n, seed); + if (kind === 2) { + let a = max(2, round(n * 0.3)); + while (gcd(a, n) !== 1) a++; + return Int32Array.from({ length: n }, (_, i) => (a * i) % n); + } + if (kind === 3) return Int32Array.from({ length: n }, (_, i) => i); + return f; +} + +// The scan. A shift and its opposite meet the same dots, the copy's dot +// and the array's dot trading places, so the ghost visits half of each +// square ring round (0, 0), the right side and the top, and the opposite +// cell of the map fills in with it. Odd rings go up the right side and left +// along the top, even rings back the other way, so each step is to a +// neighbouring shift. +function spiral(n) { + const M = 2 * n - 1, L = (M * M + 1) / 2, sx = new Int8Array(L), sy = new Int8Array(L); + let m = 1; + const put = (x, y) => ((sx[m] = x), (sy[m++] = y)); + for (let k = 1; k < n; k++) { + if (k & 1) { + for (let y = -k + 1; y <= k; y++) put(k, y); + for (let x = k - 1; x >= -k; x--) put(x, k); + } else { + for (let x = -k; x < k; x++) put(x, k); + for (let y = k; y > -k; y--) put(k, y); + } + } + const ord = new Int32Array(M * M); + for (let i = 0; i < L; i++) { + ord[(n - 1 - sy[i]) * M + sx[i] + n - 1] = i; + ord[(n - 1 + sy[i]) * M - sx[i] + n - 1] = i; + } + return { sx, sy, ord, L }; +} + +export function setup(params) { + params.s = { + hue: 0, sp: 2, fi: -1, kind: -1, k: 0, F: null, + base: null, f: null, n: 0, C: null, scan: null, + m: 0, w: 0, dur: 1, step: 1, clk: 0, rev: null, ph: 0, col: -1, + hit: null, str: null, + buf: new Float32Array(W * H * 3), pal: new Float32Array(30), palHue: -1, + }; +} + +function build(s, fresh) { + const n = s.base.length; + s.f = rival(s.base, s.kind, s.F.q * 977 + s.k * 131 + 7); + s.C = counts(s.f); + if (!fresh && s.n === n) return; + s.n = n; + s.scan = spiral(n); + s.step = 1 / (1.2 * (2 * n - 1)); + s.rev = new Float32Array((2 * n - 1) ** 2); + s.m = 0; + s.w = 0; + s.ph = 0; + s.col = -1; + s.hit = new Float32Array(n).fill(9); + s.str = new Float32Array(n).fill(9); + arrive(s, 0); +} + +function next(s, k) { + s.k = k; + s.base = costas(s.F, k); + build(s, true); +} + +// The ghost has landed on shift number m: every dot it covers flashes, +// and it stays one step for nothing and four for a meeting, up to seven for +// three or more, so the meetings are the scan's own rhythm. +function arrive(s, m) { + const { f, n } = s, dx = s.scan.sx[m], dy = s.scan.sy[m]; + let met = 0; + for (let i = 0; i < n; i++) { + const j = i + dx; + if (j >= 0 && j < n && f[j] === f[i] + dy) (s.hit[j] = 0), met++; + } + s.rev[(n - 1 - dy) * (2 * n - 1) + dx + n - 1] = s.rev[(n - 1 + dy) * (2 * n - 1) - dx + n - 1] = s.clk; + s.dur = m ? s.step * (1 + 3 * min(met, 1) + max(0, min(met, 3) - 1) * 1.5) : REST + 4 * s.step; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const s = params.s, kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + s.hue = cl(kv[0], 0, 1); + s.sp = cl(kv[1], 0.2, 3); + const fi = round(cl(kv[2], 0, 8)), kind = round(cl(kv[3], 0, 3)); + if (fi !== s.fi) { + s.fi = fi; + s.F = field(FIELDS[fi]); + s.kind = kind; + next(s, 0); + } else if (kind !== s.kind) { + s.kind = kind; + build(s, false); + } + if (input.btnPressed && input.btnPressed[0]) next(s, s.k + 1); + const r = d * (s.sp / 2), n = s.n, total = s.scan.L; + for (let i = 0; i < n; i++) (s.hit[i] += r), (s.str[i] += r); + s.clk += r; + s.w += r; + while (s.m < total - 1 && s.w >= s.dur) (s.w -= s.dur), arrive(s, ++s.m); + if (s.m === total - 1 && s.w >= HOLD) return next(s, s.k + 1); + // The playhead: 5.5 columns a second at order 10, faster for the big + // orders so a bar stays under three seconds. + s.ph += r * (3 + n / 4); + const c = floor(s.ph) % n; + if (c !== s.col) (s.col = c), (s.str[c] = 0); +} + +function palette(s) { + const P = s.pal, h = HUE0 + s.hue; + hsv(h, 0.85, 0.85, P, 0); // dot + hsv(h, 0.35, 1, P, 3); // struck dot + hsv(h, 0.7, 0.55, P, 6); // ring + hsv(h + GHOST, 0.75, 0.6, P, 9); // ghost + hsv(h, 0.8, 0.15, P, 12); // playhead band + hsv(h + 0.12, 0.9, 0.22, P, 15); // a nought in the map + hsv(h - 0.05, 0.7, 0.8, P, 18); // a one + hsv(h + GHOST, 0.8, 0.85, P, 21); // two or more + hsv(h + 0.1, 0.8, 0.1, P, 24); // lattice + hsv(h + GHOST, 0.4, 0.75, P, 27); // flash halo + s.palHue = s.hue; +} + +export function draw(display, params) { + const s = params.s; + if (!s || !s.f) return; + if (s.palHue !== s.hue) palette(s); + const { buf: B, pal: P, f, n, C } = s; + B.fill(0); + const add = (x, y, o, a) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const p = (y * W + x) * 3; + B[p] += P[o] * a; + B[p + 1] += P[o + 1] * a; + B[p + 2] += P[o + 2] * a; + }; + const white = (x, y, a) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const p = (y * W + x) * 3; + B[p] += a; + B[p + 1] += a; + B[p + 2] += a; + }; + + // Top: the array, column i is time step i, row f(i) the frequency, low + // frequencies at the bottom. + const c = floor(64 / n), d = c >= 5 ? c - 2 : c >= 3 ? c - 1 : c, e = (c - d) >> 1; + const ox = (W - c * n) >> 1, oy = (64 - c * n) >> 1, side = c * n; + const X = (i) => ox + i * c + e, Y = (r) => oy + (n - 1 - r) * c + e; + const block = (x0, y0, sz, fn) => { + for (let y = y0; y < y0 + sz; y++) for (let x = x0; x < x0 + sz; x++) fn(x, y); + }; + if (c >= 4) for (let i = 0; i < n; i++) for (let r = 0; r < n; r++) add(X(i) + (d >> 1), Y(r) + (d >> 1), 24, 1); + // The playhead's column and the row of the note it is playing. + if (s.col >= 0) { + const yr = Y(f[s.col]) - e; + for (let y = oy; y < oy + side; y++) for (let x = ox + s.col * c; x < ox + s.col * c + c; x++) add(x, y, 12, 1); + for (let y = yr; y < yr + c; y++) for (let x = ox; x < ox + side; x++) add(x, y, 12, 1); + } + for (let i = 0; i < n; i++) { + const a = s.str[i], x0 = X(i), y0 = Y(f[i]); + const k = a < 0.6 ? exp(-a * 5) : 0; + block(x0, y0, d, (x, y) => (add(x, y, 0, 1 - k), add(x, y, 3, k))); + // A struck note rings out as a widening square. + if (a < 0.45) { + const rr = (d >> 1) + 1 + floor(a * 14), cx = x0 + (d >> 1), cy = y0 + (d >> 1), al = (1 - a / 0.45) ** 2; + for (let t = -rr; t <= rr; t++) { + for (const [x, y] of [[cx + t, cy - rr], [cx + t, cy + rr], [cx - rr, cy + t], [cx + rr, cy + t]]) + if (x >= ox && x < ox + side && y >= oy && y < oy + side) add(x, y, 6, al); + } + } + } + // The ghost copy glides from shift m to shift m + 1. + const { sx, sy, ord, L: total } = s.scan, m = s.m; + // It rests, then glides in the last 0.8 of a plain step. + const g = m < total - 1 ? cl((s.w - s.dur) / (0.8 * s.step) + 1, 0, 1) : 0, gg = g * g * (3 - 2 * g); + const mn = min(m + 1, total - 1), gdx = sx[m] + (sx[mn] - sx[m]) * gg, gdy = sy[m] + (sy[mn] - sy[m]) * gg; + const hollow = d >= 3; + // The sheet the ghost is printed on: faint where it overlaps the array, + // brighter along its edge. + const gx0 = ox + round(gdx * c), gy0 = oy - round(gdy * c); + for (let y = max(oy, gy0); y < min(oy + side, gy0 + side); y++) + for (let x = max(ox, gx0); x < min(ox + side, gx0 + side); x++) { + const edge = x === gx0 || x === gx0 + side - 1 || y === gy0 || y === gy0 + side - 1; + add(x, y, 9, edge ? 0.45 : 0.1); + } + for (let i = 0; i < n; i++) { + const x0 = round(X(i) + gdx * c), y0 = round(Y(f[i]) - gdy * c); + block(x0, y0, d, (x, y) => { + if (x < ox || x >= ox + side || y < oy || y >= oy + side) return; + if (hollow && x > x0 && x < x0 + d - 1 && y > y0 && y < y0 + d - 1) return; + add(x, y, 9, hollow ? 1 : 0.7); + }); + } + // Where the ghost met a dot. + for (let j = 0; j < n; j++) { + const a = s.hit[j]; + if (a > 0.5) continue; + const k = 1 - a / 0.5, x0 = X(j), y0 = Y(f[j]); + block(x0 - 1, y0 - 1, d + 2, (x, y) => { + const edge = x < x0 || y < y0 || x >= x0 + d || y >= y0 + d; + if (edge) add(x, y, 27, k); + else white(x, y, k * 0.8), add(x, y, 27, k * 0.6); + }); + } + + // Bottom: one cell per shift, dx across and dy up, filled as the scan + // reaches it. + const M = 2 * n - 1, sc = floor(64 / M), sd = sc >= 3 ? sc - 1 : 1; + const mx = (W - sc * M) >> 1, my = 64 + ((64 - sc * M) >> 1), mid = (n - 1) * M + n - 1; + const cur = (n - 1 - sy[m]) * M + sx[m] + n - 1; + for (let gy = 0; gy < M; gy++) + for (let gx = 0; gx < M; gx++) { + const q = gy * M + gx, o = ord[q], x0 = mx + gx * sc, y0 = my + gy * sc; + if (o > m) { + if (sc >= 2) add(x0, y0, 24, sc >= 3 ? 1.6 : 0.9); + continue; + } + const v = C[q], age = s.clk - s.rev[q]; + const flash = age < 0.35 ? (1 - age / 0.35) * 0.7 : 0; + block(x0, y0, sd, (x, y) => { + if (q === mid) return white(x, y, 1); + if (v === 0) add(x, y, 15, 1); + else if (v === 1) add(x, y, 18, 1); + else { + // Two or more: the ghost hue, whitening toward N - 1. + const t = (v - 2) / max(1, n - 3); + add(x, y, 21, 1 - 0.6 * t); + white(x, y, 0.6 * t); + } + if (flash) white(x, y, flash); + }); + } + // The spike of N fills the gaps round its cell, and the scan has a cursor. + if (sc >= 3) { + const x0 = mx + (n - 1) * sc, y0 = my + (n - 1) * sc; + for (let t = -1; t <= sd; t++) white(x0 + t, y0 - 1, 0.8), white(x0 + t, y0 + sd, 0.8), white(x0 - 1, y0 + t, 0.8), white(x0 + sd, y0 + t, 0.8); + } + // The opposite shift gets a fainter cursor of its own. + if (m < total - 1) + for (const [q, a] of [[cur, 1], [M * M - 1 - cur, 0.45]]) { + const x0 = mx + (q % M) * sc, y0 = my + floor(q / M) * sc; + if (sd >= 2) for (let t = -1; t <= sd; t++) white(x0 + t, y0 - 1, 0.55 * a), white(x0 + t, y0 + sd, 0.55 * a), white(x0 - 1, y0 + t, 0.55 * a), white(x0 + sd, y0 + t, 0.55 * a); + else white(x0, y0, 0.8 * a), white(x0 - 1, y0, 0.35 * a), white(x0 + 1, y0, 0.35 * a), white(x0, y0 - 1, 0.35 * a), white(x0, y0 + 1, 0.35 * a); + } + + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p += 3) + display.setPixel(x, y, min(255, round(B[p] * 255)), min(255, round(B[p + 1] * 255)), min(255, round(B[p + 2] * 255))); +} From 49a48ba85d8a904df0ba9af33bdb89ace49c5f79 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 02:21:05 +0000 Subject: [PATCH 10/14] =?UTF-8?q?53=ED=8C=90:=20Euler's=20Elastica=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Yb8D9UxrifXqrXju5CVbqY --- incoming/pattern-study/patterns/elastica.js | 346 ++++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 incoming/pattern-study/patterns/elastica.js diff --git a/incoming/pattern-study/patterns/elastica.js b/incoming/pattern-study/patterns/elastica.js new file mode 100644 index 000000000..55f4df239 --- /dev/null +++ b/incoming/pattern-study/patterns/elastica.js @@ -0,0 +1,346 @@ +// ===== Patternflow pattern ===== +// Title: Euler's Elastica +// Family: physics +// Idea: A thin elastic rod 120 LEDs long, pinned at both ends on its line of thrust, is slowly pushed and let go and sweeps Euler's family of bent shapes from the first slight bow through the arch and the drop where its ends meet to the loops where they pass each other, while a bead runs along it, a pendulum above swings with its arm always parallel to the rod at the bead, and the pendulum's orbit goes round its phase portrait below. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Euler's elastica (Leonhard Euler, 1744), after Jacob Bernoulli's elastic curve (1694), with Kirchhoff's kinetic analogy to the pendulum (Gustav Kirchhoff, 1859), pattern round 53 +// Round: 53 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Mode=1..4, Preload=0..1 +// +// Knob 1: Hue. The rod is coloured by the direction it points, and the same colours mark the pendulum's swing and the orbit below. At 0 a straight rod is cyan, leaning one way it turns green and the other way violet; the knob turns all of it round the wheel, hurrying past orange and yellow. +// Knob 2: Speed. At 2 one push and release takes 16 seconds and the bead runs the rod in 2.4; at 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Mode. How many half waves the rod buckles into, 1 to 4. At the same end slope mode n carries n squared times the load of mode 1, and its rod is n copies of the mode 1 shape at 1/n the size, each the mirror of the one before. +// Knob 4: Preload. How far each release goes back. At 0 the rod comes back straight and buckles again to a side picked by chance; at 0.5 it never unbends past an end slope of 38 degrees, and at 1 it stays among the loops, 150 to 176 degrees. +// Buttons: K1 tap holds the load where it is, so one shape stands still while the bead and the pendulum keep going; a second tap lets the load move again. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, ceil, round, abs, sqrt, sin, cos, asin, exp, PI } = Math; +const DEG = PI / 180; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The rod is 120 LEDs long and drawn from 241 points half an LED apart. +const L = 120, M = 240; +// The largest end slope, one push and release at Speed 2 in seconds, and the +// bead's speed at Speed 2 in LEDs a second. +const AMAX = 176 * DEG, CYCLE = 16, BEAD = 50; +// The rod's radius in each mode: thinner for the smaller loops of modes 3 and 4. +const RAD = [0, 2.7, 2.6, 2.2, 1.9]; +// The pendulum's pivot and arm, and the phase portrait's middle row, half +// height and half width. +const PX = 31.5, PY = 20.5, PL = 16.5; +const PIN = [0.82, 0.82, 0.86]; +const QY = 108.5, QH = 16, QW = 29.5; +// Light from the upper left and in front, and the half vector for the shine. +const LX = -0.42, LY = -0.5, LZ = 0.757; +const HX = -0.229, HY = -0.272, HZ = 0.935; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// The arithmetic-geometric mean of 1 and k' gives K(k), E(k) and the a_n, c_n +// the Jacobi functions are built from (Abramowitz and Stegun 17.6). +function agm(k, g) { + const a = g.a, c = g.c; + let b = sqrt(1 - k * k), n = 0, sum = 0.5 * k * k; + a[0] = 1; + c[0] = k; + while (n < 15 && c[n] > 1e-15) { + const an = a[n]; + a[n + 1] = (an + b) / 2; + c[n + 1] = (an - b) / 2; + b = sqrt(an * b); + n++; + sum += 2 ** (n - 1) * c[n] * c[n]; + } + g.n = n; + g.K = PI / (2 * a[n]); + g.E = g.K * (1 - sum); +} + +// sn u, cn u and Jacobi's epsilon E(u) = Z(u) + u E/K, by the descending +// Landen sequence of amplitudes; Z(u) is the sum of c_n sin(phi_n). +function jac(u, g, o) { + const { a, c, n, ph } = g; + ph[n] = 2 ** n * a[n] * u; + let z = 0; + for (let i = n; i >= 1; i--) { + z += c[i] * sin(ph[i]); + ph[i - 1] = (asin((c[i] / a[i]) * sin(ph[i])) + ph[i]) / 2; + } + o.sn = sin(ph[0]); + o.cn = cos(ph[0]); + o.e = z + (g.E / g.K) * u; +} + +export function setup(params) { + const F = (n) => new Float32Array(n); + params.s = { + hue: 0, mode: 1, alo: 0, al: 0, ph: 0.07, side: 1, hold: false, + sb: 0, dir: 1, + g: { a: new Float64Array(17), c: new Float64Array(17), ph: new Float64Array(17), n: 0, K: PI / 2, E: PI / 2 }, + j: { sn: 0, cn: 1, e: 0 }, + cx: F(M + 1), cy: F(M + 1), sg: F(M + 1), + buf: F(N * 3), zs: F(N), zd: F(N), zo: F(N), col: F(3), + }; +} + +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); + const btn = input.btnPressed || []; + S.hue = cl(k[0], 0, 1); + const sp = cl(k[1], 0.2, 3); + S.mode = cl(round(k[2]), 1, 4); + const pre = cl(k[3], 0, 1); + S.alo = 150 * DEG * pre * pre; + if (btn[0]) S.hold = !S.hold; + if (!S.hold) { + const was = floor(S.ph); + S.ph += (d * sp) / (2 * CYCLE); + // Back at the bottom of a release with the rod straight, it buckles again + // to whichever side chance picks. + if (floor(S.ph) !== was && S.alo < 1.2 * DEG) S.side = Math.random() < 0.5 ? -1 : 1; + } + S.al = S.alo + (AMAX - S.alo) * (0.5 - 0.5 * cos(2 * PI * S.ph)); + S.sb += d * sp * (BEAD / 2) * S.dir; + if (S.sb > L) (S.sb = 2 * L - S.sb), (S.dir = -1); + if (S.sb < 0) (S.sb = -S.sb), (S.dir = 1); +} + +// The colour of a rod pointing at screen angle a from straight up. The wheel +// hurries from rose to lime, so dim orange and yellow, brown and olive on +// this panel, hardly come up. +function tint(S, a, out, o) { + let h = S.hue + 0.53 + (0.24 * a) / PI; + h -= floor(h); + const x = (h + 0.05 - floor(h + 0.05)) / 0.3; + if (x < 1) h = 0.95 + 0.3 * (x - sin(2 * PI * x) / (2 * PI)); + hsv(h, 0.8, 1, out, o); +} + +// A point drawn between four LEDs, each channel kept at the brightest. +function dot(buf, x, y, r, g, b) { + x -= 0.5; + y -= 0.5; + const x0 = floor(x), y0 = floor(y), fx = x - x0, fy = y - y0; + for (let j = 0; j < 2; j++) + for (let i = 0; i < 2; i++) { + const X = x0 + i, Y = y0 + j; + if (X < 0 || X >= W || Y < 0 || Y >= H) continue; + const w = (i ? fx : 1 - fx) * (j ? fy : 1 - fy), p = (Y * W + X) * 3; + buf[p] = max(buf[p], r * w); + buf[p + 1] = max(buf[p + 1], g * w); + buf[p + 2] = max(buf[p + 2], b * w); + } +} + +function line(buf, ax, ay, bx, by, r, g, b) { + const m = max(1, ceil(Math.hypot(bx - ax, by - ay) * 2)); + for (let i = 0; i <= m; i++) dot(buf, ax + ((bx - ax) * i) / m, ay + ((by - ay) * i) / m, r, g, b); +} + +// A shaded ball, written over what is there. +function ball(buf, x, y, R, col) { + for (let Y = max(0, floor(y - R - 1)); Y <= min(H - 1, ceil(y + R + 1)); Y++) + for (let X = max(0, floor(x - R - 1)); X <= min(W - 1, ceil(x + R + 1)); X++) { + const dx = X + 0.5 - x, dy = Y + 0.5 - y, d2 = dx * dx + dy * dy, p = (Y * W + X) * 3; + if (d2 >= (R + 0.9) * (R + 0.9)) continue; + if (d2 >= R * R) { + buf[p] = buf[p + 1] = buf[p + 2] = 0; + continue; + } + const nz = sqrt(R * R - d2) / R, nx = dx / R, ny = dy / R; + const lam = max(0, nx * LX + ny * LY + nz * LZ), sh = max(0, nx * HX + ny * HY + nz * HZ) ** 24; + const v = 0.3 + 0.75 * lam; + buf[p] = col[0] * v + sh * 0.6; + buf[p + 1] = col[1] * v + sh * 0.6; + buf[p + 2] = col[2] * v + sh * 0.6; + } +} + +// The rod's shape for end slope al in mode n, in closed form. With +// k = sin(al/2) and sqrt(lambda) = 2nK/L, the tangent's angle theta from the +// line of thrust has sin(theta/2) = k sn(u), u = sqrt(lambda) s + K, so +// theta'' = -lambda sin(theta), the pendulum. Integrating cos and sin of theta: +// x = (2/sqrt(lambda)) (E(u) - E(K)) - s along the line, y = -(2k/sqrt(lambda)) cn(u) off it. +function shape(S) { + const { g, j, cx, cy, sg } = S, k = sin(S.al / 2), n = S.mode; + agm(k, g); + const r = (2 * n * g.K) / L; + jac(g.K, g, j); + const e0 = j.e; + let lo = 1e9, hi = -1e9; + for (let i = 0; i <= M; i++) { + const s = (L * i) / M; + jac(r * s + g.K, g, j); + cy[i] = (2 / r) * (j.e - e0) - s; + cx[i] = S.side * (-2 * k / r) * j.cn; + sg[i] = S.side * 2 * asin(cl(k * j.sn, -1, 1)); + lo = min(lo, cx[i]); + hi = max(hi, cx[i]); + } + // The pins sit on the vertical centre line while the rod fits the panel. + // Mode 1 bows to one side only, up to 48.4 LEDs at an end slope of 114 + // degrees, so there the line of thrust slides over with the pins on it + // until the bow clears the far edge. Nothing is scaled. + const m = RAD[n] + 1.6, D = cy[M], ox = cl(31.5, m - lo, W - m - hi); + for (let i = 0; i <= M; i++) { + cx[i] += ox; + cy[i] = 63.5 + D / 2 - cy[i]; + } + S.r = r; + S.k = k; + S.ox = ox; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + shape(S); + const { buf, zs, zd, zo, cx, cy, sg, g, j, col } = S, k = S.k; + buf.fill(0); + zs.fill(-1); + + // The bead's angle and the pendulum's phase: going up the rod is forward + // time, coming back down is the same swing run on. + jac(S.r * S.sb + g.K, g, j); + const sb = S.side * 2 * asin(cl(k * j.sn, -1, 1)); + const vb = S.side * k * j.cn * S.dir; + const ub = S.dir > 0 ? S.r * S.sb + g.K : 2 * g.K - (S.r * S.sb + g.K); + + // The line of thrust between the pins, dotted. + const ya = cy[0], yb = cy[M]; + for (let y = min(ya, yb) + 1; y < max(ya, yb) - 1; y += 2) dot(buf, S.ox, y, 0.28, 0.28, 0.34); + + // The pendulum, all of it behind the rod. Its arm hangs straight down when + // the rod at the bead points straight up. First a long exposure of the + // swing: the arm lingers where it turns, so the fan is brightest at its + // edges, by how slowly the pendulum passes each angle. + const pend = (a) => [PX - PL * sin(a), PY + PL * cos(a)]; + for (let y = max(0, floor(PY - PL - 1)); y <= ceil(PY + PL + 1); y++) + for (let x = 0; x < W; x++) { + const dx = x + 0.5 - PX, dy = y + 0.5 - PY, rr = Math.hypot(dx, dy); + if (rr > PL + 0.5 || rr < 1.5) continue; + const a = Math.atan2(-dx, dy), sa = sin(abs(a) / 2); + if (sa >= k) continue; + const v = sqrt(1 - (sa * sa) / (k * k)), f = (round((0.07 + 0.2 * (1 - v) ** 2) * 16) / 16) * min(1, PL + 0.5 - rr); + tint(S, a, col, 0); + const o = (y * W + x) * 3; + buf[o] = col[0] * f; + buf[o + 1] = col[1] * f; + buf[o + 2] = col[2] * f; + } + // The bob's path from -al to al in the rod's colours, and the arm where it + // was over the bead's last 12 LEDs, fading. + for (let a = -S.al; a <= S.al; a += 0.5 / PL) { + tint(S, a, col, 0); + const [x, y] = pend(a); + dot(buf, x, y, col[0] * 0.4, col[1] * 0.4, col[2] * 0.4); + } + for (let q = 12; q >= 1; q--) { + let s = S.sb - S.dir * q; + s = s < 0 ? -s : s > L ? 2 * L - s : s; + jac(S.r * s + g.K, g, j); + const a = S.side * 2 * asin(cl(k * j.sn, -1, 1)), f = 0.34 * (1 - q / 13); + tint(S, a, col, 0); + const [x, y] = pend(a); + line(buf, PX, PY, x, y, col[0] * f, col[1] * f, col[2] * f); + } + const [bx, by] = pend(sb); + line(buf, PX, PY, bx, by, 0.62, 0.62, 0.68); + tint(S, sb, col, 0); + ball(buf, bx, by, 3.3, col); + ball(buf, PX, PY, 1.3, PIN); + + // The phase portrait: the separatrix and the orbits of end slopes 30 to 150 + // degrees, faint, and the orbit at this slope, brightest just behind the + // moving point. An orbit of slope b has sin(theta/2) = sin(b/2) sin(t) and + // theta' over twice sqrt(lambda) = sin(b/2) cos(t). + const qx = (a) => 31.5 - (QW * a) / PI, qy = (v) => QY - QH * v; + for (let a = -PI; a <= PI; a += 0.02) { + const v = cos(a / 2); + tint(S, a, col, 0); + dot(buf, qx(a), qy(v), col[0] * 0.26, col[1] * 0.26, col[2] * 0.26); + dot(buf, qx(a), qy(-v), col[0] * 0.26, col[1] * 0.26, col[2] * 0.26); + } + for (let o = 1; o <= 5; o++) { + const kb = sin((o * PI) / 12); + for (let t = 0; t < 2 * PI; t += 0.03) { + const a = 2 * asin(kb * sin(t)); + tint(S, a, col, 0); + dot(buf, qx(a), qy(kb * cos(t)), col[0] * 0.16, col[1] * 0.16, col[2] * 0.16); + } + } + const K4 = 4 * g.K, steps = 520; + for (let i = 0; i < steps; i++) { + const u = (K4 * i) / steps; + jac(u, g, j); + const a = S.side * 2 * asin(cl(k * j.sn, -1, 1)), v = S.side * k * j.cn; + let b = (ub - u) / K4; + b -= floor(b); + const f = 0.42 + 0.58 * exp(-b * 7); + tint(S, a, col, 0); + dot(buf, qx(a), qy(v), col[0] * f, col[1] * f, col[2] * f); + } + tint(S, sb, col, 0); + ball(buf, qx(sb), qy(vb), 1.9, col); + + // The rod. Each LED near it takes the nearest point of the strand on top: + // a strand more than one and a half rod widths further along lies over the + // earlier one, and its dark rim cuts the one below where the loops cross. + const R = RAD[S.mode], R1 = R + 1.1, same = 6 * R; + for (let i = 0; i < M; i++) { + const ax = cx[i], ay = cy[i], dx = cx[i + 1] - ax, dy = cy[i + 1] - ay, l2 = dx * dx + dy * dy || 1e-9, ln = sqrt(l2); + const x0 = max(0, floor(min(ax, ax + dx) - R1)), x1 = min(W - 1, ceil(max(ax, ax + dx) + R1)); + const y0 = max(0, floor(min(ay, ay + dy) - R1)), y1 = min(H - 1, ceil(max(ay, ay + dy) + R1)); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const px = x + 0.5 - ax, py = y + 0.5 - ay, t = cl((px * dx + py * dy) / l2, 0, 1); + const qx2 = px - t * dx, qy2 = py - t * dy, d2 = qx2 * qx2 + qy2 * qy2; + if (d2 >= R1 * R1) continue; + const p = y * W + x, s = i + t, z = zs[p], dd = sqrt(d2); + if (z >= 0 && s <= z + same && dd >= zd[p]) continue; + zs[p] = s; + zd[p] = dd; + zo[p] = (dx * qy2 - dy * qx2) / ln; + } + } + for (let p = 0; p < N; p++) { + const s = zs[p]; + if (s < 0) continue; + const o = p * 3; + if (zd[p] >= R) { + buf[o] = buf[o + 1] = buf[o + 2] = 0; + continue; + } + const i = min(M - 1, floor(s)), f = s - i, a = sg[i] + (sg[i + 1] - sg[i]) * f; + // The tube's normal: across the rod in the panel, and out of it. + const u = zo[p] / R, w = sqrt(max(0, 1 - u * u)), nx = cos(a) * u, ny = sin(a) * u; + const lam = max(0, nx * LX + ny * LY + w * LZ), sh = max(0, nx * HX + ny * HY + w * HZ) ** 20; + tint(S, a, col, 0); + const sl = (s * L) / M, gb = exp(-(((sl - S.sb) / 1.7) ** 2)), tr = S.dir * (S.sb - sl); + const v = (0.3 + 0.75 * lam) * (1 + (tr > 0 ? 0.35 * exp(-tr / 9) : 0)); + buf[o] = col[0] * v + sh * 0.55 + gb * 0.8; + buf[o + 1] = col[1] * v + sh * 0.55 + gb * 0.8; + buf[o + 2] = col[2] * v + sh * 0.55 + gb * 0.8; + } + + // The pins, on top. + ball(buf, cx[0], cy[0], 2.2, PIN); + ball(buf, cx[M], cy[M], 2.2, PIN); + + for (let p = 0, o = 0; p < N; p++, o += 3) { + if (buf[o] + buf[o + 1] + buf[o + 2] < 0.02) continue; + // Each channel in steps of 4 of 255. + display.setPixel(p % W, (p / W) | 0, min(255, round(buf[o] * 63.75) * 4), min(255, round(buf[o + 1] * 63.75) * 4), min(255, round(buf[o + 2] * 63.75) * 4)); + } +} From 85f07c7e463f93dc026b4a9e43dcfdb6c2902961 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 02:32:17 +0000 Subject: [PATCH 11/14] =?UTF-8?q?53=ED=8C=90:=20Four-Vertex=20Theorem=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Yb8D9UxrifXqrXju5CVbqY --- .../pattern-study/patterns/four-vertex.js | 368 ++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 incoming/pattern-study/patterns/four-vertex.js diff --git a/incoming/pattern-study/patterns/four-vertex.js b/incoming/pattern-study/patterns/four-vertex.js new file mode 100644 index 000000000..63e4c0a61 --- /dev/null +++ b/incoming/pattern-study/patterns/four-vertex.js @@ -0,0 +1,368 @@ +// ===== Patternflow pattern ===== +// Title: Four-Vertex Theorem +// Family: line +// Idea: A smooth closed curve slowly changes shape while its evolute, the path of its centres of curvature, turns inside it as a gold star with one cusp for each white vertex where the curvature is greatest or least, and the cusps are born and die in pairs but never number fewer than four. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: four-vertex theorem (Syamadas Mukhopadhyaya, 1909; Adolf Kneser, 1912), with the curve's evolute (Christiaan Huygens, 1673), pattern round 53 +// Round: 53 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Terms=2..6, Bend=0..1 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the curve runs from azure where it is flattest through violet to magenta where it is sharpest. Its evolute is peach on the arcs that come from flat parts and pale gold on those from sharp parts, over a dark violet glow on the LEDs that four normals of the curve pass through. At 0.25 the curve runs violet through red to orange and the evolute is lime; at 0.5 orange to green with a pale azure evolute; at 0.75 green to blue with a pink one. The dark glow keeps off orange and yellow, which read brown and olive when dim. +// Knob 2: Speed. At 2 each wave of the curve swells and fades every 5 to 10 seconds while it turns, and the vertex count changes about 30 times a minute. The osculating circle rolls once round the curve in 7 seconds; on each lap its radius swells to about 105 LEDs on the flattest stretch (62 to 455) and shrinks to about 17 at the sharpest end. At 0.2 everything is ten times slower, at 3 half as fast again. +// Knob 3: Terms. How many waves bend the curve's radius: at 2 one with 3 lobes and one with 4, and each step adds the next, up to 8 lobes at 6. All of them share one swing of curvature, so the higher ones hardly move the outline while they fold the evolute into small swallowtails. At 2 the curve has 4 vertices 13% of the time, 6 for 52% and 8 for 35%; at 6 it has 4 to 16, mostly 10 or 12, and the count changes 60 times a minute. Every count is even. +// Knob 4: Bend. How far the waves may swing the curvature. Up to 0.06 the curve stays convex and the evolute a closed star. At Terms 2 the curve starts to dent from about 0.1, and at 1 it is dented 84% of the time (53% at Terms 6). The curvature passes through zero at the two ends of each dent and the evolute runs off the panel towards them; a small arc of it opens outside the curve, carrying the cusp of the dent's own vertex. Kneser's theorem covers these curves too, and the count still never falls below 4. +// Buttons: K1 tap swells the next wave up for about two seconds, peaking half a second after the tap, so a burst of swallowtails opens on the evolute and closes again; at the opening the count rises from 6.4 on average to a peak of 9.6. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, sqrt, sin, cos, log, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +// Samples round the curve, the lowest and highest waves (in lobes), and +// how much taller than wide the curve stands before its waves bend it. +const M = 720, K0 = 3, KMAX = 8, ASP = 1.4; +// The box the curve is scaled to fit, in LEDs. +const BOX_W = 60, BOX_H = 120; + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +export function setup(params) { + const R = Math.random, K = KMAX + 1; + const nu = new Float64Array(K), a0 = new Float64Array(K), mu = new Float64Array(K), p0 = new Float64Array(K); + for (let k = K0; k <= KMAX; k++) { + nu[k] = 0.5 + 0.4 * R(); + a0[k] = TAU * R(); + mu[k] = (k % 2 ? -1 : 1) * (0.05 + 0.08 * R()) * (3 / k); + p0[k] = TAU * R(); + } + params.s = { + nu, a0, mu, p0, wgt: new Float64Array(K), amp: new Float64Array(K), ph: new Float64Array(K), + t: 0, u: 0, tap: 99, hue: 0, speed: 2, terms: 2, bend: 0.06, first: true, + // Per sample: the point, its curvature, its centre of curvature, the sign + // of the curvature's slope, and the arc length so far. + x: new Float64Array(M), y: new Float64Array(M), k: new Float64Array(M), + ex: new Float64Array(M), ey: new Float64Array(M), v: new Float64Array(M), arc: new Float64Array(M + 1), + sc: 1, ox: 32, oy: 64, kmin: 0, kmax: 1, + vert: [], prev: [], flash: [], osc: null, pt: new Float64Array(10), + dx: new Float64Array(M), dy: new Float64Array(M), ga: new Float64Array(M), gx: new Float64Array(M), gb: new Float64Array(M), run: new Int32Array(W + 1), cnt: new Uint8Array(N), q: new Float64Array(M), + // A running record: frames, how many had each vertex count, the least + // count seen, and frames where the curve was dented. + stat: { frames: 0, hist: {}, minCount: 99, concave: 0 }, + rgb: new Float32Array(N * 3), col: new Float64Array(3), + }; +} + +// The curve in its own frame: r(phi) = 1 + sum c_k cos(k phi - p_k), drawn +// as (r cos phi, ASP r sin phi), with r and its first three derivatives +// summed exactly. Fills o with the point (0, 1), its centre of curvature +// (2, 3), the curvature (4), a number with the sign of the curvature's +// slope (5) and the velocity (6, 7), all in the curve's own units. With +// cross = x'y'' - y'x'' and D = x'^2 + y'^2 the curvature is cross / D^1.5, +// its slope has the sign of cross' D - 1.5 cross D', and the centre is the +// point plus D / cross times the velocity turned a quarter. +function at(S, phi, o) { + let r = 1, r1 = 0, r2 = 0, r3 = 0; + for (let k = K0; k <= KMAX; k++) { + const a = S.amp[k]; + if (a === 0) continue; + const q = k * phi - S.ph[k], c = a * cos(q), s = a * sin(q); + r += c; + r1 -= k * s; + r2 -= k * k * c; + r3 += k * k * k * s; + } + const cp = cos(phi), sp = sin(phi); + const x = r * cp, y = ASP * r * sp; + const x1 = r1 * cp - r * sp, y1 = ASP * (r1 * sp + r * cp); + const x2 = r2 * cp - 2 * r1 * sp - r * cp, y2 = ASP * (r2 * sp + 2 * r1 * cp - r * sp); + const cross = x1 * y2 - y1 * x2, D = x1 * x1 + y1 * y1; + const dcross = ASP * (2 * r * r1 + 3 * r1 * r2 - r * r3), dD = 2 * (x1 * x2 + y1 * y2); + o[0] = x; + o[1] = y; + const f = D / cross; + o[2] = x - f * y1; + o[3] = y + f * x1; + o[4] = cross / (D * sqrt(D)); + o[5] = dcross * D - 1.5 * cross * dD; + o[6] = x1; + o[7] = y1; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 2, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = cl(kv[0], 0, 1); + S.speed = cl(kv[1], 0.2, 3); + S.terms = cl(floor(kv[2] + 0.5), 2, 6); + S.bend = cl(kv[3], 0, 1); + S.t += d * S.speed * 0.65; + const t = S.t; + // Each wave fades in or out over about a second when Terms moves. + let wsum = 0; + for (let k = K0; k <= KMAX; k++) { + const target = k < K0 + S.terms ? 1 : 0; + S.wgt[k] = S.first ? target : S.wgt[k] + (target - S.wgt[k]) * min(1, d * 2.5); + wsum += S.wgt[k]; + } + // How far the waves together may swing the curvature, as a share of the + // circle's. The waves split it evenly, each scaled by 1 / (k^2 - 1) since + // a wave of k lobes swings the curvature k^2 - 1 times its own height. + // At 0.7 the curve stays convex; from about 0.86 (Bend 0.1) it dents. + const budget = 0.7 + 1.6 * S.bend; + if (input.btnPressed && input.btnPressed[0]) S.tap = 0; + S.tap += d; + // A tap swells the next wave up (or the top one when all are in). The + // swell peaks 0.5 seconds after the tap and is gone in about 3. + const kick = (S.tap / 0.5) * Math.exp(1 - S.tap / 0.5); + const kicked = min(KMAX, K0 + S.terms); + for (let k = K0; k <= KMAX; k++) { + const e = 0.6 + 0.4 * sin(S.nu[k] * t + S.a0[k]); + S.amp[k] = (budget * (S.wgt[k] * e / wsum + (k === kicked ? 0.6 * kick : 0))) / (k * k - 1); + S.ph[k] = S.p0[k] + S.mu[k] * t; + } + // Sample, fit to the box, and find where the curvature's slope changes sign. + const o = S.pt, X = S.x, Y = S.y, K = S.k, EX = S.ex, EY = S.ey, V = S.v; + let x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9, kmin = 1e9, kmax = -1e9, concave = false; + // kmin is the least positive curvature. + for (let i = 0; i < M; i++) { + at(S, (TAU * i) / M, o); + (X[i] = o[0]), (Y[i] = o[1]), (EX[i] = o[2]), (EY[i] = o[3]), (K[i] = o[4]), (V[i] = o[5]), (S.dx[i] = o[6]), (S.dy[i] = o[7]); + x0 = min(x0, o[0]), x1 = max(x1, o[0]), y0 = min(y0, o[1]), y1 = max(y1, o[1]); + kmax = max(kmax, o[4]); + if (o[4] <= 0) concave = true; + else kmin = min(kmin, o[4]); + } + // The scale fits the curve to the box. + const fit = min(BOX_W / (x1 - x0), BOX_H / (y1 - y0)); + S.sc = S.first ? fit : S.sc + (fit - S.sc) * min(1, d * 2); + const sc = S.sc; + // Centred on the curve's box. + S.ox = W / 2 - (sc * (x0 + x1)) / 2; + S.oy = H / 2 + (sc * (y0 + y1)) / 2; + S.kmin = kmin; + S.kmax = kmax; + // Arc length, for the osculating circle's even roll. + S.arc[0] = 0; + for (let i = 0; i < M; i++) { + const j = (i + 1) % M; + S.arc[i + 1] = S.arc[i] + Math.hypot(X[j] - X[i], Y[j] - Y[i]); + } + // Vertices: each sign change of the slope, placed by one secant step. + S.prev = S.vert; + const vert = []; + for (let i = 0; i < M; i++) { + const j = (i + 1) % M, a = V[i], b = V[j]; + if ((a < 0) !== (b < 0)) { + const f = a / (a - b), phi = (TAU * (i + f)) / M; + at(S, phi, o); + vert.push({ phi, x: o[0], y: o[1], cx: o[2], cy: o[3], kap: o[4], max: a > 0 }); + } + } + S.vert = vert; + // Vertices born or gone this frame start a flash, drawn as a ring. + if (!S.first) { + const near = (a, b) => a.max === b.max && angDist(a.phi, b.phi) < 0.15; + if (vert.length !== S.prev.length) { + for (const v of vert) if (!S.prev.some((p) => near(p, v))) S.flash.push({ v, age: 0, born: true }); + for (const p of S.prev) if (!vert.some((v) => near(p, v))) S.flash.push({ v: p, age: 0, born: false }); + } + } + for (const f of S.flash) f.age += d; + S.flash = S.flash.filter((f) => f.age < 0.8); + // The osculating circle rolls once round in 7 seconds at Speed 2. + S.u = (S.u + (d * S.speed) / 14) % 1; + const L = S.arc[M], goal = S.u * L; + let lo = 0, hi = M; + while (hi - lo > 1) { + const mid = (lo + hi) >> 1; + if (S.arc[mid] <= goal) lo = mid; + else hi = mid; + } + const f = (goal - S.arc[lo]) / (S.arc[lo + 1] - S.arc[lo] || 1); + at(S, (TAU * (lo + f)) / M, o); + S.osc = { x: o[0], y: o[1], cx: o[2], cy: o[3], kap: o[4] }; + const st = S.stat; + st.frames++; + st.hist[vert.length] = (st.hist[vert.length] || 0) + 1; + st.minCount = min(st.minCount, vert.length); + if (concave) st.concave++; + S.first = false; +} + +function angDist(a, b) { + const d = abs(a - b) % TAU; + return d > PI ? TAU - d : d; +} + +// Drawing: lines are laid as points every third of an LED, each lighting +// its four nearest LEDs by how close it falls, and an LED keeps the +// brightest colour laid on it. +function plot(S, x, y, c, a) { + const fx = x - 0.5, fy = y - 0.5, ix = floor(fx), iy = floor(fy), tx = fx - ix, ty = fy - iy, rgb = S.rgb; + for (let dy = 0; dy < 2; dy++) { + const yy = iy + dy; + if (yy < 0 || yy >= H) continue; + const wy = dy ? ty : 1 - ty; + for (let dx = 0; dx < 2; dx++) { + const xx = ix + dx; + if (xx < 0 || xx >= W) continue; + const w = min(1, (dx ? tx : 1 - tx) * wy * 1.7) * a, p = (yy * W + xx) * 3; + if (rgb[p] < c[0] * w) rgb[p] = c[0] * w; + if (rgb[p + 1] < c[1] * w) rgb[p + 1] = c[1] * w; + if (rgb[p + 2] < c[2] * w) rgb[p + 2] = c[2] * w; + } + } +} +// A segment, cut to the panel first: a radius of curvature can run +// thousands of LEDs off it. +function line(S, xa, ya, xb, yb, c, a, gap) { + const dx = xb - xa, dy = yb - ya; + let t0 = 0, t1 = 1; + for (const [p, q] of [[-dx, xa + 1], [dx, W + 1 - xa], [-dy, ya + 1], [dy, H + 1 - ya]]) { + if (p === 0) { + if (q < 0) return; + } else if (p < 0) t0 = max(t0, q / p); + else t1 = min(t1, q / p); + } + if (t0 > t1) return; + const len = Math.hypot(dx, dy), n = Math.ceil(len * (t1 - t0) * 3) || 1; + for (let e = 0; e <= n; e++) { + const u = t0 + ((t1 - t0) * e) / n; + if (gap && floor((u * len) / gap) % 2) continue; + plot(S, xa + dx * u, ya + dy * u, c, a); + } +} +function dot(S, x, y, c, a, rad) { + for (let yy = floor(y - rad); yy <= floor(y + rad); yy++) + for (let xx = floor(x - rad); xx <= floor(x + rad); xx++) { + if (xx < 0 || xx >= W || yy < 0 || yy >= H) continue; + const dd = Math.hypot(xx + 0.5 - x, yy + 0.5 - y), w = cl(rad + 0.5 - dd, 0, 1) * a, p = (yy * W + xx) * 3; + if (w <= 0) continue; + S.rgb[p] = max(S.rgb[p], c[0] * w); + S.rgb[p + 1] = max(S.rgb[p + 1], c[1] * w); + S.rgb[p + 2] = max(S.rgb[p + 2], c[2] * w); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { rgb, col, sc, ox, oy, x: X, y: Y, ex: EX, ey: EY, k: K } = S; + const px = (u) => ox + sc * u, py = (v) => oy - sc * v; + const h0 = S.hue + 0.55; + rgb.fill(0); + // How many normals of the curve pass through each LED: the sign changes + // round the curve of g(phi) = (curve - LED) . tangent. Along a row, the + // sign at one sample flips at a single column, so each pair of + // neighbouring samples disagrees on one run of columns (or outside it), + // added into the row's tally as a difference. + const DX = S.dx, DY = S.dy, A = S.ga, B = S.gb, T = S.gx, run = S.run, cnt = S.cnt; + for (let i = 0; i < M; i++) A[i] = X[i] * DX[i] + Y[i] * DY[i]; + for (let yy = 0; yy < H; yy++) { + const my = (oy - yy - 0.5) / sc; + run.fill(0); + for (let i = 0; i < M; i++) { + B[i] = A[i] - my * DY[i]; + // The column (in LED units) where this sample's sign flips. + const tx = DX[i] || 1e-12; + T[i] = (B[i] / tx) * sc + ox - 0.5; + } + for (let i = 0; i < M; i++) { + const j = i + 1 < M ? i + 1 : 0; + let c1 = T[i], c2 = T[j]; + if (c1 > c2) (c1 = T[j]), (c2 = T[i]); + const lo = cl(Math.ceil(c1), 0, W), hi = cl(floor(c2) + 1, 0, W); + if ((DX[i] || 1e-12) * (DX[j] || 1e-12) > 0) { + if (hi > lo) run[lo]++, run[hi]--; + } else { + run[0]++, run[W]--; + if (hi > lo) run[lo]--, run[hi]++; + } + } + for (let xx = 0, c = 0, p = yy * W; xx < W; xx++, p++) { + c += run[xx]; + cnt[p] = c; + } + } + // The dim fill keeps off the orange and yellow part of the wheel, which + // reads brown and olive when dark: its hue runs from green round to red. + const fills = []; + for (let c = 0; c < 4; c++) { + const fh = h0 + 0.16 + 0.07 * c; + hsv(0.22 + 0.8 * (fh - floor(fh)), 0.75, 0.24 + 0.12 * c, col); + fills.push([col[0], col[1], col[2]]); + } + for (let p = 0, o = 0; p < N; p++, o += 3) { + const c = cnt[p]; + if (c <= 2) continue; + const f = fills[min(3, (c - 4) >> 1)]; + (rgb[o] = f[0]), (rgb[o + 1] = f[1]), (rgb[o + 2] = f[2]); + } + // Each sample's place between the flattest and the sharpest of the + // curve, on a log scale. + const lk0 = log(S.kmin), lk1 = log(S.kmax), Q = S.q; + for (let i = 0; i < M; i++) Q[i] = K[i] > 0 ? cl((log(K[i]) - lk0) / (lk1 - lk0 || 1), 0, 1) : 0; + // The evolute, pale and opposite the curve on the wheel, a little nearer + // the curve's own hue where its arc comes from a sharp part, and broken + // where it runs off towards an inflection. + for (let i = 0; i < M; i++) { + const j = (i + 1) % M; + if (K[i] > 0 !== K[j] > 0) continue; + const xa = px(EX[i]), ya = py(EY[i]), xb = px(EX[j]), yb = py(EY[j]); + if (abs(xb - xa) + abs(yb - ya) > 24) continue; + hsv(h0 + 0.5 + 0.12 * Q[i], 0.5, 1, col); + line(S, xa, ya, xb, yb, col, 0.9); + } + // Each vertex joined to its cusp by a dotted radius. + const join = [0.5, 0.5, 0.55]; + for (const v of S.vert) line(S, px(v.x), py(v.y), px(v.cx), py(v.cy), join, 0.45, 1); + // The osculating circle, in the colour of the curve where it touches. + const oc = S.osc; + if (oc && abs(oc.kap) > 1e-6) { + const R = sc / abs(oc.kap), cx = px(oc.cx), cy = py(oc.cy); + hsv(h0 + 0.35 * (oc.kap > 0 ? cl((log(oc.kap) - lk0) / (lk1 - lk0 || 1), 0, 1) : 0), 0.35, 0.8, col); + const n = Math.ceil(min(TAU, 300 / R) * R * 2.5); + const a0 = Math.atan2(py(oc.y) - cy, px(oc.x) - cx), span = min(PI, 150 / R); + for (let e = 0; e <= n; e++) { + const a = a0 - span + (2 * span * e) / n; + plot(S, cx + R * cos(a), cy + R * sin(a), col, 0.7); + } + } + // The curve, coloured by curvature: flat parts at the base hue, the + // sharpest a third of the wheel on. + for (let i = 0; i < M; i++) { + const j = (i + 1) % M; + hsv(h0 + 0.35 * Q[i], 0.85, 1, col); + line(S, px(X[i]), py(Y[i]), px(X[j]), py(Y[j]), col, 1); + } + // Vertices on the curve, their cusps on the evolute. + const cusp = [1, 0.92, 0.8]; + for (const v of S.vert) { + dot(S, px(v.x), py(v.y), [1, 1, 1], 1, 1.1); + dot(S, px(v.cx), py(v.cy), cusp, 1, 0.9); + } + if (oc) { + dot(S, px(oc.x), py(oc.y), [1, 1, 1], 1, 0.9); + dot(S, px(oc.cx), py(oc.cy), cusp, 1, 1); + } + const evo = [0.7, 0.7, 0.75]; + // A ring opens where a vertex was born or died: at its cusp, or at the + // vertex itself when the cusp is off the panel. + for (const f of S.flash) { + let fx = px(f.v.cx), fy = py(f.v.cy); + if (fx < 0 || fx >= W || fy < 0 || fy >= H) (fx = px(f.v.x)), (fy = py(f.v.y)); + const r = 1.5 + 6 * f.age, a = 1 - f.age / 0.8, n = 40; + for (let e = 0; e < n; e++) plot(S, fx + r * cos((TAU * e) / n), fy + r * sin((TAU * e) / n), f.born ? [1, 1, 1] : evo, a); + } + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p += 3) display.setPixel(x, y, (255 * cl(rgb[p], 0, 1)) | 0, (255 * cl(rgb[p + 1], 0, 1)) | 0, (255 * cl(rgb[p + 2], 0, 1)) | 0); +} From 60366f629a8acb05309760e0f9dcb7254631533c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 02:36:04 +0000 Subject: [PATCH 12/14] =?UTF-8?q?53=ED=8C=90:=20Barycentric=20Subdivision?= =?UTF-8?q?=20=ED=8C=A8=ED=84=B4=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Yb8D9UxrifXqrXju5CVbqY --- .../patterns/barycentric-subdivision.js | 433 ++++++++++++++++++ 1 file changed, 433 insertions(+) create mode 100644 incoming/pattern-study/patterns/barycentric-subdivision.js diff --git a/incoming/pattern-study/patterns/barycentric-subdivision.js b/incoming/pattern-study/patterns/barycentric-subdivision.js new file mode 100644 index 000000000..e5618b418 --- /dev/null +++ b/incoming/pattern-study/patterns/barycentric-subdivision.js @@ -0,0 +1,433 @@ +// ===== Patternflow pattern ===== +// Title: Barycentric Subdivision +// Family: fractal +// Idea: A triangle cut along its medians into six, and each of those again, fills the panel as a mosaic coloured by each piece's largest angle, and the camera dives child after child down one random line of descent, outlined in white three steps ahead and turning and scaling but never stretching, until the pieces are hot slivers and the walk starts again. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: iterated barycentric subdivision, whose triangles flatten (Imre Bárány, Alan F. Beardon and T. Keith Carne, 1996; Persi Diaconis and Laurent Miclo, 2011), pattern round 53 +// Round: 53 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Depth=2..3, Steer=0..1 +// +// Knob 1: Hue. Turns the whole angle scale round the wheel. At 0 a piece whose largest angle is 60 degrees is cyan, 90 azure, 120 blue, 150 violet, 170 magenta, 177 rose and 179 red-orange, and one within a third of a degree of flat is a pale amber. +// Knob 2: Speed. At 2 the camera takes 2.2 seconds to dive from one triangle into the next, slowing as it arrives; at 0.2 ten times slower, at 3 half as fast again. When the current triangle's largest angle passes 179 degrees the picture fades and starts again from an equilateral triangle; with random steps that happens after 25 dives in the median, under a minute at 2. +// Knob 3: Depth. How many levels below the current triangle the mosaic is cut. At 2 the current triangle shows as 36 pieces and the third level, 216, fades in as the camera closes on the next triangle, which then shows 36 of its own; at 3 it is 216 going to 1,296. A piece whose smallest height on the panel is under 3 LEDs is never cut, so the slivers stay whole and the finest level shows only inside the rounder pieces. +// Knob 4: Steer. The chance at each step that the camera takes the roundest of the six children instead of a random one, from 0 at 0.1 to 1 at the top. Up to 0.1 every step is random as in the theorem, and after 40 steps two descents in three have a largest angle over 179 degrees. At 0.3, two steps in nine steered, it is one in twelve. From 0.4, one step in three, the shapes hold: the median largest angle stays near 115 degrees and the colours stay cool. At 1 every step takes the roundest child and the largest angle stays between 68 and 90 degrees. +// Buttons: K1 tap starts again from an equilateral triangle. + +const W = 64, H = 128; +const { floor, min, max, sqrt, sin, cos, atan2, acos, abs, exp, log, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// A child is named by the order of the barycentric coordinates inside it: +// child c holds the points whose largest coordinate is CI[c] and second CJ[c]. +// Its corners are the vertex CI, the midpoint of the side CI-CJ and the +// centroid, in that order. With corners 0, 1 and z, child c has the shape +// 2(1+z)/3, 2(1+z)/3z, (4-2z)/3, 2(z-2)/3(z-1), 2(2z-1)/3z, 2(1-2z)/3(1-z): +// a fixed Moebius map of z, or of its mirror image for c = 1, 2 and 5. +const CI = [0, 0, 1, 1, 2, 2], CJ = [1, 2, 0, 2, 0, 1]; +// Triangles kept above the current one, for the mosaic round it. +const KEEP = 12; +// Seconds per level at Speed 1. +const LEVEL = 4.4; +// A line of descent ends when the current triangle's largest angle passes this. +const FLAT = 179; +// A triangle whose smallest height on the panel is under LOD LEDs is not cut +// further; up to twice that its children fade in. +const LOD = 3; +// Margin in LEDs when a triangle is framed. +const MG = 1; +// Angle scale: LUT entries from 60 to 180 degrees. +const NL = 512; + +function child(src, so, c, dst, d) { + const i = 2 * CI[c], j = 2 * CJ[c]; + const xi = src[so + i], yi = src[so + i + 1]; + const gx = (src[so] + src[so + 2] + src[so + 4]) / 3, gy = (src[so + 1] + src[so + 3] + src[so + 5]) / 3; + dst[d] = xi; + dst[d + 1] = yi; + dst[d + 2] = 0.5 * (xi + src[so + j]); + dst[d + 3] = 0.5 * (yi + src[so + j + 1]); + dst[d + 4] = gx; + dst[d + 5] = gy; +} + +// Where a triangle sits on the colour scale, 0 to 1: half is its largest +// angle from 60 to 180 degrees, half how far it is short of flat on a log +// scale from 30 degrees down to 0.3, so 170, 177 and 179.5 look different. +function heat(t, o) { + const a = bigAngle(t, o), d = 180 - a; + return cl(0.5 * ((a - 60) / 120) + 0.5 * cl((Math.log10(30) - Math.log10(d > 1e-9 ? d : 1e-9)) / 2, 0, 1), 0, 1); +} + +// Largest angle of a triangle, in degrees. +function bigAngle(t, o) { + const ax = t[o + 2] - t[o + 4], ay = t[o + 3] - t[o + 5]; + const bx = t[o + 4] - t[o], by = t[o + 5] - t[o + 1]; + const cx = t[o] - t[o + 2], cy = t[o + 1] - t[o + 3]; + let m = ax * ax + ay * ay, p = bx * bx + by * by, q = cx * cx + cy * cy, w; + if (p > m) (w = m), (m = p), (p = w); + if (q > m) (w = m), (m = q), (q = w); + return (acos(cl((p + q - m) / (2 * sqrt(p * q) + 1e-300), -1, 1)) * 180) / PI; +} + +// The similarity z -> a z + b that sets triangle t as large as it fits in the +// panel, preferring small turns from the present view. Writes [ar, ai, br, bi]. +function frame(t, o, out) { + let best = -1, bs = 1, bt = 0, lx = 0, ly0 = 0, ll = 0; + for (let v = 0; v < 6; v += 2) { + const w = (v + 2) % 6, dx = t[o + w] - t[o + v], dy = t[o + w + 1] - t[o + v + 1]; + if (dx * dx + dy * dy > ll) (ll = dx * dx + dy * dy), (lx = dx), (ly0 = dy); + } + const tryAt = (th) => { + if (th > PI) th -= 2 * PI; + if (th <= -PI) th += 2 * PI; + const c = cos(th), sn = sin(th); + let x0 = 1e300, x1 = -1e300, y0 = 1e300, y1 = -1e300; + for (let v = 0; v < 6; v += 2) { + const x = t[o + v], y = t[o + v + 1], rx = c * x - sn * y, ry = sn * x + c * y; + if (rx < x0) x0 = rx; + if (rx > x1) x1 = rx; + if (ry < y0) y0 = ry; + if (ry > y1) y1 = ry; + } + const sc = min((W - 2 * MG) / (x1 - x0 + 1e-300), (H - 2 * MG) / (y1 - y0 + 1e-300)); + // A long side stood upright scores a quarter more, so a sliver stands + // along the columns instead of the panel's diagonal. + const ly = sn * lx + c * ly0; + const score = sc * (1 - (0.3 * abs(th)) / PI) * (1 + 0.25 * (ly * ly / ll) ** 8); + if (score > best) (best = score), (bs = sc), (bt = th); + }; + for (let n = -48; n < 48; n++) tryAt((n * PI) / 48); + const t0 = bt; + for (let n = -16; n <= 16; n++) tryAt(t0 + (n * PI) / 768); + const ar = bs * cos(bt), ai = bs * sin(bt); + let x0 = 1e300, x1 = -1e300, y0 = 1e300, y1 = -1e300; + for (let v = 0; v < 6; v += 2) { + const x = t[o + v], y = t[o + v + 1], rx = ar * x - ai * y, ry = ai * x + ar * y; + if (rx < x0) x0 = rx; + if (rx > x1) x1 = rx; + if (ry < y0) y0 = ry; + if (ry > y1) y1 = ry; + } + out[0] = ar; + out[1] = ai; + out[2] = W / 2 - 0.5 * (x0 + x1); + out[3] = H / 2 - 0.5 * (y0 + y1); +} + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// The colour scale: cyan for round pieces through blue, violet, magenta and +// rose to a pale amber for flat ones. +function palette(s) { + for (let n = 0; n < NL; n++) { + const t = n / (NL - 1); + hsv(s.hue + 0.48 + 0.64 * t, 0.9 - 0.35 * t ** 6, 0.62 + 0.38 * t, s.lut, n * 3); + } + s.lutHue = s.hue; +} + +export function setup(params) { + const s = { + hue: 0, depth: 2, steer: 0, + // Stored triangles in world coordinates: 0 is the current one, j its j-th + // ancestor; path[j] is the child taken from j + 1 to j. + tri: new Float64Array((KEEP + 1) * 6), path: new Int8Array(KEEP + 1), nA: 1, + // The planned line of descent: plan[0] leads from the current triangle to + // the next, plan[1] and plan[2] on from there. + next: 0, plan: new Int8Array(3), dz: new Float64Array(4), u: 0, level: 0, ang: 60, + ow: new Float32Array(W * H), ot: new Float64Array(24), + gain: 1, fading: 0, lut: new Float64Array(NL * 3), lutHue: -1, tmp: new Float64Array(36), + // Drawing: one triangle per depth of the walk, the path as seen from the + // oldest kept triangle, and the frame. + P: new Float64Array(40 * 6), pc: new Int8Array(KEEP + 2), A: 0, tgt: 0, w: 0, spIn: 1, spOut: 1, + fb: new Float32Array(W * H * 3), mark: new Uint8Array(W * H), + }; + params.s = s; + // The first update reads the knobs and then plans the first steps. + s.fresh = true; +} + +function restart(s) { + // An equilateral triangle, turned at random, then framed. + const th = Math.random() * 2 * PI, t = s.tri; + for (let v = 0; v < 3; v++) { + t[2 * v] = cos(th + (v * 2 * PI) / 3); + t[2 * v + 1] = sin(th + (v * 2 * PI) / 3); + } + frame(t, 0, s.dz); + apply(t, 0, s.dz); + s.nA = 1; + s.u = 0; + s.level = 0; + s.ang = 60; + const k = s.tmp; + for (let v = 0; v < 6; v++) k[v] = t[v]; + for (let m = 0; m < 3; m++) { + s.plan[m] = pick(s, k, m * 6); + child(k, m * 6, s.plan[m], k, m * 6 + 6); + } + aim(s); +} + +function apply(t, o, z) { + for (let v = o; v < o + 6; v += 2) { + const x = t[v], y = t[v + 1]; + t[v] = z[0] * x - z[1] * y + z[2]; + t[v + 1] = z[1] * x + z[0] * y + z[3]; + } +} + +// Picks a child of triangle t: at random, or with chance Steer the roundest +// one; between children of the same shape, the one the camera reaches with +// the smallest turn. +function pick(s, t, o) { + let c = floor(Math.random() * 6) % 6; + if (Math.random() < s.steer) { + const k = s.ot, z = [0, 0, 0, 0]; + let bv = 1e9, bt = 1e9; + for (let n = 0; n < 6; n++) { + child(t, o, n, k, 0); + const a = bigAngle(k, 0); + frame(k, 0, z); + const turn = abs(atan2(z[1], z[0])); + if (a < bv - 1e-6 || (a < bv + 1e-6 && turn < bt)) (bv = min(bv, a)), (bt = turn), (c = n); + } + } + return c; +} + +// The similarity that frames the next triangle. +function aim(s) { + s.next = s.plan[0]; + child(s.tri, 0, s.next, s.tmp, 0); + frame(s.tmp, 0, s.dz); +} + +function advance(s) { + const t = s.tri, z = s.dz; + for (let j = 0; j < s.nA; j++) apply(t, j * 6, z); + const keep = min(s.nA, KEEP); + for (let j = keep; j > 0; j--) { + for (let v = 0; v < 6; v++) t[j * 6 + v] = t[(j - 1) * 6 + v]; + s.path[j] = s.path[j - 1]; + } + child(t, 6, s.next, t, 0); + s.path[0] = s.next; + s.nA = keep + 1; + s.level++; + s.ang = bigAngle(t, 0); + if (s.ang > FLAT && !s.fading) s.fading = 1; + const k = s.tmp, pl = s.plan; + pl[0] = pl[1]; + pl[1] = pl[2]; + child(t, 0, pl[0], k, 0); + child(k, 0, pl[1], k, 6); + pl[2] = pick(s, k, 6); + aim(s); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const s = params.s, k = input.knobValues || [0, 2, 2, 0.06], d = cl(dt || 0, 0, 0.1); + s.hue = cl(k[0], 0, 1); + const sp = cl(k[1], 0.2, 3); + s.depth = cl(k[2], 2, 3); + s.steer = cl((k[3] - 0.1) / 0.9, 0, 1); + if (s.fresh) { + s.fresh = false; + restart(s); + } + if (input.btnPressed && input.btnPressed[0] && !s.fading) s.fading = 2; + if (s.fading) { + // Fade out, start again, fade in. + s.gain -= d * (s.fading === 2 ? 4 : 1.5); + if (s.gain <= 0) { + s.gain = 0; + s.fading = 0; + restart(s); + } + } else s.gain = min(1, s.gain + d * 1.5); + s.u += (d * sp) / LEVEL; + while (s.u >= 1) { + s.u -= 1; + advance(s); + } +} + +// Fills one triangle's LEDs: the colour, darkened near the parent's sides +// (heights hc2 and hj) at weight wl and near the child's own inner sides +// (heights hc0, hc1) at weight wk. +function raster(s, X0, Y0, X1, Y1, X2, Y2, r, g, b, hc0, hc1, hc2, hj, wl, wk) { + const ex = X1 - X0, ey = Y1 - Y0, fx = X2 - X0, fy = Y2 - Y0, dd = ex * fy - fx * ey; + if (!(abs(dd) > 1e-12)) return; + const a1 = fy / dd, b1 = -fx / dd, c1 = (fx * Y0 - fy * X0) / dd; + const a2 = -ey / dd, b2 = ex / dd, c2 = (ey * X0 - ex * Y0) / dd; + const a0 = -a1 - a2, b0 = -b1 - b2, c0 = 1 - c1 - c2; + const ya = max(0, Math.ceil(min(Y0, Y1, Y2) - 0.5)), yb = min(H - 1, floor(max(Y0, Y1, Y2) - 0.5)); + const xa = max(0, Math.ceil(min(X0, X1, X2) - 0.5)), xb = min(W - 1, floor(max(X0, X1, X2) - 0.5)); + const { fb, mark } = s; + for (let y = ya; y <= yb; y++) { + const py = y + 0.5; + let lo = xa, hi = xb; + for (let e = 0; e < 3; e++) { + const a = e === 0 ? a0 : e === 1 ? a1 : a2, bt = e === 0 ? b0 * py + c0 : e === 1 ? b1 * py + c1 : b2 * py + c2; + if (a > 1e-12) { + const x = Math.ceil(-bt / a - 0.5); + if (x > lo) lo = x; + } else if (a < -1e-12) { + const x = floor(-bt / a - 0.5); + if (x < hi) hi = x; + } else if (bt < 0) hi = -1; + } + for (let x = lo; x <= hi; x++) { + const px = x + 0.5, q1 = a1 * px + b1 * py + c1, q2 = a2 * px + b2 * py + c2, q0 = 1 - q1 - q2; + let dl = q2 * hc2, dk = q0 * hc0; + const dj = (0.5 * q1 + q2 / 3) * hj, d1 = q1 * hc1; + if (dj < dl) dl = dj; + if (d1 < dk) dk = d1; + const el = wl * (1 - cl((dl - 0.15) / 0.6, 0, 1)), ek = wk * (1 - cl((dk - 0.15) / 0.6, 0, 1)); + const f = 1 - 0.8 * (el > ek ? el : ek), i = y * W + x; + fb[i * 3] = r * f; + fb[i * 3 + 1] = g * f; + fb[i * 3 + 2] = b * f; + mark[i] = 1; + } + } +} + +// Draws the six children of the triangle at depth d, each in a mix of its +// parent's colour and its own at weight w, with its inner sides at weight w. +function emit(s, d, w, on, inT, A2, l0, l1, l2) { + const P = s.P, o = d * 6, q = o + 6, lut = s.lut; + let id = floor(heat(P, o) * (NL - 1)) * 3; + const nr = lut[id], ng = lut[id + 1], nb = lut[id + 2]; + const s0 = sqrt(l0), s1 = sqrt(l1), s2 = sqrt(l2); + const wl = cl((A2 / (s0 + s1 + s2) - 0.3) / 0.9, 0, 1), ca = A2 / 6; + for (let c = 0; c < 6; c++) { + child(P, o, c, P, q); + let r = nr, g = ng, b = nb; + if (w > 0) { + id = floor(heat(P, q) * (NL - 1)) * 3; + r += (lut[id] - nr) * w; + g += (lut[id + 1] - ng) * w; + b += (lut[id + 2] - nb) * w; + } + const X0 = P[q], Y0 = P[q + 1], X1 = P[q + 2], Y1 = P[q + 3], X2 = P[q + 4], Y2 = P[q + 5]; + const k0 = Math.hypot(X1 - X2, Y1 - Y2), k1 = Math.hypot(X2 - X0, Y2 - Y0), k2 = Math.hypot(X0 - X1, Y0 - Y1); + const wk = w * cl((ca / (k0 + k1 + k2) - 0.3) / 0.9, 0, 1); + const sp = inT || (on && d === s.A && c === s.next) ? s.spIn : s.spOut; + const j = CJ[c], hj = A2 / (j === 0 ? s0 : j === 1 ? s1 : s2); + raster(s, X0, Y0, X1, Y1, X2, Y2, r * sp, g * sp, b * sp, ca / k0, ca / k1, ca / k2, hj, wl, wk); + } +} + +// Walks the subdivision from the oldest kept triangle down to the mosaic's +// level, skipping triangles off the panel. +function walk(s, d, on, inT) { + const P = s.P, o = d * 6; + const x0 = P[o], y0 = P[o + 1], x1 = P[o + 2], y1 = P[o + 3], x2 = P[o + 4], y2 = P[o + 5]; + if (max(x0, x1, x2) < 0 || min(x0, x1, x2) > W || max(y0, y1, y2) < 0 || min(y0, y1, y2) > H) return; + const A2 = abs((x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0)); + const l0 = (x1 - x2) ** 2 + (y1 - y2) ** 2, l1 = (x2 - x0) ** 2 + (y2 - y0) ** 2, l2 = (x0 - x1) ** 2 + (y0 - y1) ** 2; + const open = (d < s.tgt ? 1 : s.w) * cl((A2 / sqrt(max(l0, l1, l2)) - LOD) / LOD, 0, 1); + if (open >= 1 && d < s.tgt) { + for (let c = 0; c < 6; c++) { + child(P, o, c, P, o + 6); + const oc = on && d <= s.A && c === s.pc[d]; + walk(s, d + 1, oc, inT || (oc && d === s.A)); + } + } else emit(s, d, open, on, inT, A2, l0, l1, l2); +} + +function outline(ow, t, o, al) { + for (let e = 0; e < 6; e += 2) { + const ax = t[o + e], ay = t[o + e + 1], bx = t[o + ((e + 2) % 6)], by = t[o + ((e + 3) % 6)]; + const n = Math.ceil(Math.hypot(bx - ax, by - ay) * 2.5) + 1; + for (let i = 0; i <= n; i++) { + const x = floor(ax + ((bx - ax) * i) / n), y = floor(ay + ((by - ay) * i) / n); + if (x >= 0 && x < W && y >= 0 && y < H && ow[y * W + x] < al) ow[y * W + x] = al; + } + } +} + +export function draw(display, params) { + const s = params.s; + if (!s || s.fresh) return; + if (s.lutHue !== s.hue) palette(s); + const { tri, dz, P, fb, mark } = s; + // The camera: the spiral similarity from this triangle's frame to the next + // one, raised to the eased progress v, about its fixed point. + const u = s.u, v = u - (0.7 * sin(2 * PI * u)) / (2 * PI); + const ar = dz[0], ai = dz[1], den = (1 - ar) * (1 - ar) + ai * ai; + const qr = (dz[2] * (1 - ar) - dz[3] * ai) / den, qi = (dz[3] * (1 - ar) + dz[2] * ai) / den; + const ls = log(Math.hypot(ar, ai)) * v, th = atan2(ai, ar) * v; + const mr = exp(ls) * cos(th), mi = exp(ls) * sin(th); + const A = s.nA - 1; + for (let c = 0; c < 6; c += 2) { + const x = tri[A * 6 + c] - qr, y = tri[A * 6 + c + 1] - qi; + P[c] = qr + mr * x - mi * y; + P[c + 1] = qi + mi * x + mr * y; + } + for (let d = 0; d < A; d++) s.pc[d] = s.path[A - 1 - d]; + s.pc[A] = s.next; + s.A = A; + // The mosaic's depth below the current triangle runs on with the dive, so + // the next level fades in as the camera closes in. + const e = s.depth + v, L = floor(e); + s.tgt = A + L; + s.w = e - L; + const { ow, ot, plan } = s; + for (let c = 0; c < 6; c += 2) { + const x = tri[c] - qr, y = tri[c + 1] - qi; + ot[c] = qr + mr * x - mi * y; + ot[c + 1] = qi + mi * x + mr * y; + } + // Mid-dive the next triangle is lit and the rest dimmed, while the current + // triangle is wide enough on the panel to be cut. + const l0 = (ot[2] - ot[4]) ** 2 + (ot[3] - ot[5]) ** 2, l1 = (ot[4] - ot[0]) ** 2 + (ot[5] - ot[1]) ** 2, l2 = (ot[0] - ot[2]) ** 2 + (ot[1] - ot[3]) ** 2; + const tall = abs((ot[2] - ot[0]) * (ot[5] - ot[1]) - (ot[4] - ot[0]) * (ot[3] - ot[1])) / sqrt(max(l0, l1, l2)); + const g = tall > 2 * LOD ? sin(PI * v) ** 2 : 0; + s.spIn = 1 + 0.15 * g; + s.spOut = 1 - 0.3 * g; + fb.fill(0); + mark.fill(0); + walk(s, 0, true, false); + // The planned line of descent in white: the next triangle and the two inside + // it, each outline coming up as the one before it is reached. + ow.fill(0); + for (let m = 0; m < 3; m++) { + child(ot, m * 6, plan[m], ot, m * 6 + 6); + const x = m + 1 - v, al = x < 1 ? 0.5 * x : x < 2 ? 0.5 - 0.2 * (x - 1) : 0.3 * (3 - x); + if (al > 0.01) outline(ow, ot, m * 6 + 6, al); + } + // An LED whose centre fell exactly on a shared side takes its neighbour's colour. + const ex = P[2] - P[0], ey = P[3] - P[1], fx = P[4] - P[0], fy = P[5] - P[1], dd = ex * fy - fx * ey; + const gain = s.gain * 255; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let i = y * W + x; + if (!mark[i]) { + const px = x + 0.5 - P[0], py = y + 0.5 - P[1]; + const b1 = (px * fy - py * fx) / dd, b2 = (ex * py - ey * px) / dd; + if (b1 < 0 || b2 < 0 || b1 + b2 > 1) { + display.setPixel(x, y, 0, 0, 0); + continue; + } + if (x > 0 && mark[i - 1]) i--; + else if (y > 0 && mark[i - W]) i -= W; + else if (x < W - 1 && mark[i + 1]) i++; + } + const w = ow[y * W + x], f = 1 - w; + display.setPixel(x, y, (fb[i * 3] * f + w) * gain, (fb[i * 3 + 1] * f + w) * gain, (fb[i * 3 + 2] * f + w) * gain); + } +} From b96776d10d4fc93c8425851e5982a42b7086217d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 02:41:32 +0000 Subject: [PATCH 13/14] =?UTF-8?q?53=ED=8C=90:=20README=20=EC=97=90=20?= =?UTF-8?q?=EC=97=B4=20=EA=B0=9C=EC=9D=98=20=ED=91=9C=EC=99=80=20=EC=97=B0?= =?UTF-8?q?=EA=B5=AC=20=EB=85=B8=ED=8A=B8=EB=A5=BC=20=EB=8D=94=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Yb8D9UxrifXqrXju5CVbqY --- incoming/pattern-study/README.md | 79 ++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 0250d1c5b..95e2da37a 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 팹턮 연구 2026-10 -새 팹턮 428개륌 쉰여섯 갈래로 나눠 만듀었얎요. 1판에서 엎섞 갈래로 23개륌 만듀고, 2판에서 1판에 없던 아홉 갈래로 16개륌, 3판에서 하나륌, 4판에서 새 갈래읞 직묌로 하나륌 더했얎요. 5판은 새 팹턮 없읎 1판의 Euclid Pulse 륌 닀시 ì§°ê³ , 6판은 섞욎 팚널의 128쀄을 위아래로 ë‹€ 쓰는 팹턮 하나륌 더했얎요. 7판은 또렷한 몚양읎 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 8판은 새 갈래읞 Ʞ계로 하나륌 더했얎요. 9판은 새 팹턮 없읎 1판의 Sprout 을 닀시 ì§°ì–Žìš”. 10판은 섞욎 팚널을 떚얎지는 랔록의 우묌로 쓰는 팹턮 하나륌 더했얎요. 11판은 여러 색읎 ꜀ 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 12판은 새 갈래읞 천첎로 하나륌 더했얎요. 13판은 새 팹턮 없읎 1판의 Glyph Rain 을 닀시 ì§°ì–Žìš”. 14판은 절찚가 바뀌Ʞ 전에 띄욎 바퀎띌서, 섞욎 팚널의 높읎륌 ë‹€ 떚얎지는 폭포 하나륌 더했얎요. 15판은 바뀐 절찚의 첫 판읎띌 ì—Ž 갈래에 새 팚턎을 하나씩, 몚두 ì—Ž 개륌 더했얎요. 도시와 음악 갈래는 귞때 새로 엎었얎요. 16판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 바닀와 곀충 갈래륌 새로 엎었얎요. 17판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 전자 회로와 장난감 갈래륌 새로 엎었얎요. 18판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 유늬와 ꎑ묌·결정, 놀읎공원 갈래륌 새로 엎었얎요. 19판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 부엌곌 버섯·균사 갈래륌 새로 엎었얎요. 20판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 새와 화학·싀험 갈래륌 새로 엎었얎요. 21판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 종읎와 욎동·겜Ʞ 갈래륌 새로 엎었얎요. 22판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 몞곌 비행 갈래륌 새로 엎었얎요. 23판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 묎대·공연곌 짐승 갈래륌 새로 엎었얎요. 24판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 공사·걎섀곌 도자Ʞ 갈래륌 새로 엎었얎요. 25판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 농사·밭곌 화산 갈래륌 새로 엎었얎요. 26판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 얌음·극지와 믞생묌 갈래륌 새로 엎었얎요. 27판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 철도와 필늄·영화 갈래륌 새로 엎었얎요. 28판은 죌제 표가 수학 쪜윌로 바뀐 첫 판읎띌 읎늄 있는 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했고, 수론 갈래륌 새로 엎었얎요. 29판은 죌제 표가 수학 여섯곌 화장품 팝업 넷윌로 나뉜 첫 판읎띌, 읎늄 있는 수학 대상 여섯곌 화장품 맀장 벜에 틀 질감 넷을 더했고, 위상·맀듭곌 화장품 팝업 갈래륌 새로 엎었얎요. 30판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 맀장의 LED 벜에 틀 질감 넷을 화장품 팝업 갈래에 더했얎요. 31판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 ì—Žì§€ 않았얎요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜의 질감 넷을 화장품 팝업 갈래에 더했얎요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 41판은 죌제 표가 수학·추상 닀섯곌 할로윈 닀섯윌로 바뀐 첫 판읎띌, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고 할로윈 갈래륌 새로 ì—Žì–Ž 10월 31음에 틀 팹턮 닀섯을 넣었얎요. 42판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 41판읎 연 할로윈 갈래에 더했얎요. 43판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 44판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 45판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 ì—Ž 개 아래읞 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 46판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 47판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 48판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 49판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 50판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 적은 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 51판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 52판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 적은 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. [index.html](index.html) 을 람띌우저로 엎멎 전부 한 장에서 돌아가요. ë§š 위 지도에서 점을 누륎멎 ê·ž 팚턎윌로 가고, 칎드의 슬띌읎더가 녾뾌 넷읎에요. 마음에 드는 것은 「윔드 복사」로 가젞가 Pattern Lab 의 CODE 칞에 붙여 넣윌멎 같은 귞늌읎 나와요. 페읎지는 파음 하나띌서 바깥에서 받아 였는 것읎 없얎요. +새 팹턮 438개륌 쉰여섯 갈래로 나눠 만듀었얎요. 1판에서 엎섞 갈래로 23개륌 만듀고, 2판에서 1판에 없던 아홉 갈래로 16개륌, 3판에서 하나륌, 4판에서 새 갈래읞 직묌로 하나륌 더했얎요. 5판은 새 팹턮 없읎 1판의 Euclid Pulse 륌 닀시 ì§°ê³ , 6판은 섞욎 팚널의 128쀄을 위아래로 ë‹€ 쓰는 팹턮 하나륌 더했얎요. 7판은 또렷한 몚양읎 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 8판은 새 갈래읞 Ʞ계로 하나륌 더했얎요. 9판은 새 팹턮 없읎 1판의 Sprout 을 닀시 ì§°ì–Žìš”. 10판은 섞욎 팚널을 떚얎지는 랔록의 우묌로 쓰는 팹턮 하나륌 더했얎요. 11판은 여러 색읎 ꜀ 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 12판은 새 갈래읞 천첎로 하나륌 더했얎요. 13판은 새 팹턮 없읎 1판의 Glyph Rain 을 닀시 ì§°ì–Žìš”. 14판은 절찚가 바뀌Ʞ 전에 띄욎 바퀎띌서, 섞욎 팚널의 높읎륌 ë‹€ 떚얎지는 폭포 하나륌 더했얎요. 15판은 바뀐 절찚의 첫 판읎띌 ì—Ž 갈래에 새 팚턎을 하나씩, 몚두 ì—Ž 개륌 더했얎요. 도시와 음악 갈래는 귞때 새로 엎었얎요. 16판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 바닀와 곀충 갈래륌 새로 엎었얎요. 17판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 전자 회로와 장난감 갈래륌 새로 엎었얎요. 18판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 유늬와 ꎑ묌·결정, 놀읎공원 갈래륌 새로 엎었얎요. 19판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 부엌곌 버섯·균사 갈래륌 새로 엎었얎요. 20판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 새와 화학·싀험 갈래륌 새로 엎었얎요. 21판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 종읎와 욎동·겜Ʞ 갈래륌 새로 엎었얎요. 22판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 몞곌 비행 갈래륌 새로 엎었얎요. 23판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 묎대·공연곌 짐승 갈래륌 새로 엎었얎요. 24판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 공사·걎섀곌 도자Ʞ 갈래륌 새로 엎었얎요. 25판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 농사·밭곌 화산 갈래륌 새로 엎었얎요. 26판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 얌음·극지와 믞생묌 갈래륌 새로 엎었얎요. 27판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 철도와 필늄·영화 갈래륌 새로 엎었얎요. 28판은 죌제 표가 수학 쪜윌로 바뀐 첫 판읎띌 읎늄 있는 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했고, 수론 갈래륌 새로 엎었얎요. 29판은 죌제 표가 수학 여섯곌 화장품 팝업 넷윌로 나뉜 첫 판읎띌, 읎늄 있는 수학 대상 여섯곌 화장품 맀장 벜에 틀 질감 넷을 더했고, 위상·맀듭곌 화장품 팝업 갈래륌 새로 엎었얎요. 30판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 맀장의 LED 벜에 틀 질감 넷을 화장품 팝업 갈래에 더했얎요. 31판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 ì—Žì§€ 않았얎요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜의 질감 넷을 화장품 팝업 갈래에 더했얎요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 41판은 죌제 표가 수학·추상 닀섯곌 할로윈 닀섯윌로 바뀐 첫 판읎띌, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고 할로윈 갈래륌 새로 ì—Žì–Ž 10월 31음에 틀 팹턮 닀섯을 넣었얎요. 42판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 41판읎 연 할로윈 갈래에 더했얎요. 43판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 44판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 45판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 ì—Ž 개 아래읞 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 46판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 47판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 48판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 49판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 50판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 적은 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 51판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 52판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 적은 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 53판은 할로윈 몫읎 쉬게 된 첫 판읎띌, 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. [index.html](index.html) 을 람띌우저로 엎멎 전부 한 장에서 돌아가요. ë§š 위 지도에서 점을 누륎멎 ê·ž 팚턎윌로 가고, 칎드의 슬띌읎더가 녾뾌 넷읎에요. 마음에 드는 것은 「윔드 복사」로 가젞가 Pattern Lab 의 CODE 칞에 붙여 넣윌멎 같은 귞늌읎 나와요. 페읎지는 파음 하나띌서 바깥에서 받아 였는 것읎 없얎요. 팚널은 섞로로 서요. 64 LED 너비에 128 LED 높읎가 Ʞ볞읎띌서 몚든 팚턎의 뚞늬에 `@matrix 64x128` 읎 있얎요. 레포의 팹널 섀정은 귞대로예요. `shared/constants.json` 의 `PANEL_GEOM_DEFAULT` 는 `128x64_SINGLE` 읎고, 팚턎읎 가로섞로륌 바Ꟍ 판을 적윌멎 펌웚얎의 `firmware/patternflow/src/core_canvas.h` 가 90도 돌렀 팚널을 ꜉ 채워요. 1판 23개는 처음에 가로 128×64 로 만듀었닀가 2판에서 섞로로 닀시 ì§°ì–Žìš”. @@ -690,6 +690,21 @@ | [Lost Head](patterns/lost-head.js) | 할로윈 | 달 뜬 묘지에서 목 없는 핎곚읎 묘비 위 뚞늬륌 하나씩 썚 볎며 너묎 크거나 작은 뚞늬, 몚자 쓎 뚞늬, 묎는 뚞늬와 혞박을 윌쓱하며 낎렀놓닀가, 제 뚞늬가 맞윌멎 눈구멍읎 독한 쎈록윌로 쌜지고 춀을 춰요. 귞러닀 뚞늬가 ë–šì–Žì ž 굎러가요. K3 은 틀며 뚞늬 수, K4 는 크Ʞ가 틀며 정도예요. | | [Creaking Door](patterns/creaking-door.js) | 할로윈 | 볎띌 복도 끝의 묞읎 닀섯 번 덜컹읎며 삐걱 엎늬멎 쎛불 빛읎 쐐Ʞ로 넓얎지며 바닥곌 왌쪜 벜을 타고, ê·ž 빛 속윌로 손가띜읎 ꞎ 손 귞늌자가 êž°ì–Ž 나였거나 런족 몚자의 귞늌자가 지나가요. 묞읎 쟅 닫히멎 판읎 흔듀늬고 뚌지가 떚얎지고, 묞 밑 빛쀄Ʞ륌 두 발읎 지나가요. K3 은 빛읎 닿는 거늬, K4 는 공Ʞ 속 뚌지읎고, K1 을 누륎멎 묞읎 닫히거나 엎렀요. | +### 53판 + +| 팹턮 | 갈래 | 한 쀄 | +| --- | --- | --- | +| [Gaussian Periods](patterns/gaussian-periods.js) | 수론 | k 륌 n 윌로 나눈 나뚞지마닀 LED 하나가 가우슀 죌Ʞ η(k) 자늬에 쌜젞서, 엎한 개의 돌며 덞토읎드가 k mod 11 로 묌든 점의 햇삎로 서요. 점마닀 η(uk) 로 믞끄러지멎 햇삎읎 흩얎졌닀가 같은 점 집합윌로 닀시 서고, 조각듀의 색만 서로 바뀌얎요. K3 은 (n, ω) 여덟 귞늌읎고, K4 는 곱하는 닚위 u 예요. 0 은 색 묎늬륌 섞고, 1 은 묎늬륌 제자늬에 두얎 조각마닀 저 혌자 닀시 서고, 2 는 조각을 통짞로 돌렀요. 닚추륌 누륎멎 닀음 귞늌윌로 넘얎가요. | +| [Catenoid-Helicoid](patterns/catenoid-helicoid.js) | 공간·원귌 | 첎컀판을 칠한 나선멎읎 축을 따띌 팚널을 섞로로 채욎 채 돌닀가, 늘얎나지 않고 현수멎윌로 풀렞닀가 반대쪜윌로 닀시 감Ʞ는데, 칞은 낮낮 같은 크Ʞ의 정사각형읎고 축의 ꞈ빛 선은 현수멎의 허늬 원읎 돌요. K3 은 한 바퀎의 칞 수(12~36), K4 는 낎렀닀볎는 각읎고, 닚추륌 누륎멎 휚읎 ê·ž 자늬에 멈춘 채 돌Ʞ만 핎요. | +| [Belt Trick](patterns/belt-trick.js) | 위상·맀듭 | 팹널 가욎데 정육멎첎가 위아래 가장자늬 막대에 늬볞윌로 묶읞 채 한 축윌로 쉬지 않고 돌아요. 한 바퀎륌 돌멎 늬볞읎 정육멎첎륌 한 번 감아 풀 수 없게 되고, 두 바퀎짞에 늬볞읎 정육멎첎륌 돌아 넘얎가며 곧게 풀렀요. 뒀의 고늬가 첫 바퀎엔 볎띌, 둘짞 바퀎엔 쎈록윌로 찚올띌 몇 번짞 바퀎읞지 볎여요. K3 은 늬볞 수(2~8), K4 는 회전축의 Ʞ욞Ʞ읎고, 닚추륌 누륎멎 도는 방향읎 거꟞로 바뀌얎요. | +| [Leapfrogging Vortices](patterns/leapfrogging-vortices.js) | 시뮬레읎션 | 두 ì—°êž° 고늬륌 축을 지나는 닚멎윌로 뎐서, 고늬마닀 반대로 도는 엌료 나선 한 쌍읎 볎여요. ë’€ 고늬가 좁아지며 앞 고늬 사읎로 빠륎게 빠젞나가 앞서고, 앞 고늬는 넓얎지며 느렀젞 뒀로 처지Ʞ륌 되풀읎하며 위로 올띌가요. K3 은 작은 고늬가 지나는 순간의 두 고늬 너비 비로, 러람의 묞턱 5.83 을 넘윌멎 한 번 빠젞나간 고늬가 돌아였지 않아요. K4 는 고늬에 싀늰 엌료의 양읎고, 닚추륌 누륎멎 두 고늬에 새 엌료륌 입혀요. | +| [Squared Rectangle](patterns/squared-rectangle.js) | 볎로녞읎·셀 | 저항 귞묌의 전선마닀 전압 강하륌 높읎로, 전류륌 너비로 삌은 타음읎 섞로 직사각형 하나륌 빈틈없읎 메욎 채 전도도륌 따띌 늘고 쀄며 믞끄러지닀가, 몇 쎈마닀 전도도가 몚두 1로 돌아와 정사각형듀로 딱 맞아떚얎지고 읎음맀가 하얗게 번쩍여요. K3 은 타음 수(13~20), K4 는 전도도가 1에서 벗얎나는 폭읎고, 닚추륌 누륎멎 바로 정사각형윌로 ë§žì•„ë–šì–Žì žìš”. | +| [Max-Flow Min-Cut](patterns/max-flow-min-cut.js) | 알고늬슘 | 아래 원천 막대에서 위 싱크 막대까지 놓읞 삌각 귞묌의 ꎀ을 펄슀가 가장 짧은 슝가 겜로륌 따띌 하나씩 올띌가며 청록 흐늄윌로 채우고, 가득 ì°¬ ꎀ은 ꞈ색읎 돌요. Ꞟ읎 ë‹€ 막히멎 원천에서 아직 닿는 마디가 웅덩읎처럌 찚였륎닀 듀쭉날쭉한 선에서 멈추고, ê·ž 선을 걎너는 ꎀ읎 깜빡읎며 싱크 막대가 ê·ž ꎀ듀의 용량윌로 ꌭ 맞게 나뉘얎요. K3 은 귞묌의 너비(7~13 마디), K4 는 ꎀ마닀 제멋대로읞 용량의 몫읎고, 닚추륌 누륎멎 새 용량윌로 닀시 흘렀요. | +| [Costas Array](patterns/costas-array.js) | 늬듬·펄슀 | 위 절반에서 윔슀타슀 배엎읎 죌파수 도앜 늬듬윌로 욞늬는 동안 장밋빛 사볞읎 시간곌 높읎로 한 칞씩 나선을 귞늬며 밀렀 점에 닿을 때마닀 번쩍읎고, 아래 절반은 밀며 칞마닀 맞은 점의 수륌 채워 0 곌 1 의 바닥 가욎데 N 하나만 솟은 압정읎 돌요. K3 은 배엎을 짓는 유한첎(원소 11~32개), K4 는 배엎을 윔슀타슀가 아닌 순엎로 바꿔 지도에 2 읎상의 능선을 킀우는 것읎고, 닚추륌 누륎멎 닀음 배엎로 넘얎가요. | +| [Barycentric Subdivision](patterns/barycentric-subdivision.js) | 프랙탈 | 정삌각형을 섞 쀑선윌로 여섯 조각 낎고 조각마닀 또 귞렇게 자륞 몚자읎크 속윌로, 칎메띌가 묎작위로 고륞 자식 삌각형을 따띌 늘읎지 않고 돌늬고 킀우Ʞ만 하며 파고듀얎요. 조각은 가장 큰 각윌로 칠핎젞 둥귌 것은 청록, 납작할수록 자홍을 지나 혞박색읎 되고, 흰 선읎 앞윌로 듀얎갈 섞 걞음을 믞늬 ê·žì–Žìš”. K3 은 몚자읎크륌 현재 삌각형 몇 당 아래까지 자륎는지읎고, K4 는 가장 둥귌 자식을 고륌 확률읎띌 0.4 부터는 삌각형읎 납작핎지지 않아요. 닚추륌 누륎멎 정삌각형에서 닀시 시작핎요. | +| [Four-Vertex Theorem](patterns/four-vertex.js) | 선·궀적 | 섞로로 선 맀끈한 닫힌 곡선읎 천천히 몚양을 바꟞멎, 곡률 쀑심읎 귞늬는 축폐선읎 ê·ž 안에서 ꞈ빛 별로 돌고, 별의 첚점은 곡률읎 가장 크거나 작은 흰 ꌭ짓점곌 점선윌로 읎얎젞 둘씩 태얎나고 사띌젞도 넷 아래로는 쀄지 않아요. K3 은 반지늄을 휘는 묌결의 수, K4 는 곡선읎 였목하게 팚읎는 정도읎고, 닚추륌 누륎멎 닀음 묌결읎 잠깐 부풀얎 제비ꌬ늬가 한꺌번에 ì—Žë žë‹€ 닫혀요. | +| [Euler's Elastica](patterns/elastica.js) | 묌늬·슀프링 | 양 끝읎 핀에 걞늰 120 LED 짜늬 가는 막대가 천천히 눌렞닀 풀늬며 였음러의 탄성 곡선 가족을 ë‹€ 지나가요. 삎짝 휜 활에서 아치와 직각 엘띌슀티칎륌 지나, 두 끝읎 만나는 묌방욞곌 끝읎 서로 지나치는 고늬까지 가요. 위의 진자는 막대륌 달늬는 구슬 자늬의 막대와 늘 나란히 흔듀늬고, 아래에서는 ê·ž 진자의 궀도가 상평멎을 돌아요. K3 은 반파 수 1~4 읎고, K4 는 풀 때 얎디까지 돌아였는지예요. 닚추륌 누륎멎 하쀑읎 멈춰 한 몚양읎 서 있얎요. | + ## 1판 갈래마닀 녾뾌 넷의 자늬는 갈래가 달띌도 거의 같아요. K1 은 색읎고 K2 는 빠륎Ʞ예요. K3 은 몚양을 바꟞고, K4 는 ê·ž 갈래만의 손잡읎예요. 귞래서 처음 볎는 팹턮도 K1 곌 K2 는 손읎 뚌저 알아요. @@ -2676,6 +2691,50 @@ E8 의 ê·Œ 240개륌 고셋의 4₂₁ 닀멎첎 ꌭ짓점윌로 ê·žë žì–Žìš”. 죌제 6 윌로 얎두욎 복도 끝에서 삐걱 엎늬는 묞을 ê·žë žì–Žìš”. 팚널의 LED 8,192개마닀 눈에서 ꎑ선을 쏘아 너비 1.2 m, 높읎 2.4 m 읞 복도와 3 m 앞 벜의 묞을 ì°Ÿê³ , 빛을 받는 점마닀 쎛불까지 귞은 선읎 묞틀 안을 지나는지, 돌아간 묞짝에 막히는지 ë”°ì žìš”. 묞은 0.9×2.0 m 띌 30×66 LED 로 서요. 쎛불은 묞 가욎데볎닀 0.75 m 였륞쪜, 묞틀에서 1.4 m 안쪜 몚퉁읎에 있얎 눈에는 볎읎지 않아요. 묞읎 겜첩을 축윌로 ë°© 안쪜윌로 닀섯 번 덜컹읎며 54~69도까지 엎늬는 동안, 빛은 였륞쪜 묞섀죌에서 가는 쀄로 나와 왌쪜 아래로 넓얎지는 쐐Ʞ가 되고 왌쪜 벜을 타고 올띌가요. 처음 값에서 따뜻한 빛을 받은 LED 는 0.04쎈에 274개, 2쎈에 1,930개, 4쎈에 4,567개읎고, 묞읎 닫히멎 묞 밑 틈의 137개만 낚아요. 빛 속윌로 손가띜읎 ꞎ 손의 귞늌자가 묞턱에서 눈 쪜윌로 êž°ì–Ž 나와 손가띜을 였므렞닀 묌러나고, 닀음 판에는 런족 몚자륌 쓎 누군가의 귞늌자가 걞얎서 쐐Ʞ륌 가로질러요. 한 판의 6.2~7쎈에 묞읎 0.14쎈 만에 쟅 닫히멎 판읎 0.5쎈 동안 3 LED 까지 흔듀늬고 상읞방에서 뚌지 28개가 ë–šì–Žì žìš”. ì–Žë‘  속에서는 묞 밑 빛쀄Ʞ륌 두 발읎 였륞쪜에서 왌쪜윌로 지나가고, 잠시 ë’€ 엎쇠 구멍의 빛읎 0.5쎈 꺌젞요. 첫 판 뒀로는 ì—Ž 판에 셋쯀 묞읎 절반만 엎늬닀 음찍 닫혀요. 쎛불 빛은 0.9 m 에서 절반읎 되게 했얎요. 묞 앞 바닥읎 쎛불에서 1.6 m 넘게 ë–šì–Žì ž 있얎서 쐐Ʞ 끝의 판자는 짙은 빚강윌로 낚고, 쐐Ʞ 밖의 복도는 볎띌로 낚아요. 빛의 색은 앜할 때 짙은 빚강에서 셀 때 옅은 ꞈ빛윌로 였륎는 표띌 갈색윌로 가띌앉지 않아요. 빛읎 였륎는 만큌 볎띌 바탕을 두 배로 빌서 복숭앗빛도 나지 않아요. 첫 판은 안개 섞Ʞ가 0.05 + 0.55×Haze 띌 처음 값에서 복도 위쪜읎 ë‹€ 붉게 ë–Žê³ , 0.02 + 0.3×Haze² 로 낮추자 쐐Ʞ 밖읎 볎띌로 돌아왔얎요. 손 귞늌자는 처음에 쎛불읎 볎는 묞틀 평멎에 ê·žë žì–Žìš”. 쎛불읎 0.75 m 였륞쪜에 있얎 빛읎 눈 쪜윌로 1 m 올 때마닀 0.54 m 왌쪜윌로 Ʞ욞얎서, 손가띜읎 대각선 쀄묎늬로 늘얎졌얎요. 귞늌자륌 바닥 판자 위의 몚양윌로 귞늬고 1 m 에 0.3 m 만 Ʞ욞읎자 손윌로 읜혔얎요. 안개 속의 귞늌자도 ê·ž 바닥 몚양에서 셈핎서 바닥곌 얎Ꞌ나지 않아요. 사람 귞늌자는 몞통 위에 얎깚와 뚞늬륌 얹윌니 십자가나 엎쇠 구멍처럌 볎여서 런족 몚자륌 씌웠얎요. 쟅 닫힌 ë’€ 묞읎 0.05 rad 로 튀멎 ê·ž 틈의 빛읎 바닥에 점선윌로 찍혀서, 튐을 0.025 rad 로 쀄읎고 0.03 rad 아래로 엎늰 묞은 닫힌 것윌로 셌얎요. 묞 밑 빛은 처음에 0.1 m 만 퍌젞 발 귞늌자가 볎읎지 않아서 0.3 m 로 넓혔얎요. 처음 잰 값은 움직임 0.304, 복잡도 0.373, drift 0.210 읎었고, 흔듀늌을 0.2쎈 2.4 LED 에서 0.5쎈 3 LED 로 늘늬자 움직임읎 0.327 읎 됐얎요. 읎 팚널에서 새로 볞 것은 ꎑ선윌로 셈한 빛읎 쐐Ʞ 몚양을 저절로 만든닀는 것읎에요. 묞짝읎 50도쯀 엎늬멎 쐐Ʞ가 묞틀 너비륌 ë‹€ 채우는데, 바로 ê·ž 각도륌 넘윌멎서 묞짝 앞멎읎 쎛불을 받아 밝아지Ʞ 시작핎요. 둘 ë‹€ 쎛불읎 묞짝 평멎 너뚞륌 볎게 되는 순간읎띌서 같은 각도예요. 잰 값은 움직임 0.327, 복잡도 0.379, 채움 0.956, drift 0.212 읎고, `thirds` 는 97/97/92 예요. +## 53판 + +53판은 죌제 5 「수학 · 추상」 ì—Ž 개예요. 처음에는 ROUND.md 대로 수학 닀섯곌 할로윈 닀섯을 지었는데, 10월 26음에 찜작자가 할로윈 Ʞ념 팚턎은 읎제 ê·žë§Œ 만듀얎도 된닀고 핎서 ROUND.md 가 죌제 6 을 쉬게 바뀌었얎요. ë‹€ 지은 할로윈 닀섯은 넣지 않고 수학 닀섯을 더 지었얎요. 같읎 돈 52판(#263)의 PR 볞묞에서 ì—Ž 개와 지도 칞을 읜고 플핎서 곚랐고, 앞 바퀮 PR 듀에 따륌 Ꞁ은 없었얎요. 51판까지 든 418개에서 추상 갈래 가욎데 볎로녞읎·셀곌 시뮬레읎션읎 엎하나로 가장 적었얎요. 52판읎 녞읎슈·흐늄곌 입자, 데읎터 귞늌, 늬듬·펄슀, 묌결·간섭에 하나씩 넣얎서, 뚌저 지은 닀섯은 볎로녞읎·셀곌 시뮬레읎션, 수론, 공간·원귌, 위상·맀듭에 하나씩 듀얎갔얎요. 나쀑 닀섯은 귞닀음 적은 알고늬슘곌 프랙탈, 늬듬·펄슀에 하나씩, 선·궀적곌 묌늬·슀프링에 하나씩 듀얎갔얎요. ì—Ž 개 ë‹€ 「흔한 것」에도 「낯선 수학 예」에도 없얎요. Gaussian Periods 는 읎얎진 곡선을 귞늬지 않고 정수 k 의 값만 점윌로 찍얎서 슀플로귞래프가 아니에요. 처음에 떠올늰 수학 가욎데 둘은 읎믞 화장품 팝업 갈래에 있얎서 바꿚얎요. 37판의 Taffy Cream 읎 볎음랜드, 아레프, 슀템러의 막대 젓Ʞ륌, 31판의 Foam Lather 가 폰 녞읎만의 거품 법칙을 썌얎요. 죌제 1 은 움직임 × 쎘쎘핚 4,4 에 Gaussian Periods, 복잡도 × 채움 4,1 에 Leapfrogging Vortices 륌 섞웠고, 죌제 4 는 Catenoid-Helicoid 와 Max-Flow Min-Cut 읎에요. + +### Gaussian Periods + +죌제 5 와 1 로 가우슀 죌Ʞ륌 섞웠얎요. 법 n 곌 곱셈 위수가 d 읞 ω 에서 η(k) = Σ e^(2πi k ω^j / n) 읎고, 듀크와 가륎시아, 룚잠(2015)가 k 마닀 ê·ž 값을 복소평멎에 찍얎 덞토읎드가 겹친 레읎슀가 서는 것을 볎였얎요. 팚턎은 k 하나하나륌 LED 하나로 쌜고, 흐늌도 잔상도 두지 않아요. 읎얎진 곡선 t ↩ Σ e^(2πi t ω^j / n) 은 귞늬지 않아서 슀플로귞래프가 아니띌 점의 구늄읎에요. 값의 집합은 k 에 닚위 u 륌 곱핎도 귞대로띌서, 점마닀 η(uk) 로 옮겚 가며 귞늌읎 흩얎졌닀가 같은 집합윌로 닀시 서요. 여덟 귞늌곌 섞 닚위로 섞 번씩 닀시 섞우는 동안 점읎 η(uk) 에 4.4e-15 안윌로 닿았고, 처음곌 끝의 값 집합을 정렬핎 견죌멎 4.6e-15 안에서 같았얎요. 처음 귞늌은 7 × 1201 의 음곱 덞토읎드였얎요. 멈춘 판의 쎘쎘핚읎 0.248 읎었고, 점을 직선윌로 옮Ʞ멎 닀시 서는 동안 가욎데로 몰렀 0.17 까지 낎렀갔얎요. 점마닀 d 개의 닚위귌읎 저마닀 짧은 혞륌 따띌 새 자늬로 돌게 바꟞자 닀시 서는 동안에도 0.33~0.35 륌 지쌰얎요. 조각을 11 × 991 의 엎하나로 늘늬고(점 3,641개) 가로륌 1.2ë°° 넓혀 옆 가시 끝을 판 밖윌로 볎낎자 쎘쎘핚 0.324, 움직임 4.910 윌로 Defect Turbulence 와 Rule Scroll 뿐읎던 움직임 × 쎘쎘핚 4,4 에 ì„°ì–Žìš”. 둥귌 테두늬 안만 채우멎 몚서늬가 비얎서 0.3 을 넘지 못했얎요. 귞늌은 허수축을 가로로, 싀수축을 섞로로 각각 판에 맞게 늘렀서, 쌀레 η(−k) 가 좌우 거욞읎 되고 둥귌 귞늌은 킀가 폭의 두 배예요. 엎한 빛깔을 색상환에 귞냥 늘얎놓윌멎 색종읎 조각처럌 흩얎젞서, 가시 찚례대로 3 걞음씩 맀Ʞ자 테두늬가 색상환을 섞 번 도는 띠로 읜혔얎요. 녾뾌는 0.20, 0.26, 0.24, 0.21 읎에요. + +### Catenoid-Helicoid + +죌제 5 와 4 로 현수멎(였음러, 1744)읎 나선멎(뫌니에, 1776)윌로 늘얎나지 않고 휘는 것(부륎, 1862)을 섞웠얎요. x = cos Ξ sinh v sin u + sin Ξ cosh v cos u ꌎ의 연ꎀ족을 u 한 바퀮, |v| ≀ 1.15 격자 96 × 30 칞윌로 깊읎 버퍌에 귞늬고, LED 마닀 (u, v) 륌 볎간핎 첎컀와 법선을 셈핎요. Ξ 아홉 값곌 (u, v) 여섯 점에서 E, F, G 가 cosh²v, 0, cosh²v 와 8.9e-16 안윌로 같았고, 첎컀 칞 48개의 넓읎는 Ξ 여섯 값에서 상대 찚읎가 1.9e-16 읎었얎요. 2ì°š 찚분윌로 잰 평균 곡률은 8.3e-8 아래예요. 법선 (cos u/cosh v, sin u/cosh v, −tanh v) 은 Ξ 와 상ꎀ없얎서 멎읎 휘얎도 빛 셈읎 귞대로읎고, v = 0 의 곧은 축은 Ꞟ읎 2π 귞대로 현수멎의 허늬 원읎 돌요. 현수멎은 높읎 2V 에 너비 2 cosh V 띌 V = 1.2 에서도 높읎가 너비의 0.66 배륌 넘지 못핎서, 첫 판은 `thirds` 가 13/55/24 였얎요. 현수멎읎 생Ꞟ 때 시선을 24도 더 낎렀 입을 듀여닀볎게 하자 21/59/26 읎 됐고, 가장 í‚€ 큰 순간에 가는 끝읎 2 LED 쯀 판 밖윌로 나가도록 맞춀 높읎륌 62 에서 65 로 늘늬자 25/61/30 읎 됐얎요. 쀌은 Ʞ욞Ʞ마닀 한 번만 정하고 휘는 동안에는 바꟞지 않아서 칞 크Ʞ가 팹널 위에서도 귞대로예요. 얎두욎 칞을 같은 색의 얎두욎 판윌로 칠했더니 K1 0.5 에서 죌황 칞읎 갈색읎얎서, 밝은 칞의 0.4 배에 낚볎띌 바닥을 더했얎요. 녾뾌는 0.06, 0.21, 0.04, 0.16 읎에요. 나선멎의 곧은 축읎 현수멎의 허늬 원윌로 말늬는 것읎 ꞈ빛 1 LED 선 하나로 읜혀요. + +### Belt Trick + +죌제 5 로 디랙의 벚튞 묘Ʞ륌 섞웠얎요. 디랙읎 강의에서 볎읎고 M. H. A. 뉎뚌(1942)읎 「디랙의 끈 묞제」로 적은 것윌로, 닚위 사원수가 SO(3) 륌 두 겹윌로 덮는닀는 것을 볎여요. 정육멎첎가 한 축윌로 쉬지 않고 돌고, 위아래 막대에 묶읞 늬볞읎 360° 에서는 정육멎첎륌 한 번 감아 풀 수 없고 720° 에서는 곧게 풀렀요. 720° 회전은 닚위 사원수에서 고늬 q(t) = cos 2πt + sin 2πt · a 읎고, 읎 고늬륌 1 로 쀄읎는 혞몚토플 H(s, t) 륌 b = πs/2 로 (cos²b + sin²b cos 2πt) + sin b sin 2πt · a + cos b sin b (1 − cos 2πt) · v 로 셈했얎요. 2001 × 2001 격자에서 |H| 는 1 곌 4.4e-16 안읎고, 끝 조걎읎 ë‹€ 3.3e-16 안에서 맞았얎요. 360° 에서 H(s, œ) = cos πs + sin πs · v 띌 늬볞은 s 만큌 비틀늰 채 감겚요. 늬볞의 점마닀 정육멎첎 쀑심에서 잰 거늬로 s 륌 읜얎서, 정육멎첎륌 둘러싌 구멎읎 저마닀 한 덩읎로 돌아요. 귞래서 늬볞읎 서로나 정육멎첎륌 뚫고 지나가지 못핎요. 고늬가 64 폭에 듀얎가알 핎서 움직읎는 껍질은 반지늄 15~30.5 읎고 ê·ž 밖의 늬볞은 가만히 있얎요. `thirds` 는 18/33/18, 움직임 0.243, 녾뾌는 0.09, 0.06, 0.08, 0.05 예요. 첫 판은 껍질 안을 34 쀄로 나눮는데, 360° 에서 한 쀄읎 옆윌로 6 LED 넘게 밀렀 늬볞 폭볎닀 컀지자 앞멎읞 나선읎 군데군데 청록 뒷멎윌로 칠핎졌얎요. 110 쀄로 늘늬고 앞뒀륌 사각형 넓읎의 부혞로 정하자 한 색읎 됐얎요. 늬볞을 한 평멎에 나란히 둔 첫 판에서는 360° 묎렵 읎웃 늬볞읎 8개음 때 0.30 LED 까지 붙얎서, 읎웃끌늬 깊읎로 4 떚얎뜚늬고 폭을 간격의 절반윌로 쀄여 0.62 로 벌렞얎요. 옆윌로 선 늬볞은 LED 가욎데륌 비껎가 점선윌로 끊겚서 가장자늬륌 선윌로 한 번 더 귞었얎요. 축읎 우늬륌 향한 채 엎멎 360° 의 몚습읎 섞로축 둘레의 가는 ꌬ임읎띌 읜히지 않아서, 축을 섞욎 쪜에서 ì—Žì–Ž 늬볞읎 팹널 평멎에서 읎쀑 나선윌로 감Ʞ게 했얎요. + +### Leapfrogging Vortices + +죌제 5 와 1 로 헬늄홀잠(1858)의 ì—°êž° 고늬 개구늬뜀을 섞우고, Lava Lake 와 Moth Lamp 둘뿐읎던 복잡도 × 채움 4,1 칞을 겚눎얎요. 고늬마닀 축 양쪜에서 반대로 도는 점 소용돌읎 한 쌍읎띌 닚멎은 소용돌읎 넷읎에요. 귞뢰랔늬(1877)와 러람(1894)가 푌 욎동읎고, 고늬는 축 대칭읎띌 였륞쪜 둘만 RK4 로 풀고 왌쪜은 거욞읎에요. 두 고늬 너비 비가 3 − 2√2 = 0.17157 아래멎 앞 고늬가 빠젞나간 ë’€ 닀시 따띌잡히지 않아요. 셈윌로 찟은 묞턱도 0.17157 곌 0.1716 사읎였고, 0.1716 에서는 t = 38,545 에, 0.172 에서는 2,433 에 두 고늬가 닀시 나란핎졌얎요. 600쎈 동안 소용돌읎 넷의 에너지는 2.6e-12, 축 방향 충격량은 3.3e-14 만큌만 흔듀렞얎요. ê±°ìšž 없읎 넷을 따로 풀고 1e-9 만큌 밀멎 비 0.38 아래에서는 개구늬뜀읎 대칭을 깚서 거욞을 걞었얎요. 둥귌 고늬는 ê·ž 대칭을 ê¹° 수 없얎요. 엌료 알갱읎 음부는 1.3쎈마닀 윔얎륌 가로지륎는 선윌로 놓여 나선윌로 감Ʞ고, 나뚞지는 고늬의 묌방욞을 채웠닀가 ꌬ늬로 ë–šì–Žì ž 나가요. 화멎은 두 고늬의 가욎데륌 따띌가서 고늬는 52번 쀄에 뚞묌고 ꌬ늬가 아래로 흘러요. 첫 판은 엌료륌 윔얎 둘레 공윌로 놓아 복잡도 1.964, 채움 0.218 읎었얎요. 쌜진 LED 의 75% 가 25~63 의 옅은 빛읎띌 채움만 뚹고 바읎튞는 적었얎요. 묞턱을 넘는 엌료는 28 부터 0.45 제곱 곡선윌로 밝히고 채도륌 0.55 로 낮춰 섞 채널읎 ë‹€ 퍌지게 하자 2.251, 0.239 가 됐얎요. 묌에는 LED 마닀 ê·ž 묌읎 얎디서 왔는지 거꟞로 따띌가는 지도륌 두고, ê·ž 자늬의 반짝읎륌 채널마닀 24 아래로 귞렀요. 귞래서 반짝읎는 고늬 둘레에서 돌고 고늬 밖에서는 아래로 흘러가요. 마지막 값은 복잡도 2.270, 채움 0.249, 움직임 0.942 읎고 녾뾌는 0.04, 0.12, 0.13, 0.07 읎에요. K3 은 큰 고늬와 작은 고늬의 너비 비띌 3 에서 엎늬고, 러람의 묞턱 5.83 을 넘윌멎 한 번 빠젞나간 고늬가 돌아였지 않아요. + +### Squared Rectangle + +죌제 5 로 람룩슀, 슀믞슀, 슀톀, 텃(1940)읎 저항 귞묌로 지은 정사각형 분할을 섞웠얎요. 귞묌의 전선마닀 전압 강하륌 높읎로, 전류륌 너비로 삌은 타음을 마디 전위로 섞로 자늬에, 멎의 전위로 가로 자늬에 놓윌멎 킀륎히혞프 법칙 덕에 타음읎 직사각형 하나륌 빈틈없읎 메워요. 전도도가 몚두 1 읎멎 타음읎 ë‹€ 정사각형읎에요. 팚턎은 3 × 4 삌각 격자의 전선 29개 가욎데 음부로 귞묌을 짓고 ë§€ 프레임 킀륎히혞프 식을 풀얎요. 전도도가 저마닀 흔듀렀 타음읎 늘고 쀄며 믞끄러지닀가 몇 쎈마닀 몚두 1 로 돌아와 정사각형윌로 ë§žì•„ë–šì–Žì žìš”. 타음 수 13~20 에 Wander 셋, 120쎈씩 86,400 프레임을 돌렀 뎀얎요. 타음 넓읎의 합은 큰 직사각형곌 늘 3.7e-15 안에서 같았고, 겹친 곳은 없었고, 890 프레임을 쎘쎘히 찍얎 볎니 몚든 점읎 ꌭ 한 번씩 덮였얎요. ë§žì•„ë–šì–Žì§„ 13,224 프레임에서 타음의 너비와 높읎는 7.8e-16 안에서 같았얎요. ê·ž 사읎에 정사각형 분할 80 가지가 나왔고, 크Ʞ가 ë‹€ 닀륞 완전 분할도 넷읎었얎요. 타음 16 윌로 엎멎 1427 × 3167 의 16ì°š 완전 분할읎 서요. 귞묌은 믞늬 고륞 산책 여덟 쀄에서 와서 읎웃끌늬 전선 한두 개만 달띌요. 전류가 0 읞 전선읎 없고, 직사각형읎 너비의 1.75~2.35ë°° 높읎읎고, 가장 작은 정사각형읎 4.5 LED 읎상읞 것만 넣었얎요. 첫 판은 12타음에서 ì—Žë žê³  왌쪜에 같은 정사각형 닀섯읎 쀄 서서 격자처럌 밋밋핎서 K3 을 13~20 윌로 올렞얎요. 색은 처음에 타음 크Ʞ륌 정핎 둔 로귞 범위에 얹얎서 화멎읎 거의 ë‹€ 파랑읎었고 색읎 0.366 읎었얎요. 귞때 볎읎는 가장 작은 타음곌 가장 큰 타음 사읎로 닀시 펎자 청록에서 죌황까지 쓰고 0.417 읎 됐얎요. 전류 쀄묎늬는 7쀄에 2쀄을 0.72 로 얎둡게 하멎 쀄묎늬 천처럌 읜혀서, 9쀄에 1쀄을 0.8 로 옅혔얎요. ê·ž 계닚읎 40 당 아래로 낎렀가 가장자늬가 0.997 에서 0.533 읎 됐얎요. 녾뾌는 넷 ë‹€ 0.13 을 넘얎요. + +### Max-Flow Min-Cut + +죌제 5 와 4 로 최대 흐멄 최소 절닚 정늬륌 섞웠얎요. 레슀터 포드와 덞버튞 풀컀슚, 귞늬고 엘늬아슀와 파읞슀타읞, 섀넌읎 1956년에 따로 낾 정늬읎고, 흐늄은 디니잠(1970)와 에드뚌슈·칎프(1972)처럌 가장 짧은 슝가 겜로부터 ì°Ÿì•„ìš”. 팹널 아래 두 쀄읎 원천 막대, 위 두 쀄읎 싱크 막대예요. ê·ž 사읎 삌각 격자의 ꎀ은 용량 1~9 륌 굵Ʞ와 밝Ʞ로 볎여요. 겜로 하나가 펄슀 하나로 1쎈에 팚널을 올띌가고, 펄슀 뚞늬가 지난 ꎀ부터 흐늄을 받아요. Ꞟ읎 ë‹€ 막히멎 마지막 너비 우선 탐색읎 닿은 마디가 탐색 닚계 찚례로 웅덩읎처럌 찚올띌요. 웅덩읎의 테두늬는 쌍대 귞래프의 겜로읎고, ê·ž 테두늬륌 걎너는 ꎀ만 ꞈ색윌로 낚아 깜빡여요. 같은 때 싱크 막대가 절닚 ꎀ마닀 한 조각윌로 나뉘는데, 조각 너비가 용량을 흐멄 값윌로 나눈 몫읎띌 조각듀읎 막대륌 ꌭ 채워요. 팚턎의 풀읎로 2,700판을 돌렾더니 흐멄 값은 늘 잔여 귞묌에서 읜은 절닚의 용량곌 같았고, 따로 ì§  깊읎 우선 포드-풀컀슚도 같은 값을 냈얎요. 슝가 겜로의 Ꞟ읎는 한 번도 쀄지 않았얎요. 7 마디 너비에서 겜로는 평균 26.9개, 흐멄 34.0, 절닚 ꎀ 14.0개예요. 첫 판은 ꎀ마닀 따로 뜑은 용량읎 고륎게 5 얞저늬였얎요. 귞랬더니 300번 가욎데 27% 에서 절닚읎 원천 막대 바로 위에 붙얎서 웅덩읎가 띠 하나로 끝났고, ꎀ의 43% 가 가득 ì°š ꞈ색읎 판을 덮었얎요. ê·ž 가욎데 절닚은 12% 뿐읎었얎요. 용량에 맀끄러욎 지형곌 가욎데쯀을 가로지륎는 얇은 ꎀ의 묌결 띠륌 넣고 두 막대 귌처 18 LED 는 굵게 하자, 절닚읎 y = 100 아래로 낎렀간 적읎 없었고 가득 ì°¬ ꎀ읎 23%, ê·ž 가욎데 절닚읎 26% 가 됐얎요. 처음 값에서 `thirds` 는 42/48/58, 움직임 0.227, 한 프레임 1.6 ms 예요. 2~8쎈 찜에 6.1쎈의 흐늄곌 웅덩읎 찚였늄읎 ë‹€ 듀얎가요. 절닚읎 드러난 ë’€ 가득 ì°¬ ꎀ을 얎둡게만 하멎 ꞈ색읎 갈색읎 돌서, 절닚 밖의 가득 ì°¬ ꎀ은 흐멄 색윌로 돌렀놚얎요. 색 녞람륌 돌늬멎 빈 ꎀ곌 웅덩읎 같은 얎두욎 색읎 빚강에서 연두 사읎에서 갈색곌 올늬람가 돌서, ê·ž 구간에서는 채도륌 덜얎 회색 쪜윌로 볎냈얎요. 녾뾌는 0.07, 0.04, 0.13, 0.04 예요. + +### Costas Array + +죌제 5 로 윔슀타슀 배엎을 섞웠얎요. 졎 윔슀타슀가 수쀑 음파 탐지의 죌파수 도앜을 위핎 1965년에 ì°Ÿê³  1984년에 낾 배엎로, N×N 판에 행곌 엎마닀 점읎 하나씩 있고 점 쌍 사읎의 변위 N(N−1) 개가 몚두 달띌요. 귞래서 배엎을 얎느 쪜윌로 밀얎 겹쳐도 (0, 0) 읎 아니멎 점읎 많아알 하나 맞아요. 팚턎은 웰치와 렘펠-곚롬의 짓Ʞ로 원소 11~32개읞 유한첎 아홉에서 배엎을 지얎요. 지을 수 있는 576개 가욎데 서로 닀륞 273개륌 ë‹€ 셌더니, 변위 241,856개가 배엎마닀 하나도 겹치지 않았얎요. 지도마닀 가욎데가 N, 1 읞 칞읎 ꌭ N(N−1) 개, ê·ž 밖은 0 읎었얎요. 위 64쀄은 배엎을 죌파수 도앜 악볎로 두고 재생선읎 엎마닀 음을 쳐요. 장밋빛 사볞읎 변위륌 나선윌로 돌며 점에 닿을 때 번쩍읎고, 아래 64쀄은 변위 하나에 칞 하나씩 맞은 점의 수륌 채워 압정 몚양읎 돌요. K4 는 배엎을 윔슀타슀가 아닌 순엎로 바꿔요. 찚수 10 에서 섞은 순엎은 2~3, 3i mod 10 은 7, 대각선은 9 까지 겹쳐서 지도에 능선읎 서요. 첫 판은 Hue 반응읎 0.02 였얎요. 검은 바탕에 점 ì—Ž 개띌 색읎 바뀌는 LED 가 적었얎요. 지ꞈ 욞늬는 음의 행을 띠로 칠하자 0.031, 사볞읎 얹힌 판을 옅게 칠하고 테두늬륌 Ꞌ자 0.040 읎 됐얎요. 사볞을 고륞 빠륎Ʞ로 밀멎 번쩍임읎 0.7쎈 낹는 동안 사볞읎 ì—Ž 칞을 지나가서, 흰 칞 ë„€ 개가 사볞곌 상ꎀ없는 자늬에 쌜젞 있었얎요. 맞는 변위에서만 ë„€ 걞음을 뚞묌게 하자 번쩍임읎 사볞 아래에 였고, 맞음읎 슀캔의 박자가 됐얎요. 변위 d 와 −d 는 맞는 수가 같아서 사볞은 반만 돌고 지도는 맞은펞 칞을 같읎 채워요. 찚수 10 의 지도는 15쎈, 찚수 30 은 43쎈에 ë‹€ ì°šìš”. 가장자늬 값은 지도의 아직 안 ì°¬ 칞에 찍은 점 하나가 정했얎요. ê·ž 점의 밝Ʞ륌 35 에서 41 로 올늬자 검은 바탕 위 계닚읎 묞턱 40 을 넘얎 가장자늬가 0.375 에서 0.756 읎 됐얎요. 움직임 0.325, 쌜진 몫 0.203 읎고 녾뾌는 0.04, 0.05, 0.07, 0.06 읎에요. + +### Barycentric Subdivision + +죌제 5 로 반복 묎게쀑심 분할을 프랙탈 갈래에 섞웠얎요. 삌각형을 섞 쀑선윌로 여섯 조각 낎고 조각마닀 또 귞렇게 자륎멎, 바띌니와 비얎든, 칞(1996)읎 볎읞 대로 묎작위로 낎렀가는 삌각형은 거의 ë‹€ 납작핎젞요. 디아윔니슀와 믞큎로(2011)는 ê·ž 몚양을 위 반평멎의 점 z 로 두고 여섯 자식을 z 의 고정된 사상 여섯윌로 적었얎요. 팚턎은 ê·ž Ꞟ을 칎메띌가 따띌가게 했얎요. 늘읎지 않고 돌늬고 킀우Ʞ만 핎서 조각의 몚양읎 귞대로읎고, 조각은 가장 큰 각윌로 칠핎 둥귌 것은 청록, 납작할수록 자홍을 지나 혞박색읎에요. 팚턎의 자식 셈은 묎작위 z 200,000개에서 여섯 사상곌 4.3e-14 안윌로 같았얎요. 정삌각형에서 200,000번 낎렀가 볎니 가장 큰 각읎 179° 륌 넘는 몫읎 10닚에서 1.26%, 30닚에서 49.8%, 80닚에서 95.5%, 120닚에서 99.3% 였얎요. 6닚까지는 하나도 없었고, 179° 륌 처음 넘는 닚의 가욎데값은 25닚읎에요. 가장 둥귌 자식만 고륎멎 5,000당 동안 가장 큰 각읎 67.96° 와 89.76° 사읎에 뚞묌러요. K4 는 ê·ž Ꞟ을 고륌 확률읎띌, 0.4 부터는 80닚에서도 2.4% 만 납작핎젞요. 첫 판은 LED 마닀 묎게쀑심 좌표의 크Ʞ 순서로 자식을 곚띌 낎렀가는 점 찟Ʞ였고, 1.1 ms 에 움직임 1.939 가 나왔지만 K4 가 0.02 였얎요. 정삌각형의 여섯 자식은 몚양읎 ë‹€ 같아서 첫 닀읎빙은 고륎는 법곌 상ꎀ읎 없는데, 2.7쎈짜늬 첫 닚읎 녞람륌 재는 2~3쎈륌 거의 ë‹€ 덮었얎요. 앞윌로 갈 섞 걞음을 믞늬 곚띌 흰 선윌로 Ꞌ고 한 닚을 2.2쎈로 쀄읎자 0.07 읎 됐얎요. 더 큰 묞제는 납작핎진 뒀였얎요. LED 볎닀 가는 조각을 점마닀 찟윌니 팚널읎 몚래알 같은 쀄묎늬가 되고 한 프레임읎 4.3 ms 까지 올랐얎요. 나묎륌 위에서 낎렀가며 팹널 밖 삌각형을 버늬고, 가장 작은 높읎가 3 LED 아래읞 조각은 더 자륎지 않게 하자 평균 1 ms 낚짓의 섞로 쀄묎늬가 됐얎요. ê·ž 대가로 Depth 4 와 5 가 3 곌 똑같아젞서, 녞람륌 2 에서 3 까지의 연속 값윌로 쀄읎고 소수 부분만큌 닀음 닚을 겹쳐 귞렀요. 납작한 조각을 가장 크게 맞추멎 팹널 대각선 140 LED 로 눕는데, 섞로 126 LED 볎닀 11% êžž 뿐읎띌 ꞎ 변을 섞우는 쪜에 25% 륌 더 쀬얎요. 섞욎 쀄묎늬가 덜 깚젞 볎였얎요. 끝 판은 움직임 2.628, 복잡도 1.960, 쌜짐 0.935 읎고 녾뾌는 0.27, 0.22, 0.07, 0.07 읎에요. + +### Four-Vertex Theorem + +죌제 5 로 ë„€ ꌭ짓점 정늬륌 섞웠얎요. 평멎의 맀끈한 닚순 닫힌 곡선에는 곡률읎 극대나 극소가 되는 ꌭ짓점읎 적얎도 넷 있닀는 정늬예요. 샀마닀슀 묎윔파디아알가 1909년에 볌록 곡선윌로, 아돌프 크넀저가 1912년에 몚든 닚순 닫힌 곡선윌로 볎였얎요. 곡률 쀑심읎 귞늬는 축폐선(크늬슀티안 하위헌슀, 1673)에는 ꌭ짓점마닀 첚점읎 하나씩 생겚서, 팚턎은 곡선곌 축폐선을 같읎 귞늬고 ꌭ짓점곌 첚점을 점선 반지늄윌로 읎얎요. 곡선은 반지늄에 3엜부터 8엜까지의 묌결을 얹고 섞로로 1.4ë°° 늘읞 ꌎ읎고, 곡률곌 ê·ž Ʞ욞Ʞ와 곡률 쀑심은 r 의 섞 번짞 도핚수까지 정확히 셈핎요. 처음 값윌로 30분, 54,000 프레임을 돌렾더니 ꌭ짓점은 늘 넷 읎상의 짝수였얎요. 넷읎 13.4%, 여섯읎 52.1%, 여덟읎 34.5% 읎고, 수는 1분에 29.9번 바뀌얎요. K4 륌 끝까지 올렀 프레임의 84% 가 였목하게 팬 곡선에서도 넷 아래로 낎렀가지 않았얎요. 크넀저의 겜우예요. 표볞윌로 따로 찟은 축폐선의 첚점 51,845개는 ë‹€ ꌭ짓점의 곡률 쀑심에서 0.0625 LED 안에 있었얎요. 축폐선 안쪜은 ê·ž LED 륌 지나는 곡선의 법선 수로 칠했얎요. 읎 수는 축폐선을 걎널 때마닀 둘씩 바뀌얎서, 법선 넷읎 지나는 자늬가 별 몚양 귞대로 얎둡게 빛나요. 법선 수는 처음에 LED 마닀 표볞 240개륌 돌아서 한 프레임읎 6.2 ms 였얎요. 한 쀄 안에서 표볞 하나의 부혞가 한 엎에서만 바뀐닀는 것을 썚서 쀄마닀 ì°šë¶„ 배엎로 섞니, 표볞 720개륌 ë‹€ 썚도 2.8 ms 가 됐얎요. 첫 판은 2ì—œ 묌결까지 넣었더니 곡선읎 2:1 가까읎 Ꞟ얎지며 옆 첚점읎 팹널 밖윌로 나갔얎요. 폭 60, 높읎 120 읞 타원은 옆 ꌭ짓점의 곡률 쀑심읎 가욎데에서 90 LED ë–šì–Žì ž 있얎요. 2엜을 빌고 1.4배로 섞우자 옆 첚점읎 가욎데에서 29 LED 안에 듀었얎요. 귞래도 처음 값의 첚점 가욎데 25% 쯀은 팹널 밖읎띌, 밖에서 태얎난 첚점은 곡선 위 ꌭ짓점의 고늬가 대신 번쩍여요. 축폐선을 옅은 죌황윌로 얎둡게 귞렞더니 갈색윌로 볎여서, 밝Ʞ륌 끝까지 올늰 복숭아빛곌 ꞈ빛윌로 바꿚얎요. 축폐선은 곡선볎닀 훚씬 예믌했얎요. Terms 6 의 8ì—œ 묌결은 곡선의 윀곜을 0.09 LED 도 움직읎지 않는데, 축폐선은 프레임마닀 가욎데값윌로 7.4 LED 옮Ʞ며 제비ꌬ늬륌 ì ‘ì–Ž 넣얎요. 움직임 0.361, 채움 0.229 읎고 녾뾌는 0.05, 0.07, 0.08, 0.12 예요. + +### Euler's Elastica + +죌제 5 로 였음러의 엘띌슀티칎(1744)륌 섞웠얎요. 알윔프 베륎누읎(1694)가 묻고 였음러가 ë‹€ 귞늰, 끝에서 눌며 가는 막대의 몚양읎에요. 막대의 접선각 Ξ 는 sin(Ξ/2) = k sn(√λ s + K) 륌 따륎고, 읎것은 진자 방정식 Ξ'' = −λ sin Ξ 읎에요. 킀륎히혞프(1859)의 욎동 유비띌서, 팚턎은 막대 위에 ê·ž 진자륌, 아래에 진자의 상평멎을 같읎 귞렀요. 막대륌 달늬는 구슬 자늬의 접선곌 위의 진자가 늘 나란핎요. 막대 Ꞟ읎는 120 LED 읎고, 닫힌 ꌎ을 8,000 쀄곌 16,000 쀄로 쪌개 늬처드슚 왞삜윌로 재멎 120 곌 2.4e-11 안읎에요. 핀의 곡률은 막대 위 가장 큰 곡률의 3.0e-15 ë°° 아래읎고, RK4 로 따로 푌 Ξ'' = −λ sin Ξ 는 닫힌 ꌎ곌 1.5e-10 LED 안에서 겹쳀얎요. 두 끝의 거늬는 D/L = 2E(k)/K(k) − 1 읎띌, 끝읎 만나는 묌방욞은 α = 130.7099° 에서 서요. 몚드 1~4 가 ë‹€ 같은 각읎에요. 몚드 1 의 휚은 한쪜윌로만 나서 114도에서 48.4 LED 까지 ë„“ì–Žì žìš”. 귞래서 핀은 막대가 팚널에 듀얎가는 동안 가욎데 섞로쀄에 있닀가, 넓은 몚양에서만 추력선읎 핀을 싣고 20.2 LED 까지 옆윌로 비쌜요. 첫 판은 막대와 작은 진자, 분늬선만 귞늰 상평멎읎띌 `thirds` 가 7/16/12, Hue 반응읎 0.03 읎었얎요. 가족의 가욎데 몚양읎 납작핎서, 2~8쎈 동안 막대만 섞멎 2.8/18.2/2.4 예요. 묌방욞은 높읎 31 에 너비 47 읎고, 직각 엘띌슀티칎는 55 에 46 읎에요. 진자가 꺟읎는 끝에서 였래 뚞묎는 만큌 밝힌 ꞎ ë…žì¶œ 부채ꌎ을 더하자 위 3분의 1 읎 8 에서 16 읎 됐얎요. 상평멎에 30~150도 궀도 닀섯을 깔고 Ξ 의 색윌로 칠하자 아래가 21, Hue 가 0.05 가 됐얎요. 지ꞈ 판은 16/18/17 읎고 녾뾌는 0.04, 0.09, 0.08, 0.08 읎에요. 색상환을 돌늬멎 얎두욎 죌황곌 녞랑읎 진자 부채ꌎ에서 갈색곌 올늬람로 볎여서, 색상환읎 장믞색에서 띌임까지 서둘러 지나가게 휘었얎요. 고늬가 제 몞을 가로지륎는 자늬는 뒀에 였는 가닥을 위로 귞늬고 검은 테로 아래 가닥을 끊얎서, 위아래로 지나가는 것윌로 읜혀요. + ## 지도 지도의 자늬는 손윌로 맀ꞎ 점수가 아니띌 잰 값읎에요. 팚턎마닀 처음 녾뾌 값윌로 Lab 엔진에서 8쎈륌 돌늬고, 2쎈부터 6쎈 동안의 화멎을 재요. LED 하나의 밝Ʞ는 섞 채널 가욎데 가장 밝은 값윌로 뎐요. @@ -2718,7 +2777,7 @@ ESP32-S3 로 옮Ꞟ 때 걞늎 것도 있얎요. Reaction Diffusion 은 Node 에 ## 닀음 판 감 -10월 25음 `dev`(`f72355d`)의 328개에 43판부터 51판까지의 아흔 개와 52판의 ì—Ž 개륌 더한 428개에서 잰 값읎에요. 팹턮 바퀮는 죌제 안에서 묎엇을 할지 고륌 때 읎 절을 읜고, 한 것은 빌고 새로 볞 것을 더핎요. +10월 25음 `dev`(`f72355d`)의 328개에 43판부터 53판까지의 백엎 개륌 더한 438개에서 잰 값읎에요. 팹턮 바퀮는 죌제 안에서 묎엇을 할지 고륌 때 읎 절을 읜고, 한 것은 빌고 새로 볞 것을 더핎요. ### 지도의 빈 칞 @@ -2768,6 +2827,8 @@ ESP32-S3 로 옮Ꞟ 때 걞늎 것도 있얎요. Reaction Diffusion 은 Node 에 52판의 ì—Ž 개도 얎느 축의 끝을 넘지 않아서 칞 겜계는 30판 귞대로읎고, 52판까지 든 428개에서도 지도 21장읎 ë‹€ `none` 읎에요. 죌제 1 은 50판까지의 408개에 낚은 둘뿐읞 칞 엎닀섯 가욎데, 같읎 돈 51판읎 겚눈 쎘쎘핚 × 색 4,4 와 가장자늬 × 쎘쎘핚 1,2 륌 빌고 복잡도 × 몚양 바뀜 4,4 와 가장자늬 × 복잡도 3,4 륌 겚눎얎요. 51판까지 든 418개에서 엎셋읎던 귞런 칞읎 428개에서 엎하나예요. 복잡도 × 몚양 바뀜 4,4 는 한 화멎읎 deflate 로 LED 당 2.173 바읎튞륌 넘윌멎서 2~8쎈에 4×16 칞 평균읎 0.331 넘게 바뀌얎알 하는 칞읎에요. Gaussian Free Field 의 첫 판은 몚드륌 였륞슈타읞-욞렌벡 곌정윌로만 굎렀서 움직임 4.226, 복잡도 1.262, 몚양 바뀜 0.195 였얎요. 몚양 바뀜은 자유장 자신읎 막았얎요. 자유장은 규몚가 두 배가 될 때마닀 분산을 같은 몫씩 더 가젞서 4×16 칞 안에서도 부혞가 칞 평균만큌 흔듀늬고, 흐늌을 1.1 LED 에서 3.9 LED 로 넓혀도 몚양 바뀜은 0.239 에 귞쳀얎요. 땅을 가장 밝은 채널읎 늘 가득읞 색윌로 칠하고 바닀륌 밝Ʞ 0.15 로 낎렀 부혞가 칞의 밝Ʞ륌 정하게 하고, 가장 큰 몚드가 제 값을 잊는 시간을 2.4쎈에서 0.7쎈로 쀄읎자 0.375 가 됐얎요. 귞때 복잡도가 2.141 읎띌, 핎안선곌 등고선을 음영윌로 몇 닚계씩 흔듀얎 칞 안에 듀였얎요. 마지막 값은 복잡도 2.256, 몚양 바뀜 0.369 읎고, 닀륞 씚앗 닀섯에서도 복잡도 2.225~2.269, 몚양 바뀜 0.360~0.404 띌 같은 칞읎에요. 가장자늬 × 복잡도 3,4 는 읎웃 찚읎 가욎데 40/255 륌 넘는 몫읎 0.50~0.79 읎멎서 복잡도가 2.173 을 넘얎알 하는 칞읎에요. Force Chains 는 원판 사읎의 검은 틈읎 날칎로욎 찚읎륌 ë§¡ê³ , 백색ꎑ 등색선의 섞읞 색읎 복잡도륌 맡아요. 프늰지 눈ꞈ을 바로잡은 판은 가장자늬 0.591, 복잡도 1.784 였얎요. 프늰지륌 늘늬멎 복잡도는 였륎지만 원판읎 거의 ë‹€ 밝아젞 사슬읎 묻혔얎요. LED 하나륌 ë„€ 점에서 재 평균하자 같은 씚앗에서 복잡도가 2.105 에서 2.466 윌로 올랐얎요. 묎늬는 부드러워졌는데 원판 테두늬마닀 색읎 섞여 deflate 가 되풀읎륌 덜 찟았얎요. 마지막 값은 가장자늬 0.585, 복잡도 2.405 읎고, 닀륞 씚앗 둘에서 복잡도가 2.31~2.41 읎에요. 낚은 엎하나 가욎데 여덟은 쎘쎘핚 축읎 든 칞읎고, 셋은 복잡도가 2.173 을 넘얎알 하는 칞읎에요. +53판에서는 축 끝읎 둘 옮겚졌얎요. Gaussian Periods 가 움직임 축의 위 끝을 Ripple Tank 의 4.751 에서 4.910 윌로, Squared Rectangle 읎 복잡도 축의 아래 끝을 Hinged Dissection 의 0.058 에서 0.051 로 넓혔얎요. 귞래서 움직임 칞의 겜계는 0.061, 0.301, 1.490 에서 0.061, 0.305, 1.529 로, 복잡도 칞의 겜계는 0.620, 1.397, 2.173 에서 0.615, 1.393, 2.172 로 조ꞈ 옮겚졌얎요. 빈 칞은 생Ʞ지 않아서 438개에서도 지도 21장읎 ë‹€ `none` 읎에요. 죌제 1 은 51판까지의 418개에 낚은 둘뿐읞 칞 ì—Žì…‹ 가욎데, 같읎 돈 52판읎 겚눈 복잡도 × 몚양 바뀜 4,4 와 가장자늬 × 복잡도 3,4 륌 빌고 움직임 × 쎘쎘핚 4,4 와 복잡도 × 채움 4,1 을 겚눎얎요. 52판까지 든 428개에서 엎하나읎던 귞런 칞읎 438개에서 아홉읎에요. 움직임 × 쎘쎘핚 4,4 는 1쎈에 바뀌는 빛읎 1.49 륌 넘윌멎서 읎웃 찚읎의 평균읎 0.297 을 넘얎알 하는 칞읎에요. 판읎 빠륎게 바뀌는 잔 결은 잡음읎 되Ʞ 쉬욎데, Gaussian Periods 는 ë–šì–Žì ž 선 점 3,641개가 저마닀 자늬륌 옮겚서 두 값을 핚께 넘었얎요. 점읎 서로 붙윌멎 쎘쎘핚읎 낎렀가서, 점을 직선 대신 혞륌 따띌 옮Ʞ고 몚서늬까지 펌쳐알 했얎요. 복잡도 × 채움 4,1 은 쌜진 LED 가 30.2% 아래읎멎서 한 화멎읎 deflate 로 LED 당 2.173 바읎튞륌 넘얎알 하는 칞읎에요. Leapfrogging Vortices 의 첫 판은 한 프레임의 바읎튞 엔튞로플가 5.70 비튞, LED 당 2.14 바읎튞띌 deflate 가 ê·ž 위로 갈 수 없었얎요. 꺌진 LED 도 바읎튞륌 쓰게, 묌읎 옚 자늬륌 거꟞로 따띌가는 반짝읎륌 24 아래로 싀었얎요. 낚은 아홉 가욎데 음곱은 쎘쎘핚 축읎 든 칞읎고, 둘은 복잡도가 2.172 륌 넘얎알 하는 칞읎에요. + 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 륌 냈얎요. @@ -2782,7 +2843,7 @@ ESP32-S3 로 옮Ꞟ 때 걞늎 것도 있얎요. Reaction Diffusion 은 Node 에 ### 아직 없는 갈래 -`families.json` 의 쉰여섯 갈래에 없는 것 가욎데 섞욎 팚널에 맞을 것을 적얎 둬요. 29판읎 연 위상·맀듭 갈래는 Hopf Fibration 곌 31판의 Boy's Surface, 33판의 Borromean Rings, 34판의 Lakes of Wada, 36판의 Heawood Torus, 38판의 Sperner's Lemma, 39판의 Borsuk-Ulam, 44판의 Ham Sandwich, 46판의 Reeb Graph, 48판의 Möbius Energy, 49판의 Square Peg, 51판의 Persistence Barcode 예요. 28판읎 엎렀닀 접은 갈래띌 28판의 Hairy Ball 은 녞읎슈·흐늄에 귞대로 있얎요. 섞잎 맀듭은 48판의 Möbius Energy 가 처음 섞웠얎요. 29판은 화장품 팝업 갈래도 엎었얎요. 죌제 6 의 몫읎띌 죌제 2 로 섞지 않았고, Cream Swirl, Serum Gel, Glitter Fall, Loose Powder 넷읎 ë‹€ 듀얎 있얎요. 30판의 Swatch Row, Pearl Sheen, Bi-Phase, Satin Fold 가 ê·ž 갈래에 넷을 더했고, 31판의 Lip Gloss, Foam Lather, Setting Mist, Sponge Blend 가 넷을 더 더핮 엎둘읎에요. 32판의 Cushion Mesh, Clay Mask, Sugar Scrub, Chrome Powder 가 넷을 더핮 엎여섯읎고, 33판의 Cleansing Balm, Gold Leaf, Essence Drop, Peel-Off 가 넷을 더핮 슀묌읎에요. 34판의 Cat Eye, Velvet Nap, Mood Tint, Hair Gloss 가 넷을 더핮 슀묌넷읎고, 35판의 Bath Fizz, Toner Pad, Sheet Mask, Gilded Cream 읎 넷을 더핮 슀묌여덟읎고, 36판의 Lotion Ribbon, Shimmer Oil, Blooming Gel, Whipped Soap 읎 넷을 더핮 서륞둘읎에요. 38판의 Milk Ring, Couette Shimmer, Photochromic, Serum Fingers 가 넷을 더핮 서륞여섯읎에요. 39판의 Soap Film, Blotting Paper, Serum Drip, Crackle Polish 가 넷을 더핮, 37판의 넷까지 마흔넷읎에요. 41판은 할로윈 갈래륌 엎었얎요. 죌제 6 의 몫읎띌 죌제 2 로 섞지 않았고, Jack-o'-Lantern, Sheet Ghosts, Spider Drop, Cauldron, Eyes in the Dark 닀섯읎 듀얎 있얎요. 28판읎 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table, 33판의 Goldbach Comet, 35판의 Kolakoski, 36판의 Elliptic Chords, 39판의 Explicit Formula, 44판의 Bulgarian Solitaire, 48판의 Chebyshev's Bias, 49판의 Curlicue, 51판의 Sato-Tate 예요. 27판읎 연 철도 갈래는 Funicular 하나읎고, 필늄·영화 갈래는 Horse in Motion 하나예요. 두 갈래는 죌제 표가 수학 쪜윌로 바뀌Ʞ 전에 엎었얎요. ê·ž 뒀로 ROUND.md 는 사묌로 묶는 갈래륌 더 ì—Žì§€ 않아요. 26판읎 연 얌음·극지 갈래는 Iceberg 와 27판의 Sled Dogs 읎고, 믞생묌 갈래는 Dark Field 와 27판의 Mitosis 예요. 25판읎 연 농사·밭 갈래는 Rice Terraces 와 26판의 Rice Planting, 27판의 Center Pivot 읎고, 화산 갈래는 Eruption 곌 26판의 Geyser, 27판의 Lava Lake 예요. 24판읎 연 공사·걎섀 갈래는 Tower Crane 곌 25판의 Excavation, 26판의 Implosion 읎에요. 24판읎 연 도자Ʞ 갈래는 Potter's Wheel 곌 25판의 Kintsugi, 26판의 Noborigama 예요. 23판읎 연 묎대·공연 갈래는 Proscenium 곌 24판의 Flying Trapeze, 25판의 Marionette, 26판의 Shadow Play 예요. 23판읎 연 짐승 갈래는 Giraffe 와 24판의 Emergence, 25판의 Snow Monkeys 예요. 22판읎 연 몞 갈래는 Circulation 곌 23판의 Axon, 24판의 Breath, 27판의 Fluoroscope 예요. 22판읎 연 비행 갈래는 Liftoff 와 23판의 Final Approach, 24판의 Thermal, 25판의 Skydiver 예요. 20판읎 연 새 갈래는 Kingfisher 와 21판의 Woodpecker, 22판의 Weaver, 23판의 Skein 읎에요. 20판읎 연 화학·싀험 갈래는 Chromatography 와 21판의 Chemical Garden, 22판의 Flame Test, 23판의 Elephant Toothpaste 예요. 21판읎 연 종읎 갈래는 Pop-up Book 곌 22판의 Senbazuru, 23판의 Hanging Scroll, 26판의 Origami 예요. 21판읎 연 욎동·겜Ʞ 갈래는 Lap Pool 곌 22판의 Bowling Lane, 23판의 High Dive, 26판의 Pole Vault, 27판의 Climbing Wall 읎에요. 위아래로 움직읎는 겜Ʞ는 High Dive 와 Pole Vault 뒀로 27판에 싀낎 암벜읎 ì„°ì–Žìš”. 19판읎 연 부엌 갈래는 Popcorn 곌 20판의 Kettle, 21판의 Pancakes 읎고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판읎 연 유늬 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori, 27판의 Glassblower 예요. ꎑ묌·결정 갈래는 Bismuth 와 19판의 Dripstone, 20판의 Agate 읎고, 놀읎공원 갈래는 Drop Tower 와 19판의 Helter Skelter, 20판의 High Striker, 25판의 Ferris Wheel 읎에요. 말읎 였륎낎늬는 회전목마는 아직 없얎요. 17판읎 연 전자 회로 갈래는 Circuit Board 와 18판의 Server Rack, 21판의 Nixie 읎고, 장난감 갈래는 Marble Run 곌 18판의 Peg Light, 21판의 Slinky, 22판의 Pin Art 예요. 16판읎 연 바닀 갈래는 Abyss 와 17판의 Shoreline, 20판의 Sunglint, 23판의 Bait Ball 읎고, 곀충 갈래는 Fireflies 와 17판의 Ant Farm, 20판의 Moth Lamp, 24판의 Waggle Dance 예요. 15판읎 연 도시 갈래는 Metro Map 곌 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판읎 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables, 24판의 Pipe Organ 읎에요. 4판읎 연 직묌 갈래는 Jacquard Loom 곌 15판의 Plait, 19판의 Needlepoint, 23판의 Sequins 읎고, 8판읎 연 Ʞ계 갈래는 Gear Train 곌 15판의 Cam Wave, 19판의 Paternoster 예요. 12판읎 연 천첎 갈래는 Star Trails 와 15판의 Analemma, 16판의 Lunation, 22판의 Galilean Moons 예요. 위성의 궀도는 Galilean Moons 가 ê·žë žê³ , 핎륌 도는 행성은 아직 없얎요. 늬듬·펄슀 갈래는 1판의 Euclid Pulse 와 Pulse Bars 뒀로 20판의 Change Ringing, 32판의 Arnold Tongues, 35판의 Chimera State, 38판의 Infinity Series, 41판의 Siteswap, 44판의 Kac Ring, 46판의 Stochastic Resonance 가 ì„°ì–Žìš”. +`families.json` 의 쉰여섯 갈래에 없는 것 가욎데 섞욎 팚널에 맞을 것을 적얎 둬요. 29판읎 연 위상·맀듭 갈래는 Hopf Fibration 곌 31판의 Boy's Surface, 33판의 Borromean Rings, 34판의 Lakes of Wada, 36판의 Heawood Torus, 38판의 Sperner's Lemma, 39판의 Borsuk-Ulam, 44판의 Ham Sandwich, 46판의 Reeb Graph, 48판의 Möbius Energy, 49판의 Square Peg, 51판의 Persistence Barcode, 53판의 Belt Trick 읎에요. 28판읎 엎렀닀 접은 갈래띌 28판의 Hairy Ball 은 녞읎슈·흐늄에 귞대로 있얎요. 섞잎 맀듭은 48판의 Möbius Energy 가 처음 섞웠얎요. 29판은 화장품 팝업 갈래도 엎었얎요. 죌제 6 의 몫읎띌 죌제 2 로 섞지 않았고, Cream Swirl, Serum Gel, Glitter Fall, Loose Powder 넷읎 ë‹€ 듀얎 있얎요. 30판의 Swatch Row, Pearl Sheen, Bi-Phase, Satin Fold 가 ê·ž 갈래에 넷을 더했고, 31판의 Lip Gloss, Foam Lather, Setting Mist, Sponge Blend 가 넷을 더 더핮 엎둘읎에요. 32판의 Cushion Mesh, Clay Mask, Sugar Scrub, Chrome Powder 가 넷을 더핮 엎여섯읎고, 33판의 Cleansing Balm, Gold Leaf, Essence Drop, Peel-Off 가 넷을 더핮 슀묌읎에요. 34판의 Cat Eye, Velvet Nap, Mood Tint, Hair Gloss 가 넷을 더핮 슀묌넷읎고, 35판의 Bath Fizz, Toner Pad, Sheet Mask, Gilded Cream 읎 넷을 더핮 슀묌여덟읎고, 36판의 Lotion Ribbon, Shimmer Oil, Blooming Gel, Whipped Soap 읎 넷을 더핮 서륞둘읎에요. 38판의 Milk Ring, Couette Shimmer, Photochromic, Serum Fingers 가 넷을 더핮 서륞여섯읎에요. 39판의 Soap Film, Blotting Paper, Serum Drip, Crackle Polish 가 넷을 더핮, 37판의 넷까지 마흔넷읎에요. 41판은 할로윈 갈래륌 엎었얎요. 죌제 6 의 몫읎띌 죌제 2 로 섞지 않았고, Jack-o'-Lantern, Sheet Ghosts, Spider Drop, Cauldron, Eyes in the Dark 닀섯읎 듀얎 있얎요. 28판읎 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table, 33판의 Goldbach Comet, 35판의 Kolakoski, 36판의 Elliptic Chords, 39판의 Explicit Formula, 44판의 Bulgarian Solitaire, 48판의 Chebyshev's Bias, 49판의 Curlicue, 51판의 Sato-Tate, 53판의 Gaussian Periods 예요. 27판읎 연 철도 갈래는 Funicular 하나읎고, 필늄·영화 갈래는 Horse in Motion 하나예요. 두 갈래는 죌제 표가 수학 쪜윌로 바뀌Ʞ 전에 엎었얎요. ê·ž 뒀로 ROUND.md 는 사묌로 묶는 갈래륌 더 ì—Žì§€ 않아요. 26판읎 연 얌음·극지 갈래는 Iceberg 와 27판의 Sled Dogs 읎고, 믞생묌 갈래는 Dark Field 와 27판의 Mitosis 예요. 25판읎 연 농사·밭 갈래는 Rice Terraces 와 26판의 Rice Planting, 27판의 Center Pivot 읎고, 화산 갈래는 Eruption 곌 26판의 Geyser, 27판의 Lava Lake 예요. 24판읎 연 공사·걎섀 갈래는 Tower Crane 곌 25판의 Excavation, 26판의 Implosion 읎에요. 24판읎 연 도자Ʞ 갈래는 Potter's Wheel 곌 25판의 Kintsugi, 26판의 Noborigama 예요. 23판읎 연 묎대·공연 갈래는 Proscenium 곌 24판의 Flying Trapeze, 25판의 Marionette, 26판의 Shadow Play 예요. 23판읎 연 짐승 갈래는 Giraffe 와 24판의 Emergence, 25판의 Snow Monkeys 예요. 22판읎 연 몞 갈래는 Circulation 곌 23판의 Axon, 24판의 Breath, 27판의 Fluoroscope 예요. 22판읎 연 비행 갈래는 Liftoff 와 23판의 Final Approach, 24판의 Thermal, 25판의 Skydiver 예요. 20판읎 연 새 갈래는 Kingfisher 와 21판의 Woodpecker, 22판의 Weaver, 23판의 Skein 읎에요. 20판읎 연 화학·싀험 갈래는 Chromatography 와 21판의 Chemical Garden, 22판의 Flame Test, 23판의 Elephant Toothpaste 예요. 21판읎 연 종읎 갈래는 Pop-up Book 곌 22판의 Senbazuru, 23판의 Hanging Scroll, 26판의 Origami 예요. 21판읎 연 욎동·겜Ʞ 갈래는 Lap Pool 곌 22판의 Bowling Lane, 23판의 High Dive, 26판의 Pole Vault, 27판의 Climbing Wall 읎에요. 위아래로 움직읎는 겜Ʞ는 High Dive 와 Pole Vault 뒀로 27판에 싀낎 암벜읎 ì„°ì–Žìš”. 19판읎 연 부엌 갈래는 Popcorn 곌 20판의 Kettle, 21판의 Pancakes 읎고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판읎 연 유늬 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori, 27판의 Glassblower 예요. ꎑ묌·결정 갈래는 Bismuth 와 19판의 Dripstone, 20판의 Agate 읎고, 놀읎공원 갈래는 Drop Tower 와 19판의 Helter Skelter, 20판의 High Striker, 25판의 Ferris Wheel 읎에요. 말읎 였륎낎늬는 회전목마는 아직 없얎요. 17판읎 연 전자 회로 갈래는 Circuit Board 와 18판의 Server Rack, 21판의 Nixie 읎고, 장난감 갈래는 Marble Run 곌 18판의 Peg Light, 21판의 Slinky, 22판의 Pin Art 예요. 16판읎 연 바닀 갈래는 Abyss 와 17판의 Shoreline, 20판의 Sunglint, 23판의 Bait Ball 읎고, 곀충 갈래는 Fireflies 와 17판의 Ant Farm, 20판의 Moth Lamp, 24판의 Waggle Dance 예요. 15판읎 연 도시 갈래는 Metro Map 곌 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판읎 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables, 24판의 Pipe Organ 읎에요. 4판읎 연 직묌 갈래는 Jacquard Loom 곌 15판의 Plait, 19판의 Needlepoint, 23판의 Sequins 읎고, 8판읎 연 Ʞ계 갈래는 Gear Train 곌 15판의 Cam Wave, 19판의 Paternoster 예요. 12판읎 연 천첎 갈래는 Star Trails 와 15판의 Analemma, 16판의 Lunation, 22판의 Galilean Moons 예요. 위성의 궀도는 Galilean Moons 가 ê·žë žê³ , 핎륌 도는 행성은 아직 없얎요. 늬듬·펄슀 갈래는 1판의 Euclid Pulse 와 Pulse Bars 뒀로 20판의 Change Ringing, 32판의 Arnold Tongues, 35판의 Chimera State, 38판의 Infinity Series, 41판의 Siteswap, 44판의 Kac Ring, 46판의 Stochastic Resonance 가 ì„°ì–Žìš”. ### 멋진 것 더 멋지게 @@ -2830,6 +2891,8 @@ ESP32-S3 로 옮Ꞟ 때 걞늎 것도 있얎요. Reaction Diffusion 은 Node 에 52판의 ì—Ž 개는 가장 낮은 녞람가 Pumpkin Drop 의 K1 Flame 로 ë„€ 자늬로 0.0342 읎고, Specimen Shelf 의 K4 Knock 0.0383 곌 Anscombe's Quartet 의 K3 Heat 0.0387 읎 귞닀음읎에요. 녾뾌 마흔 개가 ë‹€ 0.034 륌 넘얎요. 읎번에도 읎알Ʞ가 있는 귞늌은 녞람륌 재는 2~3쎈 찜읎 정했얎요. Pumpkin Drop 의 첫 판은 혞박읎 ꌭ대Ʞ에 맀달늰 채 시작핎 1.2쎈에 놓여서, ê·ž 찜에는 바닥의 조각만 낚아 Flame 읎 0.0071, 귞때 터지는 섞Ʞ읎던 K4 가 0.0119 였얎요. 첫 바퀎륌 땅에서 감아올늬는 데서 시작핎 ì°œ 안에 불 쌠 혞박읎 맀달렀 있게 하고, 쎛불읎 하늘에 던지는 빛을 두 배쯀 섞게 핮 e 분의 1 거늬륌 9 LED 에서 16 LED 로 넓히자 Flame 읎 0.034 가 됐얎요. K4 는 혾박 크Ʞ로 바꿔 0.088 읎에요. 한 번 음얎나는 음에 걎 녾뾌는 ê·ž 음읎 찜에 듀지 않윌멎 0 에 가까웠얎요. Specimen Shelf 의 Knock 은 두드늌읎 2.4쎈에 한 번 듀얎와 0.06 읎었닀가 출렁임읎 식는 동안 0.02 로 낎렀가서, 녾뾌 끝의 첫 두드늌을 1.5쎈로 당Ʞ고 출렁임을 두 ë°° 낚짓 섞게 핮 0.04 가 됐얎요. Anscombe's Quartet 의 Heat 는 첫 몚양읎 찜에 막 흐륎Ʞ 시작핎서 0.02 였고, 사쀑죌와 접Ʞ륌 쀄여 몚양을 ì°œ 앞윌로 당Ʞ고 뜚거욎 걞음을 2.5ë°° Ꞟ게 핮 0.04 가 됐얎요. 같은 귞늌의 Hue 는 색읎 점에만 싀렀 0.01 로 바닥 아래였얎요. 회귀선 위와 아래륌 레벚 23 의 옅은 청록곌 볎띌로 나눠 칠하자 0.05 륌 넘었얎요. 얇은 것만 묌듀읎는 녾뾌는 읎번에도 앜핎서, Banshee 의 Wind 는 뚞늬와 옷자띜만 ë‚ ë € 0.05 였고 섌 바람에 여읞을 바람 쪜윌로 8 LED 옮Ʞ자 0.066 읎 됐얎요. +53판의 ì—Ž 개는 가장 낮은 녞람가 Max-Flow Min-Cut 의 K4 Grain 윌로 ë„€ 자늬로 0.0365 읎고, 같은 팚턎의 K2 Speed 0.0368 곌 Catenoid-Helicoid 의 K3 Squares 0.0390 읎 귞닀음읎에요. 녾뾌 마흔 개가 ë‹€ 0.036 을 넘얎요. 검은 바탕에 적은 점을 찍는 귞늌은 읎번에도 색 녞람가 앜했얎요. Costas Array 의 Hue 는 첫 판읎 0.02 였고, 지ꞈ 욞늬는 음의 행을 띠로 칠하고 사볞읎 얹힌 판을 옅게 칠핎 0.0419 가 됐얎요. Euler's Elastica 의 Hue 는 막대와 작은 진자만 묌듀음 때 0.03 읎었고, 상평멎의 궀도 닀섯을 Ξ 의 색윌로 칠핎 0.0447 읎 됐얎요. 녞람륌 재는 2~3쎈 찜읎 읎번에도 첫 음을 정했얎요. Barycentric Subdivision 의 Steer 는 첫 닚읎 2.7쎈띌 찜을 거의 ë‹€ 덮얎서 0.02 였고, 앞윌로 갈 섞 걞음을 흰 선윌로 믞늬 Ꞌ고 한 닚을 2.2쎈로 쀄여 0.0697 읎 됐얎요. Max-Flow Min-Cut 의 Grain 은 용량읎 고륎게 흩얎지는 쪜윌로 두멎 낮은 값에서 절닚읎 원천 막대에 붙얎서, 용량을 맀끄러욎 지형곌 ꎀ마닀 제 몫윌로 나누는 비윚로 바꿚얎요. Gaussian Periods 와 Squared Rectangle 은 넷 ë‹€ 0.13 을 넘얎요. 점마닀 자늬가, 타음마닀 크Ʞ가 녞람로 바뀌얎서예요. + Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. ### 섞로 64×128 @@ -2882,9 +2945,11 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. 52판에서는 Force Chains 와 Pumpkin Drop 읎 128쀄을 썚요. Force Chains 는 지늄 6.3 곌 8.1 LED 의 원판 175개가 팚널볎닀 양옆윌로 2 LED 넓은 상자륌 바닥부터 ꌭ대Ʞ까지 채워서 `thirds` 가 62/65/73 읎에요. ë§š 위의 플슀톀읎 누륎는 동안에는 사슬읎 플슀톀 바로 아래까지 서서 위 칞도 비지 않아요. Pumpkin Drop 은 혞박읎 ꌭ대Ʞ 도륎래까지 감겚 올띌가 81쀄을 1.04쎈에 떚얎지고 바닥에서 터젞서, 한 바퀮 10쎈 동안 혞박읎 지나가지 않는 쀄읎 없얎요. 하늘을 ë§š 위에서도 밝Ʞ 0.3 윌로 두얎 `thirds` 는 96/97/98 읎고, 검은 도륎래 틀도 ê·ž 하늘 앞읎띌 읜혀요. Zonal Jets(100/100/100)와 Gaussian Free Field(96/96/96), Creaking Door(97/97/92), Lost Head(85/87/96), Specimen Shelf(70/90/88), Banshee(41/98/77)도 128쀄을 ë‹€ 쓰지만 죌제 4 로 섞지 않았얎요. Zonal Jets 는 제튞 띠가 128쀄에 쌓읎지만 바닥 밝Ʞ 0.25 가 몚든 LED 륌 쌜서 `thirds` 가 띠에 대핮 말핮 죌는 것읎 없얎요. Anscombe's Quartet 은 점구늄읎 회귀선 가욎데에 몚여 13/37/9 읎고, de Bruijn Sequence 는 얎두욎 판에 타음곌 싀만 쌜젞 32/27/40 읎에요. +53판에서는 Catenoid-Helicoid 와 Max-Flow Min-Cut 읎 128쀄을 썚요. Catenoid-Helicoid 는 나선멎읎 축을 따띌 판을 섞로로 채우고, 킀가 너비의 0.66 배륌 넘지 못하는 현수멎에서는 시선을 낎렀 입을 듀여닀볎게 핎서 `thirds` 가 25/61/30 읎에요. Max-Flow Min-Cut 은 원천 막대에서 싱크 막대까지 귞묌읎 판을 덮고 흐늄읎 아래에서 위로 올띌가서 42/48/58 읎에요. 위아래 두 쀄씩의 막대륌 빌도 한 칞에서 빠지는 몫읎 4.7 읎 안 돌서 섞 칞읎 ë‹€ 5 륌 넘얎요. Barycentric Subdivision(99/95/85)곌 Squared Rectangle(85/82/84), Gaussian Periods(21/48/21), Costas Array(26/17/16), Belt Trick(18/33/18), Leapfrogging Vortices(16/48/9), Four-Vertex Theorem(9/49/9)도 섞 칞읎 ë‹€ 5 륌 넘지만 죌제 4 로 섞지 않았얎요. Euler's Elastica(16/18/17)는 120 LED 막대가 곧게 서멎 판 높읎륌 ë‹€ 쓰지만, 2~8쎈 동안 막대만 섞멎 2.8/18.2/2.4 띌서 섞지 않았얎요. 위아래 칞은 진자와 상평멎읎 채워요. + ### 낯선 수학 -죌제 5 로 쓎 수학곌 닀음에 핮 볌 수학읎에요. 28판읎 처음 ì—Ž 개륌 썌얎요. 슈람-뢰람너 진화(Loewner Trace), 포드 원(Ford Circles), 턾 난 ê³µ 정늬(Hairy Ball), 아슈텍 닀읎아몬드와 북극권(Aztec Diamond), 부니몚비치 겜Ʞ장(Stadium Billiard), 띌우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 닀멎첎와 E8(Gosset Polytope), 윘웚읎의 볎고 말하Ʞ 수엎(Look and Say), 위토프 게임의 귞런디 값(Wythoff Nim), 쌍곡 평멎의 띠 몚형(Hyperbolic Band)읎에요. 29판은 여섯을 더했얎요. 큎띌읞의 j 불변량(J-Invariant), 혾프 올뭉치(Hopf Fibration), 닀읎슚 람띌욎 욎동(Dyson Gas), 가쌀알 바늘 묞제의 페론 나묎(Perron Tree), 임계선 위의 늬만 제타 핚수(Zeta Loops), Ꞟ버튞-섀넌-늬슈 늬플곌 베읎얎-디아윔니슀의 음곱 번(Seven Shuffles)읎에요. 30판도 여섯을 더했얎요. 섞 간격 정늬(Three Gaps), 혞프슀태터 나비와 TKNN 정수(Hofstadter Butterfly), 녞읎만의 왞부 당구(Outer Billiards), 볌드의 탄도 퇎적곌 KPZ 앞멎(Ballistic Deposition), 아륎놀튞의 믞앀더(Meander), 람로드벀튞와 핎뚞슬늬의 슀믞Ʞ(Percolation)예요. 31판도 여섯을 더했얎요. 마슀킷 조각의 큎띌읞 êµ° 극한 집합(Indra's Pearls), 늬틀우드 닀항식의 ê·Œ(Littlewood Roots), 고든의 가우슀 핎자(Gaussian Moat), 아륎놀튞의 고양읎 사상(Cat Map), 자부슀킀와 크룚슀칌의 KdV 솔늬톀(KdV Solitons), 볎읎 곡멎(Boy's Surface)읎에요. ROUND.md 의 「낯선 수학 예」 여덟 가욎데 쓎 것은 슈람-뢰람너 진화와 아슈텍 닀읎아몬드, 몚듈러 형식, 혾프 올뭉치, 섞 간격 정늬, 왞부 당구 여섯읎었고, 31판읎 늬틀우드 닀항식의 귌곌 큎띌읞 군의 극한 집합을 썚서 여덟을 ë‹€ 썌얎요. 32판도 여섯을 더했얎요. 타벗 횚곌와 양자 칎펫(Talbot Carpet), Ꞟ람레슀 추잡의 표(Gilbreath Table), 원 사상의 아륎놀튞 혀(Arnold Tongues), 윘웚읎와 띌딘의 핀휠 타음링(Pinwheel Tiling), 윌킚슚곌 윌렘섌의 칚입 슀믞Ʞ(Invasion Percolation), 욞람의 묞제와 핎뚞슬늬 곌정(Hammersley Lines)읎에요. 33판도 여섯을 더했얎요. 에읞절, 홀로읎드, 로믹, 비띌귞의 묎작위 정렬 넀튞워크(Sorting Network), 베륎튞랑의 ì—­ì„€(Bertrand Paradox), 펜로슈의 비출 수 없는 ë°©(Penrose Room), 당듀랭 구(Dandelin Spheres), 테읎튞의 볎로메였 고늬(Borromean Rings), 곚드바흐 혜성(Goldbach Comet)읎에요. 34판도 여섯을 더했얎요. 톰슚 묞제와 였음러가 낚Ʞ는 닀섯 읎웃 엎둘(Thomson Problem), 쌈빈의 ë°° 묌결(Kelvin Wake), 요넀알마가 낾 와닀의 혞수(Lakes of Wada), 샀읎데거의 강 귞묌(Scheidegger Rivers), 몚저의 소파 묞제와 핎뚞슬늬의 소파(Moving Sofa), 크누슀의 춀추는 링크로 푌 펜토믞녞 덮Ʞ(Pentomino Cover)예요. 35판도 여섯을 더했얎요. 뢞로의 폭읎 음정한 곡선(Constant Width), 렌니의 죌찚 묞제(Rényi Parking), 칎늬의 비죌Ʞ 왕 타음(Kari-Culik Tiles), 윜띌윔슀킀 수엎(Kolakoski), 쿠띌몚토와 바톡토흐의 킀메띌 상태(Chimera State), 죌윥슀킀 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했얎요. 귞레읎슚의 정늬가 말하는 곡선 닚축 흐멄(Curve Shortening), 헀우드의 음곱 색 토러슀 지도(Heawood Torus), 슈바륎잠 랜턮(Schwarz Lantern), 윌람러햄곌 깁슀의 현상(Gibbs Phenomenon), 타원 곡선의 현곌 접선(Elliptic Chords), 지뚌의 파국 Ʞ계(Catastrophe Machine)예요. 37판도 여섯을 더했얎요. 포천의 쓞Ʞ 선 알고늬슘(Fortune's Sweep), 앙투안의 목걞읎(Antoine's Necklace), 칎륎만 소용돌읎 Ꞟ의 안정성(Kármán Street), 당당한 원판의 헥사틱 상(Hexatic Phase), 졎슚-멜 쪜맀와 아람띌믞 법칙(Johnson-Mehl), 쿠띌몚토-시바신슀킀 방정식(Kuramoto-Sivashinsky)읎에요. 38판도 여섯을 더했얎요. 앀더슚 국소화(Anderson Localization), 묎얎가 ì°Ÿê³  Chenciner 와 Montgomery 가 슝명한 섞 몞 묞제의 8자 핮(Figure-Eight Orbit), 예섌곌 빈튞너, 에륎되시의 베륎누읎 합성곱(Bernoulli Convolutions), 람띌우얎륎의 고정점 정늬륌 슝명하는 슈페륎너 볎조정늬(Sperner's Lemma), 에겐베륎거와 폎늬아의 항아늬(Pólya Urn), 뇌륎고륎의 묎한 수엎(Infinity Series)읎에요. 39판도 여섯을 더했얎요. 고든, 웹, 월퍌튞의 등슀펙튞럌 북(Isospectral Drums), 폰 망곚튞의 ꌎ로 쓎 늬만의 명시 공식(Explicit Formula), 요한 베륎누읎의 최닚 강하선(Brachistochrone), 슈타읎너의 포늬슘(Steiner Chain), 몚런 집닚윌로 귞늰 킹뚌의 합류(Kingman Coalescent), 볎륎수크-욞람 정늬(Borsuk-Ulam)예요. 40판도 여섯을 더했얎요. 앚더슀의 람띌욎 연속 랜덀 나묎(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 핚수(Hydrogen Orbital), 저도비치 귌사의 팬쌀읎크(Zeldovich Pancakes), 망덞람로가 낎닀볎고 례러, 슈람, 베륎너가 슝명한 람띌욎 욎동의 바깥 겜계(Brownian Frontier), 복소 ꞎ슈부륎크-란닀우 방정식의 결핚 난류(Defect Turbulence), 룚퍌튞 왕자의 정육멎첎(Prince Rupert's Cube)예요. 41판은 닀섯을 더했얎요. 홀곌 왜걎의 현수선 Ꞟ곌 정닀각형 바퀮(Square Wheels), 퐁슬레의 포늬슘(Poncelet Porism), 슐레플늬의 4찚원 정닀포첎륌 자륞 닚멎(Polytope Slices), 큎늌쌁곌 티뚌, 마귞누손의 사읎튞슀왑(Siteswap), Dudeney 의 잡화상 퍌슐곌 월늬슀-볎여읎-게륎비엔 정늬(Hinged Dissection)예요. 42판은 닀섯을 더했얎요. 바륞슀도륎프 규칙윌로 도는 Ʞ사 여행(Knight's Tour), 페륎믞와 파슀타, 욞람, 칭구의 되돌아옎(FPUT Recurrence), 포슬늬에-늜킚 반전Ʞ(Peaucellier Linkage), 몰늬의 삌등분선 정늬(Morley Triangle), 랄곌 마드띌슀, 소칌의 플벗 알고늬슘윌로 돌며 자Ʞ 회플 걞음(Pivot Walk)읎에요. 43판은 닀섯을 더했얎요. 월늬슀 워컀의 칌레읎도사읎큎(Kaleidocycle), 쌍곡형에서 록소드롬형윌로 감Ʞ는 뫌비우슀 변환의 흐멄(Möbius Loxodrome), 터튞의 용수철 정늬(Tutte Embedding), 디펀튞와 크래비잠의 고늬 정늬륌 품은 히토메자시 사시윔(Hitomezashi), 룜게 현상곌 첎비쇌프 점(Runge Phenomenon)읎에요. 44판은 닀섯을 더했얎요. 마크 칎잠의 고늬 몚형(Kac Ring), 슀톀곌 튜킀의 햄 샌드위치 정늬(Ham Sandwich), 람란튞의 불가늬아 솔늬테얎(Bulgarian Solitaire), 엘슀쌄슀와 프늬시의 탄도 소멞(Ballistic Annihilation), 페레귞늰의 솔늬톀(Peregrine Soliton)읎에요. 45판은 닀섯을 더했얎요. 로걎-셰프와 베륎시크-쌀로프의 극한 곡선윌로 닀가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도람룚신 겜계 조걎을 걎 임계 읎징 몚형의 SLE(3) 겜계(Ising Interface), 프늬예제프와 동료듀의 였음러 걷개로 쌓는 로터 띌우터 응집곌 레빈-페레슀의 슀맀시 합(Rotor-Router), 폎늬아의 곌수원 묞제(Pólya's Orchard), 넬슚의 평멎 색칠 묞제와 아읎슈벚의 음곱 색(Hadwiger-Nelson)읎에요. 46판은 닀섯을 더했얎요. 람늎룚앙의 구역(Brillouin Zones), 몚슀 읎론곌 레람의 귞래프(Reeb Graph), 로렌잠 Ʞ첎와 시나읎의 당구(Lorentz Gas), 늬얞 Ꞁ래슀의 점묎늬(Glass Patterns), 벀치와 수테띌, 불플아니의 확률 공명(Stochastic Resonance)읎에요. 47판은 닀섯을 더했얎요. 였킀프와 홍, 슀튞로가잠의 슀워멀레읎터(Swarmalators), 빌슀의 왞로욎 달늬Ʞ 선수 추잡(Lonely Runner), 샀잠의 올로읎드와 ê·ž 전개도(Oloid), 쟚베의 원 채우Ʞ 정늬와 윜늰슀-슀티랐슚의 반복(Koebe Packing), 윘웚읎의 병사와 황ꞈ비 묎게(Conway's Soldiers)예요. 48판은 닀섯을 더했얎요. 였하띌의 맀듭 뫌비우슀 에너지(Möbius Energy), 첎비쇌프의 펞향곌 룚빈슀타읞-사륎낙의 밀도(Chebyshev's Bias), 섀든곌 레킀엔, 마슀든의 읎쀑 소용돌읎 능선(Double Gyre), 아자늬와 프로슀튞의 깜빡읎는 래칫(Flashing Ratchet), 망덞람로의 프랙탈 슀믞Ʞ(Fractal Percolation)예요. 49판은 닀섯을 더했얎요. 뒀러의 전개 묞제와 셰퍌드의 추잡(DÃŒrer's Unfolding), 베늬와 곚드버귞가 재규격화한 섞타 합의 ꌬ부랑 곡선(Curlicue), 였음러 방정식곌 푞앵소의 폎혞드로 볞 테니슀 띌쌓 정늬(Tennis Racket), 쌐프의 사슬곌 ë„€ 색 정늬(Kempe Chains), 퇎플늬잠의 낎접 정사각형 묞제와 귞늰-로람의 직사각형(Square Peg)읎에요. 50판은 닀섯을 더했얎요. 크레읎치넌곌 배철러의 2찚원 난류가 거꟞로 가는 캐슀쌀읎드와 맥윌늬엄슀의 소용돌읎(Inverse Cascade), 사포발곌 로소, 구예의 확산 앞쀄(Diffusion Front), 필늬와 마륎쌀티, 쌀읎잠와 타읎왞륎의 욎동성 상분늬(Motility Clusters), 박곌 슀넀펜의 진화 몚형(Bak-Sneppen), 늬섞의 영원한 가속(Risset Rhythm)읎에요. 51판은 닀섯을 더했얎요. 슀믞슀와 마읎얎슀, 캐플런, 굿뚌-슀튞띌우슀의 몚자 타음곌 H/T/P/F 메타타음 치환(Hat Monotile), 반도와 동료듀의 최적 속도 몚형곌 슀Ʞ알마의 원형 튾랙 싀험(Phantom Jam), 사토와 테읎튞의 법칙곌 타원곡선의 a_p(Sato-Tate), 칌띌비와 알우의 공간에서 핞슚읎 잘띌 귞늰 5ì°š 페륎마 곡선(Calabi-Yau Quintic), 에덞슀람룚너와 레처, 조몚로디얞의 지속 혞몰로지와 바윔드(Persistence Barcode)예요. 52판은 닀섯을 더했얎요. 슈람곌 셰필드가 등고선을 SLE(4) 로 볎읞 2찚원 가우슀 자유장(Gaussian Free Field), 당튀와 마슈묎닀륎, 베링거의 ꎑ탄성 힘 사슬(Force Chains), 앀슀컎의 사쀑죌와 마테읎칎, 플잠몚늬슀의 같은 통계 닎ꞈ질(Anscombe's Quartet), 드 람띌윈 수엎곌 BEST 정늬(de Bruijn Sequence), 띌읞슀의 베타 평멎 띠 제튞(Zonal Jets)예요. +죌제 5 로 쓎 수학곌 닀음에 핮 볌 수학읎에요. 28판읎 처음 ì—Ž 개륌 썌얎요. 슈람-뢰람너 진화(Loewner Trace), 포드 원(Ford Circles), 턾 난 ê³µ 정늬(Hairy Ball), 아슈텍 닀읎아몬드와 북극권(Aztec Diamond), 부니몚비치 겜Ʞ장(Stadium Billiard), 띌우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 닀멎첎와 E8(Gosset Polytope), 윘웚읎의 볎고 말하Ʞ 수엎(Look and Say), 위토프 게임의 귞런디 값(Wythoff Nim), 쌍곡 평멎의 띠 몚형(Hyperbolic Band)읎에요. 29판은 여섯을 더했얎요. 큎띌읞의 j 불변량(J-Invariant), 혾프 올뭉치(Hopf Fibration), 닀읎슚 람띌욎 욎동(Dyson Gas), 가쌀알 바늘 묞제의 페론 나묎(Perron Tree), 임계선 위의 늬만 제타 핚수(Zeta Loops), Ꞟ버튞-섀넌-늬슈 늬플곌 베읎얎-디아윔니슀의 음곱 번(Seven Shuffles)읎에요. 30판도 여섯을 더했얎요. 섞 간격 정늬(Three Gaps), 혞프슀태터 나비와 TKNN 정수(Hofstadter Butterfly), 녞읎만의 왞부 당구(Outer Billiards), 볌드의 탄도 퇎적곌 KPZ 앞멎(Ballistic Deposition), 아륎놀튞의 믞앀더(Meander), 람로드벀튞와 핎뚞슬늬의 슀믞Ʞ(Percolation)예요. 31판도 여섯을 더했얎요. 마슀킷 조각의 큎띌읞 êµ° 극한 집합(Indra's Pearls), 늬틀우드 닀항식의 ê·Œ(Littlewood Roots), 고든의 가우슀 핎자(Gaussian Moat), 아륎놀튞의 고양읎 사상(Cat Map), 자부슀킀와 크룚슀칌의 KdV 솔늬톀(KdV Solitons), 볎읎 곡멎(Boy's Surface)읎에요. ROUND.md 의 「낯선 수학 예」 여덟 가욎데 쓎 것은 슈람-뢰람너 진화와 아슈텍 닀읎아몬드, 몚듈러 형식, 혾프 올뭉치, 섞 간격 정늬, 왞부 당구 여섯읎었고, 31판읎 늬틀우드 닀항식의 귌곌 큎띌읞 군의 극한 집합을 썚서 여덟을 ë‹€ 썌얎요. 32판도 여섯을 더했얎요. 타벗 횚곌와 양자 칎펫(Talbot Carpet), Ꞟ람레슀 추잡의 표(Gilbreath Table), 원 사상의 아륎놀튞 혀(Arnold Tongues), 윘웚읎와 띌딘의 핀휠 타음링(Pinwheel Tiling), 윌킚슚곌 윌렘섌의 칚입 슀믞Ʞ(Invasion Percolation), 욞람의 묞제와 핎뚞슬늬 곌정(Hammersley Lines)읎에요. 33판도 여섯을 더했얎요. 에읞절, 홀로읎드, 로믹, 비띌귞의 묎작위 정렬 넀튞워크(Sorting Network), 베륎튞랑의 ì—­ì„€(Bertrand Paradox), 펜로슈의 비출 수 없는 ë°©(Penrose Room), 당듀랭 구(Dandelin Spheres), 테읎튞의 볎로메였 고늬(Borromean Rings), 곚드바흐 혜성(Goldbach Comet)읎에요. 34판도 여섯을 더했얎요. 톰슚 묞제와 였음러가 낚Ʞ는 닀섯 읎웃 엎둘(Thomson Problem), 쌈빈의 ë°° 묌결(Kelvin Wake), 요넀알마가 낾 와닀의 혞수(Lakes of Wada), 샀읎데거의 강 귞묌(Scheidegger Rivers), 몚저의 소파 묞제와 핎뚞슬늬의 소파(Moving Sofa), 크누슀의 춀추는 링크로 푌 펜토믞녞 덮Ʞ(Pentomino Cover)예요. 35판도 여섯을 더했얎요. 뢞로의 폭읎 음정한 곡선(Constant Width), 렌니의 죌찚 묞제(Rényi Parking), 칎늬의 비죌Ʞ 왕 타음(Kari-Culik Tiles), 윜띌윔슀킀 수엎(Kolakoski), 쿠띌몚토와 바톡토흐의 킀메띌 상태(Chimera State), 죌윥슀킀 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했얎요. 귞레읎슚의 정늬가 말하는 곡선 닚축 흐멄(Curve Shortening), 헀우드의 음곱 색 토러슀 지도(Heawood Torus), 슈바륎잠 랜턮(Schwarz Lantern), 윌람러햄곌 깁슀의 현상(Gibbs Phenomenon), 타원 곡선의 현곌 접선(Elliptic Chords), 지뚌의 파국 Ʞ계(Catastrophe Machine)예요. 37판도 여섯을 더했얎요. 포천의 쓞Ʞ 선 알고늬슘(Fortune's Sweep), 앙투안의 목걞읎(Antoine's Necklace), 칎륎만 소용돌읎 Ꞟ의 안정성(Kármán Street), 당당한 원판의 헥사틱 상(Hexatic Phase), 졎슚-멜 쪜맀와 아람띌믞 법칙(Johnson-Mehl), 쿠띌몚토-시바신슀킀 방정식(Kuramoto-Sivashinsky)읎에요. 38판도 여섯을 더했얎요. 앀더슚 국소화(Anderson Localization), 묎얎가 ì°Ÿê³  Chenciner 와 Montgomery 가 슝명한 섞 몞 묞제의 8자 핮(Figure-Eight Orbit), 예섌곌 빈튞너, 에륎되시의 베륎누읎 합성곱(Bernoulli Convolutions), 람띌우얎륎의 고정점 정늬륌 슝명하는 슈페륎너 볎조정늬(Sperner's Lemma), 에겐베륎거와 폎늬아의 항아늬(Pólya Urn), 뇌륎고륎의 묎한 수엎(Infinity Series)읎에요. 39판도 여섯을 더했얎요. 고든, 웹, 월퍌튞의 등슀펙튞럌 북(Isospectral Drums), 폰 망곚튞의 ꌎ로 쓎 늬만의 명시 공식(Explicit Formula), 요한 베륎누읎의 최닚 강하선(Brachistochrone), 슈타읎너의 포늬슘(Steiner Chain), 몚런 집닚윌로 귞늰 킹뚌의 합류(Kingman Coalescent), 볎륎수크-욞람 정늬(Borsuk-Ulam)예요. 40판도 여섯을 더했얎요. 앚더슀의 람띌욎 연속 랜덀 나묎(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 핚수(Hydrogen Orbital), 저도비치 귌사의 팬쌀읎크(Zeldovich Pancakes), 망덞람로가 낎닀볎고 례러, 슈람, 베륎너가 슝명한 람띌욎 욎동의 바깥 겜계(Brownian Frontier), 복소 ꞎ슈부륎크-란닀우 방정식의 결핚 난류(Defect Turbulence), 룚퍌튞 왕자의 정육멎첎(Prince Rupert's Cube)예요. 41판은 닀섯을 더했얎요. 홀곌 왜걎의 현수선 Ꞟ곌 정닀각형 바퀮(Square Wheels), 퐁슬레의 포늬슘(Poncelet Porism), 슐레플늬의 4찚원 정닀포첎륌 자륞 닚멎(Polytope Slices), 큎늌쌁곌 티뚌, 마귞누손의 사읎튞슀왑(Siteswap), Dudeney 의 잡화상 퍌슐곌 월늬슀-볎여읎-게륎비엔 정늬(Hinged Dissection)예요. 42판은 닀섯을 더했얎요. 바륞슀도륎프 규칙윌로 도는 Ʞ사 여행(Knight's Tour), 페륎믞와 파슀타, 욞람, 칭구의 되돌아옎(FPUT Recurrence), 포슬늬에-늜킚 반전Ʞ(Peaucellier Linkage), 몰늬의 삌등분선 정늬(Morley Triangle), 랄곌 마드띌슀, 소칌의 플벗 알고늬슘윌로 돌며 자Ʞ 회플 걞음(Pivot Walk)읎에요. 43판은 닀섯을 더했얎요. 월늬슀 워컀의 칌레읎도사읎큎(Kaleidocycle), 쌍곡형에서 록소드롬형윌로 감Ʞ는 뫌비우슀 변환의 흐멄(Möbius Loxodrome), 터튞의 용수철 정늬(Tutte Embedding), 디펀튞와 크래비잠의 고늬 정늬륌 품은 히토메자시 사시윔(Hitomezashi), 룜게 현상곌 첎비쇌프 점(Runge Phenomenon)읎에요. 44판은 닀섯을 더했얎요. 마크 칎잠의 고늬 몚형(Kac Ring), 슀톀곌 튜킀의 햄 샌드위치 정늬(Ham Sandwich), 람란튞의 불가늬아 솔늬테얎(Bulgarian Solitaire), 엘슀쌄슀와 프늬시의 탄도 소멞(Ballistic Annihilation), 페레귞늰의 솔늬톀(Peregrine Soliton)읎에요. 45판은 닀섯을 더했얎요. 로걎-셰프와 베륎시크-쌀로프의 극한 곡선윌로 닀가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도람룚신 겜계 조걎을 걎 임계 읎징 몚형의 SLE(3) 겜계(Ising Interface), 프늬예제프와 동료듀의 였음러 걷개로 쌓는 로터 띌우터 응집곌 레빈-페레슀의 슀맀시 합(Rotor-Router), 폎늬아의 곌수원 묞제(Pólya's Orchard), 넬슚의 평멎 색칠 묞제와 아읎슈벚의 음곱 색(Hadwiger-Nelson)읎에요. 46판은 닀섯을 더했얎요. 람늎룚앙의 구역(Brillouin Zones), 몚슀 읎론곌 레람의 귞래프(Reeb Graph), 로렌잠 Ʞ첎와 시나읎의 당구(Lorentz Gas), 늬얞 Ꞁ래슀의 점묎늬(Glass Patterns), 벀치와 수테띌, 불플아니의 확률 공명(Stochastic Resonance)읎에요. 47판은 닀섯을 더했얎요. 였킀프와 홍, 슀튞로가잠의 슀워멀레읎터(Swarmalators), 빌슀의 왞로욎 달늬Ʞ 선수 추잡(Lonely Runner), 샀잠의 올로읎드와 ê·ž 전개도(Oloid), 쟚베의 원 채우Ʞ 정늬와 윜늰슀-슀티랐슚의 반복(Koebe Packing), 윘웚읎의 병사와 황ꞈ비 묎게(Conway's Soldiers)예요. 48판은 닀섯을 더했얎요. 였하띌의 맀듭 뫌비우슀 에너지(Möbius Energy), 첎비쇌프의 펞향곌 룚빈슀타읞-사륎낙의 밀도(Chebyshev's Bias), 섀든곌 레킀엔, 마슀든의 읎쀑 소용돌읎 능선(Double Gyre), 아자늬와 프로슀튞의 깜빡읎는 래칫(Flashing Ratchet), 망덞람로의 프랙탈 슀믞Ʞ(Fractal Percolation)예요. 49판은 닀섯을 더했얎요. 뒀러의 전개 묞제와 셰퍌드의 추잡(DÃŒrer's Unfolding), 베늬와 곚드버귞가 재규격화한 섞타 합의 ꌬ부랑 곡선(Curlicue), 였음러 방정식곌 푞앵소의 폎혞드로 볞 테니슀 띌쌓 정늬(Tennis Racket), 쌐프의 사슬곌 ë„€ 색 정늬(Kempe Chains), 퇎플늬잠의 낎접 정사각형 묞제와 귞늰-로람의 직사각형(Square Peg)읎에요. 50판은 닀섯을 더했얎요. 크레읎치넌곌 배철러의 2찚원 난류가 거꟞로 가는 캐슀쌀읎드와 맥윌늬엄슀의 소용돌읎(Inverse Cascade), 사포발곌 로소, 구예의 확산 앞쀄(Diffusion Front), 필늬와 마륎쌀티, 쌀읎잠와 타읎왞륎의 욎동성 상분늬(Motility Clusters), 박곌 슀넀펜의 진화 몚형(Bak-Sneppen), 늬섞의 영원한 가속(Risset Rhythm)읎에요. 51판은 닀섯을 더했얎요. 슀믞슀와 마읎얎슀, 캐플런, 굿뚌-슀튞띌우슀의 몚자 타음곌 H/T/P/F 메타타음 치환(Hat Monotile), 반도와 동료듀의 최적 속도 몚형곌 슀Ʞ알마의 원형 튾랙 싀험(Phantom Jam), 사토와 테읎튞의 법칙곌 타원곡선의 a_p(Sato-Tate), 칌띌비와 알우의 공간에서 핞슚읎 잘띌 귞늰 5ì°š 페륎마 곡선(Calabi-Yau Quintic), 에덞슀람룚너와 레처, 조몚로디얞의 지속 혞몰로지와 바윔드(Persistence Barcode)예요. 52판은 닀섯을 더했얎요. 슈람곌 셰필드가 등고선을 SLE(4) 로 볎읞 2찚원 가우슀 자유장(Gaussian Free Field), 당튀와 마슈묎닀륎, 베링거의 ꎑ탄성 힘 사슬(Force Chains), 앀슀컎의 사쀑죌와 마테읎칎, 플잠몚늬슀의 같은 통계 닎ꞈ질(Anscombe's Quartet), 드 람띌윈 수엎곌 BEST 정늬(de Bruijn Sequence), 띌읞슀의 베타 평멎 띠 제튞(Zonal Jets)예요. 53판은 엎을 더했얎요. 듀크, 가륎시아, 룚잠가 귞늰 가우슀 죌Ʞ(Gaussian Periods), 부륎가 볎읞 현수멎곌 나선멎의 휹(Catenoid-Helicoid), 디랙의 벚튞 묘Ʞ(Belt Trick), 헬늄홀잠와 러람의 ì—°êž° 고늬 개구늬뜀(Leapfrogging Vortices), 람룩슀, 슀믞슀, 슀톀, 텃의 정사각형 분할(Squared Rectangle), 포드와 풀컀슚의 최대 흐멄 최소 절닚(Max-Flow Min-Cut), 윔슀타슀 ë°°ì—Ž(Costas Array), 바띌니, 비얎든, 칞의 묎게쀑심 분할(Barycentric Subdivision), 묎윔파디아알와 크넀저의 ë„€ ꌭ짓점 정늬(Four-Vertex Theorem), 였음러의 엘띌슀티칎(Euler's Elastica)예요. 목록 밖에서 닀음에 핮 볌 것읎에요. 믌윔프슀킀의 묌음표 핚수(1904)는 페늬 나묎륌 읎진 나묎로 펮는 계닚읎에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷읎에요. Ꞟ람레슀 표는 소수 말고 닀륞 수엎로도 섞욞 수 있얎요. 였듀늬윔(1993)에 따륎멎 2 닀음에 간격읎 작고 고륎게 흩얎진 홀수가 였는 수엎에서는 1 의 엎읎 서고, 간격읎 크멎 깚젞요. 간격의 크Ʞ륌 녞람로 걞멎 1 의 엎읎 묎너지는 자늬가 볎여요. 핎뚞슬늬 곌정의 점 대신 격자 칞마닀 지수 분포 묎게륌 두멎 마지막 통곌 슀믞Ʞ(로슀튞, 1981)가 되고, 극한 몚양읎 쌍곡선에서 (√x+√y)² 의 겜계로 바뀌얎요. @@ -2938,6 +3003,8 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. 52판에서 볞 것읎에요. 수학읎 말하는 수륌 읎번에도 팚턎의 셈윌로 재 뎀얎요. Gaussian Free Field 는 장을 120쎈 굎렀 팹널 평균 분산읎 0.6271 로 읎론 값 0.6290 곌 맞았얎요. 같은 짓Ʞ륌 16×32 에서 128×256 까지 상자 넷에 옮겚 K3 1 의 핎안선 Ꞟ읎륌 섞니, 상자가 두 배가 될 때마닀 1.44, 1.46, 1.46 제곱윌로 늘얎 SLE(4) 의 찚원 1.5 에 닀가갔얎요. Anscombe's Quartet 은 섞 녾뾌 자늬에서 120쎈씩 돌며 프레임 9,221장에서 닀섯 통계가 한 장도 빠짐없읎 귞대로였얎요. de Bruijn Sequence 는 규칙곌 찚수마닀 찜읎 ë‹€ 서로 닀륎고, 시프튞 레지슀터가 000000 을 한 번도 만듀지 않는 것을 셌얎요. 정늬가 귞늌을 막Ʞ도 했얎요. 상ꎀ 0.82 는 축을 얎떻게 늘렀도 공분산 타원을 3.1 대 1 볎닀 둥Ꞁ게 만듀지 못핎서, Anscombe's Quartet 의 원곌 별은 점의 24% 륌 회귀선 양 끝의 덩얎늬 둘로 볎낎고서알 둥Ꞁ게 ì„°ì–Žìš”. 앀슀컎의 셋짞와 ë„·ì§ž 자료에서 바깥 점 하나가 하는 음읎에요. 시간윌로 거친 확률 곌정은 팚널에서 잡음읎 됐얎요. Gaussian Free Field 의 몚드륌 였륞슈타읞-욞렌벡 곌정 귞대로 굎늬멎 가장 큰 몚드도 프레임마닀 진폭의 17% 륌 새로 받아 판 전첎가 끓었고, 몚드마닀 지수 필터로 한 번 더 걞러서알 핎안선읎 흘렀얎요. 점성윌로 잘띌 둔 파수 바로 아래가 읎번에도 귞늌을 망쳀얎요. Zonal Jets 는 쎈점성 12 에서 ê·ž 껍질의 엔슀튞로플가 강제 껍질의 닀섯 배까지 튀얎 섞 LED Ꞟ읎의 잔묌결 조각읎 났고, 80 윌로 올늬자 만분의 1 아래로 낎렀갔얎요. 50판 Inverse Cascade 가 베타 평멎을 녞람에서 뺀 빗삎도 읎것읎었얎요. 묎늬가 흐늄을 거슬러 가는 것도 볎였얎요. Zonal Jets 의 흐늄선 굜읎는 1쎈에 20 LED 씩 서쪜윌로 믞끄러지는데, 같은 쀄의 묌은 3.3 LED 씩 동쪜윌로 흘러요. 로슀비 파의 서쪜 표류예요. 빛을 묌늬대로 셈하멎 색읎 저절로 늘었얎요. Force Chains 는 400~700 nm 의 31 파장을 CIE 등색 핚수로 더핮 등색선을 칠하자 복잡도가 2.0 을 넘었고, LED 하나륌 ë„€ 점에서 재 평균하자 2.4 가 됐얎요. +53판에서 볞 것읎에요. 정늬가 수륌 못 박는 대상은 읎번에도 셈윌로 지쌜뎀얎요. Squared Rectangle 은 86,400 프레임 동안 타음 넓읎의 합읎 큰 직사각형곌 3.7e-15 안에서 같았고, Max-Flow Min-Cut 은 2,700판에서 흐멄 값곌 절닚 용량읎 한 번도 얎Ꞌ나지 않았얎요. Costas Array 는 ë°°ì—Ž 273개의 변위 241,856개가 겹치지 않았고, Four-Vertex Theorem 은 54,000 프레임 동안 ꌭ짓점읎 늘 넷 읎상의 짝수였얎요. 정늬가 귞늌에서도 서렀멎 귞늬는 셈읎 정늬의 ꌎ을 따띌알 했얎요. Belt Trick 은 늬볞 점의 맀개변수륌 늬볞을 따띌 잰 Ꞟ읎 대신 정육멎첎 쀑심에서 잰 거늬로 읜얎서, 구멎마닀 한 덩읎로 돌아 늬볞읎 서로 뚫고 지나갈 수 없얎요. Gaussian Periods 는 점을 직선윌로 옮Ʞ멎 닀시 서는 동안 가욎데로 몰렀서, 닚위귌마닀 짧은 혞륌 따띌 돌게 했얎요. 수학읎 정한 비윚은 팚널에 귞대로 듀얎가지 않았얎요. 2:1 볌록 곡선의 옆 곡률 쀑심은 팹널 밖 90 LED 에 있얎서 Four-Vertex Theorem 은 1.4:1 로 섞웠고, 현수멎은 높읎가 너비의 0.66 배륌 넘지 못핎 Catenoid-Helicoid 는 시선을 낎렞얎요. 엘띌슀티칎의 묌방욞은 높읎 31 에 너비 47 읎에요. 대칭읎 정늬의 조걎읎Ʞ도 했얎요. 평멎 소용돌읎 넷은 너비 비 0.38 아래에서 슀슀로 대칭을 깚서, 둥귌 고늬륌 귞늬는 Leapfrogging Vortices 는 거욞을 걞었얎요. 극한을 말하는 정늬는 읎번에도 LED 한 칞에서 멈췄얎요. Barycentric Subdivision 은 3 LED 아래로 가는 조각을 더 자륎지 않아서, 깊읎 4 와 5 가 3 곌 같아 녞람륌 2 에서 3 사읎로 쀄였얎요. + 목록 밖에서 31판 뒀에 핮 볌 것읎에요. 페륎믞-파슀타-욞람-칭구의 되돌아옎(1955)은 KdV Solitons 와 같은 뿌늬띌 같은 판에 두지 않아요. 목록 밖에서 33판 뒀에 핮 볌 것읎에요. 뷔퐁의 바늘(1777)은 Bertrand Paradox 와 같은 ꌎ로, ꞈ을 넘는 바늘의 몫읎 2/π 로 몚읎는 막대와 핚께 섞욞 수 있얎요. 토칎륎슀킀의 ë°©(1995)은 거욞로 된 닀각형 방에서 한 점의 빛읎 닀륞 한 점에만 닿지 못하는 방읎띌 Penrose Room 의 ꎑ선 추적을 귞대로 썚요. 프늬드뚌곌 슀윔띌(1987)는 둥귌 원 셋윌로는 볎로메였 고늬륌 만듀 수 없닀는 것을 볎였는데, 원을 밀얎 넣을 때 얎디서 부딪히는지가 귞늌읎 돌요. 정렬 넀튞워크의 시간 t 의 순엎을 점윌로 찍윌멎 대각선에서 원판을 지나 반대 대각선윌로 도는 점 구늄읎 돌요. ê·ž 원판읎 2007년 녌묞읎 낎닀볞 아륎킀메데슀 법칙읎에요. 간격읎 2k 읞 소수 쌍의 수는 Goldbach Comet 곌 같은 FFT 로 섞고, k 마닀 하디-늬틀우드 읞수로 갈늬는 쀄Ʞ 귞늌읎 돌요. @@ -2978,6 +3045,8 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. 목록 밖에서 52판 뒀에 핮 볌 것읎에요. 같은 가우슀 자유장 위에 각도마닀 흐늄선을 귞윌멎 한 점에서 부채처럌 갈띌지는 SLE(κ) 곡선듀읎 서요. 제읎슚 밀러와 슀윧 셰필드의 허수 Ʞ하(2016)예요. ê· ì—Ž 끝의 응력장(조지 얎윈, 1957)을 Force Chains 의 백색ꎑ 등색선 표로 칠하멎, 균엎읎 자띌는 동안 ê·ž 끝에 나비 몚양 묎늬가 따띌가요. 마테읎칎와 플잠몚늬슀는 같은 녌묞에서 닀섯 수 요앜읎 같은 상자 귞늌 옆에서 점듀읎 한 덩얎늬에서 두 덩얎늬로 갈띌졌닀 몚읎는 것도 볎였얎요. 드 람띌윈 토러슀는 4×4 판 위에 몚든 2×2 묎늬가 ꌭ 한 번씩 나였는 2찚원 짝읎띌, 팚널을 ê·ž 판윌로 덮고 찜을 상하좌우로 굎렀 볌 만핎요. 베타 평멎에서 마찰을 더 낮추고 잠재 소용돌읎도 q + βy 의 등고선윌로 칠하멎, 동쪜 제튞가 섞임을 막는 벜읎 되는 PV 계닚(데읎비드 드늬첌, 마읎큎 맀킚타읎얎, 2008)읎 볎여요. +목록 밖에서 53판 뒀에 핮 볌 것읎에요. 극소곡멎의 연ꎀ족은 엔넀퍌 멎곌 셰륎크 멎에도 있얎서, Catenoid-Helicoid 의 첎컀와 법선 셈윌로 닀륞 극소곡멎의 휚을 귞늎 수 있얎요. 벚튞 묘Ʞ의 H(1, t) 륌 3찚원 구멎의 입첎 사영윌로 옆에 찍윌멎 360° 에 −1 에 닿았닀 720° 에 1 로 돌아였는 점읎 두 ê²¹ 덮개륌 바로 볎여요. Leapfrogging Vortices 의 거욞을 풀멎 너비 비 0.382 아래에서 개구늬뜀읎 슀슀로 대칭을 깚고 흩얎지는 애치슚(2000)의 불안정을 녾뾌 하나로 볎음 수 있얎요. 정사각형 분할은 쌍대 귞묌을 돌늬멎 90도 돌아간 분할읎 나와서, 두 귞묌을 반씩 섞얎 직사각형읎 옆윌로 눕는 장멎읎 돌요. 최소 절닚은 평멎 귞묌에서 쌍대 귞래프의 최닚 겜로띌, 닀익슀튞띌 묌결읎 왌쪜 벜에서 였륞쪜 벜윌로 번지며 웅덩읎 테두늬와 같은 선을 Ꞌ는 것을 볎음 수 있얎요. 곚롬 자는 윔슀타슀 배엎의 성질을 1찚원에 둔 것읎고, 찚수 32 와 33 은 윔슀타슀 배엎읎 하나도 알렀지지 않은 가장 작은 찚수예요. 묎게쀑심 분할에서 둥귌 자식을 고륎는 확률의 묞턱은 2/9 와 1/3 사읎에 있얎서, ê·ž 사읎륌 잘게 재멎 납작핎지는 걷Ʞ와 둥Ꞁ게 뚞묎는 ê±·êž°ê°€ 갈늬는 자늬륌 찟을 수 있얎요. 분늬선 바깥에서 넘얎 도는 진자는 곡률 부혞가 바뀌지 않는 비굎곡 엘띌슀티칎띌서, 녞람로 분늬선을 걎너멎 였음러의 1744년 귞늌의 아홉 갈래가 한 판에 읎얎젞요. 공간 곡선의 ë„€ ꌭ짓점 정늬(섞디흐, 1992)도 있얎요. + ### 화장품 팝업 죌제 6 윌로 쓎 질감곌 닀음에 핮 볌 질감읎에요. 29판읎 처음 넷을 썌얎요. 팔레튾 나읎프로 펮 바륎는 크늌(Cream Swirl), 구슬읎 뜬 저곌 빛 귞묌(Serum Gel), 쏟아젞 쌓읎는 Ꞁ늬터(Glitter Fall), 퍌프가 눌렀닀 든 압축 파우더(Loose Powder)예요. 넷 ë‹€ K1 읎 색상환 한 바퀎띌서, 색을 돌늬멎 죌황곌 녞랑읎 갈색읎나 올늬람로 볎읎는 자늬가 있었얎요. Serum Gel 은 ê·ž 둘레에서만 밝Ʞ륌 0.62 와 0.3 에서 0.92 와 0.68 로 올렞얎요. 밝은 바탕은 반짝임을 지웠얎요. 200 위에 55 륌 더한 반짝임은 거의 안 볎여서 Glitter Fall 은 얎두욎 바탕에 섰고, 흰 바탕곌 ꞈ색 한 쀄은 Loose Powder 의 아래 띠로만 썌얎요. 크늌곌 저은 가장자늬 0.028 곌 0.079 로 연구에서 가장 번지는 귞늌읎에요. ê·ž 둘읎 가장자늬 축의 아래 끝을 옮겚서 지도에 새 빈 칞읎 났얎요. 크늌은 빛만 움직여서는 움직임읎 0.019 에 뚞묌렀얎요. 결읎 깊얎알 빛읎 돌 때 바뀌는 LED 가 생겚요. 비누막읎 터질 때 구멍 가장자늬가 테음러와 컬늭의 속도(1959, 1960)로 고륎게 벌얎지며 묌방욞 테륌 낚Ʞ는 것은 Soap Film 의 끝을 늘읞 귞늌읎에요. 큎렌징 였음읎 묌을 만나 가장자늬부터 우윳빛윌로 유화되는 것은 Blotting Paper 처럌 LED 마닀 한 번만 바뀌는 셈윌로 귞늎 만핎요. @@ -3074,6 +3143,8 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. 52판 뒀에 핮 볌 것읎에요. 바닥의 투석Ʞ가 혞박을 ꌭ대Ʞ까지 쏘아 올렞닀 떚얎뜚늬멎 였늄곌 떚얎짐읎 한 포묌선에 닎겚 128쀄을 두 번 썚요. 배겜을 휘게 하는 Banshee 의 고늬로는 묌에 잠ꞎ 종탑의 종읎 ìšžë € 수멎곌 달읎 음렁읎는 장멎을 귞늎 만핎요. 표볞 병 속에 눈알 대신 뛰는 심장을 넣윌멎 선반을 두드멮 때마닀 박동읎 빚띌젞요. Lost Head 의 굎러간 뚞늬륌 닀륞 핎곚읎 죌워 쓰고 달아나멎, 같은 몞 짜Ʞ로 뚞늬 하나륌 두고 쫓는 장멎읎 돌요. Creaking Door 의 ꎑ선 셈윌로는 찜밖 번개가 찜틀곌 나뭇가지 귞늌자륌 방바닥에 던지는 장멎을 귞늎 수 있얎요. +10월 26음부터 할로윈 몫은 쉬얎요. 찜작자가 할로윈 Ʞ념 팚턎은 읎제 ê·žë§Œ 만듀얎도 된닀고 했얎요. 53판은 ê·ž 전에 닀섯을 ë‹€ 지었지만 넣지 않았고, 바퀮 람랜치의 컀밋(dad2914, fce8124, 0e7a67a, c468eb3, 6cf7431)에만 낚아요. 늪 위 저택읎 ꞈ을 따띌 갈띌젞 가띌앉는 ì–Žì…” 가의 몰띜, 눈빛 고늬가 뱀 뚞늬칌을 돌로 굳히는 메두사, 핎곚 말읎 였륎낎늬는 빈 유원지의 회전목마, 장지묞에 목 귞늌자가 늘얎나는 로쿠로쿠비, 닭닀늬로 걷는 바바 알가의 였두막읎에요. + ## 닀시 돌늬Ʞ `web/` 에서 싀행핎요. `web/node_modules` 가 있얎알 핎요. From ce083d34af47350172cfe7ee5a526f3329b410b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 03:08:10 +0000 Subject: [PATCH 14/14] =?UTF-8?q?53=ED=8C=90:=20index.html=20=EC=9D=84=204?= =?UTF-8?q?38=EA=B0=9C=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20=EC=A7=93=EB=8A=94?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Yb8D9UxrifXqrXju5CVbqY --- incoming/pattern-study/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index 82e749540..bfb3c504b 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

팹턮 연구

- - + +