From 8c9a6644c6a76f8fc6d638b6a20af9bd504e35f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 23:26:05 +0000 Subject: [PATCH 01/15] =?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=2023=ED=8C=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GhRE8HZxGnk63uCfeHdr92 From f8867fe8dea1d4ce4e65990fe8b996fd92f75e00 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 23:34:15 +0000 Subject: [PATCH 02/15] =?UTF-8?q?=EB=AC=B4=EB=8C=80=C2=B7=EA=B3=B5?= =?UTF-8?q?=EC=97=B0=EA=B3=BC=20=EC=A7=90=EC=8A=B9=20=EA=B0=88=EB=9E=98?= =?UTF-8?q?=EB=A5=BC=20=EC=97=B0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GhRE8HZxGnk63uCfeHdr92 --- 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 a1530ebed..f460d5a7d 100644 --- a/incoming/pattern-study/families.json +++ b/incoming/pattern-study/families.json @@ -40,5 +40,7 @@ "paper": { "label": "종이", "color": "#996600" }, "sport": { "label": "운동·경기", "color": "#337777" }, "body": { "label": "몸", "color": "#bb5566" }, - "flight": { "label": "비행", "color": "#9944ee" } + "flight": { "label": "비행", "color": "#9944ee" }, + "stage": { "label": "무대·공연", "color": "#ff0066" }, + "mammal": { "label": "짐승", "color": "#998844" } } From eae847cf39bc962094c4da8811d5b98dcf78bd5d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 23:55:42 +0000 Subject: [PATCH 03/15] =?UTF-8?q?Final=20Approach=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9D=84=20=EC=A7=A0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GhRE8HZxGnk63uCfeHdr92 --- .../pattern-study/patterns/final-approach.js | 639 ++++++++++++++++++ 1 file changed, 639 insertions(+) create mode 100644 incoming/pattern-study/patterns/final-approach.js diff --git a/incoming/pattern-study/patterns/final-approach.js b/incoming/pattern-study/patterns/final-approach.js new file mode 100644 index 000000000..428bfb9ae --- /dev/null +++ b/incoming/pattern-study/patterns/final-approach.js @@ -0,0 +1,639 @@ +// ===== Patternflow pattern ===== +// Title: Final Approach +// Family: flight +// Idea: From an airliner cockpit on a night landing, the runway and its approach lights converge high on the panel under a town glowing on the horizon, then stream toward the windscreen and under it with the rabbit racing ahead and the PAPI showing two white over two red, through the flare and the rollout, before the next approach begins far out. +// Author: patternflow-lab +// Date: 2026-10-23 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 23, after the ALSF-2 approach lighting system +// Round: 23 +// =============================== +// @matrix 64x128 +// @knobs Glow=0..1, Speed=0.2..3, Weather=0..3, Crosswind=-1..1 +// +// Knob 1: Glow. The colour of the town's glow on the clouds and of the sodium lamps on the horizon: sodium amber at 0, turning through green and blue to violet and back to amber at 1. +// Knob 2: Speed. How fast the airliner flies: at 2 an approach from far out to the end of the rollout takes about 18 s and the rabbit runs twice a second; at 0.2 everything runs ten times slower. +// Knob 3: Weather. A clear night with broken cloud at 0, haze where every light blooms at 1, rain on the windscreen with the wiper going at 2, and fog at 3 where the approach lights come out of the murk late. +// Knob 4: Crosswind. The airliner crabs into the wind from the left or the right, so the runway sits off to one side until it swings straight ahead at the flare. +// Buttons: K1 tap goes around: the nose comes up and the lights drop away below, then the airliner banks off and comes round for a new approach. + +const W = 64; +const H = 128; +const N = W * H; +const { min, max, abs, sin, cos, exp, sqrt, pow, floor, ceil, round, atan, log, PI } = Math; +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 rnd = () => Math.random(); + +// The camera: focal length in LEDs, the screen centre, the eye height on the +// runway in metres, and the horizon row on the approach. +const F = 150; +const CX = 31.5; +const CY = 64; +const HG = 8; +const Y0 = 32; +// A 3 degree glide path comes down to eye height HG 320 m past the +// threshold. The flare starts 8 m above that, and touchdown is where it ends. +const SLOPE = 0.0524; +const AIM = 320; +const PF = AIM - 8 / SLOPE; +const LF = 11 / SLOPE; +const TD = PF + LF * log(11 / 3); +// Cloud base above the ground, and where a new approach starts. +const HC = 520; +const FAR = -2300; +// The PAPI units from the runway outward, in degrees. +const PT = [3.5, 3.17, 2.83, 2.5]; + +function eyeH(P) { + if (P < PF) return HG + (AIM - P) * SLOPE; + if (P < TD) return HG + 11 * exp(-(P - PF) / LF) - 3; + return HG; +} + +// Kinds of light: steady, the sequenced flashers, the PAPI, city lamps that +// take the glow colour, red obstruction lights and the airport beacon. +const [STEADY, SEQ, PAPI, CITY, OBS, BEAC] = [0, 1, 2, 3, 4, 5]; +const WHITE = [1, 0.9, 0.74]; +const COOL = [0.86, 0.92, 1]; +const RED = [1, 0.1, 0.04]; +const GREEN = [0.1, 1, 0.42]; +const AMBER = [1, 0.7, 0.16]; +const BLUE = [0.2, 0.32, 1]; + +// Weather presets the knob runs between: visibility in metres, bloom, blur +// radius, cloud cover, glow, rain, fog, and light scattered in the air. +const WX = [ + [30000, 0.35, 1, 0.5, 1.0, 0, 0, 0], + [5000, 1.0, 2, 0.6, 1.25, 0, 0.22, 0.07], + [3000, 0.75, 2, 0.85, 1.35, 1, 0.15, 0.1], + [420, 1.5, 3, 1, 1.05, 0, 1, 0.2], +]; + +// A fixed tileable value noise, for clouds and grass. +const TN = 128; +const hash = (i) => { + let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; +let TX = null; +let TC = null; +function makeTex(oct) { + const t = new Float32Array(TN * TN); + let amp = 0.5; + for (let o = 0; o < oct; o++) { + const g = 4 << o; + const c = TN / g; + for (let y = 0; y < TN; y++) + for (let x = 0; x < TN; x++) { + const gx = x / c; + const gy = y / c; + const x0 = floor(gx); + const y0 = floor(gy); + let fx = gx - x0; + let fy = gy - y0; + fx = fx * fx * (3 - 2 * fx); + fy = fy * fy * (3 - 2 * fy); + const hv = (a, b) => hash(((a % g) * 131 + (b % g)) * 7 + o * 9973); + const top = hv(x0, y0) + (hv(x0 + 1, y0) - hv(x0, y0)) * fx; + const bot = hv(x0, y0 + 1) + (hv(x0 + 1, y0 + 1) - hv(x0, y0 + 1)) * fx; + t[y * TN + x] += amp * (top + (bot - top) * fy); + } + amp *= 0.5; + } + let lo = 9; + let hi = -9; + for (let i = 0; i < t.length; i++) { + lo = min(lo, t[i]); + hi = max(hi, t[i]); + } + for (let i = 0; i < t.length; i++) t[i] = (t[i] - lo) / (hi - lo); + return t; +} +function tex(u, v, A = TX) { + const x = u * TN; + const y = v * TN; + const x0 = floor(x); + const y0 = floor(y); + const fx = x - x0; + const fy = y - y0; + const a = (x0 & 127) + (y0 & 127) * TN; + const b = ((x0 + 1) & 127) + (y0 & 127) * TN; + const c = (x0 & 127) + ((y0 + 1) & 127) * TN; + const d = ((x0 + 1) & 127) + ((y0 + 1) & 127) * TN; + const top = A[a] + (A[b] - A[a]) * fx; + return top + (A[c] + (A[d] - A[c]) * fx - top) * fy; +} + +// Every light on the field and in the town. Positions are in metres, with z +// running along the runway from the threshold. +function build() { + const L = []; + const add = (x, y, z, k, i, c, n = 0) => L.push([x, y, z, k, i, c[0], c[1], c[2], n]); + const both = (fn) => { + fn(-1); + fn(1); + }; + // ALSF-2: a barrette every 30 m out to 720 m, red side rows on the inner + // 300 m, flashers on the outer part, crossbars at 300 m and 150 m. + for (let k = 1; k <= 24; k++) { + const z = -30 * k; + for (let j = -2; j <= 2; j++) add(j * 1.05, 0.6, z, STEADY, 1, WHITE); + if (k <= 10) both((sg) => { for (let j = 0; j < 3; j++) add(sg * (11 + j * 1.05), 0.6, z, STEADY, 0.75, RED); }); + if (k >= 11) add(0, 1.4, z, SEQ, 1, COOL, 24 - k); + } + both((sg) => { for (let j = 3; j <= 15; j += 1.5) add(sg * j, 0.6, -300, STEADY, 1, WHITE); }); + both((sg) => { for (let j = 3; j <= 7.5; j += 1.5) add(sg * j, 0.6, -150, STEADY, 1, WHITE); }); + // The green threshold bar and its wing bars. + for (let x = -25; x <= 25.01; x += 1.5) add(x, 0.3, 0, STEADY, 1, GREEN); + both((sg) => { for (let j = 26.5; j <= 38; j += 1.5) add(sg * j, 0.3, 0, STEADY, 1, GREEN); }); + // Runway edge lights turn yellow on the last 600 m. A red bar ends it. + for (let z = 0; z <= 3000; z += 60) both((sg) => add(sg * 23.5, 0.4, z, STEADY, 0.85, z > 2400 ? AMBER : WHITE)); + for (let x = -21; x <= 21; x += 3) add(x, 0.3, 3000, STEADY, 0.8, RED); + // Centreline every 15 m, red and white near the far end; touchdown zone bars. + for (let z = 7.5; z < 3000; z += 15) { + const red = z > 2700 || (z > 2100 && round((z - 7.5) / 15) % 2 === 1); + add(0.6, 0.05, z, STEADY, 0.7, red ? RED : COOL); + } + for (let z = 30; z <= 900; z += 30) both((sg) => { for (let j = 0; j < 3; j++) add(sg * (9.8 + j * 1.5), 0.05, z, STEADY, 0.55, COOL); }); + for (let j = 0; j < 4; j++) add(-(38.5 + 9 * j), 0.9, AIM, PAPI, 1.6, WHITE, j); + // A parallel taxiway on the right with blue edges and green lead-offs. + for (let z = -60; z <= 3000; z += 50) both((sg) => add(190 + sg * 11, 0.3, z, STEADY, 0.4, BLUE)); + for (const z0 of [500, 1300, 2100, 2800]) for (let t = 0; t <= 1; t += 1 / 12) add(26 + 160 * t, 0.1, z0 + 120 * t * t, STEADY, 0.35, GREEN); + // The apron floodlights beyond it, and the beacon on the tower. + for (let j = 0; j < 12; j++) add(330 + 260 * rnd(), 25, 900 + 1500 * rnd(), CITY, 1.3, WHITE, rnd() < 0.3 ? 0.8 : 0.1); + add(300, 35, 1400, BEAC, 1.4, WHITE); + // The town: lamps along streets on a 140 m grid that thin out between + // districts and climb the hills behind. + let n = 0; + while (n < 1150) { + const z = 3300 + 17000 * pow(rnd(), 1.4); + let x = (rnd() * 2 - 1) * 0.45 * z; + let zz = z; + if (rnd() < 0.55) x = round(x / 140) * 140 + (rnd() - 0.5) * 6; + else zz = round(z / 140) * 140 + (rnd() - 0.5) * 6; + if (tex(x / 9000 + 0.2, zz / 9000) < 0.35 + 0.25 * rnd()) continue; + const hill = max(0, (zz - 7000) / 13000) * 300 * (0.5 + tex(x / 4000, zz / 4000 + 0.5)); + const white = rnd() < 0.28; + add(x, hill + 5 + (rnd() < 0.08 ? rnd() * 90 : 0), zz, CITY, 0.55 + 0.6 * rnd(), WHITE, white ? 0.75 : 0.05); + n++; + } + // Masts and towers in the town with red lights stacked up them. + for (let j = 0; j < 7; j++) { + const z = 4500 + 9000 * rnd(); + const x = (rnd() * 2 - 1) * 0.3 * z; + const ht = 120 + 220 * rnd(); + const ph = rnd() * 6; + for (let q = 1; q <= 3; q++) add(x, (ht * q) / 3, z, OBS, 1.1, RED, ph); + } + const A = (j) => Float32Array.from(L, (e) => e[j]); + return { lx: A(0), ly: A(1), lz: A(2), lk: A(3), li: A(4), lr: A(5), lg: A(6), lb: A(7), ln: A(8), n: L.length }; +} + +function cam(s) { + return { P: s.P, h: s.h, yh: s.yh, psi: s.psi, roll: s.roll, X: s.Xp }; +} + +function approach(s, P0) { + s.mode = 0; + s.P = P0; + s.h = eyeH(P0); + s.down = false; + s.rt = 0; + s.age = 0; + s.turn = 0; + s.fresh = true; +} + +export function setup(params) { + if (!TX) { + TX = makeTex(4); + TC = makeTex(2); + } + const F3 = () => new Float32Array(N * 3); + params.s = { lt: build(), lb: F3(), bl: F3(), tmp: F3(), o: F3(), tq: 0, drops: [], tw: 0, wa: 0.35, drift: 0 }; + const s = params.s; + // The landing lights are fixed to the airframe, so their pool on the + // ground and their glow in the air stay put on the screen. + s.bm = new Float32Array(N); + s.bs = new Float32Array(N); + for (let p = 0; p < N; p++) { + const bx = (p % W) - CX; + const y = (p / W) | 0; + const by = (y - 76) / (y < 76 ? 14 : 36); + s.bm[p] = exp(-((bx / 22) ** 2) - by * by); + s.bs[p] = exp(-((bx / 26) ** 2) - ((y - 82) / 46) ** 2); + } + // Open about a kilometre out, so the first look is the approach lights. + approach(s, -1000); + s.age = 1; + s.yh = Y0; + s.psi = 0; + s.roll = 0; + s.Xp = 0; + s.v = 150; + s.fade = 1; + s.c = cam(s); + s.pc = s.c; +} + +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); + const d = (dt * clamp(k[1], 0.2, 3)) / 2; + s.tq += d; + s.age += d; + const tq = s.tq; + // Glow colour: hue rotated from sodium amber. + const hue = (0.085 + clamp(k[0], 0, 1)) % 1; + s.gc = [0, 0, 0].map((_, c) => { + const q = ([5, 3, 1][c] + hue * 6) % 6; + return 1 - 0.82 * max(0, min(q, 4 - q, 1)); + }); + // Weather, blended between the neighbouring presets. + const w = clamp(k[2], 0, 3); + const i0 = min(2, floor(w)); + const f = w - i0; + const A = WX[i0]; + const B = WX[i0 + 1]; + const lerp = (j) => A[j] + (B[j] - A[j]) * f; + s.vis = exp(log(A[0]) + (log(B[0]) - log(A[0])) * f); + [s.bloom, s.br, s.cover, s.glowK, s.rain, s.fog, s.scat] = [1, 2, 3, 4, 5, 6, 7].map(lerp); + s.br = round(s.br); + s.drift += d * 0.02; + const cw = clamp(k[3], -1, 1); + + s.pc = s.c; + if (input.btnPressed && input.btnPressed[0] && s.mode === 0) { + s.mode = 1; + s.g = 0; + s.gh = s.h; + s.gp = s.yh - Y0; + } + // Turbulence: a little bob and roll in the air, a rumble on the ground. + const air = min(1, (s.h - HG) / 25); + const bob = air * (0.7 * sin(tq * 1.7) + 0.4 * sin(tq * 4.1 + 1)) + (1 - air) * 0.25 * sin(tq * 31) * (s.v / 150); + if (s.mode === 0) { + if (!s.down) { + s.v = s.P < 0 ? 150 - 0.08 * s.P : 150 - 0.05 * s.P; + s.P += s.v * d; + if (s.P >= TD) s.down = true; + s.h = eyeH(s.P) + (s.P < PF ? 1.6 * sin(tq * 0.43) * min(1, (PF - s.P) / 500) : 0); + } else { + s.rt += d; + s.v = max(16, s.v - 26 * d); + s.P += s.v * d; + s.h = HG; + } + // The nose comes up in the flare and down after touchdown. + const flare = sstep(PF - 40, TD, s.P); + s.yh = Y0 + 5 * flare - 7 * sstep(0, 0.9, s.rt) + bob; + s.psi = cw * 0.17 * (1 - sstep(PF - 120, TD - 30, s.P)); + s.roll = air * (0.022 * sin(tq * 1.1 + 0.5) + cw * 0.02); + s.Xp = 2 * sin(tq * 0.31) * air; + s.fade = sstep(0, 0.7, s.age) * (1 - sstep(4.3, 5, s.rt)); + if (s.rt >= 5) approach(s, FAR); + } else { + // Go around: pitch up and climb, then bank away and come round again. + const g = (s.g += d); + s.v = min(190, s.v + 12 * d); + s.P += s.v * d * cos(s.turn); + s.h += 36 * sstep(0.2, 1.3, g) * d; + s.yh = Y0 + s.gp + (34 - s.gp) * sstep(0, 1.5, g) + bob; + s.roll = -0.45 * sstep(1.8, 2.9, g); + s.turn += 0.32 * sstep(1.8, 2.9, g) * d; + s.psi += 0.32 * sstep(1.8, 2.9, g) * d; + s.fade = 1 - sstep(3.1, 3.9, g); + if (g >= 3.9) approach(s, FAR); + } + s.c = cam(s); + if (s.fresh) { + s.pc = s.c; + s.fresh = false; + } + + // Rain on the windscreen, and the wiper that clears it. + if (s.rain > 0.3) { + const nd = d * 34 * s.rain; + for (let j = 0; j < 4; j++) if (rnd() < nd / 4 && s.drops.length < 70) s.drops.push({ x: rnd() * W, y: rnd() * H, r: 0.55 + 0.6 * rnd() }); + s.tw += d; + const wa = -0.12 - 0.8 * (0.5 - 0.5 * cos((2 * PI * s.tw) / 1.5)); + const lo = min(wa, s.wa); + const hi = max(wa, s.wa); + s.drops = s.drops.filter((q) => { + const a = atan((q.x - 58) / (150 - q.y)); + return a < lo - 0.03 || a > hi + 0.03; + }); + s.wa = wa; + } else { + s.wa = 0.35; + if (s.drops.length) s.drops.length = 0; + } +} + +// Additive light: a sharpened bilinear dot for far lights, a round blob for +// near ones. +function dot(B, x, y, r, g, b) { + const x0 = floor(x); + const y0 = floor(y); + let fx = x - x0; + let fy = y - y0; + fx = (fx * fx) / (fx * fx + (1 - fx) * (1 - fx)); + fy = (fy * fy) / (fy * fy + (1 - fy) * (1 - fy)); + for (let j = 0; j < 4; j++) { + const xx = x0 + (j & 1); + const yy = y0 + (j >> 1); + if (xx < 0 || xx >= W || yy < 0 || yy >= H) continue; + const wgt = (j & 1 ? fx : 1 - fx) * (j >> 1 ? fy : 1 - fy); + const i = (yy * W + xx) * 3; + B[i] += r * wgt; + B[i + 1] += g * wgt; + B[i + 2] += b * wgt; + } +} +function blob(B, x, y, r, g, b, sg) { + const R = min(4, ceil(sg * 2.2)); + const kk = -1 / (2 * sg * sg); + const xa = max(0, ceil(x - R)); + const xb = min(W - 1, floor(x + R)); + for (let yy = max(0, ceil(y - R)); yy <= min(H - 1, floor(y + R)); yy++) + for (let xx = xa; xx <= xb; xx++) { + const wgt = exp(((xx - x) ** 2 + (yy - y) ** 2) * kk); + const i = (yy * W + xx) * 3; + B[i] += r * wgt; + B[i + 1] += g * wgt; + B[i + 2] += b * wgt; + } +} + +function lights(s, B) { + const c = s.c; + const p = s.pc; + const T = s.lt; + const sn = sin(c.psi); + const cs = cos(c.psi); + const sr = sin(c.roll); + const cr = cos(c.roll); + const psn = sin(p.psi); + const pcs = cos(p.psi); + const psr = sin(p.roll); + const pcr = cos(p.roll); + const ext = 3 / s.vis; + const gc = s.gc; + const tq = s.tq; + // The rabbit runs from the outermost flasher to the 300 m bar twice a second. + const seq = (tq * 2) % 1; + const papiA = (atan((c.h - HG) / max(1, AIM - c.P)) * 180) / PI; + const beac = (tq * 0.5) % 1; + for (let i = 0; i < T.n; i++) { + const dx = T.lx[i] - c.X; + const dz = T.lz[i] - c.P; + const zc = dx * sn + dz * cs; + if (zc < 3) continue; + const xc = dx * cs - dz * sn; + const ox = (F * xc) / zc; + const oy = c.yh - CY + (F * (c.h - T.ly[i])) / zc; + const X = CX + ox * cr - oy * sr; + const Y = CY + ox * sr + oy * cr; + if (X < -5 || X > W + 4 || Y < -5 || Y > H + 4) continue; + let a = T.li[i] * min(1.8, pow(600 / zc, 0.62)) * exp(-zc * ext); + let r = T.lr[i]; + let g = T.lg[i]; + let b = T.lb[i]; + const k = T.lk[i]; + const m = T.ln[i]; + let sg = 0.4 + min(1.7, 40 / zc); + if (k === SEQ) { + let age = seq - m / 14; + if (age < 0) age += 1; + const e = exp(-age * 13); + a *= 4.5 * e; + sg += 1.1 * e; + } else if (k === PAPI) { + const wa = sstep(-0.07, 0.07, papiA - PT[m]); + g = 0.1 + 0.83 * wa; + b = 0.04 + 0.72 * wa; + } else if (k === CITY) { + r = gc[0] + (1 - gc[0]) * m; + g = gc[1] + (0.92 - gc[1]) * m; + b = gc[2] + (0.8 - gc[2]) * m; + } else if (k === OBS) a *= sstep(-0.25, 0.25, sin(tq * 3.3 + m)); + else if (k === BEAC) { + const e = exp(-(((beac % 0.5) / 0.05) ** 2)); + a *= 2 * e; + if (beac < 0.5) [r, g, b] = GREEN; + } + a *= s.fade * 255; + if (a < 3) continue; + // A light that crossed the screen fast since the last frame leaves a + // fading streak behind it. + if (zc < 500) { + const pdx = T.lx[i] - p.X; + const pdz = T.lz[i] - p.P; + const pz = pdx * psn + pdz * pcs; + if (pz > 3) { + const qx = (F * (pdx * pcs - pdz * psn)) / pz; + const qy = p.yh - CY + (F * (p.h - T.ly[i])) / pz; + const PX = CX + qx * pcr - qy * psr; + const PY = CY + qx * psr + qy * pcr; + const ddx = X - PX; + const ddy = Y - PY; + const len = sqrt(ddx * ddx + ddy * ddy); + if (len > 1.5) { + const n = min(28, ceil(len)); + const q = a * max(0.3, 1.4 / len); + for (let j = 1; j < n; j++) { + const t = j / n; + dot(B, PX + ddx * t, PY + ddy * t, q * r * t, q * g * t, q * b * t); + } + } + } + } + if (sg < 0.75) dot(B, X, Y, a * r, a * g, a * b); + else blob(B, X, Y, a * r, a * g, a * b, sg); + // On a wet runway each light lays a streak toward the windscreen. + if (s.rain > 0.05 && T.ly[i] < 1.5 && T.lz[i] > -760) { + const ls = min(10, round(s.rain * (2 + 0.12 * (Y - c.yh)))); + for (let j = 1; j <= ls; j++) { + const q = a * 0.3 * s.rain * (1 - j / (ls + 1)); + dot(B, X, Y + j, q * r, q * g, q * b); + } + } + } +} + +// Box blur, horizontal then vertical, for the bloom. +function blur(src, dst, tmp, r) { + const inv = 1 / (2 * r + 1); + for (let y = 0; y < H; y++) + for (let ch = 0; ch < 3; ch++) { + const o = y * W * 3 + ch; + let acc = 0; + for (let x = -r; x <= r; x++) acc += src[o + clamp(x, 0, W - 1) * 3]; + for (let x = 0; x < W; x++) { + tmp[o + x * 3] = acc * inv; + acc += src[o + min(W - 1, x + r + 1) * 3] - src[o + max(0, x - r) * 3]; + } + } + for (let x = 0; x < W; x++) + for (let ch = 0; ch < 3; ch++) { + const o = x * 3 + ch; + let acc = 0; + for (let y = -r; y <= r; y++) acc += tmp[o + clamp(y, 0, H - 1) * W * 3]; + for (let y = 0; y < H; y++) { + dst[o + y * W * 3] = acc * inv; + acc += tmp[o + min(H - 1, y + r + 1) * W * 3] - tmp[o + max(0, y - r) * W * 3]; + } + } +} + +// Albedo of the ground: asphalt and its paint on the runway, grass around it. +const TDZ = [150, 450, 600, 750, 900]; +function surface(X, Z) { + const ax = abs(X); + if (Z >= 0 && Z <= 3000 && ax < 23) { + let al = 0.17 + 0.05 * tex(X / 9, Z / 40); + if (ax > 21.9) al = 0.62; + else if (Z > 6 && Z < 36 && ax > 3 && (ax - 3) % 3.6 < 1.8) al = 0.75; + else if (ax < 0.45 && Z > 60 && (Z - 60) % 60 < 36) al = 0.4; + else if (Z > 300 && Z < 360 && ax > 6 && ax < 15) al = 0.72; + else if (ax > 6 && ax < 15 && (ax - 6) % 3 < 1.5) for (const z0 of TDZ) if (Z > z0 && Z < z0 + 22) al = 0.7; + // Rubber from a million touchdowns darkens the middle of the zone. + al *= 1 - (0.45 - 0.4 * tex(X / 5, Z / 25)) * sstep(260, 340, Z) * sstep(1050, 950, Z) * sstep(14, 10, ax); + return al; + } + return 0.04 + 0.07 * tex(X / 6, Z / 6); +} + +export function draw(display, params) { + const s = params.s; + if (!s || !s.gc) return; + const L = s.lb; + const BL = s.bl; + const O = s.o; + L.fill(0); + lights(s, L); + blur(L, BL, s.tmp, s.br); + blur(BL, BL, s.tmp, s.br); + + const c = s.c; + const gc = s.gc; + const sn = sin(c.psi); + const cs = cos(c.psi); + const sr = sin(c.roll); + const cr = cos(c.roll); + const ext = 3 / s.vis; + const land = 2 * s.fade; + const gk = s.glowK; + const fog = s.fog; + // The colour the murk takes: lit by the town, and by the airport in fog. + const fl = (0.05 + 0.07 * fog) * (1 + 0.6 * fog); + const fw = 0.16 * fog; + const fogC = [gc[0] * fl + fw, gc[1] * fl + fw * 1.04, gc[2] * fl + fw * 1.1]; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const ax = x - CX; + const ay = y - CY; + const xr = ax * cr + ay * sr; + const v = CY - ax * sr + ay * cr - c.yh; + // The town glows brighter in some directions than others. + const hd = xr / F + c.psi; + const dir = 0.55 + 0.8 * tex(hd * 0.8 + 0.3, 0.71); + let R = 0; + let G = 0; + let Bc = 0; + const wg = sstep(-1, 1, v); + if (wg < 1) { + // Sky: a dark blue zenith over the glow of the town, and the broken + // cloud deck lit from below. + const u = max(0, -v); + let gl = gk * dir * (0.3 * exp(-u / 6) * (1 - fog) + 0.13 * exp(-u / 30)); + if (u > 0.3 && s.cover > 0) { + const zz = (F * (HC - c.h)) / u; + const xx = (xr * zz) / F; + // Far cloud melts into the glow instead of shimmering. + let n = tex((xx * cs + zz * sn + c.X) / 5000, (-xx * sn + zz * cs + c.P) / 5000 + s.drift, TC); + n = 0.5 + (n - 0.5) * exp(-zz / 9000); + gl += sstep(1 - s.cover, 1.3 - s.cover, n) * sstep(0.3, 5, u) * (0.15 + 0.3 * exp(-u / 22)) * dir * gk; + } + let sR = 0.03 + gc[0] * gl; + let sG = 0.045 + gc[1] * gl; + let sB = 0.13 + gc[2] * gl; + // In fog the sky is the murk, darker going up. + const fu = fog * (0.75 + 0.25 * exp(-u / 20)); + sR += (fogC[0] * fu - sR) * fog; + sG += (fogC[1] * fu - sG) * fog; + sB += (fogC[2] * fu - sB) * fog; + R += sR * (1 - wg); + G += sG * (1 - wg); + Bc += sB * (1 - wg); + } + if (wg > 0) { + const vv = max(0.05, v); + const zc = (F * c.h) / vv; + const xc = (xr * zc) / F; + const dd = sqrt(zc * zc + xc * xc + c.h * c.h); + const Tr = exp(-dd * ext); + const E = land * s.bm[y * W + x] * min(1, pow(45 / dd, 1.3)) * Tr; + let gR = 0; + if (E > 0.008) gR = E * surface(xc * cs + zc * sn + c.X, -xc * sn + zc * cs + c.P); + // The glow of the town runs a little way below the horizon. + const band = gk * dir * 0.32 * exp(-vv / 2.2) * (1 - fog); + R += (gR + gc[0] * band + fogC[0] * (1 - Tr)) * wg; + G += (gR * 0.93 + gc[1] * band + fogC[1] * (1 - Tr)) * wg; + Bc += (gR * 0.8 + gc[2] * band + fogC[2] * (1 - Tr)) * wg; + } + // The landing light beams seen in the air. + const sc = s.scat * land * s.bs[y * W + x]; + const i = (y * W + x) * 3; + const fd = s.fade * 255; + O[i] = (R + sc) * fd + L[i] + BL[i] * s.bloom; + O[i + 1] = (G + sc * 0.95) * fd + L[i + 1] + BL[i + 1] * s.bloom; + O[i + 2] = (Bc + sc * 0.85) * fd + L[i + 2] + BL[i + 2] * s.bloom; + } + + // Drops on the windscreen pick up the lights behind them. + for (const q of s.drops) { + const j = (clamp(round(q.y + 3), 0, H - 1) * W + clamp(round(q.x), 0, W - 1)) * 3; + const e = 1.4 * s.fade; + blob(O, q.x, q.y, 14 * e + BL[j] * e, 16 * e + BL[j + 1] * e, 20 * e + BL[j + 2] * e, q.r); + } + // The wiper blade, dark against the glow. + if (s.rain > 0.3) { + const dx = sin(s.wa); + const dy = -cos(s.wa); + for (let t = 30; t < 175; t += 0.7) { + const x = round(58 + dx * t); + const y = round(150 + dy * t); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + for (let e = 0; e < 2; e++) { + const i = (y * W + min(W - 1, x + e)) * 3; + O[i] *= 0.12; + O[i + 1] *= 0.12; + O[i + 2] *= 0.12; + } + } + } + // A soft shoulder, with a little of an overbright channel spilling into the + // others so bright lights burn toward white. + const sh = (v) => (v < 190 ? v : 255 - 65 * exp(-(v - 190) / 65)); + for (let i = 0, p = 0; p < N; p++, i += 3) { + let r = O[i]; + let g = O[i + 1]; + let b = O[i + 2]; + const m = max(r, g, b); + if (m > 255) { + const sp = (m - 255) * 0.22; + r += sp; + g += sp; + b += sp; + } + display.setPixel(p % W, (p / W) | 0, sh(r), sh(g), sh(b)); + } +} From f6ded57d66f403b9884404c03202f629f85f3bc1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 23:55:58 +0000 Subject: [PATCH 04/15] =?UTF-8?q?Axon=20=ED=8C=A8=ED=84=B4=EC=9D=84=20?= =?UTF-8?q?=EC=A7=A0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GhRE8HZxGnk63uCfeHdr92 --- incoming/pattern-study/patterns/axon.js | 593 ++++++++++++++++++++++++ 1 file changed, 593 insertions(+) create mode 100644 incoming/pattern-study/patterns/axon.js diff --git a/incoming/pattern-study/patterns/axon.js b/incoming/pattern-study/patterns/axon.js new file mode 100644 index 000000000..117558adb --- /dev/null +++ b/incoming/pattern-study/patterns/axon.js @@ -0,0 +1,593 @@ +// ===== Patternflow pattern ===== +// Title: Axon +// Family: body +// Idea: A neuron glows green in a fluorescence micrograph of dark tissue, and each burst it fires leaves the cell body and leaps down the myelinated axon from one node of Ranvier to the next until the terminals spill glowing vesicles across the clefts onto a second neuron, which lights up in turn. +// Author: patternflow-lab +// Date: 2026-10-23 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 23, after Santiago Ramón y Cajal's drawings of neurons and fluorescence microscopy +// Round: 23 +// =============================== +// @matrix 64x128 +// @knobs Dye=0..1, Speed=0.2..3, Rate=1..8, Damage=0..1 +// +// Knob 1: Dye. The fluorescent labels, turning round the colour wheel together. At 0 the neuron is GFP green and the second neuron magenta, on cream myelin with blue nuclei. +// Knob 2: Speed. The clock of the whole slide: at 2 a healthy spike takes about a second from the cell body to the terminals, and at 0.2 everything runs ten times slower. +// Knob 3: Rate. How often the neuron fires: a burst of three spikes every 1.8 s at 1, up to about nine spikes a second at 8. +// Knob 4: Damage. Demyelination. At 0 the sheaths are whole and a spike leaps from node to node. As it rises the sheaths thin and break, and the spike crawls through the bare stretches, fading as it goes. Past about 0.3 the second neuron no longer answers, and near 1 the spike dies in the bare axon before it reaches the terminals. +// Buttons: K1 tap fires a burst of four spikes now. + +const W = 64, H = 128, N = W * H; +const { min, max, abs, exp, sin, cos, hypot, floor, ceil, PI } = Math; +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 rnd = () => Math.random(); +const F32 = (n) => new Float32Array(n); + +// The axon's centre line, a gentle S from the hillock down to where it branches. +const AX = (y) => 32 + 2.6 * sin((y - 28) * 0.07); +const YH = 28; +// Four sheaths 11 rows long, each followed by a bare node two rows long. +const NSH = 4, SA = 39.5, SL = 11, SP = 13; +const sa = (k) => SA + SP * k; +const sb = (k) => sa(k) + SL; +// Where a spike flashes: the end of the initial segment, then each node. +const NODE = [SA - 0.5, sb(0) + 1, sb(1) + 1, sb(2) + 1, sb(3) + 1]; +const YB = sb(3) + 2.5; +// Conduction in LEDs a second of slide time. +const V_AIS = 60, V_FAST = 110, V_SLOW = 14, V_NODE = 40, V_TERM = 45; +// The boutons, and how far below each the second neuron's spine sits. +const BOUT = [[9, 101], [20, 105], [31, 103], [42, 106], [54, 100]]; +const CLEFT = 6, S1 = [32, 23], S2 = [33, 121]; + +// Distance to a segment; T is how far along it the nearest point lies. +let T = 0; +function seg(px, py, ax, ay, bx, by) { + const dx = bx - ax, dy = by - ay; + T = clamp(((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy || 1), 0, 1); + return hypot(px - ax - dx * T, py - ay - dy * T); +} + +// A tapered stroke into layer L, kept by the brighter of old and new. With a +// record R it also notes, LED by LED, how far along a branch the stroke is. +function stroke(L, ax, ay, bx, by, ra, rb, wt, R) { + const r = max(ra, rb) + 1; + const x0 = max(0, floor(min(ax, bx) - r)); + const x1 = min(W - 1, ceil(max(ax, bx) + r)); + const y0 = max(0, floor(min(ay, by) - r)); + const y1 = min(H - 1, ceil(max(ay, by) + r)); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const d = seg(x + 0.5, y + 0.5, ax, ay, bx, by) - (ra + (rb - ra) * T); + const v = clamp(0.5 - d, 0, 1) * wt; + const i = y * W + x; + if (v > L[i]) L[i] = v; + if (R && v > R.c[i]) { + R.c[i] = v; + R.s[i] = R.s0 + T * R.len; + R.b[i] = R.id; + } + } +} + +function blob(L, cx, cy, rx, ry, wt, soft) { + const s = soft || 1; + for (let y = max(0, floor(cy - ry - 2)); y <= min(H - 1, ceil(cy + ry + 2)); y++) + for (let x = max(0, floor(cx - rx - 2)); x <= min(W - 1, ceil(cx + rx + 2)); x++) { + const d = (hypot((x + 0.5 - cx) / rx, (y + 0.5 - cy) / ry) - 1) * min(rx, ry); + const v = clamp(0.5 - d / s, 0, 1) * wt; + const i = y * W + x; + if (v > L[i]) L[i] = v; + } +} + +// A dendrite: short strokes that wander and thin out, forking now and then +// and set with spines. +function grow(L, x, y, a, w, len, depth, wt, spines) { + let s = 0; + while (s < len) { + const nx = x + cos(a) * 1.5; + const ny = y + sin(a) * 1.5; + const nw = max(0.42, w * 0.975); + stroke(L, x, y, nx, ny, w, nw, wt); + if (spines && rnd() < 0.28) { + const side = rnd() < 0.5 ? 1 : -1; + stroke(L, nx, ny, nx - sin(a) * side * (w + 1.3), ny + cos(a) * side * (w + 1.3), 0.3, 0.55, wt * 0.8); + } + (x = nx), (y = ny), (w = nw), (s += 1.5); + a += (rnd() - 0.5) * 0.36; + if (x < -4 || x > W + 4 || y < -4 || y > H + 4) return; + if (depth > 0 && s > 4 && rnd() < 0.1) { + const turn = (0.35 + 0.45 * rnd()) * (rnd() < 0.5 ? 1 : -1); + grow(L, x, y, a + turn, w * 0.8, (len - s) * 0.85 + 3, depth - 1, wt, spines); + a -= turn * 0.4; + } + } +} + +// A quadratic curve of strokes from p to q bent toward c. Returns its length. +function curve(L, p, c, q, r0, r1, wt, R) { + let px = p[0], py = p[1], s = 0; + for (let j = 1; j <= 14; j++) { + const u = j / 14, v = 1 - u; + const qx = v * v * p[0] + 2 * u * v * c[0] + u * u * q[0]; + const qy = v * v * p[1] + 2 * u * v * c[1] + u * u * q[1]; + const len = hypot(qx - px, qy - py); + if (R) (R.s0 = s), (R.len = len); + stroke(L, px, py, qx, qy, r0 + ((r1 - r0) * (j - 1)) / 14, r0 + (r1 - r0) * u, wt, R); + s += len; + (px = qx), (py = qy); + } + return s; +} + +function blur(L) { + const o = F32(N); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) + o[y * W + x] = 0.25 * L[y * W + max(0, x - 1)] + 0.5 * L[y * W + x] + 0.25 * L[y * W + min(W - 1, x + 1)]; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) + L[y * W + x] = 0.25 * o[max(0, y - 1) * W + x] + 0.5 * o[y * W + x] + 0.25 * o[min(H - 1, y + 1) * W + x]; +} + +// A nucleus is dark in the cell's own label and blue in the nuclear stain. +function nucleus(D, NU, x, y, rx, ry) { + const m = F32(N); + blob(m, x, y, rx, ry, 1); + for (let i = 0; i < N; i++) if (m[i] > 0) (D[i] *= 1 - 0.7 * m[i]), (NU[i] = max(NU[i], 0.8 * m[i])); +} + +// Merge a layer into the neighbours' layer, noting whose each LED is. +function merge(S, L, j) { + for (let i = 0; i < N; i++) if (L[i] > S.NB[i]) (S.NB[i] = L[i]), (S.NBI[i] = j); +} + +// Copy a piece into the damageable layer with its breaking point. +function mark(GD, GZ, L, z) { + for (let i = 0; i < N; i++) + if (L[i] > GD[i]) { + GD[i] = L[i]; + GZ[i] = z + 0.08 * rnd(); + } +} + +export function setup(params) { + const S = { + D1: F32(N), X1: F32(N), X1v: F32(N), D2: F32(N), NU: F32(N), NB: F32(N), NBI: new Uint8Array(N), + GL: F32(N), GD: F32(N), GZ: F32(N), GV: F32(N), TI: F32(N), H1: F32(N), H2: F32(N), MY: F32(N), SP1: F32(N), SP2: F32(N), + t: 0, next: 0.5, q: [], sp: [], ves: [], ca1: 0, v2: 0, ca2: 0, f2: 0, + caN: [0, 0, 0, 0], Fs: F32(5), Fn: F32(5), Fb: F32(5), + m: F32(NSH), mr: F32(NSH), nz: F32(NSH * 14), + tN: F32(5), aN: F32(5), tA: F32(NSH), tB: F32(NSH), AA: F32(NSH), AB: F32(NSH), tBo: F32(5), blen: F32(5), + dye: -1, dmg: -1, na: [], + }; + params.s = S; + const { D1, X1, D2, NU, GL, TI } = S; + + // The neuron: a pyramid of a cell body whose apical dendrite climbs to a + // tuft at the top edge. The basal dendrites spread to the sides. + blob(D1, S1[0], S1[1] + 0.5, 7, 6, 0.85); + stroke(D1, 32, 21, 32, 13, 4.6, 1.3, 0.85); + stroke(D1, 32, 27.5, AX(31.5), 31.5, 2.6, 0.9, 0.85); + grow(D1, 32, 13.5, -PI / 2 + (rnd() - 0.5) * 0.2, 1.25, 18, 3, 0.85, true); + grow(D1, 32, 9.5, -PI / 2 - 0.9, 0.8, 22, 1, 0.8, true); + grow(D1, 32, 8, -PI / 2 + 0.9, 0.8, 22, 1, 0.8, true); + grow(D1, 26.5, 27, 2.3, 1.1, 26, 2, 0.85, true); + grow(D1, 37.5, 27, 0.85, 1.1, 26, 2, 0.85, true); + grow(D1, 25.5, 21, PI + 0.3, 1.05, 24, 2, 0.85, true); + grow(D1, 38.5, 21, -0.3, 1.05, 24, 2, 0.85, true); + grow(D1, 28.5, 29, 1.95, 0.8, 14, 1, 0.8, true); + grow(D1, 35.5, 29, 1.2, 0.8, 14, 1, 0.8, true); + nucleus(D1, NU, S1[0] + 0.4, S1[1] + 0.3, 3.2, 3.6); + NU[22 * W + 33] *= 0.4; + + // The axon as a tube of LEDs, row by row, for the myelin and the spikes. + const tI = [], tY = [], tR = [], tK = [], tJ = []; + S.row = new Int32Array(H + 2); + for (let y = 0; y < H; y++) { + S.row[y] = tI.length; + if (y < YH || y > YB + 1) continue; + const yc = y + 0.5, ax = AX(yc), sl = 0.182 * cos((yc - 28) * 0.07); + for (let x = max(0, floor(ax - 5)); x <= min(W - 1, ceil(ax + 5)); x++) { + const r = (x + 0.5 - ax) / Math.sqrt(1 + sl * sl); + let k = -1; + for (let j = 0; j < NSH; j++) if (yc >= sa(j) - 0.5 && yc <= sb(j) + 0.5) k = j; + tI.push(y * W + x), tY.push(yc), tR.push(r), tK.push(k), tJ.push(rnd()); + const rr = 0.7 + 0.3 * clamp((SA - yc) / 10, 0, 1); + X1[y * W + x] = max(X1[y * W + x], clamp(rr + 0.5 - abs(r), 0, 1) * 0.8); + } + } + S.row[H] = S.row[H + 1] = tI.length; + [S.tI, S.tY, S.tR, S.tK, S.tJ] = [Int32Array.from(tI), Float32Array.from(tY), Float32Array.from(tR), Int8Array.from(tK), Float32Array.from(tJ)]; + for (let k = 0; k < NSH; k++) { + S.mr[k] = rnd(); + let a = rnd(); + for (let j = 0; j < 14; j++) { + const b = rnd(); + (S.nz[k * 14 + j] = 0.5 * a + 0.5 * b), (a = b); + } + } + + // The terminal arbor: five branches fanning out from the last node to the + // boutons, each LED tagged with how far along its branch it lies. + const R = { c: F32(N), s: F32(N), b: new Int8Array(N), s0: 0, len: 0, id: 0 }; + const P = [AX(YB), YB]; + BOUT.forEach((B, b) => { + R.id = b; + const C = [P[0] + (B[0] - P[0]) * 0.5, P[1] + (B[1] - P[1]) * 0.2 + 1]; + S.blen[b] = curve(X1, P, C, B, 0.65, 0.5, 0.8, R); + blob(X1, B[0], B[1], 1.8, 1.6, 0.95); + }); + const ti = [], ts = [], tb = [], tc = []; + for (let i = 0; i < N; i++) + if (R.c[i] > 0.05) ti.push(i), ts.push(R.s[i]), tb.push(R.b[i]), tc.push(R.c[i]); + [S.ti, S.ts, S.tb, S.tc] = [Int32Array.from(ti), Float32Array.from(ts), Int8Array.from(tb), Float32Array.from(tc)]; + + // The second neuron below: its dendrites rise to a spine under each bouton. + blob(D2, S2[0], S2[1], 7, 5.4, 0.85); + stroke(D2, 33, 118, 32, 112, 3.8, 1.1, 0.85); + BOUT.forEach((B) => { + const sx = B[0], sy = B[1] + CLEFT; + const st = [33 + (sx - 33) * 0.22, abs(sx - 33) < 4 ? 112 : 117]; + curve(D2, st, [sx * 0.8 + st[0] * 0.2, st[1] - 1], [sx, sy + 2.5], 1.1, 0.55, 0.8); + stroke(D2, sx, sy + 2.5, sx, sy + 0.8, 0.4, 0.5, 0.8); + blob(D2, sx, sy, 1.3, 1.1, 0.95); + }); + grow(D2, 27, 124, PI - 0.15, 1, 24, 2, 0.8, true); + grow(D2, 39, 124, 0.12, 1, 24, 2, 0.8, true); + grow(D2, 28, 118, PI + 0.6, 0.8, 12, 1, 0.75, true); + grow(D2, 38, 118, -0.6, 0.8, 12, 1, 0.75, true); + nucleus(D2, NU, S2[0] + 0.3, S2[1] + 0.5, 3.2, 2.8); + + // Neighbouring neurons, out of focus, each with a calcium flicker of its own. + [[8, 36], [56, 58], [9, 84]].forEach((c, j) => { + const L = F32(N), x = c[0] + (rnd() - 0.5) * 4, y = c[1] + (rnd() - 0.5) * 6; + blob(L, x, y, 3.2, 2.8, 0.7); + for (let d = 0; d < 4; d++) grow(L, x, y, (d / 4) * 2 * PI + rnd(), 0.8, 10 + 8 * rnd(), 1, 0.65, false); + blur(L); + merge(S, L, j); + blob(NU, x, y, 1.8, 1.6, 0.3, 2); + }); + + // Two myelinated axons of other cells cross behind, with nodes of their + // own. Their sheaths and the oligodendrocytes go into GD, the layer that + // Damage takes apart, each piece with its own breaking point in GZ. + const { GD, GZ } = S; + const tmp = F32(N); + [[-3, 47 + rnd() * 4, 67, 43 + rnd() * 4], [-3, 69 + rnd() * 4, 67, 82 + rnd() * 4]].forEach((A, n) => { + const len = hypot(A[2] - A[0], A[3] - A[1]), ux = (A[2] - A[0]) / len, uy = (A[3] - A[1]) / len; + const bow = (n ? -3 : 3) * (0.6 + 0.4 * rnd()); + const at = (s) => [A[0] + ux * s - uy * bow * sin((PI * s) / len), A[1] + uy * s + ux * bow * sin((PI * s) / len)]; + const nodes = []; + for (let s = 2 + rnd() * 8; s < len; s += 10) nodes.push(at(s)); + for (let j = 0; j < nodes.length - 1; j++) { + const p = nodes[j], q = nodes[j + 1], dx = (q[0] - p[0]) / 10, dy = (q[1] - p[1]) / 10; + tmp.fill(0); + sheath(tmp, p[0] + dx * 1.2, p[1] + dy * 1.2, q[0] - dx * 1.2, q[1] - dy * 1.2, 1.5, 0.27); + mark(GD, GZ, tmp, 0.15 + 0.85 * rnd()); + } + tmp.fill(0); + for (const p of nodes) blob(tmp, p[0], p[1], 0.6, 0.6, 0.4); + merge(S, tmp, 3); + S.na.push({ nodes, sp: [] }); + }); + + // Oligodendrocytes, the glia that wrap the sheaths, reaching to the axon. + const og = [[19 + rnd() * 3, 61, -1, [55, 66]], [44 + rnd() * 3, 76, 1, [70, 82]]]; + for (const [x, y, side, ys] of og) { + tmp.fill(0); + blob(tmp, x, y, 2.2, 1.9, 0.6); + for (const ty of ys) { + const tx = AX(ty) + side * 2.4; + curve(tmp, [x, y], [(x + tx) / 2, y + (ty - y) * 0.15 + (rnd() - 0.5) * 3], [tx, ty], 0.5, 0.42, 0.34); + } + mark(GD, GZ, tmp, 0.35 + 0.3 * rnd()); + } + // Astrocytes, small stars of fine processes. + for (const [x, y] of [[54, 31], [52, 95], [12, 70]]) { + blob(GL, x, y, 1.3, 1.2, 0.42); + for (let d = 0; d < 8; d++) grow(GL, x, y, (d / 8) * 2 * PI + rnd() * 0.7, 0.5, 3 + 6 * rnd(), 0, 0.22 + 0.1 * rnd(), false); + } + // Nuclei of every other cell in the slice, in the blue stain. + for (let n = 0; n < 26; n++) { + const x = rnd() * W, y = rnd() * H; + if (hypot(x - S1[0], y - S1[1]) < 10 || hypot(x - S2[0], y - S2[1]) < 10) continue; + if (y > 26 && y < 94 && abs(x - AX(y)) < 5) continue; + blob(NU, x, y, 1.4 + rnd() * 0.8, 1.2 + rnd() * 0.6, 0.2 + rnd() * 0.16, 1.5); + } + // Dark tissue: a faint, uneven autofluorescence. + const g = F32(9 * 17).map(rnd), g2 = F32(17 * 33).map(rnd); + const samp = (G, gw, x, y) => { + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy, at = (a, b) => G[b * gw + a]; + return (at(ix, iy) * (1 - fx) + at(ix + 1, iy) * fx) * (1 - fy) + (at(ix, iy + 1) * (1 - fx) + at(ix + 1, iy + 1) * fx) * fy; + }; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const n = 0.65 * samp(g, 9, x / 8, y / 8) + 0.35 * samp(g2, 17, x / 4, y / 4); + TI[y * W + x] = 0.02 + 0.055 * n * n; + S.H1[y * W + x] = exp(-((x + 0.5 - S1[0]) ** 2 + (y + 0.5 - S1[1]) ** 2) / 240); + S.H2[y * W + x] = exp(-((x + 0.5 - S2[0]) ** 2 + (y + 0.5 - S2[1]) ** 2) / 240); + } +} + +// A sheath seen from above: bright walls and a dark line down the middle. +function sheath(L, ax, ay, bx, by, R, wt) { + for (let y = max(0, floor(min(ay, by) - R - 1)); y <= min(H - 1, ceil(max(ay, by) + R + 1)); y++) + for (let x = max(0, floor(min(ax, bx) - R - 1)); x <= min(W - 1, ceil(max(ax, bx) + R + 1)); x++) { + const d = seg(x + 0.5, y + 0.5, ax, ay, bx, by); + const v = clamp(R + 0.5 - d, 0, 1) * (0.3 + 0.7 * sstep(0.1, R, d)) * wt; + const i = y * W + x; + if (v > L[i]) L[i] = v; + } +} + +function hsv(h, s, v) { + h = ((h % 1) + 1) % 1; + const f = (n) => { + const k = (n + h * 6) % 6; + return v - v * s * max(0, min(k, 4 - k, 1)); + }; + return [f(5), f(3), f(1)]; +} + +function colours(S, dye) { + S.dye = dye; + const h = 0.33 + dye; + S.c1 = hsv(h, 0.88, 1); + S.c2 = hsv(h + 0.53, 0.78, 1); + S.cm = hsv(h - 0.21, 0.3, 1); + S.cn = hsv(h + 0.31, 0.8, 1); + S.ct = hsv(h + 0.2, 0.55, 1); +} + +// The sheaths for this much damage: each thins and breaks by its own measure. +function myelin(S, dmg) { + S.dmg = dmg; + for (let k = 0; k < NSH; k++) S.m[k] = clamp(1 - dmg * (0.8 + 0.7 * S.mr[k]), 0, 1); + const { tI, tY, tR, tK, tJ, MY, X1, X1v } = S; + X1v.set(X1); + for (let j = 0; j < tI.length; j++) { + const i = tI[j], k = tK[j]; + MY[i] = 0; + if (k < 0) continue; + const m = S.m[k], y = tY[j], r = abs(tR[j]); + const Rs = 2.75 * (0.45 + 0.55 * m); + const dy = max(0, sa(k) + Rs - y, y - (sb(k) - Rs)); + const cov = clamp(0.5 - (hypot(r, dy) - Rs), 0, 1); + const thr = (1 - m) * 1.3 - 0.15; + const row = clamp(floor(y - sa(k) + 0.5), 0, 13); + const pres = sstep(thr - 0.1, thr + 0.1, S.nz[k * 14 + row] + 0.12 * tJ[j]); + MY[i] = cov * pres * (0.18 + 0.82 * sstep(0.2, 1.5, r)) * (0.5 + 0.25 * m); + X1v[i] = X1[i] * (1 - 0.9 * pres * cov); + } + const thr = dmg * 1.3 - 0.15; + for (let i = 0; i < N; i++) S.GV[i] = S.GL[i] + (S.GD[i] ? S.GD[i] * sstep(thr - 0.1, thr + 0.1, S.GZ[i]) : 0); +} + +// A spike's timetable at this much damage: when it reaches each node and how +// strong it is there. It dies inside a sheath that is too far gone. +function table(S) { + const { tN, aN, m } = S; + tN[0] = (SA - YH) / V_AIS; + aN[0] = 1; + S.blk = -1; + for (let k = 0; k < NSH; k++) { + const v = V_SLOW + (V_FAST - V_SLOW) * m[k] * m[k]; + const A0 = aN[k]; + const A1 = A0 * (1 - 0.36 * (1 - m[k]) ** 1.2); + S.tA[k] = tN[k] + 0.5 / V_NODE; + S.tB[k] = S.tA[k] + SL / v; + (S.AA[k] = A0), (S.AB[k] = A1); + if (A1 < 0.3) { + const f = (A0 - 0.3) / (A0 - A1); + S.blk = k; + S.blkY = sa(k) + SL * f; + S.blkT = S.tA[k] + (SL * f) / v; + for (let j = k + 1; j <= NSH; j++) { + tN[j] = 1e9; + aN[j] = 0; + } + break; + } + tN[k + 1] = S.tB[k] + 1 / V_NODE; + aN[k + 1] = A1; + } + S.tT = tN[NSH] + (YB - NODE[NSH]) / V_NODE; + let end = 0; + for (let b = 0; b < 5; b++) { + S.tBo[b] = S.tT + S.blen[b] / V_TERM; + end = max(end, S.tBo[b]); + } + S.tEnd = (S.blk >= 0 ? S.blkT : end) + 0.8; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s; + const k = input.knobValues || [0, 2, 1, 0.06]; + dt = clamp(dt || 0, 0, 0.1); + const dye = clamp(k[0], 0, 1); + const rate = clamp(k[2], 1, 8); + if (dye !== S.dye) colours(S, dye); + const dmg = clamp(k[3], 0, 1); + if (abs(dmg - S.dmg) > 0.001) myelin(S, dmg); + table(S); + // Slide time: Speed 2 is real time. + const st = (dt * clamp(k[1], 0.2, 3)) / 2; + S.t += st; + if (S.t >= S.next) { + S.q.push(S.t, S.t + 0.2, S.t + 0.4); + S.next = S.t + (1.8 / rate ** 0.8) * (0.75 + 0.5 * rnd()); + } + if (input.btnPressed && input.btnPressed[0]) { + for (let j = 0; j < 4; j++) S.q.push(S.t + j * 0.15); + S.next = max(S.next, S.t + 1.2); + } + S.q.sort((a, b) => a - b); + while (S.q.length && S.q[0] <= S.t) { + S.sp.push({ t: S.t - S.q.shift(), p: -1 }); + S.ca1 += 0.3 * (1.25 - S.ca1); + } + // Each spike moves on; one reaching a bouton releases its vesicles. + for (const p of S.sp) { + p.p = p.t; + p.t += st; + for (let b = 0; b < 5; b++) + if (p.p < S.tBo[b] && p.t >= S.tBo[b]) + for (let n = 0; n < 3; n++) { + const B = BOUT[b]; + S.ves.push({ b, a: S.aN[NSH], x: B[0] + (rnd() - 0.5) * 2, y: B[1] + 1.2, tx: B[0] + (rnd() - 0.5) * 1.6, ty: B[1] + CLEFT - 1, age: -n * 0.05, dur: 0.3 + 0.2 * rnd() }); + } + } + S.sp = S.sp.filter((p) => p.t < S.tEnd); + for (const v of S.ves) { + v.age += st; + if (v.age >= v.dur) { + S.v2 += (0.75 * v.a) / 15; + S.Fs[v.b] += 0.3; + } + } + S.ves = S.ves.filter((v) => v.age < v.dur); + // The second neuron fires once enough transmitter has landed. + if (S.v2 >= 1) { + (S.v2 = 0.15), (S.ca2 = min(1.3, S.ca2 + 0.8)), (S.f2 = 1); + } + (S.ca1 *= exp(-st / 0.9)), (S.v2 *= exp(-st / 0.6)), (S.ca2 *= exp(-st / 1.1)), (S.f2 *= exp(-st / 0.15)); + for (let b = 0; b < 5; b++) S.Fs[b] *= exp(-st / 0.25); + for (let j = 0; j < 3; j++) { + S.caN[j] *= exp(-st / 1.4); + if (rnd() < st * 0.3) S.caN[j] += 0.7; + } + // The neighbouring axons carry spikes now and then too. + for (const A of S.na) { + for (const p of A.sp) p.t += st; + A.sp = A.sp.filter((p) => p.t < A.nodes.length * 0.1 + 0.5); + if (rnd() < st * 0.5) A.sp.push({ t: 0, back: rnd() < 0.5 }); + } +} + +// A bright point with a soft halo, added into layer L. +function bloom(L, cx, cy, amp, rc, rh, ha) { + if (amp < 0.004) return; + const R = ceil(rh * 2.2); + for (let y = max(0, floor(cy - R)); y <= min(H - 1, ceil(cy + R)); y++) + for (let x = max(0, floor(cx - R)); x <= min(W - 1, ceil(cx + R)); x++) { + const d2 = (x + 0.5 - cx) ** 2 + (y + 0.5 - cy) ** 2; + L[y * W + x] += amp * (exp(-d2 / (rc * rc)) + ha * exp(-d2 / (rh * rh))); + } +} + +const pulse = (u, tau) => (u < -0.03 ? 0 : u < 0 ? 1 + u / 0.03 : exp(-u / tau)); + +export function draw(display, params) { + const S = params.s; + if (!S || !S.c1) return; + const { SP1, SP2, tN, aN, Fn, Fb, tI, tY, tR, tK, MY } = S; + for (const A of [SP1, SP2, Fn, Fb]) A.fill(0); + let hill = 0; + for (const p of S.sp) { + const t = p.t; + hill += exp(-t / 0.12); + for (let j = 0; j <= NSH; j++) Fn[j] += aN[j] * pulse(t - tN[j], 0.08); + // The front: always seen in the bare initial segment, seen in a sheath + // only as far as its myelin is gone. + let yf = -1, amp = 0; + if (t < tN[0]) { + yf = YH + ((SA - YH) * t) / tN[0]; + amp = 1; + } else + for (let k = 0; k < NSH; k++) { + if (S.blk === k && t >= S.blkT) { + yf = S.blkY; + amp = 0.3 * exp(-(t - S.blkT) / 0.25); + } else if (t >= S.tA[k] && t < S.tB[k]) { + const f = (t - S.tA[k]) / (S.tB[k] - S.tA[k]); + yf = sa(k) + SL * f; + amp = (S.AA[k] + (S.AB[k] - S.AA[k]) * f) * (1 - S.m[k]) ** 0.8; + } + } + if (yf >= 0 && amp > 0.01) { + for (let y = max(YH, floor(yf - 9)); y <= min(H - 1, ceil(yf + 3)); y++) + for (let j = S.row[y]; j < S.row[y + 1]; j++) { + const dy = tY[j] - yf; + const w = exp((-dy * dy) / 2.2) + (dy < 0 ? 0.35 * exp(dy / 3.5) : 0); + const r2 = tR[j] * tR[j]; + SP1[tI[j]] += amp * w * (1.1 * exp(-r2 / 0.9) + 0.15 * exp(-r2 / 6)); + } + } + // Down the terminal branches to the boutons. + if (t >= S.tT && t < S.tT + 1.2) { + const sf = (t - S.tT) * V_TERM; + const a = aN[NSH]; + for (let j = 0; j < S.ti.length; j++) { + const s = S.ts[j]; + if (s > S.blen[S.tb[j]] + 1) continue; + const d = s - sf; + if (d > -8 && d < 8) SP1[S.ti[j]] += a * S.tc[j] * 1.1 * exp((-d * d) / 3); + } + } + for (let b = 0; b < 5; b++) Fb[b] += aN[NSH] * pulse(t - S.tBo[b], 0.18); + } + // Node flashes, and the sheaths on either side glowing with them. + bloom(SP1, AX(29.5), 29.5, min(1.5, hill) * 0.5, 2, 4.5, 0.4); + for (let j = 0; j <= NSH; j++) bloom(SP1, AX(NODE[j]), NODE[j], Fn[j] * (j ? 1 : 0.7), 1.1, 3.4, 0.4); + for (let j = 0; j < tI.length; j++) { + const k = tK[j]; + if (k < 0) continue; + const g = max(Fn[k], Fn[k + 1]); + if (g > 0.01) SP1[tI[j]] += 0.9 * g * MY[tI[j]]; + } + for (let b = 0; b < 5; b++) { + bloom(SP1, BOUT[b][0], BOUT[b][1], Fb[b], 1.6, 4, 0.4); + bloom(SP2, BOUT[b][0], BOUT[b][1] + CLEFT, S.Fs[b], 1.3, 3.5, 0.35); + } + // Vesicles crossing the clefts. + for (const v of S.ves) { + if (v.age < 0) continue; + const u = v.age / v.dur; + const e = u * u * (3 - 2 * u); + bloom(SP1, v.x + (v.tx - v.x) * e, v.y + (v.ty - v.y) * e, 0.7 * (1 - 0.4 * u), 0.6, 1.8, 0.15); + } + for (const A of S.na) + for (const p of A.sp) { + const n = A.nodes.length; + for (let j = 0; j < n; j++) { + const u = p.t - j * 0.1; + if (u > -0.03 && u < 0.5) { + const q = A.nodes[p.back ? n - 1 - j : j]; + bloom(SP1, q[0], q[1], 0.55 * pulse(u, 0.08), 0.9, 2.6, 0.35); + } + } + } + + const { D1, X1v, D2, NU, NB, NBI, GV, TI, H1, H2, c1, c2, cm, cn, ct } = S; + const k1 = 0.28 + 0.95 * S.ca1; + const kx = 0.62 + 0.4 * S.ca1; + const kh1 = 0.03 + 0.32 * S.ca1; + const k2 = 0.27 + 0.4 * S.v2 + 1 * S.ca2 + 0.5 * S.f2; + const kh2 = 0.03 + 0.3 * S.ca2; + const kn = [0.45 + 1.4 * S.caN[0], 0.45 + 1.4 * S.caN[1], 0.45 + 1.4 * S.caN[2], 1]; + for (let i = 0; i < N; i++) { + const v1 = D1[i] * k1 + X1v[i] * kx + NB[i] * kn[NBI[i]] + H1[i] * kh1 + SP1[i]; + const v2 = D2[i] * k2 + H2[i] * kh2 + SP2[i]; + const vm = MY[i] + GV[i]; + const vn = NU[i]; + const vt = TI[i]; + let r = 255 * (v1 * c1[0] + v2 * c2[0] + vm * cm[0] + vn * cn[0] + vt * ct[0]); + let g = 255 * (v1 * c1[1] + v2 * c2[1] + vm * cm[1] + vn * cn[1] + vt * ct[1]); + let b = 255 * (v1 * c1[2] + v2 * c2[2] + vm * cm[2] + vn * cn[2] + vt * ct[2]); + // Where a label saturates the camera it bleaches toward white. + const top = max(r, g, b); + if (top > 210) { + const e = (top - 210) * 0.5; + r += e; + g += e; + b += e; + } + display.setPixel(i % W, (i / W) | 0, min(255, r), min(255, g), min(255, b)); + } +} From 82dda78f51695e636a9ce1a3cda9e2e1415ab0ab Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 23:59:43 +0000 Subject: [PATCH 05/15] =?UTF-8?q?Sequins=20=ED=8C=A8=ED=84=B4=EC=9D=84=20?= =?UTF-8?q?=EC=A7=A0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GhRE8HZxGnk63uCfeHdr92 --- incoming/pattern-study/patterns/sequins.js | 339 +++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 incoming/pattern-study/patterns/sequins.js diff --git a/incoming/pattern-study/patterns/sequins.js b/incoming/pattern-study/patterns/sequins.js new file mode 100644 index 000000000..5ff25b627 --- /dev/null +++ b/incoming/pattern-study/patterns/sequins.js @@ -0,0 +1,339 @@ +// ===== Patternflow pattern ===== +// Title: Sequins +// Family: textile +// Idea: A cushion of reversible flip sequins fills the panel under a lamp, silver scales in staggered rows, and an unseen fingertip strokes a heart, a star, a cat's face, a lightning bolt and a wave into it one after another, each sequin it passes turning edge on and over to its coloured side, until a flat palm sweeps the cushion silver again. +// Author: patternflow-lab +// Date: 2026-10-23 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 23, after reversible flip-sequin fabric +// Round: 23 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Drawing=0..4, Lamp=0..1 +// +// Knob 1: Hue. The colour on the back of the sequins: hot pink at 0, then on through red, gold, green and blue to violet, and pink again at 1. +// Knob 2: Speed. How fast the fingertip draws and the palm sweeps: at 2 a drawing takes about four seconds and a round seven; at 0.2 everything runs ten times slower. +// Knob 3: Drawing. What the fingertip draws next: a heart, a star, a cat's face, a lightning bolt or a wave, the rest following in that order. Turning it sweeps the cushion silver and starts the new one. +// Knob 4: Lamp. Where the lamp stands above the cushion, from upper left over the top to upper right: the highlight in every cupped sequin slides the other way, and the bright sheen across the cushion follows the lamp. +// Buttons: K1 tap sweeps the palm down the cushion now and the next drawing starts. + +const W = 64; +const H = 128; +const N = W * H; +const NDRAW = 5; +const { min, max, sqrt, sin, cos, abs, floor, round, hypot, atan2, PI } = Math; +// Sequins sit 5 LEDs apart in a row and rows 3 LEDs apart, every other row +// shifted by half a sequin. Each is a disc a little wider than the spacing, +// so the row above covers the top of the row below. +const PX = 5; +const PY = 3; +const R = 2.8; +const NC = floor(W / PX) + 2; +// The round at Speed 2, in seconds: draw, hold, sweep, rest. +const TD = 4.2; +const TH = 1.8; +const TS = 0.8; +const TR = 0.3; +const TC = TD + TH + TS + TR; +// One sequin takes this long to turn over. +const FL = 0.16; +// How far the fingertip reaches, in LEDs. +const REACH = 4.2; +// The shading of one sequin. The part that shows sits low on the disc. The +// sequins are cupped, so the lamp lights the inside wall away from it, where a +// tight highlight sits. The rim nearest the lamp falls into shadow. +const SHOW = 0.5; +const SPOT = 0.45; +const SPOT2 = 0.15; +const RIM = 0.62; +// Each sequin is tipped a little its own way and catches more or less lamp. +const GLA = 0.35; +const GLB = 0.9; +// The coloured side reflects this share of what the silver side does. +const DIM = 0.24; +const clamp = (v, a, b) => (v < a ? a : v > b ? b : v); +const hash = (i) => { + let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; + +// The sequins, listed bottom row first, so a later one lies over an earlier one. +const SX = []; +const SY = []; +const ROWS = floor(H / PY) + 1; +for (let k = ROWS - 1; k >= -1; k--) + for (let j = -1; j < NC - 1; j++) { + SX.push(j * PX + ((k & 1) ? 0 : PX / 2)); + SY.push(k * PY + 1.5); + } +const NS = SX.length; + +// The drawings, as shapes in LED coordinates: true inside. +function inPoly(x, y, pts) { + let c = false; + for (let i = 0, j = pts.length - 2; i < pts.length; j = i, i += 2) { + const xi = pts[i]; + const yi = pts[i + 1]; + const xj = pts[j]; + const yj = pts[j + 1]; + if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) c = !c; + } + return c; +} +function heart(x, y) { + const X = (x - 32) / 26; + const Y = (35 - y) / 26; + const a = X * X + Y * Y - 1; + return a * a * a - X * X * Y * Y * Y <= 0; +} +function star(x, y) { + const dx = x - 32; + const dy = y - 66; + const r = hypot(dx, dy); + const t = (((atan2(dx, -dy) / (2 * PI)) * 5) % 1 + 1) % 1; + const e = abs(t - 0.5) * 2; + return r < 13 + 19 * e * e; +} +function cat(x, y) { + const dx = x - 32; + const dy = y - 66; + const head = (dx / 22) ** 2 + (dy / 19) ** 2 < 1; + const ears = inPoly(x, y, [11, 60, 13, 34, 28, 50]) || inPoly(x, y, [53, 60, 51, 34, 36, 50]); + const eyes = ((abs(dx) - 8.5) / 4.6) ** 2 + ((dy + 3) / 5.5) ** 2 < 1; + const nose = inPoly(x, y, [28.5, 71, 35.5, 71, 32, 76]); + // Whiskers reach out past the cheeks, two to a side. + const ax = abs(dx); + const wh = ax > 21 && (abs(dy - 3 + (ax - 21) * 0.35) < 1.6 || abs(dy - 8 - (ax - 21) * 0.2) < 1.6); + return ((head || ears) && !eyes && !nose) || wh; +} +const BOLT = [40, 0, 58, 0, 38, 48, 55, 48, 14, 128, 26, 66, 8, 66]; +const bolt = (x, y) => inPoly(x, y, BOLT); +// The sea, with one wave standing up and curling over to the left. +function wave(x, y) { + const sea = y > 77 + 2.5 * sin(x * 0.35 + 1); + const back = x > 28 && y > 40 + (x - 28) ** 2 / 20; + const curl = hypot(x - 29, y - 52) < 14 && hypot(x - 24, y - 60) > 6.5 && y < 64; + const face = y > 56 && x > 31 - (y - 58) * 0.9 && x < 36; + return sea || back || curl || face; +} +const DRAWINGS = [heart, star, cat, bolt, wave]; +// How the fingertip colours each one in: the angle of its strokes. +const STROKE = [0.35, -0.5, 0, 1.25, 0.12]; + +export function setup(params) { + params.sq = { + p: new Float32Array(NS), + on: new Uint8Array(NS), + tOn: new Float32Array(NS), + tOff: new Float32Array(NS), + jit: new Float32Array(NS), + r: new Float32Array(N), + g: new Float32Array(N), + b: new Float32Array(N), + own: new Int32Array(N), + started: false, + }; + const S = params.sq; + for (let i = 0; i < NS; i++) S.jit[i] = GLA + GLB * hash(i); +} + +// The fingertip's path: strokes back and forth across the shape at an angle, +// a little different each time. +function load(S, d) { + const fn = DRAWINGS[d]; + const a = STROKE[d] + (Math.random() - 0.5) * 0.3; + const ux = cos(a); + const uy = sin(a); + const pts = []; + let m = 0; + for (let s = -76; s <= 76; s += 6, m++) { + const segs = []; + let t0 = null; + for (let t = -76; t <= 77; t++) { + const x = 32 - uy * s + ux * t; + const y = 64 + ux * s + uy * t; + const inside = t < 77 && x >= 0 && x < W && y >= 0 && y < H && fn(x, y); + if (inside && t0 === null) t0 = t; + if (!inside && t0 !== null) { + segs.push(t0, t - 1); + t0 = null; + } + } + if (m & 1) segs.reverse(); + for (const t of segs) pts.push(32 - uy * s + ux * t, 64 + ux * s + uy * t); + } + // Each sequin in the shape turns when the fingertip first comes within reach. + let len = 0; + for (let i = 2; i < pts.length; i += 2) len += hypot(pts[i] - pts[i - 2], pts[i + 1] - pts[i - 1]); + for (let n = 0; n < NS; n++) { + S.tOn[n] = 1e9; + const cx = SX[n]; + const cy = SY[n]; + if (cx < 0 || cx >= W || cy < 0 || cy >= H || !fn(cx, cy)) continue; + let at = 0; + let hit = len; + for (let i = 0; i + 2 < pts.length; i += 2) { + const dx = pts[i + 2] - pts[i]; + const dy = pts[i + 3] - pts[i + 1]; + const l = hypot(dx, dy) || 1e-6; + const fx = pts[i] - cx; + const fy = pts[i + 1] - cy; + const bb = (fx * dx + fy * dy) / l; + const cc = fx * fx + fy * fy - REACH * REACH; + const disc = bb * bb - cc; + if (disc >= 0) { + const t = cc <= 0 ? 0 : -bb - sqrt(disc); + if (t >= 0 && t <= l) { + hit = at + t; + break; + } + } + at += l; + } + S.tOn[n] = (0.1 + (hit / len) * 0.86) * TD; + } + // The palm comes down from the top, its lower edge a little uneven. + const lean = Math.random() - 0.5; + for (let n = 0; n < NS; n++) S.tOff[n] = clamp((SY[n] + 10 + 5 * sin(SX[n] * 0.09 + lean * 3) + lean * (SX[n] - 32) * 0.3) / (H + 24), 0, 1) * TS * 0.9; +} + +function sweep(S) { + if (S.c < TD + TH) S.c = TD + TH; +} + +export function update(dt, input, params) { + if (!params.sq) setup(params); + const S = params.sq; + const k = input.knobValues || [0, 2, 0, 0.06]; + dt = clamp(dt || 0, 0, 0.1); + S.hue = clamp(k[0], 0, 1); + S.rate = clamp(k[1], 0.2, 3) / 2; + const base = clamp(round(k[2]), 0, NDRAW - 1); + S.lamp = clamp(k[3], 0, 1); + if (!S.started) { + S.started = true; + S.base = base; + S.count = -1; + // Open on the drawing before, just finished, a little before the palm. + load(S, (base + NDRAW - 1) % NDRAW); + S.c = TD + TH - 2.4 * S.rate; + for (let i = 0; i < NS; i++) { + S.on[i] = S.tOn[i] <= S.c ? 1 : 0; + S.p[i] = S.on[i]; + } + } + if (base !== S.base) { + S.base = base; + S.count = -1; + sweep(S); + } + if (input.btnPressed && input.btnPressed[0]) sweep(S); + S.c += dt * S.rate; + if (S.c >= TC) { + S.c -= TC; + S.count++; + load(S, (S.base + S.count + NDRAW * 8) % NDRAW); + } + const { p, on, tOn, tOff } = S; + const c = S.c; + const sw = c - TD - TH; + for (let i = 0; i < NS; i++) { + if (sw < 0) { + if (c >= tOn[i]) on[i] = 1; + } else if (sw >= tOff[i]) on[i] = 0; + const step = (dt * S.rate) / FL; + p[i] = on[i] ? min(1, p[i] + step) : max(0, p[i] - step); + } +} + +function hsv(h, s, v) { + const f = (n) => { + const k = (n + h * 6) % 6; + return v - v * s * max(0, min(k, 4 - k, 1)); + }; + return [f(5), f(3), f(1)]; +} + +export function draw(display, params) { + const S = params.sq; + if (!S || !S.started) return; + const { p, jit, r, g, b, own } = S; + r.fill(0); + g.fill(0); + b.fill(0); + own.fill(-1); + // The lamp stands above the cushion, from upper left over to upper right. + const ang = PI * (0.78 - 0.56 * S.lamp); + const lx = cos(ang); + const ly = -sin(ang); + const tint = hsv((0.9 + S.hue) % 1, 0.9, 1); + for (let n = 0; n < NS; n++) { + const cx = SX[n]; + const cy = SY[n]; + const cs = cos(PI * p[n]); + const back = cs < 0; + const sy = max(0.55, R * abs(cs)); + const edge = abs(cs) < 0.4; + const x0 = max(0, Math.ceil(cx - R - 0.5)); + const x1 = min(W - 1, floor(cx + R - 0.5)); + const y0 = max(0, Math.ceil(cy - sy - 0.5)); + const y1 = min(H - 1, floor(cy + sy - 0.5)); + // The sheen: brighter toward the lamp, falling off to the cushion's sides. + const qx = cx - 32 - 34 * lx; + const qy = (cy - 64 - 56 * ly) * 0.6; + const sh = min(1, 0.76 + 0.3 * Math.exp(-(qx * qx + qy * qy) / 1500)) * (1 - 0.12 * ((cx - 32) / 36) ** 2); + const gl = jit[n]; + // Tipping over, a sequin's face swings through the lamp's reflection. + const tip = 1 + 0.5 * sin(PI * p[n]) ** 2; + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const u = (x + 0.5 - cx) / R; + const v = (y + 0.5 - cy) / sy; + if (u * u + v * v > 1) continue; + const i = y * W + x; + own[i] = n; + if (edge) { + // Edge on: a thin line of bare metal. + const e = 150 * sh; + r[i] = e; + g[i] = e; + b[i] = e * 1.05; + continue; + } + const sv = (y + 0.5 - cy - SHOW * abs(cs)) / (0.9 * sy); + const su = (x + 0.5 - cx) / (0.9 * R); + const dot = su * lx + sv * ly; + const hx = su + SPOT * lx; + const hy = sv + SPOT * ly; + const hl = max(0, 1 - (hx * hx + hy * hy) / SPOT2); + let val = ((1 - 0.5 * dot) * gl + 0.7 * hl) * tip; + if (u * u + v * v > RIM) val *= dot > 0 ? 0.2 : 1.1; + if (!back) { + const e = 255 * val * sh; + r[i] = e * 0.9; + g[i] = e * 0.94; + b[i] = e; + } else { + // The coloured side is darker, but its highlight still bites. + const e = 255 * val * sh * DIM; + const w = 140 * sh * hl * hl * gl; + r[i] = e * tint[0] + w * (0.5 + 0.5 * tint[0]); + g[i] = e * tint[1] + w * (0.5 + 0.5 * tint[1]); + b[i] = e * tint[2] + w * (0.5 + 0.5 * tint[2]); + } + } + } + // Shadows: the lamp is above, so a sequin lying over another throws a thin + // shadow down from its lower edge onto the one below. + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x; + const f = y > 0 && own[i - W] > own[i] ? 0.1 : 1; + if (own[i] < 0) { + display.setPixel(x, y, 10 * f, 9 * f, 12 * f); + continue; + } + display.setPixel(x, y, min(255, r[i] * f), min(255, g[i] * f), min(255, b[i] * f)); + } +} From 76946214cae453b0af39eefd3f3d7a415c5038d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 23:59:44 +0000 Subject: [PATCH 06/15] =?UTF-8?q?Bait=20Ball=20=ED=8C=A8=ED=84=B4=EC=9D=84?= =?UTF-8?q?=20=EC=A7=A0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GhRE8HZxGnk63uCfeHdr92 --- incoming/pattern-study/patterns/bait-ball.js | 465 +++++++++++++++++++ 1 file changed, 465 insertions(+) create mode 100644 incoming/pattern-study/patterns/bait-ball.js diff --git a/incoming/pattern-study/patterns/bait-ball.js b/incoming/pattern-study/patterns/bait-ball.js new file mode 100644 index 000000000..9834a0874 --- /dev/null +++ b/incoming/pattern-study/patterns/bait-ball.js @@ -0,0 +1,465 @@ +// ===== Patternflow pattern ===== +// Title: Bait Ball +// Family: ocean +// Idea: Seen from the side in open blue water, a sardine bait ball fills the panel with a thousand fish churning in layers, and when the school turns together a wave of silver runs across it until it turns away again and shows its dark blue backs, while sharks and dolphins charge through and gannets dive in from the surface, punching holes that close behind them. +// Author: patternflow-lab +// Date: 2026-10-23 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 23, after the sardine bait balls of the Sardine Run +// Round: 23 +// =============================== +// @matrix 64x128 +// @knobs Water=0..1, Speed=0.2..3, Predators=1..5, Sun=0..1 +// +// Knob 1: Water. The colour of the sea: the clear blue of open ocean at 0, turning to the green of plankton-rich coastal water at 1. +// Knob 2: Speed. How fast the school and its hunters move: at 2 a turn runs across the ball in just over a second, and the school shows its flanks for five to eight seconds before it turns away; at 0.2 it all runs ten times slower. +// Knob 3: Predators. How many sharks and dolphins work the ball, from one to five. More hunters charge more often and bring more gannets with them. The school packs tighter against them. +// Knob 4: Sun. The time of day, from noon of a Sardine Run winter at 0, when the sun stands 35 degrees up and its rays lean 38 degrees in the water, to late afternoon at 1. Toward 1 the rays lean further and fade as the sea darkens. The flash dims to cold steel. +// Buttons: K1 tap sends a hunter charging through the ball now. + +const W = 64; +const H = 128; +const N = W * H; +// Fish swim in rows one LED apart, each two body lengths behind the next and +// one length along from the fish in the rows above and below, as schools space themselves. +const ROWS = 124; +const GAP = 6; +// Each row's fish wrap round this length, wider than the panel so the wrap stays out of sight. +const SPAN = 84; +// Rows swim in bands this deep, each band the other way from the next: the ball mills. +const BAND = 13; +// How fast a turning front runs, in LEDs per second at Speed 2. +const SWEEP = 80; +const { min, max, sin, exp, sqrt, round, floor, atan2, PI } = Math; +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 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; +}; + +// A dusky shark and a common dolphin, nose to the right: # back, b belly, h the dolphin's tan flank, e eye. +const SHARK = [ + "..........#.........", + ".........##.........", + "#.......##########..", + "##...###############", + ".##############e####", + "##...bbbbbbbbbbbbbb.", + "#........#.....#....", +]; +const DOLPHIN = [ + "........#..........", + ".......##..........", + "...###########.....", + "##############e##..", + ".##hhhhhhbbbbbbbbbb", + "##...bbbbbbbbb.....", + ".........#.........", +]; +const SHARK_C = { "#": [30, 40, 54], b: [74, 84, 96], e: [4, 4, 6] }; +const DOLPHIN_C = { "#": [40, 48, 66], b: [128, 136, 148], h: [118, 104, 70], e: [4, 4, 6] }; + +export function setup(params) { + const fu = []; + const fr = []; + for (let j = 0; j < ROWS; j++) + for (let u = (j & 1) * (GAP / 2); u < SPAN - 1; u += GAP) { + fu.push(u + (Math.random() - 0.5) * 1.4); + fr.push(j); + } + const n = fu.length; + const R = (a, b) => Float32Array.from({ length: n }, () => a + Math.random() * b); + const v = new Float32Array(ROWS); + for (let j = 0; j < ROWS; j++) { + const b = floor(j / BAND); + v[j] = (b & 1 ? -1 : 1) * (3.2 + 2.4 * sin(b * 2.1 + 0.4)); + } + params.s = { + fu: Float32Array.from(fu), + fr: Uint8Array.from(fr), + edge: R(0, 1), + lag: R(0, 1), + ph: R(0, 6.283), + v, + off: new Float32Array(ROWS), + buf: new Float32Array(N * 3), + clear: new Float32Array(N), + lt: new Float32Array(129), + rt: new Float32Array(440), + tau: 0, + base: 0, + ev: [], + next: 2.08, + nextTo: 1, + nextDir: 1, + hunters: [], + gannets: [], + bubbles: [], + holes: [], + // The first hunter charges just as the panel has opened, and the school turns from it. + slots: Float32Array.from({ length: 5 }, (_, k) => 2.1 + k * (1.7 + Math.random() * 1.2)), + gTimer: 3.2 + Math.random(), + queue: [], + k: [0, 2, 1, 0.06], + }; +} + +function charge(S, from) { + const dir = from || (Math.random() < 0.5 ? 1 : -1); + S.hunters.push({ + kind: Math.random() < 0.5 ? 0 : 1, + dir, + x: dir > 0 ? -18 : W + 18, + y0: S.cy + (Math.random() * 2 - 1) * S.ry * 0.55, + y: 0, + sp: 38 + Math.random() * 16, + ph: Math.random() * 6.283, + drop: 0, + }); +} + +function dive(S, delay) { + S.queue.push({ at: S.tau + delay, x: 8 + Math.random() * 48 }); +} + +// How far a turn has carried a point: 0 shows the backs, 1 the silver flanks. +// Each front is kept as one position per row. TG is the flash right on the +// front, where the fish are mid-turn. +let TG = 0; +function fronts(S) { + for (const e of S.ev) { + if (!e.row) e.row = new Float32Array(H); + const X = (S.tau - e.at) * SWEEP - 12; + for (let y = 0; y < H; y++) e.row[y] = X - 9 * sin(y * 0.045 + e.ph) - 0.2 * (y - 64) * e.tilt; + } +} +function turned(S, x, y, lag) { + let s = S.base; + TG = 0; + const yi = y < 0 ? 0 : y > H - 1 ? H - 1 : y | 0; + for (const e of S.ev) { + const d = e.row[yi] - (e.dir > 0 ? x : 63 - x) - lag * 8; + if (d <= 0) continue; + if (d >= 8) { + s = e.to; + continue; + } + const p = sstep(0, 8, d); + TG = max(TG, 4 * p * (1 - p)); + s += (e.to - s) * p; + } + return s; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s; + const k = input.knobValues || S.k; + S.k = [clamp(k[0], 0, 1), clamp(k[1], 0.2, 3), clamp(k[2], 1, 5), clamp(k[3], 0, 1)]; + dt = clamp(dt || 0, 0, 0.1); + const d = dt * (S.k[1] / 2); + S.tau += d; + const t = S.tau; + const hunt = round(S.k[2]); + // The more hunters, the tighter the school packs. + const sc = 1 - 0.055 * (S.k[2] - 1); + S.cx = 32 + 2.5 * sin(t * 0.17); + S.cy = 73 + 3 * sin(t * 0.13 + 1); + S.rx = 32 * sc; + S.ry = 61 * sc; + for (let j = 0; j < ROWS; j++) S.off[j] += S.v[j] * d; + + // The turns: the first comes just after the panel opens on dark backs. + if (t >= S.next) { + S.ev.push({ at: S.next, to: S.nextTo, dir: S.nextDir, ph: Math.random() * 6.283, tilt: Math.random() * 2 - 1 }); + const first = S.ev.length === 1 && S.next < 3; + S.next += S.nextTo ? (first ? 7.3 : 5 + Math.random() * 3) : 2.6 + Math.random() * 2; + S.nextTo = 1 - S.nextTo; + S.nextDir = first ? -1 : Math.random() < 0.5 ? 1 : -1; + } + // A front that has left the panel is folded into the base state. + while (S.ev.length && (t - S.ev[0].at) * SWEEP > 130) S.base = S.ev.shift().to; + + // Hunters from the sides, each on its own clock. + for (let i = 0; i < 5; i++) { + if (i >= hunt) continue; + S.slots[i] -= d; + if (S.slots[i] <= 0) { + // The first comes in from the left, the side the first turn starts from. + charge(S, S.charged ? 0 : 1); + S.charged = true; + S.slots[i] = 3 + Math.random() * 4; + } + } + if (input.btnPressed && input.btnPressed[0]) charge(S); + // Gannets come in volleys of two to four. + S.gTimer -= d * (0.6 + 0.4 * hunt); + if (S.gTimer <= 0) { + const m = 2 + floor(Math.random() * 3); + for (let i = 0; i < m; i++) dive(S, i * (0.2 + Math.random() * 0.3)); + S.gTimer = 4 + Math.random() * 4; + } + for (let i = S.queue.length - 1; i >= 0; i--) { + const q = S.queue[i]; + if (t < q.at) continue; + S.queue.splice(i, 1); + S.gannets.push({ x: q.x, y: -3, vx: (Math.random() * 2 - 1) * 14, vy: 100 + Math.random() * 30, up: false, last: 0 }); + } + + for (let i = S.hunters.length - 1; i >= 0; i--) { + const h = S.hunters[i]; + h.x += h.dir * h.sp * d; + h.y = h.y0 + 7 * sin(h.x * 0.035 + h.ph); + h.drop += d; + if (h.drop > 0.07) { + h.drop = 0; + S.holes.push({ x: h.x + h.dir * 5, y: h.y, r: 10.5, age: 0, life: 0.9 }); + } + if (h.x < -36 || h.x > W + 36) S.hunters.splice(i, 1); + } + for (let i = S.gannets.length - 1; i >= 0; i--) { + const g = S.gannets[i]; + const wet = g.y > 2; + g.x += g.vx * d; + g.y += g.vy * d; + if (!wet && g.y > 2) { + // The splash: a burst of air dragged under. + for (let b = 0; b < 16; b++) bubble(S, g.x + (Math.random() * 2 - 1) * 4, 3 + Math.random() * 8); + } + if (g.y > 2 && !g.up) { + g.vy *= exp(-1.5 * d); + g.vx *= exp(-1.5 * d); + if (Math.random() < 0.9) bubble(S, g.x - g.vx * 0.04, g.y - 3 - Math.random() * 3); + if (g.vy < 20) { + g.up = true; + g.vy = -32; + } + } + g.last += d; + if (g.y > 2 && g.last > 0.06) { + g.last = 0; + S.holes.push({ x: g.x, y: g.y, r: 4.5, age: 0, life: 0.9 }); + } + if (g.up && g.y < -4) S.gannets.splice(i, 1); + } + for (let i = S.bubbles.length - 1; i >= 0; i--) { + const b = S.bubbles[i]; + b.age += d; + b.y -= b.vy * d; + b.x += sin(b.age * 6 + b.ph) * 4 * d; + if (b.age > b.life || b.y < 1) S.bubbles.splice(i, 1); + } + for (let i = S.holes.length - 1; i >= 0; i--) { + S.holes[i].age += d; + if (S.holes[i].age > S.holes[i].life) S.holes.splice(i, 1); + } +} + +function bubble(S, x, y) { + if (S.bubbles.length < 400) S.bubbles.push({ x, y, vy: 9 + Math.random() * 12, age: 0, life: 1.2 + Math.random() * 1.6, ph: Math.random() * 6.283 }); +} + +function put(buf, x, y, r, g, b) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + buf[o] = r; + buf[o + 1] = g; + buf[o + 2] = b; +} + +export function draw(display, params) { + const S = params.s; + if (!S || S.cx === undefined) return; + const { buf } = S; + const t = S.tau; + const [wq, , , sun] = S.k; + // The sea: clear blue to coastal green, light near the surface and deep below. + const top = [mix(30, 50, wq), mix(118, 150, wq), mix(196, 120, wq)]; + const deep = [mix(4, 8, wq), mix(30, 52, wq), mix(84, 44, wq)]; + const day = 1 - 0.45 * sun; + // Rays bend into the water: 55 degrees off the vertical above it is 38 below, 82 is 48. + const slope = mix(0.78, 1.11, sun); + const rayS = mix(1, 0.3, sun) * day; + // Low sun reaches the school through more water, so the flash cools as it dims. + const silver = [mix(242, 200, sun), mix(249, 214, sun), mix(255, 232, sun)]; + const flash = mix(1, 0.7, sun); + const back = [mix(12, 10, wq), mix(28, 42, wq), mix(52, 40, wq)]; + const glow = [10 * flash, 16 * flash, 22 * flash]; + const { cx, cy, rx, ry } = S; + // The ball's outline, one radius for each of 128 directions, and the rays by where they leave the surface. + const { lt, rt, clear } = S; + for (let i = 0; i <= 128; i++) { + const th = -PI + (i * PI) / 64; + lt[i] = 1 + 0.08 * sin(3 * th + 0.45 * t) + 0.06 * sin(5 * th - 0.7 * t + 1) + 0.05 * sin(2 * th + 0.25 * t + 2); + } + const lump = (th) => lt[round(((th + PI) * 64) / PI)]; + for (let i = 0; i < rt.length; i++) { + const u = i / 2 - 150; + const ray = sin(u * 0.23 + t * 0.35) + 0.8 * sin(u * 0.41 - t * 0.27 + 1.3) + 0.6 * sin(u * 0.13 + t * 0.15 + 2) - 0.9; + rt[i] = ray > 0 ? ray : 0; + } + fronts(S); + // Where the hunters have just been, the ball is open and the sea shows through. + clear.fill(0); + for (const h of S.holes) { + const rr = (h.rr = h.r * (1 - h.age / h.life) ** 0.6); + for (let y = max(0, floor(h.y - rr)); y <= min(H - 1, h.y + rr); y++) + for (let x = max(0, floor(h.x - rr * 1.6)); x <= min(W - 1, h.x + rr * 1.6); x++) { + const dd = sqrt(((x - h.x) / 1.6) ** 2 + (y - h.y) ** 2); + const c = 1 - sstep(0.45 * rr, 0.8 * rr, dd); + if (c > clear[y * W + x]) clear[y * W + x] = c; + } + } + + for (let y = 0; y < H; y++) { + const dz = y / (H - 1); + const f = dz ** 0.8; + const wr = mix(top[0], deep[0], f) * day; + const wg = mix(top[1], deep[1], f) * day; + const wb = mix(top[2], deep[2], f) * day; + const fade = exp(-y / 60) * rayS; + for (let x = 0; x < W; x++) { + const o = (y * W + x) * 3; + // Sun rays: bright where the waves above focus the light, leaning with the sun. + const u = x - slope * y; + const ray = rt[((u + 150) * 2) | 0] * fade; + let r = wr + ray * 46; + let g = wg + ray * 60; + let b = wb + ray * 58; + const ddx = (x - cx) / rx; + const ddy = (y - cy) / ry; + const q0 = sqrt(ddx * ddx + ddy * ddy); + // The outline only matters near it: the lumps change the radius by a fifth at most. + const q = q0 < 0.7 || q0 > 1.8 ? q0 : q0 / lump(atan2(ddy, ddx)); + if (q < 1.5) { + // Inside the ball the fish shade the water; outside it, light thrown off their flanks. + const m0 = 1 - sstep(0.88, 1.05, q); + const c = clear[y * W + x]; + const m = m0 * (1 - c); + const s = turned(S, x, y, 0.5); + const sh = 1 - 0.9 * m; + const hl = (s + 0.6 * TG) * (1.6 * m + 0.6 * c * m0 + (1 - m0) * 3.4 * (1 - sstep(1, 1.5, q))); + r = r * sh + glow[0] * hl; + g = g * sh + glow[1] * hl; + b = b * sh + glow[2] * hl; + } + buf[o] = r; + buf[o + 1] = g; + buf[o + 2] = b; + } + } + + // The fish. + const { fu, fr, edge, lag, ph, v, off, holes } = S; + const tick = floor(t * 6); + for (let i = 0; i < fu.length; i++) { + const j = fr[i]; + let u = (fu[i] + off[j]) % SPAN; + if (u < 0) u += SPAN; + let x = cx + u - SPAN / 2; + if (x < -4 || x > W + 3) continue; + let y = cy + (j - (ROWS - 1) / 2) + 1.2 * sin(x * 0.085 + t * 0.6); + const ddx = (x - cx) / rx; + const ddy = (y - cy) / ry; + const q0 = sqrt(ddx * ddx + ddy * ddy); + if (q0 > 0.74 && q0 / lump(atan2(ddy, ddx)) > 0.93 + 0.1 * edge[i]) continue; + // Holes the hunters punch: fish inside are pushed to the rim and flash as they flee. + let startle = 0; + for (let h = 0; h < holes.length; h++) { + const H0 = holes[h]; + const rr = H0.rr; + const hy = y - H0.y; + if (hy > rr * 1.8 || hy < -rr * 1.8) continue; + const hx = (x - H0.x) / 1.6; + const d2 = hx * hx + hy * hy; + if (d2 > rr * rr * 3.2) continue; + const dd = sqrt(d2) + 1e-3; + if (dd < rr) { + const k = (rr - (rr - dd) * 0.3) / dd; + x = H0.x + hx * 1.6 * k; + y = H0.y + hy * k; + } + startle = max(startle, 1 - dd / (rr * 1.8)); + } + let s = turned(S, x, y, lag[i]); + let g = TG; + if (hash(i, tick) < 0.012) g = 1; + s = max(s, startle); + const dir = v[j] > 0 ? 1 : -1; + const len = s > 0.5 ? 3 : 2; + // Broadside the flanks shimmer as ripples run through the school. + const rip = 0.5 + 0.5 * sin(x * 0.27 - t * 2.6 + y * 0.08 + ph[i] * 0.3); + const lit = (1 - 0.06 * s * rip * rip * rip) * flash; + const xi = round(x); + const yi = round(y); + for (let p = 0; p < len; p++) { + const w = (p === 0 ? 1 : p === 1 ? 0.95 : 0.86) * lit; + const a = s * w; + const c = (1 - s) * (p === 0 ? 1 : 0.8); + put(buf, xi - dir * p, yi, back[0] * c + silver[0] * a + 90 * g, back[1] * c + silver[1] * a + 95 * g, back[2] * c + silver[2] * a + 100 * g); + } + } + + // The hunters, dark against the silver, drawn half as large again as their art. + for (const h of S.hunters) { + const art = h.kind ? DOLPHIN : SHARK; + const col = h.kind ? DOLPHIN_C : SHARK_C; + const wd = round(art[0].length * 1.5); + const ht = round(art.length * 1.5); + const x0 = round(h.x - wd / 2); + const y0 = round(h.y - ht / 2); + for (let j = 0; j < ht; j++) + for (let i = 0; i < wd; i++) { + const c = art[floor(j / 1.5)][floor((h.dir > 0 ? i : wd - 1 - i) / 1.5)]; + if (c === ".") continue; + const cc = col[c]; + put(buf, x0 + i, y0 + j, cc[0] * day, cc[1] * day, cc[2] * day); + } + } + for (const b of S.bubbles) { + const a = 1 - b.age / b.life; + const x = round(b.x); + const y = round(b.y); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const o = (y * W + x) * 3; + buf[o] = mix(buf[o], 210, a); + buf[o + 1] = mix(buf[o + 1], 240, a); + buf[o + 2] = mix(buf[o + 2], 255, a); + } + // Gannets: white darts with buff heads and black wingtips, folded for the dive. + for (const g of S.gannets) { + const sp = sqrt(g.vx * g.vx + g.vy * g.vy) + 1e-3; + const ux = g.vx / sp; + const uy = g.vy / sp; + for (let p = 0; p < 6; p++) { + const px = round(g.x - ux * p); + const py = round(g.y - uy * p); + if (p === 0) put(buf, px, py, 250, 228, 150); + else if (p < 4) { + put(buf, px, py, 250, 250, 250); + if (p === 2) put(buf, px + 1, py, 225, 228, 232); + } else put(buf, px, py, 26, 26, 30); + } + } + // The underside of the surface, bright and broken by the waves. + for (let y = 0; y < 3; y++) + for (let x = 0; x < W; x++) { + const a = [0.85, 0.5, 0.2][y] * (0.7 + 0.3 * sin(x * 0.5 + t * 2.1) * sin(x * 0.23 - t * 1.3)); + const o = (y * W + x) * 3; + buf[o] = mix(buf[o], 200 * day, a); + buf[o + 1] = mix(buf[o + 1], 232 * day, a); + buf[o + 2] = mix(buf[o + 2], 248 * day, a); + } + + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const o = (y * W + x) * 3; + display.setPixel(x, y, min(255, buf[o]), min(255, buf[o + 1]), min(255, buf[o + 2])); + } +} From 7915c2723c2be14b51e3b46a2d5e985d3fe9439f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 00:02:49 +0000 Subject: [PATCH 07/15] =?UTF-8?q?High=20Dive=20=ED=8C=A8=ED=84=B4=EC=9D=84?= =?UTF-8?q?=20=EC=A7=A0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GhRE8HZxGnk63uCfeHdr92 --- incoming/pattern-study/patterns/high-dive.js | 557 +++++++++++++++++++ 1 file changed, 557 insertions(+) create mode 100644 incoming/pattern-study/patterns/high-dive.js diff --git a/incoming/pattern-study/patterns/high-dive.js b/incoming/pattern-study/patterns/high-dive.js new file mode 100644 index 000000000..bdafaf6b3 --- /dev/null +++ b/incoming/pattern-study/patterns/high-dive.js @@ -0,0 +1,557 @@ +// ===== Patternflow pattern ===== +// Title: High Dive +// Family: sport +// Idea: A diving hall at night seen from the side: divers climb a concrete tower and somersault ten metres in a second and a half into a pool whose rippling light plays over the walls while the scoreboard posts the marks. +// Author: patternflow-lab +// Date: 2026-10-23 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 23, after the 10 m platform event in competition diving +// Round: 23 +// =============================== +// @matrix 64x128 +// @knobs Pool=0..1, Speed=0.2..3, Dive=0..4, Lights=0..1 +// +// Knob 1: Pool. The colour of the underwater lights, aqua at 0 and once round the wheel; the pool throws it onto the walls. +// Knob 2: Speed. Real time at 2, where a dive takes about 1.6 s and a diver starts up the stairs every 6 s; ten times slower at 0.2. +// Knob 3: Dive. The next dive, from a plain forward dive through forward somersaults of 1.5 up to 3.5 to 2.5 with two twists. Harder ones jump higher and take longer to set up. The scoreboard shows the dive and its difficulty. +// Knob 4: Lights. From a dim night practice with only the tower lamps on to full competition lights. +// Buttons: K1 tap sends the waiting diver off now, or hurries the one on the stairs. + +const W = 64; +const H = 128; +const N = W * H; +// The still waterline and the pool floor; the pool's end wall stands under the tower. +const WL = 86; +const POOL_X = 14; +const FLOOR = 123; +// LEDs per metre, and gravity in LEDs per second squared. +const PX = 7; +const G = 9.81 * PX; +// The top row of each platform (10 m, 7.5 m, 5 m, 3 m) and where it ends. +const PLAT = [16, 33, 51, 65]; +const PLAT_END = [26, 21, 23, 19]; +// Where a diver's hip stands at the edge of the 10 m platform. +const EDGE = 24; +const HIP = 5.4; +const INTERVAL = 6; +const [CLIMB, WALK, STAND, FLY, UNDER, SWIM] = [0, 1, 2, 3, 4, 5]; + +// Number, difficulty in tenths, somersaults, position (2 is a tuck, 1 a pike), +// twists, take-off speed in m/s, seconds the diver stands first. +const DIVES = [ + ["101A", 16, 0.5, 0, 0, 1.6, 0.5], + ["103B", 20, 1.5, 1, 0, 2.4, 1.1], + ["105B", 30, 2.5, 1, 0, 3.0, 1.6], + ["107C", 37, 3.5, 2, 0, 3.5, 2.1], + ["5154B", 36, 2.5, 1, 2, 3.2, 2.5], +]; +const SUITS = [[255, 40, 60], [40, 110, 255], [255, 215, 30], [30, 220, 120], [220, 60, 255]]; +const SKIN = [245, 180, 140]; + +const { min, max, floor, ceil, round, sin, cos, abs, sqrt, exp, hypot, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (v) => ((v = cl(v, 0, 1)), v * v * (3 - 2 * v)); +const rnd = () => Math.random(); +const F = (n) => new Float32Array(n); +// A fixed hash, so the window lights and the glints repeat on every run. +const hash = (a, b) => ((Math.imul(a * 7919 + b, 2654435761) >>> 8) & 1023) / 1024; +const hsv = (h, s, v) => [5, 3, 1].map((n) => v - v * s * max(0, min((n + h * 6) % 6, 4 - ((n + h * 6) % 6), 1))); + +// The climb, as feet positions: in along the deck, then up the flights that +// zigzag across the stairwell to the top of the tower. +const PATH = [[-6, 83.5], [11, 83.5], [3, 74], [11, 64.5], [3, 57.5], [11, 50.5], [3, 41.5], [11, 32.5], [3, 24], [11, 15.5]]; +const SEGL = []; +let PLEN = 0; +for (let j = 1; j < PATH.length; j++) { + SEGL.push(hypot(PATH[j][0] - PATH[j - 1][0], PATH[j][1] - PATH[j - 1][1])); + PLEN += SEGL[j - 1]; +} +function along(p, out) { + let j = 0; + while (j < SEGL.length - 1 && p > SEGL[j]) p -= SEGL[j++]; + const u = cl(p / SEGL[j], 0, 1); + const a = PATH[j]; + const b = PATH[j + 1]; + out[0] = a[0] + (b[0] - a[0]) * u; out[1] = a[1] + (b[1] - a[1]) * u; + out[2] = b[0] >= a[0] ? 1 : -1; out[3] = j > 0 ? 1 : 0; +} + +// The scoreboard's letters, three wide and five tall, one bit an LED. +const FONT = { 0: 0x7b6f, 1: 0x2c97, 2: 0x73e7, 3: 0x73cf, 4: 0x5bc9, 5: 0x79cf, 6: 0x79ef, 7: 0x7249, 8: 0x7bef, 9: 0x7bcf, A: 0x2bed, B: 0x6bae, C: 0x3923, D: 0x6b6e, M: 0x5fed, ".": 4 }; + +// The hall, built once: what each LED's surface is made of and how much +// light the lamps and the pool throw on it. +function build(S) { + const [alb, em] = [F(N * 3), F(N * 3)]; + const [lt, lh, rf, cone, wb, wl] = [F(N), F(N), F(N), F(N), F(N), F(N)]; + const lamp = (x, y, lx) => exp(-((x - lx) ** 2) / 300 - (y - 1) ** 2 / 5200); + const glow = (x, y, s) => exp(-((x - 28) ** 2 + (y - 106) ** 2) / s) + exp(-((x - 52) ** 2 + (y - 106) ** 2) / s); + const flights = []; + for (let j = 1; j < PATH.length - 1; j++) flights.push([PATH[j], PATH[j + 1]]); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x; + // Back wall: painted block in panels, catching the pool's light. + let a = x % 8 ? [0.42, 0.44, 0.5] : [0.36, 0.37, 0.42]; + let r = 1; + let plat = -1; + for (let p = 0; p < 4; p++) if (y >= PLAT[p] && y <= PLAT[p] + 2 && x <= PLAT_END[p]) plat = p; + if (y <= 1) { + a = [0.18, 0.18, 0.2]; r = 0; + } else if (x >= 33 && y >= 3 && y <= 33) { + // The scoreboard. + a = x === 33 || x === 63 || y === 3 || y === 33 ? [0.5, 0.5, 0.52] : [0.03, 0.03, 0.035]; + r = 0; + } else if (y >= 9 && y < PLAT[0] && x <= 12 && (x % 6 === 0 || y === 9 || y === 12)) { + a = [0.7, 0.74, 0.8]; r = 0.5; + } else if (plat >= 0) { + const row = y - PLAT[plat]; + const v = x === PLAT_END[plat] ? 0.75 : row === 0 ? 0.92 : row === 1 ? 0.62 : 0.4; + a = [v, v * 0.97, v * 0.92]; r = row === 2 ? 1.6 : 0.8; + } else if (x <= 12 && y > PLAT[0] && y < 84) { + // The tower: two concrete legs and the stairwell between them. + if (x <= 1 || x === 12) { + const v = 0.62 - 0.06 * (y % 5 === 0); + a = [v, v * 0.97, v * 0.9]; + } else { + a = [0.24, 0.24, 0.26]; + for (const [p, q] of flights) { + const lo = min(p[0], q[0]); + const hi = max(p[0], q[0]); + if (x < lo - 1 || x > hi + 1) continue; + const xs = cl(x, lo, hi); + const sl = (q[1] - p[1]) / (q[0] - p[0]); + const xq = p[0] + floor((xs - p[0]) / 2 + (q[0] > p[0] ? 0 : 0.99)) * 2; + const yf = p[1] + sl * (cl(xq, lo, hi) - p[0]); + if (y >= ceil(yf) && y <= ceil(yf) + 1) a = [0.74, 0.72, 0.66]; + else if (abs(y - p[1] + 5.5 - sl * (xs - p[0])) < 0.5 && x >= lo && x <= hi) a = [0.4, 0.4, 0.42]; + } + r = 0.6; + } + } else if (y >= 84 && y <= 86 && x < POOL_X) { + a = [0.8, 0.8, 0.78]; r = 0.7; + } else if (y > 86 && x < POOL_X) { + // The ground under the deck, hatched like the cut concrete of a section drawing. + a = x === POOL_X - 1 ? [0.6, 0.65, 0.66] : [0.2, 0.2, 0.21]; + r = 0; + if (x < POOL_X - 1 && (x + y) % 4 === 0) em.fill(30, i * 3, i * 3 + 3); + } else if (y >= 84) { + // The gutter on the far side, and the far wall when a wave dips. + a = y < 86 ? [0.78, 0.8, 0.82] : [0.6, 0.66, 0.7]; + r = 0.6; + } else if (x >= 31 && x <= 61 && y >= 39 && y <= 63) { + // Tall windows onto the night. + if ((x - 31) % 10 === 0 || (y - 39) % 12 === 0) { + a = [0.33, 0.34, 0.37]; r = 0.6; + } else { + a = [0.06, 0.07, 0.09]; r = 0.35; + const g = (y - 39) / 24; + const c = y > 54 && hash(x, y) < 0.1; + em.set([6 + 8 * g + 120 * c, 14 + 14 * g + 90 * c, 38 + 26 * g + 40 * c], i * 3); + } + } + // Light reflected up from the pool fades with height above the water. + rf[i] = r * exp(-max(0, WL - y) / 30); alb.set(a, i * 3); + lt[i] = lamp(x, y, 6) + lamp(x, y, 22); lh[i] = lamp(x, y, 40) + lamp(x, y, 56) + 0.3; + // The platform lamp: a cone of hazy light from the ceiling onto the 10 m platform. + if (y >= 3 && y <= PLAT[0]) { + const e = y - 2; + cone[i] = sm((x - 21 + e * 0.85) / 2) * sm((23 + e * 0.42 - x) / 2) * (1 - 0.45 * (e / 14)); + } + // Underwater: light fading with depth, and the tiled far wall with its two lamps. + if (x >= POOL_X && y >= WL - 2) { + const d = y - WL; + let v = 0.34 + 0.42 * exp(-max(0, d) / 13); + if ((x - POOL_X) % 5 === 0 || (y - WL) % 5 === 4) v *= 0.82; + if (y >= FLOOR) { + // The floor, with the dark lane lines running away from us. + v = 0.62 - 0.06 * (y - FLOOR); + const dx = x - 21 - floor((x - 21) / 14) * 14 - ((y - FLOOR) * (x - 38)) / 40; + if (x >= 21 && abs(dx) < 1.2) v = 0.16; + } + wb[i] = v; + wl[i] = 1.1 * glow(x, y, 3) + 0.22 * glow(x, y, 50); + } + } + Object.assign(S, { alb, lt, lh, rf, em, cone, wb, wl }); +} + +function newDiver(S, st) { + S.num = (S.num % 9) + 1; + const suit = SUITS[floor(rnd() * 5) % 5]; + return { st, p: 0, x: 0, t: 0, num: S.num, suit, wait: rnd() * 0.4, hx: -20, face: 1, q: [0, 0, 0, 0, 0, PI, PI, 0] }; +} + +export function setup(params) { + const S = { t: 0, divers: [], drops: [], bubs: [], num: 0, spawn: 0.5, spot: 1, surge: 0.1, foam: -9, foamX: 0, hue: 0.49, dive: 0, lights: 0.06 }; + Object.assign(S, { h: F(W), hv: F(W), sy: new Int16Array(W), sh: F(W), C: F(N * 3) }); + params.s = S; build(S); + const a = newDiver(S, STAND); + a.x = EDGE; a.wait = 0; S.divers.push(a); + S.board = { num: a.num, suit: a.suit, sc: null, reveal: -1 }; +} + +// Poses are [body angle, hip and knee of each leg, each arm, twist]. +function walkPose(q, ph, stair) { + const s = sin(ph); + const lift = 0.35 * stair; + q.splice(0, 8, 0.08 * stair, 0.45 * s + lift * max(0, s), 0.5 * max(0, s) + 0.3 * stair, -0.45 * s + lift * max(0, -s), 0.5 * max(0, -s) + 0.3 * stair, PI - 0.4 * s, PI + 0.4 * s, 0); +} + +function flyPose(q, D, u) { + const [, , turns, pos, twist] = D; + const Th = turns * TAU; + let th, arm; + let s = 0; + let tw = 0; + if (turns < 1) { + // A plain forward dive: arms out like a swallow, closing overhead for the entry. + th = Th * min(1, u / 0.9); + arm = 1.5 * sm(u / 0.2) * (1 - sm((u - 0.55) / 0.25)); + } else { + // Fold up, spin fast, open out at about three quarters of the fall and line up. + const uo = 0.72; + th = u < uo ? (Th - 0.5) * (u / uo) ** 1.15 : Th - 0.5 + 0.5 * sm((u - uo) / (0.92 - uo)); + s = sm(u / 0.1) * (1 - sm((u - uo + 0.03) / 0.08)); + if (twist) { + tw = twist * TAU * sm((u - 0.05) / 0.33); s *= sm((u - 0.38) / 0.06); + } + arm = s * (pos === 2 ? 1.9 : 2.5); + if (twist) arm = max(arm, PI * 0.9 * sm(u / 0.06) * (1 - sm((u - 0.36) / 0.06))); + } + const hip = s * (pos === 2 ? 2.3 : 2.75); + const knee = pos === 2 ? s * 2.6 : 0; + q.splice(0, 8, th, hip, knee, hip, knee, arm, arm, tw); +} + +function takeOff(S, d) { + const D = DIVES[S.dive]; + Object.assign(d, { dv: D, st: FLY, t: 0, x0: d.hx, y0: d.hy, vx: D[2] < 1 ? 6 : 7.5, vy: D[5] * PX }); + // Time until the hands reach the water, with the body straight and head down. + d.T = (d.vy + sqrt(d.vy * d.vy + 2 * G * (WL - 8.6 - d.y0))) / G; + [S.board.code, S.board.dd] = D; +} + +function enter(S, d) { + const ex = round(d.hx); + for (let j = -2; j <= 2; j++) S.hv[cl(ex + j, POOL_X, W - 1)] += 30 * (1 - abs(j) * 0.25); + // A rip throws up only a few drops and drags air down the hole. + for (let j = 0; j < 12; j++) S.drops.push({ x: ex + (rnd() - 0.5) * 3, y: WL - 1, vx: (rnd() - 0.5) * 16, vy: -(14 + rnd() * 30) }); + for (let j = 0; j < 4; j++) S.drops.push({ x: ex + rnd() - 0.5, y: WL - 1, vx: (rnd() - 0.5) * 3, vy: -(30 + rnd() * 14) }); + for (let j = 0; j < 40; j++) bub(S, ex + (rnd() - 0.5) * 4, WL + 1 + rnd() * 8, 6 + rnd() * 16, 1 + rnd() * 2.2, 0.7 + 0.3 * rnd()); + S.foam = S.t; S.foamX = ex; + Object.assign(d, { st: UNDER, t: 0, xE: d.hx, yE: d.hy, vE: G * d.T - d.vy }); + S.board.reveal = 0.7; +} + +// Seven judges; the two highest and two lowest drop, the middle three times the difficulty. +function score(S) { + const b = S.board; + const q = 6.4 + rnd() * 2.7; + const sc = []; + for (let j = 0; j < 7; j++) sc.push(cl(round(q + (rnd() - 0.5) * 1.8), 3, 9)); + const order = [0, 1, 2, 3, 4, 5, 6].sort((i, j) => sc[i] - sc[j] || i - j); + b.drop = order.slice(0, 2).concat(order.slice(5)); + const v = (sc[order[2]] + sc[order[3]] + sc[order[4]]) * b.dd; + b.total = `${floor(v / 10)}.${v % 10}0`; + b.sc = sc; +} + +const onPlat = (v) => v.st === WALK || v.st === STAND; +const bub = (S, x, y, vy, life, b) => S.bubs.push({ x, y, vy, life, b }); + +// K1: whoever is on the platform goes now; failing that, the climber hurries. +function go(S) { + const d = S.divers.find(onPlat) || S.divers.find((v) => v.st === CLIMB); + if (d) d.hurry = d.go = 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 = cl(dt || 0, 0, 0.1); S.hue = (0.49 + cl(k[0], 0, 1)) % 1; S.dive = cl(round(k[2]), 0, 4); + S.lights = cl(k[3], 0, 1); + const st = (dt * cl(k[1], 0.2, 3)) / 2; + S.t += st; + if (input.btnPressed && input.btnPressed[0]) go(S); + + S.spawn -= st; + if (S.spawn <= 0 && S.divers.length < 7) { + S.spawn += INTERVAL; S.divers.push(newDiver(S, CLIMB)); + } + const busy = S.divers.some(onPlat); + const surf = (x) => WL + S.h[cl(round(x), POOL_X, W - 1)]; + const flying = S.divers.some((v) => v.st === FLY); + let lim = PLEN; + const pt = [0, 0, 0, 0]; + const top = PLAT[0] - 0.5 - HIP; + for (const d of S.divers) { + const q = d.q; + if (d.st === CLIMB) { + // Climbers keep a flight apart and wait at the top while the platform is taken. + d.p = min(d.p + st * 24 * (d.hurry ? 3 : 1), lim); + lim = d.p - 16; along(d.p, pt); d.hx = pt[0]; d.hy = pt[1] - HIP; d.face = pt[2]; + walkPose(q, d.p * 0.55, pt[3]); + if (d.p >= PLEN && !busy) { + d.st = WALK; d.x = 11; + Object.assign(S.board, { num: d.num, suit: d.suit, sc: null, reveal: -1 }); + } + } else if (d.st === WALK) { + d.hx = d.x = min(EDGE, d.x + st * 14 * (d.hurry ? 2.5 : 1)); + d.hy = top; d.face = 1; walkPose(q, d.x * 0.6, 0); + if (d.x >= EDGE) { + d.st = STAND; d.t = 0; + } + } else if (d.st === STAND) { + d.t += st; + const wait = DIVES[S.dive][6] + d.wait; + d.hx = EDGE; d.hy = top; walkPose(q, 0, 0); + // Arms come up from the sides over the last moment before take-off. + q[5] = q[6] = PI * (1 - (d.go ? 1 : sm((d.t - wait + 0.6) / 0.45))); + if (d.go || (d.t >= wait && !flying)) takeOff(S, d); + } else if (d.st === FLY) { + d.t += st; d.hx = d.x0 + d.vx * d.t; d.hy = d.y0 - d.vy * d.t + 0.5 * G * d.t * d.t; + flyPose(q, d.dv, min(1, d.t / d.T)); + if (d.t >= d.T) enter(S, d); + } else if (d.st === UNDER) { + d.t += st; q[1] = q[2] = q[3] = q[4] = q[7] = 0; + if (d.t < 0.5) { + // Straight down the hole it punched, slowing fast and trailing air. + d.yD = d.hy = d.yE + d.vE * 0.22 * (1 - exp(-d.t / 0.22)); + d.hx = d.xE; q[0] = PI; q[5] = q[6] = 0; + for (let j = 0; j < st * 260; j++) bub(S, d.hx + (rnd() - 0.5) * 3, WL + 1 + rnd() * (d.hy + 6 - WL), 8 + rnd() * 20, 1.4 + rnd() * 2, 0.5 + rnd() / 2); + } else { + // Turn up and rise to the surface, drifting away from the tower. + const u = cl((d.t - 0.5) / 1.1, 0, 1); + d.hy = d.yD + (WL + 0.8 - d.yD) * sm(u); d.hx += st * 5; + q[0] = PI - 0.75 * PI * sm(u * 1.5) + 0.25 * PI * sm((u - 0.7) / 0.3); + q[5] = q[6] = 0.3 + 2 * sm(u); + if (u >= 1) { + d.st = SWIM; d.t = 0; + } + } + } else if (d.st === SWIM) { + // Front crawl to the far side, the arms wheeling over the surface. + d.t += st; d.hx += st * 10; + d.hy = surf(d.hx) + 0.8; + const kick = 0.25 * sin(d.t * 12); + q.splice(0, 8, PI / 2, kick, 0.15, -kick, 0.15, d.t * 5.5, d.t * 5.5 + PI, 0); + const fx = cl(round(d.hx - 6), POOL_X, W - 1); + S.hv[fx] += st * 22; + if (rnd() < st * 4.5) S.drops.push({ x: fx, y: WL - 1, vx: (rnd() - 0.5) * 6, vy: -8 - rnd() * 10 }); + } + } + S.divers = S.divers.filter((d) => d.st !== SWIM || d.hx < 74); + + // The platform lamp is up while someone is out on the 10 m platform. + S.spot += (+S.divers.some(onPlat) - S.spot) * min(1, st * 2.2); + const b = S.board; + if (b.reveal > 0 && (b.reveal -= st) <= 0) score(S); + + // The surface: a damped string of water heights, kicked by the sparger + // under the landing area and by every entry. + const { h, hv } = S; + const kicks = round(st * 70 + rnd() * 0.5); + for (let j = 0; j < kicks; j++) hv[22 + floor(rnd() * 26)] += (rnd() - 0.5) * 9; + for (let s = 0; s < 3; s++) { + const q = st / 3; + for (let x = POOL_X; x < W; x++) { + const l = x > POOL_X ? h[x - 1] : h[x]; + const r = x < W - 1 ? h[x + 1] : h[x]; + hv[x] += (160 * (l + r - 2 * h[x]) - 22 * h[x] - 1.6 * hv[x]) * q; + } + for (let x = POOL_X; x < W; x++) h[x] = cl(h[x] + hv[x] * q, -3, 3); + } + let e = 0; + for (let x = POOL_X; x < W; x++) e += h[x] * h[x]; + const target = min(1.3, sqrt(e / 40)); + S.surge += (target - S.surge) * min(1, st * (target > S.surge ? 6 : 0.9)); + + // Sparger bubbles from the floor, and the entry bubbles rising. + const ns = st * 38 + rnd(); + for (let j = 1; j < ns; j++) bub(S, 22 + rnd() * 26, FLOOR - 1, 26 + rnd() * 16, 3, 0.28 + 0.2 * rnd()); + for (const u of S.bubs) { + u.y -= u.vy * st; u.vy = min(u.vy + st * 6, 40); u.x += sin(u.y * 0.5 + u.b * 20) * 0.25; + u.life -= st; + } + S.bubs = S.bubs.filter((u) => u.life > 0 && u.y > surf(u.x) + 0.5); + for (const p of S.drops) { + p.vy += G * st; p.x += p.vx * st; p.y += p.vy * st; + } + S.drops = S.drops.filter((p) => p.vy < 0 || p.y < surf(p.x)); +} + +// One figure, its hip at (hx, hy). q[0] turns it forward as it somersaults: +// 0 stands upright facing `face`. +const PART = new Float32Array(80); +function figure(S, hx, hy, face, q, suit, lf, pc) { + const { C, sy } = S; + const ct = cos(q[7]); + const ux = sin(q[0]) * face; + const uy = -cos(q[0]); + const fx = cos(q[0]) * face * ct; + const fy = sin(q[0]) * ct; + const nx = hx + ux * 3.8; + const ny = hy + uy * 3.8; + let n = 0; + const part = (ax, ay, bx, by, r, c) => { + PART.set([ax, ay, bx, by, r, c], n); + n += 6; + }; + const limb = (x, y, a, l) => [x + l * (ux * cos(a) + fx * sin(a)), y + l * (uy * cos(a) + fy * sin(a))]; + // The far leg and arm go down first, dimmer, so the near ones cover them. + for (let side = 0; side < 2; side++) { + const a1 = PI - q[1 + side * 2]; + const [kx, ky] = limb(hx, hy, a1, 2.7); + const [ax, ay] = limb(kx, ky, a1 + q[2 + side * 2], 2.7); + const [wx, wy] = limb(nx, ny, q[5 + side], 3.4); + const c = side ? 0 : 2; + if (side) { + part(hx, hy, nx, ny, 0.95, 1); + part(hx + ux * 5.1, hy + uy * 5.1, hx + ux * 5.1, hy + uy * 5.1, 1.15, 0); + } + part(hx, hy, kx, ky, 0.75, c); + part(kx, ky, ax, ay, 0.6, c); + part(nx, ny, wx, wy, 0.5, c); + } + // Nothing reaches further than 8 LEDs from the hip. + for (let y = max(0, floor(hy - 9)); y <= min(H - 1, hy + 9); y++) + for (let x = max(0, floor(hx - 9)); x <= min(W - 1, hx + 9); x++) { + const i = (y * W + x) * 3; + const px = x + 0.5; + const py = y + 0.5; + // Seen through the water a body is tinted and softer. + const wet = x >= POOL_X && y > sy[x]; + for (let j = 0; j < n; j += 6) { + const ax = PART[j]; + const ay = PART[j + 1]; + const dx = PART[j + 2] - ax; + const dy = PART[j + 3] - ay; + const tt = cl(((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy + 1e-6), 0, 1); + const a = cl(PART[j + 4] + 0.5 - sqrt((px - ax - dx * tt) ** 2 + (py - ay - dy * tt) ** 2), 0, 1); + if (a <= 0) continue; + const src = PART[j + 5] === 1 ? suit : SKIN; + const m = lf * (PART[j + 5] === 2 ? 0.62 : 1); + for (let c = 0; c < 3; c++) { + const v = wet ? src[c] * m * 0.45 + pc[c] * 70 : src[c] * m; + C[i + c] += (v - C[i + c]) * a; + } + } + } +} + +const put = (S, i, col) => S.C.set(col, i * 3); +function text(S, s, x, y, col) { + for (const ch of s) { + for (let j = 0; j < 15; j++) if ((FONT[ch] >> (14 - j)) & 1) put(S, (y + floor(j / 3)) * W + x + (j % 3), col); + x += ch === "." ? 2 : 4; + } +} +const width = (s) => s.length * 4 - 1 - (s.includes(".") ? 2 : 0); +// Lamp light, warm white. +const WARM = [1, 0.93, 0.8]; + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { C, alb, lt, lh, rf, em, cone, wb, wl, h, sy, sh, spot } = S; + const pc = hsv(S.hue, 0.78, 1); + const k4 = S.lights; + const t = S.t; + const amb = 0.06 + 0.2 * k4; + const wT = 0.55 + 0.45 * k4 + 0.25 * spot; + const wH = 0.1 + 0.9 * k4; + // Stirred water throws brighter light on the walls and stronger shafts below. + const A = (0.3 + 1.7 * S.surge) * 0.9 * 255; + const sw = 0.25 + 0.5 * S.surge; + for (let x = 0; x < W; x++) { + sy[x] = x >= POOL_X ? round(WL + h[x]) : 999; + const c = x > POOL_X && x < W - 1 ? h[x - 1] + h[x + 1] - 2 * h[x] : 0; + sh[x] = sin(x * 0.5 + t * 1.1 + 2 * h[x]) * sin(x * 0.19 - t * 0.7) - 2.5 * c; + } + const foamA = max(0, 1 - (t - S.foam) / 0.8); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x; + const j = i * 3; + if (y < sy[x]) { + // The hall, lit by the lamps and by the pool's rippling light. + const light = (amb + wT * lt[i] + wH * lh[i]) * 153 + spot * cone[i] * 120; + let cs = 0; + if (rf[i] > 0.02) { + const xc = cl((x + (WL - y) * 0.3) | 0, POOL_X, W - 1); + let q1 = 1 - abs(sin(x * 0.62 + 1.7 * sin(y * 0.29 + t * 0.8) + t * 0.9 + 1.8 * h[xc])); + let q2 = 1 - abs(sin(y * 0.5 - x * 0.2 + 1.5 * sin(x * 0.33 - t * 1.05) - t * 0.65)); + q1 *= q1; + q2 *= q2; + cs = (q1 * q1 * q1 + q2 * q2 * q2) * rf[i] * A; + } + const hz = spot * cone[i] * (40 + 30 * (1 - k4)); + for (let c = 0; c < 3; c++) C[j + c] = alb[j + c] * (light * WARM[c] + pc[c] * cs) + em[j + c] + hz * WARM[c]; + } else if (y === sy[x]) { + // The surface line, glinting where it tilts and where the sparger froths. + let gl = 0.55 + 0.45 * k4 + min(1, abs((x < W - 1 ? h[x + 1] : h[x]) - h[x - 1]) * 1.2); + if (x >= 22 && x < 48 && hash(x, floor(t * 9)) < 0.18) gl += 0.6; + gl += 1.5 * foamA * max(0, 1 - abs(x - S.foamX) / 4); + for (let c = 0; c < 3; c++) C[j + c] = (pc[c] * 0.5 + 0.5) * 122 * gl; + } else { + const d = y - WL; + let v = wb[i] * 255; + if (d < 34 && y < FLOOR) v *= 1 + sw * sh[cl(round(x - d * 0.45), 0, W - 1)] * exp(-d / 20); + const top = k4 * 30 * exp(-max(0, d) / 10); + for (let c = 0; c < 3; c++) C[j + c] = pc[c] * v + wl[i] * 255 * (0.5 + 0.5 * pc[c]) + top; + } + } + + // Bubbles catch the underwater light. + for (const u of S.bubs) { + const x = round(u.x); + const y = round(u.y); + if (x < POOL_X || x >= W || y <= sy[x] || y >= H) continue; + const v = u.b * min(1, u.life * 1.5) * 170; + for (let c = 0; c < 3; c++) C[(y * W + x) * 3 + c] += v * (0.55 + 0.45 * pc[c]); + } + + // The lamps along the ceiling: the two over the tower are on at night. + for (const [lx, w] of [[6, wT], [22, wT], [40, wH], [56, wH]]) + for (let y = 0; y < 5; y++) + for (let x = lx - 4; x <= lx + 4; x++) { + const core = abs(x - lx) <= 1 && y >= 1 && y <= 2; + if (!core && x >= 33 && y >= 3) continue; + const v = core ? 120 + 135 * min(1, w) : 90 * min(1, w) * exp(-((x - lx) ** 2 + (y - 1.5) ** 2) / 3); + for (let c = 0; c < 3; c++) C[(y * W + x) * 3 + c] = max(C[(y * W + x) * 3 + c], v * WARM[c]); + } + + // The scoreboard shows the dive and its difficulty, then the seven judges' marks. + const bd = S.board; + const [code, dd] = bd.sc ? [bd.code, bd.dd] : DIVES[S.dive]; + const amber = [255, 170, 40]; + const white = [235, 235, 225]; + text(S, String(bd.num), 35, 6, amber); + text(S, code, 62 - width(code), 6, white); + for (let j = 0; j < 15; j++) put(S, (13 + floor(j / 3)) * W + 35 + (j % 3), bd.suit); + text(S, "10M", 40, 13, [150, 150, 150]); + const dds = `${floor(dd / 10)}.${dd % 10}`; + text(S, dds, 62 - width(dds), 13, amber); + if (bd.sc) { + for (let j = 0; j < 7; j++) text(S, String(bd.sc[j]), 35 + j * 4, 20, bd.drop.includes(j) ? [80, 80, 80] : amber); + text(S, bd.total, 62 - width(bd.total), 27, white); + } + + const lf = 0.62 + 0.38 * k4; + for (const d of S.divers) { + const lit = d.st === WALK || d.st === STAND || (d.st === FLY && d.t < 0.4); + figure(S, d.hx, d.hy, d.face, d.q, d.suit, min(1.1, lf + (lit ? 0.35 * spot : 0)), pc); + } + + // Spray above the water. + for (const p of S.drops) { + const x = round(p.x); + const y = round(p.y); + if (x >= 0 && x < W && y >= 0 && y < H) for (let c = 0; c < 3; c++) C[(y * W + x) * 3 + c] = max(C[(y * W + x) * 3 + c], 210 + 40 * pc[c]); + } + + for (let i = 0; i < N; i++) display.setPixel(i % W, (i / W) | 0, min(255, C[i * 3]), min(255, C[i * 3 + 1]), min(255, C[i * 3 + 2])); +} From 1d076f7257768ecc73a60338b5be8a742b506ada Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 00:05:23 +0000 Subject: [PATCH 08/15] =?UTF-8?q?Proscenium=20=ED=8C=A8=ED=84=B4=EC=9D=84?= =?UTF-8?q?=20=EC=A7=A0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GhRE8HZxGnk63uCfeHdr92 --- incoming/pattern-study/patterns/proscenium.js | 513 ++++++++++++++++++ 1 file changed, 513 insertions(+) create mode 100644 incoming/pattern-study/patterns/proscenium.js diff --git a/incoming/pattern-study/patterns/proscenium.js b/incoming/pattern-study/patterns/proscenium.js new file mode 100644 index 000000000..07fc69704 --- /dev/null +++ b/incoming/pattern-study/patterns/proscenium.js @@ -0,0 +1,513 @@ +// ===== Patternflow pattern ===== +// Title: Proscenium +// Family: stage +// Idea: A theatre from the stalls: under a gilded arch the red house curtain rises on a painted backcloth, lamps above cut shafts through the haze, a dancer crosses in a follow spot, and the curtain falls for her bow before the next scene. +// Author: patternflow-lab +// Date: 2026-10-23 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 23, after the proscenium stage of a nineteenth-century opera house +// Round: 23 +// =============================== +// @matrix 64x128 +// @knobs Gel=0..1, Speed=0.2..3, Scene=0..3, Haze=0..1 +// +// Knob 1: Gel. The colour of the stage wash and the beams: steel blue at 0, turning through violet and rose to amber in the middle, then through green back to blue. +// Knob 2: Speed. How fast the show runs: at 2 the curtain takes 2.8 s to rise and a whole round of rise, dance, fall and bow is 12 s; at 0.2 it all runs ten times slower. +// Knob 3: Scene. The backdrop the curtain rises on: a moonlit sea, a forest, a night city or a snowfall, the others following in that order. Turning it brings the curtain in and out again on the new one. +// Knob 4: Haze. How much haze hangs in the air. At 0 the beams are thin shafts; turned up they swell and the whole stage glows in the gel. +// Buttons: K1 tap brings the curtain in now, or takes it out again if it is in. + + +const W = 64; +const H = 128; +const N = W * H; +// Inside the arch: columns XL..XR, rows from TOP down to the stage lip. +const [XL, XR, TOP, TRUSS, SKY, LENS, BACK, CURT, LIP] = [4, 59, 8, 11, 15, 17, 78, 96, 100]; +// The phases of a round, in seconds at Speed 2. +const [TRISE, TSHOW, TFALL, TBOW, THOLD] = [2.8, 4, 1.8, 3.2, 0.6]; +const LAMPS = [10, 19, 27, 36, 44, 53]; +const AIM = [34, 46, 15, 49, 20, 31]; +const { min, max, sqrt, sin, cos, exp, abs, round, floor, hypot, PI } = Math; +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 hash = (i) => { + let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; +// Smooth value noise, for the forest's leaves and bark. +function vnoise(x, y) { + const xi = floor(x), yi = floor(y); + const u = x - xi, v = y - yi; + const su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v); + const h = (a, b) => hash(a * 157 + b * 7919 + 13); + const a = h(xi, yi), b = h(xi + 1, yi), c = h(xi, yi + 1), d = h(xi + 1, yi + 1); + return a + (b - a) * su + (c - a) * sv + (a - b - c + d) * su * sv; +} +const mix3 = (a, b, t) => a.map((v, j) => v + (b[j] - v) * t); +function put(A, i, c, k = 1) { + for (let j = 0; j < 3; j++) A[i * 3 + j] = c[j] * k; +} +function add(A, i, c, k = 1) { + for (let j = 0; j < 3; j++) A[i * 3 + j] += c[j] * k; +} +const gilt = (v) => [min(255, 268 * v), 178 * v + 30 * v ** 4, 58 * v + 70 * v ** 6]; +function hsv(h, s, v) { + const f = (n) => { + const k = (n + h * 6) % 6; + return v - v * s * max(0, min(k, 4 - k, 1)); + }; + return [f(5), f(3), f(1)]; +} + +// The painted backcloths. Each writes what the paint reflects (A, 0..1) and +// what glows on its own from behind the cloth (E, 0..255). +function sea(x, y, i, A, E) { + const px = x + 0.5, py = y + 0.5; + if (py < 56) { + put(A, i, mix3([0.1, 0.14, 0.36], [0.36, 0.44, 0.7], ((py - SKY) / 41) ** 1.3)); + if (y < 48 && hash(i + 77) > 0.982) add(E, i, [1, 1, 1], 60 + 90 * hash(i + 5)); + const d = hypot(px - 43, py - 31); + if (d < 6.3) { + put(A, i, [0, 0, 0]); + put(E, i, [242, 236, 205], 0.88 + 0.12 * sqrt(1 - (d / 6.3) ** 2) - 0.14 * (vnoise(px * 0.7, py * 0.7) > 0.64)); + } else add(E, i, [80, 88, 110], exp(-(d - 6.3) / 3.5)); + // A headland on the left and a ship with its sails up on the horizon. + const ship = (abs(px - 26) < 4.2 - (py - 53.5) * 0.9 && py > 53.5) || (abs(px - 26) < 0.5 && py > 44) || (py > 45 && px > 26.5 && px < 26.6 + (py - 45) * 0.55) || (py > 47.5 && px < 25.5 && px > 25.4 - (py - 47.5) * 0.45); + if ((px < 15 && py > 43 + 1.2 * (px - 4) + 1.5 * sin(px * 1.1)) || ship) put(A, i, [0.035, 0.05, 0.09]); + } else put(A, i, mix3([0.05, 0.12, 0.3], [0.1, 0.2, 0.42], (py - 56) / 22), 1 + 0.2 * sin(py * 1.9 + 1.4 * sin(px * 0.35))); +} + +const TRUNKS = [[8, 2.2, 0.2], [14, 3.4, 1], [22, 1.7, 0], [30, 2.4, 0.5], [40, 3.8, 1], [47, 1.9, 0], [53, 3, 0.9], [58.5, 2.2, 0.3]]; +function forest(x, y, i, A) { + const px = x + 0.5, py = y + 0.5; + const t = (py - SKY) / (BACK - SKY); + let c = mix3([0.18, 0.32, 0.32], [0.5, 0.62, 0.44], t); + c = mix3(c, [0.62, 0.68, 0.48], 0.45 * exp(-((px - 32) ** 2) / 90 - ((py - 60) ** 2) / 260)); + for (const [tx, hw, near] of TRUNKS) + if (abs(px - tx - 0.7 * sin(py * 0.07 + tx)) < hw * (0.85 + 0.35 * t)) c = mix3(c, [0.09, 0.07, 0.05].map((v) => v * (0.7 + 0.5 * vnoise(px * 1.4, py * 0.22))), 0.45 + 0.45 * near); + const n = vnoise(px * 0.16, py * 0.2) * 0.65 + vnoise(px * 0.45, py * 0.5) * 0.35; + if (n - (py - 20) * 0.022 > 0.42) c = mix3([0.05, 0.16, 0.06], [0.2, 0.44, 0.14], vnoise(px * 0.9, py * 0.9)); + if (py > 67 + 2.5 * sin(px * 0.55) + 1.5 * sin(px * 1.4 + 1)) c = mix3([0.05, 0.16, 0.05], [0.16, 0.36, 0.1], hash(i * 3)); + put(A, i, c); +} + +// The city's skyline, two rows of blocks, and which LED is which window. +const [FARTOP, NEARTOP, NEARL] = [0, 0, 0].map(() => new Int16Array(W)); +const WIN = new Int16Array(N).fill(-1); +let NWIN = 0; +for (let x = 0, k = 0; x < W; k++) + for (let j = 0, w = 5 + floor(hash(k + 400) * 5), top = 38 + floor(hash(k + 500) * 16); j < w && x < W; j++) FARTOP[x++] = top; +for (let x = 0, k = 0; x < W; k++) + for (let j = 0, w = 6 + floor(hash(k + 600) * 6), top = 50 + floor(hash(k + 700) * 15); j < w && x < W; j++, x++) { + NEARTOP[x] = top; + NEARL[x] = x - j; + } +function city(x, y, i, A, E) { + const px = x + 0.5, py = y + 0.5; + put(A, i, mix3([0.1, 0.07, 0.24], [0.44, 0.25, 0.42], clamp((py - SKY) / 45, 0, 1) ** 1.2)); + if (y < 40 && hash(i + 91) > 0.986) add(E, i, [1, 1, 1], 50 + 70 * hash(i)); + if (hypot(px - 15, py - 28) < 4.4 && hypot(px - 17, py - 26.6) > 3.8) put(E, i, [240, 226, 170]); + const tower = (abs(px - 46.5) < 2.2 && py > 31) || (abs(px - 46.5) < 0.6 && py > 25); + if (py > FARTOP[x] || tower) put(A, i, [0.14, 0.12, 0.25]); + if (tower && py > 33 && x === 46 && y % 3 === 0) put(E, i, [170, 140, 90]); + if (py > NEARTOP[x]) { + put(A, i, [0.05, 0.045, 0.09]); + if ((x - NEARL[x]) % 2 === 1 && (y - NEARTOP[x]) % 3 === 2 && NEARTOP[min(W - 1, x + 1)] === NEARTOP[x]) WIN[i] = NWIN++; + } +} + +const PINES = [[9, 60, 15], [15, 58, 11], [50, 61, 16], [57, 59, 12], [22, 69, 9], [43, 70, 10]]; +function snow(x, y, i, A, E) { + const px = x + 0.5, py = y + 0.5; + let c = mix3([0.2, 0.26, 0.44], [0.48, 0.54, 0.68], (py - SKY) / 50); + if (y < 44 && hash(i + 33) > 0.988) add(E, i, [1, 1, 1], 40 + 50 * hash(i)); + if (py > 54 + 5 * sin(px * 0.08 + 0.6) + 2 * sin(px * 0.23)) c = [0.56, 0.62, 0.8]; + if (py > 65 + 4 * sin(px * 0.11 - 1.1)) c = [0.8, 0.84, 0.96]; + for (const [tx, base, h] of PINES) { + const d = py - (base - h); + if (d > 0 && d < h + 2 && abs(px - tx) < (d < h ? (d % 4) * 0.55 + d * 0.22 : 0.7) + 0.3) c = d < h && d % 4 < 0.9 ? [0.85, 0.9, 1] : [0.04, 0.13, 0.09]; + } + // A cottage on the near hill, its roof under snow and one window lit. + if (px > 30 && px < 38 && py > 62 && py < 68) c = [0.24, 0.13, 0.08]; + if (py > 58 && py <= 62 && abs(px - 34) < (py - 57.5) * 1.2) c = [0.88, 0.92, 1]; + if (px > 35.5 && px < 37 && py > 56.5 && py < 60) c = [0.2, 0.12, 0.1]; + if (px > 32.5 && px < 34.5 && py > 63.5 && py < 66) put(E, i, [255, 190, 90]); + put(A, i, c); +} +const SCENES = [sea, forest, city, snow]; + +// The front row: heads [x, top, half-width] above red seat backs. +const HEADS = [[5, 109, 4.2], [15.5, 106.5, 4.5], [27, 109.5, 4.3], [37.5, 107.5, 4.6], [49, 108.5, 4.4], [59, 106.5, 4.3]]; + +export function setup(params) { + const F = (n) => new Float32Array(n); + const U = (n) => new Uint8Array(n); + const S = { base: -1, alb: [], emi: [], arch: U(N), archC: F(N * 3), floorA: F(N), house: F(N * 3), R: F(N), G: F(N), B: F(N), M: U(N), fb: F(W), flakes: [] }; + const seat = U(N); + const edge = new Int16Array(W); + params.s = S; + for (let x = 0; x < W; x++) { + const dx = min(x - XL, XR - x); + edge[x] = x < XL || x > XR ? LIP + 2 : TOP + (dx < 9 ? round(9 - sqrt(81 - (9 - dx) ** 2)) : 0); + S.fb[x] = ((2 * PI) / 5.4) * (x + 1.3 * sin(x * 0.31)); + } + // The arch's gilt mouldings go by how far an LED is from the opening. + const crest = (x, y) => hypot((x + 0.5 - 32) / 5.5, (y + 0.5 - 6.5) / 4.5); + for (let y = 0; y <= LIP + 1; y++) for (let x = 0; x < W; x++) if (y < edge[x] || crest(x, y) < 1) S.arch[y * W + x] = 1; + for (let y = 0; y <= LIP + 1; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x; + if (!S.arch[i]) continue; + let d = 99; + for (let v = max(0, y - 6); v <= min(LIP + 1, y + 6); v++) + for (let u = max(XL, x - 6); u <= min(XR, x + 6); u++) if (!S.arch[v * W + u]) d = min(d, hypot(u - x, v - y)); + const along = x >= XL && x <= XR ? x : y; + let g = 0.52 + 0.22 * sin(along * 0.9) * sin(d * 1.7) + 0.08 * hash(i); + if (d < 5.2) g = d < 1.2 ? 1 : d < 2.2 ? 0.36 : d < 3.2 ? (along % 3 ? 0.55 : 1) : d < 4.2 ? 0.72 : 0.34; + const q = crest(x, y); + put(S.archC, i, gilt(q < 1 ? (q > 0.8 ? 1 : q > 0.62 ? 0.3 : 0.62 + 0.4 * (1 - q) - 0.25 * (abs(x - 31.5) < 1 && y > 4)) : g * (x < XL || x > XR ? 0.8 + 0.28 * (y / LIP) : 0.95))); + } + for (let s = 0; s < 4; s++) { + const A = F(N * 3); + const E = F(N * 3); + for (let y = SKY; y < BACK; y++) for (let x = XL; x <= XR; x++) SCENES[s](x, y, y * W + x, A, E); + S.alb.push(A), S.emi.push(E); + } + S.won = U(NWIN).map(() => (Math.random() < 0.55 ? 1 : 0)); + // Floorboards running away from us to the backcloth. + for (let y = BACK; y < LIP; y++) + for (let x = XL; x <= XR; x++) { + const f = ((((x + 0.5 - 32) / (y + 0.5 - 44)) * 5) % 1 + 1) % 1; + S.floorA[y * W + x] = (y === BACK ? 0.4 : 1) * (abs(f - 0.5) > 0.42 ? 0.55 : 1) * (0.92 + 0.16 * hash(y * W + x)); + } + // The house: the pit's glow and its music stand lamps behind the heads. + for (let y = LIP + 2; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x; + const px = x + 0.5, py = y + 0.5; + let c = [255, 140, 60].map((v) => v * (y < 104 ? 0.12 : 0.3 * exp(-(((py - 110) / 7) ** 2)))); + for (let k = 0; k < 7; k++) { + const d = hypot(px - (4 + k * 9.3), (py - 113.5) * 1.6); + if (d < 3) c = c.map((v, j) => v + [230, 170, 90][j] * exp(-d * d)); + } + for (const [cx, top, rx] of HEADS) { + const ry = rx * 1.25; + // One gentleman has kept his top hat on. + const hat = cx === 37.5 && ((abs(px - cx) < 3 && py > top - 5 && py < top + 1.5) || (abs(px - cx) < 5.6 && abs(py - top - 1.5) < 0.8)); + if (hat || hypot((px - cx) / rx, (py - top - ry) / ry) < 1 || (abs(px - cx) < rx * 0.55 && py > top + ry)) { + c = [3, 2, 3]; + seat[i] = 2; + } + } + // Seat backs in red velvet hide the shoulders. + const sx = (((px - 0.3) % 10.6) + 10.6) % 10.6; + if (y >= 119 && sx > 0.6 && sx < 10 && (y > 120 || (sx > 1.6 && sx < 9))) { + c = [255, 30, 36].map((v) => v * (0.1 + 0.32 * exp(-(py - 119) / 2.5))); + seat[i] = 1; + } + put(S.house, i, c); + } + // The top of each head catches a little of the stage light. + for (let i = W; i < N; i++) if (seat[i] === 2 && !seat[i - W]) put(S.house, i, [96, 62, 40]); + const rnd = Math.random; + for (let k = 0; k < 70; k++) S.flakes.push({ x: XL + rnd() * (XR - XL), y: SKY + rnd() * (LIP - SKY), v: rnd(), p: rnd() * 6 }); +} + +// Set the curtain moving from wherever it is. +function move(S, ph, quick) { + S.ph = ph; + S.o0 = S.o; + S.c = 0; + S.quick = quick; + S.dur = ph ? TFALL * max(0.25, S.o) : TRISE * max(0.25, 1 - S.o); +} + +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.gel = clamp(k[0], 0, 1); + const rate = clamp(k[1], 0.2, 3) / 2; + const base = clamp(round(k[2]), 0, 3); + S.haze = clamp(k[3], 0, 1); + if (S.base < 0) { + // Open on the curtain just about to go out. + Object.assign(S, { base, scene: base, count: 0, o: 0, t: 0, foot: 1 }); + move(S, 0, false); + S.c = -0.6; + } + if (base !== S.base) { + S.base = base; + if (S.ph === 3) { + S.count = 0; + S.scene = base; + move(S, 0, false); + } else if (S.ph === 2) S.quick = true; + else move(S, 2, true); + } + if (input.btnPressed && input.btnPressed[0]) move(S, S.ph < 2 ? 2 : 0, false); + const st = dt * rate; + S.t += st; + S.c += st; + if (S.ph === 0) { + S.o = S.o0 + (1 - S.o0) * sstep(0, S.dur, S.c); + if (S.c >= S.dur) { + S.ph = 1; + S.c = 0; + S.dir = S.count % 2 ? -1 : 1; + } + } else if (S.ph === 1) { + S.o = 1; + if (S.c >= TSHOW) move(S, 2, false); + } else if (S.ph === 2) { + S.o = S.o0 * (1 - sstep(0, S.dur, S.c)); + if (S.c >= S.dur) { + // The curtain is in: change the cloth behind it while she bows. + S.ph = 3; + S.c = 0; + S.count = S.quick ? 0 : S.count + 1; + S.scene = (S.base + S.count) % 4; + S.dur = S.quick ? THOLD : TBOW; + } + } else if (S.c >= S.dur) move(S, 0, false); + S.foot += ((S.ph === 1 ? 0.7 : 1) - S.foot) * min(1, dt * 2); + // Lit windows come and go, and snow falls from the flies. + for (let w = 0; w < S.won.length; w++) if (Math.random() < st * 0.25) S.won[w] ^= 1; + for (const f of S.flakes) { + f.x += sin(S.t * 1.3 + f.p) * 0.12 * rate; + if ((f.y += (5 + 4 * f.v) * st) >= LIP) Object.assign(f, { y: SKY - Math.random() * 6, x: XL + Math.random() * (XR - XL) }); + } +} + +// The dancer, as a mask: 1 skin, 2 tutu and bodice, 3 tights, 4 hair. +function stamp(M, x, y, r, c) { + for (let v = max(0, floor(y - r)); v <= min(H - 1, floor(y + r)); v++) + for (let u = max(0, floor(x - r)); u <= min(W - 1, floor(x + r)); u++) if ((u + 0.5 - x) ** 2 + (v + 0.5 - y) ** 2 <= r * r) M[v * W + u] = c; +} +function limb(M, x0, y0, x1, y1, r, c) { + const n = Math.ceil(hypot(x1 - x0, y1 - y0) * 2) + 1; + for (let k = 0; k <= n; k++) stamp(M, x0 + ((x1 - x0) * k) / n, y0 + ((y1 - y0) * k) / n, r, c); +} +function dancer(M, p) { + const { hx, hy, s, dir } = p; + const ux = dir * sin(p.lean), uy = -cos(p.lean); + for (const a of [p.la, p.lb]) limb(M, hx, hy, hx + dir * sin(a) * 9 * s, hy + cos(a) * 9 * s, 0.72, 3); + const sx = hx + ux * 6.2 * s * p.tq, sy = hy + uy * 6.2 * s * p.tq; + limb(M, hx, hy, sx, sy, 1.05, 2); + const ax = sx - ux * 0.6, ay = sy - uy * 0.6; + for (const a of [p.aa, p.ab]) limb(M, ax, ay, ax + dir * sin(a) * 6 * s, ay + cos(a) * 6 * s, 0.65, 1); + // The tutu: two layers square to her body. + const cx = hx + ux * 0.6, cy = hy + uy * 0.6; + limb(M, cx + uy * 5 * s, cy - ux * 5 * s, cx - uy * 5 * s, cy + ux * 5 * s, 0.85, 2); + limb(M, cx + ux + uy * 3.8 * s, cy + uy - ux * 3.8 * s, cx + ux - uy * 3.8 * s, cy + uy + ux * 3.8 * s, 0.85, 2); + const nx = sx + ux * 2.1 * s, ny = sy + uy * 2.1 * s + p.hd; + stamp(M, nx, ny, 1.75 * s, 1); + stamp(M, nx + ux * 1.4 * s - dir * 0.5, ny + uy * 1.4 * s, 0.95, 4); +} + +// Where the dancer is and how she stands, or null when she is off. +function pose(S) { + const T = S.c; + if (S.ph === 1) { + const u = (T - 0.15) / 3.6; + if (u < 0 || u > 1) return null; + const dir = S.dir; + const lc = ((T - 0.15) / 1.2) % 1; + const p = { hx: dir > 0 ? -9 + 82 * u : 73 - 82 * u, s: 1, dir, lean: 0, tq: 1, hd: 0, fy: 91, aa: 1.35, ab: -1.35, la: 0.45, lb: -0.45, cy: 79, r: 11, ox: 6, oy: -3 }; + p.hy = 91 - 9 * cos(0.45); + if (lc < 0.4) { + p.la = 0.42 * sin((lc / 0.4) * 2 * PI); + p.lb = -p.la; + p.hy = 91 - 9 * cos(p.la); + } else if (lc < 0.88) { + // The grand jete: a split in the air with the front arm raised. + const air = sin(PI * ((lc - 0.4) / 0.48)); + const sp = 1.35 * min(1, air * 1.6); + Object.assign(p, { la: sp, lb: -sp * 1.1, hy: 91 - 9 * max(cos(sp), 0.2) - 6 * air, aa: 1.35 + air, ab: -1.35 - 0.5 * air, lean: 0.08 * air }); + } + p.lev = sstep(0.05, 0.4, T) * (1 - sstep(3.45, 3.85, T)); + // The operator follows her with the spot, a little behind. + S.spx = S.spx === undefined || T < 0.2 ? p.hx : S.spx + (p.hx - S.spx) * 0.25; + p.cx = S.spx; + return p; + } + if (S.ph === 3 && !S.quick) { + // Her bow in front of the curtain, sinking into the reverence and up again. + const k = sstep(0.6, 1.3, T) * (1 - sstep(2.1, 2.7, T)); + const la = 0.14 + 0.3 * k; + const hy = 99 - 9.9 * cos(la); + return { hx: 31.5, hy, s: 1.1, dir: 1, lean: 0, tq: 1 - 0.5 * k, hd: 2.6 * k, la, lb: -la, aa: 2.5 - 1.9 * k, ab: -2.5 + 1.9 * k, fy: 99, lev: sstep(0, 0.3, T) * (1 - sstep(2.8, 3.15, T)), cx: 31.5, cy: hy - 4, r: 13, ox: 4, oy: 1 }; + } + return null; +} + +// Per row: the beams' light in the air, and their pools on the floor. +const [RB, RP, TX, TY, LI] = [W * 3, W * 3, 6, 6, 6].map((n) => new Float32Array(n)); +const HR = new Int16Array(W); +const DCOL = [0, 0, 0, 1, 0.8, 0.68, 1, 0.95, 0.98, 0.95, 0.74, 0.8, 0.42, 0.26, 0.18]; +const LIPC = gilt(0.42); + +export function draw(display, params) { + const S = params.s; + if (!S || S.base < 0) return; + const { R, G, B, M, arch, archC, fb, house } = S; + const gel = hsv((0.58 + S.gel) % 1, 0.75, 1); + const [w0, w1, w2] = gel.map((v) => v + (1 - v) * 0.45); + const [b0, b1, b2] = gel.map((v) => v + (1 - v) * 0.35); + const hz = 0.32 + 0.9 * S.haze; + const fog = 4 + 56 * S.haze; + const bw = 5.5 + 3 * S.haze; + const A = S.alb[S.scene]; + const E = S.emi[S.scene]; + const T = S.t; + const ft = S.foot; + // The lamps on the bridge, each aimed at a spot on the floor that wanders. + for (let j = 0; j < 6; j++) { + TX[j] = AIM[j] + 7 * sin(T * 0.33 + j * 1.9); + TY[j] = 88 + 4 * sin(T * 0.21 + j * 2.7); + LI[j] = 0.62 + 0.38 * sin(T * 0.4 + j * 1.3); + } + const hem = 94 - S.o * 92; + for (let x = 0; x < W; x++) HR[x] = round(hem + 0.4 + 0.4 * sin(fb[x])); + const p = pose(S); + M.fill(0); + const lev = p && p.lev > 0.04 ? p.lev : 0; + if (lev) dancer(M, p); + const DL = 250 * (0.3 + 0.7 * lev); + + for (let y = 0; y < H; y++) { + RB.fill(0); + RP.fill(0); + for (let j = 0; y > LENS && y < LIP && j < 6; j++) { + if (y > TY[j] + 1) continue; + const lx = LAMPS[j] + 0.5, ly = LENS + 1; + const dx = TX[j] - lx, dy = TY[j] - ly; + const inv = 1 / (dx * dx + dy * dy); + const tt = clamp((y + 0.5 - ly) / dy, 0, 1); + const rc = lx + dx * tt; + const rb = (1.9 + bw * tt) * 1.25; + const kk = 200 * hz * LI[j]; + // Each beam is a cone in the haze from its lens down to the floor. + for (let x = max(XL, floor(rc - rb)); x <= min(XR, rc + rb); x++) { + const ex0 = x + 0.5 - lx, ey0 = y + 0.5 - ly; + const t = (ex0 * dx + ey0 * dy) * inv; + if (t < 0 || t > 1) continue; + const ex = ex0 - t * dx, ey = ey0 - t * dy; + const wd = 0.9 + bw * t; + const q = (ex * ex + ey * ey) / (wd * wd); + if (q >= 1) continue; + const k = kk * (1 - q) * (1 - q) * (1.25 - 0.55 * t); + RB[x * 3] += b0 * k; RB[x * 3 + 1] += b1 * k; RB[x * 3 + 2] += b2 * k; + } + const qy = (y + 0.5 - TY[j]) / 3; + if (y >= BACK && qy * qy < 1) + for (let x = max(XL, floor(TX[j] - 7.5)); x <= min(XR, TX[j] + 7.5); x++) { + const qx = (x + 0.5 - TX[j]) / 7.5; + const q = 1 - qx * qx - qy * qy; + if (q <= 0) continue; + const k = 220 * LI[j] * q * sqrt(q); + RP[x * 3] += b0 * k; RP[x * 3 + 1] += b1 * k; RP[x * 3 + 2] += b2 * k; + } + } + const spotRow = lev && abs(y + 0.5 - p.cy) < p.r + 1.5; + const lv = ft * (0.22 + 0.78 * clamp((y - TOP) / (CURT - TOP), 0, 1) ** 1.5) + 0.16; + const fl = ft * 70 * clamp((y - BACK) / (LIP - BACK), 0, 1) ** 2; + + for (let x = 0; x < W; x++) { + const i = y * W + x; + const hr = HR[x]; + let r, g, b; + if (arch[i]) (r = archC[i * 3]), (g = archC[i * 3 + 1]), (b = archC[i * 3 + 2]); + else if (y > LIP + 1) (r = house[i * 3] * ft), (g = house[i * 3 + 1] * ft), (b = house[i * 3 + 2] * ft); + else if (y === LIP) { + const on = (x - XL) % 4 === 1; + r = on ? 255 : 190 * ft; g = (on ? 215 : 120) * ft; b = (on ? 140 : 50) * ft; + } else if (y === LIP + 1) [r, g, b] = LIPC; + else { + if (y < hr + 3 - (x & 1)) { + if (y < hr - 2) { + // The house curtain in velvet folds, lit from the footlights below. + const q = 0.5 + 0.5 * sin(fb[x] + 0.5 * sin((y - hem) * 0.09 + x * 0.6) * (0.4 + sstep(12, 0, hr - y))); + const v = 0.16 + 0.84 * q * (0.4 + 0.6 * q); + const sh = 26 * v * v * v * v * lv; + r = 250 * v * lv + sh + w0 * 18 * v; g = r * 0.05 + sh + w1 * 10 * v; b = r * 0.09 + sh + w2 * 14 * v; + } else { + // The gold braid, and the fringe that hangs below it. + const gv = (y < hr ? ((x + y) & 1 ? 0.95 : 0.62) : x & 1 ? 0.5 - 0.12 * (y - hr) : 0.88 - 0.1 * (y - hr)) * (0.35 + 0.75 * lv); + [r, g, b] = gilt(gv); + } + } else if (y < TRUSS) r = g = b = 3; + else if (y < SKY) { + const lace = y === TRUSS || y === SKY - 1 || (x + y) % 4 === 0 || (x - y + 64) % 4 === 0; + r = g = lace ? 52 : 4; + b = lace ? 58 : 5; + } else { + if (y < BACK) { + r = A[i * 3] * w0 * 165 + E[i * 3]; g = A[i * 3 + 1] * w1 * 165 + E[i * 3 + 1]; b = A[i * 3 + 2] * w2 * 165 + E[i * 3 + 2]; + if (S.scene === 0 && y >= 56) { + // The moon's path: short glints on the swell, wider toward us. + const gd = abs(x + 0.5 - 43) / (2 + (y - 56) * 0.22); + const cell = floor((x + 0.5 * y) / 3) + y * 31; + const gl = gd < 1.5 ? hash(cell + floor(T * 3 + hash(cell) * 9) * 4096) - 0.6 - 0.18 * gd * gd : 0; + if (gl > 0) { r += gl * 480; g += gl * 480; b += gl * 430; } + } else if (S.scene === 2) { + const w = WIN[i]; + if (w >= 0 && S.won[w]) { r += 235; g += 170; b += 80; } + if (x === 46 && y === 25 && T % 1.6 < 0.3) r += 255; + } + } else { + // The floor, lit by the wash and the lamps' pools. + const fa = S.floorA[i]; + r = fa * (w0 * 55 + fl + RP[x * 3]); g = fa * 0.88 * (w1 * 55 + fl * 0.7 + RP[x * 3 + 1]); b = fa * 0.76 * (w2 * 55 + fl * 0.35 + RP[x * 3 + 2]); + } + if (y > LENS) { + r += RB[x * 3]; g += RB[x * 3 + 1]; b += RB[x * 3 + 2]; + } else + for (let j = 0; j < 6; j++) { + // A lamp hangs from its yoke, the lens shining down. + const d = abs(x - LAMPS[j]); + if (d > 1 || (y === SKY && d)) continue; + if (y < LENS) r = g = b = y === SKY ? 60 : d ? 34 : 70; + else { + const k = (0.45 + 0.55 * LI[j]) * (d ? 190 : 255); + r = b0 * k; g = b1 * k; b = b2 * k; + } + } + } + // Haze in the air, lit by the wash. + r += fog * w0; g += fog * w1; b += fog * w2; + if (spotRow) { + // The follow spot: a pool of white light, with her shadow in it. + const ddx = x + 0.5 - p.cx, ddy = y + 0.5 - p.cy; + const inn = sstep(p.r + 1.5, p.r - 1.5, sqrt(ddx * ddx + ddy * ddy)) * lev; + if (inn > 0) { + const bx = floor(p.hx + (x + 0.5 - p.hx - p.ox) / 1.2); + const by = floor(p.fy + (y + 0.5 - p.fy - p.oy) / 1.2); + const shade = bx >= 0 && bx < W && by >= 0 && by < H && M[by * W + bx] && !M[i]; + const k = shade ? inn * 0.1 : inn; + const dk = (1 + 1.1 * k) * (shade ? 0.55 : 1); + r = r * dk + 62 * k; g = g * dk + 58 * k; b = b * dk + 52 * k; + } + } + const c = M[i]; + if (c) { r = DCOL[c * 3] * DL; g = DCOL[c * 3 + 1] * DL; b = DCOL[c * 3 + 2] * DL; } + } + R[i] = r; G[i] = g; B[i] = b; + } + } + + // Paper snow falls from the flies in front of the backcloth. + for (const f of S.scene === 3 ? S.flakes : []) { + const x = floor(f.x), y = floor(f.y), i = y * W + x; + if (y >= SKY && y >= HR[x] + 3 && !arch[i]) { + R[i] += 190; G[i] += 195; B[i] += 210; + } + } + for (let i = 0, y = 0; y < H; y++) + for (let x = 0; x < W; x++, i++) display.setPixel(x, y, R[i] > 255 ? 255 : R[i], G[i] > 255 ? 255 : G[i], B[i] > 255 ? 255 : B[i]); +} From 9e8c4b2d37509555d961745e56d5cd57288e9c60 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 00:10:47 +0000 Subject: [PATCH 09/15] =?UTF-8?q?Giraffe=20=ED=8C=A8=ED=84=B4=EC=9D=84=20?= =?UTF-8?q?=EC=A7=A0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GhRE8HZxGnk63uCfeHdr92 --- incoming/pattern-study/patterns/giraffe.js | 441 +++++++++++++++++++++ 1 file changed, 441 insertions(+) create mode 100644 incoming/pattern-study/patterns/giraffe.js diff --git a/incoming/pattern-study/patterns/giraffe.js b/incoming/pattern-study/patterns/giraffe.js new file mode 100644 index 000000000..b6b82cdd6 --- /dev/null +++ b/incoming/pattern-study/patterns/giraffe.js @@ -0,0 +1,441 @@ +// ===== Patternflow pattern ===== +// Title: Giraffe +// Family: mammal +// Idea: Under the flat crown of an umbrella thorn a reticulated giraffe strips a twig with its long dark tongue, and every so often it splays its front legs and bends down to drink at the waterhole. +// Author: patternflow-lab +// Date: 2026-10-23 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 23, after the browse line giraffes cut under the umbrella thorn acacia (Vachellia tortilis) +// Round: 23 +// =============================== +// @matrix 64x128 +// @knobs Sun=0..1, Speed=0.2..3, Coat=0..3, Wind=0..1 +// +// Knob 1: Sun. The hour, from a high white sun at 0 to a red sun setting between the giraffe's legs at 1. Round each hour the sun sinks a little and climbs back once a minute. +// Knob 2: Speed. At 2 a lick takes about three seconds; the giraffe first goes down to drink after three, then every half minute or when its twig is bare. +// Knob 3: Coat. Reticulated with thin white lines, Masai with jagged leaf patches, Rothschild with broad buff lines and white stockings, or pale leucistic, each on its own ground: red earth, gold grass, green grass, pale sand. +// Knob 4: Wind. Grass and crown lean and shake with it, and dust blows over the plain until the far herd goes hazy. +// Buttons: K1 tap sends the giraffe down to drink now. + +const [W, H] = [64, 128]; +const N = W * H; +// Horizon and waterhole; the crown is kept wider than the panel to rock. +const [HOR, PCX, PCY, PRX, PRY] = [94, 50, 113.5, 14, 4.8]; +const [CW, CH, CO] = [80, 26, 8]; +const { min, max, floor, round, sin, cos, sqrt, exp, PI } = Math; +const hyp = (a, b) => sqrt(a * a + b * b); +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const mix = (a, b, t) => a + (b - a) * t; +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +const smin = (a, b, k, h = cl(0.5 + (0.5 * (b - a)) / k, 0, 1)) => b + (a - b) * h - k * h * (1 - h); +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 light from afternoon to dusk, one row per hour. +const SKY = [ + [74, 134, 216, 186, 210, 226, 255, 250, 228, 1, 0.98, 0.92, 1], + [62, 114, 206, 236, 208, 164, 255, 238, 180, 1.04, 0.92, 0.78, 0.97], + [56, 78, 170, 255, 170, 94, 255, 198, 100, 1.08, 0.8, 0.6, 0.88], + [40, 38, 108, 248, 108, 72, 255, 132, 60, 1, 0.64, 0.52, 0.74], +]; +// Each coat: patch, line, mane, and the ground it stands on. +const COATS = [ + { p: [150, 54, 22], l: [250, 240, 214], m: [92, 42, 22], land: [204, 142, 92] }, + { p: [108, 50, 26], l: [226, 184, 122], m: [80, 42, 24], land: [214, 182, 98] }, + { p: [180, 98, 44], l: [236, 212, 164], m: [112, 62, 32], land: [160, 176, 86] }, + { p: [224, 204, 172], l: [252, 248, 238], m: [200, 168, 132], land: [224, 210, 166] }, +]; +// The pale of the lower legs. +const STOCK = [244, 234, 212]; +const [BODY, NECK, HEAD, LEGN, LEGF, MANE, OSS, TIP, EAR, TAIL, TUFT, HOOF, MUZ] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; + +// The coat as a 128 x 128 wrapping texture: 1 inside a patch, 0 on a line. +function buildCoat(P, kind) { + const S = 6.4, G = 20; + for (let y = 0; y < 128; y++) + for (let x = 0; x < 128; x++) { + let qx = x + 0.5, qy = y + 0.5, f1 = 99, f2 = 99, id = 0, ax = 0, ay = 0, v; + if (kind === 1) (qx += 3 * (vn(x * 0.3, y * 0.3) - 0.5)), (qy += 3 * (vn(x * 0.3 + 9, y * 0.3) - 0.5)); + const cx = floor(qx / S), cy = floor(qy / S); + for (let j = -1; j <= 1; j++) + for (let i = -1; i <= 1; i++) { + const gx = (cx + i + G) % G, gy = (cy + j + G) % G, fx = (cx + i + 0.15 + 0.7 * hash(gx, gy)) * S, fy = (cy + j + 0.15 + 0.7 * hash(gx + 50, gy)) * S; + const d = hyp(qx - fx, qy - fy); + if (d < f1) (f2 = f1), (f1 = d), (id = gx * 31 + gy), (ax = qx - fx), (ay = qy - fy); + else if (d < f2) f2 = d; + } + const e = f2 - f1; + if (kind === 0) v = sm(0.7, 1.4, e); + else if (kind === 1) { + // Masai patches are cut into leaves and stars. + const thr = 1.9 + sin(Math.atan2(ay, ax) * 5 + id) + 0.6 * vn(x * 0.7, y * 0.7); + v = sm(thr - 0.5, thr + 0.5, e); + } else v = kind === 2 ? sm(1.3, 2.6, e) : sm(1, 2.2, e) * 0.9; + P.tex[y * 128 + x] = v; + P.tsh[y * 128 + x] = 0.84 + 0.3 * hash(id, 7); + } + P.coat = kind; +} + +// A thick line into the wood layer. +function stroke(A, pts, r0, r1, val) { + for (let j = 0; j + 2 < pts.length; j += 2) { + const [ax, ay, bx, by] = pts.slice(j, j + 4), n = 2 * hyp(bx - ax, by - ay); + for (let s = 0; s <= n; s++) { + const t = s / n, x = mix(ax, bx, t), y = mix(ay, by, t), r = mix(r0, r1, (j + 2 * t) / (pts.length - 2)); + for (let yy = floor(y - r); yy <= y + r; yy++) + for (let xx = floor(x - r); xx <= x + r; xx++) + if (hyp(xx + 0.5 - x, yy + 0.5 - y) <= r && xx >= 0 && xx < W) A[yy * W + xx] = max(A[yy * W + xx], val * (1.1 - (0.35 * (xx + 0.5 - x)) / r)); + } + } +} + +function build(P) { + // The crown: flat layers of leaves over a flat browse line, with a hollow + // eaten up into it where the giraffe reaches. + for (let x = 0; x < CW; x++) { + const X = x - CO, top = 3 + 0.0028 * (X - 28) ** 2 + 2.2 * vn(X * 0.18, 1.5); + const bot = 19.5 + 1.6 * vn(X * 0.3, 4.5) + 0.0016 * (X - 28) ** 2 - 6.5 * exp(-(((X - 53.5) / 4.2) ** 2)); + for (let y = 0; y < CH; y++) { + const i = y * CW + x, f = (y - top) / (bot - top), n = vn(X * 0.16, y * 0.42) * 0.6 + vn(X * 0.5, y * 0.6) * 0.4; + if (f < 0 || f > 1) continue; + const k = n + 0.125 + 0.125 * sin(y * 0.9 + 2.4 * vn(X * 0.08, 2)) - 0.3 * sm(0.75, 1, f) - 0.35 * sm(0.18, 0, f); + if (k > 0.34) (P.cA[i] = 1), (P.cL[i] = (1.15 - 0.6 * f) * (0.75 + 0.5 * n) * (0.85 + 0.3 * hash(X + 300, y))); + else if (k > 0.2 && f > 0.12) P.cA[i] = 1; + } + } + // The trunk, and crooked branches fanning up to the crown. + stroke(P.wood, [58.5, 104, 57.5, 86, 59, 66, 57.5, 50], 2.2, 1.7, 1); + for (const b of [[57.5, 50, 53, 41, 48, 37, 41, 29, 31, 22, 24, 19], [57.5, 52, 55, 42, 51, 33, 50, 25, 46, 19], [58, 50, 61, 40, 61, 31, 65, 21], [58, 60, 54, 55, 46, 50]]) stroke(P.wood, b, 1.5, 0.8, 0.85); + // The ground, and dust that wraps every 128 columns. + for (let i = HOR * W; i < N; i++) { + const x = i & 63, y = i >> 6; + P.gnd[i] = mix(vn(x * 0.12, y * 0.9), vn(x * 0.6, y * 0.18), (y - HOR) / (H - HOR)) * 0.7 + 0.3 * hash(x, y + 700); + } + for (let i = 0; i < 128 * 58; i++) { + const x = i & 127, y = i >> 7, f = (X) => vn(X * 0.07, y * 0.16) * 0.7 + vn(X * 0.19, y * 0.3 + 5) * 0.3; + P.haze[i] = mix(f(x), f(x + 128), x / 128); + } + P.hill = Array.from({ length: W }, (_, x) => HOR - 2 - 2.5 * vn(x * 0.07, 3) - 1.5 * vn(x * 0.2, 7)); + // Dry grass, none standing in the water. + P.blades = []; + while (P.blades.length < 230) { + const y = HOR + 5 + (H - HOR - 4) * sqrt(rnd()), x = rnd() * 66 - 1; + if (((x - PCX) / (PRX + 1.5)) ** 2 + ((y - PCY) / (PRY + 2)) ** 2 > 1) P.blades.push([x, y, 1.5 + (y - HOR) * 0.2 * (0.6 + 0.8 * rnd()), rnd() * 7, 0.7 + 0.6 * rnd()]); + } + P.herd = Array.from({ length: 7 }, (_, i) => [i * 9.5 + rnd() * 5, HOR + 0.5 + rnd() * 2.2, 0.35 + rnd() * 0.3, rnd() * 7]); +} + +export function setup(params) { + const F = (n) => new Float32Array(n); + params.g = { + f: F(N * 3), z: new Int8Array(N), p: new Uint8Array(N), u: F(N), v: F(N), tex: F(16384), tsh: F(16384), gnd: F(N), wood: F(N), cA: F(CW * CH), cL: F(CW * CH), + haze: F(128 * 58), row: F(HOR * 3), bg: F(N * 3), coat: -1, t: 0, dk: 0, dt: 0, next: 3.4, legN: 0, legF: 0, nk: 0, tc: 0.6, tp: 2.8, tw: 0, twv: 0, tl: 9, + ear: 1, flick: 0, swish: 0, sw: 3, ox: 0.55, oxT: 1, hop: 1, fall: [], rip: [], ripT: 0, + }; + build(params.g); +} + +export function update(dt, input, params) { + if (!params.g) setup(params); + const P = params.g, k = input.knobValues || [0, 2, 0, 0.06]; + const d = cl(dt || 0, 0, 0.1) * (cl(k[1], 0.2, 3) / 2), coat = cl(round(k[2]), 0, 3), w = (P.wind = cl(k[3], 0, 1)); + if (coat !== P.coat) buildCoat(P, coat); + const t = (P.t += d), sunK = cl(k[0], 0, 1); + // The sun sinks and climbs back a little round the hour the knob sets. + P.h = sunK + (1 - sunK) * 0.3 * (0.5 - 0.5 * cos((2 * PI * t) / 60)); + + // Down to drink: the front legs go out one by one, then the neck comes down. + if (input.btnPressed && input.btnPressed[0] && P.dk === 0) P.next = 0; + const T = (P.dt += d); + if (P.dk === 0) { + if ((P.next -= d) <= 0 || (P.tl <= 0 && P.tc > 1.6)) (P.dk = 1), (P.dt = 0); + } else if (P.dk === 1) { + [P.legN, P.legF, P.nk] = [sm(0, 0.7, T), sm(0.45, 1.15, T), sm(0.8, 2.6, T)]; + if (T >= 2.6) (P.dk = 2), (P.dt = 0); + } else if (P.dk === 2) { + // Sips: the head dips and lifts, and rings spread on the water. + P.nk = 1 - 0.035 * (0.5 - 0.5 * cos(T * 4.4)); + if ((P.ripT -= d) <= 0) (P.ripT = 0.7 + rnd() * 0.4), P.rip.push([P.mx, P.my + 1, 0, 1]); + if (T >= 3.4) (P.dk = 3), (P.dt = 0); + } else { + // Up again fast, the head first, then the legs gathered in. + [P.nk, P.legN, P.legF] = [1 - sm(0, 1, T), 1 - sm(0.9, 1.6, T), 1 - sm(0.6, 1.3, T)]; + if (T >= 1.8) (P.dk = 0), (P.next = 26 + rnd() * 12), (P.tl = 9), (P.tc = 1.3); + } + + // The tongue curls round the twig to strip it. Then the jaw chews. + const browsing = P.dk === 0 && P.nk < 0.01; + if ((P.tc += d) > P.tp) (P.tc = 0), (P.tp = 2.4 + rnd() * 1.3), (P.pulled = false); + const c = P.tc, live = browsing && P.tl > 0; + P.ext = live ? (c < 0.45 ? sm(0, 0.45, c) : c < 1 ? 1 : 1 - sm(1, 1.3, c)) : 0; + P.twv += (60 * ((live && c > 0.5 && c < 1 ? 3 * sm(0.5, 0.85, c) : 0) - P.tw) - 5 * P.twv) * d; + P.tw += P.twv * d; + if (live && c > 0.85 && !P.pulled) { + P.pulled = true; + P.tl--; + for (let n = 0; n < 2; n++) P.fall.push([53.5 + rnd() * 2, 18, (rnd() - 0.5) * 4, 2 + rnd() * 3, rnd() * 7]); + } + P.chew = browsing && c > 1.3 ? 0.5 + 0.5 * sin(c * 19) : 0; + + // Ear and tail keep the flies off, and the oxpecker hops along the neck. + if ((P.ear -= d) <= 0) (P.ear = 1.5 + rnd() * 3), (P.flick = 1); + P.flick = max(0, P.flick - d * 3.5); + if ((P.sw -= d) <= 0) (P.sw = 2.5 + rnd() * 4), (P.swish = 1); + P.swish = max(0, P.swish - d * 0.9); + P.tail = 0.12 * sin(t * 1.1) + 0.5 * P.swish * sin(t * 9); + if ((P.oxT -= d) <= 0) (P.oxT = 0.7 + rnd() * 1.8), (P.ox0 = P.ox), (P.ox1 = cl(P.ox + (rnd() < 0.5 ? -1 : 1) * (0.05 + rnd() * 0.1), 0.3, 0.78)), (P.hop = 0); + if (P.hop < 1) (P.hop = min(1, P.hop + d * 4)), (P.ox = mix(P.ox0, P.ox1, P.hop)); + + // Falling leaves and rings on the pool. + P.fall = P.fall.filter((q) => ((q[4] += d), (q[0] += (q[2] + 3 * sin(q[4] * 3) + 8 * w) * d), (q[1] += q[3] * d), q[1] < 124)); + P.rip = P.rip.filter((q) => ((q[2] += d * 5), (q[3] -= d * 0.45), q[3] > 0)); + // The far herd walks on. + for (const a of P.herd) if ((a[0] -= a[2] * d) < -4) a[0] += 72; +} + +// One LED of the giraffe, unless something nearer is there. +function put(P, x, y, z, part, u, v) { + const i = y * W + x; + if (x >= 0 && x < W && y >= 0 && y < H && z >= P.z[i]) (P.z[i] = z), (P.p[i] = part), (P.u[i] = u), (P.v[i] = v); +} + +// A limb through joints, each a point and a radius. Each LED keeps how far +// along it lies (u) and across (v, plus on the mane side); the tip LEDs take +// another part. +function limb(P, J, z, part, mane = 0, tipPart = 0, tipLen = 0) { + let total = 0, s0 = 0; + for (let j = 3; j < J.length; j += 3) total += hyp(J[j] - J[j - 3], J[j + 1] - J[j - 2]); + for (let j = 0; j + 3 < J.length; j += 3) { + const [ax, ay, ra, bx, by, rb] = J.slice(j, j + 6), len = hyp(bx - ax, by - ay) || 1e-6, dx = (bx - ax) / len, dy = (by - ay) / len, pad = max(ra, rb) + mane + 1; + for (let y = floor(min(ay, by) - pad); y <= max(ay, by) + pad; y++) + for (let x = floor(min(ax, bx) - pad); x <= max(ax, bx) + pad; x++) { + const px = x + 0.5 - ax, py = y + 0.5 - ay, tt = cl(px * dx + py * dy, 0, len), dist = hyp(px - dx * tt, py - dy * tt); + const side = px * dy - py * dx, r = ra + ((rb - ra) * tt) / len, s = s0 + tt; + if (dist <= r) put(P, x, y, z, tipLen && s > total - tipLen ? tipPart : part, s, side >= 0 ? dist / r : -dist / r); + else if (mane && side > 0 && dist <= r + mane && s > 3 && s < total - 1) put(P, x, y, z, MANE, s, 1); + } + s0 += len; + } +} + +// Two bones from hip to hoof, bending the way dir says. +function ik(ax, ay, bx, by, a, b, dir) { + const dx = bx - ax, dy = by - ay, d0 = hyp(dx, dy), d = min(d0, a + b - 0.01), x = (a * a - b * b + d * d) / (2 * d), h = sqrt(max(0, a * a - x * x)); + return [ax + (dx / d0) * x - (dy / d0) * h * dir, ay + (dy / d0) * x + (dx / d0) * h * dir]; +} + +// Keys for the neck and head as they swing from the leaves to the water. +const NK = [ + [39.5, 44, 42, 28, -0.52], + [50, 40, 58, 58, 1.2], + [49, 80, 55, 99.5, 1.66], +]; + +function giraffe(P) { + P.z.fill(-1); + const lean = max(P.legN, P.legF), ca = cos(0.22 * lean), sa = sin(0.22 * lean), drop = 3 * lean; + // Body space to the panel, tilted on the hip. + const T = (x, y) => [12 + (x - 12) * ca - (y - 72) * sa, 72 + (x - 12) * sa + (y - 72) * ca + drop]; + // The legs go behind the body, the far pair darker. To drink the near + // front leg goes back and the far one forward. + for (const [sx, sy, hx, hy, dir, part, z] of [ + [31, 78, 33 + 12 * P.legF, 119.5, -1, LEGF, 1], + [16, 75, 16, 119.5, 1, LEGF, 1], + [35, 78, 37 - 14 * P.legN, 121.5, -1, LEGN, 2], + [12, 75, 11, 121.5, 1, LEGN, 2], + ]) { + const [ax, ay] = T(sx, sy), [kx, ky] = ik(ax, ay, hx, hy, 22, 22.5, dir); + limb(P, [ax, ay, dir < 0 ? 3.2 : 4, kx, ky, dir < 0 ? 1.6 : 1.8, hx, hy, 1.3], z, part, 0, HOOF, 2.2); + } + // The body, low at the rump and high at the shoulder. + for (let y = 48; y < 96; y++) + for (let x = 0; x < 50; x++) { + const qx = x + 0.5 - 12, qy = y + 0.5 - 72 - drop, bx = 12 + qx * ca + qy * sa, by = 72 - qx * sa + qy * ca; + const a = (hyp((bx - 13) / 8.5, (by - 72) / 8.5) - 1) * 8.5, b = (hyp((bx - 24) / 12.5, (by - 73.5) / 9.5) - 1) * 9.5, c = (hyp((bx - 34) / 8.5, (by - 67.5) / 13.5) - 1) * 8.5; + if (smin(smin(a, b, 5), c, 5) <= 0) put(P, x, y, 3, BODY, bx, by); + } + // The tail hangs from the rump and swishes. + const tj = T(5.5, 65); + tj.push(0.6); + for (let j = 1, x = tj[0], y = tj[1], a; j <= 4; j++) (a = PI / 2 + 0.1 + P.tail * j * 0.45), tj.push((x += 6.5 * cos(a)), (y += 6.5 * sin(a)), j === 4 ? 1.7 : 0.6); + limb(P, tj, 4, TAIL, 0, TUFT, 4.5); + + // The neck, a curve from the withers to the poll, and its mane. + const e = sm(0, 1, P.nk) * 2, i = min(1, floor(e)), K = NK[i].map((v, c) => mix(v, NK[i + 1][c], e - i)); + const bob = P.dk === 0 ? 1 - P.nk : 0, pull = P.tw * 0.25, [sx, sy] = T(36, 61), qx = K[2] + bob * 0.5 * sin(P.t * 0.9), qy = K[3] + bob * (0.4 * sin(P.t * 1.3) - pull); + const J = (P.neck = []); + for (let n = 0, t; n <= 12; n++) (t = n / 12), J.push((1 - t) ** 2 * sx + 2 * t * (1 - t) * K[0] + t * t * qx, (1 - t) ** 2 * sy + 2 * t * (1 - t) * K[1] + t * t * qy, mix(5.4, 2.8, t)); + limb(P, J, 2, NECK, 1.2); + // The head, its cheek working as it chews. + const ph = K[4] - pull * 0.05, fx = cos(ph), fy = sin(ph), nx = fy, ny = -fx, ch = P.chew; + const mx = (P.mx = qx + 10.5 * fx), my = (P.my = qy + 10.5 * fy); + limb(P, [qx - fx, qy - fy, 2.4, qx + 3 * fx - 0.4 * nx, qy + 3 * fy - 0.4 * ny, 3.1 + 0.35 * ch, mx, my, 1.6 + 0.3 * ch], 7, HEAD, 0, MUZ, 2.6); + // Ossicones, and the ear flicking at a fly. + for (const [o, z] of [[0.3, 7], [2.1, 6]]) { + const bx = qx + o * fx + 2 * nx, by = qy + o * fy + 2 * ny; + limb(P, [bx, by, 1, bx + 3.2 * (nx - 0.25 * fx), by + 3.2 * (ny - 0.25 * fy), 0.9], z, OSS, 0, TIP, 1.3); + } + const fa = 0.35 - 0.9 * P.flick * sin(P.flick * 9), bx = qx - 0.6 * fx + 1.3 * nx, by = qy - 0.6 * fy + 1.3 * ny; + limb(P, [bx, by, 1, bx + 3.6 * (-fx * cos(fa) + nx * sin(fa)), by + 3.6 * (-fy * cos(fa) + ny * sin(fa)), 0.5], 8, EAR); + P.eye = [qx + 2.8 * fx + 0.9 * nx - 0.5, qy + 2.8 * fy + 0.9 * ny - 0.5]; +} + +// A colour into the frame, mixed over what is there by a. +let F; +function set(x, y, col, a = 1, k = 1) { + x = round(x); + y = round(y); + if (x < 0 || x >= W || y < 0 || y >= H) return; + for (let c = 0, o = (y * W + x) * 3; c < 3; c++) F[o + c] = mix(F[o + c], col[c] * k, a); +} + +// Everything behind the tree. +function back(P, L, tint, land) { + const h = P.h, t = P.t, row = P.row, key = round(h * 900) * 4 + P.coat; + // Sky and plain are kept until the hour moves. + if (key !== P.bgKey) { + const B = P.bg, hill = [104, 104, 136].map((v) => v * (0.55 + 0.45 * L[12])), sx = 21 + 6 * h, sy = 30 + 66 * h, sr = 3.2 + 2 * h, g1 = 10 + 30 * h, g2 = 8 + 12 * h; + P.bgKey = key; + for (let y = 0; y < HOR; y++) for (let c = 0; c < 3; c++) row[y * 3 + c] = mix(L[c], L[3 + c], (y / HOR) ** 1.5); + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, o = i * 3; + if (y < HOR) { + const d = hyp(x + 0.5 - sx, (y + 0.5 - sy) * 1.05), gl = d < sr ? 1 : d > 48 ? 0 : 0.4 * exp(-((d - sr) * (d - sr)) / g1) + 0.16 * exp(-d / g2), hm = y >= P.hill[x] ? 0.5 : 0; + for (let c = 0, v; c < 3; c++) (v = row[y * 3 + c]), (B[o + c] = mix(v + (L[6 + c] - v * 0.4) * gl, hill[c], hm)); + } else { + const q = 1 - (y - HOR) / (H - HOR), n = 0.78 + 0.34 * P.gnd[i]; + for (let c = 0; c < 3; c++) B[o + c] = mix(land[c] * n, L[3 + c], 0.5 * q * q); + } + } + } + F.set(P.bg); + // The far herd walking along the horizon. + const hc = [74, 58, 50].map((v, c) => v * tint[c]); + for (const [x, y, , p] of P.herd) { + const s = (t * 4 + p) % 2 < 1 ? 0 : 1; + set(x, y, hc), set(x + 1, y, hc), set(x + 2, y - 1, hc), set(x + s, y + 1, hc, 0.7), set(x + 2 - s, y + 1, hc, 0.7); + } + // The giraffe's shadow stretches away from the sun. + const shx = 24 + 22 * h, shr = 17 + 12 * h; + for (let y = 118; y < 126; y++) + for (let x = 0, e; x < W; x++) if ((e = ((x - shx) / shr) ** 2 + ((y - 122) / 2.6) ** 2) < 1) for (let c = 0; c < 3; c++) F[(y * W + x) * 3 + c] *= 0.58 + 0.3 * e; + // The waterhole holds the sky, and sipping sends rings across it. + for (let y = floor(PCY - PRY); y <= PCY + PRY; y++) + for (let x = floor(PCX - PRX); x < W; x++) { + const e = ((x + 0.5 - PCX) / PRX) ** 2 + ((y + 0.5 - PCY) / PRY) ** 2, o = (y * W + x) * 3, ry = cl(round(HOR - 6 - (y - PCY + PRY) * 4), 0, HOR - 1); + if (e >= 1) continue; + let k = 0.1 * sin(x * 0.8 + t * 2.6 + y * 1.7) * (0.4 + P.wind); + for (const [cx, cy, rr, a] of P.rip) k += 0.5 * a * exp(-((hyp(x + 0.5 - cx, (y + 0.5 - cy) * 2.6) - rr) ** 2) / 0.7); + for (let c = 0; c < 3; c++) F[o + c] = e > 0.72 ? F[o + c] * 0.5 : row[ry * 3 + c] * (0.76 + k) + 6 * c; + } +} + +// The tree, its crown rocking on the wind. +function tree(P, tint) { + const t = P.t, w = P.wind, wood = [92, 72, 58].map((v, c) => v * tint[c]), lit = (P.leaf = [116, 152, 58].map((v, c) => v * tint[c])), dark = [14, 23, 10]; + for (let i = 0; i < N; i++) if (P.wood[i] > 0) for (let c = 0; c < 3; c++) F[i * 3 + c] = wood[c] * P.wood[i]; + const rock = (0.35 + 2.4 * w) * sin(t * (0.9 + w)) + 0.9 * w * sin(t * 2.3 + 1), fa = 0.08 + 0.3 * w, fw = 3 + 5 * w; + for (let y = 0; y < CH; y++) + for (let x = 0, sh = rock * (0.6 + (0.4 * (CH - y)) / CH); x < W; x++) { + const X = round(x + CO - sh), j = y * CW + X; + if (X < 0 || X >= CW || !P.cA[j]) continue; + const v = cl(P.cL[j] * (1 + fa * sin(t * fw + hash(X, y) * 40)) - 0.12, 0, 1.3); + for (let c = 0; c < 3; c++) F[(y * W + x) * 3 + c] = dark[c] + (lit[c] - dark[c]) * v; + } + const tx = (P.tx = 54 - 0.5 * P.tw), ty = (P.ty = 18.5 + 0.9 * P.tw); + for (let s = 0; s <= 8; s++) { + const u = s / 8, x = mix(57.5, tx, u) + 0.6 * sin(u * 3), y = mix(12, ty, u); + set(x, y, wood); + if (s >= 2 && (s - 2) * 1.5 < P.tl) set(x + (s & 1 ? 1 : -1), y, lit, 1, 1.35); + } +} + +// The giraffe's colours, the coat looked up by where on the body each LED lies. +function coat(P, C, tint) { + const col = [0, 0, 0], { tex, tsh } = P; + for (let i = 0; i < N; i++) { + if (P.z[i] < 0) continue; + const part = P.p[i], u = P.u[i], v = P.v[i]; + let k = 1 - 0.22 * v * v; + if (part === MANE) for (let c = 0; c < 3; c++) col[c] = C.m[c] * (0.8 + 0.25 * (round(u) & 1)); + else if (part === TIP || part === TUFT || part === HOOF) col.splice(0, 3, ...(part === HOOF ? [70, 54, 44] : [52, 36, 28])); + else if (part === TAIL) for (let c = 0; c < 3; c++) col[c] = C.m[c] * 1.3; + else if (part === MUZ || part === OSS || part === EAR) for (let c = 0; c < 3; c++) col[c] = mix(C.l[c], C.p[c], part === MUZ ? 0.5 : 0.35) * (part === MUZ ? 0.78 : 1); + else { + let tx, ty, pale = 0; + if (part === BODY) (tx = u * 1.05 + 3), (ty = v * 1.05), (k = 1.08 - 0.3 * sm(64, 84, v)); + else if (part === NECK) (tx = v * 6.2 + 77), (ty = u * 1.05 + 5); + else if (part === HEAD) (tx = u * 1.6 + 40), (ty = v * 4 + 100), (pale = 0.15); + else (tx = v * 3 + 100), (ty = u * (1 + u * 0.02) + 60), (pale = sm(18, 34, u) * (P.coat === 2 ? 1 : 0.55)); + const ti = ((floor(ty) & 127) << 7) | (floor(tx) & 127), pv = tex[ti] * (1 - pale), s = tsh[ti]; + for (let c = 0; c < 3; c++) col[c] = mix(mix(C.l[c], STOCK[c], pale), C.p[c] * s, pv); + } + if (part === LEGF) k *= 0.66; + for (let c = 0; c < 3; c++) F[i * 3 + c] = col[c] * k * tint[c]; + } + set(P.eye[0], P.eye[1], [24, 18, 14]); +} + +// Dry grass rooted between rows y0 and y1, leaning and shaking on the wind. +function grass(P, tint, y0, y1) { + const w = P.wind, gc = [226, 196, 116].map((v, c) => v * tint[c]), lw = 0.3 + 6 * w, gf = 1.6 + 2.5 * w; + for (const [bx, by, bh, ph, kk] of P.blades) { + if (by < y0 || by >= y1) continue; + const lean = (lw * (0.7 + 0.3 * sin(P.t * gf + bx * 0.22 + ph * 0.3)) * bh) / 8, n = Math.ceil(bh); + for (let s = 0, u; s <= n; s++) (u = s / n), set(bx + lean * u * u, by - u * bh, gc, 0.85, kk * (0.65 + 0.4 * u)); + } +} + +// What is in front of the giraffe. +function front(P, tint, land) { + const t = P.t, w = P.wind; + if (P.ext > 0) { + // Dark and long, out to the twig and curled round it. + const n = 14 * P.ext, tg = [70, 44, 116], tx = P.tx - 0.4, ty = P.ty - 1.2; + for (let s = 0; s <= n; s++) { + const u = s / 14, a = u / 0.75; + if (u < 0.75) set(mix(P.mx, tx, a), mix(P.my, ty, a) - sin(a * PI) * 1.2, tg); + else set(tx + 1.4 * sin((u - 0.75) * 12), ty + 0.84 * (1 - cos((u - 0.75) * 12)), tg); + } + } + // The oxpecker on the back of the neck. + const J = P.neck, s = P.ox * 12, n = min(11, floor(s)), f = s - n, j = n * 3, lift = 1.8 + mix(J[j + 2], J[j + 5], f) + sin(P.hop * PI) * 1.5; + const dx = J[j + 3] - J[j], dy = J[j + 4] - J[j + 1], dl = hyp(dx, dy) || 1, ax = dx / dl, ay = dy / dl; + const bx = mix(J[j], J[j + 3], f) + ay * lift, by = mix(J[j + 1], J[j + 4], f) - ax * lift, br = [80, 62, 48].map((v, c) => v * tint[c]); + set(bx, by, br), set(bx - ax, by - ay, br), set(bx - 2 * ax, by - 2 * ay, br, 1, 0.8); + set(bx - ax - ay * 0.9, by - ay + ax * 0.9, [196, 170, 120]), set(bx + ax, by + ay, [240, 40, 28]); + grass(P, tint, 117, H); + // Dust on the wind: a veil low in the sky and drifts over the plain. + const dc = land.map((v) => v * 1.15), sh = floor(t * (6 + 30 * w)); + for (let y = 36; y < H; y++) { + const ka = (0.1 + w) * 0.7 * sm(70, 112, y), kv = w * 0.3 * sm(36, 96, y); + for (let x = 0, a; x < W; x++) if ((a = kv + (y < 70 ? 0 : ka * cl(P.haze[(y - 70) * 128 + ((x - sh) & 127)] * 1.5 - 0.4, 0, 1))) > 0.01) set(x, y, dc, a); + } + for (const [x, y] of P.fall) set(x, y, P.leaf); +} + +export function draw(display, params) { + const P = params.g; + if (!P || P.coat < 0) return; + F = P.f; + const C = COATS[P.coat], hh = cl(P.h, 0, 1) * 3, i0 = min(2, floor(hh)), L = SKY[i0].map((v, c) => mix(v, SKY[i0 + 1][c], hh - i0)); + const tint = [9, 10, 11].map((c) => L[c] * L[12]), land = C.land.map((v, c) => v * tint[c]); + back(P, L, tint, land); + tree(P, tint); + grass(P, tint, 0, 117); + giraffe(P); + coat(P, C, tint); + front(P, tint, land); + // Whole numbers out, which the engine passes without boxing. + 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 506535d035ba1a98fc47679b482d9f76291be913 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 00:11:17 +0000 Subject: [PATCH 10/15] =?UTF-8?q?Skein=20=ED=8C=A8=ED=84=B4=EC=9D=84=20?= =?UTF-8?q?=EC=A7=A0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GhRE8HZxGnk63uCfeHdr92 --- incoming/pattern-study/patterns/skein.js | 596 +++++++++++++++++++++++ 1 file changed, 596 insertions(+) create mode 100644 incoming/pattern-study/patterns/skein.js diff --git a/incoming/pattern-study/patterns/skein.js b/incoming/pattern-study/patterns/skein.js new file mode 100644 index 000000000..35ea906e6 --- /dev/null +++ b/incoming/pattern-study/patterns/skein.js @@ -0,0 +1,596 @@ +// ===== Patternflow pattern ===== +// Title: Skein +// Family: bird +// Idea: Looking straight up at dusk as a skein of geese flies north over you in a ragged V, its tired leader now and then dropping back so another goose can take the point, and each bird turns black against the big full moon as it crosses. +// Author: patternflow-lab +// Date: 2026-10-23 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 23, after the V formation of migrating geese +// Round: 23 +// =============================== +// @matrix 64x128 +// @knobs Dusk=0..1, Speed=0.2..3, Geese=7..31, Wind=-1..1 +// +// Knob 1: Dusk. How late it is: at 0 the golden afterglow fills the top of the sky and the moon is pale, and towards 1 the glow sinks to a dim band and the moon and the stars carry a navy sky. +// Knob 2: Speed. The pace of everything: at 2 a skein crosses the panel in about ten seconds with its wings beating three times a second, and at 0.2 it all runs ten times slower. +// Knob 3: Geese. How many birds fly in the skein, from a small V of seven to a long one of thirty one whose arms reach most of the way down the panel. A big skein flies higher, so its birds look smaller. +// Knob 4: Wind. A crosswind from the side. The geese turn their bodies into it and crab across the sky, and the arm on the downwind side grows longer. The clouds drift with it too. +// Buttons: K1 tap startles the skein, and the V breaks into a honking tangle before it pulls itself back into shape. + +const W = 64; +const H = 128; +const N = W * H; +const { min, max, sin, cos, abs, hypot, exp, floor, round, PI } = Math; +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); +}; +// Ground speed of a skein at Speed 2, in LEDs a second. +const V0 = 12; +// The radius of the moon. +const MR = 11.5; +const MS = 27; + +// A fixed hash and smooth value noise, for the sky's haze and the clouds. +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.5 * noise(x, y, s) + 0.3 * noise(x * 2.1, y * 2.1, s + 7) + 0.2 * noise(x * 4.3, y * 4.3, s + 13); + +// Sky colours by how far a spot is from the afterglow, at dusk and at night. +const SKY0 = [ + [0, 255, 206, 120], + [0.1, 252, 162, 72], + [0.22, 238, 108, 82], + [0.34, 164, 74, 122], + [0.48, 74, 62, 146], + [0.66, 30, 44, 122], + [0.82, 14, 26, 84], + [0.96, 6, 12, 38], + [1.06, 3, 7, 20], + [2, 2, 5, 14], +]; +const SKY1 = [ + [0, 84, 62, 96], + [0.14, 30, 34, 88], + [0.4, 12, 22, 66], + [2, 6, 13, 48], +]; +// The undersides of clouds, lit from below by the sun that has set. +const CLOUD0 = [ + [0, 255, 232, 176], + [0.2, 255, 176, 112], + [0.4, 252, 124, 128], + [0.65, 196, 96, 156], + [2, 126, 78, 146], +]; +const C3 = [0, 0, 0]; +function ramp(P, e, out) { + let i = 0; + while (i < P.length - 2 && e > P[i + 1][0]) i++; + const a = P[i]; + const b = P[i + 1]; + const t = clamp((e - a[0]) / (b[0] - a[0]), 0, 1); + out[0] = a[1] + (b[1] - a[1]) * t; + out[1] = a[2] + (b[2] - a[2]) * t; + out[2] = a[3] + (b[3] - a[3]) * t; + return out; +} +// How far from the afterglow, 0 at the top right corner (north west) to 1 at the bottom left. +const glowDist = (x, y) => (y + 0.3 * (63 - x)) / 136; + +// A cloud: a soft patch of density, drifting sideways and coming round again. +function newCloud(c, y, x) { + c.w = 30 + floor(Math.random() * 16); + c.h = 9 + floor(Math.random() * 6); + c.y = y; + c.x = x; + c.m = new Float32Array(c.w * c.h); + const s = floor(Math.random() * 9999); + for (let j = 0; j < c.h; j++) + for (let i = 0; i < c.w; i++) { + const dx = (i + 0.5) / c.w - 0.5; + const dy = (j + 0.5) / c.h - 0.5; + const env = 1 - 4 * dx * dx - 4 * dy * dy; + const n = fbm(i * 0.2, j * 0.32, s); + c.m[j * c.w + i] = sstep(0.1, 0.55, env * 0.8 + (n - 0.45) * 1.2); + } + return c; +} + +function newBird(S, x, y) { + const b = { x, y, vx: 0, vy: -V0, hd: 0, gs: 0, ph: Math.random() * 6.28, j: Math.random() * 99, drop: 0 }; + S.birds.push(b); + return b; +} + +// A skein is a leader and two arms, each an ordered list of birds from the front. +function newSkein(S, n, x, y, fast) { + const hd = -0.34 * S.wind; + const vx = V0 * fast * sin(hd) + 9 * S.wind; + const vy = -V0 * fast * cos(hd); + const k = { x, y, hd, vx, vy, L: [], R: [], fast, want: n, rot: 3.2 + Math.random() * 2, side: Math.random() < 0.5 ? -1 : 1, tangle: 0, join: null, flap: Math.random() * 6 }; + k.lead = newBird(S, x, y); + for (let i = 1; i < n; i++) (i & 1 ? k.L : k.R).push(newBird(S, x, y)); + for (const b of [k.lead, ...k.L, ...k.R]) { + b.vx = vx; + b.vy = vy; + } + S.skeins.push(k); + place(S, k, true); + return k; +} + +// Where each bird of a skein wants to be. With snap the birds are put there. +function place(S, k, snap) { + const n = 1 + k.L.length + k.R.length; + const t = S.t; + // A big skein flies higher, so its birds look smaller and sit closer. + k.gs = clamp(1.05 - 0.013 * (n - 7), 0.74, 1.05); + const sp = k.gs * (10.2 - 0.07 * (n - 7)) * (1 + 0.04 * sin(t * 0.29 + k.flap)); + const a0 = max(0.31, 0.64 - 0.02 * (n - 7)) * (1 + 0.07 * sin(t * 0.37 + k.flap)); + const h = k.hd; + const fx = sin(h); + const fy = -cos(h); + const set = (b, tx, ty) => { + // Each bird wanders a little round its slot, so the V is never quite straight. + tx += 0.9 * sin(t * 0.7 + b.j) + k.tangle * 13 * sin(t * 1.3 + b.j * 3.1); + ty += 1.3 * sin(t * 0.53 + b.j * 1.7) + k.tangle * 11 * sin(t * 1.1 + b.j * 2.3); + b.tx = tx; + b.ty = ty; + if (snap) { + b.x = tx; + b.y = ty; + } + }; + // Startled, the whole skein bunches round its middle. + const cx = k.x - fx * 12 * k.tangle; + const cy = k.y - fy * 12 * k.tangle; + set(k.lead, k.x + (cx - k.x) * k.tangle, k.y + (cy - k.y) * k.tangle); + for (const s of [-1, 1]) { + const arm = s < 0 ? k.L : k.R; + // The downwind arm opens a little wider. + const a = a0 * (1 + 0.2 * s * S.wind); + const sx = s * sin(a); + const bk = cos(a); + for (let r = 0; r < arm.length; r++) { + const b = arm[r]; + const d = (r + 1) * sp; + // Right of the heading is (-fy, fx); back is (-fx, -fy). + let px = k.x + d * (sx * -fy - bk * fx); + let py = k.y + d * (sx * fx - bk * fy); + // A bird moving back along the arm keeps to its outside. + px += s * 4 * b.drop * cos(h); + py += s * 4 * b.drop * sin(h); + const u = k.tangle; + set(b, px + (cx - px) * u, py + (cy - py) * u); + } + } +} + +function spawn(S, first) { + const n = S.n; + const x0 = 32 - S.wind * 30 + (first ? 0 : (Math.random() - 0.5) * 14); + const ext = ((n - 1) / 2) * (8.6 - 0.11 * (n - 7)); + const y0 = first ? max(50, 110 - ext) : 140; + const k = newSkein(S, n, x0, y0, 1); + // The second, smaller skein comes on behind, and most times it catches up and joins. + const m = max(3, round(n * 0.3)); + const joins = first || Math.random() < 0.65; + const off = joins ? (Math.random() - 0.5) * 10 : (Math.random() < 0.5 ? -1 : 1) * 17; + const k2 = newSkein(S, m, x0 + off, max(150, y0 + ext + 40), joins ? 1.6 : 1.04); + k2.join = joins ? k : null; + k.next = first ? 4.5 : 8 + Math.random() * 3; +} + +export function setup(params) { + const S = { + t: 0, + birds: [], + skeins: [], + sky: new Float32Array(N * 3), + buf: new Float32Array(N * 3), + A: new Float32Array(N), + e: new Float32Array(N), + haze: new Float32Array(N), + dk: -1, + moon: new Float32Array(MS * MS), + cov: new Float32Array(MS * MS), + stars: [], + clouds: [], + n: 7, + wind: 0.06, + dusk: 0, + started: false, + }; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + S.e[y * W + x] = glowDist(x, y); + S.haze[y * W + x] = 0.94 + 0.12 * fbm(x * 0.06, y * 0.035, 5); + } + // The moon's face: a full disc, darker seas on it and a darker rim. + const seas = [ + [-3.5, -3.5, 4.6, 3.8], + [2.5, -3.2, 2.8, 2.6], + [4.2, 1.2, 3.2, 3.4], + [-6.2, 1.5, 3, 5.5], + [-0.5, 4.5, 3, 1.8], + ]; + for (let j = 0; j < MS; j++) + for (let i = 0; i < MS; i++) { + const dx = i - 13; + const dy = j - 13; + const r = hypot(dx, dy); + const cov = clamp(MR + 0.5 - r, 0, 1); + let sea = 0; + for (const [sx, sy, rx, ry] of seas) sea = max(sea, sstep(1.15, 0.2, hypot((dx - sx) / rx, (dy - sy) / ry))); + const alb = 1 - 0.15 * sea - 0.12 * (r / MR) ** 2 - 0.05 * noise(i * 0.7, j * 0.7, 3); + S.moon[j * MS + i] = alb; + S.cov[j * MS + i] = cov; + } + for (let i = 0; i < 90; i++) S.stars.push({ x: floor(Math.random() * W), y: floor(Math.random() * H), m: Math.random() ** 2.2, p: Math.random() * 6.28 }); + S.clouds.push(newCloud({}, 30, 4), newCloud({}, 68, 26), newCloud({}, 112, -6)); + params.s = S; +} + +function stepBirds(S, dt) { + const ws = S.wind; + for (const k of S.skeins) { + // The geese head a little into the wind and crab across it. + k.hd += (-0.34 * ws - k.hd) * min(1, dt * 1.5); + const v = V0 * k.fast; + const vx = v * sin(k.hd) + 9 * ws; + const vy = -v * cos(k.hd); + k.vx = vx; + k.vy = vy; + k.x += vx * dt; + k.y += vy * dt; + k.flap += dt * 2 * PI * 2.9 * (1 + 0.7 * k.tangle); + if (k.tangle > 0) { + k.tangle -= dt * 0.33; + if (k.tangle <= 0) { + // Reformed, though not always in the same order. + k.tangle = 0; + const all = [k.lead, ...k.L, ...k.R].sort(() => Math.random() - 0.5); + k.lead = all[0]; + k.L = []; + k.R = []; + for (let i = 1; i < all.length; i++) (i & 1 ? k.L : k.R).push(all[i]); + } + } + // The leader tires, drops back along one arm, and the next bird up takes the point. + k.rot -= dt; + if (k.rot <= 0 && !k.tangle) { + k.rot = 6 + Math.random() * 3; + k.side = -k.side; + const arm = k.side < 0 ? k.L : k.R; + if (arm.length) { + const old = k.lead; + k.lead = arm.shift(); + arm.push(old); + old.drop = 1; + } + } + // Keep the arms in the split the wind asks for, one bird at a time. + const m = k.L.length + k.R.length; + const wantR = round(m * clamp(0.5 + 0.36 * ws, 0.1, 0.9)); + k.bal = (k.bal || 0) - dt; + if (k.bal <= 0 && wantR !== k.R.length && !k.tangle) { + k.bal = 0.5; + if (wantR > k.R.length && k.L.length) k.R.push(k.L.pop()); + else if (k.R.length) k.L.push(k.R.pop()); + } + // The second skein joins the tail of the first when it has caught up. + if (k.join) { + const j = k.join; + const tail = [j.lead, ...j.L, ...j.R].reduce((a, b) => (b.y > a.y ? b : a)); + if (j.gone || k.y < tail.y + 10) { + const add = [k.lead, ...k.L, ...k.R]; + if (!j.gone) { + for (const b of add) (j.L.length <= j.R.length ? j.L : j.R).push(b); + j.want += add.length; + k.L = []; + k.R = []; + k.lead = null; + k.gone = true; + } + k.join = null; + } + } + } + S.skeins = S.skeins.filter((k) => !k.gone); + // When a skein is high on the panel, the next one comes in from the bottom. + for (const k of S.skeins) + if (k.next !== undefined) { + k.next -= dt; + if (k.next <= 0) { + k.next = undefined; + spawn(S, false); + } + } + // The Geese knob adds birds to the tail of the newest skein, or lets some fall away. + const main = S.skeins.filter((k) => k.join === null && k.fast === 1).pop(); + if (main) { + if (S.lastN !== S.n) main.want = max(3, main.want + S.n - (S.lastN || S.n)); + S.lastN = S.n; + S.adj = (S.adj || 0) - dt; + const n = 1 + main.L.length + main.R.length; + if (S.adj <= 0 && n !== main.want) { + S.adj = 0.15; + const arm = main.L.length <= main.R.length ? main.L : main.R; + if (n < main.want) { + const tail = arm.length ? arm[arm.length - 1] : main.lead; + arm.push(newBird(S, tail.x, max(tail.y + 18, 136))); + } else { + const long = main.L.length > main.R.length ? main.L : main.R; + const b = long.pop(); + b.stray = long === main.L ? -1 : 1; + } + } + } + for (const k of S.skeins) place(S, k, false); + // Each bird springs towards its slot and matches the skein's speed. + for (const k of S.skeins) { + const all = [k.lead, ...k.L, ...k.R]; + for (let r = 0; r < all.length; r++) { + const b = all[r]; + b.sk = k; + b.rank = r === 0 ? 0 : (r + 1) >> 1; + } + } + const om = 2.2; + for (const b of S.birds) { + const k = b.sk; + if (b.stray) { + // A bird that leaves falls back and peels away to its side. + b.vx += (b.stray * 6 + 9 * ws - b.vx) * dt; + b.vy += (-V0 * 0.4 - b.vy) * dt; + b.hd += (b.stray * 0.4 - b.hd) * dt; + } else if (k) { + const ex = b.tx - b.x; + const ey = b.ty - b.y; + const ax = om * om * ex - 2 * om * (b.vx - k.vx); + const ay = om * om * ey - 2 * om * (b.vy - k.vy); + b.vx += ax * dt; + b.vy += ay * dt; + // Not faster than a goose can close up. + const rx = b.vx - k.vx; + const ry = b.vy - k.vy; + const rs = hypot(rx, ry); + if (rs > 13) { + b.vx = k.vx + (rx * 13) / rs; + b.vy = k.vy + (ry * 13) / rs; + } + if (b.drop > 0 && hypot(ex, ey) < 2) b.drop = max(0, b.drop - dt); + // Bodies point along the skein's heading, banking a little as they shift. + const want = k.hd + clamp(0.03 * rx, -0.4, 0.4) + k.tangle * 0.6 * sin(S.t * 2.3 + b.j); + b.hd += (want - b.hd) * min(1, dt * 4); + b.gs = b.gs ? b.gs + (k.gs - b.gs) * min(1, dt) : k.gs; + // Wings beat out of step down each arm. + const tp = k.flap - b.rank * 0.9 + k.tangle * b.j; + b.ph += dt * 2 * PI * 2.9 * (1 + 0.7 * k.tangle) + 0.08 * sin(tp - b.ph); + } + b.x += b.vx * dt; + b.y += b.vy * dt; + if (b.stray) b.ph += dt * 2 * PI * 2.9; + if (b.y < -14 || b.y > 190 || b.x < -50 || b.x > 114) b.out = true; + } + // A skein whose birds have all flown off the top is gone. + for (const k of S.skeins) { + const all = [k.lead, ...k.L, ...k.R]; + if (all.every((b) => b.y < -14)) k.gone = true; + } + S.skeins = S.skeins.filter((k) => !k.gone); + S.birds = S.birds.filter((b) => !(b.out && (b.stray || !b.sk || b.sk.gone))); +} + +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.dusk = clamp(k[0], 0, 1); + const tempo = clamp(k[1], 0.2, 3) / 2; + S.n = clamp(round(k[2]), 7, 31); + S.wind = clamp(k[3], -1, 1); + if (!S.started) { + S.started = true; + spawn(S, true); + } + const d = dt * tempo; + S.t += d; + if (input.btnPressed && input.btnPressed[0]) + for (const sk of S.skeins) { + sk.tangle = 1; + for (const b of [sk.lead, ...sk.L, ...sk.R]) { + b.vx += (Math.random() - 0.5) * 30; + b.vy += (Math.random() - 0.5) * 24; + } + } + stepBirds(S, d); + // Clouds drift with the wind high up and come round again. + const cv = (2.3 + 5 * S.wind) * d; + for (const c of S.clouds) { + c.x += cv; + if (c.x > W + 2) newCloud(c, 14 + Math.random() * 100, -c.w - 2 - Math.random() * 10); + else if (c.x < -c.w - 14) newCloud(c, 14 + Math.random() * 100, W + 2); + } + // The moon climbs a little as the night goes on. + S.mx = 19 + 4 * S.dusk; + S.my = 104 - 22 * S.dusk - 5 * (1 - exp(-S.t / 50)); +} + +function paintSky(S) { + const dk = S.dusk; + const w = sstep(0.2, 1, dk); + const g = 1 + 1.8 * dk; + const c0 = [0, 0, 0]; + const c1 = [0, 0, 0]; + for (let i = 0; i < N; i++) { + const e = S.e[i]; + ramp(SKY0, e * g, c0); + ramp(SKY1, e, c1); + const hz = S.haze[i]; + S.sky[i * 3] = (c0[0] + (c1[0] - c0[0]) * w) * hz; + S.sky[i * 3 + 1] = (c0[1] + (c1[1] - c0[1]) * w) * hz; + S.sky[i * 3 + 2] = (c0[2] + (c1[2] - c0[2]) * w) * hz; + } + S.dk = dk; +} + +// A tapered stroke into the geese's coverage, kept as the larger of what is there. +function stroke(A, ax, ay, bx, by, ra, rb) { + 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 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 t = clamp(((x - ax) * dx + (y - ay) * dy) / ll, 0, 1); + const d = hypot(x - ax - dx * t, y - ay - dy * t); + const a = clamp(ra + (rb - ra) * t + 0.5 - d, 0, 1); + const i = y * W + x; + if (a > A[i]) A[i] = a; + } +} + +// A goose seen from below: neck stretched forward, wings beating at its sides. +function goose(A, b) { + const x = b.x; + const y = b.y; + if (x < -9 || x > W + 8 || y < -9 || y > H + 8) return; + const fx = sin(b.hd); + const fy = -cos(b.hd); + const P = (u, v) => [x - fy * u + fx * v, y + fx * u + fy * v]; + const g = b.gs || 1; + const s = sin(b.ph); + const up = max(0, cos(b.ph)); + // Seen from below, a wing raised or lowered is foreshortened, and on the + // upstroke it bends at the wrist and sweeps its tip back. + const th = 0.9 * s; + const span = g * (0.8 + 5.2 * cos(th) * (1 - 0.18 * up)); + const tipv = g * (-0.7 - 1.7 * up + 0.5 * s); + const head = P(0, 5.1 * g); + const neck = P(0, 1.6 * g); + const chest = P(0, 0.9 * g); + const tail = P(0, -3.5 * g); + stroke(A, chest[0], chest[1], tail[0], tail[1], 1.25 * g, 0.55 * g); + stroke(A, neck[0], neck[1], head[0], head[1], max(0.45, 0.48 * g), max(0.55, 0.7 * g)); + for (const sd of [-1, 1]) { + const sh = P(sd * 0.8 * g, 0.8 * g); + const wr = P(sd * (0.8 * g + 0.45 * (span - 0.8 * g)), g * (0.9 - 0.5 * up)); + const tp = P(sd * span, tipv); + stroke(A, sh[0], sh[1], wr[0], wr[1], 1.0 * g, 0.75 * g); + stroke(A, wr[0], wr[1], tp[0], tp[1], 0.75 * g, 0.35 * g); + } +} + +export function draw(display, params, time) { + const S = params.s; + if (!S || !S.started) return; + if (abs(S.dk - S.dusk) > 0.002) paintSky(S); + const dk = S.dusk; + const { sky, buf, A } = S; + buf.set(sky); + // Stars come out where the sky is dark enough to show them. + const night = 150 + 200 * sstep(0.1, 0.9, dk); + for (const st of S.stars) { + const i = st.y * W + st.x; + const L = max(sky[i * 3], sky[i * 3 + 1], sky[i * 3 + 2]); + const v = clamp((st.m * night - L) / 60, 0, 1) * (0.75 + 0.25 * sin(S.t * 3.1 + st.p * 7)); + buf[i * 3] += 200 * v; + buf[i * 3 + 1] += 210 * v; + buf[i * 3 + 2] += 230 * v; + } + // The moon, pale in the dusk and bright at night with a ring of glow round it. + const mx = S.mx; + const my = S.my; + const mb = 0.8 + 0.2 * dk; + const halo = 0.15 + 0.6 * dk; + const mc = [255 * mb, 246 * mb, 222 * mb]; + for (let y = max(0, floor(my - 34)); y < min(H, my + 34); y++) + for (let x = max(0, floor(mx - 34)); x < min(W, mx + 34); x++) { + const i = (y * W + x) * 3; + const r = hypot(x - mx, y - my); + const gl = halo * 60 * exp(-max(0, r - MR) / 7); + buf[i] += gl * 0.75; + buf[i + 1] += gl * 0.8; + buf[i + 2] += gl; + const ix = round(x - mx + 13); + const iy = round(y - my + 13); + if (ix < 0 || iy < 0 || ix >= MS || iy >= MS) continue; + const cv = S.cov[iy * MS + ix]; + if (cv <= 0) continue; + const m = S.moon[iy * MS + ix]; + buf[i] += (mc[0] * m - buf[i]) * cv; + buf[i + 1] += (mc[1] * m - buf[i + 1]) * cv; + buf[i + 2] += (mc[2] * m - buf[i + 2]) * cv; + } + // Clouds, their undersides lit from below by the afterglow, or grey by the moon at night. + const lw = sstep(0.05, 0.85, dk); + const g = 1 + 1.8 * dk; + for (const c of S.clouds) { + const cx = round(c.x); + const cy = round(c.y); + for (let j = 0; j < c.h; j++) { + const y = cy + j; + if (y < 0 || y >= H) continue; + for (let i = 0; i < c.w; i++) { + const x = cx + i; + if (x < 0 || x >= W) continue; + const dn = c.m[j * c.w + i]; + if (dn <= 0) continue; + const p = y * W + x; + ramp(CLOUD0, S.e[p] * g, C3); + const mn = exp(-hypot(x - mx, y - my) / 30); + const lr = C3[0] + (40 + 60 * mn - C3[0]) * lw; + const lg = C3[1] + (46 + 62 * mn - C3[1]) * lw; + const lb = C3[2] + (70 + 70 * mn - C3[2]) * lw; + const br = 0.5 + 0.5 * dn; + const q = p * 3; + const o = 0.9 * dn; + buf[q] += (lr * br - buf[q]) * o; + buf[q + 1] += (lg * br - buf[q + 1]) * o; + buf[q + 2] += (lb * br - buf[q + 2]) * o; + } + } + } + // The geese, dark against everything behind them. + A.fill(0); + for (const b of S.birds) goose(A, b); + const gr = 6; + const gg = 5; + const gb = 10; + for (let p = 0; p < N; p++) { + const a = A[p]; + const q = p * 3; + let r = buf[q]; + let gn = buf[q + 1]; + let bl = buf[q + 2]; + if (a > 0) { + r += (gr - r) * a; + gn += (gg - gn) * a; + bl += (gb - bl) * a; + } + display.setPixel(p % W, (p / W) | 0, min(255, r) | 0, min(255, gn) | 0, min(255, bl) | 0); + } +} From 57450b19bc0dfa48b7f2a3d7f5f8d87b11840183 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 00:12:25 +0000 Subject: [PATCH 11/15] =?UTF-8?q?Elephant=20Toothpaste=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=EC=9D=84=20=EC=A7=A0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GhRE8HZxGnk63uCfeHdr92 --- .../patterns/elephant-toothpaste.js | 517 ++++++++++++++++++ 1 file changed, 517 insertions(+) create mode 100644 incoming/pattern-study/patterns/elephant-toothpaste.js diff --git a/incoming/pattern-study/patterns/elephant-toothpaste.js b/incoming/pattern-study/patterns/elephant-toothpaste.js new file mode 100644 index 000000000..3e1c8102b --- /dev/null +++ b/incoming/pattern-study/patterns/elephant-toothpaste.js @@ -0,0 +1,517 @@ +// ===== Patternflow pattern ===== +// Title: Elephant Toothpaste +// Family: chemistry +// Idea: A gloved hand tips potassium iodide into a tall graduated cylinder of peroxide and dish soap striped inside with food colouring, and the fat column of striped foam that shoots out of its mouth climbs to the top of the panel before it folds over and slumps steaming into a heap on the lab bench. +// Author: patternflow-lab +// Date: 2026-10-23 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 23, after the elephant toothpaste demonstration of the school chemistry lab +// Round: 23 +// =============================== +// @matrix 64x128 +// @knobs Dye=0..1, Speed=0.2..3, Stripes=1..4, Strength=0..1 +// +// Knob 1: Dye. The food colours striped down the glass, all turned round the colour wheel together. Blue and yellow at 0 become orange and blue at 0.5 and come back round at 1. +// Knob 2: Speed. How fast the demonstration runs: at 2 the foam leaves the mouth a second after the catalyst goes in and a round from fresh cylinder to wiped bench takes 18.6 s; at 0.2 it all runs ten times slower. +// Knob 3: Stripes. How many dye colours run down the glass and so through the foam: one blue against white foam at 1, up to four colours at 4. +// Knob 4: Strength. How much stronger than the lab-grade 30% peroxide the bottle is: at 0 the column takes half a second to reach the top and stands about 25 LEDs wide, at 1 it gets there in a sixth of a second at 37 wide and leaves a taller heap. +// Buttons: K1 tap pours the catalyst now, or once the foam has finished, wipes the bench and sets down a fresh cylinder. + +const W = 64; +const H = 128; +const N = W * H; +// The cylinder's mouth and the bench top, in rows, and the liquid's depth below the mouth. +const MY = 52; +const BENCH = 114; +const LIQ = 28; +// The round at Speed 2, in seconds from the moment a fresh cylinder comes down. +const T_DOWN = 0.9; +const T_OUT = 1.4; +const T_IN = 2.2; +const T_POUR = 2.45; +const T_DRY = 3.05; +const T_BACK = 3.5; +const T_FROTH = 2.7; +const T_ERUPT = 3.4; +const T_STOP = 8.4; +const T_FOLD = 10; +const T_SLUMP = 12; +const T_WIPE = 16; +const T_LIFT = 17.6; +const T_END = 18.6; +// The study opens with the hand on its way in with the beaker. +const OPEN = 1.6; +const { min, max, floor, ceil, round, sin, cos, exp, sqrt, abs, asin, log, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ss = (a, b, v) => { + const t = cl((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; +const hash = (i) => { + let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; +// The dye hues before the Dye knob turns them: blue, yellow, pink, green. +const HUES = [0.62, 0.15, 0.93, 0.38]; +const WHITE = [255, 250, 240]; +// The light comes from the upper left and a little in front. +const LX = -0.48; +const LY = -0.56; +const LZ = 0.67; + +function hsv(h, s, v) { + const f = (n) => { + const k = (n + h * 6) % 6; + return 255 * (v - v * s * max(0, min(k, 4 - k, 1))); + }; + return [f(5), f(3), f(1)]; +} + +export function setup(params) { + const F = (n) => new Float32Array(n); + const S = { bub: F(N), stn: F(N), bg: F(N * 3), R: F(N), G: F(N), B: F(N) }; + Object.assign(S, { q: F(N), nx: F(N), ny: F(N), u: F(N), s: F(N), tx: F(N), hs: F(W), len: [], c: OPEN, t: 0 }); + // Bubbles: a few cells about 4 LEDs across with darker walls, and finer + // grain over them, on a tile that wraps both ways to slide with the foam. + const px = []; + const py = []; + for (let k = 0; k < 512; k++) { + px.push((k & 15) * 4 + 4 * hash(k * 2)); + py.push((k >> 4) * 4 + 4 * hash(k * 2 + 1)); + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let f1 = 99; + let f2 = 99; + let best = 0; + for (let j = -1; j <= 1; j++) + for (let i = -1; i <= 1; i++) { + const k = (((y >> 2) + j + 32) % 32) * 16 + (((x >> 2) + i + 16) % 16); + let dx = abs(x + 0.5 - px[k]); + let dy = abs(y + 0.5 - py[k]); + dx = min(dx, 64 - dx); + dy = min(dy, 128 - dy); + const d = sqrt(dx * dx + dy * dy); + if (d < f1) { + f2 = f1; + f1 = d; + best = k; + } else if (d < f2) f2 = d; + } + S.bub[y * W + x] = 0.74 + 0.2 * min(1, (f2 - f1) / 1.3) + 0.08 * hash(best + 777) + 0.1 * hash(y * W + x + 5000); + // Steam: a smooth field that also wraps. + const a = (2 * PI * x) / 64; + const b = (2 * PI * y) / 128; + const v = sin(a + 2 * b + 1.3) + 0.7 * sin(-2 * a + 3 * b + 4.1) + 0.5 * sin(3 * a + 5 * b + 2.2) + 0.35 * sin(5 * a - 7 * b + 5.7); + S.stn[y * W + x] = 0.5 + v / 5.1; + } + // The dark lab: a tiled wall the panel barely shows over a bench whose + // black top is lit along its front edge, with a wooden cupboard under it. + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let r; + let g; + let b; + if (y < BENCH) { + const v = (x % 8 === 0 || y % 8 === 7 ? 6 : 11 + 3 * hash(((y >> 3) << 3) + (x >> 3))) * (0.7 + 0.4 * exp(-(((x - 32) / 30) ** 2)) * (y / BENCH)); + r = v * 0.8; + g = v * 0.95; + b = v * 1.3; + } else if (y < BENCH + 3) { + const v = y === BENCH + 2 ? 2.1 : 1 + 0.08 * (y - BENCH); + r = 40 * v; + g = 43 * v; + b = 48 * v; + } else { + const door = x === 31 || x === 32 || y === 126 || y === 127; + const knob = (x === 28 || x === 35) && y > 119 && y < 122; + const v = door ? 0.5 : knob ? 2.4 : 1 - 0.02 * (y - 117); + r = 44 * v; + g = 31 * v; + b = 21 * v; + } + S.bg[(y * W + x) * 3] = r; + S.bg[(y * W + x) * 3 + 1] = g; + S.bg[(y * W + x) * 3 + 2] = b; + } + params.S = S; + fresh(S); +} + +// A fresh cylinder brings new drips of dye and a new heap. The side the +// column will fall to is picked here too. +function fresh(S) { + const rnd = Math.random; + for (let k = 0; k < 10; k++) S.len[k] = LIQ + 2 - floor(rnd() * 13); + S.side = rnd() < 0.5 ? -1 : 1; + S.ph = [rnd() * 6.3, rnd() * 6.3, rnd() * 6.3]; + for (let x = 0; x < W; x++) { + const d = (x - 32) / 27; + const h = exp(-d * d * 1.6) * (0.78 + 0.1 * sin(x * 0.45 + S.ph[0]) + 0.07 * sin(x * 1.1 + S.ph[1])); + S.hs[x] = max(0, h + 0.22 * exp(-(((x - 22.5) / 5) ** 2)) + 0.22 * exp(-(((x - 41.5) / 5) ** 2))); + } +} + +export function update(dt, input, params) { + if (!params.S) setup(params); + const S = params.S; + const k = input.knobValues || [0, 2, 1, 0.06]; + dt = cl(dt || 0, 0, 0.1); + S.dye = cl(k[0], 0, 1); + const rate = cl(k[1], 0.2, 3) / 2; + S.n = cl(round(k[2]), 1, 4); + S.st = cl(k[3], 0, 1); + S.bands = 2 * S.n + 2; + S.pal = HUES.map((h) => hsv((h + S.dye) % 1, 0.88, 1)); + if (input.btnPressed && input.btnPressed[0]) { + if (S.c < T_IN) S.c = T_IN; + else if (S.c >= T_SLUMP && S.c < T_WIPE) S.c = T_WIPE; + } + S.c += dt * rate; + S.t += dt * rate; + if (S.c >= T_END) { + S.c -= T_END; + fresh(S); + } +} + +// One LED of foam, its bubbles lit by the lamp on the rounded surface and +// coloured by the dye band it falls in. b is the band coordinate, s how far +// the foam has moved. +function foam(S, i, nx, ny, nz, b, s, tx, dim, a, sat) { + const tex = S.bub[((s | 0) & 127) * 64 + ((tx | 0) & 63)]; + const lit = max(0, LX * nx + LY * ny + LZ * nz); + const sh = (0.16 + 1.1 * lit + 0.25 * lit ** 8) * tex * dim; + b += 0.3 * (tex - 0.88); + const m = cl((cos(2 * PI * b) - 0.05) * 2.5, 0, 1) * sat; + const n = S.n; + const c = S.pal[(((floor(b + 0.5) % n) + n) % n)]; + const o = 1 - a; + S.R[i] = S.R[i] * o + (WHITE[0] + (c[0] - WHITE[0]) * m) * sh * a; + S.G[i] = S.G[i] * o + (WHITE[1] + (c[1] - WHITE[1]) * m) * sh * a; + S.B[i] = S.B[i] * o + (WHITE[2] + (c[2] - WHITE[2]) * m) * sh * a; +} + +// A disc of a foam tube. Where discs overlap, the LED keeps the one it sits +// deepest in, so a row of them makes a soft round-ended tube. +function stamp(S, cx, cy, tx, ty, w, s0, asp = 1) { + const { q, nx, ny, u, s } = S; + const iw = 1 / w; + const y1 = min(H - 1, ceil(cy + w / asp)); + const x1 = min(W - 1, ceil(cx + w)); + for (let y = max(0, floor(cy - w / asp)); y <= y1; y++) { + const dy = (y - cy) * iw * asp; + for (let x = max(0, floor(cx - w)); x <= x1; x++) { + const dx = (x - cx) * iw; + const d = dx * dx + dy * dy; + const i = y * W + x; + if (d < 1 && d < q[i]) { + q[i] = d; + nx[i] = dx; + ny[i] = dy; + u[i] = -dx * ty + dy * tx; + s[i] = s0 - (dx * tx + dy * ty) * w * 0.8; + S.tx[i] = u[i] * w; + } + } + } +} + +function cylinder(S, oy, top, M, dim, xs) { + const { R, G, B, pal, n, len } = S; + for (let ly = -1; ly < 62; ly++) { + const y = MY + ly + oy; + if (y < 0 || y >= H) continue; + for (let x = 16; x <= 48; x++) { + const i = y * W + x; + let r = -1; + let g = 0; + let b = 0; + if (ly <= 0) { + if ((ly === 0 && x >= 23 && x <= 41) || (ly < 0 && x >= 21 && x <= 24)) [r, g, b] = [120, 136, 150]; + } else if (ly < 55) { + if (x === 24 || x === 40) { + const v = x === 24 ? 1 : 0.7; + [r, g, b] = [58 * v, 74 * v, 90 * v]; + } else if (x > 24 && x < 40) { + if (ly >= top) { + // Foam that has filled the glass, seen through it. + const u = (x - 32) / 7.6; + foam(S, i, u, 0, sqrt(1 - u * u), (asin(u) / (2 * PI)) * S.bands + 0.022 * (M + ly), M + ly, x, 0.72 * dim, 1, 1); + [r, g, b] = [R[i], G[i], B[i]]; + } else if (ly >= LIQ) { + const v = ly === LIQ ? 2.4 : 1 - (ly - LIQ) * 0.012; + [r, g, b] = [25 * v, 38 * v, 46 * v]; + // The dye that has run down into the liquid spreads in it. + for (let k = 0; k < S.bands; k++) { + if (len[k] < LIQ) continue; + const sp = 0.8 + (ly - LIQ) * 0.18; + const e = 0.4 * exp(-(((x - xs[k]) / sp) ** 2) - (ly - LIQ) / 10); + const c = pal[k % n]; + r += c[0] * e; + g += c[1] * e; + b += c[2] * e; + } + } else [r, g, b] = [15, 20, 26]; + if (ly < top && ly >= 2) + for (let k = 0; k < S.bands; k++) { + if (ly > len[k] || abs(x - xs[k]) > (ly >= len[k] - 1 ? 1.1 : 0.7)) continue; + const c = pal[k % n]; + const v = xs.front[k] ? 0.85 : 0.35; + [r, g, b] = [c[0] * v, c[1] * v, c[2] * v]; + } + // The graduations, and a glint down the near side of the glass. + const tick = ly >= 6 && ly <= 50 && ly % 4 === 2 && x >= (ly % 16 === 2 ? 26 : 27) && x <= (ly % 16 === 2 ? 31 : 29); + const glint = x === 26 ? 22 : 0; + r += (tick ? 60 : 0) + glint; + g += (tick ? 62 : 0) + glint; + b += (tick ? 64 : 0) + glint; + } + } else if (ly < 57) { + if (x >= 24 && x <= 40) [r, g, b] = [70, 86, 100]; + } else if (abs(x - 32) <= 11 + (ly - 57) * 1.1) { + const v = ly === 57 ? 1.5 : 1 - (ly - 58) * 0.08; + [r, g, b] = [34 * v, 54 * v, 82 * v]; + } + if (r >= 0) { + R[i] = r; + G[i] = g; + B[i] = b; + } + } + } +} + +const GLOVE = [116, 82, 230]; +// A gloved hand in its lab-coat sleeve. Kind 1 holds the catalyst beaker and +// kind 2 presses a paper towel to the bench. Kind 3 grips the cylinder. +function hand(S, kind, hx, hy, a, pour) { + const { R, G, B } = S; + const ca = cos(a); + const sa = sin(a); + const wx = hx + ca * 10 - sa * 0.5; + const wy = hy + sa * 10 + ca * 0.5; + const slope = kind === 2 ? -0.45 : -0.12; + // The beaker's liquid stays level as it tips; pour runs 0 to 1 as it empties. + const lipx = hx + ca * -5.6 - sa * -6.6; + const lipy = hy + sa * -5.6 + ca * -6.6; + const surf = hy - 1 + (lipy + 0.5 + pour * 7 - hy + 1) * ss(-0.1, -0.9, a); + for (let y = max(0, floor(min(hy, wy) - 14)); y <= min(H - 1, ceil(max(hy, wy) + 14)); y++) + for (let x = max(0, floor(hx - 13)); x < W; x++) { + const i = y * W + x; + const dx = x - hx; + const dy = y - hy; + const lx = ca * dx + sa * dy; + const ly = -sa * dx + ca * dy; + let col = null; + let v = 1; + if (x >= wx - 1) { + const t = (y - wy - (x - wx) * slope) / (x < wx + 7 ? 4.3 : 5.8); + if (abs(t) < 1) { + v = 0.4 + 0.6 * (1 - t * t) - 0.15 * t; + col = x < wx + 7 ? GLOVE : x < wx + 8 ? [120, 120, 116] : [175, 175, 168]; + } + } + if (kind === 1 && abs(lx) <= 5.6 && abs(ly) <= 6.6) { + v = 1; + if (ly < -5.6) col = [125, 140, 155]; + else if (abs(lx) > 4.6 || ly > 5.6) col = [70, 90, 105]; + else if (y > surf) col = [190, 150, 60]; + else col = [R[i] * 0.7 + 12, G[i] * 0.7 + 14, B[i] * 0.7 + 16]; + } else if (kind === 1 && lx >= -7 && lx < -5.6 && ly >= -7.2 && ly < -5.6) col = [125, 140, 155]; + if (kind === 2) { + const e = ((lx + 0.5) / 9.5) ** 2 + ((ly - 1.5) / 3) ** 2; + if (e < 1) { + col = [190, 194, 190]; + v = (0.55 + 0.45 * S.bub[(y & 127) * 64 + ((x * 2) & 63)]) * (0.7 + 0.3 * (1 - e)); + } + const p = ((lx - 2.5) / 7.5) ** 2 + ((ly + 2.6) / 3.3) ** 2; + if (p < 1) { + col = GLOVE; + v = 0.5 + 0.5 * sqrt(1 - p) - 0.12 * ly; + } + } else { + const p = ((lx - 6.5) / 4.6) ** 2 + ((ly - 0.4) / 6.2) ** 2; + if (p < 1) { + col = GLOVE; + v = 0.4 + 0.5 * sqrt(1 - p) - 0.04 * ly; + } + // Four curled fingers across the front, the thumb over the top. + for (let j = 0; j < 4; j++) { + const fy = ly + 4.4 - j * 2.9; + const fx = lx < -0.5 ? lx + 0.5 : lx > 6 ? lx - 6 : 0; + const d = fx * fx + fy * fy; + if (d < 1.8) { + col = GLOVE; + v = 0.5 + 0.5 * sqrt(1 - d / 1.8) - 0.03 * ly; + } + } + const tx = cl((lx - 2.5) / 5.5, 0, 1); + const d = (lx - 2.5 - 5.5 * tx) ** 2 + (ly + 6.8 - 1.6 * tx) ** 2; + if (d < 2.3) { + col = GLOVE; + v = 0.65 + 0.35 * sqrt(1 - d / 2.3); + } + } + if (col) { + R[i] = col[0] * v; + G[i] = col[1] * v; + B[i] = col[2] * v; + } + } + // The catalyst falling from the lip into the mouth. + if (kind === 1 && pour > 0.02 && pour < 0.98) + for (let y = max(0, round(lipy)); y < MY + 2; y++) { + const x0 = round(lipx - 0.5 + 0.4 * sin(y * 0.9 + S.t * 30)); + for (let x = x0; x <= x0 + (pour < 0.7 ? 1 : 0); x++) { + const i = y * W + x; + R[i] = 200; + G[i] = 160; + B[i] = 70; + } + } +} + +export function draw(display, params) { + const S = params.S; + if (!S || !S.pal) return; + const { R, G, B, bg, q, nx, ny, u, s, hs } = S; + for (let i = 0; i < N; i++) { + R[i] = bg[i * 3]; + G[i] = bg[i * 3 + 1]; + B[i] = bg[i * 3 + 2]; + } + const c = S.c; + const st = S.st; + // The cylinder comes down at the start of a round and is lifted off at its end. + let oy = 0; + if (c < T_DOWN) oy = -140 * (1 - ss(0, T_DOWN, c)); + else if (c > T_LIFT + 0.35) oy = -140 * ss(T_LIFT + 0.35, T_END, c); + oy = round(oy); + + // The column rises fast and keeps pouring out while the peroxide lasts. + // Then it folds over to one side and sinks back into the glass. + const tau = c - T_ERUPT; + const Tr = 1.6 - 0.9 * st; + const x01 = cl(tau / Tr, 0, 1); + const f = ss(T_STOP, T_FOLD, c); + const sl = ss(T_FOLD - 0.8, T_SLUMP, c); + const flow = tau > 0 ? (20 + 30 * st) * 3 * (1 - exp(-min(tau, T_STOP - T_ERUPT) / 3)) + 4 * max(0, c - T_STOP) : 0; + let L = tau > 0 ? (66 + 40 * st) * (1 - (1 - x01) ** 2) : 0; + const M = tau > 0 ? L + flow : (ss(T_FROTH, T_ERUPT, c) - 1) * LIQ; + L = tau > 0 ? 2 + (L - 2) * (1 - sl) : 0; + // How far the foam inside the glass has risen above the liquid. + const top = c < T_FROTH ? 99 : LIQ * (1 - ss(T_FROTH, T_ERUPT, c)); + // Once it is down, the foam stiffens and dulls. + const se = ss(T_SLUMP, T_WIPE, c); + const dim = 1 - 0.45 * se; + + // Where the dye streaks sit round the inside of the glass. + const xs = []; + xs.front = []; + for (let k = 0; k < S.bands; k++) { + const th = ((k + 0.5) / S.bands) * 2 * PI + 0.3; + xs.push(32 + 6.8 * sin(th)); + xs.front.push(cos(th) > 0); + } + if (oy > -80) cylinder(S, oy, top, M, dim, xs); + + q.fill(2); + if (tau > 0) { + const wm = 12.5 + 6 * st; + const l0 = 14 + 20 * (1 - f); + let px = 32; + let py = MY - 3 + oy; + for (let l = 0; l <= L + 0.01; l += 1.5) { + const a = S.side * 2.8 * f * ss(l0, l0 + 26, l); + const w = (8.2 + (wm - 8.2) * (1 - exp(-l / 14))) * (1 + 0.35 * sl); + const sway = 1.3 * sin(l * 0.08 - S.t * 1.9) * min(1, l / 25) * (1 - f); + stamp(S, px + sway, py, sin(a), -cos(a), w, M - 55 * log(1 + l / 55), 1 + 0.7 * sl); + px += 1.5 * sin(a); + py -= 1.5 * cos(a); + } + } + // Foam running down the outside of the glass to the bench, first and + // fattest on the side the column fell to, swelling as it nears the heap. + for (let sd = -1; sd <= 1; sd += 2) { + const near = sd === S.side; + const dr = ss(T_STOP + (near ? 0.3 : 0.9), T_SLUMP - (near ? 0.8 : 0.2), c); + for (let l = 0; dr > 0 && l <= 60 * dr; l += 1.5) { + const x = 32 + sd * (8.5 + 3 * ss(0, 12, l) + 1.1 * sin(l * 0.17 + S.ph[0] * sd)); + const w = (3.6 + 1.3 * sin(l * 0.23 + S.ph[1] + sd) + 3.4 * ss(4, 60, l)) * (near ? 1.15 : 0.95); + stamp(S, x, MY - 1 + l + oy, 0, 1, w, 18 * (c - T_STOP) - l); + } + } + const bands = S.bands; + for (let i = 0; i < N; i++) { + const d = q[i]; + if (d >= 1) continue; + const a = cl((1 - sqrt(d)) * 5, 0, 1); + foam(S, i, nx[i], ny[i], sqrt(1 - d), (asin(cl(u[i], -1, 1)) / (2 * PI)) * bands + 0.022 * s[i], s[i], S.tx[i] + 32, dim, a, 1); + } + + // The heap on the bench, coiled foam with the dye folded through it, until + // the towel sweeps it off from the right. + const wx = 72 - 86 * ss(T_WIPE, T_LIFT, c); + const A = (30 + 12 * st) * ss(T_STOP + 1, T_SLUMP + 0.5, c) * (1 - 0.18 * se); + if (A > 0.3 && c < T_LIFT) + for (let x = 0; x < W && x < wx - 7; x++) { + const h = A * hs[x]; + if (h < 0.4) continue; + const g = A * (hs[min(W - 1, x + 1)] - hs[max(0, x - 1)]) * 0.5; + const ytop = BENCH - h; + for (let y = max(0, floor(ytop)); y < BENCH; y++) { + const dd = y + 0.5 - ytop; + const e = exp(-max(0, dd) / 4); + const coil = sin((y + 1.5 * sin(x * 0.3 + S.ph[2])) * 0.95); + let hx = -g * e + 0.25 * sin(x * 0.7 + y * 0.4); + let hy = -e + 0.4 * coil * (1 - e); + let hz = 0.45 + (1 - e); + const k = 1 / sqrt(hx * hx + hy * hy + hz * hz); + hx *= k; + hy *= k; + hz *= k; + const b = (y + 2.5 * sin(x * 0.19 + S.ph[0]) + 1.5 * sin(x * 0.47 + S.ph[1])) * 0.15 + (x - 32) * 0.03; + foam(S, y * W + x, hx, hy, hz, b, y * 2 + 40, x * 2, dim * (0.62 + 0.38 * ss(0, 3, BENCH - 1 - y)), cl(dd + 0.5, 0, 1), 1 - 0.35 * se); + } + } + + // Steam off the hot heap. + const stA = ss(T_STOP + 0.8, T_SLUMP - 1, c) * (1 - ss(T_SLUMP + 0.5, T_WIPE, c)); + if (stA > 0.01) { + const crown = BENCH - A * 0.9; + const rise = S.t * 13; + for (let y = max(0, floor(crown - 70)); y < BENCH; y++) { + const v = crown - y; + const env = ss(-8, 4, v) * exp(-max(0, v) / 26) * stA; + if (env < 0.01) continue; + const row = ((y + rise) | 0) & 127; + const sw = 4 * sin(y * 0.09 + S.t); + for (let x = 0; x < W; x++) { + const n = S.stn[row * 64 + ((x + sw) & 63)]; + const add = 280 * env * exp(-(((x - 32) / 21) ** 2)) * max(0, n - 0.4); + const i = y * W + x; + R[i] += add * 0.85; + G[i] += add * 0.9; + B[i] += add; + } + } + } + + // The hands. + if (c < T_OUT) hand(S, 3, 40.5 + 46 * ss(T_DOWN, T_OUT, c), MY + 9 + oy, 0, 0); + else if (c < T_BACK) { + const back = ss(T_DRY, T_BACK, c); + const hx = 41 + 44 * (1 - ss(T_OUT, T_IN, c)) + 46 * back; + const a = -1.25 * ss(T_IN, T_POUR, c) * (1 - 0.75 * back); + hand(S, 1, hx, 41 - 4 * back, a, ss(T_POUR, T_DRY, c)); + } else if (c >= T_WIPE && c < T_LIFT) hand(S, 2, wx, BENCH - 4, 0, 0); + else if (c >= T_LIFT) hand(S, 3, 40.5 + 44 * (1 - ss(T_LIFT, T_LIFT + 0.35, c)), MY + 9 + oy, 0, 0); + + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x; + display.setPixel(x, y, min(255, R[i]), min(255, G[i]), min(255, B[i])); + } +} From 515174d900f534e511b563ec6fbb85bf874dfa30 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 00:16:45 +0000 Subject: [PATCH 12/15] =?UTF-8?q?Hanging=20Scroll=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9D=84=20=EC=A7=A0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GhRE8HZxGnk63uCfeHdr92 --- .../pattern-study/patterns/hanging-scroll.js | 592 ++++++++++++++++++ 1 file changed, 592 insertions(+) create mode 100644 incoming/pattern-study/patterns/hanging-scroll.js diff --git a/incoming/pattern-study/patterns/hanging-scroll.js b/incoming/pattern-study/patterns/hanging-scroll.js new file mode 100644 index 000000000..6f7bed289 --- /dev/null +++ b/incoming/pattern-study/patterns/hanging-scroll.js @@ -0,0 +1,592 @@ +// ===== Patternflow pattern ===== +// Title: Hanging Scroll +// Family: paper +// Idea: A hanging scroll in a dim alcove lets its weighted roller down from the upper rod to show an ink painting on ivory paper in silk borders row by row, hangs swaying in a draft, then rolls up from the bottom so the next painting can come down. +// Author: patternflow-lab +// Date: 2026-10-23 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 23, after Japanese kakemono hanging scrolls and Song dynasty ink landscapes +// Round: 23 +// =============================== +// @matrix 64x128 +// @knobs Lamp=0..1, Speed=0.2..3, Painting=0..3, Ink=0..1 +// +// Knob 1: Lamp. The light on the scroll: a warm candle low on the left turns the paper ivory at 0, and towards 1 it gives way to cool moonlight from a window on the right. +// Knob 2: Speed. How fast the scroll moves: at 2 the roller takes under three seconds to come down, a painting hangs for seven and the draft swings it about once in two and a half. +// Knob 3: Painting. Which painting unrolls next: misty mountains with a waterfall at 0, bamboo in wind at 1, a plum branch under the moon at 2, cranes over waves at 3. Turning it rolls the scroll up and lets that one down. +// Knob 4: Ink. How wet the ink was: crisp dry-brush lines with broken edges at 0, washes that bled out into the paper and strokes feathered at their edges at 1. +// Buttons: K1 tap rolls the scroll up now and unrolls the next painting. + +const [W, H] = [64, 128]; +const N = W * H; +// The scroll hangs from a nail at row 3. Its upper rod fills rows 15 and 16, +// and the mounting runs 96 rows down from row 17 to the roller, 44 columns +// wide from column 10. The painting sits inside the mounting, 36 by 74. +const [NAIL, TOP, LEN, MX, MW] = [3, 17, 96, 10, 44]; +const [PX, PY, PW, PH] = [4, 14, 36, 74]; +const PN = PW * PH; +// At Speed 2, in seconds: unroll, hang, roll up, rest rolled. +const [TU, TS, TR, TZ] = [2.6, 7, 2, 0.5]; +const [PAPER, SILK, GOLD, BACK] = [[0.95, 0.91, 0.8], [0.2, 0.31, 0.3], [0.8, 0.61, 0.27], [0.6, 0.58, 0.5]]; +const [WOOD, BONE, BRASS, CORD] = [[0.4, 0.27, 0.16], [0.86, 0.8, 0.64], [0.85, 0.66, 0.32], [0.5, 0.3, 0.45]]; +// Seal vermilion, plum blossom and the red sun. +const PIG = [[0.82, 0.14, 0.1], [0.9, 0.3, 0.42], [0.93, 0.3, 0.12]]; +const CANDLE = [1, 0.9, 0.72]; +const MOON = [0.56, 0.67, 0.92]; +const { min, max, floor, ceil, round, sqrt, sin, cos, exp, abs, hypot, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => { + const t = cl((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; +const mix = (a, b, t) => a + (b - a) * t; +// A fixed hash and a value noise made from it, so a painting comes out the +// same every time it unrolls. +const hash = (a, b) => { + let h = Math.imul((a | 0) * 374761393 + (b | 0) * 668265263, 1274126177); + h = Math.imul(h ^ (h >>> 13), 1103515245); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; +const vn = (x, y) => { + const i = floor(x); + const j = floor(y); + const [u, v] = [sm(0, 1, x - i), sm(0, 1, y - j)]; + const [a, b, c] = [hash(i, j), hash(i + 1, j), hash(i, j + 1)]; + return a + (b - a) * u + (c - a) * v + (a - b - c + hash(i + 1, j + 1)) * u * v; +}; + +// The painting being made: wash and stroke densities, pigment cover and which +// pigment. RID is a ridge row per column for fill(), ROC how solid it is there. +let Wm, Sm, Am, Pg; +let rs = 1; +const rnd = () => (rs = (rs * 16807) % 2147483647) / 2147483647; +const RID = new Float32Array(PW); +const ROC = new Float32Array(PW); + +// A brush stroke through points [x, y, half width, ...]. dry parts the hairs +// into streaks along the stroke, more of them toward its end. +function brush(p, dens, dry = 0) { + const n = p.length / 3 - 1; + for (let k = 0; k < n; k++) { + const [ax, ay, aw, bx, by, bw] = p.slice(k * 3, k * 3 + 6); + const dx = bx - ax; + const dy = by - ay; + const L2 = dx * dx + dy * dy || 1e-6; + const L = sqrt(L2); + const m = max(aw, bw) + 1; + for (let y = max(0, floor(min(ay, by) - m)); y <= min(PH - 1, floor(max(ay, by) + m)); y++) + for (let x = max(0, floor(min(ax, bx) - m)); x <= min(PW - 1, floor(max(ax, bx) + m)); x++) { + const qx = x + 0.5 - ax; + const qy = y + 0.5 - ay; + const t = cl((qx * dx + qy * dy) / L2, 0, 1); + let a = cl(aw + (bw - aw) * t + 0.5 - hypot(qx - dx * t, qy - dy * t), 0, 1); + if (a <= 0) continue; + if (dry) { + const g = (dry * (k + t)) / n; + const c = (qx * dy - qy * dx) / L; + a *= 0.15 + 0.85 * sm(g - 0.1, g + 0.1, 0.55 * hash(floor(c * 1.8) + 99, floor(t * L * 0.5) + k * 31) + 0.45 * vn(x * 0.9, y * 0.9)); + } + const i = y * PW + x; + // A negative density leaves the paper bare instead. + if (dens < 0) Wm[i] *= 1 + dens * a; + else Sm[i] = max(Sm[i], a * dens); + } + } +} + +// A round patch of pigment, which leaves no wash under it. +function disc(cx, cy, r, pg, a0 = 0.92) { + for (let y = max(0, floor(cy - r - 1)); y < min(PH, cy + r + 1); y++) + for (let x = max(0, floor(cx - r - 1)); x < min(PW, cx + r + 1); x++) { + const a = cl(r + 0.5 - hypot(x + 0.5 - cx, y + 0.5 - cy), 0, 1); + const i = y * PW + x; + if (a > Am[i]) { + Am[i] = a * a0; + Pg[i] = pg; + } + Wm[i] *= 1 - a; + } +} + +// Fill below the ridge in RID: the wash is darkest at the ridge and fades down +// into mist, and it covers what was painted behind. edge inks the ridge line +// and cun the short texture strokes under it. +function fill(dens, fade, edge, cun, sd) { + for (let x = 0; x < PW; x++) { + const r = RID[x]; + const oc = ROC[x]; + for (let y = max(0, floor(r)); y < PH; y++) { + // The row the ridge crosses is covered only in part. + const cv = cl(y + 1 - r, 0, 1) * oc; + const d = max(0, y + 0.5 - r); + const i = y * PW + x; + const o = 0.92 * cl(d / 2 + 0.3, 0, 1) * cv; + Wm[i] = min(1, Wm[i] * (1 - o) + dens * exp(-d / fade) * (0.7 + 0.6 * vn(x * 0.5 + sd, y * 0.2)) * cv); + Sm[i] *= 1 - o; + if (d < 1.5) Sm[i] = max(Sm[i], edge * (1 - d / 1.7) * (0.35 + 0.65 * vn(x * 1.3 + sd, y * 0.5)) * cv); + const c = vn(x * 1.2 + sd, y * 0.3); + if (c > 0.64) Sm[i] = max(Sm[i], cun * (c - 0.6) * 2.6 * exp(-d / (fade * 0.8)) * oc); + } + } +} + +function mtn(cx, base, hw, ht, dens, fade, edge, cun, sd) { + for (let x = 0; x < PW; x++) { + const u = abs(x + 0.5 - cx) / hw; + RID[x] = u < 1 ? base - ht * (1 - u ** 1.6) * (0.62 + 0.45 * vn(x * 0.28 + sd, sd) + 0.3 * vn(x * 0.8, sd + 5)) : PH; + ROC[x] = sm(1, 0.75, u); + } + fill(dens, fade, edge, cun, sd); +} + +// A pine clinging to a ridge: a crooked trunk and flat layers of needles. +function pine(x, y, h, d) { + brush([x, y, 0.45, x + 0.5, y - h * 0.5, 0.35, x - 0.2, y - h, 0.3], d); + for (let k = 0; k < 3; k++) { + const yy = y - h * (0.42 + 0.26 * k); + const w = h * (0.55 - 0.13 * k); + brush([x - w, yy + 0.4, 0.45, x + 0.2, yy - 0.3, 0.75, x + w, yy + 0.2, 0.4], d); + } +} + +// A column of characters, each a few short strokes in a square cell, and the +// red seal under it. +function calli(x, y, n) { + for (let c = 0; c < n; c++) { + const cy = y + c * 4.6; + for (let k = 3 + floor(rnd() * 3); k > 0; k--) { + const t = rnd() * 4; + const a = x + rnd() * 2.6; + const b = cy + rnd() * 3; + brush(t < 1.6 ? [a - 0.3, b, 0.4, min(x + 3.2, a + 2), b + 0.2, 0.35] : t < 3 ? [a, b - 0.4, 0.4, a + 0.2, min(cy + 3.6, b + 2.2), 0.3] : [a, b, 0.55, a + 0.7, b + 0.7, 0.3], 0.92); + } + } + const sy = floor(y + n * 4.6 + 0.6); + const sx = floor(x + 0.2); + for (let j = 0; j < 3; j++) + for (let i = 0; i < 3; i++) { + const k = (sy + j) * PW + sx + i; + // Carved lines let the paper show through the red. + Am[k] = (i + j) % 2 && hash(sx + i, sy + j) < 0.6 ? 0.45 : 0.95; + Pg[k] = 0; + } +} + +function landscape() { + // The far peak towers over the left, a lower one stands behind on the right. + mtn(13, 42, 15, 34, 0.32, 10, 0.45, 0.3, 3); + mtn(29, 40, 9, 12, 0.26, 7, 0.35, 0.25, 11); + mtn(24, 56, 16, 18, 0.5, 8, 0.85, 0.6, 23); + for (const x of [15, 18, 21, 31, 34]) pine(x, RID[x] + 1.5, 4 + (x % 3), 0.9); + // A waterfall drops from a cleft in the middle ridge into the mist. + for (let y = floor(RID[28] + 3); y < 58; y++) + for (let x = 25; x < 31; x++) { + const i = y * PW + x; + const c = abs(x + 0.5 - 28 - 0.4 * sin(y * 0.45)); + const f = sm(58, 50, y); + if (c < 0.9) { + Wm[i] *= 1 - 0.9 * f; + Sm[i] *= 1 - f; + } else if (c < 2.4) Sm[i] = max(Sm[i], (0.75 * f * (2.4 - c)) / 1.5); + } + mtn(30, 63, 9, 3, 0.3, 2.5, 0.5, 0, 51); + // The near bank, with a hut under two pines. + mtn(5, 70, 17, 16, 0.72, 6, 1, 0.85, 37); + pine(3, RID[3] + 2, 7, 0.95); + pine(8, RID[8] + 2, 6, 0.95); + const hy = round(RID[15] + 4); + for (let j = -4; j <= 1; j++) + for (let i = 11; i <= 18; i++) { + Wm[(hy + j) * PW + i] *= 0.1; + Sm[(hy + j) * PW + i] = 0; + } + brush([10.5, hy - 2.2, 0.55, 14.5, hy - 4.6, 0.7, 18.5, hy - 2.2, 0.55], 0.97); + brush([12, hy - 2, 0.3, 12, hy + 0.6, 0.3], 0.7); + brush([17, hy - 2, 0.3, 17, hy + 0.6, 0.3], 0.7); + brush([14.5, hy - 1, 0.45, 14.5, hy + 0.6, 0.45], 0.85); + // Still water, a boat with its fisherman and his rod. + brush([19, 66.5, 0.25, 23, 66.5, 0.2], 0.3); + brush([28, 71.5, 0.25, 34, 71.5, 0.2], 0.28); + brush([20, 72.5, 0.2, 24, 72.5, 0.2], 0.25); + brush([22.5, 68, 0.35, 25, 69, 0.6, 28, 68.9, 0.6, 29.5, 67.7, 0.35], 0.92); + brush([26.5, 68, 0.55, 26.6, 66.4, 0.5], 0.9); + brush([27, 66.8, 0.25, 31, 63.5, 0.2], 0.6); + calli(31.6, 1.5, 4); +} + +function bamboo() { + // Three stalks lean in the wind, the far one pale. + for (const [x0, x1, y1, w, d] of [[6, 11, -2, 1.1, 0.32], [14, 20, -2, 1.7, 0.9], [25, 29, 10, 1.3, 0.72]]) { + const n = round((75 - y1) / 10.5); + for (let k = 0; k < n; k++) { + const ax = mix(x0, x1, k / n); + const ay = mix(75, y1, k / n); + const bx = mix(x0, x1, (k + 1) / n); + const by = mix(75, y1, (k + 1) / n); + brush([ax, ay - 0.8, w, bx, by + 0.8, w * 0.9], d, 0.45); + brush([bx - w - 0.5, by + 0.2, 0.45, bx + w + 0.5, by - 0.4, 0.45], min(1, d * 1.1)); + } + } + // Leaves stream off the twigs to the right. + for (const [x, y, n, d, s] of [ + [12, 8, 4, 0.35, 0.9], [9, 40, 4, 0.35, 0.9], [21, 12, 5, 0.92, 1.2], [20, 34, 5, 0.9, 1.15], + [19, 56, 4, 0.85, 1.05], [29, 24, 5, 0.75, 1], [28, 46, 3, 0.75, 0.95], + ]) { + brush([x - 3.5, y + 2.5, 0.35, x, y, 0.25], d); + for (let j = 0; j < n; j++) { + const a = 0.35 + (j / (n - 1) - 0.5) * 1.3 + (rnd() - 0.5) * 0.3; + const L = (8 + rnd() * 4) * s; + const w = (1 + rnd() * 0.3) * s; + brush([x, y, 0.25, x + cos(a) * L * 0.3, y + sin(a) * L * 0.3, w, x + cos(a + 0.12) * L, y + sin(a + 0.12) * L, 0.05], d); + } + } + calli(31.4, 42, 5); +} + +function plum() { + // The sky washed grey round a full moon left bare. + for (let y = 0; y < 50; y++) + for (let x = 0; x < PW; x++) + Wm[y * PW + x] = 0.28 * sm(8, 9.6, hypot(x + 0.5 - 23, y + 0.5 - 17)) * sm(50, 18, y) * (0.75 + 0.5 * vn(x * 0.2, y * 0.15)); + // An old branch in dry brush, straight young shoots off it, and red blossom. + brush([-1, 75, 3, 5, 63, 2.5, 3, 51, 2.1, 10, 41, 1.7, 8, 31, 1.3, 15, 21, 1, 13, 12, 0.6], 0.93, 0.9); + brush([5, 61, 1.3, 13, 56, 1, 21, 51, 0.75, 29, 42, 0.45], 0.88, 0.8); + for (const t of [[10, 41, 12, 28], [15, 21, 22, 8], [13, 12, 14, 4], [21, 51, 24, 41], [29, 42, 33, 33], [8, 31, 3, 23], [13, 56, 16, 48], [24, 47, 31, 47]]) + brush([t[0], t[1], 0.45, t[2], t[3], 0.25], 0.85); + for (const [x, y, r] of [ + [12, 30, 1.5], [12.5, 26, 1.3], [17, 16, 1.4], [20, 11, 1.5], [21.5, 7.5, 1.2], [14, 6, 1.3], + [22, 44, 1.5], [24, 40.5, 1.2], [31, 37, 1.4], [32.5, 33, 1.2], [5, 25, 1.4], [3, 21.5, 1.2], + [15.5, 50, 1.4], [27.5, 47, 1.3], [9, 37.5, 1.5], [17.5, 54, 1.3], [6, 57, 1.6], [11, 16.5, 1.1], + ]) { + disc(x, y, r, 1); + Sm[floor(y) * PW + floor(x)] = 0.6; + } + calli(31.4, 50, 4); +} + +// A red-crowned crane flying left, wings raised (up 1) or lowered (up -1). +function crane(cx, cy, s, up) { + const u = up * s; + // The far wing and the near wing are left bare in the grey sky, each with + // black flight feathers along its trailing edge. + for (const [fx, d] of [[-2.5, -0.55], [4.5, -0.95]]) { + const w = [cx, cy - 0.3 * u, 1.9 * s, cx + (fx * 0.4 + 0.6) * s, cy - 5 * u, 1.4 * s, cx + fx * s, cy - 9.5 * u, 0.7 * s]; + brush(w, d); + brush(w.map((v, k) => (k % 3 === 0 ? v + 1.6 * s : k % 3 === 2 ? 0.3 * s : v)), 0.9); + } + brush([cx - 3 * s, cy, 1.3 * s, cx + 3 * s, cy + 0.3 * s, 1.1 * s], -0.95); + // The black neck, a grey bill, the black tail and the trailing legs. + brush([cx - 2.6 * s, cy - 0.2 * s, 0.55 * s, cx - 7.8 * s, cy - 1.4 * s, 0.45 * s], 0.95); + brush([cx - 8.3 * s, cy - 1.4 * s, 0.3, cx - 10.5 * s, cy - 1 * s, 0.2], 0.5); + brush([cx + 2.5 * s, cy - 0.2 * s, 0.8 * s, cx + 4.3 * s, cy + 0.5 * s, 0.4 * s], 0.9); + brush([cx + 3 * s, cy + 0.9 * s, 0.3, cx + 9.5 * s, cy + 1.9 * s, 0.25], 0.6); + disc(cx - 8 * s, cy - 2 * s, 0.6, 0); +} + +function cranes() { + for (let y = 0; y < PH; y++) for (let x = 0; x < PW; x++) Wm[y * PW + x] = (0.5 + 0.25 * sm(62, 0, y)) * (0.8 + 0.4 * vn(x * 0.13 + 4, y * 0.09)); + disc(27, 10, 4.4, 2); + crane(19, 30, 1.3, 1); + crane(23, 47, 0.95, -1); + crane(8, 41, 0.5, 1); + // Rows of waves, each crest inked and its trough left pale. + for (let b = 0; b < 4; b++) { + const y0 = 56 + b * 5.4; + const amp = 1.4 + b * 0.8; + const f = 0.5 - b * 0.07; + for (let x = 0; x < PW; x++) { + RID[x] = y0 - amp * (0.5 + 0.5 * sin(x * f + b * 2.1)) ** 3; + ROC[x] = 1; + } + fill(0.42, 2.6, 0.9, 0, b * 13); + // A curl of foam breaks off each crest. + for (let x0 = (PI / 2 - b * 2.1 + 2 * PI * 3) / f; x0 > -2; x0 -= (2 * PI) / f) { + const py = y0 - amp; + brush([x0 + 1, py + 0.6, 0.35, x0, py - 0.6, 0.4, x0 - 1.6, py - 0.3, 0.3, x0 - 2.2, py + 0.8, 0.2], 0.85); + } + } + calli(1.6, 2, 4); +} + +const PAINT = [landscape, bamboo, plum, cranes]; + +// Each painting is kept twice: as painted with a dry brush, and as it would +// be had the ink been wet, its washes spread and its strokes bled. +function blur(src, r) { + let a = src; + for (let pass = 0; pass < 2; pass++) { + const b = new Float32Array(PN); + const c = new Float32Array(PN); + for (let y = 0; y < PH; y++) + for (let x = 0; x < PW; x++) { + let s = 0; + for (let k = -r; k <= r; k++) s += a[y * PW + cl(x + k, 0, PW - 1)]; + b[y * PW + x] = s / (2 * r + 1); + } + for (let y = 0; y < PH; y++) + for (let x = 0; x < PW; x++) { + let s = 0; + for (let k = -r; k <= r; k++) s += b[cl(y + k, 0, PH - 1) * PW + x]; + c[y * PW + x] = s / (2 * r + 1); + } + a = c; + } + return a; +} + +function paint(n) { + Wm = new Float32Array(PN); + Sm = new Float32Array(PN); + Am = new Float32Array(PN); + Pg = new Uint8Array(PN); + rs = 11 + n * 7; + PAINT[n](); + const wb = blur(Wm, 2); + const sb = blur(Sm, 1); + const hb = blur(sb, 2); + const wh = blur(wb, 3); + const d0 = new Float32Array(PN); + const d1 = new Float32Array(PN); + for (let i = 0; i < PN; i++) { + // A dry wash catches only the raised fibres of the paper, in streaks. + const x = i % PW; + const y = (i / PW) | 0; + d0[i] = 1 - (1 - Wm[i] * (0.5 + 0.45 * vn(x * 0.25, y * 0.6))) * (1 - Sm[i]); + d1[i] = 1 - exp(-1.7 * (wb[i] + 0.5 * wh[i]) - 1.5 * (sb[i] + 0.6 * hb[i])); + } + return [d0, d1, Am, Pg]; +} + +export function setup(params) { + const F = (n) => new Float32Array(n); + const P = (params.s = { F: [F(N), F(N), F(N)], A: [F(N), F(N), F(N)], S: [F(MW * LEN), F(MW * LEN), F(MW * LEN)], L: [F(W), F(W), F(W)] }); + Object.assign(P, { cache: [], base: -1, st: 0, tm: 0, u: 0, u0: 0, hold: 2.2, T: 0, rt: 0, kick: -99, th: 0, bow: 0, tw: 0, built: -1, wetD: -1, lamp: 0, wet: 0.06 }); + // The alcove: an earth wall, a wooden post at the left, and the raised floor + // with its lacquered front edge. + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let c = [0.085, 0.07, 0.055]; + let f = 0.8 + 0.4 * vn(x * 0.25, y * 0.12) + 0.08 * (hash(x, y) - 0.5); + if (x < 3) { + c = [0.22, 0.15, 0.09]; + f = (x === 2 ? 0.55 : 1) * (0.85 + 0.3 * vn(x * 2, y * 0.05)); + } else if (y === 120) f = 0.35; + else if (y > 120 && y < 125) { + c = [0.2, 0.14, 0.085]; + f = 0.8 + 0.25 * vn(x * 0.06, y * 2) + (y - 121) * 0.06; + } else if (y >= 125) { + c = [0.1, 0.075, 0.06]; + f = y === 125 ? 3.2 : 0.6; + } + for (let k = 0; k < 3; k++) P.A[k][y * W + x] = c[k] * f; + } + // The mounting: silk with a small woven figure, gold brocade strips above + // and below the painting, and the two hanging strips from the rod. + for (let y = 0; y < LEN; y++) + for (let x = 0; x < MW; x++) { + let c = SILK; + let f = (0.92 + 0.08 * ((x + y) & 1)) * ((x % 4 === 1 && y % 4 === 1) || (x % 4 === 3 && y % 4 === 3) ? 1.3 : 1); + if (x >= PX && x < PX + PW && (y === PY - 2 || y === PY - 1 || y === PY + PH || y === PY + PH + 1)) { + c = GOLD; + f = (x + 2 * y) % 3 ? 0.88 : 1.15; + } + if (x === PX - 1 || x === PX + PW) f *= 0.8; + const fu = x % 23 > 8 && x % 23 < 12; + if (fu && y <= 10) { + c = GOLD; + f = (x % 23 === 10 ? 1 : 0.8) * (y % 3 ? 0.92 : 1.12); + } else if (fu && y === 11 && x % 23 !== 10) { + c = PAPER; + f = 0.75; + } else if (x % 23 === 12 && y <= 11) f *= 0.55; + if (y === 0) f *= 0.65; + for (let k = 0; k < 3; k++) P.S[k][y * MW + x] = c[k] * f; + } +} + +function roll(P) { + if (P.st < 2) Object.assign(P, { st: 2, tm: 0, u0: P.u }); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s; + const k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + P.lamp = cl(k[0], 0, 1); + const rate = cl(k[1], 0.2, 3) / 2; + const base = cl(round(k[2]), 0, 3); + P.wet = cl(k[3], 0, 1); + if (P.base < 0) { + P.base = P.cur = base; + P.nx = (base + 1) % 4; + } + if (base !== P.base) { + P.base = P.nx = base; + roll(P); + } + if (input.btnPressed && input.btnPressed[0]) roll(P); + const st = dt * rate; + P.T += st; + P.rt += dt; + P.tm += st; + if (P.st === 0) { + P.u = sm(0, 1, P.tm / TU); + if (P.tm >= TU) Object.assign(P, { st: 1, tm: 0, kick: P.T }); + } else if (P.st === 1) { + if (P.tm >= P.hold) roll(P); + } else if (P.st === 2) { + P.u = P.u0 * (1 - sm(0, 1, P.tm / (TR * max(0.25, P.u0)))); + if (P.u <= 0) Object.assign(P, { u: 0, st: 3, tm: 0 }); + } else if (P.tm >= TZ) { + // Rolled up, the scroll is swapped for the next one, which comes down. + Object.assign(P, { cur: P.nx, nx: (P.nx + 1) % 4, st: 0, tm: 0, hold: TS }); + } + // The swing: a kick when the roller comes to rest, then the draft. + const w = (2 * PI) / 2.4; + const tk = P.T - P.kick; + const kick = tk > 0 ? 1.7 * exp(-tk / 2.2) * sin(w * tk) : 0; + const T = P.T; + P.th = (kick + 0.55 * (0.6 + 0.4 * sin(0.29 * w * T)) * sin(w * T + 0.6 * sin(0.13 * w * T))) / 110; + P.bow = 0.45 * sin(0.71 * w * T + 1.3) + 0.3 * kick; + P.tw = sin(0.57 * w * T + 0.4); +} + +function compose(P) { + const c = P.cache[P.cur] || (P.cache[P.cur] = paint(P.cur)); + const wet = P.wet; + for (let y = 0; y < PH; y++) + for (let x = 0; x < PW; x++) { + const i = y * PW + x; + const D = c[0][i] + (c[1][i] - c[0][i]) * wet; + const f = (1 - 0.9 * D) * (0.965 + 0.05 * hash(x + 7, y + 3)); + const a = c[2][i]; + const p = PIG[c[3][i]]; + for (let k = 0; k < 3; k++) P.S[k][(PY + y) * MW + PX + x] = PAPER[k] * f * (1 - a) + p[k] * a; + } + P.built = P.cur; + P.wetD = wet; +} + +export function draw(display, params) { + const P = params.s; + if (!P || P.base < 0) return; + if (P.built !== P.cur || abs(P.wetD - P.wet) > 0.004) compose(P); + const [FR, FG, FB] = P.F; + const [AR, AG, AB] = P.A; + const [SR, SG, SB] = P.S; + const [LR, LG, LB] = P.L; + const lamp = P.lamp; + const fl = 1 + (1 - lamp) * 0.035 * (sin(P.rt * 9.3) * 0.6 + sin(P.rt * 14.1 + 1) * 0.4); + // The light on each column: the candle from the left, the moon from the right. + for (let x = 0; x < W; x++) { + const gc = (1.08 - (0.42 * x) / 63) * fl; + const gm = 0.66 + (0.4 * x) / 63; + for (let k = 0; k < 3; k++) P.L[k][x] = mix(CANDLE[k] * gc, MOON[k] * gm, lamp); + } + const vy = (y) => 0.8 + 0.2 * min(1, y / 60); + const yR = TOP + P.u * LEN; + const dia = sqrt(16 + 26 * (1 - P.u)); + const yc = yR + dia / 2; + const { th, bow } = P; + const side = 1 - 2 * lamp; + + // The wall, with the scroll's soft shadow thrown away from the light. + const sx0 = MX + th * 60 + 3 * side; + for (let y = 0; y < H; y++) { + const v = vy(y); + const sy = y > 15 && y < yc + dia / 2 + 1.5 ? 0.55 : 0; + for (let x = 0; x < W; x++) { + const i = y * W + x; + const s = (1 - sy * cl(min(x + 0.5 - sx0, sx0 + MW - x - 0.5) * 0.6 + 0.5, 0, 1)) * v; + FR[i] = AR[i] * LR[x] * s; + FG[i] = AG[i] * LG[x] * s; + FB[i] = AB[i] * LB[x] * s; + } + } + + // The hanging part, each row shifted by the swing and the bow of the paper. + // The paper turned toward the light is brighter, and the rows curling onto + // the roller darker. + const glow = 1 + 0.06 * P.tw * side; + for (let y = TOP; y < yR; y++) { + const cov = min(1, yR - y); + const ty = y - TOP; + const bw = bow * sin(PI * cl((y + 0.5 - TOP) / (yR - TOP), 0, 1)); + const dx = th * (y + 0.5 - NAIL) + bw; + const v = vy(y) * glow * (0.62 + 0.38 * cl((yR - y - 0.5) / 1.6, 0, 1)) * cov; + for (let x = MX - 3; x < MX + MW + 3; x++) { + const fx = x - MX - dx; + const i0 = floor(fx); + const f = fx - i0; + const w0 = i0 >= 0 && i0 < MW ? 1 - f : 0; + const w1 = i0 >= -1 && i0 < MW - 1 ? f : 0; + const a = (w0 + w1) * cov; + if (a <= 0) continue; + const t = ty * MW + i0; + const r = (w0 ? SR[t] * w0 : 0) + (w1 ? SR[t + 1] * w1 : 0); + const g = (w0 ? SG[t] * w0 : 0) + (w1 ? SG[t + 1] * w1 : 0); + const b = (w0 ? SB[t] * w0 : 0) + (w1 ? SB[t + 1] * w1 : 0); + const s = v * (1 + (0.12 * bw * side * (31.5 - x)) / 22); + const i = y * W + x; + FR[i] = FR[i] * (1 - a) + r * LR[x] * s; + FG[i] = FG[i] * (1 - a) + g * LG[x] * s; + FB[i] = FB[i] * (1 - a) + b * LB[x] * s; + } + } + + const put = (x, y, c, f, a = 1) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const i = y * W + x; + const s = f * vy(y) * a; + FR[i] = FR[i] * (1 - a) + c[0] * LR[x] * s; + FG[i] = FG[i] * (1 - a) + c[1] * LG[x] * s; + FB[i] = FB[i] * (1 - a) + c[2] * LB[x] * s; + }; + // The nail, the cord to two rings on the rod, and the rod. + for (let y = 4; y < 15; y++) + for (const ex of [20.5, 43.5]) { + const x = mix(31.5, ex, (y - 4) / 10.5) - 0.5; + const x0 = floor(x); + put(x0, y, CORD, 1, 1 - (x - x0)); + put(x0 + 1, y, CORD, 1, x - x0); + } + for (const [x, y, f] of [[31, 2, 1.25], [32, 2, 0.8], [31, 3, 0.8], [32, 3, 0.55], [20, 14, 1.1], [43, 14, 1.1]]) put(x, y, BRASS, f); + for (let x = MX - 1; x <= MX + MW; x++) { + const e = x < MX || x >= MX + MW ? 0.75 : 1; + put(x, 15, WOOD, 1.3 * e); + put(x, 16, WOOD, 0.7 * e); + } + + // The roller and the rolled scroll on it, turning as it goes, with a knob + // of bone at each end standing proud of the roll. + const dxr = th * (yc - NAIL); + const rolled = 1 - P.u; + const ang = (rolled * LEN) / (dia / 2); + const seam = cos(ang) > 0.2 && rolled > 0.02 ? sin(ang) : 9; + const rc = [0, 1, 2].map((k) => mix(SILK[k], BACK[k], sm(0, 0.3, rolled))); + for (let y = floor(yc - 3.6); y <= yc + 3.6; y++) { + const yy = y + 0.5 - yc; + const v = yy / (dia / 2); + const vk = yy / 3.4; + for (let x = MX - 5; x < MX + MW + 5; x++) { + const px = x + 0.5 - dxr; + const kx = px < MX ? MX - px : px - MX - MW; + if (kx < 0) { + const a = cl(dia / 2 - abs(yy) + 0.5, 0, 1) * cl(0.5 - kx, 0, 1); + if (a <= 0) continue; + const n = sqrt(max(0, 1 - v * v)); + put(x, y, rc, (0.25 + 0.75 * n * (0.8 - 0.35 * v)) * (abs(v - seam) < 0.3 ? 0.6 : 1), a); + } else if (kx < 4.6) { + const e = 3.4 * sqrt(max(0, 1 - (kx / 4.4) ** 4)); + const a = cl(e - abs(yy) + 0.5, 0, 1); + if (a <= 0) continue; + const n = sqrt(max(0, 1 - vk * vk)); + const hi = 0.45 * exp(-((vk + 0.45) ** 2) / 0.05); + put(x, y, BONE, (0.3 + 0.75 * n * (0.85 - 0.3 * vk) + hi) * (kx < 0.8 ? 0.55 : 1), a); + } + } + } + + for (let i = 0; i < N; i++) display.setPixel(i % W, (i / W) | 0, min(255, FR[i] * 255), min(255, FG[i] * 255), min(255, FB[i] * 255)); +} From 1d5f75702a37857a7fbb4f0f0078d583a0ed4fcc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 00:18:42 +0000 Subject: [PATCH 13/15] =?UTF-8?q?README=20=EC=97=90=2023=ED=8C=90=20?= =?UTF-8?q?=ED=91=9C=EB=A5=BC=20=EB=84=A3=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_01GhRE8HZxGnk63uCfeHdr92 --- incoming/pattern-study/README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 80101f307..d076b7355 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 128개를 마흔두 갈래로 나눠 만들었어요. 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판도 열 갈래에 하나씩 열 개를 더했고, 몸과 비행 갈래를 새로 열었어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 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 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. 패널은 세로로 서요. 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판에서 세로로 다시 짰어요. @@ -234,6 +234,21 @@ | [Senbazuru](patterns/senbazuru.js) | 종이 | 붉은 옻칠 고리에 실로 꿴 종이학이 줄줄이 매달려 금박 학 아래로 주황에서 초록, 파랑을 지나 보라와 분홍까지 띠를 이루고, 실 끝에는 금 구슬과 비단 술이 달려요. 어두운 절 방에 외풍이 불면 줄이 흔들리고 학이 실을 축으로 돌면서 접힌 면이 하나씩 등불을 받아 번쩍여요. K1 을 누르면 돌풍이 불어 학 하나가 떨어져 나뭇잎처럼 바닥으로 내려앉아요. | | [Weaver](patterns/weaver.js) | 새 | 강 위로 뻗은 아카시아 가지 끝에서 수컷 마을베짜기새가 둥지를 지어요. 갈대 잎을 찢어 문 초록 띠를 벽에 꿰어 당기면 둥지가 고리에서 둥근 방으로, 다시 아래로 열린 입구로 한 가닥씩 자라고, 묵은 띠는 짚 색으로 바래요. K1 을 누르면 암컷이 날아와 둥지를 살피고, 다 지은 둥지가 퇴짜를 맞으면 수컷이 끊어 강에 떨어뜨리고 새 고리부터 다시 지어요. | +### 23판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Axon](patterns/axon.js) | 몸 | 어두운 조직 속에서 신경세포 하나가 초록 형광으로 빛나요. 세포체를 떠난 스파이크가 수초 사이의 랑비에 결절을 하나씩 건너뛰며 축삭을 내려가고, 말단 단추가 시냅스 틈 너머로 신호를 넘기면 아래의 자홍색 신경세포가 따라 켜져요. K4 를 올리면 수초가 벗겨져 신호가 기어가다 꺼져요. | +| [Bait Ball](patterns/bait-ball.js) | 바다 | 옆에서 본 푸른 바다를 정어리 떼 공이 거의 다 채워요. 떼가 한꺼번에 몸을 틀면 은빛 물결이 공을 가로지르고, 등을 돌리면 짙은 남색으로 가라앉아요. 상어와 돌고래가 뚫고 지나간 구멍은 뒤에서 다시 닫히고, 가넷이 수면에서 거품 꼬리를 끌며 내리꽂혀요. | +| [Elephant Toothpaste](patterns/elephant-toothpaste.js) | 화학·실험 | 보라색 장갑을 낀 손이 실험대 위 눈금실린더에 아이오딘화 칼륨을 부으면, 유리 안쪽에 그어 둔 식용 색소 줄무늬를 두른 거품 기둥이 입구에서 솟아 패널 꼭대기까지 올라요. 기둥은 한쪽으로 꺾여 실린더 옆을 타고 흘러내려 김 나는 더미로 쌓이고, 더미가 굳어 바래면 손이 닦아 내고 새 실린더를 내려놓아요. | +| [Final Approach](patterns/final-approach.js) | 비행 | 밤에 내려앉는 여객기 조종석에서 앞을 봐요. 활주로 등과 진입등이 도시 불빛이 깔린 지평선 아래로 모이고, 래빗 섬광등이 1초에 두 번 활주로 쪽으로 달려가요. 왼쪽 PAPI 가 흰 둘 빨강 둘을 보이다가, 플레어와 접지 뒤에는 중심선 등이 발밑으로 쏟아져 지나가요. K1 을 누르면 복행해요. | +| [Giraffe](patterns/giraffe.js) | 짐승 | 우산 아카시아의 납작한 수관 밑에서 그물무늬기린이 보라색 혀로 잔가지를 감아 잎을 훑고, 소등쪼기새가 목을 타고 뛰어다녀요. 가끔 앞다리를 A 자로 벌리고 목을 내려 웅덩이 물을 마시면 물에 동그란 물결이 퍼져요. K3 을 돌리면 무늬가 마사이기린이나 로스차일드기린의 것으로 바뀌고, 끝에는 흰 기린이 서요. | +| [Hanging Scroll](patterns/hanging-scroll.js) | 종이 | 어두운 도코노마 벽의 못에 족자가 걸리고, 아래 굴대가 내려가며 비단 표구 속 상아빛 종이에 그린 먹 산수를 한 줄씩 드러내요. 안개 낀 봉우리 아래 소나무와 폭포, 초가와 쪽배가 있고 붉은 낙관이 찍혀 있어요. 외풍에 조금 흔들리다 아래부터 말려 올라가면 바람 맞는 대나무가 내려오고, 달 아래 홍매와 파도 위 학이 뒤를 이어요. | +| [High Dive](patterns/high-dive.js) | 운동·경기 | 밤의 실내 다이빙장을 옆에서 봐요. 다이버가 콘크리트 타워의 지그재그 계단을 올라 10 m 플랫폼 조명 아래 서고, 1.6초 동안 공중제비를 돌며 떨어져 물에 들어가면 물속에 기포 기둥이 오르고 전광판에 심판 일곱 명의 점수가 떠요. K3 이 앞으로 뛰기부터 세 바퀴 반과 비틀기까지 다이빙을 골라요. | +| [Proscenium](patterns/proscenium.js) | 무대·공연 | 금박 프로시니엄 아래 붉은 벨벳 막이 플라이로 올라가면 달빛 바다나 숲, 밤 도시, 눈 내리는 마을을 그린 배경막이 드러나고, 브리지 조명의 빛줄기가 연무를 갈라요. 발레리나가 팔로 스폿을 받으며 도약해 무대를 건너고, 막이 내리면 막 앞에서 인사해요. 맨 아래에는 실크해트를 쓴 사람이 섞인 앞줄 관객의 뒤통수가 있어요. | +| [Sequins](patterns/sequins.js) | 직물·짜임 | 뒤집히는 스팽글을 촘촘히 단 쿠션이 패널을 채워요. 은빛 비늘이 엇갈린 줄로 겹쳐 있고, 보이지 않는 손끝이 지나가면 스팽글이 차례로 모서리를 보이며 넘어가 하트, 별, 고양이 얼굴, 번개, 파도를 분홍 면으로 그려요. 손바닥이 위에서 아래로 쓸면 다시 은빛이 돼요. | +| [Skein](patterns/skein.js) | 새 | 해 진 뒤 하늘을 올려다보면 기러기 떼가 V 자로 패널 위쪽, 북쪽으로 날아가요. 지친 선두가 한쪽 줄로 물러나면 다른 기러기가 앞에 서고, 커다란 보름달 앞을 지날 때는 새카만 실루엣이 돼요. 뒤따라온 작은 떼가 꼬리에 붙기도 해요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. From d223d2572758b0fc22abd4e029b008126364f283 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 00:26:18 +0000 Subject: [PATCH 14/15] =?UTF-8?q?index.html=20=EC=9D=84=20138=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_01GhRE8HZxGnk63uCfeHdr92 --- 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 6aa28c834..b395f51da 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -141,7 +141,7 @@

패턴 연구

- +