From 3a82dd07070d57504c4e7d2fc9918c58a7cb88ee Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 22:21:27 +0000 Subject: [PATCH 1/8] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20=EC=97=B0?= =?UTF-8?q?=EA=B5=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20=EC=88=98?= =?UTF-8?q?=ED=95=99=20=EB=8B=A4=EC=84=AF=EA=B3=BC=20=ED=95=A0=EB=A1=9C?= =?UTF-8?q?=EC=9C=88=20=EB=8B=A4=EC=84=AF=EC=9D=B4=20=EB=8D=94=20=EC=84=A0?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016styVvYJeAE1svDoUBeGKX From 972fc802f42365b4495a77ee97ffd1c3e0cfe7f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 23:22:57 +0000 Subject: [PATCH 2/8] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=205?= =?UTF-8?q?1=ED=8C=90=20=E2=80=94=20Sato-Tate,=20Jack-in-the-Box?= 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_016styVvYJeAE1svDoUBeGKX --- .../pattern-study/patterns/jack-in-the-box.js | 370 ++++++++++++++++++ incoming/pattern-study/patterns/sato-tate.js | 324 +++++++++++++++ 2 files changed, 694 insertions(+) create mode 100644 incoming/pattern-study/patterns/jack-in-the-box.js create mode 100644 incoming/pattern-study/patterns/sato-tate.js diff --git a/incoming/pattern-study/patterns/jack-in-the-box.js b/incoming/pattern-study/patterns/jack-in-the-box.js new file mode 100644 index 000000000..f35178a16 --- /dev/null +++ b/incoming/pattern-study/patterns/jack-in-the-box.js @@ -0,0 +1,370 @@ +// ===== Patternflow pattern ===== +// Title: Jack-in-the-Box +// Family: halloween +// Idea: A purple tin box with orange harlequin diamonds sits on the floor while its crank turns by itself and the box shivers at every turn, until the lid bursts open and a grinning jack-o'-lantern on a long coiled spring shoots up the panel, overshoots and bobs on the stretching and bunching coil, swaying side to side, then slowly sinks back in, the lid slams and the crank turns again. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 51 +// Round: 51 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Spring=0..1, Wobble=0..1 +// +// Knob 1: Hue. The tin and the candle in the head. The tin runs from purple at 0 through blue and teal to poison green at 1, its diamonds always orange; the candle from orange at 0 through lime at 0.25, sea green at 0.5 and blue at 0.75 to violet at 1, lighting the carving, the glow round the head and the inside of the box. The pumpkin's skin and the spring stay as they are. +// Knob 2: Speed. At 2 the lid bursts at 0.85 seconds, the head bobs for 3.6 seconds, sinks back in 1.6, the lid slams and the crank turns for 1.4 seconds before the next pop, so a pop comes every 7 seconds; the crank turns 1.3 times a second. At 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Spring. Its stiffness. At 0 a soft spring: the head rests 40 LEDs above the box, overshoots to 66 and bounces deep and slow, 0.9 bounces a second; at 1 a stiff one: the head rides 56 LEDs up and buzzes 4.5 times a second in a tight shiver that dies away. +// Knob 4: Wobble. How far the spring and the head sway. The pop throws the head to one side and it swings back and forth every 2.4 seconds, the head tilting with the top of the spring and rocking on its neck: at 0 about 2 LEDs either way, at the opening 0.06 about 3, at 1 a throw of 20 LEDs that settles to 12 either way. +// Buttons: K1 tap pops it now; with the head already out it gives the head another shove up. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, round, sqrt, sin, cos, atan2, exp, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ease = (t) => ((t = cl(t, 0, 1)), t * t * (3 - 2 * t)); +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// The box in an oblique view: the front face from column 10 to 46 and row +// 93 to 121, and one unit of depth going 0.4 right and 0.22 up, so the top +// and the right side show. The box is 24 deep. +const X0 = 10, X1 = 46, Y0 = 93, Y1 = 121, DEP = 24, DX = 0.4 * DEP, DY = 0.22 * DEP, FLOOR = 122; +// The spring stands on the middle of the top; L is how far it reaches up +// from there to the bottom of the head's collar. Shut in, the head sits at +// L = -27, its stem below the box's front edge. +const BX = (X0 + X1) / 2 + DX / 2, BY = Y0 - DY / 2, INSIDE = -27, LMAX = 68; +// The head: a pumpkin RX by RY, the collar under it, and the coil. +const RX = 12, RY = 10, NECK = 2.5, COILS = 8, CR = 5.5; +// The story at Speed 2, in seconds after a pop: bob, sink, slam; then the +// box stays shut and cranks for SHUT before the next pop. The first pop +// comes at FIRST. +const BOB = 3.6, SINK = 1.6, SLAM = 0.35, SHUT = 1.4, FIRST = 0.85, CYCLE = BOB + SINK + SLAM + SHUT; + +export function setup(params) { + params.s = { + t: 0, hue: 0, speed: 2, stiff: 0, wob: 0.06, + // Spring length and speed; time since the pop (negative while shut); + // the lid's angle and speed; the crank's angle, the jolt it gives the + // box and the trembling before a pop; which side the pop throws the + // head; the candle's flicker. + L: INSIDE, v: 0, since: -FIRST, lid: 0, lidV: 0, crank: 0, jolt: 0, tremble: 0, side: 1, flick: 1, + buf: new Float32Array(N * 3), key: "", pal: null, + }; +} + +// Where the spring wants to rest and how it moves, by stiffness s: the +// angular frequency, the rest length and the damping, light for small +// motions and heavy for fast ones, so the pop cannot fly off the panel. +function springOf(s) { + return { w: TAU * 0.9 * 5 ** s, le: 40 + 16 * s, z: 0.05 - 0.04 * s, c2: 0.012 + 0.02 * s }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = cl(k[0], 0, 1); + S.speed = cl(k[1], 0.2, 3); + S.stiff = cl(k[2], 0, 1); + S.wob = cl(k[3], 0, 1); + const ds = (d * S.speed) / 2; + S.t += ds; + // A tap: pop now if shut or on the way down, or shove the head up. + if (input.btnPressed && input.btnPressed[0]) { + if (S.since < 0 || S.since > BOB) pop(S); + else S.v += 160; + } + // The clock of one cycle: negative while the box waits for its first pop, + // 0 at a pop, and round again to the next pop at CYCLE. + const before = S.since; + S.since += ds; + if ((before < 0 && S.since >= 0) || S.since >= CYCLE) pop(S); + const sp = springOf(S.stiff), a = S.since; + // The rest length: up while it bobs, drawn down into the box as it sinks. + const out = a >= 0 && a < BOB + SINK; + const le = a < 0 ? INSIDE : a < BOB ? sp.le : a < BOB + SINK ? sp.le + (INSIDE - 2 - sp.le) * ease((a - BOB) / SINK) : INSIDE; + // The spring, in eight steps a frame: L'' = -w^2 (L - le) - 2 z w L' - c2 |L'| L'. + if (out || S.L > INSIDE + 0.5 || abs(S.v) > 1) { + const n = 8, h = ds / n; + for (let i = 0; i < n; i++) { + let acc = -sp.w * sp.w * (S.L - le) - 2 * sp.z * sp.w * S.v - sp.c2 * abs(S.v) * S.v; + // The coil cannot stretch past LMAX: it pulls back hard there. + if (S.L > LMAX) acc -= 900 * (S.L - LMAX) + 25 * max(0, S.v); + S.v += acc * h; + S.L += S.v * h; + } + // Shut in the box, it stops at the bottom. + if (!out && S.L < INSIDE) (S.L = INSIDE), (S.v = 0); + // Going down it stops on the bottom of the box. + if (S.L < INSIDE - 2) (S.L = INSIDE - 2), (S.v = max(0, S.v)); + } + // The lid: flung open at the pop to lean back past upright, then shut as + // the head goes down, with a small bounce each time. + const goal = a >= 0 && a < BOB + SINK ? 1.95 : 0; + const lk = goal ? 260 : 380, lz = goal ? 16 : 14; + S.lidV += (-lk * (S.lid - goal) - lz * S.lidV) * ds; + if (a >= BOB + SINK - 0.05 && S.L > INSIDE + 6) S.lidV = max(S.lidV, 0); + S.lid += S.lidV * ds; + if (S.lid < 0) (S.lid = 0), (S.lidV = -0.35 * S.lidV); + // While shut the crank turns 1.3 times a second and jolts the box at every + // turn; in the last 0.4 seconds before a pop the box trembles and the lid + // rattles. + const shut = a < 0 || a >= BOB + SINK + SLAM; + if (shut) { + const c0 = S.crank; + S.crank += TAU * 1.3 * ds; + if (floor(S.crank / TAU) !== floor(c0 / TAU)) S.jolt = 1; + } + S.jolt = max(0, S.jolt - ds / 0.09); + const toPop = a < 0 ? -a : a >= BOB + SINK + SLAM ? CYCLE - a : 9; + S.tremble = toPop < 0.4 ? 1 - toPop / 0.4 : 0; + S.flick += (0.8 + 0.2 * Math.random() - S.flick) * min(1, ds * 12); +} + +function pop(S) { + S.since = 0; + S.L = max(S.L, INSIDE); + S.v = max(S.v, 0); + S.side = Math.random() < 0.5 ? -1 : 1; + S.lidV = max(S.lidV, 14); +} + +// Colours that only change with Hue. +function palette(S) { + const key = S.hue.toFixed(3); + if (S.key === key) return; + S.key = key; + // The tin runs from purple through blue and teal to poison green; the + // diamonds stay orange. The candle in the head runs from orange through + // yellow and green to blue and violet. + const h = 0.78 - 0.45 * S.hue, hd = 0.075, hc = 0.09 + 0.66 * S.hue; + S.pal = { + front: hsv(h, 0.78, 0.58), side: hsv(h, 0.8, 0.36), top: hsv(h, 0.7, 0.72), lining: hsv(h, 0.75, 0.22), inner: hsv(h, 0.8, 0.07), + dia: hsv(hd, 0.92, 1), diaSide: hsv(hd - 0.01, 0.9, 0.8), diaTop: hsv(hd, 0.85, 0.9), floor: hsv(h, 0.6, 0.12), + halo: hsv(hc, 0.92, 1), cut: hsv(hc, 0.85, 1), core: hsv(hc + 0.03, 0.55, 1), + }; +} + +// The head's place and tilt from the spring: the coil leaves the box +// straight up and bends toward the head, a quadratic curve from its foot at +// (BX, BY) through (BX, BY - 0.55 L) to the collar. +function headOf(S) { + // The pop throws the head to one side; it swings back and forth every + // 2.4 seconds, wide at first and settling to a steady sway. + const a = max(0, S.since), L = S.L, amp = 0.1 + 0.9 * S.wob, reach = cl(L / 40, 0, 1); + const X = cl(amp * reach * S.side * (12 + 12 * exp(-a / 2.5)) * sin(TAU * 0.42 * a + 0.36), -20, 20); + // The head tilts with the spring's end and rocks on its neck besides. + const hx = BX + X, hy = BY - L, tilt = 0.85 * atan2(X, max(8, 0.45 * L)) + amp * reach * 0.3 * sin(TAU * 0.9 * a); + return { hx, hy, tilt, cx: hx + (RY + NECK) * sin(tilt), cy: hy - (RY + NECK) * cos(tilt) }; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + palette(S); + const P = S.pal, buf = S.buf; + // The box's jolt and trembling, in whole LEDs. + const jy = S.jolt > 0.5 ? -1 : 0, jx = S.tremble > 0 && floor(S.t * 30) % 2 ? (S.tremble > 0.5 ? 1 : 0) : 0; + const ox = X0 + jx, oy = Y0 + jy; + const head = headOf(S), glow = cl((S.L - INSIDE) / 20, 0, 1) * S.flick; + const set = (p, c, a) => { + const o = 3 * p; + buf[o] = c[0] * a; + buf[o + 1] = c[1] * a; + buf[o + 2] = c[2] * a; + }; + // Night: black above, a floor in the tin's colour, the candle's glow + // around the head. + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const o = 3 * p; + let r = 0, g = 0, b = 0; + if (y >= FLOOR) { + const f = 0.8 + 0.2 * ((y - FLOOR) / (H - FLOOR)); + (r = P.floor[0] * f), (g = P.floor[1] * f), (b = P.floor[2] * f); + } + const dx = x + 0.5 - head.cx, dy = (y + 0.5 - head.cy) * 0.85, d2 = dx * dx + dy * dy; + if (d2 < 900 && glow > 0) { + const q = (1 - sqrt(d2) / 30) ** 2 * 0.42 * glow; + r += q * P.halo[0]; + g += q * P.halo[1]; + b += q * P.halo[2]; + } + buf[o] = r; + buf[o + 1] = g; + buf[o + 2] = b; + } + // The lid on its hinge along the back edge of the top: lid angle 0 lies + // shut, past 0.22 radians it shows its lining. Open wide it stands behind + // the spring; nearly shut it lies over everything. + const lidQuad = () => { + const ph = S.lid + 0.1 * S.tremble * abs(sin(S.t * 47)), fx = -DX * cos(ph), fy = DY * cos(ph) - DEP * sin(ph), hx0 = ox + DX, hy = oy - DY; + const y0 = floor(min(hy, hy + fy)), y1 = floor(max(hy, hy + fy)), outer = fy > 0; + for (let y = max(0, y0); y <= min(H - 1, y1 + 1); y++) { + const t = fy === 0 ? 0 : cl((y + 0.5 - hy) / fy, 0, 1); + if (abs(fy) > 0.5 && ((y + 0.5 - hy) / fy < -0.02 || (y + 0.5 - hy) / fy > 1.02)) continue; + const xa = hx0 + fx * t; + for (let x = max(0, floor(xa)); x < min(W, xa + (X1 - X0)); x++) { + const u = x + 0.5 - xa, side = u < 1 || u > X1 - X0 - 1, rim = side || t > 0.85 || t < 0.12; + let c = outer ? (rim ? [0.95, 0.75, 0.3] : P.top) : side || t > 0.9 ? [0.62, 0.46, 0.18] : P.lining; + if (outer && !rim && diamond(u - 7, (1 - t) * DEP - 6)) c = P.diaTop; + // The lining darkens toward the free edge and takes the head's glow. + set(y * W + x, c, outer || side || t > 0.9 ? 1 : 1.25 - 0.55 * t + 0.6 * glow * (1 - t)); + } + } + }; + if (S.lid > 0.9) lidQuad(); + // The top: the open mouth of the box, dark, lit orange by the head inside. + for (let y = floor(oy - DY); y < oy; y++) { + const t = (oy - (y + 0.5)) / DY, xa = ox + DX * t; + for (let x = floor(xa); x < xa + (X1 - X0); x++) { + if (x < 0 || x >= W) continue; + const c = P.inner, a = 1 + 4 * glow * (1 - t); + set(y * W + x, [c[0] * a + 0.25 * glow * P.halo[0], c[1] * a + 0.25 * glow * P.halo[1], c[2] * a + 0.25 * glow * P.halo[2]], 1); + } + } + // The spring: a helix about the curve, back half first, steel bright in front. + const L = S.L, hx = head.hx, hy = head.hy, bx = BX + jx, by = BY + jy; + if (L > -2) { + const steps = COILS * 40; + for (let pass = 0; pass < 2; pass++) + for (let i = 0; i <= steps; i++) { + const s = i / steps, ph = TAU * COILS * s + 0.6, z = sin(ph); + if ((z > 0) !== (pass === 1)) continue; + // Bezier point and tangent. + const m1x = bx, m1y = by - 0.55 * L, u = 1 - s; + const px = u * u * bx + 2 * u * s * m1x + s * s * hx, py = u * u * by + 2 * u * s * m1y + s * s * hy; + let tx = 2 * u * (m1x - bx) + 2 * s * (hx - m1x), ty = 2 * u * (m1y - by) + 2 * s * (hy - m1y); + const tl = sqrt(tx * tx + ty * ty) || 1; + (tx /= tl), (ty /= tl); + const r = CR * (s < 0.04 ? 0.6 + 10 * s : 1), x = floor(px - ty * r * cos(ph) + 0.5), y = floor(py + tx * r * cos(ph) + 0.5); + if (x < 0 || x >= W || y < 0 || y >= oy + Y1 - Y0) continue; + const lit = pass ? 0.55 + 0.45 * z : 0.22 + 0.1 * (1 + z), shine = pass && z > 0.85 ? 0.25 : 0; + set(y * W + x, [0.78 * lit + shine + 0.15 * glow * lit, 0.8 * lit + shine + 0.05 * glow * lit, 0.9 * lit + shine], 1); + } + } + // The head: a ribbed pumpkin with a carved grin glowing from inside, a + // green collar of points under it and a stem on top, all tilted with the + // spring's end. + { + const ct = cos(head.tilt), st = sin(head.tilt), cx = head.cx, cy = head.cy, fl = S.flick; + for (let y = max(0, floor(cy - 15)); y < min(oy + Y1 - Y0, cy + 16); y++) + for (let x = max(0, floor(cx - 15)); x < min(W, cx + 16); x++) { + const dx = x + 0.5 - cx, dy = y + 0.5 - cy, u = dx * ct + dy * st, v = -dx * st + dy * ct; + const p = y * W + x; + // The collar: a ring of points below the head. + if (v > RY - 2 && v < RY + NECK + 2.2 && abs(u) < 9) { + const tip = RY + NECK + 2.2 - 2.2 * abs(sin((u * PI) / 3)); + if (v < tip) { + set(p, (floor((u + 9) / 3) & 1) ? [0.35, 1, 0.1] : [0.6, 0.15, 0.85], 0.9); + continue; + } + } + // The stem. + if (v < -RY + 1.5 && v > -RY - 3.2 && abs(u - 0.4 - 0.35 * (v + RY)) < 1.4) { + set(p, [0.25, 0.55, 0.12], 1); + continue; + } + // The pumpkin: an ellipse with lobes. + const ang = atan2(v, u), e = (u / RX) ** 2 + (v / RY) ** 2, lobe = 1 - 0.05 * abs(sin(ang * 3)); + if (e > lobe * lobe) continue; + const face = carved(u, v); + // The cut wall shows as a dark line along the top of each hole. + if (!face && carved(u, v + 1)) { + set(p, [0.5, 0.17, 0.02], 1); + continue; + } + if (face) { + // The candle inside: pale at the core, its full colour at the cut. + set(p, face === 2 ? P.cut : P.core, face === 2 ? 0.8 * fl : fl); + continue; + } + // Skin: ribs along the meridians, lit from the upper left, a darker + // rind at the edge so the head stands off its own glow. + const mer = u / RX / sqrt(max(0.05, 1 - (v / RY) ** 2)), rib = abs(sin(mer * PI * 2.2)) < 0.22 ? 0.84 : 1; + const light = (0.82 + 0.18 * cl(-(u + v) / RX, -1, 1)) * (e > 0.8 * lobe * lobe ? 0.78 : 1); + set(p, [light * rib, 0.44 * light * rib, 0.04], 1); + } + } + // The box's front and right side, over whatever is inside it. + for (let y = max(0, floor(oy - DY)); y <= oy + (Y1 - Y0); y++) + for (let x = ox; x < min(W, ox + (X1 - X0) + DX + 1); x++) { + const p = y * W + x, u = x + 0.5 - ox, v = y + 0.5 - oy; + if (u < X1 - X0) { + if (v < 0 || v > Y1 - Y0) continue; + // Gold rims top and bottom, purple tin with orange diamonds between. + if (v < 2 || v > Y1 - Y0 - 2) { + set(p, [0.95, 0.72, 0.25], v < 1 || v > Y1 - Y0 - 1 ? 1 : 0.75); + continue; + } + // Diamonds on a harlequin lattice, three across and two down, each + // 10 LEDs from point to point, the purple showing between. + const di = diamond(u - 7, v - 2); + const edge = u < 1 ? 1.3 : u > X1 - X0 - 1 ? 0.75 : 1; + set(p, di ? P.dia : P.front, edge); + } else { + const t = (u - (X1 - X0)) / DX; + if (t > 1) continue; + const vv = v + DY * t; + if (vv < 0 || vv > Y1 - Y0) continue; + if (vv < 2 || vv > Y1 - Y0 - 2) { + set(p, [0.6, 0.45, 0.15], 1); + continue; + } + set(p, diamond(t * DEP - 6, vv - 2) ? P.diaSide : P.side, 1); + } + } + // The crank on the side: a brass hub, an arm turning in the side's plane + // and a red knob sticking out of it. + { + const hubX = ox + (X1 - X0) + DX / 2 + 1, hubY = oy + (Y1 - Y0) / 2 - DY / 2, c = S.crank; + const ex = hubX + 1.5 + 7 * cos(c) * 0.4, ey = hubY - 7 * cos(c) * 0.22 + 7 * sin(c); + const dot = (x, y, col, a = 1) => { + x = floor(x); + y = floor(y); + if (x >= 0 && x < W && y >= 0 && y < H) set(y * W + x, col, a); + }; + for (let i = 0; i <= 10; i++) dot(hubX + (1.5 * i) / 10 + ((ex - hubX - 1.5) * i) / 10, hubY + ((ey - hubY) * i) / 10, [0.95, 0.75, 0.3]); + dot(hubX, hubY, [1, 0.9, 0.55]); + dot(hubX + 1, hubY, [0.9, 0.7, 0.3]); + for (let i = 1; i <= 3; i++) dot(ex + i, ey, [0.8, 0.6, 0.25]); + for (const [kx, ky] of [[4, 0], [5, 0], [4, -1], [5, -1], [6, 0], [4, 1], [5, 1]]) dot(ex + kx, ey + ky, [1, 0.25, 0.12], kx === 6 ? 0.8 : 1); + } + if (S.lid <= 0.9) lidQuad(); + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) display.setPixel(x, y, round(min(1, buf[3 * p]) * 255), round(min(1, buf[3 * p + 1]) * 255), round(min(1, buf[3 * p + 2]) * 255)); +} + +// Whether (a, b), in LEDs along and across a face of the tin, falls in one +// of its diamonds: every other cell of a lattice of 12 by 12 diamonds holds +// one, 10 from point to point, centred on a = 0 and b = 6. +function diamond(a, b) { + const qa = a / 12 + b / 12, qb = a / 12 - b / 12, fa = qa - floor(qa), fb = qb - floor(qb); + return (floor(qa) + floor(qb)) & 1 && max(abs(fa - 0.5), abs(fb - 0.5)) < 0.42; +} + +// The carving at (u, v) in the head's frame: 0 skin, 1 the open cut, 2 its +// lit rim. Two eyes pointing up, a small nose and a wide grin curving up at +// the corners, with a tooth hanging from the top and one standing below. +function carved(u, v) { + const au = abs(u); + // Eyes: triangles 7 wide at v = -0.5 up to a point at v = -6, centred at |u| = 5. + if (v < -0.5 && v > -6) { + const half = (3.5 * (v + 6)) / 5.5, e = abs(au - 5); + if (e < half) return v > -1.5 || e > half - 1 ? 2 : 1; + } + // Nose: a small triangle. + if (v > 0.3 && v < 2.6 && au < (v - 0.3) * 0.8 + 0.2) return 2; + // Grin: between two curves meeting at |u| = 9.5, v = 3.2. + const q = 1 - (au / 9.5) ** 2; + if (q > 0) { + const top = 3.2 + 1.3 * q, bot = 3.2 + 4.6 * q; + if (v > top && v < bot) { + if ((abs(u + 2.6) < 1.2 && v < top + 1.6) || (abs(u - 3) < 1.2 && v > bot - 1.6)) return 0; + return v > bot - 1 ? 2 : 1; + } + } + return 0; +} diff --git a/incoming/pattern-study/patterns/sato-tate.js b/incoming/pattern-study/patterns/sato-tate.js new file mode 100644 index 000000000..7bf1d0452 --- /dev/null +++ b/incoming/pattern-study/patterns/sato-tate.js @@ -0,0 +1,324 @@ +// ===== Patternflow pattern ===== +// Title: Sato-Tate +// Family: number +// Idea: Primes rain down the panel, each dropping at the column of a_p / (2 sqrt p) for an elliptic curve counted exactly mod p, and pile into a histogram that fills the faint semicircle (2/pi) sqrt(1 - x^2) layer by layer, each new layer as many primes as all the layers under it, while a curve with complex multiplication sends half its primes down the middle and heaps the rest against the walls. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Sato-Tate conjecture (Mikio Sato and John Tate, 1963), proved for elliptic curves without complex multiplication (Laurent Clozel, Michael Harris, Nicholas Shepherd-Barron and Richard Taylor, 2008-2011), pattern round 51 +// Round: 51 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Curve=0..1, Bins=-1..1 +// +// Knob 1: Hue. Turns the layers round the wheel. At 0 the first layer of primes is cyan and each later one a step further round, through azure and blue to violet and rose at the top of the full pile; at 0.5 the first is red and the later ones orange, yellow, lime and green. The outline stays a pale cream. +// Knob 2: Speed. At 2 the first drop lands at 0.9 seconds, the first 512 primes fill the dome by 2.6, the next four layers take 2.9, 4.5, 8.2 and 24 seconds, the last reaching p = 84,053, and after a 2.5 second hold the pile sinks and the count starts again from p = 3. At 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Curve. Three curves in steps. Below 1/3 the curve 37a, y^2 + y = x^3 - x, with no complex multiplication, whose primes fill the semicircle; from 1/3 y^2 = x^3 - x, where every p = 3 mod 4 has a_p = 0 and falls down the middle, and the rest follow 1/(pi sqrt(1 - x^2)) up the two walls; from 2/3 y^2 = x^3 + 1, the same for p = 2 mod 3. A new curve starts the count again. +// Knob 4: Bins. How the 63 columns of the plot are grouped: at -1 seven bins 9 LEDs wide, at -0.5 nine bins of 7, from -0.2 (so at the opening 0.06) 21 bins of 3, and from 0.6 every column its own bin. The counts are kept per column, so the bars regroup at once. +// Buttons: K1 tap lets the pile sink now and starts the count again from p = 3. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, round, sqrt, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ease = (t) => ((t = cl(t, 0, 1)), t * t * (3 - 2 * t)); +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// The plot: x = a_p / (2 sqrt p) in [-1, 1] over columns 0..62, x = c/31 - 1, +// so x = 0 is column 31 and every bin count that divides 63 has a middle bin. +// The end columns hold half the width of the others. +const COLS = 63, MID = 31; +const WIDTH = new Float32Array(COLS).fill(1 / 31); +WIDTH[0] = WIDTH[COLS - 1] = 0.5 / 31; +// Rows: the readout on rows 1..5, the rain from row 7, the pile standing on +// row 127. A density of 2/pi, the top of the semicircle, stands 50 rows tall; +// no bar stands above 72. +const TOP = 7, BASE = 127, HS = 50 / (2 / PI), HMAX = 72; +// Drops in rows and story seconds: they leave at V0 and gain G a second. +const V0 = 55, G = 170; + +// The three curves, each as the cubic f with y^2 = f(x) (after completing the +// square, so 37a's y^2 + y = x^3 - x is (2y + 1)^2 = 4x^3 - 4x + 1), with the +// odd primes of bad reduction to skip, and its law: 0 the semicircle, 1 half +// the mass at 0 and half on 1/(pi sqrt(1 - x^2)). +const CURVES = [ + { f: [4, 0, -4, 1], bad: 37, cm: 0 }, + { f: [1, 0, -1, 0], bad: 0, cm: 1 }, + { f: [1, 0, 0, 1], bad: 3, cm: 1 }, +]; +// The density of each law at x, without the point mass. +const law = (cm, x) => (cm ? 1 / (2 * PI * sqrt(max(1e-6, 1 - x * x))) : (2 / PI) * sqrt(max(0, 1 - x * x))); + +// The odd primes below 86,000, by a sieve: 8,400 of them, enough for the +// 8,192 a run counts on every curve. +const PMAX = 86000; +const PRIMES = (() => { + const comp = new Uint8Array(PMAX), out = []; + for (let i = 3; i < PMAX; i += 2) { + if (comp[i]) continue; + out.push(i); + for (let j = i * i; j < PMAX; j += 2 * i) comp[j] = 1; + } + return Int32Array.from(out); +})(); + +// a_p = p + 1 - #E(F_p) = -(sum over x of the Legendre symbol (f(x) | p)), +// exactly: a table of the squares mod p, then f(x) walked by its finite +// differences, so the loop over x adds and never multiplies. +const CHI = new Int8Array(PMAX); +function ap(p, c) { + CHI.fill(-1, 0, p); + CHI[0] = 0; + for (let i = 1, sq = 0, n = (p - 1) >> 1; i <= n; i++) { + sq += 2 * i - 1; + if (sq >= p) sq -= p; + CHI[sq] = 1; + } + const m = (v) => ((v % p) + p) % p; + let f = m(c[3]), d1 = m(c[0] + c[1] + c[2]), d2 = m(6 * c[0] + 2 * c[1]), s = 0; + const d3 = m(6 * c[0]); + for (let x = 0; x < p; x++) { + s += CHI[f]; + f = f + d1 - p; + f += (f >> 31) & p; + d1 = d1 + d2 - p; + d1 += (d1 >> 31) & p; + d2 = d2 + d3 - p; + d2 += (d2 >> 31) & p; + } + return -s; +} + +// Layers: the first 512 primes, then each layer as many as all before it. +// The pile is drawn so the layers so far fill the outline, and when a layer +// is in, the scale halves for the next. +const TARGET = [512, 1024, 2048, 4096, 8192], LAYERS = TARGET.length, LAST = TARGET[LAYERS - 1]; +const layerOf = (i) => { + let k = 0; + while (i >= TARGET[k]) k++; + return k; +}; +// How many primes have left the top by story time t of a run, piecewise linear. +const KNOTS = [[0, 0], [1.9, 512], [4.8, 1024], [9.3, 2048], [17.5, 4096], [41.5, LAST]]; +function countAt(t) { + for (let i = 1; i < KNOTS.length; i++) { + const [t0, n0] = KNOTS[i - 1], [t1, n1] = KNOTS[i]; + if (t < t1) return n0 + ((n1 - n0) * max(0, t - t0)) / (t1 - t0); + } + return LAST; +} +function timeOf(n) { + for (let i = 1; i < KNOTS.length; i++) { + const [t0, n0] = KNOTS[i - 1], [t1, n1] = KNOTS[i]; + if (n < n1) return t0 + ((t1 - t0) * (n - n0)) / (n1 - n0); + } + return KNOTS[KNOTS.length - 1][0]; +} +// Bins on Knob 4: 7, 9, 21 or 63 bins, 9, 7, 3 or 1 columns wide. +const binWidth = (k) => (k < -0.6 ? 9 : k < -0.2 ? 7 : k < 0.6 ? 3 : 1); + +// Digits 3 x 5 for the readout, a bit per LED from the top left, then a +// minus sign, a p and an a. +const FONT = [0x7b6f, 0x2c97, 0x73e7, 0x73cf, 0x5bc9, 0x79cf, 0x79ef, 0x7292, 0x7bef, 0x7bcf, 0x01c0, 0x7be4, 0x076b]; + +export function setup(params) { + params.s = { + hue: 0, speed: 2, bw: 3, curve: -1, t: 0, pi: 0, spawned: 0, landed: 0, sc: 1 / TARGET[0], done: -1, sink: -1, + lastP: 0, lastA: 0, shown: [0, 0], shownAt: -1, + cnt: new Float32Array(LAYERS * COLS), flash: new Float32Array(COLS), drops: [], + height: new Float32Array(COLS), binOf: new Int16Array(COLS), buf: new Float32Array(N * 3), pal: [], + }; + begin(params.s, 0); +} + +// A fresh run on curve k: an empty pile, the primes from 3. +function begin(S, k) { + S.curve = k; + S.t = 0; + S.pi = 0; + S.spawned = 0; + S.landed = 0; + S.sc = 1 / TARGET[0]; + S.done = -1; + S.sink = -1; + S.cnt.fill(0); + S.flash.fill(0); + S.drops.length = 0; + S.shownAt = -1; +} + +// Each bar's height in rows, per column, from the counts and the scale. +function heights(S) { + const w = S.bw, nb = COLS / w, cnt = S.cnt, h = S.height; + for (let b = 0; b < nb; b++) { + let n = 0, dx = 0; + for (let c = b * w; c < b * w + w; c++) { + dx += WIDTH[c]; + for (let L = 0; L < LAYERS; L++) n += cnt[L * COLS + c]; + } + const v = (n * S.sc * HS) / dx; + for (let c = b * w; c < b * w + w; c++) (h[c] = v), (S.binOf[c] = b); + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = cl(k[0], 0, 1); + S.speed = cl(k[1], 0.2, 3); + S.bw = binWidth(cl(k[3], -1, 1)); + const curve = min(2, floor(cl(k[2], 0, 1) * 3)); + if (curve !== S.curve) begin(S, curve); + if (input.btnPressed && input.btnPressed[0] && S.sink < 0) S.sink = S.t; + const ds = (d * S.speed) / 2, C = CURVES[S.curve]; + S.t += ds; + // New primes leave the top on the schedule, at most 60 a frame. + if (S.sink < 0) { + const want = min(LAST, floor(countAt(S.t))); + for (let n = 0; S.spawned < want && n < 60; n++) { + let p = PRIMES[S.pi++]; + if (p === C.bad) p = PRIMES[S.pi++]; + const a = ap(p, C.f), x = a / (2 * sqrt(p)); + S.drops.push({ c: round((x + 1) * MID), t0: timeOf(S.spawned), L: layerOf(S.spawned) }); + S.spawned++; + S.lastP = p; + S.lastA = a; + } + } + // The readout keeps each prime for a sixth of a second. + if (S.t - S.shownAt > 1 / 6 && S.lastP) (S.shown = [S.lastP, S.lastA]), (S.shownAt = S.t); + // The scale: the layers landed so far fill the outline. + let e = 0; + while (e < LAYERS - 1 && S.landed >= TARGET[e]) e++; + S.sc += (1 / TARGET[e] - S.sc) * (1 - exp(-ds / 0.18)); + heights(S); + // Drops fall and land on their bar. + const keep = []; + for (const D of S.drops) { + const s = S.t - D.t0; + D.y = TOP + V0 * s + 0.5 * G * s * s; + D.v = V0 + G * s; + const floorY = S.sink >= 0 ? H + 12 : BASE + 1 - min(HMAX, S.height[D.c]); + if (D.y >= floorY) { + if (S.sink < 0) { + S.cnt[D.L * COLS + D.c]++; + S.landed++; + S.flash[D.c] = 1; + } + continue; + } + keep.push(D); + } + S.drops = keep; + for (let c = 0; c < COLS; c++) S.flash[c] *= exp(-ds / 0.12); + // A full run holds for 2.5 seconds, then the pile sinks in 1.5 and the + // count starts again. + if (S.landed >= LAST && S.done < 0) S.done = S.t; + if (S.done >= 0 && S.sink < 0 && S.t > S.done + 2.5) S.sink = S.t; + if (S.sink >= 0 && S.t > S.sink + 1.5) begin(S, S.curve); +} + +// Layer colours round the wheel: at Hue 0 the first layer cyan, then azure, +// blue, violet and rose for the later ones. +function palette(S) { + if (S.palHue === S.hue) return; + S.palHue = S.hue; + S.pal = []; + for (let L = 0; L < LAYERS; L++) S.pal.push(hsv(0.5 + S.hue + 0.09 * L, 0.82, 1)); +} + +function glyph(buf, g, x0, y0, c, a) { + for (let j = 0; j < 15; j++) { + if (!((g >> (14 - j)) & 1)) continue; + const x = x0 + (j % 3), y = y0 + floor(j / 3); + if (x < 0 || x >= W) continue; + const o = 3 * (y * W + x); + for (let i = 0; i < 3; i++) buf[o + i] = c[i] * a; + } +} +// A number in 3 x 5 digits ending at column x1 (right aligned), or starting at x0. +function number(buf, v, x, y, right, c) { + const s = String(abs(v)), n = s.length + (v < 0 ? 1 : 0); + let px = right ? x - 4 * n + 1 : x; + if (v < 0) glyph(buf, FONT[10], px, y, c, 1), (px += 4); + for (const ch of s) glyph(buf, FONT[ch.charCodeAt(0) - 48], px, y, c, 1), (px += 4); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + palette(S); + const buf = S.buf, pal = S.pal, cnt = S.cnt, w = S.bw, cm = CURVES[S.curve].cm; + buf.fill(0); + const sink = S.sink >= 0 ? (HMAX + 4) * ease((S.t - S.sink) / 1.5) : 0; + // The pile: each column's bar in its layers, oldest at the bottom; a bar + // too tall for the panel keeps its layers in proportion under the ceiling. + // Every other bin is a shade darker so the bins read apart. + const cum = new Float32Array(LAYERS); + for (let c = 0; c < COLS; c++) { + const b = S.binOf[c], h0 = S.height[c]; + if (h0 <= 0.02) continue; + let tot = 0; + for (let L = 0; L < LAYERS; L++) { + let n = 0; + for (let cc = b * w; cc < b * w + w; cc++) n += cnt[L * COLS + cc]; + tot += n; + cum[L] = tot; + } + const h = min(h0, HMAX), k = h / tot, shade = w > 1 && b % 2 ? 0.8 : 1; + const top = floor(h - 1e-4), lift = h0 > HMAX ? 0.75 : 0.25 + 0.6 * S.flash[c]; + for (let r = 0, L = 0; r <= top; r++) { + const y = BASE - r + round(sink); + if (y < 0 || y >= H) continue; + while (L < LAYERS - 1 && cum[L] * k <= r + 0.5) L++; + const col = pal[L], a = (r < top ? 1 : h - r) * shade, o = 3 * (y * W + c); + const wt = r === top ? lift : 0; + for (let i = 0; i < 3; i++) buf[o + i] = (col[i] * (1 - wt) + wt) * a; + } + } + // The outline of the law in pale cream, brighter where it crosses a bar. + // A curve with complex multiplication also gets a dotted line up the middle + // for the half of its primes that sits at x = 0. + const line = [1, 0.86, 0.62]; + let prev = -1; + for (let c = 0; c < COLS; c++) { + const y = round(BASE + 1 - min(HMAX + 2, HS * law(cm, c / MID - 1))) + round(sink); + const y0 = prev < 0 ? y : prev < y ? prev + 1 : y, y1 = prev < 0 ? y : prev > y ? prev - 1 : y; + for (let yy = min(y0, y1); yy <= max(y0, y1); yy++) { + if (yy < TOP || yy >= H) continue; + const o = 3 * (yy * W + c), inside = buf[o] + buf[o + 1] + buf[o + 2] > 0.3; + for (let i = 0; i < 3; i++) buf[o + i] = inside ? buf[o + i] * 0.45 + line[i] * 0.55 : line[i] * 0.34; + } + prev = y; + } + if (cm) + for (let y = BASE + 1 - HMAX - 2 + round(sink); y <= BASE; y += 3) { + if (y < TOP || y >= H) continue; + const o = 3 * (y * W + MID); + if (buf[o] + buf[o + 1] + buf[o + 2] < 0.3) for (let i = 0; i < 3; i++) buf[o + i] = line[i] * 0.34; + } + // Drops: a bright head and a short tail, in the colour of their layer. + for (const D of S.drops) { + if (D.y === undefined) continue; + const col = pal[D.L], tail = min(7, 2 + D.v * 0.03), hy = floor(D.y); + for (let j = 0; j <= tail; j++) { + const y = hy - j; + if (y < TOP || y >= H) continue; + const a = j === 0 ? 1 : 0.85 * (1 - j / (tail + 1)) ** 1.5, wt = j === 0 ? 0.55 : j === 1 ? 0.2 : 0, o = 3 * (y * W + D.c); + for (let i = 0; i < 3; i++) buf[o + i] = max(buf[o + i], (col[i] * (1 - wt) + wt) * a); + } + } + // The readout: the newest prime at the left, its a_p at the right. + if (S.shown[0]) { + const grey = [0.62, 0.62, 0.62], lab = pal[layerOf(min(LAST - 1, S.spawned))]; + glyph(buf, FONT[11], 1, 1, lab, 1); + number(buf, S.shown[0], 6, 1, false, grey); + glyph(buf, FONT[12], 62 - 4 * (String(abs(S.shown[1])).length + (S.shown[1] < 0 ? 1 : 0)) - 4, 1, lab, 1); + number(buf, S.shown[1], 62, 1, true, grey); + } + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) display.setPixel(x, y, round(min(1, buf[3 * p]) * 255), round(min(1, buf[3 * p + 1]) * 255), round(min(1, buf[3 * p + 2]) * 255)); +} From eb48b58243c7c1e448bbdcba823f32ebe690934f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 23:24:27 +0000 Subject: [PATCH 3/8] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=205?= =?UTF-8?q?1=ED=8C=90=20=E2=80=94=20Hat=20Monotile,=20Grimoire?= 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_016styVvYJeAE1svDoUBeGKX --- incoming/pattern-study/patterns/grimoire.js | 455 ++++++++++++++++++ .../pattern-study/patterns/hat-monotile.js | 382 +++++++++++++++ 2 files changed, 837 insertions(+) create mode 100644 incoming/pattern-study/patterns/grimoire.js create mode 100644 incoming/pattern-study/patterns/hat-monotile.js diff --git a/incoming/pattern-study/patterns/grimoire.js b/incoming/pattern-study/patterns/grimoire.js new file mode 100644 index 000000000..a0e92939f --- /dev/null +++ b/incoming/pattern-study/patterns/grimoire.js @@ -0,0 +1,455 @@ +// ===== Patternflow pattern ===== +// Title: Grimoire +// Family: halloween +// Idea: A spellbook lies open on a lectern in a dark tower room beside a flickering candle, and a ghostly draft turns its pages by itself, each sheet lifting and curling over, catching the candle light before it lands; on every new spread a toxic-green sigil draws itself stroke by stroke, then its glyphs peel off the page one by one and rise up the dark as green sparks and wisps that fade near the top. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 51 +// Round: 51 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Wind=0..1, Glow=0..1 +// +// Knob 1: Hue. The colour of the magic, round the whole wheel: toxic green at 0, sky blue at 0.25, violet at 0.5, ember orange at 0.75 and green again at 1. The dark of the room moves from purple at 0 through indigo to deep teal at 1. The candle and the paper stay warm. +// Knob 2: Speed. At 2 the first page lifts at 0.5 s, a slow page takes 1.6 seconds to cross, a sigil draws in 1.4 seconds and burns for 2.6 while its glyphs rise; at 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Wind. At 0 one slow page turns every 7.8 seconds and the candle stands nearly straight; at 1 the draft riffles seven pages over in 1.1 seconds every 5.9 seconds and lifts the free edge of the next page while it waits. The flame leans and gutters with the draft, and the rising glyphs drift right. +// Knob 4: Glow. How bright the sigil burns: at 0 a thin line and a dim green light on the pages, at the opening 0.06 a little more. At 1 the sigil blazes and lights the room green, throwing four times the sparks, and every rising glyph trails a thick wisp. +// Buttons: K1 tap sends a gust that riffles pages now. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, sqrt, sin, cos, exp, round, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ease = (t) => ((t = cl(t, 0, 1)), t * t * (3 - 2 * t)); +const hash = (i) => { + let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// The book in its own frame: x across the spread from the gutter (a page +// is PW wide), y down the page from its middle (PH tall), z up off the +// paper. The lectern tilts it toward us; the eye looks down at it. +const GX = 32, YC = 87, PW = 27, PH = 44, CA = cos(0.92), SA = sin(0.92); +// The rise of the paper out of the gutter. +const zp = (x) => 2.4 * (1 - (1 - min(1, abs(x) / 12)) ** 2); +// Book frame to the panel, with a little perspective: the near edge is wider. +function proj(x, y, z, out) { + const k = 1 + 0.005 * (y * SA + z * CA); + // Seen a little from the right, so a standing sheet is not edge on. + out[0] = GX + (x + 0.3 * z) * k; + out[1] = YC + (y * CA - z * SA) * k; + out[2] = y * SA + z * CA; +} +// The candle: its flame on the panel and in the book frame. +const FX = 9, FY = 53, CANDLE = [-25, -27, 22]; +// The sigil hovers flat at ZS over the paper; its middle on the panel. +const ZS = 3, SGX = GX + 0.3 * ZS, SGY = YC - ZS * SA; + +// Light from the candle on a surface with normal n at book point p. +function lambert(px, py, pz, nx, ny, nz) { + let lx = CANDLE[0] - px, ly = CANDLE[1] - py, lz = CANDLE[2] - pz; + const d = sqrt(lx * lx + ly * ly + lz * lz); + return max(0, (lx * nx + ly * ny + lz * nz) / d) / (1 + (d * d) / 2500); +} + +// Ink on a page: lines of words with margins, laid out by the page's +// number. u runs from the gutter out, v down the page, both 0..1. +function ink(page, u, v) { + if (u < 0.12 || u > 0.9 || v < 0.08 || v > 0.93) return 0; + const yy = v * PH, line = floor(yy / 3.4); + if (yy - line * 3.4 > 1.3) return 0; + const sd = page * 131 + line * 17, xx = u * PW; + // A short last line now and then, and a blank line between paragraphs. + if (hash(sd) < 0.12 || xx > PW * (0.9 - (hash(sd + 1) < 0.25 ? 0.5 * hash(sd + 2) : 0))) return 0; + const word = floor(xx / 3.2 + hash(sd + 3) * 3); + return (xx / 3.2 + hash(sd + 3) * 3) % 1 < 0.78 - 0.25 * hash(sd + word) ? 1 : 0; +} +const PAPER = [1, 0.82, 0.56], INKC = [0.5, 0.31, 0.22]; + +// Where every LED of the lying spread is on the paper, found once by +// scattering fine samples: page x and y, how much of the LED is paper, and +// the candle light on it. +const SPX = new Float32Array(N), SPY = new Float32Array(N), SPC = new Float32Array(N), SPL = new Float32Array(N); +{ + const o = [0, 0, 0], n = new Float32Array(N); + for (let x = -PW + 0.15; x < PW; x += 0.3) + for (let y = -PH / 2 + 0.15; y < PH / 2; y += 0.3) { + proj(x, y, zp(x), o); + const p = floor(o[1]) * W + floor(o[0]); + SPX[p] += x; + SPY[p] += y; + n[p]++; + } + for (let p = 0; p < N; p++) + if (n[p]) { + (SPX[p] /= n[p]), (SPY[p] /= n[p]); + SPC[p] = min(1, n[p] / 9); + const x = SPX[p], s = (zp(x + 0.1) - zp(x - 0.1)) / 0.2, l = sqrt(1 + s * s); + // Darker into the gutter, where the paper turns away and down. + SPL[p] = lambert(x, SPY[p], zp(x), -s / l, 0, 1 / l) * (0.55 + 0.45 * min(1, abs(x) / 5)); + } +} + +// The room, the lectern, the book's cover and the candle's wax, as an +// unlit colour and how much candle light each LED takes, built per Hue. +const WALL = 0, WOOD = 1, COVER = 2, WAX = 3, EDGE = 4; +const KIND = new Uint8Array(N), CLF = new Float32Array(N), GLF = new Float32Array(N); +{ + const o = [0, 0, 0], cov = new Uint8Array(N); + // The cover: the pages' outline grown by 1.6 and pushed 2 down. + for (let x = -PW - 1.6; x < PW + 1.6; x += 0.3) + for (let y = -PH / 2 - 1.6; y < PH / 2 + 1.6; y += 0.3) { + proj(x, y, -1.2, o); + cov[floor(o[1]) * W + floor(o[0])] = 1; + } + for (let p = 0; p < N; p++) { + const x = p % W, y = floor(p / W), dx = x + 0.5 - FX, dy = y + 0.5 - FY; + let k = WALL; + // The lectern: the slanted board under the book, its front edge, a + // turned post and a foot. + const post = abs(x + 0.5 - GX) < 4.5 - 1.6 * sin(((y - 104) / 6) * PI) ** 2 && y >= 104; + if ((y >= 98 && y < 104 && abs(x + 0.5 - GX) < 29 - (y - 98) * 0.5) || post || (y >= 121 && abs(x + 0.5 - GX) < 16 - (y - 121) * 0.3)) k = WOOD; + if (y === 103 && k === WOOD) k = EDGE; + if (cov[p] || SPC[p] > 0) k = COVER; + // The candle stands behind the book's top left corner. + if (abs(x + 0.5 - FX) < 1.8 && y >= FY + 4 && y < 74 && !cov[p] && !SPC[p]) k = WAX; + KIND[p] = k; + const r = sqrt(dx * dx + dy * dy); + CLF[p] = k === WAX ? 0.9 - (y - FY - 4) * 0.035 : 1 / (1 + (r * r) / (k === WALL ? 200 : 380)) * (k === WOOD && x < GX ? 1.3 : 1); + // The sigil's light: strong on the spread, rising up the wall above it. + const gx = (x + 0.5 - SGX) / 34, gy = (y + 0.5 - SGY) / (y < SGY ? 46 : 18); + GLF[p] = exp(-(gx * gx + gy * gy) * 2); + } +} +function scene(S) { + const room = hsv(0.78 - 0.3 * S.hue, 0.7, 1), A = S.amb, L = S.lit; + for (let p = 0; p < N; p++) { + const x = p % W, y = floor(p / W), k = KIND[p]; + let a, l; + if (k === WALL) { + // Rough stone blocks of uneven length, the dark gathering toward the top. + const row = floor(y / 7); + let bx = -floor(hash(row) * 9), i = 0; + while (bx + 7 + floor(hash(row * 31 + i) * 7) <= x) bx += 7 + floor(hash(row * 31 + i++) * 7); + const mortar = y % 7 === 0 || x === bx, v = (mortar ? 0.55 : 0.8 + 0.2 * hash(row * 53 + i)) * (0.6 + 0.4 * min(1, y / 80)); + a = room.map((c) => c * 0.19 * v); + l = [(0.72 + 0.1 * room[0]) * v, (0.42 + 0.1 * room[1]) * v, (0.24 + 0.1 * room[2]) * v]; + } else if (k === WOOD || k === EDGE) { + const grain = 0.85 + 0.15 * sin(x * 0.7 + y * 0.2); + a = [0.09 * grain, 0.035, 0.08 * grain]; + l = k === EDGE ? [0.85, 0.5, 0.25] : [0.5 * grain, 0.24 * grain, 0.14]; + } else if (k === COVER) { + a = [0.09, 0.02, 0.1]; + l = [0.42, 0.14, 0.2]; + } else { + a = [0.12, 0.1, 0.08]; + l = [1, 0.85, 0.6]; + } + for (let c = 0; c < 3; c++) (A[3 * p + c] = a[c]), (L[3 * p + c] = l[c] * CLF[p]); + } +} + +// Tiny invented glyphs, 3x3, row by row from the top left. +const GLYPH = [0b111010010, 0b101010101, 0b110011001, 0b010101010, 0b111101001, 0b100111001, 0b011100011, 0b101111101, 0b110010011, 0b001010111, 0b010111100, 0b111001110]; + +// A new sigil: an outer ring, a ring of glyphs, an inner ring, a polygon or +// star in it and a small eye in the middle, as strokes in book coordinates +// drawn in that order. Each stroke is a list of points; glyphs are points. +function sigil(seed) { + const r = (i) => hash(seed * 97 + i), circle = (R, a0) => { + const pts = []; + for (let i = 0; i <= 64; i++) pts.push([R * cos(a0 + (TAU * i) / 64), R * sin(a0 + (TAU * i) / 64)]); + return pts; + }; + const a0 = r(1) * TAU, forms = [[3, 1], [4, 1], [7, 2], [7, 3], [8, 3], [5, 1], [6, 1], [9, 4]], [n, m] = forms[floor(r(2) * 8)]; + const star = []; + for (let i = 0; i <= n; i++) star.push([12.5 * cos(a0 + (TAU * i * m) / n), 12.5 * sin(a0 + (TAU * i * m) / n)]); + const ng = 8 + floor(r(3) * 4), glyphs = []; + for (let i = 0; i < ng; i++) glyphs.push({ x: 16.5 * cos(a0 + (TAU * i) / ng), y: 16.5 * sin(a0 + (TAU * i) / ng), g: floor(r(10 + i) * GLYPH.length), gone: false }); + const strokes = [circle(20.5, a0), null, circle(13.5, a0 + PI), star, circle(3, 0)]; + // Lengths, so the pen can run along them at one speed; the glyph ring + // counts as one more stroke, a glyph every 4 units. + const len = strokes.map((s) => { + if (!s) return ng * 4; + let l = 0; + for (let i = 1; i < s.length; i++) l += Math.hypot(s[i][0] - s[i - 1][0], s[i][1] - s[i - 1][1]); + return l; + }); + return { strokes, glyphs, len, total: len.reduce((a, b) => a + b, 0) }; +} + +export function setup(params) { + params.s = { + fresh: true, t: 0, hue: 0, speed: 2, wind: 0, glow: 0.06, key: "", prevBtn: false, + phase: "turn", pt: -0.5, pages: [], page: 0, leftPage: -1, sg: sigil(1), sgA: 0, draw: 1, flick: 1, lean: 0, + risers: [], sparks: [], amb: new Float32Array(N * 3), lit: new Float32Array(N * 3), + buf: new Float32Array(N * 3), zb: new Float32Array(N), mg: new Float32Array(N), ws: new Float32Array(N), o: [0, 0, 0], + }; +} + +// The pages that turn at once: one slow sheet in a calm, a riffle in a gale. +function startTurn(S, gust) { + const w = gust ? max(S.wind, 0.6) : S.wind, n = 1 + round(6 * w * w), dur = 1.6 - 1.1 * w, gap = dur * (0.32 - 0.12 * w); + S.pages = []; + for (let i = 0; i < n; i++) S.pages.push({ start: i * gap, dur, num: S.page + i }); + S.page += n; + S.phase = "turn"; + S.pt = 0; +} + +// A glyph leaves the page and rises. +function rise(S, x, y, g) { + S.risers.push({ x, y, vx: (Math.random() - 0.5) * 4, vy: -11 - 7 * Math.random(), g, age: 0, ph: Math.random() * TAU }); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = cl(k[0], 0, 1); + S.speed = cl(k[1], 0.2, 3); + S.wind = cl(k[2], 0, 1); + S.glow = cl(k[3], 0, 1); + const ds = (d * S.speed) / 2; + S.t += ds; + if (S.fresh) { + // The night is already going: the last sigil's glyphs are on their way + // up and its light is dying on the page. + S.fresh = false; + for (let i = 0, n = 7 + round(5 * S.glow); i < n; i++) { + rise(S, SGX + (Math.random() - 0.5) * 34, SGY - 4 - Math.random() * 8, floor(Math.random() * GLYPH.length)); + const r = S.risers[i], a = 0.4 + 2.6 * Math.random(); + for (let s = 0; s < a; s += 0.05) step(S, r, 0.05); + } + S.sgA = 1; + for (const g of S.sg.glyphs) g.gone = true; + S.pages = [{ start: 0, dur: 1.6 - 1.1 * S.wind, num: 0 }]; + S.page = 1; + } + const btn = !!(input.btnPressed && input.btnPressed[0]); + if (btn && !S.prevBtn && !(S.phase === "turn" && S.pt < 0.3)) { + for (const g of S.sg.glyphs) if (!g.gone && S.phase !== "turn") (g.gone = true), rise(S, ...glyphAt(S, g)); + startTurn(S, true); + } + S.prevBtn = btn; + S.pt += ds; + // The story: turn the pages, draw the sigil, burn it and let it fade, rest, then turn again. + if (S.phase === "turn") { + S.sgA = max(0, S.sgA - ds * 1.3); + for (const p of S.pages) if (S.pt > p.start + p.dur) S.leftPage = max(S.leftPage, p.num * 2 + 1); + if (S.pages.every((p) => S.pt > p.start + p.dur)) (S.phase = "draw"), (S.pt = 0), (S.draw = 0), (S.sg = sigil(S.page * 7 + 3)), (S.sgA = 1), (S.pages = []); + } else if (S.phase === "draw") { + S.draw = min(1, S.pt / 1.4); + if (S.pt > 1.4) (S.phase = "burn"), (S.pt = 0); + } else if (S.phase === "burn") { + // The glyphs peel off one by one round the ring. + const gs = S.sg.glyphs; + gs.forEach((g, i) => { + if (!g.gone && S.pt > 0.3 + (2 * i) / gs.length) (g.gone = true), rise(S, ...glyphAt(S, g)); + }); + if (S.pt > 2.6) (S.phase = "fade"), (S.pt = 0); + } else if (S.phase === "fade") { + S.sgA = 1 - S.pt / 0.6; + if (S.pt > 0.6) (S.phase = "rest"), (S.pt = 0), (S.sgA = 0); + } else if (S.pt > 1.6 - 1.4 * S.wind) startTurn(S, false); + // Sparks fly off a burning sigil and off the pen. + const burning = S.phase === "burn" ? 1 : S.phase === "draw" ? 0.5 : 0; + const want = burning * ds * (10 + 30 * S.glow); + for (let i = 0, n = floor(want) + (Math.random() < want % 1 ? 1 : 0); i < n; i++) { + const a = Math.random() * TAU, r = Math.random() < 0.5 ? 20.5 : 13.5; + proj(r * cos(a), r * sin(a), ZS, S.o); + S.sparks.push({ x: S.o[0], y: S.o[1], vx: (Math.random() - 0.5) * 6, vy: -18 - 14 * Math.random(), age: 0, life: 0.6 + Math.random() }); + } + for (const r of S.risers) step(S, r, ds); + S.risers = S.risers.filter((r) => r.y > -4); + for (const s of S.sparks) (s.x += (s.vx + 10 * S.wind) * ds), (s.y += s.vy * ds), (s.age += ds); + S.sparks = S.sparks.filter((s) => s.age < s.life); + // The flame: flicker and the draft's lean. + // The draft bows the flame and makes it gutter. + S.flick = 0.84 - 0.24 * S.wind + (0.08 + 0.12 * S.wind) * sin(S.t * 13) + 0.06 * sin(S.t * 31 + 1) + 0.05 * (Math.random() - 0.5); + S.lean = S.wind * (1.6 + 0.8 * sin(S.t * 3.1)) + 0.4 * sin(S.t * 2.3); + // The wisps fade. + const f = exp(-ds / 0.55); + for (let p = 0; p < N; p++) S.ws[p] *= f; +} + +// A rising glyph: up with a sway, drifting with the wind, leaving a wisp. +function step(S, r, ds) { + r.age += ds; + r.vy *= 1 - 0.15 * ds; + r.x += (r.vx + 5 * sin(r.age * 2.2 + r.ph) + 12 * S.wind) * ds; + r.y += r.vy * ds; + const x = floor(r.x), y = floor(r.y) + 2; + if (x >= 0 && x < W && y >= 0 && y < H) S.ws[y * W + x] = min(1.4, S.ws[y * W + x] + (0.25 + 0.5 * S.glow) * ds * 8); +} + +// Where a glyph of the sigil sits on the panel, and which it is. +function glyphAt(S, g) { + proj(g.x, g.y, ZS, S.o); + return [S.o[0], S.o[1], g.g]; +} + +// One turning sheet as fine samples: its section bends from the gutter +// (angle th0) to the free edge (th1), lifting off the right-hand pile and +// landing on the left. The face toward us shows text and takes the candle +// light; a glint runs across it as it faces the flame. +function sheet(S, pg, u) { + const { buf, zb, o } = S; + const th0 = PI * ease(u), bend = 0.8 * sin(TAU * u) + 0.25 * S.wind * sin(S.t * 9) * sin(PI * u); + const th1 = cl(th0 + bend, 0.03, PI - 0.03), lift = 1 - sin(PI * u); + let x = 0, z = 0; + const NS = 54, ds = PW / NS; + for (let i = 0; i < NS; i++) { + const s = (i + 0.5) / NS, th = th0 + (th1 - th0) * s ** 1.6, c = cos(th), sn = sin(th); + x += c * ds; + z += sn * ds; + const zz = z + zp(x) * lift + 0.3, front = th < PI / 2; + // Normal of the face we see, and its light. + const nx = front ? -sn : sn, nz = front ? c : -c; + const li = lambert(x, 0, zz, nx, 0, nz), glint = li > 0 ? max(0, nx * -0.55 + nz * 0.83) ** 24 : 0; + const lv = 0.38 + 1.6 * li * S.flick; + const side = front ? pg.num * 2 : pg.num * 2 + 1, uu = s; + for (let y = -PH / 2 + 0.25; y < PH / 2; y += 0.5) { + proj(x, y, zz, o); + const px = floor(o[0]), py = floor(o[1]); + if (px < 0 || px >= W || py < 0 || py >= H) continue; + const p = py * W + px; + if (o[2] < zb[p]) continue; + zb[p] = o[2]; + const k = ink(side, uu, (y + PH / 2) / PH), edge = s > 0.97 || abs(y) > PH / 2 - 0.6 ? 0.8 : 1; + for (let ch = 0; ch < 3; ch++) buf[3 * p + ch] = (k ? INKC[ch] : PAPER[ch]) * min(1.15, lv) * edge + glint * 0.45; + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const key = S.hue.toFixed(3); + if (key !== S.key) (S.key = key), scene(S); + const { buf, amb, lit, mg, ws, zb, sg, o } = S, fl = S.flick, mc = hsv(0.3 + S.hue, 0.8, 1); + const burn = S.sgA * (S.phase === "burn" ? 1 + 0.25 * sin(S.t * 17) : S.phase === "draw" ? 0.6 + 0.4 * S.draw : 0.8); + const gl = burn * (0.08 + 0.92 * S.glow); + // Room, lectern, cover and candle wax, lit by the flame and the sigil. + for (let p = 0, q = 0; p < N; p++, q += 3) + for (let c = 0; c < 3; c++) buf[q + c] = amb[q + c] + lit[q + c] * fl + mc[c] * GLF[p] * gl * 0.8; + // The lying spread: the left page shows the back of the last sheet that + // turned, the right one the next sheet. + const left = S.leftPage, right = S.page * 2; + for (let p = 0; p < N; p++) { + const cv = SPC[p]; + if (!cv) continue; + const x = SPX[p], pg = x < 0 ? left : right; + const k = ink(pg, abs(x) / PW, (SPY[p] + PH / 2) / PH), lv = 0.22 + 1.7 * SPL[p] * fl, q = 3 * p; + for (let c = 0; c < 3; c++) { + const v = (k ? INKC[c] : PAPER[c]) * lv + mc[c] * GLF[p] * gl * 0.6; + buf[q + c] += (v - buf[q + c]) * cv; + } + } + // The next sheet flutters in the draft, under the hovering sigil. + zb.fill(-1e9); + if (S.wind > 0.05 && S.phase !== "turn") sheet(S, { num: S.page }, 0.05 * S.wind * (0.7 + 0.3 * sin(S.t * 7) * sin(S.t * 2.9))); + // The sigil: strokes up to where the pen is, each LED a bright line with a glow. + mg.fill(0); + if (S.sgA > 0) { + let budget = (S.phase === "draw" ? S.draw : 1) * sg.total, pen = null; + sg.strokes.forEach((st, si) => { + if (budget <= 0) return; + if (!st) { + sg.glyphs.forEach((g, i) => { + if (budget > i * 4 && !g.gone) { + const [gx, gy, gi] = glyphAt(S, g); + for (let b = 0; b < 9; b++) if ((GLYPH[gi] >> (8 - b)) & 1) put(mg, floor(gx) - 1 + (b % 3), floor(gy) - 1 + floor(b / 3), 1); + } + }); + budget -= sg.len[si]; + return; + } + let l = 0; + for (let i = 1; i < st.length && l < budget; i++) { + const [ax, ay] = st[i - 1], [bx, by] = st[i], sl = Math.hypot(bx - ax, by - ay); + for (let f = 0; f < sl && l + f < budget; f += 0.45) { + const x = ax + ((bx - ax) * f) / sl, y = ay + ((by - ay) * f) / sl; + proj(x, y, ZS, o); + put(mg, floor(o[0]), floor(o[1]), 1); + pen = [o[0], o[1]]; + } + l += sl; + } + budget -= sg.len[si]; + }); + const a = S.sgA * (0.55 + 0.45 * S.glow), core = [0.3 + 0.7 * mc[0], 0.3 + 0.7 * mc[1], 0.3 + 0.7 * mc[2]]; + for (let y = 60; y < 104; y++) + for (let x = 1; x < W - 1; x++) { + const p = y * W + x, m = mg[p], h = (mg[p - 1] + mg[p + 1] + mg[p - W] + mg[p + W]) * 0.25 + (mg[p - W - 1] + mg[p - W + 1] + mg[p + W - 1] + mg[p + W + 1]) * 0.12; + if (!m && !h) continue; + for (let c = 0; c < 3; c++) { + const q = 3 * p + c; + buf[q] = m ? buf[q] + (core[c] * min(1, burn + 0.2) - buf[q]) * min(1, a * 1.6) : buf[q] + mc[c] * h * a * 0.6; + } + } + // The pen: a white-hot point where the line is being drawn. + if (pen && S.phase === "draw") glow(buf, pen[0], pen[1], [1, 1, 0.9], 1.8, 1); + } + // Turning sheets, over the spread and its sigil. + zb.fill(-1e9); + for (const pg of S.pages) { + const u = (S.pt - pg.start) / pg.dur; + if (u > 0 && u < 1) sheet(S, pg, u); + } + // The candle flame, leaning with the draft, and its halo. + const lean = S.lean, fh = 6 + 1.2 * sin(S.t * 9) + 0.6 * (fl - 0.82) * 5; + for (let y = floor(FY - fh - 1); y <= FY + 4; y++) + for (let x = FX - 5; x <= FX + 5; x++) { + const v = (FY + 3.5 - (y + 0.5)) / (fh + 3.5); + if (v < 0 || v > 1 || x < 0) continue; + const cx = FX + lean * v * v, wdt = 1.9 * sin(PI * (0.15 + 0.85 * v) ** 0.8) * (1 - v * 0.6), dx = abs(x + 0.5 - cx); + if (dx > wdt + 0.5) continue; + const a = cl(wdt + 0.5 - dx, 0, 1), hot = 1 - v, q = 3 * (y * W + x), col = [1, 0.45 + 0.5 * hot, 0.1 + 0.6 * hot * hot]; + for (let c = 0; c < 3; c++) buf[q + c] += (col[c] * fl * 1.15 - buf[q + c]) * a; + } + glow(buf, FX + lean * 0.5, FY - 2, [1, 0.5, 0.15], 9, 0.28 * fl); + // Wisps, rising glyphs and sparks in the colour of the magic. + for (let p = 0, q = 0; p < N; p++, q += 3) { + const w = ws[p]; + if (w > 0.01) { + const a = min(1, w) * (0.3 + 0.3 * S.glow) * min(1, floor(p / W) / 30); + for (let c = 0; c < 3; c++) buf[q + c] += mc[c] * a; + } + } + for (const r of S.risers) { + const a = min(1, r.age / 0.2) * cl((r.y - 4) / 34, 0, 1), x0 = floor(r.x) - 1, y0 = floor(r.y) - 1, gb = GLYPH[r.g]; + for (let b = 0; b < 9; b++) { + const x = x0 + (b % 3), y = y0 + floor(b / 3); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const q = 3 * (y * W + x), on = (gb >> (8 - b)) & 1 ? a : a * 0.25; + for (let c = 0; c < 3; c++) buf[q + c] += ((0.35 + 0.65 * mc[c]) * 1.1 - buf[q + c]) * on; + } + } + for (const s of S.sparks) { + const x = floor(s.x), y = floor(s.y); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const a = (1 - s.age / s.life) * (0.6 + 0.4 * sin(s.age * 40)), q = 3 * (y * W + x); + for (let c = 0; c < 3; c++) buf[q + c] += (0.5 + 0.5 * mc[c]) * a; + } + for (let y = 0, q = 0; y < H; y++) + for (let x = 0; x < W; x++, q += 3) display.setPixel(x, y, min(255, round(buf[q] * 255)), min(255, round(buf[q + 1] * 255)), min(255, round(buf[q + 2] * 255))); +} + +function put(mg, x, y, v) { + if (x >= 0 && x < W && y >= 0 && y < H) mg[y * W + x] = v; +} +// A round soft light added at (x, y). +function glow(buf, gx, gy, col, r, a) { + for (let y = max(0, floor(gy - 2 * r)); y < min(H, gy + 2 * r); y++) + for (let x = max(0, floor(gx - 2 * r)); x < min(W, gx + 2 * r); x++) { + const dx = x + 0.5 - gx, dy = y + 0.5 - gy, f = a * exp(-(dx * dx + dy * dy) / (r * r)), q = 3 * (y * W + x); + for (let c = 0; c < 3; c++) buf[q + c] += col[c] * f; + } +} diff --git a/incoming/pattern-study/patterns/hat-monotile.js b/incoming/pattern-study/patterns/hat-monotile.js new file mode 100644 index 000000000..975e22fa9 --- /dev/null +++ b/incoming/pattern-study/patterns/hat-monotile.js @@ -0,0 +1,382 @@ +// ===== Patternflow pattern ===== +// Title: Hat Monotile +// Family: cells +// Idea: The panel fills with copies of one 13-sided tile, the hat, cluster by cluster in the order its metatile substitution nests them, coloured by the metatile each hat belongs to with the rare mirrored hats in orange, and while the window drifts across a patch of 7,921 hats that never repeats, waves of colour regroup the same hats into the bigger clusters of the next two levels. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the hat, an aperiodic monotile (David Smith, Joseph Samuel Myers, Craig S. Kaplan and Chaim Goodman-Strauss, 2023), built by its H/T/P/F metatile substitution as in Kaplan's hatviz (reimplemented), with the paper's continuous family Tile(a, b), pattern round 51 +// Round: 51 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Size=9..16, Shape=-1..1 +// +// Knob 1: Hue. Turns the five label colours round the wheel together. At 0 the H hats are blue, the mirrored H1 hats orange, T yellow, P magenta and F green. +// Knob 2: Speed. At 2 the panel fills in 1.6 seconds and the window drifts 2.5 LEDs a second. A cycle takes 36 seconds: the colours regroup at 11 and 17 seconds and fall back at 23, and from 34.4 the panel clears to build again at 36; at 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Size. How many LEDs across one hat is, from 9 to 16. At 9 about 220 hats share the panel, at 16 about 70. +// Knob 4: Shape. Re-measures every edge of the same tiling with two lengths, a for the edges along the hexagon sides and b for the others, as Tile(a, b) of the paper: at -1 the comet Tile(1, 0), at -0.5 the turtle Tile(sqrt 3, 1), at -0.25 Tile(1, 1), from 0 to 0.12 the hat Tile(1, sqrt 3) itself, at 1 the chevron Tile(0, 1). +// Buttons: K1 tap clears the panel cluster by cluster and builds it again. + +const W = 64, H = 128, NP = W * H; +const { floor, ceil, min, max, round, sin, cos, atan2, sqrt, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ease = (u) => ((u = cl(u, 0, 1)), u * u * (3 - 2 * u)); + +// The substitution. This is Craig Kaplan's hatviz construction, rewritten: +// four metatiles H, T, P and F, each a polygon holding hats (H four, one of +// them mirrored, T one, P and F two), are glued into a patch by a fixed list +// of rules and the patch is cut into the four metatiles of the next level. +// Transforms are affine maps [a, b, c, d, e, f]: x' = a x + b y + c, y' = d x + e y + f. +const R3 = sqrt(3) / 2; +const pt = (x, y) => ({ x, y }); +const hexPt = (x, y) => pt(x + 0.5 * y, R3 * y); +const ID = [1, 0, 0, 0, 1, 0]; +const mul = (A, B) => [A[0] * B[0] + A[1] * B[3], A[0] * B[1] + A[1] * B[4], A[0] * B[2] + A[1] * B[5] + A[2], A[3] * B[0] + A[4] * B[3], A[3] * B[1] + A[4] * B[4], A[3] * B[2] + A[4] * B[5] + A[5]]; +function inv(T) { + const d = T[0] * T[4] - T[1] * T[3]; + return [T[4] / d, -T[1] / d, (T[1] * T[5] - T[2] * T[4]) / d, -T[3] / d, T[0] / d, (T[2] * T[3] - T[0] * T[5]) / d]; +} +const add = (p, q) => pt(p.x + q.x, p.y + q.y), sub = (p, q) => pt(p.x - q.x, p.y - q.y); +const move = (x, y) => [1, 0, x, 0, 1, y]; +const turnAbout = (p, a) => mul(move(p.x, p.y), mul([cos(a), -sin(a), 0, sin(a), cos(a), 0], move(-p.x, -p.y))); +const apply = (M, p) => pt(M[0] * p.x + M[1] * p.y + M[2], M[3] * p.x + M[4] * p.y + M[5]); +// The similarity taking segment p1 q1 onto segment p2 q2. +const seg = (p, q) => [q.x - p.x, p.y - q.y, p.x, q.y - p.y, q.x - p.x, p.y]; +const match = (p1, q1, p2, q2) => mul(seg(p2, q2), inv(seg(p1, q1))); +function cross(p1, q1, p2, q2) { + const d = (q2.y - p2.y) * (q1.x - p1.x) - (q2.x - p2.x) * (q1.y - p1.y); + const u = ((q2.x - p2.x) * (p1.y - p2.y) - (q2.y - p2.y) * (p1.x - p2.x)) / d; + return pt(p1.x + u * (q1.x - p1.x), p1.y + u * (q1.y - p1.y)); +} +// The hat on the triangular lattice, eight kites of a hexagon grid. The +// midpoint of its one long edge is kept as a vertex, so the outline has 14 +// corners and every edge is one of two kinds. +const HAT = [[0, 0], [-1, -1], [0, -2], [1, -2], [2, -2], [2, -1], [4, -2], [5, -1], [4, 0], [3, 0], [2, 2], [0, 3], [0, 2], [-1, 2]].map(([a, b]) => hexPt(a, b)); +const NV = 14; +// Edge kind: 0 for a unit edge along a hexagon side (length a), 1 for a +// sqrt 3 edge from a hexagon's centre to the middle of a side (length b). +const KIND = HAT.map((p, i) => (abs1(sub(HAT[(i + 1) % NV], p)) < 1.01 ? 0 : 1)); +function abs1(p) { + return sqrt(p.x * p.x + p.y * p.y); +} +// Hat labels: 0 H, 1 H1 (the mirrored one), 2 T, 3 P, 4 F. +const hat = (label) => ({ hat: true, label }); +const meta = (shape, label) => ({ shape, label, kids: [] }); +const kid = (m, T, g) => m.kids.push({ T, g }); +const at = (m, n, i) => apply(m.kids[n].T, m.kids[n].g.shape[i]); +function recentre(m) { + let cx = 0, cy = 0; + for (const p of m.shape) (cx += p.x), (cy += p.y); + (cx /= m.shape.length), (cy /= m.shape.length); + m.shape = m.shape.map((p) => pt(p.x - cx, p.y - cy)); + for (const k of m.kids) k.T = mul(move(-cx, -cy), k.T); +} +function first() { + const Ho = [pt(0, 0), pt(4, 0), pt(4.5, R3), pt(2.5, 5 * R3), pt(1.5, 5 * R3), pt(-0.5, R3)]; + const h = meta(Ho, 0), t = meta([pt(0, 0), pt(3, 0), pt(1.5, 3 * R3)], 2); + const p = meta([pt(0, 0), pt(4, 0), pt(3, 2 * R3), pt(-1, 2 * R3)], 3); + const f = meta([pt(0, 0), pt(3, 0), pt(3.5, R3), pt(3, 2 * R3), pt(-1, 2 * R3)], 4); + // The hat outline without the long edge's midpoint, as hatviz indexes it. + const o = HAT.filter((_, i) => i !== 3); + kid(h, match(o[5], o[7], Ho[5], Ho[0]), hat(0)); + kid(h, match(o[9], o[11], Ho[1], Ho[2]), hat(0)); + kid(h, match(o[5], o[7], Ho[3], Ho[4]), hat(0)); + kid(h, mul(move(2.5, R3), mul([-0.5, -R3, 0, R3, -0.5, 0], [0.5, 0, 0, 0, -0.5, 0])), hat(1)); + kid(t, [0.5, 0, 0.5, 0, 0.5, R3], hat(2)); + for (const [m, l] of [[p, 3], [f, 4]]) { + kid(m, [0.5, 0, 1.5, 0, 0.5, R3], hat(l)); + kid(m, mul(move(0, 2 * R3), mul([0.5, R3, 0, -R3, 0.5, 0], [0.5, 0, 0, 0, 0.5, 0])), hat(l)); + } + return [h, t, p, f]; +} +// The patch rules: [shape] for the first, [child, edge, shape, its edge] to +// glue a new metatile to one edge of a child, or [child, corner, child, +// corner, shape, its edge] to glue it between corners of two children. +const RULES = [["H"], [0, 0, "P", 2], [1, 0, "H", 2], [2, 0, "P", 2], [3, 0, "H", 2], [4, 4, "P", 2], [0, 4, "F", 3], [2, 4, "F", 3], [4, 1, 3, 2, "F", 0], [8, 3, "H", 0], [9, 2, "P", 0], [10, 2, "H", 0], [11, 4, "P", 2], [12, 0, "H", 2], [13, 0, "F", 3], [14, 2, "F", 1], [15, 3, "H", 4], [8, 2, "F", 1], [17, 3, "H", 0], [18, 2, "P", 0], [19, 2, "H", 2], [20, 4, "F", 3], [20, 0, "P", 2], [22, 0, "H", 2], [23, 4, "F", 3], [23, 0, "F", 3], [16, 0, "P", 2], [9, 4, 0, 2, "T", 2], [4, 0, "F", 3]]; +function patch([h, t, p, f]) { + const out = meta([], -1), S = { H: h, T: t, P: p, F: f }; + for (const r of RULES) { + if (r.length === 1) kid(out, ID, S[r[0]]); + else if (r.length === 4) { + const c = out.kids[r[0]], poly = c.g.shape, g = S[r[2]], n = g.shape.length; + kid(out, match(g.shape[r[3]], g.shape[(r[3] + 1) % n], apply(c.T, poly[(r[1] + 1) % poly.length]), apply(c.T, poly[r[1]])), g); + } else { + const g = S[r[4]], n = g.shape.length; + kid(out, match(g.shape[r[5]], g.shape[(r[5] + 1) % n], at(out, r[2], r[3]), at(out, r[0], r[1])), g); + } + } + return out; +} +function next(P) { + const b1 = at(P, 8, 2), b2 = at(P, 21, 2), rb = apply(turnAbout(b1, (-2 * PI) / 3), b2); + const p72 = at(P, 7, 2), p252 = at(P, 25, 2), llc = cross(b1, rb, at(P, 6, 2), p72); + let w = apply(turnAbout(pt(0, 0), -PI / 3), sub(at(P, 6, 2), llc)); + const nH = [llc, b1, add(b1, w), at(P, 14, 2)]; + w = apply(turnAbout(pt(0, 0), -PI / 3), w); + nH.push(sub(nH[3], w), at(P, 6, 2)); + const take = (m, list) => (list.forEach((i) => kid(m, P.kids[i].T, P.kids[i].g)), m); + const h = take(meta(nH, 0), [0, 9, 16, 27, 26, 6, 1, 8, 10, 15]); + const p = take(meta([p72, add(p72, sub(b1, llc)), b1, llc], 3), [7, 2, 3, 4, 28]); + const f = take(meta([b2, at(P, 24, 2), at(P, 25, 0), p252, add(p252, sub(llc, b1))], 4), [21, 20, 22, 23, 24, 25]); + const A = nH[2], B = add(nH[1], sub(nH[4], nH[5])); + const t = take(meta([B, apply(turnAbout(B, -PI / 3), A), A], 2), [11]); + for (const m of [h, t, p, f]) recentre(m); + return [h, t, p, f]; +} + +// Four substitutions: an H metatile of 7,921 hats, 1,009 of them mirrored. +// Each hat keeps its label and its turn (0..5, sixty degrees each), with the +// index and label of the cluster holding it at levels 0 to 3 (level 0 is the +// metatile of one to four hats, level 3 a cluster of up to 1,156). The order +// of the list is the order the substitution nests them. +const LEVELS = 4; +const TILING = (() => { + let m = first(); + for (let i = 0; i < LEVELS; i++) m = next(patch(m)); + const list = []; + let serial = 0; + const walk = (g, T, ids, labs) => { + if (g.hat) return list.push({ T, label: g.label, ids: ids.slice(), labs: labs.slice() }); + ids.push(serial++); + labs.push(g.label); + for (const k of g.kids) walk(k.g, mul(T, k.T), ids, labs); + ids.pop(); + labs.pop(); + }; + walk(m[0], ID, [], []); + const n = list.length; + const label = new Uint8Array(n), turn = new Uint8Array(n), cid = new Int32Array(n * 4), clab = new Uint8Array(n * 4); + const vx = new Float64Array(n * NV), vy = new Float64Array(n * NV); + list.forEach((h, k) => { + label[k] = h.label; + turn[k] = (round(atan2(h.T[3], h.T[0]) / (PI / 3)) + 6) % 6; + // ids run from the top (level 4) down to level 0. + for (let l = 0; l < 4; l++) (cid[k * 4 + l] = h.ids[LEVELS - l]), (clab[k * 4 + l] = h.labs[LEVELS - l]); + HAT.forEach((p, i) => { + const q = apply(h.T, p); + (vx[k * NV + i] = q.x), (vy[k * NV + i] = q.y); + }); + }); + // Lift every corner into two parts, the sum of the a edges and the sum of + // the b edges along a path to it from one fixed corner. An a edge always + // meets an a edge, so the lift does not depend on the path, and a * A + + // b * B is the same tiling made of Tile(a, b). + // Corners are keyed by their lattice place relative to the first corner. + const key = (x, y) => (round((x - vx[0]) * 4) + 4096) * 16384 + round((y - vy[0]) / (R3 / 2)) + 4096; + const share = new Map(); + for (let i = 0; i < n * NV; i++) { + const kk = key(vx[i], vy[i]); + const l = share.get(kk); + l ? l.push(i) : share.set(kk, [i]); + } + const LA = new Float32Array(n * NV * 4), known = new Map(), seen = new Uint8Array(n), stack = [0]; + known.set(key(vx[0], vy[0]), [0, 0, 0, 0]); + seen[0] = 1; + while (stack.length) { + const k = stack.pop(); + let i0 = 0; + while (!known.has(key(vx[k * NV + i0], vy[k * NV + i0]))) i0++; + let [ax, ay, bx, by] = known.get(key(vx[k * NV + i0], vy[k * NV + i0])); + for (let s = 0; s < NV; s++) { + const i = (i0 + s) % NV, j = (i + 1) % NV, o = k * NV + i, kk = key(vx[o], vy[o]); + if (!known.has(kk)) known.set(kk, [ax, ay, bx, by]); + LA.set([ax, ay, bx, by], o * 4); + for (const r of share.get(kk)) { + const h = (r / NV) | 0; + if (!seen[h]) (seen[h] = 1), stack.push(h); + } + const dx = vx[k * NV + j] - vx[o], dy = vy[k * NV + j] - vy[o]; + if (KIND[i]) (bx += dx), (by += dy); + else (ax += dx), (ay += dy); + } + } + // Over the whole patch the a part grows as a fixed similarity MA of the + // position (a least-squares fit), the b part as I - MA. Dividing by the mix + // keeps any Tile(a, b) the hat's size and lined up with the hat tiling. + let a11 = 0, a12 = 0, a21 = 0, a22 = 0, xx = 0, xy = 0, yy = 0; + for (let i = 0; i < n * NV; i++) { + const px = vx[i] - vx[0], py = vy[i] - vy[0], A = LA[i * 4], B = LA[i * 4 + 1]; + (a11 += A * px), (a12 += A * py), (a21 += B * px), (a22 += B * py), (xx += px * px), (xy += px * py), (yy += py * py); + } + const d = xx * yy - xy * xy; + const MA = [(a11 * yy - a12 * xy) / d, (a12 * xx - a11 * xy) / d, (a21 * yy - a22 * xy) / d, (a22 * xx - a21 * xy) / d]; + // The patch's middle in lift coordinates: the top metatile is centred on 0. + return { n, label, turn, cid, clab, LA, MA, cx: -vx[0], cy: -vy[0] }; +})(); +const NH = TILING.n; + +// Size of a hat across, in lattice units: the hats sit at half scale. +const ACROSS = 2.75; +// Story seconds at Speed 2 for the build and the cycle. WAVES says when the +// colours regroup and to which level; a wave and a clearing take WAVE and +// CLEAR seconds, and the window drifts DRIFT LEDs a second. +const BUILD = 1.6, CYCLE = 36, WAVES = [[11, 1], [17, 2], [23, 0]], WAVE = 1.6, CLEAR = 1.2, DRIFT = 2.5; +// Hue of each label at Hue 0: H blue, H1 orange, T yellow, P magenta, F green. +const HUES = [0.6, 0.075, 0.15, 0.84, 0.36]; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +export function setup(params) { + params.s = { + fresh: true, t: 0, hue: 0, speed: 2, size: 9, shape: 0.06, shapeKey: NaN, + PX: new Float32Array(NH * NV), PY: new Float32Array(NH * NV), CX: new Float32Array(NH), CY: new Float32Array(NH), + born: new Float32Array(NH), gone: new Float32Array(NH).fill(1e9), flip: new Float32Array(NH).fill(-10), from: 0, to: 0, + cycle: 0, waveAt: 0, clearAt: 1e9, arc: 0, ox: 0, oy: 0, u: 1, + id: new Int32Array(NP), col: new Float32Array(NH * 3), lv: new Uint8Array(NH), vis: new Int32Array(NH), nvis: 0, xs: new Float64Array(32), + }; +} + +// Shape knob to the angle of (a, b): comet at 0 degrees, turtle 30, Tile(1, 1) 45, hat 60, chevron 90. +const angle = (k) => (k < 0 ? 60 + 60 * k : k < 0.12 ? 60 : 60 + (30 * (k - 0.12)) / 0.88); + +function reshape(S) { + const th = (angle(S.shape) * PI) / 180, wa = cos(th), wb = sin(th) / sqrt(3), { LA, MA } = TILING; + const L = [wa * MA[0] + wb * (1 - MA[0]), wa * MA[1] - wb * MA[1], wa * MA[2] - wb * MA[2], wa * MA[3] + wb * (1 - MA[3])]; + const d = L[0] * L[3] - L[1] * L[2], i0 = L[3] / d, i1 = -L[1] / d, i2 = -L[2] / d, i3 = L[0] / d; + const { PX, PY, CX, CY } = S; + for (let k = 0; k < NH; k++) { + let sx = 0, sy = 0; + for (let i = k * NV; i < (k + 1) * NV; i++) { + const x = wa * LA[4 * i] + wb * LA[4 * i + 2], y = wa * LA[4 * i + 1] + wb * LA[4 * i + 3]; + sx += PX[i] = i0 * x + i1 * y; + sy += PY[i] = i2 * x + i3 * y; + } + CX[k] = sx / NV; + CY[k] = sy / NV; + } +} + +// The hats whose middles are on the panel now, in nesting order. +function onPanel(S) { + const { CX, CY, u, ox, oy, vis } = S, r = 2 * u + 2; + let n = 0; + for (let k = 0; k < NH; k++) { + const x = CX[k] * u + ox, y = CY[k] * u + oy; + if (x > -r && x < W + r && y > -r && y < H + r) vis[n++] = k; + } + return n; +} + +// Each hat on the panel gets a time in nesting order across span seconds. +function stagger(S, out, t0, span, rest) { + const n = onPanel(S); + out.fill(t0 + rest); + for (let i = 0; i < n; i++) out[S.vis[i]] = t0 + (span * i) / max(1, n - 1); +} + +// The window: a slow loop round the middle of the patch, kept inside it. +function view(S) { + S.u = S.size / ACROSS; + const R = cl(64 - 74 / S.u, 6, 40), a = S.arc / R + 2; + S.ox = round(W / 2 - (TILING.cx + R * cos(a)) * S.u); + S.oy = round(H / 2 - (TILING.cy + 0.8 * R * sin(a)) * S.u); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = cl(k[0], 0, 1); + S.speed = cl(k[1], 0.2, 3); + S.size = cl(k[2], 9, 16); + S.shape = cl(k[3], -1, 1); + if (S.shape !== S.shapeKey) (S.shapeKey = S.shape), reshape(S); + const ds = (d * S.speed) / 2; + S.t += ds; + S.arc += (DRIFT * ds) / (S.size / ACROSS); + view(S); + if (S.fresh) { + S.fresh = false; + stagger(S, S.born, 0, BUILD, BUILD); + } + const press = input.btnPressed && input.btnPressed[0]; + const c = S.t - S.cycle; + if ((press && S.clearAt > S.t + CLEAR) || (c > CYCLE - CLEAR - 0.4 && S.clearAt > 1e8)) { + // Clear in nesting order, then build again where the window is then. + S.clearAt = S.t; + stagger(S, S.gone, S.t, CLEAR, CLEAR); + } + if (S.t > S.clearAt + CLEAR + 0.4) { + S.cycle = S.t; + S.clearAt = 1e9; + S.gone.fill(1e9); + S.from = S.to = 0; + S.waveAt = 0; + stagger(S, S.born, S.t, BUILD, BUILD); + } + for (const [w, v] of WAVES) + if (c >= w && S.waveAt < w) { + S.waveAt = w; + S.from = S.to; + S.to = v; + stagger(S, S.flip, S.t, WAVE, WAVE); + } +} + +// The label a hat shows in a view: its own, or that of the cluster of level +// 1 or 2 holding it. The mirrored hats keep their own colour in every view. +const shown = (k, v) => (v === 0 || TILING.label[k] === 1 ? TILING.label[k] : TILING.clab[k * 4 + v]); + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { id, col, lv, PX, PY, u, ox, oy, xs, born, gone, flip, t } = S, cid = TILING.cid; + id.fill(-1); + const n = onPanel(S), rgb = [0, 0, 0]; + for (let i = 0; i < n; i++) { + const k = S.vis[i]; + // Fill the hat's 14-gon, LED centres inside it. + let y0 = 1e9, y1 = -1e9; + for (let j = k * NV; j < (k + 1) * NV; j++) (y0 = min(y0, PY[j])), (y1 = max(y1, PY[j])); + const r0 = max(0, ceil(y0 * u + oy - 0.5)), r1 = min(H - 1, ceil(y1 * u + oy - 0.5) - 1); + for (let r = r0; r <= r1; r++) { + const py = r + 0.5; + let m = 0; + for (let a = k * NV + NV - 1, b = k * NV; b < (k + 1) * NV; a = b++) { + const ya = PY[a] * u + oy, yb = PY[b] * u + oy; + if (ya <= py !== yb <= py) { + const xa = PX[a] * u + ox; + let x = xa + ((py - ya) * (PX[b] * u + ox - xa)) / (yb - ya), q = m++; + while (q > 0 && xs[q - 1] > x) (xs[q] = xs[q - 1]), q--; + xs[q] = x; + } + } + for (let q = 0; q + 1 < m; q += 2) { + const xa = max(0, ceil(xs[q] - 0.5)), xb = min(W - 1, ceil(xs[q + 1] - 0.5) - 1); + for (let x = xa; x <= xb; x++) id[r * W + x] = k; + } + } + // Its colour: the label's hue, one of six close shades for its turn, + // a white flash as it arrives or changes colour, dark before and after. + const tn = TILING.turn[k], age = t - born[k], fl = t - flip[k]; + const v = (lv[k] = fl >= 0 ? S.to : S.from); + hsv(HUES[shown(k, v)] + S.hue + 0.012 * (tn - 2.5), 0.92 - 0.045 * tn, 1 - 0.02 * tn, rgb, 0); + const glow = max(age >= 0 ? exp(-age / 0.18) : 0, fl >= 0 ? 0.7 * exp(-fl / 0.25) : 0); + const on = age < 0 ? 0 : (1 - ease((t - gone[k]) / 0.3)) * min(1, 0.75 + age / 0.12); + for (let c = 0; c < 3; c++) col[k * 3 + c] = on * (rgb[c] + (1 - rgb[c]) * glow); + } + // A hat that differs from the LED to its right or below loses that LED to + // a dark seam, one LED wide. While a view of level 1 or 2 shows, a seam + // inside one cluster keeps a third of the colour, so the clusters read as + // pieces with black between them. + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const k = id[p]; + if (k < 0) { + display.setPixel(x, y, 0, 0, 0); + continue; + } + const kr = x < W - 1 ? id[p + 1] : k, kd = y < H - 1 ? id[p + W] : k, L = lv[k]; + let f = 1; + if (kr !== k || kd !== k) { + const same = (q) => q === k || (L > 0 && q >= 0 && lv[q] === L && cid[q * 4 + L] === cid[k * 4 + L]); + f = same(kr) && same(kd) ? 0.3 : 0; + } + display.setPixel(x, y, round(col[k * 3] * 255 * f), round(col[k * 3 + 1] * 255 * f), round(col[k * 3 + 2] * 255 * f)); + } +} From 86c0fdfd21944744fb23637c61253140004b88a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 23:29:32 +0000 Subject: [PATCH 4/8] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=205?= =?UTF-8?q?1=ED=8C=90=20=E2=80=94=20Phantom=20Jam,=20Tower=20Stair?= 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_016styVvYJeAE1svDoUBeGKX --- .../pattern-study/patterns/phantom-jam.js | 341 ++++++++++++++++ .../pattern-study/patterns/tower-stair.js | 373 ++++++++++++++++++ 2 files changed, 714 insertions(+) create mode 100644 incoming/pattern-study/patterns/phantom-jam.js create mode 100644 incoming/pattern-study/patterns/tower-stair.js diff --git a/incoming/pattern-study/patterns/phantom-jam.js b/incoming/pattern-study/patterns/phantom-jam.js new file mode 100644 index 000000000..e9860a8f9 --- /dev/null +++ b/incoming/pattern-study/patterns/phantom-jam.js @@ -0,0 +1,341 @@ +// ===== Patternflow pattern ===== +// Title: Phantom Jam +// Family: sim +// Idea: Cars run round a tall ring road at the panel's edge, each driver easing toward the speed its gap allows, and with no crash or roadworks a stop-and-go jam forms and creeps backward round the ring while every car keeps going forward, as the space-time diagram inside the track shows with car lines slanting one way and red jam stripes the other. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the optimal velocity model of traffic (Masako Bando, Katsuya Hasebe, Akihiro Nakayama, Akihiro Shibata and Yuki Sugiyama, 1995) and the circuit experiment of Yuki Sugiyama and seven others (2008), pattern round 51 +// Round: 51 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Cars=22..44, Sensitivity=0.5..2.5 +// +// Knob 1: Hue. The colour of free flow, on the moving cars and their lines: cyan green at 0, blue at 0.5, violet at 1. Stopped cars and the jam stripes stay brake red, with amber for the speeds between. One car stays white so its line can be followed through the jams. +// Knob 2: Speed. At 2 a car at full speed laps the ring in 5 seconds, or 11 at the even spacing of 22 cars, and the diagram takes 10 new rows a second, so it holds the last 11.6 seconds; at 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Cars. How many cars share the ring, from 22, the number in Sugiyama's circuit, to 44. Turning it lets a car join at the widest gap or leave from the tightest, which is a kink of its own. More cars mean shorter gaps and slower flow: at 44 the ring crawls in amber between red queues. +// Knob 4: Sensitivity. How hard each driver chases the speed its gap allows, the model's a. Uniform flow is unstable when a is below twice the slope of the speed law at its gap. At 0.5 every number of cars jams hard and the queues stand still. At 1.5 only 22 to 29 cars are unstable, and a wave takes about a minute to grow. From 2 on every kink heals and the lines run parallel. +// Buttons: K1 tap makes the driver nearest the start line, the white tick at the bottom of the track, brake to a stop. + +const W = 64, H = 128, N = W * H; +const { floor, ceil, min, max, abs, sqrt, sin, cos, tanh, round, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (t) => ((t = cl(t, 0, 1)), t * t * (3 - 2 * t)); +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// The model. Ring length in model units, the headway where the speed law is +// steepest, and the speed law V(h) = tanh(h - C) + tanh(C), which runs from +// 0 at h = 0 to VMAX for long gaps. Uniform flow at headway h is unstable +// when a < 2 V'(h) = 2 / cosh(h - C)^2. +const L = 39, C = 1.87, VMAX = 2 * tanh(C); +const V = (h) => tanh(h - C) + tanh(C); +// Fixed physics step, the smallest bumper-to-bumper headway (a car is 0.35 +// units long), and the model time between two rows of the diagram. +const DT = 0.05, HMIN = 0.47, ROW = 0.4; + +// The track's centre line: straights at x = 3 and x = 61 from y = 14 to +// y = 114, joined by half ellipses 29 LEDs across and 11 deep. Arc length s +// starts at the bottom middle and runs right along the bottom, up the right +// straight, over the top and down the left. +const CX = 32, XL = 3, XR = 61, YT = 14, YB = 114, RX = 29, RY = 11; +const PT = []; +{ + const n = 400; + for (let i = 0; i <= n; i++) { + const a = ((PI / 2) * i) / n; + PT.push([CX + RX * sin(a), YB + RY * cos(a)]); + } + for (let i = 1; i <= n; i++) PT.push([XR, YB - ((YB - YT) * i) / n]); + for (let i = 1; i <= 2 * n; i++) { + const a = (PI * i) / (2 * n); + PT.push([CX + RX * cos(a), YT - RY * sin(a)]); + } + for (let i = 1; i <= n; i++) PT.push([XL, YT + ((YB - YT) * i) / n]); + for (let i = 1; i <= n; i++) { + const a = ((PI / 2) * i) / n; + PT.push([CX - RX * cos(a), YB + RY * sin(a)]); + } + let s = 0; + PT[0].push(0); + for (let i = 1; i < PT.length; i++) PT[i].push((s += Math.hypot(PT[i][0] - PT[i - 1][0], PT[i][1] - PT[i - 1][1]))); +} +const LT = PT[PT.length - 1][2], NB = ceil(LT); +// Where s is on the panel, every half LED of arc. +const SX = new Float32Array(2 * NB + 2), SY = new Float32Array(2 * NB + 2); +for (let i = 0, j = 0; i < SX.length; i++) { + const s = (i / 2) % LT; + if (s < PT[j][2]) j = 0; + while (j < PT.length - 2 && PT[j + 1][2] < s) j++; + const a = PT[j], b = PT[j + 1], u = (s - a[2]) / (b[2] - a[2] || 1); + SX[i] = a[0] + (b[0] - a[0]) * u; + SY[i] = a[1] + (b[1] - a[1]) * u; +} + +// Every LED's arc length and its distance from the centre line (negative +// inside), then its part: 0 dark, 1 outer kerb, 2 lane, 3 inner kerb, 4 the +// gap, 5 the diagram. +const TS = new Float32Array(N), KIND = new Uint8Array(N); +// The diagram: 52 columns for the ring unrolled, 116 rows of time. +const DX0 = 6, DY0 = 6, DCOL = 52, DROWS = 116; +const DIAG = []; +{ + const sR = PT[400][2], sL = PT[1600][2]; + const cap = PT.filter((q) => q[1] < YT || q[1] > YB); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, px = x + 0.5, py = y + 0.5; + let d, s; + if (py >= YT && py <= YB) { + const dl = px - XL, dr = XR - px; + if (dl < dr) (d = -dl), (s = sL + (py - YT)); + else (d = -dr), (s = sR + (YB - py)); + } else { + let best = 1e9, q = cap[0]; + for (const c of cap) { + const e = (c[0] - px) ** 2 + (c[1] - py) ** 2; + if (e < best) (best = e), (q = c); + } + const ey = py < YT ? YT : YB, inside = ((px - CX) / RX) ** 2 + ((py - ey) / RY) ** 2 < 1; + d = inside ? -sqrt(best) : sqrt(best); + s = q[2]; + } + TS[p] = s; + const k = d > 2 ? 0 : d > 1 ? 1 : d > -1 ? 2 : d > -2 ? 3 : d > -3 ? 4 : 5; + KIND[p] = k; + if (k === 5) DIAG.push(p, x - DX0, y - DY0); + } +} +// Lane LEDs by whole LED of arc, for drawing cars. +const LANE = Array.from({ length: NB }, () => []); +for (let p = 0; p < N; p++) if (KIND[p] === 2) LANE[floor(TS[p]) % NB].push(p); + +export function setup(params) { + params.s = { + hue: -1, speed: 2, n: 0, a: 0.5, clock: 0, head: 0, cars: [], + lineA: new Float32Array(DROWS * DCOL), lineF: new Float32Array(DROWS * DCOL), field: new Float32Array(DROWS * DCOL), + lut: new Float32Array(66 * 6), buf: new Float32Array(N * 3), ready: false, + }; +} + +// The colour of a speed, f = v / VMAX: brake red when stopped, amber at an +// eighth of top speed, the free-flow colour from a third of it. +function speedColour(f, mov) { + if (f < 0.12) { + const u = sm((f - 0.03) / 0.09); + return [1, 0.06 + 0.44 * u, 0.03 * (1 - u)]; + } + const u = sm((f - 0.12) / 0.23), c = [1 + (mov[0] - 1) * u, 0.5 + (mov[1] - 0.5) * u, mov[2] * u], m = max(c[0], c[1], c[2]); + return [c[0] / m, c[1] / m, c[2] / m]; +} + +// One physics step of every driver: dv/dt = a (V(h) - v), with a little +// jitter in each driver's foot. A braking driver aims for 0 instead. +function step(S) { + const cars = S.cars, n = cars.length; + for (let i = 0; i < n; i++) { + const c = cars[i], h = (cars[(i + 1) % n].x - c.x + L) % L || L; + let acc = S.a * (V(h) - c.v); + if (c.brake > 0) { + acc = -2.5 * c.v; + c.brake -= DT; + } + c.acc = acc + 0.06 * (Math.random() - 0.5); + } + for (const c of cars) { + c.v = max(0, c.v + c.acc * DT); + c.x = (c.x + c.v * DT) % L; + } + // No one drives into the car ahead. + for (let i = 0; i < n; i++) { + const c = cars[i], f = cars[(i + 1) % n], h = (f.x - c.x + L) % L; + if (n > 1 && h < HMIN) { + c.x = (f.x - HMIN + L) % L; + c.v = min(c.v, f.v); + } + } + if ((S.clock += DT) >= ROW - 1e-9) { + S.clock -= ROW; + record(S); + } +} + +// A new top row of the diagram: each car's stretch since the last row as a +// line one column wide, coloured by its speed, over the speed field between +// the cars, which shows dark red where the traffic stands. +function record(S) { + const cars = S.cars, n = cars.length; + S.head = (S.head + 1) % DROWS; + const o = S.head * DCOL, A = S.lineA, F = S.lineF, G = S.field; + A.fill(0, o, o + DCOL); + G.fill(1, o, o + DCOL); + for (const c of cars) { + const c1 = (c.x / L) * DCOL; + let c0 = c.pc < 0 ? c1 : c.pc; + if (c0 > c1 + DCOL / 2) c0 -= DCOL; + const lo = min(c1 - 0.5, c0 + 0.5), hi = c1 + 0.5, f = c.tr ? 2 : c.v / VMAX; + for (let j = floor(lo); j <= floor(hi); j++) { + const cov = min(hi, j + 1) - max(lo, j), jj = (j + DCOL) % DCOL; + if (cov > A[o + jj] || (c.tr && cov > 0.3)) (A[o + jj] = cov), (F[o + jj] = f); + } + c.pc = c1; + } + for (let i = 0; i < n; i++) { + const c = cars[i], d = cars[(i + 1) % n], x0 = (c.x / L) * DCOL; + let x1 = (d.x / L) * DCOL; + if (x1 <= x0) x1 += DCOL; + const f0 = c.v / VMAX, f1 = d.v / VMAX; + for (let j = ceil(x0 - 0.5); j <= floor(x1 - 0.5); j++) { + const u = (j + 0.5 - x0) / (x1 - x0 || 1); + G[o + (j % DCOL)] = f0 + (f1 - f0) * u; + } + } +} + +// Cars evenly spaced at the equilibrium speed, then the diagram's whole +// height of history run at once, one driver lifting off a little at the +// start of it. +function begin(S) { + const n = S.n, h0 = L / n, v0 = V(h0); + S.cars = []; + for (let i = 0; i < n; i++) S.cars.push({ x: i * h0, v: v0, acc: 0, brake: 0, pc: -1 }); + S.cars[0].v = v0 * 0.6; + S.cars[0].tr = true; + S.lineA.fill(0); + S.field.fill(1); + S.clock = 0; + const steps = round((DROWS * ROW) / DT); + for (let i = 0; i < steps; i++) step(S); +} + +// K3 moved: one car joins in the widest gap or leaves the tightest. +function retune(S) { + const cars = S.cars, n = cars.length; + let best = -1, at = 0; + for (let i = 0; i < n; i++) { + const h = (cars[(i + 1) % n].x - cars[i].x + L) % L; + if (S.n > n ? h > best : best < 0 || h < best) (best = h), (at = i); + } + if (S.n > n) { + const a = cars[at], b = cars[(at + 1) % n]; + cars.splice(at + 1, 0, { x: (a.x + best / 2) % L, v: (a.v + b.v) / 2, acc: 0, brake: 0, pc: -1 }); + } else { + if (cars[at].tr) cars[(at + 1) % n].tr = true; + cars.splice(at, 1); + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.speed = cl(k[1], 0.2, 3); + S.a = cl(k[3], 0.5, 2.5); + S.n = round(cl(k[2], 22, 44)); + if (!S.ready) { + S.ready = true; + begin(S); + } else if (S.n !== S.cars.length) retune(S); + if (input.btnPressed && input.btnPressed[0]) { + let best = 1e9, who = S.cars[0]; + for (const c of S.cars) { + const e = min(c.x, L - c.x); + if (e < best) (best = e), (who = c); + } + who.brake = 2; + } + S.acc = (S.acc || 0) + d * S.speed * 2; + while (S.acc >= DT) { + S.acc -= DT; + step(S); + } + // Hue: free flow from cyan green round to violet. + const hue = cl(k[0], 0, 1); + if (hue !== S.hue) { + S.hue = hue; + S.mov = hsv(0.42 + 0.4 * hue, 0.85, 1); + // Lines: stopped cars bright, free ones dimmer, so the jams stand out + // of the hatching; the last entry is the one white car. Under them the + // speed field, dark red where the traffic stands. + for (let i = 0; i <= 65; i++) { + const f = i / 64, c = i > 64 ? [1, 1, 1] : speedColour(f, S.mov), b = i > 64 ? 1 : 0.95 - 0.45 * sm((f - 0.05) / 0.3), o = 6 * i, jam = 0.22 * sm(1 - f / 0.12); + S.lut[o] = c[0] * b; + S.lut[o + 1] = c[1] * b; + S.lut[o + 2] = c[2] * b; + S.lut[o + 3] = jam; + S.lut[o + 4] = jam * 0.08; + S.lut[o + 5] = jam * 0.12; + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.ready) return; + const buf = S.buf, lut = S.lut, mov = S.mov; + buf.fill(0); + // The diagram, newest row at the top. + const A = S.lineA, F = S.lineF, G = S.field; + for (let i = 0; i < DIAG.length; i += 3) { + const p = DIAG[i], r = (S.head - DIAG[i + 2] + DROWS) % DROWS, o = r * DCOL + DIAG[i + 1]; + const g = 6 * round(cl(G[o], 0, 1) * 64), a = A[o] * (0.9 - (0.5 * DIAG[i + 2]) / DROWS), l = 6 * (F[o] > 1.5 ? 65 : round(cl(F[o], 0, 1) * 64)), q = 3 * p; + buf[q] = lut[g + 3] + lut[l] * a; + buf[q + 1] = lut[g + 4] + lut[l + 1] * a; + buf[q + 2] = lut[g + 5] + lut[l + 2] * a; + } + // The road: dim kerbs tinted by the free-flow colour, dark lane, a white + // start line at the bottom middle. + const kerb = [0.12 + 0.08 * mov[0], 0.12 + 0.08 * mov[1], 0.12 + 0.08 * mov[2]]; + for (let p = 0; p < N; p++) { + const k = KIND[p], q = 3 * p; + if (k === 1 || k === 3) (buf[q] = kerb[0]), (buf[q + 1] = kerb[1]), (buf[q + 2] = kerb[2]); + else if (k === 2) { + const sl = TS[p] < 1 || TS[p] > LT - 1; + buf[q] = sl ? 0.5 : 0.04; + buf[q + 1] = sl ? 0.5 : 0.04; + buf[q + 2] = sl ? 0.5 : 0.05; + } + } + // Brake lights glow on the road round each braking car's tail. + for (const c of S.cars) { + if (c.acc > -0.12 && c.v > 0.08) continue; + const s = (c.x / L) * LT, gx = SX[round(2 * s) % SX.length], gy = SY[round(2 * s) % SY.length]; + const amp = c.v < 0.08 ? 0.35 : 0.5; + for (let y = floor(gy - 4); y <= ceil(gy + 4); y++) + for (let x = floor(gx - 4); x <= ceil(gx + 4); x++) { + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const r = Math.hypot(x + 0.5 - gx, y + 0.5 - gy); + if (r > 4) continue; + const w = amp * (1 - r / 4) ** 2, q = 3 * (y * W + x); + buf[q] += w; + buf[q + 1] += w * 0.06; + buf[q + 2] += w * 0.04; + } + } + // The cars: dashes three LEDs long, coloured by speed, tail lit red when braking. + for (const c of S.cars) { + const sr = (c.x / L) * LT, col = c.tr ? [1, 1, 1] : speedColour(c.v / VMAX, mov), braking = c.acc < -0.12 || c.v < 0.08; + for (let b = floor(sr) - 1; b <= floor(sr) + 4; b++) { + for (const p of LANE[((b % NB) + NB) % NB]) { + let u = TS[p] - sr; + if (u < -LT / 2) u += LT; + if (u > LT / 2) u -= LT; + const cov = min(u + 0.5, 3) - max(u - 0.5, 0); + if (cov <= 0) continue; + const q = 3 * p, w = min(1, cov); + const tail = braking && u < 1, front = u > 2; + const r = tail ? 1 : front ? 0.6 + 0.4 * col[0] : col[0], g = tail ? 0.05 : front ? 0.6 + 0.4 * col[1] : col[1], bl = tail ? 0.03 : front ? 0.6 + 0.4 * col[2] : col[2]; + buf[q] += (r - buf[q]) * w; + buf[q + 1] += (g - buf[q + 1]) * w; + buf[q + 2] += (bl - buf[q + 2]) * w; + } + } + } + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const q = 3 * p; + display.setPixel(x, y, round(cl(buf[q], 0, 1) * 255), round(cl(buf[q + 1], 0, 1) * 255), round(cl(buf[q + 2], 0, 1) * 255)); + } +} diff --git a/incoming/pattern-study/patterns/tower-stair.js b/incoming/pattern-study/patterns/tower-stair.js new file mode 100644 index 000000000..b52bf7409 --- /dev/null +++ b/incoming/pattern-study/patterns/tower-stair.js @@ -0,0 +1,373 @@ +// ===== Patternflow pattern ===== +// Title: Tower Stair +// Family: halloween +// Idea: A hooded climber carries a candle up the spiral stair of a round stone tower cut open to show its inside, the flame lighting the curved wall and throwing his huge shadow round it as he winds up past slit windows full of purple night, until a draft at the top blows the candle out and two green eyes look in at the window in the dark; then the candle is lit again at the foot of the stair. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 51 +// Round: 51 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Turns=3..8, Reach=-1..1 +// +// Knob 1: Hue. The moonlight on the stone and the night in the windows, through the cold colours: purple at 0, indigo at 0.3, blue at 0.6, green at 1. The flame stays orange and the eyes green. +// Knob 2: Speed. At 2 the climb from the foot to the top takes 10 seconds with 3 turns and 16 with 8, then the draft and the dark take 2.8 seconds before a match is struck at the foot again; at 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Turns. How many times the stair winds round in the panel's height, from 3 to 8, and the whole tower is rebuilt at once. At 3 a turn has 15 steps and the climber stands 21 LEDs tall; at 8 the stair is a tight coil of 7 steps a turn and he is 7 LEDs tall. +// Knob 4: Reach. How far the candle light carries before it falls to half: 14 LEDs at -1, a small warm pool round the climber; 29 at the opening 0.06; 56 at 1, so the wall across the well lights up and the shadow stands out on it. +// Buttons: K1 tap blows the candle out where it is. + +const W = 64, H = 128, N = W * H; +const { floor, ceil, min, max, abs, sqrt, sin, cos, atan2, round, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (t) => ((t = cl(t, 0, 1)), t * t * (3 - 2 * t)); +const wrap = (a) => a - TAU * floor((a + PI) / TAU); +const ang = (x, z) => { + const a = atan2(x, z); + return a < 0 ? a + TAU : a; +}; +const hash = (i) => { + let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// The tower in LEDs: its axis at x = 32, the wall's inside radius, the +// treads' inner edge and the line the climber walks. The view looks down a +// little, so a point nearer by z sits K lower: screen y = YB - Y + K z. +// Angles a run from the front (z toward us) round to the right. +const RW = 30, RIN = 16, WALK = 23, K = 0.4, YB = 115; +// Surfaces: wall, tread top, riser, tread end, inner edge, floor, the cut wall. +const WALL = 1, TOP = 2, RISE = 3, END = 4, EDGE = 5, FLOOR = 6, CUT = 7; + +export function setup(params) { + params.s = { + t: 0, turns: 0, hue: -1, speed: 2, reach: 29, phase: 0, u: 0.05, tp: 0, flare: 0.5, flick: 1, + kind: new Uint8Array(N), pos: new Float32Array(N * 3), nrm: new Float32Array(N * 3), alb: new Float32Array(N), amb: new Float32Array(N), + eh: new Float32Array(N), win: new Float32Array(N), buf: new Float32Array(N * 3), fz: new Float32Array(N), fl: new Float32Array(N), wins: [], + }; +} + +// The tower for this many turns. The stair is a solid helix of steps: step +// j rises rise * j and a slab th thick sits under each tread. Every LED's +// first surface along its view line is found once, stepping back from the +// front, with its place, its facing, its stone and its share of moonlight. +function build(S) { + const T = S.turns, pitch = 118 / T, st = cl(round(pitch / 2.6), 7, 16), rise = pitch / st, ds = TAU / st, th = rise + 1.5; + Object.assign(S, { pitch, rise, th, ds }); + const { kind, pos, nrm, alb, amb, eh, win } = S; + // Two slit windows a turn at the back, half way up the corridor. + const wins = []; + for (let n = 0; n <= T; n++) + for (const f of [0.38, 0.62]) { + const a = f * TAU, y = pitch * (n + f) + th + 0.45 * (pitch - th), hh = cl(0.15 * pitch, 3, 6.5), sy = YB - y + K * RW * cos(a); + if (sy > hh + 3 && sy < H - 20) wins.push({ a, y, hh, sx: 32 + RW * sin(a), sy, n: 0 }); + } + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const wx = x + 0.5 - 32, c = YB - (y + 0.5); + let k = 0, Y = c, z = 0, nx = 0, ny = 0, nz = 1, e = 0, j = 0, aa = 0; + win[p] = 0; + if (abs(wx) >= RW) k = CUT; + else { + const zw = sqrt(RW * RW - wx * wx); + let pj = -1, well = false; + for (z = zw; z > -zw; z -= 0.25) { + Y = c + K * z; + if (Y < 0) { + (z = -c / K), (Y = 0), (k = FLOOR), (nz = 0), (ny = 1); + break; + } + const r2 = wx * wx + z * z; + if (r2 < RIN * RIN) { + (well = true), (pj = -1); + continue; + } + aa = ang(wx, z); + j = floor(aa / ds); + const m = floor((Y - rise * j) / pitch), base = rise * j + pitch * m; + if (m >= 0 && Y - base < th) { + const r = sqrt(r2); + if (z === zw) (k = END), (nx = wx / r), (ny = 0), (nz = z / r); + else if (well) (k = EDGE), (nx = -wx / r), (ny = 0), (nz = -z / r); + else if (pj >= 0 && pj !== j) (k = RISE), (nx = -cos(aa)), (ny = 0), (nz = sin(aa)); + else (k = TOP), (ny = 1), (nz = 0); + e = Y - base - th; + j += st * m; + break; + } + (pj = j), (well = false); + } + if (!k) { + (z = -zw), (Y = c + K * z), (k = WALL), (nx = -wx / RW), (ny = 0), (nz = zw / RW); + aa = ang(wx, z); + e = (((Y - rise * floor(aa / ds) - th) % pitch) + pitch) % pitch; + for (let i = 0; i < wins.length; i++) { + const w = wins[i], da = RW * wrap(aa - w.a), dy = Y - w.y; + if (abs(dy) < w.hh + 2 && abs(da) < 3.6 - (0.4 * abs(dy)) / w.hh) win[p] = -1; + if (abs(dy) < w.hh - (abs(da) > 1 ? 0.8 : 0) && abs(da) < 1.6) { + // Where a sliver of moon would be: a crescent opening left. + const my = abs(dy - 0.3 * w.hh), moon = (da > 0.5 && my < 1.6) || (da > -0.5 && da <= 0.5 && abs(my - 2) < 0.6); + win[p] = 0.5 + 0.5 * cl((dy + w.hh) / (2 * w.hh), 0, 1) + (moon ? 10 : 0); + // A slit LED is never lit, so it keeps its window's number here. + w.n++; + e = i; + } + } + } + } + kind[p] = k; + pos.set([wx, Y, z], 3 * p); + nrm.set([nx, ny, nz], 3 * p); + eh[p] = e; + // Stone: wall courses 4 LEDs high with blocks 7 long, mortar darker; + // each step its own stone; flags on the floor. + let al = 1; + if (k === WALL || k === CUT) { + const s = k === CUT ? (wx > 0 ? 16 : 2) : RW * aa, row = floor(Y / 4), off = s + (row & 1) * 3.5, blk = floor(off / 7); + al = Y - 4 * row < 0.9 || off - 7 * blk < 0.9 ? 0.62 : 0.85 + 0.3 * hash(row * 131 + blk); + } else if (k === FLOOR) al = abs(wx % 8) < 0.7 || abs(z % 6) < 0.6 ? 0.6 : 0.9 + 0.2 * hash(floor(wx / 8) * 7 + floor(z / 6)); + else al = 0.85 + 0.3 * hash(j * 17 + 3) - (k === TOP && aa - ds * floor(aa / ds) > ds - 0.06 ? 0.25 : 0); + alb[p] = al; + // Moonlight: tread tops brightest and the steps in front more than + // those behind, risers and the wall darker, a window's splay in shadow. + const front = 0.75 + 0.25 * (z / RW); + amb[p] = win[p] < 0 ? 0.5 : k === TOP ? 1.9 * front : k === END ? 1.3 : k === EDGE ? 1.15 : k === RISE ? 0.9 * front : k === FLOOR ? 0.9 : k === CUT ? 1.2 : 0.8; + } + // The windows the stair leaves in sight, highest first: the eyes come to + // the top one, the moon shows in the next. + const seen = wins.filter((w) => w.n > 5).sort((u, v) => v.y - u.y), moon = wins.indexOf(seen[1]); + for (let p = 0; p < N; p++) if (win[p] >= 10) win[p] = eh[p] === moon ? 3 : win[p] - 10; + S.wins = seen; +} + +// Is the way from the candle to this point clear of the stair? The stair +// is solid, so on the candle's side of the well a point is lit only inside +// the candle's own turn of the corridor; across the well the light must +// also clear the treads at both ends of its path. e is the point's height +// above the tread under it. +function clear(S, C, px, pY, pz, e) { + const dx = px - C[0], dz = pz - C[2], L2 = dx * dx + dz * dz || 1e-6, D = sqrt(L2), room = S.pitch - S.th; + const b = -(C[0] * dx + C[2] * dz) / L2, t = cl(b, 0, 1), mx = C[0] + t * dx, mz = C[2] + t * dz; + if (mx * mx + mz * mz >= RIN * RIN) { + const f = pY - C[4] - (S.pitch * wrap(ang(px, pz) - C[3])) / TAU; + return f > -0.7 && f < room + 0.3; + } + const s = (pY - C[1]) / D, cc = C[0] * C[0] + C[2] * C[2] - RIN * RIN, disc = sqrt(max(0, b * b - cc / L2)); + const h1 = C[1] - C[4] + s * (b - disc) * D, h2 = e - s * (1 - b - disc) * D; + return h1 > 0 && h1 < room && h2 > -0.7 && h2 < room + 0.3; +} + +// Shortest distance between segments PQ and AB, squared; segT keeps where +// along AB it falls, for a tapered capsule's radius there. +let segT = 0; +function segDist2(px, py, pz, qx, qy, qz, ax, ay, az, bx, by, bz) { + const d1x = qx - px, d1y = qy - py, d1z = qz - pz, d2x = bx - ax, d2y = by - ay, d2z = bz - az, rx = px - ax, ry = py - ay, rz = pz - az; + const a = d1x * d1x + d1y * d1y + d1z * d1z, e = d2x * d2x + d2y * d2y + d2z * d2z || 1e-6, f = d2x * rx + d2y * ry + d2z * rz; + const c = d1x * rx + d1y * ry + d1z * rz, bd = d1x * d2x + d1y * d2y + d1z * d2z, den = a * e - bd * bd; + let s = den > 1e-9 ? cl((bd * f - c * e) / den, 0, 1) : 0, t = (bd * s + f) / e; + if (t < 0) (t = 0), (s = cl(-c / a, 0, 1)); + else if (t > 1) (t = 1), (s = cl((bd - c) / a, 0, 1)); + const ux = px + d1x * s - ax - d2x * t, uy = py + d1y * s - ay - d2y * t, uz = pz + d1z * s - az - d2z * t; + segT = t; + return ux * ux + uy * uy + uz * uz; +} + +// The climber at progress u along the stair: tapered capsules [ax, ay, az, +// bx, by, bz, ra, rb] in the tower's frame (x right, Y up, z toward us), and +// the flame C = [x, Y, z, angle, height of the tread under it]. +function climber(S) { + const pitch = S.pitch, hgt = 0.6 * (pitch - S.th); + const a0 = 0.25, a1 = (TAU * (108 - 1.05 * hgt - S.th)) / pitch, a = a0 + (a1 - a0) * S.u; + const walk = (a * WALK) / (0.2 * hgt), fy = (pitch * a) / TAU - 0.5 * S.rise + S.th + 0.04 * hgt * abs(sin(walk)); + const sa = sin(a), ca = cos(a); + // Local axes: forward along the stair, out toward the wall, up. + const at = (f, o, u) => [(WALK + o * hgt) * sa + f * hgt * ca, fy + u * hgt, (WALK + o * hgt) * ca - f * hgt * sa]; + const caps = []; + const cap = (p, q, ra, rb) => caps.push([...p, ...q, ra * hgt, rb * hgt]); + const sw = 0.03 * sin(walk); + // A cloak that widens to the hem, square shoulders, a hood with a point. + cap(at(-0.03, 0, 0.1), at(0.01, 0, 0.58), 0.21, 0.11); + cap(at(0.02, 0.07, 0.68), at(0.02, -0.07, 0.68), 0.09, 0.09); + cap(at(0.06, 0, 0.84), at(0.07, 0, 0.85), 0.1, 0.1); + cap(at(0.03, 0, 0.88), at(-0.09, 0, 1.06), 0.08, 0.012); + // The arm reaches forward and in, holding the candle over the well side. + const hand = at(0.2 + sw, -0.22, 0.64); + cap(at(0.03, -0.08, 0.7), hand, 0.045, 0.035); + const C = [hand[0], hand[1] + 0.1 * hgt + 1.5, hand[2], 0, 0]; + C[3] = ang(C[0], C[2]); + C[4] = fy + (pitch * wrap(C[3] - a)) / TAU; + let mx = 0, my = 0, mz = 0; + for (const c of caps) (mx += c[0] + c[3]), (my += c[1] + c[4]), (mz += c[2] + c[5]); + S.P = { a, hgt, caps, hand, C, mid: [mx / 10, my / 10, mz / 10], rad: 0.78 * hgt }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.speed = cl(k[1], 0.2, 3); + S.reach = 28 * 2 ** cl(k[3], -1, 1); + const T = round(cl(k[2], 3, 8)); + if (T !== S.turns) { + S.turns = T; + build(S); + } + const ds = (d * S.speed) / 2, D = 10 * sqrt(T / 3); + S.t += ds; + S.tp += ds; + S.flare = max(0, S.flare - ds); + if (input.btnPressed && input.btnPressed[0] && S.phase === 0) (S.phase = 1), (S.tp = 0.25); + // 0 climbing, 1 a draft bends the flame, 2 dark with eyes at the window. + if (S.phase === 0) { + S.u = min(1, S.u + ds / D); + if (S.u >= 1) (S.phase = 1), (S.tp = 0); + } else if (S.phase === 1 && S.tp > 0.6) (S.phase = 2), (S.tp = 0); + else if (S.phase === 2 && S.tp > 2.2) (S.phase = 0), (S.tp = 0), (S.u = 0), (S.flare = 0.5); + const fl = 0.9 + 0.06 * sin(S.t * 23) + 0.04 * sin(S.t * 37 + 1); + S.flick = S.phase === 0 ? fl : S.phase === 1 ? fl * (1 - sm(S.tp / 0.6)) * (0.6 + 0.4 * sin(S.t * 61)) : 0; + climber(S); + const hue = cl(k[0], 0, 1); + if (hue !== S.hue) { + S.hue = hue; + S.tint = hsv(0.76 - 0.36 * hue, 0.68, 1); + S.sky = hsv(0.76 - 0.36 * hue, 0.75, 1); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.P) return; + const { kind, pos, nrm, alb, amb, eh, win, buf, fz, fl, P, tint, sky } = S; + const C = P.C, lit = S.flick, R = S.reach, cut = 3 * R, caps = P.caps, LC = [1, 0.55, 0.2]; + for (let p = 0; p < N; p++) { + const q = 3 * p, w = win[p]; + if (w > 0) { + // The night through a slit, deeper at the top, and a sliver of moon. + const v = w > 2 ? 1 : 0.2 + 0.5 * w; + buf[q] = w > 2 ? 0.95 : sky[0] * v; + buf[q + 1] = w > 2 ? 0.9 : sky[1] * v; + buf[q + 2] = w > 2 ? 0.7 : sky[2] * v; + continue; + } + const k = kind[p], a = alb[p] * amb[p] * 0.2; + let r = tint[0] * a, g = tint[1] * a, b = tint[2] * a; + if (lit > 0.01) { + const px = pos[q], py = pos[q + 1], pz = pos[q + 2], lx = C[0] - px, ly = C[1] - py, lz = C[2] - pz, dd = sqrt(lx * lx + ly * ly + lz * lz) || 1; + if (dd < cut) { + const ln = (nrm[q] * lx + nrm[q + 1] * ly + nrm[q + 2] * lz) / dd; + let li = (sm((cut - dd) / R) * (0.2 + 0.8 * max(0, ln))) / (1 + (dd / R) ** 2); + if (ln < -0.3) li = 0; + if (li > 0.004 && k !== CUT && !clear(S, C, px, py, pz, eh[p])) li = 0; + if (li > 0.004) { + // The climber's shadow: does the way to the flame pass through him? + const mx = P.mid[0] - px, my = P.mid[1] - py, mz = P.mid[2] - pz, tt = cl((mx * lx + my * ly + mz * lz) / (dd * dd), 0, 1); + const ex = mx - lx * tt, ey = my - ly * tt, ez = mz - lz * tt; + if (ex * ex + ey * ey + ez * ez < P.rad * P.rad) + for (const c of caps) { + const d2 = segDist2(px, py, pz, C[0], C[1], C[2], c[0], c[1], c[2], c[3], c[4], c[5]), rr = c[6] + (c[7] - c[6]) * segT; + if (d2 < rr * rr) li = 0; + } + } + li *= lit * 1.5 * alb[p]; + r += LC[0] * li; + g += LC[1] * li; + b += LC[2] * li; + } + } + if (w < 0) (r *= 0.55), (g *= 0.55), (b *= 0.55); + buf[q] = r; + buf[q + 1] = g; + buf[q + 2] = b; + } + // The climber: a dark cloak, the side toward the flame lit warm. + fz.fill(-99); + for (const c of caps) { + const sx0 = 32 + c[0], sy0 = YB - c[1] + K * c[2], sx1 = 32 + c[3], sy1 = YB - c[4] + K * c[5], ra = c[6], rb = c[7], rr = max(ra, rb); + const ex = sx1 - sx0, ey = sy1 - sy0, e2 = ex * ex + ey * ey || 1e-6; + for (let y = max(0, floor(min(sy0, sy1) - rr)); y <= min(H - 1, ceil(max(sy0, sy1) + rr)); y++) + for (let x = max(0, floor(min(sx0, sx1) - rr)); x <= min(W - 1, ceil(max(sx0, sx1) + rr)); x++) { + const qx = x + 0.5 - sx0, qy = y + 0.5 - sy0, u = cl((qx * ex + qy * ey) / e2, 0, 1), hx = qx - u * ex, hy = qy - u * ey, h2 = hx * hx + hy * hy, ru = ra + (rb - ra) * u; + if (h2 > ru * ru) continue; + const p = y * W + x, hz = sqrt(ru * ru - h2), zz = c[2] + (c[5] - c[2]) * u + hz; + if (zz < pos[3 * p + 2] - 1 || zz < fz[p]) continue; + fz[p] = zz; + const wxp = c[0] + (c[3] - c[0]) * u + hx, wyp = c[1] + (c[4] - c[1]) * u - hy; + const lx = C[0] - wxp, ly = C[1] - wyp, lz = C[2] - zz, dd = sqrt(lx * lx + ly * ly + lz * lz) || 1; + fl[p] = max(0, (hx * lx - hy * ly + hz * lz) / (ru * dd)) / (1 + (dd / R) ** 2); + } + } + for (let p = 0; p < N; p++) + if (fz[p] > -99) { + const q = 3 * p, v = fl[p] * lit; + buf[q] = 0.035 + 1.1 * v; + buf[q + 1] = 0.02 + 0.55 * v; + buf[q + 2] = 0.05 + 0.2 * v; + } + // The hand, the candle and its flame, hidden where the climber is in + // front of them, and a halo round the flame either way. + const sx = (v) => 32 + v[0], sy = (v) => YB - v[1] + K * v[2], hx = sx(P.hand), hy = sy(P.hand), fx = sx(C), fy = sy(C), front = C[2] + 1; + const put = (x, y, r, g, b) => { + (x = floor(x)), (y = floor(y)); + if (x < 0 || x >= W || y < 0 || y >= H) return; + const p = y * W + x, q = 3 * p; + if (fz[p] > front || pos[3 * p + 2] > front + 0.5) return; + buf[q] = r; + buf[q + 1] = g; + buf[q + 2] = b; + }; + const glow = (cx, cy, rad, amp, col) => { + for (let y = max(0, floor(cy - rad)); y <= min(H - 1, ceil(cy + rad)); y++) + for (let x = max(0, floor(cx - rad)); x <= min(W - 1, ceil(cx + rad)); x++) { + const dd = Math.hypot(x + 0.5 - cx, y + 0.5 - cy); + if (dd > rad) continue; + const v = amp * (1 - dd / rad) ** 2, q = 3 * (y * W + x); + buf[q] += col[0] * v; + buf[q + 1] += col[1] * v; + buf[q + 2] += col[2] * v; + } + }; + if (S.phase < 2) { + const sk = 0.25 + 0.6 * lit; + put(hx, hy, sk, 0.6 * sk, 0.42 * sk); + put(hx + (fx > hx ? -1 : 1), hy, 0.8 * sk, 0.48 * sk, 0.33 * sk); + for (let y = floor(hy) - 1; y > floor(fy); y--) put(fx, y, 0.75, 0.7, 0.6); + } + if (lit > 0.01) { + glow(fx, fy - 0.5, 8, 0.5 * lit, LC); + const sway = S.phase === 1 ? 1.4 * sin(S.t * 30) : 0.4 * sin(S.t * 11); + put(fx, fy, 1, 0.95 * lit, 0.6 * lit); + put(fx + sway * 0.5, fy - 1, 1, 0.75 * lit, 0.25 * lit); + put(fx + sway, fy - 2, 0.9 * lit, 0.4 * lit, 0.05); + } else if (S.phase === 2 && S.tp < 1) + // A thread of smoke from the wick. + for (let i = 0; i < 6; i++) put(fx + 1.2 * sin(S.tp * 6 + i), fy - 1 - i - 4 * S.tp, 0.3 * (1 - S.tp), 0.28 * (1 - S.tp), 0.3 * (1 - S.tp)); + // The match struck at the foot of the stair before each climb. + if (S.flare > 0) glow(fx, fy - 1, 3 + 8 * (0.5 - S.flare), 2 * S.flare, [1, 0.85, 0.55]); + // Two green eyes at the top window while it is dark. + if (S.phase === 2 && S.tp > 0.3 && S.tp < 2 && S.wins.length) { + const w = S.wins[0]; + const e = sm((S.tp - 0.3) / 0.3) * (1 - sm((S.tp - 1.7) / 0.3)) * (abs(S.tp - 1.15) < 0.07 ? 0.1 : 1); + glow(w.sx, w.sy - 1, 6, 0.2 * e, [0.3, 1, 0.3]); + // A pale face fills the slit behind them. + for (let y = floor(w.sy - 3); y <= w.sy + 2; y++) + for (let x = floor(w.sx - 1.5); x <= w.sx + 1; x++) { + const q = 3 * (y * W + x); + if (y >= 0 && x >= 0 && x < W) (buf[q] += 0.14 * e), (buf[q + 1] += 0.17 * e), (buf[q + 2] += 0.14 * e); + } + for (const dx of [-1, 1]) { + const x = floor(w.sx + dx), y = floor(w.sy - 1), q = 3 * (y * W + x); + if (x >= 0 && x < W && y >= 0) (buf[q] = 0.3 * e), (buf[q + 1] = e), (buf[q + 2] = 0.2 * e); + } + } + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const q = 3 * p; + display.setPixel(x, y, round(cl(buf[q], 0, 1) * 255), round(cl(buf[q + 1], 0, 1) * 255), round(cl(buf[q + 2], 0, 1) * 255)); + } +} From 1742497f75af2aa4539ec3c30cf825abca469702 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 23:35:36 +0000 Subject: [PATCH 5/8] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=205?= =?UTF-8?q?1=ED=8C=90=20=E2=80=94=20Calabi-Yau=20Quintic,=20Zombie=20Shamb?= =?UTF-8?q?le?= 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_016styVvYJeAE1svDoUBeGKX --- incoming/pattern-study/patterns/calabi-yau.js | 222 ++++++++ .../pattern-study/patterns/zombie-shamble.js | 489 ++++++++++++++++++ 2 files changed, 711 insertions(+) create mode 100644 incoming/pattern-study/patterns/calabi-yau.js create mode 100644 incoming/pattern-study/patterns/zombie-shamble.js diff --git a/incoming/pattern-study/patterns/calabi-yau.js b/incoming/pattern-study/patterns/calabi-yau.js new file mode 100644 index 000000000..9af9537e3 --- /dev/null +++ b/incoming/pattern-study/patterns/calabi-yau.js @@ -0,0 +1,222 @@ +// ===== Patternflow pattern ===== +// Title: Calabi-Yau Quintic +// Family: space +// Idea: The complex curve z1^5 + z2^5 = 1, a slice of the quintic Calabi-Yau, built from Andrew Hanson's 25 patches and projected into space, turns slowly on black with each patch in its own colour, so the seams show where the patches join and where five of them fan in to one point. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Calabi-Yau manifolds (Eugenio Calabi, 1954; Shing-Tung Yau, 1977), the Fermat quintic slice drawn after Andrew J. Hanson (1994), pattern round 51 +// Round: 51 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Angle=-45..45, Spread=0..1 +// +// Knob 1: Hue. Turns every patch round the wheel together. A patch takes its hue from k1, the fifth root of unity in front of z1, and a small shift from k2: at 0 the five k1 families are cyan, violet, rose, orange and green. +// Knob 2: Speed. At 2 the surface turns once about its upright axis in 24 seconds and nods 18 degrees up and down every 16; at 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Angle. The projection angle alpha, in degrees from Hanson's 45. The upright axis shows cos(alpha) Im z1 + sin(alpha) Im z2, so at -45 it is Im z1 alone and at 45 Im z2 alone, and the whole surface reshapes as the knob turns. +// Knob 4: Spread. An exploded view: each patch moves out from the centre along the line to its own middle, stretched 1.8 times up and down and 0.8 times across so the pieces use the panel's height. Whole at 0.06, 25 separate pieces at 1. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, sqrt, sin, cos, atan2, hypot, abs, pow, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// Grid of one patch: NU steps across x in [-A, A], NV across y in [0, pi/2]. +const NU = 24, NV = 12, VU = NU + 1, VV = NV + 1, PV = VU * VV, NP = 25 * PV, A = 1.1; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// Hanson's patch (k1, k2) at z = x + iy: +// z1 = e^(2 pi i k1/5) cosh(z)^(2/5), z2 = e^(2 pi i k2/5) (-i sinh z)^(2/5), +// principal branches, so z1^5 + z2^5 = cosh^2 z - sinh^2 z = 1. +function point(k1, k2, x, y, out, o) { + const cr = Math.cosh(x) * cos(y), ci = Math.sinh(x) * sin(y); + const sr = Math.sinh(x) * cos(y), si = Math.cosh(x) * sin(y); + // -i sinh z + const mr = si, mi = -sr; + const r1 = pow(hypot(cr, ci), 0.4), a1 = 0.4 * atan2(ci, cr) + (TAU * k1) / 5; + const r2 = pow(hypot(mr, mi), 0.4), a2 = 0.4 * atan2(mi, mr) + (TAU * k2) / 5; + out[o] = r1 * cos(a1); + out[o + 1] = r1 * sin(a1); + out[o + 2] = r2 * cos(a2); + out[o + 3] = r2 * sin(a2); +} + +export function setup(params) { + const F = (n) => new Float32Array(n); + const s = { + t: 0, yaw: 0.6, hue: 0, alpha: PI / 4, spread: 0.06, + // Re z1, Im z1, Re z2, Im z2 for every grid point of every patch. + z: F(NP * 4), cen: F(25 * 4), + px: F(NP), py: F(NP), pz: F(NP), sx: F(NP), sy: F(NP), nx: F(NP), ny: F(NP), nz: F(NP), + zb: F(N), id: new Int16Array(N), wa: F(N), wb: F(N), tv: new Int32Array(N), col: F(N * 3), + pal: F(25 * 3), palHue: -1, + }; + // The grid crowds toward x = 0 and toward both ends of y, where the + // patches meet at the points z2 = 0 and z1 = 0 and the 2/5 power stretches them. + const gx = F(VU), gy = F(VV); + for (let i = 0; i < VU; i++) { + const u = (2 * i) / NU - 1; + gx[i] = A * Math.sign(u) * pow(abs(u), 1.7); + } + for (let j = 0; j < VV; j++) gy[j] = (PI / 4) * (1 - cos((PI * j) / NV)); + for (let p = 0; p < 25; p++) { + const k1 = (p / 5) | 0, k2 = p % 5; + for (let j = 0; j < VV; j++) + for (let i = 0; i < VU; i++) { + const v = p * PV + j * VU + i; + point(k1, k2, gx[i], gy[j], s.z, v * 4); + for (let c = 0; c < 4; c++) s.cen[p * 4 + c] += s.z[v * 4 + c] / PV; + } + } + params.s = s; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const s = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + s.hue = cl(k[0], 0, 1); + const sp = cl(k[1], 0.2, 3); + s.alpha = ((45 + cl(k[2], -45, 45)) * PI) / 180; + s.spread = cl(k[3], 0, 1); + s.t += d * sp; + s.yaw += d * sp * 0.13; +} + +function palette(s) { + for (let p = 0; p < 25; p++) { + const k1 = (p / 5) | 0, k2 = p % 5; + hsv(s.hue + 0.5 + 0.2 * k1 + 0.035 * (k2 - 2), 0.62 + 0.07 * k2, 1, s.pal, p * 3); + } + s.palHue = s.hue; +} + +export function draw(display, params) { + const s = params.s; + if (!s) return; + if (s.palHue !== s.hue) palette(s); + const { z, cen, px, py, pz, sx, sy, nx, ny, nz, zb, id, wa, wb, tv, col, pal } = s; + const ca = cos(s.alpha), sa = sin(s.alpha); + // The turn about the upright (projected) axis and a slow nod. + const cy = cos(s.yaw), sy0 = sin(s.yaw), el = 0.32 * sin(s.t * 0.19), ce = cos(el), se = sin(el); + const sc = 19.5 / (1 + 0.25 * s.spread), CX = 31.5, CY = 64, CAM = 8; + const sp = s.spread; + for (let p = 0; p < 25; p++) { + // The patch's own middle, turned like the rest; the exploded view moves + // the patch out along it, 1.8 times as far up and down and 0.8 across. + const X = cen[p * 4] * sp, Y = cen[p * 4 + 2] * sp, Z = (ca * cen[p * 4 + 1] + sa * cen[p * 4 + 3]) * sp; + const xr = X * cy - Y * sy0, yr = X * sy0 + Y * cy; + const ox = xr * 0.8, oz = (yr * se + Z * ce) * 1.8, oy = yr * ce - Z * se; + for (let v = p * PV, e = v + PV; v < e; v++) { + const X = z[v * 4], Y = z[v * 4 + 2], Z = ca * z[v * 4 + 1] + sa * z[v * 4 + 3]; + // Yaw about the upright axis, then the nod about the screen's x axis. + const xr = X * cy - Y * sy0, yr = X * sy0 + Y * cy; + const yy = yr * ce - Z * se + oy, zz = yr * se + Z * ce + oz, xx = xr + ox; + px[v] = xx; + py[v] = zz; + pz[v] = yy; + const f = (sc * CAM) / (CAM + yy); + sx[v] = CX + f * xx; + sy[v] = CY - f * zz; + } + } + // Normals from the grid's neighbours. + for (let p = 0; p < 25; p++) + for (let j = 0; j < VV; j++) + for (let i = 0; i < VU; i++) { + const v = p * PV + j * VU + i; + const a = i > 0 ? v - 1 : v, b = i < NU ? v + 1 : v, c = j > 0 ? v - VU : v, e = j < NV ? v + VU : v; + const ux = px[b] - px[a], uy = py[b] - py[a], uz = pz[b] - pz[a]; + const wx = px[e] - px[c], wy = py[e] - py[c], wz = pz[e] - pz[c]; + const qx = uy * wz - uz * wy, qy = uz * wx - ux * wz, qz = ux * wy - uy * wx; + const l = 1 / (sqrt(qx * qx + qy * qy + qz * qz) + 1e-12); + nx[v] = qx * l; + ny[v] = qy * l; + nz[v] = qz * l; + } + zb.fill(1e9); + id.fill(-1); + for (let p = 0; p < 25; p++) + for (let j = 0; j < NV; j++) + for (let i = 0; i < NU; i++) { + const v = p * PV + j * VU + i; + tri(s, v, v + 1, v + VU + 1, p); + tri(s, v, v + VU + 1, v + VU, p); + } + // Shade into col. + const LX = -0.5, LY = 0.6, LZ = -0.62, HZ = LZ - 1, HL = 1 / sqrt(LX * LX + LY * LY + HZ * HZ); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const q = y * W + x, p = id[q], o = q * 3; + if (p < 0) { + col[o] = col[o + 1] = col[o + 2] = 0; + continue; + } + const t0 = tv[q], a = wa[q], b = wb[q], c = 1 - a - b; + const v0 = t0 & 0x3fff, v1 = v0 + ((t0 >> 14) & 0xff), v2 = v0 + ((t0 >> 22) & 0xff); + let mx = nx[v0] * c + nx[v1] * a + nx[v2] * b, my = ny[v0] * c + ny[v1] * a + ny[v2] * b, mz = nz[v0] * c + nz[v1] * a + nz[v2] * b; + const l = 1 / (sqrt(mx * mx + my * my + mz * mz) + 1e-12); + mx *= l; + my *= l; + mz *= l; + // Two-sided: the eye looks along +depth, so a normal is turned to face it. + if (mz > 0) (mx = -mx), (my = -my), (mz = -mz); + const ld = max(0, mx * LX + my * LY + mz * LZ), hh = max(0, (mx * LX + my * LY + mz * HZ) * HL); + const spc = pow(hh, 30) * 0.5; + // The far side dims a quarter; light falls off toward an indigo shade + // rather than toward black, so a turned-away yellow reads violet, not olive. + const fog = 1 - 0.25 * cl((zb[q] + 1.6) / 3.2, 0, 1), lit = (0.1 + 0.9 * ld) * fog, sh = (1 - ld) * 0.22 * fog; + const r = pal[p * 3] * lit + 0.35 * sh + spc, g = pal[p * 3 + 1] * lit + 0.12 * sh + spc, bb = pal[p * 3 + 2] * lit + sh + spc; + // Seams: an LED beside another patch, joined to it or passing in front, is dimmed. + const rp = x < W - 1 ? id[q + 1] : p, dp = y < H - 1 ? id[q + W] : p; + const k = (rp >= 0 && rp !== p) || (dp >= 0 && dp !== p) ? 127 : 255; + col[o] = min(255, r * k); + col[o + 1] = min(255, g * k); + col[o + 2] = min(255, bb * k); + } + // An empty LED on the outline takes a third of its lit neighbours' + // light, which softens the stairs of the edge against black. + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const q = y * W + x, o = q * 3; + if (id[q] >= 0) { + display.setPixel(x, y, col[o], col[o + 1], col[o + 2]); + continue; + } + let r = 0, g = 0, b = 0; + for (const n of [x > 0 ? q - 1 : -1, x < W - 1 ? q + 1 : -1, y > 0 ? q - W : -1, y < H - 1 ? q + W : -1]) + if (n >= 0 && id[n] >= 0) (r += col[n * 3]), (g += col[n * 3 + 1]), (b += col[n * 3 + 2]); + display.setPixel(x, y, r * 0.09, g * 0.09, b * 0.09); + } +} + +// One triangle into the depth buffer, keeping the barycentric weights of +// the nearest surface at each LED centre. +function tri(s, i0, i1, i2, p) { + const { sx, sy, pz, zb, id, wa, wb, tv } = s; + const x0 = sx[i0], y0 = sy[i0], x1 = sx[i1], y1 = sy[i1], x2 = sx[i2], y2 = sy[i2]; + const den = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0); + if (abs(den) < 1e-9) return; + const inv = 1 / den; + let xa = min(x0, x1, x2), xb = max(x0, x1, x2), ya = min(y0, y1, y2), yb = max(y0, y1, y2); + const X0 = max(0, Math.ceil(xa - 0.5)), X1 = min(W - 1, floor(xb - 0.5)), Y0 = max(0, Math.ceil(ya - 0.5)), Y1 = min(H - 1, floor(yb - 0.5)); + if (X0 > X1 || Y0 > Y1) return; + const z0 = pz[i0], z1 = pz[i1], z2 = pz[i2]; + const pack = i0 | ((i1 - i0) << 14) | ((i2 - i0) << 22); + for (let y = Y0; y <= Y1; y++) { + const cy = y + 0.5 - y0; + for (let x = X0; x <= X1; x++) { + const cx = x + 0.5 - x0; + const a = (cx * (y2 - y0) - cy * (x2 - x0)) * inv, b = ((x1 - x0) * cy - (y1 - y0) * cx) * inv; + if (a < -1e-6 || b < -1e-6 || a + b > 1 + 1e-6) continue; + const d = z0 + (z1 - z0) * a + (z2 - z0) * b, q = y * W + x; + if (d < zb[q]) (zb[q] = d), (id[q] = p), (wa[q] = a), (wb[q] = b), (tv[q] = pack); + } + } +} diff --git a/incoming/pattern-study/patterns/zombie-shamble.js b/incoming/pattern-study/patterns/zombie-shamble.js new file mode 100644 index 000000000..8813b18e5 --- /dev/null +++ b/incoming/pattern-study/patterns/zombie-shamble.js @@ -0,0 +1,489 @@ +// ===== Patternflow pattern ===== +// Title: Zombie Shamble +// Family: halloween +// Idea: A horde of zombies shambles toward you down a foggy country lane at night, black figures rim-lit by a sickly green glow behind the horizon with their arms reaching and heads lolling, small in the far fog and growing until the nearest fill the panel and come apart in the mist. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 51 +// Round: 51 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Horde=5..18, Fog=0..1 +// +// Knob 1: Hue. The glow, the fog and the green of the rims and eyes together: toxic green at 0, sea green at 0.25, sky blue at 0.5, blue violet at 0.75 and magenta at 1, while the sky leans from purple to indigo and back. +// Knob 2: Speed. At 2 each zombie closes a tenth of its distance a second, so it grows on the panel at one steady rate, at a little under two steps a second; the nearest, 1.95 m off at the start, begins to come apart in the fog two seconds later and is gone by about six. At 0.2 ten times slower with short shuffling steps, at 3 half as fast again with longer strides and harder lurches. The fog drifts on the same clock. +// Knob 3: Horde. How many walk at once, 5 to 18. They open spread from 2 m to 26 m, nearest first, a bigger horde as a wider wall of near ones, and each count puts its nearest zombie in a different place. Three in five drag a leg, and two in five have eyes that glow. +// Knob 4: Fog. How deep and thick the ground fog lies: at 0 a mist 0.3 m deep that hides the far lane and the feet of the far zombies, at 1 a bank 1.35 m deep and nearly six times as thick that swallows them to the chest. +// Buttons: K1 tap snaps every head upright to look at you, face turned to the eye and eyes lit green, for about two and a half seconds. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, sqrt, sin, cos, exp, round, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => { + const t = cl((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; +const hash = (i) => { + let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = [v, q, p, p, t, v][i]; + out[1] = [t, v, v, q, p, p][i]; + out[2] = [p, p, t, v, v, q][i]; +} + +// Value noise on a 256 x 256 lattice. +const NT = new Float32Array(65536); +for (let i = 0; i < 65536; i++) NT[i] = hash(i * 7 + 3); +function vn(x, y) { + const xi = floor(x), yi = floor(y); + let fx = x - xi, fy = y - yi; + fx = fx * fx * (3 - 2 * fx); + fy = fy * fy * (3 - 2 * fy); + const x0 = xi & 255, x1 = (xi + 1) & 255, y0 = (yi & 255) << 8, y1 = ((yi + 1) & 255) << 8; + const a = NT[y0 + x0], b = NT[y0 + x1], c = NT[y1 + x0], d = NT[y1 + x1]; + return a + (b - a) * fx + (c - a) * fy + (a - b - c + d) * fx * fy; +} + +// The eye: horizon row, vanishing column, LEDs per metre at one metre and +// eye height in metres. A point x metres to the side, h up and z ahead +// lands at column VX + F x / z and row YH + F (HC - h) / z. +const YH = 32, VX = 32, F = 100, HC = 1.45; +// The glow behind the horizon stands a little right of the lane. +const GX = 36; +// Half the lane's width; how far off zombies appear, where they are gone +// and where they start to come apart in the fog; the most there can be. +const ROAD = 1.1, ZMAX = 26, ZOUT = 1.05, ZFADE = 1.6, MAXZ = 18; +// Each zombie keeps to its line x of the lane until about ZC metres out, +// then bears in on the eye, so from there on it holds its column and only +// grows. +const ZC = 5, column = (Z) => VX + (F * Z.x) / sqrt(Z.z * Z.z + ZC * ZC); + +// The glow's reach on the panel, the same every frame. +const GL = new Float32Array(N); +for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const dx = x + 0.5 - GX, dy = y + 0.5 - YH; + GL[y * W + x] = exp(-(dx * dx) / 900 - (dy * dy) / 110) * 0.8 + exp(-(dx * dx) / 3000 - (dy * dy) / 700) * 0.35; + } + +// ---- Scenery: dead trees, a fence and a far treeline, drawn once as +// coverage with the depth of what covers each LED. +function capsuleInto(A, Z, x0, y0, x1, y1, r0, r1, z0, z1) { + const ex = x1 - x0, ey = y1 - y0, ll = ex * ex + ey * ey || 1e-9, R = max(r0, r1) + 1; + const X0 = max(0, floor(min(x0, x1) - R)), X1 = min(W - 1, floor(max(x0, x1) + R)); + const Y0 = max(0, floor(min(y0, y1) - R)), Y1 = min(H - 1, floor(max(y0, y1) + R)); + for (let y = Y0; y <= Y1; y++) + for (let x = X0; x <= X1; x++) { + let c = 0, uu = 0; + for (let j = 0; j < 3; j++) + for (let i = 0; i < 3; i++) { + const px = x + (i + 0.5) / 3 - x0, py = y + (j + 0.5) / 3 - y0, u = cl((px * ex + py * ey) / ll, 0, 1); + const dx = px - u * ex, dy = py - u * ey; + if (dx * dx + dy * dy <= (r0 + (r1 - r0) * u) ** 2) c++, (uu = u); + } + if (!c) continue; + const q = y * W + x, zz = z0 + (z1 - z0) * uu; + A[q] = max(A[q], c / 9); + if (zz < Z[q]) Z[q] = zz; + } +} +// A crooked limb in the tree's own plane, in metres: from (x, h) at angle a +// from upright, len long, radius r, walked in short kinked steps that give +// off side limbs, mostly toward the lane (side tow). +function limb(A, Z, bx, z, x, h, a, len, r, depth, seed, tow) { + const s = F / z, n = max(2, round(len / 0.4)); + for (let i = 0; i < n; i++) { + const k = seed * 13 + i * 7, l = len / n, r0 = r * (1 - (0.5 * i) / n), r1 = r * (1 - (0.5 * (i + 1)) / n); + a += (hash(k) - 0.5) * 0.9; + const x2 = x + l * sin(a), h2 = h + l * cos(a); + capsuleInto(A, Z, VX + s * (bx + x), YH + s * (HC - h), VX + s * (bx + x2), YH + s * (HC - h2), max(0.3, r0 * s), max(0.28, r1 * s), z, z); + if (depth > 0 && i > 0 && hash(k + 1) < 0.55) { + const side = hash(k + 2) < 0.65 ? tow : -tow; + limb(A, Z, bx, z, x2, h2, a + side * (0.6 + 0.6 * hash(k + 3)), (len - i * l) * (0.45 + 0.35 * hash(k + 4)), r1 * 0.6, depth - 1, k + 5, tow); + } + (x = x2), (h = h2); + } +} +function blur(A) { + const T = new Float32Array(N); + for (let q = 0; q < N; q++) { + const x = q % W; + T[q] = (2 * A[q] + A[x > 0 ? q - 1 : q] + A[x < W - 1 ? q + 1 : q]) / 4; + } + for (let q = 0; q < N; q++) A[q] = (2 * T[q] + T[q >= W ? q - W : q] + T[q < N - W ? q + W : q]) / 4; +} +function scenery() { + const A = new Float32Array(N), Z = new Float32Array(N).fill(1e9); + // Trees: side, distance, lean, height, seed. + const trees = [[-2.3, 8.5, 0.15, 4.2, 1], [2.5, 11, -0.12, 4.4, 2], [-2.6, 16, 0.1, 4.6, 3], [2.9, 23, -0.1, 4.2, 4], [-3.1, 33, 0.05, 4.8, 5], [3.4, 48, -0.1, 4.5, 6]]; + for (const [bx, z, lean, ht, sd] of trees) { + const s = F / z, tow = bx < 0 ? 1 : -1; + // Root flare, then the trunk and its crown. + capsuleInto(A, Z, VX + s * (bx - 0.25), YH + s * HC, VX + s * bx, YH + s * (HC - 0.45), 0.18 * s, 0.17 * s, z, z); + capsuleInto(A, Z, VX + s * (bx + 0.25), YH + s * HC, VX + s * bx, YH + s * (HC - 0.45), 0.14 * s, 0.17 * s, z, z); + limb(A, Z, bx, z, 0, 0.3, lean, ht, 0.13, 3, sd * 31, tow); + } + // The fence on the left: posts every 2.6 m, two rails, one post leaning + // and one rail fallen. + for (let i = 0; i < 14; i++) { + const z = 4.4 + 2.6 * i, s = F / z, x = -2.2, lean = i === 2 ? 0.25 : (hash(i + 40) - 0.5) * 0.1; + capsuleInto(A, Z, VX + s * x, YH + s * HC, VX + s * (x + lean), YH + s * (HC - 1.15), 0.06 * s, 0.05 * s, z, z); + if (i === 13) break; + const z2 = z + 2.6, s2 = F / z2; + for (const [h, k] of [[0.5, 0], [0.95, 1]]) { + const drop = i === 4 && k === 1 ? 0.7 : 0; + capsuleInto(A, Z, VX + s * x, YH + s * (HC - h), VX + s2 * x, YH + s2 * (HC - h + drop), 0.025 * s, 0.025 * s2, z, z2); + } + } + // A far treeline on the horizon. + for (let x = 0; x < W; x++) { + const ht = 1.5 + 2.2 * vn(x / 5, 7) + 1.6 * max(0, vn(x / 2.3, 11) - 0.55); + for (let y = YH - 6; y < YH + 1; y++) { + const c = cl(ht - (YH + 0.5 - y), 0, 1), q = y * W + x; + if (c > A[q]) (A[q] = c), (Z[q] = 60); + } + } + blur(A); + return { A, Z }; +} + +// Where the opening zombies stand across the panel, nearest first, so a +// bigger horde opens as a wider wall of them. +const COLS = [-0.45, 0.5, -0.92, 0.95, 0.08, -0.7, 0.75, -0.2, 0.3, -1, 1, -0.55, 0.6, -0.1, 0.15, -0.85, 0.85, -0.35]; + +export function setup(params) { + const zs = []; + for (let i = 0; i < MAXZ; i++) zs.push({ z: 0, seed: i * 17 + 5 }); + const sc = scenery(), A = (n) => new Float32Array(n); + params.s = { + t: 0, gait: 1, hue: 0, fog: 0.06, count: 5, look: 0, lookT: 0, started: false, zs, sA: sc.A, sZ: sc.Z, + col: A(N * 3), base: A(N * 3), fogC: A(N * 3), fc: A(N * 3), nb: A(33 * 65), bg: A(N), nz: A(N), dist: A(N), R: A(65), caps: A(20 * 6), + sky: [0, 0, 0], glow: [0, 0, 0], fogHue: [0, 0, 0], palHue: -1, + }; +} + +function spawn(Z, z, x) { + Z.seed += 101; + const r = (j) => hash(Z.seed * 3 + j); + Z.z = z || ZMAX - 4 * r(0); + // x: the column to stand in now, or none for a line drawn at random. + Z.x = x === undefined ? (r(1) - 0.5) * 2.6 : ((x - VX) * sqrt(Z.z * Z.z + ZC * ZC)) / F; + Z.ph = r(2); + Z.rate = 0.75 + 0.35 * r(3); + // Which leg drags (-1 left, 1 right, 0 neither), how the arms go, which + // way the head hangs and how far. + Z.drag = r(4) < 0.6 ? (r(5) < 0.5 ? -1 : 1) : 0; + Z.arms = floor(r(6) * 3); + Z.loll = (r(7) < 0.5 ? -1 : 1) * (0.3 + 0.35 * r(8)); + Z.size = 0.92 + 0.2 * r(9); + Z.eyes = r(10) < 0.4; + Z.raiseL = (r(11) - 0.4) * 0.18; + Z.raiseR = (r(12) - 0.4) * 0.18; + // Half turned one way or the other, and stooped. + Z.yaw = (r(13) < 0.5 ? -1 : 1) * (0.45 + 0.4 * r(14)); + Z.stoop = 0.12 + 0.2 * r(15); + Z.fade = z ? 1 : 0; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const s = params.s, k = input.knobValues || [0, 2, 5, 0.06], d = cl(dt || 0, 0, 0.1); + s.hue = cl(k[0], 0, 1); + const sp = cl(k[1], 0.2, 3); + s.count = round(cl(k[2], 5, MAXZ)); + s.fog = cl(k[3], 0, 1); + if (!s.started) { + // The opening horde, nearest first: one already close, the rest spread + // evenly in the log of distance, which is how walking at a speed in + // proportion to distance spreads them. + for (let i = 0; i < s.count; i++) spawn(s.zs[i], 1.95 * (ZMAX / 1.95) ** ((i + 0.6 * hash(i + 9) * (i > 0)) / s.count), VX + (i ? 26 * COLS[i] : 13 * sin(s.count * 2.4))); + s.started = true; + } + const dd = (d * sp) / 2; + s.t += dd; + // A faster walk takes longer strides and lurches harder. + s.gait = 0.5 + 0.25 * sp; + const b = input.btnPressed || []; + if (b[0]) s.lookT = 3; + s.lookT = max(0, s.lookT - d); + s.look = cl(s.look + (s.lookT > 0.6 ? d / 0.12 : -d / 0.8), 0, 1); + for (let i = 0; i < MAXZ; i++) { + const Z = s.zs[i]; + if (i < s.count && !Z.z) spawn(Z, 0); + if (!Z.z) continue; + // They come on at a tenth of their distance a second at Speed 2, so + // each grows on the panel at the same steady rate and the near ones + // linger; slower again while a dragged leg comes through. + const u = (Z.ph + Z.rate * s.t) % 1; + Z.z -= dd * 0.1 * Z.z * (Z.drag ? 0.6 + 0.8 * abs(sin(TAU * u)) : 1); + Z.fade = i < s.count ? min(1, Z.fade + d * 0.5) : Z.fade - d; + if (Z.z < ZOUT || Z.fade < 0) i < s.count ? spawn(Z, 0) : (Z.z = 0); + } +} + +// The colours, and everything on the panel that only they change: the sky +// and the lane before fog, and the fog's own light at each LED. +function palette(s) { + const h = s.hue, { sky, glow, fogHue, base, fogC } = s; + // At 0 a toxic green glow under a purple sky; the glow turns through + // cyan and blue to magenta and the sky leans toward indigo on the way. + hsv(0.3 + 0.52 * h, 0.85, 1, glow); + hsv(0.78 - 0.14 * sin(PI * h), 0.82, 1, sky); + hsv(0.32 + 0.52 * h, 0.55, 1, fogHue); + for (let y = 0; y < H; y++) { + const yc = y + 0.5, dy = yc - YH; + for (let x = 0; x < W; x++) { + const q = y * W + x, o = q * 3, gl = GL[q], dx = x + 0.5 - GX; + let v, sh; + if (dy < 0) { + const u = yc / YH; + (v = 0.19 + 0.16 * u * u), (sh = gl); + } else { + // A wet lane between dark fields, mirroring the glow, brightest far + // off where the eye meets it at a graze: two wheel ruts full of water + // and a dark strip of grass between them. + const z = (F * HC) / dy, ax = abs(((x + 0.5 - VX) * z) / F); + const road = (1 - sm(ROAD - 0.12, ROAD + 0.12, ax)) * (0.45 + 0.55 * sm(0.12, 0.3, ax)), rut = exp(-(((ax - 0.55) / 0.16) ** 2)); + v = 0.05 + 0.05 * road; + sh = road * (0.7 + 0.5 * rut) * (0.12 + 0.4 * exp(-dy / 26) + 0.25 * exp(-(dx * dx) / (60 + 4 * dy))); + } + const fl = 0.42 + 0.65 * gl; + for (let c = 0; c < 3; c++) { + base[o + c] = (sky[c] * v + glow[c] * sh) * 255; + fogC[o + c] = fogHue[c] * 255 * fl; + } + } + } + s.palHue = h; +} + +// The fog's density at height h: thick at the ground and gone above a soft +// top that the Fog knob raises from 0.3 m to the chest. R is its integral +// up from the ground, in 64 steps to 2.4 m. +function fogTable(s) { + const top = 0.3 + 1.05 * s.fog, rho = 0.55 + 2.6 * s.fog, R = s.R; + let acc = 0; + R[0] = 0; + for (let i = 1; i <= 64; i++) { + acc += (rho * (1 - sm(top * 0.45, top * 1.15, ((i - 0.5) / 64) * 2.4)) * 2.4) / 64; + R[i] = acc; + } +} +const Rat = (R, h) => { + const u = cl((h / 2.4) * 64, 0, 64), i = min(63, floor(u)); + return R[i] + (R[i + 1] - R[i]) * (u - i); +}; +// Optical depth from the eye to a point z ahead at height h: z times the +// mean density between the two heights. +function tau(R, z, h) { + const dh = HC - h; + if (abs(dh) < 0.02) return (z * (Rat(R, HC + 0.02) - Rat(R, HC - 0.02))) / 0.04; + return (z * (Rat(R, HC) - Rat(R, h))) / dh; +} + +// One zombie's pose as capsules in LEDs, [x0, y0, x1, y1, r0, r1] each. +// The body is built in metres, feet at 0, x to its right, y up and z +// forward toward the eye, then turned by its yaw: a zombie half turned +// shows both its reaching arms off to one side. +function pose(s, Z, out) { + const z = Z.z, sc = (F / z) * Z.size, X = (Z.X = column(Z)), Y = YH + (F * HC) / z; + const u = (Z.ph + Z.rate * s.t) % 1, w = TAU * u, cy = cos(Z.yaw), sy = sin(Z.yaw), G = s.gait; + // The lurch: the trunk rolls over the planted leg, more so onto the good + // one when the other drags; it stoops forward by Z.stoop. + const roll = G * (0.08 * sin(w + 0.6) + (Z.drag ? 0.08 * Z.drag * max(0, sin(w * -Z.drag)) : 0)), cr = cos(roll), sr = sin(roll); + const hipY = 0.92 - 0.035 * abs(cos(w)), hx = 0.04 * sin(w), cs = cos(Z.stoop), ss = sin(Z.stoop); + let n = 0; + const q = [0, 0, 0, 0]; + // A point of the upper body: stoop about the hips, then roll. + const up = (x, y, f) => { + const y1 = y * cs, z1 = y * ss + f; + return [hx + x * cr + y1 * sr, hipY + y1 * cr - x * sr, z1]; + }; + const P = (a, b, r0, r1) => { + out[n++] = X + (a[0] * cy + a[2] * sy) * sc; + out[n++] = Y - a[1] * sc; + out[n++] = X + (b[0] * cy + b[2] * sy) * sc; + out[n++] = Y - b[1] * sc; + out[n++] = r0 * sc; + out[n++] = r1 * sc; + }; + for (const side of [-1, 1]) { + // Each leg swings forward in its half of the cycle; a dragged leg + // barely lifts and trails behind. + const dragged = Z.drag === side, ph = side < 0 ? w : w + PI, l = max(0, sin(ph)) * (dragged ? 0.15 : G); + const fz = (dragged ? 0.08 : 0.24 * G) * cos(ph - PI / 2) - (dragged ? 0.12 : 0), fy = 0.05 + 0.13 * l; + const hip = [hx + side * 0.1, hipY, 0], knee = [hx + side * 0.115, 0.5 + 0.06 * l, fz * 0.5 + 0.1 * l], foot = [side * 0.13, fy, fz]; + P(hip, knee, 0.09, 0.066); + P(knee, foot, 0.066, 0.05); + P(foot, [foot[0], foot[1] - 0.02, foot[2] + 0.1], 0.055, 0.05); + } + const neck = up(0, 0.5, 0), chest = up(0, 0.28, 0), sl = up(-0.17, 0.44, 0), sr2 = up(0.17, 0.44, 0); + P(up(0, 0.04, 0), chest, 0.15, 0.18); + P(chest, up(0, 0.44, 0), 0.18, 0.13); + P(sl, sr2, 0.08, 0.08); + // Rags of a shirt hanging below the waist. + for (const [a, b, c] of [[-0.13, -0.17, 0.13], [0.02, 0.05, 0.11], [0.12, 0.15, 0.09]]) P(up(a, 0.06, 0), up(b, -c, 0), 0.05, 0.022); + for (const side of [-1, 1]) { + // Arms reach forward at shoulder height with the hands hanging limp; + // some carry one arm dead at the side. + const sh = side < 0 ? sl : sr2, bob = 0.035 * G * sin(w + side * 1.3), raise = side < 0 ? Z.raiseL : Z.raiseR; + if (Z.arms === 2 && side > 0) { + const el = [sh[0] + 0.05, sh[1] - 0.3, sh[2] + 0.03], wr = [sh[0] + 0.07, sh[1] - 0.56, sh[2] + 0.06]; + P(sh, el, 0.065, 0.05); + P(el, wr, 0.05, 0.04); + P(wr, [wr[0] + 0.01, wr[1] - 0.1, wr[2]], 0.042, 0.03); + } else { + const el = [sh[0] + side * 0.09, sh[1] - 0.04 + raise + bob, sh[2] + 0.27], wr = [el[0] + side * 0.07, el[1] + 0.02 + raise * 0.5 + bob, el[2] + 0.25]; + P(sh, el, 0.07, 0.056); + P(el, wr, 0.056, 0.042); + P(wr, [wr[0], wr[1] - 0.13, wr[2] + 0.05], 0.046, 0.028); + } + } + // The head hangs to one side and swings with the step, unless it has + // snapped up to look; its jaw hangs open below it. + const la = (Z.loll + 0.2 * sin(w + 1.3)) * (1 - s.look) + roll, nk = [neck[0], neck[1] - 0.06, neck[2]]; + const hc = [neck[0] + 0.13 * sin(la), neck[1] + 0.13 * cos(la), neck[2] + 0.04]; + P(nk, hc, 0.065, 0.055); + P(hc, [hc[0] - 0.06 * sin(la), hc[1] - 0.06 * cos(la), hc[2] + 0.02], 0.11, 0.085); + return n; +} + +export function draw(display, params) { + const s = params.s; + if (!s) return; + if (s.palHue !== s.hue) palette(s); + fogTable(s); + const { col, base, bg, fogC, fc, nz, nb, dist, R, sA, sZ, glow, caps } = s, t = s.t; + // The fog's billows, on every other LED each way: they drift across and + // the bank breathes slowly. + for (let j = 0; j <= H / 2; j++) + for (let i = 0; i <= W / 2; i++) { + const x = 2 * i, yc = 2 * j + 0.5; + nb[j * 33 + i] = 0.6 + 0.8 * vn((x + 9 * t) / 13, yc / 5 + 0.3 * t) * (0.6 + 0.4 * vn((x - 4 * t) / 9, yc / 4 + 50)); + } + // The sky, the lane under its fog, and the scenery, fogged at its depth. + for (let y = 0; y < H; y++) { + const yc = y + 0.5, dy = yc - YH, tg = dy > 0 ? tau(R, (F * HC) / dy, 0) : 0, j = y >> 1, fy = (y & 1) * 0.5; + for (let x = 0; x < W; x++) { + const q = y * W + x, o = q * 3, i = j * 33 + (x >> 1), fx = (x & 1) * 0.5; + const n0 = nb[i] + (nb[i + 1] - nb[i]) * fx, n1 = nb[i + 33] + (nb[i + 34] - nb[i + 33]) * fx, n = n0 + (n1 - n0) * fy, L = 0.45 + 0.55 * n; + nz[q] = n; + // The billows catch the light unevenly too. + (fc[o] = fogC[o] * L), (fc[o + 1] = fogC[o + 1] * L), (fc[o + 2] = fogC[o + 2] * L); + let r = base[o], g = base[o + 1], b = base[o + 2]; + if (tg > 0) { + const f = 1 - exp(-tg * n); + r += (fc[o] - r) * f; + g += (fc[o + 1] - g) * f; + b += (fc[o + 2] - b) * f; + } + const a = sA[q]; + if (a > 0) { + const z = sZ[q], gl = GL[q], f = 1 - exp(-tau(R, z, HC - (dy * z) / F) * n) * (z > 50 ? 0.45 : 1); + const tr = 10 + glow[0] * 40 * gl, tgr = 6 + glow[1] * 40 * gl, tb = 14 + glow[2] * 40 * gl; + r += (tr + (fc[o] - tr) * f - r) * a; + g += (tgr + (fc[o + 1] - tgr) * f - g) * a; + b += (tb + (fc[o + 2] - tb) * f - b) * a; + } + col[o] = r; + col[o + 1] = g; + col[o + 2] = b; + bg[q] = max(r, g, b); + } + } + // Zombies, far to near, each as a soft distance field of its capsules. + const order = s.zs.filter((Z) => Z.z > 0).sort((a, b) => b.z - a.z); + for (const Z of order) { + const nc = pose(s, Z, caps), z = Z.z, scl = (F / z) * Z.size, fw = cl(0.6 + scl / 50, 0.8, 1.45); + let X0 = W, X1 = -1, Y0 = H, Y1 = -1; + for (let c = 0; c < nc; c += 6) { + const r = max(caps[c + 4], caps[c + 5]) + fw + 1; + X0 = min(X0, caps[c] - r, caps[c + 2] - r); + X1 = max(X1, caps[c] + r, caps[c + 2] + r); + Y0 = min(Y0, caps[c + 1] - r, caps[c + 3] - r); + Y1 = max(Y1, caps[c + 1] + r, caps[c + 3] + r); + } + (X0 = max(0, floor(X0))), (X1 = min(W - 1, floor(X1))), (Y0 = max(0, floor(Y0))), (Y1 = min(H - 1, floor(Y1))); + if (X0 > X1 || Y0 > Y1) continue; + for (let y = Y0; y <= Y1; y++) dist.fill(1e9, y * W + X0, y * W + X1 + 1); + for (let c = 0; c < nc; c += 6) { + const x0 = caps[c], y0 = caps[c + 1], x1 = caps[c + 2], y1 = caps[c + 3], r0 = caps[c + 4], r1 = caps[c + 5]; + const ex = x1 - x0, ey = y1 - y0, ll = ex * ex + ey * ey || 1e-9, R2 = max(r0, r1) + fw + 1; + const xa = max(X0, floor(min(x0, x1) - R2)), xb = min(X1, floor(max(x0, x1) + R2)), ya = max(Y0, floor(min(y0, y1) - R2)), yb = min(Y1, floor(max(y0, y1) + R2)); + for (let y = ya; y <= yb; y++) + for (let x = xa, q = y * W + xa; x <= xb; x++, q++) { + const px = x + 0.5 - x0, py = y + 0.5 - y0, u = cl((px * ex + py * ey) / ll, 0, 1), dx = px - u * ex, dy = py - u * ey; + const rr = r0 + (r1 - r0) * u, d2 = dx * dx + dy * dy, far = rr + fw; + if (d2 > far * far) continue; + const dd = sqrt(d2) - rr; + if (dd < dist[q]) dist[q] = dd; + } + } + // Black with a rim of the glow behind it, as bright as what it stands + // against, then fogged by the air between. + const fade = Z.fade * sm(ZOUT, ZFADE, z); + for (let y = Y0; y <= Y1; y++) { + const h = HC - ((y + 0.5 - YH) * z) / F, th = tau(R, z, h); + for (let x = X0, q = y * W + X0; x <= X1; x++, q++) { + const dd = dist[q]; + if (dd > fw) continue; + // A fading zombie comes apart where the fog billows thickest first. + let a = sm(fw, -fw, dd) * (fade > 0.99 ? 1 : sm(0, 0.35, fade * 1.4 - 0.5 * (nz[q] - 0.6))); + if (sZ[q] < z) a *= 1 - sA[q]; + if (a <= 0) continue; + const o = q * 3, rim = dd < -6 ? 0 : exp(min(0, dd) / 1.4) * min(1, bg[q] / 150) * 0.85, f = 1 - exp(-th * nz[q]) * (0.6 + 0.4 * fade); + const r = 18 + glow[0] * 200 * rim, g = 9 + glow[1] * 200 * rim, b = 32 + glow[2] * 200 * rim; + col[o] += (r + (fc[o] - r) * f - col[o]) * a; + col[o + 1] += (g + (fc[o + 1] - g) * f - col[o + 1]) * a; + col[o + 2] += (b + (fc[o + 2] - b) * f - col[o + 2]) * a; + } + } + // Eyes: some always, every one while the heads are up. + const eo = (Z.eyes ? 0.7 : 0) + s.look * 0.6; + if (eo > 0 && scl > 9) { + // On the face, which turns to the eye as the head comes up. + const yw = Z.yaw * (1 - s.look), hx = (caps[nc - 6] - Z.X) / scl + 0.08 * sin(yw), ey = caps[nc - 5] + 0.01 * scl; + const e = min(1, eo) * exp(-tau(R, z, 1.62)) * fade; + for (const side of [-1, 1]) glowDot(col, Z.X + (hx + side * 0.045 * cos(yw)) * scl, ey, max(0.5, 0.022 * scl), glow, e); + } + } + grain(display, col, nz); +} + +function glowDot(col, cx, cy, r, c, e) { + for (let y = max(0, floor(cy - 4)); y <= min(H - 1, floor(cy + 4)); y++) + for (let x = max(0, floor(cx - 4)); x <= min(W - 1, floor(cx + 4)); x++) { + const d2 = (x + 0.5 - cx) ** 2 + (y + 0.5 - cy) ** 2, k = e * (exp(-d2 / (r * r)) + 0.35 * exp(-d2 / (5 * r * r))), o = (y * W + x) * 3; + col[o] += (120 + c[0] * 135) * k; + col[o + 1] += (120 + c[1] * 135) * k; + col[o + 2] += (120 + c[2] * 135) * k; + } +} + +// Grain: a fine checker half a step above and below each LED's level, the +// mist's droplets, as strong as the picture around it allows so it never +// makes a step of more than 40 levels, and a little stronger where the fog +// billows thick. +const lv = new Float32Array(N); +function grain(display, col, nz) { + for (let q = 0; q < N; q++) lv[q] = min(255, max(col[q * 3], col[q * 3 + 1], col[q * 3 + 2])); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const q = y * W + x, m = lv[q], o = q * 3; + let g = 0; + if (x > 0) g = max(g, abs(m - lv[q - 1])); + if (x < W - 1) g = max(g, abs(m - lv[q + 1])); + if (y > 0) g = max(g, abs(m - lv[q - W])); + if (y < H - 1) g = max(g, abs(m - lv[q + W])); + const amp = max(0, min(19.5 * (0.8 + 0.15 * nz[q]), 0.45 * m, 0.45 * (255 - m), (39 - g) / 2)); + const f = m > 0.5 ? (m + ((x + y) & 1 ? amp : -amp)) / m : 1; + display.setPixel(x, y, min(255, round(col[o] * f)), min(255, round(col[o + 1] * f)), min(255, round(col[o + 2] * f))); + } +} From 57b964c818677e1ee5d873b9dd4ca3da12375dfa Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 23:42:39 +0000 Subject: [PATCH 6/8] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=205?= =?UTF-8?q?1=ED=8C=90=20=E2=80=94=20Persistence=20Barcode,=20Bat=20Form?= 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_016styVvYJeAE1svDoUBeGKX --- incoming/pattern-study/patterns/bat-form.js | 452 ++++++++++++++++++ .../patterns/persistence-barcode.js | 394 +++++++++++++++ 2 files changed, 846 insertions(+) create mode 100644 incoming/pattern-study/patterns/bat-form.js create mode 100644 incoming/pattern-study/patterns/persistence-barcode.js diff --git a/incoming/pattern-study/patterns/bat-form.js b/incoming/pattern-study/patterns/bat-form.js new file mode 100644 index 000000000..507585639 --- /dev/null +++ b/incoming/pattern-study/patterns/bat-form.js @@ -0,0 +1,452 @@ +// ===== Patternflow pattern ===== +// Title: Bat Form +// Family: halloween +// Idea: In the tall pointed window of a dark tower room a vampire stands black against a crimson and orange dusk, raises his arms so his cape spreads into a wide sail, shrinks as the cape's hem breaks into the scallops of a bat's wings, flies a loop out into the sky and back to the sill, and grows into the vampire again and folds his cape, while far bats circle over the hills. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 51 +// Round: 51 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Cape=-1..1, Flock=0..1 +// +// Knob 1: Hue. The dusk: overhead it stays purple, and the glow over the hills walks back round the wheel from gold at 0 through red at 0.15, magenta at 0.3, violet at 0.45 and blue at 0.6 to teal at 0.8 and lime green at 1. The vampire stays black, his eyes red and his cape's lining dark crimson. +// Knob 2: Speed. At 2 one change takes 10 seconds: his arms rise from 0.25 s and the cape is wide by 1.15 s, he shrinks into the bat from 2 to 3.7 s, the bat beats its wings four to five times a second, flies its loop from 3.4 to 7.6 s and lands, he is a man again by 8.9 s and has folded the cape by 9.7 s. At 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Cape. How far the cape opens, and with it the bat's wings: at -1 the open cape is 25 LEDs across and the wings 25; at 0, where it opens, 43 and 45; at 1 the cape is 62 across, spilling over the jambs, and the wings span the panel. +// Knob 4: Flock. How many far bats circle high in the sky and low over the hills, hazed a little toward the sky so the vampire stays the blackest thing: none at 0, 7 at 0.06 where it opens, 60 at 0.5 and a swarm of 120 at 1. +// Buttons: K1 tap while he stands starts the change now; tapped while he folds his cape, he opens it again. + +const W = 64, H = 128, N = W * H; +const { floor, ceil, round, min, max, abs, sqrt, sin, cos, exp, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ease = (t) => ((t = cl(t, 0, 1)), t * t * (3 - 2 * t)); +const lerp = (a, b, t) => a + (b - a) * t; +const hash = (i) => { + let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// The window: an equilateral pointed arch over straight jambs, its sill a +// ledge across the room. The vampire's chest stands at PX, PY. +const WX0 = 7, WX1 = 57, SPRING = 50, SILL = 104, SPAN = WX1 - WX0, TOP = SPRING - SPAN * 0.866; +const G0 = 1.15, PX = 32, PY = SILL - 30 * G0; +const inWindow = (x, y) => y <= SILL && x >= WX0 && x <= WX1 && (y >= SPRING || (Math.hypot(x - WX1, y - SPRING) <= SPAN && Math.hypot(x - WX0, y - SPRING) <= SPAN)); +// The dusk from the top of the window to the hills: height, how far the +// hue has gone from the purple overhead toward the hue over the hills, +// saturation and value. +const DUSK = [[0, 0, 0.85, 0.36], [0.3, 0.24, 0.85, 0.52], [0.55, 0.52, 0.85, 0.7], [0.78, 0.76, 0.9, 0.95], [0.9, 0.91, 0.85, 1], [1, 1, 0.7, 1]]; +// The far hills: their top per column. +const HILL = new Float32Array(W); +for (let x = 0; x < W; x++) HILL[x] = 99.5 + 1.6 * sin(x * 0.11 + 1) + 1.2 * sin(x * 0.31) + 0.6 * sin(x * 0.9); +// Two dead trees on the hills as line segments [x0, y0, x1, y1]. +const TREES = []; +for (const [bx, h, s] of [[13, 12, 1], [51, 9, -1]]) { + const by = HILL[bx]; + TREES.push([bx, by, bx + 0.5 * s, by - h], [bx + 0.3 * s, by - 0.55 * h, bx + 3.5 * s, by - 0.85 * h], [bx + 0.4 * s, by - 0.75 * h, bx - 2.5 * s, by - h - 1], [bx + 2 * s, by - 0.7 * h, bx + 3 * s, by - 0.6 * h - 3]); +} + +// Three poses of the right half of the figure, in LEDs, feet at (0, 0) and +// up negative; the left half mirrors it. head: centre and radius. collar: +// inner and outer foot and the tip; it rises into the bat's ear. sh, waist, +// hip, foot: the body's side. arm: shoulder, elbow, wrist. tips: the cape's +// hem, which becomes the trailing edge of the wing, and att where the +// membrane meets the body; dep is how deep the scallops between them are. +const CLOSED = { + head: [0, -40.5, 3.6], collar: [2.4, -36.6, 6.2, -35.4, 7.4, -46.5], + sh: [5.6, -35.4], waist: [4, -22], hip: [4.2, -19.5], foot: [2.6, 0], + arm: [5.4, -35.4, 7.2, -26, 8.4, -16], tips: [11.2, -1.6, 8.2, -1, 5.2, -0.8, 2.6, -0.8], att: [0.4, -0.8], dep: 0.12, +}; +const SPREAD = { + head: [0, -40.5, 3.6], collar: [2.4, -36.6, 6.2, -35.4, 7.4, -46.5], + sh: [5.6, -35.4], waist: [4, -22], hip: [4.2, -19.5], foot: [3.4, 0], + arm: [5.6, -35.4, 12.5, -40.5, 18.5, -45.5], tips: [20.5, -24, 16.5, -10.5, 11, -4.5, 5.6, -2], att: [1.6, -2.4], dep: 0.22, +}; +const BAT = { + head: [0, -33.6, 2.6], collar: [0.5, -35.4, 2.6, -34.3, 2.6, -39.8], + sh: [2.3, -31.3], waist: [1.9, -27.5], hip: [1.5, -25.8], foot: [1, -23], + arm: [2.3, -31.3, 8.5, -35.6, 14.5, -37], tips: [27, -30.5, 21.5, -26.6, 15, -25.2, 8.5, -26.6], att: [2.3, -27], dep: 0.5, +}; +const mixPose = (A, B, t, o) => { + for (const k in A) o[k] = typeof A[k] === "number" ? lerp(A[k], B[k], t) : A[k].map((v, i) => lerp(v, B[k][i], t)); + return o; +}; + +// Small bats far over the hills: orbit centre, radii, speed and phase, +// half span and wing beat. +const FLOCK = []; +for (let i = 0; i < 120; i++) { + const r = (k) => hash(i * 7 + k); + // Most fly high, where the cape never covers them; each orbit fits the + // arch at its height. + const cy = r(11) < 0.7 ? 14 + 44 * r(2) : 60 + 32 * r(2), half = (cy >= SPRING ? SPAN : sqrt(SPAN * SPAN - (cy - SPRING) ** 2)) - SPAN / 2 - 3; + const rx = min(half * 0.6, 3 + 9 * r(3)); + FLOCK.push({ cx: 32 + (2 * r(1) - 1) * max(0, half - rx), cy, rx, ry: 2 + 4 * r(4), w: (r(5) < 0.5 ? -1 : 1) * (0.5 + 0.8 * r(6)), ph: TAU * r(7), s: 3 + 4.4 * r(8) ** 2, f: 9 + 6 * r(9), fp: TAU * r(10) }); +} + +// A small bat's right half, half span 1, from the top of the head round to +// the middle of its belly: x, y and how far the beat lifts the point. +const MINI = [0, -0.36, 0, 0.1, -0.4, 0, 0.16, -0.66, 0, 0.24, -0.34, 0, 0.5, -0.5, 1, 1, -0.3, 1.4, 0.84, -0.04, 1.1, 0.7, 0.04, 0.9, 0.6, -0.06, 0.8, 0.44, 0.12, 0.6, 0.34, 0, 0.4, 0.2, 0.18, 0.2, 0.1, 0.32, 0, 0, 0.34, 0]; + +// One change in story seconds at Speed 2. +const CYCLE = 10; + +export function setup(params) { + params.s = { + t: 0, clock: 0, hue: 0, speed: 2, cape: 0, flock: 0.06, key: "", btn: false, flap: 0, spread: 0, m: 0, lift: 0, fly: 0, beat: 0, + base: new Float32Array(N * 3), buf: new Float32Array(N * 3), win: new Uint8Array(N), covB: new Float32Array(N), covM: new Float32Array(N), + pose: {}, pts: new Float64Array(64), xs: [], col: new Float32Array(3), + }; +} + +// The room, the window and the dusk, kept until Hue moves. +function background(S) { + const { base, win } = S, c = S.col, hue = S.hue; + for (let p = 0; p < N; p++) { + const x = p % W, y = floor(p / W), cx = x + 0.5, cy = y + 0.5; + win[p] = inWindow(cx, cy) ? 1 : 0; + } + for (let p = 0; p < N; p++) { + const x = p % W, y = floor(p / W), cx = x + 0.5, cy = y + 0.5, o = 3 * p; + if (win[p]) { + sky(hue, (cy - TOP) / (SILL - TOP), c); + let [r, g, b] = c; + // The jamb: a band just inside the opening where the stone is lit. + let edge = 9; + for (let dy = -2; dy <= 2; dy++) + for (let dx = -2; dx <= 2; dx++) { + const xx = x + dx, yy = y + dy; + if (xx < 0 || xx >= W || yy < 0 || yy >= H || !win[yy * W + xx]) edge = min(edge, max(abs(dx), abs(dy))); + } + if (edge <= 2 && cy < SILL - 0.5) { + const k = edge === 1 ? 0.42 : 0.62; + (r = r * k + 0.04), (g = g * k + 0.02), (b = b * k + 0.06); + } + // The hills and their trees, dark against the glow. + if (cy > HILL[x]) [r, g, b] = cy - HILL[x] < 1 ? [r * 0.35, g * 0.25, b * 0.4] : [0.05, 0.02, 0.07]; + base[o] = r; + base[o + 1] = g; + base[o + 2] = b; + } else { + // The room: dark stone in courses, warmed near the window, the sill's + // top edge lit. + const near = max(0, 1 - distWin(cx, cy) / 14); + const course = (y % 6 === 5) || ((x + (floor(y / 6) % 2) * 5) % 10 === 0) ? 0.55 : 1; + sky(hue, 0.55, c); + let k = (0.06 + 0.16 * near * near) * course; + let r = 0.05 * course + c[0] * k, g = 0.02 * course + c[1] * k, b = 0.08 * course + c[2] * k; + if (y >= SILL && y < SILL + 6 && x >= WX0 - 3 && x <= WX1 + 3) { + sky(hue, 0.95, c); + const top = y === SILL ? 0.75 : y === SILL + 1 ? 0.3 : y < SILL + 4 ? 0.12 : 0.07; + (r = c[0] * top + 0.03), (g = c[1] * top + 0.01), (b = c[2] * top + 0.05); + } + base[o] = r; + base[o + 1] = g; + base[o + 2] = b; + } + } + for (const [x0, y0, x1, y1] of TREES) { + const n = ceil(max(abs(x1 - x0), abs(y1 - y0)) * 2); + for (let i = 0; i <= n; i++) { + const x = floor(lerp(x0, x1, i / n)), y = floor(lerp(y0, y1, i / n)), o = 3 * (y * W + x); + if (x >= 0 && x < W && y >= 0 && y < H) base.set([0.05, 0.02, 0.07], o); + } + } +} +// Rough distance from (x, y) to the window opening. +function distWin(x, y) { + if (y > SILL) return Math.hypot(max(0, WX0 - x, x - WX1), y - SILL); + if (y >= SPRING) return max(0, WX0 - x, x - WX1); + return max(0, Math.hypot(x - (x < 32 ? WX1 : WX0), y - SPRING) - SPAN, WX0 - x, x - WX1); +} +function sky(hue, t, out) { + t = cl(t, 0, 1); + let i = 0; + while (i < DUSK.length - 2 && t > DUSK[i + 1][0]) i++; + const A = DUSK[i], B = DUSK[i + 1], u = (t - A[0]) / (B[0] - A[0]); + // Over the hills gold at Hue 0, walking back through red, magenta, + // violet and blue to green at 1; overhead always purple, so the dim top + // never turns brown. + const low = 1.12 - 0.85 * hue; + hsv(0.79 + (low - 0.79) * lerp(A[1], B[1], u), lerp(A[2], B[2], u), lerp(A[3], B[3], u), out, 0); +} + +// Coverage of a polygon added into cov: four sub-rows per LED, the span +// ends shared out by area, nonzero winding. +function fill(S, cov, P, n) { + let y0 = 1e9, y1 = -1e9; + for (let i = 0; i < n; i++) (y0 = min(y0, P[2 * i + 1])), (y1 = max(y1, P[2 * i + 1])); + const s0 = max(0, floor(y0 * 4)), s1 = min(4 * H - 1, ceil(y1 * 4)), xs = S.xs; + for (let sy = s0; sy <= s1; sy++) { + const y = (sy + 0.5) / 4, row = floor(y) * W; + xs.length = 0; + for (let i = 0, j = n - 1; i < n; j = i++) { + const ay = P[2 * j + 1], by = P[2 * i + 1]; + if (ay <= y !== by <= y) { + const ax = P[2 * j], bx = P[2 * i]; + xs.push(ax + ((y - ay) / (by - ay)) * (bx - ax), by > ay ? 1 : -1); + } + } + // Sort the crossings by x (a few, so insertion sort). + for (let i = 2; i < xs.length; i += 2) { + const vx = xs[i], vd = xs[i + 1]; + let j = i - 2; + while (j >= 0 && xs[j] > vx) (xs[j + 2] = xs[j]), (xs[j + 3] = xs[j + 1]), (j -= 2); + xs[j + 2] = vx; + xs[j + 3] = vd; + } + let wind = 0, from = 0; + for (let i = 0; i < xs.length; i += 2) { + const before = wind; + wind += xs[i + 1]; + if (before === 0) from = xs[i]; + else if (wind === 0) span(cov, row, from, xs[i], 0.25); + } + } +} +function span(cov, row, xa, xb, w) { + xa = max(xa, 0); + xb = min(xb, W); + if (xb <= xa) return; + const ia = floor(xa), ib = floor(xb); + if (ia === ib) { + cov[row + ia] += (xb - xa) * w; + return; + } + cov[row + ia] += (ia + 1 - xa) * w; + for (let x = ia + 1; x < ib; x++) cov[row + x] += w; + if (ib < W) cov[row + ib] += (xb - ib) * w; +} + +// Where the story is at story time t: the spread s (0 cape closed, 1 arms +// up and the cape wide), the change m (0 man, 1 bat), the flight f along its +// loop, the lift off the sill and the wing beat's strength. +function story(S) { + const t = S.t; + S.spread = t < 5 ? ease((t - 0.25) / 0.9) : 1 - ease((t - 8.9) / 0.8); + S.m = t < 5 ? ease((t - 2) / 1.7) : 1 - ease((t - 7.8) / 1.1); + S.lift = ease((t - 2.9) / 0.8) - ease((t - 7.6) / 0.5); + S.fly = ease((t - 3.4) / 4.2); + S.beat = ease((t - 2.3) / 0.8) * (1 - ease((t - 8) / 0.7)); +} + +// The figure's outline for the current story, into covB (body) and covM +// (cape and wings), and the eyes. +function figure(S) { + const wC = 0.92 + 0.4 * S.cape, m = S.m, mb = ease((m - 0.15) / 0.85), mw = ease(m / 0.85); + const A = mixPose(CLOSED, SPREAD, S.spread, S.pa || (S.pa = {})); + // The arm swings up through its angles, keeping its length, so the cape + // opens like a wing instead of being pulled flat. + const c0 = CLOSED.arm, c1 = SPREAD.arm, ang = (q, i) => Math.atan2(q[i + 3] - q[i + 1], q[i + 2] - q[i]), len = (q, i) => Math.hypot(q[i + 2] - q[i], q[i + 3] - q[i + 1]); + for (let i = 0; i < 4; i += 2) { + const th = lerp(ang(c0, i), ang(c1, i), S.spread), l = lerp(len(c0, i), len(c1, i), S.spread); + A.arm[i + 2] = A.arm[i] + l * cos(th); + A.arm[i + 3] = A.arm[i + 1] + l * sin(th); + } + const Pz = mixPose(A, BAT, mw, S.pose); + // The body changes a little behind the wings. + for (const k of ["head", "sh", "waist", "hip", "foot"]) Pz[k] = A[k].map((v, i) => lerp(v, BAT[k][i], mb)); + // Cape: the arms and hem reach wider with the knob, more so once spread. + const wx = lerp(1 + 0.6 * (wC - 1), wC, max(S.spread, m)); + // Where the chest is, how big the figure is and how it leans. + const f = S.fly, a = TAU * f, depth = 1 - 0.6 * sin(PI * f) ** 2; + const g = G0 * lerp(1, 0.78, mb) * depth, cx = PX + 13 * sin(a), cy = PY - 4 * S.lift - 22 * (1 - cos(a)), bank = 0.32 * sin(a) * sin(PI * f); + // Out past the window the wall hides what strays beyond the opening. + S.clip = depth < 0.97; + const flap = S.beat * 0.55 * sin(S.flap), cb = cos(bank), sb = sin(bank); + const pt = (x, y, side) => { + // local (feet at 0, chest at -30) to the panel. + const lx = side * x, ly = y + 30; + return [cx + g * (cb * lx - sb * ly), cy + g * (sb * lx + cb * ly)]; + }; + const P = S.pts; + S.box = [99, -99, 999, -99]; + const poly = (cov, list) => { + for (let i = 0; i < list.length; i++) { + P[2 * i] = list[i][0]; + P[2 * i + 1] = list[i][1]; + S.box[0] = min(S.box[0], list[i][0]); + S.box[1] = max(S.box[1], list[i][0]); + S.box[2] = min(S.box[2], list[i][1]); + S.box[3] = max(S.box[3], list[i][1]); + } + fill(S, cov, P, list.length); + }; + for (const side of [1, -1]) { + // Wing: arm then hem, turned about the shoulder by the beat. + const [sx, sy] = Pz.sh, ca = cos(flap), sa = sin(flap); + const rot = (x, y, ch = 1) => { + const dx = x * wx - sx, dy = (y - sy) * ch; + return [sx + ca * dx + sa * dy, sy - sa * dx + ca * dy]; + }; + // The arms lift the sail higher as well as wider with the knob. + const up = lerp(1, 1 + 0.22 * S.cape, max(S.spread, m)); + const ar = Pz.arm, el = rot(ar[2], ar[3], up), wr = rot(ar[4], ar[5], up); + const T = []; + // The hem stirs in the draught from the window while the cape is wide. + const stir = 0.9 * S.spread * (1 - mw); + const ch = lerp(1, 1 + 0.2 * S.cape, mw); + for (let i = 0; i < 4; i++) T.push(rot(Pz.tips[2 * i] + stir * sin(S.clock * 2.3 + i * 1.7 + side), Pz.tips[2 * i + 1] + stir * 0.6 * sin(S.clock * 1.9 + i * 2.3), ch)); + T.push(Pz.att); + const out = [[sx, sy - 1.2], el, wr, T[0]]; + for (let i = 0; i < 4; i++) { + // A scallop from tip i to i + 1, bowed toward the shoulder. + const [x0, y0] = T[i], [x1, y1] = T[i + 1], mx = (x0 + x1) / 2, my = (y0 + y1) / 2, L = Math.hypot(x1 - x0, y1 - y0); + const ux = sx - mx, uy = sy - my, ul = Math.hypot(ux, uy) || 1, d = Pz.dep * L; + const qx = mx + (ux / ul) * d, qy = my + (uy / ul) * d; + for (let k = 1; k <= 4; k++) { + const u = k / 4, v = 1 - u; + out.push([v * v * x0 + 2 * u * v * qx + u * u * x1, v * v * y0 + 2 * u * v * qy + u * u * y1]); + } + } + poly(S.covM, out.map(([x, y]) => pt(x, y, side))); + // Arm bones over the membrane. + const arm = (a0, a1, r0, r1) => { + const dx = a1[0] - a0[0], dy = a1[1] - a0[1], l = Math.hypot(dx, dy) || 1, nx = -dy / l, ny = dx / l; + poly(S.covB, [[a0[0] + nx * r0, a0[1] + ny * r0], [a1[0] + nx * r1, a1[1] + ny * r1], [a1[0] - nx * r1, a1[1] - ny * r1], [a0[0] - nx * r0, a0[1] - ny * r0]].map(([x, y]) => pt(x, y, side))); + }; + const thick = lerp(1.3, 0.75, mw); + arm([sx, sy], el, thick * 1.2, thick); + arm(el, wr, thick, thick * 0.8); + // Collar, rising into the ear. + const c = Pz.collar; + poly(S.covB, [[c[0], c[1]], [c[2], c[3]], [c[4], c[5]]].map(([x, y]) => pt(x, y, side))); + // Body side: neck, shoulder, waist, hip, leg to the foot, and the + // middle line back up. + const hd = Pz.head, nk = hd[1] + hd[2] * 0.7; + poly(S.covB, [[0, nk], [hd[2] * 0.45, nk], [Pz.sh[0], Pz.sh[1]], [Pz.waist[0], Pz.waist[1]], [Pz.hip[0], Pz.hip[1]], [Pz.foot[0] + 0.9, Pz.foot[1]], [Pz.foot[0] - 1.6 * (1 - mb) - 0.3, Pz.foot[1]], [0.4, Pz.hip[1] + 1.5 * (1 - mb)], [0, Pz.hip[1] + 1.5 * (1 - mb)]].map(([x, y]) => pt(x, y, side))); + } + // Head. + const hd = Pz.head, circ = []; + for (let i = 0; i < 14; i++) circ.push(pt(hd[0] + hd[2] * cos((TAU * i) / 14), hd[1] + hd[2] * sin((TAU * i) / 14), 1)); + poly(S.covB, circ); + // Eyes, while it faces the room. + const away = sin(PI * f) > 0.35; + S.eyes = away ? null : [pt(hd[2] * 0.36, hd[1] - 0.2, 1), pt(hd[2] * 0.36, hd[1] - 0.2, -1)]; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = cl(k[0], 0, 1); + S.speed = cl(k[1], 0.2, 3); + S.cape = cl(k[2], -1, 1); + S.flock = cl(k[3], 0, 1); + const ds = (d * S.speed) / 2; + const btn = !!(input.btnPressed && input.btnPressed[0]); + // A tap while he stands starts the change. While he folds the cape he + // opens it again from the same width: the fold is the spread run + // backwards, so the time that matches is a mirror image. + if (btn && !S.btn) { + if (S.t < 0.25) S.t = 0.25; + else if (S.t >= 8.9) S.t = 0.25 + 0.9 * (1 - cl((S.t - 8.9) / 0.8, 0, 1)); + } + S.btn = btn; + S.t = (S.t + ds) % CYCLE; + S.clock += ds; + S.flap += ds * TAU * (4.2 + 1.2 * S.beat * (1 - S.fly)); + story(S); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const key = S.hue.toFixed(3); + if (key !== S.key) { + S.key = key; + background(S); + } + const { base, buf, win, covB, covM } = S; + buf.set(base); + // Thin clouds drifting across the dusk. + for (let y = floor(TOP); y < 92; y++) + for (let x = WX0; x <= WX1; x++) { + const p = y * W + x; + if (!win[p]) continue; + let c = 0; + for (let i = 0; i < 3; i++) { + const yc = 24 + 21 * i + 2 * sin(x * 0.08 + i * 2 + S.clock * 0.1), dy = (y - yc) / (1.2 + 0.6 * i); + const patch = 0.5 + 0.5 * sin((x + S.clock * (1.2 + 0.4 * i)) * (0.13 + 0.03 * i) + i * 4); + c += exp(-dy * dy) * patch; + } + if (c > 0.02) { + const o = 3 * p, k = 1 - 0.45 * min(1, c); + buf[o] *= k; + buf[o + 1] *= k; + buf[o + 2] *= k * 0.95 + 0.05 * c; + } + } + // Small bats against the sky, seen only through the window: a filled + // outline each, the wings lifted and dropped by the beat. + const nb = round(120 * S.flock), sb = S.covM, P = S.pts; + sb.fill(0); + for (let i = 0; i < nb; i++) { + // Each flutters off its orbit in small jerks, as bats do. + const b = FLOCK[i], a = b.ph + b.w * S.clock * 0.9, c = S.clock * b.f * 0.37 + b.fp; + const x = b.cx + b.rx * cos(a) + 1.2 * sin(c) * sin(2.3 * c), y = b.cy + b.ry * sin(a) + 1.4 * sin(1.7 * c + 1) * sin(0.9 * c); + const lift = -0.5 * sin(b.f * S.clock + b.fp); + let n = 0; + for (const sd of [1, -1]) + for (let j = 0; j < MINI.length; j += 3) { + const k = sd > 0 ? j : MINI.length - 3 - j, lx = MINI[k], ly = MINI[k + 1] + MINI[k + 2] * lift; + P[2 * n] = x + sd * lx * b.s; + P[2 * n + 1] = y + ly * b.s; + n++; + } + fill(S, sb, P, n); + } + if (nb) + for (let p = 0; p < N; p++) { + const a = min(1, sb[p]); + if (a <= 0 || !win[p]) continue; + // Hazed a little toward the sky, so they stay apart from the black + // figure in front of them. + const o = 3 * p; + buf[o] += (0.2 * base[o] + 0.01 - buf[o]) * a; + buf[o + 1] += (0.2 * base[o + 1] - buf[o + 1]) * a; + buf[o + 2] += (0.2 * base[o + 2] + 0.02 - buf[o + 2]) * a; + } + // The vampire or the bat. + covB.fill(0); + covM.fill(0); + figure(S); + const bx0 = max(0, floor(S.box[0]) - 1), bx1 = min(W - 1, ceil(S.box[1]) + 1), by0 = max(0, floor(S.box[2]) - 1), by1 = min(H - 1, ceil(S.box[3]) + 1); + // The cape's lining shows dark crimson while it is spread, and goes as it + // becomes a wing. + const lin = 0.16 * S.spread * (1 - ease(S.m / 0.6)), lining = [lin, lin * 0.05, lin * 0.12]; + for (let y = by0; y <= by1; y++) + for (let x = bx0; x <= bx1; x++) { + const p = y * W + x, cb = min(1, covB[p]), cm = min(1, covM[p]), a = max(cb, cm); + if (a <= 0 || (S.clip && !win[p])) continue; + const o = 3 * p, fr = 0.02 * (1 - cb) + 0.015, fg = 0.008, fbl = 0.03; + const lr = lining[0] * (1 - cb), lg = lining[1] * (1 - cb), lb = lining[2] * (1 - cb); + buf[o] += (fr + lr - buf[o]) * a; + buf[o + 1] += (fg + lg - buf[o + 1]) * a; + buf[o + 2] += (fbl + lb - buf[o + 2]) * a; + } + if (S.eyes) { + const glow = 0.6 + 0.4 * sin(S.clock * 3); + for (const [ex, ey] of S.eyes) { + const x = floor(ex), y = floor(ey); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const o = 3 * (y * W + x); + buf[o] = glow; + buf[o + 1] = 0.05 * glow; + buf[o + 2] = 0.04 * glow; + } + } + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const o = 3 * p; + display.setPixel(x, y, round(cl(buf[o], 0, 1) * 255), round(cl(buf[o + 1], 0, 1) * 255), round(cl(buf[o + 2], 0, 1) * 255)); + } +} diff --git a/incoming/pattern-study/patterns/persistence-barcode.js b/incoming/pattern-study/patterns/persistence-barcode.js new file mode 100644 index 000000000..9c042cd64 --- /dev/null +++ b/incoming/pattern-study/patterns/persistence-barcode.js @@ -0,0 +1,394 @@ +// ===== Patternflow pattern ===== +// Title: Persistence Barcode +// Family: topology +// Idea: Disks grow round points scattered about a ring until they swallow it while the Delaunay edges and triangles they reach join in, and below them the barcode of that growth, reduced exactly over Z/2, lights up behind a sweeping line: short pale teal bars for points whose disks merge and one long orange bar for the ring's hole, whose loop and shrinking rim glow the same orange until the hole closes. +// Author: patternflow-lab +// Date: 2026-10-25 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: persistent homology (Herbert Edelsbrunner, David Letscher and Afra Zomorodian, 2002) of the alpha complex of the points (alpha shapes, Herbert Edelsbrunner, David Kirkpatrick and Raimund Seidel, 1983), drawn as a barcode (Afra Zomorodian and Gunnar Carlsson, 2005), pattern round 51 +// Round: 51 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Noise=0..1, Rings=1..3 +// +// Knob 1: Hue. Turns the loops' colours round the wheel while the disks swing between violet and green and the merging bars between pale teal and pale violet, so nothing dim goes brown. At 0 violet disks, teal bars and an orange loop (the second and third rings pink and yellow); at 0.25 green loops on teal disks; at 0.5 blue loops on green disks; at 0.75 magenta loops. +// Knob 2: Speed. At 2 the disks grow from nothing to 1.12 times the radius that closes the last hole in 7 seconds, the whole barcode holds for 1.5 s and a fresh sample comes at 9 s; at 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Noise. How far the points scatter from their circles: a twentieth of the radius at 0, where one ring's bar spans about three quarters of the ruler with one or two short bars beside it; a third at 1, where the ring frays into clusters and its bar spans under half among some 28 short ones. Turning it slides the same points, and the barcode is computed again at once. +// Knob 4: Rings. One, two or three circles sharing 40, 38 or 36 points, so that many long bars stand in the barcode, orange, pink and yellow, each loop drawn in its bar's colour. +// Buttons: K1 tap draws a fresh sample now. + +const W = 64, H = 128; +const { floor, round, min, max, abs, sqrt, sin, cos, log, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// The cloud is the square of rows 0..63; the barcode starts at row BT, and +// the bottom row is its ruler of r. +const CH = 64, BT = 68; +// The rings for one, two and three circles, [cx, cy, R] in LEDs, and how +// many points they share. +const LAYOUT = [ + { n: 40, rings: [[31.5, 31.5, 24]] }, + { n: 38, rings: [[21, 21, 15.5], [45, 45, 11]] }, + { n: 36, rings: [[15, 15, 10.5], [33, 33, 9], [50.5, 50.5, 8.5]] }, +]; +const MAXN = 40; +// Story seconds at Speed 2: the disks grow, the full barcode holds, the +// picture fades and the next sample begins. +const GROW = 7, HOLD = 1.5, FADE = 0.5, CYCLE = GROW + HOLD + FADE; + +export function setup(params) { + params.s = { + t: 0, hue: 0, speed: 2, noise: 0, rings: 1, key: "", seed: 0, first: true, btn: false, + rnd: null, n: 0, X: new Float64Array(MAXN), Y: new Float64Array(MAXN), K: null, + buf: new Float32Array(W * H * 3), pal: new Float32Array(8 * 3), + }; + fresh(params.s); +} + +// A new sample's randomness: each point's place along its circle and its +// standard normal offset from it, and each ring's turn, size and shift. +function fresh(S) { + const R = Math.random, gauss = () => cl(sqrt(-2 * log(1 - R())) * cos(TAU * R()), -2.6, 2.6); + S.rnd = { + ja: Array.from({ length: MAXN }, () => R() - 0.5), + z: Array.from({ length: MAXN }, gauss), + ring: Array.from({ length: 3 }, () => [TAU * R(), 1 + 0.08 * (R() - 0.5), 2 * (R() - 0.5), 2 * (R() - 0.5)]), + sym: floor(8 * R()), + }; + S.seed++; + S.key = ""; +} + +// The points for the current knobs from the sample's randomness, so turning +// Noise slides them instead of drawing new ones. +function place(S) { + const L = LAYOUT[S.rings - 1], rr = S.rnd, sig = 0.05 + 0.28 * S.noise; + const tot = L.rings.reduce((a, g) => a + g[2], 0); + let i = 0; + L.rings.forEach((g, k) => { + const m = k === L.rings.length - 1 ? L.n - i : round((L.n * g[2]) / tot), [ph, sz, sx, sy] = rr.ring[k]; + for (let j = 0; j < m; j++, i++) { + const th = ph + (TAU * (j + 0.7 * rr.ja[i])) / m, rad = g[2] * sz * (1 + sig * rr.z[i]); + let u = g[0] + sx + rad * cos(th) - 31.5, v = g[1] + sy + rad * sin(th) - 31.5; + if (rr.sym & 1) u = -u; + if (rr.sym & 2) v = -v; + if (rr.sym & 4) [u, v] = [v, u]; + // Points pushed against the edge are nudged apart, so two never meet. + S.X[i] = cl(31.5 + u, 1, 62.5) + 0.001 * i; + S.Y[i] = cl(31.5 + v, 1, CH - 1.5) + 0.0007 * i; + } + }); + S.n = L.n; +} + +// The alpha complex of the points and its persistence. Delaunay triangles +// are the triples whose circumcircle holds no other point, and a triangle +// enters when the disks reach its circumradius. An edge enters at half its +// length, except when a triangle on one side has an obtuse angle facing it +// (its third point lies inside the circle on the edge); then the edge enters +// with that triangle. At radius r this complex has the shape of the union of +// the disks, so the barcode is the barcode of the picture. +// H0 comes from joining components in order; H1 from reducing the +// triangles' boundary columns over Z/2, each surviving column's lowest edge +// giving birth to the hole the triangle kills. The reduced column is that +// hole's loop and the triangles summed into it are what fills it. +function persistence(X, Y, n) { + const T = []; + for (let a = 0; a < n; a++) + for (let b = a + 1; b < n; b++) + for (let c = b + 1; c < n; c++) { + const ax = X[a], ay = Y[a], bx = X[b] - ax, by = Y[b] - ay, cx = X[c] - ax, cy = Y[c] - ay; + const d = 2 * (bx * cy - by * cx); + if (abs(d) < 1e-9) continue; + const b2 = bx * bx + by * by, c2 = cx * cx + cy * cy, ux = (cy * b2 - by * c2) / d, uy = (bx * c2 - cx * b2) / d; + const R2 = ux * ux + uy * uy, ox = ax + ux, oy = ay + uy; + let ok = true; + for (let e = 0; e < n && ok; e++) { + if (e === a || e === b || e === c) continue; + const dx = X[e] - ox, dy = Y[e] - oy; + if (dx * dx + dy * dy < R2 * (1 - 1e-9)) ok = false; + } + if (ok) T.push({ v: [a, b, c], x: ox, y: oy, R: sqrt(R2), e: [] }); + } + const emap = new Map(), E = []; + T.forEach((t) => { + const [a, b, c] = t.v; + for (const [p, q, o] of [[a, b, c], [a, c, b], [b, c, a]]) { + const k = p * 64 + q; + let e = emap.get(k); + if (!e) { + e = { a: p, b: q, v: 0.5 * Math.hypot(X[q] - X[p], Y[q] - Y[p]), att: null, o: 0 }; + emap.set(k, e); + E.push(e); + } + t.e.push(e); + if ((X[p] - X[o]) * (X[q] - X[o]) + (Y[p] - Y[o]) * (Y[q] - Y[o]) < 0) e.att = t; + } + }); + for (const e of E) if (e.att) e.v = e.att.R; + E.sort((p, q) => p.v - q.v); + E.forEach((e, i) => (e.o = i)); + T.sort((p, q) => p.R - q.R); + T.forEach((t, i) => (t.o = i)); + // H0: every point is born at 0; joining two components kills one. + const up = Array.from({ length: n }, (_, i) => i), find = (i) => (up[i] === i ? i : (up[i] = find(up[i]))); + const h0 = []; + for (const e of E) { + const ra = find(e.a), rb = find(e.b); + if (ra !== rb) { + up[ra] = rb; + h0.push(e.v); + } + } + while (h0.length < n) h0.push(Infinity); + // H1: column reduction. + const nw = (E.length + 31) >> 5, tw = (T.length + 31) >> 5, owner = new Int32Array(E.length).fill(-1); + const cols = [], vs = [], h1 = []; + const top = (c) => { + for (let w = nw - 1; w >= 0; w--) if (c[w]) return 32 * w + 31 - Math.clz32(c[w]); + return -1; + }; + const bits = (c, w) => { + const out = []; + for (let i = 0; i < 32 * w; i++) if (c[i >> 5] & (1 << (i & 31))) out.push(i); + return out; + }; + let zero = 0; + T.forEach((t, j) => { + const col = new Uint32Array(nw), vv = new Uint32Array(tw); + for (const e of t.e) col[e.o >> 5] ^= 1 << (e.o & 31); + vv[j >> 5] |= 1 << (j & 31); + let low = top(col); + while (low >= 0 && owner[low] >= 0) { + const k = owner[low]; + for (let w = 0; w < nw; w++) col[w] ^= cols[k][w]; + for (let w = 0; w < tw; w++) vv[w] ^= vs[k][w]; + low = top(col); + } + cols.push(col); + vs.push(vv); + if (low < 0) return; + owner[low] = j; + const b = E[low].v, d = t.R; + if (d - b > 1e-9) h1.push({ b, d, cyc: bits(col, nw), reg: bits(vv, tw) }); + else zero++; + }); + let essential = 0; + E.forEach((e, i) => { + if (owner[i] < 0) essential++; + }); + // Edges that joined components are never pivots, so essential counts the + // n - 1 of them plus any loop that never closed (none in a triangulated + // disk). zero counts loops filled at the radius they closed. + return { T, E, h0, h1, zero, essential: essential - (n - 1) }; +} + +// Rebuild the complex, the per-LED maps of the cloud and the barcode's rows. +function build(S) { + place(S); + const { X, Y, n } = S, K = (S.K = persistence(X, Y, n)); + const { T, h0, h1 } = K; + let far = 0; + for (const d of h0) if (d < Infinity) far = max(far, d); + for (const c of h1) far = max(far, c.d); + K.rMax = max(8, far * 1.12); + // Each loop's colour: the three most persistent loops, when they are + // long enough to be the rings' holes, get a colour each, the rest one more. + const ranked = [...h1].sort((p, q) => q.d - q.b - (p.d - p.b)), top = ranked.length ? ranked[0].d - ranked[0].b : 0; + ranked.forEach((c, i) => (c.col = i < 3 && c.d - c.b > 0.3 * top ? i : 3)); + // Nearest point and containing triangle for every LED of the cloud. + const P = CH * W; + K.dm = new Float32Array(P); + K.tri = new Int16Array(P).fill(-1); + for (let p = 0; p < P; p++) { + const x = (p % W) + 0.5, y = floor(p / W) + 0.5; + let d = 1e9; + for (let i = 0; i < n; i++) d = min(d, (X[i] - x) ** 2 + (Y[i] - y) ** 2); + K.dm[p] = sqrt(d); + } + for (const t of T) { + const [a, b, c] = t.v, x0 = max(0, floor(min(X[a], X[b], X[c]))), x1 = min(W - 1, floor(max(X[a], X[b], X[c]))); + const y0 = max(0, floor(min(Y[a], Y[b], Y[c]))), y1 = min(CH - 1, floor(max(Y[a], Y[b], Y[c]))); + const s = (X[b] - X[a]) * (Y[c] - Y[a]) - (Y[b] - Y[a]) * (X[c] - X[a]); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const px = x + 0.5, py = y + 0.5; + const w0 = ((X[b] - px) * (Y[c] - py) - (Y[b] - py) * (X[c] - px)) * s; + const w1 = ((X[c] - px) * (Y[a] - py) - (Y[c] - py) * (X[a] - px)) * s; + const w2 = ((X[a] - px) * (Y[b] - py) - (Y[a] - py) * (X[b] - px)) * s; + if (w0 >= 0 && w1 >= 0 && w2 >= 0) K.tri[y * W + x] = t.o; + } + } + // Which loops each triangle lies inside, the soonest to close first. + K.own = T.map(() => []); + [...h1].sort((p, q) => p.d - q.d).forEach((c) => c.reg.forEach((j) => K.own[j].push(c))); + // Barcode rows: H0 from the longest down, then H1 by birth. The rings' + // bars come first, as thick as the room allows up to 4 rows. The short + // bars then take one row each from what is left, longest first; those + // that do not fit are left out. A dark row stands between bars. + const room = H - 2 - (BT + n + 3), long = ranked.filter((c) => c.col < 3); + let th = 4; + while (th > 1 && long.length * (th + 1) > room + 1) th--; + let left = room + 1 - long.length * (th + 1); + const shown = [...long]; + for (const c of ranked) if (c.col === 3 && left >= 2) shown.push(c), (left -= 2); + shown.sort((p, q) => p.b - q.b); + K.rows = []; + [...h0].sort((p, q) => q - p).forEach((d, i) => K.rows.push({ y: BT + i, b: 0, d, col: -1, h: 1 })); + let y = BT + n + 3; + for (const c of shown) { + const h = c.col < 3 ? th : 1; + K.rows.push({ y, b: c.b, d: c.d, col: c.col, h }); + y += h + 1; + } +} + +function plot(buf, x, y, c, a) { + if (x < 0 || x >= W || y < 0 || y >= CH) return; + const o = 3 * (y * W + x); + buf[o] = max(buf[o], c[0] * a); + buf[o + 1] = max(buf[o + 1], c[1] * a); + buf[o + 2] = max(buf[o + 2], c[2] * a); +} +function line(buf, x0, y0, x1, y1, c, a) { + const n = max(1, Math.ceil(max(abs(x1 - x0), abs(y1 - y0)))); + for (let i = 0; i <= n; i++) plot(buf, floor(x0 + ((x1 - x0) * i) / n), floor(y0 + ((y1 - y0) * i) / n), c, a); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = cl(k[0], 0, 1); + S.speed = cl(k[1], 0.2, 3); + S.noise = cl(k[2], 0, 1); + S.rings = round(cl(k[3], 1, 3)); + const btn = !!(input.btnPressed && input.btnPressed[0]); + if (btn && !S.btn) { + fresh(S); + S.t = 0; + S.first = false; + } + S.btn = btn; + S.t += (d * S.speed) / 2; + if (S.t >= CYCLE) { + S.t -= CYCLE; + S.first = false; + fresh(S); + } + const key = `${S.seed} ${S.rings} ${S.noise.toFixed(2)}`; + if (key !== S.key) { + S.key = key; + build(S); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.K) return; + const K = S.K, buf = S.buf, pal = S.pal, X = S.X, Y = S.Y; + const r = K.rMax * cl(S.t / GROW, 0, 1); + const fade = cl((CYCLE - S.t) / FADE, 0, 1) * (S.first ? 1 : cl(S.t / 0.3, 0, 1)); + // Palette: 0..3 the loops, 4 merging (H0), 5 disks, 6 triangles, 7 + // points. The dim colours stay in the cool half of the wheel and the + // merging colour is pale, so neither goes brown; only the bright loop + // colours turn the whole way round. + const u = S.hue, sw = sin(PI * u) ** 2, cool = 0.72 - 0.38 * sw, pale = 0.52 + 0.3 * sw; + hsv(u + 0.07, 0.92, 1, pal, 0); + hsv(u + 0.9, 0.72, 1, pal, 3); + hsv(u + 0.15, 0.85, 1, pal, 6); + hsv(u + 0.98, 0.4, 0.85, pal, 9); + hsv(pale, 0.55, 0.95, pal, 12); + hsv(cool, 0.7, 1, pal, 15); + hsv(cool - 0.05, 0.55, 1, pal, 18); + hsv(pale, 0.12, 1, pal, 21); + const C = (i) => [pal[3 * i], pal[3 * i + 1], pal[3 * i + 2]]; + const cLoop = [C(0), C(1), C(2), C(3)], cH0 = C(4), cDisk = C(5), cTri = C(6), cPt = C(7); + buf.fill(0); + // The union of disks with its scalloped edge, the triangles in it, each + // open hole lined with its loop's colour, and the hole that just closed + // flashing. + const flashR = 0.06 * K.rMax; + for (let p = 0, P = CH * W; p < P; p++) { + const dm = K.dm[p], t = K.tri[p], o = 3 * p; + let a = 0; + if (dm <= r + 0.5) a = 0.17 * cl(r + 0.5 - dm, 0, 1) + (r - dm < 1.2 && r > 0.8 ? 0.2 : 0); + let rr = cDisk[0] * a, gg = cDisk[1] * a, bb = cDisk[2] * a; + if (t >= 0) { + if (K.T[t].R <= r) (rr += cTri[0] * 0.12), (gg += cTri[1] * 0.12), (bb += cTri[2] * 0.12); + for (const cy of K.own[t]) { + if (cy.b <= r && r < cy.d) { + const e = dm - r; + if (e > 0) { + const L = cLoop[cy.col], q = e < 1 ? 0.9 : e < 2 ? 0.38 : 0; + (rr = L[0] * q), (gg = L[1] * q), (bb = L[2] * q); + } + break; + } + if (r >= cy.d && r - cy.d < flashR) { + const L = cLoop[cy.col], q = 0.7 * (1 - (r - cy.d) / flashR); + (rr += L[0] * q), (gg += L[1] * q), (bb += L[2] * q); + break; + } + } + } + buf[o] = rr; + buf[o + 1] = gg; + buf[o + 2] = bb; + } + // Edges that have entered, bright as they arrive and dimmer after. + for (const e of K.E) { + if (e.v > r) break; + const a = 0.32 + 0.5 * Math.exp(-(r - e.v) / (0.05 * K.rMax)); + line(buf, X[e.a], Y[e.a], X[e.b], Y[e.b], cH0, a); + } + // The loops that are alive, each in its bar's colour. + for (const c of K.h1) + if (c.b <= r && r < c.d) for (const i of c.cyc) line(buf, X[K.E[i].a], Y[K.E[i].a], X[K.E[i].b], Y[K.E[i].b], cLoop[c.col], 1); + for (let i = 0; i < S.n; i++) { + const x = floor(X[i]), y = floor(Y[i]); + plot(buf, x, y, cPt, 1); + for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) plot(buf, x + dx, y + dy, cPt, 0.3); + } + // The ruler: a tick every 4 LEDs of radius, the sweep's place bright. + const span = W - 2, col = (v) => 1 + round((min(v, K.rMax) / K.rMax) * span), xs = col(r); + const set = (x, y, c, a) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = 3 * (y * W + x); + buf[o] = max(buf[o], c[0] * a); + buf[o + 1] = max(buf[o + 1], c[1] * a); + buf[o + 2] = max(buf[o + 2], c[2] * a); + }; + for (let v = 0; v <= K.rMax; v += 4) set(col(v), H - 1, cPt, 0.35); + for (let x = 0; x < W; x += 2) set(x, BT - 2, cH0, 0.2); + set(xs, H - 1, cPt, 1); + // Bars: solid up to the sweep, brightest while alive; dotted beyond it, + // where the sweep has yet to go. The sweep line runs down between them + // to the ruler on the bottom row. + for (let y = BT - 1; y < H - 1; y++) set(xs, y, cPt, 0.2); + for (const row of K.rows) { + const c = row.col < 0 ? cH0 : cLoop[row.col], x0 = col(row.b), x1 = max(x0, row.d === Infinity ? W - 1 : col(row.d) - 1); + const alive = row.b <= r && r < row.d, h = row.h; + for (let j = 0; j < h; j++) { + const y = row.y + j, rim = j === 0 || j === h - 1; + for (let x = x0; x <= x1; x++) { + if (x <= xs) set(x, y, c, alive ? 1 : row.col < 0 ? 0.62 : 0.8); + else if ((rim && (x - x0) % 2 === 0) || x === x1) set(x, y, c, 0.4); + } + if (alive) set(xs, y, cPt, 1); + } + } + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const o = 3 * p; + display.setPixel(x, y, round(cl(buf[o] * fade, 0, 1) * 255), round(cl(buf[o + 1] * fade, 0, 1) * 255), round(cl(buf[o + 2] * fade, 0, 1) * 255)); + } +} From 276b0e16c7b02d0b341eb5d8c0d96cea21c39355 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 23:48:35 +0000 Subject: [PATCH 7/8] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=205?= =?UTF-8?q?1=ED=8C=90=20=E2=80=94=20README=20=EC=97=B0=EA=B5=AC=20?= =?UTF-8?q?=EB=85=B8=ED=8A=B8?= 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_016styVvYJeAE1svDoUBeGKX --- incoming/pattern-study/README.md | 85 +++++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 6 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 30550350f..1c788d04a 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 팹턮 연구 2026-10 -새 팹턮 398개륌 쉰여섯 갈래로 나눠 만듀었얎요. 1판에서 엎섞 갈래로 23개륌 만듀고, 2판에서 1판에 없던 아홉 갈래로 16개륌, 3판에서 하나륌, 4판에서 새 갈래읞 직묌로 하나륌 더했얎요. 5판은 새 팹턮 없읎 1판의 Euclid Pulse 륌 닀시 ì§°ê³ , 6판은 섞욎 팚널의 128쀄을 위아래로 ë‹€ 쓰는 팹턮 하나륌 더했얎요. 7판은 또렷한 몚양읎 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 8판은 새 갈래읞 Ʞ계로 하나륌 더했얎요. 9판은 새 팹턮 없읎 1판의 Sprout 을 닀시 ì§°ì–Žìš”. 10판은 섞욎 팚널을 떚얎지는 랔록의 우묌로 쓰는 팹턮 하나륌 더했얎요. 11판은 여러 색읎 ꜀ 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 12판은 새 갈래읞 천첎로 하나륌 더했얎요. 13판은 새 팹턮 없읎 1판의 Glyph Rain 을 닀시 ì§°ì–Žìš”. 14판은 절찚가 바뀌Ʞ 전에 띄욎 바퀎띌서, 섞욎 팚널의 높읎륌 ë‹€ 떚얎지는 폭포 하나륌 더했얎요. 15판은 바뀐 절찚의 첫 판읎띌 ì—Ž 갈래에 새 팚턎을 하나씩, 몚두 ì—Ž 개륌 더했얎요. 도시와 음악 갈래는 귞때 새로 엎었얎요. 16판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 바닀와 곀충 갈래륌 새로 엎었얎요. 17판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 전자 회로와 장난감 갈래륌 새로 엎었얎요. 18판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 유늬와 ꎑ묌·결정, 놀읎공원 갈래륌 새로 엎었얎요. 19판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 부엌곌 버섯·균사 갈래륌 새로 엎었얎요. 20판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 새와 화학·싀험 갈래륌 새로 엎었얎요. 21판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 종읎와 욎동·겜Ʞ 갈래륌 새로 엎었얎요. 22판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 몞곌 비행 갈래륌 새로 엎었얎요. 23판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 묎대·공연곌 짐승 갈래륌 새로 엎었얎요. 24판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 공사·걎섀곌 도자Ʞ 갈래륌 새로 엎었얎요. 25판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 농사·밭곌 화산 갈래륌 새로 엎었얎요. 26판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 얌음·극지와 믞생묌 갈래륌 새로 엎었얎요. 27판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 철도와 필늄·영화 갈래륌 새로 엎었얎요. 28판은 죌제 표가 수학 쪜윌로 바뀐 첫 판읎띌 읎늄 있는 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했고, 수론 갈래륌 새로 엎었얎요. 29판은 죌제 표가 수학 여섯곌 화장품 팝업 넷윌로 나뉜 첫 판읎띌, 읎늄 있는 수학 대상 여섯곌 화장품 맀장 벜에 틀 질감 넷을 더했고, 위상·맀듭곌 화장품 팝업 갈래륌 새로 엎었얎요. 30판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 맀장의 LED 벜에 틀 질감 넷을 화장품 팝업 갈래에 더했얎요. 31판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 ì—Žì§€ 않았얎요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜의 질감 넷을 화장품 팝업 갈래에 더했얎요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 41판은 죌제 표가 수학·추상 닀섯곌 할로윈 닀섯윌로 바뀐 첫 판읎띌, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고 할로윈 갈래륌 새로 ì—Žì–Ž 10월 31음에 틀 팹턮 닀섯을 넣었얎요. 42판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 41판읎 연 할로윈 갈래에 더했얎요. 43판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 44판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 45판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 ì—Ž 개 아래읞 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 46판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 47판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 48판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 49판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. [index.html](index.html) 을 람띌우저로 엎멎 전부 한 장에서 돌아가요. ë§š 위 지도에서 점을 누륎멎 ê·ž 팚턎윌로 가고, 칎드의 슬띌읎더가 녾뾌 넷읎에요. 마음에 드는 것은 「윔드 복사」로 가젞가 Pattern Lab 의 CODE 칞에 붙여 넣윌멎 같은 귞늌읎 나와요. 페읎지는 파음 하나띌서 바깥에서 받아 였는 것읎 없얎요. +새 팹턮 408개륌 쉰여섯 갈래로 나눠 만듀었얎요. 1판에서 엎섞 갈래로 23개륌 만듀고, 2판에서 1판에 없던 아홉 갈래로 16개륌, 3판에서 하나륌, 4판에서 새 갈래읞 직묌로 하나륌 더했얎요. 5판은 새 팹턮 없읎 1판의 Euclid Pulse 륌 닀시 ì§°ê³ , 6판은 섞욎 팚널의 128쀄을 위아래로 ë‹€ 쓰는 팹턮 하나륌 더했얎요. 7판은 또렷한 몚양읎 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 8판은 새 갈래읞 Ʞ계로 하나륌 더했얎요. 9판은 새 팹턮 없읎 1판의 Sprout 을 닀시 ì§°ì–Žìš”. 10판은 섞욎 팚널을 떚얎지는 랔록의 우묌로 쓰는 팹턮 하나륌 더했얎요. 11판은 여러 색읎 ꜀ 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 12판은 새 갈래읞 천첎로 하나륌 더했얎요. 13판은 새 팹턮 없읎 1판의 Glyph Rain 을 닀시 ì§°ì–Žìš”. 14판은 절찚가 바뀌Ʞ 전에 띄욎 바퀎띌서, 섞욎 팚널의 높읎륌 ë‹€ 떚얎지는 폭포 하나륌 더했얎요. 15판은 바뀐 절찚의 첫 판읎띌 ì—Ž 갈래에 새 팚턎을 하나씩, 몚두 ì—Ž 개륌 더했얎요. 도시와 음악 갈래는 귞때 새로 엎었얎요. 16판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 바닀와 곀충 갈래륌 새로 엎었얎요. 17판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 전자 회로와 장난감 갈래륌 새로 엎었얎요. 18판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 유늬와 ꎑ묌·결정, 놀읎공원 갈래륌 새로 엎었얎요. 19판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 부엌곌 버섯·균사 갈래륌 새로 엎었얎요. 20판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 새와 화학·싀험 갈래륌 새로 엎었얎요. 21판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 종읎와 욎동·겜Ʞ 갈래륌 새로 엎었얎요. 22판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 몞곌 비행 갈래륌 새로 엎었얎요. 23판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 묎대·공연곌 짐승 갈래륌 새로 엎었얎요. 24판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 공사·걎섀곌 도자Ʞ 갈래륌 새로 엎었얎요. 25판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 농사·밭곌 화산 갈래륌 새로 엎었얎요. 26판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 얌음·극지와 믞생묌 갈래륌 새로 엎었얎요. 27판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 철도와 필늄·영화 갈래륌 새로 엎었얎요. 28판은 죌제 표가 수학 쪜윌로 바뀐 첫 판읎띌 읎늄 있는 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했고, 수론 갈래륌 새로 엎었얎요. 29판은 죌제 표가 수학 여섯곌 화장품 팝업 넷윌로 나뉜 첫 판읎띌, 읎늄 있는 수학 대상 여섯곌 화장품 맀장 벜에 틀 질감 넷을 더했고, 위상·맀듭곌 화장품 팝업 갈래륌 새로 엎었얎요. 30판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 맀장의 LED 벜에 틀 질감 넷을 화장품 팝업 갈래에 더했얎요. 31판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 ì—Žì§€ 않았얎요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜의 질감 넷을 화장품 팝업 갈래에 더했얎요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 41판은 죌제 표가 수학·추상 닀섯곌 할로윈 닀섯윌로 바뀐 첫 판읎띌, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고 할로윈 갈래륌 새로 ì—Žì–Ž 10월 31음에 틀 팹턮 닀섯을 넣었얎요. 42판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 41판읎 연 할로윈 갈래에 더했얎요. 43판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 44판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 45판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 ì—Ž 개 아래읞 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 46판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 47판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 48판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 49판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 51판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. [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판에서 섞로로 닀시 ì§°ì–Žìš”. @@ -645,6 +645,21 @@ | [Pit and Pendulum](patterns/pit-pendulum.js) | 할로윈 | 횃불 쌠 지하 감옥에서 컀닀란 쎈승달 칌날읎 벜에 귞늌자륌 휘두륎며 한 번 지날 때마닀 돌 칚대 쪜윌로 낎렀였고, 쥐듀읎 몚여듀닀 손 한 댘 위에서 횃불읎 잊아듀멎 쇠사슬읎 칌날을 ì–Žë‘  속윌로 끌얎올렀요. K3 은 한 번 지날 때 낎렀였는 깊읎, K4 는 횃불읎 닿는 거늬예요. | | [Party Lights](patterns/party-lights.js) | 할로윈 | 죌황곌 볎띌, 독한 쎈록, 댈처럌 흰 전구 쀄읎 얎두욎 현ꎀ 벜에 처지며 빛을 달늬고 반짝읎고 숚 쉬듯 쌜고 박자에 번쩍읎며, 바람읎 쀄을 귞넀처럌 흔듀멎 종읎 박쥐듀읎 싀에 맀달렀 빙Ꞁ 돌아요. K3 은 전구 쀄 수, K4 는 바람읎에요. | +### 51판 + +| 팹턮 | 갈래 | 한 쀄 | +| --- | --- | --- | +| [Hat Monotile](patterns/hat-monotile.js) | 볎로녞읎·셀 | 13각형 타음 하나읞 몚자가 메타타음 치환읎 감싞는 찚례대로 덩읎짞 팚널을 채우고, 거욞에 비친 몚자는 여덟에 하나ꌎ로 죌황에 서요. 되풀읎하지 않는 7,921장 조각 위로 찜읎 흘러가는 동안 색의 묌결읎 같은 몚자듀을 한 닚계, 두 닚계 큰 덩읎로 닀시 묶얎요. K3 은 몚자 크Ʞ, K4 는 같은 타음링의 변을 두 Ꞟ읎로 닀시 재얎 혜성에서 거북곌 몚자륌 지나 갈맀Ʞ표 몚양까지 바꟞는 Tile(a, b) 예요. | +| [Phantom Jam](patterns/phantom-jam.js) | 시뮬레읎션 | 팹널 가장자늬의 í‚€ 큰 고늬 도로륌 찚듀읎 앞찚와의 간격만 볎고 달늬멎 사고도 공사도 없읎 정첎가 생겚, 찚는 앞윌로 가는데 정첎는 뒀로 밀렀나요. 튾랙 안의 시공간 귞늌에서 찚의 선은 한쪜윌로, 빚간 정첎 쀄묎늬는 반대쪜윌로 êž°ìšžì–Žìš”. K3 은 ì°š 수(22~44), K4 는 욎전자가 간격에 맞춰 속도륌 고치는 믌감도 a 읎고, 닚추륌 누륎멎 출발선 가까욎 ì°šê°€ ꞉정거핎요. | +| [Sato-Tate](patterns/sato-tate.js) | 수론 | 타원곡선의 점을 mod p 로 섞얎 얻은 a_p/(2√p) 자늬에 소수가 빗방욞로 떚얎지고, 바닥의 막대가 희믞한 반원 (2/π)√(1−x²) 을 채워요. 한 겹읎 찚멎 눈ꞈ읎 반윌로 쀄고 닀음 겹읎 같은 반원을 닀시 쌓아 닀섯 겹의 돔읎 돌요. K3 은 곡선 셋 가욎데 하나륌, K4 는 막대 수륌 곚띌요. | +| [Calabi-Yau Quintic](patterns/calabi-yau.js) | 공간·원귌 | 5ì°š 페륎마 곡선 z1^5 + z2^5 = 1 을 핞슚의 25 조각윌로 지얎 공간에 섞우멎, 조각마닀 색읎 닀륞 죌늄진 공읎 천천히 돌며 닀섯 조각읎 한 점에 몚읎는 읎음맀륌 볎여요. K3 은 허수부륌 섞는 투영각 α, K4 는 25 조각을 떌얎 놓는 펌친 귞늌읎에요. | +| [Persistence Barcode](patterns/persistence-barcode.js) | 위상·맀듭 | 고늬륌 따띌 흩은 점마닀 원판읎 자띌 고늬륌 삌킀는 동안 아래에서는 ê·ž 성장의 바윔드가 훑는 선 뒀로 쌜지고, 고늬 구멍의 ꞎ 죌황 막대와 ê·ž 구멍을 두륞 고늬가 같은 색윌로 빛나닀 구멍읎 닫히멎 핚께 꺌젞요. K3 은 점읎 원에서 흩얎지는 정도, K4 는 점을 뜑는 원의 수(하나, 둘, 셋)예요. | +| [Bat Form](patterns/bat-form.js) | 할로윈 | 얎두욎 탑 방의 런족한 찜턱에 선 흡혈귀가 진홍곌 죌황의 핮 질 녘을 등지고 팔을 듀얎 망토륌 펌치고, 망토 닚읎 박쥐 날개의 묌결로 갈띌지며 작아젞 하늘로 고늬륌 귞늬고 날아갔닀 돌아와 닀시 사람읎 되얎 망토륌 여며요. K3 은 망토가 펌쳐지는 너비와 날개 크Ʞ, K4 는 멀늬 얞덕 위륌 도는 작은 박쥐 떌의 수예요. | +| [Tower Stair](patterns/tower-stair.js) | 할로윈 | 둥귌 돌탑을 잘띌 볎읞 나선 계닚을 두걎 쓎 사람읎 쎛불을 듀고 였륎멎, 불빛읎 굜은 벜을 비추며 귞의 컀닀란 귞늌자륌 벜에 드늬워요. ꌭ대Ʞ에서 바람읎 쎛불을 끄멎 ì–Žë‘  속 찜에 쎈록 눈 둘읎 뜚고, 쎛불은 계닚 아래에서 닀시 쌜젞요. K3 은 계닚읎 감Ʞ는 바퀮 수(3~8), K4 는 불빛읎 닿는 거늬읎고, 닚추륌 누륎멎 ê·ž 자늬에서 쎛불읎 꺌젞요. | +| [Jack-in-the-Box](patterns/jack-in-the-box.js) | 할로윈 | 죌황 마늄몚륌 두륞 볎띌 깡통 상자의 손잡읎가 저절로 돌닀 뚜껑읎 터지멎, 용수철 끝의 혾박 등 뚞늬가 팹널 위까지 솟았닀가 출렁읎며 흔듀늬고 천천히 상자로 돌아가 뚜껑읎 닫혀요. K3 은 용수철의 당당핹, K4 는 옆윌로 흔듀늬는 정도읎고, 닚추륌 누륎멎 바로 튀얎나와요. | +| [Zombie Shamble](patterns/zombie-shamble.js) | 할로윈 | 지평선 너빾 독한 쎈록 빛을 등진 좀비 떌가 안개 낀 시곚Ꞟ을 비틀대며 닀가였고, 가장 가까욎 것은 팚널을 채욎 ë’€ 안개 속윌로 부서젞요. K3 은 좀비 수, K4 는 안개의 높읎와 짙Ʞ읎고, 닚추륌 누륎멎 몚든 고개가 듀렀 쎈록 눈윌로 우늬륌 뎐요. | +| [Grimoire](patterns/grimoire.js) | 할로윈 | 탑 ë°© 독서대 위에 펌친 마법서의 책장을 볎읎지 않는 바람읎 넘Ʞ멎, 종읎가 휘얎 넘얎가며 쎛불을 받아 빛나요. 새로 펌친 멎마닀 독한 쎈록 읞장읎 한 획씩 귞렀지고, ê·ž Ꞁ자듀읎 하나씩 ë–šì–Žì ž 나와 쎈록 불티와 ì—°êž° ꌬ늬륌 끌며 팹널 위로 올띌가요. K3 은 바람, K4 는 읞장읎 타는 밝Ʞ읎고, 닚추륌 누륎멎 돌풍읎 책장을 몰아 넘겚요. | + ## 1판 갈래마닀 녾뾌 넷의 자늬는 갈래가 달띌도 거의 같아요. K1 은 색읎고 K2 는 빠륎Ʞ예요. K3 은 몚양을 바꟞고, K4 는 ê·ž 갈래만의 손잡읎예요. 귞래서 처음 볎는 팹턮도 K1 곌 K2 는 손읎 뚌저 알아요. @@ -2499,6 +2514,50 @@ E8 의 ê·Œ 240개륌 고셋의 4₂₁ 닀멎첎 ꌭ짓점윌로 ê·žë žì–Žìš”. 죌제 6 윌로 현ꎀ에 걎 할로윈 전구 쀄을 ê·žë žì–Žìš”. 묎서욎 장멎볎닀 10월 31음 ë°€ 파티에 틀 ꟞밈읎에요. 얎두욎 판자벜에 전구 쀄 셋에서 음곱읎 높읎륌 달늬핎 처지고, 쀄마닀 4×6 LED 전구 여섯읎 죌황곌 볎띌, 독한 쎈록, 댈처럌 흰 빛을 ë„€ 닚의 빛묎늬로 쌜요. 빛읎 쀄을 따띌 달늬는 ì°šë¡€, 반짝읎는 ì°šë¡€, 천천히 숚 쉬는 ì°šë¡€, 1분에 120번 박자로 번쩍읎는 찚례가 4쎈씩 돌아요. 바람읎 불멎 쀄읎 벜에서 귞넀처럌 듀렀 처진 바닥읎 옆윌로 믞끄러지고, 둘짞 쀄 아래 8 LED 싀에 맀닚 검은 종읎 박쥐듀읎 빙Ꞁ 돌며 퍌덕여요. 첫 판은 채움 0.231, 색 0.115 였얎요. 전구가 작았고, 반짝읎는 찚례는 거의 얎두웠고, 박쥐는 벜 앞에서 볎읎지 않았얎요. 전구륌 4×6 타원윌로 킀우고 빛묎늬륌 13 LED 로 넓히고, 박쥐륌 13×7 에서 9×5 로 쀄여 전구 아래 빛묎늬 속윌로 낎늬자 채움 0.535, 색 0.182 가 됐얎요. 검은 박쥐는 쌜진 전구 곁에서만 읜혀서, 빛읎 쀄을 따띌 달늬는 찚례에 가장 잘 볎여요. 박자 번쩍임은 0.1쎈 만에 꺌지멎 프레임 대부분읎 얎두워서, 번쩍음 때마닀 0.1쎈(ë„·ì§ž 박자는 0.22쎈) 뚞묞 ë’€ 꺌젞요. 얎둑한 죌황은 짙은 빚강윌로, 얎둑한 흰빛은 띌벀더 회색윌로 칠핎서 얎느 것도 갈색윌로 볎읎지 않아요. 첫 바람은 0.1쎈에 불얎 Gust 가 0.02 였고, 1.7쎈로 믞뀄 녞람륌 재는 찜에 듀게 하자 0.09 가 됐얎요. 녾뾌 값의 제곱귌을 쓰고 흔듀늌의 끝을 부드럜게 막아 0.069 에 ì„°ì–Žìš”. 한 프레임은 빛묎늬 반지늄을 믞늬 셈핎 4.7 ms 에서 1.2~1.8 ms 가 됐얎요. +## 51판 + +51판은 죌제 5 「수학 · 추상」 닀섯곌 죌제 6 「할로윈」 닀섯읎에요. 같읎 돈 50판(#261)의 PR 볞묞에서 ì—Ž 개와 지도 칞을 읜고, 2찚원 난류의 역캐슀쌀읎드와 확산 전선, 까마귀 떌의 핎곚곌 였래된 우묌 같은 것을 플핎서 곚랐얎요. 앞 바퀮 PR 듀에 따륌 Ꞁ은 없었얎요. 수학은 Hat Monotile, Phantom Jam, Sato-Tate, Calabi-Yau Quintic, Persistence Barcode 읎고 볎로녞읎·셀, 시뮬레읎션, 수론, 공간·원귌, 위상·맀듭에 하나씩 듀얎갔얎요. 49판까지 넣얎 섞멎 추상 갈래 가욎데 녞읎슈·흐늄읎 아홉윌로 가장 적었고, 볎로녞읎·셀곌 데읎터 귞늌, 입자, 늬듬·펄슀, 시뮬레읎션읎 엎읎었얎요. 50판읎 녞읎슈·흐늄곌 프랙탈, 입자, 데읎터 귞늌, 늬듬·펄슀에 하나씩 넣얎서, 읎 판은 낚은 갈래 가욎데 적은 닀섯을 곚랐얎요. 몚자 타음은 31판 뒀에 적혀 있던 감읎고, 넷은 목록 밖에서 새로 찟았얎요. 닀섯 ë‹€ 「흔한 것」에도 「낯선 수학 예」에도 없얎요. 할로윈은 Bat Form, Tower Stair, Jack-in-the-Box, Zombie Shamble, Grimoire 예요. 박쥐로 쀄얎드는 흡혈귀와 쎛불 듀고 나선 계닚을 였륎는 사람은 49판 뒀에 적혀 있던 감읎고, 셋은 새로 찟았얎요. 처음 떠올늰 싀험싀의 알곱의 사닀늬와 귀신 듀늰 파읎프 였륎간은 6판의 Jacob's Ladder 와 24판의 Pipe Organ 읎 읎믞 귞렀서, 깜짝 상자와 마법서로 바꿚얎요. 죌제 1 은 49판까지의 398개에도 빈 칞곌 하나뿐읞 칞읎 없얎서, 둘뿐읞 칞 엎음곱 가욎데 50판읎 겚눈 움직임 × 복잡도 4,4 와 쎘쎘핚 × 몚양 바뀜 3,4 륌 빌고 쎘쎘핚 × 색 4,4 와 가장자늬 × 쎘쎘핚 1,2 륌 겚눎얎요. Hat Monotile 읎 Aztec Diamond 와 Millefiori 뿐읎던 쎘쎘핚 × 색 4,4 에, Zombie Shamble 읎 Graveyard Fog 와 Sponge Blend 뿐읎던 가장자늬 × 쎘쎘핚 1,2 에 ì„°ì–Žìš”. 죌제 4 는 Sato-Tate 와 Tower Stair 가 맡았고, Phantom Jam 도 튞랙곌 시공간 귞늌윌로 128쀄을 ë‹€ 썚요. ì—Ž 개는 얎느 축의 끝도 넘지 않았고, 복잡도는 0.158 에서 2.005, 움직임은 0.114 에서 2.430 읎에요. ì—Ž 개는 `shelf.json` 의 얎느 묶음에도 넣지 않아서 페읎지 ë§š 위 「아직 안 추늰 것」에 서요. 닀섯 음Ꟍ읎 수학 하나와 할로윈 하나씩 ì§°ê³ , 볞첎가 몚자 타음의 치환을 뚌저 셈핎 넘Ʞ고 ì—Ž 개의 화멎을 ê·žë € 볎고 합쳀얎요. + +### Hat Monotile + +죌제 5 와 1 로 몚자 타음을 섞웠얎요. 데읎비드 슀믞슀, 조지프 새뮀얌 마읎얎슀, 크레읎귞 캐플런, 하임 굿뚌-슀튞띌우슀가 2023년에 찟은 13각형윌로, 혌자서 평멎을 덮지만 결윔 되풀읎하지 않아요. 캐플런의 hatviz 가 쓰는 H/T/P/F 메타타음 치환을 팹턮 안에 닀시 짜서 ë„€ 번 돌늬멎 몚자 7,921장읎 나였고, ê·ž 가욎데 거욞에 비친 H1 1,009장을 죌황윌로 칠했얎요. ꌭ짓점마닀 a 변곌 b 변의 합을 따로 듀얎 올늬멎 a 변은 늘 a 변곌만 맞닿아서 ꌭ짓점 49,246개의 값읎 얎느 Ꞟ로 가도 같았고, 귞래서 K4 가 같은 타음링을 혜성 Tile(1, 0)에서 갈맀Ʞ표 Tile(0, 1)까지 닀시 재요. 0, 30, 45, 60, 75, 90도 여섯 몚양윌로 조각 전첎륌 닚위당 3 LED 로 찍얎 볎니 겜계가 LED 쀑심에 딱 걞늰 몚자의 10개 말고는 겹친 LED 가 없었고, 가욎데 반지늄 68 닚위 안에 빈 곳도 없었얎요. 처음에는 몚자 너비 6 LED 에서 detail 0.371, colour 0.526 읎었는데, ê·ž 크Ʞ에서는 몚자가 색종읎 부슀러Ʞ로만 볎였얎요. 크Ʞ륌 9 로 올늬고, 얎두욎 죌황읎 갈색윌로 볎여서 회전마닀 닀륎던 여섯 음영을 밝Ʞ에서 채도 쪜윌로 옮게더니 detail 0.366, colour 0.629 로 빈 칞 detail × colour 4,4 에 ì„°ì–Žìš”. 1·2닚계 덩읎로 색을 묶을 때는 덩읎 안의 읎음맀에 색을 3분의 1 낚겚서 덩읎가 검은 선에 둘며 한 조각윌로 읜혀요. 읎 팚널에서 새로 볎읎는 것은 ê±°ìšž 몚자가 얌마나 드묞지와, 같은 몚자듀읎 묌결 하나에 몇 ê²¹ 큰 덩읎로 묶읎는 몚습읎에요. + +### Phantom Jam + +죌제 5 와 4 로 반도 마사윔와 동료듀(1995)의 최적 속도 몚형읎 만드는 유령 정첎륌 섞웠얎요. 찚는 앞찚와의 간격 h 만 볎고 dv/dt = a (V(h) − v) 로 속도륌 고치는데, 고륞 흐늄은 a 가 2V′(h) 볎닀 작윌멎 저절로 묎너젞요. 슀Ʞ알마 유킀의 팀(2008)은 230 m 원형 튞랙에 ì°š 22대륌 돌렀 읎 정첎륌 싀제로 볎였얎요. 팚턎은 직선읎 100 LED 읞 겜Ʞ장 튞랙을 팹널 가장자늬에 두고, 안쪜 52 × 116 칞에 고늬륌 펌친 시공간 귞늌을 귞렀요. V(h) = tanh(h − 1.87) + tanh(1.87) 에 고늬 Ꞟ읎가 39 띌서 22대의 간격 1.77 에서 묞턱읎 a = 1.98 읎고, K4 륌 1.5 에 두멎 22~29대만 불안정핎요. 첫 판은 찚의 선을 2~3 LED 굵Ʞ로 ê·žì–Ž 귞늌 전첎가 쎘쎘한 빗ꞈ읎었고, 빗ꞈ읎 거꟞로 Ʞ욎 것처럌 읜혔얎요(움직임 2.59, 채움 0.58). 선을 1 LED 로 가늘게 바꟞고 자유 흐늄의 선을 얎둡게 했더니 빚간 정첎 쀄묎늬가 떠올랐얎요. 흰 ì°š 한 대륌 두얎 ê·ž 선읎 정첎에서 섰닀가 닀시 달늬는 것을 따띌가게 했고, 14대로 엎멎 가는 쀄묎늬가 하나뿐읎띌 K3 범위륌 22~44 로 옮겚 슀Ʞ알마의 22대에서 엎늬게 했얎요. 몚형읎 첫 프레임에 11.6쎈의 지난 흐늄을 한꺌번에 돌렀서, 아래의 고륞 빗ꞈ읎 위로 갈수록 휘얎 정첎가 되는 것읎 처음부터 한 장에 볎여요. + +### Sato-Tate + +죌제 5 와 4 로 사토-테읎튞 법칙을 섞웠얎요. 사토 믞킀였와 졎 테읎튞가 1963년 묎렵 낎닀볞 법칙읎고, 복소곱셈읎 없는 곡선은 큎로저, 핎늬슀, 셰퍌드배런, 테음러가 2008~2011년에 슝명했얎요. 팚턎은 37a 곡선 y² + y = x³ − x 의 a_p 륌 소수마닀 제곱 표와 3ì°š 찚분윌로 O(p) 에 섞얎서, x_p = a_p/(2√p) 의 엎에 빗방욞을 떚얎뜚렀요. 처음 512개가 2.6쎈에 반원을 채우멎 눈ꞈ읎 반윌로 쀄얎요. 닀음 겹은 앞의 겹을 ë‹€ 합친 만큌의 소수로 같은 반원을 닀시 쌓고, 42쎈에 p = 84,053 까지 닀섯 겹의 묎지개 돔읎 서요. 팚턎의 a_p 는 곡선마닀 p < 400 의 좋은 소수 76~77개에서 점을 하나하나 섌 값곌 몚두 같았고, 8,192개 소수 얎디에서도 |x_p| 가 1 에 닿지 않았얎요. 37a 에서 x² 곌 x⁎ 의 평균은 0.2488 곌 0.1236 윌로 반원의 1/4 곌 1/8 에 맞았얎요. 복소곱셈 곡선 y² = x³ − x 와 y² = x³ + 1 은 p ≡ 3 (mod 4) 4,108개와 p ≡ 2 (mod 3) 4,114개가 몚두 a_p = 0 읎었고, x⁎ 평균 0.1865 와 0.1855 가 3/16 에 가까웠얎요. 첫 판은 첫 겹읎 256개띌 3쎈의 21칞 막대가 8~25개로 듀쭉날쭉핎서, 첫 겹을 512개로 늘늬고 1.9쎈에 ë‹€ 떚얎뜚렞얎요. 첫 겹읎 장믞색윌로 엎렀서 ê²¹ 색의 찚례륌 뒀집얎 청록에서 엎늬게 했고, 빗방욞을 빠륎게 핮 첫 착지륌 1.03쎈에서 0.93쎈로 당게얎요. + +### Calabi-Yau Quintic + +죌제 5 로 칌띌비-알우 닀양첎의 한 조각을 섞웠얎요. 에우제니였 칌띌비(1954)가 묻고 싱퉁 알우(1977)가 슝명한 공간읎고, 앀드룚 핞슚(1994)은 5ì°š 페륎마 곡멎을 C² 의 복소 곡선 z1^5 + z2^5 = 1 로 잘띌 25 조각윌로 ê·žë žì–Žìš”. 조각 (k1, k2) 는 z1 = e^(2πik1/5) cosh(z)^(2/5), z2 = e^(2πik2/5) (−i sinh z)^(2/5) 읎고, 팚턎은 읎것을 (Re z1, Re z2, cos α Im z1 + sin α Im z2) 로 섞워 깊읎 버퍌에 삌각형 14,400개로 귞렀요. 팹턮 자신의 핚수로 격자 8,125점에서 |z1^5 + z2^5 − 1| 은 6.6e-15, 쎘쎘한 격자 2,015,025점에서 1.2e-14 였고, y = 0 곌 π/2 가장자늬의 10,050점은 몚두 닀륞 조각 위에 1.5e-15 안윌로 놓여 읎음맀 50개륌 읎뀘얎요. 처음 값에서 움직임 0.215, 쎘쎘핚 0.044, 한 프레임 2.1 ms 읎고 녾뾌는 0.11, 0.11, 0.10, 0.19 예요. 곡멎은 섞 축의 분산읎 0.458 로 같아 섞욞 ꞎ 쪜읎 없얎서, 팹널 가욎데 64×75 쯀에 서고 K4 의 펌친 귞늌은 위아래로 1.8ë°° 벌렀 128쀄을 ë‹€ 썚요. 조각마닀 맀개변수 격자선을 ê·žì–Ž 뎀더니 격자가 몰늰 자늬가 잔 빗ꞈ윌로 읜혀서 뺐얎요. 귞늘진 녾란 조각읎 올늬람로 볎여서 귞늘을 검정 대신 낚볎띌 쪜윌로 떚얎뜚렞얎요. 읎 팚널에서는 닀섯 조각읎 한 점윌로 몚읎는 부채ꌎ곌 조각읎 서로 뚫고 지나가는 읎음맀가 얎두욎 1 LED 선윌로 또렷하게 볎여요. + +### Persistence Barcode + +죌제 5 로 지속 혞몰로지의 바윔드륌 섞웠얎요. 허버튞 에덞슀람룚너와 데읎비드 레처, 아프띌 조몚로디얞읎 2002년에 낾 지속 혞몰로지읎고, 바윔드 귞늌은 조몚로디얞곌 군나륎 칌손(2005)읎에요. 위쪜 64×64 에는 고늬에서 흩얎 뜑은 점 36~40개마닀 원판읎 자띌고, 아래에는 ê·ž 성장의 바윔드륌 Z/2 위 겜계 행렬 축앜윌로 정확히 셈핎 귞렀요. Vietoris-Rips 대신 알파 복합첎륌 썚서, 반지늄 r 에서 복합첎가 귞렀진 원판 합집합곌 같은 몚양읎 되게 했얎요. 귞래서 구멍의 죌황 테가 쀄얎듀닀 사띌지는 순간읎 막대 끝곌 같아요. 손윌로 셀 수 있는 겜우부터 뎀얎요. 변 20 읞 정사각형 ë„€ 점은 H0 셋읎 10 에서 죜고 H1 하나가 10 에서 14.1423 까지 삎아서, 변의 반에서 태얎나 대각선의 반에서 죜었얎요. 팚턎읎 뜑은 표볞 200개의 432개 반지늄에서는 였음러 지표와 성분 수, 6ë°° 격자로 섌 합집합의 구멍 수가 ë‹€ 바윔드와 같았고, 닫히지 않은 H1 은 0 읎었얎요. 고늬 하나에 Noise 0 읎멎 ꞎ 막대가 눈ꞈ의 0.76 을 찚지하고 H1 막대가 평균 1.8개였얎요. Noise 1 읎멎 0.44 로 쀄고, 막대가 28.7개로 늘얎 둘짞 막대가 첫짞의 46% 까지 자랐얎요. 첫 판은 엎늰 구멍을 얎둑한 죌황윌로 채웠는데 팚널에서 갈색읎띌, 구멍 가장자늬 두 LED 만 밝히고 가욎데는 검게 뒀얎요. 고늬 하나음 때 바윔드 아래 3분의 1 읎 비얎 있얎서, 점을 40개로 늘늬고 고늬 막대륌 ë„€ 쀄 굵Ʞ로 하고 r 의 눈ꞈ을 ë§š 아래 쀄로 낎렞얎요. 색 녞람가 반쯀읎멎 합쳐지는 막대가 올늬람가 돌서, ê·ž 색을 옅은 청록에서 옅은 볎띌 사읎로만 였가게 했얎요. + +### Bat Form + +죌제 6 윌로 박쥐로 변하는 흡혈귀륌 ê·žë žì–Žìš”. 49판 뒀에 적혀 있던 감읎에요. 볎늄달 대신, 얎두욎 탑 ë°© 안에서 런족한 ì°œ 너빾 핮 질 녘을 볎게 핎서 검은 싀룚엣읎 가장 밝은 하늘 앞에 서요. Werewolf 처럌 댈대 하나륌 닫힌 망토, 펌친 망토, 박쥐의 섞 자섞 사읎로 옮게얎요. 망토는 얎깚에서 팔꿈치와 손목을 지나 닚의 ë„€ 점곌 부채ꌎ 묌결로 읎은 닀각형읎에요. ê·ž 닚의 점듀읎 귞대로 날개 뒷전읎 돌서 망토 닚읎 날개의 묌결로 갈띌지는 게 볎여요. 거늬장을 섞지 않고 닀각형을 ë„€ 하위 행윌로 칠핎서 2 ms 얞저늬예요. 날아간 박쥐는 ì°œ 밖읎띌 벜읎 가늬고, 고늬 끝에서 0.4 배까지 작아졌닀 돌아와요. 첫 판은 얞덕읎 90쀄부터 찚서 닀늬와 망토 닚읎 묻혔고, 박쥐는 날개가 뭉툭핮 나방처럌 볎였얎요. 얞덕을 얇게 깎고 날개륌 êžžê³  런족하게 바꿚얎요. 팔꿈치와 손목을 곧게 옮Ʞ니 팔읎 짧아지며 망토가 상자처럌 펎젞서, 팔을 각도로 돌렀 올늬게 했얎요. Cape 는 처음에 0.0115 였는데, 망토륌 2~3쎈 ì°œ 안에 펌쳐 두고 범위륌 -1..1 로 넓혀 0.04 가 됐얎요. Flock 은 펌친 망토 뒀에 가렀 0.02 였는데, 박쥐륌 위쪜 하늘의 아치 폭 안에 띄우고 120마늬까지 늘렀 0.04 가 됐얎요. ë–Œ 박쥐가 뚞늬 옆을 지나멎 검은 몞에 붙얎 뚞늬가 둘로 볎여서, 뚌 박쥐는 하늘빛윌로 조ꞈ 흐늬게 칠했얎요. + +### Tower Stair + +죌제 6 곌 4 로 49판읎 낚ꞎ 감읞, 쎛불 듀고 나선 계닚을 였륎는 탑을 섞웠얎요. 둥귌 돌탑의 앞 절반을 잘띌 낎고 조ꞈ 낎렀닀볞 비슀듬한 투영(화멎 y = 115 − Y + 0.4 z)윌로 귞렀서, 계닚은 팹널 높읎륌 감는 윔음읎 되고 앞의 디딀판은 밝게, 뒀의 것은 얎둡게 볎여요. 탑을 지을 때 LED 마닀 시선을 0.25 씩 밟아 처음 닿는 멎을 한 번 ì°Ÿì•„ 두고, 프레임마닀 쎛불 빛만 닀시 셈핎요. 계닚은 속읎 ì°¬ 나선읎띌 빛은 쎛불곌 같은 잵의 복도 안에서만 벜에 닿고, 귞늌자는 쎛불까지의 선분읎 몞의 캡슐 닀섯에 닿는지 3찚원윌로 셈핎요. 첫 판은 거의 옆에서 볞 투영(K = 0.22)곌 고륞 달빛읎띌 계닚읎 벜돌 묎늬에 묻혀 나선윌로 읜히지 않았고, 녾뾌 반응도 0.02~0.05 였얎요. 낎렀닀볎는 각을 킀우고 디딀판 윗멎의 달빛을 벜의 두 ë°° 넘게 올늬자 계닚읎 윔음로 읜혔얎요. 쎛불을 몞 앞에 듀멎 귞늌자가 등 ë’€ 벜에서 ë„€ 배로 컀젞 복도 천장에 잘늰 검은 덩얎늬가 되얎서, 쎛불을 얎깚 높읎의 안쪜 앞윌로 옮겚 두걎 끝읎 볎읎는 2.5배쯀의 귞늌자로 쀄였얎요. 읎 팚널에서 새로 볞 것은, 였륎는 사람읎 탑 뒀쪜을 돌 때 반 바퀮 위의 앞 디딀판에 몞읎 가렀도 쎛불의 묎늬와 벜의 귞늌자로 ì–Žë”” 있는지 읜힌닀는 거예요. + +### Jack-in-the-Box + +죌제 6 윌로 깜짝 상자륌 ê·žë žì–Žìš”. 죌황 마늄몚륌 두륞 볎띌 깡통 상자륌 비슀듬히 낎렀닀뎐서 윗멎곌 옆멎읎 볎읎고, 옆멎의 놋쇠 손잡읎가 저절로 돌며 한 바퀎마닀 상자가 1 LED 듀썩여요. 0.85쎈에 뚜껑읎 터지멎 쎈록곌 볎띌 깃을 당 혾박 등 뚞늬가 솟아요. 처음 값에서는 66 LED 까지 치솟았닀가 0.9 Hz 로 깊게 출렁읎고, 7쎈마닀 닀시 튀얎나와요. 용수철은 L'' = −ω²(L − L₀) − 2ζωL' − c|L'|L' 륌 프레임마닀 여덟 걞음윌로 풀얎요. 빠륌 때만 섞게 묎는 제곱 감쇠 덕에 당당한 용수철도 첫 솟음읎 팚널을 넘지 않고 4.5 Hz 떚늌윌로 잊아듀얎요. 첫 판은 닫힌 뚜껑 위로 혾박 ꌭ지가 삐젞나와서 상자 속 자늬륌 L = −16 에서 −27 로 낎렞얎요. 조밀한 마늄몚는 죌황 십자 점윌로 읜혀서 10 LED 마늄몚륌 셋씩 두 쀄로 킀웠얎요. 녾란 얌룩읎던 얌굎은 눈곌 입을 닀시 귞늬고 구멍 위에 얎두욎 칌자국 쀄을 넣얎서알 웃는 혞박읎 됐얎요. K1 은 깡통 색만 돌멮 때 0.04 여서 뚞늬 속 쎛불 색도 핚께 돌렀 0.06 읎 됐얎요. Wobble 은 흔듀늌을 팝 시각에서 시작하는 2.4쎈 진자로 바꿔 0.03 에서 0.05 가 됐얎요. + +### Zombie Shamble + +죌제 6 윌로 안개 낀 시곚Ꞟ을 따띌 닀가였는 좀비 떌륌 ê·žë žê³ , 죌제 1 의 빈 칞 가장자늬 × 쎘쎘핚 1,2 륌 겚눎얎요. 지평선 너빾 독한 쎈록 빛을 등진 검은 몞에 빛 테가 둘늬고, 멀늬 안개 속에서 작게 나와 가까욎 것은 팚널을 채욎 ë’€ 안개 속윌로 부서젞요. 좀비는 거늬에 비례하는 빠륎Ʞ로 닀가와서 팹널 위에서 늘 같은 비윚로 컀지고, 5 m 안에서는 우늬 쪜윌로 꺟얎 듀얎 제 자늬륌 지쌜요. 가장자늬 0.032, 쎘쎘핚 0.120 윌로 Graveyard Fog 와 Sponge Blend 뿐읎던 칞에 섰고, 녾뾌는 0.15, 0.04, 0.06, 0.09 예요. 쎘쎘핚은 1 LED 첎컀 알갱읎로 채우고 반 걞음을 (39 − 읎웃곌의 가장 큰 찚읎)/2 로 묶얎서, 알갱읎륌 섞게 넣얎도 40 닚을 넘는 계닚읎 생Ʞ지 않아요. 첫 판은 눈높읎 1.6 m 에 좀비륌 거늬로 고륎게 흩얎서 가장 가까욎 것읎 47 LED 였고 아래 40% 가 비었얎요. 눈높읎륌 1.45 m 로 낮추고 처음 좀비륌 1.95 m 에 섞우자 팚널읎 찌얎요. 옆윌로 벌늰 팔은 멀늬서 탁자로, 가까읎서 허수아비로 읜혀서, 몞을 3찚원윌로 짓고 반쯀 돌렀 뻗은 팔 둘읎 한쪜윌로 나였게 했얎요. 알갱읎가 ±19 닚읎띌 25 당 아래의 바퀮 자국은 묻히고 굵은 몚양만 읜혀서, Ꞟ은 빛을 비추는 젖은 쐐Ʞ로 바꿚얎요. + +### Grimoire + +죌제 6 윌로 슀슀로 넘얎가는 마법서륌 섞웠얎요. 책은 제 좌표에서 종읎 닚멎을 굜힌 곡선윌로 짓고, 독서대가 앞윌로 Ʞ욞읞 채 위에서 조ꞈ 였륞쪜윌로 낎렀닀볞 원귌윌로 팚널에 찍얎요. 넘얎가는 장은 닚멎의 각읎 0 에서 π 로 가는 동안 자유닚읎 듀늎 때는 앞서고 낎렀앉을 때는 뒀처젞서 종읎처럌 휘고, 볎읎는 멎의 법선윌로 쎛불 빛곌 반짝임을 받아요. 처음 판은 바로 앞에서 뎐서 반쯀 넘얎간 장읎 선 하나로 사띌졌는데, 볎는 방향을 였륞쪜윌로 0.3 만큌 비끌자 선 장읎 좁은 평행사변형윌로 낚았얎요. 읞장도 처음에는 종읎 굎곡을 따띌 귞렀서 가욎데 홈에서 꺟읞 하튞처럌 읜혔고, 종읎 위 3 닚위에 뜬 평평한 원윌로 바꿚얎요. 붉은 벜돌처럌 볎읎던 벜은 Ꞟ읎가 닀륞 볎랏빛 돌로 바꟞고 쎛불 빛읎 닿는 거늬륌 쀄였얎요. 돌풍에 넘얎가는 장 위로 읞장읎 겹쳐 귞렀지던 것은 귞늬는 찚례륌 바꿔 고쳀얎요. 녾뾌 반응은 처음에 0.02~0.03 읎었는데, 바람읎 불꜃을 낮추고 Glow 가 ë°© 전첎륌 쎈록윌로 비추게 하자 K1 0.042, K2 0.051, K3 0.043, K4 0.045 가 됐얎요. + ## 지도 지도의 자늬는 손윌로 맀ꞎ 점수가 아니띌 잰 값읎에요. 팚턎마닀 처음 녾뾌 값윌로 Lab 엔진에서 8쎈륌 돌늬고, 2쎈부터 6쎈 동안의 화멎을 재요. LED 하나의 밝Ʞ는 섞 채널 가욎데 가장 밝은 값윌로 뎐요. @@ -2541,7 +2600,7 @@ ESP32-S3 로 옮Ꞟ 때 걞늎 것도 있얎요. Reaction Diffusion 은 Node 에 ## 닀음 판 감 -10월 25음 `dev`(`f72355d`)의 328개에 43판부터 49판까지의 음흔 개륌 더한 398개에서 잰 값읎에요. 팹턮 바퀮는 죌제 안에서 묎엇을 할지 고륌 때 읎 절을 읜고, 한 것은 빌고 새로 볞 것을 더핎요. +10월 25음 `dev`(`f72355d`)의 328개에 43판부터 49판까지의 음흔 개와 51판의 ì—Ž 개륌 더한 408개에서 잰 값읎에요. 팹턮 바퀮는 죌제 안에서 묎엇을 할지 고륌 때 읎 절을 읜고, 한 것은 빌고 새로 볞 것을 더핎요. ### 지도의 빈 칞 @@ -2585,6 +2644,8 @@ ESP32-S3 로 옮Ꞟ 때 걞늎 것도 있얎요. Reaction Diffusion 은 Node 에 49판의 ì—Ž 개도 얎느 축의 끝을 넘지 않아서 칞 겜계는 30판 귞대로읎고, 398개에서도 지도 21장읎 ë‹€ `none` 읎에요. 죌제 1 은 48판까지의 388개에 낚은 둘뿐읞 칞 엎아홉 가욎데 쎘쎘핚 × 몚양 바뀜 2,4 와 쎘쎘핚 × 채움 4,1 을 겚눠서, 귞런 칞읎 398개에서 엎음곱읎에요. 쎘쎘핚 × 몚양 바뀜 2,4 는 읎웃 찚읎의 평균읎 0.082~0.189 읎멎서 2~8쎈에 랔록 평균읎 0.331 넘게 바뀌얎알 하는 칞읎에요. DÃŒrer's Unfolding 의 첫 판은 몚양 바뀜읎 0.409 로 읎믞 칞 안읎었지만, 닫힌 입첎가 찜의 대부분을 찚지핎서 쎘쎘핚읎 0.039 였얎요. 멎에 판화처럌 빗ꞈ을 치고, 빗ꞈ을 겜첩에 45도로 눕히고, 귞묌을 3.4쎈까지 펌친 채 두얎서 0.113 에 ì„°ì–Žìš”. 빗ꞈ은 밝Ʞ륌 덜얎 낮는 만큌 쎘쎘핚을 올렞고, 45도로 눕히자 덜얎 낾 밝Ʞ에 대한 쎘쎘핚읎 1.1 배에서 1.4 배가 됐얎요. 몚양 바뀜 0.365 는 두 입첎 사읎의 얎두욎 순간읎 정핎서, 펌친 귞묌의 평균 밝Ʞ와 같아요. Polytope Slices 와 같은 셈읎에요. 쎘쎘핚 × 채움 4,1 은 읎웃 찚읎의 평균읎 0.297 을 넘윌멎서 쌜진 LED 가 30.2% 아래읞 칞읎에요. 쌜진 LED 가 옆윌로 붙얎 있윌멎 쎘쎘핚읎 쌜진 몫을 넘Ʞ 얎렀워요. Curlicue 는 쌜진 LED 륌 ë‹€ 첎컀판의 한 색 위에 두얎서 얎느 두 LED 도 옆윌로 닿지 않아요. 같은 점을 귞냥 격자에 찍윌멎 쎘쎘핚읎 쌜진 몫의 0.6 배쯀읎었고, 첎컀판에서는 2.0 배쯀읎었얎요. 한쪜 합만 귞늬멎 쌜진 몫읎 0.12~0.15 에 뚞묌러서 대칭 합윌로 바꟞고, 새로 자란 반쪜을 대각선 두 LED 펜윌로 ê·žì–Ž 쎘쎘핚 0.335, 채움 0.168 읎 됐얎요. êžž 전첎륌 ê·ž 펜윌로 귞윌멎 0.360 읎띌 축 끝의 Rule Scroll(0.375)에 너묎 가까웠얎요. ê·ž 끝읎 0.42 넘게 밀늬멎 Curlicue 가 ì…‹ì§ž 엎로 넘얎가요. 낚은 엎음곱 가욎데 엎하나는 쎘쎘핚 축읎 든 칞읎고, 여섯은 복잡도가 2.173 을 넘얎알 하는 칞읎에요. +51판의 ì—Ž 개도 얎느 축의 끝을 넘지 않아서 칞 겜계는 30판 귞대로읎고, 408개에서도 지도 21장읎 ë‹€ `none` 읎에요. 죌제 1 은 49판까지의 398개에 낚은 둘뿐읞 칞 엎음곱 가욎데, 같읎 돈 50판읎 겚눈 움직임 × 복잡도 4,4 와 쎘쎘핚 × 몚양 바뀜 3,4 륌 빌고 쎘쎘핚 × 색 4,4 와 가장자늬 × 쎘쎘핚 1,2 륌 겚눠서, 귞런 칞읎 408개에서 엎닀섯읎에요. 쎘쎘핚 × 색 4,4 는 읎웃 찚읎의 평균읎 0.297 을 넘윌멎서 색읎 0.544 륌 넘얎알 하는 칞읎에요. Aztec Diamond 처럌 작고 평평한 조각을 얎두욎 읎음맀로 가륎고 섌 색 여럿을 깔멎 서요. Hat Monotile 의 첫 판은 몚자 너비 6 LED 에서 쎘쎘핚 0.371, 색 0.526 읎었는데, ê·ž 크Ʞ에서는 몚자가 색종읎 부슀러Ʞ로 볎여서 너비륌 9 로 킀웠얎요. 쎘쎘핚은 0.366 윌로 조ꞈ만 낎렀갔고, 회전마닀 밝Ʞ로 가륎던 음영을 채도로 가륎자 색읎 0.629 로 올띌 칞에 ì„°ì–Žìš”. 가장자늬 × 쎘쎘핚 1,2 는 40 닚을 넘는 읎웃 찚읎가 6 닚을 넘는 것의 21% 아래읎멎서 읎웃 찚읎의 평균읎 0.082~0.190 읞 칞읎에요. 부드러욎 귞늌에 쎘쎘핚을 죌는 Ꞟ은 Graveyard Fog 처럌 1 LED 첎컀 알갱읎였얎요. Zombie Shamble 은 알갱읎의 반 걞음을 (39 − 읎웃곌의 가장 큰 찚읎)/2 로 묶얎서 얎느 계닚도 40 닚을 넘지 않게 했고, 가장자늬 0.032, 쎘쎘핚 0.120 읎 됐얎요. 낚은 엎닀섯 가욎데 아홉은 쎘쎘핚 축읎 든 칞읎고, 여섯은 복잡도가 2.173 을 넘얎알 하는 칞읎에요. + 23판부터 28판까지는 `*` 가 붙은 칞읎 없었얎요. 22판까지 낚은 두 칞, 쎘쎘핚 × 몚양 바뀜 3,4 와 4,3 에 23판의 Bait Ball 곌 Sequins 가 ì„°ì–Žìš”. 하나 낚은 빈 칞읎던 복잡도 × 채움 4,3 에는 24판의 Waggle Dance 가 ì„°ì–Žìš”. 복잡도가 2.173 을 넘고 채움읎 0.558~0.815 읞 자늬예요. 복잡도 축 끝의 Contour Map(2.735)을 빌고 섞멎 찚는 칞읎띌 `*` 가 붙지 않았고, ê·ž 아래 복잡도 2.4~2.6 에 선 Woodpecker, Dripstone, Agate, Guilloché 는 채움읎 ë‹€ 0.93 을 넘었얎요. 읎제 지도 21장의 칞읎 ë‹€ 찚서 `--gaps` 는 몚든 쀄에 `none` 을 찍얎요. 죌제 1 은 축 끝의 팹턮 하나가 축을 늘렀 새 빈 칞읎 날 때까지 고륌 칞읎 없얎요. Waggle Dance 에서 얎렀욎 쪜은 채움읎었얎요. 벌집은 꺌진 LED 가 거의 없는 귞늌읎띌, 빈 ë°© 바닥곌 뮉한 ë°© 사읎 읎음맀륌 24 아래로 눌러 채움 0.732 륌 냈얎요. 복잡도는 쌜진 LED 의 섞 채널을 따로 흔드는 낟알로 쉜게 올띌서, 첫 판은 였히렀 2.766 윌로 축 끝을 넘었얎요. 25판도 Ʞ쀀 148개의 21장읎 ë‹€ `none` 읎띌 죌제 1 은 겚눌 칞읎 없었얎요. ê·ž 대신 팚턎읎 하나만 선 칞을 셌얎요. 148개에서 49칞읎었고, 낚은 자늬로 ì§  Penrose 가 ê·ž 가욎데 여섯 칞에 둘짞로 서서 158개에서는 43칞읎에요. Penrose 가 겚눈 칞은 쎘쎘핚 × 색 3,4 예요. 쎘쎘핚 0.195~0.299 에 색 0.509 위읞 자늬읎고, 4판의 Jacquard Loom 혌자 서 있었얎요. Penrose 는 쎘쎘핚 0.264, 색 0.583 윌로 ê±°êž° ì„°ì–Žìš”. 같은 값윌로 움직임 × 쎘쎘핚 4,3 곌 쎘쎘핚 × 채움 3,3 의 Moire Rings, 쎘쎘핚 × 복잡도 3,2 의 Peg Light, 복잡도 × 색 2,4 의 Gear Train, 채움 × 색 3,4 의 Marbling 곁에도 ì„°ì–Žìš”. 1 LED 얎두욎 선윌로 나눈 작은 타음에 선명한 색을 여럿 두멎 쎘쎘핚곌 색읎 핚께 높은 자늬에 닿아요. 26판도 Ʞ쀀 158개의 21장읎 ë‹€ `none` 읎었얎요. 팚턎읎 하나뿐읞 칞 43 가욎데 넷을 겚눎얎요. Dark Field 는 검은 바탕 위의 흩은 빛윌로 채움 0.263 에 쎘쎘핚 0.222, 복잡도 2.081 읎띌, Foxfire 혌자읎던 복잡도 × 채움 3,1 곌 Peg Light 혌자읎던 쎘쎘핚 × 채움 3,1 에 씚앗 여덟읎 ë‹€ 듀얎요. Origami 는 얎두욎 탁자 위 종읎 한 장읎띌 채움 0.264 읎고, 2쎈의 정사각형읎 8쎈에 좁은 학 Ʞ볞형읎 되얎 몚양 바뀜 0.255 로 Domino Run 혌자읎던 채움 × 몚양 바뀜 1,3 에 서요. Lichtenberg 는 Rabbit Ears 혌자읎던 복잡도 × 몚양 바뀜 4,3 에 씚앗 여덟 가욎데 음곱읎 듀얎요. ë„€ 칞 말고는 바뀐 칞읎 없얎서, 168개에서 팚턎읎 하나뿐읞 칞은 39칞읎에요. 27판도 Ʞ쀀 168개의 21장읎 ë‹€ `none` 읎었고, 39칞 가욎데 둘을 겚눎얎요. Mitosis 는 채움 0.263, 색 0.540 윌로 Neon Alley 곁의 채움 × 색 1,4 에 서요. 판의 4분의 1만 쌜는 귞늌에서 색 0.5 륌 넘Ʞ렀멎 쌜진 LED 가 거의 ë‹€ 순색 최대 밝Ʞ여알 핎서, 형ꎑ의 번짐은 셈에 듀지 않는 21/255 아래로 눌렀얎요. Lava Lake 는 복잡도 2.222, 채움 0.271 로 Moth Lamp 곁의 복잡도 × 채움 4,1 에 서요. 복잡도는 프레임 바읎튞 값의 엔튞로플륌 거의 귞대로 따띌서, 얎두욎 지각의 결을 채널마닀 0~24 전부로 넓히자 1.866 읎 2.010 읎 됐얎요. 붉은 용암은 쌜진 LED 하나에 24 륌 넘는 바읎튞가 빚강 하나뿐읎띌, 읎음맀륌 늘늬Ʞ볎닀 죌황 쪜윌로 데우는 펞읎 채움을 덜 쓰고 복잡도륌 벌었얎요. 색 축의 아래 끝은 Cloud Chamber 의 0.052, 몚양 바뀜 축의 아래 끝은 Climbing Wall 의 0.021 로 옮겚졌지만 혌자 서는 칞은 귞대로띌서, 178개에서 팚턎읎 하나뿐읞 칞은 37칞읎에요. 28판도 Ʞ쀀 178개의 21장읎 ë‹€ `none` 읎었고, 37칞 가욎데 둘을 겚눎얎요. Aztec Diamond 는 쎘쎘핚 0.350, 색 0.640 윌로 Millefiori 곁의 쎘쎘핚 × 색 4,4 에 서요. 쎘쎘핚은 가장 밝은 채널로 재서 ë„€ 색 도믞녞가 맞닿아도 턱읎 없었고, LED 에 묶은 첎컀판읎 ê·ž 턱을 쀬얎요. Stadium Billiard 는 움직임 2.362, 채움 0.186 윌로 Warp 곁의 움직임 × 채움 4,1 에 서요. ê³µ 읎백 개가 1쎈에 125 LED 륌 가멎서 짧은 자췚만 낚겚요. 겚누지 않은 닀섯 칞에도 둘짞가 듀얎서 188개에서 팚턎읎 하나뿐읞 칞은 30칞읎에요. 29판도 Ʞ쀀 188개의 21장읎 ë‹€ `none` 읎었고, 30칞 가욎데 둘을 겚눎얎요. 28판읎 몚듈러 형식의 등고선윌로 겚눠 볎띌던 가장자늬 × 움직임 1,4(Mandelbrot 혌자)는 J-Invariant 가 움직임 0.372 로 몚자랐고, Hopf Fibration 읎 가장자늬 0.207, 움직임 1.607 로 ì„°ì–Žìš”. 원 음흔 개가 저마닀 1쎈에 몇 LED 씩 쓞고 가서 움직임읎 높고, 원마닀 옅은 빛 번짐을 둬서 가장자늬가 낮아요. J-Invariant 는 복잡도 2.693, 색 0.400 윌로 Spectrogram 곁의 복잡도 × 색 4,3 에 서요. 같은 판의 Cream Swirl(0.028)곌 Serum Gel(0.079)읎 가장자늬 축의 아래 끝을 Fire 의 0.120 에서 낎렀서, 가장자늬 칞 겜계가 0.305, 0.560, 0.815 에서 0.232, 0.514, 0.796 윌로 옮겚졌얎요. 귞래서 198개에서는 가장자늬 × 쎘쎘핚 1,2 가 23판 뒀로 처음 `*` 가 붙은 빈 칞읎에요. 가장자늬 0.232 아래에 쎘쎘핚 0.091~0.195 읞 자늬띌, 번지는 바탕 위에 잔 결읎 고륎게 깔늰 귞늌읎 듀얎섀 수 있얎요. 같은 지도의 1,3 곌 1,4, 가장자늬 × 채움 1,1 도 비었지만 끝의 팹턮 하나륌 빌멎 찚서 `*` 가 없얎요. 198개에서 팚턎읎 하나뿐읞 칞은 24칞읎에요. 쎘쎘핚읎 0.299 륌 넘는 팚턎읎 20판에서 셋 늘었고, 셋 ë‹€ ê·ž 값을 닀륞 Ꞟ로 냈얎요. Agate 는 1 LED 띠륌 벜까지의 거늬로 귞얎서, 축에 나란한 띠는 읎웃 쌍의 절반에서만 찚읎가 나고 대각선 띠는 70% 쯀에서 나요. 방향읎 고륎게 섞읎멎 띠 너비 0.85 LED 얞저늬가 가장 높았얎요. Millefiori 는 쎘쎘핚을 가장 밝은 채널로 재서 윔발튞와 칎나늬아 녞랑읎 맞닿아도 턱읎 0 읎었얎요. 고늬마닀 밝은 유늬와 ê·ž 유늬륌 얎둡게 뜑은 유늬륌 번갈아 넣얎알 했얎요. Sunglint 는 반짝임을 묌결 띠마닀 첎컀판의 한쪜 칞에만 둬서 반짝임끌늬 닿지 않게 했얎요. 셋 ë‹€ 채움읎 0.85 륌 넘얎서, 쎘쎘핚 × 채움 4,4 는 꺌진 LED 없읎도 닿는 칞읎었얎요. 21판의 Sandpile 읎 음곱짞예요. 칞 하나가 LED 하나읎고 알 셋 옆의 알 둘읎 가장 흔한 읎웃 쌍읎띌, 밝Ʞ륌 0, 2, 1, 3 찚례로 올렀 더믞 안의 쎘쎘핚읎 0.40 얞저늬예요. 판의 75% 는 덮여알 0.3 을 넘얎요. 22판의 Pin Art 와 Night Flight 가 여덟짞와 아홉짞예요. Pin Art 는 핀곌 틈의 첎컀 결읎띌 읎웃 쌍읎 몚두 핀곌 틈의 쌍읎고, Night Flight 는 Pancakes 의 첎컀 결에 1 LED 가로등을 LED 의 12.7% 만큌 얹얎서 가장자늬 0.258 에 쎘쎘핚 0.328 읎에요. 쎘쎘핚곌 가장자늬는 가장 밝은 채널로 재서, 색을 섞는 자늬마닀 섞은 ë’€ 가장 밝은 채널을 닀시 맞춰알 했얎요. 땅곌 강묌을 섞은 자늬에서 가장 밝은 채널읎 0.68 까지 낎렀가 첎컀 결의 37 읎 41~60 윌로 넘얎갔고, 귞대로 둔 판은 가장자늬가 0.05 높았얎요. 23판의 Sequins 가 엎짞예요. 첎컀 ê²° 없읎, 가로 5 LED 와 섞로 3 LED 간격윌로 엇갈렀 당 슀팜Ꞁ의 빛점곌 귞늌자 쀄로 0.327 을 냈얎요. 쎘쎘핚은 묎늬의 몚양볎닀 턱의 크Ʞ륌 따띌가서, 슀팜Ꞁ 몞통을 0.8 에서 0.6 윌로 얎둡게 하자 은빛 멎읎 0.339 에서 0.311 로 낎렀갔얎요. 26판의 Lichtenberg 는 0.2 륌 넘ꞎ 판을 버렞얎요. 판 얎디서나 돋는 1 LED 잔가지로 0.248 을 냈더니 검은 직교 귞묌읎 나묎판을 덮얎 믞로로 볎였얎요. 잔가지륌 탄 쀄Ʞ에만 달아 빈 나묎륌 42% 낚Ʞ자 0.130 읎었얎요. 쎘쎘핚은 판 전첎 읎웃 쌍의 평균읎띌, 빈 나묎가 넓게 낹는 귞늌은 쀄Ʞ 둘레가 아묎늬 잘아도 평균읎 낮아요. 같은 판의 Dark Field 는 검은 바탕에 섞균 몇백곌 밝은 테두늬륌 판 곳곳에 흩얎 0.222 륌 냈얎요. @@ -2599,7 +2660,7 @@ ESP32-S3 로 옮Ꞟ 때 걞늎 것도 있얎요. Reaction Diffusion 은 Node 에 ### 아직 없는 갈래 -`families.json` 의 쉰여섯 갈래에 없는 것 가욎데 섞욎 팚널에 맞을 것을 적얎 둬요. 29판읎 연 위상·맀듭 갈래는 Hopf Fibration 곌 31판의 Boy's Surface, 33판의 Borromean Rings, 34판의 Lakes of Wada, 36판의 Heawood Torus, 38판의 Sperner's Lemma, 39판의 Borsuk-Ulam, 44판의 Ham Sandwich, 46판의 Reeb Graph, 48판의 Möbius Energy, 49판의 Square Peg 예요. 28판읎 엎렀닀 접은 갈래띌 28판의 Hairy Ball 은 녞읎슈·흐늄에 귞대로 있얎요. 섞잎 맀듭은 48판의 Möbius Energy 가 처음 섞웠얎요. 29판은 화장품 팝업 갈래도 엎었얎요. 죌제 6 의 몫읎띌 죌제 2 로 섞지 않았고, Cream Swirl, Serum Gel, Glitter Fall, Loose Powder 넷읎 ë‹€ 듀얎 있얎요. 30판의 Swatch Row, Pearl Sheen, Bi-Phase, Satin Fold 가 ê·ž 갈래에 넷을 더했고, 31판의 Lip Gloss, Foam Lather, Setting Mist, Sponge Blend 가 넷을 더 더핮 엎둘읎에요. 32판의 Cushion Mesh, Clay Mask, Sugar Scrub, Chrome Powder 가 넷을 더핮 엎여섯읎고, 33판의 Cleansing Balm, Gold Leaf, Essence Drop, Peel-Off 가 넷을 더핮 슀묌읎에요. 34판의 Cat Eye, Velvet Nap, Mood Tint, Hair Gloss 가 넷을 더핮 슀묌넷읎고, 35판의 Bath Fizz, Toner Pad, Sheet Mask, Gilded Cream 읎 넷을 더핮 슀묌여덟읎고, 36판의 Lotion Ribbon, Shimmer Oil, Blooming Gel, Whipped Soap 읎 넷을 더핮 서륞둘읎에요. 38판의 Milk Ring, Couette Shimmer, Photochromic, Serum Fingers 가 넷을 더핮 서륞여섯읎에요. 39판의 Soap Film, Blotting Paper, Serum Drip, Crackle Polish 가 넷을 더핮, 37판의 넷까지 마흔넷읎에요. 41판은 할로윈 갈래륌 엎었얎요. 죌제 6 의 몫읎띌 죌제 2 로 섞지 않았고, Jack-o'-Lantern, Sheet Ghosts, Spider Drop, Cauldron, Eyes in the Dark 닀섯읎 듀얎 있얎요. 28판읎 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table, 33판의 Goldbach Comet, 35판의 Kolakoski, 36판의 Elliptic Chords, 39판의 Explicit Formula, 44판의 Bulgarian Solitaire, 48판의 Chebyshev's Bias, 49판의 Curlicue 예요. 27판읎 연 철도 갈래는 Funicular 하나읎고, 필늄·영화 갈래는 Horse in Motion 하나예요. 두 갈래는 죌제 표가 수학 쪜윌로 바뀌Ʞ 전에 엎었얎요. ê·ž 뒀로 ROUND.md 는 사묌로 묶는 갈래륌 더 ì—Žì§€ 않아요. 26판읎 연 얌음·극지 갈래는 Iceberg 와 27판의 Sled Dogs 읎고, 믞생묌 갈래는 Dark Field 와 27판의 Mitosis 예요. 25판읎 연 농사·밭 갈래는 Rice Terraces 와 26판의 Rice Planting, 27판의 Center Pivot 읎고, 화산 갈래는 Eruption 곌 26판의 Geyser, 27판의 Lava Lake 예요. 24판읎 연 공사·걎섀 갈래는 Tower Crane 곌 25판의 Excavation, 26판의 Implosion 읎에요. 24판읎 연 도자Ʞ 갈래는 Potter's Wheel 곌 25판의 Kintsugi, 26판의 Noborigama 예요. 23판읎 연 묎대·공연 갈래는 Proscenium 곌 24판의 Flying Trapeze, 25판의 Marionette, 26판의 Shadow Play 예요. 23판읎 연 짐승 갈래는 Giraffe 와 24판의 Emergence, 25판의 Snow Monkeys 예요. 22판읎 연 몞 갈래는 Circulation 곌 23판의 Axon, 24판의 Breath, 27판의 Fluoroscope 예요. 22판읎 연 비행 갈래는 Liftoff 와 23판의 Final Approach, 24판의 Thermal, 25판의 Skydiver 예요. 20판읎 연 새 갈래는 Kingfisher 와 21판의 Woodpecker, 22판의 Weaver, 23판의 Skein 읎에요. 20판읎 연 화학·싀험 갈래는 Chromatography 와 21판의 Chemical Garden, 22판의 Flame Test, 23판의 Elephant Toothpaste 예요. 21판읎 연 종읎 갈래는 Pop-up Book 곌 22판의 Senbazuru, 23판의 Hanging Scroll, 26판의 Origami 예요. 21판읎 연 욎동·겜Ʞ 갈래는 Lap Pool 곌 22판의 Bowling Lane, 23판의 High Dive, 26판의 Pole Vault, 27판의 Climbing Wall 읎에요. 위아래로 움직읎는 겜Ʞ는 High Dive 와 Pole Vault 뒀로 27판에 싀낎 암벜읎 ì„°ì–Žìš”. 19판읎 연 부엌 갈래는 Popcorn 곌 20판의 Kettle, 21판의 Pancakes 읎고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판읎 연 유늬 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori, 27판의 Glassblower 예요. ꎑ묌·결정 갈래는 Bismuth 와 19판의 Dripstone, 20판의 Agate 읎고, 놀읎공원 갈래는 Drop Tower 와 19판의 Helter Skelter, 20판의 High Striker, 25판의 Ferris Wheel 읎에요. 말읎 였륎낎늬는 회전목마는 아직 없얎요. 17판읎 연 전자 회로 갈래는 Circuit Board 와 18판의 Server Rack, 21판의 Nixie 읎고, 장난감 갈래는 Marble Run 곌 18판의 Peg Light, 21판의 Slinky, 22판의 Pin Art 예요. 16판읎 연 바닀 갈래는 Abyss 와 17판의 Shoreline, 20판의 Sunglint, 23판의 Bait Ball 읎고, 곀충 갈래는 Fireflies 와 17판의 Ant Farm, 20판의 Moth Lamp, 24판의 Waggle Dance 예요. 15판읎 연 도시 갈래는 Metro Map 곌 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판읎 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables, 24판의 Pipe Organ 읎에요. 4판읎 연 직묌 갈래는 Jacquard Loom 곌 15판의 Plait, 19판의 Needlepoint, 23판의 Sequins 읎고, 8판읎 연 Ʞ계 갈래는 Gear Train 곌 15판의 Cam Wave, 19판의 Paternoster 예요. 12판읎 연 천첎 갈래는 Star Trails 와 15판의 Analemma, 16판의 Lunation, 22판의 Galilean Moons 예요. 위성의 궀도는 Galilean Moons 가 ê·žë žê³ , 핎륌 도는 행성은 아직 없얎요. 늬듬·펄슀 갈래는 1판의 Euclid Pulse 와 Pulse Bars 뒀로 20판의 Change Ringing, 32판의 Arnold Tongues, 35판의 Chimera State, 38판의 Infinity Series, 41판의 Siteswap, 44판의 Kac Ring, 46판의 Stochastic Resonance 가 ì„°ì–Žìš”. +`families.json` 의 쉰여섯 갈래에 없는 것 가욎데 섞욎 팚널에 맞을 것을 적얎 둬요. 29판읎 연 위상·맀듭 갈래는 Hopf Fibration 곌 31판의 Boy's Surface, 33판의 Borromean Rings, 34판의 Lakes of Wada, 36판의 Heawood Torus, 38판의 Sperner's Lemma, 39판의 Borsuk-Ulam, 44판의 Ham Sandwich, 46판의 Reeb Graph, 48판의 Möbius Energy, 49판의 Square Peg, 51판의 Persistence Barcode 예요. 28판읎 엎렀닀 접은 갈래띌 28판의 Hairy Ball 은 녞읎슈·흐늄에 귞대로 있얎요. 섞잎 맀듭은 48판의 Möbius Energy 가 처음 섞웠얎요. 29판은 화장품 팝업 갈래도 엎었얎요. 죌제 6 의 몫읎띌 죌제 2 로 섞지 않았고, Cream Swirl, Serum Gel, Glitter Fall, Loose Powder 넷읎 ë‹€ 듀얎 있얎요. 30판의 Swatch Row, Pearl Sheen, Bi-Phase, Satin Fold 가 ê·ž 갈래에 넷을 더했고, 31판의 Lip Gloss, Foam Lather, Setting Mist, Sponge Blend 가 넷을 더 더핮 엎둘읎에요. 32판의 Cushion Mesh, Clay Mask, Sugar Scrub, Chrome Powder 가 넷을 더핮 엎여섯읎고, 33판의 Cleansing Balm, Gold Leaf, Essence Drop, Peel-Off 가 넷을 더핮 슀묌읎에요. 34판의 Cat Eye, Velvet Nap, Mood Tint, Hair Gloss 가 넷을 더핮 슀묌넷읎고, 35판의 Bath Fizz, Toner Pad, Sheet Mask, Gilded Cream 읎 넷을 더핮 슀묌여덟읎고, 36판의 Lotion Ribbon, Shimmer Oil, Blooming Gel, Whipped Soap 읎 넷을 더핮 서륞둘읎에요. 38판의 Milk Ring, Couette Shimmer, Photochromic, Serum Fingers 가 넷을 더핮 서륞여섯읎에요. 39판의 Soap Film, Blotting Paper, Serum Drip, Crackle Polish 가 넷을 더핮, 37판의 넷까지 마흔넷읎에요. 41판은 할로윈 갈래륌 엎었얎요. 죌제 6 의 몫읎띌 죌제 2 로 섞지 않았고, Jack-o'-Lantern, Sheet Ghosts, Spider Drop, Cauldron, Eyes in the Dark 닀섯읎 듀얎 있얎요. 28판읎 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table, 33판의 Goldbach Comet, 35판의 Kolakoski, 36판의 Elliptic Chords, 39판의 Explicit Formula, 44판의 Bulgarian Solitaire, 48판의 Chebyshev's Bias, 49판의 Curlicue, 51판의 Sato-Tate 예요. 27판읎 연 철도 갈래는 Funicular 하나읎고, 필늄·영화 갈래는 Horse in Motion 하나예요. 두 갈래는 죌제 표가 수학 쪜윌로 바뀌Ʞ 전에 엎었얎요. ê·ž 뒀로 ROUND.md 는 사묌로 묶는 갈래륌 더 ì—Žì§€ 않아요. 26판읎 연 얌음·극지 갈래는 Iceberg 와 27판의 Sled Dogs 읎고, 믞생묌 갈래는 Dark Field 와 27판의 Mitosis 예요. 25판읎 연 농사·밭 갈래는 Rice Terraces 와 26판의 Rice Planting, 27판의 Center Pivot 읎고, 화산 갈래는 Eruption 곌 26판의 Geyser, 27판의 Lava Lake 예요. 24판읎 연 공사·걎섀 갈래는 Tower Crane 곌 25판의 Excavation, 26판의 Implosion 읎에요. 24판읎 연 도자Ʞ 갈래는 Potter's Wheel 곌 25판의 Kintsugi, 26판의 Noborigama 예요. 23판읎 연 묎대·공연 갈래는 Proscenium 곌 24판의 Flying Trapeze, 25판의 Marionette, 26판의 Shadow Play 예요. 23판읎 연 짐승 갈래는 Giraffe 와 24판의 Emergence, 25판의 Snow Monkeys 예요. 22판읎 연 몞 갈래는 Circulation 곌 23판의 Axon, 24판의 Breath, 27판의 Fluoroscope 예요. 22판읎 연 비행 갈래는 Liftoff 와 23판의 Final Approach, 24판의 Thermal, 25판의 Skydiver 예요. 20판읎 연 새 갈래는 Kingfisher 와 21판의 Woodpecker, 22판의 Weaver, 23판의 Skein 읎에요. 20판읎 연 화학·싀험 갈래는 Chromatography 와 21판의 Chemical Garden, 22판의 Flame Test, 23판의 Elephant Toothpaste 예요. 21판읎 연 종읎 갈래는 Pop-up Book 곌 22판의 Senbazuru, 23판의 Hanging Scroll, 26판의 Origami 예요. 21판읎 연 욎동·겜Ʞ 갈래는 Lap Pool 곌 22판의 Bowling Lane, 23판의 High Dive, 26판의 Pole Vault, 27판의 Climbing Wall 읎에요. 위아래로 움직읎는 겜Ʞ는 High Dive 와 Pole Vault 뒀로 27판에 싀낎 암벜읎 ì„°ì–Žìš”. 19판읎 연 부엌 갈래는 Popcorn 곌 20판의 Kettle, 21판의 Pancakes 읎고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판읎 연 유늬 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori, 27판의 Glassblower 예요. ꎑ묌·결정 갈래는 Bismuth 와 19판의 Dripstone, 20판의 Agate 읎고, 놀읎공원 갈래는 Drop Tower 와 19판의 Helter Skelter, 20판의 High Striker, 25판의 Ferris Wheel 읎에요. 말읎 였륎낎늬는 회전목마는 아직 없얎요. 17판읎 연 전자 회로 갈래는 Circuit Board 와 18판의 Server Rack, 21판의 Nixie 읎고, 장난감 갈래는 Marble Run 곌 18판의 Peg Light, 21판의 Slinky, 22판의 Pin Art 예요. 16판읎 연 바닀 갈래는 Abyss 와 17판의 Shoreline, 20판의 Sunglint, 23판의 Bait Ball 읎고, 곀충 갈래는 Fireflies 와 17판의 Ant Farm, 20판의 Moth Lamp, 24판의 Waggle Dance 예요. 15판읎 연 도시 갈래는 Metro Map 곌 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판읎 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables, 24판의 Pipe Organ 읎에요. 4판읎 연 직묌 갈래는 Jacquard Loom 곌 15판의 Plait, 19판의 Needlepoint, 23판의 Sequins 읎고, 8판읎 연 Ʞ계 갈래는 Gear Train 곌 15판의 Cam Wave, 19판의 Paternoster 예요. 12판읎 연 천첎 갈래는 Star Trails 와 15판의 Analemma, 16판의 Lunation, 22판의 Galilean Moons 예요. 위성의 궀도는 Galilean Moons 가 ê·žë žê³ , 핎륌 도는 행성은 아직 없얎요. 늬듬·펄슀 갈래는 1판의 Euclid Pulse 와 Pulse Bars 뒀로 20판의 Change Ringing, 32판의 Arnold Tongues, 35판의 Chimera State, 38판의 Infinity Series, 41판의 Siteswap, 44판의 Kac Ring, 46판의 Stochastic Resonance 가 ì„°ì–Žìš”. ### 멋진 것 더 멋지게 @@ -2641,6 +2702,8 @@ ESP32-S3 로 옮Ꞟ 때 걞늎 것도 있얎요. Reaction Diffusion 은 Node 에 49판의 ì—Ž 개는 가장 낮은 녞람가 Music Box 의 K4 Wind 로 ë„€ 자늬로 0.0453 읎고, Gargoyle 의 K2 Speed 0.0531 곌 Party Lights 의 K2 Speed 0.0544 가 귞닀음읎에요. 녾뾌 마흔 개가 ë‹€ 0.045 륌 넘얎요. Wind 는 태엜을 감은 만큌읎띌 녞람륌 재는 2~3쎈 ì°œ 뒀의 음을 바꟞는 녞람였얎요. 가장 낮은 자늬가 2.6쎈쯀 풀렀 멈추게 2.6 + 16.4√Wind 쎈로 걞얎서 셈에 듀었얎요. Kempe Chains 의 ë„·ì§ž 녾뾌도 사슬 빛을 바꟞는 Glow 로는 ê·ž 찜에 사슬읎 쌜젞 있을 때만 볎여서, 첫 프레임부터 국겜을 ë‹€ 바꟞는 Sizes 로 바꿚얎요. Werewolf 의 Clouds 는 첫 판에서 0.0088 로 검사에 걞렞얎요. 구늄읎 2쎈 전에 달을 ë‹€ 지나가서 찜에 낚은 구늄읎 없었고, 뒀따륎는 구늄 셋을 4쎈까지 달 앞에 두자 0.069 가 됐얎요. 녞람가 바꟞는 넓읎륌 귞늌자로 늘늬Ʞ도 했얎요. Pit and Pendulum 의 Speed 와 Descent 는 칌날만 바꿔서 0.04 였고, 횃불을 낎렀 칌날 귞늌자륌 밝은 벜에 크게 던지자 0.098 곌 0.076 읎 됐얎요. +51판의 ì—Ž 개는 가장 낮은 녞람가 Bat Form 의 K4 Flock 윌로 ë„€ 자늬로 0.0370 읎고, Zombie Shamble 의 K2 Speed 0.0416 곌 Grimoire 의 K1 Hue 0.0422 가 귞닀음읎에요. 녾뾌 마흔 개가 ë‹€ 0.03 을 넘얎요. 읎번에도 녞람가 2~3쎈 찜에 화멎에 있는 것을 바꟞게 고쳀얎요. Bat Form 의 Cape 는 첫 판에서 0.0115 였는데, 망토륌 ê·ž ì°œ 안에 펌쳐 두고 범위륌 -1..1 로 넓히자 0.0426 읎 됐얎요. Flock 은 펌친 망토 뒀에 가렀 0.02 였고, 떌륌 위쪜 하늘의 아치 폭 안에 띄우고 120마늬까지 늘렀 0.037 읎 됐얎요. 흔듀늌은 읎알Ʞ의 첫 사걎에 맞춰 시작핎알 찜에 볎였얎요. Jack-in-the-Box 의 Wobble 은 t = 0 에서 돌던 진자륌 뚜껑읎 터지는 순간에 시작하게 바꟞자 0.03 에서 0.05 가 됐얎요. 녞람가 바꟞는 넓읎륌 ë°© 전첎로 넓히Ʞ도 했얎요. Grimoire 는 0.02~0.03 읎었는데, 바람읎 쎛불을 낮추고 Glow 가 방을 쎈록윌로 비추게 하자 넷 ë‹€ 0.042 륌 넘었얎요. Zombie Shamble 의 Horde 는 좀비 수만 바꿀 때 0.02~0.03 읎었고, 처음 섞우는 쀄을 떌의 크Ʞ에 따띌 바꟞자 0.058 읎 됐얎요. Hat Monotile 은 넷 ë‹€ 0.37 을 넘얎요. 찜읎 LED 한 칞씩 흘러가서 녞람마닀 몚자 수천 장의 자늬가 바뀌Ʞ 때묞읎에요. + Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. ### 섞로 64×128 @@ -2687,9 +2750,11 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. 49판에서는 Gargoyle 곌 Pit and Pendulum 읎 128쀄을 썚요. Gargoyle 은 성당곌 하늘읎 판을 덮얎 `thirds` 가 97/92/91 읎에요. 성당곌 하늘, Ꞟ을 지우고 가고음곌 묌쀄Ʞ, 비, 웅덩읎만 귞늬멎 22/8/14 예요. 첫 판은 ê·ž 값읎 20.8/5.0/11.1 읎었고 Speed 0.2 에서는 가욎데가 2.5 였얎요. 아래로 꺟읎는 묌쀄Ʞ는 가로 폭을 지쌜서, 입에서 굵게 낎볎낎도 가욎데 3분의 1 에서는 1.5~2 LED 였얎요. 출발점을 옆윌로 ±1 LED 퍌뜚늬고 옆 빠륎Ʞ의 흔듀늌을 0.8 에서 2.2 LED/쎈로 킀우자 30쀄에서 3.0, 100쀄에서 3.6 LED 가 됐얎요. 비는 팹널 밖에서 생겚 떚얎지던 것을 비가 Ʞ욎 쪜에서 생Ʞ게 고쳐 70 방욞에서 90 방욞읎 됐얎요. Pit and Pendulum 은 67/95/53 읎고, 벜곌 바닥을 지우고 막대와 칌날, 돌 칚대, 쥐, 횃불 빛만 귞늬멎 10/23/31 읎에요. 칌날 귞늌자륌 밝은 벜에 던지렀고 횃불을 42쀄에서 52쀄로 낮렾더니 지욎 귞늌의 위 3분의 1 읎 7 로 낎렀가서, 권양Ʞ 드럌곌 두 LED 사슬로 10 에 되돌렞얎요. Kempe Chains(93/92/93)와 Party Lights(54/58/47), Werewolf(53/86/48), Music Box(45/80/91), Square Peg(35/82/40), DÃŒrer's Unfolding(32/45/20), Tennis Racket(24/28/67), Curlicue(14/20/14)도 섞 칞읎 ë‹€ 5 륌 넘지만 죌제 4 로 섞지 않았얎요. +51판에서는 Sato-Tate 와 Tower Stair 가 128쀄을 썚요. Sato-Tate 는 위 두 칞에 소수의 빗방욞읎 떚얎지고 아래 칞에 막대가 쌓여 `thirds` 가 16/14/70 읎에요. Tower Stair 는 돌탑의 벜곌 디딀판읎 판을 덮얎 99/96/99 예요. 쎛불읎 꺌진 얎둠에서도 달빛 든 돌을 24 넘게 칠핎서 섞 칞읎 비지 않아요. Phantom Jam 은 팹널 가장자늬륌 도는 튞랙곌 ê·ž 안의 시공간 귞늌윌로 45/50/45 읎고, 테두늬읞 튞랙을 지우고 재도 34/42/34 예요. Zombie Shamble(96/90/95)곌 Grimoire(84/99/96), Bat Form(74/87/54), Hat Monotile(61/61/62), Persistence Barcode(91/51/10), Calabi-Yau Quintic(16/83/15), Jack-in-the-Box(9/28/68)도 섞 칞읎 ë‹€ 5 륌 넘지만 죌제 4 로 섞지 않았얎요. Calabi-Yau Quintic 의 조각은 섞 축의 분산읎 ë‹€ 0.458 읎띌 섞욞 ꞎ 쪜읎 없고, 펌친 귞늌만 128쀄을 채워요. + ### 낯선 수학 -죌제 5 로 쓎 수학곌 닀음에 핮 볌 수학읎에요. 28판읎 처음 ì—Ž 개륌 썌얎요. 슈람-뢰람너 진화(Loewner Trace), 포드 원(Ford Circles), 턾 난 ê³µ 정늬(Hairy Ball), 아슈텍 닀읎아몬드와 북극권(Aztec Diamond), 부니몚비치 겜Ʞ장(Stadium Billiard), 띌우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 닀멎첎와 E8(Gosset Polytope), 윘웚읎의 볎고 말하Ʞ 수엎(Look and Say), 위토프 게임의 귞런디 값(Wythoff Nim), 쌍곡 평멎의 띠 몚형(Hyperbolic Band)읎에요. 29판은 여섯을 더했얎요. 큎띌읞의 j 불변량(J-Invariant), 혾프 올뭉치(Hopf Fibration), 닀읎슚 람띌욎 욎동(Dyson Gas), 가쌀알 바늘 묞제의 페론 나묎(Perron Tree), 임계선 위의 늬만 제타 핚수(Zeta Loops), Ꞟ버튞-섀넌-늬슈 늬플곌 베읎얎-디아윔니슀의 음곱 번(Seven Shuffles)읎에요. 30판도 여섯을 더했얎요. 섞 간격 정늬(Three Gaps), 혞프슀태터 나비와 TKNN 정수(Hofstadter Butterfly), 녞읎만의 왞부 당구(Outer Billiards), 볌드의 탄도 퇎적곌 KPZ 앞멎(Ballistic Deposition), 아륎놀튞의 믞앀더(Meander), 람로드벀튞와 핎뚞슬늬의 슀믞Ʞ(Percolation)예요. 31판도 여섯을 더했얎요. 마슀킷 조각의 큎띌읞 êµ° 극한 집합(Indra's Pearls), 늬틀우드 닀항식의 ê·Œ(Littlewood Roots), 고든의 가우슀 핎자(Gaussian Moat), 아륎놀튞의 고양읎 사상(Cat Map), 자부슀킀와 크룚슀칌의 KdV 솔늬톀(KdV Solitons), 볎읎 곡멎(Boy's Surface)읎에요. ROUND.md 의 「낯선 수학 예」 여덟 가욎데 쓎 것은 슈람-뢰람너 진화와 아슈텍 닀읎아몬드, 몚듈러 형식, 혾프 올뭉치, 섞 간격 정늬, 왞부 당구 여섯읎었고, 31판읎 늬틀우드 닀항식의 귌곌 큎띌읞 군의 극한 집합을 썚서 여덟을 ë‹€ 썌얎요. 32판도 여섯을 더했얎요. 타벗 횚곌와 양자 칎펫(Talbot Carpet), Ꞟ람레슀 추잡의 표(Gilbreath Table), 원 사상의 아륎놀튞 혀(Arnold Tongues), 윘웚읎와 띌딘의 핀휠 타음링(Pinwheel Tiling), 윌킚슚곌 윌렘섌의 칚입 슀믞Ʞ(Invasion Percolation), 욞람의 묞제와 핎뚞슬늬 곌정(Hammersley Lines)읎에요. 33판도 여섯을 더했얎요. 에읞절, 홀로읎드, 로믹, 비띌귞의 묎작위 정렬 넀튞워크(Sorting Network), 베륎튞랑의 ì—­ì„€(Bertrand Paradox), 펜로슈의 비출 수 없는 ë°©(Penrose Room), 당듀랭 구(Dandelin Spheres), 테읎튞의 볎로메였 고늬(Borromean Rings), 곚드바흐 혜성(Goldbach Comet)읎에요. 34판도 여섯을 더했얎요. 톰슚 묞제와 였음러가 낚Ʞ는 닀섯 읎웃 엎둘(Thomson Problem), 쌈빈의 ë°° 묌결(Kelvin Wake), 요넀알마가 낾 와닀의 혞수(Lakes of Wada), 샀읎데거의 강 귞묌(Scheidegger Rivers), 몚저의 소파 묞제와 핎뚞슬늬의 소파(Moving Sofa), 크누슀의 춀추는 링크로 푌 펜토믞녞 덮Ʞ(Pentomino Cover)예요. 35판도 여섯을 더했얎요. 뢞로의 폭읎 음정한 곡선(Constant Width), 렌니의 죌찚 묞제(Rényi Parking), 칎늬의 비죌Ʞ 왕 타음(Kari-Culik Tiles), 윜띌윔슀킀 수엎(Kolakoski), 쿠띌몚토와 바톡토흐의 킀메띌 상태(Chimera State), 죌윥슀킀 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했얎요. 귞레읎슚의 정늬가 말하는 곡선 닚축 흐멄(Curve Shortening), 헀우드의 음곱 색 토러슀 지도(Heawood Torus), 슈바륎잠 랜턮(Schwarz Lantern), 윌람러햄곌 깁슀의 현상(Gibbs Phenomenon), 타원 곡선의 현곌 접선(Elliptic Chords), 지뚌의 파국 Ʞ계(Catastrophe Machine)예요. 37판도 여섯을 더했얎요. 포천의 쓞Ʞ 선 알고늬슘(Fortune's Sweep), 앙투안의 목걞읎(Antoine's Necklace), 칎륎만 소용돌읎 Ꞟ의 안정성(Kármán Street), 당당한 원판의 헥사틱 상(Hexatic Phase), 졎슚-멜 쪜맀와 아람띌믞 법칙(Johnson-Mehl), 쿠띌몚토-시바신슀킀 방정식(Kuramoto-Sivashinsky)읎에요. 38판도 여섯을 더했얎요. 앀더슚 국소화(Anderson Localization), 묎얎가 ì°Ÿê³  Chenciner 와 Montgomery 가 슝명한 섞 몞 묞제의 8자 핮(Figure-Eight Orbit), 예섌곌 빈튞너, 에륎되시의 베륎누읎 합성곱(Bernoulli Convolutions), 람띌우얎륎의 고정점 정늬륌 슝명하는 슈페륎너 볎조정늬(Sperner's Lemma), 에겐베륎거와 폎늬아의 항아늬(Pólya Urn), 뇌륎고륎의 묎한 수엎(Infinity Series)읎에요. 39판도 여섯을 더했얎요. 고든, 웹, 월퍌튞의 등슀펙튞럌 북(Isospectral Drums), 폰 망곚튞의 ꌎ로 쓎 늬만의 명시 공식(Explicit Formula), 요한 베륎누읎의 최닚 강하선(Brachistochrone), 슈타읎너의 포늬슘(Steiner Chain), 몚런 집닚윌로 귞늰 킹뚌의 합류(Kingman Coalescent), 볎륎수크-욞람 정늬(Borsuk-Ulam)예요. 40판도 여섯을 더했얎요. 앚더슀의 람띌욎 연속 랜덀 나묎(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 핚수(Hydrogen Orbital), 저도비치 귌사의 팬쌀읎크(Zeldovich Pancakes), 망덞람로가 낎닀볎고 례러, 슈람, 베륎너가 슝명한 람띌욎 욎동의 바깥 겜계(Brownian Frontier), 복소 ꞎ슈부륎크-란닀우 방정식의 결핚 난류(Defect Turbulence), 룚퍌튞 왕자의 정육멎첎(Prince Rupert's Cube)예요. 41판은 닀섯을 더했얎요. 홀곌 왜걎의 현수선 Ꞟ곌 정닀각형 바퀮(Square Wheels), 퐁슬레의 포늬슘(Poncelet Porism), 슐레플늬의 4찚원 정닀포첎륌 자륞 닚멎(Polytope Slices), 큎늌쌁곌 티뚌, 마귞누손의 사읎튞슀왑(Siteswap), Dudeney 의 잡화상 퍌슐곌 월늬슀-볎여읎-게륎비엔 정늬(Hinged Dissection)예요. 42판은 닀섯을 더했얎요. 바륞슀도륎프 규칙윌로 도는 Ʞ사 여행(Knight's Tour), 페륎믞와 파슀타, 욞람, 칭구의 되돌아옎(FPUT Recurrence), 포슬늬에-늜킚 반전Ʞ(Peaucellier Linkage), 몰늬의 삌등분선 정늬(Morley Triangle), 랄곌 마드띌슀, 소칌의 플벗 알고늬슘윌로 돌며 자Ʞ 회플 걞음(Pivot Walk)읎에요. 43판은 닀섯을 더했얎요. 월늬슀 워컀의 칌레읎도사읎큎(Kaleidocycle), 쌍곡형에서 록소드롬형윌로 감Ʞ는 뫌비우슀 변환의 흐멄(Möbius Loxodrome), 터튞의 용수철 정늬(Tutte Embedding), 디펀튞와 크래비잠의 고늬 정늬륌 품은 히토메자시 사시윔(Hitomezashi), 룜게 현상곌 첎비쇌프 점(Runge Phenomenon)읎에요. 44판은 닀섯을 더했얎요. 마크 칎잠의 고늬 몚형(Kac Ring), 슀톀곌 튜킀의 햄 샌드위치 정늬(Ham Sandwich), 람란튞의 불가늬아 솔늬테얎(Bulgarian Solitaire), 엘슀쌄슀와 프늬시의 탄도 소멞(Ballistic Annihilation), 페레귞늰의 솔늬톀(Peregrine Soliton)읎에요. 45판은 닀섯을 더했얎요. 로걎-셰프와 베륎시크-쌀로프의 극한 곡선윌로 닀가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도람룚신 겜계 조걎을 걎 임계 읎징 몚형의 SLE(3) 겜계(Ising Interface), 프늬예제프와 동료듀의 였음러 걷개로 쌓는 로터 띌우터 응집곌 레빈-페레슀의 슀맀시 합(Rotor-Router), 폎늬아의 곌수원 묞제(Pólya's Orchard), 넬슚의 평멎 색칠 묞제와 아읎슈벚의 음곱 색(Hadwiger-Nelson)읎에요. 46판은 닀섯을 더했얎요. 람늎룚앙의 구역(Brillouin Zones), 몚슀 읎론곌 레람의 귞래프(Reeb Graph), 로렌잠 Ʞ첎와 시나읎의 당구(Lorentz Gas), 늬얞 Ꞁ래슀의 점묎늬(Glass Patterns), 벀치와 수테띌, 불플아니의 확률 공명(Stochastic Resonance)읎에요. 47판은 닀섯을 더했얎요. 였킀프와 홍, 슀튞로가잠의 슀워멀레읎터(Swarmalators), 빌슀의 왞로욎 달늬Ʞ 선수 추잡(Lonely Runner), 샀잠의 올로읎드와 ê·ž 전개도(Oloid), 쟚베의 원 채우Ʞ 정늬와 윜늰슀-슀티랐슚의 반복(Koebe Packing), 윘웚읎의 병사와 황ꞈ비 묎게(Conway's Soldiers)예요. 48판은 닀섯을 더했얎요. 였하띌의 맀듭 뫌비우슀 에너지(Möbius Energy), 첎비쇌프의 펞향곌 룚빈슀타읞-사륎낙의 밀도(Chebyshev's Bias), 섀든곌 레킀엔, 마슀든의 읎쀑 소용돌읎 능선(Double Gyre), 아자늬와 프로슀튞의 깜빡읎는 래칫(Flashing Ratchet), 망덞람로의 프랙탈 슀믞Ʞ(Fractal Percolation)예요. 49판은 닀섯을 더했얎요. 뒀러의 전개 묞제와 셰퍌드의 추잡(DÃŒrer's Unfolding), 베늬와 곚드버귞가 재규격화한 섞타 합의 ꌬ부랑 곡선(Curlicue), 였음러 방정식곌 푞앵소의 폎혞드로 볞 테니슀 띌쌓 정늬(Tennis Racket), 쌐프의 사슬곌 ë„€ 색 정늬(Kempe Chains), 퇎플늬잠의 낎접 정사각형 묞제와 귞늰-로람의 직사각형(Square Peg)읎에요. +죌제 5 로 쓎 수학곌 닀음에 핮 볌 수학읎에요. 28판읎 처음 ì—Ž 개륌 썌얎요. 슈람-뢰람너 진화(Loewner Trace), 포드 원(Ford Circles), 턾 난 ê³µ 정늬(Hairy Ball), 아슈텍 닀읎아몬드와 북극권(Aztec Diamond), 부니몚비치 겜Ʞ장(Stadium Billiard), 띌우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 닀멎첎와 E8(Gosset Polytope), 윘웚읎의 볎고 말하Ʞ 수엎(Look and Say), 위토프 게임의 귞런디 값(Wythoff Nim), 쌍곡 평멎의 띠 몚형(Hyperbolic Band)읎에요. 29판은 여섯을 더했얎요. 큎띌읞의 j 불변량(J-Invariant), 혾프 올뭉치(Hopf Fibration), 닀읎슚 람띌욎 욎동(Dyson Gas), 가쌀알 바늘 묞제의 페론 나묎(Perron Tree), 임계선 위의 늬만 제타 핚수(Zeta Loops), Ꞟ버튞-섀넌-늬슈 늬플곌 베읎얎-디아윔니슀의 음곱 번(Seven Shuffles)읎에요. 30판도 여섯을 더했얎요. 섞 간격 정늬(Three Gaps), 혞프슀태터 나비와 TKNN 정수(Hofstadter Butterfly), 녞읎만의 왞부 당구(Outer Billiards), 볌드의 탄도 퇎적곌 KPZ 앞멎(Ballistic Deposition), 아륎놀튞의 믞앀더(Meander), 람로드벀튞와 핎뚞슬늬의 슀믞Ʞ(Percolation)예요. 31판도 여섯을 더했얎요. 마슀킷 조각의 큎띌읞 êµ° 극한 집합(Indra's Pearls), 늬틀우드 닀항식의 ê·Œ(Littlewood Roots), 고든의 가우슀 핎자(Gaussian Moat), 아륎놀튞의 고양읎 사상(Cat Map), 자부슀킀와 크룚슀칌의 KdV 솔늬톀(KdV Solitons), 볎읎 곡멎(Boy's Surface)읎에요. ROUND.md 의 「낯선 수학 예」 여덟 가욎데 쓎 것은 슈람-뢰람너 진화와 아슈텍 닀읎아몬드, 몚듈러 형식, 혾프 올뭉치, 섞 간격 정늬, 왞부 당구 여섯읎었고, 31판읎 늬틀우드 닀항식의 귌곌 큎띌읞 군의 극한 집합을 썚서 여덟을 ë‹€ 썌얎요. 32판도 여섯을 더했얎요. 타벗 횚곌와 양자 칎펫(Talbot Carpet), Ꞟ람레슀 추잡의 표(Gilbreath Table), 원 사상의 아륎놀튞 혀(Arnold Tongues), 윘웚읎와 띌딘의 핀휠 타음링(Pinwheel Tiling), 윌킚슚곌 윌렘섌의 칚입 슀믞Ʞ(Invasion Percolation), 욞람의 묞제와 핎뚞슬늬 곌정(Hammersley Lines)읎에요. 33판도 여섯을 더했얎요. 에읞절, 홀로읎드, 로믹, 비띌귞의 묎작위 정렬 넀튞워크(Sorting Network), 베륎튞랑의 ì—­ì„€(Bertrand Paradox), 펜로슈의 비출 수 없는 ë°©(Penrose Room), 당듀랭 구(Dandelin Spheres), 테읎튞의 볎로메였 고늬(Borromean Rings), 곚드바흐 혜성(Goldbach Comet)읎에요. 34판도 여섯을 더했얎요. 톰슚 묞제와 였음러가 낚Ʞ는 닀섯 읎웃 엎둘(Thomson Problem), 쌈빈의 ë°° 묌결(Kelvin Wake), 요넀알마가 낾 와닀의 혞수(Lakes of Wada), 샀읎데거의 강 귞묌(Scheidegger Rivers), 몚저의 소파 묞제와 핎뚞슬늬의 소파(Moving Sofa), 크누슀의 춀추는 링크로 푌 펜토믞녞 덮Ʞ(Pentomino Cover)예요. 35판도 여섯을 더했얎요. 뢞로의 폭읎 음정한 곡선(Constant Width), 렌니의 죌찚 묞제(Rényi Parking), 칎늬의 비죌Ʞ 왕 타음(Kari-Culik Tiles), 윜띌윔슀킀 수엎(Kolakoski), 쿠띌몚토와 바톡토흐의 킀메띌 상태(Chimera State), 죌윥슀킀 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했얎요. 귞레읎슚의 정늬가 말하는 곡선 닚축 흐멄(Curve Shortening), 헀우드의 음곱 색 토러슀 지도(Heawood Torus), 슈바륎잠 랜턮(Schwarz Lantern), 윌람러햄곌 깁슀의 현상(Gibbs Phenomenon), 타원 곡선의 현곌 접선(Elliptic Chords), 지뚌의 파국 Ʞ계(Catastrophe Machine)예요. 37판도 여섯을 더했얎요. 포천의 쓞Ʞ 선 알고늬슘(Fortune's Sweep), 앙투안의 목걞읎(Antoine's Necklace), 칎륎만 소용돌읎 Ꞟ의 안정성(Kármán Street), 당당한 원판의 헥사틱 상(Hexatic Phase), 졎슚-멜 쪜맀와 아람띌믞 법칙(Johnson-Mehl), 쿠띌몚토-시바신슀킀 방정식(Kuramoto-Sivashinsky)읎에요. 38판도 여섯을 더했얎요. 앀더슚 국소화(Anderson Localization), 묎얎가 ì°Ÿê³  Chenciner 와 Montgomery 가 슝명한 섞 몞 묞제의 8자 핮(Figure-Eight Orbit), 예섌곌 빈튞너, 에륎되시의 베륎누읎 합성곱(Bernoulli Convolutions), 람띌우얎륎의 고정점 정늬륌 슝명하는 슈페륎너 볎조정늬(Sperner's Lemma), 에겐베륎거와 폎늬아의 항아늬(Pólya Urn), 뇌륎고륎의 묎한 수엎(Infinity Series)읎에요. 39판도 여섯을 더했얎요. 고든, 웹, 월퍌튞의 등슀펙튞럌 북(Isospectral Drums), 폰 망곚튞의 ꌎ로 쓎 늬만의 명시 공식(Explicit Formula), 요한 베륎누읎의 최닚 강하선(Brachistochrone), 슈타읎너의 포늬슘(Steiner Chain), 몚런 집닚윌로 귞늰 킹뚌의 합류(Kingman Coalescent), 볎륎수크-욞람 정늬(Borsuk-Ulam)예요. 40판도 여섯을 더했얎요. 앚더슀의 람띌욎 연속 랜덀 나묎(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 핚수(Hydrogen Orbital), 저도비치 귌사의 팬쌀읎크(Zeldovich Pancakes), 망덞람로가 낎닀볎고 례러, 슈람, 베륎너가 슝명한 람띌욎 욎동의 바깥 겜계(Brownian Frontier), 복소 ꞎ슈부륎크-란닀우 방정식의 결핚 난류(Defect Turbulence), 룚퍌튞 왕자의 정육멎첎(Prince Rupert's Cube)예요. 41판은 닀섯을 더했얎요. 홀곌 왜걎의 현수선 Ꞟ곌 정닀각형 바퀮(Square Wheels), 퐁슬레의 포늬슘(Poncelet Porism), 슐레플늬의 4찚원 정닀포첎륌 자륞 닚멎(Polytope Slices), 큎늌쌁곌 티뚌, 마귞누손의 사읎튞슀왑(Siteswap), Dudeney 의 잡화상 퍌슐곌 월늬슀-볎여읎-게륎비엔 정늬(Hinged Dissection)예요. 42판은 닀섯을 더했얎요. 바륞슀도륎프 규칙윌로 도는 Ʞ사 여행(Knight's Tour), 페륎믞와 파슀타, 욞람, 칭구의 되돌아옎(FPUT Recurrence), 포슬늬에-늜킚 반전Ʞ(Peaucellier Linkage), 몰늬의 삌등분선 정늬(Morley Triangle), 랄곌 마드띌슀, 소칌의 플벗 알고늬슘윌로 돌며 자Ʞ 회플 걞음(Pivot Walk)읎에요. 43판은 닀섯을 더했얎요. 월늬슀 워컀의 칌레읎도사읎큎(Kaleidocycle), 쌍곡형에서 록소드롬형윌로 감Ʞ는 뫌비우슀 변환의 흐멄(Möbius Loxodrome), 터튞의 용수철 정늬(Tutte Embedding), 디펀튞와 크래비잠의 고늬 정늬륌 품은 히토메자시 사시윔(Hitomezashi), 룜게 현상곌 첎비쇌프 점(Runge Phenomenon)읎에요. 44판은 닀섯을 더했얎요. 마크 칎잠의 고늬 몚형(Kac Ring), 슀톀곌 튜킀의 햄 샌드위치 정늬(Ham Sandwich), 람란튞의 불가늬아 솔늬테얎(Bulgarian Solitaire), 엘슀쌄슀와 프늬시의 탄도 소멞(Ballistic Annihilation), 페레귞늰의 솔늬톀(Peregrine Soliton)읎에요. 45판은 닀섯을 더했얎요. 로걎-셰프와 베륎시크-쌀로프의 극한 곡선윌로 닀가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도람룚신 겜계 조걎을 걎 임계 읎징 몚형의 SLE(3) 겜계(Ising Interface), 프늬예제프와 동료듀의 였음러 걷개로 쌓는 로터 띌우터 응집곌 레빈-페레슀의 슀맀시 합(Rotor-Router), 폎늬아의 곌수원 묞제(Pólya's Orchard), 넬슚의 평멎 색칠 묞제와 아읎슈벚의 음곱 색(Hadwiger-Nelson)읎에요. 46판은 닀섯을 더했얎요. 람늎룚앙의 구역(Brillouin Zones), 몚슀 읎론곌 레람의 귞래프(Reeb Graph), 로렌잠 Ʞ첎와 시나읎의 당구(Lorentz Gas), 늬얞 Ꞁ래슀의 점묎늬(Glass Patterns), 벀치와 수테띌, 불플아니의 확률 공명(Stochastic Resonance)읎에요. 47판은 닀섯을 더했얎요. 였킀프와 홍, 슀튞로가잠의 슀워멀레읎터(Swarmalators), 빌슀의 왞로욎 달늬Ʞ 선수 추잡(Lonely Runner), 샀잠의 올로읎드와 ê·ž 전개도(Oloid), 쟚베의 원 채우Ʞ 정늬와 윜늰슀-슀티랐슚의 반복(Koebe Packing), 윘웚읎의 병사와 황ꞈ비 묎게(Conway's Soldiers)예요. 48판은 닀섯을 더했얎요. 였하띌의 맀듭 뫌비우슀 에너지(Möbius Energy), 첎비쇌프의 펞향곌 룚빈슀타읞-사륎낙의 밀도(Chebyshev's Bias), 섀든곌 레킀엔, 마슀든의 읎쀑 소용돌읎 능선(Double Gyre), 아자늬와 프로슀튞의 깜빡읎는 래칫(Flashing Ratchet), 망덞람로의 프랙탈 슀믞Ʞ(Fractal Percolation)예요. 49판은 닀섯을 더했얎요. 뒀러의 전개 묞제와 셰퍌드의 추잡(DÃŒrer's Unfolding), 베늬와 곚드버귞가 재규격화한 섞타 합의 ꌬ부랑 곡선(Curlicue), 였음러 방정식곌 푞앵소의 폎혞드로 볞 테니슀 띌쌓 정늬(Tennis Racket), 쌐프의 사슬곌 ë„€ 색 정늬(Kempe Chains), 퇎플늬잠의 낎접 정사각형 묞제와 귞늰-로람의 직사각형(Square Peg)읎에요. 51판은 닀섯을 더했얎요. 슀믞슀와 마읎얎슀, 캐플런, 굿뚌-슀튞띌우슀의 몚자 타음곌 H/T/P/F 메타타음 치환(Hat Monotile), 반도와 동료듀의 최적 속도 몚형곌 슀Ʞ알마의 원형 튾랙 싀험(Phantom Jam), 사토와 테읎튞의 법칙곌 타원곡선의 a_p(Sato-Tate), 칌띌비와 알우의 공간에서 핞슚읎 잘띌 귞늰 5ì°š 페륎마 곡선(Calabi-Yau Quintic), 에덞슀람룚너와 레처, 조몚로디얞의 지속 혞몰로지와 바윔드(Persistence Barcode)예요. 목록 밖에서 닀음에 핮 볌 것읎에요. 믌윔프슀킀의 묌음표 핚수(1904)는 페늬 나묎륌 읎진 나묎로 펮는 계닚읎에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷읎에요. Ꞟ람레슀 표는 소수 말고 닀륞 수엎로도 섞욞 수 있얎요. 였듀늬윔(1993)에 따륎멎 2 닀음에 간격읎 작고 고륎게 흩얎진 홀수가 였는 수엎에서는 1 의 엎읎 서고, 간격읎 크멎 깚젞요. 간격의 크Ʞ륌 녞람로 걞멎 1 의 엎읎 묎너지는 자늬가 볎여요. 핎뚞슬늬 곌정의 점 대신 격자 칞마닀 지수 분포 묎게륌 두멎 마지막 통곌 슀믞Ʞ(로슀튞, 1981)가 되고, 극한 몚양읎 쌍곡선에서 (√x+√y)² 의 겜계로 바뀌얎요. @@ -2737,7 +2802,9 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. 49판에서 볞 것읎에요. 정늬가 수륌 못 박는 대상은 읎번에도 셈윌로 지쌜뎀얎요. DÃŒrer's Unfolding 은 묎작위 귞묌 120,000개가 하나도 겹치지 않았고, 귞묌의 틈읎 데칎륎튞 정늬의 720° 륌 8.0e-13° 안에서 ë§žì·„ì–Žìš”. Kempe Chains 는 259,200 프레임읎 ë‹€ ë„€ 색읎었고, Square Peg 는 1,800 프레임 가욎데 1,799 에서 정사각형 수가 홀수였얎요. 귞늌읎 Ʞ억하는 것은 정늬가 말하는 귞래프가 아니었얎요. Kempe Chains 는 0.4쎈 Ʞ억한 국겜까지 넣은 귞래프에서 11번 막혔고, ê·ž 귞래프는 ê·ž 순간 ë„€ 색윌로 칠핎지지 않았얎요. 귞렀진 지도만 평멎 귞래프였얎요. 찟는 셈읎 놓친 것은 정늬가 틀며 것처럌 볎였얎요. Square Peg 의 첫 판은 600 프레임 가욎데 479 만 홀수였는데, 놓친 정사각형의 92% 가 접힌 옆구늬 속의 3 LED 아래 정사각형읎었얎요. 닫힌 ꌎ의 핎가 셈을 재 죌었얎요. Tennis Racket 의 뒀집힘 사읎 시간은 알윔비 타원 핚수의 2K(k) 로 낾 값곌 상대 였찚 1e-8~3e-7 안에서 같았얎요. 묎늬수륌 녞람로 믞끄러뜚늬멎 부분합읎 흐튞러졌얎요. Curlicue 는 x 륌 1e-6 만 바꿔도 뚌 걞음의 위상읎 π n² dx 만큌 움직여서, x 륌 움직읎는 대신 재규격화 사슬을 걞었얎요. 읎찚 묎늬수의 사슬은 1e-9 안에서 되돌아왔고, e 와 π 의 사슬은 1 의 첚점 가까읎 지나서 틈읎 11~16% 였얎요. 읎웃 찚읎륌 재는 축에서는 LED 의 자늬가 셈을 정했얎요. 같은 점을 귞냥 격자에 찍윌멎 쎘쎘핚읎 쌜진 몫의 0.6 배였고, 첎컀판의 한 색에만 찍자 2.0 배였얎요. -목록 밖에서 31판 뒀에 핮 볌 것읎에요. 몚자 타음(슀믞슀, 마읎얎슀, 캐플런, 굿뚌-슀튞띌우슀, 2023)은 한 가지 타음로만 평멎을 비죌Ʞ로 덮고, 뒀집얎 놓읞 타음읎 판에 드묞드묞 흩얎젞요. 페륎믞-파슀타-욞람-칭구의 되돌아옎(1955)은 KdV Solitons 와 같은 뿌늬띌 같은 판에 두지 않아요. +51판에서 볞 것읎에요. 정늬가 수륌 못 박는 대상은 읎번에도 셈윌로 지쌜뎀얎요. Sato-Tate 의 a_p 는 곡선마닀 p < 400 의 좋은 소수에서 점을 하나하나 섌 값곌 ë‹€ 같았고, 8,192개 소수 얎디에서도 |x_p| 가 1 에 닿지 않아 하섞의 부등식을 지쌰얎요. 복소곱셈 곡선 둘에서는 a_p = 0 읎얎알 하는 소수 4,108개와 4,114개가 ë‹€ 0 읎었얎요. Persistence Barcode 는 표볞 200개의 432개 반지늄에서 였음러 지표와 성분 수, 격자로 섌 구멍 수가 ë‹€ 바윔드와 맞았얎요. Calabi-Yau Quintic 의 z1^5 + z2^5 는 쎘쎘한 격자 2,015,025점에서 1 곌 1.2e-14 안읎었얎요. Hat Monotile 은 치환윌로 지은 몚자 7,921장을 여섯 몚양윌로 LED 에 찍얎 겹친 곳곌 빈 곳을 셌얎요. 셈한 것읎 귞늰 것곌 같아알 했얎요. Persistence Barcode 는 Vietoris-Rips 대신 알파 복합첎륌 썚서, 귞렀진 원판 합집합의 구멍읎 닫히는 순간곌 H1 막대의 끝읎 같아요. 쀄읎 많은 시공간 귞늌은 빗ꞈ읎 됐얎요. Phantom Jam 의 ì°š 선을 2~3 LED 굵Ʞ로 귞윌멎 52칞 폭읎 빗ꞈ윌로 찚서 거꟞로 Ʞ욎 쀄처럌 읜혔고, 1 LED 로 가늘게 하고 자유 흐늄의 선을 얎둡게 하자 빚간 정첎 쀄묎늬가 떠올랐얎요. 둥귌 대상은 섞욎 팚널을 ë‹€ 채우지 못했얎요. Calabi-Yau Quintic 의 조각은 섞 축의 분산읎 ë‹€ 0.458 읎띌 가욎데 64×75 에 서요. 같은 타음링의 변을 두 Ꞟ읎로 나눠 듀얎 올늬멎 몚양만 바뀌었얎요. Hat Monotile 은 ꌭ짓점 49,246개에서 a 변곌 b 변의 합읎 얎느 Ꞟ로 가도 같아서, K4 하나가 혜성 Tile(1, 0)에서 갈맀Ʞ표 Tile(0, 1)까지 Tile(a, b) 가족을 ë‹€ 지나가요. + +목록 밖에서 31판 뒀에 핮 볌 것읎에요. 페륎믞-파슀타-욞람-칭구의 되돌아옎(1955)은 KdV Solitons 와 같은 뿌늬띌 같은 판에 두지 않아요. 목록 밖에서 33판 뒀에 핮 볌 것읎에요. 뷔퐁의 바늘(1777)은 Bertrand Paradox 와 같은 ꌎ로, ꞈ을 넘는 바늘의 몫읎 2/π 로 몚읎는 막대와 핚께 섞욞 수 있얎요. 토칎륎슀킀의 ë°©(1995)은 거욞로 된 닀각형 방에서 한 점의 빛읎 닀륞 한 점에만 닿지 못하는 방읎띌 Penrose Room 의 ꎑ선 추적을 귞대로 썚요. 프늬드뚌곌 슀윔띌(1987)는 둥귌 원 셋윌로는 볎로메였 고늬륌 만듀 수 없닀는 것을 볎였는데, 원을 밀얎 넣을 때 얎디서 부딪히는지가 귞늌읎 돌요. 정렬 넀튞워크의 시간 t 의 순엎을 점윌로 찍윌멎 대각선에서 원판을 지나 반대 대각선윌로 도는 점 구늄읎 돌요. ê·ž 원판읎 2007년 녌묞읎 낎닀볞 아륎킀메데슀 법칙읎에요. 간격읎 2k 읞 소수 쌍의 수는 Goldbach Comet 곌 같은 FFT 로 섞고, k 마닀 하디-늬틀우드 읞수로 갈늬는 쀄Ʞ 귞늌읎 돌요. @@ -2771,6 +2838,8 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. 목록 밖에서 49판 뒀에 핮 볌 것읎에요. 깎은 정읎십멎첎는 묎작위 몚서늬 전개 5,000개 가욎데 마흔 몇 개가 겹쳐서, DÃŒrer's Unfolding 의 셈윌로 겹치는 귞묌곌 겹치지 않는 귞묌을 나란히 펌치멎 셰퍌드의 추잡읎 왜 아직 ì—Žë € 있는지 볎여요. 에너지륌 조ꞈ씩 잃는 굳은 몞은 가장 큰 축윌로 옮겚 가 돌아요. 1958년 익슀플로러 1혞가 휘는 안테나 때묞에 가장 작은 축에서 가장 큰 축윌로 돌아누웠얎요. Tennis Racket 의 적분에 감쇠륌 걞멎 폎혞드륌 걎너 큰 축윌로 낎렀앉는 Ꞟ을 귞늎 수 있얎요. 허버튞 볞(1977)은 곡선 위 점 쌍을 뫌비우슀 띠로 놓아 몚든 조륎당 곡선읎 직사각형을 품는닀는 것을 볎였얎요. Square Peg 의 곡선에서 ê·ž 띠륌 섞워 귞늬멎 띠가 제 몞을 지나는 자늬가 직사각형읎에요. +목록 밖에서 51판 뒀에 핮 볌 것읎에요. 슀펙터(슀믞슀, 마읎얎슀, 캐플런, 굿뚌-슀튞띌우슀, 2023)는 ê±°ìšž 없읎도 되풀읎하지 않는 타음읎띌, ê·ž 치환윌로 Hat Monotile 의 덩읎 묌결을 돌늬멎 죌황 ê±°ìšž 몚자가 없는 짝읎 돌요. 종수 2 곡선의 a_p 륌 Sato-Tate 의 빗방욞로 떚얎뜚늬멎 반원 대신 USp(4) 대각합의 돔읎 쌓여서, 같은 법칙읎 닀륞 몚양을 채우는 것읎 볎여요. Calabi-Yau Quintic 의 조각 귞늬Ʞ륌 찚수 n 을 녞람에 걎 페륎마 곡선 z1^n + z2^n = 1 로 넓히멎 조각 n² 개와 종수 (n−1)(n−2)/2 가 핚께 바뀌얎요. Persistence Barcode 의 알파 복합첎륌 구 위의 점읎나 토러슀 닚멎윌로 옮Ʞ멎 H2 막대까지 서요. 두 ì°šì„  고늬에서 찚선을 바꟞는 찚는 Phantom Jam 의 정첎륌 옆 찚선윌로 옮겚서, 시공간 귞늌 둘을 나란히 섞욞 수 있얎요. + ### 화장품 팝업 죌제 6 윌로 쓎 질감곌 닀음에 핮 볌 질감읎에요. 29판읎 처음 넷을 썌얎요. 팔레튾 나읎프로 펮 바륎는 크늌(Cream Swirl), 구슬읎 뜬 저곌 빛 귞묌(Serum Gel), 쏟아젞 쌓읎는 Ꞁ늬터(Glitter Fall), 퍌프가 눌렀닀 든 압축 파우더(Loose Powder)예요. 넷 ë‹€ K1 읎 색상환 한 바퀎띌서, 색을 돌늬멎 죌황곌 녞랑읎 갈색읎나 올늬람로 볎읎는 자늬가 있었얎요. Serum Gel 은 ê·ž 둘레에서만 밝Ʞ륌 0.62 와 0.3 에서 0.92 와 0.68 로 올렞얎요. 밝은 바탕은 반짝임을 지웠얎요. 200 위에 55 륌 더한 반짝임은 거의 안 볎여서 Glitter Fall 은 얎두욎 바탕에 섰고, 흰 바탕곌 ꞈ색 한 쀄은 Loose Powder 의 아래 띠로만 썌얎요. 크늌곌 저은 가장자늬 0.028 곌 0.079 로 연구에서 가장 번지는 귞늌읎에요. ê·ž 둘읎 가장자늬 축의 아래 끝을 옮겚서 지도에 새 빈 칞읎 났얎요. 크늌은 빛만 움직여서는 움직임읎 0.019 에 뚞묌렀얎요. 결읎 깊얎알 빛읎 돌 때 바뀌는 LED 가 생겚요. 비누막읎 터질 때 구멍 가장자늬가 테음러와 컬늭의 속도(1959, 1960)로 고륎게 벌얎지며 묌방욞 테륌 낚Ʞ는 것은 Soap Film 의 끝을 늘읞 귞늌읎에요. 큎렌징 였음읎 묌을 만나 가장자늬부터 우윳빛윌로 유화되는 것은 Blotting Paper 처럌 LED 마닀 한 번만 바뀌는 셈윌로 귞늎 만핎요. @@ -2849,7 +2918,11 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. 49판에서 볞 것읎에요. 몞 둘의 거늬장을 통짞로 섞윌멎 팔닀늬가 녹았얎요. Werewolf 는 사람곌 늑대의 거늬장을 섞자 팔읎 늘얎나지 않고 닀늬 속윌로 녹아듀얎서, 댈대 하나의 댈륌 옮Ʞ고 뚞늬만 거늬장 둘을 섞었얎요. 1~2 LED 의 작은 것은 읎번에도 닀륞 것윌로 읜혔얎요. Gargoyle 의 뚌 탑에 쌠 찜은 죌황 막대였고 Ꞟ의 쀄묎늬는 쇠찜삎읎었얎요. Werewolf 의 빛 테 두륞 바위는 탁자였고, Ꞟ읎 0 읞 귀는 몚자챙읎었얎요. Music Box 의 믞뉎에튞 검은 치마는 달빛 앞에서 죌황 고늬로 볎였얎요. 아래로 꺟읎는 묌은 가로 폭을 지쌰얎요. Gargoyle 의 묌쀄Ʞ는 입에서 굵게 낎볎낎도 가욎데 3분의 1 에서 1.5~2 LED 여서, 출발점곌 옆 빠륎Ʞ륌 흔듀얎 굵혔얎요. 검은 것은 읎번에도 밝은 것 곁에서만 읜혀서, Party Lights 의 종읎 박쥐는 쌜진 전구 곁에서만 볎여요. 멈추는 몚습은 멈출 때륌 셈핎알 했얎요. Music Box 의 발레늬나륌 조향윌로 섞우자 871 프레임에서 거꟞로 돌았고, 저절로 느렀지는 Ꞟ읎가 ꌭ 한 바퀎로 끝나게 풀늬는 시간을 고륎자 0 읎 됐얎요. 귞늌자가 녞람륌 삎렞얎요. Pit and Pendulum 의 칌날은 제 넓읎만 바꿔서 녞람가 0.04 였고, 횃불을 낎렀 칌날 귞늌자륌 밝은 벜에 크게 던지자 Speed 가 0.098 읎 됐얎요. -49판 뒀에 핮 볌 것읎에요. Werewolf 의 댈대 옮ꞰꞰ로 흡혈귀가 망토륌 펎며 박쥐로 쀄얎드는 변신을 귞늎 수 있얎요. Pit and Pendulum 의 귞늌자 던지Ʞ로는 쎛불 든 손읎 나선 계닚을 였륎며 벜의 귞늌자륌 늘읎는 장멎을 섞욎 팚널의 높읎로 귞늎 만핎요. +51판읎 닀섯을 더 ê·žë žì–Žìš”. 핮 질 녘 찜턱에서 망토륌 펎고 박쥐로 쀄얎드는 흡혈귀(Bat Form), 쎛불 듀고 둥귌 탑의 나선 계닚을 였륎는 두걎 쓎 사람(Tower Stair), 혾박 등 뚞늬가 용수철 끝에서 튀얎나였는 깜짝 상자(Jack-in-the-Box), 안개 낀 시곚Ꞟ로 닀가였는 좀비 ë–Œ(Zombie Shamble), 바람읎 책장을 넘Ʞ멎 읞장읎 귞렀지고 Ꞁ자가 날아였륎는 마법서(Grimoire)예요. 흡혈귀와 나선 계닚은 49판 뒀에 적혀 있던 감읎에요. 움직임은 Zombie Shamble 의 0.224 에서 Jack-in-the-Box 의 0.394, 복잡도는 Jack-in-the-Box 의 0.244 에서 Zombie Shamble 의 2.005 예요. + +51판에서 볞 것읎에요. 싀룚엣은 읎번에도 밝은 것 앞에서만 읜혔고, 작은 검은 것은 큰 검은 것에 붙었얎요. Bat Form 은 볎늄달 대신 탑 방의 런족한 ì°œ 너빾 핮 질 녘 앞에 흡혈귀륌 섞웠는데, ë–Œ 박쥐가 뚞늬 옆을 지나멎 검은 몞에 붙얎 뚞늬가 둘로 볎여서 뚌 박쥐륌 하늘빛윌로 조ꞈ 흐늬게 칠했얎요. 망토의 닚읎 날개 뒷전읎 되는 댈대 하나로 Werewolf 의 자섞 옮ꞰꞰ륌 귞대로 썌고, 팔꿈치와 손목을 곧게 옮Ʞ멎 망토가 상자처럌 펎젞서 팔은 각도로 돌렞얎요. 사묌의 ꌎ은 읎번에도 닀륞 것윌로 읜혔얎요. Zombie Shamble 의 옆윌로 벌늰 팔은 멀늬서 탁자, 가까읎서 허수아비였고, Jack-in-the-Box 의 잔 마늄몚는 죌황 십자 점읎었얎요. 녾란 얌룩읎던 혾박 얌굎은 구멍 위에 얎두욎 칌자국 쀄을 넣얎서알 웃는 얌굎읎 됐얎요. 귞늌자는 크게 던질수록 덜 읜히Ʞ도 했얎요. Tower Stair 의 쎛불을 몞 앞에 듀멎 귞늌자가 ë„€ 배로 컀젞 천장에 잘늰 덩얎늬가 돌서, 2.5배쯀윌로 쀄였얎요. 반쯀 넘얎간 책장은 바로 앞에서 볎멎 선 하나로 사띌젞서, Grimoire 는 볎는 방향을 조ꞈ 비ꌈ얎요. 부드러욎 칞에서는 굵은 몚양만 낚았얎요. Zombie Shamble 은 1 LED 알갱읎가 ±19 닚읎띌 25 당 아래의 묎늬가 묻혀서, Ꞟ의 바퀮 자국을 빛을 비추는 젖은 쐐Ʞ로 바꿚얎요. + +51판 뒀에 핮 볌 것읎에요. Bat Form 의 찜턱에서 박쥐 떌가 한데 몚여 흡혈귀의 몞읎 되는 반대 방향의 변신을 귞늎 만핎요. Jack-in-the-Box 의 뚜껑읎 터질 때 뚞늬 대신 박쥐 떌나 유령읎 쏟아젞 나였게 하멎, ì—Ž 때마닀 나였는 것읎 바뀌는 상자가 돌요. Tower Stair 의 나선 계닚을 ꌭ대Ʞ에서 아래로 낎렀닀볎멎 쎛불읎 소용돌읎처럌 멀얎지며 잵마닀 귞늌자가 겹쳐 돌아요. Grimoire 의 Ꞁ자듀읎 찜밖윌로 날아가 밀하늘에 별자늬로 몚읎고, ê·ž 별자늬가 닀음 장의 읞장읎 되는 판도 있얎요. Zombie Shamble 의 떌륌 위에서 낎렀닀볞 곚목윌로 옮겚 헬빙의 사회적 힘 몚형(1995)윌로 걷게 하멎, 쎈록 눈 점듀읎 곚목을 메우닀 횃불 앞에서 갈띌젞요. ## 닀시 돌늬Ʞ From bed173d5b676e4687241e00e5bdb3466a54fd924 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 00:17:34 +0000 Subject: [PATCH 8/8] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=205?= =?UTF-8?q?1=ED=8C=90=20=E2=80=94=20index.html=20=EB=8B=A4=EC=8B=9C=20?= =?UTF-8?q?=EC=A7=93=EA=B8=B0?= 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_016styVvYJeAE1svDoUBeGKX --- incoming/pattern-study/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index ca8ec4775..3ef71484d 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

팹턮 연구

- - + +