From 09a0d576094b1b936ca78a4d30d9a016baae920a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 02:59:16 +0000 Subject: [PATCH 01/17] =?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=2025=ED=8C=90:=20?= =?UTF-8?q?=EC=83=88=20=EC=A3=BC=EC=A0=9C=20=ED=95=98=EB=82=98=EB=A1=9C=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B4=20=EA=B0=9C?= 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_01DbfsCYkpNNJ5diTKTtPZEu From 8e6efa118b822bd3a05b1aa7f7b685665ec90fd6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 03:06:08 +0000 Subject: [PATCH 02/17] =?UTF-8?q?families.json=20=EC=97=90=20=EB=86=8D?= =?UTF-8?q?=EC=82=AC=C2=B7=EB=B0=AD=EA=B3=BC=20=ED=99=94=EC=82=B0=20?= =?UTF-8?q?=EA=B0=88=EB=9E=98=EB=A5=BC=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DbfsCYkpNNJ5diTKTtPZEu --- 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 9c78ac3d3..a863838be 100644 --- a/incoming/pattern-study/families.json +++ b/incoming/pattern-study/families.json @@ -44,5 +44,7 @@ "stage": { "label": "무대·공연", "color": "#ff0066" }, "mammal": { "label": "짐승", "color": "#998844" }, "construction": { "label": "공사·건설", "color": "#ffee00" }, - "pottery": { "label": "도자기", "color": "#b05030" } + "pottery": { "label": "도자기", "color": "#b05030" }, + "farm": { "label": "농사·밭", "color": "#449911" }, + "volcano": { "label": "화산", "color": "#bb2200" } } From 3269c2805779d15e2a86f1122e603296ade5d05a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 03:30:36 +0000 Subject: [PATCH 03/17] ferris-wheel: lakeside Ferris wheel whose spoke and rim lights run six shows over its own reflection Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DbfsCYkpNNJ5diTKTtPZEu --- .../pattern-study/patterns/ferris-wheel.js | 408 ++++++++++++++++++ 1 file changed, 408 insertions(+) create mode 100644 incoming/pattern-study/patterns/ferris-wheel.js diff --git a/incoming/pattern-study/patterns/ferris-wheel.js b/incoming/pattern-study/patterns/ferris-wheel.js new file mode 100644 index 000000000..744e86b87 --- /dev/null +++ b/incoming/pattern-study/patterns/ferris-wheel.js @@ -0,0 +1,408 @@ +// ===== Patternflow pattern ===== +// Title: Ferris Wheel +// Family: fair +// Idea: A big Ferris wheel turns slowly on a lakeside promenade at night with its gondolas hanging level, the light strips on its spokes and rim run one light show after another, and the lake below breaks the wheel and its lights into ripples. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 25, after the light shows of the observation wheels on lake and river fronts +// Round: 25 +// =============================== +// @matrix 64x128 +// @knobs Lights=0..1, Speed=0.2..3, Spokes=12..32, Wind=0..1 +// +// Knob 1: Lights. Turns the show's three colours round the colour wheel together with the glow on the sky and the water, starting from azure, rose and violet. +// Knob 2: Speed. How fast the night runs: at 2 the wheel turns once in 40 s and each show lasts 6.5 s; at 0.2 the wheel, the shows, the clouds and the ripples all run ten times slower. +// Knob 3: Spokes. How many lit spokes the wheel has, 12 to 32. Up to 18 there is a gondola at the end of every spoke, past that at every other one. +// Knob 4: Wind. Near 0 the lake is a mirror with a slow swell under a few wisps of cloud; towards 1 cloud blows in lit from below by the wheel, the gondolas swing on their pivots and the reflection breaks into long streaks of light. +// Buttons: K1 tap starts the next show at once. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, round, sin, cos, sqrt, exp, abs, atan2, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const fr = (v) => v - floor(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 F32 = (n) => new Float32Array(n); +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 hsv(h, s, v) { + h = fr(h); + const f = (n) => { + const k = (n + h * 6) % 6; + return 255 * v * (1 - s * max(0, min(k, 4 - k, 1))); + }; + return [f(5), f(3), f(1)]; +} + +// The wheel: hub, outer rim with the lights, inner rim, and the truss between. +const CX = 32, CY = 38.5, R = 29.5, RI = 26.3, HUB = 3.6; +// The first row of the lake, which is the line the scene mirrors in, the +// promenade the legs stand on, and how much the reflection is squeezed. +const SEA = 87, FOOT = 84, SQ = 0.72; +// Light bins along each spoke and round the rim; the shows paint these. +const NB = 24, NR = 120; +const SHOWS = 6, DUR = 6.5, FADE = 0.35; +const CLOUD = [12, 14, 28], LAKE = [3, 7, 22], TINT = [0.85, 0.92, 1]; +// A sine table for the ripples, which take five sines an LED. +const SN = F32(1024).map((_, i) => sin((i * TAU) / 1024)); +const sn = (v) => SN[(v * 162.97) & 1023]; +const STEEL = [26, 28, 42], LEG = [22, 24, 38], WARM = [255, 214, 150], WHITE = [255, 250, 240]; + +export function setup(params) { + const P = (params.s = { + t: 0, wt: 0, co: 0, rot: 0.2, st: 0, sh: 0, prev: -1, ps: 0, ns: 12, ng: 12, wind: 0.06, h: 0.53, + f: F32(N * 3), cn: F32(256 * 76), sky: F32(SEA * W * 3), gm: F32(SEA * W), fg: F32(N * 3), fm: F32(N), ga: F32(N * 3), + sp: F32(32 * NB * 3), rm: F32(NR * 3), hb: [0, 0, 0], sp2: F32(32 * NB * 3), rm2: F32(NR * 3), hb2: [0, 0, 0], + stars: [], people: [], cab: [], + }); + // Night sky, paler towards the horizon, and the hills behind the far shore + // with a few houses lit. + for (let y = 0; y < SEA; y++) + for (let x = 0; x < W; x++) { + const v = y / SEA, o = (y * W + x) * 3, hy = 77 + 2.4 * sin(x * 0.11 + 0.7) + 1.4 * sin(x * 0.29 + 2); + let c = [3 + 14 * v * v, 5 + 18 * v * v, 22 + 46 * v * v]; + if (y > hy) c = [7, 8, 18]; + if (y > hy + 1 && hash(x, y) < 0.025) c = [120, 96, 60]; + P.sky.set(c, o); + // Where the wheel's light lands on the haze: round the wheel and low over the shore. + const r = sqrt((x + 0.5 - CX) ** 2 + (y + 0.5 - CY) ** 2); + P.gm[y * W + x] = 0.75 * exp(-(max(0, r - 18) ** 2) / 420) + 0.45 * exp(-(SEA - y) / 22); + } + // Cloud: three octaves of value noise 256 wide, so it can scroll round. + const seed = floor(rnd() * 999); + const vn = (x, y, cx, cy, s) => { + const nx = 256 / cx, gx = x / cx, gy = y / cy, ix = floor(gx), iy = floor(gy), u = sm(0, 1, gx - ix), v = sm(0, 1, gy - iy); + const h = (i, j) => hash(((ix + i) % nx) + s, iy + j); + return mix(mix(h(0, 0), h(1, 0), u), mix(h(0, 1), h(1, 1), u), v); + }; + for (let y = 0; y < 76; y++) for (let x = 0; x < 256; x++) P.cn[y * 256 + x] = 0.55 * vn(x, y, 32, 9, seed) + 0.3 * vn(x, y, 16, 5, seed + 7) + 0.15 * vn(x, y, 8, 3, seed + 13); + for (let i = 0; i < 36; i++) P.stars.push([floor(rnd() * W), floor(rnd() * 46), 40 + rnd() * 70, rnd() * TAU, 0.6 + rnd()]); + // The promenade, the boarding platform with its stairs, two lamps and the + // ticket booth, kept as one layer with its own coverage. + const fg = (x, y, c, a = 1) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + P.fg.set(c, (y * W + x) * 3); + P.fm[y * W + x] = a; + }; + const glow = (gx, gy, c, k, s) => { + for (let y = max(0, gy - 12); y < min(SEA, gy + 12); y++) + for (let x = max(0, gx - 12); x < min(W, gx + 12); x++) { + const g = k * exp(-((x - gx) ** 2 + (y - gy) ** 2) / s); + for (let n = 0; n < 3; n++) P.ga[(y * W + x) * 3 + n] += c[n] * g; + } + }; + for (let x = 0; x < W; x++) { + fg(x, 84, [34, 28, 32]); + fg(x, 85, [28, 24, 30]); + fg(x, 86, [64, 60, 70]); + } + for (let x = 22; x <= 42; x++) fg(x, 77, [78, 74, 92]), fg(x, 78, [30, 30, 42]); + for (const x of [24, 32, 40]) for (let y = 79; y < 84; y++) fg(x, y, [22, 22, 32]); + for (let i = 0; i < 6; i++) fg(21 - i, 78 + i, [60, 56, 70]), fg(21 - i, 79 + i, [26, 24, 34]); + for (const lx of [5, 46]) { + for (let y = 76; y < 84; y++) fg(lx, y, [36, 36, 48]); + fg(lx, 75, [255, 230, 180]); + fg(lx - 1, 75, [120, 100, 70]); + fg(lx + 1, 75, [120, 100, 70]); + glow(lx, 76, WARM, 0.32, 14); + } + for (let x = 53; x <= 62; x++) { + const st = ((x - 53) >> 1) & 1 ? [226, 218, 206] : [200, 36, 56]; + fg(x, 72, st); + if (!(x & 1)) fg(x, 73, st); + for (let y = 74; y < 84; y++) fg(x, y, x === 53 || x === 62 ? [40, 22, 30] : [70, 34, 46]); + } + for (let y = 75; y <= 78; y++) for (let x = 55; x <= 60; x++) fg(x, y, [255, 196, 120].map((v) => v * (0.85 - 0.08 * (y - 75)))); + for (const [x, y] of [[58, 76], [57, 78], [58, 78], [59, 78]]) fg(x, y, [40, 24, 24]); + fg(58, 77, [56, 32, 30]); + for (let x = 54; x <= 61; x++) fg(x, 79, [160, 116, 70]), fg(x, 81, (x & 1) ? [240, 200, 90] : [110, 70, 40]); + glow(58, 80, [255, 180, 100], 0.28, 18); + // People on the promenade, a queue at the foot of the stairs and two who walk. + for (const [x, w] of [[9, 0], [14, 0], [17, 0], [49, 0], [27, 2.2], [40, -1.6]]) + P.people.push({ x: x + 0.5, w, c: hsv(rnd(), 0.5, 0.25 + 0.15 * rnd()), sk: rnd() < 0.5 }); + for (let i = 0; i < 32; i++) P.cab.push([0.55 + 0.45 * rnd(), rnd() < 0.4]); +} + +function next(P) { + P.prev = P.sh; + P.ps = P.st; + P.sh = (P.sh + 1) % SHOWS; + P.st = 0; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + const v = cl(k[1], 0.2, 3) / 2; + let ns = round(cl(k[2], 12, 32)); + if (ns > 18) ns += ns & 1; + Object.assign(P, { h: fr(0.53 + cl(k[0], 0, 1)), ns, ng: ns > 18 ? ns / 2 : ns, wind: cl(k[3], 0, 1) }); + P.t += dt; + P.wt += dt * v * (0.7 + 1.5 * P.wind); + P.co += dt * v * (1 + 10 * P.wind); + P.rot += (dt * v * TAU) / 40; + P.st += dt * v; + if ((input.btnPressed && input.btnPressed[0]) || P.st >= DUR) next(P); + for (const q of P.people) + if (q.w) { + q.x += q.w * dt * v; + if (q.x > W + 3) q.x = -3; + if (q.x < -3) q.x = W + 3; + } +} + +// One show at its time s paints the spoke bins, the rim bins and the hub. +function paint(P, sh, s, SP, RM, HB) { + const ns = P.ns, [A, B, C] = P.pal, rot = P.rot; + const spk = (k, b, c, I) => { + const o = (k * NB + b) * 3; + (SP[o] = c[0] * I), (SP[o + 1] = c[1] * I), (SP[o + 2] = c[2] * I); + }; + const rim = (j, c, I) => ((RM[j * 3] = c[0] * I), (RM[j * 3 + 1] = c[1] * I), (RM[j * 3 + 2] = c[2] * I)); + const lerp = (a, b, t) => [mix(a[0], b[0], t), mix(a[1], b[1], t), mix(a[2], b[2], t)]; + const hub = (c, I) => ((HB[0] = c[0] * I), (HB[1] = c[1] * I), (HB[2] = c[2] * I)); + const uB = (b) => (b + 0.5) / NB; + if (sh === 0) { + // Light runs out along the spokes, brightest where a beam sweeps round + // against the turn; marquee bulbs chase round the rim and three white + // comets run through them. + const sw = fr(-s * 0.3), mq = floor(s * 9); + for (let k = 0; k < ns; k++) { + const dl = fr(k / ns - sw), bm = exp(-dl / 0.22), wt = 0.5 * exp(-dl * ns * 1.5); + for (let b = 0; b < NB; b++) { + const u = uB(b), ds = exp(-(((fr(u * 2 - s * 1.1) - 0.5) / 0.17) ** 2)); + spk(k, b, lerp(lerp(C, A, u), WHITE, wt), 0.14 + 0.25 * bm + 0.61 * ds * (0.45 + 0.55 * bm)); + } + } + for (let j = 0; j < NR; j++) { + let I = 0; + for (let m = 0; m < 3; m++) I = max(I, exp(-fr(fr(s * 0.42 + m / 3) - j / NR) / 0.04)); + rim(j, lerp(B, WHITE, I), max(I, (floor(j / 2) + mq) % 3 ? 0.22 : 0.9)); + } + hub(A, 0.8); + } else if (sh === 1) { + // A colour wipe across the whole wheel, lit full, with a white front. + const X = CX + 35 * cos((PI * s) / 3.25), f0 = 0.88 * sm(0, 0.5, s); + const col = (x) => lerp(lerp(A, B, sm(X - 2.5, X + 2.5, x)), WHITE, 0.7 * exp(-(((x - X) / 2.2) ** 2))); + for (let k = 0; k < ns; k++) { + const c = cos(rot + (TAU * k) / ns); + for (let b = 0; b < NB; b++) spk(k, b, col(CX + (HUB + uB(b) * (R - HUB)) * c), f0); + } + for (let j = 0; j < NR; j++) rim(j, col(CX + R * cos(rot + (TAU * (j + 0.5)) / NR)), f0); + hub(col(CX), f0); + } else if (sh === 2) { + // A spiral arm winds out from the hub and round the wheel. + for (let k = 0; k < ns; k++) + for (let b = 0; b < NB; b++) { + const u = uB(b), v = fr(u * 0.9 + k / ns - s * 0.45); + spk(k, b, lerp(B, C, u), 0.07 + 0.93 * exp(-(((v - 0.5) / 0.2) ** 2))); + } + for (let j = 0; j < NR; j++) rim(j, A, fr((j / NR) * ns * 0.5 - s * 0.9) < 0.5 ? 0.95 : 0.12); + hub(B, 0.9); + } else if (sh === 3) { + // Spoke after spoke lights up round the wheel, holds, and blinks out. + const n = (s / 3.6) * ns, blink = s > 4.8 && fr((s - 4.8) * 2.6) > 0.5; + for (let k = 0; k < ns; k++) { + const I = blink ? 0.04 : k < floor(n) ? 0.9 : k === floor(n) ? fr(n) : 0.05; + for (let b = 0; b < NB; b++) spk(k, b, k === floor(n) ? WHITE : k & 1 ? B : A, I); + } + for (let j = 0; j < NR; j++) rim(j, C, blink ? 0.04 : j / NR < n / ns ? 0.95 : 0.08); + hub(WHITE, blink ? 0.1 : 0.85); + } else if (sh === 4) { + // Rings run out from the hub, each a colour, and flash the rim on arriving. + SP.fill(0, 0, ns * NB * 3); + let Ir = 0.08, Ih = 0.2, cr = A; + for (let m = 0; m < 3; m++) { + // Rings are born in the order A, C, B, so each takes the next colour. + const f = fr(s * 0.4 + m / 3), c = P.pal[m], e = exp(-(((1 - f) / 0.06) ** 2)); + for (let k = 0; k < ns; k++) + for (let b = 0; b < NB; b++) { + const I = exp(-(((uB(b) - f) / 0.07) ** 2)), o = (k * NB + b) * 3; + for (let n = 0; n < 3; n++) SP[o + n] += c[n] * I; + } + if (e > Ir) (Ir = e), (cr = c); + Ih = max(Ih, exp(-((f / 0.08) ** 2))); + } + for (let k = 0; k < ns * NB * 3; k++) SP[k] += 14; + for (let j = 0; j < NR; j++) rim(j, cr, max(0.1, Ir)); + hub(WHITE, Ih); + } else { + // Every spoke its own colour, the rainbow turning round, with sparkles. + const tw = floor(s * 7); + for (let k = 0; k < ns; k++) { + const c = hsv(P.h + fr(k / ns + s * 0.12), 0.85, 1); + for (let b = 0; b < NB; b++) hash(k * NB + b, tw) < 0.07 ? spk(k, b, WHITE, 1) : spk(k, b, c, 0.72); + } + for (let j = 0; j < NR; j++) rim(j, hsv(P.h + fr(j / NR - s * 0.25), 0.8, 1), 0.75); + hub(WHITE, 0.8); + } +} + +let F; +const add = (x, y, c, k) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + for (let n = 0; n < 3; n++) F[o + n] += c[n] * k; +}; +const put = (x, y, c, k = 1) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + for (let n = 0; n < 3; n++) F[o + n] = c[n] * k; +}; + +// A leg of the A-frame from the hub to the promenade, a dark girder. +function leg(ax, ay, bx, by, hw, edge) { + const dx = bx - ax, dy = by - ay, ll = dx * dx + dy * dy; + for (let y = floor(ay); y <= by; y++) + for (let x = max(0, floor(min(ax, bx) - 2)); x <= min(W - 1, max(ax, bx) + 2); x++) { + const qx = x + 0.5 - ax, qy = y + 0.5 - ay, t = cl((qx * dx + qy * dy) / ll, 0, 1), ex = qx - dx * t, ey = qy - dy * t, d = sqrt(ex * ex + ey * ey); + const w = cl(hw + 0.5 - d, 0, 1); + if (w <= 0) continue; + const o = (y * W + x) * 3, lit = 0.5 + 0.5 * cl((d - hw + 0.6) * 1.5, 0, 1); + const fl = 0.32 * exp(-(FOOT - y) / 9); + for (let n = 0; n < 3; n++) F[o + n] = mix(F[o + n], LEG[n] + edge[n] * lit + WARM[n] * fl, w); + } +} + +export function draw(display, params) { + const P = params.s; + if (!P || !P.f) return; + F = P.f; + const ns = P.ns, rot = P.rot, t = P.t; + P.pal = [hsv(P.h, 0.85, 1), hsv(P.h + 0.42, 0.8, 1), hsv(P.h + 0.2, 0.75, 1)]; + const SP = P.sp, RM = P.rm, HB = P.hb; + paint(P, P.sh, P.st, SP, RM, HB); + if (P.prev >= 0 && P.st < FADE) { + const q = P.st / FADE; + paint(P, P.prev, P.ps + P.st, P.sp2, P.rm2, P.hb2); + for (let i = 0; i < ns * NB * 3; i++) SP[i] = mix(P.sp2[i], SP[i], q); + for (let i = 0; i < NR * 3; i++) RM[i] = mix(P.rm2[i], RM[i], q); + for (let n = 0; n < 3; n++) HB[n] = mix(P.hb2[n], HB[n], q); + } + // The wheel's light in the haze: the mean colour of every bin. + const gc = [0, 0, 0], ws = ((R - HUB) / NB) * ns, wr = (TAU * R) / NR; + for (let i = 0; i < ns * NB; i++) for (let n = 0; n < 3; n++) gc[n] += SP[i * 3 + n] * ws; + for (let j = 0; j < NR; j++) for (let n = 0; n < 3; n++) gc[n] += RM[j * 3 + n] * wr * ns; + for (let n = 0; n < 3; n++) gc[n] /= ns * NB * ws + NR * wr * ns; + // The haze takes a dim show nearly whole and a bright one by less. + const gk = 0.62 / (1 + (1.6 * max(...gc)) / 255); + for (let n = 0; n < 3; n++) gc[n] *= gk; + + // Sky, haze, stars and a thin moon. + for (let i = 0, o = 0; i < SEA * W; i++, o += 3) { + const g = P.gm[i]; + for (let n = 0; n < 3; n++) F[o + n] = P.sky[o + n] + gc[n] * g; + } + for (const [x, y, b, ph, sp] of P.stars) add(x, y, [1, 1, 1.15], b * (0.65 + 0.35 * sin(t * sp + ph))); + for (let y = 3; y < 11; y++) + for (let x = 51; x < 60; x++) { + const a = (x + 0.5 - 55) ** 2 + (y + 0.5 - 7) ** 2, b = (x + 0.5 - 56.6) ** 2 + (y + 0.5 - 6) ** 2; + const k = cl(3.4 - sqrt(a), 0, 1) * cl(sqrt(b) - 2.9, 0, 1); + if (k > 0) add(x, y, [236, 228, 196], k); + } + // Clouds come in on the wind, lit from below by the wheel. + const wd = P.wind, th = 0.68 - 0.4 * wd, co = floor(P.co), cf = P.co - co; + for (let y = 0; y < 76; y++) { + const hf = sm(76, 56, y); + for (let x = 0; x < W; x++) { + const c0 = y * 256, n = mix(P.cn[c0 + ((x + co) & 255)], P.cn[c0 + ((x + co + 1) & 255)], cf), dn = sm(th - 0.04, th + 0.22, n) * hf * 0.7; + if (dn <= 0) continue; + const i = y * W + x, o = i * 3, g = 3 * (P.gm[i] + 0.2); + for (let k = 0; k < 3; k++) F[o + k] = mix(F[o + k], CLOUD[k] + gc[k] * g, dn); + } + } + + // Spokes, truss and rims, lit from the show's bins. + const st = STEEL.map((v, n) => v + gc[n] * 0.5); + for (let y = 7; y <= 70; y++) { + const dy = y + 0.5 - CY; + for (let x = 0; x < W; x++) { + const dx = x + 0.5 - CX, r2 = dx * dx + dy * dy; + if (r2 > (R + 1.4) ** 2) continue; + const r = sqrt(r2), o = (y * W + x) * 3; + let a = (atan2(dy, dx) - rot) / TAU; + a -= floor(a); + const q = a * ns, kk = round(q), d = (abs(q - kk) * TAU * r) / ns; + const wsp = r > HUB && r < R ? cl(1.25 - 1.5 * d, 0, 1) : 0; + const wrm = cl(1.25 - 1.5 * abs(r - R), 0, 1); + let wst = cl(1 - 1.6 * abs(r - RI), 0, 1) * 0.8; + if (r > RI && r < R) { + const z = abs(fr(a * ns * 2) - 0.5) * 2, tt = (r - RI) / (R - RI); + wst = max(wst, cl(1 - 2.2 * abs(z - tt), 0, 1) * 0.7); + } + const occ = max(wsp, wrm, wst); + if (occ <= 0) continue; + const so = ((kk % ns) * NB + min(NB - 1, floor(((r - HUB) / (R - HUB)) * NB))) * 3, ro = (floor(a * NR) % NR) * 3; + for (let n = 0; n < 3; n++) F[o + n] = F[o + n] * (1 - 0.85 * occ) + st[n] * occ + SP[so + n] * wsp + RM[ro + n] * wrm; + } + } + + // The gondolas hang level from the rim, swinging in the wind. + const [A, B, C] = P.pal, step = ns / P.ng; + for (let i = 0; i < P.ng; i++) { + const ang = rot + (TAU * i * step) / ns, px = CX + R * cos(ang), py = CY + R * sin(ang); + const sw = wd * (0.45 * sin(t * 1.8 + i * 1.1) + 0.25 * sin(t * 3.1 + i * 2.3)) + 0.05 * sin(t * 1.4 + i); + const gx = round(px - 0.5), gy = round(py - 0.5), cx = round(px - 0.5 + 3.2 * sin(sw)), [wk, empty] = P.cab[i]; + const body = [A, B, C][i % 3].map((v, n) => v * 0.62 + gc[n]); + put(gx, gy, [150, 150, 170]); + put(round(px - 0.5 + 1.2 * sin(sw)), gy + 1, [60, 60, 76]); + for (let dx = -1; dx <= 1; dx++) put(cx + dx, gy + 2, body), put(cx + dx, gy + 5, body, 0.75); + for (let dy = 3; dy <= 4; dy++) { + put(cx - 2, gy + dy, body, 0.85); + put(cx + 2, gy + dy, body, 0.85); + for (let dx = -1; dx <= 1; dx++) put(cx + dx, gy + dy, WARM, (empty || dx || dy > 3 ? 0.78 : 0.2) * wk); + } + } + + // The A-frame in front of the wheel, and the hub on it. + const edge = gc.map((v) => v * 1.4); + leg(CX, CY, 12, FOOT, 0.85, edge); + leg(CX, CY, 52, FOOT, 0.85, edge); + for (let y = 33; y <= 44; y++) + for (let x = 26; x <= 38; x++) { + const r = sqrt((x + 0.5 - CX) ** 2 + (y + 0.5 - CY) ** 2), k = cl(HUB + 0.5 - r, 0, 1); + if (k <= 0) continue; + const o = (y * W + x) * 3, core = exp(-(r * r) / 2.5); + for (let n = 0; n < 3; n++) F[o + n] = mix(F[o + n], 30 + HB[n] * (1 - r / 6) + WHITE[n] * core * 0.6, k); + } + + // The shore: promenade, platform, lamps, booth, the warm light they throw. + for (let i = 70 * W, o = i * 3; i < SEA * W; i++, o += 3) { + const m = P.fm[i]; + for (let n = 0; n < 3; n++) F[o + n] = mix(F[o + n], P.fg[o + n], m) + P.ga[o + n]; + } + for (let x = 23; x <= 41; x += 2) put(x, 76, B, floor(t * 4 - x / 2) % 3 ? 0.35 : 0.85); + for (let x = 53; x <= 62; x += 2) put(x, 71, WARM, floor(t * 5 + x / 2) % 2 ? 0.3 : 1); + for (const q of P.people) { + const x = floor(q.x), gait = q.w ? floor(q.x * 1.5) & 1 : 0; + put(x, 79, q.sk ? [150, 110, 86] : [70, 52, 44]); + for (let y = 80; y <= 82; y++) put(x, y, q.c), put(x + 1, y, q.c, 0.8); + put(x + gait, 83, [16, 16, 24]); + put(x + 1 - gait, 83, [16, 16, 24], 0.7); + } + + // The lake: the scene above, mirrored, squeezed and broken by the ripples. + const wt = P.wt, gw = 0.55 + 0.35 * wd, pw = 3 - 1.2 * wd, sp = 0.5 + 6 * wd; + const row = (ys, sx) => (cl(round(ys), 0, SEA - 1) * W + sx) * 3; + for (let y = SEA; y < H; y++) { + const d = y + 0.5 - SEA, dn = d / (H - SEA), amp = (0.35 + 1.9 * wd) * (0.45 + dn), rf = 0.7 - 0.3 * dn; + for (let x = 0; x < W; x++) { + const ox = amp * (0.6 * sn(d * 0.75 - wt * 1.9 + x * 0.04) + 0.4 * sn(d * 2.3 + x * 0.37 - wt * 3.3)); + const ys = SEA - 0.5 - d / SQ + amp * 0.45 * sn(x * 0.45 + d * 1.1 - wt * 2.6), sx = cl(round(x + ox), 0, W - 1); + const hl = (0.5 + 0.5 * sn(d * 1.7 - wt * 1.4 + 1.6 * sn(x * 0.15 + d * 0.3 + wt * 0.5))) ** pw, o = (y * W + x) * 3; + // Choppy water stretches each light into a streak: the brightest of the + // rows a wave face can show here, above the mirror point and below it. + const s1 = row(ys, sx), s0 = row(ys - sp, sx), s2 = row(ys + sp, sx), s3 = row(ys - sp / 2, sx), s4 = row(ys + sp / 2, sx); + for (let n = 0; n < 3; n++) + F[o + n] = LAKE[n] * (1 + dn * 0.5) + gc[n] * gw * hl + (0.55 * F[s1 + n] + 0.5 * max(F[s0 + n], F[s2 + n], F[s3 + n], F[s4 + n])) * rf * TINT[n]; + } + } + 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 1553d130ef8283a35f8a236b1ac89d814f758940 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 03:34:25 +0000 Subject: [PATCH 04/17] rice-terraces: hillside paddies that mirror the sunrise, step by step up to the ridge Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DbfsCYkpNNJ5diTKTtPZEu --- .../pattern-study/patterns/rice-terraces.js | 342 ++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 incoming/pattern-study/patterns/rice-terraces.js diff --git a/incoming/pattern-study/patterns/rice-terraces.js b/incoming/pattern-study/patterns/rice-terraces.js new file mode 100644 index 000000000..eb7de7e48 --- /dev/null +++ b/incoming/pattern-study/patterns/rice-terraces.js @@ -0,0 +1,342 @@ +// ===== Patternflow pattern ===== +// Title: Rice Terraces +// Family: farm +// Idea: Rice terraces climb in curved steps from the foot of the panel to a forested ridge, every flooded paddy a mirror of the sunrise between dark earth walls with a thin lit rim, while clouds slide across the mirrors and the mist lifts behind the ridge. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 25, after the Hani rice terraces of Yuanyang and the terraces of Banaue +// Round: 25 +// =============================== +// @matrix 64x128 +// @knobs Season=0..1, Speed=0.2..3, Terraces=12..24, Hour=0..1 +// +// Knob 1: Season. At 0 the paddies are flooded in spring and each one holds the sky; then rows of seedlings stand in the water, the rice closes over it green, and at 1 it is gold with four terraces in ten already cut to stubble. +// Knob 2: Speed. At 2 a cloud crosses the sky in about seventeen seconds, its reflection slides faster over the near paddies, and the valley mist rises and settles every quarter minute; at 0.2 it all runs ten times slower. +// Knob 3: Terraces. How many steps climb from the foot of the panel to the ridge, twelve to twenty-four. The near ones are deep behind walls two or three LEDs high, and the far ones thin to a line over the water. +// Knob 4: Hour. Sunrise just after 0, the sun's path glinting in the paddies under it and mist behind the ridge; then morning, noon, a gold afternoon and sunset, and at 1 night, with stars and a lamp in each village door. +// Buttons: K1 tap opens the sluice at the top: water spills over each bund in turn down to the foot, and ripples run across every paddy it fills. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, ceil, round, sin, cos, abs, exp, log, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const mix = (a, b, t) => a + (b - a) * t; +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +const F32 = (n) => new Float32Array(n); +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; +} + +// Terrace 0 sits just under the panel, the last one at the ridge. The sky's +// horizon row, and the classes a LED can belong to. +const [YB, YT, YH] = [134, 38, 34]; +const [SKY, FAR, NEAR, WOOD, WATER, WALL, RIM] = [0, 1, 2, 3, 4, 5, 6]; +// The row of the sky (0 the zenith, 31 the horizon) that the water on each +// panel row holds: low sky in the far paddies, the sky overhead in the near ones. +const RR = F32(H).map((v, y) => 26 - 20 * cl((y - 36) / 92, 0, 1) ** 0.9); +const FOAM = [220, 236, 250], LAG = 0.22, AT = [0.82, 0.88, 1], MUD = [7, 5.6, 4.4], STUB = [150, 120, 70]; + +// Light by the hour: sky at the zenith, sky at the horizon, sunlit cloud, +// then ambient and direct light and how much mist lies in the valley. +const HOURS = [ + [0, 14, 22, 70, 150, 100, 150, 255, 150, 140, 0.3, 0.12, 1], + [0.07, 34, 64, 150, 255, 176, 110, 255, 200, 150, 0.5, 0.55, 0.95], + [0.25, 40, 100, 200, 190, 215, 235, 255, 245, 225, 0.7, 0.6, 0.4], + [0.5, 36, 96, 206, 170, 205, 240, 255, 255, 250, 0.74, 0.66, 0.15], + [0.72, 40, 80, 180, 250, 200, 130, 255, 215, 150, 0.64, 0.56, 0.2], + [0.86, 40, 36, 100, 255, 120, 70, 255, 140, 90, 0.42, 0.36, 0.4], + [1, 6, 8, 28, 40, 36, 70, 60, 60, 90, 0.16, 0, 0.6], +]; +// The season: earth wall, bund rim, rice. Flooded, planted, green, ripening, cut. +const SEASONS = [ + [0, 66, 48, 36, 206, 184, 150, 120, 200, 70], + [0.35, 60, 74, 40, 150, 196, 96, 110, 204, 60], + [0.65, 42, 84, 34, 110, 176, 64, 46, 156, 40], + [0.85, 80, 84, 40, 186, 176, 84, 184, 184, 50], + [1, 96, 76, 44, 224, 184, 96, 244, 178, 40], +]; +function key(T, u) { + let i = 0; + while (i < T.length - 2 && u > T[i + 1][0]) i++; + const a = T[i], b = T[i + 1], f = cl((u - a[0]) / (b[0] - a[0]), 0, 1); + return a.map((v, c) => mix(v, b[c], f)); +} + +export function setup(params) { + const P = (params.s = { + t: 0, ox: 0, mt: 0, n: 0, surge: 99, sk: F32(32 * 3), cl: F32(128 * 32), f: F32(N * 3), + cls: new Uint8Array(N), kk: new Int8Array(N), e: F32(N), dep: F32(N), tx: F32(N), walk: [[10 + rnd() * 44, 1], [10 + rnd() * 44, -1]], + }); + P.ox = rnd() * 128; + // Egrets: where each stands (column, how far up the steps), and how long it waits. + P.birds = [0, 1, 2].map(() => ({ x: 6 + rnd() * 52, kf: 0.05 + rnd() * 0.6, w: 1 + rnd() * 8, p: -1 })); + // Cloud noise, 128 columns that wrap and 32 rows from the zenith down. + for (let i = 0; i < 128 * 32; i++) { + const x = i & 127, y = i >> 7, f = (X) => 0.55 * vn(X * 0.06, y * 0.22) + 0.3 * vn(X * 0.15, y * 0.5 + 9) + 0.15 * vn(X * 0.4, y + 3); + P.cl[i] = mix(f(x), f(x - 128), x / 128); + } + for (let i = 0; i < N; i++) P.tx[i] = vn((i & 63) * 0.5, (i >> 6) * 0.6); +} + +// The terraces for n steps. A level u climbs the panel in steps that shrink +// with distance, bent by slow noise into spurs and gullies; each whole value +// of u is a bund. For each LED: which terrace, and whether it is water, the +// earth wall under a bund, or the bund's lit rim. +function build(P, n) { + const q = 0.28 ** (1 / n), L = -log(q), b0 = ((YB - YT) * L) / (1 - q ** n); + const V = (y) => ((y = 1 - ((YB - y) * L) / b0), y > 1e-4 ? log(y) / -L : 99); + const { cls, kk, e, dep } = P, u = F32(H + 20), yc = F32(n + 6); + const put = (x, y, k, i = y * W + x) => y >= 0 && y < H && cls[i] >= WATER && ((cls[i] = RIM), (kk[i] = k)); + P.lip = F32((n + 2) * W); + P.ridge = new Int16Array(W); + for (let x = 0; x < W; x++) { + // A spur comes down left of centre and a gully cuts in on the right. + const sp = 1.3 * exp(-(((x - 24) / 13) ** 2)) - 1 * exp(-(((x - 55) / 9) ** 2)); + for (let y = H + 19, m = -9; y >= 0; y--) { + const v0 = V(y), xs = (x - 32) / (q ** v0) ** 0.4 + 40; + const v = v0 + sp + 1.2 * (vn(xs * 0.035, v0 * 0.07) * 2 - 1 + 0.4 * (vn(xs * 0.085 + 7, v0 * 0.15 + 5) * 2 - 1)); + u[y] = m = max(m, v); + } + // Where each bund crosses this column, to a fraction of a LED. + yc.fill(H + 40); + for (let y = H + 18; y >= 0; y--) for (let k = max(-3, floor(u[y + 1]) + 1); k <= min(n, u[y]); k++) yc[k + 3] = y + (u[y] - k) / max(1e-3, u[y] - u[y + 1]); + for (let k = 0; k <= n; k++) P.lip[k * W + x] = yc[k + 3]; + let y = H - 1; + for (; y >= 0 && u[y] < n; y--) { + const k = max(-3, floor(u[y])), lip = yc[k + 4], bp = yc[k + 3] < H + 40 ? yc[k + 3] - lip : b0, d = y - lip, rh = cl(0.22 * bp - 0.6, 0, 3.5), i = y * W + x; + if (d < 1) (cls[i] = RIM), (kk[i] = k + 1); + else if (d < 1 + rh) (cls[i] = WALL), (kk[i] = k + 1), (e[i] = (d - 1) / rh); + else (cls[i] = WATER), (kk[i] = k), (e[i] = d - 1 - rh), (dep[i] = bp - 1 - rh); + } + P.ridge[x] = y; + const top = y - 5 - 3 * vn(x * 0.35, 4) - abs(sin(x * 0.9)) * 1.5; + for (; y >= 0; y--) { + const i = y * W + x; + cls[i] = y > top ? WOOD : y >= 22 + 4 * sin(0.06 * x + 0.5) + 2.5 * sin(0.17 * x + 2) ? NEAR : y >= 15 + 5 * sin(0.045 * x + 2.4) + 2 * sin(0.13 * x + 1) ? FAR : SKY; + } + } + // Close the rim where a bund steps a row or more from one column to the next. + for (let k = 1; k <= n; k++) + for (let x = 0; x < W - 1; x++) { + let a = ceil(P.lip[k * W + x]), b = ceil(P.lip[k * W + x + 1]), xa = x, xb = x + 1; + if (a > b) [a, b, xa, xb] = [b, a, xb, xa]; + const m = floor((a + b) / 2); + for (let y = a + 1; y < b; y++) put(y <= m ? xa : xb, y, k); + if (b > a) put(xb, m, k); + } + // How bright each rim is: faint where the steps are thin and far, and + // brighter where the bund turns to face the light. + const lp = (k, x) => P.lip[k * W + cl(x, 0, W - 1)]; + for (let i = 0; i < N; i++) + if (cls[i] === RIM) { + const k = kk[i], x = i & 63; + e[i] = k > 0 ? cl((lp(k - 1, x) - lp(k, x) - 2) / 7, 0.2, 1) : 1; + dep[i] = k >= 0 ? lp(k, x + 1) - lp(k, x - 1) : 0; + } + for (P.kw = 1; P.lip[P.kw * W + 32] > 117; P.kw++); + P.n = n; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 12, 0.06], rd = cl(dt || 0, 0, 0.1), d = rd * (cl(k[1], 0.2, 3) / 2); + const n = round(cl(k[2], 12, 24)); + if (n !== P.n) build(P, n); + P.s = cl(k[0], 0, 1); + P.h = cl(k[3], 0, 1); + P.t += d; + P.ox += 5 * d; + P.mt += d; + P.surge += d; + if (input.btnPressed && input.btnPressed[0]) P.surge = 0; + // The ploughing team and the farmer up on the high bund walk to the end and turn. + for (const w of P.walk) { + w[0] += w[1] * 2.2 * d; + if (w[0] > 56 || w[0] < 6) w[1] = -w[1]; + } + // An egret waits, then flies to another paddy in a low arc. + for (const b of P.birds) + if (b.p >= 0) { + if ((b.p += d / 1.8) >= 1) Object.assign(b, { p: -1, x: b.tx, kf: b.tk, w: 4 + rnd() * 9 }); + } else if ((b.w -= d) <= 0) Object.assign(b, { p: 0, tx: 6 + rnd() * 52, tk: 0.05 + rnd() * 0.6, y0: egY(P, b.x, b.kf) }); +} + +// Where an egret stands: a little behind the bund of its terrace. +const egY = (P, x, kf) => P.lip[max(1, round(kf * P.n)) * W + round(cl(x, 0, 63))] - 2; +function egrets(P, L, wet) { + const c = lit([250, 248, 240], L); + for (const b of P.birds) { + if (b.p < 0) { + const x = b.x, y = egY(P, x, b.kf); + put(x, y, c), put(x, y - 1, c), put(x + 1, y - 2, c), put(x, y + 1, c, 0.35 * wet), put(x, y + 2, c, 0.25 * wet); + } else { + const y1 = egY(P, b.tx, b.tk), x = mix(b.x, b.tx, b.p), y = mix(b.y0, y1, b.p) - 14 * sin(PI * b.p), up = floor(P.t * 7) & 1; + put(x, y, c), put(x - 1, y - up, c), put(x + 1, y - up, c), put(x - 2, y - 2 * up, c, 0.7), put(x + 2, y - 2 * up, c, 0.7); + } + } +} + +// The sky at table x and row r (0 the zenith, 31 the horizon), with its clouds. +function skyAt(P, K, xx, r, out) { + const s = P.sk, c = P.cl, xi = floor(xx), fx = xx - xi, i = r * 128; + const v = mix(c[i + (xi & 127)], c[i + ((xi + 1) & 127)], fx), cd = cl((v - 0.46) * 8, 0, 1) * (0.4 + 0.6 * sm(0, 6, 31 - r)); + const lit = 0.3 + 0.7 * cd; + for (let j = 0; j < 3; j++) out[j] = mix(s[r * 3 + j], mix(mix(K[1 + j], K[4 + j], 0.5) * 0.75, K[7 + j], lit), cd * 0.94); + return cd; +} + +let F; +function put(x, y, c, a = 1) { + x = round(x); + y = round(y); + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + for (let j = 0; j < 3; j++) F[o + j] = mix(F[o + j], c[j], a); +} +const lit = (c, L) => c.map((v, j) => v * L[j]); + +// Hani houses: earth walls under tall thatch, lamps in the doors at night, +// smoke from the hearth of the middle one. +const HOUSE = ["..TTT..", ".TTTTT.", "TTTTTTT", ".WWDWW.", ".WWDWW."]; +function village(P, L, night, amb) { + const th = lit([176, 128, 70], L), wa = lit([186, 150, 112], L), dr = night > 0.3 ? [255 * night, 170 * night, 80 * night] : [40, 30, 26]; + for (const [hx, dy] of [[5, 3], [13, 5], [21, 2]]) { + const by = P.ridge[hx] + dy; + for (let i = -4; i <= 4; i++) put(hx + i, by + 1, lit([70, 56, 40], L)); + HOUSE.forEach((row, j) => [...row].forEach((ch, i) => ch !== "." && put(hx + i - 3, by - 4 + j, ch === "T" ? th.map((v) => v * (j === 2 ? 0.8 : 1)) : ch === "W" ? wa : dr))); + } + const by = P.ridge[13] + 1; + for (let j = 0; j < 14; j++) put(14 + j * 0.3 + sin(P.t * 1.3 - j * 0.6) * 0.7, by - 3 - j, [220, 216, 220].map((v) => v * (0.4 + 0.6 * amb)), 0.6 * (1 - j / 14)); +} + +// Each bund lets a thread of water down its wall through a gap; when the +// sluice is open the thread runs wide and white. +function streams(P, fo, I) { + for (let k = 1; k < P.n; k++) { + const x = 5 + floor(hash(k, 11) * 54), y0 = ceil(P.lip[k * W + x]), f = I[k + 3], w = f > 0.3 ? 1 : 0; + for (let y = y0; y < y0 + 5 && y < H; y++) { + const C = P.cls[y * W + x]; + if (y > y0 && C === WATER) { + put(x, y, fo, 0.35); + break; + } + const st = (y - floor(P.t * 9)) % 3 === 0 ? 1 : 0.7; + for (let i = -w; i <= w; i++) put(x + i, y, fo.map((v) => v * st), 0.85); + } + } +} + +// A farmer ploughs behind his buffalo in a near paddy, both mirrored in the +// water, and another farmer walks a high bund. +const BUFF = ["......hh", ".BBBBBBH", "BBBBBBBHH", ".BBBBBB.."]; +function walkers(P, L, s, sk) { + const near = P.walk[0], far = P.walk[1], dir = near[1], k = P.kw, wet = 1 - sm(0.3, 0.6, s); + const feet = (x) => P.lip[k * W + round(cl(x, 0, 63))] - 1 - 3 * wet; + const fig = (x, y, c) => (put(x, y, c), wet > 0 && put(x, 2 * feet(near[0]) - y + 1, c.map((v, j) => v * 0.6 + sk[60 + j] * 0.2), 0.6 * wet)); + const bx = near[0], by = feet(bx), fx = bx - 5 * dir; + // The furrow of churned water behind them. + for (let i = 4; i < 20; i++) put(fx - i * dir, feet(fx - i * dir), [120, 100, 80].map((v, j) => v * L[j]), 0.5 * wet * (1 - i / 20)); + BUFF.forEach((row, j) => [...row].forEach((ch, i) => ch !== "." && fig(bx + (dir > 0 ? i - 4 : 4 - i), by - 3 + j, ch === "h" ? lit([230, 220, 200], L) : lit(j > 1 ? [30, 27, 30] : [70, 62, 62], L)))); + const hat = lit([240, 210, 136], L), body = lit([30, 34, 70], L); + for (const [i, j, c] of [[0, -4, hat], [-1, -3, hat], [0, -3, hat], [1, -3, hat], [0, -2, body], [0, -1, body], [-dir, -1, body], [0, 0, body]]) fig(fx + i, by + j, c); + // The plough's handle between them. + fig(fx + dir, by - 1, [70, 56, 44]), fig(fx + 2 * dir, by, [70, 56, 44]); + const k2 = k + round((P.n - k) * 0.55), gx = far[0], gy = P.lip[k2 * W + round(gx)] - 1; + put(gx, gy - 1, hat), put(gx, gy, body); +} + +export function draw(display, params) { + const P = params.s; + if (!P || !P.n) return; + const K = key(HOURS, P.h), S = key(SEASONS, P.s), f = (F = P.f), sk = P.sk, n = P.n, t = P.t, s = P.s, cls = P.cls, tx = P.tx; + const [amb, dir, mist] = [K[10], K[11], K[12]], night = sm(0.86, 1, P.h); + for (let r = 0; r < 32; r++) { + const e = sm(0, 0.65, 1 - r / 31) ** 0.8; + for (let j = 0; j < 3; j++) sk[r * 3 + j] = mix(K[4 + j], K[1 + j], e); + } + // The sun along its day from right to left. + const a = cl(P.h / 0.86, 0, 1) * PI, sx = 32 + 26 * cos(a), sy = 16 - 80 * sin(a) ** 1.5, side = sx > 32 ? 1 : -1, rs = 31 - 13 * (1 - sy / YH), sunUp = (1 - sm(11, 15, sy)) * (1 - night); + const rice1 = sm(0.1, 0.3, s), rice2 = sm(0.3, 0.6, s), cut = sm(0.88, 1, s); + const c = P.cl, ox = P.ox, col = [0, 0, 0], L = [0, 0, 0], L0 = AT.map((v, j) => amb * v + (dir * K[7 + j]) / 255); + const sh0 = (0.5 + 0.5 * sm(0, 0.2, P.h)) * (1 - night) * 0.8 * dir; + const far = AT.map((v, j) => K[1 + j] * 0.9 + 16), near = [30, 44, 70].map((v) => v * (amb + 0.3)), wood = [28, 66, 32].map((v) => v * 1.3); + // How hard water is spilling over each bund: the sluice opens at the top and + // the overflow runs down one terrace every LAG seconds. + const I = F32(n + 8), HZ = F32(n + 8), fo = FOAM.map((v, j) => mix(v, K[7 + j], 0.3) * (0.4 + 0.6 * sm(0.1, 0.5, amb))); + for (let k = -3; k <= n + 1; k++) { + const tk = P.surge - (n - k) * LAG; + HZ[k + 3] = 0.35 * cl(k / n, 0, 1) ** 1.6; + I[k + 3] = tk > 0 ? exp(-tk * 0.9) * sm(0, 0.15, tk) : 0; + } + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, o = i * 3, C = cls[i]; + // Cloud shadow on the hillside, sliding across. + const shade = cl((c[((floor(y * 0.22) + 3) & 31) * 128 + (floor(x * 0.75 + ox * 1.2 + 40) & 127)] - 0.48) * 4, 0, 1) * sh0; + for (let j = 0; j < 3; j++) L[j] = L0[j] - (shade * K[7 + j]) / 255; + if (C < WOOD) { + const r = cl(round(31 - 13 * (1 - y / YH)), 0, 31), cd = skyAt(P, K, x * 1.3 + ox, r, col); + if (C === SKY) { + const ds = Math.hypot(x - sx, (y - sy) * 1.1), g = (1 - night) * (0.5 * exp(-ds / 9) * (1 - cd * 0.6) + (ds < 2.6 ? 1 : 0)), st = night > 0.2 && hash(x, y) > 0.985 ? 200 * night : 0; + for (let j = 0; j < 3; j++) col[j] += K[7 + j] * g + st; + } else for (let j = 0; j < 3; j++) col[j] = (C === FAR ? mix(sk[84 + j], far[j], 0.45) : mix(sk[84 + j], near[j], 0.75)) * (0.9 + 0.1 * tx[i]); + } else if (C === WOOD) for (let j = 0; j < 3; j++) col[j] = wood[j] * (0.6 + 0.6 * tx[i]) * L[j]; + else { + const k = P.kk[i], ee = P.e[i], fl = I[k + 3]; + if (C === WALL) { + // Water sheets down the wall when the terrace above overflows. + const st = fl > 0.01 ? 0.85 * fl : 0, sv = st ? 0.5 + 0.5 * sin(y * 1.9 - P.surge * 14 + x * 1.3) ** 2 : 0; + for (let j = 0; j < 3; j++) col[j] = mix(S[1 + j] * L[j] * (1.05 - 0.4 * ee), fo[j] * sv, st); + } else if (C === RIM) { + const ra = ee * (0.55 + 0.45 * cl(0.5 + 0.3 * P.dep[i] * side, 0, 1)); + for (let j = 0; j < 3; j++) col[j] = mix(mix(S[1 + j] * L[j], S[4 + j] * (L[j] + 0.3) + sk[84 + j] * 0.25, ra), fo[j], fl); + } else { + // The mirror: the far paddies hold the low sky, the near ones the sky + // overhead, and each holds its own wall dark along the back. + const dp = max(2, P.dep[i]), u = cl(ee / dp, 0, 1), rr = RR[y] - 3 * u, r = round(rr); + skyAt(P, K, (x - 32) * (0.4 + r / 40) + 32 + ox + k * 1.5, r, col); + const gust = cl((c[((floor(y * 0.3) + 17) & 31) * 128 + (floor(x * 0.5 + ox * 1.6) & 127)] - 0.55) * 5, 0, 1), back = dp > 4 ? 0.5 * exp(-ee / 0.9) : 0; + // Ripples run out from the wall when water spills in from above. + const fi = I[k + 4], rip = fi > 0.01 ? fi * (0.5 + 0.5 * sin(ee * 1.7 - (P.surge - (n - k - 1) * LAG) * 9)) * 0.5 : 0; + // The sun's own path down the paddies that hold its height of sky. + const gl = sunUp && abs(x - sx) < 7 ? sunUp * exp(-(((x - sx) / 2.2) ** 2) - ((rr - rs) / 1.3) ** 2) * (1 - 0.6 * gust) * (0.75 + 0.25 * sin(x * 2.3 + y * 1.7 + t * 6)) : 0; + for (let j = 0; j < 3; j++) col[j] = mix(mix(col[j] * 0.88, sk[45 + j] * 0.55, 0.5 * gust), S[1 + j] * L[j], back * (1 - fi)) + MUD[j] * L[j] + fo[j] * rip + K[7 + j] * gl; + // Rice: seedlings in rows along the contour, then a closed crop. + const ra = max((x + k) % 2 === 0 && floor(ee) % 2 === 0 ? rice1 : 0, rice2); + if (ra > 0) { + const v = (floor(ee) & 1 ? 1 : 0.84) * (0.9 + 0.2 * hash(x, y * 7 + k)) * (1 + 0.3 * cl((gust - 0.1) * 2, 0, 1)), isCut = (k * 7 + 23) % 10 < cut * 3.5; + for (let j = 0; j < 3; j++) col[j] = mix(col[j], (isCut ? STUB[j] * (x % 3 ? 1 : 0.7) : S[7 + j]) * L[j] * v, ra); + } + } + // The far steps fade into the haze. + const hz = HZ[k + 3]; + for (let j = 0; j < 3; j++) col[j] = mix(col[j], sk[72 + j] * 0.9, hz); + } + f[o] = col[0]; + f[o + 1] = col[1]; + f[o + 2] = col[2]; + } + // The village under the forest, the bunds' outlets, the farmers. + village(P, L0, night, amb); + streams(P, fo, I); + walkers(P, L0, s, sk); + egrets(P, L0, 1 - sm(0.3, 0.6, s)); + // Mist: a bank in the valley behind the ridge and a thinner one over the steps. + const mt = P.mt, y1 = 28 + 4 * sin(mt * 0.4), y2 = 66 + 22 * sin(mt * 0.2 + 1), mc = [0, 1, 2].map((j) => mix(sk[93 + j], 250, 0.55) * (0.7 + 0.3 * amb)); + for (let y = 0; y < H; y++) { + const w1 = mist * 0.9 * exp(-(((y - y1) / 6) ** 2)), w2 = mist * 0.45 * exp(-(((y - y2) / 7) ** 2)); + if (w1 + w2 < 0.01) continue; + for (let x = 0, i = y * W; x < W; x++, i++) { + const dm = min(0.9, ((cls[i] < WOOD ? w1 : 0) + w2) * (0.4 + 0.6 * vn(x * 0.08 + mt * 0.3, y * 0.2))); + for (let j = 0; j < 3; j++) f[i * 3 + j] = mix(f[i * 3 + j], mc[j], dm); + } + } + 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 1902519ee52689129fd61cdaa7463ff88981f39b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 03:35:35 +0000 Subject: [PATCH 05/17] eruption: a stratovolcano cut in half, gas slugs climbing from the magma chamber to burst in the crater Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DbfsCYkpNNJ5diTKTtPZEu --- incoming/pattern-study/patterns/eruption.js | 472 ++++++++++++++++++++ 1 file changed, 472 insertions(+) create mode 100644 incoming/pattern-study/patterns/eruption.js diff --git a/incoming/pattern-study/patterns/eruption.js b/incoming/pattern-study/patterns/eruption.js new file mode 100644 index 000000000..fbbc6a68e --- /dev/null +++ b/incoming/pattern-study/patterns/eruption.js @@ -0,0 +1,472 @@ +// ===== Patternflow pattern ===== +// Title: Eruption +// Family: volcano +// Idea: A stratovolcano cut in half at dusk: gas slugs leave the glowing magma chamber, climb the conduit through bent beds of ochre, ash and basalt and burst in the crater, throwing bombs on arcs while lava runs down the flanks and an ash column with lightning in it climbs to the top of the panel. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 25, after the cutaway drawing of a stratovolcano in a geology textbook +// Round: 25 +// =============================== +// @matrix 64x128 +// @knobs Style=0..1, Speed=0.2..3, Pressure=0..1, Wind=-1..1 +// +// Knob 1: Style. Strombolian at 0: a slug of gas bursts in the crater every couple of seconds and throws glowing bombs on arcs. Hawaiian near 0.5: a steady lava fountain, a full lava lake and wide flows down both flanks. Plinian at 1: a conduit full of froth feeds a grey column that spreads under the top of the panel, lightning runs through it and glowing avalanches pour down the flanks. +// Knob 2: Speed. At 2 a slug takes about a second and a half to climb from the chamber to the crater; at 0.2 everything runs ten times slower. +// Knob 3: Pressure. How full the magma chamber is. It swells and brightens, bends the beds above it further up, and pushes a dike towards the right flank and a sill into the beds on the left; near 1 the dike breaks out and a flank vent pours lava of its own. +// Knob 4: Wind. Bends the ash column to either side, more the higher it rises; the ash falling out of it slants downwind, and the bombs drift a little with it. +// Buttons: K1 tap sets off a big explosion: a crater-wide flash, a shower of bombs, a burst of ash with lightning in it and a surge of lava down both flanks. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, round, sin, cos, sqrt, exp, abs, hypot } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const mix = (a, b, t) => a + (b - a) * t; +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +const rnd = () => Math.random(); +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 axis, the crater rim's half width and crest row, the crater floor, the +// old ground the cone was built on, the chamber's centre row, gravity in rows +// per second squared, and the cells of lava down each flank. +const [C, RIM, RIMY, FLOOR, BASE, CHY, G, NL] = [32, 6, 53, 58, 86, 113, 34, 26]; +// The flank below the rim: steep at the summit, flattening to the panel's edge at row 84. +const flank = (dx) => RIMY + 31 * cl((dx - RIM) / 26, 0, 2) ** 0.8; +const SURF = new Float32Array(W + 1); +for (let x = 0; x <= W; x++) { + const dx = abs(x - C); + SURF[x] = dx < RIM ? FLOOR - (FLOOR - RIMY) * (dx / RIM) ** 2 : flank(dx) + 1.1 * (vn(x * 0.3, 7.5) - 0.5) * sm(RIM, RIM + 4, dx); +} +const sAt = (X) => ((X = cl(X, 0, W - 0.001)), mix(SURF[floor(X)], SURF[floor(X) + 1], X - floor(X))); +// The conduit's centre on each row, wandering a little on its way up. +const CXA = new Float32Array(H).map((_, y) => C + 1.3 * sin((y - FLOOR) * 0.11)); +// The dike leaves the conduit at row 99 and reaches the right flank here. +const VX = CXA[99] + 20; +// How the older beds arch up over the chamber, column by column. +const BEND = new Float32Array(W).map((_, x) => exp(-(((x + 0.5 - C) / 15) ** 2))); + +// The cone's beds (lava, ochre tephra, ash, red scoria, pale pumice) and the +// older rock under it (soil, sandstone, shale, red beds, limestone, dark +// shale), each as colour index and thickness from the top down. +const CC = [[56, 50, 68], [134, 92, 40], [100, 96, 94], [112, 48, 32], [150, 130, 94]]; +const BC = [[72, 48, 32], [142, 104, 50], [74, 74, 88], [118, 54, 36], [140, 130, 108], [46, 46, 62]]; +const beds = (seq) => { + const out = []; + while (out.length < 96) for (let i = 0; i < seq.length; i += 2) for (let j = 0; j < seq[i + 1]; j++) out.push(seq[i]); + return out; +}; +const CB = beds([0, 2, 1, 2, 2, 1, 0, 3, 3, 2, 1, 3, 4, 1, 0, 2, 2, 2, 3, 2, 0, 3, 1, 2, 2, 2, 4, 2, 0, 2, 3, 3, 1, 2]); +const BB = beds([0, 2, 1, 4, 2, 3, 3, 4, 4, 3, 5, 3, 1, 5, 3, 3, 2, 4, 4, 4, 5, 3, 1, 4, 3, 4, 2, 5]); + +// Magma from crust-dark red to white heat, as 256 steps over 0..1.25. +const LUT = new Float32Array(256 * 3); +{ + const K = [[0, 40, 6, 4], [0.2, 120, 14, 4], [0.4, 205, 46, 6], [0.6, 255, 110, 16], [0.78, 255, 182, 48], [0.95, 255, 236, 140], [1.15, 255, 255, 230]]; + for (let i = 0; i < 256; i++) { + const T = (i / 255) * 1.25; + let k = 0; + while (k < K.length - 2 && T > K[k + 1][0]) k++; + const u = cl((T - K[k][0]) / (K[k + 1][0] - K[k][0]), 0, 1); + for (let c = 0; c < 3; c++) LUT[i * 3 + c] = mix(K[k][c + 1], K[k + 1][c + 1], u); + } +} +const lut = (T) => cl(round(T * 204), 0, 255) * 3; + +// Sky rows: navy overhead, violet, and the last of the dusk low behind the flanks. +const SKY = [[0, 6, 9, 38], [40, 18, 20, 64], [64, 48, 26, 80], [86, 150, 64, 70]]; + +export function setup(params) { + const F32 = (n) => new Float32Array(n); + const S = (params.s = { + f: F32(N * 3), st: new Uint8ClampedArray(N * 3), kind: new Uint8Array(N), off: F32(N), gr: F32(N), vg: F32(N), sd: F32(N), A: F32(N), hot: F32(N), + t: 0, warm: 0, next: 0.5, slugs: [], parts: [], puffs: [], bolts: [], bub: [], glow: 0, fl: 0, flx: C, fly: 30, pr: 0.5, boost: 0, nf: 0, nv: 0, ndg: 0, npd: 1.5, lake: 0.5, pend: 0, + lv: [0, 1].map(() => ({ v: F32(NL), T: F32(NL), ph: 0 })), + }); + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, X = x + 0.5, Y = y + 0.5, o = i * 3, dx = abs(X - C), s = sAt(X); + S.vg[i] = exp(-hypot(X - C, (Y - FLOOR + 1) * 0.8) / 15); + S.gr[i] = [0.86, 0.95, 1, 1.08][floor(hash(x, y + 77) * 4)]; + S.sd[i] = Y - s; + if (Y < s) { + // Sky, with a few stars high up. + let k = 0; + while (k < SKY.length - 2 && Y > SKY[k + 1][0]) k++; + const u = sm(SKY[k][0], SKY[k + 1][0], Y), st = y < 48 && hash(x, y) < 0.014 ? 50 + 110 * hash(y, x) : 0; + for (let c = 0; c < 3; c++) S.st[o + c] = mix(SKY[k][c + 1], SKY[k + 1][c + 1], u) + st * (c ? 1 : 0.9); + } else if (Y < BASE - 3 * exp(-(((X - C) / 15) ** 2))) { + // The cone: beds laid parallel to the flanks, peaked over the old summit the crater blew away. + S.kind[i] = 1; + const ls = dx >= RIM ? flank(dx) : RIMY - (RIM - sqrt(dx * dx + 0.6)) * 0.8; + const d = cl(floor(Y - ls + 1.6 * (vn(x * 0.16, y * 0.09 + 30) - 0.5)), 0, 95), c0 = Y - s < 1.4 ? [52, 44, 50] : CC[CB[d]]; + for (let c = 0; c < 3; c++) S.st[o + c] = c0[c] * S.gr[i]; + } else { + S.kind[i] = 2; + S.off[i] = 1.6 * (vn(x * 0.11, y * 0.07 + 60) - 0.5); + } + } + for (let k = 0; k < 14; k++) S.bub.push(bubble(0)); +} + +// A gas bubble somewhere low in the chamber. +function bubble(P) { + return { x: C + (rnd() - 0.5) * (16 + 7 * P) * 1.4, y: CHY + rnd() * (7.5 + 2.5 * P) * 0.6 }; +} + +function puff(S, x, y, vy, r, a, tv, ny, fade, hot = 0.6, g = 0) { + // The oldest puff gives way when there are too many. + if (S.puffs.length > 110) S.puffs.shift(); + S.puffs.push({ x, y, vx: 0, vy, rx: r, ry: r, a, tv, ny, fade, hot, g, u: 22 }); +} + +function bolt(S) { + const c = S.puffs.filter((p) => !p.g && p.y < 50 && p.rx > 3.5 && p.a > 0.25); + if (!c.length) return; + const p = c[floor(rnd() * c.length)], pts = []; + let x = p.x + (rnd() - 0.5) * p.rx, y = p.y - p.ry * 0.4; + const n = 5 + floor(rnd() * 8); + for (let j = 0; j < n; j++) pts.push([x, y]), (x += (rnd() - 0.5) * 4), (y += 1.5 + rnd() * 1.5); + const m = pts[floor(n / 2)], br = [m]; + for (let j = 0, bx = m[0], by = m[1], s = rnd() < 0.5 ? -1 : 1; j < 3; j++) br.push([(bx += s * (1 + rnd() * 2)), (by += 1 + rnd())]); + S.bolts.push({ pts, br, life: 0.24 }); + [S.flx, S.fly] = pts[floor(n / 2)]; +} + +// A burst in the crater, its bombs, its ash and its light. +function burst(S, str, haw) { + S.glow = min(2.2, S.glow + 0.9 * str); + S.boost += haw * 1.5; + // Bombs fly wide; a fan of finer spatter goes up steeper and falls back round the crater. + const nb = round((14 + 20 * str) * (1 - 0.6 * haw)); + for (let n = 0; n < nb; n++) { + const b = n % 3 ? 0 : 1, v = (b ? 24 + 22 * rnd() : 20 + 20 * rnd()) * sqrt(str) * (1 + 0.25 * S.P), a = (rnd() - 0.5) * (b ? 1.15 : 0.6); + fling(S, C + (rnd() - 0.5) * 3, FLOOR - 1, v, a, b); + } + for (let n = 0; n < 2 + (str > 0.8 ? 1 : 0); n++) puff(S, C + (rnd() - 0.5) * 3, FLOOR - 3 - n * 3, -(14 + 10 * str), 3.5 + rnd(), 1 + 0.3 * str, -8, 2 + rnd() * 12, 0.04); + // Most bursts charge the fresh ash enough for a bolt a moment later. + if (rnd() < 0.25 + 0.5 * str) S.pend = 0.5 + 0.4 * rnd(); +} + +// One bomb (b 1) or blob of spatter thrown from x, y at speed v, a radians off vertical. +const fling = (S, x, y, v, a, b = 0, vx = 0) => S.parts.push({ x, y, vx: v * sin(a) + vx, vy: -v * cos(a), T: 1, m: 0, b }); + +// Lava poured into one flank's cell k: volume v at temperature T. +function pour(L, k, v, T = 1) { + k = cl(k, 0, NL - 1); + L.T[k] = (L.T[k] * L.v[k] + T * v) / (L.v[k] + v + 1e-6); + L.v[k] += v; +} + +function step(S, d) { + const s1 = S.k1, P = S.P, w = S.wind; + const strom = 1 - sm(0.2, 0.5, s1), haw = sm(0.15, 0.45, s1) * (1 - sm(0.6, 0.85, s1)), plin = sm(0.55, 0.9, s1); + Object.assign(S, { strom, haw, plin }); + S.t += d; + const ax = 16 + 7 * P, ay = 7.5 + 2.5 * P, top = CHY - ay; + // Pressure builds in the chamber, and a slug of gas leaves for the conduit when it is ready. + S.pr = min(1, S.pr + d * (0.3 + 0.4 * P)); + // A Plinian column draws on the chamber in long surges instead. + if (plin > 0) S.pr = mix(S.pr, 0.5 + 0.5 * sin(S.t * 1.6), min(1, plin * d * 3)); + if ((S.next -= d) <= 0 && plin < 0.8) { + S.slugs.push({ y: top + 1, str: 0.6 + 0.4 * rnd() + 0.3 * P }); + S.pr = max(0, S.pr - 0.6); + S.next = mix((1.3 + 1.7 * rnd()) / (1 + 0.7 * P), 0.5 + 0.5 * rnd(), max(haw, plin)); + } + for (const g of S.slugs) { + // It speeds up as it expands near the top. + const u = cl((g.y - FLOOR) / (top - FLOOR), 0, 1); + g.y -= (16 + 30 * (1 - u)) * (1 + 0.5 * P) * d; + if (g.y <= FLOOR + 1) (g.done = 1), burst(S, g.str * (strom + 0.35 * haw + 0.25 * plin), haw); + } + S.slugs = S.slugs.filter((g) => !g.done); + // Bubbles rise through the chamber and gather under its roof by the conduit's mouth. + for (const b of S.bub) { + const ex = C - b.x, v = (3 + 4 * P + 3 * S.pr) * d; + b.x += cl(ex, -1, 1) * v * 0.5 + sin(S.t * 2.3 + b.y) * 0.3 * v; + b.y -= v; + if (((b.x - C) / ax) ** 2 + ((b.y - CHY) / ay) ** 2 > 0.8) Object.assign(b, bubble(P)); + } + // The fountain: a steady jet of spatter in Hawaiian style, surging after each slug, + // and a smaller one from the flank vent once the dike has broken out. + S.nf += d * haw * (60 + 50 * min(1, S.boost)) * (1 + 0.5 * P); + for (; S.nf >= 1; S.nf--) fling(S, C + (rnd() - 0.5) * 3, FLOOR - 1, (24 + 16 * rnd()) * (0.85 + 0.3 * P) * (1 + 0.3 * min(1, S.boost)), (rnd() - 0.5) * 0.45); + S.nv += d * 14 * sm(0.88, 1, P); + for (; S.nv >= 1; S.nv--) fling(S, VX + (rnd() - 0.5) * 2, sAt(VX) - 1, 10 + 8 * rnd(), (rnd() - 0.5) * 0.6, 0, 3); + S.boost = max(0, S.boost - d * 1.2); + // Gas and ash: a thin plume always, a roaring column in Plinian style. + S.ndg += d * (2.2 + 1.5 * haw + 11 * plin * (0.4 + S.pr)); + for (; S.ndg >= 1; S.ndg--) + if (rnd() < plin) puff(S, C + (rnd() - 0.5) * 5, FLOOR - 3, -(30 + 14 * rnd()), 5.5, 1, -20, 3 + 7 * rnd(), 0.04, 0.4); + else puff(S, C + (rnd() - 0.5) * 3, FLOOR - 2, -7, 3, 0.45 + 0.2 * haw, -6, 4 + 14 * rnd(), 0.045, 0.3); + // Plinian: part of the column collapses and pours down a flank. + if (plin > 0.3 && (S.npd -= d * plin) <= 0) { + S.npd = 2 + 2 * rnd(); + const g = rnd() < 0.5 ? -1 : 1; + for (let n = 0; n < 4; n++) puff(S, C + g * (RIM + 2 * n), RIMY + 2 * n, 0, 4, 1, 0, 0, 0.05, 1.2, g), (S.puffs[S.puffs.length - 1].u = 24 - 3 * n); + } + for (const p of S.puffs) { + if (p.g) { + // An avalanche of hot ash hugging the flank, slowing as it spreads, then lifting off. + p.x += p.g * p.u * d; + p.u = max(3, p.u - 3 * d); + p.rx += d * 2.5; + p.ry = min(8, p.ry + d * 1.5); + p.y = sAt(p.x) - p.ry * 0.35; + if (p.u < 5 || p.x < 1 || p.x > W - 1) Object.assign(p, { g: 0, vy: -3, tv: -5, ny: 14 + 10 * rnd(), fade: 0.06 }); + } else { + const rise = sm(p.ny - 2, p.ny + 14, p.y); + p.vy += (p.tv * rise - p.vy) * 1.6 * d; + p.y += p.vy * d; + p.rx = min(30, p.rx + d * (0.8 + 0.12 * abs(p.vy) + 3.5 * (1 - rise) * p.a)); + p.ry = min(12, p.ry + d * (0.6 + 0.05 * abs(p.vy))); + } + p.x += (p.vx + w * (4 + 18 * cl(1 - p.y / 60, 0, 1))) * d; + p.a -= d * p.fade; + p.hot = max(0, p.hot - d * 0.7); + } + S.puffs = S.puffs.filter((p) => p.a > 0.04 && p.x + p.rx > -4 && p.x - p.rx < W + 4 && p.y + p.ry > -8); + // Bombs and spatter fly, land, roll and cool. + for (const q of S.parts) { + if (!q.m) { + q.vy += G * d; + q.vx += w * 7 * d; + q.x += q.vx * d; + q.y += q.vy * d; + q.T -= d * (q.b ? 0.16 : 0.35); + if (q.vy > 0 && q.y >= sAt(q.x)) { + const dx = abs(q.x - C); + if (dx < RIM - 0.5) q.dead = 1; + else if (!q.b) (q.dead = 1), pour(S.lv[q.x > C ? 1 : 0], floor(dx - RIM), 0.05, max(0.5, q.T)); + else Object.assign(q, { m: 1, u: 5 + abs(q.vx) * 0.3, s: q.x > C ? 1 : -1 }); + } + } else { + q.x += q.s * q.u * d; + q.u = max(0, q.u - 6 * d); + q.y = sAt(q.x) - 0.5; + q.T -= d * 0.3; + } + if (q.T <= 0 || q.x < -2 || q.x > W + 2) q.dead = 1; + } + S.parts = S.parts.filter((q) => !q.dead); + if (S.parts.length > 400) S.parts.splice(0, S.parts.length - 400); + // Lava over the rim and down the flanks, slower as it cools and crusts. + const eff = (0.45 + 0.6 * P + 1.2 * haw) * (1 - 0.6 * plin); + for (let s = 0; s < 2; s++) { + const L = S.lv[s], v = L.v, T = L.T; + pour(L, 0, eff * (s ? 0.55 : 1) * d); + if (s && P > 0.88) pour(L, floor(VX - C - RIM), 1.4 * sm(0.88, 1, P) * d); + L.ph += d * (1 + 2 * T[2]); + for (let k = NL - 1; k >= 0; k--) { + const f = min(v[k], v[k] * (0.2 + 2.2 * T[k] * T[k]) * (k < 6 ? 1.6 : 0.9) * d); + v[k] -= f; + if (k < NL - 1) pour(L, k + 1, f, T[k]); + T[k] = max(0, T[k] - d * (0.03 + 0.015 / (v[k] + 0.2))); + if (T[k] < 0.1) v[k] = max(0, v[k] - d * 0.03); + } + } + S.lake = mix(S.lake, (1.2 + 1.2 * P + 4 * haw) * (1 - plin), min(1, d * 2)); + // Lightning in the ash. + if (rnd() < d * (0.2 + 1.3 * plin + 0.4 * S.glow)) bolt(S); + if (S.pend > 0 && (S.pend -= d) <= 0) bolt(S); + for (const b of S.bolts) b.life -= d; + S.bolts = S.bolts.filter((b) => b.life > 0); + S.fl = S.bolts.reduce((m, b) => max(m, floor(b.life * 40) % 3 === 1 ? 0.35 : 1), 0); + S.glow *= exp(-d * 2.5); +} + +// K1 tap: the big one. +function blast(S) { + S.slugs.length = 0; + S.pr = 0; + burst(S, 2.4, 0); + S.glow = 2.2; + for (let n = 0; n < 5; n++) puff(S, C + (rnd() - 0.5) * 6, FLOOR - 4, -(26 + 12 * rnd()), 4 + rnd() * 2, 1, -10, 2 + 8 * rnd(), 0.04, 1); + for (let s = 0; s < 2; s++) pour(S.lv[s], 0, 1.6); + S.pend = 0.15; + S.next = 1.5; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], sp = cl(k[1], 0.2, 3) / 2; + S.k1 = cl(k[0], 0, 1); + S.P = cl(k[2], 0, 1); + S.wind = cl(k[3], -1, 1); + // The volcano has been going for a while before you look. + if (!S.warm) { + S.warm = 1; + for (let n = round(60 + 120 * sp); n-- > 0; ) step(S, 1 / 30); + } + if (input.btnPressed && input.btnPressed[0]) blast(S); + step(S, cl(dt || 0, 0, 0.1) * sp); +} + +let F; +function put(x, y, c, a = 1) { + (x = floor(x)), (y = floor(y)); + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + for (let i = 0; i < 3; i++) F[o + i] = mix(F[o + i], c[i], a); +} +const lc = (T) => ((T = lut(T)), [LUT[T], LUT[T + 1], LUT[T + 2]]); + +export function draw(display, params) { + const S = params.s; + if (!S || S.k1 === undefined) return; + F = S.f; + const { P, t, haw, plin, kind, st, A, hot, vg, wind } = S; + const ax = 16 + 7 * P + 0.8 * S.pr, ay = 7.5 + 2.5 * P + 0.5 * S.pr, top = CHY - ay, bamp = 3 + 6 * P + 1.4 * S.pr, bend = BEND.map((v) => v * bamp); + const L = 0.3 + 0.7 * S.glow + 0.6 * haw + 0.5 * plin, fl = S.fl; + + // Sky, cone and the older beds bent up over the chamber. + for (let i = 0; i < N; i++) { + const o = i * 3, kd = kind[i]; + if (kd === 0) { + const g = L * vg[i]; + (F[o] = st[o] + 150 * g + 7 * fl), (F[o + 1] = st[o + 1] + 48 * g + 8 * fl), (F[o + 2] = st[o + 2] + 16 * g + 18 * fl); + } else if (kd === 1) { + const g = L * vg[i] * (S.sd[i] < 2.5 ? 1 : 0.2); + (F[o] = st[o] + 120 * g), (F[o + 1] = st[o + 1] + 40 * g), (F[o + 2] = st[o + 2] + 10 * g); + } else { + const d = cl((i >> 6) + 0.5 - BASE + bend[i & 63] + S.off[i], 0, 94), d0 = floor(d), u = d - d0; + const a = BC[BB[d0]], b = BC[BB[d0 + 1]], gr = S.gr[i]; + for (let c = 0; c < 3; c++) F[o + c] = mix(a[c], b[c], u) * gr; + } + } + + // The magma: chamber, the feeder below it, the conduit, the dike and the sill, with the rock round them glowing. + const glowRock = (o, g) => { + for (let c = 0; c < 3; c++) F[o + c] = mix(F[o + c], [210, 50, 16][c], min(0.85, g)); + }; + const melt = (o, T) => { + const k = lut(T); + (F[o] = LUT[k]), (F[o + 1] = LUT[k + 1]), (F[o + 2] = LUT[k + 2]); + }; + for (let y = max(BASE, floor(top - 6)); y < H; y++) + for (let x = max(0, floor(C - ax - 7)); x < min(W, C + ax + 7); x++) { + const X = x + 0.5, Y = y + 0.5, e = hypot((X - C) / ax, (Y - CHY) / ay), o = (y * W + x) * 3; + if (e < 1) { + const T = 0.28 + 0.38 * vn(X * 0.17 + 0.7 * sin(Y * 0.17 + t * 0.5), Y * 0.3 + t * 1.2) + 0.2 * (1 - e * e) - 0.25 * sm(0.72, 1, e) + 0.16 * S.pr + 0.1 * P + 0.35 * exp(-((X - C) ** 2) / 9 - ((Y - top) ** 2) / 6) * S.pr; + melt(o, T); + } else if (Y > CHY && abs(X - C - 1.5 * sin(y * 0.2)) < 1.3) melt(o, 0.38 + 0.08 * sin(y * 0.5 - t * 3)); + else glowRock(o, exp(-((e - 1) * ay) / 2.4) * (0.5 + 0.4 * S.pr + 0.2 * P)); + } + for (const b of S.bub) put(b.x, b.y, lc(1.05)); + const slug = (y) => S.slugs.reduce((m, g) => max(m, exp(-(((y - g.y) / 2.6) ** 2))), 0); + for (let y = FLOOR; y <= top + 1; y++) { + const cx = CXA[y], sg = slug(y), hw = 1.35 + 1.3 * sg + 0.6 * plin; + for (let x = max(0, floor(cx - hw - 5)); x < min(W, cx + hw + 5); x++) { + const o = (y * W + x) * 3, q = abs(x + 0.5 - cx); + if (q < hw) { + const ves = hash(x, floor(y + t * 14)) < 0.12 ? 0.25 : 0; + melt(o, mix(0.58 + 0.12 * sin(y * 0.45 + t * 7) + ves + 0.5 * sg, 0.5 + 0.6 * vn(x * 0.6, (y + t * 45) * 0.25), plin)); + } else if (kind[y * W + x]) glowRock(o, exp(-(q - hw) / 1.5) * (0.4 + 0.5 * sg + 0.3 * plin)); + } + } + // The dike towards the right flank, and the sill along the beds on the left. + const dk = sm(0.2, 0.9, P), sl = sm(0.45, 1, P); + for (let j = 0; j < 46 * dk; j++) { + const u = j / 46, x = CXA[99] + (VX - CXA[99]) * u, y = 99 - (99 - sAt(VX)) * u; + put(x, y, lc(0.55 + 0.1 * sin(j * 0.6 - t * 5))); + put(x + 1, y, [210, 50, 16], 0.45); + } + for (let j = 0; j < 20 * sl; j++) { + const x = CXA[93] - 2 - j, y = 93 + 0.006 * j * j + 2 * sin(j * 0.15); + put(x, y, lc(0.5 + 0.08 * sin(j * 0.7 + t * 4))); + if (j < 14 * sl) put(x, y + 1, lc(0.42)); + } + + // The ash: every puff summed, then shaded over the sky. + A.fill(0); + hot.fill(0); + for (const p of S.puffs) { + const x0 = max(0, floor(p.x - p.rx)), x1 = min(W - 1, floor(p.x + p.rx)), y0 = max(0, floor(p.y - p.ry)), y1 = min(H - 1, floor(p.y + p.ry)); + for (let y = y0; y <= y1; y++) { + const dy = (y + 0.5 - p.y) / p.ry; + for (let x = x0; x <= x1; x++) { + const dx = (x + 0.5 - p.x) / p.rx, q = 1 - dx * dx - dy * dy; + if (q > 0) (A[y * W + x] += p.a * q * q), (hot[y * W + x] += p.hot * q * q); + } + } + } + // Ash falling out of the plume aloft, slanting downwind as it falls. + const col = new Float32Array(W); + for (const p of S.puffs) + if (!p.g && p.y < 45 && p.rx > 4) for (let x = max(0, floor(p.x - p.rx)); x < min(W, p.x + p.rx); x++) col[x] += p.a * max(0, 1 - ((x + 0.5 - p.x) / p.rx) ** 2); + for (let y = 14; y < 84; y++) { + const f = 0.11 * sm(12, 30, y) * (1 - sm(56, 84, y)), sh = wind * (y - 14) * 0.55; + for (let x = 0; x < W; x++) { + const c = col[cl(round(x - sh), 0, W - 1)]; + if (c > 0.05) A[y * W + x] += f * min(1.5, c) * (0.3 + vn(x * 0.9, y * 0.12 - t * 3)); + } + } + const { flx, fly } = S; + for (let i = 0; i < N; i++) { + if (A[i] < 0.01 || kind[i]) continue; + const x = i & 63, y = i >> 6, o = i * 3, tex = vn(x * 0.22 - t * wind * 0.5, y * 0.2 + t * 0.7); + const al = min(0.94, 1 - exp(-A[i] * (1.3 + 1.7 * tex))), sh = 0.55 + 0.6 * tex, vl = 0.8 * L * vg[i] + 0.6 * hot[i], fy = fl * exp(-hypot(x - flx, (y - fly) * 0.7) / 16); + F[o] = mix(F[o], 104 * sh + 255 * vl + 130 * fy, al); + F[o + 1] = mix(F[o + 1], 98 * sh + 100 * vl + 140 * fy, al); + F[o + 2] = mix(F[o + 2], 100 * sh + 34 * vl + 210 * fy, al); + } + + // The crater: the vent, the lava lake, and the flash of a burst. + const ll = FLOOR + 0.6 - S.lake; + for (let y = RIMY - 1; y <= FLOOR + 1; y++) + for (let x = C - RIM; x < C + RIM; x++) { + const i = y * W + x; + if (!kind[i] && y + 0.5 > ll) melt(i * 3, 0.55 + 0.3 * vn(x * 0.6 + t, y * 0.8) + 0.2 * S.glow - 0.12 * (y + 0.5 - ll < 1 ? 1 : 0)); + } + if (S.glow > 0.02) + for (let y = FLOOR - 14; y <= FLOOR + 3; y++) + for (let x = C - 12; x < C + 12; x++) { + const o = (y * W + x) * 3, g = S.glow * exp(-hypot(x + 0.5 - C, y + 0.5 - FLOOR + 1) / 3.2); + (F[o] += 255 * g), (F[o + 1] += 190 * g), (F[o + 2] += 90 * g); + } + + // Lava down the flanks: hot and bright near the rim, crusting dark as it cools. + for (let s = 0; s < 2; s++) { + const { v, T, ph } = S.lv[s]; + for (let k = 0; k < NL; k++) { + if (v[k] < 0.01) continue; + const x = s ? C + RIM + k : C - RIM - 1 - k, y0 = min(SURF[x], SURF[x + 1]), y1 = max(SURF[x], SURF[x + 1]), th = min(4, v[k] * 3.2), al = min(1, v[k] * 8); + // Its glow on the air just above it. + for (let j = 1; j <= 3; j++) put(x, y0 - th - j, [255, 90, 20], (0.35 * al * T[k]) / j); + for (let y = floor(y0 - th); y <= floor(y1); y++) { + const n = vn(k * 0.7 - ph * 2, y * 0.6), tt = T[k], c = lc(0.22 + 0.62 * tt + 0.18 * n); + const cr = max(sm(0.75, 0.25, tt) * sm(0.3, 0.6, vn(k * 1.3 - ph * 2, y * 1.1 + 9)), sm(0.12, 0.02, tt)), g = S.gr[(y * W + x) & 8191]; + put(x, y, [mix(c[0], 64 * g, cr), mix(c[1], 52 * g, cr), mix(c[2], 54 * g, cr)], al); + } + } + } + + // Bombs and spatter, with a short trail while they fly. + for (const q of S.parts) { + const c = lc(0.25 + 0.85 * q.T); + if (!q.m) for (let j = 1; j <= 3; j++) put(q.x - q.vx * 0.022 * j, q.y - q.vy * 0.022 * j, c, 0.5 / j); + put(q.x, q.y, c); + if (q.b && q.T > 0.3) put(q.x + 1, q.y, c, 0.6); + } + + // Lightning. + for (const b of S.bolts) { + if (floor(b.life * 40) % 3 === 1) continue; + for (const pts of [b.pts, b.br]) + for (let j = 1; j < pts.length; j++) { + const [x0, y0] = pts[j - 1], [x1, y1] = pts[j], m = max(abs(x1 - x0), abs(y1 - y0)) * 1.5 + 1; + for (let u = 0; u <= m; u++) { + const x = mix(x0, x1, u / m), y = mix(y0, y1, u / m); + put(x - 1, y, [120, 110, 255], 0.35), put(x + 1, y, [120, 110, 255], 0.35), put(x, y, [240, 240, 255]); + } + } + } + + 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 a5c3badef68f3587f4ebfffff28fc7eecea45654 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 03:36:07 +0000 Subject: [PATCH 06/17] skydiver: a load jumps through the cloud deck, holds a ring and spirals down under canopy Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DbfsCYkpNNJ5diTKTtPZEu --- incoming/pattern-study/patterns/skydiver.js | 373 ++++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 incoming/pattern-study/patterns/skydiver.js diff --git a/incoming/pattern-study/patterns/skydiver.js b/incoming/pattern-study/patterns/skydiver.js new file mode 100644 index 000000000..f3bfbc940 --- /dev/null +++ b/incoming/pattern-study/patterns/skydiver.js @@ -0,0 +1,373 @@ +// ===== Patternflow pattern ===== +// Title: Skydiver +// Family: flight +// Idea: A jump plane crosses the top of the sky and lets out a load of skydivers who punch down through the cloud deck, take grips in a ring, break off and open their ram-air canopies, then spiral down to the target circle on the drop zone while the next load is already on its way up. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 25, after formation skydiving loads at a drop zone +// Round: 25 +// =============================== +// @matrix 64x128 +// @knobs Hour=0..1, Speed=0.2..3, Divers=5..12, Wind=-1..1 +// +// Knob 1: Hour. A clear midday at 0, the deep blue overhead going pale at the horizon, to the sunset load at 1, when the sun sits on the horizon, the clouds go pink and the canopies fly in warm light. +// Knob 2: Speed. At 2 a new plane comes every fifteen seconds, its divers are in freefall for about seven and under canopy for about twenty, so one load is always landing while the next is in the air. At 0.2 it all runs ten times slower. +// Knob 3: Divers. How many jump together, five to twelve. A bigger load comes in a longer plane and builds a wider ring, and as many canopies come down. +// Knob 4: Wind. The cloud deck, the far cumulus and the cloud shadows on the fields travel with it, the plane runs in against it, the windsock fills, and the canopies turn in to land facing it. Near 0 they land towards you. +// Buttons: K1 tap brings the next load in at once, and its divers go as soon as the door is over the field. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, ceil, round, sin, cos, abs, exp, atan2, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const mix = (a, b, t) => a + (b - a) * t; +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +const rnd = () => Math.random(); +function hash(i, j) { + let h = (Math.imul(i, 374761393) + Math.imul(j, 668265263)) | 0; + h = Math.imul(h ^ (h >>> 13), 1274126177); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +} +function vn(x, y) { + const xi = floor(x), yi = floor(y), u = x - xi, v = y - yi, su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v); + const a = hash(xi, yi), b = hash(xi + 1, yi), c = hash(xi, yi + 1); + return a + (b - a) * su + (c - a) * sv + (a - b - c + hash(xi + 1, yi + 1)) * su * sv; +} + +// Rows: the plane's fuselage, the flat base of the cloud deck, the horizon, the landing field. +const [PY, DB, HZ, GL] = [8, 30, 101, 118]; +// Depth goes onto the screen at this scale, nearer is lower; canopies are seen flatter. +const [ZS, ZC] = [0.55, 0.3]; +// Terminal velocity and the plane's speed in LEDs a second, the target, the windsock, seconds between loads. +const [VT, PV, TX, WX, CYCLE] = [8.5, 14, 34, 57, 15]; +// Sky top, middle, horizon; sun; lit and shaded cloud; daylight. Midday to sunset. +const KEYS = [ + [8, 28, 104, 18, 62, 148, 112, 158, 204, 255, 250, 225, 252, 252, 255, 150, 162, 190, 1, 1, 1], + [12, 34, 112, 56, 96, 172, 196, 190, 184, 255, 226, 160, 255, 238, 210, 146, 140, 170, 1, 0.92, 0.8], + [20, 24, 90, 104, 84, 146, 255, 160, 84, 255, 186, 100, 255, 194, 136, 126, 96, 136, 1, 0.78, 0.58], + [12, 12, 50, 96, 46, 104, 255, 104, 44, 255, 136, 56, 255, 136, 96, 82, 54, 92, 0.86, 0.56, 0.42], +]; +const CAN = [[255, 30, 60], [255, 214, 0], [20, 110, 255], [255, 50, 190], [80, 235, 40], [255, 120, 0], [0, 220, 235], [150, 60, 255], [245, 245, 245], [255, 80, 80], [20, 190, 110], [255, 160, 200]]; +const SUIT = [[255, 220, 40], [240, 40, 40], [40, 200, 255], [250, 250, 250], [255, 110, 0], [190, 70, 255], [60, 230, 90], [255, 80, 180]]; +const HELM = [[250, 250, 250], [30, 30, 36], [255, 200, 0], [230, 40, 40]]; +const STRIPE = [[200, 30, 40], [20, 70, 170], [230, 150, 0]]; + +export function setup(params) { + const F32 = (n) => new Float32Array(n); + const P = (params.s = { t: 0, rt: 0, loads: [], next: 0, cx: 0, w: 0, f: F32(N * 3), bg: F32(N * 3), key: -1, top: F32(128), far: F32(128) }); + // Cumulus along a strip that wraps every 128 columns, each a cluster of round puffs: the deck and the far ones. + for (const [T, nc, sz] of [[P.top, 4, 1], [P.far, 6, 0.5]]) + for (let k = 0; k < nc; k++) { + const c = (k * 128) / nc + rnd() * 8, wd = (14 + rnd() * 14) * sz, hm = (5 + rnd() * 7) * sz; + for (let j = 0; j < 11; j++) { + // Bubbles stacked higher towards the middle of the cloud. + const u = rnd() * 2 - 1, r = (2 + rnd() * 3.2) * (sz + 0.3) * 0.75, px = c + u * (wd / 2 - r * 0.5), e = max(0, (hm - r) * (1 - u * u) * (0.5 + 0.5 * rnd())); + for (let x = 0; x < 128; x++) { + const dx = ((((x - px) % 128) + 192) % 128) - 64; + // A round top, and shoulders sloping down to the base. + const ax = abs(dx); + if (ax < r + e) T[x] = max(T[x], ax < r ? e + Math.sqrt(r * r - dx * dx) : e + r - ax); + } + } + } +} + +const shuffle = (n) => { + const a = [...Array(n).keys()]; + for (let i = n - 1; i > 0; i--) { + const j = floor(rnd() * (i + 1)); + [a[i], a[j]] = [a[j], a[i]]; + } + return a; +}; + +// A load: the plane runs in against the wind and puts the ring upwind of the target. +function spawn(P, n, btn) { + // A big load gets out of the door faster, so every ring has the same time to build. + const w = P.w, dir = w >= 0 ? -1 : 1, len = 17 + n, ex = (0.5 + 0.06 * n) / (n - 1), half = ((n - 1) * ex) / 2, x0 = dir < 0 ? 66 : -2; + const te0 = btn ? 1.3 : max(1.3, (dir * (31 - 12 * w - x0) + len - 12) / PV - half); + const cx = x0 + dir * PV * (te0 + half) - dir * (len - 12), ci = shuffle(CAN.length), si = shuffle(SUIT.length), rk = shuffle(n), lp = shuffle(n); + const d = []; + for (let i = 0; i < n; i++) { + // Exit time, canopy colours (white or a second colour in the middle cells), suit, helmet, + // descent rate under canopy, which way the spiral turns, and the spot on the field. + d.push({ + i, te: te0 + i * ex, st: 0, x: 0, y: 0, z: 0, h: 0, g: 0, o: 0, rank: rk[i], can: ci[i], cb: rnd() < 0.5 ? 8 : ci[(i + 5) % 12], suit: si[i % 8], helm: floor(rnd() * 4), + zd: rnd() * 2 - 1, k: 2.2 + rnd() * 1.2, om: (rnd() < 0.5 ? -1 : 1) * (0.8 + rnd() * 0.4), lx: TX + (lp[i] / (n - 1) - 0.5) * 26 + rnd() * 2, lz: rnd() * 12 - 4, + }); + } + P.loads.push({ t: 0, n, dir, len, x0, te0, cx, px: x0, rot0: rnd() * TAU, rr: 4.6 / sin(PI / n), tb: te0 + 2 * half + 4.2, d, sc: STRIPE[floor(rnd() * 3)] }); + if (P.loads.length > 4) P.loads.shift(); +} + +const fall = (s) => (s <= 0 ? 0 : VT * (s - (1 - exp(-1.5 * s)) / 1.5)); +const thr = (s) => (s <= 0 ? 0 : (PV * (1 - exp(-1.4 * s))) / 1.4); + +function runLoad(P, L, d) { + const T = (L.t += d), w = P.w, dir = L.dir, n = L.n; + L.px = L.x0 + dir * PV * T; + const s0 = T - L.te0, ccx = L.cx + w * 2 * max(0, s0), ccy = PY + 3 + fall(s0 - 0.15), rot = L.rot0 + 0.25 * s0; + const hf = atan2(1 - 0.9 * abs(w), -w), cf = cos(hf), sf = sin(hf); + for (const D of L.d) { + if (!D.st && T >= D.te) (D.st = 1), (D.xd = L.x0 + dir * PV * D.te - dir * (L.len - 6)); + if (D.st === 1 && T >= L.tb) { + // Break-off: where the ring was, frozen, then turn away and track out. + const sb = L.tb - L.te0, th = L.rot0 + 0.25 * sb + (TAU * D.i) / n; + Object.assign(D, { st: 2, th, bx: L.cx + w * 2 * sb + L.rr * cos(th), bz: L.rr * sin(th), by: PY + 3 + fall(sb - 0.15) }); + } + if (D.st === 1) { + const s = T - D.te, th = rot + (TAU * D.i) / n, g = sm(0.4, 2.3, s); + D.x = mix(D.xd + dir * thr(s) + w * 2 * s, ccx + L.rr * cos(th), g); + D.y = mix(PY + 3 + fall(s), ccy, g); + D.z = mix(D.zd, L.rr * sin(th), g); + D.h = th + PI; + D.g = g; + } else if (D.st === 2) { + const sb = T - L.tb, out = (10 + 0.6 * n) * (1 - exp(-1.1 * sb)); + D.x = cl(D.bx + cos(D.th) * out, 2, 61); + D.z = D.bz + sin(D.th) * out * 0.6; + D.y = D.by + 9.5 * sb; + D.h = D.th + PI + PI * sm(0, 0.6, sb); + D.g = 0; + if (sb >= 0.7 + (1.4 * D.rank) / (n - 1)) Object.assign(D, { st: 3, td: T, yd: D.y, vy: 9.5, ax: D.x, az: D.z, h0: D.h }); + } else if (D.st === 3) { + // Under canopy: open, slow down, spiral in over the field and turn to land. + const sc = T - D.td; + D.o = min(1, sc / 0.9); + D.vy += ((D.y > GL - 5 ? 1.3 : D.o > 0.55 ? D.k : 9.5) - D.vy) * min(1, 5 * d); + D.y += D.vy * d; + const pr = cl((D.y - D.yd) / (GL - D.yd), 0, 1), fx = D.lx - cf * 8, fz = D.lz - sf * 8; + if (pr < 0.8) { + const e = sm(0, 0.8, pr), r = 13 * sin(PI * (pr / 0.8)) ** 0.6, h = D.h0 + D.om * sc, sg = D.om > 0 ? 1 : -1; + // The wind carries the spiral downwind on the way down, and the last turn brings it back. + D.x = mix(D.ax, fx, e) + sg * r * sin(h) + w * 12 * sin(PI * e); + D.z = mix(D.az, fz, e) - sg * r * cos(h); + const b = sm(0.65, 0.8, pr); + D.h = atan2(mix(sin(h), sf, b), mix(cos(h), cf, b)); + } else { + const q = sm(0.8, 1, pr); + (D.x = mix(fx, D.lx, q)), (D.z = mix(fz, D.lz, q)), (D.h = hf); + } + D.x = cl(D.x, 3, 60); + D.fl = sm(GL - 5, GL, D.y); + if (D.y >= GL) Object.assign(D, { st: 4, tl: T, y: GL, x: D.lx, z: D.lz, h: hf }); + } else if (D.st === 4 && T - D.tl > 7.5) D.st = 5; + } +} + +function tick(P, d, n) { + P.t += d; + P.cx += (3.2 + 9 * P.w) * d; + if ((P.next -= d) <= 0) spawn(P, n, 0), (P.next += CYCLE); + for (const L of P.loads) runLoad(P, L, d); + P.loads = P.loads.filter((L) => L.t < 60 && L.d.some((D) => D.st < 5)); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 0.06], rd = cl(dt || 0, 0, 0.1), d = rd * (cl(k[1], 0.2, 3) / 2), n = round(cl(k[2], 5, 12)); + P.w = cl(k[3], -1, 1); + P.h = cl(k[0], 0, 1); + P.rt += rd; + // One load landing, one high under canopy, and the third plane on its run. + if (!P.ready) { + P.ready = 1; + P.next = 1e9; + for (const s of [0, 19, 30, 32.5]) { + while (P.t < s - 1e-6) tick(P, 1 / 15, n); + s < 32 && spawn(P, n, 0); + } + P.next = CYCLE - 2.5; + } + if (input.btnPressed && input.btnPressed[0]) spawn(P, n, 1), (P.next = CYCLE); + tick(P, d, n); +} + +let F, LT, K; +const at = (x, y) => ((x = round(x)), (y = round(y)), x < 0 || x >= W || y < 0 || y >= H ? -1 : (y * W + x) * 3); +function put(x, y, c, a = 1) { + const o = at(x, y); + if (o >= 0) for (let i = 0; i < 3; i++) F[o + i] = mix(F[o + i], c[i], a); +} +function seg(x0, y0, x1, y1, c, a = 1) { + const n = max(abs(x1 - x0), abs(y1 - y0)) * 1.4 + 1; + for (let i = 0; i <= n; i++) put(mix(x0, x1, i / n), mix(y0, y1, i / n), c, a); +} +const lit = (c, k = 1) => [c[0] * LT[0] * k, c[1] * LT[1] * k, c[2] * LT[2] * k]; +const KC = (i) => [K[i], K[i + 1], K[i + 2]]; + +// Sky, sun, hills and the drop zone, kept until the hour moves. +function backdrop(P) { + const B = P.bg, h = P.h, key = round(h * 300); + if (key === P.key) return; + P.key = key; + const zen = KC(0), mid = KC(3), hor = KC(6), sun = KC(9), sy = mix(-16, 90, sm(0.3, 1, h)); + const FIELDS = [[74, 124, 44], [112, 140, 52], [150, 146, 72], [60, 104, 48], [126, 112, 64]]; + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, o = i * 3; + let c; + const hill = HZ - 1.5 - 2.5 * vn(x * 0.09, 50); + if (y < hill) { + const f = y / HZ, u = f < 0.6 ? sm(0, 1, f / 0.6) : ((f - 0.6) / 0.4) ** 1.6; + c = f < 0.6 ? zen.map((v, j) => mix(v, mid[j], u)) : mid.map((v, j) => mix(v, hor[j], u)); + const ds = Math.hypot(x - 47, (y - sy) * 1.1), g = 0.5 * exp(-ds / 9) + 0.25 * exp(-ds / 30); + c = ds < 3.6 ? sun : c.map((v, j) => v + sun[j] * g * (0.4 + 0.6 * h)); + } else if (y < HZ) { + c = lit(hor.map((v, j) => mix(v, [44, 72, 70][j], 0.6))); + } else { + const zf = y - HZ + 2, gx = ((x - 32) * 24) / zf, gz = 260 / zf; + if (y >= 104 && y <= 106) c = y === 105 && (floor(gx / 5) & 3) === 0 ? [220, 220, 220] : [92, 92, 98]; + else if (y >= 109) c = [70, 150, 48].map((v) => v * (floor(gx / 7) & 1 ? 0.86 : 1)); + else c = FIELDS[floor(hash(floor(gx / 40 + 60), floor(gz / 22)) * 5)]; + const e = ((x - TX) / 11) ** 2 + ((y - GL) / 3.6) ** 2; + if (e < 1) c = e < 0.07 ? [245, 245, 245] : [204, 182, 132].map((v) => v * (e > 0.6 ? 0.84 : 1)); + c = lit(c).map((v, j) => mix(v, hor[j], exp(-(zf - 2) / 4) * 0.65)); + } + // The hangar by the runway, its door open. + if (x >= 3 && x <= 16 && y <= 105 && y >= 97 + ((x - 9.5) / 6.5) ** 2 * 3) c = lit(x > 6 && x < 13 && y > 100 ? [34, 34, 42] : [196, 196, 204], y < 99 ? 1.08 : 1); + B[o] = c[0]; + B[o + 1] = c[1]; + B[o + 2] = c[2]; + } +} + +// The far cumulus sitting on the horizon haze. +function farClouds(P) { + const lt = KC(12), sh = KC(15), hor = KC(6); + for (let x = 0; x < W; x++) { + const xx = x * 0.9 + P.cx * 0.6 + 40, i = floor(xx), f = xx - i, hgt = mix(P.far[i & 127], P.far[(i + 1) & 127], f); + for (let y = floor(98 - hgt); y <= 98; y++) { + const u = (98 - y) / max(1, hgt), c = sh.map((v, j) => mix(mix(v, lt[j], cl(u * 1.3, 0, 1)), hor[j], 0.35)); + put(x, y, c, cl(y - (98 - hgt) + 0.5, 0, 1) * 0.92); + } + } +} + +// The deck the divers fall through: domes on top, a flat grey base. +function deck(P) { + const lt = KC(12), sh = KC(15); + for (let x = 0; x < W; x++) { + const xx = x + P.cx + 7, i = floor(xx), f = xx - i, hgt = mix(P.top[i & 127], P.top[(i + 1) & 127], f); + if (hgt < 0.3) continue; + for (let y = floor(DB - hgt); y <= DB; y++) { + const u = (DB - y) / max(1, hgt), b = cl(u * 1.15 + (vn(xx * 0.35, y * 0.4) - 0.5) * 0.6, 0, 1); + put(x, y, sh.map((v, j) => mix(v, lt[j], b)), cl(y - (DB - hgt) + 0.5, 0, 1) * (y === DB ? 0.7 : 0.95)); + } + } +} + +function plane(P, L) { + const { len, dir, px } = L, X = (u) => px - dir * (len - u), body = lit([236, 236, 242]), belly = lit([168, 170, 180]), glass = lit([50, 74, 110]), dark = [22, 22, 28]; + for (let u = -1; u <= len; u += 0.5) { + const x = X(u); + if (u < 4.5) for (let y = PY - 2 - (u < 2 ? 5 : 5 - (u - 2) * 2); y < PY - 2; y++) put(x, y, body); + if (u < 3.5) put(x, PY - 1, belly); + if (u < 1) continue; + const bot = u < 7 ? PY + 1 - (7 - u) * 0.45 : PY + 1, top = u > len - 3 ? PY - 2 + (u - len + 3) * 0.7 : PY - 2; + for (let y = ceil(top); y <= bot; y++) put(x, y, y === PY ? lit(L.sc) : y > PY ? belly : body); + } + for (let u = 9; u < len - 5; u += 2) put(X(u), PY - 1, glass); + put(X(len - 2), PY - 1, glass), put(X(len - 3), PY - 1, glass); + for (let u = len * 0.42; u <= len * 0.66; u += 0.5) put(X(u), PY - 3, lit([214, 216, 224])); + for (let u = len * 0.6; u <= len * 0.74; u += 0.5) put(X(u), PY - 2, lit([150, 150, 160])); + for (let y = PY - 5; y <= PY; y++) put(X(len * 0.77), y, [220, 220, 230], 0.25 + 0.2 * rnd()); + put(X(len * 0.55), PY + 2, dark), put(X(len - 2), PY + 2, dark); + // The door is open on the run in, with the next diver crouched in it. + const next = L.d.find((D) => !D.st); + if (next && L.t > L.te0 - 1.6) { + for (let u = 4.5; u <= 6.5; u += 0.5) for (let y = PY - 1; y <= PY + 1; y++) put(X(u), y, dark); + put(X(6), PY, lit(SUIT[next.suit])), put(X(6), PY - 1, lit(HELM[next.helm])); + } + if ((P.rt * 1.4) % 1 < 0.25) put(X(len * 0.5), PY - 4, [255, 40, 30]); +} + +// A diver in freefall seen from above and behind: spread arms and legs, the rig on the back, the helmet. +const bp = (D, a, b) => { + const c = cos(D.h) * 1.3, s = sin(D.h) * 1.3; + return [D.x + c * a - s * b, D.y + (D.z + s * a + c * b) * ZS]; +}; +function diver(D) { + const sc = lit(SUIT[D.suit]), ln = (a, b) => seg(...bp(D, ...a), ...bp(D, ...b), sc); + for (const m of [-1, 1]) { + ln([-0.9, 0.4 * m], [-2.8, 1.6 * m]); + if (D.g < 0.97) ln([0.9, 0.8 * m], [2.1, 2.3 * m]); + } + ln([-0.9, 0], [0.9, 0]), ln([0.9, -0.9], [0.9, 0.9]); + put(...bp(D, -0.1, 0), [26, 26, 32]); + put(...bp(D, 1.9, 0), lit(HELM[D.helm])); +} + +// A canopy: cells, lines, the jumper in the harness. On the ground it falls in behind the jumper, who gathers it up. +function canopy(D, T) { + const sx = D.x, sy = D.y + D.z * ZS, ca = CAN[D.can], cb = CAN[D.cb], down = D.st === 4, sl = down ? T - D.tl : 0, a = 1 - sm(5.5, 7.5, sl); + const suit = lit(SUIT[D.suit]); + if (!down && D.o < 0.3) { + // Pilot chute and bag going up. + seg(sx, sy - 1, sx, sy - 2 - D.o * 22, lit([200, 200, 210]), 0.5), put(sx, sy - 2 - D.o * 22, lit([245, 245, 245])); + put(sx, sy - 1, lit(HELM[D.helm])), put(sx, sy, suit), put(sx + 1, sy, suit); + return; + } + const kk = down ? 1 : sm(0.25, 0.8, D.o), pop = 1 + 0.18 * sin(PI * sm(0.55, 1, D.o)), c = sm(0, 1.2, sl); + const ch = cos(D.h), shh = sin(D.h), zm = 1 + D.z * 0.015, hs = (1 + 5.2 * kk) * pop * mix(1, 0.75, c) * zm, lr = (2 + 6 * kk) * (1 - c) * zm + 0.5 * c; + const ox = down ? -ch * 4 * c : 0, oy = down ? -shh * 4 * c * ZS : 0, cy = sy - 1.5 - lr + oy, droop = mix(1.1, 0.2, c) + D.fl * 0.6; + const svx = -shh * hs, svy = ch * hs * ZC, fx = ch * 2 * zm, fy = shh * 2 * ZC * (1 - c); + if (!down || c < 1) for (const t of [-0.9, -0.35, 0.35, 0.9]) seg(sx, sy - 1.5, sx + ox + svx * t, cy + svy * t + droop * t * t + 1, lit([210, 210, 220]), 0.3 * a); + for (let pass = 0; pass < 2; pass++) + for (let t = -1; t <= 1; t += 0.45 / hs) { + const cell = min(8, floor((t + 1) * 4.5)), col = cell > 2 && cell < 6 ? cb : ca, k = (cell & 1 ? 0.8 : 1) * (pass ? 1 : 0.5) * (1.12 - 0.12 * pop); + for (let q = -1; q <= 1; q += 0.5) put(sx + ox + svx * t + fx * q, cy + svy * t + fy * q + droop * t * t + (pass ? 0 : 1), lit(col, k * (0.92 + 0.08 * q)), a); + } + put(sx, sy - 2, lit(HELM[D.helm]), a), put(sx, sy - 1, suit, a), put(sx + (D.fl > 0.3 ? 1 : 0), sy, suit.map((v) => v * 0.7), a); +} + +function sock(P) { + const w = P.w, aw = abs(w), sx = w >= 0 ? 1 : -1; + for (let y = 104; y <= 116; y++) put(WX, y, lit([200, 200, 206])); + for (let u = 0; u <= 6; u += 0.5) { + const x = WX + sx * u * (0.3 + 0.7 * aw), y = 104 + u * (1 - aw) * 0.85 + sin(P.rt * 9 - u) * 0.35 * aw, col = lit(floor(u / 1.5) & 1 ? [240, 240, 240] : [255, 110, 20]); + put(x, y, col), u < 4 && put(x, y + 1, col, 0.8); + } +} + +export function draw(display, params) { + const P = params.s; + if (!P || P.h === undefined) return; + F = P.f; + const hh = P.h * 3, i0 = min(2, floor(hh)); + K = KEYS[i0].map((v, c) => mix(v, KEYS[i0 + 1][c], hh - i0)); + LT = KC(18); + backdrop(P); + F.set(P.bg); + // Cloud shadows crossing the fields. + for (let y = HZ; y < H; y++) { + const zf = y - HZ + 2; + for (let x = 0; x < W; x++) { + const v = vn((((x - 32) * 24) / zf - P.cx * 1.1) * 0.035, (260 / zf) * 0.05), k = 1 - 0.5 * sm(0.45, 0.58, v), o = (y * W + x) * 3; + (F[o] *= k), (F[o + 1] *= k), (F[o + 2] *= k); + } + } + farClouds(P); + sock(P); + const A = []; + for (const L of P.loads) for (const D of L.d) if (D.st === 4) canopy(D, L.t); + for (const L of P.loads) if (L.px > -40 && L.px < 104) plane(P, L); + for (const L of P.loads) for (const D of L.d) if (D.st && D.st < 4) A.push([D, L]); + A.sort((a, b) => a[0].z - b[0].z); + for (const [D, L] of A) { + if (D.st === 3) canopy(D, L.t); + else { + diver(D); + // Grips: the arms of two docked neighbours meet between them. + const E = L.d[(D.i + 1) % L.n]; + if (D.st === 1 && E.st === 1 && D.g > 0.97 && E.g > 0.97) { + const d2 = (u, v) => (u[0] - v[0]) ** 2 + (u[1] - v[1]) ** 2, [a, b, c, e] = [bp(D, 0.9, -0.9), bp(D, 0.9, 0.9), bp(E, 0.9, -0.9), bp(E, 0.9, 0.9)]; + const p = d2(a, c) + d2(a, e) < d2(b, c) + d2(b, e) ? a : b, q = d2(p, c) < d2(p, e) ? c : e; + const m = [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2]; + seg(...p, ...m, lit(SUIT[D.suit])), seg(...m, ...q, lit(SUIT[E.suit])); + } + } + } + deck(P); + 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 6ad7ef2fbcd33da87628eef006103c41807204b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 03:40:16 +0000 Subject: [PATCH 07/17] marionette: add a jester marionette dancing on threads from a cross-shaped control Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DbfsCYkpNNJ5diTKTtPZEu --- incoming/pattern-study/patterns/marionette.js | 412 ++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 incoming/pattern-study/patterns/marionette.js diff --git a/incoming/pattern-study/patterns/marionette.js b/incoming/pattern-study/patterns/marionette.js new file mode 100644 index 000000000..e1f5cf855 --- /dev/null +++ b/incoming/pattern-study/patterns/marionette.js @@ -0,0 +1,412 @@ +// ===== Patternflow pattern ===== +// Title: Marionette +// Family: stage +// Idea: A jester marionette dances in a follow spot on a little stage, every joint hanging from a thread that runs up to the wooden cross in the puppeteer's hand on the lamplit bridge, so each tilt of the cross shows in the threads and the limbs they pull. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 25, after the string marionettes of the European puppet theatres +// Round: 25 +// =============================== +// @matrix 64x128 +// @knobs Costume=0..1, Speed=0.2..3, Number=0..4, Spot=-1..1 +// +// Knob 1: Costume. The jester's motley: emerald at 0, then blue, magenta, scarlet and amber. The painted backcloth changes with it, a blue night behind the emerald. +// Knob 2: Speed. The tempo: at 2 the jig goes at 144 beats a minute and the five numbers take 22.5 s; at 0.2 ten times slower. +// Knob 3: Number. The number the show opens on, each in its own light: the jig in warm light, the bow under a pin spot, the flop in moonlight, the stamping dance over footlights, the strut across the stage. The rest follow in order. +// Knob 4: Spot. Where the follow spot stands out front, left at -1 to right at 1. The pool slides away from it, and the shadow of the puppet and its threads falls on the backcloth on the other side. +// Buttons: K1 tap: the puppeteer jerks the cross up, the puppet leaps with its knees tucked, lands and bows. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, ceil, round, sin, cos, abs, sqrt, exp, atan2, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const mix = (a, b, t) => a + (b - a) * t; +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +const fr = (v) => v - floor(v); +function hash(i, j) { + let h = (Math.imul(i, 374761393) + Math.imul(j, 668265263)) | 0; + h = Math.imul(h ^ (h >>> 13), 1274126177); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +} +function hsv(h, s, v) { + h = fr(h) * 6; + const i = floor(h), f = h - i, p = v * (1 - s), q = v * (1 - s * f), t = v * (1 - s * (1 - f)); + return [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; +} + +// Rows: bridge rail, grip, top of the backcloth, its foot, the ankles' floor, the apron. +const [RAIL, GRIP, CLOTH, BACK, FLOOR, APRON] = [3, 11, 28, 108, 117, 124]; +// Gravity in LEDs/s² (the 46 LED puppet is about 60 cm), damping per step, beats a second at Speed 2. +const [G, DAMP, BPS] = [700, 0.992, 2.4]; +// The puppet standing, x from its middle: temples, shoulders, hips, elbows, hands, knees, ankles. +const REST = [-3, 74, 3, 74, -5, 80, 5, 80, -3, 94, 3, 94, -6, 88, 6, 88, -7, 96, 7, 96, -3, 106, 3, 106, -3, 117, 3, 117]; +// Bones and their stiffness. The neck's diagonals are soft so the head can nod. +const BONES = "01 02 13 03. 12. 23 45 24 35 25 34 26 68 37 79 4a ac 5b bd".split(" ").map((s) => [parseInt(s[0], 16), parseInt(s[1], 16), s[2] ? 0.2 : 1]); +// The threads in the order of their anchors: temples, hands, the small of the back, knees. +const STR = [[0], [1], [8], [9], [4, 5], [10], [11]]; +// How low each particle may go. +const LIM = [3.5, 3.5, 2, 2, 2, 2, 1.5, 1.5, 1.3, 1.3, 1.6, 1.6, 0, 0].map((v) => FLOOR - v); +// Beats in each number. +const LEN = [14, 8, 10, 10, 12]; +// Each number's light: wash, follow spot, its radius, footlights, pin spot, rail lamps. +const CUES = [ + [0.5, 0.42, 0.34, 1, 0.9, 0.72, 21, 0.5, 0, 1], + [0.05, 0.05, 0.1, 0.3, 0.3, 0.34, 13, 0.12, 1, 0.8], + [0.12, 0.2, 0.42, 0.72, 0.84, 1, 20, 0.08, 0, 0.7], + [0.42, 0.1, 0.16, 1, 0.66, 0.36, 17, 1.2, 0, 1], + [0.58, 0.55, 0.46, 1, 1, 0.94, 25, 0.45, 0, 1], +]; +const WOOD = [205, 150, 95], FACE = [232, 194, 156], GOLD = [255, 200, 80], FT = [1, 0.72, 0.42], LM = [1, 0.78, 0.52], PK = [1, 0.97, 0.9]; + +export function setup(params) { + const F32 = (n) => new Float32Array(n); + const P = (params.s = { + p: F32(42), q: F32(42), f: F32(N * 3), lt: F32(N * 3), m: F32(N), alb: F32(N * 3), pi: new Int8Array(N), + b: 0, num: 0, nk: -1, cue: CUES[0].slice(), leap: -1, walk: 0, sx: 32, sy: 92, bell: [38, 62, 0, 0], dust: [], k1: 0, s4: 0.06, flash: 0, stc: [0, 0], + C: { x: 32, y: GRIP, r: 0, p: 0, w: 0, kL: 0, kR: 0, hL: 0, hR: 0, sH: 0, sB: 0, lean: 0 }, + }); + for (let i = 0; i < 14; i++) (P.p[i * 3] = P.q[i * 3] = 32 + REST[i * 2]), (P.p[i * 3 + 1] = P.q[i * 3 + 1] = REST[i * 2 + 1]); + const p = P.p, A = (P.A = anchors(P.C)); + P.bl = BONES.map(([a, b]) => sqrt((p[a * 3] - p[b * 3]) ** 2 + (p[a * 3 + 1] - p[b * 3 + 1]) ** 2)); + P.sl = STR.map((s, j) => { + const m = mid(p, s); + return sqrt((m[0] - A[j][0]) ** 2 + (m[1] - A[j][1]) ** 2 + (m[2] - A[j][2]) ** 2) + (j > 4 ? 2 : 1); + }); + paint(P); + phys(P, A, A, 1 / 120, 150); +} + +const mid = (p, s) => [0, 1, 2].map((c) => (p[s[0] * 3 + c] + p[s[s.length - 1] * 3 + c]) / 2); + +// The cross: a stick from the grip with a bar across it. The leg bar is in the other hand below it. +function anchors(C) { + const cp = cos(C.p), sp = sin(C.p), cr = cos(C.r), sr = sin(C.r), cw = cos(C.w), sw = sin(C.w); + const T = (lx, ly) => { + const y1 = ly * cp, z1 = ly * sp, x2 = lx * cr - y1 * sr, y2 = lx * sr + y1 * cr; + return [C.x + x2 * cw + z1 * sw, C.y + y2, -x2 * sw + z1 * cw]; + }; + const L = (s, k) => [C.x + s * 7 * cw, C.y + 19 - k, 3 - s * 7 * sw]; + return [T(-4, 5), T(4, 5), T(-12, 5), T(12, 5), T(0, 16), L(-1, C.kL), L(1, C.kR), T(0, 0)]; +} + +// Verlet steps: gravity, bones, threads that pull only when taut, the floor. +function phys(P, A0, A1, h, n) { + const p = P.p, q = P.q, C = P.C, h2 = h * h, d = [0, 0, 0]; + for (let st = 0; st < n; st++) { + const f = (st + 1) / n; + let rx = p[9] - p[6], rz = p[11] - p[8]; + const rl = sqrt(rx * rx + rz * rz) || 1; + (rx /= rl), (rz /= rl); + for (let i = 0; i < 14; i++) { + // Knees bend forward, elbows out and back, the head leans in a bow. + const sd = i & 1 ? 30 : -30, a = i > 9 && i < 12 ? [-90 * rz, 90 * rx] : i === 6 || i === 7 ? [sd * rx + 25 * rz, sd * rz - 25 * rx] : i < 2 ? [-C.lean * 400 * rz, C.lean * 400 * rx] : [0, 0]; + for (let c = 0, o = i * 3; c < 3; c++, o++) { + const v = (p[o] - q[o]) * DAMP; + (q[o] = p[o]), (p[o] += v + (c === 1 ? G : a[c >> 1]) * h2); + } + } + for (let it = 0; it < 6; it++) { + BONES.forEach(([a, b, k], j) => { + let l = 0; + for (let c = 0; c < 3; c++) (d[c] = p[b * 3 + c] - p[a * 3 + c]), (l += d[c] * d[c]); + const m = (1 - P.bl[j] / (sqrt(l) || 1e-6)) * 0.5 * k; + for (let c = 0; c < 3; c++) (p[a * 3 + c] += d[c] * m), (p[b * 3 + c] -= d[c] * m); + }); + for (let s = 0; s < 7; s++) { + const m = mid(p, STR[s]), L = max(4, P.sl[s] + (s < 2 ? C.sH : s === 2 ? -C.hL : s === 3 ? -C.hR : s === 4 ? C.sB : 0)); + let l = 0; + for (let c = 0; c < 3; c++) (d[c] = m[c] - mix(A0[s][c], A1[s][c], f)), (l += d[c] * d[c]); + if ((l = sqrt(l)) > L) for (const i of STR[s]) for (let c = 0; c < 3; c++) p[i * 3 + c] -= (d[c] * (l - L)) / l; + } + for (let i = 0; i < 14; i++) { + const o = i * 3; + if (p[o + 1] > LIM[i]) { + // A foot that comes down hard raises dust. + if (i > 11 && it === 0 && p[o + 1] - q[o + 1] > 0.45 && P.stc[i - 12] <= 0) stamp(P, p[o], p[o + 1] - q[o + 1]), (P.stc[i - 12] = 0.25); + (p[o + 1] = q[o + 1] = LIM[i]), (q[o] = p[o] - (p[o] - q[o]) * 0.5), (q[o + 2] = p[o + 2] - (p[o + 2] - q[o + 2]) * 0.5); + } + (p[o] = cl(p[o], 3, 60)), (p[o + 2] = cl(p[o + 2], -14, 14)); + } + } + } +} + +function stamp(P, x, v) { + for (let i = 0; i < 7; i++) P.dust.push([x + (Math.random() - 0.5) * 4, FLOOR + 1, (Math.random() - 0.5) * 40, -6 - Math.random() * 14 * v, 0.5 + Math.random() * 0.5]); +} + +// What the puppeteer does with the cross in each number, beat by beat. +function script(n, b) { + const T = { x: 32, y: GRIP, r: 0, p: 0, w: 0, kL: 0, kR: 0, hL: 0, hR: 0, sH: 0, sB: 0, lean: 0, au: 0 }; + const ph = fr(b), bu = sin(PI * ph), odd = floor(b) & 1; + if (n === 0) { + // The jig: a hop on every beat, knees and arms up in turn, across and back. + const tr = (TAU * b) / 8, sw = sin((PI * b) / 2), e = 1 - sm(LEN[0] - 1.5, LEN[0], b); + (T.x = 32 + 10 * sin(tr) * e), (T.w = 0.5 * cos(tr) * e), (T.y -= 3.5 * bu * e), (T[odd ? "kR" : "kL"] = 9 * bu * bu * e); + (T.r = 0.3 * sw), (T.hL = 13 * max(0, sw) * e), (T.hR = 13 * max(0, -sw) * e); + } else if (n === 1) { + // The bow: the head threads let out, the back thread holds, the cross tips over. + const u = sm(1, 2.4, b) * (1 - sm(5, 6.6, b)); + (T.y += u), (T.p = -0.5 * u), (T.sH = 11 * u), (T.sB = -3 * u), (T.lean = 1.3 * u), (T.w = 0.6 * u), (T.hR = 12 * sm(0.2, 1.2, b) * (1 - sm(4.6, 6.2, b))), (T.au = 1); + } else if (n === 2) { + // The flop: all let go, a heap on the floor, the head comes up to look round, lifted back up, a shake. + const dr = sm(0.8, 1.15, b) * (1 - sm(5.2, 6.8, b)), lk = sm(2.2, 2.9, b) * (1 - sm(4.4, 5, b)); + (T.x = 27), (T.y += 26 * dr), (T.sH = -14 * lk * dr), (T.w = 0.7 * dr + 0.6 * sin((TAU * (b - 2.2)) / 2.2) * lk), (T.au = 1); + T.r = 0.35 * sin(TAU * b) * sm(6.8, 7.3, b) * (1 - sm(9, 9.8, b)); + } else if (n === 3) { + // The stamp: a knee drawn up slowly and let drop hard on the beat. + (T.x = 37), (T[odd ? "kR" : "kL"] = 11 * sm(0.1, 0.6, ph) * (1 - sm(0.82, 0.9, ph))), (T.r = (odd ? 0.22 : -0.22) * bu); + (T.lean = -0.25), (T.hL = T.hR = 5), (T.w = 0.3 * sin((TAU * b) / 4)), (T.au = 1); + } else { + // The strut: across the stage and back, facing the way it goes. + const tr = (TAU * b) / 12; + (T.x = 32 + 12 * cos(tr)), (T.w = -0.8 * sin(tr)), (T.y -= 1.5 * bu), (T[odd ? "kR" : "kL"] = 6 * bu), (T.r = 0.25 * sin(PI * b)), (T.p = -0.1); + } + return T; +} + +// K1's leap, in seconds: crouch, jerk up, fly, land, bow. +function leap(P, T, rd) { + const u = P.leap, a = 1 - sm(2.7, 3.3, u), air = sm(0.25, 0.4, u) * (1 - sm(0.55, 0.95, u)), tuck = sm(0.3, 0.45, u) * (1 - sm(0.75, 0.95, u)); + const bow = sm(1.2, 1.7, u) * (1 - sm(2.4, 3, u)), cr = sm(0, 0.2, u) * (1 - sm(0.22, 0.32, u)); + (T.y = mix(T.y, GRIP + 4 * cr - 10 * air + 4 * bow, a)), (T.kL = T.kR = mix(T.kL, 7 * tuck, a)), (T.hL = T.hR = mix(T.hL, 15 * tuck, a)); + (T.sH = mix(T.sH, 10 * bow, a)), (T.lean = mix(T.lean, bow, a)), (T.p = mix(T.p, -0.5 * bow, a)), (T.r *= 1 - a), (T.w *= 1 - a), (T.x = mix(T.x, P.C.x, a)); + if (u < 0.4 && u + rd >= 0.4) P.flash = 1; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 0.06], rd = cl(dt || 0, 0, 0.05), d = (rd * cl(k[1], 0.2, 3)) / 2; + (P.k1 = cl(k[0], 0, 1)), (P.s4 = cl(k[3], -1, 1)); + const nk = round(cl(k[2], 0, 4)); + if (nk !== P.nk) { + if (P.nk < 0) P.cue = CUES[nk].slice(); + (P.nk = P.num = nk), (P.b = 0); + } + if ((P.b += d * BPS) >= LEN[P.num]) (P.b -= LEN[P.num]), (P.num = (P.num + 1) % 5); + if (input.btnPressed && input.btnPressed[0] && P.leap < 0) P.leap = 0; + const T = script(P.num, P.b), C = P.C, x0 = C.x; + if (P.leap >= 0 && (leap(P, T, rd), (P.leap += rd) > 3.3)) P.leap = -1; + const ex = min(1, rd / 0.45), e = min(1, rd / 0.05); + for (const key in C) C[key] = mix(C[key], T[key], key === "x" ? ex : e); + // Steps wherever the cross travels in a number without its own. + const dx = abs(C.x - x0); + P.walk += dx / 5; + if (T.au && rd > 0) { + const lift = min(1, dx / rd / 6) * 6 * sin(PI * fr(P.walk)) ** 2; + floor(P.walk) & 1 ? (C.kR = max(C.kR, lift)) : (C.kL = max(C.kL, lift)); + } + const A = anchors(C), n = max(1, ceil(rd * 120 - 1e-6)); + if (rd > 0) phys(P, P.A, A, rd / n, n); + P.A = A; + for (let i = 0; i < 10; i++) P.cue[i] = mix(P.cue[i], CUES[P.num][i], min(1, rd * 2.2)); + // The follow spot keeps to the body. + const p = P.p; + (P.sx = mix(P.sx, (p[6] + p[9] + p[12] + p[15]) / 4, min(1, rd * 3.5))), (P.sy = mix(P.sy, (p[7] + p[10] + p[13] + p[16]) / 4 + 3, min(1, rd * 2.5))); + // The bell on the cap swings after the head. + const hd = head(p), B = P.bell, gx = hd.x + hd.ux * 7 - hd.uy * 6, gy = hd.y + hd.uy * 7 + hd.ux * 6 + 2; + (B[2] += ((gx - B[0]) * 160 - B[2] * 7) * rd), (B[3] += ((gy - B[1]) * 160 - B[3] * 7 + 60) * rd), (B[0] += B[2] * rd), (B[1] += B[3] * rd); + (B[0] = cl(B[0], gx - 6, gx + 6)), (B[1] = cl(B[1], gy - 6, gy + 6)); + P.dust = P.dust.filter((s) => ((s[3] += 50 * rd), (s[0] += s[2] * rd), (s[1] = min(FLOOR + 2, s[1] + s[3] * rd)), (s[4] -= rd) > 0)); + (P.stc[0] -= rd), (P.stc[1] -= rd), (P.flash = max(0, P.flash - rd * 1.5)); +} + +// The head: its middle, which way is up, how far it is turned and nodded. +function head(p) { + const x = (p[0] + p[3]) / 2, y = (p[1] + p[4]) / 2 - 0.5, z = (p[2] + p[5]) / 2, sx = (p[6] + p[9]) / 2, sy = (p[7] + p[10]) / 2, sz = (p[8] + p[11]) / 2; + const l = Math.hypot(x - sx, y - sy) || 1; + return { x, y, ux: (x - sx) / l, uy: (y - sy) / l, yaw: atan2(-(p[5] - p[2]), p[3] - p[0]), nod: (z - sz) / 6.3 }; +} + +// What does not move: velvet, rail, backcloth, boards, apron. +function paint(P) { + const A = P.alb, I = P.pi; + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, o = i * 3, n = hash(x, y); + let c = [0.05, 0.045, 0.06]; + I[i] = -1; + if (y >= RAIL && y <= RAIL + 3) { + const g = (0.8 + 0.3 * hash(floor((x + y * 7) / 11), y)) * (y === RAIL ? 1.2 : y === RAIL + 3 ? 0.6 : 1) * (0.92 + 0.08 * n); + c = [0.55 * g, 0.36 * g, 0.22 * g]; + } else if (y >= CLOTH - 2 && y < CLOTH) c = [0.13, 0.1, 0.08]; + else if (y >= CLOTH && y < BACK) { + // Painted night: stars over two rows of hills, in soft folds. + const hf = 86 + 4 * sin(x * 0.09 + 0.5) + 2 * sin(x * 0.27 + 1), hn = 97 + 3 * sin(x * 0.13 + 2) + 1.5 * sin(x * 0.41); + const cx = floor(x / 7), cy = floor(y / 8), r = hash(cx, cy + 50), sx = cx * 7 + 1 + floor(r * 5), sy = cy * 8 + 1 + floor(hash(cx + 9, cy) * 6); + I[i] = y >= hn ? 2 : y >= hf ? 1 : r < 0.5 && y < hf - 3 ? (x === sx && y === sy ? 3 : abs(x - sx) + abs(y - sy) === 1 ? 4 : 0) : 0; + c = [(0.8 + 0.2 * (0.5 + 0.5 * sin(x * 0.95 + 0.6 * sin(y * 0.05)))) * (0.95 + 0.1 * n), 0, 0]; + } else if (y >= BACK && y < APRON) { + // Boards running towards you. + const u = (x - 31.5) / ((y - 30) / (APRON - 30)) / 6 + 0.5, bd = floor(u), g = (0.8 + 0.2 * hash(bd, 3)) * mix(0.45, 1, cl(min(fr(u), 1 - fr(u)) * 9 - 0.3, 0, 1)) * (y > APRON - 3 ? 1.12 : 1) * (0.92 + 0.14 * hash(x, bd * 31 + floor(y / 3))); + c = [0.6 * g, 0.42 * g, 0.27 * g]; + } else if (y >= APRON) c = y === APRON ? [0.4, 0.28, 0.18] : [0.14, 0.09, 0.07]; + (A[o] = c[0]), (A[o + 1] = c[1]), (A[o + 2] = c[2]); + } +} + +let F, L, M, MODE, SIDE; +// One LED of the puppet or cross: the shadow mask, or its colour in the light there. +function dot(x, y, c, nx = 0, flat = 0) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const i = y * W + x, o = i * 3; + if (MODE === 0) return void (M[i] = 1); + const k = 1 - 0.2 * nx * nx + 0.4 * SIDE * nx; + for (let j = 0; j < 3; j++) F[o + j] = c[j] * max(flat, L[o + j]) * k; +} +// A capsule; the colour may be a function of how far along. +function seg(x0, y0, x1, y1, r, c, flat = 0) { + const dx = x1 - x0, dy = y1 - y0, l2 = dx * dx + dy * dy || 1e-6; + for (let y = floor(min(y0, y1) - r); y <= ceil(max(y0, y1) + r); y++) + for (let x = floor(min(x0, x1) - r); x <= ceil(max(x0, x1) + r); x++) { + const px = x - x0, py = y - y0, t = cl((px * dx + py * dy) / l2, 0, 1), ex = px - dx * t, ey = py - dy * t; + if (ex * ex + ey * ey <= r * r) dot(x, y, typeof c === "function" ? c(t) : c, cl(ex / r, -1, 1), flat); + } +} +const disc = (x, y, r, c, flat) => seg(x, y, x, y, r, c, flat); + +// A thread: one LED wide over two columns, a faint halo, a glint that slides as it tilts. +function thread(ax, ay, bx, by, gl) { + const n = ceil(abs(by - ay)) + 1, tilt = (bx - ax) / (by - ay || 1), g0 = 0.5 + 6 * (tilt + SIDE * 0.06); + for (let i = 0; i <= n; i++) { + const t = i / n, x = mix(ax, bx, t), y = round(mix(ay, by, t)), x0 = floor(x), f = x - x0; + if (y < 0 || y >= H) continue; + if (MODE === 0) { + if (x0 >= 0 && x0 < W) M[y * W + x0] = max(M[y * W + x0], 0.45); + continue; + } + const o = (y * W + cl(x0, 0, W - 1)) * 3, gls = gl * 150 * exp(-((((t - g0) * n) / 5) ** 2)); + for (let e = -1; e < 3; e++) { + const xx = x0 + e, a = e < 0 ? 0.12 - 0.12 * f : e ? (e > 1 ? 0.12 * f : 0.12 + 0.88 * f) : 1 - 0.88 * f, q = (y * W + xx) * 3; + if (xx < 0 || xx >= W) continue; + for (let j = 0; j < 3; j++) F[q + j] += a * (24 + 64 * L[o + j] + gls); + } + } +} + +function puppet(P, C1, C2) { + const p = P.p, X = (i) => p[i * 3], Y = (i) => p[i * 3 + 1], Z = (i) => p[i * 3 + 2], tz = (Z(2) + Z(3) + Z(4) + Z(5)) / 4; + const stock = (t) => (floor(t * 5) & 1 ? C1 : [235, 228, 210]); + const limb = (a, b, c, up, low, r1, r2) => { + seg(X(a), Y(a), X(b), Y(b), r1, up), seg(X(b), Y(b), X(c), Y(c), r2, low); + }; + const arm = (s) => (limb(2 + s, 6 + s, 8 + s, C2, C1, 1.6, 1.2), disc(X(8 + s), Y(8 + s), 1.4, FACE)); + const leg = (s) => { + limb(4 + s, 10 + s, 12 + s, C2, stock, 1.8, 1.2); + const fx = X(12 + s), fy = Y(12 + s) + 1.2, yw = atan2(-(Z(3) - Z(2)), X(3) - X(2)); + seg(fx - 1.2, fy, fx + 1.2 + 2.4 * sin(yw), fy, 1.1, C2.map((v) => v * 0.55)), dot(round(fx + 2.4 + 2.6 * sin(yw)), round(fy - 1), GOLD); + }; + const parts = [[(Z(6) + Z(8)) / 2, () => arm(0)], [(Z(7) + Z(9)) / 2, () => arm(1)], [(Z(10) + Z(12)) / 2, () => leg(0)], [(Z(11) + Z(13)) / 2, () => leg(1)]]; + for (const [z, f] of parts) if (z < tz - 1) f(); + // The doublet in diamonds, flaring to a hem of points, with a belt. + const sx = (X(2) + X(3)) / 2, sy = (Y(2) + Y(3)) / 2, hx = (X(4) + X(5)) / 2, hy = (Y(4) + Y(5)) / 2, ax = hx - sx, ay = hy - sy, al = ax * ax + ay * ay || 1; + let ex = X(3) - X(2) + X(5) - X(4), ey = Y(3) - Y(2) + Y(5) - Y(4); + const el = Math.hypot(ex, ey) || 1, hwS = max(2.5, Math.hypot(X(3) - X(2), Y(3) - Y(2)) / 2), hwH = max(2.5, Math.hypot(X(5) - X(4), Y(5) - Y(4)) * 0.62); + (ex /= el), (ey /= el); + for (let y = floor(min(sy, hy) - 6); y <= ceil(max(sy, hy) + 6); y++) + for (let x = floor(min(sx, hx) - 7); x <= ceil(max(sx, hx) + 7); x++) { + const v = ((x - sx) * ax + (y - sy) * ay) / al, hw = mix(hwS, hwH, v), u = ((x - sx - ax * v) * ex + (y - sy - ay * v) * ey) / hw; + if (v < -0.05 || abs(u) > 1 || v > 1.1 + 0.12 * abs(fr(u * 2.5) - 0.5) * 2) continue; + const belt = v > 0.84 && v < 0.94, dia = (floor(u * 1.6 + v * 3.2) + floor(u * 1.6 - v * 3.2)) & 1; + dot(x, y, belt ? (abs(u) < 0.22 ? GOLD : [60, 40, 30]) : dia ? C1 : C2, u); + } + for (const [z, f] of parts) if (z >= tz - 1) f(); + // The ruff, the head, the face, the cap and its bell. + seg(X(2), Y(2) - 1, X(3), Y(3) - 1, 1.7, (t) => (floor(t * 9) & 1 ? [250, 246, 236] : [205, 200, 192])); + const hd = head(p), R = 4.3, px = -hd.uy, py = hd.ux; + disc(hd.x, hd.y, R, FACE); + if (MODE && hd.nod < 0.7) + for (const [ph, hy, c] of [[0.5, -0.4, [30, 22, 34]], [0.8, 1.2, [240, 120, 110]], [0, 2.1, [150, 40, 46]], [-0.5, -0.4, [30, 22, 34]], [-0.8, 1.2, [240, 120, 110]]]) { + const a = ph + hd.yaw; + if (cos(a) > 0.25) dot(round(hd.x + R * 0.85 * sin(a)), round(hd.y + hy + hd.nod * 3), c); + } + const b0x = hd.x + hd.ux * 3.1, b0y = hd.y + hd.uy * 3.1, b1x = hd.x + hd.ux * 8 + px, b1y = hd.y + hd.uy * 8 + py, [bx, by] = P.bell; + for (let i = 0, lx = b0x, ly = b0y; i <= 8; i++) { + const t = i / 8, x = mix(mix(b0x, b1x, t), mix(b1x, bx, t), t), y = mix(mix(b0y, b1y, t), mix(b1y, by, t), t); + seg(lx, ly, x, y, mix(3.3, 0.8, t), C1), (lx = x), (ly = y); + } + seg(hd.x + hd.ux * 2.3 - px * 4, hd.y + hd.uy * 2.3 - py * 4, hd.x + hd.ux * 2.3 + px * 4, hd.y + hd.uy * 2.3 + py * 4, 0.9, C2); + disc(bx, by, 1.3, GOLD); +} + +// The puppeteer's sleeves and hands, the cross and the leg bar. +function cross(P) { + const A = P.A, [gx, gy] = A[7], sleeve = [46, 38, 60], skin = [232, 176, 140], cuff = [236, 232, 222]; + const arm = (x0, x1, y1) => seg(x0, -3, x1, y1, 2.6, (t) => (t > 0.84 ? cuff : sleeve), 0.32); + seg(A[5][0] - 1.5, A[5][1], A[6][0] + 1.5, A[6][1], 0.7, WOOD, 0.35); + seg(gx, gy, A[4][0], A[4][1], 0.9, WOOD, 0.35), seg(A[2][0], A[2][1], A[3][0], A[3][1], 0.8, WOOD, 0.35); + for (let j = 0; j < 7; j++) dot(round(A[j][0]), round(A[j][1]), [220, 220, 230], 0, 0.5); + arm(gx + 17, gx + 1.5, gy - 1.5), disc(gx + 0.5, gy - 0.5, 2.2, skin, 0.4); + arm(A[5][0] - 22, A[5][0] - 2.5, A[5][1] - 1), disc(A[5][0] - 2, A[5][1] - 0.5, 2, skin, 0.4); +} + +function stage(P, pal) { + const [wr, wg, wb, sr0, sg0, sb0, rad0, foot, pin, lamp] = P.cue, s = P.s4, A = P.alb, I = P.pi, p = P.p; + const fl = 1 + 0.5 * P.flash, rad = rad0 + 7 * P.flash, S = [sr0 * fl, sg0 * fl, sb0 * fl], WS = [wr, wg, wb]; + const px = P.sx - s * 11, py = P.sy, rx = rad * (1 + 0.45 * abs(s)), ry = rad * 1.35, sdx = -s * 15, fx = (p[36] + p[39]) / 2, pinX = P.sx; + const lift = [0, 1].map((j) => 1 - sm(0, 8, FLOOR - p[37 + j * 3])); + for (let y = 0; y < H; y++) { + const ft = y < APRON ? foot * exp((y - APRON) / 14) : 0, wash = y < CLOTH - 2 ? (y >= RAIL && y <= RAIL + 3 ? 0.3 : 0.08) : 1, cw = 2 + (y + 12) * 0.085; + for (let x = 0; x < W; x++) { + const i = y * W + x, o = i * 3, dx = (x - px) / rx, dy = (y - py) / ry, pool = 1 - sm(0.84, 1, sqrt(dx * dx + dy * dy)); + const lm = lamp * (1 / (1 + ((x - 4.5) ** 2 + (y - 10) ** 2) / 260) + 1 / (1 + ((x - 59.5) ** 2 + (y - 10) ** 2) / 260)); + let cone = 0; + if (pin > 0.01) cone = y >= BACK ? pin * (1 - sm(0.7, 1, sqrt(((x - pinX) / 12) ** 2 + ((y - FLOOR - 1) / 4) ** 2))) : pin * (1 - sm(0.6, 1, abs(x - pinX) / cw)); + for (let j = 0; j < 3; j++) L[o + j] = wash * WS[j] + pool * S[j] + ft * FT[j] + lm * LM[j] + cone * PK[j]; + // The shadow stands up from the feet, larger, away from the spot. + let sh = 0, k = 1; + if (y >= CLOTH && y < APRON && pool > 0) { + const mx = round(fx + (x - fx - sdx) / 1.12), my = round(FLOOR + (y - FLOOR + 3) / 1.12); + if (mx >= 0 && mx < W && my >= 0 && my < H) sh = M[my * W + mx] * pool * 0.8; + } + if (y > BACK && y < APRON) for (let j = 0; j < 2; j++) k *= 1 - 0.6 * lift[j] * exp(-1.5 * (((x - p[36 + j * 3]) / 3.5) ** 2 + ((y - FLOOR - 1.5) / 1.3) ** 2)); + const c = I[i] >= 0 ? pal[I[i]] : null, f = c ? A[o] : 1, cf = c ? 0.8 * cone : 0; + for (let j = 0; j < 3; j++) F[o + j] = (c ? c[j] : A[o + j]) * f * k * (L[o + j] - sh * S[j] - cf * PK[j]) * 255; + } + } +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + [F, L, M, SIDE] = [P.f, P.lt, P.m, P.s4]; + const h = 0.36 + P.k1, hc = 0.61 + 0.3 * sin(TAU * P.k1), A = P.A, p = P.p; + const C1 = hsv(h, 0.8, 0.8).map((v) => v * 255), C2 = hsv(h + 0.06, 0.75, 0.42).map((v) => v * 255); + const sky = hsv(hc, 0.62, 0.34), star = [0.95, 0.78, 0.4], pal = [sky, hsv(hc + 0.06, 0.55, 0.46), hsv(hc + 0.12, 0.6, 0.58), star, sky.map((v, j) => mix(v, star[j], 0.4))]; + const threads = (from, to) => { + for (let s = from; s < to; s++) { + const m = mid(p, STR[s]), side = s < 2 ? (s ? 2 : -2) * cos(head(p).yaw) : 0; + thread(A[s][0], A[s][1], m[0] + side, m[1] - (s < 2 ? 1 : 0), s === 4 ? 0.4 : 1); + } + }; + // The shadow first: the puppet and its threads as a mask. + M.fill(0), (MODE = 0), threads(0, 7), puppet(P, C1, C2); + (MODE = 1), stage(P, pal); + threads(0, 7), puppet(P, C1, C2), cross(P); + // Dust from the stamps, the haze in the pin beam, the footlights. + for (const [x, y, , , l] of P.dust) { + const o = (round(y) * W + round(x)) * 3; + if (x >= 0 && x < W && y >= 0 && y < H) for (let j = 0; j < 3; j++) F[o + j] += [190, 160, 130][j] * l * 0.6 * max(0.4, L[o + j]); + } + const pin = P.cue[8]; + if (pin > 0.01) + for (let y = 0; y < BACK; y++) { + const cw = 2 + (y + 12) * 0.085; + for (let x = 0; x < W; x++) { + const c = pin * 0.09 * (1 - sm(0.6, 1, abs(x - P.sx) / cw)) * 255; + for (let j = 0; j < 3; j++) F[(y * W + x) * 3 + j] += c * PK[j]; + } + } + // The clip lamps on the rail and the footlights. + for (const x of [4.5, 59.5]) { + seg(x - 2, 7.5, x + 2, 7.5, 1.2, [40, 80, 58], 0.5), seg(x - 0.5, 9.2, x + 0.5, 9.2, 0.6, [255, 226, 170], P.cue[9]); + for (let i = -3; i < 4; i++) for (let j = 0; j < 3; j++) F[(10 * W + round(x + i)) * 3 + j] += (80 * P.cue[9] * LM[j]) / (1 + i * i); + } + const fb = min(1, P.cue[7] * 1.2 + 0.15); + for (const x of [6, 19, 32, 45, 57]) + for (let i = -2; i <= 3; i++) { + const a = i === 0 || i === 1 ? fb : fb * 0.25; + for (let j = 0; j < 3; j++) F[(125 * W + x + i) * 3 + j] += 255 * a * FT[j]; + } + for (let i = 0, o = 0; i < N; i++, o += 3) display.setPixel(i & 63, i >> 6, min(255, max(0, F[o])) | 0, min(255, max(0, F[o + 1])) | 0, min(255, max(0, F[o + 2])) | 0); +} From 9287d715f367ca981da8e66d98142bd459fa995c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 03:40:36 +0000 Subject: [PATCH 08/17] snow-monkeys: macaques soak in a steaming snowy hot spring at dusk Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DbfsCYkpNNJ5diTKTtPZEu --- .../pattern-study/patterns/snow-monkeys.js | 474 ++++++++++++++++++ 1 file changed, 474 insertions(+) create mode 100644 incoming/pattern-study/patterns/snow-monkeys.js diff --git a/incoming/pattern-study/patterns/snow-monkeys.js b/incoming/pattern-study/patterns/snow-monkeys.js new file mode 100644 index 000000000..750809b16 --- /dev/null +++ b/incoming/pattern-study/patterns/snow-monkeys.js @@ -0,0 +1,474 @@ +// ===== Patternflow pattern ===== +// Title: Snow Monkeys +// Family: mammal +// Idea: At dusk in the snow at Jigokudani, Japanese macaques soak to the shoulders in a steaming hot spring, dozing with snow on their heads and grooming each other, while a mother wades in off a boulder, her baby leaps in after her with a splash, and clumps of steam roll up the valley through the falling snow. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 25, after the Japanese macaques (Macaca fuscata) that bathe in the hot spring at Jigokudani, Nagano +// Round: 25 +// =============================== +// @matrix 64x128 +// @knobs Light=0..1, Speed=0.2..3, Monkeys=3..14, Wind=-1..1 +// +// Knob 1: Light. The blue snow light of early evening at 0, through a violet dusk, to night at 1, when the stone lantern by the pool burns orange and lights the steam, the water and the monkeys near it. +// Knob 2: Speed. At 2 the steam takes about ten seconds to climb from the water to the top of the panel, and the mother soaks for about twenty seconds, then sits on her boulder about as long; at 0.2 it all runs ten times slower. +// Knob 3: Monkeys. How many soak in the pool or sit on the rim rocks besides the mother and her baby, from three (a pair grooming and one asleep up to its chin) to a crowded fourteen. Turned up, they surface one by one; turned down, they sink away and the ones on the rim slip down behind their rocks. Wet fur steams, so more monkeys put up more steam. +// Knob 4: Wind. Calm in the middle, where the steam rises in clumps and sways; turned either way it leans the steam over and tears it off downwind, slants the snow and thickens it. +// Buttons: K1 tap sends the baby on the boulder leaping into the water with a splash; it paddles back and climbs out again. On its own it jumps about six seconds in, then every half minute or so. + +const W = 64, H = 128, N = W * H, GW = 32, GH = 64; +const { min, max, floor, round, sin, cos, sqrt, exp, abs, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const mix = (a, b, t) => a + (b - a) * t; +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +const 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 back edge of the pool, and how big a monkey stands at a depth row. +const PB = 67; +const scl = (y) => 0.74 + (y - PB) * 0.0135; +// The stone lantern's fire box. +const [LX, LY] = [8.5, 50.5]; +// The light from evening to night: ambient, sky overhead, sky at the ridge, +// lit steam, and the lantern's strength. +const LIGHT = [ + [0, 0.5, 0.6, 0.86, 24, 44, 104, 70, 98, 160, 196, 212, 240, 0.25], + [0.5, 0.36, 0.4, 0.66, 16, 20, 62, 84, 66, 118, 150, 156, 196, 0.65], + [1, 0.2, 0.24, 0.42, 5, 8, 24, 18, 24, 54, 84, 92, 130, 1.1], +]; +const LC = [1, 0.62, 0.3]; +const SNOW = [232, 238, 250], FUR = [218, 184, 150], FACE = [250, 96, 92], EAR = [214, 132, 124], HAND = [120, 84, 78], EYE = [40, 22, 20]; +const ROCK = [70, 64, 62], NEEDLE = [24, 46, 36], TRUNK = [52, 38, 30], WATER = [34, 70, 74], STONE = [118, 114, 108]; +// Boulders as half ellipses standing on their water line: centre, radii, +// depth row. The back rim, the boulder in the pool, the near rocks. +const ROCKS = [ + [2, 68, 12, 8, 66], [17, 69, 8, 6, 66], [30, 69, 9, 5, 66], [45, 68, 11, 6, 66], [61, 68, 10, 9, 66], + [53, 92, 13, 12, 90], + [6, 132, 15, 14, 126], [28, 133, 12, 12, 127], [48, 132, 11, 13, 126], [64, 129, 10, 15, 125], +]; +// Cedars on the slope, far to near: apex x and row, height, nearness. +const TREES = []; +for (let i = 0; i < 10; i++) TREES.push([2 + i * 6.7 + 3 * hash(i, 1), 4 + 9 * hash(i, 2), 16 + 8 * hash(i, 3), 0.3]); +for (let i = 0; i < 7; i++) TREES.push([i * 10.5 + 5 * hash(i, 4), 16 + 12 * hash(i, 5), 24 + 10 * hash(i, 6), 0.6]); +TREES.push([3, 12, 52, 1], [61, 6, 56, 1]); +// Places in the order the Monkeys knob fills them: x, depth row (the seat +// row on the rim), how low it sits in the water (0 shoulders out, 1 chin in +// it), and what it does (0 looks about, 1 dozes, 2 grooms the one before +// it, 3 sits on the rim rocks). +const SLOTS = [ + [24, 101, 0.15, 1], [34, 93, 0.1, 2], [11, 117, 0.6, 1], [30, 64.5, 0, 3], [52, 119, 0.25, 0], [8, 90, 0.2, 0], + [41, 63, 0, 3], [61, 110, 0.3, 0], [31, 79, 0.15, 0], [37, 118, 0.3, 1], [23, 121, 0.5, 1], [14, 77, 0.2, 1], + [5, 104, 0.2, 0], [46, 86, 0.1, 0], +]; +// The boulder seats and the way down into the water, as x and bottom row. +const SEAT = [52, 81], EDGE = [46, 83.5], FOOT = [44, 93], SPOT = [47, 103], BSEAT = [60, 83], BLAND = [53, 102], BFOOT = [60, 94]; + +function ramp(g, c) { + let j = 0; + while (j < LIGHT.length - 2 && g > LIGHT[j + 1][0]) j++; + const a = LIGHT[j], b = LIGHT[j + 1]; + return mix(a[c + 1], b[c + 1], cl((g - a[0]) / (b[0] - a[0]), 0, 1)); +} + +export function setup(params) { + const F32 = (n) => new Float32Array(n); + const P = (params.s = { + t: 0, init: false, f: F32(N * 3), bg: F32(N * 3), il: F32(N * 3), lm: F32(N), z: new Uint8Array(N), G: F32(GW * GH), key: -1, + puffs: [], drops: [], acc: 0, vent: 0.5, fx: 0, fy: 0, gust: 0, w: 0, k1: 0, n: 3, spl: null, + R: { st: 0, t: 0, dur: 3, lk: 0, lt: 0, lc: 1 }, B: { st: 0, t: 0, nj: 6.2, lk: 0, lt: 0, lc: 2 }, + }); + P.sd = 7; + P.mk = SLOTS.map(([x, y, sk, pose], i) => ({ x, y, sk, pose, s: pose > 2 ? 0.86 : scl(y), p: 0, sw: i * 0.3, hy: y - 8, lk: pose === 2 ? -0.8 : 0, lt: 0, lc: 1 + rnd() * 3, e: pose === 1 ? 1 : 0, bl: 2 + rnd() * 3, ph: rnd() * 7, i })); + P.flakes = Array.from({ length: 180 }, () => { + const z = rnd(); + return { x: rnd() * W, y: rnd() * H, z, ph: rnd() * 7, land: 40 + z * 88 }; + }); + P.flakes.forEach((f) => (f.y = rnd() * f.land)); + P.rg = Array.from({ length: W }, (_, x) => 8 + 5 * vn(x * 0.07, 1.3) - 0.06 * x); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) P.lm[y * W + x] = exp(-((x + 0.5 - LX) ** 2 + (y + 0.5 - LY) ** 2 * 0.8) / 1500); +} + +function spawn(P, x, y, a, g = rnd) { + P.puffs.push({ x, y, a, r: 2.5 + 2.5 * g(), age: 0, life: 7 + 5 * g(), vy: -(6 + 5 * g()), ph: g() * 7 }); +} + +// The steam: puffs off the water, thick clumps from the spring's mouth, all +// rising, growing and fading, swayed by the gusts and leaned by the wind. +function steam(P, d) { + const w = P.w; + P.gust = 0.55 * sin(P.t * 0.31) + 0.3 * sin(P.t * 0.83 + 2); + for (P.acc += d * 1.8; P.acc > 1; P.acc--) spawn(P, 2 + rnd() * 60 - w * 22, 74 + rnd() * 44, 0.2 + 0.25 * rnd()); + if ((P.vent -= d) < 0) { + P.vent = 0.7 + rnd() * 1.1; + const vx = [9, 22, 36][floor(rnd() * 3)] + rnd() * 6 - 3 - w * 14, vy = 72 + rnd() * 16; + for (let n = 0; n < 3; n++) spawn(P, vx + rnd() * 8 - 4, vy + rnd() * 6, 0.4 + 0.25 * rnd()); + } + // Wet fur steams in the cold: a wisp off each monkey's head now and then, + // from a generator of its own so the rest of the steam keeps its course. + for (const m of P.mk) + if (m.p > 0.5 && (m.sw -= d) < 0) { + const g = () => ((P.sd = (Math.imul(P.sd, 1103515245) + 12345) >>> 0) / 4294967296); + m.sw = 0.35 + g() * 0.6; + spawn(P, m.x + (g() - 0.5) * 6 * m.s, m.hy - 3 * m.s, 0.25 + 0.2 * g(), g); + } + const wx = w * 15 + P.gust * 7; + for (const q of P.puffs) { + q.age += d; + const up = sm(84, 20, q.y); + q.x += (wx * (0.3 + 0.7 * up) + 1.6 * sin(q.age * 1.3 + q.ph)) * d; + q.y += q.vy * (0.5 + 0.5 * sm(0, 2.5, q.age)) * d; + q.r += (0.9 + 0.8 * abs(w)) * d; + } + P.puffs = P.puffs.filter((q) => q.age < q.life * (1 - 0.35 * abs(w)) && q.y + q.r > -4 && q.x > -30 && q.x < 94); + P.fx -= wx * 0.2 * d; + P.fy += 1.8 * d; +} + +// Where the mother is on her way between the boulder and the pool, u 0..1. +function path(u) { + if (u < 0.3) { + const q = sm(0, 0.3, u); + return [mix(SEAT[0], EDGE[0], q), mix(SEAT[1], EDGE[1], q), 999, 90]; + } + if (u < 0.55) { + const q = sm(0.3, 0.55, u); + return [mix(EDGE[0], FOOT[0], q), mix(EDGE[1], FOOT[1], q), 999, 90 + 3 * q]; + } + const q = sm(0.55, 1, u), wl = mix(FOOT[1], SPOT[1], q); + return [mix(FOOT[0], SPOT[0], q), wl + 9.5 * q * scl(wl), wl, wl]; +} + +function look(m, d) { + if ((m.lc -= d) < 0) (m.lc = 1.2 + rnd() * 3), (m.lt = floor(rnd() * 3) - 1); + m.lk += (m.lt - m.lk) * min(1, d * 5); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + const d = (cl(dt || 0, 0, 0.1) * cl(k[1], 0.2, 3)) / 2; + P.k1 = cl(k[0], 0, 1); + P.n = round(cl(k[2], 3, 14)); + P.w = cl(k[3], -1, 1); + if (!P.init) { + // Open on a pool already steaming. + for (let i = 0; i < 120; i++) (P.t += 0.1), steam(P, 0.1); + P.t = 0; + for (const m of P.mk) m.p = m.i < P.n ? 1 : 0; + P.init = true; + } + const T = (P.t += d); + steam(P, d); + for (const m of P.mk) { + m.p = cl(m.p + (m.i < P.n ? d : -d) * 0.8, 0, 1); + if (m.pose % 3 === 0) { + look(m, d); + if ((m.bl -= d) < 0) m.bl = 2 + rnd() * 4; + m.e = m.bl < 0.15 ? 1 : 0; + } + } + // The mother sits on the boulder, wades in, soaks, and climbs out again. + const R = P.R; + if ((R.t += d) >= R.dur) { + R.st = (R.st + 1) % 4; + (R.t = 0), (R.dur = [16 + rnd() * 6, 3, 18 + rnd() * 6, 3.4][R.st]); + } + look(R, d); + // The baby on the boulder: a leap, a splash, a paddle back and a climb. + const B = P.B; + if (input.btnPressed && input.btnPressed[0] && B.st === 0) B.nj = 0; + if (B.st === 0 && (B.nj -= d) <= 0) (B.st = 1), (B.t = 0); + if (B.st) B.t += d; + if (B.st === 1 && B.t >= 0.7) { + (B.st = 2), (B.t = 0), (P.spl = { x: BLAND[0], y: BLAND[1], t: 0 }); + for (let n = 0; n < 34; n++) { + const a = (rnd() - 0.5) * 2.4; + P.drops.push([BLAND[0] + a * 2, BLAND[1] - 1, a * 14 + (rnd() - 0.5) * 6, -12 - rnd() * 22, BLAND[1] + (rnd() - 0.3) * 6]); + } + for (let n = 0; n < 4; n++) spawn(P, BLAND[0] + rnd() * 10 - 5, BLAND[1] - 4, 0.8); + } + if (B.st === 2 && B.t >= 4.2) (B.st = 3), (B.t = 0); + if (B.st === 3 && B.t >= 1.3) (B.st = 0), (B.t = 0), (B.nj = 24 + rnd() * 10); + look(B, d); + if (P.spl && (P.spl.t += d) > 2) P.spl = null; + P.drops = P.drops.filter((q) => { + (q[0] += q[2] * d), (q[1] += q[3] * d), (q[3] += 70 * d); + return !(q[3] > 0 && q[1] > q[4]); + }); + for (const f of P.flakes) { + f.y += (5 + 9 * f.z) * (1 + 0.6 * abs(P.w)) * d; + f.x += ((P.w * 18 + P.gust * 4) * (0.4 + 0.6 * f.z) + 0.9 * sin(T * 1.7 + f.ph)) * d; + if (f.y > f.land) (f.y = -2 - rnd() * 4), (f.x = rnd() * W); + f.x = ((f.x % W) + W) % W; + } +} + +// The valley, the rocks, the lantern and the still water, kept until the light changes. +function backdrop(P) { + P.key = round(P.k1 * 60); + const g = P.k1, A = [0, 1, 2].map((c) => ramp(g, c)), top = [3, 4, 5].map((c) => ramp(g, c)), low = [6, 7, 8].map((c) => ramp(g, c)); + const lamp = ramp(g, 12), BG = P.bg, IL = P.il, Z = P.z; + P.steamC = [9, 10, 11].map((c) => ramp(g, c)); + P.lamp = lamp; + P.low = low; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x, o = i * 3, lm = P.lm[i]; + for (let c = 0; c < 3; c++) IL[o + c] = A[c] + LC[c] * lamp * lm; + let col = null, k = 1, z = 0; + const rg = P.rg[x]; + if (y >= rg) { + // Snow on the slope, darker where scrub and rock break through. + col = SNOW; + k = 0.3 + 0.16 * vn(x * 0.12, y * 0.07) + 0.12 * (y / 66); + if (vn(x * 0.45 + 3, y * 0.28) > 0.7) k *= 0.45; + } + for (const [tx, ty, th, f] of TREES) { + // Tiers of drooping branches, each loaded with snow along its top. + const dx = abs(x + 0.5 - tx), ts = 3.2 + 2.6 * f, yy = y - ty - dx * 0.5, kq = floor(yy / ts), wi = yy - kq * ts; + if (yy < 0 || y > ty + th + 3) continue; + if (y > ty + th) { + if (dx < 0.4 + f) (col = TRUNK), (k = 0.6); + continue; + } + if (dx >= 0.5 + (y - ty) * 0.34 * (0.68 + 0.32 * (wi / ts)) * (0.85 + 0.3 * hash(kq, tx | 0))) continue; + if (wi < 1 + 0.6 * f && hash(x, y) > 0.12) (col = SNOW), (k = 0.4 + 0.32 * f - 0.1 * (dx / th)); + else (col = NEEDLE), (k = wi > ts - 0.7 ? 0.5 : 0.9 + 0.2 * hash(x + 7, y)); + if (f < 1) k = mix(k, 0.34, (1 - f) * 0.45); + } + if (y >= PB) (col = WATER), (z = 1), (k = 0.5 + 0.6 * ((y - PB) / 54)); + for (const [cx, cy, rx, ry, dz] of ROCKS) { + const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry, dd = nx * nx + ny * ny; + if (dd > 1 || y + 0.5 > cy) continue; + z = dz; + const nz = sqrt(1 - dd); + if (ny < -0.42 - 0.22 * nz + 0.25 * (vn(x * 0.6, cy) - 0.5)) (col = SNOW), (k = 0.5 + 0.28 * nz - 0.12 * ny); + else (col = ROCK), (k = (0.6 + 0.5 * nz - 0.3 * nx) * (0.75 + 0.4 * vn(x * 0.5, y * 0.5))); + } + Z[i] = z; + if (!col) { + const v = (y / rg) ** 1.5; + for (let c = 0; c < 3; c++) BG[o + c] = mix(top[c], low[c], v); + continue; + } + for (let c = 0; c < 3; c++) BG[o + c] = col[c] * k * IL[o + c] + (z === 1 ? low[c] * 0.12 : 0); + } + // The stone lantern: finial, snowed roof, fire box with its glowing window, + // pillar and foot, standing on the rim rock. + const ROWS = [[42, 0.6, 1], [44, 3.6, 0], [46, 5, 1], [48, 2.6, 2], [54, 3.6, 1], [55, 1.3, 1], [60, 2.8, 1], [62, 0, 1]]; + for (let r = 0; r < ROWS.length - 1; r++) + for (let y = ROWS[r][0]; y < ROWS[r + 1][0]; y++) + for (let x = 0; x < 18; x++) { + const dx = abs(x + 0.5 - LX), hw = ROWS[r][1] + (r === 2 ? y - 46 : 0) * 0.6, o = (y * W + x) * 3; + if (dx > hw) continue; + const kind = ROWS[r][2], win = kind === 2 && dx < 1.6 && y > 48 && y < 53; + for (let c = 0; c < 3; c++) + BG[o + c] = win ? [255, 200, 120][c] * min(1, 0.72 + 0.3 * lamp) : (kind ? STONE[c] * (0.6 + 0.25 * (LX - x) / hw) : SNOW[c] * 0.75) * IL[o + c]; + } +} + +let F, IL, Z, M; +// An ellipse of fur or skin on the monkey being drawn, under its water line +// and behind any rock nearer than it. +function blob(cx, cy, rx, ry, col, sn, fur) { + for (let y = max(0, floor(cy - ry)), y1 = min(H - 1, cy + ry); y <= y1 && y + 0.5 < M.wl; y++) { + const ny = (y + 0.5 - cy) / ry; + for (let x = max(0, floor(cx - rx)), x1 = min(W - 1, cx + rx); x <= x1; x++) { + const nx = (x + 0.5 - cx) / rx, dd = nx * nx + ny * ny, i = y * W + x; + if (dd > 1 || Z[i] > M.z) continue; + const nz = sqrt(1 - dd); + let k, c = col; + if (fur) { + k = (0.36 + 0.55 * nz - 0.25 * ny - 0.12 * nx) * (0.84 + 0.3 * hash(x - M.x0, y - M.y0)); + if (sn && ny < sn * 0.5 - 1.05 + 0.25 * hash(x - M.x0, 9)) (c = SNOW), (k = 0.75 + 0.25 * nz); + else k *= 0.6 + 0.4 * sm(0.3, 2.2, M.wl - y - 0.5); + } else k = 0.72 + 0.3 * nz - 0.15 * ny; + paint(i, c, k); + } + } +} +function paint(i, c, k) { + const o = i * 3; + for (let n = 0; n < 3; n++) F[o + n] = c[n] * k * (IL[o + n] * 0.6 + 0.4); +} +function dot(x, y, c, k) { + (x = floor(x)), (y = floor(y)); + if (x >= 0 && x < W && y >= 0 && y + 0.5 < M.wl && Z[y * W + x] <= M.z) paint(y * W + x, c, k); +} +// A limb of fur from a to b. +function rod(ax, ay, bx, by, r, col) { + const dx = bx - ax, dy = by - ay, ll = dx * dx + dy * dy || 1; + for (let y = max(0, floor(min(ay, by) - r)); y <= min(H - 1, max(ay, by) + r) && y + 0.5 < M.wl; y++) + for (let x = max(0, floor(min(ax, bx) - r)); x <= min(W - 1, max(ax, bx) + r); x++) { + const qx = x + 0.5 - ax, qy = y + 0.5 - ay, t = cl((qx * dx + qy * dy) / ll, 0, 1), ex = qx - dx * t, ey = qy - dy * t; + if (ex * ex + ey * ey > r * r || Z[y * W + x] > M.z) continue; + paint(y * W + x, col, (0.75 - 0.2 * (ey / r)) * (0.84 + 0.3 * hash(x, y))); + } +} + +// One macaque: x, bottom row, scale, water line, depth, the turn of its head +// (-1..1), how far it bows, eyes shut, snow on its head, sat on a rock. +function monkey(x, yb, s, wl, z, lk, bow, shut, sn, sat) { + M = { wl, z, x0: floor(x), y0: floor(yb) }; + blob(x, yb - 6.6 * s, 6.8 * s, 6.6 * s, FUR, sat ? 0.3 : 0, 1); + if (sat) { + for (const sd of [-1, 1]) { + blob(x + sd * 3.4 * s, yb - 2.6 * s, 2.7 * s, 2.3 * s, FUR, 0, 1); + blob(x + sd * 4.2 * s, yb - 0.6 * s, 1.5 * s, 0.9 * s, HAND, 0, 0); + } + for (const sd of [-1, 1]) rod(x + sd * 5 * s, yb - 10 * s, x + sd * 1.4 * s, yb - 4.8 * s, 1.4 * s, FUR); + blob(x, yb - 4.7 * s, 2 * s, 1 * s, HAND, 0, 0); + } + const R = 4.4 * s, hx = x + lk * 0.5 * s, hy = yb - (15.2 - bow) * s; + for (const sd of [-1, 1]) blob(hx + sd * R * (1 - 0.2 * lk * sd), hy - 0.3 * s, 1.1 * s + 0.2, 1.3 * s + 0.2, EAR, 0, 0); + blob(hx, hy, R * 1.05, R * 0.97, FUR, sn, 1); + const fx = hx + lk * 1.4 * s, fy = hy + (0.9 + bow * 0.25) * s; + blob(fx, fy, 2.2 * s * (1 - 0.2 * abs(lk)), 2.6 * s, FACE, 0, 0); + for (const sd of [-1, 1]) dot(fx + sd * 0.95 * s, fy - 0.6 * s, shut > 0.5 ? FACE : EYE, shut > 0.5 ? 0.45 : 1); + dot(fx, fy + 1.2 * s, FACE, 0.5); +} + +export function draw(display, params) { + const P = params.s; + if (!P || !P.init) return; + if (P.key !== round(P.k1 * 60)) backdrop(P); + (F = P.f), (IL = P.il), (Z = P.z); + F.set(P.bg); + const T = P.t, lamp = P.lamp, BG = P.bg; + // The water: a slow shiver over it and the lantern's broken reflection. + for (let y = PB; y < H; y++) { + const sx = 0.7 * sin(y * 0.5 + T * 1.3), rx = LX + 1.4 * sin(y * 0.9 + T * 3), rk = lamp * 140 * sm(104, 70, y); + for (let x = 0; x < W; x++) { + const i = y * W + x; + if (Z[i] !== 1) continue; + const r = vn(x * 0.3 + sx, y * 0.9 - T * 0.8), o = i * 3, dx = x + 0.5 - rx, ref = dx * dx < 30 && rk ? rk * r * exp((-dx * dx) / 5) : 0; + for (let c = 0; c < 3; c++) F[o + c] = BG[o + c] * (0.75 + 0.55 * r) + ref * LC[c]; + } + } + // Who is where, back to front. + const L = []; + for (const m of P.mk) { + if (m.p <= 0) continue; + if (m.pose > 2) { + // On the rim it slides down behind its rock as it goes. + const yb = m.y + 20 * (1 - m.p) * m.s; + m.hy = yb - 15.2 * m.s; + L.push([65, () => monkey(m.x, yb, m.s, 999, 65, m.lk, 0, m.e, 0.6, 1)]); + continue; + } + const wl = m.y + 0.35 * sin(T * 0.7 + m.ph), sk = m.sk + (m.pose === 1 ? 0.08 * sin(T * 0.5 + m.ph) : 0); + const yb = wl + (9.5 + 3.4 * sk + 20 * (1 - m.p)) * m.s; + m.hy = yb - (15.2 - (m.pose ? 1 : 0)) * m.s; + L.push([m.y, () => monkey(m.x, yb, m.s, wl, m.y, m.lk, m.pose ? 1 : 0, m.e, m.pose === 1 ? 1 : 0.55, 0), m]); + } + const R = P.R, u = R.st === 0 ? 0 : R.st === 2 ? 1 : R.st === 1 ? R.t / 3 : 1 - R.t / 3.4; + const [rx, ryb, rwl, rz] = path(u), rs = scl(rz); + L.push([rz, () => monkey(rx, ryb, rs, rwl, rz, R.lk, 0, R.st === 2 ? 1 : 0, 0.7, rwl > 900), rwl < 900 && { x: rx, s: rs, p: 1, pose: 0, ph: 0 }]); + const B = P.B, bs = 0.64; + let bx, byb, bwl = 999, bz = 90; + if (B.st === 0) [bx, byb] = BSEAT; + else if (B.st === 1) { + const q = B.t / 0.7; + (bx = mix(BSEAT[0], BLAND[0], q)), (byb = mix(BSEAT[1], BLAND[1], q) - 14 * sin(PI * q)), (bz = mix(90, BLAND[1], q)); + } else if (B.st === 2) { + const q = sm(0.8, 4.2, B.t); + (bwl = mix(BLAND[1], BFOOT[1], q)), (bx = mix(BLAND[0], BFOOT[0], q)), (bz = bwl); + byb = bwl + (12.5 + 14 * (1 - sm(0.15, 0.6, B.t))) * bs * scl(bwl); + } else { + const q = sm(0, 1.3, B.t); + (bx = mix(BFOOT[0], BSEAT[0], q)), (byb = mix(BFOOT[1], BSEAT[1], q)), (bz = mix(94, 90, q)); + } + L.push([bz, () => monkey(bx, byb, bs * scl(bz), bwl, bz, B.lk, 0, 0, 0.4, B.st !== 2), B.st === 2 && { x: bx, s: 0.6, p: 1, pose: 0, ph: 3 }]); + L.sort((a, b) => a[0] - b[0]); + // Rings on the water round everyone in it. + for (const [z, , m] of L) { + if (!m) continue; + const s = m.s * m.p; + for (let n = 0; n < 3; n++) { + const r = 6.5 * s + ((T * (m.pose === 1 ? 1.6 : 3) + n * 4.4 + m.ph) % 13), a = (1 - (r - 6.5 * s) / 13) * 0.8 * m.p; + for (let y = floor(z - r * 0.3 - 1); y <= z + r * 0.3 + 1; y++) + for (let x = max(0, floor(m.x - r - 1)); x <= min(W - 1, m.x + r + 1); x++) { + const i = y * W + x; + if (y >= H || Z[i] !== 1) continue; + const e = sqrt(((x + 0.5 - m.x) / r) ** 2 + ((y + 0.5 - z) / (r * 0.3)) ** 2); + if (abs(e - 1) * r < 0.7) for (let c = 0; c < 3; c++) F[i * 3 + c] += (P.low[c] * 0.6 + 60 * IL[i * 3 + c]) * a; + } + } + } + for (const [, fn, m] of L) { + fn(); + // The groomer's hands in the fur of the one in front of it. + if (m && m.i === 0 && P.mk[1].p > 0.5) { + const g = P.mk[1], gs = g.s, hx = m.x + 2 * m.s, hy = m.hy - 3 * m.s; + M = { wl: g.y, z: m.y + 1, x0: 0, y0: 0 }; + rod(g.x - 4.8 * gs, g.y - 1.5 * gs, hx + 1.4 * sin(T * 3.1), hy + 0.8 * cos(T * 3.1), 1.3 * gs, FUR); + rod(g.x - 2 * gs, g.y - 1.5 * gs, hx + 4 * m.s, hy + 5 * m.s + sin(T * 2.3), 1.3 * gs, FUR); + M.wl = 999; + blob(hx + 1.4 * sin(T * 3.1), hy + 0.8 * cos(T * 3.1), 1.2 * gs, 1 * gs, HAND, 0, 0); + } + } + // The splash: a plume, foam spreading and fading, a ring running out, and + // the drops falling back. + M = { wl: 999, z: 200, x0: 0, y0: 0 }; + if (P.spl) { + const q = P.spl.t, { x: sx, y: sy } = P.spl, h = 7 * (1 - sm(0, 0.45, q)); + if (h > 0.5) blob(sx, sy - h, 1.6 + 2 * q, h + 0.5, SNOW, 0, 0); + for (let y = floor(sy - 3), rx = 3 + 6 * q, ry = 1.4 + 1.2 * q; y <= sy + 3 && q < 1.4; y++) + for (let x = max(0, floor(sx - rx)); x <= min(W - 1, sx + rx); x++) { + const e = 1 - ((x + 0.5 - sx) / rx) ** 2 - ((y + 0.5 - sy) / ry) ** 2, o = (y * W + x) * 3; + if (e > 0) for (let c = 0; c < 3; c++) F[o + c] += (SNOW[c] * IL[o + c] * 1.2 - F[o + c]) * min(1, e * 3) * (1 - q / 1.4); + } + const r = 4 + 20 * q; + for (let x = 0; x < W; x++) + for (const sd of [-1, 1]) { + const dx = (x + 0.5 - P.spl.x) / r, y = floor(P.spl.y + sd * r * 0.3 * sqrt(max(0, 1 - dx * dx))); + if (abs(dx) < 1 && y < H && Z[y * W + x] === 1) for (let c = 0; c < 3; c++) F[(y * W + x) * 3 + c] += 120 * (1 - q / 2) * IL[(y * W + x) * 3 + c]; + } + } + for (const [x, y] of P.drops) dot(x, y, SNOW, 1); + // The steam over everything, lit by the sky and the lantern. + const G = P.G, SC = P.steamC; + G.fill(0); + for (const q of P.puffs) { + const a = q.a * sm(0, 0.9, q.age) * (1 - sm(q.life * 0.5, q.life, q.age)), gx = q.x / 2, gy = q.y / 2, rr = q.r * 0.75; + for (let j = max(0, floor(gy - rr * 1.3)); j <= min(GH - 1, gy + rr * 1.3); j++) + for (let i = max(0, floor(gx - rr)); i <= min(GW - 1, gx + rr); i++) { + const e = 1 - (i + 0.5 - gx) ** 2 / (rr * rr) - (j + 0.5 - gy) ** 2 / (rr * rr * 1.6); + if (e > 0) G[j * GW + i] += a * e * e; + } + } + // A low mist on the water, and the steam torn into wisps. + for (let j = 0, c = 0; j < GH; j++) + for (let i = 0, mist = 0.2 * sm(PB - 8, PB + 14, j * 2 + 1); i < GW; i++, c++) + if ((G[c] += mist) > 0.01) G[c] *= 0.2 + 0.85 * vn(i * 0.24 + P.fx, j * 0.17 + P.fy) + 0.45 * vn(i * 0.62 - P.fx, j * 0.44 + P.fy * 2); + for (let y = 0; y < H; y++) { + const v = y / 2 - 0.25, j0 = cl(floor(v), 0, GH - 1), j1 = min(GH - 1, j0 + 1), fv = cl(v - j0, 0, 1), ay = 0.4 + 0.45 * sm(108, 56, y); + for (let x = 0; x < W; x++) { + const u2 = x / 2 - 0.25, i0 = cl(floor(u2), 0, GW - 1), i1 = min(GW - 1, i0 + 1), fu = cl(u2 - i0, 0, 1); + const D = mix(mix(G[j0 * GW + i0], G[j0 * GW + i1], fu), mix(G[j1 * GW + i0], G[j1 * GW + i1], fu), fv); + if (D < 0.05) continue; + const a = ay * sm(0.05, 1.3, D), i = y * W + x, o = i * 3, lm = P.lm[i] * lamp * 200; + for (let c = 0; c < 3; c++) F[o + c] += (SC[c] + LC[c] * lm - F[o + c]) * a; + } + } + // Snow falling, thicker in the wind. + const nf = floor(110 + 70 * abs(P.w)); + for (let n = 0; n < nf; n++) { + const f = P.flakes[n], x = floor(f.x), y = floor(f.y); + if (y < 0 || y >= H) continue; + const i = y * W + x, o = i * 3, b = 0.45 + 0.5 * f.z; + for (let c = 0; c < 3; c++) F[o + c] += (SNOW[c] * (IL[o + c] * 0.6 + 0.4) - F[o + c]) * b; + } + 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 f62335088f358d35fc628c2d3df5f4538ea8c465 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 03:40:57 +0000 Subject: [PATCH 09/17] excavation: a strutted city pit in section, dug a stage at a time Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DbfsCYkpNNJ5diTKTtPZEu --- incoming/pattern-study/patterns/excavation.js | 390 ++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 incoming/pattern-study/patterns/excavation.js diff --git a/incoming/pattern-study/patterns/excavation.js b/incoming/pattern-study/patterns/excavation.js new file mode 100644 index 000000000..e8adeb510 --- /dev/null +++ b/incoming/pattern-study/patterns/excavation.js @@ -0,0 +1,390 @@ +// ===== Patternflow pattern ===== +// Title: Excavation +// Family: construction +// Idea: A city excavation cut open in section: an excavator on the floor of a strutted pit heaps up spoil, a clamshell grab on a gantry hauls it a hundred rows up to dump trucks on the street, and each stage deeper a new tier of steel struts is welded in. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 25, after the strutted basement excavations dug between city buildings +// Round: 25 +// =============================== +// @matrix 64x128 +// @knobs Hour=0..1, Speed=0.2..3, Depth=8..10, Water=0..1 +// +// Knob 1: Hour. Midday at 0, a shaft of sun falling through the deck opening past the tiers of struts; afternoon slants it onto the far wall, and at 1 it is night and sodium lamps on the struts and a floodlight on the trolley light the pit. +// Knob 2: Speed. At 2 a grab takes about nine seconds from the heap to the truck and a stage of nine rows about a minute; at 0.2 it all runs ten times slower, clouds and their shadows too. +// Knob 3: Depth. How many of the eleven stages are dug when it starts, eight to ten, each with its strut. After the eleventh the floor gets its concrete and the view moves on to the next lot. +// Knob 4: Water. Groundwater seeping through the lagging, the pool round the pump and the hose's flow; past halfway it rains and the shaft of sun fades. +// Buttons: K1 tap sounds the warning and fires a small blast at the face; dust fills the bottom of the pit and the floor drops. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, round, sin, cos, abs, exp, sqrt, atan2, acos, hypot, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const mix = (a, b, t) => a + (b - a) * t; +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +const rnd = () => Math.random(); +function hash(i, j) { + let h = (Math.imul(i, 374761393) + Math.imul(j, 668265263)) | 0; + h = Math.imul(h ^ (h >>> 13), 1274126177); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +} +function vn(x, y) { + const xi = floor(x), yi = floor(y), u = x - xi, v = y - yi, su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v); + const a = hash(xi, yi), b = hash(xi + 1, yi), c = hash(xi, yi + 1); + return a + (b - a) * su + (c - a) * sv + (a - b - c + hash(xi + 1, yi + 1)) * su * sv; +} + +// Street, the pit's inside, the opening's edge, the heap, the truck bed, the last stage. +const [GL, PL, PR, OR, HX, TT, FIN] = [20, 10, 53, 33, 21, 43, 11]; +const strutY = (k) => 18 + 9 * k, floorY = (k) => 23 + 9 * k; +const KP = [29, 41], EXP = [37, 41, 44, 41]; +// Fill, clay, sand and gravel, weathered rock, rock, and the rows they end at. +const STRATA = [[96, 84, 78], [186, 106, 58], [206, 186, 142], [154, 138, 94], [118, 126, 146]]; +const BOT = [27, 45, 64, 101, 999]; +const layer = (x, y) => { + let i = 0; + while (i < 4 && y >= BOT[i] + 5 * (vn(x * 0.09, i * 5.3) - 0.5)) i++; + return i; +}; +// Noon, afternoon, dusk, night: sky top, horizon, sun, ambient, buildings. +const KEYS = [ + [50, 116, 222, 168, 206, 240, 1, 0.95, 0.84, 0.92, 0.95, 1, 150, 158, 176], + [56, 100, 196, 232, 200, 160, 1, 0.82, 0.56, 0.8, 0.75, 0.72, 140, 128, 132], + [34, 32, 90, 236, 110, 70, 0.9, 0.5, 0.26, 0.5, 0.42, 0.5, 80, 66, 90], + [5, 7, 26, 22, 26, 60, 0, 0, 0, 0.13, 0.14, 0.22, 30, 32, 48], +]; +const WIN = [255, 196, 120], CAP = [176, 172, 166], DECK = [124, 118, 110], ROAD = [74, 74, 80], RAIN = [190, 210, 240], FLOOD = [1, 0.97, 0.9]; +const WALLS = [[8, [120, 86, 54]], [9, [96, 104, 118]], [54, [96, 104, 118]], [55, [120, 86, 54]]]; +const YEL = [250, 178, 26], GAN = [244, 204, 50], GRAB = [246, 142, 30], STEEL = [150, 160, 178], HOSE = [50, 80, 170]; + +export function setup(params) { + const F32 = (n) => new Float32Array(n); + const P = (params.s = { + f: F32(N * 3), lm: F32(N * 3), so: F32(N * 3), bw: F32(N * 3), sn: F32(N * 3), st: F32(N), shf: F32(N), sh: F32(200), rs: F32(H), srf: F32(W), dig: F32(W), + t: 0, rt: 0, nk: -1, cx: 42, du: [], pa: [], sl: -1, bl: -1, dust: 0.3, arc: 0, tx: 0, tv: 0, tph: 0, load: 2, tgo: 0, + }); + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, o = i * 3, L = layer(x, y), hh = hash(x, y), pd = hypot(x - 3, y - 24.5), px = (x - PL) % 7; + // The ground in section, each layer with its grain; a water main and ducts. + let k = 0.86 + 0.22 * hh, c = STRATA[L]; + if (L === 0 && hh > 0.88) c = [150, 146, 140]; + else if (L === 1) k = 0.9 + 0.14 * vn(x * 0.4, y * 0.7); + else if (L === 2 && hash(x >> 1, y >> 1) < 0.2) c = [136, 132, 128]; + else if (L === 3 && abs(vn(x * 0.3, y * 0.3) - 0.5) < 0.04) k = 0.55; + else if (L === 4 && (x + y * 0.7 + 5 * vn(x * 0.2, y * 0.1)) % 13 < 0.9) k = 0.6; + if (pd < 2.2) (c = pd > 1.2 ? [96, 120, 150] : [26, 30, 36]), (k = 1); + if (x > 57 && x < 63 && (y === 23 || y === 24) && x & 1) (c = [230, 120, 40]), (k = 1); + // The far wall: H-piles and lagging, shotcrete and rock bolts lower down. + const q = 0.8 + 0.35 * hash(floor((y - GL) / 3), floor((x - PL) / 7)), r = 0.84 + 0.22 * vn(x * 0.3, y * 0.3); + const b = y < GL + 2 ? CAP : !px ? [86, 92, 104] : y > 65 ? (px === 3 && y % 8 === 4 ? [60, 60, 66] : [136 * r, 132 * r, 126 * r]) : (y - GL) % 3 > 1 ? [44, 34, 28] : [132 * q, 94 * q, 60 * q]; + for (let j = 0; j < 3; j++) (P.so[o + j] = c[j] * k), (P.bw[o + j] = b[j]); + P.st[i] = sm(0.42, 0.72, vn(x * 0.8, y * 0.06)); + } + skyline(P); +} + +function skyline(P) { + P.bd = []; + for (let x = 0, w, s; x < W; x += w) (s = !x || x > 50), (w = !x ? 8 : s ? W - x : min(4 + floor(rnd() * 5), 56 - x)), P.bd.push([x, x + w - 1, s ? floor(rnd() * 3) : 6 + floor(rnd() * 8), rnd(), s]); +} + +// A new lot, dug to stage k, with the grab already on its way down. +function lot(P, k) { + Object.assign(P, { k, fy: floorY(k) + 2.5, hv: 64, wx: -1, mat: -1, ex: 41, ei: 1, sc: 1, ec: 0, full: 0, E: { p: [7, -9, 0] }, C: { p: [HX, 30, 1] } }); + P.dig.fill(0); + go(P.E, 0, 0.7, [12, -3, 0]), go(P.C, 1, (P.fy - 43) / 46, [HX, P.fy - 13, 1]); +} +// A move eases a pose to b; the step functions chain moves. +const go = (m, ph, dur, b) => Object.assign(m, { ph, t: 0, dur: max(0.15, dur), a: m.p.slice(), b }); +function tick(m, d) { + const u = sm(0, 1, (m.t += d) / m.dur); + for (let i = 0; i < 3; i++) m.p[i] = mix(m.a[i], m.b[i], u); + return u; +} +const heapH = (P) => min(13, sqrt(1.1 * P.hv)); +const heapTop = (P) => P.fy - heapH(P); +const matCol = (P) => STRATA[layer(30, P.fy + 2)]; +function puff(P, x, y, n, a, s = 1) { + for (let i = 0; i < n; i++) P.du.push([x + (rnd() - 0.5) * 6 * s, y - rnd() * 2 * s, (1.5 + rnd() * 2) * s, a, (rnd() - 0.5) * 3 * s, -(2 + rnd() * 3) * s]); + P.dust = min(1, P.dust + 0.05 * n * a); +} + +// Excavator: dig, lift, swing, drop it on the heap, swing back. +function eStep(P, E) { + const ph = E.ph, up = [7, -9, 0]; + if (ph === 0) go(E, 1, 1, [8, 1.5, 0]); + else if (ph === 1) { + const bx = round(P.ex + 8); + for (let i = -3; i <= 3; i++) if (bx + i <= PR) P.dig[bx + i] = max(P.dig[bx + i], 2.2 * cos(i * 0.39)); + (P.fy += 32 / 44), (P.ec = 1), puff(P, bx, P.fy, 1, 0.3); + if (P.fy >= floorY(P.k + 1)) (P.fy = floorY(++P.k)), P.k < FIN ? (P.wx = PL) : (P.mat = 0); + go(E, 2, 0.6, up); + } else if (ph === 2) go(E, 3, 1, [7, -9, PI]); + else if (ph === 3) go(E, 4, 0.6, [cl(P.ex - HX - 3, 6, 15), -heapH(P) * 0.7 - 3, PI]); + else if (ph === 4) (P.hv += 32), (P.ec = 0), puff(P, P.ex - E.p[0], heapTop(P), 2, 0.3), go(E, 5, 1.1, up); + else if (P.wx >= 0 || P.k >= FIN || P.hv > 150) go(E, 7, 0.5, up); + else if (ph === 5 && ++P.sc % 3 === 0) (P.ex0 = P.ex), (P.exT = EXP[(P.ei = (P.ei + 1) % 4)]), go(E, 6, 1.4, up); + else go(E, 0, 0.7, [12, -3, 0]); +} + +// Grab: down open, close, haul up, over the truck, open. +function cStep(P, C) { + const ph = C.ph, top = heapTop(P), up = [HX, 7, 0]; + if (ph === 0) P.hv > 14 ? go(C, 1, 0.3 + (top - 12 - C.p[1]) / 46, [HX, top - 5, 1]) : go(C, 0, 0.3, up); + else if (ph === 1) go(C, 2, 0.7, [HX, top - 4, 0]); + else if (ph === 2) { + const g = min(P.hv, 60); + (P.hv -= g), (P.full = g > 4), puff(P, HX, top, 2, 0.3), go(C, 3, 0.4 + (C.p[1] - 7) / 36, up); + } else if (ph < 5) P.tph === 0 && P.load < 3 ? go(C, 5, 1.2, [TT, 7, 0]) : go(C, 4, 0.3, up); + else if (ph === 5) { + if (P.full) { + (P.load++), (P.full = 0), puff(P, TT, 11, 3, 0.5), P.load > 2 && (P.tgo = 0.6); + for (let i = 0; i < 10; i++) P.pa.push([TT + (rnd() - 0.5) * 6, 13, 0, 4, 0.3, 2]); + } + go(C, 6, 0.6, [TT, 7, 1]); + } else go(C, ph === 6 ? 7 : 0, ph === 6 ? 1.1 : 0.2, up); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 8, 0.06]; + const rd = cl(dt || 0, 0, 0.1), d = rd * (cl(k[1], 0.2, 3) / 2), nk = round(cl(k[2], 8, 10)), h = (P.h = cl(k[0], 0, 1)), wt = (P.wt = cl(k[3], 0, 1)); + if (nk !== P.nk) lot(P, (P.nk = nk)); + (P.t += d), (P.rt += rd), (P.cx += 3.6 * d); + Object.assign(P, { dayF: 1 - sm(0.55, 0.92, h), sunI: (1 - sm(0.6, 0.85, h)) * (1 - 0.9 * sm(0.3, 0.85, wt)), slope: 0.12 + 0.95 * sm(0, 0.8, h), lampK: sm(0.45, 0.8, h) }); + // K1 tap: the warning lamp, then the blast at the face. + if (input.btnPressed && input.btnPressed[0] && P.bl < 0) P.bl = 0; + if (P.bl >= 0 && (P.bl += rd) >= 0.8 && !P.fired) { + P.fired = 1; + for (let i = 0; i < 26; i++) P.pa.push([40 + (rnd() - 0.5) * 10, P.fy - 1, (rnd() - 0.5) * 30, -10 - rnd() * 22, 1.6, 2]); + for (let i = PL; i <= PR; i++) P.dig[i] += 1.4 * exp(-(((i - 40) / 9) ** 2)); + puff(P, 40, P.fy, 9, 1.2, 2), (P.fy += 0.8), (P.hv += 30), (P.dust = 1); + } + if (P.bl > 1.5) (P.bl = -1), (P.fired = 0); + const { E, C } = P, u = tick(E, d); + if (E.ph === 6) { + const nx = mix(P.ex0, P.exT, u); + (P.trk += nx - P.ex), (P.ex = nx); + } + if (u >= 1) eStep(P, E); + if (tick(C, d) >= 1) cStep(P, C); + if (P.full && C.ph === 3 && rnd() < 4 * d) P.pa.push([C.p[0] + (rnd() - 0.5) * 5, C.p[1] + 7, 0, 2, 2, 2]); + // A truck leaves with three grabs and an empty one backs in. + if (P.tph === 0 && P.load > 2 && (P.tgo -= d) <= 0) (P.tph = 1), (P.tv = 0); + if (P.tph === 1 && (P.tx += (P.tv = min(18, P.tv + 10 * d)) * d) > 30) (P.tph = 2), (P.load = 0); + if (P.tph === 2 && (P.tx -= (1.5 + 0.8 * P.tx) * d) <= 0) (P.tx = 0), (P.tph = 0); + // The new strut is welded in from wall to wall. + if (P.wx >= 0) { + P.arc = rnd() < 0.85 ? 0.5 + 0.5 * rnd() : 0; + if (rnd() < 14 * d) P.pa.push([P.wx, strutY(P.k) + 2, (rnd() - 0.5) * 10, -2 - rnd() * 4, 0.6 + rnd() * 0.5, 0]); + if ((P.wx += 7 * d) > PR + 1) (P.wx = -1), (P.arc = 0); + } + // At the bottom: the blinding concrete, then on to the next lot. + if (P.k >= FIN && P.hv < 15 && (P.mat += d / 6) > 1.4 && P.sl < 0) (P.snap = 1), (P.sl = 0), lot(P, P.nk), skyline(P); + if (P.sl >= 0 && (P.sl += rd / 2.2) >= 1) P.sl = -1; + // Seepage drips below the water table. + const wtY = 78 - 34 * wt, e = exp(-0.25 * d); + if (P.fy > wtY + 6 && rnd() < 6 * wt * d) P.pa.push([PL + 1 + rnd() * 42, wtY + rnd() * (P.fy - wtY - 5), 0, 0, 3, 1]); + for (let i = PL; i <= PR; i++) P.dig[i] *= e; + P.dust = max(0.1, P.dust * e); + P.du = P.du.filter((p) => ((p[0] += p[4] * d), (p[1] += p[5] * d), (p[2] += 2.2 * d), (p[3] -= 0.22 * d) > 0)); + P.pa = P.pa.filter((p) => ((p[3] += 40 * d), (p[0] += p[2] * d), (p[1] += p[3] * d), (p[4] -= d) > 0 && p[1] < (p[0] >= PL && p[0] <= PR ? P.fy : H))); +} + +let F, LM; +const at = (x, y) => ((x = round(x)), (y = round(y)), x < 0 || x >= W || y < 0 || y >= H ? -1 : (y * W + x) * 3); +function sur(x, y, r, g, b, k = 1, o = at(x, y)) { + if (o >= 0) (F[o] = r * LM[o] * k), (F[o + 1] = g * LM[o + 1] * k), (F[o + 2] = b * LM[o + 2] * k); +} +const surc = (x, y, c, k = 1) => sur(x, y, c[0], c[1], c[2], k); +function put(x, y, c, a = 1, o = at(x, y)) { + if (o >= 0) for (let i = 0; i < 3; i++) F[o + i] = mix(F[o + i], c[i], a); +} +function add(x, y, c, k, o = at(x, y)) { + if (o >= 0) for (let i = 0; i < 3; i++) F[o + i] += c[i] * k; +} +function line(x0, y0, x1, y1, c, k = 1) { + const n = max(abs(x1 - x0), abs(y1 - y0)) * 1.3 + 1; + for (let i = 0; i <= n; i++) surc(mix(x0, x1, i / n), mix(y0, y1, i / n), c, k); +} +const cloudA = (P, x, y) => cl((0.6 * vn((x + P.cx) * 0.07, y * 0.18) + 0.4 * vn((x + P.cx) * 0.19, y * 0.4 + 5) - 0.6 + 0.3 * P.wt) * 5, 0, 1); + +function lamp(x0, y0, r2, k, c) { + const R = Math.ceil(sqrt(r2 * 4.6)), gx = []; + for (let x = max(0, x0 - R); x <= min(W - 1, x0 + R); x++) gx.push(x, k * exp(-((x - x0) ** 2) / r2)); + for (let y = max(0, y0 - R); y <= min(H - 1, y0 + R); y++) + for (let i = 0, g = exp(-((y - y0) ** 2) / r2); i < gx.length; i += 2) for (let j = 0, o = (y * W + gx[i]) * 3; j < 3; j++) LM[o + j] += c[j] * gx[i + 1] * g; +} + +// Daylight with cloud shadows, the shaft of sun, the lamps. +function light(P, K) { + const { sunI: sI, slope: s, rs, sh, shf, k: pk, lampK: nl } = P, ks = min(pk, FIN - 1); + for (let i = 0; i < 200; i++) sh[i] = 1 - 0.85 * cloudA(P, i - 96, 7); + rs.fill(1); + for (let j = 1, y; j <= ks; j++) (y = strutY(j)), (rs[y + 2] = rs[y + 4] = 0.6), (rs[y + 3] = 0.4); + for (let i = 0; i < N; i++) { + const x = i & 63, y = i >> 6, o = i * 3, x0 = x - s * (y - GL), out = y < GL + 2; + const a = out ? 1 : (0.62 - 0.003 * (y - GL)) * (x > OR ? 0.75 : 1); + const l = out ? 0.5 * sI * sh[x + 80] : sI > 0 && x0 > PL - 1.5 && x0 < OR + 1.5 ? sI * sm(PL - 1.5, PL + 1, x0) * (1 - sm(OR - 1, OR + 1.5, x0)) * sh[floor(x0) + 80] * rs[y] * (1 - 0.0025 * (y - GL)) : 0; + shf[i] = out ? 0 : l; + for (let c = 0; c < 3; c++) LM[o + c] = K[9 + c] * a + K[6 + c] * l * 0.95; + } + // Floods on the lowest strut; sodium lamps after dark. + lamp(17, strutY(ks) + 2, 260, 0.7, FLOOD), lamp(47, strutY(ks) + 2, 260, 0.7, FLOOD); + if (nl > 0) for (let j = 1; j <= ks; j++) lamp(j & 1 ? 12 : 51, strutY(j) + 2, 60, 1.3 * nl, [1, 0.62, 0.24]); + if (P.arc > 0) lamp(round(P.wx), strutY(pk) + 1, 70, 1.4 * P.arc, [0.7, 0.85, 1]); + if (P.bl > 0.8) lamp(40, round(P.fy) - 3, 260, 1.8 * (1 - sm(0.8, 1.2, P.bl)), [1, 0.8, 0.5]); + const tx = P.C.p[0]; + if (nl > 0) + for (let y = 8; y < min(H, P.fy + 2); y++) + for (let hw = 1.5 + 0.16 * (y - 7), x = max(0, floor(tx - hw)); x <= min(W - 1, tx + hw); x++) + for (let c = 0, e = ((0.9 * nl) / (1 + (y - 7) / 70)) * (1 - abs(x - tx) / hw); c < 3; c++) LM[(y * W + x) * 3 + c] += e * (c > 1 ? 0.92 : 1); +} + +function sky(P, K) { + const ov = 0.75 * sm(0.2, 1, P.wt), day = P.dayF, dk = 0.3 + 0.7 * day, cc = [0, 1, 2].map((c) => mix(mix(K[3 + c], 255, 0.55) * dk, 130 * dk, ov)); + for (let i = 0; i < GL * W; i++) { + const x = i & 63, y = i >> 6, u = y / (GL - 1), a = cloudA(P, x, y) * (0.9 - 0.4 * u); + for (let c = 0; c < 3; c++) F[i * 3 + c] = mix(mix(mix(K[c], K[3 + c], u * u), 120 * (0.25 + 0.75 * day), ov), cc[c], a); + } + // The street's buildings, the two beside the pit tall. + for (const [x0, x1, top, v, side] of P.bd) + for (let y = top; y < GL; y++) + for (let x = x0; x <= x1; x++) { + const win = (x - x0) % 3 === 1 && (y - top) % 3 === 1 && x < x1, lit = hash(x * 7, y + floor(v * 99)) < 0.45 && day < 0.7; + for (let c = 0, b; c < 3; c++) (b = K[12 + c] * (0.78 + 0.3 * v) * (x === x0 ? 0.8 : 1)), (b = win ? (lit ? WIN[c] * (1 - day) : b * 0.55 + 18 * c * day) : b), (F[(y * W + x) * 3 + c] = side ? b : mix(b, K[3 + c], 0.3)); + } +} + +function ground(P) { + const { so, bw, srf, wt, st, fy } = P, wtY = 78 - 34 * wt, sec = 0.42 + 0.52 * P.dayF, mc = matCol(P), hh = heapH(P), kc = STEEL.map((v) => v * 0.5 * sec); + for (let x = PL; x <= PR; x++) srf[x] = fy + P.dig[x] + 0.6 * (hash(x, 7) - 0.5); + for (let i = GL * W; i < N; i++) { + // Pavement, deck and caps; far wall and heap; the ground in section. + const x = i & 63, y = i >> 6, o = i * 3, pit = x >= PL && x <= PR; + let A = so, a = o, L = 0, k = sec; + if (y < GL + 2 && !(pit && x <= OR)) (A = (x > 6 && x < PL) || (x > PR && x < 57) ? CAP : x > OR && x < 56 ? DECK : ROAD), (a = 0), (L = 1), (k = y === GL ? 1.18 : A === DECK && (x + y) & 1 ? 0.86 : 1); + else if (pit && y < srf[x]) (L = 1), y >= srf[x] - max(0, hh - abs(x - HX) * 1.1) ? ((A = mc), (a = 0), (k = 0.85 + 0.3 * hash(x, y))) : ((A = bw), (k = y > wtY ? 1 - 0.5 * wt * st[i] : 1)); + else if (pit && y < srf[x] + 1) (L = 1), (k = 1.15); + else if (!pit && y > wtY + 4) k *= 1 - 0.12 * wt; + for (let c = 0; c < 3; c++) F[o + c] = A[a + c] * k * (L ? LM[o + c] : 1); + } + // Walls and king posts down to their toes; the deck's beams. + for (let y = GL + 2; y < 126; y++) { + for (const [x, c] of WALLS) for (let i = 0; i < 3; i++) F[(y * W + x) * 3 + i] = c[i] * sec; + for (const x of KP) for (let i = 0; i < 2; i++) y < srf[x] ? sur(x + i, y, ...STEEL, 0.62 + 0.23 * i) : put(x + i, y, kc); + } + for (let x = OR + 1; x <= PR; x++) sur(x, GL + 2, 70, 74, 84), sur(x, GL + 3, 90, 94, 104, x % 6 ? 0.7 : 1.2); + // Struts with wales and jacks. + for (let j = 1; j <= min(P.k, FIN - 1); j++) { + const y = strutY(j), end = j === P.k && P.wx >= 0 ? P.wx : PR; + for (let x = PL; x <= end; x++) sur(x, y, ...STEEL, x % 11 === 3 ? 0.8 : 1.25), sur(x, y + 1, ...STEEL, 0.75); + for (let i = -1; i <= 2; i++) { + for (const x of [PL, PL + 1, PR - 1, PR]) sur(x, y + i, 80, 84, 96); + if (end >= PR) for (let x = 46; x <= 48; x++) surc(x, y + i, YEL, i < 0 || i > 1 ? 0.7 : 1); + } + } + // Blinding concrete on the last floor. + const me = PL + min(1, P.mat) * (P.ex - PL - 8); + if (P.mat > 0) for (let x = PL; x <= me; x++) for (let j = 0, o; j < 3; j++) (o = at(x, srf[x] - j)), put(x, srf[x] - j, [0, 1, 2].map((c) => 200 * max(0.55, LM[o + c]) * (j ? 0.9 : 1.1))); + if (P.mat > 0 && P.mat < 1) line(30, GL + 1, me, fy - 3, [96, 96, 104]); + // The pump's pool, and its hose up the wall to the tank. + for (let x = 45; x <= PR; x++) + for (let y = round(fy - 0.4 - 2.6 * wt), g; y < srf[x]; y++) (g = 0.5 + 0.5 * sin(x * 1.3 + P.t * 5 + y * 2)), sur(x, y, 60 + 60 * g, 96 + 60 * g, 132 + 50 * g); + for (let j = 1; j <= 2; j++) sur(51, fy - j, ...HOSE), sur(52, fy - j, ...HOSE); + for (let y = fy - 2; y > GL - 2; y--) surc(PR, y, HOSE); + for (let x = PR; x < 60; x++) surc(x, GL - 2, HOSE); + for (let x = 59; x <= 62; x++) for (let y = 16; y < GL; y++) sur(x, y, 120, 140, 160, y === 16 ? 1.2 : 0.8); + if (wt > 0.03) for (let j = 0; j < 2; j++) put(59 + j, GL - 2 + j, RAIN, min(1, wt * 6) * (0.5 + 0.5 * sin(P.t * 20 + j))); +} + +function excavator(P) { + const ex = P.ex, gy = round(P.fy), [bu, bv, ph] = P.E.p, f = cos(ph), X = (u) => ex + u * f, hw = 4.5 * abs(f) + 3.2 * abs(sin(ph)), nl = P.lampK; + for (let i = -6; i <= 6; i++) for (let j = 1; j <= 3; j++) (abs(i) < 6 || j === 2) && sur(ex + i, gy - j, ...(j !== 2 && (i + floor(P.trk)) & 1 ? [92, 92, 96] : [46, 46, 50])); + for (let i = -3; i <= 3; i++) sur(ex + i, gy - 4, 52, 50, 48); + for (let j = 5; j <= 7; j++) for (let x = round(ex - hw); x <= round(ex + hw); x++) surc(x, gy - j, YEL, j === 5 ? 0.7 : (x - ex) * f < -2.5 ? 0.82 : 1); + const ca = round(min(X(0.5), X(3.5))), cb = max(ca + 1, round(max(X(0.5), X(3.5)))); + for (let j = 8; j <= 11; j++) for (let x = ca; x <= cb; x++) j === 11 || x === (f >= 0 ? ca : cb) ? surc(x, gy - j, YEL) : (sur(x, gy - j, 110, 150, 180, 0.9), add(x, gy - j, WIN, 0.5 * nl)); + // Boom and arm reach the bucket by two-link IK. + const [pu, pv, L1, L2] = [3, -6, 9, 8.5]; + let tu = bu - pu, tv = bv - pv; + const dd = hypot(tu, tv) || 1, dc = cl(dd, 2, L1 + L2 - 0.05); + (tu *= dc / dd), (tv *= dc / dd); + const t1 = atan2(tv, tu) - acos(cl((L1 * L1 + dc * dc - L2 * L2) / (2 * L1 * dc), -1, 1)), eu = pu + L1 * cos(t1), ev = pv + L1 * sin(t1), bx = X(pu + tu), by = gy + pv + tv; + line(X(pu), gy + pv, X(eu), gy + ev, YEL), line(X(pu), gy + pv + 1, X(eu), gy + ev + 1, YEL, 0.75); + line(X(4), gy - 5, X(pu + (eu - pu) * 0.55), gy + pv + (ev - pv) * 0.55 + 1, [150, 150, 156]); + line(X(eu), gy + ev, bx, by, YEL, 0.9); + for (let i = 0; i <= 2; i++) sur(bx - i * f, by, 74, 74, 80), sur(bx - i * f, by + 1, 74, 74, 80, 0.8), P.ec && surc(bx - i * f, by - 1, matCol(P), 1.1); + add(X(4.5), gy - 9, FLOOD, 255 * nl); +} + +// A worker in hi-vis, feet on row y; a headlamp after dusk. +function man(P, x, y, arm) { + sur(x, y, 40, 48, 90), sur(x, y - 1, 255, 128, 20), sur(x, y - 2, 255, 255, 240), arm && sur(x + 1, y - 2 - (arm > 0.5), 255, 128, 20); + add(x, y - 2, FLOOD, 200 * P.lampK); +} + +function grab(P) { + const [tx, by, op] = P.C.p, X = round(tx), Y = round(by), HW = [3, 3.4, 3.4, 3, 2]; + for (let y = 7; y < Y; y++) sur(X - 1, y, 200, 200, 210), sur(X + 1, y, 200, 200, 210); + for (let i = -1; i <= 1; i++) surc(X + i, Y, GAN, 1.2), surc(X + i, Y + 1, GAN, 0.85); + for (let j = 0; j < 5; j++) + for (let i = -6, a; i <= 6; i++) (a = abs(i)) >= op * (0.4 + 0.9 * j) - 0.01 && a <= HW[j] + op * 0.9 * j && (j === 4 || (op < 0.3 && !i) ? sur(X + i, Y + 2 + j, 96, 100, 110) : surc(X + i, Y + 2 + j, GRAB, j ? 1 : 1.2)); + if (P.full && op < 0.5) for (let i = -2; i <= 2; i++) surc(X + i, Y + 2, matCol(P), 1.2); + for (let i = -3; i <= 3; i++) sur(X + i, 5, 70, 74, 84), surc(X + i, 6, GAN, abs(i) < 2 ? 0.6 : 1); +} + +function gantry(P) { + for (let y = 2; y < GL; y++) for (const x of [4, 58]) surc(x, y, GAN), (y + (x >> 2)) % 3 && surc(x + 1, y, GAN, 0.7); + for (let x = 3; x <= 60; x++) surc(x, 2, GAN, 1.1), surc(x, 4, GAN), (x + 1) % 3 || surc(x, 3, GAN, 0.8); + put(32, 1, P.rt % 1.4 < 0.7 ? [255, 40, 30] : [90, 16, 14]); + // The railing, and the signalman at the edge. + for (let x = 8; x <= OR; x++) sur(x, 17, ...((x >> 1) & 1 ? [230, 60, 50] : [236, 236, 236])), x % 6 === 2 && (sur(x, 18, 200, 200, 200), sur(x, 19, 200, 200, 200)); + man(P, 35, GL - 1, P.C.ph < 3 ? (P.rt * 2) % 1 : 0); +} + +function truck(P) { + const o = round(P.tx), mc = matCol(P), hL = [0, 1.6, 2.7, 3.7][min(3, P.load)]; + if (o > 30) return; + for (let x = 37; x <= 50; x++) + for (let y = 9; y <= 16; y++) + y > 12 ? sur(x + o, y, 196, 56, 44, y === 13 ? 1.25 : (x - 37) % 4 ? 1 : 0.75) : y > 12 - hL * (1 - ((x - 43.5) / 7) ** 2) && surc(x + o, y, mc, 0.85 + 0.3 * hash(x, y)); + for (let x = 51; x <= 56; x++) for (let y = 10; y <= 16; y++) (y > 10 || x > 51) && (y < 14 && y > 10 && x > 52 ? sur(x + o, y, 60, 90, 120) : sur(x + o, y, 50, 96, 200)); + for (let x = 37; x <= 56; x++) sur(x + o, 17, 50, 50, 54); + for (const x of [39, 43, 53]) sur(x + o, 18, 30, 30, 34), sur(x + o + 1, 18, 30, 30, 34), sur(x + o, 19, 120, 120, 126), sur(x + o + 1, 19, 30, 30, 34); + add(57 + o, 15, FLOOD, 255 * P.lampK), add(36 + o, 16, [255, 30, 20], P.lampK); +} + +export function draw(display, params) { + const P = params.s; + if (!P || P.h === undefined) return; + [F, LM] = [P.f, P.lm]; + if (P.snap) P.sn.set(F), (P.snap = 0); + const hh = P.h * 3, i0 = min(2, floor(hh)), K = KEYS[i0].map((v, c) => mix(v, KEYS[i0 + 1][c], hh - i0)), mc = matCol(P), fy = round(P.fy); + light(P, K), sky(P, K), gantry(P), ground(P), excavator(P); + man(P, 12, round(P.srf[12] - max(0, heapH(P) - 9.9)) - 1, 0); + if (P.wx >= 0) man(P, round(P.wx) - 1, fy - 1, 1); + truck(P), grab(P); + // Sparks, drips, spoil and dust. + for (const [x, y, , , l, k] of P.pa) k ? (k > 1 ? surc(x, y, mc, 1.3) : put(x, y, [170, 210, 255], 0.8)) : put(x, y, [255, 200, 90], min(1, l * 2)); + for (const [px, py, r, a] of P.du) { + const o0 = at(cl(px, 0, 63), cl(py, 0, 127)), R = Math.ceil(r), pc = mc.map((v, c) => mix(v, 210, 0.5) * min(1.3, 0.3 + LM[o0 + c])); + for (let y = round(py) - R; y <= py + R; y++) + for (let x = round(px) - R, q; x <= px + R; x++) (q = 1 - ((x - px) ** 2 + (y - py) ** 2) / (r * r)) > 0 && put(x, y, pc, min(1, a * q * q * 0.9)); + } + // Haze in the shaft, thicker with dust. + for (let i = 0, hz = 8 + 30 * P.dust; i < N; i++) if (P.shf[i] > 0.01) for (let c = 0; c < 3; c++) F[i * 3 + c] += K[6 + c] * P.shf[i] * hz; + if (P.arc > 0) add(round(P.wx), strutY(P.k) + 1, FLOOD, 500); + if (P.bl >= 0 && P.bl < 0.8 && P.bl % 0.3 < 0.15) put(34, GL - 4, [255, 170, 30]), add(34, GL - 3, [255, 140, 20], 0.6); + // Rain past halfway, not under the deck. + for (let i = 0, rain = sm(0.45, 1, P.wt); i < 160 * rain; i++) { + const y = ((hash(i, 2) * 150 + P.t * 70) % 150) - 10, x = (hash(i, 1) * 80 + y * 0.25) % 64; + if (x <= OR + 0.5 || x >= PR + 1 || y <= GL + 1) for (let j = 0; j < 3; j++) put(x - j * 0.25, y - j, RAIN, 0.6 * (1 - j / 3) * (0.4 + 0.6 * P.dayF)); + } + const off = P.sl >= 0 ? round(64 * sm(0, 1, P.sl)) : 0; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const xs = x + off, B = xs < W && off ? P.sn : F, o = (y * W + (xs % W)) * 3; + display.setPixel(x, y, min(255, B[o]) | 0, min(255, B[o + 1]) | 0, min(255, B[o + 2]) | 0); + } +} From d37f3490d1641639be3c7bd89faaffc6140a9969 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 03:49:13 +0000 Subject: [PATCH 10/17] population-pyramid: add a cohort-model age pyramid that runs 240 years from triangle to urn Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DbfsCYkpNNJ5diTKTtPZEu --- .../patterns/population-pyramid.js | 246 ++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 incoming/pattern-study/patterns/population-pyramid.js diff --git a/incoming/pattern-study/patterns/population-pyramid.js b/incoming/pattern-study/patterns/population-pyramid.js new file mode 100644 index 000000000..32e5ccf98 --- /dev/null +++ b/incoming/pattern-study/patterns/population-pyramid.js @@ -0,0 +1,246 @@ +// ===== Patternflow pattern ===== +// Title: Population Pyramid +// Family: data +// Idea: The population pyramid of an imagined country runs through 240 years one row per year of age, men left and women right, each ten years of births one colour band climbing as it ages, from the sharp triangle of many births and many deaths through the baby boom to a top-heavy urn, and then starts again. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 25, after the animated age pyramids of national statistics offices and the cohort-component model demographers project with +// Round: 25 +// =============================== +// @matrix 64x128 +// @knobs Palette=0..1, Speed=0.25..6, Births=-1..1, Migration=-1..1 +// +// Knob 1: Palette. Turns the colours of the birth decades round the wheel; the decades step bright, middle and dark in threes so neighbouring bands part. +// Knob 2: Speed. At 2 three years go by each second and the 240 years from 1820 to 2060 take 80 seconds; at 6 the bands race up the pyramid. +// Knob 3: Births. Scales the fertility the story runs on, from 0.63 times at the left to 1.6 times at the right. In the middle the country starts near six children a woman and ends near one; turned down the base pinches in, turned up it spreads. +// Knob 4: Migration. Within a tenth of the middle nobody moves. Turned up, young adults arrive each year around age 26 and their part of each bar is drawn paler at its outer end as they grow old in the country; turned down, they leave, and a dotted ghost past the bar's end shows where it would reach had they stayed. +// Buttons: K1 tap makes this year a war or an epidemic, in turn: births fall by about half, and either young men or the very young and the old die, so a notch forms and climbs. + +const [W, H] = [64, 128]; +const N = W * H; +const { min, max, floor, exp, abs } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const mix = (a, b, t) => a + (b - a) * t; +const rnd = () => Math.random(); + +// Ages 0 to 105 have a row each; the top row holds everyone older. +const NA = 107; +// Row of age 0, first bar column for men (going left) and for women (going right), the widest bar. +const [Y0, MX, FX, BAR] = [121, 30, 33, 23]; +const [START, END] = [1820, 2060]; + +// Mortality level: 0 where a quarter of babies die in their first year and life lasts 30 to 33 years on average, 1 where it lasts 79 to 83. +const LK = [[1820, 0], [1850, 0.02], [1880, 0.14], [1910, 0.34], [1950, 0.66], [1975, 0.84], [2000, 0.94], [2060, 1]]; +// Children a woman, before Births scales them. +const TK = [[1820, 6], [1865, 6], [1885, 5.5], [1905, 4.3], [1926, 2.9], [1933, 2.2], [1940, 2.3], [1947, 3.1], [1955, 3.5], [1962, 3.3], [1968, 2.5], [1977, 1.8], [1992, 1.55], [2008, 1.25], [2035, 1.05], [2060, 1]]; +// Two wars, the influenza after the first, and a cholera year in the opening decade. +const EV = [[1914, 1918, 0], [1940, 1945, 0], [1919, 1919, 1], [1823, 1823, 1]]; + +const key = (K, y) => { + if (y <= K[0][0]) return K[0][1]; + for (let i = 1; i < K.length; i++) + if (y <= K[i][0]) { + const t = (y - K[i - 1][0]) / (K[i][0] - K[i - 1][0]); + return mix(K[i - 1][1], K[i][1], t * t * (3 - 2 * t)); + } + return K[K.length - 1][1]; +}; + +// Where in a woman's life the children come: a hump peaking at 28. +const PHI = new Float64Array(NA); +{ + let s = 0; + for (let a = 15; a < 50; a++) s += PHI[a] = (a - 14) ** 2.5 * exp(-(a - 14) / 5.5); + for (let a = 15; a < 50; a++) PHI[a] /= s; +} +// Where migrants are in life: around 26. +const MIG = new Float64Array(NA).map((_, a) => exp(-(((a - 26) / 8) ** 2))); + +// Yearly hazard of dying at age a. kind: -1 a plain year, 0 war, 1 epidemic, 2 famine. +function hazard(a, L, male, kind) { + let h = a === 0 ? mix(0.27, 0.0035, L) : a < 15 ? mix(0.04, 0.0003, L) * exp(-(a - 1) / 2.2) : 0; + h += mix(0.008, 0.0003, L) * (male && a > 14 && a < 40 ? 1.9 : 1); + h += 0.1 * exp(0.1 * (a - mix(64, 86, L) - (male ? 0 : mix(2, 4, L)))); + if (kind === 0 && male && a > 17 && a < 43) h += 0.09; + if (kind === 1) h += 0.015 + (a > 17 && a < 41 ? 0.025 : 0) + (a < 5 ? 0.08 : 0) + (a > 64 ? 0.06 : 0); + if (kind === 2) h += 0.02 + (a < 5 ? 0.07 : 0) + (a > 59 ? 0.04 : 0); + return h; +} + +// A state is six rows of ages: men and women living here, the part of them who came as migrants, and those who left. +const SV = new Float64Array(NA); +// One year of the cohort-component model: the dead leave, everyone is a year older, the year's births come in, migrants come or go. +function step(src, dst, y, fk, mig, kind, push) { + const L = key(LK, y); + let b = 0; + for (let a = 15; a < 50; a++) b += src[1][a] * PHI[a]; + b *= key(TK, y) * fk * [1, 0.62, 0.75, 0.85][kind + 1] * (push ? 0.7 : 1); + for (let s = 0; s < 2; s++) { + for (let a = 0; a < NA; a++) SV[a] = exp(-hazard(a, L, !s, kind) * (a ? 1 : 0.5)); + for (let k = s; k < 6; k += 2) { + const S = src[k], D = dst[k]; + D[NA - 1] = S[NA - 1] * SV[NA - 1] + S[NA - 2] * SV[NA - 2]; + for (let a = NA - 2; a > 0; a--) D[a] = S[a - 1] * SV[a - 1]; + D[0] = k < 2 ? b * (s ? 0.488 : 0.512) * SV[0] : 0; + } + const [D, I, E] = [dst[s], dst[s + 2], dst[s + 4]]; + for (let a = 12; a < 60; a++) + if (mig > 0) { + const n = mig * 0.0006 * MIG[a] * (s ? 0.9 : 1.1); + (D[a] += n), (I[a] += n); + } else if (mig < 0) { + const f = 1 - exp(mig * 0.065 * MIG[a]); + (E[a] += D[a] * f), (D[a] *= 1 - f), (I[a] *= 1 - f); + } + } + // Keep the people living here at one: the bars are shares of them. + let t = 0; + for (let a = 0; a < NA; a++) t += dst[0][a] + dst[1][a]; + for (const A of dst) for (let a = 0; a < NA; a++) A[a] /= t; +} + +// A crisis by the script, or by chance in the hungry decades. +const kindOf = (y) => { + for (const [a, b, k] of EV) if (y >= a && y <= b) return k; + return y < 1880 && rnd() < 0.07 ? 2 : -1; +}; + +// 3x5 digits, then M and F. +const GL = [0x7b6f, 0x2c97, 0x73e7, 0x73cf, 0x5bc9, 0x79cf, 0x79ef, 0x7249, 0x7bef, 0x7bcf, 0x5fed, 0x79a4]; + +export function setup(params) { + const S = () => Array.from({ length: 6 }, () => new Float64Array(NA)); + params.s = { cur: S(), nxt: S(), tmp: S(), y: START, u: 0, scl: 0, f: new Float32Array(N * 3), ready: 0, war: 0, fade: 0, kinds: new Int8Array(END - START + 2) }; +} + +// Run the opening regime for 150 years so it reaches its own shape and the knobs show from the first frame. +function warm(P, fk, mig) { + for (let s = 0; s < 6; s++) for (let a = 0; a < NA; a++) P.cur[s][a] = s < 2 ? exp(-0.04 * a) : 0; + for (let i = 0; i < 150; i++) { + step(P.cur, P.tmp, START - 150 + i, fk, mig, rnd() < 0.07 ? 2 : -1, 0); + [P.cur, P.tmp] = [P.tmp, P.cur]; + } + P.y = START; + P.u = 0; + P.kinds.fill(-1); + next(P, fk, mig, 0); + P.scl = 0; +} + +// Work out the year being lived now, from the state at its first day. +function next(P, fk, mig, push) { + const k = push ? push - 1 : kindOf(P.y); + P.kinds[P.y - START] = k; + step(P.cur, P.nxt, P.y, fk, mig, k, push); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + const fk = exp(0.47 * cl(k[2], -1, 1)), k4 = cl(k[3], -1, 1), mig = Math.sign(k4) * max(0, abs(k4) - 0.1) / 0.9, spd = 1.5 * cl(k[1], 0.25, 6); + P.hue = k[0]; + if (!P.ready) (P.ready = 1), warm(P, fk, mig); + // K1 tap: this year is a war, then an epidemic, by turns. + if (input.btnPressed && input.btnPressed[0] && P.y < END) next(P, fk, mig, (P.war ^= 1) ? 1 : 2); + if (P.fade > 0) { + // The story is over: hold the urn a moment, dim, start again from the old triangle, light up. + P.fade += d; + if (P.fade > 2 && P.y >= END) warm(P, fk, mig); + if (P.fade > 2.8) P.fade = 0; + } else { + P.u += d * spd; + for (let i = 0; i < 6 && P.u >= 1; i++) { + P.u -= 1; + [P.cur, P.nxt] = [P.nxt, P.cur]; + if (++P.y >= END) { + (P.u = 0), (P.fade = 0.001); + break; + } + next(P, fk, mig, 0); + } + } + // The widest bar sets the scale, followed gently so a single year does not jolt it. + let m = 1e-9; + for (let a = 0; a < NA; a++) m = max(m, P.cur[0][a], P.cur[1][a], P.nxt[0][a], P.nxt[1][a]); + const want = BAR / m; + P.scl = P.scl ? P.scl * exp(cl(Math.log(want / P.scl), -1, 1) * min(1, d * 1.5)) : want; + P.scl = min(P.scl, (BAR + 0.5) / m); +} + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h), f = h - i, p = v * (1 - s), q = v * (1 - s * f), t = v * (1 - s * (1 - f)); + return [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i % 6].map((c) => c * 255); +} + +let FB; +function put(x, y, c, a = 1) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + FB[o] = c[0] * a; + FB[o + 1] = c[1] * a; + FB[o + 2] = c[2] * a; +} +function glyph(g, x, y, c, s = 1) { + for (let r = 0; r < 5; r++) + for (let q = 0; q < 3; q++) + if ((GL[g] >> (14 - r * 3 - q)) & 1) for (let i = 0; i < s; i++) for (let j = 0; j < s; j++) put(x + q * s + j, y + r * s + i, c); +} +function num(n, x, y, c, w, s = 1) { + const t = String(n).padStart(w, "0"); + for (let i = 0; i < t.length; i++) glyph(+t[i], x + i * 4 * s, y, c, s); +} + +// A birth decade's colours: the band, its migrants, its leavers and its label. Decades step bright, middle, dark; +// the darker two keep off orange and yellow, which go brown and olive when dim. +const LV = [1, 0.66, 0.4]; +function decade(hue, c) { + const l = ((c % 3) + 3) % 3, h0 = hue + c * 0.137, h = l ? 0.2 + (h0 - floor(h0)) * 0.82 : h0; + return [hsv(h, 0.8, LV[l]), hsv(h, 0.3, max(0.8, LV[l])), hsv(h, 0.6, 0.55), hsv(h, 0.55, 1)]; +} + +export function draw(display, params) { + const P = params.s; + if (!P || !P.ready) return; + FB = P.f; + FB.fill(0); + const u = P.u, hue = 0.55 + (P.hue || 0), vis = P.fade > 1.2 ? min(1, abs(P.fade - 2) / 0.8) : 1; + // The age-0 row holds last year's births; the bands move up a row half way through the year. + const yb = P.y - 1 + (u >= 0.5 ? 1 : 0); + const grid = [38, 38, 38], lab = [110, 110, 120]; + // Faint lines every ten years of age, and the ages up the left side. + for (let a = 0; a <= 100; a += 10) { + const y = Y0 - a; + for (let x = 1; x < W; x += 2) put(x, y, grid); + if (a % 20 === 0) num(a, 0, y - 2, lab, 1); + } + // The bars, one row per year of age, mid-year between the two states. + const C = P.cur, X = P.nxt, at = (s, a) => mix(C[s][a], X[s][a], u) * P.scl, memo = {}, dec = (c) => memo[c] || (memo[c] = decade(hue, c)); + for (let a = 0; a < NA; a++) { + const y = Y0 - a, [c, pale, ghost] = dec(floor((yb - a) / 10)); + for (let s = 0; s < 2; s++) { + const len = min(BAR, at(s, a)), imm = min(len, at(s + 2, a)), out = min(BAR + 1 - len, at(s + 4, a)); + const px = (i, q, f = 1) => put(s ? FX + i : MX - i, y, q, f); + const n = floor(len), fr = len - n; + for (let i = 0; i < n; i++) px(i, i + 0.5 < len - imm ? c : pale); + if (fr > 0.05) px(n, imm > 0.5 ? pale : c, fr); + // Those who left, dotted past the end of the bar. + for (let i = n + 1; i < len + out; i++) if ((i + a) & 1) px(i, ghost); + } + } + // The birth decade of each band, written beside it on the right in its own colour. + for (let c = floor((yb - 106) / 10); c <= floor(yb / 10); c++) { + const top = min(NA - 1, yb - c * 10), bot = max(0, yb - c * 10 - 9); + if (top - bot < 4) continue; + // A decade with hardly anyone left keeps a dim label. + let w = 0; + for (let a = bot; a <= top; a++) w = max(w, at(0, a), at(1, a)); + num((c * 10) % 100, 57, Math.round(Y0 - cl(yb - c * 10 - 4.5, bot + 2, top - 2)) - 2, dec(c)[3].map((q) => q * cl(w / 3, 0.3, 1)), 2); + } + // The year being lived, in the colour of the decade being born, red in a year of war or epidemic. + const kind = P.kinds[P.y - START] ?? -1, ycol = kind === 0 || kind === 1 ? [255, 60, 40] : decade(hue, floor(P.y / 10))[3]; + num(P.y, 17, 2, ycol, 4, 2); + glyph(10, 18, 123, lab), glyph(11, 43, 123, lab); + for (let i = 0, o = 0; i < N; i++, o += 3) display.setPixel(i & 63, i >> 6, min(255, FB[o] * vis) | 0, min(255, FB[o + 1] * vis) | 0, min(255, FB[o + 2] * vis) | 0); +} From c5e40c383b44707f4fff2f08e8937b21cee8bf2d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 03:54:03 +0000 Subject: [PATCH 11/17] population-pyramid: open in 1995 so the old reach the top third from the first seconds Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DbfsCYkpNNJ5diTKTtPZEu --- .../patterns/population-pyramid.js | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/incoming/pattern-study/patterns/population-pyramid.js b/incoming/pattern-study/patterns/population-pyramid.js index 32e5ccf98..78264a406 100644 --- a/incoming/pattern-study/patterns/population-pyramid.js +++ b/incoming/pattern-study/patterns/population-pyramid.js @@ -12,7 +12,7 @@ // @knobs Palette=0..1, Speed=0.25..6, Births=-1..1, Migration=-1..1 // // Knob 1: Palette. Turns the colours of the birth decades round the wheel; the decades step bright, middle and dark in threes so neighbouring bands part. -// Knob 2: Speed. At 2 three years go by each second and the 240 years from 1820 to 2060 take 80 seconds; at 6 the bands race up the pyramid. +// Knob 2: Speed. At 2 three years go by each second: it opens in 1995, reaches 2060 in 22 seconds, and the whole 240 years from 1820 take 80 seconds after that; at 6 the bands race up the pyramid. // Knob 3: Births. Scales the fertility the story runs on, from 0.63 times at the left to 1.6 times at the right. In the middle the country starts near six children a woman and ends near one; turned down the base pinches in, turned up it spreads. // Knob 4: Migration. Within a tenth of the middle nobody moves. Turned up, young adults arrive each year around age 26 and their part of each bar is drawn paler at its outer end as they grow old in the country; turned down, they leave, and a dotted ghost past the bar's end shows where it would reach had they stayed. // Buttons: K1 tap makes this year a war or an epidemic, in turn: births fall by about half, and either young men or the very young and the old die, so a notch forms and climbs. @@ -28,7 +28,9 @@ const rnd = () => Math.random(); const NA = 107; // Row of age 0, first bar column for men (going left) and for women (going right), the widest bar. const [Y0, MX, FX, BAR] = [121, 30, 33, 23]; -const [START, END] = [1820, 2060]; +// The story runs from 1820 to 2060 and goes back to 1820. The first start opens in OPEN, the boom in middle age and +// the old reaching the top of the panel, with the years before it run unseen. +const [START, END, OPEN] = [1820, 2060, 1995]; // Mortality level: 0 where a quarter of babies die in their first year and life lasts 30 to 33 years on average, 1 where it lasts 79 to 83. const LK = [[1820, 0], [1850, 0.02], [1880, 0.14], [1910, 0.34], [1950, 0.66], [1975, 0.84], [2000, 0.94], [2060, 1]]; @@ -87,10 +89,10 @@ function step(src, dst, y, fk, mig, kind, push) { const [D, I, E] = [dst[s], dst[s + 2], dst[s + 4]]; for (let a = 12; a < 60; a++) if (mig > 0) { - const n = mig * 0.0006 * MIG[a] * (s ? 0.9 : 1.1); + const n = mig * 0.00045 * MIG[a] * (s ? 0.9 : 1.1); (D[a] += n), (I[a] += n); } else if (mig < 0) { - const f = 1 - exp(mig * 0.065 * MIG[a]); + const f = 1 - exp(mig * 0.035 * MIG[a]); (E[a] += D[a] * f), (D[a] *= 1 - f), (I[a] *= 1 - f); } } @@ -114,17 +116,19 @@ export function setup(params) { params.s = { cur: S(), nxt: S(), tmp: S(), y: START, u: 0, scl: 0, f: new Float32Array(N * 3), ready: 0, war: 0, fade: 0, kinds: new Int8Array(END - START + 2) }; } -// Run the opening regime for 150 years so it reaches its own shape and the knobs show from the first frame. -function warm(P, fk, mig) { +// Run the opening regime for 110 years, longer than anyone lives, so it reaches its own shape and the knobs show from the first frame, +// then the story up to the year it opens on. +function warm(P, fk, mig, open) { for (let s = 0; s < 6; s++) for (let a = 0; a < NA; a++) P.cur[s][a] = s < 2 ? exp(-0.04 * a) : 0; - for (let i = 0; i < 150; i++) { - step(P.cur, P.tmp, START - 150 + i, fk, mig, rnd() < 0.07 ? 2 : -1, 0); + for (let i = 0; i < 110; i++) { + step(P.cur, P.tmp, START - 110 + i, fk, mig, rnd() < 0.07 ? 2 : -1, 0); [P.cur, P.tmp] = [P.tmp, P.cur]; } P.y = START; P.u = 0; P.kinds.fill(-1); next(P, fk, mig, 0); + while (P.y < open) [P.cur, P.nxt] = [P.nxt, P.cur], P.y++, next(P, fk, mig, 0); P.scl = 0; } @@ -140,13 +144,13 @@ export function update(dt, input, params) { const P = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); const fk = exp(0.47 * cl(k[2], -1, 1)), k4 = cl(k[3], -1, 1), mig = Math.sign(k4) * max(0, abs(k4) - 0.1) / 0.9, spd = 1.5 * cl(k[1], 0.25, 6); P.hue = k[0]; - if (!P.ready) (P.ready = 1), warm(P, fk, mig); + if (!P.ready) (P.ready = 1), warm(P, fk, mig, OPEN); // K1 tap: this year is a war, then an epidemic, by turns. if (input.btnPressed && input.btnPressed[0] && P.y < END) next(P, fk, mig, (P.war ^= 1) ? 1 : 2); if (P.fade > 0) { // The story is over: hold the urn a moment, dim, start again from the old triangle, light up. P.fade += d; - if (P.fade > 2 && P.y >= END) warm(P, fk, mig); + if (P.fade > 2 && P.y >= END) warm(P, fk, mig, START); if (P.fade > 2.8) P.fade = 0; } else { P.u += d * spd; From 6c8f377adbcaa61f7844f0de626ba2d40a1e21ac Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 04:00:35 +0000 Subject: [PATCH 12/17] kintsugi: tea bowl that cracks, opens and closes again with gold seams under a swinging lantern Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DbfsCYkpNNJ5diTKTtPZEu --- incoming/pattern-study/patterns/kintsugi.js | 437 ++++++++++++++++++++ 1 file changed, 437 insertions(+) create mode 100644 incoming/pattern-study/patterns/kintsugi.js diff --git a/incoming/pattern-study/patterns/kintsugi.js b/incoming/pattern-study/patterns/kintsugi.js new file mode 100644 index 000000000..24935f158 --- /dev/null +++ b/incoming/pattern-study/patterns/kintsugi.js @@ -0,0 +1,437 @@ +// ===== Patternflow pattern ===== +// Title: Kintsugi +// Family: pottery +// Idea: A tea bowl on a dark indigo cloth cracks, its shards drift a little apart and turn while lacquer and gold run along their broken edges, then close up again, and a swinging paper lantern sends a glint along the gold seams of every earlier mend. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 25, after kintsugi, the Japanese mending of broken ware with urushi lacquer and powdered gold +// Round: 25 +// =============================== +// @matrix 64x128 +// @knobs Glaze=0..1, Speed=0.2..3, Mends=2..8, Lamp=-1..1 +// +// Knob 1: Glaze. Celadon jade at 0, then shino white with its fire blush, persimmon kaki and raku black at 1. The foot ring stays bare clay, and the broken edges show the same clay before the lacquer goes on. +// Knob 2: Speed. At 2 a break takes about six seconds from the crack to the closed seam, the bowl rests about six seconds between breaks and the lantern swings once in six and a half; at 0.2 it all runs ten times slower. +// Knob 3: Mends. How many earlier breaks the bowl carries, from two to eight: their gold seams, the gold-filled chips in the rim and here and there a piece of another bowl set in where a shard was lost. The more gold, the more warm light it throws on the cloth. Each new mend lays bright gold, and the oldest wears back into the glaze. +// Knob 4: Lamp. Where the lantern hangs, from left of the bowl at -1 to right of it at 1. The lit side of the bowl, the glint on the gold, the pool of light on the cloth and the bowl's shadow move with it. +// Buttons: K1 tap breaks the bowl now, while it stands whole. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, round, sin, cos, sqrt, exp, atan2, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const mix = (a, b, t) => a + (b - a) * t; +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); +const rnd = () => Math.random(); +const F32 = (n) => new Float32Array(n); +function hash(i, j) { + let h = (Math.imul(i, 374761393) + Math.imul(j, 668265263)) | 0; + h = Math.imul(h ^ (h >>> 13), 1274126177); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +} +function vn(x, y) { + const xi = floor(x), yi = floor(y), u = x - xi, v = y - yi, su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v); + const a = hash(xi, yi), b = hash(xi + 1, yi), c = hash(xi, yi + 1); + return a + (b - a) * su + (c - a) * sv + (a - b - c + hash(xi + 1, yi + 1)) * su * sv; +} + +// The bowl turned about x = CX, its foot on the cloth at FB, HT tall with a +// rim RT across, seen a little from above: a circle r across shows E * r tall. +// Its picture is kept in a band of rows from BY0, BH rows tall. +const CX = 32, FB = 104, HT = 40, E = 0.3, RT = 28, HORIZON = 84, BY0 = 52, BH = 58, NB = 64 * BH; +const R = (h) => (h < 4 ? 11.5 : 13.6 + 14.4 * sin((min(1, (h - 4) / (HT - 4)) * PI) / 2) ** 0.75); + +// Each glaze and the colour it pools to, from celadon to raku black. +const GLAZE = [ + [[112, 184, 150], [62, 134, 128]], + [[238, 230, 214], [238, 150, 96]], + [[240, 120, 44], [150, 60, 26]], + [[46, 42, 42], [92, 74, 62]], +]; +const CLAY = [206, 158, 116], BODY = [232, 212, 178], URUSHI = [184, 44, 30], GOLD = [255, 206, 86], OLDG = [236, 160, 64], GLINT = [255, 246, 216], SHEEN = [255, 238, 210]; +const WALL = [92, 74, 56], WOOD = [104, 66, 40], CLOTH = [46, 62, 132]; +// Rest, crack, open, gild, close and shine, in seconds at Speed 2. +const DUR = [5, 0.45, 1.3, 2.4, 1.5, 1.2]; + +export function setup(params) { + const P = (params.s = { + ph: 0, pt: 0, tau: 0, wd: 2.4, count: 0, mends: 2, glaze: 0, gs: 0, PX: 33, LX: 33, LY: 29, fade: 1, flare: 0, + mat: new Uint8Array(NB), nrm: F32(NB * 3), ao: F32(NB), mot: F32(NB), jit: F32(NB), sid: new Int32Array(NB), fid: new Int32Array(NB), off: new Uint8Array(16), + col: F32(NB * 3), f: F32(N * 3), bg: F32(N * 3), dust: [], + }); + geometry(P); + // The breaks the bowl has already been through, oldest first. + for (let k = 0; k < 8; k++) seams(P, crack(P, 4 + floor(rnd() * 3)), 1 + floor(rnd() * 3)); + brk(P); + backdrop(P); + swing(P); +} + +// Where each LED of the bowl's picture sits on it: outer wall, inside, lip or +// foot, which way it faces, how much the bowl shades it, and the glaze's pooling. +function geometry(P) { + const { mat, nrm, ao, mot, jit } = P; + for (let b = 0; b < NB; b++) { + const x = b & 63, y = (b >> 6) + BY0, px = x + 0.5 - CX, h0 = FB - y - 0.5; + let h = h0, r = 0, u = 9, c = 0, n = null, m = 0, a = 1; + for (let it = 0; it < 4; it++) { + r = R(h); + u = px / r; + c = u * u < 1 ? sqrt(1 - u * u) : 0; + h = h0 + E * r * c; + } + r = R(h); + u = px / r; + if (h >= 0 && h <= HT && u * u < 1) { + c = sqrt(1 - u * u); + const dR = (R(h + 0.6) - R(h - 0.6)) / 1.2, nl = 1 / sqrt(1 + dR * dR); + m = h < 4 ? 4 : h > HT - 1.1 ? 3 : 1; + n = m === 3 ? [u * 0.5, 0.8, 0.35] : [u * nl, -dR * nl, c * nl]; + a = m === 4 ? 0.8 : 1; + } else { + const ex = px / RT, ey = (y + 0.5 - (FB - HT)) / (E * RT), e2 = ex * ex + ey * ey; + if (e2 >= 1) continue; + // Inside: the far wall seen over the rim, darker the deeper it goes. + let hb = HT; + for (let it = 0; it < 4; it++) { + r = R(hb); + u = cl(px / r, -1, 1); + c = sqrt(1 - u * u); + hb = cl(h0 - E * r * c, 4, HT); + } + const dR = (R(hb + 0.6) - R(hb - 0.6)) / 1.2, nl = 1 / sqrt(1 + dR * dR); + m = e2 > 0.84 && ey < 0.2 ? 3 : 2; + n = m === 3 ? [ex * 0.5, 0.8, 0.35] : [-u * nl, dR * nl, c * nl]; + a = m === 3 ? 1 : (0.3 + 0.7 * ((hb - 4) / (HT - 4)) ** 0.6) * (1 - 0.35 * sm(0.2, 0.95, ey)); + } + mat[b] = m; + nrm.set(n, b * 3); + ao[b] = a; + mot[b] = sm(0.3, 0.85, 0.75 * vn(x / 4.5, y / 3.5) + 0.25 * hash(x, y + 999)) * (m === 2 ? 1 : 0.8); + jit[b] = (hash(x + 77, y) - 0.5) * 0.6; + } +} + +// A break: the bowl's picture split into n shards with ragged edges, taller +// than wide, like cracks that run down from the rim. +function crack(P, n) { + const S = [], reg = new Uint8Array(NB), ox = rnd() * 400, oy = rnd() * 400; + while (S.length < n) { + const b = floor(rnd() * NB); + if (P.mat[b]) S.push([(b & 63) + 0.5, (b >> 6) + 0.5]); + } + for (let b = 0; b < NB; b++) { + if (!P.mat[b]) continue; + const x = (b & 63) + 0.5, y = (b >> 6) + 0.5; + const wx = x + 5 * (vn(x / 7 + ox, y / 7 + oy) - 0.5) + 1.6 * (vn(x / 2 + oy, y / 2) - 0.5); + const wy = y + 5 * (vn(x / 7 + oy, y / 7 + ox + 50) - 0.5) + 1.6 * (vn(x / 2, y / 2 + ox) - 0.5); + let best = 1e9; + for (let i = 0; i < n; i++) { + const dx = wx - S[i][0], dy = (wy - S[i][1]) * 0.75, dd = dx * dx + dy * dy; + if (dd < best) (best = dd), (reg[b] = i + 1); + } + } + // Most breaks also knock a chip out of the rim, and the gold fills it. + if (rnd() < 0.8) { + let c; + do c = floor(rnd() * NB); + while (P.mat[c] !== 3); + const r = 2.4 + rnd() * 1.8, cx = c & 63, cy = c >> 6; + for (let b = 0; b < NB; b++) if (reg[b] && ((b & 63) - cx) ** 2 + ((b >> 6) - cy) ** 2 < r * r) reg[b] = 255; + } + // Some lose a shard for good, and a piece of another bowl is set in its + // place (yobitsugi). + if (rnd() < 0.55) { + let c; + do c = floor(rnd() * NB); + while (P.mat[c] !== 1 && P.mat[c] !== 2); + const r = 4.5 + rnd() * 2.5, cx = c & 63, cy = c >> 6; + for (let b = 0; b < NB; b++) { + const x = b & 63, y = b >> 6, dx = x - cx, dy = (y - cy) * 1.25; + if (reg[b] && reg[b] < 255 && dx * dx + dy * dy < (r * (0.75 + 0.5 * vn(x / 3 + ox, y / 3))) ** 2) reg[b] = 254; + } + } + return reg; +} +// One side of a crack (for the seam it leaves) or both (for the edges it opens). +function edge(reg, b, both) { + const r = reg[b], x = b & 63; + if (!r) return false; + for (const nb of [x < 63 ? b + 1 : -1, x > 0 ? b - 1 : -1, b + 64 < NB ? b + 64 : -1, b - 64]) { + if (nb < 0 || !reg[nb] || reg[nb] === r) continue; + if (both || reg[nb] > r) return true; + } + return false; +} +// A closed break becomes the newest seam, with its gold chip and the piece +// from another bowl, glazed off glazes along. +function seams(P, reg, off) { + const id = ++P.count; + P.off[id & 15] = off; + for (let b = 0; b < NB; b++) { + if (reg[b] === 254) P.fid[b] = id; + if (reg[b] === 255 || edge(reg, b, 0)) P.sid[b] = id; + } +} + +// The next break: its shards, which way each one drifts and turns, the +// crack's run from where it is struck, and the order the gold goes round. +function brk(P) { + const n = 4 + floor(rnd() * 3), reg = crack(P, n), S = []; + for (let i = 0; i < n; i++) S.push({ cx: 0, cy: 0, c: 0, rad: 0, e: [] }); + for (let b = 0; b < NB; b++) + if (reg[b] && reg[b] < 254) { + const s = S[reg[b] - 1]; + (s.cx += (b & 63) + 0.5), (s.cy += (b >> 6) + BY0 + 0.5), s.c++; + } + for (const s of S) { + if (!s.c) continue; + (s.cx /= s.c), (s.cy /= s.c); + const dx = s.cx - CX, sg = dx < 0 ? -1 : 1; + Object.assign(s, { tx: dx * 0.28 + sg * (1.5 + rnd() * 1.5), ty: (s.cy - 80) * 0.16 + 1 + rnd(), rot: (rnd() - 0.5) * 0.5, p: rnd() * TAU, o: rnd() }); + } + const cb = new Uint8Array(NB), ce = new Uint8Array(NB), ga = F32(NB), cd = F32(NB); + let ib; + do ib = floor(rnd() * NB); + while (P.mat[ib] !== 1); + let md = 1; + for (let b = 0; b < NB; b++) { + if (!reg[b] || reg[b] > 253) continue; + const s = S[reg[b] - 1], x = (b & 63) + 0.5, y = (b >> 6) + BY0 + 0.5; + const dx = s.cx - x, dy = s.cy - y; + s.rad = max(s.rad, sqrt(dx * dx + dy * dy) + 1.5); + if (edge(reg, b, 1)) (ce[b] = 1), s.e.push(b), (ga[b] = (atan2(dy, dx) / TAU + 0.5 + s.o) % 1); + if (edge(reg, b, 0)) (cb[b] = 1), (md = max(md, (cd[b] = sqrt((x - (ib & 63)) ** 2 + (y - (ib >> 6) - BY0) ** 2)))); + } + for (let b = 0; b < NB; b++) cd[b] /= md; + Object.assign(P, { reg, S, cb, ce, ga, cd, noff: 1 + floor(rnd() * 3), ix: (ib & 63) + 0.5, iy: (ib >> 6) + BY0 + 0.5 }); +} + +// The plaster wall with straw in it, the beam the lantern hangs from, and the +// folded indigo cloth, before the lantern lights them. +function backdrop(P) { + const B = P.bg; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const o = (y * W + x) * 3, q = hash(x, y); + let col, f; + if (y < 3) (col = WOOD), (f = (y === 2 ? 0.55 : 0.9) * (0.85 + 0.15 * vn(x / 6, y))); + else if (y < HORIZON) (col = WALL), (f = (0.86 + 0.2 * vn(x / 4, y / 4) + 0.04 * q) * (q > 0.985 ? 1.3 : 1)); + else { + // Folds that run across the cloth and ease towards the front. + const v = 0.5 + 0.5 * sin(x * 0.11 - y * 0.17 + 2.2 * sin(y * 0.05 + x * 0.04)) * sin(x * 0.07 + y * 0.05 + 1); + (col = CLOTH), (f = (0.6 + 0.5 * v ** 1.4) * (0.94 + 0.06 * ((x + y) & 1)) * (y === HORIZON ? 1.35 : 1)); + } + for (let c = 0; c < 3; c++) B[o + c] = col[c] * f; + } +} + +function swing(P) { + const w = (TAU / 6.5) * P.tau, a = 0.42 * sin(w) + 0.07 * sin(2.3 * w + 1); + P.LX = P.PX + 26 * sin(a); + P.LY = 3 + 26 * cos(a); +} + +function go(P, ph) { + if (ph === 5) seams(P, P.reg, P.noff), brk(P); + if (ph === 0) P.wd = DUR[0]; + P.ph = ph; + P.pt = 0; +} + +// Where a shard is now: turned by th about its middle and moved by tx, ty. +function place(P, s) { + const ph = P.ph, p = P.pt / DUR[ph], e = ph === 2 ? 1 - (1 - p) ** 3 : ph === 3 ? 1 : ph === 4 ? 1 - sm(0, 1, p) : 0; + const th = (s.rot + 0.05 * sin(P.tau * 1.1 + s.p)) * e; + return [cos(th), sin(th), s.cx + (s.tx + 0.8 * sin(P.tau * 1.7 + s.p)) * e, s.cy + (s.ty + 0.6 * cos(P.tau * 1.3 + s.p)) * e]; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + const d = (cl(dt || 0, 0, 0.1) * cl(k[1], 0.2, 3)) / 2; + P.glaze = cl(k[0], 0, 1); + P.mends = round(cl(k[2], 2, 8)); + P.PX = 32 + 14 * cl(k[3], -1, 1); + P.tau += d; + if (input.btnPressed && input.btnPressed[0] && (P.ph === 0 || P.ph === 5)) go(P, 1); + if ((P.pt += d) >= (P.ph ? DUR[P.ph] : P.wd)) go(P, (P.ph + 1) % 6); + const p = P.pt / DUR[P.ph]; + P.fade = P.ph === 3 ? 1 - sm(0.2, 1, p) : P.ph === 4 ? 0 : 1; + P.flare = P.ph === 5 ? 1 - p : 0; + swing(P); + + // Gold powder dusted on the lacquer falls past the shards to the cloth. + if (P.ph === 3 && p > 0.3 && rnd() < d * 16) { + const s = P.S[floor(rnd() * P.S.length)]; + if (s.e.length) { + const b = s.e[floor(rnd() * s.e.length)], [co, si, tx, ty] = place(P, s), qx = (b & 63) + 0.5 - s.cx, qy = (b >> 6) + BY0 + 0.5 - s.cy; + P.dust.push([co * qx - si * qy + tx, si * qx + co * qy + ty, 0, FB + 5 + rnd() * 19, 1, rnd() * TAU]); + } + } + P.dust = P.dust.filter((q) => { + if (q[1] < q[3]) (q[2] += 40 * d), (q[1] = min(q[3], q[1] + q[2] * d)); + else q[4] -= d / 10; + return q[4] > 0; + }); + if (P.dust.length > 90) P.dust.shift(); +} + +// The bowl's colours for this moment: glaze lit by the lantern, the seams of +// the mends it carries, and the crack, the bare clay, the lacquer and the +// gold of the break under way. +function shade(P) { + // The bowl's glaze and the three others, round the ring of four. + const G = [0, 1, 2, 3].map((k) => { + const g = P.glaze * 3 + k, i = floor(g) % 4, f = g - floor(g); + return [0, 1].map((j) => GLAZE[i][j].map((v, n) => mix(v, GLAZE[(i + 1) % 4][j][n], f))); + }); + const { mat, nrm, ao, mot, jit, sid, fid, col, LX, LY, ph, count, fade, flare } = P, lo = count - P.mends, p = P.pt / (ph ? DUR[ph] : P.wd); + const lac = ph === 3 ? sm(0, 0.5, p) * 1.05 : ph === 4 ? 2 : 0, gil = ph === 3 ? sm(0.3, 1, p) * 1.05 : ph === 4 ? 2 : 0, open = ph >= 2 && ph <= 4; + const vis = (id) => (id > lo ? (id === lo + 1 ? fade : 1) : 0); + let gs = 0, r = 0, gg = 0, bb = 0, kg = 0, gl = 0; + const gold = (a, age, fl) => { + const t = min(1, age / 8); + gs += a * fl * (kg + gl); + r = mix(r, (mix(GOLD[0], OLDG[0], t) * kg + GLINT[0] * gl) * fl, a); + gg = mix(gg, (mix(GOLD[1], OLDG[1], t) * kg + GLINT[1] * gl) * fl, a); + bb = mix(bb, (mix(GOLD[2], OLDG[2], t) * kg + GLINT[2] * gl) * fl, a); + }; + const flat = (c, k) => ((r = c[0] * k), (gg = c[1] * k), (bb = c[2] * k)); + for (let b = 0; b < NB; b++) { + const m = mat[b]; + if (!m) continue; + const x = (b & 63) + 0.5, y = (b >> 6) + BY0 + 0.5, o = b * 3, nx = nrm[o], ny = nrm[o + 1], nz = nrm[o + 2]; + let lx = LX - x, ly = y - LY, lz = 24; + const dd = lx * lx + ly * ly + lz * lz, il = 1 / sqrt(dd), fall = 0.55 + 0.6 * exp(-dd / 5000); + (lx *= il), (ly *= il), (lz *= il); + const hz = lz + 1, hl = 1 / sqrt(lx * lx + ly * ly + hz * hz), dif = max(0, nx * lx + ny * ly + nz * lz) * fall; + kg = 0.62 + 0.55 * dif; + gl = min(1.1, 1.7 * fall * max(0, ((nx + jit[b]) * lx + ny * ly + nz * hz) * hl) ** 30); + if (m === 4) flat(CLAY, (0.16 + 0.8 * dif) * ao[b]); + else { + // Glaze, or the piece of another bowl set into it. + const t = mot[b], fi = fid[b], nw = ph === 4 && P.reg[b] === 254, gk = nw ? P.noff : fi > lo ? P.off[fi & 15] : 0, ga = !nw && fi === lo + 1 ? fade : 1; + const k = (0.2 + 0.3 * max(0, -ny) + 0.95 * dif) * ao[b] * (m === 3 ? 1.15 : 1), sh = 0.6 * fall * max(0, (nx * lx + ny * ly + nz * hz) * hl) ** 24; + const A = G[0], B = G[gk]; + r = mix(mix(A[0][0], A[1][0], t), mix(B[0][0], B[1][0], t), ga) * k + SHEEN[0] * sh; + gg = mix(mix(A[0][1], A[1][1], t), mix(B[0][1], B[1][1], t), ga) * k + SHEEN[1] * sh; + bb = mix(mix(A[0][2], A[1][2], t), mix(B[0][2], B[1][2], t), ga) * k + SHEEN[2] * sh; + } + const id = sid[b], a = vis(id); + if (open && P.ce[b]) { + // An opened edge: bare clay, then red lacquer, then gold. + const q = P.ga[b]; + if (q < gil) gold(1, 0, 1.15); + else if (q < lac) flat(URUSHI, 0.5 + 0.6 * dif); + else flat(BODY, 0.45 + 0.7 * dif); + } else if (a > 0) gold(a, count - id, id === count ? 1 + 0.6 * flare : 1); + else if (flare > 0) { + // Fresh gold glows a little past the line it fills. + for (let j = 0; j < 4; j++) { + const nb = b + (j === 0 ? 1 : j === 1 ? -1 : j === 2 ? 64 : -64); + if (nb >= 0 && nb < NB && sid[nb] === count) { + gold(0.6 * flare, 0, 1); + break; + } + } + } + if (ph === 1 && P.cb[b] && P.cd[b] < p * 1.6) (r *= 0.12), (gg *= 0.12), (bb *= 0.12); + (col[o] = r), (col[o + 1] = gg), (col[o + 2] = bb); + } + P.gs = gs; +} + +// The wall and cloth lit by the lantern, the bowl's shadow thrown away from +// it, and the warm light its gold throws down on the cloth. +function back(P, F) { + const { LX, LY, bg } = P, sx = CX - (LX - CX) * 0.45, gx = CX + (LX - CX) * 0.3, gk = min(1.6, P.gs / 700); + // The glows fall off as a column term times a row term. + const X = [], Y = []; + for (let x = 0; x < W; x++) { + const dx = (x + 0.5 - LX) ** 2; + X.push([exp(-dx / 380), exp(-dx / 1400), exp(-dx / 1500), gk * exp(-((x + 0.5 - gx) ** 2) / 600)]); + } + for (let y = 0; y < H; y++) Y.push([1.2 * exp(-((y + 0.5 - LY) ** 2) / 380), 0.3 * exp(-((y - LY) ** 2) / 4000), 0.9 * exp(-((y - 118) ** 2) / 700), exp(-((y - 111) ** 2) / 160)]); + for (let y = 0, o = 0; y < H; y++) { + const [a, b, c, d] = Y[y], ey = (y + 0.5 - FB - 3) / 7; + for (let x = 0; x < W; x++, o += 3) { + const C = X[x]; + if (y < HORIZON) { + const k = 0.3 + a * C[0] + b * C[1]; + (F[o] = bg[o] * k), (F[o + 1] = bg[o + 1] * k), (F[o + 2] = bg[o + 2] * k); + continue; + } + const ex = (x + 0.5 - sx) / 31, g = d * C[3] * (bg[o + 2] / CLOTH[2]); + let k = 0.42 + c * C[2]; + if (ey * ey < 1.4 && ex * ex < 1.4) k *= 1 - 0.72 * sm(1.15, 0.55, sqrt(ex * ex + ey * ey)); + k *= 1 + 0.7 * g; + (F[o] = bg[o] * k + 96 * g), (F[o + 1] = bg[o + 1] * k + 74 * g), (F[o + 2] = bg[o + 2] * k + 16 * g); + } + } +} + +const put = (F, x, y, c, k) => { + if ((x = floor(x)) >= 0 && x < W && (y = floor(y)) >= 0 && y < H) for (let n = 0, o = (y * W + x) * 3; n < 3; n++) F[o + n] = c[n] * k; +}; + +// The paper lantern on its cord: ribs across the lit paper, black lacquer caps. +function lantern(P, F) { + const { LX, LY, PX } = P; + for (let t = 0; t < 1; t += 0.04) put(F, mix(PX, LX, t), mix(3, LY - 10, t), [70, 58, 46], 1); + for (let yy = -10; yy <= 10; yy++) { + const y = round(LY) + yy, cap = yy <= -9 || yy >= 9, w = cap ? 3.6 : 6.3 * sqrt(max(0, 1 - (yy / 9.6) ** 2)) + 0.4; + for (let x = floor(LX - w); x <= LX + w; x++) { + const dx = x + 0.5 - LX, cov = cl(w - Math.abs(dx) + 0.5, 0, 1); + if (y < 0 || x < 0 || x >= W || !cov) continue; + const o = (y * W + x) * 3, q = dx / w, gl = 1 - q * q, rib = (yy + 9) % 3 ? 1 : 0.7; + for (let n = 0; n < 3; n++) { + const c = cap ? [38, 30, 26][n] * (1.2 - 0.4 * q) : mix([236, 118, 44][n], [255, 226, 160][n], gl * (1 - (yy / 10) ** 2)) * rib; + F[o + n] = mix(F[o + n], c, cov); + } + } + } +} + +export function draw(display, params) { + const P = params.s; + if (!P || !P.mat) return; + const F = P.f, { col, mat, ph } = P; + shade(P); + back(P, F); + lantern(P, F); + if (ph >= 2 && ph <= 4) { + // Each shard drawn where it has drifted, read back through its turn. + for (let i = 0; i < P.S.length; i++) { + const s = P.S[i]; + if (!s.c) continue; + const [co, si, tx, ty] = place(P, s); + for (let y = max(0, floor(ty - s.rad)); y <= min(H - 1, ty + s.rad); y++) + for (let x = max(0, floor(tx - s.rad)); x <= min(W - 1, tx + s.rad); x++) { + const dx = x + 0.5 - tx, dy = y + 0.5 - ty, bx = floor(co * dx + si * dy + s.cx), by = floor(-si * dx + co * dy + s.cy) - BY0; + if (bx < 0 || bx > 63 || by < 0 || by >= BH) continue; + const b = by * 64 + bx, o = (y * W + x) * 3; + if (P.reg[b] === i + 1) (F[o] = col[b * 3]), (F[o + 1] = col[b * 3 + 1]), (F[o + 2] = col[b * 3 + 2]); + } + } + // The lost chip comes back as gold, and the lost shard as a piece of + // another bowl, as the shards close. + const a = ph === 4 ? sm(0.4, 1, P.pt / DUR[4]) : 0; + if (a) for (let b = 0, o = BY0 * W * 3; b < NB; b++, o += 3) if (P.reg[b] > 253) for (let n = 0; n < 3; n++) F[o + n] = mix(F[o + n], col[b * 3 + n], a); + } else for (let b = 0; b < NB; b++) if (mat[b]) F.set(col.subarray(b * 3, b * 3 + 3), (BY0 * W + b) * 3); + // The ring of the strike. + if (ph === 1) { + const p = P.pt / DUR[1]; + for (let y = -7; y <= 7; y++) + for (let x = -7; x <= 7; x++) { + const k = (1 - p) * exp(-(x * x + y * y) / (2 + 10 * p)), xx = floor(P.ix + x), yy = floor(P.iy + y); + if (xx >= 0 && xx < W && yy >= 0 && yy < H) for (let n = 0, o = (yy * W + xx) * 3; n < 3; n++) F[o + n] += 255 * k; + } + } + for (const q of P.dust) { + const x = floor(q[0]), y = floor(q[1]), k = q[1] < q[3] ? 0.9 : q[4] * (0.25 + 0.75 * max(0, sin(q[5] + P.tau * 4)) ** 3); + if (x >= 0 && x < W && y >= 0 && y < H) for (let n = 0, o = (y * W + x) * 3; n < 3; n++) F[o + n] = mix(F[o + n], GOLD[n], k); + } + 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 08cd51ba30853069206c674e6ffe230691f1486f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 04:06:55 +0000 Subject: [PATCH 13/17] =?UTF-8?q?README=20=EC=97=90=2025=ED=8C=90=20?= =?UTF-8?q?=ED=91=9C=EC=99=80=20=EC=A0=88,=20=EB=8B=A4=EC=9D=8C=20?= =?UTF-8?q?=ED=8C=90=20=EA=B0=90=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4(Pe?= =?UTF-8?q?nrose=20=EC=9E=90=EB=A6=AC=EB=8A=94=20=EB=B9=84=EC=96=B4=20?= =?UTF-8?q?=EC=9E=88=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_01DbfsCYkpNNJ5diTKTtPZEu --- incoming/pattern-study/README.md | 71 +++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index abdcd3523..394a30fdb 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 148개를 마흔여섯 갈래로 나눠 만들었어요. 1판에서 열세 갈래로 23개를 만들고, 2판에서 1판에 없던 아홉 갈래로 16개를, 3판에서 하나를, 4판에서 새 갈래인 직물로 하나를 더했어요. 5판은 새 패턴 없이 1판의 Euclid Pulse 를 다시 짰고, 6판은 세운 패널의 128줄을 위아래로 다 쓰는 패턴 하나를 더했어요. 7판은 또렷한 모양이 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 8판은 새 갈래인 기계로 하나를 더했어요. 9판은 새 패턴 없이 1판의 Sprout 을 다시 짰어요. 10판은 세운 패널을 떨어지는 블록의 우물로 쓰는 패턴 하나를 더했어요. 11판은 여러 색이 꽤 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 12판은 새 갈래인 천체로 하나를 더했어요. 13판은 새 패턴 없이 1판의 Glyph Rain 을 다시 짰어요. 14판은 절차가 바뀌기 전에 띄운 바퀴라서, 세운 패널의 높이를 다 떨어지는 폭포 하나를 더했어요. 15판은 바뀐 절차의 첫 판이라 열 갈래에 새 패턴을 하나씩, 모두 열 개를 더했어요. 도시와 음악 갈래는 그때 새로 열었어요. 16판도 열 갈래에 하나씩 열 개를 더했고, 바다와 곤충 갈래를 새로 열었어요. 17판도 열 갈래에 하나씩 열 개를 더했고, 전자 회로와 장난감 갈래를 새로 열었어요. 18판도 열 갈래에 하나씩 열 개를 더했고, 유리와 광물·결정, 놀이공원 갈래를 새로 열었어요. 19판도 열 갈래에 하나씩 열 개를 더했고, 부엌과 버섯·균사 갈래를 새로 열었어요. 20판도 열 갈래에 하나씩 열 개를 더했고, 새와 화학·실험 갈래를 새로 열었어요. 21판도 열 갈래에 하나씩 열 개를 더했고, 종이와 운동·경기 갈래를 새로 열었어요. 22판도 열 갈래에 하나씩 열 개를 더했고, 몸과 비행 갈래를 새로 열었어요. 23판도 열 갈래에 하나씩 열 개를 더했고, 무대·공연과 짐승 갈래를 새로 열었어요. 24판도 열 갈래에 하나씩 열 개를 더했고, 공사·건설과 도자기 갈래를 새로 열었어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 158개를 마흔여덟 갈래로 나눠 만들었어요. 1판에서 열세 갈래로 23개를 만들고, 2판에서 1판에 없던 아홉 갈래로 16개를, 3판에서 하나를, 4판에서 새 갈래인 직물로 하나를 더했어요. 5판은 새 패턴 없이 1판의 Euclid Pulse 를 다시 짰고, 6판은 세운 패널의 128줄을 위아래로 다 쓰는 패턴 하나를 더했어요. 7판은 또렷한 모양이 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 8판은 새 갈래인 기계로 하나를 더했어요. 9판은 새 패턴 없이 1판의 Sprout 을 다시 짰어요. 10판은 세운 패널을 떨어지는 블록의 우물로 쓰는 패턴 하나를 더했어요. 11판은 여러 색이 꽤 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 12판은 새 갈래인 천체로 하나를 더했어요. 13판은 새 패턴 없이 1판의 Glyph Rain 을 다시 짰어요. 14판은 절차가 바뀌기 전에 띄운 바퀴라서, 세운 패널의 높이를 다 떨어지는 폭포 하나를 더했어요. 15판은 바뀐 절차의 첫 판이라 열 갈래에 새 패턴을 하나씩, 모두 열 개를 더했어요. 도시와 음악 갈래는 그때 새로 열었어요. 16판도 열 갈래에 하나씩 열 개를 더했고, 바다와 곤충 갈래를 새로 열었어요. 17판도 열 갈래에 하나씩 열 개를 더했고, 전자 회로와 장난감 갈래를 새로 열었어요. 18판도 열 갈래에 하나씩 열 개를 더했고, 유리와 광물·결정, 놀이공원 갈래를 새로 열었어요. 19판도 열 갈래에 하나씩 열 개를 더했고, 부엌과 버섯·균사 갈래를 새로 열었어요. 20판도 열 갈래에 하나씩 열 개를 더했고, 새와 화학·실험 갈래를 새로 열었어요. 21판도 열 갈래에 하나씩 열 개를 더했고, 종이와 운동·경기 갈래를 새로 열었어요. 22판도 열 갈래에 하나씩 열 개를 더했고, 몸과 비행 갈래를 새로 열었어요. 23판도 열 갈래에 하나씩 열 개를 더했고, 무대·공연과 짐승 갈래를 새로 열었어요. 24판도 열 갈래에 하나씩 열 개를 더했고, 공사·건설과 도자기 갈래를 새로 열었어요. 25판도 열 갈래에 하나씩 열 개를 더했고, 농사·밭과 화산 갈래를 새로 열었어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. 패널은 세로로 서요. 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판에서 세로로 다시 짰어요. @@ -264,6 +264,21 @@ | [Tower Crane](patterns/tower-crane.js) | 공사·건설 | 해 질 녘 올라가는 콘크리트 건물 옆에 노란 타워크레인이 서서, 마당의 벽판과 철근 다발, 거푸집, 콘크리트 버킷을 차례로 꼭대기 층에 올려요. 붐이 원근 속에서 돌며 짧아졌다 길어지고, 버킷이 바닥을 부으면 건물이 한 층 자라 새 층 창에 불이 들어와요. K1 을 끝까지 돌리면 밤이 되어 투광등이 켜져요. | | [Waggle Dance](patterns/waggle-dance.js) | 곤충 | 관찰 벌통의 벌집 한 장이 패널 높이로 서요. 위에 봉한 꿀, 그 아래 여덟 색 꽃가루 띠와 애벌레 방이 있고, 벌 여든 마리가 기어 다녀요. 아래쪽 빈 벌집에서는 꽃가루를 단 벌이 몸을 흔들며 8 자 춤을 추고 따르는 벌들이 둘러서요. 춤의 각도가 해에서 본 꽃밭의 방향이라 K4 를 돌리면 모든 춤이 같이 돌아요. | +### 25판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Eruption](patterns/eruption.js) | 화산 | 해 질 녘 성층화산을 세로로 잘라 봐요. 아래 마그마 방에서 떠난 가스 덩어리가 황토와 잿빛 지층 사이 화도를 타고 올라 분화구에서 터지면, 화산탄이 포물선을 그리며 떨어지고 재 기둥은 패널 꼭대기까지 올라 번개를 품어요. K1 이 스트롬볼리식에서 하와이식 용암 분수를 지나 플리니식 재 기둥까지 분출을 바꾸고, K1 을 누르면 큰 폭발이 나요. | +| [Excavation](patterns/excavation.js) | 공사·건설 | 도심 빌딩 사이의 깊은 터파기를 세로로 잘라 봐요. 굴착기가 바닥에 쌓은 흙을 갠트리의 클램셸이 100줄 위 덤프트럭까지 끌어올리고, 바닥이 한 단 깊어지면 흙막이 벽 사이에 강철 버팀보가 한 단 더 용접돼 걸려요. K1 을 돌리면 데크 틈으로 떨어지던 햇빛 기둥이 기울다가 밤의 나트륨등으로 바뀌고, 누르면 경고등 뒤에 발파가 일어나 바닥에 먼지가 차요. | +| [Ferris Wheel](patterns/ferris-wheel.js) | 놀이공원 | 밤 호숫가에서 지름 59 LED 의 관람차가 돌고, 곤돌라는 바퀴가 돌아도 늘 똑바로 매달려 있어요. 살과 테두리의 불빛이 쇼 여섯 가지를 6.5초씩 이어 가고, 호수가 바퀴와 불빛을 일렁이며 비춰요. K4 Wind 를 올리면 구름이 몰려오고 물그림자가 긴 빛줄기로 부서지며, 버튼은 다음 쇼로 넘겨요. | +| [Kintsugi](patterns/kintsugi.js) | 도자기 | 어두운 쪽빛 천 위의 청자 찻사발에 금이 가고, 조각들이 조금 벌어져 돌아요. 깨진 날에 붉은 옻과 금이 돈 다음 다시 붙어요. 위에 매달린 종이 등이 흔들리며 금빛 이음매에 반짝임을 지나가게 하고, K3 Mends 를 올릴수록 지나온 깨짐의 금빛 그물이 촘촘해져요. K1 을 누르면 사발이 바로 깨져요. | +| [Marionette](patterns/marionette.js) | 무대·공연 | 작은 인형극 무대에서 어릿광대 꼭두각시가 팔로 스폿을 받으며 지그를 춰요. 머리와 두 손, 두 무릎, 등에 맨 실 일곱이 꼭대기 다리에서 인형사가 쥔 십자 조종대까지 올라가서, 조종대가 기울면 실이 따라 기울고 팔과 무릎이 끌려 올라가요. K4 가 스폿을 옆으로 옮기면 인형과 실의 그림자가 반대쪽 배경막에 서고, K1 을 누르면 인형이 무릎을 접고 높이 뛰었다가 인사해요. | +PENROSE_ROW +| [Population Pyramid](patterns/population-pyramid.js) | 데이터 그림 | 가상의 나라의 인구 피라미드가 한 살에 한 줄씩 움직여요. 왼쪽이 남자, 오른쪽이 여자이고, 10년마다 태어난 세대가 제 색 띠로 해마다 한 줄씩 올라가요. 1995년의 베이비붐 띠가 위로 올라가 2060년에 위가 무거운 항아리꼴이 되면, 1820년의 뾰족한 삼각형으로 돌아가 240년을 다시 지나요. K3 을 내리면 아래가 좁아져 거꾸로 선 피라미드가 되고, K1 을 누르면 그해가 전쟁이나 유행병이 되어 패인 홈이 위로 올라가요. | +| [Rice Terraces](patterns/rice-terraces.js) | 농사·밭 | 세운 패널 아래 끝에서 숲이 선 능선까지 물 댄 다랑논이 등고선처럼 휘어 쌓이고, 논마다 해 뜨는 하늘을 비춰요. 위로 갈수록 층이 얇아지고 노을빛이 짙어지며, 구름 반사가 거울 논들을 가로질러 흘러가고 가까운 논에서는 농부가 물소를 몰아 논을 갈아요. K1 을 돌리면 모가 줄지어 서고 초록으로 덮였다가 누렇게 익고, K1 을 누르면 맨 위 물꼬가 열려 물이 둑을 하나씩 넘어 내려가요. | +| [Skydiver](patterns/skydiver.js) | 비행 | 강하장 위로 점프기가 지나가며 문에서 다이버들을 내보내요. 다이버들은 구름층을 뚫고 내려가 손을 잡아 고리를 만들었다가 흩어지고, 저마다 색 램에어 캐노피를 펴서 나선을 그리며 들판의 착지 원으로 내려와요. K1 을 끝까지 돌리면 해가 지평선에 걸린 노을 점프가 되고, K1 을 누르면 다음 비행기가 바로 들어와요. | +| [Snow Monkeys](patterns/snow-monkeys.js) | 짐승 | 눈 내리는 지고쿠다니의 해 질 녘, 일본원숭이들이 김 오르는 노천 온천에 어깨까지 잠겨 머리에 눈을 얹은 채 졸거나 서로 털을 골라 줘요. 바위 위의 어미가 물로 걸어 들어오고, 덩어리진 김은 눈 덮인 삼나무 비탈을 지나 패널 꼭대기까지 피어올라요. K1 을 끝까지 돌리면 밤이 되어 석등이 김을 주황으로 물들이고, K1 을 누르면 새끼가 물보라를 일으키며 뛰어들어요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -1018,6 +1033,50 @@ K4 Spate 0 이면 물이 실 몇 가닥으로 갈라져요. 처음 값 0.06 이 남은 자리로 물리·스프링 갈래의 넷째를 세웠어요. 진자 줄을 위에서 내려다봐서, 16~24개의 추가 4번 줄부터 124번 줄까지 세로로 늘어서고 저마다 ±22 LED 를 옆으로 흔들려요. i 번째 진자가 한 주기에 22+i 번 흔들리고, 시작 노브에서 한 주기가 28초예요. 0.65초에 판에서 놓으면 2~8초 동안 줄이 뱀처럼 휘고, 10초쯤 세 줄, 15초쯤 두 줄로 엇갈렸다가 28초에 다시 한 줄이 돼요. `Math.random` 을 쓰지 않아서 씨앗 열여섯이 다 같은 값이에요. 첫 판은 색이 0.060 으로 축 아래 끝 Rainstorm 의 0.066 밑이었어요. 호두나무 판을 붉게 하고 바닥을 밝히자 0.098 이 됐는데, 이번에는 모양 바뀜이 0.039 에서 0.023 으로 떨어졌어요. 모양 바뀜은 추와 바닥의 밝기 차이에서 나와서, 추를 밝히고 바닥을 다시 어둡게 해 색 0.080, 모양 바뀜 0.033 에 세웠어요. 판에 붙잡았다 놓는 첫 장면은 모양 바뀜을 올리지 않았어요. 0.030 이 0.029 가 됐어요. +## 25판 + +INTRO_25 + +### Rice Terraces + +주제 2 로 농사·밭 갈래를 열고, 주제 4 로 세운 패널의 아래 끝에서 숲이 선 능선까지를 다랑논 열두 층에서 스물네 층으로 채웠어요. 층의 높이는 위로 갈수록 줄어서 처음 자리에서 가까운 논은 14줄, 능선 밑 논은 4줄이에요. 왼쪽으로 내려오는 산줄기와 오른쪽 골짜기에 느린 잡음을 더해 등고선이 휘어요. 논마다 하늘을 비추는데, 먼 논은 지평선 쪽 하늘을, 가까운 논은 머리 위 하늘을 비춰요. 그래서 위로 갈수록 노을빛이고 아래로 올수록 파래요. 하늘과 논을 다 칠해서 `thirds` 는 100/100/100 이고, 2~8초 동안 바뀐 LED 는 26.5/30.2/77.6% 예요. 하늘 0~35줄을 빼고 재도 위 3분의 1이 12.5% 라서, 맨 위 논들의 구름 반사가 그 3분의 1을 움직여요. 첫 판은 모양 바뀜이 0.023 이었어요. 논이 지평선 쪽 주황만 비춰서 구름과 하늘의 밝기 차가 작았어요. 하늘을 고도 표로 다시 짜서 가까운 논이 짙은 파랑의 머리 위 하늘을 비추게 하자, 구름 반사가 블록만 하게 지나가며 0.063 이 됐어요. 처음 구름 자리를 씨앗으로 고르게 하자 연구 씨앗이 0.046 으로 내려갔어요. 구름 가장자리를 가파르게 세워서 지금은 씨앗 아홉에서 0.056~0.110 이에요. 첫 판의 둑 테두리는 점선이었어요. 둑이 지나는 자리를 픽셀 가운데에서 재서 열의 반에서 테두리가 빠졌어요. 경계 바로 아래 줄을 테두리로 삼고, 한 줄 넘게 꺾이는 곳은 이어 메우자 선이 이어졌어요. K3 를 24 로 올리면 먼 층에 흙벽 한 줄과 테두리 한 줄만 남아서 위 절반이 갈색 결이 됐어요. 벽 높이를 층 높이의 0.22 배에서 0.6 을 뺀 값으로 바꾸자, 2.7줄보다 얇은 층에서는 벽이 사라지고 물 위의 선만 남았어요. 노브 넷은 0.12, 0.07, 0.13, 0.42 예요. + +### Eruption + +주제 2 로 화산 갈래를 열고, 주제 4 로 성층화산의 세로 단면 하나가 128줄을 다 쓰게 했어요. 분화구 테두리가 53번 줄, 원뿔이 앉은 옛 땅이 86번 줄, 마그마 방 가운데가 113번 줄이에요. 그래서 위 3분의 1은 재 기둥, 가운데는 원뿔과 화도, 아래는 위로 휜 지층과 마그마 방이에요. 하늘과 지층을 다 칠해서 `thirds` 는 100/100/100 이고, 2~8초 동안 바뀐 LED 는 77.0/57.8/23.8% 예요. 하늘의 불빛을 멈추고 재도 51.4~66.9/45.6~55.7/20.3~22.4% 예요. 지층이 부푸는 것까지 멈추면 아래 3분의 1이 15.7~17.7% 라서, 그 값은 마그마 방의 대류와 화도를 오르는 가스 덩어리에서 와요. 첫 판은 모양 바뀜이 0.015 로 Galton Board 아래였고, K4 Wind 는 0.0099 로 검사에 걸렸어요. 폭발마다 반지름 2.5 의 재 덩어리 하나가 1초에 4줄씩 오르다 사라져서 재 기둥이 위 20줄에 닿지 못했어요. 반지름 3.5~4.5 의 덩어리 두셋을 1초에 8줄로 올리자 모양 바뀜이 0.154 가 됐어요. 하늘에 더하던 번개 섬광을 (26,30,60) 에서 (7,8,18) 로 줄이자 위 3분의 1의 바뀐 LED 가 97.6% 에서 62.4% 로 내려갔어요. 그 대신 모양 바뀜이 씨앗 여덟에서 0.027~0.076 으로 흔들렸어요. 번개를 폭발마다 0.25 + 0.5 × 세기의 확률로 붙이자 0.069~0.095 로 모였어요. K4 Wind 는 재 기둥을 키운 뒤에도 0.02~0.03 이었어요. 기둥에서 떨어지는 재가 바람 쪽으로 한 줄에 0.55 LED 씩 비껴 내리게 하자 0.05 가 됐어요. 식어 가는 용암은 황토 지층과 밝기가 같아서 산 옆을 흐르는 줄로 읽히지 않았어요. 지층의 가장 밝은 채널을 150 아래로 내리고, 원뿔 겉에 1.4 LED 두께의 검은 화산재 껍질을 두자 용암이 그 위에서 보여요. K1 을 끝까지 돌린 플리니식은 처음에 기둥이 끊기고 꼭대기 우산 구름만 떠 있었어요. 재 덩어리를 90개까지만 받았는데, 36초를 사는 우산 구름이 자리를 다 차지해 새 덩어리가 들어가지 못했어요. 가득 차면 가장 오래된 덩어리를 빼게 하자 기둥이 돌아왔어요. + +### Excavation + +주제 4 로 공사·건설 갈래의 둘째를 세웠어요. Tower Crane 이 위로 자라는 건물이라 이쪽은 아래로 자라는 구덩이예요. 맨 위 20줄에 하늘과 이웃 건물, 갠트리와 덤프트럭을 두었어요. 그 아래 100줄은 흙막이 벽과 버팀보 여덟 단이 걸린 터파기이고, 바닥 아래는 풍화암과 암반 단면이에요. 바탕을 다 칠해서 `thirds` 는 98/97/100 이고, 2~8초 동안 바뀐 LED 는 39.6/36.4/18.6% 예요. 구름을 멈추고 재도 24.3/12.5/15.3% 라서, 클램셸이 97줄 바닥까지 내려갔다 올라오는 길과 굴착기가 128줄을 다 써요. 모양 바뀜은 구름에 기대요. 첫 판은 데크 틈으로 떨어지는 햇빛 기둥에 연무를 짙게 칠해 0.044 였어요. 연무에 아래 단이 하얗게 묻혀서 절반쯤으로 줄이자 0.037 로 내려갔어요. 구름의 처음 자리를 옮겨 틈으로 드는 햇빛의 평균이 2초의 0.52 에서 8초의 0.97 로 개게 하자 0.054 가 됐고, 씨앗 여덟에서 0.054~0.055 예요. 구름을 멈추면 0.013 이라 기계만으로는 Galton Board 의 0.025 에 못 미쳐요. 첫 판은 27,722 바이트에 3.7 ms 였어요. 등의 빛 웅덩이를 가로와 세로 가우스의 곱으로 나눠 한 프레임의 exp 를 1만 번에서 300번 아래로 줄였어요. 굴착기와 클램셸의 움직임은 도우미 하나로 묶었어요. 그렇게 24,734 바이트에 2.0~2.2 ms 가 됐어요. 노브 반응은 K1 Hour 0.25, K2 Speed 0.05, K3 Depth 0.05, K4 Water 0.08 이에요. + +### Marionette + +주제 4 로 무대·공연 갈래의 셋째를 세웠어요. 24판 「다음 판 감」이 적어 둔, 위의 조종대에서 내려온 실에 매달려 춤추는 꼭두각시예요. 위 3분의 1에는 인형사가 기댄 다리 난간과 클립 램프가 있고, 그 아래에서 인형사의 손이 십자 조종대를 쥐어요. 실 일곱이 58~80줄을 내려와 모자까지 50줄 남짓한 인형에 닿아요. 인형은 입자 열넷과 뼈 열아홉, 팽팽할 때만 당기는 실 일곱으로 푸는 verlet 몸이에요. 춤은 조종대가 가는 길만 짜고 관절은 실이 끌어서, 조종대가 옆으로 가면 인형이 실 끝의 진자처럼 늦게 따라와요. `thirds` 는 54/77/79 이고 바뀐 LED 는 72.7/83.5/94.0% 예요. 무대를 검게 지우고 인형과 실, 조종대만 재면 켜진 LED 는 20.5/18.1/10.4% 인데 바뀐 LED 는 67.3/76.8/64.2% 예요. 첫 판의 실은 40 + 120 × 빛에 둘레 20% 로 그려서, 흰 막대 일곱이 철창처럼 인형을 가렸어요. 24 + 64 × 빛에 둘레 12% 로 낮추고 무릎 실을 몸 앞에서 뒤로 옮기자 가는 실로 읽혀요. 앞에서 본 인사는 쪼그려 앉는 것과 같아 보였어요. 그래서 인사하는 동안 조종대를 0.6 라디안 돌려 몸이 비스듬히 숙는 것이 보이게 했어요. 모양 바뀜 0.169 가운데 0.144 는 조명을 고정해도 남아요. 지그가 ±10 LED 를 오가고 팔로 스폿 웅덩이가 따라가서예요. K4 Spot 은 그림자만 옮길 때 0.049 였고, 웅덩이를 스폿 반대쪽으로 11 LED 밀고 45% 넓히자 0.055 가 됐어요. K3 Number 는 순서마다 조명이 달라서 0.19 로 넷 가운데 가장 높아요. 기능을 다 넣은 판은 26,294 바이트였어요. 물리와 무대 반복문을 채널 루프로 묶어 24,887 바이트가 됐어요. + +### Skydiver + +주제 4 로 비행 갈래의 넷째를 세웠어요. 위 3분의 1은 점프기와 구름층이고, 다이버 고리가 그 아래에서 만들어져 가운데 3분의 1에서 캐노피로 피어나요. 아래에는 격납고와 바람 자루, 착지 원이 있는 강하장이 서요. 하늘과 땅을 다 칠해서 `thirds` 는 100/100/99 이고, 2~8초 동안 바뀐 LED 는 45.4/66.1/56.0% 예요. 구름과 그 그림자를 멈춘 사본으로 재도 26.7/66.2/23.7% 라서 다이버와 캐노피가 128줄을 다 써요. 고리가 고리로 읽히도록 깊이를 0.55 배로 눕혀 비스듬히 내려다본 타원으로 그렸어요. 캐노피는 폭 방향만 0.3 배로 눕혀서 옆으로 날 때도 날개꼴로 보여요. 첫 판은 모양 바뀜이 0.029 였어요. 16줄 칸 줄마다 나눠 보니 캐노피와 고리의 몫은 줄마다 0.001 언저리였어요. 13×4 LED 캐노피는 칸 높이의 4분의 1만 덮어요. 구름층 줄은 0.005, 들판의 구름 그림자 줄은 0.0125 였어요. 하늘 가운데를 (34,92,184) 에서 (18,62,148) 로 어둡게 하자 0.034 가 됐어요. 뾰족하던 구름 꼭대기를 둥근 거품을 쌓은 적운으로 바꾸자 거품이 서로 이어져 흰 띠가 됐고 0.030 으로 떨어졌어요. 띠는 옆으로 흘러도 칸 평균이 그대로예요. 128열에 적운을 넷만 두어 틈을 내자 구름층 줄의 몫이 0.0049 에서 0.0142 가 되어 0.045 가 됐어요. 미리 돌리기에서 비행기를 0, 19, 30초에 띄워 둘째 조의 캐노피가 2~8초에 가운데 3분의 1을 지나게 하자 0.047 이 됐어요. 그러자 가운데 바뀐 LED 도 45% 에서 66% 가 됐어요. 씨앗 여덟에서는 0.044~0.052 예요. K3 Divers 를 12 로 두면 펴는 차례가 0.3초씩 벌어져 마지막 캐노피가 95번 줄에서 펴졌어요. 펴는 차례를 조 크기와 상관없이 1.4초 안에 넣고 문을 나서는 간격을 (0.5 + 0.06n)/(n−1) 초로 두자, 다섯에서 열둘까지 모두 53~70번 줄에서 펴요. + +### Population Pyramid + +주제 4 로 데이터 그림 갈래에 넷째를 세웠어요. 패턴 안에서 코호트 모형을 돌려요. 해마다 나이별 사망률로 사람을 덜고, 모두 한 살씩 올리고, 15~49살 여자 수에 출산율을 곱해 0살 줄을 채워요. 한 살이 한 줄이라 0살이 121번 줄, 105살이 16번 줄이에요. 첫 판은 이야기의 첫해인 1820년에서 열렸어요. 그때는 아기 넷에 하나가 첫해에 죽고 기대수명이 30~33년이라 막대가 0~78살에만 있었어요. `thirds` 는 13/20/40 이었지만 위 3분의 1은 연도와 눈금과 라벨뿐이었어요. 세대 라벨 칸을 가리면 그 3분의 1의 바뀐 LED 가 3.3% 였어요. 여는 해를 1995년으로 옮기고 그 앞 175년은 켤 때 보이지 않게 돌리자, 2~8초에 베이비붐 띠가 37~73살에 서고 80~100살 막대가 위 3분의 1에 닿아요. 이제 `thirds` 는 20/59/45 예요. 눈금과 라벨과 연도를 지운 사본으로 재도 위 3분의 1이 켜진 LED 7.3%, 바뀐 LED 8.9% 예요. 모양 바뀜은 1820년대 삼각형에서 0.073, 1995년에서는 0.114 예요. 2060년이 지나면 1820년으로 돌아가 240년을 80초에 다시 지나요. 세대 띠를 처음에는 10년마다 밝게와 어둡게(1, 0.56) 번갈아 칠했어요. 띠 둘이 20줄 주기라 16줄 높이 칸 안에서 평균이 크게 바뀌지 않아서 모양 바뀜이 0.029 였어요. 밝기를 세 단(1, 0.66, 0.4)으로 돌리자 다른 것은 그대로인 채 0.073 이 됐어요. 어둡게 칠한 주황과 노랑은 갈색과 올리브로 보여서, 어두운 두 단은 색상을 초록에서 빨강 사이로만 돌려요. K4 Migration 은 처음에 0.011 이었어요. 젊은 어른을 곱셈으로 늘리면 그만큼 아기도 늘어서, 미리 돌린 모양이 거의 같았거든요. 들어온 사람을 따로 세어 막대 끝을 옅게 칠하고, 떠난 사람은 막대 너머 점선으로 그리자 1820년에서 0.043, 1995년에서 0.094 가 됐어요. + +### Snow Monkeys + +남은 자리로 짐승 갈래의 셋째를 세웠어요. 아래 절반은 온천과 원숭이, 위 절반은 눈 덮인 삼나무 비탈이고, 김이 둘을 잇게 했어요. 원숭이 머리는 깊이마다 8~13 LED 너비이고 바위 위 새끼는 6 LED 예요. 빨간 얼굴을 머리의 아래 가운데에 두고 머리 꼭대기에 눈을 얹었어요. 귀를 머리 옆으로 1 LED 남짓 내밀자 원숭이로 읽혔어요. 첫 판은 K3 Monkeys 가 0.010 으로 검사에 걸렸어요. 3~9 마리에 머리가 6~11 LED 라서, 원숭이가 늘어도 바뀌는 LED 가 적었어요. 원숭이를 1.2 배 키우고 테두리 바위에 앉는 원숭이 둘을 넣자 0.030 이 됐어요. 그다음 털을 (168,146,124) 에서 (218,184,150) 으로 밝혔는데 0.0329 가 0.0333 이 되는 데 그쳤어요. 자리를 열넷으로 늘려 앞줄에 큰 원숭이 둘을 더하자 0.043 이 됐어요. 젖은 털에서 오르는 김은 따로 둔 생성기로 뽑아요. 그래서 원숭이 수가 바뀌어도 나머지 김의 난수 차례는 그대로예요. 모양 바뀜은 첫 판부터 0.095 였고, 지금은 0.115, 씨앗 여덟에서 0.109~0.146 이에요. 이 값은 거의 다 김에서 와요. 김과 눈송이를 끄면 원숭이와 물결만으로 0.024 라서 Galton Board 의 0.025 에도 못 미쳐요. Thermal 처럼 되지 않게 김 뒤의 비탈을 어둡게 두었어요. 김을 빼면 위 60줄의 평균 밝기가 69 이고, 저녁의 김은 240 이에요. 첫 판의 김은 잔 결 잡음이 고르게 덮여 안개처럼 보였어요. 불투명도를 밀도의 부드러운 계단으로 바꾸고 샘 입구 세 곳에서 덩어리를 뿜게 하자 기둥으로 피어오르는 김이 됐어요. 물 가까이에서는 김의 불투명도를 0.4 로 낮춰서 얼굴이 가려지지 않아요. 한 번 일어나는 일은 2~8초 창 안에 두었어요. 어미는 3.0초에 바위에서 내려와 6.0초에 물속 자리에 앉고, 새끼는 6.2초에 뛰어 6.9초에 물에 떨어져요. + +### Ferris Wheel + +남은 자리로 놀이공원 갈래의 넷째를 세웠어요. Drop Tower 뒤에 테두리로만 서던 관람차를 주인공으로 올렸어요. 위 87줄에서 지름 59 LED 바퀴가 40초에 한 바퀴 돌고, 곤돌라는 바퀴 끝에 매달려 늘 똑바로 서요. 아래 41줄은 호수예요. 위 장면을 0.72 로 눌러 비추니 바퀴 축까지 물에 들어오고, 아래 3분의 1의 바뀐 LED 가 80~81% 예요. 살과 테두리는 LED 마다 가장 가까운 살까지의 거리로 그려요. 그래서 살 수가 12에서 32로 바뀌어도 1 LED 줄이 끊기지 않아요. 쇼 여섯 가지는 살마다 24칸, 테두리 120칸에 빛을 칠해요. 바퀴가 제자리에서 도는 것만으로는 4×16 블록이 거의 그대로예요. 연무와 구름을 멈추고 재면 모양 바뀜이 0.045 이고, 쇼의 평균 빛을 하늘 연무와 구름 밑면에 묻히자 0.100 이 됐어요. 6.5초에 어둑한 첫 쇼가 바퀴 전체가 켜지는 색 쓸기로 넘어가는데, 그 순간이 2~8초 창 안에 들어요. 첫 판은 K1 0.02, K4 0.01 이었어요. 첫 쇼가 어두워서 색을 바꿀 빛이 적었고, 바람은 물결만 흔들었거든요. 첫 쇼의 살에 바깥으로 달려 나가는 빛을 넣고 연무를 넓히자 K1 이 0.044 가 됐어요. 바람에 구름과 긴 빛줄기를 묶자 K4 는 0.079 가 됐어요. 연무를 그대로 밝히면 색 쓸기 동안 하늘이 청록으로 물들어서, 밝은 쇼일수록 연무가 빛을 덜 받게 눌렀어요. 그러자 모양 바뀜이 0.176 에서 0.096 으로 내려갔어요. 물결의 시간을 Speed 에 묶자 K2 는 0.045 에서 0.051 이 됐어요. 출렁이는 물에 비친 빛은 위아래 세 줄을 평균하면 세 점으로 갈라졌어요. 다섯 줄 가운데 가장 밝은 값을 쓰자 세로 빛줄기로 이어졌어요. + +### Kintsugi + +남은 자리로 도자기 갈래의 둘째를 세웠어요. 24판의 Potter's Wheel 이 흙을 빚는 일이었다면, 이번에는 구운 사발이 깨지고 금으로 다시 붙는 일이 주인공이에요. 사발은 너비 56 LED 에 52줄이라 판의 27% 를 덮어요. 깨질 때마다 조각 넷에서 여섯이 축에서 옆으로 7 LED 까지 벌어졌다가 6초 만에 닫혀요. 모양 바뀜은 0.136, 씨앗 여덟으로 0.138~0.147 이에요. 이 값은 대부분 6.5초에 한 번 ±10.6 LED 로 흔들리는 종이 등이 내요. 등을 멈추면 0.039~0.053 이고, 깨짐을 멈추면 0.116 이에요. 첫 판은 조각을 사발 가운데에서 바깥으로 밀었어요. 그러자 틈이 짙은 나무뿌리처럼 읽혀서, 옆으로 벌리고 깨진 날을 밝은 태토 색으로 칠했더니 조각으로 읽혔어요. 노브에서 걸린 것은 K3 Mends 였어요. 이음매가 1 LED 금선이라 2..10 의 양 끝이 0.031 이었어요. 선 옆 LED 에 금빛을 30% 묻히면 0.04 까지 올랐지만 사발이 거의 금으로 덮였어요. 그래서 묻힘은 막 붙은 이음매에만 남겼어요. 대신 사발이 품은 금의 양만큼 앞쪽 천에 따뜻한 빛이 지게 해서 0.054 가 됐어요. 깨질 때 잃은 조각 자리에 다른 사발의 조각을 끼우는 요비쓰기와, 금으로 메운 입술의 이 빠짐도 Mends 와 함께 늘어요. K1 Glaze 도 첫 판은 0.04 였어요. 아래를 보는 사발 벽이 거의 검어서였고, 천에서 올라오는 빛을 벽 아래쪽에 더하자 0.057 이 됐어요. 한 가지는 몇 분을 돌려 보고서야 보였어요. 지나온 깨짐은 조각 셋에서 다섯, 새 깨짐은 다섯에서 일곱으로 갈라서, 같은 Mends 에서도 2분 뒤에는 그물이 두 배쯤 촘촘했어요. 둘 다 넷에서 여섯으로 맞추고 범위를 2..8 로 줄였어요. + +### Penrose + +PENROSE_PARA + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -1060,7 +1119,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 24일 `dev`(`b1af21e`)의 138개에 24판의 열 개를 더한 148개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 24일 `dev`(`85baa0d`)의 148개에 25판의 열 개를 더한 158개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -1070,7 +1129,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 촘촘함이 0.299 를 넘는 패턴이 20판에서 셋 늘었고, 셋 다 그 값을 다른 길로 냈어요. Agate 는 1 LED 띠를 벽까지의 거리로 그어서, 축에 나란한 띠는 이웃 쌍의 절반에서만 차이가 나고 대각선 띠는 70% 쯤에서 나요. 방향이 고르게 섞이면 띠 너비 0.85 LED 언저리가 가장 높았어요. Millefiori 는 촘촘함을 가장 밝은 채널로 재서 코발트와 카나리아 노랑이 맞닿아도 턱이 0 이었어요. 고리마다 밝은 유리와 그 유리를 어둡게 뽑은 유리를 번갈아 넣어야 했어요. Sunglint 는 반짝임을 물결 띠마다 체커판의 한쪽 칸에만 둬서 반짝임끼리 닿지 않게 했어요. 셋 다 채움이 0.85 를 넘어서, 촘촘함 × 채움 4,4 는 꺼진 LED 없이도 닿는 칸이었어요. 21판의 Sandpile 이 일곱째예요. 칸 하나가 LED 하나이고 알 셋 옆의 알 둘이 가장 흔한 이웃 쌍이라, 밝기를 0, 2, 1, 3 차례로 올려 더미 안의 촘촘함이 0.40 언저리예요. 판의 75% 는 덮여야 0.3 을 넘어요. 22판의 Pin Art 와 Night Flight 가 여덟째와 아홉째예요. Pin Art 는 핀과 틈의 체커 결이라 이웃 쌍이 모두 핀과 틈의 쌍이고, Night Flight 는 Pancakes 의 체커 결에 1 LED 가로등을 LED 의 12.7% 만큼 얹어서 가장자리 0.258 에 촘촘함 0.328 이에요. 촘촘함과 가장자리는 가장 밝은 채널로 재서, 색을 섞는 자리마다 섞은 뒤 가장 밝은 채널을 다시 맞춰야 했어요. 땅과 강물을 섞은 자리에서 가장 밝은 채널이 0.68 까지 내려가 체커 결의 37 이 41~60 으로 넘어갔고, 그대로 둔 판은 가장자리가 0.05 높았어요. 23판의 Sequins 가 열째예요. 체커 결 없이, 가로 5 LED 와 세로 3 LED 간격으로 엇갈려 단 스팽글의 빛점과 그림자 줄로 0.327 을 냈어요. 촘촘함은 무늬의 모양보다 턱의 크기를 따라가서, 스팽글 몸통을 0.8 에서 0.6 으로 어둡게 하자 은빛 면이 0.339 에서 0.311 로 내려갔어요. -모양 바뀜이 높은 칸을 겨누면 움직이는 것과 바탕의 밝기가 달라야 해요. 3판의 Lava Lamp 는 방울이 화면 높이를 다 오르는데도 왁스와 물이 둘 다 밝아서 모양 바뀜이 0.068 이에요. 4판의 Jacquard Loom 도 천이 1초에 4 LED 씩 내려가는데, 씨실 여섯 색이 모두 밝기 끝까지라 0.041 이에요. 19판의 Rabbit Ears 도 첫 판에서는 그림이 눈 속으로 사라져도 블록 평균이 0.5 쯤으로 같아서 0.153 이었어요. 바다를 어둡게 하고 신호가 없을 때 눈을 밝게 올리자 0.286 이 됐어요. 모양 바뀜의 칸은 4 LED 너비에 16 LED 높이라서, 옆으로 쓸고 가는 것이 위아래로 오르내리는 것보다 네 배 잘 잡혀요. 7판의 Searchlights 가 그렇게 0.357 까지 갔어요. 색이 많아도 모양 바뀜은 밝기만 봐요. 11판의 Balloon Box 는 천의 모든 색을 밝기 끝에 둬서, 모양 바뀜과 가장자리는 풍선과 어두운 하늘의 차이에서만 와요. 16판의 Domino Run 은 꺼지는 쪽에서 모양 바뀜을 얻었어요. 넘어지는 도미노는 같은 4×16 칸 안에서 눕기만 해서 처음에 0.096 이었고, 낮은 램프가 누운 면을 스치게 하자 켜진 LED 가 0.44 에서 0.15 로 줄며 0.261 이 됐어요. 모양 바뀜은 2초 화면에서 멀어진 거리라 6초 동안 바뀐 양을 넘지 못해서, 움직임의 여섯 배가 끝이에요. 16판의 Lunation 은 2~8초 동안 어두워지는 LED 가 하나도 없게 짜서 움직임 0.058 에 모양 바뀜 0.348 로 그 끝에 섰어요. 아래 끝도 끝이에요. 모양 바뀜 축의 아래 끝은 Galton Board 의 0.025 이고, 17판의 열 개 가운데 여섯이 처음 판에 그 아래였어요. Ant Farm 0.010, One Bit 0.014, Marble Run 0.019, Circuit Board 0.021, Crossroads 0.023, Shoreline 0.029 예요. 바탕이 서 있고 작은 것만 움직이면 4×16 칸 평균이 거의 그대로라서, 블록만 한 것이 천천히 바뀌어야 해요. Ant Farm 은 젖은 모래 얼룩, Circuit Board 는 데워지는 선, Marble Run 은 구슬이 지나며 켜는 플라스틱 조각, One Bit 는 오르내리는 안개 둑으로 0.04~0.06 에 섰어요. 18판에서는 Server Rack 의 첫 판이 0.024 였어요. 깜박이는 LED 는 4×16 칸 안에서 평균이 지워져서, 백업이 쓴 드라이브의 빛길을 켜 둔 채 남기고 부하 물결을 넓혀 0.098 이 됐어요. 거꾸로 1초가 안 되는 섬광은 모양 바뀜을 크게 올려요. 모양 바뀜은 첫 화면에서 가장 멀어진 프레임 하나로 정해져서, 18판 Drop Tower 의 0.4초 남짓 보이는 브레이크 섬광이 0.087 을 0.163 으로 올렸어요. 19판에서는 Helter Skelter 의 첫 판이 0.021 이었어요. 전구 체이스가 나선을 따라 내려가며 미끄럼틀과 탑을 밝히고 8초마다 전구가 1.5초 다 켜지게 하자 0.069 가 됐어요. 21판의 Bread Mould 는 곰팡이로 어두워진 빵을 밝은 새 빵이 옆에서 밀어내는 일을 2.4초에 두어 3.5초에 0.356 에 닿았어요. 도마를 늙은 곰팡이와 새 속살 사이의 밝기 100 쯤에 두어서, 빵이 덮는 넓이만 모양 바뀜에 들어가요.아래 끝을 넘을 뻔한 패턴도 있었어요. 21판의 Slinky 는 250 LED 남짓한 스프링이 계단을 내려가기만 할 때 0.014 로 Galton Board 아래였고, 내려앉은 칸마다 계단등이 켜져 8초 남게 하자 0.056 이 됐어요. 22판에서는 아래 끝을 넘은 첫 판이 둘이었어요. Weaver 는 0.017 이었고, 하늘 아닌 곳에 구름 그림자 두 줄을 1초에 3 LED 씩 지나가게 하자 0.044 가 됐어요. Galilean Moons 는 0.022 였고, 도표의 하루를 6줄에서 10줄로 늘리자 0.031 이 됐어요. 높은 쪽은 셈으로 맞출 수 있었어요. Pin Art 는 0.613 × 0.575 = 0.352 가 잰 값 0.357 이고, Flipdot 은 0.36 × (1 − 0.248) 이 잰 값 0.265 예요. 둘 다 바뀐 넓이에 칸 평균의 차이를 곱한 셈이에요. 23판에서는 그 셈을 그림의 차례로 맞췄어요. Sequins 는 2초 화면에 아래 52% 의 파도가 있고 7.6초에 위 33% 의 하트가 있어서 판의 75% 가 바뀌어 0.250 이에요. Bait Ball 은 어두운 등을 보이던 떼가 2.1초부터 은빛으로 돌아서 0.372 이고, 은빛일 때 공 둘레 물까지 밝히는 빛무리가 0.304 를 0.348 로 올렸어요. 씨앗마다 0.351~0.400 으로 흔들리던 값은 2초 화면에 사냥꾼이 있느냐로 갈렸고, 첫 돌진 시각을 고정하자 0.367~0.377 로 좁아졌어요. 거꾸로 Elephant Toothpaste 는 밝은 기둥이 어두운 판을 위아래로 다 가로질러도 패널 너비의 40% 쯤만 덮어서 0.102 에 그쳤어요. 24판에서는 하늘이 모양 바뀜을 가렸어요. Thermal 은 적운이 자라는데도 첫 판이 0.021 이었어요. 가장 밝은 채널로 보면 200~222 의 파란 하늘과 흰 구름이 거의 같아서, 천정을 (14,50,150) 으로 어둡게 하자 구름이 자라는 위의 16줄 띠 둘이 0.0066 에서 0.0207 이 됐어요. Pendulum Wave 는 색을 올리려고 바닥을 밝히자 추와 바닥의 차이가 줄어 모양 바뀜이 0.039 에서 0.023 으로 떨어졌어요. Shoot 'em Up 은 보스의 마지막 폭발 섬광 하나가 8초 창 안에 들면 0.30, 밖이면 0.09~0.13 이에요. +모양 바뀜이 높은 칸을 겨누면 움직이는 것과 바탕의 밝기가 달라야 해요. 3판의 Lava Lamp 는 방울이 화면 높이를 다 오르는데도 왁스와 물이 둘 다 밝아서 모양 바뀜이 0.068 이에요. 4판의 Jacquard Loom 도 천이 1초에 4 LED 씩 내려가는데, 씨실 여섯 색이 모두 밝기 끝까지라 0.041 이에요. 19판의 Rabbit Ears 도 첫 판에서는 그림이 눈 속으로 사라져도 블록 평균이 0.5 쯤으로 같아서 0.153 이었어요. 바다를 어둡게 하고 신호가 없을 때 눈을 밝게 올리자 0.286 이 됐어요. 모양 바뀜의 칸은 4 LED 너비에 16 LED 높이라서, 옆으로 쓸고 가는 것이 위아래로 오르내리는 것보다 네 배 잘 잡혀요. 7판의 Searchlights 가 그렇게 0.357 까지 갔어요. 색이 많아도 모양 바뀜은 밝기만 봐요. 11판의 Balloon Box 는 천의 모든 색을 밝기 끝에 둬서, 모양 바뀜과 가장자리는 풍선과 어두운 하늘의 차이에서만 와요. 16판의 Domino Run 은 꺼지는 쪽에서 모양 바뀜을 얻었어요. 넘어지는 도미노는 같은 4×16 칸 안에서 눕기만 해서 처음에 0.096 이었고, 낮은 램프가 누운 면을 스치게 하자 켜진 LED 가 0.44 에서 0.15 로 줄며 0.261 이 됐어요. 모양 바뀜은 2초 화면에서 멀어진 거리라 6초 동안 바뀐 양을 넘지 못해서, 움직임의 여섯 배가 끝이에요. 16판의 Lunation 은 2~8초 동안 어두워지는 LED 가 하나도 없게 짜서 움직임 0.058 에 모양 바뀜 0.348 로 그 끝에 섰어요. 아래 끝도 끝이에요. 모양 바뀜 축의 아래 끝은 Galton Board 의 0.025 이고, 17판의 열 개 가운데 여섯이 처음 판에 그 아래였어요. Ant Farm 0.010, One Bit 0.014, Marble Run 0.019, Circuit Board 0.021, Crossroads 0.023, Shoreline 0.029 예요. 바탕이 서 있고 작은 것만 움직이면 4×16 칸 평균이 거의 그대로라서, 블록만 한 것이 천천히 바뀌어야 해요. Ant Farm 은 젖은 모래 얼룩, Circuit Board 는 데워지는 선, Marble Run 은 구슬이 지나며 켜는 플라스틱 조각, One Bit 는 오르내리는 안개 둑으로 0.04~0.06 에 섰어요. 18판에서는 Server Rack 의 첫 판이 0.024 였어요. 깜박이는 LED 는 4×16 칸 안에서 평균이 지워져서, 백업이 쓴 드라이브의 빛길을 켜 둔 채 남기고 부하 물결을 넓혀 0.098 이 됐어요. 거꾸로 1초가 안 되는 섬광은 모양 바뀜을 크게 올려요. 모양 바뀜은 첫 화면에서 가장 멀어진 프레임 하나로 정해져서, 18판 Drop Tower 의 0.4초 남짓 보이는 브레이크 섬광이 0.087 을 0.163 으로 올렸어요. 19판에서는 Helter Skelter 의 첫 판이 0.021 이었어요. 전구 체이스가 나선을 따라 내려가며 미끄럼틀과 탑을 밝히고 8초마다 전구가 1.5초 다 켜지게 하자 0.069 가 됐어요. 21판의 Bread Mould 는 곰팡이로 어두워진 빵을 밝은 새 빵이 옆에서 밀어내는 일을 2.4초에 두어 3.5초에 0.356 에 닿았어요. 도마를 늙은 곰팡이와 새 속살 사이의 밝기 100 쯤에 두어서, 빵이 덮는 넓이만 모양 바뀜에 들어가요.아래 끝을 넘을 뻔한 패턴도 있었어요. 21판의 Slinky 는 250 LED 남짓한 스프링이 계단을 내려가기만 할 때 0.014 로 Galton Board 아래였고, 내려앉은 칸마다 계단등이 켜져 8초 남게 하자 0.056 이 됐어요. 22판에서는 아래 끝을 넘은 첫 판이 둘이었어요. Weaver 는 0.017 이었고, 하늘 아닌 곳에 구름 그림자 두 줄을 1초에 3 LED 씩 지나가게 하자 0.044 가 됐어요. Galilean Moons 는 0.022 였고, 도표의 하루를 6줄에서 10줄로 늘리자 0.031 이 됐어요. 높은 쪽은 셈으로 맞출 수 있었어요. Pin Art 는 0.613 × 0.575 = 0.352 가 잰 값 0.357 이고, Flipdot 은 0.36 × (1 − 0.248) 이 잰 값 0.265 예요. 둘 다 바뀐 넓이에 칸 평균의 차이를 곱한 셈이에요. 23판에서는 그 셈을 그림의 차례로 맞췄어요. Sequins 는 2초 화면에 아래 52% 의 파도가 있고 7.6초에 위 33% 의 하트가 있어서 판의 75% 가 바뀌어 0.250 이에요. Bait Ball 은 어두운 등을 보이던 떼가 2.1초부터 은빛으로 돌아서 0.372 이고, 은빛일 때 공 둘레 물까지 밝히는 빛무리가 0.304 를 0.348 로 올렸어요. 씨앗마다 0.351~0.400 으로 흔들리던 값은 2초 화면에 사냥꾼이 있느냐로 갈렸고, 첫 돌진 시각을 고정하자 0.367~0.377 로 좁아졌어요. 거꾸로 Elephant Toothpaste 는 밝은 기둥이 어두운 판을 위아래로 다 가로질러도 패널 너비의 40% 쯤만 덮어서 0.102 에 그쳤어요. 24판에서는 하늘이 모양 바뀜을 가렸어요. Thermal 은 적운이 자라는데도 첫 판이 0.021 이었어요. 가장 밝은 채널로 보면 200~222 의 파란 하늘과 흰 구름이 거의 같아서, 천정을 (14,50,150) 으로 어둡게 하자 구름이 자라는 위의 16줄 띠 둘이 0.0066 에서 0.0207 이 됐어요. Pendulum Wave 는 색을 올리려고 바닥을 밝히자 추와 바닥의 차이가 줄어 모양 바뀜이 0.039 에서 0.023 으로 떨어졌어요. Shoot 'em Up 은 보스의 마지막 폭발 섬광 하나가 8초 창 안에 들면 0.30, 밖이면 0.09~0.13 이에요. 25판에서는 서 있는 그림이 넓은 빛 하나로 모양 바뀜을 얻었어요. Excavation 은 굴착기와 클램셸만 움직일 때 0.013 이었고, 데크 틈으로 드는 햇빛 기둥이 2초의 구름 그늘(틈 아래 평균 0.52)에서 8초에 0.97 로 개게 하자 0.054 가 됐어요. Kintsugi 는 6.5초에 한 번 흔들리는 종이 등의 빛무리만으로 0.116 이라서, 사발이 깨지는 몫은 등을 멈춘 사본으로 따로 재야 보였어요. 그 사본은 0.039~0.053 이에요. Snow Monkeys 도 김을 끄면 0.115 가 0.024 로 떨어져요. 김 뒤의 비탈을 평균 밝기 69 로 어둡게 두어서 240 의 김이 블록을 바꿔요. 이어진 구름층은 흘러도 모양 바뀜을 내지 않아요. Skydiver 는 적운이 서로 이어져 흰 띠가 되자 0.030 이었고, 128열에 적운을 넷만 두어 틈을 내자 구름층이 지나는 16줄 칸 줄의 몫이 0.0049 에서 0.0142 가 되어 0.045 가 됐어요. 제자리에서 도는 원도 거의 내지 않아요. Ferris Wheel 은 연무와 구름을 멈추면 0.045 였고, 쇼의 평균 빛을 하늘 연무에 묻히자 0.100 이 됐어요. 띠가 위아래로 흐르는 그림은 밝기의 주기가 값을 정했어요. Population Pyramid 의 10줄 세대 띠를 두 단 밝기로 번갈아 칠하면 20줄 주기라 16줄 칸 안의 평균이 거의 그대로여서 0.029 였어요. 세 단으로 30줄 주기를 만들자 다른 것은 그대로인 채 0.073 이 됐어요. Rice Terraces 는 하늘을 비추는 논이 판의 70% 쯤이라 바탕이 모양 바뀜을 냈어요. 논이 지평선 쪽 주황만 비출 때는 0.023 이었고, 가까운 논이 짙은 파랑의 머리 위 하늘을 비추게 하자 구름 반사가 블록만 하게 지나가며 0.063 이 됐어요. 움직임 축의 아래 끝도 끝이에요. 아래 끝은 Star Trails 의 0.019 이고, 18판의 Stained Glass 는 첫 판이 0.007, Peg Light 는 0.023 이었어요. 블록만 한 구름 그림자가 1초에 5 LED 로 지나가면 모양 바뀜은 0.036 이어도 1초 사이에 바뀌는 빛은 작아요. Stained Glass 는 그림자를 11 LED 덩어리로 얼룩지게 하고 1초에 18 LED 로 지나가게 해서 0.027 이 됐어요. Peg Light 는 1초에 페그 100개를 꽂아도 LED 하나가 한 번 바뀌는 셈이라 그 몫이 0.012 였고, 켜진 페그마다 ±3.5% 로 1.4 Hz 에 일렁이게 해서 0.035 가 됐어요. @@ -1080,19 +1139,19 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ### 아직 없는 갈래 -`families.json` 의 마흔여섯 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 24판이 연 공사·건설 갈래는 Tower Crane 하나예요. 굴착기가 땅을 파 내려가는 터파기 현장은 아래로 깊이를 써요. 24판이 연 도자기 갈래도 Potter's Wheel 하나예요. 장작 가마의 불구멍으로 불꽃이 새는 소성은 아직 없어요. 23판이 연 무대·공연 갈래는 Proscenium 과 24판의 Flying Trapeze 예요. 위의 조종대에서 내려온 줄에 매달려 춤추는 꼭두각시는 128줄을 다 써요. 23판이 연 짐승 갈래는 Giraffe 와 24판의 Emergence 예요. 22판이 연 몸 갈래는 Circulation 과 23판의 Axon, 24판의 Breath 예요. 22판이 연 비행 갈래는 Liftoff 와 23판의 Final Approach, 24판의 Thermal 이에요. 낙하산을 펴고 내려오는 스카이다이버는 아직 없어요. 20판이 연 새 갈래는 Kingfisher 와 21판의 Woodpecker, 22판의 Weaver, 23판의 Skein 이에요. 20판이 연 화학·실험 갈래는 Chromatography 와 21판의 Chemical Garden, 22판의 Flame Test, 23판의 Elephant Toothpaste 예요. 21판이 연 종이 갈래는 Pop-up Book 과 22판의 Senbazuru, 23판의 Hanging Scroll 이에요. 접힌 종이학이 펼쳐지는 그림은 아직 없어요. 21판이 연 운동·경기 갈래는 Lap Pool 과 22판의 Bowling Lane, 23판의 High Dive 예요. 위아래로 움직이는 경기는 High Dive 가 했고, 장대높이뛰기와 실내 암벽은 아직 없어요. 19판이 연 부엌 갈래는 Popcorn 과 20판의 Kettle, 21판의 Pancakes 이고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판이 연 유리 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori 예요. 광물·결정 갈래는 Bismuth 와 19판의 Dripstone, 20판의 Agate 이고, 놀이공원 갈래는 Drop Tower 와 19판의 Helter Skelter, 20판의 High Striker 예요. 관람차는 Drop Tower 뒤에 테두리로만 서요. 17판이 연 전자 회로 갈래는 Circuit Board 와 18판의 Server Rack, 21판의 Nixie 이고, 장난감 갈래는 Marble Run 과 18판의 Peg Light, 21판의 Slinky, 22판의 Pin Art 예요. 16판이 연 바다 갈래는 Abyss 와 17판의 Shoreline, 20판의 Sunglint, 23판의 Bait Ball 이고, 곤충 갈래는 Fireflies 와 17판의 Ant Farm, 20판의 Moth Lamp, 24판의 Waggle Dance 예요. 15판이 연 도시 갈래는 Metro Map 과 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판이 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables, 24판의 Pipe Organ 이에요. 4판이 연 직물 갈래는 Jacquard Loom 과 15판의 Plait, 19판의 Needlepoint, 23판의 Sequins 이고, 8판이 연 기계 갈래는 Gear Train 과 15판의 Cam Wave, 19판의 Paternoster 예요. 12판이 연 천체 갈래는 Star Trails 와 15판의 Analemma, 16판의 Lunation, 22판의 Galilean Moons 예요. 위성의 궤도는 Galilean Moons 가 그렸고, 해를 도는 행성은 아직 없어요. 리듬·펄스 갈래는 1판의 Euclid Pulse 와 Pulse Bars 뒤로 20판의 Change Ringing 이 셋째예요. +`families.json` 의 마흔여덟 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 25판이 연 농사·밭 갈래는 Rice Terraces 하나이고, 화산 갈래는 Eruption 하나예요. 모를 심는 사람들이 줄지어 물러나는 모내기는 아직 없어요. 24판이 연 공사·건설 갈래는 Tower Crane 과 25판의 Excavation 이에요. 24판이 연 도자기 갈래는 Potter's Wheel 과 25판의 Kintsugi 예요. 장작 가마의 불구멍으로 불꽃이 새는 소성은 아직 없어요. 23판이 연 무대·공연 갈래는 Proscenium 과 24판의 Flying Trapeze, 25판의 Marionette 예요. 막 뒤에서 빛을 받는 그림자 인형극은 아직 없어요. 23판이 연 짐승 갈래는 Giraffe 와 24판의 Emergence, 25판의 Snow Monkeys 예요. 22판이 연 몸 갈래는 Circulation 과 23판의 Axon, 24판의 Breath 예요. 22판이 연 비행 갈래는 Liftoff 와 23판의 Final Approach, 24판의 Thermal, 25판의 Skydiver 예요. 20판이 연 새 갈래는 Kingfisher 와 21판의 Woodpecker, 22판의 Weaver, 23판의 Skein 이에요. 20판이 연 화학·실험 갈래는 Chromatography 와 21판의 Chemical Garden, 22판의 Flame Test, 23판의 Elephant Toothpaste 예요. 21판이 연 종이 갈래는 Pop-up Book 과 22판의 Senbazuru, 23판의 Hanging Scroll 이에요. 접힌 종이학이 펼쳐지는 그림은 아직 없어요. 21판이 연 운동·경기 갈래는 Lap Pool 과 22판의 Bowling Lane, 23판의 High Dive 예요. 위아래로 움직이는 경기는 High Dive 가 했고, 장대높이뛰기와 실내 암벽은 아직 없어요. 19판이 연 부엌 갈래는 Popcorn 과 20판의 Kettle, 21판의 Pancakes 이고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판이 연 유리 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori 예요. 광물·결정 갈래는 Bismuth 와 19판의 Dripstone, 20판의 Agate 이고, 놀이공원 갈래는 Drop Tower 와 19판의 Helter Skelter, 20판의 High Striker, 25판의 Ferris Wheel 이에요. 말이 오르내리는 회전목마는 아직 없어요. 17판이 연 전자 회로 갈래는 Circuit Board 와 18판의 Server Rack, 21판의 Nixie 이고, 장난감 갈래는 Marble Run 과 18판의 Peg Light, 21판의 Slinky, 22판의 Pin Art 예요. 16판이 연 바다 갈래는 Abyss 와 17판의 Shoreline, 20판의 Sunglint, 23판의 Bait Ball 이고, 곤충 갈래는 Fireflies 와 17판의 Ant Farm, 20판의 Moth Lamp, 24판의 Waggle Dance 예요. 15판이 연 도시 갈래는 Metro Map 과 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판이 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables, 24판의 Pipe Organ 이에요. 4판이 연 직물 갈래는 Jacquard Loom 과 15판의 Plait, 19판의 Needlepoint, 23판의 Sequins 이고, 8판이 연 기계 갈래는 Gear Train 과 15판의 Cam Wave, 19판의 Paternoster 예요. 12판이 연 천체 갈래는 Star Trails 와 15판의 Analemma, 16판의 Lunation, 22판의 Galilean Moons 예요. 위성의 궤도는 Galilean Moons 가 그렸고, 해를 도는 행성은 아직 없어요. 리듬·펄스 갈래는 1판의 Euclid Pulse 와 Pulse Bars 뒤로 20판의 Change Ringing 이 셋째예요. ### 멋진 것 더 멋지게 이제는 창작자가 댓글로 고른 패턴만 고쳐요. 고를 때 볼 잰 값이에요. -노브 반응이 두 자리로 0.03 인 노브는 23판까지 열하나예요. 1판과 2판의 넷은 Hourglass 의 K2 Flow 가 0.029 로 가장 낮고, 같은 패턴의 K4 Layers 가 0.030 이에요. Flip Clock 의 K4 Light 가 0.032, Sparks 의 K1 Spread 가 0.034 예요. 13판의 Glyph Rain 은 K1 Hue 가 0.028 에서 0.066 으로, K4 Mutate 가 0.033 에서 0.066 으로 올랐어요. 판이 거의 빈 패턴은 노브가 바꿀 LED 가 적어서 노브가 다 낮게 나와요. Glyph Rain 은 채움이 0.059 에서 0.215 가 되자 노브 넷이 다 올랐어요. 9판의 Sprout 은 K1 Hue 가 0.045 로 올랐어요. 5판의 Euclid Pulse 는 K4 Steps 가 0.05 로 넷 가운데 가장 낮아요. 처음 자리의 Pulses 3 에서는 Steps 가 빛 조각의 폭만 바꿔요. 15판의 열 개는 가장 낮은 노브가 Ivy 의 K2 Speed 0.049 예요. 빠르기 노브는 새로 시작한 그림의 2~3초를 봐서, 빠르기만 바꾸는 노브는 낮게 나와요. 1 LED 너비 선이 주인공인 패턴은 선 옆 1 LED 에 그 빛을 옅게 묻히면 노브 반응이 두 배쯤 돼요. 15판의 Player Piano 는 그렇게 노브 넷이 0.02 언저리에서 0.05~0.08 로 올랐어요. 16판의 열 개는 가장 낮은 노브가 Fireflies 의 K4 Coupling 0.04 예요. 대부분이 어두운 그림은 노브 반응이 화면 평균 빛의 두 배쯤에서 멈춰요. Fireflies 는 평균 밝기가 0.03 언저리이고, 16판의 Wireworld 는 전자가 더하는 빛이 프레임 차이로 0.046 이라 K3 Pulses 가 0.05 예요. 노브 하나가 바탕을 바꾸게 하면 쉽게 올라가서, Fireflies 의 K1 Moon 은 0.08 이에요. 17판의 Frost 도 K1 Lamp 가 가로등 둘레의 서리만 물들일 때는 0.01~0.02 였고, 하늘 아래쪽과 눈 덮인 길까지 물들이자 0.05 가 됐어요. 17판의 열 개는 가장 낮은 노브가 Crossroads 의 K2 Pace 0.04 예요. 노브 검사는 다섯 설정을 같은 씨앗으로 돌려서, 노브가 붓질 수나 개미 수를 바꾸면 난수 차례가 갈려 반응이 부풀어요. 17판 Brushstrokes 의 K2~K4 와 Ant Farm 의 K3 에는 그 몫도 들어 있어요. 18판의 열 개는 가장 낮은 노브가 Server Rack 과 Turntables 의 K1~K3 으로 0.04 언저리예요. 큰 물건 하나에서 작은 부분만 움직이는 노브는 낮게 나와요. 18판 Longcase Clock 의 K3 은 처음에 추의 진폭을 바꾸는 Swing 이었는데 0.0036 으로 검사에 걸렸고, 촛불 자리를 옮기는 Candle 로 바꾸자 0.11 이 됐어요. 미리 돌리기로 처음 상태를 맞출 때는 빠르기 노브를 같이 봐야 해요. Server Rack 은 어느 빠르기에서든 2초에 같은 상태가 오게 맞췄다가 K2 가 0.0091 로 걸렸어요. 맞추는 시점은 패턴 시간에 두고 미리 돌리기는 노브 빠르기로 돌려야 빠르기마다 그림이 갈려요. 19판의 열 개는 가장 낮은 노브가 Foxfire 의 K1 Glow 와 Paternoster 의 K4 Busy 로 0.033 이에요. 패널의 10분의 1쯤 덮는 것의 색만 바꾸는 노브는 0.03 에 못 미쳐요. 19판 Popcorn 의 K4 는 처음에 버터 색을 바꾸는 Butter 였는데 0.012~0.02 여서, 더미 높이를 정하는 Heap 으로 바꿨어요. Dust Motes 의 Dust 는 먼지 수만 바꿀 때 0.011 이었고, 빛줄기 안개를 먼지 양에 묶자 0.04 가 됐어요. 노브 반응은 세 채널의 차이를 평균해요. Helter Skelter 의 빨강과 크림 줄무늬는 차이가 초록과 파랑에만 있어서, 탑이 눈에 띄게 비틀려도 0.02 였어요. 20판의 열 개는 가장 낮은 노브가 Kingfisher 의 K3 Fish 와 Kettle 의 K3 Fill 로 둘 다 0.040 이에요. 물고기 수나 물 높이만 바꿀 때는 0.012 와 0.038 이었고, Kingfisher 는 물풀과 갈대가 함께 늘게, Kettle 은 물이 많을수록 늦게 끓게 해서 올렸어요. 21판의 열 개는 가장 낮은 노브가 Woodpecker 의 K2 Speed 와 Lap Pool 의 K1 Stroke 로 둘 다 0.044 예요. 사람 넷의 영법만 바꿀 때 Stroke 는 0.02 였고, 평영과 접영이 바닥의 햇빛 무늬에 짧은 물결을 섞게 해서 올렸어요. 주인공 뒤에 서는 것을 더하는 노브는 가려져서 낮게 나와요. Pop-up Book 의 K3 Layers 는 뒤에 카드를 더할 때 0.01 이었고, 여섯째 층을 맨 앞에 서는 터널북 틀로 바꾸자 0.05 가 됐어요. Fiddlehead 의 K3 Fronds 도 늘어난 잎이 가운데 포기 뒤에서 자랄 때 0.02~0.03 이었고, 양옆 포기의 잎을 가운데 위로 기울게 하자 0.047 이 됐어요. 22판의 열 개는 가장 낮은 노브가 Circulation 의 K3 Detail 0.025 이고, 이 연구에서 두 자리로 0.02 인 노브는 이것 하나예요. 굵은 줄기와 첫 곁가지가 처음 자리에서 몸 LED 2618개 가운데 1943개를 덮어서 처음에 0.008 이었어요. 세운 패널에 사람 하나를 세우면 몸이 판의 32% 쯤이라, 몸 안에서만 바뀌는 노브는 화면 평균으로 나뉘어 낮게 나와요. 22판 Bowling Lane 의 K2 Speed 와 K3 Hook 은 0.03 이에요. 노브 검사가 보는 2~3초 무렵에 공의 길이 Hook 마다 화살표 언저리에서 만나서, 길만 바꿀 때 K3 는 0.01 이었어요. 빛나는 오일의 길이를 Hook 에 묶어 늘 보이는 결과를 하나 붙이자 올랐어요. 패널의 4분의 1만 바꾸는 노브는 그 안을 흐려도 0.04 언저리에서 멈춰요. Galilean Moons 의 K4 Seeing 은 접안렌즈 안만 흐릴 때 0.039 였고, 원반 둘레로 번지는 빛을 넓히자 0.054 가 됐어요. 23판의 열 개는 가장 낮은 노브가 Hanging Scroll 의 K4 Ink 0.032 와 Skein 의 K3 Geese 0.033 이에요. Ink 는 젖은 먹을 흐림으로만 그릴 때 0.022 였어요. 흐림은 눈에는 크게 달라 보여도 LED 하나하나의 밝기를 조금씩만 바꿔요. 넓은 면이 조금 바뀌는 쪽이 노브 반응을 올려서, Giraffe 의 K4 Wind 는 풀과 수관만 흔들 때 0.03 이었고 낮은 하늘에 먼지 막을 덮자 0.06 이 됐어요. 24판의 열 개는 가장 낮은 노브가 Potter's Wheel 의 K2 Speed 0.042 와 Pipe Organ 의 K4 Tremulant 0.042 예요. Breath 는 첫 판부터 노브 넷이 0.09~0.13 이었어요. 숨을 내쉰 폐가 판의 42% 를 덮고 노브마다 폐 전체의 색이나 크기를 바꿔서, 22판 Circulation 처럼 몸 안에서만 바뀌는 노브가 낮게 나오는 일이 없었어요. 첫 3초에 일어나지 않는 일만 바꾸는 노브는 0 에 가까워요. Shoot 'em Up 의 K4 Bullets 는 보스 탄이 3.6초부터 날아서 처음에 0.0041 이었고, 탄이 짙을수록 바닥을 어둡게 하자 0.10 이 됐어요. 빛이 이웃에 번지면 노브가 올라요. Pipe Organ 의 K1 Stops 는 소리 나는 파이프만 물들일 때 0.010 이었고, 그 빛이 좌우 16열의 주석에 비치게 하자 0.02, 리드 음전에 배음 파이프를 더하자 0.062 가 됐어요. +노브 반응이 두 자리로 0.03 인 노브는 23판까지 열하나예요. 1판과 2판의 넷은 Hourglass 의 K2 Flow 가 0.029 로 가장 낮고, 같은 패턴의 K4 Layers 가 0.030 이에요. Flip Clock 의 K4 Light 가 0.032, Sparks 의 K1 Spread 가 0.034 예요. 13판의 Glyph Rain 은 K1 Hue 가 0.028 에서 0.066 으로, K4 Mutate 가 0.033 에서 0.066 으로 올랐어요. 판이 거의 빈 패턴은 노브가 바꿀 LED 가 적어서 노브가 다 낮게 나와요. Glyph Rain 은 채움이 0.059 에서 0.215 가 되자 노브 넷이 다 올랐어요. 9판의 Sprout 은 K1 Hue 가 0.045 로 올랐어요. 5판의 Euclid Pulse 는 K4 Steps 가 0.05 로 넷 가운데 가장 낮아요. 처음 자리의 Pulses 3 에서는 Steps 가 빛 조각의 폭만 바꿔요. 15판의 열 개는 가장 낮은 노브가 Ivy 의 K2 Speed 0.049 예요. 빠르기 노브는 새로 시작한 그림의 2~3초를 봐서, 빠르기만 바꾸는 노브는 낮게 나와요. 1 LED 너비 선이 주인공인 패턴은 선 옆 1 LED 에 그 빛을 옅게 묻히면 노브 반응이 두 배쯤 돼요. 15판의 Player Piano 는 그렇게 노브 넷이 0.02 언저리에서 0.05~0.08 로 올랐어요. 16판의 열 개는 가장 낮은 노브가 Fireflies 의 K4 Coupling 0.04 예요. 대부분이 어두운 그림은 노브 반응이 화면 평균 빛의 두 배쯤에서 멈춰요. Fireflies 는 평균 밝기가 0.03 언저리이고, 16판의 Wireworld 는 전자가 더하는 빛이 프레임 차이로 0.046 이라 K3 Pulses 가 0.05 예요. 노브 하나가 바탕을 바꾸게 하면 쉽게 올라가서, Fireflies 의 K1 Moon 은 0.08 이에요. 17판의 Frost 도 K1 Lamp 가 가로등 둘레의 서리만 물들일 때는 0.01~0.02 였고, 하늘 아래쪽과 눈 덮인 길까지 물들이자 0.05 가 됐어요. 17판의 열 개는 가장 낮은 노브가 Crossroads 의 K2 Pace 0.04 예요. 노브 검사는 다섯 설정을 같은 씨앗으로 돌려서, 노브가 붓질 수나 개미 수를 바꾸면 난수 차례가 갈려 반응이 부풀어요. 17판 Brushstrokes 의 K2~K4 와 Ant Farm 의 K3 에는 그 몫도 들어 있어요. 18판의 열 개는 가장 낮은 노브가 Server Rack 과 Turntables 의 K1~K3 으로 0.04 언저리예요. 큰 물건 하나에서 작은 부분만 움직이는 노브는 낮게 나와요. 18판 Longcase Clock 의 K3 은 처음에 추의 진폭을 바꾸는 Swing 이었는데 0.0036 으로 검사에 걸렸고, 촛불 자리를 옮기는 Candle 로 바꾸자 0.11 이 됐어요. 미리 돌리기로 처음 상태를 맞출 때는 빠르기 노브를 같이 봐야 해요. Server Rack 은 어느 빠르기에서든 2초에 같은 상태가 오게 맞췄다가 K2 가 0.0091 로 걸렸어요. 맞추는 시점은 패턴 시간에 두고 미리 돌리기는 노브 빠르기로 돌려야 빠르기마다 그림이 갈려요. 19판의 열 개는 가장 낮은 노브가 Foxfire 의 K1 Glow 와 Paternoster 의 K4 Busy 로 0.033 이에요. 패널의 10분의 1쯤 덮는 것의 색만 바꾸는 노브는 0.03 에 못 미쳐요. 19판 Popcorn 의 K4 는 처음에 버터 색을 바꾸는 Butter 였는데 0.012~0.02 여서, 더미 높이를 정하는 Heap 으로 바꿨어요. Dust Motes 의 Dust 는 먼지 수만 바꿀 때 0.011 이었고, 빛줄기 안개를 먼지 양에 묶자 0.04 가 됐어요. 노브 반응은 세 채널의 차이를 평균해요. Helter Skelter 의 빨강과 크림 줄무늬는 차이가 초록과 파랑에만 있어서, 탑이 눈에 띄게 비틀려도 0.02 였어요. 20판의 열 개는 가장 낮은 노브가 Kingfisher 의 K3 Fish 와 Kettle 의 K3 Fill 로 둘 다 0.040 이에요. 물고기 수나 물 높이만 바꿀 때는 0.012 와 0.038 이었고, Kingfisher 는 물풀과 갈대가 함께 늘게, Kettle 은 물이 많을수록 늦게 끓게 해서 올렸어요. 21판의 열 개는 가장 낮은 노브가 Woodpecker 의 K2 Speed 와 Lap Pool 의 K1 Stroke 로 둘 다 0.044 예요. 사람 넷의 영법만 바꿀 때 Stroke 는 0.02 였고, 평영과 접영이 바닥의 햇빛 무늬에 짧은 물결을 섞게 해서 올렸어요. 주인공 뒤에 서는 것을 더하는 노브는 가려져서 낮게 나와요. Pop-up Book 의 K3 Layers 는 뒤에 카드를 더할 때 0.01 이었고, 여섯째 층을 맨 앞에 서는 터널북 틀로 바꾸자 0.05 가 됐어요. Fiddlehead 의 K3 Fronds 도 늘어난 잎이 가운데 포기 뒤에서 자랄 때 0.02~0.03 이었고, 양옆 포기의 잎을 가운데 위로 기울게 하자 0.047 이 됐어요. 22판의 열 개는 가장 낮은 노브가 Circulation 의 K3 Detail 0.025 이고, 이 연구에서 두 자리로 0.02 인 노브는 이것 하나예요. 굵은 줄기와 첫 곁가지가 처음 자리에서 몸 LED 2618개 가운데 1943개를 덮어서 처음에 0.008 이었어요. 세운 패널에 사람 하나를 세우면 몸이 판의 32% 쯤이라, 몸 안에서만 바뀌는 노브는 화면 평균으로 나뉘어 낮게 나와요. 22판 Bowling Lane 의 K2 Speed 와 K3 Hook 은 0.03 이에요. 노브 검사가 보는 2~3초 무렵에 공의 길이 Hook 마다 화살표 언저리에서 만나서, 길만 바꿀 때 K3 는 0.01 이었어요. 빛나는 오일의 길이를 Hook 에 묶어 늘 보이는 결과를 하나 붙이자 올랐어요. 패널의 4분의 1만 바꾸는 노브는 그 안을 흐려도 0.04 언저리에서 멈춰요. Galilean Moons 의 K4 Seeing 은 접안렌즈 안만 흐릴 때 0.039 였고, 원반 둘레로 번지는 빛을 넓히자 0.054 가 됐어요. 23판의 열 개는 가장 낮은 노브가 Hanging Scroll 의 K4 Ink 0.032 와 Skein 의 K3 Geese 0.033 이에요. Ink 는 젖은 먹을 흐림으로만 그릴 때 0.022 였어요. 흐림은 눈에는 크게 달라 보여도 LED 하나하나의 밝기를 조금씩만 바꿔요. 넓은 면이 조금 바뀌는 쪽이 노브 반응을 올려서, Giraffe 의 K4 Wind 는 풀과 수관만 흔들 때 0.03 이었고 낮은 하늘에 먼지 막을 덮자 0.06 이 됐어요. 24판의 열 개는 가장 낮은 노브가 Potter's Wheel 의 K2 Speed 0.042 와 Pipe Organ 의 K4 Tremulant 0.042 예요. Breath 는 첫 판부터 노브 넷이 0.09~0.13 이었어요. 숨을 내쉰 폐가 판의 42% 를 덮고 노브마다 폐 전체의 색이나 크기를 바꿔서, 22판 Circulation 처럼 몸 안에서만 바뀌는 노브가 낮게 나오는 일이 없었어요. 첫 3초에 일어나지 않는 일만 바꾸는 노브는 0 에 가까워요. Shoot 'em Up 의 K4 Bullets 는 보스 탄이 3.6초부터 날아서 처음에 0.0041 이었고, 탄이 짙을수록 바닥을 어둡게 하자 0.10 이 됐어요. 빛이 이웃에 번지면 노브가 올라요. Pipe Organ 의 K1 Stops 는 소리 나는 파이프만 물들일 때 0.010 이었고, 그 빛이 좌우 16열의 주석에 비치게 하자 0.02, 리드 음전에 배음 파이프를 더하자 0.062 가 됐어요. 25판의 열 개는 가장 낮은 노브가 Snow Monkeys 의 K3 Monkeys 0.043 과 Ferris Wheel 의 K1 Lights 0.044 예요. PENROSE_KNOB수를 세는 노브는 더하는 것의 밝기보다 넓이를 따라가요. Monkeys 는 처음에 0.010 이었고, 원숭이 털을 (168,146,124) 에서 (218,184,150) 으로 밝혀도 0.0004 만 올랐어요. 앞줄에 1.4 배 큰 원숭이 둘을 더하자 0.043 이 됐어요. 1 LED 선이 주인공인 노브는 선이 바닥에 던지는 빛으로도 올라요. Kintsugi 의 K3 Mends 는 금선만 늘릴 때 0.031 이었고, 사발이 품은 금의 양만큼 앞쪽 천에 따뜻한 빛이 지게 하자 선을 굵히지 않고 0.054 가 됐어요. 미리 돌려 안정된 상태에서 여는 모형에서는 몫이 따라 바뀌는 노브가 사라져요. Population Pyramid 의 K4 Migration 은 젊은 어른을 곱셈으로 늘리면 아기도 같이 늘어 0.011 이었고, 들어온 사람을 따로 세어 막대 끝을 옅게 칠하자 0.094 가 됐어요. Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. ### 세로 64×128 -2~8초 동안 켜진 LED 가 5% 아래인 3분의 1이 있는 패턴이 넷이에요. `study.ts` 표의 `thirds` 칸이 이 값이에요. Jelly 4.5% 와 Pulse Bars 2.6% 는 위 3분의 1이 비고, Sparks 2.0% 와 Dragon Fold 4.9% 는 아래 3분의 1이 비어요. Dragon Fold 는 문턱 바로 아래예요. 6판의 Jacob's Ladder 는 위, 가운데, 아래가 27.0%, 21.4%, 35.1% 예요. 햇빛층에서 심해까지 내려가는 바다는 16판의 Abyss 가 했고, 덩굴은 15판의 Ivy 가 했어요. 어두운 판 위의 가는 빛 하나는 128줄을 다 지나가도 3분의 1마다 10% 를 넘기 어려워요. 6판의 처음 아크는 가운데 3분의 1을 7.4% 켰어요. 9판의 Sprout 은 접시꽃 둘이 위 3분의 1을 8.1% 켜요. 이렇게 철이 도는 패턴은 2~8초가 어느 철에 걸리느냐에 따라 값이 몇 배씩 달라서, 씨앗 여럿으로 재 봐야 해요. 테두리를 그리는 패턴은 테두리를 빼고 재요. 10판의 Tetromino Well 은 양쪽 벽 2 LED 만으로 3분의 1마다 6.3% 를 켜서, 벽을 넣어 재면 위 3분의 1이 늘 문턱을 넘어요. 바탕을 다 그리는 패턴은 `thirds` 칸이 3분의 1마다 90 을 넘어요. 14판의 Waterfall 은 98/93/97 이에요. 그런 패턴은 2~8초 동안 밝기가 24/255 넘게 바뀌는 LED 로 세어야 그림이 128줄을 쓰는지 보여요. 그렇게 세면 Waterfall 의 위 3분의 1은 25.9% 예요. 15판의 Ivy 는 벽을 그려 `thirds` 가 84/89/82 이고, 바뀐 LED 로 세면 48.7%, 70.6%, 29.2% 예요. 가만히 있는 가는 선은 켜져 있어도 바뀌지 않아요. 15판의 Incense 는 곧은 연기 실이 가운데 3분의 1을 지나도 바뀐 LED 가 3.7% 였고, 소용돌이가 그 3분의 1 안에서 시작되게 하자 15.5% 가 됐어요. 16판의 Abyss 는 물을 다 칠해서 `thirds` 가 100/80/22 이고, 바뀐 LED 로 세면 84.5%, 59.3%, 36.0% 예요. 한밤층은 아귀와 불꽃, 잠수정 등빛뿐이라 아래 3분의 1이 가장 낮아요. 16판의 Harp 는 액자를 빼고 줄만 재면 48/53/12 이고, 바뀐 LED 는 아래 3분의 1이 9.8% 예요. 거기는 받침과 공명통 발이고, 줄은 300 LED 남짓한 삼각형에만 있어요. 17판의 Marble Run 은 레일과 리프트가 서 있어서 `thirds` 가 39/30/68 이고, 바뀐 LED 는 40.7%, 54.3%, 59.7% 예요. 17판의 Ant Farm 은 모래가 바탕이라 `thirds` 가 100/97/96 이고, 바뀐 LED 는 6.9%, 34.5%, 53.4% 예요. 위 3분의 1은 모래 윗면과 그 위의 어두운 유리라서 개미가 드나드는 입구와 더미만 바뀌어요. 18판의 Server Rack 은 앞판을 13~23 으로 24 아래에 두어서, `thirds` 63/63/61 이 다 드라이브와 코드와 LED 의 빛이에요. 바뀐 LED 셈은 2~8초 가운데 한 프레임만 24 를 넘어도 그 LED 를 세서, 무작위 반짝임이 값을 부풀려요. 18판의 Pint 는 잔 기포 반짝임을 켜면 50.3%, 60.2%, 40.0% 이고 끄면 50.2%, 46.8%, 26.3% 예요. 빛의 흔들림도 그래요. Longcase Clock 은 촛불일 때 41.8%, 45.6%, 45.3% 이고 흔들림 없는 달빛에서는 2.6%, 4.2%, 5.9% 예요. 자라는 그림은 미리 돌리기를 시간 대신 상태로 멈추면 씨앗마다 같은 자리에서 열려요. 18판의 Bismuth 는 무리 꼭대기가 20번 줄에 닿을 때 멈춰요. 14번 줄까지 기다리면 2초에 이미 다 자라 있어서 모양 바뀜이 0.027 로 떨어졌어요. 19판의 Dripstone 은 잘린 암반이 4/255 쯤이라 위아래가 검게 보여 `thirds` 가 68/99/77 이었고, 암반에 제 빛을 주자 92/98/99 가 됐어요. Foxfire 는 땅을 80번 줄에 두었을 때 위 3분의 1이 5% 였고, 64번 줄로 올리고 그루터기를 세우자 16% 가 됐어요. 쌓이는 그림은 2~8초에 쌓이는 자리가 보이게 열어야 해요. Popcorn 은 더미가 높게 시작하면 아래 3분의 1의 바뀐 LED 가 4.4% 였고, 낮게 시작해 자라게 하자 27.9% 가 됐어요. 넓은 면을 물들이는 조명은 반짝임 없이도 바뀐 LED 를 올려요. Dripstone 은 세 조명이 15초에 한 바퀴 번갈아 켜져서 57.6%, 84.9%, 79.2% 예요. 20판에서는 한 번 일어나는 일이 2~8초 창에 드는지가 값을 갈랐어요. High Striker 는 7.3초에 종이 한 번 울려서 위 3분의 1의 바뀐 LED 가 6.5% 에서 95.6% 가 됐고, Kettle 은 7.1초에 스위치가 꺼지는 것이 창에 들어 모양 바뀜이 0.065 에서 0.168 이 됐어요. 위로 오르는 앞선은 모양 바뀜을 적게 내요. Chromatography 는 6초 동안 앞선이 26줄을 올라도 블록이 16줄 높이라 0.040 이었어요. 21판의 세로 일곱은 Nixie 를 빼고 방이나 바탕을 다 그려서 `thirds` 가 3분의 1마다 70 을 넘었어요. 그런 그림은 바탕을 지우거나 멈추고 한 번 더 재야 주인공이 128줄을 쓰는지 보여요. Chemical Garden 은 유리와 방을 검게 지우자 정원만의 위 3분의 1이 6.6% 였고, 씨앗 열여섯 가운데 하나는 3.2% 였어요. Lap Pool 은 바닥 햇빛 무늬의 시간을 멈추자 바뀐 LED 가 3분의 1마다 18.7~52.1% 였어요. Woodpecker 는 새를 빼고 재도 바뀐 LED 가 57.3/35.1/50.0% 라서, 그 값은 거의 다 잎 사이로 움직이는 햇빛 반점에서 와요. 조금 위에서 본 장면은 세로를 싸게 얻어요. Pop-up Book 은 누운 페이지가 깊이의 5분의 1 줄만 써서, 책을 아래 34줄에 두고도 선 카드가 위 3분의 1까지 닿아요. 닉시관 하나는 지름이 높이의 절반쯤이라 Nixie 의 IN-18 은 54 LED 너비에 109줄로 서요.Slinky 는 계단과 벽을 빼고 스프링과 그림자만 세면 켜진 LED 가 0.4/2.6/3.0% 이고, 스프링이 켠 계단등까지 세면 20.9/16.5/8.6% 예요. 22판의 세로 다섯 가운데 넷은 바탕을 다 칠해서 `thirds` 가 말해 주는 것이 적었어요. 바탕을 검게 지운 사본으로 다시 재면 Liftoff 는 로켓만의 위 3분의 1이 켜진 LED 3.7%, 바뀐 LED 8.2% 라서, 그 3분의 1의 변화는 대부분 카메라가 고개를 들며 어두워지는 하늘에서 와요. Bowling Lane 은 공을 던지지 않게 하면 바뀐 LED 가 5.2/9.5/6.6% 로 떨어져서, 그 값은 거의 다 공과 그 빛이에요. Circulation 은 바탕을 21 아래로 내려 몸만 켜진 LED 로 세게 해서 `thirds` 가 34/50/26 이에요. 남은 자리로 짠 Flame Test 는 실험대가 어두워 아래 3분의 1의 바뀐 LED 가 4.6% 였어요. 불꽃빛의 몫을 0.42 에서 0.52 로 올려도 1.3 포인트만 올라서, 어두운 바닥은 빛을 비추는 것보다 2~8초 창 안에 그 자리로 가는 움직임이 있어야 바뀐 LED 가 올라요. 23판의 세로 일곱 가운데 Proscenium 은 금박 아치가 세 3분의 1을 다 지나가서, 아치를 검게 바꿔 다시 재면 `thirds` 가 94/97/69 에서 63/85/65 가 돼요. Final Approach 는 위 3분의 1을 밤하늘이 채워서, 도시 빛을 받은 천정을 33/255 로 두어 문턱 24 를 넘겼어요. 가는 선 하나가 지나가는 3분의 1은 둘레를 같이 밝혀야 해요. Axon 의 가운데 3분의 1은 축삭 한 줄이 지나가는 자리라서, 수초의 빛과 비스듬히 지나가는 이웃 축삭 둘을 두어 바뀐 LED 가 23.6~35.5% 가 됐어요. 한 번 일어나는 일은 2~8초 창에 들게 짰어요. Hanging Scroll 은 첫 그림이 걸려 있는 시간을 2.2초로 줄여 4.8~6.8초에 말려 올라가게 했고, Giraffe 는 첫 물 마시기를 3.4초에 두었어요. Elephant Toothpaste 의 아래 3분의 1은 바뀐 LED 가 9.7% 로 일곱 가운데 가장 낮아요. 기둥이 6.8초에 꺾이기 전까지는 실린더와 실험대가 서 있기만 해서예요. 24판의 세로 일곱 가운데 다섯은 바탕을 다 칠해서 바뀐 LED 로 봤어요. 23판이 물은 어두운 하늘의 떼는 Emergence 가 답했어요. 하늘을 지우고 박쥐만 세면 3분의 1마다 13.4~13.6, 9.9~10.2, 5.1~5.3% 가 켜져서, 기둥이 좁은 아래 3분의 1은 문턱을 겨우 넘어요. 2~8초 동안 바뀐 LED 로 세면 아래 3분의 1도 23.5~25.0% 예요. 자라는 것의 키가 위 3분의 1을 정해요. Potter's Wheel 은 그릇을 64~90줄로 빚을 때 씨앗 넷 가운데 둘이 위 3분의 1의 바뀐 LED 0.6% 와 0.0% 였고, 76~92줄로 키우자 열여섯 모두 15.2% 를 넘었어요. 바탕의 움직임을 멈추고 다시 재면 주인공의 몫이 보여요. Tower Crane 은 구름을 멈춰도 바뀐 LED 가 45.2/54.1/36.3% 이고, Pipe Organ 은 장미창 빛을 꺼도 3분의 1마다 18.2% 를 넘어요. Flying Trapeze 는 `thirds` 가 81/35/34 로 일곱 가운데 가장 고르지 않지만, 팔로 스폿 빛줄기가 연무 속을 쓸어서 바뀐 LED 는 93.9/87.9/57.5% 예요. +2~8초 동안 켜진 LED 가 5% 아래인 3분의 1이 있는 패턴이 넷이에요. `study.ts` 표의 `thirds` 칸이 이 값이에요. Jelly 4.5% 와 Pulse Bars 2.6% 는 위 3분의 1이 비고, Sparks 2.0% 와 Dragon Fold 4.9% 는 아래 3분의 1이 비어요. Dragon Fold 는 문턱 바로 아래예요. 6판의 Jacob's Ladder 는 위, 가운데, 아래가 27.0%, 21.4%, 35.1% 예요. 햇빛층에서 심해까지 내려가는 바다는 16판의 Abyss 가 했고, 덩굴은 15판의 Ivy 가 했어요. 어두운 판 위의 가는 빛 하나는 128줄을 다 지나가도 3분의 1마다 10% 를 넘기 어려워요. 6판의 처음 아크는 가운데 3분의 1을 7.4% 켰어요. 9판의 Sprout 은 접시꽃 둘이 위 3분의 1을 8.1% 켜요. 이렇게 철이 도는 패턴은 2~8초가 어느 철에 걸리느냐에 따라 값이 몇 배씩 달라서, 씨앗 여럿으로 재 봐야 해요. 테두리를 그리는 패턴은 테두리를 빼고 재요. 10판의 Tetromino Well 은 양쪽 벽 2 LED 만으로 3분의 1마다 6.3% 를 켜서, 벽을 넣어 재면 위 3분의 1이 늘 문턱을 넘어요. 바탕을 다 그리는 패턴은 `thirds` 칸이 3분의 1마다 90 을 넘어요. 14판의 Waterfall 은 98/93/97 이에요. 그런 패턴은 2~8초 동안 밝기가 24/255 넘게 바뀌는 LED 로 세어야 그림이 128줄을 쓰는지 보여요. 그렇게 세면 Waterfall 의 위 3분의 1은 25.9% 예요. 15판의 Ivy 는 벽을 그려 `thirds` 가 84/89/82 이고, 바뀐 LED 로 세면 48.7%, 70.6%, 29.2% 예요. 가만히 있는 가는 선은 켜져 있어도 바뀌지 않아요. 15판의 Incense 는 곧은 연기 실이 가운데 3분의 1을 지나도 바뀐 LED 가 3.7% 였고, 소용돌이가 그 3분의 1 안에서 시작되게 하자 15.5% 가 됐어요. 16판의 Abyss 는 물을 다 칠해서 `thirds` 가 100/80/22 이고, 바뀐 LED 로 세면 84.5%, 59.3%, 36.0% 예요. 한밤층은 아귀와 불꽃, 잠수정 등빛뿐이라 아래 3분의 1이 가장 낮아요. 16판의 Harp 는 액자를 빼고 줄만 재면 48/53/12 이고, 바뀐 LED 는 아래 3분의 1이 9.8% 예요. 거기는 받침과 공명통 발이고, 줄은 300 LED 남짓한 삼각형에만 있어요. 17판의 Marble Run 은 레일과 리프트가 서 있어서 `thirds` 가 39/30/68 이고, 바뀐 LED 는 40.7%, 54.3%, 59.7% 예요. 17판의 Ant Farm 은 모래가 바탕이라 `thirds` 가 100/97/96 이고, 바뀐 LED 는 6.9%, 34.5%, 53.4% 예요. 위 3분의 1은 모래 윗면과 그 위의 어두운 유리라서 개미가 드나드는 입구와 더미만 바뀌어요. 18판의 Server Rack 은 앞판을 13~23 으로 24 아래에 두어서, `thirds` 63/63/61 이 다 드라이브와 코드와 LED 의 빛이에요. 바뀐 LED 셈은 2~8초 가운데 한 프레임만 24 를 넘어도 그 LED 를 세서, 무작위 반짝임이 값을 부풀려요. 18판의 Pint 는 잔 기포 반짝임을 켜면 50.3%, 60.2%, 40.0% 이고 끄면 50.2%, 46.8%, 26.3% 예요. 빛의 흔들림도 그래요. Longcase Clock 은 촛불일 때 41.8%, 45.6%, 45.3% 이고 흔들림 없는 달빛에서는 2.6%, 4.2%, 5.9% 예요. 자라는 그림은 미리 돌리기를 시간 대신 상태로 멈추면 씨앗마다 같은 자리에서 열려요. 18판의 Bismuth 는 무리 꼭대기가 20번 줄에 닿을 때 멈춰요. 14번 줄까지 기다리면 2초에 이미 다 자라 있어서 모양 바뀜이 0.027 로 떨어졌어요. 19판의 Dripstone 은 잘린 암반이 4/255 쯤이라 위아래가 검게 보여 `thirds` 가 68/99/77 이었고, 암반에 제 빛을 주자 92/98/99 가 됐어요. Foxfire 는 땅을 80번 줄에 두었을 때 위 3분의 1이 5% 였고, 64번 줄로 올리고 그루터기를 세우자 16% 가 됐어요. 쌓이는 그림은 2~8초에 쌓이는 자리가 보이게 열어야 해요. Popcorn 은 더미가 높게 시작하면 아래 3분의 1의 바뀐 LED 가 4.4% 였고, 낮게 시작해 자라게 하자 27.9% 가 됐어요. 넓은 면을 물들이는 조명은 반짝임 없이도 바뀐 LED 를 올려요. Dripstone 은 세 조명이 15초에 한 바퀴 번갈아 켜져서 57.6%, 84.9%, 79.2% 예요. 20판에서는 한 번 일어나는 일이 2~8초 창에 드는지가 값을 갈랐어요. High Striker 는 7.3초에 종이 한 번 울려서 위 3분의 1의 바뀐 LED 가 6.5% 에서 95.6% 가 됐고, Kettle 은 7.1초에 스위치가 꺼지는 것이 창에 들어 모양 바뀜이 0.065 에서 0.168 이 됐어요. 위로 오르는 앞선은 모양 바뀜을 적게 내요. Chromatography 는 6초 동안 앞선이 26줄을 올라도 블록이 16줄 높이라 0.040 이었어요. 21판의 세로 일곱은 Nixie 를 빼고 방이나 바탕을 다 그려서 `thirds` 가 3분의 1마다 70 을 넘었어요. 그런 그림은 바탕을 지우거나 멈추고 한 번 더 재야 주인공이 128줄을 쓰는지 보여요. Chemical Garden 은 유리와 방을 검게 지우자 정원만의 위 3분의 1이 6.6% 였고, 씨앗 열여섯 가운데 하나는 3.2% 였어요. Lap Pool 은 바닥 햇빛 무늬의 시간을 멈추자 바뀐 LED 가 3분의 1마다 18.7~52.1% 였어요. Woodpecker 는 새를 빼고 재도 바뀐 LED 가 57.3/35.1/50.0% 라서, 그 값은 거의 다 잎 사이로 움직이는 햇빛 반점에서 와요. 조금 위에서 본 장면은 세로를 싸게 얻어요. Pop-up Book 은 누운 페이지가 깊이의 5분의 1 줄만 써서, 책을 아래 34줄에 두고도 선 카드가 위 3분의 1까지 닿아요. 닉시관 하나는 지름이 높이의 절반쯤이라 Nixie 의 IN-18 은 54 LED 너비에 109줄로 서요.Slinky 는 계단과 벽을 빼고 스프링과 그림자만 세면 켜진 LED 가 0.4/2.6/3.0% 이고, 스프링이 켠 계단등까지 세면 20.9/16.5/8.6% 예요. 22판의 세로 다섯 가운데 넷은 바탕을 다 칠해서 `thirds` 가 말해 주는 것이 적었어요. 바탕을 검게 지운 사본으로 다시 재면 Liftoff 는 로켓만의 위 3분의 1이 켜진 LED 3.7%, 바뀐 LED 8.2% 라서, 그 3분의 1의 변화는 대부분 카메라가 고개를 들며 어두워지는 하늘에서 와요. Bowling Lane 은 공을 던지지 않게 하면 바뀐 LED 가 5.2/9.5/6.6% 로 떨어져서, 그 값은 거의 다 공과 그 빛이에요. Circulation 은 바탕을 21 아래로 내려 몸만 켜진 LED 로 세게 해서 `thirds` 가 34/50/26 이에요. 남은 자리로 짠 Flame Test 는 실험대가 어두워 아래 3분의 1의 바뀐 LED 가 4.6% 였어요. 불꽃빛의 몫을 0.42 에서 0.52 로 올려도 1.3 포인트만 올라서, 어두운 바닥은 빛을 비추는 것보다 2~8초 창 안에 그 자리로 가는 움직임이 있어야 바뀐 LED 가 올라요. 23판의 세로 일곱 가운데 Proscenium 은 금박 아치가 세 3분의 1을 다 지나가서, 아치를 검게 바꿔 다시 재면 `thirds` 가 94/97/69 에서 63/85/65 가 돼요. Final Approach 는 위 3분의 1을 밤하늘이 채워서, 도시 빛을 받은 천정을 33/255 로 두어 문턱 24 를 넘겼어요. 가는 선 하나가 지나가는 3분의 1은 둘레를 같이 밝혀야 해요. Axon 의 가운데 3분의 1은 축삭 한 줄이 지나가는 자리라서, 수초의 빛과 비스듬히 지나가는 이웃 축삭 둘을 두어 바뀐 LED 가 23.6~35.5% 가 됐어요. 한 번 일어나는 일은 2~8초 창에 들게 짰어요. Hanging Scroll 은 첫 그림이 걸려 있는 시간을 2.2초로 줄여 4.8~6.8초에 말려 올라가게 했고, Giraffe 는 첫 물 마시기를 3.4초에 두었어요. Elephant Toothpaste 의 아래 3분의 1은 바뀐 LED 가 9.7% 로 일곱 가운데 가장 낮아요. 기둥이 6.8초에 꺾이기 전까지는 실린더와 실험대가 서 있기만 해서예요. 24판의 세로 일곱 가운데 다섯은 바탕을 다 칠해서 바뀐 LED 로 봤어요. 23판이 물은 어두운 하늘의 떼는 Emergence 가 답했어요. 하늘을 지우고 박쥐만 세면 3분의 1마다 13.4~13.6, 9.9~10.2, 5.1~5.3% 가 켜져서, 기둥이 좁은 아래 3분의 1은 문턱을 겨우 넘어요. 2~8초 동안 바뀐 LED 로 세면 아래 3분의 1도 23.5~25.0% 예요. 자라는 것의 키가 위 3분의 1을 정해요. Potter's Wheel 은 그릇을 64~90줄로 빚을 때 씨앗 넷 가운데 둘이 위 3분의 1의 바뀐 LED 0.6% 와 0.0% 였고, 76~92줄로 키우자 열여섯 모두 15.2% 를 넘었어요. 바탕의 움직임을 멈추고 다시 재면 주인공의 몫이 보여요. Tower Crane 은 구름을 멈춰도 바뀐 LED 가 45.2/54.1/36.3% 이고, Pipe Organ 은 장미창 빛을 꺼도 3분의 1마다 18.2% 를 넘어요. Flying Trapeze 는 `thirds` 가 81/35/34 로 일곱 가운데 가장 고르지 않지만, 팔로 스폿 빛줄기가 연무 속을 쓸어서 바뀐 LED 는 93.9/87.9/57.5% 예요. 25판의 세로 여섯 가운데 넷은 바탕을 다 칠해서 `thirds` 가 3분의 1마다 97 을 넘었고, 바탕을 멈추거나 지운 사본으로 다시 쟀어요. Eruption 은 하늘의 불빛과 부푸는 지층을 멈춰도 바뀐 LED 가 56.4~67.4/39.1~55.5/15.7~17.7% 이고, 아래 3분의 1의 값은 마그마 방의 대류와 화도를 오르는 가스 덩어리에서 와요. 빨강이 255 에 붙은 불빛은 가장 밝은 채널로 재면 결이 보이지 않아요. 플리니식에서 마그마 방이 밝기 끝에 붙자 아래 3분의 1의 바뀐 LED 가 5.3% 였고, 방의 온도가 기둥의 맥박을 따라 0.16 오르내리게 하자 17.7% 가 됐어요. Excavation 은 구름을 멈추면 24.3/12.5/15.3%, Skydiver 는 구름과 그림자를 멈추면 26.7/66.2/23.7% 예요. Rice Terraces 는 하늘 36줄을 빼도 위 3분의 1의 바뀐 LED 가 12.5% 라서, 맨 위 논들의 구름 반사가 그 3분의 1을 움직여요. 가는 선도 쓸고 지나가면 바뀐 LED 를 많이 켜요. Marionette 는 무대를 검게 지우면 인형과 실, 조종대가 켜는 LED 가 20.5/18.1/10.4% 인데 바뀐 LED 는 67.3/76.8/64.2% 예요. 조종대가 기울 때마다 실 일곱이 옆으로 쓸려서, 15판 Incense 의 곧은 연기 실이 3.7% 였던 것과 반대예요. 물에 비친 그림은 아래 3분의 1을 싸게 채워요. Ferris Wheel 은 위 장면을 0.72 로 눌러 41줄 호수에 비춰서 아래 3분의 1의 바뀐 LED 가 80~81% 예요. 이야기가 있는 데이터 그림은 여는 해가 세로를 정해요. Population Pyramid 는 1820년에서 열 때 막대가 0~78살에만 있어서, `thirds` 13/20/40 의 위 3분의 1이 연도와 라벨뿐이었어요. 라벨과 눈금을 지운 사본의 위 3분의 1은 1975년에서 열면 5.6%, 1985년 6.2%, 1995년 7.3%, 2000년 7.8% 여서 1995년에서 열어요. ## 다시 돌리기 From acf6a5d8e8139df01fd765f8840b4d82e187c4c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 04:08:39 +0000 Subject: [PATCH 14/17] penrose: Penrose rhombs zoom forever into the five-fold centre, splitting in a ring of light Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DbfsCYkpNNJ5diTKTtPZEu --- incoming/pattern-study/patterns/penrose.js | 313 +++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 incoming/pattern-study/patterns/penrose.js diff --git a/incoming/pattern-study/patterns/penrose.js b/incoming/pattern-study/patterns/penrose.js new file mode 100644 index 000000000..69a92fb01 --- /dev/null +++ b/incoming/pattern-study/patterns/penrose.js @@ -0,0 +1,313 @@ +// ===== Patternflow pattern ===== +// Title: Penrose +// Family: geometry +// Idea: A Penrose tiling of fat and thin rhombs zooms for ever into its five-fold centre, and a five-petalled ring of light opening from the middle splits every tile it passes into tiles a golden ratio smaller. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 25, after Roger Penrose's rhomb tiling (P3) and Raphael Robinson's golden triangles +// Round: 25 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Size=7..15, Glow=0..1 +// +// Knob 1: Hue. Turns all the tile colours round the colour wheel together. +// Knob 2: Speed. How fast it zooms. At 2 the picture grows by the golden ratio every three and a third seconds and a new ring opens each time; at 0.2 it takes half a minute. +// Knob 3: Size. The tile edge in LEDs, 7 to 15, when a level begins; the tiles grow from there until the ring splits them. +// Knob 4: Glow. How much of the light comes from the ring. At 0 the glass is lit all over and the ring is a thin line of light; at 1 the glass stands dark and lights up only behind the ring. +// Buttons: K1 tap turns the tiling into kites and darts (P2) behind a ring that opens from the centre; tap again for the rhombs. + +// The centre sits on an LED column, so the edges along the five-fold axis +// draw as one dark LED rather than two grey ones. +const W = 64, H = 128, N = W * H, CX = 31.5, CY = 64; +const { min, max, floor, ceil, sqrt, exp, cos, sin, abs, round, hypot, PI } = Math; +const PHI = (1 + sqrt(5)) / 2; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The smallest Size, how far the splitting ring runs in one level, the reach +// of its five petals and how many levels lie between the seed and the tiles. +const EMIN = 7, RHO = 130, PET = 0.12, DEEP = 6; + +// Robinson's golden triangles. A triangle [kind, A, B, C, parent] is half a +// tile: AB and AC are tile edges, BC runs through the middle of the tile. +// Kind 0 is the sharp one (36-72-72), kind 1 the blunt one (108-36-36). +// Rhombs (P3) and kites and darts (P2) split them by different rules. +function split(fam, T, out, idx, R) { + const put = (k, ax, ay, bx, by, cx, cy) => { + if (reach(ax, ay, bx, by, cx, cy) < R) out.push([k, ax, ay, bx, by, cx, cy, idx]); + }; + const [k, ax, ay, bx, by, cx, cy] = T; + const g = (p, q) => p + (q - p) / PHI; + if (fam === 0) { + if (k === 0) { + const px = g(ax, bx), py = g(ay, by); + put(0, cx, cy, px, py, bx, by); + put(1, px, py, cx, cy, ax, ay); + } else { + const qx = g(bx, ax), qy = g(by, ay), rx = g(bx, cx), ry = g(by, cy); + put(1, rx, ry, cx, cy, ax, ay); + put(1, qx, qy, rx, ry, bx, by); + put(0, rx, ry, qx, qy, ax, ay); + } + } else if (k === 0) { + const qx = g(ax, bx), qy = g(ay, by), rx = g(bx, cx), ry = g(by, cy); + put(1, rx, ry, qx, qy, bx, by); + put(0, qx, qy, ax, ay, rx, ry); + put(0, cx, cy, ax, ay, rx, ry); + } else { + const px = g(cx, ax), py = g(cy, ay); + put(1, bx, by, px, py, ax, ay); + put(0, px, py, cx, cy, bx, by); + } +} + +// How far the origin is from a triangle. +function seg(ax, ay, bx, by) { + const dx = bx - ax, dy = by - ay, t = cl(-(ax * dx + ay * dy) / (dx * dx + dy * dy), 0, 1); + return hypot(ax + t * dx, ay + t * dy); +} +function reach(ax, ay, bx, by, cx, cy) { + const s1 = ax * by - ay * bx, s2 = bx * cy - by * cx, s3 = cx * ay - cy * ax; + if ((s1 >= 0 && s2 >= 0 && s3 >= 0) || (s1 <= 0 && s2 <= 0 && s3 <= 0)) return 0; + return min(seg(ax, ay, bx, by), seg(bx, by, cx, cy), seg(cx, cy, ax, ay)); +} + +// One tiling, three levels deep. Level 0 has tile edge 1; levels 1 and 2 are +// it split once and twice. Level 2 blown up by phi squared and turned 36 +// degrees is level 0 again, which is what lets the zoom run for ever. The +// rhombs grow from a star of five fat rhombs, kites and darts from a sun of +// five kites; both come back to themselves every second split. Each level +// reaches just past the corners of the panel at the smallest Size. +function build(fam, scale) { + const s = EMIN * scale, R = [75 / s + 3, 75 / s + 2, 75 / (s * PHI) + 1.5]; + let T = []; + for (let i = 0; i < 10; i++) { + const a = -PI / 2 + ((2 * PI) / 5) * (i >> 1), b = a + (i & 1 ? PI : -PI) / 5, L = PHI ** DEEP; + if (fam === 0) T.push([1, cos(b) * L, sin(b) * L, 0, 0, cos(a) * L * PHI, sin(a) * L * PHI, 0]); + else T.push([0, cos(b) * L, sin(b) * L, 0, 0, cos(a) * L, sin(a) * L, 0]); + } + const levels = []; + for (let d = 0; d < DEEP + 2; d++) { + const out = []; + T.forEach((t, i) => split(fam, t, out, i, R[max(0, d - DEEP + 1)])); + T = out; + if (d >= DEEP - 1) levels.push(T); + } + return levels; +} + +// Every corner where tiles meet is one of seven kinds; for kites and darts +// Conway named them sun, star, ace, deuce, jack, queen and king. A tile's +// colour class comes from its kind and the kinds of its four corners: ten +// classes of rhomb, eight of kite and dart. +const vkey = (x, y) => round(x * 4096) * 1048576 + round(y * 4096); +// The angle at A, B and C in tenths of a turn, sharp then blunt, per tiling. +const ANG = [ + [1, 2, 2, 3, 1, 1], + [2, 1, 2, 1, 3, 1], +]; +function classify(fam, levels) { + const an = ANG[fam]; + const maps = levels.map((L) => { + const V = new Map(); + const add = (x, y, slot) => { + const k = vkey(x, y); + let c = V.get(k); + if (!c) V.set(k, (c = new Array(16).fill(0))); + c[slot]++; + }; + for (const [k, ax, ay, bx, by, cx, cy] of L) { + add(ax, ay, k * 8 + an[k * 3]); + add(bx, by, k * 8 + 4 + an[k * 3 + 1]); + add(cx, cy, k * 8 + 4 + an[k * 3 + 2]); + } + // A corner cut off at the edge of the patch does not close to 360 degrees. + for (const [k, c] of V) V.set(k, c.reduce((s, n, i) => s + n * (i & 3), 0) === 10 ? c.join(" ") : "x"); + return V; + }); + const vnames = [...new Set(maps.flatMap((V) => [...V.values()]))].sort(), X = vnames.indexOf("x"); + const id = new Map(vnames.map((n, i) => [n, i])); + const codes = levels.map((L, j) => { + const V = maps[j], vt = (x, y) => id.get(V.get(vkey(x, y))), mid = new Map(); + L.forEach((t, i) => { + const k = vkey(t[3] + t[5], t[4] + t[6]); + mid.set(k, mid.has(k) ? [mid.get(k)[0], i] : [i]); + }); + return L.map((t, i) => { + const pair = mid.get(vkey(t[3] + t[5], t[4] + t[6])), o = L[pair[0] === i ? pair[1] ?? i : pair[0]]; + const a0 = vt(t[1], t[2]), a1 = vt(o[1], o[2]), b0 = vt(t[3], t[4]), b1 = vt(t[5], t[6]); + if (a0 === X || a1 === X || b0 === X || b1 === X) return t[0] * 4096; + return t[0] * 4096 + min(a0, a1) * 512 + max(a0, a1) * 64 + min(b0, b1) * 8 + max(b0, b1); + }); + }); + const names = [...new Set(codes.flat())].sort((p, q) => p - q), cls = new Map(names.map((n, i) => [n, i])); + return codes.map((L) => L.map((n) => cls.get(n))); +} + +// Packed for drawing: corners, colour class, parent, and the tile's middle as +// its distance from the centre (pulled in along five petals) and how far the +// triangle can reach past it. +const petal = (x, y, r) => (r > 1e-6 ? (5 * x ** 4 * y - 10 * x * x * y ** 3 + y ** 5) / r ** 5 : 0); +function pack(levels, cls) { + return levels.map((L, j) => { + const n = L.length, P = { n, xy: new Float32Array(n * 6), cls: new Uint8Array(cls[j]), par: new Int32Array(n), rq: new Float32Array(n), near: new Float32Array(n), age: new Float32Array(n) }; + L.forEach((t, i) => { + for (let q = 0; q < 6; q++) P.xy[i * 6 + q] = t[q + 1]; + P.par[i] = t[7]; + // The middle of BC is the middle of the whole tile, so both halves of + // a tile split together. + const mx = (t[3] + t[5]) / 2, my = (t[4] + t[6]) / 2, r = hypot(mx, my); + P.rq[i] = r * (1 + PET * petal(mx, my, r)); + P.near[i] = r - max(hypot(t[1] - mx, t[2] - my), hypot(t[3] - mx, t[4] - my)); + }); + return P; + }); +} + +// P2 is drawn half a level larger so the two tilings show tiles of about the +// same area. +const SCALE = [1, sqrt(PHI)]; +const FAM = [0, 1].map((f) => { + const levels = build(f, SCALE[f]); + return pack(levels, classify(f, levels)); +}); + +// Hue offset, saturation and brightness of each colour class, in class order: +// the sharp kind first (thin rhombs, kites), then the blunt kind (fat rhombs, +// darts), each led by a fallback for tiles cut off at the edge of the patch, +// which never reach the panel. Fat rhombs round a five-pointed star and the +// darts of a star are gold. +const PAL = [ + [ + [0.6, 0.8, 0.75], + [0.5, 0.85, 0.85], + [0.42, 0.7, 0.95], + [0.63, 0.85, 0.85], + [0.12, 0.85, 1], + [0.13, 0.89, 1], + [0.15, 0.85, 1], + [0.88, 0.75, 0.9], + [0.76, 0.8, 0.75], + [0.98, 0.8, 0.9], + [0.1, 0.89, 1], + [0.33, 0.8, 0.8], + ], + [ + [0.55, 0.8, 0.8], + [0.5, 0.85, 0.85], + [0.36, 0.75, 0.85], + [0.62, 0.85, 0.85], + [0.45, 0.75, 0.95], + [0.12, 0.85, 1], + [0.13, 0.89, 1], + [0.9, 0.75, 0.9], + [0.02, 0.8, 0.95], + [0.78, 0.8, 0.8], + ], +]; +const GLOW = [255, 226, 150]; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + const rgb = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + for (let c = 0; c < 3; c++) out[o + c] = rgb[c] * 255; +} + +// Each LED's distance from the centre, pulled in along five petals: the +// regrouping ring after a K1 tap opens along it. +const RR = new Float32Array(N); +for (let p = 0; p < N; p++) { + const x = (p % W) + 0.5 - CX, y = floor(p / W) + 0.5 - CY, r = hypot(x, y); + RR[p] = r * (1 + PET * petal(x, y, r)); +} + +export function setup(params) { + params.s = { Z: 0, rot: 0, fam: 0, to: -1, rm: 0, hue: 0, e: EMIN, glow: 0.06, buf: new Float32Array(N * 3), col: new Float32Array(16 * 3) }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + P.hue = k[0]; + P.e = cl(k[2], EMIN, 15); + P.glow = cl(k[3], 0, 1); + // Two levels of zoom, phi squared, bring level 2 to where level 0 was, a + // tenth of a turn round. + P.Z += d * cl(k[1], 0.2, 3) * 0.15; + if (P.Z >= 2) (P.Z -= 2), (P.rot = (P.rot + PI / 5) % (2 * PI)); + if (P.to >= 0) { + P.rm += d * 45; + if (P.rm > 86) (P.fam = P.to), (P.to = -1); + } else if (input.btnPressed && input.btnPressed[0]) (P.to = 1 - P.fam), (P.rm = 0); +} + +// One golden triangle onto the LEDs, colour (r, g, b) and line light gl. +// Within LW of a tile edge an LED darkens into the 1-LED line between tiles; +// mode 1 and 2 keep only the LEDs inside or outside the regrouping ring. +const LW = 0.72; +function tri(P, xy, o, co, si, r, g, b, gl, mode) { + const ax = CX + co * xy[o] - si * xy[o + 1], ay = CY + si * xy[o] + co * xy[o + 1]; + const bx = CX + co * xy[o + 2] - si * xy[o + 3], by = CY + si * xy[o + 2] + co * xy[o + 3]; + const cx = CX + co * xy[o + 4] - si * xy[o + 5], cy = CY + si * xy[o + 4] + co * xy[o + 5]; + const x0 = max(0, ceil(min(ax, bx, cx) - 0.5)), x1 = min(W - 1, floor(max(ax, bx, cx) - 0.5)); + const y0 = max(0, ceil(min(ay, by, cy) - 0.5)), y1 = min(H - 1, floor(max(ay, by, cy) - 0.5)); + if (x0 > x1 || y0 > y1) return; + const sg = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax) > 0 ? 1 : -1, buf = P.buf; + // Signed distances to AB, BC and CA as a*X + b*Y + c, positive inside. + const l1 = sg / hypot(bx - ax, by - ay), l2 = sg / hypot(cx - bx, cy - by), l3 = sg / hypot(ax - cx, ay - cy); + const a1 = (ay - by) * l1, b1 = (bx - ax) * l1, c1 = ((by - ay) * ax - (bx - ax) * ay) * l1; + const a2 = (by - cy) * l2, b2 = (cx - bx) * l2, c2 = ((cy - by) * bx - (cx - bx) * by) * l2; + const a3 = (cy - ay) * l3, b3 = (ax - cx) * l3, c3 = ((ay - cy) * cx - (ax - cx) * cy) * l3; + for (let y = y0; y <= y1; y++) { + const Y = y + 0.5, X = x0 + 0.5; + let d1 = a1 * X + b1 * Y + c1, d2 = a2 * X + b2 * Y + c2, d3 = a3 * X + b3 * Y + c3; + for (let x = x0, p = y * W + x0; x <= x1; x++, p++, d1 += a1, d2 += a2, d3 += a3) { + if (d1 < -0.001 || d2 < -0.001 || d3 < -0.001 || (mode && (RR[p] < P.rm) !== (mode === 1))) continue; + // The line's light keeps to its core, so a small tile behind the ring + // keeps its colour. + const w = cl((LW - min(d1, d3)) * 2.2, 0, 1), m = 1 - 0.93 * w, v = w * w * gl, q = p * 3; + buf[q] = r * m + (GLOW[0] - r * m) * v; + buf[q + 1] = g * m + (GLOW[1] - g * m) * v; + buf[q + 2] = b * m + (GLOW[2] - b * m) * v; + } + } +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const h = P.Z >= 1 ? 1 : 0, rho = (P.Z - h) * RHO, buf = P.buf, col = P.col, dim = 1 - 0.65 * P.glow; + buf.fill(0); + const jobs = P.to < 0 ? [[P.fam, 0]] : [[P.fam, 2], [P.to, 1]]; + for (const [fi, mode] of jobs) { + const S = P.e * PHI ** P.Z * SCALE[fi], co = cos(P.rot) * S, si = sin(P.rot) * S, lim = 73 / S; + const Lc = FAM[fi][h], Lf = FAM[fi][h + 1], pal = PAL[fi]; + for (let c = 0; c < pal.length; c++) hsv(P.hue + pal[c][0], pal[c][1], pal[c][2], col, c * 3); + // A tile splits once the ring has passed its middle. The ring runs RHO + // LEDs a level, well past the corners, so every tile on show has split + // and its light has died before the next level begins. + for (let i = 0; i < Lc.n; i++) { + const a = (Lc.age[i] = rho - S * Lc.rq[i]); + if (a > 0 || Lc.near[i] > lim) continue; + const c = Lc.cls[i] * 3; + tri(P, Lc.xy, i * 6, co, si, col[c] * dim, col[c + 1] * dim, col[c + 2] * dim, 0, mode); + } + // Behind the ring the new edges flash, and the glass lights up and fades + // back down to what Glow leaves it. + for (let j = 0; j < Lf.n; j++) { + const a = Lc.age[Lf.par[j]]; + if (a <= 0 || Lf.near[j] > lim) continue; + const c = Lf.cls[j] * 3, lit = dim + (1 - dim) * exp(-a / 10), fl = 0.3 * P.glow * exp(-a / 5); + const r = col[c] * lit, g = col[c + 1] * lit, b = col[c + 2] * lit; + tri(P, Lf.xy, j * 6, co, si, r + (255 - r) * fl, g + (255 - g) * fl, b + (255 - b) * fl, exp(-a / 2.5), mode); + } + } + // The regrouping ring itself, a line of light. + if (P.to >= 0) + for (let p = 0; p < N; p++) { + const w = 1 - abs(RR[p] - P.rm) / 1.6; + if (w > 0) for (let q = 0; q < 3; q++) buf[p * 3 + q] += (GLOW[q] - buf[p * 3 + q]) * w * 0.85; + } + for (let p = 0, y = 0; y < H; y++) for (let x = 0; x < W; x++, p += 3) display.setPixel(x, y, buf[p], buf[p + 1], buf[p + 2]); +} From 01315cba2ebb0bd5b3babcbe05fa2ffb0a448b49 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 04:10:57 +0000 Subject: [PATCH 15/17] =?UTF-8?q?README=20=EC=97=90=20Penrose=20=EC=A4=84?= =?UTF-8?q?=EA=B3=BC=20=EC=A0=88=EC=9D=84=20=EC=B1=84=EC=9A=B4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DbfsCYkpNNJ5diTKTtPZEu --- incoming/pattern-study/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 394a30fdb..d3f04fbdf 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -273,7 +273,7 @@ | [Ferris Wheel](patterns/ferris-wheel.js) | 놀이공원 | 밤 호숫가에서 지름 59 LED 의 관람차가 돌고, 곤돌라는 바퀴가 돌아도 늘 똑바로 매달려 있어요. 살과 테두리의 불빛이 쇼 여섯 가지를 6.5초씩 이어 가고, 호수가 바퀴와 불빛을 일렁이며 비춰요. K4 Wind 를 올리면 구름이 몰려오고 물그림자가 긴 빛줄기로 부서지며, 버튼은 다음 쇼로 넘겨요. | | [Kintsugi](patterns/kintsugi.js) | 도자기 | 어두운 쪽빛 천 위의 청자 찻사발에 금이 가고, 조각들이 조금 벌어져 돌아요. 깨진 날에 붉은 옻과 금이 돈 다음 다시 붙어요. 위에 매달린 종이 등이 흔들리며 금빛 이음매에 반짝임을 지나가게 하고, K3 Mends 를 올릴수록 지나온 깨짐의 금빛 그물이 촘촘해져요. K1 을 누르면 사발이 바로 깨져요. | | [Marionette](patterns/marionette.js) | 무대·공연 | 작은 인형극 무대에서 어릿광대 꼭두각시가 팔로 스폿을 받으며 지그를 춰요. 머리와 두 손, 두 무릎, 등에 맨 실 일곱이 꼭대기 다리에서 인형사가 쥔 십자 조종대까지 올라가서, 조종대가 기울면 실이 따라 기울고 팔과 무릎이 끌려 올라가요. K4 가 스폿을 옆으로 옮기면 인형과 실의 그림자가 반대쪽 배경막에 서고, K1 을 누르면 인형이 무릎을 접고 높이 뛰었다가 인사해요. | -PENROSE_ROW +| [Penrose](patterns/penrose.js) | 기하·대칭 | 굵은 마름모와 가는 마름모의 펜로즈 타일링이 다섯 겹 대칭의 가운데로 끝없이 들어가요. 가운데서 꽃잎 다섯 장 꼴로 열리는 빛의 고리가 지나가면 타일이 황금비만큼 작은 타일로 나뉘고, 굵은 마름모 다섯이 모인 별은 금색이에요. Glow 를 올리면 유리가 어두워져 고리 뒤만 밝고, K1 을 누르면 연과 화살(P2)로 바뀌어요. | | [Population Pyramid](patterns/population-pyramid.js) | 데이터 그림 | 가상의 나라의 인구 피라미드가 한 살에 한 줄씩 움직여요. 왼쪽이 남자, 오른쪽이 여자이고, 10년마다 태어난 세대가 제 색 띠로 해마다 한 줄씩 올라가요. 1995년의 베이비붐 띠가 위로 올라가 2060년에 위가 무거운 항아리꼴이 되면, 1820년의 뾰족한 삼각형으로 돌아가 240년을 다시 지나요. K3 을 내리면 아래가 좁아져 거꾸로 선 피라미드가 되고, K1 을 누르면 그해가 전쟁이나 유행병이 되어 패인 홈이 위로 올라가요. | | [Rice Terraces](patterns/rice-terraces.js) | 농사·밭 | 세운 패널 아래 끝에서 숲이 선 능선까지 물 댄 다랑논이 등고선처럼 휘어 쌓이고, 논마다 해 뜨는 하늘을 비춰요. 위로 갈수록 층이 얇아지고 노을빛이 짙어지며, 구름 반사가 거울 논들을 가로질러 흘러가고 가까운 논에서는 농부가 물소를 몰아 논을 갈아요. K1 을 돌리면 모가 줄지어 서고 초록으로 덮였다가 누렇게 익고, K1 을 누르면 맨 위 물꼬가 열려 물이 둑을 하나씩 넘어 내려가요. | | [Skydiver](patterns/skydiver.js) | 비행 | 강하장 위로 점프기가 지나가며 문에서 다이버들을 내보내요. 다이버들은 구름층을 뚫고 내려가 손을 잡아 고리를 만들었다가 흩어지고, 저마다 색 램에어 캐노피를 펴서 나선을 그리며 들판의 착지 원으로 내려와요. K1 을 끝까지 돌리면 해가 지평선에 걸린 노을 점프가 되고, K1 을 누르면 다음 비행기가 바로 들어와요. | @@ -1075,7 +1075,7 @@ INTRO_25 ### Penrose -PENROSE_PARA +남은 자리로 기하·대칭 갈래의 넷째를 세웠어요. 촘촘함 × 색 3,4 칸에 4판의 Jacquard Loom 하나만 서 있어서, 작은 타일 사이에 1 LED 어두운 선을 긋고 선명한 색을 여럿 넣어 그 칸을 겨눴어요. 촘촘함 0.264, 색 0.583 으로 그 칸에 들었어요. 로빈슨 삼각형을 나누는 셈을 돌려 보니 타일링은 황금비 한 번에 제자리로 오지 않았어요. 한 번 나눈 판을 φ 배로 키우면 반지름 8 안의 변 700개 가운데 230개만 맞았어요. 두 번 나눈 판을 φ² 배로 키워 36도 돌리면 710개가 다 맞았어요. 그래서 두 단계를 한 바퀴로 돌고 바퀴마다 36도씩 돌려요. 흔히 쓰는 씨앗인 빨간 삼각형 열 개의 해는 나눈 뒤 가운데가 다시 빨갛게 돌아오지 않았어요. 그래서 굵은 마름모 다섯의 별을 씨앗으로 했어요. 연과 화살은 연 다섯의 해에서 자라고, 똑같이 두 번 나누면 돌아와요. 타일은 일곱 가지 꼭짓점 가운데 네 꼭짓점이 무엇이냐로 칠했어요. 마름모는 열 갈래, 연과 화살은 여덟 갈래예요. 고리는 한 단계에 130 LED 를 가서 모서리를 한참 지나요. 그래서 다음 단계가 오기 전에 모든 타일이 나뉘고 빛도 꺼져요. 첫 판은 K4 Glow 가 고리만 밝혀서 노브 반응이 0.01 이었어요. 고리 사이 유리를 어둡게 하는 노브로 바꾸자 0.23 이 됐어요. 한 프레임이 2.7 ms 였는데, 그 가운데 0.8 ms 는 첫 프레임에 삼각형 목록을 짓는 187 ms 였어요. 꼭짓점 각을 acos 대신 표로 읽자 첫 프레임이 83 ms, 한 프레임이 1.4 ms 가 됐어요. 타일 크기가 색을 움직였어요. 타일이 클수록 어두운 선의 몫이 줄어서, 타일 변 7 LED 에서 0.613 이던 색이 8 LED 에서는 0.633 으로 축 끝 0.627 을 넘었어요. 그래서 7 에서 열고 채도를 0.94 배로 내려 0.583 에 세웠어요. 너비 64 의 가운데는 두 LED 사이라서, 다섯 겹 축을 지나는 세로 변이 134/255 의 회색 두 줄로 그려졌어요. 가운데를 31.5 로 옮기자 17/255 의 어두운 한 줄이 됐어요. 모양 바뀜은 줌으로 타일이 4×16 칸을 흘러 지나가서 첫 판부터 0.083 이었고, 지금은 0.098 이에요. ## 지도 @@ -1145,7 +1145,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 이제는 창작자가 댓글로 고른 패턴만 고쳐요. 고를 때 볼 잰 값이에요. -노브 반응이 두 자리로 0.03 인 노브는 23판까지 열하나예요. 1판과 2판의 넷은 Hourglass 의 K2 Flow 가 0.029 로 가장 낮고, 같은 패턴의 K4 Layers 가 0.030 이에요. Flip Clock 의 K4 Light 가 0.032, Sparks 의 K1 Spread 가 0.034 예요. 13판의 Glyph Rain 은 K1 Hue 가 0.028 에서 0.066 으로, K4 Mutate 가 0.033 에서 0.066 으로 올랐어요. 판이 거의 빈 패턴은 노브가 바꿀 LED 가 적어서 노브가 다 낮게 나와요. Glyph Rain 은 채움이 0.059 에서 0.215 가 되자 노브 넷이 다 올랐어요. 9판의 Sprout 은 K1 Hue 가 0.045 로 올랐어요. 5판의 Euclid Pulse 는 K4 Steps 가 0.05 로 넷 가운데 가장 낮아요. 처음 자리의 Pulses 3 에서는 Steps 가 빛 조각의 폭만 바꿔요. 15판의 열 개는 가장 낮은 노브가 Ivy 의 K2 Speed 0.049 예요. 빠르기 노브는 새로 시작한 그림의 2~3초를 봐서, 빠르기만 바꾸는 노브는 낮게 나와요. 1 LED 너비 선이 주인공인 패턴은 선 옆 1 LED 에 그 빛을 옅게 묻히면 노브 반응이 두 배쯤 돼요. 15판의 Player Piano 는 그렇게 노브 넷이 0.02 언저리에서 0.05~0.08 로 올랐어요. 16판의 열 개는 가장 낮은 노브가 Fireflies 의 K4 Coupling 0.04 예요. 대부분이 어두운 그림은 노브 반응이 화면 평균 빛의 두 배쯤에서 멈춰요. Fireflies 는 평균 밝기가 0.03 언저리이고, 16판의 Wireworld 는 전자가 더하는 빛이 프레임 차이로 0.046 이라 K3 Pulses 가 0.05 예요. 노브 하나가 바탕을 바꾸게 하면 쉽게 올라가서, Fireflies 의 K1 Moon 은 0.08 이에요. 17판의 Frost 도 K1 Lamp 가 가로등 둘레의 서리만 물들일 때는 0.01~0.02 였고, 하늘 아래쪽과 눈 덮인 길까지 물들이자 0.05 가 됐어요. 17판의 열 개는 가장 낮은 노브가 Crossroads 의 K2 Pace 0.04 예요. 노브 검사는 다섯 설정을 같은 씨앗으로 돌려서, 노브가 붓질 수나 개미 수를 바꾸면 난수 차례가 갈려 반응이 부풀어요. 17판 Brushstrokes 의 K2~K4 와 Ant Farm 의 K3 에는 그 몫도 들어 있어요. 18판의 열 개는 가장 낮은 노브가 Server Rack 과 Turntables 의 K1~K3 으로 0.04 언저리예요. 큰 물건 하나에서 작은 부분만 움직이는 노브는 낮게 나와요. 18판 Longcase Clock 의 K3 은 처음에 추의 진폭을 바꾸는 Swing 이었는데 0.0036 으로 검사에 걸렸고, 촛불 자리를 옮기는 Candle 로 바꾸자 0.11 이 됐어요. 미리 돌리기로 처음 상태를 맞출 때는 빠르기 노브를 같이 봐야 해요. Server Rack 은 어느 빠르기에서든 2초에 같은 상태가 오게 맞췄다가 K2 가 0.0091 로 걸렸어요. 맞추는 시점은 패턴 시간에 두고 미리 돌리기는 노브 빠르기로 돌려야 빠르기마다 그림이 갈려요. 19판의 열 개는 가장 낮은 노브가 Foxfire 의 K1 Glow 와 Paternoster 의 K4 Busy 로 0.033 이에요. 패널의 10분의 1쯤 덮는 것의 색만 바꾸는 노브는 0.03 에 못 미쳐요. 19판 Popcorn 의 K4 는 처음에 버터 색을 바꾸는 Butter 였는데 0.012~0.02 여서, 더미 높이를 정하는 Heap 으로 바꿨어요. Dust Motes 의 Dust 는 먼지 수만 바꿀 때 0.011 이었고, 빛줄기 안개를 먼지 양에 묶자 0.04 가 됐어요. 노브 반응은 세 채널의 차이를 평균해요. Helter Skelter 의 빨강과 크림 줄무늬는 차이가 초록과 파랑에만 있어서, 탑이 눈에 띄게 비틀려도 0.02 였어요. 20판의 열 개는 가장 낮은 노브가 Kingfisher 의 K3 Fish 와 Kettle 의 K3 Fill 로 둘 다 0.040 이에요. 물고기 수나 물 높이만 바꿀 때는 0.012 와 0.038 이었고, Kingfisher 는 물풀과 갈대가 함께 늘게, Kettle 은 물이 많을수록 늦게 끓게 해서 올렸어요. 21판의 열 개는 가장 낮은 노브가 Woodpecker 의 K2 Speed 와 Lap Pool 의 K1 Stroke 로 둘 다 0.044 예요. 사람 넷의 영법만 바꿀 때 Stroke 는 0.02 였고, 평영과 접영이 바닥의 햇빛 무늬에 짧은 물결을 섞게 해서 올렸어요. 주인공 뒤에 서는 것을 더하는 노브는 가려져서 낮게 나와요. Pop-up Book 의 K3 Layers 는 뒤에 카드를 더할 때 0.01 이었고, 여섯째 층을 맨 앞에 서는 터널북 틀로 바꾸자 0.05 가 됐어요. Fiddlehead 의 K3 Fronds 도 늘어난 잎이 가운데 포기 뒤에서 자랄 때 0.02~0.03 이었고, 양옆 포기의 잎을 가운데 위로 기울게 하자 0.047 이 됐어요. 22판의 열 개는 가장 낮은 노브가 Circulation 의 K3 Detail 0.025 이고, 이 연구에서 두 자리로 0.02 인 노브는 이것 하나예요. 굵은 줄기와 첫 곁가지가 처음 자리에서 몸 LED 2618개 가운데 1943개를 덮어서 처음에 0.008 이었어요. 세운 패널에 사람 하나를 세우면 몸이 판의 32% 쯤이라, 몸 안에서만 바뀌는 노브는 화면 평균으로 나뉘어 낮게 나와요. 22판 Bowling Lane 의 K2 Speed 와 K3 Hook 은 0.03 이에요. 노브 검사가 보는 2~3초 무렵에 공의 길이 Hook 마다 화살표 언저리에서 만나서, 길만 바꿀 때 K3 는 0.01 이었어요. 빛나는 오일의 길이를 Hook 에 묶어 늘 보이는 결과를 하나 붙이자 올랐어요. 패널의 4분의 1만 바꾸는 노브는 그 안을 흐려도 0.04 언저리에서 멈춰요. Galilean Moons 의 K4 Seeing 은 접안렌즈 안만 흐릴 때 0.039 였고, 원반 둘레로 번지는 빛을 넓히자 0.054 가 됐어요. 23판의 열 개는 가장 낮은 노브가 Hanging Scroll 의 K4 Ink 0.032 와 Skein 의 K3 Geese 0.033 이에요. Ink 는 젖은 먹을 흐림으로만 그릴 때 0.022 였어요. 흐림은 눈에는 크게 달라 보여도 LED 하나하나의 밝기를 조금씩만 바꿔요. 넓은 면이 조금 바뀌는 쪽이 노브 반응을 올려서, Giraffe 의 K4 Wind 는 풀과 수관만 흔들 때 0.03 이었고 낮은 하늘에 먼지 막을 덮자 0.06 이 됐어요. 24판의 열 개는 가장 낮은 노브가 Potter's Wheel 의 K2 Speed 0.042 와 Pipe Organ 의 K4 Tremulant 0.042 예요. Breath 는 첫 판부터 노브 넷이 0.09~0.13 이었어요. 숨을 내쉰 폐가 판의 42% 를 덮고 노브마다 폐 전체의 색이나 크기를 바꿔서, 22판 Circulation 처럼 몸 안에서만 바뀌는 노브가 낮게 나오는 일이 없었어요. 첫 3초에 일어나지 않는 일만 바꾸는 노브는 0 에 가까워요. Shoot 'em Up 의 K4 Bullets 는 보스 탄이 3.6초부터 날아서 처음에 0.0041 이었고, 탄이 짙을수록 바닥을 어둡게 하자 0.10 이 됐어요. 빛이 이웃에 번지면 노브가 올라요. Pipe Organ 의 K1 Stops 는 소리 나는 파이프만 물들일 때 0.010 이었고, 그 빛이 좌우 16열의 주석에 비치게 하자 0.02, 리드 음전에 배음 파이프를 더하자 0.062 가 됐어요. 25판의 열 개는 가장 낮은 노브가 Snow Monkeys 의 K3 Monkeys 0.043 과 Ferris Wheel 의 K1 Lights 0.044 예요. PENROSE_KNOB수를 세는 노브는 더하는 것의 밝기보다 넓이를 따라가요. Monkeys 는 처음에 0.010 이었고, 원숭이 털을 (168,146,124) 에서 (218,184,150) 으로 밝혀도 0.0004 만 올랐어요. 앞줄에 1.4 배 큰 원숭이 둘을 더하자 0.043 이 됐어요. 1 LED 선이 주인공인 노브는 선이 바닥에 던지는 빛으로도 올라요. Kintsugi 의 K3 Mends 는 금선만 늘릴 때 0.031 이었고, 사발이 품은 금의 양만큼 앞쪽 천에 따뜻한 빛이 지게 하자 선을 굵히지 않고 0.054 가 됐어요. 미리 돌려 안정된 상태에서 여는 모형에서는 몫이 따라 바뀌는 노브가 사라져요. Population Pyramid 의 K4 Migration 은 젊은 어른을 곱셈으로 늘리면 아기도 같이 늘어 0.011 이었고, 들어온 사람을 따로 세어 막대 끝을 옅게 칠하자 0.094 가 됐어요. +노브 반응이 두 자리로 0.03 인 노브는 23판까지 열하나예요. 1판과 2판의 넷은 Hourglass 의 K2 Flow 가 0.029 로 가장 낮고, 같은 패턴의 K4 Layers 가 0.030 이에요. Flip Clock 의 K4 Light 가 0.032, Sparks 의 K1 Spread 가 0.034 예요. 13판의 Glyph Rain 은 K1 Hue 가 0.028 에서 0.066 으로, K4 Mutate 가 0.033 에서 0.066 으로 올랐어요. 판이 거의 빈 패턴은 노브가 바꿀 LED 가 적어서 노브가 다 낮게 나와요. Glyph Rain 은 채움이 0.059 에서 0.215 가 되자 노브 넷이 다 올랐어요. 9판의 Sprout 은 K1 Hue 가 0.045 로 올랐어요. 5판의 Euclid Pulse 는 K4 Steps 가 0.05 로 넷 가운데 가장 낮아요. 처음 자리의 Pulses 3 에서는 Steps 가 빛 조각의 폭만 바꿔요. 15판의 열 개는 가장 낮은 노브가 Ivy 의 K2 Speed 0.049 예요. 빠르기 노브는 새로 시작한 그림의 2~3초를 봐서, 빠르기만 바꾸는 노브는 낮게 나와요. 1 LED 너비 선이 주인공인 패턴은 선 옆 1 LED 에 그 빛을 옅게 묻히면 노브 반응이 두 배쯤 돼요. 15판의 Player Piano 는 그렇게 노브 넷이 0.02 언저리에서 0.05~0.08 로 올랐어요. 16판의 열 개는 가장 낮은 노브가 Fireflies 의 K4 Coupling 0.04 예요. 대부분이 어두운 그림은 노브 반응이 화면 평균 빛의 두 배쯤에서 멈춰요. Fireflies 는 평균 밝기가 0.03 언저리이고, 16판의 Wireworld 는 전자가 더하는 빛이 프레임 차이로 0.046 이라 K3 Pulses 가 0.05 예요. 노브 하나가 바탕을 바꾸게 하면 쉽게 올라가서, Fireflies 의 K1 Moon 은 0.08 이에요. 17판의 Frost 도 K1 Lamp 가 가로등 둘레의 서리만 물들일 때는 0.01~0.02 였고, 하늘 아래쪽과 눈 덮인 길까지 물들이자 0.05 가 됐어요. 17판의 열 개는 가장 낮은 노브가 Crossroads 의 K2 Pace 0.04 예요. 노브 검사는 다섯 설정을 같은 씨앗으로 돌려서, 노브가 붓질 수나 개미 수를 바꾸면 난수 차례가 갈려 반응이 부풀어요. 17판 Brushstrokes 의 K2~K4 와 Ant Farm 의 K3 에는 그 몫도 들어 있어요. 18판의 열 개는 가장 낮은 노브가 Server Rack 과 Turntables 의 K1~K3 으로 0.04 언저리예요. 큰 물건 하나에서 작은 부분만 움직이는 노브는 낮게 나와요. 18판 Longcase Clock 의 K3 은 처음에 추의 진폭을 바꾸는 Swing 이었는데 0.0036 으로 검사에 걸렸고, 촛불 자리를 옮기는 Candle 로 바꾸자 0.11 이 됐어요. 미리 돌리기로 처음 상태를 맞출 때는 빠르기 노브를 같이 봐야 해요. Server Rack 은 어느 빠르기에서든 2초에 같은 상태가 오게 맞췄다가 K2 가 0.0091 로 걸렸어요. 맞추는 시점은 패턴 시간에 두고 미리 돌리기는 노브 빠르기로 돌려야 빠르기마다 그림이 갈려요. 19판의 열 개는 가장 낮은 노브가 Foxfire 의 K1 Glow 와 Paternoster 의 K4 Busy 로 0.033 이에요. 패널의 10분의 1쯤 덮는 것의 색만 바꾸는 노브는 0.03 에 못 미쳐요. 19판 Popcorn 의 K4 는 처음에 버터 색을 바꾸는 Butter 였는데 0.012~0.02 여서, 더미 높이를 정하는 Heap 으로 바꿨어요. Dust Motes 의 Dust 는 먼지 수만 바꿀 때 0.011 이었고, 빛줄기 안개를 먼지 양에 묶자 0.04 가 됐어요. 노브 반응은 세 채널의 차이를 평균해요. Helter Skelter 의 빨강과 크림 줄무늬는 차이가 초록과 파랑에만 있어서, 탑이 눈에 띄게 비틀려도 0.02 였어요. 20판의 열 개는 가장 낮은 노브가 Kingfisher 의 K3 Fish 와 Kettle 의 K3 Fill 로 둘 다 0.040 이에요. 물고기 수나 물 높이만 바꿀 때는 0.012 와 0.038 이었고, Kingfisher 는 물풀과 갈대가 함께 늘게, Kettle 은 물이 많을수록 늦게 끓게 해서 올렸어요. 21판의 열 개는 가장 낮은 노브가 Woodpecker 의 K2 Speed 와 Lap Pool 의 K1 Stroke 로 둘 다 0.044 예요. 사람 넷의 영법만 바꿀 때 Stroke 는 0.02 였고, 평영과 접영이 바닥의 햇빛 무늬에 짧은 물결을 섞게 해서 올렸어요. 주인공 뒤에 서는 것을 더하는 노브는 가려져서 낮게 나와요. Pop-up Book 의 K3 Layers 는 뒤에 카드를 더할 때 0.01 이었고, 여섯째 층을 맨 앞에 서는 터널북 틀로 바꾸자 0.05 가 됐어요. Fiddlehead 의 K3 Fronds 도 늘어난 잎이 가운데 포기 뒤에서 자랄 때 0.02~0.03 이었고, 양옆 포기의 잎을 가운데 위로 기울게 하자 0.047 이 됐어요. 22판의 열 개는 가장 낮은 노브가 Circulation 의 K3 Detail 0.025 이고, 이 연구에서 두 자리로 0.02 인 노브는 이것 하나예요. 굵은 줄기와 첫 곁가지가 처음 자리에서 몸 LED 2618개 가운데 1943개를 덮어서 처음에 0.008 이었어요. 세운 패널에 사람 하나를 세우면 몸이 판의 32% 쯤이라, 몸 안에서만 바뀌는 노브는 화면 평균으로 나뉘어 낮게 나와요. 22판 Bowling Lane 의 K2 Speed 와 K3 Hook 은 0.03 이에요. 노브 검사가 보는 2~3초 무렵에 공의 길이 Hook 마다 화살표 언저리에서 만나서, 길만 바꿀 때 K3 는 0.01 이었어요. 빛나는 오일의 길이를 Hook 에 묶어 늘 보이는 결과를 하나 붙이자 올랐어요. 패널의 4분의 1만 바꾸는 노브는 그 안을 흐려도 0.04 언저리에서 멈춰요. Galilean Moons 의 K4 Seeing 은 접안렌즈 안만 흐릴 때 0.039 였고, 원반 둘레로 번지는 빛을 넓히자 0.054 가 됐어요. 23판의 열 개는 가장 낮은 노브가 Hanging Scroll 의 K4 Ink 0.032 와 Skein 의 K3 Geese 0.033 이에요. Ink 는 젖은 먹을 흐림으로만 그릴 때 0.022 였어요. 흐림은 눈에는 크게 달라 보여도 LED 하나하나의 밝기를 조금씩만 바꿔요. 넓은 면이 조금 바뀌는 쪽이 노브 반응을 올려서, Giraffe 의 K4 Wind 는 풀과 수관만 흔들 때 0.03 이었고 낮은 하늘에 먼지 막을 덮자 0.06 이 됐어요. 24판의 열 개는 가장 낮은 노브가 Potter's Wheel 의 K2 Speed 0.042 와 Pipe Organ 의 K4 Tremulant 0.042 예요. Breath 는 첫 판부터 노브 넷이 0.09~0.13 이었어요. 숨을 내쉰 폐가 판의 42% 를 덮고 노브마다 폐 전체의 색이나 크기를 바꿔서, 22판 Circulation 처럼 몸 안에서만 바뀌는 노브가 낮게 나오는 일이 없었어요. 첫 3초에 일어나지 않는 일만 바꾸는 노브는 0 에 가까워요. Shoot 'em Up 의 K4 Bullets 는 보스 탄이 3.6초부터 날아서 처음에 0.0041 이었고, 탄이 짙을수록 바닥을 어둡게 하자 0.10 이 됐어요. 빛이 이웃에 번지면 노브가 올라요. Pipe Organ 의 K1 Stops 는 소리 나는 파이프만 물들일 때 0.010 이었고, 그 빛이 좌우 16열의 주석에 비치게 하자 0.02, 리드 음전에 배음 파이프를 더하자 0.062 가 됐어요. 25판의 열 개는 가장 낮은 노브가 Snow Monkeys 의 K3 Monkeys 0.043 과 Ferris Wheel 의 K1 Lights 0.044 예요. 노브 하나가 지나가는 빛만 밝히면 낮게 나와요. Penrose 의 K4 Glow 는 고리만 밝힐 때 0.01 이었고, 고리 사이 유리를 어둡게 하는 노브로 바꾸자 0.23 이 됐어요. 수를 세는 노브는 더하는 것의 밝기보다 넓이를 따라가요. Monkeys 는 처음에 0.010 이었고, 원숭이 털을 (168,146,124) 에서 (218,184,150) 으로 밝혀도 0.0004 만 올랐어요. 앞줄에 1.4 배 큰 원숭이 둘을 더하자 0.043 이 됐어요. 1 LED 선이 주인공인 노브는 선이 바닥에 던지는 빛으로도 올라요. Kintsugi 의 K3 Mends 는 금선만 늘릴 때 0.031 이었고, 사발이 품은 금의 양만큼 앞쪽 천에 따뜻한 빛이 지게 하자 선을 굵히지 않고 0.054 가 됐어요. 미리 돌려 안정된 상태에서 여는 모형에서는 몫이 따라 바뀌는 노브가 사라져요. Population Pyramid 의 K4 Migration 은 젊은 어른을 곱셈으로 늘리면 아기도 같이 늘어 0.011 이었고, 들어온 사람을 따로 세어 막대 끝을 옅게 칠하자 0.094 가 됐어요. Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. From 6838bbf9e4e8dc9f30acb93d455bceb35dcbd986 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 04:20:04 +0000 Subject: [PATCH 16/17] =?UTF-8?q?README=20=EC=9D=98=2025=ED=8C=90=20?= =?UTF-8?q?=EB=A8=B8=EB=A6=AC=20=EB=AC=B8=EB=8B=A8=EA=B3=BC=20=EB=B9=88=20?= =?UTF-8?q?=EC=B9=B8=20=EB=AC=B8=EB=8B=A8=EC=9D=84=20=EC=93=B0=EA=B3=A0,?= =?UTF-8?q?=20=EC=85=8B=20=EB=82=98=EC=97=B4=EC=97=90=20=EA=B1=B8=EB=A6=B0?= =?UTF-8?q?=20=ED=8C=A8=ED=84=B4=20=EC=A3=BC=EC=84=9D=20=EB=91=90=20?= =?UTF-8?q?=EC=A4=84=EC=9D=84=20=EA=B3=A0=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DbfsCYkpNNJ5diTKTtPZEu --- incoming/pattern-study/README.md | 4 ++-- incoming/pattern-study/index.html | 2 +- incoming/pattern-study/patterns/ferris-wheel.js | 2 +- incoming/pattern-study/patterns/snow-monkeys.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index d3f04fbdf..d072b1567 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1035,7 +1035,7 @@ K4 Spate 0 이면 물이 실 몇 가닥으로 갈라져요. 처음 값 0.06 이 ## 25판 -INTRO_25 +25판도 새 패턴 열 개를 열 갈래에 하나씩 냈어요. 주제 1 은 이번에도 겨눌 칸이 없었어요. 기준 148개의 `--gaps` 가 21장 모두 `none` 이었어요. 그 대신 남은 자리로 짠 Penrose 가 패턴이 하나뿐이던 칸 여섯에 둘째로 섰어요. 주제 2 로 농사·밭 갈래를 Rice Terraces 로, 화산 갈래를 Eruption 으로 열었어요. 주제 4 는 그 둘과 Excavation, Marionette, Skydiver, Population Pyramid 로 여섯이에요. 여섯 다 3분의 1마다 켜진 LED 도, 2~8초 동안 24/255 넘게 바뀐 LED 도 5% 를 넘어요. 바탕을 멈추거나 라벨을 지운 사본으로 다시 재도 주인공만으로 3분의 1마다 5% 를 넘어요. Snow Monkeys 와 Ferris Wheel, Kintsugi, Penrose 는 주제 없이 남은 자리로 짰어요. 공사·건설과 도자기는 패턴이 하나뿐이던 갈래라 이번에 둘째가 섰고, 무대·공연과 짐승에는 셋째가, 비행과 놀이공원, 데이터 그림, 기하·대칭에는 넷째가 섰어요. 주제 3 은 앞 바퀴 PR 에 창작자 글이 없어서 하지 않았어요. 열 개를 다 넣고 재도 일곱 축의 양 끝은 그대로라서 칸 경계는 14판과 같아요. ### Rice Terraces @@ -1125,7 +1125,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 `study.ts --check --gaps` 가 `*` 를 붙인 칸 가운데 그림이 떠오르는 칸이에요. 칸은 가로와 세로를 낮은 쪽부터 1~4 로 세요. 페이지 기본 지도(가장자리 × 움직임)는 16칸이 다 차 있어요. -23판 뒤로는 `*` 가 붙은 칸이 없어요. 22판까지 남은 두 칸, 촘촘함 × 모양 바뀜 3,4 와 4,3 에 23판의 Bait Ball 과 Sequins 가 섰어요. 하나 남은 빈 칸이던 복잡도 × 채움 4,3 에는 24판의 Waggle Dance 가 섰어요. 복잡도가 2.173 을 넘고 채움이 0.558~0.815 인 자리예요. 복잡도 축 끝의 Contour Map(2.735)을 빼고 세면 차는 칸이라 `*` 가 붙지 않았고, 그 아래 복잡도 2.4~2.6 에 선 Woodpecker, Dripstone, Agate, Guilloché 는 채움이 다 0.93 을 넘었어요. 이제 지도 21장의 칸이 다 차서 `--gaps` 는 모든 줄에 `none` 을 찍어요. 주제 1 은 축 끝의 패턴 하나가 축을 늘려 새 빈 칸이 날 때까지 고를 칸이 없어요. Waggle Dance 에서 어려운 쪽은 채움이었어요. 벌집은 꺼진 LED 가 거의 없는 그림이라, 빈 방 바닥과 봉한 방 사이 이음매를 24 아래로 눌러 채움 0.732 를 냈어요. 복잡도는 켜진 LED 의 세 채널을 따로 흔드는 낟알로 쉽게 올라서, 첫 판은 오히려 2.766 으로 축 끝을 넘었어요. +23판 뒤로는 `*` 가 붙은 칸이 없어요. 22판까지 남은 두 칸, 촘촘함 × 모양 바뀜 3,4 와 4,3 에 23판의 Bait Ball 과 Sequins 가 섰어요. 하나 남은 빈 칸이던 복잡도 × 채움 4,3 에는 24판의 Waggle Dance 가 섰어요. 복잡도가 2.173 을 넘고 채움이 0.558~0.815 인 자리예요. 복잡도 축 끝의 Contour Map(2.735)을 빼고 세면 차는 칸이라 `*` 가 붙지 않았고, 그 아래 복잡도 2.4~2.6 에 선 Woodpecker, Dripstone, Agate, Guilloché 는 채움이 다 0.93 을 넘었어요. 이제 지도 21장의 칸이 다 차서 `--gaps` 는 모든 줄에 `none` 을 찍어요. 주제 1 은 축 끝의 패턴 하나가 축을 늘려 새 빈 칸이 날 때까지 고를 칸이 없어요. Waggle Dance 에서 어려운 쪽은 채움이었어요. 벌집은 꺼진 LED 가 거의 없는 그림이라, 빈 방 바닥과 봉한 방 사이 이음매를 24 아래로 눌러 채움 0.732 를 냈어요. 복잡도는 켜진 LED 의 세 채널을 따로 흔드는 낟알로 쉽게 올라서, 첫 판은 오히려 2.766 으로 축 끝을 넘었어요. 25판도 기준 148개의 21장이 다 `none` 이라 주제 1 은 겨눌 칸이 없었어요. 그 대신 패턴이 하나만 선 칸을 셌어요. 148개에서 49칸이었고, 남은 자리로 짠 Penrose 가 그 가운데 여섯 칸에 둘째로 서서 158개에서는 43칸이에요. Penrose 가 겨눈 칸은 촘촘함 × 색 3,4 예요. 촘촘함 0.195~0.299 에 색 0.509 위인 자리이고, 4판의 Jacquard Loom 혼자 서 있었어요. Penrose 는 촘촘함 0.264, 색 0.583 으로 거기 섰어요. 같은 값으로 움직임 × 촘촘함 4,3 과 촘촘함 × 채움 3,3 의 Moire Rings, 촘촘함 × 복잡도 3,2 의 Peg Light, 복잡도 × 색 2,4 의 Gear Train, 채움 × 색 3,4 의 Marbling 곁에도 섰어요. 1 LED 어두운 선으로 나눈 작은 타일에 선명한 색을 여럿 두면 촘촘함과 색이 함께 높은 자리에 닿아요. 촘촘함이 0.299 를 넘는 패턴이 20판에서 셋 늘었고, 셋 다 그 값을 다른 길로 냈어요. Agate 는 1 LED 띠를 벽까지의 거리로 그어서, 축에 나란한 띠는 이웃 쌍의 절반에서만 차이가 나고 대각선 띠는 70% 쯤에서 나요. 방향이 고르게 섞이면 띠 너비 0.85 LED 언저리가 가장 높았어요. Millefiori 는 촘촘함을 가장 밝은 채널로 재서 코발트와 카나리아 노랑이 맞닿아도 턱이 0 이었어요. 고리마다 밝은 유리와 그 유리를 어둡게 뽑은 유리를 번갈아 넣어야 했어요. Sunglint 는 반짝임을 물결 띠마다 체커판의 한쪽 칸에만 둬서 반짝임끼리 닿지 않게 했어요. 셋 다 채움이 0.85 를 넘어서, 촘촘함 × 채움 4,4 는 꺼진 LED 없이도 닿는 칸이었어요. 21판의 Sandpile 이 일곱째예요. 칸 하나가 LED 하나이고 알 셋 옆의 알 둘이 가장 흔한 이웃 쌍이라, 밝기를 0, 2, 1, 3 차례로 올려 더미 안의 촘촘함이 0.40 언저리예요. 판의 75% 는 덮여야 0.3 을 넘어요. 22판의 Pin Art 와 Night Flight 가 여덟째와 아홉째예요. Pin Art 는 핀과 틈의 체커 결이라 이웃 쌍이 모두 핀과 틈의 쌍이고, Night Flight 는 Pancakes 의 체커 결에 1 LED 가로등을 LED 의 12.7% 만큼 얹어서 가장자리 0.258 에 촘촘함 0.328 이에요. 촘촘함과 가장자리는 가장 밝은 채널로 재서, 색을 섞는 자리마다 섞은 뒤 가장 밝은 채널을 다시 맞춰야 했어요. 땅과 강물을 섞은 자리에서 가장 밝은 채널이 0.68 까지 내려가 체커 결의 37 이 41~60 으로 넘어갔고, 그대로 둔 판은 가장자리가 0.05 높았어요. 23판의 Sequins 가 열째예요. 체커 결 없이, 가로 5 LED 와 세로 3 LED 간격으로 엇갈려 단 스팽글의 빛점과 그림자 줄로 0.327 을 냈어요. 촘촘함은 무늬의 모양보다 턱의 크기를 따라가서, 스팽글 몸통을 0.8 에서 0.6 으로 어둡게 하자 은빛 면이 0.339 에서 0.311 로 내려갔어요. diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index d33b9cb48..eb47ce9e7 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -141,7 +141,7 @@

패턴 연구

- + +