From b1cc9bf264ba7efd0f239f9f12b27ffbaabf6924 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 05:15:58 +0000 Subject: [PATCH 01/31] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=EC=97=B0=EA=B5=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=2026?= =?UTF-8?q?=ED=8C=90=20=EC=83=88=20=ED=8C=A8=ED=84=B4=20=EC=97=B4=20?= =?UTF-8?q?=EA=B0=9C=EA=B0=80=20=EC=84=A0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BvYAVbR9mNHMMAp7tN8evZ From 650fb60d6e38637812e52839d352863261f876b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 05:21:35 +0000 Subject: [PATCH 02/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20=EC=96=BC=EC=9D=8C=C2=B7=EA=B7=B9=EC=A7=80?= =?UTF-8?q?=EC=99=80=20=EB=AF=B8=EC=83=9D=EB=AC=BC=20=EA=B0=88=EB=9E=98?= =?UTF-8?q?=EB=A5=BC=20=EC=97=B0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/families.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/incoming/pattern-study/families.json b/incoming/pattern-study/families.json index a863838be..2937fc977 100644 --- a/incoming/pattern-study/families.json +++ b/incoming/pattern-study/families.json @@ -46,5 +46,7 @@ "construction": { "label": "공사·건설", "color": "#ffee00" }, "pottery": { "label": "도자기", "color": "#b05030" }, "farm": { "label": "농사·밭", "color": "#449911" }, - "volcano": { "label": "화산", "color": "#bb2200" } + "volcano": { "label": "화산", "color": "#bb2200" }, + "polar": { "label": "얼음·극지", "color": "#00ffff" }, + "microbe": { "label": "미생물", "color": "#aa00aa" } } From 790eda3bda7d1e7b8873d717b559507cd6bc79dc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 05:52:32 +0000 Subject: [PATCH 03/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Geyser?= 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_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/patterns/geyser.js | 449 ++++++++++++++++++++++ 1 file changed, 449 insertions(+) create mode 100644 incoming/pattern-study/patterns/geyser.js diff --git a/incoming/pattern-study/patterns/geyser.js b/incoming/pattern-study/patterns/geyser.js new file mode 100644 index 000000000..fe0d11751 --- /dev/null +++ b/incoming/pattern-study/patterns/geyser.js @@ -0,0 +1,449 @@ +// ===== Patternflow pattern ===== +// Title: Geyser +// Family: volcano +// Idea: A Strokkur-style geyser in a snowy geothermal field, cut open below the ground: steam bubbles climb the conduit under a turquoise pool, the pool swells into a glassy dome and bursts into a white column up to the top of the panel, spray falls back, the steam drifts off on the wind and the pool drains, gurgles and fills again. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 26, after Strokkur in the Haukadalur geothermal field, Iceland +// Round: 26 +// =============================== +// @matrix 64x128 +// @knobs Light=0..1, Speed=0.2..3, Power=0..1, Wind=-1..1 +// +// Knob 1: Light. A low winter sun at noon at 0, the blue hour at 0.5, and at 1 a moonlit night with green aurora over the field and the visitors' phones glowing. +// Knob 2: Speed. At 2 one cycle, dome to dome, takes about thirteen seconds; at 0.2 ten times longer. +// Knob 3: Power. At 0 one jet that just reaches the top of the panel. Turned up, the dome swells bigger and bursts sooner, the column widens and punches out of the top, the jet lasts longer in two surges and the steam cloud grows. +// Knob 4: Wind. At the start a light breeze drifts the steam to the right; turned left it drops to calm and then carries it left, turned right it tears it off to the right. The column and the spray lean with it. +// Buttons: K1 tap sets it off now, if it is filling or draining: the big bubble leaves the chamber and the dome and the jet follow. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, round, sin, sqrt, exp, abs, hypot } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const mix = (a, b, t) => a + (b - a) * t; +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +const rnd = () => Math.random(); +const sc = (c, k) => c.map((v, n) => v * (k.length ? k[n] : k)); +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 vn(x, y) { + const xi = floor(x), yi = floor(y), u = x - xi, v = y - yi, su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v); + const a = hash(xi, yi), b = hash(xi + 1, yi), c = hash(xi, yi + 1); + return a + (b - a) * su + (c - a) * sv + (a - b - c + hash(xi + 1, yi + 1)) * su * sv; +} + +// Rows: the pool's brim, the bowl's floor, the snow on the flat, the horizon, +// the drained water level; the vent's column; the chamber's centre and radii. +const [BRIM, BOWL, GY, HZ, LOW, C] = [71, 80, 75, 61, 103, 32]; +const [KX, KY, KRX, KRY] = [30.5, 116, 10, 6.5]; +// Gravity in rows per second squared. +const G = 150; +// Seconds at Speed 2: refill, the big bubble's climb, the dome, the drain. +const [HEAT, RISE, DOME, FALL] = [7.5, 0.7, 0.45, 3.5]; + +// The ground: the bowl, the sinter mound with a lip at the brim, the flat. +const SURF = new Float32Array(W).map((_, x) => { + const dx = abs(x + 0.5 - C); + if (dx < 12) return BRIM + (BOWL - BRIM) * (1 - (dx / 12) ** 2) ** 0.7; + if (dx < 27) return BRIM - 0.8 * (1 - sm(12, 16, dx)) + (GY - BRIM) * sm(14, 27, dx); + return GY + 0.8 * (vn(x * 0.2, 3) - 0.5); +}); +// The conduit wanders a little and narrows at row 95. +const CXA = new Float32Array(H).map((_, y) => C - 0.5 + 0.8 * sin(y * 0.23) + 0.4 * sin(y * 0.61 + 1)); +const CHW = new Float32Array(H).map((_, y) => 2 + 0.4 * sin(y * 0.37) - 0.6 * exp(-(((y - 95) / 3) ** 2))); +// The hill behind the field on the right, the ice cap on the horizon at the left. +const HILL = new Float32Array(W).map((_, x) => HZ - 20 * sm(30, 66, x) ** 0.9 - 1.2 * vn(x * 0.35, 4)); +const CAP = new Float32Array(W).map((_, x) => HZ - 1.5 - 2.6 * sqrt(max(0, 1 - ((x + 0.5 - 13) / 17) ** 2))); +// Cracks feeding ground water to the chamber. +const FIS = [[[0, 94], [7, 98], [15, 104], [22, 112]], [[63, 103], [55, 108], [47, 113], [40, 117]]]; +// The beds, top down, colour and rows: sinter, geyserite, ochre, sinter, rust, +// sulphur, grey clay, ochre, dark rust, tuff, sulphur, dark clay, basalt. +const LAY = [[206, 200, 186, 3], [176, 172, 166, 2], [172, 122, 54, 2], [196, 188, 172, 2], [150, 70, 40, 3], [206, 182, 62, 1], [126, 116, 108, 4], [162, 112, 52, 2], [120, 56, 36, 2], [104, 92, 84, 4], [200, 176, 64, 1], [84, 74, 72, 5], [58, 58, 68, 99]]; +const SULF = [214, 190, 70], RUST = [158, 76, 42], LINE = [214, 210, 196], SNOW = [214, 222, 236]; +const MAT = [[96, 116, 52], [214, 112, 38], [212, 186, 70]]; +const COOL = [24, 62, 150], HOT = [44, 176, 204], FOAM = [212, 232, 236]; +const JACK = [[200, 46, 40], [232, 192, 44], [40, 92, 200], [44, 44, 52], [58, 140, 92], [222, 112, 34], [150, 60, 150]]; + +// Noon, blue hour, night: sky overhead and at the horizon, daylight, steam lit +// and in shade, the cut rock, the water. +const LIGHT = [ + [0, [20, 62, 152], [122, 156, 198], [0.95, 0.93, 0.88], [236, 234, 226], [118, 134, 168], 1, 1], + [0.5, [10, 24, 74], [66, 88, 148], [0.42, 0.48, 0.7], [158, 168, 212], [50, 60, 108], 0.85, 0.78], + [1, [3, 7, 24], [12, 26, 54], [0.24, 0.3, 0.42], [132, 154, 168], [30, 42, 70], 0.62, 0.6], +]; +function lightAt(g) { + const a = LIGHT[g > 0.5 ? 1 : 0], b = LIGHT[g > 0.5 ? 2 : 1], u = (g - a[0]) / (b[0] - a[0]); + const m = (k) => (a[k].length ? a[k].map((v, c) => mix(v, b[k][c], u)) : mix(a[k], b[k], u)); + return { top: m(1), low: m(2), amb: m(3), lit: m(4), dark: m(5), rock: m(6), water: m(7), sun: 1 - sm(0, 0.4, g), ag: sm(0.15, 0.45, g) * (1 - sm(0.55, 0.85, g)), moon: sm(0.6, 0.95, g), stars: sm(0.4, 0.85, g), aur: sm(0.6, 0.95, g) }; +} + +export function setup(params) { + const F32 = (n) => new Float32Array(n); + const S = (params.s = { + f: F32(N * 3), bg: F32(N * 3), rock: F32(N * 3), kind: new Uint8Array(N), sky: new Uint8Array(N), fu: F32(N), D: F32(N), CB: F32(N), + t: 0, ph: 0, pt: HEAT - 1.25, wl: BRIM, wl0: LOW, T: 0.9, T0: 0.25, hv: 0, dh: 0, bw: 6, key: -1, ca: 99, ja: 99, + bub: [], par: [], spr: [], puffs: [], pop: 0, nb: 0, np: 0, ns: 0, nv: 0, ox: 0, oy: 0, big: -1, + }); + const { kind, fu, rock } = S; + // Kinds: 0 the view, 1 the bowl, 2 rock, 3 conduit, 4 crack, 5 chamber. + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, X = x + 0.5, Y = y + 0.5; + let k = Y >= SURF[x] ? 2 : abs(X - C) < 12 && Y >= BRIM ? 1 : 0; + if (k === 2 && ((X - KX) / KRX) ** 2 + ((Y - KY) / KRY) ** 2 < 1 + 0.5 * (vn(X * 0.35, Y * 0.35) - 0.5)) k = 5; + else if (k === 2 && y < KY && abs(X - CXA[y]) < CHW[y]) k = 3; + kind[i] = k; + if (!k && Y < min(HILL[x], CAP[x])) S.sky[i] = 1; + } + for (const p of FIS) + for (let j = 1, len = 0; j < p.length; j++) { + const [x0, y0] = p[j - 1], [x1, y1] = p[j], n = hypot(x1 - x0, y1 - y0) * 3; + for (let u = 0; u <= n; u++) { + const i = floor(mix(y0, y1, u / n)) * W + floor(mix(x0, x1, u / n)); + if (kind[i] === 2) (kind[i] = 4), (fu[i] = len + u / 3); + } + len += n / 3; + } + // The cut rock: beds bend with the ground, a silica lining and sulphur and + // rust stains round every water way, columnar basalt below, bacterial mats + // on the mound and snow on the flat. + const cav = []; + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, X = x + 0.5, Y = y + 0.5, dx = abs(X - C), s = SURF[x]; + if (kind[i] >= 3) cav.push(i); + if (kind[i] !== 2) continue; + let d = Y - s + 2.4 * (vn(x * 0.08, y * 0.06 + 9) - 0.5) + 0.05 * (X - C), j = 0, dist = 9, gr = 0.9 + 0.18 * hash(x, y + 77); + while (d > LAY[j][3] && j < LAY.length - 1) (d -= LAY[j][3]), j++; + let c = LAY[j]; + if (j === LAY.length - 1) { + const jx = (X + 3 * vn(1.7, Y * 0.12)) / 6; + gr *= (jx - floor(jx) < 0.17 ? 0.6 : 1) * (0.85 + 0.3 * hash(floor(jx), 5)); + } + for (let v = -5; v <= 5; v++) + for (let u = -5; u <= 5; u++) { + const xx = x + u, yy = y + v, k = xx >= 0 && xx < W && yy < H ? kind[yy * W + xx] : 2; + if (k !== 2 && k) dist = min(dist, hypot(u, v) * (k === 4 ? 2.5 : 1)); + } + if (dist < 1.5) c = LINE; + else if (dist < 5) c = c.map((v, n) => mix(v, (dist < 3 ? SULF : RUST)[n], (5 - dist) * 0.17)); + if (dx > 13 && dx < 26 && Y - s < 1.4) c = MAT[floor(cl(vn(X * 0.45, 2) * 2.2 - 0.3, 0, 2.99))]; + if (dx >= 26 && Y - s < 1.5) c = SNOW; + for (let n = 0; n < 3; n++) rock[i * 3 + n] = c[n] * gr; + } + S.cav = Int32Array.from(cav); + // Visitors behind the rope on both sides, in their parkas. + S.ppl = []; + for (const [x0, x1] of [[2, 18], [46, 62]]) + for (let x = x0 + rnd() * 2; x < x1; x += 2.2 + rnd() * 2.4) + S.ppl.push({ x: floor(x), h: rnd() < 0.3 ? 5 : 4, c: JACK[floor(rnd() * 7)], hat: rnd() < 0.5, dl: rnd() * 0.9, up: rnd() < 0.7 }); +} + +// The breeze in columns per second at row y, stronger aloft. +const wind = (S, y) => (6 + 16 * S.w) * (0.35 + 0.65 * sm(80, 8, y)); +function puff(S, x, y, r, a, vy, life, gr, rmax) { + if (S.puffs.length > 210) S.puffs.shift(); + S.puffs.push({ x, y, r, a, vy, rise: -(1 + 2.5 * rnd()), life, gr, rmax, age: 0, ph: rnd() * 7 }); +} +const spray = (S, x, y, vx, vy) => S.spr.length < 700 && S.spr.push({ x, y, vx, vy, life: 2.5 }); + +function enter(S, ph) { + Object.assign(S, { ph, pt: 0, wl0: S.wl, T0: S.T }); + if (ph === 2) S.ca = 0; + if (ph === 3) { + // The dome tears open in a ring of spray. + S.ja = 0; + for (let n = 0; n < 40; n++) spray(S, C + (rnd() - 0.5) * S.bw * 1.6, BRIM - S.dh * rnd(), (rnd() - 0.5) * 70, -(40 + 90 * rnd())); + for (let n = 0; n < 6; n++) puff(S, C + (rnd() - 0.5) * 12, BRIM - 3 - 4 * rnd(), 3 + rnd() * 2, 0.6, -(10 + 10 * rnd()), 8 + 3 * rnd(), 1.5, 12 + 6 * S.P); + } +} + +function step(S, d, hold) { + const P = S.P, Dj = 1.1 + 1.1 * P, Rl = RISE - 0.35 * P, Dl = DOME - 0.2 * P, t = (S.t += d); + if (!hold) (S.pt += d), (S.ca += d), (S.ja += d); + if (S.pt >= [HEAT, Rl, Dl, Dj, FALL][S.ph]) enter(S, (S.ph + 1) % 5); + const { ph, pt } = S; + // The water level and how hot the water is. + if (ph === 0) (S.wl = mix(S.wl0, BRIM, sm(0, 6, pt))), (S.T = mix(S.T0, 1, sm(0, HEAT, pt))); + else if (ph === 1) (S.wl = mix(S.wl0, BRIM, sm(0, 0.5, pt))), (S.T = mix(S.T0, 1, sm(0, 0.4, pt))); + else if (ph === 2) S.wl = BRIM; + else if (ph === 3) (S.wl = BRIM), (S.T = 1 - 0.65 * sm(0, Dj, pt)); + else (S.wl = mix(S.wl0, LOW, sm(0.2, 2.6, pt)) + 1.6 * exp(-pt * 1.1) * sin(pt * 10) * sm(0.3, 0.8, pt)), (S.T = mix(S.T0, 0.25, sm(0, FALL, pt))); + // The pool heaves before it goes, the big bubble climbs, the dome swells. + S.hv = ph === 0 ? (1.1 + P) * sm(4.5, 7.5, pt) * sin(t * 4.2) ** 2 : ph === 1 ? 1 + (2.2 + P) * sm(0, Rl, pt) : 0; + S.big = ph === 1 ? mix(KY - KRY + 1, BOWL - 2, sm(0, Rl, pt) ** 1.4) : -1; + S.bw = 7.5 + 4 * P; + S.dh = ph === 2 ? (9 + 7 * P) * sm(0, Dl, pt) ** 0.8 + 0.5 * sin(pt * 30) : 0; + + // Steam bubbles grow as they climb from the chamber's floor. They collapse + // in cooler water; once it is hot through they reach the pool and pop. + for (S.nb += d * (1.5 + 18 * S.T * S.T) * (ph === 3 ? 3 : ph === 4 ? 0.3 : 1) * (1 + 0.8 * P); S.nb >= 1; S.nb--) + if (S.bub.length < 160) S.bub.push({ x: KX + (rnd() - 0.5) * KRX * 1.3, y: KY + KRY * (0.25 + 0.5 * rnd()), r: 0.4, v: 3 + 4 * rnd() }); + const cr = ph > 0 && ph < 4 ? -9 : mix(106, BOWL - 4, sm(0.35, 0.92, S.T)); + for (const b of S.bub) { + const yi = cl(floor(b.y), 0, H - 1), inC = b.y < KY - KRY + 1.5; + if (inC) (b.x += (CXA[yi] + 0.5 - b.x) * min(1, d * 8)), (b.v = min(32, b.v + 26 * d)); + else (b.x += (KX - b.x) * d * 0.4 * sm(KY, KY - KRY, b.y) + sin(t * 3 + b.y) * d), (b.v = min(14, b.v + 8 * d)); + b.y -= b.v * d * (ph === 3 ? 1.8 : 1); + b.r = min(inC ? (b.y < BOWL ? 1.2 : CHW[yi] - 0.3) : 2.3, b.r + d * (0.3 + 0.7 * S.T)); + if (b.y < cr || (S.big > 0 && b.y > S.big && b.y < S.big + 10)) b.dead = 1; + else if (b.y <= S.wl + 0.5) { + b.dead = 1; + if (S.wl < BOWL) { + S.pop = min(1.5, S.pop + 0.15 * b.r); + if (rnd() < 0.5) puff(S, b.x + (rnd() - 0.5) * 8, S.wl - 2, 1.8, 0.3, -6, 3 + 2 * rnd(), 1, 5); + } + } + } + S.bub = S.bub.filter((b) => !b.dead); + S.pop *= exp(-3 * d); + + // The jet: slabs of water fly up and spread; past the top they break up. + if (ph === 3) { + const u = pt / Dj, v = sqrt(2 * G * (64 + 46 * P)) * (1 - 0.25 * sm(0.7, 1, u)) * (1 - 0.6 * sm(0.45, 0.8, P) * exp(-(((u - 0.48) / 0.09) ** 2))); + for (S.np += d * v * 0.6; S.np >= 1; S.np--) S.par.push({ x: C + (rnd() - 0.5) * 1.2, y: BRIM - 1, vy: -v * (0.95 + 0.1 * rnd()), w: (3.5 + 3.5 * P) * (0.85 + 0.3 * rnd()), a: 1, ap: 0 }); + } + for (const q of S.par) { + q.vy += G * d; + q.y += q.vy * d; + q.x += wind(S, q.y) * 0.35 * d; + q.w += d * (4.5 + 4.5 * P) * (q.ap ? 2 : 1); + if (!q.ap && q.vy > -25) { + q.ap = 1; + for (let n = 0; n < 2; n++) spray(S, q.x + (rnd() - 0.5) * q.w * 2, q.y, (rnd() - 0.5) * 64, q.vy + (rnd() - 0.6) * 30); + } + if (rnd() < d * 3) { + const sd = rnd() < 0.5 ? -1 : 1; + spray(S, q.x + sd * q.w, q.y, sd * (8 + 26 * rnd()), q.vy * 0.75); + } + if (q.ap) q.a -= d * 1.6; + if (q.y > BRIM || q.a <= 0) q.dead = 1; + } + S.par = S.par.filter((q) => !q.dead); + // Steam boils off the column while it stands and a little after. + if (S.ja < Dj + 0.8 && S.par.length) + for (S.ns += d * (40 + 40 * P); S.ns >= 1; S.ns--) { + const q = S.par[floor(rnd() * S.par.length)]; + puff(S, q.x + (rnd() - 0.5) * q.w * 1.8, q.y, 2.5 + q.w * 0.6, 0.55 + 0.25 * rnd(), max(-30, q.vy * 0.3), 10 + 4 * rnd() + 3 * P, 2 + 1.5 * rnd(), 17 + 7 * P); + } + for (const s of S.spr) { + s.vy = min(90, s.vy + G * 0.85 * d); + s.vx *= 1 - 0.5 * d; + s.x += (s.vx + wind(S, s.y) * 0.5) * d; + s.y += s.vy * d; + s.life -= d; + const x = floor(s.x), y = floor(s.y); + if (s.life < 0 || x < 0 || x >= W || y >= H || (y >= 0 && S.kind[y * W + x] && s.vy > 0)) s.dead = 1; + } + S.spr = S.spr.filter((s) => !s.dead); + + // Gentle steam off the pool, and from small vents out on the field. + const wet = S.wl < BOWL - 1; + for (S.nv += d * (2 + (wet ? 4 + 5 * S.T : 1) * (1 + P)); S.nv >= 1; S.nv--) + if (rnd() < 0.7) puff(S, C + (rnd() - 0.5) * (wet ? 20 : 6), (wet ? S.wl : BOWL) - 1, 1.6 + rnd(), 0.22 + 0.2 * rnd(), -(5 + 3 * rnd()), 4 + 2 * rnd(), 1, 6); + else { + const v = [[7, 67], [52, 64], [21, 63], [60, 70]][floor(rnd() * 4)]; + puff(S, v[0] + rnd() - 0.5, v[1], 1.1, 0.3, -(4 + 2 * rnd()), 3.5 + 2 * rnd(), 0.8, 4); + } + for (const q of S.puffs) { + q.age += d; + q.vy += (q.rise - q.vy) * 1.2 * d; + q.y += q.vy * d; + q.x += (wind(S, q.y) + 1.2 * sin(q.age * 1.1 + q.ph)) * d; + q.r = min(q.rmax, q.r + q.gr * d); + } + S.puffs = S.puffs.filter((q) => q.age < q.life && q.y + q.r > -6 && q.x > -26 && q.x < W + 26); + S.ox += wind(S, 25) * 0.2 * d; + S.oy -= 0.6 * d; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + [S.g, S.P, S.w] = [cl(k[0], 0, 1), cl(k[2], 0, 1), cl(k[3], -1, 1)]; + if (!S.init) { + // Open on a steaming field with the last cloud still high up; the pool holds its phase. + for (let n = 0; n < 18; n++) puff(S, -10 + n * 4 + rnd() * 4, 8 + 24 * rnd(), 6 + 4 * rnd(), 0.4 + 0.15 * rnd(), -2, 15 + 4 * rnd(), 0.8, 16), (S.puffs[n].age = 2 + 4 * rnd()); + for (let n = 0; n < 150; n++) step(S, 1 / 30, 1); + S.bub.length = S.pop = S.t = 0; + S.init = 1; + } + if (input.btnPressed && input.btnPressed[0] && (S.ph === 0 || S.ph === 4)) enter(S, 1); + step(S, (cl(dt || 0, 0, 0.1) * cl(k[1], 0.2, 3)) / 2); +} + +// The view and the cut rock, kept until the light changes. +function backdrop(S) { + const L = (S.L = lightAt(S.g)), BG = S.bg, R = S.rock, { amb, top, low } = L; + S.key = round(S.g * 60); + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, X = x + 0.5, Y = y + 0.5, o = i * 3, k = S.kind[i]; + let c; + if (k >= 2) c = k === 2 ? [R[o], R[o + 1], R[o + 2]] : [26, 28, 36]; + else if (k === 1) { + // The far wall of the bowl, wet sinter. + const v = (0.75 + 0.3 * hash(x, y)) * (0.8 + 0.25 * vn(X * 0.5, Y * 0.9)); + c = [62 * v, 86 * v, 90 * v]; + } + if (c) c = sc(c, L.rock); + else if (S.sky[i]) { + const sd = hypot(X - 9, Y - 53), md = hypot(X - 13, Y - 15), v = (Y / HZ) ** 1.3; + const sg = L.sun * (0.6 * exp(-sd / 9) + (sd < 1.7 ? 1.1 : 0)), ag = L.ag * exp(-(((Y - 58) / 9) ** 2)) * (1 - X / 80); + const mg = L.moon * ((md < 2.4 ? 0.9 : 0) + 0.12 * exp(-md / 5)), st = hash(x, y) < 0.013 && Y < 52 ? L.stars * (90 + 140 * hash(y, x)) : 0; + c = top.map((a, n) => mix(a, low[n], v) + sg * [255, 206, 130][n] + ag * [110, 92, 168][n] + mg * [228, 230, 238][n] + st * [0.85, 0.9, 1][n]); + } else if (Y < HZ) { + // The ice cap; snow and scree on the hill. + const sn = vn(x * 0.16, y * 0.3) + 0.35 * vn(x * 0.5, y * 0.8) - 0.01 * (Y - HILL[x]) > 0.55; + c = Y < HILL[x] ? [222, 232, 246].map((a, n) => a * amb[n] * 0.92 + low[n] * 0.1) : sc(sn ? sc(SNOW, 0.74 + 0.004 * x) : [64, 60, 62], amb); + } else if (Y < HZ + 1.5) c = sc([92, 100, 118], amb); + else { + // The flat field: snow, and bare warm ground near the springs. + const f = (Y - HZ) / (GY - HZ), b = 0.7 * sm(0.6, 0.66, vn((X * 0.1) / (0.35 + f), y * 0.6)) * sm(27, 10, abs(X - C)); + c = SNOW.map((a, n) => mix(a * (0.8 + 0.2 * f), [112, 92, 70][n], b) * amb[n]); + } + for (let n = 0; n < 3; n++) BG[o + n] = c[n]; + } +} + +// Add an elliptical blob to a density field. +function splat(A, cx, cy, rx, ry, a) { + const y1 = min(H - 1, cy + ry), x0 = max(0, floor(cx - rx)), x1 = min(W - 1, cx + rx); + for (let y = max(0, floor(cy - ry)); y <= y1; y++) { + const dy = (y + 0.5 - cy) / ry, dd = 1 - dy * dy; + for (let x = x0; x <= x1; x++) { + const dx = (x + 0.5 - cx) / rx, e = dd - dx * dx; + if (e > 0) A[y * W + x] += a * e; + } + } +} + +let F; +function put(x, y, c, a = 1) { + (x = floor(x)), (y = floor(y)); + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + for (let n = 0; n < 3; n++) F[o + n] = mix(F[o + n], c[n], a); +} + +// Shade a density field over the frame, lit on the side facing the light. +function shade(S, A, k0, lit, dark, tint, jet) { + for (let i = 0; i < N; i++) { + const v = A[i]; + if (v < 0.015) continue; + const x = i & 63, y = i >> 6, o = i * 3; + const tex = jet ? vn(x * 0.5, (y + S.t * 10) * 0.25) : vn(x * 0.18 - S.ox, y * 0.18 - S.oy); + const al = min(0.95, 1 - exp(-v * (k0 + 1.6 * tex))), gx = v - A[x > 1 && y > 1 ? i - (jet ? 2 : 2 * W + 2) : i]; + const sh = cl(0.52 + (jet ? 0.9 : 0.5) * gx + 0.45 * (tex - 0.5), 0.12, 1.02), tt = jet ? sm(BRIM - 16, BRIM, y) * 0.6 : 0; + for (let n = 0; n < 3; n++) F[o + n] = mix(F[o + n], mix(mix(dark[n], lit[n], sh), jet ? tint[n] : 0, tt), al); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.init) return; + if (S.key !== round(S.g * 60)) backdrop(S); + F = S.f; + F.set(S.bg); + const L = S.L, { t, kind, D, CB, ph, wl, T, P } = S, amb = L.amb, Lw = L.water, Lr = L.rock; + + // Aurora: a green hem, rays rising into violet. + if (L.aur > 0.01) + for (let k = 0; k < 2; k++) + for (let x = 0; x < W; x++) { + const yb = 30 + 8 * k + 5 * sin(x * 0.08 + t * 0.22 + k * 2.1) + 8 * (vn(x * 0.07 + t * 0.12, k * 7) - 0.5); + const I = L.aur * sm(0.3, 0.75, vn(x * 0.09 - t * 0.1 + k * 3, t * 0.04 + k)) * (0.5 + 0.5 * vn(x * 0.7 + t * 0.5, k * 11 + t * 0.3)); + if (I < 0.02) continue; + for (let y = max(0, floor(yb - 26)); y <= yb; y++) { + const i = y * W + x, u = (yb - y) / 26, e = I * (u < 0.08 ? u / 0.08 : exp(-u * 2.2)) * 0.9, v = sm(0.35, 1, u); + if (S.sky[i]) (F[i * 3] += mix(40, 96, v) * e), (F[i * 3 + 1] += mix(214, 60, v) * e), (F[i * 3 + 2] += mix(120, 176, v) * e); + } + } + + // The visitors behind the rope; once the dome rises most hold up a phone. + for (const p of S.ppl) { + const hy = GY - p.h, jc = sc(p.c, amb); + put(p.x, GY - 1, sc([30, 30, 36], amb)); + for (let y = hy + 1; y < GY - 1; y++) put(p.x, y, jc); + put(p.x, hy, p.hat ? jc : sc([226, 186, 160], amb)); + if (p.up && S.ca > p.dl && S.ca < 6 + p.dl) put(p.x + 1, hy, jc), put(p.x + 1, hy - 1, L.stars > 0.3 ? [180, 210, 255] : [40, 40, 50]); + } + for (const x of [1, 8, 15, 49, 56, 63]) for (let y = GY - 3; y < GY; y++) put(x, y, sc([70, 56, 40], amb)); + for (let x = 1; x < 64; x++) if (x < 16 || x > 48) put(x, GY - 3 + (sin(((x - 1) / 7) * Math.PI) > 0.2 ? 1 : 0), sc([214, 184, 64], amb), 0.8); + + // All the steam, summed and shaded. + D.fill(0); + for (const q of S.puffs) { + const a = q.a * sm(0, 0.4, q.age) * (1 - sm(q.life * 0.45, q.life, q.age)); + if (a > 0.01) splat(D, q.x, q.y, q.r, q.r, a); + } + shade(S, D, 1.1, L.lit, L.dark); + + // The pool in section, its heave, ripples and dome. + const foam = ph === 3 ? 1 : ph === 4 ? 1 - sm(0, 1.2, S.pt) : 0; + for (let x = 20; x < 44; x++) { + const dx = x + 0.5 - C; + let top = wl - S.hv * max(0, 1 - (dx / 10) ** 2) + S.pop * sin(x * 1.3 - t * 14) * 0.6 + foam * 1.5 * (hash(x, floor(t * 20)) - 0.5); + const dome = S.dh > 0 && abs(dx) < S.bw ? S.dh * (1 - (dx / S.bw) ** 2) ** 0.75 : 0; + if (dome) top = min(top, BRIM - dome); + for (let y = max(0, floor(top)); y < SURF[x]; y++) { + const z = y + 0.5 - top; + if (z < 0 || (kind[y * W + x] !== 1 && y >= BRIM)) continue; + let c; + if (dome && y < BRIM) { + const nx = dx / S.bw, ny = (BRIM - y) / (S.dh + 0.1), sp = exp(-((nx + 0.42) ** 2) / 0.03 - (ny - 0.6) ** 2 / 0.08); + c = [40, 150, 176].map((v, n) => (mix(v, [110, 226, 230][n], ny * 0.8 + 0.3 * abs(nx)) * (0.85 + 0.25 * vn(x * 0.5, y * 0.5 - t * 6)) + 150 * sp + (z < 1 ? 70 : 0)) * Lw); + } else if (z < 1) c = sc([150, 224, 226], Lw); + else c = [60, 190, 200].map((v, n) => mix(v, [18, 70, 140][n], sm(0, 10, z)) * Lw * (0.88 + 0.24 * vn(x * 0.45 + t * 0.6, y * 0.7 - t * 0.4))); + if (foam && z < 3 && hash(x, y + floor(t * 25)) < 0.6 * foam) c = sc(FOAM, Lw); + put(x, y, c); + } + } + + // The column and its spray. + CB.fill(0); + for (const q of S.par) splat(CB, q.x, q.y, q.w, 2.8, q.a); + if (S.par.length) shade(S, CB, 1.8, L.lit.map((v) => min(240, v * 1.04)), L.dark, [70 * Lw, 200 * Lw, 212 * Lw], 1); + for (const s of S.spr) { + const a = min(1, s.life * 1.5) * 0.85; + put(s.x - s.vx * 0.02, s.y - s.vy * 0.02, L.lit, a * 0.35); + put(s.x, s.y, L.lit, a); + } + + // Under the ground: the conduit, the chamber and the cracks feeding it. + const froth = ph === 3 ? 1 - 0.5 * sm(0.6, 1, S.pt / (1.1 + 1.1 * P)) : ph === 2 ? 0.3 : ph === 1 ? 0.12 : 0; + const inflow = ph === 0 ? 1 - sm(4, 6.5, S.pt) * 0.8 : ph === 4 ? sm(1, 3, S.pt) : 0.2; + for (const i of S.cav) { + const x = i & 63, y = i >> 6, o = i * 3, k = kind[i]; + let r, g, b; + if (k === 4) { + const p = inflow * sm(0.55, 1, sin(S.fu[i] * 0.7 - t * 7)); + (r = 20 + 80 * p), (g = 46 + 130 * p), (b = 104 + 110 * p); + } else if (k === 3 && y + 0.5 < wl) { + // Drained: wet walls with a film running down. + const f = ph === 4 && hash(x, floor(y - t * 26)) < 0.12; + (r = f ? 70 : 30), (g = f ? 104 : 33), (b = f ? 124 : 42); + } else if (froth && hash(x, floor(y + t * 80)) < froth * (k === 5 ? 0.4 : 0.55)) [r, g, b] = FOAM; + else { + // Cool water blue, hot water turquoise. + const Tl = cl(T * (0.7 + 0.3 * sm(BOWL, KY + KRY, y)) + (k === 5 ? 0.08 : 0), 0, 1), sh = (0.86 + 0.28 * vn(x * 0.6, y * 0.3 + t * 2)) * (k === 3 && y === floor(wl) ? 1.3 : 1); + (r = mix(COOL[0], HOT[0], Tl) * sh), (g = mix(COOL[1], HOT[1], Tl) * sh), (b = mix(COOL[2], HOT[2], Tl) * sh); + } + (F[o] = r * Lr), (F[o + 1] = g * Lr), (F[o + 2] = b * Lr); + } + for (const { x, y, r } of S.bub) + if (r < 0.9) put(x, y, FOAM, 0.85); + else + for (let v = floor(y - r); v <= y + r; v++) + for (let u = floor(x - r); u <= x + r; u++) { + const e = hypot(u + 0.5 - x, v + 0.5 - y) / r; + if (e < 1) put(u, v, e > 0.6 ? [150, 220, 230] : FOAM, 0.9); + } + if (S.big > 0) + for (let y = floor(S.big); y < S.big + 10; y++) { + const cx = CXA[y] + 0.5, hw = CHW[y] * min(1, 0.5 + 0.5 * (y + 1 - S.big)) - 0.3; + for (let x = floor(cx - hw); x < cx + hw; x++) put(x, y, abs(x + 0.5 - cx) > hw - 1 ? [160, 236, 240] : [236, 246, 248], 0.97); + } + + for (let i = 0, o = 0; i < N; i++, o += 3) display.setPixel(i & 63, i >> 6, min(255, F[o]) | 0, min(255, F[o + 1]) | 0, min(255, F[o + 2]) | 0); +} From 5477d7b8b5928e8539b12572dcce7b8f6ab41854 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 05:54:40 +0000 Subject: [PATCH 04/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Geyser?= 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_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/patterns/geyser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/incoming/pattern-study/patterns/geyser.js b/incoming/pattern-study/patterns/geyser.js index fe0d11751..6da501329 100644 --- a/incoming/pattern-study/patterns/geyser.js +++ b/incoming/pattern-study/patterns/geyser.js @@ -202,7 +202,7 @@ function step(S, d, hold) { // The jet: slabs of water fly up and spread; past the top they break up. if (ph === 3) { const u = pt / Dj, v = sqrt(2 * G * (64 + 46 * P)) * (1 - 0.25 * sm(0.7, 1, u)) * (1 - 0.6 * sm(0.45, 0.8, P) * exp(-(((u - 0.48) / 0.09) ** 2))); - for (S.np += d * v * 0.6; S.np >= 1; S.np--) S.par.push({ x: C + (rnd() - 0.5) * 1.2, y: BRIM - 1, vy: -v * (0.95 + 0.1 * rnd()), w: (3.5 + 3.5 * P) * (0.85 + 0.3 * rnd()), a: 1, ap: 0 }); + for (S.np += d * v * 0.6; S.np >= 1; S.np--) S.par.push({ x: C + (rnd() - 0.5) * 1.2, y: BRIM - 1 - v * d * rnd(), vy: -v * (0.95 + 0.1 * rnd()), w: (3.5 + 3.5 * P) * (0.85 + 0.3 * rnd()), a: 1, ap: 0 }); } for (const q of S.par) { q.vy += G * d; From 57d59b242af175ea36aa1b682373dca26c7507ad Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 05:49:04 +0000 Subject: [PATCH 05/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Rice=20Planting?= 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_01BvYAVbR9mNHMMAp7tN8evZ --- .../pattern-study/patterns/rice-planting.js | 482 ++++++++++++++++++ 1 file changed, 482 insertions(+) create mode 100644 incoming/pattern-study/patterns/rice-planting.js diff --git a/incoming/pattern-study/patterns/rice-planting.js b/incoming/pattern-study/patterns/rice-planting.js new file mode 100644 index 000000000..36b5aa9d9 --- /dev/null +++ b/incoming/pattern-study/patterns/rice-planting.js @@ -0,0 +1,482 @@ +// ===== Patternflow pattern ===== +// Title: Rice Planting +// Family: farm +// Idea: Seen straight down over a flooded paddy, a line of planters in straw hats steps backward along a guide rope with red markers, pushing seedlings into the mud hill by hill, so the field behind them fills with rows of green while the open water ahead mirrors drifting clouds, an egret stalks there and bundles of seedlings land with a splash. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 26, after monaegi, the hand transplanting of rice along a marked rope (motjul) in Korea +// Round: 26 +// =============================== +// @matrix 64x128 +// @knobs Hour=0..1, Speed=0.2..3, Planters=4..8, Wind=0..1 +// +// Knob 1: Hour. A misty morning at 0, a deep blue noon with the sun's glint in the paddy, a gold afternoon, and at 1 dusk, when the clouds in the water turn orange. +// Knob 2: Speed. At 2 a planter pushes in a hill every third of a second and the rope moves a row about every two seconds; 0.2 is ten times slower. +// Knob 3: Planters. Four to eight in the line, sharing the fourteen hills of a row, so more hands move the rope sooner and the field is further along when you look. +// Knob 4: Wind. Calm water is a mirror; turned up, cat's paws darken it and break it into glitter, the clouds run faster and the seedlings lean. +// Buttons: K1 tap and the crew pushes in the rest of the row at once and the rope moves on. + +const W = 64, H = 128, N = W * H, PW = 61, NH = 14, NR = 25; +const { min, max, floor, ceil, round, sin, cos, abs, exp, sqrt, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const mix = (a, b, t) => a + (b - a) * t; +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +const rnd = () => Math.random(); +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; +} +// Value noise that wraps every p cells, so the sky texture tiles. +function vnp(x, y, p, o) { + const xi = floor(x), yi = floor(y), u = x - xi, v = y - yi, su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v); + const g = (a, b) => hash(a % p, (b % p) + o); + const a = g(xi, yi), b = g(xi + 1, yi), c = g(xi, yi + 1); + return a + (b - a) * su + (c - a) * sv + (a - b - c + g(xi + 1, yi + 1)) * su * sv; +} + +// A paddy and its left bund are 61 columns of the world; the hills of a row +// stand every four columns, and the rows every four LEDs from row 5 to 101. +const HX = (j) => 5 + 4 * j; +const rowY = (r, dn) => (dn > 0 ? 5 + 4 * r : 101 - 4 * r); +// The light by the hour: sky overhead, sky toward the sun, sunlit cloud, +// cloud underside, light on the people and the fields, and mist. +const HOURS = [ + [0, 46, 92, 168, 116, 150, 204, 250, 242, 236, 150, 162, 192, 0.88, 0.92, 0.98, 0.32], + [0.25, 36, 92, 186, 110, 160, 218, 254, 250, 244, 156, 172, 204, 0.96, 0.98, 1, 0.12], + [0.5, 18, 66, 176, 64, 126, 210, 255, 255, 255, 168, 180, 206, 1.05, 1.05, 1, 0], + [0.75, 44, 92, 176, 176, 164, 150, 255, 224, 170, 160, 140, 156, 1, 0.9, 0.74, 0], + [1, 36, 34, 98, 170, 86, 96, 255, 150, 70, 112, 64, 100, 0.82, 0.56, 0.46, 0.12], +]; +function key(u) { + let i = 0; + while (i < HOURS.length - 2 && u > HOURS[i + 1][0]) i++; + const a = HOURS[i], b = HOURS[i + 1], f = cl((u - a[0]) / (b[0] - a[0]), 0, 1); + return a.map((v, c) => mix(v, b[c], f)); +} +const HAT = [234, 190, 102], SKIN = [226, 176, 136], PANTS = [[60, 62, 120], [126, 62, 110], [70, 84, 72]]; +const SHIRTS = [[236, 236, 228], [104, 144, 224], [238, 136, 164], [150, 208, 96], [184, 152, 228], [214, 78, 70], [112, 204, 204], [70, 96, 176]]; +const SEED = [134, 222, 72], LEAF = [70, 166, 56], ROOT = [168, 136, 100], TIE = [230, 200, 112], NUR = [96, 196, 66]; +const ROPE = [200, 170, 116], MARK = [[240, 52, 40], [252, 216, 60]], GRASS = [62, 116, 40], EARTH = [134, 106, 74], MUD = [120, 92, 58]; +const WHITE = [248, 248, 242], BILL = [244, 196, 40]; + +export function setup(params) { + const F32 = (n) => new Float32Array(n); + const P = (params.s = { + t: 0, f: F32(N * 3), rip: F32(N), tb: F32(H), ht: F32(16384), bm: new Uint8Array(W), cd: F32(16384), cs: F32(16384), nz: F32(16384), k: 0, dn: 1, r: 0, fl: new Uint8Array(NH), + mv: -1.5, sl: -1.5, cam: 0.5, n: 0, h: 0.5, w: 0.5, ox: rnd() * 128, oy: rnd() * 128, rp: [], bu: [], init: false, + }); + // The sky: clouds with steep edges, the size of a few drift blocks, and a + // slower field that says where the wind roughens the water and where mist lies. + for (let i = 0; i < 16384; i++) { + const x = i & 127, y = i >> 7, q = (f, o) => vnp((x * f) / 128, (y * f) / 128, f, o); + const v = 0.55 * q(6, 0) + 0.3 * q(14, 50) + 0.15 * q(32, 90); + P.cd[i] = sm(0.5, 0.6, v); + P.cs[i] = cl((v - 0.5) * 4, 0, 1); + P.nz[i] = 0.6 * q(5, 130) + 0.4 * q(12, 170); + P.ht[i] = hash(x, y); + } + P.crew = SHIRTS.map((s, i) => ({ sh: (i * 3 + floor(rnd() * 8)) % 8, pa: floor(rnd() * 3), sp: 0.85 + rnd() * 0.3, list: [], q: 0, ph: 0.5, home: 32.5, wig: rnd() * 7 })); + P.eg = { x: 10 + rnd() * 44, y: 84 + rnd() * 14, a: rnd() * 6.28, m: 0, t: 1 + rnd(), st: -1.5, sx: 0.5, sy: 0.5, tx: 0.5, ty: 0.5, sk: -1.5 }; + P.ca = { x: 14 + rnd() * 36, tx: 32.5, w: 0.6 + rnd(), tt: -0.5 }; + P.pu = { x: 18 + rnd() * 30, x1: 0, old: [] }; + P.pu.x1 = P.pu.x + 10 + rnd() * 12; +} + +// Each planter takes a run of the fourteen hills; on alternate rows they work it the other way. +function assign(P) { + const n = P.n; + P.crew.forEach((c, i) => { + const a = floor((i * NH) / n), b = floor(((i + 1) * NH) / n); + c.list = []; + if (i < n) for (let j = a; j < b; j++) if (!P.fl[j]) c.list.push(j); + if (P.r & 1) c.list.reverse(); + c.q = 0; + c.ph = 0; + if (i < n) c.home = (HX(a) + HX(b - 1)) / 2; + }); +} +const fresh = (P) => (P.fl.fill(0), assign(P)); +const ropeY = (P) => (P.mv >= 0 ? mix(rowY(P.r, P.dn), rowY(P.r + 1, P.dn), sm(0, 1, P.mv)) : rowY(P.r, P.dn)); +const ring = (P, x, y, s, life = 0.9) => P.rp.length < 48 && P.rp.push({ x: x + 1e-6, y: y + 1e-6, s, a: 1e-6, life }); +function plant(P, j) { + P.fl[j] = 1; + ring(P, P.k * PW + HX(j), rowY(P.r, P.dn), 0.9); +} +// Bundles of seedlings lie about the open water, waiting for the line; ft +// is how far through its flight a thrown one is, below 0 once it floats. +const bun = (x, y, ft, tx, ty) => ({ x, y, fx: x, fy: y, tx, ty, a: floor(rnd() * 8), ft }); +function scatter(P, k, lo, hi) { + for (let i = 0; i < 12; i++) P.bu.push(bun(k * PW + 8 + rnd() * 46, lo + rnd() * (hi - lo), -1.5, 0.5, 0.5)); +} +// Where open water lies ahead of the line in this paddy, as rows lo..hi. +function open(P, gap) { + const ry = ropeY(P); + return P.dn > 0 ? [ry + gap, 99] : [7, ry - gap]; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, kv = input.knobValues || [0, 2, 4, 0.06]; + const d = (cl(dt || 0, 0, 0.1) * cl(kv[1], 0.2, 3)) / 2, n = round(cl(kv[2], 4, 8)); + P.h = cl(kv[0], 0, 1); + P.w = cl(kv[3], 0, 1); + if (!P.init) { + // They have been at it since dawn: the more hands, the further along. + P.n = n; + P.r = 8 + round((n - 4) * 1.5) + floor(rnd() * 2); + assign(P); + scatter(P, 0, rowY(P.r, 1) + 10, 99); + P.init = true; + } + if (n !== P.n) (P.n = n), assign(P); + P.t += d; + P.ox += (3.4 + 5 * P.w) * d; + P.oy += (1.4 + 2 * P.w) * d; + const btn = input.btnPressed && input.btnPressed[0]; + if (P.sl < 0) { + if (btn && P.mv < 0) for (let j = 0; j < NH; j++) if (!P.fl[j]) plant(P, j); + if (P.mv < 0) { + for (let i = 0; i < P.n; i++) { + const c = P.crew[i]; + if (c.q >= c.list.length) continue; + const j = c.list[c.q], p0 = c.ph; + if (P.fl[j] && p0 < 0.6) { + c.q++; + continue; + } + c.ph += (d * c.sp) / 0.36; + if (p0 < 0.6 && c.ph >= 0.6) plant(P, j); + if (c.ph >= 1) (c.ph = 0), c.q++; + } + if (P.fl.every((v) => v)) { + if (P.r >= NR - 1) { + // The last row is in: the crew crosses the bund to the next paddy, + // where the carrier has already thrown the bundles. + P.sl = 0; + scatter(P, P.k + 1, 10, 96); + P.eg.m === 0 && fly(P, P.k + 1); + } else P.mv = 0; + } + } else if ((P.mv += d / 0.5) >= 1) { + (P.mv = -1), P.r++, fresh(P); + for (let i = 0; i < P.n; i++) ring(P, P.k * PW + P.crew[i].home, rowY(P.r, P.dn) + P.dn * 10, 0.5); + } + } else if ((P.sl += d / 3.5) >= 1) (P.sl = -1), P.k++, (P.dn = -P.dn), (P.r = 0), fresh(P); + P.cam = P.k * PW + (P.sl >= 0 ? PW * sm(0, 1, P.sl) : 0); + const ry = ropeY(P), dn = P.dn; + // The line picks up the bundles it reaches. + P.bu = P.bu.filter((b) => { + const k = floor(b.x / PW); + return b.ft >= 0 || k > P.k || (k === P.k && (P.sl >= 0 || (b.y - ry) * dn > 5)); + }); + for (const b of P.bu) + if (b.ft >= 0 && (b.ft += d / 0.75) >= 1) (b.ft = -1.5), (b.x = b.tx), (b.y = b.ty), ring(P, b.x, b.y, 1.2, 1.2); + // The carrier on the levee throws a bundle ahead of the line now and then. + const C = P.ca; + if (C.tt > 0) C.tt -= d; + else if (abs(C.tx - C.x) > 0.6) C.x += Math.sign(C.tx - C.x) * 7 * d; + else if ((C.w -= d) < 0) { + const [lo, hi] = open(P, 10); + if (P.sl < 0 && hi - lo > 6 && P.bu.length < 16) { + const ty = max(lo, hi - 46) + rnd() * (hi - max(lo, hi - 46)); + P.bu.push(bun(C.x, 104.5, 1e-6, P.k * PW + 8 + rnd() * 46, ty)); + C.tt = 0.5; + } + C.w = 1.6 + rnd() * 2.4; + C.tx = P.cam + 6 + rnd() * 52; + } + // The seedling puller works left along the nursery bed. + const U = P.pu; + U.x -= 0.5 * d; + if (U.x < P.cam - 8) U.old.push([U.x + 2, U.x1]) > 3 && U.old.shift(), (U.x1 = (U.x = P.cam + 70) + 2); + egret(P, d, ry); + for (const q of P.rp) q.a += d; + P.rp = P.rp.filter((q) => q.a < q.life); +} + +// The egret flies to open water in paddy k, away from the line. +function fly(P, k) { + const E = P.eg, [lo, hi] = k === P.k ? open(P, 26) : [12, 92]; + if (hi - lo < 8) return fly(P, k + 1); + Object.assign(E, { m: 1, t: 0, sx: E.x, sy: E.y, tx: k * PW + 8 + rnd() * 46, ty: lo + rnd() * (hi - lo) }); + E.a = Math.atan2(E.ty - E.sy, E.tx - E.sx); +} +function egret(P, d, ry) { + const E = P.eg; + if (E.m === 1) { + if ((E.t += d / 2.4) >= 1) (E.m = 0), (E.x = E.tx), (E.y = E.ty), (E.t = 1.5), ring(P, E.x, E.y, 0.8); + return; + } + if (E.sk >= 0 && (E.sk += d / 0.35) >= 1) E.sk = -1; + // Too near the line, or left behind in a planted paddy: off it goes. + if (P.sl < 0 && (floor(E.x / PW) < P.k || (floor(E.x / PW) === P.k && (E.y - ry) * P.dn < 15))) return fly(P, P.k); + if (E.st >= 0) { + E.st += d / 0.45; + const q = sm(0, 1, E.st); + E.x = mix(E.sx, E.tx, q); + E.y = mix(E.sy, E.ty, q); + if (E.st >= 1) (E.st = -1), ring(P, E.x - 1.5 * cos(E.a), E.y - 1.5 * sin(E.a), 0.45, 0.7); + } else if ((E.t -= d) < 0) { + // A slow step, now and then a strike at something in the mud. + if (rnd() < 0.25) (E.sk = 0), ring(P, E.x + 5 * cos(E.a), E.y + 5 * sin(E.a), 0.7); + else { + const lx = E.x - P.k * PW; + E.a += (rnd() - 0.5) * 1.4; + if (lx < 10) E.a = 0.2 * (rnd() - 0.5); + if (lx > 52) E.a = PI + 0.2 * (rnd() - 0.5); + if (E.y > 96 || E.y < 10) E.a = (E.y > 50 ? -PI : PI) / 2 + (rnd() - 0.5); + Object.assign(E, { st: 0, sx: E.x, sy: E.y, tx: E.x + 1.6 * cos(E.a), ty: E.y + 1.6 * sin(E.a) }); + } + E.t = 0.6 + rnd() * 1.8; + } +} + +let F, L; +function put(x, y, c, k = 1, a = 1) { + x = round(x); + y = round(y); + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + for (let j = 0; j < 3; j++) F[o + j] = mix(F[o + j], c[j] * k * L[j], a); +} +// Points given along a heading (u forward, v to the side), turned to angle a. +function spr(x, y, a, pts) { + const c = cos(a), s = sin(a); + for (const [u, v, col, k] of pts) { + const px = round(x + u * c - v * s), py = round(y + u * s + v * c), o = (py * W + px) * 3; + if (col) put(px, py, col, k); + else if (px >= 0 && px < W && py >= 0 && py < H) (F[o] *= k), (F[o + 1] *= k), (F[o + 2] *= k); + } +} +function disc(x, y, r, col, sh) { + x = round(x); + y = round(y); + for (let dy = -3; dy <= 3; dy++) + for (let dx = -3; dx <= 3; dx++) { + const e = sqrt(dx * dx + dy * dy); + if (e > r + 0.35) continue; + put(x + dx, y + dy, col, (1.14 - 0.32 * (e / r) ** 2) * (e > r - 0.55 ? 0.82 : 1) - (sh * 0.1 * (dx + dy)) / r); + } +} +function line(ax, ay, bx, by, c, k) { + const n = max(1, round(max(abs(bx - ax), abs(by - ay)))); + for (let i = 0; i <= n; i++) put(mix(ax, bx, i / n), mix(ay, by, i / n), c, k); +} +const BUNDLE = [[-2, 0, ROOT, 0.9], [-1, 0, TIE, 1], [0, 0, SEED, 0.95], [1, 0, LEAF, 1], [2, 0, LEAF, 0.8], [1, -1, LEAF, 0.75], [1, 1, LEAF, 0.75]]; + +// One planter bent over, seen from above: the hat, the back and hips +// behind it, the arms reaching out past the brim to the water, a bundle in +// one hand and the other pushing a hill in at the rope. +const BACK = [3.4, 3.4, 3, 2.6, 2.1]; +function planter(P, c, x, y, dn, ry, x0, hr, walk) { + const sh = SHIRTS[c.sh], pa = PANTS[c.pa], st = walk ? round(sin(P.t * 9 + c.wig)) : 0; + let side = c.home < 32 ? 1 : -1, e = 0, tx = x + side * 3, ty = y - dn * 3.6; + if (!walk && c.q < c.list.length) { + const hx = x0 + HX(c.list[c.q]); + side = hx >= x ? 1 : -1; + e = c.ph < 0.6 ? sm(0, 0.5, c.ph) : 1 - sm(0.65, 1, c.ph); + tx = mix(x + side * 3, hx, e); + ty = mix(y - dn * 3.6, ry + dn * 0.6, e); + } + for (let s = -1; s <= 1; s += 2) { + put(x + s, y + dn * (8 + (s * st > 0 ? 1 : 0)), pa, 0.7); + for (let dx = -1; dx <= 1; dx++) put(x + dx, y + dn * 7, pa, 0.9 - 0.1 * abs(dx)); + } + BACK.forEach((hw, j) => { + for (let dx = -3; dx <= 3; dx++) if (abs(dx) <= hw) put(x + dx, y + dn * (2 + j), sh, (0.98 - 0.07 * j) * (abs(dx) > hw - 1 ? 0.85 : 1)); + }); + line(x + side * 2.8, y + dn * 1.5, tx, ty, sh, 0.9); + put(tx, ty, SKIN); + const bx = x - side * 3.2, by = y - dn * 3; + line(x - side * 2.8, y + dn * 1.5, bx, by, sh, 0.9); + put(bx, by, SKIN); + put(bx, by - dn, SEED); + put(bx + side, by - dn * 1.6, LEAF, 0.9); + put(bx - side, by - dn, LEAF, 0.8); + disc(x + (tx - x) * 0.15, y - dn * e * 0.8, hr, HAT, 1); +} + +// Ripple rings from every push, step and splash, summed per LED. +function ripples(P, cam) { + const RIP = P.rip; + RIP.fill(0); + for (const q of P.rp) { + const R = 0.6 + q.a * 6.5, a = q.s * (1 - q.a / q.life), qx = q.x - cam; + for (let y = max(0, floor(q.y - R - 1)); y <= min(H - 1, q.y + R + 1); y++) + for (let x = max(0, floor(qx - R - 1)); x <= min(W - 1, qx + R + 1); x++) { + const e = abs(sqrt((x - qx) ** 2 + (y - q.y) ** 2) - R); + if (e < 0.8) RIP[y * W + x] += a * (1 - e / 0.8); + } + } +} + +// The bunds, the levee, the nursery, and the water mirroring the sky. +function ground(P, K, cam, ry) { + const f = F, k = P.k, dn = P.dn, t = P.t, w = P.w, h = P.h, RIP = P.rip, CD = P.cd, CS = P.cs, NZ = P.nz, HT = P.ht, TB = P.tb; + const ox = floor(P.ox), oy = floor(P.oy), wo = floor(t * (3 + 6 * w)), ws = 0.75 - 0.6 * w, wa1 = min(1, w * 4); + // The sun's glint, near noon, sliding across the paddy with the hour. + const gu = cl((h - 0.22) / 0.56, 0, 1), gs = sm(0.22, 0.38, h) * (1 - sm(0.62, 0.8, h)), gx = 6 + 52 * gu, gy = 70 - 30 * sin(PI * gu); + // The nursery bed is bare where the seedlings have been pulled. + const BM = P.bm; + BM.fill(0); + for (const [a, b] of [...P.pu.old, [P.pu.x + 2, P.pu.x1]]) for (let x = max(0, ceil(a - cam)); x <= min(W - 1, b - cam); x++) BM[x] = 1; + // How muddy the water is on each row of this paddy: churned round the + // crew's feet and behind them, settling further back. + for (let y = 0; y < H; y++) { + const ds = (ry - y) * dn; + TB[y] = P.sl >= 0 ? 0.22 : sm(-14, -9, ds) * (0.16 + 0.3 * exp(-max(0, ds) / 14)); + } + const [z0, z1, z2, s0, s1, s2, l0, l1, l2, h0, h1, h2, , , , mist] = K.slice(1), L0 = L[0], L1 = L[1], L2 = L[2], M0 = MUD[0] * L0, M1 = MUD[1] * L1, M2 = MUD[2] * L2; + const m0 = mix(s0, 250, 0.6), m1 = mix(s1, 250, 0.6), m2 = mix(s2, 250, 0.6), my = floor(t * 1.5), mx = floor(t * 2.5) + 40; + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, wx = x + cam, kk = floor(wx / PW), lx = wx - kk * PW, o = i * 3, hv = HT[(wx & 127) | (y << 7)]; + const wet = y >= 3 && y <= 103 && lx >= 3 ? 1 : y === 110 || y === 116 || y === 122 ? 0.7 : 0; + if (!wet) { + let c = GRASS, v = 0.8 + 0.4 * hv; + if (y >= 110) { + const bare = y >= 117 && y <= 121 && BM[x]; + (c = bare ? MUD : NUR), (v = bare ? 0.55 + 0.3 * hv : (y === 111 || y === 115 || y === 117 || y === 121 || y === 123 ? 0.62 : 0.72) + 0.45 * hv); + } else if (y >= 104) (c = y >= 106 && y <= 108 ? EARTH : GRASS), (v = y === 104 ? 0.6 + 0.3 * hv : v); + else if (y < 3 ? y === 1 : lx === 1) (c = EARTH), (v = 0.75 + 0.35 * hv); + else if (y === 2 || (y > 2 && lx === 2)) v *= 0.7; + f[o] = c[0] * v * L0; + f[o + 1] = c[1] * v * L1; + f[o + 2] = c[2] * v * L2; + continue; + } + const ci = ((y + oy) & 127) * 128 + ((x + ox) & 127), cd = CD[ci], cs = CS[ci], g = (0.28 * x) / 63 + 0.52 * (1 - y / 127); + let br = 0.9; + if (wa1 > 0) { + const nz = NZ[(((y >> 1) + wo) & 127) | ((((x >> 1) + (wo >> 1)) & 127) << 7)]; + // A cat's paw darkens the mirror and breaks it into glitter. + if (nz > ws) { + const wa = sm(ws, ws + 0.16, nz) * wa1; + br *= 1 - 0.3 * wa; + if (HT[((x * 5 + wo * 3) & 127) | (((y + wo) & 127) << 7)] > 0.87) br += 0.45 * wa; + } + } + const tb = kk < k ? 0.22 : kk === k && wet === 1 ? TB[y] : 0, rp = RIP[i]; + let gl = 0; + if (gs > 0 && abs(x - gx) < 16) { + gl = gs * exp(-((x - gx) ** 2 + ((y - gy) * 0.8) ** 2) / 40) * (1 - tb); + if (w > 0.1) gl *= mix(1, HT[((x * 7 + wo) & 127) | (y << 7)] > 0.75 ? 2.6 : 0.25, w); + } + const r0 = z0 + (s0 - z0) * g, r1 = z1 + (s1 - z1) * g, r2 = z2 + (s2 - z2) * g; + const q0 = (r0 + (h0 + (l0 - h0) * cs - r0) * cd) * br, q1 = (r1 + (h1 + (l1 - h1) * cs - r1) * cd) * br, q2 = (r2 + (h2 + (l2 - h2) * cs - r2) * cd) * br; + // Morning mist lies over the water in drifting banks. + let ms = mist > 0.01 ? (NZ[((((y >> 1) + my) & 127) << 7) | ((x + mx) & 127)] - 0.5) * 2.4 : 0; + ms = ms > 0 ? mist * min(ms, 0.7) : 0; + const m = 0.7 + 0.4 * hv, wk = wet < 1 ? 0.8 : 1, c0 = (q0 + (M0 * m - q0) * tb + rp * (60 + 0.35 * q0) + 255 * L0 * gl) * wk; + const c1 = (q1 + (M1 * m - q1) * tb + rp * (60 + 0.35 * q1) + 255 * L1 * gl) * wk, c2 = (q2 + (M2 * m - q2) * tb + rp * (60 + 0.35 * q2) + 255 * L2 * gl) * wk; + f[o] = c0 + (m0 - c0) * ms; + f[o + 1] = c1 + (m1 - c1) * ms; + f[o + 2] = c2 + (m2 - c2) * ms; + } +} + +// The rope with its markers, one per hill still to plant, and every hill +// planted so far, leaning and swaying with the wind. +function field(P, cam, ry) { + const k = P.k, dn = P.dn, t = P.t, w = P.w, x0 = k * PW - cam; + if (P.sl < 0) { + const yr = round(ry); + for (let x = 1; x <= 62; x++) put(x0 + x, yr, ROPE, 0.85); + if (P.mv < 0) for (let j = 0; j < NH; j++) if (!P.fl[j]) put(x0 + HX(j), yr, MARK[j % 3 === 2 ? 1 : 0]); + } + for (let kk = floor(cam / PW); kk <= floor((cam + 63) / PW) && kk <= k; kk++) { + const xk = kk * PW - cam; + for (let gi = 0; gi < NR; gi++) { + const y = 5 + 4 * gi, ord = kk < k ? -1 : dn > 0 ? gi : NR - 1 - gi; + if (kk === k && ord > P.r) continue; + for (let j = 0; j < NH; j++) { + if (kk === k && ord === P.r && !P.fl[j]) continue; + const x = xk + HX(j), v = 0.85 + 0.3 * hash(kk * 31 + j, gi), sw = sin(t * 2.6 + x * 0.3 + y * 0.15) * (0.25 + 0.9 * w) + 0.9 * w, ln = round(cl(sw, -1, 1)); + put(x, y, SEED, v); + put(x + ln, y - 1, LEAF, v); + put(x + (v > 1 ? 1 : -1), y, LEAF, 0.75 * v); + } + } + } +} + +// The egret: wading with its neck folded (out in a strike), or on the wing. +function egretDraw(P, cam) { + const E = P.eg, pts = []; + if (E.m === 0) { + const nk = E.sk >= 0 ? 1.8 + 2.4 * sin(PI * E.sk) : 1.8; + for (let u = -3.5; u <= 1.5; u += 0.5) + for (let v = -1.5; v <= 1.5; v += 0.5) if ((u + 1) ** 2 / 6.5 + (v * v) / 2.1 <= 1) pts.push([u, v, WHITE, 0.9 + 0.06 * v]); + for (let u = 2; u <= nk + 0.5; u += 0.5) pts.push([u, 0.5 * sin(u * 1.6), WHITE, 1]); + pts.push([nk + 1, 0, WHITE, 1], [nk + 2, 0, BILL, 1], [nk + 2.8, 0, BILL, 0.8]); + spr(E.x - cam + 1, E.y + 1, E.a, pts.map(([u, v]) => [u, v, 0, 0.7])); + spr(E.x - cam, E.y, E.a, pts); + return; + } + const q = sm(0, 1, E.t), ex = mix(E.sx, E.tx, q) - cam, ey = mix(E.sy, E.ty, q) - 8 * sin(PI * E.t), sp = 3 + 3 * abs(sin(P.t * 8)); + for (let u = -2; u <= 1; u += 0.5) pts.push([u, 0, WHITE, 1], [u, 0.6, WHITE, 0.9], [u, -0.6, WHITE, 0.9]); + for (let v = 1; v <= sp; v += 0.5) for (let s = -1; s <= 1; s += 2) pts.push([-0.4 * (v / sp) ** 2, s * v, WHITE, v > sp - 1 ? 0.75 : 1], [0.6 - 0.6 * (v / sp), s * v, WHITE, 0.85]); + pts.push([2, 0, WHITE, 1], [3, 0, WHITE, 1], [4, 0, BILL, 1]); + spr(ex, ey, E.a, pts); +} + +// The crew and the rope holders on the bunds, all in straw hats. +function crew(P, cam, ry) { + const dn = P.dn, x0 = P.k * PW - cam, hr = P.n > 6 ? 2.6 : 3; + if (P.sl < 0) { + for (let i = 0; i < P.n; i++) { + const c = P.crew[i], lag = P.mv >= 0 ? sm(0.15, 1, P.mv) : 1, cy = mix(rowY(P.r, dn), ry, lag) + dn * 4; + planter(P, c, x0 + c.home, cy, dn, ry, x0, hr, false); + } + for (let hx = 1; hx < 63; hx += 61) { + put(x0 + hx + (hx < 9 ? -2 : 2), ry + dn * 2, SHIRTS[hx & 7], 0.8); + disc(x0 + hx, ry + dn * 0.5, 2, HAT, 1); + } + return; + } + const q = sm(0, 1, P.sl), y0 = rowY(NR - 1, dn) + dn * 4, y1 = rowY(0, -dn) - dn * 4, fd = q < 0.5 ? dn : -dn; + for (let i = 0; i < P.n; i++) planter(P, P.crew[i], P.crew[i].home, mix(y0, y1, q), fd, 0, 0, hr, true); + for (let hx = 1; hx < 63; hx += 61) disc(hx, mix(y0, y1, q) - fd * 4, 2, HAT, 1); +} + +// The carrier on the levee with bundles on his A-frame, the puller in the +// nursery with the bundles he has tied laid in the channel, and every bundle +// afloat or in the air. +function helpers(P, cam) { + const C = P.ca, cx = C.x - cam, U = P.pu, ux = U.x - cam, pull = sin(P.t * 7); + for (const b of P.bu) if (b.ft < 0) spr(b.x - cam, b.y, (b.a * PI) / 4, BUNDLE); + // Trays of seedlings from the nursery wait on the levee path. + for (let kk = floor(cam / PW); kk <= floor((cam + 63) / PW); kk++) + for (let tx = 9; tx < 60; tx += 36) + for (let dx = 0; dx < 8; dx++) + for (let dy = 0; dy < 3; dy++) put(kk * PW + tx + dx - cam, 106 + dy, dx % 7 ? NUR : ROOT, dx % 7 ? 0.75 + 0.4 * hash(dx + tx, dy + kk * 5) : 0.6); + for (let dx = -2; dx <= 2; dx++) for (let dy = 2; dy <= 4; dy++) put(cx + dx, 106 + dy, (dx + dy) & 1 ? SEED : LEAF, dy === 2 ? 0.75 : 0.95); + put(cx - 2, 109, TIE), put(cx + 2, 109, TIE); + if (C.tt > 0) line(cx + 1, 105, cx + 2, 101 + 2 * C.tt, SKIN, 0.9); + disc(cx, 106, 2, HAT, 1); + for (let bx = U.x + 6; bx < U.x1 - 1; bx += 5) spr(bx - cam, 116, PI / 2, BUNDLE); + for (let dy = -1; dy <= 1; dy++) for (let dx = 2; dx <= 4; dx++) put(ux + dx, 119 + dy, SHIRTS[5], 0.85); + line(ux - 1, 118, ux - 2.5 - pull, 118, SKIN, 0.9); + line(ux - 1, 120, ux - 2.5 + pull, 120, SKIN, 0.9); + disc(ux, 119, 2, HAT, 1); +} +function thrown(P, cam) { + for (const b of P.bu) if (b.ft >= 0) spr(mix(b.fx, b.tx, b.ft) - cam, mix(b.fy, b.ty, b.ft) - 7 * sin(PI * b.ft), b.ft * 9, BUNDLE); +} + +export function draw(display, params) { + const P = params.s; + if (!P || !P.init) return; + const K = key(P.h), cam = floor(P.cam), ry = ropeY(P), f = (F = P.f); + L = [K[13], K[14], K[15]]; + ripples(P, cam); + ground(P, K, cam, ry); + field(P, cam, ry); + if (P.eg.m === 0) egretDraw(P, cam); + crew(P, cam, ry); + helpers(P, cam); + thrown(P, cam); + if (P.eg.m === 1) egretDraw(P, cam); + for (let i = 0, o = 0; i < N; i++, o += 3) display.setPixel(i & 63, i >> 6, min(255, f[o]) | 0, min(255, f[o + 1]) | 0, min(255, f[o + 2]) | 0); +} From 4bb275432d487ec45e2de49db62045f3c1f1cbfd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 05:55:24 +0000 Subject: [PATCH 06/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Implosion?= 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_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/patterns/implosion.js | 477 +++++++++++++++++++ 1 file changed, 477 insertions(+) create mode 100644 incoming/pattern-study/patterns/implosion.js diff --git a/incoming/pattern-study/patterns/implosion.js b/incoming/pattern-study/patterns/implosion.js new file mode 100644 index 000000000..74b917959 --- /dev/null +++ b/incoming/pattern-study/patterns/implosion.js @@ -0,0 +1,477 @@ +// ===== Patternflow pattern ===== +// Title: Implosion +// Family: construction +// Idea: An old concrete tower block among low buildings at dawn is brought down by explosive demolition: a siren light flashes, a ripple of charges runs down the floors, the tower folds and drops into its own footprint under a grey-ochre dust cloud that boils up to the top of the panel and drifts off, and in time-lapse the rubble is cleared and a new tower rises in scaffolding for the next blast. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 26, after the explosive demolitions of 1960s concrete tower blocks watched by crowds behind the site fence +// Round: 26 +// =============================== +// @matrix 64x128 +// @knobs Light=0..1, Speed=0.2..3, Storeys=20..30, Wind=-1..1 +// +// Knob 1: Light. Blue dawn at 0, work lamps in a third of the windows and the floodlights faint; then low morning sun from the left, then flat overcast grey, and at 1 night, when two floodlight masts throw beams up the tower and into the dust. +// Knob 2: Speed. At 2 the charges go 2.4 s in, the tower is down within three seconds and a round from blast to blast takes 36 s; at 0.2 it all runs ten times slower. +// Knob 3: Storeys. Twenty to thirty storeys. Twenty stand 80 rows under the roof plant and the mast; past 26 the floors get lower so the roof stays on the panel. +// Knob 4: Wind. Calm at 0, round clouds and the dust going straight up; either way the clouds stretch into slanting streaks, and the dust cloud, the chimney smoke, the water cannon's spray and the scattering pigeons lean with it. +// Buttons: K1 tap detonates now: the siren, then the charges. During the rebuild it finishes the tower first. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, ceil, round, sin, cos, abs, exp, sqrt, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const mix = (a, b, t) => a + (b - a) * t; +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +const rnd = () => Math.random(); +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; +} +// Value noise that repeats every cx by cy cells, so the 64x128 tile has no seam. +function vn(x, y, cx = 1e4, cy = 1e4) { + const xi = floor(x), yi = floor(y), u = x - xi, v = y - yi, su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v); + const h = (i, j) => hash(((i % cx) + cx) % cx, ((j % cy) + cy) % cy); + const a = h(xi, yi), b = h(xi + 1, yi), c = h(xi, yi + 1); + return a + (b - a) * su + (c - a) * sv + (a - b - c + h(xi + 1, yi + 1)) * su * sv; +} + +// The site's ground row; the tower's front face from x 19, 22 wide, and its side face, 6; roof plant and mast rows. +const G = 110, X0 = 19, TW = 28, CX = 33, MAST = 16; +// A round at Speed 2, in seconds: charges, the fall, diggers in, time-lapse, lot clear, frame rising, frame up, next round. +const T_CH = 2.4, T_FALL = 3, T_EX = 11, T_TL = 19, T_CLR = 25, T_RISE = 26.5, T_UP = 32.5, CYC = 36; +const MP = 240, PF = 12; +// Dawn, morning sun, overcast, night: sky top, middle, horizon; ambient; sun; lit windows; floodlights; cloud lit, dark; cover. +const KEYS = [ + [16, 30, 86, 52, 84, 156, 214, 146, 150, 0.4, 0.45, 0.66, 0.55, 0.32, 0.3, 0.34, 0.35, 236, 156, 156, 58, 60, 108, 0.5], + [46, 108, 206, 110, 164, 228, 236, 206, 160, 0.55, 0.58, 0.68, 1, 0.86, 0.62, 0.06, 0, 255, 244, 226, 150, 160, 190, 0.38], + [96, 102, 112, 136, 140, 146, 170, 170, 168, 0.66, 0.66, 0.68, 0.08, 0.08, 0.08, 0.12, 0.12, 190, 192, 196, 100, 104, 112, 0.9], + [3, 5, 18, 8, 14, 38, 34, 32, 58, 0.14, 0.15, 0.24, 0, 0, 0, 0.5, 1, 40, 38, 58, 12, 14, 28, 0.45], +]; +const TILE = [80, 110, 170], BRICK = [170, 84, 56], PALE = [190, 176, 150], RUST = [150, 80, 50]; +const COATS = [[30, 34, 56], [56, 30, 36], [34, 46, 34], [50, 44, 34], [250, 120, 20]]; +const MID = [0, 0, 0], LAMP = [200, 190, 150], DARK = [30, 28, 28], HOLE = [14, 12, 12], RED = [255, 40, 30]; +const CON = [160, 154, 144], WARM = [255, 188, 104], YEL = [242, 182, 32], DUST0 = [150, 147, 142], DUST1 = [190, 164, 120], SIL = [12, 12, 16], AMBER = [255, 150, 30]; + +function light(h) { + const u = cl(h, 0, 1) * 3, i = min(2, floor(u)), f = u - i; + return KEYS[i].map((v, j) => mix(v, KEYS[i + 1][j], f)); +} + +export function setup(params) { + const f = (n) => new Float32Array(n); + const P = (params.s = { F: f(N * 3), NT: f(N), BA: f(N * 3), BU: f(N), BK: new Uint8Array(N), FL: f(N), SKY: f(H * 3), pf: f(MP * PF), np: 0, db: [], sp: [], T: 0, rt: 0, fr: 0, S: 0, cyc: 0, co: 0, w: 0 }); + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6; + P.NT[i] = 0.5 * vn(x / 16, y / 16, 4, 8) + 0.3 * vn(x / 8, y / 8, 8, 16) + 0.2 * vn(x / 4, y / 4, 16, 32); + // Two floodlight masts by the fence aim their beams up the tower. + for (const lx of [2, 61]) { + const dx = x - lx, dy = y - 82, dd = sqrt(dx * dx + dy * dy) + 0.5, ax = CX - lx, ay = -32, c = (dx * ax + dy * ay) / (dd * sqrt(ax * ax + ay * ay)); + P.FL[i] += exp(-(1 - c) * 32) / (1 + dd / 40) + 0.9 * exp((-dd * dd) / 5); + } + } + scenery(P); + P.cr = []; + for (let x = 2; x < 63; x += 3 + floor(rnd() * 3)) if (rnd() < 0.85) P.cr.push([x, 111 + floor(rnd() * 4), 4 + floor(rnd() * 3), rnd(), rnd(), P.cr.length % 7 === 3 ? 4 : floor(rnd() * 4)]); + newRound(P); +} + +// The street, the site, two low blocks, a far skyline with a spire, a factory chimney and a water cannon on a roof. +function scenery(P) { + const top = new Float32Array(W); + for (let x = 0; x < W; ) for (let j = 0, w = 3 + floor(rnd() * 6), t = 72 + floor(rnd() * 16); j < w && x < W; j++) top[x++] = t; + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, hh = hash(x, y), o = i * 3; + let k = 2, c = null; + if (y > 118) c = [58, 58, 64].map((v) => v * (0.9 + 0.2 * hh)); + else if (y === 118) c = [120, 118, 112]; + else if (y >= G) c = [118, 104, 86].map((v) => v * (0.8 + 0.35 * hh)); + else if (x >= 51 && x <= 55 && y >= 85 && y <= 89) c = abs(x - 53) + abs(y - 86.5) < 1.6 ? [40, 44, 50] : [116, 120, 128]; + else if (x <= 14 && y >= 82) { + c = y < 84 ? [170, 160, 150] : [148, 92, 70].map((v) => v * (0.88 + 0.2 * hash(x >> 1, y))); + if (y >= 86 && (y < 103 ? (x & 3) && (x & 3) < 3 && (y - 86) % 4 < 2 : y > 104 && x > 1 && x < 13)) (k = 3), (P.BU[i] = y > 104 ? hh * 0.4 : hh); + } else if (x >= 49 && y >= 90) { + c = y < 92 ? [190, 182, 170] : [176, 162, 138].map((v) => v * (0.9 + 0.15 * hash(x, y >> 2))); + if (y >= 94 && y < 108 && (x - 50) % 4 > 0 && (x - 50) % 4 < 3 && (y - 94) % 4 < 2) (k = 3), (P.BU[i] = hh); + } else if (x >= 56 && x <= 59 && y >= 44) c = y === 44 ? [180, 172, 160] : (y >= 47 && y <= 48) || (y >= 52 && y <= 53) ? [176, 70, 56] : [150, 138, 124]; + else if (y >= top[x] || (y >= 58 && abs(x - 9) <= (y - 58) / 6)) { + k = hash(x, y >> 1) < 0.07 && y & 1 ? 4 : 1; + c = [60, 66, 86].map((v) => v * (0.85 + 0.25 * hash(x >> 2, 3))); + } else k = 0; + P.BK[i] = k; + if (c) for (let j = 0; j < 3; j++) P.BA[o + j] = c[j]; + } +} + +// The tower's facade for S storeys: one kind per cell, each window's chance to be lit, the crack field. +function facade(P, S) { + const p = min(4, 100 / S), BH = round(S * p), BT = BH + MAST, n = TW * BT, st = []; + for (let s = 0; s <= S; s++) st.push(round(s * p)); + const A = new Float32Array(n), K = new Uint8Array(n), U = new Float32Array(n), C = new Float32Array(n), RS = new Int16Array(BT).fill(-1); + for (let b = 0, s = 0; b < BT; b++) { + while (s < S - 1 && st[s + 1] <= b) s++; + const r = b - st[s]; + if (b < BH) RS[b] = s; + for (let c = 0; c < TW; c++) { + const j = b * TW + c, q = (c - 1) & 3, bay = (c - 1) >> 2; + let k = 0, a = 0.92 + 0.14 * hash(s * 7 + bay, 11); + if (b < BH) { + if (c < 22) { + k = r === 0 ? 3 : c > 0 && c < 21 && q < 3 && (r === 1 || r === 2 || (s === 0 && r === 3)) ? 2 : 1; + if (k === 1 && r === 3 && hash(c, 5) < 0.4) a *= 0.86; + U[j] = s === 0 ? (bay === 2 ? 0 : 0.95) : hash(s * 13 + bay, 3); + } else (k = c === 22 ? 4 : (c === 24 || c === 25) && (r === 1 || r === 2) ? 5 : 4), (a = c === 22 ? 1.15 : a), (U[j] = hash(s * 17 + c, 9)); + if (r === 0) a *= 0.8; + } else if (b === BH) k = c < 22 ? 6 : 0; + else if (b <= BH + 4 && ((c >= 5 && c <= 14) || (c >= 16 && c <= 19 && b <= BH + 3))) (k = 6), (a = 0.82); + else if ((c === 10 && b < BT - 1) || (b === BH + 9 && c >= 8 && c <= 12) || (b === BH + 7 && c === 11)) k = 7; + else if (c === 10 && b === BT - 1) k = 8; + K[j] = k; + A[j] = a; + C[j] = abs(vn(c * 0.3 + 5, b * 0.12) - 0.5) * 2 + 0.6 * abs(vn(c * 0.9, b * 0.7) - 0.5); + } + } + const chg = []; + for (let s = round(S * 0.78); s >= 2; s -= 3) chg.push(s); + Object.assign(P, { S, BH, BT, st, fa: A, fk: K, fu: U, fc: C, rs: RS, chg: chg.concat([1, 0]), fold: st[round(S * 0.42)], fls: new Float32Array(S), bl: new Uint8Array(S) }); +} + +function newRound(P) { + Object.assign(P, { cyc: P.cyc + 1, dir: rnd() < 0.5 ? -1 : 1, fi: 0, b0: 0, b1: 0, pd1: 0, pc2: 0, a1: 0, a2: 0, ad: 0, bd: [] }); + if (P.bl) P.bl.fill(0); + for (let j = 0; j < 8; j++) P.bd.push([0, 0, 0, 0, rnd() * 6.28, 7 + rnd() * 10, 0, rnd() * 6]); + P.frag = [[-9 + rnd() * 3, 9, 0.6, -0.3], [3 + rnd() * 3, 8, -0.7, 0.25]]; +} + +// The lower floors give first; the upper block then folds into them. +const fall = (tc) => (tc <= 0 ? [0, 0] : [11 * tc * tc, tc > 0.5 ? 6 * (tc - 0.5) ** 2 : 0]); + +function addPuff(P, x, y, vx, vy, r, g, a, life, rise, tint) { + if (P.np < MP) P.pf.set([x, y, vx, vy, r, r, g, a, 0, life, rise, tint], P.np++ * PF); +} +function basePuff(P, kind) { + const x = X0 + rnd() * TW, out = x - CX, roll = kind < 2 && rnd() < 0.4; + addPuff(P, x, G - 1 - rnd() * 4, out * 0.8 + (rnd() - 0.5) * 12 + (roll ? Math.sign(out || 1) * (10 + rnd() * 10) : 0), roll ? -1 - rnd() * 3 : -(5 + rnd() * 12) - (kind === 2 ? 14 : 0), 3 + rnd() * 3, 2 + rnd() * 2.5, 1 + rnd() * 0.5, roll ? 12 + rnd() * 5 : 8.5 + rnd() * 5, roll ? 1 + rnd() * 3 : 8 + rnd() * (kind === 2 ? 30 : 24), rnd()); +} +function debris(P, x, y, s) { + P.db.push([x, y, s * (4 + rnd() * 14), -4 - rnd() * 14, 1.5]); +} + +// A charge goes off on storey s: its windows blow out and spit dust both ways. +function fire(P, s) { + const y = G - 1 - (P.st[s] + P.st[s + 1]) / 2; + P.bl[s] = 1; + for (let n = 0; n < 10; n++) { + const sd = n & 1 ? 1 : -1, edge = n < 4; + addPuff(P, edge ? (sd < 0 ? X0 - 1 : X0 + TW) : X0 + 2 + rnd() * (TW - 4), y + (rnd() - 0.5) * 2, edge ? sd * (10 + rnd() * 12) : sd * rnd() * 3, -rnd(), 1 + rnd() * 0.8, 1.2 + rnd(), 0.7, 1.6 + rnd(), 0.5, rnd() * 0.5); + if (edge) debris(P, sd < 0 ? X0 - 1 : X0 + TW, y, sd); + } + if (P.fi === 1) for (const b of P.bd) (b[6] = 1), (b[2] = (b[0] - CX) * 2 + (rnd() - 0.5) * 14 + P.w * 14), (b[3] = -8 - rnd() * 14); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 20, 0.06]; + const rd = cl(dt || 0, 0, 0.1), d = rd * (cl(k[1], 0.2, 3) / 2), S = round(cl(k[2], 20, 30)), w = (P.w = cl(k[3], -1, 1)); + if (S !== P.S) facade(P, S); + (P.rt += rd), P.fr++; + if (input.btnPressed && input.btnPressed[0]) { + if (P.T < T_CH - 0.7) P.T = T_CH - 0.7; + else if (P.T >= T_TL) newRound(P), (P.T = T_CH - 0.7); + } + if ((P.T += d) >= CYC) newRound(P), (P.T -= CYC); + const T = P.T, tl = 0.85 * sm(T_TL, T_TL + 1.2, T) * (1 - sm(CYC - 2.5, CYC - 0.4, T)); + // Time-lapse: days go by, dawn to night and back every 2 s. + P.he = mix(cl(k[0], 0, 1), 0.5 - 0.5 * cos((2 * PI * (T - T_TL)) / 2), tl); + P.L = light(P.he); + P.co += (w * 2 + tl * (w < 0 ? -24 : 24)) * d; + + // Charges, top to bottom, then the shudder. + while (P.fi < P.chg.length && T >= T_CH + P.fi * 0.09 && T < T_TL) fire(P, P.chg[P.fi++]); + P.gfl = 0; + P.chg.forEach((s, n) => { + const t0 = T_CH + n * 0.09; + P.gfl = max(P.gfl, (P.fls[s] = T >= t0 ? exp(-(T - t0) * 9) : 0)); + }); + P.shk = T > T_CH && T < T_FALL + 0.6 ? round((rnd() - 0.5) * 2.6 * (1 - sm(T_FALL, T_FALL + 0.6, T))) : 0; + + // The fall: dust where floors are crushed, at the foot and at the fold. + const tc = T - T_FALL, [d1, c2] = T < T_TL ? fall(tc) : [0, 0], cons = min(P.BT, d1 + c2), phM = 22 + (P.S - 20) * 0.6; + if (tc > 0 && T < T_TL) { + if (!P.b0) for (let n = (P.b0 = 1) && 24; n--; ) basePuff(P, 1); + const e1 = max(0, d1 - P.pd1), e2 = max(0, c2 - P.pc2); + if (d1 + c2 < P.BT) (P.a1 += e1 * 1.1), (P.a2 += e2 * 0.35), (P.ad += e1 * 0.5); + for (; P.a1 >= 1; P.a1--) basePuff(P, 0); + for (const yf = G - 1 - P.fold + d1; P.a2 >= 1; P.a2--) { + const sd = rnd() < 0.5 ? -1 : 1; + addPuff(P, CX + sd * (8 + rnd() * 8), min(G - 2, yf), sd * (8 + rnd() * 14), -2 - rnd() * 6, 2 + rnd() * 1.5, 3 + rnd() * 3, 0.7, 8 + rnd() * 4, 4 + rnd() * 6, rnd()); + } + for (; P.ad >= 1; P.ad--) debris(P, rnd() < 0.5 ? X0 - 1 : X0 + TW, G - 2 - rnd() * 4, rnd() < 0.5 ? -1 : 1); + if (!P.b1 && cons >= P.BT) for (let n = (P.b1 = 1) && 20; n--; ) basePuff(P, 2); + (P.pd1 = d1), (P.pc2 = c2); + } + P.ph = T < T_FALL ? 0 : T < T_TL ? phM * sm(0, 1, cons / P.BT) : phM * (1 - sm(T_TL + 0.5, T_CLR - 0.5, T)); + + // Dust puffs rise, slow, swell and drift with the wind, stronger aloft. + const pf = P.pf, e1 = 1 - exp(-0.7 * d), e2 = 1 - exp(-0.9 * d); + for (let p = 0; p < P.np; p++) { + const q = p * PF, age = (pf[q + 8] += d); + if (age > pf[q + 9]) { + if (p < --P.np) pf.copyWithin(q, P.np * PF, P.np * PF + PF); + p--; + continue; + } + pf[q + 2] += (w * (3 + 9 * (1 - pf[q + 1] / H)) - pf[q + 2]) * e1; + pf[q + 3] += (-pf[q + 10] * exp(-age / 4) - pf[q + 3]) * e2; + (pf[q] += pf[q + 2] * d), (pf[q + 1] += pf[q + 3] * d), (pf[q + 4] += (pf[q + 6] * d) / (1 + 0.4 * age)); + const fl = G - 0.35 * pf[q + 4]; + if (pf[q + 1] > fl) (pf[q + 1] = fl), (pf[q + 3] = min(0, pf[q + 3])); + } + P.db = P.db.filter((b) => ((b[3] += 70 * d), (b[0] += b[2] * d), (b[1] += b[3] * d), (b[4] -= d) > 0 && b[1] < G + 1)); + // The water cannon on the right-hand roof wets the site until the time-lapse. + if (T > 1 && T < T_TL) for (let n = 0; n < 3 && rnd() < 45 * d; n++) P.sp.push([52, 86, -15 - rnd() * 6 + w * 6, -10 - rnd() * 5, 0]); + P.sp = P.sp.filter((s) => ((s[3] += 26 * d), (s[0] += s[2] * d), (s[1] += s[3] * d), (s[4] += d) < 2 && s[1] < 104)); + // Pigeons circle the roof until the first charge, then scatter. + for (const b of P.bd) + if (b[6]) (b[0] += b[2] * d), (b[1] += b[3] * d), (b[3] -= 4 * d), (b[7] += 14 * d); + else (b[4] += 0.9 * d), (b[0] = CX + cos(b[4]) * b[5] * 1.3), (b[1] = G - P.BH - 10 + sin(b[4]) * b[5] * 0.35), (b[7] += 9 * d); +} + +let F, L, P; +const idx = (x, y) => ((x = round(x)), (y = round(y)), x < 0 || x >= W || y < 0 || y >= H ? -1 : (y * W + x) * 3); +function put(x, y, c, a = 1, o = idx(x, y)) { + if (o >= 0) for (let j = 0; j < 3; j++) F[o + j] += (c[j] - F[o + j]) * a; +} +function add(x, y, c, k, o = idx(x, y)) { + if (o >= 0) for (let j = 0; j < 3; j++) F[o + j] += c[j] * k; +} +function glow(x, y, c, k, r) { + for (let j = -r; j <= r; j++) for (let i = -r; i <= r; i++) add(x + i, y + j, c, k * exp(-(i * i + j * j) / (0.3 * r * r))); +} +const lit = (c, m = 1, s = 0.5, fq = 0) => [0, 1, 2].map((j) => c[j] * (L[9 + j] * m + L[12 + j] * s + fq)); +function line(x0, y0, x1, y1, c) { + for (let i = 0, n = max(abs(x1 - x0), abs(y1 - y0)) * 1.4 + 1; i <= n; i++) put(mix(x0, x1, i / n), mix(y0, y1, i / n), c); +} +const fq = (x, y) => (P.FL[cl(round(y), 0, H - 1) * W + cl(round(x), 0, W - 1)] || 0) * L[16]; + +function back() { + const { BA, BU, BK, FL, NT, SKY } = P, aw = abs(P.w), sx = 0.6 - 0.3 * aw, sh = 0.85 * P.w, sy = 1 + 0.8 * aw, th = 0.5 + (0.5 - L[23] - 0.15 * aw) * 0.32, co = P.co; + for (let y = 0; y < H; y++) { + const t = min(1, y / 96), s1 = sm(0, 0.6, t), s2 = sm(0.4, 1, t) ** 2; + for (let c = 0; c < 3; c++) SKY[y * 3 + c] = mix(mix(L[c], L[3 + c], s1), L[6 + c], s2); + } + const sunV = sm(0.12, 0.3, P.he) * (1 - sm(0.42, 0.58, P.he)), night = sm(0.78, 1, P.he), fld = L[16], lw = L[15]; + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, o = i * 3, k = BK[i], fb = fld * FL[i]; + let r, g, b; + if (k === 0) { + (r = SKY[y * 3]), (g = SKY[y * 3 + 1]), (b = SKY[y * 3 + 2]); + // Round clouds when calm, slanting streaks in wind. + if (y < 96) { + const n = NT[((floor(y * sy + 40) & 127) << 6) | (floor(x * sx + y * sh + co) & 63)], cd = sm(th - 0.06, th + 0.08, n) * (1 - sm(66, 96, y)) * 0.9; + if (cd > 0) { + const lb = cl(0.15 + y / 110 + (n - th) * 2, 0, 1); + (r = mix(r, mix(L[20], L[17], lb), cd)), (g = mix(g, mix(L[21], L[18], lb), cd)), (b = mix(b, mix(L[22], L[19], lb), cd)); + } + } + let sg = 0; + if (sunV > 0) { + const ds = (x - 9) ** 2 + (y - 60) ** 2; + sg = sunV * (255 * exp(-ds / 7) + 70 * exp(-sqrt(ds) / 9)); + } + if (night > 0) sg += night * (((x - 52) ** 2 + (y - 20) ** 2 < 6) * 220 + (y < 70 && hash(x, y) > 0.985 ? 90 + 60 * sin(P.rt * 3 + x) : 0)); + (r += sg + 56 * fb), (g += sg * 0.9 + 60 * fb), (b += sg * 0.7 + 66 * fb); + } else if (k === 1 || k === 4) { + const hz = 0.55 - y / 400; + (r = mix(BA[o] * L[9], SKY[y * 3], hz)), (g = mix(BA[o + 1] * L[10], SKY[y * 3 + 1], hz)), (b = mix(BA[o + 2] * L[11], SKY[y * 3 + 2], hz)); + if (k === 4 && hash(x, 77) < lw * 1.4) (r += 120), (g += 86), (b += 40); + } else if (k === 3 && BU[i] < lw * 1.1) (r = WARM[0] * 0.8), (g = WARM[1] * 0.8), (b = WARM[2] * 0.8); + else { + const m = k === 3 ? 0.25 : 1, f = (y < 84 ? 0.35 : 0.5) * (x < 15 || x > 55 ? 1 : 0.6); + (r = BA[o] * m * (L[9] + L[12] * f + fb * 0.6)), (g = BA[o + 1] * m * (L[10] + L[13] * f + fb * 0.6)), (b = BA[o + 2] * m * (L[11] + L[14] * f + fb * 0.6)); + if (k === 3) (r += SKY[y * 3] * 0.2), (g += SKY[y * 3 + 1] * 0.2), (b += SKY[y * 3 + 2] * 0.25); + } + (F[o] = r), (F[o + 1] = g), (F[o + 2] = b); + } + // Light masts at night, and the chimney's smoke bending with the wind. + for (const lx of [2, 61]) for (let y = 82; y < G; y++) put(lx, y, [30, 30, 34]); + if (fld > 0.05) for (const lx of [2, 61]) put(lx, 82, [255, 250, 230], min(1, fld * 1.5)); + const sc = lit([176, 172, 168], 1, 0.6), ph = (P.rt * 0.5) % 1; + for (let j = 0; j < 12; j++) { + const s = j + ph, x = 57.5 + P.w * (s * 2.2 + s * s * 0.14) + sin(P.rt * 0.7 + j) * 0.6, y = 42 - s * 2.4 * (1 - 0.55 * aw), r = 1.4 + s * 0.42, a = 0.45 * (1 - s / 12.5) * min(1, s); + for (let v = -ceil(r); v <= r; v++) for (let u = -ceil(r); u <= r; u++) { const q = 1 - (u * u + v * v) / (r * r); if (q > 0) put(x + u, y + v, sc, a * q); } + } +} + +// The tower, standing, falling or rising again in scaffolding. +function tower() { + const { fa: A, fk: K, fu: U, fc: C, rs: RS, FL, SKY, T, BT, BH, fold: F0 } = P, tc = T - T_FALL; + if (T >= T_TL && T < T_RISE) return; + const reb = T >= T_RISE, built = reb ? BH * sm(T_RISE, T_UP, T) : BT, strip = reb ? BT * (1 - sm(T_UP, CYC - 0.6, T)) : 0; + const [d1, c2] = reb ? [0, 0] : fall(tc), l1 = reb ? 0 : P.dir * 3 * sm(0.1, 1.2, tc), l2 = reb ? 0 : -P.dir * 10 * sm(0.3, 1.6, tc), ck = reb ? 0 : 0.09 * sm(0, 1.2, tc), lw = L[15], fld = L[16], tick = floor(T * 20); + const scaf = lit([184, 186, 178], 1, 0.6), net = lit([64, 124, 104], 1, 0.4), slab = lit([130, 126, 118]), mast = lit([120, 124, 132], 1, 0.6); + for (let y = max(0, G - BT - 1); y < G; y++) { + let b = G - 1 + d1 + c2 - y, xo, up = 1; + if (b >= F0 + c2 && b < BT) xo = l1 + (l2 * (b - F0 - c2)) / (BT - F0); + else if ((b = G - 1 + d1 - y) >= 0 && b < F0) (xo = (l1 * b) / F0), (up = 0); + else continue; + const bi = floor(b), xs = round(xo + P.shk), s = RS[bi], fl = s >= 0 ? P.fls[s] : 0, rag = tc > 0 && !reb && (G - y < 3 || (up && c2 > 0 && b - F0 - c2 < 2)); + const top = sm(70, 25, y), hy = (y >> 1) * 3; + for (let c = 0; c < TW; c++) { + const j = bi * TW + c, k = K[j], x = X0 + c + xs; + if (!k || x < 0 || x >= W || (rag && hash(x * 13 + y, tick) < 0.4)) continue; + const i = y * W + x, o = i * 3, side = k === 4 || k === 5, fq = FL[i] * fld; + let col = MID; + if (reb && (bi < strip || (k > 5 && strip > BH))) { + if (bi >= built || k > 5) continue; + col = c % 3 === 0 || bi % 3 === 0 ? scaf : hash(c >> 2, bi >> 3) < 0.4 ? net : k === 3 ? slab : hash(c >> 1, s) < 0.12 ? LAMP : DARK; + } else if (k === 2 || k === 5) { + const v = side ? 0.65 : 1; + if (s >= 0 && P.bl[s]) col = HOLE; + else if (U[j] < lw) for (let q = 0, u = (0.7 + 0.3 * sin(U[j] * 60)) * v; q < 3; q++) MID[q] = WARM[q] * u; + else for (let q = 0; q < 3; q++) MID[q] = (SKY[hy + q] * 0.45 + L[6 + q] * 0.12 + 76 * fq) * v; + } else if (k === 7) col = mast; + else if (k === 8) col = (P.rt * 1.5) % 1 < 0.5 ? RED : HOLE; + else for (let q = 0, a = A[j] * (C[j] < ck ? 0.3 : 1), m = side ? 0.6 : 1, sn = (side ? 0.06 : 0.55) + top * 0.4; q < 3; q++) MID[q] = CON[q] * a * (L[9 + q] * m + L[12 + q] * sn + fq * 1.6); + const f2 = rag ? 0.55 : 1; + (F[o] = col[0] * f2 + fl * 300), (F[o + 1] = col[1] * f2 + fl * 250), (F[o + 2] = col[2] * f2 + fl * 170); + } + if (reb && bi < min(built, strip)) for (const x of [X0 - 1 + xs, X0 + TW + xs]) put(x, y, scaf); + } + // Each charge lights the air round its floor. + P.chg.forEach((s) => { + const fl = P.fls[s]; + if (fl > 0.02) for (let y = -6, yc = G - 1 - (P.st[s] + P.st[s + 1]) / 2; y <= 6; y++) for (let x = X0 - 9; x < X0 + TW + 9; x++) { + const dx = x < X0 ? X0 - x : x >= X0 + TW ? x - X0 - TW + 1 : 0; + if (dx) add(x, yc + y, [255, 196, 120], fl * exp(-(dx * dx) / 12 - (y * y) / 8)); + } + }); +} + +function pileH(x) { + const u = (x - CX) / 22; + return abs(u) >= 1 || P.ph < 0.3 ? 0 : P.ph * (1 - u * u) ** 0.8 * (0.8 + 0.4 * vn(x * 0.25, P.cyc * 7)) + (hash(x, P.cyc) - 0.5) * 1.6 * min(1, P.ph / 4); +} +// Rubble: broken slabs, rebar and bits of painted rooms; two pieces of facade still with their windows. +function pile() { + if (P.ph < 0.3) return; + for (let x = CX - 22; x <= CX + 22; x++) { + const hp = pileH(x), sl = pileH(x + 1) - pileH(x - 1), sn = cl(0.45 - 0.3 * sl, 0, 1); + for (let y = round(G - hp), y0 = y; y < G + 3; y++) { + const cx = floor((x + (y >> 1) * 1.7) / 3), cy = y >> 1, v = hash(cx, cy + P.cyc * 97), e = (y & 1) === 0 ? 1.18 : 0.86, dp = 1 - 0.35 * sm(0, 12, y - y0); + const c = v > 0.96 ? TILE : v < 0.05 ? BRICK : v > 0.9 ? PALE : CON, o = (y * W + x) * 3, f = fq(x, y); + let m = dp * (c === CON ? (0.65 + 0.45 * v) * e : 1); + if (y === y0 && hash(x, 9) < 0.3) m = -1; + else if (hash(cx * 7 + x, y * 3 + P.cyc) < 0.1) m *= 0.22; + if (y < H) for (let j = 0; j < 3; j++) F[o + j] = m < 0 ? RUST[j] * (L[9 + j] + 0.3) : c[j] * m * (L[9 + j] + L[12 + j] * sn + f); + } + } + if (P.ph > 6) + for (const [dx, w, sk, tl] of P.frag) { + const fx = CX + dx, fy = G - pileH(fx + w / 2) + 3; + for (let u = 0; u < w; u++) for (let v = 0; v < 7; v++) { + const hole = (v === 1 || v === 2 || v === 5) && u % 3 > 0; + put(fx + u + v * sk, fy - v + u * tl, hole ? [16, 14, 14] : lit(CON.map((q) => q * (v === 0 ? 0.7 : 0.95)), 1, 0.6, fq(fx, fy))); + } + } +} + +// Two long-reach diggers climb onto the heap and pick it apart, faster in the time-lapse. +function digger(x, f, ph) { + const g = round(G - max(0, pileH(x) - 1)), q = fq(x, g) + 0.32, Y = lit(YEL, 1, 0.6, q), Yd = Y.map((v) => v * 0.7), D = lit([70, 68, 66], 1, 0.4, q); + for (let i = -4; i <= 4; i++) put(x + i, g - 1, i & 1 ? D : D.map((v) => v * 1.8)), put(x + i, g - 2, D); + for (let i = -5; i <= 2; i++) for (let j = 3; j <= 5; j++) put(x + i * f, g - j, i < -3 ? Yd : Y); + for (let j = 6; j <= 9; j++) for (let i = 1; i <= 3; i++) put(x + i * f, g - j, i === 2 && j > 6 && j < 9 ? lit([90, 130, 170], 1, 0.8, q) : Y); + const a1 = 1 + 0.3 * sin(ph), bx = x + 2 * f, by = g - 5, ex = bx + f * cos(a1) * 11, ey = by - sin(a1) * 11, a2 = -1.2 + 0.5 * sin(ph + 1.3), kx = ex + f * cos(a2) * 8, ky = ey - sin(a2) * 8; + line(bx, by, ex, ey, Y), line(bx - f, by, ex - f, ey, Yd), line(ex, ey, kx, ky, Y); + for (let i = 0; i < 2; i++) for (let j = 0; j < 3; j++) put(kx + i * f, ky + j, D); + if ((P.rt * 1.6 + f * 0.25) % 1 < 0.5) glow(x + 2 * f, g - 10, AMBER, 0.45, 2); +} +function machines() { + const T = P.T; + if (T < T_EX || T > T_CLR + 1) return; + const tl = T > T_TL ? 1 : 0, out = sm(T_CLR - 1, T_CLR + 0.5, T); + for (const [x0, xa, f, j] of [[-10, CX - 13, 1, 0], [76, CX + 13, -1, 1]]) { + const x = mix(mix(x0, xa, sm(T_EX + j, T_EX + 2.5 + j, T)), x0, out) + tl * round((hash(floor(T * 4), j) - 0.5) * 6); + digger(round(x), f, T * (tl ? 9 : 2.2) + j * 2); + } +} + +// Fence with mesh, posts and barrier tape; the siren lights on two posts. +function fence() { + const mc = lit([150, 152, 158], 1, 0.4), pc = lit([96, 98, 104], 1, 0.5); + for (let y = 102; y < 118; y++) for (let x = 0; x < W; x++) { + if (x % 12 === 5 || y === 102) put(x, y, pc); + else if ((x + y) % 3 === 0 || (x - y + 300) % 3 === 0) put(x, y, mc, 0.28); + } + for (let x = 0; x < W; x++) put(x, 107 + round(sin(x * 0.6 + P.rt * 5) * (0.3 + abs(P.w))), (x + 40) % 4 < 2 ? lit([250, 210, 40], 1, 0.5, 0.15) : [20, 20, 20]); + const T = P.T; + for (const [x, s] of [[5, 0], [53, 0.5]]) { + const on = T < T_FALL + 0.8 && (P.rt * 2.5 + s) % 1 < 0.45; + put(x, 100, on ? [255, 190, 80] : [90, 50, 20]), put(x, 101, [60, 60, 60]); + if (on) glow(x, 100, AMBER, 0.6, 5); + } +} + +// Dust: each puff is a ball lit from the sun's side and from the sky above, laid over the ones before it, so the billows keep their edges. +function dust() { + const { NT, pf, FL } = P, fld = L[16] * 1.8, gf = P.gfl * 0.6, [L9, L10, L11, L12, L13, L14] = L.slice(9, 15); + for (let p = 0; p < P.np; p++) { + const q = p * PF, px = pf[q], py = pf[q + 1], r = pf[q + 4], age = pf[q + 8], life = pf[q + 9]; + const a = pf[q + 7] * sm(0, 0.25, age) * (1 - sm(0.4 * life, life, age)) * (pf[q + 5] / r) ** 0.35; + if (a < 0.01) continue; + const t = pf[q + 11], ox = (p * 23) & 63, oy = p * 41 + floor(age * 3), ir = 1 / r, pb = 0.85 + 0.3 * hash(p, P.cyc); + const a0 = mix(DUST0[0], DUST1[0], t) * pb, a1 = mix(DUST0[1], DUST1[1], t) * pb, a2 = mix(DUST0[2], DUST1[2], t) * pb; + for (let y = max(0, ceil(py - r)), y1 = min(H - 1, floor(py + r)); y <= y1; y++) { + const dy = (y - py) * ir, dy2 = dy * dy, row = y * W, nr = ((y + oy) & 127) << 6, n2 = ((y + oy + 64) & 127) << 6, sh = 0.45 + 0.55 * sm(G, 40, y), sk = 0.8 - 0.25 * dy; + for (let x = max(0, ceil(px - r)), x1 = min(W - 1, floor(px + r)); x <= x1; x++) { + const dx = (x - px) * ir, d2 = dx * dx + dy2; + if (d2 >= 1) continue; + let e = 2 - d2 * (0.7 + 2.4 * NT[nr | ((x + ox) & 63)]); + if (e <= 0.02) continue; + if (e > 1) e = 1; + const k = min(0.95, a * e * e * (3 - 2 * e)), i = row + x, o = i * 3, l = -0.6 * dx - 0.5 * dy + 0.62 * sqrt(1 - d2); + const gr = 0.6 + 0.8 * NT[n2 | ((x * 2 + ox) & 63)], sun = (0.25 + 0.8 * (l > 0 ? l : 0)) * sh * gr, sky = sk * (0.55 + 0.45 * gr), fl = FL[i] * fld + gf; + F[o] += (a0 * (L9 * sky + L12 * sun + fl) - F[o]) * k; + F[o + 1] += (a1 * (L10 * sky + L13 * sun + fl) - F[o + 1]) * k; + F[o + 2] += (a2 * (L11 * sky + L14 * sun + fl) - F[o + 2]) * k; + } + } + } +} + +function people() { + const T = P.T, ph = T > T_CH - 1.5 && T < T_TL ? 0.6 : 0.12, rim = lit([70, 70, 80], 1, 0.5, 0).map((v) => v + P.gfl * 120); + for (const [x, hy0, w, r1, r2, cc] of P.cr) { + const hy = hy0 - (T > T_FALL && T < T_TL && r2 < 0.35 && sin(P.rt * 9 + r2 * 40) > 0.6 ? 1 : 0), hw = w >> 1, coat = lit(COATS[cc & 3], 0.5, 0.25, 0.1), vest = cc === 4; + for (let y = hy + 3; y < H; y++) for (let i = -hw + (y === hy + 3); i <= hw - (w & 1 ? 0 : 1) - (y === hy + 3); i++) put(x + i, y, vest && y > hy + 3 && y < hy + 9 ? (y === hy + 6 ? lit([220, 220, 210], 1, 0.5, 0.3) : lit(COATS[4], 1, 0.5, 0.2)) : coat); + for (let j = 0; j < 3; j++) for (let i = -1; i <= 1; i++) if (j || !i) put(x + i, hy + j, SIL); + add(x, hy, rim, 0.5), add(x - hw + 1, hy + 3, rim, 0.35); + if (r1 < ph) { + put(x + 2, hy + 2, SIL), put(x + 2, hy + 1, SIL), put(x + 2, hy, SIL); + const fl = T > T_CH && T < T_TL - 8 && hash(P.fr, x) < 0.015; + put(x + 2, hy - 1, fl ? [255, 255, 255] : [150, 180, 240]), put(x + 2, hy - 2, fl ? [255, 255, 255] : [150, 180, 240]); + } + } +} + +export function draw(display, params) { + P = params.s; + if (!P || !P.L || !P.fk) return; + (F = P.F), (L = P.L); + back(); + tower(); + pile(); + machines(); + fence(); + for (const [x, y] of P.db) put(x, y, lit([150, 144, 136], 1, 0.6, fq(x, y))); + const wl = lit([170, 190, 210], 1, 0.5, 0.2); + for (const [x, y] of P.sp) add(x, y, wl, 0.16), add(x + 1, y, wl, 0.08), add(x, y + 1, wl, 0.08); + dust(); + for (const b of P.bd) { + const up = sin(b[7]) > 0 ? -1 : 0, c = L[16] > 0.7 ? [140, 140, 150] : [24, 24, 30]; + put(b[0], b[1], c), put(b[0] - 1, b[1] + up, c), put(b[0] + 1, b[1] + up, c); + } + people(); + for (let i = 0, o = 0; i < N; i++, o += 3) display.setPixel(i & 63, i >> 6, cl(F[o], 0, 255) | 0, cl(F[o + 1], 0, 255) | 0, cl(F[o + 2], 0, 255) | 0); +} From 4e1653e89b258b41e7f9d2503dda7e323905bb00 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 05:58:45 +0000 Subject: [PATCH 07/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Noborigama?= 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_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/patterns/noborigama.js | 602 ++++++++++++++++++ 1 file changed, 602 insertions(+) create mode 100644 incoming/pattern-study/patterns/noborigama.js diff --git a/incoming/pattern-study/patterns/noborigama.js b/incoming/pattern-study/patterns/noborigama.js new file mode 100644 index 000000000..07ffc3bc1 --- /dev/null +++ b/incoming/pattern-study/patterns/noborigama.js @@ -0,0 +1,602 @@ +// ===== Patternflow pattern ===== +// Title: Noborigama +// Family: pottery +// Idea: A climbing kiln cut open on a hillside at night: the flame climbs from the firebox through a chain of vaulted chambers stacked with pots, white-yellow below and orange above, while a potter with a lamp climbs the ladders to throw split wood into the stoke holes, and each stoke pushes flame out of the spy holes and black smoke and sparks out of the chimney into the stars. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 26, after the noborigama, the wood-fired climbing kiln of Japan and Korea +// Round: 26 +// =============================== +// @matrix 64x128 +// @knobs Firing=-1..1, Speed=0.2..3, Chambers=3..6, Draft=-1..1 +// +// Knob 1: Firing. How far the firing has come. At -1 it is the first day: only the firebox burns hot and the chambers glow dull red. At 0 the lower chambers are at white-yellow heat and the top one is still orange; at 1, the last day, every chamber is close to white. +// Knob 2: Speed. At 2 the potter stokes every few seconds and climbs from one chamber to the next in about three; at 0.2 everything runs ten times slower. +// Knob 3: Chambers. How many chambers climb the slope between the firebox and the chimney, three to six. With more, each one is lower and narrower and carries fewer shelves of pots. +// Knob 4: Draft. Wind over the hill, from the left at -1 to the right at 1. It leans the smoke, the sparks, the flames inside the chambers and the tongues out of the spy holes. +// Buttons: K1 tap stokes every hole at once: wood goes into each chamber and the firebox, flame bursts from every spy hole and the chimney throws out a big black cloud full of sparks. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, round, sin, sqrt, exp, abs, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const mix = (a, b, t) => a + (b - a) * t; +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +const rnd = () => Math.random(); +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 vn(x, y) { + const xi = floor(x), yi = floor(y), u = x - xi, v = y - yi, su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v); + const a = hash(xi, yi), b = hash(xi + 1, yi), c = hash(xi, yi + 1); + return a + (b - a) * su + (c - a) * sv + (a - b - c + hash(xi + 1, yi + 1)) * su * sv; +} + +// A strip of fine noise 256 rows tall that wraps, for the quick flicker in +// flame and smoke: cheaper to look up than to work out for every LED. +const FN = new Float32Array(64 * 256); +for (let y = 0; y < 256; y++) + for (let x = 0; x < 64; x++) { + const X = x * 0.9, Y = (y * 84) / 256, xi = floor(X), yi = floor(Y), u = X - xi, v = Y - yi, su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v); + const a = hash(xi, yi % 84), b = hash(xi + 1, yi % 84), c = hash(xi, (yi + 1) % 84), e = hash(xi + 1, (yi + 1) % 84); + FN[y * 64 + x] = a + (b - a) * su + (c - a) * sv + (a - b - c + e) * su * sv; + } + +// Brick, gas and flame by temperature: dull red at 0.15, orange at 0.6, white +// heat at 1. Red stops at 232: pinned at 255 it hid the flicker of the hottest +// chambers, since an LED reads by its brightest channel. +const RAMP = [[0, 24, 4, 2], [0.15, 92, 13, 4], [0.3, 158, 30, 6], [0.45, 204, 68, 12], [0.6, 226, 114, 24], [0.75, 232, 160, 50], [0.88, 232, 194, 96], [1, 232, 216, 160], [1.25, 236, 230, 204]]; +const LUT = new Float32Array(768); +for (let i = 0; i < 256; i++) { + const T = i / 204; + let k = 0; + while (k < RAMP.length - 2 && T > RAMP[k + 1][0]) k++; + const u = cl((T - RAMP[k][0]) / (RAMP[k + 1][0] - RAMP[k][0]), 0, 1); + for (let c = 0; c < 3; c++) LUT[i * 3 + c] = mix(RAMP[k][c + 1], RAMP[k + 1][c + 1], u); +} +const lut = (T) => cl(round(T * 204), 0, 255) * 3; + +// What each LED is, decided again whenever the number of chambers changes. +const SKY = 0, EARTH = 1, FILL = 2, WALL = 3, GAS = 4, POT = 5, SHELF = 6, HOLE = 7, FLUE = 8; +// A vault on low walls: the height of the roof across a chamber, 0.4 at the walls, 1 at the crown. +const arch = (u) => 0.4 + 0.6 * sqrt(max(0, 1 - (2 * u - 1) ** 2)); +// The chimney's mouth row. +const TOP = 18; +const WOOD = [150, 118, 66], TOWEL = [214, 204, 184], FACE = [176, 116, 78], COAT = [46, 60, 128], LEGS = [74, 62, 54]; +// Jar, flared bowl, bottle and cup: half width up the pot, bottom to top. +const prof = (k, t) => (k === 0 ? (t > 0.84 ? 0.5 : 0.6 + 0.4 * sin(PI * t)) : k === 1 ? 0.5 + 0.5 * t : k === 2 ? (t < 0.55 ? 0.55 + 0.45 * sin((PI * t) / 0.55) : t > 0.88 ? 0.5 : 0.3) : 0.8); + +export function setup(params) { + const F32 = (n) => new Float32Array(n); + const S = (params.s = { + t: 0, n: 0, k1: 0, wind: 0.06, cf: 2, + kind: new Uint8Array(N), ch: new Uint8Array(N), V: F32(N), J: F32(N), bg: F32(N * 3), gw: F32(N), f: F32(N * 3), A: F32(W * 64), K: F32(W * 64), Q: F32(W * 64), + T: F32(8).fill(-1), Hc: F32(8).fill(-1), age: F32(8).fill(9), q: F32(8), open: F32(8), Te: F32(8), Br: F32(8), + puffs: [], sparks: [], wood: [], logs: [], slugs: [], ev: [], stars: [], motes: [], + pot: { A: 0, tgt: 0, mode: 1, tm: -1.6, thr: 0, k: 0, ph: 0 }, fb: { tm: 1, an: 0 }, flare: 0, np: 0, ns: 0, hand: 0, + }); + for (let k = 0; k < 40; k++) S.motes.push({ x: rnd() * W, y: rnd() * H, e: rnd() < 0.45 ? 1 : 0, ph: rnd() * 7 }); + // Logs already burning in the firebox. + for (let k = 0; k < 3; k++) S.logs.push({ c: 0, x: 8 + k * 6 + rnd() * 3, y: 120 + (k & 1), len: 5, life: 0.4 + 0.5 * rnd() }); +} + +const front = (S) => ((S.k1 + 1) / 2) * (S.n + 0.3); + +function build(S, n) { + S.n = n; + const P = 77 / n, h = P - 3, Wc = 34 - 2 * n, s = (43 - Wc) / (n - 1); + // The firebox, then the chambers up the slope, each P rows above the last. + const g = (S.g = [{ L: 6, W: 25, F: 123, h: 11 }]); + for (let c = 1; c <= n; c++) g.push({ L: 14 + (c - 1) * s, W: Wc, F: 108 - (c - 1) * P, h }); + const inT = (S.inT = (k, X) => { + const q = g[k], u = (X - q.L) / q.W; + return u > 0 && u < 1 ? q.F - q.h * arch(u) : 1e3; + }); + const outT = (k, X) => { + const q = g[k], u = (X - q.L + 2) / (q.W + 4); + return u > 0 && u < 1 ? q.F - (q.h + 2) * arch(u) : 1e3; + }; + const tp = g[n], CX = (S.CX = tp.L + 0.62 * tp.W); + const hill = (X) => (X < 4 ? 123 : X < tp.L - 2 ? mix(123, tp.F + 2, (X - 4) / (tp.L - 6)) : mix(tp.F + 2, tp.F - 5, (X - tp.L + 2) / (66 - tp.L))); + // Flame passages through the floor of each chamber, over the crown of the one below. + const holes = []; + for (let c = 1; c <= n; c++) { + const a = g[c - 1], b = g[c], xc = a.L + a.W / 2; + for (let x = round(xc - 0.3 * a.W); x <= xc + 0.3 * a.W; x += 4) if (x > b.L && x < b.L + b.W - 2) holes.push([x, c]); + } + const { kind, ch, V, bg, gw } = S; + S.stars = []; + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, X = x + 0.5, Y = y + 0.5, o = i * 3; + let k = SKY, c = 0, v = 0; + for (let j = 0; j <= n && !k; j++) if (Y < g[j].F && Y > inT(j, X)) (k = GAS), (c = j), (v = (g[j].F - Y) / g[j].h); + if (!k && abs(X - CX) < 2 && Y > TOP && Y < inT(n, X) + 0.5) (k = FLUE), (c = n), (v = (Y - TOP) / (inT(n, CX) - TOP)); + if (!k) for (const [hx, hc] of holes) if (x === hx && Y > g[hc].F && Y < inT(hc - 1, X)) (k = HOLE), (c = hc); + for (let j = 0; j <= n && !k; j++) if (Y < g[j].F + 2 && Y > outT(j, X)) (k = WALL), (c = j); + if (!k && Y > TOP && ((abs(X - CX) < 4 && Y < outT(n, X) + 1) || (Y < TOP + 2 && abs(X - CX) < 5))) (k = WALL), (c = n + 1); + for (let j = 1; j <= n && !k; j++) if (X > g[j].L - 2 && X < g[j].L + g[j].W + 2 && Y > g[j].F + 2) k = FILL; + if (!k && Y > hill(X)) k = EARTH; + // The fire mouth in the firebox's front wall. + if (k === WALL && !c && x < 6 && y > 116) (k = GAS), (v = (123 - Y) / 11); + kind[i] = k; + ch[i] = c; + V[i] = v; + const r = hash(x, y), nz = 0.8 + 0.4 * vn(x * 0.3, y * 0.3); + let col; + if (k === SKY) { + const u = sm(0, 112, Y), dm = Math.hypot(X - 11, Y - 11), cut = Math.hypot(X - 12.6, Y - 10); + col = [mix(5, 20, u), mix(9, 26, u), mix(32, 62, u)]; + if (dm < 3.6 && cut > 3.2) col = [226, 222, 196]; + else if (dm < 12) for (let j = 0; j < 3; j++) col[j] += [24, 26, 30][j] * exp(-dm / 3.5); + else if (r < 0.014 && y < 100) { + const b = 70 + 140 * hash(y, x); + col = col.map((v2, j) => v2 + b * [0.95, 0.95, 1][j]); + S.stars.push(i); + } + } else if (k === EARTH) { + // Beds of clay and gravel along the slope, stones, and grass on top. + const dd = Y - hill(X), bd = 0.75 + 0.5 * vn(dd * 0.3 + x * 0.04, 3.5 + x * 0.02); + col = dd < 1.5 ? [26, 40, 26] : r < 0.035 ? [62, 56, 50] : [34 * bd * nz, 26 * bd * nz, 21 * bd * nz]; + } + else if (k === FILL) col = [52 * nz, 36 * nz, 28 * nz]; + else if (k === WALL) { + const m = c > n ? y % 3 === 0 || (x + (floor(y / 3) & 1) * 2) % 4 === 0 : r < 0.25; + col = [96, 66, 50].map((v2) => v2 * (m ? 0.62 : 0.85 + 0.3 * r)); + } else col = [0, 0, 0]; + bg[o] = col[0]; + bg[o + 1] = col[1]; + bg[o + 2] = col[2]; + } + // Inner face of each wall, the brick the fire touches. + for (let i = 0; i < N; i++) + if (kind[i] === WALL) { + const x = i & 63, y = i >> 6; + let hot = 0; + for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { + const X = x + dx, Y = y + dy; + if (X >= 0 && X < W && Y >= 0 && Y < H && kind[Y * W + X] >= GAS) hot = 1; + } + V[i] = hot; + } + // The earth and the air round the kiln are warm: how near each LED is to a wall, and which chamber's. + gw.fill(0); + for (let i = 0; i < N; i++) + if (kind[i] <= FILL) { + const x = i & 63, y = i >> 6; + let best = 9; + for (let dy = -4; dy <= 4; dy++) + for (let dx = -4; dx <= 4; dx++) { + const X = x + dx, Y = y + dy, d = abs(dx) + abs(dy); + if (X >= 0 && X < W && Y >= 0 && Y < H && kind[Y * W + X] === WALL && d < best) (best = d), (ch[i] = ch[Y * W + X]); + } + gw[i] = best < 9 ? exp(-best / 1.4) * (kind[i] ? 0.8 : 0.6) : 0; + } + // Flame jets over each passage in a chamber's floor, and the pull towards the way out at its crown. + const J = S.J.fill(0); + for (let i = 0; i < N; i++) { + if (kind[i] !== GAS || !ch[i]) continue; + const x = i & 63, c = ch[i], v = V[i], xo = c === n ? CX : g[c].L + g[c].W / 2; + let e = 0.5 * exp(-abs(x + 0.5 - xo) / 3) * sm(0.55, 1, v); + for (const [hx, hc] of holes) if (hc === c) e = max(e, exp(-abs(x - hx) / 1.3 - v * 2.2)); + J[i] = e; + } + // Shelves and pots: two or three levels a chamber, under the vault. + for (let c = 1; c <= n; c++) { + const q = g[c], nl = q.h >= 16 ? 3 : 2, lh = q.h / nl, x0 = round(q.L + max(6, 0.22 * q.W)), x1 = floor(q.L + q.W - 1); + for (let j = 0; j < nl; j++) { + const ys = round(q.F - j * lh), base = ys - 1; + const set = (x, y, k2, v2) => { + const i = y * W + x; + if (x >= 0 && x < W && kind[i] === GAS && ch[i] === c) (kind[i] = k2), (V[i] = v2); + }; + if (j) { + for (let x = x0; x <= x1; x++) if (inT(c, x + 0.5) < ys - 1) set(x, ys, SHELF, 0); + for (let y = ys + 1; y < round(q.F - (j - 1) * lh) - (j > 1 ? 0 : 0); y++) set(x0, y, SHELF, 0), set(x1 - 1, y, SHELF, 0); + } + for (let x = x0 + 1; x < x1 - 1; ) { + const ty = floor(hash(c * 7 + j, x) * 4), w = [5, 5, 4, 3][ty]; + let ph = min(floor(lh - 2), [5, 3, 6, 4][ty]); + const room = floor(base - max(inT(c, x + 0.5), inT(c, x + w - 0.5)) - 1); + ph = min(ph, room); + if (ph < 2 || x + w > x1) break; + for (let r = 0; r < ph; r++) { + const rad = (prof(ty, (r + 0.5) / ph) * w) / 2; + for (let xx = x; xx < x + w; xx++) { + const dx = xx + 0.5 - x - w / 2; + if (abs(dx) <= rad + 0.15) set(xx, base - r, POT, 0.5 - dx / w + 0.2 * (r === ph - 1 ? 1 : 0)); + } + } + x += w + 1; + } + } + } + // Ladders up the front of each chamber, and the potter's way from one stoke hole to the next. + const xs = (c) => g[c].L - 4; + const px = [], py = []; + S.lad = []; + S.Ac = [0]; + S.spy = [0]; + for (let c = 1; c <= n; c++) { + const yw = outT(c, g[c].L - 1.5); + S.lad.push([round(xs(c)), yw, c === 1 ? outT(0, xs(1)) : outT(c - 1, xs(c))]); + S.spy.push(round(g[c].F - max(5, 0.35 * h))); + S.Ac.push(px.length); + px.push(xs(c)), py.push(g[c].F + 2); + if (c < n) { + px.push(xs(c)), py.push(yw); + for (let x = g[c].L - 1.5; x < xs(c + 1); x += 1.5) px.push(x), py.push(outT(c, x)); + px.push(xs(c + 1)), py.push(outT(c, xs(c + 1))); + } + } + const pl = [0]; + for (let j = 1; j < px.length; j++) pl.push(pl[j - 1] + Math.hypot(px[j] - px[j - 1], py[j] - py[j - 1])); + Object.assign(S, { px, py, pl }); + S.Ac = S.Ac.map((j, c) => (c ? pl[j] : 0)); + // Where the potter stands now: by the chamber above the one being fired, the first time. + const pt = S.pot; + if (!S.built) (S.built = 1), (pt.tgt = min(n, cl(round(front(S)), 1, n) + 1)); + pt.tgt = min(pt.tgt, n); + pt.A = S.Ac[pt.tgt]; + if (pt.mode === 0) pt.mode = 2; + S.logs = S.logs.filter((l) => l.c <= n); + S.wood = []; + S.slugs = []; +} + +// A stoke as the kiln feels it: reduction first, the flame starved and smoky, then the surge. +const red = (a) => (a < 0.2 ? a / 0.2 : exp(-(a - 0.2) / 1.1)); +const surge = (a) => sm(0.9, 2.1, a) * exp(-max(0, a - 2.1) / 2.4); + +function pulse(S, c, s) { + S.age[c] = 0; + S.q[c] = s; + S.ev.push({ at: 0.25 + 0.12 * (S.n - c), fn: () => S.slugs.push({ y: S.inT(S.n, S.CX), a: 0.5 + 0.5 * s }) }); +} + +function stoke(S, c) { + const q = S.g[c]; + pulse(S, c, 1); + if (c) S.logs.push({ c, x: q.L + 0.5 + rnd() * 2, y: round(q.F) - 1, len: 3 + floor(rnd() * 3), life: 1 }); + else { + S.logs.push({ c, x: q.L + 2 + rnd() * 9, y: 120 + floor(rnd() * 2), len: 4 + floor(rnd() * 3), life: 1 }); + // The firebox's flame goes on through the first chamber. + S.ev.push({ at: 0.35, fn: () => pulse(S, 1, 0.6) }); + } +} + +function throwTo(S, c, x0, y0, delay = 0) { + const q = S.g[c]; + S.wood.push({ x0, y0, x1: q.L - 0.5, y1: round(q.F) - 2.5, a: -delay, c }); + S.open[c] = 1.3 + delay; +} + +function puff(S, x, y, vy, r, a, k, hot) { + if (S.puffs.length > 110) S.puffs.shift(); + S.puffs.push({ x, y, vy, r, a, k, hot, ph: rnd() * 7 }); +} + +function spark(S, x, y, s) { + if (S.sparks.length > 160) S.sparks.shift(); + const life = 0.8 + 1.4 * rnd(); + S.sparks.push({ x, y, vx: (rnd() - 0.5) * 6 * s, vy: -(10 + 14 * rnd()) * s, life, L: life }); +} + +// Black smoke reaching the top of the chimney: a cloud, a flare and a fountain of sparks. +function belch(S, a) { + for (let j = 0; j < 3; j++) puff(S, S.CX + (rnd() - 0.5) * 3, TOP - 1 - j * 2, -11 - 6 * rnd(), 2.6 + rnd(), 0.6 + 0.6 * a, 1, 1); + for (let j = 0; j < 14 * a; j++) spark(S, S.CX + (rnd() - 0.5) * 3, TOP - 1, 1.3); + S.flare = min(1.4, S.flare + a); +} + +function stokeAll(S) { + for (let c = 1; c <= S.n; c++) throwTo(S, c, S.g[c].L - 7, S.g[c].F - 6, 0.05 * c); + throwTo(S, 0, 1, 115); + S.hand = 0.6; +} + +const posAt = (S, A) => { + const { px, py, pl } = S; + let j = 1; + while (j < pl.length - 1 && pl[j] < A) j++; + const u = cl((A - pl[j - 1]) / max(1e-6, pl[j] - pl[j - 1]), 0, 1); + return [mix(px[j - 1], px[j], u), mix(py[j - 1], py[j], u), abs(px[j] - px[j - 1]) < 0.01]; +}; + +function step(S, d) { + S.t += d; + const { n, T, age, q, Hc, Te, Br, wind } = S, fr = front(S), peak = 0.5 + 0.45 * sm(0, 0.5, (S.k1 + 1) / 2); + S.cf = cl(round(fr), 1, n); + for (let c = 0; c <= n; c++) { + const tg = max(0.2, peak - (1.1 / n) * max(0, c - fr)); + T[c] = T[c] < 0 ? tg : T[c] + (tg - T[c]) * min(1, d * 0.8); + age[c] += d; + // Wind up the hill pulls on the chimney and the fire burns hotter; wind down it chokes the draft. + Te[c] = T[c] + q[c] * (0.1 * surge(age[c]) - 0.15 * red(age[c])) + 0.02 * sin(S.t * 1.3 + c * 2.1) + 0.06 * wind; + Br[c] = 1 - 0.36 * q[c] * red(age[c]); + if (Hc[c] < 0) Hc[c] = T[c] - 0.1; + // Heat work for the cones: it creeps up with the temperature and hardly comes back. + Hc[c] += (Te[c] - Hc[c]) * d * (Te[c] > Hc[c] ? 0.07 : 0.02); + S.open[c] = max(0, S.open[c] - d); + } + Te[n + 1] = Te[n] - 0.22; + Br[n + 1] = Br[n]; + for (const e of S.ev) if ((e.at -= d) <= 0) e.fn(); + S.ev = S.ev.filter((e) => e.at > 0); + + // The potter: stoke twice, watch the spy hole, then go where the fire needs wood. + const pt = S.pot; + pt.ph += d; + if (pt.mode === 0) { + const goal = S.Ac[pt.tgt]; + pt.A += cl(goal - pt.A, -11 * d, 11 * d); + if (abs(goal - pt.A) < 1e-3) (pt.mode = 1), (pt.tm = 0), (pt.thr = 0); + } else if (pt.mode === 1) { + pt.tm += d; + if (pt.thr < 2 && pt.tm > 0.3 + 0.7 * pt.thr) { + const [x, y] = posAt(S, pt.A); + throwTo(S, pt.tgt, x - 1, y - 8); + pt.thr++; + } + if (pt.tm > 1.8) (pt.mode = 2), (pt.tm = 0.8 + rnd()); + } else if ((pt.tm -= d) <= 0) { + pt.k++; + pt.tgt = pt.k % 3 === 0 ? min(n, S.cf + 1) : S.cf; + pt.mode = 0; + } + // The firebox stoker feeds the mouth. + const fb = S.fb; + fb.an = max(0, fb.an - d); + if ((fb.tm -= d) <= 0) (fb.tm = 3 + rnd()), (fb.an = 0.5), throwTo(S, 0, 2.5, 116); + S.hand = max(0, S.hand - d); + for (const w of S.wood) if ((w.a += d / 0.3) >= 1) (w.done = 1), stoke(S, w.c); + S.wood = S.wood.filter((w) => !w.done); + for (const l of S.logs) l.life -= d * (0.12 + 0.2 * T[l.c]); + S.logs = S.logs.filter((l) => l.life > 0); + // Black smoke climbing the chimney. + for (const s of S.slugs) if ((s.y -= d * 30) < TOP) (s.done = 1), belch(S, s.a); + S.slugs = S.slugs.filter((s) => !s.done); + S.flare = max(0.15 + 0.35 * sm(0.6, 1, T[n]), S.flare * exp(-d * 1.6)); + air(S, d); + // Ash falls and embers float, across the whole hillside. + for (const m of S.motes) { + m.x += (wind * 9 + 1 + 1.5 * sin(S.t * 0.7 + m.ph)) * d * (m.e ? 1.3 : 1); + m.y += ((m.e ? -3.5 : 2.2) + 0.8 * sin(S.t * 1.3 + m.ph * 2)) * d; + m.x = ((m.x % 65) + 65) % 65; + m.y = ((m.y % 129) + 129) % 129; + } +} + +// Smoke: a steady grey plume, slowing and spreading under the top of the panel, and sparks. +function air(S, d) { + const { n, T, wind } = S; + for (S.np += d * 6; S.np >= 1; S.np--) puff(S, S.CX + (rnd() - 0.5) * 2, TOP - 1, -7 - 3 * rnd(), 3, 0.55 + 0.2 * T[n], 0, 0.7); + for (const p of S.puffs) { + // A strong wind bends the plume over and draws it out downwind. + p.vy += (-3.5 * (1 - 0.55 * abs(wind)) * cl(p.y / 12, 0.3, 1) - p.vy) * d * 1.2; + p.y += p.vy * d; + p.x += (wind * 18 * (0.5 + 0.5 * cl((TOP - p.y) / TOP, 0, 1)) + 1.2 * sin(S.t * 0.9 + p.ph)) * d; + p.r = min(10, p.r + d * (1.5 + 1 * p.k + 2 * cl((8 - p.y) / 8, 0, 1))); + p.a -= d * (0.05 + 0.04 * (1 - p.k)); + p.hot = max(0, p.hot - d * (1.4 - 0.9 * p.k)); + } + S.puffs = S.puffs.filter((p) => p.a > 0.03 && p.y + p.r > -3 && p.x > -14 && p.x < W + 14); + for (S.ns += d * (2 + 5 * T[n]) * (1 + 0.6 * wind); S.ns >= 1; S.ns--) spark(S, S.CX + (rnd() - 0.5) * 3, TOP - 1, 1); + for (const p of S.sparks) { + p.vy += (-6 - p.vy) * d * 1.5; + p.vx += ((rnd() - 0.5) * 40 + (wind * 16 - p.vx) * 2) * d; + p.x += p.vx * d; + p.y += p.vy * d; + p.life -= d; + } + S.sparks = S.sparks.filter((p) => p.life > 0 && p.y > -1); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 3, 0.06]; + S.k1 = cl(k[0], -1, 1); + S.wind = cl(k[3], -1, 1); + const n = cl(round(k[2]), 3, 6); + if (n !== S.n) build(S, n); + // The kiln has been smoking for hours before you look. + if (!S.warm) { + S.warm = 1; + step(S, 0); + for (let j = 0; j < 75; j++) air(S, 1 / 30); + } + if (input.btnPressed && input.btnPressed[0]) stokeAll(S); + step(S, cl(dt || 0, 0, 0.1) * (cl(k[1], 0.2, 3) / 2)); +} + +let F; +function put(x, y, c, a = 1) { + (x = floor(x)), (y = floor(y)); + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + for (let i = 0; i < 3; i++) F[o + i] = mix(F[o + i], c[i], a); +} +function add(x, y, c, a) { + (x = floor(x)), (y = floor(y)); + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + for (let i = 0; i < 3; i++) F[o + i] += c[i] * a; +} +function glow(x, y, r, c, a) { + for (let Y = floor(y - r); Y <= y + r; Y++) for (let X = floor(x - r); X <= x + r; X++) add(X, Y, c, a * max(0, 1 - Math.hypot(X + 0.5 - x, Y + 0.5 - y) / r) ** 2); +} +// Colours handed to put() come from a ring of four arrays, to spare the garbage collector. +const RING = [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]; +let ri = 0; +const lc = (T, b = 1) => { + const r = RING[(ri = (ri + 1) & 3)]; + T = lut(T); + (r[0] = LUT[T] * b), (r[1] = LUT[T + 1] * b), (r[2] = LUT[T + 2] * b); + return r; +}; + +// A figure nine LEDs tall, feet on row y, facing the kiln on the right: a +// white towel, an indigo jacket lit orange on the kiln side, and a paper lantern. +function figure(x, y, pose, ph, rim, lamp) { + (x = round(x)), (y = round(y)); + const st = floor(ph * 6) & 1, rc = [255, 150, 60]; + const pts = [[-1, -8, TOWEL], [0, -8, TOWEL], [0, -7, FACE, 1]]; + for (let dy = -6; dy <= -4; dy++) pts.push([-1, dy, COAT], [0, dy, COAT], [1, dy, COAT, 1]); + const L = (a, b2) => pts.push([a, -3, LEGS], [b2, -2, LEGS], [b2, -1, LEGS], [b2, 0, LEGS]); + if (pose === 1 && st) L(0, 0); + else if (pose === 2) L(st ? -1 : 1, st ? -1 : 1), L(st ? 1 : -1, st ? 1 : -1), pts.push([st ? -1 : 1, -7, COAT, 1], [st ? 1 : -1, -3, LEGS]); + else L(-1, -1), L(1, 1); + // Throwing: the arm swings the wood from behind the head into the hole. + if (pose === 3) pts.push(...(ph < 0.5 ? [[-1, -7, COAT], [-2, -8, COAT]] : [[2, -5, COAT, 1], [3, -5, FACE, 1]])); + for (const [dx, dy, c, k] of pts) put(x + dx, y + dy, k ? c.map((v, i) => mix(v, rc[i], rim)) : c); + if (lamp) { + put(x - 2, y - 4, [120, 70, 40]); + put(x - 2, y - 3, [255, 222, 150]), put(x - 2, y - 2, [240, 160, 80]); + glow(x - 1.5, y - 2, 7, [70, 44, 16], 1); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.n) return; + F = S.f; + const { n, t, kind, ch, V, J, bg, gw, Te, Br, g, wind, A, K, Q } = S, lean = wind * 2.2 + 0.8, fs = 1 + 0.45 * wind, fo = floor(t * fs * 17); + for (let i = 0, o = 0; i < N; i++, o += 3) { + const k = kind[i], c = ch[i]; + if (k <= FILL) { + (F[o] = bg[o]), (F[o + 1] = bg[o + 1]), (F[o + 2] = bg[o + 2]); + if (gw[i] > 0.02) { + const j = lut(Te[c] * 0.7), w = gw[i] * 0.28 * Br[c]; + (F[o] += LUT[j] * w), (F[o + 1] += LUT[j + 1] * w), (F[o + 2] += LUT[j + 2] * w); + } + continue; + } + const x = i & 63, y = i >> 6, X = x + 0.5, Y = y + 0.5, T = Te[c], B = Br[c]; + let j = 0, b = 1; + if (k === WALL) { + const hot = V[i]; + j = lut(T * 0.85 - 0.05); + b = B * (hot ? 0.5 : 0.1); + (F[o] = bg[o] * (hot ? 0.45 : 0.95) + LUT[j] * b), (F[o + 1] = bg[o + 1] * (hot ? 0.45 : 0.95) + LUT[j + 1] * b), (F[o + 2] = bg[o + 2] * (hot ? 0.45 : 0.95) + LUT[j + 2] * b); + continue; + } + if (k === GAS) { + const v = V[i]; + const n1 = vn(X * 0.45 - lean * (0.3 + v) * 3 + c * 19, (Y + t * fs * 10) * 0.16), n2 = FN[((y + fo) & 255) * 64 + x]; + let I = sm(0.25, 0.85, 0.65 * n1 + 0.35 * n2 + 0.22 * (1 - v) - 0.12 + 0.45 * J[i]); + // The firebox floor: a bed of embers under the logs. + if (!c && v < 0.3) I = sm(0.35, 0.85, 0.7 * vn(X * 0.7, Y * 0.9 + t * 0.4) + 0.3 * vn(X * 1.7 + t, Y * 1.5) + 0.1); + // Darker flame is cooler, so it goes orange rather than grey. The firebox burns rougher. + const r = c ? 0.14 : 0.26; + j = lut(T - r + 2 * r * I); + b = B * (1 - 2 * r + 2 * r * I); + } else if (k === POT) (j = lut(T - 0.18)), (b = B * (0.32 + 0.22 * V[i] + 0.14 * FN[((y + (fo >> 1)) & 255) * 64 + (x >> 1)])); + else if (k === SHELF) (j = lut(T - 0.2)), (b = B * 0.4); + else if (k === HOLE) (j = lut(mix(Te[c - 1], T, 0.5) + 0.04)), (b = 0.85 + 0.15 * sin(t * 9 + x)); + else { + // The flue: hot gas rising in bands, and the black smoke of each stoke going up through it. + let dk = 0; + for (const s of S.slugs) dk = max(dk, s.a * exp(-(((Y - s.y) / 3) ** 2))); + j = lut(T - 0.12 - 0.2 * V[i] + 0.1 * vn(X * 0.6, (Y + t * 22) * 0.3)); + b = B * (0.6 + 0.3 * vn(X * 0.5 + 9, (Y + t * 26) * 0.2)) * (1 - 0.85 * dk); + } + (F[o] = LUT[j] * b), (F[o + 1] = LUT[j + 1] * b), (F[o + 2] = LUT[j + 2] * b); + } + // Stars twinkle. + for (const i of S.stars) if (kind[i] === SKY) for (let c = 0; c < 3; c++) F[i * 3 + c] *= 0.75 + 0.25 * sin(t * 2.3 + i); + + // Burning wood on the chamber floors and in the firebox. + for (const l of S.logs) { + const T = Te[l.c], len = max(1, round(l.len * min(1, l.life * 1.6))); + for (let k = 0; k < len; k++) { + put(l.x + k, l.y, (k + floor(t * 4 + l.x)) % 3 ? lc(T - 0.45, 0.45) : lc(T + 0.1)); + // A log in the firebox is thicker, its top edge burning. + if (!l.c) put(l.x + k, l.y - 1, lc(T - 0.1 + 0.15 * sin(t * 7 + k + l.x)), 0.85); + } + } + // Holes in the front walls: the stoke hole, open while wood goes in, and the spy hole with its cones. + for (let c = 1; c <= n; c++) { + const q = g[c], T = Te[c], wx = floor(q.L - 1.5), sy = S.spy[c], fy = round(q.F), op = sm(0, 0.3, S.open[c]); + for (let y = fy - 3; y <= fy - 2; y++) for (let x = wx; x <= wx + 1; x++) put(x, y, op > 0.05 ? lc(T + 0.05, op) : lc(T - 0.3, 0.35)); + if (op > 0.05) glow(wx - 1, fy - 2.5, 6, lc(T - 0.1), 0.35 * op); + put(wx, sy, lc(T, 0.9)), put(wx + 1, sy, lc(T)); + // Three cones on a pad: each bends over once the heat work passes its number. + for (let x = wx + 2; x <= wx + 6; x++) put(x, sy + 2, lc(T - 0.2, 0.4)); + for (let m = 0; m < 3; m++) { + const b2 = sm(0.62 + 0.12 * m - 0.05, 0.62 + 0.12 * m + 0.06, S.Hc[c]), cx = wx + 2 + 2 * m, cc = lc(T - 0.15, 0.5); + put(cx, sy + 1, cc); + if (b2 < 0.75) put(cx + (b2 > 0.4 ? 1 : 0), sy, cc); + put(cx + (b2 > 0.4 ? 1 : 0), b2 < 0.75 ? sy - 1 : sy + 1, cc); + if (b2 >= 0.75) put(cx + 1, sy, cc, 0.5); + } + } + // Ladders. + for (const [lx, y0, y1] of S.lad) + for (let y = floor(y0); y <= y1; y++) { + put(lx - 1, y, WOOD, 0.7), put(lx + 1, y, WOOD, 0.7); + if (!(y % 3)) put(lx, y, WOOD, 0.6); + } + // Tongues of flame out of the spy holes after a stoke, leaning with the draft. + for (let c = 1; c <= n; c++) { + const s = max(S.q[c] * red(S.age[c]) * sm(0.15, 0.35, S.age[c]), 0.55 * S.q[c - 1] * red(S.age[c - 1] - 0.3) * sm(0.4, 0.6, S.age[c - 1])) + 0.3 * max(0, -wind) * (0.7 + 0.3 * sin(t * 3 + c)); + if (s < 0.05) continue; + const x0 = floor(g[c].L - 1.5) - 0.5, y0 = S.spy[c] + 0.5, len = 2 + 9 * s * (0.8 + 0.2 * sin(t * 13 + c)), T = Te[c]; + glow(x0, y0, 5, [120, 50, 10], s); + for (let j = 0; j < len; j++) { + const u = j / len, x = x0 - j * (1 - 0.6 * max(0, wind)) + wind * j * 0.4, y = y0 - j * j * 0.06 - j * 0.15 + sin(t * 17 + j) * 0.4 * u, cc = lc(T + 0.3 - 0.6 * u); + put(x, y, cc, 1 - u * 0.3); + if (u < 0.7) put(x, y - 1, lc(T + 0.1 - 0.6 * u), 0.8 - u), put(x, y + 1, lc(T - 0.6 * u), 0.5 - 0.5 * u); + } + } + // The flying wood. + for (const w of S.wood) { + if (w.a < 0) continue; + const x = mix(w.x0, w.x1, w.a), y = mix(w.y0, w.y1, w.a) - 2 * sin(PI * w.a); + put(x, y, [196, 150, 96]), put(x - 1, y + (w.a < 0.5 ? 1 : 0), [130, 86, 50]); + } + // The potter and the firebox stoker. + const pt = S.pot, [px, py, climb] = posAt(S, pt.A), cc = S.cf; + const pose = S.hand > 0 || pt.mode === 1 ? 3 : pt.mode === 0 ? (climb ? 2 : 1) : 0; + const ph = pose === 3 ? (S.hand > 0 ? 1 - S.hand / 0.6 : (pt.tm % 0.7) / 0.7) : pt.ph; + figure(px, py, pose, ph, 0.25 + 0.6 * sm(0, 0.3, S.open[pt.tgt]) * (pt.mode === 1 ? 1 : 0), 1); + const fbo = S.fb.an > 0 ? 3 : 0; + figure(1, 122, fbo, 1 - S.fb.an / 0.5, 0.5 + 0.3 * sin(t * 7), 0); + glow(5, 120, 6, lc(Te[0] - 0.1), 0.35); + void cc; + + // Smoke: every puff summed over the top half, then shaded: grey lit by the fire, black after a stoke. + A.fill(0), K.fill(0), Q.fill(0); + for (const p of S.puffs) { + const r = p.r; + for (let y = max(0, floor(p.y - r)); y <= min(63, p.y + r); y++) + for (let x = max(0, floor(p.x - r)); x <= min(W - 1, p.x + r); x++) { + const dx = (x + 0.5 - p.x) / r, dy = (y + 0.5 - p.y) / r, w = 1 - dx * dx - dy * dy; + if (w > 0) { + const a = p.a * w * w, i = y * W + x; + (A[i] += a), (K[i] += a * p.k), (Q[i] += a * p.hot); + } + } + } + for (let i = 0; i < W * 64; i++) { + if (A[i] < 0.01) continue; + const x = i & 63, y = i >> 6, o = i * 3, tx = 0.6 * vn(x * 0.25 - t * wind, y * 0.25 + t * 0.8) + 0.4 * FN[((y + (fo >> 2)) & 255) * 64 + x], al = min(0.95, (1 - exp(-A[i] * 2.2)) * (0.6 + 0.6 * tx)); + const dk = K[i] / A[i], hq = min(1, Q[i] / A[i]) * (0.4 + 0.6 * sm(30, 10, y)), gr = 0.7 + 0.5 * tx; + F[o] = mix(F[o], mix(118 * gr, 16, dk) + 160 * hq, al); + F[o + 1] = mix(F[o + 1], mix(106 * gr, 11, dk) + 60 * hq, al); + F[o + 2] = mix(F[o + 2], mix(104 * gr, 10, dk) + 14 * hq, al); + } + // Flame licking out of the chimney's mouth. + const fl = S.flare, CX = S.CX; + glow(CX, TOP, 6, [110, 40, 8], fl * 0.8); + for (let j = 0; j < 2 + 7 * fl; j++) { + const u = j / (2 + 7 * fl), x = CX + wind * j * 0.6 + sin(t * 11 + j) * 0.6 * u; + put(x, TOP - 1 - j, lc(Te[n] - 0.05 - 0.45 * u), 1 - 0.5 * u); + if (u < 0.5) put(x - 1, TOP - 1 - j, lc(Te[n] - 0.25 - 0.4 * u), 0.6), put(x + 1, TOP - 1 - j, lc(Te[n] - 0.25 - 0.4 * u), 0.6); + } + // Sparks, ash and embers. + for (const p of S.sparks) put(p.x, p.y, lc(0.35 + 0.75 * (p.life / p.L)), min(1, p.life * 2)); + for (const m of S.motes) m.e ? put(m.x, m.y, [230, 110, 30], 0.5 + 0.4 * sin(t * 5 + m.ph)) : put(m.x, m.y, [80, 78, 84], 0.6); + + for (let i = 0, o = 0; i < N; i++, o += 3) display.setPixel(i & 63, i >> 6, min(255, F[o]) | 0, min(255, F[o + 1]) | 0, min(255, F[o + 2]) | 0); +} From efbf3314992ab7697013df5b9b1f566fa255305d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:04:59 +0000 Subject: [PATCH 08/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Implosion?= 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_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/patterns/implosion.js | 426 ++++++++----------- 1 file changed, 173 insertions(+), 253 deletions(-) diff --git a/incoming/pattern-study/patterns/implosion.js b/incoming/pattern-study/patterns/implosion.js index 74b917959..3727db2c4 100644 --- a/incoming/pattern-study/patterns/implosion.js +++ b/incoming/pattern-study/patterns/implosion.js @@ -1,20 +1,20 @@ // ===== Patternflow pattern ===== // Title: Implosion // Family: construction -// Idea: An old concrete tower block among low buildings at dawn is brought down by explosive demolition: a siren light flashes, a ripple of charges runs down the floors, the tower folds and drops into its own footprint under a grey-ochre dust cloud that boils up to the top of the panel and drifts off, and in time-lapse the rubble is cleared and a new tower rises in scaffolding for the next blast. +// Idea: An old tower block comes down at dawn: charges ripple down the floors, it folds into its footprint under a grey-ochre dust cloud that boils up to the top and drifts off, and in time-lapse a new tower rises in scaffolding. // Author: patternflow-lab // Date: 2026-10-24 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: original, pattern round 26, after the explosive demolitions of 1960s concrete tower blocks watched by crowds behind the site fence +// Lineage: original, pattern round 26, after the explosive demolitions of 1960s concrete tower blocks // Round: 26 // =============================== // @matrix 64x128 // @knobs Light=0..1, Speed=0.2..3, Storeys=20..30, Wind=-1..1 // -// Knob 1: Light. Blue dawn at 0, work lamps in a third of the windows and the floodlights faint; then low morning sun from the left, then flat overcast grey, and at 1 night, when two floodlight masts throw beams up the tower and into the dust. -// Knob 2: Speed. At 2 the charges go 2.4 s in, the tower is down within three seconds and a round from blast to blast takes 36 s; at 0.2 it all runs ten times slower. -// Knob 3: Storeys. Twenty to thirty storeys. Twenty stand 80 rows under the roof plant and the mast; past 26 the floors get lower so the roof stays on the panel. -// Knob 4: Wind. Calm at 0, round clouds and the dust going straight up; either way the clouds stretch into slanting streaks, and the dust cloud, the chimney smoke, the water cannon's spray and the scattering pigeons lean with it. +// Knob 1: Light. Blue dawn at 0 with work lamps in a third of the windows; morning sun from the left; flat overcast grey; at 1 night, when two floodlight masts throw beams up the tower and into the dust. +// Knob 2: Speed. At 2 the charges go 2.4 s in, the tower is down three seconds later and a round takes 36 s; at 0.2 it all runs ten times slower. +// Knob 3: Storeys. Twenty to thirty. Twenty stand 80 rows under the roof plant and mast; past 25 the floors get lower so the roof stays on the panel. +// Knob 4: Wind. Calm at 0, dust going straight up. Either way the clouds stretch into slanting streaks and the dust, chimney smoke, water cannon spray and pigeons lean with it. // Buttons: K1 tap detonates now: the siren, then the charges. During the rebuild it finishes the tower first. const W = 64, H = 128, N = W * H; @@ -22,13 +22,14 @@ const { min, max, floor, ceil, round, sin, cos, abs, exp, sqrt, PI } = Math; const cl = (v, a, b) => (v < a ? a : v > b ? b : v); const mix = (a, b, t) => a + (b - a) * t; const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); -const rnd = () => Math.random(); +const rnd = (a = 1) => Math.random() * a; +const side = () => (rnd() < 0.5 ? -1 : 1); 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; } -// Value noise that repeats every cx by cy cells, so the 64x128 tile has no seam. +// Value noise that wraps every cx by cy cells. function vn(x, y, cx = 1e4, cy = 1e4) { const xi = floor(x), yi = floor(y), u = x - xi, v = y - yi, su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v); const h = (i, j) => hash(((i % cx) + cx) % cx, ((j % cy) + cy) % cy); @@ -36,208 +37,169 @@ function vn(x, y, cx = 1e4, cy = 1e4) { return a + (b - a) * su + (c - a) * sv + (a - b - c + h(xi + 1, yi + 1)) * su * sv; } -// The site's ground row; the tower's front face from x 19, 22 wide, and its side face, 6; roof plant and mast rows. -const G = 110, X0 = 19, TW = 28, CX = 33, MAST = 16; -// A round at Speed 2, in seconds: charges, the fall, diggers in, time-lapse, lot clear, frame rising, frame up, next round. +// Ground row; the tower's front face from x 19 (22 wide) and side face (6). +const G = 110, X0 = 19, TW = 28, CX = 33; +// One round at Speed 2, in seconds. const T_CH = 2.4, T_FALL = 3, T_EX = 11, T_TL = 19, T_CLR = 25, T_RISE = 26.5, T_UP = 32.5, CYC = 36; const MP = 240, PF = 12; -// Dawn, morning sun, overcast, night: sky top, middle, horizon; ambient; sun; lit windows; floodlights; cloud lit, dark; cover. +// Dawn, morning, overcast, night: sky top, mid, horizon; ambient; sun; lit windows; floods; cloud lit, dark; cover. const KEYS = [ [16, 30, 86, 52, 84, 156, 214, 146, 150, 0.4, 0.45, 0.66, 0.55, 0.32, 0.3, 0.34, 0.35, 236, 156, 156, 58, 60, 108, 0.5], [46, 108, 206, 110, 164, 228, 236, 206, 160, 0.55, 0.58, 0.68, 1, 0.86, 0.62, 0.06, 0, 255, 244, 226, 150, 160, 190, 0.38], [96, 102, 112, 136, 140, 146, 170, 170, 168, 0.66, 0.66, 0.68, 0.08, 0.08, 0.08, 0.12, 0.12, 190, 192, 196, 100, 104, 112, 0.9], [3, 5, 18, 8, 14, 38, 34, 32, 58, 0.14, 0.15, 0.24, 0, 0, 0, 0.5, 1, 40, 38, 58, 12, 14, 28, 0.45], ]; -const TILE = [80, 110, 170], BRICK = [170, 84, 56], PALE = [190, 176, 150], RUST = [150, 80, 50]; -const COATS = [[30, 34, 56], [56, 30, 36], [34, 46, 34], [50, 44, 34], [250, 120, 20]]; -const MID = [0, 0, 0], LAMP = [200, 190, 150], DARK = [30, 28, 28], HOLE = [14, 12, 12], RED = [255, 40, 30]; -const CON = [160, 154, 144], WARM = [255, 188, 104], YEL = [242, 182, 32], DUST0 = [150, 147, 142], DUST1 = [190, 164, 120], SIL = [12, 12, 16], AMBER = [255, 150, 30]; - -function light(h) { - const u = cl(h, 0, 1) * 3, i = min(2, floor(u)), f = u - i; - return KEYS[i].map((v, j) => mix(v, KEYS[i + 1][j], f)); -} +const TILE = [80, 110, 170], BRICK = [170, 84, 56], PALE = [190, 176, 150], RUST = [150, 80, 50], AMBER = [255, 150, 30]; +const COATS = [[30, 34, 56], [56, 30, 36], [34, 46, 34], [50, 44, 34], [250, 120, 20]], SIL = [12, 12, 16], PHONE = [150, 180, 240]; +const WIN = [204, 150, 83], GLASS = [60, 84, 110], MID = [0, 0, 0], LAMP = [200, 190, 150], DARK = [30, 28, 28], HOLE = [14, 12, 12], RED = [255, 40, 30]; +const CON = [160, 154, 144], WARM = [255, 188, 104], YEL = [242, 182, 32], DUST0 = [150, 147, 142], DUST1 = [190, 164, 120]; +let F, L, P; export function setup(params) { const f = (n) => new Float32Array(n); - const P = (params.s = { F: f(N * 3), NT: f(N), BA: f(N * 3), BU: f(N), BK: new Uint8Array(N), FL: f(N), SKY: f(H * 3), pf: f(MP * PF), np: 0, db: [], sp: [], T: 0, rt: 0, fr: 0, S: 0, cyc: 0, co: 0, w: 0 }); + P = params.s = { F: f(N * 3), NT: f(N), BA: f(N * 3), BU: f(N), BK: new Uint8Array(N), FL: f(N), SKY: f(H * 3), pf: f(MP * PF), np: 0, cr: [], T: 0, rt: 0, S: 0, cyc: 0, co: 0, w: 0 }; for (let i = 0; i < N; i++) { const x = i & 63, y = i >> 6; P.NT[i] = 0.5 * vn(x / 16, y / 16, 4, 8) + 0.3 * vn(x / 8, y / 8, 8, 16) + 0.2 * vn(x / 4, y / 4, 16, 32); - // Two floodlight masts by the fence aim their beams up the tower. + // Two floodlight masts by the fence aim up the tower. for (const lx of [2, 61]) { - const dx = x - lx, dy = y - 82, dd = sqrt(dx * dx + dy * dy) + 0.5, ax = CX - lx, ay = -32, c = (dx * ax + dy * ay) / (dd * sqrt(ax * ax + ay * ay)); + const dx = x - lx, dy = y - 82, dd = sqrt(dx * dx + dy * dy) + 0.5, ax = CX - lx, c = (dx * ax - dy * 32) / (dd * sqrt(ax * ax + 1024)); P.FL[i] += exp(-(1 - c) * 32) / (1 + dd / 40) + 0.9 * exp((-dd * dd) / 5); } } - scenery(P); - P.cr = []; - for (let x = 2; x < 63; x += 3 + floor(rnd() * 3)) if (rnd() < 0.85) P.cr.push([x, 111 + floor(rnd() * 4), 4 + floor(rnd() * 3), rnd(), rnd(), P.cr.length % 7 === 3 ? 4 : floor(rnd() * 4)]); - newRound(P); + scenery(); + for (let x = 2; x < 63; x += 3 + floor(rnd(3))) if (rnd() < 0.85) P.cr.push([x, 111 + floor(rnd(4)), 4 + floor(rnd(3)), rnd(), rnd(), P.cr.length % 7 === 3 ? 4 : floor(rnd(4))]); + newRound(); } -// The street, the site, two low blocks, a far skyline with a spire, a factory chimney and a water cannon on a roof. -function scenery(P) { - const top = new Float32Array(W); - for (let x = 0; x < W; ) for (let j = 0, w = 3 + floor(rnd() * 6), t = 72 + floor(rnd() * 16); j < w && x < W; j++) top[x++] = t; +// Street, site, two low blocks, far skyline and spire, chimney, a water cannon on a roof. +function scenery() { + const top = []; + for (let x = 0; x < W; ) for (let j = 0, w = 3 + rnd(6), t = 72 + rnd(16); j < w && x < W; j++) top[x++] = t; for (let i = 0; i < N; i++) { - const x = i & 63, y = i >> 6, hh = hash(x, y), o = i * 3; - let k = 2, c = null; - if (y > 118) c = [58, 58, 64].map((v) => v * (0.9 + 0.2 * hh)); + const x = i & 63, y = i >> 6, hh = hash(x, y), q = (x - 50) & 3; + let k = 2, c = 0, v = 1; + if (y > 118) (c = [58, 58, 64]), (v = 0.9 + 0.2 * hh); else if (y === 118) c = [120, 118, 112]; - else if (y >= G) c = [118, 104, 86].map((v) => v * (0.8 + 0.35 * hh)); - else if (x >= 51 && x <= 55 && y >= 85 && y <= 89) c = abs(x - 53) + abs(y - 86.5) < 1.6 ? [40, 44, 50] : [116, 120, 128]; - else if (x <= 14 && y >= 82) { - c = y < 84 ? [170, 160, 150] : [148, 92, 70].map((v) => v * (0.88 + 0.2 * hash(x >> 1, y))); - if (y >= 86 && (y < 103 ? (x & 3) && (x & 3) < 3 && (y - 86) % 4 < 2 : y > 104 && x > 1 && x < 13)) (k = 3), (P.BU[i] = y > 104 ? hh * 0.4 : hh); - } else if (x >= 49 && y >= 90) { - c = y < 92 ? [190, 182, 170] : [176, 162, 138].map((v) => v * (0.9 + 0.15 * hash(x, y >> 2))); - if (y >= 94 && y < 108 && (x - 50) % 4 > 0 && (x - 50) % 4 < 3 && (y - 94) % 4 < 2) (k = 3), (P.BU[i] = hh); - } else if (x >= 56 && x <= 59 && y >= 44) c = y === 44 ? [180, 172, 160] : (y >= 47 && y <= 48) || (y >= 52 && y <= 53) ? [176, 70, 56] : [150, 138, 124]; - else if (y >= top[x] || (y >= 58 && abs(x - 9) <= (y - 58) / 6)) { - k = hash(x, y >> 1) < 0.07 && y & 1 ? 4 : 1; - c = [60, 66, 86].map((v) => v * (0.85 + 0.25 * hash(x >> 2, 3))); - } else k = 0; + else if (y >= G) (c = [118, 104, 86]), (v = 0.8 + 0.35 * hh); + else if (x > 50 && x < 56 && y > 84 && y < 90) c = abs(x - 53) + abs(y - 86.5) < 1.6 ? [40, 44, 50] : [116, 120, 128]; + else if (x < 15 && y > 81) { + (c = y < 84 ? [170, 160, 150] : [148, 92, 70]), (v = y < 84 ? 1 : 0.88 + 0.2 * hash(x >> 1, y)); + if (y > 85 && y < 106 && x & 3 && (x & 3) < 3 && (y - 86) % 4 < 2) (k = 3), (P.BU[i] = hh); + } else if (x > 48 && y > 89) { + (c = y < 92 ? [190, 182, 170] : [176, 162, 138]), (v = 0.9 + 0.15 * hash(x, y >> 2)); + if (y > 93 && y < 108 && q && q < 3 && (y - 94) % 4 < 2) (k = 3), (P.BU[i] = hh); + } else if (x > 55 && x < 60 && y > 43) c = y === 44 ? [180, 172, 160] : y > 46 && y < 54 && y % 5 > 1 && y % 5 < 4 ? [176, 70, 56] : [150, 138, 124]; + else if (y >= top[x] || (y > 57 && abs(x - 9) <= (y - 58) / 6)) (k = 1), (c = [60, 66, 86]), (v = 0.85 + 0.25 * hash(x >> 2, 3)); + else k = 0; P.BK[i] = k; - if (c) for (let j = 0; j < 3; j++) P.BA[o + j] = c[j]; + if (c) for (let j = 0; j < 3; j++) P.BA[i * 3 + j] = c[j] * v; } } -// The tower's facade for S storeys: one kind per cell, each window's chance to be lit, the crack field. -function facade(P, S) { - const p = min(4, 100 / S), BH = round(S * p), BT = BH + MAST, n = TW * BT, st = []; +// The facade for S storeys: a kind per cell, each window's chance to be lit. +function facade(S) { + const p = min(4, 100 / S), BH = round(S * p), BT = BH + 16, n = TW * BT, st = [], chg = []; + const A = new Float32Array(n), K = new Uint8Array(n), U = new Float32Array(n), RS = new Int16Array(BT).fill(-1); for (let s = 0; s <= S; s++) st.push(round(s * p)); - const A = new Float32Array(n), K = new Uint8Array(n), U = new Float32Array(n), C = new Float32Array(n), RS = new Int16Array(BT).fill(-1); for (let b = 0, s = 0; b < BT; b++) { while (s < S - 1 && st[s + 1] <= b) s++; - const r = b - st[s]; + const r = b - st[s], wr = r === 1 || r === 2; if (b < BH) RS[b] = s; for (let c = 0; c < TW; c++) { - const j = b * TW + c, q = (c - 1) & 3, bay = (c - 1) >> 2; + const j = b * TW + c, bay = (c - 1) >> 2; let k = 0, a = 0.92 + 0.14 * hash(s * 7 + bay, 11); if (b < BH) { - if (c < 22) { - k = r === 0 ? 3 : c > 0 && c < 21 && q < 3 && (r === 1 || r === 2 || (s === 0 && r === 3)) ? 2 : 1; - if (k === 1 && r === 3 && hash(c, 5) < 0.4) a *= 0.86; - U[j] = s === 0 ? (bay === 2 ? 0 : 0.95) : hash(s * 13 + bay, 3); - } else (k = c === 22 ? 4 : (c === 24 || c === 25) && (r === 1 || r === 2) ? 5 : 4), (a = c === 22 ? 1.15 : a), (U[j] = hash(s * 17 + c, 9)); - if (r === 0) a *= 0.8; + if (c < 22) (k = !r ? 3 : c && c < 21 && ((c - 1) & 3) < 3 && wr ? 2 : 1), (U[j] = hash(s * 13 + bay, 3)); + else (k = c > 23 && c < 26 && wr ? 5 : 4), (a = c === 22 ? 1.15 : a), (U[j] = hash(s * 17 + c, 9)); + if (!r) a *= 0.8; } else if (b === BH) k = c < 22 ? 6 : 0; - else if (b <= BH + 4 && ((c >= 5 && c <= 14) || (c >= 16 && c <= 19 && b <= BH + 3))) (k = 6), (a = 0.82); - else if ((c === 10 && b < BT - 1) || (b === BH + 9 && c >= 8 && c <= 12) || (b === BH + 7 && c === 11)) k = 7; - else if (c === 10 && b === BT - 1) k = 8; - K[j] = k; - A[j] = a; - C[j] = abs(vn(c * 0.3 + 5, b * 0.12) - 0.5) * 2 + 0.6 * abs(vn(c * 0.9, b * 0.7) - 0.5); + else if (b <= BH + 4 && ((c > 4 && c < 15) || (c > 15 && c < 20 && b <= BH + 3))) (k = 6), (a = 0.82); + else if (c === 10) k = b < BT - 1 ? 7 : 8; + else if ((b === BH + 9 && c > 7 && c < 13) || (b === BH + 7 && c === 11)) k = 7; + (K[j] = k), (A[j] = a); } } - const chg = []; for (let s = round(S * 0.78); s >= 2; s -= 3) chg.push(s); - Object.assign(P, { S, BH, BT, st, fa: A, fk: K, fu: U, fc: C, rs: RS, chg: chg.concat([1, 0]), fold: st[round(S * 0.42)], fls: new Float32Array(S), bl: new Uint8Array(S) }); + Object.assign(P, { S, BH, BT, st, fa: A, fk: K, fu: U, rs: RS, chg: chg.concat([1, 0]), fold: st[round(S * 0.42)], fls: new Float32Array(S), bl: new Uint8Array(S) }); } -function newRound(P) { - Object.assign(P, { cyc: P.cyc + 1, dir: rnd() < 0.5 ? -1 : 1, fi: 0, b0: 0, b1: 0, pd1: 0, pc2: 0, a1: 0, a2: 0, ad: 0, bd: [] }); - if (P.bl) P.bl.fill(0); - for (let j = 0; j < 8; j++) P.bd.push([0, 0, 0, 0, rnd() * 6.28, 7 + rnd() * 10, 0, rnd() * 6]); - P.frag = [[-9 + rnd() * 3, 9, 0.6, -0.3], [3 + rnd() * 3, 8, -0.7, 0.25]]; +function newRound() { + Object.assign(P, { cyc: P.cyc + 1, dir: side(), fi: 0, b0: 0, b1: 0, pd1: 0, pc2: 0, a1: 0, a2: 0, bd: [] }); + P.bl?.fill(0); + for (let j = 0; j < 8; j++) P.bd.push([0, 0, 0, 0, rnd(6.28), 7 + rnd(10), 0, rnd(6)]); } // The lower floors give first; the upper block then folds into them. +const yS = (s) => G - 1 - (P.st[s] + P.st[s + 1]) / 2; const fall = (tc) => (tc <= 0 ? [0, 0] : [11 * tc * tc, tc > 0.5 ? 6 * (tc - 0.5) ** 2 : 0]); - -function addPuff(P, x, y, vx, vy, r, g, a, life, rise, tint) { - if (P.np < MP) P.pf.set([x, y, vx, vy, r, r, g, a, 0, life, rise, tint], P.np++ * PF); -} -function basePuff(P, kind) { - const x = X0 + rnd() * TW, out = x - CX, roll = kind < 2 && rnd() < 0.4; - addPuff(P, x, G - 1 - rnd() * 4, out * 0.8 + (rnd() - 0.5) * 12 + (roll ? Math.sign(out || 1) * (10 + rnd() * 10) : 0), roll ? -1 - rnd() * 3 : -(5 + rnd() * 12) - (kind === 2 ? 14 : 0), 3 + rnd() * 3, 2 + rnd() * 2.5, 1 + rnd() * 0.5, roll ? 12 + rnd() * 5 : 8.5 + rnd() * 5, roll ? 1 + rnd() * 3 : 8 + rnd() * (kind === 2 ? 30 : 24), rnd()); -} -function debris(P, x, y, s) { - P.db.push([x, y, s * (4 + rnd() * 14), -4 - rnd() * 14, 1.5]); +const puff = (x, y, vx, vy, r, g, a, life, rise) => P.np < MP && P.pf.set([x, y, vx, vy, r, r, g, a, 0, life, rise, rnd()], P.np++ * PF); +// Dust from crushed floors: most climbs, some rolls out along the ground. +function basePuff(kind) { + const x = X0 + rnd(TW), out = x - CX, roll = kind < 2 && rnd() < 0.4; + puff(x, G - 1 - rnd(4), out * 0.8 + rnd(12) - 6 + (roll ? Math.sign(out || 1) * (10 + rnd(10)) : 0), roll ? -1 - rnd(3) : -5 - rnd(12) - (kind > 1 ? 14 : 0), 3 + rnd(3), 2 + rnd(2.5), 1 + rnd(0.5), roll ? 12 + rnd(5) : 8.5 + rnd(5), roll ? 1 + rnd(3) : 8 + rnd(kind > 1 ? 30 : 24)); } - -// A charge goes off on storey s: its windows blow out and spit dust both ways. -function fire(P, s) { - const y = G - 1 - (P.st[s] + P.st[s + 1]) / 2; +// A charge on storey s: its windows blow out and spit dust. +function fire(s) { + const y = yS(s); P.bl[s] = 1; for (let n = 0; n < 10; n++) { - const sd = n & 1 ? 1 : -1, edge = n < 4; - addPuff(P, edge ? (sd < 0 ? X0 - 1 : X0 + TW) : X0 + 2 + rnd() * (TW - 4), y + (rnd() - 0.5) * 2, edge ? sd * (10 + rnd() * 12) : sd * rnd() * 3, -rnd(), 1 + rnd() * 0.8, 1.2 + rnd(), 0.7, 1.6 + rnd(), 0.5, rnd() * 0.5); - if (edge) debris(P, sd < 0 ? X0 - 1 : X0 + TW, y, sd); + const sd = n & 1 ? 1 : -1, ex = sd < 0 ? X0 - 1 : X0 + TW; + puff(n < 4 ? ex : X0 + 2 + rnd(TW - 4), y + rnd(2) - 1, sd * (n < 4 ? 10 + rnd(12) : rnd(3)), -rnd(), 1 + rnd(0.8), 1.2 + rnd(), 0.7, 1.6 + rnd(), 0.5); } - if (P.fi === 1) for (const b of P.bd) (b[6] = 1), (b[2] = (b[0] - CX) * 2 + (rnd() - 0.5) * 14 + P.w * 14), (b[3] = -8 - rnd() * 14); + if (P.fi === 1) for (const b of P.bd) (b[6] = 1), (b[2] = (b[0] - CX) * 2 + rnd(14) - 7 + P.w * 14), (b[3] = -8 - rnd(14)); } export function update(dt, input, params) { if (!params.s) setup(params); - const P = params.s, k = input.knobValues || [0, 2, 20, 0.06]; - const rd = cl(dt || 0, 0, 0.1), d = rd * (cl(k[1], 0.2, 3) / 2), S = round(cl(k[2], 20, 30)), w = (P.w = cl(k[3], -1, 1)); - if (S !== P.S) facade(P, S); - (P.rt += rd), P.fr++; - if (input.btnPressed && input.btnPressed[0]) { - if (P.T < T_CH - 0.7) P.T = T_CH - 0.7; - else if (P.T >= T_TL) newRound(P), (P.T = T_CH - 0.7); - } - if ((P.T += d) >= CYC) newRound(P), (P.T -= CYC); - const T = P.T, tl = 0.85 * sm(T_TL, T_TL + 1.2, T) * (1 - sm(CYC - 2.5, CYC - 0.4, T)); - // Time-lapse: days go by, dawn to night and back every 2 s. - P.he = mix(cl(k[0], 0, 1), 0.5 - 0.5 * cos((2 * PI * (T - T_TL)) / 2), tl); - P.L = light(P.he); + P = params.s; + const k = input.knobValues || [0, 2, 20, 0.06], rd = cl(dt || 0, 0, 0.1), d = rd * (cl(k[1], 0.2, 3) / 2), S = round(cl(k[2], 20, 30)), w = (P.w = cl(k[3], -1, 1)); + if (S !== P.S) facade(S); + P.rt += rd; + if (input.btnPressed?.[0]) P.T < T_CH - 0.7 ? (P.T = T_CH - 0.7) : P.T >= T_TL && (newRound(), (P.T = T_CH - 0.7)); + if ((P.T += d) >= CYC) newRound(), (P.T -= CYC); + const T = P.T, tl = 0.85 * sm(T_TL, T_TL + 1.2, T) * (1 - sm(CYC - 2.5, CYC - 0.4, T)), tc = T - T_FALL, [d1, c2] = T < T_TL ? fall(tc) : [0, 0], cons = min(P.BT, d1 + c2), phM = 10 + S * 0.6; + // Time-lapse: a day every 2 s, and the clouds race. + const u = (P.he = mix(cl(k[0], 0, 1), 0.5 - 0.5 * cos(PI * (T - T_TL)), tl)) * 3, i = min(2, floor(u)); + P.L = KEYS[i].map((v, j) => mix(v, KEYS[i + 1][j], u - i)); P.co += (w * 2 + tl * (w < 0 ? -24 : 24)) * d; // Charges, top to bottom, then the shudder. - while (P.fi < P.chg.length && T >= T_CH + P.fi * 0.09 && T < T_TL) fire(P, P.chg[P.fi++]); + while (P.fi < P.chg.length && T >= T_CH + P.fi * 0.09 && T < T_TL) fire(P.chg[P.fi++]); P.gfl = 0; - P.chg.forEach((s, n) => { - const t0 = T_CH + n * 0.09; - P.gfl = max(P.gfl, (P.fls[s] = T >= t0 ? exp(-(T - t0) * 9) : 0)); - }); + P.chg.forEach((s, n) => (P.gfl = max(P.gfl, (P.fls[s] = T >= T_CH + n * 0.09 ? exp(-(T - T_CH - n * 0.09) * 9) : 0)))); P.shk = T > T_CH && T < T_FALL + 0.6 ? round((rnd() - 0.5) * 2.6 * (1 - sm(T_FALL, T_FALL + 0.6, T))) : 0; - // The fall: dust where floors are crushed, at the foot and at the fold. - const tc = T - T_FALL, [d1, c2] = T < T_TL ? fall(tc) : [0, 0], cons = min(P.BT, d1 + c2), phM = 22 + (P.S - 20) * 0.6; + // The fall: dust where floors are crushed, at the foot and the fold. if (tc > 0 && T < T_TL) { - if (!P.b0) for (let n = (P.b0 = 1) && 24; n--; ) basePuff(P, 1); + if (!P.b0) for (let n = (P.b0 = 24); n--; ) basePuff(1); const e1 = max(0, d1 - P.pd1), e2 = max(0, c2 - P.pc2); - if (d1 + c2 < P.BT) (P.a1 += e1 * 1.1), (P.a2 += e2 * 0.35), (P.ad += e1 * 0.5); - for (; P.a1 >= 1; P.a1--) basePuff(P, 0); - for (const yf = G - 1 - P.fold + d1; P.a2 >= 1; P.a2--) { - const sd = rnd() < 0.5 ? -1 : 1; - addPuff(P, CX + sd * (8 + rnd() * 8), min(G - 2, yf), sd * (8 + rnd() * 14), -2 - rnd() * 6, 2 + rnd() * 1.5, 3 + rnd() * 3, 0.7, 8 + rnd() * 4, 4 + rnd() * 6, rnd()); - } - for (; P.ad >= 1; P.ad--) debris(P, rnd() < 0.5 ? X0 - 1 : X0 + TW, G - 2 - rnd() * 4, rnd() < 0.5 ? -1 : 1); - if (!P.b1 && cons >= P.BT) for (let n = (P.b1 = 1) && 20; n--; ) basePuff(P, 2); + if (d1 + c2 < P.BT) (P.a1 += e1 * 1.1), (P.a2 += e2 * 0.35); + for (; P.a1 >= 1; P.a1--) basePuff(0); + for (let sd; P.a2 >= 1; P.a2--) (sd = side()), puff(CX + sd * (8 + rnd(8)), min(G - 2, G - 1 - P.fold + d1), sd * (8 + rnd(14)), -2 - rnd(6), 2 + rnd(1.5), 3 + rnd(3), 0.7, 8 + rnd(4), 4 + rnd(6)); + if (!P.b1 && cons >= P.BT) for (let n = (P.b1 = 20); n--; ) basePuff(2); (P.pd1 = d1), (P.pc2 = c2); } P.ph = T < T_FALL ? 0 : T < T_TL ? phM * sm(0, 1, cons / P.BT) : phM * (1 - sm(T_TL + 0.5, T_CLR - 0.5, T)); - // Dust puffs rise, slow, swell and drift with the wind, stronger aloft. - const pf = P.pf, e1 = 1 - exp(-0.7 * d), e2 = 1 - exp(-0.9 * d); - for (let p = 0; p < P.np; p++) { - const q = p * PF, age = (pf[q + 8] += d); - if (age > pf[q + 9]) { - if (p < --P.np) pf.copyWithin(q, P.np * PF, P.np * PF + PF); - p--; + // Puffs rise, slow, swell and drift, faster aloft. + const pf = P.pf, ex = 1 - exp(-0.7 * d), ey = 1 - exp(-0.9 * d); + for (let p = 0, q, age; p < P.np; p++) { + if ((age = pf[(q = p * PF) + 8] += d) > pf[q + 9]) { + if (p-- < --P.np) pf.copyWithin(q, P.np * PF, P.np * PF + PF); continue; } - pf[q + 2] += (w * (3 + 9 * (1 - pf[q + 1] / H)) - pf[q + 2]) * e1; - pf[q + 3] += (-pf[q + 10] * exp(-age / 4) - pf[q + 3]) * e2; - (pf[q] += pf[q + 2] * d), (pf[q + 1] += pf[q + 3] * d), (pf[q + 4] += (pf[q + 6] * d) / (1 + 0.4 * age)); - const fl = G - 0.35 * pf[q + 4]; - if (pf[q + 1] > fl) (pf[q + 1] = fl), (pf[q + 3] = min(0, pf[q + 3])); + pf[q + 2] += (w * (3 + 9 * (1 - pf[q + 1] / H)) - pf[q + 2]) * ex; + pf[q + 3] += (-pf[q + 10] * exp(-age / 4) - pf[q + 3]) * ey; + (pf[q] += pf[q + 2] * d), (pf[q + 1] = min(G - 0.35 * pf[q + 4], pf[q + 1] + pf[q + 3] * d)), (pf[q + 4] += (pf[q + 6] * d) / (1 + 0.4 * age)); } - P.db = P.db.filter((b) => ((b[3] += 70 * d), (b[0] += b[2] * d), (b[1] += b[3] * d), (b[4] -= d) > 0 && b[1] < G + 1)); - // The water cannon on the right-hand roof wets the site until the time-lapse. - if (T > 1 && T < T_TL) for (let n = 0; n < 3 && rnd() < 45 * d; n++) P.sp.push([52, 86, -15 - rnd() * 6 + w * 6, -10 - rnd() * 5, 0]); - P.sp = P.sp.filter((s) => ((s[3] += 26 * d), (s[0] += s[2] * d), (s[1] += s[3] * d), (s[4] += d) < 2 && s[1] < 104)); + const fly = (a, g, keep) => a.filter((b) => ((b[3] += g * d), (b[0] += b[2] * d), (b[1] += b[3] * d), keep(b))); // Pigeons circle the roof until the first charge, then scatter. for (const b of P.bd) - if (b[6]) (b[0] += b[2] * d), (b[1] += b[3] * d), (b[3] -= 4 * d), (b[7] += 14 * d); + if (b[6]) fly([b], -4, () => 1), (b[7] += 14 * d); else (b[4] += 0.9 * d), (b[0] = CX + cos(b[4]) * b[5] * 1.3), (b[1] = G - P.BH - 10 + sin(b[4]) * b[5] * 0.35), (b[7] += 9 * d); } -let F, L, P; const idx = (x, y) => ((x = round(x)), (y = round(y)), x < 0 || x >= W || y < 0 || y >= H ? -1 : (y * W + x) * 3); function put(x, y, c, a = 1, o = idx(x, y)) { if (o >= 0) for (let j = 0; j < 3; j++) F[o + j] += (c[j] - F[o + j]) * a; @@ -248,172 +210,133 @@ function add(x, y, c, k, o = idx(x, y)) { function glow(x, y, c, k, r) { for (let j = -r; j <= r; j++) for (let i = -r; i <= r; i++) add(x + i, y + j, c, k * exp(-(i * i + j * j) / (0.3 * r * r))); } -const lit = (c, m = 1, s = 0.5, fq = 0) => [0, 1, 2].map((j) => c[j] * (L[9 + j] * m + L[12 + j] * s + fq)); +const lit = (c, m = 1, s = 0.5, f = 0) => [0, 1, 2].map((j) => c[j] * (L[9 + j] * m + L[12 + j] * s + f)); function line(x0, y0, x1, y1, c) { for (let i = 0, n = max(abs(x1 - x0), abs(y1 - y0)) * 1.4 + 1; i <= n; i++) put(mix(x0, x1, i / n), mix(y0, y1, i / n), c); } -const fq = (x, y) => (P.FL[cl(round(y), 0, H - 1) * W + cl(round(x), 0, W - 1)] || 0) * L[16]; +const fq = (x, y) => P.FL[cl(round(y), 0, H - 1) * W + cl(round(x), 0, W - 1)] * L[16]; function back() { - const { BA, BU, BK, FL, NT, SKY } = P, aw = abs(P.w), sx = 0.6 - 0.3 * aw, sh = 0.85 * P.w, sy = 1 + 0.8 * aw, th = 0.5 + (0.5 - L[23] - 0.15 * aw) * 0.32, co = P.co; - for (let y = 0; y < H; y++) { - const t = min(1, y / 96), s1 = sm(0, 0.6, t), s2 = sm(0.4, 1, t) ** 2; - for (let c = 0; c < 3; c++) SKY[y * 3 + c] = mix(mix(L[c], L[3 + c], s1), L[6 + c], s2); - } - const sunV = sm(0.12, 0.3, P.he) * (1 - sm(0.42, 0.58, P.he)), night = sm(0.78, 1, P.he), fld = L[16], lw = L[15]; + const { BA, BU, BK, FL, NT, SKY } = P, aw = abs(P.w), sx = 0.6 - 0.3 * aw, sh = 0.85 * P.w, sy = 1 + 0.8 * aw, th = 0.5 + (0.5 - L[23] - 0.15 * aw) * 0.32; + const sunV = 280 * sm(0.12, 0.3, P.he) * (1 - sm(0.42, 0.58, P.he)), night = sm(0.78, 1, P.he), fld = L[16], lw = L[15]; + for (let y = 0; y < H; y++) for (let c = 0, t = min(1, y / 96); c < 3; c++) SKY[y * 3 + c] = mix(mix(L[c], L[3 + c], sm(0, 0.6, t)), L[6 + c], sm(0.4, 1, t) ** 2); for (let i = 0; i < N; i++) { - const x = i & 63, y = i >> 6, o = i * 3, k = BK[i], fb = fld * FL[i]; - let r, g, b; - if (k === 0) { - (r = SKY[y * 3]), (g = SKY[y * 3 + 1]), (b = SKY[y * 3 + 2]); - // Round clouds when calm, slanting streaks in wind. - if (y < 96) { - const n = NT[((floor(y * sy + 40) & 127) << 6) | (floor(x * sx + y * sh + co) & 63)], cd = sm(th - 0.06, th + 0.08, n) * (1 - sm(66, 96, y)) * 0.9; - if (cd > 0) { - const lb = cl(0.15 + y / 110 + (n - th) * 2, 0, 1); - (r = mix(r, mix(L[20], L[17], lb), cd)), (g = mix(g, mix(L[21], L[18], lb), cd)), (b = mix(b, mix(L[22], L[19], lb), cd)); - } - } - let sg = 0; - if (sunV > 0) { - const ds = (x - 9) ** 2 + (y - 60) ** 2; - sg = sunV * (255 * exp(-ds / 7) + 70 * exp(-sqrt(ds) / 9)); - } + const x = i & 63, y = i >> 6, o = i * 3, k = BK[i], fb = fld * FL[i], so = y * 3; + if (!k) { + // Round clouds when calm, slanting streaks in wind; sun, moon, stars. + const n = NT[((floor(y * sy + 40) & 127) << 6) | (floor(x * sx + y * sh + P.co) & 63)], cd = max(0, sm(th - 0.06, th + 0.08, n) * (1 - sm(66, 96, y)) * 0.9), lb = cl(0.15 + y / 110 + (n - th) * 2, 0, 1); + let sg = sunV > 0 ? sunV * exp(-((x - 9) ** 2 + (y - 60) ** 2) / 12) : 0; if (night > 0) sg += night * (((x - 52) ** 2 + (y - 20) ** 2 < 6) * 220 + (y < 70 && hash(x, y) > 0.985 ? 90 + 60 * sin(P.rt * 3 + x) : 0)); - (r += sg + 56 * fb), (g += sg * 0.9 + 60 * fb), (b += sg * 0.7 + 66 * fb); - } else if (k === 1 || k === 4) { - const hz = 0.55 - y / 400; - (r = mix(BA[o] * L[9], SKY[y * 3], hz)), (g = mix(BA[o + 1] * L[10], SKY[y * 3 + 1], hz)), (b = mix(BA[o + 2] * L[11], SKY[y * 3 + 2], hz)); - if (k === 4 && hash(x, 77) < lw * 1.4) (r += 120), (g += 86), (b += 40); - } else if (k === 3 && BU[i] < lw * 1.1) (r = WARM[0] * 0.8), (g = WARM[1] * 0.8), (b = WARM[2] * 0.8); + for (let c = 0; c < 3; c++) F[o + c] = mix(SKY[so + c], mix(L[20 + c], L[17 + c], lb), cd) + sg * (1 - 0.15 * c) + (56 + 5 * c) * fb; + } else if (k === 3 && BU[i] < lw * 1.1) F.set(WIN, o); else { - const m = k === 3 ? 0.25 : 1, f = (y < 84 ? 0.35 : 0.5) * (x < 15 || x > 55 ? 1 : 0.6); - (r = BA[o] * m * (L[9] + L[12] * f + fb * 0.6)), (g = BA[o + 1] * m * (L[10] + L[13] * f + fb * 0.6)), (b = BA[o + 2] * m * (L[11] + L[14] * f + fb * 0.6)); - if (k === 3) (r += SKY[y * 3] * 0.2), (g += SKY[y * 3 + 1] * 0.2), (b += SKY[y * 3 + 2] * 0.25); + const m = k === 3 ? 0.25 : 1, f = (y < 84 ? 0.35 : 0.5) * (x < 15 || x > 55 ? 1 : 0.6), gl = k === 3 ? 0.2 : 0; + for (let c = 0; c < 3; c++) F[o + c] = k === 1 ? mix(BA[o + c] * L[9 + c], SKY[so + c], 0.55 - y / 400) : SKY[so + c] * gl + BA[o + c] * m * (L[9 + c] + L[12 + c] * f + fb * 0.6); } - (F[o] = r), (F[o + 1] = g), (F[o + 2] = b); } - // Light masts at night, and the chimney's smoke bending with the wind. - for (const lx of [2, 61]) for (let y = 82; y < G; y++) put(lx, y, [30, 30, 34]); - if (fld > 0.05) for (const lx of [2, 61]) put(lx, 82, [255, 250, 230], min(1, fld * 1.5)); - const sc = lit([176, 172, 168], 1, 0.6), ph = (P.rt * 0.5) % 1; + // Light masts; chimney smoke bending with the wind. + for (const lx of [2, 61]) { + for (let y = 82; y < G; y++) put(lx, y, [30, 30, 34]); + if (fld > 0.05) put(lx, 82, [255, 250, 230], min(1, fld * 1.5)); + } + const sc = lit([176, 172, 168]), ph = (P.rt * 0.5) % 1; for (let j = 0; j < 12; j++) { const s = j + ph, x = 57.5 + P.w * (s * 2.2 + s * s * 0.14) + sin(P.rt * 0.7 + j) * 0.6, y = 42 - s * 2.4 * (1 - 0.55 * aw), r = 1.4 + s * 0.42, a = 0.45 * (1 - s / 12.5) * min(1, s); - for (let v = -ceil(r); v <= r; v++) for (let u = -ceil(r); u <= r; u++) { const q = 1 - (u * u + v * v) / (r * r); if (q > 0) put(x + u, y + v, sc, a * q); } + for (let v = -ceil(r); v <= r; v++) for (let u = -ceil(r), q; u <= r; u++) if ((q = 1 - (u * u + v * v) / (r * r)) > 0) put(x + u, y + v, sc, a * q); } } -// The tower, standing, falling or rising again in scaffolding. +// The tower standing, falling, or rising in scaffolding. function tower() { - const { fa: A, fk: K, fu: U, fc: C, rs: RS, FL, SKY, T, BT, BH, fold: F0 } = P, tc = T - T_FALL; + const { fa: A, fk: K, fu: U, rs: RS, FL, SKY, T, BT, BH, fold: F0 } = P, tc = T - T_FALL; if (T >= T_TL && T < T_RISE) return; - const reb = T >= T_RISE, built = reb ? BH * sm(T_RISE, T_UP, T) : BT, strip = reb ? BT * (1 - sm(T_UP, CYC - 0.6, T)) : 0; - const [d1, c2] = reb ? [0, 0] : fall(tc), l1 = reb ? 0 : P.dir * 3 * sm(0.1, 1.2, tc), l2 = reb ? 0 : -P.dir * 10 * sm(0.3, 1.6, tc), ck = reb ? 0 : 0.09 * sm(0, 1.2, tc), lw = L[15], fld = L[16], tick = floor(T * 20); - const scaf = lit([184, 186, 178], 1, 0.6), net = lit([64, 124, 104], 1, 0.4), slab = lit([130, 126, 118]), mast = lit([120, 124, 132], 1, 0.6); + const reb = T >= T_RISE, built = reb ? BH * sm(T_RISE, T_UP, T) : BT, strip = reb ? BT * (1 - sm(T_UP, CYC - 0.6, T)) : 0, lw = L[15], fld = L[16], tick = floor(T * 20); + const [d1, c2] = reb ? [0, 0] : fall(tc), l1 = reb ? 0 : P.dir * 3 * sm(0.1, 1.2, tc), l2 = reb ? 0 : -P.dir * 10 * sm(0.3, 1.6, tc); + const scaf = lit([184, 186, 178]), net = lit([64, 124, 104]), slab = lit([130, 126, 118]), mast = lit([120, 124, 132]); for (let y = max(0, G - BT - 1); y < G; y++) { + // The block above the fold or the one under it; both shear. let b = G - 1 + d1 + c2 - y, xo, up = 1; if (b >= F0 + c2 && b < BT) xo = l1 + (l2 * (b - F0 - c2)) / (BT - F0); else if ((b = G - 1 + d1 - y) >= 0 && b < F0) (xo = (l1 * b) / F0), (up = 0); else continue; const bi = floor(b), xs = round(xo + P.shk), s = RS[bi], fl = s >= 0 ? P.fls[s] : 0, rag = tc > 0 && !reb && (G - y < 3 || (up && c2 > 0 && b - F0 - c2 < 2)); - const top = sm(70, 25, y), hy = (y >> 1) * 3; + const top = sm(70, 25, y), hy = (y >> 1) * 3, f2 = rag ? 0.55 : 1; for (let c = 0; c < TW; c++) { const j = bi * TW + c, k = K[j], x = X0 + c + xs; if (!k || x < 0 || x >= W || (rag && hash(x * 13 + y, tick) < 0.4)) continue; - const i = y * W + x, o = i * 3, side = k === 4 || k === 5, fq = FL[i] * fld; + const o = (y * W + x) * 3, sd = k === 4 || k === 5, fq = FL[y * W + x] * fld; let col = MID; if (reb && (bi < strip || (k > 5 && strip > BH))) { if (bi >= built || k > 5) continue; col = c % 3 === 0 || bi % 3 === 0 ? scaf : hash(c >> 2, bi >> 3) < 0.4 ? net : k === 3 ? slab : hash(c >> 1, s) < 0.12 ? LAMP : DARK; } else if (k === 2 || k === 5) { - const v = side ? 0.65 : 1; + const v = sd ? 0.65 : 1; if (s >= 0 && P.bl[s]) col = HOLE; else if (U[j] < lw) for (let q = 0, u = (0.7 + 0.3 * sin(U[j] * 60)) * v; q < 3; q++) MID[q] = WARM[q] * u; else for (let q = 0; q < 3; q++) MID[q] = (SKY[hy + q] * 0.45 + L[6 + q] * 0.12 + 76 * fq) * v; } else if (k === 7) col = mast; else if (k === 8) col = (P.rt * 1.5) % 1 < 0.5 ? RED : HOLE; - else for (let q = 0, a = A[j] * (C[j] < ck ? 0.3 : 1), m = side ? 0.6 : 1, sn = (side ? 0.06 : 0.55) + top * 0.4; q < 3; q++) MID[q] = CON[q] * a * (L[9 + q] * m + L[12 + q] * sn + fq * 1.6); - const f2 = rag ? 0.55 : 1; + else for (let q = 0, m = sd ? 0.6 : 1, sn = (sd ? 0.06 : 0.55) + top * 0.4; q < 3; q++) MID[q] = CON[q] * A[j] * (L[9 + q] * m + L[12 + q] * sn + fq * 1.6); (F[o] = col[0] * f2 + fl * 300), (F[o + 1] = col[1] * f2 + fl * 250), (F[o + 2] = col[2] * f2 + fl * 170); } - if (reb && bi < min(built, strip)) for (const x of [X0 - 1 + xs, X0 + TW + xs]) put(x, y, scaf); } - // Each charge lights the air round its floor. - P.chg.forEach((s) => { - const fl = P.fls[s]; - if (fl > 0.02) for (let y = -6, yc = G - 1 - (P.st[s] + P.st[s + 1]) / 2; y <= 6; y++) for (let x = X0 - 9; x < X0 + TW + 9; x++) { - const dx = x < X0 ? X0 - x : x >= X0 + TW ? x - X0 - TW + 1 : 0; - if (dx) add(x, yc + y, [255, 196, 120], fl * exp(-(dx * dx) / 12 - (y * y) / 8)); - } - }); + // Each charge lights the air by its floor. + for (const s of P.chg) { + const fl = P.fls[s], yc = yS(s); + if (fl > 0.02) for (let y = -6; y <= 6; y++) for (let x = X0 - 9, dx; x < X0 + TW + 9; x++) if ((dx = x < X0 ? X0 - x : max(0, x - X0 - TW + 1))) add(x, yc + y, [255, 196, 120], fl * exp(-(dx * dx) / 12 - (y * y) / 8)); + } } function pileH(x) { const u = (x - CX) / 22; return abs(u) >= 1 || P.ph < 0.3 ? 0 : P.ph * (1 - u * u) ** 0.8 * (0.8 + 0.4 * vn(x * 0.25, P.cyc * 7)) + (hash(x, P.cyc) - 0.5) * 1.6 * min(1, P.ph / 4); } -// Rubble: broken slabs, rebar and bits of painted rooms; two pieces of facade still with their windows. +// Rubble: broken slabs, gaps, rebar and bits of painted rooms. function pile() { if (P.ph < 0.3) return; for (let x = CX - 22; x <= CX + 22; x++) { - const hp = pileH(x), sl = pileH(x + 1) - pileH(x - 1), sn = cl(0.45 - 0.3 * sl, 0, 1); + const hp = pileH(x), sn = cl(0.45 - 0.3 * (pileH(x + 1) - pileH(x - 1)), 0, 1); for (let y = round(G - hp), y0 = y; y < G + 3; y++) { - const cx = floor((x + (y >> 1) * 1.7) / 3), cy = y >> 1, v = hash(cx, cy + P.cyc * 97), e = (y & 1) === 0 ? 1.18 : 0.86, dp = 1 - 0.35 * sm(0, 12, y - y0); - const c = v > 0.96 ? TILE : v < 0.05 ? BRICK : v > 0.9 ? PALE : CON, o = (y * W + x) * 3, f = fq(x, y); - let m = dp * (c === CON ? (0.65 + 0.45 * v) * e : 1); + const cx = floor((x + (y >> 1) * 1.7) / 3), v = hash(cx, (y >> 1) + P.cyc * 97), c = v > 0.96 ? TILE : v < 0.05 ? BRICK : v > 0.9 ? PALE : CON, o = (y * W + x) * 3, f = fq(x, y); + let m = (1 - 0.35 * sm(0, 12, y - y0)) * (c === CON ? (0.65 + 0.45 * v) * (y & 1 ? 0.86 : 1.18) : 1); if (y === y0 && hash(x, 9) < 0.3) m = -1; else if (hash(cx * 7 + x, y * 3 + P.cyc) < 0.1) m *= 0.22; - if (y < H) for (let j = 0; j < 3; j++) F[o + j] = m < 0 ? RUST[j] * (L[9 + j] + 0.3) : c[j] * m * (L[9 + j] + L[12 + j] * sn + f); + for (let j = 0; j < 3; j++) F[o + j] = m < 0 ? RUST[j] * (L[9 + j] + 0.3) : c[j] * m * (L[9 + j] + L[12 + j] * sn + f); } } - if (P.ph > 6) - for (const [dx, w, sk, tl] of P.frag) { - const fx = CX + dx, fy = G - pileH(fx + w / 2) + 3; - for (let u = 0; u < w; u++) for (let v = 0; v < 7; v++) { - const hole = (v === 1 || v === 2 || v === 5) && u % 3 > 0; - put(fx + u + v * sk, fy - v + u * tl, hole ? [16, 14, 14] : lit(CON.map((q) => q * (v === 0 ? 0.7 : 0.95)), 1, 0.6, fq(fx, fy))); - } - } } -// Two long-reach diggers climb onto the heap and pick it apart, faster in the time-lapse. +// Two long-reach diggers climb the heap and pick it apart. function digger(x, f, ph) { - const g = round(G - max(0, pileH(x) - 1)), q = fq(x, g) + 0.32, Y = lit(YEL, 1, 0.6, q), Yd = Y.map((v) => v * 0.7), D = lit([70, 68, 66], 1, 0.4, q); - for (let i = -4; i <= 4; i++) put(x + i, g - 1, i & 1 ? D : D.map((v) => v * 1.8)), put(x + i, g - 2, D); + const g = round(G - max(0, pileH(x) - 1)), q = fq(x, g) + 0.32, Y = lit(YEL, 1, 0.6, q), Yd = Y.map((v) => v * 0.7), D = lit([70, 68, 66], 1, 0.5, q); + for (let i = -4; i <= 4; i++) put(x + i, g - 1, D, i & 1 ? 1 : 0.4), put(x + i, g - 2, D); for (let i = -5; i <= 2; i++) for (let j = 3; j <= 5; j++) put(x + i * f, g - j, i < -3 ? Yd : Y); - for (let j = 6; j <= 9; j++) for (let i = 1; i <= 3; i++) put(x + i * f, g - j, i === 2 && j > 6 && j < 9 ? lit([90, 130, 170], 1, 0.8, q) : Y); + for (let j = 6; j <= 9; j++) for (let i = 1; i <= 3; i++) put(x + i * f, g - j, i === 2 && j > 6 && j < 9 ? GLASS : Y); const a1 = 1 + 0.3 * sin(ph), bx = x + 2 * f, by = g - 5, ex = bx + f * cos(a1) * 11, ey = by - sin(a1) * 11, a2 = -1.2 + 0.5 * sin(ph + 1.3), kx = ex + f * cos(a2) * 8, ky = ey - sin(a2) * 8; line(bx, by, ex, ey, Y), line(bx - f, by, ex - f, ey, Yd), line(ex, ey, kx, ky, Y); for (let i = 0; i < 2; i++) for (let j = 0; j < 3; j++) put(kx + i * f, ky + j, D); if ((P.rt * 1.6 + f * 0.25) % 1 < 0.5) glow(x + 2 * f, g - 10, AMBER, 0.45, 2); } function machines() { - const T = P.T; - if (T < T_EX || T > T_CLR + 1) return; - const tl = T > T_TL ? 1 : 0, out = sm(T_CLR - 1, T_CLR + 0.5, T); - for (const [x0, xa, f, j] of [[-10, CX - 13, 1, 0], [76, CX + 13, -1, 1]]) { - const x = mix(mix(x0, xa, sm(T_EX + j, T_EX + 2.5 + j, T)), x0, out) + tl * round((hash(floor(T * 4), j) - 0.5) * 6); - digger(round(x), f, T * (tl ? 9 : 2.2) + j * 2); - } + const T = P.T, out = sm(T_CLR - 1, T_CLR + 0.5, T); + if (T > T_EX && T < T_CLR + 1) + for (const [x0, xa, f, j] of [[-10, CX - 13, 1, 0], [76, CX + 13, -1, 1]]) + digger(round(mix(mix(x0, xa, sm(T_EX + j, T_EX + 2.5 + j, T)), x0, out)), f, T * (T > T_TL ? 9 : 2.2) + j * 2); } -// Fence with mesh, posts and barrier tape; the siren lights on two posts. +// Fence, mesh, barrier tape; siren lights on two posts. function fence() { - const mc = lit([150, 152, 158], 1, 0.4), pc = lit([96, 98, 104], 1, 0.5); - for (let y = 102; y < 118; y++) for (let x = 0; x < W; x++) { - if (x % 12 === 5 || y === 102) put(x, y, pc); - else if ((x + y) % 3 === 0 || (x - y + 300) % 3 === 0) put(x, y, mc, 0.28); - } - for (let x = 0; x < W; x++) put(x, 107 + round(sin(x * 0.6 + P.rt * 5) * (0.3 + abs(P.w))), (x + 40) % 4 < 2 ? lit([250, 210, 40], 1, 0.5, 0.15) : [20, 20, 20]); - const T = P.T; + const mc = lit([150, 152, 158]), pc = lit([96, 98, 104]), tape = lit([250, 210, 40], 1, 0.5, 0.15); + for (let y = 102; y < 118; y++) for (let x = 0; x < W; x++) x % 12 === 5 || y === 102 ? put(x, y, pc) : ((x + y) % 3 === 0 || (x - y + 300) % 3 === 0) && put(x, y, mc, 0.28); + for (let x = 0; x < W; x++) put(x, 107 + round(sin(x * 0.6 + P.rt * 5) * (0.3 + abs(P.w))), (x + 40) % 4 < 2 ? tape : SIL); for (const [x, s] of [[5, 0], [53, 0.5]]) { - const on = T < T_FALL + 0.8 && (P.rt * 2.5 + s) % 1 < 0.45; - put(x, 100, on ? [255, 190, 80] : [90, 50, 20]), put(x, 101, [60, 60, 60]); + const on = P.T < T_FALL + 0.8 && (P.rt * 2.5 + s) % 1 < 0.45; + put(x, 100, on ? [255, 190, 80] : [90, 50, 20]); if (on) glow(x, 100, AMBER, 0.6, 5); } } -// Dust: each puff is a ball lit from the sun's side and from the sky above, laid over the ones before it, so the billows keep their edges. +// Each puff is a grainy ball lit by sun and sky, laid over the last, so billows keep their edges. function dust() { const { NT, pf, FL } = P, fld = L[16] * 1.8, gf = P.gfl * 0.6, [L9, L10, L11, L12, L13, L14] = L.slice(9, 15); for (let p = 0; p < P.np; p++) { @@ -421,17 +344,18 @@ function dust() { const a = pf[q + 7] * sm(0, 0.25, age) * (1 - sm(0.4 * life, life, age)) * (pf[q + 5] / r) ** 0.35; if (a < 0.01) continue; const t = pf[q + 11], ox = (p * 23) & 63, oy = p * 41 + floor(age * 3), ir = 1 / r, pb = 0.85 + 0.3 * hash(p, P.cyc); - const a0 = mix(DUST0[0], DUST1[0], t) * pb, a1 = mix(DUST0[1], DUST1[1], t) * pb, a2 = mix(DUST0[2], DUST1[2], t) * pb; + const [a0, a1, a2] = DUST0.map((v, c) => mix(v, DUST1[c], t) * pb); for (let y = max(0, ceil(py - r)), y1 = min(H - 1, floor(py + r)); y <= y1; y++) { - const dy = (y - py) * ir, dy2 = dy * dy, row = y * W, nr = ((y + oy) & 127) << 6, n2 = ((y + oy + 64) & 127) << 6, sh = 0.45 + 0.55 * sm(G, 40, y), sk = 0.8 - 0.25 * dy; + const dy = (y - py) * ir, dy2 = dy * dy, row = y * W, nr = ((y + oy) & 127) << 6, sh = 0.45 + 0.55 * sm(G, 40, y), sk = 0.8 - 0.25 * dy; for (let x = max(0, ceil(px - r)), x1 = min(W - 1, floor(px + r)); x <= x1; x++) { const dx = (x - px) * ir, d2 = dx * dx + dy2; if (d2 >= 1) continue; - let e = 2 - d2 * (0.7 + 2.4 * NT[nr | ((x + ox) & 63)]); + const n = NT[nr | ((x + ox) & 63)]; + let e = 2 - d2 * (0.7 + 2.4 * n); if (e <= 0.02) continue; if (e > 1) e = 1; const k = min(0.95, a * e * e * (3 - 2 * e)), i = row + x, o = i * 3, l = -0.6 * dx - 0.5 * dy + 0.62 * sqrt(1 - d2); - const gr = 0.6 + 0.8 * NT[n2 | ((x * 2 + ox) & 63)], sun = (0.25 + 0.8 * (l > 0 ? l : 0)) * sh * gr, sky = sk * (0.55 + 0.45 * gr), fl = FL[i] * fld + gf; + const gr = 0.4 + 1.2 * n, sun = (0.25 + 0.8 * (l > 0 ? l : 0)) * sh * gr, sky = sk * (0.55 + 0.45 * gr), fl = FL[i] * fld + gf; F[o] += (a0 * (L9 * sky + L12 * sun + fl) - F[o]) * k; F[o + 1] += (a1 * (L10 * sky + L13 * sun + fl) - F[o + 1]) * k; F[o + 2] += (a2 * (L11 * sky + L14 * sun + fl) - F[o + 2]) * k; @@ -440,17 +364,16 @@ function dust() { } } +// The crowd: coats, marshals in hi-vis, phones up once the siren goes. function people() { - const T = P.T, ph = T > T_CH - 1.5 && T < T_TL ? 0.6 : 0.12, rim = lit([70, 70, 80], 1, 0.5, 0).map((v) => v + P.gfl * 120); - for (const [x, hy0, w, r1, r2, cc] of P.cr) { - const hy = hy0 - (T > T_FALL && T < T_TL && r2 < 0.35 && sin(P.rt * 9 + r2 * 40) > 0.6 ? 1 : 0), hw = w >> 1, coat = lit(COATS[cc & 3], 0.5, 0.25, 0.1), vest = cc === 4; - for (let y = hy + 3; y < H; y++) for (let i = -hw + (y === hy + 3); i <= hw - (w & 1 ? 0 : 1) - (y === hy + 3); i++) put(x + i, y, vest && y > hy + 3 && y < hy + 9 ? (y === hy + 6 ? lit([220, 220, 210], 1, 0.5, 0.3) : lit(COATS[4], 1, 0.5, 0.2)) : coat); + const T = P.T, ph = T > T_CH - 1.5 && T < T_TL ? 0.6 : 0.12, vest = lit(COATS[4], 1, 0.5, 0.2), strip = lit([220, 220, 210], 1, 0.5, 0.3); + for (const [x, hy, w, r1, , cc] of P.cr) { + const hw = w >> 1, coat = lit(COATS[cc & 3], 0.5, 0.25, 0.1); + for (let y = hy + 3, e; y < H; y++) for (let i = -hw + (e = y === hy + 3); i <= hw - !(w & 1) - e; i++) put(x + i, y, cc > 3 && y > hy + 3 && y < hy + 9 ? (y === hy + 6 ? strip : vest) : coat); for (let j = 0; j < 3; j++) for (let i = -1; i <= 1; i++) if (j || !i) put(x + i, hy + j, SIL); - add(x, hy, rim, 0.5), add(x - hw + 1, hy + 3, rim, 0.35); if (r1 < ph) { - put(x + 2, hy + 2, SIL), put(x + 2, hy + 1, SIL), put(x + 2, hy, SIL); - const fl = T > T_CH && T < T_TL - 8 && hash(P.fr, x) < 0.015; - put(x + 2, hy - 1, fl ? [255, 255, 255] : [150, 180, 240]), put(x + 2, hy - 2, fl ? [255, 255, 255] : [150, 180, 240]); + const sc = T > T_CH && T < 11 && hash(floor(P.rt * 30), x) < 0.015 ? [255, 255, 255] : PHONE; + for (let j = -2; j < 3; j++) put(x + 2, hy + j, j < 0 ? sc : SIL); } } } @@ -464,14 +387,11 @@ export function draw(display, params) { pile(); machines(); fence(); - for (const [x, y] of P.db) put(x, y, lit([150, 144, 136], 1, 0.6, fq(x, y))); - const wl = lit([170, 190, 210], 1, 0.5, 0.2); - for (const [x, y] of P.sp) add(x, y, wl, 0.16), add(x + 1, y, wl, 0.08), add(x, y + 1, wl, 0.08); + const wl = lit([170, 190, 210], 1, 0.5, 0.2), bc = L[16] > 0.7 ? [140, 140, 150] : [24, 24, 30]; + // The water cannon on the right roof wets the site. + for (let j = 0, t; j < 60 && P.T > 1 && P.T < T_TL; j++) (t = ((j * 0.618 + P.rt * 0.8) % 1) * 1.5), add(52 + (P.w * 6 - 15 - 6 * hash(j, 5)) * t, 86 - (10 + 5 * hash(j, 6)) * t + 13 * t * t, wl, 0.2); dust(); - for (const b of P.bd) { - const up = sin(b[7]) > 0 ? -1 : 0, c = L[16] > 0.7 ? [140, 140, 150] : [24, 24, 30]; - put(b[0], b[1], c), put(b[0] - 1, b[1] + up, c), put(b[0] + 1, b[1] + up, c); - } + for (const [x, y, , , , , , fp] of P.bd) for (let i = -1, up = sin(fp) > 0 ? -1 : 0; i < 2; i++) put(x + i, y + (i ? up : 0), bc); people(); for (let i = 0, o = 0; i < N; i++, o += 3) display.setPixel(i & 63, i >> 6, cl(F[o], 0, 255) | 0, cl(F[o + 1], 0, 255) | 0, cl(F[o + 2], 0, 255) | 0); } From bff1cf35696bfdc7463eb7aeaef5e160fcf8a181 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:00:28 +0000 Subject: [PATCH 09/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Lichtenberg?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pattern-study/patterns/lichtenberg.js | 585 ++++++++++++++++++ 1 file changed, 585 insertions(+) create mode 100644 incoming/pattern-study/patterns/lichtenberg.js diff --git a/incoming/pattern-study/patterns/lichtenberg.js b/incoming/pattern-study/patterns/lichtenberg.js new file mode 100644 index 000000000..8b58f1195 --- /dev/null +++ b/incoming/pattern-study/patterns/lichtenberg.js @@ -0,0 +1,585 @@ +// ===== Patternflow pattern ===== +// Title: Lichtenberg +// Family: fractal +// Idea: High voltage creeps over a wet board of pale wood from an electrode at each end, two branching trees of blue-white plasma and orange embers burn toward each other and char black, and when they meet an arc flashes down the joined path, the power cuts, the embers die and the next board slides in. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 26, after fractal wood burning and the dielectric breakdown model (Niemeyer, Pietronero and Wiesmann, 1984) +// Round: 26 +// =============================== +// @matrix 64x128 +// @knobs Wood=0..1, Speed=0.2..3, Pull=0.5..4, Lead=-1..1 +// +// Knob 1: Wood. The board: pale maple at 0, then white birch, walnut and black-stained oak at 1. The char is black-brown on the pale boards; on the stained one the burn eats through the stain and leaves grey ash. +// Knob 2: Speed. At 2 the power comes on a second and a quarter after a board is in, the trees meet four and a half seconds later, and a board takes eleven seconds from wet to gone; at 0.2 it all runs ten times slower. +// Knob 3: Pull. How hard the far electrode's field draws the growth, the exponent of the dielectric breakdown model: at 0.5 the trees grow bushy, feathered with side twigs, and fill the board; at 4 they shoot at each other in a few straight leaders with a twig here and there. +// Knob 4: Lead. Which electrode leads: at -1 the top tree burns most of the way down, at 1 the bottom one climbs most of the way up, and near 0 they meet halfway. +// Buttons: K1 tap cuts the power and slides the next board in now. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, round, exp, sqrt, pow, sin, abs, log2 } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const rnd = () => Math.random(); +const F32 = (n) => new Float32Array(n); +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 vn(x, y) { + const xi = floor(x), yi = floor(y), u = x - xi, v = y - yi, su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v); + const a = hash(xi, yi), b = hash(xi + 1, yi), c = hash(xi, yi + 1); + return a + (b - a) * su + (c - a) * sv + (a - b - c + hash(xi + 1, yi + 1)) * su * sv; +} + +// The board at rest fills columns X0..X1 and rows Y0..Y1 of the panel; the +// top electrode touches it at (TX, TY) and the bottom one at (BX, BY). +const X0 = 2, X1 = 61, Y0 = 3, Y1 = 124, TX = 32, TY = 9, BX = 31, BY = 118; +// A board slides down this far to make way for the next, leaving a few rows +// of the rig showing between them. +const SL = 131; +// Seconds at Speed 2: wet board before the power, growth until the trees +// meet, the arc, the embers dying, the slide. +const SOAK = 1.25, TG = 4.8, ARC = 0.5, COOL = 3, SLIDE = 1.6; +// Cells each tree adds a second at the front when it keeps pace with its +// schedule, and twigs a second it sprouts anywhere along its branches. +const RATE = 200, TWIG = 260; +// A cell that joined the tree this recently is still plasma. +const TIP = 0.16; +const DIRS = [[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]]; +// The DIRS index of a step (dx, dy), at (dy + 1) * 3 + dx + 1. +const DIX = [5, 6, 7, 4, -1, 0, 3, 2, 1]; +const N4 = [-1, 1, -W, W], ND = [-W - 1, -W + 1, W - 1, W + 1], LUT = [0, 1, 16]; + +// Board, grain, knot and char colours for maple, birch, walnut and +// black-stained oak. Char is black-brown on the pale woods; on the stained +// board the burn eats through the stain and leaves grey ash. +const WOODS = [ + [[238, 206, 160], [160, 110, 66], [128, 76, 40], [30, 19, 14]], + [[244, 232, 204], [210, 186, 142], [120, 90, 58], [30, 19, 14]], + [[140, 90, 58], [88, 54, 34], [50, 30, 18], [26, 16, 12]], + [[50, 44, 46], [30, 26, 28], [18, 14, 14], [92, 86, 80]], +]; +const SMOKE = [196, 190, 184], RIG = [16, 17, 22]; + +// One board's wood: grain darkness (above 1 inside a knot), how wet the soda +// wash left each spot, and a small per-channel speckle. +function wood() { + const g = F32(N), wet = F32(N), nz = new Int8Array(N * 3); + const ox = rnd() * 300, oy = rnd() * 300, per = 2.6 + rnd() * 0.6, tilt = (rnd() - 0.5) * 0.06; + const knots = []; + for (let k = 0, nk = 1 + floor(rnd() * 3); k < nk; k++) + knots.push([X0 + 9 + rnd() * (X1 - X0 - 18), Y0 + 14 + rnd() * (Y1 - Y0 - 28), 1.6 + rnd() * 1.4]); + for (let y = Y0; y <= Y1; y++) + for (let x = X0; x <= X1; x++) { + const i = y * W + x; + let u = x + tilt * y + 9 * (vn(x / 26 + ox, y / 40 + oy) - 0.5) + 2 * (vn(x / 7 + oy, y / 11 + ox) - 0.5), kd = 0; + for (const [kx, ky, kr] of knots) { + const dx = x - kx, dy = (y - ky) / 2.2, d2 = dx * dx + dy * dy; + // The grain bends round a knot and crowds in close to it. + u += (kr * kr * 3 * dx) / (d2 + kr * kr); + kd = max(kd, 1 - sqrt(d2) / kr); + } + // Thin dark latewood lines, a pore streak here and there. + const c = 0.5 + 0.5 * Math.cos((2 * Math.PI * u) / per); + let v = c ** 4 * (0.8 + 0.6 * vn(x / 5 + ox, y / 30)) + 0.22 * (hash(x, floor(y / 4) + 7) - 0.5); + if (kd > 0) v = 1 + kd * 0.6; + g[i] = cl(v, 0, 1.6); + wet[i] = 0.5 + 0.5 * vn(x / 9 + oy, y / 9 + ox); + for (let ch = 0; ch < 3; ch++) nz[i * 3 + ch] = round((rnd() - 0.5) * 6); + } + return { g, wet, nz, col: F32(N * 3), at: -1 }; +} + +// A board's colours for this setting of K1: grain and knots, the lamp above, +// the bevelled edges and the speckle. +function paint(B, wood) { + const wv = wood * 3, wi = min(2, floor(wv)), wf = wv - wi, WC = []; + for (let j = 0; j < 4; j++) WC.push([0, 1, 2].map((c) => WOODS[wi][j][c] + (WOODS[wi + 1][j][c] - WOODS[wi][j][c]) * wf)); + B.ch = WC[3]; + for (let y = Y0; y <= Y1; y++) + for (let x = X0; x <= X1; x++) { + const i = y * W + x, gv = B.g[i], lo = gv <= 1 ? 0 : 1, t = gv <= 1 ? gv : (gv - 1) / 0.6; + let lf = 1.02 - 0.12 * (y / H); + if (x === X0 || y === Y0) lf *= 1.08; + if (x === X1 || y === Y1) lf *= 0.7; + for (let c = 0; c < 3; c++) B.col[i * 3 + c] = (WC[lo][c] + (WC[lo + 1][c] - WC[lo][c]) * t) * lf + B.nz[i * 3 + c]; + } + B.at = wood; +} + +export function setup(params) { + const P = (params.s = { + time: 0, ph: 0, pt: 0, tau: 0, power: 0, wood: 0, eta: 0.5, lead: 0, ym: 64, + own: new Uint8Array(N), twg: new Uint8Array(N), par: new Int32Array(N), cnt: new Int32Array(N), born: F32(N), + phi: F32(N), phi0: F32(N), chr: F32(N), sco: F32(N), ht: F32(N), + cand: [new Int32Array(N), new Int32Array(N)], cw: [F32(N), F32(N)], nc: [0, 0], acc: [0, 0], tac: [0, 0], tw: [], ntw: 0, cells: [new Int32Array(N), new Int32Array(N)], ncell: [1, 1], + fr: [TY, BY], meet: -1, meetQ: -1, path: new Int32Array(N), np: 0, + pl: F32(N), sm: F32(N), puffs: [], ring: new Int32Array(512), nr: 0, hot: new Int32Array(32), nh: 0, flash: 0, + wa: wood(), wb: wood(), + }); + for (let k = 0; k < 256; k++) P.tw.push({ c: 0, dx: 0, dy: 0, k: 1, left: 0 }); + for (let k = 0; k < 70; k++) P.puffs.push({ on: 0 }); + // The field round the two bare electrodes, solved once from a straight ramp. + for (let y = Y0; y <= Y1; y++) for (let x = X0; x <= X1; x++) P.phi[y * W + x] = cl((y - TY) / (BY - TY), 0, 1); + (P.own[TY * W + TX] = 1), (P.phi[TY * W + TX] = 0), (P.own[BY * W + BX] = 2), (P.phi[BY * W + BX] = 1); + relax(P, 400); + P.phi0.set(P.phi); + fresh(P); +} + +// A clean board under the electrodes, wet and unburnt. +function fresh(P) { + for (const a of [P.own, P.twg, P.cnt, P.chr, P.sco, P.ht]) a.fill(0); + P.par.fill(-1); + P.born.fill(-9); + P.phi.set(P.phi0); + for (const t of P.tw) t.left = 0; + const t = TY * W + TX, b = BY * W + BX; + (P.own[t] = P.cnt[t] = P.cnt[b] = 1), (P.own[b] = 2), (P.cells[0][0] = t), (P.cells[1][0] = b); + Object.assign(P, { ncell: [1, 1], fr: [TY, BY], acc: [0, 0], tac: [0, 0], meet: -1, meetQ: -1, np: 0, tau: 0 }); +} + +// Red-black over-relaxation of Laplace's equation on the board: the top tree +// held at 0, the bottom one at 1, the board's edges insulating. +function relax(P, sweeps) { + const { own, phi } = P; + for (let s = 0; s < sweeps; s++) + for (let c = 0; c < 2; c++) + for (let y = Y0; y <= Y1; y++) { + const r = y * W; + for (let x = X0 + ((y + c) & 1); x <= X1; x += 2) { + const i = r + x; + if (own[i]) continue; + const v = phi[i]; + const a = x > X0 ? phi[i - 1] : v, b = x < X1 ? phi[i + 1] : v, u = y > Y0 ? phi[i - W] : v, d = y < Y1 ? phi[i + W] : v; + phi[i] = v + 1.85 * ((a + b + u + d) * 0.25 - v); + } + } +} + +// Every wood cell beside exactly one cell of a tree, or touching one only +// at a corner, may join it, with a chance that goes as the field there to +// the power eta. The board stands clear of the array's edges and nothing +// off it ever joins, so its neighbours need no bounds checks. +function scan(P, eta) { + const { own, phi, cand, cw } = P, n = [0, 0], t = [0, 0]; + for (let y = Y0; y <= Y1; y++) + for (let x = X0; x <= X1; x++) { + const i = y * W + x; + if (own[i]) continue; + // Neighbours of each tree counted in one number, four bits a tree. + const s4 = LUT[own[i - 1]] + LUT[own[i + 1]] + LUT[own[i - W]] + LUT[own[i + W]]; + const s8 = LUT[own[i - W - 1]] + LUT[own[i - W + 1]] + LUT[own[i + W - 1]] + LUT[own[i + W + 1]]; + for (let k = 0; k < 2; k++) { + const a = (s4 >> (4 * k)) & 15; + if (a > 1 || (!a && ((s8 >> (4 * k)) & 15) !== 1)) continue; + t[k] += pow(max(k ? 1 - phi[i] : phi[i], 1e-5), eta); + cand[k][n[k]] = i; + cw[k][n[k]++] = t[k]; + } + } + P.nc = n; +} + +// A cell joins tree k only if it keeps the branches one LED wide: it grows +// from one cell, beside it or at its corner, and nothing else of the tree +// may touch it but that cell's own neighbours. Touching the other tree is +// the meeting. +function join(P, c, k, tw = 0) { + const { own } = P; + if (own[c]) return false; + let p = -1, m = 0; + for (const o of N4) if (own[c + o] === k) (p = c + o), m++; + if (m > 1) return false; + if (p < 0) for (const o of ND) if (own[c + o] === k && p < 0) p = c + o; + if (p < 0) return false; + const px = p & 63, py = p >> 6; + let other = -1; + for (let dy = -1; dy <= 1; dy++) + for (let dx = -1; dx <= 1; dx++) { + const q = c + dy * W + dx; + if (q === c || q === p || !own[q]) continue; + if (own[q] !== k) { + // The trees may not close before their time. + if (P.tau < TG - 0.25) return false; + other = q; + } else if (abs((q & 63) - px) > 1 || abs((q >> 6) - py) > 1) return false; + } + add(P, c, p, k, tw); + if (other >= 0) (P.meet = c), (P.meetQ = other); + return true; +} + +function add(P, c, p, k, tw) { + const { own, chr, sco, cnt, par } = P; + own[c] = k; + P.twg[c] = tw; + par[c] = p; + cnt[c] = 1; + P.born[c] = P.time; + P.ht[c] = 1; + chr[c] = 1; + P.phi[c] = k === 1 ? 0 : 1; + const y = c >> 6, x = c & 63; + if (k === 1) P.fr[0] = max(P.fr[0], y); + else P.fr[1] = min(P.fr[1], y); + scorch(P, x, y, 1, 0.04); + P.ring[P.nr++ & 511] = c; + P.cells[k - 1][P.ncell[k - 1]++] = c; + // The current that feeds every cell beyond reaches it through all the + // cells back to the electrode, and the busiest burn into wide grooves. + for (let a = p; a >= 0; a = par[a]) { + const n = ++cnt[a]; + if (n === 80 || n === 300 || n === 1000) { + const ax = a & 63, ay = a >> 6, r = n === 80 ? 1 : n === 300 ? 1.5 : 2.2; + scorch(P, ax, ay, r, n === 80 ? 0.6 : 0.9, 1); + scorch(P, ax, ay, r + 1.5, 0.2); + } + } +} + +// With burn set, char a groove of radius r round (x, y), still warm; +// without, brown the wood within r by v, less towards the edge. +function scorch(P, x, y, r, v, burn) { + const R = Math.ceil(r); + for (let dy = -R; dy <= R; dy++) + for (let dx = -R; dx <= R; dx++) { + const qx = x + dx, qy = y + dy, d2 = dx * dx + dy * dy, q = qy * W + qx; + if (d2 > r * r || qx < X0 || qx > X1 || qy < Y0 || qy > Y1) continue; + if (!burn) P.sco[q] = min(1, P.sco[q] + v * (1 - d2 / (r * r + 1))); + else (P.chr[q] = max(P.chr[q], v)), (P.ht[q] = max(P.ht[q], 0.5 * v)); + } +} + +function grow(P, d) { + relax(P, 3); + // Each tree has a front it should have reached by now, eased in so the + // growth speeds up as the trees close, and a cell budget that rises + // steeply when it lags. + const s = min(1, P.tau / TG) ** 1.7, late = max(0, P.tau - TG); + const lag = [TY + (P.ym - TY) * s - P.fr[0], P.fr[1] - (BY - (BY - P.ym) * s)]; + // Side twigs sprout off the branches, more of them the weaker the pull. + // The leading electrode's tree takes the larger share of both. + for (let k = 0; k < 2; k++) { + const f = exp(cl(lag[k] / 6, -2, 2.5)), sh = 1 + (k ? 0.85 : -0.85) * P.lead; + P.acc[k] += d * RATE * sh * f * (1 + 4 * late); + P.tac[k] += d * TWIG * sh * (1.25 - P.eta / 4) ** 2 * cl(f, 0.35, 1.5); + } + twigs(P); + if (P.acc[0] < 1 && P.acc[1] < 1 && P.tac[0] < 1 && P.tac[1] < 1) return; + scan(P, P.eta + 2 * late); + for (let k = 0; k < 2; k++) { + const n = min(120, floor(P.acc[k])), C = P.cand[k], S = P.cw[k], m = P.nc[k]; + P.acc[k] -= n; + if (!m) continue; + const tot = S[m - 1]; + for (let t = 0, tries = 0; t < n && tries < 3 * n; tries++) { + const r = rnd() * tot; + let lo = 0, hi = m - 1; + while (lo < hi) { + const mid = (lo + hi) >> 1; + if (S[mid] < r) lo = mid + 1; + else hi = mid; + } + if (join(P, C[lo], k + 1)) t++; + if (P.meet >= 0) return; + } + } + // A twig sprouts an eighth or a quarter turn off a branch the front laid + // down (seldom off another twig) and runs on straight for a few LEDs: the + // feathering along every branch. + for (let tries = 0, k = 0; tries < 400; tries++) { + if (P.tac[k] < 1) k = 1 - k; + if (P.tac[k] < 1) break; + const nc = P.ncell[k]; + const a = P.cells[k][floor(rnd() * nc)], pa = P.par[a]; + if (pa < 0 || (P.twg[a] && rnd() < 0.9)) continue; + const j = DIX[((a >> 6) - (pa >> 6) + 1) * 3 + (a & 63) - (pa & 63) + 1]; + const dir = (j + (rnd() < 0.5 ? 1 : 7) * (rnd() < 0.85 ? 1 : 2)) & 7; + const x = (a & 63) + DIRS[dir][0], y = (a >> 6) + DIRS[dir][1], c = y * W + x; + if (x < X0 || x > X1 || y < Y0 || y > Y1 || !join(P, c, k + 1, 1)) continue; + P.tac[k]--; + if (P.meet >= 0) return; + Object.assign(P.tw[P.ntw++ & 255], { c, dx: DIRS[dir][0], dy: DIRS[dir][1], k: k + 1, left: 2 + floor(rnd() * 7) }); + } + P.tac = P.tac.map((v) => min(v, 4)); +} + +function twigs(P) { + for (const t of P.tw) { + if (t.left <= 0) continue; + t.left--; + let dx = t.dx, dy = t.dy; + // Now and then a twig bends by an eighth of a turn. + if (rnd() < 0.12) { + const a = (DIX[(dy + 1) * 3 + dx + 1] + (rnd() < 0.5 ? 1 : 7)) & 7; + (dx = DIRS[a][0]), (dy = DIRS[a][1]); + } + const x = (t.c & 63) + dx, y = (t.c >> 6) + dy, c = y * W + x; + if (x < X0 || x > X1 || y < Y0 || y > Y1 || !join(P, c, t.k, 1)) { + t.left = 0; + continue; + } + t.c = c; + if (P.meet >= 0) return; + } +} + +// The joined path, from the top electrode to the bottom one. +function trace(P) { + const top = P.own[P.meet] === 1 ? P.meet : P.meetQ, bot = top === P.meet ? P.meetQ : P.meet; + let n = 0; + for (let a = top; a >= 0; a = P.par[a]) P.path[n++] = a; + P.path.subarray(0, n).reverse(); + for (let a = bot; a >= 0; a = P.par[a]) P.path[n++] = a; + P.np = n; + for (let j = 0; j < n; j++) { + const a = P.path[j], x = a & 63, y = a >> 6; + scorch(P, x, y, 1.2, 1, 1); + scorch(P, x, y, 3.2, 0.5); + P.ht[a] = 1; + } +} + +function go(P, ph) { + P.ph = ph; + P.pt = 0; + if (ph === 1) P.power = 1; + if (ph === 2) trace(P), (P.flash = 1); + if (ph === 3 || ph === 4) P.power = 0; + if (ph === 0) ([P.wa, P.wb] = [P.wb, P.wa]), fresh(P); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0.5, 0.06]; + const d = (cl(dt || 0, 0, 0.1) * cl(k[1], 0.2, 3)) / 2; + P.wood = cl(k[0], 0, 1); + P.eta = cl(k[2], 0.5, 4); + P.lead = cl(k[3], -1, 1); + P.ym = (TY + BY) / 2 - 44 * P.lead; + P.time += d; + P.pt += d; + if (input.btnPressed && input.btnPressed[0] && P.ph > 0 && P.ph < 4) go(P, 4); + const ph = P.ph; + if (ph === 0 && P.pt >= SOAK) go(P, 1); + else if (ph === 1) { + P.tau += d; + grow(P, d); + // A board the trees cannot cross trips the breaker without an arc. + if (P.meet >= 0) go(P, 2); + else if (P.tau > TG + 3) go(P, 3); + } else if (ph === 2) { + // The arc keeps its path white hot. + for (let j = 0; j < P.np; j++) P.ht[P.path[j]] = 1; + if (P.pt >= ARC) go(P, 3); + } else if (ph === 3 && P.pt >= COOL) go(P, 4); + else if (ph === 4 && P.pt >= SLIDE) go(P, 0), (P.wb = wood()); + P.flash = max(0, P.flash - d * 9); + heat(P, d); + smoke(P, d); +} + +// Embers: fresh channels glow and cool to char, while the power is on the +// busy trunks stay hot, and anything hot browns the wood round it. +function heat(P, d) { + const { ht, own, cnt, sco, hot } = P, dec = exp(-d / (P.power ? 0.3 : 0.9)); + let sum = 0, nh = 0; + for (let y = Y0; y <= Y1; y++) + for (let x = X0; x <= X1; x++) { + const i = y * W + x; + let h = ht[i]; + if (h < 0.004 && !own[i]) continue; + h *= dec; + if (P.power && own[i] && cnt[i] > 24) h = max(h, min(0.6, 0.12 * (log2(cnt[i]) - 3.5)) * (0.85 + 0.15 * rnd())); + ht[i] = h; + if (h > 0.2 && (cnt[i] > 12 || !own[i])) { + sum += h; + for (const o of N4) sco[i + o] = min(1, sco[i + o] + d * h * 0.3); + if (nh < 32) hot[nh++] = i; + else if (rnd() < 0.05) hot[floor(rnd() * 32)] = i; + } + } + P.nh = nh; + P.heatSum = sum; +} + +// Smoke puffs rise off the hot channels, spread and thin out. +function smoke(P, d) { + let want = (P.ph === 4 ? 0 : P.heatSum) * d * 0.14; + for (const s of P.puffs) { + if (s.on) { + s.age += d; + if (s.age > s.life) { + s.on = 0; + continue; + } + s.x += (s.vx + 3 * sin(s.age * 2.3 + s.y * 0.1)) * d; + s.y += s.vy * d; + s.r += d * 1.6; + continue; + } + if (want > rnd() && P.nh) { + const i = P.hot[floor(rnd() * P.nh)]; + Object.assign(s, { on: 1, x: i & 63, y: i >> 6, vx: (rnd() - 0.5) * 4, vy: -8 - rnd() * 8, r: 1, a: 0.16 + rnd() * 0.16, age: 0, life: 2.5 + rnd() * 2 }); + want -= 1; + } + } +} + +// Plasma at (x, y): how strongly it covers each LED round it. +function plasma(L, x, y, r, v) { + const R = Math.ceil(r); + for (let dy = -R; dy <= R; dy++) + for (let dx = -R; dx <= R; dx++) { + const qx = x + dx, qy = y + dy, d2 = dx * dx + dy * dy; + if (qx >= 0 && qx < W && qy >= 0 && qy < H && d2 <= r * r) L[qy * W + qx] += v / (1 + d2 * 2.5); + } +} + +// Ember colour by heat: dull red, orange, then yellow-white. +function ember(h, out) { + if (h < 0.25) (out[0] = 700 * h), (out[1] = 140 * h), (out[2] = 24 * h); + else if (h < 0.6) { + const t = (h - 0.25) / 0.35; + (out[0] = 175 + 80 * t), (out[1] = 35 + 75 * t), (out[2] = 6 + 14 * t); + } else { + const t = (h - 0.6) / 0.4; + (out[0] = 255), (out[1] = 110 + 110 * t), (out[2] = 20 + 120 * t); + } +} + +const EM = [0, 0, 0]; + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const { own, chr, sco, ht, born, pl, sm } = P; + pl.fill(0); + sm.fill(0); + if (P.wa.at !== P.wood) paint(P.wa, P.wood); + if (P.ph === 4 && P.wb.at !== P.wood) paint(P.wb, P.wood); + const off = P.ph === 4 ? SL * (1 - (1 + Math.cos((Math.PI * min(1, P.pt / SLIDE)))) / 2) : 0, oy = round(off); + + // Plasma at the tips, and short streamers crackling ahead of them. + if (P.power) { + for (let j = 0; j < 512 && j < P.nr; j++) { + const c = P.ring[(P.nr - 1 - j) & 511], age = P.time - born[c]; + if (age > TIP) break; + // Twigs behind the front only flash orange; the plasma rides the + // advancing edge of each tree. + if (P.twg[c] && (own[c] === 1 ? (c >> 6) < P.fr[0] - 14 : (c >> 6) > P.fr[1] + 14)) continue; + const f = (1 - age / TIP) * (0.5 + 0.5 * rnd()), x = c & 63, y = (c >> 6) + oy; + plasma(pl, x, y, 1.5, f); + if (rnd() < 0.2) { + let sx = x, sy = y; + const dir = own[c] === 1 ? 1 : -1; + for (let st = 0, len = 2 + floor(rnd() * 5); st < len; st++) { + if (rnd() < 0.6) sy += dir; + else sx += rnd() < 0.5 ? -1 : 1; + if (sx < 0 || sx >= W || sy < 0 || sy >= H) break; + pl[sy * W + sx] += 0.9 - st * 0.12; + } + } + } + // A blue corona at each electrode. + const z = 0.4 + 0.4 * rnd(); + plasma(pl, TX, TY + oy, 2.5, z); + plasma(pl, BX, BY + oy, 2.5, z); + } + // The arc down the joined path, flickering sideways. + if (P.ph === 2) { + const f = (1 - 0.6 * (P.pt / ARC)) * (0.7 + 0.3 * rnd()), seed = rnd() * 99; + for (let j = 0; j < P.np; j++) { + const a = P.path[j], x = (a & 63) + round(1.6 * (vn(j * 0.3, seed) - 0.5)), y = a >> 6; + if (j % 2 === 0) plasma(pl, x, y, 3.5, 0.35 * f); + pl[y * W + x] += 3 * f; + } + } + for (const s of P.puffs) { + if (!s.on) continue; + // A wisp, twice as tall as it is wide. + const a = s.a * sin((Math.PI * s.age) / s.life), R = Math.ceil(s.r), R2 = 2 * R; + for (let dy = -R2; dy <= R2; dy++) + for (let dx = -R; dx <= R; dx++) { + const qx = round(s.x) + dx, qy = round(s.y) + dy, d2 = (dx * dx + dy * dy * 0.25) / (s.r * s.r); + if (qx < 0 || qx >= W || qy < 0 || qy >= H || d2 >= 1) continue; + sm[qy * W + qx] += a * (1 - d2) * (1 - d2); + } + } + + const fl = P.flash * 70, CH = P.wa.ch; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + let r, g, b; + // Which board, if any, is under this LED. + let yb = y - oy, B = P.wa, burnt = 1; + if (P.ph === 4 && yb < Y0) (yb += SL), (B = P.wb), (burnt = 0); + if (x >= X0 && x <= X1 && yb >= Y0 && yb <= Y1) { + const i = yb * W + x, C = B.col; + // The wet wash glosses the wood until the burn dries it. + const s = burnt ? sco[i] : 0, lf = 0.94 + 0.12 * B.wet[i] * max(0, 1 - 3 * s); + r = C[i * 3] * lf; + g = C[i * 3 + 1] * lf; + b = C[i * 3 + 2] * lf; + if (burnt) { + if (s > 0) (r *= 1 - 0.5 * s), (g *= 1 - 0.66 * s), (b *= 1 - 0.8 * s); + const c = chr[i]; + if (c > 0) (r += (CH[0] - r) * c), (g += (CH[1] - g) * c), (b += (CH[2] - b) * c); + const h = ht[i]; + if (h > 0.02 && c > 0.3) { + // Thick char glows only along its rim. + const rim = (x > X0 && chr[i - 1] < 0.5) || (x < X1 && chr[i + 1] < 0.5) || chr[i - W] < 0.5 || chr[i + W] < 0.5; + ember(h * (rim ? 1 : 0.45), EM); + r = max(r, EM[0]); + g = max(g, EM[1]); + b = max(b, EM[2]); + } + } + r += fl; + g += fl; + b += fl * 1.2; + } else { + // The dark rig behind, with the board's shadow to its right. + const sh = x === X1 + 1 || x === X1 + 2 ? 0.55 : 1, q = hash(x, y); + r = (RIG[0] + 6 * q) * sh; + g = (RIG[1] + 6 * hash(y, x)) * sh; + b = (RIG[2] + 6 * q) * sh; + if (x === 0 || x === 63) (r = 64 + 10 * q), (g = 66 + 10 * q), (b = 74 + 10 * q); + } + const m = min(0.7, sm[p]); + r += (SMOKE[0] - r) * m; + g += (SMOKE[1] - g) * m; + b += (SMOKE[2] - b) * m; + // Plasma: deep blue where it is thin, blue-white where it is thick. + const t = pl[p]; + if (t > 0.01) { + const u = min(1, t), w = min(1, t / 2.5); + r += (90 + 160 * w - r) * u; + g += (120 + 130 * w - g) * u; + b += (255 - b) * u; + } + display.setPixel(x, y, cl(r, 0, 255) | 0, cl(g, 0, 255) | 0, cl(b, 0, 255) | 0); + } + electrodes(display, P); +} + +// Brass clamps with their tips on the board, the red lead in from the top +// and the black one from the bottom. +function electrodes(display, P) { + for (let e = 0; e < 2; e++) { + const cx = e ? BX : TX, s = e ? -1 : 1, y0 = e ? H - 1 : 0, lead = e ? [[96, 98, 110], [36, 36, 42]] : [[236, 84, 64], [172, 28, 22]]; + for (let j = 0; j <= 8; j++) + for (let dx = -3; dx <= 3; dx++) { + const lit = j === 4 || dx === -3, dark = j === 7 || dx === 3; + let c = null; + if (j <= 3 && abs(dx) <= 1) c = lead[dx < 0 ? 0 : 1]; + else if (j >= 4 && j <= 7) c = lit ? [236, 200, 110] : dark ? [130, 96, 40] : [196, 156, 70]; + else if (j === 8 && !dx) c = P.power ? [210, 220, 255] : [240, 214, 130]; + if (c) display.setPixel(cx + dx, y0 + s * j, c[0], c[1], c[2]); + } + } +} From d01f6bf6ac966030b7316e30e27c63ec4b6406f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:11:01 +0000 Subject: [PATCH 10/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Noborigama?= 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_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/patterns/noborigama.js | 530 ++++++------------ 1 file changed, 184 insertions(+), 346 deletions(-) diff --git a/incoming/pattern-study/patterns/noborigama.js b/incoming/pattern-study/patterns/noborigama.js index 07ffc3bc1..9a062f4ff 100644 --- a/incoming/pattern-study/patterns/noborigama.js +++ b/incoming/pattern-study/patterns/noborigama.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Noborigama // Family: pottery -// Idea: A climbing kiln cut open on a hillside at night: the flame climbs from the firebox through a chain of vaulted chambers stacked with pots, white-yellow below and orange above, while a potter with a lamp climbs the ladders to throw split wood into the stoke holes, and each stoke pushes flame out of the spy holes and black smoke and sparks out of the chimney into the stars. +// Idea: A climbing kiln cut open on a hillside at night: flame climbs from the firebox through vaulted chambers of pots, white-yellow below and red above, and each time the potter stokes, flame bursts from the spy holes and black smoke and sparks leave the chimney. // Author: patternflow-lab // Date: 2026-10-24 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -11,14 +11,14 @@ // @matrix 64x128 // @knobs Firing=-1..1, Speed=0.2..3, Chambers=3..6, Draft=-1..1 // -// Knob 1: Firing. How far the firing has come. At -1 it is the first day: only the firebox burns hot and the chambers glow dull red. At 0 the lower chambers are at white-yellow heat and the top one is still orange; at 1, the last day, every chamber is close to white. -// Knob 2: Speed. At 2 the potter stokes every few seconds and climbs from one chamber to the next in about three; at 0.2 everything runs ten times slower. -// Knob 3: Chambers. How many chambers climb the slope between the firebox and the chimney, three to six. With more, each one is lower and narrower and carries fewer shelves of pots. -// Knob 4: Draft. Wind over the hill, from the left at -1 to the right at 1. It leans the smoke, the sparks, the flames inside the chambers and the tongues out of the spy holes. -// Buttons: K1 tap stokes every hole at once: wood goes into each chamber and the firebox, flame bursts from every spy hole and the chimney throws out a big black cloud full of sparks. +// Knob 1: Firing. The first day at -1: only the firebox burns hot, the chambers glow dull red. At 0 the lower chambers are white-yellow and the top one red; at 1, the last day, all are near white. +// Knob 2: Speed. At 2 the potter stokes every few seconds; 0.2 is ten times slower. +// Knob 3: Chambers. Three to six chambers between the firebox and the chimney; with more, each is lower, with fewer shelves. +// Knob 4: Draft. Wind from the left at -1 to the right at 1 leans smoke, sparks and flames. Up the hill it pulls the fire hotter; down it, it chokes the chimney and the spy holes lick flame. +// Buttons: K1 tap stokes every hole at once: flame from every spy hole, then a black cloud full of sparks from the chimney. const W = 64, H = 128, N = W * H; -const { min, max, floor, round, sin, sqrt, exp, abs, PI } = Math; +const { min, max, floor, round, sin, sqrt, exp, abs, hypot, PI } = Math; const cl = (v, a, b) => (v < a ? a : v > b ? b : v); const mix = (a, b, t) => a + (b - a) * t; const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); @@ -28,21 +28,14 @@ function hash(i, j) { h = Math.imul(h ^ (h >>> 13), 1274126177); return ((h ^ (h >>> 16)) >>> 0) / 4294967296; } -function vn(x, y) { +function vn(x, y, p = 1e9) { const xi = floor(x), yi = floor(y), u = x - xi, v = y - yi, su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v); - const a = hash(xi, yi), b = hash(xi + 1, yi), c = hash(xi, yi + 1); - return a + (b - a) * su + (c - a) * sv + (a - b - c + hash(xi + 1, yi + 1)) * su * sv; + const a = hash(xi, yi % p), b = hash(xi + 1, yi % p), c = hash(xi, (yi + 1) % p); + return a + (b - a) * su + (c - a) * sv + (a - b - c + hash(xi + 1, (yi + 1) % p)) * su * sv; } - -// A strip of fine noise 256 rows tall that wraps, for the quick flicker in -// flame and smoke: cheaper to look up than to work out for every LED. -const FN = new Float32Array(64 * 256); -for (let y = 0; y < 256; y++) - for (let x = 0; x < 64; x++) { - const X = x * 0.9, Y = (y * 84) / 256, xi = floor(X), yi = floor(Y), u = X - xi, v = Y - yi, su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v); - const a = hash(xi, yi % 84), b = hash(xi + 1, yi % 84), c = hash(xi, (yi + 1) % 84), e = hash(xi + 1, (yi + 1) % 84); - FN[y * 64 + x] = a + (b - a) * su + (c - a) * sv + (a - b - c + e) * su * sv; - } +// Fine noise that wraps every 256 rows, for the quick flicker of flame and smoke. +const FN = new Float32Array(64 * 256).map((_, i) => vn((i & 63) * 0.9, ((i >> 6) * 84) / 256, 84)); +const fn = (x, y) => FN[(y & 255) * 64 + (x & 63)]; // Brick, gas and flame by temperature: dull red at 0.15, orange at 0.6, white // heat at 1. Red stops at 232: pinned at 255 it hid the flicker of the hottest @@ -50,35 +43,33 @@ for (let y = 0; y < 256; y++) const RAMP = [[0, 24, 4, 2], [0.15, 92, 13, 4], [0.3, 158, 30, 6], [0.45, 204, 68, 12], [0.6, 226, 114, 24], [0.75, 232, 160, 50], [0.88, 232, 194, 96], [1, 232, 216, 160], [1.25, 236, 230, 204]]; const LUT = new Float32Array(768); for (let i = 0; i < 256; i++) { - const T = i / 204; let k = 0; - while (k < RAMP.length - 2 && T > RAMP[k + 1][0]) k++; + const T = i / 204; + while (k < 7 && T > RAMP[k + 1][0]) k++; const u = cl((T - RAMP[k][0]) / (RAMP[k + 1][0] - RAMP[k][0]), 0, 1); for (let c = 0; c < 3; c++) LUT[i * 3 + c] = mix(RAMP[k][c + 1], RAMP[k + 1][c + 1], u); } const lut = (T) => cl(round(T * 204), 0, 255) * 3; +// One colour array for every put(), to spare the garbage collector. +const C3 = [0, 0, 0]; +const lc = (T, b = 1) => ((T = lut(T)), C3[0] = LUT[T] * b, C3[1] = LUT[T + 1] * b, C3[2] = LUT[T + 2] * b, C3); -// What each LED is, decided again whenever the number of chambers changes. -const SKY = 0, EARTH = 1, FILL = 2, WALL = 3, GAS = 4, POT = 5, SHELF = 6, HOLE = 7, FLUE = 8; -// A vault on low walls: the height of the roof across a chamber, 0.4 at the walls, 1 at the crown. +const SKY = 0, EARTH = 1, WALL = 3, GAS = 4, POT = 5, SHELF = 6, HOLE = 7, FLUE = 8; +// A vault on low walls: roof height across a chamber, 0.4 at the walls, 1 at the crown. const arch = (u) => 0.4 + 0.6 * sqrt(max(0, 1 - (2 * u - 1) ** 2)); -// The chimney's mouth row. -const TOP = 18; -const WOOD = [150, 118, 66], TOWEL = [214, 204, 184], FACE = [176, 116, 78], COAT = [46, 60, 128], LEGS = [74, 62, 54]; +const TOP = 18; // the chimney's mouth row +const WOOD = [150, 118, 66], TOWEL = [214, 204, 184], FACE = [176, 116, 78], COAT = [46, 60, 128], LEGS = [74, 62, 54], RIM = [255, 150, 60]; // Jar, flared bowl, bottle and cup: half width up the pot, bottom to top. const prof = (k, t) => (k === 0 ? (t > 0.84 ? 0.5 : 0.6 + 0.4 * sin(PI * t)) : k === 1 ? 0.5 + 0.5 * t : k === 2 ? (t < 0.55 ? 0.55 + 0.45 * sin((PI * t) / 0.55) : t > 0.88 ? 0.5 : 0.3) : 0.8); export function setup(params) { const F32 = (n) => new Float32Array(n); const S = (params.s = { - t: 0, n: 0, k1: 0, wind: 0.06, cf: 2, - kind: new Uint8Array(N), ch: new Uint8Array(N), V: F32(N), J: F32(N), bg: F32(N * 3), gw: F32(N), f: F32(N * 3), A: F32(W * 64), K: F32(W * 64), Q: F32(W * 64), + t: 0, n: 0, kind: new Uint8Array(N), ch: new Uint8Array(N), V: F32(N), bg: F32(N * 3), gw: F32(N), f: F32(N * 3), A: F32(W * 64), K: F32(W * 64), Q: F32(W * 64), T: F32(8).fill(-1), Hc: F32(8).fill(-1), age: F32(8).fill(9), q: F32(8), open: F32(8), Te: F32(8), Br: F32(8), - puffs: [], sparks: [], wood: [], logs: [], slugs: [], ev: [], stars: [], motes: [], - pot: { A: 0, tgt: 0, mode: 1, tm: -1.6, thr: 0, k: 0, ph: 0 }, fb: { tm: 1, an: 0 }, flare: 0, np: 0, ns: 0, hand: 0, + puffs: [], sparks: [], logs: [], slugs: [], ev: [], motes: [], pot: { A: 0, tgt: 0, mode: 1, tm: -1.6, thr: 0, k: 0, ph: 0 }, fb: { tm: 1, an: 0 }, flare: 0, np: 0, ns: 0, }); - for (let k = 0; k < 40; k++) S.motes.push({ x: rnd() * W, y: rnd() * H, e: rnd() < 0.45 ? 1 : 0, ph: rnd() * 7 }); - // Logs already burning in the firebox. + for (let k = 0; k < 40; k++) S.motes.push({ x: rnd() * W, y: rnd() * H, e: rnd() < 0.45, ph: rnd() * 7 }); for (let k = 0; k < 3; k++) S.logs.push({ c: 0, x: 8 + k * 6 + rnd() * 3, y: 120 + (k & 1), len: 5, life: 0.4 + 0.5 * rnd() }); } @@ -87,142 +78,97 @@ const front = (S) => ((S.k1 + 1) / 2) * (S.n + 0.3); function build(S, n) { S.n = n; const P = 77 / n, h = P - 3, Wc = 34 - 2 * n, s = (43 - Wc) / (n - 1); - // The firebox, then the chambers up the slope, each P rows above the last. + // The firebox, then the chambers, each P rows above the last and s LEDs to the right. const g = (S.g = [{ L: 6, W: 25, F: 123, h: 11 }]); for (let c = 1; c <= n; c++) g.push({ L: 14 + (c - 1) * s, W: Wc, F: 108 - (c - 1) * P, h }); - const inT = (S.inT = (k, X) => { - const q = g[k], u = (X - q.L) / q.W; - return u > 0 && u < 1 ? q.F - q.h * arch(u) : 1e3; + // The roof row over X inside a chamber; with e = 2, the top of its brick. + const inT = (S.inT = (k, X, e = 0) => { + const q = g[k], u = (X - q.L + e) / (q.W + 2 * e); + return u > 0 && u < 1 ? q.F - (q.h + e) * arch(u) : 1e3; }); - const outT = (k, X) => { - const q = g[k], u = (X - q.L + 2) / (q.W + 4); - return u > 0 && u < 1 ? q.F - (q.h + 2) * arch(u) : 1e3; - }; - const tp = g[n], CX = (S.CX = tp.L + 0.62 * tp.W); + const outT = (k, X) => inT(k, X, 2), tp = g[n], CX = (S.CX = tp.L + 0.62 * tp.W); const hill = (X) => (X < 4 ? 123 : X < tp.L - 2 ? mix(123, tp.F + 2, (X - 4) / (tp.L - 6)) : mix(tp.F + 2, tp.F - 5, (X - tp.L + 2) / (66 - tp.L))); - // Flame passages through the floor of each chamber, over the crown of the one below. + // Flame passages through each chamber's floor, over the crown of the one below. const holes = []; - for (let c = 1; c <= n; c++) { - const a = g[c - 1], b = g[c], xc = a.L + a.W / 2; - for (let x = round(xc - 0.3 * a.W); x <= xc + 0.3 * a.W; x += 4) if (x > b.L && x < b.L + b.W - 2) holes.push([x, c]); - } + for (let c = 1; c <= n; c++) for (let a = g[c - 1], x = round(a.L + 0.2 * a.W); x <= a.L + 0.8 * a.W; x += 4) if (x > g[c].L && x < g[c].L + Wc - 2) holes.push([x, c]); const { kind, ch, V, bg, gw } = S; S.stars = []; for (let i = 0; i < N; i++) { - const x = i & 63, y = i >> 6, X = x + 0.5, Y = y + 0.5, o = i * 3; - let k = SKY, c = 0, v = 0; - for (let j = 0; j <= n && !k; j++) if (Y < g[j].F && Y > inT(j, X)) (k = GAS), (c = j), (v = (g[j].F - Y) / g[j].h); - if (!k && abs(X - CX) < 2 && Y > TOP && Y < inT(n, X) + 0.5) (k = FLUE), (c = n), (v = (Y - TOP) / (inT(n, CX) - TOP)); - if (!k) for (const [hx, hc] of holes) if (x === hx && Y > g[hc].F && Y < inT(hc - 1, X)) (k = HOLE), (c = hc); - for (let j = 0; j <= n && !k; j++) if (Y < g[j].F + 2 && Y > outT(j, X)) (k = WALL), (c = j); - if (!k && Y > TOP && ((abs(X - CX) < 4 && Y < outT(n, X) + 1) || (Y < TOP + 2 && abs(X - CX) < 5))) (k = WALL), (c = n + 1); - for (let j = 1; j <= n && !k; j++) if (X > g[j].L - 2 && X < g[j].L + g[j].W + 2 && Y > g[j].F + 2) k = FILL; + const x = i & 63, y = i >> 6, X = x + 0.5, Y = y + 0.5, r = hash(x, y), nz = 0.8 + 0.4 * vn(x * 0.3, y * 0.3); + let k = SKY, c = 0, v = 0, col = [0, 0, 0]; + for (let j = 0; j <= n && !k; j++) if (Y < g[j].F && Y > inT(j, X)) k = GAS, c = j, v = (g[j].F - Y) / g[j].h; + if (!k && abs(X - CX) < 2 && Y > TOP && Y < inT(n, X) + 0.5) k = FLUE, c = n, v = (Y - TOP) / (inT(n, CX) - TOP); + if (!k) for (const [hx, hc] of holes) if (x === hx && Y > g[hc].F && Y < inT(hc - 1, X)) k = HOLE, c = hc; + for (let j = 0; j <= n && !k; j++) if (Y < g[j].F + 2 && Y > outT(j, X)) k = WALL, c = j; + if (!k && Y > TOP && ((abs(X - CX) < 4 && Y < outT(n, X) + 1) || (Y < TOP + 2 && abs(X - CX) < 5))) k = WALL, c = n + 1; if (!k && Y > hill(X)) k = EARTH; // The fire mouth in the firebox's front wall. - if (k === WALL && !c && x < 6 && y > 116) (k = GAS), (v = (123 - Y) / 11); - kind[i] = k; - ch[i] = c; - V[i] = v; - const r = hash(x, y), nz = 0.8 + 0.4 * vn(x * 0.3, y * 0.3); - let col; + if (k === WALL && !c && x < 6 && y > 116) k = GAS, v = (123 - Y) / 11; + kind[i] = k, ch[i] = c, V[i] = v; if (k === SKY) { - const u = sm(0, 112, Y), dm = Math.hypot(X - 11, Y - 11), cut = Math.hypot(X - 12.6, Y - 10); - col = [mix(5, 20, u), mix(9, 26, u), mix(32, 62, u)]; - if (dm < 3.6 && cut > 3.2) col = [226, 222, 196]; - else if (dm < 12) for (let j = 0; j < 3; j++) col[j] += [24, 26, 30][j] * exp(-dm / 3.5); - else if (r < 0.014 && y < 100) { - const b = 70 + 140 * hash(y, x); - col = col.map((v2, j) => v2 + b * [0.95, 0.95, 1][j]); - S.stars.push(i); - } + const u = sm(0, 112, Y), dm = hypot(X - 11, Y - 11), e = 26 * exp(-dm / 3.5), st = r < 0.014 && y < 100 && dm > 12 ? 70 + 140 * hash(y, x) : 0; + col = [5 + 15 * u + e + st, 9 + 17 * u + e + st, 32 + 30 * u + e + st]; + // A crescent moon. + if (dm < 3.6 && hypot(X - 12.6, Y - 10) > 3.2) col = [226, 222, 196]; + if (st) S.stars.push(i); } else if (k === EARTH) { - // Beds of clay and gravel along the slope, stones, and grass on top. - const dd = Y - hill(X), bd = 0.75 + 0.5 * vn(dd * 0.3 + x * 0.04, 3.5 + x * 0.02); - col = dd < 1.5 ? [26, 40, 26] : r < 0.035 ? [62, 56, 50] : [34 * bd * nz, 26 * bd * nz, 21 * bd * nz]; + // Beds along the slope, stones, grass on top. + const dd = Y - hill(X), bd = (0.7 + 0.5 * vn(dd * 0.3 + x * 0.04, 3.5 + x * 0.02)) * nz; + col = dd < 1.5 ? [24, 38, 24] : r < 0.035 ? [58, 52, 46] : [30 * bd, 23 * bd, 19 * bd]; } - else if (k === FILL) col = [52 * nz, 36 * nz, 28 * nz]; - else if (k === WALL) { - const m = c > n ? y % 3 === 0 || (x + (floor(y / 3) & 1) * 2) % 4 === 0 : r < 0.25; - col = [96, 66, 50].map((v2) => v2 * (m ? 0.62 : 0.85 + 0.3 * r)); - } else col = [0, 0, 0]; - bg[o] = col[0]; - bg[o + 1] = col[1]; - bg[o + 2] = col[2]; + // Brick, coursed on the chimney. + else if (k === WALL) col = [96, 66, 50].map((v2) => v2 * ((c > n ? y % 3 === 0 || (x + (floor(y / 3) & 1) * 2) % 4 === 0 : r < 0.25) ? 0.62 : 0.85 + 0.3 * r)); + bg.set(col, i * 3); } - // Inner face of each wall, the brick the fire touches. - for (let i = 0; i < N; i++) - if (kind[i] === WALL) { - const x = i & 63, y = i >> 6; - let hot = 0; - for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { - const X = x + dx, Y = y + dy; - if (X >= 0 && X < W && Y >= 0 && Y < H && kind[Y * W + X] >= GAS) hot = 1; - } - V[i] = hot; - } - // The earth and the air round the kiln are warm: how near each LED is to a wall, and which chamber's. gw.fill(0); - for (let i = 0; i < N; i++) - if (kind[i] <= FILL) { - const x = i & 63, y = i >> 6; + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, k = kind[i]; + // The brick the fire touches. + if (k === WALL) V[i] = [1, -1, W, -W].some((d) => kind[i + d] >= GAS && (abs(d) > 1 || (x + d) >> 6 === 0)) ? 1 : 0; + // Earth and air by the kiln are warm: how near each LED is to a wall, and whose. + if (k <= EARTH) { let best = 9; for (let dy = -4; dy <= 4; dy++) for (let dx = -4; dx <= 4; dx++) { const X = x + dx, Y = y + dy, d = abs(dx) + abs(dy); - if (X >= 0 && X < W && Y >= 0 && Y < H && kind[Y * W + X] === WALL && d < best) (best = d), (ch[i] = ch[Y * W + X]); + if (X >= 0 && X < W && Y >= 0 && Y < H && kind[Y * W + X] === WALL && d < best) best = d, ch[i] = ch[Y * W + X]; } - gw[i] = best < 9 ? exp(-best / 1.4) * (kind[i] ? 0.8 : 0.6) : 0; + gw[i] = best < 9 ? exp(-best / 1.4) * (k ? 0.8 : 0.6) : 0; } - // Flame jets over each passage in a chamber's floor, and the pull towards the way out at its crown. - const J = S.J.fill(0); - for (let i = 0; i < N; i++) { - if (kind[i] !== GAS || !ch[i]) continue; - const x = i & 63, c = ch[i], v = V[i], xo = c === n ? CX : g[c].L + g[c].W / 2; - let e = 0.5 * exp(-abs(x + 0.5 - xo) / 3) * sm(0.55, 1, v); - for (const [hx, hc] of holes) if (hc === c) e = max(e, exp(-abs(x - hx) / 1.3 - v * 2.2)); - J[i] = e; } // Shelves and pots: two or three levels a chamber, under the vault. + const nl = h >= 16 ? 3 : 2, lh = h / nl; for (let c = 1; c <= n; c++) { - const q = g[c], nl = q.h >= 16 ? 3 : 2, lh = q.h / nl, x0 = round(q.L + max(6, 0.22 * q.W)), x1 = floor(q.L + q.W - 1); + const q = g[c], x0 = round(q.L + max(6, 0.22 * Wc)), x1 = floor(q.L + Wc - 1); + const set = (x, y, k2, v2) => kind[y * W + x] === GAS && ch[y * W + x] === c && ((kind[y * W + x] = k2), (V[y * W + x] = v2)); for (let j = 0; j < nl; j++) { const ys = round(q.F - j * lh), base = ys - 1; - const set = (x, y, k2, v2) => { - const i = y * W + x; - if (x >= 0 && x < W && kind[i] === GAS && ch[i] === c) (kind[i] = k2), (V[i] = v2); - }; if (j) { for (let x = x0; x <= x1; x++) if (inT(c, x + 0.5) < ys - 1) set(x, ys, SHELF, 0); - for (let y = ys + 1; y < round(q.F - (j - 1) * lh) - (j > 1 ? 0 : 0); y++) set(x0, y, SHELF, 0), set(x1 - 1, y, SHELF, 0); + for (let y = ys + 1; y < round(q.F - (j - 1) * lh); y++) set(x0, y, SHELF, 0), set(x1 - 1, y, SHELF, 0); } for (let x = x0 + 1; x < x1 - 1; ) { const ty = floor(hash(c * 7 + j, x) * 4), w = [5, 5, 4, 3][ty]; - let ph = min(floor(lh - 2), [5, 3, 6, 4][ty]); - const room = floor(base - max(inT(c, x + 0.5), inT(c, x + w - 0.5)) - 1); - ph = min(ph, room); + const ph = min(floor(lh - 2), [5, 3, 6, 4][ty], floor(base - max(inT(c, x + 0.5), inT(c, x + w - 0.5)) - 1)); if (ph < 2 || x + w > x1) break; - for (let r = 0; r < ph; r++) { - const rad = (prof(ty, (r + 0.5) / ph) * w) / 2; + for (let r = 0; r < ph; r++) for (let xx = x; xx < x + w; xx++) { const dx = xx + 0.5 - x - w / 2; - if (abs(dx) <= rad + 0.15) set(xx, base - r, POT, 0.5 - dx / w + 0.2 * (r === ph - 1 ? 1 : 0)); + if (abs(dx) <= (prof(ty, (r + 0.5) / ph) * w) / 2 + 0.15) set(xx, base - r, POT, 0.5 - dx / w + (r === ph - 1 ? 0.2 : 0)); } - } x += w + 1; } } } - // Ladders up the front of each chamber, and the potter's way from one stoke hole to the next. - const xs = (c) => g[c].L - 4; - const px = [], py = []; + // Ladders up each chamber's front, and the potter's way between the stoke holes. + const xs = (c) => g[c].L - 4, px = [], py = [], at = [0], pl = [0]; S.lad = []; - S.Ac = [0]; S.spy = [0]; for (let c = 1; c <= n; c++) { const yw = outT(c, g[c].L - 1.5); - S.lad.push([round(xs(c)), yw, c === 1 ? outT(0, xs(1)) : outT(c - 1, xs(c))]); + S.lad.push([round(xs(c)), yw, outT(c - 1, xs(c))]); S.spy.push(round(g[c].F - max(5, 0.35 * h))); - S.Ac.push(px.length); + at.push(px.length); px.push(xs(c)), py.push(g[c].F + 2); if (c < n) { px.push(xs(c)), py.push(yw); @@ -230,175 +176,141 @@ function build(S, n) { px.push(xs(c + 1)), py.push(outT(c, xs(c + 1))); } } - const pl = [0]; - for (let j = 1; j < px.length; j++) pl.push(pl[j - 1] + Math.hypot(px[j] - px[j - 1], py[j] - py[j - 1])); - Object.assign(S, { px, py, pl }); - S.Ac = S.Ac.map((j, c) => (c ? pl[j] : 0)); - // Where the potter stands now: by the chamber above the one being fired, the first time. + for (let j = 1; j < px.length; j++) pl.push(pl[j - 1] + hypot(px[j] - px[j - 1], py[j] - py[j - 1])); + Object.assign(S, { px, py, pl, Ac: at.map((j, c) => (c ? pl[j] : 0)) }); + // The first time, the potter stands by the chamber above the one being fired. const pt = S.pot; - if (!S.built) (S.built = 1), (pt.tgt = min(n, cl(round(front(S)), 1, n) + 1)); + if (!S.built) S.built = 1, pt.tgt = min(n, cl(round(front(S)), 1, n) + 1); pt.tgt = min(pt.tgt, n); pt.A = S.Ac[pt.tgt]; - if (pt.mode === 0) pt.mode = 2; + if (!pt.mode) pt.mode = 2; S.logs = S.logs.filter((l) => l.c <= n); - S.wood = []; S.slugs = []; } -// A stoke as the kiln feels it: reduction first, the flame starved and smoky, then the surge. +// A stoke: reduction first, the flame starved and smoky, then the surge. const red = (a) => (a < 0.2 ? a / 0.2 : exp(-(a - 0.2) / 1.1)); const surge = (a) => sm(0.9, 2.1, a) * exp(-max(0, a - 2.1) / 2.4); function pulse(S, c, s) { - S.age[c] = 0; - S.q[c] = s; + S.age[c] = 0, S.q[c] = s; + // Its black smoke reaches the chimney later the lower the chamber. S.ev.push({ at: 0.25 + 0.12 * (S.n - c), fn: () => S.slugs.push({ y: S.inT(S.n, S.CX), a: 0.5 + 0.5 * s }) }); } function stoke(S, c) { const q = S.g[c]; pulse(S, c, 1); - if (c) S.logs.push({ c, x: q.L + 0.5 + rnd() * 2, y: round(q.F) - 1, len: 3 + floor(rnd() * 3), life: 1 }); - else { - S.logs.push({ c, x: q.L + 2 + rnd() * 9, y: 120 + floor(rnd() * 2), len: 4 + floor(rnd() * 3), life: 1 }); - // The firebox's flame goes on through the first chamber. - S.ev.push({ at: 0.35, fn: () => pulse(S, 1, 0.6) }); - } + S.logs.push({ c, x: q.L + (c ? 0.5 + rnd() * 2 : 2 + rnd() * 9), y: c ? round(q.F) - 1 : 120 + (rnd() < 0.5), len: (c ? 3 : 4) + floor(rnd() * 3), life: 1 }); + // The firebox's flame goes on through the first chamber. + if (!c) S.ev.push({ at: 0.35, fn: () => pulse(S, 1, 0.6) }); } -function throwTo(S, c, x0, y0, delay = 0) { - const q = S.g[c]; - S.wood.push({ x0, y0, x1: q.L - 0.5, y1: round(q.F) - 2.5, a: -delay, c }); +function throwTo(S, c, delay = 0) { + S.ev.push({ at: 0.3 + delay, fn: () => stoke(S, c) }); S.open[c] = 1.3 + delay; } -function puff(S, x, y, vy, r, a, k, hot) { +function puff(S, x, y, vy, r, a, k) { if (S.puffs.length > 110) S.puffs.shift(); - S.puffs.push({ x, y, vy, r, a, k, hot, ph: rnd() * 7 }); + S.puffs.push({ x, y, vy, r, a, k, hot: 0.7 + 0.3 * k, ph: rnd() * 7 }); } -function spark(S, x, y, s) { +function spark(S, s) { if (S.sparks.length > 160) S.sparks.shift(); const life = 0.8 + 1.4 * rnd(); - S.sparks.push({ x, y, vx: (rnd() - 0.5) * 6 * s, vy: -(10 + 14 * rnd()) * s, life, L: life }); -} - -// Black smoke reaching the top of the chimney: a cloud, a flare and a fountain of sparks. -function belch(S, a) { - for (let j = 0; j < 3; j++) puff(S, S.CX + (rnd() - 0.5) * 3, TOP - 1 - j * 2, -11 - 6 * rnd(), 2.6 + rnd(), 0.6 + 0.6 * a, 1, 1); - for (let j = 0; j < 14 * a; j++) spark(S, S.CX + (rnd() - 0.5) * 3, TOP - 1, 1.3); - S.flare = min(1.4, S.flare + a); -} - -function stokeAll(S) { - for (let c = 1; c <= S.n; c++) throwTo(S, c, S.g[c].L - 7, S.g[c].F - 6, 0.05 * c); - throwTo(S, 0, 1, 115); - S.hand = 0.6; + S.sparks.push({ x: S.CX + (rnd() - 0.5) * 3, y: TOP - 1, vx: (rnd() - 0.5) * 6 * s, vy: -(10 + 14 * rnd()) * s, life, L: life }); } -const posAt = (S, A) => { +function posAt(S, A) { const { px, py, pl } = S; let j = 1; while (j < pl.length - 1 && pl[j] < A) j++; const u = cl((A - pl[j - 1]) / max(1e-6, pl[j] - pl[j - 1]), 0, 1); return [mix(px[j - 1], px[j], u), mix(py[j - 1], py[j], u), abs(px[j] - px[j - 1]) < 0.01]; -}; +} function step(S, d) { S.t += d; - const { n, T, age, q, Hc, Te, Br, wind } = S, fr = front(S), peak = 0.5 + 0.45 * sm(0, 0.5, (S.k1 + 1) / 2); + const { n, T, age, q, Hc, Te, Br, wind, pot: pt, fb } = S, fr = front(S), peak = 0.5 + 0.45 * sm(0, 0.5, (S.k1 + 1) / 2); S.cf = cl(round(fr), 1, n); for (let c = 0; c <= n; c++) { + // Fired chambers hold white heat; above the one being fired each is cooler. const tg = max(0.2, peak - (1.1 / n) * max(0, c - fr)); T[c] = T[c] < 0 ? tg : T[c] + (tg - T[c]) * min(1, d * 0.8); age[c] += d; - // Wind up the hill pulls on the chimney and the fire burns hotter; wind down it chokes the draft. + // Wind up the hill pulls on the chimney; wind down it chokes the draft. Te[c] = T[c] + q[c] * (0.1 * surge(age[c]) - 0.15 * red(age[c])) + 0.02 * sin(S.t * 1.3 + c * 2.1) + 0.06 * wind; Br[c] = 1 - 0.36 * q[c] * red(age[c]); + // Heat work for the cones creeps up and hardly comes back. if (Hc[c] < 0) Hc[c] = T[c] - 0.1; - // Heat work for the cones: it creeps up with the temperature and hardly comes back. Hc[c] += (Te[c] - Hc[c]) * d * (Te[c] > Hc[c] ? 0.07 : 0.02); S.open[c] = max(0, S.open[c] - d); } - Te[n + 1] = Te[n] - 0.22; - Br[n + 1] = Br[n]; + Te[n + 1] = Te[n] - 0.22, Br[n + 1] = Br[n]; for (const e of S.ev) if ((e.at -= d) <= 0) e.fn(); S.ev = S.ev.filter((e) => e.at > 0); - - // The potter: stoke twice, watch the spy hole, then go where the fire needs wood. - const pt = S.pot; + // The potter stokes twice, watches the spy hole, then goes where wood is needed. pt.ph += d; - if (pt.mode === 0) { - const goal = S.Ac[pt.tgt]; - pt.A += cl(goal - pt.A, -11 * d, 11 * d); - if (abs(goal - pt.A) < 1e-3) (pt.mode = 1), (pt.tm = 0), (pt.thr = 0); + if (!pt.mode) { + pt.A += cl(S.Ac[pt.tgt] - pt.A, -11 * d, 11 * d); + if (abs(S.Ac[pt.tgt] - pt.A) < 1e-3) pt.mode = 1, pt.tm = 0, pt.thr = 0; } else if (pt.mode === 1) { pt.tm += d; if (pt.thr < 2 && pt.tm > 0.3 + 0.7 * pt.thr) { - const [x, y] = posAt(S, pt.A); - throwTo(S, pt.tgt, x - 1, y - 8); + throwTo(S, pt.tgt); pt.thr++; } - if (pt.tm > 1.8) (pt.mode = 2), (pt.tm = 0.8 + rnd()); - } else if ((pt.tm -= d) <= 0) { - pt.k++; - pt.tgt = pt.k % 3 === 0 ? min(n, S.cf + 1) : S.cf; - pt.mode = 0; - } - // The firebox stoker feeds the mouth. - const fb = S.fb; + if (pt.tm > 1.8) pt.mode = 2, pt.tm = 0.8 + rnd(); + } else if ((pt.tm -= d) <= 0) pt.tgt = ++pt.k % 3 ? S.cf : min(n, S.cf + 1), pt.mode = 0; + // The stoker feeds the fire mouth. fb.an = max(0, fb.an - d); - if ((fb.tm -= d) <= 0) (fb.tm = 3 + rnd()), (fb.an = 0.5), throwTo(S, 0, 2.5, 116); - S.hand = max(0, S.hand - d); - for (const w of S.wood) if ((w.a += d / 0.3) >= 1) (w.done = 1), stoke(S, w.c); - S.wood = S.wood.filter((w) => !w.done); + if ((fb.tm -= d) <= 0) fb.tm = 3 + rnd(), fb.an = 0.5, throwTo(S, 0); for (const l of S.logs) l.life -= d * (0.12 + 0.2 * T[l.c]); S.logs = S.logs.filter((l) => l.life > 0); - // Black smoke climbing the chimney. - for (const s of S.slugs) if ((s.y -= d * 30) < TOP) (s.done = 1), belch(S, s.a); + // Black smoke up the chimney: at the top, a billow, a flare and sparks. + for (const s of S.slugs) + if ((s.y -= d * 30) < TOP) { + s.done = 1; + for (let j = 0; j < 3; j++) puff(S, S.CX + (rnd() - 0.5) * 3, TOP - 1 - j * 2, -6 - 4 * rnd(), 3 + rnd(), 0.6 + 0.6 * s.a, 1); + for (let j = 0; j < 14 * s.a; j++) spark(S, 1.3); + S.flare = min(1.4, S.flare + s.a); + } S.slugs = S.slugs.filter((s) => !s.done); S.flare = max(0.15 + 0.35 * sm(0.6, 1, T[n]), S.flare * exp(-d * 1.6)); air(S, d); - // Ash falls and embers float, across the whole hillside. - for (const m of S.motes) { - m.x += (wind * 9 + 1 + 1.5 * sin(S.t * 0.7 + m.ph)) * d * (m.e ? 1.3 : 1); - m.y += ((m.e ? -3.5 : 2.2) + 0.8 * sin(S.t * 1.3 + m.ph * 2)) * d; - m.x = ((m.x % 65) + 65) % 65; - m.y = ((m.y % 129) + 129) % 129; - } + // Ash and embers. + for (const m of S.motes) m.x = (m.x + (wind * 9 + 1 + 1.5 * sin(S.t * 0.7 + m.ph)) * d + 65) % 65, m.y = (m.y + (m.e ? -3.5 : 2.2) * d + 129) % 129; } -// Smoke: a steady grey plume, slowing and spreading under the top of the panel, and sparks. +// A grey plume that slows and spreads under the top of the panel, and sparks. function air(S, d) { const { n, T, wind } = S; - for (S.np += d * 6; S.np >= 1; S.np--) puff(S, S.CX + (rnd() - 0.5) * 2, TOP - 1, -7 - 3 * rnd(), 3, 0.55 + 0.2 * T[n], 0, 0.7); + for (S.np += d * 6; S.np >= 1; S.np--) puff(S, S.CX + (rnd() - 0.5) * 2, TOP - 1, -7 - 3 * rnd(), 3, 0.55 + 0.2 * T[n], 0); for (const p of S.puffs) { // A strong wind bends the plume over and draws it out downwind. p.vy += (-3.5 * (1 - 0.55 * abs(wind)) * cl(p.y / 12, 0.3, 1) - p.vy) * d * 1.2; p.y += p.vy * d; p.x += (wind * 18 * (0.5 + 0.5 * cl((TOP - p.y) / TOP, 0, 1)) + 1.2 * sin(S.t * 0.9 + p.ph)) * d; - p.r = min(10, p.r + d * (1.5 + 1 * p.k + 2 * cl((8 - p.y) / 8, 0, 1))); + p.r = min(10 + p.k, p.r + d * (1.5 + 1.2 * p.k + 2 * cl((8 - p.y) / 8, 0, 1))); p.a -= d * (0.05 + 0.04 * (1 - p.k)); p.hot = max(0, p.hot - d * (1.4 - 0.9 * p.k)); } S.puffs = S.puffs.filter((p) => p.a > 0.03 && p.y + p.r > -3 && p.x > -14 && p.x < W + 14); - for (S.ns += d * (2 + 5 * T[n]) * (1 + 0.6 * wind); S.ns >= 1; S.ns--) spark(S, S.CX + (rnd() - 0.5) * 3, TOP - 1, 1); + for (S.ns += d * (2 + 5 * T[n]) * (1 + 0.6 * wind); S.ns >= 1; S.ns--) spark(S, 1); for (const p of S.sparks) { p.vy += (-6 - p.vy) * d * 1.5; p.vx += ((rnd() - 0.5) * 40 + (wind * 16 - p.vx) * 2) * d; - p.x += p.vx * d; - p.y += p.vy * d; - p.life -= d; + p.x += p.vx * d, p.y += p.vy * d, p.life -= d; } S.sparks = S.sparks.filter((p) => p.life > 0 && p.y > -1); } export function update(dt, input, params) { if (!params.s) setup(params); - const S = params.s, k = input.knobValues || [0, 2, 3, 0.06]; + const S = params.s, k = input.knobValues || [0, 2, 3, 0.06], n = cl(round(k[2]), 3, 6); S.k1 = cl(k[0], -1, 1); S.wind = cl(k[3], -1, 1); - const n = cl(round(k[2]), 3, 6); if (n !== S.n) build(S, n); // The kiln has been smoking for hours before you look. if (!S.warm) { @@ -406,197 +318,123 @@ export function update(dt, input, params) { step(S, 0); for (let j = 0; j < 75; j++) air(S, 1 / 30); } - if (input.btnPressed && input.btnPressed[0]) stokeAll(S); + if (input.btnPressed && input.btnPressed[0]) for (let c = 0; c <= n; c++) throwTo(S, c, 0.05 * c); step(S, cl(dt || 0, 0, 0.1) * (cl(k[1], 0.2, 3) / 2)); } let F; -function put(x, y, c, a = 1) { - (x = floor(x)), (y = floor(y)); +function put(x, y, c, a = 1, add = 0) { + x = floor(x), y = floor(y); if (x < 0 || x >= W || y < 0 || y >= H) return; - const o = (y * W + x) * 3; - for (let i = 0; i < 3; i++) F[o + i] = mix(F[o + i], c[i], a); -} -function add(x, y, c, a) { - (x = floor(x)), (y = floor(y)); - if (x < 0 || x >= W || y < 0 || y >= H) return; - const o = (y * W + x) * 3; - for (let i = 0; i < 3; i++) F[o + i] += c[i] * a; + for (let i = 0, o = (y * W + x) * 3; i < 3; i++) F[o + i] = add ? F[o + i] + c[i] * a : mix(F[o + i], c[i], a); } function glow(x, y, r, c, a) { - for (let Y = floor(y - r); Y <= y + r; Y++) for (let X = floor(x - r); X <= x + r; X++) add(X, Y, c, a * max(0, 1 - Math.hypot(X + 0.5 - x, Y + 0.5 - y) / r) ** 2); + for (let Y = floor(y - r); Y <= y + r; Y++) for (let X = floor(x - r); X <= x + r; X++) put(X, Y, c, a * max(0, 1 - hypot(X + 0.5 - x, Y + 0.5 - y) / r) ** 2, 1); } -// Colours handed to put() come from a ring of four arrays, to spare the garbage collector. -const RING = [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]; -let ri = 0; -const lc = (T, b = 1) => { - const r = RING[(ri = (ri + 1) & 3)]; - T = lut(T); - (r[0] = LUT[T] * b), (r[1] = LUT[T + 1] * b), (r[2] = LUT[T + 2] * b); - return r; -}; -// A figure nine LEDs tall, feet on row y, facing the kiln on the right: a -// white towel, an indigo jacket lit orange on the kiln side, and a paper lantern. +// A figure nine LEDs tall, feet on row y, facing the kiln: white towel, indigo +// jacket lit orange on the kiln side, paper lantern. function figure(x, y, pose, ph, rim, lamp) { - (x = round(x)), (y = round(y)); - const st = floor(ph * 6) & 1, rc = [255, 150, 60]; - const pts = [[-1, -8, TOWEL], [0, -8, TOWEL], [0, -7, FACE, 1]]; + x = round(x), y = round(y); + const a = floor(ph * 6) & 1 ? -1 : 1, pts = [[-1, -8, TOWEL], [0, -8, TOWEL], [0, -7, FACE, 1]]; for (let dy = -6; dy <= -4; dy++) pts.push([-1, dy, COAT], [0, dy, COAT], [1, dy, COAT, 1]); - const L = (a, b2) => pts.push([a, -3, LEGS], [b2, -2, LEGS], [b2, -1, LEGS], [b2, 0, LEGS]); - if (pose === 1 && st) L(0, 0); - else if (pose === 2) L(st ? -1 : 1, st ? -1 : 1), L(st ? 1 : -1, st ? 1 : -1), pts.push([st ? -1 : 1, -7, COAT, 1], [st ? 1 : -1, -3, LEGS]); + const L = (l, b) => pts.push([l, -3, LEGS], [b, -2, LEGS], [b, -1, LEGS], [b, 0, LEGS]); + if (pose === 1 && a < 0) L(0, 0); + else if (pose === 2) L(a, a), L(-a, -a), pts.push([a, -7, COAT, 1], [-a, -3, LEGS]); else L(-1, -1), L(1, 1); - // Throwing: the arm swings the wood from behind the head into the hole. if (pose === 3) pts.push(...(ph < 0.5 ? [[-1, -7, COAT], [-2, -8, COAT]] : [[2, -5, COAT, 1], [3, -5, FACE, 1]])); - for (const [dx, dy, c, k] of pts) put(x + dx, y + dy, k ? c.map((v, i) => mix(v, rc[i], rim)) : c); - if (lamp) { - put(x - 2, y - 4, [120, 70, 40]); - put(x - 2, y - 3, [255, 222, 150]), put(x - 2, y - 2, [240, 160, 80]); - glow(x - 1.5, y - 2, 7, [70, 44, 16], 1); - } + for (const [dx, dy, c, k] of pts) put(x + dx, y + dy, k ? c.map((v, i) => mix(v, RIM[i], rim)) : c); + if (lamp) put(x - 2, y - 4, [120, 70, 40]), put(x - 2, y - 3, [255, 222, 150]), put(x - 2, y - 2, [240, 160, 80]), glow(x - 1.5, y - 2, 7, [70, 44, 16], 1); } export function draw(display, params) { const S = params.s; if (!S || !S.n) return; F = S.f; - const { n, t, kind, ch, V, J, bg, gw, Te, Br, g, wind, A, K, Q } = S, lean = wind * 2.2 + 0.8, fs = 1 + 0.45 * wind, fo = floor(t * fs * 17); + const { n, t, kind, ch, V, bg, gw, Te, Br, g, wind, A, K, Q, q, age } = S, lean = wind * 2.2 + 0.8, fs = 1 + 0.45 * wind, fo = floor(t * fs * 17); for (let i = 0, o = 0; i < N; i++, o += 3) { - const k = kind[i], c = ch[i]; - if (k <= FILL) { - (F[o] = bg[o]), (F[o + 1] = bg[o + 1]), (F[o + 2] = bg[o + 2]); - if (gw[i] > 0.02) { - const j = lut(Te[c] * 0.7), w = gw[i] * 0.28 * Br[c]; - (F[o] += LUT[j] * w), (F[o + 1] += LUT[j + 1] * w), (F[o + 2] += LUT[j + 2] * w); - } - continue; - } - const x = i & 63, y = i >> 6, X = x + 0.5, Y = y + 0.5, T = Te[c], B = Br[c]; - let j = 0, b = 1; - if (k === WALL) { - const hot = V[i]; - j = lut(T * 0.85 - 0.05); - b = B * (hot ? 0.5 : 0.1); - (F[o] = bg[o] * (hot ? 0.45 : 0.95) + LUT[j] * b), (F[o + 1] = bg[o + 1] * (hot ? 0.45 : 0.95) + LUT[j + 1] * b), (F[o + 2] = bg[o + 2] * (hot ? 0.45 : 0.95) + LUT[j + 2] * b); - continue; - } - if (k === GAS) { - const v = V[i]; - const n1 = vn(X * 0.45 - lean * (0.3 + v) * 3 + c * 19, (Y + t * fs * 10) * 0.16), n2 = FN[((y + fo) & 255) * 64 + x]; - let I = sm(0.25, 0.85, 0.65 * n1 + 0.35 * n2 + 0.22 * (1 - v) - 0.12 + 0.45 * J[i]); + const k = kind[i], c = ch[i], x = i & 63, y = i >> 6, X = x + 0.5, Y = y + 0.5, T = Te[c], B = Br[c], v = V[i]; + // Each LED: backdrop times m plus a ramp colour times b. + let j = lut(T * 0.7), b = 0, m = 0; + if (k <= EARTH) m = 1, b = gw[i] * 0.28 * B; + else if (k === WALL) m = v ? 0.45 : 0.95, j = lut(T * 0.85 - 0.05), b = B * (v ? 0.5 : 0.1); + else if (k === GAS) { + let I = sm(0.25, 0.85, 0.65 * vn(X * 0.45 - lean * (0.3 + v) * 3 + c * 19, (Y + t * fs * 10) * 0.16) + 0.35 * fn(x, y + fo) + 0.25 * (1 - v) - 0.07); // The firebox floor: a bed of embers under the logs. if (!c && v < 0.3) I = sm(0.35, 0.85, 0.7 * vn(X * 0.7, Y * 0.9 + t * 0.4) + 0.3 * vn(X * 1.7 + t, Y * 1.5) + 0.1); - // Darker flame is cooler, so it goes orange rather than grey. The firebox burns rougher. + // Darker flame is cooler, orange rather than grey. The firebox burns rougher. const r = c ? 0.14 : 0.26; - j = lut(T - r + 2 * r * I); - b = B * (1 - 2 * r + 2 * r * I); - } else if (k === POT) (j = lut(T - 0.18)), (b = B * (0.32 + 0.22 * V[i] + 0.14 * FN[((y + (fo >> 1)) & 255) * 64 + (x >> 1)])); - else if (k === SHELF) (j = lut(T - 0.2)), (b = B * 0.4); - else if (k === HOLE) (j = lut(mix(Te[c - 1], T, 0.5) + 0.04)), (b = 0.85 + 0.15 * sin(t * 9 + x)); + j = lut(T - r + 2 * r * I), b = B * (1 - 2 * r + 2 * r * I); + } else if (k === POT) j = lut(T - 0.18), b = B * (0.32 + 0.22 * v + 0.14 * fn(x >> 1, y + (fo >> 1))); + else if (k === SHELF) j = lut(T - 0.2), b = B * 0.4; + else if (k === HOLE) j = lut(mix(Te[c - 1], T, 0.5) + 0.04), b = 0.85 + 0.15 * sin(t * 9 + x); else { - // The flue: hot gas rising in bands, and the black smoke of each stoke going up through it. + // The flue: hot gas rising in bands, and each stoke's black smoke going up. let dk = 0; for (const s of S.slugs) dk = max(dk, s.a * exp(-(((Y - s.y) / 3) ** 2))); - j = lut(T - 0.12 - 0.2 * V[i] + 0.1 * vn(X * 0.6, (Y + t * 22) * 0.3)); - b = B * (0.6 + 0.3 * vn(X * 0.5 + 9, (Y + t * 26) * 0.2)) * (1 - 0.85 * dk); + j = lut(T - 0.12 - 0.2 * v + 0.1 * vn(X * 0.6, (Y + t * 22) * 0.3)), b = B * (0.6 + 0.3 * vn(X * 0.5 + 9, (Y + t * 26) * 0.2)) * (1 - 0.85 * dk); } - (F[o] = LUT[j] * b), (F[o + 1] = LUT[j + 1] * b), (F[o + 2] = LUT[j + 2] * b); + for (let e = 0; e < 3; e++) F[o + e] = bg[o + e] * m + LUT[j + e] * b; } - // Stars twinkle. - for (const i of S.stars) if (kind[i] === SKY) for (let c = 0; c < 3; c++) F[i * 3 + c] *= 0.75 + 0.25 * sin(t * 2.3 + i); + for (const i of S.stars) for (let e = 0; e < 3; e++) F[i * 3 + e] *= 0.75 + 0.25 * sin(t * 2.3 + i); - // Burning wood on the chamber floors and in the firebox. - for (const l of S.logs) { - const T = Te[l.c], len = max(1, round(l.len * min(1, l.life * 1.6))); - for (let k = 0; k < len; k++) { + // Burning wood on the floors; in the firebox, thicker logs with their tops alight. + for (const l of S.logs) + for (let k = 0, T = Te[l.c], len = max(1, round(l.len * min(1, l.life * 1.6))); k < len; k++) { put(l.x + k, l.y, (k + floor(t * 4 + l.x)) % 3 ? lc(T - 0.45, 0.45) : lc(T + 0.1)); - // A log in the firebox is thicker, its top edge burning. if (!l.c) put(l.x + k, l.y - 1, lc(T - 0.1 + 0.15 * sin(t * 7 + k + l.x)), 0.85); } - } - // Holes in the front walls: the stoke hole, open while wood goes in, and the spy hole with its cones. + for (const [lx, y0, y1] of S.lad) for (let y = floor(y0); y <= y1; y++) put(lx - 1, y, WOOD, 0.7), put(lx + 1, y, WOOD, 0.7), y % 3 || put(lx, y, WOOD, 0.6); for (let c = 1; c <= n; c++) { - const q = g[c], T = Te[c], wx = floor(q.L - 1.5), sy = S.spy[c], fy = round(q.F), op = sm(0, 0.3, S.open[c]); + // The stoke hole, open while wood goes in, and the spy hole. + const T = Te[c], wx = floor(g[c].L - 1.5), sy = S.spy[c], fy = round(g[c].F), op = sm(0, 0.3, S.open[c]); for (let y = fy - 3; y <= fy - 2; y++) for (let x = wx; x <= wx + 1; x++) put(x, y, op > 0.05 ? lc(T + 0.05, op) : lc(T - 0.3, 0.35)); if (op > 0.05) glow(wx - 1, fy - 2.5, 6, lc(T - 0.1), 0.35 * op); put(wx, sy, lc(T, 0.9)), put(wx + 1, sy, lc(T)); - // Three cones on a pad: each bends over once the heat work passes its number. for (let x = wx + 2; x <= wx + 6; x++) put(x, sy + 2, lc(T - 0.2, 0.4)); + // Three cones on a pad, each bending once the heat work passes its number. for (let m = 0; m < 3; m++) { - const b2 = sm(0.62 + 0.12 * m - 0.05, 0.62 + 0.12 * m + 0.06, S.Hc[c]), cx = wx + 2 + 2 * m, cc = lc(T - 0.15, 0.5); - put(cx, sy + 1, cc); - if (b2 < 0.75) put(cx + (b2 > 0.4 ? 1 : 0), sy, cc); - put(cx + (b2 > 0.4 ? 1 : 0), b2 < 0.75 ? sy - 1 : sy + 1, cc); - if (b2 >= 0.75) put(cx + 1, sy, cc, 0.5); + const b = sm(0.57 + 0.12 * m, 0.68 + 0.12 * m, S.Hc[c]), cx = wx + 2 + 2 * m + (b > 0.4), cc = lc(T - 0.15, 0.5); + put(cx - (b > 0.4), sy + 1, cc), put(cx, sy, cc, b < 0.75 ? 1 : 0.5), put(cx, b < 0.75 ? sy - 1 : sy + 1, cc); } - } - // Ladders. - for (const [lx, y0, y1] of S.lad) - for (let y = floor(y0); y <= y1; y++) { - put(lx - 1, y, WOOD, 0.7), put(lx + 1, y, WOOD, 0.7); - if (!(y % 3)) put(lx, y, WOOD, 0.6); - } - // Tongues of flame out of the spy holes after a stoke, leaning with the draft. - for (let c = 1; c <= n; c++) { - const s = max(S.q[c] * red(S.age[c]) * sm(0.15, 0.35, S.age[c]), 0.55 * S.q[c - 1] * red(S.age[c - 1] - 0.3) * sm(0.4, 0.6, S.age[c - 1])) + 0.3 * max(0, -wind) * (0.7 + 0.3 * sin(t * 3 + c)); + // Flame out of the spy hole after a stoke, or whenever the wind chokes the chimney. + const s = max(q[c] * red(age[c]) * sm(0.15, 0.35, age[c]), 0.55 * q[c - 1] * red(age[c - 1] - 0.3) * sm(0.4, 0.6, age[c - 1])) + 0.3 * max(0, -wind) * (0.7 + 0.3 * sin(t * 3 + c)); if (s < 0.05) continue; - const x0 = floor(g[c].L - 1.5) - 0.5, y0 = S.spy[c] + 0.5, len = 2 + 9 * s * (0.8 + 0.2 * sin(t * 13 + c)), T = Te[c]; + const x0 = wx - 0.5, y0 = sy + 0.5, len = 2 + 9 * s * (0.8 + 0.2 * sin(t * 13 + c)); glow(x0, y0, 5, [120, 50, 10], s); for (let j = 0; j < len; j++) { - const u = j / len, x = x0 - j * (1 - 0.6 * max(0, wind)) + wind * j * 0.4, y = y0 - j * j * 0.06 - j * 0.15 + sin(t * 17 + j) * 0.4 * u, cc = lc(T + 0.3 - 0.6 * u); - put(x, y, cc, 1 - u * 0.3); + const u = j / len, x = x0 - j * (1 - 0.6 * max(0, wind)) + wind * j * 0.4, y = y0 - j * j * 0.06 - j * 0.15 + sin(t * 17 + j) * 0.4 * u; + put(x, y, lc(T + 0.3 - 0.6 * u), 1 - u * 0.3); if (u < 0.7) put(x, y - 1, lc(T + 0.1 - 0.6 * u), 0.8 - u), put(x, y + 1, lc(T - 0.6 * u), 0.5 - 0.5 * u); } } - // The flying wood. - for (const w of S.wood) { - if (w.a < 0) continue; - const x = mix(w.x0, w.x1, w.a), y = mix(w.y0, w.y1, w.a) - 2 * sin(PI * w.a); - put(x, y, [196, 150, 96]), put(x - 1, y + (w.a < 0.5 ? 1 : 0), [130, 86, 50]); - } - // The potter and the firebox stoker. - const pt = S.pot, [px, py, climb] = posAt(S, pt.A), cc = S.cf; - const pose = S.hand > 0 || pt.mode === 1 ? 3 : pt.mode === 0 ? (climb ? 2 : 1) : 0; - const ph = pose === 3 ? (S.hand > 0 ? 1 - S.hand / 0.6 : (pt.tm % 0.7) / 0.7) : pt.ph; - figure(px, py, pose, ph, 0.25 + 0.6 * sm(0, 0.3, S.open[pt.tgt]) * (pt.mode === 1 ? 1 : 0), 1); - const fbo = S.fb.an > 0 ? 3 : 0; - figure(1, 122, fbo, 1 - S.fb.an / 0.5, 0.5 + 0.3 * sin(t * 7), 0); + const pt = S.pot, [px, py, climb] = posAt(S, pt.A), pose = pt.mode === 1 ? 3 : pt.mode ? 0 : climb ? 2 : 1; + figure(px, py, pose, pose === 3 ? (pt.tm % 0.7) / 0.7 : pt.ph, 0.25 + 0.6 * sm(0, 0.3, S.open[pt.tgt]) * (pose === 3), 1); + figure(1, 122, S.fb.an > 0 ? 3 : 0, 1 - S.fb.an / 0.5, 0.5 + 0.3 * sin(t * 7), 0); glow(5, 120, 6, lc(Te[0] - 0.1), 0.35); - void cc; - // Smoke: every puff summed over the top half, then shaded: grey lit by the fire, black after a stoke. + // Smoke: puffs summed, then shaded grey, lit from below, or black after a stoke. A.fill(0), K.fill(0), Q.fill(0); - for (const p of S.puffs) { - const r = p.r; - for (let y = max(0, floor(p.y - r)); y <= min(63, p.y + r); y++) - for (let x = max(0, floor(p.x - r)); x <= min(W - 1, p.x + r); x++) { - const dx = (x + 0.5 - p.x) / r, dy = (y + 0.5 - p.y) / r, w = 1 - dx * dx - dy * dy; - if (w > 0) { - const a = p.a * w * w, i = y * W + x; - (A[i] += a), (K[i] += a * p.k), (Q[i] += a * p.hot); - } + for (const p of S.puffs) + for (let y = max(0, floor(p.y - p.r)); y <= min(63, p.y + p.r); y++) + for (let x = max(0, floor(p.x - p.r)); x <= min(W - 1, p.x + p.r); x++) { + const dx = (x + 0.5 - p.x) / p.r, dy = (y + 0.5 - p.y) / p.r, w = p.a * (1 - dx * dx - dy * dy) ** 2, i = y * W + x; + if (dx * dx + dy * dy < 1) A[i] += w, K[i] += w * p.k, Q[i] += w * p.hot; } - } for (let i = 0; i < W * 64; i++) { if (A[i] < 0.01) continue; - const x = i & 63, y = i >> 6, o = i * 3, tx = 0.6 * vn(x * 0.25 - t * wind, y * 0.25 + t * 0.8) + 0.4 * FN[((y + (fo >> 2)) & 255) * 64 + x], al = min(0.95, (1 - exp(-A[i] * 2.2)) * (0.6 + 0.6 * tx)); - const dk = K[i] / A[i], hq = min(1, Q[i] / A[i]) * (0.4 + 0.6 * sm(30, 10, y)), gr = 0.7 + 0.5 * tx; - F[o] = mix(F[o], mix(118 * gr, 16, dk) + 160 * hq, al); - F[o + 1] = mix(F[o + 1], mix(106 * gr, 11, dk) + 60 * hq, al); - F[o + 2] = mix(F[o + 2], mix(104 * gr, 10, dk) + 14 * hq, al); + const y = i >> 6, tx = 0.6 * vn((i & 63) * 0.25 - t * wind, y * 0.25 + t * 0.8) + 0.4 * fn(i, y + (fo >> 2)), gr = 0.7 + 0.5 * tx; + const al = min(0.95, (1 - exp(-A[i] * 2.2)) * (0.6 + 0.6 * tx)), dk = K[i] / A[i], hq = min(1, Q[i] / A[i]) * (0.4 + 0.6 * sm(30, 10, y)); + const o = i * 3; + F[o] = mix(F[o], mix(118 * gr, 16, dk) + 160 * hq, al), F[o + 1] = mix(F[o + 1], mix(106 * gr, 11, dk) + 60 * hq, al), F[o + 2] = mix(F[o + 2], mix(104 * gr, 10, dk) + 14 * hq, al); } // Flame licking out of the chimney's mouth. - const fl = S.flare, CX = S.CX; - glow(CX, TOP, 6, [110, 40, 8], fl * 0.8); - for (let j = 0; j < 2 + 7 * fl; j++) { - const u = j / (2 + 7 * fl), x = CX + wind * j * 0.6 + sin(t * 11 + j) * 0.6 * u; - put(x, TOP - 1 - j, lc(Te[n] - 0.05 - 0.45 * u), 1 - 0.5 * u); - if (u < 0.5) put(x - 1, TOP - 1 - j, lc(Te[n] - 0.25 - 0.4 * u), 0.6), put(x + 1, TOP - 1 - j, lc(Te[n] - 0.25 - 0.4 * u), 0.6); - } - // Sparks, ash and embers. + const fh = 2 + 7 * S.flare; + glow(S.CX, TOP, 6, [110, 40, 8], S.flare * 0.8); + for (let j = 0, u = 0; j < fh; u = ++j / fh) + for (let e = -1; e < 2; e++) (u < 0.5 || !e) && put(S.CX + e + wind * j * 0.6 + sin(t * 11 + j) * 0.6 * u, TOP - 1 - j, lc(Te[n] - 0.05 - 0.45 * u - 0.2 * abs(e)), e ? 0.6 : 1 - 0.5 * u); for (const p of S.sparks) put(p.x, p.y, lc(0.35 + 0.75 * (p.life / p.L)), min(1, p.life * 2)); - for (const m of S.motes) m.e ? put(m.x, m.y, [230, 110, 30], 0.5 + 0.4 * sin(t * 5 + m.ph)) : put(m.x, m.y, [80, 78, 84], 0.6); + for (const m of S.motes) put(m.x, m.y, m.e ? [230, 110, 30] : [80, 78, 84], m.e ? 0.5 + 0.4 * sin(t * 5 + m.ph) : 0.6); for (let i = 0, o = 0; i < N; i++, o += 3) display.setPixel(i & 63, i >> 6, min(255, F[o]) | 0, min(255, F[o + 1]) | 0, min(255, F[o + 2]) | 0); } From 16a8a86ed8c859ada503f5b30ac84a81f50118f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:13:05 +0000 Subject: [PATCH 11/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Dark=20Field?= 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_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/patterns/dark-field.js | 492 ++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 incoming/pattern-study/patterns/dark-field.js diff --git a/incoming/pattern-study/patterns/dark-field.js b/incoming/pattern-study/patterns/dark-field.js new file mode 100644 index 000000000..967554bca --- /dev/null +++ b/incoming/pattern-study/patterns/dark-field.js @@ -0,0 +1,492 @@ +// ===== Patternflow pattern ===== +// Title: Dark Field +// Family: microbe +// Idea: A drop of pond water under a dark-field microscope, everything alive glowing on black by the light it scatters: Volvox roll with daughters inside, paramecia glide in a shimmer of cilia, diatoms slide and stop, Vorticella bells feed on long stalks that snap into coils, and what lies outside the focal plane blurs to a soft halo. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 26, after dark-field and Rheinberg illumination in the light microscope +// Round: 26 +// =============================== +// @matrix 64x128 +// @knobs Light=0..1, Speed=0.2..3, Crowd=0..1, Focus=-1..1 +// +// Knob 1: Light. At 0 classic dark field, white and gold on black. At 0.5 a Rheinberg filter: a deep blue field, each edge yellow where it faces sideways and red where it faces up or down. At 1 phase contrast: a grey field, the bodies dark inside bright halos. +// Knob 2: Speed. At 2 a paramecium crosses the panel in about six seconds; at 0.2 the drop runs ten times slower. +// Knob 3: Crowd. At 0 three Volvox, three paramecia, five diatoms, five Vorticella bells, a rotifer and a few hundred bacteria; at 1 more of each, fading in. +// Knob 4: Focus. Which depth is sharp: near 0 the algal thread with its Vorticella and rotifer, toward -1 the water under the coverslip, toward 1 the slide's glass, where diatoms glide among settled grit. +// Buttons: K1 tap knocks the slide: the view jolts, every Vorticella snaps its stalk into a coil and the rotifer pulls in its crown. On its own one bell snaps four seconds in, then one every ten seconds or so. + +const W = 64, H = 128, N = W * H, HW = 32, HH = 64; +const { min, max, floor, round, ceil, sin, cos, sqrt, exp, abs, PI, atan2 } = 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 rnd = () => Math.random(); +const rr = (a, b) => a + (b - a) * rnd(); +function h32(a, b) { + let h = Math.imul(a ^ Math.imul(b, 0x27d4eb2d), 0x9e3779b1); + h = Math.imul(h ^ (h >>> 15), 0x85ebca77); + h ^= h >>> 13; + return (h ^ (h >>> 16)) >>> 0; +} + +// Small things live on a torus a little larger than the view, so what leaves +// one side comes back at the other out of sight. Big swimmers turn back +// toward the middle near an edge. +const wrap = (o, m = 12) => ((o.x -= (W + 2 * m) * floor((o.x + m) / (W + 2 * m))), (o.y -= (H + 2 * m) * floor((o.y + m) / (H + 2 * m)))); +const go = (o, v) => ((o.x += cos(o.h) * v), (o.y += sin(o.h) * v)); +function swim(o, d) { + if ((o.z += o.vz * d) > 1 || o.z < -1) o.vz = -o.vz; + if (o.x > 8 && o.x < 56 && o.y > 12 && o.y < 116) return; + let e = atan2(64 - o.y, 32 - o.x) - o.h; + o.h += cl(e - TAU * round(e / TAU), -1, 1) * 1.2 * d; +} +// Colours each thing scatters in dark field. +const C = { + cell: [110, 255, 118], vrim: [175, 255, 165], dau: [64, 255, 92], par: [200, 222, 255], gran: [255, 236, 196], dia: [255, 194, 72], + vor: [222, 236, 255], rot: [255, 226, 186], eug: [132, 255, 140], amb: [255, 184, 100], gry: [212, 206, 198], wall: [205, 242, 182], + chl: [84, 236, 90], osc: [120, 250, 222], +}; +// Rheinberg: an annular filter yellow at its sides and red top and bottom, so +// an edge's colour follows the way it faces (q = nx² - ny²), on a blue field. +const RA = [255, 220, 46], RB = [255, 44, 40]; +const RM = RA.map((v, i) => (v + RB[i]) / 2), RH = RA.map((v, i) => (v - RB[i]) / 2); +const FIELD = [4, 16, 60], PHASE = [74, 82, 72]; + +// The algal thread, a Spirogyra filament across the lower third: its centre +// at arc length s and its unit tangent. +const FL = 136; +let fx = 0, fy = 0, ftx = 1, fty = 0; +function fil(s) { + const dy = -0.2 + 0.225 * cos(s * 0.045 + 0.3), l = sqrt(1 + dy * dy); + (fx = s - 30), (fy = 109 - 0.2 * s + 5 * sin(s * 0.045 + 0.3)), (ftx = 1 / l), (fty = dy / l); +} + +// Light goes into a sharp buffer and, by how far the thing lies from the +// focal plane, into a halo buffer at half resolution that is blurred and +// spread back over the LEDs. +let S, Q, ox = 0, oy = 0, foc = 0, m1 = 0, wS = 1, wQ = 0, lastP = -1; +const K0 = [0, 0, 0], K1 = [0, 0, 0]; +function col(c, g = 1) { + for (let i = 0; i < 3; i++) (K0[i] = (g * (c[i] + m1 * (RM[i] - c[i]))) / 255), (K1[i] = (g * m1 * RH[i]) / 255); +} +function dep(z) { + const b = max(0, abs(z - foc) - 0.15) * 2.4; + wS = max(0, 1 - b); + wQ = min(1, b) / (1 + 0.5 * max(0, b - 1)); +} +function put(x, y, s, q = 0) { + const xi = round(x - ox), yi = round(y - oy), o = (yi * W + xi) * 3; + if (xi < 0 || yi < 0 || xi >= W || yi >= H || o === lastP) return; + lastP = o; + const r = s * (K0[0] + q * K1[0]), g = s * (K0[1] + q * K1[1]), b = s * (K0[2] + q * K1[2]), h = ((yi >> 1) * HW + (xi >> 1)) * 3; + if (wS) (S[o] += r * wS), (S[o + 1] += g * wS), (S[o + 2] += b * wS); + if (wQ) (Q[h] += r * wQ), (Q[h + 1] += g * wQ), (Q[h + 2] += b * wQ); +} +// A point: small things lose themselves out of focus, so less goes to the halo. +function dot(x, y, s, q = 0) { + const k = wQ; + (lastP = -1), (wQ *= 0.4); + put(x, y, s, q); + wQ = k; +} +function line(x0, y0, x1, y1, s) { + const dx = x1 - x0, dy = y1 - y0, n = max(1, ceil(max(abs(dx), abs(dy)))), q = (dy * dy - dx * dx) / (dx * dx + dy * dy + 1e-6); + for (let k = 0; k < n; k++) put(x0 + (dx * k) / n, y0 + (dy * k) / n, s, q); +} +function ring(cx, cy, r, s) { + lastP = -1; + for (let k = 0, n = max(8, ceil(TAU * r * 1.4)); k < n; k++) { + const c = cos((k / n) * TAU), sn = sin((k / n) * TAU); + put(cx + r * c, cy + r * sn, s, c * c - sn * sn); + } +} +// A body's own frame: at(u, v) is u along its axis and v across it. +let FX = 0, FY = 0, FC = 1, FS = 0; +const frame = (x, y, a) => ((FX = x), (FY = y), (FC = cos(a)), (FS = sin(a))); +const at = (u, v, s, f = put, q = FS * FS - FC * FC) => f(FX + u * FC - v * FS, FY + u * FS + v * FC, s, q); +// A three-wide box blur run twice, on the half-resolution grid. +function blur(A, T) { + for (let pass = 0; pass < 4; pass++) { + const [a, b, s, n] = pass & 1 ? [T, A, HW * 3, HH] : [A, T, 3, HW]; + for (let y = 0, o = 0; y < HH; y++) + for (let x = 0; x < HW; x++) + for (let c = 0, i = pass & 1 ? y : x; c < 3; c++, o++) b[o] = (a[o - (i > 0 ? s : 0)] + a[o] + a[o + (i < n - 1 ? s : 0)]) / 3; + } +} +// Light to LED level: black below 0.1, a gain of 3, a soft shoulder above 0.8. +const LUT = Float32Array.from({ length: 1024 }, (_, i) => { + const v = max(0, i / 256 - 0.1) * 3; + return 255 * (v < 0.8 ? v : 1 - 0.2 * exp(-(v - 0.8) / 0.2)); +}); + +export function setup(params) { + const F = (n) => new Float32Array(n), side = () => (rnd() < 0.5 ? -1 : 1), A = (n, f) => Array.from({ length: n }, (_, i) => ({ x: rr(-12, 76), y: rr(-12, 140), h: rnd() * TAU, ...f(i) })); + const P = (params.s = { t: 0, fr: 0, S: F(N * 3), Q: F(HW * HH * 3), T: F(HW * HH * 3), QS: F(HW * HH), O: F(N * 3), L: F(N), V: F(N), OP: F(140), nx: 0, ny: 0, ex: 0, ey: 0, nt: 3.2, ja: 0, jt: 9, jx: 0, jy: 0, s1: rnd() * 7, s2: rnd() * 7 }); + // The round field stop of the eyepiece clips the panel's two ends. + for (let p = 0; p < N; p++) P.V[p] = sm(67.5, 62, sqrt(((p & 63) - 31.5) ** 2 + (((p >> 6) - 63.5) * 1.02) ** 2)); + // Volvox: cells on a sphere (a Fibonacci spiral) and daughter colonies inside. + P.vol = [[0.18, 12, 21], [-0.8, 9, 57], [0.2, 9.5, 112], [0.62, 11, 80]].map(([z, R, y]) => { + const n = round(0.7 * R * R), pts = F(n * 3), d = []; + for (let k = 0; k < n; k++) { + const yv = 1 - (2 * k + 1) / n, q = sqrt(1 - yv * yv); + pts.set([cos(k * 2.4) * q, yv, sin(k * 2.4) * q], k * 3); + } + for (let k = 0, m = 3 + rnd() * 3; k < m; k++) { + const a = rnd() * TAU, b = rr(-1, 1), r = rr(0.2, 0.52), q = sqrt(1 - b * b) * r; + d.push([cos(a) * q, b * r, sin(a) * q, rr(0.17, 0.25) * R]); + } + return { x: rr(16, 48), y: y + rr(-9, 9), z, vz: rr(-0.025, 0.025), R, h: rnd() * TAU, v: rr(1.6, 2.8), ph: 0, w: rr(0.4, 0.7) * side(), pts, d }; + }); + // Paramecia: slippers with an oral groove that swaps sides as they spin. + P.par = [[0.05, 42], [0.14, 115], [-0.12, 70], [0.75, 20], [-0.85, 92]].map(([z, y]) => ({ + x: rr(14, 50), y: y + rr(-10, 10), z, vz: rr(-0.05, 0.05), h: rnd() * TAU, v: rr(8, 11), a: rr(11.5, 13.5), B: rr(4.4, 5.2), spin: 0, ws: rr(1.6, 2.4), + rev: 0, tt: rr(3, 8), cy: 0, cv: [rnd(), rnd()], + })); + // Diatoms: two glide along the thread, two on floating debris, the rest on + // the slide's glass, deeper down. + P.dia = A(9, (i) => ({ z: i < 2 ? 0 : i < 4 ? rr(-0.05, 0.2) : rr(0.75, 0.92), a: rr(6, 9), B: rr(2.1, 2.6), go: rnd() < 0.6, tt: rr(0.5, 3), dir: side(), v: rr(3.5, 6), f: i < 2 ? 44 + 34 * i + rr(-6, 6) : -1 })); + // Oscillatoria: blue-green threads that glide along their length and wave both ends. + P.osc = [[18, 17, -0.04], [46, 40, 0.1], [30, 117, 0.3]].map(([x, y, z], i) => ({ x: x + rr(-9, 9), y: y + rr(-9, 9), z, h: rr(-0.6, 0.6) + (i & 1) * PI, L: 2 * round(rr(17, 24)), k: rr(-0.015, 0.015), ph: rnd() * 7, v: rr(1.2, 2.2) })); + // Vorticella bells on the thread: anchor, lean, stalk length. Bell 0 snaps first. + P.vor = [[46, -0.22, 46], [50, 0.16, 36], [41, -0.55, 30], [56, 0.45, 42], [36, -0.05, 52], [62, 0.1, 28], [30, -0.4, 40]].map(([s, a, L], i) => ({ s, a: a - PI / 2, L, ext: 1, st: 0, tt: i ? rr(9, 18) : 4.1, cp: 0 })); + P.rot = [[84, -2.05], [16, -1.15]].map(([s, a]) => ({ s, a, L: 21, ext: 1, st: 0, tt: rr(7, 12), cp: 0, sw: rnd() * 7 })); + P.fla = A(44, (i) => ({ z: rr(-0.9, 0.9), v: rr(6, 18), tt: 0, k: i % 3, ph: rnd() * 7 })); + // Detritus: grains and glittering flakes, amber plant matter or grey grit, + // a third of it settled on the slide. Bacteria: points crowding near the + // thread they feed on, most jiggling, some swimming. + P.det = A(48, (i) => { + const pts = [], m = 1 + floor(rnd() * rnd() * 9), e = m > 3 ? 2 : 1; + for (let k = 0; k < m; k++) pts.push([e * round(rr(-1.5, 1.5)), e * round(rr(-1.5, 1.5)), rr(0.35, 0.8)]); + return { z: i % 3 ? rr(-1, 1) * rnd() : rr(0.8, 1), pts, k: rnd() < 0.55 }; + }); + P.bac = A(580, () => ({ z: rr(-1, 1) * rnd(), s: rr(0.4, 0.8), m: rnd() < 0.3 })); +} + +// Knocking the slide. +function knock(P) { + const a = rnd() * TAU; + (P.ja = 3.5), (P.jt = 0), (P.jx = cos(a)), (P.jy = sin(a)); + for (const o of [...P.vor, ...P.rot]) if (o.st !== 1) o.st = 1; + for (const o of P.par) (o.rev = 0.6), (o.tr = rr(-3, 3)); + for (const o of [...P.vol, ...P.fla, ...P.det]) (o.x += P.jx * rr(1, 4)), (o.y += P.jy * rr(1, 4)); +} + +// A stalk or body that waits, snaps in to lo, holds, and stretches out again. +function cycle(o, d, down, lo, up, hold, wait) { + if (o.st === 0 && (o.tt -= d) < 0) o.st = 1; + else if (o.st === 1 && (o.ext -= d * down) <= lo) (o.ext = lo), (o.st = 2), (o.tt = hold * rr(1, 2)); + else if (o.st === 2 && (o.tt -= d) < 0) o.st = 3; + else if (o.st === 3 && (o.ext += d * up * (1.12 - o.ext)) >= 1) (o.ext = 1), (o.st = 0), (o.tt = wait * rr(1, 2.2)); +} + +function life(P, d, crowd, first) { + // Each kind fades its members in and out by the Crowd knob. + const each = (L, k, f) => + L.forEach((o, i) => { + const tg = i < k ? 1 : 0; + o.p = first ? tg : o.p + (tg - o.p) * min(1, d * 2.5); + f(o, i); + }); + const t = P.t; + each(P.vol, 3 + round(crowd), (o, i) => { + o.h += sin(t * 0.21 + i * 2) * 0.15 * d; + go(o, o.v * d); + o.ph += o.w * d; + swim(o, d); + }); + each(P.par, 3 + round(2 * crowd), (o, i) => { + o.spin += o.ws * d; + if (o.rev > 0) (o.rev -= d), (o.h += o.tr * d), go(o, -o.v * 0.5 * d); + else { + o.h += sin(t * 0.5 + i * 2.3) * 0.25 * d; + if ((o.tt -= d) < 0) (o.rev = 0.7), (o.tr = rr(-3, 3)), (o.tt = rr(4, 9)); + go(o, o.v * d); + } + o.cy += d * 0.7; + (o.cv[0] = (o.cv[0] + d * 0.24) % 1), (o.cv[1] = (o.cv[1] + d * 0.21) % 1); + swim(o, d); + }); + // Diatoms glide on their raphe, stop, and set off again, often backwards. + each(P.dia, 5 + round(4 * crowd), (o) => { + if ((o.tt -= d) < 0) (o.go = !o.go), (o.tt = o.go ? rr(2, 4) : rr(0.8, 2.2)), o.go && rnd() < 0.4 && (o.dir = -o.dir); + const v = o.go ? o.v * o.dir * d * sm(0, 0.4, o.tt) : 0; + if (o.f < 0) go(o, v), wrap(o); + else if ((o.f += v) < 8 || o.f > FL - 8) (o.dir = -o.dir), (o.f = cl(o.f, 8, FL - 8)); + }); + // Oscillatoria reverse when their front end runs out toward an edge. + each(P.osc, 2 + round(crowd), (o) => { + o.ph += d * 1.4; + go(o, o.v * d); + if ((o.x < 6 || o.x > 58 || o.y < 8 || o.y > 120) && cos(o.h) * (32 - o.x) + sin(o.h) * (64 - o.y) < 0) o.h += PI; + }); + each(P.vor, 5 + round(2 * crowd), (b) => ((b.cp += d * 7), cycle(b, d, 14, 0.13, 0.38, 0.5, 8))); + each(P.rot, 1 + round(crowd), (r) => ((r.cp += d * 9), cycle(r, d, 4, 0.5, 1.3, 1, 6))); + each(P.fla, 20 + round(24 * crowd), (o) => { + if ((o.tt -= d) < 0) (o.h += rr(-2, 2)), (o.v = rr(5, 20)), (o.tt = rr(0.25, 1.4)); + o.h += sin(o.ph + t * 3) * d * 1.2; + go(o, o.v * d); + wrap(o); + }); + // The slow current carries grit and bacteria up and to the right, fastest + // midway between the coverslip (z -1) and the slide (z 1). + const j = sqrt(d * 30); + for (const [L, k, jj] of [[P.det, 28 + round(20 * crowd), 0.06], [P.bac, 460 + round(120 * crowd), 0.5]]) + each(L, k, (o) => { + const f = 1 - o.z * o.z; + o.x += 1.7 * f * d + (rnd() - 0.5) * jj * j; + o.y -= f * d + (rnd() - 0.5) * jj * j; + if (o.m) rnd() < d * 0.8 && (o.h += rr(-2.5, 2.5)), go(o, 9 * d); + wrap(o); + }); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + const rd = cl(dt || 0, 0, 0.1), d = (rd * cl(k[1], 0.2, 3)) / 2, l = cl(k[0], 0, 1); + (P.m1 = cl(l * 2, 0, 1)), (P.m2 = cl(l * 2 - 1, 0, 1)), (P.foc = cl(k[3], -1, 1)); + if (input.btnPressed && input.btnPressed[0]) knock(P); + const t = (P.t += d); + // The stage is never quite still: a slow creep, a nudge now and then, and + // the shake of a knock. + if ((P.nt -= d) < 0) (P.nt = rr(6, 11)), (P.nx = cl(P.nx + rr(-6, 6), -5, 5)), (P.ny = cl(P.ny + rr(-8, 8), -7, 7)); + const e = min(1, d * 3), sh = P.ja * exp(-(P.jt += rd) * 5) * sin(P.jt * 40); + (P.ex += (P.nx - P.ex) * e), (P.ey += (P.ny - P.ey) * e); + P.ox = 2.5 * sin(t * 0.13 + P.s1) + 1.2 * sin(t * 0.31 + 2) + P.ex + sh * P.jx; + P.oy = 3.5 * sin(t * 0.09 + P.s2) + 1.5 * sin(t * 0.23) + P.ey + sh * P.jy; + life(P, d, cl(k[2], 0, 1), !P.init); + P.init = true; +} + +function thread() { + dep(0); + col(C.wall); + for (const sd of [-2.6, 2.6]) { + lastP = -1; + for (let s = 0; s <= FL; s += 0.7) fil(s), put(fx - fty * sd, fy + ftx * sd, 0.62, fty * fty - ftx * ftx); + } + for (let s = 5; s < FL; s += 9) fil(s), line(fx - fty * 1.6, fy + ftx * 1.6, fx + fty * 1.6, fy - ftx * 1.6, 0.3); + // The chloroplast, a green ribbon winding round inside the cell wall. + col(C.chl); + for (let s = 0; s <= FL; s += 1.3) fil(s), dot(fx - fty * 1.7 * sin(s * 0.5), fy + ftx * 1.7 * sin(s * 0.5), 0.5 + 0.25 * cos(s * 0.5)); +} + +// Frames a thing standing on the thread at arc length s, leaning by a. +function foot(s, a) { + fil(s); + frame(fx + fty * 2.6, fy - ftx * 2.6, a); +} + +function vorticella(b, i, t) { + foot(b.s, b.a + 0.07 * sin(t * 0.6 + i * 1.7)); + const L = b.L * b.ext, amp = 2 * (1 - b.ext) ** 0.6, bend = 3 * sin(t * 0.45 + i) * b.ext, m = ceil(L * 1.5) + 4; + dep(0); + col(C.par, b.p); + // The stalk, straight while it feeds and a tight coil when it has snapped. + let x = FX, y = FY; + for (let j = 1; j <= m; j++) { + const u = (j / m) * L, v = bend * sin((PI * j) / m) + amp * sin((j / m) * 6 * TAU), X = FX + u * FC - v * FS, Y = FY + u * FS + v * FC; + line(x, y, X, Y, 0.5); + (x = X), (y = Y); + } + // The bell, a cup that closes to a ball when the stalk snaps; the cilia + // whirl round its lip while it feeds. + frame(x, y, atan2(FS, FC)); + const open = sm(0.55, 0.95, b.ext), lb = 6 + 2 * open; + const hwOf = (u) => { + const ball = 3.1 * sqrt(max(0, 1 - ((2 * u) / lb - 1) ** 2)); + return ball + (0.8 + 4 * (u / lb) ** 0.9 - ball) * open; + }; + col(C.vor, b.p); + for (const sd of [-1, 1]) { + lastP = -1; + for (let u = 0; u <= lb; u += 0.5) at(u, hwOf(u) * sd, 0.75); + } + for (let k = -1; k < 2; k++) at(4.1 + k * 1.6, k * 1.2, 0.4, dot); + const hw = hwOf(lb); + if (open < 0.1) return; + lastP = -1; + for (let k = 0; k < 24; k++) at(lb + 1.3 * sin((k / 24) * TAU), hw * cos((k / 24) * TAU), 0.55 * open); + for (let k = 0; k < 9; k++) { + const th = (k / 9) * TAU + b.cp, s = sin(th); + at(lb + 2.3 + 0.9 * s, (hw + 0.9) * cos(th), (s > 0 ? 0.85 : 0.35) * open, dot); + } +} + +function rotifer(r, i, t) { + foot(r.s, r.a + 0.22 * sin(t * 0.4 + r.sw)); + const L = r.L * r.ext, g = r.p, cr = sm(0.82, 0.97, r.ext); + dep(0); + col(C.rot, g); + // A foot of three telescoping joints, a swollen trunk, a neck, the head, + // and inside it the jaws of the mastax, chewing. + const hwOf = (u) => (u < 0.36 ? 0.9 + 0.4 * floor(u / 0.12) : u < 0.8 ? 2.1 + 2.5 * sin((PI * (u - 0.36)) / 0.44) : u < 0.88 ? 2 : 2.9); + for (const sd of [-1, 1]) { + lastP = -1; + for (let u = 0; u <= L; u += 0.5) at(u, hwOf(u / L) * sd, 0.75); + at(-1.2, sd * 1.3, 0.7, dot); + at(0.72 * L, sd * (0.6 + 0.5 * sin(r.cp * 0.45)), 0.85, dot); + } + for (let k = 0; k < 7; k++) at(L * (0.42 + 0.05 * k), 1.8 * sin(k * 2.4 + t * 0.6), 0.38 + 0.2 * sin(k * 1.7 + t * 2), dot); + // The crown: two discs of cilia whirling the opposite way to each other. + lastP = -1; + if (cr < 0.02) for (let v = -2.8; v <= 2.8; v += 0.7) at(L, v, 0.6); + else + for (const sd of [-1, 1]) { + const cx = FX + FC * (L + 1.8) - FS * sd * 2.9, cy = FY + FS * (L + 1.8) + FC * sd * 2.9; + ring(cx, cy, 2.4 * cr, 0.3 * cr); + for (let k = 0; k < 10; k++) { + const th = (k / 10) * TAU + sd * r.cp; + dot(cx + cos(th) * 3.2 * cr, cy + sin(th) * 3.2 * cr, (0.55 + 0.45 * sin(th * 3 + t * 5)) * cr * g); + } + } +} + +function volvox(o) { + const { x, y, R, pts } = o, g = o.p, ax = cos(o.h), ay = sin(o.h), c = cos(o.ph), s = sin(o.ph), c1 = 1 - c; + // Rolling about its swimming axis, which lies in the view plane (Rodrigues). + const rot = (px, py, pz, k) => { + const kd = (ax * px + ay * py) * c1, u = px * c + ay * pz * s + ax * kd, v = py * c - ax * pz * s + ay * kd; + return k ? dot(x + R * u, y + R * v, pz * c + (ax * py - ay * px) * s > 0 ? 0.85 : 0.5, u * u - v * v) : frame(x + R * u, y + R * v, 0); + }; + dep(o.z); + col(C.cell, g); + for (let k = 0; k < pts.length; k += 3) rot(pts[k], pts[k + 1], pts[k + 2], 1); + col(C.vrim, g); + ring(x, y, R + 0.4, 0.55); + // The faint diffraction fringe outside the rim. + ring(x, y, R + 2.6, 0.3); + col(C.dau, g); + for (const [px, py, pz, r] of o.d) rot(px, py, pz, 0), ring(FX, FY, r, 0.95), dot(FX, FY, 0.7); +} + +function paramecium(o, i, t) { + const { a, B } = o, gs = cos(o.spin), n = ceil(TAU * sqrt((a * a + B * B) / 2) * 1.3); + frame(o.x, o.y, o.h); + dep(o.z); + col(C.par, o.p); + // Outline and cilia: a metachronal wave runs back along the body. + for (let pass = 0; pass < 2; pass++) { + lastP = -1; + for (let k = 0; k < n; k++) { + const th = (k / n) * TAU, ct = cos(th), st = sin(th), bw = B * (1 + 0.16 * ct - 0.08 * ct * ct), wv = sin(th * 9 - t * 10); + let nu = ct / a, nv = st / bw, nl = sqrt(nu * nu + nv * nv); + const nx = nu * FC - nv * FS, ny = nu * FS + nv * FC, q = (nx * nx - ny * ny) / (nl * nl); + (nu = (1.5 * nu) / nl), (nv = (1.5 * nv) / nl); + if (!pass) at(a * ct, bw * st, 0.62 + 0.25 * wv, put, q); + else if (wv > 0.2) at(a * ct + nu, bw * st + nv, 0.6 * wv, dot, q); + } + } + // The oral groove, a bright furrow from the front to the middle that swaps + // sides as the cell spins on its long axis. + lastP = -1; + for (let u = 0.05; u < 0.62; u += 0.06) at(u * a, gs * B * (0.3 + 0.75 * u) * (u < 0.5 ? 1 : 1 - (u - 0.5) * 6), 0.5 * abs(gs) + 0.12, put, 0); + // Granules and food vacuoles carried round by cyclosis, and the two + // contractile vacuoles filling and emptying. + col(C.gran, o.p); + for (let k = 0; k < 23; k++) { + const th = k * 2.4 + o.cy * (k % 3 ? 0.6 : 1), r = 0.25 + 0.6 * ((k * 0.618) % 1); + at(0.8 * a * r * cos(th), 0.62 * B * r * sin(th), k % 3 ? 0.36 + 0.16 * sin(t * 3 + k) : 0.6, dot, 0); + } + col(C.par, o.p); + o.cv.forEach((c, j) => c > 0.31 && c < 0.85 && ring(FX + (j ? -0.55 : 0.55) * a * FC, FY + (j ? -0.55 : 0.55) * a * FS, 1.6 * c, 0.55)); +} + +function diatom(o) { + const { a, B } = o; + if (o.f < 0) frame(o.x, o.y, o.h); + else fil(o.f), frame(fx + fty * (3.4 + B), fy - ftx * (3.4 + B), atan2(fty, ftx)); + dep(o.z); + col(C.dia, o.p); + // The rim glints at each stria; the raphe runs down the middle, broken at + // the central nodule. + for (const sd of [-1, 1]) { + lastP = -1; + for (let u = -a; u <= a; u += 0.6) at(u, sd * B * (1 - (u / a) ** 2) ** 0.75, 0.62 + 0.3 * cos(u * PI)); + } + for (let u = -0.8 * a; u <= 0.8 * a; u += 1.6) if (abs(u) > 1) at(u, 0, 0.6, dot); + at(0, 0, 0.8, dot); +} + +function oscillatoria(o, i, t, OP) { + const n = o.L, hl = n / 2; + // The centre line, from the middle out to each end, bending as the ends wave. + for (const sd of [-1, 1]) { + let x = o.x, y = o.y; + for (let s = 1; s <= hl; s++) { + const a = o.h + o.k * s * sd + 0.6 * sin(o.ph + sd) * (s / hl) ** 3; + (x += cos(a) * sd), (y += sin(a) * sd), (OP[(hl + s * sd) * 2] = x), (OP[(hl + s * sd) * 2 + 1] = y); + } + } + (OP[n] = o.x), (OP[n + 1] = o.y); + dep(o.z); + col(C.osc, o.p); + for (const e of [-1.2, 1.2]) { + lastP = -1; + for (let k = 0; k < n; k++) { + const x = OP[k * 2], y = OP[k * 2 + 1], dx = OP[k * 2 + 2] - x, dy = OP[k * 2 + 3] - y; + put(x - dy * e, y + dx * e, 0.6, dy * dy - dx * dx); + } + } + // Cross walls between the cells. + for (let k = 1; k < n; k += 2) dot(OP[k * 2], OP[k * 2 + 1], 0.4); +} + +export function draw(display, params) { + const P = params.s; + if (!P || !P.init) return; + (S = P.S), (Q = P.Q), (ox = P.ox), (oy = P.oy), (foc = P.foc), (m1 = P.m1); + S.fill(0); + Q.fill(0); + const t = P.t, show = (L, f) => L.forEach((o, i) => o.p > 0.02 && f(o, i, t, P.OP)); + thread(); + show(P.vor, vorticella); + show(P.rot, rotifer); + show(P.dia, diatom); + show(P.osc, oscillatoria); + show(P.vol, volvox); + show(P.par, paramecium); + show(P.fla, (o) => { + const c = cos(o.h), s = sin(o.h); + dep(o.z); + col(o.k ? C.par : C.eug, o.p); + dot(o.x, o.y, 0.9); + dot(o.x - c * 1.2, o.y - s * 1.2, 0.55); + for (let k = 1; k <= 3; k++) dot(o.x + c * (1 + k) - s * sin(t * 20 + k * 1.3) * 0.7, o.y + s * (1 + k) + c * sin(t * 20 + k * 1.3) * 0.7, 0.3); + }); + show(P.det, (o) => { + dep(o.z); + col(o.k ? C.amb : C.gry, o.p); + for (const [dx, dy, s] of o.pts) dot(o.x + dx, o.y + dy, s); + }); + col(C.par); + show(P.bac, (o) => (dep(o.z), dot(o.x, o.y, o.s * o.p))); + blur(Q, P.T); + // Light into LEDs, toned by the brightest channel so a green stays green, + // each lit LED scintillating a little, and under it all the shimmer of + // particles too small to resolve, below 24/255. + const fr = (P.fr = (P.fr + 1) | 0), m2 = P.m2, { V, L, O, QS } = P, c3 = K0; + for (let i = 0; i < HW * HH; i++) QS[i] = Q[i * 3] + Q[i * 3 + 1] + Q[i * 3 + 2]; + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + // The halo cell under the LED and its neighbours toward it, bilinear. + const o = p * 3, i = x >> 1, j = (y >> 1) * HW, i2 = cl(i + (x & 1) * 2 - 1, 0, HW - 1), j2 = cl((y >> 1) + (y & 1) * 2 - 1, 0, HH - 1) * HW; + const A = j + i, B = j + i2, c = j2 + i, e = j2 + i2, hl = QS[A] + QS[B] + QS[c] + QS[e] > 0, hh = h32(p, fr), sh = hh >>> 24; + for (let n = 0; n < 3; n++) c3[n] = S[o + n] + (hl ? 0.17 * Q[A * 3 + n] + 0.056 * (Q[B * 3 + n] + Q[c * 3 + n]) + 0.019 * Q[e * 3 + n] : 0); + const mx = max(c3[0], c3[1], c3[2]), k = mx > 0.001 ? (LUT[min(1023, (mx * 256) | 0)] / mx) * (0.8 + sh / 640) : 0; + for (let n = 0, hb; n < 3; n++) (hb = (hh >>> (n * 8)) & 255), (c3[n] = V[p] * (c3[n] * k * (1 + hb / 2200) + sh * (0.062 + (hb & 15) / 500) + m1 * FIELD[n])); + if (!m2) display.setPixel(x, y, min(255, c3[0]) | 0, min(255, c3[1]) | 0, min(255, c3[2]) | 0); + else O.set(c3, o), (L[p] = (mx * k) / 255); + } + if (!m2) return; + // Phase contrast: the field grey, each body darker than it, and a bright + // halo on the LEDs beside it. + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const l = L[p], o = p * 3, dk = 1 - 0.8 * cl(l * 1.6, 0, 1), hl = max(0, max(L[p - (x > 0)], L[p + (x < 63)], L[p - (y > 0) * W], L[p + (y < 127) * W]) - l) * 160; + const v = (i) => min(255, O[o + i] + m2 * (V[p] * (PHASE[i] * dk + hl + (h32(p, fr + i) & 15)) - O[o + i])) | 0; + display.setPixel(x, y, v(0), v(1), v(2)); + } +} From 6079c22fb345bd326b966cb6ce7345664edf6858 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:13:20 +0000 Subject: [PATCH 12/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Iceberg?= 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_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/patterns/iceberg.js | 407 +++++++++++++++++++++ 1 file changed, 407 insertions(+) create mode 100644 incoming/pattern-study/patterns/iceberg.js diff --git a/incoming/pattern-study/patterns/iceberg.js b/incoming/pattern-study/patterns/iceberg.js new file mode 100644 index 000000000..add85b5e0 --- /dev/null +++ b/incoming/pattern-study/patterns/iceberg.js @@ -0,0 +1,407 @@ +// ===== Patternflow pattern ===== +// Title: Iceberg +// Family: polar +// Idea: An iceberg cut through at the waterline: a ninth of it in the air with penguins on top, the rest below in banded glacier blue under shafts of light and bubble streams, until a slab calves off, splashes and sets it rocking. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 26, after the schoolbook drawing of an iceberg with eight ninths of it under the water +// Round: 26 +// =============================== +// @matrix 64x128 +// @knobs Light=0..1, Speed=0.2..3, Shape=0..1, Swell=0..1 +// +// Knob 1: Light. Midnight sun at 0, gold on the horizon at the right, lighting the berg's right faces; overcast noon in the middle; blue twilight at 1 with a moon, stars and an aurora. +// Knob 2: Speed. At 2 the first slab breaks off 3.5 seconds in and the next every 15 to 25 seconds; 0.2 is ten times slower. +// Knob 3: Shape. A tabular berg at 0, flat topped with five penguins on the snow; a pinnacle at 1, two spires of older, bluer ice over a keel that nearly reaches the bottom. +// Knob 4: Swell. How high the swell runs and how hard the berg bobs and leans in it; faster clouds, whitecaps on the far sea. +// Buttons: K1 tap breaks a slab off the berg now. + +const W = 64, H = 128, N = W * H; +// Sea level, horizon, the berg's middle; its grid (u across, v down from sea level) turns about row PV. +const WL = 30, HZ = 26, BX = 28, BW = 64, BH = 128, U0 = -32, V0 = -28, PV = 34, NB = BW * BH; +const { min, max, floor, round, sin, cos, sqrt, exp, abs, PI } = Math; +const hypot = (a, b) => sqrt(a * a + b * b); +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const mix = (a, b, t) => a + (b - a) * t; +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +const rnd = () => Math.random(); +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 vn(x, y, p = 1e9) { + const xi = floor(x), yi = floor(y), u = x - xi, v = y - yi, su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v), x1 = (xi + 1) % p, y1 = (yi + 1) % p; + const a = hash(xi % p, yi % p), b = hash(x1, yi % p), c = hash(xi % p, y1); + return a + (b - a) * su + (c - a) * sv + (a - b - c + hash(x1, y1)) * su * sv; +} +// Caustic noise; the colour the water leaves at each row. +const CT = new Float32Array(4096).map((_, k) => vn(((k & 63) * 20) / 64, ((k >> 6) * 20) / 64, 20)); +const AT = new Float32Array(H * 3).map((_, q) => exp(-max(0, floor(q / 3) + 0.5 - WL) / [26, 75, 170][q % 3])); + +// The top: a snow plain with sheer cliffs, or two spires. +const PIN = [[-13.5, 1], [-12, -3], [-10, -6], [-7.5, -10], [-5.5, -16], [-3.5, -19.5], [-1.5, -17.5], [0.5, -11], [2.5, -7.5], [4.5, -8.5], [6.5, -13], [8, -16], [9.5, -15], [11.5, -8], [13, -2.5], [14, 1]]; +function topAt(u, s) { + let i = 1; + while (i < PIN.length - 1 && u > PIN[i][0]) i++; + const [a, b] = PIN[i - 1], [c, e] = PIN[i], A = u > 0 ? u : -u * 1.05; + const tab = A < 15 ? -11 + 0.8 * (vn(u * 0.3, 3) - 0.5) + 1.3 * sm(12.5, 15, A) : mix(-9.7, 1, cl((A - 15) / 1.1, 0, 1)); + return mix(tab, b + (e - b) * cl((u - a) / (c - a), 0, 1), s); +} +// Half widths under the water, tabular and pinnacle, a little lopsided. +const CV = [0, 5, 12, 22, 34, 46, 58, 68, 76, 82, 86]; +const WT = [16.6, 20.5, 22, 22.5, 21.5, 21, 19.5, 16.5, 12.5, 8, 0]; +const WP = [13.6, 15.5, 16.5, 16.5, 15.5, 14, 12, 9.5, 7, 4, 0]; +const J = (h) => CV.map((_, i) => (i ? 0.88 + 0.24 * hash(i, h) : 1.03)), JL = J(5), JR = J(9); +function edgeAt(v, s) { + const q = v / (1 + 0.1 * s); + if (v < 0 || q >= 86) return [0, 0]; + let i = 0; + while (CV[i + 1] <= q) i++; + const f = (q - CV[i]) / (CV[i + 1] - CV[i]), w = mix(mix(WT[i], WP[i], s), mix(WT[i + 1], WP[i + 1], s), f), c = 2 * sin(v * 0.05); + return [c - w * mix(JL[i], JL[i + 1], f), c + w * mix(JR[i], JR[i + 1], f)]; +} + +// Midnight sun, noon, twilight: sky stops, lit and shaded face, snow, water top +// and bottom, cloud lit and dark, shafts, light's side, ice, caustics. +const KF = [ + [[[0, 28, 50, 116], [10, 100, 104, 166], [19, 232, 158, 132], [26, 250, 200, 128]], [246, 206, 158], [70, 118, 196], [242, 226, 206], [14, 86, 114], [3, 12, 40], [250, 196, 168], [118, 104, 150], [1, 1, 1, 1]], + [[[0, 118, 130, 148], [10, 156, 164, 178], [19, 186, 192, 200], [26, 200, 205, 210]], [214, 220, 226], [148, 166, 192], [226, 230, 236], [28, 108, 132], [5, 20, 48], [212, 216, 222], [128, 134, 150], [0.35, 0, 1.05, 0.5]], + [[[0, 6, 12, 42], [10, 16, 30, 80], [19, 38, 62, 120], [26, 62, 94, 148]], [160, 188, 232], [58, 84, 148], [176, 198, 236], [8, 44, 80], [2, 6, 24], [70, 86, 130], [26, 34, 66], [0.45, -1, 0.62, 0.5]], +].map((k) => Float64Array.from(k.flat(2))); +// Glacier ice; the pinnacle is older ice, bluer all through. +const BASE = [92, 192, 226], PALE = [190, 234, 246], CLEAR = [30, 120, 214], VEIN = [12, 50, 172], RIM = [40, 64, 60], TINT = [0.7, 0.9, 1.06]; +const BLK = [12, 14, 22], WHT = [226, 228, 236], SUN = [255, 214, 150], MOON = [224, 228, 214], AUR = [30, 190, 110], LT = [0.55, 0.92, 1]; + +export function setup(params) { + const F32 = (n) => new Float32Array(n), pen = () => Object.fromEntries("on h u f st tu x y vx vy tt nb lx ly lt".split(" ").map((k) => [k, 0.5])); + params.s = { + f: F32(N * 3), ba: F32(N), sf: F32(W), rt: F32(176), row: F32(H * 3), P: new Float64Array(41), ex: F32(BH * 2), TT: F32(BW * 4 + 1), + M: F32(NB), IC: [F32(NB), F32(NB), F32(NB)], SN: F32(NB), NX: F32(NB), AL: F32(NB), D: F32(NB), + s: -1, t: 0.01, th: 0.01, thv: 0.01, thq: 0.01, by: 0.01, byv: 0.01, A: 0.5, k1: -1, k4: 0.06, live: 0, kx: 49.5, ky: 112.5, + next: 3.4, nd: 0.5, pend: null, crack: 0.5, cuts: [], pieces: [], bubs: [], spray: [], rings: [], vacc: [0.5, 0.5, 0.5, 0.5, 0.5], dv: null, + cl: [0, 1, 2, 3].map((k) => ({ x: 6 + k * 17 + 6 * rnd(), y: 5 + ((k * 5) % 13) + 2 * rnd(), w: 7 + 5 * rnd(), h: 1.7 + rnd() })), + kr: Array.from({ length: 46 }, () => ({ p: rnd() * 6.3, q: rnd() * 6.3, w: 0.4 + 0.5 * rnd(), r: 1 + 6 * rnd() })), + pens: [0, 1, 2, 3, 4].map((i) => Object.assign(pen(), { f: i % 2 ? 1 : -1, st: 0 })), + }; +} + +const toW = (S, u, v) => [BX + cos(S.th) * u - sin(S.th) * (v - PV), WL + PV + S.by + sin(S.th) * u + cos(S.th) * (v - PV)]; +// What the cuts leave at u, v. Cuts heal outward as the berg turns. +function cutA(cuts, u, v) { + let a = 1; + for (const c of cuts) if (v < 0.6 && (u - c.cx) ** 2 + (v - c.cy) ** 2 < 225) a *= cl(0.5 - (c.nx * u + c.ny * v - c.d), 0, 1); + return a; +} +const topB = (S, u) => S.TT[cl(round((u - U0) * 4), 0, BW * 4)]; +function edgeU(S, side) { + for (let a = 0; a < 26; a += 0.5) if (topB(S, side * a) > -1.5 || cutA(S.cuts, side * a, topB(S, side * a) + 0.7) < 0.5) return side * (a - 1); + return side * 12; +} +const surf = (S, x) => S.sf[cl(round(x), 0, W - 1)]; + +function build(S, s) { + S.s = s; + const { M, SN, NX, AL, D, TT, ex } = S, E = [], face = []; + for (let q = 0; q <= BW * 4; q++) TT[q] = topAt(U0 + q / 4, s); + for (let q = 0; q < BH * 4; q++) E.push(edgeAt(V0 + (q + 0.5) / 4, s)); + for (let k = 0; k < NB; k++) { + let n = 0; + for (let j = 0; j < 16; j++) { + const qu = (k & 63) * 4 + (j & 3), qv = (k >> 6) * 4 + (j >> 2), u = U0 + (qu + 0.5) / 4, v = V0 + (qv + 0.5) / 4; + if (v < 0 ? v >= TT[qu] : u > E[qv][0] && u < E[qv][1]) n++; + } + M[k] = n / 16; + } + // Distance to open water: a glowing rim, a darker core. + for (let k = 0; k < NB; k++) D[k] = M[k] < 0.5 ? 0 : 20; + for (let k = BW + 1; k < NB - 1; k++) D[k] = min(D[k], D[k - 1] + 1, D[k - BW] + 1, D[k - BW - 1] + 1.4, D[k - BW + 1] + 1.4); + for (let k = NB - BW - 2; k > 0; k--) D[k] = min(D[k], D[k + 1] + 1, D[k + BW] + 1, D[k + BW + 1] + 1.4, D[k + BW - 1] + 1.4); + for (let j = 0; j < BH * 2; j++) ex[j] = j & 1 ? -99 : 99; + for (let k = 0; k < NB; k++) { + const gu = k & 63, gv = k >> 6, u = U0 + gu + 0.5, v = V0 + gv + 0.5; + if (M[k] <= 0) continue; + // Facets: lit or shaded in the air; below, melt scallops lit on their lower walls. + let bd = 1e9, id = 0, sv = 0; + for (let j = 0; j < 9; j++) { + const cu = floor(u / 5) + (j % 3) - 1, cv = floor(v / 5) + floor(j / 3) - 1, w = (cv + hash(cv, cu + 50)) * 5, dd = (u - (cu + hash(cu, cv)) * 5) ** 2 + (v - w) ** 2; + if (dd < bd) (bd = dd), (id = hash(cu + 99, cv)), (sv = w); + } + const qu = gu * 4 + 2, sl = TT[min(BW * 4, qu + 2)] - TT[max(0, qu - 2)], dep = v - TT[qu]; + NX[k] = mix(sl / sqrt(1 + sl * sl), cl(u / 24 + 1.3 * (id - 0.5), -1, 1), sm(1, 3, dep)); + SN[k] = sm(2.4, 1.3, dep) * sm(1.6, 0.8, abs(sl)); + AL[k] = (0.86 + 0.14 * hash(gu, 7)) * (0.88 + 0.24 * id) * (v > -1.5 && v < 0 ? 0.7 : 1); + // Layers of white and clear blue ice, dark veins of refrozen meltwater. + const f = (v + u * 0.14 + 7 * vn(u * 0.04, v * 0.025 + 9)) / 8, n = floor(f), band = f - n, h1 = hash(n, 5); + const mot = (0.84 + 0.3 * vn(u * 0.1 + 3, v * 0.07)) * (1 + 0.2 * cl((v - sv) / 3, -1, 1)); + const pale = sm(0.5, 0.6, band) * (1 - sm(0.6 + 0.3 * h1, 0.7 + 0.3 * h1, band)) * sm(0.25, 0.5, hash(n, 6)), clear = (1 - sm(0.2, 0.45, band)) * sm(0.4, 0.8, h1); + const vein = max(sm(0.11, 0.03, abs(vn(u * 0.08 + 11, v * 0.045) * 2 - 1)), 0.8 * sm(0.1, 0.025, abs(vn(u * 0.045 + 31, v * 0.11 + 5) * 2 - 1))) * sm(0.25, 0.5, vn(u * 0.035 + 20, v * 0.03)); + const dk = D[k], core = (0.5 + 0.5 * exp(-max(0, dk - 1) / 5)) * (gv > 1 && M[k - 2 * BW] < 0.5 ? 1.3 : 1) * (gv < BH - 2 && M[k + 2 * BW] < 0.5 ? 0.76 : 1); + for (let c = 0; c < 3; c++) S.IC[c][k] = mix(mix(mix(BASE[c] * mot, PALE[c], 0.8 * pale), CLEAR[c], 0.6 * clear), VEIN[c], 0.9 * vein) * core * mix(1, TINT[c], s) + exp(-dk / 1.2) * RIM[c]; + if (v > 3 && v < 80 && dk > 1.5) face.push([u, v]); + (ex[gv * 2] = min(ex[gv * 2], u)), (ex[gv * 2 + 1] = max(ex[gv * 2 + 1], u)); + } + S.vents = [0, 1, 2, 3, 4].map((i) => face[floor(hash(i, 41) * face.length)]); + // Penguin places: flat, three columns apart, clear of the slabs. + const slots = []; + for (const u of [0.5, -6, 6.5, -3, 3.5, -8, 8, -1.5, 2, -4.5, 5]) { + const q = round((u - U0) * 4); + if (abs(TT[q + 6] - TT[q - 6]) < 1.6 && TT[q] < -2.5 && TT[q] > -15 && slots.length < 5 && slots.every((w) => abs(w - u) > 2.9)) slots.push(u + 0.01); + } + S.pens.forEach((p, i) => ((p.on = i < slots.length ? 1 : 0), p.on && (p.h = slots[i]), p.st || (p.u = p.h))); +} + +const bub = (S, x, y, vy = -(4.5 + 5 * rnd()), wb = 0.6) => S.bubs.length < 260 && S.bubs.push({ x, y, vy, wb, ph: rnd() * 6.3, dead: 0 }); +function splash(S, x, v, g) { + const y = surf(S, x) - 0.5; + for (let k = 0; k < 7 + 29 * g; k++) S.spray.push({ x: x + (rnd() - 0.5) * (2 + 3 * g), y, vx: (rnd() - 0.5) * (10 + 14 * g), vy: -(6 + rnd() * (9 + 17 * g)), l: 0.99 }); + S.rings.push({ x, t: 0.01, a: g ? min(3.2, 0.1 * v + 1.4) : 0.6 }); + for (let k = 0; k < 8 + 22 * g; k++) bub(S, x + (rnd() - 0.5) * 6, y + 1 + rnd() * (5 + 9 * g)); +} + +// A crack opens behind a slab of outer ice; it goes half a second later. +function calve(S) { + if (S.pend || S.pieces.some((p) => p.f)) return; + let side = rnd() < 0.5 ? -1 : 1; + if (S.cuts.some((c) => c.side === side && c.age < 40)) side = -side; + let ue = edgeU(S, side); + if (abs(ue) < 12) (side = -side), (ue = edgeU(S, side)); + if (abs(ue) < 12) return; + const px = side * max(9, abs(ue) - 3.5 - 3 * rnd()), nx = side * 0.993, d = nx * px + 0.6; + const c = { nx, ny: -0.12, d, d0: d, cx: px, cy: -5, side, age: 0.01, bu: 0.5, bv: 0.5, ex: 0.5, hx: 0.5, hy: 0.5 }; + let u0 = 99, u1 = -99, v0 = 99, v1 = -99; + for (let k = 0; k < NB; k++) { + const u = U0 + (k & 63) + 0.5, v = V0 + (k >> 6) + 0.5; + if (S.M[k] > 0.5 && cutA([c], u, v) < 0.5 && cutA(S.cuts, u, v) > 0.5) (u0 = min(u0, u)), (u1 = max(u1, u)), (v0 = min(v0, v)), (v1 = max(v1, v)); + } + if (u1 - u0 < 1.5) return; + Object.assign(c, { bu: (u0 + u1) / 2, bv: (v0 + v1) / 2, hx: (u1 - u0) / 2 + 0.5, hy: (v1 - v0) / 2 + 0.5 }); + c.ex = hypot(c.hx, c.hy) + 1.5; + (S.pend = c), (S.crack = 0.5); +} + +function step(S, d) { + const t = (S.t += d), A = S.A, sf = S.sf; + // The swell, and rings running out from each splash. + const swell = (x) => A * (0.7 * sin(x * 0.15 - t * 1.4) + 0.3 * sin(x * 0.34 + t * 0.95 + 1)); + for (let x = 0; x < W; x++) { + sf[x] = WL + swell(x + 0.5); + for (const r of S.rings) { + const q = abs(x + 0.5 - r.x) - 11 * r.t; + sf[x] += r.a * exp(-r.t * 0.5 - (q / 5) ** 2) * sin(q * 0.9); + } + } + for (const r of S.rings) r.t += d; + S.rings = S.rings.filter((r) => r.t < 7); + S.thv += (-2.4 * (S.th - S.thq - 0.022 * A * cos(BX * 0.15 - t * 1.4)) - 0.45 * S.thv) * d; + S.byv += (-3 * (S.by - 0.5 * swell(BX)) - 1.2 * S.byv) * d; + (S.th += S.thv * d), (S.by += S.byv * d), (S.thq *= exp(-d / 60)); + for (const c of S.cl) (c.x += (0.7 + 3 * S.k4) * d) > W + c.w + 2 && (c.x = -c.w - 2); + + if (S.live && (S.next -= d) <= 0) calve(S), (S.next = 15 + 10 * rnd()); + if (S.pend && (S.crack -= d) <= 0) { + // The berg loses weight on that side, rocks and settles leaning a little. + const c = S.pend, [x, y] = toW(S, c.bu, c.bv); + S.cuts.push(c); + S.pieces.push({ c, x, y, vx: c.side * 2.5, vy: 0.5, a: S.th, va: c.side * (1.4 + rnd()), f: 1, ta: 0.5 }); + (S.pend = null), (S.thv -= c.side * 0.1), (S.thq -= c.side * 0.012), (S.byv -= 1.6); + } + for (const c of S.cuts) (c.age += d) > 20 && (c.d += d * 0.12); + S.cuts = S.cuts.filter((c) => c.age < 130); + for (const p of S.pieces) { + if (p.f) { + (p.vy += 45 * d), (p.x += p.vx * d), (p.y += p.vy * d), (p.a += p.va * d); + if (p.y > surf(S, p.x) - 1) splash(S, p.x, p.vy, 1), (p.f = 0), (p.vy *= 0.3), (p.ta = round(p.a / (PI / 2)) * (PI / 2)); + } else { + const c = p.c, hy = abs(sin(p.a - S.th)) * c.hx + abs(cos(p.a - S.th)) * c.hy; + (p.vy += (-5 * (p.y - surf(S, p.x) - 0.75 * hy) - 1.8 * p.vy) * d), (p.va += (-4 * (p.a - p.ta) - 1.6 * p.va) * d); + (p.y += p.vy * d), (p.a += p.va * d), (p.x += c.side * 1.4 * d); + } + } + S.pieces = S.pieces.filter((p) => abs(p.x - W / 2) < W / 2 + p.c.ex + 2); + S.vents.forEach((v, i) => { + if (sin(t * 0.6 + i * 2.1) > -0.2) for (S.vacc[i] += d * 3.5; S.vacc[i] >= 1; S.vacc[i]--) bub(S, ...toW(S, v[0], v[1]), -(5 + 0.6 * i + 1.2 * rnd()), 0.9); + }); + for (const b of S.bubs) { + (b.y += b.vy * d), (b.x += sin(t * 6 + b.ph) * b.wb * d); + if (b.y < surf(S, b.x) + 0.3 || b.x < -1 || b.x > W + 1) b.dead = 1; + } + S.bubs = S.bubs.filter((b) => !b.dead); + for (const p of S.spray) (p.vy += 50 * d), (p.x += p.vx * d), (p.y += p.vy * d), (p.l -= d * 0.8); + S.spray = S.spray.filter((p) => p.l > 0 && !(p.vy > 0 && p.y > surf(S, p.x) + 0.5)); + (S.kx = 49 + 8 * sin(t * 0.1)), (S.ky = 112 + 7 * sin(t * 0.13 + 1)); + penguins(S, d, t); +} + +// Now and then a penguin waddles to the edge, jumps, swims to the krill and leaps back. +function penguins(S, d, t) { + for (const p of S.pens) if (!p.st && rnd() < d * 0.2) p.f = -p.f; + let p = S.dv; + if (!p && S.live && (S.nd -= d) <= 0) { + const free = S.pens.filter((q) => !q.st && q.on); + if (!free.length) return; + p = S.dv = free.reduce((a, q) => (q.u > a.u ? q : a)); + Object.assign(p, { st: 1, tu: edgeU(S, 1) - 0.5 }); + } + if (!p) return; + if (p.st === 1 || p.st === 5) { + const g = p.st === 1 ? p.tu : p.h; + p.f = g > p.u ? 1 : -1; + p.u += p.f * min(abs(g - p.u), 4 * d); + if (abs(g - p.u) < 0.01) { + if (p.st === 5) (p.st = 0), (S.dv = null), (S.nd = 5 + 6 * rnd()); + else ([p.x, p.y] = toW(S, p.u, topB(S, p.u) - 0.3)), Object.assign(p, { st: 2, vx: 7, vy: -9 }); + } + } else if (p.st === 2) { + (p.vy += 40 * d), (p.x += p.vx * d), (p.y += p.vy * d); + if (p.vy > 0 && p.y > surf(S, p.x)) splash(S, p.x, 8, 0), Object.assign(p, { st: 3, vy: 12, vx: 3, tt: 0.01, nb: 0.01 }); + } else if (p.st === 3) { + const back = (p.tt += d) > 5, dx = (back ? W - 6 : S.kx + 5 * cos(t * 2.2)) - p.x, dy = (back ? WL - 2 : S.ky + 3 * sin(t * 2.2)) - p.y, r = hypot(dx, dy) + 1e-6, sp = back ? 17 : 14; + (p.vx += ((dx / r) * sp - p.vx) * min(1, 2.2 * d)), (p.vy += ((dy / r) * sp - p.vy) * min(1, 2.2 * d)); + (p.x = cl(p.x + p.vx * d, 1, W - 2)), (p.y = min(p.y + p.vy * d, H - 3)); + if ((p.nb += d) > 0.07) (p.nb = 0), bub(S, p.x - p.vx * 0.12, p.y - p.vy * 0.12); + if (back && p.y < surf(S, p.x) + 1.5) splash(S, p.x, 6, 0), Object.assign(p, { st: 4, lx: p.x, ly: p.y, lt: 0.01, u: edgeU(S, 1) - 1 }); + } else { + const [lx, ly] = toW(S, p.u, topB(S, p.u) - 0.3), l = (p.lt = min(1, p.lt + d / 0.7)); + (p.vx = lx - p.x), (p.vy = ly - p.y), (p.x = mix(p.lx, lx, l)), (p.y = mix(p.ly, ly, l) - 40 * l * (1 - l)); + if (l >= 1) p.st = 5; + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], k1 = cl(k[0], 0, 1), k3 = cl(k[2], 0, 1); + if (abs(k3 - S.s) > 0.01) build(S, k3); + (S.k4 = cl(k[3], 0, 1)), (S.A = 0.45 + 3 * S.k4); + if (k1 !== S.k1) { + const [a, b, u] = k1 < 0.5 ? [0, 1, sm(0, 0.5, k1)] : [1, 2, sm(0.5, 1, k1)]; + for (let j = 0; j < 41; j++) S.P[j] = mix(KF[a][j], KF[b][j], u); + S.k1 = k1; + } + for (; !S.live; S.live = S.t > 4 ? 1 : 0) step(S, 1 / 30); + if (input.btnPressed && input.btnPressed[0]) S.next = 0; + step(S, cl(dt || 0, 0, 0.1) * cl(k[1], 0.2, 3) * 0.5); +} + +let F, G, PL, RT, LR, LS, LC; +const LA = [0, 0, 0, 0], C3 = new Float64Array(3); +// Light under water: slanting shafts, and caustics on the ice (one noise twice, crossed). +function glow(x, y, dep, b) { + const q = x + LS * (y - WL) + 56, q0 = cl(floor(q), 0, 174); + let L = LR * mix(RT[q0], RT[q0 + 1], q - q0) * exp(-dep / 36) * 64 * (1 + 0.5 * b); + if (b > 0 && dep < 34) L += LC * sm(0.75, 1, 1 - abs(CT[((y + LA[1]) & 63) * 64 + ((x + LA[0]) & 63)] - CT[((x + LA[2]) & 63) * 64 + ((y + LA[3]) & 63)]) * 4) * exp(-dep / 12) * (80 * b + 22); + return L; +} +function put(x, y, c, a = 1) { + (x = floor(x)), (y = floor(y)); + if (x < 0 || x >= W || y < 0 || y >= H) return; + for (let i = 0, o = (y * W + x) * 3; i < 3; i++) F[o + i] = mix(F[o + i], c[i], a); +} +const bl = (A, k, fx, fy) => mix(mix(A[k], A[k + 1], fx), mix(A[k + BW], A[k + BW + 1], fx), fy); +// One LED of the berg, or of slab q, read at body point u, v. +function paint(x, y, u, v, q) { + const S = G, P = PL, gx = u - U0 - 0.5, gy = v - V0 - 0.5, x0 = floor(gx), y0 = floor(gy), i = y * W + x, o = i * 3, s = S.sf[x], c = C3, pc = S.pend; + if (x0 < 0 || y0 < 0 || x0 >= BW - 1 || y0 >= BH - 1 || (q && (v > 0.6 || (u - q.cx) ** 2 + (v - q.cy) ** 2 > 225))) return; + const k = y0 * BW + x0, fx = gx - x0, fy = gy - y0, a = bl(S.M, k, fx, fy) * (q ? cl(0.5 + (q.nx * u + q.ny * v - q.d0), 0, 1) : S.cuts.length && v < 0.6 ? cutA(S.cuts, u, v) : 1); + if (a < 0.02) return; + if (y + 0.5 < s) { + const nx = bl(S.NX, k, fx, fy), sn = bl(S.SN, k, fx, fy), al = bl(S.AL, k, fx, fy), li = P[38] ? cl(0.5 + 0.8 * nx * P[38], 0, 1) : 0.6 + 0.25 * nx; + for (let j = 0; j < 3; j++) c[j] = mix(mix(P[19 + j], P[16 + j], li), P[22 + j] * (0.88 + 0.12 * li), sn) * al; + } else { + const L = glow(x, y, y + 0.5 - s, 1); + for (let j = 0; j < 3; j++) c[j] = bl(S.IC[j], k, fx, fy) * AT[y * 3 + j] * P[39] + 0.35 * S.row[y * 3 + j] + L * LT[j]; + } + const ck = pc && v < 0.6 && abs(pc.nx * u + pc.ny * v - pc.d) < 0.6 ? 0.4 : 1; + for (let j = 0; j < 3; j++) F[o + j] = mix(q ? F[o + j] : 0, c[j] * ck, q ? a : 1); + S.ba[i] = max(S.ba[i], a); +} + +export function draw(display, params) { + const S = params.s; + if (!S || S.k1 < 0) return; + F = S.f; + const { ba, sf, rt, row, t, P, k1, ex } = S; + const sunA = 1 - sm(0, 0.4, k1), moonA = sm(0.6, 1, k1), aur = sm(0.7, 1, k1), stars = sm(0.55, 0.9, k1); + for (let y = 0; y < H; y++) { + let k = 0; + while (k < 2 && y > P[k * 4 + 4]) k++; + const u = y <= HZ ? sm(P[k * 4], P[k * 4 + 4], y) : cl((y - WL) / 97, 0, 1) ** 0.75; + for (let j = 0; j < 3; j++) row[y * 3 + j] = y <= HZ ? mix(P[k * 4 + 1 + j], P[k * 4 + 5 + j], u) : mix(P[25 + j], P[28 + j], u); + } + for (let q = 0; q < 176; q++) rt[q] = sm(0.45, 0.82, vn((q - 56) * 0.13 - t * 0.3, t * 0.05 + 7)); + (G = S), (PL = P), (RT = rt), (LR = P[37]), (LS = P[38] * 0.45), (LC = P[40] * (0.7 + 0.6 * S.k4)); + [7, -5, -6, 4].forEach((v, j) => (LA[j] = floor(t * v))); + + // The berg first, then the background where it shows, then the slabs. + ba.fill(0); + const ct = cos(S.th), st = sin(S.th), oy = WL + PV + S.by; + for (let y = 0; y < H; y++) { + const dy = y + 0.5 - oy, g0 = floor(ct * dy + PV - V0 - 0.5); + let u0 = 99, u1 = -99; + for (let j = max(0, g0 - 3); j <= min(BH - 1, g0 + 3); j++) (u0 = min(u0, ex[j * 2])), (u1 = max(u1, ex[j * 2 + 1])); + for (let x = max(0, floor(BX + u0 - st * dy - 2)); x < min(W, BX + u1 - st * dy + 3); x++) { + const dx = x + 0.5 - BX; + paint(x, y, ct * dx + st * dy, -st * dx + ct * dy + PV, null); + } + } + const sx = 55, sy = 21.5, mx = 12, my = 8, c = C3; + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, o = i * 3, A = ba[i], dep = y + 0.5 - sf[x]; + if (A > 0.995) continue; + let g = 0, e = 0; + if (y < HZ) { + const ds = hypot(x + 0.5 - sx, (y + 0.5 - sy) * 1.1), dm = hypot(x + 0.5 - mx, y + 0.5 - my), cu = aur > 0 ? 0.5 + 0.5 * sin(x * 0.2 + 3 * vn(x * 0.05 + t * 0.06, 1) + t * 0.25) : 0; + g = moonA * (0.25 * exp(-dm / 5) + (dm < 2.6 && hypot(x - 0.8 - mx, y + 1.1 - my) > 2.3 ? 0.9 : 0)) + (stars > 0 && hash(x, y + 11) < 0.016 ? stars * (0.25 + 0.5 * hash(y, x)) * (0.7 + 0.3 * sin(t * 3 + x * y)) * sm(HZ, 8, y) : 0); + e = cu ? aur * sm(19, 6, y) * sm(0, 4, y) * cu * cu * vn(x * 0.6, t * 0.4) : 0; + const gs = ds < 30 ? sunA * (0.55 * exp(-ds / 7) + (ds < 2.6 ? 1.2 : 0)) : 0; + for (let j = 0; j < 3; j++) c[j] = row[y * 3 + j] + SUN[j] * gs + MOON[j] * g + AUR[j] * e; + for (const q of S.cl) { + const qx = (x + 0.5 - q.x) / q.w, qy = (y + 0.5 - q.y) / q.h; + if (qx * qx + qy * qy > 2) continue; + const a = 0.92 * sm(0, 0.45, 1 - qx * qx - qy * qy + 0.45 * (vn(x * 0.35 + q.w, y * 0.6) - 0.5)), u = sm(-1, 1, qy); + for (let j = 0; j < 3; j++) c[j] = mix(c[j], mix(P[34 + j], P[31 + j], u), a); + } + } else if (dep < 0) { + // The far sea: the sun's glitter path, whitecaps. + g = sunA * (abs(x + 0.5 - sx) < 1.5 + (y - HZ) * 1.2 && hash(x, y + floor(t * 7) * 64) < 0.45 ? 0.75 : 0); + e = S.k4 > 0.35 && hash(x + floor(t * 2) * 7, y) < (S.k4 - 0.35) * 0.3 ? 0.8 : 0; + for (let j = 0; j < 3; j++) c[j] = mix(row[36 + j] * 0.55 + row[90 + j] * 0.45 + SUN[j] * g, 230, e); + } else { + // The surface's silver underside. + (g = glow(x, y, dep, 0)), (e = dep < 2.5 ? (1 - dep / 2.5) * 0.5 : 0); + for (let j = 0; j < 3; j++) c[j] = mix(row[y * 3 + j] + (j ? 90 : 60) * e + g * LT[j], row[78 + j] * 0.9, dep < 1 ? 0.55 : 0); + } + for (let j = 0; j < 3; j++) F[o + j] = mix(c[j], F[o + j], A); + } + for (const p of S.pieces) { + const ca = cos(p.a), sa = sin(p.a), e = p.c.ex; + for (let y = max(0, floor(p.y - e)); y < min(H, p.y + e); y++) + for (let x = max(0, floor(p.x - e)); x < min(W, p.x + e); x++) { + const dx = x + 0.5 - p.x, dy = y + 0.5 - p.y; + paint(x, y, p.c.bu + ca * dx + sa * dy, p.c.bv - sa * dx + ca * dy, p.c); + } + } + sprites(S, t, ba); + for (let i = 0, o = 0; i < N; i++, o += 3) display.setPixel(i & 63, i >> 6, min(255, F[o]) | 0, min(255, F[o + 1]) | 0, min(255, F[o + 2]) | 0); +} + +function sprites(S, t, ba) { + for (const k of S.kr) { + const x = S.kx + cos(k.p + t * k.w) * k.r * 1.3, y = S.ky + sin(k.q + t * k.w * 1.3) * k.r * 0.7, i = floor(y) * W + floor(x); + if (i >= 0 && i < N && ba[i] < 0.5) put(x, y, [255, 128, 118], (0.25 + 0.35 * S.P[39]) * (0.7 + 0.3 * sin(t * 4 + k.p * 9))); + } + for (const b of S.bubs) put(b.x, b.y, [214, 246, 255], 0.6); + for (const p of S.pens) { + if (!p.st && !p.on) continue; + if (p.st === 0 || p.st === 1 || p.st === 5) { + const [x, y] = toW(S, p.u, topB(S, p.u) - 0.3), f = p.f, B = round(x - 0.5) + (f > 0 ? 0 : 1), Y = floor(y) - (p.st && sin(t * 14) > 0 ? 1 : 0); + put(B, Y - 3, BLK), put(B + f, Y - 3, BLK), put(B + 2 * f, Y - 3, [240, 168, 60], 0.8); + for (let j = 0; j < 3; j++) put(B, Y - j, BLK), put(B + f, Y - j, WHT); + } else { + const r = hypot(p.vx, p.vy) + 1e-6, ux = p.vx / r, uy = p.vy / r; + for (let j = -1.5; j <= 1.5; j += 0.5) put(p.x + ux * j, p.y + uy * j, BLK), j < 1 && put(p.x + ux * j - uy * (ux < 0 ? -1 : 1), p.y + uy * j + abs(ux), WHT, 0.9); + } + } + for (const p of S.spray) put(p.x, p.y, [232, 240, 246], min(1, p.l * 1.3)); +} From f964bb3b284603b935f81796a54e73c0a3a3e1bc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:14:27 +0000 Subject: [PATCH 13/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Noborigama?= 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_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/patterns/noborigama.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/incoming/pattern-study/patterns/noborigama.js b/incoming/pattern-study/patterns/noborigama.js index 9a062f4ff..9372b99fd 100644 --- a/incoming/pattern-study/patterns/noborigama.js +++ b/incoming/pattern-study/patterns/noborigama.js @@ -213,7 +213,7 @@ function throwTo(S, c, delay = 0) { function puff(S, x, y, vy, r, a, k) { if (S.puffs.length > 110) S.puffs.shift(); - S.puffs.push({ x, y, vy, r, a, k, hot: 0.7 + 0.3 * k, ph: rnd() * 7 }); + S.puffs.push({ x, y, vy, r, a, k, hot: 0.7 - 0.2 * k, ph: rnd() * 7 }); } function spark(S, s) { @@ -253,7 +253,7 @@ function step(S, d) { // The potter stokes twice, watches the spy hole, then goes where wood is needed. pt.ph += d; if (!pt.mode) { - pt.A += cl(S.Ac[pt.tgt] - pt.A, -11 * d, 11 * d); + pt.A += cl(S.Ac[pt.tgt] - pt.A, -13 * d, 13 * d); if (abs(S.Ac[pt.tgt] - pt.A) < 1e-3) pt.mode = 1, pt.tm = 0, pt.thr = 0; } else if (pt.mode === 1) { pt.tm += d; @@ -261,7 +261,7 @@ function step(S, d) { throwTo(S, pt.tgt); pt.thr++; } - if (pt.tm > 1.8) pt.mode = 2, pt.tm = 0.8 + rnd(); + if (pt.tm > 1.5) pt.mode = 2, pt.tm = 0.6 + 0.6 * rnd(); } else if ((pt.tm -= d) <= 0) pt.tgt = ++pt.k % 3 ? S.cf : min(n, S.cf + 1), pt.mode = 0; // The stoker feeds the fire mouth. fb.an = max(0, fb.an - d); @@ -294,7 +294,7 @@ function air(S, d) { p.x += (wind * 18 * (0.5 + 0.5 * cl((TOP - p.y) / TOP, 0, 1)) + 1.2 * sin(S.t * 0.9 + p.ph)) * d; p.r = min(10 + p.k, p.r + d * (1.5 + 1.2 * p.k + 2 * cl((8 - p.y) / 8, 0, 1))); p.a -= d * (0.05 + 0.04 * (1 - p.k)); - p.hot = max(0, p.hot - d * (1.4 - 0.9 * p.k)); + p.hot = max(0, p.hot - d * 1.4); } S.puffs = S.puffs.filter((p) => p.a > 0.03 && p.y + p.r > -3 && p.x > -14 && p.x < W + 14); for (S.ns += d * (2 + 5 * T[n]) * (1 + 0.6 * wind); S.ns >= 1; S.ns--) spark(S, 1); @@ -404,8 +404,8 @@ export function draw(display, params) { glow(x0, y0, 5, [120, 50, 10], s); for (let j = 0; j < len; j++) { const u = j / len, x = x0 - j * (1 - 0.6 * max(0, wind)) + wind * j * 0.4, y = y0 - j * j * 0.06 - j * 0.15 + sin(t * 17 + j) * 0.4 * u; - put(x, y, lc(T + 0.3 - 0.6 * u), 1 - u * 0.3); - if (u < 0.7) put(x, y - 1, lc(T + 0.1 - 0.6 * u), 0.8 - u), put(x, y + 1, lc(T - 0.6 * u), 0.5 - 0.5 * u); + put(x, y, lc(T + 0.45 - 0.6 * u), 1 - u * 0.3); + if (u < 0.7) put(x, y - 1, lc(T + 0.25 - 0.6 * u), 0.8 - u), put(x, y + 1, lc(T + 0.1 - 0.6 * u), 0.5 - 0.5 * u); } } const pt = S.pot, [px, py, climb] = posAt(S, pt.A), pose = pt.mode === 1 ? 3 : pt.mode ? 0 : climb ? 2 : 1; From 3b91e4d4b64aed9d60b1dd452c996d2cabd03aa6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:15:33 +0000 Subject: [PATCH 14/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Noborigama?= 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_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/patterns/noborigama.js | 1 + 1 file changed, 1 insertion(+) diff --git a/incoming/pattern-study/patterns/noborigama.js b/incoming/pattern-study/patterns/noborigama.js index 9372b99fd..53a4ec07d 100644 --- a/incoming/pattern-study/patterns/noborigama.js +++ b/incoming/pattern-study/patterns/noborigama.js @@ -186,6 +186,7 @@ function build(S, n) { if (!pt.mode) pt.mode = 2; S.logs = S.logs.filter((l) => l.c <= n); S.slugs = []; + S.ev = []; } // A stoke: reduction first, the flame starved and smoky, then the surge. From 5cd3f08c9b59a93804756e10d7f55feffb456c08 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:16:29 +0000 Subject: [PATCH 15/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Dark=20Field?= 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_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/patterns/dark-field.js | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/incoming/pattern-study/patterns/dark-field.js b/incoming/pattern-study/patterns/dark-field.js index 967554bca..9f360e2e8 100644 --- a/incoming/pattern-study/patterns/dark-field.js +++ b/incoming/pattern-study/patterns/dark-field.js @@ -13,7 +13,7 @@ // // Knob 1: Light. At 0 classic dark field, white and gold on black. At 0.5 a Rheinberg filter: a deep blue field, each edge yellow where it faces sideways and red where it faces up or down. At 1 phase contrast: a grey field, the bodies dark inside bright halos. // Knob 2: Speed. At 2 a paramecium crosses the panel in about six seconds; at 0.2 the drop runs ten times slower. -// Knob 3: Crowd. At 0 three Volvox, three paramecia, five diatoms, five Vorticella bells, a rotifer and a few hundred bacteria; at 1 more of each, fading in. +// Knob 3: Crowd. At 0 three Volvox, three paramecia, five diatoms, two Oscillatoria threads, five Vorticella bells, a rotifer and a few hundred bacteria; at 1 more of each, fading in. // Knob 4: Focus. Which depth is sharp: near 0 the algal thread with its Vorticella and rotifer, toward -1 the water under the coverslip, toward 1 the slide's glass, where diatoms glide among settled grit. // Buttons: K1 tap knocks the slide: the view jolts, every Vorticella snaps its stalk into a coil and the rotifer pulls in its crown. On its own one bell snaps four seconds in, then one every ten seconds or so. @@ -123,7 +123,7 @@ const LUT = Float32Array.from({ length: 1024 }, (_, i) => { export function setup(params) { const F = (n) => new Float32Array(n), side = () => (rnd() < 0.5 ? -1 : 1), A = (n, f) => Array.from({ length: n }, (_, i) => ({ x: rr(-12, 76), y: rr(-12, 140), h: rnd() * TAU, ...f(i) })); - const P = (params.s = { t: 0, fr: 0, S: F(N * 3), Q: F(HW * HH * 3), T: F(HW * HH * 3), QS: F(HW * HH), O: F(N * 3), L: F(N), V: F(N), OP: F(140), nx: 0, ny: 0, ex: 0, ey: 0, nt: 3.2, ja: 0, jt: 9, jx: 0, jy: 0, s1: rnd() * 7, s2: rnd() * 7 }); + const P = (params.s = { t: 0, fr: 0, S: F(N * 3), Q: F(HW * HH * 3), T: F(HW * HH * 3), QS: F(HW * HH), L: F(N), V: F(N), OP: F(140), nx: 0, ny: 0, ex: 0, ey: 0, nt: 3.2, ja: 0, jt: 9, jx: 0, jy: 0, s1: rnd() * 7, s2: rnd() * 7 }); // The round field stop of the eyepiece clips the panel's two ends. for (let p = 0; p < N; p++) P.V[p] = sm(67.5, 62, sqrt(((p & 63) - 31.5) ** 2 + (((p >> 6) - 63.5) * 1.02) ** 2)); // Volvox: cells on a sphere (a Fibonacci spiral) and daughter colonies inside. @@ -467,7 +467,7 @@ export function draw(display, params) { // Light into LEDs, toned by the brightest channel so a green stays green, // each lit LED scintillating a little, and under it all the shimmer of // particles too small to resolve, below 24/255. - const fr = (P.fr = (P.fr + 1) | 0), m2 = P.m2, { V, L, O, QS } = P, c3 = K0; + const fr = (P.fr = (P.fr + 1) | 0), m2 = P.m2, { V, L, QS } = P, c3 = K0; for (let i = 0; i < HW * HH; i++) QS[i] = Q[i * 3] + Q[i * 3 + 1] + Q[i * 3 + 2]; for (let y = 0, p = 0; y < H; y++) for (let x = 0; x < W; x++, p++) { @@ -475,18 +475,15 @@ export function draw(display, params) { const o = p * 3, i = x >> 1, j = (y >> 1) * HW, i2 = cl(i + (x & 1) * 2 - 1, 0, HW - 1), j2 = cl((y >> 1) + (y & 1) * 2 - 1, 0, HH - 1) * HW; const A = j + i, B = j + i2, c = j2 + i, e = j2 + i2, hl = QS[A] + QS[B] + QS[c] + QS[e] > 0, hh = h32(p, fr), sh = hh >>> 24; for (let n = 0; n < 3; n++) c3[n] = S[o + n] + (hl ? 0.17 * Q[A * 3 + n] + 0.056 * (Q[B * 3 + n] + Q[c * 3 + n]) + 0.019 * Q[e * 3 + n] : 0); - const mx = max(c3[0], c3[1], c3[2]), k = mx > 0.001 ? (LUT[min(1023, (mx * 256) | 0)] / mx) * (0.8 + sh / 640) : 0; + const mx = max(c3[0], c3[1], c3[2]), k = mx > 0.001 ? (LUT[min(1023, (mx * 256) | 0)] / mx) * (0.8 + sh / 640) : 0, l = (mx * k) / 255; for (let n = 0, hb; n < 3; n++) (hb = (hh >>> (n * 8)) & 255), (c3[n] = V[p] * (c3[n] * k * (1 + hb / 2200) + sh * (0.062 + (hb & 15) / 500) + m1 * FIELD[n])); - if (!m2) display.setPixel(x, y, min(255, c3[0]) | 0, min(255, c3[1]) | 0, min(255, c3[2]) | 0); - else O.set(c3, o), (L[p] = (mx * k) / 255); - } - if (!m2) return; - // Phase contrast: the field grey, each body darker than it, and a bright - // halo on the LEDs beside it. - for (let y = 0, p = 0; y < H; y++) - for (let x = 0; x < W; x++, p++) { - const l = L[p], o = p * 3, dk = 1 - 0.8 * cl(l * 1.6, 0, 1), hl = max(0, max(L[p - (x > 0)], L[p + (x < 63)], L[p - (y > 0) * W], L[p + (y < 127) * W]) - l) * 160; - const v = (i) => min(255, O[o + i] + m2 * (V[p] * (PHASE[i] * dk + hl + (h32(p, fr + i) & 15)) - O[o + i])) | 0; - display.setPixel(x, y, v(0), v(1), v(2)); + // Phase contrast: the field grey, each body darker than it, and a + // bright halo on the LEDs beside it. + if (m2) { + const dk = 1 - 0.8 * cl(l * 1.6, 0, 1), ha = max(0, max(L[p - (x > 0)], L[p + (x < 63)], L[p - (y > 0) * W], L[p + (y < 127) * W]) - l) * 160; + for (let n = 0; n < 3; n++) c3[n] += m2 * (V[p] * (PHASE[n] * dk + ha + ((hh >>> (n * 8 + 3)) & 15)) - c3[n]); + } + L[p] = l; + display.setPixel(x, y, min(255, c3[0]) | 0, min(255, c3[1]) | 0, min(255, c3[2]) | 0); } } From 89f09a9106b9ed8dc7c2c40eca80e5732b7ec6cf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:19:10 +0000 Subject: [PATCH 16/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Dark=20Field=20=EC=9D=98=20=EA=B2=B0=EC=9D=84?= =?UTF-8?q?=20=ED=94=84=EB=A0=88=EC=9E=84=EB=A7=88=EB=8B=A4=20=EC=83=88?= =?UTF-8?q?=EB=A1=9C=20=EB=BF=8C=EB=A6=AC=EC=A7=80=20=EC=95=8A=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_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/patterns/dark-field.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/incoming/pattern-study/patterns/dark-field.js b/incoming/pattern-study/patterns/dark-field.js index 9f360e2e8..056e7051a 100644 --- a/incoming/pattern-study/patterns/dark-field.js +++ b/incoming/pattern-study/patterns/dark-field.js @@ -123,7 +123,7 @@ const LUT = Float32Array.from({ length: 1024 }, (_, i) => { export function setup(params) { const F = (n) => new Float32Array(n), side = () => (rnd() < 0.5 ? -1 : 1), A = (n, f) => Array.from({ length: n }, (_, i) => ({ x: rr(-12, 76), y: rr(-12, 140), h: rnd() * TAU, ...f(i) })); - const P = (params.s = { t: 0, fr: 0, S: F(N * 3), Q: F(HW * HH * 3), T: F(HW * HH * 3), QS: F(HW * HH), L: F(N), V: F(N), OP: F(140), nx: 0, ny: 0, ex: 0, ey: 0, nt: 3.2, ja: 0, jt: 9, jx: 0, jy: 0, s1: rnd() * 7, s2: rnd() * 7 }); + const P = (params.s = { t: 0, S: F(N * 3), Q: F(HW * HH * 3), T: F(HW * HH * 3), QS: F(HW * HH), L: F(N), V: F(N), OP: F(140), nx: 0, ny: 0, ex: 0, ey: 0, nt: 3.2, ja: 0, jt: 9, jx: 0, jy: 0, s1: rnd() * 7, s2: rnd() * 7 }); // The round field stop of the eyepiece clips the panel's two ends. for (let p = 0; p < N; p++) P.V[p] = sm(67.5, 62, sqrt(((p & 63) - 31.5) ** 2 + (((p >> 6) - 63.5) * 1.02) ** 2)); // Volvox: cells on a sphere (a Fibonacci spiral) and daughter colonies inside. @@ -465,15 +465,17 @@ export function draw(display, params) { show(P.bac, (o) => (dep(o.z), dot(o.x, o.y, o.s * o.p))); blur(Q, P.T); // Light into LEDs, toned by the brightest channel so a green stays green, - // each lit LED scintillating a little, and under it all the shimmer of - // particles too small to resolve, below 24/255. - const fr = (P.fr = (P.fr + 1) | 0), m2 = P.m2, { V, L, QS } = P, c3 = K0; + // each lit LED with a grain of its own, and under it all the still grain + // of particles too small to resolve, below 24/255. The grain stays put: + // drawn anew every frame it was static, and it took the motion from 1.38 + // to 2.31. + const m2 = P.m2, { V, L, QS } = P, c3 = K0; for (let i = 0; i < HW * HH; i++) QS[i] = Q[i * 3] + Q[i * 3 + 1] + Q[i * 3 + 2]; for (let y = 0, p = 0; y < H; y++) for (let x = 0; x < W; x++, p++) { // The halo cell under the LED and its neighbours toward it, bilinear. const o = p * 3, i = x >> 1, j = (y >> 1) * HW, i2 = cl(i + (x & 1) * 2 - 1, 0, HW - 1), j2 = cl((y >> 1) + (y & 1) * 2 - 1, 0, HH - 1) * HW; - const A = j + i, B = j + i2, c = j2 + i, e = j2 + i2, hl = QS[A] + QS[B] + QS[c] + QS[e] > 0, hh = h32(p, fr), sh = hh >>> 24; + const A = j + i, B = j + i2, c = j2 + i, e = j2 + i2, hl = QS[A] + QS[B] + QS[c] + QS[e] > 0, hh = h32(p, 7), sh = hh >>> 24; for (let n = 0; n < 3; n++) c3[n] = S[o + n] + (hl ? 0.17 * Q[A * 3 + n] + 0.056 * (Q[B * 3 + n] + Q[c * 3 + n]) + 0.019 * Q[e * 3 + n] : 0); const mx = max(c3[0], c3[1], c3[2]), k = mx > 0.001 ? (LUT[min(1023, (mx * 256) | 0)] / mx) * (0.8 + sh / 640) : 0, l = (mx * k) / 255; for (let n = 0, hb; n < 3; n++) (hb = (hh >>> (n * 8)) & 255), (c3[n] = V[p] * (c3[n] * k * (1 + hb / 2200) + sh * (0.062 + (hb & 15) / 500) + m1 * FIELD[n])); From f520fd265a383446851275667ff716916c9dd18b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:19:41 +0000 Subject: [PATCH 17/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Shadow=20Play?= 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_01BvYAVbR9mNHMMAp7tN8evZ --- .../pattern-study/patterns/shadow-play.js | 408 ++++++++++++++++++ 1 file changed, 408 insertions(+) create mode 100644 incoming/pattern-study/patterns/shadow-play.js diff --git a/incoming/pattern-study/patterns/shadow-play.js b/incoming/pattern-study/patterns/shadow-play.js new file mode 100644 index 000000000..55669a110 --- /dev/null +++ b/incoming/pattern-study/patterns/shadow-play.js @@ -0,0 +1,408 @@ +// ===== Patternflow pattern ===== +// Title: Shadow Play +// Family: stage +// Idea: A wayang kulit night seen from the audience: an oil lamp behind the white screen throws the lacy leather shadows of a knight and an ogre who meet, threaten, fight until the ogre is flung away, and then the leaf-shaped gunungan sweeps in trembling to close the scene and open the next. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 26, after the Javanese wayang kulit shadow play +// Round: 26 +// =============================== +// @matrix 64x128 +// @knobs View=0..1, Speed=0.2..3, Lamp=0..1, Flicker=0..1 +// +// Knob 1: View. Where you sit. At 0 in front of the screen: black lace shadows on the warm cloth. Turned up you go round to the dalang's side, where the same puppets show their gold, red and indigo paint under the hanging lamp against the dim back of the screen. +// Knob 2: Speed. The pace of the play: at 2 a scene, from the gunungan leaving the middle to the gunungan planted there again, takes 14.4 s; at 0.2 ten times slower. +// Knob 3: Lamp. How close the blencong hangs to the screen. At 0 it is far back: sharp shadows on an evenly lit cloth. Turned up, the hot spot tightens, the corners go dark, and every shadow grows and blurs. +// Knob 4: Flicker. How much the oil flame gutters and sways. At 0.06 the light breathes; at 1 a draught makes it jump and swing, and the shadows swing and soften with it. +// Buttons: K1 tap: the dalang strikes the cempala; the knight lunges, the ogre reels and the screen shakes. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, ceil, round, sin, cos, abs, sqrt, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const mix = (a, b, t) => a + (b - a) * t; +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +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; +} + +// Rows: the top beam, the cloth, the line the puppets stand on, the hem, the banana trunk, the first row the audience can reach. +const BEAM = 3, CLOTH = 10, GROUND = 92, HEM = 103, TRUNK = 104, AUD = 88; +// One scene in story seconds (real seconds at Speed 2). +const SCENE = 14.4; +// The paint, seen from the dalang's side. +const GOLD = [0.95, 0.72, 0.26], RED = [0.86, 0.2, 0.13], INDIGO = [0.22, 0.3, 0.8], TEAL = [0.1, 0.52, 0.48], HAIR = [0.13, 0.1, 0.15]; +const HORN = [0.32, 0.2, 0.11], BAMBOO = [0.62, 0.5, 0.3], IVORY = [0.96, 0.92, 0.82]; +// Lace: the punched holes, as a lattice in puppet space: spacing along, spacing across, laid diagonally. +const LA = [0, 2.5, 2.0, 1.9, 2.3], LB = [0, 2.5, 3.6, 1.9, 2.3], LD = [0, 0, 1, 0, 0], LR = [0, 0.9, 0.9, 0.8, 1.15]; + +// Knight and ogre outlines in puppet space: x forward, y up from the soles. +const K_TORSO = [-5.2, 40.4, -3.6, 41.6, 3.2, 41.6, 4.6, 40.4, 3.4, 35, 1.9, 28.5, -2.1, 28.5, -3.6, 35]; +const K_KAIN = [-3.2, 28.5, 3.2, 28.5, 4.4, 20, 3.4, 13, 1.6, 5, -0.6, 13, -4.5, 16, -9.8, 9.5, -5.8, 22]; +const K_NOSE = [3.8, 49.6, 8.6, 45.2, 4.6, 46.4]; +const O_TORSO = [-8.2, 47, -6.5, 48.4, 6, 48.4, 7.8, 46.5, 7, 38, 5.5, 31, -5.5, 31, -7.5, 39]; +const O_KAIN = [-6.2, 31.5, 6.2, 31.5, 8.8, 21.5, 2, 17, -4, 19.5, -10, 23]; +const O_CROWN = [-0.5, 58.5, 6.2, 58.5, 5.4, 62, 2.8, 67, 0.2, 61.8]; + +// Seconds of a scene: when each puppet enters, and where it goes. +const KX = [1.7, -16, 3.6, 18, 5.6, 14, 6, 23, 6.4, 14, 6.8, 17, 7.2, 13, 7.6, 24, 8, 16, 8.5, 20, 9, 22, 10.8, 28, 12.3, 86]; +const OX = [2.3, 84, 4.2, 47, 5.6, 50, 6, 47, 6.4, 49, 6.8, 40, 7.2, 50, 7.6, 47, 8, 39, 8.5, 42, 9, 40]; +function key(t, k) { + if (t <= k[0]) return k[1]; + for (let i = 2; i < k.length; i += 2) if (t < k[i]) return mix(k[i - 1], k[i + 1], sm(k[i - 2], k[i], t)); + return k[k.length - 1]; +} +const pose = (hc, sz) => ({ on: false, x: 32, y: GROUND, z: 0.02, rot: 0, face: 1, a1: 0, a2: 0, b1: 0, b2: 0, jaw: 0, hc, sz }); + +export function setup(params) { + const R = () => Math.random() * TAU; + const P = (params.s = { + M: new Float32Array(N), A: new Float32Array(N), C: new Float32Array(N * 3), F: new Float32Array(N * 3), + BG: new Float32Array(N * 3), PT: new Uint8Array(N), LW: new Float32Array(81), HB: new Float32Array((H - AUD) * W), + t: 0, rt: 0, view: 0, lamp: 0, fl: 0.06, knock: -1, ph: [R(), R(), R(), R(), R(), R(), R()], dir: Math.random() < 0.5 ? 1 : -1, + K: pose(28, 1.22), O: pose(32, 1.12), G: pose(0, 1.04), I: 1, bil: 0, LX: 31.5, LY: 46, flame: 7, shx: 0, shy: 0, + }); + // Half the gunungan's width at each height: a leaf with a pointed tip. + for (let v = 0; v <= 80; v++) P.LW[v] = v > 78 ? 0 : v < 22 ? 18 * sqrt(1 - ((22 - v) / 31) ** 2) : 18 * (1 - ((v - 22) / 56) ** 1.7) ** 1.15; + // What does not move, as colour before the light: beam, border, posts, cloth, hem, trunk, floor. + const B = P.BG; + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, o = i * 3, n = hash(x, y); + let c = [0.02, 0.018, 0.03], t = 1; + if (y >= BEAM && y < BEAM + 4) { + const g = [0.55, 0.85, 1, 0.65][y - BEAM] * ((x + (y & 1) * 2) % 5 === 0 ? 0.55 : 0.9 + 0.1 * n); + c = [0.5 * g, 0.31 * g, 0.16 * g]; + } else if (y >= BEAM + 4 && y < CLOTH) c = y === CLOTH - 1 ? (x & 1 ? [0.85, 0.62, 0.24] : [0.3, 0.07, 0.06]) : [0.34, 0.07, 0.06].map((v) => v * (0.85 + 0.15 * n)); + else if (y >= CLOTH && y < HEM && (x < 2 || x > 61)) { + const g = x === 1 || x === 62 ? 0.75 : 0.42; + c = [0.46 * g, 0.28 * g, 0.15 * g]; + } else if (y >= CLOTH && y < HEM) (t = 0), (c = [0.955 + 0.045 * n - (y === 57 ? 0.07 : y === 58 ? 0.025 : 0) - (x & 1 ? 0.012 : 0), 0, 0]); + else if (y === HEM) c = [0.3, 0.06, 0.05]; + else if (y >= TRUNK && y < TRUNK + 8) { + const g = [0.7, 1, 0.95, 0.85, 0.72, 0.58, 0.44, 0.3][y - TRUNK] * (0.82 + 0.18 * hash(floor(x / 4) + y * 17, y)); + (t = 2), (c = [0.6 * g, 0.64 * g, 0.4 * g]); + } else if (y >= TRUNK + 8) c = [0.05, 0.035, 0.03].map((v) => v * (1 - (y - TRUNK - 8) / 20)); + (B[o] = c[0]), (B[o + 1] = c[1]), (B[o + 2] = c[2]), (P.PT[i] = t); + } +} + +// The knight's scene, in story seconds, for a scene that runs left to right. +function knight(t, Q, rt, kb) { + Q.on = t > 1.7 && t < 12.3; + Q.x = key(t, KX); + const v = (key(t + 0.05, KX) - key(t - 0.05, KX)) * 10, walk = cl(abs(v) / 12, 0, 1), st = sin(PI * t * 2.6); + const fight = sm(5.5, 5.8, t) * (1 - sm(8.6, 8.9, t)), w = TAU * 1.6 * (t - 5.6), reach = fight * cl((Q.x - 14) / 9, 0, 1); + const point = sm(3.7, 4.1, t) * (1 - sm(4.8, 5.2, t)), heave = sm(8.5, 9.1, t) * (1 - sm(9.7, 10.5, t)); + Q.y = GROUND - walk * 1.8 * abs(st); + Q.z = 0.02; + Q.rot = -0.05 * walk + fight * 0.06 * sin(w + 0.5) + heave * 0.16 - 0.14 * kb; + Q.a1 = 0.12 + 0.3 * walk * st + 1.45 * point + fight * 0.7 + 1.5 * reach + 2.5 * heave + 1.4 * kb; + Q.a2 = 0.15 + 0.35 * point + fight * 0.9 - 0.7 * reach + 0.2 * heave; + Q.b1 = -0.12 + 0.06 * sin(rt * 1.7) - 0.25 * walk * st + fight * 0.3 * sin(w + 2); + Q.b2 = 0.2; +} + +// The ogre: in from the other side, a looming threat, the fight, the throw. +function ogre(t, Q, rt, kb) { + Q.on = t > 2.3 && t < 10.6; + const u = cl((t - 9.2) / 1.4, 0, 1); + Q.x = key(t, OX) + 30 * u; + const v = (key(t + 0.05, OX) - key(t - 0.05, OX)) * 10, walk = cl(abs(v) / 12, 0, 1) * (1 - u), st = sin(PI * t * 2.1); + const fight = sm(5.5, 5.8, t) * (1 - sm(8.6, 8.9, t)), w = TAU * 1.6 * (t - 5.6), reach = fight * cl((50 - Q.x) / 10, 0, 1); + const loom = sm(4.3, 4.9, t) * (1 - sm(4.95, 5.5, t)); + Q.y = GROUND - walk * 2.6 * abs(st) - 115 * u * u; + Q.z = 0.02 + 0.16 * loom + 0.28 * u; + Q.rot = -0.04 * walk + 0.07 * walk * st - 0.1 * loom + fight * 0.07 * sin(w + 3.6) + 5.6 * u + 0.35 * kb; + Q.a1 = 0.3 + 0.25 * walk * st + 2.0 * loom + fight * (0.6 + 0.3 * sin(w)) + 1.6 * reach + 1.2 * u; + Q.a2 = 0.35 + 0.5 * loom + fight * 0.6 - 0.5 * reach; + Q.b1 = -0.4 - 0.3 * walk * st + 0.5 * loom; Q.b2 = 0.5; + Q.jaw = 0.08 + loom * (0.25 + 0.15 * sin(rt * 14)) + fight * 0.25 * (0.5 + 0.5 * sin(w * 2)) + 0.45 * u + 0.4 * kb; +} + +// The gunungan: planted in the middle, pulled away trembling, back at the end. +function gunungan(t, Q, rt, kb) { + Q.on = t < 3.3 || t > 10.5; + let e, mv; + if (t < 7) (e = cl((t - 1.4) / 1.9, 0, 1) ** 2), (Q.x = 32 + 58 * e), (mv = sin(PI * cl((t - 1.4) / 1.9, 0, 1))); + else (e = 1 - sm(10.5, 12.6, t)), (Q.x = 32 - 58 * e), (mv = sin(PI * e)); + Q.y = 98 - 10 * mv; + Q.z = 0.02 + 0.3 * e; + Q.rot = -0.45 * e + (0.012 + 0.06 * mv + 0.12 * kb) * sin(rt * TAU * 8.5) + 0.01 * sin(rt * 2.3); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 0.06], rd = cl(dt || 0, 0, 0.05); + (P.view = cl(k[0], 0, 1)), (P.lamp = cl(k[2], 0, 1)), (P.fl = cl(k[3], 0, 1)); + (P.rt += rd), (P.t += (rd * cl(k[1], 0.2, 3)) / 2); + if (input.btnPressed && input.btnPressed[0]) P.knock = 0; + if (P.knock >= 0 && (P.knock += rd) > 1.2) P.knock = -1; + const kn = P.knock < 0 ? 9 : P.knock, kb = kn < 0.45 ? sin((PI * kn) / 0.45) : 0, kr = sin(PI * cl((kn - 0.1) / 0.5, 0, 1)), sh = kn < 0.4 ? 1 - kn / 0.4 : 0; + // The flame: a breathing flicker, now and then a gutter, a sway that moves the light. + const f = P.fl, rt = P.rt, ph = P.ph, amp = 0.035 + 0.2 * f; + const n = 0.5 * sin(7.3 * rt + ph[0]) + 0.3 * sin(12.9 * rt + ph[1]) + 0.2 * sin(23.1 * rt + ph[2]), gut = max(0, sin(1.7 * rt + ph[3]) + 0.6 * sin(3.1 * rt + ph[6])) ** 3 * 0.25 * f; + P.I = 1 - 0.12 * f + amp * n - 0.35 * gut + 0.2 * sh; + // A draught also bellies the cloth away from the puppets, so their shadows swell and soften. + P.bil = 0.07 * f * (0.6 + 0.4 * sin(0.9 * rt + ph[5])); + P.LX = 31.5 + (1 + 11 * f) * (0.65 * sin(2.1 * rt + ph[4]) + 0.35 * sin(0.83 * rt + ph[5])) + 2 * amp * sin(9.7 * rt + ph[6]); + P.LY = 46 - 3 * gut - 3 * f * sin(1.3 * rt + ph[2]); + P.flame = 7 * (1 + 1.5 * f) * (1 + 0.8 * amp * sin(11.3 * rt + ph[1]) + 0.6 * gut); + (P.shx = 1.3 * sh * sin(kn * 70)), (P.shy = 0.9 * sh * sin(kn * 53 + 1)); + // The play. + const sc = floor(P.t / SCENE), t = P.t - sc * SCENE, dir = sc & 1 ? -P.dir : P.dir; + (P.K.face = dir), (P.O.face = -dir), (P.G.face = dir); + knight(t, P.K, rt, kb), ogre(t, P.O, rt, kr), gunungan(t, P.G, rt, sh); + (P.K.x += 7 * kb), (P.O.x += 6 * kr); + for (const Q of [P.K, P.O, P.G]) if (dir < 0) Q.x = 63 - Q.x; +} + +// The puppet being drawn: screen from puppet space and back, its magnification and blur. +let M, A, C, TX, TY, F11, F12, F21, F22, I11, I12, I21, I22, MAG, PEN, HV, LC; +function place(P, Q, D) { + const f = cl((Q.z + P.bil) / D, 0, 0.62), m = 1 / (1 - f), c = cos(Q.rot), s = sin(Q.rot), fc = Q.face, k = m * Q.sz, hc = Q.hc * Q.sz; + (MAG = k), (PEN = 1 + (P.flame * f) / (1 - f)), (HV = cl(1.25 - 0.42 * PEN, 0, 0.85)); + (F11 = k * fc * c), (F12 = -k * fc * s), (F21 = -k * s), (F22 = -k * c); + TX = P.LX * (1 - m) + m * (Q.x + fc * s * hc) + P.shx; + TY = P.LY * (1 - m) + m * (Q.y - hc + c * hc) + P.shy; + const det = F11 * F22 - F12 * F21; + (I11 = F22 / det), (I12 = -F12 / det), (I21 = -F21 / det), (I22 = F11 / det); +} + +function holes(u, v, k) { + let a = u, b = v; + if (LD[k]) (a = (u + v) * 0.7071), (b = (v - u) * 0.7071); + if (k === 4) (a += 0.8 * sin(v * 0.6 + abs(u) * 0.35)), (b += 0.5 * sin(abs(u) * 0.7)); + const pa = LA[k], pb = LB[k], j = round(b / pb), aa = a + (j & 1) * pa * 0.5, du = aa - round(aa / pa) * pa, dv = b - j * pb; + return cl((LR[k] - sqrt(du * du + dv * dv)) * 1.15 + 0.1, 0, 1); +} + +// The gunungan's inside: rim, gate, tree, the two beasts, and lace everywhere else. +let LL; +function leaf(u, v, hw) { + const au = abs(u); + (LL = 0), (LC = GOLD); + if (hw - au < 1.4 || v < 1.3) return (LC = RED), 1; + if (v < 15 && au < 8) { + if (v < 9) return (LC = RED), au > 1 && au < 3.3 && v > 2.2 && v < 7.6 ? 0.12 : 1; + return au < 8 - (v - 9) * 1.15 ? 1 : ((LL = 4), (LC = TEAL), 1); + } + if (au < 1.9 - v * 0.012 && v < 70) return 1; + for (let b = 0; b < 6; b++) { + const vb = 19 + 8 * b + 0.5 * au + 0.02 * au * au; + if (abs(v - vb) < 1.05 && au < min(hw - 2.4, 15 - b)) return 1; + } + const bx = (au - 6.5) / 3.4, by = (v - 17.5) / 1.9; + if (bx * bx + by * by < 1) return (LC = HORN), 1; + return (LL = 4), (LC = TEAL), 1; +} + +// One part: kind 0 a tapered rod (x0, y0, x1, y1, r0, r1), 1 an ellipse (cx, cy, rx, ry, turn), 2 a polygon, 3 the gunungan's leaf. +// lace picks the holes; a part with no colour is a hole cut through what is already there (lace is then how deep). +function part(k, a, b, c, d, e, f, lace, col) { + let u0, u1, v0, v1, rp = 99; + const ce = cos(e || 0), se = sin(e || 0); + if (k === 0) { + const r = max(e, f); + (u0 = min(a, c) - r), (u1 = max(a, c) + r), (v0 = min(b, d) - r), (v1 = max(b, d) + r), (rp = r * MAG); + } else if (k === 1) { + const r = max(c, d); + (u0 = a - r), (u1 = a + r), (v0 = b - r), (v1 = b + r), (rp = min(c, d) * MAG); + } else if (k === 2) { + (u0 = v0 = 1e9), (u1 = v1 = -1e9); + for (let i = 0; i < a.length; i += 2) (u0 = min(u0, a[i])), (u1 = max(u1, a[i])), (v0 = min(v0, a[i + 1])), (v1 = max(v1, a[i + 1])); + } else (u0 = -19), (u1 = 19), (v0 = 0), (v1 = 79); + let x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9; + for (let q = 0; q < 4; q++) { + const u = q & 1 ? u1 : u0, v = q & 2 ? v1 : v0, X = F11 * u + F12 * v + TX, Y = F21 * u + F22 * v + TY; + (x0 = min(x0, X)), (x1 = max(x1, X)), (y0 = min(y0, Y)), (y1 = max(y1, Y)); + } + const pad = PEN * 0.5 + 0.5, thin = min(1, (2 * rp + 1) / PEN), abx = c - a, aby = d - b, ab2 = abx * abx + aby * aby || 1e-9; + const xa = max(0, floor(x0 - pad)), xb = min(W - 1, ceil(x1 + pad)), ya = max(0, floor(y0 - pad)), yb = min(H - 1, ceil(y1 + pad)); + for (let y = ya; y <= yb; y++) + for (let x = xa; x <= xb; x++) { + const dx = x - TX, dy = y - TY, u = I11 * dx + I12 * dy, v = I21 * dx + I22 * dy; + let dd, op = 1, ln = lace, cc = col; + if (k === 0) { + const t = cl(((u - a) * abx + (v - b) * aby) / ab2, 0, 1), qx = u - a - abx * t, qy = v - b - aby * t; + dd = sqrt(qx * qx + qy * qy) - mix(e, f, t); + } else if (k === 1) { + const pu = u - a, pv = v - b, lu = (pu * ce + pv * se) / c, lv = (pv * ce - pu * se) / d; + dd = (sqrt(lu * lu + lv * lv) - 1) * min(c, d); + } else if (k === 2) { + let md = 1e9, ins = false; + for (let i = 0, j = a.length - 2; i < a.length; j = i, i += 2) { + const xi = a[i], yi = a[i + 1], ex = a[j] - xi, ey = a[j + 1] - yi; + if (yi > v !== a[j + 1] > v && u < (ex * (v - yi)) / ey + xi) ins = !ins; + const s = cl(((u - xi) * ex + (v - yi) * ey) / (ex * ex + ey * ey), 0, 1), qx = u - xi - ex * s, qy = v - yi - ey * s; + md = min(md, qx * qx + qy * qy); + } + dd = ins ? -sqrt(md) : sqrt(md); + } else { + const vi = cl(v, 0, 79.9), v0i = floor(vi), hw = mix(LL_W[v0i], LL_W[v0i + 1], vi - v0i); + dd = max(abs(u) - hw, -v, v - 78); + if (dd < 0.6) (op = leaf(u, v, hw)), (ln = LL), (cc = LC); + } + const dp = dd * MAG; + if (dp > pad) continue; + let cs = cl(0.5 - dp / PEN, 0, 1) * thin, cv = cl(0.5 - dp, 0, 1); + const i = y * W + x, o = i * 3; + if (!cc) { + const dp = lace < 0 ? 0.9 * holes(u, v, -lace) : lace, s = 1 - cs * dp * min(1, HV * 1.3), sv = 1 - cv * dp; + (M[i] *= s), (A[i] *= sv), (C[o] *= sv), (C[o + 1] *= sv), (C[o + 2] *= sv); + continue; + } + if (ln) { + const h = holes(u, v, ln); + (cs *= 1 - h * HV), (cv *= 1 - h * 0.85); + } + cs *= op; + cv *= op; + if (cs > M[i]) M[i] = cs; + if (cv > 0) (C[o] += (cc[0] - C[o]) * cv), (C[o + 1] += (cc[1] - C[o + 1]) * cv), (C[o + 2] += (cc[2] - C[o + 2]) * cv), (A[i] += (1 - A[i]) * cv); + } +} +let LL_W; +const rod = (x0, y0, x1, y1, r0, r1, lace, col) => part(0, x0, y0, x1, y1, r0, r1, lace, col); +const ell = (cx, cy, rx, ry, turn, lace, col) => part(1, cx, cy, rx, ry, turn, 0, lace, col); +const poly = (pts, lace, col) => part(2, pts, 0, 0, 0, 0, 0, lace, col); + +// An arm on its two joints, the pointing hand, and the thin rod down to the dalang's fist. +function arm(sx, sy, a1, a2, l1, l2, r1, r2, col) { + const ex = sx + l1 * sin(a1), ey = sy - l1 * cos(a1), hx = ex + l2 * sin(a1 + a2), hy = ey - l2 * cos(a1 + a2); + rod(sx, sy, ex, ey, r1, r1 * 0.85, 0, col), rod(ex, ey, hx, hy, r2 * 0.95, r2 * 0.75, 0, col); + rod(hx, hy, hx + 2.4 * sin(a1 + a2 + 0.25), hy - 2.4 * cos(a1 + a2 + 0.25), r2 * 0.75, 0.3, 0, col); + rod(ex - 0.2, ey, ex + 0.2, ey, r1 * 1.15, r1 * 1.15, 0, GOLD); + const gx = 2 - hx, gy = -14 - hy, l = sqrt(gx * gx + gy * gy) || 1, L = min(l, 30) / l; + rod(hx, hy, hx + gx * L, hy + gy * L, 0.42, 0.42, 0, BAMBOO); +} + +function drawKnight(Q) { + rod(0.4, 34, 0.4, -16, 0.5, 0.45, 0, HORN); + arm(-4.6, 39.4, Q.b1, Q.b2, 12, 12.5, 1.0, 0.85, GOLD); + rod(-1, 17, -2.6, 1.5, 1.15, 0.95, 0, GOLD), rod(-2.6, 0.8, 1.2, 0.6, 0.8, 0.7, 0, GOLD); + rod(1.5, 17, 3.6, 1.5, 1.15, 0.95, 0, GOLD), rod(3.6, 0.8, 7.6, 0.6, 0.8, 0.7, 0, GOLD); + poly(K_KAIN, 2, INDIGO); + rod(3, 27, 5.4, 11, 0.8, 0.45, 0, RED); + poly(K_TORSO, 0, GOLD); + rod(-2.6, 29, 2.4, 29, 1.1, 1.1, 0, RED); + rod(-2.2, 33, 2, 33, 0.9, 0.9, -1, null); + rod(0.5, 40.5, 1.8, 44, 1.3, 1.1, 0, GOLD); + ell(-1.6, 46, 1.1, 2.6, -0.5, 1, GOLD); + ell(2.2, 47, 2.8, 3.6, 0.35, 0, GOLD); + poly(K_NOSE, 0, GOLD); + rod(1.5, 50.5, -2.5, 53.4, 1.6, 1.4, 3, HAIR), rod(-2.5, 53.4, -4.8, 50.2, 1.4, 0.6, 3, HAIR); + rod(-0.5, 50, 4.2, 50.6, 0.7, 0.6, 0, GOLD); + rod(3.6, 48, 5.1, 47.6, 0.38, 0.38, 0.9, null); + rod(-3.4, 38.6, 0.3, 36.6, 0.75, 0.75, -3, null), rod(0.3, 36.6, 3.4, 38.6, 0.75, 0.75, -3, null); + arm(3.6, 39.4, Q.a1, Q.a2, 12, 12.5, 1.0, 0.85, GOLD); +} + +function drawOgre(Q) { + rod(0.4, 40, 0.4, -16, 0.55, 0.5, 0, HORN); + arm(-7.5, 46, Q.b1, Q.b2, 12.5, 13, 1.6, 1.3, RED); + rod(-3, 27, -7, 15, 2, 1.6, 0, RED), rod(-7, 15, -7.5, 2, 1.6, 1.3, 0, RED), rod(-8.1, 1, -3.6, 0.8, 1.1, 1, 0, RED); + rod(3, 27, 7, 15, 2, 1.6, 0, RED), rod(7, 15, 7.5, 2, 1.6, 1.3, 0, RED), rod(7.5, 1, 12, 0.8, 1.1, 1, 0, RED); + poly(O_KAIN, 2, TEAL); + rod(5, 29, 8, 10, 1, 0.6, 0, GOLD); + poly(O_TORSO, 0, RED), ell(3.6, 37, 4.8, 5.4, 0, 0, RED); + rod(-5.5, 45.6, 0.5, 42.5, 1, 1, -3, null), rod(0.5, 42.5, 6, 45.6, 1, 1, -3, null), ell(-1.5, 38, 3, 4, 0.2, -1, null); + rod(-6, 31.2, 6, 31.2, 1.2, 1.2, 0, GOLD); + ell(-3.5, 54, 5.5, 7.5, 0.35, 3, HAIR); + rod(-6, 59, -10.5, 63, 1.3, 0.3, 0, HAIR), rod(-8, 52, -13, 50, 1.3, 0.3, 0, HAIR), rod(-6.5, 47.5, -10.5, 43, 1.1, 0.3, 0, HAIR); + rod(0.5, 47, 2, 51, 2.5, 2.2, 0, RED); + ell(3.2, 55.3, 5, 4.4, 0, 0, RED), ell(9.3, 54.8, 2.4, 2.0, 0, 0, RED); + const j = Q.jaw, jx = 3.5 + 6.3 * cos(j), jy = 50.6 - 6.3 * sin(j); + rod(3.5, 50.6, jx, jy, 1.3, 1.0, 0, RED), rod(jx - 0.9, jy + 0.5, jx - 0.5, jy + 2.7, 0.55, 0.15, 0, IVORY); + rod(7.6, 51.3, 7.9, 49.3, 0.5, 0.15, 0, IVORY); + poly(O_CROWN, 1, GOLD); + ell(-0.5, 53, 1.5, 3, -0.4, 1, GOLD); + rod(6.3, 56.4, 6.3, 56.4, 1.5, 1.5, 0.9, null); + rod(6.6, 56.4, 6.6, 56.4, 0.6, 0.6, 0, HAIR); + arm(6, 46, Q.a1, Q.a2, 12.5, 13, 1.6, 1.3, RED); +} + +function drawGunungan() { + rod(0, 6, 0, -12, 0.7, 0.6, 0, HORN); + part(3, 0, 0, 0, 0, 0, 0, 0, GOLD); +} + +// A soft-edged ellipse painted over the frame, or added as glow. +function blob(F, cx, cy, rx, ry, col, a, add) { + for (let y = max(0, floor(cy - ry - 1)); y <= min(H - 1, ceil(cy + ry + 1)); y++) + for (let x = max(0, floor(cx - rx - 1)); x <= min(W - 1, ceil(cx + rx + 1)); x++) { + const ex = (x - cx) / rx, ey = (y - cy) / ry, d = sqrt(ex * ex + ey * ey), o = (y * W + x) * 3; + const c = add ? max(0, 1 - d) ** 2 * a : cl((1 - d) * min(rx, ry) + 0.5, 0, 1) * a; + for (let j = 0; j < 3; j++) F[o + j] = add ? F[o + j] + col[j] * c : mix(F[o + j], col[j], c); + } +} + +// The audience: heads and shoulders against the light, [x, top, rx, ry, kind]: bare, a peci cap, a headscarf. +const HEADS = [[6, 98, 4.6, 5.6, 1], [19, 95, 5, 6, 0], [33, 99.5, 4.4, 5.4, 2], [46, 96, 5, 6.2, 1], [58.5, 98.5, 4.6, 5.6, 0]]; +// Their outline, as coverage in rows AUD and down: head, ears or scarf, neck, shoulders. +function audience(HB, rt) { + HB.fill(0); + for (let j = 0; j < 5; j++) { + const [hx, ht, rx, ry, kd] = HEADS[j], cx = hx + 0.6 * sin(rt * 0.45 + j * 1.7), top = ht + 0.4 * sin(rt * 0.7 + j * 2.3), cy = top + ry, sr = rx * 2.6; + for (let y = floor(top - 3); y < H; y++) + for (let x = max(0, floor(cx - sr)); x <= min(W - 1, ceil(cx + sr)); x++) { + const ax = abs(x - cx), ex = ax / (rx + (kd === 2 ? 0.8 : 0)), ey = (y - cy) / ry, sx = ax / sr, sy = (y - top - 2 * ry - 11) / 9; + let d = min((sqrt(ex * ex + ey * ey) - 1) * rx, (sqrt(sx * sx + sy * sy) - 1) * 9); + if (kd === 1) d = min(d, max(ax - rx * (0.86 + 0.04 * (y - top)), abs(y - top + 0.2) - 2.3)); + if (kd === 2) d = min(d, max(ax - rx * (0.9 + (y - cy) * 0.09), cy - y)); + else d = min(d, (sqrt((ax - rx) ** 2 + (y - cy - 0.6) ** 2) - 1.1) * 1.5, max(ax - rx * 0.62, cy - y)); + const i = (y - AUD) * W + x; + HB[i] = max(HB[i], cl(0.5 - d, 0, 1)); + } + } +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + (M = P.M), (A = P.A), (C = P.C), (LL_W = P.LW); + M.fill(0), A.fill(0), C.fill(0); + const D = 1 - 0.55 * P.lamp; + if (P.K.on) place(P, P.K, D), drawKnight(P.K); + if (P.O.on) place(P, P.O, D), drawOgre(P.O); + if (P.G.on) place(P, P.G, D), drawGunungan(); + const F = P.F, B = P.BG, PT = P.PT, I = P.I, LX = P.LX, LY = P.LY, R2 = (46 * D) ** 2, vw = sm(0, 1, P.view), rd = 0.12 * (1 - P.I) * P.fl, top = I * 228 * (0.95 + 0.05 / D); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x, o = i * 3, dx = x - LX, dy = y - LY, fo = 1 / (1 + (dx * dx + dy * dy) / R2), pt = PT[i]; + if (pt === 0) { + // The cloth: lamp light through cotton, the shadows on it; or from behind, the paint over the dim cloth. + const L = top * (0.2 + 0.8 * fo) * B[o], lr = L, lg = L * (0.7 + 0.18 * fo - rd), lb = L * (0.36 + 0.3 * fo - rd), s = 1 - 0.94 * M[i]; + let r = lr * s, g = lg * s, b = lb * s; + if (vw > 0) { + const k = 0.3 * (1 - A[i]), lit = I * (0.5 + 0.55 * fo) * 255; + (r = mix(r, lr * k + C[o] * lit, vw)), (g = mix(g, lg * k + C[o + 1] * lit, vw)), (b = mix(b, lb * k + C[o + 2] * lit, vw)); + } + (F[o] = r), (F[o + 1] = g), (F[o + 2] = b); + } else { + const s = I * (0.3 + 0.7 * fo) * 255 * (pt === 2 ? 1 - 0.85 * M[i] : 1); + (F[o] = B[o] * s), (F[o + 1] = B[o + 1] * s), (F[o + 2] = B[o + 2] * s); + } + } + // From the dalang's side the blencong hangs in view: a bronze garuda on a chain, the flame at its beak. + if (vw > 0.01) { + const bx = LX + 2, fl = P.flame / 7; + for (let y = 0; y < 11; y++) blob(F, round(bx), y, 0.6, 0.6, y & 1 ? [90, 62, 30] : [150, 105, 50], vw); + blob(F, bx + 6, 10.5, 2.2, 3.2, [150, 96, 36], vw), blob(F, bx, 13, 5, 2.2, [170, 112, 42], vw), blob(F, bx - 4.2, 11, 1.4, 1.6, [200, 140, 60], vw); + blob(F, bx - 6.2, 12.8 - fl, 1.6 * fl, 6 * fl, [255, 140, 40], 0.5 * vw * I, 1), blob(F, bx - 6.2, 12.5 - fl * 0.6, 0.8, 1.9 * fl, [255, 240, 190], vw); + } + // The audience against the light, the rims of their heads lit by the screen. + const HB = P.HB; + audience(HB, P.rt); + for (let y = AUD + 2; y < H; y++) + for (let x = 0; x < W; x++) { + const k = (y - AUD) * W + x, c = HB[k]; + if (c <= 0) continue; + const o = (y * W + x) * 3, q = (min(y, HEM - 1) * W + x) * 3, side = min(x > 0 ? HB[k - 1] : 1, x < W - 1 ? HB[k + 1] : 1); + const rim = (1 - (HB[k - W] + HB[k - 2 * W]) / 2) * 0.6 + (1 - side) * 0.25; + for (let j = 0; j < 3; j++) F[o + j] = F[o + j] * (1 - c) + c * (6 + rim * [1, 0.76, 0.48][j] * max(70, F[q] * 0.9)); + } + for (let i = 0, o = 0; i < N; i++, o += 3) display.setPixel(i & 63, i >> 6, min(255, max(0, F[o])) | 0, min(255, max(0, F[o + 1])) | 0, min(255, max(0, F[o + 2])) | 0); +} From 3810e0ff06246a9eba1ebd0375066a4df793570c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:24:32 +0000 Subject: [PATCH 18/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Shadow=20Play?= 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_01BvYAVbR9mNHMMAp7tN8evZ --- .../pattern-study/patterns/shadow-play.js | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/incoming/pattern-study/patterns/shadow-play.js b/incoming/pattern-study/patterns/shadow-play.js index 55669a110..6ba6d80c9 100644 --- a/incoming/pattern-study/patterns/shadow-play.js +++ b/incoming/pattern-study/patterns/shadow-play.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Shadow Play // Family: stage -// Idea: A wayang kulit night seen from the audience: an oil lamp behind the white screen throws the lacy leather shadows of a knight and an ogre who meet, threaten, fight until the ogre is flung away, and then the leaf-shaped gunungan sweeps in trembling to close the scene and open the next. +// Idea: Wayang kulit from the audience: an oil lamp behind the white screen throws the lacy leather shadows of a knight and an ogre who meet, threaten and fight until the ogre is flung away, then the leaf-shaped gunungan sweeps in trembling to close the scene and open the next. // Author: patternflow-lab // Date: 2026-10-24 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -11,11 +11,11 @@ // @matrix 64x128 // @knobs View=0..1, Speed=0.2..3, Lamp=0..1, Flicker=0..1 // -// Knob 1: View. Where you sit. At 0 in front of the screen: black lace shadows on the warm cloth. Turned up you go round to the dalang's side, where the same puppets show their gold, red and indigo paint under the hanging lamp against the dim back of the screen. +// Knob 1: View. At 0 you sit in front of the screen: black lace shadows on warm cloth. Turned up you go round to the dalang's side, where the same puppets show their gold, red and indigo paint under the hanging lamp. // Knob 2: Speed. The pace of the play: at 2 a scene, from the gunungan leaving the middle to the gunungan planted there again, takes 14.4 s; at 0.2 ten times slower. -// Knob 3: Lamp. How close the blencong hangs to the screen. At 0 it is far back: sharp shadows on an evenly lit cloth. Turned up, the hot spot tightens, the corners go dark, and every shadow grows and blurs. -// Knob 4: Flicker. How much the oil flame gutters and sways. At 0.06 the light breathes; at 1 a draught makes it jump and swing, and the shadows swing and soften with it. -// Buttons: K1 tap: the dalang strikes the cempala; the knight lunges, the ogre reels and the screen shakes. +// Knob 3: Lamp. How close the blencong hangs to the screen. At 0 it is far back and the shadows are sharp. Turned up, the hot spot tightens, the corners go dark, and every shadow grows and blurs. +// Knob 4: Flicker. How much the oil flame gutters and sways. At 0.06 the light breathes; at 1 a draught makes it jump and swing, and the shadows soften. +// Buttons: K1 tap: the dalang strikes the cempala; the knight lunges, the ogre reels (or the gunungan shivers) and the screen shakes. const W = 64, H = 128, N = W * H; const { min, max, floor, ceil, round, sin, cos, abs, sqrt, PI } = Math; @@ -65,6 +65,8 @@ export function setup(params) { t: 0, rt: 0, view: 0, lamp: 0, fl: 0.06, knock: -1, ph: [R(), R(), R(), R(), R(), R(), R()], dir: Math.random() < 0.5 ? 1 : -1, K: pose(28, 1.22), O: pose(32, 1.12), G: pose(0, 1.04), I: 1, bil: 0, LX: 31.5, LY: 46, flame: 7, shx: 0, shy: 0, }); + // The puppets planted in the trunk at either end of the screen, only their fronts showing: an ogre king, a knight. + P.SIMP = [[-6.5, 1, 1.32, 0], [65.5, -1, 1.4, 1]].map(([x, face, sz, k]) => Object.assign(pose(30, sz), { x, face, k, y: GROUND + 4, a2: 0.1, b1: -0.1, b2: 0.15 })); // Half the gunungan's width at each height: a leaf with a pointed tip. for (let v = 0; v <= 80; v++) P.LW[v] = v > 78 ? 0 : v < 22 ? 18 * sqrt(1 - ((22 - v) / 31) ** 2) : 18 * (1 - ((v - 22) / 56) ** 1.7) ** 1.15; // What does not move, as colour before the light: beam, border, posts, cloth, hem, trunk, floor. @@ -97,7 +99,7 @@ function knight(t, Q, rt, kb) { const fight = sm(5.5, 5.8, t) * (1 - sm(8.6, 8.9, t)), w = TAU * 1.6 * (t - 5.6), reach = fight * cl((Q.x - 14) / 9, 0, 1); const point = sm(3.7, 4.1, t) * (1 - sm(4.8, 5.2, t)), heave = sm(8.5, 9.1, t) * (1 - sm(9.7, 10.5, t)); Q.y = GROUND - walk * 1.8 * abs(st); - Q.z = 0.02; + Q.z = 0.03; Q.rot = -0.05 * walk + fight * 0.06 * sin(w + 0.5) + heave * 0.16 - 0.14 * kb; Q.a1 = 0.12 + 0.3 * walk * st + 1.45 * point + fight * 0.7 + 1.5 * reach + 2.5 * heave + 1.4 * kb; Q.a2 = 0.15 + 0.35 * point + fight * 0.9 - 0.7 * reach + 0.2 * heave; @@ -114,7 +116,7 @@ function ogre(t, Q, rt, kb) { const fight = sm(5.5, 5.8, t) * (1 - sm(8.6, 8.9, t)), w = TAU * 1.6 * (t - 5.6), reach = fight * cl((50 - Q.x) / 10, 0, 1); const loom = sm(4.3, 4.9, t) * (1 - sm(4.95, 5.5, t)); Q.y = GROUND - walk * 2.6 * abs(st) - 115 * u * u; - Q.z = 0.02 + 0.16 * loom + 0.28 * u; + Q.z = 0.03 + 0.16 * loom + 0.28 * u; Q.rot = -0.04 * walk + 0.07 * walk * st - 0.1 * loom + fight * 0.07 * sin(w + 3.6) + 5.6 * u + 0.35 * kb; Q.a1 = 0.3 + 0.25 * walk * st + 2.0 * loom + fight * (0.6 + 0.3 * sin(w)) + 1.6 * reach + 1.2 * u; Q.a2 = 0.35 + 0.5 * loom + fight * 0.6 - 0.5 * reach; @@ -180,14 +182,14 @@ function holes(u, v, k) { } // The gunungan's inside: rim, gate, tree, the two beasts, and lace everywhere else. -let LL; +let LN, LW; function leaf(u, v, hw) { const au = abs(u); - (LL = 0), (LC = GOLD); + (LN = 0), (LC = GOLD); if (hw - au < 1.4 || v < 1.3) return (LC = RED), 1; if (v < 15 && au < 8) { if (v < 9) return (LC = RED), au > 1 && au < 3.3 && v > 2.2 && v < 7.6 ? 0.12 : 1; - return au < 8 - (v - 9) * 1.15 ? 1 : ((LL = 4), (LC = TEAL), 1); + return au < 8 - (v - 9) * 1.15 ? 1 : ((LN = 4), (LC = TEAL), 1); } if (au < 1.9 - v * 0.012 && v < 70) return 1; for (let b = 0; b < 6; b++) { @@ -196,7 +198,7 @@ function leaf(u, v, hw) { } const bx = (au - 6.5) / 3.4, by = (v - 17.5) / 1.9; if (bx * bx + by * by < 1) return (LC = HORN), 1; - return (LL = 4), (LC = TEAL), 1; + return (LN = 4), (LC = TEAL), 1; } // One part: kind 0 a tapered rod (x0, y0, x1, y1, r0, r1), 1 an ellipse (cx, cy, rx, ry, turn), 2 a polygon, 3 the gunungan's leaf. @@ -241,16 +243,16 @@ function part(k, a, b, c, d, e, f, lace, col) { } dd = ins ? -sqrt(md) : sqrt(md); } else { - const vi = cl(v, 0, 79.9), v0i = floor(vi), hw = mix(LL_W[v0i], LL_W[v0i + 1], vi - v0i); + const vi = cl(v, 0, 79.9), v0i = floor(vi), hw = mix(LW[v0i], LW[v0i + 1], vi - v0i); dd = max(abs(u) - hw, -v, v - 78); - if (dd < 0.6) (op = leaf(u, v, hw)), (ln = LL), (cc = LC); + if (dd < 0.6) (op = leaf(u, v, hw)), (ln = LN), (cc = LC); } const dp = dd * MAG; if (dp > pad) continue; let cs = cl(0.5 - dp / PEN, 0, 1) * thin, cv = cl(0.5 - dp, 0, 1); const i = y * W + x, o = i * 3; if (!cc) { - const dp = lace < 0 ? 0.9 * holes(u, v, -lace) : lace, s = 1 - cs * dp * min(1, HV * 1.3), sv = 1 - cv * dp; + const dep = lace < 0 ? 0.9 * holes(u, v, -lace) : lace, s = 1 - cs * dep * min(1, HV * 1.3), sv = 1 - cv * dep; (M[i] *= s), (A[i] *= sv), (C[o] *= sv), (C[o + 1] *= sv), (C[o + 2] *= sv); continue; } @@ -264,7 +266,6 @@ function part(k, a, b, c, d, e, f, lace, col) { if (cv > 0) (C[o] += (cc[0] - C[o]) * cv), (C[o + 1] += (cc[1] - C[o + 1]) * cv), (C[o + 2] += (cc[2] - C[o + 2]) * cv), (A[i] += (1 - A[i]) * cv); } } -let LL_W; const rod = (x0, y0, x1, y1, r0, r1, lace, col) => part(0, x0, y0, x1, y1, r0, r1, lace, col); const ell = (cx, cy, rx, ry, turn, lace, col) => part(1, cx, cy, rx, ry, turn, 0, lace, col); const poly = (pts, lace, col) => part(2, pts, 0, 0, 0, 0, 0, lace, col); @@ -340,6 +341,7 @@ function blob(F, cx, cy, rx, ry, col, a, add) { } // The audience: heads and shoulders against the light, [x, top, rx, ry, kind]: bare, a peci cap, a headscarf. +const RIM = [1, 0.76, 0.48]; const HEADS = [[6, 98, 4.6, 5.6, 1], [19, 95, 5, 6, 0], [33, 99.5, 4.4, 5.4, 2], [46, 96, 5, 6.2, 1], [58.5, 98.5, 4.6, 5.6, 0]]; // Their outline, as coverage in rows AUD and down: head, ears or scarf, neck, shoulders. function audience(HB, rt) { @@ -362,9 +364,10 @@ function audience(HB, rt) { export function draw(display, params) { const P = params.s; if (!P) return; - (M = P.M), (A = P.A), (C = P.C), (LL_W = P.LW); + (M = P.M), (A = P.A), (C = P.C), (LW = P.LW); M.fill(0), A.fill(0), C.fill(0); const D = 1 - 0.55 * P.lamp; + for (const S of P.SIMP) place(P, S, D), S.k ? drawKnight(S) : drawOgre(S); if (P.K.on) place(P, P.K, D), drawKnight(P.K); if (P.O.on) place(P, P.O, D), drawOgre(P.O); if (P.G.on) place(P, P.G, D), drawGunungan(); @@ -377,7 +380,7 @@ export function draw(display, params) { const L = top * (0.2 + 0.8 * fo) * B[o], lr = L, lg = L * (0.7 + 0.18 * fo - rd), lb = L * (0.36 + 0.3 * fo - rd), s = 1 - 0.94 * M[i]; let r = lr * s, g = lg * s, b = lb * s; if (vw > 0) { - const k = 0.3 * (1 - A[i]), lit = I * (0.5 + 0.55 * fo) * 255; + const k = 0.3 * (1 - A[i]), lit = I * (0.45 + 0.5 * fo) * 250; (r = mix(r, lr * k + C[o] * lit, vw)), (g = mix(g, lg * k + C[o + 1] * lit, vw)), (b = mix(b, lb * k + C[o + 2] * lit, vw)); } (F[o] = r), (F[o + 1] = g), (F[o + 2] = b); @@ -402,7 +405,7 @@ export function draw(display, params) { if (c <= 0) continue; const o = (y * W + x) * 3, q = (min(y, HEM - 1) * W + x) * 3, side = min(x > 0 ? HB[k - 1] : 1, x < W - 1 ? HB[k + 1] : 1); const rim = (1 - (HB[k - W] + HB[k - 2 * W]) / 2) * 0.6 + (1 - side) * 0.25; - for (let j = 0; j < 3; j++) F[o + j] = F[o + j] * (1 - c) + c * (6 + rim * [1, 0.76, 0.48][j] * max(70, F[q] * 0.9)); + for (let j = 0; j < 3; j++) F[o + j] = F[o + j] * (1 - c) + c * (6 + rim * RIM[j] * max(70, F[q] * 0.9)); } for (let i = 0, o = 0; i < N; i++, o += 3) display.setPixel(i & 63, i >> 6, min(255, max(0, F[o])) | 0, min(255, max(0, F[o + 1])) | 0, min(255, max(0, F[o + 2])) | 0); } From d9ae14f0ecf2a48bff357119b94cc0691bec12de Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:25:04 +0000 Subject: [PATCH 19/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Shadow=20Play?= 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_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/patterns/shadow-play.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/incoming/pattern-study/patterns/shadow-play.js b/incoming/pattern-study/patterns/shadow-play.js index 6ba6d80c9..df4c11e4f 100644 --- a/incoming/pattern-study/patterns/shadow-play.js +++ b/incoming/pattern-study/patterns/shadow-play.js @@ -47,7 +47,7 @@ const O_TORSO = [-8.2, 47, -6.5, 48.4, 6, 48.4, 7.8, 46.5, 7, 38, 5.5, 31, -5.5, const O_KAIN = [-6.2, 31.5, 6.2, 31.5, 8.8, 21.5, 2, 17, -4, 19.5, -10, 23]; const O_CROWN = [-0.5, 58.5, 6.2, 58.5, 5.4, 62, 2.8, 67, 0.2, 61.8]; -// Seconds of a scene: when each puppet enters, and where it goes. +// Where the knight and the ogre stand through a scene: story second, column (a scene that runs left to right). const KX = [1.7, -16, 3.6, 18, 5.6, 14, 6, 23, 6.4, 14, 6.8, 17, 7.2, 13, 7.6, 24, 8, 16, 8.5, 20, 9, 22, 10.8, 28, 12.3, 86]; const OX = [2.3, 84, 4.2, 47, 5.6, 50, 6, 47, 6.4, 49, 6.8, 40, 7.2, 50, 7.6, 47, 8, 39, 8.5, 42, 9, 40]; function key(t, k) { From 9cb4ec8c15bfce97aed7a06485e964cb9225d7d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:26:53 +0000 Subject: [PATCH 20/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Lichtenberg=20=EB=A5=BC=20=EA=B5=B5=EC=9D=80?= =?UTF-8?q?=20=EC=A4=84=EA=B8=B0=EC=99=80=20=EA=B9=83=ED=84=B8=20=EC=9E=94?= =?UTF-8?q?=EA=B0=80=EC=A7=80=EC=9D=98=20=EB=82=98=EB=AC=B4=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pattern-study/patterns/lichtenberg.js | 239 ++++++++---------- 1 file changed, 103 insertions(+), 136 deletions(-) diff --git a/incoming/pattern-study/patterns/lichtenberg.js b/incoming/pattern-study/patterns/lichtenberg.js index 8b58f1195..94f48a757 100644 --- a/incoming/pattern-study/patterns/lichtenberg.js +++ b/incoming/pattern-study/patterns/lichtenberg.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Lichtenberg // Family: fractal -// Idea: High voltage creeps over a wet board of pale wood from an electrode at each end, two branching trees of blue-white plasma and orange embers burn toward each other and char black, and when they meet an arc flashes down the joined path, the power cuts, the embers die and the next board slides in. +// Idea: High voltage creeps over a wet board of pale wood from an electrode at each end; two branching trees of blue-white plasma and orange embers burn toward each other and char black, an arc flashes down the joined path, the embers die and the next board slides in. // Author: patternflow-lab // Date: 2026-10-24 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -9,11 +9,11 @@ // Round: 26 // =============================== // @matrix 64x128 -// @knobs Wood=0..1, Speed=0.2..3, Pull=0.5..4, Lead=-1..1 +// @knobs Wood=0..1, Speed=0.2..3, Pull=1..4, Lead=-1..1 // -// Knob 1: Wood. The board: pale maple at 0, then white birch, walnut and black-stained oak at 1. The char is black-brown on the pale boards; on the stained one the burn eats through the stain and leaves grey ash. -// Knob 2: Speed. At 2 the power comes on a second and a quarter after a board is in, the trees meet four and a half seconds later, and a board takes eleven seconds from wet to gone; at 0.2 it all runs ten times slower. -// Knob 3: Pull. How hard the far electrode's field draws the growth, the exponent of the dielectric breakdown model: at 0.5 the trees grow bushy, feathered with side twigs, and fill the board; at 4 they shoot at each other in a few straight leaders with a twig here and there. +// Knob 1: Wood. Pale maple at 0, then white birch, walnut and black-stained oak at 1. The char is black-brown on the pale boards; on the stained one the burn eats through to grey ash. +// Knob 2: Speed. At 2 the power comes on a second and a half after a board is in, the trees meet four and a half seconds later, and a board takes about eleven seconds from wet to gone; at 0.2 it all runs ten times slower. +// Knob 3: Pull. The exponent of the dielectric breakdown model, how hard the far electrode's field draws the growth: at 1 the trees fork like a river delta, each channel feathered with fine twigs and open wood between; at 4 they shoot at each other in a few straight leaders. // Knob 4: Lead. Which electrode leads: at -1 the top tree burns most of the way down, at 1 the bottom one climbs most of the way up, and near 0 they meet halfway. // Buttons: K1 tap cuts the power and slides the next board in now. @@ -33,38 +33,43 @@ function vn(x, y) { return a + (b - a) * su + (c - a) * sv + (a - b - c + hash(xi + 1, yi + 1)) * su * sv; } -// The board at rest fills columns X0..X1 and rows Y0..Y1 of the panel; the -// top electrode touches it at (TX, TY) and the bottom one at (BX, BY). +// The board at rest fills columns X0..X1 and rows Y0..Y1; the electrodes +// touch it at (TX, TY) and (BX, BY). const X0 = 2, X1 = 61, Y0 = 3, Y1 = 124, TX = 32, TY = 9, BX = 31, BY = 118; -// A board slides down this far to make way for the next, leaving a few rows -// of the rig showing between them. +// A board slides down this far for the next, with a few rows of rig between. const SL = 131; // Seconds at Speed 2: wet board before the power, growth until the trees // meet, the arc, the embers dying, the slide. -const SOAK = 1.25, TG = 4.8, ARC = 0.5, COOL = 3, SLIDE = 1.6; +const SOAK = 1.0, TG = 4.8, ARC = 0.5, COOL = 3, SLIDE = 1.6; // Cells each tree adds a second at the front when it keeps pace with its -// schedule, and twigs a second it sprouts anywhere along its branches. -const RATE = 200, TWIG = 260; +// schedule, and fringe twigs a second for every cell of its channels. +const RATE = 200, TWIG = 0.14; // A cell that joined the tree this recently is still plasma. const TIP = 0.16; const DIRS = [[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]]; -// The DIRS index of a step (dx, dy), at (dy + 1) * 3 + dx + 1. -const DIX = [5, 6, 7, 4, -1, 0, 3, 2, 1]; +// How readily a channel, a twig and a twig's twig sprout a fringe twig, +// and how far each new generation runs: three or four LEDs, two, then one. +const GEN = [1, 0.4, 0.25], LEN = [2, 1, 0]; +// A channel feeding this many cells beyond it burns a groove one, two, +// three and four steps wider, with a brown scorch round it: the trunks +// near the electrodes run thick and dark. +const WIDE = [12, 50, 160, 500]; +// How dark the curl's bands run, in grain darkness. +const CURL = 0.45; const N4 = [-1, 1, -W, W], ND = [-W - 1, -W + 1, W - 1, W + 1], LUT = [0, 1, 16]; // Board, grain, knot and char colours for maple, birch, walnut and -// black-stained oak. Char is black-brown on the pale woods; on the stained -// board the burn eats through the stain and leaves grey ash. +// black-stained oak, where the burn leaves grey ash. const WOODS = [ - [[238, 206, 160], [160, 110, 66], [128, 76, 40], [30, 19, 14]], + [[228, 196, 150], [160, 110, 66], [128, 76, 40], [30, 19, 14]], [[244, 232, 204], [210, 186, 142], [120, 90, 58], [30, 19, 14]], [[140, 90, 58], [88, 54, 34], [50, 30, 18], [26, 16, 12]], [[50, 44, 46], [30, 26, 28], [18, 14, 14], [92, 86, 80]], ]; const SMOKE = [196, 190, 184], RIG = [16, 17, 22]; -// One board's wood: grain darkness (above 1 inside a knot), how wet the soda -// wash left each spot, and a small per-channel speckle. +// One board's wood: grain darkness (above 1 in a knot), how wet the soda +// wash left each spot, and a per-channel speckle. function wood() { const g = F32(N), wet = F32(N), nz = new Int8Array(N * 3); const ox = rnd() * 300, oy = rnd() * 300, per = 2.6 + rnd() * 0.6, tilt = (rnd() - 0.5) * 0.06; @@ -81,19 +86,20 @@ function wood() { u += (kr * kr * 3 * dx) / (d2 + kr * kr); kd = max(kd, 1 - sqrt(d2) / kr); } - // Thin dark latewood lines, a pore streak here and there. + // Thin dark latewood lines, a pore streak here and there, and the curl + // of figured maple across the grain, a band every two rows or so. const c = 0.5 + 0.5 * Math.cos((2 * Math.PI * u) / per); let v = c ** 4 * (0.8 + 0.6 * vn(x / 5 + ox, y / 30)) + 0.22 * (hash(x, floor(y / 4) + 7) - 0.5); + v += CURL * (0.5 + 0.5 * Math.sin((2 * Math.PI * (y + 1.5 * vn(x / 6 + oy, y / 20))) / 2.3)); if (kd > 0) v = 1 + kd * 0.6; g[i] = cl(v, 0, 1.6); wet[i] = 0.5 + 0.5 * vn(x / 9 + oy, y / 9 + ox); - for (let ch = 0; ch < 3; ch++) nz[i * 3 + ch] = round((rnd() - 0.5) * 6); + for (let ch = 0; ch < 3; ch++) nz[i * 3 + ch] = round((rnd() - 0.5) * 4); } return { g, wet, nz, col: F32(N * 3), at: -1 }; } -// A board's colours for this setting of K1: grain and knots, the lamp above, -// the bevelled edges and the speckle. +// A board's colours for this K1: grain, knots, lamp, bevels and speckle. function paint(B, wood) { const wv = wood * 3, wi = min(2, floor(wv)), wf = wv - wi, WC = []; for (let j = 0; j < 4; j++) WC.push([0, 1, 2].map((c) => WOODS[wi][j][c] + (WOODS[wi + 1][j][c] - WOODS[wi][j][c]) * wf)); @@ -111,16 +117,15 @@ function paint(B, wood) { export function setup(params) { const P = (params.s = { - time: 0, ph: 0, pt: 0, tau: 0, power: 0, wood: 0, eta: 0.5, lead: 0, ym: 64, + time: 0, ph: 0, pt: 0, tau: 0, power: 0, wood: 0, eta: 1, lead: 0, ym: 64, own: new Uint8Array(N), twg: new Uint8Array(N), par: new Int32Array(N), cnt: new Int32Array(N), born: F32(N), phi: F32(N), phi0: F32(N), chr: F32(N), sco: F32(N), ht: F32(N), - cand: [new Int32Array(N), new Int32Array(N)], cw: [F32(N), F32(N)], nc: [0, 0], acc: [0, 0], tac: [0, 0], tw: [], ntw: 0, cells: [new Int32Array(N), new Int32Array(N)], ncell: [1, 1], + cand: [new Int32Array(N), new Int32Array(N)], cw: [F32(N), F32(N)], nc: [0, 0], acc: [0, 0], tac: [0, 0], tw: [], ntw: 0, cells: [new Int32Array(N), new Int32Array(N)], ncell: [1, 1], nsk: [1, 1], fr: [TY, BY], meet: -1, meetQ: -1, path: new Int32Array(N), np: 0, pl: F32(N), sm: F32(N), puffs: [], ring: new Int32Array(512), nr: 0, hot: new Int32Array(32), nh: 0, flash: 0, wa: wood(), wb: wood(), }); - for (let k = 0; k < 256; k++) P.tw.push({ c: 0, dx: 0, dy: 0, k: 1, left: 0 }); - for (let k = 0; k < 70; k++) P.puffs.push({ on: 0 }); + for (let k = 0; k < 256; k++) P.tw.push({ left: 0 }), k < 70 && P.puffs.push({ on: 0 }); // The field round the two bare electrodes, solved once from a straight ramp. for (let y = Y0; y <= Y1; y++) for (let x = X0; x <= X1; x++) P.phi[y * W + x] = cl((y - TY) / (BY - TY), 0, 1); (P.own[TY * W + TX] = 1), (P.phi[TY * W + TX] = 0), (P.own[BY * W + BX] = 2), (P.phi[BY * W + BX] = 1); @@ -138,7 +143,7 @@ function fresh(P) { for (const t of P.tw) t.left = 0; const t = TY * W + TX, b = BY * W + BX; (P.own[t] = P.cnt[t] = P.cnt[b] = 1), (P.own[b] = 2), (P.cells[0][0] = t), (P.cells[1][0] = b); - Object.assign(P, { ncell: [1, 1], fr: [TY, BY], acc: [0, 0], tac: [0, 0], meet: -1, meetQ: -1, np: 0, tau: 0 }); + Object.assign(P, { ncell: [1, 1], nsk: [1, 1], fr: [TY, BY], acc: [0, 0], tac: [0, 0], meet: -1, meetQ: -1, np: 0, tau: 0 }); } // Red-black over-relaxation of Laplace's equation on the board: the top tree @@ -159,10 +164,9 @@ function relax(P, sweeps) { } } -// Every wood cell beside exactly one cell of a tree, or touching one only -// at a corner, may join it, with a chance that goes as the field there to -// the power eta. The board stands clear of the array's edges and nothing -// off it ever joins, so its neighbours need no bounds checks. +// A wood cell beside one cell of a tree, or touching one at a corner, may +// join it, with a chance that goes as the field there to the power eta. +// Nothing off the board ever joins, so neighbours need no bounds checks. function scan(P, eta) { const { own, phi, cand, cw } = P, n = [0, 0], t = [0, 0]; for (let y = Y0; y <= Y1; y++) @@ -183,10 +187,9 @@ function scan(P, eta) { P.nc = n; } -// A cell joins tree k only if it keeps the branches one LED wide: it grows -// from one cell, beside it or at its corner, and nothing else of the tree -// may touch it but that cell's own neighbours. Touching the other tree is -// the meeting. +// A cell joins tree k only if branches stay one LED wide: nothing of the +// tree may touch it but the cell it grows from and that cell's neighbours. +// Touching the other tree is the meeting. function join(P, c, k, tw = 0) { const { own } = P; if (own[c]) return false; @@ -213,29 +216,21 @@ function join(P, c, k, tw = 0) { } function add(P, c, p, k, tw) { - const { own, chr, sco, cnt, par } = P; - own[c] = k; - P.twg[c] = tw; - par[c] = p; - cnt[c] = 1; - P.born[c] = P.time; - P.ht[c] = 1; - chr[c] = 1; - P.phi[c] = k === 1 ? 0 : 1; - const y = c >> 6, x = c & 63; - if (k === 1) P.fr[0] = max(P.fr[0], y); - else P.fr[1] = min(P.fr[1], y); + const { cnt, par } = P, y = c >> 6, x = c & 63; + (P.own[c] = k), (P.twg[c] = tw), (par[c] = p), (cnt[c] = P.ht[c] = P.chr[c] = 1), (P.born[c] = P.time), (P.phi[c] = k - 1); + P.fr[k - 1] = k === 1 ? max(P.fr[0], y) : min(P.fr[1], y); scorch(P, x, y, 1, 0.04); P.ring[P.nr++ & 511] = c; P.cells[k - 1][P.ncell[k - 1]++] = c; + if (!tw) P.nsk[k - 1]++; // The current that feeds every cell beyond reaches it through all the // cells back to the electrode, and the busiest burn into wide grooves. for (let a = p; a >= 0; a = par[a]) { - const n = ++cnt[a]; - if (n === 80 || n === 300 || n === 1000) { - const ax = a & 63, ay = a >> 6, r = n === 80 ? 1 : n === 300 ? 1.5 : 2.2; - scorch(P, ax, ay, r, n === 80 ? 0.6 : 0.9, 1); - scorch(P, ax, ay, r + 1.5, 0.2); + const w = WIDE.indexOf(++cnt[a]); + if (w >= 0) { + const ax = a & 63, ay = a >> 6, r = 1 + 0.6 * w; + scorch(P, ax, ay, r, w ? 0.9 : 0.7, 1); + scorch(P, ax, ay, r + 2, 0.35); } } } @@ -256,16 +251,17 @@ function scorch(P, x, y, r, v, burn) { function grow(P, d) { relax(P, 3); // Each tree has a front it should have reached by now, eased in so the - // growth speeds up as the trees close, and a cell budget that rises - // steeply when it lags. - const s = min(1, P.tau / TG) ** 1.7, late = max(0, P.tau - TG); + // growth speeds up as the trees close (a hard pull sends leaders racing + // out at once), and a cell budget that rises steeply when it lags. + const s = min(1, P.tau / TG) ** (1.35 - 0.15 * P.eta), late = max(0, P.tau - TG); const lag = [TY + (P.ym - TY) * s - P.fr[0], P.fr[1] - (BY - (BY - P.ym) * s)]; - // Side twigs sprout off the branches, more of them the weaker the pull. - // The leading electrode's tree takes the larger share of both. + // The leading electrode's tree takes the larger share of the front's + // cells; fringe twigs sprout as fast as there is channel to carry them, + // fewer the harder the pull. for (let k = 0; k < 2; k++) { const f = exp(cl(lag[k] / 6, -2, 2.5)), sh = 1 + (k ? 0.85 : -0.85) * P.lead; P.acc[k] += d * RATE * sh * f * (1 + 4 * late); - P.tac[k] += d * TWIG * sh * (1.25 - P.eta / 4) ** 2 * cl(f, 0.35, 1.5); + P.tac[k] += d * TWIG * P.nsk[k] * ((4.5 - P.eta) / 3.5) ** 2; } twigs(P); if (P.acc[0] < 1 && P.acc[1] < 1 && P.tac[0] < 1 && P.tac[1] < 1) return; @@ -277,32 +273,30 @@ function grow(P, d) { const tot = S[m - 1]; for (let t = 0, tries = 0; t < n && tries < 3 * n; tries++) { const r = rnd() * tot; - let lo = 0, hi = m - 1; - while (lo < hi) { - const mid = (lo + hi) >> 1; - if (S[mid] < r) lo = mid + 1; - else hi = mid; - } + let lo = 0; + for (let hi = m - 1, mid; lo < hi; ) (mid = (lo + hi) >> 1), S[mid] < r ? (lo = mid + 1) : (hi = mid); if (join(P, C[lo], k + 1)) t++; if (P.meet >= 0) return; } } - // A twig sprouts an eighth or a quarter turn off a branch the front laid - // down (seldom off another twig) and runs on straight for a few LEDs: the - // feathering along every branch. - for (let tries = 0, k = 0; tries < 400; tries++) { + // A fringe twig sprouts off a burnt channel, an eighth of a turn off it + // or now and then a quarter, and runs a few LEDs, bending as it goes. A + // twig sprouts shorter twigs of its own, three generations deep. + for (let tries = 0, k = 0; tries < 300; tries++) { if (P.tac[k] < 1) k = 1 - k; if (P.tac[k] < 1) break; - const nc = P.ncell[k]; - const a = P.cells[k][floor(rnd() * nc)], pa = P.par[a]; - if (pa < 0 || (P.twg[a] && rnd() < 0.9)) continue; - const j = DIX[((a >> 6) - (pa >> 6) + 1) * 3 + (a & 63) - (pa & 63) + 1]; + const a = P.cells[k][floor(rnd() * P.ncell[k])], g = P.twg[a]; + if (P.par[a] < 0 || g > 2 || rnd() > GEN[g]) continue; + // The branch's way here, read over four cells so one step cannot throw it. + let b = a; + for (let n = 0; n < 4 && P.par[b] >= 0; n++) b = P.par[b]; + const j = round((Math.atan2((a >> 6) - (b >> 6), (a & 63) - (b & 63)) * 4) / Math.PI) & 7; const dir = (j + (rnd() < 0.5 ? 1 : 7) * (rnd() < 0.85 ? 1 : 2)) & 7; const x = (a & 63) + DIRS[dir][0], y = (a >> 6) + DIRS[dir][1], c = y * W + x; - if (x < X0 || x > X1 || y < Y0 || y > Y1 || !join(P, c, k + 1, 1)) continue; + if (x < X0 || x > X1 || y < Y0 || y > Y1 || !join(P, c, k + 1, g + 1)) continue; P.tac[k]--; if (P.meet >= 0) return; - Object.assign(P.tw[P.ntw++ & 255], { c, dx: DIRS[dir][0], dy: DIRS[dir][1], k: k + 1, left: 2 + floor(rnd() * 7) }); + Object.assign(P.tw[P.ntw++ & 255], { c, d0: dir, dir, k: k + 1, g: g + 1, left: LEN[g] + floor(rnd() * LEN[g]) }); } P.tac = P.tac.map((v) => min(v, 4)); } @@ -311,14 +305,13 @@ function twigs(P) { for (const t of P.tw) { if (t.left <= 0) continue; t.left--; - let dx = t.dx, dy = t.dy; - // Now and then a twig bends by an eighth of a turn. - if (rnd() < 0.12) { - const a = (DIX[(dy + 1) * 3 + dx + 1] + (rnd() < 0.5 ? 1 : 7)) & 7; - (dx = DIRS[a][0]), (dy = DIRS[a][1]); + // A twig wanders an eighth of a turn, never further off its way. + if (rnd() < 0.25) { + const dir = (t.dir + (rnd() < 0.5 ? 1 : 7)) & 7, off = (dir - t.d0 + 8) & 7; + if (off <= 1 || off >= 7) t.dir = dir; } - const x = (t.c & 63) + dx, y = (t.c >> 6) + dy, c = y * W + x; - if (x < X0 || x > X1 || y < Y0 || y > Y1 || !join(P, c, t.k, 1)) { + const x = (t.c & 63) + DIRS[t.dir][0], y = (t.c >> 6) + DIRS[t.dir][1], c = y * W + x; + if (x < X0 || x > X1 || y < Y0 || y > Y1 || !join(P, c, t.k, t.g)) { t.left = 0; continue; } @@ -337,31 +330,27 @@ function trace(P) { P.np = n; for (let j = 0; j < n; j++) { const a = P.path[j], x = a & 63, y = a >> 6; - scorch(P, x, y, 1.2, 1, 1); - scorch(P, x, y, 3.2, 0.5); + scorch(P, x, y, 2, 1, 1); + scorch(P, x, y, 4, 0.5); P.ht[a] = 1; } } function go(P, ph) { - P.ph = ph; - P.pt = 0; + (P.ph = ph), (P.pt = 0); if (ph === 1) P.power = 1; if (ph === 2) trace(P), (P.flash = 1); - if (ph === 3 || ph === 4) P.power = 0; + if (ph > 2) P.power = 0; if (ph === 0) ([P.wa, P.wb] = [P.wb, P.wa]), fresh(P); } export function update(dt, input, params) { if (!params.s) setup(params); - const P = params.s, k = input.knobValues || [0, 2, 0.5, 0.06]; + const P = params.s, k = input.knobValues || [0, 2, 1, 0.06]; const d = (cl(dt || 0, 0, 0.1) * cl(k[1], 0.2, 3)) / 2; - P.wood = cl(k[0], 0, 1); - P.eta = cl(k[2], 0.5, 4); - P.lead = cl(k[3], -1, 1); + (P.wood = cl(k[0], 0, 1)), (P.eta = cl(k[2], 1, 4)), (P.lead = cl(k[3], -1, 1)); P.ym = (TY + BY) / 2 - 44 * P.lead; - P.time += d; - P.pt += d; + (P.time += d), (P.pt += d); if (input.btnPressed && input.btnPressed[0] && P.ph > 0 && P.ph < 4) go(P, 4); const ph = P.ph; if (ph === 0 && P.pt >= SOAK) go(P, 1); @@ -378,8 +367,7 @@ export function update(dt, input, params) { } else if (ph === 3 && P.pt >= COOL) go(P, 4); else if (ph === 4 && P.pt >= SLIDE) go(P, 0), (P.wb = wood()); P.flash = max(0, P.flash - d * 9); - heat(P, d); - smoke(P, d); + heat(P, d), smoke(P, d); } // Embers: fresh channels glow and cool to char, while the power is on the @@ -408,11 +396,10 @@ function heat(P, d) { // Smoke puffs rise off the hot channels, spread and thin out. function smoke(P, d) { - let want = (P.ph === 4 ? 0 : P.heatSum) * d * 0.14; + let want = (P.ph === 4 ? 0 : P.heatSum) * d * 0.22; for (const s of P.puffs) { if (s.on) { - s.age += d; - if (s.age > s.life) { + if ((s.age += d) > s.life) { s.on = 0; continue; } @@ -440,15 +427,10 @@ function plasma(L, x, y, r, v) { } // Ember colour by heat: dull red, orange, then yellow-white. +const EK = [0, 0.25, 0.6, 1], EC = [[0, 0, 0], [175, 35, 6], [255, 110, 20], [255, 220, 140]]; function ember(h, out) { - if (h < 0.25) (out[0] = 700 * h), (out[1] = 140 * h), (out[2] = 24 * h); - else if (h < 0.6) { - const t = (h - 0.25) / 0.35; - (out[0] = 175 + 80 * t), (out[1] = 35 + 75 * t), (out[2] = 6 + 14 * t); - } else { - const t = (h - 0.6) / 0.4; - (out[0] = 255), (out[1] = 110 + 110 * t), (out[2] = 20 + 120 * t); - } + const j = h < 0.25 ? 0 : h < 0.6 ? 1 : 2, t = (min(h, 1) - EK[j]) / (EK[j + 1] - EK[j]); + for (let c = 0; c < 3; c++) out[c] = EC[j][c] + (EC[j + 1][c] - EC[j][c]) * t; } const EM = [0, 0, 0]; @@ -468,21 +450,17 @@ export function draw(display, params) { for (let j = 0; j < 512 && j < P.nr; j++) { const c = P.ring[(P.nr - 1 - j) & 511], age = P.time - born[c]; if (age > TIP) break; - // Twigs behind the front only flash orange; the plasma rides the - // advancing edge of each tree. + // Twigs behind the front only flash orange. if (P.twg[c] && (own[c] === 1 ? (c >> 6) < P.fr[0] - 14 : (c >> 6) > P.fr[1] + 14)) continue; const f = (1 - age / TIP) * (0.5 + 0.5 * rnd()), x = c & 63, y = (c >> 6) + oy; plasma(pl, x, y, 1.5, f); - if (rnd() < 0.2) { - let sx = x, sy = y; - const dir = own[c] === 1 ? 1 : -1; - for (let st = 0, len = 2 + floor(rnd() * 5); st < len; st++) { - if (rnd() < 0.6) sy += dir; + if (rnd() < 0.2) + for (let st = 0, sx = x, sy = y, len = 2 + floor(rnd() * 5); st < len; st++) { + if (rnd() < 0.6) sy += own[c] === 1 ? 1 : -1; else sx += rnd() < 0.5 ? -1 : 1; if (sx < 0 || sx >= W || sy < 0 || sy >= H) break; pl[sy * W + sx] += 0.9 - st * 0.12; } - } } // A blue corona at each electrode. const z = 0.4 + 0.4 * rnd(); @@ -510,7 +488,7 @@ export function draw(display, params) { } } - const fl = P.flash * 70, CH = P.wa.ch; + const fl = P.flash * 70, CH = P.wa.ch, dry = P.ph > 1 ? 1 : P.ph ? P.tau / (TG + 0.5) : 0; for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) { const p = y * W + x; @@ -520,11 +498,10 @@ export function draw(display, params) { if (P.ph === 4 && yb < Y0) (yb += SL), (B = P.wb), (burnt = 0); if (x >= X0 && x <= X1 && yb >= Y0 && yb <= Y1) { const i = yb * W + x, C = B.col; - // The wet wash glosses the wood until the burn dries it. - const s = burnt ? sco[i] : 0, lf = 0.94 + 0.12 * B.wet[i] * max(0, 1 - 3 * s); - r = C[i * 3] * lf; - g = C[i * 3 + 1] * lf; - b = C[i * 3 + 2] * lf; + // The wet wash glosses the wood; the current steams it off the whole + // board and the burn dries it at once. + const s = burnt ? sco[i] : 0, lf = 0.86 + 0.28 * B.wet[i] * max(0, 1 - 3 * s - (burnt ? dry : 0)); + (r = C[i * 3] * lf), (g = C[i * 3 + 1] * lf), (b = C[i * 3 + 2] * lf); if (burnt) { if (s > 0) (r *= 1 - 0.5 * s), (g *= 1 - 0.66 * s), (b *= 1 - 0.8 * s); const c = chr[i]; @@ -534,33 +511,23 @@ export function draw(display, params) { // Thick char glows only along its rim. const rim = (x > X0 && chr[i - 1] < 0.5) || (x < X1 && chr[i + 1] < 0.5) || chr[i - W] < 0.5 || chr[i + W] < 0.5; ember(h * (rim ? 1 : 0.45), EM); - r = max(r, EM[0]); - g = max(g, EM[1]); - b = max(b, EM[2]); + (r = max(r, EM[0])), (g = max(g, EM[1])), (b = max(b, EM[2])); } } - r += fl; - g += fl; - b += fl * 1.2; + (r += fl), (g += fl), (b += fl * 1.2); } else { // The dark rig behind, with the board's shadow to its right. - const sh = x === X1 + 1 || x === X1 + 2 ? 0.55 : 1, q = hash(x, y); - r = (RIG[0] + 6 * q) * sh; - g = (RIG[1] + 6 * hash(y, x)) * sh; - b = (RIG[2] + 6 * q) * sh; + const sh = x > X1 ? 0.55 : 1, q = hash(x, y); + (r = (RIG[0] + 6 * q) * sh), (g = (RIG[1] + 6 * hash(y, x)) * sh), (b = (RIG[2] + 6 * q) * sh); if (x === 0 || x === 63) (r = 64 + 10 * q), (g = 66 + 10 * q), (b = 74 + 10 * q); } const m = min(0.7, sm[p]); - r += (SMOKE[0] - r) * m; - g += (SMOKE[1] - g) * m; - b += (SMOKE[2] - b) * m; + (r += (SMOKE[0] - r) * m), (g += (SMOKE[1] - g) * m), (b += (SMOKE[2] - b) * m); // Plasma: deep blue where it is thin, blue-white where it is thick. const t = pl[p]; if (t > 0.01) { const u = min(1, t), w = min(1, t / 2.5); - r += (90 + 160 * w - r) * u; - g += (120 + 130 * w - g) * u; - b += (255 - b) * u; + (r += (90 + 160 * w - r) * u), (g += (120 + 130 * w - g) * u), (b += (255 - b) * u); } display.setPixel(x, y, cl(r, 0, 255) | 0, cl(g, 0, 255) | 0, cl(b, 0, 255) | 0); } From 4bd7dfb47f59e8d14367e6394ab8bfc533e153f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:47:58 +0000 Subject: [PATCH 21/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20README=20=EC=97=90=2026=ED=8C=90=20=ED=91=9C?= =?UTF-8?q?=EC=99=80=20=EC=A0=88=EC=9D=84=20=EC=93=B0=EB=8A=94=20=EC=A4=91?= 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_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/README.md | 61 ++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index d072b1567..ce046993e 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 158개를 마흔여덟 갈래로 나눠 만들었어요. 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판도 열 갈래에 하나씩 열 개를 더했고, 농사·밭과 화산 갈래를 새로 열었어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 168개를 쉰 갈래로 나눠 만들었어요. 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판도 열 갈래에 하나씩 열 개를 더했고, 얼음·극지와 미생물 갈래를 새로 열었어요. [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판에서 세로로 다시 짰어요. @@ -279,6 +279,21 @@ | [Skydiver](patterns/skydiver.js) | 비행 | 강하장 위로 점프기가 지나가며 문에서 다이버들을 내보내요. 다이버들은 구름층을 뚫고 내려가 손을 잡아 고리를 만들었다가 흩어지고, 저마다 색 램에어 캐노피를 펴서 나선을 그리며 들판의 착지 원으로 내려와요. K1 을 끝까지 돌리면 해가 지평선에 걸린 노을 점프가 되고, K1 을 누르면 다음 비행기가 바로 들어와요. | | [Snow Monkeys](patterns/snow-monkeys.js) | 짐승 | 눈 내리는 지고쿠다니의 해 질 녘, 일본원숭이들이 김 오르는 노천 온천에 어깨까지 잠겨 머리에 눈을 얹은 채 졸거나 서로 털을 골라 줘요. 바위 위의 어미가 물로 걸어 들어오고, 덩어리진 김은 눈 덮인 삼나무 비탈을 지나 패널 꼭대기까지 피어올라요. K1 을 끝까지 돌리면 밤이 되어 석등이 김을 주황으로 물들이고, K1 을 누르면 새끼가 물보라를 일으키며 뛰어들어요. | +### 26판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Dark Field](patterns/dark-field.js) | 미생물 | 암시야 현미경 속 연못물 한 방울이에요. 볼복스와 짚신벌레, 돌말, 해캄 실 위의 종벌레가 검은 바탕에 흩은 빛으로만 빛나요. K1 이 라인베르크와 위상차로 조명을 바꾸고, 누르면 종벌레 자루가 용수철처럼 감겨요. | +| [Geyser](patterns/geyser.js) | 화산 | 눈 덮인 지열 지대의 간헐천을 땅속까지 잘라 봐요. 화도에 기포가 차면 청록 웅덩이가 유리 돔으로 부풀다 흰 물기둥으로 터지고, 김 구름이 바람에 흘러가요. K1 은 정오에서 오로라 밤까지 빛을 바꾸고, 누르면 바로 분출해요. | +| [Iceberg](patterns/iceberg.js) | 얼음·극지 | 물높이에서 자른 빙산이 9분의 1만 물 위에 내밀고 펭귄을 얹은 채 너울에 흔들리다, 판이 떨어져 물보라를 일으켜요. 물 아래 몸은 푸른 얼음 줄무늬와 탄산처럼 오르는 거품 줄기로 패널 바닥까지 내려가요. K1 은 백야의 해에서 오로라 뜬 극야로, 버튼은 판을 떼어 내요. | +| [Implosion](patterns/implosion.js) | 공사·건설 | 새벽 하늘 아래 스무 층 아파트에 장약 섬광이 층마다 타고 내려오면, 탑이 꺾이며 제자리로 주저앉고 회황색 먼지구름이 꼭대기까지 끓어올라요. 타임랩스로 비계 속에 새 탑이 서고, K1 을 누르면 바로 발파해요. | +| [Lichtenberg](patterns/lichtenberg.js) | 프랙탈 | 소다물을 바른 단풍나무 판의 위아래 전극에서 깃털 잔가지를 단 프랙탈 나무가 파란 플라스마와 주황 불씨를 달고 서로를 향해 타 들어가고, 만나면 아크가 번쩍여요. K1 이 판의 나무를 바꾸고, 누르면 다음 판이에요. | +| [Noborigama](patterns/noborigama.js) | 도자기 | 밤 언덕의 오름가마를 세로로 잘라 봐요. 불이 그릇 쌓인 칸들을 타고 올라 아래 칸은 희고 위 칸은 붉으며, 장작을 넣을 때마다 감시 구멍에서 불길이, 굴뚝에서 검은 연기와 불티가 나와요. K1 은 소성 날짜를 바꾸고, 누르면 모든 구멍에 한꺼번에 불을 넣어요. | +<> +<> +| [Rice Planting](patterns/rice-planting.js) | 농사·밭 | 물 댄 논을 위에서 내려다봐요. 밀짚모자를 쓴 일꾼들이 못줄을 따라 뒷걸음질하며 모를 한 포기씩 꽂고, 앞쪽 거울 물에는 구름과 모춤, 백로가 있어요. K1 은 아침 안개에서 노을까지 돌리고, 누르면 남은 모를 한꺼번에 꽂고 줄을 넘겨요. | +| [Shadow Play](patterns/shadow-play.js) | 무대·공연 | 등잔 불빛을 받은 흰 막에 와양 쿨릿 가죽 인형의 레이스 그림자가 비쳐요. 기사가 거인과 싸워 던져 버리면 구눙안이 떨며 쓸고 들어와 다음 장을 열고, K1 을 돌리면 막 뒤로 돌아가 금빛과 빨강, 남색 칠이 보여요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -1077,6 +1092,46 @@ K4 Spate 0 이면 물이 실 몇 가닥으로 갈라져요. 처음 값 0.06 이 남은 자리로 기하·대칭 갈래의 넷째를 세웠어요. 촘촘함 × 색 3,4 칸에 4판의 Jacquard Loom 하나만 서 있어서, 작은 타일 사이에 1 LED 어두운 선을 긋고 선명한 색을 여럿 넣어 그 칸을 겨눴어요. 촘촘함 0.264, 색 0.583 으로 그 칸에 들었어요. 로빈슨 삼각형을 나누는 셈을 돌려 보니 타일링은 황금비 한 번에 제자리로 오지 않았어요. 한 번 나눈 판을 φ 배로 키우면 반지름 8 안의 변 700개 가운데 230개만 맞았어요. 두 번 나눈 판을 φ² 배로 키워 36도 돌리면 710개가 다 맞았어요. 그래서 두 단계를 한 바퀴로 돌고 바퀴마다 36도씩 돌려요. 흔히 쓰는 씨앗인 빨간 삼각형 열 개의 해는 나눈 뒤 가운데가 다시 빨갛게 돌아오지 않았어요. 그래서 굵은 마름모 다섯의 별을 씨앗으로 했어요. 연과 화살은 연 다섯의 해에서 자라고, 똑같이 두 번 나누면 돌아와요. 타일은 일곱 가지 꼭짓점 가운데 네 꼭짓점이 무엇이냐로 칠했어요. 마름모는 열 갈래, 연과 화살은 여덟 갈래예요. 고리는 한 단계에 130 LED 를 가서 모서리를 한참 지나요. 그래서 다음 단계가 오기 전에 모든 타일이 나뉘고 빛도 꺼져요. 첫 판은 K4 Glow 가 고리만 밝혀서 노브 반응이 0.01 이었어요. 고리 사이 유리를 어둡게 하는 노브로 바꾸자 0.23 이 됐어요. 한 프레임이 2.7 ms 였는데, 그 가운데 0.8 ms 는 첫 프레임에 삼각형 목록을 짓는 187 ms 였어요. 꼭짓점 각을 acos 대신 표로 읽자 첫 프레임이 83 ms, 한 프레임이 1.4 ms 가 됐어요. 타일 크기가 색을 움직였어요. 타일이 클수록 어두운 선의 몫이 줄어서, 타일 변 7 LED 에서 0.613 이던 색이 8 LED 에서는 0.633 으로 축 끝 0.627 을 넘었어요. 그래서 7 에서 열고 채도를 0.94 배로 내려 0.583 에 세웠어요. 너비 64 의 가운데는 두 LED 사이라서, 다섯 겹 축을 지나는 세로 변이 134/255 의 회색 두 줄로 그려졌어요. 가운데를 31.5 로 옮기자 17/255 의 어두운 한 줄이 됐어요. 모양 바뀜은 줌으로 타일이 4×16 칸을 흘러 지나가서 첫 판부터 0.083 이었고, 지금은 0.098 이에요. +## 26판 + +<> + +### Iceberg + +주제 2 로 얼음·극지 갈래를 열고, 주제 4 로 물높이 30번 줄에서 자른 빙산 단면이 128줄을 다 쓰게 했어요. 물 위로는 19번 줄부터 11줄만 서고, 물 아래 몸은 너비 45 LED 까지 부풀었다가 탁상형은 116번 줄, 첨탑형은 125번 줄에서 용골이 끝나요. 하늘과 바다를 다 칠해서 `thirds` 는 99/99/99 예요. 하늘과 물을 검게 지운 사본으로 빙산만 재면 켜진 LED 가 씨앗 넷에서 30/67/30% 이고, 얼음에 비친 빛까지 꺼도 바뀐 LED 가 26~31/17~21/16~22% 예요. 3.4초에 판이 떨어진 뒤 빙산이 흔들려서 용골까지 움직여요. 첫 판은 모양 바뀜이 0.038, K2~K4 가 0.04 였어요. 물속 빛줄기를 46 에서 64 로 밝히고 넓히고 너울을 키우자 0.054 가 됐어요. 첨탑형을 좁고 깊고 더 푸른 얼음으로 바꾸자 K3 은 0.05 가 됐어요. 가장 넓은 반폭은 22.5 에서 16.5 LED 가 됐어요. 첫 판의 거품은 얼음에 닿으면 옆으로 미끄러져 빙산 둘레에 점선 테두리처럼 붙었어요. 얼음 앞면 다섯 곳에서 곧게 올라가게 하자 탄산처럼 오르는 줄기로 읽혀요. 먼 바다 띠는 노을 하늘에 0.6 을 곱한 (150,120,77) 갈색이었고, 12번 줄의 하늘과 물 색을 섞자 푸른 바다에 해의 금빛 길이 섰어요. 첫 판은 한 프레임이 4.4 ms 였고 그 가운데 30% 가 쓰레기 수거였어요. 행 색을 `.map` 으로 짓고 펭귄 필드가 정수에서 실수로 바뀌어서, draw 가 660 프레임에 136번 최적화를 잃었어요. 팔레트를 `Float64Array` 하나로 펴고 필드를 처음부터 실수로 두자 1.9~2.0 ms 가 됐어요. 33,361 바이트이던 파일은 첨탑 꼭대기가 떨어지는 장면과 얼음 조각, 바닥 흙띠를 빼고 빙산과 떨어진 판을 한 함수로 그려 24,704 바이트가 됐어요. 노브 넷은 0.18, 0.05, 0.05, 0.05 예요. + +### Dark Field + +주제 2 로 미생물 갈래를 열었어요. 주제 1 은 겨눌 `*` 칸이 없어서, 채움 0.302 아래에서 패턴이 하나뿐이던 칸 둘을 겨눴어요. 복잡도 × 채움 3,1 은 Foxfire 혼자, 촘촘함 × 채움 3,1 은 Peg Light 혼자 서 있었어요. 검은 바탕에 볼복스 셋, 짚신벌레 셋, 돌말 다섯, 오실라토리아 두 가닥, 해캄 실 위의 종벌레 다섯과 윤충, 세균 몇백이 흩은 빛으로만 빛나요. 첫 판은 채움 0.263 에 촘촘함 0.081, 5.2 ms 였고, 켜진 LED 2,456 개 가운데 1,397 개가 이웃과 밝기 차가 거의 없는 초점 밖 후광이었어요. 후광을 반 해상도 버퍼 하나로 모으고 세기를 0.6 에서 0.35 로 낮추자 그런 LED 가 1,772 개 가운데 515 개로 줄었고 2.9 ms 가 됐어요. 빛을 0.1 아래에서 자르고 세 배로 올렸더니 초록 볼복스가 흰색으로 바래서, 가장 밝은 채널로 톤을 매겼어요. 그러자 색이 돌아오고 복잡도가 2.058 에서 2.148 로 올랐어요. 하나씩 빼고 재 보니 초점 밖 짚신벌레는 채움 0.031 에 촘촘함 0.009 만 냈어요. 짚신벌레를 초점면 가까이 옮기고 돌말 속 줄무늬를 테두리 반짝임으로 바꾼 다음 위 3분의 1에 오실라토리아를 더하자, 씨앗 여덟의 촘촘함이 0.195~0.220 에서 0.203~0.238 이 됐어요. 그 판은 움직임이 2.306 이었어요. 켜진 LED 마다 ±20% 의 반짝임과 검은 바탕 전체에 23/255 아래의 결을 프레임마다 새로 뿌려서, 패널에서는 텔레비전 잡음처럼 끓는 판이었어요. 결을 LED 마다 한 번 정해 두자 움직임이 1.381 로 내려갔고, 복잡도는 2.080 에서 2.081, 촘촘함과 채움은 그대로였어요. 결이 그 자리에 서 있어도 복잡도는 똑같이 올라요. 지금 씨앗 여덟이 모두 촘촘함 0.203~0.238, 복잡도 2.062~2.124, 채움 0.247~0.283 으로 두 칸에 들어요. 라인베르크 조명은 바탕을 파랗게 채워 채움이 1 로 올라서, 채움 × 색 1,4 칸(Neon Alley 혼자)에는 닿지 않았어요. 노브 넷은 0.53, 0.26, 0.06, 0.18 이에요. + +### Noborigama + +주제 4 로 도자기 갈래의 셋째를 세웠어요. 25판 「다음 바퀴」가 적어 둔 장작 가마예요. 밤 언덕의 오름가마를 세로로 잘라서, 123번 줄의 아궁이에서 꼭대기 칸 천장(34번 줄)까지 둥근 천장의 칸 셋에서 여섯이 비탈을 오르고, 18번 줄의 굴뚝 입에서 연기와 불티가 별하늘로 나가요. 하늘과 언덕을 지운 사본에서 2~8초 동안 바뀐 LED 는 씨앗 넷에서 위 28.4~32.0%, 가운데 54.2~58.2%, 아래 37.8~40.0% 예요. 꼭대기 칸 위 0~32번 줄만 세어도 굴뚝과 연기, 불티로 25.1~31.3% 예요. 25판이 물은 문턱에 가마 불꽃도 걸렸어요. 빨강이 255 까지 가고 일렁임을 온도로만 주었을 때 불때기를 끄고 재 보니, 흰 열의 1번 칸 기체 LED 는 0.8%, 2번 칸은 4.2% 만 바뀌었어요. 붉은 꼭대기 칸은 95.3% 였어요. 빨강을 232 에서 멈추고 일렁임이 밝기를 0.72~1 배로 흔들게 하자 1번 칸은 96.9%, 2번 칸은 97.0% 가 됐고, 아래 3분의 1은 12.6~14.2% 에서 31.0~33.7% 로 올랐어요. 불을 넣으면 온도가 0.15 내려가며 밝기가 36% 줄었다가 0.1 올라요. K4 Draft 는 연기와 불꽃만 기울일 때 0.0117 로 검사에 걸렸어요. 언덕으로 부는 바람이 굴뚝을 끌어 불을 달구고, 내리부는 바람에는 감시 구멍마다 불을 토하게 하고, 연기를 2.5초 미리 피워 두자 0.044 가 됐어요. 첫 판은 30,009 바이트였어요. 칸 밑 통로 위의 불길 밝기와 칸 아래 석축의 따로 된 재질을 빼서 24,884 바이트가 됐어요. 노브 넷은 0.10, 0.04, 0.07, 0.04 예요. + +### Rice Planting + +주제 4 로 농사·밭 갈래의 둘째를 세웠어요. 25판의 Rice Terraces 가 옆에서 본 다랑논이라, 이쪽은 물 댄 논을 바로 위에서 내려다본 모내기예요. 한 줄은 14포기이고 4 LED 간격으로 25줄이 서요. 밀짚모자를 쓴 네 사람이 빨강과 노랑 표시가 달린 못줄을 따라 뒷걸음질하며 한 포기씩 꽂고, 꽂을 때마다 물에 고리가 퍼져요. 줄 뒤는 모가 선 논이고 앞은 구름이 흘러가는 거울 물이에요. 그 물에 모춤이 떠 있고 백로가 걸어 다니며, 아래 끝에는 모판을 놓은 논둑과 모를 찌는 못자리가 있어요. 마지막 줄을 꽂으면 화면이 옆 논으로 61 LED 미끄러지고, 다음 논은 아래에서 위로 심어요. 물과 둑을 지운 사본에서 켜진 LED 는 씨앗 넷에서 15~16/16~18/40% 예요. 못자리까지 지우면 아래 3분의 1이 3.9~5.0% 였어요. 논둑 길에 8×3 모판 둘을 놓아 5.7~6.7% 로 올렸어요. 심은 쪽 흙탕물 0.3~0.55 와 안개 0.45 는 위 3분의 1을 잿빛으로 만들어 모양 바뀜이 0.047 에서 0.039 로 떨어졌어요. 흙탕물을 0.16~0.46 으로 줄이고, 아침 하늘의 머리 위를 (46,92,168) 로 짙게 하고, 구름을 초당 2.4 에서 3.4 LED 로 빠르게 하자 0.051 이 됐어요. 다른 씨앗 셋은 0.036~0.040 이에요. 첫 판은 한 프레임 5.4 ms 였어요. 드물게 도는 백로의 부리 찌르기가 큰 draw 함수의 최적화를 열세 번, 꼴이 두 가지인 모춤 객체가 마흔아홉 번 되돌렸어요. 함수를 여섯으로 나누고 객체 꼴을 하나로 맞추자 1.3 ms 가 됐어요. 노브 넷은 0.20, 0.08, 0.06, 0.10 이에요. + +### Implosion + +주제 4 로 공사·건설 갈래의 셋째를 세웠어요. Tower Crane 이 짓고 Excavation 이 파니, 이쪽은 허물어요. 새벽 하늘 아래 110번 줄 땅에 스무 층(80줄) 판상형 아파트가 옥상 기계실과 안테나를 이고 서 있어요. 2.4초에 위층부터 장약 섬광이 0.09초 간격으로 일곱 층을 타고 내려오고, 3초에 아래층이 먼저 주저앉아요. 높이의 42% 에서 윗덩이가 반대로 꺾이며 2.5초 만에 제자리로 내려앉고, 먼지는 7초쯤 패널 꼭대기에 닿아요. 한 바퀴는 36초이고, 먼지가 걷히면 타임랩스로 비계 속에 새 탑이 서요. 먼지는 처음에 퍼프의 빛을 무게로 평균 내서 칠했더니 판 전체가 평평한 회색 안개가 되고 모양 바뀜이 0.046 이었어요. 퍼프마다 해와 하늘에서 빛을 받는 공으로 그려 앞의 것 위에 겹치고 가장자리를 잡음으로 갉자 뭉게구름이 되어 0.09 가 됐어요. 공이 반질반질해 보여 해 쪽 명암을 줄이고 결을 넣은 지금은 0.077 이에요. 하늘과 건물, 울타리, 구경꾼을 검게 지운 사본으로 재도 씨앗 넷에서 켜진 LED 가 13.0~19.6/64.1~68.0/53.5~55.4%, 바뀐 LED 가 52.3~76.6/100/66.1~71.1% 예요. K4 Wind 는 0.041 이었고, 바람이 셀수록 구름이 더 끼게 하자 0.049 가 됐어요. 잔해 더미는 처음 14줄이라 울타리와 사람들 뒤에 숨어서 22줄로 키웠어요. 첫 판은 29,393 바이트라 갈라짐 무늬와 더미 위 벽 조각, 날아가는 파편을 빼서 24,914 바이트로 줄였어요. 노브 넷은 0.32, 0.06, 0.06, 0.05 예요. + +### Geyser + +주제 4 로 화산 갈래의 둘째를 세웠어요. Eruption 에 용암과 재가 있으니, 이쪽은 스트로퀴르 같은 간헐천을 땅속까지 세로로 잘랐어요. 71번 줄이 웅덩이 가장자리이고, 80번 줄에서 너비 4 LED 의 화도가 116번 줄 언저리의 물방까지 내려가며, 둘레 지층에 흰 규화 테와 유황빛, 녹빛 물듦이 둘러요. 처음 노브에서는 1.95~2.4초에 청록 유리 돔이 부풀고, 2.4초에 터진 물기둥이 3.3초쯤 6번 줄에 닿고, 3.5초부터 웅덩이가 103번 줄까지 빠져요. Speed 2 에서 한 바퀴는 13.25초예요. 하늘과 땅을 검게 지운 사본에서도 아래 3분의 1이 13.6% 켜지고 14.2~14.7% 바뀌어서, 화도와 물방만으로 문턱을 넘어요. 첫 판은 K3 Power 가 0.0069 로 걸렸어요. 2~3초 창에 돔만 들어서였고, Power 가 큰 기포와 돔을 줄여 더 일찍 터지게 하자 0.048 이 됐어요. 김은 처음에 좁은 흰 기둥이었어요. 덩어리 가장자리를 일차로 바꾸고 왼쪽 위 빛의 기울기로 음영을 주자 뭉게구름이 됐고, 정오 하늘을 (30,84,170) 에서 (20,62,152) 로 짙게 하자 모양 바뀜이 0.036 에서 0.044~0.050 이 됐어요. K4 Wind 는 0.02 였어요. 앞 분출이 남긴 구름을 두껍게 하고 바탕 바람을 초당 3 에서 6 LED 로 올리자 0.048 이 됐어요. Speed 3 에서는 물기둥이 9줄 간격의 원반으로 줄무늬졌어요. 한 프레임의 물 덩어리를 같은 줄에서 띄웠기 때문이라, 출발 자리를 프레임 안에서 흩어 고쳤어요. 노브 넷은 0.27, 0.07, 0.05, 0.05 예요. + +<> + +### Lichtenberg + +남은 자리로 프랙탈 갈래의 넷째를 세우고, 촘촘함과 모양 바뀜이 함께 높은 칸을 겨눴어요. 세운 판재 위아래 끝의 전극에서 두 나무가 유전 파괴 모형(DBM)으로 자라 서로를 향해 타 들어가고, 만나면 이어진 길로 아크가 번쩍인 뒤 전원이 끊기고 다음 판이 미끄러져 들어와요. 앞쪽 끝은 판 전체에서 매 프레임 푸는 라플라스 퍼텐셜의 Pull 제곱에 비례해 자라요. 첫 판은 촘촘함 × 모양 바뀜 3,3(Flipdot 혼자)과 복잡도 × 모양 바뀜 4,3(Rabbit Ears 혼자)에 씨앗 여덟이 다 들었어요. 그런데 트리 2,557칸 가운데 2,333칸이 판 어디서나 돋는 잔가지라서, 4~8초에 1 LED 검은 직교 그물이 판을 덮었어요. 나무무늬보다 미로로 읽혔고, 8초에 1 LED 안에 탄 칸이 없는 나무는 4.0% 였어요. 줄기가 매단 칸 수가 12, 50, 160, 500 을 넘을 때마다 홈을 한 단씩 넓히고, 잔가지는 탄 줄기에서만 줄기 방향의 45도 앞으로 돋아 3~4, 2, 1 LED 로 짧아지며 세 대까지 갈라지게 했어요. 전극 쪽이 굵고 검은 나무가 되고 열린 나무가 42.2% 가 됐어요. 그 대가로 촘촘함이 0.248 에서 0.130 으로 내려가 3,3 칸을 잃었어요. 결 가로로 2.3줄마다 도는 단풍나무 곱슬무늬는 맨 판을 0.093 에서 0.115 로 올렸을 뿐이에요. 이 칸에 선 패턴들처럼 촘촘함 0.2 를 넘기려면 판 전체에 1 LED 결이 깔려야 해서, 빈 나무가 보여야 하는 그림과는 맞지 않았어요. 모양 바뀜은 열린 나무가 40% 를 넘자 0.20 아래로 떨어졌어요. 소다물 광택의 폭을 0.12 에서 0.28 로 키우고 전원이 켜진 동안 판 전체에서 마르게 하자 0.241 이 됐어요. 복잡도가 2.612 라서 4,3 칸에는 씨앗 여덟 가운데 일곱이 남아요. 첫 판에서 판을 60열로 넓혔을 때 복잡도가 2.748 로 축 끝을 넘었고, 채널마다 ±3 의 결을 나무 색에만 굽고 숯은 고른 색으로 두어 되돌렸어요. K3 Pull 은 범위를 0.5..4 에서 1..4 로 바꿔 η=1 에서 열고, 강할수록 앞쪽이 일찍 뻗게 해 노브 반응이 0.02 에서 0.04 가 됐어요. 노브 넷은 0.46, 0.13, 0.04, 0.07 이에요. + +<> + +### Shadow Play + +남은 자리로 무대·공연 갈래의 넷째를 세웠어요. 여러 판 「다음 판 감」에 남아 있던 그림자 인형극을 자바의 와양 쿨릿으로 짰어요. 관객 쪽에서 본 흰 막이 10~102번 줄을 채우고, 46번 줄에 맺힌 등잔 빛이 가운데를 밝히고 가장자리로 갈수록 어두워요. 기사와 거인은 키가 67줄과 75줄이고 막대까지 치면 86줄과 93줄이라, 구멍 뚫린 가죽 무늬째 그림자로 그려요. 인형이 등잔 쪽으로 다가가면 그 거리만큼 그림자가 커지고 반그림자 너비만큼 흐려져요. 그래서 거인이 다가오는 순간과 던져져 날아가는 순간, 구눙안이 쓸고 지나가는 순간에 그림자가 부풀고 번져요. 한 장은 Speed 2 에서 14.4초이고, 다음 장은 좌우를 뒤집어 놀아요. 막을 고르게 밝히고 깜박임을 끈 사본으로 재도 인형만으로 2~8초에 바뀐 LED 가 3분의 1마다 62.5/90.0/35.5% 예요. 판이 대본대로 흘러서 씨앗 넷의 값이 0.5 포인트 안에서 같아요. 막의 가장 밝은 채널은 223~235 에 두어 등잔의 일렁임이 보이게 했어요. 처음 판은 12.0 ms 였어요. 아래 관객 머리 다섯의 윤곽을 LED 마다 네 번씩 다시 셈해서였고, 머리마다 상자 안에서 한 번만 셈하자 2.4 ms 가 됐어요. K4 Flicker 는 불빛만 흔들 때 0.03 이었고, 바람이 막을 인형에서 밀어내 그림자가 부풀고 흐려지게 하자 0.06 이 됐어요. 노브 넷은 0.22, 0.15, 0.10, 0.06 이에요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -1119,7 +1174,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 24일 `dev`(`85baa0d`)의 148개에 25판의 열 개를 더한 158개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 24일 `dev`(`0510761`)의 158개에 26판의 열 개를 더한 168개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -1139,7 +1194,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ### 아직 없는 갈래 -`families.json` 의 마흔여덟 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 25판이 연 농사·밭 갈래는 Rice Terraces 하나이고, 화산 갈래는 Eruption 하나예요. 모를 심는 사람들이 줄지어 물러나는 모내기는 아직 없어요. 24판이 연 공사·건설 갈래는 Tower Crane 과 25판의 Excavation 이에요. 24판이 연 도자기 갈래는 Potter's Wheel 과 25판의 Kintsugi 예요. 장작 가마의 불구멍으로 불꽃이 새는 소성은 아직 없어요. 23판이 연 무대·공연 갈래는 Proscenium 과 24판의 Flying Trapeze, 25판의 Marionette 예요. 막 뒤에서 빛을 받는 그림자 인형극은 아직 없어요. 23판이 연 짐승 갈래는 Giraffe 와 24판의 Emergence, 25판의 Snow Monkeys 예요. 22판이 연 몸 갈래는 Circulation 과 23판의 Axon, 24판의 Breath 예요. 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 이에요. 접힌 종이학이 펼쳐지는 그림은 아직 없어요. 21판이 연 운동·경기 갈래는 Lap Pool 과 22판의 Bowling Lane, 23판의 High Dive 예요. 위아래로 움직이는 경기는 High Dive 가 했고, 장대높이뛰기와 실내 암벽은 아직 없어요. 19판이 연 부엌 갈래는 Popcorn 과 20판의 Kettle, 21판의 Pancakes 이고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판이 연 유리 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori 예요. 광물·결정 갈래는 Bismuth 와 19판의 Dripstone, 20판의 Agate 이고, 놀이공원 갈래는 Drop Tower 와 19판의 Helter Skelter, 20판의 High Striker, 25판의 Ferris Wheel 이에요. 말이 오르내리는 회전목마는 아직 없어요. 17판이 연 전자 회로 갈래는 Circuit Board 와 18판의 Server Rack, 21판의 Nixie 이고, 장난감 갈래는 Marble Run 과 18판의 Peg Light, 21판의 Slinky, 22판의 Pin Art 예요. 16판이 연 바다 갈래는 Abyss 와 17판의 Shoreline, 20판의 Sunglint, 23판의 Bait Ball 이고, 곤충 갈래는 Fireflies 와 17판의 Ant Farm, 20판의 Moth Lamp, 24판의 Waggle Dance 예요. 15판이 연 도시 갈래는 Metro Map 과 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판이 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables, 24판의 Pipe Organ 이에요. 4판이 연 직물 갈래는 Jacquard Loom 과 15판의 Plait, 19판의 Needlepoint, 23판의 Sequins 이고, 8판이 연 기계 갈래는 Gear Train 과 15판의 Cam Wave, 19판의 Paternoster 예요. 12판이 연 천체 갈래는 Star Trails 와 15판의 Analemma, 16판의 Lunation, 22판의 Galilean Moons 예요. 위성의 궤도는 Galilean Moons 가 그렸고, 해를 도는 행성은 아직 없어요. 리듬·펄스 갈래는 1판의 Euclid Pulse 와 Pulse Bars 뒤로 20판의 Change Ringing 이 셋째예요. +`families.json` 의 쉰 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 26판이 연 얼음·극지 갈래는 Iceberg 하나이고, 해빙 위를 달리는 썰매개 무리는 아직 없어요. 미생물 갈래는 Dark Field 하나이고, 형광으로 물들인 세포가 둘로 나뉘는 그림은 아직 없어요. 25판이 연 농사·밭 갈래는 Rice Terraces 와 26판의 Rice Planting 이고, 화산 갈래는 Eruption 과 26판의 Geyser 예요. 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 예요. 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 예요. 위아래로 움직이는 경기는 High Dive 와 Pole Vault 가 했고, 실내 암벽은 아직 없어요. 19판이 연 부엌 갈래는 Popcorn 과 20판의 Kettle, 21판의 Pancakes 이고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판이 연 유리 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori 예요. 광물·결정 갈래는 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 이 셋째예요. ### 멋진 것 더 멋지게 From 7f1d2df496c2e93ec71520a743ebc523f1965b2f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:49:15 +0000 Subject: [PATCH 22/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Origami?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- incoming/pattern-study/patterns/origami.js | 667 +++++++++++++++++++++ 1 file changed, 667 insertions(+) create mode 100644 incoming/pattern-study/patterns/origami.js diff --git a/incoming/pattern-study/patterns/origami.js b/incoming/pattern-study/patterns/origami.js new file mode 100644 index 000000000..513a17ab0 --- /dev/null +++ b/incoming/pattern-study/patterns/origami.js @@ -0,0 +1,667 @@ +// ===== Patternflow pattern ===== +// Title: Origami +// Family: paper +// Idea: A square of origami paper lies on a dark table under a lamp and folds into a crane one crease at a time, each flap swinging over with the light sliding across its face and the white back turning up, then the crane opens flat again and its crease pattern glows faintly before the next sheet is laid on top. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 26, after the orizuru, the paper crane folded from the bird base +// Round: 26 +// =============================== +// @matrix 64x128 +// @knobs Paper=0..1, Speed=0.2..3, Sheets=1..3, Lamp=-1..1 +// +// Knob 1: Paper. Which sheet comes off the pack: teal kami at 0, then indigo chiyogami printed with seigaiha waves, gold foil, pink with white kanoko dots, yellow, green ichimatsu checks, silver foil and vermilion at 1. Each new sheet is the next one in the pack. +// Knob 2: Speed. How fast the hands fold: at 2 a fold takes about a second and a sheet goes from flat to crane and back in twenty-three seconds; at 0.2 it runs ten times slower. +// Knob 3: Sheets. How many sheets fold at once, one above the other, each a step behind the last: one big sheet at 1, three small ones at 3. +// Knob 4: Lamp. Where the lamp stands, from the left of the table at -1 to the right at 1. The flaps that turn toward it light up, the far side of the sheet falls into shade and a lifted flap throws its shadow the other way. +// Buttons: K1 tap opens the crane flat now, or lays the next sheet if it is already open. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, ceil, round, sin, cos, sqrt, abs, hypot, log, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ease = (x) => (x <= 0 ? 0 : x >= 1 ? 1 : x * x * (3 - 2 * x)); +const mix = (a, b, t) => a + (b - a) * t; + +// The sheet is a diamond with corners (0, ±1) and (±1, 0), its crease-pattern +// coordinates. K is where the petal fold's hinge crosses the square base and Q +// is half the width of a leg once it is thinned. +const K = 1 - Math.SQRT1_2; +const Q = Math.SQRT1_2 * Math.tan(PI / 16); + +// An affine map [a, b, c, d, e, f]: x' = a x + b y + e, y' = c x + d y + f. +const mul = (M, A) => [ + M[0] * A[0] + M[1] * A[2], M[0] * A[1] + M[1] * A[3], + M[2] * A[0] + M[3] * A[2], M[2] * A[1] + M[3] * A[3], + M[0] * A[4] + M[1] * A[5] + M[4], M[2] * A[4] + M[3] * A[5] + M[5], +]; +// A flap turned by theta about the hinge through (hx, hy) with unit normal +// (nx, ny) toward the flap, as seen from straight above: k = cos(theta) - 1. +// At k = -2 the flap lies flat on the other side, mirrored. +function hingeM(h, k) { + const [hx, hy, nx, ny] = h; + const hn = hx * nx + hy * ny; + return [1 + k * nx * nx, k * nx * ny, k * nx * ny, 1 + k * ny * ny, -k * hn * nx, -k * hn * ny]; +} + +// The crane. Each step is one or more folds: a crease line, a point on the +// side that moves, which facets go (by the eighth of the sheet they came from +// or where they lie now), and +1 for a valley fold that lands on top or -1 +// for one that goes behind. +const ALL = () => true; +const sec = (...s) => (f) => s.includes(f.sec); +const low = (...s) => (f, x, y) => y > K && s.includes(f.sec); +const legL = (f, x, y) => x < 0 && y > K; +const legR = (f, x, y) => x > 0 && y > K; +// The legs turn up about a line through (0, YP) along NL: neck and tail rise +// 40 degrees above the table's edge. +const YP = 0.34; +const NL = [-0.906, 0.423]; +const CRANE = [ + // In half, corner to corner. + [{ l: [-1, 0, 1, 0], s: [0, -1], m: 1, sel: ALL }], + // Turned so the fold lies along the top. + [], + // The side corners down to the bottom one: the square base. + [{ l: [0, 0, -0.5, 0.5], s: [-1, 0], m: 1, sel: ALL }, { l: [0, 0, 0.5, 0.5], s: [1, 0], m: 1, sel: ALL }], + // Kite folds, the front layer to the front and the back one behind. + [{ l: [0, 1, -K, K], s: [-0.5, 0.5], m: 1, sel: sec(3) }, { l: [0, 1, K, K], s: [0.5, 0.5], m: 1, sel: sec(0) }, + { l: [0, 1, -K, K], s: [-0.5, 0.5], m: -1, sel: sec(2) }, { l: [0, 1, K, K], s: [0.5, 0.5], m: -1, sel: sec(1) }], + // The petal folds: front and back layers lift up past the top. + [{ l: [-1, K, 1, K], s: [0, 1], m: 1, sel: sec(0, 3) }, { l: [-1, K, 1, K], s: [0, 1], m: -1, sel: sec(1, 2) }], + // The sides of the middle layers tuck in: the bird base. + [{ l: [0, 1, -K, K], s: [-0.5, 0.5], m: -1, sel: low(4, 5) }, { l: [0, 1, K, K], s: [0.5, 0.5], m: -1, sel: low(6, 7) }], + // The legs thinned. + [{ l: [0, 1, -Q, K], s: [-K, K], m: -1, sel: legL }, { l: [0, 1, Q, K], s: [K, K], m: -1, sel: legR }], + // The legs reverse-folded up into neck and tail. + [{ l: [0, YP, NL[0], YP + NL[1]], s: [0, 1], m: -1, sel: legL }, { l: [0, YP, -NL[0], YP + NL[1]], s: [0, 1], m: -1, sel: legR }], + // The head. + [{ head: 1, m: -1, sel: (f, x) => f.H[8] && x < 0 }], +]; +// Seconds per step at Speed 2, and how the sheet lies on the table at each +// state: turned 45 degrees it is a square on the panel. +const DUR = [1.1, 0.7, 1.1, 0.8, 1.1, 0.8, 0.7, 1.0, 0.6]; +const PHI = [PI / 4, PI / 4, 0, 0, 0, 0, 0, 0, 0, 0]; +// The finished crane flaps its front wing about the line it was lifted on. +const FLAPH = [0, K, 0, -1, 1]; + +function reflectPt(p, h) { + const d = (p[0] - h[0]) * h[2] + (p[1] - h[1]) * h[3]; + return [p[0] - 2 * d * h[2], p[1] - 2 * d * h[3]]; +} + +function lineOf(mv) { + const [x1, y1, x2, y2] = mv.l; + let nx = y1 - y2; + let ny = x2 - x1; + if ((mv.s[0] - x1) * nx + (mv.s[1] - y1) * ny < 0) (nx = -nx), (ny = -ny); + const l = hypot(nx, ny); + return [x1, y1, nx / l, ny / l]; +} + +// Cut a convex polygon by the sign of d, keeping the side s. +function clip(cp, d, s) { + const out = []; + const m = d.length; + for (let i = 0; i < m; i++) { + const j = (i + 1) % m; + const a = d[i] * s; + const b = d[j] * s; + if (a >= 0) out.push(cp[2 * i], cp[2 * i + 1]); + if ((a > 0 && b < 0) || (a < 0 && b > 0)) { + const t = a / (a - b); + out.push(cp[2 * i] + t * (cp[2 * j] - cp[2 * i]), cp[2 * i + 1] + t * (cp[2 * j + 1] - cp[2 * i + 1])); + } + } + return out; +} + +// Fold the sheet through every step once, cutting facets along each crease +// they cross. Each facet keeps where it lies, which way up and in which layer +// at every state, so any moment of the folding and unfolding is a lookup. +function build(steps) { + const C = [[1, 0], [0.5, 0.5], [0, 1], [-0.5, 0.5], [-1, 0], [-0.5, -0.5], [0, -1], [0.5, -0.5]]; + const F = C.map((c, s) => ({ cp: [0, 0, c[0], c[1], ...C[(s + 1) % 8]], sec: s, T: [[1, 0, 0, 1, 0, 0]], L: [0], H: [null] })); + const n = steps.length; + const lines = []; + for (let k = 1; k <= n; k++) { + let top = -1e9; + let bot = 1e9; + for (const f of F) { + f.T[k] = f.T[k - 1]; + f.L[k] = f.L[k - 1]; + f.H[k] = null; + top = max(top, f.L[k - 1]); + bot = min(bot, f.L[k - 1]); + } + for (const mv of steps[k - 1]) { + let h; + if (mv.head) { + // The tip of the neck, bent down and forward. + const tip = reflectPt([0, 1], lines[k - 2][0]); + const r = [-0.766, -0.643]; + const J = [tip[0] - 0.13 * r[0], tip[1] - 0.13 * r[1]]; + const to = [J[0] - 0.13 * 0.9, J[1] + 0.13 * 0.44]; + const nx = tip[0] - to[0]; + const ny = tip[1] - to[1]; + const l = hypot(nx, ny); + h = [(tip[0] + to[0]) / 2, (tip[1] + to[1]) / 2, nx / l, ny / l]; + } else h = lineOf(mv); + (lines[k - 1] = lines[k - 1] || []).push(h); + const [px, py, nx, ny] = h; + const moved = []; + for (const f of F.slice()) { + if (f.H[k]) continue; + const A = f.T[k - 1]; + const cp = f.cp; + const m = cp.length / 2; + let cx = 0; + let cy = 0; + const d = []; + for (let j = 0; j < m; j++) { + const x = A[0] * cp[2 * j] + A[1] * cp[2 * j + 1] + A[4]; + const y = A[2] * cp[2 * j] + A[3] * cp[2 * j + 1] + A[5]; + cx += x / m; + cy += y / m; + const dd = (x - px) * nx + (y - py) * ny; + d.push(abs(dd) < 1e-9 ? 0 : dd); + } + if (!mv.sel(f, cx, cy) || max(...d) < 1e-6) continue; + let g = f; + if (min(...d) < -1e-6) { + g = { cp: clip(cp, d, 1), sec: f.sec, T: f.T.slice(), L: f.L.slice(), H: f.H.slice() }; + f.cp = clip(cp, d, -1); + F.push(g); + } + moved.push(g); + } + let lo = 1e9; + let hi = -1e9; + for (const g of moved) (lo = min(lo, g.L[k - 1])), (hi = max(hi, g.L[k - 1])); + for (const g of moved) { + g.T[k] = mul(hingeM(h, -2), g.T[k - 1]); + g.H[k] = [px, py, nx, ny, mv.m]; + g.L[k] = mv.m > 0 ? top + 1 + hi - g.L[k - 1] : bot - 1 - (g.L[k - 1] - lo); + } + } + } + // Facets that lie together at a state share a group there: a crease shows + // between two facets that lie together now but were parted at the deepest + // fold. + const nf = F.length; + const gid = new Int32Array(nf * (n + 1)); + const det = new Int8Array(nf * (n + 1)); + for (let k = 0; k <= n; k++) { + const seen = new Map(); + F.forEach((f, i) => { + const key = f.T[k].map((v) => round(v * 1e4)).join(); + if (!seen.has(key)) seen.set(key, seen.size); + gid[i * (n + 1) + k] = seen.get(key); + const A = f.T[k]; + det[i * (n + 1) + k] = A[0] * A[3] - A[1] * A[2] > 0 ? 1 : -1; + }); + } + // The extent of each state, turned as it lies, for the camera. + const box = []; + for (let k = 0; k <= n; k++) { + const c = cos(PHI[k]); + const s = sin(PHI[k]); + let x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9; + for (const f of F) { + const A = f.T[k]; + for (let j = 0; j < f.cp.length; j += 2) { + const x = A[0] * f.cp[j] + A[1] * f.cp[j + 1] + A[4]; + const y = A[2] * f.cp[j] + A[3] * f.cp[j + 1] + A[5]; + const u = x * c - y * s; + const v = x * s + y * c; + (x0 = min(x0, u)), (x1 = max(x1, u)), (y0 = min(y0, v)), (y1 = max(y1, v)); + } + } + const u = (x0 + x1) / 2; + const v = (y0 + y1) / 2; + box.push([u * c + v * s, -u * s + v * c, x1 - x0, y1 - y0]); + } + const tilt = new Float32Array(nf * 2); + for (let i = 0; i < nf * 2; i++) tilt[i] = Math.random() - 0.5; + return { F, n, gid, det, box, tilt }; +} + +// Papers: colour, second colour, print. 0 plain kami, 1 seigaiha waves, +// 2 metal foil, 3 kanoko dots, 4 ichimatsu checks. +const PAPERS = [ + [[0.16, 0.72, 0.8], null, 0], + [[0.13, 0.2, 0.58], [0.78, 0.86, 0.96], 1], + [[0.96, 0.7, 0.26], null, 2], + [[0.96, 0.46, 0.6], [1, 0.95, 0.96], 3], + [[0.98, 0.78, 0.12], null, 0], + [[0.12, 0.46, 0.26], [0.94, 0.88, 0.7], 4], + [[0.74, 0.78, 0.84], null, 2], + [[0.9, 0.16, 0.12], null, 0], +]; +const WHITE = [0.95, 0.93, 0.87]; + +function print(kind, s, t) { + if (kind === 1) { + // Rows of scales two wide, each row half a scale lower and covering the one above. + const X = s * 3; + const Y = t * 3; + const j = ceil(2 * Y); + for (let jj = j + 1; jj >= j; jj--) { + const ox = jj & 1; + const dx = X - ox - 2 * round((X - ox) / 2); + const dy = Y - jj / 2; + const r = sqrt(dx * dx + dy * dy); + if (r < 1 || jj === j) return (r * 3) % 1 < 0.4 ? 1 : 0; + } + } + if (kind === 3) { + const X = s * 5; + const Y = t * 5; + const ox = floor(Y) & 1 ? 0.5 : 0; + const dx = X + ox - floor(X + ox) - 0.5; + const dy = Y - floor(Y) - 0.5; + return dx * dx + dy * dy < 0.06 ? 1 : 0; + } + if (kind === 4) return (floor(s * 4 + 4) + floor(t * 4 + 4)) & 1; + return 0; +} + +// The order a sheet goes through. +const WAIT = 0, IN = 1, REST = 2, FOLD = 3, HOLD = 4, SIT = 5, UNFOLD = 6, CREASE = 7; + +function newSheet(sl, first) { + sl.old = first ? null : { seq: sl.seq, kmax: sl.kmax }; + sl.seq++; + sl.ph = IN; + sl.pt = 0; + sl.k = 0; + sl.u = 1; + sl.kmax = 0; + sl.rush = false; +} + +function slot(i) { + return { i, ph: WAIT, pt: -2.4 * i, k: 0, u: 1, kmax: 0, seq: -1, old: null, rush: false }; +} + +export function setup(params) { + const grain = new Float32Array(N); + for (let y = 0; y < H; y++) { + let w = Math.random(); + for (let x = 0; x < W; x++) { + w += 0.25 * (Math.random() - 0.5); + grain[y * W + x] = 0.75 + 0.2 * sin(y * 0.9 + 3 * sin(x * 0.05 + y * 0.02)) + 0.1 * w; + } + } + params.s = { + M: build(CRANE), + slots: [slot(0)], + n: 1, + base: 0, + rate: 1, + lamp: 0.06, + lampKey: null, + grain, + bg: new Float32Array(N * 3), + LX: new Float32Array(N), + LY: new Float32Array(N), + LZ: new Float32Array(N), + LI: new Float32Array(N), + HZ: new Float32Array(N), + lp: [0, 0, 1], + }; +} + +function stepSlot(sl, h, M) { + if (sl.rush && (sl.ph === UNFOLD || sl.ph === CREASE)) h *= 4; + sl.pt += h; + switch (sl.ph) { + case WAIT: + if (sl.pt >= 0) newSheet(sl, true); + break; + case IN: + if (sl.pt >= 1.2) (sl.ph = REST), (sl.pt = 0), (sl.old = null); + break; + case REST: + if (sl.pt >= 1) (sl.ph = FOLD), (sl.k = 1), (sl.u = 0); + break; + case FOLD: + sl.u = min(1, sl.u + h / DUR[sl.k - 1]); + if (sl.u >= 1) (sl.kmax = max(sl.kmax, sl.k)), (sl.ph = HOLD), (sl.pt = 0); + break; + case HOLD: + if (sl.pt >= 0.25) { + if (sl.k < M.n) (sl.k++), (sl.u = 0), (sl.ph = FOLD); + else (sl.ph = SIT), (sl.pt = 0); + } + break; + case SIT: + if (sl.pt >= 3) sl.ph = UNFOLD; + break; + case UNFOLD: + sl.u -= h / 0.45; + if (sl.u <= 0) { + if (sl.k > 1) (sl.k--), (sl.u += 1); + else (sl.k = 0), (sl.u = 1), (sl.ph = CREASE), (sl.pt = 0); + } + break; + case CREASE: + if (sl.pt >= 2.5 || sl.rush) newSheet(sl, false); + break; + } +} + +export function update(dt, input, params) { + const S = params.s; + if (!S) return; + const k = input.knobValues || [0, 2, 1, 0.06]; + dt = cl(dt || 0, 0, 0.1); + S.base = floor(cl(k[0], 0, 1) * (PAPERS.length - 0.01)); + S.rate = cl(k[1], 0.2, 3) / 2; + S.lamp = cl(k[3], -1, 1); + const n = cl(round(k[2]), 1, 3); + if (n !== S.n) { + S.n = n; + while (S.slots.length < n) S.slots.push(slot(S.slots.length)); + S.slots.length = n; + } + if (input.btnPressed && input.btnPressed[0]) + for (const sl of S.slots) { + if (sl.ph === FOLD || sl.ph === HOLD || sl.ph === SIT) { + if (sl.ph !== FOLD) sl.u = 1; + sl.kmax = max(sl.kmax, sl.k); + sl.ph = UNFOLD; + } else if (sl.ph !== WAIT) { + if (sl.ph === UNFOLD || sl.ph === CREASE) sl.rush = true; + else newSheet(sl, false); + } + } + for (const sl of S.slots) stepSlot(sl, dt * S.rate, S.M); +} + +// The lamp: a bulb LH above the table at (lx, LY0). Each LED keeps the unit +// vector to it, the half vector's height for the shine on foil, and how much +// light reaches it. The table is dark walnut, kept below 24 of 255. +const LH = 80; +const LY0 = 34; +function lampField(S) { + const lx = 31.5 + 44 * S.lamp; + const { LX, LY, LZ, LI, HZ, bg, grain } = S; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + const dx = lx - x - 0.5; + const dy = LY0 - y - 0.5; + const r2 = dx * dx + dy * dy; + const l = sqrt(r2 + LH * LH); + LX[p] = dx / l; + LY[p] = dy / l; + LZ[p] = LH / l; + LI[p] = (LH * LH) / (LH * LH + 0.4 * r2); + HZ[p] = (LZ[p] + 1) / sqrt(LX[p] * LX[p] + LY[p] * LY[p] + (LZ[p] + 1) * (LZ[p] + 1)); + const w = min(21, 255 * 0.085 * (0.25 + 0.75 * LI[p]) * grain[p]); + bg[p * 3] = w; + bg[p * 3 + 1] = w * 0.56; + bg[p * 3 + 2] = w * 0.32; + } + S.lp = [lx, LY0, LH]; +} + +// Per frame: every facet on the table, as drawn. +const MAXR = 1600; +const RA = new Float32Array(MAXR * 6); +const RN = new Float32Array(MAXR * 3); +const RK = new Float32Array(MAXR); +const RS = new Int8Array(MAXR); +const RP = new Int16Array(MAXR); +const RG = new Int32Array(MAXR); +const RX = new Int32Array(MAXR); +const RU = new Int8Array(MAXR); +const RL = new Float32Array(MAXR); +const RQ = new Int16Array(MAXR); +const RM = new Float32Array(MAXR); +const ORD = []; +const RCP = []; +const ID = new Int16Array(N); +const SHK = new Float32Array(N); +const VX = new Float32Array(64); +const VY = new Float32Array(64); +let nr = 0; + +// Fill a convex polygon with v; mode 0 writes facet ids, mode 1 raises the +// shadow key. +function fill(cnt, v, mode) { + let ya = 1e9; + let yb = -1e9; + for (let i = 0; i < cnt; i++) (ya = min(ya, VY[i])), (yb = max(yb, VY[i])); + const y0 = max(0, ceil(ya - 0.5)); + const y1 = min(H - 1, floor(yb - 0.5)); + for (let y = y0; y <= y1; y++) { + const py = y + 0.5; + let xl = 1e9; + let xr = -1e9; + for (let i = 0; i < cnt; i++) { + const j = i + 1 < cnt ? i + 1 : 0; + const ay = VY[i]; + const by = VY[j]; + if ((ay <= py && by > py) || (by <= py && ay > py)) { + const x = VX[i] + ((py - ay) / (by - ay)) * (VX[j] - VX[i]); + (xl = min(xl, x)), (xr = max(xr, x)); + } + } + const x0 = max(0, ceil(xl - 0.5)); + const x1 = min(W - 1, floor(xr - 0.5)); + const row = y * W; + if (mode === 0) for (let x = x0; x <= x1; x++) ID[row + x] = v; + else for (let x = x0; x <= x1; x++) if (SHK[row + x] < v) SHK[row + x] = v; + } +} + +// The camera for state j of a sheet in its slot: turned PHI[j], scaled to fit. +function viewOf(M, j, fw, fh, zmax) { + const b = M.box[j]; + return [PHI[j], log(min(fw / b[2], fh / b[3], zmax)), b[0], b[1]]; +} + +// Put one sheet on the table: model M at step k, progress u, in slot +// region (cy, fw, fh), paper index pi. dx and dphi slide a new sheet in. +function placeSheet(S, M, k, u, flap, kmax, pi, cyp, fw, fh, zmax, dx, dphi, keyBase, mem, qid) { + const n1 = M.n + 1; + const go = k > 0 && u > 0 && u < 1; + const jb = k === 0 ? 0 : u >= 1 ? k : k - 1; + const a = viewOf(M, k > 0 ? k - 1 : 0, fw, fh, zmax); + const b = viewOf(M, k, fw, fh, zmax); + const w = k > 0 ? ease(u) : 1; + const phi = mix(a[0], b[0], w) + dphi; + const z = exp(mix(a[1], b[1], w)); + const ccx = mix(a[2], b[2], w); + const ccy = mix(a[3], b[3], w); + const c = cos(phi) * z; + const s = sin(phi) * z; + const V = [c, -s, s, c, 31.5 + dx - (c * ccx - s * ccy), cyp - (s * ccx + c * ccy)]; + const [lx, ly, lh] = S.lp; + M.F.forEach((f, i) => { + if (nr >= MAXR) return; + const r = nr++; + // The hinge this facet turns on now, if any: the fold under way, or the + // finished crane's wing beat. + let hg = null; + let th = 0; + if (go && f.H[k]) (hg = f.H[k]), (th = PI * ease(u)); + else if (flap > 0 && (f.sec === 0 || f.sec === 3)) (hg = FLAPH), (th = flap); + const A0 = f.T[jb]; + const ct = cos(th); + const st = sin(th); + const A = mul(V, hg ? mul(hingeM(hg, ct - 1), A0) : A0); + RA.set(A, r * 6); + RCP[r] = f.cp; + let side = M.det[i * n1 + jb]; + let nx = 0; + let ny = 0; + let nz = 1; + let key = f.L[jb]; + if (hg) { + const m = hg[4]; + // The hinge normal as it lies on the panel. + const hx = hg[2] * cos(phi) - hg[3] * sin(phi); + const hy = hg[2] * sin(phi) + hg[3] * cos(phi); + nx = -m * st * hx; + ny = -m * st * hy; + nz = ct; + if (ct < 0) (nx = -nx), (ny = -ny), (nz = -nz), (side = -side); + key = (m > 0 ? 1e6 : -1e6) + (ct >= 0 ? f.L[jb] : go ? f.L[k] : -f.L[jb]); + // A lifted flap throws its shadow away from the lamp. + if (m > 0 && st > 0.02) { + const cp = f.cp; + const cnt = cp.length / 2; + for (let j = 0; j < cnt; j++) { + const x0 = A0[0] * cp[2 * j] + A0[1] * cp[2 * j + 1] + A0[4]; + const y0 = A0[2] * cp[2 * j] + A0[3] * cp[2 * j + 1] + A0[5]; + const hz = ((x0 - hg[0]) * hg[2] + (y0 - hg[1]) * hg[3]) * st * z; + const px = A[0] * cp[2 * j] + A[1] * cp[2 * j + 1] + A[4]; + const py = A[2] * cp[2 * j] + A[3] * cp[2 * j + 1] + A[5]; + VX[j] = px + (hz * (px - lx)) / lh; + VY[j] = py + (hz * (py - ly)) / lh; + } + fill(cnt, keyBase + key, 1); + } + } + const tl = 0.05 + 0.1 * mem; + nx += tl * M.tilt[2 * i]; + ny += tl * M.tilt[2 * i + 1]; + const nl = hypot(nx, ny, nz); + RN[r * 3] = nx / nl; + RN[r * 3 + 1] = ny / nl; + RN[r * 3 + 2] = nz / nl; + RS[r] = side; + RK[r] = keyBase + key; + RP[r] = pi; + RG[r] = M.gid[i * n1 + jb]; + RX[r] = M.gid[i * n1 + kmax]; + RU[r] = M.det[i * n1 + kmax]; + RL[r] = f.L[kmax]; + RQ[r] = qid; + RM[r] = mem; + ORD.push(r); + }); + return V; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + if (S.lampKey !== S.lamp) (S.lampKey = S.lamp), lampField(S); + const { M, LX, LY, LZ, LI, HZ, bg } = S; + nr = 0; + ORD.length = 0; + SHK.fill(-1e12); + const n = S.slots.length; + const sh = H / n; + const zmax = [74, 50, 34][n - 1]; + const fw = n === 1 ? 60 : n === 2 ? 56 : 46; + const fh = sh - (n === 1 ? 12 : 8); + let q = 0; + for (const sl of S.slots) { + if (sl.ph === WAIT) continue; + const cyp = sl.i * sh + sh / 2; + const pi = (S.base + 3 * sl.i + sl.seq) % PAPERS.length; + if (sl.old) placeSheet(S, M, 0, 1, 0, sl.old.kmax, (S.base + 3 * sl.i + sl.old.seq) % PAPERS.length, cyp, fw, fh, zmax, 0, 0, -1e8, 1, q++); + let dx = 0; + let dphi = 0; + if (sl.ph === IN) { + const e = 1 - ease(sl.pt / 1.2); + dx = -78 * e; + dphi = 0.35 * e; + } + let flap = 0; + if (sl.ph === SIT) { + // The crane beats its wing once. + const t = (sl.pt - 0.6) / 1.6; + if (t > 0 && t < 1) flap = 0.78 * PI * sin(PI * t) ** 2; + } + const mem = sl.ph === UNFOLD || sl.ph === CREASE || (sl.ph === FOLD && sl.kmax >= M.n) ? 1 : 0; + placeSheet(S, M, sl.k, sl.u, flap, sl.kmax, pi, cyp, fw, fh, zmax, dx, dphi, 0, mem, q++); + } + ORD.sort((a, b) => RK[a] - RK[b]); + ID.fill(-1); + for (const r of ORD) { + const cp = RCP[r]; + const o = r * 6; + const cnt = cp.length / 2; + for (let j = 0; j < cnt; j++) { + VX[j] = RA[o] * cp[2 * j] + RA[o + 1] * cp[2 * j + 1] + RA[o + 4]; + VY[j] = RA[o + 2] * cp[2 * j] + RA[o + 3] * cp[2 * j + 1] + RA[o + 5]; + } + fill(cnt, r, 0); + } + const lx = S.lp[0]; + const sx = lx < 31.5 ? -1 : 1; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + const r = ID[p]; + let R; + let G; + let B; + if (r < 0) { + const f = SHK[p] > -1e11 ? 0.55 : 1; + R = bg[p * 3] * f; + G = bg[p * 3 + 1] * f; + B = bg[p * 3 + 2] * f; + } else { + const o = r * 6; + const a = RA[o]; + const b = RA[o + 1]; + const c = RA[o + 2]; + const d = RA[o + 3]; + const det = a * d - b * c; + const P = PAPERS[RP[r]]; + let col = P[0]; + let kind = P[2]; + if (RS[r] < 0) (col = WHITE), (kind = -1); + else if (kind === 1 || kind === 3 || kind === 4) { + if (abs(det) > 1e-3) { + const X = x + 0.5 - RA[o + 4]; + const Y = y + 0.5 - RA[o + 5]; + const cu = (d * X - b * Y) / det; + const cv = (-c * X + a * Y) / det; + if (print(kind, cu + cv, cv - cu)) col = P[1]; + } + } + const nx = RN[r * 3]; + const ny = RN[r * 3 + 1]; + const nz = RN[r * 3 + 2]; + const dif = max(0, nx * LX[p] + ny * LY[p] + nz * LZ[p]); + const I = LI[p]; + let lit = I * (0.22 + 0.9 * dif); + let sp = 0; + const hz = nz * HZ[p]; + if (kind === 2) { + lit *= 0.45; + sp = 1.3 * I * max(0, hz) ** 30; + } else sp = 0.06 * I * max(0, hz) ** 8; + // Shade under a lifted flap, and along the edge of a layer above. + const k = RK[r]; + let f = SHK[p] > k ? 0.55 : 1; + const qx = x - sx; + const qy = y - 1; + if (qx >= 0 && qx < W && qy >= 0) { + const s = ID[qy * W + qx]; + if (s >= 0 && RQ[s] === RQ[r] && RK[s] > k + 0.5 && RG[s] !== RG[r]) f *= 0.66; + } + // Creases left from the folding. + if (RM[r] > 0) { + for (const s of [x + 1 < W ? ID[p + 1] : -1, y + 1 < H ? ID[p + W] : -1]) + if (s >= 0 && s !== r && RQ[s] === RQ[r] && RG[s] === RG[r] && RX[s] !== RX[r]) { + const up = RL[s] > RL[r] ? s : r; + f *= RU[up] * RS[r] > 0 ? 1.22 : 0.72; + break; + } + } + R = 255 * f * (col[0] * lit + sp * (kind === 2 ? col[0] : 1)); + G = 255 * f * (col[1] * lit + sp * (kind === 2 ? col[1] : 1)); + B = 255 * f * (col[2] * lit + sp * (kind === 2 ? col[2] : 1)); + } + display.setPixel(x, y, min(255, R), min(255, G), min(255, B)); + } +} From 79939b7996099d44c12a4e34f86bf7483eb4d879 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:54:04 +0000 Subject: [PATCH 23/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Pole=20Vault?= 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_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/patterns/pole-vault.js | 426 ++++++++++++++++++ 1 file changed, 426 insertions(+) create mode 100644 incoming/pattern-study/patterns/pole-vault.js diff --git a/incoming/pattern-study/patterns/pole-vault.js b/incoming/pattern-study/patterns/pole-vault.js new file mode 100644 index 000000000..b6f35b521 --- /dev/null +++ b/incoming/pattern-study/patterns/pole-vault.js @@ -0,0 +1,426 @@ +// ===== Patternflow pattern ===== +// Title: Pole Vault +// Family: sport +// Idea: A pole vault under night floodlights, seen from the side: the camera runs with the vaulter, the pole bends deep in the box, the vaulter swings up head-down, turns over a bar near the top of the panel and drops onto the blue mat as the judge raises a white or a red flag. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 26, after the pole vault in athletics +// Round: 26 +// =============================== +// @matrix 64x128 +// @knobs Colour=0..1, Speed=0.2..3, Bar=5..6.2, Wind=-1..1 +// +// Knob 1: Colour. The kit, gold with blue shorts at 0 and once round the wheel; the stand lights, seats and boards take the opposite colour. +// Knob 2: Speed. Real time at 2, where the vault from plant to landing takes 2.4 s; ten times slower at 0.2. +// Knob 3: Bar. The opening height, 5.00 to 6.20 m at 14 rows a metre; a higher bar takes a longer pole and a longer run-up. It goes up 10 cm after each clearance and back here after three misses. +// Knob 4: Wind. Headwind to tailwind: the roof flags stream one way or the other, and a tailwind speeds up the run. +// Buttons: K1 tap sends the next vaulter down the runway now. + +const W = 64, H = 128, N = W * H; +// The runway (GY), the pole's tip in the box, 14 rows a metre. A point z LEDs +// nearer the camera lands OBX right and OBY down. The standards stand XS past +// the box, ZS either side; the mat starts at MAT0, MZ either side, MH tall. +const GY = 104, BOXY = GY + 2, PXM = 14, OBX = 0.55, OBY = 0.22, XS = 5, ZS = 13, MAT0 = 3, MZ = 11, MH = 9; +// The camera's left edge on the bar, gravity at Speed 2, the body. +const CAMF = -26, G = 170, TORSO = 4.2, ARM = 5, LEG = 3.3, TACC = 0.7, TA = 1.25, PSI1 = 1.64; +const [SET, RUN, POLE, FLY, MAT] = [0, 1, 2, 3, 4]; +// The stands texture: CW columns that wrap, rows ST to 83. +const CW = 256, ST = 17, CH = 84 - ST; + +const { min, max, floor, ceil, round, sin, cos, abs, sqrt, exp, atan2, asin, acos, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (v) => ((v = cl(v, 0, 1)), v * v * (3 - 2 * v)); +const lerp = (a, b, u) => a + (b - a) * u; +const rnd = () => Math.random(); +// A camera flash somewhere in the stands. +const flash = (t0) => ({ x: floor(rnd() * W), y: ST + 2 + floor(rnd() * 64), t0 }); +const F = (n) => new Float32Array(n); +const hash = (a, b) => ((Math.imul(a * 7919 + b * 104729, 2654435761) >>> 8) & 1023) / 1024; +const hsv = (h, s, v) => [5, 3, 1].map((n) => v - v * s * max(0, min((n + h * 6) % 6, 4 - ((n + h * 6) % 6), 1))); +const md = (a, m) => ((a % m) + m) % m; +const sc3 = (c, k, o = 0) => [c[0] * k + o, c[1] * k + o, c[2] * k + o]; +let C = null; +const put = (x, y, c) => x >= 0 && x < W && y >= 0 && y < H && C.set(c, (y * W + x) * 3); + +// Poses: [time, body angle, arm, near hip, near knee, far hip, far knee]. On +// the pole the body rocks back to head-down (-PI); after the half turn it +// goes on the other way, face down over the bar and onto the back. +const TAKE = [1.5, 1.6, -0.5, 0.3]; +const VAULT = [[0, 0.08, 0.15, 1.5, 1.6, -0.5, 0.3], [0.2, -0.3, 0.25, 1.1, 1.3, 0.6, 0.2], [0.42, -1.25, 0.4, 2.1, 1.8, 2, 1.5], [0.62, -2.4, 0.9, 0.9, 0.5, 0.8, 0.4], [0.82, -3.05, 2.5, 0.2, 0.05, 0.15, 0.05], [1, -3.14, 0.35, 0.1, 0, 0.1, 0]]; +const FLYK = [[0, PI, 0.35, 0.1, 0, 0.1, 0], [1, 1.39 * PI, 0.4, 0.7, 0.4, 0.6, 0.5], [1.4, 1.95 * PI, 0.5, 0.6, 0.3, 0.5, 0.4], [2, 2.45 * PI, 1.5, 1.2, 0.6, 1, 0.8]]; +const MATK = [[0, 2.45 * PI, 1.5, 1.2, 0.6, 1, 0.8], [0.4, 2.5 * PI, 1.7, 0.8, 0.9, 0.7, 1], [0.95, 2 * PI + 0.15, 2.7, 1.5, 1.8, 1.4, 1.7]]; +function key(T, u, out) { + let j = 0; + while (j < T.length - 2 && u > T[j + 1][0]) j++; + const a = T[j], b = T[j + 1], f = cl((u - a[0]) / (b[0] - a[0]), 0, 1); + for (let i = 1; i < 7; i++) out[i - 1] = a[i] + (b[i] - a[i]) * f; + return out; +} +// How far below the hip the lower foot is, for a body leaning th. +const drop = (th, L) => LEG * max(cos(th - L[0]) + cos(th - L[0] + L[1]), cos(th - L[2]) + cos(th - L[2] + L[3])); + +// Built once: the stands, two LEDs a seat in rows three deep, aisles, empty +// seats in the team colour, the upper tier in the roof's shadow; the glare. +function build(S) { + const cr = F(CW * CH * 3), seat = F(CW * CH), glare = F(30 * 128); + for (let y = 0; y < CH; y++) { + const lo = y + ST >= 52, ty = y + ST - (lo ? 52 : ST), band = ty % 3, tier = floor(ty / 3) + lo * 40; + const li = lo ? 0.62 + (0.38 * ty) / 31 : 0.3 + (0.42 * ty) / 29; + for (let x = 0, sn = 0; x < CW; x++, sn = x >> 1) { + let c = x % 48 < 2 ? [58, 58, 64] : [20, 20, 25]; + if (x % 48 > 1 && band < 2) + if (hash(sn, tier) > 0.8) (seat[y * CW + x] = (0.24 + 0.08 * band) * li), (c = [3, 3, 4]); + else c = band ? sc3(hsv(hash(sn * 3, tier), 0.55, 0.2 + 0.14 * hash(tier, sn)), 255) : x & 1 ? [14, 11, 10] : hash(sn, tier + 99) < 0.5 ? [112, 78, 58] : [60, 42, 32]; + cr.set(sc3(c, li), (y * CW + x) * 3); + } + } + for (let i = 0; i < 30 * 128; i++) { + const dx = (i % 128) - 64, dy = floor(i / 128) - 3.5, d2 = dx * dx + dy * dy; + glare[i] = 70 * exp(-d2 / 16) + 30 * exp(-d2 / 110) + 30 * exp(-dy * dy / 1.5 - abs(dx) / 12); + } + Object.assign(S, { cr, seat, glare }); +} + +export function setup(params) { + const S = { t: 0, ph: SET, pt: 0, tset: 1.95, n: 0, notch: 0, miss: 0, base: 5, by: 34, cam: CAMF, wind: 0, hk: 0.13, marks: [], hudF: 0, verdict: 0, done: 0 }; + Object.assign(S, { clear: true, fig: null, free: null, flashes: [], C: F(N * 3), q: F(6) }); + S.bar = { st: 0, t: 0, y: [0, 0], v: [0, 0] }; + build(S); + params.s = S; + newVaulter(S); +} + +// Each vaulter wears a different kit; the stand lights take the opposite colour. +const newVaulter = (S) => (S.vo = [0, 0.36, 0.62][S.n++ % 3]); +function colours(S) { + const hue = (S.hk + S.vo) % 1, cols = [sc3(hsv(hue, 0.82, 1), 245), sc3(hsv((hue + 0.5) % 1, 0.75, 0.8), 210), [232, 168, 128], [150, 100, 70]]; + S.cols = cols.concat(cols.map((c) => sc3(c, 0.62))); + S.acc = hsv((S.hk + 0.5) % 1, 0.75, 1); + S.tint = sc3(S.acc, 0.5, 0.5); +} + +// The cut to the runway: the takeoff spot is where a straight pole reaches +// the box from hands held straight up. +function startRun(S) { + if (S.ph === MAT) newVaulter(S); + const h = S.base + 0.1 * S.notch, Lg = 92.5 - GY + PXM * h, th = 0.08, hy = GY - drop(th, TAKE) - 0.8; + const px = TORSO * sin(th) + ARM * sin(th + 0.15), py = hy - TORSO * cos(th) - ARM * cos(th + 0.15); + const dx = sqrt(Lg * Lg - (BOXY - py) ** 2), D = 88 + 16 * (h - 5); + S.R = { Lg, D, dist: 0, Xto: -dx - px, hy, bto: -asin((BOXY - py) / Lg), psi0: atan2(BOXY - py, dx) }; + Object.assign(S, { ph: RUN, pt: 0, clear: rnd() < 0.72 - 0.08 * S.notch, wob: rnd() < 0.5, free: null, verdict: 0, done: 0, cam: S.R.Xto - D - 16 }); + S.bar = { st: 0, t: 0, y: [0, 0], v: [0, 0] }; + runPose(S); +} + +function runPose(S) { + const R = S.R, rem = R.D - R.dist, X = R.Xto - rem, ph = (R.dist / 15) * PI + 0.6, e2 = sm(1 - rem / 15), L = []; + for (let i = 0; i < 2; i++) L.push(0.2 + 0.7 * sin(ph + i * PI), 0.25 + 1.3 * max(0, cos(ph + i * PI + 0.9))); + for (let i = 0; i < 4; i++) L[i] = lerp(L[i], TAKE[i], e2); + const th = lerp(0.16, 0.08, e2), hy = lerp(GY - drop(th, L) - 0.3, R.hy, e2), e = sm(1 - rem / 19); + // The pole tip drops over the last strides; then the top hand goes up as + // the tip slides into the box. + const p1x = lerp(X - 1, X + TORSO * sin(th) + ARM * sin(th + 0.15), e), p1y = lerp(hy - 0.5, hy - TORSO * cos(th) - ARM * cos(th + 0.15), e); + const beta = lerp(lerp(1.25 + 0.04 * sin(ph * 2), 0.12, sm((70 - rem) / 51)), R.bto, e), dx = cos(beta), dy = -sin(beta); + S.fig = { x: X, y: hy, th, fc: 1, L, hands: [p1x, p1y, p1x + 4.5 * dx, p1y + 4.5 * dy] }; + S.pole = { x: p1x - 2.5 * dx, y: p1y - 2.5 * dy, x1: p1x + R.Lg * dx, y1: p1y + R.Lg * dy }; +} + +// The bent pole as an arc of length L from the box to the top hand. +const ARC = F(5); +function arc(px, py, c, L) { + const r = cl(c / L, 0.3, 1); + let x = sqrt(6 * (1 - r)) + 1e-4; + for (let i = 0; i < 5; i++) x = cl(x - (sin(x) - r * x) / (cos(x) - r), 1e-4, 1.56); + const R = L / (2 * x), d = R * cos(x), cx = px / 2 + ((py - BOXY) / c) * d, cy = (py + BOXY) / 2 - (px / c) * d, a0 = atan2(BOXY - cy, -cx); + ARC.set([cx, cy, R, a0, md(atan2(py - cy, px - cx) - a0 + PI, 2 * PI) - PI]); + return ARC; +} + +function polePose(S) { + const R = S.R, u = min(1, S.pt / TA), L = R.Lg; + const c = L * (1 - 0.25 * sin(PI * min(1, u / 0.78)) ** 1.4), psi = R.psi0 + (PSI1 - R.psi0) * (1 - (1 - u) ** 1.6); + const p1x = -c * cos(psi), p1y = BOXY - c * sin(psi), q = key(VAULT, u, S.q), th = q[0], a = q[1]; + const fc = cos(PI * sm((u - 0.66) / 0.24)), ux = sin(th), uy = -cos(th); + // Hang the body from the top hand; the bottom hand is 4.5 LEDs down the arc. + const ax = ux * cos(a) + cos(th) * fc * sin(a), ay = uy * cos(a) + sin(th) * fc * sin(a); + const A = arc(p1x, p1y, c, L), b = A[3] + (A[4] * (L - 4.5)) / L; + S.fig = { x: p1x - ARM * ax - TORSO * ux, y: p1y - ARM * ay - TORSO * uy, th, fc, L: q.slice(2), hands: [p1x, p1y, A[0] + A[2] * cos(b), A[1] + A[2] * sin(b)] }; + S.pole = { c, L, px: p1x, py: p1y }; +} + +function matPose(S, tau) { + const q = key(MATK, tau, S.q), c = S.clear, e = sm((tau - (c ? 1.1 : 1.6)) / 0.3), w = max(0, tau - 1.9) * 20, s = sin(w * 0.4); + let L = q.slice(2).map((v) => v * (1 - e)), arms = [lerp(q[1], c ? 0.25 : 2.9, e), lerp(q[1] + 0.3, c ? 0.45 : 3.1, e)]; + // Up with arms high and a hop if the bar stayed; then off to the right. + const Y = lerp(GY - MH - 1 + 0.6 * sm((tau - 0.4) / 0.5), GY - MH - 6.6 - 1.5 * c * abs(sin((tau - 1.1) * 9)) * (tau < 1.9), e); + if (w) [L, arms] = [[0.5 * s, 0.4, -0.5 * s, 0.4], [PI - 0.4 * s, PI + 0.4 * s]]; + S.fig = { x: S.lx + 2 * sm(tau / 0.5) + w, y: w ? GY - MH - 6.6 : Y, th: w ? 0.05 : lerp(q[0], 2 * PI, e), fc: w ? 1 : -1, L, arms }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 5, 0.06]; + const st = (cl(dt || 0, 0, 0.1) * cl(k[1], 0.2, 3)) / 2, first = S.t === 0; + S.hk = (0.13 + cl(k[0], 0, 1)) % 1; + S.base = round(cl(k[2], 5, 6.2) * 20) / 20; + S.wind = cl(k[3], -1, 1); + S.t += st; + if (input.btnPressed && input.btnPressed[0] && (S.ph === SET || S.ph === MAT)) startRun(S); + S.pt += st; + const R = S.R, b = S.bar, tau = S.pt, tgt = GY - PXM * (S.base + 0.1 * S.notch); + if (first) S.by = tgt; + if (b.st !== 1 && b.st !== 3) S.by += (tgt - S.by) * min(1, st * 3); + if (S.ph > POLE) S.cam = CAMF; + + if (S.ph === SET) { + [S.cam, S.fig] = [CAMF, null]; + if (tau >= S.tset) startRun(S); + } else if (S.ph === RUN) { + R.dist = min(R.D, R.dist + 62 * (1 + 0.12 * S.wind) * min(1, 0.3 + tau / TACC) * st); + const rem = R.D - R.dist; + runPose(S); + S.cam = R.Xto - rem - lerp(16, 6, sm(1 - rem / 40)); + if (rem <= 0) [S.ph, S.pt, R.cam] = [POLE, 0, S.cam]; + } else if (S.ph === POLE) { + polePose(S); + S.cam = lerp(R.cam, CAMF, sm(tau / TA / 0.75)); + if (tau >= TA) { + // Let go: the hips fly on to just over the bar, or into it. + const f = S.fig, ya = S.by - (S.clear ? 3.5 : 0.3), tap = sqrt((2 * max(3, f.y - ya)) / G); + S.fl = { x: f.x, y: f.y, vx: cl((XS + 1 - f.x) / tap, 6, 30), vy: G * tap, tap, tf: sqrt((2 * max(10, GY - MH - 1.5 - ya)) / G) }; + S.free = { a: PI / 2 - PSI1, w: 1.4, L: R.Lg + 2.5, amax: acos(2 / (R.Lg + 2.5)) }; + [S.ph, S.pt] = [FLY, 0]; + } + } else if (S.ph === FLY) { + const f = S.fl, X = f.x + f.vx * tau, Y = f.y - f.vy * tau + 0.5 * G * tau * tau; + const q = key(FLYK, tau < f.tap ? tau / f.tap : 1 + (tau - f.tap) / f.tf, S.q); + S.fig = { x: X, y: Y, th: q[0], fc: -1, L: q.slice(2), arms: [q[1], q[1] + 0.25] }; + // Every camera in the house goes off at the top, the two in the infield first. + if (!f.fl && (f.fl = tau >= f.tap - 0.05)) + for (let i = 0; i < 18; i++) S.flashes.push(i < 2 ? { x: i * 36 + 20, y: 114 + i * 5, t0: S.t + i * 0.1 } : flash(S.t + rnd() * 0.6)); + if (tau >= f.tap + 0.05 && b.st === 0 && (!S.clear || S.wob)) [b.st, b.t] = [S.clear ? 2 : 1, 0]; + if (tau > f.tap && Y >= GY - MH - 1.5) Object.assign(S, { ph: MAT, pt: 0, lx: X }); + } else if (S.ph === MAT) { + matPose(S, tau); + if (tau >= 0.5 && !S.verdict) [S.verdict, S.hudF, S.miss] = [2 - S.clear, 1, S.miss + !S.clear, S.marks.push(S.clear ? "0" : "X")]; + // A clearance puts the bar up a notch; a third miss sends it back. + if (tau >= 1.6 && !S.done && (S.done = 1) && (S.clear || S.miss >= 3)) [S.marks, S.miss, S.notch] = [[], 0, S.clear && S.notch < 6 ? S.notch + 1 : 0]; + if (tau >= 2.3 && b.st === 3) b.st = 4; + if (tau >= 2.9) Object.assign(S, { ph: SET, pt: 0, tset: 0.7, free: null }), newVaulter(S), (b.st = 0); + } + + // The pole, once let go, falls back about the box onto the runway. + const P = S.free; + if (P) { + P.w += ((3 * G) / (2 * P.L)) * sin(P.a) * st; + P.a += P.w * st; + if (P.a > P.amax) [P.a, P.w] = [P.amax, P.w > 0.6 ? -0.3 * P.w : 0]; + } + // The bar shivers, and when it was hit it falls off the pegs onto the mat. + b.t += st; + if (b.st === 1 && b.t > 0.3) Object.assign(b, { st: 3, y: [S.by - OBY * ZS, S.by + OBY * ZS], v: [0, 0] }); + for (let i = 0, fl; i < 2 && b.st === 3; i++) { + if (b.t < 0.3 + i * 0.08) continue; + b.y[i] += (b.v[i] += G * st) * st; + if (b.y[i] > (fl = GY - MH + OBY * MZ * (2 * i - 1) - 0.5)) [b.y[i], b.v[i]] = [fl, b.v[i] > 40 ? -0.3 * b.v[i] : 0]; + } + S.hudF = max(0, S.hudF - st * 1.6); + S.flashes = S.flashes.filter((f) => S.t - f.t0 < 0.2); + if (rnd() < st * 0.8) S.flashes.push(flash(S.t)); +} + +// A figure, hip at (hx, hy): th turns the body clockwise from upright, fc +// faces it right (1) or left (-1). Arms reach for the hands, or hang at angles. +const PART = F(96); +function figure(S, hx, hy, th, fc, arms, L, hands, cols, sc) { + hx -= S.cam; + const ux = sin(th), uy = -cos(th), fx = cos(th) * fc, fy = sin(th) * fc, nx = hx + ux * TORSO * sc, ny = hy + uy * TORSO * sc; + let n = 0; + const part = (ax, ay, bx, by, r, c) => (PART.set([ax, ay, bx, by, r * sc, c], n), (n += 6)); + const limb = (x, y, a, l) => [x + l * sc * (ux * cos(a) + fx * sin(a)), y + l * sc * (uy * cos(a) + fy * sin(a))]; + // Far limbs first and in shade, then the body, then the near limbs. + for (let side = 1; side >= 0; side--) { + const [kx, ky] = limb(hx, hy, PI - L[side * 2], LEG), d = side * 4; + const [ax, ay] = limb(kx, ky, PI - L[side * 2] + L[side * 2 + 1], LEG); + const [wx, wy] = hands ? [hands[side * 2] - S.cam, hands[side * 2 + 1]] : limb(nx, ny, arms[side], ARM); + part(hx, hy, kx, ky, 0.85, 1 + d); + part(kx, ky, ax, ay, 0.62, 2 + d); + part(nx, ny, wx, wy, 0.55, 2 + d); + if (side) part(hx, hy, nx, ny, 1.05, 0), part(nx + ux * 1.7 * sc, ny + uy * 1.7 * sc, nx + ux * 1.8 * sc, ny + uy * 1.8 * sc, 1.25, 3); + } + let [x0, x1, y0, y1] = [99, -99, 999, -99]; + for (let j = 0; j < n; j += 6) [x0, x1, y0, y1] = [min(x0, PART[j], PART[j + 2]), max(x1, PART[j], PART[j + 2]), min(y0, PART[j + 1], PART[j + 3]), max(y1, PART[j + 1], PART[j + 3])]; + for (let y = max(0, floor(y0 - 2)); y <= min(H - 1, ceil(y1 + 2)); y++) + for (let x = max(0, floor(x0 - 2)); x <= min(W - 1, ceil(x1 + 2)); x++) { + const px = x + 0.5, py = y + 0.5, i = y * W + x; + for (let j = 0; j < n; j += 6) { + const ax = PART[j], ay = PART[j + 1], dx = PART[j + 2] - ax, dy = PART[j + 3] - ay; + const tt = cl(((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy + 1e-6), 0, 1); + const a = cl(PART[j + 4] + 0.5 - sqrt((px - ax - dx * tt) ** 2 + (py - ay - dy * tt) ** 2), 0, 1); + if (a <= 0) continue; + const col = cols[PART[j + 5]]; + for (let c = 0; c < 3; c++) C[i * 3 + c] += (col[c] - C[i * 3 + c]) * a; + } + } +} + +// The pole, two samples an LED, each shared over four LEDs. +const POLEC = [235, 232, 214]; +function splat(x, y) { + const ix = floor(x - 0.5), iy = floor(y - 0.5), fx = x - 0.5 - ix, fy = y - 0.5 - iy; + for (let j = 0; j < 4; j++) { + const xx = ix + (j & 1), yy = iy + (j >> 1), w = (j & 1 ? fx : 1 - fx) * (j >> 1 ? fy : 1 - fy), k = (yy * W + xx) * 3; + if (xx >= 0 && xx < W && yy >= 0 && yy < H) for (let c = 0; c < 3; c++) C[k + c] += (POLEC[c] - C[k + c]) * w; + } +} +function seg(x0, y0, x1, y1) { + for (let i = 0, n = ceil(Math.hypot(x1 - x0, y1 - y0) * 2) + 1; i <= n; i++) splat(lerp(x0, x1, i / n), lerp(y0, y1, i / n)); +} + +const FONT = { 0: 0x7b6f, 1: 0x2c97, 2: 0x73e7, 3: 0x73cf, 4: 0x5bc9, 5: 0x79cf, 6: 0x79ef, 7: 0x7249, 8: 0x7bef, 9: 0x7bcf, M: 0x5fed, X: 0x5aad, ".": 4 }; +function text(s, x, y, col) { + for (const ch of s) { + for (let j = 0; j < 15; j++) if ((FONT[ch] >> (14 - j)) & 1) put(x + (j % 3), y + floor(j / 3), col); + x += ch === "." ? 2 : 4; + } +} +// A photographer kneeling in the infield, lens up at the bar. +const SNAP = "..oo.....oo.cc.####c.######..####...###...##.##.."; + +// Sky, roof, stands, LED ribbon, boards, track and grass, each panning by its depth. +function back(S) { + const { cr, seat } = S, cam = S.cam, t = S.t, A = S.acc, T = S.tint, s0 = round(cam * 0.4), bx = cam * 0.85; + for (let y = 0, j = 0; y < H; y++) + for (let x = 0; x < W; x++, j += 3) { + let r, g, b, v = -1; + if (y < 13) (r = 5 + y), (g = 8 + 1.2 * y + 4 * T[1]), (b = 22 + 2 * y); + else if (y < ST) { + if (y === 15 && md(s0 + x, 3) === 0) v = 190; + else r = g = b = y === 13 ? 46 : y === 16 ? 10 : 22; + } else if (y >= 46 && y < 52) { + v = y > 46 && y < 51 ? (md(floor((s0 + x + t * 12) / 2) * 7 + y, 11) < 5 ? 210 : 50) : 14; + } else if (y < 84) { + const i = (y - ST) * CW + md(s0 + x, CW), sw = seat[i] * 255; + (r = cr[i * 3] * T[0] + sw * A[0]), (g = cr[i * 3 + 1] * T[1] + sw * A[1]), (b = cr[i * 3 + 2] * T[2] + sw * A[2]); + } else if (y < 92) { + const u = x + bx, sg = floor(u / 40), l = u - sg * 40, on = y > 85 && y < 90 && hash(floor((l + t * 9) / 2) + sg * 50, y) < 0.5; + if (l < 1 || y === 91) r = g = b = 6; + else if (md(sg, 2) ? on : !on) v = on ? 150 : 55; + else r = g = b = on ? 150 : 8; + } else if (y < 107) { + // The track, then the runway with white edges and metre marks. + const X = x + cam, li = y > 100 ? 1 : 0.55 + 0.03 * (y - 92), sp = hash(md(floor(X), 997), y) * 14; + if (y === 96 || y === 101 || y === 106 || (y === 105 && md(floor(X), 14) === 0)) r = g = b = 175 * li; + else (r = (140 + sp) * li), (g = (58 + sp * 0.4) * li), (b = (40 + sp * 0.3) * li); + } else { + // Infield grass in mown stripes. + const q = (md(floor((x + cam * 1.15 + (y - 107) * 1.3) / 11), 2) ? 0.78 : 1) * (1 - (y - 107) * 0.02); + (r = 24 * q), (g = 74 * q), (b = 32 * q); + } + if (v >= 0) (r = A[0] * v + (y < ST) * 60), (g = A[1] * v + (y < ST) * 60), (b = A[2] * v + (y < ST) * 60); + C[j] = r; + C[j + 1] = g; + C[j + 2] = b; + } + // Floodlight towers above the roof, and flags streaming or hanging limp. + for (let k = -3; k <= 3; k++) { + const xi = round(k * 48 + 12 - cam * 0.2); + for (let y = 1; y < 13; y++) + for (let x = xi - 5; x <= xi + 5; x++) + if (y < 7) put(x, y, y > 1 && y < 6 && (x - xi) % 2 === 0 && abs(x - xi) < 5 ? [255, 244, 222] : [40, 42, 52]); + else if (x === xi) put(x, y, [34, 36, 46]); + } + const wd = S.wind, e = cl(abs(wd) * 1.4 + 0.25, 0, 1) * (wd < 0 ? -1 : 1), a = abs(e); + for (let fx = 20 - md(s0, 32) - 32; fx < W + 6; fx += 32) + for (let i = 0; i < 22; i++) + if (i < 7) put(fx, 6 + i, [110, 110, 118]); + else put(round(fx + e * (i % 5 + 1)), 6 + floor((i - 7) / 5) + round((i % 5 + 1) * ((1 - a) * 0.9 + sin(t * (5 + 7 * a) - i) * 0.12 * a)), i > 11 && i < 17 ? [235, 235, 235] : sc3(A, 230)); + // The photographers in the infield, and camera flashes going off. + for (let k = 0; k < 2; k++) for (let r = 0; r < 49; r++) if (SNAP[r] !== ".") put(round(k * 36 - 16 - cam * 1.15) + (r % 7), 113 + k * 5 + floor(r / 7), SNAP[r] === "#" && r > 6 ? [12, 14, 20] : [48, 48, 56]); + for (const f of S.flashes) { + const a = 1 - (t - f.t0) / 0.2; + for (let i = 0; i < 25 && a <= 1; i++) { + const dx = (i % 5) - 2, dy = floor(i / 5) - 2, x = f.x + dx, d = abs(dx) + abs(dy), j = ((f.y + dy) * W + x) * 3; + const v = (d ? (d > 1 ? (f.y > 100 && dx * dy === 0 ? 120 : 0) : 150) : 255) * a; + if (x >= 0 && x < W && v > 0) for (let c = 0; c < 3; c++) C[j + c] = max(C[j + c], v); + } + } +} + +// The mat from a little above: the top, the end facing the runway and the side +// facing us. Then the box and its stop board. +function mat(S) { + const cam = S.cam, top = GY - MH, xe = round(-cam); + for (let y = 89; y < 108; y++) + for (let x = 0; x < W; x++) { + const py = y + 0.5, X0 = x + 0.5 + cam, z = (py - top) / OBY, X = X0 - OBX * z, Xf = X0 - OBX * MZ, zl = (X0 - MAT0) / OBX, v = 0.86 + 0.012 * z; + if (abs(z) <= MZ && X >= MAT0) put(x, y, md(X, 16) < 1 || z > MZ - 1 ? [110, 150, 250] : [44 * v, 98 * v, 232 * v]); + else if (Xf >= MAT0 && py >= top + OBY * MZ && py <= GY + OBY * MZ) put(x, y, abs(py - GY + 0.6) < 0.6 && md(Xf, 9) > 3 ? [215, 220, 235] : md(Xf, 18) < 1 ? [18, 40, 120] : [30, 66, 176]); + else if (abs(zl) <= MZ && py >= top + OBY * zl && py <= GY + OBY * zl) put(x, y, [22, 50, 140]); + } + for (let x = xe - 7; x <= xe; x++) for (let y = GY - 1; y <= GY + 1; y++) put(x, y, x === xe ? [200, 200, 212] : y < GY ? [90, 90, 100] : [10, 10, 14]); +} + +function standard(S, z) { + const x = round(XS + OBX * z - S.cam), yb = round(GY + OBY * z), yt = round(S.by + OBY * z - 5), near = z > 0; + for (let y = yt; y <= yb; y++) for (let w = 0; w < 1 + near; w++) put(x + w, y, sc3([1, 1.02, 1.12], (near ? (w ? 120 : 175) : 100) * (y === yt ? 1.25 : 1))); + for (let w = -2; w <= 2; w++) put(x + w, yb, [60, 60, 66]); + put(x + (near ? -1 : 1), round(S.by + OBY * z), [220, 220, 225]); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const cam = S.cam, P = S.pole, b = S.bar; + C = S.C; + colours(S); + back(S); + standard(S, -ZS); + mat(S); + + // The pole: carried, bent into an arc in the box, then let go. + if (S.ph === RUN) seg(P.x - cam, P.y, P.x1 - cam, P.y1); + else if (S.ph === POLE) { + const A = arc(P.px, P.py, P.c, P.L), n = ceil(P.L * 2), a = A[3] + A[4], s = A[4] > 0 ? 2.5 : -2.5, ex = A[0] + A[2] * cos(a) - cam, ey = A[1] + A[2] * sin(a); + for (let i = 0; i <= n; i++) splat(A[0] + A[2] * cos(A[3] + (A[4] * i) / n) - cam, A[1] + A[2] * sin(A[3] + (A[4] * i) / n)); + seg(ex, ey, ex - s * sin(a), ey + s * cos(a)); + } else if (S.free) seg(-cam, BOXY, -cam - S.free.L * sin(S.free.a), BOXY - S.free.L * cos(S.free.a)); + + // The bar, striped, on its pegs or on its way down. + let [y0, y1] = [S.by - OBY * ZS, S.by + OBY * ZS]; + const w = b.st === 1 || b.st === 2 ? sin(b.t * 48) * exp(-b.t * (b.st === 2 ? 5 : 2)) * 1.3 : 0; + if (b.st >= 3) [y0, y1] = b.y; + for (let i = 0; i <= 36 && b.st !== 4; i++) put(floor(lerp(XS - OBX * ZS, XS + OBX * ZS, i / 36) - cam), floor(lerp(y0 + w, y1 + w * 0.6, i / 36)), floor(i / 5.2) % 2 ? [240, 240, 236] : sc3(S.acc, 120, 60)); + + // The vaulter, then the near upright in front. + const f = S.fig; + if (f) figure(S, f.x, f.y, f.th, f.fc, f.arms, f.L, f.hands, S.cols, 1); + standard(S, ZS); + + // The judge by the box, a flag up once the bar has settled. + const up = S.ph === MAT && S.verdict && S.pt < 2.6, jc = [[34, 50, 120], [70, 70, 82], [225, 165, 125], [40, 40, 44]]; + figure(S, -13.2, 101.6, 0, 1, [up ? 0.2 : 2.9, 2.95], [0, 0, 0, 0], null, jc.concat(jc), 0.85); + for (let r = 0; r < 9 && up; r++) put(round(-12.3 - cam) + (r % 3), 91 + floor(r / 3), S.verdict === 1 ? [245, 245, 245] : [235, 25, 25]); + + // Glare from the lamp banks, over everything. + for (let k = -3; k <= 3; k++) { + const xi = round(k * 48 + 12 - cam * 0.2); + for (let y = 0; y < 30; y++) + for (let x = max(0, xi - 60), j = (y * W + x) * 3; x < min(W, xi + 60); x++, j += 3) { + const v = S.glare[y * 128 + x - xi + 64]; + C[j] += v; + C[j + 1] += v * 0.96; + C[j + 2] += v * 0.88; + } + } + + // The results board: height, marks at it, the vaulter's colours. + const hc = S.verdict === 1 ? [255, 255, 255] : [255, 40, 40]; + for (let y = 1; y < 13; y++) for (let x = 39; x < 63; x++) for (let c = 0, j = (y * W + x) * 3; c < 3; c++) C[j + c] = y === 1 ? S.acc[c] * 230 : C[j + c] * 0.2 + [6, 10, 26][c] * 0.8 + hc[c] * S.hudF * 0.75; + text((S.base + 0.1 * S.notch).toFixed(2) + "M", 41, 3, [255, 180, 40]); + S.marks.slice(-3).forEach((m, i) => text(m, 41 + i * 4, 8, m === "X" ? [255, 50, 40] : [240, 240, 240])); + for (let r = 0; r < 30; r++) put(55 + (r % 6), 8 + floor(r / 6), r < 12 ? S.cols[0] : S.cols[1]); + + for (let i = 0; i < N; i++) display.setPixel(i % W, (i / W) | 0, min(255, C[i * 3]), min(255, C[i * 3 + 1]), min(255, C[i * 3 + 2])); +} From 6a1f7cdc43831aab5e40f17ef414b0ee7a0dee96 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:56:08 +0000 Subject: [PATCH 24/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20README=20=EC=97=90=20Pole=20Vault=20=EB=A5=BC?= =?UTF-8?q?=20=EC=93=B4=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_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index ce046993e..89f9d39ab 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -290,7 +290,7 @@ | [Lichtenberg](patterns/lichtenberg.js) | 프랙탈 | 소다물을 바른 단풍나무 판의 위아래 전극에서 깃털 잔가지를 단 프랙탈 나무가 파란 플라스마와 주황 불씨를 달고 서로를 향해 타 들어가고, 만나면 아크가 번쩍여요. K1 이 판의 나무를 바꾸고, 누르면 다음 판이에요. | | [Noborigama](patterns/noborigama.js) | 도자기 | 밤 언덕의 오름가마를 세로로 잘라 봐요. 불이 그릇 쌓인 칸들을 타고 올라 아래 칸은 희고 위 칸은 붉으며, 장작을 넣을 때마다 감시 구멍에서 불길이, 굴뚝에서 검은 연기와 불티가 나와요. K1 은 소성 날짜를 바꾸고, 누르면 모든 구멍에 한꺼번에 불을 넣어요. | <> -<> +| [Pole Vault](patterns/pole-vault.js) | 운동·경기 | 밤 경기장에서 카메라가 장대를 든 선수를 따라 달리고, 활처럼 휜 장대를 타고 거꾸로 오른 선수가 패널 꼭대기 가까운 가로대를 넘어 매트에 떨어지면 심판이 흰 깃발이나 빨간 깃발을 들어요. K3 이 가로대를 5.00 m 에서 6.20 m 까지 올리고, 누르면 다음 선수가 바로 달려요. | | [Rice Planting](patterns/rice-planting.js) | 농사·밭 | 물 댄 논을 위에서 내려다봐요. 밀짚모자를 쓴 일꾼들이 못줄을 따라 뒷걸음질하며 모를 한 포기씩 꽂고, 앞쪽 거울 물에는 구름과 모춤, 백로가 있어요. K1 은 아침 안개에서 노을까지 돌리고, 누르면 남은 모를 한꺼번에 꽂고 줄을 넘겨요. | | [Shadow Play](patterns/shadow-play.js) | 무대·공연 | 등잔 불빛을 받은 흰 막에 와양 쿨릿 가죽 인형의 레이스 그림자가 비쳐요. 기사가 거인과 싸워 던져 버리면 구눙안이 떨며 쓸고 들어와 다음 장을 열고, K1 을 돌리면 막 뒤로 돌아가 금빛과 빨강, 남색 칠이 보여요. | @@ -1120,7 +1120,9 @@ K4 Spate 0 이면 물이 실 몇 가닥으로 갈라져요. 처음 값 0.06 이 주제 4 로 화산 갈래의 둘째를 세웠어요. Eruption 에 용암과 재가 있으니, 이쪽은 스트로퀴르 같은 간헐천을 땅속까지 세로로 잘랐어요. 71번 줄이 웅덩이 가장자리이고, 80번 줄에서 너비 4 LED 의 화도가 116번 줄 언저리의 물방까지 내려가며, 둘레 지층에 흰 규화 테와 유황빛, 녹빛 물듦이 둘러요. 처음 노브에서는 1.95~2.4초에 청록 유리 돔이 부풀고, 2.4초에 터진 물기둥이 3.3초쯤 6번 줄에 닿고, 3.5초부터 웅덩이가 103번 줄까지 빠져요. Speed 2 에서 한 바퀴는 13.25초예요. 하늘과 땅을 검게 지운 사본에서도 아래 3분의 1이 13.6% 켜지고 14.2~14.7% 바뀌어서, 화도와 물방만으로 문턱을 넘어요. 첫 판은 K3 Power 가 0.0069 로 걸렸어요. 2~3초 창에 돔만 들어서였고, Power 가 큰 기포와 돔을 줄여 더 일찍 터지게 하자 0.048 이 됐어요. 김은 처음에 좁은 흰 기둥이었어요. 덩어리 가장자리를 일차로 바꾸고 왼쪽 위 빛의 기울기로 음영을 주자 뭉게구름이 됐고, 정오 하늘을 (30,84,170) 에서 (20,62,152) 로 짙게 하자 모양 바뀜이 0.036 에서 0.044~0.050 이 됐어요. K4 Wind 는 0.02 였어요. 앞 분출이 남긴 구름을 두껍게 하고 바탕 바람을 초당 3 에서 6 LED 로 올리자 0.048 이 됐어요. Speed 3 에서는 물기둥이 9줄 간격의 원반으로 줄무늬졌어요. 한 프레임의 물 덩어리를 같은 줄에서 띄웠기 때문이라, 출발 자리를 프레임 안에서 흩어 고쳤어요. 노브 넷은 0.27, 0.07, 0.05, 0.05 예요. -<> +### Pole Vault + +주제 4 로 운동·경기 갈래의 넷째를 세웠어요. 여러 판 「다음 판 감」에 남아 있던 장대높이뛰기예요. 밤 경기장을 옆에서 보고, 카메라가 장대를 든 선수를 따라 달리다가 장대가 박스에 꽂혀 활처럼 휘었다 펴지는 동안 가로대 쪽으로 따라가 멈춰요. 가로대는 1 m 에 14줄이라 5.00 m 가 34번 줄, 6.20 m 가 17번 줄이에요. 거꾸로 올라 반 바퀴 돈 선수의 엉덩이가 가로대 3.5줄 위에서 정점을 찍어요. 선수는 13줄 남짓인데 장대는 5.00 m 에서 58.5 LED 라서 몸의 네다섯 배예요. 실제로는 2.8 배쯤이지만 그래야 위 3분의 1의 가로대에 닿아요. 처음 장대는 가장 휠 때 현이 길이의 0.66 이라 반원 고리로 보였고, 0.75 로 줄이고 현이 더 빨리 서게 하자 활처럼 읽혔어요. 첫 도움닫기를 1.2초에서 1.95초로 늦춰서 출발 1.97초, 도약 3.58초, 정점 5.14초, 착지 6.03초가 다 2~8초 창에 들어요. 씨앗 여덟 가운데 일곱이 가로대를 넘어요. 경기장을 지우고 선수와 장대, 가로대, 지주, 매트, 활주로만 재면 켜진 LED 는 위 3분의 1이 1.4~1.7%, 가운데가 4.9~5.0% 뿐이에요. 바뀐 LED 로는 22.8~23.2/92.8/49.4% 예요. 가는 장대가 휘며 쓸고 지나가서 가운데 3분의 1을 거의 다 한 번씩 바꿔요. K1 은 처음에 0.02 였고, 관중석을 팀 색 조명으로 물들이자 0.05 가 됐어요. 첫 판은 33.8 KB 였어요. 빛기둥 속 안개와 휴대폰 불빛, 잔상, 매트 눌림을 빼서 24,980 바이트로 줄였고, 화소마다 배열을 만들던 배경을 바로 쓰게 하자 한 프레임이 1.26 ms 에서 0.62 ms 가 됐어요. 노브 넷은 0.05, 0.09, 0.06, 0.04 예요. ### Lichtenberg From 0b99a550674671d282de6508ad3a648b191d41ff Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:56:12 +0000 Subject: [PATCH 25/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Origami?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- incoming/pattern-study/patterns/origami.js | 368 ++++++++------------- 1 file changed, 146 insertions(+), 222 deletions(-) diff --git a/incoming/pattern-study/patterns/origami.js b/incoming/pattern-study/patterns/origami.js index 513a17ab0..016f468e8 100644 --- a/incoming/pattern-study/patterns/origami.js +++ b/incoming/pattern-study/patterns/origami.js @@ -11,8 +11,8 @@ // @matrix 64x128 // @knobs Paper=0..1, Speed=0.2..3, Sheets=1..3, Lamp=-1..1 // -// Knob 1: Paper. Which sheet comes off the pack: teal kami at 0, then indigo chiyogami printed with seigaiha waves, gold foil, pink with white kanoko dots, yellow, green ichimatsu checks, silver foil and vermilion at 1. Each new sheet is the next one in the pack. -// Knob 2: Speed. How fast the hands fold: at 2 a fold takes about a second and a sheet goes from flat to crane and back in twenty-three seconds; at 0.2 it runs ten times slower. +// Knob 1: Paper. Which sheet comes off the pack: teal kami at 0, then indigo chiyogami printed with seigaiha waves, gold foil, pink with white kanoko dots, yellow, green ichimatsu checks, silver foil and vermilion at 1. Each new sheet is the next one in the pack, and the pack in the corner shows which. +// Knob 2: Speed. How fast the hands fold: at 2 a fold takes about a second and a sheet goes from flat to crane and back to flat in twenty-two seconds; at 0.2 it all runs ten times slower. // Knob 3: Sheets. How many sheets fold at once, one above the other, each a step behind the last: one big sheet at 1, three small ones at 3. // Knob 4: Lamp. Where the lamp stands, from the left of the table at -1 to the right at 1. The flaps that turn toward it light up, the far side of the sheet falls into shade and a lifted flap throws its shadow the other way. // Buttons: K1 tap opens the crane flat now, or lays the next sheet if it is already open. @@ -35,6 +35,8 @@ const mul = (M, A) => [ M[2] * A[0] + M[3] * A[2], M[2] * A[1] + M[3] * A[3], M[0] * A[4] + M[1] * A[5] + M[4], M[2] * A[4] + M[3] * A[5] + M[5], ]; +const tx = (A, c, j) => A[0] * c[j] + A[1] * c[j + 1] + A[4]; +const ty = (A, c, j) => A[2] * c[j] + A[3] * c[j + 1] + A[5]; // A flap turned by theta about the hinge through (hx, hy) with unit normal // (nx, ny) toward the flap, as seen from straight above: k = cos(theta) - 1. // At k = -2 the flap lies flat on the other side, mirrored. @@ -75,8 +77,8 @@ const CRANE = [ [{ l: [0, 1, -Q, K], s: [-K, K], m: -1, sel: legL }, { l: [0, 1, Q, K], s: [K, K], m: -1, sel: legR }], // The legs reverse-folded up into neck and tail. [{ l: [0, YP, NL[0], YP + NL[1]], s: [0, 1], m: -1, sel: legL }, { l: [0, YP, -NL[0], YP + NL[1]], s: [0, 1], m: -1, sel: legR }], - // The head. - [{ head: 1, m: -1, sel: (f, x) => f.H[8] && x < 0 }], + // The tip of the neck reverse-folded down into the head. + [{ l: [-0.515, -0.013, -0.374, 0.004], s: [-0.506, -0.084], m: -1, sel: (f, x) => f.H[8] && x < 0 }], ]; // Seconds per step at Speed 2, and how the sheet lies on the table at each // state: turned 45 degrees it is a square on the panel. @@ -85,34 +87,24 @@ const PHI = [PI / 4, PI / 4, 0, 0, 0, 0, 0, 0, 0, 0]; // The finished crane flaps its front wing about the line it was lifted on. const FLAPH = [0, K, 0, -1, 1]; -function reflectPt(p, h) { - const d = (p[0] - h[0]) * h[2] + (p[1] - h[1]) * h[3]; - return [p[0] - 2 * d * h[2], p[1] - 2 * d * h[3]]; -} - +// A crease as a point on it and its unit normal toward the side that moves. function lineOf(mv) { const [x1, y1, x2, y2] = mv.l; - let nx = y1 - y2; - let ny = x2 - x1; - if ((mv.s[0] - x1) * nx + (mv.s[1] - y1) * ny < 0) (nx = -nx), (ny = -ny); - const l = hypot(nx, ny); - return [x1, y1, nx / l, ny / l]; + const l = hypot(x2 - x1, y2 - y1) * ((mv.s[0] - x1) * (y1 - y2) + (mv.s[1] - y1) * (x2 - x1) < 0 ? -1 : 1); + return [x1, y1, (y1 - y2) / l, (x2 - x1) / l]; } // Cut a convex polygon by the sign of d, keeping the side s. function clip(cp, d, s) { const out = []; - const m = d.length; - for (let i = 0; i < m; i++) { - const j = (i + 1) % m; - const a = d[i] * s; + d.forEach((v, i) => { + const j = (i + 1) % d.length; + const a = v * s; const b = d[j] * s; + const t = a / (a - b); if (a >= 0) out.push(cp[2 * i], cp[2 * i + 1]); - if ((a > 0 && b < 0) || (a < 0 && b > 0)) { - const t = a / (a - b); - out.push(cp[2 * i] + t * (cp[2 * j] - cp[2 * i]), cp[2 * i + 1] + t * (cp[2 * j + 1] - cp[2 * i + 1])); - } - } + if (a * b < 0) out.push(cp[2 * i] + t * (cp[2 * j] - cp[2 * i]), cp[2 * i + 1] + t * (cp[2 * j + 1] - cp[2 * i + 1])); + }); return out; } @@ -123,31 +115,12 @@ function build(steps) { const C = [[1, 0], [0.5, 0.5], [0, 1], [-0.5, 0.5], [-1, 0], [-0.5, -0.5], [0, -1], [0.5, -0.5]]; const F = C.map((c, s) => ({ cp: [0, 0, c[0], c[1], ...C[(s + 1) % 8]], sec: s, T: [[1, 0, 0, 1, 0, 0]], L: [0], H: [null] })); const n = steps.length; - const lines = []; for (let k = 1; k <= n; k++) { - let top = -1e9; - let bot = 1e9; - for (const f of F) { - f.T[k] = f.T[k - 1]; - f.L[k] = f.L[k - 1]; - f.H[k] = null; - top = max(top, f.L[k - 1]); - bot = min(bot, f.L[k - 1]); - } + const top = max(...F.map((f) => f.L[k - 1])); + const bot = min(...F.map((f) => f.L[k - 1])); + for (const f of F) (f.T[k] = f.T[k - 1]), (f.L[k] = f.L[k - 1]), (f.H[k] = null); for (const mv of steps[k - 1]) { - let h; - if (mv.head) { - // The tip of the neck, bent down and forward. - const tip = reflectPt([0, 1], lines[k - 2][0]); - const r = [-0.766, -0.643]; - const J = [tip[0] - 0.13 * r[0], tip[1] - 0.13 * r[1]]; - const to = [J[0] - 0.13 * 0.9, J[1] + 0.13 * 0.44]; - const nx = tip[0] - to[0]; - const ny = tip[1] - to[1]; - const l = hypot(nx, ny); - h = [(tip[0] + to[0]) / 2, (tip[1] + to[1]) / 2, nx / l, ny / l]; - } else h = lineOf(mv); - (lines[k - 1] = lines[k - 1] || []).push(h); + const h = lineOf(mv); const [px, py, nx, ny] = h; const moved = []; for (const f of F.slice()) { @@ -158,11 +131,10 @@ function build(steps) { let cx = 0; let cy = 0; const d = []; - for (let j = 0; j < m; j++) { - const x = A[0] * cp[2 * j] + A[1] * cp[2 * j + 1] + A[4]; - const y = A[2] * cp[2 * j] + A[3] * cp[2 * j + 1] + A[5]; - cx += x / m; - cy += y / m; + for (let j = 0; j < 2 * m; j += 2) { + const x = tx(A, cp, j); + const y = ty(A, cp, j); + (cx += x / m), (cy += y / m); const dd = (x - px) * nx + (y - py) * ny; d.push(abs(dd) < 1e-9 ? 0 : dd); } @@ -188,40 +160,35 @@ function build(steps) { // Facets that lie together at a state share a group there: a crease shows // between two facets that lie together now but were parted at the deepest // fold. + // And the extent of each state, turned as it lies, for the camera. const nf = F.length; const gid = new Int32Array(nf * (n + 1)); const det = new Int8Array(nf * (n + 1)); + const box = []; for (let k = 0; k <= n; k++) { const seen = new Map(); + const c = cos(PHI[k]); + const s = sin(PHI[k]); + const b = [1e9, -1e9, 1e9, -1e9]; F.forEach((f, i) => { - const key = f.T[k].map((v) => round(v * 1e4)).join(); + const A = f.T[k]; + const key = A.map((v) => round(v * 1e4)).join(); if (!seen.has(key)) seen.set(key, seen.size); gid[i * (n + 1) + k] = seen.get(key); - const A = f.T[k]; det[i * (n + 1) + k] = A[0] * A[3] - A[1] * A[2] > 0 ? 1 : -1; - }); - } - // The extent of each state, turned as it lies, for the camera. - const box = []; - for (let k = 0; k <= n; k++) { - const c = cos(PHI[k]); - const s = sin(PHI[k]); - let x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9; - for (const f of F) { - const A = f.T[k]; for (let j = 0; j < f.cp.length; j += 2) { - const x = A[0] * f.cp[j] + A[1] * f.cp[j + 1] + A[4]; - const y = A[2] * f.cp[j] + A[3] * f.cp[j + 1] + A[5]; + const x = tx(A, f.cp, j); + const y = ty(A, f.cp, j); const u = x * c - y * s; const v = x * s + y * c; - (x0 = min(x0, u)), (x1 = max(x1, u)), (y0 = min(y0, v)), (y1 = max(y1, v)); + (b[0] = min(b[0], u)), (b[1] = max(b[1], u)), (b[2] = min(b[2], v)), (b[3] = max(b[3], v)); } - } - const u = (x0 + x1) / 2; - const v = (y0 + y1) / 2; - box.push([u * c + v * s, -u * s + v * c, x1 - x0, y1 - y0]); + }); + const u = (b[0] + b[1]) / 2; + const v = (b[2] + b[3]) / 2; + box.push([u * c + v * s, -u * s + v * c, b[1] - b[0], b[3] - b[2]]); } - const tilt = new Float32Array(nf * 2); + const tilt = F32(nf * 2); for (let i = 0; i < nf * 2; i++) tilt[i] = Math.random() - 0.5; return { F, n, gid, det, box, tilt }; } @@ -231,7 +198,7 @@ function build(steps) { const PAPERS = [ [[0.16, 0.72, 0.8], null, 0], [[0.13, 0.2, 0.58], [0.78, 0.86, 0.96], 1], - [[0.96, 0.7, 0.26], null, 2], + [[1, 0.78, 0.3], null, 2], [[0.96, 0.46, 0.6], [1, 0.95, 0.96], 3], [[0.98, 0.78, 0.12], null, 0], [[0.12, 0.46, 0.26], [0.94, 0.88, 0.7], 4], @@ -269,23 +236,13 @@ function print(kind, s, t) { // The order a sheet goes through. const WAIT = 0, IN = 1, REST = 2, FOLD = 3, HOLD = 4, SIT = 5, UNFOLD = 6, CREASE = 7; -function newSheet(sl, first) { - sl.old = first ? null : { seq: sl.seq, kmax: sl.kmax }; - sl.seq++; - sl.ph = IN; - sl.pt = 0; - sl.k = 0; - sl.u = 1; - sl.kmax = 0; - sl.rush = false; -} - -function slot(i) { - return { i, ph: WAIT, pt: -2.4 * i, k: 0, u: 1, kmax: 0, seq: -1, old: null, rush: false }; -} +const newSheet = (sl, first) => + Object.assign(sl, { old: first ? null : { seq: sl.seq, kmax: sl.kmax }, seq: sl.seq + 1, ph: IN, pt: 0, k: 0, u: 1, kmax: 0, rush: false }); +const slot = (i) => ({ i, ph: WAIT, pt: -2.4 * i, k: 0, u: 1, kmax: 0, seq: -1, old: null, rush: false }); +const F32 = (n) => new Float32Array(n); export function setup(params) { - const grain = new Float32Array(N); + const grain = F32(N); for (let y = 0; y < H; y++) { let w = Math.random(); for (let x = 0; x < W; x++) { @@ -293,62 +250,28 @@ export function setup(params) { grain[y * W + x] = 0.75 + 0.2 * sin(y * 0.9 + 3 * sin(x * 0.05 + y * 0.02)) + 0.1 * w; } } - params.s = { - M: build(CRANE), - slots: [slot(0)], - n: 1, - base: 0, - rate: 1, - lamp: 0.06, - lampKey: null, - grain, - bg: new Float32Array(N * 3), - LX: new Float32Array(N), - LY: new Float32Array(N), - LZ: new Float32Array(N), - LI: new Float32Array(N), - HZ: new Float32Array(N), - lp: [0, 0, 1], - }; + params.s = { M: build(CRANE), slots: [slot(0)], n: 1, base: 0, rate: 1, lamp: 0.06, lampKey: null, grain, bg: F32(N * 3) }; + for (const k of ["LX", "LY", "LZ", "LI", "HZ"]) params.s[k] = F32(N); } function stepSlot(sl, h, M) { if (sl.rush && (sl.ph === UNFOLD || sl.ph === CREASE)) h *= 4; sl.pt += h; - switch (sl.ph) { - case WAIT: - if (sl.pt >= 0) newSheet(sl, true); - break; - case IN: - if (sl.pt >= 1.2) (sl.ph = REST), (sl.pt = 0), (sl.old = null); - break; - case REST: - if (sl.pt >= 1) (sl.ph = FOLD), (sl.k = 1), (sl.u = 0); - break; - case FOLD: - sl.u = min(1, sl.u + h / DUR[sl.k - 1]); - if (sl.u >= 1) (sl.kmax = max(sl.kmax, sl.k)), (sl.ph = HOLD), (sl.pt = 0); - break; - case HOLD: - if (sl.pt >= 0.25) { - if (sl.k < M.n) (sl.k++), (sl.u = 0), (sl.ph = FOLD); - else (sl.ph = SIT), (sl.pt = 0); - } - break; - case SIT: - if (sl.pt >= 3) sl.ph = UNFOLD; - break; - case UNFOLD: - sl.u -= h / 0.45; - if (sl.u <= 0) { - if (sl.k > 1) (sl.k--), (sl.u += 1); - else (sl.k = 0), (sl.u = 1), (sl.ph = CREASE), (sl.pt = 0); - } - break; - case CREASE: - if (sl.pt >= 2.5 || sl.rush) newSheet(sl, false); - break; - } + const ph = sl.ph; + if (ph === WAIT && sl.pt >= 0) newSheet(sl, true); + else if (ph === IN && sl.pt >= 1.2) Object.assign(sl, { ph: REST, pt: 0, old: null }); + else if (ph === REST && sl.pt >= 1) Object.assign(sl, { ph: FOLD, k: 1, u: 0 }); + else if (ph === FOLD) { + sl.u = min(1, sl.u + h / DUR[sl.k - 1]); + if (sl.u >= 1) Object.assign(sl, { kmax: max(sl.kmax, sl.k), ph: HOLD, pt: 0 }); + } else if (ph === HOLD && sl.pt >= 0.25) { + if (sl.k < M.n) Object.assign(sl, { k: sl.k + 1, u: 0, ph: FOLD }); + else Object.assign(sl, { ph: SIT, pt: 0 }); + } else if (ph === SIT && sl.pt >= 3) sl.ph = UNFOLD; + else if (ph === UNFOLD && (sl.u -= h / 0.45) <= 0) { + if (sl.k > 1) (sl.k--), (sl.u += 1); + else Object.assign(sl, { k: 0, u: 1, ph: CREASE, pt: 0 }); + } else if (ph === CREASE && (sl.pt >= 2.5 || sl.rush)) newSheet(sl, false); } export function update(dt, input, params) { @@ -381,10 +304,13 @@ export function update(dt, input, params) { // The lamp: a bulb LH above the table at (lx, LY0). Each LED keeps the unit // vector to it, the half vector's height for the shine on foil, and how much -// light reaches it. The table is dark walnut, kept below 24 of 255. +// light reaches it. The table is dark walnut, kept below 24 of 255; on it lie +// the pack of paper, the next sheet on top, and a bone folder. const LH = 80; const LY0 = 34; -function lampField(S) { +const PC = cos(-0.2); +const PS = sin(-0.2); +function lampField(S, next) { const lx = 31.5 + 44 * S.lamp; const { LX, LY, LZ, LI, HZ, bg, grain } = S; for (let y = 0; y < H; y++) @@ -399,33 +325,50 @@ function lampField(S) { LZ[p] = LH / l; LI[p] = (LH * LH) / (LH * LH + 0.4 * r2); HZ[p] = (LZ[p] + 1) / sqrt(LX[p] * LX[p] + LY[p] * LY[p] + (LZ[p] + 1) * (LZ[p] + 1)); + const lit = LI[p] * (0.22 + 0.9 * LZ[p]); const w = min(21, 255 * 0.085 * (0.25 + 0.75 * LI[p]) * grain[p]); - bg[p * 3] = w; - bg[p * 3 + 1] = w * 0.56; - bg[p * 3 + 2] = w * 0.32; + let c = [w, w * 0.56, w * 0.32]; + // The pack: four sheets a little askew, each showing an edge of its colour. + for (let j = 0; j < 4; j++) { + const X = x + 0.5 + 1 - 0.9 * j; + const Y = y + 0.5 - 1 - 1.3 * j; + const u = (X * PC + Y * PS) / 13; + const v = (Y * PC - X * PS) / 13; + if (abs(u) < 1 && abs(v) < 1) { + const P = PAPERS[(next + j) % PAPERS.length]; + const col = P[2] !== 2 && print(P[2], u, v) ? P[1] : P[0]; + const e = (abs(u) > 0.92 || abs(v) > 0.92 ? 0.7 : 1) * (j ? 0.8 : 1) * (P[2] === 2 ? 0.75 : 1); + c = col.map((v) => 255 * v * lit * e); + break; + } + } + // The bone folder, rounded at both ends, its back catching the lamp. + const ax = x + 0.5 - 47; + const ay = y + 0.5 - 118; + const t = cl((ax * 14 - ay * 15) / 421, 0, 1); + const d = hypot(ax - 14 * t, ay + 15 * t); + if (d < 1.7) c = [0.8, 0.74, 0.62].map((v) => 255 * v * lit * (0.75 + 0.35 * (1 - d / 1.7))); + bg[p * 3] = c[0]; + bg[p * 3 + 1] = c[1]; + bg[p * 3 + 2] = c[2]; } S.lp = [lx, LY0, LH]; } // Per frame: every facet on the table, as drawn. +// RA maps it from the crease pattern to the panel, RN is its lit normal, RK +// its draw order; RS the side up, RP the paper, RQ the sheet; RG and RX its +// group now and at the deepest fold, RU and RL its side and layer there. const MAXR = 1600; -const RA = new Float32Array(MAXR * 6); -const RN = new Float32Array(MAXR * 3); -const RK = new Float32Array(MAXR); -const RS = new Int8Array(MAXR); -const RP = new Int16Array(MAXR); -const RG = new Int32Array(MAXR); -const RX = new Int32Array(MAXR); -const RU = new Int8Array(MAXR); -const RL = new Float32Array(MAXR); -const RQ = new Int16Array(MAXR); -const RM = new Float32Array(MAXR); +const [RA, RN, RK, RL, RM] = [6, 3, 1, 1, 1].map((k) => F32(MAXR * k)); +const [RS, RU] = [new Int8Array(MAXR), new Int8Array(MAXR)]; +const [RP, RQ, RG, RX] = [0, 0, 0, 0].map(() => new Int32Array(MAXR)); const ORD = []; const RCP = []; const ID = new Int16Array(N); -const SHK = new Float32Array(N); -const VX = new Float32Array(64); -const VY = new Float32Array(64); +const SHK = F32(N); +const VX = F32(64); +const VY = F32(64); let nr = 0; // Fill a convex polygon with v; mode 0 writes facet ids, mode 1 raises the @@ -465,7 +408,7 @@ function viewOf(M, j, fw, fh, zmax) { // Put one sheet on the table: model M at step k, progress u, in slot // region (cy, fw, fh), paper index pi. dx and dphi slide a new sheet in. -function placeSheet(S, M, k, u, flap, kmax, pi, cyp, fw, fh, zmax, dx, dphi, keyBase, mem, qid) { +function placeSheet(S, M, k, u, flap, kmax, pi, cyp, fw, fh, zmax, dx, dy, dphi, keyBase, mem, qid) { const n1 = M.n + 1; const go = k > 0 && u > 0 && u < 1; const jb = k === 0 ? 0 : u >= 1 ? k : k - 1; @@ -478,7 +421,7 @@ function placeSheet(S, M, k, u, flap, kmax, pi, cyp, fw, fh, zmax, dx, dphi, key const ccy = mix(a[3], b[3], w); const c = cos(phi) * z; const s = sin(phi) * z; - const V = [c, -s, s, c, 31.5 + dx - (c * ccx - s * ccy), cyp - (s * ccx + c * ccy)]; + const V = [c, -s, s, c, 31.5 + dx - (c * ccx - s * ccy), cyp + dy - (s * ccx + c * ccy)]; const [lx, ly, lh] = S.lp; M.F.forEach((f, i) => { if (nr >= MAXR) return; @@ -515,20 +458,20 @@ function placeSheet(S, M, k, u, flap, kmax, pi, cyp, fw, fh, zmax, dx, dphi, key const cp = f.cp; const cnt = cp.length / 2; for (let j = 0; j < cnt; j++) { - const x0 = A0[0] * cp[2 * j] + A0[1] * cp[2 * j + 1] + A0[4]; - const y0 = A0[2] * cp[2 * j] + A0[3] * cp[2 * j + 1] + A0[5]; - const hz = ((x0 - hg[0]) * hg[2] + (y0 - hg[1]) * hg[3]) * st * z; - const px = A[0] * cp[2 * j] + A[1] * cp[2 * j + 1] + A[4]; - const py = A[2] * cp[2 * j] + A[3] * cp[2 * j + 1] + A[5]; - VX[j] = px + (hz * (px - lx)) / lh; - VY[j] = py + (hz * (py - ly)) / lh; + const hz = (((tx(A0, cp, 2 * j) - hg[0]) * hg[2] + (ty(A0, cp, 2 * j) - hg[1]) * hg[3]) * st * z) / lh; + const px = tx(A, cp, 2 * j); + const py = ty(A, cp, 2 * j); + VX[j] = px + hz * (px - lx); + VY[j] = py + hz * (py - ly); } fill(cnt, keyBase + key, 1); } } - const tl = 0.05 + 0.1 * mem; - nx += tl * M.tilt[2 * i]; - ny += tl * M.tilt[2 * i + 1]; + // Paper never lies quite flat: each layer tips a little its own way, and a + // sheet opened out again keeps a tilt in every facet between its creases. + const g = M.gid[i * n1 + jb]; + nx += 0.06 * M.tilt[2 * g] + 0.12 * mem * M.tilt[2 * i + 1]; + ny += 0.06 * M.tilt[2 * g + 1] + 0.12 * mem * M.tilt[2 * i]; const nl = hypot(nx, ny, nz); RN[r * 3] = nx / nl; RN[r * 3 + 1] = ny / nl; @@ -536,7 +479,7 @@ function placeSheet(S, M, k, u, flap, kmax, pi, cyp, fw, fh, zmax, dx, dphi, key RS[r] = side; RK[r] = keyBase + key; RP[r] = pi; - RG[r] = M.gid[i * n1 + jb]; + RG[r] = g; RX[r] = M.gid[i * n1 + kmax]; RU[r] = M.det[i * n1 + kmax]; RL[r] = f.L[kmax]; @@ -544,13 +487,14 @@ function placeSheet(S, M, k, u, flap, kmax, pi, cyp, fw, fh, zmax, dx, dphi, key RM[r] = mem; ORD.push(r); }); - return V; } export function draw(display, params) { const S = params.s; if (!S) return; - if (S.lampKey !== S.lamp) (S.lampKey = S.lamp), lampField(S); + const next = (S.base + S.slots[0].seq + 1) % PAPERS.length; + const lk = S.lamp + 10 * next; + if (S.lampKey !== lk) (S.lampKey = lk), lampField(S, next); const { M, LX, LY, LZ, LI, HZ, bg } = S; nr = 0; ORD.length = 0; @@ -565,13 +509,16 @@ export function draw(display, params) { if (sl.ph === WAIT) continue; const cyp = sl.i * sh + sh / 2; const pi = (S.base + 3 * sl.i + sl.seq) % PAPERS.length; - if (sl.old) placeSheet(S, M, 0, 1, 0, sl.old.kmax, (S.base + 3 * sl.i + sl.old.seq) % PAPERS.length, cyp, fw, fh, zmax, 0, 0, -1e8, 1, q++); + if (sl.old) placeSheet(S, M, 0, 1, 0, sl.old.kmax, (S.base + 3 * sl.i + sl.old.seq) % PAPERS.length, cyp, fw, fh, zmax, 0, 0, 0, -1e8, 1, q++); let dx = 0; + let dy = 0; let dphi = 0; if (sl.ph === IN) { + // Slid off the pack in the corner. const e = 1 - ease(sl.pt / 1.2); - dx = -78 * e; - dphi = 0.35 * e; + dx = -62 * e; + dy = (8 - cyp) * e; + dphi = -0.2 * e; } let flap = 0; if (sl.ph === SIT) { @@ -579,20 +526,16 @@ export function draw(display, params) { const t = (sl.pt - 0.6) / 1.6; if (t > 0 && t < 1) flap = 0.78 * PI * sin(PI * t) ** 2; } - const mem = sl.ph === UNFOLD || sl.ph === CREASE || (sl.ph === FOLD && sl.kmax >= M.n) ? 1 : 0; - placeSheet(S, M, sl.k, sl.u, flap, sl.kmax, pi, cyp, fw, fh, zmax, dx, dphi, 0, mem, q++); + const mem = sl.ph === UNFOLD || sl.ph === CREASE ? 1 : 0; + placeSheet(S, M, sl.k, sl.u, flap, sl.kmax, pi, cyp, fw, fh, zmax, dx, dy, dphi, 0, mem, q++); } ORD.sort((a, b) => RK[a] - RK[b]); ID.fill(-1); for (const r of ORD) { const cp = RCP[r]; - const o = r * 6; - const cnt = cp.length / 2; - for (let j = 0; j < cnt; j++) { - VX[j] = RA[o] * cp[2 * j] + RA[o + 1] * cp[2 * j + 1] + RA[o + 4]; - VY[j] = RA[o + 2] * cp[2 * j] + RA[o + 3] * cp[2 * j + 1] + RA[o + 5]; - } - fill(cnt, r, 0); + const A = RA.subarray(r * 6, r * 6 + 6); + for (let j = 0; j < cp.length; j += 2) (VX[j / 2] = tx(A, cp, j)), (VY[j / 2] = ty(A, cp, j)); + fill(cp.length / 2, r, 0); } const lx = S.lp[0]; const sx = lx < 31.5 ? -1 : 1; @@ -600,55 +543,37 @@ export function draw(display, params) { for (let x = 0; x < W; x++) { const p = y * W + x; const r = ID[p]; - let R; - let G; - let B; if (r < 0) { const f = SHK[p] > -1e11 ? 0.55 : 1; - R = bg[p * 3] * f; - G = bg[p * 3 + 1] * f; - B = bg[p * 3 + 2] * f; + display.setPixel(x, y, bg[p * 3] * f, bg[p * 3 + 1] * f, bg[p * 3 + 2] * f); } else { + // The print, found back in the crease pattern: paper square s, t in -1..1. const o = r * 6; - const a = RA[o]; - const b = RA[o + 1]; - const c = RA[o + 2]; - const d = RA[o + 3]; + const [a, b, c, d] = [RA[o], RA[o + 1], RA[o + 2], RA[o + 3]]; const det = a * d - b * c; const P = PAPERS[RP[r]]; let col = P[0]; let kind = P[2]; if (RS[r] < 0) (col = WHITE), (kind = -1); - else if (kind === 1 || kind === 3 || kind === 4) { - if (abs(det) > 1e-3) { - const X = x + 0.5 - RA[o + 4]; - const Y = y + 0.5 - RA[o + 5]; - const cu = (d * X - b * Y) / det; - const cv = (-c * X + a * Y) / det; - if (print(kind, cu + cv, cv - cu)) col = P[1]; - } + else if (P[1] && abs(det) > 1e-3) { + const X = x + 0.5 - RA[o + 4]; + const Y = y + 0.5 - RA[o + 5]; + const cu = (d * X - b * Y) / det; + const cv = (a * Y - c * X) / det; + if (print(kind, cu + cv, cv - cu)) col = P[1]; } - const nx = RN[r * 3]; - const ny = RN[r * 3 + 1]; const nz = RN[r * 3 + 2]; - const dif = max(0, nx * LX[p] + ny * LY[p] + nz * LZ[p]); const I = LI[p]; - let lit = I * (0.22 + 0.9 * dif); - let sp = 0; - const hz = nz * HZ[p]; - if (kind === 2) { - lit *= 0.45; - sp = 1.3 * I * max(0, hz) ** 30; - } else sp = 0.06 * I * max(0, hz) ** 8; + const hz = max(0, nz * HZ[p]); + // Kami is matte with a little sheen; foil is darker but throws the lamp back. + const fo = kind === 2; + const lit = I * (0.22 + 0.9 * max(0, RN[r * 3] * LX[p] + RN[r * 3 + 1] * LY[p] + nz * LZ[p])) * (fo ? 0.8 : 1); + const sp = I * (fo ? 0.25 + 0.9 * hz ** 12 : 0.06 * hz ** 8); // Shade under a lifted flap, and along the edge of a layer above. const k = RK[r]; let f = SHK[p] > k ? 0.55 : 1; - const qx = x - sx; - const qy = y - 1; - if (qx >= 0 && qx < W && qy >= 0) { - const s = ID[qy * W + qx]; - if (s >= 0 && RQ[s] === RQ[r] && RK[s] > k + 0.5 && RG[s] !== RG[r]) f *= 0.66; - } + const s = x - sx >= 0 && x - sx < W && y > 0 ? ID[p - W - sx] : -1; + if (s >= 0 && RQ[s] === RQ[r] && RK[s] > k + 0.5 && RG[s] !== RG[r]) f *= 0.66; // Creases left from the folding. if (RM[r] > 0) { for (const s of [x + 1 < W ? ID[p + 1] : -1, y + 1 < H ? ID[p + W] : -1]) @@ -658,10 +583,9 @@ export function draw(display, params) { break; } } - R = 255 * f * (col[0] * lit + sp * (kind === 2 ? col[0] : 1)); - G = 255 * f * (col[1] * lit + sp * (kind === 2 ? col[1] : 1)); - B = 255 * f * (col[2] * lit + sp * (kind === 2 ? col[2] : 1)); + const m = 255 * f * (fo ? lit + sp : lit); + const e = fo ? 0 : 255 * f * sp; + display.setPixel(x, y, min(255, col[0] * m + e), min(255, col[1] * m + e), min(255, col[2] * m + e)); } - display.setPixel(x, y, min(255, R), min(255, G), min(255, B)); } } From 251d5e31ac4f5fd347b82ba9db60729a06916424 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:56:48 +0000 Subject: [PATCH 26/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20Origami?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- incoming/pattern-study/patterns/origami.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/patterns/origami.js b/incoming/pattern-study/patterns/origami.js index 016f468e8..80c7f144c 100644 --- a/incoming/pattern-study/patterns/origami.js +++ b/incoming/pattern-study/patterns/origami.js @@ -55,8 +55,8 @@ const sec = (...s) => (f) => s.includes(f.sec); const low = (...s) => (f, x, y) => y > K && s.includes(f.sec); const legL = (f, x, y) => x < 0 && y > K; const legR = (f, x, y) => x > 0 && y > K; -// The legs turn up about a line through (0, YP) along NL: neck and tail rise -// 40 degrees above the table's edge. +// The legs turn up about a line through (0, YP) along NL, so neck and tail +// rise at 40 degrees. const YP = 0.34; const NL = [-0.906, 0.423]; const CRANE = [ From c897444211fd6fe63f6625493de9104822e81d61 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 06:58:36 +0000 Subject: [PATCH 27/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20README=20=EC=97=90=20Origami=20=EB=A5=BC=20?= =?UTF-8?q?=EC=93=B4=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_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 89f9d39ab..74169381f 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -289,7 +289,7 @@ | [Implosion](patterns/implosion.js) | 공사·건설 | 새벽 하늘 아래 스무 층 아파트에 장약 섬광이 층마다 타고 내려오면, 탑이 꺾이며 제자리로 주저앉고 회황색 먼지구름이 꼭대기까지 끓어올라요. 타임랩스로 비계 속에 새 탑이 서고, K1 을 누르면 바로 발파해요. | | [Lichtenberg](patterns/lichtenberg.js) | 프랙탈 | 소다물을 바른 단풍나무 판의 위아래 전극에서 깃털 잔가지를 단 프랙탈 나무가 파란 플라스마와 주황 불씨를 달고 서로를 향해 타 들어가고, 만나면 아크가 번쩍여요. K1 이 판의 나무를 바꾸고, 누르면 다음 판이에요. | | [Noborigama](patterns/noborigama.js) | 도자기 | 밤 언덕의 오름가마를 세로로 잘라 봐요. 불이 그릇 쌓인 칸들을 타고 올라 아래 칸은 희고 위 칸은 붉으며, 장작을 넣을 때마다 감시 구멍에서 불길이, 굴뚝에서 검은 연기와 불티가 나와요. K1 은 소성 날짜를 바꾸고, 누르면 모든 구멍에 한꺼번에 불을 넣어요. | -<> +| [Origami](patterns/origami.js) | 종이 | 어두운 호두나무 탁자의 램프 아래에서 청록 종이 한 장이 아홉 단계로 한 주름씩 접혀 종이학이 돼요. 학은 날개를 한 번 친 뒤 다시 펼쳐지고, 펼친 종이에는 산접기와 골접기의 주름 무늬가 남아요. K1 이 세이가이하 무늬 종이와 금박 같은 종이를 고르고, 누르면 학이 바로 펼쳐져요. | | [Pole Vault](patterns/pole-vault.js) | 운동·경기 | 밤 경기장에서 카메라가 장대를 든 선수를 따라 달리고, 활처럼 휜 장대를 타고 거꾸로 오른 선수가 패널 꼭대기 가까운 가로대를 넘어 매트에 떨어지면 심판이 흰 깃발이나 빨간 깃발을 들어요. K3 이 가로대를 5.00 m 에서 6.20 m 까지 올리고, 누르면 다음 선수가 바로 달려요. | | [Rice Planting](patterns/rice-planting.js) | 농사·밭 | 물 댄 논을 위에서 내려다봐요. 밀짚모자를 쓴 일꾼들이 못줄을 따라 뒷걸음질하며 모를 한 포기씩 꽂고, 앞쪽 거울 물에는 구름과 모춤, 백로가 있어요. K1 은 아침 안개에서 노을까지 돌리고, 누르면 남은 모를 한꺼번에 꽂고 줄을 넘겨요. | | [Shadow Play](patterns/shadow-play.js) | 무대·공연 | 등잔 불빛을 받은 흰 막에 와양 쿨릿 가죽 인형의 레이스 그림자가 비쳐요. 기사가 거인과 싸워 던져 버리면 구눙안이 떨며 쓸고 들어와 다음 장을 열고, K1 을 돌리면 막 뒤로 돌아가 금빛과 빨강, 남색 칠이 보여요. | @@ -1128,7 +1128,9 @@ K4 Spate 0 이면 물이 실 몇 가닥으로 갈라져요. 처음 값 0.06 이 남은 자리로 프랙탈 갈래의 넷째를 세우고, 촘촘함과 모양 바뀜이 함께 높은 칸을 겨눴어요. 세운 판재 위아래 끝의 전극에서 두 나무가 유전 파괴 모형(DBM)으로 자라 서로를 향해 타 들어가고, 만나면 이어진 길로 아크가 번쩍인 뒤 전원이 끊기고 다음 판이 미끄러져 들어와요. 앞쪽 끝은 판 전체에서 매 프레임 푸는 라플라스 퍼텐셜의 Pull 제곱에 비례해 자라요. 첫 판은 촘촘함 × 모양 바뀜 3,3(Flipdot 혼자)과 복잡도 × 모양 바뀜 4,3(Rabbit Ears 혼자)에 씨앗 여덟이 다 들었어요. 그런데 트리 2,557칸 가운데 2,333칸이 판 어디서나 돋는 잔가지라서, 4~8초에 1 LED 검은 직교 그물이 판을 덮었어요. 나무무늬보다 미로로 읽혔고, 8초에 1 LED 안에 탄 칸이 없는 나무는 4.0% 였어요. 줄기가 매단 칸 수가 12, 50, 160, 500 을 넘을 때마다 홈을 한 단씩 넓히고, 잔가지는 탄 줄기에서만 줄기 방향의 45도 앞으로 돋아 3~4, 2, 1 LED 로 짧아지며 세 대까지 갈라지게 했어요. 전극 쪽이 굵고 검은 나무가 되고 열린 나무가 42.2% 가 됐어요. 그 대가로 촘촘함이 0.248 에서 0.130 으로 내려가 3,3 칸을 잃었어요. 결 가로로 2.3줄마다 도는 단풍나무 곱슬무늬는 맨 판을 0.093 에서 0.115 로 올렸을 뿐이에요. 이 칸에 선 패턴들처럼 촘촘함 0.2 를 넘기려면 판 전체에 1 LED 결이 깔려야 해서, 빈 나무가 보여야 하는 그림과는 맞지 않았어요. 모양 바뀜은 열린 나무가 40% 를 넘자 0.20 아래로 떨어졌어요. 소다물 광택의 폭을 0.12 에서 0.28 로 키우고 전원이 켜진 동안 판 전체에서 마르게 하자 0.241 이 됐어요. 복잡도가 2.612 라서 4,3 칸에는 씨앗 여덟 가운데 일곱이 남아요. 첫 판에서 판을 60열로 넓혔을 때 복잡도가 2.748 로 축 끝을 넘었고, 채널마다 ±3 의 결을 나무 색에만 굽고 숯은 고른 색으로 두어 되돌렸어요. K3 Pull 은 범위를 0.5..4 에서 1..4 로 바꿔 η=1 에서 열고, 강할수록 앞쪽이 일찍 뻗게 해 노브 반응이 0.02 에서 0.04 가 됐어요. 노브 넷은 0.46, 0.13, 0.04, 0.07 이에요. -<> +### Origami + +남은 자리로 종이 갈래의 넷째를 세우고, 채움이 낮은데 모양 바뀜이 큰 칸을 겨눴어요. 21판부터 「다음 판 감」에 남아 있던, 접힌 종이학이 펼쳐지는 그림이에요. 어두운 호두나무 탁자의 램프 아래에서 종이 한 장이 아홉 단계로 접혀요. 반으로 접고, 돌리고, 사각 기본형과 연 접기, 앞뒤 꽃잎 접기로 학 기본형을 만든 다음, 다리를 가늘게 하고 목과 꼬리를 뒤집어 접고 머리를 세워요. 주름선마다 면을 잘라 두고 플랩을 경첩 둘레로 돌리며 경첩을 가로지르는 좌표를 cos θ 만큼 눌러요. 그래서 넘어가는 면은 어두워지고 흰 뒷면이 올라오며, 들린 플랩은 램프 반대쪽으로 그림자를 드리워요. 학이 날개를 한 번 친 다음 다시 펼쳐지면 산접기는 밝은 선으로, 골접기는 어두운 선으로 주름 무늬가 남아요. 겨눈 칸은 채움 × 모양 바뀜 1,3 이에요. 채움 0.302 아래에 모양 바뀜 0.219~0.332 인 자리이고, 16판의 Domino Run 혼자 서 있었어요. 첫 판은 첫 접기 동안 종이를 마름모로 돌리며 카메라가 물러나서, 삼각형이 60×30 LED 로 작아지고 모양 바뀜이 0.193 에 그쳤어요. 접는 동안 정사각형 그대로 두고 돌리기를 따로 한 단계로 떼어 내고, 청록 종이의 가장 밝은 채널을 0.66 에서 0.8 로 올리자 0.254 가 됐어요. 2초의 큰 정사각형이 8초에 좁은 학 기본형이 되는 것이 블록 평균을 크게 옮겨요. 왼쪽 위 종이 묶음을 처음에 38 LED 로 그렸더니 채움이 0.222 에서 0.316 으로 올라 칸 경계 0.302 를 넘었어요. 묶음을 26 LED 로 줄이자 0.264 가 되어, 씨앗 여덟이 모두 채움 0.264, 모양 바뀜 0.252~0.255 로 그 칸에 들어요. 플랩이 1초에 한 번쯤 넘어가서 움직임은 0.243 이라, 움직임 × 모양 바뀜 1,3(Day and Night 혼자)에는 닿지 않았어요. 새 종이에 대각선 줄이 희미하게 비쳤어요. 미리 잘라 둔 여덟 조각이 저마다 기울어서였고, 기울기를 같은 층의 면끼리 나누게 바꾸자 새 종이는 평평해지고 펼친 종이만 면마다 기울어요. 세이가이하 무늬는 비늘 간격을 1 로 두었더니 패널에서 잡음으로 보여서, 간격을 2 로 넓혀 고리 세 겹이 읽히게 했어요. 노브 넷은 0.21, 0.25, 0.25, 0.06 이에요. ### Shadow Play From 656b612597b626a5f380cfb75189b3b6a3ca817c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 07:01:20 +0000 Subject: [PATCH 28/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20README=20=EC=9D=98=2026=ED=8C=90=20=EB=A8=B8?= =?UTF-8?q?=EB=A6=AC=EC=99=80=20=EB=8B=A4=EC=9D=8C=20=ED=8C=90=20=EA=B0=90?= =?UTF-8?q?=EC=9D=84=20=EC=93=B0=EB=8A=94=20=EC=A4=91?= 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_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 74169381f..8120bbdad 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1094,7 +1094,7 @@ K4 Spate 0 이면 물이 실 몇 가닥으로 갈라져요. 처음 값 0.06 이 ## 26판 -<> +26판도 새 패턴 열 개를 열 갈래에 하나씩 냈어요. 주제 1 은 이번에도 겨눌 칸이 없었어요. 기준 158개의 `--gaps` 가 21장 모두 `none` 이었어요. 그 대신 패턴이 하나뿐이던 칸 넷을 Dark Field 와 Lichtenberg, Origami 로 겨눠 거기 둘째로 세웠어요. 주제 2 로 얼음·극지 갈래를 Iceberg 로, 미생물 갈래를 Dark Field 로 열었어요. 주제 4 는 Iceberg 와 Noborigama, Rice Planting, Implosion, Geyser, Pole Vault 로 여섯이에요. 여섯 다 3분의 1마다 켜진 LED 가 5% 를 넘고, 바탕을 지운 사본으로 다시 재도 주인공만으로 3분의 1마다 켜진 LED 나 2~8초 동안 바뀐 LED 가 5% 를 넘어요. Lichtenberg 와 Origami, Shadow Play 는 주제 없이 남은 자리로 짰어요. 농사·밭과 화산은 패턴이 하나뿐이던 갈래라 이번에 둘째가 섰고, 공사·건설과 도자기에는 셋째가, 무대·공연과 프랙탈, 종이, 운동·경기에는 넷째가 섰어요. 주제 3 은 앞 바퀴 PR 에 창작자 글이 없어서 하지 않았어요. <> ### Iceberg @@ -1184,13 +1184,13 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 `study.ts --check --gaps` 가 `*` 를 붙인 칸 가운데 그림이 떠오르는 칸이에요. 칸은 가로와 세로를 낮은 쪽부터 1~4 로 세요. 페이지 기본 지도(가장자리 × 움직임)는 16칸이 다 차 있어요. -23판 뒤로는 `*` 가 붙은 칸이 없어요. 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 어두운 선으로 나눈 작은 타일에 선명한 색을 여럿 두면 촘촘함과 색이 함께 높은 자리에 닿아요. +23판 뒤로는 `*` 가 붙은 칸이 없어요. 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.254 로 Domino Run 혼자이던 채움 × 모양 바뀜 1,3 에 서요. Lichtenberg 는 Rabbit Ears 혼자이던 복잡도 × 모양 바뀜 4,3 에 씨앗 여덟 가운데 일곱이 들어요. 168개에서 패턴이 하나뿐인 칸은 <>칸이에요. -촘촘함이 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 로 내려갔어요. +촘촘함이 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 를 냈어요. -모양 바뀜이 높은 칸을 겨누면 움직이는 것과 바탕의 밝기가 달라야 해요. 3판의 Lava Lamp 는 방울이 화면 높이를 다 오르는데도 왁스와 물이 둘 다 밝아서 모양 바뀜이 0.068 이에요. 4판의 Jacquard Loom 도 천이 1초에 4 LED 씩 내려가는데, 씨실 여섯 색이 모두 밝기 끝까지라 0.041 이에요. 19판의 Rabbit Ears 도 첫 판에서는 그림이 눈 속으로 사라져도 블록 평균이 0.5 쯤으로 같아서 0.153 이었어요. 바다를 어둡게 하고 신호가 없을 때 눈을 밝게 올리자 0.286 이 됐어요. 모양 바뀜의 칸은 4 LED 너비에 16 LED 높이라서, 옆으로 쓸고 가는 것이 위아래로 오르내리는 것보다 네 배 잘 잡혀요. 7판의 Searchlights 가 그렇게 0.357 까지 갔어요. 색이 많아도 모양 바뀜은 밝기만 봐요. 11판의 Balloon Box 는 천의 모든 색을 밝기 끝에 둬서, 모양 바뀜과 가장자리는 풍선과 어두운 하늘의 차이에서만 와요. 16판의 Domino Run 은 꺼지는 쪽에서 모양 바뀜을 얻었어요. 넘어지는 도미노는 같은 4×16 칸 안에서 눕기만 해서 처음에 0.096 이었고, 낮은 램프가 누운 면을 스치게 하자 켜진 LED 가 0.44 에서 0.15 로 줄며 0.261 이 됐어요. 모양 바뀜은 2초 화면에서 멀어진 거리라 6초 동안 바뀐 양을 넘지 못해서, 움직임의 여섯 배가 끝이에요. 16판의 Lunation 은 2~8초 동안 어두워지는 LED 가 하나도 없게 짜서 움직임 0.058 에 모양 바뀜 0.348 로 그 끝에 섰어요. 아래 끝도 끝이에요. 모양 바뀜 축의 아래 끝은 Galton Board 의 0.025 이고, 17판의 열 개 가운데 여섯이 처음 판에 그 아래였어요. Ant Farm 0.010, One Bit 0.014, Marble Run 0.019, Circuit Board 0.021, Crossroads 0.023, Shoreline 0.029 예요. 바탕이 서 있고 작은 것만 움직이면 4×16 칸 평균이 거의 그대로라서, 블록만 한 것이 천천히 바뀌어야 해요. Ant Farm 은 젖은 모래 얼룩, Circuit Board 는 데워지는 선, Marble Run 은 구슬이 지나며 켜는 플라스틱 조각, One Bit 는 오르내리는 안개 둑으로 0.04~0.06 에 섰어요. 18판에서는 Server Rack 의 첫 판이 0.024 였어요. 깜박이는 LED 는 4×16 칸 안에서 평균이 지워져서, 백업이 쓴 드라이브의 빛길을 켜 둔 채 남기고 부하 물결을 넓혀 0.098 이 됐어요. 거꾸로 1초가 안 되는 섬광은 모양 바뀜을 크게 올려요. 모양 바뀜은 첫 화면에서 가장 멀어진 프레임 하나로 정해져서, 18판 Drop Tower 의 0.4초 남짓 보이는 브레이크 섬광이 0.087 을 0.163 으로 올렸어요. 19판에서는 Helter Skelter 의 첫 판이 0.021 이었어요. 전구 체이스가 나선을 따라 내려가며 미끄럼틀과 탑을 밝히고 8초마다 전구가 1.5초 다 켜지게 하자 0.069 가 됐어요. 21판의 Bread Mould 는 곰팡이로 어두워진 빵을 밝은 새 빵이 옆에서 밀어내는 일을 2.4초에 두어 3.5초에 0.356 에 닿았어요. 도마를 늙은 곰팡이와 새 속살 사이의 밝기 100 쯤에 두어서, 빵이 덮는 넓이만 모양 바뀜에 들어가요.아래 끝을 넘을 뻔한 패턴도 있었어요. 21판의 Slinky 는 250 LED 남짓한 스프링이 계단을 내려가기만 할 때 0.014 로 Galton Board 아래였고, 내려앉은 칸마다 계단등이 켜져 8초 남게 하자 0.056 이 됐어요. 22판에서는 아래 끝을 넘은 첫 판이 둘이었어요. Weaver 는 0.017 이었고, 하늘 아닌 곳에 구름 그림자 두 줄을 1초에 3 LED 씩 지나가게 하자 0.044 가 됐어요. Galilean Moons 는 0.022 였고, 도표의 하루를 6줄에서 10줄로 늘리자 0.031 이 됐어요. 높은 쪽은 셈으로 맞출 수 있었어요. Pin Art 는 0.613 × 0.575 = 0.352 가 잰 값 0.357 이고, Flipdot 은 0.36 × (1 − 0.248) 이 잰 값 0.265 예요. 둘 다 바뀐 넓이에 칸 평균의 차이를 곱한 셈이에요. 23판에서는 그 셈을 그림의 차례로 맞췄어요. Sequins 는 2초 화면에 아래 52% 의 파도가 있고 7.6초에 위 33% 의 하트가 있어서 판의 75% 가 바뀌어 0.250 이에요. Bait Ball 은 어두운 등을 보이던 떼가 2.1초부터 은빛으로 돌아서 0.372 이고, 은빛일 때 공 둘레 물까지 밝히는 빛무리가 0.304 를 0.348 로 올렸어요. 씨앗마다 0.351~0.400 으로 흔들리던 값은 2초 화면에 사냥꾼이 있느냐로 갈렸고, 첫 돌진 시각을 고정하자 0.367~0.377 로 좁아졌어요. 거꾸로 Elephant Toothpaste 는 밝은 기둥이 어두운 판을 위아래로 다 가로질러도 패널 너비의 40% 쯤만 덮어서 0.102 에 그쳤어요. 24판에서는 하늘이 모양 바뀜을 가렸어요. Thermal 은 적운이 자라는데도 첫 판이 0.021 이었어요. 가장 밝은 채널로 보면 200~222 의 파란 하늘과 흰 구름이 거의 같아서, 천정을 (14,50,150) 으로 어둡게 하자 구름이 자라는 위의 16줄 띠 둘이 0.0066 에서 0.0207 이 됐어요. Pendulum Wave 는 색을 올리려고 바닥을 밝히자 추와 바닥의 차이가 줄어 모양 바뀜이 0.039 에서 0.023 으로 떨어졌어요. Shoot 'em Up 은 보스의 마지막 폭발 섬광 하나가 8초 창 안에 들면 0.30, 밖이면 0.09~0.13 이에요. 25판에서는 서 있는 그림이 넓은 빛 하나로 모양 바뀜을 얻었어요. Excavation 은 굴착기와 클램셸만 움직일 때 0.013 이었고, 데크 틈으로 드는 햇빛 기둥이 2초의 구름 그늘(틈 아래 평균 0.52)에서 8초에 0.97 로 개게 하자 0.054 가 됐어요. Kintsugi 는 6.5초에 한 번 흔들리는 종이 등의 빛무리만으로 0.116 이라서, 사발이 깨지는 몫은 등을 멈춘 사본으로 따로 재야 보였어요. 그 사본은 0.039~0.053 이에요. Snow Monkeys 도 김을 끄면 0.115 가 0.024 로 떨어져요. 김 뒤의 비탈을 평균 밝기 69 로 어둡게 두어서 240 의 김이 블록을 바꿔요. 이어진 구름층은 흘러도 모양 바뀜을 내지 않아요. Skydiver 는 적운이 서로 이어져 흰 띠가 되자 0.030 이었고, 128열에 적운을 넷만 두어 틈을 내자 구름층이 지나는 16줄 칸 줄의 몫이 0.0049 에서 0.0142 가 되어 0.045 가 됐어요. 제자리에서 도는 원도 거의 내지 않아요. Ferris Wheel 은 연무와 구름을 멈추면 0.045 였고, 쇼의 평균 빛을 하늘 연무에 묻히자 0.100 이 됐어요. 띠가 위아래로 흐르는 그림은 밝기의 주기가 값을 정했어요. Population Pyramid 의 10줄 세대 띠를 두 단 밝기로 번갈아 칠하면 20줄 주기라 16줄 칸 안의 평균이 거의 그대로여서 0.029 였어요. 세 단으로 30줄 주기를 만들자 다른 것은 그대로인 채 0.073 이 됐어요. Rice Terraces 는 하늘을 비추는 논이 판의 70% 쯤이라 바탕이 모양 바뀜을 냈어요. 논이 지평선 쪽 주황만 비출 때는 0.023 이었고, 가까운 논이 짙은 파랑의 머리 위 하늘을 비추게 하자 구름 반사가 블록만 하게 지나가며 0.063 이 됐어요. +모양 바뀜이 높은 칸을 겨누면 움직이는 것과 바탕의 밝기가 달라야 해요. 3판의 Lava Lamp 는 방울이 화면 높이를 다 오르는데도 왁스와 물이 둘 다 밝아서 모양 바뀜이 0.068 이에요. 4판의 Jacquard Loom 도 천이 1초에 4 LED 씩 내려가는데, 씨실 여섯 색이 모두 밝기 끝까지라 0.041 이에요. 19판의 Rabbit Ears 도 첫 판에서는 그림이 눈 속으로 사라져도 블록 평균이 0.5 쯤으로 같아서 0.153 이었어요. 바다를 어둡게 하고 신호가 없을 때 눈을 밝게 올리자 0.286 이 됐어요. 모양 바뀜의 칸은 4 LED 너비에 16 LED 높이라서, 옆으로 쓸고 가는 것이 위아래로 오르내리는 것보다 네 배 잘 잡혀요. 7판의 Searchlights 가 그렇게 0.357 까지 갔어요. 색이 많아도 모양 바뀜은 밝기만 봐요. 11판의 Balloon Box 는 천의 모든 색을 밝기 끝에 둬서, 모양 바뀜과 가장자리는 풍선과 어두운 하늘의 차이에서만 와요. 16판의 Domino Run 은 꺼지는 쪽에서 모양 바뀜을 얻었어요. 넘어지는 도미노는 같은 4×16 칸 안에서 눕기만 해서 처음에 0.096 이었고, 낮은 램프가 누운 면을 스치게 하자 켜진 LED 가 0.44 에서 0.15 로 줄며 0.261 이 됐어요. 모양 바뀜은 2초 화면에서 멀어진 거리라 6초 동안 바뀐 양을 넘지 못해서, 움직임의 여섯 배가 끝이에요. 16판의 Lunation 은 2~8초 동안 어두워지는 LED 가 하나도 없게 짜서 움직임 0.058 에 모양 바뀜 0.348 로 그 끝에 섰어요. 아래 끝도 끝이에요. 모양 바뀜 축의 아래 끝은 Galton Board 의 0.025 이고, 17판의 열 개 가운데 여섯이 처음 판에 그 아래였어요. Ant Farm 0.010, One Bit 0.014, Marble Run 0.019, Circuit Board 0.021, Crossroads 0.023, Shoreline 0.029 예요. 바탕이 서 있고 작은 것만 움직이면 4×16 칸 평균이 거의 그대로라서, 블록만 한 것이 천천히 바뀌어야 해요. Ant Farm 은 젖은 모래 얼룩, Circuit Board 는 데워지는 선, Marble Run 은 구슬이 지나며 켜는 플라스틱 조각, One Bit 는 오르내리는 안개 둑으로 0.04~0.06 에 섰어요. 18판에서는 Server Rack 의 첫 판이 0.024 였어요. 깜박이는 LED 는 4×16 칸 안에서 평균이 지워져서, 백업이 쓴 드라이브의 빛길을 켜 둔 채 남기고 부하 물결을 넓혀 0.098 이 됐어요. 거꾸로 1초가 안 되는 섬광은 모양 바뀜을 크게 올려요. 모양 바뀜은 첫 화면에서 가장 멀어진 프레임 하나로 정해져서, 18판 Drop Tower 의 0.4초 남짓 보이는 브레이크 섬광이 0.087 을 0.163 으로 올렸어요. 19판에서는 Helter Skelter 의 첫 판이 0.021 이었어요. 전구 체이스가 나선을 따라 내려가며 미끄럼틀과 탑을 밝히고 8초마다 전구가 1.5초 다 켜지게 하자 0.069 가 됐어요. 21판의 Bread Mould 는 곰팡이로 어두워진 빵을 밝은 새 빵이 옆에서 밀어내는 일을 2.4초에 두어 3.5초에 0.356 에 닿았어요. 도마를 늙은 곰팡이와 새 속살 사이의 밝기 100 쯤에 두어서, 빵이 덮는 넓이만 모양 바뀜에 들어가요.아래 끝을 넘을 뻔한 패턴도 있었어요. 21판의 Slinky 는 250 LED 남짓한 스프링이 계단을 내려가기만 할 때 0.014 로 Galton Board 아래였고, 내려앉은 칸마다 계단등이 켜져 8초 남게 하자 0.056 이 됐어요. 22판에서는 아래 끝을 넘은 첫 판이 둘이었어요. Weaver 는 0.017 이었고, 하늘 아닌 곳에 구름 그림자 두 줄을 1초에 3 LED 씩 지나가게 하자 0.044 가 됐어요. Galilean Moons 는 0.022 였고, 도표의 하루를 6줄에서 10줄로 늘리자 0.031 이 됐어요. 높은 쪽은 셈으로 맞출 수 있었어요. Pin Art 는 0.613 × 0.575 = 0.352 가 잰 값 0.357 이고, Flipdot 은 0.36 × (1 − 0.248) 이 잰 값 0.265 예요. 둘 다 바뀐 넓이에 칸 평균의 차이를 곱한 셈이에요. 23판에서는 그 셈을 그림의 차례로 맞췄어요. Sequins 는 2초 화면에 아래 52% 의 파도가 있고 7.6초에 위 33% 의 하트가 있어서 판의 75% 가 바뀌어 0.250 이에요. Bait Ball 은 어두운 등을 보이던 떼가 2.1초부터 은빛으로 돌아서 0.372 이고, 은빛일 때 공 둘레 물까지 밝히는 빛무리가 0.304 를 0.348 로 올렸어요. 씨앗마다 0.351~0.400 으로 흔들리던 값은 2초 화면에 사냥꾼이 있느냐로 갈렸고, 첫 돌진 시각을 고정하자 0.367~0.377 로 좁아졌어요. 거꾸로 Elephant Toothpaste 는 밝은 기둥이 어두운 판을 위아래로 다 가로질러도 패널 너비의 40% 쯤만 덮어서 0.102 에 그쳤어요. 24판에서는 하늘이 모양 바뀜을 가렸어요. Thermal 은 적운이 자라는데도 첫 판이 0.021 이었어요. 가장 밝은 채널로 보면 200~222 의 파란 하늘과 흰 구름이 거의 같아서, 천정을 (14,50,150) 으로 어둡게 하자 구름이 자라는 위의 16줄 띠 둘이 0.0066 에서 0.0207 이 됐어요. Pendulum Wave 는 색을 올리려고 바닥을 밝히자 추와 바닥의 차이가 줄어 모양 바뀜이 0.039 에서 0.023 으로 떨어졌어요. Shoot 'em Up 은 보스의 마지막 폭발 섬광 하나가 8초 창 안에 들면 0.30, 밖이면 0.09~0.13 이에요. 25판에서는 서 있는 그림이 넓은 빛 하나로 모양 바뀜을 얻었어요. Excavation 은 굴착기와 클램셸만 움직일 때 0.013 이었고, 데크 틈으로 드는 햇빛 기둥이 2초의 구름 그늘(틈 아래 평균 0.52)에서 8초에 0.97 로 개게 하자 0.054 가 됐어요. Kintsugi 는 6.5초에 한 번 흔들리는 종이 등의 빛무리만으로 0.116 이라서, 사발이 깨지는 몫은 등을 멈춘 사본으로 따로 재야 보였어요. 그 사본은 0.039~0.053 이에요. Snow Monkeys 도 김을 끄면 0.115 가 0.024 로 떨어져요. 김 뒤의 비탈을 평균 밝기 69 로 어둡게 두어서 240 의 김이 블록을 바꿔요. 이어진 구름층은 흘러도 모양 바뀜을 내지 않아요. Skydiver 는 적운이 서로 이어져 흰 띠가 되자 0.030 이었고, 128열에 적운을 넷만 두어 틈을 내자 구름층이 지나는 16줄 칸 줄의 몫이 0.0049 에서 0.0142 가 되어 0.045 가 됐어요. 제자리에서 도는 원도 거의 내지 않아요. Ferris Wheel 은 연무와 구름을 멈추면 0.045 였고, 쇼의 평균 빛을 하늘 연무에 묻히자 0.100 이 됐어요. 띠가 위아래로 흐르는 그림은 밝기의 주기가 값을 정했어요. Population Pyramid 의 10줄 세대 띠를 두 단 밝기로 번갈아 칠하면 20줄 주기라 16줄 칸 안의 평균이 거의 그대로여서 0.029 였어요. 세 단으로 30줄 주기를 만들자 다른 것은 그대로인 채 0.073 이 됐어요. Rice Terraces 는 하늘을 비추는 논이 판의 70% 쯤이라 바탕이 모양 바뀜을 냈어요. 논이 지평선 쪽 주황만 비출 때는 0.023 이었고, 가까운 논이 짙은 파랑의 머리 위 하늘을 비추게 하자 구름 반사가 블록만 하게 지나가며 0.063 이 됐어요. 26판의 Implosion 은 먼지를 퍼프의 빛을 무게로 평균 내 칠하자 판 전체가 평평한 회색 안개가 되어 0.046 이었어요. 퍼프마다 해와 하늘에서 빛을 받는 공으로 그려 앞의 것 위에 겹치자 0.09 가 됐어요. 흰 김도 그래서, Geyser 는 김 덩어리에 왼쪽 위 빛의 음영을 주고 정오 하늘을 (20,62,152) 로 짙게 해 0.036 에서 0.044~0.050 이 됐어요. 판 전체가 한꺼번에 바뀌는 빛도 모양 바뀜을 내요. Lichtenberg 는 빈 나무가 40% 를 넘자 0.20 아래로 내려갔고, 소다물 광택이 전원이 켜진 동안 판 전체에서 마르게 하자 0.241 이 됐어요. -움직임 축의 아래 끝도 끝이에요. 아래 끝은 Star Trails 의 0.019 이고, 18판의 Stained Glass 는 첫 판이 0.007, Peg Light 는 0.023 이었어요. 블록만 한 구름 그림자가 1초에 5 LED 로 지나가면 모양 바뀜은 0.036 이어도 1초 사이에 바뀌는 빛은 작아요. Stained Glass 는 그림자를 11 LED 덩어리로 얼룩지게 하고 1초에 18 LED 로 지나가게 해서 0.027 이 됐어요. Peg Light 는 1초에 페그 100개를 꽂아도 LED 하나가 한 번 바뀌는 셈이라 그 몫이 0.012 였고, 켜진 페그마다 ±3.5% 로 1.4 Hz 에 일렁이게 해서 0.035 가 됐어요. +움직임 축의 아래 끝도 끝이에요. 아래 끝은 Star Trails 의 0.019 이고, 18판의 Stained Glass 는 첫 판이 0.007, Peg Light 는 0.023 이었어요. 블록만 한 구름 그림자가 1초에 5 LED 로 지나가면 모양 바뀜은 0.036 이어도 1초 사이에 바뀌는 빛은 작아요. Stained Glass 는 그림자를 11 LED 덩어리로 얼룩지게 하고 1초에 18 LED 로 지나가게 해서 0.027 이 됐어요. Peg Light 는 1초에 페그 100개를 꽂아도 LED 하나가 한 번 바뀌는 셈이라 그 몫이 0.012 였고, 켜진 페그마다 ±3.5% 로 1.4 Hz 에 일렁이게 해서 0.035 가 됐어요. 위 끝 쪽은 프레임마다 새로 뿌리는 결이 값을 부풀려요. 26판의 Dark Field 는 켜진 LED 마다 ±20% 의 반짝임과 검은 바탕 전체의 23/255 아래 결을 프레임마다 새로 뿌려서 움직임이 2.306 이었어요. 패널에서는 텔레비전 잡음처럼 끓는 판이었어요. 같은 결을 LED 마다 한 번 정해 두자 움직임은 1.381 로 내려갔고, 복잡도는 2.080 에서 2.081, 촘촘함과 채움은 그대로였어요. 복잡도는 한 화면을 재서, 결이 그 자리에 서 있어도 똑같이 올라요. 13판에서 채움 축의 아래 끝이 Glyph Rain 의 0.059 에서 Dragon Fold 의 0.117 로 올라가서, 채움 칸의 경계가 0.257, 0.530, 0.802 에서 0.302, 0.558, 0.815 로 옮겨졌어요. 그래서 복잡도 × 채움 3,1 은 한동안 `*` 가 빠졌고, 18판 뒤에는 다시 `*` 가 붙어 있었어요. 19판의 Foxfire 가 채움 0.196, 복잡도 2.001 로 그 칸을 채웠어요. @@ -1210,7 +1210,7 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. ### 세로 64×128 -2~8초 동안 켜진 LED 가 5% 아래인 3분의 1이 있는 패턴이 넷이에요. `study.ts` 표의 `thirds` 칸이 이 값이에요. Jelly 4.5% 와 Pulse Bars 2.6% 는 위 3분의 1이 비고, Sparks 2.0% 와 Dragon Fold 4.9% 는 아래 3분의 1이 비어요. Dragon Fold 는 문턱 바로 아래예요. 6판의 Jacob's Ladder 는 위, 가운데, 아래가 27.0%, 21.4%, 35.1% 예요. 햇빛층에서 심해까지 내려가는 바다는 16판의 Abyss 가 했고, 덩굴은 15판의 Ivy 가 했어요. 어두운 판 위의 가는 빛 하나는 128줄을 다 지나가도 3분의 1마다 10% 를 넘기 어려워요. 6판의 처음 아크는 가운데 3분의 1을 7.4% 켰어요. 9판의 Sprout 은 접시꽃 둘이 위 3분의 1을 8.1% 켜요. 이렇게 철이 도는 패턴은 2~8초가 어느 철에 걸리느냐에 따라 값이 몇 배씩 달라서, 씨앗 여럿으로 재 봐야 해요. 테두리를 그리는 패턴은 테두리를 빼고 재요. 10판의 Tetromino Well 은 양쪽 벽 2 LED 만으로 3분의 1마다 6.3% 를 켜서, 벽을 넣어 재면 위 3분의 1이 늘 문턱을 넘어요. 바탕을 다 그리는 패턴은 `thirds` 칸이 3분의 1마다 90 을 넘어요. 14판의 Waterfall 은 98/93/97 이에요. 그런 패턴은 2~8초 동안 밝기가 24/255 넘게 바뀌는 LED 로 세어야 그림이 128줄을 쓰는지 보여요. 그렇게 세면 Waterfall 의 위 3분의 1은 25.9% 예요. 15판의 Ivy 는 벽을 그려 `thirds` 가 84/89/82 이고, 바뀐 LED 로 세면 48.7%, 70.6%, 29.2% 예요. 가만히 있는 가는 선은 켜져 있어도 바뀌지 않아요. 15판의 Incense 는 곧은 연기 실이 가운데 3분의 1을 지나도 바뀐 LED 가 3.7% 였고, 소용돌이가 그 3분의 1 안에서 시작되게 하자 15.5% 가 됐어요. 16판의 Abyss 는 물을 다 칠해서 `thirds` 가 100/80/22 이고, 바뀐 LED 로 세면 84.5%, 59.3%, 36.0% 예요. 한밤층은 아귀와 불꽃, 잠수정 등빛뿐이라 아래 3분의 1이 가장 낮아요. 16판의 Harp 는 액자를 빼고 줄만 재면 48/53/12 이고, 바뀐 LED 는 아래 3분의 1이 9.8% 예요. 거기는 받침과 공명통 발이고, 줄은 300 LED 남짓한 삼각형에만 있어요. 17판의 Marble Run 은 레일과 리프트가 서 있어서 `thirds` 가 39/30/68 이고, 바뀐 LED 는 40.7%, 54.3%, 59.7% 예요. 17판의 Ant Farm 은 모래가 바탕이라 `thirds` 가 100/97/96 이고, 바뀐 LED 는 6.9%, 34.5%, 53.4% 예요. 위 3분의 1은 모래 윗면과 그 위의 어두운 유리라서 개미가 드나드는 입구와 더미만 바뀌어요. 18판의 Server Rack 은 앞판을 13~23 으로 24 아래에 두어서, `thirds` 63/63/61 이 다 드라이브와 코드와 LED 의 빛이에요. 바뀐 LED 셈은 2~8초 가운데 한 프레임만 24 를 넘어도 그 LED 를 세서, 무작위 반짝임이 값을 부풀려요. 18판의 Pint 는 잔 기포 반짝임을 켜면 50.3%, 60.2%, 40.0% 이고 끄면 50.2%, 46.8%, 26.3% 예요. 빛의 흔들림도 그래요. Longcase Clock 은 촛불일 때 41.8%, 45.6%, 45.3% 이고 흔들림 없는 달빛에서는 2.6%, 4.2%, 5.9% 예요. 자라는 그림은 미리 돌리기를 시간 대신 상태로 멈추면 씨앗마다 같은 자리에서 열려요. 18판의 Bismuth 는 무리 꼭대기가 20번 줄에 닿을 때 멈춰요. 14번 줄까지 기다리면 2초에 이미 다 자라 있어서 모양 바뀜이 0.027 로 떨어졌어요. 19판의 Dripstone 은 잘린 암반이 4/255 쯤이라 위아래가 검게 보여 `thirds` 가 68/99/77 이었고, 암반에 제 빛을 주자 92/98/99 가 됐어요. Foxfire 는 땅을 80번 줄에 두었을 때 위 3분의 1이 5% 였고, 64번 줄로 올리고 그루터기를 세우자 16% 가 됐어요. 쌓이는 그림은 2~8초에 쌓이는 자리가 보이게 열어야 해요. Popcorn 은 더미가 높게 시작하면 아래 3분의 1의 바뀐 LED 가 4.4% 였고, 낮게 시작해 자라게 하자 27.9% 가 됐어요. 넓은 면을 물들이는 조명은 반짝임 없이도 바뀐 LED 를 올려요. Dripstone 은 세 조명이 15초에 한 바퀴 번갈아 켜져서 57.6%, 84.9%, 79.2% 예요. 20판에서는 한 번 일어나는 일이 2~8초 창에 드는지가 값을 갈랐어요. High Striker 는 7.3초에 종이 한 번 울려서 위 3분의 1의 바뀐 LED 가 6.5% 에서 95.6% 가 됐고, Kettle 은 7.1초에 스위치가 꺼지는 것이 창에 들어 모양 바뀜이 0.065 에서 0.168 이 됐어요. 위로 오르는 앞선은 모양 바뀜을 적게 내요. Chromatography 는 6초 동안 앞선이 26줄을 올라도 블록이 16줄 높이라 0.040 이었어요. 21판의 세로 일곱은 Nixie 를 빼고 방이나 바탕을 다 그려서 `thirds` 가 3분의 1마다 70 을 넘었어요. 그런 그림은 바탕을 지우거나 멈추고 한 번 더 재야 주인공이 128줄을 쓰는지 보여요. Chemical Garden 은 유리와 방을 검게 지우자 정원만의 위 3분의 1이 6.6% 였고, 씨앗 열여섯 가운데 하나는 3.2% 였어요. Lap Pool 은 바닥 햇빛 무늬의 시간을 멈추자 바뀐 LED 가 3분의 1마다 18.7~52.1% 였어요. Woodpecker 는 새를 빼고 재도 바뀐 LED 가 57.3/35.1/50.0% 라서, 그 값은 거의 다 잎 사이로 움직이는 햇빛 반점에서 와요. 조금 위에서 본 장면은 세로를 싸게 얻어요. Pop-up Book 은 누운 페이지가 깊이의 5분의 1 줄만 써서, 책을 아래 34줄에 두고도 선 카드가 위 3분의 1까지 닿아요. 닉시관 하나는 지름이 높이의 절반쯤이라 Nixie 의 IN-18 은 54 LED 너비에 109줄로 서요.Slinky 는 계단과 벽을 빼고 스프링과 그림자만 세면 켜진 LED 가 0.4/2.6/3.0% 이고, 스프링이 켠 계단등까지 세면 20.9/16.5/8.6% 예요. 22판의 세로 다섯 가운데 넷은 바탕을 다 칠해서 `thirds` 가 말해 주는 것이 적었어요. 바탕을 검게 지운 사본으로 다시 재면 Liftoff 는 로켓만의 위 3분의 1이 켜진 LED 3.7%, 바뀐 LED 8.2% 라서, 그 3분의 1의 변화는 대부분 카메라가 고개를 들며 어두워지는 하늘에서 와요. Bowling Lane 은 공을 던지지 않게 하면 바뀐 LED 가 5.2/9.5/6.6% 로 떨어져서, 그 값은 거의 다 공과 그 빛이에요. Circulation 은 바탕을 21 아래로 내려 몸만 켜진 LED 로 세게 해서 `thirds` 가 34/50/26 이에요. 남은 자리로 짠 Flame Test 는 실험대가 어두워 아래 3분의 1의 바뀐 LED 가 4.6% 였어요. 불꽃빛의 몫을 0.42 에서 0.52 로 올려도 1.3 포인트만 올라서, 어두운 바닥은 빛을 비추는 것보다 2~8초 창 안에 그 자리로 가는 움직임이 있어야 바뀐 LED 가 올라요. 23판의 세로 일곱 가운데 Proscenium 은 금박 아치가 세 3분의 1을 다 지나가서, 아치를 검게 바꿔 다시 재면 `thirds` 가 94/97/69 에서 63/85/65 가 돼요. Final Approach 는 위 3분의 1을 밤하늘이 채워서, 도시 빛을 받은 천정을 33/255 로 두어 문턱 24 를 넘겼어요. 가는 선 하나가 지나가는 3분의 1은 둘레를 같이 밝혀야 해요. Axon 의 가운데 3분의 1은 축삭 한 줄이 지나가는 자리라서, 수초의 빛과 비스듬히 지나가는 이웃 축삭 둘을 두어 바뀐 LED 가 23.6~35.5% 가 됐어요. 한 번 일어나는 일은 2~8초 창에 들게 짰어요. Hanging Scroll 은 첫 그림이 걸려 있는 시간을 2.2초로 줄여 4.8~6.8초에 말려 올라가게 했고, Giraffe 는 첫 물 마시기를 3.4초에 두었어요. Elephant Toothpaste 의 아래 3분의 1은 바뀐 LED 가 9.7% 로 일곱 가운데 가장 낮아요. 기둥이 6.8초에 꺾이기 전까지는 실린더와 실험대가 서 있기만 해서예요. 24판의 세로 일곱 가운데 다섯은 바탕을 다 칠해서 바뀐 LED 로 봤어요. 23판이 물은 어두운 하늘의 떼는 Emergence 가 답했어요. 하늘을 지우고 박쥐만 세면 3분의 1마다 13.4~13.6, 9.9~10.2, 5.1~5.3% 가 켜져서, 기둥이 좁은 아래 3분의 1은 문턱을 겨우 넘어요. 2~8초 동안 바뀐 LED 로 세면 아래 3분의 1도 23.5~25.0% 예요. 자라는 것의 키가 위 3분의 1을 정해요. Potter's Wheel 은 그릇을 64~90줄로 빚을 때 씨앗 넷 가운데 둘이 위 3분의 1의 바뀐 LED 0.6% 와 0.0% 였고, 76~92줄로 키우자 열여섯 모두 15.2% 를 넘었어요. 바탕의 움직임을 멈추고 다시 재면 주인공의 몫이 보여요. Tower Crane 은 구름을 멈춰도 바뀐 LED 가 45.2/54.1/36.3% 이고, Pipe Organ 은 장미창 빛을 꺼도 3분의 1마다 18.2% 를 넘어요. Flying Trapeze 는 `thirds` 가 81/35/34 로 일곱 가운데 가장 고르지 않지만, 팔로 스폿 빛줄기가 연무 속을 쓸어서 바뀐 LED 는 93.9/87.9/57.5% 예요. 25판의 세로 여섯 가운데 넷은 바탕을 다 칠해서 `thirds` 가 3분의 1마다 97 을 넘었고, 바탕을 멈추거나 지운 사본으로 다시 쟀어요. Eruption 은 하늘의 불빛과 부푸는 지층을 멈춰도 바뀐 LED 가 56.4~67.4/39.1~55.5/15.7~17.7% 이고, 아래 3분의 1의 값은 마그마 방의 대류와 화도를 오르는 가스 덩어리에서 와요. 빨강이 255 에 붙은 불빛은 가장 밝은 채널로 재면 결이 보이지 않아요. 플리니식에서 마그마 방이 밝기 끝에 붙자 아래 3분의 1의 바뀐 LED 가 5.3% 였고, 방의 온도가 기둥의 맥박을 따라 0.16 오르내리게 하자 17.7% 가 됐어요. Excavation 은 구름을 멈추면 24.3/12.5/15.3%, Skydiver 는 구름과 그림자를 멈추면 26.7/66.2/23.7% 예요. Rice Terraces 는 하늘 36줄을 빼도 위 3분의 1의 바뀐 LED 가 12.5% 라서, 맨 위 논들의 구름 반사가 그 3분의 1을 움직여요. 가는 선도 쓸고 지나가면 바뀐 LED 를 많이 켜요. Marionette 는 무대를 검게 지우면 인형과 실, 조종대가 켜는 LED 가 20.5/18.1/10.4% 인데 바뀐 LED 는 67.3/76.8/64.2% 예요. 조종대가 기울 때마다 실 일곱이 옆으로 쓸려서, 15판 Incense 의 곧은 연기 실이 3.7% 였던 것과 반대예요. 물에 비친 그림은 아래 3분의 1을 싸게 채워요. Ferris Wheel 은 위 장면을 0.72 로 눌러 41줄 호수에 비춰서 아래 3분의 1의 바뀐 LED 가 80~81% 예요. 이야기가 있는 데이터 그림은 여는 해가 세로를 정해요. Population Pyramid 는 1820년에서 열 때 막대가 0~78살에만 있어서, `thirds` 13/20/40 의 위 3분의 1이 연도와 라벨뿐이었어요. 라벨과 눈금을 지운 사본의 위 3분의 1은 1975년에서 열면 5.6%, 1985년 6.2%, 1995년 7.3%, 2000년 7.8% 여서 1995년에서 열어요. +2~8초 동안 켜진 LED 가 5% 아래인 3분의 1이 있는 패턴이 넷이에요. `study.ts` 표의 `thirds` 칸이 이 값이에요. Jelly 4.5% 와 Pulse Bars 2.6% 는 위 3분의 1이 비고, Sparks 2.0% 와 Dragon Fold 4.9% 는 아래 3분의 1이 비어요. Dragon Fold 는 문턱 바로 아래예요. 6판의 Jacob's Ladder 는 위, 가운데, 아래가 27.0%, 21.4%, 35.1% 예요. 햇빛층에서 심해까지 내려가는 바다는 16판의 Abyss 가 했고, 덩굴은 15판의 Ivy 가 했어요. 어두운 판 위의 가는 빛 하나는 128줄을 다 지나가도 3분의 1마다 10% 를 넘기 어려워요. 6판의 처음 아크는 가운데 3분의 1을 7.4% 켰어요. 9판의 Sprout 은 접시꽃 둘이 위 3분의 1을 8.1% 켜요. 이렇게 철이 도는 패턴은 2~8초가 어느 철에 걸리느냐에 따라 값이 몇 배씩 달라서, 씨앗 여럿으로 재 봐야 해요. 테두리를 그리는 패턴은 테두리를 빼고 재요. 10판의 Tetromino Well 은 양쪽 벽 2 LED 만으로 3분의 1마다 6.3% 를 켜서, 벽을 넣어 재면 위 3분의 1이 늘 문턱을 넘어요. 바탕을 다 그리는 패턴은 `thirds` 칸이 3분의 1마다 90 을 넘어요. 14판의 Waterfall 은 98/93/97 이에요. 그런 패턴은 2~8초 동안 밝기가 24/255 넘게 바뀌는 LED 로 세어야 그림이 128줄을 쓰는지 보여요. 그렇게 세면 Waterfall 의 위 3분의 1은 25.9% 예요. 15판의 Ivy 는 벽을 그려 `thirds` 가 84/89/82 이고, 바뀐 LED 로 세면 48.7%, 70.6%, 29.2% 예요. 가만히 있는 가는 선은 켜져 있어도 바뀌지 않아요. 15판의 Incense 는 곧은 연기 실이 가운데 3분의 1을 지나도 바뀐 LED 가 3.7% 였고, 소용돌이가 그 3분의 1 안에서 시작되게 하자 15.5% 가 됐어요. 16판의 Abyss 는 물을 다 칠해서 `thirds` 가 100/80/22 이고, 바뀐 LED 로 세면 84.5%, 59.3%, 36.0% 예요. 한밤층은 아귀와 불꽃, 잠수정 등빛뿐이라 아래 3분의 1이 가장 낮아요. 16판의 Harp 는 액자를 빼고 줄만 재면 48/53/12 이고, 바뀐 LED 는 아래 3분의 1이 9.8% 예요. 거기는 받침과 공명통 발이고, 줄은 300 LED 남짓한 삼각형에만 있어요. 17판의 Marble Run 은 레일과 리프트가 서 있어서 `thirds` 가 39/30/68 이고, 바뀐 LED 는 40.7%, 54.3%, 59.7% 예요. 17판의 Ant Farm 은 모래가 바탕이라 `thirds` 가 100/97/96 이고, 바뀐 LED 는 6.9%, 34.5%, 53.4% 예요. 위 3분의 1은 모래 윗면과 그 위의 어두운 유리라서 개미가 드나드는 입구와 더미만 바뀌어요. 18판의 Server Rack 은 앞판을 13~23 으로 24 아래에 두어서, `thirds` 63/63/61 이 다 드라이브와 코드와 LED 의 빛이에요. 바뀐 LED 셈은 2~8초 가운데 한 프레임만 24 를 넘어도 그 LED 를 세서, 무작위 반짝임이 값을 부풀려요. 18판의 Pint 는 잔 기포 반짝임을 켜면 50.3%, 60.2%, 40.0% 이고 끄면 50.2%, 46.8%, 26.3% 예요. 빛의 흔들림도 그래요. Longcase Clock 은 촛불일 때 41.8%, 45.6%, 45.3% 이고 흔들림 없는 달빛에서는 2.6%, 4.2%, 5.9% 예요. 자라는 그림은 미리 돌리기를 시간 대신 상태로 멈추면 씨앗마다 같은 자리에서 열려요. 18판의 Bismuth 는 무리 꼭대기가 20번 줄에 닿을 때 멈춰요. 14번 줄까지 기다리면 2초에 이미 다 자라 있어서 모양 바뀜이 0.027 로 떨어졌어요. 19판의 Dripstone 은 잘린 암반이 4/255 쯤이라 위아래가 검게 보여 `thirds` 가 68/99/77 이었고, 암반에 제 빛을 주자 92/98/99 가 됐어요. Foxfire 는 땅을 80번 줄에 두었을 때 위 3분의 1이 5% 였고, 64번 줄로 올리고 그루터기를 세우자 16% 가 됐어요. 쌓이는 그림은 2~8초에 쌓이는 자리가 보이게 열어야 해요. Popcorn 은 더미가 높게 시작하면 아래 3분의 1의 바뀐 LED 가 4.4% 였고, 낮게 시작해 자라게 하자 27.9% 가 됐어요. 넓은 면을 물들이는 조명은 반짝임 없이도 바뀐 LED 를 올려요. Dripstone 은 세 조명이 15초에 한 바퀴 번갈아 켜져서 57.6%, 84.9%, 79.2% 예요. 20판에서는 한 번 일어나는 일이 2~8초 창에 드는지가 값을 갈랐어요. High Striker 는 7.3초에 종이 한 번 울려서 위 3분의 1의 바뀐 LED 가 6.5% 에서 95.6% 가 됐고, Kettle 은 7.1초에 스위치가 꺼지는 것이 창에 들어 모양 바뀜이 0.065 에서 0.168 이 됐어요. 위로 오르는 앞선은 모양 바뀜을 적게 내요. Chromatography 는 6초 동안 앞선이 26줄을 올라도 블록이 16줄 높이라 0.040 이었어요. 21판의 세로 일곱은 Nixie 를 빼고 방이나 바탕을 다 그려서 `thirds` 가 3분의 1마다 70 을 넘었어요. 그런 그림은 바탕을 지우거나 멈추고 한 번 더 재야 주인공이 128줄을 쓰는지 보여요. Chemical Garden 은 유리와 방을 검게 지우자 정원만의 위 3분의 1이 6.6% 였고, 씨앗 열여섯 가운데 하나는 3.2% 였어요. Lap Pool 은 바닥 햇빛 무늬의 시간을 멈추자 바뀐 LED 가 3분의 1마다 18.7~52.1% 였어요. Woodpecker 는 새를 빼고 재도 바뀐 LED 가 57.3/35.1/50.0% 라서, 그 값은 거의 다 잎 사이로 움직이는 햇빛 반점에서 와요. 조금 위에서 본 장면은 세로를 싸게 얻어요. Pop-up Book 은 누운 페이지가 깊이의 5분의 1 줄만 써서, 책을 아래 34줄에 두고도 선 카드가 위 3분의 1까지 닿아요. 닉시관 하나는 지름이 높이의 절반쯤이라 Nixie 의 IN-18 은 54 LED 너비에 109줄로 서요.Slinky 는 계단과 벽을 빼고 스프링과 그림자만 세면 켜진 LED 가 0.4/2.6/3.0% 이고, 스프링이 켠 계단등까지 세면 20.9/16.5/8.6% 예요. 22판의 세로 다섯 가운데 넷은 바탕을 다 칠해서 `thirds` 가 말해 주는 것이 적었어요. 바탕을 검게 지운 사본으로 다시 재면 Liftoff 는 로켓만의 위 3분의 1이 켜진 LED 3.7%, 바뀐 LED 8.2% 라서, 그 3분의 1의 변화는 대부분 카메라가 고개를 들며 어두워지는 하늘에서 와요. Bowling Lane 은 공을 던지지 않게 하면 바뀐 LED 가 5.2/9.5/6.6% 로 떨어져서, 그 값은 거의 다 공과 그 빛이에요. Circulation 은 바탕을 21 아래로 내려 몸만 켜진 LED 로 세게 해서 `thirds` 가 34/50/26 이에요. 남은 자리로 짠 Flame Test 는 실험대가 어두워 아래 3분의 1의 바뀐 LED 가 4.6% 였어요. 불꽃빛의 몫을 0.42 에서 0.52 로 올려도 1.3 포인트만 올라서, 어두운 바닥은 빛을 비추는 것보다 2~8초 창 안에 그 자리로 가는 움직임이 있어야 바뀐 LED 가 올라요. 23판의 세로 일곱 가운데 Proscenium 은 금박 아치가 세 3분의 1을 다 지나가서, 아치를 검게 바꿔 다시 재면 `thirds` 가 94/97/69 에서 63/85/65 가 돼요. Final Approach 는 위 3분의 1을 밤하늘이 채워서, 도시 빛을 받은 천정을 33/255 로 두어 문턱 24 를 넘겼어요. 가는 선 하나가 지나가는 3분의 1은 둘레를 같이 밝혀야 해요. Axon 의 가운데 3분의 1은 축삭 한 줄이 지나가는 자리라서, 수초의 빛과 비스듬히 지나가는 이웃 축삭 둘을 두어 바뀐 LED 가 23.6~35.5% 가 됐어요. 한 번 일어나는 일은 2~8초 창에 들게 짰어요. Hanging Scroll 은 첫 그림이 걸려 있는 시간을 2.2초로 줄여 4.8~6.8초에 말려 올라가게 했고, Giraffe 는 첫 물 마시기를 3.4초에 두었어요. Elephant Toothpaste 의 아래 3분의 1은 바뀐 LED 가 9.7% 로 일곱 가운데 가장 낮아요. 기둥이 6.8초에 꺾이기 전까지는 실린더와 실험대가 서 있기만 해서예요. 24판의 세로 일곱 가운데 다섯은 바탕을 다 칠해서 바뀐 LED 로 봤어요. 23판이 물은 어두운 하늘의 떼는 Emergence 가 답했어요. 하늘을 지우고 박쥐만 세면 3분의 1마다 13.4~13.6, 9.9~10.2, 5.1~5.3% 가 켜져서, 기둥이 좁은 아래 3분의 1은 문턱을 겨우 넘어요. 2~8초 동안 바뀐 LED 로 세면 아래 3분의 1도 23.5~25.0% 예요. 자라는 것의 키가 위 3분의 1을 정해요. Potter's Wheel 은 그릇을 64~90줄로 빚을 때 씨앗 넷 가운데 둘이 위 3분의 1의 바뀐 LED 0.6% 와 0.0% 였고, 76~92줄로 키우자 열여섯 모두 15.2% 를 넘었어요. 바탕의 움직임을 멈추고 다시 재면 주인공의 몫이 보여요. Tower Crane 은 구름을 멈춰도 바뀐 LED 가 45.2/54.1/36.3% 이고, Pipe Organ 은 장미창 빛을 꺼도 3분의 1마다 18.2% 를 넘어요. Flying Trapeze 는 `thirds` 가 81/35/34 로 일곱 가운데 가장 고르지 않지만, 팔로 스폿 빛줄기가 연무 속을 쓸어서 바뀐 LED 는 93.9/87.9/57.5% 예요. 25판의 세로 여섯 가운데 넷은 바탕을 다 칠해서 `thirds` 가 3분의 1마다 97 을 넘었고, 바탕을 멈추거나 지운 사본으로 다시 쟀어요. Eruption 은 하늘의 불빛과 부푸는 지층을 멈춰도 바뀐 LED 가 56.4~67.4/39.1~55.5/15.7~17.7% 이고, 아래 3분의 1의 값은 마그마 방의 대류와 화도를 오르는 가스 덩어리에서 와요. 빨강이 255 에 붙은 불빛은 가장 밝은 채널로 재면 결이 보이지 않아요. 플리니식에서 마그마 방이 밝기 끝에 붙자 아래 3분의 1의 바뀐 LED 가 5.3% 였고, 방의 온도가 기둥의 맥박을 따라 0.16 오르내리게 하자 17.7% 가 됐어요. Excavation 은 구름을 멈추면 24.3/12.5/15.3%, Skydiver 는 구름과 그림자를 멈추면 26.7/66.2/23.7% 예요. Rice Terraces 는 하늘 36줄을 빼도 위 3분의 1의 바뀐 LED 가 12.5% 라서, 맨 위 논들의 구름 반사가 그 3분의 1을 움직여요. 가는 선도 쓸고 지나가면 바뀐 LED 를 많이 켜요. Marionette 는 무대를 검게 지우면 인형과 실, 조종대가 켜는 LED 가 20.5/18.1/10.4% 인데 바뀐 LED 는 67.3/76.8/64.2% 예요. 조종대가 기울 때마다 실 일곱이 옆으로 쓸려서, 15판 Incense 의 곧은 연기 실이 3.7% 였던 것과 반대예요. 물에 비친 그림은 아래 3분의 1을 싸게 채워요. Ferris Wheel 은 위 장면을 0.72 로 눌러 41줄 호수에 비춰서 아래 3분의 1의 바뀐 LED 가 80~81% 예요. 이야기가 있는 데이터 그림은 여는 해가 세로를 정해요. Population Pyramid 는 1820년에서 열 때 막대가 0~78살에만 있어서, `thirds` 13/20/40 의 위 3분의 1이 연도와 라벨뿐이었어요. 라벨과 눈금을 지운 사본의 위 3분의 1은 1975년에서 열면 5.6%, 1985년 6.2%, 1995년 7.3%, 2000년 7.8% 여서 1995년에서 열어요. 26판의 세로 여섯 가운데 다섯은 바탕을 다 칠해서 `thirds` 가 3분의 1마다 79 이상이었고, 바탕을 지운 사본으로 다시 쟀어요. 단면을 물높이나 땅에서 자르면 주인공이 세 3분의 1에 다 걸쳐요. Iceberg 는 물높이를 30번 줄에 두어 빙산의 9분의 8이 물 아래에 서서, 하늘과 물을 지워도 빙산만으로 켜진 LED 가 30/67/30% 예요. Geyser 는 땅속 화도와 물방이 아래 3분의 1을 맡아서, 하늘과 땅을 지운 사본의 아래 3분의 1이 13.6% 켜지고 14.2~14.7% 바뀌어요. 가마 불꽃은 Eruption 의 마그마 방과 같은 문턱에 걸렸어요. Noborigama 의 흰 열 칸은 빨강이 255 에 붙고 일렁임이 온도로만 갈 때 2~8초 동안 바뀐 LED 가 0.8% 와 4.2% 였어요. 빨강을 232 에서 멈추고 밝기를 0.72~1 배로 흔들자 96.9% 와 97.0% 가 됐어요. 가장 밝은 채널이 끝에 붙은 불빛은 색만 흔들어서는 바뀐 LED 로 잡히지 않아요. 위에서 내려다본 판은 3분의 1마다 맡을 것을 따로 두어야 해요. Rice Planting 은 심은 모가 위, 일꾼 줄이 가운데, 모판과 못자리가 아래를 맡아서 물을 지운 사본의 켜진 LED 가 15/16~18/40% 예요. 못자리까지 지우면 아래 3분의 1이 3.9~5.0% 라서, 논둑 길에 모판 둘을 놓아 5.7~6.7% 로 올렸어요. 한 번 일어나는 일은 이번에도 2~8초 창에 두었어요. Implosion 은 2.4초에 발파해 7초쯤 먼지가 꼭대기에 닿고, Pole Vault 는 1.97초에 달려 나가 5.14초에 가로대 위 정점을 찍어요. 가는 선이 쓸고 지나가는 그림은 켜진 LED 가 적어도 바뀐 LED 가 많아요. Pole Vault 는 경기장을 지우면 선수와 장대가 위 3분의 1을 1.4~1.7% 만 켜지만, 2~8초 동안 바뀐 LED 는 22.8~23.2% 이고 장대가 휘며 쓰는 가운데 3분의 1은 92.8% 예요. ## 다시 돌리기 From 019a2dae02c1f08164d496c21b6457d533cc8cae Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 07:11:56 +0000 Subject: [PATCH 29/31] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2026=ED=8C=90:=20index.html=20=EC=9D=84=20168=EA=B0=9C?= =?UTF-8?q?=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20=EC=A7=93=EA=B3=A0=20README=20?= =?UTF-8?q?=EB=A5=BC=20=EB=A7=88=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BvYAVbR9mNHMMAp7tN8evZ --- incoming/pattern-study/README.md | 4 ++-- incoming/pattern-study/index.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 8120bbdad..206a9d967 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1094,7 +1094,7 @@ K4 Spate 0 이면 물이 실 몇 가닥으로 갈라져요. 처음 값 0.06 이 ## 26판 -26판도 새 패턴 열 개를 열 갈래에 하나씩 냈어요. 주제 1 은 이번에도 겨눌 칸이 없었어요. 기준 158개의 `--gaps` 가 21장 모두 `none` 이었어요. 그 대신 패턴이 하나뿐이던 칸 넷을 Dark Field 와 Lichtenberg, Origami 로 겨눠 거기 둘째로 세웠어요. 주제 2 로 얼음·극지 갈래를 Iceberg 로, 미생물 갈래를 Dark Field 로 열었어요. 주제 4 는 Iceberg 와 Noborigama, Rice Planting, Implosion, Geyser, Pole Vault 로 여섯이에요. 여섯 다 3분의 1마다 켜진 LED 가 5% 를 넘고, 바탕을 지운 사본으로 다시 재도 주인공만으로 3분의 1마다 켜진 LED 나 2~8초 동안 바뀐 LED 가 5% 를 넘어요. Lichtenberg 와 Origami, Shadow Play 는 주제 없이 남은 자리로 짰어요. 농사·밭과 화산은 패턴이 하나뿐이던 갈래라 이번에 둘째가 섰고, 공사·건설과 도자기에는 셋째가, 무대·공연과 프랙탈, 종이, 운동·경기에는 넷째가 섰어요. 주제 3 은 앞 바퀴 PR 에 창작자 글이 없어서 하지 않았어요. <> +26판도 새 패턴 열 개를 열 갈래에 하나씩 냈어요. 주제 1 은 이번에도 겨눌 칸이 없었어요. 기준 158개의 `--gaps` 가 21장 모두 `none` 이었어요. 그 대신 패턴이 하나뿐이던 칸 넷을 Dark Field 와 Lichtenberg, Origami 로 겨눠 거기 둘째로 세웠어요. 주제 2 로 얼음·극지 갈래를 Iceberg 로, 미생물 갈래를 Dark Field 로 열었어요. 주제 4 는 Iceberg 와 Noborigama, Rice Planting, Implosion, Geyser, Pole Vault 로 여섯이에요. 여섯 다 3분의 1마다 켜진 LED 가 5% 를 넘고, 바탕을 지운 사본으로 다시 재도 주인공만으로 3분의 1마다 켜진 LED 나 2~8초 동안 바뀐 LED 가 5% 를 넘어요. Lichtenberg 와 Origami, Shadow Play 는 주제 없이 남은 자리로 짰어요. 농사·밭과 화산은 패턴이 하나뿐이던 갈래라 이번에 둘째가 섰고, 공사·건설과 도자기에는 셋째가, 무대·공연과 프랙탈, 종이, 운동·경기에는 넷째가 섰어요. 주제 3 은 앞 바퀴 PR 에 창작자 글이 없어서 하지 않았어요. 열 개를 다 넣고 재도 일곱 축의 양 끝은 그대로라서 칸 경계는 14판과 같아요. 혼자 서는 칸을 새로 만든 패턴도 없어서, 패턴이 하나뿐인 칸은 43 에서 39 가 됐어요. ### Iceberg @@ -1184,7 +1184,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 `study.ts --check --gaps` 가 `*` 를 붙인 칸 가운데 그림이 떠오르는 칸이에요. 칸은 가로와 세로를 낮은 쪽부터 1~4 로 세요. 페이지 기본 지도(가장자리 × 움직임)는 16칸이 다 차 있어요. -23판 뒤로는 `*` 가 붙은 칸이 없어요. 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.254 로 Domino Run 혼자이던 채움 × 모양 바뀜 1,3 에 서요. Lichtenberg 는 Rabbit Ears 혼자이던 복잡도 × 모양 바뀜 4,3 에 씨앗 여덟 가운데 일곱이 들어요. 168개에서 패턴이 하나뿐인 칸은 <>칸이에요. +23판 뒤로는 `*` 가 붙은 칸이 없어요. 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칸이에요. 촘촘함이 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 를 냈어요. diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index 97d0476a3..040fd0572 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -141,7 +141,7 @@

패턴 연구

- + +