From 0f4bf13d05edc76df118b6dfef7079bffb4d4c93 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 00:55:16 +0000 Subject: [PATCH 01/33] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=2024=ED=8C=90=20(pfpat?= =?UTF-8?q?=2022=EB=B0=94=ED=80=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KTNzLtP3gH9rQENoucsegc From 22d31d732c6680cbda3f12be2400704336b09375 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:01:09 +0000 Subject: [PATCH 02/33] =?UTF-8?q?=EA=B3=B5=EC=82=AC=C2=B7=EA=B1=B4?= =?UTF-8?q?=EC=84=A4=EA=B3=BC=20=EB=8F=84=EC=9E=90=EA=B8=B0=20=EA=B0=88?= =?UTF-8?q?=EB=9E=98=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_01KTNzLtP3gH9rQENoucsegc --- 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 f460d5a7d..9c78ac3d3 100644 --- a/incoming/pattern-study/families.json +++ b/incoming/pattern-study/families.json @@ -42,5 +42,7 @@ "body": { "label": "몸", "color": "#bb5566" }, "flight": { "label": "비행", "color": "#9944ee" }, "stage": { "label": "무대·공연", "color": "#ff0066" }, - "mammal": { "label": "짐승", "color": "#998844" } + "mammal": { "label": "짐승", "color": "#998844" }, + "construction": { "label": "공사·건설", "color": "#ffee00" }, + "pottery": { "label": "도자기", "color": "#b05030" } } From ff2dfb3c699c99301b07d10bdfc656c08f4e5e98 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:17:07 +0000 Subject: [PATCH 03/33] emergence: first running version (wip) --- incoming/pattern-study/patterns/emergence.js | 342 +++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 incoming/pattern-study/patterns/emergence.js diff --git a/incoming/pattern-study/patterns/emergence.js b/incoming/pattern-study/patterns/emergence.js new file mode 100644 index 000000000..9ff374bb1 --- /dev/null +++ b/incoming/pattern-study/patterns/emergence.js @@ -0,0 +1,342 @@ +// ===== Patternflow pattern ===== +// Title: Emergence +// Family: mammal +// Idea: At dusk over dry hill country a dark ribbon of free-tailed bats pours out of a cave mouth and corkscrews up the whole sky, splitting into two strands and joining again before it streams off sideways high up, and when a hawk dives into it the bats ripple away and leave a hole that closes behind it. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 24, after the evening emergence of Mexican free-tailed bats (Tadarida brasiliensis) from Bracken Cave, Texas +// Round: 24 +// =============================== +// @matrix 64x128 +// @knobs Dusk=0..1, Speed=0.2..3, Bats=300..1200, Wind=-1..1 +// +// Knob 1: Dusk. ... +// Knob 2: Speed. ... +// Knob 3: Bats. ... +// Knob 4: Wind. ... +// Buttons: K1 tap sends a hawk diving into the column. + +const [W, H] = [64, 128]; +const N = W * H; +const { min, max, floor, round, sin, cos, 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 hyp = (a, b) => sqrt(a * a + b * b); +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; +} + +// The cave mouth: its middle column, floor row, half width and height. +const [CX, CB, CW, CH] = [24, 121, 9, 9.5]; +// The row where the bats come out, the most bats the knob asks for, the +// spine's length in rows, and a bat's climb at Speed 2 in LEDs a second. +const [CY, MAXB, HM, V0] = [115, 1200, 118, 28]; +// The column's front at the start, and the length of one trip. +const [F0, LT] = [52, 150]; + +// The sky from the top (0) to the horizon (1): afterglow, late dusk, night. +const SKIES = [ + [[0, 30, 26, 98], [0.2, 66, 46, 138], [0.42, 160, 70, 134], [0.62, 236, 104, 100], [0.8, 255, 146, 72], [1, 255, 196, 112]], + [[0, 12, 14, 62], [0.3, 30, 30, 100], [0.6, 94, 52, 116], [0.84, 190, 84, 78], [1, 228, 126, 66]], + [[0, 5, 9, 32], [0.45, 10, 18, 56], [0.8, 22, 30, 74], [1, 44, 42, 84]], +]; +// Rock, far hills, scrub and the lit undersides of the clouds at each hour. +const LAND = [ + [92, 64, 54, 116, 72, 96, 34, 36, 24, 255, 178, 150], + [56, 40, 46, 64, 44, 80, 24, 26, 22, 140, 66, 96], + [28, 28, 40, 30, 30, 60, 14, 18, 22, 18, 20, 38], +]; +// A bat against the light, and the hawk. +const BAT = [14, 10, 12]; + +function ramp(S, g, c) { + let j = 0; + while (j < S.length - 2 && g > S[j + 1][0]) j++; + const a = S[j], b = S[j + 1]; + return mix(a[c + 1], b[c + 1], cl((g - a[0]) / (b[0] - a[0]), 0, 1)); +} + +export function setup(params) { + const F = (n) => new Float32Array(n); + const P = (params.s = { + t: 0, init: false, bg: F(N * 3), f: F(N * 3), D: F(N), key: -1, co: 0, hk: null, ring: null, nh: 3.4, + S: F(MAXB), V: F(MAXB), A: F(MAXB), B: F(MAXB), TU: F(MAXB), RH: F(MAXB), DR: F(MAXB), JF: F(MAXB), JP: F(MAXB), + FR: F(MAXB), FP: F(MAXB), OX: F(MAXB), OY: F(MAXB), VX: F(MAXB), VY: F(MAXB), BX: F(MAXB), BY: F(MAXB), Z: F(MAXB), + SX: F(HM), SR: F(HM), SP: F(HM), SS: F(HM), RID: F(W), FAR: F(W), CT: F(128 * 72), + }); + // The near hill climbs over the cave; a flat mesa stands far off on the right. + for (let x = 0; x < W; x++) { + P.RID[x] = 99 + 0.07 * x - 10 * exp(-(((x - 21) / 15) ** 2)) + 2 * vn(x * 0.13, 3) + 1.2 * vn(x * 0.4, 9) - 1.6; + P.FAR[x] = 90 + 2.5 * vn(x * 0.09, 1) + 1.5 * vn(x * 0.3, 2) - 7 * sm(38, 43, x) * (1 - sm(57, 61, x)) - 2 * exp(-(((x - 8) / 9) ** 2)); + } + // Scrub: juniper and mesquite clumps on the slope and along the skyline. + P.bush = []; + for (let n = 0; n < 34; n++) { + const x = rnd() * W, top = n < 10, r = top ? 1 + rnd() * 1.2 : 0.8 + rnd() * 1.3; + const y = top ? P.RID[floor(x)] - r * 0.4 : P.RID[floor(x)] + 2 + rnd() * (H - P.RID[floor(x)] - 2); + if (((x - CX) / (CW + 3)) ** 2 + ((CB - y) / (CH + 3)) ** 2 > 1 || y > CB + 2) P.bush.push([x, y, r]); + } + P.stars = Array.from({ length: 80 }, () => [floor(rnd() * W), floor(rnd() * 86), 0.4 + rnd() * 0.6, rnd() * 7, 1 + rnd() * 3]); + // Thin streaks of high cloud, wrapping every 128 columns. + for (let y = 0; y < 72; y++) + for (let x = 0; x < 128; x++) { + const f = (X) => 0.6 * vn(X * 0.035, y * 0.3) + 0.4 * vn(X * 0.11, y * 0.5 + 7); + const band = sm(0, 10, y) * (1 - sm(56, 72, y)) * (0.5 + 0.5 * sin(y * 0.21 + 1)); + P.CT[y * 128 + x] = sm(0.5, 0.75, mix(f(x), f(x + 128), x / 128)) * band; + } +} + +// Where a bat turns off the column, how wide it swings and which way. +function route(P, i) { + const top = rnd() < 0.16; + P.TU[i] = top ? 98 + 14 * rnd() : P.tu + 13 * rnd(); + P.RH[i] = top ? 30 : 15 + 7 * rnd(); + P.DR[i] = rnd() < cl(0.72 + 0.6 * P.w, 0.04, 0.96) ? 1 : -1; +} + +function place(P, M) { + for (let i = 0; i < MAXB; i++) { + const a = rnd() * 2 - 1; + P.A[i] = (a < 0 ? -1 : 1) * abs(a) ** 0.8; + P.B[i] = (rnd() - 0.5) * 1.2; + P.V[i] = V0 * (0.8 + 0.4 * rnd()); + P.JF[i] = 1.4 + rnd() * 2.4; + P.JP[i] = rnd() * 7; + P.FR[i] = 4 + rnd() * 2.6; + P.FP[i] = rnd(); + route(P, i); + P.S[i] = i < M ? F0 - rnd() * LT : -1 - rnd() * LT; + } + P.init = true; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + const d = cl(dt || 0, 0, 0.1) * (cl(k[1], 0.2, 3) / 2), M = round(cl(k[2], 300, 1200)); + P.k1 = cl(k[0], 0, 1); + P.w = cl(k[3], -1, 1); + const T = (P.t += d); + // The height where the stream peels off wanders slowly. + P.tu = 80 + 8 * sin(T * 0.23 + 1); + if (!P.init) place(P, M); + const w = P.w, { SX, SR, SP, SS } = P; + P.co += d * (1.2 + 4 * w); + + // The spine of the column: it leans downwind, corkscrews and breathes, and + // now and then tears in two along its middle and closes again higher up. + const Rk = 5 + (3.5 * (M - 300)) / 900, ph = (T - 2.4) % 12, split = T < 2.4 ? 0 : sm(0, 1.4, ph) * (1 - sm(4.6, 6.2, ph)); + for (let h = 0; h < HM; h++) { + const A = 1 + 7.5 * sm(4, 92, h); + SX[h] = CX + w * (0.13 * h + 0.0011 * h * h) + A * (sin(0.052 * h - 1.3 * T) + 0.4 * sin(0.12 * h + 0.75 * T + 2)); + SR[h] = Rk * (0.32 + 0.68 * sm(0, 26, h)) * (1 + 0.005 * h) * (1 + 0.3 * sin(0.075 * h - 2.1 * T)); + SP[h] = 0.055 * h - 1.7 * T; + SS[h] = h > 18 && h < 96 ? split * 9 * sin((PI * (h - 18)) / 78) : 0; + } + + // The hawk: a stoop through the column on a curve, out the other side. + if (input.btnPressed && input.btnPressed[0] && !P.hk) P.nh = 0; + if (!P.hk && (P.nh -= d) <= 0) { + const s = rnd() < 0.5 ? 1 : -1, ht = floor(46 + rnd() * 26), tx = SX[ht], ty = CY - ht, x0 = 32 + s * 46, y0 = -12, x2 = 32 - s * 52, y2 = ty - 34; + P.hk = { t: 0, x0, y0, x2, y2, cx: 2 * tx - (x0 + x2) / 2, cy: 2 * ty - (y0 + y2) / 2, hit: false }; + } + const hk = P.hk; + if (hk) { + const u = (hk.t += d) / 2.3, v = 1 - u; + if (u >= 1) (P.hk = null), (P.nh = 12 + rnd() * 8); + else { + hk.x = v * v * hk.x0 + 2 * u * v * hk.cx + u * u * hk.x2; + hk.y = v * v * hk.y0 + 2 * u * v * hk.cy + u * u * hk.y2; + const dx = v * (hk.cx - hk.x0) + u * (hk.x2 - hk.cx), dy = v * (hk.cy - hk.y0) + u * (hk.y2 - hk.cy), l = hyp(dx, dy) || 1; + (hk.ux = dx / l), (hk.uy = dy / l), (hk.u = u); + if (!hk.hit && u >= 0.5) (hk.hit = true), (P.ring = { x: hk.x, y: hk.y, t: 0 }); + } + } + const rg = P.ring; + if (rg && (rg.t += d) > 1.4) P.ring = null; + const hx = P.hk ? P.hk.x : -999, hy = P.hk ? P.hk.y : -999, rr = rg ? 4 + 26 * rg.t : 0, ra = rg ? 1 - rg.t / 1.4 : 0; + + const { S, V, A, B, TU, RH, DR, JF, JP, OX, OY, VX, VY, BX, BY, Z } = P; + for (let i = 0; i < MAXB; i++) { + if (S[i] < 0 && i >= M) continue; + const s = (S[i] += V[i] * d); + if (s < 0) continue; + const hh = min(s, TU[i]), j = min(HM - 2, floor(hh)), f = hh - j; + const r = mix(SR[j], SR[j + 1], f), p = mix(SP[j], SP[j + 1], f), ca = cos(p), sa = sin(p), a = A[i], b = B[i]; + let x = mix(SX[j], SX[j + 1], f) + r * (a * ca - b * sa) + (a < 0 ? -1 : 1) * mix(SS[j], SS[j + 1], f) + 1.1 * sin(T * JF[i] + JP[i]); + let y = CY - hh + 0.8 * sin(T * JF[i] * 1.3 + JP[i] * 2); + Z[i] = a * sa + b * ca; + if (s > TU[i]) { + // Off the top of the column in a curve, then straight out sideways. + const u = s - TU[i], rho = RH[i], al = min(u / rho, 1.25), st = max(0, u - rho * 1.25); + x += DR[i] * (rho * (1 - cos(al)) + st * 0.949); + y -= rho * sin(al) + st * 0.315; + } + // Away from the hawk and the ring it sends out, then back into the stream. + let ax = -18 * OX[i] - 6 * VX[i], ay = -18 * OY[i] - 6 * VY[i], dx = x + OX[i] - hx, dy = y + OY[i] - hy, q = dx * dx + dy * dy; + if (q < 196) { + const e = sqrt(q) + 0.3, g = (1700 * (1 - e / 14) ** 2) / e; + (ax += g * dx), (ay += g * dy); + } + if (rg) { + (dx = x + OX[i] - rg.x), (dy = y + OY[i] - rg.y); + const e = hyp(dx, dy) + 0.5, wv = e - rr; + if (wv * wv < 30) { + const g = (480 * ra * exp((-wv * wv) / 6)) / e; + (ax += g * dx), (ay += g * dy); + } + } + VX[i] += ax * d; + VY[i] += ay * d; + OX[i] = cl(OX[i] + VX[i] * d, -16, 16); + OY[i] = cl(OY[i] + VY[i] * d, -16, 16); + x += OX[i]; + y += OY[i]; + if (x < -3 || x > W + 2 || y < -3) { + // Out of sight: back to the cave mouth for another trip. + S[i] = i < M ? -rnd() * 4 : -1 - rnd() * LT; + OX[i] = OY[i] = VX[i] = VY[i] = 0; + route(P, i); + continue; + } + BX[i] = x; + BY[i] = y; + } +} + +// The sky, the moon, the hills and the cave, kept until the hour moves. +function backdrop(P, k) { + const B = P.bg, i0 = k < 0.5 ? 0 : 1, f = k < 0.5 ? k * 2 : k * 2 - 1; + const L = LAND[i0].map((v, c) => mix(v, LAND[i0 + 1][c], f)); + const glow = 1 - sm(0.2, 0.9, k), mx = 41, my = 134 - 86 * sm(0.12, 1, k), mr = 8.5, mc = [mix(250, 246, sm(100, 60, my)), mix(184, 240, sm(100, 60, my)), mix(124, 220, sm(100, 60, my))]; + P.cc = L.slice(9, 12); + for (let y = 0; y < H; y++) { + const g = y / 100, sky = [0, 1, 2].map((c) => mix(ramp(SKIES[i0], g, c), ramp(SKIES[i0 + 1], g, c), f)); + for (let x = 0; x < W; x++) { + const o = (y * W + x) * 3, rid = P.RID[x]; + let col; + if (y < rid) { + // Brighter where the sun went down, behind the far mesa. + const wg = 1 + 0.2 * glow * exp(-(((x - 50) / 20) ** 2)) * sm(0.3, 1, g); + col = sky.map((v) => v * wg); + const md = hyp(x + 0.5 - mx, y + 0.5 - my); + if (md < mr) { + const ma = 0.82 + 0.18 * sm(0.35, 0.65, vn(x * 0.4 + 3, y * 0.4)) - 0.12 * (md / mr) ** 4; + col = mc.map((v) => v * ma); + } else col = col.map((v, c) => v + mc[c] * 0.3 * sm(0.25, 0.8, k) * exp(-(md - mr) / 5)); + if (y >= P.FAR[x]) { + const fv = 0.86 + 0.14 * vn(x * 0.3, y * 0.3); + col = col.map((v, c) => mix(v, L[3 + c] * fv, 0.8)); + } + } else { + // Limestone with darker strata, catching light along the skyline. + const n = vn(x * 0.22, y * 0.45), n2 = vn(x * 0.07 + 5, y * 0.16); + let v = (0.6 + 0.4 * n + 0.25 * sm(0.55, 0.7, n2)) * (1 + 0.3 * exp(-(y - rid) / 2.5)) * (1 - (0.28 * (y - 92)) / 36); + if (((y + 4 * vn(x * 0.05, 7)) % 6) < 1) v *= 0.72; + const e = hyp((x + 0.5 - CX) / CW, (CB - y - 0.5) / CH) * (0.94 + 0.16 * vn(x * 0.6, y * 0.6)); + // The pale outcrop the cave opens in, cracked, with a ledge on top. + const ot = CB - CH - 6 + 2.5 * vn(x * 0.2, 4), ow = 16 + 4 * vn(y * 0.2, 6) - 6 * sm(CB - 3, CB - CH - 6, y); + if (y >= ot && y < CB + 1 && abs(x + 0.5 - CX) < ow) { + v = (1.05 + 0.3 * n) * (y < ot + 1 ? 1.3 : 1) * (vn(x * 0.9, y * 0.1) < 0.28 ? 0.66 : 1) * (1 - 0.25 * sm(ow - 3, ow, abs(x + 0.5 - CX))); + if (e < 1.35) v *= 0.75 + 0.25 * sm(1, 1.35, e); + } + if (y < CB && e < 1) { + // The mouth, black inside and a little lit at the lip. + const lip = sm(0.75, 1, e); + col = [3 + 12 * lip, 2 + 8 * lip, 3 + 8 * lip]; + } else { + if (y >= CB) v *= (1.1 + 0.25 * hash(x, y)) * (1 - 0.45 * exp(-(((x - CX) / CW) ** 2)) * sm(CB + 5, CB, y)); + col = L.slice(0, 3).map((c) => c * v); + } + } + (B[o] = col[0]), (B[o + 1] = col[1]), (B[o + 2] = col[2]); + } + } + for (const [bx, by, br] of P.bush) + for (let y = floor(by - br); y <= by + br; y++) + for (let x = floor(bx - br); x <= bx + br; x++) { + const e = hyp(x + 0.5 - bx, (y + 0.5 - by) * 1.2) / br; + if (x < 0 || x >= W || y < 0 || y >= H || e > 1) continue; + const o = (y * W + x) * 3, v = (0.8 + 0.4 * hash(x, y + 50)) * (y + 0.5 < by - br * 0.3 ? 1.45 : 1); + for (let c = 0; c < 3; c++) B[o + c] = L[6 + c] * v; + } +} + +export function draw(display, params) { + const P = params.s; + if (!P || !P.init) return; + const k = P.k1, key = round(k * 400); + if (key !== P.key) backdrop(P, k), (P.key = key); + const F = P.f, D = P.D, T = P.t; + F.set(P.bg); + // Stars come out as the glow goes, behind the cloud streaks. + const sk = sm(0.4, 0.95, k); + if (sk > 0) + for (const [x, y, b, ph, fq] of P.stars) { + if (y >= P.FAR[x] - 1) continue; + const o = (y * W + x) * 3, v = sk * b * (0.75 + 0.25 * sin(T * fq + ph)); + (F[o] += 190 * v), (F[o + 1] += 190 * v), (F[o + 2] += 210 * v); + } + // High cloud drifts with the wind, lit from below by the sun that has set. + const cc = P.cc, sh = floor(P.co); + for (let y = 0; y < 72; y++) + for (let x = 0; x < W; x++) { + const a = P.CT[y * 128 + ((x - sh) & 127)]; + if (a < 0.02 || y >= P.FAR[x]) continue; + const o = (y * W + x) * 3, g = 0.55 + y / 110, al = 0.55 * a; + for (let c = 0; c < 3; c++) F[o + c] = mix(F[o + c], cc[c] * g, al); + } + + // Bats and the hawk as darkness laid over the light. + D.fill(0); + const put = (x, y, a) => { + if (x >= 0 && x < W && y >= 0 && y < H) D[y * W + x] += a; + }; + const M = P.S.length; + for (let i = 0; i < M; i++) { + if (P.S[i] < 0) continue; + const x = round(P.BX[i]), y = round(P.BY[i]), near = P.Z[i] > -0.3, q = floor(((T * P.FR[i] + P.FP[i]) % 1) * 3) - 1; + if (near) put(x, y, 0.95), put(x - 1, y + q, 0.8), put(x + 1, y + q, 0.8); + else put(x, y, 0.6), put(x + (q ? 1 : -1), y + q, 0.35); + } + const hk = P.hk; + if (hk && hk.x !== undefined) { + const { x, y, ux, uy, u } = hk, nx = -uy, ny = ux; + const seg = (x0, y0, x1, y1, r) => { + const n = 2 * hyp(x1 - x0, y1 - y0) + 1; + for (let s = 0; s <= n; s++) { + const px = mix(x0, x1, s / n), py = mix(y0, y1, s / n); + for (let yy = floor(py - r); yy <= py + r; yy++) for (let xx = floor(px - r); xx <= px + r; xx++) if (hyp(xx + 0.5 - px, yy + 0.5 - py) <= r + 0.25) put(xx, yy, 0.6); + } + }; + seg(x + 2.2 * ux, y + 2.2 * uy, x - 3 * ux, y - 3 * uy, 1); + seg(x - 3 * ux, y - 3 * uy, x - 4.6 * ux + 0.9 * nx, y - 4.6 * uy + 0.9 * ny, 0.6); + seg(x - 3 * ux, y - 3 * uy, x - 4.6 * ux - 0.9 * nx, y - 4.6 * uy - 0.9 * ny, 0.6); + if (u > 0.14 && u < 0.5) { + // Wings folded back in the stoop. + for (const sg of [1, -1]) seg(x + 0.8 * ux + sg * nx, y + 0.8 * uy + sg * ny, x - 4 * ux + sg * 2.4 * nx, y - 4 * uy + sg * 2.4 * ny, 0.6); + } else { + // Spread and beating as it climbs away. + const fl = 1.6 * sin(hk.t * 15); + for (const sg of [1, -1]) { + const wx = x + sg * 3.2 * nx + 1.1 * ux, wy = y + sg * 3.2 * ny + 1.1 * uy; + seg(x + 0.5 * ux, y + 0.5 * uy, wx, wy, 0.9); + seg(wx, wy, x + sg * 6.2 * nx - (0.6 + fl) * ux, y + sg * 6.2 * ny - (0.6 + fl) * uy, 0.55); + } + } + } + for (let i = 0, o = 0; i < N; i++, o += 3) { + const a = min(1, D[i]), b = 1 - a; + display.setPixel(i & 63, i >> 6, min(255, F[o] * b + BAT[0] * a) | 0, min(255, F[o + 1] * b + BAT[1] * a) | 0, min(255, F[o + 2] * b + BAT[2] * a) | 0); + } +} From 0986a865f33898aaed2b6360a2282f27282005d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:19:25 +0000 Subject: [PATCH 04/33] emergence: readable column, outcrop round the cave, denser start --- incoming/pattern-study/patterns/emergence.js | 21 ++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/incoming/pattern-study/patterns/emergence.js b/incoming/pattern-study/patterns/emergence.js index 9ff374bb1..6445dab37 100644 --- a/incoming/pattern-study/patterns/emergence.js +++ b/incoming/pattern-study/patterns/emergence.js @@ -9,7 +9,7 @@ // Round: 24 // =============================== // @matrix 64x128 -// @knobs Dusk=0..1, Speed=0.2..3, Bats=300..1200, Wind=-1..1 +// @knobs Dusk=0..1, Speed=0.2..3, Bats=400..1400, Wind=-1..1 // // Knob 1: Dusk. ... // Knob 2: Speed. ... @@ -37,12 +37,12 @@ function vn(x, y) { } // The cave mouth: its middle column, floor row, half width and height. -const [CX, CB, CW, CH] = [24, 121, 9, 9.5]; +const [CX, CB, CW, CH] = [27, 121, 9, 9.5]; // The row where the bats come out, the most bats the knob asks for, the // spine's length in rows, and a bat's climb at Speed 2 in LEDs a second. -const [CY, MAXB, HM, V0] = [115, 1200, 118, 28]; +const [CY, MAXB, HM, V0] = [115, 1400, 118, 28]; // The column's front at the start, and the length of one trip. -const [F0, LT] = [52, 150]; +const [F0, LT] = [90, 150]; // The sky from the top (0) to the horizon (1): afterglow, late dusk, night. const SKIES = [ @@ -76,7 +76,7 @@ export function setup(params) { }); // The near hill climbs over the cave; a flat mesa stands far off on the right. for (let x = 0; x < W; x++) { - P.RID[x] = 99 + 0.07 * x - 10 * exp(-(((x - 21) / 15) ** 2)) + 2 * vn(x * 0.13, 3) + 1.2 * vn(x * 0.4, 9) - 1.6; + P.RID[x] = 99 + 0.07 * x - 10 * exp(-(((x - 23) / 15) ** 2)) + 2 * vn(x * 0.13, 3) + 1.2 * vn(x * 0.4, 9) - 1.6; P.FAR[x] = 90 + 2.5 * vn(x * 0.09, 1) + 1.5 * vn(x * 0.3, 2) - 7 * sm(38, 43, x) * (1 - sm(57, 61, x)) - 2 * exp(-(((x - 8) / 9) ** 2)); } // Scrub: juniper and mesquite clumps on the slope and along the skyline. @@ -123,7 +123,7 @@ function place(P, M) { export function update(dt, input, params) { if (!params.s) setup(params); const P = params.s, k = input.knobValues || [0, 2, 0, 0.06]; - const d = cl(dt || 0, 0, 0.1) * (cl(k[1], 0.2, 3) / 2), M = round(cl(k[2], 300, 1200)); + const d = cl(dt || 0, 0, 0.1) * (cl(k[1], 0.2, 3) / 2), M = round(cl(k[2], 400, 1400)); P.k1 = cl(k[0], 0, 1); P.w = cl(k[3], -1, 1); const T = (P.t += d); @@ -135,11 +135,11 @@ export function update(dt, input, params) { // The spine of the column: it leans downwind, corkscrews and breathes, and // now and then tears in two along its middle and closes again higher up. - const Rk = 5 + (3.5 * (M - 300)) / 900, ph = (T - 2.4) % 12, split = T < 2.4 ? 0 : sm(0, 1.4, ph) * (1 - sm(4.6, 6.2, ph)); + const Rk = 5 + (3.5 * (M - 400)) / 1000, ph = (T - 2.4) % 12, split = T < 2.4 ? 0 : sm(0, 1.4, ph) * (1 - sm(4.6, 6.2, ph)); for (let h = 0; h < HM; h++) { const A = 1 + 7.5 * sm(4, 92, h); - SX[h] = CX + w * (0.13 * h + 0.0011 * h * h) + A * (sin(0.052 * h - 1.3 * T) + 0.4 * sin(0.12 * h + 0.75 * T + 2)); - SR[h] = Rk * (0.32 + 0.68 * sm(0, 26, h)) * (1 + 0.005 * h) * (1 + 0.3 * sin(0.075 * h - 2.1 * T)); + SX[h] = CX + w * (0.17 * h + 0.0009 * h * h) + A * (sin(0.052 * h - 1.3 * T) + 0.4 * sin(0.12 * h + 0.75 * T + 2)); + SR[h] = Rk * (0.5 + 0.5 * sm(0, 22, h)) * (1 + 0.005 * h) * (1 + 0.3 * sin(0.075 * h - 2.1 * T)); SP[h] = 0.055 * h - 1.7 * T; SS[h] = h > 18 && h < 96 ? split * 9 * sin((PI * (h - 18)) / 78) : 0; } @@ -169,7 +169,8 @@ export function update(dt, input, params) { const { S, V, A, B, TU, RH, DR, JF, JP, OX, OY, VX, VY, BX, BY, Z } = P; for (let i = 0; i < MAXB; i++) { if (S[i] < 0 && i >= M) continue; - const s = (S[i] += V[i] * d); + // Fast out of the mouth, then the steady climb. + const s = (S[i] += V[i] * d * (S[i] < 40 ? 1.6 - 0.015 * S[i] : 1)); if (s < 0) continue; const hh = min(s, TU[i]), j = min(HM - 2, floor(hh)), f = hh - j; const r = mix(SR[j], SR[j + 1], f), p = mix(SP[j], SP[j + 1], f), ca = cos(p), sa = sin(p), a = A[i], b = B[i]; From bfff715cd43a974261c11eb7a11d087f1058c835 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:22:16 +0000 Subject: [PATCH 05/33] tower-crane: first running version --- .../pattern-study/patterns/tower-crane.js | 561 ++++++++++++++++++ 1 file changed, 561 insertions(+) create mode 100644 incoming/pattern-study/patterns/tower-crane.js diff --git a/incoming/pattern-study/patterns/tower-crane.js b/incoming/pattern-study/patterns/tower-crane.js new file mode 100644 index 000000000..4df794154 --- /dev/null +++ b/incoming/pattern-study/patterns/tower-crane.js @@ -0,0 +1,561 @@ +// ===== Patternflow pattern ===== +// Title: Tower Crane +// Family: construction +// Idea: A yellow tower crane beside a concrete frame going up at dusk hoists a precast wall, rebar, formwork and the concrete bucket from the yard to the top deck, slewing its jib round in perspective, and each bucket pours the deck so the building grows a floor. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 24, after the tower cranes that stand beside a high-rise frame and lift each floor's materials up to the deck +// Round: 24 +// =============================== +// @matrix 64x128 +// @knobs Dusk=0..1, Speed=0.2..3, Floors=8..13, Wind=-1..1 +// +// Knob 1: Dusk. ... +// Knob 2: Speed. ... +// Knob 3: Floors. ... +// Knob 4: Wind. ... +// Buttons: K1 tap pours the deck now, and the building grows a floor. + +const [W, H] = [64, 128]; +const N = W * H; +const { min, max, floor, round, sin, cos, abs, exp, 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)); +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; +} + +// Street level, one storey, and the most floors that fit under the jib. +const [GY, FH, NMAX] = [114, 6, 13]; +// The frame runs from column 3 to 39: six columns two LEDs wide round five +// bays, and the bay by the crane is the lift core. +const [BX0, BX1, CORE] = [3, 39, 4]; +// The crane: mast centre, the jib's bottom chord at the root, jib and +// counter-jib lengths, the top of the tower head. +const [MX, JY, JL, CJL, APEX] = [46, 15, 42, 15, 3]; +// The hook's highest row, the radius it picks at over the yard, the yard's load line. +const [HTOP, PICK_R, PICK_Y] = [21, 11, 120]; +// A precast wall panel, rebar, formwork, the concrete bucket: width, height, +// and the column of the deck each is set down on. +const LW = [8, 11, 9, 5], LH = [5, 2, 4, 6], DROP = [35, 20, 12, 6]; +// Sky top, middle, horizon; the sun; lit and shaded cloud; the light on things. +// Four steps from late afternoon to night. +const KEYS = [ + [34, 72, 156, 128, 142, 186, 255, 190, 112, 255, 236, 170, 255, 236, 204, 150, 130, 160, 1, 0.9, 0.74], + [30, 50, 130, 168, 108, 140, 255, 140, 62, 255, 168, 78, 255, 170, 116, 104, 70, 108, 0.86, 0.64, 0.48], + [16, 20, 70, 64, 44, 96, 196, 86, 78, 226, 90, 52, 176, 92, 104, 46, 36, 66, 0.44, 0.38, 0.46], + [4, 6, 22, 10, 14, 40, 44, 34, 60, 0, 0, 0, 42, 36, 56, 16, 16, 30, 0.2, 0.21, 0.3], +]; +const FLC = [1, 0.94, 0.82]; +const YEL = [255, 196, 22]; +// The floodlight tower in the yard and where it is aimed. +const [LX, LY] = [60.5, 84.5]; +const [AX, AY] = (([x, y]) => [x / Math.hypot(x, y), y / Math.hypot(x, y)])([-34, 31]); + +export function setup(params) { + const F = (n) => new Float32Array(n); + const P = (params.s = { + f: F(N * 3), bg: F(N * 3), lm: F(N * 3), fl: F(N), row: F(H * 3), cloud: F(128 * 64), sky: new Int16Array(W), + t: 0, rt: 0, n: 0, nk: -1, stage: 3, cage: 1, form: 1, item: -1, ix: 0, pourX: -1, top: 0, pourBtn: 0, lightNew: -1, + th: PI, r: MX - DROP[3], hy: 40, load: 3, ph: "I", pt: 0, pd: 1, f0: [0, 0, 0], f1: [0, 0, 0], + sw: 0, swv: 0, vx: 0, ptx: MX - DROP[3], on: new Uint8Array(NMAX), onT: F(NMAX).fill(9), nsw: 2.4, cx: 0, + sp: [], weld: 0.4, welding: 0, wk: [], mix: 2, cars: [], ncar: 1.4, bgKey: -1, + }); + // Clouds: a strip of soft noise that wraps every 128 columns. + for (let i = 0; i < 128 * 64; i++) { + const x = i & 127, y = i >> 7, f = (X) => 0.55 * vn(X * 0.05, y * 0.13) + 0.3 * vn(X * 0.13, y * 0.3 + 9) + 0.15 * vn(X * 0.4, y * 0.6 + 3); + P.cloud[i] = mix(f(x), f(x + 128), x / 128); + } + // The city behind: blocks of four to nine columns. + for (let x = 0; x < W; ) { + const w = 4 + floor(rnd() * 6), h = 98 + floor(rnd() * 12); + for (let i = 0; i < w && x < W; i++, x++) P.sky[x] = h; + } + for (let k = 0; k < NMAX; k++) P.on[k] = rnd() < 0.35 ? 1 : 0; + for (let i = 0; i < 3; i++) P.wk.push({ x: 8 + i * 11 + rnd() * 4, tx: 8 + rnd() * 28, wait: rnd() * 2 }); +} + +// A move of the crane: slew angle, trolley radius and hook row, eased from +// where they are to where they go. +function go(P, ph, dur, to) { + P.ph = ph; + P.pt = 0; + P.pd = dur; + P.f0 = [P.th, P.r, P.hy]; + P.f1 = [to.th === undefined ? P.th : to.th, to.r === undefined ? P.r : to.r, to.hy === undefined ? P.hy : to.hy]; +} + +const deckY = (P) => GY - P.n * FH; +const tip = (P) => [MX + P.r * cos(P.th), JY + 1 - P.r * sin(P.th) * 0.1]; +const bucketHy = (P) => deckY(P) - FH - 2 - LH[3] - 3; + +function finishFloor(P) { + if (P.n < NMAX) P.n++; + else P.top = 1; + P.stage = 0; + P.cage = P.form = 0; + P.item = -1; + P.pourX = -1; + // The floor that has just been glazed lights up a moment later. + P.nsw = min(P.nsw, 0.7); + P.lightNew = P.n - 3; +} + +// The next move when one is done: down to the yard, rig, hoist, slew over, +// lower, set down (or pour), and up again. +function step(P) { + const T = P.ph, type = P.load; + if (T === "A") go(P, "B", 0.6, {}); + else if (T === "B") (P.load = P.stage), go(P, "C", 2.8, { hy: HTOP }); + else if (T === "C") go(P, "D", 2.4, { th: P.th + PI, r: MX - DROP[type] }); + else if (T === "D") go(P, "E", 1.5, { hy: type === 3 ? bucketHy(P) : deckY(P) - 4 - LH[type] }); + else if (T === "E") { + if (type === 3) (P.pourX = BX0), go(P, "P", 3, { r: MX - BX1 + 2 }); + else { + // Set down: the panel goes into the core, rebar and formwork lie on the deck. + if (type) (P.item = type), (P.ix = round(tip(P)[0])); + if (type === 1) P.cage = 0; + if (type === 2) P.form = 0; + P.stage = max(P.stage, type + 1); + P.load = -1; + for (const w of P.wk) w.tx = tip(P)[0] + (rnd() - 0.5) * 10; + go(P, "F", 0.9, {}); + } + } else if (T === "P" || T === "I") finishFloor(P), go(P, "F", 0.5, {}); + else if (T === "F") go(P, "G", 0.6, { hy: P.hy - 12 }); + else (P.load = -1), go(P, "A", 1.8, { th: (floor(P.th / TAU + 1e-3) + 1) * TAU, r: PICK_R, hy: PICK_Y - LH[P.stage] - 3 }); +} + +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); + const nf = round(cl(k[2], 8, 13)); + if (nf !== P.nk) { + // A new count of floors: the hook keeps its place over the deck. + const sh = (nf - P.n) * FH; + P.nk = P.n = nf; + P.top = 0; + if (P.ph === "I") (P.hy = bucketHy(P)), (P.pourX = BX0), go(P, "I", 3.2, { r: MX - BX1 + 2 }); + else if ("EPFG".includes(P.ph)) (P.hy -= sh), (P.f0[2] -= sh), (P.f1[2] -= sh); + } + P.w = cl(k[3], -1, 1); + const t = (P.t += d); + P.rt += rd; + // The evening deepens a little and comes back once every minute and a half. + const k0 = cl(k[0], 0, 1); + P.h = k0 + (1 - k0) * 0.1 * (0.5 - 0.5 * cos((TAU * t) / 90)); + if (input.btnPressed && input.btnPressed[0] && P.pourX < 0 && !P.top) (P.pourBtn = 1), (P.pourX = BX0); + + // The crane. + P.pt += d; + const u = sm(0, 1, P.pt / P.pd), lin = min(1, P.pt / P.pd), pour = P.ph === "P" || P.ph === "I"; + P.th = mix(P.f0[0], P.f1[0], u); + P.r = mix(P.f0[1], P.f1[1], pour ? lin : u); + P.hy = mix(P.f0[2], P.f1[2], u); + if (pour) P.pourX = max(P.pourX, tip(P)[0] + 2); + if (P.pt >= P.pd) step(P); + if (P.pourBtn) { + P.pourX += 30 * d; + if (P.pourX > BX1) (P.pourBtn = 0), finishFloor(P); + } + // The load swings on its cable when the trolley speeds up or slows, and in the wind. + const [tx, ty] = tip(P), L = max(4, P.hy - ty), vx = d > 0 ? (tx - P.ptx) / d : 0, ax = d > 0 ? (vx - P.vx) / d : 0; + P.ptx = tx; + P.vx = vx; + if (d > 0) { + const eq = P.w * (0.06 + 0.04 * sin(t * 1.7)) * (P.load >= 0 ? 1 : 1.6); + P.swv += (-(110 / L) * (P.sw - eq) - cl(ax, -40, 40) / L - 0.8 * P.swv) * d; + P.sw = cl(P.sw + P.swv * d, -0.3, 0.3); + } + P.swx = cl(L * sin(P.sw), -3.5, 3.5); + + // The rebar is tied up the columns and the formwork goes round them. + if (P.stage >= 2) P.cage = min(1, P.cage + d * 0.3); + if (P.stage >= 3) P.form = min(1, P.form + d * 0.3); + + // A floor switches its lights on or off together. + for (let i = 0; i < NMAX; i++) P.onT[i] += rd; + if ((P.nsw -= d) <= 0) { + const g = max(1, P.n - 2), want = P.lightNew; + let f = want >= 0 && want < g ? want : floor(rnd() * g); + if (want < 0) { + let lit = 0; + for (let i = 0; i < g; i++) lit += P.on[i]; + for (let n = 0; n < 6 && P.on[f] === (lit > g * 0.5 ? 0 : 1); n++) f = floor(rnd() * g); + } + P.on[f] = want >= 0 ? 1 : 1 - P.on[f]; + P.onT[f] = 0; + P.lightNew = -1; + P.nsw = 1.6 + rnd() * 2; + } + + // Welding on the deck, and its sparks falling down the face. + if ((P.weld -= d) <= 0) (P.welding = 1 - P.welding), (P.weld = P.welding ? 1.4 + rnd() * 1.6 : 0.4 + rnd() * 0.8); + const wy = deckY(P) - 2; + if (P.welding) for (let n = 0; n < 4; n++) if (rnd() < d * 18) P.sp.push([18.5 + rnd() - 0.5, wy, (rnd() - 0.5) * 14, -4 - rnd() * 7, 1 + rnd() * 0.9, 18.5, wy]); + P.sp = P.sp.filter((s) => ((s[5] = s[0]), (s[6] = s[1]), (s[3] += 46 * d), (s[0] += s[2] * d), (s[1] += s[3] * d), (s[4] -= d) > 0 && s[1] < GY)); + + // Workers walk about the deck. + for (const w of P.wk) { + if (abs(w.tx - w.x) > 0.3) w.x += Math.sign(w.tx - w.x) * min(abs(w.tx - w.x), 4 * d); + else if ((w.wait -= d) <= 0) (w.tx = 6 + rnd() * 30), (w.wait = 1 + rnd() * 3); + w.x = cl(w.x, 5, 37); + } + + // The mixer comes in for the bucket and backs out after the pour. + P.mix += cl((P.stage >= 2 && !P.top ? 2 : -26) - P.mix, -9 * d, 9 * d); + + // Cars along the street. + if ((P.ncar -= d) <= 0) { + const dir = rnd() < 0.5 ? 1 : -1; + P.cars.push([dir > 0 ? -8 : W + 1, dir * (11 + rnd() * 6), floor(rnd() * 4)]); + P.ncar = 2 + rnd() * 4; + } + P.cars = P.cars.filter((c) => ((c[0] += c[1] * d), c[0] > -12 && c[0] < W + 12)); + + // Clouds drift with the wind. + P.cx += (1.5 + 9 * P.w) * d; +} + +// --- drawing ----------------------------------------------------------- + +let F, LM; +function put(x, y, c, 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 i = 0; i < 3; i++) F[o + i] = mix(F[o + i], c[i], a); +} +function add(x, y, c, k) { + x = round(x); + y = round(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] * k; +} +// A surface: its own colour under the light that falls on that LED. +function sur(x, y, r, g, b, k = 1) { + x = round(x); + y = round(y); + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + F[o] = r * LM[o] * k; + F[o + 1] = g * LM[o + 1] * k; + F[o + 2] = b * LM[o + 2] * k; +} +const surc = (x, y, c, k = 1) => sur(x, y, c[0], c[1], c[2], k); +function line(x0, y0, x1, y1, c, k = 1) { + const n = max(abs(x1 - x0), abs(y1 - y0)) * 1.2 + 1; + for (let i = 0; i <= n; i++) surc(mix(x0, x1, i / n), mix(y0, y1, i / n), c, k); +} + +// Sky, sun and the city beyond, kept until the evening moves on. +function sky(P, K) { + const h = P.h, key = round(h * 300); + if (key === P.bgKey) return F.set(P.bg); + P.bgKey = key; + const B = P.bg, row = P.row, night = sm(0.4, 0.9, h), vis = 1 - sm(0.78, 0.95, h); + const sx = 55, sy = 62 + 52 * h; + for (let y = 0; y < H; y++) { + const f = min(1, y / GY), a = f < 0.5 ? 0 : 3, u = f < 0.5 ? f / 0.5 : (f - 0.5) / 0.5; + for (let c = 0; c < 3; c++) row[y * 3 + c] = mix(K[a + c], K[a + 3 + c], a ? u * u : sm(0, 1, u)); + } + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, o = i * 3; + if (y >= P.sky[x]) { + const win = night * (hash(x, y * 3) < 0.16 && y & 1 ? 1 : 0); + for (let c = 0; c < 3; c++) B[o + c] = mix(K[3 + c], K[6 + c], 0.55) * 0.6 * (x & 1 ? 1 : 0.86) + win * [210, 160, 90][c]; + continue; + } + const d = Math.hypot(x + 0.5 - sx, y + 0.5 - sy), g = vis * (0.55 * exp(-((d - 3.6) ** 2) / 50) + 0.3 * exp(-d / 16)); + for (let c = 0; c < 3; c++) B[o + c] = d < 3.6 && vis > 0 ? mix(row[y * 3 + c], K[9 + c], vis) : row[y * 3 + c] + K[9 + c] * g * 0.7; + } + F.set(B); +} + +function clouds(P, K) { + const cover = 0.56 - 0.18 * abs(P.w), C = P.cloud; + for (let y = 0; y < 100; y++) { + const ty = floor(y * 0.6) & 63, fade = 1 - sm(50, 100, y); + for (let x = 0; x < W; x++) { + if (y >= P.sky[x]) continue; + const v = C[(ty << 7) | (floor(x + P.cx) & 127)], a = cl((v - cover) * 7, 0, 1) * fade; + if (a <= 0.01) continue; + const lt = cl(1.25 - (v - cover) * 4 + (y - 40) / 120, 0, 1); + put(x, y, [mix(K[15], K[12], lt), mix(K[16], K[13], lt), mix(K[17], K[14], lt)], a * 0.92); + } + } +} + +// The light on every surface: what is left of the day, cloud shadows passing +// over, and the floodlights once they come on. +function light(P, K) { + const sunA = 1 - sm(0.45, 0.85, P.h), fon = sm(0.3, 0.55, P.h), FLb = P.fl, C = P.cloud; + const cover = 0.5 - 0.18 * abs(P.w), [tx, ty] = tip(P), hx = tx + P.swx * 0.5; + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, o = i * 3; + const v = C[((floor(y * 0.25 + 20) & 63) << 7) | (floor(x * 0.8 + P.cx * 2.2 + 40) & 127)]; + const sh = 1 - 0.45 * sunA * cl((v - cover) * 5, 0, 1); + let fl = 0; + if (fon > 0) { + const dx = x + 0.5 - LX, dy = y + 0.5 - LY, dd = Math.hypot(dx, dy) + 0.01, ca = (dx * AX + dy * AY) / dd; + fl = (exp((ca - 1) / 0.03) * 1.5) / (1 + (dd / 60) ** 2); + if (y > ty + 1) { + const q = (x + 0.5 - hx) / (1.2 + 0.16 * (y - ty)); + fl += (exp(-q * q) * 0.9) / (1 + ((y - ty) / 90) ** 2); + } + fl *= fon; + } + FLb[i] = fl; + for (let c = 0; c < 3; c++) LM[o + c] = K[18 + c] * sh + fl * FLC[c]; + } +} + +function ground(P) { + for (let y = GY; y < H; y++) + for (let x = 0; x < W; x++) { + const n = 0.82 + 0.3 * hash(x, y + 900); + if (y >= 124) sur(x, y, 62 * n, 60 * n, 66 * n); + else if (y === 123) sur(x, y, 150, 146, 140); + else sur(x, y, 150 * n, 126 * n, 98 * n); + } + for (let x = 2; x < W; x += 8) sur(x, 126, 190, 180, 120), sur(x + 1, 126, 190, 180, 120); +} + +// The concrete frame, its windows, and the floor going up on the deck. +function building(P) { + const n = P.n, dk = deckY(P), g = n - 2, night = sm(0.4, 0.9, P.h), row = P.row; + for (let y = dk; y < GY; y++) { + const k = floor((GY - 1 - y) / FH), ry = y - (GY - (k + 1) * FH); + for (let x = BX0; x <= BX1; x++) { + const cx = (x - BX0) % 7, b = floor((x - BX0) / 7), bx = cx - 2; + if (ry === 0) sur(x, y, 228, 220, 206); + else if (cx < 2) sur(x, y, 206, 198, 184); + else if (ry === 1) sur(x, y, 178, 172, 162); + else if (b === CORE) sur(x, y, 196, 190, 180, (x + y) % 9 === 0 ? 0.8 : 0.95); + else if (k < g) { + // Glass: the sky in it, or a lit room behind. + if (bx === 2) { + sur(x, y, 80, 80, 88); + continue; + } + const o = (y * W + x) * 3, rr = cl(round(y * 0.3 + 4), 0, GY - 1) * 3; + let on = P.on[k] ? hash(k, b * 7 + 1) < 0.92 : hash(k, b * 7 + 3) < 0.1 + 0.6 * night; + if (on && P.on[k] && P.onT[k] < 0.5) on = hash(k * 13 + b, floor(P.onT[k] * 22)) > 0.45; + const lc = hash(k, b + 40) < 0.75 ? [255, 204, 128] : [206, 230, 255], a = on ? 0.8 + 0.2 * night : 0; + for (let c = 0; c < 3; c++) F[o + c] = mix(row[rr + c] * 0.42 + c * 5, lc[c], a) * (ry === 2 ? 1.12 : 1); + } else { + // An open floor: dark inside, the back columns, a work lamp at night. + if (night > 0.05 && bx === 2 && ry === 2 && hash(k, b) < 0.6) put(x, y, [255, 214, 150].map((v) => v * night)); + else sur(x, y, 60, 54, 52, bx === 2 ? 0.6 : 0.28); + } + } + } + // Ties from the mast to the frame. + for (let k = 2; k < n; k += 3) for (let x = BX1 + 1; x < MX - 2; x++) sur(x, GY - (k + 1) * FH + 1, 200, 170, 60); + + // The floor on the deck: starter bars, the core panel, the rebar cages, the formwork. + const top = dk - FH, st = P.top ? 0 : P.stage; + if (st >= 1) for (let y = top + 1; y < dk; y++) for (let x = BX0 + 30; x <= BX0 + 34; x++) sur(x, y, 176, 172, 164, x === BX0 + 32 ? 0.8 : 1); + for (let ci = 0; ci < 6; ci++) { + const x = BX0 + ci * 7, hc = st >= 2 ? round(P.cage * (FH - 1)) : 0; + for (let j = 1; j <= max(2, hc); j++) sur(x, dk - j, 176, 96, 50), j & 1 && sur(x + 1, dk - j, 130, 76, 44); + if (st >= 3 && ci / 6 < P.form) for (let j = 1; j < FH; j++) sur(x, dk - j, 218, 162, 84), sur(x + 1, dk - j, 196, 140, 70); + } + if (st >= 3) { + const fx = BX0 + 37 * P.form; + for (let x = BX0; x <= min(BX1, fx); x++) { + sur(x, top, 214, 156, 80); + if ((x - BX0) % 7 === 4) for (let j = 1; j < FH; j++) sur(x, top + j, 150, 150, 156, 0.8); + } + } + if (P.pourX >= 0) for (let x = BX0; x <= min(BX1, P.pourX); x++) sur(x, top, 140, 140, 150), (x - BX0) % 7 < 2 && sur(x, top + 1, 140, 140, 150, 0.9); + if (P.item > 0) drawLoad(P.item, P.ix, dk - LH[P.item]); + if (P.top) { + // Topped out: a little tree and a flag on the roof. + for (let j = 1; j < 9; j++) sur(21, dk - j, 120, 110, 100); + for (let j = 0; j < 4; j++) for (let i = 1; i <= 4 - abs(j - 1); i++) sur(21 + i * (P.w < 0 ? -1 : 1), dk - 8 + j, 230, j & 1 ? 230 : 40, j & 1 ? 230 : 40); + for (let j = 0; j < 4; j++) for (let i = -j; i <= j; i++) sur(26 + i, dk - 5 + j, 40, 120, 60); + } + // The workers on the deck in hi-vis, and the welder by a column. + for (const w of P.wk) { + const x = round(w.x); + sur(x, dk - 1, 50, 56, 90), sur(x, dk - 2, 255, 130, 20), sur(x, dk - 3, 250, 250, 240); + } + sur(17, dk - 1, 50, 56, 90), sur(17, dk - 2, 255, 130, 20), sur(17, dk - 3, 60, 60, 70); +} + +// One of the four loads, centred on x with its top on row y0. +function drawLoad(type, cx, y0) { + const w = LW[type], h = LH[type], x0 = round(cx - w / 2), y = round(y0); + for (let j = 0; j < h; j++) + for (let i = 0; i < w; i++) { + const X = x0 + i, Y = y + j; + if (type === 0) { + if (!(i >= 2 && i <= 4 && j >= 1 && j <= 3)) sur(X, Y, 150, 146, 140, i === 0 || j === 0 ? 1.25 : 1); + } else if (type === 1) sur(X, Y, i === 2 || i === 8 ? 60 : 186, i === 2 || i === 8 ? 54 : 92, 48, j ? 0.72 : 1); + else if (type === 2) sur(X, Y, 226, 170, 92, j & 1 ? 0.55 : 1); + else if (i >= (j >= h - 2 ? 1 : 0) && i < w - (j >= h - 2 ? 1 : 0)) sur(X, Y, 120, 120, 128, j === 1 ? 0.6 : j === 0 ? 1.3 : 1); + } + if (type === 3) sur(x0 + 2, y + h, 70, 70, 76); +} + +// The hook block, its cable and slings, and whatever it carries. +function hook(P) { + const [tx, ty] = tip(P), hx = tx + P.swx, hy = P.hy; + line(tx, ty + 1, hx, hy - 1, [120, 120, 130], 0.9); + for (let i = -1; i <= 1; i++) surc(hx + i, hy - 1, YEL); + sur(hx, hy, 60, 60, 66), sur(hx, hy + 1, 90, 90, 96); + if (P.load >= 0) { + const w = LW[P.load] / 2 - 0.5; + line(hx, hy + 1, hx - w, hy + 3, [110, 110, 116], 0.8), line(hx, hy + 1, hx + w, hy + 3, [110, 110, 116], 0.8); + drawLoad(P.load, hx + 0.5, hy + 3); + // Concrete runs from the bucket into the forms. + if (P.ph === "P" || P.ph === "I") for (let j = hy + 10; j < deckY(P) - FH; j++) sur(hx + 0.5, j, 140, 140, 150); + } +} + +// The yard: stock waiting by the crane, the load being rigged, the floodlight tower. +function yard(P) { + drawLoad(2, 53, 115), drawLoad(1, 54, 118); + if (P.ph === "A" || P.ph === "B") { + const s = P.stage, x = MX + PICK_R - LW[s] / 2 - 2, arm = P.ph === "B" ? (P.pt * 6) & 1 : 0; + drawLoad(s, MX + PICK_R + 0.5, PICK_Y - LH[s]); + sur(x, PICK_Y - 1, 50, 56, 90), sur(x, PICK_Y - 2, 255, 130, 20), sur(x, PICK_Y - 3, 250, 250, 240), sur(x + 1, PICK_Y - 2 - arm, 255, 130, 20); + } + for (let y = LY; y < 122; y++) sur(62, y, 120, 120, 126); + const fon = sm(0.3, 0.55, P.h); + for (let x = 59; x <= 62; x++) for (let y = 83; y <= 84; y++) put(x, y, y === 84 && x < 62 ? [mix(70, 255, fon), mix(70, 250, fon), mix(76, 230, fon)] : [90, 90, 96]); +} + +// The crane: mast, ties, slewing ring, cab, tower head, jib and counter-jib. +function crane(P) { + const c = cos(P.th), sn = sin(P.th), blink = P.rt % 1.2 < 0.55, fon = sm(0.3, 0.55, P.h); + const jib = () => { + for (let s = 0; s <= JL; s += 0.3) { + const px = MX + s * c, py = JY - s * sn * 0.1, ht = 3 - (1.2 * s) / JL, tri = abs(((s / 2.4) % 2) - 1), wd = sn * 0.9; + surc(px - wd, py, YEL, 0.85), surc(px + wd, py, YEL, 0.85), surc(px, py - ht, YEL), surc(px + wd * (1 - tri), py - tri * ht, YEL, 0.75); + } + }; + const counter = () => { + for (let s = 0; s <= CJL; s += 0.3) { + const px = MX - s * c, py = JY + s * sn * 0.1; + surc(px, py, YEL, 0.85), surc(px, py - 1, YEL, s % 2 < 0.6 ? 0.8 : 0); + if (s > 6 && s < 8.5) for (let j = 1; j <= 2; j++) sur(px, py - 1 - j, 80, 84, 92); + if (s > CJL - 5) for (let j = 1; j <= 4; j++) sur(px, py + j, 190, 184, 172, (floor(s) & 1) === 0 ? 0.8 : 1); + } + }; + const pend = () => { + line(MX, APEX + 1, MX + 28 * c, JY - 28 * sn * 0.1 - 2.2, [110, 104, 96], 0.8); + line(MX, APEX + 1, MX - CJL * c, JY + CJL * sn * 0.1 - 1, [110, 104, 96], 0.8); + }; + const cab = () => { + const x = MX + 3.5 * c; + for (let j = 0; j < 4; j++) + for (let i = -1; i <= 1; i++) { + if (j > 0 && j < 3 && i > -1) put(x + i, JY + 1 + j, P.h > 0.5 ? [255 * fon, 226 * fon, 160 * fon] : [110, 150, 196]); + else surc(x + i, JY + 1 + j, [240, 232, 210]); + } + }; + // What faces away is drawn first. + if (sn < 0) jib(), pend(), cab(); + else counter(), pend(); + // The mast: two chords and a lattice of diagonals. + for (let y = JY + 3; y < 118; y++) { + surc(MX - 2, y, YEL, 0.72), surc(MX + 2, y, YEL); + for (let xi = 1; xi <= 3; xi++) if (((y + xi) & 3) === 0 || ((y - xi) & 3) === 0) surc(MX - 2 + xi, y, YEL, 0.8); + } + for (let x = MX - 4; x <= MX + 4; x++) for (let y = 117; y <= 119; y++) sur(x, y, 170, 166, 160); + for (let x = MX - 3; x <= MX + 3; x++) sur(x, JY + 1, 80, 80, 86), sur(x, JY + 2, 110, 110, 116); + // The tower head. + for (let y = APEX; y <= JY - 3; y++) { + const hw = (1.6 * (y - APEX)) / (JY - 3 - APEX); + surc(MX - hw, y, YEL), surc(MX + hw, y, YEL); + if ((y & 1) === 0) for (let x = round(MX - hw); x <= MX + hw; x++) surc(x, y, YEL, 0.8); + } + if (sn < 0) counter(), pend(); + else jib(), pend(), cab(); + // The trolley, and the warning lights on the head and at the jib tip. + const [tx, ty] = tip(P); + for (let i = -1; i <= 1; i++) sur(tx + i, ty, 90, 90, 96); + for (const [x, y] of [[MX, APEX - 1], [MX + JL * c, JY - JL * sn * 0.1 - 2]]) { + if (!blink) { + put(x, y, [110, 20, 16]); + continue; + } + put(x, y, [255, 40, 28]); + for (let j = -2; j <= 2; j++) for (let i = -2; i <= 2; i++) if (i || j) add(x + i, y + j, [255, 30, 20], (0.12 + 0.3 * fon) / (i * i + j * j)); + } +} + +// The concrete mixer, its drum turning. +function mixer(P) { + const x0 = round(P.mix), night = sm(0.4, 0.9, P.h); + if (x0 < -24) return; + for (let y = 115; y <= 121; y++) + for (let i = 0; i < 13; i++) { + const e = ((i - 6) / 6.8) ** 2 + ((y - 118 + (i - 6) * 0.18) / 3.4) ** 2; + if (e < 1) sur(x0 + i, y, ...((((i * 0.7 + y * 1.4 - P.t * 9) % 4) + 4) % 4 < 2 ? [240, 120, 40] : [230, 228, 220]), 1 - 0.3 * e); + } + for (let i = 0; i < 20; i++) sur(x0 + i, 122, 70, 70, 76); + for (let y = 116; y < 122; y++) for (let i = 15; i < 20; i++) sur(x0 + i, y, ...(y < 119 && i > 16 ? [110, 150, 190] : [230, 60, 40])); + for (const i of [2, 5, 16]) sur(x0 + i, 123, 30, 30, 34), sur(x0 + i + 1, 123, 30, 30, 34); + if (night > 0.1) add(x0 + 20, 120, [255, 240, 200], night); +} + +function cars(P) { + const night = sm(0.35, 0.9, P.h), cols = [[200, 40, 36], [220, 220, 226], [40, 80, 170], [60, 64, 70]]; + for (const [cx, v, ci] of P.cars) { + const x0 = round(cx), dir = v > 0 ? 1 : -1, fx = dir > 0 ? x0 + 6 : x0; + for (let i = 0; i < 7; i++) for (let j = 0; j < 2; j++) surc(x0 + i, 125 + j, cols[ci]); + for (let i = 2; i < 5; i++) sur(x0 + i, 124, 120, 150, 180); + add(fx, 125, [255, 250, 220], 0.3 + night), add(dir > 0 ? x0 : x0 + 6, 125, [255, 20, 10], 0.2 + night); + if (night > 0.1) for (let i = 1; i < 8; i++) add(fx + dir * i, 126 + (i > 4), [255, 240, 200], night * 0.5 * (1 - i / 8)); + } +} + +export function draw(display, params) { + const P = params.s; + if (!P || P.h === undefined) return; + F = P.f; + LM = P.lm; + const hh = cl(P.h, 0, 1) * 3, i0 = min(2, floor(hh)), K = KEYS[i0].map((v, c) => mix(v, KEYS[i0 + 1][c], hh - i0)); + sky(P, K); + clouds(P, K); + light(P, K); + ground(P); + const behind = sin(P.th) < -0.15; + if (behind) hook(P); + building(P); + yard(P); + crane(P); + mixer(P); + cars(P); + if (!behind) hook(P); + // The welding arc, its sparks, and the floodlight beams in the dusk air. + const fon = sm(0.3, 0.55, P.h), FLb = P.fl; + if (P.welding) { + const y = deckY(P) - 2, fl = 0.6 + 0.4 * rnd(); + put(18, y, [235, 244, 255]); + for (let j = -2; j <= 2; j++) for (let i = -2; i <= 2; i++) if (i || j) add(18 + i, y + j, [160, 190, 255], (fl * 0.6) / (i * i + j * j)); + } + for (const [x, y, , , l, px, py] of P.sp) { + const a = min(1, l * 1.4); + put(px, py, [255, 120, 30], a * 0.6), put(x, y, [255, 170 + 80 * min(1, l), 70 * min(1, l)], a); + } + if (fon > 0) for (let i = 0, o = 0; i < N; i++, o += 3) if (FLb[i] > 0.01) for (let c = 0; c < 3; c++) F[o + c] += FLb[i] * 22 * FLC[c]; + 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 67254c54aea4cd45dcec6a953cc2afb5983ac074 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:23:38 +0000 Subject: [PATCH 06/33] thermal: first version runs --- incoming/pattern-study/patterns/thermal.js | 710 +++++++++++++++++++++ 1 file changed, 710 insertions(+) create mode 100644 incoming/pattern-study/patterns/thermal.js diff --git a/incoming/pattern-study/patterns/thermal.js b/incoming/pattern-study/patterns/thermal.js new file mode 100644 index 000000000..c3a803a6e --- /dev/null +++ b/incoming/pattern-study/patterns/thermal.js @@ -0,0 +1,710 @@ +// ===== Patternflow pattern ===== +// Title: Thermal +// Family: flight +// Idea: On a summer afternoon a white sailplane circles up a thermal off a dark ploughed field in a corkscrew of ellipses, sharing it with two buzzards and rising thistledown, until it reaches the base of the cumulus the thermal is building and glides off to find the next one while another glider joins low down. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 24, after thermal soaring under fair-weather cumulus over English farmland +// Round: 24 +// =============================== +// @matrix 64x128 +// @knobs Hour=0..1, Speed=0.2..3, Gliders=1..4, Wind=-1..1 +// +// Knob 1: Hour. Bright midday at 0, with a deep blue sky, white cloud and short shadows, turning to golden late afternoon at 1, when the sun comes in low from the left, the haze goes amber and the hedges and trees throw long shadows across the fields. +// Knob 2: Speed. At 2 the glider takes about six seconds a turn and climbs from the hedges to the cloud base in some twenty; the cloud builds, drifts off and is replaced about every 26 s. At 0.2 it all runs ten times slower. +// Knob 3: Gliders. How many gliders share the thermal, stacked at different heights, from one to four. A day good enough for four also builds a bigger cloud with a darker base, and sends more thistledown up the column. +// Knob 4: Wind. The thermal and its cloud lean downwind, so the turns drift sideways as the gliders climb; the cloud shadows and the far cumulus travel faster. Near 0 it is almost calm. +// Buttons: K1 tap winch-launches a new glider from the field: it rolls, rears up on the cable, drops it and turns into the thermal. + +const W = 64; +const H = 128; +const N = W * H; +const { min, max, sin, cos, abs, sqrt, exp, floor, round, pow, PI } = Math; +const TAU = 2 * PI; +const clamp = (v, a, b) => (v < a ? a : v > b ? b : v); +const sstep = (a, b, v) => { + const t = clamp((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; +const mix = (a, b, t) => a + (b - a) * t; +const mix3 = (a, b, t) => [mix(a[0], b[0], t), mix(a[1], b[1], t), mix(a[2], b[2], t)]; +const rnd = () => Math.random(); +// The horizon, the first row of fields, the cloud base, and the row where the thermal leaves the ground. +const HOR = 97; +const FY = 100; +const YB = 36; +const YG = 116; +// The ploughed field the thermal comes off: its column on the panel and its distance. +const SRCX = 29; +const DSRC = 9.6; +// We look up a little at the gliders, so a level circle shows as a thin ellipse. +const SE = sin(0.3); +// The crafts themselves are drawn from nearer their own level, so a banked wing does not stand on end. +const BE = sin(0.1); +const BC = cos(0.1); + +const hash = (x, y, s) => { + let h = Math.imul((x * 374761393 + y * 668265263 + s * 982451653) | 0, 1274126177); + h = Math.imul(h ^ (h >>> 13), 1274126177); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; +function noise(x, y, s) { + const ix = floor(x); + const iy = floor(y); + const fx = x - ix; + const fy = y - iy; + const u = fx * fx * (3 - 2 * fx); + const v = fy * fy * (3 - 2 * fy); + const a = hash(ix, iy, s); + const b = hash(ix + 1, iy, s); + const c = hash(ix, iy + 1, s); + const d = hash(ix + 1, iy + 1, s); + return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v; +} +const fbm = (x, y, s) => 0.55 * noise(x, y, s) + 0.3 * noise(x * 2.1, y * 2.1, s + 7) + 0.15 * noise(x * 4.3, y * 4.3, s + 13); + +// Crops in full midday sun, and their texture: 0 mottled, 1 rows that run away from us. +const CROP = [ + [70, 150, 45, 0], + [38, 106, 36, 1], + [216, 184, 94, 1], + [198, 160, 66, 0], + [142, 178, 74, 0], + [96, 62, 40, 1], +]; +const PICK = [0, 0, 0, 1, 1, 2, 2, 2, 3, 3, 4, 4]; + +// Bodies as tapered strokes in the craft's own axes: forward, right, up, and two radii. +const GLIDER = [ + [3.6, 0, -0.1, -4.8, 0, 0.15, 0.85, 0.45], + [0.6, 0, 0.25, 0.25, -9, 0.85, 0.75, 0.45], + [0.6, 0, 0.25, 0.25, 9, 0.85, 0.75, 0.45], + [-4.2, 0, 0.1, -4.9, 0, 2.4, 0.55, 0.45], + [-4.9, -2.6, 2.4, -4.9, 2.6, 2.4, 0.45, 0.45], +]; +const BUZZ = [ + [1.3, 0, 0, -1.3, 0, 0, 0.6, 0.45], + [0.2, 0, 0.1, -0.3, -3.7, 0.75, 0.8, 0.45], + [0.2, 0, 0.1, -0.3, 3.7, 0.75, 0.8, 0.45], + [-1.2, 0, 0, -2.5, 0, 0.1, 0.45, 0.65], +]; + +// A cumulus as round puffs over a flat base: u across, v up, r, and when in its growth each puff appears. +function puffs() { + const L = []; + const add = (u, v, r, t0) => L.push({ u: u + (rnd() - 0.5) * 0.14, v: v + (rnd() - 0.5) * 0.08, r: r * (0.88 + 0.24 * rnd()), t0, ph: rnd() * TAU }); + for (let i = 0; i < 5; i++) add(-0.76 + i * 0.38, 0.2, 0.34, i === 2 ? 0 : 0.03 + 0.12 * rnd()); + for (let i = 0; i < 4; i++) add(-0.5 + i * 0.33, 0.45, 0.33, 0.08 + 0.17 * rnd()); + for (let i = 0; i < 3; i++) add(-0.3 + i * 0.3, 0.68, 0.3, 0.3 + 0.2 * rnd()); + for (let i = 0; i < 2; i++) add((rnd() - 0.5) * 0.4, 0.86, 0.26, 0.55 + 0.15 * rnd()); + return L; +} +const newCloud = (x, a) => ({ x, a, g: 0, al: 1, det: false, p: puffs(), sd: floor(rnd() * 999) }); + +function buildFields(S) { + const strips = []; + const trees = []; + const us = ((SRCX - 32) * DSRC) / 64; + const hw = (11 * DSRC) / 64; + let D = 5.6; + while (D < 66) { + const D2 = D * (1.22 + 0.12 * rnd()); + const wid = () => ((12 + 18 * rnd()) * pow(6 / D, 0.35) * D) / 64; + let b = []; + for (let u = -0.62 * D2 - rnd() * wid(); u < 0.62 * D2 + 0.3; u += wid()) b.push(u); + const src = D <= DSRC && DSRC < D2; + if (src) { + b = b.filter((u) => u < us - hw - 0.3 || u > us + hw + 0.3); + b.push(us - hw, us + hw); + b.sort((p, q) => p - q); + } + const t = []; + for (let j = 0; j < b.length - 1; j++) { + let k; + do k = PICK[floor(rnd() * PICK.length)]; + while (j && k === t[j - 1]); + t.push(k); + } + if (src) t[b.indexOf(us - hw)] = 5; + const s = { d0: D, d1: D2, b, t, hh: 0.12 + 0.08 * rnd(), i: strips.length }; + strips.push(s); + // Trees stand in the hedge along the near edge of each strip, and at some field corners. + for (let j = 0; j < 2; j++) if (rnd() < 0.6) trees.push({ u: (rnd() - 0.5) * 1.1 * D, D: D + 0.05, rc: 0.2 + 0.14 * rnd() }); + for (let j = 1; j < b.length - 1; j++) if (rnd() < 0.2) trees.push({ u: b[j], D: D + 0.05, rc: 0.2 + 0.12 * rnd() }); + D = D2; + } + for (const tr of trees) tr.hc = tr.rc * 1.5 + 0.15; + trees.sort((p, q) => q.D - p.D); + S.strips = strips; + S.trees = trees; +} + +export function setup(params) { + const S = { + t: 0, + hour: -1, + bg: new Float32Array(N * 3), + buf: new Float32Array(N * 3), + A: new Float32Array(N), + fu: new Float32Array(N), + fd: new Float32Array(N), + fn: new Float32Array(N), + gl: [], + down: [], + clouds: [], + far: [], + shad: [], + spawnG: 0, + spawnB: 0, + cable: null, + started: false, + }; + buildFields(S); + for (let y = FY; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + const D = 180 / (y + 0.5 - HOR); + S.fd[p] = D; + S.fu[p] = ((x + 0.5 - 32) * D) / 64; + S.fn[p] = fbm(x * 0.15, y * 0.3, 11); + } + for (let i = 0; i < 3; i++) S.far.push({ x: 8 + i * 24 + rnd() * 10, yb: 87 - i * 5 - rnd() * 2, hw: 5 + 2.5 * i + rnd() * 2, ht: 3 + i + rnd() * 1.5, p: puffs(), sd: floor(rnd() * 999) }); + S.shad.push({ x: 6, D: 7.4, r: 19, rd: 2.6 }, { x: 50, D: 16, r: 15, rd: 5.5 }); + params.s = S; +} + +// The thermal's centre at a row: it leans downwind with height and wanders a little. +const colX = (S, y) => SRCX + S.lean * (YG - y) + 1.5 * sin(S.t * 0.21); + +function sunLight(h) { + const k = mix(1, 0.9, h); + return [mix(1, 1.16, h) * k, mix(1, 0.86, h) * k, mix(1, 0.56, h) * k]; +} +const SHADE = [0.42, 0.47, 0.6]; +const hazeOf = (h) => mix3([176, 202, 222], [236, 186, 122], h); +function skyAt(x, y, h) { + const top = mix3([14, 50, 150], [30, 52, 128], h); + const mid = mix3([44, 108, 206], [70, 104, 178], h); + const lo = hazeOf(h); + const t = y / 96; + const c = t < 0.62 ? mix3(top, mid, sstep(0, 0.62, t)) : mix3(mid, lo, sstep(0.62, 1, t)); + const gw = pow(h, 1.5) * exp(-x / 24) * exp(-abs(y - 78) / 36); + return [c[0] + 110 * gw, c[1] + 64 * gw, c[2] + 14 * gw]; +} + +// The ground colour seen at a point of the panel: hedges, fields, their shadows, and the haze. +function fieldAt(S, x, y, sun, Lsh, haze) { + const D = 180 / (y - HOR); + const u = ((x - 32) * D) / 64; + const st = S.strips; + let k = 0; + while (k < st.length - 1 && D >= st[k].d1) k++; + const hz = 0.55 * sstep(5, 75, D); + let alb = null; + let lit = 1; + // A hedge on the near edge of a strip hides the ground just behind it. + for (let j = k; j >= 0 && st[j].d0 > D * 0.8; j--) { + const s = st[j]; + const hh = s.hh * (0.7 + 0.6 * noise(u * 2.6, s.i * 9, 3)); + if (y >= HOR + (180 - 64 * hh) / s.d0) { + const n = 0.8 + 0.4 * noise(u * 6, y, 5); + alb = [34 * n, 78 * n, 30 * n]; + lit = 0.85; + break; + } + } + if (!alb) { + const s = st[k]; + const b = s.b; + let j = 0; + while (j < b.length - 2 && u >= b[j + 1]) j++; + const hwu = ((0.8 * pow(6 / D, 0.4) + 0.25) * D) / 64; + if (abs(u - b[j]) < hwu || abs(u - b[j + 1]) < hwu) { + alb = [30, 70, 28]; + lit = 0.7; + } else { + const c = CROP[s.t[j]]; + const tx = c[3] ? 0.8 + 0.34 * (0.5 + 0.5 * sin(u * TAU * 6.5)) : 0.86 + 0.28 * fbm(u * 2.5, D * 0.7, s.t[j] + j); + alb = [c[0] * tx, c[1] * tx, c[2] * tx]; + // Shadows: of the hedge on the field's left, of the hedge in front, and of the trees. + if (u - b[j] < s.hh * Lsh * 0.85 || D - s.d0 < s.hh * Lsh * 0.4) lit = 0; + else + for (const tr of S.trees) { + const du = u - tr.u - tr.hc * Lsh * 0.85; + const dd = D - tr.D - tr.hc * Lsh * 0.45; + if (abs(dd) > 3 + Lsh) continue; + const len = tr.rc * (1 + 0.55 * Lsh); + const al = (du * 0.86 + dd * 0.5) / len; + const pe = (-du * 0.5 + dd * 0.86) / (tr.rc * 1.8); + if (al * al + pe * pe < 1) { + lit = 0; + break; + } + } + } + } + const out = [0, 0, 0]; + for (let c = 0; c < 3; c++) out[c] = mix(alb[c] * mix(SHADE[c], sun[c], lit), haze[c], hz); + return out; +} + +function paint(S) { + const h = S.hour; + const sun = sunLight(h); + const haze = hazeOf(h); + const Lsh = 0.35 + 4.2 * pow(h, 1.6); + const bg = S.bg; + const farT = []; + const nearT = []; + for (let x = 0; x < W; x++) { + farT.push(83 + 5 * fbm(x * 0.045, 1.3, 21)); + nearT.push(91.5 + 3 * fbm(x * 0.09, 4.1, 22) - 1.5 * sstep(0.55, 0.8, noise(x * 0.5, 0, 23))); + } + const ridgeF = mix3(haze, [64, 96, 116], 0.38); + const ridgeN = mix3(haze, [52 * sun[0], 98 * sun[1], 54 * sun[2]], 0.58); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const q = (y * W + x) * 3; + let r = 0; + let g = 0; + let b = 0; + for (let sy = 0; sy < 2; sy++) + for (let sx = 0; sx < 2; sx++) { + const xs = x + 0.25 + 0.5 * sx; + const ys = y + 0.25 + 0.5 * sy; + let c; + if (ys >= FY) c = fieldAt(S, xs, ys, sun, Lsh, haze); + else if (ys >= nearT[x]) { + const m = 0.84 + 0.3 * fbm(x * 0.35, ys * 0.9, 24); + c = [ridgeN[0] * m, ridgeN[1] * m, ridgeN[2] * m]; + } else if (ys >= farT[x]) c = ridgeF; + else c = skyAt(xs, ys, h); + r += c[0]; + g += c[1]; + b += c[2]; + } + bg[q] = r / 4; + bg[q + 1] = g / 4; + bg[q + 2] = b / 4; + } + // Tree crowns, far ones first, lit from the sun's side. + for (const tr of S.trees) { + const cx = 32 + (64 * tr.u) / tr.D; + const cy = HOR + (180 - 64 * tr.hc) / tr.D; + const R = (64 * tr.rc) / tr.D; + if (cx < -R - 1 || cx > W + R + 1 || cy - R > H) continue; + for (let y = max(FY - 4, floor(cy - R - 1)); y <= min(H - 1, cy + R + 1); y++) + for (let x = max(0, floor(cx - R - 1)); x <= min(W - 1, cx + R + 1); x++) { + const dx = (x + 0.5 - cx) / R; + const dy = (y + 0.5 - cy) / R; + const a = clamp((1 - sqrt(dx * dx + dy * dy)) * R + 0.5, 0, 1); + if (a <= 0) continue; + const l = clamp(0.45 - 0.7 * dx - 0.4 * dy, 0, 1); + const n = 0.85 + 0.3 * hash(x, y, 31); + const q = (y * W + x) * 3; + const hz = 0.55 * sstep(5, 75, tr.D); + const al = [40, 94, 38]; + for (let c = 0; c < 3; c++) bg[q + c] += (mix(al[c] * n * mix(SHADE[c], sun[c] * 1.1, l), haze[c], hz) - bg[q + c]) * a; + } + } + S.painted = h; +} + +function addCraft(S, kind, st, X, yc, dir) { + const R = kind ? 5.5 + rnd() : 11.5 + rnd(); + const o = { kind, st, X, yc, Z: -dir * R, R, dir, th: dir > 0 ? -PI / 2 : PI / 2, bank: 0, pitch: 0, off: 0, leave: false, s: 0, w: kind ? 1.75 + 0.3 * rnd() : 1.05, c0: kind ? 1.9 : 2.4 }; + S.gl.push(o); + return o; +} +function launch(S) { + const left = rnd() < 0.5; + const X0 = left ? 4 + rnd() * 10 : 50 + rnd() * 10; + const dir = left ? 1 : -1; + const o = addCraft(S, 0, "launch", X0, 0, dir); + o.yc = 121 + o.Z * SE; + o.X0 = X0; + o.cable = true; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s; + const k = input.knobValues || [0, 2, 0, 0.06]; + dt = clamp(dt || 0, 0, 0.1); + S.hour = clamp(k[0], 0, 1); + const tempo = clamp(k[1], 0.2, 3) / 2; + const kg = clamp(k[2], 1, 4); + S.n = round(kg); + const wind = clamp(k[3], -1, 1); + if (!S.started) { + S.started = true; + S.wind = wind; + S.ks = 1 + 0.14 * (kg - 1); + S.lean = S.wind * 0.22; + const n = S.n; + for (let i = 0; i < n; i++) { + const o = addCraft(S, 0, "climb", 0, n === 1 ? 72 : 82 - (i * 36) / (n - 1), 1); + o.th = rnd() * TAU; + o.bank = 0.5; + } + for (const y of [86, 62]) { + const o = addCraft(S, 1, "climb", 0, y, 1); + o.th = rnd() * TAU; + o.bank = 0.62; + } + for (let i = 0; i < 26 + 10 * 3; i++) S.down.push({ y: YB + rnd() * (YG - YB), a: rnd() * TAU, r: 0.5 + rnd() * 5.5, v: 3 + rnd() * 3, tw: rnd() * TAU }); + const c = newCloud(colX(S, YB), 1.5); + const old = newCloud(colX(S, YB) + (wind < 0 ? -1 : 1) * 24, 26); + old.det = true; + old.al = 0.9; + S.clouds.push(old, c); + } + const d = dt * tempo; + S.t += d; + S.wind += (wind - S.wind) * min(1, dt * 2); + S.ks += (1 + 0.14 * (kg - 1) - S.ks) * min(1, dt * 2); + S.lean = S.wind * 0.22; + const vd = (S.wind < 0 ? -1 : 1) * 2 + 6 * S.wind; + if (input.btnPressed && input.btnPressed[0]) launch(S); + + // The cloud over the thermal builds; after 26 s it drifts off and thins, and a new one starts. + for (const c of S.clouds) { + c.a += d; + c.g = 1 - exp(-c.a / 6.5); + if (!c.det) { + c.x += (colX(S, YB) - c.x) * min(1, d * 0.6); + if (c.a > 26) { + c.det = true; + S.clouds.push(newCloud(colX(S, YB), 0)); + } + } else { + c.x += vd * d; + c.al -= d / 8; + } + } + S.clouds = S.clouds.filter((c) => c.al > 0); + for (const f of S.far) { + f.x += vd * 0.35 * d; + if (f.x > W + f.hw * 1.5) f.x = -f.hw * 1.5; + if (f.x < -f.hw * 1.5) f.x = W + f.hw * 1.5; + } + for (const s of S.shad) { + s.x += vd * 0.9 * d; + if (s.x > W + s.r + 4) s.x = -s.r - 4 - rnd() * 16; + if (s.x < -s.r - 4) s.x = W + s.r + 4 + rnd() * 16; + } + + // Gliders and buzzards. + const top = [YB + 8, YB + 18]; + for (const o of S.gl) { + const cx = colX(S, o.yc) + (o.kind ? 2 : 0); + if (o.st === "launch") { + o.s += d; + const v = 16 * min(1, o.s / 0.5); + o.pitch = o.cable ? 0.95 * sstep(0.5, 1.1, o.s) : max(0, o.pitch - d * 1.2); + o.X += o.dir * v * cos(o.pitch) * d; + o.yc -= v * sin(o.pitch) * d; + if (o.cable && o.yc < 88) { + o.cable = false; + S.cable = { x: o.X + o.dir * 3, y: o.yc - o.Z * SE - 2.5, xw: o.X0 + o.dir * 58, t: 0 }; + } + if (!o.cable && o.pitch <= 0) { + o.st = "climb"; + o.th = o.dir > 0 ? -PI / 2 : PI / 2; + o.off = o.X - cx; + } + } else if (o.st === "climb") { + const th0 = o.th; + o.th += o.w * d; + o.off *= exp(-d * 0.35); + o.yc -= o.c0 * (1 + 0.45 * cos(o.th + 0.8)) * d; + o.bank += ((o.kind ? 0.62 : 0.5) - o.bank) * min(1, d * 2); + if (o.yc < top[o.kind]) o.leave = true; + // A glider that is done straightens up the next time it flies across the panel. + if (o.leave && cos(th0) * cos(o.th) <= 0) { + o.st = "out"; + o.th = sin(o.th) > 0 ? PI / 2 : -PI / 2; + o.dir = -sin(o.th) > 0 ? 1 : -1; + } + o.X = cx + o.off + o.R * cos(o.th); + o.Z = o.R * sin(o.th); + } else { + const v = o.R * o.w; + o.X += o.dir * v * d; + o.yc += (o.st === "out" ? 0.8 : 0.4) * d; + o.bank *= exp(-d * 3); + if (o.st === "in" && o.dir * (o.X - cx) >= 0) o.st = "climb"; + if (o.X < -24 || o.X > W + 24) o.gone = true; + } + } + S.gl = S.gl.filter((o) => !o.gone); + // Keep the wanted number in the thermal: the highest leaves early, or a newcomer glides in low. + for (const kind of [0, 1]) { + const want = kind ? 2 : S.n; + const act = S.gl.filter((o) => o.kind === kind && o.st !== "out"); + const key = kind ? "spawnB" : "spawnG"; + S[key] -= d; + if (act.length < want && S[key] <= 0) { + const dir = rnd() < 0.5 ? 1 : -1; + addCraft(S, kind, "in", dir > 0 ? -14 : W + 14, (kind ? 84 : 86) + rnd() * 6, dir); + S[key] = kind ? 1.5 + rnd() * 2 : 2.5; + } else if (act.length > want) { + const hi = act.filter((o) => o.st === "climb" && !o.leave).sort((p, q) => p.yc - q.yc)[0]; + if (hi) hi.leave = true; + } + } + if (S.cable) { + S.cable.t += d; + S.cable.y += 9 * d; + if (S.cable.t > 2) S.cable = null; + } + // Thistledown rides the thermal up from the ploughed field. + for (const p of S.down) { + p.y -= p.v * d; + p.a += (1.6 / (1 + p.r * 0.3)) * d; + if (p.y < YB + 1) { + p.y = YG - 3 + rnd() * 5; + p.r = 0.5 + rnd() * 5.5; + } + } +} + +// A tapered stroke into the coverage buffer, keeping the larger value, and growing the box drawn into. +function stroke(S, ax, ay, bx, by, ra, rb) { + const A = S.A; + const x0 = max(0, floor(min(ax, bx) - 2)); + const x1 = min(W - 1, Math.ceil(max(ax, bx) + 2)); + const y0 = max(0, floor(min(ay, by) - 2)); + const y1 = min(H - 1, Math.ceil(max(ay, by) + 2)); + if (x0 > x1 || y0 > y1) return; + const bb = S.bb; + bb[0] = min(bb[0], x0); + bb[1] = min(bb[1], y0); + bb[2] = max(bb[2], x1); + bb[3] = max(bb[3], y1); + const dx = bx - ax; + const dy = by - ay; + const ll = dx * dx + dy * dy || 1; + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const px = x + 0.5 - ax; + const py = y + 0.5 - ay; + const t = clamp((px * dx + py * dy) / ll, 0, 1); + const ex = px - dx * t; + const ey = py - dy * t; + const a = clamp(ra + (rb - ra) * t + 0.5 - sqrt(ex * ex + ey * ey), 0, 1); + const i = y * W + x; + if (a > A[i]) A[i] = a; + } +} +function flush(S, col, alpha) { + const { A, buf, bb } = S; + for (let y = bb[1]; y <= bb[3]; y++) + for (let x = bb[0]; x <= bb[2]; x++) { + const i = y * W + x; + const a = min(1, A[i] * alpha); + if (a <= 0) continue; + A[i] = 0; + const q = i * 3; + buf[q] += (col[0] - buf[q]) * a; + buf[q + 1] += (col[1] - buf[q + 1]) * a; + buf[q + 2] += (col[2] - buf[q + 2]) * a; + } + bb[0] = bb[1] = 1e9; + bb[2] = bb[3] = -1; +} + +// A craft in its own frame: heading, pitch and bank, seen from below and to the side. +function craft(S, o, h) { + const hx = o.st === "climb" ? -sin(o.th) : o.dir; + const hz = o.st === "climb" ? cos(o.th) : 0; + const cp = cos(o.pitch); + const sp = sin(o.pitch); + const F = [hx * cp, sp, hz * cp]; + const U0 = [-hx * sp, cp, -hz * sp]; + const R0 = [-hz, 0, hx]; + const cb = cos(o.bank * 0.7); + const sb = sin(o.bank * 0.7); + const U = [U0[0] * cb + R0[0] * sb, U0[1] * cb, U0[2] * cb + R0[2] * sb]; + const R = [R0[0] * cb - U0[0] * sb, -U0[1] * sb, R0[2] * cb - U0[2] * sb]; + const sx = o.X; + const sy = o.yc - o.Z * SE; + const P = (f, r, u) => { + const wx = F[0] * f + R[0] * r + U[0] * u; + const wy = F[1] * f + R[1] * r + U[1] * u; + const wz = F[2] * f + R[2] * r + U[2] * u; + return [sx + wx, sy - (wy * BC + wz * BE)]; + }; + for (const m of o.kind ? BUZZ : GLIDER) { + const a = P(m[0], m[1], m[2]); + const b = P(m[3], m[4], m[5]); + stroke(S, a[0], a[1], b[0], b[1], m[6], m[7]); + } + // From below we see the shaded underside; banked towards us, the sunlit top. + const top = sstep(-0.3, 0.4, -U[1] * BE + U[2] * BC); + if (o.kind) { + flush(S, [mix(58, 86, top), mix(42, 62, top), mix(30, 40, top)], 1); + return; + } + const lit = mix3([236, 240, 246], [255, 226, 176], h); + const f = 0.84 + 0.16 * top; + flush(S, [lit[0] * f, lit[1] * f, lit[2] * f], 1.35); + // Once a turn, the wing tilts to the sun and flashes. + const gx = mix(-0.35, -0.8, h); + const gz = mix(1, 0.6, h); + const gl = (sstep(0.92, 1, (R0[0] * gx + R0[2] * gz) / sqrt(gx * gx + gz * gz) * Math.sign(o.bank || 1)) * abs(o.bank)) / 0.5; + if (gl > 0.02) { + const w = P(0.4, 3.5 * Math.sign(o.bank || 1), 0.6); + for (let y = max(0, floor(w[1] - 5)); y <= min(H - 1, w[1] + 5); y++) + for (let x = max(0, floor(w[0] - 5)); x <= min(W - 1, w[0] + 5); x++) { + const ex = abs(x + 0.5 - w[0]); + const ey = abs(y + 0.5 - w[1]); + const v = gl * (exp(-sqrt(ex * ex + ey * ey) / 1.3) + 0.5 * exp(-min(ex, ey) * 2.5 - max(ex, ey) / 2.5)); + const q = (y * W + x) * 3; + S.buf[q] += 255 * v; + S.buf[q + 1] += 246 * v; + S.buf[q + 2] += mix(225, 170, h) * v; + } + } +} + +function drawCloud(S, c, hw, ht, g, yb, al, haze) { + const h = S.hour; + const t = S.t; + const buf = S.buf; + const B = []; + const lean = S.lean * 0.6; + let x0 = 1e9; + let x1 = -1e9; + let y0 = 1e9; + for (const p of c.p) { + const gr = sstep(p.t0, p.t0 + 0.25, g) * (c.det ? sqrt(c.al) : 1); + if (gr <= 0.02) continue; + const r = p.r * hw * gr * (1 + 0.05 * sin(t * 0.7 + p.ph)); + const bx = c.x + p.u * hw + lean * p.v * ht + 0.04 * hw * sin(t * 0.5 + p.ph); + const by = yb - p.v * ht - 0.03 * ht * sin(t * 0.4 + p.ph * 2); + B.push(bx, by, r); + x0 = min(x0, bx - r); + x1 = max(x1, bx + r); + y0 = min(y0, by - r); + } + if (!B.length) return; + const L = [mix(-0.45, -0.85, h), mix(-0.7, -0.28, h), 0.5]; + const ll = sqrt(L[0] * L[0] + L[1] * L[1] + L[2] * L[2]); + const dark = 1 - 0.2 * g * (c.det ? 0.5 : 1); + const litC = mix3([252, 252, 250], [255, 214, 150], h); + const shC = mix3([150, 162, 192], [150, 124, 146], h).map((v) => v * dark); + const baseC = mix3([126, 132, 152], [108, 90, 100], h).map((v) => v * mix(1, 0.78, g)); + const hz = hazeOf(h); + for (let y = max(0, floor(y0 - 2)); y <= min(H - 1, yb + 1); y++) + for (let x = max(0, floor(x0 - 2)); x <= min(W - 1, x1 + 2); x++) { + const px = x + 0.5; + const py = y + 0.5; + let best = -9; + let bi = 0; + for (let i = 0; i < B.length; i += 3) { + const dx = px - B[i]; + const dy = py - B[i + 1]; + const v = 1 - sqrt(dx * dx + dy * dy) / B[i + 2]; + if (v > best) { + best = v; + bi = i; + } + } + if (best < -0.4) continue; + const r = B[bi + 2]; + const dd = best + 0.32 * (noise(px * 0.32, py * 0.32 - t * 0.12, c.sd) - 0.5); + let a = clamp(dd * r * 0.7, 0, 1) * clamp(yb + 0.7 - py + 0.6 * noise(px * 0.4, 1, c.sd), 0, 1) * al; + if (a <= 0) continue; + const nx = (px - B[bi]) / r; + const ny = (py - B[bi + 1]) / r; + const nz = sqrt(max(0, 1 - nx * nx - ny * ny)); + let lw = clamp(((nx * L[0] + ny * L[1] + nz * L[2]) / ll + 0.35) / 1.35, 0, 1); + lw *= 0.55 + 0.45 * clamp((yb - py) / (0.45 * ht), 0, 1); + const bs = sstep(yb - 2.6, yb - 0.6, py); + const q = (y * W + x) * 3; + for (let k = 0; k < 3; k++) { + let v = mix(mix(shC[k], litC[k], lw), baseC[k], bs); + v = mix(v, hz[k], haze); + buf[q + k] += (v - buf[q + k]) * a; + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.started) return; + if (S.painted === undefined || abs(S.painted - S.hour) > 0.003) paint(S); + const h = S.hour; + const { buf, bg } = S; + buf.set(bg); + S.bb = [1e9, 1e9, -1, -1]; + + // Cloud shadows on the fields: the thermal's cloud over its field, pushed off by the sun, and two from clouds out of sight. + const sh = []; + for (const c of S.clouds) { + const hw = (8 + 18 * c.g) * S.ks; + const ru = ((hw * DSRC) / 64) * 0.95 * (c.det ? sqrt(c.al) : 1); + sh.push(((c.x - 32 + 3 + 30 * h) * DSRC) / 64, DSRC * (1.04 + 1.1 * h), ru, ru * 1.3 + 0.5, c.al * (0.55 + 0.45 * c.g)); + } + for (const s of S.shad) sh.push(((s.x - 32) * s.D) / 64, s.D, (s.r * s.D) / 64, s.rd, 1); + const sc = [0.46, 0.5, 0.62]; + for (let p = FY * W; p < N; p++) { + const u = S.fu[p]; + const D = S.fd[p]; + let m = 0; + for (let i = 0; i < sh.length; i += 5) { + const du = (u - sh[i]) / sh[i + 2]; + const dv = (D - sh[i + 1]) / sh[i + 3]; + const q = du * du + dv * dv; + if (q > 2.2) continue; + m = max(m, sstep(1.05, 0.6, sqrt(q) + 0.35 * (S.fn[p] - 0.5)) * sh[i + 4]); + } + if (m > 0) { + const q = p * 3; + buf[q] *= mix(1, sc[0], m); + buf[q + 1] *= mix(1, sc[1], m); + buf[q + 2] *= mix(1, sc[2], m); + } + } + + // Far cumulus along the horizon, then the clouds the thermal builds. + for (const f of S.far) drawCloud(S, f, f.hw, f.ht, 1, f.yb, 0.9, 0.5); + for (const c of S.clouds) drawCloud(S, c, (8 + 18 * c.g) * S.ks, (6 + 26 * c.g) * S.ks, c.g, YB, c.det ? min(1, c.al * 1.4) : 1, 0); + + // Thistledown catching the sun as it rises. + const nd = 26 + round(10 * (S.ks - 1) / 0.14); + const dc = mix3([240, 240, 228], [255, 222, 160], h); + for (let i = 0; i < nd && i < S.down.length; i++) { + const p = S.down[i]; + const x = round(colX(S, p.y) + p.r * cos(p.a) + 0.8 * sin(S.t * 1.7 + p.tw) - 0.5); + const y = round(p.y + p.r * sin(p.a) * SE - 0.5); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const a = (0.45 + 0.4 * sin(S.t * 3 + p.tw * 5)) * sstep(YB, YB + 8, p.y) * sstep(YG + 2, YG - 6, p.y); + const q = (y * W + x) * 3; + for (let k = 0; k < 3; k++) buf[q + k] += (dc[k] - buf[q + k]) * a; + } + + // The winch cable, and the cable that falls when the glider lets go. + for (const o of S.gl) + if (o.st === "launch" && o.cable) { + stroke(S, o.X + o.dir * 3 * cos(o.pitch), o.yc - o.Z * SE - 3 * sin(o.pitch), o.X0 + o.dir * 58, 118, 0.18, 0.18); + flush(S, [36, 34, 38], 0.75); + } + if (S.cable) { + const c = S.cable; + stroke(S, c.x, c.y, c.xw, 118, 0.18, 0.18); + flush(S, [36, 34, 38], 0.75 * (1 - c.t / 2)); + stroke(S, c.x, c.y - 1, c.x, c.y - 1, 0.7, 0.7); + flush(S, [230, 50, 40], 1 - c.t / 2); + } + // Gliders and buzzards, far side of their circles first. + const order = S.gl.slice().sort((p, q) => p.Z - q.Z); + for (const o of order) craft(S, o, h); + + for (let p = 0; p < N; p++) { + const q = p * 3; + display.setPixel(p % W, (p / W) | 0, min(255, buf[q]) | 0, min(255, buf[q + 1]) | 0, min(255, buf[q + 2]) | 0); + } +} From 4dc1dbfb9198291446e0ee21c67d549af49d24a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:24:17 +0000 Subject: [PATCH 07/33] pipe-organ: first running version --- incoming/pattern-study/patterns/pipe-organ.js | 448 ++++++++++++++++++ 1 file changed, 448 insertions(+) create mode 100644 incoming/pattern-study/patterns/pipe-organ.js diff --git a/incoming/pattern-study/patterns/pipe-organ.js b/incoming/pattern-study/patterns/pipe-organ.js new file mode 100644 index 000000000..06b9f8373 --- /dev/null +++ b/incoming/pattern-study/patterns/pipe-organ.js @@ -0,0 +1,448 @@ +// ===== Patternflow pattern ===== +// Title: Pipe Organ +// Family: music +// Idea: The tin front pipes of a church organ case stand in towers and flats while an unseen organist plays a chorale, and each pipe that speaks lights at its mouth with a standing wave shimmering up its body, the bass in the tall pipes and the tune in the small ones, as light from a rose window drifts across the case. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 24, after the Baroque organ case of towers and flats (Arp Schnitger's Hamburg cases) and four-part chorale harmony +// Round: 24 +// =============================== +// @matrix 64x128 +// @knobs Stops=0..1, Tempo=0.2..3, Voices=1..6, Tremulant=0..1 +// +// Knob 1: Stops. The registration: flutes at 0 glow soft blue with one broad band, principals in the middle speak silver-white and add the octave above each note, and the reeds at 1 burn gold at the mouth and red up the body with many bands and the fifteenth as well. +// Knob 2: Tempo. Chords a second over two: at 2 a chord takes a second and a phrase with its held last chord about ten, at 0.2 one chord holds for ten seconds. +// Knob 3: Voices. How many parts the hands play over the pedal: 1 is the tune alone over the bass, 3 the full four-part chorale, and 4 to 6 fill the chord with more pipes. +// Knob 4: Tremulant. The wind shakes: the shimmer in the pipes wavers and throbs, and the two candles on the case flicker until the whole case flickers with them. +// Buttons: K1 tap is full organ: every pipe in the case speaks at once for a few seconds. + +const W = 64; +const H = 128; +const N = W * H; +const { min, max, floor, round, sin, cos, abs, exp, sqrt, atan2, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +const mix = (a, b, t) => a + (b - a) * t; +const 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; +} + +// Pipe d sounds the d-th note of a major scale up from the longest pipe, and +// an open pipe halves its length each octave. +const STEP = [0, 2, 4, 5, 7, 9, 11]; +const len = (d) => 92 * 2 ** (-(12 * floor(d / 7) + STEP[d % 7]) / 12); +// The front: the lowest note in the middle of the centre tower, then pairs, +// the lower note left and the next one right, as the C and C sharp chests of +// a real case split a scale between the two sides. +const PIPES = []; +function pair(d, x, w, top, mouth, base) { + const L = d ? (len(d) + len(d + 1)) / 2 : len(0); + const t = round(top >= 0 ? top : mouth - L); + const m = round(top >= 0 ? top + L : mouth); + const mh = w >= 5 ? 2 : 1; + PIPES[d] = { x, w, top: t, m, mh, f0: m + mh + 1, base }; + if (d) PIPES[d + 1] = { x: W - x - w, w, top: t, m, mh, f0: m + mh + 1, base }; +} +// The centre tower and the side towers hang from their crowns, so their +// mouths step; the flats stand on equal feet, so their tops ramp. +pair(0, 29, 6, 9, 0, 110); +pair(1, 23, 5, 12, 0, 110); +pair(3, 1, 4, 30, 0, 110); +pair(5, 6, 4, 38, 0, 110); +pair(7, 19, 3, -1, 102, 110); +pair(9, 15, 3, -1, 102, 110); +pair(11, 11, 3, -1, 102, 110); +pair(13, 19, 3, -1, 44, 50); +pair(15, 15, 3, -1, 44, 50); +pair(17, 11, 3, -1, 44, 50); +const NP = 19; + +// Eight-chord phrases on the scale's chords, I = 0 to vi = 5. The first one +// always opens the piece. +const PHRASES = [ + [0, 4, 5, 3, 0, 1, 4, 0], + [0, 5, 1, 4, 0, 3, 4, 0], + [5, 3, 0, 4, 5, 1, 4, 4], + [0, 3, 0, 4, 5, 1, 4, 0], + [3, 4, 0, 5, 1, 0, 4, 0], + [0, 2, 5, 3, 1, 4, 4, 0], +]; +// Voices in the order the knob adds them: soprano, bass, alto, tenor, then +// three that fill the chord. +const VORD = [0, 3, 1, 2, 4, 5, 6]; + +// Surfaces, as how much of each light they give back. +const TIN = [0.9, 0.95, 1]; +const WOOD = [0.42, 0.25, 0.15]; +const GILT = [1.3, 1, 0.42]; +const DARK = [0.07, 0.05, 0.045]; +const STONE = [0.9, 0.88, 0.84]; +// Grey daylight from the nave, the candles, and the lamp on the music desk. +const DAY = [56, 56, 64]; +const CAN = [200, 126, 58]; +const LAMP = [255, 210, 140]; +// The rose window's glass. +const RUBY = [230, 40, 60]; +const SAPH = [50, 90, 255]; +const AMBER = [255, 200, 80]; +const GREEN = [60, 200, 110]; +const ROSE = [RUBY, SAPH, GREEN, SAPH]; +// Stop colours: flute, principal, reed at the mouth and up the body, full organ. +const FLUTE = [0.35, 0.62, 1]; +const PRIN = [0.84, 0.9, 1]; +const REED0 = [1, 0.77, 0.27]; +const REED1 = [1, 0.28, 0.14]; +const FULL = [1, 0.86, 0.58]; + +// Round tin: a highlight a third of the way across and a dark far edge. +function shade(p) { + return (0.2 + 0.5 * sin(PI * p) ** 0.7 + 0.55 * exp(-(((p - 0.36) / 0.14) ** 2))) * (1.1 - 0.4 * p); +} + +// Carved and gilt pipe shades: pierced scrolls. +function fret(x, y) { + return sin(x * 1.25 + 2 * sin(y * 0.52)) * sin(y * 0.9 + 1.6 * sin(x * 0.61)); +} + +function build(P) { + const A = P.A; + const put = (x, y, c, k = 1, a = 1) => { + if (x < 0 || x >= W || y < 0 || y >= H || a <= 0) return; + const o = (y * W + x) * 3; + for (let n = 0; n < 3; n++) A[o + n] = mix(A[o + n], c[n] * k, a); + }; + // Left half and its mirror. + const two = (x, y, c, k, a) => (put(x, y, c, k, a), put(W - 1 - x, y, c, k, a)); + const row = (x0, x1, y, c, k) => { + for (let x = x0; x <= x1; x++) two(x, y, c, k); + }; + // The church wall: courses of ashlar. + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const cs = floor(y / 5), off = (cs & 1) * 4, mortar = y % 5 === 4 || (x + off) % 8 === 7; + put(x, y, STONE, mortar ? 0.14 : 0.27 + 0.1 * hash(floor((x + off) / 8), cs)); + } + // The case, dark inside. + for (let y = 0; y < 114; y++) for (let x = 0; x < 32; x++) if (x > 21 || (x > 9 ? y >= 11 : y >= 22)) two(x, y, DARK, 1); + // Stiles between the towers and the flats. + for (const [x, y0] of [[0, 30], [10, 16], [22, 9]]) for (let y = y0; y < 110; y++) two(x, y, WOOD, 1.05 + 0.15 * sin(y * 0.8) * (y % 12 < 2 ? 1 : 0)); + // The side towers' crowns, each with a gilt urn and a flame on top. + row(0, 10, 29, GILT, 0.9); + for (let y = 26; y <= 28; y++) row(0, 10, y, WOOD, 0.9); + for (const x of [2, 5, 8]) two(x, 27, GILT, 1.05); + row(0, 10, 25, GILT, 1.05); + row(0, 10, 24, WOOD, 0.7); + row(0, 10, 23, WOOD, 1); + row(0, 10, 22, GILT, 1.15); + const URN = [[21, 2], [20, 1], [19, 2], [18, 2], [17, 1], [16, 0], [15, 1], [14, 0]]; + for (const [y, r] of URN) for (let x = 5 - r; x <= 5 + r; x++) two(x, y, GILT, 1.15 - 0.18 * (x - 5 + r)); + // The upper flats' cornice and a scroll pediment. + row(10, 22, 15, GILT, 0.9); + for (let y = 12; y <= 14; y++) for (let x = 10; x <= 22; x++) two(x, y, WOOD, y === 13 && x % 2 ? 1.25 : 0.9); + row(10, 22, 11, GILT, 1.1); + row(12, 20, 10, GILT, 0.75); + for (const [x, y] of [[12, 9], [13, 9], [19, 9], [20, 9], [14, 8], [15, 8], [17, 8], [18, 8], [16, 7], [16, 6]]) two(x, y, GILT, 1.05); + // The centre tower's crown, with a carved crest over it. + row(22, 31, 8, GILT, 0.95); + for (let y = 5; y <= 7; y++) for (let x = 22; x <= 31; x++) two(x, y, x % 4 === 1 && y === 6 ? GILT : WOOD, x % 4 === 1 && y === 6 ? 1.1 : 0.9); + row(22, 31, 4, GILT, 1.1); + for (let x = 23; x <= 31; x++) { + const h = 4 * (1 - ((x - 31.5) / 9) ** 2); + for (let y = floor(4 - h); y < 4; y++) if (fret(x, y + 3) > -0.2 || x === 31) two(x, y, GILT, x === 31 ? 1.25 : 0.8 + 0.3 * fret(x, y)); + } + // Pipe shades over every pipe that stops short of the carving above it. + const GAP = [5, 14, 18, 28, 35, 45, 49, 58]; + for (const q of PIPES) { + const y0 = q.base === 50 ? 16 : q.x === 1 || q.x === 6 || q.x > 53 ? 30 : q.x > 22 && q.x < 41 ? 9 : 54; + for (let x = q.x - 1; x <= q.x + q.w; x++) { + const own = x >= q.x && x < q.x + q.w; + if (own || GAP.indexOf(x) >= 0) + for (let y = y0; y < q.top; y++) { + const v = fret(x, y); + if (own && y === q.top - 1) put(x, y, GILT, 1.05); + else if (v > -0.1) put(x, y, GILT, 0.55 + 0.5 * v); + } + } + } + // Under the lower flats the pipe shades of the upper ones, and the shelf they stand on. + row(10, 22, 50, GILT, 0.95); + row(10, 22, 51, WOOD, 1); + row(10, 22, 52, WOOD, 0.9); + row(10, 22, 53, WOOD, 0.55); + // The pipes: body, mouth with its lips, and the conical foot. + for (const q of PIPES) { + const { x, w, top, m, mh, f0, base } = q; + for (let y = top; y < m; y++) for (let j = 0; j < w; j++) put(x + j, y, TIN, shade((j + 0.5) / w) * (y === top ? 0.7 : 1)); + for (let j = 0; j < w; j++) { + const inner = j > 0 && j < w - 1, s = shade((j + 0.5) / w); + if (inner) put(x + j, m - 1, TIN, s * 1.3); + for (let y = m; y < m + mh; y++) put(x + j, y, inner ? DARK : TIN, inner ? 0.8 : s); + put(x + j, m + mh, TIN, s * (inner ? 1.15 : 0.9)); + } + if (w >= 5) for (let j = floor((w - 1) / 2); j <= floor(w / 2); j++) put(x + j, m - 2, TIN, 1.25); + const cx = x + w / 2; + for (let y = f0; y < base; y++) { + const hw = mix(w / 2, 0.7, (y - f0) / max(1, base - 1 - f0)); + for (let xx = floor(cx - hw); xx <= cx + hw; xx++) { + const cov = cl(hw - abs(xx + 0.5 - cx) + 0.5, 0, 1); + put(xx, y, TIN, 0.9 * shade(cl((xx + 0.5 - cx + hw) / (2 * hw), 0, 1)), cov); + } + } + } + // The impost the pipes stand on. + for (let x = 0; x < W; x++) { + put(x, 110, GILT, 1); + put(x, 111, WOOD, x % 16 === 0 ? 0.6 : 1); + put(x, 112, WOOD, x % 16 === 0 ? 0.6 : 0.95); + put(x, 113, WOOD, 0.5); + } + // Candles on the stiles between the side towers and the flats. + for (let y = 98; y <= 101; y++) two(10, y, [0.95, 0.9, 0.8], 1); + for (let x = 9; x <= 11; x++) two(x, 102, GILT, 1.1); + // The console: the music desk with its lamp, and one manual. + for (let y = 114; y < H; y++) for (let x = 0; x < W; x++) put(x, y, WOOD, 0.7); + for (let y = 114; y <= 119; y++) for (let x = 16; x <= 47; x++) put(x, y, WOOD, 0.85); + for (let y = 115; y <= 119; y++) + for (let x = 21; x <= 42; x++) put(x, y, [0.95, 0.9, 0.76], y === 116 || y === 118 ? 0.62 : hash(x, y) < 0.13 ? 0.3 : 1); + for (let x = 27; x <= 36; x++) put(x, 114, GILT, 1.15); + for (let k = 0; k < 21; k++) { + for (let y = 121; y <= 125; y++) for (let x = 3 * k; x < 3 * k + 2; x++) put(x, y, [0.92, 0.88, 0.78], y === 125 ? 0.7 : 1); + const n = (k + 6) % 7; + if (k < 20 && n !== 2 && n !== 6) for (let y = 121; y <= 123; y++) put(3 * k + 2, y, DARK, 1), put(3 * k + 3, y, DARK, 1); + } + // Light: grey daylight from above, warm candlelight, the desk lamp. + for (let i = 0; i < N; i++) { + const x = (i & 63) + 0.5, y = (i >> 6) + 0.5; + P.day[i] = 0.45 + 0.55 * (1 - y / H) ** 1.3; + P.cL[i] = 0.85 / (1 + ((x - 10.5) ** 2 + (y - 96) ** 2) / 450); + P.cR[i] = 0.85 / (1 + ((x - 53.5) ** 2 + (y - 96) ** 2) / 450); + P.lamp[i] = y > 115 ? 1.1 / (1 + (x - 32) ** 2 / 90 + (y - 115) ** 2 / 40) : 0.45 / (1 + ((x - 32) ** 2 + (y - 114) ** 2) / 140); + } +} + +// The nearest scale degree in lo..hi under cap whose note is in pcs, moving +// from prev, sitting still costing `same`. +function near(lo, hi, pcs, prev, same, cap) { + let best = -1; + let bs = 1e9; + for (let d = lo; d <= hi; d++) { + if (d >= cap || pcs.indexOf(d % 7) < 0) continue; + const j = abs(d - prev), s = j + (j === 0 ? same : 0) + (j > 4 ? 2 : 0) + rnd() * 0.9; + if (s < bs) (bs = s), (best = d); + } + return best < 0 ? (cap < 99 ? near(lo, hi, pcs, prev, same, 99) : prev) : best; +} + +// The next chord of the chorale, voiced from the last one. +function chord(P) { + if (P.pi >= P.ph.length) (P.ph = PHRASES[floor(rnd() * PHRASES.length)]), (P.pi = 0); + const r = P.ph[P.pi++], last = P.pi === P.ph.length, pen = P.pi === P.ph.length - 1; + const T = [r, (r + 2) % 7, (r + 4) % 7]; + if (pen && r === 4 && rnd() < 0.6) T.push(3); + const [s0, a0, t0, b0] = P.prev; + const B = near(0, 7, [!last && !pen && rnd() < 0.3 ? T[1] : r], b0, 0, 99); + const S = near(11, 18, last && r === 0 ? [0] : T, s0, 1.6, 99); + const A = near(8, 14, T, a0, 0.4, S); + const miss = T.filter((c) => c !== S % 7 && c !== A % 7 && c !== B % 7); + const Tn = near(4, 11, miss.length ? miss : T, t0, 0.4, A); + const v = [S, A, Tn, B]; + // Three more parts fill the chord: the bass an octave up, one in the middle, one high. + for (const goal of [B + 7, 10, 16]) { + let best = -1; + let bs = 1e9; + for (let d = 0; d < NP; d++) { + const s = abs(d - goal) + rnd() * 0.5; + if (T.indexOf(d % 7) >= 0 && v.indexOf(d) < 0 && s < bs) (bs = s), (best = d); + } + v.push(best); + } + P.prev = v; + return { v, dur: last ? 2 : 1, last, pass: [-1, -1, -1, -1], at: 0 }; +} + +// A part moving a third walks through the note between on the half beat. +function passing(c, nx) { + if (c.last) return; + for (let k = 0; k < 4; k++) if (abs(nx.v[k] - c.v[k]) === 2 && rnd() < (k ? 0.4 : 0.6)) c.pass[k] = (c.v[k] + nx.v[k]) / 2; +} + +export function setup(params) { + const F32 = (n) => new Float32Array(n); + const P = (params.s = { + A: F32(N * 3), F: F32(N * 3), day: F32(N), cL: F32(N), cR: F32(N), lamp: F32(N), + amp: F32(NP), tgt: F32(NP), on: F32(NP), chiff: F32(NP), keys: F32(NP), + t: 0, beat: 0, fullT: 99, full: 0, fl: 1, fr: 1, s: 0, tr: 0, + ph1: rnd() * 6.28, ph2: rnd() * 6.28, ph: PHRASES[0], pi: 0, prev: [14, 11, 7, 0, 7, 9, 16], + }); + build(P); + P.cur = chord(P); + P.nx = chord(P); + passing(P.cur, P.nx); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s; + const k = input.knobValues || [0, 2, 0, 0.06]; + const d = cl(dt || 0, 0, 0.1); + const s = (P.s = cl(k[0], 0, 1)); + const tempo = cl(k[1], 0.2, 3); + const nv = cl(round(k[2]), 1, 6); + const tr = (P.tr = cl(k[3], 0, 1)); + const t = (P.t += d); + P.beat += (d * tempo) / 2; + while (P.beat >= P.cur.at + P.cur.dur) { + const at = P.cur.at + P.cur.dur; + P.cur = P.nx; + P.cur.at = at; + P.nx = chord(P); + passing(P.cur, P.nx); + } + if (input.btnPressed && input.btnPressed[0]) P.fullT = 0; + P.fullT += d; + P.full = P.fullT < 2.8 ? sm(0, 0.06, P.fullT) : 1 - sm(2.8, 3.6, P.fullT); + + // Which pipes speak: each part's note, the octave above from the + // principals on, two octaves above with the reeds. + const c = P.cur, pos = P.beat - c.at, r4 = 0.8 * sm(0.3, 0.55, s), r2 = 0.65 * sm(0.7, 0.92, s); + P.tgt.fill(0); + P.keys.fill(0); + if (!(c.last && pos > c.dur - 0.22)) + for (let n = 0; n <= nv; n++) { + const v = VORD[n], g = v < 4 && c.pass[v] >= 0 && pos >= c.dur / 2 ? c.pass[v] : c.v[v]; + if (g < 0) continue; + P.tgt[g] = 1; + if (v !== 3) P.keys[g] = 1; + if (g + 7 < NP) P.tgt[g + 7] = max(P.tgt[g + 7], r4); + if (g + 11 < NP) P.tgt[g + 11] = max(P.tgt[g + 11], r2); + if (g + 14 < NP) P.tgt[g + 14] = max(P.tgt[g + 14], r2); + } + for (let i = 0; i < NP; i++) { + const g = max(P.tgt[i], 0.9 * P.full); + if (g > 0.5 && P.on[i] < 0.5) P.chiff[i] = 1; + P.on[i] = g; + P.amp[i] += (g - P.amp[i]) * (1 - exp(-d / (g > P.amp[i] ? 0.035 : 0.14))); + P.chiff[i] *= exp(-d / 0.18); + } + // Candles in the draught: they flicker harder as the tremulant shakes the wind. + const fa = 0.06 + 0.8 * tr; + const fl = (ph) => 1 - 0.35 * fa + fa * (0.6 * sin(t * 7.1 + ph) + 0.45 * sin(t * 11.9 + 2 * ph) + 0.35 * sin(t * 4.3 + 3 * ph) * sin(t * 17.3)); + P.fl = cl(fl(0), 0.1, 2); + P.fr = cl(fl(2.1), 0.1, 2); +} + +// A stop's colour, u from the mouth (0) to the top (1). +function stopCol(out, s, u, full) { + for (let n = 0; n < 3; n++) { + const v = s < 0.5 ? mix(FLUTE[n], PRIN[n], s * 2) : mix(PRIN[n], mix(REED0[n], REED1[n], u), s * 2 - 1); + out[n] = mix(v, FULL[n], full); + } +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const { A, F, day, cL, cR, lamp, t } = P; + // The case in daylight, candlelight and lamplight. + for (let i = 0, o = 0; i < N; i++, o += 3) { + const dl = day[i], cn = cL[i] * P.fl + cR[i] * P.fr, lp = lamp[i]; + for (let n = 0; n < 3; n++) F[o + n] = A[o + n] * (DAY[n] * dl + CAN[n] * cn + LAMP[n] * lp); + } + // The rose window's light, cast long and slow across the case. + const px = 32 + 17 * sin(t * 0.05 + P.ph1), py = 60 + 34 * sin(t * 0.031 + P.ph2), RX = 15, RY = 24; + const I0 = 0.65 * (0.8 + 0.2 * sin(t * 0.23 + P.ph1)); + for (let y = max(0, floor(py - RY)); y <= min(H - 1, py + RY); y++) + for (let x = max(0, floor(px - RX)); x <= min(W - 1, px + RX); x++) { + const dx = (x + 0.5 - px) / RX, dy = (y + 0.5 - py) / RY, r = sqrt(dx * dx + dy * dy); + if (r >= 1) continue; + const a = atan2(dy, dx) / (2 * PI) + 0.5; + let c, lead; + if (r < 0.2) (c = AMBER), (lead = r > 0.15); + else if (r < 0.5) { + const q = a * 8; + (c = floor(q) & 1 ? RUBY : SAPH), (lead = q - floor(q) < 0.1 || r < 0.24); + } else { + const q = a * 16; + (c = ROSE[floor(q) & 3]), (lead = q - floor(q) < 0.08 || r < 0.54); + } + const kk = I0 * (lead ? 0.45 : 1) * sm(1, 0.82, r), o = (y * W + x) * 3; + for (let n = 0; n < 3; n++) F[o + n] += A[o + n] * c[n] * kk; + } + // The candle flames. + for (const [x, f] of [[10, P.fl], [53, P.fr]]) { + const add = (xx, y, c, k) => { + const o = (y * W + xx) * 3; + for (let n = 0; n < 3; n++) F[o + n] += c[n] * k; + }; + add(x, 97, [255, 235, 170], min(1.2, f)); + add(x, 96, [255, 160, 60], min(1.2, f)); + add(x, 95, [200, 80, 20], cl(f - 0.9, 0, 1)); + add(x - 1, 97, [140, 70, 20], 0.5 * f); + add(x + 1, 97, [140, 70, 20], 0.5 * f); + } + // The pipes that speak: the mouth lights and a standing wave shimmers up + // the body, its bands creeping upward and wavering with the tremulant. + const s = P.s, tr = P.tr, gain = 300 * (0.7 + 0.65 * s), bands = 2 + 4 * s; + const trem = sin(2 * PI * 5.4 * t), tm = 1 - 0.9 * tr * (0.5 + 0.5 * trem), wob = 0.3 * tr * trem; + const col = [0, 0, 0]; + for (let i = 0; i < NP; i++) { + const am = P.amp[i]; + if (am < 0.004) continue; + const { x, w, top, m, mh, base } = PIPES[i]; + const a = am * gain * tm, span = m - top, osc = 0.7 + 0.3 * cos(t * (5 + 0.6 * i) + i * 1.7); + const bm = max(bands, 5 * P.full), creep = 0.3 * t + i * 0.37; + // Its light on the polished tin and the gilt round it. + stopCol(col, s, 0.4, P.full); + const kb = am * tm * (0.3 + 0.45 * s) * 255; + for (let y = max(0, top - 10); y < min(H, base + 6); y++) { + const ly = kb * (y < top ? exp((y - top) / 4) : y > m ? exp((m - y) / 7) : 1); + for (let xx = max(0, x - 16); xx < min(W, x + w + 16); xx++) { + const dd = xx < x ? x - xx : xx >= x + w ? xx - x - w + 1 : 0, lb = ly / (1 + dd * dd * 0.05), o = (y * W + xx) * 3; + for (let n = 0; n < 3; n++) F[o + n] += A[o + n] * col[n] * lb; + } + } + for (let y = top; y < m; y++) { + const u = (m - 0.5 - y) / span; + const bv = abs(cos(PI * (bm * u - creep + wob))) ** 2.5 * osc; + const g = a * (0.7 + 0.3 * exp(-3 * u)) * (0.42 + 0.58 * bv); + stopCol(col, s, u, P.full); + for (let j = -2; j < w + 2; j++) { + const xx = x + j; + if (xx < 0 || xx >= W) continue; + const cr = j < 0 || j >= w ? (j === -1 || j === w ? 0.2 : 0.07) : 0.6 + 0.4 * sin((PI * (j + 0.5)) / w); + const o = (y * W + xx) * 3; + for (let n = 0; n < 3; n++) F[o + n] += col[n] * g * cr; + } + } + // The mouth: hot in the slot, flaring as the pipe speaks, and a glow round it. + stopCol(col, s, 0, P.full); + const cx = x + w / 2, cy = m + mh / 2, hot = a * (0.8 + 1.1 * P.chiff[i]); + for (let y = m - 4; y <= m + mh + 4; y++) + for (let xx = x - 3; xx < x + w + 3; xx++) { + if (xx < 0 || xx >= W) continue; + const e = ((xx + 0.5 - cx) / (w / 2 + 1.2)) ** 2 + ((y + 0.5 - cy) / 2.6) ** 2; + if (e > 3) continue; + const slot = y >= m && y < m + mh && xx > x && xx < x + w - 1; + const o = (y * W + xx) * 3, k = slot ? hot * 1.2 : hot * 0.42 * exp(-e * 1.3); + for (let n = 0; n < 3; n++) F[o + n] += mix(col[n], 1, slot ? 0.55 : 0.15) * k; + } + } + // Keys held down on the manual. + for (let i = 0; i < NP; i++) + if (P.keys[i] > 0) + for (let y = 121; y <= 125; y++) + for (let x = 3 * (i + 1); x < 3 * (i + 1) + 2; x++) { + const o = (y * W + x) * 3; + for (let n = 0; n < 3; n++) F[o + n] *= y === 125 ? 0.35 : 0.6; + } + for (let i = 0, o = 0; i < N; i++, o += 3) display.setPixel(i & 63, i >> 6, knee(F[o]), knee(F[o + 1]), knee(F[o + 2])); +} + +// Light past 180 rolls off towards 255, so a speaking pipe keeps its colour. +function knee(v) { + return (v < 180 ? v : 255 - 75 * exp(-(v - 180) / 75)) | 0; +} From 62da6b04bc5ac3d2acfdc5187dda1c8692771ffc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:24:48 +0000 Subject: [PATCH 08/33] emergence: open the base, cave arch in a limestone outcrop, moon behind the column, header --- incoming/pattern-study/patterns/emergence.js | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/incoming/pattern-study/patterns/emergence.js b/incoming/pattern-study/patterns/emergence.js index 6445dab37..e3530edc2 100644 --- a/incoming/pattern-study/patterns/emergence.js +++ b/incoming/pattern-study/patterns/emergence.js @@ -1,21 +1,21 @@ // ===== Patternflow pattern ===== // Title: Emergence // Family: mammal -// Idea: At dusk over dry hill country a dark ribbon of free-tailed bats pours out of a cave mouth and corkscrews up the whole sky, splitting into two strands and joining again before it streams off sideways high up, and when a hawk dives into it the bats ripple away and leave a hole that closes behind it. +// Idea: At dusk over dry hill country a dark ribbon of free-tailed bats pours out of a cave mouth and corkscrews up the whole sky, splits in two and joins again, streams off sideways at the top, and ripples open round a hawk that dives through it. // Author: patternflow-lab // Date: 2026-10-24 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: original, pattern round 24, after the evening emergence of Mexican free-tailed bats (Tadarida brasiliensis) from Bracken Cave, Texas +// Lineage: original, pattern round 24, after the summer evening emergence of Mexican free-tailed bats (Tadarida brasiliensis) from the caves of the Texas Hill Country // Round: 24 // =============================== // @matrix 64x128 // @knobs Dusk=0..1, Speed=0.2..3, Bats=400..1400, Wind=-1..1 // -// Knob 1: Dusk. ... -// Knob 2: Speed. ... -// Knob 3: Bats. ... -// Knob 4: Wind. ... -// Buttons: K1 tap sends a hawk diving into the column. +// Knob 1: Dusk. How late it is: at 0 the afterglow burns orange behind the far mesa and goes rose, then indigo overhead, and towards 1 it sinks into night, the stars come out and a full moon rises behind the column, so the bats and the hawk cross it as black shapes. +// Knob 2: Speed. The pace of everything: at 2 a bat climbs from the cave mouth to the top of the panel in about four seconds and beats its wings five times a second, and at 0.2 it all runs ten times slower. +// Knob 3: Bats. How many bats are out, from a stream of 400 to a river of 1400 that also widens the column. Turned down, the bats already in the air finish their trip and fewer follow them out of the cave. +// Knob 4: Wind. A crosswind: the column leans downwind and most of the bats peel off that way at the top, and the high cloud streaks drift with it. With no wind about three in four turn right. +// Buttons: K1 tap sends a hawk diving through the column now. On its own the first one comes about four seconds in, then one every fifteen to twenty seconds. const [W, H] = [64, 128]; const N = W * H; @@ -139,7 +139,7 @@ export function update(dt, input, params) { for (let h = 0; h < HM; h++) { const A = 1 + 7.5 * sm(4, 92, h); SX[h] = CX + w * (0.17 * h + 0.0009 * h * h) + A * (sin(0.052 * h - 1.3 * T) + 0.4 * sin(0.12 * h + 0.75 * T + 2)); - SR[h] = Rk * (0.5 + 0.5 * sm(0, 22, h)) * (1 + 0.005 * h) * (1 + 0.3 * sin(0.075 * h - 2.1 * T)); + SR[h] = Rk * (1.25 - 0.55 * sm(0, 26, h) + 0.3 * sm(20, 50, h)) * (1 + 0.005 * h) * (1 + 0.3 * sin(0.075 * h - 2.1 * T)); SP[h] = 0.055 * h - 1.7 * T; SS[h] = h > 18 && h < 96 ? split * 9 * sin((PI * (h - 18)) / 78) : 0; } @@ -153,7 +153,7 @@ export function update(dt, input, params) { const hk = P.hk; if (hk) { const u = (hk.t += d) / 2.3, v = 1 - u; - if (u >= 1) (P.hk = null), (P.nh = 12 + rnd() * 8); + if (u >= 1) (P.hk = null), (P.nh = 12 + rnd() * 6); else { hk.x = v * v * hk.x0 + 2 * u * v * hk.cx + u * u * hk.x2; hk.y = v * v * hk.y0 + 2 * u * v * hk.cy + u * u * hk.y2; @@ -173,7 +173,7 @@ export function update(dt, input, params) { const s = (S[i] += V[i] * d * (S[i] < 40 ? 1.6 - 0.015 * S[i] : 1)); if (s < 0) continue; const hh = min(s, TU[i]), j = min(HM - 2, floor(hh)), f = hh - j; - const r = mix(SR[j], SR[j + 1], f), p = mix(SP[j], SP[j + 1], f), ca = cos(p), sa = sin(p), a = A[i], b = B[i]; + const r = mix(SR[j], SR[j + 1], f), p = mix(SP[j], SP[j + 1], f), ca = cos(p), sa = sin(p), a = A[i], b = B[i] * (1.9 - 0.9 * sm(0, 30, hh)); let x = mix(SX[j], SX[j + 1], f) + r * (a * ca - b * sa) + (a < 0 ? -1 : 1) * mix(SS[j], SS[j + 1], f) + 1.1 * sin(T * JF[i] + JP[i]); let y = CY - hh + 0.8 * sin(T * JF[i] * 1.3 + JP[i] * 2); Z[i] = a * sa + b * ca; @@ -219,7 +219,7 @@ export function update(dt, input, params) { function backdrop(P, k) { const B = P.bg, i0 = k < 0.5 ? 0 : 1, f = k < 0.5 ? k * 2 : k * 2 - 1; const L = LAND[i0].map((v, c) => mix(v, LAND[i0 + 1][c], f)); - const glow = 1 - sm(0.2, 0.9, k), mx = 41, my = 134 - 86 * sm(0.12, 1, k), mr = 8.5, mc = [mix(250, 246, sm(100, 60, my)), mix(184, 240, sm(100, 60, my)), mix(124, 220, sm(100, 60, my))]; + const glow = 1 - sm(0.2, 0.9, k), mx = 37, my = 134 - 88 * sm(0.12, 1, k), mr = 8.5, mc = [mix(250, 246, sm(100, 60, my)), mix(184, 240, sm(100, 60, my)), mix(124, 220, sm(100, 60, my))]; P.cc = L.slice(9, 12); for (let y = 0; y < H; y++) { const g = y / 100, sky = [0, 1, 2].map((c) => mix(ramp(SKIES[i0], g, c), ramp(SKIES[i0 + 1], g, c), f)); @@ -244,7 +244,7 @@ function backdrop(P, k) { const n = vn(x * 0.22, y * 0.45), n2 = vn(x * 0.07 + 5, y * 0.16); let v = (0.6 + 0.4 * n + 0.25 * sm(0.55, 0.7, n2)) * (1 + 0.3 * exp(-(y - rid) / 2.5)) * (1 - (0.28 * (y - 92)) / 36); if (((y + 4 * vn(x * 0.05, 7)) % 6) < 1) v *= 0.72; - const e = hyp((x + 0.5 - CX) / CW, (CB - y - 0.5) / CH) * (0.94 + 0.16 * vn(x * 0.6, y * 0.6)); + const e = (abs((x + 0.5 - CX) / CW) ** 2.4 + abs((CB - y - 0.5) / CH) ** 2.4) ** (1 / 2.4) * (0.96 + 0.09 * vn(x * 0.7, y * 0.7)); // The pale outcrop the cave opens in, cracked, with a ledge on top. const ot = CB - CH - 6 + 2.5 * vn(x * 0.2, 4), ow = 16 + 4 * vn(y * 0.2, 6) - 6 * sm(CB - 3, CB - CH - 6, y); if (y >= ot && y < CB + 1 && abs(x + 0.5 - CX) < ow) { From a63d89e520cdb7f37722d1f594ddf27d8599c0f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:26:11 +0000 Subject: [PATCH 09/33] emergence: wider column for a big stream, broader moon glow --- incoming/pattern-study/patterns/emergence.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/patterns/emergence.js b/incoming/pattern-study/patterns/emergence.js index e3530edc2..47d1a7bed 100644 --- a/incoming/pattern-study/patterns/emergence.js +++ b/incoming/pattern-study/patterns/emergence.js @@ -135,7 +135,7 @@ export function update(dt, input, params) { // The spine of the column: it leans downwind, corkscrews and breathes, and // now and then tears in two along its middle and closes again higher up. - const Rk = 5 + (3.5 * (M - 400)) / 1000, ph = (T - 2.4) % 12, split = T < 2.4 ? 0 : sm(0, 1.4, ph) * (1 - sm(4.6, 6.2, ph)); + const Rk = 5 + (5 * (M - 400)) / 1000, ph = (T - 2.4) % 12, split = T < 2.4 ? 0 : sm(0, 1.4, ph) * (1 - sm(4.6, 6.2, ph)); for (let h = 0; h < HM; h++) { const A = 1 + 7.5 * sm(4, 92, h); SX[h] = CX + w * (0.17 * h + 0.0009 * h * h) + A * (sin(0.052 * h - 1.3 * T) + 0.4 * sin(0.12 * h + 0.75 * T + 2)); @@ -234,7 +234,7 @@ function backdrop(P, k) { if (md < mr) { const ma = 0.82 + 0.18 * sm(0.35, 0.65, vn(x * 0.4 + 3, y * 0.4)) - 0.12 * (md / mr) ** 4; col = mc.map((v) => v * ma); - } else col = col.map((v, c) => v + mc[c] * 0.3 * sm(0.25, 0.8, k) * exp(-(md - mr) / 5)); + } else col = col.map((v, c) => v + mc[c] * sm(0.25, 0.8, k) * (0.22 * exp(-(md - mr) / 4) + 0.1 * exp(-(md - mr) / 14))); if (y >= P.FAR[x]) { const fv = 0.86 + 0.14 * vn(x * 0.3, y * 0.3); col = col.map((v, c) => mix(v, L[3 + c] * fv, 0.8)); From 217a6964edd552906ce0de5177e2953f87035a92 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:27:09 +0000 Subject: [PATCH 10/33] potters-wheel: first version, a pot thrown under one lamp --- .../pattern-study/patterns/potters-wheel.js | 481 ++++++++++++++++++ 1 file changed, 481 insertions(+) create mode 100644 incoming/pattern-study/patterns/potters-wheel.js diff --git a/incoming/pattern-study/patterns/potters-wheel.js b/incoming/pattern-study/patterns/potters-wheel.js new file mode 100644 index 000000000..d8ab9a63d --- /dev/null +++ b/incoming/pattern-study/patterns/potters-wheel.js @@ -0,0 +1,481 @@ +// ===== Patternflow pattern ===== +// Title: Potter's Wheel +// Family: pottery +// Idea: At night under one warm lamp a potter centres a lump of wet clay on the spinning wheel, opens it and pulls the wall up into a tall pot whose throwing rings catch the light, wires it off the wheel and sets it on the shelves, where the earlier pots dry from dark to bone-pale. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 24, after throwing a pot on the potter's wheel +// Round: 24 +// =============================== +// @matrix 64x128 +// @knobs Clay=0..1, Speed=0.2..3, Pots=6..20, Lamp=0..1 +// +// Knob 1: Clay. The clay body: terracotta at 0, then buff stoneware, grey porcelain and black clay at 1. The pot on the wheel, the slip in the splash pan and the pots drying on the shelves all take it. +// Knob 2: Speed. At 2 a throw takes about fourteen seconds from the slapped lump to the pot set on the shelf, and the wheel turns about once a second; at 0.2 it all runs ten times slower. +// Knob 3: Pots. How many earlier pieces stand drying on the three shelves of the rack, from six to twenty. The oldest are bone-dry, and each new pot takes the place of the oldest, which goes to the kiln. +// Knob 4: Lamp. Where the lamp hangs, from left of the wheel at 0 to right of it at 1. The lit side of the pot, the highlight down its throwing rings, its shadow on the wall and the light on the shelves move with it. +// Buttons: K1 tap makes the wall slump, and the potter balls the clay up and centres it again. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, ceil, round, sin, cos, sqrt, exp, asin, abs, 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)); +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; +} + +// The wheel head's centre and radius, the splash pan round it, and the tilt +// of the view: a circle r across on the wheel shows as an ellipse E * r tall. +const [CX, BY, E, WR, PR] = [32, 111, 0.21, 21, 34]; +const PY = BY + 2.5; +// Three shelf boards with seven slots each, the order the slots fill, and +// the size a pot stands at on the shelf against its size on the wheel. +const SHELF = [26, 53, 80]; +const SLOT = []; +for (const sy of SHELF) for (let i = 0; i < 7; i++) SLOT.push([4.6 + i * 9.13, sy]); +const ORDER = [1, 5, 8, 12, 14, 20, 3, 9, 11, 0, 6, 7, 13, 15, 19, 2, 4, 10, 16, 18, 17]; +const SHS = 0.25; + +// Each clay thrown and wet, leather-hard, and bone-dry. +const CLAY = [ + [[255, 112, 56], [196, 96, 58], [238, 158, 108]], + [[236, 170, 104], [190, 148, 100], [244, 222, 182]], + [[194, 198, 206], [184, 184, 182], [250, 248, 240]], + [[110, 96, 96], [86, 80, 78], [156, 148, 142]], +]; +const SKIN = [150, 98, 76], WALL = [84, 62, 50], WOOD = [132, 90, 58], METAL = [150, 152, 156], PLASTIC = [76, 80, 90], SLEEVE = [58, 66, 86], SHINE = [255, 232, 196]; + +// Pot shapes as a height and the radius at keys up the wall: cylinder, bottle, +// vase with a belly and a flared lip, jar, and trumpet vase. +const SHAPES = [ + (j) => [76 + 8 * j, [[0, 10.5], [0.45, 10.2 + j], [0.9, 9.6], [1, 11]]], + (j) => [84 + 8 * j, [[0, 11], [0.3, 14.5], [0.7, 4.2], [0.93, 4], [1, 5.6]]], + (j) => [82 + 8 * j, [[0, 8], [0.42, 15], [0.8, 5], [0.9, 5.4], [1, 9.5]]], + (j) => [76 + 6 * j, [[0, 8.5], [0.55, 15.5], [0.9, 9], [1, 10.5]]], + (j) => [86 + 6 * j, [[0, 9.5], [0.2, 9.8], [0.62, 5.5], [1, 14]]], +]; +function key(K, v) { + let i = 0; + while (i < K.length - 2 && v > K[i + 1][0]) i++; + return mix(K[i][1], K[i + 1][1], sm(K[i][0], K[i + 1][0], v)); +} +// A radius looked up along a wall kept one entry per row. +const at = (pr, h, Hp) => { + h = h < 0 ? 0 : h > Hp ? Hp : h; + const i = h | 0; + return pr[i] + (pr[i + 1] - pr[i]) * (h - i); +}; +function newSpec(not) { + let type; + // The first throw of the night is always one of the three tall ones. + do type = floor(rnd() * SHAPES.length); + while (type === not || (not < -1 && type % 3 === 0)); + const [Hf, K0] = SHAPES[type](rnd()), g = 0.92 + 0.16 * rnd(), K = K0.map(([v, r]) => [v, r * g]); + const pr = new Float32Array(ceil(Hf) + 3); + for (let h = 0; h < pr.length; h++) pr[h] = key(K, min(1, h / Hf)) + 1.4 * exp(-h / 2.2); + // Where the hands shape it: the belly, then the neck above. + let vb = 0.5, vn = 0.85, rb = 0, rn = 99; + for (const [v, r] of K) if (v > 0.1 && v < 0.85 && r > rb) (rb = r), (vb = v); + for (const [v, r] of K) if (v > vb && v < 1 && r < rn) (rn = r), (vn = v); + return { type, Hf, pr, rim: K[K.length - 1][1], vb, vn }; +} + +// The steps of a throw, how long each takes at Speed 2, and how fast the +// wheel turns through it (turns a second): slapped down, centred, opened, +// three pulls, shaped, shown, wired off, lifted to the shelf, and the slump +// and the balling up after a K1 tap. +const DUR = [0.45, 1, 0.55, 4.5, 2.6, 1.6, 0.9, 1.8, 1.2, 0.9]; +const SPIN = [0.4, 1.3, 1.1, 0.95, 0.75, 0.65, 0.3, 0, 0.9, 1]; +// The cylinder after each pull, and its wall at the rim. +const RC = [12.5, 11.3, 10.5, 9.9], WALL_T = [4, 2.7, 2.1, 1.7]; +const cyl = (k, h) => RC[k] - 0.012 * h + 1.4 * exp(-h / 2.2); +const dome = (v, r) => r * sqrt(max(0, 1 - v ** 2.4)) + 1.2 * exp(-v * 8); + +export function setup(params) { + const P = (params.s = { + t: 0, ph: 0, pt: 0, rot: 0, w: 0.4, H: 11, ri: 0, wa: 0, wt: 0, yo: 0, sH: 11, sri: 0, n: 6, LX: 12, clay: 0, + pr: new Float32Array(128), snap: new Float32Array(128), f: new Float32Array(N * 3), bg: new Float32Array(N * 3), + hd: null, hs: null, g: null, wire: -1, pieces: [], sp: [], dr: [], free: 0, bgKey: -1, + }); + P.spec = newSpec(-2); + // The pieces already on the shelves, newest first. + for (let i = 0; i < 21; i++) P.pieces.push({ slot: ORDER[i], spec: newSpec(i ? P.pieces[i - 1].spec.type : -1), dry: min(1, 0.1 + i * 0.17) }); + // Old slip spattered in the pan. + for (let i = 0; i < 60; i++) { + const a = rnd() * TAU, r = 0.66 + 0.3 * rnd(); + P.sp.push(i < 44 ? [CX + PR * r * sin(a), PY + PR * E * r * cos(a), rnd() < 0.3 ? 2 : 1, 0.6 + 0.5 * rnd()] : [2 + rnd() * 60, PY + 8 + rnd() * 4, 1, 0.5 + 0.4 * rnd()]); + } + shape(P, 0); +} + +function go(P, ph) { + // The oldest piece on show goes to the kiln and the new pot takes its place. + if (ph === 7) P.free = P.pieces.splice(min(P.n, P.pieces.length) - 1, 1)[0].slot; + if (ph === 0 && P.ph === 7) P.pieces.unshift({ slot: P.free, spec: P.spec, dry: 0 }), (P.spec = newSpec(P.spec.type)); + P.ph = ph; + P.pt = 0; + P.snap.set(P.pr); + P.sH = P.H; + P.sri = P.ri; +} + +function fill(P, Hn, f) { + P.H = Hn; + for (let h = 0, n = min(126, ceil(Hn) + 2); h <= n; h++) P.pr[h] = max(0, f(min(h, Hn))); +} +const snapR = (P, v) => at(P.snap, v * P.sH, P.sH); +const offset = (P, h) => P.wa * sin(P.rot) + P.wt * (cl(h / P.H, 0, 1) ** 1.5) * sin(P.rot + 0.8); + +// The clay's wall for this moment of the throw, and where the hands are. +function shape(P, p) { + const sp = P.spec; + let hand = null; + P.wa = 0; + P.wt = 0.2; + P.yo = 0; + P.g = null; + P.wire = -1; + P.ring = 0.07; + switch (P.ph) { + case 0: { + // Slapped down from the hands, squashing as it lands. + const sq = p > 0.6 ? 1 - 0.3 * sin((PI * (p - 0.6)) / 0.4) : 1, r = 10.5 / sqrt(sq), Hn = 11 * sq; + P.yo = p < 0.6 ? -44 * (1 - (p / 0.6) ** 2) : 0; + fill(P, Hn, (h) => dome(h / Hn, r)); + (P.ri = 0), (P.wa = 0.8), (P.ring = 0.04); + hand = [5, -0.3, 0]; + break; + } + case 1: { + // Centred: coned up and pressed down again while the wobble dies. + const Hn = 11 + 10 * sin(PI * sm(0.1, 0.9, p)), r = 10.5 * sqrt(11 / Hn); + fill(P, Hn, (h) => dome(h / Hn, r)); + (P.ri = 0), (P.wa = 2.6 * (1 - sm(0, 0.8, p))), (P.ring = 0.05); + hand = [Hn * 0.42, -0.5 + 0.4 * sin(p * 20), 0]; + break; + } + case 2: { + const q = sm(0, 1, p), Hn = mix(P.sH, 13, q); + fill(P, Hn, (h) => mix(snapR(P, h / Hn), cyl(0, h), q)); + (P.ri = (RC[0] - WALL_T[0]) * sm(0.15, 1, p)), (P.ring = 0.07); + hand = [Hn - 1.5, -0.2, 0]; + break; + } + case 3: { + // Three pulls. The hands squeeze the wall from the base to the rim and + // a roll of clay rides up ahead of them. + const k = min(2, floor(P.pt / 1.5)), a = (P.pt - k * 1.5) / 1.2, Hp = sp.Hf * 0.93; + const HS = [13, mix(13, Hp, 0.42), mix(13, Hp, 0.74), Hp]; + if (a <= 1) { + const Hn = mix(HS[k], HS[k + 1], sm(0, 1, a)), hh = Hn * min(1, a * 1.04) * 0.95, roll = 1.3 * (1 - sm(0.9, 1, a)); + fill(P, Hn, (h) => mix(cyl(k + 1, h), cyl(k, h), sm(hh - 1, hh + 2.5, h)) + roll * exp(-(((h - hh - 2.6) / 2.2) ** 2)) - 0.8 * exp(-(((h - hh) / 1.4) ** 2))); + P.ri = mix(RC[k] - WALL_T[k], RC[k + 1] - WALL_T[k + 1], sm(0.85, 1, a)); + hand = [hh, -0.3, 0]; + } else { + const Hn = HS[k + 1], b = (a - 1) / 0.25; + fill(P, Hn, (h) => cyl(k + 1, h)); + P.ri = RC[k + 1] - WALL_T[k + 1]; + hand = [mix(Hn * 0.95, 3, sm(0, 1, b)), 1.5 * sin(PI * b), 0]; + } + break; + } + case 4: { + // Shaped: the belly pushed out from inside, then the neck collared in. + const q = sm(0, 1, p), Hn = mix(P.sH, sp.Hf, q); + fill(P, Hn, (h) => mix(snapR(P, h / Hn), at(sp.pr, (h / Hn) * sp.Hf, sp.Hf), q)); + P.ri = mix(P.sri, sp.rim - 1.4, q); + hand = [Hn * mix(sp.vb, sp.vn, sm(0.4, 0.75, p)), -0.2, 0]; + break; + } + case 8: { + // Slumped: the wall sinks, bellies and flops round as the wheel turns. + const q = sm(0, 1, p), Hn = mix(P.sH, max(12, P.sH * 0.3), q ** 0.8); + fill(P, Hn, (h) => mix(snapR(P, h / Hn), snapR(P, (h / Hn) * 0.6) * (1 + 0.7 * (h / Hn) ** 2) + (3 * h) / Hn, q)); + P.ri = P.sri > 0 ? max(0, at(P.pr, Hn, Hn) - 1.8) : 0; + (P.wt = 6 * sin(PI * min(1, p * 1.3)) + 2), (P.wa = 1); + hand = [Hn * 0.5, 4 + 20 * sm(0, 0.3, p), 10 * sm(0, 0.3, p)]; + break; + } + case 9: { + const q = sm(0, 1, p), Hn = mix(P.sH, 11, q); + fill(P, Hn, (h) => mix(snapR(P, h / Hn), dome(h / Hn, 10.5), q)); + (P.ri = P.sri * (1 - q)), (P.wa = 2.6), (P.wt = 2 * (1 - q)), (P.ring = 0.04); + hand = [Hn * 0.45, mix(6, -0.5, sm(0, 0.5, p)), 0]; + break; + } + default: { + fill(P, sp.Hf, (h) => at(sp.pr, h, sp.Hf)); + P.ri = sp.rim - 1.4; + // Shown: the hands come away. + if (P.ph === 5) hand = [sp.Hf * sp.vn, 2 + 30 * sm(0, 0.6, p), 22 * sm(0, 0.6, p)]; + if (P.ph === 6) { + // Wired off: the hands come in to the wheel and draw the wire under. + const a = sm(0, 0.25, p), q = sm(0.25, 1, p), y = mix(BY + 20, BY + E * P.pr[0] * (1 - 2 * q) + 0.5, a); + P.wire = a < 1 ? -1 : q; + P.hd = [CX - mix(40, 25, a), y, CX + mix(40, 25, a), y]; + return; + } + if (P.ph === 7) { + // Lifted, then carried back to its slot on the shelf. + const q1 = sm(0, 0.3, p), q2 = sm(0.3, 1, p), [sx, sy] = SLOT[P.free], s = mix(1, SHS, q2 ** 0.6); + P.g = [mix(CX, sx, q2), mix(BY - 9 * q1, sy + 0.5, q2) - 14 * sin(PI * q2), s]; + if (q2 < 0.5) { + const r = sp.pr[3] * s, y = P.g[1] - 3 * s + 40 * sm(0.2, 0.5, q2); + P.hd = [P.g[0] - r, y, P.g[0] + r, y]; + } else P.hd = null; + return; + } + } + } + if (hand) { + const [h, gap, dy] = hand, r = at(P.pr, h, P.H), o = offset(P, h), y = BY + P.yo - h + dy; + P.hd = [CX + o - r - gap, y, CX + o + r + gap, y]; + } else P.hd = null; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + const d = (cl(dt || 0, 0, 0.1) * cl(k[1], 0.2, 3)) / 2; + P.clay = cl(k[0], 0, 1); + P.n = round(cl(k[2], 6, 20)); + P.LX = mix(9, 55, cl(k[3], 0, 1)); + P.t += d; + if (input.btnPressed && input.btnPressed[0] && P.ph >= 1 && P.ph <= 5) go(P, 8); + if ((P.pt += d) >= DUR[P.ph]) go(P, [1, 2, 3, 4, 5, 6, 7, 0, 9, 1][P.ph]); + P.w += (SPIN[P.ph] - P.w) * min(1, d * 3); + P.rot += TAU * P.w * d; + const landing = P.ph === 0 && P.yo < 0; + shape(P, P.pt / DUR[P.ph]); + // The hands follow their mark, easing over the big jumps between steps. + if (!P.hd) P.hs = null; + else if (!P.hs) P.hs = P.hd.slice(); + else for (let i = 0; i < 4; i++) abs(P.hd[i] - P.hs[i]) > 3 ? (P.hs[i] += (P.hd[i] - P.hs[i]) * min(1, d * 14)) : (P.hs[i] = P.hd[i]); + + // Slip flung off the wall by the hands, and spattered as the lump lands. + const fling = (x, y, sd, n) => { + for (let i = 0; i < n; i++) P.dr.push([x, y, sd * (8 + rnd() * 26), -8 - rnd() * 26, PY - 4 + rnd() * 10]); + }; + if (P.hs && (P.ph === 3 || P.ph === 4 || P.ph === 8) && rnd() < d * 3) rnd() < 0.5 ? fling(P.hs[0], P.hs[1], -1, 1) : fling(P.hs[2], P.hs[3], 1, 1); + if (landing && P.yo === 0) fling(CX - 9, BY - 2, -1, 3), fling(CX + 9, BY - 2, 1, 3); + P.dr = P.dr.filter((q) => { + (q[0] += q[2] * d), (q[1] += q[3] * d), (q[3] += 150 * d); + if (q[3] > 0 && q[1] >= q[4]) { + P.sp.push([q[0], q[1], rnd() < 0.3 ? 2 : 1, 0.7 + 0.5 * rnd()]); + if (P.sp.length > 110) P.sp.shift(); + return false; + } + return q[0] > -2 && q[0] < W + 2; + }); + for (const q of P.pieces) q.dry = min(1, q.dry + d / 75); +} + +// The wall, the rack and the lamp's glow, kept until the lamp moves. +function backdrop(P) { + P.bgKey = bgKey(P); + // Clay dust lies on everything in the room and takes the clay's colour. + const B = P.bg, LX = P.LX, dust = P.dust, wall = WALL.map((v, n) => mix(v, dust[n] * 0.4, 0.55)), wood = WOOD.map((v, n) => mix(v, dust[n] * 0.6, 0.35)); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const dx = x + 0.5 - LX, dy = y - 10, o = (y * W + x) * 3; + let f = 0.24 + 0.85 * exp(-(dx * dx + dy * dy * 0.3) / 1500), col = wall; + if (dy > 0) { + const cw = 4 + dy * 0.72; + f += 0.32 * sm(cw + 6, cw - 3, abs(dx)) * exp(-dy / 90); + } + f *= 0.9 + 0.16 * hash(x >> 1, y >> 1); + for (const sy of SHELF) { + if (y === sy) (col = wood), (f *= 1.35); + else if (y > sy && y <= sy + 3) (col = wood), (f *= 0.75 + 0.15 * hash(x >> 2, sy)); + else if (y > sy + 3 && y <= sy + 6) f *= 0.45 + 0.17 * (y - sy - 4); + } + if ((x < 2 || x > 61) && y < SHELF[2] + 4) (col = wood), (f *= x === 1 || x === 62 ? 0.9 : 0.6); + const g = 0.55 * exp(-(dx * dx + (y - 10.5) ** 2) / 30); + for (let c = 0; c < 3; c++) B[o + c] = col[c] * f + [255, 196, 120][c] * g; + } +} + +// The light from the lamp at a spot: its direction and how much reaches it. +function light(P, x, y) { + const lx = cl((P.LX - x) / 24, -1.3, 1.3), ly = 0.3 + (y - 10) / 300, n = sqrt(lx * lx + ly * ly + 0.25); + return [lx / n, ly / n, 0.5 / n, 0.6 + 0.5 * exp(-((x - P.LX) ** 2 + ((y - 10) * 0.5) ** 2) / 1800)]; +} + +let F; +const bgKey = (P) => round(P.LX * 8) * 64 + round(P.clay * 48); +// A thrown shape turned on its axis, lit by the lamp: wall, rim and the +// dark inside. Off-centre clay swings its axis round with the wheel. +function solid(o) { + const { cx, by, s, pr, Hp, L, alb } = o, Hs = Hp * s, [l0, l1, l2, lm] = L; + let Rm = 0; + for (let i = 0; i <= Hp + 1; i++) Rm = max(Rm, pr[i]); + Rm *= s; + const R = (h) => at(pr, h / s, Hp) * s, off = (h) => o.wa * o.s1 + o.wt * (cl(h / Hs, 0, 1) ** 1.5) * o.s2; + const RT = R(Hs), cT = cx + off(Hs), yT = by - Hs, ryT = E * RT + 0.35, ri = o.ri * s, ryi = E * ri + 0.25, wb = abs(o.wa) + abs(o.wt); + const hn = sqrt(l0 * l0 + l1 * l1 + (l2 + 1) ** 2), hx = l0 / hn, hy = l1 / hn, hz = (l2 + 1) / hn; + const y0 = max(0, floor(yT - ryT - 1)), y1 = min(H - 1, ceil(by + E * Rm + 1)), x0 = max(0, floor(cx - Rm - wb - 1)), x1 = min(W - 1, ceil(cx + Rm + wb + 1)); + for (let y = y0; y <= y1; y++) + for (let x = x0, h0 = by - y - 0.5, ox = wb ? off(h0) : 0; x <= x1; x++) { + const px = x + 0.5 - cx - ox, o3 = (y * W + x) * 3; + let h = h0, r = 0, u = 9, c = 0, k, sp = 0; + for (let it = 0; it < 3; it++) { + r = R(h); + u = r > 0.05 ? px / r : 9; + c = u * u < 1 ? sqrt(1 - u * u) : 0; + h = h0 + E * r * c; + } + r = R(h); + u = r > 0.05 ? px / r : 9; + if (h >= 0 && h <= Hs && u * u < 1) { + c = sqrt(1 - u * u); + const hp = h / s, dR = (R(h + 0.6) - R(h - 0.6)) / 1.2 + o.ring * 2.03 * cos(hp * 2.03), nl = 1 / sqrt(1 + dR * dR); + const dif = max(0, (u * l0 - dR * l1 + c * l2) * nl), nh = max(0, (u * hx - dR * hy + c * hz) * nl); + k = (o.amb + 0.95 * dif + 0.2 * max(0, (-u * l0 + 0.4 * c) * nl)) * lm; + sp = o.spec * nh ** 40; + if (o.fl) { + // Slip streaks and grog flecks ride round with the wheel. + const az = asin(u) - o.rot, a = ((az % TAU) + TAU) % TAU, i = floor(a * 6.366), j = floor(hp / 2.3), q = hash(i, j); + const st = 1 + 0.1 * sin(3 * az + 1.3) + 0.07 * sin(5 * az + hp * 0.11); + k *= st; + sp *= st; + if (q < 0.2 && abs(a - (i + 0.5) / 6.366) * r < 0.7 && abs(hp - (j + 0.5) * 2.3) < 0.75) k *= q < 0.1 ? 1.5 : 0.55; + } + } else { + const ex = (x + 0.5 - cT) / RT, ey = (y + 0.5 - yT) / ryT; + if (RT < 0.4 || ex * ex + ey * ey > 1) continue; + const ix = (x + 0.5 - cT) / (ri + 0.01), iy = (y + 0.5 - yT) / ryi; + if (ri > 0.4 && ix * ix + iy * iy < 1) k = (0.05 + 0.42 * cl(-ix * l0 * 1.3 + 0.2, 0, 1) * (iy < 0.3 ? 1 : 0.55)) * lm; + else (k = (0.3 + 0.8 * l1) * lm), (sp = o.spec * 0.3); + } + for (let n = 0; n < 3; n++) F[o3 + n] = alb[n] * k + SHINE[n] * sp; + } +} + +// The earlier pots on the shelves, wet and dark, then leather-hard, then pale. +function shelves(P, C) { + for (let i = 0; i < min(P.n, P.pieces.length); i++) { + const q = P.pieces[i], [sx, sy] = SLOT[q.slot], dr = q.dry, a = dr < 0.45 ? dr / 0.45 : (dr - 0.45) / 0.55; + const alb = dr < 0.45 ? C[0].map((v, n) => mix(v * 0.62, C[1][n], a)) : C[1].map((v, n) => mix(v, C[2][n], a)); + solid({ cx: sx, by: sy + 0.5, s: SHS, pr: q.spec.pr, Hp: q.spec.Hf, ri: q.spec.rim - 1.4, wa: 0, wt: 0, s1: 0, s2: 0, rot: 0, alb, spec: 0.6 * (1 - sm(0, 0.4, dr)), L: light(P, sx, sy - 10), ring: 0, fl: 0, amb: 0.3 }); + } +} + +const put = (x, y, col, k) => { + x = round(x - 0.5); + y = round(y - 0.5); + if (x >= 0 && x < W && y >= 0 && y < H) for (let c = 0, o = (y * W + x) * 3; c < 3; c++) F[o + c] = col[c] * k; +}; + +// The lamp: a cord, an enamel shade and the bulb under it. +function lamp(P) { + const LX = P.LX; + for (let y = 0; y < 12; y++) + for (let x = max(0, floor(LX - 8)); x <= min(W - 1, LX + 8); x++) { + const dx = x + 0.5 - LX, hw = 1.3 + (y - 3) * 0.85; + if (y < 3 ? abs(dx) < 0.6 : y < 10 ? abs(dx) < hw : abs(dx) < (y === 10 ? 2.4 : 1.2)) { + const o = (y * W + x) * 3, col = y < 3 ? [44, 38, 34] : y === 9 ? [236, 170, 96] : y < 9 ? [38, 72, 62] : y === 10 ? [255, 244, 214] : [255, 226, 168]; + const k = y > 2 && y < 9 ? 0.7 + 0.6 * (1 - (y - 3) / 6) * (dx < 0 ? 1 : 0.6) : 1; + for (let c = 0; c < 3; c++) F[o + c] = col[c] * k; + } + } +} + +// The pot's shadow thrown on the wall behind, leaning away from the lamp. +function shadow(P) { + for (let y = max(0, floor(BY - P.H - 6)); y < 106; y++) { + const h = BY + 4 - y; + if (h < 0 || h > P.H) continue; + const r = at(P.pr, h, P.H) * 1.08, xc = CX + (CX - P.LX) * (0.12 + h * 0.007); + for (let x = max(0, floor(xc - r - 2)); x <= min(W - 1, xc + r + 2); x++) { + const k = 1 - 0.5 * sm(r + 1.5, r - 1, abs(x + 0.5 - xc)), o = (y * W + x) * 3; + (F[o] *= k), (F[o + 1] *= k), (F[o + 2] *= k); + } + } +} + +// The splash pan with its slip, and the wheel head turning in it. +function wheel(P, A) { + const L = light(P, CX, PY), [l0, l1, l2, lm] = L, mt = METAL.map((v, n) => mix(v, A[n], 0.7)); + for (let y = 100; y < H; y++) + for (let x = 0; x < W; x++) { + const ex = (x + 0.5 - CX) / PR, ey = (y + 0.5 - PY) / (PR * E), dd = ex * ex + ey * ey, o = (y * W + x) * 3; + const wx = (x + 0.5 - CX) / WR, wy = (y + 0.5 - BY) / (WR * E), wd = wx * wx + wy * wy, side = (z) => 0.15 + 0.8 * max(0, z * l0 + sqrt(max(0, 1 - z * z)) * l2); + let col, k; + if (wd < 1) (col = mt), (k = (0.3 + 0.6 * l1) * lm * (0.9 + 0.1 * cos(sqrt(wd) * WR * 1.4))); + else if (wy > 0 && wx * wx < 1 && y + 0.5 < BY + WR * E * sqrt(1 - wx * wx) + 2.5) (col = METAL), (k = 0.5 * side(wx) * lm); + else if (dd < 1) dd > 0.86 ? ((col = PLASTIC), (k = (0.6 + 0.6 * l1) * lm)) : ((col = A), (k = (ey < 0 ? 0.26 + 0.12 * (1 + ey) : 0.4) * lm)); + else if (ey > 0 && ex * ex < 1 && y + 0.5 < PY + PR * E * sqrt(1 - ex * ex) + 6) (col = PLASTIC), (k = side(ex) * lm); + else if (y > PY) (col = WALL), (k = 0.2); + else continue; + for (let c = 0; c < 3; c++) F[o + c] = col[c] * k; + } + // Slip spattered on the pan, then the wheel's own marks going round. + for (const [x, y, z, t] of P.sp) { + const ex = (x - CX) / WR, ey = (y - BY) / (WR * E); + if (ex * ex + ey * ey < 1.1) continue; + put(x, y, A, 0.5 * t * lm); + if (z > 1) put(x + 1, y, A, 0.4 * t * lm); + } + for (let j = 0; j < 6; j++) { + const a = P.rot + j * 1.9, r = j < 2 ? 16 : 11.5 + j * 1.6; + put(CX + r * sin(a) + 0.5, BY + E * r * cos(a) + 0.5, j < 2 ? [20, 20, 22] : A, j < 2 ? 1 : 0.85 * lm); + } +} + +// Skin lit by the lamp, round in section. +function cap(ax, ay, bx, by, ra, rb, col, L, tip) { + const dx = bx - ax, dy = by - ay, ll = dx * dx + dy * dy || 1e-6, rm = max(ra, rb) + 1; + for (let y = max(0, floor(min(ay, by) - rm)); y <= min(H - 1, max(ay, by) + rm); y++) + for (let x = max(0, floor(min(ax, bx) - rm)); x <= min(W - 1, max(ax, bx) + rm); x++) { + const qx = x + 0.5 - ax, qy = y + 0.5 - ay, t = cl((qx * dx + qy * dy) / ll, 0, 1), ex = qx - dx * t, ey = qy - dy * t, r = mix(ra, rb, t), dd = ex * ex + ey * ey; + if (dd > r * r) continue; + const nz = sqrt(1 - dd / (r * r)), k = (0.16 + 0.9 * max(0, (ex / r) * L[0] - (ey / r) * L[1] + nz * L[2])) * L[3], C = tip && t > 0.5 ? tip : col, o = (y * W + x) * 3; + for (let c = 0; c < 3; c++) F[o + c] = C[c] * k; + } +} + +// A hand on the clay at x, y: the forearm down to the side, the palm, three +// fingers round the wall with slip on their tips, and the thumb. +function hand(P, sd, x, y, slip) { + const L = light(P, x, y); + cap(x + sd * 22, y + 26, x + sd * 46, y + 62, 5.6, 6.6, SLEEVE, L); + cap(x + sd * 6.5, y + 3.5, x + sd * 23, y + 27, 3.4, 4.6, SKIN, L); + cap(x + sd * 7.5, y + 3.5, x + sd * 3.2, y - 0.3, 3.6, 3.3, SKIN, L); + cap(x + sd * 3.2, y - 0.3, x - sd * 1.8, y + 0.8, 2.6, 2, SKIN, L, slip); + cap(x + sd * 3.8, y - 3, x + sd * 2.6, y - 4.4, 1.4, 1.2, SKIN, L, slip); +} + +export function draw(display, params) { + const P = params.s; + if (!P || !P.pr) return; + F = P.f; + const c = P.clay * 3, i0 = min(2, floor(c)), f = c - i0, C = [0, 1, 2].map((j) => CLAY[i0][j].map((v, n) => mix(v, CLAY[i0 + 1][j][n], f))), A = C[0]; + P.dust = C[2]; + if (P.bgKey !== bgKey(P)) backdrop(P); + F.set(P.bg); + shelves(P, C); + lamp(P); + if (!P.g) shadow(P); + wheel(P, A); + const wire = (front) => { + if (P.wire < 0 || !P.hs || P.wire < 0.5 !== front) return; + for (let x = P.hs[0]; x <= P.hs[2]; x++) put(x + 0.5, P.hs[1] + 0.5, [220, 222, 230], 0.9); + }; + wire(false); + const [gx, gy, gs] = P.g || [CX, BY + P.yo, 1]; + solid({ cx: gx, by: gy, s: gs, pr: P.pr, Hp: P.H, ri: P.ri, wa: P.wa, wt: P.wt, s1: sin(P.rot), s2: sin(P.rot + 0.8), rot: P.rot, alb: A, spec: 0.9, L: light(P, gx, gy - P.H * gs * 0.5), ring: P.ring, fl: 1, amb: 0.08 }); + wire(true); + if (P.hs) { + const slip = SKIN.map((v, n) => mix(v, A[n], 0.6)); + hand(P, -1, P.hs[0], P.hs[1], slip); + hand(P, 1, P.hs[2], P.hs[3], slip); + } + for (const [x, y] of P.dr) put(x, y, A, 0.8); + 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 1e2930194adeb58511db211f1d342e9a3e612cb2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:27:30 +0000 Subject: [PATCH 11/33] waggle-dance: first running version (WIP) --- .../pattern-study/patterns/waggle-dance.js | 706 ++++++++++++++++++ 1 file changed, 706 insertions(+) create mode 100644 incoming/pattern-study/patterns/waggle-dance.js diff --git a/incoming/pattern-study/patterns/waggle-dance.js b/incoming/pattern-study/patterns/waggle-dance.js new file mode 100644 index 000000000..7d8d554cc --- /dev/null +++ b/incoming/pattern-study/patterns/waggle-dance.js @@ -0,0 +1,706 @@ +// ===== Patternflow pattern ===== +// Title: Waggle Dance +// Family: insect +// Idea: One comb of an observation hive stands behind glass, capped honey across the top, a band of pollen and the brood below, while a hundred bees crawl over it and a forager home from the flowers dances her figure of eight, each waggle run pointing as far from straight up as the flowers lie from the sun, with a ring of followers turning to face her. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 24, after Karl von Frisch's reading of the honeybee waggle dance (Tanzsprache und Orientierung der Bienen, 1965) +// Round: 24 +// =============================== +// @matrix 64x128 +// @knobs Light=0..1, Speed=0.2..3, Bees=80..240, Sun=-1..1 +// +// Knob 1: Light. Daylight from the window at 0, with the sun lying across the glass in four panes; towards 1 the curtain closes and a dim red viewing lamp takes over, which the bees cannot see, so they carry on as before. +// Knob 2: Speed. At 2 a waggle run takes a second and its loop back a little more, and the crawlers walk about a body length a second; at 0.2 everything runs ten times slower. +// Knob 3: Bees. Eighty to 240 bees on the comb; more walk in from the sides when it is turned up, and the comb disappears under them. +// Knob 4: Sun. The sun's bearing against the clover field. At 0.06 the field lies nearly under the sun and the runs point almost straight up; turning it swings every dance round and slides the patch of window sun across the glass. +// Buttons: K1 tap brings a forager home from a rich patch; she comes up from the entrance and dances long and hard, and more followers gather round her. + +const W = 64; +const H = 128; +const N = W * H; +// Cells six LEDs across the flats, rows five apart (a hexagon squashed by +// four in a hundred, so every cell is drawn alike); the frame's top bar, +// its bottom bar and the entrance slot in it. +const CW = 6; +const RH = 5; +const TOP = 5; +const BOT = 123; +const Y0 = TOP + 2.5; +const NC = 13; +const NR = 27; +const ENX = 9; +const [EMPTY, HONEY, NECTAR, POLLEN, BROOD, LARVA, EGG] = [0, 1, 2, 3, 4, 5, 6]; +const [CRAWL, REST, DIP, FOLLOW, DANCE, HOME, EXIT, LEAVE] = [0, 1, 2, 3, 4, 5, 6, 7]; +// Pollen: orange, yellow, ochre, rust, purple-grey, buff, olive, red-orange. +const POLLENS = [ + [0.95, 0.52, 0.08], + [0.95, 0.8, 0.16], + [0.74, 0.54, 0.17], + [0.6, 0.25, 0.1], + [0.44, 0.37, 0.5], + [0.86, 0.68, 0.36], + [0.55, 0.58, 0.24], + [0.9, 0.4, 0.12], +]; +// Bees: a dark race, the usual, a golden one. Thorax, then the amber bands. +const THX = [[0.45, 0.33, 0.17], [0.72, 0.5, 0.2], [0.85, 0.62, 0.26]]; +const AMB = [[0.62, 0.44, 0.18], [1, 0.72, 0.2], [1, 0.84, 0.32]]; +const DARK = [0.07, 0.05, 0.03]; +const HEAD = [0.05, 0.04, 0.03]; +const WING = [0.8, 0.84, 0.9]; +const SUNC = [1, 0.84, 0.56]; +const REDC = [1, 0.09, 0.035]; +// Where the flowers lie, against the main field, and how often foragers come from each. +const FIELDS = [0, 0, 0.95, -1.9]; + +const { min, max, sin, cos, abs, sqrt, floor, round, exp, atan2, hypot, PI } = Math; +const clamp = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => ((v = clamp((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +const wrap = (a) => a - 2 * PI * round(a / (2 * PI)); +const rnd = () => Math.random(); +function hash(a, b) { + let h = Math.imul(a, 374761393) ^ Math.imul(b, 668265263); + 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; +} +// Distance from a cell's centre in its own hexagon, 3 on the wall. +const hexd = (dx, dy) => max(abs(dx), abs(0.5 * dx + 0.9 * dy), abs(0.5 * dx - 0.9 * dy)); +const cellX = (r, c) => CW * (c - 1) + (r & 1 ? 3.5 : 0.5); +const cellY = (r) => Y0 + (r - 1) * RH; + +// Which cell each LED sits in, and where in it. +function grid(S) { + for (let y = TOP; y < BOT; y++) + for (let x = 0; x < W; x++) { + const r0 = round((y + 0.5 - Y0) / RH) + 1; + let best = 99; + let next = 99; + const p = y * W + x; + for (let r = max(0, r0 - 1); r <= min(NR - 1, r0 + 1); r++) { + const off = r & 1 ? 3.5 : 0.5; + const c0 = round((x + 0.5 - off) / CW) + 1; + for (let c = max(0, c0 - 1); c <= min(NC - 1, c0 + 1); c++) { + const dx = x + 0.5 - cellX(r, c); + const dy = y + 0.5 - cellY(r); + const d = hexd(dx, dy); + if (d < best) { + next = best; + S.cid2[p] = S.cid[p]; + best = d; + S.cid[p] = r * NC + c; + S.hd[p] = d; + S.dx[p] = dx; + S.dy[p] = dy; + } else if (d < next) { + next = d; + S.cid2[p] = r * NC + c; + } + } + } + } +} + +// Honey over the top and down the corners, a band of pollen, brood below. +function layout(S) { + for (let r = 0; r < NR; r++) + for (let c = 0; c < NC; c++) { + const i = r * NC + c; + const x = cellX(r, c); + const y = cellY(r); + const ex = (x - 32) / 33; + const ey = (y - 76) / 31; + const e = sqrt(ex * ex + ey * ey) + 0.18 * (vn(x * 0.09, y * 0.09) - 0.5); + const u = rnd(); + let t; + if (y < 26 || (e > 1.28 && y < 70)) t = u < 0.82 ? HONEY : u < 0.95 ? NECTAR : EMPTY; + else if (e > 1 && y < 80) t = u < 0.62 ? POLLEN : u < 0.78 ? NECTAR : EMPTY; + else if (e < 0.72) t = u < 0.72 ? BROOD : u < 0.82 ? LARVA : u < 0.85 ? EGG : EMPTY; + else if (e < 1) t = u < 0.34 ? LARVA : u < 0.48 ? EGG : u < 0.64 ? BROOD : u < 0.68 ? POLLEN : EMPTY; + else t = u < 0.12 ? POLLEN : u < 0.27 ? NECTAR : EMPTY; + S.type[i] = t; + S.age[i] = rnd(); + S.tint[i] = rnd(); + S.gap[i] = rnd() * 6.283; + S.pol[i] = floor(rnd() * POLLENS.length); + } +} + +// Each LED's colour as it would be in white light, and how much of the +// sun reaches it: cell bottoms stay in shadow. +function paint(S) { + const { base, expo } = S; + for (let p = 0; p < N; p++) { + const x = p % W; + const y = (p / W) | 0; + const q = p * 3; + let r, g, b; + let ex = 1; + if (y < TOP || y >= BOT) { + // Pine, the grain running along the bar, a groove where it meets the comb. + const gr = 0.8 + 0.2 * sin(x * 0.23 + 4 * vn(x * 0.06, y * 0.7 + (y < TOP ? 0 : 9))); + const edge = y === TOP - 1 || y === BOT ? 0.55 : 1; + const k = gr * edge * (0.9 + 0.1 * hash(p, 5)); + [r, g, b] = [0.6 * k, 0.42 * k, 0.24 * k]; + if (y > BOT + 1 && y < H - 1 && abs(x + 0.5 - ENX) < 5) [r, g, b] = [0.02, 0.015, 0.01]; + } else { + const i = S.cid[p]; + const t = S.type[i]; + const d = S.hd[p]; + const dx = S.dx[p]; + const dy = S.dy[p]; + const rr = hypot(dx, dy); + const tn = S.tint[i]; + const lit = -(dx + dy) / 3; + if (t === HONEY) { + const k = (1 - 0.12 * (d / 2.6) ** 2 + 0.06 * lit) * (0.95 + 0.06 * hash(p, 1)); + [r, g, b] = [(0.97 - 0.05 * tn) * k, (0.9 - 0.16 * tn) * k, (0.66 - 0.28 * tn) * k]; + } else if (t === NECTAR) { + // Nectar: dark amber down in the cell, a bright meniscus round it, and a glint. + const f = (0.08 + 0.92 * sm(1.2, 2.3, d)) * (0.75 + 0.3 * tn); + [r, g, b] = [0.07 + 0.62 * f, 0.03 + 0.36 * f, 0.006 + 0.06 * f]; + if (abs(dx + 1) < 0.6 && abs(dy + 1) < 0.7) [r, g, b] = [0.95, 0.9, 0.74]; + ex = 0.6; + } else if (t === POLLEN) { + const c = POLLENS[S.pol[i]]; + const k = (0.8 + 0.25 * tn) * (1 - 0.35 * sm(1.6, 2.5, d)); + [r, g, b] = [c[0] * k * (0.82 + 0.3 * hash(p, 2)), c[1] * k * (0.82 + 0.3 * hash(p, 3)), c[2] * k * (0.82 + 0.3 * hash(p, 4))]; + ex = 0.8; + } else if (t === BROOD) { + const k = (1 - 0.32 * (rr / 2.7) ** 2 + 0.1 * lit) * (hash(p, 6) < 0.14 ? 0.78 : 1) * (0.9 + 0.15 * tn); + [r, g, b] = [0.8 * k, 0.68 * k, 0.5 * k]; + } else { + // An open cell: dark at the bottom, its lower inside wall catching the light. + const lip = sm(1.6, 2.4, d) * (dy > 0.5 ? 1 : 0.3); + const k = (0.04 + 0.1 * lip) * (0.75 + 0.5 * tn); + [r, g, b] = [k, k * 0.68, k * 0.38]; + ex = 0.2 + 0.8 * lip; + if (t === LARVA) { + // The larva curled in a C on its bed of jelly, bigger as it grows. + const a = S.age[i]; + const R = 0.4 + 1.1 * a; + const th = 0.45 + 0.55 * a; + const ang = wrap(atan2(dy, dx) - S.gap[i]); + let on = a < 0.3 ? sm(1.2, 0.5, rr) : sm(th + 0.3, th - 0.3, abs(rr - R)) * sm(0.5, 1, abs(ang)); + if (rr < 1.9) [r, g, b] = [r + 0.18, g + 0.17, b + 0.14]; + const sh = 0.82 + 0.18 * clamp(lit + 0.5, 0, 1) + 0.05 * sin(ang * 5); + r += (0.97 * sh - r) * on; + g += (0.94 * sh - g) * on; + b += (0.85 * sh - b) * on; + ex += (0.9 - ex) * on; + } else if (t === EGG && rr < 0.7) [r, g, b] = [0.86, 0.85, 0.78]; + } + // The walls: pale wax round the honey, the rims of open cells lit, + // and a dark seam where two brood caps meet. + const w = sm(2.4, 2.6, d); + if (w > 0) { + const t2 = S.type[S.cid2[p]]; + const pale = t === HONEY || t === NECTAR ? 1 : 0; + const seam = (t === BROOD || t2 === BROOD) && (t === BROOD || t === HONEY) && (t2 === BROOD || t2 === HONEY) ? 0.15 : 1; + const k = (0.85 + 0.3 * hash(p, 8)) * seam; + const wr = (0.34 + 0.18 * pale) * k; + const wg = (0.24 + 0.15 * pale) * k; + const wb = (0.11 + 0.08 * pale) * k; + r += (wr - r) * w; + g += (wg - g) * w; + b += (wb - b) * w; + ex += ((seam < 1 ? 0.15 : 1) - ex) * w; + } + } + // The comb sits back in the hive's shade; the bees on it catch the light. + base[q] = r * 0.66; + base[q + 1] = g * 0.66; + base[q + 2] = b * 0.66; + expo[p] = ex; + } +} + +// The window throws its four panes of sun on the glass; they slide and lean with the sun. +function sunPatch(S, s) { + const cx = 32 + 22 * s; + const cy = 74 - 6 * abs(s); + const sh = -0.5 * s; + for (let p = 0; p < N; p++) { + const v = ((p / W) | 0) + 0.5 - cy; + const u = (p % W) + 0.5 - cx - sh * v; + let m = sm(22, 16, abs(u)) * sm(40, 33, abs(v)); + m *= 1 - 0.85 * sm(1.9, 0.7, abs(u)); + m *= 1 - 0.85 * sm(1.9, 0.7, abs(v + 5)); + S.sun[p] = m; + } + S.sunK = s; +} + +function newBee(S, x, y, h) { + const u = rnd(); + const b = { x, y, h, m: CRAWL, t: rnd() * 3, v: 2.6 + 2.6 * rnd(), s: 1.12 + 0.14 * rnd(), tone: u < 0.18 ? 0 : u < 0.82 ? 1 : 2, ph: rnd() * 6.28, w: 0, wag: 0, wp: 0, D: null }; + S.bees.push(b); + return b; +} + +// A bee to the dance floor on the lower comb, clear of the other dances. +function spot(S) { + let x = 32; + let y = 90; + for (let k = 0; k < 30; k++) { + x = 16 + 32 * rnd(); + y = 84 + 28 * rnd(); + if (S.dances.every((D) => hypot(D.cx - x, D.cy - y) > 26)) break; + } + return [x, y]; +} + +function newDance(S, b, vig) { + const [cx, cy] = spot(S); + const D = { + b, cx, cy, + fl: FIELDS[floor(rnd() * FIELDS.length)] + 0.12 * (rnd() - 0.5), + L: vig ? 11 : 8 + 2.5 * rnd(), + tw: vig ? 1.25 : 0.85 + 0.3 * rnd(), + tr: vig ? 0.9 : 1 + 0.3 * rnd(), + tau: 0, side: rnd() < 0.5 ? -1 : 1, + n: vig ? 26 : 9 + floor(10 * rnd()), + amp: vig ? 1 : 0.65 + 0.2 * rnd(), + want: vig ? 10 : 6 + floor(3 * rnd()), + on: false, foll: [], rec: 0, + }; + D.th = D.fl - S.sunA; + b.D = D; + S.dances.push(D); + return D; +} + +// The bees nearest the dance turn and gather round it. +function recruit(S, D, snap) { + const free = S.bees.filter((b) => (b.m === CRAWL || b.m === REST) && hypot(b.x - D.cx, b.y - D.cy) < 24); + free.sort((a, b) => hypot(a.x - D.cx, a.y - D.cy) - hypot(b.x - D.cx, b.y - D.cy)); + for (const b of free) { + if (D.foll.length >= D.want) break; + b.m = FOLLOW; + b.D = D; + b.a = atan2(b.x - D.cx, -(b.y - D.cy)); + b.t = 5 + 10 * rnd(); + D.foll.push(b); + if (snap) { + const R = D.L / 2 + 3.8; + b.x = D.cx + R * sin(b.a); + b.y = D.cy - R * cos(b.a); + b.h = b.a + PI; + } + } +} + +function seek(b, tx, ty, sp, dt) { + const dx = tx - b.x; + const dy = ty - b.y; + const r = hypot(dx, dy); + if (r > 0.25) { + const want = atan2(dx, -dy); + const e = wrap(want - b.h); + b.h += clamp(e, -5 * dt, 5 * dt); + const st = min(r, sp * dt) * max(0.15, cos(e)); + b.x += sin(b.h) * st; + b.y -= cos(b.h) * st; + } + return r; +} + +function stepDance(S, D, dt) { + const b = D.b; + D.th += clamp(wrap(D.fl - S.sunA - D.th), -1.5 * dt, 1.5 * dt); + const ux = sin(D.th); + const uy = -cos(D.th); + if (!D.on) { + // The forager hurries up from the entrance to where she will dance. + b.m = HOME; + if (seek(b, D.cx - (ux * D.L) / 2, D.cy - (uy * D.L) / 2, 9, dt) < 1.2) { + D.on = true; + b.m = DANCE; + recruit(S, D, false); + } + return; + } + D.tau += dt; + if (D.tau >= D.tw + D.tr) { + D.tau -= D.tw + D.tr; + D.side = -D.side; + if (--D.n <= 0) { + // Done: she goes back out, and some of her followers go after the flowers. + D.gone = true; + b.m = EXIT; + b.wag = 0; + for (const f of D.foll) { + f.m = rnd() < 0.4 ? EXIT : CRAWL; + f.t = 1 + 2 * rnd(); + } + return; + } + } + let want; + if (D.tau < D.tw) { + const f = D.tau / D.tw - 0.5; + b.x = D.cx + ux * D.L * f; + b.y = D.cy + uy * D.L * f; + want = D.th; + b.wag = D.amp; + b.wp += dt * 2 * PI * 13; + } else { + const s = (D.tau - D.tw) / D.tr; + const ps = D.side * PI * s; + const rx = ux * cos(ps) - uy * sin(ps); + const ry = ux * sin(ps) + uy * cos(ps); + b.x = D.cx + (rx * D.L) / 2; + b.y = D.cy + (ry * D.L) / 2; + want = atan2(-D.side * ry, -D.side * rx); + b.wag = 0; + } + b.h += clamp(wrap(want - b.h), -14 * dt, 14 * dt); + // Followers keep their place round her, turned to face her, and drift with her. + D.foll = D.foll.filter((f) => f.m === FOLLOW && f.D === D); + D.rec -= dt; + if (D.rec <= 0) { + D.rec = 1.5; + if (D.foll.length < D.want) recruit(S, D, false); + } + const da = atan2(b.x - D.cx, -(b.y - D.cy)); + for (const f of D.foll) { + f.a += dt * 0.35 * sin(wrap(da - f.a)); + const R = D.L / 2 + 3.8 + 0.4 * sin(S.t * 2 + f.ph); + if (seek(f, D.cx + R * sin(f.a), D.cy - R * cos(f.a), 6, dt) < 1.2) f.h += clamp(wrap(atan2(b.x - f.x, -(b.y - f.y)) - f.h), -6 * dt, 6 * dt); + if ((f.t -= dt) <= 0) { + f.m = CRAWL; + f.t = 1 + 2 * rnd(); + f.h += PI; + } + } +} + +function crawl(S, b, dt) { + b.w += (rnd() - 0.5) * 10 * sqrt(dt); + b.w *= 1 - min(1, 1.5 * dt); + let want = null; + const ex = b.x < 4 ? 1 : b.x > W - 4 ? -1 : 0; + const ey = b.y < TOP + 4 ? 1 : b.y > BOT - 4 ? -1 : 0; + if (ex || ey) want = atan2(ex + (ey ? 0 : 0.01 * (32 - b.x)), -ey); + b.h += b.w * dt; + if (want !== null) b.h += clamp(wrap(want - b.h), -3 * dt, 3 * dt); + // Clear of the dances, unless following one. + for (const D of S.dances) { + const dx = b.x - D.cx; + const dy = b.y - D.cy; + const r = hypot(dx, dy); + if (D.on && r < D.L / 2 + 3) { + b.x += (dx / (r + 0.01)) * 3 * dt; + b.y += (dy / (r + 0.01)) * 3 * dt; + b.h += clamp(wrap(atan2(dx, -dy) - b.h), -3 * dt, 3 * dt); + } + } + const st = b.v * dt; + b.x += sin(b.h) * st; + b.y -= cos(b.h) * st; +} + +function step(S, dt) { + const { bees } = S; + for (const b of bees) { + const m = b.m; + if (m === DANCE || m === HOME || m === FOLLOW) continue; + if (m === EXIT) { + if (seek(b, ENX, BOT + 1.5, 7, dt) < 1.5) b.out = true; + continue; + } + if (m === LEAVE) { + b.h += clamp(wrap((b.x < 32 ? -PI / 2 : PI / 2) - b.h), -3 * dt, 3 * dt); + b.x += sin(b.h) * b.v * dt; + b.y -= cos(b.h) * b.v * dt; + if (b.x < -5 || b.x > W + 5) b.out = true; + continue; + } + if (m === CRAWL) crawl(S, b, dt); + else if (m === REST) b.h += sin(S.t * 6 + b.ph) * 0.5 * dt; + if ((b.t -= dt) > 0) continue; + if (m === DIP) { + b.x -= sin(b.h) * 1.5; + b.y += cos(b.h) * 1.5; + b.h += PI * (0.6 + 0.8 * rnd()); + } + const u = rnd(); + b.m = CRAWL; + b.t = 1 + 3 * rnd(); + if (u < 0.28) { + b.m = REST; + b.t = 0.8 + 2.5 * rnd(); + } else if (u < 0.5 && m !== DIP) { + // Head first into a cell: to feed a larva, take nectar or pack pollen. + const hx = round(b.x + sin(b.h) * 2.2 - 0.5); + const hy = round(b.y - cos(b.h) * 2.2 - 0.5); + if (hx >= 0 && hx < W && hy >= TOP + 2 && hy < BOT - 2) { + const i = S.cid[hy * W + hx]; + const t = S.type[i]; + if (t !== HONEY && t !== BROOD) { + const r = floor(i / NC); + const c = i % NC; + b.m = DIP; + b.t = 2 + 4 * rnd(); + b.x = cellX(r, c) - sin(b.h) * 1.5; + b.y = cellY(r) + cos(b.h) * 1.5; + } + } + } + } + for (const D of S.dances) stepDance(S, D, dt); + S.dances = S.dances.filter((D) => !D.gone); + for (const b of bees) if (b.D && (b.D.gone || (b.m !== FOLLOW && b.m !== DANCE && b.m !== HOME))) b.D = null; + // Bees shoulder each other apart; those still at a cell or dancing stand firm. + const G = S.grid; + for (const k of G) k.length = 0; + for (const b of bees) { + const gx = clamp(floor(b.x / 8), 0, 7); + const gy = clamp(floor(b.y / 8), 0, 15); + G[gy * 8 + gx].push(b); + } + for (const a of bees) { + if (a.m === REST || a.m === DIP || a.m === DANCE) continue; + const gx = clamp(floor(a.x / 8), 0, 7); + const gy = clamp(floor(a.y / 8), 0, 15); + for (let j = max(0, gy - 1); j <= min(15, gy + 1); j++) + for (let i = max(0, gx - 1); i <= min(7, gx + 1); i++) + for (const b of G[j * 8 + i]) { + if (b === a) continue; + const dx = a.x - b.x; + const dy = a.y - b.y; + const r = hypot(dx, dy); + if (r < 3.6 && r > 0.01) { + const k = (3.6 - r) * 0.25; + a.x += (dx / r) * k; + a.y += (dy / r) * k; + } + } + } + S.bees = bees.filter((b) => !b.out); +} + +export function setup(params) { + const S = { + t: 0, started: false, sunA: 0, sunK: 9, next: 6, add: 0, + cid: new Int16Array(N), cid2: new Int16Array(N), hd: new Float32Array(N), dx: new Float32Array(N), dy: new Float32Array(N), + type: new Uint8Array(NR * NC), age: new Float32Array(NR * NC), tint: new Float32Array(NR * NC), gap: new Float32Array(NR * NC), pol: new Uint8Array(NR * NC), + base: new Float32Array(N * 3), expo: new Float32Array(N), A: new Float32Array(N * 3), X: new Float32Array(N), + amb: new Float32Array(N), pool: new Float32Array(N), sun: new Float32Array(N), jit: new Float32Array(N * 3), + bees: [], dances: [], grid: Array.from({ length: 128 }, () => []), + c0: 4.2, c1: 8.5, cd: 0.7, + }; + grid(S); + layout(S); + paint(S); + for (let p = 0; p < N; p++) { + const x = p % W; + const y = (p / W) | 0; + S.amb[p] = 0.84 + 0.12 * (1 - y / H) + 0.1 * (vn(x * 0.05, y * 0.04) - 0.5) - 0.12 * ((x - 31.5) / 32) ** 4; + S.pool[p] = 0.4 + 0.65 * exp(-((x - 42) ** 2 + (y - 46) ** 2) / 2600); + const j = 0.94 + 0.12 * hash(p, 19); + for (let c = 0; c < 3; c++) S.jit[p * 3 + c] = j * (0.97 + 0.06 * hash(p, 20 + c)); + } + params.s = S; +} + +function start(S, n) { + // Bees thickest on the brood, two dances already going. + for (let k = 0; k < n; k++) { + let x, y; + for (let j = 0; j < 4; j++) { + x = 2 + 60 * rnd(); + y = TOP + 3 + (BOT - TOP - 6) * rnd(); + if (rnd() < 0.35 + 0.65 * sm(30, 70, y)) break; + } + newBee(S, x, y, rnd() * 6.28); + } + for (let k = 0; k < 2; k++) { + const b = newBee(S, 0, 0, 0); + const D = newDance(S, b, false); + D.fl = k ? 0.08 : -0.1; + D.th = D.fl - S.sunA; + D.cx = k ? 22 : 40; + D.cy = k ? 109 : 90; + D.on = true; + b.m = DANCE; + D.tau = rnd() * (D.tw + D.tr); + D.rec = 1.5; + stepDance(S, D, 0); + b.h = D.tau < D.tw ? D.th : b.h; + recruit(S, D, true); + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s; + const k = input.knobValues || [0, 2, 0, 0.06]; + dt = clamp(dt || 0, 0, 0.1); + S.light = clamp(k[0], 0, 1); + const tempo = clamp(k[1], 0.2, 3) / 2; + const want = round(clamp(k[2], 80, 240)); + const sk = clamp(k[3], -1, 1); + S.sunA = sk * PI; + if (abs(sk - S.sunK) > 0.002) sunPatch(S, sk); + if (!S.started) { + S.started = true; + start(S, want); + } + const d = dt * tempo; + S.t += d; + // Clouds cross the sun now and then; the first one early. + if (S.t > S.c1 + 1.5) { + S.c0 = S.t + 8 + 20 * rnd(); + S.c1 = S.c0 + 3 + 6 * rnd(); + S.cd = 0.4 + 0.5 * rnd(); + } + S.cloud = 1 - S.cd * sm(S.c0, S.c0 + 1.5, S.t) * sm(S.c1 + 1.5, S.c1, S.t); + const vig = input.btnPressed && input.btnPressed[0]; + if ((S.next -= d) <= 0 || vig) { + if (S.dances.length < (vig ? 4 : 3)) { + const b = newBee(S, ENX + 4 * (rnd() - 0.5), BOT + 1, 0); + b.m = HOME; + newDance(S, b, vig); + } + if (!vig) S.next = 5 + 8 * rnd(); + } + // Bees walk in from the sides, or wander off them, to match the knob. + let ord = 0; + for (const b of S.bees) if (b.m <= FOLLOW) ord++; + if ((S.add -= dt) <= 0) { + S.add = 0.1; + if (ord < want) { + const l = rnd() < 0.5; + newBee(S, l ? -3 : W + 3, TOP + 8 + (BOT - TOP - 16) * rnd(), l ? PI / 2 : -PI / 2).t = 1.5; + } else if (ord > want + 2) { + const b = S.bees.find((b) => b.m === CRAWL); + if (b) b.m = LEAVE; + } + } + step(S, d); +} + +// One bee into the albedo: her body worked out on a 12 x 12 patch first, +// then her shadow a step down and right, head, thorax, banded abdomen, wings. +const PC = new Float32Array(144); +const PP = new Uint8Array(144); +const PU = new Float32Array(144); +const PV = new Float32Array(144); +function bee(S, b) { + const { A, X } = S; + const s = b.s; + const fx = sin(b.h); + const fy = -cos(b.h); + const dip = b.m === DIP; + const wg = b.wag > 0; + const wag = wg ? b.wag * sin(b.wp) : b.m === REST ? 0.15 * sin(S.t * 9 + b.ph) : 0; + const bl = wg ? 0.4 : 0; + const th = THX[b.tone]; + const am = AMB[b.tone]; + const shim = 0.24 + 0.14 * sin(S.t * 2.3 + b.ph + b.x * 0.3); + const x0 = floor(b.x) - 5; + const y0 = floor(b.y) - 5; + for (let j = 0, k = 0; j < 12; j++) + for (let i = 0; i < 12; i++, k++) { + const X0 = x0 + i + 0.5 - b.x; + const Y0 = y0 + j + 0.5 - b.y; + const u = (X0 * fx + Y0 * fy) / s; + const v = (Y0 * fx - X0 * fy) / s - wag * (0.3 + 0.7 * clamp(-u / 2.5, 0, 1)); + PC[k] = shape(u, v, s, dip, bl); + PP[k] = PART; + PU[k] = u; + PV[k] = v; + } + for (let j = 0, k = 0; j < 12; j++) + for (let i = 0; i < 12; i++, k++) { + const px = x0 + i; + const py = y0 + j; + if (px < 0 || px >= W || py < 0 || py >= H) continue; + const p = py * W + px; + const q = p * 3; + const c = PC[k]; + const sh = i && j ? PC[k - 13] : 0; + if (sh > 0) { + const f = 1 - 0.7 * sh * (1 - c); + A[q] *= f; + A[q + 1] *= f; + A[q + 2] *= f; + } + const u = PU[k]; + const v = PV[k]; + // A waggling dancer buzzes her wings into a pale blur round her. + let wa = wg ? 0.55 * clamp(1.6 - hypot((u + 0.7) / 1.5, v / 1.7), 0, 1) : 0; + if (c <= 0 && wa <= 0) continue; + let r, g, bb; + const pt = PP[k]; + if (pt === 1) [r, g, bb] = HEAD; + else if (pt === 2) [r, g, bb] = th; + else { + const st = u > -0.85 ? 1 : u > -1.35 ? 0 : u > -1.9 ? 0.8 : 0; + r = DARK[0] + (am[0] - DARK[0]) * st; + g = DARK[1] + (am[1] - DARK[1]) * st; + bb = DARK[2] + (am[2] - DARK[2]) * st; + } + if (u > -2.2 && u < 0.3 && abs(v) > 0.35 && abs(v) < 1.15) wa = max(wa, shim); + r += (WING[0] - r) * wa; + g += (WING[1] - g) * wa; + bb += (WING[2] - bb) * wa; + const a = max(c, wa); + A[q] += (r - A[q]) * a; + A[q + 1] += (g - A[q + 1]) * a; + A[q + 2] += (bb - A[q + 2]) * a; + X[p] += (1 - X[p]) * a; + } +} + +// Coverage of a bee's body at (u along, v across) in body lengths of +// about five LEDs; PART says which piece. +let PART = 0; +function shape(u, v, s, dip, bl) { + let c = clamp((1 - hypot((u + 1.3) / 1.5, v / (1.12 + bl))) * 1.2 * s + 0.5, 0, 1); + PART = 3; + if (!dip || u < 0.3) { + const k = clamp((1 - hypot((u - 0.5) / 0.8, v)) * 0.9 * s + 0.5, 0, 1); + if (k > c) [c, PART] = [k, 2]; + } + if (!dip) { + const k = clamp((1 - hypot((u - 1.65) / 0.6, v / 0.75)) * 0.7 * s + 0.5, 0, 1); + if (k > c) [c, PART] = [k, 1]; + } + return c; +} + +const knee = (c) => (c < 190 ? c : 190 + 65 * (1 - exp(-(c - 190) / 65))); + +export function draw(display, params) { + const S = params.s; + if (!S || !S.started) return; + const { A, X, base, expo, amb, sun, pool, jit } = S; + A.set(base); + X.set(expo); + for (const b of S.bees) if (b.m !== DANCE) bee(S, b); + for (const b of S.bees) if (b.m === DANCE) bee(S, b); + // Daylight warms and fades as the curtain closes, the sun goes first, and the red lamp comes up. + const L = S.light; + const day = (1 - sm(0.1, 0.85, L)) * 255; + const dc = [day, day * (0.94 - 0.2 * L), day * (0.82 - 0.35 * L)]; + const sk = 0.65 * S.cloud * (1 - sm(0.05, 0.55, L)) * 255; + const red = 0.85 * sm(0.3, 0.95, L) * 255; + for (let p = 0; p < N; p++) { + const q = p * 3; + const a = amb[p]; + const s = sun[p] * X[p] * sk; + const rp = pool[p] * red; + const r = knee(A[q] * (a * dc[0] + s * SUNC[0] + rp * REDC[0]) * jit[q]); + const g = knee(A[q + 1] * (a * dc[1] + s * SUNC[1] + rp * REDC[1]) * jit[q + 1]); + const b = knee(A[q + 2] * (a * dc[2] + s * SUNC[2] + rp * REDC[2]) * jit[q + 2]); + display.setPixel(p % W, (p / W) | 0, r | 0, g | 0, b | 0); + } +} From ca0dc8f4e4b9f2cb805eec9a80f726cfd8334584 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:28:27 +0000 Subject: [PATCH 12/33] thermal: blended cloud shading, smaller file --- incoming/pattern-study/patterns/thermal.js | 587 +++++++-------------- 1 file changed, 189 insertions(+), 398 deletions(-) diff --git a/incoming/pattern-study/patterns/thermal.js b/incoming/pattern-study/patterns/thermal.js index c3a803a6e..f91b7cc31 100644 --- a/incoming/pattern-study/patterns/thermal.js +++ b/incoming/pattern-study/patterns/thermal.js @@ -1,48 +1,38 @@ // ===== Patternflow pattern ===== // Title: Thermal // Family: flight -// Idea: On a summer afternoon a white sailplane circles up a thermal off a dark ploughed field in a corkscrew of ellipses, sharing it with two buzzards and rising thistledown, until it reaches the base of the cumulus the thermal is building and glides off to find the next one while another glider joins low down. +// Idea: A white sailplane corkscrews up a summer thermal off a dark ploughed field with two buzzards and rising thistledown, until at the base of the cumulus the thermal is building it straightens and glides off, and another glider joins low down. // Author: patternflow-lab // Date: 2026-10-24 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: original, pattern round 24, after thermal soaring under fair-weather cumulus over English farmland +// Lineage: original, pattern round 24, after thermal soaring under fair-weather cumulus over farmland // Round: 24 // =============================== // @matrix 64x128 // @knobs Hour=0..1, Speed=0.2..3, Gliders=1..4, Wind=-1..1 // -// Knob 1: Hour. Bright midday at 0, with a deep blue sky, white cloud and short shadows, turning to golden late afternoon at 1, when the sun comes in low from the left, the haze goes amber and the hedges and trees throw long shadows across the fields. -// Knob 2: Speed. At 2 the glider takes about six seconds a turn and climbs from the hedges to the cloud base in some twenty; the cloud builds, drifts off and is replaced about every 26 s. At 0.2 it all runs ten times slower. -// Knob 3: Gliders. How many gliders share the thermal, stacked at different heights, from one to four. A day good enough for four also builds a bigger cloud with a darker base, and sends more thistledown up the column. -// Knob 4: Wind. The thermal and its cloud lean downwind, so the turns drift sideways as the gliders climb; the cloud shadows and the far cumulus travel faster. Near 0 it is almost calm. -// Buttons: K1 tap winch-launches a new glider from the field: it rolls, rears up on the cable, drops it and turns into the thermal. +// Knob 1: Hour. Bright midday at 0, to golden late afternoon at 1: the sun comes in low from the left, the haze goes amber and hedges and trees throw long shadows. +// Knob 2: Speed. At 2 a turn takes six seconds, the climb to the cloud base about twenty, and each cloud builds for 26 s before it drifts off. At 0.2 all of it runs ten times slower. +// Knob 3: Gliders. One to four gliders stacked in the thermal. A day good enough for four also builds a bigger cloud with a darker base and sends more thistledown up. +// Knob 4: Wind. The thermal and its cloud lean downwind, so the turns drift as the gliders climb, and cloud shadows and far cumulus travel faster. Near 0 it is almost calm. +// Buttons: K1 tap winch-launches a glider from the field; it rears up on the cable, lets go and turns into the thermal. -const W = 64; -const H = 128; +const [W, H] = [64, 128]; const N = W * H; -const { min, max, sin, cos, abs, sqrt, exp, floor, round, pow, PI } = Math; +const { min, max, sin, cos, abs, sqrt, exp, floor, round, pow, sign, PI } = Math; const TAU = 2 * PI; const clamp = (v, a, b) => (v < a ? a : v > b ? b : v); -const sstep = (a, b, v) => { - const t = clamp((v - a) / (b - a), 0, 1); - return t * t * (3 - 2 * t); -}; +const sstep = (a, b, v) => ((v = clamp((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); const mix = (a, b, t) => a + (b - a) * t; const mix3 = (a, b, t) => [mix(a[0], b[0], t), mix(a[1], b[1], t), mix(a[2], b[2], t)]; const rnd = () => Math.random(); -// The horizon, the first row of fields, the cloud base, and the row where the thermal leaves the ground. -const HOR = 97; -const FY = 100; -const YB = 36; -const YG = 116; -// The ploughed field the thermal comes off: its column on the panel and its distance. -const SRCX = 29; -const DSRC = 9.6; -// We look up a little at the gliders, so a level circle shows as a thin ellipse. +// Rows: horizon, first field row, cloud base, where the thermal leaves the ground. +const [HOR, FY, YB, YG] = [97, 100, 36, 116]; +// The ploughed field the thermal comes off: its column and its distance. +const [SRCX, DSRC] = [29, 9.6]; +// We look up a little, so a level circle is a thin ellipse; the craft are drawn flatter so a banked wing does not stand on end. const SE = sin(0.3); -// The crafts themselves are drawn from nearer their own level, so a banked wing does not stand on end. -const BE = sin(0.1); -const BC = cos(0.1); +const [BE, BC] = [sin(0.1), cos(0.1)]; const hash = (x, y, s) => { let h = Math.imul((x * 374761393 + y * 668265263 + s * 982451653) | 0, 1274126177); @@ -50,47 +40,20 @@ const hash = (x, y, s) => { return ((h ^ (h >>> 16)) >>> 0) / 4294967296; }; function noise(x, y, s) { - const ix = floor(x); - const iy = floor(y); - const fx = x - ix; - const fy = y - iy; - const u = fx * fx * (3 - 2 * fx); - const v = fy * fy * (3 - 2 * fy); - const a = hash(ix, iy, s); - const b = hash(ix + 1, iy, s); - const c = hash(ix, iy + 1, s); - const d = hash(ix + 1, iy + 1, s); - return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v; + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy, u = fx * fx * (3 - 2 * fx), v = fy * fy * (3 - 2 * fy); + const a = hash(ix, iy, s), b = hash(ix + 1, iy, s), c = hash(ix, iy + 1, s); + return a + (b - a) * u + (c - a) * v + (a - b - c + hash(ix + 1, iy + 1, s)) * u * v; } const fbm = (x, y, s) => 0.55 * noise(x, y, s) + 0.3 * noise(x * 2.1, y * 2.1, s + 7) + 0.15 * noise(x * 4.3, y * 4.3, s + 13); -// Crops in full midday sun, and their texture: 0 mottled, 1 rows that run away from us. -const CROP = [ - [70, 150, 45, 0], - [38, 106, 36, 1], - [216, 184, 94, 1], - [198, 160, 66, 0], - [142, 178, 74, 0], - [96, 62, 40, 1], -]; +// Pasture, beans, stubble, wheat, hay and plough in midday sun; 1 marks rows running away from us. +const CROP = [[70, 150, 45, 0], [38, 106, 36, 1], [216, 184, 94, 1], [198, 160, 66, 0], [142, 178, 74, 0], [96, 62, 40, 1]]; const PICK = [0, 0, 0, 1, 1, 2, 2, 2, 3, 3, 4, 4]; +// Tapered strokes in the craft's axes: forward, right, up, twice, then two radii. +const GLIDER = [[3.6, 0, -0.1, -4.8, 0, 0.15, 0.85, 0.45], [0.6, 0, 0.25, 0.25, -9, 0.85, 0.75, 0.45], [0.6, 0, 0.25, 0.25, 9, 0.85, 0.75, 0.45], [-4.2, 0, 0.1, -4.9, 0, 2.4, 0.55, 0.45], [-4.9, -2.6, 2.4, -4.9, 2.6, 2.4, 0.45, 0.45]]; +const BUZZ = [[1.3, 0, 0, -1.3, 0, 0, 0.6, 0.45], [0.2, 0, 0.1, -0.3, -3.7, 0.75, 0.8, 0.45], [0.2, 0, 0.1, -0.3, 3.7, 0.75, 0.8, 0.45], [-1.2, 0, 0, -2.5, 0, 0.1, 0.45, 0.65]]; -// Bodies as tapered strokes in the craft's own axes: forward, right, up, and two radii. -const GLIDER = [ - [3.6, 0, -0.1, -4.8, 0, 0.15, 0.85, 0.45], - [0.6, 0, 0.25, 0.25, -9, 0.85, 0.75, 0.45], - [0.6, 0, 0.25, 0.25, 9, 0.85, 0.75, 0.45], - [-4.2, 0, 0.1, -4.9, 0, 2.4, 0.55, 0.45], - [-4.9, -2.6, 2.4, -4.9, 2.6, 2.4, 0.45, 0.45], -]; -const BUZZ = [ - [1.3, 0, 0, -1.3, 0, 0, 0.6, 0.45], - [0.2, 0, 0.1, -0.3, -3.7, 0.75, 0.8, 0.45], - [0.2, 0, 0.1, -0.3, 3.7, 0.75, 0.8, 0.45], - [-1.2, 0, 0, -2.5, 0, 0.1, 0.45, 0.65], -]; - -// A cumulus as round puffs over a flat base: u across, v up, r, and when in its growth each puff appears. +// A cumulus as puffs over a flat base: u across, v up, r, and when in its growth each appears. function puffs() { const L = []; const add = (u, v, r, t0) => L.push({ u: u + (rnd() - 0.5) * 0.14, v: v + (rnd() - 0.5) * 0.08, r: r * (0.88 + 0.24 * rnd()), t0, ph: rnd() * TAU }); @@ -102,13 +65,10 @@ function puffs() { } const newCloud = (x, a) => ({ x, a, g: 0, al: 1, det: false, p: puffs(), sd: floor(rnd() * 999) }); +// Fields lie in strips at distance D (row = HOR + 180 / D), each cut into fields across. function buildFields(S) { - const strips = []; - const trees = []; - const us = ((SRCX - 32) * DSRC) / 64; - const hw = (11 * DSRC) / 64; - let D = 5.6; - while (D < 66) { + const strips = [], trees = [], us = ((SRCX - 32) * DSRC) / 64, hw = (11 * DSRC) / 64; + for (let D = 5.6; D < 66; ) { const D2 = D * (1.22 + 0.12 * rnd()); const wid = () => ((12 + 18 * rnd()) * pow(6 / D, 0.35) * D) / 64; let b = []; @@ -127,87 +87,56 @@ function buildFields(S) { t.push(k); } if (src) t[b.indexOf(us - hw)] = 5; - const s = { d0: D, d1: D2, b, t, hh: 0.12 + 0.08 * rnd(), i: strips.length }; - strips.push(s); - // Trees stand in the hedge along the near edge of each strip, and at some field corners. + strips.push({ d0: D, d1: D2, b, t, hh: 0.12 + 0.08 * rnd(), i: strips.length }); + // Trees in the hedge along each strip's near edge, and at some corners. for (let j = 0; j < 2; j++) if (rnd() < 0.6) trees.push({ u: (rnd() - 0.5) * 1.1 * D, D: D + 0.05, rc: 0.2 + 0.14 * rnd() }); for (let j = 1; j < b.length - 1; j++) if (rnd() < 0.2) trees.push({ u: b[j], D: D + 0.05, rc: 0.2 + 0.12 * rnd() }); D = D2; } for (const tr of trees) tr.hc = tr.rc * 1.5 + 0.15; - trees.sort((p, q) => q.D - p.D); S.strips = strips; - S.trees = trees; + S.trees = trees.sort((p, q) => q.D - p.D); } export function setup(params) { - const S = { - t: 0, - hour: -1, - bg: new Float32Array(N * 3), - buf: new Float32Array(N * 3), - A: new Float32Array(N), - fu: new Float32Array(N), - fd: new Float32Array(N), - fn: new Float32Array(N), - gl: [], - down: [], - clouds: [], - far: [], - shad: [], - spawnG: 0, - spawnB: 0, - cable: null, - started: false, - }; + const F = () => new Float32Array(N); + const S = { t: 0, bg: new Float32Array(N * 3), buf: new Float32Array(N * 3), A: F(), fu: F(), fd: F(), fn: F(), gl: [], down: [], clouds: [], far: [], spawn: [0, 0], cable: null, bb: [1e9, 1e9, -1, -1] }; buildFields(S); for (let y = FY; y < H; y++) for (let x = 0; x < W; x++) { const p = y * W + x; - const D = 180 / (y + 0.5 - HOR); - S.fd[p] = D; - S.fu[p] = ((x + 0.5 - 32) * D) / 64; + S.fd[p] = 180 / (y + 0.5 - HOR); + S.fu[p] = ((x + 0.5 - 32) * S.fd[p]) / 64; S.fn[p] = fbm(x * 0.15, y * 0.3, 11); } - for (let i = 0; i < 3; i++) S.far.push({ x: 8 + i * 24 + rnd() * 10, yb: 87 - i * 5 - rnd() * 2, hw: 5 + 2.5 * i + rnd() * 2, ht: 3 + i + rnd() * 1.5, p: puffs(), sd: floor(rnd() * 999) }); - S.shad.push({ x: 6, D: 7.4, r: 19, rd: 2.6 }, { x: 50, D: 16, r: 15, rd: 5.5 }); + for (let i = 0; i < 3; i++) S.far.push({ x: 8 + i * 24 + rnd() * 10, yb: 87 - i * 5 - rnd() * 2, hw: 5 + 2.5 * i + rnd() * 2, ht: 3 + i + rnd() * 1.5, p: puffs(), sd: floor(rnd() * 999), m: 0.35, e: 44 }); + S.shad = [{ x: 6, D: 7.4, r: 19, rd: 2.6, m: 0.9, e: 62 }, { x: 50, D: 16, r: 15, rd: 5.5, m: 0.9, e: 60 }]; params.s = S; } // The thermal's centre at a row: it leans downwind with height and wanders a little. const colX = (S, y) => SRCX + S.lean * (YG - y) + 1.5 * sin(S.t * 0.21); - -function sunLight(h) { - const k = mix(1, 0.9, h); - return [mix(1, 1.16, h) * k, mix(1, 0.86, h) * k, mix(1, 0.56, h) * k]; -} +const sunLight = (h, k = mix(1, 0.9, h)) => [mix(1, 1.16, h) * k, mix(1, 0.86, h) * k, mix(1, 0.56, h) * k]; const SHADE = [0.42, 0.47, 0.6]; const hazeOf = (h) => mix3([176, 202, 222], [236, 186, 122], h); function skyAt(x, y, h) { - const top = mix3([14, 50, 150], [30, 52, 128], h); - const mid = mix3([44, 108, 206], [70, 104, 178], h); - const lo = hazeOf(h); - const t = y / 96; - const c = t < 0.62 ? mix3(top, mid, sstep(0, 0.62, t)) : mix3(mid, lo, sstep(0.62, 1, t)); + const top = mix3([14, 50, 150], [30, 52, 128], h), mid = mix3([44, 108, 206], [70, 104, 178], h), t = y / 96; + const c = t < 0.66 ? mix3(top, mid, sstep(0, 0.66, t)) : mix3(mid, hazeOf(h), sstep(0.66, 1, t)); const gw = pow(h, 1.5) * exp(-x / 24) * exp(-abs(y - 78) / 36); return [c[0] + 110 * gw, c[1] + 64 * gw, c[2] + 14 * gw]; } -// The ground colour seen at a point of the panel: hedges, fields, their shadows, and the haze. +// The ground seen at a point of the panel: hedges, fields, their shadows, and the haze. function fieldAt(S, x, y, sun, Lsh, haze) { - const D = 180 / (y - HOR); - const u = ((x - 32) * D) / 64; - const st = S.strips; + const D = 180 / (y - HOR), u = ((x - 32) * D) / 64, st = S.strips; let k = 0; while (k < st.length - 1 && D >= st[k].d1) k++; - const hz = 0.55 * sstep(5, 75, D); let alb = null; let lit = 1; - // A hedge on the near edge of a strip hides the ground just behind it. + // The hedge on a strip's near edge hides the ground just behind it. for (let j = k; j >= 0 && st[j].d0 > D * 0.8; j--) { const s = st[j]; - const hh = s.hh * (0.7 + 0.6 * noise(u * 2.6, s.i * 9, 3)); - if (y >= HOR + (180 - 64 * hh) / s.d0) { + if (y >= HOR + (180 - 64 * s.hh * (0.7 + 0.6 * noise(u * 2.6, s.i * 9, 3))) / s.d0) { const n = 0.8 + 0.4 * noise(u * 6, y, 5); alb = [34 * n, 78 * n, 30 * n]; lit = 0.85; @@ -215,8 +144,7 @@ function fieldAt(S, x, y, sun, Lsh, haze) { } } if (!alb) { - const s = st[k]; - const b = s.b; + const s = st[k], b = s.b; let j = 0; while (j < b.length - 2 && u >= b[j + 1]) j++; const hwu = ((0.8 * pow(6 / D, 0.4) + 0.25) * D) / 64; @@ -224,19 +152,15 @@ function fieldAt(S, x, y, sun, Lsh, haze) { alb = [30, 70, 28]; lit = 0.7; } else { - const c = CROP[s.t[j]]; - const tx = c[3] ? 0.8 + 0.34 * (0.5 + 0.5 * sin(u * TAU * 6.5)) : 0.86 + 0.28 * fbm(u * 2.5, D * 0.7, s.t[j] + j); + const c = CROP[s.t[j]], tx = c[3] ? 0.97 + 0.17 * sin(u * TAU * 6.5) : 0.86 + 0.28 * fbm(u * 2.5, D * 0.7, s.t[j] + j); alb = [c[0] * tx, c[1] * tx, c[2] * tx]; - // Shadows: of the hedge on the field's left, of the hedge in front, and of the trees. + // Shadows of the hedges left and in front, and of the trees. if (u - b[j] < s.hh * Lsh * 0.85 || D - s.d0 < s.hh * Lsh * 0.4) lit = 0; else for (const tr of S.trees) { - const du = u - tr.u - tr.hc * Lsh * 0.85; - const dd = D - tr.D - tr.hc * Lsh * 0.45; + const du = u - tr.u - tr.hc * Lsh * 0.85, dd = D - tr.D - tr.hc * Lsh * 0.45; if (abs(dd) > 3 + Lsh) continue; - const len = tr.rc * (1 + 0.55 * Lsh); - const al = (du * 0.86 + dd * 0.5) / len; - const pe = (-du * 0.5 + dd * 0.86) / (tr.rc * 1.8); + const al = (du * 0.86 + dd * 0.5) / (tr.rc * (1 + 0.55 * Lsh)), pe = (-du * 0.5 + dd * 0.86) / (tr.rc * 1.8); if (al * al + pe * pe < 1) { lit = 0; break; @@ -244,68 +168,38 @@ function fieldAt(S, x, y, sun, Lsh, haze) { } } } - const out = [0, 0, 0]; - for (let c = 0; c < 3; c++) out[c] = mix(alb[c] * mix(SHADE[c], sun[c], lit), haze[c], hz); - return out; + const hz = 0.55 * sstep(5, 75, D); + return [0, 1, 2].map((c) => mix(alb[c] * mix(SHADE[c], sun[c], lit), haze[c], hz)); } function paint(S) { - const h = S.hour; - const sun = sunLight(h); - const haze = hazeOf(h); - const Lsh = 0.35 + 4.2 * pow(h, 1.6); - const bg = S.bg; - const farT = []; - const nearT = []; - for (let x = 0; x < W; x++) { - farT.push(83 + 5 * fbm(x * 0.045, 1.3, 21)); - nearT.push(91.5 + 3 * fbm(x * 0.09, 4.1, 22) - 1.5 * sstep(0.55, 0.8, noise(x * 0.5, 0, 23))); - } - const ridgeF = mix3(haze, [64, 96, 116], 0.38); + const h = S.hour, sun = sunLight(h), haze = hazeOf(h), Lsh = 0.35 + 4.2 * pow(h, 1.6), bg = S.bg, ridgeF = mix3(haze, [64, 96, 116], 0.38); const ridgeN = mix3(haze, [52 * sun[0], 98 * sun[1], 54 * sun[2]], 0.58); - for (let y = 0; y < H; y++) - for (let x = 0; x < W; x++) { + for (let x = 0; x < W; x++) { + const farT = 83 + 5 * fbm(x * 0.045, 1.3, 21), nearT = 91.5 + 3 * fbm(x * 0.09, 4.1, 22) - 1.5 * sstep(0.55, 0.8, noise(x * 0.5, 0, 23)); + for (let y = 0; y < H; y++) { const q = (y * W + x) * 3; - let r = 0; - let g = 0; - let b = 0; - for (let sy = 0; sy < 2; sy++) - for (let sx = 0; sx < 2; sx++) { - const xs = x + 0.25 + 0.5 * sx; - const ys = y + 0.25 + 0.5 * sy; - let c; - if (ys >= FY) c = fieldAt(S, xs, ys, sun, Lsh, haze); - else if (ys >= nearT[x]) { - const m = 0.84 + 0.3 * fbm(x * 0.35, ys * 0.9, 24); - c = [ridgeN[0] * m, ridgeN[1] * m, ridgeN[2] * m]; - } else if (ys >= farT[x]) c = ridgeF; - else c = skyAt(xs, ys, h); - r += c[0]; - g += c[1]; - b += c[2]; - } - bg[q] = r / 4; - bg[q + 1] = g / 4; - bg[q + 2] = b / 4; + bg[q] = bg[q + 1] = bg[q + 2] = 0; + for (let k = 0; k < 4; k++) { + const xs = x + 0.25 + 0.5 * (k & 1), ys = y + 0.25 + 0.5 * (k >> 1); + let c; + if (ys >= FY) c = fieldAt(S, xs, ys, sun, Lsh, haze); + else if (ys >= nearT) c = ridgeN.map((v) => v * (0.84 + 0.3 * fbm(x * 0.35, ys * 0.9, 24))); + else if (ys >= farT) c = ridgeF; + else c = skyAt(xs, ys, h); + for (let i = 0; i < 3; i++) bg[q + i] += c[i] / 4; + } } + } // Tree crowns, far ones first, lit from the sun's side. for (const tr of S.trees) { - const cx = 32 + (64 * tr.u) / tr.D; - const cy = HOR + (180 - 64 * tr.hc) / tr.D; - const R = (64 * tr.rc) / tr.D; - if (cx < -R - 1 || cx > W + R + 1 || cy - R > H) continue; + const cx = 32 + (64 * tr.u) / tr.D, cy = HOR + (180 - 64 * tr.hc) / tr.D, R = (64 * tr.rc) / tr.D, hz = 0.55 * sstep(5, 75, tr.D); for (let y = max(FY - 4, floor(cy - R - 1)); y <= min(H - 1, cy + R + 1); y++) for (let x = max(0, floor(cx - R - 1)); x <= min(W - 1, cx + R + 1); x++) { - const dx = (x + 0.5 - cx) / R; - const dy = (y + 0.5 - cy) / R; - const a = clamp((1 - sqrt(dx * dx + dy * dy)) * R + 0.5, 0, 1); + const dx = (x + 0.5 - cx) / R, dy = (y + 0.5 - cy) / R, a = clamp((1 - sqrt(dx * dx + dy * dy)) * R + 0.5, 0, 1); if (a <= 0) continue; - const l = clamp(0.45 - 0.7 * dx - 0.4 * dy, 0, 1); - const n = 0.85 + 0.3 * hash(x, y, 31); - const q = (y * W + x) * 3; - const hz = 0.55 * sstep(5, 75, tr.D); - const al = [40, 94, 38]; - for (let c = 0; c < 3; c++) bg[q + c] += (mix(al[c] * n * mix(SHADE[c], sun[c] * 1.1, l), haze[c], hz) - bg[q + c]) * a; + const l = clamp(0.45 - 0.7 * dx - 0.4 * dy, 0, 1), n = 0.85 + 0.3 * hash(x, y, 31), q = (y * W + x) * 3; + [40, 94, 38].forEach((al, c) => (bg[q + c] += (mix(al * n * mix(SHADE[c], sun[c] * 1.1, l), haze[c], hz) - bg[q + c]) * a)); } } S.painted = h; @@ -313,90 +207,62 @@ function paint(S) { function addCraft(S, kind, st, X, yc, dir) { const R = kind ? 5.5 + rnd() : 11.5 + rnd(); - const o = { kind, st, X, yc, Z: -dir * R, R, dir, th: dir > 0 ? -PI / 2 : PI / 2, bank: 0, pitch: 0, off: 0, leave: false, s: 0, w: kind ? 1.75 + 0.3 * rnd() : 1.05, c0: kind ? 1.9 : 2.4 }; + const o = { kind, st, X, yc, Z: -dir * R, R, dir, th: (-dir * PI) / 2, bank: st === "climb" ? 0.5 + 0.12 * kind : 0, pitch: 0, off: 0, s: 0, w: kind ? 1.75 + 0.3 * rnd() : 1.05, c0: kind ? 1.9 : 2.4 }; S.gl.push(o); return o; } -function launch(S) { - const left = rnd() < 0.5; - const X0 = left ? 4 + rnd() * 10 : 50 + rnd() * 10; - const dir = left ? 1 : -1; - const o = addCraft(S, 0, "launch", X0, 0, dir); - o.yc = 121 + o.Z * SE; - o.X0 = X0; - o.cable = true; -} export function update(dt, input, params) { if (!params.s) setup(params); - const S = params.s; - const k = input.knobValues || [0, 2, 0, 0.06]; + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06]; dt = clamp(dt || 0, 0, 0.1); S.hour = clamp(k[0], 0, 1); - const tempo = clamp(k[1], 0.2, 3) / 2; - const kg = clamp(k[2], 1, 4); - S.n = round(kg); - const wind = clamp(k[3], -1, 1); + const tempo = clamp(k[1], 0.2, 3) / 2, kg = clamp(k[2], 1, 4), n = (S.n = round(kg)), wind = clamp(k[3], -1, 1); if (!S.started) { S.started = true; S.wind = wind; S.ks = 1 + 0.14 * (kg - 1); - S.lean = S.wind * 0.22; - const n = S.n; - for (let i = 0; i < n; i++) { - const o = addCraft(S, 0, "climb", 0, n === 1 ? 72 : 82 - (i * 36) / (n - 1), 1); - o.th = rnd() * TAU; - o.bank = 0.5; - } - for (const y of [86, 62]) { - const o = addCraft(S, 1, "climb", 0, y, 1); - o.th = rnd() * TAU; - o.bank = 0.62; - } - for (let i = 0; i < 26 + 10 * 3; i++) S.down.push({ y: YB + rnd() * (YG - YB), a: rnd() * TAU, r: 0.5 + rnd() * 5.5, v: 3 + rnd() * 3, tw: rnd() * TAU }); - const c = newCloud(colX(S, YB), 1.5); - const old = newCloud(colX(S, YB) + (wind < 0 ? -1 : 1) * 24, 26); - old.det = true; - old.al = 0.9; - S.clouds.push(old, c); + S.lean = wind * 0.22; + for (let i = 0; i < n; i++) addCraft(S, 0, "climb", 0, n === 1 ? 72 : 82 - (i * 36) / (n - 1), 1).th = rnd() * TAU; + for (const y of [86, 62]) addCraft(S, 1, "climb", 0, y, 1).th = rnd() * TAU; + for (let i = 0; i < 56; i++) S.down.push({ y: YB + rnd() * (YG - YB), a: rnd() * TAU, r: 0.5 + rnd() * 5.5, v: 3 + rnd() * 3, tw: rnd() * TAU }); + // The cloud of the last thermal is drifting off as the next one starts. + S.clouds.push(Object.assign(newCloud(colX(S, YB) + (wind < 0 ? -24 : 24), 26), { det: true, al: 0.9 }), newCloud(colX(S, YB), 1.5)); } const d = dt * tempo; S.t += d; S.wind += (wind - S.wind) * min(1, dt * 2); S.ks += (1 + 0.14 * (kg - 1) - S.ks) * min(1, dt * 2); S.lean = S.wind * 0.22; - const vd = (S.wind < 0 ? -1 : 1) * 2 + 6 * S.wind; - if (input.btnPressed && input.btnPressed[0]) launch(S); + const vd = (S.wind < 0 ? -2 : 2) + 6 * S.wind; + if (input.btnPressed && input.btnPressed[0]) { + const dir = rnd() < 0.5 ? 1 : -1, o = addCraft(S, 0, "launch", dir > 0 ? 4 + rnd() * 10 : 50 + rnd() * 10, 0, dir); + Object.assign(o, { yc: 121 + o.Z * SE, X0: o.X, cable: true }); + } - // The cloud over the thermal builds; after 26 s it drifts off and thins, and a new one starts. + // The thermal's cloud builds for 26 s, then drifts off and thins as a new one starts. for (const c of S.clouds) { c.a += d; c.g = 1 - exp(-c.a / 6.5); - if (!c.det) { + if (c.det) { + c.x += vd * d; + c.al -= d / 8; + } else { c.x += (colX(S, YB) - c.x) * min(1, d * 0.6); if (c.a > 26) { c.det = true; S.clouds.push(newCloud(colX(S, YB), 0)); } - } else { - c.x += vd * d; - c.al -= d / 8; } } S.clouds = S.clouds.filter((c) => c.al > 0); - for (const f of S.far) { - f.x += vd * 0.35 * d; - if (f.x > W + f.hw * 1.5) f.x = -f.hw * 1.5; - if (f.x < -f.hw * 1.5) f.x = W + f.hw * 1.5; - } - for (const s of S.shad) { - s.x += vd * 0.9 * d; - if (s.x > W + s.r + 4) s.x = -s.r - 4 - rnd() * 16; - if (s.x < -s.r - 4) s.x = W + s.r + 4 + rnd() * 16; + // Far cumulus and the shadows of clouds out of sight drift with the wind and come round again. + for (const f of S.far.concat(S.shad)) { + f.x += vd * f.m * d; + if ((f.x - 32) * vd > 0 && abs(f.x - 32) > f.e) f.x = 32 - sign(f.x - 32) * (f.e - 1); } - // Gliders and buzzards. - const top = [YB + 8, YB + 18]; + // Gliders and buzzards: launched, gliding in, circling up, or gliding off. for (const o of S.gl) { const cx = colX(S, o.yc) + (o.kind ? 2 : 0); if (o.st === "launch") { @@ -411,7 +277,6 @@ export function update(dt, input, params) { } if (!o.cable && o.pitch <= 0) { o.st = "climb"; - o.th = o.dir > 0 ? -PI / 2 : PI / 2; o.off = o.X - cx; } } else if (o.st === "climb") { @@ -420,44 +285,42 @@ export function update(dt, input, params) { o.off *= exp(-d * 0.35); o.yc -= o.c0 * (1 + 0.45 * cos(o.th + 0.8)) * d; o.bank += ((o.kind ? 0.62 : 0.5) - o.bank) * min(1, d * 2); - if (o.yc < top[o.kind]) o.leave = true; - // A glider that is done straightens up the next time it flies across the panel. + if (o.yc < YB + (o.kind ? 18 : 8)) o.leave = true; + // Done, it straightens up the next time it flies across the panel. if (o.leave && cos(th0) * cos(o.th) <= 0) { o.st = "out"; o.th = sin(o.th) > 0 ? PI / 2 : -PI / 2; - o.dir = -sin(o.th) > 0 ? 1 : -1; + o.dir = -sign(sin(o.th)); } o.X = cx + o.off + o.R * cos(o.th); o.Z = o.R * sin(o.th); } else { - const v = o.R * o.w; - o.X += o.dir * v * d; + o.X += o.dir * o.R * o.w * d; o.yc += (o.st === "out" ? 0.8 : 0.4) * d; o.bank *= exp(-d * 3); if (o.st === "in" && o.dir * (o.X - cx) >= 0) o.st = "climb"; - if (o.X < -24 || o.X > W + 24) o.gone = true; + if (abs(o.X - 32) > 56) o.gone = true; } } S.gl = S.gl.filter((o) => !o.gone); - // Keep the wanted number in the thermal: the highest leaves early, or a newcomer glides in low. + // Too many: the highest leaves early. Too few: a newcomer glides in low. for (const kind of [0, 1]) { - const want = kind ? 2 : S.n; const act = S.gl.filter((o) => o.kind === kind && o.st !== "out"); - const key = kind ? "spawnB" : "spawnG"; - S[key] -= d; - if (act.length < want && S[key] <= 0) { + const want = kind ? 2 : n; + S.spawn[kind] -= d; + if (act.length < want && S.spawn[kind] <= 0) { const dir = rnd() < 0.5 ? 1 : -1; - addCraft(S, kind, "in", dir > 0 ? -14 : W + 14, (kind ? 84 : 86) + rnd() * 6, dir); - S[key] = kind ? 1.5 + rnd() * 2 : 2.5; + addCraft(S, kind, "in", 32 - dir * 46, (kind ? 84 : 86) + rnd() * 6, dir); + S.spawn[kind] = kind ? 1.5 + rnd() * 2 : 2.5; } else if (act.length > want) { const hi = act.filter((o) => o.st === "climb" && !o.leave).sort((p, q) => p.yc - q.yc)[0]; if (hi) hi.leave = true; } } - if (S.cable) { - S.cable.t += d; - S.cable.y += 9 * d; - if (S.cable.t > 2) S.cable = null; + const cb = S.cable; + if (cb) { + cb.y += 9 * d; + if ((cb.t += d) > 2) S.cable = null; } // Thistledown rides the thermal up from the ploughed field. for (const p of S.down) { @@ -470,31 +333,20 @@ export function update(dt, input, params) { } } -// A tapered stroke into the coverage buffer, keeping the larger value, and growing the box drawn into. +// A tapered stroke into the coverage buffer, growing the box drawn into. function stroke(S, ax, ay, bx, by, ra, rb) { - const A = S.A; - const x0 = max(0, floor(min(ax, bx) - 2)); - const x1 = min(W - 1, Math.ceil(max(ax, bx) + 2)); - const y0 = max(0, floor(min(ay, by) - 2)); - const y1 = min(H - 1, Math.ceil(max(ay, by) + 2)); + const { A, bb } = S, x0 = max(0, floor(min(ax, bx) - 2)), x1 = min(W - 1, Math.ceil(max(ax, bx) + 2)); + const y0 = max(0, floor(min(ay, by) - 2)), y1 = min(H - 1, Math.ceil(max(ay, by) + 2)); if (x0 > x1 || y0 > y1) return; - const bb = S.bb; bb[0] = min(bb[0], x0); bb[1] = min(bb[1], y0); bb[2] = max(bb[2], x1); bb[3] = max(bb[3], y1); - const dx = bx - ax; - const dy = by - ay; - const ll = dx * dx + dy * dy || 1; + const dx = bx - ax, dy = by - ay, ll = dx * dx + dy * dy || 1; for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) { - const px = x + 0.5 - ax; - const py = y + 0.5 - ay; - const t = clamp((px * dx + py * dy) / ll, 0, 1); - const ex = px - dx * t; - const ey = py - dy * t; - const a = clamp(ra + (rb - ra) * t + 0.5 - sqrt(ex * ex + ey * ey), 0, 1); - const i = y * W + x; + const px = x + 0.5 - ax, py = y + 0.5 - ay, t = clamp((px * dx + py * dy) / ll, 0, 1), ex = px - dx * t, ey = py - dy * t; + const a = clamp(ra + (rb - ra) * t + 0.5 - sqrt(ex * ex + ey * ey), 0, 1), i = y * W + x; if (a > A[i]) A[i] = a; } } @@ -502,133 +354,92 @@ function flush(S, col, alpha) { const { A, buf, bb } = S; for (let y = bb[1]; y <= bb[3]; y++) for (let x = bb[0]; x <= bb[2]; x++) { - const i = y * W + x; - const a = min(1, A[i] * alpha); + const i = y * W + x, a = min(1, A[i] * alpha); if (a <= 0) continue; A[i] = 0; - const q = i * 3; - buf[q] += (col[0] - buf[q]) * a; - buf[q + 1] += (col[1] - buf[q + 1]) * a; - buf[q + 2] += (col[2] - buf[q + 2]) * a; + for (let c = 0; c < 3; c++) buf[i * 3 + c] += (col[c] - buf[i * 3 + c]) * a; } bb[0] = bb[1] = 1e9; bb[2] = bb[3] = -1; } -// A craft in its own frame: heading, pitch and bank, seen from below and to the side. +// A craft in its own frame (heading, pitch, bank), seen from below and to the side. function craft(S, o, h) { - const hx = o.st === "climb" ? -sin(o.th) : o.dir; - const hz = o.st === "climb" ? cos(o.th) : 0; - const cp = cos(o.pitch); - const sp = sin(o.pitch); - const F = [hx * cp, sp, hz * cp]; - const U0 = [-hx * sp, cp, -hz * sp]; - const R0 = [-hz, 0, hx]; - const cb = cos(o.bank * 0.7); - const sb = sin(o.bank * 0.7); - const U = [U0[0] * cb + R0[0] * sb, U0[1] * cb, U0[2] * cb + R0[2] * sb]; - const R = [R0[0] * cb - U0[0] * sb, -U0[1] * sb, R0[2] * cb - U0[2] * sb]; - const sx = o.X; + const cl = o.st === "climb", hx = cl ? -sin(o.th) : o.dir, hz = cl ? cos(o.th) : 0, cp = cos(o.pitch), sp = sin(o.pitch); + const F = [hx * cp, sp, hz * cp], U0 = [-hx * sp, cp, -hz * sp], R0 = [-hz, 0, hx], cb = cos(o.bank * 0.7), sb = sin(o.bank * 0.7); + const U = [U0[0] * cb + R0[0] * sb, U0[1] * cb, U0[2] * cb + R0[2] * sb], R = [R0[0] * cb - U0[0] * sb, -U0[1] * sb, R0[2] * cb - U0[2] * sb]; const sy = o.yc - o.Z * SE; - const P = (f, r, u) => { - const wx = F[0] * f + R[0] * r + U[0] * u; - const wy = F[1] * f + R[1] * r + U[1] * u; - const wz = F[2] * f + R[2] * r + U[2] * u; - return [sx + wx, sy - (wy * BC + wz * BE)]; - }; + const P = (f, r, u) => [o.X + F[0] * f + R[0] * r + U[0] * u, sy - (F[1] * f + R[1] * r + U[1] * u) * BC - (F[2] * f + R[2] * r + U[2] * u) * BE]; for (const m of o.kind ? BUZZ : GLIDER) { - const a = P(m[0], m[1], m[2]); - const b = P(m[3], m[4], m[5]); + const a = P(m[0], m[1], m[2]), b = P(m[3], m[4], m[5]); stroke(S, a[0], a[1], b[0], b[1], m[6], m[7]); } // From below we see the shaded underside; banked towards us, the sunlit top. const top = sstep(-0.3, 0.4, -U[1] * BE + U[2] * BC); - if (o.kind) { - flush(S, [mix(58, 86, top), mix(42, 62, top), mix(30, 40, top)], 1); - return; - } - const lit = mix3([236, 240, 246], [255, 226, 176], h); + if (o.kind) return flush(S, [mix(58, 86, top), mix(42, 62, top), mix(30, 40, top)], 1); const f = 0.84 + 0.16 * top; - flush(S, [lit[0] * f, lit[1] * f, lit[2] * f], 1.35); - // Once a turn, the wing tilts to the sun and flashes. - const gx = mix(-0.35, -0.8, h); - const gz = mix(1, 0.6, h); - const gl = (sstep(0.92, 1, (R0[0] * gx + R0[2] * gz) / sqrt(gx * gx + gz * gz) * Math.sign(o.bank || 1)) * abs(o.bank)) / 0.5; - if (gl > 0.02) { - const w = P(0.4, 3.5 * Math.sign(o.bank || 1), 0.6); - for (let y = max(0, floor(w[1] - 5)); y <= min(H - 1, w[1] + 5); y++) - for (let x = max(0, floor(w[0] - 5)); x <= min(W - 1, w[0] + 5); x++) { - const ex = abs(x + 0.5 - w[0]); - const ey = abs(y + 0.5 - w[1]); - const v = gl * (exp(-sqrt(ex * ex + ey * ey) / 1.3) + 0.5 * exp(-min(ex, ey) * 2.5 - max(ex, ey) / 2.5)); - const q = (y * W + x) * 3; - S.buf[q] += 255 * v; - S.buf[q + 1] += 246 * v; - S.buf[q + 2] += mix(225, 170, h) * v; - } - } + flush(S, mix3([236, 240, 246], [255, 226, 176], h).map((v) => v * f), 1.35); + // Once a turn the wing tilts to the sun and flashes. + const gx = mix(-0.35, -0.8, h), gz = mix(1, 0.6, h); + const gl = (sstep(0.92, 1, ((R0[0] * gx + R0[2] * gz) / sqrt(gx * gx + gz * gz)) * sign(o.bank || 1)) * abs(o.bank)) / 0.5; + if (gl < 0.02) return; + const w = P(0.4, 3.5 * sign(o.bank || 1), 0.6); + for (let y = max(0, floor(w[1] - 5)); y <= min(H - 1, w[1] + 5); y++) + for (let x = max(0, floor(w[0] - 5)); x <= min(W - 1, w[0] + 5); x++) { + const ex = abs(x + 0.5 - w[0]), ey = abs(y + 0.5 - w[1]); + const v = gl * (exp(-sqrt(ex * ex + ey * ey) / 1.3) + 0.5 * exp(-min(ex, ey) * 2.5 - max(ex, ey) / 2.5)), q = (y * W + x) * 3; + S.buf[q] += 255 * v; + S.buf[q + 1] += 246 * v; + S.buf[q + 2] += mix(225, 170, h) * v; + } } function drawCloud(S, c, hw, ht, g, yb, al, haze) { - const h = S.hour; - const t = S.t; - const buf = S.buf; - const B = []; - const lean = S.lean * 0.6; - let x0 = 1e9; - let x1 = -1e9; - let y0 = 1e9; + const { buf, t } = S, h = S.hour, B = [], lean = S.lean * 0.6; + let x0 = 1e9, x1 = -1e9, y0 = 1e9; for (const p of c.p) { - const gr = sstep(p.t0, p.t0 + 0.25, g) * (c.det ? sqrt(c.al) : 1); + const gr = sstep(p.t0, p.t0 + 0.25, g); if (gr <= 0.02) continue; - const r = p.r * hw * gr * (1 + 0.05 * sin(t * 0.7 + p.ph)); - const bx = c.x + p.u * hw + lean * p.v * ht + 0.04 * hw * sin(t * 0.5 + p.ph); + const r = p.r * hw * gr * (1 + 0.05 * sin(t * 0.7 + p.ph)), bx = c.x + p.u * hw + lean * p.v * ht + 0.04 * hw * sin(t * 0.5 + p.ph); const by = yb - p.v * ht - 0.03 * ht * sin(t * 0.4 + p.ph * 2); B.push(bx, by, r); x0 = min(x0, bx - r); x1 = max(x1, bx + r); y0 = min(y0, by - r); } - if (!B.length) return; - const L = [mix(-0.45, -0.85, h), mix(-0.7, -0.28, h), 0.5]; - const ll = sqrt(L[0] * L[0] + L[1] * L[1] + L[2] * L[2]); - const dark = 1 - 0.2 * g * (c.det ? 0.5 : 1); + const L = [mix(-0.45, -0.85, h), mix(-0.7, -0.28, h), 0.5], ll = sqrt(L[0] * L[0] + L[1] * L[1] + L[2] * L[2]); const litC = mix3([252, 252, 250], [255, 214, 150], h); - const shC = mix3([150, 162, 192], [150, 124, 146], h).map((v) => v * dark); + const shC = mix3([150, 162, 192], [150, 124, 146], h).map((v) => v * (1 - 0.2 * g * (c.det ? 0.5 : 1))); const baseC = mix3([126, 132, 152], [108, 90, 100], h).map((v) => v * mix(1, 0.78, g)); const hz = hazeOf(h); for (let y = max(0, floor(y0 - 2)); y <= min(H - 1, yb + 1); y++) for (let x = max(0, floor(x0 - 2)); x <= min(W - 1, x1 + 2); x++) { - const px = x + 0.5; - const py = y + 0.5; - let best = -9; - let bi = 0; + const px = x + 0.5, py = y + 0.5; + // The normal blends the puffs nearby, weighted by how deep inside each the pixel is. + let best = -9, r = 1, sw = 0, nx = 0, ny = 0; for (let i = 0; i < B.length; i += 3) { - const dx = px - B[i]; - const dy = py - B[i + 1]; - const v = 1 - sqrt(dx * dx + dy * dy) / B[i + 2]; + const dx = (px - B[i]) / B[i + 2], dy = (py - B[i + 1]) / B[i + 2], v = 1 - sqrt(dx * dx + dy * dy); + if (v < -0.5) continue; if (v > best) { best = v; - bi = i; + r = B[i + 2]; } + const w = exp(7 * v); + sw += w; + nx += dx * w; + ny += dy * w; } if (best < -0.4) continue; - const r = B[bi + 2]; - const dd = best + 0.32 * (noise(px * 0.32, py * 0.32 - t * 0.12, c.sd) - 0.5); - let a = clamp(dd * r * 0.7, 0, 1) * clamp(yb + 0.7 - py + 0.6 * noise(px * 0.4, 1, c.sd), 0, 1) * al; + // Cauliflower edges; a cloud that is dying frays from the edges in. + const n = noise(px * 0.32, py * 0.32 - t * 0.12, c.sd); + const a = clamp((best + 0.32 * (n - 0.5) - (1 - al) * 0.8) * r * 0.7, 0, 1) * clamp(yb + 0.7 - py + 0.6 * noise(px * 0.4, 1, c.sd), 0, 1) * sqrt(al); if (a <= 0) continue; - const nx = (px - B[bi]) / r; - const ny = (py - B[bi + 1]) / r; + nx /= sw; + ny /= sw; const nz = sqrt(max(0, 1 - nx * nx - ny * ny)); - let lw = clamp(((nx * L[0] + ny * L[1] + nz * L[2]) / ll + 0.35) / 1.35, 0, 1); - lw *= 0.55 + 0.45 * clamp((yb - py) / (0.45 * ht), 0, 1); - const bs = sstep(yb - 2.6, yb - 0.6, py); - const q = (y * W + x) * 3; - for (let k = 0; k < 3; k++) { - let v = mix(mix(shC[k], litC[k], lw), baseC[k], bs); - v = mix(v, hz[k], haze); - buf[q + k] += (v - buf[q + k]) * a; - } + const lw = clamp(((nx * L[0] + ny * L[1] + nz * L[2]) / ll + 0.45) / 1.45 + 0.3 * (n - 0.5), 0, 1) * (0.55 + 0.45 * clamp((yb - py) / (0.45 * ht), 0, 1)); + const bs = sstep(yb - 2.6, yb - 0.6, py), q = (y * W + x) * 3; + for (let k = 0; k < 3; k++) buf[q + k] += (mix(mix(mix(shC[k], litC[k], lw), baseC[k], bs), hz[k], haze) - buf[q + k]) * a; } } @@ -636,75 +447,55 @@ export function draw(display, params) { const S = params.s; if (!S || !S.started) return; if (S.painted === undefined || abs(S.painted - S.hour) > 0.003) paint(S); - const h = S.hour; - const { buf, bg } = S; - buf.set(bg); - S.bb = [1e9, 1e9, -1, -1]; + const h = S.hour, buf = S.buf; + buf.set(S.bg); - // Cloud shadows on the fields: the thermal's cloud over its field, pushed off by the sun, and two from clouds out of sight. + // Cloud shadows: each thermal cloud's, pushed off its field by the sun, and two from clouds out of sight. const sh = []; for (const c of S.clouds) { - const hw = (8 + 18 * c.g) * S.ks; - const ru = ((hw * DSRC) / 64) * 0.95 * (c.det ? sqrt(c.al) : 1); - sh.push(((c.x - 32 + 3 + 30 * h) * DSRC) / 64, DSRC * (1.04 + 1.1 * h), ru, ru * 1.3 + 0.5, c.al * (0.55 + 0.45 * c.g)); + const ru = (((8 + 18 * c.g) * S.ks * DSRC) / 64) * 0.95 * (c.det ? sqrt(c.al) : 1); + sh.push(((c.x - 29 + 30 * h) * DSRC) / 64, DSRC * (1.04 + 1.1 * h), ru, ru * 1.3 + 0.5, c.al * (0.55 + 0.45 * c.g)); } for (const s of S.shad) sh.push(((s.x - 32) * s.D) / 64, s.D, (s.r * s.D) / 64, s.rd, 1); const sc = [0.46, 0.5, 0.62]; for (let p = FY * W; p < N; p++) { - const u = S.fu[p]; - const D = S.fd[p]; let m = 0; for (let i = 0; i < sh.length; i += 5) { - const du = (u - sh[i]) / sh[i + 2]; - const dv = (D - sh[i + 1]) / sh[i + 3]; - const q = du * du + dv * dv; - if (q > 2.2) continue; - m = max(m, sstep(1.05, 0.6, sqrt(q) + 0.35 * (S.fn[p] - 0.5)) * sh[i + 4]); - } - if (m > 0) { - const q = p * 3; - buf[q] *= mix(1, sc[0], m); - buf[q + 1] *= mix(1, sc[1], m); - buf[q + 2] *= mix(1, sc[2], m); + const du = (S.fu[p] - sh[i]) / sh[i + 2], dv = (S.fd[p] - sh[i + 1]) / sh[i + 3], q = du * du + dv * dv; + if (q < 2.2) m = max(m, sstep(1.05, 0.6, sqrt(q) + 0.35 * (S.fn[p] - 0.5)) * sh[i + 4]); } + if (m > 0) for (let c = 0; c < 3; c++) buf[p * 3 + c] *= mix(1, sc[c], m); } // Far cumulus along the horizon, then the clouds the thermal builds. for (const f of S.far) drawCloud(S, f, f.hw, f.ht, 1, f.yb, 0.9, 0.5); - for (const c of S.clouds) drawCloud(S, c, (8 + 18 * c.g) * S.ks, (6 + 26 * c.g) * S.ks, c.g, YB, c.det ? min(1, c.al * 1.4) : 1, 0); + for (const c of S.clouds) drawCloud(S, c, (8 + 18 * c.g) * S.ks, (6 + 26 * c.g) * S.ks, c.g, YB, c.det ? c.al : 1, 0); // Thistledown catching the sun as it rises. - const nd = 26 + round(10 * (S.ks - 1) / 0.14); - const dc = mix3([240, 240, 228], [255, 222, 160], h); - for (let i = 0; i < nd && i < S.down.length; i++) { - const p = S.down[i]; - const x = round(colX(S, p.y) + p.r * cos(p.a) + 0.8 * sin(S.t * 1.7 + p.tw) - 0.5); - const y = round(p.y + p.r * sin(p.a) * SE - 0.5); - if (x < 0 || x >= W || y < 0 || y >= H) continue; + const dc = mix3([240, 240, 228], [255, 222, 160], h), nd = 26 + round((10 * (S.ks - 1)) / 0.14); + for (let i = 0; i < nd; i++) { + const p = S.down[i], x = round(colX(S, p.y) + p.r * cos(p.a) + 0.8 * sin(S.t * 1.7 + p.tw) - 0.5), y = round(p.y + p.r * sin(p.a) * SE - 0.5); + if (x < 0 || x >= W) continue; const a = (0.45 + 0.4 * sin(S.t * 3 + p.tw * 5)) * sstep(YB, YB + 8, p.y) * sstep(YG + 2, YG - 6, p.y); - const q = (y * W + x) * 3; - for (let k = 0; k < 3; k++) buf[q + k] += (dc[k] - buf[q + k]) * a; + for (let k = 0, q = (y * W + x) * 3; k < 3; k++) buf[q + k] += (dc[k] - buf[q + k]) * a; } - // The winch cable, and the cable that falls when the glider lets go. + // The winch cable, and the one that falls on its red parachute after release. + const dark = [36, 34, 38]; for (const o of S.gl) - if (o.st === "launch" && o.cable) { + if (o.cable) { stroke(S, o.X + o.dir * 3 * cos(o.pitch), o.yc - o.Z * SE - 3 * sin(o.pitch), o.X0 + o.dir * 58, 118, 0.18, 0.18); - flush(S, [36, 34, 38], 0.75); + flush(S, dark, 0.75); } - if (S.cable) { - const c = S.cable; - stroke(S, c.x, c.y, c.xw, 118, 0.18, 0.18); - flush(S, [36, 34, 38], 0.75 * (1 - c.t / 2)); - stroke(S, c.x, c.y - 1, c.x, c.y - 1, 0.7, 0.7); - flush(S, [230, 50, 40], 1 - c.t / 2); + const cb = S.cable; + if (cb) { + stroke(S, cb.x, cb.y, cb.xw, 118, 0.18, 0.18); + flush(S, dark, 0.75 * (1 - cb.t / 2)); + stroke(S, cb.x, cb.y - 1, cb.x, cb.y - 1, 0.7, 0.7); + flush(S, [230, 50, 40], 1 - cb.t / 2); } - // Gliders and buzzards, far side of their circles first. - const order = S.gl.slice().sort((p, q) => p.Z - q.Z); - for (const o of order) craft(S, o, h); + // Gliders and buzzards, the far side of their circles first. + for (const o of S.gl.slice().sort((p, q) => p.Z - q.Z)) craft(S, o, h); - for (let p = 0; p < N; p++) { - const q = p * 3; - display.setPixel(p % W, (p / W) | 0, min(255, buf[q]) | 0, min(255, buf[q + 1]) | 0, min(255, buf[q + 2]) | 0); - } + for (let p = 0; p < N; p++) display.setPixel(p % W, (p / W) | 0, min(255, buf[p * 3]) | 0, min(255, buf[p * 3 + 1]) | 0, min(255, buf[p * 3 + 2]) | 0); } From f8d990ed05a9033e6c071fb3a3c4b70a5f02e2ac Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:29:37 +0000 Subject: [PATCH 13/33] tower-crane: pour in the first seconds, lit windows, under 25 KB --- .../pattern-study/patterns/tower-crane.js | 356 ++++++------------ 1 file changed, 120 insertions(+), 236 deletions(-) diff --git a/incoming/pattern-study/patterns/tower-crane.js b/incoming/pattern-study/patterns/tower-crane.js index 4df794154..60395450f 100644 --- a/incoming/pattern-study/patterns/tower-crane.js +++ b/incoming/pattern-study/patterns/tower-crane.js @@ -1,21 +1,21 @@ // ===== Patternflow pattern ===== // Title: Tower Crane // Family: construction -// Idea: A yellow tower crane beside a concrete frame going up at dusk hoists a precast wall, rebar, formwork and the concrete bucket from the yard to the top deck, slewing its jib round in perspective, and each bucket pours the deck so the building grows a floor. +// Idea: A yellow tower crane hoists a precast wall, rebar, formwork and the concrete bucket from the yard to the top deck of a concrete frame going up at dusk, slewing its jib round in perspective, and each pour adds a floor. // Author: patternflow-lab // Date: 2026-10-24 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: original, pattern round 24, after the tower cranes that stand beside a high-rise frame and lift each floor's materials up to the deck +// Lineage: original, pattern round 24, after the tower cranes that stand beside a rising concrete frame // Round: 24 // =============================== // @matrix 64x128 -// @knobs Dusk=0..1, Speed=0.2..3, Floors=8..13, Wind=-1..1 +// @knobs Dusk=0..1, Speed=0.2..3, Floors=9..13, Wind=-1..1 // -// Knob 1: Dusk. ... -// Knob 2: Speed. ... -// Knob 3: Floors. ... -// Knob 4: Wind. ... -// Buttons: K1 tap pours the deck now, and the building grows a floor. +// Knob 1: Dusk. From late afternoon at 0, the sun low over the yard and the frame lit gold, to night at 1, when the sky goes navy, the floodlight and the trolley's lamp come on and most windows are lit. +// Knob 2: Speed. At 2 one lift takes about ten seconds: down to the yard, rig, hoist the whole height, slew over and set down. At 0.2 it all runs ten times slower. +// Knob 3: Floors. How many floors are up when it starts, nine to thirteen. Every fourth lift is the concrete bucket, and its pour adds a floor until the frame has thirteen and tops out with a fir tree on the roof. +// Knob 4: Wind. Clouds thicken and drift with it either way, their shadows cross the frame, and the load leans and swings on its cable. +// Buttons: K1 tap sets the floor's formwork in place and pours the deck at once, and the building grows a floor. const [W, H] = [64, 128]; const N = W * H; @@ -38,19 +38,15 @@ function vn(x, y) { // Street level, one storey, and the most floors that fit under the jib. const [GY, FH, NMAX] = [114, 6, 13]; -// The frame runs from column 3 to 39: six columns two LEDs wide round five -// bays, and the bay by the crane is the lift core. +// The frame, x 3 to 39: six columns two LEDs wide, five bays, the one by the crane the lift core. const [BX0, BX1, CORE] = [3, 39, 4]; -// The crane: mast centre, the jib's bottom chord at the root, jib and -// counter-jib lengths, the top of the tower head. +// Mast centre, the jib's bottom chord at the root, jib and counter-jib lengths, top of the tower head. const [MX, JY, JL, CJL, APEX] = [46, 15, 42, 15, 3]; // The hook's highest row, the radius it picks at over the yard, the yard's load line. const [HTOP, PICK_R, PICK_Y] = [21, 11, 120]; -// A precast wall panel, rebar, formwork, the concrete bucket: width, height, -// and the column of the deck each is set down on. +// Wall panel, rebar, formwork, bucket: width, height, the column it is set down on. const LW = [8, 11, 9, 5], LH = [5, 2, 4, 6], DROP = [35, 20, 12, 6]; -// Sky top, middle, horizon; the sun; lit and shaded cloud; the light on things. -// Four steps from late afternoon to night. +// Sky top, middle, horizon; sun; lit and shaded cloud; light on things. Afternoon to night. const KEYS = [ [34, 72, 156, 128, 142, 186, 255, 190, 112, 255, 236, 170, 255, 236, 204, 150, 130, 160, 1, 0.9, 0.74], [30, 50, 130, 168, 108, 140, 255, 140, 62, 255, 168, 78, 255, 170, 116, 104, 70, 108, 0.86, 0.64, 0.48], @@ -67,10 +63,8 @@ export function setup(params) { const F = (n) => new Float32Array(n); const P = (params.s = { f: F(N * 3), bg: F(N * 3), lm: F(N * 3), fl: F(N), row: F(H * 3), cloud: F(128 * 64), sky: new Int16Array(W), - t: 0, rt: 0, n: 0, nk: -1, stage: 3, cage: 1, form: 1, item: -1, ix: 0, pourX: -1, top: 0, pourBtn: 0, lightNew: -1, - th: PI, r: MX - DROP[3], hy: 40, load: 3, ph: "I", pt: 0, pd: 1, f0: [0, 0, 0], f1: [0, 0, 0], - sw: 0, swv: 0, vx: 0, ptx: MX - DROP[3], on: new Uint8Array(NMAX), onT: F(NMAX).fill(9), nsw: 2.4, cx: 0, - sp: [], weld: 0.4, welding: 0, wk: [], mix: 2, cars: [], ncar: 1.4, bgKey: -1, + t: 0, rt: 0, nk: -1, stage: 3, cage: 1, form: 1, item: -1, lightNew: -1, th: PI, r: 40, load: 3, ph: "I", sw: 0, swv: 0, vx: 0, ptx: 6, + on: new Uint8Array(NMAX), onT: F(NMAX).fill(9), nsw: 2.4, cx: 0, sp: [], weld: 0.4, welding: 0, wk: [], mix: 2, cars: [], ncar: 1.4, }); // Clouds: a strip of soft noise that wraps every 128 columns. for (let i = 0; i < 128 * 64; i++) { @@ -78,22 +72,13 @@ export function setup(params) { P.cloud[i] = mix(f(x), f(x + 128), x / 128); } // The city behind: blocks of four to nine columns. - for (let x = 0; x < W; ) { - const w = 4 + floor(rnd() * 6), h = 98 + floor(rnd() * 12); - for (let i = 0; i < w && x < W; i++, x++) P.sky[x] = h; - } - for (let k = 0; k < NMAX; k++) P.on[k] = rnd() < 0.35 ? 1 : 0; - for (let i = 0; i < 3; i++) P.wk.push({ x: 8 + i * 11 + rnd() * 4, tx: 8 + rnd() * 28, wait: rnd() * 2 }); + for (let x = 0, w = 0, h = 0; x < W; x++, w--) w > 0 || ((w = 4 + floor(rnd() * 6)), (h = 98 + floor(rnd() * 12))), (P.sky[x] = h); + for (let k = 0; k < NMAX; k++) (P.on[k] = rnd() < 0.35 ? 1 : 0), k < 3 && P.wk.push({ x: 8 + k * 11 + rnd() * 4, tx: 8 + rnd() * 28, wait: rnd() * 2 }); } -// A move of the crane: slew angle, trolley radius and hook row, eased from -// where they are to where they go. +// A move of the crane: slew angle, trolley radius and hook row, eased to where they go. function go(P, ph, dur, to) { - P.ph = ph; - P.pt = 0; - P.pd = dur; - P.f0 = [P.th, P.r, P.hy]; - P.f1 = [to.th === undefined ? P.th : to.th, to.r === undefined ? P.r : to.r, to.hy === undefined ? P.hy : to.hy]; + Object.assign(P, { ph, pt: 0, pd: dur, f0: [P.th, P.r, P.hy], f1: [to.th ?? P.th, to.r ?? P.r, to.hy ?? P.hy] }); } const deckY = (P) => GY - P.n * FH; @@ -103,160 +88,113 @@ const bucketHy = (P) => deckY(P) - FH - 2 - LH[3] - 3; function finishFloor(P) { if (P.n < NMAX) P.n++; else P.top = 1; - P.stage = 0; - P.cage = P.form = 0; - P.item = -1; - P.pourX = -1; // The floor that has just been glazed lights up a moment later. - P.nsw = min(P.nsw, 0.7); - P.lightNew = P.n - 3; + Object.assign(P, { stage: 0, cage: 0, form: 0, item: -1, pourX: -1, nsw: min(P.nsw, 0.7), lightNew: P.n - 3 }); } -// The next move when one is done: down to the yard, rig, hoist, slew over, -// lower, set down (or pour), and up again. +// Down to the yard, rig, hoist, slew over, lower, set down (or pour), up again. function step(P) { const T = P.ph, type = P.load; - if (T === "A") go(P, "B", 0.6, {}); - else if (T === "B") (P.load = P.stage), go(P, "C", 2.8, { hy: HTOP }); + if (T === "A") go(P, "B", 0.5, {}); + else if (T === "B") (P.load = P.top && P.stage > 2 ? (P.stage = 0) : P.stage), go(P, "C", 2.8, { hy: HTOP }); else if (T === "C") go(P, "D", 2.4, { th: P.th + PI, r: MX - DROP[type] }); else if (T === "D") go(P, "E", 1.5, { hy: type === 3 ? bucketHy(P) : deckY(P) - 4 - LH[type] }); else if (T === "E") { if (type === 3) (P.pourX = BX0), go(P, "P", 3, { r: MX - BX1 + 2 }); else { // Set down: the panel goes into the core, rebar and formwork lie on the deck. - if (type) (P.item = type), (P.ix = round(tip(P)[0])); - if (type === 1) P.cage = 0; - if (type === 2) P.form = 0; - P.stage = max(P.stage, type + 1); - P.load = -1; - for (const w of P.wk) w.tx = tip(P)[0] + (rnd() - 0.5) * 10; - go(P, "F", 0.9, {}); + const x = round(tip(P)[0]); + if (type) (P.item = type), (P.ix = x), type < 2 ? (P.cage = 0) : (P.form = 0); + for (const w of P.wk) w.tx = x + (rnd() - 0.5) * 10; + (P.stage = max(P.stage, type + 1)), (P.load = -1), go(P, "F", 0.9, {}); } - } else if (T === "P" || T === "I") finishFloor(P), go(P, "F", 0.5, {}); - else if (T === "F") go(P, "G", 0.6, { hy: P.hy - 12 }); - else (P.load = -1), go(P, "A", 1.8, { th: (floor(P.th / TAU + 1e-3) + 1) * TAU, r: PICK_R, hy: PICK_Y - LH[P.stage] - 3 }); + } else if (T === "P" || T === "I") finishFloor(P), go(P, "F", 0.4, {}); + else if (T === "F") go(P, "G", 0.5, { hy: P.hy - 12 }); + else (P.load = -1), go(P, "A", 1.5, { th: (floor(P.th / TAU + 1e-3) + 1) * TAU, r: PICK_R, hy: PICK_Y - LH[P.stage] - 3 }); } 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); - const nf = round(cl(k[2], 8, 13)); + const P = params.s, k = input.knobValues || [0, 2, 0, 0.06], rd = cl(dt || 0, 0, 0.1), d = rd * (cl(k[1], 0.2, 3) / 2), nf = round(cl(k[2], 9, 13)); if (nf !== P.nk) { // A new count of floors: the hook keeps its place over the deck. const sh = (nf - P.n) * FH; P.nk = P.n = nf; P.top = 0; - if (P.ph === "I") (P.hy = bucketHy(P)), (P.pourX = BX0), go(P, "I", 3.2, { r: MX - BX1 + 2 }); + if (P.ph === "I") (P.hy = bucketHy(P)), (P.pourX = BX0), go(P, "I", 3, { r: MX - BX1 + 2 }); else if ("EPFG".includes(P.ph)) (P.hy -= sh), (P.f0[2] -= sh), (P.f1[2] -= sh); } P.w = cl(k[3], -1, 1); - const t = (P.t += d); + const t = (P.t += d), k0 = cl(k[0], 0, 1); P.rt += rd; // The evening deepens a little and comes back once every minute and a half. - const k0 = cl(k[0], 0, 1); - P.h = k0 + (1 - k0) * 0.1 * (0.5 - 0.5 * cos((TAU * t) / 90)); - if (input.btnPressed && input.btnPressed[0] && P.pourX < 0 && !P.top) (P.pourBtn = 1), (P.pourX = BX0); - - // The crane. - P.pt += d; - const u = sm(0, 1, P.pt / P.pd), lin = min(1, P.pt / P.pd), pour = P.ph === "P" || P.ph === "I"; + P.h = k0 + (1 - k0) * 0.05 * (1 - cos((TAU * t) / 90)); + // K1 tap: everything for the floor is in place at once and the deck pours. + if (input.btnPressed && input.btnPressed[0] && P.pourX < 0 && !P.top) Object.assign(P, { pourBtn: 1, pourX: BX0, stage: 3, cage: 1, form: 1 }); + const lin = min(1, (P.pt += d) / P.pd), u = sm(0, 1, lin), pour = P.ph === "P" || P.ph === "I"; P.th = mix(P.f0[0], P.f1[0], u); P.r = mix(P.f0[1], P.f1[1], pour ? lin : u); P.hy = mix(P.f0[2], P.f1[2], u); if (pour) P.pourX = max(P.pourX, tip(P)[0] + 2); - if (P.pt >= P.pd) step(P); - if (P.pourBtn) { - P.pourX += 30 * d; - if (P.pourX > BX1) (P.pourBtn = 0), finishFloor(P); - } + if (lin >= 1) step(P); + if (P.pourBtn && (P.pourX += 30 * d) > BX1) (P.pourBtn = 0), finishFloor(P); // The load swings on its cable when the trolley speeds up or slows, and in the wind. - const [tx, ty] = tip(P), L = max(4, P.hy - ty), vx = d > 0 ? (tx - P.ptx) / d : 0, ax = d > 0 ? (vx - P.vx) / d : 0; - P.ptx = tx; - P.vx = vx; + const [tx, ty] = tip(P), L = max(4, P.hy - ty); if (d > 0) { - const eq = P.w * (0.06 + 0.04 * sin(t * 1.7)) * (P.load >= 0 ? 1 : 1.6); - P.swv += (-(110 / L) * (P.sw - eq) - cl(ax, -40, 40) / L - 0.8 * P.swv) * d; + const vx = (tx - P.ptx) / d, ax = cl((vx - P.vx) / d, -30, 30), eq = P.w * (0.06 + 0.04 * sin(t * 1.7)) * (P.load >= 0 ? 1 : 1.6); + P.swv += (-(110 / L) * (P.sw - eq) - (0.5 * ax) / L - 1.4 * P.swv) * d; P.sw = cl(P.sw + P.swv * d, -0.3, 0.3); + [P.ptx, P.vx] = [tx, vx]; } - P.swx = cl(L * sin(P.sw), -3.5, 3.5); - + // The rigger steadies the load before it goes up. + if (P.ph === "B") (P.sw *= 1 - min(1, 5 * d)), (P.swv *= 1 - min(1, 5 * d)); + P.swx = cl(L * sin(P.sw), -2.5, 2.5); // The rebar is tied up the columns and the formwork goes round them. - if (P.stage >= 2) P.cage = min(1, P.cage + d * 0.3); - if (P.stage >= 3) P.form = min(1, P.form + d * 0.3); - + if (P.stage > 1) P.cage = min(1, P.cage + d * 0.3); + if (P.stage > 2) P.form = min(1, P.form + d * 0.3); // A floor switches its lights on or off together. for (let i = 0; i < NMAX; i++) P.onT[i] += rd; if ((P.nsw -= d) <= 0) { - const g = max(1, P.n - 2), want = P.lightNew; - let f = want >= 0 && want < g ? want : floor(rnd() * g); - if (want < 0) { - let lit = 0; - for (let i = 0; i < g; i++) lit += P.on[i]; - for (let n = 0; n < 6 && P.on[f] === (lit > g * 0.5 ? 0 : 1); n++) f = floor(rnd() * g); - } - P.on[f] = want >= 0 ? 1 : 1 - P.on[f]; - P.onT[f] = 0; - P.lightNew = -1; - P.nsw = 1.6 + rnd() * 2; + const f = P.lightNew >= 0 ? P.lightNew : floor(rnd() * max(1, P.n - 2)); + P.on[f] = P.lightNew >= 0 ? 1 : 1 - P.on[f]; + Object.assign(P, { lightNew: -1, nsw: 1.6 + rnd() * 2 }).onT[f] = 0; } - // Welding on the deck, and its sparks falling down the face. if ((P.weld -= d) <= 0) (P.welding = 1 - P.welding), (P.weld = P.welding ? 1.4 + rnd() * 1.6 : 0.4 + rnd() * 0.8); const wy = deckY(P) - 2; - if (P.welding) for (let n = 0; n < 4; n++) if (rnd() < d * 18) P.sp.push([18.5 + rnd() - 0.5, wy, (rnd() - 0.5) * 14, -4 - rnd() * 7, 1 + rnd() * 0.9, 18.5, wy]); + if (P.welding) for (let n = 0; n < 4; n++) if (rnd() < d * 18) P.sp.push([18 + rnd(), wy, (rnd() - 0.5) * 14, -4 - rnd() * 7, 1 + rnd() * 0.9, 18.5, wy]); P.sp = P.sp.filter((s) => ((s[5] = s[0]), (s[6] = s[1]), (s[3] += 46 * d), (s[0] += s[2] * d), (s[1] += s[3] * d), (s[4] -= d) > 0 && s[1] < GY)); - // Workers walk about the deck. - for (const w of P.wk) { - if (abs(w.tx - w.x) > 0.3) w.x += Math.sign(w.tx - w.x) * min(abs(w.tx - w.x), 4 * d); + for (const w of P.wk) + if (abs(w.tx - w.x) > 0.3) w.x = cl(w.x + cl(w.tx - w.x, -4 * d, 4 * d), 5, 37); else if ((w.wait -= d) <= 0) (w.tx = 6 + rnd() * 30), (w.wait = 1 + rnd() * 3); - w.x = cl(w.x, 5, 37); - } - // The mixer comes in for the bucket and backs out after the pour. P.mix += cl((P.stage >= 2 && !P.top ? 2 : -26) - P.mix, -9 * d, 9 * d); - // Cars along the street. - if ((P.ncar -= d) <= 0) { - const dir = rnd() < 0.5 ? 1 : -1; - P.cars.push([dir > 0 ? -8 : W + 1, dir * (11 + rnd() * 6), floor(rnd() * 4)]); - P.ncar = 2 + rnd() * 4; - } + if ((P.ncar -= d) <= 0) (P.ncar = 2 + rnd() * 4), P.cars.push(rnd() < 0.5 ? [-8, 11 + rnd() * 6, floor(rnd() * 4)] : [W + 1, -11 - rnd() * 6, floor(rnd() * 4)]); P.cars = P.cars.filter((c) => ((c[0] += c[1] * d), c[0] > -12 && c[0] < W + 12)); - // Clouds drift with the wind. P.cx += (1.5 + 9 * P.w) * d; } -// --- drawing ----------------------------------------------------------- - let F, LM; -function put(x, y, c, 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 i = 0; i < 3; i++) F[o + i] = mix(F[o + i], c[i], a); +const at = (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 = at(x, y)) { + if (o >= 0) for (let i = 0; i < 3; i++) F[o + i] = mix(F[o + i], c[i], a); } -function add(x, y, c, k) { - x = round(x); - y = round(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] * k; +function add(x, y, c, k, o = at(x, y)) { + if (o >= 0) for (let i = 0; i < 3; i++) F[o + i] += c[i] * k; } // A surface: its own colour under the light that falls on that LED. -function sur(x, y, r, g, b, k = 1) { - x = round(x); - y = round(y); - if (x < 0 || x >= W || y < 0 || y >= H) return; - const o = (y * W + x) * 3; - F[o] = r * LM[o] * k; - F[o + 1] = g * LM[o + 1] * k; - F[o + 2] = b * LM[o + 2] * k; +function sur(x, y, r, g, b, k = 1, o = at(x, y)) { + if (o >= 0) (F[o] = r * LM[o] * k), (F[o + 1] = g * LM[o + 1] * k), (F[o + 2] = b * LM[o + 2] * k); } const surc = (x, y, c, k = 1) => sur(x, y, c[0], c[1], c[2], k); +// A lamp's glow round it, and a worker three LEDs tall. +const glow = (x, y, c, k) => { + for (let j = -2; j <= 2; j++) for (let i = -2; i <= 2; i++) if (i || j) add(x + i, y + j, c, k / (i * i + j * j)); +}; +const man = (x, y, head = [250, 250, 240]) => (sur(x, y, 50, 56, 90), sur(x, y - 1, 255, 130, 20), surc(x, y - 2, head)); function line(x0, y0, x1, y1, c, k = 1) { const n = max(abs(x1 - x0), abs(y1 - y0)) * 1.2 + 1; for (let i = 0; i <= n; i++) surc(mix(x0, x1, i / n), mix(y0, y1, i / n), c, k); @@ -264,11 +202,10 @@ function line(x0, y0, x1, y1, c, k = 1) { // Sky, sun and the city beyond, kept until the evening moves on. function sky(P, K) { - const h = P.h, key = round(h * 300); - if (key === P.bgKey) return F.set(P.bg); - P.bgKey = key; - const B = P.bg, row = P.row, night = sm(0.4, 0.9, h), vis = 1 - sm(0.78, 0.95, h); - const sx = 55, sy = 62 + 52 * h; + const h = P.h, B = P.bg; + if (round(h * 300) === P.bgKey) return F.set(B); + P.bgKey = round(h * 300); + const row = P.row, night = sm(0.4, 0.9, h), vis = 1 - sm(0.78, 0.95, h); for (let y = 0; y < H; y++) { const f = min(1, y / GY), a = f < 0.5 ? 0 : 3, u = f < 0.5 ? f / 0.5 : (f - 0.5) / 0.5; for (let c = 0; c < 3; c++) row[y * 3 + c] = mix(K[a + c], K[a + 3 + c], a ? u * u : sm(0, 1, u)); @@ -280,7 +217,7 @@ function sky(P, K) { for (let c = 0; c < 3; c++) B[o + c] = mix(K[3 + c], K[6 + c], 0.55) * 0.6 * (x & 1 ? 1 : 0.86) + win * [210, 160, 90][c]; continue; } - const d = Math.hypot(x + 0.5 - sx, y + 0.5 - sy), g = vis * (0.55 * exp(-((d - 3.6) ** 2) / 50) + 0.3 * exp(-d / 16)); + const d = Math.hypot(x - 54.5, y - 61.5 - 52 * h), g = vis * (0.55 * exp(-((d - 3.6) ** 2) / 50) + 0.3 * exp(-d / 16)); for (let c = 0; c < 3; c++) B[o + c] = d < 3.6 && vis > 0 ? mix(row[y * 3 + c], K[9 + c], vis) : row[y * 3 + c] + K[9 + c] * g * 0.7; } F.set(B); @@ -292,47 +229,39 @@ function clouds(P, K) { const ty = floor(y * 0.6) & 63, fade = 1 - sm(50, 100, y); for (let x = 0; x < W; x++) { if (y >= P.sky[x]) continue; - const v = C[(ty << 7) | (floor(x + P.cx) & 127)], a = cl((v - cover) * 7, 0, 1) * fade; - if (a <= 0.01) continue; - const lt = cl(1.25 - (v - cover) * 4 + (y - 40) / 120, 0, 1); - put(x, y, [mix(K[15], K[12], lt), mix(K[16], K[13], lt), mix(K[17], K[14], lt)], a * 0.92); + const v = C[(ty << 7) | (floor(x + P.cx) & 127)], a = cl((v - cover) * 7, 0, 1) * fade, lt = cl(1.25 - (v - cover) * 4 + (y - 40) / 120, 0, 1); + if (a > 0.01) put(x, y, [mix(K[15], K[12], lt), mix(K[16], K[13], lt), mix(K[17], K[14], lt)], a * 0.92); } } } -// The light on every surface: what is left of the day, cloud shadows passing -// over, and the floodlights once they come on. +// The light on every surface: the day, cloud shadows, the floodlights after dusk. function light(P, K) { const sunA = 1 - sm(0.45, 0.85, P.h), fon = sm(0.3, 0.55, P.h), FLb = P.fl, C = P.cloud; const cover = 0.5 - 0.18 * abs(P.w), [tx, ty] = tip(P), hx = tx + P.swx * 0.5; for (let i = 0; i < N; i++) { - const x = i & 63, y = i >> 6, o = i * 3; - const v = C[((floor(y * 0.25 + 20) & 63) << 7) | (floor(x * 0.8 + P.cx * 2.2 + 40) & 127)]; - const sh = 1 - 0.45 * sunA * cl((v - cover) * 5, 0, 1); + const x = i & 63, y = i >> 6, o = i * 3, v = C[((floor(y * 0.25 + 20) & 63) << 7) | (floor(x * 0.8 + P.cx * 2.2 + 40) & 127)]; let fl = 0; if (fon > 0) { const dx = x + 0.5 - LX, dy = y + 0.5 - LY, dd = Math.hypot(dx, dy) + 0.01, ca = (dx * AX + dy * AY) / dd; - fl = (exp((ca - 1) / 0.03) * 1.5) / (1 + (dd / 60) ** 2); + fl = (exp((ca - 1) / 0.03) * 1.2) / (1 + (dd / 60) ** 2); if (y > ty + 1) { const q = (x + 0.5 - hx) / (1.2 + 0.16 * (y - ty)); - fl += (exp(-q * q) * 0.9) / (1 + ((y - ty) / 90) ** 2); + fl += (exp(-q * q) * 0.7) / (1 + ((y - ty) / 90) ** 2); } fl *= fon; } FLb[i] = fl; - for (let c = 0; c < 3; c++) LM[o + c] = K[18 + c] * sh + fl * FLC[c]; + for (let c = 0; c < 3; c++) LM[o + c] = K[18 + c] * (1 - 0.45 * sunA * cl((v - cover) * 5, 0, 1)) + fl * FLC[c]; } } -function ground(P) { - for (let y = GY; y < H; y++) - for (let x = 0; x < W; x++) { - const n = 0.82 + 0.3 * hash(x, y + 900); - if (y >= 124) sur(x, y, 62 * n, 60 * n, 66 * n); - else if (y === 123) sur(x, y, 150, 146, 140); - else sur(x, y, 150 * n, 126 * n, 98 * n); - } - for (let x = 2; x < W; x += 8) sur(x, 126, 190, 180, 120), sur(x + 1, 126, 190, 180, 120); +// The yard's gravel, the kerb, the street and its markings. +function ground() { + for (let i = GY * W; i < N; i++) { + const x = i & 63, y = i >> 6, n = 0.82 + 0.3 * hash(x, y + 900); + y === 126 && (x + 6) % 8 < 2 ? sur(x, y, 190, 180, 120) : y > 123 ? sur(x, y, 62 * n, 60 * n, 66 * n) : y > 122 ? sur(x, y, 150, 146, 140) : sur(x, y, 150 * n, 126 * n, 98 * n); + } } // The concrete frame, its windows, and the floor going up on the deck. @@ -344,18 +273,15 @@ function building(P) { const cx = (x - BX0) % 7, b = floor((x - BX0) / 7), bx = cx - 2; if (ry === 0) sur(x, y, 228, 220, 206); else if (cx < 2) sur(x, y, 206, 198, 184); - else if (ry === 1) sur(x, y, 178, 172, 162); + else if (ry === 1) k < g ? sur(x, y, 78, 86, 102) : sur(x, y, 178, 172, 162); else if (b === CORE) sur(x, y, 196, 190, 180, (x + y) % 9 === 0 ? 0.8 : 0.95); + else if (k < g && bx === 2) sur(x, y, 80, 80, 88); else if (k < g) { // Glass: the sky in it, or a lit room behind. - if (bx === 2) { - sur(x, y, 80, 80, 88); - continue; - } const o = (y * W + x) * 3, rr = cl(round(y * 0.3 + 4), 0, GY - 1) * 3; let on = P.on[k] ? hash(k, b * 7 + 1) < 0.92 : hash(k, b * 7 + 3) < 0.1 + 0.6 * night; if (on && P.on[k] && P.onT[k] < 0.5) on = hash(k * 13 + b, floor(P.onT[k] * 22)) > 0.45; - const lc = hash(k, b + 40) < 0.75 ? [255, 204, 128] : [206, 230, 255], a = on ? 0.8 + 0.2 * night : 0; + const lc = hash(k, b + 40) < 0.75 ? [255, 184, 88] : [200, 228, 255], a = on ? 0.9 + 0.1 * night : 0; for (let c = 0; c < 3; c++) F[o + c] = mix(row[rr + c] * 0.42 + c * 5, lc[c], a) * (ry === 2 ? 1.12 : 1); } else { // An open floor: dark inside, the back columns, a work lamp at night. @@ -369,33 +295,20 @@ function building(P) { // The floor on the deck: starter bars, the core panel, the rebar cages, the formwork. const top = dk - FH, st = P.top ? 0 : P.stage; - if (st >= 1) for (let y = top + 1; y < dk; y++) for (let x = BX0 + 30; x <= BX0 + 34; x++) sur(x, y, 176, 172, 164, x === BX0 + 32 ? 0.8 : 1); + if (st > 0) for (let y = top + 1; y < dk; y++) for (let x = BX0 + 30; x <= BX0 + 34; x++) sur(x, y, 176, 172, 164, x === BX0 + 32 ? 0.8 : 1); for (let ci = 0; ci < 6; ci++) { - const x = BX0 + ci * 7, hc = st >= 2 ? round(P.cage * (FH - 1)) : 0; - for (let j = 1; j <= max(2, hc); j++) sur(x, dk - j, 176, 96, 50), j & 1 && sur(x + 1, dk - j, 130, 76, 44); - if (st >= 3 && ci / 6 < P.form) for (let j = 1; j < FH; j++) sur(x, dk - j, 218, 162, 84), sur(x + 1, dk - j, 196, 140, 70); + const x = BX0 + ci * 7; + for (let j = 1; j <= max(2, st > 1 ? round(P.cage * 5) : 0); j++) sur(x, dk - j, 176, 96, 50), j & 1 && sur(x + 1, dk - j, 130, 76, 44); + if (st > 2 && ci / 6 < P.form) for (let j = 1; j < FH; j++) sur(x, dk - j, 218, 162, 84), sur(x + 1, dk - j, 196, 140, 70); } - if (st >= 3) { - const fx = BX0 + 37 * P.form; - for (let x = BX0; x <= min(BX1, fx); x++) { - sur(x, top, 214, 156, 80); - if ((x - BX0) % 7 === 4) for (let j = 1; j < FH; j++) sur(x, top + j, 150, 150, 156, 0.8); - } - } - if (P.pourX >= 0) for (let x = BX0; x <= min(BX1, P.pourX); x++) sur(x, top, 140, 140, 150), (x - BX0) % 7 < 2 && sur(x, top + 1, 140, 140, 150, 0.9); + if (st > 2) for (let x = BX0; x <= BX0 + 36 * P.form; x++) for (let j = 0; j < ((x - BX0) % 7 === 4 ? FH : 1); j++) j ? sur(x, top + j, 150, 150, 156, 0.8) : sur(x, top, 214, 156, 80); + if (P.pourX >= 0 && !P.top) for (let x = BX0; x <= min(BX1, P.pourX); x++) sur(x, top, 140, 140, 150), (x - BX0) % 7 < 2 && sur(x, top + 1, 140, 140, 150, 0.9); if (P.item > 0) drawLoad(P.item, P.ix, dk - LH[P.item]); - if (P.top) { - // Topped out: a little tree and a flag on the roof. - for (let j = 1; j < 9; j++) sur(21, dk - j, 120, 110, 100); - for (let j = 0; j < 4; j++) for (let i = 1; i <= 4 - abs(j - 1); i++) sur(21 + i * (P.w < 0 ? -1 : 1), dk - 8 + j, 230, j & 1 ? 230 : 40, j & 1 ? 230 : 40); - for (let j = 0; j < 4; j++) for (let i = -j; i <= j; i++) sur(26 + i, dk - 5 + j, 40, 120, 60); - } + // Topped out: a little fir tree on the roof. + if (P.top) for (let j = 0; j < 5; j++) for (let i = -j; i <= j; i++) sur(24 + i, dk - 6 + j, 40, 120, 60); // The workers on the deck in hi-vis, and the welder by a column. - for (const w of P.wk) { - const x = round(w.x); - sur(x, dk - 1, 50, 56, 90), sur(x, dk - 2, 255, 130, 20), sur(x, dk - 3, 250, 250, 240); - } - sur(17, dk - 1, 50, 56, 90), sur(17, dk - 2, 255, 130, 20), sur(17, dk - 3, 60, 60, 70); + for (const w of P.wk) man(w.x, dk - 1); + man(17, dk - 1, [60, 60, 70]); } // One of the four loads, centred on x with its top on row y0. @@ -404,9 +317,8 @@ function drawLoad(type, cx, y0) { for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) { const X = x0 + i, Y = y + j; - if (type === 0) { - if (!(i >= 2 && i <= 4 && j >= 1 && j <= 3)) sur(X, Y, 150, 146, 140, i === 0 || j === 0 ? 1.25 : 1); - } else if (type === 1) sur(X, Y, i === 2 || i === 8 ? 60 : 186, i === 2 || i === 8 ? 54 : 92, 48, j ? 0.72 : 1); + if (type === 0) (i < 2 || i > 4 || !j || j > 3) && sur(X, Y, 150, 146, 140, i && j ? 1 : 1.25); + else if (type === 1) sur(X, Y, i === 2 || i === 8 ? 60 : 186, i === 2 || i === 8 ? 54 : 92, 48, j ? 0.72 : 1); else if (type === 2) sur(X, Y, 226, 170, 92, j & 1 ? 0.55 : 1); else if (i >= (j >= h - 2 ? 1 : 0) && i < w - (j >= h - 2 ? 1 : 0)) sur(X, Y, 120, 120, 128, j === 1 ? 0.6 : j === 0 ? 1.3 : 1); } @@ -434,11 +346,11 @@ function yard(P) { if (P.ph === "A" || P.ph === "B") { const s = P.stage, x = MX + PICK_R - LW[s] / 2 - 2, arm = P.ph === "B" ? (P.pt * 6) & 1 : 0; drawLoad(s, MX + PICK_R + 0.5, PICK_Y - LH[s]); - sur(x, PICK_Y - 1, 50, 56, 90), sur(x, PICK_Y - 2, 255, 130, 20), sur(x, PICK_Y - 3, 250, 250, 240), sur(x + 1, PICK_Y - 2 - arm, 255, 130, 20); + man(x, PICK_Y - 1), sur(x + 1, PICK_Y - 2 - arm, 255, 130, 20); } for (let y = LY; y < 122; y++) sur(62, y, 120, 120, 126); const fon = sm(0.3, 0.55, P.h); - for (let x = 59; x <= 62; x++) for (let y = 83; y <= 84; y++) put(x, y, y === 84 && x < 62 ? [mix(70, 255, fon), mix(70, 250, fon), mix(76, 230, fon)] : [90, 90, 96]); + for (let x = 59; x <= 62; x++) put(x, 83, [90, 90, 96]), put(x, 84, x < 62 ? [70 + 185 * fon, 70 + 180 * fon, 76 + 154 * fon] : [90, 90, 96]); } // The crane: mast, ties, slewing ring, cab, tower head, jib and counter-jib. @@ -446,7 +358,8 @@ function crane(P) { const c = cos(P.th), sn = sin(P.th), blink = P.rt % 1.2 < 0.55, fon = sm(0.3, 0.55, P.h); const jib = () => { for (let s = 0; s <= JL; s += 0.3) { - const px = MX + s * c, py = JY - s * sn * 0.1, ht = 3 - (1.2 * s) / JL, tri = abs(((s / 2.4) % 2) - 1), wd = sn * 0.9; + // Swung towards you the tip is nearer and bigger, swung away it shrinks. + const sc = 1 + (0.45 * s * sn) / JL, px = MX + s * c, py = JY - s * sn * 0.1, ht = (3 - (1.2 * s) / JL) * sc, tri = abs(((s / 2.4) % 2) - 1), wd = sn * 1.2 * sc; surc(px - wd, py, YEL, 0.85), surc(px + wd, py, YEL, 0.85), surc(px, py - ht, YEL), surc(px + wd * (1 - tri), py - tri * ht, YEL, 0.75); } }; @@ -463,12 +376,8 @@ function crane(P) { line(MX, APEX + 1, MX - CJL * c, JY + CJL * sn * 0.1 - 1, [110, 104, 96], 0.8); }; const cab = () => { - const x = MX + 3.5 * c; - for (let j = 0; j < 4; j++) - for (let i = -1; i <= 1; i++) { - if (j > 0 && j < 3 && i > -1) put(x + i, JY + 1 + j, P.h > 0.5 ? [255 * fon, 226 * fon, 160 * fon] : [110, 150, 196]); - else surc(x + i, JY + 1 + j, [240, 232, 210]); - } + for (let q = 0, i, j; q < 12; q++) + (i = (q % 3) - 1), (j = (q / 3) | 0), j % 3 && i > -1 ? put(MX + 3.5 * c + i, JY + 1 + j, P.h > 0.5 ? [255 * fon, 226 * fon, 160 * fon] : [110, 150, 196]) : surc(MX + 3.5 * c + i, JY + 1 + j, [240, 232, 210]); }; // What faces away is drawn first. if (sn < 0) jib(), pend(), cab(); @@ -478,8 +387,7 @@ function crane(P) { surc(MX - 2, y, YEL, 0.72), surc(MX + 2, y, YEL); for (let xi = 1; xi <= 3; xi++) if (((y + xi) & 3) === 0 || ((y - xi) & 3) === 0) surc(MX - 2 + xi, y, YEL, 0.8); } - for (let x = MX - 4; x <= MX + 4; x++) for (let y = 117; y <= 119; y++) sur(x, y, 170, 166, 160); - for (let x = MX - 3; x <= MX + 3; x++) sur(x, JY + 1, 80, 80, 86), sur(x, JY + 2, 110, 110, 116); + for (let x = MX - 4; x <= MX + 4; x++) sur(x, 117, 170, 166, 160), sur(x, 118, 170, 166, 160), sur(x, 119, 170, 166, 160), abs(x - MX) < 4 && (sur(x, JY + 1, 80, 80, 86), sur(x, JY + 2, 110, 110, 116)); // The tower head. for (let y = APEX; y <= JY - 3; y++) { const hw = (1.6 * (y - APEX)) / (JY - 3 - APEX); @@ -490,18 +398,14 @@ function crane(P) { else jib(), pend(), cab(); // The trolley, and the warning lights on the head and at the jib tip. const [tx, ty] = tip(P); - for (let i = -1; i <= 1; i++) sur(tx + i, ty, 90, 90, 96); + sur(tx - 1, ty, 90, 90, 96), sur(tx, ty, 90, 90, 96), sur(tx + 1, ty, 90, 90, 96); for (const [x, y] of [[MX, APEX - 1], [MX + JL * c, JY - JL * sn * 0.1 - 2]]) { - if (!blink) { - put(x, y, [110, 20, 16]); - continue; - } - put(x, y, [255, 40, 28]); - for (let j = -2; j <= 2; j++) for (let i = -2; i <= 2; i++) if (i || j) add(x + i, y + j, [255, 30, 20], (0.12 + 0.3 * fon) / (i * i + j * j)); + put(x, y, blink ? [255, 40, 28] : [110, 20, 16]); + if (blink) glow(x, y, [255, 30, 20], 0.12 + 0.3 * fon); } } -// The concrete mixer, its drum turning. +// The mixer, its drum turning. function mixer(P) { const x0 = round(P.mix), night = sm(0.4, 0.9, P.h); if (x0 < -24) return; @@ -519,43 +423,23 @@ function mixer(P) { function cars(P) { const night = sm(0.35, 0.9, P.h), cols = [[200, 40, 36], [220, 220, 226], [40, 80, 170], [60, 64, 70]]; for (const [cx, v, ci] of P.cars) { - const x0 = round(cx), dir = v > 0 ? 1 : -1, fx = dir > 0 ? x0 + 6 : x0; - for (let i = 0; i < 7; i++) for (let j = 0; j < 2; j++) surc(x0 + i, 125 + j, cols[ci]); - for (let i = 2; i < 5; i++) sur(x0 + i, 124, 120, 150, 180); - add(fx, 125, [255, 250, 220], 0.3 + night), add(dir > 0 ? x0 : x0 + 6, 125, [255, 20, 10], 0.2 + night); - if (night > 0.1) for (let i = 1; i < 8; i++) add(fx + dir * i, 126 + (i > 4), [255, 240, 200], night * 0.5 * (1 - i / 8)); + const x0 = round(cx), fx = v > 0 ? x0 + 6 : x0; + for (let i = 0; i < 21; i++) i < 14 ? surc(x0 + (i >> 1), 125 + (i & 1), cols[ci]) : i < 17 && sur(x0 + i - 12, 124, 120, 150, 180); + add(fx, 125, [255, 250, 220], 0.3 + night), add(x0 + 6 - fx + x0, 125, [255, 20, 10], 0.2 + night); } } export function draw(display, params) { const P = params.s; if (!P || P.h === undefined) return; - F = P.f; - LM = P.lm; + [F, LM] = [P.f, P.lm]; const hh = cl(P.h, 0, 1) * 3, i0 = min(2, floor(hh)), K = KEYS[i0].map((v, c) => mix(v, KEYS[i0 + 1][c], hh - i0)); - sky(P, K); - clouds(P, K); - light(P, K); - ground(P); - const behind = sin(P.th) < -0.15; - if (behind) hook(P); - building(P); - yard(P); - crane(P); - mixer(P); - cars(P); - if (!behind) hook(P); + // The hook goes behind the frame when the jib swings away from you. + const behind = sin(P.th) < -0.15, y = deckY(P) - 2, FLb = P.fl; + sky(P, K), clouds(P, K), light(P, K), ground(), behind && hook(P), building(P), yard(P), crane(P), mixer(P), cars(P), behind || hook(P); // The welding arc, its sparks, and the floodlight beams in the dusk air. - const fon = sm(0.3, 0.55, P.h), FLb = P.fl; - if (P.welding) { - const y = deckY(P) - 2, fl = 0.6 + 0.4 * rnd(); - put(18, y, [235, 244, 255]); - for (let j = -2; j <= 2; j++) for (let i = -2; i <= 2; i++) if (i || j) add(18 + i, y + j, [160, 190, 255], (fl * 0.6) / (i * i + j * j)); - } - for (const [x, y, , , l, px, py] of P.sp) { - const a = min(1, l * 1.4); - put(px, py, [255, 120, 30], a * 0.6), put(x, y, [255, 170 + 80 * min(1, l), 70 * min(1, l)], a); - } - if (fon > 0) for (let i = 0, o = 0; i < N; i++, o += 3) if (FLb[i] > 0.01) for (let c = 0; c < 3; c++) F[o + c] += FLb[i] * 22 * FLC[c]; + if (P.welding) put(18, y, [235, 244, 255]), glow(18, y, [160, 190, 255], 0.36 + 0.24 * rnd()); + for (const [x, y, , , l, px, py] of P.sp) put(px, py, [255, 120, 30], min(1, l * 1.4) * 0.6), put(x, y, [255, 170 + 80 * min(1, l), 70 * min(1, l)], min(1, l * 1.4)); + if (P.h > 0.3) for (let i = 0, o = 0; i < N; i++, o += 3) if (FLb[i] > 0.01) for (let c = 0; c < 3; c++) F[o + c] += FLb[i] * 22 * FLC[c]; 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 dad25583053ed80f52bb7a9fcf1765d169fee1bf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:30:05 +0000 Subject: [PATCH 14/33] pipe-organ: brighter speaking pipes, reed ranks, candles that gutter, header --- incoming/pattern-study/patterns/pipe-organ.js | 73 ++++++++++--------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/incoming/pattern-study/patterns/pipe-organ.js b/incoming/pattern-study/patterns/pipe-organ.js index 06b9f8373..09a596a44 100644 --- a/incoming/pattern-study/patterns/pipe-organ.js +++ b/incoming/pattern-study/patterns/pipe-organ.js @@ -1,21 +1,21 @@ // ===== Patternflow pattern ===== // Title: Pipe Organ // Family: music -// Idea: The tin front pipes of a church organ case stand in towers and flats while an unseen organist plays a chorale, and each pipe that speaks lights at its mouth with a standing wave shimmering up its body, the bass in the tall pipes and the tune in the small ones, as light from a rose window drifts across the case. +// Idea: The tin front pipes of a church organ stand in towers and flats while an unseen organist plays a chorale, and each pipe that speaks lights at its mouth with a standing wave shimmering up its body, the bass in the tall pipes and the tune in the small ones, as light from a rose window drifts across the case. // Author: patternflow-lab // Date: 2026-10-24 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: original, pattern round 24, after the Baroque organ case of towers and flats (Arp Schnitger's Hamburg cases) and four-part chorale harmony +// Lineage: original, pattern round 24, after the tower-and-flat fronts of North German Baroque organ cases and four-part chorale harmony // Round: 24 // =============================== // @matrix 64x128 // @knobs Stops=0..1, Tempo=0.2..3, Voices=1..6, Tremulant=0..1 // -// Knob 1: Stops. The registration: flutes at 0 glow soft blue with one broad band, principals in the middle speak silver-white and add the octave above each note, and the reeds at 1 burn gold at the mouth and red up the body with many bands and the fifteenth as well. -// Knob 2: Tempo. Chords a second over two: at 2 a chord takes a second and a phrase with its held last chord about ten, at 0.2 one chord holds for ten seconds. -// Knob 3: Voices. How many parts the hands play over the pedal: 1 is the tune alone over the bass, 3 the full four-part chorale, and 4 to 6 fill the chord with more pipes. -// Knob 4: Tremulant. The wind shakes: the shimmer in the pipes wavers and throbs, and the two candles on the case flicker until the whole case flickers with them. -// Buttons: K1 tap is full organ: every pipe in the case speaks at once for a few seconds. +// Knob 1: Stops. The registration. Flutes at 0 glow soft blue in two broad bands; principals in the middle speak silver-white and add the octave above each note; the reeds at 1 burn gold at the mouth and red up the body in six bands, with the octave below, the twelfth and the fifteenth speaking too. +// Knob 2: Tempo. At 2 a chord takes a second, so a phrase of eight with its held last chord takes about nine; at 0.2 a chord holds for ten seconds and at 3 for two thirds of one. +// Knob 3: Voices. How many parts the hands play over the pedal: at 1 the tune alone over the bass, at 3 the four-part chorale, and 4 to 6 add parts that fill out the chord. +// Knob 4: Tremulant. The wind shakes. The shimmer in the speaking pipes throbs and wavers, and the two candles on the case flicker and gutter until the light on the whole lower case flickers with them. +// Buttons: K1 tap is full organ: every pipe in the front speaks at once for about three seconds. const W = 64; const H = 128; @@ -95,7 +95,7 @@ const ROSE = [RUBY, SAPH, GREEN, SAPH]; const FLUTE = [0.35, 0.62, 1]; const PRIN = [0.84, 0.9, 1]; const REED0 = [1, 0.77, 0.27]; -const REED1 = [1, 0.28, 0.14]; +const REED1 = [1, 0.2, 0.08]; const FULL = [1, 0.86, 0.58]; // Round tin: a highlight a third of the way across and a dark far edge. @@ -201,8 +201,8 @@ function build(P) { put(x, 113, WOOD, 0.5); } // Candles on the stiles between the side towers and the flats. - for (let y = 98; y <= 101; y++) two(10, y, [0.95, 0.9, 0.8], 1); - for (let x = 9; x <= 11; x++) two(x, 102, GILT, 1.1); + for (let y = 98; y <= 101; y++) two(10, y, [0.95, 0.9, 0.8], 0.5); + for (let x = 9; x <= 11; x++) two(x, 102, GILT, 0.6); // The console: the music desk with its lamp, and one manual. for (let y = 114; y < H; y++) for (let x = 0; x < W; x++) put(x, y, WOOD, 0.7); for (let y = 114; y <= 119; y++) for (let x = 16; x <= 47; x++) put(x, y, WOOD, 0.85); @@ -307,8 +307,9 @@ export function update(dt, input, params) { P.full = P.fullT < 2.8 ? sm(0, 0.06, P.fullT) : 1 - sm(2.8, 3.6, P.fullT); // Which pipes speak: each part's note, the octave above from the - // principals on, two octaves above with the reeds. - const c = P.cur, pos = P.beat - c.at, r4 = 0.8 * sm(0.3, 0.55, s), r2 = 0.65 * sm(0.7, 0.92, s); + // principals on, and with the reeds the octave below, the twelfth and the + // fifteenth. + const c = P.cur, pos = P.beat - c.at, r4 = 0.8 * sm(0.3, 0.55, s), r2 = 0.75 * sm(0.7, 0.92, s); P.tgt.fill(0); P.keys.fill(0); if (!(c.last && pos > c.dur - 0.22)) @@ -318,6 +319,7 @@ export function update(dt, input, params) { P.tgt[g] = 1; if (v !== 3) P.keys[g] = 1; if (g + 7 < NP) P.tgt[g + 7] = max(P.tgt[g + 7], r4); + if (g >= 7) P.tgt[g - 7] = max(P.tgt[g - 7], r2); if (g + 11 < NP) P.tgt[g + 11] = max(P.tgt[g + 11], r2); if (g + 14 < NP) P.tgt[g + 14] = max(P.tgt[g + 14], r2); } @@ -328,9 +330,10 @@ export function update(dt, input, params) { P.amp[i] += (g - P.amp[i]) * (1 - exp(-d / (g > P.amp[i] ? 0.035 : 0.14))); P.chiff[i] *= exp(-d / 0.18); } - // Candles in the draught: they flicker harder as the tremulant shakes the wind. - const fa = 0.06 + 0.8 * tr; - const fl = (ph) => 1 - 0.35 * fa + fa * (0.6 * sin(t * 7.1 + ph) + 0.45 * sin(t * 11.9 + 2 * ph) + 0.35 * sin(t * 4.3 + 3 * ph) * sin(t * 17.3)); + // Candles in the draught: as the tremulant shakes the wind they flicker + // harder and burn lower. + const fa = 0.05 + tr; + const fl = (ph) => 1 - 0.4 * fa + fa * (0.6 * sin(t * 7.1 + ph) + 0.45 * sin(t * 11.9 + 2 * ph) + 0.35 * sin(t * 4.3 + 3 * ph) * sin(t * 17.3)); P.fl = cl(fl(0), 0.1, 2); P.fr = cl(fl(2.1), 0.1, 2); } @@ -338,7 +341,7 @@ export function update(dt, input, params) { // A stop's colour, u from the mouth (0) to the top (1). function stopCol(out, s, u, full) { for (let n = 0; n < 3; n++) { - const v = s < 0.5 ? mix(FLUTE[n], PRIN[n], s * 2) : mix(PRIN[n], mix(REED0[n], REED1[n], u), s * 2 - 1); + const v = s < 0.5 ? mix(FLUTE[n], PRIN[n], s * 2) : mix(PRIN[n], mix(REED0[n], REED1[n], min(1, u * 1.6)), s * 2 - 1); out[n] = mix(v, FULL[n], full); } } @@ -372,21 +375,13 @@ export function draw(display, params) { const kk = I0 * (lead ? 0.45 : 1) * sm(1, 0.82, r), o = (y * W + x) * 3; for (let n = 0; n < 3; n++) F[o + n] += A[o + n] * c[n] * kk; } - // The candle flames. - for (const [x, f] of [[10, P.fl], [53, P.fr]]) { - const add = (xx, y, c, k) => { - const o = (y * W + xx) * 3; - for (let n = 0; n < 3; n++) F[o + n] += c[n] * k; - }; - add(x, 97, [255, 235, 170], min(1.2, f)); - add(x, 96, [255, 160, 60], min(1.2, f)); - add(x, 95, [200, 80, 20], cl(f - 0.9, 0, 1)); - add(x - 1, 97, [140, 70, 20], 0.5 * f); - add(x + 1, 97, [140, 70, 20], 0.5 * f); - } // The pipes that speak: the mouth lights and a standing wave shimmers up // the body, its bands creeping upward and wavering with the tremulant. - const s = P.s, tr = P.tr, gain = 300 * (0.7 + 0.65 * s), bands = 2 + 4 * s; + const s = P.s, tr = P.tr, gain = 240 * (0.8 + 0.4 * s), bands = 2 + 4 * s, floor0 = 0.45 - 0.25 * s; + // Many pipes at once light the case less each, or the chord drowns in its own light. + let ns = 0; + for (let i = 0; i < NP; i++) ns += P.amp[i]; + const spread = (1 - 0.6 * P.full) / (1 + 0.12 * max(0, ns - 2)); const trem = sin(2 * PI * 5.4 * t), tm = 1 - 0.9 * tr * (0.5 + 0.5 * trem), wob = 0.3 * tr * trem; const col = [0, 0, 0]; for (let i = 0; i < NP; i++) { @@ -397,7 +392,7 @@ export function draw(display, params) { const bm = max(bands, 5 * P.full), creep = 0.3 * t + i * 0.37; // Its light on the polished tin and the gilt round it. stopCol(col, s, 0.4, P.full); - const kb = am * tm * (0.3 + 0.45 * s) * 255; + const kb = am * tm * (0.22 + 0.5 * s) * spread * 255; for (let y = max(0, top - 10); y < min(H, base + 6); y++) { const ly = kb * (y < top ? exp((y - top) / 4) : y > m ? exp((m - y) / 7) : 1); for (let xx = max(0, x - 16); xx < min(W, x + w + 16); xx++) { @@ -408,7 +403,7 @@ export function draw(display, params) { for (let y = top; y < m; y++) { const u = (m - 0.5 - y) / span; const bv = abs(cos(PI * (bm * u - creep + wob))) ** 2.5 * osc; - const g = a * (0.7 + 0.3 * exp(-3 * u)) * (0.42 + 0.58 * bv); + const g = a * (0.75 + 0.25 * exp(-3 * u)) * (floor0 + (1 - floor0) * bv); stopCol(col, s, u, P.full); for (let j = -2; j < w + 2; j++) { const xx = x + j; @@ -427,9 +422,21 @@ export function draw(display, params) { const e = ((xx + 0.5 - cx) / (w / 2 + 1.2)) ** 2 + ((y + 0.5 - cy) / 2.6) ** 2; if (e > 3) continue; const slot = y >= m && y < m + mh && xx > x && xx < x + w - 1; - const o = (y * W + xx) * 3, k = slot ? hot * 1.2 : hot * 0.42 * exp(-e * 1.3); - for (let n = 0; n < 3; n++) F[o + n] += mix(col[n], 1, slot ? 0.55 : 0.15) * k; + const o = (y * W + xx) * 3, k = slot ? hot * 2 : hot * 0.5 * exp(-e * 1.3); + for (let n = 0; n < 3; n++) F[o + n] += mix(col[n], 1, slot ? 0.7 : 0.2) * k; + } + } + // The candle flames, each with a halo in the air round it. + for (const [x, f] of [[10, P.fl], [53, P.fr]]) { + for (let y = 92; y <= 100; y++) + for (let xx = x - 3; xx <= x + 3; xx++) { + const e = (xx - x) ** 2 + ((y - 96.5) * 0.8) ** 2, o = (y * W + xx) * 3; + for (let n = 0; n < 3; n++) F[o + n] += CAN[n] * 0.45 * f * exp(-e / 3); } + for (const [y, c, k] of [[97, [255, 250, 225], 1], [96, [255, 236, 160], min(1, f)], [95, [255, 190, 80], cl(f, 0, 1)], [94, [255, 120, 30], cl(f - 0.8, 0, 1)]]) { + const o = (y * W + x) * 3; + for (let n = 0; n < 3; n++) F[o + n] = max(F[o + n], c[n] * k); + } } // Keys held down on the manual. for (let i = 0; i < NP; i++) From 2b4374e05f5db48a73529476954cf4a38b32ecb4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:30:18 +0000 Subject: [PATCH 15/33] thermal: sample open sky once when painting --- incoming/pattern-study/patterns/thermal.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/patterns/thermal.js b/incoming/pattern-study/patterns/thermal.js index f91b7cc31..8bec9dc73 100644 --- a/incoming/pattern-study/patterns/thermal.js +++ b/incoming/pattern-study/patterns/thermal.js @@ -180,14 +180,15 @@ function paint(S) { for (let y = 0; y < H; y++) { const q = (y * W + x) * 3; bg[q] = bg[q + 1] = bg[q + 2] = 0; - for (let k = 0; k < 4; k++) { + // The open sky is smooth enough for one sample. + for (let k = y + 1 < farT ? 3 : 0; k < 4; k++) { const xs = x + 0.25 + 0.5 * (k & 1), ys = y + 0.25 + 0.5 * (k >> 1); let c; if (ys >= FY) c = fieldAt(S, xs, ys, sun, Lsh, haze); else if (ys >= nearT) c = ridgeN.map((v) => v * (0.84 + 0.3 * fbm(x * 0.35, ys * 0.9, 24))); else if (ys >= farT) c = ridgeF; else c = skyAt(xs, ys, h); - for (let i = 0; i < 3; i++) bg[q + i] += c[i] / 4; + for (let i = 0; i < 3; i++) bg[q + i] += y + 1 < farT ? c[i] : c[i] / 4; } } } From 4b8fcaf5c9650dce2ab6a7008f62153ec93c0618 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:30:53 +0000 Subject: [PATCH 16/33] flying-trapeze: first version runs (WIP) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KTNzLtP3gH9rQENoucsegc --- .../pattern-study/patterns/flying-trapeze.js | 798 ++++++++++++++++++ 1 file changed, 798 insertions(+) create mode 100644 incoming/pattern-study/patterns/flying-trapeze.js diff --git a/incoming/pattern-study/patterns/flying-trapeze.js b/incoming/pattern-study/patterns/flying-trapeze.js new file mode 100644 index 000000000..78f143337 --- /dev/null +++ b/incoming/pattern-study/patterns/flying-trapeze.js @@ -0,0 +1,798 @@ +// ===== Patternflow pattern ===== +// Title: Flying Trapeze +// Family: stage +// Idea: Looking up into a circus big top at night, a flyer swings out from the board, lets go at the top of the arc, turns her somersaults in the follow spots and is caught by the catcher hanging by his knees, then swings home to the bar or drops into the safety net over the dark crowd. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 24, after the flying trapeze act Jules Léotard first flew at the Cirque Napoléon in 1859 +// Round: 24 +// =============================== +// @matrix 64x128 +// @knobs Lights=0..1, Speed=0.2..3, Turns=1..3, Haze=0..1 +// +// Knob 1: Lights. The gels in the follow spots and the sequins on the costumes: warm white and gold at 0, amber and rose, then pink and lavender, and at 1 one pink spot and one blue. The house light on the canvas turns with them. +// Knob 2: Speed. How fast the act runs: at 2 a swing of the fly bar takes 2.4 s and a flyer lets go every eight to ten seconds; at 0.2 it all runs ten times slower. +// Knob 3: Turns. The somersaults in the trick, one, two or three. A bigger trick swings higher and flies longer, and the house goes darker for it while every spot finds the flyer. +// Knob 4: Haze. How much haze hangs under the canvas. A little shows the beams as thin shafts; turned up they swell and the whole tent glows in the gels. +// Buttons: K1 tap starts a drum roll: the next trick is the triple, and it ends in the net. + +const W = 64; +const H = 128; +const N = W * H; +const { min, max, sin, cos, abs, sqrt, exp, floor, ceil, round, atan2, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => { + const t = cl((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; +const mix = (a, b, t) => a + (b - a) * t; +const ease = (s) => s - sin(TAU * s) * (0.8 / TAU); +// One swing of the fly bar, in seconds at Speed 2. +const PB = 2.4; +const OM = TAU / PB; +// The fly bar's crane and rope, and the catcher's cradle, rope and swing. +const [PFX, PFY, LF] = [24, 30, 34]; +const [PCX, PCY, LC, B] = [57, 30, 20, 0.18]; +// The board's top, where the next flyer waits, and the ladder. +const [BY, WX, LX] = [68, 1.2, 2.6]; +// Figure scale, hips above the feet, hands above the hips. +const SC = 1; +const LEG = 3.8 * SC; +const HL = 5.7 * SC; +const G = 90; +const TR = 0.45; +// Follow spots, below the panel behind the crowd, and how fast their cones open. +const SPOT = [ + [-14, 166], + [78, 166], + [32, 186], +]; +const TA = 0.046; + +// Gels by the Lights knob: house wash (0..1), three spots, two flyers and the catcher. +const PAL = [ + [[1, 0.8, 0.52], [255, 226, 170], [255, 244, 222], [255, 206, 120], [255, 182, 36], [205, 222, 255], [70, 120, 255]], + [[1, 0.64, 0.5], [255, 182, 120], [255, 150, 150], [255, 208, 150], [255, 96, 112], [255, 206, 60], [250, 240, 230]], + [[0.82, 0.52, 0.86], [255, 122, 196], [186, 142, 255], [255, 166, 214], [255, 62, 196], [160, 104, 255], [110, 236, 214]], + [[0.46, 0.44, 1], [255, 76, 170], [66, 128, 255], [172, 96, 255], [255, 44, 156], [36, 150, 255], [245, 245, 255]], +]; +const SKIN = [255, 196, 156]; +const STEEL = [170, 176, 190]; +const ROPE = [150, 138, 116]; + +function palette(S, k) { + const u = cl(k, 0, 1) * 3; + const i = min(2, floor(u)); + const f = u - i; + for (let j = 0; j < 7; j++) for (let c = 0; c < 3; c++) S.P[j][c] = mix(PAL[i][j][c], PAL[i + 1][j][c], f); +} + +const mkF = (c) => ({ m: 'wait', x: WX, y: BY - LEG, ph: 0, fs: 1, a1: 2.9, a2: 2.9, l1: 0, l2: 0, k1: 0, k2: 0, hx: null, hy: 0, T: 0, vx: 0, vy: 0, px: WX, py: BY - LEG, c }); +function rest(f) { + f.l1 = f.l2 = f.k1 = f.k2 = 0; + f.a1 = f.a2 = 2.9; + f.hx = null; +} + +export function setup(params) { + const S = { + t: 0, + started: false, + F: new Float32Array(N * 3), + LT: new Float32Array(N * 3), + CA: new Float32Array(N * 3), + CI: new Float32Array(N), + AUD: new Float32Array(N), + ROW: new Int8Array(N).fill(-1), + POOL: new Float32Array(H), + bar: 'free', + A: 0.55, + rd: null, + phc: 0, + ct: null, + psi: 0, + fl: [mkF(4), mkF(5)], + star: 0, + sp: SPOT.map(([sx, sy]) => ({ sx, sy, ax: 30, ay: 50, ux: 0, uy: -1, D: 110 })), + drum: 0, + drumB: 0, + pop: 0, + wash: 1, + trip: false, + nextFin: true, + turns: 1, + net: { s: 0, v: 0, x: 32 }, + fx: [], + phones: [], + P: PAL[0].map((c) => c.slice()), + hz: 0.3, + }; + // The canvas: red and cream panels running down from the peak, each billowing between its seams. + for (let y = 0; y < H; y++) { + S.POOL[y] = exp(-(((y - 17) / 15) ** 2)); + for (let x = 0; x < W; x++) { + const i = y * W + x; + const dx = x - 32; + const dy = y - 5; + const a = atan2(dx, dy); + const red = 0.5 + 0.5 * cl(sin(a * 7) * 4, -1, 1); + const bil = 0.84 - 0.16 * cos(a * 14); + const near = sm(2.5, 6, sqrt(dx * dx + dy * dy)); + S.CA[i * 3] = mix(0.95, 0.88, red) * bil * near; + S.CA[i * 3 + 1] = mix(0.86, 0.12, red) * bil * near; + S.CA[i * 3 + 2] = mix(0.68, 0.12, red) * bil * near; + S.CI[i] = (0.3 + 0.7 * exp(-((dx * dx) / 900 + (dy * dy) / 300))) * sm(40, 20, y); + } + } + // The crowd: rows of heads and shoulders, bigger towards the front. + const rows = [ + [108.5, 1.9, 4.6], + [114, 2.3, 5.4], + [120.5, 2.8, 6.4], + [127.5, 3.4, 7.6], + ]; + rows.forEach(([base, r, gap], k) => { + for (let cx = -2 + Math.random() * gap; cx < W + 3; cx += gap * (0.8 + 0.4 * Math.random())) { + const hy = base - r * 1.15 + (Math.random() - 0.5) * 1.2; + const hx = cx + (Math.random() - 0.5); + for (let y = max(0, floor(hy - r - 1)); y < H; y++) + for (let x = max(0, floor(hx - r * 2.4)); x <= min(W - 1, ceil(hx + r * 2.4)); x++) { + const hd = r + 0.5 - sqrt((x - hx) ** 2 + (y - hy) ** 2); + const sh = 1 - sqrt(((x - hx) / (r * 2.2)) ** 2 + ((y - base) / (r * 0.9)) ** 2); + const c = y >= base ? 1 : cl(max(hd, sh * r * 2), 0, 1); + const i = y * W + x; + if (c > S.AUD[i]) S.AUD[i] = c; + if (c > 0.5) S.ROW[i] = k; + } + if (Math.random() < 0.09) S.phones.push([round(hx + 1), round(hy - r - 1)]); + } + }); + params.s = S; +} + +// The catcher on his cradle at swing angle psi: knees over the bar, body and arms hanging. +function cpose(psi) { + const ps = psi * 1.2; + const bx = PCX + LC * sin(psi); + const by = PCY + LC * cos(psi); + const sx = sin(ps); + const sy = cos(ps); + const hx = bx + 1.9 * SC * sx; + const hy = by + 1.9 * SC * sy; + const shx = hx + 3 * SC * sx; + const shy = hy + 3 * SC * sy; + // Arms reach down and a little towards the flyer. + const ax = sx * cos(0.2) - cos(ps) * sin(0.2); + const ay = sy * cos(0.2) + sin(ps) * sin(0.2); + return { bx, by, x: hx, y: hy, ph: PI - ps, hx: shx + 2.7 * SC * ax, hy: shy + 2.7 * SC * ay }; +} + +const barAt = (th) => [PFX + LF * sin(th), PFY + LF * cos(th)]; + +function newRound(S, go, who) { + const trip = S.trip; + S.trip = false; + const turns = trip ? 3 : S.turns; + const fin = trip || S.nextFin; + S.nextFin = !fin; + const tf = 0.75 + 0.25 * turns; + const tr = go + 1.5 * PB; + S.rd = { turns, fin, tf, h: 8 + 3 * turns, tg: go, tr, tc: tr + tf, rel: false, A0: S.A, A: 0.55 + 0.03 * (turns - 1), t0: S.t, over: false, cnt: 0 }; + S.star = who; + // The catcher times his swing to be out at the front of it when she arrives. + const dt = max(0.3, S.rd.tc - S.t); + let k = round((S.phc + OM * dt) / TAU); + if ((TAU * k - S.phc) / dt < 0.6 * OM) k++; + S.ct = { ph: TAU * k, t: S.rd.tc, ev: 0 }; +} + +function burst(S, n) { + for (let i = 0; i < n; i++) S.fx.push({ x: 2 + Math.random() * 60, y: 106 + Math.random() * 20, a: 1, w: Math.random() * 0.25 }); +} + +function netY(x) { + return 98.5 + 1.6 * sin((PI * cl(x, 0, 63)) / 63); +} + +function catcherEvent(S, ev) { + const rd = S.rd; + const f = S.fl[S.star]; + if (ev === 0) { + if (f.m === 'fly') f.m = 'caught'; + S.pop = 1; + burst(S, 7); + S.ct = rd.fin ? { ph: S.phc + 1.5 * PI, t: S.t + 2.4, ev: 2 } : { ph: S.phc + TAU, t: rd.tg + 3.5 * PB - TR, ev: 1 }; + } else if (ev === 1) { + f.m = 'ret'; + f.T = 0; + f.sx = f.x; + f.sy = f.y; + f.ph0 = f.ph; + } else { + f.m = 'drop'; + f.T = 0; + f.ph0 = f.ph; + f.vx = f.vx0; + f.vy = f.vy0; + const dy = netY(f.x + f.vx * 0.9) - 1 - f.y; + f.TF = (-f.vy + sqrt(f.vy * f.vy + 2 * G * max(1, dy))) / G; + rd.over = true; + } +} + +function stepFlyer(S, f, d, i) { + const rd = S.rd; + const t = S.t; + f.T += d; + const [hbx, hby] = barAt(-S.A); + const th = S.th; + const [bx, by] = barAt(th); + switch (f.m) { + case 'wait': + rest(f); + f.fs = 1; + f.x += cl(WX - f.x, -6 * d, 6 * d); + f.y = BY - LEG; + f.ph = 0.04 * sin(t * 1.3 + i); + break; + case 'hold': + rest(f); + f.fs = 1; + f.x += cl(hbx - 1 - f.x, -7 * d, 7 * d); + f.y = BY - LEG; + f.ph = 0.12 * sm(3, 0.5, abs(hbx - 1 - f.x)); + if (abs(hbx - 1 - f.x) < 1.5) { + f.hx = hbx; + f.hy = hby; + } + if (t >= rd.tg) { + S.bar = 'free'; + f.m = 'swing'; + f.T = 0; + f.sx = f.x; + f.sy = f.y; + } + break; + case 'swing': + case 'ride': { + f.fs = f.m === 'ride' ? -1 : 1; + const k = f.m === 'swing' ? sm(0, 0.3, f.T) : 1; + f.x = mix(f.sx, bx + HL * sin(th), k); + f.y = mix(f.sy, by + HL * cos(th), k); + f.ph = -th * k; + const u = (f.fs * th) / S.A; + f.l1 = f.l2 = (0.95 * max(0, u) + 0.35 * min(0, u)) * k; + f.k1 = f.k2 = 0.25 * max(0, -u) * k; + f.hx = bx; + f.hy = by; + if (f.m === 'swing' && !rd.rel && t >= rd.tr) { + const c = cpose(-B); + f.m = 'fly'; + rd.rel = true; + f.Rx = f.x; + f.Ry = f.y; + f.Cx = c.hx + HL * sin(-0.8 * B); + f.Cy = c.hy + HL * cos(-0.8 * B); + f.phR = f.ph; + f.pkR = f.l1; + f.hx = null; + } + if (f.m === 'ride' && t >= rd.tg + 4 * PB) { + // Home on the board. The flyer who was waiting takes the bar next. + S.bar = 'hook'; + rd.over = true; + f.m = 'land'; + f.T = 0; + f.sx = f.x; + f.sy = f.y; + const o = S.fl[1 - i]; + if (o.m === 'wait') { + o.m = 'hold'; + f.next = 'wait'; + newRound(S, t + 1.1, 1 - i); + } else { + f.next = 'hold'; + newRound(S, t + 1.1, i); + } + } + break; + } + case 'fly': { + const s = cl((t - rd.tr) / rd.tf, 0, 1); + f.x = mix(f.Rx, f.Cx, s); + f.y = mix(f.Ry, f.Cy, s) - 4 * rd.h * s * (1 - s); + f.ph = mix(f.phR, 0.8 * B - TAU * rd.turns, ease(s)); + const u = sm(0.04, 0.22, s) * (1 - sm(0.7, 0.9, s)); + f.l1 = f.l2 = max(f.pkR * (1 - sm(0, 0.12, s)), 2.2 * u); + f.k1 = f.k2 = 2.4 * u; + f.a1 = f.a2 = 2 * u; + f.hx = null; + break; + } + case 'caught': { + const c = S.cp; + const tp = S.psi * 0.8; + f.x = c.hx + HL * sin(tp); + f.y = c.hy + HL * cos(tp); + f.ph = -tp; + f.fs = 1; + f.l1 = f.l2 = 0.4 * sin(S.phc + 0.6); + f.k1 = f.k2 = 0; + f.hx = c.hx; + f.hy = c.hy; + break; + } + case 'ret': { + // A half turn in the air and back to the fly bar, which comes out to meet her. + const s = cl(f.T / TR, 0, 1); + const [ex, ey] = barAt(rd.A); + f.x = mix(f.sx, ex + HL * sin(rd.A), s); + f.y = mix(f.sy, ey + HL * cos(rd.A), s) - 18 * s * (1 - s); + f.ph = mix(f.ph0, -rd.A, s); + f.fs = s < 0.5 ? 1 : -1; + f.a1 = 1.3 * sin(PI * s); + f.a2 = -f.a1; + f.l1 = f.l2 = 0.3 * sin(PI * s); + f.hx = null; + if (s >= 1) f.m = 'ride'; + break; + } + case 'land': { + const k = sm(0, 0.35, f.T); + f.x = mix(f.sx, hbx - 1, k); + f.y = mix(f.sy, BY - LEG, k); + f.ph = mix(f.ph, 0, k); + rest(f); + f.hx = hbx; + f.hy = hby; + if (f.T > 0.4) f.m = f.next; + break; + } + case 'drop': { + f.vy += G * d; + f.x += f.vx * d; + f.y += f.vy * d; + const s = cl(f.T / f.TF, 0, 1); + f.ph = mix(f.ph0, -PI / 2 - TAU, ease(s)); + const u = sm(0.1, 0.3, s) * (1 - sm(0.62, 0.85, s)); + f.l1 = f.l2 = 2.2 * u; + f.k1 = f.k2 = 2.4 * u; + f.a1 = f.a2 = 2 * u + 0.6 * (1 - u); + f.hx = null; + if (f.y >= netY(f.x) - 1) { + f.m = 'net'; + f.T = 0; + S.net.x = f.x; + S.pop = max(S.pop, 0.8); + burst(S, 6); + } + break; + } + case 'net': { + // Bouncing on the net: a stiff spring while she is in it, a fall while she is out. + const n = ceil(d / 0.006); + const h = d / max(1, n); + for (let j = 0; j < n; j++) { + const pen = f.y - (netY(f.x) - 1); + const acc = G - (pen > 0 ? 420 * pen + 7 * f.vy : 0); + f.vy += acc * h; + f.y += f.vy * h; + f.x += f.vx * h; + f.vx *= 1 - 2.5 * h; + if (pen > 0) { + S.net.s = pen; + S.net.v = f.vy; + } else { + S.net.v += (-900 * S.net.s - 22 * S.net.v) * h; + S.net.s += S.net.v * h; + } + } + S.net.x = f.x; + S.net.hold = true; + f.ph = -PI / 2; + f.l1 = f.l2 = 0.6; + f.k1 = f.k2 = 0.4; + f.a1 = 1.6 + 0.5 * sin(f.T * 9); + f.a2 = 1.6 - 0.5 * sin(f.T * 9); + if (f.T > 2.6) { + f.m = 'up'; + f.T = 0; + f.sy = f.y; + } + break; + } + case 'up': + case 'tada': + case 'walk': { + S.net.s += (1.5 - S.net.s) * min(1, d * 8); + S.net.v = 0; + S.net.x = f.x; + S.net.hold = true; + const top = netY(f.x) + S.net.s - LEG - 0.6; + if (f.m === 'up') { + const k = sm(0, 0.5, f.T); + f.ph = mix(-PI / 2, 0, k); + f.y = mix(f.sy, top, k); + rest(f); + if (f.T > 0.55) (f.m = 'tada'), (f.T = 0); + } else if (f.m === 'tada') { + // Arms up for the crowd. + f.ph = 0; + f.y = top; + f.a1 = 0.55; + f.a2 = -0.55; + if (f.T > 0.8) (f.m = 'walk'), (f.T = 0); + } else { + f.fs = -1; + f.x -= 13 * d; + f.y = top - 1.1 * abs(sin(f.T * 8)); + f.ph = 0; + f.l1 = 0.5 * sin(f.T * 8); + f.l2 = -f.l1; + f.a1 = PI - 0.5 * sin(f.T * 8); + f.a2 = PI + 0.5 * sin(f.T * 8); + if (f.x <= LX) (f.m = 'climb'), (f.T = 0); + } + break; + } + case 'climb': { + // Up the rope ladder to the board. + f.x = LX; + f.y -= 12 * d; + f.fs = -1; + f.ph = 0; + const w = sin(f.T * 9); + f.a1 = 0.35 + 0.45 * w; + f.a2 = 0.35 - 0.45 * w; + f.l1 = 0.9 * max(0, w); + f.l2 = 0.9 * max(0, -w); + f.k1 = 1.5 * max(0, w); + f.k2 = 1.5 * max(0, -w); + f.hx = null; + if (f.y <= BY - LEG) { + f.y = BY - LEG; + f.m = 'wait'; + } + break; + } + } + if (d > 0) { + f.vx0 = (f.x - f.px) / d; + f.vy0 = (f.y - f.py) / d; + } + f.px = f.x; + f.py = f.y; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s; + const kv = input.knobValues || [0, 2, 0, 0.06]; + const rdt = cl(dt || 0, 0, 0.1); + const d = rdt * (cl(kv[1], 0.2, 3) / 2); + palette(S, kv[0]); + S.turns = cl(round(kv[2]), 1, 3); + S.hz = 0.26 + 0.74 * cl(kv[3], 0, 1); + if (!S.started) { + // Open mid-act: the first flyer is already out on her first swing. + S.started = true; + S.fl[0].m = 'swing'; + S.fl[0].T = 1; + S.fl[0].sx = S.fl[0].sy = 0; + newRound(S, -0.9, 0); + S.A = S.rd.A; + S.bar = 'free'; + S.phc = S.ct.ph - OM * S.rd.tc; + } + const tp = S.t; + S.t += d; + const t = S.t; + const rd = S.rd; + if (input.btnPressed && input.btnPressed[0]) { + S.drumB = 1; + if (!rd.rel && !rd.over) { + rd.turns = 3; + rd.fin = true; + S.nextFin = false; + rd.tf = 1.5; + rd.h = 17; + rd.tc = rd.tr + rd.tf; + if (S.ct && S.ct.ev === 0) S.ct.t = rd.tc; + } else S.trip = true; + } + S.A = S.bar === 'hook' && !rd.over ? mix(rd.A0, rd.A, sm(rd.t0, rd.tg, t)) : rd.A; + S.th = S.bar === 'hook' ? -S.A : -S.A * cos(OM * (t - rd.tg)); + // The catcher, phase-locked to the next thing he has to be on time for. + if (S.ct) { + const rem = S.ct.t - tp; + if (rem <= d) { + S.phc = S.ct.ph; + const ev = S.ct.ev; + S.ct = null; + S.psi = -B * cos(S.phc); + S.cp = cpose(S.psi); + catcherEvent(S, ev); + } else S.phc += ((S.ct.ph - S.phc) * d) / rem; + } else S.phc += OM * d; + S.psi = -B * cos(S.phc); + S.cp = cpose(S.psi); + S.net.hold = false; + for (let i = 0; i < 2; i++) stepFlyer(S, S.fl[i], d, i); + if (!S.net.hold) { + S.net.v += (-900 * S.net.s - 22 * S.net.v) * min(d, 0.02); + S.net.s += S.net.v * min(d, 0.02); + S.net.s *= exp(-d * 3); + } + // After a drop the bar swings empty until it comes back to the board and the next flyer hooks it. + const r2 = S.rd; + if (r2.over && S.bar === 'free') { + const c = floor((t - r2.tg) / PB); + if (c > floor((tp - r2.tg) / PB)) { + const w = S.fl.findIndex((f) => f.m === 'wait'); + if (w >= 0) { + S.bar = 'hook'; + S.fl[w].m = 'hold'; + newRound(S, t + 0.7, w); + } + } + } + // Light cues: the house dims for the drum roll and comes up on the catch. + const R = S.rd; + const star = S.fl[S.star]; + const roll = (!R.rel && t > R.tg + 0.5 * PB) || star.m === 'fly' || S.drumB > 0 ? 1 : 0; + S.drum += (roll - S.drum) * (1 - exp(-d * 4)); + S.drumB = max(0, S.drumB - rdt * 0.4); + S.pop *= exp(-d * 0.9); + const dim = 0.7 - 0.22 * (R.turns - 1); + S.wash = (1 - S.drum * (1 - dim)) * (1 + 0.8 * S.pop); + // The follow spots: two on the flyer, the third on the catcher unless the trick is big. + const fin = S.fl.find((f) => f.m === 'drop' || f.m === 'net' || f.m === 'up' || f.m === 'tada'); + const t0 = fin || star; + const t1 = fin && t < R.tg + 0.4 ? fin : star; + const big = S.drum > 0.5 && R.turns >= 2; + const tg = [t0, t1, big ? star : { x: S.cp.x, y: S.cp.y + 3 }]; + const k = 1 - exp(-d * 8); + for (let j = 0; j < 3; j++) { + const s = S.sp[j]; + s.ax += (tg[j].x - s.ax) * k; + s.ay += (tg[j].y - 1 - s.ay) * k; + const vx = s.ax - s.sx; + const vy = s.ay - s.sy; + s.D = sqrt(vx * vx + vy * vy); + s.ux = vx / s.D; + s.uy = vy / s.D; + } + // Camera flashes in the crowd. + if (Math.random() < rdt * 0.9) burst(S, 1); + for (const e of S.fx) e.a -= rdt * 9; + S.fx = S.fx.filter((e) => e.a > 0); +} + +// A stroke lit by the light field, with optional sequin sparkle. +function seg(S, ax, ay, bx, by, r, c, sp) { + const F = S.F; + const L = S.LT; + const x0 = max(0, floor(min(ax, bx) - r - 1)); + const x1 = min(W - 1, ceil(max(ax, bx) + r + 1)); + const y0 = max(0, floor(min(ay, by) - r - 1)); + const y1 = min(H - 1, ceil(max(ay, by) + r + 1)); + const dx = bx - ax; + const dy = by - ay; + const ll = dx * dx + dy * dy || 1e-6; + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const t = cl(((x - ax) * dx + (y - ay) * dy) / ll, 0, 1); + const ex = x - ax - dx * t; + const ey = y - ay - dy * t; + const a = cl(r + 0.5 - sqrt(ex * ex + ey * ey), 0, 1); + if (a <= 0) continue; + const q = (y * W + x) * 3; + const s = sp ? 0.72 + 0.56 * Math.random() : 1; + F[q] += (c[0] * L[q] * s - F[q]) * a; + F[q + 1] += (c[1] * L[q + 1] * s - F[q + 1]) * a; + F[q + 2] += (c[2] * L[q + 2] * s - F[q + 2]) * a; + } +} + +function glow(S, x, y, r, c, k) { + const F = S.F; + for (let yy = max(0, floor(y - r)); yy <= min(H - 1, ceil(y + r)); yy++) + for (let xx = max(0, floor(x - r)); xx <= min(W - 1, ceil(x + r)); xx++) { + const g = k * exp(-((xx - x) ** 2 + (yy - y) ** 2) / (0.25 * r * r)); + const q = (yy * W + xx) * 3; + F[q] += c[0] * g; + F[q + 1] += c[1] * g; + F[q + 2] += c[2] * g; + } +} + +// A stick figure: hips at (x, y), body angle ph (0 is head up), facing fs. +function fig(S, f, col) { + const bx = sin(f.ph); + const by = -cos(f.ph); + const nx = cos(f.ph) * f.fs; + const ny = sin(f.ph) * f.fs; + const dv = (a, l) => [(bx * cos(a) + nx * sin(a)) * l, (by * cos(a) + ny * sin(a)) * l]; + const shx = f.x + bx * 3 * SC; + const shy = f.y + by * 3 * SC; + for (const [l, k] of [ + [f.l1, f.k1], + [f.l2, f.k2], + ]) { + const [kx, ky] = dv(PI - l, 1.9 * SC); + const [fx, fy] = dv(PI - l + k, 1.9 * SC); + seg(S, f.x, f.y, f.x + kx, f.y + ky, 0.6, col, 1); + seg(S, f.x + kx, f.y + ky, f.x + kx + fx, f.y + ky + fy, 0.5, col, 1); + } + seg(S, f.x, f.y, shx, shy, 0.8, col, 1); + for (const a of [f.a1, f.a2]) { + let ex = f.hx; + let ey = f.hy; + if (ex === null) { + const [ax, ay] = dv(a, 2.7 * SC); + ex = shx + ax; + ey = shy + ay; + } + seg(S, shx, shy, ex, ey, 0.42, SKIN, 0); + } + seg(S, shx + bx * 1.45 * SC, shy + by * 1.45 * SC, shx + bx * 1.55 * SC, shy + by * 1.55 * SC, 0.75, SKIN, 0); +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.started) return; + const { F, LT, CA, CI, AUD, POOL, P } = S; + const wash = S.wash; + const wc = P[0]; + const hz = S.hz; + const hzA = hz * (0.7 - 0.22 * hz); + const halo = hz * hz * 0.18; + const hl = Math.log(max(1, halo * 400)); + const amb = 0.12 + 0.12 * wash; + const sp = S.sp.map((s, j) => [s.sx, s.sy, s.ux, s.uy, s.D, P[1 + j][0] / 255, P[1 + j][1] / 255, P[1 + j][2] / 255]); + const kc = 0.48 * 255 * wash; + const air = 9 * hz * wash; + for (let y = 0; y < H; y++) { + const pb = POOL[y]; + // The ring below the crowd glows in the house light. + const ring = 50 * sm(117, 104, y) * sm(100, 104, y) * wash; + for (let x = 0; x < W; x++) { + const i = y * W + x; + const q = i * 3; + const ci = CI[i] * kc; + let r = 4 + wc[0] * (air + ring) + CA[q] * wc[0] * ci; + let g = 4 + wc[1] * (air + ring) + CA[q + 1] * wc[1] * ci; + let b = 9 + wc[2] * (air + ring) + CA[q + 2] * wc[2] * ci; + let lr = amb * wc[0]; + let lg = amb * wc[1]; + let lb = amb * wc[2]; + const cv = CI[i] > 0.02 ? 0.9 * pb * sm(0, 0.3, CI[i]) * 255 : 0; + for (let j = 0; j < 3; j++) { + const s = sp[j]; + const vx = x - s[0]; + const vy = y - s[1]; + const al = vx * s[2] + vy * s[3]; + const pp = abs(vx * s[3] - vy * s[2]); + const R = 0.8 + al * TA; + if (pp < R) { + const p = sm(R, R * 0.45, pp); + lr += s[5] * p * 1.2; + lg += s[6] * p * 1.2; + lb += s[7] * p * 1.2; + const fb = (al > s[4] ? 0.8 : 1) * 1.3 / (1 + (al / 110) ** 2); + const k = p * fb * hzA * 255; + r += s[5] * k; + g += s[6] * k; + b += s[7] * k; + // Past the flyer the beam lands on the canvas as a pool. + if (cv > 0 && al > s[4]) { + const c = cv * p; + r += CA[q] * s[5] * c; + g += CA[q + 1] * s[6] * c; + b += CA[q + 2] * s[7] * c; + } + } + if (pp < (R + 9) * hl) { + const h = halo * exp(-pp / (R + 9)) * 255; + r += s[5] * h; + g += s[6] * h; + b += s[7] * h; + } + } + F[q] = r; + F[q + 1] = g; + F[q + 2] = b; + LT[q] = min(1.25, lr); + LT[q + 1] = min(1.25, lg); + LT[q + 2] = min(1.25, lb); + } + } + const t = S.t; + // The bale ring at the peak and its ring of bulbs. + seg(S, 32, 7, 32, 29, 0.7, STEEL, 0); + for (let j = 0; j < 22; j++) { + const a = (TAU * j) / 22; + const x = 32 + 20 * sin(a); + const y = 6.5 + 5 * cos(a); + const ch = (((j / 22 - t * 0.9) % 1) + 1) % 1 < 0.22 ? 1 : 0; + let v = 0.75 + 0.15 * sin(t * 3 + j * 2.1); + v = mix(v, 0.3 + 0.9 * ch, S.drum) + 0.5 * S.pop; + v *= 0.7 + 0.3 * cos(a) * 0.5 + 0.15; + glow(S, x, y, 3.2, [255, 196, 110], 0.42 * v); + const q = (round(y) * W + round(x)) * 3; + F[q] += 200 * v; + F[q + 1] += 160 * v; + F[q + 2] += 90 * v; + } + seg(S, 30, 5, 34, 5, 1.1, [60, 62, 70], 0); + // The rig: a truss under the peak, guy lines, the uprights, the board and the ladder. + seg(S, 3, PFY - 2, 61, PFY - 2, 0.4, STEEL, 0); + seg(S, 3, PFY, 61, PFY, 0.35, STEEL, 0); + for (let x = 5; x < 61; x += 4) seg(S, x, PFY - 2, x + 2, PFY, 0.25, STEEL, 0); + seg(S, 4, PFY - 2, 24, 7, 0.25, ROPE, 0); + seg(S, 60, PFY - 2, 40, 7, 0.25, ROPE, 0); + seg(S, 62, PFY - 2, 62, 103, 0.35, ROPE, 0); + seg(S, 0, PFY - 2, 0, BY, 0.35, ROPE, 0); + const [hbx] = barAt(-S.rd.A); + seg(S, 0, BY + 0.6, hbx + 0.4, BY + 0.6, 0.55, [190, 160, 120], 0); + seg(S, 1, BY + 1, 1, 97, 0.3, ROPE, 0); + seg(S, 4.2, BY + 1, 4.2, 97, 0.3, ROPE, 0); + for (let y = BY + 3; y < 97; y += 3) seg(S, 1, y, 4.2, y, 0.25, ROPE, 0); + // The safety net, sagging and dented where a flyer lands in it. + const nt = S.net; + for (let x = 0; x < W; x++) { + const dn = nt.s * exp(-(((x - nt.x) / 8) ** 2)); + const far = 94.5 + 1.2 * sin((PI * x) / 63) + 0.6 * dn; + const near = 103 + 2 * sin((PI * x) / 63) + dn; + for (let y = floor(far - 1); y <= ceil(near + 1); y++) { + if (y < 0 || y >= H) continue; + const v = (y - far) / (near - far); + const fa = x * 0.34 + v * 3; + const fb = x * 0.34 - v * 3; + let ln = max(sm(0.3, 0.48, abs((fa % 1) - 0.5)), sm(0.3, 0.48, abs((((fb % 1) + 1) % 1) - 0.5))); + ln *= v > -0.06 && v < 1.06 ? 1 : 0; + ln = max(ln, sm(0.9, 0.2, abs(y - far)), sm(0.9, 0.2, abs(y - near))); + if (ln <= 0) continue; + const q = (y * W + x) * 3; + F[q] += (205 * LT[q] - F[q]) * ln * 0.8; + F[q + 1] += (196 * LT[q + 1] - F[q + 1]) * ln * 0.8; + F[q + 2] += (176 * LT[q + 2] - F[q + 2]) * ln * 0.8; + } + } + // The fly bar on its ropes. + const [bx, by] = barAt(S.th); + seg(S, PFX - 1.2, PFY, bx - 1.2, by, 0.28, ROPE, 0); + seg(S, PFX + 1.2, PFY, bx + 1.2, by, 0.28, ROPE, 0); + seg(S, bx - 1.8, by, bx + 1.8, by, 0.45, [215, 215, 225], 0); + // The catcher's cradle and the catcher. + const c = S.cp; + seg(S, PCX - 1.2, PCY, c.bx - 1.2, c.by, 0.28, ROPE, 0); + seg(S, PCX + 1.2, PCY, c.bx + 1.2, c.by, 0.28, ROPE, 0); + seg(S, c.bx - 1.8, c.by, c.bx + 1.8, c.by, 0.45, [215, 215, 225], 0); + fig(S, { x: c.x, y: c.y, ph: c.ph, fs: 1, l1: 0, l2: 0, k1: 1.5, k2: 1.3, a1: 0, a2: 0, hx: c.hx, hy: c.hy }, P[6]); + for (const f of S.fl) fig(S, f, P[f.c]); + // The crowd in front of it all, a dark sea with phones and camera flashes. + // Each head is rimmed by the light behind it. + for (let i = W; i < N; i++) { + const a = AUD[i]; + if (a <= 0) continue; + const q = i * 3; + const k = S.ROW[i]; + const rim = k > S.ROW[i - W] ? (1 - 0.2 * k) * a : 0; + for (let c = 0; c < 3; c++) F[q + c] += ((12 - 2.5 * k) * wc[c] - F[q + c]) * a + rim * (16 * wash * wc[c] + 60 * max(0, LT[q + c] - 0.3)); + } + for (const [x, y] of S.phones) { + if (x < 0 || x > W - 2 || y < 0 || y >= H) continue; + const q = (y * W + x) * 3; + F[q] += 70; + F[q + 1] += 92; + F[q + 2] += 130; + F[q + 3] += 70; + F[q + 4] += 92; + F[q + 5] += 130; + } + for (const e of S.fx) glow(S, e.x, e.y, 2.2, [255, 250, 240], e.a * e.a * (1 - e.w)); + 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 41a9125030ec3593ef9415f9c34e6ff353690819 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:33:05 +0000 Subject: [PATCH 17/33] potters-wheel: softer rings, taller pots, three shelves, dust takes the clay --- .../pattern-study/patterns/potters-wheel.js | 92 ++++++++----------- 1 file changed, 40 insertions(+), 52 deletions(-) diff --git a/incoming/pattern-study/patterns/potters-wheel.js b/incoming/pattern-study/patterns/potters-wheel.js index d8ab9a63d..ba7fc6bed 100644 --- a/incoming/pattern-study/patterns/potters-wheel.js +++ b/incoming/pattern-study/patterns/potters-wheel.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Potter's Wheel // Family: pottery -// Idea: At night under one warm lamp a potter centres a lump of wet clay on the spinning wheel, opens it and pulls the wall up into a tall pot whose throwing rings catch the light, wires it off the wheel and sets it on the shelves, where the earlier pots dry from dark to bone-pale. +// Idea: At night under one warm lamp a potter centres wet clay on the spinning wheel, opens it and pulls the wall up into a tall pot whose throwing rings catch the light, wires it off and sets it on the shelves, where the earlier pots dry from dark to bone-pale. // Author: patternflow-lab // Date: 2026-10-24 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -11,9 +11,9 @@ // @matrix 64x128 // @knobs Clay=0..1, Speed=0.2..3, Pots=6..20, Lamp=0..1 // -// Knob 1: Clay. The clay body: terracotta at 0, then buff stoneware, grey porcelain and black clay at 1. The pot on the wheel, the slip in the splash pan and the pots drying on the shelves all take it. -// Knob 2: Speed. At 2 a throw takes about fourteen seconds from the slapped lump to the pot set on the shelf, and the wheel turns about once a second; at 0.2 it all runs ten times slower. -// Knob 3: Pots. How many earlier pieces stand drying on the three shelves of the rack, from six to twenty. The oldest are bone-dry, and each new pot takes the place of the oldest, which goes to the kiln. +// Knob 1: Clay. The clay body: terracotta at 0, then buff stoneware, grey porcelain and black clay at 1. The pot, the slip in the splash pan, the pots drying on the shelves and the dust on the wall all take it. +// Knob 2: Speed. At 2 a throw takes about thirteen seconds from the slapped lump to the pot set on the shelf, and the wheel turns about once a second; at 0.2 it all runs ten times slower. +// Knob 3: Pots. How many earlier pieces stand drying on the rack's three shelves, from six to twenty. The oldest are bone-dry, and each new pot takes the place of the oldest, which goes to the kiln. // Knob 4: Lamp. Where the lamp hangs, from left of the wheel at 0 to right of it at 1. The lit side of the pot, the highlight down its throwing rings, its shadow on the wall and the light on the shelves move with it. // Buttons: K1 tap makes the wall slump, and the potter balls the clay up and centres it again. @@ -24,6 +24,7 @@ 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 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); @@ -40,7 +41,7 @@ const SHELF = [26, 53, 80]; const SLOT = []; for (const sy of SHELF) for (let i = 0; i < 7; i++) SLOT.push([4.6 + i * 9.13, sy]); const ORDER = [1, 5, 8, 12, 14, 20, 3, 9, 11, 0, 6, 7, 13, 15, 19, 2, 4, 10, 16, 18, 17]; -const SHS = 0.25; +const SHS = 0.27; // Each clay thrown and wet, leather-hard, and bone-dry. const CLAY = [ @@ -52,13 +53,16 @@ const CLAY = [ const SKIN = [150, 98, 76], WALL = [84, 62, 50], WOOD = [132, 90, 58], METAL = [150, 152, 156], PLASTIC = [76, 80, 90], SLEEVE = [58, 66, 86], SHINE = [255, 232, 196]; // Pot shapes as a height and the radius at keys up the wall: cylinder, bottle, -// vase with a belly and a flared lip, jar, and trumpet vase. +// vase with a belly and a flared lip, jar and trumpet vase on the wheel, and +// bowls and cups from earlier in the evening on the shelves. const SHAPES = [ (j) => [76 + 8 * j, [[0, 10.5], [0.45, 10.2 + j], [0.9, 9.6], [1, 11]]], (j) => [84 + 8 * j, [[0, 11], [0.3, 14.5], [0.7, 4.2], [0.93, 4], [1, 5.6]]], (j) => [82 + 8 * j, [[0, 8], [0.42, 15], [0.8, 5], [0.9, 5.4], [1, 9.5]]], (j) => [76 + 6 * j, [[0, 8.5], [0.55, 15.5], [0.9, 9], [1, 10.5]]], (j) => [86 + 6 * j, [[0, 9.5], [0.2, 9.8], [0.62, 5.5], [1, 14]]], + (j) => [26 + 6 * j, [[0, 7], [0.45, 14], [1, 19]]], + (j) => [40 + 6 * j, [[0, 9], [0.5, 11.5], [0.85, 10.5], [1, 12]]], ]; function key(K, v) { let i = 0; @@ -66,18 +70,14 @@ function key(K, v) { return mix(K[i][1], K[i + 1][1], sm(K[i][0], K[i + 1][0], v)); } // A radius looked up along a wall kept one entry per row. -const at = (pr, h, Hp) => { - h = h < 0 ? 0 : h > Hp ? Hp : h; - const i = h | 0; - return pr[i] + (pr[i + 1] - pr[i]) * (h - i); -}; -function newSpec(not) { +const at = (pr, h, Hp, i) => ((h = cl(h, 0, Hp)), (i = h | 0), pr[i] + (pr[i + 1] - pr[i]) * (h - i)); +function newSpec(not, all) { let type; - // The first throw of the night is always one of the three tall ones. - do type = floor(rnd() * SHAPES.length); + // The first throw of the night is always one of the three tallest. + do type = floor(rnd() * (all ? 7 : 5)); while (type === not || (not < -1 && type % 3 === 0)); const [Hf, K0] = SHAPES[type](rnd()), g = 0.92 + 0.16 * rnd(), K = K0.map(([v, r]) => [v, r * g]); - const pr = new Float32Array(ceil(Hf) + 3); + const pr = F32(ceil(Hf) + 3); for (let h = 0; h < pr.length; h++) pr[h] = key(K, min(1, h / Hf)) + 1.4 * exp(-h / 2.2); // Where the hands shape it: the belly, then the neck above. let vb = 0.5, vn = 0.85, rb = 0, rn = 99; @@ -90,7 +90,7 @@ function newSpec(not) { // wheel turns through it (turns a second): slapped down, centred, opened, // three pulls, shaped, shown, wired off, lifted to the shelf, and the slump // and the balling up after a K1 tap. -const DUR = [0.45, 1, 0.55, 4.5, 2.6, 1.6, 0.9, 1.8, 1.2, 0.9]; +const DUR = [0.45, 0.9, 0.5, 4.5, 2.6, 1.6, 0.9, 1.8, 1.2, 0.9]; const SPIN = [0.4, 1.3, 1.1, 0.95, 0.75, 0.65, 0.3, 0, 0.9, 1]; // The cylinder after each pull, and its wall at the rim. const RC = [12.5, 11.3, 10.5, 9.9], WALL_T = [4, 2.7, 2.1, 1.7]; @@ -99,13 +99,13 @@ const dome = (v, r) => r * sqrt(max(0, 1 - v ** 2.4)) + 1.2 * exp(-v * 8); export function setup(params) { const P = (params.s = { - t: 0, ph: 0, pt: 0, rot: 0, w: 0.4, H: 11, ri: 0, wa: 0, wt: 0, yo: 0, sH: 11, sri: 0, n: 6, LX: 12, clay: 0, - pr: new Float32Array(128), snap: new Float32Array(128), f: new Float32Array(N * 3), bg: new Float32Array(N * 3), + ph: 0, pt: 0, rot: 0, w: 0.4, H: 11, ri: 0, wa: 0, wt: 0, yo: 0, sH: 11, sri: 0, n: 6, LX: 12, clay: 0, + pr: F32(128), snap: F32(128), f: F32(N * 3), bg: F32(N * 3), hd: null, hs: null, g: null, wire: -1, pieces: [], sp: [], dr: [], free: 0, bgKey: -1, }); P.spec = newSpec(-2); // The pieces already on the shelves, newest first. - for (let i = 0; i < 21; i++) P.pieces.push({ slot: ORDER[i], spec: newSpec(i ? P.pieces[i - 1].spec.type : -1), dry: min(1, 0.1 + i * 0.17) }); + for (let i = 0; i < 21; i++) P.pieces.push({ slot: ORDER[i], spec: newSpec(i ? P.pieces[i - 1].spec.type : -1, 1), dry: min(1, 0.1 + i * 0.17) }); // Old slip spattered in the pan. for (let i = 0; i < 60; i++) { const a = rnd() * TAU, r = 0.66 + 0.3 * rnd(); @@ -118,11 +118,8 @@ function go(P, ph) { // The oldest piece on show goes to the kiln and the new pot takes its place. if (ph === 7) P.free = P.pieces.splice(min(P.n, P.pieces.length) - 1, 1)[0].slot; if (ph === 0 && P.ph === 7) P.pieces.unshift({ slot: P.free, spec: P.spec, dry: 0 }), (P.spec = newSpec(P.spec.type)); - P.ph = ph; - P.pt = 0; P.snap.set(P.pr); - P.sH = P.H; - P.sri = P.ri; + Object.assign(P, { ph, pt: 0, sH: P.H, sri: P.ri }); } function fill(P, Hn, f) { @@ -136,17 +133,12 @@ const offset = (P, h) => P.wa * sin(P.rot) + P.wt * (cl(h / P.H, 0, 1) ** 1.5) * function shape(P, p) { const sp = P.spec; let hand = null; - P.wa = 0; - P.wt = 0.2; - P.yo = 0; - P.g = null; - P.wire = -1; - P.ring = 0.07; + Object.assign(P, { wa: 0, wt: 0.2, yo: 0, g: null, wire: -1, ring: 0.07 }); switch (P.ph) { case 0: { // Slapped down from the hands, squashing as it lands. const sq = p > 0.6 ? 1 - 0.3 * sin((PI * (p - 0.6)) / 0.4) : 1, r = 10.5 / sqrt(sq), Hn = 11 * sq; - P.yo = p < 0.6 ? -44 * (1 - (p / 0.6) ** 2) : 0; + P.yo = p < 0.6 ? -56 * (1 - (p / 0.6) ** 2) : 0; fill(P, Hn, (h) => dome(h / Hn, r)); (P.ri = 0), (P.wa = 0.8), (P.ring = 0.04); hand = [5, -0.3, 0]; @@ -171,7 +163,7 @@ function shape(P, p) { // Three pulls. The hands squeeze the wall from the base to the rim and // a roll of clay rides up ahead of them. const k = min(2, floor(P.pt / 1.5)), a = (P.pt - k * 1.5) / 1.2, Hp = sp.Hf * 0.93; - const HS = [13, mix(13, Hp, 0.42), mix(13, Hp, 0.74), Hp]; + const HS = [13, mix(13, Hp, 0.55), mix(13, Hp, 0.82), Hp]; if (a <= 1) { const Hn = mix(HS[k], HS[k + 1], sm(0, 1, a)), hh = Hn * min(1, a * 1.04) * 0.95, roll = 1.3 * (1 - sm(0.9, 1, a)); fill(P, Hn, (h) => mix(cyl(k + 1, h), cyl(k, h), sm(hh - 1, hh + 2.5, h)) + roll * exp(-(((h - hh - 2.6) / 2.2) ** 2)) - 0.8 * exp(-(((h - hh) / 1.4) ** 2))); @@ -246,7 +238,6 @@ export function update(dt, input, params) { P.clay = cl(k[0], 0, 1); P.n = round(cl(k[2], 6, 20)); P.LX = mix(9, 55, cl(k[3], 0, 1)); - P.t += d; if (input.btnPressed && input.btnPressed[0] && P.ph >= 1 && P.ph <= 5) go(P, 8); if ((P.pt += d) >= DUR[P.ph]) go(P, [1, 2, 3, 4, 5, 6, 7, 0, 9, 1][P.ph]); P.w += (SPIN[P.ph] - P.w) * min(1, d * 3); @@ -260,9 +251,9 @@ export function update(dt, input, params) { // Slip flung off the wall by the hands, and spattered as the lump lands. const fling = (x, y, sd, n) => { - for (let i = 0; i < n; i++) P.dr.push([x, y, sd * (8 + rnd() * 26), -8 - rnd() * 26, PY - 4 + rnd() * 10]); + while (n--) P.dr.push([x, y, sd * (8 + rnd() * 26), -8 - rnd() * 26, PY - 4 + rnd() * 10]); }; - if (P.hs && (P.ph === 3 || P.ph === 4 || P.ph === 8) && rnd() < d * 3) rnd() < 0.5 ? fling(P.hs[0], P.hs[1], -1, 1) : fling(P.hs[2], P.hs[3], 1, 1); + if (P.hs && (P.ph === 3 || P.ph === 4) && rnd() < d * 3) rnd() < 0.5 ? fling(P.hs[0], P.hs[1], -1, 1) : fling(P.hs[2], P.hs[3], 1, 1); if (landing && P.yo === 0) fling(CX - 9, BY - 2, -1, 3), fling(CX + 9, BY - 2, 1, 3); P.dr = P.dr.filter((q) => { (q[0] += q[2] * d), (q[1] += q[3] * d), (q[3] += 150 * d); @@ -276,7 +267,7 @@ export function update(dt, input, params) { for (const q of P.pieces) q.dry = min(1, q.dry + d / 75); } -// The wall, the rack and the lamp's glow, kept until the lamp moves. +// The wall, the rack and the lamp's glow, kept until the lamp or the clay changes. function backdrop(P) { P.bgKey = bgKey(P); // Clay dust lies on everything in the room and takes the clay's colour. @@ -358,31 +349,28 @@ function solid(o) { } // The earlier pots on the shelves, wet and dark, then leather-hard, then pale. +const STILL = { s: SHS, wa: 0, wt: 0, s1: 0, s2: 0, rot: 0, ring: 0, fl: 0, amb: 0.38 }; function shelves(P, C) { - for (let i = 0; i < min(P.n, P.pieces.length); i++) { + // While a pot is carried over, its slot stands empty. + for (let i = 0; i < min(P.n - (P.ph === 7), P.pieces.length); i++) { const q = P.pieces[i], [sx, sy] = SLOT[q.slot], dr = q.dry, a = dr < 0.45 ? dr / 0.45 : (dr - 0.45) / 0.55; const alb = dr < 0.45 ? C[0].map((v, n) => mix(v * 0.62, C[1][n], a)) : C[1].map((v, n) => mix(v, C[2][n], a)); - solid({ cx: sx, by: sy + 0.5, s: SHS, pr: q.spec.pr, Hp: q.spec.Hf, ri: q.spec.rim - 1.4, wa: 0, wt: 0, s1: 0, s2: 0, rot: 0, alb, spec: 0.6 * (1 - sm(0, 0.4, dr)), L: light(P, sx, sy - 10), ring: 0, fl: 0, amb: 0.3 }); + solid({ ...STILL, cx: sx, by: sy + 0.5, pr: q.spec.pr, Hp: q.spec.Hf, ri: q.spec.rim - 1.4, alb, spec: 0.6 * (1 - sm(0, 0.4, dr)), L: light(P, sx, sy - 10) }); } } const put = (x, y, col, k) => { - x = round(x - 0.5); - y = round(y - 0.5); - if (x >= 0 && x < W && y >= 0 && y < H) for (let c = 0, o = (y * W + x) * 3; c < 3; c++) F[o + c] = col[c] * k; + if ((x = round(x - 0.5)) >= 0 && x < W && (y = round(y - 0.5)) >= 0 && y < H) for (let c = 0, o = (y * W + x) * 3; c < 3; c++) F[o + c] = col[c] * k; }; // The lamp: a cord, an enamel shade and the bulb under it. function lamp(P) { - const LX = P.LX; for (let y = 0; y < 12; y++) - for (let x = max(0, floor(LX - 8)); x <= min(W - 1, LX + 8); x++) { - const dx = x + 0.5 - LX, hw = 1.3 + (y - 3) * 0.85; - if (y < 3 ? abs(dx) < 0.6 : y < 10 ? abs(dx) < hw : abs(dx) < (y === 10 ? 2.4 : 1.2)) { - const o = (y * W + x) * 3, col = y < 3 ? [44, 38, 34] : y === 9 ? [236, 170, 96] : y < 9 ? [38, 72, 62] : y === 10 ? [255, 244, 214] : [255, 226, 168]; - const k = y > 2 && y < 9 ? 0.7 + 0.6 * (1 - (y - 3) / 6) * (dx < 0 ? 1 : 0.6) : 1; - for (let c = 0; c < 3; c++) F[o + c] = col[c] * k; - } + for (let x = max(0, floor(P.LX - 8)); x <= min(W - 1, P.LX + 8); x++) { + const dx = x + 0.5 - P.LX; + if (abs(dx) >= (y < 3 ? 0.6 : y < 10 ? 1.3 + (y - 3) * 0.85 : y < 11 ? 2.4 : 1.2)) continue; + const k = y > 2 && y < 9 ? 0.7 + 0.6 * (1 - (y - 3) / 6) * (dx < 0 ? 1 : 0.6) : 1; + put(x + 0.5, y + 0.5, y < 3 ? [44, 38, 34] : y < 9 ? [38, 72, 62] : [[236, 170, 96], [255, 244, 214], [255, 226, 168]][y - 9], k); } } @@ -401,11 +389,11 @@ function shadow(P) { // The splash pan with its slip, and the wheel head turning in it. function wheel(P, A) { - const L = light(P, CX, PY), [l0, l1, l2, lm] = L, mt = METAL.map((v, n) => mix(v, A[n], 0.7)); + const [l0, l1, l2, lm] = light(P, CX, PY), mt = METAL.map((v, n) => mix(v, A[n], 0.7)), side = (z) => 0.15 + 0.8 * max(0, z * l0 + sqrt(max(0, 1 - z * z)) * l2); for (let y = 100; y < H; y++) for (let x = 0; x < W; x++) { const ex = (x + 0.5 - CX) / PR, ey = (y + 0.5 - PY) / (PR * E), dd = ex * ex + ey * ey, o = (y * W + x) * 3; - const wx = (x + 0.5 - CX) / WR, wy = (y + 0.5 - BY) / (WR * E), wd = wx * wx + wy * wy, side = (z) => 0.15 + 0.8 * max(0, z * l0 + sqrt(max(0, 1 - z * z)) * l2); + const wx = (x + 0.5 - CX) / WR, wy = (y + 0.5 - BY) / (WR * E), wd = wx * wx + wy * wy; let col, k; if (wd < 1) (col = mt), (k = (0.3 + 0.6 * l1) * lm * (0.9 + 0.1 * cos(sqrt(wd) * WR * 1.4))); else if (wy > 0 && wx * wx < 1 && y + 0.5 < BY + WR * E * sqrt(1 - wx * wx) + 2.5) (col = METAL), (k = 0.5 * side(wx) * lm); @@ -440,8 +428,8 @@ function cap(ax, ay, bx, by, ra, rb, col, L, tip) { } } -// A hand on the clay at x, y: the forearm down to the side, the palm, three -// fingers round the wall with slip on their tips, and the thumb. +// A hand on the clay at x, y: the sleeve and forearm down to the side, the +// back of the hand, the fingers round the wall with slip on them, the thumb. function hand(P, sd, x, y, slip) { const L = light(P, x, y); cap(x + sd * 22, y + 26, x + sd * 46, y + 62, 5.6, 6.6, SLEEVE, L); From 72a2f5c32cd2b088b89061d3c2e6169f8a9b1be1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:38:49 +0000 Subject: [PATCH 18/33] waggle-dance: dim comb with lit bees, dance floor on the lower comb, under 25 KB --- .../pattern-study/patterns/waggle-dance.js | 324 ++++++++---------- 1 file changed, 134 insertions(+), 190 deletions(-) diff --git a/incoming/pattern-study/patterns/waggle-dance.js b/incoming/pattern-study/patterns/waggle-dance.js index 7d8d554cc..919a7ebf1 100644 --- a/incoming/pattern-study/patterns/waggle-dance.js +++ b/incoming/pattern-study/patterns/waggle-dance.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Waggle Dance // Family: insect -// Idea: One comb of an observation hive stands behind glass, capped honey across the top, a band of pollen and the brood below, while a hundred bees crawl over it and a forager home from the flowers dances her figure of eight, each waggle run pointing as far from straight up as the flowers lie from the sun, with a ring of followers turning to face her. +// Idea: Bees crawl over one comb of an observation hive while foragers home from the flowers dance figures of eight, each waggle run as far from straight up as the flowers lie from the sun, with a ring of followers turned to face her. // Author: patternflow-lab // Date: 2026-10-24 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -11,51 +11,34 @@ // @matrix 64x128 // @knobs Light=0..1, Speed=0.2..3, Bees=80..240, Sun=-1..1 // -// Knob 1: Light. Daylight from the window at 0, with the sun lying across the glass in four panes; towards 1 the curtain closes and a dim red viewing lamp takes over, which the bees cannot see, so they carry on as before. -// Knob 2: Speed. At 2 a waggle run takes a second and its loop back a little more, and the crawlers walk about a body length a second; at 0.2 everything runs ten times slower. -// Knob 3: Bees. Eighty to 240 bees on the comb; more walk in from the sides when it is turned up, and the comb disappears under them. -// Knob 4: Sun. The sun's bearing against the clover field. At 0.06 the field lies nearly under the sun and the runs point almost straight up; turning it swings every dance round and slides the patch of window sun across the glass. -// Buttons: K1 tap brings a forager home from a rich patch; she comes up from the entrance and dances long and hard, and more followers gather round her. +// Knob 1: Light. Daylight at 0, the window's sun lying across the glass in four panes; towards 1 the curtain closes and a red lamp comes up, which bees cannot see. +// Knob 2: Speed. At 2 a waggle run takes about a second and a bee walks about a body length a second; 0.2 is ten times slower. +// Knob 3: Bees. Eighty to 240; turned up, more walk in from the sides until they hide the comb. +// Knob 4: Sun. The sun's bearing against the clover field: at 0.06 the runs point nearly straight up, and turning it swings every dance and slides the window's sun across the glass. +// Buttons: K1 tap brings a forager home from a rich patch; she runs up from the entrance and dances long and hard, with more followers round her. -const W = 64; -const H = 128; +const [W, H] = [64, 128]; const N = W * H; // Cells six LEDs across the flats, rows five apart (a hexagon squashed by // four in a hundred, so every cell is drawn alike); the frame's top bar, // its bottom bar and the entrance slot in it. -const CW = 6; -const RH = 5; -const TOP = 5; -const BOT = 123; +const [CW, RH, TOP, BOT, NC, NR, ENX] = [6, 5, 5, 123, 13, 27, 9]; const Y0 = TOP + 2.5; -const NC = 13; -const NR = 27; -const ENX = 9; const [EMPTY, HONEY, NECTAR, POLLEN, BROOD, LARVA, EGG] = [0, 1, 2, 3, 4, 5, 6]; -const [CRAWL, REST, DIP, FOLLOW, DANCE, HOME, EXIT, LEAVE] = [0, 1, 2, 3, 4, 5, 6, 7]; +const [CRAWL, REST, DIP, FOLLOW, DANCE, HOME, EXIT] = [0, 1, 2, 3, 4, 5, 6]; // Pollen: orange, yellow, ochre, rust, purple-grey, buff, olive, red-orange. -const POLLENS = [ - [0.95, 0.52, 0.08], - [0.95, 0.8, 0.16], - [0.74, 0.54, 0.17], - [0.6, 0.25, 0.1], - [0.44, 0.37, 0.5], - [0.86, 0.68, 0.36], - [0.55, 0.58, 0.24], - [0.9, 0.4, 0.12], -]; +const POLLENS = [[0.95, 0.52, 0.08], [0.95, 0.8, 0.16], [0.74, 0.54, 0.17], [0.6, 0.25, 0.1], + [0.44, 0.37, 0.5], [0.86, 0.68, 0.36], [0.55, 0.58, 0.24], [0.9, 0.4, 0.12]]; // Bees: a dark race, the usual, a golden one. Thorax, then the amber bands. -const THX = [[0.45, 0.33, 0.17], [0.72, 0.5, 0.2], [0.85, 0.62, 0.26]]; +const THX = [[0.5, 0.37, 0.2], [0.78, 0.56, 0.24], [0.9, 0.68, 0.3]]; const AMB = [[0.62, 0.44, 0.18], [1, 0.72, 0.2], [1, 0.84, 0.32]]; -const DARK = [0.07, 0.05, 0.03]; -const HEAD = [0.05, 0.04, 0.03]; -const WING = [0.8, 0.84, 0.9]; -const SUNC = [1, 0.84, 0.56]; -const REDC = [1, 0.09, 0.035]; +const [DARK, HEAD, WING] = [[0.3, 0.18, 0.08], [0.14, 0.1, 0.06], [0.8, 0.84, 0.9]]; +const [SUNC, REDC] = [[1, 0.84, 0.56], [1, 0.09, 0.035]]; // Where the flowers lie, against the main field, and how often foragers come from each. const FIELDS = [0, 0, 0.95, -1.9]; -const { min, max, sin, cos, abs, sqrt, floor, round, exp, atan2, hypot, PI } = Math; +const { min, max, sin, cos, abs, sqrt, floor, round, exp, atan2, PI } = Math; +const len = (a, b) => sqrt(a * a + b * b); const clamp = (v, a, b) => (v < a ? a : v > b ? b : v); const sm = (a, b, v) => ((v = clamp((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); const wrap = (a) => a - 2 * PI * round(a / (2 * PI)); @@ -65,23 +48,18 @@ function hash(a, b) { 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; -} // Distance from a cell's centre in its own hexagon, 3 on the wall. const hexd = (dx, dy) => max(abs(dx), abs(0.5 * dx + 0.9 * dy), abs(0.5 * dx - 0.9 * dy)); const cellX = (r, c) => CW * (c - 1) + (r & 1 ? 3.5 : 0.5); const cellY = (r) => Y0 + (r - 1) * RH; +const cap = (t) => t === BROOD || t === HONEY; // Which cell each LED sits in, and where in it. function grid(S) { for (let y = TOP; y < BOT; y++) for (let x = 0; x < W; x++) { const r0 = round((y + 0.5 - Y0) / RH) + 1; - let best = 99; - let next = 99; + let [best, next] = [99, 99]; const p = y * W + x; for (let r = max(0, r0 - 1); r <= min(NR - 1, r0 + 1); r++) { const off = r & 1 ? 3.5 : 0.5; @@ -95,9 +73,7 @@ function grid(S) { S.cid2[p] = S.cid[p]; best = d; S.cid[p] = r * NC + c; - S.hd[p] = d; - S.dx[p] = dx; - S.dy[p] = dy; + [S.hd[p], S.dx[p], S.dy[p]] = [d, dx, dy]; } else if (d < next) { next = d; S.cid2[p] = r * NC + c; @@ -115,20 +91,16 @@ function layout(S) { const x = cellX(r, c); const y = cellY(r); const ex = (x - 32) / 33; - const ey = (y - 76) / 31; - const e = sqrt(ex * ex + ey * ey) + 0.18 * (vn(x * 0.09, y * 0.09) - 0.5); + const ey = (y - 70) / 28; + const e = sqrt(ex * ex + ey * ey) + 0.05 * (sin(x * 0.21 + y * 0.13) + sin(y * 0.29 - x * 0.17 + 2)); const u = rnd(); let t; - if (y < 26 || (e > 1.28 && y < 70)) t = u < 0.82 ? HONEY : u < 0.95 ? NECTAR : EMPTY; - else if (e > 1 && y < 80) t = u < 0.62 ? POLLEN : u < 0.78 ? NECTAR : EMPTY; + if (y < 24 || (e > 1.28 && y < 64)) t = u < 0.82 ? HONEY : u < 0.95 ? NECTAR : EMPTY; + else if (e > 1 && y < 72) t = u < 0.62 ? POLLEN : u < 0.78 ? NECTAR : EMPTY; else if (e < 0.72) t = u < 0.72 ? BROOD : u < 0.82 ? LARVA : u < 0.85 ? EGG : EMPTY; else if (e < 1) t = u < 0.34 ? LARVA : u < 0.48 ? EGG : u < 0.64 ? BROOD : u < 0.68 ? POLLEN : EMPTY; else t = u < 0.12 ? POLLEN : u < 0.27 ? NECTAR : EMPTY; - S.type[i] = t; - S.age[i] = rnd(); - S.tint[i] = rnd(); - S.gap[i] = rnd() * 6.283; - S.pol[i] = floor(rnd() * POLLENS.length); + [S.type[i], S.age[i], S.tint[i], S.gap[i], S.pol[i]] = [t, rnd(), rnd(), rnd() * 6.283, floor(rnd() * 8)]; } } @@ -142,25 +114,21 @@ function paint(S) { const q = p * 3; let r, g, b; let ex = 1; + let dim = 0.66; if (y < TOP || y >= BOT) { // Pine, the grain running along the bar, a groove where it meets the comb. - const gr = 0.8 + 0.2 * sin(x * 0.23 + 4 * vn(x * 0.06, y * 0.7 + (y < TOP ? 0 : 9))); + const gr = 0.8 + 0.2 * sin(x * 0.23 + 2 * sin(x * 0.07 + y * 1.3)); const edge = y === TOP - 1 || y === BOT ? 0.55 : 1; const k = gr * edge * (0.9 + 0.1 * hash(p, 5)); [r, g, b] = [0.6 * k, 0.42 * k, 0.24 * k]; if (y > BOT + 1 && y < H - 1 && abs(x + 0.5 - ENX) < 5) [r, g, b] = [0.02, 0.015, 0.01]; } else { - const i = S.cid[p]; - const t = S.type[i]; - const d = S.hd[p]; - const dx = S.dx[p]; - const dy = S.dy[p]; - const rr = hypot(dx, dy); - const tn = S.tint[i]; - const lit = -(dx + dy) / 3; + const [i, d, dx, dy] = [S.cid[p], S.hd[p], S.dx[p], S.dy[p]]; + const [t, tn, rr, lit] = [S.type[i], S.tint[i], len(dx, dy), -(dx + dy) / 3]; if (t === HONEY) { + dim = 1; const k = (1 - 0.12 * (d / 2.6) ** 2 + 0.06 * lit) * (0.95 + 0.06 * hash(p, 1)); - [r, g, b] = [(0.97 - 0.05 * tn) * k, (0.9 - 0.16 * tn) * k, (0.66 - 0.28 * tn) * k]; + [r, g, b] = [(1 - 0.04 * tn) * k, (0.88 - 0.16 * tn) * k, (0.6 - 0.26 * tn) * k]; } else if (t === NECTAR) { // Nectar: dark amber down in the cell, a bright meniscus round it, and a glint. const f = (0.08 + 0.92 * sm(1.2, 2.3, d)) * (0.75 + 0.3 * tn); @@ -169,8 +137,9 @@ function paint(S) { ex = 0.6; } else if (t === POLLEN) { const c = POLLENS[S.pol[i]]; + dim = 0.8; const k = (0.8 + 0.25 * tn) * (1 - 0.35 * sm(1.6, 2.5, d)); - [r, g, b] = [c[0] * k * (0.82 + 0.3 * hash(p, 2)), c[1] * k * (0.82 + 0.3 * hash(p, 3)), c[2] * k * (0.82 + 0.3 * hash(p, 4))]; + [r, g, b] = c.map((v, n) => v * k * (0.82 + 0.3 * hash(p, 2 + n))); ex = 0.8; } else if (t === BROOD) { const k = (1 - 0.32 * (rr / 2.7) ** 2 + 0.1 * lit) * (hash(p, 6) < 0.14 ? 0.78 : 1) * (0.9 + 0.15 * tn); @@ -184,8 +153,7 @@ function paint(S) { if (t === LARVA) { // The larva curled in a C on its bed of jelly, bigger as it grows. const a = S.age[i]; - const R = 0.4 + 1.1 * a; - const th = 0.45 + 0.55 * a; + const [R, th] = [0.4 + 1.1 * a, 0.45 + 0.55 * a]; const ang = wrap(atan2(dy, dx) - S.gap[i]); let on = a < 0.3 ? sm(1.2, 0.5, rr) : sm(th + 0.3, th - 0.3, abs(rr - R)) * sm(0.5, 1, abs(ang)); if (rr < 1.9) [r, g, b] = [r + 0.18, g + 0.17, b + 0.14]; @@ -202,30 +170,25 @@ function paint(S) { if (w > 0) { const t2 = S.type[S.cid2[p]]; const pale = t === HONEY || t === NECTAR ? 1 : 0; - const seam = (t === BROOD || t2 === BROOD) && (t === BROOD || t === HONEY) && (t2 === BROOD || t2 === HONEY) ? 0.15 : 1; + const seam = (t === BROOD || t2 === BROOD) && cap(t) && cap(t2) ? 0.15 : 1; const k = (0.85 + 0.3 * hash(p, 8)) * seam; - const wr = (0.34 + 0.18 * pale) * k; - const wg = (0.24 + 0.15 * pale) * k; - const wb = (0.11 + 0.08 * pale) * k; - r += (wr - r) * w; - g += (wg - g) * w; - b += (wb - b) * w; + r += ((0.34 + 0.18 * pale) * k - r) * w; + g += ((0.24 + 0.15 * pale) * k - g) * w; + b += ((0.11 + 0.08 * pale) * k - b) * w; ex += ((seam < 1 ? 0.15 : 1) - ex) * w; } } // The comb sits back in the hive's shade; the bees on it catch the light. - base[q] = r * 0.66; - base[q + 1] = g * 0.66; - base[q + 2] = b * 0.66; + base[q] = r * dim; + base[q + 1] = g * dim; + base[q + 2] = b * dim; expo[p] = ex; } } // The window throws its four panes of sun on the glass; they slide and lean with the sun. function sunPatch(S, s) { - const cx = 32 + 22 * s; - const cy = 74 - 6 * abs(s); - const sh = -0.5 * s; + const [cx, cy, sh] = [32 + 22 * s, 74 - 6 * abs(s), -0.5 * s]; for (let p = 0; p < N; p++) { const v = ((p / W) | 0) + 0.5 - cy; const u = (p % W) + 0.5 - cx - sh * v; @@ -239,21 +202,23 @@ function sunPatch(S, s) { function newBee(S, x, y, h) { const u = rnd(); - const b = { x, y, h, m: CRAWL, t: rnd() * 3, v: 2.6 + 2.6 * rnd(), s: 1.12 + 0.14 * rnd(), tone: u < 0.18 ? 0 : u < 0.82 ? 1 : 2, ph: rnd() * 6.28, w: 0, wag: 0, wp: 0, D: null }; + const b = { x, y, h, m: CRAWL, t: rnd() * 3, v: 2.6 + 2.6 * rnd(), s: 1.12 + 0.14 * rnd(), tone: u < 0.18 ? 0 : u < 0.82 ? 1 : 2, ph: rnd() * 6.28, w: 0, wag: 0, wp: 0 }; S.bees.push(b); return b; } // A bee to the dance floor on the lower comb, clear of the other dances. function spot(S) { - let x = 32; - let y = 90; - for (let k = 0; k < 30; k++) { - x = 16 + 32 * rnd(); - y = 84 + 28 * rnd(); - if (S.dances.every((D) => hypot(D.cx - x, D.cy - y) > 26)) break; + let best = [32, 104]; + let far = -1; + for (let k = 0; k < 24; k++) { + const x = 12 + 40 * rnd(); + const y = 80 + 33 * rnd(); + // The lower the better, near the entrance, but well clear of the others. + const d = min(24, ...S.dances.map((D) => len(D.cx - x, D.cy - y))) + 0.1 * y; + if (d > far) [far, best] = [d, [x, y]]; } - return [x, y]; + return best; } function newDance(S, b, vig) { @@ -268,7 +233,7 @@ function newDance(S, b, vig) { n: vig ? 26 : 9 + floor(10 * rnd()), amp: vig ? 1 : 0.65 + 0.2 * rnd(), want: vig ? 10 : 6 + floor(3 * rnd()), - on: false, foll: [], rec: 0, + on: false, foll: [], rec: 0, pc: floor(rnd() * 3), }; D.th = D.fl - S.sunA; b.D = D; @@ -278,8 +243,8 @@ function newDance(S, b, vig) { // The bees nearest the dance turn and gather round it. function recruit(S, D, snap) { - const free = S.bees.filter((b) => (b.m === CRAWL || b.m === REST) && hypot(b.x - D.cx, b.y - D.cy) < 24); - free.sort((a, b) => hypot(a.x - D.cx, a.y - D.cy) - hypot(b.x - D.cx, b.y - D.cy)); + const free = S.bees.filter((b) => (b.m === CRAWL || b.m === REST) && len(b.x - D.cx, b.y - D.cy) < 24); + free.sort((a, b) => len(a.x - D.cx, a.y - D.cy) - len(b.x - D.cx, b.y - D.cy)); for (const b of free) { if (D.foll.length >= D.want) break; b.m = FOLLOW; @@ -288,7 +253,7 @@ function recruit(S, D, snap) { b.t = 5 + 10 * rnd(); D.foll.push(b); if (snap) { - const R = D.L / 2 + 3.8; + const R = D.L / 2 + 3.4; b.x = D.cx + R * sin(b.a); b.y = D.cy - R * cos(b.a); b.h = b.a + PI; @@ -297,9 +262,8 @@ function recruit(S, D, snap) { } function seek(b, tx, ty, sp, dt) { - const dx = tx - b.x; - const dy = ty - b.y; - const r = hypot(dx, dy); + const [dx, dy] = [tx - b.x, ty - b.y]; + const r = len(dx, dy); if (r > 0.25) { const want = atan2(dx, -dy); const e = wrap(want - b.h); @@ -314,12 +278,11 @@ function seek(b, tx, ty, sp, dt) { function stepDance(S, D, dt) { const b = D.b; D.th += clamp(wrap(D.fl - S.sunA - D.th), -1.5 * dt, 1.5 * dt); - const ux = sin(D.th); - const uy = -cos(D.th); + const [ux, uy] = [sin(D.th), -cos(D.th)]; if (!D.on) { // The forager hurries up from the entrance to where she will dance. b.m = HOME; - if (seek(b, D.cx - (ux * D.L) / 2, D.cy - (uy * D.L) / 2, 9, dt) < 1.2) { + if (seek(b, D.cx - (ux * D.L) / 2, D.cy - (uy * D.L) / 2, D.n > 20 ? 13 : 9, dt) < 1.2) { D.on = true; b.m = DANCE; recruit(S, D, false); @@ -353,8 +316,7 @@ function stepDance(S, D, dt) { } else { const s = (D.tau - D.tw) / D.tr; const ps = D.side * PI * s; - const rx = ux * cos(ps) - uy * sin(ps); - const ry = ux * sin(ps) + uy * cos(ps); + const [rx, ry] = [ux * cos(ps) - uy * sin(ps), ux * sin(ps) + uy * cos(ps)]; b.x = D.cx + (rx * D.L) / 2; b.y = D.cy + (ry * D.L) / 2; want = atan2(-D.side * ry, -D.side * rx); @@ -371,7 +333,7 @@ function stepDance(S, D, dt) { const da = atan2(b.x - D.cx, -(b.y - D.cy)); for (const f of D.foll) { f.a += dt * 0.35 * sin(wrap(da - f.a)); - const R = D.L / 2 + 3.8 + 0.4 * sin(S.t * 2 + f.ph); + const R = D.L / 2 + 3.4 + 0.4 * sin(S.t * 2 + f.ph); if (seek(f, D.cx + R * sin(f.a), D.cy - R * cos(f.a), 6, dt) < 1.2) f.h += clamp(wrap(atan2(b.x - f.x, -(b.y - f.y)) - f.h), -6 * dt, 6 * dt); if ((f.t -= dt) <= 0) { f.m = CRAWL; @@ -387,20 +349,9 @@ function crawl(S, b, dt) { let want = null; const ex = b.x < 4 ? 1 : b.x > W - 4 ? -1 : 0; const ey = b.y < TOP + 4 ? 1 : b.y > BOT - 4 ? -1 : 0; - if (ex || ey) want = atan2(ex + (ey ? 0 : 0.01 * (32 - b.x)), -ey); + if (ex || ey) want = atan2(ex, -ey); b.h += b.w * dt; if (want !== null) b.h += clamp(wrap(want - b.h), -3 * dt, 3 * dt); - // Clear of the dances, unless following one. - for (const D of S.dances) { - const dx = b.x - D.cx; - const dy = b.y - D.cy; - const r = hypot(dx, dy); - if (D.on && r < D.L / 2 + 3) { - b.x += (dx / (r + 0.01)) * 3 * dt; - b.y += (dy / (r + 0.01)) * 3 * dt; - b.h += clamp(wrap(atan2(dx, -dy) - b.h), -3 * dt, 3 * dt); - } - } const st = b.v * dt; b.x += sin(b.h) * st; b.y -= cos(b.h) * st; @@ -415,12 +366,16 @@ function step(S, dt) { if (seek(b, ENX, BOT + 1.5, 7, dt) < 1.5) b.out = true; continue; } - if (m === LEAVE) { - b.h += clamp(wrap((b.x < 32 ? -PI / 2 : PI / 2) - b.h), -3 * dt, 3 * dt); - b.x += sin(b.h) * b.v * dt; - b.y -= cos(b.h) * b.v * dt; - if (b.x < -5 || b.x > W + 5) b.out = true; - continue; + // Others give the dancer and her followers room. + for (const D of S.dances) { + const [dx, dy] = [b.x - D.cx, b.y - D.cy]; + const r = len(dx, dy); + if (D.on && r < D.L / 2 + 9) { + if (m !== CRAWL) b.t = 0; + b.x += (dx / (r + 0.01)) * 4 * dt; + b.y += (dy / (r + 0.01)) * 4 * dt; + b.h += clamp(wrap(atan2(dx, -dy) - b.h), -4 * dt, 4 * dt); + } } if (m === CRAWL) crawl(S, b, dt); else if (m === REST) b.h += sin(S.t * 6 + b.ph) * 0.5 * dt; @@ -445,10 +400,8 @@ function step(S, dt) { const t = S.type[i]; if (t !== HONEY && t !== BROOD) { const r = floor(i / NC); - const c = i % NC; - b.m = DIP; - b.t = 2 + 4 * rnd(); - b.x = cellX(r, c) - sin(b.h) * 1.5; + [b.m, b.t] = [DIP, 2 + 4 * rnd()]; + b.x = cellX(r, i % NC) - sin(b.h) * 1.5; b.y = cellY(r) + cos(b.h) * 1.5; } } @@ -458,52 +411,36 @@ function step(S, dt) { S.dances = S.dances.filter((D) => !D.gone); for (const b of bees) if (b.D && (b.D.gone || (b.m !== FOLLOW && b.m !== DANCE && b.m !== HOME))) b.D = null; // Bees shoulder each other apart; those still at a cell or dancing stand firm. - const G = S.grid; - for (const k of G) k.length = 0; - for (const b of bees) { - const gx = clamp(floor(b.x / 8), 0, 7); - const gy = clamp(floor(b.y / 8), 0, 15); - G[gy * 8 + gx].push(b); - } for (const a of bees) { if (a.m === REST || a.m === DIP || a.m === DANCE) continue; - const gx = clamp(floor(a.x / 8), 0, 7); - const gy = clamp(floor(a.y / 8), 0, 15); - for (let j = max(0, gy - 1); j <= min(15, gy + 1); j++) - for (let i = max(0, gx - 1); i <= min(7, gx + 1); i++) - for (const b of G[j * 8 + i]) { - if (b === a) continue; - const dx = a.x - b.x; - const dy = a.y - b.y; - const r = hypot(dx, dy); - if (r < 3.6 && r > 0.01) { - const k = (3.6 - r) * 0.25; - a.x += (dx / r) * k; - a.y += (dy / r) * k; - } - } + for (const b of bees) { + const dx = a.x - b.x; + const dy = a.y - b.y; + if (abs(dx) > 3.6 || abs(dy) > 3.6 || b === a) continue; + const r = len(dx, dy); + if (r < 3.6 && r > 0.01) { + a.x += (dx / r) * (3.6 - r) * 0.25; + a.y += (dy / r) * (3.6 - r) * 0.25; + } + } } S.bees = bees.filter((b) => !b.out); } export function setup(params) { - const S = { - t: 0, started: false, sunA: 0, sunK: 9, next: 6, add: 0, - cid: new Int16Array(N), cid2: new Int16Array(N), hd: new Float32Array(N), dx: new Float32Array(N), dy: new Float32Array(N), - type: new Uint8Array(NR * NC), age: new Float32Array(NR * NC), tint: new Float32Array(NR * NC), gap: new Float32Array(NR * NC), pol: new Uint8Array(NR * NC), - base: new Float32Array(N * 3), expo: new Float32Array(N), A: new Float32Array(N * 3), X: new Float32Array(N), - amb: new Float32Array(N), pool: new Float32Array(N), sun: new Float32Array(N), jit: new Float32Array(N * 3), - bees: [], dances: [], grid: Array.from({ length: 128 }, () => []), - c0: 4.2, c1: 8.5, cd: 0.7, - }; + const S = { t: 0, started: false, sunA: 0, sunK: 9, next: 6, add: 0, c0: 4.2, c1: 8.5, cd: 0.7, bees: [], dances: [] }; + for (const k of ["cid", "cid2"]) S[k] = new Int16Array(N); + for (const k of ["hd", "dx", "dy", "expo", "X", "amb", "pool", "sun"]) S[k] = new Float32Array(N); + for (const k of ["base", "A", "jit"]) S[k] = new Float32Array(N * 3); + for (const k of ["age", "tint", "gap"]) S[k] = new Float32Array(NR * NC); + for (const k of ["type", "pol"]) S[k] = new Uint8Array(NR * NC); grid(S); layout(S); paint(S); for (let p = 0; p < N; p++) { - const x = p % W; - const y = (p / W) | 0; - S.amb[p] = 0.84 + 0.12 * (1 - y / H) + 0.1 * (vn(x * 0.05, y * 0.04) - 0.5) - 0.12 * ((x - 31.5) / 32) ** 4; - S.pool[p] = 0.4 + 0.65 * exp(-((x - 42) ** 2 + (y - 46) ** 2) / 2600); + const [x, y] = [p % W, (p / W) | 0]; + S.amb[p] = 0.74 + 0.2 * (1 - y / H) + 0.03 * sin(x * 0.11 + y * 0.05) - 0.12 * ((x - 31.5) / 32) ** 4; + S.pool[p] = 0.42 + 0.6 * exp(-((x - 40) ** 2 + (y - 74) ** 2) / 3000); const j = 0.94 + 0.12 * hash(p, 19); for (let c = 0; c < 3; c++) S.jit[p * 3 + c] = j * (0.97 + 0.06 * hash(p, 20 + c)); } @@ -526,8 +463,8 @@ function start(S, n) { const D = newDance(S, b, false); D.fl = k ? 0.08 : -0.1; D.th = D.fl - S.sunA; - D.cx = k ? 22 : 40; - D.cy = k ? 109 : 90; + D.cx = k ? 20 : 43; + D.cy = k ? 111 : 102; D.on = true; b.m = DANCE; D.tau = rnd() * (D.tw + D.tr); @@ -571,7 +508,7 @@ export function update(dt, input, params) { } if (!vig) S.next = 5 + 8 * rnd(); } - // Bees walk in from the sides, or wander off them, to match the knob. + // Bees walk in from the sides, or go out at the entrance, to match the knob. let ord = 0; for (const b of S.bees) if (b.m <= FOLLOW) ord++; if ((S.add -= dt) <= 0) { @@ -581,7 +518,7 @@ export function update(dt, input, params) { newBee(S, l ? -3 : W + 3, TOP + 8 + (BOT - TOP - 16) * rnd(), l ? PI / 2 : -PI / 2).t = 1.5; } else if (ord > want + 2) { const b = S.bees.find((b) => b.m === CRAWL); - if (b) b.m = LEAVE; + if (b) b.m = EXIT; } } step(S, d); @@ -591,52 +528,48 @@ export function update(dt, input, params) { // then her shadow a step down and right, head, thorax, banded abdomen, wings. const PC = new Float32Array(144); const PP = new Uint8Array(144); -const PU = new Float32Array(144); -const PV = new Float32Array(144); function bee(S, b) { const { A, X } = S; - const s = b.s; - const fx = sin(b.h); - const fy = -cos(b.h); - const dip = b.m === DIP; - const wg = b.wag > 0; + const [s, fx, fy, dip, wg] = [b.s, sin(b.h), -cos(b.h), b.m === DIP, b.wag > 0]; const wag = wg ? b.wag * sin(b.wp) : b.m === REST ? 0.15 * sin(S.t * 9 + b.ph) : 0; - const bl = wg ? 0.4 : 0; - const th = THX[b.tone]; - const am = AMB[b.tone]; + const [bl, th, am] = [wg ? 0.4 : 0, THX[b.tone], AMB[b.tone]]; const shim = 0.24 + 0.14 * sin(S.t * 2.3 + b.ph + b.x * 0.3); - const x0 = floor(b.x) - 5; - const y0 = floor(b.y) - 5; - for (let j = 0, k = 0; j < 12; j++) - for (let i = 0; i < 12; i++, k++) { + // A box round her, the whole patch for a dancer and her blur. + const ax = wg ? 5 : s * (3.2 * abs(fx) + 1.9 * abs(fy)) + 0.5; + const ay = wg ? 5 : s * (3.2 * abs(fy) + 1.9 * abs(fx)) + 0.5; + const [x0, y0] = [floor(b.x - ax), floor(b.y - ay)]; + const nw = min(12, floor(b.x + ax) - x0 + 2); + const nh = min(12, floor(b.y + ay) - y0 + 2); + for (let j = 0, k = 0; j < nh; j++) + for (let i = 0; i < nw; i++, k++) { const X0 = x0 + i + 0.5 - b.x; const Y0 = y0 + j + 0.5 - b.y; const u = (X0 * fx + Y0 * fy) / s; const v = (Y0 * fx - X0 * fy) / s - wag * (0.3 + 0.7 * clamp(-u / 2.5, 0, 1)); PC[k] = shape(u, v, s, dip, bl); PP[k] = PART; - PU[k] = u; - PV[k] = v; } - for (let j = 0, k = 0; j < 12; j++) - for (let i = 0; i < 12; i++, k++) { + for (let j = 0, k = 0; j < nh; j++) + for (let i = 0; i < nw; i++, k++) { const px = x0 + i; const py = y0 + j; + const c = PC[k]; if (px < 0 || px >= W || py < 0 || py >= H) continue; const p = py * W + px; const q = p * 3; - const c = PC[k]; - const sh = i && j ? PC[k - 13] : 0; + const sh = i && j ? PC[k - nw - 1] : 0; if (sh > 0) { const f = 1 - 0.7 * sh * (1 - c); A[q] *= f; A[q + 1] *= f; A[q + 2] *= f; } - const u = PU[k]; - const v = PV[k]; + const X0 = px + 0.5 - b.x; + const Y0 = py + 0.5 - b.y; + const u = (X0 * fx + Y0 * fy) / s; + const v = (Y0 * fx - X0 * fy) / s - wag * (0.3 + 0.7 * clamp(-u / 2.5, 0, 1)); // A waggling dancer buzzes her wings into a pale blur round her. - let wa = wg ? 0.55 * clamp(1.6 - hypot((u + 0.7) / 1.5, v / 1.7), 0, 1) : 0; + let wa = wg ? 0.55 * clamp(1.6 - len((u + 0.7) / 1.5, v / 1.7), 0, 1) : 0; if (c <= 0 && wa <= 0) continue; let r, g, bb; const pt = PP[k]; @@ -658,20 +591,31 @@ function bee(S, b) { A[q + 2] += (bb - A[q + 2]) * a; X[p] += (1 - X[p]) * a; } + // A forager home from the field carries her pollen loads on her hind legs. + if (b.D && (b.m === DANCE || b.m === HOME)) { + const c = POLLENS[b.D.pc]; + for (const sd of [-1, 1]) { + const px = round(b.x + s * (-0.7 * fx - sd * 1.25 * fy) - 0.5); + const py = round(b.y + s * (-0.7 * fy + sd * 1.25 * fx) - 0.5); + if (px < 0 || px >= W || py < 0 || py >= H) continue; + const q = (py * W + px) * 3; + [A[q], A[q + 1], A[q + 2], X[py * W + px]] = [...c, 1]; + } + } } // Coverage of a bee's body at (u along, v across) in body lengths of // about five LEDs; PART says which piece. let PART = 0; function shape(u, v, s, dip, bl) { - let c = clamp((1 - hypot((u + 1.3) / 1.5, v / (1.12 + bl))) * 1.2 * s + 0.5, 0, 1); + let c = clamp((1 - len((u + 1.3) / 1.5, v / (1.12 + bl))) * 1.2 * s + 0.5, 0, 1); PART = 3; if (!dip || u < 0.3) { - const k = clamp((1 - hypot((u - 0.5) / 0.8, v)) * 0.9 * s + 0.5, 0, 1); + const k = clamp((1 - len((u - 0.5) / 0.8, v)) * 0.9 * s + 0.5, 0, 1); if (k > c) [c, PART] = [k, 2]; } if (!dip) { - const k = clamp((1 - hypot((u - 1.65) / 0.6, v / 0.75)) * 0.7 * s + 0.5, 0, 1); + const k = clamp((1 - len((u - 1.65) / 0.6, v / 0.75)) * 0.7 * s + 0.5, 0, 1); if (k > c) [c, PART] = [k, 1]; } return c; @@ -689,10 +633,10 @@ export function draw(display, params) { for (const b of S.bees) if (b.m === DANCE) bee(S, b); // Daylight warms and fades as the curtain closes, the sun goes first, and the red lamp comes up. const L = S.light; - const day = (1 - sm(0.1, 0.85, L)) * 255; - const dc = [day, day * (0.94 - 0.2 * L), day * (0.82 - 0.35 * L)]; - const sk = 0.65 * S.cloud * (1 - sm(0.05, 0.55, L)) * 255; - const red = 0.85 * sm(0.3, 0.95, L) * 255; + const day = (1 - sm(0.05, 0.62, L)) * 255; + const dc = [day, day * (0.94 - 0.4 * L), day * (0.82 - 0.7 * L)]; + const sk = 0.9 * S.cloud * (1 - sm(0, 0.45, L)) * 255; + const red = 0.9 * sm(0.2, 0.72, L) * 255; for (let p = 0; p < N; p++) { const q = p * 3; const a = amb[p]; From 4167326ba2d5e0c40b2200301840a116090cf86a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:40:10 +0000 Subject: [PATCH 19/33] flying-trapeze: circus big top, flyer, catcher, net and follow spots Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KTNzLtP3gH9rQENoucsegc --- .../pattern-study/patterns/flying-trapeze.js | 757 +++++------------- 1 file changed, 214 insertions(+), 543 deletions(-) diff --git a/incoming/pattern-study/patterns/flying-trapeze.js b/incoming/pattern-study/patterns/flying-trapeze.js index 78f143337..989c23aa2 100644 --- a/incoming/pattern-study/patterns/flying-trapeze.js +++ b/incoming/pattern-study/patterns/flying-trapeze.js @@ -12,45 +12,33 @@ // @knobs Lights=0..1, Speed=0.2..3, Turns=1..3, Haze=0..1 // // Knob 1: Lights. The gels in the follow spots and the sequins on the costumes: warm white and gold at 0, amber and rose, then pink and lavender, and at 1 one pink spot and one blue. The house light on the canvas turns with them. -// Knob 2: Speed. How fast the act runs: at 2 a swing of the fly bar takes 2.4 s and a flyer lets go every eight to ten seconds; at 0.2 it all runs ten times slower. +// Knob 2: Speed. How fast the act runs: at 2 the fly bar swings out and back in 2.4 s and a flyer lets go every eight to eleven seconds; at 0.2 it all runs ten times slower. // Knob 3: Turns. The somersaults in the trick, one, two or three. A bigger trick swings higher and flies longer, and the house goes darker for it while every spot finds the flyer. // Knob 4: Haze. How much haze hangs under the canvas. A little shows the beams as thin shafts; turned up they swell and the whole tent glows in the gels. // Buttons: K1 tap starts a drum roll: the next trick is the triple, and it ends in the net. -const W = 64; -const H = 128; +const [W, H] = [64, 128]; const N = W * H; -const { min, max, sin, cos, abs, sqrt, exp, floor, ceil, round, atan2, PI } = Math; +const { min, max, sin, cos, abs, sqrt, exp, floor, ceil, round, atan2, log, PI } = Math; const TAU = 2 * PI; const cl = (v, a, b) => (v < a ? a : v > b ? b : v); -const sm = (a, b, v) => { - const t = cl((v - a) / (b - a), 0, 1); - return t * t * (3 - 2 * t); -}; +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); const mix = (a, b, t) => a + (b - a) * t; const ease = (s) => s - sin(TAU * s) * (0.8 / TAU); -// One swing of the fly bar, in seconds at Speed 2. +const rnd = () => Math.random(); +// A swing of the fly bar out and back, in seconds at Speed 2. const PB = 2.4; const OM = TAU / PB; -// The fly bar's crane and rope, and the catcher's cradle, rope and swing. +// The fly bar's crane and rope; the catcher's cradle, rope and swing. const [PFX, PFY, LF] = [24, 30, 34]; const [PCX, PCY, LC, B] = [57, 30, 20, 0.18]; -// The board's top, where the next flyer waits, and the ladder. +// The board's top, where the next flyer waits on it, and the rope ladder. const [BY, WX, LX] = [68, 1.2, 2.6]; -// Figure scale, hips above the feet, hands above the hips. -const SC = 1; -const LEG = 3.8 * SC; -const HL = 5.7 * SC; -const G = 90; -const TR = 0.45; -// Follow spots, below the panel behind the crowd, and how fast their cones open. -const SPOT = [ - [-14, 166], - [78, 166], - [32, 186], -]; +// Hips above feet, hands above hips, gravity, and the time of the flight back to the bar. +const [LEG, HL, G, TR] = [3.8, 5.7, 90, 0.45]; +// Follow spots below the panel, behind the crowd, and how fast their cones open. +const SPOT = [[-14, 166], [78, 166], [32, 186]]; const TA = 0.046; - // Gels by the Lights knob: house wash (0..1), three spots, two flyers and the catcher. const PAL = [ [[1, 0.8, 0.52], [255, 226, 170], [255, 244, 222], [255, 206, 120], [255, 182, 36], [205, 222, 255], [70, 120, 255]], @@ -58,128 +46,68 @@ const PAL = [ [[0.82, 0.52, 0.86], [255, 122, 196], [186, 142, 255], [255, 166, 214], [255, 62, 196], [160, 104, 255], [110, 236, 214]], [[0.46, 0.44, 1], [255, 76, 170], [66, 128, 255], [172, 96, 255], [255, 44, 156], [36, 150, 255], [245, 245, 255]], ]; -const SKIN = [255, 196, 156]; -const STEEL = [170, 176, 190]; -const ROPE = [150, 138, 116]; - -function palette(S, k) { - const u = cl(k, 0, 1) * 3; - const i = min(2, floor(u)); - const f = u - i; - for (let j = 0; j < 7; j++) for (let c = 0; c < 3; c++) S.P[j][c] = mix(PAL[i][j][c], PAL[i + 1][j][c], f); -} +const [SKIN, STEEL, ROPE, BAR] = [[255, 196, 156], [170, 176, 190], [150, 138, 116], [215, 215, 225]]; +const ONNET = { drop: 1, net: 1, up: 1, tada: 1 }; -const mkF = (c) => ({ m: 'wait', x: WX, y: BY - LEG, ph: 0, fs: 1, a1: 2.9, a2: 2.9, l1: 0, l2: 0, k1: 0, k2: 0, hx: null, hy: 0, T: 0, vx: 0, vy: 0, px: WX, py: BY - LEG, c }); -function rest(f) { - f.l1 = f.l2 = f.k1 = f.k2 = 0; - f.a1 = f.a2 = 2.9; - f.hx = null; -} +const mkF = (c) => ({ m: 'wait', x: WX, y: BY - LEG, ph: 0, fs: 1, a1: 2.9, a2: 2.9, l1: 0, l2: 0, k1: 0, k2: 0, hx: null, T: 0, sx: 0, sy: 0, c }); +// Thighs, knees and arms of a figure, both sides. +const lim = (f, l1, l2, k1, k2, a1, a2) => Object.assign(f, { l1, l2, k1, k2, a1, a2 }); +const rest = (f) => ((f.hx = null), lim(f, 0, 0, 0, 0, 2.9, 2.9)); +const barAt = (th) => [PFX + LF * sin(th), PFY + LF * cos(th)]; +const netY = (x) => 98.5 + 1.6 * sin((PI * cl(x, 0, 63)) / 63); export function setup(params) { + const F32 = (n) => new Float32Array(n); const S = { - t: 0, - started: false, - F: new Float32Array(N * 3), - LT: new Float32Array(N * 3), - CA: new Float32Array(N * 3), - CI: new Float32Array(N), - AUD: new Float32Array(N), - ROW: new Int8Array(N).fill(-1), - POOL: new Float32Array(H), - bar: 'free', - A: 0.55, - rd: null, - phc: 0, - ct: null, - psi: 0, - fl: [mkF(4), mkF(5)], - star: 0, + t: 0, started: false, F: F32(N * 3), LT: F32(N * 3), CA: F32(N * 3), CI: F32(N), AUD: F32(N), ROW: new Int8Array(N).fill(-1), POOL: F32(H), + bar: 'free', A: 0.55, phc: 0, fl: [mkF(4), mkF(5)], star: 0, sp: SPOT.map(([sx, sy]) => ({ sx, sy, ax: 30, ay: 50, ux: 0, uy: -1, D: 110 })), - drum: 0, - drumB: 0, - pop: 0, - wash: 1, - trip: false, - nextFin: true, - turns: 1, - net: { s: 0, v: 0, x: 32 }, - fx: [], - phones: [], - P: PAL[0].map((c) => c.slice()), - hz: 0.3, + drum: 0, drumB: 0, pop: 0, wash: 1, trip: false, nextFin: true, turns: 1, net: { s: 0, v: 0, x: 32 }, fx: [], phones: [], P: PAL[0].map((c) => c.slice()), hz: 0.3, }; // The canvas: red and cream panels running down from the peak, each billowing between its seams. for (let y = 0; y < H; y++) { S.POOL[y] = exp(-(((y - 17) / 15) ** 2)); for (let x = 0; x < W; x++) { - const i = y * W + x; - const dx = x - 32; - const dy = y - 5; - const a = atan2(dx, dy); + const i = y * W + x, dx = x - 32, dy = y - 5, a = atan2(dx, dy); const red = 0.5 + 0.5 * cl(sin(a * 7) * 4, -1, 1); - const bil = 0.84 - 0.16 * cos(a * 14); - const near = sm(2.5, 6, sqrt(dx * dx + dy * dy)); - S.CA[i * 3] = mix(0.95, 0.88, red) * bil * near; - S.CA[i * 3 + 1] = mix(0.86, 0.12, red) * bil * near; - S.CA[i * 3 + 2] = mix(0.68, 0.12, red) * bil * near; + const k = (0.84 - 0.16 * cos(a * 14)) * sm(2.5, 6, sqrt(dx * dx + dy * dy)); + S.CA[i * 3] = mix(0.95, 0.88, red) * k; + S.CA[i * 3 + 1] = mix(0.86, 0.12, red) * k; + S.CA[i * 3 + 2] = mix(0.68, 0.12, red) * k; S.CI[i] = (0.3 + 0.7 * exp(-((dx * dx) / 900 + (dy * dy) / 300))) * sm(40, 20, y); } } - // The crowd: rows of heads and shoulders, bigger towards the front. - const rows = [ - [108.5, 1.9, 4.6], - [114, 2.3, 5.4], - [120.5, 2.8, 6.4], - [127.5, 3.4, 7.6], - ]; - rows.forEach(([base, r, gap], k) => { - for (let cx = -2 + Math.random() * gap; cx < W + 3; cx += gap * (0.8 + 0.4 * Math.random())) { - const hy = base - r * 1.15 + (Math.random() - 0.5) * 1.2; - const hx = cx + (Math.random() - 0.5); + // The crowd: rows of heads and shoulders, bigger towards the front, each row a solid band below. + [[108.5, 1.9, 4.6], [114, 2.3, 5.4], [120.5, 2.8, 6.4], [127.5, 3.4, 7.6]].forEach(([base, r, gap], k) => { + for (let cx = -2 + rnd() * gap; cx < W + 3; cx += gap * (0.8 + 0.4 * rnd())) { + const hy = base - r * 1.15 + (rnd() - 0.5) * 1.2, hx = cx + rnd() - 0.5; for (let y = max(0, floor(hy - r - 1)); y < H; y++) for (let x = max(0, floor(hx - r * 2.4)); x <= min(W - 1, ceil(hx + r * 2.4)); x++) { const hd = r + 0.5 - sqrt((x - hx) ** 2 + (y - hy) ** 2); const sh = 1 - sqrt(((x - hx) / (r * 2.2)) ** 2 + ((y - base) / (r * 0.9)) ** 2); - const c = y >= base ? 1 : cl(max(hd, sh * r * 2), 0, 1); - const i = y * W + x; + const c = y >= base ? 1 : cl(max(hd, sh * r * 2), 0, 1), i = y * W + x; if (c > S.AUD[i]) S.AUD[i] = c; if (c > 0.5) S.ROW[i] = k; } - if (Math.random() < 0.09) S.phones.push([round(hx + 1), round(hy - r - 1)]); + if (rnd() < 0.09) S.phones.push([round(hx + 1), round(hy - r - 1)]); } }); params.s = S; } -// The catcher on his cradle at swing angle psi: knees over the bar, body and arms hanging. +// The catcher at swing angle psi: knees over the bar, body hanging, arms reaching for the flyer. function cpose(psi) { - const ps = psi * 1.2; - const bx = PCX + LC * sin(psi); - const by = PCY + LC * cos(psi); - const sx = sin(ps); - const sy = cos(ps); - const hx = bx + 1.9 * SC * sx; - const hy = by + 1.9 * SC * sy; - const shx = hx + 3 * SC * sx; - const shy = hy + 3 * SC * sy; - // Arms reach down and a little towards the flyer. - const ax = sx * cos(0.2) - cos(ps) * sin(0.2); - const ay = sy * cos(0.2) + sin(ps) * sin(0.2); - return { bx, by, x: hx, y: hy, ph: PI - ps, hx: shx + 2.7 * SC * ax, hy: shy + 2.7 * SC * ay }; + const ps = psi * 1.2, bx = PCX + LC * sin(psi), by = PCY + LC * cos(psi), sx = sin(ps), sy = cos(ps); + const hx = bx + 1.9 * sx, hy = by + 1.9 * sy; + const ax = sx * cos(0.2) - sy * sin(0.2), ay = sy * cos(0.2) + sx * sin(0.2); + return { bx, by, x: hx, y: hy, ph: PI - ps, fs: 1, l1: 0, l2: 0, k1: 1.5, k2: 1.3, hx: hx + 3 * sx + 2.7 * ax, hy: hy + 3 * sy + 2.7 * ay }; } -const barAt = (th) => [PFX + LF * sin(th), PFY + LF * cos(th)]; - function newRound(S, go, who) { - const trip = S.trip; + const trip = S.trip, turns = trip ? 3 : S.turns, fin = trip || S.nextFin, tf = 0.75 + 0.25 * turns, tr = go + 1.5 * PB; S.trip = false; - const turns = trip ? 3 : S.turns; - const fin = trip || S.nextFin; S.nextFin = !fin; - const tf = 0.75 + 0.25 * turns; - const tr = go + 1.5 * PB; - S.rd = { turns, fin, tf, h: 8 + 3 * turns, tg: go, tr, tc: tr + tf, rel: false, A0: S.A, A: 0.55 + 0.03 * (turns - 1), t0: S.t, over: false, cnt: 0 }; + S.rd = { turns, fin, tf, h: 8 + 3 * turns, tg: go, tr, tc: tr + tf, rel: false, A0: S.A, A: 0.55 + 0.03 * (turns - 1), t0: S.t, over: false }; S.star = who; // The catcher times his swing to be out at the front of it when she arrives. const dt = max(0.3, S.rd.tc - S.t); @@ -188,34 +116,23 @@ function newRound(S, go, who) { S.ct = { ph: TAU * k, t: S.rd.tc, ev: 0 }; } -function burst(S, n) { - for (let i = 0; i < n; i++) S.fx.push({ x: 2 + Math.random() * 60, y: 106 + Math.random() * 20, a: 1, w: Math.random() * 0.25 }); -} - -function netY(x) { - return 98.5 + 1.6 * sin((PI * cl(x, 0, 63)) / 63); -} +const burst = (S, n) => { + for (let i = 0; i < n; i++) S.fx.push({ x: 2 + rnd() * 60, y: 106 + rnd() * 20, a: 1, w: rnd() * 0.25 }); +}; function catcherEvent(S, ev) { - const rd = S.rd; - const f = S.fl[S.star]; + const rd = S.rd, f = S.fl[S.star]; + f.T = 0; + f.ph0 = f.ph; if (ev === 0) { + // The catch: the house comes up and the crowd's cameras go off. if (f.m === 'fly') f.m = 'caught'; - S.pop = 1; - burst(S, 7); + burst(S, 7, (S.pop = 1)); S.ct = rd.fin ? { ph: S.phc + 1.5 * PI, t: S.t + 2.4, ev: 2 } : { ph: S.phc + TAU, t: rd.tg + 3.5 * PB - TR, ev: 1 }; - } else if (ev === 1) { - f.m = 'ret'; - f.T = 0; - f.sx = f.x; - f.sy = f.y; - f.ph0 = f.ph; - } else { - f.m = 'drop'; - f.T = 0; - f.ph0 = f.ph; - f.vx = f.vx0; - f.vy = f.vy0; + } else if (ev === 1) [f.m, f.sx, f.sy] = ['ret', f.x, f.y]; + else { + // He lets her go on the swing back, and she falls turning over into the net. + [f.m, f.vx, f.vy] = ['drop', f.vx0, f.vy0]; const dy = netY(f.x + f.vx * 0.9) - 1 - f.y; f.TF = (-f.vy + sqrt(f.vy * f.vy + 2 * G * max(1, dy))) / G; rd.over = true; @@ -223,508 +140,286 @@ function catcherEvent(S, ev) { } function stepFlyer(S, f, d, i) { - const rd = S.rd; - const t = S.t; + const rd = S.rd, t = S.t, th = S.th, [hbx, hby] = barAt(-S.A), [bx, by] = barAt(th), nt = S.net; f.T += d; - const [hbx, hby] = barAt(-S.A); - const th = S.th; - const [bx, by] = barAt(th); switch (f.m) { case 'wait': + case 'hold': { + // Standing on the board; the one whose turn it is steps up and takes the bar. + const h = f.m === 'hold', gx = h ? hbx - 1 : WX; rest(f); - f.fs = 1; - f.x += cl(WX - f.x, -6 * d, 6 * d); - f.y = BY - LEG; - f.ph = 0.04 * sin(t * 1.3 + i); - break; - case 'hold': - rest(f); - f.fs = 1; - f.x += cl(hbx - 1 - f.x, -7 * d, 7 * d); - f.y = BY - LEG; - f.ph = 0.12 * sm(3, 0.5, abs(hbx - 1 - f.x)); - if (abs(hbx - 1 - f.x) < 1.5) { - f.hx = hbx; - f.hy = hby; - } - if (t >= rd.tg) { - S.bar = 'free'; - f.m = 'swing'; - f.T = 0; - f.sx = f.x; - f.sy = f.y; - } + f.x += cl(gx - f.x, -7 * d, 7 * d); + [f.fs, f.y, f.ph] = [1, BY - LEG, h ? 0.12 * sm(3, 0.5, abs(gx - f.x)) : 0.04 * sin(t * 1.3 + i)]; + if (!h) break; + if (abs(gx - f.x) < 1.5) [f.hx, f.hy] = [hbx, hby]; + if (t >= rd.tg) [S.bar, f.m, f.T, f.sx, f.sy] = ['free', 'swing', 0, f.x, f.y]; break; + } case 'swing': case 'ride': { f.fs = f.m === 'ride' ? -1 : 1; - const k = f.m === 'swing' ? sm(0, 0.3, f.T) : 1; - f.x = mix(f.sx, bx + HL * sin(th), k); - f.y = mix(f.sy, by + HL * cos(th), k); - f.ph = -th * k; - const u = (f.fs * th) / S.A; - f.l1 = f.l2 = (0.95 * max(0, u) + 0.35 * min(0, u)) * k; - f.k1 = f.k2 = 0.25 * max(0, -u) * k; - f.hx = bx; - f.hy = by; + const k = f.m === 'swing' ? sm(0, 0.3, f.T) : 1, u = (f.fs * th) / S.A; + [f.x, f.y, f.ph] = [mix(f.sx, bx + HL * sin(th), k), mix(f.sy, by + HL * cos(th), k), -th * k]; + // Legs lead at the front of each swing and trail at the back. + const l = (0.95 * max(0, u) + 0.35 * min(0, u)) * k; + lim(f, l, l, 0.25 * max(0, -u) * k, 0.25 * max(0, -u) * k, f.a1, f.a2); + [f.hx, f.hy] = [bx, by]; if (f.m === 'swing' && !rd.rel && t >= rd.tr) { + // Let go at the top of the swing, aimed at where the catcher's hands will be. const c = cpose(-B); - f.m = 'fly'; rd.rel = true; - f.Rx = f.x; - f.Ry = f.y; - f.Cx = c.hx + HL * sin(-0.8 * B); - f.Cy = c.hy + HL * cos(-0.8 * B); - f.phR = f.ph; - f.pkR = f.l1; - f.hx = null; + Object.assign(f, { m: 'fly', Rx: f.x, Ry: f.y, Cx: c.hx + HL * sin(-0.8 * B), Cy: c.hy + HL * cos(-0.8 * B), phR: f.ph, pkR: f.l1, hx: null }); } if (f.m === 'ride' && t >= rd.tg + 4 * PB) { // Home on the board. The flyer who was waiting takes the bar next. S.bar = 'hook'; rd.over = true; - f.m = 'land'; - f.T = 0; - f.sx = f.x; - f.sy = f.y; + [f.m, f.T, f.sx, f.sy] = ['land', 0, f.x, f.y]; const o = S.fl[1 - i]; - if (o.m === 'wait') { - o.m = 'hold'; - f.next = 'wait'; - newRound(S, t + 1.1, 1 - i); - } else { - f.next = 'hold'; - newRound(S, t + 1.1, i); - } + if (o.m === 'wait') o.m = 'hold'; + f.next = o.m === 'hold' ? 'wait' : 'hold'; + newRound(S, t + 1.1, o.m === 'hold' ? 1 - i : i); } break; } case 'fly': { - const s = cl((t - rd.tr) / rd.tf, 0, 1); - f.x = mix(f.Rx, f.Cx, s); - f.y = mix(f.Ry, f.Cy, s) - 4 * rd.h * s * (1 - s); - f.ph = mix(f.phR, 0.8 * B - TAU * rd.turns, ease(s)); - const u = sm(0.04, 0.22, s) * (1 - sm(0.7, 0.9, s)); - f.l1 = f.l2 = max(f.pkR * (1 - sm(0, 0.12, s)), 2.2 * u); - f.k1 = f.k2 = 2.4 * u; - f.a1 = f.a2 = 2 * u; - f.hx = null; + const s = cl((t - rd.tr) / rd.tf, 0, 1), u = sm(0.04, 0.22, s) * (1 - sm(0.7, 0.9, s)); + [f.x, f.y, f.ph] = [mix(f.Rx, f.Cx, s), mix(f.Ry, f.Cy, s) - 4 * rd.h * s * (1 - s), mix(f.phR, 0.8 * B - TAU * rd.turns, ease(s))]; + const l = max(f.pkR * (1 - sm(0, 0.12, s)), 2.2 * u); + lim(f, l, l, 2.4 * u, 2.4 * u, 2 * u, 2 * u); break; } case 'caught': { - const c = S.cp; - const tp = S.psi * 0.8; - f.x = c.hx + HL * sin(tp); - f.y = c.hy + HL * cos(tp); - f.ph = -tp; - f.fs = 1; - f.l1 = f.l2 = 0.4 * sin(S.phc + 0.6); - f.k1 = f.k2 = 0; - f.hx = c.hx; - f.hy = c.hy; + const c = S.cp, tp = S.psi * 0.8; + [f.x, f.y, f.ph, f.fs, f.hx, f.hy] = [c.hx + HL * sin(tp), c.hy + HL * cos(tp), -tp, 1, c.hx, c.hy]; + lim(f, 0.4 * sin(S.phc + 0.6), 0.4 * sin(S.phc + 0.6), 0, 0, 0, 0); break; } case 'ret': { // A half turn in the air and back to the fly bar, which comes out to meet her. - const s = cl(f.T / TR, 0, 1); - const [ex, ey] = barAt(rd.A); - f.x = mix(f.sx, ex + HL * sin(rd.A), s); - f.y = mix(f.sy, ey + HL * cos(rd.A), s) - 18 * s * (1 - s); - f.ph = mix(f.ph0, -rd.A, s); - f.fs = s < 0.5 ? 1 : -1; - f.a1 = 1.3 * sin(PI * s); - f.a2 = -f.a1; - f.l1 = f.l2 = 0.3 * sin(PI * s); + const s = cl(f.T / TR, 0, 1), [ex, ey] = barAt(rd.A); + [f.x, f.y, f.ph, f.fs] = [mix(f.sx, ex + HL * sin(rd.A), s), mix(f.sy, ey + HL * cos(rd.A), s) - 18 * s * (1 - s), mix(f.ph0, -rd.A, s), s < 0.5 ? 1 : -1]; + const a = sin(PI * s); f.hx = null; + lim(f, 0.3 * a, 0.3 * a, 0, 0, 1.3 * a, -1.3 * a); if (s >= 1) f.m = 'ride'; break; } case 'land': { const k = sm(0, 0.35, f.T); - f.x = mix(f.sx, hbx - 1, k); - f.y = mix(f.sy, BY - LEG, k); - f.ph = mix(f.ph, 0, k); rest(f); - f.hx = hbx; - f.hy = hby; + [f.x, f.y, f.ph, f.hx, f.hy] = [mix(f.sx, hbx - 1, k), mix(f.sy, BY - LEG, k), mix(f.ph, 0, k), hbx, hby]; if (f.T > 0.4) f.m = f.next; break; } case 'drop': { f.vy += G * d; - f.x += f.vx * d; - f.y += f.vy * d; - const s = cl(f.T / f.TF, 0, 1); + [f.x, f.y] = [f.x + f.vx * d, f.y + f.vy * d]; + const s = cl(f.T / f.TF, 0, 1), u = sm(0.1, 0.3, s) * (1 - sm(0.62, 0.85, s)); f.ph = mix(f.ph0, -PI / 2 - TAU, ease(s)); - const u = sm(0.1, 0.3, s) * (1 - sm(0.62, 0.85, s)); - f.l1 = f.l2 = 2.2 * u; - f.k1 = f.k2 = 2.4 * u; - f.a1 = f.a2 = 2 * u + 0.6 * (1 - u); - f.hx = null; + lim(f, 2.2 * u, 2.2 * u, 2.4 * u, 2.4 * u, 0.6 + 1.4 * u, 0.6 + 1.4 * u).hx = null; if (f.y >= netY(f.x) - 1) { - f.m = 'net'; - f.T = 0; - S.net.x = f.x; + [f.m, f.T, nt.x] = ['net', 0, f.x]; S.pop = max(S.pop, 0.8); burst(S, 6); } break; } case 'net': { - // Bouncing on the net: a stiff spring while she is in it, a fall while she is out. - const n = ceil(d / 0.006); - const h = d / max(1, n); + // Bouncing on her back: a stiff spring while she is in the net, a fall while she is out. + const n = max(1, ceil(d / 0.006)), h = d / n; for (let j = 0; j < n; j++) { const pen = f.y - (netY(f.x) - 1); - const acc = G - (pen > 0 ? 420 * pen + 7 * f.vy : 0); - f.vy += acc * h; - f.y += f.vy * h; - f.x += f.vx * h; - f.vx *= 1 - 2.5 * h; - if (pen > 0) { - S.net.s = pen; - S.net.v = f.vy; - } else { - S.net.v += (-900 * S.net.s - 22 * S.net.v) * h; - S.net.s += S.net.v * h; - } - } - S.net.x = f.x; - S.net.hold = true; - f.ph = -PI / 2; - f.l1 = f.l2 = 0.6; - f.k1 = f.k2 = 0.4; - f.a1 = 1.6 + 0.5 * sin(f.T * 9); - f.a2 = 1.6 - 0.5 * sin(f.T * 9); - if (f.T > 2.6) { - f.m = 'up'; - f.T = 0; - f.sy = f.y; + f.vy += (G - (pen > 0 ? 420 * pen + 7 * f.vy : 0)) * h; + [f.y, f.x, f.vx] = [f.y + f.vy * h, f.x + f.vx * h, f.vx * (1 - 2.5 * h)]; + if (pen > 0) [nt.s, nt.v] = [pen, f.vy]; + else (nt.v += (-900 * nt.s - 22 * nt.v) * h), (nt.s += nt.v * h); } + [nt.x, nt.hold, f.ph] = [f.x, true, -PI / 2]; + lim(f, 0.6, 0.6, 0.4, 0.4, 1.6 + 0.5 * sin(f.T * 9), 1.6 - 0.5 * sin(f.T * 9)); + if (f.T > 2.6) [f.m, f.T, f.sy] = ['up', 0, f.y]; break; } case 'up': case 'tada': case 'walk': { - S.net.s += (1.5 - S.net.s) * min(1, d * 8); - S.net.v = 0; - S.net.x = f.x; - S.net.hold = true; - const top = netY(f.x) + S.net.s - LEG - 0.6; + nt.s += (1.5 - nt.s) * min(1, d * 8); + [nt.v, nt.x, nt.hold] = [0, f.x, true]; + const top = netY(f.x) + nt.s - LEG - 0.6; if (f.m === 'up') { const k = sm(0, 0.5, f.T); - f.ph = mix(-PI / 2, 0, k); - f.y = mix(f.sy, top, k); rest(f); - if (f.T > 0.55) (f.m = 'tada'), (f.T = 0); + [f.ph, f.y] = [mix(-PI / 2, 0, k), mix(f.sy, top, k)]; + if (f.T > 0.55) [f.m, f.T] = ['tada', 0]; } else if (f.m === 'tada') { // Arms up for the crowd. - f.ph = 0; - f.y = top; - f.a1 = 0.55; - f.a2 = -0.55; - if (f.T > 0.8) (f.m = 'walk'), (f.T = 0); + [f.ph, f.y] = [0, top]; + lim(f, 0, 0, 0, 0, 0.55, -0.55); + if (f.T > 0.8) [f.m, f.T] = ['walk', 0]; } else { - f.fs = -1; - f.x -= 13 * d; - f.y = top - 1.1 * abs(sin(f.T * 8)); - f.ph = 0; - f.l1 = 0.5 * sin(f.T * 8); - f.l2 = -f.l1; - f.a1 = PI - 0.5 * sin(f.T * 8); - f.a2 = PI + 0.5 * sin(f.T * 8); - if (f.x <= LX) (f.m = 'climb'), (f.T = 0); + // Bouncing along the net to the ladder. + const w = sin(f.T * 8); + [f.fs, f.x, f.y, f.ph] = [-1, f.x - 13 * d, top - 1.1 * abs(w), 0]; + lim(f, 0.5 * w, -0.5 * w, 0, 0, PI - 0.5 * w, PI + 0.5 * w); + if (f.x <= LX) [f.m, f.T] = ['climb', 0]; } break; } case 'climb': { // Up the rope ladder to the board. - f.x = LX; - f.y -= 12 * d; - f.fs = -1; - f.ph = 0; const w = sin(f.T * 9); - f.a1 = 0.35 + 0.45 * w; - f.a2 = 0.35 - 0.45 * w; - f.l1 = 0.9 * max(0, w); - f.l2 = 0.9 * max(0, -w); - f.k1 = 1.5 * max(0, w); - f.k2 = 1.5 * max(0, -w); - f.hx = null; - if (f.y <= BY - LEG) { - f.y = BY - LEG; - f.m = 'wait'; - } + [f.x, f.y, f.fs, f.ph, f.hx] = [LX, f.y - 12 * d, -1, 0, null]; + const p = max(0, w), q = max(0, -w); + lim(f, 0.9 * p, 0.9 * q, 1.5 * p, 1.5 * q, 0.35 + 0.45 * w, 0.35 - 0.45 * w); + if (f.y <= BY - LEG) [f.y, f.m] = [BY - LEG, 'wait']; break; } } - if (d > 0) { - f.vx0 = (f.x - f.px) / d; - f.vy0 = (f.y - f.py) / d; - } - f.px = f.x; - f.py = f.y; + if (d > 0) [f.vx0, f.vy0] = [(f.x - f.px) / d, (f.y - f.py) / d]; + [f.px, f.py] = [f.x, f.y]; } export function update(dt, input, params) { if (!params.s) setup(params); - const S = params.s; - const kv = input.knobValues || [0, 2, 0, 0.06]; - const rdt = cl(dt || 0, 0, 0.1); - const d = rdt * (cl(kv[1], 0.2, 3) / 2); - palette(S, kv[0]); - S.turns = cl(round(kv[2]), 1, 3); - S.hz = 0.26 + 0.74 * cl(kv[3], 0, 1); + const S = params.s, kv = input.knobValues || [0, 2, 0, 0.06], rdt = cl(dt || 0, 0, 0.1), d = rdt * (cl(kv[1], 0.2, 3) / 2); + const u = cl(kv[0], 0, 1) * 3, pi = min(2, floor(u)); + for (let j = 0; j < 7; j++) for (let c = 0; c < 3; c++) S.P[j][c] = mix(PAL[pi][j][c], PAL[pi + 1][j][c], u - pi); + [S.turns, S.hz] = [cl(round(kv[2]), 1, 3), 0.26 + 0.74 * cl(kv[3], 0, 1)]; if (!S.started) { // Open mid-act: the first flyer is already out on her first swing. - S.started = true; - S.fl[0].m = 'swing'; - S.fl[0].T = 1; - S.fl[0].sx = S.fl[0].sy = 0; + [S.started, S.fl[0].m, S.fl[0].T] = [true, 'swing', 1]; newRound(S, -0.9, 0); - S.A = S.rd.A; - S.bar = 'free'; - S.phc = S.ct.ph - OM * S.rd.tc; + [S.A, S.phc] = [S.rd.A, S.ct.ph - OM * S.rd.tc]; } - const tp = S.t; - S.t += d; - const t = S.t; - const rd = S.rd; + const tp = S.t, t = (S.t += d), rd = S.rd; if (input.btnPressed && input.btnPressed[0]) { + // Drum roll: this trick becomes the triple if she has not let go yet, else the next one. S.drumB = 1; if (!rd.rel && !rd.over) { - rd.turns = 3; - rd.fin = true; + Object.assign(rd, { turns: 3, fin: true, tf: 1.5, h: 17, tc: rd.tr + 1.5 }); S.nextFin = false; - rd.tf = 1.5; - rd.h = 17; - rd.tc = rd.tr + rd.tf; if (S.ct && S.ct.ev === 0) S.ct.t = rd.tc; } else S.trip = true; } S.A = S.bar === 'hook' && !rd.over ? mix(rd.A0, rd.A, sm(rd.t0, rd.tg, t)) : rd.A; S.th = S.bar === 'hook' ? -S.A : -S.A * cos(OM * (t - rd.tg)); // The catcher, phase-locked to the next thing he has to be on time for. - if (S.ct) { - const rem = S.ct.t - tp; - if (rem <= d) { - S.phc = S.ct.ph; - const ev = S.ct.ev; - S.ct = null; - S.psi = -B * cos(S.phc); - S.cp = cpose(S.psi); - catcherEvent(S, ev); - } else S.phc += ((S.ct.ph - S.phc) * d) / rem; - } else S.phc += OM * d; - S.psi = -B * cos(S.phc); - S.cp = cpose(S.psi); - S.net.hold = false; - for (let i = 0; i < 2; i++) stepFlyer(S, S.fl[i], d, i); - if (!S.net.hold) { - S.net.v += (-900 * S.net.s - 22 * S.net.v) * min(d, 0.02); - S.net.s += S.net.v * min(d, 0.02); - S.net.s *= exp(-d * 3); - } + const ct = S.ct, rem = ct ? ct.t - tp : 0; + S.phc = ct ? (rem <= d ? ct.ph : S.phc + ((ct.ph - S.phc) * d) / rem) : S.phc + OM * d; + S.cp = cpose((S.psi = -B * cos(S.phc))); + if (ct && rem <= d) (S.ct = null), catcherEvent(S, ct.ev); + const nt = S.net, h = min(d, 0.02); + nt.hold = false; + S.fl.forEach((f, i) => stepFlyer(S, f, d, i)); + // An empty net rings and settles. + if (!nt.hold) (nt.v += (-900 * nt.s - 22 * nt.v) * h), (nt.s = (nt.s + nt.v * h) * exp(-d * 3)); // After a drop the bar swings empty until it comes back to the board and the next flyer hooks it. - const r2 = S.rd; - if (r2.over && S.bar === 'free') { - const c = floor((t - r2.tg) / PB); - if (c > floor((tp - r2.tg) / PB)) { - const w = S.fl.findIndex((f) => f.m === 'wait'); - if (w >= 0) { - S.bar = 'hook'; - S.fl[w].m = 'hold'; - newRound(S, t + 0.7, w); - } - } - } - // Light cues: the house dims for the drum roll and comes up on the catch. const R = S.rd; - const star = S.fl[S.star]; - const roll = (!R.rel && t > R.tg + 0.5 * PB) || star.m === 'fly' || S.drumB > 0 ? 1 : 0; + const w = S.fl.findIndex((f) => f.m === 'wait'); + if (R.over && S.bar === 'free' && w >= 0 && floor((t - R.tg) / PB) > floor((tp - R.tg) / PB)) [S.bar, S.fl[w].m] = ['hook', 'hold'], newRound(S, t + 0.7, w); + // Light cues: the house dims for the drum roll, more for a bigger trick, and comes up on the catch. + const r = S.rd, star = S.fl[S.star]; + const roll = (!r.rel && t > r.tg + 0.5 * PB) || star.m === 'fly' || S.drumB > 0 ? 1 : 0; S.drum += (roll - S.drum) * (1 - exp(-d * 4)); S.drumB = max(0, S.drumB - rdt * 0.4); S.pop *= exp(-d * 0.9); - const dim = 0.7 - 0.22 * (R.turns - 1); - S.wash = (1 - S.drum * (1 - dim)) * (1 + 0.8 * S.pop); + S.wash = (1 - S.drum * (0.3 + 0.22 * (r.turns - 1))) * (1 + 0.8 * S.pop); // The follow spots: two on the flyer, the third on the catcher unless the trick is big. - const fin = S.fl.find((f) => f.m === 'drop' || f.m === 'net' || f.m === 'up' || f.m === 'tada'); - const t0 = fin || star; - const t1 = fin && t < R.tg + 0.4 ? fin : star; - const big = S.drum > 0.5 && R.turns >= 2; - const tg = [t0, t1, big ? star : { x: S.cp.x, y: S.cp.y + 3 }]; + const fin = S.fl.find((f) => ONNET[f.m]); + const tg = [fin || star, fin && t < r.tg + 0.4 ? fin : star, S.drum > 0.5 && r.turns >= 2 ? star : { x: S.cp.x, y: S.cp.y + 3 }]; const k = 1 - exp(-d * 8); - for (let j = 0; j < 3; j++) { - const s = S.sp[j]; - s.ax += (tg[j].x - s.ax) * k; - s.ay += (tg[j].y - 1 - s.ay) * k; - const vx = s.ax - s.sx; - const vy = s.ay - s.sy; - s.D = sqrt(vx * vx + vy * vy); - s.ux = vx / s.D; - s.uy = vy / s.D; - } + S.sp.forEach((s, j) => { + const vx = (s.ax += (tg[j].x - s.ax) * k) - s.sx, vy = (s.ay += (tg[j].y - 1 - s.ay) * k) - s.sy, D = (s.D = sqrt(vx * vx + vy * vy)); + [s.ux, s.uy] = [vx / D, vy / D]; + }); // Camera flashes in the crowd. - if (Math.random() < rdt * 0.9) burst(S, 1); + if (rnd() < rdt * 0.9) burst(S, 1); for (const e of S.fx) e.a -= rdt * 9; S.fx = S.fx.filter((e) => e.a > 0); } // A stroke lit by the light field, with optional sequin sparkle. function seg(S, ax, ay, bx, by, r, c, sp) { - const F = S.F; - const L = S.LT; - const x0 = max(0, floor(min(ax, bx) - r - 1)); - const x1 = min(W - 1, ceil(max(ax, bx) + r + 1)); - const y0 = max(0, floor(min(ay, by) - r - 1)); - const y1 = min(H - 1, ceil(max(ay, by) + r + 1)); - const dx = bx - ax; - const dy = by - ay; - const ll = dx * dx + dy * dy || 1e-6; - for (let y = y0; y <= y1; y++) - for (let x = x0; x <= x1; x++) { - const t = cl(((x - ax) * dx + (y - ay) * dy) / ll, 0, 1); - const ex = x - ax - dx * t; - const ey = y - ay - dy * t; + const { F, LT } = S, dx = bx - ax, dy = by - ay, ll = dx * dx + dy * dy || 1e-6; + const x1 = min(W - 1, ceil(max(ax, bx) + r + 1)), y1 = min(H - 1, ceil(max(ay, by) + r + 1)); + for (let y = max(0, floor(min(ay, by) - r - 1)); y <= y1; y++) + for (let x = max(0, floor(min(ax, bx) - r - 1)); x <= x1; x++) { + const t = cl(((x - ax) * dx + (y - ay) * dy) / ll, 0, 1), ex = x - ax - dx * t, ey = y - ay - dy * t; const a = cl(r + 0.5 - sqrt(ex * ex + ey * ey), 0, 1); if (a <= 0) continue; - const q = (y * W + x) * 3; - const s = sp ? 0.72 + 0.56 * Math.random() : 1; - F[q] += (c[0] * L[q] * s - F[q]) * a; - F[q + 1] += (c[1] * L[q + 1] * s - F[q + 1]) * a; - F[q + 2] += (c[2] * L[q + 2] * s - F[q + 2]) * a; + const q = (y * W + x) * 3, s = sp ? 0.72 + 0.56 * rnd() : 1; + for (let j = 0; j < 3; j++) F[q + j] += (c[j] * LT[q + j] * s - F[q + j]) * a; } } -function glow(S, x, y, r, c, k) { - const F = S.F; +function glow(F, x, y, r, c, k) { for (let yy = max(0, floor(y - r)); yy <= min(H - 1, ceil(y + r)); yy++) for (let xx = max(0, floor(x - r)); xx <= min(W - 1, ceil(x + r)); xx++) { - const g = k * exp(-((xx - x) ** 2 + (yy - y) ** 2) / (0.25 * r * r)); - const q = (yy * W + xx) * 3; - F[q] += c[0] * g; - F[q + 1] += c[1] * g; - F[q + 2] += c[2] * g; + const g = k * exp(-((xx - x) ** 2 + (yy - y) ** 2) / (0.25 * r * r)), q = (yy * W + xx) * 3; + for (let j = 0; j < 3; j++) F[q + j] += c[j] * g; } } // A stick figure: hips at (x, y), body angle ph (0 is head up), facing fs. function fig(S, f, col) { - const bx = sin(f.ph); - const by = -cos(f.ph); - const nx = cos(f.ph) * f.fs; - const ny = sin(f.ph) * f.fs; + const bx = sin(f.ph), by = -cos(f.ph), nx = cos(f.ph) * f.fs, ny = sin(f.ph) * f.fs; const dv = (a, l) => [(bx * cos(a) + nx * sin(a)) * l, (by * cos(a) + ny * sin(a)) * l]; - const shx = f.x + bx * 3 * SC; - const shy = f.y + by * 3 * SC; - for (const [l, k] of [ - [f.l1, f.k1], - [f.l2, f.k2], - ]) { - const [kx, ky] = dv(PI - l, 1.9 * SC); - const [fx, fy] = dv(PI - l + k, 1.9 * SC); + const shx = f.x + bx * 3, shy = f.y + by * 3; + for (const [l, k] of [[f.l1, f.k1], [f.l2, f.k2]]) { + const [kx, ky] = dv(PI - l, 1.9), [fx, fy] = dv(PI - l + k, 1.9); seg(S, f.x, f.y, f.x + kx, f.y + ky, 0.6, col, 1); seg(S, f.x + kx, f.y + ky, f.x + kx + fx, f.y + ky + fy, 0.5, col, 1); } seg(S, f.x, f.y, shx, shy, 0.8, col, 1); for (const a of [f.a1, f.a2]) { - let ex = f.hx; - let ey = f.hy; - if (ex === null) { - const [ax, ay] = dv(a, 2.7 * SC); - ex = shx + ax; - ey = shy + ay; - } - seg(S, shx, shy, ex, ey, 0.42, SKIN, 0); + const [ax, ay] = dv(a, 2.7); + seg(S, shx, shy, f.hx === null ? shx + ax : f.hx, f.hx === null ? shy + ay : f.hy, 0.42, SKIN, 0); } - seg(S, shx + bx * 1.45 * SC, shy + by * 1.45 * SC, shx + bx * 1.55 * SC, shy + by * 1.55 * SC, 0.75, SKIN, 0); + seg(S, shx + bx * 1.45, shy + by * 1.45, shx + bx * 1.55, shy + by * 1.55, 0.75, SKIN, 0); } export function draw(display, params) { const S = params.s; if (!S || !S.started) return; - const { F, LT, CA, CI, AUD, POOL, P } = S; - const wash = S.wash; - const wc = P[0]; - const hz = S.hz; - const hzA = hz * (0.7 - 0.22 * hz); - const halo = hz * hz * 0.18; - const hl = Math.log(max(1, halo * 400)); - const amb = 0.12 + 0.12 * wash; - const sp = S.sp.map((s, j) => [s.sx, s.sy, s.ux, s.uy, s.D, P[1 + j][0] / 255, P[1 + j][1] / 255, P[1 + j][2] / 255]); - const kc = 0.48 * 255 * wash; - const air = 9 * hz * wash; + const { F, LT, CA, CI, AUD, ROW, POOL, P, wash, hz } = S, wc = P[0]; + const hzA = hz * (0.7 - 0.22 * hz), halo = hz * hz * 0.18, hl = log(max(1, halo * 400)); + const am = wc.map((v) => v * (0.12 + 0.12 * wash)), kc = 0.48 * 255 * wash, air = 9 * hz * wash; + const sp = S.sp.map((s, j) => [s.sx, s.sy, s.ux, s.uy, s.D, ...P[1 + j].map((v) => v / 255)]); for (let y = 0; y < H; y++) { - const pb = POOL[y]; - // The ring below the crowd glows in the house light. - const ring = 50 * sm(117, 104, y) * sm(100, 104, y) * wash; + // The ring below the net glows in the house light behind the crowd. + const pb = POOL[y], ring = 50 * sm(117, 104, y) * sm(100, 104, y) * wash + air; for (let x = 0; x < W; x++) { - const i = y * W + x; - const q = i * 3; - const ci = CI[i] * kc; - let r = 4 + wc[0] * (air + ring) + CA[q] * wc[0] * ci; - let g = 4 + wc[1] * (air + ring) + CA[q + 1] * wc[1] * ci; - let b = 9 + wc[2] * (air + ring) + CA[q + 2] * wc[2] * ci; - let lr = amb * wc[0]; - let lg = amb * wc[1]; - let lb = amb * wc[2]; + const i = y * W + x, q = i * 3, ci = CI[i] * kc; + let r = 4 + wc[0] * ring + CA[q] * wc[0] * ci, g = 4 + wc[1] * ring + CA[q + 1] * wc[1] * ci, b = 9 + wc[2] * ring + CA[q + 2] * wc[2] * ci; + let lr = 0, lg = 0, lb = 0; const cv = CI[i] > 0.02 ? 0.9 * pb * sm(0, 0.3, CI[i]) * 255 : 0; - for (let j = 0; j < 3; j++) { - const s = sp[j]; - const vx = x - s[0]; - const vy = y - s[1]; - const al = vx * s[2] + vy * s[3]; - const pp = abs(vx * s[3] - vy * s[2]); - const R = 0.8 + al * TA; + for (const s of sp) { + const vx = x - s[0], vy = y - s[1], al = vx * s[2] + vy * s[3], pp = abs(vx * s[3] - vy * s[2]), R = 0.8 + al * TA; if (pp < R) { - const p = sm(R, R * 0.45, pp); - lr += s[5] * p * 1.2; - lg += s[6] * p * 1.2; - lb += s[7] * p * 1.2; - const fb = (al > s[4] ? 0.8 : 1) * 1.3 / (1 + (al / 110) ** 2); - const k = p * fb * hzA * 255; - r += s[5] * k; - g += s[6] * k; - b += s[7] * k; - // Past the flyer the beam lands on the canvas as a pool. - if (cv > 0 && al > s[4]) { - const c = cv * p; - r += CA[q] * s[5] * c; - g += CA[q + 1] * s[6] * c; - b += CA[q + 2] * s[7] * c; - } + // Lit air along the beam, and past the flyer a pool where it lands on the canvas. + const p = sm(R, R * 0.45, pp), k = ((p * (al > s[4] ? 0.8 : 1) * 1.3) / (1 + (al / 110) ** 2)) * hzA * 255, c = cv > 0 && al > s[4] ? cv * p : 0; + (lr += s[5] * p), (lg += s[6] * p), (lb += s[7] * p); + (r += s[5] * (k + CA[q] * c)), (g += s[6] * (k + CA[q + 1] * c)), (b += s[7] * (k + CA[q + 2] * c)); } + // Haze scatters a wide glow round each beam. if (pp < (R + 9) * hl) { const h = halo * exp(-pp / (R + 9)) * 255; - r += s[5] * h; - g += s[6] * h; - b += s[7] * h; + (r += s[5] * h), (g += s[6] * h), (b += s[7] * h); } } - F[q] = r; - F[q + 1] = g; - F[q + 2] = b; - LT[q] = min(1.25, lr); - LT[q + 1] = min(1.25, lg); - LT[q + 2] = min(1.25, lb); + (F[q] = r), (F[q + 1] = g), (F[q + 2] = b); + (LT[q] = min(1.25, lr * 1.2 + am[0])), (LT[q + 1] = min(1.25, lg * 1.2 + am[1])), (LT[q + 2] = min(1.25, lb * 1.2 + am[2])); } } const t = S.t; - // The bale ring at the peak and its ring of bulbs. + // The king pole, the ring of bulbs round the peak, which chase in a drum roll, and the bale ring. seg(S, 32, 7, 32, 29, 0.7, STEEL, 0); for (let j = 0; j < 22; j++) { - const a = (TAU * j) / 22; - const x = 32 + 20 * sin(a); - const y = 6.5 + 5 * cos(a); - const ch = (((j / 22 - t * 0.9) % 1) + 1) % 1 < 0.22 ? 1 : 0; - let v = 0.75 + 0.15 * sin(t * 3 + j * 2.1); - v = mix(v, 0.3 + 0.9 * ch, S.drum) + 0.5 * S.pop; - v *= 0.7 + 0.3 * cos(a) * 0.5 + 0.15; - glow(S, x, y, 3.2, [255, 196, 110], 0.42 * v); + const a = (TAU * j) / 22, x = 32 + 20 * sin(a), y = 6.5 + 5 * cos(a), ch = (((j / 22 - t * 0.9) % 1) + 1) % 1 < 0.22 ? 1 : 0; + const v = (mix(0.75 + 0.15 * sin(t * 3 + j * 2.1), 0.3 + 0.9 * ch, S.drum) + 0.5 * S.pop) * (0.85 + 0.15 * cos(a)); + glow(F, x, y, 3.2, [255, 196, 110], 0.42 * v); const q = (round(y) * W + round(x)) * 3; - F[q] += 200 * v; - F[q + 1] += 160 * v; - F[q + 2] += 90 * v; + for (let k = 0; k < 3; k++) F[q + k] += [200, 160, 90][k] * v; } seg(S, 30, 5, 34, 5, 1.1, [60, 62, 70], 0); // The rig: a truss under the peak, guy lines, the uprights, the board and the ladder. @@ -735,64 +430,40 @@ export function draw(display, params) { seg(S, 60, PFY - 2, 40, 7, 0.25, ROPE, 0); seg(S, 62, PFY - 2, 62, 103, 0.35, ROPE, 0); seg(S, 0, PFY - 2, 0, BY, 0.35, ROPE, 0); - const [hbx] = barAt(-S.rd.A); - seg(S, 0, BY + 0.6, hbx + 0.4, BY + 0.6, 0.55, [190, 160, 120], 0); + seg(S, 0, BY + 0.6, barAt(-S.rd.A)[0] + 0.4, BY + 0.6, 0.55, [190, 160, 120], 0); seg(S, 1, BY + 1, 1, 97, 0.3, ROPE, 0); seg(S, 4.2, BY + 1, 4.2, 97, 0.3, ROPE, 0); for (let y = BY + 3; y < 97; y += 3) seg(S, 1, y, 4.2, y, 0.25, ROPE, 0); - // The safety net, sagging and dented where a flyer lands in it. + // The safety net, a sagging diamond mesh, dented where a flyer is in it. const nt = S.net; for (let x = 0; x < W; x++) { - const dn = nt.s * exp(-(((x - nt.x) / 8) ** 2)); - const far = 94.5 + 1.2 * sin((PI * x) / 63) + 0.6 * dn; - const near = 103 + 2 * sin((PI * x) / 63) + dn; - for (let y = floor(far - 1); y <= ceil(near + 1); y++) { - if (y < 0 || y >= H) continue; - const v = (y - far) / (near - far); - const fa = x * 0.34 + v * 3; - const fb = x * 0.34 - v * 3; - let ln = max(sm(0.3, 0.48, abs((fa % 1) - 0.5)), sm(0.3, 0.48, abs((((fb % 1) + 1) % 1) - 0.5))); - ln *= v > -0.06 && v < 1.06 ? 1 : 0; - ln = max(ln, sm(0.9, 0.2, abs(y - far)), sm(0.9, 0.2, abs(y - near))); - if (ln <= 0) continue; + const dn = nt.s * exp(-(((x - nt.x) / 8) ** 2)), far = 94.5 + 1.2 * sin((PI * x) / 63) + 0.6 * dn, near = 103 + 2 * sin((PI * x) / 63) + dn; + for (let y = max(0, floor(far - 1)); y <= min(H - 1, ceil(near + 1)); y++) { + const v = (y - far) / (near - far), fa = x * 0.34 + v * 3, fb = x * 0.34 - v * 3; + let ln = v > -0.06 && v < 1.06 ? max(sm(0.3, 0.48, abs((fa % 1) - 0.5)), sm(0.3, 0.48, abs((((fb % 1) + 1) % 1) - 0.5))) : 0; + ln = max(ln, sm(0.9, 0.2, abs(y - far)), sm(0.9, 0.2, abs(y - near))) * 0.8; const q = (y * W + x) * 3; - F[q] += (205 * LT[q] - F[q]) * ln * 0.8; - F[q + 1] += (196 * LT[q + 1] - F[q + 1]) * ln * 0.8; - F[q + 2] += (176 * LT[q + 2] - F[q + 2]) * ln * 0.8; + if (ln > 0) for (let j = 0; j < 3; j++) F[q + j] += ([205, 196, 176][j] * LT[q + j] - F[q + j]) * ln; } } - // The fly bar on its ropes. - const [bx, by] = barAt(S.th); - seg(S, PFX - 1.2, PFY, bx - 1.2, by, 0.28, ROPE, 0); - seg(S, PFX + 1.2, PFY, bx + 1.2, by, 0.28, ROPE, 0); - seg(S, bx - 1.8, by, bx + 1.8, by, 0.45, [215, 215, 225], 0); - // The catcher's cradle and the catcher. - const c = S.cp; - seg(S, PCX - 1.2, PCY, c.bx - 1.2, c.by, 0.28, ROPE, 0); - seg(S, PCX + 1.2, PCY, c.bx + 1.2, c.by, 0.28, ROPE, 0); - seg(S, c.bx - 1.8, c.by, c.bx + 1.8, c.by, 0.45, [215, 215, 225], 0); - fig(S, { x: c.x, y: c.y, ph: c.ph, fs: 1, l1: 0, l2: 0, k1: 1.5, k2: 1.3, a1: 0, a2: 0, hx: c.hx, hy: c.hy }, P[6]); + // The fly bar on its ropes, the catcher's cradle, the catcher and the flyers. + const [bx, by] = barAt(S.th), c = S.cp; + for (const [px, py, x, y] of [[PFX, PFY, bx, by], [PCX, PCY, c.bx, c.by]]) { + seg(S, px - 1.2, py, x - 1.2, y, 0.28, ROPE, 0); + seg(S, px + 1.2, py, x + 1.2, y, 0.28, ROPE, 0); + seg(S, x - 1.8, y, x + 1.8, y, 0.45, BAR, 0); + } + fig(S, c, P[6]); for (const f of S.fl) fig(S, f, P[f.c]); - // The crowd in front of it all, a dark sea with phones and camera flashes. - // Each head is rimmed by the light behind it. + // The crowd in front of it all, a dark sea, each head rimmed by the light behind it. for (let i = W; i < N; i++) { const a = AUD[i]; if (a <= 0) continue; - const q = i * 3; - const k = S.ROW[i]; - const rim = k > S.ROW[i - W] ? (1 - 0.2 * k) * a : 0; - for (let c = 0; c < 3; c++) F[q + c] += ((12 - 2.5 * k) * wc[c] - F[q + c]) * a + rim * (16 * wash * wc[c] + 60 * max(0, LT[q + c] - 0.3)); - } - for (const [x, y] of S.phones) { - if (x < 0 || x > W - 2 || y < 0 || y >= H) continue; - const q = (y * W + x) * 3; - F[q] += 70; - F[q + 1] += 92; - F[q + 2] += 130; - F[q + 3] += 70; - F[q + 4] += 92; - F[q + 5] += 130; + const q = i * 3, k = ROW[i], rim = k > ROW[i - W] ? (1 - 0.2 * k) * a : 0; + for (let j = 0; j < 3; j++) F[q + j] += ((12 - 2.5 * k) * wc[j] - F[q + j]) * a + rim * (16 * wash * wc[j] + 60 * max(0, LT[q + j] - 0.3)); } - for (const e of S.fx) glow(S, e.x, e.y, 2.2, [255, 250, 240], e.a * e.a * (1 - e.w)); + // Phones held up to film, and camera flashes. + for (const [x, y] of S.phones) if (x >= 0 && x < W - 1) for (let j = 0; j < 6; j++) F[(y * W + x) * 3 + j] += [70, 92, 130][j % 3]; + for (const e of S.fx) glow(F, e.x, e.y, 2.2, [255, 250, 240], e.a * e.a * (1 - e.w)); 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 2b153ba67f976ac77db02e0c9827d10485ffcfd1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:41:16 +0000 Subject: [PATCH 20/33] waggle-dance: warm lamp between daylight and red, lighter first cloud, tighter code --- .../pattern-study/patterns/waggle-dance.js | 113 ++++++------------ 1 file changed, 35 insertions(+), 78 deletions(-) diff --git a/incoming/pattern-study/patterns/waggle-dance.js b/incoming/pattern-study/patterns/waggle-dance.js index 919a7ebf1..270596918 100644 --- a/incoming/pattern-study/patterns/waggle-dance.js +++ b/incoming/pattern-study/patterns/waggle-dance.js @@ -5,13 +5,13 @@ // Author: patternflow-lab // Date: 2026-10-24 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: original, pattern round 24, after Karl von Frisch's reading of the honeybee waggle dance (Tanzsprache und Orientierung der Bienen, 1965) +// Lineage: original, pattern round 24, after Karl von Frisch's reading of the honeybee's waggle dance // Round: 24 // =============================== // @matrix 64x128 // @knobs Light=0..1, Speed=0.2..3, Bees=80..240, Sun=-1..1 // -// Knob 1: Light. Daylight at 0, the window's sun lying across the glass in four panes; towards 1 the curtain closes and a red lamp comes up, which bees cannot see. +// Knob 1: Light. Daylight at 0, the window's sun lying across the glass in four panes; the curtain closes on the sun, a warm lamp takes over by 0.5, and towards 1 it turns to a red lamp the bees cannot see. // Knob 2: Speed. At 2 a waggle run takes about a second and a bee walks about a body length a second; 0.2 is ten times slower. // Knob 3: Bees. Eighty to 240; turned up, more walk in from the sides until they hide the comb. // Knob 4: Sun. The sun's bearing against the clover field: at 0.06 the runs point nearly straight up, and turning it swings every dance and slides the window's sun across the glass. @@ -33,7 +33,7 @@ const POLLENS = [[0.95, 0.52, 0.08], [0.95, 0.8, 0.16], [0.74, 0.54, 0.17], [0.6 const THX = [[0.5, 0.37, 0.2], [0.78, 0.56, 0.24], [0.9, 0.68, 0.3]]; const AMB = [[0.62, 0.44, 0.18], [1, 0.72, 0.2], [1, 0.84, 0.32]]; const [DARK, HEAD, WING] = [[0.3, 0.18, 0.08], [0.14, 0.1, 0.06], [0.8, 0.84, 0.9]]; -const [SUNC, REDC] = [[1, 0.84, 0.56], [1, 0.09, 0.035]]; +const [SUNC, DAYC, WARM, REDC] = [[1, 0.84, 0.56], [1, 0.94, 0.82], [1, 0.66, 0.34], [1, 0.09, 0.035]]; // Where the flowers lie, against the main field, and how often foragers come from each. const FIELDS = [0, 0, 0.95, -1.9]; @@ -88,11 +88,8 @@ function layout(S) { for (let r = 0; r < NR; r++) for (let c = 0; c < NC; c++) { const i = r * NC + c; - const x = cellX(r, c); - const y = cellY(r); - const ex = (x - 32) / 33; - const ey = (y - 70) / 28; - const e = sqrt(ex * ex + ey * ey) + 0.05 * (sin(x * 0.21 + y * 0.13) + sin(y * 0.29 - x * 0.17 + 2)); + const [x, y] = [cellX(r, c), cellY(r)]; + const e = len((x - 32) / 33, (y - 70) / 28) + 0.05 * (sin(x * 0.21 + y * 0.13) + sin(y * 0.29 - x * 0.17 + 2)); const u = rnd(); let t; if (y < 24 || (e > 1.28 && y < 64)) t = u < 0.82 ? HONEY : u < 0.95 ? NECTAR : EMPTY; @@ -223,18 +220,10 @@ function spot(S) { function newDance(S, b, vig) { const [cx, cy] = spot(S); - const D = { - b, cx, cy, - fl: FIELDS[floor(rnd() * FIELDS.length)] + 0.12 * (rnd() - 0.5), - L: vig ? 11 : 8 + 2.5 * rnd(), - tw: vig ? 1.25 : 0.85 + 0.3 * rnd(), - tr: vig ? 0.9 : 1 + 0.3 * rnd(), - tau: 0, side: rnd() < 0.5 ? -1 : 1, - n: vig ? 26 : 9 + floor(10 * rnd()), - amp: vig ? 1 : 0.65 + 0.2 * rnd(), - want: vig ? 10 : 6 + floor(3 * rnd()), - on: false, foll: [], rec: 0, pc: floor(rnd() * 3), - }; + // A forager from a rich patch runs longer, keeps at it and draws more followers. + const D = { b, cx, cy, fl: FIELDS[floor(rnd() * 4)] + 0.12 * (rnd() - 0.5), L: vig ? 11 : 8 + 2.5 * rnd(), tw: vig ? 1.25 : 0.85 + 0.3 * rnd(), + tr: vig ? 0.9 : 1 + 0.3 * rnd(), tau: 0, side: rnd() < 0.5 ? -1 : 1, n: vig ? 26 : 9 + floor(10 * rnd()), amp: vig ? 1 : 0.65 + 0.2 * rnd(), + want: vig ? 10 : 6 + floor(3 * rnd()), on: false, foll: [], rec: 0, pc: floor(rnd() * 3) }; D.th = D.fl - S.sunA; b.D = D; S.dances.push(D); @@ -247,10 +236,7 @@ function recruit(S, D, snap) { free.sort((a, b) => len(a.x - D.cx, a.y - D.cy) - len(b.x - D.cx, b.y - D.cy)); for (const b of free) { if (D.foll.length >= D.want) break; - b.m = FOLLOW; - b.D = D; - b.a = atan2(b.x - D.cx, -(b.y - D.cy)); - b.t = 5 + 10 * rnd(); + Object.assign(b, { m: FOLLOW, D, a: atan2(b.x - D.cx, -(b.y - D.cy)), t: 5 + 10 * rnd() }); D.foll.push(b); if (snap) { const R = D.L / 2 + 3.4; @@ -295,13 +281,8 @@ function stepDance(S, D, dt) { D.side = -D.side; if (--D.n <= 0) { // Done: she goes back out, and some of her followers go after the flowers. - D.gone = true; - b.m = EXIT; - b.wag = 0; - for (const f of D.foll) { - f.m = rnd() < 0.4 ? EXIT : CRAWL; - f.t = 1 + 2 * rnd(); - } + [D.gone, b.m, b.wag] = [true, EXIT, 0]; + for (const f of D.foll) [f.m, f.t] = [rnd() < 0.4 ? EXIT : CRAWL, 1 + 2 * rnd()]; return; } } @@ -335,11 +316,7 @@ function stepDance(S, D, dt) { f.a += dt * 0.35 * sin(wrap(da - f.a)); const R = D.L / 2 + 3.4 + 0.4 * sin(S.t * 2 + f.ph); if (seek(f, D.cx + R * sin(f.a), D.cy - R * cos(f.a), 6, dt) < 1.2) f.h += clamp(wrap(atan2(b.x - f.x, -(b.y - f.y)) - f.h), -6 * dt, 6 * dt); - if ((f.t -= dt) <= 0) { - f.m = CRAWL; - f.t = 1 + 2 * rnd(); - f.h += PI; - } + if ((f.t -= dt) <= 0) [f.m, f.t, f.h] = [CRAWL, 1 + 2 * rnd(), f.h + PI]; } } @@ -380,18 +357,13 @@ function step(S, dt) { if (m === CRAWL) crawl(S, b, dt); else if (m === REST) b.h += sin(S.t * 6 + b.ph) * 0.5 * dt; if ((b.t -= dt) > 0) continue; - if (m === DIP) { - b.x -= sin(b.h) * 1.5; - b.y += cos(b.h) * 1.5; - b.h += PI * (0.6 + 0.8 * rnd()); - } + // Backing out of a cell, she turns away. + if (m === DIP) [b.x, b.y, b.h] = [b.x - sin(b.h) * 1.5, b.y + cos(b.h) * 1.5, b.h + PI * (0.6 + 0.8 * rnd())]; const u = rnd(); b.m = CRAWL; b.t = 1 + 3 * rnd(); - if (u < 0.28) { - b.m = REST; - b.t = 0.8 + 2.5 * rnd(); - } else if (u < 0.5 && m !== DIP) { + if (u < 0.28) [b.m, b.t] = [REST, 0.8 + 2.5 * rnd()]; + else if (u < 0.5 && m !== DIP) { // Head first into a cell: to feed a larva, take nectar or pack pollen. const hx = round(b.x + sin(b.h) * 2.2 - 0.5); const hy = round(b.y - cos(b.h) * 2.2 - 0.5); @@ -428,7 +400,7 @@ function step(S, dt) { } export function setup(params) { - const S = { t: 0, started: false, sunA: 0, sunK: 9, next: 6, add: 0, c0: 4.2, c1: 8.5, cd: 0.7, bees: [], dances: [] }; + const S = { t: 0, started: false, sunA: 0, sunK: 9, next: 6, add: 0, c0: 4.2, c1: 8.5, cd: 0.55, bees: [], dances: [] }; for (const k of ["cid", "cid2"]) S[k] = new Int16Array(N); for (const k of ["hd", "dx", "dy", "expo", "X", "amb", "pool", "sun"]) S[k] = new Float32Array(N); for (const k of ["base", "A", "jit"]) S[k] = new Float32Array(N * 3); @@ -461,14 +433,9 @@ function start(S, n) { for (let k = 0; k < 2; k++) { const b = newBee(S, 0, 0, 0); const D = newDance(S, b, false); - D.fl = k ? 0.08 : -0.1; + Object.assign(D, { fl: k ? 0.08 : -0.1, cx: k ? 20 : 43, cy: k ? 111 : 102, on: true, tau: rnd() * (D.tw + D.tr), rec: 1.5 }); D.th = D.fl - S.sunA; - D.cx = k ? 20 : 43; - D.cy = k ? 111 : 102; - D.on = true; b.m = DANCE; - D.tau = rnd() * (D.tw + D.tr); - D.rec = 1.5; stepDance(S, D, 0); b.h = D.tau < D.tw ? D.th : b.h; recruit(S, D, true); @@ -571,24 +538,16 @@ function bee(S, b) { // A waggling dancer buzzes her wings into a pale blur round her. let wa = wg ? 0.55 * clamp(1.6 - len((u + 0.7) / 1.5, v / 1.7), 0, 1) : 0; if (c <= 0 && wa <= 0) continue; - let r, g, bb; const pt = PP[k]; - if (pt === 1) [r, g, bb] = HEAD; - else if (pt === 2) [r, g, bb] = th; - else { - const st = u > -0.85 ? 1 : u > -1.35 ? 0 : u > -1.9 ? 0.8 : 0; - r = DARK[0] + (am[0] - DARK[0]) * st; - g = DARK[1] + (am[1] - DARK[1]) * st; - bb = DARK[2] + (am[2] - DARK[2]) * st; - } + const col = pt === 1 ? HEAD : pt === 2 ? th : null; + const st = u > -0.85 ? 1 : u > -1.35 ? 0 : u > -1.9 ? 0.8 : 0; if (u > -2.2 && u < 0.3 && abs(v) > 0.35 && abs(v) < 1.15) wa = max(wa, shim); - r += (WING[0] - r) * wa; - g += (WING[1] - g) * wa; - bb += (WING[2] - bb) * wa; const a = max(c, wa); - A[q] += (r - A[q]) * a; - A[q + 1] += (g - A[q + 1]) * a; - A[q + 2] += (bb - A[q + 2]) * a; + for (let n = 0; n < 3; n++) { + let e = col ? col[n] : DARK[n] + (am[n] - DARK[n]) * st; + e += (WING[n] - e) * wa; + A[q + n] += (e - A[q + n]) * a; + } X[p] += (1 - X[p]) * a; } // A forager home from the field carries her pollen loads on her hind legs. @@ -631,20 +590,18 @@ export function draw(display, params) { X.set(expo); for (const b of S.bees) if (b.m !== DANCE) bee(S, b); for (const b of S.bees) if (b.m === DANCE) bee(S, b); - // Daylight warms and fades as the curtain closes, the sun goes first, and the red lamp comes up. + // The curtain closes on the sun first; the room light goes from daylight + // to a warm lamp over the hive by 0.5, and from that to the red lamp. const L = S.light; - const day = (1 - sm(0.05, 0.62, L)) * 255; - const dc = [day, day * (0.94 - 0.4 * L), day * (0.82 - 0.7 * L)]; - const sk = 0.9 * S.cloud * (1 - sm(0, 0.45, L)) * 255; - const red = 0.9 * sm(0.2, 0.72, L) * 255; + const [w, rd, pw] = [sm(0, 0.5, L), sm(0.5, 1, L), sm(0.15, 0.6, L)]; + const dc = [0, 1, 2].map((c) => (DAYC[c] + (WARM[c] - DAYC[c]) * w + (REDC[c] - WARM[c]) * rd) * (1 - 0.1 * w - 0.1 * rd) * 255); + const sk = 0.9 * S.cloud * (1 - sm(0, 0.4, L)) * 255; + const o = [0, 0, 0]; for (let p = 0; p < N; p++) { const q = p * 3; - const a = amb[p]; + const a = amb[p] + (pool[p] - amb[p]) * pw; const s = sun[p] * X[p] * sk; - const rp = pool[p] * red; - const r = knee(A[q] * (a * dc[0] + s * SUNC[0] + rp * REDC[0]) * jit[q]); - const g = knee(A[q + 1] * (a * dc[1] + s * SUNC[1] + rp * REDC[1]) * jit[q + 1]); - const b = knee(A[q + 2] * (a * dc[2] + s * SUNC[2] + rp * REDC[2]) * jit[q + 2]); - display.setPixel(p % W, (p / W) | 0, r | 0, g | 0, b | 0); + for (let c = 0; c < 3; c++) o[c] = knee(A[q + c] * (a * dc[c] + s * SUNC[c]) * jit[q + c]) | 0; + display.setPixel(p % W, (p / W) | 0, o[0], o[1], o[2]); } } From bd7a446be9127f5f727319c753624c779867623e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:43:55 +0000 Subject: [PATCH 21/33] waggle-dance: shade pollen and nectar walls, rake light off upright walls (lit 0.766 -> 0.732) --- incoming/pattern-study/patterns/waggle-dance.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/incoming/pattern-study/patterns/waggle-dance.js b/incoming/pattern-study/patterns/waggle-dance.js index 270596918..6c5764f52 100644 --- a/incoming/pattern-study/patterns/waggle-dance.js +++ b/incoming/pattern-study/patterns/waggle-dance.js @@ -94,7 +94,7 @@ function layout(S) { let t; if (y < 24 || (e > 1.28 && y < 64)) t = u < 0.82 ? HONEY : u < 0.95 ? NECTAR : EMPTY; else if (e > 1 && y < 72) t = u < 0.62 ? POLLEN : u < 0.78 ? NECTAR : EMPTY; - else if (e < 0.72) t = u < 0.72 ? BROOD : u < 0.82 ? LARVA : u < 0.85 ? EGG : EMPTY; + else if (e < 0.72) t = u < 0.68 ? BROOD : u < 0.78 ? LARVA : u < 0.81 ? EGG : EMPTY; else if (e < 1) t = u < 0.34 ? LARVA : u < 0.48 ? EGG : u < 0.64 ? BROOD : u < 0.68 ? POLLEN : EMPTY; else t = u < 0.12 ? POLLEN : u < 0.27 ? NECTAR : EMPTY; [S.type[i], S.age[i], S.tint[i], S.gap[i], S.pol[i]] = [t, rnd(), rnd(), rnd() * 6.283, floor(rnd() * 8)]; @@ -128,14 +128,15 @@ function paint(S) { [r, g, b] = [(1 - 0.04 * tn) * k, (0.88 - 0.16 * tn) * k, (0.6 - 0.26 * tn) * k]; } else if (t === NECTAR) { // Nectar: dark amber down in the cell, a bright meniscus round it, and a glint. - const f = (0.08 + 0.92 * sm(1.2, 2.3, d)) * (0.75 + 0.3 * tn); + const f = (0.08 + 0.92 * sm(1.5, 2.4, d)) * (0.75 + 0.3 * tn); [r, g, b] = [0.07 + 0.62 * f, 0.03 + 0.36 * f, 0.006 + 0.06 * f]; if (abs(dx + 1) < 0.6 && abs(dy + 1) < 0.7) [r, g, b] = [0.95, 0.9, 0.74]; ex = 0.6; } else if (t === POLLEN) { const c = POLLENS[S.pol[i]]; dim = 0.8; - const k = (0.8 + 0.25 * tn) * (1 - 0.35 * sm(1.6, 2.5, d)); + // Packed down in the cell, its inside wall in shadow round it. + const k = (0.8 + 0.25 * tn) * (1 - 0.85 * sm(1.5, 2.3, d)); [r, g, b] = c.map((v, n) => v * k * (0.82 + 0.3 * hash(p, 2 + n))); ex = 0.8; } else if (t === BROOD) { @@ -168,7 +169,8 @@ function paint(S) { const t2 = S.type[S.cid2[p]]; const pale = t === HONEY || t === NECTAR ? 1 : 0; const seam = (t === BROOD || t2 === BROOD) && cap(t) && cap(t2) ? 0.15 : 1; - const k = (0.85 + 0.3 * hash(p, 8)) * seam; + // Light from above rakes the slanted walls; the upright ones stand side on to it. + const k = (0.85 + 0.3 * hash(p, 8)) * seam * (!pale && abs(dx) > 2.6 ? 0.45 : 1); r += ((0.34 + 0.18 * pale) * k - r) * w; g += ((0.24 + 0.15 * pale) * k - g) * w; b += ((0.11 + 0.08 * pale) * k - b) * w; From 59a0d8128403d4531658f95d31aed541bd9dfd26 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:44:53 +0000 Subject: [PATCH 22/33] pendulum-wave: first running version --- .../pattern-study/patterns/pendulum-wave.js | 329 ++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 incoming/pattern-study/patterns/pendulum-wave.js diff --git a/incoming/pattern-study/patterns/pendulum-wave.js b/incoming/pattern-study/patterns/pendulum-wave.js new file mode 100644 index 000000000..7e3a82173 --- /dev/null +++ b/incoming/pattern-study/patterns/pendulum-wave.js @@ -0,0 +1,329 @@ +// ===== Patternflow pattern ===== +// Title: Pendulum Wave +// Family: physics +// Idea: Seen straight down from above, a row of pendulums hangs from a beam down the middle of a lamplit table, each shorter one swinging once more per cycle than the last, so the row that leaves the release board as one straight line winds into a travelling snake, splits into three and two interleaved rows, scatters, and lines up again at the end of the cycle. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 24, after the pendulum wave demonstration (Harvard Natural Sciences Lecture Demonstrations; R. E. Berg, "Pendulum waves", American Journal of Physics 59, 1991) +// Round: 24 +// =============================== +// @matrix 64x128 +// @knobs Finish=0..1, Speed=0.2..3, Pendulums=16..24, Trails=0..1 +// +// Knob 1: Finish. The bobs and the lamp over them: polished steel under a cool lamp on a walnut table at 0, then brass and copper under a warm lamp, and at 1 coloured glass beads in a rainbow down the row over a pale lab bench, each bead focusing a spot of its colour into its shadow. +// Knob 2: Speed. The pace of time. At 2 one cycle takes 30 seconds and the longest pendulum swings in a second and a quarter; at 0.2 it all runs ten times slower, at 3 half as fast again. +// Knob 3: Pendulums. How many hang from the beam, 16 to 24, always filling the 128 rows; with more of them the bobs are smaller and the shortest swings 47 times a cycle. +// Knob 4: Trails. A long exposure: at 0.06 each bob drags a short streak, and turned up every bob paints the arc of its swing in fading light, so the snake becomes a woven ribbon. +// Buttons: K1 tap catches every bob on the release board and lets them all go together, so the wave starts over from one straight line. + +const W = 64; +const H = 128; +const N = W * H; +const { min, max, floor, ceil, round, sin, cos, sqrt, exp, 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)); + +// The beam's centre line (it covers columns 30 and 31) and the swing. +const BX = 31; +const AMP = 22; +// One cycle at Speed 2, in seconds, and how many times the longest +// pendulum swings in it; each shorter one swings once more. +const TC = 28; +const N0 = 22; +// The lamp, off the left edge, and the beam's height over the table, in +// LEDs. The longest string is 13 LEDs, so its bob hangs 4 over the table. +const [LX, LY, LZ] = [-34, 58, 120]; +const ZB = 17; +const L0 = 13; +const MAXN = 24; + +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; +} +function hsv(h, s, v) { + const f = (n) => { + const k = (n + h * 6) % 6; + return v * (1 - s * max(0, min(k, 4 - k, 1))); + }; + return [f(5) * 255, f(3) * 255, f(1) * 255]; +} + +// The lamp's colour, and the metal, at Finish 0, 1/3, 2/3 and 1. +const LAMPS = [ + [184, 206, 248], + [255, 214, 160], + [255, 192, 142], + [238, 236, 226], +]; +const METALS = [ + [196, 208, 226], + [236, 172, 72], + [240, 122, 74], + [240, 122, 74], +]; +const stop = (T, f) => { + const u = cl(f, 0, 1) * 3, i = min(2, floor(u)); + return T[i].map((v, c) => mix(v, T[i + 1][c], u - i)); +}; + +// The table: walnut boards running along the beam, and a pale epoxy bench. +// The lamp's pool on it never moves, so it is worked out once. +function build(P) { + const seams = [11, 37, 58]; + const ax = 14 - LX, ay = 62 - LY, az = -LZ, al = sqrt(ax * ax + ay * ay + az * az); + let top = 0; + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6; + let pk = 0; + for (const s of seams) if (x >= s) pk++; + const sx = x + pk * 17.3, warp = 2.2 * vn(sx * 0.12, y * 0.018 + pk * 7); + const grain = vn(sx * 0.62 + warp, y * 0.04 + pk * 13), fine = vn(sx * 1.9, y * 0.22 + pk * 5); + const k = (0.74 + 0.36 * grain + 0.12 * fine) * (seams.includes(x) ? 0.6 : 1) * (0.88 + 0.24 * hash(pk, 99)); + P.wood[i * 3] = 0.76 * k; + P.wood[i * 3 + 1] = 0.37 * k; + P.wood[i * 3 + 2] = 0.16 * k; + const b = 0.95 + 0.05 * hash(x, y + 300) + 0.05 * vn(x * 0.25, y * 0.25); + P.bench[i * 3] = 0.7 * b; + P.bench[i * 3 + 1] = 0.74 * b; + P.bench[i * 3 + 2] = 0.7 * b; + // Inverse square and the floor's slant, inside the shade's cone. + const vx = x + 0.5 - LX, vy = y + 0.5 - LY, d = sqrt(vx * vx + vy * vy + LZ * LZ); + const ca = (vx * ax + vy * ay + LZ * -az) / (d * al); + const p = (LZ / d) ** 3 * (0.25 + 0.75 * sm(0.83, 0.97, ca)); + P.pool[i] = p; + top = max(top, p); + // The beam's shadow: a straight band, since the lamp stands level with it. + const bx = LX + (BX - LX) * (LZ / (LZ - ZB)), hw = (LZ / (LZ - ZB)) * 1; + P.beamD[i] = 0.5 * (1 - sm(hw - 0.9, hw + 0.9, Math.abs(x + 0.5 - bx))); + } + for (let i = 0; i < N; i++) P.pool[i] /= top; +} + +export function setup(params) { + const F = (n) => new Float32Array(n); + params.s = { + wood: F(N * 3), bench: F(N * 3), pool: F(N), beamD: F(N), D: F(N), tr: F(N * 3), f: F(N * 3), + x: F(MAXN), prev: F(MAXN), from: F(MAXN), y: F(MAXN), z: F(MAXN), r: F(MAXN), + c: 0, n: 0, hold: 0.3, board: 1, fin: 0, tau: 0.05, first: true, + }; + params.s.from.fill(BX + AMP); + build(params.s); +} + +// Where each pendulum hangs, how high its bob is and how big it looks. +function layout(P, n) { + P.n = n; + const s = H / n, rb = min(2.35, 0.31 * s); + for (let i = 0; i < MAXN; i++) { + P.y[i] = floor(s / 2 + s * i) + 0.5; + // A string's length goes as the square of its period. + P.z[i] = ZB - L0 * (N0 / (N0 + i)) ** 2; + P.r[i] = rb * (0.93 + 0.022 * P.z[i]); + } +} + +// The colours the finish gives the lamp and each bob. +function colours(P) { + const f = P.fin, n = P.n; + P.lamp = stop(LAMPS, f); + P.glass = sm(0.62, 1, f); + P.benchK = sm(0.55, 0.95, f); + // Brass and copper tint their own highlights; steel and glass do not. + P.tint = (0.15 + 0.7 * sm(0, 0.33, f)) * (1 - P.glass); + const m = stop(METALS, min(f, 2 / 3)); + P.col = []; + for (let i = 0; i < n; i++) { + const b = hsv((0.02 + (0.8 * i) / (n - 1)) % 1, 0.82, 1); + P.col.push(m.map((v, c) => mix(v, b[c], P.glass))); + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + const real = cl(dt || 0, 0, 0.1), d = (real * cl(k[1], 0.2, 3)) / 2; + P.fin = cl(k[0], 0, 1); + P.tau = 0.05 + 0.45 * cl(k[3], 0, 1) ** 1.25; + const n = cl(round(k[2]), 16, 24); + if (n !== P.n) layout(P, n); + colours(P); + + // The button: a board comes in, every bob is caught against it, and all + // are let go at once. + if (input.btnPressed && input.btnPressed[0] && P.hold < 0) { + P.hold = 0; + P.from.set(P.x); + } + if (P.hold >= 0) { + P.hold += real; + const e = sm(0.05, 0.6, P.hold); + for (let i = 0; i < MAXN; i++) P.x[i] = mix(P.from[i], BX + AMP, e); + P.board = sm(0, 0.35, P.hold); + if (P.hold >= 0.95) (P.hold = -1), (P.c = 0); + } else { + P.c = (P.c + d) % TC; + for (let i = 0; i < MAXN; i++) P.x[i] = BX + AMP * cos((2 * PI * (N0 + i) * P.c) / TC); + P.board = max(0, P.board - real * 5); + } + if (P.first) (P.first = false), P.prev.set(P.x); + + // The long exposure fades in the pendulums' own time, so a trail is as + // long on the table at any speed. Each bob paints the way it came this + // frame, older parts dimmer. + const tr = P.tr, fade = exp(-d / P.tau), span = max(d, 0.004); + for (let i = 0; i < tr.length; i++) tr[i] *= fade; + for (let i = 0; i < n; i++) { + const x0 = P.prev[i], x1 = P.x[i], y = P.y[i], rt = P.r[i] * 0.8, steps = max(1, ceil(Math.abs(x1 - x0) / 0.6)); + const col = P.col[i].map((v, c) => (v * 0.55 + P.lamp[c] * 0.25) * 0.85); + for (let s = 1; s <= steps; s++) { + const t = s / steps, xs = mix(x0, x1, t), lev = exp((-(1 - t) * span) / P.tau); + for (let yy = floor(y - rt - 1); yy <= y + rt + 1; yy++) + for (let xx = floor(xs - rt - 1); xx <= xs + rt + 1; xx++) { + if (xx < 0 || xx >= W || yy < 0 || yy >= H) continue; + const dx = xx + 0.5 - xs, dy = yy + 0.5 - y, q = (dx * dx + dy * dy) / (rt * rt); + if (q >= 1.6) continue; + const a = lev * exp(-1.6 * q), o = (yy * W + xx) * 3; + for (let c = 0; c < 3; c++) tr[o + c] = max(tr[o + c], col[c] * a); + } + } + } + P.prev.set(P.x); +} + +// Where the lamp throws a point at height z onto the table. +const proj = (x, y, z) => { + const k = LZ / (LZ - z); + return [LX + (x - LX) * k, LY + (y - LY) * k]; +}; + +// A shiny ball, sampled two by two inside each LED. +function ball(F, cx, cy, cz, r, base, lamp, g, tint) { + const sc = lamp.map((v, c) => v * mix(1, (base[c] / 255) * 1.15, tint)); + // The light leans twice as far as the lamp really stands, so the glint + // sits clearly on the lamp's side of a ball five LEDs across. + let lx = 2 * (LX - cx), ly = 2 * (LY - cy), lz = LZ - cz; + const ll = sqrt(lx * lx + ly * ly + lz * lz); + (lx /= ll), (ly /= ll), (lz /= ll); + let hx = lx, hy = ly, hz = lz + 1; + const hl = sqrt(hx * hx + hy * hy + hz * hz); + (hx /= hl), (hy /= hl), (hz /= hl); + const lxy = sqrt(lx * lx + ly * ly) || 1; + const col = [0, 0, 0]; + for (let y = floor(cy - r - 1); y <= cy + r + 1; y++) + for (let x = floor(cx - r - 1); x <= cx + r + 1; x++) { + if (x < 0 || x >= W || y < 0 || y >= H) continue; + let cov = 0; + col[0] = col[1] = col[2] = 0; + for (let sy = 0; sy < 2; sy++) + for (let sx = 0; sx < 2; sx++) { + const dx = (x + 0.25 + 0.5 * sx - cx) / r, dy = (y + 0.25 + 0.5 * sy - cy) / r, q = dx * dx + dy * dy; + if (q >= 1) continue; + cov += 0.25; + const nz = sqrt(1 - q), diff = max(0, dx * lx + dy * ly + nz * lz), nh = max(0, dx * hx + dy * hy + nz * hz), spec = nh ** 18, sheen = nh ** 3; + // Metal: the lamp's side lit, the rim holding the dark table. + const rim = 0.4 + 0.6 * nz, back = max(0, -(dx * lx + dy * ly) / lxy) * sqrt(q); + for (let c = 0; c < 3; c++) { + const m = base[c] * (0.1 + 0.85 * diff * sqrt(diff)) * rim, gl = base[c] * (0.3 + 0.25 * diff + 0.9 * back); + col[c] += (mix(m, gl, g) + sc[c] * (spec * 1.6 + 0.12 * sheen)) * 0.25; + } + } + if (cov <= 0) continue; + const o = (y * W + x) * 3; + for (let c = 0; c < 3; c++) F[o + c] = F[o + c] * (1 - cov) + col[c]; + } +} + +export function draw(display, params) { + const P = params.s; + if (!P || !P.n) return; + const F = P.f, D = P.D, n = P.n, lamp = P.lamp, g = P.glass, bk = P.benchK; + + // Shadows on the table: the beam, each string and each bob. A bob higher + // up throws its shadow further from the lamp and softer. + D.set(P.beamD); + const sh = []; + for (let i = 0; i < n; i++) { + const [sx, sy] = proj(P.x[i], P.y[i], P.z[i]), [px, py] = proj(BX, P.y[i], ZB); + sh.push([sx, sy]); + const rs = P.r[i] * 0.95, pen = 0.45 + 0.09 * P.z[i], dk = (0.9 - 0.02 * P.z[i]) * (1 - 0.35 * g); + for (let y = floor(sy - rs - pen - 1); y <= sy + rs + pen + 1; y++) + for (let x = floor(sx - rs - pen - 1); x <= sx + rs + pen + 1; x++) { + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const dd = sqrt((x + 0.5 - sx) ** 2 + (y + 0.5 - sy) ** 2), v = dk * (1 - sm(rs - pen, rs + pen, dd)), j = y * W + x; + if (v > D[j]) D[j] = v; + } + const len = sqrt((sx - px) ** 2 + (sy - py) ** 2), m = ceil(len * 1.5); + for (let s = 0; s <= m; s++) { + const t = s / max(1, m), x = floor(mix(px, sx, t)), y = floor(mix(py, sy, t)); + if (x >= 0 && x < W && y >= 0 && y < H && D[y * W + x] < 0.2) D[y * W + x] = 0.2; + } + } + + // The table under the lamp. + const fl = 0.42; + for (let i = 0, o = 0; i < N; i++, o += 3) { + const e = (0.1 + 0.9 * P.pool[i] * (1 - D[i])) * fl; + for (let c = 0; c < 3; c++) F[o + c] = mix(P.wood[o + c], P.bench[o + c], bk) * lamp[c] * e + P.tr[o + c]; + } + // A glass bead gathers the lamp into a spot of its colour in its shadow. + if (g > 0.01) + for (let i = 0; i < n; i++) { + const [sx, sy] = sh[i], rc = P.r[i] * 0.55, col = P.col[i]; + for (let y = floor(sy - 3); y <= sy + 3; y++) + for (let x = floor(sx - 3); x <= sx + 3; x++) { + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const q = ((x + 0.5 - sx) ** 2 + (y + 0.5 - sy) ** 2) / (rc * rc), a = g * 0.75 * exp(-q) * (0.4 + 0.6 * P.pool[y * W + x]), o = (y * W + x) * 3; + for (let c = 0; c < 3; c++) F[o + c] += col[c] * a; + } + } + + // Strings: a fine line from the beam out to each bob, catching the lamp. + for (let i = 0; i < n; i++) { + const x1 = P.x[i], y = floor(P.y[i]), a0 = min(BX, x1), a1 = max(BX, x1); + for (let x = floor(a0); x < a1; x++) { + if (x < 0 || x >= W) continue; + const o = (y * W + x) * 3, a = 0.5 * cl(min(x + 1 - a0, a1 - x), 0, 1); + for (let c = 0; c < 3; c++) F[o + c] = mix(F[o + c], lamp[c] * 0.5, a); + } + } + // The bobs, the higher ones last. + for (let i = 0; i < n; i++) ball(F, P.x[i], P.y[i], P.z[i], P.r[i], P.col[i], lamp, g, P.tint); + + // The release board, a strip of pale wood that holds the row straight. + if (P.board > 0.01) { + const bx = BX + AMP + 3.2; + for (let y = 0; y < H; y++) + for (let x = floor(bx); x < bx + 2 && x < W; x++) { + const o = (y * W + x) * 3, v = x === floor(bx) ? 0.85 : 0.55; + for (let c = 0; c < 3; c++) F[o + c] = mix(F[o + c], [224, 186, 132][c] * (lamp[c] / 255) * v, P.board); + } + } + + // The beam over everything: brushed metal, its lamp side bright, with a + // clamp at every pivot. + for (let y = 0; y < H; y++) { + const grain = 0.92 + 0.08 * hash(y, 7); + for (let x = BX - 1; x <= BX; x++) { + const o = (y * W + x) * 3, v = (x < BX ? 0.5 : 0.24) * grain; + for (let c = 0; c < 3; c++) F[o + c] = lamp[c] * v; + } + } + for (let i = 0; i < n; i++) { + const y = floor(P.y[i]); + for (let x = BX - 1; x <= BX; x++) { + const o = (y * W + x) * 3, v = x < BX ? 0.78 : 0.4; + for (let c = 0; c < 3; c++) F[o + c] = lamp[c] * v; + } + } + + 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 1998e13d96bdf17c2a90749827e5a407841e3ff8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:46:58 +0000 Subject: [PATCH 23/33] breath: first running version --- incoming/pattern-study/patterns/breath.js | 457 ++++++++++++++++++++++ 1 file changed, 457 insertions(+) create mode 100644 incoming/pattern-study/patterns/breath.js diff --git a/incoming/pattern-study/patterns/breath.js b/incoming/pattern-study/patterns/breath.js new file mode 100644 index 000000000..b0d032720 --- /dev/null +++ b/incoming/pattern-study/patterns/breath.js @@ -0,0 +1,457 @@ +// ===== Patternflow pattern ===== +// Title: Breath +// Family: body +// Idea: A pair of lungs drawn like an anatomical plate from the throat to the diaphragm breathes: the diaphragm drops, the ribs lift, the lungs swell, cold air runs down the bronchial tree into the alveoli and they flush from dusky violet to red, then the warm air rises back out as a puff at the top. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 24, after anatomical plates of the lungs, with the bronchial tree grown by volume-filling branching (Tawhai, Pullan and Hunter, 2000) +// Round: 24 +// =============================== +// @matrix 64x128 +// @knobs Air=0..1, Rate=0.2..3, Depth=0..1, Altitude=0..1 +// +// Knob 1: Air. What is breathed: clear cool air at 0, cold winter air at 0.5 where every breath comes out as a white cloud at the top, and city smog at 1 that browns the sky, turns the air in the airways yellow-grey and leaves soot in the alveoli. +// Knob 2: Rate. Breaths a minute: about 9 asleep at 0.2, 17 at 2 (one every 3.5 s), and a shallow pant of 33 at 3. +// Knob 3: Depth. A quiet breath at 0, where the domes of the diaphragm drop 6 LEDs, up to a deep sigh at 1 that drops them twice as far and spreads the ribs wide. +// Knob 4: Altitude. Sea level at 0, the top of Everest at 1: each breath brings less oxygen, so the alveoli flush only to a dusky purple, and the breathing quickens and deepens on its own. +// Buttons: K1 tap makes the lungs cough: a quick gasp, then three jolts that blow warm air up the tree and out in puffs. + +const W = 64, H = 128, N = W * H, A0 = 2; +const { min, max, abs, sin, cos, exp, sqrt, floor, ceil, round, hypot, atan2, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +const mix = (a, b, t) => a + (b - a) * t; +const 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 diaphragm under the lungs breathed out, x then y: the right dome higher +// over the liver, the heart sitting in the dip between the domes. +const DK = [-1, 118.5, 5, 113.8, 9, 108.4, 13, 103.8, 17, 101.6, 21, 101, 25, 101.8, 29, 104.2, 33, 106, 37, 106.6, 41, 106.6, 45, 105.8, 49, 106.8, 53, 110.2, 57, 115, 60, 117.8, 65, 122.5]; +const D0T = F32(68); +for (let i = 0, k = 0; i < 68; i++) { + while (k < DK.length - 4 && i - 1 > DK[k + 2]) k += 2; + D0T[i] = mix(DK[k + 1], DK[k + 3], (i - 1 - DK[k]) / (DK[k + 2] - DK[k])); +} +for (let n = 0; n < 4; n++) for (let i = 1; i < 67; i++) D0T[i] = (D0T[i - 1] + 2 * D0T[i] + D0T[i + 1]) / 4; +const d0 = (x) => ((x = cl(x + 1, 0, 66.99)), D0T[floor(x)] + (D0T[floor(x) + 1] - D0T[floor(x)]) * (x - floor(x))); +// How far the diaphragm drops at each column: most under the two domes, so +// they flatten. +const GX = Float32Array.from({ length: W }, (_, x) => 0.32 + 0.68 * (exp(-(((x - 17) / 10) ** 2)) + exp(-(((x - 46) / 10) ** 2)))); +// The outlines of the right lung (on the viewer's left) and the smaller left +// lung with the heart's notch, breathed out. The diaphragm cuts their bases. +const LUNGS = [ + [19, 21, 15, 22.5, 11.5, 26, 8.5, 32, 6.3, 40, 4.8, 50, 4, 60, 3.6, 72, 3.6, 84, 4, 96, 4.6, 106, 5.6, 126, 28.8, 126, 28.6, 104, 28.2, 98, 27.6, 90, 27.2, 82, 27.6, 74, 28.6, 66, 29, 58, 28.6, 50, 27.8, 42, 26.6, 34, 24.8, 27, 22.5, 22.5], + [44.5, 22, 48.5, 23.5, 52, 27, 55, 33, 57.2, 41, 58.8, 51, 59.6, 62, 60, 74, 60.2, 86, 60.2, 98, 59.9, 108, 59.2, 126, 41.5, 126, 41.5, 106, 41, 103, 42.4, 98, 44.5, 92, 45.4, 86, 44.4, 80, 41.8, 75, 38.6, 71, 36.6, 66, 36, 58, 36.4, 50, 37.2, 42, 38.4, 34, 40, 28, 42, 23.5], +]; +// Airway walls by generation (9 is the channel through the larynx), and how +// strongly each shows over the lung. +const WC = [[226, 216, 204], [226, 216, 204], [228, 200, 206], [226, 192, 206], [224, 186, 206], [222, 182, 206], [220, 178, 206], [218, 176, 206], [216, 174, 206], [0, 0, 0]]; +const AG = [1, 1, 0.95, 0.9, 0.85, 0.78, 0.7, 0.62, 0.56, 0]; +// The cough: seconds and lung volume, and the moments of the three jolts. +const CK = [0, 0, 0.32, 0.97, 0.44, 0.97, 0.54, 0.66, 0.84, 0.66, 0.94, 0.38, 1.24, 0.38, 1.34, 0.12, 1.9, 0]; +const CT = [0.44, 0.84, 1.24]; + +function sdPoly(v, px, py) { + let d = 1e9, s = 1; + for (let i = 0, j = v.length - 2; i < v.length; j = i, i += 2) { + const ex = v[j] - v[i], ey = v[j + 1] - v[i + 1], wx = px - v[i], wy = py - v[i + 1]; + const t = cl((wx * ex + wy * ey) / (ex * ex + ey * ey), 0, 1), bx = wx - ex * t, by = wy - ey * t; + d = min(d, bx * bx + by * by); + const c1 = py >= v[i + 1], c2 = py < v[j + 1], c3 = ex * wy > ey * wx; + if ((c1 && c2 && c3) || (!c1 && !c2 && !c3)) s = -s; + } + return s * sqrt(d); +} +function samp(A, x, y) { + x = cl(x, 0, 62.999); + y = cl(y, 0, 126.999); + const xi = x | 0, yi = y | 0, fx = x - xi, p = yi * W + xi, a = A[p] + (A[p + 1] - A[p]) * fx; + return a + (A[p + W] + (A[p + W + 1] - A[p + W]) * fx - a) * (y - yi); +} + +export function setup(params) { + const P = (params.s = { + t: 0, ph: 0.68, lp: 0.68, vol: 0.6, D: 0.5, air: 0, alt: 0, ct: -1, cv: 0, jolt: 0, hp: 0, hb: 0, envIn: 0, envOut: 0, Fin: -1, Fx: 2, bf: [], puffs: [], + sd: F32(N), lid: new Int8Array(N), own: new Int16Array(N).fill(-1), cap: F32(N), cph: F32(N), und: F32(N * 3), ua: F32(N), GL: F32(N), + SX: F32(H), KY: F32(W), alvA: F32(N), alvS: F32(N), alvK: new Int16Array(N), trA: F32(N), trD: F32(N), trV: F32(N), trS: F32(N), trG: new Int8Array(N), rib: F32(N), O: F32(N * 3), + seg: [], ax: [], ay: [], as: [], soot: [], cd: [], cb: [], trH: F32(N), + }); + build(P); +} + +function addSeg(P, x0, y0, x1, y1, r, g, d) { + P.seg.push(x0, y0, x1, y1, r, g, d, d + hypot(x1 - x0, y1 - y0)); + return d + hypot(x1 - x0, y1 - y0); +} + +// Volume-filling branching: a branch's alveoli are split by the line from its +// end through their centroid, and each half gets a child that runs 40% of the +// way to its own centroid, until a handful is left: one grape cluster. +function grow(P, x0, y0, dx, dy, pts, g, r, d) { + const { ax, ay } = P, n = pts.length; + let cx = 0, cy = 0; + for (const i of pts) (cx += ax[i]), (cy += ay[i]); + (cx /= n), (cy /= n); + if (n <= 7 || g > 8) { + for (const i of pts) (P.ak[i] = P.cd.length), (ax[i] += (cx - ax[i]) * 0.3), (ay[i] += (cy - ay[i]) * 0.3); + P.cd.push(d + hypot(cx - x0, cy - y0)); + P.cb.push(0.84 + 0.32 * rnd()); + return; + } + let ux = cx - x0, uy = cy - y0, ul = hypot(ux, uy); + if (ul < 0.5) (ux = dx), (uy = dy), (ul = hypot(dx, dy)); + (ux /= ul), (uy /= ul); + const side = (i) => (ax[i] - x0) * uy - (ay[i] - y0) * ux; + let A = pts.filter((i) => side(i) > 0), B = pts.filter((i) => side(i) <= 0); + if (A.length < 2 || B.length < 2) { + const s = pts.slice().sort((i, j) => side(i) - side(j)); + (A = s.slice(0, n >> 1)), (B = s.slice(n >> 1)); + } + const pa = atan2(dy, dx), lim = g === 2 ? 1.75 : 1.1; + for (const Q of [A, B]) { + let qx = 0, qy = 0; + for (const i of Q) (qx += ax[i]), (qy += ay[i]); + (qx = qx / Q.length - x0), (qy = qy / Q.length - y0); + let a = atan2(qy, qx), da = a - pa; + da = ((((da + PI) % (2 * PI)) + 2 * PI) % (2 * PI)) - PI; + if (abs(da) > lim) a = pa + Math.sign(da) * lim; + let len = max(1.2, hypot(qx, qy) * (Q.length <= 7 ? 0.55 : 0.4)), x1, y1; + for (;;) { + (x1 = x0 + cos(a) * len), (y1 = y0 + sin(a) * len); + if (len <= 1.2 || samp(P.sd, x1, y1) < -0.6) break; + len *= 0.8; + } + const rr = max(0.5, r * Math.cbrt(Q.length / n)); + grow(P, x1, y1, x1 - x0, y1 - y0, Q, g + 1, rr, addSeg(P, x0, y0, x1, y1, rr, g, d)); + } +} + +function build(P) { + const { sd, lid, own, cap, cph, GL } = P; + for (let p = 0; p < N; p++) { + const x = p & 63, y = p >> 6, sl = d0(x + 0.5) - d0(x - 0.5), cut = (y - d0(x) + 0.6) / sqrt(1 + sl * sl); + const a = max(sdPoly(LUNGS[0], x, y), cut), b = max(sdPoly(LUNGS[1], x, y), cut); + sd[p] = min(a, b) + 0.9 * (vn(x * 0.32, y * 0.32) - 0.5); + lid[p] = a < b ? 0 : 1; + cap[p] = hash(x * 3 + 1, y * 7) > 0.6 ? 0.4 + 0.6 * hash(x, y + 99) : 0; + cph[p] = hash(x + 77, y) * 2 * PI; + GL[p] = exp(-(((x - 32) / 26) ** 2) - (((y - 66) / 50) ** 2)); + } + // Alveoli on a jittered honeycomb inside the lungs. + const pts = [[], []]; + for (let j = 0; j < 56; j++) + for (let i = 0; i < 29; i++) { + const x = 1 + i * 2.25 + (j & 1) * 1.125 + (rnd() - 0.5) * 0.7, y = 19 + j * 1.95 + (rnd() - 0.5) * 0.7; + if (x > 62.5 || y > 125 || samp(sd, x, y) > -1) continue; + pts[lid[round(y) * W + round(x)]].push(P.ax.length); + P.ax.push(x), P.ay.push(y), P.as.push(0.86 + 0.28 * rnd()), P.soot.push(rnd()); + } + P.ak = new Int16Array(P.ax.length); + // The larynx and trachea, the short steep right bronchus and the longer + // left one, then the trees. + let d = addSeg(P, 32, 3, 32, 16.5, 1, 9, 0); + d = addSeg(P, 32, 16.5, 32, 48.5, 3, 0, d); + grow(P, 26.6, 57, -4.8, 8.5, pts[0], 2, 2.3, addSeg(P, 31.3, 48.5, 26.6, 57, 2.3, 1, d)); + grow(P, 40.4, 56.6, 7.8, 8.1, pts[1], 2, 2.05, addSeg(P, 32.7, 48.5, 40.4, 56.6, 2.05, 1, d)); + P.dmax = max(...P.cd); + P.cd = Float32Array.from(P.cd, (v) => v / P.dmax); + P.cox = F32(P.cd.length).fill(0.55); + P.airK = F32(P.cd.length); + // Each lung pixel takes the cluster of its nearest alveolus. + const best = F32(N).fill(9); + P.ax.forEach((cx, i) => { + const cy = P.ay[i]; + for (let y = max(0, round(cy) - 3); y <= min(H - 1, round(cy) + 3); y++) + for (let x = max(0, round(cx) - 3); x <= min(W - 1, round(cx) + 3); x++) { + const e = hypot(x - cx, y - cy), p = y * W + x; + if (e < best[p]) (best[p] = e), (own[p] = P.ak[i]); + } + }); + under(P); +} + +// What lies behind the lungs, breathed out, as colour times cover: the +// larynx, the diaphragm and, dim under it, the liver and the stomach. +function under(P) { + const { und, ua } = P; + const lay = (p, r, g, b, a) => { + if (a <= 0) return; + const o = p * 3; + (und[o] += (r - und[o]) * a), (und[o + 1] += (g - und[o + 1]) * a), (und[o + 2] += (b - und[o + 2]) * a), (ua[p] += (1 - ua[p]) * a); + }; + for (let p = 0; p < N; p++) { + const x = p & 63, y = p >> 6, t = y - d0(x), ax = abs(x - 32); + if (t > 3) { + const xb = 37 + 3 * sin(y * 0.21), f = (1 - 0.55 * sm(3, 20, t)) * (0.7 + 0.5 * vn(x * 0.17, y * 0.17)) * (0.4 + 0.6 * sm(0.3, 2, abs(x - xb))); + x < xb ? lay(p, 70 * f, 22 * f, 22 * f, cl(t - 3, 0, 1)) : lay(p, 58 * f, 34 * f, 48 * f, cl(t - 3, 0, 1)); + } + // The diaphragm: a band of muscle with a lit upper face, its tendon pale + // in the middle under the heart. + if (t > -0.5 && t < 4) { + const ten = sm(24, 29, x) * sm(43, 38, x), st = 0.8 + 0.2 * sin(x * 1.9 + t * 0.9), lit = 1 + 0.45 * sm(1.6, 0, t); + lay(p, mix(232 * st, 214, ten) * 0.72 * lit, mix(82 * st, 198, ten) * 0.66 * lit, mix(96, 188, ten) * 0.66 * lit, cl(min(t + 0.5, 4 - t), 0, 1)); + } + if (y < 19) { + const hy = 3.4 + 0.03 * ax * ax; + if (ax < 9.5) lay(p, 200, 192, 176, 0.5 * cl(1 - abs(y - hy) / 0.9, 0, 1)); + const top = 6 + 2.2 * max(0, 1 - ax / 2.4), hw = 7.4 - 0.42 * (y - 6), e = min(y - top, 14.6 - y, hw - ax); + if (e > -0.5) lay(p, 206, 196, 184, cl(e + 0.5, 0, 1) * (0.42 + 0.2 * exp(-((ax / 1.8) ** 2)) + 0.25 * sm(1.2, 0.2, e))); + if (y > 15 && y < 17.6 && ax < 4.9) lay(p, 214, 204, 192, 0.55 * cl(4.9 - ax, 0, 1)); + } + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + P.t += dt; + P.air = cl(k[0], 0, 1); + const rate = cl(k[1], 0.2, 3), dep = cl(k[2], 0, 1), alt = (P.alt = cl(k[3], 0, 1)); + const bpm = (8 + 4.5 * rate + 12 * max(0, rate - 2) ** 2) * (1 + 0.45 * alt); + P.D = (0.5 + 0.5 * dep) * (1 - 0.4 * sm(2, 3, rate)) * (1 + 0.22 * alt); + if (input.btnPressed && input.btnPressed[0] && P.ct < 0) (P.ct = 0), (P.cv = P.vol), (P.cn = 0); + let vol = 0, envIn = 0, envOut = 0, Fin = -1, Fx = 2; + P.bf = []; + P.jolt = 0; + if (P.ct >= 0) { + // A gasp, then three coughs, each a jolt and a burst of warm air up the tree. + const c = (P.ct += dt); + let j = 0; + while (j < CK.length - 4 && c > CK[j + 2]) j += 2; + vol = mix(j ? CK[j + 1] : P.cv, CK[j + 3], sm(CK[j], CK[j + 2], c)); + if (c < 0.4) (envIn = sin((PI * c) / 0.4) ** 0.5), (Fin = 1.12 * (1 - (1 - cl(c / 0.3, 0, 1)) ** 2)); + for (const tc of CT) { + const u = c - tc; + if (u > 0 && u < 0.7) P.bf.push(1 - u * 3.2), (P.jolt = max(P.jolt, 2.4 * exp(-u / 0.08))); + } + if (P.cn < 3 && c > CT[P.cn] + 0.28) P.puffs.push([0, 1.3, rnd()]), P.cn++; + if (c > 1.9) (P.ct = -1), (P.ph = 0.9); + } else { + // In for 38% of a breath, out for half, then a short rest. + const p = (P.ph = (P.ph + (dt * bpm) / 60) % 1); + if (p < 0.38) (vol = 0.5 - 0.5 * cos((PI * p) / 0.38)), (envIn = sin((PI * p) / 0.38) ** 0.5), (Fin = 1.12 * (1 - (1 - cl(p / 0.3, 0, 1)) ** 2)); + else if (p < 0.88) { + const u = (p - 0.38) / 0.5; + (vol = 0.5 + 0.5 * cos(PI * u)), (envOut = sin(PI * u) ** 0.5), (Fx = 1.06 - 1.2 * sm(0.02, 0.8, u)); + } + if (P.lp < 0.64 && p >= 0.64) P.puffs.push([0, 1, rnd()]); + P.lp = p; + } + Object.assign(P, { vol, envIn, envOut, Fin, Fx }); + // The alveoli the cold air has reached take up oxygen fast; the rest give it + // up slowly to the blood that carries it away. + const wS = max(0, 2 * P.air - 1), hi = 1 - 0.66 * alt - 0.14 * wS, { cd, cox, airK } = P; + for (let i = 0; i < cd.length; i++) { + cox[i] += Fin > cd[i] ? (hi - cox[i]) * (1 - exp(-dt * 7)) : (0.05 - cox[i]) * (1 - exp(-dt * 0.85)); + airK[i] = envIn * exp(-(((Fin - cd[i]) / 0.07) ** 2)); + } + P.hp = (P.hp + (dt * (62 + 10 * rate + 40 * alt)) / 60) % 1; + P.hb = exp(-(((P.hp - 0.06) / 0.05) ** 2)) + 0.6 * exp(-(((P.hp - 0.3) / 0.05) ** 2)); + P.puffs = P.puffs.filter((f) => (f[0] += dt) < 5); +} + +// Rest coordinates to the panel, breathed in by the per-row and per-column +// scales: out to the sides, down with the diaphragm. +let FX = 0, FY = 0; +function fw(P, xr, yr) { + const { SX, KY } = P, kx = cl(32 + (xr - 32) * SX[cl(round(yr), 0, H - 1)], 0, 62.99), ki = floor(kx); + FY = A0 + (yr - A0) * (KY[ki] + (KY[ki + 1] - KY[ki]) * (kx - ki)); + FX = 32 + (xr - 32) * SX[cl(round(FY), 0, H - 1)]; +} + +function alveoli(P) { + const { alvA, alvS, alvK, ax, ay, as } = P, R = 1.02 + 0.52 * P.vol * P.D; + alvA.fill(0); + for (let i = 0; i < ax.length; i++) { + fw(P, ax[i], ay[i]); + const cx = FX, cy = FY, r = R * as[i]; + for (let y = max(0, ceil(cy - r - 0.5)); y <= min(H - 1, floor(cy + r + 0.5)); y++) + for (let x = max(0, ceil(cx - r - 0.5)); x <= min(W - 1, floor(cx + r + 0.5)); x++) { + const dx = x - cx, dy = y - cy, e = sqrt(dx * dx + dy * dy), a = cl(r + 0.5 - e, 0, 1), p = y * W + x; + if (a <= 0) continue; + const q = e / r, hx = dx + 0.4 * r, hy = dy + 0.45 * r, hl = max(0, 1 - sqrt(hx * hx + hy * hy) / (0.75 * r)); + const s = 0.72 + 0.28 * sqrt(max(0, 1 - q * q)) + 0.38 * hl * hl; + if (a * s > alvA[p] * alvS[p]) (alvA[p] = a), (alvS[p] = s), (alvK[p] = i); + } + } +} + +function tree(P) { + const { seg: S, trA, trD, trV, trS, trG, trH } = P, dm = 1 / P.dmax, dil = 1 + 0.1 * P.vol * P.D; + trA.fill(0); + trH.fill(0); + for (let j = 0; j < S.length; j += 8) { + fw(P, S[j], S[j + 1]); + const x0 = FX, y0 = FY; + fw(P, S[j + 2], S[j + 3]); + const ex = FX - x0, ey = FY - y0, l2 = ex * ex + ey * ey || 1e-6, r = S[j + 4] * dil, g = S[j + 5], da = S[j + 6], db = S[j + 7]; + for (let y = max(0, floor(min(y0, FY) - r - 1)); y <= min(H - 1, ceil(max(y0, FY) + r + 1)); y++) + for (let x = max(0, floor(min(x0, FX) - r - 1)); x <= min(W - 1, ceil(max(x0, FX) + r + 1)); x++) { + const t = cl(((x - x0) * ex + (y - y0) * ey) / l2, 0, 1), px = x - x0 - ex * t, py = y - y0 - ey * t, e = sqrt(px * px + py * py); + const a = cl(r + 0.5 - e, 0, 1), p = y * W + x, h = cl(r + 1.7 - e, 0, 1); + if (h > trH[p]) trH[p] = h; + if (a > trA[p]) (trA[p] = a), (trS[p] = da + (db - da) * t), (trD[p] = trS[p] * dm), (trV[p] = e / r), (trG[p] = g); + } + } +} + +// The collarbones and ten ribs, faint arcs that lift and spread on a breath. +function ribs(P) { + const { rib, SX } = P, v = P.vol * P.D; + rib.fill(0); + const dot = (fx, fy, rad, k) => { + for (let y = max(0, round(fy) - 1); y <= min(H - 1, round(fy) + 1); y++) + for (let x = max(0, round(fx) - 1); x <= min(W - 1, round(fx) + 1); x++) { + const a = cl(rad + 0.5 - sqrt((x - fx) ** 2 + (y - fy) ** 2), 0, 1) * k, p = y * W + x; + if (a > rib[p]) rib[p] = a; + } + }; + for (let i = 0; i <= 10; i++) + for (const s of [-1, 1]) { + if (i === 0) { + for (let n = 0; n <= 50; n++) { + const u = n / 50; + dot(32 + s * (2.5 + 22 * u), 19 - 4.5 * u + 1.6 * sin(PI * u) - 1.2 * v, 0.9, 1); + } + continue; + } + const yr = 23 + (i - 1) * 8.4, y0 = yr - v * (1 + 0.42 * i), hw = (1.5 + 28.4 * (1 - exp(-(yr - 15) / 14))) * SX[round(yr)]; + const rx = hw - 3, ry = (5.5 + 0.55 * i) * (1 - 0.25 * v), n = ceil((rx + ry) * 2.2); + for (let m = 0; m <= n; m++) { + const th = (1.62 * m) / n; + dot(32 + s * (3 + rx * sin(th)), y0 + ry * (1 - cos(th)), 0.6, sm(0.2, 0.6, th)); + } + } +} + +export function draw(display, params) { + const P = params.s; + if (!P || !P.cox) return; + const { SX, KY, sd, own, cox, cb, airK, cap, cph, und, ua, alvA, alvS, alvK, trA, trD, trV, trS, trG, trH, rib, O, ak, soot, GL } = P; + const t = P.t, v = P.vol * P.D, air = P.air, hb = P.hb, { envIn, envOut, Fin, Fx, bf } = P; + const wC = max(0, 1 - 2 * air), wW = 1 - abs(2 * air - 1), wS = max(0, 2 * air - 1); + const pal = (c, w, s) => [0, 1, 2].map((i) => c[i] * wC + w[i] * wW + s[i] * wS); + const bgT = pal([3, 7, 18], [16, 26, 48], [62, 50, 32]), bgB = pal([2, 4, 12], [8, 14, 30], [36, 28, 20]), glo = pal([8, 18, 40], [22, 38, 64], [40, 30, 18]); + const cold = pal([150, 205, 255], [215, 238, 255], [196, 178, 128]), warm = pal([255, 196, 160], [255, 224, 206], [190, 160, 130]); + const puffC = pal([255, 214, 196], [236, 242, 255], [168, 158, 144]), puffK = 0.34 * wC + 0.95 * wW + 0.6 * wS; + const oxA = [240 - 64 * wS, 46 + 26 * wS, 58 + 20 * wS], deA = [74, 58, 156], oxF = [80, 10, 30], deF = [24, 14, 50]; + for (let y = 0; y < H; y++) SX[y] = 1 + 0.085 * v * (0.25 + 0.75 * sm(15, 115, y)); + for (let x = 0; x < W; x++) KY[x] = 1 + 0.14 * v * GX[x]; + alveoli(P); + tree(P); + ribs(P); + // The breath leaving at the top: each puff rises, spreads and thins. + const pf = []; + for (const [age, str, sd0] of P.puffs) { + const R = 3 + (6 + 6 * wW) * age; + pf.push(32 + (sd0 - 0.5) * 8, 7 - 5 * age, R, str * sm(0, 0.3, age) * exp(-age / (0.9 + 1.4 * wW)), sd0 * 40); + } + const hs = 1 / (1 - 0.035 * hb); + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const fy = y / 127, gl = GL[p]; + let hz = 1; + if (wS > 0) hz = mix(1, 0.6 + 0.8 * vn(x * 0.07 + t * 0.3, y * 0.045 - t * 0.06), wS); + let r = (bgT[0] + (bgB[0] - bgT[0]) * fy + glo[0] * gl) * hz, g = (bgT[1] + (bgB[1] - bgT[1]) * fy + glo[1] * gl) * hz, b = (bgT[2] + (bgB[2] - bgT[2]) * fy + glo[2] * gl) * hz; + const xr = cl(32 + (x - 32) / SX[y], 0, 62.999), yr = cl(A0 + (y - A0) / KY[x], 0, 126.999); + const xi = xr | 0, yi = yr | 0, fx = xr - xi, fz = yr - yi, q = yi * W + xi; + if (ua[q] + ua[q + 1] + ua[q + W] + ua[q + W + 1] > 0) { + const w0 = (1 - fx) * (1 - fz) * ua[q], w1 = fx * (1 - fz) * ua[q + 1], w2 = (1 - fx) * fz * ua[q + W], w3 = fx * fz * ua[q + W + 1], a = 1 - w0 - w1 - w2 - w3; + const o0 = q * 3, o2 = o0 + W * 3; + r = r * a + und[o0] * w0 + und[o0 + 3] * w1 + und[o2] * w2 + und[o2 + 3] * w3; + g = g * a + und[o0 + 1] * w0 + und[o0 + 4] * w1 + und[o2 + 1] * w2 + und[o2 + 4] * w3; + b = b * a + und[o0 + 2] * w0 + und[o0 + 5] * w1 + und[o2 + 2] * w2 + und[o2 + 5] * w3; + } + // The heart, dim behind the lungs, its apex down to the left. + if (xr > 20 && xr < 56 && yr > 60 && yr < 110) { + const dx = xr - 36.6, dy = yr - 86, hv = (dx * 0.6 + dy * 0.8) * hs, hu = (dx * 0.8 - dy * 0.6) * hs, eu = hu / (12.5 * (1 - 0.36 * sm(-6, 19, hv))), ev = hv / 19; + const a = cl((1 - sqrt(eu * eu + ev * ev)) * 9, 0, 1); + if (a > 0) { + const k = (0.4 + 0.32 * hb) * (0.65 + 0.45 * sm(9, -9, hu)); + (r = mix(r, 150 * k, a)), (g = mix(g, 26 * k, a)), (b = mix(b, 44 * k, a)); + } + } + // The lung: a dusky body, the grape clusters of alveoli, the capillary + // net shimmering on them, the pleura at the edge. + const s = samp(sd, xr, yr), qn = round(yr) * W + round(xr); + let m = 0; + if (s < 1.2) { + const c = own[qn], ox = c >= 0 ? cox[c] : 0.3; + m = cl(0.5 - s, 0, 1); + if (m > 0) { + const k = 0.92 * m; + (r = mix(r, deF[0] + (oxF[0] - deF[0]) * ox, k)), (g = mix(g, deF[1] + (oxF[1] - deF[1]) * ox, k)), (b = mix(b, deF[2] + (oxF[2] - deF[2]) * ox, k)); + } + const aa = alvA[p]; + if (aa > 0) { + const i = alvK[p], cc = ak[i], o = cox[cc], sh = alvS[p] * cb[cc] * (soot[i] < wS * 0.4 ? 0.3 : 1), af = 0.4 * airK[cc]; + (r = mix(r, (deA[0] + (oxA[0] - deA[0]) * o) * sh + cold[0] * af, aa)), (g = mix(g, (deA[1] + (oxA[1] - deA[1]) * o) * sh + cold[1] * af, aa)), (b = mix(b, (deA[2] + (oxA[2] - deA[2]) * o) * sh + cold[2] * af, aa)); + } + if (m > 0 && cap[qn] > 0) { + const k = cap[qn] * (0.5 + 0.5 * sin(t * 6 + cph[qn] + 2 * hb)) * (0.12 + 0.34 * ox) * m; + (r += 230 * k), (g += 30 * k), (b += 50 * k); + } + const rm = cl(1 - abs(s + 0.45) / 0.75, 0, 1) * 0.5; + if (rm > 0) (r = mix(r, 126 + 129 * ox, rm)), (g = mix(g, 100 + 24 * ox, rm)), (b = mix(b, 214 - 70 * ox, rm)); + // A shadow along each airway lifts the tree off the alveoli. + const hk = trH[p] * 0.5 * m; + (r *= 1 - hk), (g *= 1 - hk), (b *= 1 - hk); + } + // The airways, and the air in them. + const ta = trA[p]; + if (ta > 0) { + const gg = trG[p], vv = trV[p], d = trD[p], wc = WC[gg], al = AG[gg] * ta; + let wb = 0.85; + if (gg <= 1) { + const ring = 0.5 + 0.5 * cos(trS[p] * 2.73); + wb = (0.5 + 0.5 * sm(0.35, 0.95, vv)) * (0.66 + 0.44 * ring * ring); + } else if (gg <= 3) wb = 0.65 + 0.35 * sm(0.3, 1, vv); + (r = mix(r, wc[0] * wb, al)), (g = mix(g, wc[1] * wb, al)), (b = mix(b, wc[2] * wb, al)); + let ca = 0, wa = 0; + if (envIn > 0 && d < Fin) ca = envIn * (0.3 + 0.7 * exp(-(((Fin - d) / 0.05) ** 2))); + if (envOut > 0 && d > Fx) wa = envOut * (0.24 + 0.76 * exp(-(((d - Fx) / 0.05) ** 2))); + for (const f of bf) wa += 1.3 * exp(-(((d - f) / 0.06) ** 2)) + (d > f ? 0.25 : 0); + const lf = (gg <= 1 ? 1 - 0.5 * sm(0.55, 1, vv) : 1) * ta * (gg === 9 ? 0.8 : 0.9); + (r += (cold[0] * ca + warm[0] * wa) * lf), (g += (cold[1] * ca + warm[1] * wa) * lf), (b += (cold[2] * ca + warm[2] * wa) * lf); + } + const rb = rib[p]; + if (rb > 0) { + const k = rb * (0.4 - 0.12 * m); + (r = mix(r, 220, k)), (g = mix(g, 210, k)), (b = mix(b, 196, k)); + } + if (y < 48 && pf.length) { + let pa = 0; + for (let j = 0; j < pf.length; j += 5) { + const dx = (x - pf[j]) / (pf[j + 2] * 1.4), dy = (y - pf[j + 1]) / pf[j + 2], e = dx * dx + dy * dy; + if (e < 4) pa += pf[j + 3] * exp(-e * 1.5) * (0.55 + 0.9 * (vn(x * 0.2 + pf[j + 4], y * 0.2 + t * 0.9) - 0.5)); + } + if (pa > 0) { + const k = min(1, pa * puffK); + (r = mix(r, puffC[0], k)), (g = mix(g, puffC[1], k)), (b = mix(b, puffC[2], k)); + } + } + if (wS > 0) { + const k = 0.14 * wS * hz; + (r = mix(r, 120, k)), (g = mix(g, 96, k)), (b = mix(b, 62, k)); + } + (O[p * 3] = r), (O[p * 3 + 1] = g), (O[p * 3 + 2] = b); + } + // A cough jolts the whole chest up. + const jl = round(P.jolt); + for (let y = 0; y < H; y++) { + const o = min(H - 1, y + jl) * W * 3; + for (let x = 0; x < W; x++) display.setPixel(x, y, min(255, O[o + x * 3]) | 0, min(255, O[o + x * 3 + 1]) | 0, min(255, O[o + x * 3 + 2]) | 0); + } +} From 8d724b69c69991923208b704f523c7badb43363c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:48:50 +0000 Subject: [PATCH 24/33] pendulum-wave: finish stops, trail curve, lamp-side glint --- .../pattern-study/patterns/pendulum-wave.js | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/incoming/pattern-study/patterns/pendulum-wave.js b/incoming/pattern-study/patterns/pendulum-wave.js index 7e3a82173..1a095a78c 100644 --- a/incoming/pattern-study/patterns/pendulum-wave.js +++ b/incoming/pattern-study/patterns/pendulum-wave.js @@ -5,22 +5,22 @@ // Author: patternflow-lab // Date: 2026-10-24 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: original, pattern round 24, after the pendulum wave demonstration (Harvard Natural Sciences Lecture Demonstrations; R. E. Berg, "Pendulum waves", American Journal of Physics 59, 1991) +// Lineage: original, pattern round 24, after the pendulum wave demonstration of the Harvard Natural Sciences Lecture Demonstrations, fifteen pendulums tuned to 51 to 65 swings a minute // Round: 24 // =============================== // @matrix 64x128 // @knobs Finish=0..1, Speed=0.2..3, Pendulums=16..24, Trails=0..1 // -// Knob 1: Finish. The bobs and the lamp over them: polished steel under a cool lamp on a walnut table at 0, then brass and copper under a warm lamp, and at 1 coloured glass beads in a rainbow down the row over a pale lab bench, each bead focusing a spot of its colour into its shadow. -// Knob 2: Speed. The pace of time. At 2 one cycle takes 30 seconds and the longest pendulum swings in a second and a quarter; at 0.2 it all runs ten times slower, at 3 half as fast again. -// Knob 3: Pendulums. How many hang from the beam, 16 to 24, always filling the 128 rows; with more of them the bobs are smaller and the shortest swings 47 times a cycle. -// Knob 4: Trails. A long exposure: at 0.06 each bob drags a short streak, and turned up every bob paints the arc of its swing in fading light, so the snake becomes a woven ribbon. +// Knob 1: Finish. The bobs and the lamp over them: polished steel under a cool lamp on a walnut table at 0, brass at 0.25 and copper at 0.5 under a warm lamp, sea-green glass beads on a pale lab bench at 0.75, opening into a rainbow down the row at 1. Each glass bead focuses a spot of its own colour into its shadow. +// Knob 2: Speed. The pace of time. At 2 a cycle takes 28 seconds and the longest pendulum swings 22 times in it, once every 1.27 seconds; at 0.2 it all runs ten times slower, at 3 half as fast again. +// Knob 3: Pendulums. How many hang from the beam, 16 to 24, always filling the 128 rows. With more of them the bobs are smaller and the shortest swings 45 times a cycle against the longest's 22. +// Knob 4: Trails. A long exposure in the pendulums' own time: at 0.06 each bob drags a short streak, and turned up each one paints more of its arc in fading light, until at 1 the comets run together into a ladder of glowing bars with the bobs racing along them. // Buttons: K1 tap catches every bob on the release board and lets them all go together, so the wave starts over from one straight line. const W = 64; const H = 128; const N = W * H; -const { min, max, floor, ceil, round, sin, cos, sqrt, exp, PI } = Math; +const { min, max, floor, ceil, round, cos, sqrt, exp, 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)); @@ -57,21 +57,23 @@ function hsv(h, s, v) { return [f(5) * 255, f(3) * 255, f(1) * 255]; } -// The lamp's colour, and the metal, at Finish 0, 1/3, 2/3 and 1. +// The lamp's colour, and the metal, at Finish 0, 1/4, 1/2 and 3/4 and on. const LAMPS = [ [184, 206, 248], [255, 214, 160], [255, 192, 142], [238, 236, 226], + [238, 236, 226], ]; const METALS = [ [196, 208, 226], [236, 172, 72], [240, 122, 74], [240, 122, 74], + [240, 122, 74], ]; const stop = (T, f) => { - const u = cl(f, 0, 1) * 3, i = min(2, floor(u)); + const u = cl(f, 0, 1) * 4, i = min(3, floor(u)); return T[i].map((v, c) => mix(v, T[i + 1][c], u - i)); }; @@ -92,17 +94,17 @@ function build(P) { P.wood[i * 3 + 1] = 0.37 * k; P.wood[i * 3 + 2] = 0.16 * k; const b = 0.95 + 0.05 * hash(x, y + 300) + 0.05 * vn(x * 0.25, y * 0.25); - P.bench[i * 3] = 0.7 * b; - P.bench[i * 3 + 1] = 0.74 * b; - P.bench[i * 3 + 2] = 0.7 * b; + P.bench[i * 3] = 0.8 * b; + P.bench[i * 3 + 1] = 0.84 * b; + P.bench[i * 3 + 2] = 0.8 * b; // Inverse square and the floor's slant, inside the shade's cone. const vx = x + 0.5 - LX, vy = y + 0.5 - LY, d = sqrt(vx * vx + vy * vy + LZ * LZ); const ca = (vx * ax + vy * ay + LZ * -az) / (d * al); const p = (LZ / d) ** 3 * (0.25 + 0.75 * sm(0.83, 0.97, ca)); P.pool[i] = p; top = max(top, p); - // The beam's shadow: a straight band, since the lamp stands level with it. - const bx = LX + (BX - LX) * (LZ / (LZ - ZB)), hw = (LZ / (LZ - ZB)) * 1; + // The beam's shadow: it lies level, so its shadow is a straight band. + const bx = LX + (BX - LX) * (LZ / (LZ - ZB)), hw = LZ / (LZ - ZB); P.beamD[i] = 0.5 * (1 - sm(hw - 0.9, hw + 0.9, Math.abs(x + 0.5 - bx))); } for (let i = 0; i < N; i++) P.pool[i] /= top; @@ -135,14 +137,15 @@ function layout(P, n) { function colours(P) { const f = P.fin, n = P.n; P.lamp = stop(LAMPS, f); - P.glass = sm(0.62, 1, f); - P.benchK = sm(0.55, 0.95, f); + P.glass = sm(0.52, 0.74, f); + P.benchK = sm(0.5, 0.74, f); // Brass and copper tint their own highlights; steel and glass do not. - P.tint = (0.15 + 0.7 * sm(0, 0.33, f)) * (1 - P.glass); - const m = stop(METALS, min(f, 2 / 3)); + P.tint = (0.15 + 0.7 * sm(0, 0.25, f)) * (1 - P.glass); + const m = stop(METALS, f), u = sm(0.75, 1, f); P.col = []; + // Sea-green glass at 3/4, opening into a rainbow down the row at 1. for (let i = 0; i < n; i++) { - const b = hsv((0.02 + (0.8 * i) / (n - 1)) % 1, 0.82, 1); + const b = hsv(mix(0.42, 0.02 + (0.8 * i) / (n - 1), u), 0.82, 1); P.col.push(m.map((v, c) => mix(v, b[c], P.glass))); } } @@ -152,7 +155,7 @@ export function update(dt, input, params) { const P = params.s, k = input.knobValues || [0, 2, 0, 0.06]; const real = cl(dt || 0, 0, 0.1), d = (real * cl(k[1], 0.2, 3)) / 2; P.fin = cl(k[0], 0, 1); - P.tau = 0.05 + 0.45 * cl(k[3], 0, 1) ** 1.25; + P.tau = 0.05 + 0.3 * cl(k[3], 0, 1) ** 1.6; const n = cl(round(k[2]), 16, 24); if (n !== P.n) layout(P, n); colours(P); @@ -254,7 +257,7 @@ export function draw(display, params) { for (let i = 0; i < n; i++) { const [sx, sy] = proj(P.x[i], P.y[i], P.z[i]), [px, py] = proj(BX, P.y[i], ZB); sh.push([sx, sy]); - const rs = P.r[i] * 0.95, pen = 0.45 + 0.09 * P.z[i], dk = (0.9 - 0.02 * P.z[i]) * (1 - 0.35 * g); + const rs = P.r[i] * 0.95, pen = 0.45 + 0.09 * P.z[i], dk = (0.9 - 0.02 * P.z[i]) * (1 - 0.15 * g); for (let y = floor(sy - rs - pen - 1); y <= sy + rs + pen + 1; y++) for (let x = floor(sx - rs - pen - 1); x <= sx + rs + pen + 1; x++) { if (x < 0 || x >= W || y < 0 || y >= H) continue; From 1484cb2704a7690095a32914c5232afc528c9822 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:51:45 +0000 Subject: [PATCH 25/33] shoot-em-up: first running version --- .../pattern-study/patterns/shoot-em-up.js | 1060 +++++++++++++++++ 1 file changed, 1060 insertions(+) create mode 100644 incoming/pattern-study/patterns/shoot-em-up.js diff --git a/incoming/pattern-study/patterns/shoot-em-up.js b/incoming/pattern-study/patterns/shoot-em-up.js new file mode 100644 index 000000000..cd30b431f --- /dev/null +++ b/incoming/pattern-study/patterns/shoot-em-up.js @@ -0,0 +1,1060 @@ +// ===== Patternflow pattern ===== +// Title: Shoot 'em Up +// Family: pixel +// Idea: A vertical arcade shooter plays itself on a tall cabinet screen: a white fighter weaves over a scrolling sea of islands, a coast, fields, a little town and a river, shooting down waves of enemy craft and gunboats, until a boss ship fills the top of the screen and sprays rings, spirals and fans of bullets for it to thread through. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 24, after the vertically scrolling arcade shooters of the 1980s +// Round: 24 +// =============================== +// @matrix 64x128 +// @knobs Stage=0..1, Speed=0.2..3, Waves=1..5, Bullets=0..1 +// +// Knob 1: Stage. Which stage the cabinet plays, in four steps: a tropical sea with green fields at 0, a red desert canyon, a city at night with lit windows and neon roofs, and deep space past a ringed planet. +// Knob 2: Speed. The pace of the game: at 2 the ground scrolls 12 LEDs a second and the first boss arrives two seconds in, and at 0.2 everything plays ten times slower. +// Knob 3: Waves. How many enemies come: at 1 a formation of five or six every three seconds, at 5 a bigger one every second, more gunboats on the sea, and waves that keep coming while the boss is there. +// Knob 4: Bullets. How thick the enemy fire is and how furious the boss: at 0.06 a ring of nine and a slow spiral, at 1 rings of thirty, a three-armed spiral and wide fans, with the boss's core burning red. +// Buttons: K1 tap drops a bomb: a white flash and a shock ring that clears the bullets and hurts everything it crosses. K4 tap calls the boss now. + +const W = 64; +const H = 128; +const N = W * H; +const { min, max, sin, cos, abs, atan2, hypot, floor, round, sqrt, PI } = Math; +const clamp = (v, a, b) => (v < a ? a : v > b ? b : v); +const rnd = (a, b) => a + (b - a) * Math.random(); +// Ground scroll at Speed 2, in LEDs a second; the clouds pass 1.8 times as fast. +const V0 = 12; +const CLOUD = 1.8; +// The map repeats every 1024 rows: sea and islands, coast, fields, town, river, coast, sea. +const PERIOD = 1024; + +const hash = (x, y, s) => { + let h = Math.imul((x * 374761393 + y * 668265263 + s * 982451653) | 0, 1274126177); + h = Math.imul(h ^ (h >>> 13), 1274126177); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; +function noise(x, y, s) { + const ix = floor(x); + const iy = floor(y); + const fx = x - ix; + const fy = y - iy; + const u = fx * fx * (3 - 2 * fx); + const v = fy * fy * (3 - 2 * fy); + const a = hash(ix, iy, s); + const b = hash(ix + 1, iy, s); + const c = hash(ix, iy + 1, s); + const d = hash(ix + 1, iy + 1, s); + return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v; +} + +// Ground types, one palette entry each per stage. +const DEEP = 0; +const WAVE = 1; +const CAP = 2; +const SHAL = 3; +const SAND = 4; +const GRASS = 5; +const TREE = 6; +const FLDA = 7; +const FLDB = 8; +const ROAD = 9; +const ROOF = 10; +const WALL = 11; +const TRUNK = 12; +const RIVER = 13; +const LIGHT = 14; +const HEDGE = 15; +const PALS = [ + [0x123c9a, 0x1e5cc4, 0xd2ecff, 0x2aa8cc, 0xf0d488, 0x4aae3c, 0x1f6a2a, 0x9ccc4a, 0x6e9a30, 0x76706a, 0xd8482e, 0xf2e6c8, 0x8a5a2a, 0x2c7ed8, 0xfff2c0, 0x2e5226], + [0x5c2018, 0x8a3a22, 0xd8966a, 0xb4502a, 0xf0b464, 0xdc9a4c, 0x4a8a3c, 0xf2c278, 0xc8843e, 0x6e4630, 0xc8602e, 0xf6dcaa, 0x5a3418, 0x2aa89a, 0xffe6aa, 0x7a2c1a], + [0x0c1846, 0x172a62, 0x8ca8e6, 0x1c386a, 0x50506a, 0x163e34, 0x0e2c24, 0x40446a, 0x2a2c48, 0x1e1e30, 0xb43c96, 0xffd25a, 0x2e2224, 0x12265e, 0xffa83c, 0x1c1c2c], + [0x08061c, 0x1e0e40, 0xe6e6ff, 0x34185e, 0x9a9088, 0x5a6280, 0x4c4440, 0x868ca8, 0x464c66, 0x1c2034, 0xff3c3c, 0xb8c2d8, 0x3c3432, 0x28dcff, 0x78ffc8, 0x2a2e40], +].map((p) => { + const a = new Float32Array(48); + p.forEach((c, i) => { + a[i * 3] = c >> 16; + a[i * 3 + 1] = (c >> 8) & 255; + a[i * 3 + 2] = c & 255; + }); + return a; +}); +// Clouds per stage: colour, how dark the underside is, alpha, and the shade of their shadow. +const CLOUDS = [ + [236, 242, 255, 0.74, 0.72, 0.62], + [236, 200, 156, 0.78, 0.6, 0.7], + [84, 96, 146, 0.7, 0.66, 0.72], + [150, 76, 214, 0.6, 0.5, 1], +]; + +// Sprites as rows of letters; "." is clear. Colours are hex, -1 is the boss's glowing core. +function mk(rows, pal, mirror) { + const w0 = rows[0].length; + const w = mirror ? w0 * 2 : w0; + const px = []; + const mask = new Uint8Array(w * rows.length); + rows.forEach((r, j) => { + for (let i = 0; i < r.length; i++) { + if (r[i] === ".") continue; + px.push(i, j, pal[r[i]]); + mask[j * w + i] = 1; + if (mirror) { + px.push(w - 1 - i, j, pal[r[i]]); + mask[j * w + w - 1 - i] = 1; + } + } + }); + return { w, h: rows.length, px, mask }; +} +const PLAYER = mk( + ["....w....", "...wcw...", "...wbw...", "..gwbwg..", "..gwwwg..", ".wgwwwgw.", "wwwwgwwww", "rww.g.wwr", "r..www..r", "...r.r..."], + { w: 0xdce4f0, c: 0x96f0ff, b: 0x286ee6, g: 0x828ca0, r: 0xe62832 }, +); +const FIGHTER = mk(["o.....o", "oo.d.oo", "ooodooo", ".odwdo.", "..ddd..", "...d..."], { o: 0xff8c1e, d: 0xaa3214, w: 0xfff0c8 }); +const DART = mk(["mm...mm", "mmm.mmm", ".mmmmm.", ".mmcmm.", "..mmm..", "..mdm..", "...m..."], { m: 0xd246e6, c: 0x78f0ff, d: 0x6e1e8c }); +const BOAT = mk( + [".kkkkk.", "kbbbbbk", "kbaaabk", "kbaaabk", "kbbbbbk", "kbbbbbk", "kbaaabk", "kbbbbbk", ".kbbbk.", ".kbbbk.", "..kbk..", "...k..."], + { k: 0x1e2028, b: 0x969baa, a: 0x5a5f6e }, +); +const LIFE = mk(["..w..", ".www.", "wwwww", "r.w.r"], { w: 0xdce4f0, r: 0xe62832 }); +const BOSS = mk( + [ + ".............kkgggk..kkkkkkk", + "kk...........kacccc..kbbbbbb", + "kykkk........kabbbc..kbbrrbb", + "kybbbkkk.....kabbbc..kbbrrbb", + "kbbbbbbbkk...kabbbc..kbbbbbb", + "kbbbbbbbbbkkkbabbbc..kbbbbbb", + "kbbrrrbbbbbbbbabbbc..kbbbkkk", + "kaarrrrrrbbbbbabbbc..kbbbkee", + "kccabbrrrrrrbbabbbckkbbbbkee", + ".kkcaabgggrrrrabbbcbbbbbbkee", + "...kcaagygbbrrrrrrbbbbbbbkee", + "....kacgggbbbbbrrrrrrbbbbkee", + "......kkcaabbbbbbbrrrbbbbkee", + "........kacaaabgggbbbbbbbkee", + ".........akccabgygbbbbbbbkee", + "...........kkaagggbbbbbbbkee", + ".............kccaabbbbbbbkkk", + "..............kkccabbbbbbbbb", + "................kkcaabbbbbbb", + "..................kccbbyybbb", + "...................kkabbbbbb", + ".....................kabbbbb", + ".....................kcbbbbb", + "......................kabbbb", + ".......................kabbg", + ".......................kcbbg", + "........................kabg", + ".........................kag", + ".........................kcg", + "..........................kg", + ], + { k: 0x14141e, c: 0x464a60, a: 0x787e96, b: 0xb4bacc, r: 0xdc323c, y: 0xffd646, g: 0x2c2c38, e: -1 }, + true, +); +const PALM = ["t.t.t", ".ttt.", "ttktt", ".ttt.", "t.t.t"]; +// 3x5 digits and a P, one octal digit per row from the top. +const GLYPH = [0o75557, 0o26227, 0o71747, 0o71317, 0o55711, 0o74717, 0o74757, 0o71122, 0o75757, 0o75717, 0o75744]; +// Enemy bullet colours: the boss's rings, its spirals, and aimed shots. +const BUL = [ + [255, 70, 170], + [70, 220, 255], + [255, 170, 40], +]; + +// ---- the ground ------------------------------------------------------------- + +// -1 is open sea and 1 is land; the coasts run a little slanted across the screen. +function profile(x, wy) { + const v = (wy % PERIOD) + 0.3 * (x - 32); + if (v < 250 || v > 830) return -1; + if (v < 290) return -1 + (v - 250) / 20; + if (v < 790) return 1; + return 1 - (v - 790) / 20; +} +const ISL = { x: 0, y: 0, r: 0 }; +function islands(sd, x, wy) { + const cx = floor(x / 32); + const cy = floor(wy / 44); + let best = 0; + for (let j = cy - 1; j <= cy + 1; j++) + for (let i = cx - 1; i <= cx + 1; i++) { + if (hash(i, j, sd + 1) > 0.7) continue; + const ix = i * 32 + 4 + hash(i, j, sd + 2) * 24; + const iy = j * 44 + 6 + hash(i, j, sd + 3) * 32; + const r = 12 + hash(i, j, sd + 4) * 10; + const b = 1.9 * (1 - hypot(x - ix, wy - iy) / r); + if (b > best) { + best = b; + ISL.x = ix; + ISL.y = iy; + ISL.r = r; + } + } + return best; +} +function terrain(sd, x, wy) { + const pr = profile(x, wy); + let e = pr + 0.5 * (noise(x / 11, wy / 11, sd) - 0.5) + 0.24 * (noise(x / 4.5, wy / 4.5, sd + 9) - 0.5); + if (pr < 0) e += islands(sd, x, wy) * min(1, -pr); + if (e < -0.08) { + if (e > -0.12) return CAP; + if (e > -0.21) return SHAL; + const cx = x >> 3; + const cy = floor(wy / 5); + const h = hash(cx, cy, sd + 21); + if (h < 0.4 && wy === cy * 5 + floor(hash(cx, cy, sd + 23) * 4)) { + const c0 = cx * 8 + floor(hash(cx, cy, sd + 22) * 6); + if (x >= c0 && x < c0 + (h < 0.15 ? 3 : 2)) return CAP; + } + if (e > -0.36) return WAVE; + return noise(x / 6, wy / 2, sd + 5) > 0.78 ? WAVE : DEEP; + } + if (e < 0.08) return SAND; + if (pr < -0.5) { + // An island: grass, and a palm at the middle of a big one. + const dx = round(x - ISL.x); + const dy = round(wy - ISL.y); + if (ISL.r > 13 && abs(dx) <= 2 && abs(dy) <= 2) { + const c = PALM[dy + 2][dx + 2]; + if (c !== ".") return c === "k" ? TRUNK : TREE; + } + return GRASS; + } + return land(sd, x, wy, wy % PERIOD); +} +function land(sd, x, wy, u) { + const dr = abs(x - 30 - 9 * sin(wy / 70)); + if (u > 560 && u < 770) { + const a = (u - 560) / 34; + const dv = abs(x - 32 - 23 * sin(a)) / sqrt(1 + (0.68 * cos(a)) ** 2); + if (dv < 4.4) { + if (dr < 2.6) return dr > 1.6 ? WALL : ROAD; + return dv < 3.3 ? RIVER : SAND; + } + } + if (dr < 1.6) return dr < 0.6 && wy % 6 < 3 ? LIGHT : ROAD; + if (u > 420 && u < 540) { + // The town: a grid of streets, houses with roofs, and a tree in each empty plot. + const gx = x + 4; + const px = floor(gx / 9); + const py = floor(wy / 9); + const lx = gx - px * 9; + const ly = wy - py * 9; + if (lx === 0 || ly === 0) return ROAD; + if (hash(px, py, sd + 41) < 0.72) { + if (lx >= 2 && lx <= 6 && ly >= 3 && ly <= 7) return lx === 4 ? TRUNK : ROOF; + if (lx >= 2 && lx <= 6 && ly === 2) return WALL; + if ((lx === 7 && ly >= 2 && ly <= 6) || (ly === 1 && lx >= 3 && lx <= 7)) return HEDGE; + return GRASS; + } + return hypot(lx - 4.5, ly - 4.5) < 2.7 ? TREE : GRASS; + } + // Fields: a patchwork of crops, meadows and woods with hedges between. + const band = floor(wy / 14); + const gx = x + floor(hash(band, 0, sd + 51) * 16); + const cx = floor(gx / 16); + const lx = gx - cx * 16; + const ly = wy - band * 14; + const h = hash(cx, band, sd + 52); + if (lx === 0 || ly === 0) return h < 0.6 ? HEDGE : GRASS; + const kind = floor(h * 5); + if (kind === 0) return wy % 3 === 0 ? FLDB : FLDA; + if (kind === 1) return gx % 3 === 0 ? FLDB : FLDA; + if (kind === 2) return hash(gx, wy, sd + 53) < 0.12 ? FLDB : FLDA; + const tx = floor(gx / 5); + const ty = floor(wy / 5); + if (kind === 4 && hash(tx, ty, sd + 54) < 0.6) return GRASS; + const d = hypot(gx - tx * 5 - 1 - hash(tx, ty, sd + 55) * 3, wy - ty * 5 - 1 - hash(tx, ty, sd + 56) * 3); + return d < 1.9 ? TREE : GRASS; +} +// A cloud: round puffs on a flat base, 3 where the sun lights its top, 2 its body, 1 its grey underside. +function newCloud(S, cw) { + const w = floor(rnd(22, 40)); + const h = floor(rnd(10, 16)); + const n = 3 + floor(w / 9); + const P = []; + for (let i = 0; i < n; i++) { + const r = h * 0.5 * (0.5 + 0.5 * sin((PI * (i + 0.5)) / n)) + rnd(-0.6, 0.8); + P.push((w * (i + 0.5)) / n + rnd(-1.5, 1.5), h - 1.5 - r * 0.55, r); + } + const inn = (x, y) => { + if (x < 0 || y < 0 || x >= w || y > h - 1.5) return false; + for (let i = 0; i < P.length; i += 3) if (hypot(x + 0.5 - P[i], y + 0.5 - P[i + 1]) < P[i + 2]) return true; + return false; + }; + const m = new Uint8Array(w * h); + for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) if (inn(x, y)) m[y * w + x] = !inn(x, y + 2) ? 1 : !inn(x - 1, y - 2) ? 3 : 2; + S.clouds.push({ x: floor(rnd(-14, 56 - w / 2)), cw: cw + h, w, h, m }); +} +function fill(S) { + const top = floor(S.scroll) + H + 24; + while (S.gen < top) { + const o = (S.gen & 255) * W; + for (let x = 0; x < W; x++) S.ter[o + x] = terrain(S.sd, x, S.gen); + S.gen++; + } + const cb = floor(S.scroll * CLOUD); + while (S.cnext < cb + H + 20) { + newCloud(S, S.cnext); + S.cnext += floor(rnd(24, 52)); + } + S.clouds = S.clouds.filter((c) => cb + H - 1 - c.cw < H); +} + +// ---- the game --------------------------------------------------------------- + +export function setup(params) { + const S = { + t: 0, + scroll: 85, + sd: floor(Math.random() * 9000), + ter: new Uint8Array(256 * W), + cb: new Uint8Array(N), + clouds: [], + buf: new Float32Array(N * 3), + sh: new Float32Array(N), + en: [], + boats: [], + eb: [], + ps: [], + fx: [], + pop: [], + caps: [], + boss: null, + bossAt: 3.2, + bossN: 0, + waveAt: 0.4, + waveN: 0, + boatAt: 0.2, + kills: 0, + score: 0, + flash: 0, + bomb: null, + pl: { x: 32, y: 112, inv: 0, dead: 0, lvl: 0, lvlT: 0, lives: 3, ft: 0, pu: -9 }, + stage: 0, + tempo: 1, + waves: 1, + fury: 0.06, + started: false, + }; + S.gen = floor(S.scroll) - 2; + S.cnext = floor(S.scroll * CLOUD) - 6; + fill(S); + params.s = S; +} + +function shoot(S, x, y, a, v, c, big) { + if (S.eb.length < 380) S.eb.push({ x, y, vx: cos(a) * v, vy: sin(a) * v, c, big }); +} +const aim = (S, x, y) => atan2(S.pl.y - y, S.pl.x - x); +function boom(S, x, y, s) { + const sp = []; + for (let i = 0; i < 4 + s * 3; i++) { + const a = Math.random() * 6.283; + const v = (20 + Math.random() * 30) * s; + sp.push(cos(a) * v, sin(a) * v); + } + S.fx.push({ x, y, t0: S.t, s, sp, d: 0.45 + 0.12 * s }); +} +function popup(S, x, y, v) { + S.pop.push({ x, y, v, t0: S.t }); +} + +function spawnWave(S) { + const n = S.waveN++; + if (n % 2 === 0) { + // A line that swoops in an S, down from the top or across from a side. + const m = 4 + round(S.waves); + const side = n % 4 === 0 ? 1 : -1; + const top = n % 6 === 4; + const cx = rnd(26, 38); + const A = side * rnd(14, 19); + const f = rnd(0.036, 0.048); + const y0 = rnd(8, 30); + for (let i = 0; i < m; i++) S.en.push({ k: 0, top, side, y0, born: S.t + i * 0.3, cx, A, f, x: -99, y: -99, hp: 6, fl: 0, ft: rnd(0.4, 2.4) }); + } else { + // A V that comes down, holds, and dives at the fighter one by one. + const m = 2 * floor(2 + S.waves * 0.55) + 1; + const cx = rnd(24, 40); + for (let i = 0; i < m; i++) { + const r = (i + 1) >> 1; + const s = i & 1 ? -1 : 1; + S.en.push({ k: 1, born: S.t, x: cx + s * r * 6, y: -8 - r * 6, hy: 34 - r * 6, st: 0, vx: 0, vy: 50, hp: 7, fl: 0, hold: 0.5 + r * 0.25 + Math.random() * 0.2 }); + } + } +} +function spawnBoat(S) { + const wy = floor(S.scroll) + H + 8; + for (let n = 0; n < 6; n++) { + const x = floor(rnd(6, 58)); + let ok = true; + for (let j = -7; j <= 7 && ok; j += 2) + for (let i = -4; i <= 4; i += 2) if (S.ter[((wy + j) & 255) * W + clamp(x + i, 0, 63)] > CAP) ok = false; + if (ok) { + S.boats.push({ x, wy, hp: 6, fl: 0, ft: rnd(0.6, 1.6), a: PI / 2 }); + return; + } + } +} +const boatY = (S, b) => floor(S.scroll) + H - 1 - round(b.wy); + +function kill(S, x, y, v, s) { + boom(S, x, y, s); + popup(S, x, y - 4, v); + S.score += v; + if (++S.kills % 9 === 0) S.caps.push({ x, y, t0: S.t }); +} + +function stepEnemies(S, d) { + const P = S.pl; + const f = S.fury; + for (const e of S.en) { + e.fl = max(0, e.fl - d); + const a = S.t - e.born; + if (a < 0) continue; + if (e.k === 0) { + if (e.top) { + e.y = -6 + 34 * a; + e.x = e.cx + e.A * sin(e.f * e.y); + } else { + e.x = e.side > 0 ? -6 + 38 * a : 70 - 38 * a; + e.y = e.y0 + 14 * a + 13 * sin(3.2 * a); + } + e.ft -= d; + if (e.ft <= 0) { + e.ft = rnd(1.2, 2.6); + if (e.y > 0 && e.y < 80 && Math.random() < 0.3 + 0.7 * f) shoot(S, e.x, e.y + 3, aim(S, e.x, e.y), 34, 2, 0); + } + } else { + if (e.st === 0) { + e.y += 50 * d; + if (e.y >= e.hy) { + e.st = 1; + e.t1 = S.t; + } + } else if (e.st === 1) { + e.x += sin(S.t * 3 + e.hy) * 4 * d; + if (S.t - e.t1 > e.hold) { + e.st = 2; + const g = aim(S, e.x, e.y); + e.vx = cos(g) * 30; + e.vy = sin(g) * 30; + if (Math.random() < 0.35 + 0.65 * f) shoot(S, e.x, e.y + 3, g, 42, 2, 0); + } + } else { + const sp = hypot(e.vx, e.vy); + const k = min(130, sp + 110 * d) / sp; + e.vx *= k; + e.vy *= k; + e.x += e.vx * d; + e.y += e.vy * d; + } + } + } + S.en = S.en.filter((e) => e.hp > 0 && e.y < 140 && e.x > -20 && e.x < 84); + for (const b of S.boats) { + b.fl = max(0, b.fl - d); + const sy = boatY(S, b); + if (S.ter[((round(b.wy) - 8) & 255) * W + clamp(b.x, 0, 63)] <= CAP) b.wy -= 3 * d; + let g = aim(S, b.x, sy - 2) - b.a; + g = ((g + 3 * PI) % (2 * PI)) - PI; + b.a += clamp(g, -2 * d, 2 * d); + b.ft -= d; + if (b.ft <= 0 && sy > 2 && sy < 96) { + b.ft = 2.2 - 1.3 * f; + const m = f > 0.5 ? 3 : 1; + for (let i = 0; i < m; i++) shoot(S, b.x + cos(b.a) * 4, sy - 2 + sin(b.a) * 4, b.a + (i - (m - 1) / 2) * 0.25, 38, 2, 0); + } + } + S.boats = S.boats.filter((b) => b.hp > 0 && boatY(S, b) < 140); +} + +function stepBoss(S, d) { + let B = S.boss; + if (!B) { + if (S.t < S.bossAt) return; + B = S.boss = { x: 32, y: -34, t0: S.t, st: 0, hp: S.bossN ? 320 + 40 * S.bossN : 60, ph: S.bossN ? 2.8 : 1.6, fl: 0, ang: 0, nx: 0, nr: 0, nf: 0, nd: 0 }; + } + const f = S.fury; + const a = S.t - B.t0; + B.fl = max(0, B.fl - d); + if (B.st === 2) { + // Blowing apart: explosions all over the hull, then one big one. + if (S.t >= B.nd) { + B.nd = S.t + 0.07; + const i = floor(Math.random() * BOSS.px.length / 3) * 3; + boom(S, B.x - 28 + BOSS.px[i], B.y + BOSS.px[i + 1], rnd(0.7, 1.4)); + } + if (S.t - B.t2 > 1.7) { + boom(S, B.x, B.y + 12, 3.2); + for (let i = 0; i < 6; i++) boom(S, B.x + rnd(-24, 24), B.y + rnd(4, 26), rnd(1, 1.8)); + popup(S, B.x - 6, B.y + 14, 5000); + S.score += 5000; + S.flash = 0.75; + S.caps.push({ x: B.x, y: B.y + 20, t0: S.t }); + S.boss = null; + S.bossAt = S.t + rnd(18, 30); + S.bossN++; + } + return; + } + if (B.st === 0) { + const u = min(1, a / 1.4); + B.y = -34 + 40 * (1 - (1 - u) * (1 - u)); + if (u >= 1) { + B.st = 1; + B.t1 = S.t; + } + return; + } + B.x = 32 + 3 * sin((S.t - B.t1) * 0.8); + const cy = B.y + 11; + const ph = floor((S.t - B.t1) / B.ph) % 3; + if (ph === 0 && S.t >= B.nr) { + B.nr = S.t + 1.5 - 0.8 * f; + const n = round(12 + 18 * f); + const a0 = Math.random() * 6.283; + for (let i = 0; i < n; i++) shoot(S, B.x, cy, a0 + (i * 2 * PI) / n, 28, 0, 1); + } + if (ph === 1 && S.t >= B.nx) { + B.nx = S.t + 1 / (7 + 23 * f); + B.ang += 0.37; + const arms = f > 0.5 ? 3 : 2; + for (let i = 0; i < arms; i++) shoot(S, B.x, cy, B.ang + (i * 2 * PI) / arms, 32, 1, 0); + } + if (ph === 2 && S.t >= B.nf) { + B.nf = S.t + 1.05 - 0.55 * f; + const m = 3 + round(6 * f); + for (const s of [-1, 1]) { + const tx = B.x + s * 19.5; + const ty = B.y + 10; + const g = aim(S, tx, ty); + for (let i = 0; i < m; i++) shoot(S, tx, ty, g + (i - (m - 1) / 2) * (0.12 + 0.1 * f), 42, 2, 0); + } + } +} + +function bomb(S) { + if (S.pl.dead > 0) return; + S.bomb = { x: S.pl.x, y: S.pl.y, t0: S.t, id: S.t }; + S.flash = max(S.flash, 0.9); +} + +function stepPlayer(S, d) { + const P = S.pl; + if (P.dead > 0) { + P.dead -= d; + if (P.dead <= 0) { + P.x = 32; + P.y = 134; + P.inv = 2.5; + } + return; + } + P.inv = max(0, P.inv - d); + P.lvlT -= d; + if (P.lvlT <= 0 && P.lvl > 0) { + P.lvl--; + P.lvlT = 14; + } + // Where the fighter would like to be: under the boss, under the nearest enemy, on a capsule. + const wv = 32 + 18 * sin(S.t * 0.53); + let ax = wv; + const B = S.boss; + if (B && B.st > 0) ax = B.x + 10 * sin(S.t * 1.1); + else if (S.caps.length) ax = S.caps[0].x; + else { + let by = -99; + for (const e of S.en) if (e.y > by && e.y < P.y - 14 && e.y > 0) ((by = e.y), (ax = e.x)); + for (const b of S.boats) { + const y = boatY(S, b); + if (y > by && y < P.y - 14 && y > 0) ((by = y), (ax = b.x)); + } + ax = 0.55 * ax + 0.45 * wv; + } + // Then it picks the nearby spot that the bullets will miss. + let best = 1e9; + let bx = P.x; + let byy = P.y; + for (const ox of [-12, -7, -3, 0, 3, 7, 12]) + for (const oy of [-6, 0, 6]) { + const cx = clamp(P.x + ox, 5, 58); + const cy = clamp(P.y + oy, 98, 116); + let c = 0.05 * abs(cx - ax) + 0.04 * abs(cy - 111) + 0.02 * abs(ox); + for (const b of S.eb) { + if (abs(b.y - cy) > 40) continue; + for (const tau of [0.06, 0.18, 0.34]) { + const dx = b.x + b.vx * tau - cx; + const dy = b.y + b.vy * tau - cy; + const q = dx * dx + 0.6 * dy * dy; + if (q < 42) c += ((42 - q) / 42) * (tau < 0.1 ? 4 : 2); + } + } + if (c < best) ((best = c), (bx = cx), (byy = cy)); + } + const sp = 52 * d; + P.x += clamp(bx - P.x, -sp, sp); + P.y += clamp(byy - P.y, -sp, sp); + P.ft -= d; + while (P.ft <= 0) { + P.ft += 0.1; + const v = 170; + const add = (x, vx) => S.ps.push({ x: P.x + x, y: P.y - 5, vx, vy: -v }); + add(-2, 0); + add(2, 0); + if (P.lvl >= 1) add(0, 0); + if (P.lvl >= 2) { + add(-3, -38); + add(3, 38); + } + } +} + +function stepShots(S, d) { + const B = S.boss; + for (const s of S.ps) { + s.x += s.vx * d; + s.y += s.vy * d; + if (s.y < -4) { + s.dead = 1; + continue; + } + for (const e of S.en) { + if (e.hp <= 0 || abs(s.x - e.x) > 4 || abs(s.y - e.y) > 4) continue; + s.dead = 1; + e.fl = 0.06; + if (--e.hp <= 0) kill(S, e.x, e.y, e.k ? 150 : 100, 1); + break; + } + if (s.dead) continue; + for (const b of S.boats) { + const by = boatY(S, b); + if (b.hp <= 0 || abs(s.x - b.x) > 3.5 || abs(s.y - by) > 6) continue; + s.dead = 1; + b.fl = 0.06; + if (--b.hp <= 0) kill(S, b.x, by, 300, 1.3); + break; + } + if (s.dead || !B || B.st !== 1) continue; + const ix = round(s.x - B.x + 28); + const iy = round(s.y - B.y); + if (ix >= 0 && ix < 56 && iy >= 0 && iy < 30 && BOSS.mask[iy * 56 + ix]) { + s.dead = 1; + B.fl = 0.05; + S.score += 10; + if (--B.hp <= 0) { + B.st = 2; + B.t2 = S.t; + S.eb.length = 0; + } + } + } + S.ps = S.ps.filter((s) => !s.dead); +} + +function stepBullets(S, d) { + const P = S.pl; + for (const b of S.eb) { + b.x += b.vx * d; + b.y += b.vy * d; + if (b.x < -3 || b.x > 66 || b.y < -3 || b.y > 130) b.dead = 1; + else if (P.dead <= 0 && P.inv <= 0 && abs(b.x - P.x) < 2 && abs(b.y - P.y) < 3) { + b.dead = 1; + boom(S, P.x, P.y, 1.6); + P.dead = 1.2; + P.lvl = 0; + if (--P.lives < 0) P.lives = 3; + } + } + const M = S.bomb; + if (M) { + const r = (S.t - M.t0) * 150; + for (const b of S.eb) if (hypot(b.x - M.x, b.y - M.y) < r) b.dead = 1; + for (const e of S.en) + if (e.y > 0 && e.hp > 0 && e.bombed !== M.id && hypot(e.x - M.x, e.y - M.y) < r) { + e.bombed = M.id; + e.hp -= 4; + if (e.hp <= 0) kill(S, e.x, e.y, e.k ? 150 : 100, 1); + } + for (const b of S.boats) { + const by = boatY(S, b); + if (by > 0 && b.hp > 0 && b.bombed !== M.id && hypot(b.x - M.x, by - M.y) < r) { + b.bombed = M.id; + b.hp -= 4; + if (b.hp <= 0) kill(S, b.x, by, 300, 1.3); + } + } + const B = S.boss; + if (B && B.st === 1 && B.bombed !== M.id && r > P.y - B.y - 20) { + B.bombed = M.id; + B.fl = 0.2; + B.hp -= 15; + if (B.hp <= 0) ((B.st = 2), (B.t2 = S.t)); + } + if (r > 160) S.bomb = null; + } + S.eb = S.eb.filter((b) => !b.dead); + for (const c of S.caps) { + c.y += 14 * d; + if (P.dead <= 0 && abs(c.x - P.x) < 6 && abs(c.y - P.y) < 7) { + c.dead = 1; + P.lvl = min(2, P.lvl + 1); + P.lvlT = 16; + P.pu = S.t; + } + } + S.caps = S.caps.filter((c) => !c.dead && c.y < 134); + S.fx = S.fx.filter((f) => S.t - f.t0 < f.d); + S.pop = S.pop.filter((p) => S.t - p.t0 < 0.9); +} + +function step(S, d) { + S.t += d; + S.scroll += V0 * d; + S.flash = max(0, S.flash - 2.6 * d); + fill(S); + const w = S.waves; + if (S.t >= S.waveAt) { + const busy = S.boss && S.boss.st < 2; + spawnWave(S); + S.waveAt = S.t + (S.waveN === 1 ? 0.45 : busy ? 2 : 1) * (3.3 / w ** 0.75); + } + if (S.t >= S.boatAt) { + spawnBoat(S); + S.boatAt = S.t + rnd(3.5, 6) / w; + } + stepEnemies(S, d); + stepBoss(S, d); + stepPlayer(S, d); + stepShots(S, d); + stepBullets(S, d); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s; + const k = input.knobValues || [0, 2, 0, 0.06]; + dt = clamp(dt || 0, 0, 0.1); + S.stage = clamp(k[0], 0, 1); + S.tempo = clamp(k[1], 0.2, 3) / 2; + S.waves = clamp(k[2], 1, 5); + S.fury = clamp(k[3], 0, 1); + if (!S.started) { + // The first second is played before the first frame, so the screen opens busy. + S.started = true; + for (let i = 0; i < 30; i++) step(S, 1 / 30); + } + const bp = input.btnPressed || []; + if (bp[0]) bomb(S); + if (bp[3] && !S.boss) S.bossAt = S.t; + let d = dt * S.tempo; + while (d > 0) { + const s = min(d, 1 / 30); + step(S, s); + d -= s; + } +} + +// ---- drawing ------------------------------------------------------------------ + +function put(buf, x, y, r, g, b) { + x = round(x); + y = round(y); + if (x < 0 || y < 0 || x >= W || y >= H) return; + const q = (y * W + x) * 3; + buf[q] = r; + buf[q + 1] = g; + buf[q + 2] = b; +} +// mode 0 draws, 1 draws it white, 2 stamps its shadow, 3 draws it lit up by a hit. +function spr(S, sp, x0, y0, mode, glow) { + const p = sp.px; + const buf = S.buf; + for (let i = 0; i < p.length; i += 3) { + const x = x0 + p[i]; + const y = y0 + p[i + 1]; + if (x < 0 || y < 0 || x >= W || y >= H) continue; + if (mode === 2) { + S.sh[y * W + x] = min(S.sh[y * W + x], 0.55); + continue; + } + const c = p[i + 2]; + const q = (y * W + x) * 3; + if (mode === 1) { + buf[q] = 255; + buf[q + 1] = 255; + buf[q + 2] = 255; + } else if (mode === 3) { + buf[q] = min(255, (c >> 16) + 60); + buf[q + 1] = min(255, ((c >> 8) & 255) + 45); + buf[q + 2] = min(255, (c & 255) + 20); + } else if (c < 0) { + buf[q] = glow[0]; + buf[q + 1] = glow[1]; + buf[q + 2] = glow[2]; + } else { + buf[q] = c >> 16; + buf[q + 1] = (c >> 8) & 255; + buf[q + 2] = c & 255; + } + } +} +function glyph(buf, code, x0, y0, r, g, b) { + for (let j = 0; j < 5; j++) { + const bits = (code >> (3 * (4 - j))) & 7; + for (let i = 0; i < 3; i++) if ((bits >> (2 - i)) & 1) put(buf, x0 + i, y0 + j, r, g, b); + } +} +function number(buf, v, x0, y0, r, g, b, len) { + let s = String(v); + while (s.length < len) s = "0" + s; + for (let i = 0; i < s.length; i++) glyph(buf, GLYPH[s.charCodeAt(i) - 48], x0 + i * 4, y0, r, g, b); +} +function ring(buf, cx, cy, R, w, c) { + for (let y = floor(cy - R - w); y <= cy + R + w; y++) + for (let x = floor(cx - R - w); x <= cx + R + w; x++) { + const d = hypot(x - cx, y - cy); + if (abs(d - R) < w) put(buf, x, y, c[0], c[1], c[2]); + } +} +function disc(buf, cx, cy, R, r, g, b) { + for (let y = floor(cy - R); y <= cy + R; y++) + for (let x = floor(cx - R); x <= cx + R; x++) if (hypot(x - cx, y - cy) <= R) put(buf, x, y, r, g, b); +} + +function drawGround(S, si) { + const buf = S.buf; + const pal = PALS[si]; + const base = floor(S.scroll); + const t = S.t; + for (let y = 0; y < H; y++) { + const wy = base + H - 1 - y; + const ro = (wy & 255) * W; + for (let x = 0; x < W; x++) { + let k = S.ter[ro + x]; + if (k === CAP && (t * 1.6 + hash(x, wy, 3) * 4) % 2 > 1.55) k = WAVE; + const q = (y * W + x) * 3; + let r = pal[k * 3]; + let g = pal[k * 3 + 1]; + let b = pal[k * 3 + 2]; + if (si === 2 && (k === FLDA || k === ROOF) && (x + wy) % 2 === 0 && hash(x, wy, 5) < 0.4) ((r = 255), (g = 214), (b = 110)); + if (si === 3 && k === DEEP) { + const h = hash(x, wy, 6); + if (h < 0.035) r = g = b = 90 + 4600 * h * (0.7 + 0.3 * sin(t * 4 + h * 900)); + } + buf[q] = r; + buf[q + 1] = g; + buf[q + 2] = b; + } + } + if (si === 3) { + // A ringed planet far below, drifting by slower than the station. + const py = ((S.scroll * 0.4) % 330) - 60; + const px = 44; + for (let y = max(0, floor(py - 26)); y < min(H, py + 26); y++) + for (let x = 0; x < W; x++) { + const k = S.ter[((base + H - 1 - y) & 255) * W + x]; + if (k > SHAL && k !== CAP) continue; + const dx = x - px; + const dy = y - py; + const u = (dx * 0.94 + dy * 0.34) / 31; + const v = (dy * 0.94 - dx * 0.34) / 6; + const onRing = abs(u * u + v * v - 1) < 0.16; + const inside = dx * dx + dy * dy < 19 * 19; + const q = (y * W + x) * 3; + if (inside && !(onRing && v > 0)) { + const band = floor((dy + 40) / 5) % 2; + const lit = dx * 0.7 + dy * 0.5 > 8 ? 0.5 : 1; + buf[q] = (band ? 220 : 186) * lit; + buf[q + 1] = (band ? 150 : 104) * lit; + buf[q + 2] = (band ? 96 : 62) * lit; + } else if (onRing) { + buf[q] = 210; + buf[q + 1] = 196; + buf[q + 2] = 160; + } + } + } +} + +export function draw(display, params, time) { + const S = params.s; + if (!S || !S.started) return; + const buf = S.buf; + const sh = S.sh; + const si = min(3, floor(S.stage * 4)); + const t = S.t; + drawGround(S, si); + // Gunboats on the water, each with its wake and a turret that follows the fighter. + for (const b of S.boats) { + const y = boatY(S, b); + for (let j = 1; j < 8; j++) { + const o = 1 + (j >> 1); + const a = 1 - j / 9; + for (const s of [-1, 1]) put(buf, b.x + s * o, y - 6 - j, 200 * a + 30, 225 * a + 40, 255 * a); + } + spr(S, BOAT, b.x - 3, y - 6, b.fl > 0 ? 1 : 0); + for (let j = -1; j <= 1; j++) for (let i = -1; i <= 1; i++) put(buf, b.x + i, y - 3 + j, 90, 95, 110); + for (let i = 1; i <= 3; i++) put(buf, b.x + cos(b.a) * i, y - 3 + sin(b.a) * i, 30, 30, 40); + } + // Shadows: the clouds' and the aircraft's, a little below and to the right. + // Thick fire dims the ground, the way bullet-hell shooters dim their backdrops so the bullets read. + const C = CLOUDS[si]; + const cbase = floor(S.scroll * CLOUD); + const dim = 1 - 0.36 * S.fury; + const cb = S.cb; + cb.fill(0); + for (const c of S.clouds) { + const sy = cbase + H - 1 - c.cw; + for (let j = 0; j < c.h; j++) { + const y = sy + j; + if (y < 0 || y >= H) continue; + for (let i = 0; i < c.w; i++) { + const x = c.x + i; + if (x >= 0 && x < W && c.m[j * c.w + i]) cb[y * W + x] = c.m[j * c.w + i]; + } + } + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) sh[y * W + x] = (x >= 6 && y >= 9 && cb[(y - 9) * W + x - 6] ? C[5] : 1) * dim; + const B = S.boss; + for (const e of S.en) if (e.y > -9) spr(S, e.k ? DART : FIGHTER, round(e.x) + 2, round(e.y) + 6, 2); + if (B) spr(S, BOSS, round(B.x) - 22, round(B.y) + 9, 2); + const P = S.pl; + if (P.dead <= 0) spr(S, PLAYER, round(P.x) + 1, round(P.y) + 3, 2); + for (let i = 0; i < N; i++) { + const s = sh[i]; + if (s < 1) { + buf[i * 3] *= s; + buf[i * 3 + 1] *= s; + buf[i * 3 + 2] *= s; + } + } + // Clouds. + for (let i = 0; i < N; i++) { + const l = cb[i]; + if (!l) continue; + const k = l === 1 ? C[3] : l === 3 ? 1.12 : 1; + const a = l === 3 ? C[4] + 0.12 : C[4]; + const q = i * 3; + buf[q] += (min(255, C[0] * k) - buf[q]) * a; + buf[q + 1] += (min(255, C[1] * k) - buf[q + 1]) * a; + buf[q + 2] += (min(255, C[2] * k) - buf[q + 2]) * a; + } + // Capsules: they blink red and blue and carry a P. + for (const c of S.caps) { + const red = floor(t * 7) % 2; + for (let j = 0; j < 7; j++) + for (let i = 0; i < 5; i++) { + if ((i === 0 || i === 4) && (j === 0 || j === 6)) continue; + put(buf, c.x - 2 + i, c.y - 3 + j, red ? 230 : 40, red ? 40 : 90, red ? 50 : 240); + } + glyph(buf, GLYPH[10], round(c.x) - 1, round(c.y) - 2, 255, 255, 255); + } + for (const e of S.en) if (e.y > -9) spr(S, e.k ? DART : FIGHTER, round(e.x) - 3, round(e.y) - 3, e.fl > 0 ? 1 : 0); + if (B) { + const p = 0.5 + 0.5 * sin(t * 9); + const f = S.fury; + const glow = [120 + 135 * f + 60 * p, 230 - 170 * f + 25 * p * (1 - f), 255 - 215 * f]; + const dying = B.st === 2 && floor(t * 16) % 2 === 0; + const bx = round(B.x) - 28; + const by = round(B.y); + // Engine flames from its four nozzles, upwards. + for (const ex of [15, 16, 17, 38, 39, 40]) + for (let j = 1; j <= 2 + ((t * 20 + ex) % 3 | 0); j++) put(buf, bx + ex, by - j, 255, 230 - j * 50, 120 - j * 40); + spr(S, BOSS, bx, by, dying ? 1 : B.fl > 0 && floor(t * 20) % 3 === 0 ? 3 : 0, glow); + } + if (P.dead <= 0 && (P.inv <= 0 || floor(t * 12) % 2 === 0)) { + const x0 = round(P.x) - 4; + const y0 = round(P.y) - 5; + const FL = [ + [255, 255, 220], + [255, 220, 80], + [255, 140, 30], + [200, 60, 20], + ]; + for (const ex of [3, 5]) { + const L = 2 + ((t * 24 + ex) % 3 | 0); + for (let j = 0; j < L; j++) put(buf, x0 + ex, y0 + 10 + j, ...FL[min(3, j + (L < 3 ? 1 : 0))]); + } + spr(S, PLAYER, x0, y0, 0); + const a = (t - P.pu) / 0.35; + if (a < 1) ring(buf, P.x, P.y, 3 + a * 9, 0.7, [180, 255, 255]); + } + for (const s of S.ps) { + put(buf, s.x, s.y, 255, 255, 230); + put(buf, s.x, s.y + 1, 255, 240, 120); + put(buf, s.x, s.y + 2, 255, 200, 60); + } + // Explosions: a white flash, then a ring that cools from yellow to red, and sparks. + for (const f of S.fx) { + const a = (t - f.t0) / f.d; + if (a < 0 || a >= 1) continue; + const R = (1.5 + 4.5 * a) * f.s; + if (a < 0.16) disc(buf, f.x, f.y, (1 + 1.6 * (a / 0.16)) * f.s, 255, 255, 235); + else { + if (a < 0.5) disc(buf, f.x, f.y, R * 0.5, 255, a < 0.3 ? 210 : 120, 40); + ring(buf, f.x, f.y, R, 0.75, a < 0.45 ? [255, 220, 80] : a < 0.72 ? [255, 130, 30] : [180, 46, 24]); + } + if (a < 0.85) for (let i = 0; i < f.sp.length; i += 2) put(buf, f.x + f.sp[i] * a * f.d, f.y + f.sp[i + 1] * a * f.d, 255, 245, 170); + } + // Enemy bullets, bright on top of everything, in a glow that grows with the fire. + const hr = 1 + round(2.4 * S.fury); + if (hr > 1) + for (const b of S.eb) { + const c = BUL[b.c]; + for (let j = -hr; j <= hr; j++) + for (let i = -hr; i <= hr; i++) { + const x = round(b.x) + i; + const y = round(b.y) + j; + if (abs(i) + abs(j) > hr || x < 0 || y < 0 || x >= W || y >= H) continue; + const q = (y * W + x) * 3; + buf[q] += (c[0] - buf[q]) * 0.4; + buf[q + 1] += (c[1] - buf[q + 1]) * 0.4; + buf[q + 2] += (c[2] - buf[q + 2]) * 0.4; + } + } + for (const b of S.eb) { + const c = BUL[b.c]; + const x = round(b.x); + const y = round(b.y); + put(buf, x - 1, y, ...c); + put(buf, x + 1, y, ...c); + put(buf, x, y - 1, ...c); + put(buf, x, y + 1, ...c); + if (b.big) for (const [i, j] of [[-1, -1], [1, -1], [-1, 1], [1, 1]]) put(buf, x + i, y + j, c[0] * 0.6, c[1] * 0.6, c[2] * 0.6); + put(buf, x, y, 255, 255, 255); + } + for (const p of S.pop) { + const a = (t - p.t0) / 0.9; + if (a > 0.75 && floor(t * 20) % 2) continue; + const s = String(p.v); + number(buf, p.v, round(p.x - s.length * 2), round(p.y - a * 9), 255, 240, 120, 0); + } + // The bomb: a shock ring from the fighter, and the whole screen flashes white. + if (S.bomb) { + const r = (t - S.bomb.t0) * 150; + ring(buf, S.bomb.x, S.bomb.y, r, 1.6, [200, 250, 255]); + } + if (S.flash > 0) { + const a = min(1, S.flash); + for (let i = 0; i < N * 3; i++) buf[i] += (255 - buf[i]) * a * 0.85; + } + // The score and the lives, small, on a dark strip. + for (let y = 0; y < 7; y++) + for (let x = 0; x < W; x++) { + if (x > 25 && x < 62 - 6 * P.lives) continue; + const q = (y * W + x) * 3; + buf[q] *= 0.18; + buf[q + 1] *= 0.18; + buf[q + 2] *= 0.18; + } + number(buf, S.score % 1000000, 1, 1, 255, 255, 255, 6); + for (let i = 0; i < P.lives; i++) spr(S, LIFE, 58 - i * 6, 2, 0); + for (let p = 0; p < N; p++) { + const q = p * 3; + display.setPixel(p % W, (p / W) | 0, min(255, buf[q]) | 0, min(255, buf[q + 1]) | 0, min(255, buf[q + 2]) | 0); + } +} From 42a1d21a1ea12834cfc7748a24399f049c799f9b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:52:35 +0000 Subject: [PATCH 26/33] breath: monopodial tree, clouds, cough puffs --- incoming/pattern-study/patterns/breath.js | 61 ++++++++++++++++------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/incoming/pattern-study/patterns/breath.js b/incoming/pattern-study/patterns/breath.js index b0d032720..b6d23b9d3 100644 --- a/incoming/pattern-study/patterns/breath.js +++ b/incoming/pattern-study/patterns/breath.js @@ -35,6 +35,20 @@ function vn(x, y) { return a + (b - a) * su + (c - a) * sv + (a - b - c + hash(xi + 1, yi + 1)) * su * sv; } +// A tiling noise, 64 by 128, for the breath clouds and the smog, read +// smoothly at any offset. +function vt(x, y, px, py) { + 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) % px, y1 = (yi + 1) % py; + const a = hash(xi, yi), b = hash(x1, yi), c = hash(xi, y1); + return a + (b - a) * su + (c - a) * sv + (a - b - c + hash(x1, y1)) * su * sv; +} +const NT = Float32Array.from({ length: N }, (_, p) => 0.62 * vt((p & 63) / 8, (p >> 6) / 8, 8, 16) + 0.38 * vt((p & 63) / 4, (p >> 6) / 4, 16, 32)); +function nt(x, y) { + const xi = floor(x), yi = floor(y), u = x - xi, v = y - yi, x0 = xi & 63, x1 = (xi + 1) & 63, y0 = (yi & 127) * W, y1 = ((yi + 1) & 127) * W; + const a = NT[y0 + x0] + (NT[y0 + x1] - NT[y0 + x0]) * u; + return a + (NT[y1 + x0] + (NT[y1 + x1] - NT[y1 + x0]) * u - a) * v; +} + // The diaphragm under the lungs breathed out, x then y: the right dome higher // over the liver, the heart sitting in the dip between the domes. const DK = [-1, 118.5, 5, 113.8, 9, 108.4, 13, 103.8, 17, 101.6, 21, 101, 25, 101.8, 29, 104.2, 33, 106, 37, 106.6, 41, 106.6, 45, 105.8, 49, 106.8, 53, 110.2, 57, 115, 60, 117.8, 65, 122.5]; @@ -57,7 +71,7 @@ const LUNGS = [ // Airway walls by generation (9 is the channel through the larynx), and how // strongly each shows over the lung. const WC = [[226, 216, 204], [226, 216, 204], [228, 200, 206], [226, 192, 206], [224, 186, 206], [222, 182, 206], [220, 178, 206], [218, 176, 206], [216, 174, 206], [0, 0, 0]]; -const AG = [1, 1, 0.95, 0.9, 0.85, 0.78, 0.7, 0.62, 0.56, 0]; +const AG = [1, 1, 0.95, 0.9, 0.82, 0.72, 0.62, 0.52, 0.46, 0]; // The cough: seconds and lung volume, and the moments of the three jolts. const CK = [0, 0, 0.32, 0.97, 0.44, 0.97, 0.54, 0.66, 0.84, 0.66, 0.94, 0.38, 1.24, 0.38, 1.34, 0.12, 1.9, 0]; const CT = [0.44, 0.84, 1.24]; @@ -109,10 +123,18 @@ function grow(P, x0, y0, dx, dy, pts, g, r, d) { P.cb.push(0.84 + 0.32 * rnd()); return; } - let ux = cx - x0, uy = cy - y0, ul = hypot(ux, uy); + // The lobes split along the line from the branch to their centroid; past + // them each set is cut across its long axis, a near half and a far one. + let ux = cx - x0, uy = cy - y0, ul = hypot(ux, uy), ox = x0, oy = y0; + if (g > 2) { + let sxx = 0, syy = 0, sxy = 0; + for (const i of pts) (sxx += (ax[i] - cx) ** 2), (syy += (ay[i] - cy) ** 2), (sxy += (ax[i] - cx) * (ay[i] - cy)); + const th = 0.5 * atan2(2 * sxy, sxx - syy) + PI / 2; + (ux = cos(th)), (uy = sin(th)), (ul = 1), (ox = cx), (oy = cy); + } if (ul < 0.5) (ux = dx), (uy = dy), (ul = hypot(dx, dy)); (ux /= ul), (uy /= ul); - const side = (i) => (ax[i] - x0) * uy - (ay[i] - y0) * ux; + const side = (i) => (ax[i] - ox) * uy - (ay[i] - oy) * ux; let A = pts.filter((i) => side(i) > 0), B = pts.filter((i) => side(i) <= 0); if (A.length < 2 || B.length < 2) { const s = pts.slice().sort((i, j) => side(i) - side(j)); @@ -141,7 +163,7 @@ function build(P) { const { sd, lid, own, cap, cph, GL } = P; for (let p = 0; p < N; p++) { const x = p & 63, y = p >> 6, sl = d0(x + 0.5) - d0(x - 0.5), cut = (y - d0(x) + 0.6) / sqrt(1 + sl * sl); - const a = max(sdPoly(LUNGS[0], x, y), cut), b = max(sdPoly(LUNGS[1], x, y), cut); + const a = y < 15 ? 6 : max(sdPoly(LUNGS[0], x, y), cut), b = y < 15 ? 6 : max(sdPoly(LUNGS[1], x, y), cut); sd[p] = min(a, b) + 0.9 * (vn(x * 0.32, y * 0.32) - 0.5); lid[p] = a < b ? 0 : 1; cap[p] = hash(x * 3 + 1, y * 7) > 0.6 ? 0.4 + 0.6 * hash(x, y + 99) : 0; @@ -206,7 +228,8 @@ function under(P) { const hy = 3.4 + 0.03 * ax * ax; if (ax < 9.5) lay(p, 200, 192, 176, 0.5 * cl(1 - abs(y - hy) / 0.9, 0, 1)); const top = 6 + 2.2 * max(0, 1 - ax / 2.4), hw = 7.4 - 0.42 * (y - 6), e = min(y - top, 14.6 - y, hw - ax); - if (e > -0.5) lay(p, 206, 196, 184, cl(e + 0.5, 0, 1) * (0.42 + 0.2 * exp(-((ax / 1.8) ** 2)) + 0.25 * sm(1.2, 0.2, e))); + // The thyroid cartilage, its left plate lit and the right in shade. + if (e > -0.5) lay(p, 206, 196, 184, cl(e + 0.5, 0, 1) * (0.42 + 0.2 * exp(-((ax / 1.8) ** 2)) + 0.25 * sm(1.2, 0.2, e)) * (x < 32 ? 1.05 : 0.78)); if (y > 15 && y < 17.6 && ax < 4.9) lay(p, 214, 204, 192, 0.55 * cl(4.9 - ax, 0, 1)); } } @@ -236,7 +259,7 @@ export function update(dt, input, params) { const u = c - tc; if (u > 0 && u < 0.7) P.bf.push(1 - u * 3.2), (P.jolt = max(P.jolt, 2.4 * exp(-u / 0.08))); } - if (P.cn < 3 && c > CT[P.cn] + 0.28) P.puffs.push([0, 1.3, rnd()]), P.cn++; + if (P.cn < 3 && c > CT[P.cn] + 0.28) P.puffs.push([0, 2, rnd()]), P.cn++; if (c > 1.9) (P.ct = -1), (P.ph = 0.9); } else { // In for 38% of a breath, out for half, then a short rest. @@ -276,11 +299,12 @@ function alveoli(P) { alvA.fill(0); for (let i = 0; i < ax.length; i++) { fw(P, ax[i], ay[i]); - const cx = FX, cy = FY, r = R * as[i]; + const cx = FX, cy = FY, r = R * as[i], lim = (r + 0.5) ** 2; for (let y = max(0, ceil(cy - r - 0.5)); y <= min(H - 1, floor(cy + r + 0.5)); y++) for (let x = max(0, ceil(cx - r - 0.5)); x <= min(W - 1, floor(cx + r + 0.5)); x++) { - const dx = x - cx, dy = y - cy, e = sqrt(dx * dx + dy * dy), a = cl(r + 0.5 - e, 0, 1), p = y * W + x; - if (a <= 0) continue; + const dx = x - cx, dy = y - cy, e2 = dx * dx + dy * dy; + if (e2 >= lim) continue; + const e = sqrt(e2), a = cl(r + 0.5 - e, 0, 1), p = y * W + x; const q = e / r, hx = dx + 0.4 * r, hy = dy + 0.45 * r, hl = max(0, 1 - sqrt(hx * hx + hy * hy) / (0.75 * r)); const s = 0.72 + 0.28 * sqrt(max(0, 1 - q * q)) + 0.38 * hl * hl; if (a * s > alvA[p] * alvS[p]) (alvA[p] = a), (alvS[p] = s), (alvK[p] = i); @@ -296,11 +320,12 @@ function tree(P) { fw(P, S[j], S[j + 1]); const x0 = FX, y0 = FY; fw(P, S[j + 2], S[j + 3]); - const ex = FX - x0, ey = FY - y0, l2 = ex * ex + ey * ey || 1e-6, r = S[j + 4] * dil, g = S[j + 5], da = S[j + 6], db = S[j + 7]; + const ex = FX - x0, ey = FY - y0, l2 = ex * ex + ey * ey || 1e-6, r = S[j + 4] * dil, g = S[j + 5], da = S[j + 6], db = S[j + 7], lim = (r + 1.7) ** 2, hr = min(1, r); for (let y = max(0, floor(min(y0, FY) - r - 1)); y <= min(H - 1, ceil(max(y0, FY) + r + 1)); y++) for (let x = max(0, floor(min(x0, FX) - r - 1)); x <= min(W - 1, ceil(max(x0, FX) + r + 1)); x++) { - const t = cl(((x - x0) * ex + (y - y0) * ey) / l2, 0, 1), px = x - x0 - ex * t, py = y - y0 - ey * t, e = sqrt(px * px + py * py); - const a = cl(r + 0.5 - e, 0, 1), p = y * W + x, h = cl(r + 1.7 - e, 0, 1); + const t = cl(((x - x0) * ex + (y - y0) * ey) / l2, 0, 1), px = x - x0 - ex * t, py = y - y0 - ey * t, e2 = px * px + py * py; + if (e2 >= lim) continue; + const e = sqrt(e2), a = cl(r + 0.5 - e, 0, 1), p = y * W + x, h = cl(r + 1.7 - e, 0, 1) * hr; if (h > trH[p]) trH[p] = h; if (a > trA[p]) (trA[p] = a), (trS[p] = da + (db - da) * t), (trD[p] = trS[p] * dm), (trV[p] = e / r), (trG[p] = g); } @@ -323,7 +348,7 @@ function ribs(P) { if (i === 0) { for (let n = 0; n <= 50; n++) { const u = n / 50; - dot(32 + s * (2.5 + 22 * u), 19 - 4.5 * u + 1.6 * sin(PI * u) - 1.2 * v, 0.9, 1); + dot(32 + s * (4.5 + 21 * u), 18.6 - 4.2 * u + 1.4 * sin(PI * u) - 1.2 * v, 0.5 + 0.4 * (2 * u - 1) ** 2, 0.85); } continue; } @@ -345,7 +370,7 @@ export function draw(display, params) { const pal = (c, w, s) => [0, 1, 2].map((i) => c[i] * wC + w[i] * wW + s[i] * wS); const bgT = pal([3, 7, 18], [16, 26, 48], [62, 50, 32]), bgB = pal([2, 4, 12], [8, 14, 30], [36, 28, 20]), glo = pal([8, 18, 40], [22, 38, 64], [40, 30, 18]); const cold = pal([150, 205, 255], [215, 238, 255], [196, 178, 128]), warm = pal([255, 196, 160], [255, 224, 206], [190, 160, 130]); - const puffC = pal([255, 214, 196], [236, 242, 255], [168, 158, 144]), puffK = 0.34 * wC + 0.95 * wW + 0.6 * wS; + const puffC = pal([255, 214, 196], [236, 242, 255], [168, 158, 144]), puffK = 0.4 * wC + 1.5 * wW + 0.7 * wS; const oxA = [240 - 64 * wS, 46 + 26 * wS, 58 + 20 * wS], deA = [74, 58, 156], oxF = [80, 10, 30], deF = [24, 14, 50]; for (let y = 0; y < H; y++) SX[y] = 1 + 0.085 * v * (0.25 + 0.75 * sm(15, 115, y)); for (let x = 0; x < W; x++) KY[x] = 1 + 0.14 * v * GX[x]; @@ -355,15 +380,15 @@ export function draw(display, params) { // The breath leaving at the top: each puff rises, spreads and thins. const pf = []; for (const [age, str, sd0] of P.puffs) { - const R = 3 + (6 + 6 * wW) * age; - pf.push(32 + (sd0 - 0.5) * 8, 7 - 5 * age, R, str * sm(0, 0.3, age) * exp(-age / (0.9 + 1.4 * wW)), sd0 * 40); + const R = 3 + (5 + 8 * wW) * age; + pf.push(32 + (sd0 - 0.5) * 8, 6 - (4.6 - 1.6 * wW) * age, R, str * sm(0, 0.25, age) * exp(-age / (0.9 + 1.6 * wW)), sd0 * 40); } const hs = 1 / (1 - 0.035 * hb); for (let y = 0, p = 0; y < H; y++) for (let x = 0; x < W; x++, p++) { const fy = y / 127, gl = GL[p]; let hz = 1; - if (wS > 0) hz = mix(1, 0.6 + 0.8 * vn(x * 0.07 + t * 0.3, y * 0.045 - t * 0.06), wS); + if (wS > 0) hz = mix(1, 0.6 + 0.8 * nt(x * 0.55 + t * 2.4, y * 0.36 - t * 0.5), wS); let r = (bgT[0] + (bgB[0] - bgT[0]) * fy + glo[0] * gl) * hz, g = (bgT[1] + (bgB[1] - bgT[1]) * fy + glo[1] * gl) * hz, b = (bgT[2] + (bgB[2] - bgT[2]) * fy + glo[2] * gl) * hz; const xr = cl(32 + (x - 32) / SX[y], 0, 62.999), yr = cl(A0 + (y - A0) / KY[x], 0, 126.999); const xi = xr | 0, yi = yr | 0, fx = xr - xi, fz = yr - yi, q = yi * W + xi; @@ -435,7 +460,7 @@ export function draw(display, params) { let pa = 0; for (let j = 0; j < pf.length; j += 5) { const dx = (x - pf[j]) / (pf[j + 2] * 1.4), dy = (y - pf[j + 1]) / pf[j + 2], e = dx * dx + dy * dy; - if (e < 4) pa += pf[j + 3] * exp(-e * 1.5) * (0.55 + 0.9 * (vn(x * 0.2 + pf[j + 4], y * 0.2 + t * 0.9) - 0.5)); + if (e < 4) pa += pf[j + 3] * exp(-e * 1.4) * cl(0.4 + 1.4 * (nt(x * 1.4 + pf[j + 4], y * 1.4 + t * 7) - 0.5) + 0.5 * exp(-e * 3), 0, 1.4); } if (pa > 0) { const k = min(1, pa * puffK); From fe7944079d9f9ef55fe1f84436c6b3ff9056ed9d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 01:59:13 +0000 Subject: [PATCH 27/33] breath: ribs over the lungs, dimmer abdomen, under 25 KB --- incoming/pattern-study/patterns/breath.js | 124 +++++++++------------- 1 file changed, 49 insertions(+), 75 deletions(-) diff --git a/incoming/pattern-study/patterns/breath.js b/incoming/pattern-study/patterns/breath.js index b6d23b9d3..d7d807eec 100644 --- a/incoming/pattern-study/patterns/breath.js +++ b/incoming/pattern-study/patterns/breath.js @@ -11,9 +11,9 @@ // @matrix 64x128 // @knobs Air=0..1, Rate=0.2..3, Depth=0..1, Altitude=0..1 // -// Knob 1: Air. What is breathed: clear cool air at 0, cold winter air at 0.5 where every breath comes out as a white cloud at the top, and city smog at 1 that browns the sky, turns the air in the airways yellow-grey and leaves soot in the alveoli. -// Knob 2: Rate. Breaths a minute: about 9 asleep at 0.2, 17 at 2 (one every 3.5 s), and a shallow pant of 33 at 3. -// Knob 3: Depth. A quiet breath at 0, where the domes of the diaphragm drop 6 LEDs, up to a deep sigh at 1 that drops them twice as far and spreads the ribs wide. +// Knob 1: Air. What is breathed: clear cool air at 0, cold winter air at 0.5 where every breath comes out as a white cloud at the top, and city smog at 1 that browns the sky and leaves soot in the alveoli. +// Knob 2: Rate. Breaths a minute: about 9 asleep at 0.2, 17 at 2 (one every 3.5 s), and a shallow pant of 34 at 3. +// Knob 3: Depth. A quiet breath at 0, where the domes of the diaphragm drop 7 LEDs, up to a deep sigh at 1 that drops them 14 and lifts and spreads the ribs twice as far. // Knob 4: Altitude. Sea level at 0, the top of Everest at 1: each breath brings less oxygen, so the alveoli flush only to a dusky purple, and the breathing quickens and deepens on its own. // Buttons: K1 tap makes the lungs cough: a quick gasp, then three jolts that blow warm air up the tree and out in puffs. @@ -35,20 +35,6 @@ function vn(x, y) { return a + (b - a) * su + (c - a) * sv + (a - b - c + hash(xi + 1, yi + 1)) * su * sv; } -// A tiling noise, 64 by 128, for the breath clouds and the smog, read -// smoothly at any offset. -function vt(x, y, px, py) { - 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) % px, y1 = (yi + 1) % py; - const a = hash(xi, yi), b = hash(x1, yi), c = hash(xi, y1); - return a + (b - a) * su + (c - a) * sv + (a - b - c + hash(x1, y1)) * su * sv; -} -const NT = Float32Array.from({ length: N }, (_, p) => 0.62 * vt((p & 63) / 8, (p >> 6) / 8, 8, 16) + 0.38 * vt((p & 63) / 4, (p >> 6) / 4, 16, 32)); -function nt(x, y) { - const xi = floor(x), yi = floor(y), u = x - xi, v = y - yi, x0 = xi & 63, x1 = (xi + 1) & 63, y0 = (yi & 127) * W, y1 = ((yi + 1) & 127) * W; - const a = NT[y0 + x0] + (NT[y0 + x1] - NT[y0 + x0]) * u; - return a + (NT[y1 + x0] + (NT[y1 + x1] - NT[y1 + x0]) * u - a) * v; -} - // The diaphragm under the lungs breathed out, x then y: the right dome higher // over the liver, the heart sitting in the dip between the domes. const DK = [-1, 118.5, 5, 113.8, 9, 108.4, 13, 103.8, 17, 101.6, 21, 101, 25, 101.8, 29, 104.2, 33, 106, 37, 106.6, 41, 106.6, 45, 105.8, 49, 106.8, 53, 110.2, 57, 115, 60, 117.8, 65, 122.5]; @@ -70,8 +56,8 @@ const LUNGS = [ ]; // Airway walls by generation (9 is the channel through the larynx), and how // strongly each shows over the lung. -const WC = [[226, 216, 204], [226, 216, 204], [228, 200, 206], [226, 192, 206], [224, 186, 206], [222, 182, 206], [220, 178, 206], [218, 176, 206], [216, 174, 206], [0, 0, 0]]; -const AG = [1, 1, 0.95, 0.9, 0.82, 0.72, 0.62, 0.52, 0.46, 0]; +const WC = Array.from({ length: 10 }, (_, g) => (g < 2 ? [226, 216, 204] : [232 - 2 * g, 208 - 4 * g, 206])); +const AG = Array.from({ length: 10 }, (_, g) => (g < 2 ? 1 : g > 8 ? 0 : 1.1 - 0.08 * g)); // The cough: seconds and lung volume, and the moments of the three jolts. const CK = [0, 0, 0.32, 0.97, 0.44, 0.97, 0.54, 0.66, 0.84, 0.66, 0.94, 0.38, 1.24, 0.38, 1.34, 0.12, 1.9, 0]; const CT = [0.44, 0.84, 1.24]; @@ -87,6 +73,10 @@ function sdPoly(v, px, py) { } return s * sqrt(d); } +// The colour being built for one LED, and mixing toward another by k. +const C = F32(3); +const DA = [74, 58, 156]; +const bl = (r, g, b, k) => ((C[0] += (r - C[0]) * k), (C[1] += (g - C[1]) * k), (C[2] += (b - C[2]) * k)); function samp(A, x, y) { x = cl(x, 0, 62.999); y = cl(y, 0, 126.999); @@ -96,17 +86,18 @@ function samp(A, x, y) { export function setup(params) { const P = (params.s = { - t: 0, ph: 0.68, lp: 0.68, vol: 0.6, D: 0.5, air: 0, alt: 0, ct: -1, cv: 0, jolt: 0, hp: 0, hb: 0, envIn: 0, envOut: 0, Fin: -1, Fx: 2, bf: [], puffs: [], - sd: F32(N), lid: new Int8Array(N), own: new Int16Array(N).fill(-1), cap: F32(N), cph: F32(N), und: F32(N * 3), ua: F32(N), GL: F32(N), - SX: F32(H), KY: F32(W), alvA: F32(N), alvS: F32(N), alvK: new Int16Array(N), trA: F32(N), trD: F32(N), trV: F32(N), trS: F32(N), trG: new Int8Array(N), rib: F32(N), O: F32(N * 3), + t: 0, ph: 0.68, lp: 0.68, vol: 0.6, D: 0.5, air: 0, ct: -1, jolt: 0, hp: 0, hb: 0, envIn: 0, envOut: 0, Fin: -1, Fx: 2, bf: [], puffs: [], + sd: F32(N), own: new Int16Array(N).fill(-1), und: F32(N * 3), ua: F32(N), GL: F32(N), + SX: F32(H), KY: F32(W), alvA: F32(N), alvS: F32(N), alvK: new Int16Array(N), trA: F32(N), trV: F32(N), trS: F32(N), trG: new Int8Array(N), rib: F32(N), O: F32(N * 3), seg: [], ax: [], ay: [], as: [], soot: [], cd: [], cb: [], trH: F32(N), }); build(P); } function addSeg(P, x0, y0, x1, y1, r, g, d) { - P.seg.push(x0, y0, x1, y1, r, g, d, d + hypot(x1 - x0, y1 - y0)); - return d + hypot(x1 - x0, y1 - y0); + const e = d + hypot(x1 - x0, y1 - y0); + P.seg.push(x0, y0, x1, y1, r, g, d, e); + return e; } // Volume-filling branching: a branch's alveoli are split by the line from its @@ -146,7 +137,7 @@ function grow(P, x0, y0, dx, dy, pts, g, r, d) { for (const i of Q) (qx += ax[i]), (qy += ay[i]); (qx = qx / Q.length - x0), (qy = qy / Q.length - y0); let a = atan2(qy, qx), da = a - pa; - da = ((((da + PI) % (2 * PI)) + 2 * PI) % (2 * PI)) - PI; + da = atan2(sin(da), cos(da)); if (abs(da) > lim) a = pa + Math.sign(da) * lim; let len = max(1.2, hypot(qx, qy) * (Q.length <= 7 ? 0.55 : 0.4)), x1, y1; for (;;) { @@ -160,14 +151,11 @@ function grow(P, x0, y0, dx, dy, pts, g, r, d) { } function build(P) { - const { sd, lid, own, cap, cph, GL } = P; + const { sd, own, GL } = P; for (let p = 0; p < N; p++) { const x = p & 63, y = p >> 6, sl = d0(x + 0.5) - d0(x - 0.5), cut = (y - d0(x) + 0.6) / sqrt(1 + sl * sl); const a = y < 15 ? 6 : max(sdPoly(LUNGS[0], x, y), cut), b = y < 15 ? 6 : max(sdPoly(LUNGS[1], x, y), cut); sd[p] = min(a, b) + 0.9 * (vn(x * 0.32, y * 0.32) - 0.5); - lid[p] = a < b ? 0 : 1; - cap[p] = hash(x * 3 + 1, y * 7) > 0.6 ? 0.4 + 0.6 * hash(x, y + 99) : 0; - cph[p] = hash(x + 77, y) * 2 * PI; GL[p] = exp(-(((x - 32) / 26) ** 2) - (((y - 66) / 50) ** 2)); } // Alveoli on a jittered honeycomb inside the lungs. @@ -176,7 +164,7 @@ function build(P) { for (let i = 0; i < 29; i++) { const x = 1 + i * 2.25 + (j & 1) * 1.125 + (rnd() - 0.5) * 0.7, y = 19 + j * 1.95 + (rnd() - 0.5) * 0.7; if (x > 62.5 || y > 125 || samp(sd, x, y) > -1) continue; - pts[lid[round(y) * W + round(x)]].push(P.ax.length); + pts[x < 32 ? 0 : 1].push(P.ax.length); P.ax.push(x), P.ay.push(y), P.as.push(0.86 + 0.28 * rnd()), P.soot.push(rnd()); } P.ak = new Int16Array(P.ax.length); @@ -216,7 +204,7 @@ function under(P) { const x = p & 63, y = p >> 6, t = y - d0(x), ax = abs(x - 32); if (t > 3) { const xb = 37 + 3 * sin(y * 0.21), f = (1 - 0.55 * sm(3, 20, t)) * (0.7 + 0.5 * vn(x * 0.17, y * 0.17)) * (0.4 + 0.6 * sm(0.3, 2, abs(x - xb))); - x < xb ? lay(p, 70 * f, 22 * f, 22 * f, cl(t - 3, 0, 1)) : lay(p, 58 * f, 34 * f, 48 * f, cl(t - 3, 0, 1)); + x < xb ? lay(p, 60 * f, 20 * f, 22 * f, cl(t - 3, 0, 1)) : lay(p, 50 * f, 30 * f, 44 * f, cl(t - 3, 0, 1)); } // The diaphragm: a band of muscle with a lit upper face, its tendon pale // in the middle under the heart. @@ -241,7 +229,7 @@ export function update(dt, input, params) { dt = cl(dt || 0, 0, 0.1); P.t += dt; P.air = cl(k[0], 0, 1); - const rate = cl(k[1], 0.2, 3), dep = cl(k[2], 0, 1), alt = (P.alt = cl(k[3], 0, 1)); + const rate = cl(k[1], 0.2, 3), dep = cl(k[2], 0, 1), alt = cl(k[3], 0, 1); const bpm = (8 + 4.5 * rate + 12 * max(0, rate - 2) ** 2) * (1 + 0.45 * alt); P.D = (0.5 + 0.5 * dep) * (1 - 0.4 * sm(2, 3, rate)) * (1 + 0.22 * alt); if (input.btnPressed && input.btnPressed[0] && P.ct < 0) (P.ct = 0), (P.cv = P.vol), (P.cn = 0); @@ -295,11 +283,11 @@ function fw(P, xr, yr) { } function alveoli(P) { - const { alvA, alvS, alvK, ax, ay, as } = P, R = 1.02 + 0.52 * P.vol * P.D; + const { alvA, alvS, alvK, ax, ay, as } = P, rA = 1.02 + 0.52 * P.vol * P.D; alvA.fill(0); for (let i = 0; i < ax.length; i++) { fw(P, ax[i], ay[i]); - const cx = FX, cy = FY, r = R * as[i], lim = (r + 0.5) ** 2; + const cx = FX, cy = FY, r = rA * as[i], lim = (r + 0.5) ** 2; for (let y = max(0, ceil(cy - r - 0.5)); y <= min(H - 1, floor(cy + r + 0.5)); y++) for (let x = max(0, ceil(cx - r - 0.5)); x <= min(W - 1, floor(cx + r + 0.5)); x++) { const dx = x - cx, dy = y - cy, e2 = dx * dx + dy * dy; @@ -313,7 +301,7 @@ function alveoli(P) { } function tree(P) { - const { seg: S, trA, trD, trV, trS, trG, trH } = P, dm = 1 / P.dmax, dil = 1 + 0.1 * P.vol * P.D; + const { seg: S, trA, trV, trS, trG, trH } = P, dil = 1 + 0.1 * P.vol * P.D; trA.fill(0); trH.fill(0); for (let j = 0; j < S.length; j += 8) { @@ -327,7 +315,7 @@ function tree(P) { if (e2 >= lim) continue; const e = sqrt(e2), a = cl(r + 0.5 - e, 0, 1), p = y * W + x, h = cl(r + 1.7 - e, 0, 1) * hr; if (h > trH[p]) trH[p] = h; - if (a > trA[p]) (trA[p] = a), (trS[p] = da + (db - da) * t), (trD[p] = trS[p] * dm), (trV[p] = e / r), (trG[p] = g); + if (a > trA[p]) (trA[p] = a), (trS[p] = da + (db - da) * t), (trV[p] = e / r), (trG[p] = g); } } } @@ -356,7 +344,7 @@ function ribs(P) { const rx = hw - 3, ry = (5.5 + 0.55 * i) * (1 - 0.25 * v), n = ceil((rx + ry) * 2.2); for (let m = 0; m <= n; m++) { const th = (1.62 * m) / n; - dot(32 + s * (3 + rx * sin(th)), y0 + ry * (1 - cos(th)), 0.6, sm(0.2, 0.6, th)); + dot(32 + s * (3 + rx * sin(th)), y0 + ry * (1 - cos(th)), 0.7, sm(0.2, 0.6, th)); } } } @@ -364,14 +352,14 @@ function ribs(P) { export function draw(display, params) { const P = params.s; if (!P || !P.cox) return; - const { SX, KY, sd, own, cox, cb, airK, cap, cph, und, ua, alvA, alvS, alvK, trA, trD, trV, trS, trG, trH, rib, O, ak, soot, GL } = P; + const { SX, KY, sd, own, cox, cb, airK, und, ua, alvA, alvS, alvK, trA, trV, trS, trG, trH, rib, O, ak, soot, GL } = P, dm = 1 / P.dmax; const t = P.t, v = P.vol * P.D, air = P.air, hb = P.hb, { envIn, envOut, Fin, Fx, bf } = P; const wC = max(0, 1 - 2 * air), wW = 1 - abs(2 * air - 1), wS = max(0, 2 * air - 1); const pal = (c, w, s) => [0, 1, 2].map((i) => c[i] * wC + w[i] * wW + s[i] * wS); const bgT = pal([3, 7, 18], [16, 26, 48], [62, 50, 32]), bgB = pal([2, 4, 12], [8, 14, 30], [36, 28, 20]), glo = pal([8, 18, 40], [22, 38, 64], [40, 30, 18]); const cold = pal([150, 205, 255], [215, 238, 255], [196, 178, 128]), warm = pal([255, 196, 160], [255, 224, 206], [190, 160, 130]); - const puffC = pal([255, 214, 196], [236, 242, 255], [168, 158, 144]), puffK = 0.4 * wC + 1.5 * wW + 0.7 * wS; - const oxA = [240 - 64 * wS, 46 + 26 * wS, 58 + 20 * wS], deA = [74, 58, 156], oxF = [80, 10, 30], deF = [24, 14, 50]; + const puffC = pal([255, 214, 196], [236, 242, 255], [168, 158, 144]), puffK = 0.6 * wC + 1.5 * wW + 0.7 * wS; + const oxA = [240 - 64 * wS, 46 + 26 * wS, 58 + 20 * wS]; for (let y = 0; y < H; y++) SX[y] = 1 + 0.085 * v * (0.25 + 0.75 * sm(15, 115, y)); for (let x = 0; x < W; x++) KY[x] = 1 + 0.14 * v * GX[x]; alveoli(P); @@ -381,23 +369,21 @@ export function draw(display, params) { const pf = []; for (const [age, str, sd0] of P.puffs) { const R = 3 + (5 + 8 * wW) * age; - pf.push(32 + (sd0 - 0.5) * 8, 6 - (4.6 - 1.6 * wW) * age, R, str * sm(0, 0.25, age) * exp(-age / (0.9 + 1.6 * wW)), sd0 * 40); + pf.push(32 + (sd0 - 0.5) * 8, 6 - 3.2 * age, R, str * sm(0, 0.25, age) * exp(-age / (0.9 + 1.6 * wW)), sd0 * 40); } const hs = 1 / (1 - 0.035 * hb); for (let y = 0, p = 0; y < H; y++) for (let x = 0; x < W; x++, p++) { const fy = y / 127, gl = GL[p]; let hz = 1; - if (wS > 0) hz = mix(1, 0.6 + 0.8 * nt(x * 0.55 + t * 2.4, y * 0.36 - t * 0.5), wS); - let r = (bgT[0] + (bgB[0] - bgT[0]) * fy + glo[0] * gl) * hz, g = (bgT[1] + (bgB[1] - bgT[1]) * fy + glo[1] * gl) * hz, b = (bgT[2] + (bgB[2] - bgT[2]) * fy + glo[2] * gl) * hz; + if (wS > 0) hz = 1 + wS * (0.18 * sin(x * 0.2 + y * 0.05 + t * 0.6) + 0.18 * sin(y * 0.13 - x * 0.06 - t * 0.35)); + for (let c = 0; c < 3; c++) C[c] = (bgT[c] + (bgB[c] - bgT[c]) * fy + glo[c] * gl) * hz; const xr = cl(32 + (x - 32) / SX[y], 0, 62.999), yr = cl(A0 + (y - A0) / KY[x], 0, 126.999); const xi = xr | 0, yi = yr | 0, fx = xr - xi, fz = yr - yi, q = yi * W + xi; if (ua[q] + ua[q + 1] + ua[q + W] + ua[q + W + 1] > 0) { const w0 = (1 - fx) * (1 - fz) * ua[q], w1 = fx * (1 - fz) * ua[q + 1], w2 = (1 - fx) * fz * ua[q + W], w3 = fx * fz * ua[q + W + 1], a = 1 - w0 - w1 - w2 - w3; const o0 = q * 3, o2 = o0 + W * 3; - r = r * a + und[o0] * w0 + und[o0 + 3] * w1 + und[o2] * w2 + und[o2 + 3] * w3; - g = g * a + und[o0 + 1] * w0 + und[o0 + 4] * w1 + und[o2 + 1] * w2 + und[o2 + 4] * w3; - b = b * a + und[o0 + 2] * w0 + und[o0 + 5] * w1 + und[o2 + 2] * w2 + und[o2 + 5] * w3; + for (let c = 0; c < 3; c++) C[c] = C[c] * a + und[o0 + c] * w0 + und[o0 + 3 + c] * w1 + und[o2 + c] * w2 + und[o2 + 3 + c] * w3; } // The heart, dim behind the lungs, its apex down to the left. if (xr > 20 && xr < 56 && yr > 60 && yr < 110) { @@ -405,7 +391,7 @@ export function draw(display, params) { const a = cl((1 - sqrt(eu * eu + ev * ev)) * 9, 0, 1); if (a > 0) { const k = (0.4 + 0.32 * hb) * (0.65 + 0.45 * sm(9, -9, hu)); - (r = mix(r, 150 * k, a)), (g = mix(g, 26 * k, a)), (b = mix(b, 44 * k, a)); + bl(150 * k, 26 * k, 44 * k, a); } } // The lung: a dusky body, the grape clusters of alveoli, the capillary @@ -415,63 +401,51 @@ export function draw(display, params) { if (s < 1.2) { const c = own[qn], ox = c >= 0 ? cox[c] : 0.3; m = cl(0.5 - s, 0, 1); - if (m > 0) { - const k = 0.92 * m; - (r = mix(r, deF[0] + (oxF[0] - deF[0]) * ox, k)), (g = mix(g, deF[1] + (oxF[1] - deF[1]) * ox, k)), (b = mix(b, deF[2] + (oxF[2] - deF[2]) * ox, k)); - } + if (m > 0) bl(24 + 56 * ox, 14 - 4 * ox, 50 - 20 * ox, 0.92 * m); const aa = alvA[p]; if (aa > 0) { const i = alvK[p], cc = ak[i], o = cox[cc], sh = alvS[p] * cb[cc] * (soot[i] < wS * 0.4 ? 0.3 : 1), af = 0.4 * airK[cc]; - (r = mix(r, (deA[0] + (oxA[0] - deA[0]) * o) * sh + cold[0] * af, aa)), (g = mix(g, (deA[1] + (oxA[1] - deA[1]) * o) * sh + cold[1] * af, aa)), (b = mix(b, (deA[2] + (oxA[2] - deA[2]) * o) * sh + cold[2] * af, aa)); + for (let c = 0; c < 3; c++) C[c] += ((DA[c] + (oxA[c] - DA[c]) * o) * sh + cold[c] * af - C[c]) * aa; } - if (m > 0 && cap[qn] > 0) { - const k = cap[qn] * (0.5 + 0.5 * sin(t * 6 + cph[qn] + 2 * hb)) * (0.12 + 0.34 * ox) * m; - (r += 230 * k), (g += 30 * k), (b += 50 * k); + const cv = hash(qn, 5); + if (m > 0 && cv > 0.6) { + const k = (cv - 0.35) * (0.75 + 0.75 * sin(t * 6 + cv * 90 + 2 * hb)) * (0.12 + 0.34 * ox) * m; + (C[0] += 230 * k), (C[1] += 30 * k), (C[2] += 50 * k); } const rm = cl(1 - abs(s + 0.45) / 0.75, 0, 1) * 0.5; - if (rm > 0) (r = mix(r, 126 + 129 * ox, rm)), (g = mix(g, 100 + 24 * ox, rm)), (b = mix(b, 214 - 70 * ox, rm)); + if (rm > 0) bl(126 + 129 * ox, 100 + 24 * ox, 214 - 70 * ox, rm); // A shadow along each airway lifts the tree off the alveoli. - const hk = trH[p] * 0.5 * m; - (r *= 1 - hk), (g *= 1 - hk), (b *= 1 - hk); + bl(0, 0, 0, trH[p] * 0.5 * m); } // The airways, and the air in them. const ta = trA[p]; if (ta > 0) { - const gg = trG[p], vv = trV[p], d = trD[p], wc = WC[gg], al = AG[gg] * ta; + const gg = trG[p], vv = trV[p], d = trS[p] * dm, wc = WC[gg], al = AG[gg] * ta; let wb = 0.85; if (gg <= 1) { const ring = 0.5 + 0.5 * cos(trS[p] * 2.73); wb = (0.5 + 0.5 * sm(0.35, 0.95, vv)) * (0.66 + 0.44 * ring * ring); } else if (gg <= 3) wb = 0.65 + 0.35 * sm(0.3, 1, vv); - (r = mix(r, wc[0] * wb, al)), (g = mix(g, wc[1] * wb, al)), (b = mix(b, wc[2] * wb, al)); + bl(wc[0] * wb, wc[1] * wb, wc[2] * wb, al); let ca = 0, wa = 0; if (envIn > 0 && d < Fin) ca = envIn * (0.3 + 0.7 * exp(-(((Fin - d) / 0.05) ** 2))); if (envOut > 0 && d > Fx) wa = envOut * (0.24 + 0.76 * exp(-(((d - Fx) / 0.05) ** 2))); for (const f of bf) wa += 1.3 * exp(-(((d - f) / 0.06) ** 2)) + (d > f ? 0.25 : 0); const lf = (gg <= 1 ? 1 - 0.5 * sm(0.55, 1, vv) : 1) * ta * (gg === 9 ? 0.8 : 0.9); - (r += (cold[0] * ca + warm[0] * wa) * lf), (g += (cold[1] * ca + warm[1] * wa) * lf), (b += (cold[2] * ca + warm[2] * wa) * lf); + for (let c = 0; c < 3; c++) C[c] += (cold[c] * ca + warm[c] * wa) * lf; } const rb = rib[p]; - if (rb > 0) { - const k = rb * (0.4 - 0.12 * m); - (r = mix(r, 220, k)), (g = mix(g, 210, k)), (b = mix(b, 196, k)); - } + if (rb > 0) bl(220, 210, 196, rb * (0.42 + 0.1 * m)); if (y < 48 && pf.length) { let pa = 0; for (let j = 0; j < pf.length; j += 5) { const dx = (x - pf[j]) / (pf[j + 2] * 1.4), dy = (y - pf[j + 1]) / pf[j + 2], e = dx * dx + dy * dy; - if (e < 4) pa += pf[j + 3] * exp(-e * 1.4) * cl(0.4 + 1.4 * (nt(x * 1.4 + pf[j + 4], y * 1.4 + t * 7) - 0.5) + 0.5 * exp(-e * 3), 0, 1.4); - } - if (pa > 0) { - const k = min(1, pa * puffK); - (r = mix(r, puffC[0], k)), (g = mix(g, puffC[1], k)), (b = mix(b, puffC[2], k)); + if (e < 4) pa += pf[j + 3] * exp(-e * 1.4) * cl(0.4 + 1.4 * (vn(x * 0.2 + pf[j + 4], y * 0.2 + t * 0.9) - 0.5) + 0.5 * exp(-e * 3), 0, 1.4); } + if (pa > 0) bl(puffC[0], puffC[1], puffC[2], min(1, pa * puffK)); } - if (wS > 0) { - const k = 0.14 * wS * hz; - (r = mix(r, 120, k)), (g = mix(g, 96, k)), (b = mix(b, 62, k)); - } - (O[p * 3] = r), (O[p * 3 + 1] = g), (O[p * 3 + 2] = b); + if (wS > 0) bl(120, 96, 62, 0.14 * wS * hz); + O.set(C, p * 3); } // A cough jolts the whole chest up. const jl = round(P.jolt); From d26b868396528549dd0a975f83ad704a4c208bc1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 02:04:55 +0000 Subject: [PATCH 28/33] =?UTF-8?q?README=20=EC=97=90=2024=ED=8C=90=20?= =?UTF-8?q?=ED=91=9C=EB=A5=BC=20=EB=84=A3=EB=8A=94=EB=8B=A4=20(Shoot=20'em?= =?UTF-8?q?=20Up=20=EC=9D=80=20=EB=92=A4=EC=97=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KTNzLtP3gH9rQENoucsegc --- incoming/pattern-study/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 8e485b9b6..f46700506 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -249,6 +249,20 @@ | [Sequins](patterns/sequins.js) | 직물·짜임 | 뒤집히는 스팽글을 촘촘히 단 쿠션이 패널을 채워요. 은빛 비늘이 엇갈린 줄로 겹쳐 있고, 보이지 않는 손끝이 지나가면 스팽글이 차례로 모서리를 보이며 넘어가 하트, 별, 고양이 얼굴, 번개, 파도를 분홍 면으로 그려요. 손바닥이 위에서 아래로 쓸면 다시 은빛이 돼요. | | [Skein](patterns/skein.js) | 새 | 해 진 뒤 하늘을 올려다보면 기러기 떼가 V 자로 패널 위쪽, 북쪽으로 날아가요. 지친 선두가 한쪽 줄로 물러나면 다른 기러기가 앞에 서고, 커다란 보름달 앞을 지날 때는 새카만 실루엣이 돼요. 뒤따라온 작은 떼가 꼬리에 붙기도 해요. | +### 24판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Breath](patterns/breath.js) | 몸 | 후두에서 횡격막까지 해부도처럼 그린 폐가 숨을 쉬어요. 들이쉬면 횡격막이 내려가고 갈비뼈가 들리며, 푸르스름한 공기가 일곱 번 더 갈라지는 기관지를 타고 내려가 보랏빛 허파꽈리를 빨갛게 물들여요. K1 이 맑은 공기에서 입김이 하얗게 서리는 겨울과 매연 낀 도시까지 공기를 바꾸고, K1 을 누르면 기침을 해요. | +| [Emergence](patterns/emergence.js) | 짐승 | 해 질 녘 메마른 언덕의 동굴 입구에서 박쥐 떼가 검은 리본처럼 쏟아져 나와, 노을 하늘을 비틀며 패널 꼭대기까지 기둥으로 올라가요. 기둥은 두 가닥으로 갈라졌다 다시 붙고, 매가 내리꽂히면 박쥐들이 고리처럼 물러났다가 구멍이 닫혀요. K1 을 끝까지 돌리면 밤이 되어 박쥐가 보름달 앞을 지나가요. | +| [Flying Trapeze](patterns/flying-trapeze.js) | 무대·공연 | 밤의 서커스 천막 꼭대기를 올려다봐요. 빨강과 크림 줄무늬 천 아래에서 플라이어가 공중그네를 타고 나가 공중제비를 돌고, 무릎으로 매달린 캐처가 손을 잡아요. 팔로 스폿 셋이 연무 속에 빛줄기를 그으며 따라가고, 차례가 끝나면 그물로 떨어져 튀어 올라요. K1 을 누르면 북이 울리고 세 바퀴 공중제비가 나와요. | +| [Pendulum Wave](patterns/pendulum-wave.js) | 물리·스프링 | 길이가 조금씩 다른 진자 줄을 위에서 내려다봐요. 한꺼번에 놓은 추들이 한 줄로 흔들리다가 뱀처럼 휘고, 세 줄, 두 줄로 엇갈렸다가 28초 만에 다시 한 줄로 모여요. K1 이 강철에서 놋쇠와 구리를 지나 무지개 유리구슬까지 추를 바꾸고, K4 를 올리면 추가 지나간 자리에 빛 꼬리가 남아요. | +| [Pipe Organ](patterns/pipe-organ.js) | 음악·악기 | 어두운 성당의 파이프오르간 정면이 패널을 채워요. 보이지 않는 오르가니스트가 여덟 화음 코랄을 되풀이하면, 소리 나는 파이프마다 입에서 빛이 켜지고 몸통을 따라 정상파 띠가 올라가요. 가운데 가장 긴 파이프가 가장 낮은 음이에요. K1 이 플루트의 파랑, 프린시펄의 은빛, 리드의 금빛과 빨강 사이에서 음전을 고르고, K1 을 누르면 파이프 열아홉이 다 울려요. | +| [Potter's Wheel](patterns/potters-wheel.js) | 도자기 | 밤의 도자기 공방에서 녹색 갓 램프 아래 물레가 돌아요. 두 손이 흙덩이를 가운데로 모아 벽을 끌어올리면 그릇이 76~92줄 높이로 자라고, 물레 자국이 램프 쪽에서 반짝이며 옆으로 흘러가요. 다 빚은 병과 항아리는 줄로 잘려 선반에 올라가 젖은 흙빛에서 하얗게 말라 가요. K1 이 흙을 테라코타에서 검은 흙까지 바꾸고, K1 을 누르면 그릇이 주저앉아요. | +| [Thermal](patterns/thermal.js) | 비행 | 여름 오후의 밭 위에서 흰 글라이더가 열기류를 타고 타원을 그리며 맴돌아 올라가요. 말똥가리 둘이 더 좁게 같이 돌고, 꼭대기의 적운은 자라며 바닥이 어두워져요. 구름 바닥에 닿은 글라이더는 다음 열기류로 미끄러져 가고 아래에서 새 글라이더가 들어와요. K1 을 누르면 윈치가 글라이더 하나를 끌어올려요. | +| [Tower Crane](patterns/tower-crane.js) | 공사·건설 | 해 질 녘 올라가는 콘크리트 건물 옆에 노란 타워크레인이 서서, 마당의 벽판과 철근 다발, 거푸집, 콘크리트 버킷을 차례로 꼭대기 층에 올려요. 붐이 원근 속에서 돌며 짧아졌다 길어지고, 버킷이 바닥을 부으면 건물이 한 층 자라 새 층 창에 불이 들어와요. K1 을 끝까지 돌리면 밤이 되어 투광등이 켜져요. | +| [Waggle Dance](patterns/waggle-dance.js) | 곤충 | 관찰 벌통의 벌집 한 장이 패널 높이로 서요. 위에 봉한 꿀, 그 아래 여덟 색 꽃가루 띠와 애벌레 방이 있고, 벌 여든 마리가 기어 다녀요. 아래쪽 빈 벌집에서는 꽃가루를 단 벌이 몸을 흔들며 8 자 춤을 추고 따르는 벌들이 둘러서요. 춤의 각도가 해에서 본 꽃밭의 방향이라 K4 를 돌리면 모든 춤이 같이 돌아요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. From 9f68381e97f137b9fd54d19aafbdf8ee3ce92fcd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 02:11:21 +0000 Subject: [PATCH 29/33] shoot-em-up: fit under 25 KB, cloud objects, new boss --- .../pattern-study/patterns/shoot-em-up.js | 1125 +++++------------ 1 file changed, 291 insertions(+), 834 deletions(-) diff --git a/incoming/pattern-study/patterns/shoot-em-up.js b/incoming/pattern-study/patterns/shoot-em-up.js index cd30b431f..748fa54bd 100644 --- a/incoming/pattern-study/patterns/shoot-em-up.js +++ b/incoming/pattern-study/patterns/shoot-em-up.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Shoot 'em Up // Family: pixel -// Idea: A vertical arcade shooter plays itself on a tall cabinet screen: a white fighter weaves over a scrolling sea of islands, a coast, fields, a little town and a river, shooting down waves of enemy craft and gunboats, until a boss ship fills the top of the screen and sprays rings, spirals and fans of bullets for it to thread through. +// Idea: An arcade shooter plays itself: a fighter weaves over a scrolling sea, coast, fields and town, shooting down waves of craft and gunboats, until a boss fills the top and sprays rings and spirals of bullets. // Author: patternflow-lab // Date: 2026-10-24 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -11,11 +11,11 @@ // @matrix 64x128 // @knobs Stage=0..1, Speed=0.2..3, Waves=1..5, Bullets=0..1 // -// Knob 1: Stage. Which stage the cabinet plays, in four steps: a tropical sea with green fields at 0, a red desert canyon, a city at night with lit windows and neon roofs, and deep space past a ringed planet. -// Knob 2: Speed. The pace of the game: at 2 the ground scrolls 12 LEDs a second and the first boss arrives two seconds in, and at 0.2 everything plays ten times slower. -// Knob 3: Waves. How many enemies come: at 1 a formation of five or six every three seconds, at 5 a bigger one every second, more gunboats on the sea, and waves that keep coming while the boss is there. -// Knob 4: Bullets. How thick the enemy fire is and how furious the boss: at 0.06 a ring of nine and a slow spiral, at 1 rings of thirty, a three-armed spiral and wide fans, with the boss's core burning red. -// Buttons: K1 tap drops a bomb: a white flash and a shock ring that clears the bullets and hurts everything it crosses. K4 tap calls the boss now. +// Knob 1: Stage. Four stages: a tropical sea and green fields at 0, a red desert canyon, a city at night, a space station among stars. +// Knob 2: Speed. The pace of the game; at 2 the ground scrolls 12 LEDs a second and the first boss comes two seconds in. +// Knob 3: Waves. From a formation every three seconds at 1 to a bigger one every second at 5, with more gunboats. +// Knob 4: Bullets. How thick the fire is, from rings of twelve at 0.06 to rings of thirty and three-armed spirals at 1, with the ground dimmed so they read. +// Buttons: K1 tap drops a bomb: a white flash and a shock ring that clears the bullets. const W = 64; const H = 128; @@ -23,11 +23,9 @@ const N = W * H; const { min, max, sin, cos, abs, atan2, hypot, floor, round, sqrt, PI } = Math; const clamp = (v, a, b) => (v < a ? a : v > b ? b : v); const rnd = (a, b) => a + (b - a) * Math.random(); -// Ground scroll at Speed 2, in LEDs a second; the clouds pass 1.8 times as fast. +// Ground scroll at Speed 2 in LEDs a second; clouds go 1.8 times as fast. const V0 = 12; const CLOUD = 1.8; -// The map repeats every 1024 rows: sea and islands, coast, fields, town, river, coast, sea. -const PERIOD = 1024; const hash = (x, y, s) => { let h = Math.imul((x * 374761393 + y * 668265263 + s * 982451653) | 0, 1274126177); @@ -35,51 +33,20 @@ const hash = (x, y, s) => { return ((h ^ (h >>> 16)) >>> 0) / 4294967296; }; function noise(x, y, s) { - const ix = floor(x); - const iy = floor(y); - const fx = x - ix; - const fy = y - iy; - const u = fx * fx * (3 - 2 * fx); - const v = fy * fy * (3 - 2 * fy); - const a = hash(ix, iy, s); - const b = hash(ix + 1, iy, s); + const ix = floor(x), iy = floor(y), u = (x -= ix) * x * (3 - 2 * x), v = (y -= iy) * y * (3 - 2 * y), a = hash(ix, iy, s), b = hash(ix + 1, iy, s); const c = hash(ix, iy + 1, s); - const d = hash(ix + 1, iy + 1, s); - return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v; + return a + (b - a) * u + (c - a) * v + (a - b - c + hash(ix + 1, iy + 1, s)) * u * v; } -// Ground types, one palette entry each per stage. -const DEEP = 0; -const WAVE = 1; -const CAP = 2; -const SHAL = 3; -const SAND = 4; -const GRASS = 5; -const TREE = 6; -const FLDA = 7; -const FLDB = 8; -const ROAD = 9; -const ROOF = 10; -const WALL = 11; -const TRUNK = 12; -const RIVER = 13; -const LIGHT = 14; -const HEDGE = 15; +// Ground types, one colour each in every stage's palette. +const [DEEP, WAVE, CAP, SHAL, SAND, GRASS, TREE, FLDA, FLDB, ROAD, ROOF, WALL, TRUNK, RIVER, LIGHT, HEDGE] = [...Array(16).keys()]; const PALS = [ - [0x123c9a, 0x1e5cc4, 0xd2ecff, 0x2aa8cc, 0xf0d488, 0x4aae3c, 0x1f6a2a, 0x9ccc4a, 0x6e9a30, 0x76706a, 0xd8482e, 0xf2e6c8, 0x8a5a2a, 0x2c7ed8, 0xfff2c0, 0x2e5226], - [0x5c2018, 0x8a3a22, 0xd8966a, 0xb4502a, 0xf0b464, 0xdc9a4c, 0x4a8a3c, 0xf2c278, 0xc8843e, 0x6e4630, 0xc8602e, 0xf6dcaa, 0x5a3418, 0x2aa89a, 0xffe6aa, 0x7a2c1a], - [0x0c1846, 0x172a62, 0x8ca8e6, 0x1c386a, 0x50506a, 0x163e34, 0x0e2c24, 0x40446a, 0x2a2c48, 0x1e1e30, 0xb43c96, 0xffd25a, 0x2e2224, 0x12265e, 0xffa83c, 0x1c1c2c], - [0x08061c, 0x1e0e40, 0xe6e6ff, 0x34185e, 0x9a9088, 0x5a6280, 0x4c4440, 0x868ca8, 0x464c66, 0x1c2034, 0xff3c3c, 0xb8c2d8, 0x3c3432, 0x28dcff, 0x78ffc8, 0x2a2e40], -].map((p) => { - const a = new Float32Array(48); - p.forEach((c, i) => { - a[i * 3] = c >> 16; - a[i * 3 + 1] = (c >> 8) & 255; - a[i * 3 + 2] = c & 255; - }); - return a; -}); -// Clouds per stage: colour, how dark the underside is, alpha, and the shade of their shadow. + "123c9a1e5cc4d2ecff2aa8ccf0d4884aae3c1f6a2a9ccc4a6e9a3076706ad8482ef2e6c88a5a2a2c7ed8fff2c02e5226", + "5c20188a3a22d8966ab4502af0b464dc9a4c4a8a3cf2c278c8843e6e4630c8602ef6dcaa5a34182aa89affe6aa7a2c1a", + "0c1846172a628ca8e61c386a50506a163e340e2c2440446a2a2c481e1e30b43c96ffd25a2e222412265effa83c1c1c2c", + "08061c1e0e40e6e6ff34185e9a90885a62804c4440868ca8464c661c2034ff3c3cb8c2d83c343228dcff78ffc82a2e40", +].map((s) => s.match(/../g).map((h) => parseInt(h, 16))); +// Clouds per stage: colour, underside shade, alpha, shadow shade. const CLOUDS = [ [236, 242, 255, 0.74, 0.72, 0.62], [236, 200, 156, 0.78, 0.6, 0.7], @@ -87,110 +54,77 @@ const CLOUDS = [ [150, 76, 214, 0.6, 0.5, 1], ]; -// Sprites as rows of letters; "." is clear. Colours are hex, -1 is the boss's glowing core. -function mk(rows, pal, mirror) { - const w0 = rows[0].length; - const w = mirror ? w0 * 2 : w0; - const px = []; - const mask = new Uint8Array(w * rows.length); +// Sprites are rows of letters, "." clear; a mirrored one is its left half. +function mk(src, pal, mirror) { + const rows = src.trim().split(/\s+/), w = rows[0].length * (mirror ? 2 : 1), px = [], mask = new Uint8Array(w * rows.length); rows.forEach((r, j) => { - for (let i = 0; i < r.length; i++) { - if (r[i] === ".") continue; - px.push(i, j, pal[r[i]]); - mask[j * w + i] = 1; - if (mirror) { - px.push(w - 1 - i, j, pal[r[i]]); - mask[j * w + w - 1 - i] = 1; - } - } + for (let i = 0; i < r.length; i++) + if (r[i] !== ".") for (const x of mirror ? [i, w - 1 - i] : [i]) px.push(x, j, pal[r[i]]), (mask[j * w + x] = 1); }); - return { w, h: rows.length, px, mask }; + return { px, mask }; } -const PLAYER = mk( - ["....w....", "...wcw...", "...wbw...", "..gwbwg..", "..gwwwg..", ".wgwwwgw.", "wwwwgwwww", "rww.g.wwr", "r..www..r", "...r.r..."], - { w: 0xdce4f0, c: 0x96f0ff, b: 0x286ee6, g: 0x828ca0, r: 0xe62832 }, -); -const FIGHTER = mk(["o.....o", "oo.d.oo", "ooodooo", ".odwdo.", "..ddd..", "...d..."], { o: 0xff8c1e, d: 0xaa3214, w: 0xfff0c8 }); -const DART = mk(["mm...mm", "mmm.mmm", ".mmmmm.", ".mmcmm.", "..mmm..", "..mdm..", "...m..."], { m: 0xd246e6, c: 0x78f0ff, d: 0x6e1e8c }); -const BOAT = mk( - [".kkkkk.", "kbbbbbk", "kbaaabk", "kbaaabk", "kbbbbbk", "kbbbbbk", "kbaaabk", "kbbbbbk", ".kbbbk.", ".kbbbk.", "..kbk..", "...k..."], - { k: 0x1e2028, b: 0x969baa, a: 0x5a5f6e }, -); -const LIFE = mk(["..w..", ".www.", "wwwww", "r.w.r"], { w: 0xdce4f0, r: 0xe62832 }); +const PLAYER = mk("....w.... ...wcw... ...wbw... ..gwbwg.. ..gwwwg.. .wgwwwgw. wwwwgwwww rww.g.wwr r..www..r ...r.r...", { w: 0xdce4f0, c: 0x96f0ff, b: 0x286ee6, g: 0x828ca0, r: 0xe62832 }); +const FIGHTER = mk("o.....o oo.d.oo ooodooo .odwdo. ..ddd.. ...d...", { o: 0xff8c1e, d: 0xaa3214, w: 0xfff0c8 }); +const DART = mk("mm...mm mmm.mmm .mmmmm. .mmcmm. ..mmm.. ..mdm.. ...m...", { m: 0xd246e6, c: 0x78f0ff, d: 0x6e1e8c }); +const BOAT = mk(".kkkkk. kbbbbbk kbaaabk kbaaabk kbbbbbk kbbbbbk kbaaabk kbbbbbk .kbbbk. .kbbbk. ..kbk.. ...k...", { k: 0x1e2028, b: 0x969baa, a: 0x5a5f6e }); +// The boss, 56 wide; "e" is its glowing core. const BOSS = mk( - [ - ".............kkgggk..kkkkkkk", - "kk...........kacccc..kbbbbbb", - "kykkk........kabbbc..kbbrrbb", - "kybbbkkk.....kabbbc..kbbrrbb", - "kbbbbbbbkk...kabbbc..kbbbbbb", - "kbbbbbbbbbkkkbabbbc..kbbbbbb", - "kbbrrrbbbbbbbbabbbc..kbbbkkk", - "kaarrrrrrbbbbbabbbc..kbbbkee", - "kccabbrrrrrrbbabbbckkbbbbkee", - ".kkcaabgggrrrrabbbcbbbbbbkee", - "...kcaagygbbrrrrrrbbbbbbbkee", - "....kacgggbbbbbrrrrrrbbbbkee", - "......kkcaabbbbbbbrrrbbbbkee", - "........kacaaabgggbbbbbbbkee", - ".........akccabgygbbbbbbbkee", - "...........kkaagggbbbbbbbkee", - ".............kccaabbbbbbbkkk", - "..............kkccabbbbbbbbb", - "................kkcaabbbbbbb", - "..................kccbbyybbb", - "...................kkabbbbbb", - ".....................kabbbbb", - ".....................kcbbbbb", - "......................kabbbb", - ".......................kabbg", - ".......................kcbbg", - "........................kabg", - ".........................kag", - ".........................kcg", - "..........................kg", - ], + ` +.............kkgggk..kkkkkkk +kk...........kacccc..kbbbbbb +kykkk........kabbbc..kbbrrbb +kybbbkkk.....kabbbc..kbbrrbb +kbbbbbbbkk...kabbbc..kbbbbbb +kbbbbbbbbbkkkbabbbc..kbbbbbb +kbbrrrbbbbbbbbabbbc..kbbbkkk +kaarrrrrrbbbbbabbbc..kbbbkee +kccabbrrrrrrbbabbbckkbbbbkee +.kkcaabgggrrrrabbbcbbbbbbkee +...kcaagygbbrrrrrrbbbbbbbkee +....kacgggbbbbbrrrrrrbbbbkee +......kkcaabbbbbbbrrrbbbbkee +........kacaaabgggbbbbbbbkee +.........kkccabgygbbbbbbbkee +...........kkaagggbbbbbbbkee +.............kccaabbbbbbbkkk +..............kkccabbbbbbbbb +................kkcaabbbbbbb +..................kccbbyybbb +...................kkabbbbbb +.....................kabbbbb +.....................kcbbbbb +......................kabbbb +.......................kabbg +.......................kcbbg +........................kabg +.........................kag +.........................kcg +..........................kg`, { k: 0x14141e, c: 0x464a60, a: 0x787e96, b: 0xb4bacc, r: 0xdc323c, y: 0xffd646, g: 0x2c2c38, e: -1 }, true, ); -const PALM = ["t.t.t", ".ttt.", "ttktt", ".ttt.", "t.t.t"]; -// 3x5 digits and a P, one octal digit per row from the top. -const GLYPH = [0o75557, 0o26227, 0o71747, 0o71317, 0o55711, 0o74717, 0o74757, 0o71122, 0o75757, 0o75717, 0o75744]; -// Enemy bullet colours: the boss's rings, its spirals, and aimed shots. +const PALM = "t.t.t .ttt. ttktt .ttt. t.t.t".split(" "); +// 3x5 digits, a P, and a ship for the lives; one octal digit per row. +const GLYPH = [0o75557, 0o26227, 0o71747, 0o71317, 0o55711, 0o74717, 0o74757, 0o71122, 0o75757, 0o75717, 0o75744, 0o22775]; const BUL = [ [255, 70, 170], [70, 220, 255], [255, 170, 40], ]; -// ---- the ground ------------------------------------------------------------- - -// -1 is open sea and 1 is land; the coasts run a little slanted across the screen. +// The map repeats every 1024 rows: sea, coast, fields, town, river, coast. -1 is sea, 1 land. function profile(x, wy) { - const v = (wy % PERIOD) + 0.3 * (x - 32); - if (v < 250 || v > 830) return -1; - if (v < 290) return -1 + (v - 250) / 20; - if (v < 790) return 1; - return 1 - (v - 790) / 20; + const v = (wy % 1024) + 0.3 * (x - 32); + return v < 250 || v > 830 ? -1 : v < 290 ? (v - 270) / 20 : v < 790 ? 1 : (810 - v) / 20; } -const ISL = { x: 0, y: 0, r: 0 }; +const ISL = {}; function islands(sd, x, wy) { - const cx = floor(x / 32); - const cy = floor(wy / 44); let best = 0; - for (let j = cy - 1; j <= cy + 1; j++) - for (let i = cx - 1; i <= cx + 1; i++) { - if (hash(i, j, sd + 1) > 0.7) continue; - const ix = i * 32 + 4 + hash(i, j, sd + 2) * 24; - const iy = j * 44 + 6 + hash(i, j, sd + 3) * 32; - const r = 12 + hash(i, j, sd + 4) * 10; + for (let j = floor(wy / 44) - 1, j1 = j + 2; j <= j1; j++) + for (let i = floor(x / 32) - 1, i1 = i + 2; i <= i1; i++) { + const ix = i * 32 + 4 + hash(i, j, sd + 2) * 24, iy = j * 44 + 6 + hash(i, j, sd + 3) * 32, r = 12 + hash(i, j, sd + 4) * 10; const b = 1.9 * (1 - hypot(x - ix, wy - iy) / r); - if (b > best) { - best = b; - ISL.x = ix; - ISL.y = iy; - ISL.r = r; - } + if (b > best && hash(i, j, sd + 1) < 0.7) ((best = b), (ISL.x = ix), (ISL.y = iy), (ISL.r = r)); } return best; } @@ -201,146 +135,59 @@ function terrain(sd, x, wy) { if (e < -0.08) { if (e > -0.12) return CAP; if (e > -0.21) return SHAL; - const cx = x >> 3; - const cy = floor(wy / 5); - const h = hash(cx, cy, sd + 21); - if (h < 0.4 && wy === cy * 5 + floor(hash(cx, cy, sd + 23) * 4)) { - const c0 = cx * 8 + floor(hash(cx, cy, sd + 22) * 6); - if (x >= c0 && x < c0 + (h < 0.15 ? 3 : 2)) return CAP; - } - if (e > -0.36) return WAVE; - return noise(x / 6, wy / 2, sd + 5) > 0.78 ? WAVE : DEEP; + const cx = x >> 3, cy = floor(wy / 5), h = hash(cx, cy, sd + 21), c0 = cx * 8 + floor(hash(cx, cy, sd + 22) * 6); + if (h < 0.4 && wy === cy * 5 + floor(hash(cx, cy, sd + 23) * 4) && x >= c0 && x < c0 + (h < 0.15 ? 3 : 2)) return CAP; + return e > -0.36 || noise(x / 6, wy / 2, sd + 5) > 0.78 ? WAVE : DEEP; } if (e < 0.08) return SAND; if (pr < -0.5) { - // An island: grass, and a palm at the middle of a big one. - const dx = round(x - ISL.x); - const dy = round(wy - ISL.y); - if (ISL.r > 13 && abs(dx) <= 2 && abs(dy) <= 2) { - const c = PALM[dy + 2][dx + 2]; - if (c !== ".") return c === "k" ? TRUNK : TREE; - } - return GRASS; + const dx = round(x - ISL.x), dy = round(wy - ISL.y), c = ISL.r > 13 && abs(dx) <= 2 && abs(dy) <= 2 ? PALM[dy + 2][dx + 2] : "."; + return c === "." ? GRASS : c === "k" ? TRUNK : TREE; } - return land(sd, x, wy, wy % PERIOD); -} -function land(sd, x, wy, u) { - const dr = abs(x - 30 - 9 * sin(wy / 70)); + const u = wy % 1024, dr = abs(x - 30 - 9 * sin(wy / 70)); if (u > 560 && u < 770) { - const a = (u - 560) / 34; - const dv = abs(x - 32 - 23 * sin(a)) / sqrt(1 + (0.68 * cos(a)) ** 2); - if (dv < 4.4) { - if (dr < 2.6) return dr > 1.6 ? WALL : ROAD; - return dv < 3.3 ? RIVER : SAND; - } + const a = (u - 560) / 34, dv = abs(x - 32 - 23 * sin(a)) / sqrt(1 + (0.68 * cos(a)) ** 2); + if (dv < 4.4) return dr < 2.6 ? (dr > 1.6 ? WALL : ROAD) : dv < 3.3 ? RIVER : SAND; } if (dr < 1.6) return dr < 0.6 && wy % 6 < 3 ? LIGHT : ROAD; - if (u > 420 && u < 540) { - // The town: a grid of streets, houses with roofs, and a tree in each empty plot. - const gx = x + 4; - const px = floor(gx / 9); - const py = floor(wy / 9); - const lx = gx - px * 9; - const ly = wy - py * 9; + if (u > 420 && u < 510 && dr < 22 + 6 * sin(wy / 9)) { + // The town: in each plot a house (roof, front wall, shadow) or a tree. + const lx = (x + 4) % 9, ly = wy % 9; if (lx === 0 || ly === 0) return ROAD; - if (hash(px, py, sd + 41) < 0.72) { - if (lx >= 2 && lx <= 6 && ly >= 3 && ly <= 7) return lx === 4 ? TRUNK : ROOF; - if (lx >= 2 && lx <= 6 && ly === 2) return WALL; - if ((lx === 7 && ly >= 2 && ly <= 6) || (ly === 1 && lx >= 3 && lx <= 7)) return HEDGE; - return GRASS; - } - return hypot(lx - 4.5, ly - 4.5) < 2.7 ? TREE : GRASS; - } - // Fields: a patchwork of crops, meadows and woods with hedges between. - const band = floor(wy / 14); - const gx = x + floor(hash(band, 0, sd + 51) * 16); - const cx = floor(gx / 16); - const lx = gx - cx * 16; - const ly = wy - band * 14; - const h = hash(cx, band, sd + 52); - if (lx === 0 || ly === 0) return h < 0.6 ? HEDGE : GRASS; - const kind = floor(h * 5); - if (kind === 0) return wy % 3 === 0 ? FLDB : FLDA; - if (kind === 1) return gx % 3 === 0 ? FLDB : FLDA; - if (kind === 2) return hash(gx, wy, sd + 53) < 0.12 ? FLDB : FLDA; - const tx = floor(gx / 5); - const ty = floor(wy / 5); - if (kind === 4 && hash(tx, ty, sd + 54) < 0.6) return GRASS; - const d = hypot(gx - tx * 5 - 1 - hash(tx, ty, sd + 55) * 3, wy - ty * 5 - 1 - hash(tx, ty, sd + 56) * 3); - return d < 1.9 ? TREE : GRASS; + if (hash(floor((x + 4) / 9), floor(wy / 9), sd + 41) > 0.6) return hypot(lx - 4.5, ly - 4.5) < 2.7 ? TREE : GRASS; + if (lx >= 2 && lx <= 6 && ly >= 2) return ly === 2 ? WALL : ly > 7 ? GRASS : lx === 4 ? TRUNK : ROOF; + return (lx === 7 && ly >= 2 && ly <= 6) || (ly === 1 && lx >= 3) ? HEDGE : GRASS; + } + const band = floor(wy / 14), gx = x + floor(hash(band, 0, sd + 51) * 16), h = hash(floor(gx / 16), band, sd + 52); + if (gx % 16 === 0 || wy % 14 === 0) return h < 0.6 ? HEDGE : GRASS; + const kind = floor(h * 4); + if (kind < 3) return (kind ? (kind > 1 ? hash(gx, wy, sd + 53) < 0.12 : gx % 3 === 0) : wy % 3 === 0) ? FLDB : FLDA; + const tx = floor(gx / 5), ty = floor(wy / 5); + return hypot(gx - tx * 5 - 1 - hash(tx, ty, sd + 55) * 3, wy - ty * 5 - 1 - hash(tx, ty, sd + 56) * 3) < 1.9 ? TREE : GRASS; } -// A cloud: round puffs on a flat base, 3 where the sun lights its top, 2 its body, 1 its grey underside. +// A cloud: puffs on a flat base, 2 its body and 1 its underside. function newCloud(S, cw) { - const w = floor(rnd(22, 40)); - const h = floor(rnd(10, 16)); - const n = 3 + floor(w / 9); - const P = []; + const w = floor(rnd(22, 40)), h = floor(rnd(10, 16)), n = 3 + floor(w / 9), P = []; for (let i = 0; i < n; i++) { const r = h * 0.5 * (0.5 + 0.5 * sin((PI * (i + 0.5)) / n)) + rnd(-0.6, 0.8); P.push((w * (i + 0.5)) / n + rnd(-1.5, 1.5), h - 1.5 - r * 0.55, r); } - const inn = (x, y) => { - if (x < 0 || y < 0 || x >= w || y > h - 1.5) return false; - for (let i = 0; i < P.length; i += 3) if (hypot(x + 0.5 - P[i], y + 0.5 - P[i + 1]) < P[i + 2]) return true; - return false; - }; + const inn = (x, y) => x >= 0 && y >= 0 && x < w && y <= h - 1.5 && P.some((_, i) => i % 3 === 0 && hypot(x + 0.5 - P[i], y + 0.5 - P[i + 1]) < P[i + 2]); const m = new Uint8Array(w * h); - for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) if (inn(x, y)) m[y * w + x] = !inn(x, y + 2) ? 1 : !inn(x - 1, y - 2) ? 3 : 2; + for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) if (inn(x, y)) m[y * w + x] = inn(x, y + 2) ? 2 : 1; S.clouds.push({ x: floor(rnd(-14, 56 - w / 2)), cw: cw + h, w, h, m }); } function fill(S) { - const top = floor(S.scroll) + H + 24; - while (S.gen < top) { - const o = (S.gen & 255) * W; - for (let x = 0; x < W; x++) S.ter[o + x] = terrain(S.sd, x, S.gen); - S.gen++; - } + for (const top = floor(S.scroll) + H + 24; S.gen < top; S.gen++) for (let x = 0; x < W; x++) S.ter[(S.gen & 255) * W + x] = terrain(S.sd, x, S.gen); const cb = floor(S.scroll * CLOUD); - while (S.cnext < cb + H + 20) { - newCloud(S, S.cnext); - S.cnext += floor(rnd(24, 52)); - } + for (; S.cnext < cb + H + 20; S.cnext += floor(rnd(24, 52))) newCloud(S, S.cnext); S.clouds = S.clouds.filter((c) => cb + H - 1 - c.cw < H); } -// ---- the game --------------------------------------------------------------- - export function setup(params) { - const S = { - t: 0, - scroll: 85, - sd: floor(Math.random() * 9000), - ter: new Uint8Array(256 * W), - cb: new Uint8Array(N), - clouds: [], - buf: new Float32Array(N * 3), - sh: new Float32Array(N), - en: [], - boats: [], - eb: [], - ps: [], - fx: [], - pop: [], - caps: [], - boss: null, - bossAt: 3.2, - bossN: 0, - waveAt: 0.4, - waveN: 0, - boatAt: 0.2, - kills: 0, - score: 0, - flash: 0, - bomb: null, - pl: { x: 32, y: 112, inv: 0, dead: 0, lvl: 0, lvlT: 0, lives: 3, ft: 0, pu: -9 }, - stage: 0, - tempo: 1, - waves: 1, - fury: 0.06, - started: false, - }; - S.gen = floor(S.scroll) - 2; - S.cnext = floor(S.scroll * CLOUD) - 6; + const S = { t: 0, scroll: 85, gen: 83, cnext: 147, sd: floor(Math.random() * 9000), ter: new Uint8Array(256 * W), cb: new Uint8Array(N), buf: new Float32Array(N * 3), sh: new Float32Array(N) }; + Object.assign(S, { clouds: [], en: [], boats: [], eb: [], ps: [], fx: [], pop: [], caps: [], bossAt: 3.2, bossN: 0, waveAt: 0.4, waveN: 0, boatAt: 0.2, kills: 0, score: 0, flash: 0 }); + S.pl = { x: 32, y: 112, lvl: 0, lvlT: 0, ft: 0 }; fill(S); params.s = S; } @@ -348,364 +195,194 @@ export function setup(params) { function shoot(S, x, y, a, v, c, big) { if (S.eb.length < 380) S.eb.push({ x, y, vx: cos(a) * v, vy: sin(a) * v, c, big }); } -const aim = (S, x, y) => atan2(S.pl.y - y, S.pl.x - x); +function fan(S, x, y, m, gap, v) { + const g = atan2(S.pl.y - y, S.pl.x - x); + for (let i = 0; i < m; i++) shoot(S, x, y, g + (i - (m - 1) / 2) * gap, v, 2, 0); +} function boom(S, x, y, s) { const sp = []; for (let i = 0; i < 4 + s * 3; i++) { - const a = Math.random() * 6.283; - const v = (20 + Math.random() * 30) * s; - sp.push(cos(a) * v, sin(a) * v); + const a = rnd(0, 6.283); + sp.push(cos(a) * rnd(20, 50) * s, sin(a) * rnd(20, 50) * s); } S.fx.push({ x, y, t0: S.t, s, sp, d: 0.45 + 0.12 * s }); } -function popup(S, x, y, v) { +function score(S, x, y, v) { S.pop.push({ x, y, v, t0: S.t }); + S.score += v; } +// A hit; a kill leaves an explosion, its score, and every ninth a capsule. +function hurt(S, e, n, x, y) { + e.fl = 0.06; + if ((e.hp -= n) > 0) return; + boom(S, x, y, e.boat ? 1.3 : 1); + score(S, x, y - 4, e.boat ? 300 : e.k ? 150 : 100); + if (++S.kills % 9 === 0) S.caps.push({ x, y }); +} +const boatY = (S, b) => floor(S.scroll) + H - 1 - round(b.wy); +const yOf = (S, e) => (e.boat ? boatY(S, e) : e.y); -function spawnWave(S) { - const n = S.waveN++; - if (n % 2 === 0) { - // A line that swoops in an S, down from the top or across from a side. - const m = 4 + round(S.waves); - const side = n % 4 === 0 ? 1 : -1; - const top = n % 6 === 4; - const cx = rnd(26, 38); - const A = side * rnd(14, 19); - const f = rnd(0.036, 0.048); - const y0 = rnd(8, 30); - for (let i = 0; i < m; i++) S.en.push({ k: 0, top, side, y0, born: S.t + i * 0.3, cx, A, f, x: -99, y: -99, hp: 6, fl: 0, ft: rnd(0.4, 2.4) }); - } else { - // A V that comes down, holds, and dives at the fighter one by one. - const m = 2 * floor(2 + S.waves * 0.55) + 1; - const cx = rnd(24, 40); - for (let i = 0; i < m; i++) { - const r = (i + 1) >> 1; - const s = i & 1 ? -1 : 1; - S.en.push({ k: 1, born: S.t, x: cx + s * r * 6, y: -8 - r * 6, hy: 34 - r * 6, st: 0, vx: 0, vy: 50, hp: 7, fl: 0, hold: 0.5 + r * 0.25 + Math.random() * 0.2 }); +function spawn(S) { + const w = S.waves; + if (S.t >= S.waveAt) { + const n = S.waveN++, m = 4 + round(w); + if (n % 2 === 0) { + const o = { k: 0, side: n % 4 ? -1 : 1, y0: rnd(8, 30) }; + for (let i = 0; i < m; i++) S.en.push({ ...o, born: S.t + i * 0.3, x: -99, y: -99, hp: 6 }); + } else { + const cx = rnd(24, 40); + for (let i = 0; i < (m | 1); i++) { + const r = (i + 1) >> 1; + S.en.push({ k: 1, born: S.t, x: cx + (i & 1 ? r : -r) * 6, y: -8 - r * 6, hy: 34 - r * 6, st: 0, hp: 7, hold: 0.5 + r * 0.25 + rnd(0, 0.2) }); + } } + S.waveAt = S.t + (n === 0 ? 0.45 : S.boss ? 2 : 1) * (3.3 / w ** 0.75); } -} -function spawnBoat(S) { + if (S.t < S.boatAt) return; + // A gunboat, where its hull fits on open water. const wy = floor(S.scroll) + H + 8; + S.boatAt = S.t + 0.3; for (let n = 0; n < 6; n++) { const x = floor(rnd(6, 58)); let ok = true; - for (let j = -7; j <= 7 && ok; j += 2) - for (let i = -4; i <= 4; i += 2) if (S.ter[((wy + j) & 255) * W + clamp(x + i, 0, 63)] > CAP) ok = false; - if (ok) { - S.boats.push({ x, wy, hp: 6, fl: 0, ft: rnd(0.6, 1.6), a: PI / 2 }); - return; - } + for (let j = -7; j <= 7; j += 2) for (let i = -4; i <= 4; i += 2) ok &&= S.ter[((wy + j) & 255) * W + x + i] <= CAP; + if (ok) return S.boats.push({ boat: 1, x, wy, hp: 10, ft: rnd(0.6, 1.6) }), (S.boatAt += rnd(1.1, 2.7) / w ** 0.7); } } -const boatY = (S, b) => floor(S.scroll) + H - 1 - round(b.wy); - -function kill(S, x, y, v, s) { - boom(S, x, y, s); - popup(S, x, y - 4, v); - S.score += v; - if (++S.kills % 9 === 0) S.caps.push({ x, y, t0: S.t }); -} function stepEnemies(S, d) { - const P = S.pl; const f = S.fury; for (const e of S.en) { - e.fl = max(0, e.fl - d); const a = S.t - e.born; + e.fl -= d; if (a < 0) continue; if (e.k === 0) { - if (e.top) { - e.y = -6 + 34 * a; - e.x = e.cx + e.A * sin(e.f * e.y); - } else { - e.x = e.side > 0 ? -6 + 38 * a : 70 - 38 * a; - e.y = e.y0 + 14 * a + 13 * sin(3.2 * a); - } - e.ft -= d; - if (e.ft <= 0) { - e.ft = rnd(1.2, 2.6); - if (e.y > 0 && e.y < 80 && Math.random() < 0.3 + 0.7 * f) shoot(S, e.x, e.y + 3, aim(S, e.x, e.y), 34, 2, 0); + e.x = e.side > 0 ? -6 + 38 * a : 70 - 38 * a; + e.y = e.y0 + 14 * a + 13 * sin(3.2 * a); + } else if (e.st === 0) { + if ((e.y += 50 * d) >= e.hy) ((e.st = 1), (e.t1 = S.t)); + } else if (e.st === 1) { + if (S.t - e.t1 > e.hold) { + const g = atan2(S.pl.y - e.y, S.pl.x - e.x); + ((e.st = 2), (e.vx = cos(g) * 30), (e.vy = sin(g) * 30)); + if (Math.random() < 0.35 + 0.65 * f) fan(S, e.x, e.y + 3, 1 + 2 * round(f * 1.5), 0.25, 42); } } else { - if (e.st === 0) { - e.y += 50 * d; - if (e.y >= e.hy) { - e.st = 1; - e.t1 = S.t; - } - } else if (e.st === 1) { - e.x += sin(S.t * 3 + e.hy) * 4 * d; - if (S.t - e.t1 > e.hold) { - e.st = 2; - const g = aim(S, e.x, e.y); - e.vx = cos(g) * 30; - e.vy = sin(g) * 30; - if (Math.random() < 0.35 + 0.65 * f) shoot(S, e.x, e.y + 3, g, 42, 2, 0); - } - } else { - const sp = hypot(e.vx, e.vy); - const k = min(130, sp + 110 * d) / sp; - e.vx *= k; - e.vy *= k; - e.x += e.vx * d; - e.y += e.vy * d; - } + const k = min(130, hypot(e.vx, e.vy) + 110 * d) / hypot(e.vx, e.vy); + e.x += (e.vx *= k) * d; + e.y += (e.vy *= k) * d; } } S.en = S.en.filter((e) => e.hp > 0 && e.y < 140 && e.x > -20 && e.x < 84); for (const b of S.boats) { - b.fl = max(0, b.fl - d); const sy = boatY(S, b); - if (S.ter[((round(b.wy) - 8) & 255) * W + clamp(b.x, 0, 63)] <= CAP) b.wy -= 3 * d; - let g = aim(S, b.x, sy - 2) - b.a; - g = ((g + 3 * PI) % (2 * PI)) - PI; - b.a += clamp(g, -2 * d, 2 * d); - b.ft -= d; - if (b.ft <= 0 && sy > 2 && sy < 96) { + b.fl -= d; + b.a = atan2(S.pl.y - sy + 3, S.pl.x - b.x); + if ((b.ft -= d) <= 0 && sy > 2 && sy < 96) { b.ft = 2.2 - 1.3 * f; - const m = f > 0.5 ? 3 : 1; - for (let i = 0; i < m; i++) shoot(S, b.x + cos(b.a) * 4, sy - 2 + sin(b.a) * 4, b.a + (i - (m - 1) / 2) * 0.25, 38, 2, 0); + for (let i = f > 0.5 ? -1 : 0; i <= (f > 0.5 ? 1 : 0); i++) shoot(S, b.x + cos(b.a) * 4, sy - 3 + sin(b.a) * 4, b.a + i * 0.25, 38, 2, 0); } } S.boats = S.boats.filter((b) => b.hp > 0 && boatY(S, b) < 140); } function stepBoss(S, d) { - let B = S.boss; - if (!B) { - if (S.t < S.bossAt) return; - B = S.boss = { x: 32, y: -34, t0: S.t, st: 0, hp: S.bossN ? 320 + 40 * S.bossN : 60, ph: S.bossN ? 2.8 : 1.6, fl: 0, ang: 0, nx: 0, nr: 0, nf: 0, nd: 0 }; - } const f = S.fury; - const a = S.t - B.t0; - B.fl = max(0, B.fl - d); + // The first boss is short, so its fight fits the first seconds. + if (!S.boss && S.t >= S.bossAt) S.boss = { x: 32, y: -34, t0: S.t, st: 0, hp: S.bossN ? 320 + 40 * S.bossN : 60, ph: S.bossN ? 2.8 : 1.6, ang: 0 }; + const B = S.boss; + if (!B) return; + B.fl -= d; if (B.st === 2) { - // Blowing apart: explosions all over the hull, then one big one. - if (S.t >= B.nd) { + if (!(S.t < B.nd)) { + boom(S, B.x + rnd(-22, 22), B.y + rnd(4, 24), rnd(0.7, 1.4)); B.nd = S.t + 0.07; - const i = floor(Math.random() * BOSS.px.length / 3) * 3; - boom(S, B.x - 28 + BOSS.px[i], B.y + BOSS.px[i + 1], rnd(0.7, 1.4)); } if (S.t - B.t2 > 1.7) { boom(S, B.x, B.y + 12, 3.2); for (let i = 0; i < 6; i++) boom(S, B.x + rnd(-24, 24), B.y + rnd(4, 26), rnd(1, 1.8)); - popup(S, B.x - 6, B.y + 14, 5000); - S.score += 5000; - S.flash = 0.75; - S.caps.push({ x: B.x, y: B.y + 20, t0: S.t }); - S.boss = null; - S.bossAt = S.t + rnd(18, 30); - S.bossN++; + score(S, B.x - 6, B.y + 14, 5000); + S.caps.push({ x: B.x, y: B.y + 20 }); + Object.assign(S, { flash: 0.75, boss: null, bossAt: S.t + rnd(18, 30), bossN: S.bossN + 1 }); } - return; - } - if (B.st === 0) { - const u = min(1, a / 1.4); + } else if (B.st === 0) { + const u = min(1, (S.t - B.t0) / 1.4); B.y = -34 + 40 * (1 - (1 - u) * (1 - u)); - if (u >= 1) { - B.st = 1; - B.t1 = S.t; + if (u >= 1) ((B.st = 1), (B.t1 = S.t)); + } else { + // In turns: a ring, a spiral, aimed fans from the wing guns. + B.x = 32 + 3 * sin((S.t - B.t1) * 0.8); + const ph = floor((S.t - B.t1) / B.ph) % 3; + if (ph === 0 && !(S.t < B.nr)) { + const n = round(12 + 18 * f), a0 = rnd(0, 6.283); + B.nr = S.t + 1.5 - 0.8 * f; + for (let i = 0; i < n; i++) shoot(S, B.x, B.y + 11, a0 + (i * 2 * PI) / n, 28, 0, 1); } - return; - } - B.x = 32 + 3 * sin((S.t - B.t1) * 0.8); - const cy = B.y + 11; - const ph = floor((S.t - B.t1) / B.ph) % 3; - if (ph === 0 && S.t >= B.nr) { - B.nr = S.t + 1.5 - 0.8 * f; - const n = round(12 + 18 * f); - const a0 = Math.random() * 6.283; - for (let i = 0; i < n; i++) shoot(S, B.x, cy, a0 + (i * 2 * PI) / n, 28, 0, 1); - } - if (ph === 1 && S.t >= B.nx) { - B.nx = S.t + 1 / (7 + 23 * f); - B.ang += 0.37; - const arms = f > 0.5 ? 3 : 2; - for (let i = 0; i < arms; i++) shoot(S, B.x, cy, B.ang + (i * 2 * PI) / arms, 32, 1, 0); - } - if (ph === 2 && S.t >= B.nf) { - B.nf = S.t + 1.05 - 0.55 * f; - const m = 3 + round(6 * f); - for (const s of [-1, 1]) { - const tx = B.x + s * 19.5; - const ty = B.y + 10; - const g = aim(S, tx, ty); - for (let i = 0; i < m; i++) shoot(S, tx, ty, g + (i - (m - 1) / 2) * (0.12 + 0.1 * f), 42, 2, 0); + if (ph === 1 && !(S.t < B.nx)) { + const arms = f > 0.5 ? 3 : 2; + B.nx = S.t + 1 / (7 + 23 * f); + B.ang += 0.37; + for (let i = 0; i < arms; i++) shoot(S, B.x, B.y + 11, B.ang + (i * 2 * PI) / arms, 32, 1, 0); + } + if (ph === 2 && !(S.t < B.nf)) { + B.nf = S.t + 1.05 - 0.55 * f; + for (const s of [-1, 1]) fan(S, B.x + s * 19.5, B.y + 10, 3 + round(6 * f), 0.12 + 0.1 * f, 42); } } } -function bomb(S) { - if (S.pl.dead > 0) return; - S.bomb = { x: S.pl.x, y: S.pl.y, t0: S.t, id: S.t }; - S.flash = max(S.flash, 0.9); -} - function stepPlayer(S, d) { - const P = S.pl; - if (P.dead > 0) { - P.dead -= d; - if (P.dead <= 0) { - P.x = 32; - P.y = 134; - P.inv = 2.5; - } - return; - } - P.inv = max(0, P.inv - d); - P.lvlT -= d; - if (P.lvlT <= 0 && P.lvl > 0) { - P.lvl--; - P.lvlT = 14; - } - // Where the fighter would like to be: under the boss, under the nearest enemy, on a capsule. + const P = S.pl, B = S.boss; + if ((P.lvlT -= d) <= 0 && P.lvl > 0) ((P.lvl -= 1), (P.lvlT = 14)); + // Its aim: the boss, a capsule, or half way from its weave to the nearest enemy. const wv = 32 + 18 * sin(S.t * 0.53); - let ax = wv; - const B = S.boss; + let ax = wv, ny = -99; if (B && B.st > 0) ax = B.x + 10 * sin(S.t * 1.1); else if (S.caps.length) ax = S.caps[0].x; - else { - let by = -99; - for (const e of S.en) if (e.y > by && e.y < P.y - 14 && e.y > 0) ((by = e.y), (ax = e.x)); - for (const b of S.boats) { - const y = boatY(S, b); - if (y > by && y < P.y - 14 && y > 0) ((by = y), (ax = b.x)); - } - ax = 0.55 * ax + 0.45 * wv; - } - // Then it picks the nearby spot that the bullets will miss. - let best = 1e9; - let bx = P.x; - let byy = P.y; - for (const ox of [-12, -7, -3, 0, 3, 7, 12]) - for (const oy of [-6, 0, 6]) { - const cx = clamp(P.x + ox, 5, 58); - const cy = clamp(P.y + oy, 98, 116); - let c = 0.05 * abs(cx - ax) + 0.04 * abs(cy - 111) + 0.02 * abs(ox); - for (const b of S.eb) { - if (abs(b.y - cy) > 40) continue; - for (const tau of [0.06, 0.18, 0.34]) { - const dx = b.x + b.vx * tau - cx; - const dy = b.y + b.vy * tau - cy; - const q = dx * dx + 0.6 * dy * dy; - if (q < 42) c += ((42 - q) / 42) * (tau < 0.1 ? 4 : 2); + else for (const e of [...S.en, ...S.boats]) if (yOf(S, e) > max(0, ny) && yOf(S, e) < P.y - 14) ((ny = yOf(S, e)), (ax = 0.55 * e.x + 0.45 * wv)); + // Then the nearby spot the bullets will miss on its way there. + let best = 1e9, bx = P.x, by = P.y; + for (const ox of [-14, -9, -5, -2, 0, 2, 5, 9, 14]) + for (const oy of [-8, -3, 0, 4]) { + const cx = clamp(P.x + ox, 5, 58), cy = clamp(P.y + oy, 96, 116), dist = max(0.01, hypot(cx - P.x, cy - P.y)); + let c = 0.05 * abs(cx - ax) + 0.04 * abs(cy - 111) + 0.015 * dist; + for (const b of S.eb) + for (const tau of abs(b.y - cy) < 45 ? [0.05, 0.12, 0.2, 0.3, 0.42] : []) { + const k = min(1, (60 * tau) / dist), dx = b.x + b.vx * tau - P.x - (cx - P.x) * k, dy = b.y + b.vy * tau - P.y - (cy - P.y) * k; + c += max(0, 30 - dx * dx - 0.7 * dy * dy) / (tau < 0.15 ? 6 : 15); } - } - if (c < best) ((best = c), (bx = cx), (byy = cy)); + if (c < best) ((best = c), (bx = cx), (by = cy)); } - const sp = 52 * d; - P.x += clamp(bx - P.x, -sp, sp); - P.y += clamp(byy - P.y, -sp, sp); - P.ft -= d; - while (P.ft <= 0) { - P.ft += 0.1; - const v = 170; - const add = (x, vx) => S.ps.push({ x: P.x + x, y: P.y - 5, vx, vy: -v }); - add(-2, 0); - add(2, 0); - if (P.lvl >= 1) add(0, 0); - if (P.lvl >= 2) { - add(-3, -38); - add(3, 38); - } - } + P.x += clamp(bx - P.x, -60 * d, 60 * d); + P.y += clamp(by - P.y, -60 * d, 60 * d); + // Two streams, three after a capsule, five after two. + for (P.ft -= d; P.ft <= 0; P.ft += 0.1) + for (const [x, vx] of [[-2, 0], [2, 0], [0, 0], [-3, -38], [3, 38]].slice(0, [2, 3, 5][P.lvl])) S.ps.push({ x: P.x + x, y: P.y - 5, vx, vy: -170 }); } function stepShots(S, d) { - const B = S.boss; + const B = S.boss, P = S.pl; for (const s of S.ps) { s.x += s.vx * d; s.y += s.vy * d; - if (s.y < -4) { - s.dead = 1; - continue; - } - for (const e of S.en) { - if (e.hp <= 0 || abs(s.x - e.x) > 4 || abs(s.y - e.y) > 4) continue; - s.dead = 1; - e.fl = 0.06; - if (--e.hp <= 0) kill(S, e.x, e.y, e.k ? 150 : 100, 1); - break; - } - if (s.dead) continue; - for (const b of S.boats) { - const by = boatY(S, b); - if (b.hp <= 0 || abs(s.x - b.x) > 3.5 || abs(s.y - by) > 6) continue; - s.dead = 1; - b.fl = 0.06; - if (--b.hp <= 0) kill(S, b.x, by, 300, 1.3); - break; - } - if (s.dead || !B || B.st !== 1) continue; - const ix = round(s.x - B.x + 28); - const iy = round(s.y - B.y); - if (ix >= 0 && ix < 56 && iy >= 0 && iy < 30 && BOSS.mask[iy * 56 + ix]) { - s.dead = 1; - B.fl = 0.05; - S.score += 10; - if (--B.hp <= 0) { - B.st = 2; - B.t2 = S.t; - S.eb.length = 0; - } - } + if (s.y < -4) s.dead = 1; + for (const e of [...S.en, ...S.boats]) if (!s.dead && e.hp > 0 && abs(s.x - e.x) <= 4 && abs(s.y - yOf(S, e)) <= (e.boat ? 6 : 4)) ((s.dead = 1), hurt(S, e, 1, e.x, yOf(S, e))); + const ix = round(s.x - B?.x + 28), iy = round(s.y - B?.y); + if (!s.dead && B?.st === 1 && ix >= 0 && ix < 56 && iy >= 0 && iy < 30 && BOSS.mask[iy * 56 + ix]) ((s.dead = 1), (B.fl = 0.05), (S.score += 10), --B.hp); } - S.ps = S.ps.filter((s) => !s.dead); -} - -function stepBullets(S, d) { - const P = S.pl; + // A bullet that reaches the fighter bursts on its shield. for (const b of S.eb) { b.x += b.vx * d; b.y += b.vy * d; if (b.x < -3 || b.x > 66 || b.y < -3 || b.y > 130) b.dead = 1; - else if (P.dead <= 0 && P.inv <= 0 && abs(b.x - P.x) < 2 && abs(b.y - P.y) < 3) { - b.dead = 1; - boom(S, P.x, P.y, 1.6); - P.dead = 1.2; - P.lvl = 0; - if (--P.lives < 0) P.lives = 3; - } - } - const M = S.bomb; - if (M) { - const r = (S.t - M.t0) * 150; - for (const b of S.eb) if (hypot(b.x - M.x, b.y - M.y) < r) b.dead = 1; - for (const e of S.en) - if (e.y > 0 && e.hp > 0 && e.bombed !== M.id && hypot(e.x - M.x, e.y - M.y) < r) { - e.bombed = M.id; - e.hp -= 4; - if (e.hp <= 0) kill(S, e.x, e.y, e.k ? 150 : 100, 1); - } - for (const b of S.boats) { - const by = boatY(S, b); - if (by > 0 && b.hp > 0 && b.bombed !== M.id && hypot(b.x - M.x, by - M.y) < r) { - b.bombed = M.id; - b.hp -= 4; - if (b.hp <= 0) kill(S, b.x, by, 300, 1.3); - } - } - const B = S.boss; - if (B && B.st === 1 && B.bombed !== M.id && r > P.y - B.y - 20) { - B.bombed = M.id; - B.fl = 0.2; - B.hp -= 15; - if (B.hp <= 0) ((B.st = 2), (B.t2 = S.t)); - } - if (r > 160) S.bomb = null; + else if (abs(b.x - P.x) < 1.6 && abs(b.y - P.y) < 2.5) ((b.dead = 1), boom(S, b.x, b.y, 0.4)); } - S.eb = S.eb.filter((b) => !b.dead); + if (B?.st === 1 && B.hp <= 0) ((B.st = 2), (B.t2 = S.t), (S.eb.length = 0)); for (const c of S.caps) { c.y += 14 * d; - if (P.dead <= 0 && abs(c.x - P.x) < 6 && abs(c.y - P.y) < 7) { - c.dead = 1; - P.lvl = min(2, P.lvl + 1); - P.lvlT = 16; - P.pu = S.t; - } + if (abs(c.x - P.x) < 6 && abs(c.y - P.y) < 7) ((c.dead = 1), (P.lvl = min(2, P.lvl + 1)), (P.lvlT = 16)); } - S.caps = S.caps.filter((c) => !c.dead && c.y < 134); + for (const k of ["ps", "eb", "caps"]) S[k] = S[k].filter((o) => !o.dead && o.y < 134); S.fx = S.fx.filter((f) => S.t - f.t0 < f.d); S.pop = S.pop.filter((p) => S.t - p.t0 < 0.9); } @@ -715,346 +392,126 @@ function step(S, d) { S.scroll += V0 * d; S.flash = max(0, S.flash - 2.6 * d); fill(S); - const w = S.waves; - if (S.t >= S.waveAt) { - const busy = S.boss && S.boss.st < 2; - spawnWave(S); - S.waveAt = S.t + (S.waveN === 1 ? 0.45 : busy ? 2 : 1) * (3.3 / w ** 0.75); - } - if (S.t >= S.boatAt) { - spawnBoat(S); - S.boatAt = S.t + rnd(3.5, 6) / w; - } + spawn(S); stepEnemies(S, d); stepBoss(S, d); stepPlayer(S, d); stepShots(S, d); - stepBullets(S, d); } export function update(dt, input, params) { if (!params.s) setup(params); - const S = params.s; - const k = input.knobValues || [0, 2, 0, 0.06]; - dt = clamp(dt || 0, 0, 0.1); - S.stage = clamp(k[0], 0, 1); - S.tempo = clamp(k[1], 0.2, 3) / 2; - S.waves = clamp(k[2], 1, 5); - S.fury = clamp(k[3], 0, 1); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + [S.stage, S.waves, S.fury] = [clamp(k[0], 0, 1), clamp(k[2], 1, 5), clamp(k[3], 0, 1)]; + // The first second plays before the first frame. if (!S.started) { - // The first second is played before the first frame, so the screen opens busy. - S.started = true; + S.started = 1; for (let i = 0; i < 30; i++) step(S, 1 / 30); } - const bp = input.btnPressed || []; - if (bp[0]) bomb(S); - if (bp[3] && !S.boss) S.bossAt = S.t; - let d = dt * S.tempo; - while (d > 0) { - const s = min(d, 1 / 30); - step(S, s); - d -= s; + // The bomb clears the bullets and hurts everything on the screen. + if (input.btnPressed?.[0]) { + Object.assign(S, { flash: 0.9, bomb: S.t, eb: [] }); + for (const e of [...S.en, ...S.boats]) if (e.hp > 0 && yOf(S, e) > 0) hurt(S, e, 4, e.x, yOf(S, e)); + if (S.boss?.st === 1) S.boss.hp -= 15; } + for (let d = (clamp(dt || 0, 0, 0.1) * clamp(k[1], 0.2, 3)) / 2; d > 0; d -= 1 / 30) step(S, min(d, 1 / 30)); } -// ---- drawing ------------------------------------------------------------------ - function put(buf, x, y, r, g, b) { - x = round(x); - y = round(y); - if (x < 0 || y < 0 || x >= W || y >= H) return; - const q = (y * W + x) * 3; - buf[q] = r; - buf[q + 1] = g; - buf[q + 2] = b; + const q = (round(y) * W + round(x)) * 3; + if (x > -0.5 && y > -0.5 && x < W - 0.5 && y < H - 0.5) ((buf[q] = r), (buf[q + 1] = g), (buf[q + 2] = b)); +} +function mix(buf, q, r, g, b, a) { + buf[q] += (r - buf[q]) * a; + buf[q + 1] += (g - buf[q + 1]) * a; + buf[q + 2] += (b - buf[q + 2]) * a; } -// mode 0 draws, 1 draws it white, 2 stamps its shadow, 3 draws it lit up by a hit. +// Mode 1 draws a sprite white, 2 stamps its shadow. function spr(S, sp, x0, y0, mode, glow) { const p = sp.px; - const buf = S.buf; for (let i = 0; i < p.length; i += 3) { - const x = x0 + p[i]; - const y = y0 + p[i + 1]; - if (x < 0 || y < 0 || x >= W || y >= H) continue; + const x = x0 + p[i], y = y0 + p[i + 1], c = mode ? 0xffffff : p[i + 2]; if (mode === 2) { - S.sh[y * W + x] = min(S.sh[y * W + x], 0.55); - continue; - } - const c = p[i + 2]; - const q = (y * W + x) * 3; - if (mode === 1) { - buf[q] = 255; - buf[q + 1] = 255; - buf[q + 2] = 255; - } else if (mode === 3) { - buf[q] = min(255, (c >> 16) + 60); - buf[q + 1] = min(255, ((c >> 8) & 255) + 45); - buf[q + 2] = min(255, (c & 255) + 20); - } else if (c < 0) { - buf[q] = glow[0]; - buf[q + 1] = glow[1]; - buf[q + 2] = glow[2]; - } else { - buf[q] = c >> 16; - buf[q + 1] = (c >> 8) & 255; - buf[q + 2] = c & 255; - } - } -} -function glyph(buf, code, x0, y0, r, g, b) { - for (let j = 0; j < 5; j++) { - const bits = (code >> (3 * (4 - j))) & 7; - for (let i = 0; i < 3; i++) if ((bits >> (2 - i)) & 1) put(buf, x0 + i, y0 + j, r, g, b); + if (x >= 0 && y >= 0 && x < W && y < H) S.sh[y * W + x] = min(S.sh[y * W + x], 0.55); + } else if (c < 0) put(S.buf, x, y, ...glow); + else put(S.buf, x, y, c >> 16, (c >> 8) & 255, c & 255); } } -function number(buf, v, x0, y0, r, g, b, len) { - let s = String(v); - while (s.length < len) s = "0" + s; - for (let i = 0; i < s.length; i++) glyph(buf, GLYPH[s.charCodeAt(i) - 48], x0 + i * 4, y0, r, g, b); +function number(buf, v, x0, y0, r, g, b) { + const s = String(v); + for (let n = 0; n < s.length; n++) + for (let j = 0; j < 15; j++) if ((GLYPH["0123456789PA".indexOf(s[n])] >> (14 - j)) & 1) put(buf, x0 + n * 4 + (j % 3), y0 + floor(j / 3), r, g, b); } +// A ring of width w, or a disc for w 0. function ring(buf, cx, cy, R, w, c) { for (let y = floor(cy - R - w); y <= cy + R + w; y++) - for (let x = floor(cx - R - w); x <= cx + R + w; x++) { - const d = hypot(x - cx, y - cy); - if (abs(d - R) < w) put(buf, x, y, c[0], c[1], c[2]); - } -} -function disc(buf, cx, cy, R, r, g, b) { - for (let y = floor(cy - R); y <= cy + R; y++) - for (let x = floor(cx - R); x <= cx + R; x++) if (hypot(x - cx, y - cy) <= R) put(buf, x, y, r, g, b); -} - -function drawGround(S, si) { - const buf = S.buf; - const pal = PALS[si]; - const base = floor(S.scroll); - const t = S.t; - for (let y = 0; y < H; y++) { - const wy = base + H - 1 - y; - const ro = (wy & 255) * W; - for (let x = 0; x < W; x++) { - let k = S.ter[ro + x]; - if (k === CAP && (t * 1.6 + hash(x, wy, 3) * 4) % 2 > 1.55) k = WAVE; - const q = (y * W + x) * 3; - let r = pal[k * 3]; - let g = pal[k * 3 + 1]; - let b = pal[k * 3 + 2]; - if (si === 2 && (k === FLDA || k === ROOF) && (x + wy) % 2 === 0 && hash(x, wy, 5) < 0.4) ((r = 255), (g = 214), (b = 110)); - if (si === 3 && k === DEEP) { - const h = hash(x, wy, 6); - if (h < 0.035) r = g = b = 90 + 4600 * h * (0.7 + 0.3 * sin(t * 4 + h * 900)); - } - buf[q] = r; - buf[q + 1] = g; - buf[q + 2] = b; - } - } - if (si === 3) { - // A ringed planet far below, drifting by slower than the station. - const py = ((S.scroll * 0.4) % 330) - 60; - const px = 44; - for (let y = max(0, floor(py - 26)); y < min(H, py + 26); y++) - for (let x = 0; x < W; x++) { - const k = S.ter[((base + H - 1 - y) & 255) * W + x]; - if (k > SHAL && k !== CAP) continue; - const dx = x - px; - const dy = y - py; - const u = (dx * 0.94 + dy * 0.34) / 31; - const v = (dy * 0.94 - dx * 0.34) / 6; - const onRing = abs(u * u + v * v - 1) < 0.16; - const inside = dx * dx + dy * dy < 19 * 19; - const q = (y * W + x) * 3; - if (inside && !(onRing && v > 0)) { - const band = floor((dy + 40) / 5) % 2; - const lit = dx * 0.7 + dy * 0.5 > 8 ? 0.5 : 1; - buf[q] = (band ? 220 : 186) * lit; - buf[q + 1] = (band ? 150 : 104) * lit; - buf[q + 2] = (band ? 96 : 62) * lit; - } else if (onRing) { - buf[q] = 210; - buf[q + 1] = 196; - buf[q + 2] = 160; - } - } - } + for (let x = floor(cx - R - w); x <= cx + R + w; x++) if (w ? abs(hypot(x - cx, y - cy) - R) < w : hypot(x - cx, y - cy) <= R) put(buf, x, y, ...c); } export function draw(display, params, time) { const S = params.s; if (!S || !S.started) return; - const buf = S.buf; - const sh = S.sh; - const si = min(3, floor(S.stage * 4)); - const t = S.t; - drawGround(S, si); - // Gunboats on the water, each with its wake and a turret that follows the fighter. + const { buf, sh, cb, pl: P, boss: B, t } = S, + si = min(3, floor(S.stage * 4)), pal = PALS[si]; + for (let y = 0; y < H; y++) + for (let x = 0, wy = floor(S.scroll) + H - 1 - y; x < W; x++) { + let k = S.ter[(wy & 255) * W + x]; + if (k === CAP && (t * 1.6 + hash(x, wy, 3) * 4) % 2 > 1.55) k = WAVE; + put(buf, x, y, pal[k * 3], pal[k * 3 + 1], pal[k * 3 + 2]); + if (si === 2 && k === FLDA && (x + wy) % 2 === 0 && hash(x, wy, 5) < 0.4) put(buf, x, y, 255, 214, 110); + const h = si === 3 && k === DEEP ? hash(x, wy, 6) : 1; + if (h < 0.035) put(buf, x, y, ...Array(3).fill(90 + 4600 * h * (0.7 + 0.3 * sin(t * 4 + x * y)))); + } for (const b of S.boats) { const y = boatY(S, b); - for (let j = 1; j < 8; j++) { - const o = 1 + (j >> 1); - const a = 1 - j / 9; - for (const s of [-1, 1]) put(buf, b.x + s * o, y - 6 - j, 200 * a + 30, 225 * a + 40, 255 * a); - } spr(S, BOAT, b.x - 3, y - 6, b.fl > 0 ? 1 : 0); - for (let j = -1; j <= 1; j++) for (let i = -1; i <= 1; i++) put(buf, b.x + i, y - 3 + j, 90, 95, 110); for (let i = 1; i <= 3; i++) put(buf, b.x + cos(b.a) * i, y - 3 + sin(b.a) * i, 30, 30, 40); } - // Shadows: the clouds' and the aircraft's, a little below and to the right. - // Thick fire dims the ground, the way bullet-hell shooters dim their backdrops so the bullets read. + // Shadows, then the clouds. Thick fire dims the ground as bullet-hell games do. const C = CLOUDS[si]; - const cbase = floor(S.scroll * CLOUD); - const dim = 1 - 0.36 * S.fury; - const cb = S.cb; cb.fill(0); - for (const c of S.clouds) { - const sy = cbase + H - 1 - c.cw; - for (let j = 0; j < c.h; j++) { - const y = sy + j; - if (y < 0 || y >= H) continue; - for (let i = 0; i < c.w; i++) { - const x = c.x + i; - if (x >= 0 && x < W && c.m[j * c.w + i]) cb[y * W + x] = c.m[j * c.w + i]; - } - } - } - for (let y = 0; y < H; y++) - for (let x = 0; x < W; x++) sh[y * W + x] = (x >= 6 && y >= 9 && cb[(y - 9) * W + x - 6] ? C[5] : 1) * dim; - const B = S.boss; - for (const e of S.en) if (e.y > -9) spr(S, e.k ? DART : FIGHTER, round(e.x) + 2, round(e.y) + 6, 2); + for (const c of S.clouds) + for (let j = 0, sy = floor(S.scroll * CLOUD) + H - 1 - c.cw; j < c.h; j++) + for (let i = 0, x = c.x, y = sy + j; i < c.w; i++, x++) if (x >= 0 && x < W && y >= 0 && y < H && c.m[j * c.w + i]) cb[y * W + x] = c.m[j * c.w + i]; + for (let i = 0; i < N; i++) sh[i] = (i % W >= 6 && i >= 9 * W && cb[i - 9 * W - 6] ? C[5] : 1) * (1 - 0.36 * S.fury); + for (const e of S.en) spr(S, e.k ? DART : FIGHTER, round(e.x) + 2, round(e.y) + 6, 2); if (B) spr(S, BOSS, round(B.x) - 22, round(B.y) + 9, 2); - const P = S.pl; - if (P.dead <= 0) spr(S, PLAYER, round(P.x) + 1, round(P.y) + 3, 2); + spr(S, PLAYER, round(P.x) + 1, round(P.y) + 3, 2); for (let i = 0; i < N; i++) { - const s = sh[i]; - if (s < 1) { - buf[i * 3] *= s; - buf[i * 3 + 1] *= s; - buf[i * 3 + 2] *= s; - } - } - // Clouds. - for (let i = 0; i < N; i++) { - const l = cb[i]; - if (!l) continue; - const k = l === 1 ? C[3] : l === 3 ? 1.12 : 1; - const a = l === 3 ? C[4] + 0.12 : C[4]; - const q = i * 3; - buf[q] += (min(255, C[0] * k) - buf[q]) * a; - buf[q + 1] += (min(255, C[1] * k) - buf[q + 1]) * a; - buf[q + 2] += (min(255, C[2] * k) - buf[q + 2]) * a; - } - // Capsules: they blink red and blue and carry a P. - for (const c of S.caps) { - const red = floor(t * 7) % 2; - for (let j = 0; j < 7; j++) - for (let i = 0; i < 5; i++) { - if ((i === 0 || i === 4) && (j === 0 || j === 6)) continue; - put(buf, c.x - 2 + i, c.y - 3 + j, red ? 230 : 40, red ? 40 : 90, red ? 50 : 240); - } - glyph(buf, GLYPH[10], round(c.x) - 1, round(c.y) - 2, 255, 255, 255); - } - for (const e of S.en) if (e.y > -9) spr(S, e.k ? DART : FIGHTER, round(e.x) - 3, round(e.y) - 3, e.fl > 0 ? 1 : 0); - if (B) { - const p = 0.5 + 0.5 * sin(t * 9); - const f = S.fury; - const glow = [120 + 135 * f + 60 * p, 230 - 170 * f + 25 * p * (1 - f), 255 - 215 * f]; - const dying = B.st === 2 && floor(t * 16) % 2 === 0; - const bx = round(B.x) - 28; - const by = round(B.y); - // Engine flames from its four nozzles, upwards. - for (const ex of [15, 16, 17, 38, 39, 40]) - for (let j = 1; j <= 2 + ((t * 20 + ex) % 3 | 0); j++) put(buf, bx + ex, by - j, 255, 230 - j * 50, 120 - j * 40); - spr(S, BOSS, bx, by, dying ? 1 : B.fl > 0 && floor(t * 20) % 3 === 0 ? 3 : 0, glow); - } - if (P.dead <= 0 && (P.inv <= 0 || floor(t * 12) % 2 === 0)) { - const x0 = round(P.x) - 4; - const y0 = round(P.y) - 5; - const FL = [ - [255, 255, 220], - [255, 220, 80], - [255, 140, 30], - [200, 60, 20], - ]; - for (const ex of [3, 5]) { - const L = 2 + ((t * 24 + ex) % 3 | 0); - for (let j = 0; j < L; j++) put(buf, x0 + ex, y0 + 10 + j, ...FL[min(3, j + (L < 3 ? 1 : 0))]); - } - spr(S, PLAYER, x0, y0, 0); - const a = (t - P.pu) / 0.35; - if (a < 1) ring(buf, P.x, P.y, 3 + a * 9, 0.7, [180, 255, 255]); - } - for (const s of S.ps) { - put(buf, s.x, s.y, 255, 255, 230); - put(buf, s.x, s.y + 1, 255, 240, 120); - put(buf, s.x, s.y + 2, 255, 200, 60); - } - // Explosions: a white flash, then a ring that cools from yellow to red, and sparks. + const l = cb[i], k = l === 1 ? C[3] : 1; + mix(buf, i * 3, 0, 0, 0, 1 - sh[i]); + if (l) mix(buf, i * 3, C[0] * k, C[1] * k, C[2] * k, C[4]); + } + for (const c of S.caps) ring(buf, c.x, c.y, 2.9, 0, floor(t * 7) % 2 ? [230, 40, 50] : [40, 90, 240]), number(buf, "P", c.x - 1, c.y - 2, 255, 255, 255); + for (const e of S.en) spr(S, e.k ? DART : FIGHTER, round(e.x) - 3, round(e.y) - 3, e.fl > 0 ? 1 : 0); + // The boss's core pulses cyan, or red as the fury rises. + const f = S.fury, p = 0.5 + 0.5 * sin(t * 9); + if (B) spr(S, BOSS, round(B.x) - 28, round(B.y), floor(t * 16) % 3 === 0 && (B.st === 2 || B.fl > 0) ? 1 : 0, [120 + 135 * f + 60 * p, 230 - 170 * f + 25 * p * (1 - f), 255 - 215 * f]); + for (const ex of [3, 5]) + for (let j = 0, L = 2 + ((t * 24 + ex) % 3 | 0), c; j < L; j++) put(buf, round(P.x) - 4 + ex, round(P.y) + 5 + j, 255 - 12 * (c = min(3, j + (L < 3))) * c, 255 - 65 * c, 220 - 70 * c); + spr(S, PLAYER, round(P.x) - 4, round(P.y) - 5, 0); + for (const s of S.ps) for (let j = 0; j < 3; j++) put(buf, s.x, s.y + j, 255, 255 - 28 * j, 230 - 85 * j); + // Explosions: a white flash, a ring cooling from yellow to red, sparks. for (const f of S.fx) { - const a = (t - f.t0) / f.d; - if (a < 0 || a >= 1) continue; - const R = (1.5 + 4.5 * a) * f.s; - if (a < 0.16) disc(buf, f.x, f.y, (1 + 1.6 * (a / 0.16)) * f.s, 255, 255, 235); - else { - if (a < 0.5) disc(buf, f.x, f.y, R * 0.5, 255, a < 0.3 ? 210 : 120, 40); - ring(buf, f.x, f.y, R, 0.75, a < 0.45 ? [255, 220, 80] : a < 0.72 ? [255, 130, 30] : [180, 46, 24]); - } - if (a < 0.85) for (let i = 0; i < f.sp.length; i += 2) put(buf, f.x + f.sp[i] * a * f.d, f.y + f.sp[i + 1] * a * f.d, 255, 245, 170); + const a = (t - f.t0) / f.d, R = (1.5 + 4.5 * a) * f.s; + if (a < 0.16) ring(buf, f.x, f.y, (1 + 10 * a) * f.s, 0, [255, 255, 235]); + else if (a < 0.5) ring(buf, f.x, f.y, R * 0.5, 0, [255, a < 0.3 ? 210 : 120, 40]); + if (a >= 0.16) ring(buf, f.x, f.y, R, 0.75, a < 0.45 ? [255, 220, 80] : a < 0.72 ? [255, 130, 30] : [180, 46, 24]); + for (let i = 0; a < 0.85 && i < f.sp.length; i += 2) put(buf, f.x + f.sp[i] * a * f.d, f.y + f.sp[i + 1] * a * f.d, 255, 245, 170); } - // Enemy bullets, bright on top of everything, in a glow that grows with the fire. - const hr = 1 + round(2.4 * S.fury); - if (hr > 1) - for (const b of S.eb) { - const c = BUL[b.c]; - for (let j = -hr; j <= hr; j++) - for (let i = -hr; i <= hr; i++) { - const x = round(b.x) + i; - const y = round(b.y) + j; - if (abs(i) + abs(j) > hr || x < 0 || y < 0 || x >= W || y >= H) continue; - const q = (y * W + x) * 3; - buf[q] += (c[0] - buf[q]) * 0.4; - buf[q + 1] += (c[1] - buf[q + 1]) * 0.4; - buf[q + 2] += (c[2] - buf[q + 2]) * 0.4; - } - } for (const b of S.eb) { const c = BUL[b.c]; - const x = round(b.x); - const y = round(b.y); - put(buf, x - 1, y, ...c); - put(buf, x + 1, y, ...c); - put(buf, x, y - 1, ...c); - put(buf, x, y + 1, ...c); - if (b.big) for (const [i, j] of [[-1, -1], [1, -1], [-1, 1], [1, 1]]) put(buf, x + i, y + j, c[0] * 0.6, c[1] * 0.6, c[2] * 0.6); - put(buf, x, y, 255, 255, 255); + for (let i = 0; i < 9; i++) if (i % 2 || (b.big && i !== 4)) put(buf, b.x + (i % 3) - 1, b.y + floor(i / 3) - 1, ...c.map((v) => (i % 2 ? v : v * 0.6))); + put(buf, b.x, b.y, 255, 255, 255); } for (const p of S.pop) { const a = (t - p.t0) / 0.9; - if (a > 0.75 && floor(t * 20) % 2) continue; - const s = String(p.v); - number(buf, p.v, round(p.x - s.length * 2), round(p.y - a * 9), 255, 240, 120, 0); - } - // The bomb: a shock ring from the fighter, and the whole screen flashes white. - if (S.bomb) { - const r = (t - S.bomb.t0) * 150; - ring(buf, S.bomb.x, S.bomb.y, r, 1.6, [200, 250, 255]); - } - if (S.flash > 0) { - const a = min(1, S.flash); - for (let i = 0; i < N * 3; i++) buf[i] += (255 - buf[i]) * a * 0.85; - } - // The score and the lives, small, on a dark strip. - for (let y = 0; y < 7; y++) - for (let x = 0; x < W; x++) { - if (x > 25 && x < 62 - 6 * P.lives) continue; - const q = (y * W + x) * 3; - buf[q] *= 0.18; - buf[q + 1] *= 0.18; - buf[q + 2] *= 0.18; - } - number(buf, S.score % 1000000, 1, 1, 255, 255, 255, 6); - for (let i = 0; i < P.lives; i++) spr(S, LIFE, 58 - i * 6, 2, 0); - for (let p = 0; p < N; p++) { - const q = p * 3; - display.setPixel(p % W, (p / W) | 0, min(255, buf[q]) | 0, min(255, buf[q + 1]) | 0, min(255, buf[q + 2]) | 0); - } + if (a < 0.75 || floor(t * 20) % 2) number(buf, p.v, round(p.x - String(p.v).length * 2), round(p.y - a * 9), 255, 240, 120); + } + if (t - S.bomb < 1) ring(buf, P.x, P.y, (t - S.bomb) * 150, 1.6, [200, 250, 255]); + for (let i = 0; S.flash > 0 && i < N * 3; i++) buf[i] += (255 - buf[i]) * min(1, S.flash) * 0.85; + for (let i = 0; i < 7 * W; i++) if (i % W < 26 || i % W >= 50) mix(buf, i * 3, 0, 0, 0, 0.82); + number(buf, String(S.score % 1e6).padStart(6, "0"), 1, 1, 255, 255, 255); + number(buf, "AAA", 52, 1, 220, 228, 240); + for (let p = 0; p < N; p++) display.setPixel(p % W, (p / W) | 0, min(255, buf[p * 3]) | 0, min(255, buf[p * 3 + 1]) | 0, min(255, buf[p * 3 + 2]) | 0); } From b1222505a611be9754c5ec1c9b5f31acba310a90 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 02:13:50 +0000 Subject: [PATCH 30/33] shoot-em-up: gentler boss hit blink, rings of twelve at the start knobs --- incoming/pattern-study/patterns/shoot-em-up.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/incoming/pattern-study/patterns/shoot-em-up.js b/incoming/pattern-study/patterns/shoot-em-up.js index 748fa54bd..282415e1e 100644 --- a/incoming/pattern-study/patterns/shoot-em-up.js +++ b/incoming/pattern-study/patterns/shoot-em-up.js @@ -17,15 +17,12 @@ // Knob 4: Bullets. How thick the fire is, from rings of twelve at 0.06 to rings of thirty and three-armed spirals at 1, with the ground dimmed so they read. // Buttons: K1 tap drops a bomb: a white flash and a shock ring that clears the bullets. -const W = 64; -const H = 128; -const N = W * H; +const W = 64, H = 128, N = W * H; const { min, max, sin, cos, abs, atan2, hypot, floor, round, sqrt, PI } = Math; const clamp = (v, a, b) => (v < a ? a : v > b ? b : v); const rnd = (a, b) => a + (b - a) * Math.random(); // Ground scroll at Speed 2 in LEDs a second; clouds go 1.8 times as fast. -const V0 = 12; -const CLOUD = 1.8; +const V0 = 12, CLOUD = 1.8; const hash = (x, y, s) => { let h = Math.imul((x * 374761393 + y * 668265263 + s * 982451653) | 0, 1274126177); @@ -33,8 +30,8 @@ const hash = (x, y, s) => { return ((h ^ (h >>> 16)) >>> 0) / 4294967296; }; function noise(x, y, s) { - const ix = floor(x), iy = floor(y), u = (x -= ix) * x * (3 - 2 * x), v = (y -= iy) * y * (3 - 2 * y), a = hash(ix, iy, s), b = hash(ix + 1, iy, s); - const c = hash(ix, iy + 1, s); + const ix = floor(x), iy = floor(y), u = (x -= ix) * x * (3 - 2 * x), v = (y -= iy) * y * (3 - 2 * y); + const a = hash(ix, iy, s), b = hash(ix + 1, iy, s), c = hash(ix, iy + 1, s); return a + (b - a) * u + (c - a) * v + (a - b - c + hash(ix + 1, iy + 1, s)) * u * v; } @@ -314,7 +311,7 @@ function stepBoss(S, d) { B.x = 32 + 3 * sin((S.t - B.t1) * 0.8); const ph = floor((S.t - B.t1) / B.ph) % 3; if (ph === 0 && !(S.t < B.nr)) { - const n = round(12 + 18 * f), a0 = rnd(0, 6.283); + const n = floor(12 + 18 * f), a0 = rnd(0, 6.283); B.nr = S.t + 1.5 - 0.8 * f; for (let i = 0; i < n; i++) shoot(S, B.x, B.y + 11, a0 + (i * 2 * PI) / n, 28, 0, 1); } @@ -486,7 +483,7 @@ export function draw(display, params, time) { for (const e of S.en) spr(S, e.k ? DART : FIGHTER, round(e.x) - 3, round(e.y) - 3, e.fl > 0 ? 1 : 0); // The boss's core pulses cyan, or red as the fury rises. const f = S.fury, p = 0.5 + 0.5 * sin(t * 9); - if (B) spr(S, BOSS, round(B.x) - 28, round(B.y), floor(t * 16) % 3 === 0 && (B.st === 2 || B.fl > 0) ? 1 : 0, [120 + 135 * f + 60 * p, 230 - 170 * f + 25 * p * (1 - f), 255 - 215 * f]); + if (B) spr(S, BOSS, round(B.x) - 28, round(B.y), (B.st === 2 ? floor(t * 16) % 2 : B.fl > 0 && floor(t * 20) % 4 === 0) ? 1 : 0, [120 + 135 * f + 60 * p, 230 - 170 * f + 25 * p * (1 - f), 255 - 215 * f]); for (const ex of [3, 5]) for (let j = 0, L = 2 + ((t * 24 + ex) % 3 | 0), c; j < L; j++) put(buf, round(P.x) - 4 + ex, round(P.y) + 5 + j, 255 - 12 * (c = min(3, j + (L < 3))) * c, 255 - 65 * c, 220 - 70 * c); spr(S, PLAYER, round(P.x) - 4, round(P.y) - 5, 0); From 27ad21a03ce0b2e763366ed424fdbf78bb9c7f39 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 02:17:15 +0000 Subject: [PATCH 31/33] =?UTF-8?q?README=20=EC=97=90=2024=ED=8C=90=20?= =?UTF-8?q?=EC=A0=88=EA=B3=BC=20=EB=8B=A4=EC=9D=8C=20=ED=8C=90=20=EA=B0=90?= =?UTF-8?q?=EC=9D=84=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_01KTNzLtP3gH9rQENoucsegc --- incoming/pattern-study/README.md | 59 ++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index f46700506..abdcd3523 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 138개를 마흔네 갈래로 나눠 만들었어요. 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판도 열 갈래에 하나씩 열 개를 더했고, 무대·공연과 짐승 갈래를 새로 열었어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 148개를 마흔여섯 갈래로 나눠 만들었어요. 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판도 열 갈래에 하나씩 열 개를 더했고, 공사·건설과 도자기 갈래를 새로 열었어요. [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판에서 세로로 다시 짰어요. @@ -259,6 +259,7 @@ | [Pendulum Wave](patterns/pendulum-wave.js) | 물리·스프링 | 길이가 조금씩 다른 진자 줄을 위에서 내려다봐요. 한꺼번에 놓은 추들이 한 줄로 흔들리다가 뱀처럼 휘고, 세 줄, 두 줄로 엇갈렸다가 28초 만에 다시 한 줄로 모여요. K1 이 강철에서 놋쇠와 구리를 지나 무지개 유리구슬까지 추를 바꾸고, K4 를 올리면 추가 지나간 자리에 빛 꼬리가 남아요. | | [Pipe Organ](patterns/pipe-organ.js) | 음악·악기 | 어두운 성당의 파이프오르간 정면이 패널을 채워요. 보이지 않는 오르가니스트가 여덟 화음 코랄을 되풀이하면, 소리 나는 파이프마다 입에서 빛이 켜지고 몸통을 따라 정상파 띠가 올라가요. 가운데 가장 긴 파이프가 가장 낮은 음이에요. K1 이 플루트의 파랑, 프린시펄의 은빛, 리드의 금빛과 빨강 사이에서 음전을 고르고, K1 을 누르면 파이프 열아홉이 다 울려요. | | [Potter's Wheel](patterns/potters-wheel.js) | 도자기 | 밤의 도자기 공방에서 녹색 갓 램프 아래 물레가 돌아요. 두 손이 흙덩이를 가운데로 모아 벽을 끌어올리면 그릇이 76~92줄 높이로 자라고, 물레 자국이 램프 쪽에서 반짝이며 옆으로 흘러가요. 다 빚은 병과 항아리는 줄로 잘려 선반에 올라가 젖은 흙빛에서 하얗게 말라 가요. K1 이 흙을 테라코타에서 검은 흙까지 바꾸고, K1 을 누르면 그릇이 주저앉아요. | +| [Shoot 'em Up](patterns/shoot-em-up.js) | 픽셀 아트 | 세로로 흐르는 아케이드 슈팅 게임이 혼자 돌아요. 섬과 해안, 들판과 빨간 지붕 마을이 아래로 흘러가고, 흰 전투기가 노란 탄을 쏘며 S 자로 들어오는 편대와 포탑을 돌리는 포함을 떨어뜨려요. 화면 위 3분의 1을 덮는 보스가 내려와 고리와 나선 탄막을 뿌리다 연쇄 폭발로 부서져요. K1 이 열대 바다에서 붉은 협곡, 밤 도시, 우주 정거장까지 판을 바꾸고, K1 을 누르면 폭탄이 터져요. | | [Thermal](patterns/thermal.js) | 비행 | 여름 오후의 밭 위에서 흰 글라이더가 열기류를 타고 타원을 그리며 맴돌아 올라가요. 말똥가리 둘이 더 좁게 같이 돌고, 꼭대기의 적운은 자라며 바닥이 어두워져요. 구름 바닥에 닿은 글라이더는 다음 열기류로 미끄러져 가고 아래에서 새 글라이더가 들어와요. K1 을 누르면 윈치가 글라이더 하나를 끌어올려요. | | [Tower Crane](patterns/tower-crane.js) | 공사·건설 | 해 질 녘 올라가는 콘크리트 건물 옆에 노란 타워크레인이 서서, 마당의 벽판과 철근 다발, 거푸집, 콘크리트 버킷을 차례로 꼭대기 층에 올려요. 붐이 원근 속에서 돌며 짧아졌다 길어지고, 버킷이 바닥을 부으면 건물이 한 층 자라 새 층 창에 불이 들어와요. K1 을 끝까지 돌리면 밤이 되어 투광등이 켜져요. | | [Waggle Dance](patterns/waggle-dance.js) | 곤충 | 관찰 벌통의 벌집 한 장이 패널 높이로 서요. 위에 봉한 꿀, 그 아래 여덟 색 꽃가루 띠와 애벌레 방이 있고, 벌 여든 마리가 기어 다녀요. 아래쪽 빈 벌집에서는 꽃가루를 단 벌이 몸을 흔들며 8 자 춤을 추고 따르는 벌들이 둘러서요. 춤의 각도가 해에서 본 꽃밭의 방향이라 K4 를 돌리면 모든 춤이 같이 돌아요. | @@ -973,6 +974,50 @@ K4 Spate 0 이면 물이 실 몇 가닥으로 갈라져요. 처음 값 0.06 이 지도 주제 없이 남은 자리로 새 갈래에 넷째 패턴을 냈어요. 22판 「다음 판 감」이 아직 없다고 적은, 밤하늘을 건너는 철새 떼예요. 1판의 Flock 은 이웃만 보고 나는 무리이고, Skein 은 선두를 꼭짓점에 두고 양 팔에 줄을 서는 V 자 대형이에요. 선두는 처음에 3~5초에, 그 뒤로는 6~9초마다 한쪽 팔로 물러나요. 하늘의 노을과 달무리가 판을 거의 다 밝혀서, 걱정은 채움의 아래 끝이 아니라 위 끝이었어요. 채움은 0.975 로 축 끝 1.0 아래이고, 오른쪽 아래 구석 하늘을 거의 검게 낮춰도 달무리가 그 구석을 다시 밝혀서 0.981 에서 0.975 로만 내려갔어요. 씨앗 열여섯에서 `thirds` 는 97/97/96, 바뀐 LED 는 위 72~77%, 가운데 77~84%, 아래 49~57%, 모양 바뀜은 0.057~0.068 이에요. K3 Geese 는 작은 기러기 몇 마리만 늘 때 0.02 였고, 큰 떼일수록 높이 날아 작게 보이도록 크기와 간격을 같이 바꾸고 첫 떼를 패널 가운데에서 열자 0.033 이 됐어요. +## 24판 + +24판도 새 패턴 열 개를 열 갈래에 하나씩 냈어요. 주제 1 은 겨눌 칸이 없었어요. 23판 뒤로 `--gaps` 가 찍는 칸에 `*` 가 하나도 없어서예요. 그 대신 남은 자리로 짠 Waggle Dance 가 `*` 없이 비어 있던 마지막 칸, 복잡도 × 채움 4,3 에 섰어요. 이제 지도 21장의 칸이 다 차요. 주제 2 로 공사·건설 갈래를 Tower Crane 으로, 도자기 갈래를 Potter's Wheel 로 열었어요. 주제 4 는 Tower Crane 과 Potter's Wheel, Emergence, Flying Trapeze, Thermal, Pipe Organ, Shoot 'em Up 으로 일곱이에요. 일곱 다 3분의 1마다 켜진 LED 도, 2~8초 동안 24/255 넘게 바뀐 LED 도 5% 를 넘어요. Waggle Dance 와 Breath, Pendulum Wave 는 주제 없이 남은 자리로 짰어요. 무대·공연과 짐승은 패턴이 하나뿐이던 갈래라 이번에 둘째가 섰고, 몸과 비행에는 셋째가, 곤충과 물리·스프링, 음악·악기, 픽셀 아트에는 넷째가 섰어요. 주제 3 은 앞 바퀴 PR 에 창작자 글이 없어서 하지 않았어요. 열 개를 다 넣고 재도 일곱 축의 양 끝은 그대로라서 칸 경계는 14판과 같아요. + +### Tower Crane + +주제 2 로 공사·건설 갈래를 열고, 주제 4 로 세운 패널의 128줄을 짓고 있는 건물 하나로 채웠어요. 위 3분의 1은 붐과 균형추, 가운데는 콘크리트 골조와 창, 아래는 믹서 트럭과 투광등이 선 마당이에요. 하늘과 건물을 다 칠해서 `thirds` 는 99/95/100 이고, 2~8초 동안 바뀐 LED 는 52.6/62.2/47.8% 예요. 구름을 멈추고 재도 45.2/54.1/36.3% 라서, 그 값은 갈고리가 마당까지 내려갔다가 벽판을 35번 줄까지 끌어올리는 길과 도는 붐에서 와요. 첫 판은 갈고리가 마당으로 내려가는 데서 열리고 첫 타설이 22초쯤이어서 모양 바뀜이 0.020 으로 Galton Board 아래였어요. 타설 중간에서 열어 3.0초에 한 층이 끝나고 0.7초 뒤 새 층 창에 불이 들어오게 하고 위 하늘을 어둡게 하자 0.036 이 됐고, 지금은 씨앗 열여섯에서 0.036~0.048 이에요. K2 Speed 도 타설과 새 층이 첫 3초 안에 들어오자 0.03 에서 0.05 가 됐어요. 낮의 창은 콘크리트와 밝기가 같아 창으로 읽히지 않았어요. 유리 낀 층의 창턱을 (78,86,102) 로 어둡게 하고 켜진 유리를 호박색으로 두었어요. + +### Potter's Wheel + +주제 2 로 도자기 갈래를 열고, 주제 4 로 물레 위에서 자라는 그릇 하나가 패널 높이의 60~72% 를 쓰게 했어요. 선반에서 마르는 그릇이 위 3분의 1을 채워서 `thirds` 는 97/92/79 이고, 바뀐 LED 는 씨앗 열여섯에서 위 15.2~31.6%, 가운데 91.5~97.2%, 아래 81.0~81.3% 예요. 그릇을 64~90줄로 빚을 때는 씨앗 넷에서 위 3분의 1의 바뀐 LED 가 14.5, 0.6, 16.1, 0.0% 였어요. 다 빚은 그릇이 위 3분의 1에 닿느냐가 씨앗마다 갈렸어요. 76~92줄로 키우고 첫 그릇을 늘 가장 키 큰 세 꼴 가운데 하나로 두자 다 넘었어요. 첫 판의 물레 자국은 용수철이나 쌓은 동전처럼 보였어요. 자국의 깊이를 0.32 에서 0.07 로 줄이고 반사광 지수를 16 에서 40 으로 올리자 자국이 램프 쪽 반사 띠 안에서만 보이고, 그 띠 안에서 흙 알갱이와 흙물 자국이 옆으로 흘러가 물레가 도는 것이 읽혀요. K1 Clay 는 그릇과 흙물 색만 바꿀 때 0.02 였고, 흙먼지가 벽과 선반 판자에 흙빛을 묻히게 하자 0.066 이 됐어요. + +### Emergence + +주제 4 로 짐승 갈래의 둘째를 세웠어요. 23판이 「다음 바퀴」에 남긴 물음은 어두운 하늘에서 박쥐 떼만으로 3분의 1마다 5% 를 넘기느냐였어요. 하늘과 언덕, 구름을 지우고 박쥐만 제 어두운 만큼 밝혀 재면, 한 프레임에 켜진 LED 가 씨앗 넷에서 13.4~13.6/9.9~10.2/5.1~5.3% 예요. 아래 3분의 1은 기둥이 좁고 동굴 입구 앞의 박쥐까지 세서 겨우 넘어요. 2~8초 동안 바뀐 LED 로 세면 76.5~79.3/72.0~81.1/23.5~25.0% 라서 떼는 128줄을 다 쓰고, 표의 `thirds` 94/96/92 는 거의 다 노을 하늘이에요. 매 한 마리의 몫도 셌어요. 4.5초에 매가 기둥을 뚫고 지나가면 바뀐 LED 가 위 3분의 1에서 6 포인트, 가운데에서 17~26 포인트 올라요. 박쥐는 날개를 V, 一, ^ 꼴로 바꾸는 1~3 LED 점이에요. 첫 판은 동굴 위 기둥 밑동이 검은 줄기 하나로 뭉쳐 연기처럼 보였어요. 리본 두께를 ±0.35 에서 ±0.6 으로 넓히고 입구에서 나오는 속도를 1.6 배로 올리자, 아래 15줄에 박쥐 28마리가 흩어져 하나하나 읽혀요. K1 을 끝까지 돌린 밤에는 모양 바뀜이 0.024~0.026 으로 축 아래 끝에 붙어요. + +### Flying Trapeze + +주제 4 로 무대·공연 갈래의 둘째를 세웠어요. 위 3분의 1은 천막 꼭대기의 줄무늬 천과 전구 고리, 가운데는 그네와 캐처, 아래는 그물과 어두운 객석이에요. `thirds` 는 81/35/34 이고 바뀐 LED 는 93.9/87.9/57.5% 예요. 사람이 10 LED 키라서 모양 바뀜과 노브 반응은 거의 다 사람을 따라가는 팔로 스폿 빛줄기와, 받기와 착지에 맞춰 바뀌는 천막 조명에서 와요. 첫 판은 천을 50번 줄까지 밝히고 연무가 짙어서 채움 0.610, 모양 바뀜 0.193 에 사람이 빛 속에 묻혔어요. 빛줄기를 좁히고 등에서 멀수록 옅어지게 하고, 천을 어둡게 해 트러스 위에서 끝나게 하자 채움 0.516, 모양 바뀜 0.135 가 되고 사람이 보였어요. 그 대신 K1 Lights 가 0.08 에서 0.05 로 내려갔어요. 세운 패널의 너비가 64 LED 라 손을 놓는 자리와 받는 자리가 5~8 LED 밖에 안 떨어져서, 재주는 건너가는 것보다 제자리에서 도는 고리로 보여요. 시작 노브에서는 2.7초에 손을 놓아 3.7초에 잡히고, 다음 차례는 6.1초에 그물로 떨어져 7.0초에 튀어 올라요. + +### Thermal + +주제 4 로 비행 갈래의 셋째를 세웠어요. 아래는 밭과 산울타리, 가운데는 열기류를 맴도는 글라이더와 말똥가리 둘, 위는 자라는 적운이에요. 하늘과 땅을 다 칠해서 `thirds` 는 100/100/99 이고, 바뀐 LED 는 씨앗 열여섯에서 위 32.4~36.3%, 가운데 21.2~25.3%, 아래 24.5~30.3% 예요. 아래 3분의 1의 값은 구름 그림자가 밭을 건너가며 내요. 첫 판은 모양 바뀜이 0.021 이었어요. 구름이 자라는 것이 0.008 밖에 보태지 않아서 칸마다 재 보니, 가장 밝은 채널로 보면 200~222 의 파란 하늘과 흰 구름이 거의 같은 밝기였어요. 천정을 (22,76,196) 에서 (14,50,150) 으로, 중간 하늘을 (52,122,222) 에서 (44,108,206) 으로 어둡게 하자 위의 16줄 띠 둘이 0.0066 에서 0.0207 이 되고 모양 바뀜이 0.042 가 됐어요. 지금은 씨앗 열여섯에서 0.034~0.039 예요. 움직임 0.027 은 Analemma, Stained Glass 와 같은 값으로, Star Trails 다음으로 낮아요. 글라이더는 실제 꼴대로 투영해서, 날개 18 LED 가 다 보이는 것은 이쪽으로 오거나 멀어질 때이고 옆을 보일 때는 동체와 T 꼬리 10 LED 남짓이에요. + +### Pipe Organ + +주제 4 로 음악·악기 갈래의 넷째를 세웠어요. 앞 파이프 열아홉이 길이대로 다장조 음계이고, 가운데 탑의 92줄 파이프가 가장 낮은 음이에요. 베이스는 탑 파이프를, 선율은 위쪽 작은 파이프를 오가서 화음이 바뀔 때마다 패널 위아래가 같이 바뀌어요. 케이스를 다 칠해 `thirds` 는 63/84/85 예요. 장미창 빛을 끄고 재도 바뀐 LED 가 씨앗 넷에서 24.3~39.8/38.6~52.2/18.2~27.1% 라서, 128줄을 바꾸는 것은 음악이에요. 첫 판에서는 쉬는 주석 파이프가 밝고 푸르러서 파란 플루트 빛이 묻혔어요. K1 0.0061, K4 0.0090, 모양 바뀜 0.023 이었어요. 바탕 빛을 (112,120,140) 에서 (56,56,64) 로 낮춰도 K1 은 0.010 이었고, 소리 나는 파이프의 빛이 좌우 16열까지 이웃 주석에 비치게 하자 모양 바뀜 0.079, K1 0.02 가 되어 처음 검사를 통과했어요. 리드 음전에 16피트와 12도, 15도를 더하자 K1 이 0.062 가 됐어요. + +### Shoot 'em Up + +주제 4 로 픽셀 아트 갈래의 넷째를 세웠어요. 위 3분의 1에는 점수와 보스, 가운데에는 편대, 아래에는 100~118번 줄의 전투기가 서요. 바닥이 1초에 12 LED 로 흘러서 `thirds` 는 98/99/100, 바뀐 LED 는 97.3/100.0/100.0% 이고, 이 값은 거의 다 흐르는 바닥이에요. 노브 검사는 첫 3초를 봐요. 처음 K4 Bullets 는 그때 보스 탄이 하나도 없어서 0.0041 이었고, 탄막 게임처럼 탄이 짙을수록 바닥을 최대 36% 어둡게 하자 0.10 이 됐어요. 첫 보스는 체력을 110 에서 60 으로 줄여 2.2초에 내려와 7.1초부터 부서지게 했어요. 마지막 폭발 섬광은 8.8초에 두었어요. 그 섬광 하나가 창 안에 들면 모양 바뀜이 0.30 이고, 창 밖이면 씨앗 열여섯에서 0.090~0.128 이에요. 판마다 색 몇 가지로만 칠해서 가장자리가 0.743 으로 열 개 가운데 가장 또렷하고, 복잡도는 0.308 로 가장 낮아요. 첫 판은 37 KB 였어요. 25 KB 아래로 줄이며 전투기가 죽고 다시 나는 것과 배의 물결 자국, 우주 판의 고리 행성을 뺐어요. + +### Waggle Dance + +남은 자리로 곤충 갈래의 넷째를 세우고, 지도에 남은 하나뿐인 빈 칸인 복잡도 × 채움 4,3 을 겨눴어요. 복잡도가 2.173 을 넘고 채움이 0.558~0.815 인 자리예요. 씨앗 열여섯 모두 그 칸에 서요. 복잡도 2.471~2.550, 채움 0.705~0.749 예요. 첫 판은 복잡도 2.766 으로 축 끝 2.735 를 넘었고 채움이 0.850 이었어요. 봉한 애벌레 방 사이 이음매와 벌을 어둡게 하자 2.653, 0.711 이 됐는데, 열린 방의 테두리를 밝혀야 육각 격자가 읽혔고 그러자 2.765, 0.838 로 돌아갔어요. 이음매를 0.15 로 낮추고 창 볕이 거기 닿지 않게 하고, 애벌레 방을 올려 아래 줄을 빈 벌집으로 두자 2.689, 0.782 가 됐어요. 그다음은 벌이 보이게 하는 일이었어요. 벌과 벌집이 같은 호박색이라, 벌집의 반사율을 0.66 으로 낮추고 벌을 금빛으로 밝히자 2.488, 0.749 가 됐어요. LED 마다 밝기 ±6%, 채널마다 ±3% 의 낟알이 복잡도를 0.13 올리지만, 낟알 없이도 2.434 로 칸 안이에요. 육각 줄 간격을 5.196 에서 5 로 4% 눌러야 모든 칸이 3-5-5-5-3 LED 로 똑같이 그려졌어요. + +### Breath + +남은 자리로 몸 갈래의 셋째를 세웠어요. 23판까지 「다음 판 감」이 아직 없다고 적어 둔 숨 쉬는 폐예요. 후두에서 횡격막까지 해부도처럼 그렸고, 기관이 두 기관지로 갈라진 뒤 일곱 번 더 갈라져 포도송이 129개, 허파꽈리 681개에 닿아요. 숨을 내쉰 폐가 판의 42% 를 덮어서, 22판 Circulation 처럼 몸 안에서만 바뀌는 노브가 낮게 나오는 일은 없었어요. 첫 판부터 노브 넷이 0.10/0.13/0.09/0.10 이었어요. 노브마다 폐 전체의 색이나 크기를 바꿔서예요. 숨마다 허파꽈리가 보라에서 빨강으로 물들어 모양 바뀜이 0.135 이고, 씨앗 열여섯에서 0.134~0.137 이에요. 처음 기관지 나무는 갈래마다 끝점과 무게중심을 잇는 선으로 나눴는데, 평면의 폐가 너비의 두 배 높이라 아래 엽이 길게 나란한 줄로 찼어요. 셋째 갈래부터 긴 축을 가로질러 나누자 줄기 하나에 곁가지가 붙는 꼴이 됐어요. 가는 가지는 둘레에 반지름만큼 최대 1.7 LED 의 그림자를 둘러야 허파꽈리 위에서 보였어요. 세 반복문의 `Math.hypot` 을 `sqrt` 로 바꾸자 한 프레임이 2.9 ms 에서 1.9 ms 가 됐어요. 숨을 내쉴 때 허파꽈리가 보라로 돌아가는 것은 그림의 몫이에요. 실제로 폐를 떠나는 피는 산소를 실은 채예요. + +### Pendulum Wave + +남은 자리로 물리·스프링 갈래의 넷째를 세웠어요. 진자 줄을 위에서 내려다봐서, 16~24개의 추가 4번 줄부터 124번 줄까지 세로로 늘어서고 저마다 ±22 LED 를 옆으로 흔들려요. i 번째 진자가 한 주기에 22+i 번 흔들리고, 시작 노브에서 한 주기가 28초예요. 0.65초에 판에서 놓으면 2~8초 동안 줄이 뱀처럼 휘고, 10초쯤 세 줄, 15초쯤 두 줄로 엇갈렸다가 28초에 다시 한 줄이 돼요. `Math.random` 을 쓰지 않아서 씨앗 열여섯이 다 같은 값이에요. 첫 판은 색이 0.060 으로 축 아래 끝 Rainstorm 의 0.066 밑이었어요. 호두나무 판을 붉게 하고 바닥을 밝히자 0.098 이 됐는데, 이번에는 모양 바뀜이 0.039 에서 0.023 으로 떨어졌어요. 모양 바뀜은 추와 바닥의 밝기 차이에서 나와서, 추를 밝히고 바닥을 다시 어둡게 해 색 0.080, 모양 바뀜 0.033 에 세웠어요. 판에 붙잡았다 놓는 첫 장면은 모양 바뀜을 올리지 않았어요. 0.030 이 0.029 가 됐어요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -1015,17 +1060,17 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 24일 `dev`(`02d079e`)의 128개에 23판의 열 개를 더한 138개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 24일 `dev`(`b1af21e`)의 138개에 24판의 열 개를 더한 148개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 `study.ts --check --gaps` 가 `*` 를 붙인 칸 가운데 그림이 떠오르는 칸이에요. 칸은 가로와 세로를 낮은 쪽부터 1~4 로 세요. 페이지 기본 지도(가장자리 × 움직임)는 16칸이 다 차 있어요. -23판 뒤로는 `*` 가 붙은 칸이 없어요. 22판까지 남은 두 칸, 촘촘함 × 모양 바뀜 3,4 와 4,3 에 23판의 Bait Ball 과 Sequins 가 섰어요. 남은 빈 칸은 복잡도 × 채움 4,3 하나예요. 복잡도가 2.173 을 넘고 채움이 0.558~0.815 인 자리인데, 복잡도 축 끝의 Contour Map(2.735)을 빼고 세면 차는 칸이라 `*` 가 붙지 않아요. 그 아래 복잡도 2.4~2.6 에 선 Woodpecker, Dripstone, Agate, Guilloché 는 채움이 다 0.93 을 넘어요. +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 으로 축 끝을 넘었어요. 촘촘함이 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 로 내려갔어요. -모양 바뀜이 높은 칸을 겨누면 움직이는 것과 바탕의 밝기가 달라야 해요. 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 에 그쳤어요. +모양 바뀜이 높은 칸을 겨누면 움직이는 것과 바탕의 밝기가 달라야 해요. 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 이에요. 움직임 축의 아래 끝도 끝이에요. 아래 끝은 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 가 됐어요. @@ -1035,19 +1080,19 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ### 아직 없는 갈래 -`families.json` 의 마흔네 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 23판이 연 무대·공연 갈래는 Proscenium 하나예요. 서커스 천막 꼭대기의 공중그네가 그 갈래에 맞아요. 23판이 연 짐승 갈래도 Giraffe 하나예요. 해 질 녘 동굴에서 줄지어 날아오르는 박쥐 떼는 128줄을 다 써요. 22판이 연 몸 갈래는 Circulation 과 23판의 Axon 이에요. 숨 쉬는 폐는 아직 없어요. 22판이 연 비행 갈래는 Liftoff 와 23판의 Final Approach 예요. 열기류를 타고 맴돌며 오르는 글라이더는 아직 없어요. 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 예요. 관람차는 Drop Tower 뒤에 테두리로만 서요. 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 예요. 15판이 연 도시 갈래는 Metro Map 과 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판이 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables 예요. 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` 의 마흔여섯 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 24판이 연 공사·건설 갈래는 Tower Crane 하나예요. 굴착기가 땅을 파 내려가는 터파기 현장은 아래로 깊이를 써요. 24판이 연 도자기 갈래도 Potter's Wheel 하나예요. 장작 가마의 불구멍으로 불꽃이 새는 소성은 아직 없어요. 23판이 연 무대·공연 갈래는 Proscenium 과 24판의 Flying Trapeze 예요. 위의 조종대에서 내려온 줄에 매달려 춤추는 꼭두각시는 128줄을 다 써요. 23판이 연 짐승 갈래는 Giraffe 와 24판의 Emergence 예요. 22판이 연 몸 갈래는 Circulation 과 23판의 Axon, 24판의 Breath 예요. 22판이 연 비행 갈래는 Liftoff 와 23판의 Final Approach, 24판의 Thermal 이에요. 낙하산을 펴고 내려오는 스카이다이버는 아직 없어요. 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 예요. 관람차는 Drop Tower 뒤에 테두리로만 서요. 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 이 셋째예요. ### 멋진 것 더 멋지게 이제는 창작자가 댓글로 고른 패턴만 고쳐요. 고를 때 볼 잰 값이에요. -노브 반응이 두 자리로 0.03 인 노브는 23판까지 열하나예요. 1판과 2판의 넷은 Hourglass 의 K2 Flow 가 0.029 로 가장 낮고, 같은 패턴의 K4 Layers 가 0.030 이에요. Flip Clock 의 K4 Light 가 0.032, Sparks 의 K1 Spread 가 0.034 예요. 13판의 Glyph Rain 은 K1 Hue 가 0.028 에서 0.066 으로, K4 Mutate 가 0.033 에서 0.066 으로 올랐어요. 판이 거의 빈 패턴은 노브가 바꿀 LED 가 적어서 노브가 다 낮게 나와요. Glyph Rain 은 채움이 0.059 에서 0.215 가 되자 노브 넷이 다 올랐어요. 9판의 Sprout 은 K1 Hue 가 0.045 로 올랐어요. 5판의 Euclid Pulse 는 K4 Steps 가 0.05 로 넷 가운데 가장 낮아요. 처음 자리의 Pulses 3 에서는 Steps 가 빛 조각의 폭만 바꿔요. 15판의 열 개는 가장 낮은 노브가 Ivy 의 K2 Speed 0.049 예요. 빠르기 노브는 새로 시작한 그림의 2~3초를 봐서, 빠르기만 바꾸는 노브는 낮게 나와요. 1 LED 너비 선이 주인공인 패턴은 선 옆 1 LED 에 그 빛을 옅게 묻히면 노브 반응이 두 배쯤 돼요. 15판의 Player Piano 는 그렇게 노브 넷이 0.02 언저리에서 0.05~0.08 로 올랐어요. 16판의 열 개는 가장 낮은 노브가 Fireflies 의 K4 Coupling 0.04 예요. 대부분이 어두운 그림은 노브 반응이 화면 평균 빛의 두 배쯤에서 멈춰요. Fireflies 는 평균 밝기가 0.03 언저리이고, 16판의 Wireworld 는 전자가 더하는 빛이 프레임 차이로 0.046 이라 K3 Pulses 가 0.05 예요. 노브 하나가 바탕을 바꾸게 하면 쉽게 올라가서, Fireflies 의 K1 Moon 은 0.08 이에요. 17판의 Frost 도 K1 Lamp 가 가로등 둘레의 서리만 물들일 때는 0.01~0.02 였고, 하늘 아래쪽과 눈 덮인 길까지 물들이자 0.05 가 됐어요. 17판의 열 개는 가장 낮은 노브가 Crossroads 의 K2 Pace 0.04 예요. 노브 검사는 다섯 설정을 같은 씨앗으로 돌려서, 노브가 붓질 수나 개미 수를 바꾸면 난수 차례가 갈려 반응이 부풀어요. 17판 Brushstrokes 의 K2~K4 와 Ant Farm 의 K3 에는 그 몫도 들어 있어요. 18판의 열 개는 가장 낮은 노브가 Server Rack 과 Turntables 의 K1~K3 으로 0.04 언저리예요. 큰 물건 하나에서 작은 부분만 움직이는 노브는 낮게 나와요. 18판 Longcase Clock 의 K3 은 처음에 추의 진폭을 바꾸는 Swing 이었는데 0.0036 으로 검사에 걸렸고, 촛불 자리를 옮기는 Candle 로 바꾸자 0.11 이 됐어요. 미리 돌리기로 처음 상태를 맞출 때는 빠르기 노브를 같이 봐야 해요. Server Rack 은 어느 빠르기에서든 2초에 같은 상태가 오게 맞췄다가 K2 가 0.0091 로 걸렸어요. 맞추는 시점은 패턴 시간에 두고 미리 돌리기는 노브 빠르기로 돌려야 빠르기마다 그림이 갈려요. 19판의 열 개는 가장 낮은 노브가 Foxfire 의 K1 Glow 와 Paternoster 의 K4 Busy 로 0.033 이에요. 패널의 10분의 1쯤 덮는 것의 색만 바꾸는 노브는 0.03 에 못 미쳐요. 19판 Popcorn 의 K4 는 처음에 버터 색을 바꾸는 Butter 였는데 0.012~0.02 여서, 더미 높이를 정하는 Heap 으로 바꿨어요. Dust Motes 의 Dust 는 먼지 수만 바꿀 때 0.011 이었고, 빛줄기 안개를 먼지 양에 묶자 0.04 가 됐어요. 노브 반응은 세 채널의 차이를 평균해요. Helter Skelter 의 빨강과 크림 줄무늬는 차이가 초록과 파랑에만 있어서, 탑이 눈에 띄게 비틀려도 0.02 였어요. 20판의 열 개는 가장 낮은 노브가 Kingfisher 의 K3 Fish 와 Kettle 의 K3 Fill 로 둘 다 0.040 이에요. 물고기 수나 물 높이만 바꿀 때는 0.012 와 0.038 이었고, Kingfisher 는 물풀과 갈대가 함께 늘게, Kettle 은 물이 많을수록 늦게 끓게 해서 올렸어요. 21판의 열 개는 가장 낮은 노브가 Woodpecker 의 K2 Speed 와 Lap Pool 의 K1 Stroke 로 둘 다 0.044 예요. 사람 넷의 영법만 바꿀 때 Stroke 는 0.02 였고, 평영과 접영이 바닥의 햇빛 무늬에 짧은 물결을 섞게 해서 올렸어요. 주인공 뒤에 서는 것을 더하는 노브는 가려져서 낮게 나와요. Pop-up Book 의 K3 Layers 는 뒤에 카드를 더할 때 0.01 이었고, 여섯째 층을 맨 앞에 서는 터널북 틀로 바꾸자 0.05 가 됐어요. Fiddlehead 의 K3 Fronds 도 늘어난 잎이 가운데 포기 뒤에서 자랄 때 0.02~0.03 이었고, 양옆 포기의 잎을 가운데 위로 기울게 하자 0.047 이 됐어요. 22판의 열 개는 가장 낮은 노브가 Circulation 의 K3 Detail 0.025 이고, 이 연구에서 두 자리로 0.02 인 노브는 이것 하나예요. 굵은 줄기와 첫 곁가지가 처음 자리에서 몸 LED 2618개 가운데 1943개를 덮어서 처음에 0.008 이었어요. 세운 패널에 사람 하나를 세우면 몸이 판의 32% 쯤이라, 몸 안에서만 바뀌는 노브는 화면 평균으로 나뉘어 낮게 나와요. 22판 Bowling Lane 의 K2 Speed 와 K3 Hook 은 0.03 이에요. 노브 검사가 보는 2~3초 무렵에 공의 길이 Hook 마다 화살표 언저리에서 만나서, 길만 바꿀 때 K3 는 0.01 이었어요. 빛나는 오일의 길이를 Hook 에 묶어 늘 보이는 결과를 하나 붙이자 올랐어요. 패널의 4분의 1만 바꾸는 노브는 그 안을 흐려도 0.04 언저리에서 멈춰요. Galilean Moons 의 K4 Seeing 은 접안렌즈 안만 흐릴 때 0.039 였고, 원반 둘레로 번지는 빛을 넓히자 0.054 가 됐어요. 23판의 열 개는 가장 낮은 노브가 Hanging Scroll 의 K4 Ink 0.032 와 Skein 의 K3 Geese 0.033 이에요. Ink 는 젖은 먹을 흐림으로만 그릴 때 0.022 였어요. 흐림은 눈에는 크게 달라 보여도 LED 하나하나의 밝기를 조금씩만 바꿔요. 넓은 면이 조금 바뀌는 쪽이 노브 반응을 올려서, Giraffe 의 K4 Wind 는 풀과 수관만 흔들 때 0.03 이었고 낮은 하늘에 먼지 막을 덮자 0.06 이 됐어요. +노브 반응이 두 자리로 0.03 인 노브는 23판까지 열하나예요. 1판과 2판의 넷은 Hourglass 의 K2 Flow 가 0.029 로 가장 낮고, 같은 패턴의 K4 Layers 가 0.030 이에요. Flip Clock 의 K4 Light 가 0.032, Sparks 의 K1 Spread 가 0.034 예요. 13판의 Glyph Rain 은 K1 Hue 가 0.028 에서 0.066 으로, K4 Mutate 가 0.033 에서 0.066 으로 올랐어요. 판이 거의 빈 패턴은 노브가 바꿀 LED 가 적어서 노브가 다 낮게 나와요. Glyph Rain 은 채움이 0.059 에서 0.215 가 되자 노브 넷이 다 올랐어요. 9판의 Sprout 은 K1 Hue 가 0.045 로 올랐어요. 5판의 Euclid Pulse 는 K4 Steps 가 0.05 로 넷 가운데 가장 낮아요. 처음 자리의 Pulses 3 에서는 Steps 가 빛 조각의 폭만 바꿔요. 15판의 열 개는 가장 낮은 노브가 Ivy 의 K2 Speed 0.049 예요. 빠르기 노브는 새로 시작한 그림의 2~3초를 봐서, 빠르기만 바꾸는 노브는 낮게 나와요. 1 LED 너비 선이 주인공인 패턴은 선 옆 1 LED 에 그 빛을 옅게 묻히면 노브 반응이 두 배쯤 돼요. 15판의 Player Piano 는 그렇게 노브 넷이 0.02 언저리에서 0.05~0.08 로 올랐어요. 16판의 열 개는 가장 낮은 노브가 Fireflies 의 K4 Coupling 0.04 예요. 대부분이 어두운 그림은 노브 반응이 화면 평균 빛의 두 배쯤에서 멈춰요. Fireflies 는 평균 밝기가 0.03 언저리이고, 16판의 Wireworld 는 전자가 더하는 빛이 프레임 차이로 0.046 이라 K3 Pulses 가 0.05 예요. 노브 하나가 바탕을 바꾸게 하면 쉽게 올라가서, Fireflies 의 K1 Moon 은 0.08 이에요. 17판의 Frost 도 K1 Lamp 가 가로등 둘레의 서리만 물들일 때는 0.01~0.02 였고, 하늘 아래쪽과 눈 덮인 길까지 물들이자 0.05 가 됐어요. 17판의 열 개는 가장 낮은 노브가 Crossroads 의 K2 Pace 0.04 예요. 노브 검사는 다섯 설정을 같은 씨앗으로 돌려서, 노브가 붓질 수나 개미 수를 바꾸면 난수 차례가 갈려 반응이 부풀어요. 17판 Brushstrokes 의 K2~K4 와 Ant Farm 의 K3 에는 그 몫도 들어 있어요. 18판의 열 개는 가장 낮은 노브가 Server Rack 과 Turntables 의 K1~K3 으로 0.04 언저리예요. 큰 물건 하나에서 작은 부분만 움직이는 노브는 낮게 나와요. 18판 Longcase Clock 의 K3 은 처음에 추의 진폭을 바꾸는 Swing 이었는데 0.0036 으로 검사에 걸렸고, 촛불 자리를 옮기는 Candle 로 바꾸자 0.11 이 됐어요. 미리 돌리기로 처음 상태를 맞출 때는 빠르기 노브를 같이 봐야 해요. Server Rack 은 어느 빠르기에서든 2초에 같은 상태가 오게 맞췄다가 K2 가 0.0091 로 걸렸어요. 맞추는 시점은 패턴 시간에 두고 미리 돌리기는 노브 빠르기로 돌려야 빠르기마다 그림이 갈려요. 19판의 열 개는 가장 낮은 노브가 Foxfire 의 K1 Glow 와 Paternoster 의 K4 Busy 로 0.033 이에요. 패널의 10분의 1쯤 덮는 것의 색만 바꾸는 노브는 0.03 에 못 미쳐요. 19판 Popcorn 의 K4 는 처음에 버터 색을 바꾸는 Butter 였는데 0.012~0.02 여서, 더미 높이를 정하는 Heap 으로 바꿨어요. Dust Motes 의 Dust 는 먼지 수만 바꿀 때 0.011 이었고, 빛줄기 안개를 먼지 양에 묶자 0.04 가 됐어요. 노브 반응은 세 채널의 차이를 평균해요. Helter Skelter 의 빨강과 크림 줄무늬는 차이가 초록과 파랑에만 있어서, 탑이 눈에 띄게 비틀려도 0.02 였어요. 20판의 열 개는 가장 낮은 노브가 Kingfisher 의 K3 Fish 와 Kettle 의 K3 Fill 로 둘 다 0.040 이에요. 물고기 수나 물 높이만 바꿀 때는 0.012 와 0.038 이었고, Kingfisher 는 물풀과 갈대가 함께 늘게, Kettle 은 물이 많을수록 늦게 끓게 해서 올렸어요. 21판의 열 개는 가장 낮은 노브가 Woodpecker 의 K2 Speed 와 Lap Pool 의 K1 Stroke 로 둘 다 0.044 예요. 사람 넷의 영법만 바꿀 때 Stroke 는 0.02 였고, 평영과 접영이 바닥의 햇빛 무늬에 짧은 물결을 섞게 해서 올렸어요. 주인공 뒤에 서는 것을 더하는 노브는 가려져서 낮게 나와요. Pop-up Book 의 K3 Layers 는 뒤에 카드를 더할 때 0.01 이었고, 여섯째 층을 맨 앞에 서는 터널북 틀로 바꾸자 0.05 가 됐어요. Fiddlehead 의 K3 Fronds 도 늘어난 잎이 가운데 포기 뒤에서 자랄 때 0.02~0.03 이었고, 양옆 포기의 잎을 가운데 위로 기울게 하자 0.047 이 됐어요. 22판의 열 개는 가장 낮은 노브가 Circulation 의 K3 Detail 0.025 이고, 이 연구에서 두 자리로 0.02 인 노브는 이것 하나예요. 굵은 줄기와 첫 곁가지가 처음 자리에서 몸 LED 2618개 가운데 1943개를 덮어서 처음에 0.008 이었어요. 세운 패널에 사람 하나를 세우면 몸이 판의 32% 쯤이라, 몸 안에서만 바뀌는 노브는 화면 평균으로 나뉘어 낮게 나와요. 22판 Bowling Lane 의 K2 Speed 와 K3 Hook 은 0.03 이에요. 노브 검사가 보는 2~3초 무렵에 공의 길이 Hook 마다 화살표 언저리에서 만나서, 길만 바꿀 때 K3 는 0.01 이었어요. 빛나는 오일의 길이를 Hook 에 묶어 늘 보이는 결과를 하나 붙이자 올랐어요. 패널의 4분의 1만 바꾸는 노브는 그 안을 흐려도 0.04 언저리에서 멈춰요. Galilean Moons 의 K4 Seeing 은 접안렌즈 안만 흐릴 때 0.039 였고, 원반 둘레로 번지는 빛을 넓히자 0.054 가 됐어요. 23판의 열 개는 가장 낮은 노브가 Hanging Scroll 의 K4 Ink 0.032 와 Skein 의 K3 Geese 0.033 이에요. Ink 는 젖은 먹을 흐림으로만 그릴 때 0.022 였어요. 흐림은 눈에는 크게 달라 보여도 LED 하나하나의 밝기를 조금씩만 바꿔요. 넓은 면이 조금 바뀌는 쪽이 노브 반응을 올려서, Giraffe 의 K4 Wind 는 풀과 수관만 흔들 때 0.03 이었고 낮은 하늘에 먼지 막을 덮자 0.06 이 됐어요. 24판의 열 개는 가장 낮은 노브가 Potter's Wheel 의 K2 Speed 0.042 와 Pipe Organ 의 K4 Tremulant 0.042 예요. Breath 는 첫 판부터 노브 넷이 0.09~0.13 이었어요. 숨을 내쉰 폐가 판의 42% 를 덮고 노브마다 폐 전체의 색이나 크기를 바꿔서, 22판 Circulation 처럼 몸 안에서만 바뀌는 노브가 낮게 나오는 일이 없었어요. 첫 3초에 일어나지 않는 일만 바꾸는 노브는 0 에 가까워요. Shoot 'em Up 의 K4 Bullets 는 보스 탄이 3.6초부터 날아서 처음에 0.0041 이었고, 탄이 짙을수록 바닥을 어둡게 하자 0.10 이 됐어요. 빛이 이웃에 번지면 노브가 올라요. Pipe Organ 의 K1 Stops 는 소리 나는 파이프만 물들일 때 0.010 이었고, 그 빛이 좌우 16열의 주석에 비치게 하자 0.02, 리드 음전에 배음 파이프를 더하자 0.062 가 됐어요. 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초에 꺾이기 전까지는 실린더와 실험대가 서 있기만 해서예요. +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% 예요. ## 다시 돌리기 From f723822e3be8ef4113b637ed00f1c5ea813fd92b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 02:17:46 +0000 Subject: [PATCH 32/33] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=20=EB=84=B7=EC=9D=98=20=EC=85=8B=20=EB=82=98=EC=97=B4=EC=9D=84?= =?UTF-8?q?=20=EA=B3=A0=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KTNzLtP3gH9rQENoucsegc --- incoming/pattern-study/patterns/flying-trapeze.js | 2 +- incoming/pattern-study/patterns/pendulum-wave.js | 2 +- incoming/pattern-study/patterns/potters-wheel.js | 2 +- incoming/pattern-study/patterns/thermal.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/incoming/pattern-study/patterns/flying-trapeze.js b/incoming/pattern-study/patterns/flying-trapeze.js index 989c23aa2..7dff0c4d3 100644 --- a/incoming/pattern-study/patterns/flying-trapeze.js +++ b/incoming/pattern-study/patterns/flying-trapeze.js @@ -36,7 +36,7 @@ const [PCX, PCY, LC, B] = [57, 30, 20, 0.18]; const [BY, WX, LX] = [68, 1.2, 2.6]; // Hips above feet, hands above hips, gravity, and the time of the flight back to the bar. const [LEG, HL, G, TR] = [3.8, 5.7, 90, 0.45]; -// Follow spots below the panel, behind the crowd, and how fast their cones open. +// Follow spots below the panel behind the crowd, and how fast their cones open. const SPOT = [[-14, 166], [78, 166], [32, 186]]; const TA = 0.046; // Gels by the Lights knob: house wash (0..1), three spots, two flyers and the catcher. diff --git a/incoming/pattern-study/patterns/pendulum-wave.js b/incoming/pattern-study/patterns/pendulum-wave.js index 1a095a78c..dfbed38c8 100644 --- a/incoming/pattern-study/patterns/pendulum-wave.js +++ b/incoming/pattern-study/patterns/pendulum-wave.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Pendulum Wave // Family: physics -// Idea: Seen straight down from above, a row of pendulums hangs from a beam down the middle of a lamplit table, each shorter one swinging once more per cycle than the last, so the row that leaves the release board as one straight line winds into a travelling snake, splits into three and two interleaved rows, scatters, and lines up again at the end of the cycle. +// Idea: Seen straight down from above, a row of pendulums hangs from a beam down the middle of a lamplit table, each shorter one swinging once more per cycle than the last, so the row that leaves the release board as one straight line winds into a travelling snake and splits into three and two interleaved rows before it scatters and lines up again at the end of the cycle. // Author: patternflow-lab // Date: 2026-10-24 // SPDX-License-Identifier: CC-BY-SA-4.0 diff --git a/incoming/pattern-study/patterns/potters-wheel.js b/incoming/pattern-study/patterns/potters-wheel.js index ba7fc6bed..a6eb6f42c 100644 --- a/incoming/pattern-study/patterns/potters-wheel.js +++ b/incoming/pattern-study/patterns/potters-wheel.js @@ -43,7 +43,7 @@ for (const sy of SHELF) for (let i = 0; i < 7; i++) SLOT.push([4.6 + i * 9.13, s const ORDER = [1, 5, 8, 12, 14, 20, 3, 9, 11, 0, 6, 7, 13, 15, 19, 2, 4, 10, 16, 18, 17]; const SHS = 0.27; -// Each clay thrown and wet, leather-hard, and bone-dry. +// Each clay at every stage of drying, from thrown and wet to bone-dry. const CLAY = [ [[255, 112, 56], [196, 96, 58], [238, 158, 108]], [[236, 170, 104], [190, 148, 100], [244, 222, 182]], diff --git a/incoming/pattern-study/patterns/thermal.js b/incoming/pattern-study/patterns/thermal.js index 8bec9dc73..abedb46ed 100644 --- a/incoming/pattern-study/patterns/thermal.js +++ b/incoming/pattern-study/patterns/thermal.js @@ -53,7 +53,7 @@ const PICK = [0, 0, 0, 1, 1, 2, 2, 2, 3, 3, 4, 4]; const GLIDER = [[3.6, 0, -0.1, -4.8, 0, 0.15, 0.85, 0.45], [0.6, 0, 0.25, 0.25, -9, 0.85, 0.75, 0.45], [0.6, 0, 0.25, 0.25, 9, 0.85, 0.75, 0.45], [-4.2, 0, 0.1, -4.9, 0, 2.4, 0.55, 0.45], [-4.9, -2.6, 2.4, -4.9, 2.6, 2.4, 0.45, 0.45]]; const BUZZ = [[1.3, 0, 0, -1.3, 0, 0, 0.6, 0.45], [0.2, 0, 0.1, -0.3, -3.7, 0.75, 0.8, 0.45], [0.2, 0, 0.1, -0.3, 3.7, 0.75, 0.8, 0.45], [-1.2, 0, 0, -2.5, 0, 0.1, 0.45, 0.65]]; -// A cumulus as puffs over a flat base: u across, v up, r, and when in its growth each appears. +// A cumulus as puffs over a flat base: each puff's place (u across, v up), its radius r and when in its growth it appears. function puffs() { const L = []; const add = (u, v, r, t0) => L.push({ u: u + (rnd() - 0.5) * 0.14, v: v + (rnd() - 0.5) * 0.08, r: r * (0.88 + 0.24 * rnd()), t0, ph: rnd() * TAU }); From f279fd3508b2139c4242480a2f63931c076a5143 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 02:36:41 +0000 Subject: [PATCH 33/33] =?UTF-8?q?index.html=20=EC=9D=84=20148=EA=B0=9C?= =?UTF-8?q?=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20=EC=A7=93=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KTNzLtP3gH9rQENoucsegc --- incoming/pattern-study/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index b395f51da..d33b9cb48 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -141,7 +141,7 @@

패턴 연구

- +