From a6fed31a05aa9307bcfa836d06c04e6c30f23f34 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 14:35:24 +0000 Subject: [PATCH 01/10] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=2033=ED=8C=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117bhD83MGCSerAR798cUib From 1427f7b5de059603c968459e0d0caa970e721a66 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 15:11:16 +0000 Subject: [PATCH 02/10] pattern-study: Borromean Rings, Peel-Off Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117bhD83MGCSerAR798cUib --- .../pattern-study/patterns/borromean-rings.js | 264 +++++++++++++++ incoming/pattern-study/patterns/peel-off.js | 310 ++++++++++++++++++ 2 files changed, 574 insertions(+) create mode 100644 incoming/pattern-study/patterns/borromean-rings.js create mode 100644 incoming/pattern-study/patterns/peel-off.js diff --git a/incoming/pattern-study/patterns/borromean-rings.js b/incoming/pattern-study/patterns/borromean-rings.js new file mode 100644 index 000000000..fcad7487d --- /dev/null +++ b/incoming/pattern-study/patterns/borromean-rings.js @@ -0,0 +1,264 @@ +// ===== Patternflow pattern ===== +// Title: Borromean Rings +// Family: topology +// Idea: Three lit tubes bent into the golden-rectangle model of the Borromean rings, stretched to 2.3 times their height to stand on the panel, turn slowly round the tall one's axis so the over and under crossings come round in turn, and no two of them are linked: take one away and the other two slide apart. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Borromean rings (Peter Guthrie Tait, 1876), pattern round 33 +// Round: 33 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Aspect=1.618..2.4, Tube=0..1 +// +// Knob 1: Hue. Turns the three colours round the wheel together, a third apart. At 0 the tall ring is azure, the ring across it pink and the flat ring round the waist lime; a ring that comes round to orange or yellow is laid paler, peach or butter, so its shaded side does not go brown. +// Knob 2: Speed. How fast the rings turn. At 2 one turn takes 20 s and the view nods up and down every 27 s; at 0.2 ten times slower. +// Knob 3: Aspect. Long side over short side of the rectangles before the stretch. At 1.618 they are golden and their 12 corners would be an icosahedron; at 2.4 they are longer and thinner and the room where each passes through the next grows from 0.62 to 1 short side. +// Knob 4: Tube. How much of that room the tubes fill: 29% at 0, so there are about three LEDs of air where one ring passes through the next; at 1, 45%, and they pass within a LED of each other without touching. +// Buttons: K1 tap fades out the ring across the tall one (pink at the opening). With it gone the flat ring slides down the tall ring and off its bottom end in 3 s, since the two were never linked, waits below it, climbs back, and the third ring returns to lock them again; 7.4 s in all. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, ceil, sqrt, sin, cos, abs, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (v) => ((v = cl(v, 0, 1)), v * v * (3 - 2 * v)); + +// The model, short side a = 1. Each ring lies in one coordinate plane: +// R1 in x = 0, half sides z1 = r and y1 = SIG, +// R2 in z = 0, half sides x2 = 1 and y2 = SIG r (the tall one), +// R3 in y = 0, half sides x3 = r and z3 = 1 (the flat one round the waist). +// Unstretched (SIG = 1, r = phi) these are Tait's three golden rectangles. +// The link only asks y1 < y2, x2 < x3 and z3 < z1: along each axis one ring +// passes through the next. Stretching along y keeps all three, so the model +// can be drawn 2.3 times as tall, which the 64 x 128 panel needs: unstretched +// it fits a cube and would light a band about 50 rows high. +const SIG = 2.3; +// Corners rounded to 0.4 of the short side, the camera 12 units away. +const RC = 0.4, CAM = 12; +// Light from the upper left and in front, and the half vector for the shine. +const LX = -0.48, LY = -0.62, LZ = -0.62, LN = 1 / sqrt(LX * LX + LY * LY + LZ * LZ); +const lx = LX * LN, ly = LY * LN, lz = LZ * LN; +const HX = lx, HY = ly, HZ = lz - 1, HN = 1 / sqrt(HX * HX + HY * HY + HZ * HZ); +const hx = HX * HN, hy = HY * HN, hz = HZ * HN; + +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; +} + +// Points round a rounded rectangle with half sides A (along u) and B (along +// v), about one every `step`, as (u, v) pairs. Four sides and four arcs. +function loop(A, B, c, step, out) { + let n = 0; + const side = (u0, v0, u1, v1) => { + const len = sqrt((u1 - u0) ** 2 + (v1 - v0) ** 2), k = max(1, ceil(len / step)); + for (let i = 0; i < k; i++) (out[n++] = u0 + ((u1 - u0) * i) / k), (out[n++] = v0 + ((v1 - v0) * i) / k); + }; + const arc = (cu, cv, a0) => { + const k = max(2, ceil((c * PI) / 2 / step)); + for (let i = 0; i < k; i++) { + const a = a0 + ((PI / 2) * i) / k; + out[n++] = cu + c * cos(a); + out[n++] = cv + c * sin(a); + } + }; + const a = A - c, b = B - c; + side(A, -b, A, b); + arc(a, b, 0); + side(a, B, -a, B); + arc(-a, b, PI / 2); + side(-A, b, -A, -b); + arc(-a, -b, PI); + side(-a, -B, a, -B); + arc(a, -b, 1.5 * PI); + return n >> 1; +} + +export function setup(params) { + params.s = { + turn: 0.6, nod: 0, hue: 0, speed: 2, r: 1.618, tube: 0.06, cut: -1, + z: [new Float32Array(N), new Float32Array(N), new Float32Array(N)], + c: [new Float32Array(N * 3), new Float32Array(N * 3), new Float32Array(N * 3)], + fid: new Int8Array(N), fz: new Float32Array(N), uv: new Float32Array(8192), pts: new Float32Array(16384), col: new Float32Array(9), + }; +} + +// K1 tap, seconds: R1 fades 0 to 0.8, the other two slide apart to 3.0, +// hold to 4.4, slide back to 6.6, R1 returns by 7.4. +const T_END = 7.4; +function cutState(t) { + if (t < 0) return [1, 0, 0]; + const fade = t < 0.8 ? 1 - sm(t / 0.8) : t < 6.6 ? 0 : sm((t - 6.6) / 0.8); + const sep = t < 0.8 ? 0 : t < 3 ? sm((t - 0.8) / 2.2) : t < 4.4 ? 1 : t < 6.6 ? 1 - sm((t - 4.4) / 2.2) : 0; + return [fade, sep, sm(t / 1.2) * (1 - sm((t - 6.2) / 1.2))]; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 1.618, 0.06], d = cl(dt || 0, 0, 0.1); + P.hue = k[0]; + P.speed = cl(k[1], 0.2, 3); + P.r = cl(k[2], 1.618, 2.4); + P.tube = cl(k[3], 0, 1); + P.turn += d * P.speed * 0.157; + P.nod += d * P.speed * 0.116; + if (P.cut >= 0) { + P.cut += d; + if (P.cut > T_END) P.cut = -1; + } + if (input.btnPressed && input.btnPressed[0] && P.cut < 0) P.cut = 0; +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const r = P.r, ZB = P.z, CB = P.c, uv = P.uv, pts = P.pts, col = P.col, fid = P.fid, fz = P.fz; + // The room where a ring passes through the next: one short side, or r - 1 + // of it where the long side is shorter than two. + const room = min(1, r - 1), tr = (0.29 + 0.16 * P.tube) * room; + // LEDs to a unit: the flat ring's corners swing out to rxz from the axis, + // and the panel is 64 across; the tall ring is SIG r high each side of the + // middle, and the panel is 128 tall. + const rxz = sqrt((r - RC) ** 2 + (1 - RC) ** 2) + RC + tr; + const S = min(27 / rxz, 55 / (SIG * r + tr)); + const [fade, sep, low] = cutState(P.cut); + // With R1 gone the flat ring drops down the tall ring until it + // clears the bottom end with 0.6 of a unit of air between the tubes, and + // the tall ring rises a little to meet it. + const drop = sep * (SIG * r + 2 * tr + 0.6); + const up2 = drop * 0.2, down3 = drop * 0.8; + + // The rings turn round the tall ring's axis and the view looks down on them + // from 14 to 30 degrees, so the flat ring always shows its hole. While the + // rings come apart the eye sinks to 7 degrees below the middle, near the + // height the flat ring drops to, so the air between it and the bottom of + // the tall ring shows. + const th = P.turn, ct = cos(th), st = sin(th); + const el = (0.38 + 0.14 * sin(P.nod)) * (1 - low) - 0.12 * low, ce = cos(el), se = sin(el); + + for (let i = 0; i < 9; i += 3) hsv(P.hue + 0.58 + (i / 9), 0.8, 1, col, i); + // Round orange and yellow the colour goes brown in the shade, so there it + // keeps less saturation and more light. + const warm = [0, 0, 0]; + for (let i = 0; i < 3; i++) { + let h = P.hue + 0.58 + i / 3; + h -= floor(h); + let dh = h - 0.12; + if (dh > 0.5) dh -= 1; + warm[i] = sm(1 - abs(dh) / 0.17); + for (let c = 0; c < 3; c++) col[i * 3 + c] += (1 - col[i * 3 + c]) * 0.45 * warm[i]; + } + + // Ring order in the buffers: 0 the tall ring (R2), 1 the one across it (R1), 2 the flat one (R3). + for (let ring = 0; ring < 3; ring++) { + const zb = ZB[ring], cb = CB[ring]; + zb.fill(1e9); + let A, B; + if (ring === 0) (A = 1), (B = SIG * r); + else if (ring === 1) (A = r), (B = SIG); + else (A = r), (B = 1); + const n = loop(A, B, RC * min(A, B), tr * 0.3, uv); + const cr = col[ring * 3], cg = col[ring * 3 + 1], cbl = col[ring * 3 + 2], wl = warm[ring]; + // Every point on screen first, so each can see its neighbours for the + // tube's direction. + for (let i = 0; i < n; i++) { + const u = uv[i * 2], v = uv[i * 2 + 1]; + let X, Y, Z; + if (ring === 0) (X = u), (Y = v + up2), (Z = 0); + else if (ring === 1) (X = 0), (Y = v), (Z = u); + else (X = u), (Y = -down3), (Z = v); + // Turn about y, then tip the view down by el. + const xr = X * ct + Z * st, zr = -X * st + Z * ct; + const up = Y * ce + zr * se, dep = zr * ce - Y * se; + const sc = (S * CAM) / (CAM + dep); + pts[i * 4] = 31.5 + xr * sc; + pts[i * 4 + 1] = 63.5 - up * sc; + pts[i * 4 + 2] = dep * S; + pts[i * 4 + 3] = tr * sc; + } + for (let i = 0; i < n; i++) { + const o = i * 4, sx = pts[o], sy = pts[o + 1], dz = pts[o + 2], rp = pts[o + 3]; + const a = ((i + 1) % n) * 4, b = ((i + n - 1) % n) * 4; + let tx = pts[a] - pts[b], ty = pts[a + 1] - pts[b + 1], tz = pts[a + 2] - pts[b + 2]; + const tl = 1 / (sqrt(tx * tx + ty * ty + tz * tz) + 1e-9); + (tx *= tl), (ty *= tl), (tz *= tl); + // Nearer is brighter: a quarter darker at the back of the rings. + const fog = 1 - 0.25 * cl((dz / S + 2) / 4, 0, 1); + const x0 = max(0, floor(sx - rp)), x1 = min(W - 1, ceil(sx + rp)), y0 = max(0, floor(sy - rp)), y1 = min(H - 1, ceil(sy + rp)); + const r2 = rp * rp; + for (let y = y0; y <= y1; y++) { + const ny = y - sy, ny2 = ny * ny; + for (let x = x0; x <= x1; x++) { + const nx = x - sx, d2 = nx * nx + ny2; + if (d2 >= r2) continue; + const nzp = sqrt(r2 - d2), z = dz - nzp, p = y * W + x; + if (z >= zb[p]) continue; + zb[p] = z; + // The tube is drawn as a chain of spheres, but the normal is the + // tube's: the sphere's, less its part along the tube. + const dt = nx * tx + ny * ty - nzp * tz; + let Nx = nx - dt * tx, Ny = ny - dt * ty, Nz = -nzp - dt * tz; + const nl = 1 / (sqrt(Nx * Nx + Ny * Ny + Nz * Nz) + 1e-9); + (Nx *= nl), (Ny *= nl), (Nz *= nl); + const dif = max(0, Nx * lx + Ny * ly + Nz * lz); + const hv = max(0, Nx * hx + Ny * hy + Nz * hz), sp = hv ** 24 * 0.85; + const lit = (0.2 + 0.3 * wl + 0.8 * dif) * fog; + cb[p * 3] = cr * lit + sp; + cb[p * 3 + 1] = cg * lit + sp; + cb[p * 3 + 2] = cbl * lit + sp; + } + } + } + } + + // The nearest solid ring at each LED, for the gaps. + const al = [1, fade, 1]; + for (let p = 0; p < N; p++) { + let best = -1, bz = 1e9; + for (let ring = 0; ring < 3; ring++) if (al[ring] > 0.5 && ZB[ring][p] < bz) (bz = ZB[ring][p]), (best = ring); + fid[p] = best; + fz[p] = bz; + } + const gapDepth = tr * S * 1.1; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + // Layers front to back: R1 fades by its alpha. + const z0 = ZB[0][p], z1 = ZB[1][p], z2 = ZB[2][p]; + let R = 0, G = 0, Bc = 0; + if (z0 < 1e8 || z1 < 1e8 || z2 < 1e8) { + const ord = z0 <= z1 ? (z1 <= z2 ? [0, 1, 2] : z0 <= z2 ? [0, 2, 1] : [2, 0, 1]) : z0 <= z2 ? [1, 0, 2] : z1 <= z2 ? [1, 2, 0] : [2, 1, 0]; + let keep = 1; + for (let j = 0; j < 3 && keep > 0.001; j++) { + const ring = ord[j]; + if (ZB[ring][p] > 1e8) break; + const a = al[ring] * keep, cb = CB[ring]; + R += cb[p * 3] * a; + G += cb[p * 3 + 1] * a; + Bc += cb[p * 3 + 2] * a; + keep -= a; + } + // A dark LED where a nearer tube of another ring passes over this + // one, so the crossing reads as over and under. + const me = fid[p]; + if (me >= 0) { + const zme = fz[p]; + let dark = 0; + for (let dy = -1; dy <= 1; dy++) { + const yy = y + dy; + if (yy < 0 || yy >= H) continue; + for (let dx = -1; dx <= 1; dx++) { + const xx = x + dx; + if (xx < 0 || xx >= W || (dx === 0 && dy === 0)) continue; + const q = yy * W + xx, o = fid[q]; + if (o >= 0 && o !== me && fz[q] < zme - gapDepth) dark = 1; + } + } + if (dark) (R *= 0.12), (G *= 0.12), (Bc *= 0.12); + } + } + display.setPixel(x, y, cl(R * 255, 0, 255), cl(G * 255, 0, 255), cl(Bc * 255, 0, 255)); + } +} diff --git a/incoming/pattern-study/patterns/peel-off.js b/incoming/pattern-study/patterns/peel-off.js new file mode 100644 index 000000000..46c931215 --- /dev/null +++ b/incoming/pattern-study/patterns/peel-off.js @@ -0,0 +1,310 @@ +// ===== Patternflow pattern ===== +// Title: Peel-Off +// Family: cosmetic +// Idea: A glossy tinted gel film covers a pearly skin-toned base and an unseen hand peels it from the top edge down, the film lifting into a rolling curl with a bright rim and its paler underside out, the peel line wavering as the film catches, stretches and snaps free, until the sheet leaves the bottom and a wet new coat flows down from the top and dries to a soft satin for the next peel. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 33, after a peel-off gel mask lifted off in one sheet +// Round: 33 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Thickness=0..1, Curl=0..1 +// +// Knob 1: Hue. The gel's colour, all the way round the wheel from violet at 0; the curl shows it paler. From red round to lime the gel is laid lighter than the skin, coral, peach, butter and pale lime, because at the violet's depth those hues read as brick, tan, khaki and olive. +// Knob 2: Speed. How fast it all runs. At 2 the peel crosses the 128 rows in 7.5 s, the bare skin shows for 1.2 s, the new coat flows down in 2.4 s and each row of it dries in about 3, 15 s round; at 0.2 ten times slower. +// Knob 3: Thickness. How thick the gel is laid. At 0 a sheer film that lets the pearl of the skin show through it; at 1 a deep, nearly opaque coat with a stronger shine. +// Knob 4: Curl. How tight the film rolls as it comes away: at 0 a roll 9 rows across at the top that grows to 14 as it gathers the sheet on its way down; at 1, 26 growing to 40. +// Buttons: K1 tap starts the peel at once; on bare skin it lays the new coat first and peels as soon as the coat is down. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, sqrt, sin, cos, exp, abs, atan2, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (v) => ((v = cl(v, 0, 1)), v * v * (3 - 2 * v)); +const hash = (i) => { + let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; + +// Smooth noise in a 128 x 128 tile that wraps, octaves of 8, 16 and 32 +// cells, built once: the pearl of the skin and the gel's uneven thickness. +const T = 128, TM = T - 1; +const TEX = new Float32Array(T * T); +function buildTex() { + TEX.fill(0); + let amp = 1, tot = 0; + for (let o = 0; o < 3; o++) { + const cells = 8 << o, size = T / cells, g = new Float32Array(cells * cells); + for (let i = 0; i < g.length; i++) g[i] = hash(i * 7 + o * 9973) * 2 - 1; + for (let y = 0; y < T; y++) { + const gy = floor(y / size), fy = sm(y / size - gy), y1 = (gy + 1) % cells; + for (let x = 0; x < T; x++) { + const gx = floor(x / size), fx = sm(x / size - gx), x1 = (gx + 1) % cells; + const a = g[gy * cells + gx], b = g[gy * cells + x1], c = g[y1 * cells + gx], d = g[y1 * cells + x1]; + const top = a + (b - a) * fx; + TEX[y * T + x] += (top + (c + (d - c) * fx - top) * fy) * amp; + } + } + tot += amp; + amp *= 0.5; + } + for (let i = 0; i < TEX.length; i++) TEX[i] /= tot; +} +function tap(u, v) { + const fu = floor(u), fv = floor(v), x0 = fu & TM, y0 = fv & TM, x1 = (x0 + 1) & TM, y1 = (y0 + 1) & TM; + const tx = u - fu, ty = v - fv; + const a = TEX[y0 * T + x0], b = TEX[y0 * T + x1], c = TEX[y1 * T + x0], d = TEX[y1 * T + x1]; + return a + (b - a) * tx + (c - a) * ty + (a - b - c + d) * tx * ty; +} + +function hsv(h, s, v, out) { + const f = (n) => { + const k = (n + (h - floor(h)) * 6) % 6; + return v - v * s * max(0, min(k, 4 - k, 1)); + }; + out[0] = f(5); + out[1] = f(3); + out[2] = f(1); +} + +// Phases: the film peels, the skin shows bare, a new coat flows on, it dries. +const PEEL = 0, BARE = 1, COAT = 2, DRY = 3; +const PEEL_T = 7.5, BARE_T = 1.2, COAT_T = 2.4, DRY_T = 3.8; + +// The light: above and a little left, in front. +const LX = -0.15, LY = -0.75, LZ = -0.64, LN = 1 / sqrt(LX * LX + LY * LY + LZ * LZ); +const HX = LX * LN, HY = LY * LN, HZ = LZ * LN - 1, HN = 1 / sqrt(HX * HX + HY * HY + HZ * HZ); + +export function setup(params) { + buildTex(); + params.s = { + t: 0, hue: 0, speed: 2, thick: 0, curl: 0.06, + phase: PEEL, pt: 0, coatAge: 99, queue: false, + yf: new Float32Array(W), rr: new Float32Array(W), roll: 0, R0: 6, + unc: new Float32Array(N).fill(-1), + sticks: [{ x: 20, w: 9, a: 4, age: -0.8, len: 1.2 }, { x: 46, w: 7, a: 3.5, age: -2.6, len: 1.0 }], + tint: new Float32Array(3), warm: 0, lastHue: NaN, + }; +} + +// Where the coat's leading edge is, pt seconds into the coat: it flows fast +// from the top and slows as it reaches the bottom. +const coatFront = (pt) => -4 + 136 * (1 - (1 - min(1, pt / COAT_T)) ** 2); +// When it passed row y, for how wet that row still is. +const coatPassed = (y) => COAT_T * (1 - sqrt(cl(1 - (y + 4) / 136, 0, 1))); + +// The peel line: it starts above the top edge and leaves below the bottom. +function peelY(P, pr) { + const span = H + 2.6 * P.R0 + 6, e = pr < 0.08 ? (pr * pr) / 0.16 : pr - 0.04; + return -1.3 * P.R0 - 2 + (span * e) / 0.96; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 0.06], d0 = cl(dt || 0, 0, 0.1); + P.hue = k[0]; + P.speed = cl(k[1], 0.2, 3); + P.thick = cl(k[2], 0, 1); + P.curl = cl(k[3], 0, 1); + P.R0 = 5.5 + 10 * P.curl; + const d = (d0 * P.speed) / 2; + P.t += d; + P.pt += d; + P.coatAge += d; + + if (input.btnPressed && input.btnPressed[0]) { + if (P.phase === DRY) (P.phase = PEEL), (P.pt = 0), P.unc.fill(-1); + else if (P.phase === BARE) (P.phase = COAT), (P.pt = 0), (P.coatAge = 0), (P.queue = true); + else if (P.phase === COAT) P.queue = true; + } + if (P.phase === PEEL && P.pt >= PEEL_T) (P.phase = BARE), (P.pt = 0); + else if (P.phase === BARE && P.pt >= BARE_T) (P.phase = COAT), (P.pt = 0), (P.coatAge = 0); + else if (P.phase === COAT && P.pt >= COAT_T) { + P.phase = P.queue ? PEEL : DRY; + P.pt = 0; + P.queue = false; + if (P.phase === PEEL) P.unc.fill(-1); + } else if (P.phase === DRY && P.pt >= DRY_T) (P.phase = PEEL), (P.pt = 0), P.unc.fill(-1); + + // Spots where the gel holds on. Each one holds the peel line back for about + // a second while the film stretches, up to 5 rows, then lets go: the line + // snaps forward past straight and settles in a fifth of a second. + for (const s of P.sticks) { + s.age += d; + if (s.age > s.len + 0.35) { + s.age = -0.3 - 1.4 * Math.random(); + s.x = 6 + Math.random() * 52; + s.w = 6 + Math.random() * 6; + s.a = 2.5 + Math.random() * 3; + s.len = 0.8 + Math.random() * 0.6; + } + } + if (P.phase === PEEL) { + const pr = P.pt / PEEL_T, Y = peelY(P, pr), t = P.t; + // The roll gathers the sheet and grows by half on its way down. + const R = P.R0 * (0.85 + 0.45 * pr); + P.roll = Y; + for (let x = 0; x < W; x++) { + let y = Y + 1.3 * sin(0.11 * x + 0.9 * t) + 0.8 * sin(0.27 * x - 1.4 * t + 1) + 1.2 * tap(x * 0.9, t * 3); + for (const s of P.sticks) { + if (s.age < 0) continue; + const g = exp(-(((x - s.x) / s.w) ** 2)); + let lag; + if (s.age < s.len) lag = s.a * (s.age / s.len) ** 1.5; + else { + const q = s.age - s.len; + lag = s.a * exp(-q / 0.07) * cos(q * 26); + } + y -= lag * g; + } + P.yf[x] = y; + P.rr[x] = R * (1 + 0.07 * sin(0.2 * x + 0.5 * t)); + // Skin the peel has just passed over keeps a fresh glow for a while. + const top = min(H, max(0, floor(y))); + for (let yy = 0; yy < top; yy++) { + const p = yy * W + x; + if (P.unc[p] < 0) P.unc[p] = t; + } + } + } +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const t = P.t, ph = P.phase, k3 = P.thick, tint = P.tint; + if (P.lastHue !== P.hue) { + // From red through orange and gold to lime the gel is lifted towards + // coral, peach, butter and pale lime: less saturation and more light than + // the skin, so it reads as a lit jelly. At 0.6 of full these hues read as + // brick, tan, khaki and olive. + let h = P.hue + 0.78; + h -= floor(h); + let dh = h - 0.13; + if (dh > 0.5) dh -= 1; + P.warm = sm(1 - abs(dh) / 0.21); + hsv(h, 0.68 - 0.28 * P.warm, 1, tint); + P.lastHue = P.hue; + } + const warm = P.warm, tr = tint[0], tg = tint[1], tb = tint[2]; + // The gel's own colour at the start knobs is about 0.6 of full: deep enough + // that the peel reads as a change of light, not only of colour. + const deepV = 0.6 + 0.65 * warm - 0.14 * k3 * (1 - warm), op = 0.55 + 0.4 * k3; + const cf = ph === COAT ? coatFront(P.pt) : 0; + // A strip light's reflection drifts slowly across the gel. + const sweep = 52 + 34 * sin(t * 0.21); + // The broad glow on the skin wanders. + const gx = 32 + 20 * sin(t * 0.17), gy = 64 + 40 * sin(t * 0.11 + 1); + + for (let x = 0; x < W; x++) { + const yf = P.yf[x], R = P.rr[x], wob = 1.5 * sin(0.2 * x + 3 * P.pt) + sin(0.37 * x - 2 * P.pt); + for (let y = 0; y < H; y++) { + const p = y * W + x; + // Is there gel here, and how wet is it? + let film; + if (ph === PEEL) film = y > yf; + else if (ph === DRY) film = true; + else if (ph === BARE) film = false; + else film = y < cf + wob; + const wet = film ? exp(-max(0, P.coatAge - coatPassed(y)) / 1.4) : 0; + + // The skin: a warm pearl whose sheen turns faintly pink, lilac and mint + // across the panel, a broad soft glow that wanders, fine shimmer, and a + // fresh glow where the film has just come off. + const q = 0.5 * tap(x * 0.45 + 7, y * 0.45 + t * 0.8) + (x * 0.6 + y) / 170 + t * 0.03; + const gl = exp(-((x - gx) ** 2 + (y - gy) ** 2 * 0.3) / 1300); + const fr = P.unc[p] >= 0 ? exp(-(t - P.unc[p]) / 2.2) : 0; + const L = 0.8 + 0.13 * gl + 0.1 * fr + 0.02 * (hash(p) - 0.5); + let r = (1 + 0.035 * cos(TAU * q) + 0.03 * fr) * L; + let g = (0.875 + 0.03 * cos(TAU * (q - 0.33))) * L; + let b = (0.83 + 0.04 * cos(TAU * (q - 0.67)) + 0.015 * fr) * L; + const hs = hash(p + 101); + if (hs < 0.022) { + const tw = max(0, sin(t * (2.4 + 40 * hs) + hash(p + 5) * 40)) ** 10 * (0.4 + 0.3 * gl); + r += tw; + g += tw; + b += tw; + } + + if (film) { + // The gel: its own deep colour over the skin seen through it, uneven + // in thickness, deeper while wet. Wet, the strip light shows as a + // sharp band that the coat's ripples bend; dry, a broad soft sheen + // over a fine satin grain. + const th = tap(x * 0.8 + 60, y * 0.8 + 31); + const dv = deepV * (1 + (0.2 * th - 0.1 * wet) * (1 - 0.7 * warm)), thr = 1 - op; + r = tr * dv * op + r * (0.5 + 0.5 * tr) * thr * 0.8; + g = tg * dv * op + g * (0.5 + 0.5 * tg) * thr * 0.8; + b = tb * dv * op + b * (0.5 + 0.5 * tb) * thr * 0.8; + const rip = ph === COAT ? 2.5 * wet * sin((y - cf) * 0.32 - P.pt * 5) * exp(-max(0, cf - y) / 30) : 0; + const band = x * 0.5 + y * 0.42 + th * 4 + rip - sweep; + const wd = 13 - 8.5 * wet, amp = 0.15 + 0.12 * k3 + 0.55 * wet; + let hl = amp * exp(-((band / wd) ** 2)) + 0.3 * wet * exp(-(((band + 24) / 2.5) ** 2)); + // Behind the coat's edge the wet gel still moves in low swells, each + // catching a thin line of light, and they die out as it settles. + if (ph === COAT && y < cf + wob) hl += 0.3 * wet * max(0, sin((y - cf - wob) * 0.42 - P.pt * 5)) ** 8 * exp(-(cf - y) / 26); + const grain = (1 - wet) * 0.04 * (hash(p + 33) - 0.5); + r += hl * (0.85 + 0.15 * tr) + grain; + g += hl * (0.85 + 0.15 * tg) + grain; + b += hl * (0.85 + 0.15 * tb) + grain; + // The new coat's leading edge is a ridge of wet gel: a line of light + // on its upper slope, deeper colour where it is thickest. + if (ph === COAT) { + const e = y - (cf + wob); + if (e > -6) { + const deep = 1 - 0.28 * exp(-(((e + 1.2) / 1.3) ** 2)), hi = 0.75 * exp(-(((e + 3.2) / 0.9) ** 2)); + r = r * deep + hi; + g = g * deep + hi; + b = b * deep + hi; + } + } + // Below the curl, its shadow on the film, lighter on the warm hues, + // where a deep shadow turns butter to olive. + if (ph === PEEL) { + const sd = (0.45 - 0.22 * warm) * exp(-max(0, y - yf - R * 0.9) / (0.7 * R + 1)); + r *= 1 - sd; + g *= 1 - sd; + b *= 1 - sd; + } + } + + // The curl: a cylinder of film lying on the skin along the peel line, + // its paler underside out. Its material turns as it rolls, carrying faint + // stretch lines and the edge of the outer turn round with it, and light + // coming through the gel tints its lower edge. + if (ph === PEEL) { + const v = (y - yf) / R; + if (v < -1 && v > -1.6) { + // A little shade on the skin just above the roll. + const ao = 0.2 * sm((v + 1.6) / 0.6); + r *= 1 - ao; + g *= 1 - ao; + b *= 1 - ao; + } + if (v > -1.05 && v < 1.05) { + const vv = cl(v, -0.999, 0.999), nz = sqrt(1 - vv * vv); + const dif = max(0, (vv * LY - nz * LZ) * LN); + const sp = max(0, (vv * HY - nz * HZ) * HN) ** 30; + const s = atan2(vv, nz) * R - P.roll; + const str = 0.05 * sin(s * 0.8 + tap(x * 0.7, 40) * 5); + let ed = (s / (TAU * R)) % 1; + if (ed < 0) ed += 1; + const line = 0.16 * exp(-(((ed > 0.5 ? ed - 1 : ed) / 0.025) ** 2)); + const lit = (0.32 + 0.3 * warm + 0.62 * dif) * (1 + str - line); + const rim = (1 - nz) ** 2 * 0.4, glow = 0.35 * sm((vv - 0.1) / 0.8); + const ur = 0.3 + 0.7 * tr, ug = 0.3 + 0.7 * tg, ub = 0.3 + 0.7 * tb; + const cr = ur * lit + (rim + glow) * tr + sp; + const cg = ug * lit + (rim + glow) * tg + sp; + const cbl = ub * lit + (rim + glow) * tb + sp; + const cov = cl((1 - abs(v)) * R + 0.5, 0, 1); + r += (cr - r) * cov; + g += (cg - g) * cov; + b += (cbl - b) * cov; + } + } + display.setPixel(x, y, cl(r * 255, 0, 255), cl(g * 255, 0, 255), cl(b * 255, 0, 255)); + } + } +} From 17ecabb568ee46ffb6ddf41d1a5d72acae45603e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 15:12:47 +0000 Subject: [PATCH 03/10] pattern-study: Bertrand Paradox, Gold Leaf Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117bhD83MGCSerAR798cUib --- .../patterns/bertrand-paradox.js | 214 ++++++++++++ incoming/pattern-study/patterns/gold-leaf.js | 328 ++++++++++++++++++ 2 files changed, 542 insertions(+) create mode 100644 incoming/pattern-study/patterns/bertrand-paradox.js create mode 100644 incoming/pattern-study/patterns/gold-leaf.js diff --git a/incoming/pattern-study/patterns/bertrand-paradox.js b/incoming/pattern-study/patterns/bertrand-paradox.js new file mode 100644 index 000000000..69705dd8a --- /dev/null +++ b/incoming/pattern-study/patterns/bertrand-paradox.js @@ -0,0 +1,214 @@ +// ===== Patternflow pattern ===== +// Title: Bertrand Paradox +// Family: data +// Idea: Three discs stacked down the panel are filled with random chords chosen three different ways, two random points on the rim, a random point on a random radius and a random midpoint, chords longer than the side of the inscribed triangle in one colour and shorter ones in the opposite, while a gauge beside each disc counts the long ones and settles on its own answer, 1/3, 1/2 and 1/4. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Bertrand paradox (Joseph Bertrand, 1889), pattern round 33 +// Round: 33 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Chords=0..1, Persist=0..1 +// +// Knob 1: Hue. Turns the pair of colours round the wheel together. At 0 the long chords are magenta and the short ones green; at 0.25 orange and azure; at 0.5 the two swap. +// Knob 2: Speed. The clock of the whole picture. At 2 each disc takes 24 chords a second and a chord crumbles away over about a second; at 0.2 both run ten times slower, so each disc holds the same few chords, they change slowly and the counts creep. +// Knob 3: Chords. How many chords each disc takes a second: 24 at 0, 76 at 0.5, 240 at 1. Turned up, the lines give way to a stipple of each method's own density: heavy at the rim in the top disc, even in the middle one, hollow in the bottom one, with the long colour filling the circle of half the radius that only long chords cross. +// Knob 4: Persist. How long a chord takes to crumble: about a second at the opening, about six at 1, where the old chords pile up under the new ones into the same density stipple. +// Buttons: K1 tap clears the three discs and their counts, and the gauges start again from nothing. + +const W = 64, H = 128, N = W * H; +const { floor, round, sqrt, sin, cos, abs, exp, min, max, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// One disc in each third of the 128 rows, 40 LEDs across, set left, right, +// left so each has 22 columns beside it for its gauge. +const R = 19.5; +const DISCS = [ + { cx: 20.5, cy: 21.5, side: 1, num: 1, den: 3 }, + { cx: 43.5, cy: 64, side: -1, num: 1, den: 2 }, + { cx: 20.5, cy: 106.5, side: 1, num: 1, den: 4 }, +]; +const BAR = 40; +// The points each method picked for its newest chords, shown white for a +// quarter of a second: the rim pair, the point on the radius, the midpoint. +const SEEDS = 16, SEED_LIFE = 0.25; +// Chords a second times the fade time at the opening knobs. +const OPEN = 24 * 1.1 * 7 ** 0.06; + +// 3 x 5 digits, row by row from the top, three bits a row. +const FONT = { + 0: 0b111101101101111, 1: 0b010110010010111, 2: 0b111001111100111, 3: 0b111001111001111, + 4: 0b101101111001001, 5: 0b111100111001111, 6: 0b111100111101111, 7: 0b111001001001001, + 8: 0b111101111101111, 9: 0b111101111001111, "/": 0b001001010100100, "-": 0b000000111000000, +}; + +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 [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i].map((c) => c * 255); +} + +// The faint frame: each disc's rim and its inscribed triangle, point up. +// They sit at 24 of 255, just visible on the panel and still dark to the +// study's count of lit LEDs, which starts above 24. +function buildFrame() { + const f = new Uint8Array(N); + for (const d of DISCS) { + for (let a = 0; a < 720; a++) { + const t = (a / 720) * 2 * PI, x = floor(d.cx + R * cos(t)), y = floor(d.cy + R * sin(t)); + if (x >= 0 && x < W && y >= 0 && y < H) f[y * W + x] = 1; + } + for (let k = 0; k < 3; k++) { + const a0 = -PI / 2 + (k * 2 * PI) / 3, a1 = a0 + (2 * PI) / 3; + const x0 = d.cx + R * cos(a0), y0 = d.cy + R * sin(a0), x1 = d.cx + R * cos(a1), y1 = d.cy + R * sin(a1); + const n = 80; + for (let j = 0; j <= n; j++) f[floor(y0 + ((y1 - y0) * j) / n) * W + floor(x0 + ((x1 - x0) * j) / n)] = 2; + } + } + return f; +} + +export function setup(params) { + const thr = new Float32Array(N); + // Each LED lets go of a fading chord at its own level, so a chord crumbles + // LED by LED instead of dimming, and every lit LED is at full brightness. + // Half-lit lines would turn the saturated pair towards grey. + for (let i = 0; i < N; i++) thr[i] = 0.1 + 0.9 * Math.random(); + params.s = { + accL: new Float32Array(N), accS: new Float32Array(N), thr, frame: buildFrame(), + long: [0, 0, 0], total: [0, 0, 0], carry: 0, hue: 0, clock: 0, scale: 1, cap: 2.5, + seeds: new Int32Array(3 * SEEDS).fill(-1), born: new Float32Array(3 * SEEDS), next: [0, 0, 0], + }; +} + +// A chord of the unit circle by method m, as its two ends and its distance +// p from the centre. It is longer than the triangle's side, sqrt 3, exactly +// when p < 1/2, when it cuts the triangle's incircle. +function chord(m) { + const rnd = Math.random; + if (m === 0) { + // Two uniform points on the rim: long with probability 1/3. + const a = 2 * PI * rnd(), b = 2 * PI * rnd(); + return [cos(a), sin(a), cos(b), sin(b), abs(cos((a - b) / 2))]; + } + // A uniform point on a random radius (m = 1, long with probability 1/2) + // or a uniform point in the disc (m = 2, 1/4) as the chord's midpoint. + const f = 2 * PI * rnd(), p = m === 1 ? rnd() : sqrt(rnd()), h = sqrt(1 - p * p), c = cos(f), s = sin(f); + return [p * c - h * s, p * s + h * c, p * c + h * s, p * s - h * c, p]; +} + +function seed(P, d, x, y) { + x = floor(x); + y = floor(y); + if (x < 0 || x >= W || y < 0 || y >= H) return; + const j = d * SEEDS + P.next[d]; + P.next[d] = (P.next[d] + 1) % SEEDS; + P.seeds[j] = y * W + x; + P.born[j] = P.clock; +} + +// Disc d draws its chords by method d. +function lay(P, d) { + const D = DISCS[d], [ax, ay, bx, by, p] = chord(d), isLong = p < 0.5; + if (d === 0) { + seed(P, d, D.cx + R * ax, D.cy + R * ay); + seed(P, d, D.cx + R * bx, D.cy + R * by); + } else seed(P, d, D.cx + (R * (ax + bx)) / 2, D.cy + (R * (ay + by)) / 2); + const acc = isLong ? P.accL : P.accS; + const x0 = D.cx + R * ax, y0 = D.cy + R * ay, dx = R * (bx - ax), dy = R * (by - ay); + const n = Math.ceil(max(abs(dx), abs(dy))); + let last = -1; + for (let j = 0; j <= n; j++) { + const x = floor(x0 + (dx * j) / max(1, n)), y = floor(y0 + (dy * j) / max(1, n)); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const i = y * W + x; + if (i === last) continue; + last = i; + acc[i] = min(acc[i] + 1, P.cap); + } + P.total[d]++; + if (isLong) P.long[d]++; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + P.hue = k[0]; + const clock = d * (cl(k[1], 0.2, 3) / 2); + const rate = 24 * 10 ** cl(k[2], 0, 1), tau = 1.1 * 7 ** cl(k[3], 0, 1); + if (input.btnPressed && input.btnPressed[0]) { + P.accL.fill(0); + P.accS.fill(0); + P.long = [0, 0, 0]; + P.total = [0, 0, 0]; + P.seeds.fill(-1); + } + P.clock += clock; + // With more chords alive than at the opening, the level an LED needs to + // light rises with them (to the 0.85th power), so the discs turn from a + // few lines into a stipple of each method's own density instead of + // filling solid: even in the middle disc, heavy at the rim in the top one, + // hollow in the bottom one. + P.scale = max(1, (rate * tau) / OPEN) ** 0.85; + P.cap = 2.5 * P.scale; + const fade = exp(-clock / tau), aL = P.accL, aS = P.accS; + for (let i = 0; i < N; i++) { + aL[i] *= fade; + aS[i] *= fade; + } + P.carry += rate * clock; + const n = floor(P.carry); + P.carry -= n; + for (let j = 0; j < n; j++) for (let m = 0; m < 3; m++) lay(P, m); +} + +function glyph(display, ch, x0, y0, sc, c) { + const bits = FONT[ch]; + for (let r = 0; r < 5; r++) + for (let q = 0; q < 3; q++) + if ((bits >> (14 - r * 3 - q)) & 1) + for (let u = 0; u < sc; u++) for (let v = 0; v < sc; v++) display.setPixel(x0 + q * sc + u, y0 + r * sc + v, c[0], c[1], c[2]); +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + // Magenta for the long chords, green for the short: at full saturation + // the pair sits as far apart as two colours can on the panel's red-green + // and yellow-blue axes, and neither opens on red. + const cL = hsv(P.hue + 5 / 6, 1, 1), cS = hsv(P.hue + 1 / 3, 1, 1); + const { accL, accS, thr, frame } = P, sc = P.scale; + for (let i = 0; i < N; i++) { + const a = accL[i], b = accS[i], t = a + b, x = i % W, y = (i / W) | 0; + if (t > thr[i] * sc) { + // A lit LED is at full brightness: the fade shows as crumbling. + const c = a >= b ? cL : cS; + display.setPixel(x, y, c[0], c[1], c[2]); + } else if (frame[i]) display.setPixel(x, y, 24, 24, 24); + else display.setPixel(x, y, 0, 0, 0); + } + for (let j = 0; j < P.seeds.length; j++) { + const i = P.seeds[j]; + if (i >= 0 && P.clock - P.born[j] < SEED_LIFE) display.setPixel(i % W, (i / W) | 0, 255, 255, 255); + } + for (let d = 0; d < 3; d++) { + const D = DISCS[d], yb = round(D.cy + R) - 1, top = yb - BAR + 1; + const bx = D.side > 0 ? 43 : 18, tx = D.side > 0 ? 49 : 1; + const tot = P.total[d], f = tot ? P.long[d] / tot : 0, hgt = round(f * BAR); + // The bar: the share of long chords so far, out of 40 rows. + for (let j = 0; j < hgt; j++) for (let q = 0; q < 3; q++) display.setPixel(bx + q, yb - j, cL[0], cL[1], cL[2]); + // The answer this method gives, as a white tick and its fraction. + const ty = yb - round((D.num / D.den) * BAR); + for (let q = -1; q <= 3; q++) display.setPixel(bx + q, ty, 255, 255, 255); + const lx = D.side > 0 ? tx : tx + 3; + glyph(display, D.num, lx, ty - 2, 1, cS); + glyph(display, "/", lx + 4, ty - 2, 1, cS); + glyph(display, D.den, lx + 8, ty - 2, 1, cS); + // The running share in percent, two digits, in the long colour. + const pct = min(99, round(f * 100)); + const s = tot ? [floor(pct / 10), pct % 10] : ["-", "-"]; + glyph(display, s[0], tx, top + 1, 2, cL); + glyph(display, s[1], tx + 8, top + 1, 2, cL); + } +} diff --git a/incoming/pattern-study/patterns/gold-leaf.js b/incoming/pattern-study/patterns/gold-leaf.js new file mode 100644 index 000000000..0bdb91f48 --- /dev/null +++ b/incoming/pattern-study/patterns/gold-leaf.js @@ -0,0 +1,328 @@ +// ===== Patternflow pattern ===== +// Title: Gold Leaf +// Family: cosmetic +// Idea: Crumpled flakes of gold foil hang in a clear tinted serum and drift round a slow convection current without ever settling, each torn flake tumbling so its crumple facets flash gold-white one after another as they face the light and drop to deep gold as they turn away, near flakes large and sharp, far ones small, dim and soft. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 33, after a serum with flakes of gold leaf floating in it +// Round: 33 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Flakes=40..120, Size=0..1 +// +// Knob 1: Hue. Turns the foil all the way round the wheel, and the serum's faint tint with it. At 0 the foil is yellow gold; at 0.07 green gold, at 0.5 blue foil in a blue-black serum, at 0.88 rose gold and at 0.93 copper. Round orange and yellow the shadowed facets stay bright and saturated, so the foil never goes brown. +// Knob 2: Speed. How fast the current carries the flakes and how fast they tumble. At 2 the near flakes drift up to 3 LEDs a second round two convection cells, one above the other, and a flake turns over every 4 to 12 seconds; at 0.2 all of it runs ten times slower. +// Knob 3: Flakes. How many flakes hang in the serum: 40 at the opening, about a third of them far back, small and soft; up to 120, a crowd of foil. +// Knob 4: Size. How big the flakes are: at the opening the nearest are about 12 LEDs across and the farthest about 5; at 1 about 23 and 10, and the facets read one by one. +// Buttons: K1 tap stirs the serum: within a second the flakes are swept into a slow vortex the shape of the panel, a turn every 5 to 10 seconds, tumbling up to five times faster and flashing all over; over the next eight seconds it dies away and the convection takes them back. + +const W = 64, H = 128, N = W * H; +const MAXF = 120, SS = 3; +const { min, max, floor, sqrt, sin, cos, exp, abs, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => ((v = cl((v - a) / (b - a), 0, 1)), v * v * (3 - 2 * v)); + +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 [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; +} + +// The light: from the upper left and in front. Y runs down the panel, the +// viewer looks along -z. +const LX = -0.4, LY = -0.7, LZ = 0.6, LN = sqrt(LX * LX + LY * LY + LZ * LZ); +const L = [LX / LN, LY / LN, LZ / LN]; +const HV = [L[0], L[1], L[2] + 1], HN = sqrt(HV[0] ** 2 + HV[1] ** 2 + HV[2] ** 2); +const HH = [HV[0] / HN, HV[1] / HN, HV[2] / HN]; + +// A torn flake: 6 to 9 points round a ragged outline, each pushed up or down +// a little, fanned from an off-centre middle point into facets. The heights +// are the crumple: every facet tilts its own way. +function makeFlake() { + const rnd = Math.random, k = 6 + floor(rnd() * 4), e = 0.6 + 0.4 * rnd(), ea = rnd() * PI; + const v = [(rnd() - 0.5) * 0.3, (rnd() - 0.5) * 0.3, (rnd() - 0.5) * 0.3]; + for (let i = 0; i < k; i++) { + const a = (2 * PI * (i + 0.7 * (rnd() - 0.5))) / k, r = 0.55 + 0.45 * rnd(); + const x = r * cos(a), y = r * sin(a) * e; + v.push(x * cos(ea) - y * sin(ea), x * sin(ea) + y * cos(ea), (rnd() - 0.5) * 0.8); + } + const nrm = []; + for (let i = 0; i < k; i++) { + const a = 1 + i, b = 1 + ((i + 1) % k); + const ux = v[a * 3] - v[0], uy = v[a * 3 + 1] - v[1], uz = v[a * 3 + 2] - v[2]; + const wx = v[b * 3] - v[0], wy = v[b * 3 + 1] - v[1], wz = v[b * 3 + 2] - v[2]; + let nx = uy * wz - uz * wy, ny = uz * wx - ux * wz, nz = ux * wy - uy * wx; + const l = sqrt(nx * nx + ny * ny + nz * nz) || 1; + if (nz < 0) (nx = -nx), (ny = -ny), (nz = -nz); + nrm.push(nx / l, ny / l, nz / l); + } + const ax = rnd() - 0.5, ay = rnd() - 0.5, az = rnd() - 0.5, al = sqrt(ax * ax + ay * ay + az * az) || 1; + const q0 = [rnd() - 0.5, rnd() - 0.5, rnd() - 0.5, rnd() - 0.5], ql = sqrt(q0.reduce((s, c) => s + c * c, 0)) || 1; + return { + k, v: new Float32Array(v), n: new Float32Array(nrm), + x: 3 + rnd() * (W - 6), y: 3 + rnd() * (H - 6), z: rnd(), sz: 0.75 + 0.5 * rnd(), + q: q0.map((c) => c / ql), axis: [ax / al, ay / al, az / al], spin: (0.5 + rnd()) * (rnd() < 0.5 ? -1 : 1), ph: rnd() * 2 * PI, + }; +} + +export function setup(params) { + const flakes = []; + for (let i = 0; i < MAXF; i++) flakes.push(makeFlake()); + params.s = { + flakes, t: 0, stir: 0, stirT: -1, hue: 0, count: 40, size: 0.06, + far: new Float32Array(N * 4), tmp: new Float32Array(N * 4), frame: new Float32Array(N * 3), + sub: new Int16Array(64 * 64 * SS * SS), lut: new Float32Array(256 * 3), lutHue: NaN, serum: [], + }; +} + +// The current: two slow convection cells stacked in the panel, from the +// stream function sin(pi x/W) sin(2 pi y/H), which is zero on all four walls, +// so the flakes go round and never leave. A weaker mode of smaller cells +// swells and fades over a minute, so no flake runs the same loop twice. +// Nothing pulls them down: the flakes float, they never settle. +function flow(x, y, t, out) { + const X = (PI * x) / W, Y = (2 * PI * y) / H, s = 0.35 * sin(t * 0.11); + out[0] = sin(X) * cos(Y) + s * sin(2 * X) * 1.5 * cos(1.5 * Y) * 0.5; + out[1] = -cos(X) * sin(Y) - s * 2 * cos(2 * X) * sin(1.5 * Y) * 0.5; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 40, 0.06], d = cl(dt || 0, 0, 0.1) * (cl(k[1], 0.2, 3) / 2); + P.hue = k[0]; + P.count = floor(cl(k[2], 40, 120)); + P.size = cl(k[3], 0, 1); + P.t += d; + if (input.btnPressed && input.btnPressed[0]) P.stirT = 0; + // The stir: a second to wind up, then it dies away over about eight. + if (P.stirT >= 0) { + P.stirT += d; + P.stir = P.stirT < 1 ? sm(0, 1, P.stirT) : exp(-(P.stirT - 1) / 3.5); + if (P.stirT > 20) (P.stirT = -1), (P.stir = 0); + } + const v = [0, 0], S = P.stir; + for (let i = 0; i < MAXF; i++) { + const f = P.flakes[i]; + flow(f.x, f.y, P.t, v); + // Near flakes move faster across the panel than far ones. + const par = 0.6 + 0.6 * f.z, sp = 2.6 * par; + let vx = v[0] * sp + 0.4 * sin(P.t * 0.5 + f.ph), vy = v[1] * sp + 0.4 * cos(P.t * 0.37 + f.ph * 1.3); + if (S > 0) { + // The vortex runs round ellipses the shape of the panel, fastest + // half way out, so the flakes swing round without piling on a wall. + const ex = (f.x - W / 2) / (W / 2), ey = (f.y - H / 2) / (H / 2), r = sqrt(ex * ex + ey * ey); + const g = S * 1.6 * cl(1.15 - r, 0, 1) * par; + vx += g * -0.5 * (f.y - H / 2); + vy += g * 2 * (f.x - W / 2); + } + f.x = cl(f.x + vx * d, 1, W - 2); + f.y = cl(f.y + vy * d, 1, H - 2); + // Tumble: turn about the flake's own axis, which wanders slowly. + const ang = f.spin * (1 + 4 * S) * d, a = f.axis; + a[0] += 0.3 * sin(P.t * 0.23 + f.ph) * d; + a[1] += 0.3 * cos(P.t * 0.19 + f.ph * 2) * d; + const al = sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]); + a[0] /= al; + a[1] /= al; + a[2] /= al; + const sh = sin(ang / 2), ch = cos(ang / 2), dq = [ch, a[0] * sh, a[1] * sh, a[2] * sh], q = f.q; + const n0 = dq[0] * q[0] - dq[1] * q[1] - dq[2] * q[2] - dq[3] * q[3]; + const n1 = dq[0] * q[1] + dq[1] * q[0] + dq[2] * q[3] - dq[3] * q[2]; + const n2 = dq[0] * q[2] - dq[1] * q[3] + dq[2] * q[0] + dq[3] * q[1]; + const n3 = dq[0] * q[3] + dq[1] * q[2] - dq[2] * q[1] + dq[3] * q[0]; + const ql = sqrt(n0 * n0 + n1 * n1 + n2 * n2 + n3 * n3); + q[0] = n0 / ql; + q[1] = n1 / ql; + q[2] = n2 / ql; + q[3] = n3 / ql; + } +} + +// The foil's colour against its light, 0 to 1.3 over 256 steps: deep, gold, +// pale gold, white-gold. +function buildLut(P) { + const h = P.hue + 0.115 - floor(P.hue + 0.115); + // Round orange and yellow the deep end stays bright and saturated so it + // reads as dark gold, not brown; round red and pink the foil is paler, rose + // gold rather than red. + const warm = sm(0.0, 0.05, h) * (1 - sm(0.17, 0.24, h)), rose = 1 - sm(0.02, 0.07, abs(h - 0.995 + (h < 0.5 ? 1 : 0))); + const sat = 1 - 0.4 * rose, lift = max(warm, rose); + const stops = [ + [0, hsv(h - 0.025, 0.95 * sat, 0.4 + 0.18 * lift)], + [0.35, hsv(h - 0.01, 0.88 * sat, 0.72 + 0.12 * lift)], + [0.7, hsv(h + 0.005, 0.68 * sat, 1)], + [1.0, hsv(h + 0.01, 0.34 * sat, 1)], + [1.3, hsv(h + 0.01, 0.06, 1)], + ]; + for (let i = 0; i < 256; i++) { + const b = (i / 255) * 1.3; + let j = 0; + while (j < stops.length - 2 && b > stops[j + 1][0]) j++; + const [b0, c0] = stops[j], [b1, c1] = stops[j + 1], f = cl((b - b0) / (b1 - b0), 0, 1); + for (let c = 0; c < 3; c++) P.lut[i * 3 + c] = (c0[c] + (c1[c] - c0[c]) * f) * 255; + } + // The serum: the foil's hue, faint and greyed, lighter at the top. + const top = hsv(h - 0.02, (0.5 - 0.2 * warm) * sat, 0.11), bot = hsv(h - 0.02, (0.6 - 0.2 * warm) * sat, 0.025); + const lit = hsv(h + 0.01, 0.3 * sat, 1); + P.serum = [top[0] * 255, top[1] * 255, top[2] * 255, bot[0] * 255, bot[1] * 255, bot[2] * 255, lit[0] * 255, lit[1] * 255, lit[2] * 255]; + P.lutHue = P.hue; +} + +const rot = new Float32Array(9); +function quatMat(q) { + const [w, x, y, z] = q; + rot[0] = 1 - 2 * (y * y + z * z); rot[1] = 2 * (x * y - w * z); rot[2] = 2 * (x * z + w * y); + rot[3] = 2 * (x * y + w * z); rot[4] = 1 - 2 * (x * x + z * z); rot[5] = 2 * (y * z - w * x); + rot[6] = 2 * (x * z - w * y); rot[7] = 2 * (y * z + w * x); rot[8] = 1 - 2 * (x * x + y * y); +} + +const PX = new Float32Array(10 * 3), FC = new Float32Array(9 * 4), ORD = []; + +// One flake into buffer buf (RGB, or RGBA premultiplied when far), over what +// is there. Each LED takes 3 x 3 samples, 2 x 2 on a big flake; a facet +// nearer the viewer covers the ones behind it, and the share of samples a +// flake covers is how much of the LED it takes. +function drawFlake(P, f, buf, rgba, dim, glints) { + quatMat(f.q); + const s = (6.5 + 6 * P.size) * f.sz * (0.5 + 0.7 * f.z), k = f.k, v = f.v, n = f.n, lut = P.lut; + let x0 = W, x1 = 0, y0 = H, y1 = 0; + for (let i = 0; i <= k; i++) { + const a = v[i * 3], b = v[i * 3 + 1], c = v[i * 3 + 2]; + const X = f.x + s * (rot[0] * a + rot[1] * b + rot[2] * c), Y = f.y + s * (rot[3] * a + rot[4] * b + rot[5] * c); + PX[i * 3] = X; + PX[i * 3 + 1] = Y; + PX[i * 3 + 2] = rot[6] * a + rot[7] * b + rot[8] * c; + x0 = min(x0, X); + x1 = max(x1, X); + y0 = min(y0, Y); + y1 = max(y1, Y); + } + // Big flakes need fewer samples to an LED for the same smooth edge. + const ss = s > 7 ? 2 : SS; + const bx = max(0, floor(x0)), by = max(0, floor(y0)), bw = min(W, floor(x1) + 1) - bx, bh = min(H, floor(y1) + 1) - by; + if (bw <= 0 || bh <= 0 || bw > 64 || bh > 64) return; + const sw = bw * ss, sub = P.sub; + sub.fill(-1, 0, sw * bh * ss); + ORD.length = 0; + for (let i = 0; i < k; i++) { + // The facet's normal turned with the flake, towards the viewer: foil + // shines from both faces. + const a = n[i * 3], b = n[i * 3 + 1], c = n[i * 3 + 2]; + let nx = rot[0] * a + rot[1] * b + rot[2] * c, ny = rot[3] * a + rot[4] * b + rot[5] * c, nz = rot[6] * a + rot[7] * b + rot[8] * c; + if (nz < 0) (nx = -nx), (ny = -ny), (nz = -nz); + const spec = max(0, nx * HH[0] + ny * HH[1] + nz * HH[2]) ** 18, diff = max(0, nx * L[0] + ny * L[1] + nz * L[2]); + // A little base light, the lamp's diffuse share, the glow of the serum + // above for facets tipped up, and a narrow highlight, cos^18, half + // strength 16 degrees off the mirror angle: a facet flashes as it + // sweeps through and its neighbours, tilted their own ways, stay deep. + const env = 0.5 - 0.5 * ny; + const br = (0.1 + 0.45 * diff + 0.25 * env + 0.95 * spec) * dim; + FC[i * 4] = cl(floor((br / 1.3) * 255), 0, 255); + FC[i * 4 + 1] = spec; + const j1 = 1 + i, j2 = 1 + ((i + 1) % k); + FC[i * 4 + 2] = (PX[2] + PX[j1 * 3 + 2] + PX[j2 * 3 + 2]) / 3; + ORD.push(i); + } + ORD.sort((a, b) => FC[a * 4 + 2] - FC[b * 4 + 2]); + for (const i of ORD) { + const j1 = 1 + i, j2 = 1 + ((i + 1) % k); + const ax = PX[0], ay = PX[1], bxx = PX[j1 * 3], byy = PX[j1 * 3 + 1], cx = PX[j2 * 3], cy = PX[j2 * 3 + 1]; + const area = (bxx - ax) * (cy - ay) - (byy - ay) * (cx - ax); + if (abs(area) < 1e-4) continue; + const sg = area > 0 ? 1 : -1; + const sx0 = max(0, floor((min(ax, bxx, cx) - bx) * ss)), sx1 = min(sw - 1, floor((max(ax, bxx, cx) - bx) * ss)); + const sy0 = max(0, floor((min(ay, byy, cy) - by) * ss)), sy1 = min(bh * ss - 1, floor((max(ay, byy, cy) - by) * ss)); + for (let sy = sy0; sy <= sy1; sy++) { + const py = by + (sy + 0.5) / ss; + for (let sx = sx0; sx <= sx1; sx++) { + const px = bx + (sx + 0.5) / ss; + const e0 = ((bxx - ax) * (py - ay) - (byy - ay) * (px - ax)) * sg; + const e1 = ((cx - bxx) * (py - byy) - (cy - byy) * (px - bxx)) * sg; + const e2 = ((ax - cx) * (py - cy) - (ay - cy) * (px - cx)) * sg; + if (e0 >= 0 && e1 >= 0 && e2 >= 0) sub[sy * sw + sx] = i; + } + } + if (glints && FC[i * 4 + 1] > 0.6) glints.push((ax + bxx + cx) / 3, (ay + byy + cy) / 3, FC[i * 4 + 1]); + } + for (let y = 0; y < bh; y++) + for (let x = 0; x < bw; x++) { + let r = 0, g = 0, b = 0, c = 0; + for (let u = 0; u < ss; u++) + for (let w = 0; w < ss; w++) { + const i = sub[(y * ss + u) * sw + x * ss + w]; + if (i < 0) continue; + const li = FC[i * 4] * 3; + r += lut[li]; + g += lut[li + 1]; + b += lut[li + 2]; + c++; + } + if (!c) continue; + const al = c / (ss * ss), p = (by + y) * W + bx + x; + if (rgba) { + const o = p * 4; + buf[o] = buf[o] * (1 - al) + r / (ss * ss); + buf[o + 1] = buf[o + 1] * (1 - al) + g / (ss * ss); + buf[o + 2] = buf[o + 2] * (1 - al) + b / (ss * ss); + buf[o + 3] = buf[o + 3] * (1 - al) + al; + } else { + const o = p * 3; + buf[o] = buf[o] * (1 - al) + r / (ss * ss); + buf[o + 1] = buf[o + 1] * (1 - al) + g / (ss * ss); + buf[o + 2] = buf[o + 2] * (1 - al) + b / (ss * ss); + } + } +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + if (P.lutHue !== P.hue) buildLut(P); + const fr = P.frame, far = P.far, tmp = P.tmp, t = P.t, sv = P.serum; + // The serum: a soft gradient and a slow caustic shimmer that fades with + // depth, light let in from the top. + const colw = P.colw || (P.colw = new Float32Array(W)); + for (let x = 0; x < W; x++) colw[x] = 1.5 * sin(x * 0.17 - t * 0.31); + for (let y = 0; y < H; y++) { + const g = y / (H - 1), dep = exp(-y / 70), roww = 1.7 * sin(y * 0.11 + t * 0.4) + t * 0.23; + for (let x = 0; x < W; x++) { + const a = sin(x * 0.19 + roww), b = sin(y * 0.13 + colw[x] - t * 0.17); + const c = a + b, caus = exp(-c * c * 6) * 0.9 * dep, o = (y * W + x) * 3; + for (let ch = 0; ch < 3; ch++) fr[o + ch] = sv[ch] * (1 - g) + sv[3 + ch] * g + caus * 0.07 * sv[6 + ch]; + } + } + const fl = P.flakes.slice(0, P.count).sort((a, b) => a.z - b.z); + far.fill(0); + let i = 0; + // Far flakes, dimmer, into their own layer, softened by a 1-2-1 blur. + for (; i < fl.length && fl[i].z < 0.35; i++) drawFlake(P, fl[i], far, true, 0.6 + 0.6 * fl[i].z, null); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const o = (y * W + x) * 4, l = x > 0 ? o - 4 : o, r = x < W - 1 ? o + 4 : o; + for (let c = 0; c < 4; c++) tmp[o + c] = (far[l + c] + 2 * far[o + c] + far[r + c]) / 4; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const o = (y * W + x) * 4, u = y > 0 ? o - W * 4 : o, dn = y < H - 1 ? o + W * 4 : o, f3 = (y * W + x) * 3; + const a = (tmp[u + 3] + 2 * tmp[o + 3] + tmp[dn + 3]) / 4; + for (let c = 0; c < 3; c++) fr[f3 + c] = fr[f3 + c] * (1 - a) + (tmp[u + c] + 2 * tmp[o + c] + tmp[dn + c]) / 4; + } + // Near flakes, sharp, over the top; their brightest facets throw a glint. + const glints = []; + for (; i < fl.length; i++) drawFlake(P, fl[i], fr, false, 1, fl[i].z > 0.6 ? glints : null); + for (let g = 0; g < glints.length; g += 3) { + const gx = floor(glints[g]), gy = floor(glints[g + 1]), s = (glints[g + 2] - 0.6) / 0.4; + for (let r = -2; r <= 2; r++) { + const a = s * (r === 0 ? 1 : abs(r) === 1 ? 0.55 : 0.22); + for (const [x, y] of r === 0 ? [[gx, gy]] : [[gx + r, gy], [gx, gy + r]]) { + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const o = (y * W + x) * 3; + fr[o] += 255 * a; + fr[o + 1] += 250 * a; + fr[o + 2] += 225 * a; + } + } + } + for (let p = 0, o = 0; p < N; p++, o += 3) display.setPixel(p % W, (p / W) | 0, min(255, fr[o]), min(255, fr[o + 1]), min(255, fr[o + 2])); +} From 43c1418bd5fd304d3c9d1c472c64d4f854538eae Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 15:14:28 +0000 Subject: [PATCH 04/10] pattern-study: Sorting Network, Goldbach Comet Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117bhD83MGCSerAR798cUib --- .../pattern-study/patterns/goldbach-comet.js | 269 ++++++++++++++ .../pattern-study/patterns/sorting-network.js | 331 ++++++++++++++++++ 2 files changed, 600 insertions(+) create mode 100644 incoming/pattern-study/patterns/goldbach-comet.js create mode 100644 incoming/pattern-study/patterns/sorting-network.js diff --git a/incoming/pattern-study/patterns/goldbach-comet.js b/incoming/pattern-study/patterns/goldbach-comet.js new file mode 100644 index 000000000..fc3ba8e1e --- /dev/null +++ b/incoming/pattern-study/patterns/goldbach-comet.js @@ -0,0 +1,269 @@ +// ===== Patternflow pattern ===== +// Title: Goldbach Comet +// Family: number +// Idea: Every even number n up to 524,288 is a dot placed across the panel by how many ways it splits into two primes, measured against the Hardy-Littlewood count, with n climbing down the rows; the dots gather into sharp coloured streaks, one for each set of 3, 5 and 7 dividing n, that start as a loose spray and narrow as n grows, and fold a prime at a time into a single line when the correction is turned in. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Goldbach's comet, the partition counts of Goldbach's conjecture (Christian Goldbach, 1742), pattern round 33 +// Round: 33 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Correction=0..1, Smear=0..1 +// +// Knob 1: Hue. Turns the five colours round the wheel together. At 0 numbers divisible by 3 are gold, by 5 (not 3) green, by 7 (not 3 or 5) pink, by 11 sky blue and the rest pale blue. +// Knob 2: Speed. How fast n climbs: 0.12 octaves a second times the knob. The 128 rows hold four octaves of n, so at 2 the rows move up about eight a second and the tape runs from n = 512 to 524,288 and starts over in 43 seconds; at 0.2 it takes seven minutes. +// Knob 3: Correction. Divides the count by the Hardy-Littlewood singular series a prime at a time. At 0 each streak sits at its own factor from left to right: 1 for n with no odd prime up to 7, then 6/5 for 7, 4/3 for 5, 8/5 for 35, 2 for 3, 12/5 for 21, 8/3 for 15 and 16/5 for 105. Up to 1/3 the 7s fold onto the streaks beside them, up to 2/3 the 5s, and in the last third the 3s, while the whole comet glides into one line in the middle of the panel. +// Knob 4: Smear. How much of the series from the primes 11 and up is left in: almost none at the start, so each streak is one sharp line. At 1 each fans out to the right about four LEDs wide, since a multiple of 11 has 10/9 as many partitions, of 13 12/11 as many, and so on; that is the comet with nothing divided out but the smooth count. +// Buttons: K1 tap folds the correction all the way in over three seconds, holds the single line for two and lets it out again over three; with Correction past 0.5 it goes the other way. + +const W = 64, H = 128, LOGM = 19, M = 1 << LOGM, HN = M >> 1; +const U0 = 9, U1 = LOGM, SPAN = 4, CAP = 160, XS = 40, X0 = 8, MID = 0.6; +const L3 = Math.log(2), L5 = Math.log(4 / 3), L7 = Math.log(6 / 5); +const C2 = 0.6601618158468696; +const { min, max, floor, ceil, log, exp, cos, sin, PI, LN2 } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (v) => ((v = cl(v, 0, 1)), v * v * (3 - 2 * v)); + +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; +} + +// Complex FFT of size n in place. tc/ts hold e^(-i pi k / half) at +// half + k for every stage, so each stage reads its twiddles in a row. +function fft(re, im, n, tc, ts, sign) { + for (let i = 1, j = 0; i < n; i++) { + let bit = n >> 1; + for (; j & bit; bit >>= 1) j ^= bit; + j ^= bit; + if (i < j) { + let t = re[i]; + (re[i] = re[j]), (re[j] = t); + (t = im[i]), (im[i] = im[j]), (im[j] = t); + } + } + for (let half = 1; half < n; half <<= 1) + for (let i = 0, len = half << 1; i < n; i += len) + for (let k = 0; k < half; k++) { + const cr = tc[half + k], ci = sign * ts[half + k], a = i + k, b = a + half; + const br = re[b], bi = im[b], xr = br * cr - bi * ci, xi = br * ci + bi * cr; + re[b] = re[a] - xr; + im[b] = im[a] - xi; + re[a] += xr; + im[a] += xi; + } +} + +// g(n) for every even n up to 2^19, the number of ways n = p + q with +// primes p <= q. Odd primes 2a + 1 make a 0/1 string over a < 2^18, and +// its square under convolution counts the ordered pairs with +// a + b = n/2 - 1. The string is real, so it rides as one complex string +// of half the length (even a in the real part, odd a in the imaginary) +// and is pulled apart after the transform. Pairing up the 43,390 primes +// below 2^19 one by one would be 512 million steps; this is 55 to 110 ms. +// Checked against counting pairs one by one for every even n below 20,000 +// and every 997th one above, up to g(2^19) = 2367: no misses. Run to 2^20 +// it gave g(10^6) = 5402 with rounding errors under 2e-11, but took twice +// as long. +function goldbach(comp) { + const n = HN, tc = new Float64Array(M), ts = new Float64Array(M); + // The top stage by rotation, started afresh from cos and sin every 64 + // steps: 4096 calls to them instead of 262,144. + const sr = cos(-PI / n), si = sin(-PI / n); + for (let k = 0; k < n; k++) { + if ((k & 63) === 0) (tc[n + k] = cos((-PI * k) / n)), (ts[n + k] = sin((-PI * k) / n)); + else { + const a = tc[n + k - 1], b = ts[n + k - 1]; + (tc[n + k] = a * sr - b * si), (ts[n + k] = a * si + b * sr); + } + } + for (let half = n >> 1; half >= 1; half >>= 1) + for (let k = 0, st = n / half; k < half; k++) (tc[half + k] = tc[n + k * st]), (ts[half + k] = ts[n + k * st]); + const zr = new Float64Array(n), zi = new Float64Array(n); + for (let a = 1; a < n; a++) if (!comp[2 * a + 1]) (a & 1 ? zi : zr)[a >> 1] = 1; + fft(zr, zi, n, tc, ts, 1); + // X[k] and X[k + n] of the full string from Z[k] and Z[n - k]; square + // them; fold the squares back into one half-length string, pairs k and + // n - k together so it can be done in place. + const out = new Float64Array(4); + const fold = (k, j, o) => { + const ar = zr[k], ai = zi[k], br = zr[j], bi = -zi[j]; + const er = (ar + br) / 2, ei = (ai + bi) / 2, odr = (ai - bi) / 2, odi = -(ar - br) / 2; + const wr = tc[n + k], wi = ts[n + k], tr = odr * wr - odi * wi, ti = odr * wi + odi * wr; + const x1r = er + tr, x1i = ei + ti, x2r = er - tr, x2i = ei - ti; + const y1r = x1r * x1r - x1i * x1i, y1i = 2 * x1r * x1i, y2r = x2r * x2r - x2i * x2i, y2i = 2 * x2r * x2i; + const sr = (y1r + y2r) / 2, si = (y1i + y2i) / 2, dr = (y1r - y2r) / 2, di = (y1i - y2i) / 2; + out[o] = sr - (di * wr - dr * wi); + out[o + 1] = si + (dr * wr + di * wi); + }; + for (let k = 0; k <= n >> 1; k++) { + const j = (n - k) & (n - 1); + fold(k, j, 0); + fold(j, k, 2); + (zr[k] = out[0]), (zi[k] = out[1]), (zr[j] = out[2]), (zi[j] = out[3]); + } + fft(zr, zi, n, tc, ts, -1); + const g = new Int32Array(HN + 1); + for (let k = 0; k + 1 <= HN; k++) { + const h = k + 1, c = Math.round((k & 1 ? zi[k >> 1] : zr[k >> 1]) / n) + (h & 1 && h > 2 && !comp[h] ? 1 : 0); + g[h] = c >> 1; + } + g[2] = 1; + return g; +} + +// The Hardy-Littlewood count without its singular series: C2 times the +// integral of dt / (ln t ln(n - t)) from 2 to n - 2, on a table of 97 +// points from n = 2^8 to 2^20 and read between them in ln n. C2 n / ln^2 n +// alone falls 23% short at n = 2000 and 15% at 500,000, and that drift +// would tip the streaks four LEDs across the tape. With the integral, g(n) +// over the whole prediction averages 0.953 at n = 2000, 0.982 at 20,000 +// and 0.997 at 500,000, spread 5.7%, 2.9% and 0.9% about it, so a streak +// narrows from a spread of about two LEDs to a third of one. +function hlTable() { + const t = new Float64Array(97); + for (let j = 0; j < 97; j++) { + const n = 2 ** (8 + j / 8), a = log(2), b = log(n / 2), K = 200, h = (b - a) / K; + let s = 0; + for (let i = 0; i <= K; i++) { + const u = a + i * h, x = exp(u); + s += ((i === 0 || i === K ? 1 : i & 1 ? 4 : 2) * x) / (u * log(n - x)); + } + t[j] = log((2 * C2 * s * h) / 3); + } + return t; +} + +export function setup(params) { + const comp = new Uint8Array(M + 1); + for (let i = 3; i * i <= M; i += 2) if (!comp[i]) for (let j = i * i; j <= M; j += 2 * i) comp[j] = 1; + const g = goldbach(comp), hl = hlTable(); + // For each even n = 2h: which of 3, 5, 7 and 11 divide it (bits 1, 2, 4 + // and 8), and the log of the rest of its singular series, the product of + // (p - 1)/(p - 2) over the odd primes p from 11 up that divide it. + const bits = new Uint8Array(HN + 1), lr = new Float32Array(HN + 1), res = new Float32Array(HN + 1); + for (let p = 3; p <= HN; p += 2) { + if (comp[p]) continue; + if (p > 7) { + const f = log((p - 1) / (p - 2)); + for (let h = p; h <= HN; h += p) lr[h] += f; + } else for (let h = p, b = p === 3 ? 1 : p === 5 ? 2 : 4; h <= HN; h += p) bits[h] |= b; + } + for (let h = 11; h <= HN; h += 11) bits[h] |= 8; + // res is how far g(n) sits from the whole prediction, in ln: 0 would be + // exactly Hardy-Littlewood. The prediction is read off the table once + // every 16 numbers and drawn straight between. + let hA = 0, hB = 0; + const hlAt = (h) => { + const z = cl((log(2 * h) / LN2 - 8) * 8, 0, 96), j = min(95, floor(z)); + return hl[j] + (hl[j + 1] - hl[j]) * (z - j); + }; + for (let h = 4; h <= HN; h++) { + if (((h - 4) & 15) === 0) (hA = hlAt(h)), (hB = hlAt(h + 16)); + const b = bits[h], small = (b & 1 ? L3 : 0) + (b & 2 ? L5 : 0) + (b & 4 ? L7 : 0); + res[h] = log(g[h]) - lr[h] - small - (hA + ((hB - hA) * ((h - 4) & 15)) / 16); + } + params.s = { + bits, lr, res, u: 10.6, hue: 0, speed: 2, kap: 0, smear: 0.06, bump: -1, + acc: new Float32Array(W * 4), pal: new Float32Array(15), palHue: NaN, rgb: new Float32Array(W * H * 3), + off: new Float32Array(16), cls: new Uint8Array(16), + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + P.hue = k[0]; + P.speed = cl(k[1], 0.2, 3); + P.smear = cl(k[3], 0, 1); + const k3 = cl(k[2], 0, 1); + if (input.btnPressed && input.btnPressed[0]) P.bump = 0; + // The tap sends the correction to the far end from the knob and back: + // in for three seconds, held for two, out for three. + let kap = k3; + if (P.bump >= 0) { + P.bump += d / 8; + const b = P.bump < 3 / 8 ? sm((P.bump * 8) / 3) : P.bump < 5 / 8 ? 1 : sm(((1 - P.bump) * 8) / 3); + kap = k3 + ((k3 < 0.5 ? 1 : 0) - k3) * b; + if (P.bump >= 1) P.bump = -1; + } + P.kap = kap; + P.u += d * P.speed * 0.12; +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const { bits, lr, res, acc, pal, rgb, off, cls } = P; + if (P.palHue !== P.hue) { + // Gold, green, pink, sky blue and pale blue at Hue 0. The gold is kept + // light so a thin streak of it does not read brown. + const base = [[0.13, 0.62], [0.36, 0.75], [0.9, 0.55], [0.55, 0.7], [0.62, 0.35]]; + for (let c = 0; c < 5; c++) hsv(P.hue + base[c][0], base[c][1], 1, pal, c * 3); + P.palHue = P.hue; + } + // The correction comes in a prime at a time: the first third of the knob + // folds the 7s onto the streaks beside them, the second the 5s, the last + // the 3s, while everything glides to meet in the middle. + const kap = P.kap * 3, w7 = sm(kap), w5 = sm(kap - 1), w3 = sm(kap - 2); + for (let b = 0; b < 16; b++) { + off[b] = (b & 1 ? L3 * (1 - w3) : 0) + (b & 2 ? L5 * (1 - w5) : 0) + (b & 4 ? L7 * (1 - w7) : 0); + cls[b] = (b & 1 ? 0 : b & 2 ? 1 : b & 4 ? 2 : b & 8 ? 3 : 4) * 3; + } + // Smear leaves in the primes from 11 up. Each of them moves its + // multiples right by ln((p - 1)/(p - 2)), 0.105 for 11, 0.087 for 13, + // 0.065 for 17, so left in they fan every streak out four LEDs wide. + const shift = X0 + XS * MID * w3 - 0.5, keep = P.smear; + // The view is four octaves of n, a strip of ln2 n: row y holds + // [u + y du, u + (y + 1) du). The tape runs from 2^9 to 2^19 and starts + // over after ten dark rows. + const du = SPAN / H, u0 = U0, tape = U1 - u0 + 10 * du; + for (let y = 0; y < H; y++) { + acc.fill(0); + let uu = u0 + ((((P.u - u0 + y * du) % tape) + tape) % tape); + if (uu + du <= U1) { + const hlo = max(4, ceil(2 ** uu / 2)), hhi = min(HN + 1, ceil(2 ** (uu + du) / 2)); + const count = hhi - hlo; + // Every n in the row while there are few, every s-th when there are + // more than CAP. s is a power of 2: a stride of 17 would keep only + // multiples of 17 and move every streak 2.3 LEDs right by its 16/15. + const s = count > CAP ? 2 ** ceil(Math.log2(count / CAP)) : 1; + for (let h = ceil(hlo / s) * s; h < hhi; h += s) { + const b = bits[h], x = shift + XS * (res[h] + keep * lr[h] + off[b]), x0 = floor(x), f = x - x0, c = cls[b]; + if (x0 >= 0 && x0 < W) { + const o = x0 * 4, w = 1 - f; + (acc[o] += w), (acc[o + 1] += w * pal[c]), (acc[o + 2] += w * pal[c + 1]), (acc[o + 3] += w * pal[c + 2]); + } + if (x0 + 1 >= 0 && x0 + 1 < W) { + const o = x0 * 4 + 4; + (acc[o] += f), (acc[o + 1] += f * pal[c]), (acc[o + 2] += f * pal[c + 1]), (acc[o + 3] += f * pal[c + 2]); + } + } + } + // Dots in one LED add up, on a log scale against the busiest LED of the + // row: full at 40% of it, and one dot lights an LED to about a third in + // a crowded row and to half in a sparse one, so a lone dot in a far + // tail stays faint and the core of a streak stands out. The colour is + // the mix of the classes that landed there. + let top = 0; + for (let x = 0; x < W; x++) top = max(top, acc[x * 4]); + const norm = 1 / log(1 + 2 * max(4, 0.4 * top)); + for (let x = 0; x < W; x++) { + const o = x * 4, a = acc[o], q = (y * W + x) * 3; + if (a <= 0) { + rgb[q] = rgb[q + 1] = rgb[q + 2] = 0; + continue; + } + const lev = min(1, log(1 + 2 * a) * norm) / a; + rgb[q] = acc[o + 1] * lev; + rgb[q + 1] = acc[o + 2] * lev; + rgb[q + 2] = acc[o + 3] * lev; + } + } + for (let y = 0, q = 0; y < H; y++) + for (let x = 0; x < W; x++, q += 3) display.setPixel(x, y, min(255, rgb[q] * 255), min(255, rgb[q + 1] * 255), min(255, rgb[q + 2] * 255)); +} diff --git a/incoming/pattern-study/patterns/sorting-network.js b/incoming/pattern-study/patterns/sorting-network.js new file mode 100644 index 000000000..b4860ffbd --- /dev/null +++ b/incoming/pattern-study/patterns/sorting-network.js @@ -0,0 +1,331 @@ +// ===== Patternflow pattern ===== +// Title: Sorting Network +// Family: line +// Idea: Sixteen coloured threads run down the panel and trade places one adjacent pair at a time along a uniformly random sorting network, the shortest way to reverse their order, so every thread swings across the panel on a sine curve while the swaps, marked as bright dots, crowd towards the middle in a semicircle. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: uniform random sorting networks (Omer Angel, Alexander Holroyd, Dan Romik, Bálint Virág, 2007), pattern round 33 +// Round: 33 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Wires=16..32, Bubble=0..1 +// +// Knob 1: Hue. Turns the threads' colours round the wheel together; at 0 they run gold, green, cyan, blue to violet from the thread that starts on the left to the one that starts on the right. +// Knob 2: Speed. Rows per second the picture scrolls up, four times the knob: at 2 eight rows a second and one whole network, 128 rows, every 16 seconds; at 0.2 a network takes 160 seconds. +// Knob 3: Wires. How many threads, 16 at the start up to 32. One network always fills 128 rows, so 16 threads make 120 swaps in it, about one a row, and 32 make 496, about four a row. +// Knob 4: Bubble. Blends each network from uniform at 0 to the bubble-sort network at 1, where one thread at a time runs straight across all the threads before it on a row of dots and the rest step aside in parallel curves. At 0.5 bundles of parallel threads show through, and by 0.75 the sine curves are gone. The networks on the panel change as it turns. +// Buttons: K1 tap lights one thread for eight seconds and dims the rest, so its sine curve stands out from end to end; each tap picks another. + +const W = 64, H = 128, NET = 128; +const HMAX = 12, HALF_PI = Math.PI / 2; +const { min, max, floor, ceil, sin, cos } = Math; +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; +} + +// A uniformly random standard Young tableau of the staircase (n-1, ..., 1), +// by the hook walk of Greene, Nijenhuis and Wilf (1979): start on a uniform +// cell, jump to a uniform other cell of its hook until the walk stands on a +// corner, write the largest number not yet used there, take the corner off. +// T[r * (n-1) + c], 0-indexed, rows of n-1-r cells. +function hookWalk(n) { + const R = n - 1, len = new Int32Array(R), T = new Int32Array(R * R); + for (let r = 0; r < R; r++) len[r] = R - r; + let cells = (n * (n - 1)) / 2; + for (let val = cells; val >= 1; val--, cells--) { + let k = floor(Math.random() * cells), r = 0; + while (k >= len[r]) (k -= len[r]), r++; + let c = k; + for (;;) { + const arm = len[r] - 1 - c; + let leg = 0; + while (r + leg + 1 < R && len[r + leg + 1] > c) leg++; + if (arm + leg === 0) break; + const h = floor(Math.random() * (arm + leg)); + if (h < arm) c += 1 + h; + else r += 1 + h - arm; + } + T[r * R + c] = val; + len[r]--; + } + return T; +} + +// The tableau read row by row, 1 to N: under Edelman-Greene it is the +// bubble-sort network run backwards in time, 1 | 2 1 | 3 2 1 | ..., where +// the thread in place k walks left past all k threads before it in one +// straight diagonal. Read column by column it is the mirror image. +function readTableau(n, byRow) { + const R = n - 1, T = new Int32Array(R * R); + let v = 1; + for (let a = 0; a < R; a++) for (let b = 0; b < R - a; b++) T[byRow ? a * R + b : b * R + a] = v++; + return T; +} + +// Edelman-Greene (1987), as Angel, Holroyd, Romik and Virag lay it out: +// N times over, the largest entry N sits in a corner (i, j) with i + j = n, +// 1-indexed; swap places j and j + 1; empty that cell and slide the hole to +// (1, 1), each time moving in the larger of the neighbours above and to the +// left; add 1 to every entry and write 1 in (1, 1). Adding 1 to everything +// is kept as one offset, so a network costs N * n steps, not N * N. Checked +// in a scratch script over 1400 networks with n from 3 to 32: every swap +// takes a pair still in order and each network ends reversed; at n = 4 all +// 16 reduced words of the longest permutation came out equally often. +function edelmanGreene(T0, n, out) { + const R = n - 1, N = (n * (n - 1)) / 2, t = Int32Array.from(T0); + let off = 0; + for (let s = 0; s < N; s++) { + let r = 0; + while (r < R && t[r * R + R - 1 - r] + off !== N) r++; + let c = R - 1 - r; + out[s] = c; + for (;;) { + const up = r > 0 ? t[(r - 1) * R + c] : -1e9, lf = c > 0 ? t[r * R + c - 1] : -1e9; + if (r === 0 && c === 0) break; + if (up > lf) (t[r * R + c] = up), r--; + else (t[r * R + c] = lf), c--; + } + off++; + t[0] = 1 - off; + } +} + +// Each cell gets (1 - b) * uniform + b * bubble. Both tableaux grow along +// rows and down columns, so their blend does too, and ranking the cells by +// it is a standard tableau again: a sorting network for every b. +function network(P, net) { + const n = P.n, R = n - 1, N = (n * (n - 1)) / 2, b = P.bubble; + const Tb = (net.m & 1) === 0 ? P.tabRow : P.tabCol; + let T = net.tu; + if (b > 0) { + const key = new Float64Array(R * R), ids = []; + for (let r = 0; r < R; r++) for (let c = 0; c < R - r; c++) { + const i = r * R + c; + key[i] = (1 - b) * net.tu[i] + b * Tb[i]; + ids.push(i); + } + ids.sort((x, y) => key[x] - key[y] || x - y); + T = new Int32Array(R * R); + for (let k = 0; k < N; k++) T[ids[k]] = k + 1; + } + edelmanGreene(T, n, net.sw); + // The cheap form of the check, once a network: every swap must take a + // pair still in its starting order, and the end must be the reversal. + // A miss falls back on bubble sort, which is always right. + const a = Int32Array.from({ length: n }, (_, i) => i); + let ok = true; + for (let s = 0; s < N && ok; s++) { + const p = net.sw[s], x = a[p]; + if (x > a[p + 1]) ok = false; + (a[p] = a[p + 1]), (a[p + 1] = x); + } + for (let i = 0; i < n && ok; i++) if (a[i] !== n - 1 - i) ok = false; + if (!ok) edelmanGreene(Tb, n, net.sw); + net.b = b; +} + +function rebuild(P) { + const n = P.n; + P.tabRow = readTableau(n, true); + P.tabCol = readTableau(n, false); + P.nets = new Map(); + P.cols = new Float32Array(n * 3); + // Four threads are drawn bright, both edges and two between, 0, 5, 10 + // and 15 of sixteen: the edges swing all the way across on half a cosine, + // the two inside on arcs whose height the network picks. + P.lead = new Uint8Array(n); + for (let k = 0; k < 4; k++) P.lead[Math.round((k * (n - 1)) / 3)] = 1; + P.colHue = NaN; + const cap = 4 * ((n * (n - 1)) / 2) + 8; + P.mt = new Float32Array(cap); + P.md = new Int8Array(cap); + P.mw = new Int32Array(cap); + P.mh = new Float32Array(cap); + P.mr = new Float32Array(cap); + P.dt_ = new Float32Array(cap); + P.dp = new Int32Array(cap); + P.da = new Int32Array(cap); + P.db = new Int32Array(cap); +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, n: 16, bubble: 0.06, scroll: 0, hl: -1, hlT: 0, hlA: 0, + buf: new Float32Array(W * H * 3), + }; + rebuild(params.s); +} + +function getNet(P, m) { + let net = P.nets.get(m); + if (!net) { + net = { m, tu: hookWalk(P.n), sw: new Int32Array((P.n * (P.n - 1)) / 2), b: -1 }; + P.nets.set(m, net); + } + if (net.b !== P.bubble) network(P, net); + return net; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 16, 0.06], d = cl(dt || 0, 0, 0.1); + P.hue = k[0]; + P.speed = cl(k[1], 0.2, 3); + const n = cl(Math.round(k[2]), 16, 32); + if (n !== P.n) (P.n = n), rebuild(P), (P.hl = -1), (P.hlT = 0); + // Bubble moves in steps of 1/200 so a knob resting between two readings + // does not rebuild the networks every frame. + P.bubble = Math.round(cl(k[3], 0, 1) * 200) / 200; + if (input.btnPressed && input.btnPressed[0]) { + let w = floor(Math.random() * (P.hl < 0 ? P.n : P.n - 1)); + if (P.hl >= 0 && w >= P.hl) w++; + P.hl = w; + P.hlT = 8; + } + P.hlT = max(0, P.hlT - d); + P.hlA += ((P.hlT > 0 ? 1 : 0) - P.hlA) * min(1, d * 5); + P.scroll += d * P.speed * 4; + const first = floor(P.scroll / NET) - 1; + for (const m of P.nets.keys()) if (m < first) P.nets.delete(m); +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const n = P.n, N = (n * (n - 1)) / 2, buf = P.buf, sp = W / n; + if (P.colHue !== P.hue) { + // Gold through green, cyan and blue to violet: the thread that starts on + // the left is gold at Hue 0, the one on the right violet, never red. + for (let v = 0; v < n; v++) hsv(P.hue + 0.14 + (0.64 * v) / (n - 1), 0.82, 1, P.cols, v * 3); + P.colHue = P.hue; + } + buf.fill(0); + // Every move in sight, in time order. Row y holds network time scroll + y; + // network m fills times 128 m to 128 (m + 1) and starts from the identity + // when m is even, from the reversal when m is odd, so threads run on from + // one network into the next and nothing has to be carried between them. + const T0 = P.scroll - HMAX - 1, T1 = P.scroll + H + HMAX + 1; + const cnt = new Int32Array(n), mt = P.mt, md = P.md, mw = P.mw, mh = P.mh, mr = P.mr; + // Moves are grouped by thread: thread w owns slots w * per .. w * per + per. + const per = floor(mt.length / n); + let nd = 0; + for (let m = floor(T0 / NET); m <= floor(T1 / NET); m++) { + const net = getNet(P, m), st = new Int32Array(n); + for (let p = 0; p < n; p++) st[p] = m & 1 ? n - 1 - p : p; + for (let s = 0; s < N; s++) { + const t = m * NET + ((s + 0.5) * NET) / N, p = net.sw[s], a = st[p], b = st[p + 1]; + st[p] = b; + st[p + 1] = a; + if (t < T0 || t > T1) continue; + if (cnt[a] >= per || cnt[b] >= per) continue; + const ia = a * per + cnt[a]++, ib = b * per + cnt[b]++; + (mt[ia] = t), (md[ia] = 1), (mw[ia] = p); + (mt[ib] = t), (md[ib] = -1), (mw[ib] = p + 1); + P.dt_[nd] = t; + P.dp[nd] = p; + P.da[nd] = a; + P.db[nd] = b; + nd++; + } + } + // A thread leaves the middle of its place halfway between its swaps, + // meets its partner on the boundary at the swap and settles in the middle + // of the next place halfway to its swap after, never taking more than + // HMAX rows a side. Each thread keeps its own timing, so it is always + // under way and still crosses its partner at the dot: both are on the + // boundary at the swap time. + for (let w = 0; w < n; w++) { + const o = w * per; + for (let i = 0; i < cnt[w]; i++) { + mh[o + i] = i > 0 ? min(HMAX, (mt[o + i] - mt[o + i - 1]) / 2) : HMAX; + mr[o + i] = i + 1 < cnt[w] ? min(HMAX, (mt[o + i + 1] - mt[o + i]) / 2) : HMAX; + } + } + const hlA = P.hlA, cols = P.cols; + for (let w = 0; w < n; w++) { + const o = w * per; + // Place of thread w at the top of the window: where its first move in + // sight starts from, or, with no move in sight, where it sits. + let i = 0; + let base = cnt[w] > 0 ? mw[o] - md[o] : placeAt(P, w, T0); + const lead = P.lead[w], lit = w === P.hl ? 1 : 1 - 0.75 * hlA; + const gain = ((lead ? 0.85 : 0.4) + (w === P.hl ? (1 - (lead ? 0.85 : 0.4)) * hlA : 0)) * lit; + const cr = cols[w * 3] * gain, cg = cols[w * 3 + 1] * gain, cb = cols[w * 3 + 2] * gain; + // A quarter cosine into the boundary and a quarter sine out of it. + const xAt = (t) => { + while (i < cnt[w] && t >= mt[o + i] + mr[o + i]) (base += md[o + i]), i++; + let pos = base; + if (i < cnt[w]) { + const u = t - mt[o + i]; + if (u >= 0) pos += md[o + i] * (0.5 + 0.5 * sin((u / mr[o + i]) * HALF_PI)); + else if (u > -mh[o + i]) pos += md[o + i] * 0.5 * (1 - cos(((u + mh[o + i]) / mh[o + i]) * HALF_PI)); + } + return (pos + 0.5) * sp; + }; + for (let y = 0; y < H; y++) { + const t = P.scroll + y, xa = xAt(t), xm = xAt(t + 0.5), xb = xAt(t + 1); + const lo = min(xa, xm, xb), hi = max(xa, xm, xb); + const c0 = max(0, floor(lo - 1)), c1 = min(W - 1, ceil(hi + 1)); + for (let c = c0; c <= c1; c++) { + const cx = c + 0.5, dist = cx < lo ? lo - cx : cx > hi ? cx - hi : 0; + const a = cl((1 - dist) * 1.6, 0, 1); + if (a <= 0) continue; + const q = (y * W + c) * 3; + buf[q] += cr * a; + buf[q + 1] += cg * a; + buf[q + 2] += cb * a; + } + } + } + // The swaps: a pale dot where the two threads meet, half white and half + // the mix of the pair. Their density across the panel is the semicircle. + for (let j = 0; j < nd; j++) { + const yc = P.dt_[j] - P.scroll, xc = (P.dp[j] + 1) * sp; + if (yc < -2 || yc > H + 2) continue; + const a = P.da[j], b = P.db[j]; + const dim = hlA > 0 && a !== P.hl && b !== P.hl ? 1 - 0.6 * hlA : 1; + const r = (0.55 + 0.25 * (cols[a * 3] + cols[b * 3])) * dim; + const g = (0.55 + 0.25 * (cols[a * 3 + 1] + cols[b * 3 + 1])) * dim; + const bl = (0.55 + 0.25 * (cols[a * 3 + 2] + cols[b * 3 + 2])) * dim; + for (let y = floor(yc - 1.5); y <= ceil(yc + 1.5); y++) { + if (y < 0 || y >= H) continue; + for (let x = floor(xc - 1.5); x <= ceil(xc + 1.5); x++) { + if (x < 0 || x >= W) continue; + const dx = x + 0.5 - xc, dy = y + 0.5 - yc, f = cl(1.15 - (dx * dx + dy * dy) * 0.55, 0, 1); + if (f <= 0) continue; + const q = (y * W + x) * 3; + buf[q] = max(buf[q], r * f); + buf[q + 1] = max(buf[q + 1], g * f); + buf[q + 2] = max(buf[q + 2], bl * f); + } + } + } + for (let p = 0, q = 0; p < W * H; p++, q += 3) { + const x = p % W, y = (p / W) | 0; + display.setPixel(x, y, min(255, buf[q] * 255), min(255, buf[q + 1] * 255), min(255, buf[q + 2] * 255)); + } +} + +// Where thread w sits at time t, by playing its network from the start. +function placeAt(P, w, t) { + const n = P.n, N = (n * (n - 1)) / 2, m = floor(t / NET), net = getNet(P, m); + let pos = m & 1 ? n - 1 - w : w; + const st = new Int32Array(n); + for (let p = 0; p < n; p++) st[p] = m & 1 ? n - 1 - p : p; + for (let s = 0; s < N; s++) { + if (m * NET + ((s + 0.5) * NET) / N > t) break; + const p = net.sw[s], a = st[p]; + st[p] = st[p + 1]; + st[p + 1] = a; + if (st[p] === w) pos = p; + else if (st[p + 1] === w) pos = p + 1; + } + return pos; +} From 0f1bbc4244830af5a6b20d2dea8bb7225675684f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 15:14:43 +0000 Subject: [PATCH 05/10] pattern-study: Dandelin Spheres, Cleansing Balm Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117bhD83MGCSerAR798cUib --- .../pattern-study/patterns/cleansing-balm.js | 372 +++++++++++++++ .../patterns/dandelin-spheres.js | 428 ++++++++++++++++++ 2 files changed, 800 insertions(+) create mode 100644 incoming/pattern-study/patterns/cleansing-balm.js create mode 100644 incoming/pattern-study/patterns/dandelin-spheres.js diff --git a/incoming/pattern-study/patterns/cleansing-balm.js b/incoming/pattern-study/patterns/cleansing-balm.js new file mode 100644 index 000000000..e2e9a1e32 --- /dev/null +++ b/incoming/pattern-study/patterns/cleansing-balm.js @@ -0,0 +1,372 @@ +// ===== Patternflow pattern ===== +// Title: Cleansing Balm +// Family: cosmetic +// Idea: A whipped sherbet balm fills the panel, matte and frosty with spoon-scooped hollows and lips, until warmth climbs from the bottom and unseen fingertips press into it, and wherever it reaches the balm slumps into clear glossy oil with a wet rim at its edge, highlights gliding over it and a lighter glow showing through, while the oil runs down and pools at the bottom before a fresh layer is whipped on from the top. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 33, after a sherbet cleansing balm melting into oil +// Round: 33 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Warmth=0..1, Grain=0..1 +// +// Knob 1: Hue. The balm's colour, all the way round the wheel from pink sherbet at 0, the oil a deeper clear tint of it. Round orange and yellow the balm is paled and the oil lifted to honey, so neither goes brown. +// Knob 2: Speed. At 2 the warm front climbs 5 to 12 LEDs a second, a fingertip comes down every 1.4 to 3.2 s and one melt runs 18 to 22 s from fresh balm to fresh balm; at 0.2 all of it ten times slower. +// Knob 3: Warmth. How fast and how far it melts: at 0 the front climbs 5 LEDs a second and stops about 48 rows from the top, with a fingertip every 3.2 s; at 1 it climbs 12 a second and takes the whole panel, with a bigger fingertip every 1.4 s. +// Knob 4: Grain. The frost in the matte balm: at 0 a fine sugar a LED across over a soft whip; at 1 coarse crystals two or three LEDs across, with more sparkling specks and lumpier whip. The oil is smooth at any setting. +// Buttons: K1 tap presses a warm fingertip into the balm, and a round patch 20 LEDs across melts in a quarter of a second. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, sqrt, sin, cos, exp, abs, PI } = Math; +const rnd = () => Math.random(); +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (v) => ((v = cl(v, 0, 1)), v * v * (3 - 2 * v)); +const hash = (i) => { + let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; + +// Pink sherbet at Hue 0. +const H0 = 0.95; +// A melted LED gives REL of oil. A film FILM thick clings where it is; above +// that the oil runs down, faster the thicker it is, and a cell holds at most +// CAP, so the pool fills up from the bottom row. +const REL = 0.42, FILM = 0.07, CAP = 1, KFLOW = 9; + +// Smooth noise in a 128 x 128 tile, three octaves of 4, 8 and 16 cells. +const T = 128, TM = T - 1; +const TEX = new Float32Array(T * T); +const fade = (t) => t * t * (3 - 2 * t); +function buildTex() { + TEX.fill(0); + let amp = 1, tot = 0; + for (let o = 0; o < 3; o++) { + const cells = 4 << o, size = T / cells, g = new Float32Array(cells * cells); + for (let i = 0; i < g.length; i++) g[i] = rnd() * 2 - 1; + for (let y = 0; y < T; y++) { + const gy = floor(y / size), fy = fade(y / size - gy), y1 = (gy + 1) % cells; + for (let x = 0; x < T; x++) { + const gx = floor(x / size), fx = fade(x / size - gx), x1 = (gx + 1) % cells; + const a = g[gy * cells + gx], b = g[gy * cells + x1], c = g[y1 * cells + gx], d = g[y1 * cells + x1]; + const top = a + (b - a) * fx; + TEX[y * T + x] += (top + (c + (d - c) * fx - top) * fy) * amp; + } + } + tot += amp; + amp *= 0.5; + } + for (let i = 0; i < TEX.length; i++) TEX[i] /= tot; +} +function tap(u, v) { + const fu = floor(u), fv = floor(v), x0 = fu & TM, y0 = fv & TM, x1 = (x0 + 1) & TM, y1 = (y0 + 1) & TM; + const tx = u - fu, ty = v - fv; + const a = TEX[y0 * T + x0], b = TEX[y0 * T + x1], c = TEX[y1 * T + x0], d = TEX[y1 * T + x1]; + return a + (b - a) * tx + (c - a) * ty + (a - b - c + d) * tx * ty; +} + +// The frost: a fine grain, one value per LED, and a coarse one, the same +// values blurred over 3 x 3 LEDs and scaled back up, so its crystals are two +// or three LEDs across. SPK picks the sparkling specks. +const GF = Float32Array.from({ length: N }, (_, i) => hash(i + 77) * 2 - 1); +const GC = new Float32Array(N); +for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let s = 0; + for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) s += GF[((y + dy + H) % H) * W + ((x + dx + W) % W)]; + GC[y * W + x] = s / 3; + } +const SPK = Float32Array.from({ length: N }, (_, i) => hash(i + 991)); + +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) { + buildTex(); + const F = (n) => new Float32Array(n); + params.s = { + Hb: F(N), mb: F(N), nz: F(N), Hn: F(N), mbN: F(N), nzN: F(N), + m: F(N), F: F(N), age: F(N), Ho: F(N), Hs: F(N), wo: F(N), + phase: 0, Yf: H + 10, Yl: -10, row: 0, hold: 0, T: 0, next: 1, fingers: [], dir: 1, + hue: 0, speed: 2, warm: 0, grain: 0.06, wm: 0, started: false, col: F(7 * 3), colHue: NaN, + }; +} + +// A fresh layer of balm. A soft whip of lumps, then fifteen spoon scoops, each +// an oval hollow 15 to 25 LEDs long with faint rings where the spoon's edge +// dragged and a lip of balm piled up at the end the spoon left by. nz is how +// long each LED holds out against warmth, and mk is the sherbet's marbling. +function lay(Hh, mk, nz) { + const ox = rnd() * T, oy = rnd() * T; + for (let y = 0, i = 0; y < H; y++) + for (let x = 0; x < W; x++, i++) { + Hh[i] = 0.3 * tap(x * 1.7 + ox, y * 1.7 + oy); + mk[i] = tap(x * 0.55 + oy, y * 0.38 + ox + 40); + nz[i] = 0.9 * tap(x * 2.6 + ox + 60, y * 2.6 + oy); + } + for (let k = 0; k < 15; k++) { + const L = 15 + rnd() * 10, Wd = L * (0.5 + 0.15 * rnd()), a = rnd() * 2 * PI, ca = cos(a), sa = sin(a); + const cx = rnd() * W, cy = -6 + rnd() * (H + 12), E = L * 1.4; + for (let y = max(0, floor(cy - E)); y <= min(H - 1, floor(cy + E)); y++) + for (let x = max(0, floor(cx - E)); x <= min(W - 1, floor(cx + E)); x++) { + const dx = x + 0.5 - cx, dy = y + 0.5 - cy, u = (dx * ca + dy * sa) / L, v = (-dx * sa + dy * ca) / Wd; + const rho = sqrt(u * u + v * v); + if (rho > 1.4) continue; + const i = y * W + x, wgt = 1 - sm((rho - 0.82) / 0.3); + const dish = rho < 1 ? -0.75 * (1 - rho * rho) + 0.025 * sin(rho * 19) : 0; + const lip = 0.55 * exp(-(((rho - 1.02) / 0.14) ** 2)) * (0.4 + 0.6 * sm((u + 1) / 2)); + Hh[i] = Hh[i] * (1 - wgt) + dish * wgt + lip; + } + } +} + +// A fingertip: it comes down at (x, y) and its warm print spreads to radius R +// over `grow` seconds, melting what it covers. +function finger(S, big) { + let x = 8 + rnd() * (W - 16), y = 0; + // Into the balm, above the front. + const hiY = min(H - 10, S.Yf - 14); + if (hiY > 12) y = 8 + rnd() * (hiY - 8); + else y = 10 + rnd() * (H - 20); + if (big) x = 14 + rnd() * (W - 28); + S.fingers.push({ x, y, R: big ? 10 : 6 + rnd() * 4 + 3 * S.warm, grow: big ? 0.25 : 0.8, t: 0, r: 0 }); +} + +function step(S, st) { + const { m, F, age, Hb, nz } = S; + S.T += st; + if (S.phase === 0) { + S.Yf -= st * (5 + 7 * S.warm); + S.next -= st; + if (S.next <= 0) { + S.next = (3.2 - 1.8 * S.warm) * (0.7 + 0.6 * rnd()); + finger(S, false); + } + if (S.Yf < H - (0.57 + 0.5 * S.warm) * (H + 12)) { + S.phase = 1; + S.hold = 0; + } + } else if (S.phase === 1) { + S.hold += st; + if (S.hold > 1.5) { + // Whip a fresh layer on from the top. + S.phase = 2; + S.Yl = -6; + S.row = 0; + S.Yf = H + 1000; + lay(S.Hn, S.mbN, S.nzN); + } + } else { + S.Yl += st * 46; + const to = min(H, floor(S.Yl)); + for (; S.row < to; S.row++) + for (let x = 0, i = S.row * W; x < W; x++, i++) { + Hb[i] = S.Hn[i]; + S.mb[i] = S.mbN[i]; + nz[i] = S.nzN[i]; + m[i] = 0; + F[i] = 0; + age[i] = 0; + } + if (S.Yl > H + 8) { + S.phase = 0; + S.Yf = H + 10; + S.next = 0.6; + S.fingers.length = 0; + } + } + // The front: LEDs below it melt, ridges and the lumps of nz holding out a + // few rows longer, so it climbs ragged. Once an LED starts it melts through + // in about 0.4 s, and that band is the wet rim. + for (let y = 0, i = 0; y < H; y++) { + const wf = (y - S.Yf) / 4; + for (let x = 0; x < W; x++, i++) { + if (m[i] >= 1) { + age[i] += st; + continue; + } + if (m[i] > 0 || wf > 0.9 * Hb[i] + nz[i]) { + const dm = min(1 - m[i], st * 2.4); + m[i] += dm; + F[i] += dm * REL; + } + } + } + const fs = S.fingers; + for (let k = fs.length - 1; k >= 0; k--) { + const f = fs[k]; + f.t += st; + f.r = f.R * sm(f.t / f.grow); + const E = f.R + 3; + for (let y = max(0, floor(f.y - E)); y <= min(H - 1, floor(f.y + E)); y++) + for (let x = max(0, floor(f.x - E)); x <= min(W - 1, floor(f.x + E)); x++) { + const i = y * W + x; + if (m[i] > 0) continue; + const d = Math.hypot(x + 0.5 - f.x, y + 0.5 - f.y); + if (d < f.r - 1.5 * (0.9 * Hb[i] + nz[i])) m[i] = 0.01; + } + if (f.t > f.grow + 0.5) fs.splice(k, 1); + } + // The oil runs down a row at a time, bottom row first so nothing moves two + // rows in one step. + for (let y = H - 2; y >= 0; y--) + for (let x = 0, i = y * W; x < W; x++, i++) { + // Unmelted balm soaks up a little, so oil only trickles over it. + const mob = F[i] - (m[i] > 0 ? FILM : 0.22); + if (mob <= 0) continue; + const j = i + W; + let fl = min(mob, st * KFLOW * mob * (0.25 + F[i])); + fl = min(fl, CAP - F[j]); + if (fl <= 0) continue; + F[i] -= fl; + F[j] += fl; + } + // The oil levels out sideways, quickly where it is deep, so the pool's top + // is flat and a film does not keep the stripes it ran down in. + S.dir = -S.dir; + for (let y = 0; y < H; y++) { + const r0 = y * W; + for (let n = 0; n < W - 1; n++) { + const x = S.dir > 0 ? n : W - 2 - n, i = r0 + x, d = F[i] - F[i + 1]; + if (F[i] < FILM && F[i + 1] < FILM) continue; + const mv = d * (F[i] > 0.3 || F[i + 1] > 0.3 ? 0.35 : 0.15); + F[i] -= mv; + F[i + 1] += mv; + } + } +} + +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 = k[0] || 0; + S.speed = cl(k[1], 0.2, 3); + S.warm = cl(k[2], 0, 1); + S.grain = cl(k[3], 0, 1); + if (!S.started) { + S.started = true; + lay(S.Hb, S.mb, S.nz); + // Open with the bottom fifth already melted and a fingertip or two in. + S.Yf = H - 12; + S.next = 0.3; + for (let i = 0; i < 70; i++) step(S, 1 / 20); + } + if (input.btnPressed && input.btnPressed[0]) finger(S, true); + step(S, (d * S.speed) / 2); +} + +// Colours: the sherbet, its creamy marbling, the glow under the oil, the +// oil's own tint, the wet rim and the frost's specks. +function palette(S) { + const c = S.col, h = S.hue + H0, hf = h - floor(h); + // Orange and gold go brown at middle light on the LEDs, so round them + // everything is lifted and paled: apricot, peach and butter. + const dw = abs(hf - 0.13), wm = sm(1.5 - min(dw, 1 - dw) / 0.12); + hsv(h, 0.44 - 0.14 * wm, 0.98, c, 0); + hsv(h - 0.015, 0.22 - 0.06 * wm, 1, c, 3); + hsv(h - 0.02, 0.32 - 0.1 * wm, 1, c, 6); + hsv(h + 0.015, 0.78 - 0.38 * wm, 0.62 + 0.36 * wm, c, 9); + hsv(h, 0.22, 1, c, 12); + hsv(h + 0.05, 0.08, 1, c, 15); + S.wm = wm; + S.colHue = S.hue; +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.started) return; + if (S.colHue !== S.hue) palette(S); + const { Hb, mb, m, F, age, Ho, Hs, col } = S, t = S.T; + // How much of each LED is oil: all of it once melted, and a wet film + // where oil runs down over balm that has not melted yet. + const wo = S.wo; + for (let i = 0; i < N; i++) wo[i] = max(m[i], 0.7 * sm((F[i] - 0.12) / 0.3)); + // The oil's surface: what is left of the balm's shape as it slumps flat in + // the two seconds after melting, the oil's own depth, and slow ripples + // carried down the panel at about 6 LEDs a second, then softened over + // 3 x 3 LEDs so the oil has no grain at all. + for (let y = 0, i = 0; y < H; y++) + for (let x = 0; x < W; x++, i++) { + if (wo[i] <= 0) { + Ho[i] = Hb[i] * 0.7; + continue; + } + const sl = m[i] > 0 ? max(0, 1 - age[i] / 2) : 1, f = min(F[i], 1.2); + Ho[i] = 0.7 * Hb[i] * sl + 1.2 * f + 0.35 * tap(x * 1.1 + 20, y * 0.7 - t * 5) * (0.4 + f); + } + for (let y = 0, i = 0; y < H; y++) + for (let x = 0; x < W; x++, i++) Hs[i] = (Ho[i - (x > 0 ? 1 : 0)] + 2 * Ho[i] + Ho[i + (x < W - 1 ? 1 : 0)]) * 0.25; + for (let y = 0, i = 0; y < H; y++) + for (let x = 0; x < W; x++, i++) Ho[i] = (Hs[i - (y > 0 ? W : 0)] + 2 * Hs[i] + Hs[i + (y < H - 1 ? W : 0)]) * 0.25; + const g = S.grain, gA = 0.04 + 0.17 * g, spD = 0.03 + 0.09 * g, spV = 0.09 + 0.08 * g, lumps = 1 + 0.35 * g; + // A small light 60 degrees off the face of the panel wanders round the top + // left, so flat oil never catches it and only the slopes of ripples, + // slumping lumps and the pool's edge glint. Two long reflections of strip + // lights sweep across the oil and back every 18 s at Speed 2. + const az = -2.2 + 0.7 * sin(t * 0.37), lx = 0.866 * cos(az), ly = 0.866 * sin(az), lz = 0.5; + const hx0 = lx, hy0 = ly, hz0 = lz + 1, hl = Math.hypot(hx0, hy0, hz0); + const hx = hx0 / hl, hy = hy0 / hl, hz = hz0 / hl; + const band = 1.1 * sin(t * 0.35), band2 = band + (band > 0 ? -1.4 : 1.4); + // Warm oil at middle light is brown, so round orange and gold the oil + // glows brighter and turns to honey. + const lift = 1 + 0.32 * S.wm, tk = 0.85 + 0.25 * S.wm; + // The edge of a fresh layer being whipped on stands up as a rolled ridge. + const whip = S.phase === 2, Yl = S.Yl; + for (let y = 0, i = 0; y < H; y++) { + const up = y > 0 ? -W : 0, dn = y < H - 1 ? W : 0; + const roll = whip ? 0.9 * exp(-(((y - Yl + 2.5) / 2.6) ** 2)) : 0, rollU = whip ? 0.9 * exp(-(((y - 1 - Yl + 2.5) / 2.6) ** 2)) : 0; + for (let x = 0; x < W; x++, i++) { + const lf = x > 0 ? -1 : 0, rt = x < W - 1 ? 1 : 0; + const mm = m[i], w = wo[i]; + let r = 0, gg = 0, b = 0; + if (mm < 1) { + // Matte balm: soft light from the top left, a little darker deep in + // the hollows, the marbling between sherbet and cream, and frost. + const ddx = (Hb[i + rt] - Hb[i + lf]) * 2.2 * lumps, ddy = (Hb[i + dn] - Hb[i + up]) * 2.2 * lumps + (roll - rollU) * 3; + const dif = (0.45 * ddx + 0.62 * ddy + 0.64) / sqrt(ddx * ddx + ddy * ddy + 1); + const sh = 0.8 + 0.22 * dif + 0.05 * cl(Hb[i] + roll, -0.8, 0.6); + const mk = sm(0.5 + 1.2 * sin(mb[i] * 11 + 0.8)); + const f = sh * (1 + gA * (GF[i] * (1 - g) + GC[i] * g)); + const sp = SPK[i] > 1 - spD ? spV : 0; + r = (col[0] + (col[3] - col[0]) * mk) * f + sp; + gg = (col[1] + (col[4] - col[1]) * mk) * f + sp; + b = (col[2] + (col[5] - col[2]) * mk) * f + sp; + } + if (w > 0) { + // Oil: the glow under it seen through a tint that deepens with the + // oil's depth, a sharp highlight from the small light, and the strip + // light's long reflection. + const nx = -(Ho[i + rt] - Ho[i + lf]) * 3.5, ny = -(Ho[i + dn] - Ho[i + up]) * 3.5, nl = 1 / sqrt(nx * nx + ny * ny + 1); + const nd = (nx * hx + ny * hy + hz) * nl; + const spec = nd > 0.9 ? ((nd - 0.9) / 0.1) ** 2 : 0; + const e = 2 * nl * (nx * 0.5 + ny * 0.85) * nl + (x - 32) / 90 + (y - 64) / 70, q = (e - band) / 0.14, q2 = (e - band2) / 0.1; + const s = min(1, spec) + 0.42 * exp(-q * q) + 0.28 * exp(-q2 * q2); + const dep = 0.3 + F[i], a = 1 - exp(-1.1 * dep); + const glow = (0.55 + 0.3 * tap(x * 0.6 + 70, y * 0.4 - t * 1.5)) * lift; + const orr = col[6] * glow * (1 - a) + col[9] * a * tk + s; + const og = col[7] * glow * (1 - a) + col[10] * a * tk + s; + const ob = col[8] * glow * (1 - a) + col[11] * a * tk + s; + r = r * (1 - w) + orr * w; + gg = gg * (1 - w) + og * w; + b = b * (1 - w) + ob * w; + // The wet rim: the band where the balm is turning, and the oil's + // edge against balm, a pale glassy line with a glint. + let rim = mm > 0 && mm < 1 ? 4 * mm * (1 - mm) : 0; + if (mm >= 1 && (m[i + lf] < 0.5 || m[i + rt] < 0.5 || m[i + up] < 0.5 || m[i + dn] < 0.5)) rim = 0.8; + if (rim > 0) { + const k = 0.45 * rim; + r += (col[12] - r) * k + 0.1 * rim; + gg += (col[13] - gg) * k + 0.1 * rim; + b += (col[14] - b) * k + 0.1 * rim; + } + } + display.setPixel(x, y, cl(r * 255, 0, 255), cl(gg * 255, 0, 255), cl(b * 255, 0, 255)); + } + } +} diff --git a/incoming/pattern-study/patterns/dandelin-spheres.js b/incoming/pattern-study/patterns/dandelin-spheres.js new file mode 100644 index 000000000..83fbfd672 --- /dev/null +++ b/incoming/pattern-study/patterns/dandelin-spheres.js @@ -0,0 +1,428 @@ +// ===== Patternflow pattern ===== +// Title: Dandelin Spheres +// Family: geometry +// Idea: A plane cuts a glass double cone in a conic while the two spheres that touch both the cone and the plane sit in it, each touching the plane at a focus and the cone along a circle, and a point running round the conic lights its two distances to the foci in the same colours as the two equal stretches of the cone's line through it, as the whole scene turns slowly about the axis. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Dandelin spheres (Germinal Pierre Dandelin, 1822), pattern round 33 +// Round: 33 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Tilt=30..90, Cone=16..34 +// +// Knob 1: Hue. Turns the scene round the wheel together. At 0 the lower sphere is teal, the upper one violet and the cone azure, the conic is white, and the two pairs of equal lengths are gold and pink. +// Knob 2: Speed. At 2 the scene turns once round the axis in about 44 s, hurrying past the moment the plane is seen edge on, and the point runs round the opening ellipse in about 4 s; at 0.2 both go ten times slower. +// Knob 3: Tilt. The plane's angle from level, in degrees. At 30 it cuts an ellipse of eccentricity 0.52 between a small sphere and one twice its size; the ellipse stretches as it tilts, at 74 the upper sphere has run off to infinity and the cut is a parabola, and past that the sphere comes back up from below in the other nappe and the cut is a hyperbola, one branch in each nappe. The view backs off to take in the lower nappe from about 50. +// Knob 4: Cone. The cone's half-angle, 16 to 34 degrees. A wider cone at the same tilt gives a rounder ellipse, a far bigger upper sphere and a camera standing further back, and the parabola comes at a tilt of 90 minus this angle, 56 at 34. +// Buttons: K1 tap swings the plane up to upright and back, through the parabola to the hyperbola and home again, in about 11 s at Speed 2. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, sqrt, sin, cos, tan, abs, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (v) => ((v = cl(v, 0, 1)), v * v * (3 - 2 * v)); +const RAD = PI / 180; + +// World units: the apex at the origin, y up the axis. The plane turns about +// the horizontal line through (XP, YP) along z, so at 90 degrees it stands +// 0.3 off the axis and still cuts a hyperbola instead of two crossing lines. +const XP = -0.3, YP = 0.95; +// The cone is cut off at these heights. +const YTOP = 2.25, YBOT = -2.4; +// The plane is drawn where |x| and |z| are under this. +const PX = 1.05; +// Camera: this far from the point it looks at, this many degrees above it. +const DIST = 9, ELEV = 28 * RAD; +// Five lines of the cone, round its axis. +const GEN = 5; +// The opening hue sits here on the wheel: teal. +const H0 = 0.5; + +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; +} +// How far a hue sits in the orange and yellow that turn brown when shaded. +const warm = (h) => { + h -= floor(h); + return max(0, 1 - abs(h - 0.11) / 0.1); +}; + +export function setup(params) { + params.s = { + th: 0.6, ph: 0, sw: -1, hue: 0, tilt: 30, cone: 16, speed: 2, lo: -0.25, + base: new Float32Array(N * 3), lin: new Float32Array(N * 3), + zO: new Float32Array(N), zC: new Float32Array(N), zP: new Float32Array(N), + col: new Float32Array(8 * 3), colHue: NaN, sc: {}, + }; +} + +// The scene for a tilt and half-angle in radians: the plane n.p = h with +// n = (-sin b, cos b, 0), and the two spheres on the axis at heights s with +// radius |s| sin a. A sphere at s is tangent to the cone, and tangent to the +// plane when |cos b s - h| = |s| sin a, so s = h / (cos b + sin a), the one +// between the apex and the plane, and s = h / (cos b - sin a), past the plane +// while cos b > sin a (an ellipse), at infinity when they are equal (a +// parabola), and below the apex in the other nappe once sin a is the larger +// (a hyperbola). +function scene(S, b, a) { + const sa = sin(a), ca = cos(a), sb = sin(b), cb = cos(b), h = cb * YP - sb * XP; + const sc = S.sc; + sc.sa = sa; + sc.ca = ca; + sc.k = sa / ca; + sc.nx = -sb; + sc.ny = cb; + sc.h = h; + sc.s1 = h / (cb + sa); + const den = cb - sa; + sc.s2 = abs(den) > 1e-4 ? h / den : 1e9; + sc.r1 = abs(sc.s1) * sa; + sc.r2 = abs(sc.s2) * sa; + // A focus is the foot of the perpendicular from a sphere's centre to the + // plane; a contact circle is where the sphere meets the cone, at height + // s cos^2 a. Along a line of the cone it sits s cos a from the apex. + for (const [s, k] of [[sc.s1, 1], [sc.s2, 2]]) { + const d = cb * s - h; + sc["fx" + k] = -d * sc.nx; + sc["fy" + k] = s - d * sc.ny; + sc["g" + k] = s * ca; + } + sc.e = sb / ca; +} + +// The conic as a curve over the cone's azimuth phi: the line of the cone at +// phi, t (sin a cos phi, cos a, sin a sin phi), meets the plane at +// t = h / (cos b cos a - sin b sin a cos phi). On a hyperbola t changes sign +// where that denominator does, and the point crosses to the other nappe. +function conicT(sc, phi) { + const den = sc.ny * sc.ca + sc.nx * sc.sa * cos(phi); + return abs(den) < 1e-6 ? 1e9 : sc.h / den; +} + +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 = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.tilt = cl(k[2], 30, 90); + S.cone = cl(k[3], 16, 34); + const sp = S.speed / 2; + if (input.btnPressed && input.btnPressed[0]) S.sw = 0; + let tilt = S.tilt; + if (S.sw >= 0) { + // Up to upright in 4.5 s, a 2 s rest, and back in 4.5 s, at Speed 2. + S.sw += (d * sp) / 11; + const u = S.sw, up = u < 0.41 ? sm(u / 0.41) : u < 0.59 ? 1 : 1 - sm((u - 0.59) / 0.41); + tilt += (90 - tilt) * up; + if (S.sw >= 1) S.sw = -1; + } + scene(S, tilt * RAD, S.cone * RAD); + const sc = S.sc; + // The view: level with the ellipse case's spheres while the cut is an + // ellipse, widening downwards to take in the other nappe as the eccentricity + // passes 0.8 on its way to the hyperbola. + const lo = -0.25 - 1.95 * sm((sc.e - 0.8) / 0.3); + S.lo += (lo - S.lo) * min(1, d * 3); + // The turn hurries past the azimuths where the camera sees the plane edge + // on and the conic flattens to a line, and lingers where it lies open. + const edgeOn = abs(sc.nx * cos(ELEV) * sin(S.th) + sc.ny * sin(ELEV)); + S.th += d * sp * ((2 * PI) / 36) * (0.6 + 1.8 * exp(-((edgeOn / 0.18) ** 2))); + // The point moves at about 0.55 units a second along the conic. Off the + // drawn part of a hyperbola or parabola it skips ahead to where it comes + // back in. + let left = d * sp * 0.55, guard = 0; + while (left > 0 && guard++ < 40) { + const t0 = conicT(sc, S.ph), t1 = conicT(sc, S.ph + 0.01); + const y0 = t0 * sc.ca; + if (y0 > YTOP || y0 < YBOT + 0.1) { + S.ph += 0.05; + continue; + } + const x0 = t0 * sc.sa * cos(S.ph), z0 = t0 * sc.sa * sin(S.ph); + const x1 = t1 * sc.sa * cos(S.ph + 0.01), z1 = t1 * sc.sa * sin(S.ph + 0.01), y1 = t1 * sc.ca; + const ds = Math.hypot(x1 - x0, y1 - y0, z1 - z0) / 0.01; + const step = min(left / max(ds, 1e-3), 0.3); + S.ph += step; + left -= step * ds; + } + S.ph %= 2 * PI; +} + +// Colours: lower sphere, upper sphere, cone, plane, conic, the two +// segment colours, white. +function palette(S) { + const c = S.col, h = S.hue + H0; + // The cone and the plane are seen faint, where a warm hue reads olive, so + // they lose more of their colour there than the bright parts do. + const set = (i, hh, s, v) => { + const w = warm(hh); + hsv(hh, s * (1 - (i === 2 || i === 3 ? 0.7 : 0.35) * w), v, c, i * 3); + }; + set(0, h, 0.72, 1); + set(1, h + 0.2, 0.62, 1); + set(2, h + 0.1, 0.55, 1); + set(3, h + 0.04, 0.5, 1); + set(4, h + 0.62, 0.14, 1); + set(5, h + 0.62, 0.6, 1); + set(6, h + 0.42, 0.55, 1); + set(7, 0, 0, 1); + S.colHue = S.hue; +} + +export function draw(display, params) { + const S = params.s; + if (!S || S.sc.h === undefined) return; + if (S.colHue !== S.hue) palette(S); + const sc = S.sc, col = S.col, base = S.base, lin = S.lin, zO = S.zO, zC = S.zC, zP = S.zP; + const hi = YTOP, lo = S.lo, ty = (hi + lo) / 2; + // A wider cone pulls the camera back, by the square root of how much + // wider it is than the 16 degree one, so a wide ellipse still fits. + const f = (128 * DIST) / ((hi - lo) * cos(ELEV) * sqrt(sc.k / tan(16 * RAD))); + // Camera basis. fwd looks at (0, ty, 0) from DIST away, ELEV above. + const ce = cos(ELEV), se = sin(ELEV), st = sin(S.th), ct = cos(S.th); + const ox = DIST * ce * st, oy = ty + DIST * se, oz = DIST * ce * ct; + const fx = -ce * st, fy = -se, fz = -ce * ct; + const rx = ct, rz = -st; // right = fwd x up, level + const ux = -fy * rz, uy = rz * fx - rx * fz, uz = fy * rx; + // The light hangs above and to the left of the camera. + let lx = -0.55 * rx + 0.75 * ux - 0.5 * fx, ly = 0.75 * uy - 0.5 * fy, lz = -0.55 * rz + 0.75 * uz - 0.5 * fz; + const ll = Math.hypot(lx, ly, lz); + lx /= ll; + ly /= ll; + lz /= ll; + const k2 = sc.k * sc.k, nx = sc.nx, ny = sc.ny, h = sc.h; + // Each sphere: height, radius, palette slot, and how warm its hue is. + const sph = [ + [sc.s1, sc.r1, 0, warm(S.hue + H0)], + [sc.s2, sc.r2, 3, warm(S.hue + H0 + 0.2)], + ]; + for (let py = 0, p = 0; py < H; py++) { + const v = (63.5 - py) / f, bg = 0.025 + 0.02 * (py / H); + for (let px = 0; px < W; px++, p++) { + const u = (px - 31.5) / f; + const dx = fx + u * rx + v * ux, dy = fy + v * uy, dz = fz + u * rz + v * uz; + const dd = dx * dx + dy * dy + dz * dz, il = 1 / sqrt(dd); + let r = bg * 0.6, g = bg * 0.75, b = bg * 1.5, tO = 1e9; + // The spheres, opaque and lit. They are drawn whole even where they + // rise past the cut cone, a ball sitting in a cup. + for (let q = 0; q < 2; q++) { + const s = sph[q][0], rad = sph[q][1]; + if (rad <= 0 || abs(s) > 60) continue; + const ocy = oy - s, bb = ox * dx + ocy * dy + oz * dz, cc = ox * ox + ocy * ocy + oz * oz - rad * rad, disc = bb * bb - dd * cc; + if (disc <= 0) continue; + const t = (-bb - sqrt(disc)) / dd; + if (t <= 0 || t >= tO) continue; + tO = t; + const o = sph[q][2]; + const hx = (ox + t * dx) / rad, hy = (oy + t * dy - s) / rad, hz = (oz + t * dz) / rad; + const di = max(0, hx * lx + hy * ly + hz * lz); + // Blinn highlight on the half vector between the light and the eye. + const vx = lx - dx * il, vy = ly - dy * il, vz = lz - dz * il; + const vl = Math.hypot(vx, vy, vz); + const hs = max(0, (hx * vx + hy * vy + hz * vz) / vl), spec = hs ** 28 * 0.75; + const rim = -(hx * dx + hy * dy + hz * dz) * il; + // Orange and yellow go brown in shade, so a warm sphere gets more + // fill light and stays gold. + const wq = sph[q][3], lit = 0.12 + 0.3 * wq + (0.8 - 0.2 * wq) * di * (0.55 + 0.45 * rim); + r = col[o] * lit + spec; + g = col[o + 1] * lit + spec; + b = col[o + 2] * lit + spec; + } + zO[p] = tO; + // The cone: x^2 + z^2 = k^2 y^2, both nappes. Glass, so each crossing + // adds a little light, more where the surface is seen edge on. + const A = dx * dx + dz * dz - k2 * dy * dy, B = 2 * (ox * dx + oz * dz - k2 * oy * dy), C = ox * ox + oz * oz - k2 * oy * oy; + let tc1 = 1e9, tc2 = 1e9; + const disc = B * B - 4 * A * C; + if (disc > 0 && abs(A) > 1e-9) { + const sq = sqrt(disc); + let ta = (-B - sq) / (2 * A), tb = (-B + sq) / (2 * A); + if (ta > tb) [ta, tb] = [tb, ta]; + for (const t of [ta, tb]) { + const y = oy + t * dy; + if (t > 0 && y < YTOP && y > YBOT) { + if (tc1 > 1e8) tc1 = t; + else tc2 = t; + } + } + } + zC[p] = tc1; + // The plane, clipped to a square in x and z and to the cone's height. + // It is brighter inside the cone, where it is the conic's own disc, and + // lit along its edges. + let tp = 1e9, pin = false, pm = 0; + const nd = nx * dx + ny * dy; + if (abs(nd) > 1e-6) { + const t = (h - nx * ox - ny * oy) / nd; + if (t > 0 && t < tO) { + const x = ox + t * dx, y = oy + t * dy, z = oz + t * dz; + const m = min(PX - abs(x), PX - abs(z), y - YBOT, YTOP - y); + if (m > 0) { + tp = t; + pin = x * x + z * z < k2 * y * y; + pm = (m * f) / t; + } + } + } + zP[p] = tp; + // The glass layers in front of the opaque hit, far to near: the far + // wall of the cone, then the near wall, with the plane wherever it falls. + const pa = pin ? 0.2 : 0.12, pe = (pin ? 0.3 : 0.08) + 0.5 * exp(-pm * pm * 0.8); + const pos = tp > tc2 ? 0 : tp > tc1 ? 1 : 2; + for (let L = 0; L < 3; L++) { + const isP = L === pos, t = isP ? tp : (L < pos ? L : L - 1) === 0 ? tc2 : tc1; + if (t >= tO || t > 1e8) continue; + let a, e, o; + if (isP) { + a = pa; + e = pe; + o = 9; + } else { + const x = ox + t * dx, y = oy + t * dy, z = oz + t * dz, gy = -k2 * y, gl = Math.hypot(x, gy, z) || 1; + const ci = abs((x * dx + gy * dy + z * dz) / gl) * il, fr = (1 - ci) * (1 - ci); + a = 0.06 + 0.2 * fr; + e = 0.07 + 0.42 * fr; + o = 6; + } + r = r * (1 - a) + col[o] * e; + g = g * (1 - a) + col[o + 1] * e; + b = b * (1 - a) + col[o + 2] * e; + } + base[p * 3] = r; + base[p * 3 + 1] = g; + base[p * 3 + 2] = b; + } + } + + // Lines and points, splatted over the shading. Behind a sphere they show + // through dimmed, behind the cone's near wall or the plane a little dimmer. + lin.fill(0); + const ssa = sc.sa, sca = sc.ca; + let cr = 0, cg = 0, cb = 0, cw = 0; + // A thick line is laid as two strands half an LED either side of its + // path on the screen, (tox, toy) apart. + let thick = false, tox = 0, toy = 0; + const put = (sx, sy, k) => { + const ix = floor(sx), iy = floor(sy), ax = sx - ix, ay = sy - iy; + for (let j = 0; j < 4; j++) { + const X = ix + (j & 1), Y = iy + (j >> 1); + if (X < 0 || X >= W || Y < 0 || Y >= H) continue; + const wt = k * (j & 1 ? ax : 1 - ax) * (j >> 1 ? ay : 1 - ay), o = (Y * W + X) * 3; + lin[o] += cr * wt; + lin[o + 1] += cg * wt; + lin[o + 2] += cb * wt; + } + }; + const splat = (x, y, z, w) => { + const qx = x - ox, qy = y - oy, qz = z - oz, zz = qx * fx + qy * fy + qz * fz; + if (zz < 0.1) return; + const sx = 31.5 + (f * (qx * rx + qz * rz)) / zz, sy = 63.5 - (f * (qx * ux + qy * uy + qz * uz)) / zz; + if (sx < -2 || sx > W + 1 || sy < -2 || sy > H + 1) return; + const ni = cl(Math.round(sy), 0, H - 1) * W + cl(Math.round(sx), 0, W - 1); + let k = w; + if (zz > zO[ni] + 0.04) k *= 0.38; + if (zz > zC[ni] + 0.04) k *= 0.62; + if (zz > zP[ni] + 0.03) k *= 0.75; + if (!thick) put(sx, sy, k); + else { + put(sx + tox, sy + toy, k * 0.62); + put(sx - tox, sy - toy, k * 0.62); + } + }; + const proj = (x, y, z) => { + const qx = x - ox, qy = y - oy, qz = z - oz, zz = max(0.1, qx * fx + qy * fy + qz * fz); + return [(f * (qx * rx + qz * rz)) / zz, (f * (qx * ux + qy * uy + qz * uz)) / zz]; + }; + const seg = (ax, ay, az, bx, by, bz) => { + const [sx0, sy0] = proj(ax, ay, az), [sx1, sy1] = proj(bx, by, bz); + const L = Math.hypot(sx1 - sx0, sy1 - sy0), n = max(1, Math.ceil(L / 0.6)), w = (L / n) * cw; + tox = L > 0 ? (-(sy1 - sy0) / L) * 0.55 : 0; + toy = L > 0 ? ((sx1 - sx0) / L) * 0.55 : 0; + for (let i = 0; i <= n; i++) { + const t = i / n; + splat(ax + (bx - ax) * t, ay + (by - ay) * t, az + (bz - az) * t, i === 0 || i === n ? w / 2 : w); + } + }; + const use = (o, w) => { + cr = col[o * 3]; + cg = col[o * 3 + 1]; + cb = col[o * 3 + 2]; + cw = w; + }; + // A point on the line of the cone at azimuth phi, t along it. + const gp = (phi, t) => [t * ssa * cos(phi), t * sca, t * ssa * sin(phi)]; + // The cone's lines through the apex and its two rims. + use(2, 0.3); + const tTop = YTOP / sca, tBot = YBOT / sca; + for (let i = 0; i < GEN; i++) { + const phi = (i * 2 * PI) / GEN + 0.3; + seg(...gp(phi, tTop), 0, 0, 0); + seg(0, 0, 0, ...gp(phi, tBot)); + } + const ring = (y, rad, n) => { + let px = rad, pz = 0; + for (let i = 1; i <= n; i++) { + const a = (i * 2 * PI) / n, qx = rad * cos(a), qz = rad * sin(a); + seg(px, y, pz, qx, y, qz); + px = qx; + pz = qz; + } + }; + use(2, 0.5); + ring(YTOP, YTOP * sc.k, 72); + ring(YBOT, -YBOT * sc.k, 72); + // The contact circles, in each sphere's colour, brighter. + const okS = (s, r) => r > 0 && abs(s) < 60; + use(0, 0.95); + if (okS(sc.s1, sc.r1)) ring(sc.g1 * sca, abs(sc.g1 * ssa), 64); + use(1, 0.95); + if (okS(sc.s2, sc.r2) && sc.g2 * sca < YTOP && sc.g2 * sca > YBOT) ring(sc.g2 * sca, abs(sc.g2 * ssa), 64); + // The conic, drawn in pieces wherever it lies inside the cut cone. + use(4, 1.35); + const NPH = 240; + let prev = null; + for (let i = 0; i <= NPH; i++) { + const phi = (i * 2 * PI) / NPH, t = conicT(sc, phi), y = t * sca; + const okp = y < YTOP && y > YBOT; + const cur = okp ? gp(phi, t) : null; + if (cur && prev && abs(cur[1] - prev[1]) < 1.2) seg(...prev, ...cur); + prev = cur; + } + // The point, its two distances to the foci and the line of the cone + // through it, cut where it meets the two contact circles. Each pair in one + // colour: PF1 equals the stretch to circle 1, PF2 the stretch to circle 2. + const tP = conicT(sc, S.ph), yP = tP * sca; + if (yP < YTOP && yP > YBOT) { + const [Px, Py, Pz] = gp(S.ph, tP); + const clipT = (t) => cl(t, tBot, tTop); + thick = true; + use(5, 1.5); + if (okS(sc.s1, sc.r1)) { + seg(Px, Py, Pz, sc.fx1, sc.fy1, 0); + seg(Px, Py, Pz, ...gp(S.ph, clipT(sc.g1))); + } + use(6, 1.5); + if (okS(sc.s2, sc.r2)) { + seg(Px, Py, Pz, sc.fx2, sc.fy2, 0); + seg(Px, Py, Pz, ...gp(S.ph, clipT(sc.g2))); + } else seg(Px, Py, Pz, ...gp(S.ph, sc.g2 > 0 ? tTop : tBot)); + thick = false; + use(7, 2.2); + for (const [ddx, ddy] of [[0, 0], [0.012, 0], [-0.012, 0], [0, 0.012], [0, -0.012]]) splat(Px + ddx, Py + ddy, Pz, 1); + } + // The foci, white points where the spheres touch the plane. + use(7, 1.6); + for (const [s, r, fxx, fyy] of [[sc.s1, sc.r1, sc.fx1, sc.fy1], [sc.s2, sc.r2, sc.fx2, sc.fy2]]) { + if (!okS(s, r) || fyy > YTOP || fyy < YBOT) continue; + for (const [ddx, ddy] of [[0, 0], [0.014, 0], [-0.014, 0], [0, 0.014], [0, -0.014]]) splat(fxx + ddx, fyy + ddy, 0, 1); + } + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + const o = p * 3; + display.setPixel(x, y, cl((base[o] + lin[o]) * 255, 0, 255), cl((base[o + 1] + lin[o + 1]) * 255, 0, 255), cl((base[o + 2] + lin[o + 2]) * 255, 0, 255)); + } +} From fa32bdd6e4d990b56ade2593804e3b93e73b2931 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 15:31:34 +0000 Subject: [PATCH 06/10] pattern-study: Penrose Room, Essence Drop Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117bhD83MGCSerAR798cUib --- .../pattern-study/patterns/essence-drop.js | 386 ++++++++++++++++++ .../pattern-study/patterns/penrose-room.js | 295 +++++++++++++ 2 files changed, 681 insertions(+) create mode 100644 incoming/pattern-study/patterns/essence-drop.js create mode 100644 incoming/pattern-study/patterns/penrose-room.js diff --git a/incoming/pattern-study/patterns/essence-drop.js b/incoming/pattern-study/patterns/essence-drop.js new file mode 100644 index 000000000..200943dde --- /dev/null +++ b/incoming/pattern-study/patterns/essence-drop.js @@ -0,0 +1,386 @@ +// ===== Patternflow pattern ===== +// Title: Essence Drop +// Family: cosmetic +// Idea: In slow motion a bead of essence falls into a clear tinted pool, throws up a crown that sheds droplets, opens a cavity that snaps shut into a jet whose tip pinches off and falls back, while ripples carry glints along the glossy surface and the drop's deeper colour curls down through the pool as a vortex ring and slowly melts into it. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 33, after slow-motion film of a drop falling into clear essence +// Round: 33 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Viscosity=0..1, Size=0..1 +// +// Knob 1: Hue. The essence's colour, all the way round the wheel from aqua at 0; round orange and gold the pool and the bloom are lifted and paled so they stay champagne, never brown. +// Knob 2: Speed. How slow the film runs: at 2 a drop takes 1.4 s to fall, lands every 5.5 s, and its crown, cavity and jet are over in 2.4 s while the bloom sinks for about 3 s more; at 0.2 all of it is ten times slower. +// Knob 3: Viscosity. Watery at 0: a crown of nine fingers throws its beads, and a jet eight and a half drop radii high pinches off a bead. Turned up, the fingers go by 0.5 and the crown by 0.7, the jet shortens to a round mound with no bead, the drop sinks in on a thick neck, the thread behind it stretches long, everything runs up to 2.3 times slower, drops come every 7.7 s, the pool holds more colour and the bloom sinks as a slow compact plume. +// Knob 4: Size. The drop's radius, 3.4 LEDs at 0, 3.7 at the start and 7.6 at 1, and the crown, the cavity, the jet and the bloom with it; the jet stops at 48 LEDs. +// Buttons: K1 tap lets a drop fall now. + +const W = 64, H = 128, N = W * H, Y0 = 66; +const { min, max, floor, sqrt, sin, cos, exp, abs, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (v) => ((v = cl(v, 0, 1)), v * v * (3 - 2 * v)); +const smin = (a, b, k) => { + const h = max(k - abs(a - b), 0) / k; + return min(a, b) - h * h * k * 0.25; +}; +// Light comes from the upper left. +const LX = -0.55, LY = -0.835; +const MAXP = 2000; + +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 = { + hue: 0, speed: 2, visc: 0, size: 0.06, next: 0.4, T: 0, + ev: [], dye: new Float32Array(N), deep: new Float32Array(N), dt2: new Float32Array(N), sdf: new Float32Array(N), rip: new Float32Array(W), + px: new Float32Array(MAXP), py: new Float32Array(MAXP), pa: new Float32Array(MAXP), pe: new Int32Array(MAXP), np: 0, + circ: [], caps: [], pal: new Float32Array(13), palKey: NaN, + }; +} + +function spawn(S) { + const r = 3.4 + 4.2 * S.size, R = Math.random; + const x = 32 + (R() - 0.5) * 10; + // Nine fingers round the front of the crown, each its own length, and the + // beads they throw each their own speed, so the spray fans out. + const fing = []; + for (let j = 0; j < 9; j++) fing.push([PI * (j + 0.5 + 0.35 * (R() - 0.5)) / 9, 0.6 + 0.65 * R(), 7 + 6 * R(), 4 + 5 * R()]); + S.ev.push({ id: (S.idn = (S.idn || 0) + 1), x, r, te: 0, Tf: 1.4 * (1 + 0.5 * S.visc), dyed: false, ga: 0, a: 0, yv: 0, fing }); + if (S.ev.length > 4) S.ev.shift(); +} + +// The phases of one splash, as functions of tau, the seconds since impact on +// the film's own clock: the cavity opens and shuts by 0.9, the crown stands +// by 0.35 and falls by 1.1, the jet rises from 0.85 and is gone by 2.35. +const TC = 0.9, TCR = 1.1, TJ0 = 0.85, TJ = 1.5, TPINCH = 0.42; +function slowOf(v) { + return 1 + 1.3 * v; +} + +// The dye: when the drop goes under, a blob of particles is set in the +// velocity field of a vortex pair, the cross-section of a vortex ring. The +// pair moves down at U = G / (4 pi a), slowing as G decays and widening as it +// slows, and the particles wind into its two curls and trail a stem. +function stepDye(S, dts) { + const v = S.visc; + for (const e of S.ev) { + const tau = (e.te - e.Tf) / slowOf(v); + if (tau < 0.08) continue; + if (!e.dyed) { + e.dyed = true; + e.a = 1.7 * e.r; + e.yv = Y0 + 2 * e.r; + e.ga = 4 * PI * e.a * (13 + 1.4 * e.r) * (1 - 0.55 * v); + const n = floor(160 + 40 * e.r); + for (let i = 0; i < n; i++) { + if (S.np >= MAXP) break; + const ang = Math.random() * 2 * PI, rad = sqrt(Math.random()) * 1.9 * e.r; + S.px[S.np] = e.x + cos(ang) * rad; + S.py[S.np] = Y0 + 1.6 * e.r + sin(ang) * rad * 0.8; + S.pa[S.np] = 0; + S.pe[S.np] = e.id; + S.np++; + } + } + // Viscosity eats the ring: its spin decays over 2.8 s in water, 1.5 s in gel. + e.ga *= exp(-dts / (2.8 - 1.3 * v)); + const U = e.ga / (4 * PI * e.a); + e.yv += U * dts; + e.a += 0.3 * e.r * dts * (1 - 0.6 * v); + } + const dye = S.dye; + let k = 0; + for (let i = 0; i < S.np; i++) { + let e = null; + for (const f of S.ev) if (f.id === S.pe[i]) e = f; + let x = S.px[i], y = S.py[i]; + const age = (S.pa[i] += dts); + if (!e || age > 12) continue; + // Two Lamb-Oseen cores, opposite spins, the fluid between them going down. + let ux = 0, uy = 0; + const d2 = (0.85 * e.a) ** 2; + for (let s = -1; s <= 1; s += 2) { + const dx = x - (e.x + s * e.a), dy = y - e.yv, g = (-s * e.ga) / (2 * PI * (dx * dx + dy * dy + d2)); + ux += -g * dy; + uy += g * dx; + } + x += (ux + (Math.random() - 0.5) * 7) * dts; + y += (uy + (Math.random() - 0.5) * 7) * dts; + x = cl(x, 0.5, W - 1.5); + y = cl(y, Y0 + 1, H - 1.5); + // Each particle leaves colour where it passes, less as it ages, so the + // curls show where it went and the old colour spreads out. + const w = dts * 1.4 * exp(-age / 5), ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy, o = iy * W + ix; + dye[o] += w * (1 - fx) * (1 - fy); + dye[o + 1] += w * fx * (1 - fy); + dye[o + W] += w * (1 - fx) * fy; + dye[o + W + 1] += w * fx * fy; + S.px[k] = x; + S.py[k] = y; + S.pa[k] = age; + S.pe[k] = S.pe[i]; + k++; + } + S.np = k; + // Diffusion and a slow fade into the pool: a gel holds the colour longer. + const t2 = S.dt2, kap = cl(dts * (1.6 - 1.1 * v), 0, 0.24), fade = exp(-dts / (7 + 5 * v)); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x, c = dye[i]; + const l = (x > 0 ? dye[i - 1] : c) + (x < W - 1 ? dye[i + 1] : c) + (y > 0 ? dye[i - W] : c) + (y < H - 1 ? dye[i + W] : c) - 4 * c; + t2[i] = (c + kap * l) * fade; + } + dye.set(t2); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + const d = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + S.speed = cl(k[1], 0.2, 3); + S.visc = cl(k[2] || 0, 0, 1); + S.size = cl(k[3], 0, 1); + const dts = (d * S.speed) / 2; + S.T += dts; + if (input.btnPressed && input.btnPressed[0]) spawn(S); + S.next -= dts; + if (S.next <= 0) { + spawn(S); + S.next += 5.5 * (1 + 0.4 * S.visc); + } + for (const e of S.ev) e.te += dts; + S.ev = S.ev.filter((e) => e.te - e.Tf < 16 * slowOf(S.visc)); + stepDye(S, dts); +} + +// Shapes for the frame: circles and tapered capsules, each smooth-joined to +// the pool so a bead touching the surface melts into it. +function build(S) { + const C = (S.circ = []), K = (S.caps = []), v = S.visc, rip = S.rip; + rip.fill(0); + S.cav = []; + S.crowns = []; + for (const e of S.ev) { + const r = e.r, x0 = e.x; + if (e.te < e.Tf) { + // Falling, faster and faster. The drop hangs from a thread out of the + // top; the thread stretches, breaks (later the thicker the essence) and + // its lower end balls up into a small bead that trails the drop. + const u = e.te / e.Tf, yb = -2 * r - 4 + (Y0 + 2 * r + 4) * (0.3 * u + 0.7 * u * u), cy = yb - r; + C.push([x0, cy, r, 1.4, 1]); + const tb = 0.45 + 0.42 * v; + if (u < tb) K.push([x0, cy - 0.5 * r, x0, -6, 0.42 * r, 0.12 * r * (1 + v), 1.6, 1]); + else { + const f = (u - tb) / (1 - tb), sy = cy - r - 2 - (6 + 14 * v) * sqrt(f); + C.push([x0, sy, 0.32 * r, 1, 1]); + K.push([x0, -6, x0, -6 + (cy - 12 + 6) * max(0, 1 - 3 * f), 0.15 * r, 0.12 * r, 1, 1]); + } + continue; + } + const tau = (e.te - e.Tf) / slowOf(v); + // The drop sinks into the surface instead of vanishing at the touch: in a + // gel it takes 0.75 s and a thick neck joins it to the pool. + const ts = 0.15 + 0.6 * v; + if (tau < ts) C.push([x0, Y0 - r + 2.2 * r * (tau / ts), r * (1 - 0.3 * (tau / ts)), 1.4 + 3 * v, 1]); + // Ripples: a packet from the impact and a smaller one when the jet falls, + // running out along the surface at 9 LEDs a second, 4 in a gel, about + // nine LEDs from crest to crest. + for (const [t0, a0] of [[0, 0.9 * (1 - 0.6 * v)], [TJ0 + TJ * 0.8, 0.45 * (1 - 0.7 * v)]]) { + const tt = tau - t0; + if (tt <= 0) continue; + const c = 9 - 5 * v, A = (a0 * r * exp(-tt / 1.8)) / 3.6; + for (let x = 0; x < W; x++) { + const s = abs(x + 0.5 - x0) - c * tt - 2, en = exp(-(s * s) / 60); + if (en > 0.01) rip[x] += A * en * sin(s * 0.7); + } + } + // The cavity: an air pocket pushed down under the surface. + if (tau < TC) { + const Rc = r * (1.2 + 1.3 * (1 - exp(-tau / 0.18))) * (1 - 0.3 * v), Dc = r * 2.5 * (1 - 0.55 * v) * sin((PI * tau) / TC); + if (Dc > 0.3) S.cav.push([x0, Rc, Dc]); + } + // The crown: two thin walls leaning out, a translucent sheet between + // them, and fingers round the rim whose beads fly off at 0.42. + const hc = r * 3.1 * max(0, 1 - v / 0.7), R = r * (1.4 + 1.5 * (1 - exp(-min(tau, TCR) / 0.25))); + if (hc > 0.2 && tau < TCR) { + const Hc = hc * Math.pow(sin((PI * tau) / TCR), 0.6), fl = 0.35 * Hc; + if (Hc > 0.5) { + for (let s = -1; s <= 1; s += 2) K.push([x0 + s * R, Y0 + 1, x0 + s * (R + fl), Y0 - Hc, 0.75, 0.5, 1.2]); + S.crowns.push([x0, R, fl, Hc]); + if (tau < 0.42 && v < 0.5) { + const g = sm(tau / 0.3) * (1 - v / 0.5); + for (const [ph, lk] of e.fing) { + const cs = cos(ph), bx = x0 + (R + fl) * cs, lf = lk * 0.55 * hc * g, tx = bx + 0.35 * lf * cs, ty = Y0 - Hc - lf; + K.push([bx, Y0 - Hc + 0.5, tx, ty, 0.5, 0.3, 0.9]); + C.push([tx, ty, 0.26 * r, 0.9]); + } + } + } + } + if (tau >= 0.42 && v < 0.5) { + // The beads thrown from the fingers fly out and up, slow, and drop back. + const t = tau - 0.42, Hp = hc * Math.pow(sin((PI * 0.42) / TCR), 0.6), Rp = r * (1.4 + 1.5 * (1 - exp(-0.42 / 0.25))) + 0.35 * Hp; + for (const [ph, lk, vy, vx] of e.fing) { + const cs = cos(ph), lf = lk * 0.55 * hc * (1 - v / 0.5); + const y = Y0 - Hp - lf - vy * t + 16 * t * t, x = x0 + (Rp + 0.35 * lf) * cs + vx * cs * t; + if (y < Y0 + 2) C.push([x, y, 0.26 * r * (1 - v / 0.5), 0.9]); + } + } + // The jet: when the cavity snaps shut a column climbs out of the middle; + // in water its tip pinches off into a bead that flies on and falls back. + if (tau > TJ0) { + const s = (tau - TJ0) / TJ, hj = min(48, r * 8.5) * (1 - 0.8 * v), rb = 0.85 * r * (1 + 0.8 * v), pinch = v < 0.55; + if (s < 1) { + const Hj = hj * sin(PI * s); + const top = Y0 - Hj; + K.push([x0, Y0 + 3, x0, top, rb, (0.5 + 0.5 * v) * r, 2.5 + 2.5 * v]); + if (!pinch || s < TPINCH) C.push([x0, top - 0.3 * r, (0.7 + 0.3 * v) * r, 1.3 + 2 * v]); + } + if (pinch && s >= TPINCH) { + const t = (s - TPINCH) * TJ, Hp = hj * sin(PI * TPINCH), y = Y0 - Hp - 0.9 * r - 7 * t + 18 * t * t; + if (y < Y0 + 2) C.push([x0, y, 0.68 * r, 1.3]); + } + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + build(S); + const sdf = S.sdf, rip = S.rip, v = S.visc, deep = S.deep; + // The liquid's signed distance: negative inside. The pool's surface, + // minus the cavities, then every shape joined on smoothly. The falling drop + // is the concentrated essence, so where it is the colour is deep. + for (let y = 0, i = 0; y < H; y++) for (let x = 0; x < W; x++, i++) sdf[i] = Y0 + rip[x] - (y + 0.5); + deep.fill(0); + for (const [cx, R, D] of S.cav) + for (let y = Y0 - 2; y < min(H, Y0 + D + 3); y++) + for (let x = max(0, floor(cx - R - 3)); x < min(W, cx + R + 3); x++) { + const dx = (x + 0.5 - cx) / R, dy = (y + 0.5 - Y0) / D, da = (sqrt(dx * dx + dy * dy) - 1) * min(R, D), i = y * W + x; + sdf[i] = -smin(-sdf[i], da, 1.5); + } + for (const [ax, ay, bx, by, ra, rb, k, dp] of S.caps) { + const x0 = max(0, floor(min(ax, bx) - ra - k - 1)), x1 = min(W - 1, floor(max(ax, bx) + ra + k + 1)); + const y0 = max(0, floor(min(ay, by) - ra - k - 1)), y1 = min(H - 1, floor(max(ay, by) + ra + k + 1)); + const ex = bx - ax, ey = by - ay, ll = ex * ex + ey * ey || 1; + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const qx = x + 0.5 - ax, qy = y + 0.5 - ay, t = cl((qx * ex + qy * ey) / ll, 0, 1); + const dd = Math.hypot(qx - ex * t, qy - ey * t) - (ra + (rb - ra) * t), i = y * W + x; + sdf[i] = smin(sdf[i], dd, k); + if (dp) deep[i] = max(deep[i], cl(1 - dd, 0, 1)); + } + } + for (const [cx, cy, r, k, dp] of S.circ) { + const x0 = max(0, floor(cx - r - k - 1)), x1 = min(W - 1, floor(cx + r + k + 1)), y0 = max(0, floor(cy - r - k - 1)), y1 = min(H - 1, floor(cy + r + k + 1)); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const i = y * W + x, dd = Math.hypot(x + 0.5 - cx, y + 0.5 - cy) - r; + sdf[i] = smin(sdf[i], dd, k); + if (dp) deep[i] = max(deep[i], cl(1 - dd, 0, 1)); + } + } + // Palette: backdrop, essence, and the deep colour the drop carries. Round + // orange and gold everything is lifted and paled so it reads champagne, + // never brown, and there the deep colour is brighter than the pool instead + // of darker. A thicker essence holds more colour. + const pal = S.pal, h = S.hue + 0.52, hf = h - floor(h), key = hf + 7 * v; + if (S.palKey !== key) { + const wm = sm((hf - 0.02) / 0.04) * (1 - sm((hf - 0.19) / 0.06)); + hsv(h + 0.02, 0.14 - 0.06 * wm, 1, pal, 0); + hsv(h + 0.06, 0.4 - 0.15 * wm, 1, pal, 10); + hsv(h, (0.38 + 0.22 * v) * (1 - 0.4 * wm), 1, pal, 3); + hsv(h + 0.015, 0.92 - 0.35 * wm, 1, pal, 6); + pal[9] = wm; + S.palKey = key; + } + const wm = pal[9], lift = 1 + 0.18 * wm, dv = 0.46 + 0.5 * wm, dye = S.dye, T = S.T; + for (let y = 0, i = 0; y < H; y++) { + const below = y + 0.5 - Y0; + for (let x = 0; x < W; x++, i++) { + const d = sdf[i]; + // The backdrop: a soft key light high in the middle, deepening towards + // the surface. + const gx = (x + 0.5 - 32) / 32, gy = (y - 14) / 46; + const gl = exp(-(gx * gx + gy * gy)), bv = min(1, (0.4 + 0.46 * gl) * lift); + let r = (pal[10] + (pal[0] - pal[10]) * gl) * bv, g = (pal[11] + (pal[1] - pal[11]) * gl) * bv, b = (pal[12] + (pal[2] - pal[12]) * gl) * bv; + // The crown's sheet: thin film between its walls, brighter at the two + // sides where the eye looks along it, and along the lip. + for (const [cx, R, fl, Hc] of S.crowns) { + const hgt = -below; + if (hgt < -0.5 || hgt > Hc) continue; + const Rh = R + (fl * hgt) / Hc, u = abs(x + 0.5 - cx) / Rh; + if (u >= 1) continue; + const a = 0.16 + 0.3 * min(1, 1 / sqrt(1 - u * u) - 1) + 0.4 * exp(-((hgt - Hc + 0.6) ** 2)); + r += (pal[3] * 0.92 - r) * a; + g += (pal[4] * 0.92 - g) * a; + b += (pal[5] * 0.92 - b) * a; + } + // The open surface, seen edge on: a gloss line along it, glinting white + // where a ripple tilts it towards the light. It is drawn across the + // boundary, so it does not flicker as the surface moves by a fraction. + const pd = Y0 + rip[x] - (y + 0.5), open = abs(d - pd) < 0.02; + let spec = 0; + if (open && abs(pd) < 2.5) { + const sl = ((x < W - 1 ? rip[x + 1] : rip[x]) - (x > 0 ? rip[x - 1] : rip[x])) / 2, nd = (sl * LX - LY) / sqrt(1 + sl * sl); + spec = exp(-((pd + 0.3) ** 2) / 0.45) * (0.16 + 0.85 * Math.pow(max(0, nd), 12)); + } + if (d < 0.5) { + const cov = cl(0.5 - d, 0, 1); + // Inside the liquid: the essence, brighter just under the surface and + // deeper towards the bottom, slow shafts of light from the surface + // swaying through it, and the deep colour where the drop has gone. + const depth = cl(below / (H - Y0), 0, 1); + let lv = below < 0 ? 0.88 : 0.8 - 0.36 * depth; + if (below >= 0) { + const sx = x + 0.4 * below; + lv += 0.16 * (1 - depth) * (0.5 + 0.5 * sin(0.3 * sx - 0.7 * T + 1.3 * sin(0.11 * sx + 0.23 * T))); + } + lv = min(1, lv * lift); + let lr = pal[3] * lv, lg = pal[4] * lv, lb = pal[5] * lv; + const da = max(deep[i] * 0.9, 1 - exp(-dye[i] * 0.9)); + if (da > 0.003) { + lr += (pal[6] * dv - lr) * da; + lg += (pal[7] * dv - lg) * da; + lb += (pal[8] * dv - lb) * da; + } + let rim = 0, inner = 0; + if (!open) { + // Everything but the open surface: the normal from the distance + // field, a white highlight where it faces the light, a gloss on what + // faces up, a darker rim on the far side and light gathered inside + // the bottom of a bead. + const xm = x > 0 ? sdf[i - 1] : d, xp = x < W - 1 ? sdf[i + 1] : d, ym = y > 0 ? sdf[i - W] : d, yp = y < H - 1 ? sdf[i + W] : d; + let nx = xp - xm, ny = yp - ym; + const nl = Math.hypot(nx, ny) || 1; + nx /= nl; + ny /= nl; + const edge = cl(1 + d / 1.6, 0, 1), nd = nx * LX + ny * LY; + spec += edge * (0.9 * Math.pow(max(0, nd), 7) + 0.22 * max(0, -ny)); + rim = edge * 0.35 * (1 - max(0, nd)); + inner = cl((-d - 0.6) / 1.2, 0, 1) * cl((3 + d) / 1.5, 0, 1) * Math.pow(max(0, -nd), 2) * (below < 1 ? 0.6 : 0); + } + lr = lr * (1 - rim) + inner * pal[3]; + lg = lg * (1 - rim) + inner * pal[4]; + lb = lb * (1 - rim) + inner * pal[5]; + r += (lr - r) * cov; + g += (lg - g) * cov; + b += (lb - b) * cov; + } + r += spec; + g += spec; + b += spec; + display.setPixel(x, y, 255 * min(1, r), 255 * min(1, g), 255 * min(1, b)); + } + } +} diff --git a/incoming/pattern-study/patterns/penrose-room.js b/incoming/pattern-study/patterns/penrose-room.js new file mode 100644 index 000000000..265005d0c --- /dev/null +++ b/incoming/pattern-study/patterns/penrose-room.js @@ -0,0 +1,295 @@ +// ===== Patternflow pattern ===== +// Title: Penrose Room +// Family: physics +// Idea: A point of light wanders in Penrose's mirrored room, two half-ellipses joined by a corridor whose thin walls run from focus to focus, and its traced rays fill the corridor and the ellipses while the two side chambers stay dark, until the light floats up through an ellipse into a chamber and the lit and dark parts of the room swap. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Penrose unilluminable room (Roger Penrose, 1958), pattern round 33 +// Round: 33 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Offset=-4..4, Bounces=0..1 +// +// Knob 1: Hue. The colour of the light. At 0 the light straight from the lamp is a pale aqua and light that has met more mirrors turns deeper, to azure after five; the walls are a dim violet a quarter turn on. Round orange and yellow the light goes paler, so far light reads as butter, not olive. +// Knob 2: Speed. How fast the light wanders and how soon it moves: at 2 it stays 3.2 s in the corridor at the start, takes 1.8 s to float through an ellipse into a chamber, and after that stays 5 to 8 s in the corridor and 7 to 10 s in a chamber; at 0.2 all of it is ten times slower. +// Knob 3: Offset. How far the two corridor walls stand off the foci, in LEDs. At 0 they run exactly from focus to focus and not one ray crosses into the dark part; moved 4 LEDs in or out, the gap between wall and focus lets rays through and the dark part fills with a dimmer glow. +// Knob 4: Bounces. How many times each ray is reflected: 14 at the start; none at 0, where only what the lamp sees straight on is lit and from a chamber the far side of the room stays dark; 40 at 1, where more of the light has met many mirrors and the room turns a deeper blue. +// Buttons: K1 tap sends the light across now, from the corridor into a side chamber or back. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, sqrt, sin, cos, exp, abs, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The room, in LEDs. Two half-ellipses with semi-axes 31.5 and 26 put their +// flat sides at rows 26.5 and 101.5, and their arcs touch rows 0 and 127, so +// the room is the whole panel. The foci sit on the flat sides at 17.78 either +// side of the middle column; the outer walls are the ellipses' own tangents +// at the ends of the major axis, columns 0 and 63. +const A = 31.5, B = 26, C = sqrt(A * A - B * B), CX = 32; +const YT = 0.5 + B, YB = 127.5 - B, YM = 64; +const IA2 = 1 / (A * A), IB2 = 1 / (B * B), EPS = 1e-6; +// Each reflection keeps 60% of the light. The glow is LEVEL times the light +// over the mean of the lit LEDs, raised to GAMMA: the mean shows at 92 of 255 +// and the glow round the lamp comes close to full. +const REFL = 0.6, LEVEL = 0.36, GAMMA = 0.7; + +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 = { + hue: 0, speed: 2, off: 0, nb: 14, w: C, + buf: new Float32Array(N), tmp: new Float32Array(N), hz: new Float32Array(N), kb: new Float32Array(N), kz: new Float32Array(N), wall: new Float32Array(N), wallW: NaN, + lu: 0, ly: YM, region: 0, tau: 0, stay: 3.2, mode: 0, + tt: 0, from: 0, to: 0, expo: 0, bz: new Float64Array(8), col: new Float32Array(51), + }; +} + +// Where the light may stand: the corridor between the walls, or one side +// chamber, a little clear of every wall. +function clampTo(S, region, u) { + const w = S.w; + if (region === 0) return cl(u, -(w - 1.2), w - 1.2); + return region * cl(region * u, w + 1.2, A - 1.2); +} + +// A slow wander that starts from the middle of the place, so the light +// arrives at the middle and sets off from there without a jump. +function wander(S) { + const w = S.w, t = S.tau; + const y = YM + 30 * sin(0.4 * t); + if (S.region === 0) return [(w - 3.5) * sin(0.55 * t), y]; + const uc = (S.region * (w + A)) / 2, au = max(0, (A - w) / 2 - 2.5); + return [uc + au * sin(0.7 * t), y]; +} + +// Crossing over: a cubic curve that leaves straight up (or down) through the +// flat side nearer the light, arcs through the half-ellipse and drops +// straight into the middle of the other place. From the corridor it leaves +// between the foci and from a chamber outside them, as a ray would. +function startTransit(S) { + const u0 = S.lu, y0 = S.ly, w = S.w; + S.from = S.region; + S.to = S.region === 0 ? (u0 > 0.5 ? 1 : u0 < -0.5 ? -1 : Math.random() < 0.5 ? -1 : 1) : 0; + const u3 = S.to === 0 ? 0 : (S.to * (w + A)) / 2, cy = y0 <= YM ? YT - 32 : YB + 32; + S.bz.set([u0, y0, u0, cy, u3, cy, u3, YM]); + S.mode = 1; + S.tt = 0; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + const d = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + S.speed = cl(k[1], 0.2, 3); + S.off = cl(k[2] || 0, -4, 4); + S.w = C + S.off; + S.nb = Math.round(40 * Math.pow(cl(k[3], 0, 1), 0.373)); + const sp = S.speed / 2; + + if (input.btnPressed && input.btnPressed[0] && S.mode === 0) startTransit(S); + if (S.mode === 0) { + S.tau += d * sp; + S.stay -= d * sp; + const p = wander(S); + S.lu = clampTo(S, S.region, p[0]); + S.ly = p[1]; + if (S.stay <= 0) startTransit(S); + } else { + S.tt = min(1, S.tt + (d * sp) / 1.8); + const e = S.tt * S.tt * (3 - 2 * S.tt), f = 1 - e, b = S.bz; + const c0 = f * f * f, c1 = 3 * f * f * e, c2 = 3 * f * e * e, c3 = e * e * e; + let u = c0 * b[0] + c1 * b[2] + c2 * b[4] + c3 * b[6]; + const y = c0 * b[1] + c1 * b[3] + c2 * b[5] + c3 * b[7]; + // Between the flat sides the light is in a place, never in a wall. + // In a half-ellipse it keeps clear of the arc: with the walls 4 LEDs out, + // the curve's top would just touch it. + let yy = y; + if (y > YT && y < YB) u = clampTo(S, S.tt < 0.5 ? S.from : S.to, u); + else { + const yc = y <= YT ? YT : YB, v = y - yc, E = u * u * IA2 + v * v * IB2; + if (E > 0.85) { + const q = sqrt(0.85 / E); + u *= q; + yy = yc + v * q; + } + } + S.lu = u; + S.ly = yy; + if (S.tt >= 1) { + S.mode = 0; + S.region = S.to; + S.tau = 0; + S.stay = S.region === 0 ? 5 + 3 * Math.random() : 7 + 3 * Math.random(); + } + } + + // The light fades over about a sixth of a second, so a place goes dark + // soon after the light has left it. + const buf = S.buf, kb = S.kb, fade = exp(-d / 0.16); + for (let i = 0; i < N; i++) { + buf[i] *= fade; + kb[i] *= fade; + } + trace(S, d * 30); + // Exposure follows the mean of the lit LEDs over about half a second, the + // way an eye adjusts, so a chamber, where every ray soon hits a wall and + // loses 40% of its light, glows about as bright as the corridor. + let sum = 0, n = 0; + for (let i = 0; i < N; i++) + if (buf[i] > 0.5) { + sum += buf[i]; + n++; + } + const m = n ? sum / n : 1; + S.expo = S.expo > 0 ? S.expo + (m - S.expo) * (1 - exp(-d / 0.5)) : m; +} + +// Rays leave the light evenly spaced round the circle, the whole fan turned +// by a random angle each frame, and bounce off the mirrors; each reflection +// keeps 60% of the light, so the first few reflections draw the shapes and +// the later ones fill in. A ray's path is added to the buffer one LED step at +// a time, so a pixel gathers light in proportion to the rays crossing it. +function trace(S, gain) { + const nb = S.nb, R = cl(floor(4200 / (nb + 1)), 120, 300), w = S.w, buf = S.buf, kb = S.kb; + const phase = Math.random(), w0 = (gain * 0.5 * 300) / R; + for (let r = 0; r < R; r++) { + const th = (2 * PI * (r + phase)) / R; + let u = S.lu, y = S.ly, dx = cos(th), dy = sin(th), wt = w0; + for (let k = 0; k <= nb; k++) { + let best = 1e9, kind = 0, nx = 0, ny = 0; + // The two arcs: an ellipse counts as a mirror only on its own side of + // its flat side, which is open to the room between them. + for (let e = 0; e < 2; e++) { + const yc = e ? YB : YT, v = y - yc; + const qa = dx * dx * IA2 + dy * dy * IB2, qb = 2 * (u * dx * IA2 + v * dy * IB2), qc = u * u * IA2 + v * v * IB2 - 1; + const disc = qb * qb - 4 * qa * qc; + if (disc <= 0) continue; + const sq = sqrt(disc); + for (let j = 0; j < 2; j++) { + const t = (-qb + (j ? sq : -sq)) / (2 * qa); + if (t <= EPS || t >= best) continue; + const hy = y + dy * t; + if (e ? hy >= YB : hy <= YT) { + best = t; + kind = 1; + nx = (u + dx * t) * IA2; + ny = (hy - yc) * IB2; + break; + } + } + } + // The outer walls and the two thin corridor walls, mirrors on both sides. + if (dx !== 0) + for (let j = 0; j < 4; j++) { + const X = j === 0 ? A : j === 1 ? -A : j === 2 ? w : -w, t = (X - u) / dx; + if (t <= EPS || t >= best) continue; + const hy = y + dy * t; + if (hy >= YT && hy <= YB) { + best = t; + kind = 2; + } + } + if (kind === 0) break; + const u1 = u + dx * best, y1 = y + dy * best; + const n = floor(best) + 1, sx = (dx * best) / n, sy = (dy * best) / n, ww = (wt * best) / n; + let px = CX + u + sx * 0.5, py = y + sy * 0.5; + for (let i = 0; i < n; i++) { + const ix = px | 0, iy = py | 0; + if (ix >= 0 && ix < W && iy >= 0 && iy < H) { + buf[iy * W + ix] += ww; + kb[iy * W + ix] += ww * k; + } + px += sx; + py += sy; + } + u = u1; + y = y1; + if (kind === 2) dx = -dx; + else { + const q = (2 * (dx * nx + dy * ny)) / (nx * nx + ny * ny); + dx -= q * nx; + dy -= q * ny; + } + wt *= REFL; + } + } +} + +// The walls as one-LED lines, anti-aliased by distance: the arcs, the outer +// walls and the two corridor walls from focus to focus (or wherever Offset +// has moved them). +function buildWalls(S) { + const wall = S.wall, w = S.w; + for (let py = 0, i = 0; py < H; py++) + for (let px = 0; px < W; px++, i++) { + const u = px + 0.5 - CX, y = py + 0.5, au = abs(u); + let dmin = 9; + if (y <= YT || y >= YB) { + const v = y - (y <= YT ? YT : YB); + const E = u * u * IA2 + v * v * IB2, g = 2 * sqrt(u * u * IA2 * IA2 + v * v * IB2 * IB2); + dmin = abs(E - 1) / max(g, 1e-6); + dmin = min(dmin, sqrt((au - w) * (au - w) + v * v)); + } else dmin = min(abs(au - A), abs(au - w)); + wall[i] = cl(1 - dmin, 0, 1); + } + S.wallW = w; +} + +// A soft blur, 1 4 6 4 1 across and then down, about an LED wide, so the +// rays glow instead of drawing hard lines and the edge of the light steps +// down by under 40 levels a LED. +function blur(src, out, tmp) { + for (let y = 0; y < H; y++) { + const o = y * W; + for (let x = 0; x < W; x++) + tmp[o + x] = (src[o + max(0, x - 2)] + 4 * src[o + max(0, x - 1)] + 6 * src[o + x] + 4 * src[o + min(W - 1, x + 1)] + src[o + min(W - 1, x + 2)]) / 16; + } + for (let y = 0; y < H; y++) { + const o0 = max(0, y - 2) * W, o1 = max(0, y - 1) * W, o2 = y * W, o3 = min(H - 1, y + 1) * W, o4 = min(H - 1, y + 2) * W; + for (let x = 0; x < W; x++) out[o2 + x] = (tmp[o0 + x] + 4 * tmp[o1 + x] + 6 * tmp[o2 + x] + 4 * tmp[o3 + x] + tmp[o4 + x]) / 16; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + if (S.wallW !== S.w) buildWalls(S); + const hz = S.hz, kz = S.kz, wall = S.wall, col = S.col; + blur(S.buf, hz, S.tmp); + blur(S.kb, kz, S.tmp); + // Colour by how many mirrors the light has met: the direct light is a pale + // tint of the hue, and each reflection turns it a little further round the + // wheel and deeper, a sixth of a turn by the fifth. + // Round orange, yellow and yellow-green, 0.03 to 0.29 on the wheel, the + // colour goes paler so the dimmer far light reads as butter, not olive. + const h = S.hue + 0.5; + for (let j = 0; j < 16; j++) { + const f = 1 - exp(-j / 3), hj = h + 0.17 * f, hf = hj - floor(hj); + const mud = cl((hf - 0.01) / 0.04, 0, 1) * cl((0.33 - hf) / 0.06, 0, 1); + hsv(hj, (0.18 + 0.62 * f) * (1 - 0.6 * mud), 1, col, j * 3); + } + hsv(h + 0.27, 0.45, 34, col, 48); + const lx = CX + S.lu, ly = S.ly, ie = 1 / max(1, S.expo); + for (let y = 0, i = 0; y < H; y++) + for (let x = 0; x < W; x++, i++) { + const L = hz[i], v = min(1, LEVEL * Math.pow(L * ie, GAMMA)); + // The mean number of reflections here, read between two palette steps. + const kb = L > 1e-4 ? min(14.99, kz[i] / L) : 0, j = floor(kb), f = kb - j, o = j * 3; + const ddx = x + 0.5 - lx, ddy = y + 0.5 - ly, core = 255 * exp(-(ddx * ddx + ddy * ddy) / 1.1); + // A wall is a dim line in the dark and a darker line across the glow, + // a mirror seen edge on, so it never stands 40 levels off its sides. + const wv = wall[i], g = 255 * v * (1 - 0.5 * wv); + const r = g * (col[o] + (col[o + 3] - col[o]) * f) + wv * col[48] + core; + const gg = g * (col[o + 1] + (col[o + 4] - col[o + 1]) * f) + wv * col[49] + core; + const b = g * (col[o + 2] + (col[o + 5] - col[o + 2]) * f) + wv * col[50] + core; + display.setPixel(x, y, min(255, r), min(255, gg), min(255, b)); + } +} From 135844ba60a0b033fc5b826b603e187f475fb2e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 15:36:26 +0000 Subject: [PATCH 07/10] pattern-study: Essence Drop lifts warm hues to cream Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117bhD83MGCSerAR798cUib --- .../pattern-study/patterns/essence-drop.js | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/incoming/pattern-study/patterns/essence-drop.js b/incoming/pattern-study/patterns/essence-drop.js index 200943dde..e0f667981 100644 --- a/incoming/pattern-study/patterns/essence-drop.js +++ b/incoming/pattern-study/patterns/essence-drop.js @@ -11,7 +11,7 @@ // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Viscosity=0..1, Size=0..1 // -// Knob 1: Hue. The essence's colour, all the way round the wheel from aqua at 0; round orange and gold the pool and the bloom are lifted and paled so they stay champagne, never brown. +// Knob 1: Hue. The essence's colour, all the way round the wheel from aqua at 0. From about 0.42 to 0.8, rose through orange and gold to lime, the backdrop and the pool are lifted towards cream and paled so they read rose, peach, champagne, butter or pale lime, never brown or olive, and the bloom glows brighter than the pool instead of darker. // Knob 2: Speed. How slow the film runs: at 2 a drop takes 1.4 s to fall, lands every 5.5 s, and its crown, cavity and jet are over in 2.4 s while the bloom sinks for about 3 s more; at 0.2 all of it is ten times slower. // Knob 3: Viscosity. Watery at 0: a crown of nine fingers throws its beads, and a jet eight and a half drop radii high pinches off a bead. Turned up, the fingers go by 0.5 and the crown by 0.7, the jet shortens to a round mound with no bead, the drop sinks in on a thick neck, the thread behind it stretches long, everything runs up to 2.3 times slower, drops come every 7.7 s, the pool holds more colour and the bloom sinks as a slow compact plume. // Knob 4: Size. The drop's radius, 3.4 LEDs at 0, 3.7 at the start and 7.6 at 1, and the crown, the cavity, the jet and the bloom with it; the jet stops at 48 LEDs. @@ -290,21 +290,24 @@ export function draw(display, params) { if (dp) deep[i] = max(deep[i], cl(1 - dd, 0, 1)); } } - // Palette: backdrop, essence, and the deep colour the drop carries. Round - // orange and gold everything is lifted and paled so it reads champagne, - // never brown, and there the deep colour is brighter than the pool instead - // of darker. A thicker essence holds more colour. + // Palette: backdrop, essence, and the deep colour the drop carries. A + // thicker essence holds more colour. From rose through orange and gold to + // lime, hue -0.12 to 0.35, any of these at half brightness reads brown, + // khaki or olive on the LEDs, so there everything is lifted towards cream + // and paled: the backdrop to 200 of 255 or more with a shallow vignette, the + // pool to about 215, and the deep colour goes brighter than the pool + // instead of darker. const pal = S.pal, h = S.hue + 0.52, hf = h - floor(h), key = hf + 7 * v; if (S.palKey !== key) { - const wm = sm((hf - 0.02) / 0.04) * (1 - sm((hf - 0.19) / 0.06)); + const hw = hf > 0.5 ? hf - 1 : hf, wm = sm((hw + 0.12) / 0.06) * (1 - sm((hw - 0.28) / 0.07)); hsv(h + 0.02, 0.14 - 0.06 * wm, 1, pal, 0); - hsv(h + 0.06, 0.4 - 0.15 * wm, 1, pal, 10); - hsv(h, (0.38 + 0.22 * v) * (1 - 0.4 * wm), 1, pal, 3); - hsv(h + 0.015, 0.92 - 0.35 * wm, 1, pal, 6); + hsv(h + 0.06 * (1 - wm), 0.4 - 0.3 * wm, 1, pal, 10); + hsv(h, (0.38 + 0.22 * v) * (1 - 0.5 * wm), 1, pal, 3); + hsv(h + 0.015, 0.92 - 0.3 * wm, 1, pal, 6); pal[9] = wm; S.palKey = key; } - const wm = pal[9], lift = 1 + 0.18 * wm, dv = 0.46 + 0.5 * wm, dye = S.dye, T = S.T; + const wm = pal[9], dv = 0.46 + 0.5 * wm, dye = S.dye, T = S.T; for (let y = 0, i = 0; y < H; y++) { const below = y + 0.5 - Y0; for (let x = 0; x < W; x++, i++) { @@ -312,7 +315,7 @@ export function draw(display, params) { // The backdrop: a soft key light high in the middle, deepening towards // the surface. const gx = (x + 0.5 - 32) / 32, gy = (y - 14) / 46; - const gl = exp(-(gx * gx + gy * gy)), bv = min(1, (0.4 + 0.46 * gl) * lift); + const gl = exp(-(gx * gx + gy * gy)), bv = min(1, 0.4 + 0.46 * gl + wm * (0.36 - 0.26 * gl)); let r = (pal[10] + (pal[0] - pal[10]) * gl) * bv, g = (pal[11] + (pal[1] - pal[11]) * gl) * bv, b = (pal[12] + (pal[2] - pal[12]) * gl) * bv; // The crown's sheet: thin film between its walls, brighter at the two // sides where the eye looks along it, and along the lip. @@ -341,12 +344,12 @@ export function draw(display, params) { // deeper towards the bottom, slow shafts of light from the surface // swaying through it, and the deep colour where the drop has gone. const depth = cl(below / (H - Y0), 0, 1); - let lv = below < 0 ? 0.88 : 0.8 - 0.36 * depth; + let lv = below < 0 ? 0.88 + 0.09 * wm : 0.8 - 0.36 * depth + wm * (0.15 + 0.16 * depth); if (below >= 0) { const sx = x + 0.4 * below; lv += 0.16 * (1 - depth) * (0.5 + 0.5 * sin(0.3 * sx - 0.7 * T + 1.3 * sin(0.11 * sx + 0.23 * T))); } - lv = min(1, lv * lift); + lv = min(1, lv); let lr = pal[3] * lv, lg = pal[4] * lv, lb = pal[5] * lv; const da = max(deep[i] * 0.9, 1 - exp(-dye[i] * 0.9)); if (da > 0.003) { From 33b7f812b3ba2d802290734f7982c38e23f41723 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 15:48:56 +0000 Subject: [PATCH 08/10] =?UTF-8?q?Penrose=20Room=20=EC=9D=98=20=EB=B9=9B?= =?UTF-8?q?=EC=9D=84=20=EB=B0=9D=ED=98=80=20=EC=A7=80=EB=8F=84=EC=9D=98=20?= =?UTF-8?q?=EB=8B=A4=EB=A5=B8=20=EB=B9=88=EC=9E=90=EB=A6=AC=EB=A1=9C=20?= =?UTF-8?q?=EC=98=AE=EA=B8=B4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 32판의 Clay Mask 가 가장자리 × 모양 바뀜 1,3 에 둘째로 서서, Bokeh 혼자인 1,4 로 겨눈다. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117bhD83MGCSerAR798cUib --- incoming/pattern-study/patterns/penrose-room.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/incoming/pattern-study/patterns/penrose-room.js b/incoming/pattern-study/patterns/penrose-room.js index 265005d0c..2ef1e7c6f 100644 --- a/incoming/pattern-study/patterns/penrose-room.js +++ b/incoming/pattern-study/patterns/penrose-room.js @@ -30,9 +30,10 @@ const A = 31.5, B = 26, C = sqrt(A * A - B * B), CX = 32; const YT = 0.5 + B, YB = 127.5 - B, YM = 64; const IA2 = 1 / (A * A), IB2 = 1 / (B * B), EPS = 1e-6; // Each reflection keeps 60% of the light. The glow is LEVEL times the light -// over the mean of the lit LEDs, raised to GAMMA: the mean shows at 92 of 255 -// and the glow round the lamp comes close to full. -const REFL = 0.6, LEVEL = 0.36, GAMMA = 0.7; +// over the mean of the lit LEDs, raised to GAMMA: the mean shows at 128 of 255 +// and the glow round the lamp comes close to full. At 92 the swap moved the +// picture 0.270 on the study's drift scale; at 128 it moves it 0.365. +const REFL = 0.6, LEVEL = 0.50, GAMMA = 0.7; function hsv(h, s, v, out, o) { h = (h - floor(h)) * 6; From fe8a68db8a2437171ee4a640c0b05df1b7787782 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 15:51:52 +0000 Subject: [PATCH 09/10] =?UTF-8?q?README=20=EC=97=90=2033=ED=8C=90=20?= =?UTF-8?q?=ED=91=9C=EC=99=80=20=EC=97=B0=EA=B5=AC=20=EB=85=B8=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117bhD83MGCSerAR798cUib --- incoming/pattern-study/README.md | 83 +++++++++++++++++-- .../pattern-study/patterns/essence-drop.js | 2 +- 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 10b3368fa..a72dca02a 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 228개를 쉰다섯 갈래로 나눠 만들었어요. 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판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽의 질감 넷을 화장품 팝업 갈래에 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 238개를 쉰다섯 갈래로 나눠 만들었어요. 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판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. [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판에서 세로로 다시 짰어요. @@ -384,6 +384,21 @@ | [Sugar Scrub](patterns/sugar-scrub.js) | 화장품 팝업 | 장밋빛 오일 밤 위에 설탕 결정 칠백 알쯤이 떠 있고, 보이지 않는 손끝이 지름 26 LED 원을 그리며 지나간 자리에 분홍빛 광택 고리가 남아요. 손끝에 끌린 알갱이는 구르며 하얀 십자로 반짝이고 녹아 작아져요. 10초마다 새 한 숟갈이 더미로 얹혔다가 다음 원에 쓸려 퍼져요. | | [Talbot Carpet](patterns/talbot-carpet.js) | 물결·간섭 | 좁은 슬릿 격자를 지난 빛의 세기를, 격자에서 멀어지는 거리를 세로로 두고 그려요. 슬릿 상이 한 탤벗 길이마다 그대로, 반 길이마다 반 주기 옆으로 되살아나고, 그 사이 분수 거리에서는 둘과 넷으로 갈라진 상과 곧은 빛줄기가 엇갈려요. 남색 골과 청록 바닥, 흰 방울이 1 LED 망점 위에서 1초에 15줄씩 올라가요. | +### 33판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Bertrand Paradox](patterns/bertrand-paradox.js) | 데이터 그림 | 지름 40 LED 의 원 셋이 세 칸에 하나씩 서서, 원마다 다른 뜻의 무작위로 현을 1초에 24개씩 긋고 현은 LED 하나씩 부서져 사라져요. 삼각형 변보다 긴 현은 자홍, 짧은 현은 초록이고, 옆 막대와 숫자가 긴 현의 몫을 세어 위 원은 1/3 로, 가운데는 1/2 로, 아래는 1/4 로 모여요. K3 을 올리면 선이 원마다 다른 밀도의 점묘가 되고, K1 을 누르면 원과 셈을 비우고 처음부터 다시 그어요. | +| [Borromean Rings](patterns/borromean-rings.js) | 위상·매듭 | 테이트의 보로메오 고리를 황금 직사각형 세 장으로 만들고 세로로 2.3배 늘여 세웠어요. 하늘색 고리가 키 크게 서고 분홍 고리가 그것을 가로지르며 연두 고리가 허리를 둘러 20초에 한 바퀴 돌고, 앞 관이 뒤 관을 지나는 자리마다 LED 한 줄이 어두워져 위아래가 읽혀요. K3 은 직사각형의 긴 변 비를 1.618 에서 2.4 까지 바꾸고, K1 을 누르면 분홍 고리가 사라진 동안 연두 고리가 긴 고리 아래 끝으로 빠져나갔다 돌아와요. | +| [Cleansing Balm](patterns/cleansing-balm.js) | 화장품 팝업 | 서리 낀 셔벗 밤이 무광으로 판을 채우고, 아래에서 올라오는 온기와 보이지 않는 손끝이 닿은 곳마다 밤이 녹아 맑은 광택 오일이 돼요. 오일은 흘러내려 바닥에 고이고, 다 녹으면 위에서 새 밤이 다시 휘핑돼요. K3 은 녹는 빠르기와 높이를 정하고, K1 을 누르면 손끝 하나가 지름 20 LED 를 한 번에 녹여요. | +| [Dandelin Spheres](patterns/dandelin-spheres.js) | 기하·대칭 | 유리 원뿔을 평면이 자른 원뿔곡선 위아래로 당들랭 구 둘이 원뿔에 끼어 평면에 닿아 있어요. 곡선을 도는 점에서 두 초점까지의 거리와 원뿔 모선 위 두 토막이 금색과 분홍으로 짝지어 같은 길이로 보여요. K3 은 평면 기울기로 30도의 타원에서 74도의 포물선을 지나 쌍곡선까지 가고, K1 을 누르면 평면이 11초 동안 똑바로 섰다가 돌아와요. | +| [Essence Drop](patterns/essence-drop.js) | 화장품 팝업 | 맑은 물빛 에센스에 짙은 색 방울이 느린 화면으로 떨어져 손가락 아홉 개의 왕관을 세우고, 파인 자리가 닫히며 솟은 기둥 끝에서 구슬이 떨어져 나가요. 방울의 짙은 색은 소용돌이 고리로 말려 가라앉으며 웅덩이에 풀려요. K3 을 올리면 왕관과 구슬이 사라지고 젤처럼 무겁게 잠기며, K1 은 색상환을 다 돌고 누르면 방울이 바로 떨어져요. | +| [Gold Leaf](patterns/gold-leaf.js) | 화장품 팝업 | 맑은 세럼에 구겨진 금박 조각 마흔 개가 떠서 위아래 두 대류 칸을 따라 천천히 돌 뿐 가라앉지 않아요. 조각이 뒤집힐 때마다 빛을 향한 면이 흰 금빛으로 번쩍이고 돌아선 면은 짙은 금으로 내려가며, 먼 조각은 작고 흐려요. K1 은 색상환을 다 돌아 금박을 장밋빛 금이나 푸른 박으로 바꾸고, 누르면 세럼을 저어 조각이 느린 소용돌이로 돌며 다섯 배 빨리 뒤집혀요. | +| [Goldbach Comet](patterns/goldbach-comet.js) | 수론 | 짝수 n 을 두 소수의 합으로 쓰는 가짓수를 매끈한 예측값으로 나눠 가로로 찍고, n 은 아래로 커져요. n 이 3 이나 5 나 7 로 나뉘는지에 따라 여덟 줄기가 색으로 갈라지고, 아래로 갈수록 줄기가 가늘어지며 52만까지 가면 처음부터 다시 흘러요. K3 은 7 부터 소수 하나씩 줄기를 접어 한 줄로 모으고, K1 을 누르면 8초 동안 다 접었다 펴요. | +| [Peel-Off](patterns/peel-off.js) | 화장품 팝업 | 보랏빛 젤 막이 진줏빛 살결을 덮고, 위에서부터 벗겨지는 막이 옅은 안쪽을 드러낸 원통으로 말려 128줄을 내려가요. 막이 아래로 빠지면 젖은 새 막이 위에서 흘러내려 반무광으로 마르고, K3 은 그 막을 비치는 얇은 막에서 거의 불투명한 막까지 두껍게 해요. K1 은 색상환을 다 돌고, 누르면 바로 벗기기 시작해요. | +| [Penrose Room](patterns/penrose-room.js) | 물리·스프링 | 거울로 된 펜로즈의 방에서 빛 한 점이 떠돌고, 수백 개의 광선이 반사하며 복도와 위아래 반타원을 밝히는 동안 양옆 방은 캄캄해요. 빛이 반타원을 지나 옆방으로 건너가면 밝은 곳과 어두운 곳이 뒤바뀌어요. K3 은 복도 벽을 초점에서 4 LED 까지 옮겨 어둠에 빛이 새게 하고, K1 을 누르면 빛이 바로 건너가요. | +| [Sorting Network](patterns/sorting-network.js) | 선·궤적 | 실 열여섯 가닥이 이웃끼리 한 쌍씩 자리를 바꿔 128줄 만에 순서가 거꾸로 되고, 다음 네트워크가 이어받아 실마다 패널을 사인 곡선으로 가로질러요. 균일한 무작위 정렬 네트워크라 바꾼 자리를 찍은 점이 가운데로 반원 꼴로 몰려요. K3 은 실을 32가닥까지 늘리고, K1 을 누르면 실 하나만 8초 동안 밝혀 그 곡선을 보여 줘요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -1490,6 +1505,50 @@ E8 의 근 240개를 고셋의 4₂₁ 다면체 꼭짓점으로 그렸어요. 주제 6 으로 크롬 파우더가 무광에서 거울로 바뀌는 앞선을 그렸어요. Pearl Sheen 의 박막 간섭이나 Satin Fold 의 주름과 겹치지 않게, 날카로운 반사 경계와 문지른 자리를 따라 내려가는 앞선이 주인공이에요. 어두운 스튜디오(벽 밝기 0.2~0.4)에 비친 로즈골드는 중간 밝기가 갈색으로 보였어요. 제품 사진이 크롬을 찍는 식대로 벽을 0.56~0.78 로 밝히고 검은 반사판 띠를 세우자 금속으로 읽혔어요. 반지름 7 의 (1−d)² 붓으로 짠 첫 어플리케이터는 원 중심에 오래 머물러 거울 동그라미가 격자로 박혔어요. 반지름 10 의 납작한 (1−d⁴) 붓으로 바꾸고 한 번 건널 때 네 바퀴에 못 미치게 돌리자 앞선이 깨끗해졌어요. 수평선에 세로 물결을 크게 주면 닫힌 곡선이 생겨 술잔 같은 물건으로 보였어요. 반사 지도의 등고선이 닫히면 사물로 읽혀서 한 줄 물결로 뒀어요. 노브 반응을 재는 2~3초에는 거울이 위 4분의 1쯤뿐이라 거울 색만 바꾸던 Holo 가 0.02 였고, 가루 알갱이마다 오로라 색을 주자 0.10 이 됐어요. +## 33판 + +33판도 주제 5 「낯선 수학」 여섯과 주제 6 「화장품 브랜드 팝업」 넷이에요. 같이 돈 32판(#225)이 핀휠 타일링과 길브레스 표, 쿠션 팩트 망사 같은 것을 먼저 골라서, 이 판은 그와 겹치지 않게 골랐어요. 수학은 Sorting Network, Bertrand Paradox, Penrose Room, Dandelin Spheres, Borromean Rings, Goldbach Comet 이고, 화장품은 Cleansing Balm, Gold Leaf, Essence Drop, Peel-Off 예요. 무작위 정렬 네트워크는 31판이 「다음 판 감」에 적어 둔 감이고, 나머지 다섯은 이 판이 목록 밖에서 찾았어요. 여섯 다 있는 갈래에 들어가서 주제 2 로 연 갈래는 없어요. Borromean Rings 가 위상·매듭 갈래의 셋째, Goldbach Comet 이 수론 갈래의 다섯째예요. 화장품 넷 가운데 클렌징 밤과 금박 세럼은 「다음에 해 볼 질감」에 적혀 있던 감이에요. 주제 1 은 218개의 `--gaps` 가 `*` 를 찍은 두 칸, 가장자리 × 촘촘함 1,3 과 1,4 를 32판이 맡아서 30판처럼 패턴이 하나뿐인 칸을 겨눴어요. Bertrand Paradox 가 Neon Alley 혼자이던 채움 × 색 1,4 에 섰고, 같은 값으로 Penrose 혼자이던 촘촘함 × 색 3,4 에도 섰어요. Penrose Room 은 처음에 Plait 혼자이던 가장자리 × 모양 바뀜 1,3 을 겨눴는데, 32판을 합치고 보니 Clay Mask 가 먼저 둘째로 서 있었어요. 빛을 밝혀 Bokeh 혼자이던 1,4 로 옮겼어요. 패턴이 하나뿐인 칸은 32판까지의 228개에서 27 이었고 238개에서 24 예요. 열 개 다 어느 축의 끝도 넘지 않아서 칸 경계는 30판 그대로이고, 지도 21장이 다 `none` 이에요. 주제 4 는 Sorting Network 와 Bertrand Paradox 예요. Sorting Network 는 한 네트워크가 128줄을 다 써서 `thirds` 가 51/51/50 이고, Bertrand Paradox 는 원 셋이 세 칸에 하나씩 서서 24/28/25 예요. Goldbach Comet 과 Penrose Room 도 3분의 1마다 48% 를 넘게 켜요. 다섯 일꾼이 둘씩 짰고, 본체가 연구의 Lab 하네스와 각 slug 의 씨앗으로 다시 그려 보고 합쳤어요. + +### Sorting Network + +주제 5 와 4 로 31판이 「다음 판 감」에 적어 둔 무작위 정렬 네트워크를 그렸고, `thirds` 는 51/51/50 이에요. 실마다 궤적이 사인 곡선에 가깝다는 것은 에인절, 홀로이드, 로믹, 비라그(2007)가 내다보고 도베르뉴가 2018년에 증명했어요. 이웃 쌍을 아무렇게나 바꾸면 이 분포가 나오지 않아서, 계단 꼴 영 타블로를 그린-나이엔후이스-윌프의 훅 걷기로 고르게 뽑고 에델만-그린 대응으로 바꿈 차례를 얻었어요. 따로 돌린 셈에서 실이 3~32가닥인 네트워크 1400개가 다 거꾸로 끝났고, 4가닥의 16가지 차례는 16만 번 가운데 9873~10266 번씩 나왔어요. 처음 판은 실이 바꿀 때만 3.5줄 동안 건너고 나머지는 곧게 서서 벌집 무늬로만 읽혔어요. 실마다 앞뒤 바꿈 사이의 절반씩 쓰며 늘 움직이게 하고 네 가닥만 밝히자 큰 호가 보였어요. K4 는 균일한 타블로와 거품 정렬 타블로의 칸 값을 섞어 순위를 다시 매겨요. 행과 열을 따라 커지는 성질이 섞어도 남아서 어느 값에서나 정렬 네트워크이고, 0.75 쯤이면 사인 곡선이 곧은 대각선 다발로 바뀌어요. + +### Bertrand Paradox + +주제 5 로 베르트랑의 역설(1889)을 골랐고, 주제 4 와 주제 1 의 채움 × 색 1,4 칸도 같이 겨눴어요. 원 셋을 128줄의 세 칸에 하나씩 엇갈려 놓았어요. 둘레의 두 점을 잇는 위 원은 긴 현이 1/3 로, 반지름 위의 한 점에서 긋는 가운데 원은 1/2 로, 원판 안의 한 점을 중점으로 삼는 아래 원은 1/4 로 모여요. 삼각형 변보다 긴 현은 반지름 절반의 내접원을 지나는 현이라 그 안쪽은 늘 자홍이고, 아래 원은 가운데가 비어요. 처음 판은 켜진 LED 를 84% 에서 100% 사이로 두어 채움 0.245 에 색 0.537 이었어요. 꺼진 LED 까지 세는 색 셈에서는 반쯤 밝은 LED 가 색을 깎아서, 바래는 현을 어둡게 하지 않고 LED 마다 다른 문턱에서 하나씩 꺼지게 한 채 켜진 LED 는 다 255 로 두자 0.557 이 됐어요. 막대를 두 칸에서 세 칸으로 넓히고 현이 바래는 시간을 1.0 초에서 1.1 초로 늘려 채움 0.264, 색 0.576 이고 `thirds` 는 24/28/25 예요. 채움 × 색 1,4 에 Neon Alley 다음 둘째로 섰고, 촘촘함 0.227 로 Penrose 혼자이던 촘촘함 × 색 3,4 에도 섰어요. + +### Penrose Room + +주제 5 와 1 로 펜로즈의 비출 수 없는 방(1958)을 그렸어요. 위아래 반타원의 초점을 얇은 거울 벽 둘이 이어 복도를 만들어서, 초점 사이를 지난 광선은 몇 번을 튕겨도 초점 사이로만 돌아오고 양옆 방에 들지 못해요. 광선 2만 개를 40번씩 튕겨 보니 복도의 빛이 평평한 변을 53만 2천 번 건너는 동안 초점 밖으로 나간 적이 없었고, 벽을 3 LED 옮기자 7만~10만 번 새었어요. 반타원의 끝은 복도의 빛 아래서도 어둡지 않았어요. 초점을 스치는 광선이 거기까지 닿아서, 정말 캄캄한 곳은 빛이 없는 쪽의 옆방이나 복도뿐이에요. 처음 판은 광선 밝기가 포화돼 가장자리 0.320, 모양 바뀜 0.586 이었어요. 반사마다 빛의 40% 를 잃게 하고 켜진 LED 의 평균을 92 에 맞추는 노출을 걸자 가장자리 0.044, 모양 바뀜 0.270 이 됐어요. 처음 3.2초는 복도가 밝고 5초에 빛이 옆방에 닿아서, 2~8초의 창 안에서 밝은 곳과 어두운 곳이 한 번 뒤바뀌어요. 처음에는 Plait 혼자이던 가장자리 × 모양 바뀜 1,3 을 겨눴는데, 32판을 합치고 보니 Clay Mask 가 모양 바뀜 0.224 로 먼저 둘째로 서 있었어요. 켜진 LED 의 평균을 128 로 올려 모양 바뀜을 0.365 로 밀고 Bokeh 혼자이던 1,4 에 세웠고, 가장자리는 0.075 예요. + +### Dandelin Spheres + +주제 5 로 당들랭 구(1822)를 골랐어요. 평면이 원뿔을 자른 곡선과, 원뿔과 평면에 함께 닿는 두 구를 64×128 광선 추적으로 그렸어요. 구는 평면에 초점으로 닿고 원뿔에는 원으로 닿아요. 곡선을 도는 점에서 두 초점까지의 거리가 모선 위 두 토막과 같아서, 같은 길이끼리 금색과 분홍으로 칠했어요. 처음에는 카메라를 14도 위에 두었는데, 25도 기운 평면이 돌다가 자주 옆면으로 보여 타원이 한 줄로 납작해졌어요. 카메라를 28도로 올리고 평면이 옆면으로 보이는 각도는 빨리 지나가게 하자 타원이 거의 늘 열린 채로 읽혀요. 원뿔의 반각이 16도라 기울기가 74도를 넘으면 위 구가 위로 빠져나갔다가 아래 원뿔에서 다시 올라오고, 세운 패널에서는 그 길이 다 보여요. 움직임 0.179, 모양 바뀜 0.030 이고 `thirds` 는 95/71/15 예요. + +### Borromean Rings + +주제 5 로 테이트(1876)의 보로메오 고리를 위상·매듭 갈래의 셋째로 그렸어요. 꼭짓점 열둘이 정이십면체를 이루는 황금 직사각형 세 장이 모델인데, 그대로 두면 정육면체에 들어맞아 128줄 가운데 50줄쯤만 켜져요. 세 고리가 걸리는 조건은 축마다 한 고리가 다음 고리를 지나는 부등식뿐이라, 세로로 2.3배 늘여도 같은 고리예요. 그렇게 세우자 키 큰 고리가 100줄 남짓을 쓰고 `thirds` 는 23/40/12, 가장자리는 0.455 가 됐어요. 관은 구를 이어 깊이 버퍼로 그리는데, 구의 법선을 그대로 쓰면 하이라이트가 점선으로 끊겨서 관 방향 성분을 빼 매끈한 줄로 만들었어요. 앞 관이 다른 고리의 뒤 관을 지나는 자리에 LED 한 줄을 어둡게 두자 이 패널에서도 위아래가 또렷이 읽혔어요. K1 을 누르면 분홍 고리가 빠지고 연두 고리가 긴 고리 아래 끝으로 미끄러져 나가요. 내려다보는 시점에서는 빠진 고리가 끝과 겹쳐 보여서, 그동안 눈높이를 가운데보다 7도 아래로 내렸어요. + +### Goldbach Comet + +주제 5 로 골드바흐 혜성을 수론 갈래의 다섯째로 골랐어요. 소수 표시 열을 FFT 로 제곱해 2^19 까지 짝수의 분할 수를 셋업에서 55~110 ms 에 다 세고, 2만 아래 짝수 전부와 그 위 997번째마다 하나를 직접 센 값과 맞춰 봤더니 어긋난 것이 없었어요. n/ln²n 으로 나누면 n = 2000 에서 23%, 50만에서 15% 모자라서 줄기가 4 LED 쯤 기울었어요. C2 에 ∫dt/(ln t ln(n − t)) 를 곱한 값으로 나누자 비가 n = 2000 에서 0.953, 50만에서 0.997 로 1 에 붙었어요. 처음 판은 11 이상 소수의 인수까지 남겨서 줄기마다 4 LED 쯤 번진 띠가 겹쳤고 채움이 0.652 였어요. 그 인수를 미리 나눠 두고 K4 로만 되돌리게 하자 1, 6/5, 4/3, 8/5, 2, 12/5, 8/3, 16/5 의 여덟 줄기가 어두운 골을 사이에 두고 섰고, 채움 0.525, `thirds` 53/55/48 이에요. K3 은 작은 소수를 7 부터 하나씩 접어요. 7 이 접히면 분홍 줄이 옆 줄에 붙고, 마지막에 3 이 접힐 때 금색 무리와 나머지가 패널 가운데에서 한 줄로 만나요. + +### Cleansing Balm + +주제 6 으로 셔벗 클렌징 밤이 오일로 녹는 것을 그렸어요. 「다음에 해 볼 질감」에 적혀 있던 감이고, 주인공은 무광에서 유광으로 바뀌는 경계예요. 밤은 숟가락 자국과 서리 알갱이가 있는 무광으로 칠하고, 녹은 곳은 알갱이 없이 매끈한 오일로 바꿔 하이라이트가 미끄러지게 했어요. 움직임 0.134, 모양 바뀜 0.120 이고 `thirds` 는 100/100/100 이에요. 밝은 채널만 읽는 잣대에 녹는 것이 잡히도록 처음 분홍에서는 오일을 밤보다 확실히 어둡게 두었어요. 처음 띄운 판에서는 움직이는 빛이 화면 정면 가까이 올 때마다 평평한 오일이 통째로 하얗게 탔어요. 빛을 정면에서 60도 비켜 두니 잔물결과 고인 오일의 가장자리에서만 반짝여요. 주황과 노랑 쪽 색상에서는 오일이 갈색과 올리브로 가라앉아서, 그 범위에서는 오일을 꿀빛으로 밝혔어요. + +### Gold Leaf + +주제 6 으로 「다음에 해 볼 질감」에 적혀 있던 금박 세럼을 그렸어요. 29판의 Glitter Fall 이 글리터를 쏟아 쌓는 것과 달리, 조각은 네 벽에서 0 이 되는 흐름 함수 sin(πx/64)·sin(2πy/128) 의 대류 두 칸을 따라 돌 뿐 가라앉지 않아요. 조각마다 6~9 꼭짓점의 찢긴 다각형을 높이를 달리해 구기고 부채꼴 면으로 나눠서, 사원수로 뒤집을 때 면마다 반사각을 따로 지나며 하나씩 번쩍여요. 먼 3분의 1은 따로 그려 1-2-1 로 흐린 뒤 깔고 가까운 조각은 3×3 표본으로 또렷하게 얹어요. 처음 판은 조각 24개가 가까운 것도 지름 6 LED 쯤이라 움직임 0.034 에 K3 과 K4 반응이 0.02 였고, 그림자 면은 주황 갈색으로 읽혔어요. 가장 가까운 조각을 12 LED 로 키워 40개를 띄우고 짙은 면의 명도를 0.50 에서 0.58 로 올리자 움직임이 0.147 이 되고 노브 반응은 넷 다 0.09 를 넘었어요. 세럼을 금빛으로 물들이면 어두운 곳이 그대로 진흙색이 돼서, 위 0.11 에서 아래 0.025 까지 거의 검게 두고 물결 빛만 옅은 금으로 얹었어요. + +### Essence Drop + +주제 6 으로 맑은 에센스에 방울이 떨어지는 스킨케어 광고의 느린 화면을 그렸어요. 웅덩이의 면과 튀어 오르는 모양을 모두 거리장 하나에 부드럽게 합쳐서, 구슬이 면에 닿으면 녹아들고 기둥 끝은 목이 가늘어지며 떨어져 나가요. 가라앉는 색은 와류 고리의 단면인 반대로 도는 소용돌이 둘에 입자 수백 개를 실어 그렸고, 입자가 지나간 자리에 색을 남겨 퍼지게 했어요. 처음 판은 모양 바뀜이 0.018 로 축의 아래 끝 0.021 에 못 미쳤어요. 피어나는 색이 웅덩이와 밝기가 거의 같았고 시작부터 앞 방울의 색을 깔아 둔 탓이에요. 짙은 색을 웅덩이보다 어둡게 두고 깔아 둔 색을 빼자 모양 바뀜이 0.030 이 됐어요. 면의 광택선은 덮임이 0 에 가까운 줄에도 더해져 점선처럼 깜박여서, 경계를 가로질러 따로 그리자 이어진 선이 됐어요. 처음에는 K1 0.5~0.7 에서 수면 위 바탕이 124, 100, 80 같은 갈색이나 카키로 보였어요. 장미색부터 라임까지의 색상에서는 바탕을 200 위로, 웅덩이를 215 언저리로 올리고 채도를 반으로 내려 크림 쪽으로 옮겼고, 그 범위에서는 짙은 색을 거꾸로 웅덩이보다 밝게 해요. + +### Peel-Off + +주제 6 으로 필오프 젤 마스크가 한 장으로 벗겨지는 것을 그렸어요. 막은 떨어지는 줄에서 원통으로 말리는데, 바깥으로 나온 면이 막의 안쪽이라 색이 옅고 위쪽에 흰 빛 줄이 서요. 벗기는 선에는 막이 1초쯤 붙잡혀 2.5~5.5줄 늘어났다가 0.2초 만에 곧은 자리를 지나치며 풀리는 곳을 더해서, 자로 그은 선이 되지 않아요. 움직임과 모양 바뀜은 가장 밝은 채널만 읽어서 막을 제 색의 0.6 밝기로 깊게 두고 살결은 0.8 쯤으로 밝혔고, 움직임 0.232, 모양 바뀜 0.226 이 나왔어요. 그 깊이에서는 K1 0.30 의 주황이 황갈색으로, 0.45 의 연두가 올리브로 보여서 빨강부터 연두까지는 막을 살결보다 밝게 올리고 컬 밑 그림자도 반으로 줄였어요. 처음 판은 젖은 새 막의 두께 잡음이 빛 띠를 벌레처럼 비틀어서, 잡음을 18에서 4로 줄이고 앞머리 뒤로 잦아드는 물결 줄을 따로 그렸어요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -1532,7 +1591,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 24일 `dev`(`19cc7fe`)의 218개에 32판의 열 개를 더한 228개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 24일 `dev`(`07b2766`)의 228개에 33판의 열 개를 더한 238개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -1544,6 +1603,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 32판의 Talbot Carpet 이 1,3 에, Cushion Mesh 가 1,4 에 섰어요. 32판의 열 개도 어느 축의 끝을 넘지 않아서 칸 경계는 30판 그대로예요. 1열 위쪽 칸에 서는 길은 Sponge Blend 처럼 한 LED 체커였어요. 한쪽 칸에 매끈한 빛을 두고 옆 칸을 그 이웃 가운데 가장 어두운 것보다 꼭 40 높게 세우면, 이웃 쌍이 다 40 이하의 무른 차이라서 가장자리가 0 인 채로 촘촘함이 40/255 언저리까지 올라요. 그 위는 255 짜리 점이 만드는 날카로운 쌍으로 얻어요. 그 쌍이 셈에 드는 쌍의 21% 를 넘으면 가장자리가 0.21 을 넘어서, 1,4 는 가장자리 0.170~0.210 의 좁은 띠예요. Cushion Mesh 는 255 인 LED 를 프레임마다 769개로 고정해서 두 값이 씨앗과 상관없이 서요. 228개에서는 지도 21장이 다 `none` 이고, 패턴이 하나뿐인 칸은 31판까지의 26 에서 27 이 됐어요. 새로 찬 두 칸이 하나씩이고, Plait 혼자이던 가장자리 × 모양 바뀜 1,3 에 Clay Mask 가 둘째로 섰어요. +33판의 열 개도 어느 축의 끝을 넘지 않아서 칸 경계는 30판 그대로이고, 238개에서도 지도 21장이 다 `none` 이에요. 그래서 주제 1 은 30판처럼 패턴이 하나뿐인 칸을 겨눴고, 228개에서 27 이던 그런 칸이 24 가 됐어요. 채움 × 색 1,4 는 LED 의 70% 넘게 꺼진 채 색이 0.544 를 넘어야 하는 칸이에요. 색은 꺼진 LED 까지 세는 셈이라 반쯤 밝은 LED 가 값을 깎았어요. Bertrand Paradox 는 켜진 LED 를 다 255 로 두고 바래는 현을 LED 하나씩 끄자 0.537 에서 0.576 이 됐고, R − G 가 반대인 자홍과 초록이 그 값을 냈어요. 가장자리 × 모양 바뀜 1,4 는 번진 그림이 2~6초 사이에 블록 평균으로 0.331 넘게 바뀌어야 해요. Penrose Room 은 밝은 곳과 어두운 곳이 뒤바뀌는 순간을 그 창 안에 두고, 켜진 LED 의 평균을 92 에서 128 로 올려 모양 바뀜을 0.270 에서 0.365 로 밀었어요. 가장자리는 0.044 에서 0.075 로만 올랐어요. + 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 를 냈어요. @@ -1558,7 +1619,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ### 아직 없는 갈래 -`families.json` 의 쉰다섯 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 29판이 연 위상·매듭 갈래는 Hopf Fibration 과 31판의 Boy's Surface 예요. 28판이 열려다 접은 갈래라 28판의 Hairy Ball 은 노이즈·흐름에 그대로 있어요. 세잎 매듭처럼 이름 있는 매듭이 그 갈래의 셋째로 맞아요. 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 가 넷을 더해 열여섯이에요. 28판이 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table 이에요. 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 이 셋째예요. +`families.json` 의 쉰다섯 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 29판이 연 위상·매듭 갈래는 Hopf Fibration 과 31판의 Boy's Surface, 33판의 Borromean Rings 예요. 28판이 열려다 접은 갈래라 28판의 Hairy Ball 은 노이즈·흐름에 그대로 있어요. 세잎 매듭처럼 이름 있는 매듭이 그 갈래의 넷째로 맞아요. 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 가 넷을 더해 스물이에요. 28판이 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table, 33판의 Goldbach Comet 이에요. 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 이 셋째예요. ### 멋진 것 더 멋지게 @@ -1568,6 +1629,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 32판의 열 개는 가장 낮은 노브가 Cushion Mesh 의 K1 Hue 와 Chrome Powder 의 K4 Grain 으로 둘 다 0.05 예요. Cushion Mesh 는 지도의 칸을 지키려고 밝기를 실 0, 구멍 40, 방울 255 로 묶어 두어서, 색만 바꾸는 K1 이 0.04 였고 방울과 망사의 채도를 올려 0.05 가 됐어요. 2~3초 창에 아직 오지 않은 넓이만 바꾸는 노브는 낮게 나와요. Chrome Powder 의 K3 Holo 는 그 창에 위 4분의 1쯤뿐인 거울의 색만 바꿀 때 0.02 였고, 아직 문지르지 않은 가루 알갱이에도 색을 주자 0.10 이 됐어요. Arnold Tongues 의 K4 Depth 는 혀와 바탕의 대비를 높이자 0.03 으로 떨어졌고, 아래 끝을 3 에서 2 로 내려 1/3 과 2/3 혀가 꺼지게 하자 0.06 이 됐어요. Invasion Percolation 의 K4 는 입구 너비를 바꿀 때 0.0043 이었어요. 침입 스미기는 입구가 넓어도 같은 길을 찾아서, 문턱끼리 닮은 거리를 바꾸는 Grain 으로 바꾸자 0.13 이 됐어요. +33판의 열 개는 가장 낮은 노브가 Essence Drop 의 K2 Speed 와 K4 Size 로 둘 다 0.03 이에요. 웅덩이와 바탕이 다 켜진 그림에서 방울 하나가 바꾸는 LED 가 적어서, 웅덩이 속을 천천히 지나는 빛줄기를 더하고서야 Speed 가 0.02 에서 0.03 이 됐어요. Gold Leaf 는 조각이 작을 때 K3 Flakes 와 K4 Size 가 0.02 였고, 가장 가까운 조각을 지름 12 LED 로 키우자 둘 다 0.09 가 됐어요. Cleansing Balm 의 K4 Grain 도 결의 진폭을 0.09 에서 0.21 까지 넓혀 0.02 에서 0.04 가 됐어요. + Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. ### 세로 64×128 @@ -1582,9 +1645,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 32판의 셋은 세로축을 수학이 정했어요. Gilbreath Table 은 줄이 표의 깊이라 `thirds` 가 97/100/96 이고, 소수 간격이 남은 위 10줄을 붙박고 그 아래만 내려보내서 2~8초에 128줄 가운데 107줄이 바뀌어요. Arnold Tongues 는 줄의 높이가 결합 세기 K 라 100/100/100 이에요. K 를 직선으로 두면 혀가 위 3분의 1에서야 벌어져서, K = 1 − (1−u)² 로 위쪽 행을 늘렸어요. Talbot Carpet 은 줄이 격자에서 멀어지는 거리라 96/96/96 이에요. 셋 다 바탕까지 수학이라 지울 바탕이 없어요. +33판에서는 Sorting Network 가 한 네트워크의 바꿈 120번을 128줄에 담아 51/51/50 이고, Bertrand Paradox 는 원 셋을 세 칸에 하나씩 엇갈려 놓아 24/28/25 예요. Goldbach Comet 은 줄이 log₂ n 이라 네 옥타브가 패널에 들어 53/55/48 이고, Penrose Room 은 반타원 둘과 복도가 패널 전체라 68/61/72 예요. 3차원 대상은 이번에도 세로를 따로 골라야 했어요. Borromean Rings 의 황금 직사각형 셋은 꼭짓점이 정이십면체라 어느 쪽에서 봐도 둥글어서, 그대로는 128줄 가운데 50줄쯤만 켰어요. 세로로 2.3배 늘여도 고리가 걸리는 부등식은 그대로라 늘이자 23/40/12 가 됐어요. Dandelin Spheres 는 원뿔의 꼭짓점이 아래에 있어 95/71/15 예요. + ### 낯선 수학 -주제 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)이에요. +주제 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)이에요. 목록 밖에서 다음에 해 볼 것이에요. 콜라코스키 수열(1965)은 제 길이 부호를 읽으면 제가 되는 1 과 2 의 줄이라 리듬·펄스에 맞아요. 영 도형의 플랑셰렐 성장은 상자가 하나씩 붙으며 로건-셰프와 베르시크-케로프의 극한 곡선(1977)으로 다가가요. 아즈텍 다이아몬드의 북극권과 같은 극한 모양이라 같은 판에 두지 않아요. 슈타이너 사슬은 반전 하나로 고리의 원들이 미끄러지는 포리즘이고, 민코프스키의 물음표 함수(1904)는 페리 나무를 이진 나무로 펴는 계단이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 렌니의 주차 문제(1958)는 줄마다 길이 1 인 차가 아무 데나 서다가 빈자리가 다 1 보다 짧아지면 멈추는데, 128줄이 다 멈추면 채운 몫이 렌니 상수 0.7476 언저리에 모여요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -1598,7 +1663,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 32판에서 본 것이에요. 수학이 정한 규칙이 셈을 줄였어요. Gilbreath Table 은 0 과 2 의 줄이 XOR 이라 뤼카 정리로 2^s 줄을 한 번에 건너뛰어 1만 줄 아래를 열고, Talbot Carpet 은 τ 에 대해 주기가 1 이라 한 길이를 512줄 표로 만들어 스크롤만 해요. 흔히 그리는 그림이 그 수학이 아닌 때도 있었어요. 핀휠을 중점으로 나누는 그림은 방향이 여덟 가지에서 멈춰서, 단계마다 방향 수를 세어 보고서야 콘웨이의 자르기로 바꿨어요. 64열에서 가늘어지는 대상은 축을 휘어야 읽혔어요. 아르놀트 혀는 너비가 K^q 처럼 자라서 K 를 직선으로 두면 1/2 혀 말고는 세로 실선이었어요. -목록 밖에서 31판 뒤에 해 볼 것이에요. 모자 타일(스미스, 마이어스, 캐플런, 굿먼-스트라우스, 2023)은 한 가지 타일로만 평면을 비주기로 덮고, 뒤집어 놓인 타일이 판에 드문드문 흩어져요. 무작위 정렬 망(에인절, 홀로이드, 로믹, 비라그, 2007)은 이웃 맞바꿈만으로 n 개의 차례를 뒤집는 길인데, 줄마다 맞바꿈 하나를 두면 실 하나하나가 사인 곡선에 다가가 128줄이 짠 천처럼 돼요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. +33판에서 본 것이에요. 분포가 정리의 주인공인 대상은 고르게 뽑는 셈이 그림을 정했어요. 정렬 네트워크는 이웃 쌍을 아무렇게나 바꾸면 사인 곡선이 나오지 않아서, 훅 걷기로 영 타블로를 고르게 뽑아 에델만-그린 대응으로 옮겼어요. 베르트랑의 역설은 그 뽑는 법 셋이 그대로 원 셋이에요. 예측이 매끈한 값을 줄 때는 나누는 값을 정확히 맞춰야 줄기가 섰어요. Goldbach Comet 은 n/ln²n 으로 나누면 줄기가 4 LED 기울었고, 적분 꼴로 나누자 곧게 섰어요. 정리가 어둠을 말하는 대상은 그 어둠이 정말 0 인지 셈으로 먼저 봤어요. Penrose Room 은 광선 2만 개를 40번씩 튕겨 초점 밖으로 나간 광선이 하나도 없는 것을 본 뒤에 노출을 걸었어요. 3차원 대상은 수학이 허락하는 만큼 늘여 패널에 맞췄고, Borromean Rings 는 세로로 2.3배 늘여도 같은 고리예요. + +목록 밖에서 31판 뒤에 해 볼 것이에요. 모자 타일(스미스, 마이어스, 캐플런, 굿먼-스트라우스, 2023)은 한 가지 타일로만 평면을 비주기로 덮고, 뒤집어 놓인 타일이 판에 드문드문 흩어져요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. + +목록 밖에서 33판 뒤에 해 볼 것이에요. 뷔퐁의 바늘(1777)은 Bertrand Paradox 와 같은 꼴로, 금을 넘는 바늘의 몫이 2/π 로 모이는 막대와 함께 세울 수 있어요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. ### 화장품 팝업 @@ -1610,7 +1679,9 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 32판이 넷을 더 썼어요. 쿠션 팩트의 망사와 맺히는 방울(Cushion Mesh), 마르며 색이 빠지는 클레이(Clay Mask), 오일 속에서 구르고 녹는 설탕 결정(Sugar Scrub), 무광에서 거울로 바뀌는 크롬 파우더(Chrome Powder)예요. 「다음에 해 볼 질감」에 남아 있던 쿠션 퍼프와 클레이 마스크를 썼어요. 젖은 광은 평행광으로 그리면 64×128 에서 점 몇 개로 사라져서, Clay Mask 는 점광원의 반사점이 판을 떠다니게 했어요. 금속은 어두운 스튜디오에서 갈색으로 보였고, Chrome Powder 는 제품 사진처럼 벽을 밝히고 검은 반사판 띠를 세워서 읽혔어요. 반사의 등고선이 닫히면 술잔 같은 사물로 읽혔어요. Cushion Mesh 는 지도의 칸이 밝기를 실 0, 구멍 40 으로 정해서 채움이 0.500 이에요. 바탕을 다 켠 30판의 넷과 달리 채움 축의 가운데에 서요. -다음에 해 볼 질감이에요. 클렌징 밤은 셔벗처럼 서 있다가 체온에 녹아 맑은 기름이 되는데, 녹는 앞머리에서 무른 무광이 윤 나는 액체로 바뀌는 것이 그림이에요. 금박을 띄운 세럼은 구겨진 금박 조각이 떠서 천천히 돌며 빛을 받는데, Glitter Fall 과 겹치지 않게 떨어지지 않고 떠 있는 쪽이어야 해요. 세팅 미스트가 유리판에 맺혀 굵어지고 흘러내리는 것은 Lip Gloss 와 Setting Mist 사이라 같은 판에 두지 않아요. 매니큐어 붓이 남긴 줄이 스스로 펴지는 것은 Cream Swirl 과 가까워서 같은 판에 두지 않아요. 자석 젤 네일은 보이지 않는 자석을 따라 반짝이 띠가 모였다 흩어지는 그림이에요. 젖은 립글로스나 탑코트가 굳는 것은 Clay Mask 처럼 광이 먼저, 색이 나중에 빠지는 두 문턱으로 그릴 만해요. +33판이 넷을 더 썼어요. 셔벗 밤이 녹아 오일이 되는 클렌징 밤(Cleansing Balm), 떠서 뒤집히는 금박 조각(Gold Leaf), 에센스에 떨어지는 방울과 왕관(Essence Drop), 한 장으로 말려 벗겨지는 젤 마스크(Peel-Off)예요. 앞의 둘은 「다음에 해 볼 질감」에 적혀 있던 감이에요. 주황과 노랑 쪽의 갈색과 올리브는 넷 다에서 다시 왔어요. 짙게 둔 바탕이나 그림자가 문제였고, 넷 다 그 색상 범위에서만 바탕을 밝히고 채도를 내렸어요. Essence Drop 은 수면 위 바탕이 124, 100, 80 이던 것을 200 위로 올렸고, Gold Leaf 는 세럼을 거의 검게 두고 금박의 짙은 면을 0.58 로 올렸어요. 무광과 유광의 차이는 밝기로 옮겨야 잡혔어요. Cleansing Balm 은 오일을 밤보다 어둡게, Peel-Off 는 막을 살결보다 어둡게 두어서 움직임과 모양 바뀜이 그 차이를 읽어요. + +다음에 해 볼 질감이에요. 세팅 미스트가 유리판에 맺혀 굵어지고 흘러내리는 것은 Lip Gloss 와 Setting Mist 사이라 같은 판에 두지 않아요. 매니큐어 붓이 남긴 줄이 스스로 펴지는 것은 Cream Swirl 과 가까워서 같은 판에 두지 않아요. 자석 젤 네일은 보이지 않는 자석을 따라 반짝이 띠가 모였다 흩어지는 그림이에요. 젖은 립글로스나 탑코트가 굳는 것은 Clay Mask 처럼 광이 먼저, 색이 나중에 빠지는 두 문턱으로 그릴 만해요. 립밤이 체온에 녹아 번지거나 고체 향수가 문질러 펴지는 것은 Cleansing Balm 의 녹는 경계를 가로로 돌린 그림이에요. 흰 크림에 금박이 박혀 크림과 함께 천천히 접히는 것은 떠 있는 Gold Leaf 와 다른 그림이에요. 크림 한 방울이 젤 위에 떨어져 천천히 잠기는 것은 Essence Drop 의 거리장을 그대로 써요. 시트 마스크가 굴곡을 따라 붙으며 공기 방울이 밀려 빠지는 것은 Peel-Off 를 거꾸로 돌린 그림이에요. ## 다시 돌리기 diff --git a/incoming/pattern-study/patterns/essence-drop.js b/incoming/pattern-study/patterns/essence-drop.js index e0f667981..754aa2cd9 100644 --- a/incoming/pattern-study/patterns/essence-drop.js +++ b/incoming/pattern-study/patterns/essence-drop.js @@ -227,7 +227,7 @@ function build(S) { } } if (tau >= 0.42 && v < 0.5) { - // The beads thrown from the fingers fly out and up, slow, and drop back. + // The beads thrown from the fingers fly out and up, then slow and drop back. const t = tau - 0.42, Hp = hc * Math.pow(sin((PI * 0.42) / TCR), 0.6), Rp = r * (1.4 + 1.5 * (1 - exp(-0.42 / 0.25))) + 0.35 * Hp; for (const [ph, lk, vy, vx] of e.fing) { const cs = cos(ph), lf = lk * 0.55 * hc * (1 - v / 0.5); From de879b87e7285522e274add689122f2188561309 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 16:07:03 +0000 Subject: [PATCH 10/10] =?UTF-8?q?index.html=20=EC=9D=84=2033=ED=8C=90?= =?UTF-8?q?=EA=B9=8C=EC=A7=80=20=EB=8B=A4=EC=8B=9C=20=EC=A7=93=EB=8A=94?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117bhD83MGCSerAR798cUib --- incoming/pattern-study/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index bcbe8c52f..b8c8832a2 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -141,7 +141,7 @@

패턴 연구

- +