From e50eb6069198a4861ad400b59abdc0ba8f21edb9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 16:30:08 +0000 Subject: [PATCH 1/9] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20=EC=97=B0?= =?UTF-8?q?=EA=B5=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=2035=ED=8C=90?= =?UTF-8?q?=20=ED=8C=A8=ED=84=B4=EC=9D=B4=20=EC=84=A0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SW1UfDNBr6xKa7hi5BogQV From b792e0bbf6c53a3c623805fd5e85b8f071700ce2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 17:03:05 +0000 Subject: [PATCH 2/9] incoming: add constant-width and renyi-parking (pattern round 35) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SW1UfDNBr6xKa7hi5BogQV --- .../pattern-study/patterns/constant-width.js | 264 +++++++++++++++++ .../pattern-study/patterns/renyi-parking.js | 270 ++++++++++++++++++ 2 files changed, 534 insertions(+) create mode 100644 incoming/pattern-study/patterns/constant-width.js create mode 100644 incoming/pattern-study/patterns/renyi-parking.js diff --git a/incoming/pattern-study/patterns/constant-width.js b/incoming/pattern-study/patterns/constant-width.js new file mode 100644 index 000000000..bbec902cb --- /dev/null +++ b/incoming/pattern-study/patterns/constant-width.js @@ -0,0 +1,264 @@ +// ===== Patternflow pattern ===== +// Title: Constant Width +// Family: geometry +// Idea: A column of curves of constant width 62, Reuleaux triangles, pentagons and heptagons, smooth curves built from odd harmonics and a disc, slides down a channel exactly 62 LEDs wide, each turning at its own rate while it touches both walls and the lines it shares with the curves above and below, so the stack never jams and never comes apart. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Curves of constant width, the Reuleaux polygons (Franz Reuleaux, 1876), and Barbier's theorem that they all have perimeter pi times their width (Joseph-Émile Barbier, 1860) +// Round: 35 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Round=0..1, Spin=0..1 +// +// Knob 1: Hue. Turns the eight colours round the wheel together. At 0 the Reuleaux polygons are gold, rose, teal and lime, lit to 81%, and the smooth curves and the disc violet, azure, indigo and sky at 52%. Where a deep one would land between orange and lime it is lifted and paled, since there it would read brown or olive. +// Knob 2: Speed. How fast the column slides: 3 rows a second times the knob, so at 2 a curve takes 10 seconds to pass down its own height of 62 rows and at 0.2 it takes 100. +// Knob 3: Round. Mixes every curve with the disc of the same width, support function by support function, which keeps the width at 62. At 0 the Reuleaux triangle has the least area a curve of width 62 can have, 2709 LEDs; at 1 every curve is the disc, 3019. The perimeter stays 194.8 the whole way. +// Knob 4: Spin. How fast the curves turn, 0.4 radians a second at 0 and 3 at 1; neighbours turn opposite ways and each at its own multiple of that. +// Buttons: K1 tap turns the corner trace on or off: each corner leaves a gold line in its own 62 x 62 square, and a turning Reuleaux triangle draws the square with rounded corners that the Watts drill cuts. + +const W = 64, H = 128, WD = 62, N = 256, CYCLE = 8; +const { min, max, floor, ceil, round, sin, cos, sqrt, abs, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const hash = (i) => { + let h = Math.imul(i ^ 0x9e3779b9, 0x85ebca6b); + h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// The eight curves of the cycle, all of width WD. A Reuleaux n-gon is the +// intersection of the n disks of radius WD centred on the corners of a +// regular n-gon whose corner-to-far-corner distance is WD. A harmonic curve +// has support function h = WD/2 + sum a cos(k t + c) over odd k from 3 up: +// odd k is what makes h(t) + h(t + pi) = WD, the width in every direction, +// and the radius of curvature h + h'' = WD/2 - sum a (k^2 - 1) cos(k t + c) +// stays above 0, so it is convex. The amplitudes below keep that radius at +// least 2 LEDs. +const SHAPES = [ + { n: 3 }, + { harm: [[3, 3.6, 0]] }, + { n: 5 }, + { harm: [[3, 2.2, 0], [5, 0.5, 1.1]] }, + { n: 7 }, + { disc: true }, + { n: 3 }, + { harm: [[3, 2.6, 0], [7, 0.16, 2.0]] }, +]; +// Base hue and brightness for each place in the cycle. The sharp curves are +// lit brighter than the smooth ones, so the column reads as light and deep +// shapes taking turns as it slides. The map's drift comes mostly from the +// dark corners of each square, which swing round as the curves turn: with +// all eight at one brightness it reads 0.263, with these 0.277. +const BASE = [[0.13, 0.86], [0.76, 0.55], [0.95, 0.86], [0.6, 0.55], [0.48, 0.86], [0.68, 0.55], [0.3, 0.86], [0.54, 0.55]]; +// The body angles each curve's trace follows: the n corners of a Reuleaux +// polygon and the k peaks of a curve with one harmonic, which all run round +// the same path, and for a curve of two harmonics the one point where its +// support function peaks, since its corners are not alike and each would +// draw a path of its own. +for (const s of SHAPES) { + if (s.n) s.tr = Array.from({ length: s.n }, (_, c) => (TAU * c) / s.n); + else if (s.harm && s.harm.length === 1) { + const [k, , c0] = s.harm[0]; + s.tr = Array.from({ length: k }, (_, m) => (TAU * m - c0) / k); + } else if (s.harm) { + let best = -1e9, at = 0; + for (let i = 0; i < 3600; i++) { + const b = (TAU * i) / 3600; + let h = 0; + for (const [k, a, c] of s.harm) h += a * cos(k * b + c); + if (h > best) (best = h), (at = b); + } + s.tr = [at]; + } else s.tr = [0]; +} + +// Support function h and its derivative at body angle b, for a curve mixed +// a share t of the way to the disc. +function support(s, b, t, out) { + let h, d; + if (s.n) { + const n = s.n, R = WD / (2 * cos(PI / (2 * n))), step = TAU / n, half = PI / (2 * n); + let del = b - round(b / step) * step; + if (abs(del) <= half) (h = R * cos(del)), (d = -R * sin(del)); + else { + // The opposite direction falls in a corner's cone, so here the curve + // is the arc of radius WD round that corner. + const b2 = b + PI; + del = b2 - round(b2 / step) * step; + (h = WD - R * cos(del)), (d = R * sin(del)); + } + } else if (s.harm) { + (h = WD / 2), (d = 0); + for (const [k, a, c] of s.harm) (h += a * cos(k * b + c)), (d -= a * k * sin(k * b + c)); + } else (h = WD / 2), (d = 0); + out[0] = (1 - t) * h + (t * WD) / 2; + out[1] = (1 - t) * d; +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, round: 0, spin: 0.06, + // The column's slide in rows and the spin clock in radians. + slide: 15, clock: 0, + trace: false, traces: new Map(), + px: new Float32Array(N), py: new Float32Array(N), + xl: new Float32Array(H), xr: new Float32Array(H), + lab: new Int8Array(W * H), rgb: new Float32Array(W * H * 3), + col: new Float32Array(CYCLE * 6), colHue: NaN, sup: new Float32Array(2), + }; +} + +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.round = cl(k[2], 0, 1); + P.spin = cl(k[3], 0, 1); + if (input.btnPressed && input.btnPressed[0]) { + P.trace = !P.trace; + P.traces.clear(); + } + P.slide += d * P.speed * 3; + P.clock += d * (0.4 + 2.6 * P.spin); +} + +// The angle a curve in slot j has turned to: neighbours turn opposite ways, +// each at a rate between 0.7 and 1.3 times the clock, from a start of its own. +const angle = (j, clock) => hash(j * 3 + 1) * TAU + (j & 1 ? -1 : 1) * (0.7 + 0.6 * hash(j * 3 + 2)) * clock; + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const { px, py, xl, xr, lab, rgb, col, sup, traces } = P; + if (P.colHue !== P.hue) { + // Fill and rim colours for each place in the cycle. The rim is the fill + // pushed to full brightness and halfway to white, at least 40 levels + // above every fill so it reads as a hard line. + // A deep fill from orange round to lime reads as brown or olive, so in + // that band the deep curves are lifted toward the light ones' level and + // paled a little. + for (let c = 0; c < CYCLE; c++) { + const h = P.hue + BASE[c][0], hf = h - floor(h), band = BASE[c][1] < 0.7 ? max(0, 1 - abs(hf - 0.14) / 0.15) : 0; + hsv(h, 0.8 - 0.22 * band, (BASE[c][1] + 0.3 * band) * 0.94, col, c * 6); + hsv(h, 0.45, 1, col, c * 6 + 3); + } + P.colHue = P.hue; + } + lab.fill(0); + rgb.fill(0); + // Slot j's top line sits at row slide - j * WD: the slots stack upward + // with j and the column slides down as slide grows. + const jLo = floor((P.slide - H) / WD), jHi = ceil(P.slide / WD); + const vis = []; + for (let j = jLo; j <= jHi; j++) { + const top = P.slide - j * WD; + if (top >= H || top + WD <= 0) continue; + const shape = SHAPES[((j % CYCLE) + CYCLE) % CYCLE], phi = angle(j, P.clock); + // Boundary points from the support function, panel direction a = 2 pi i + // / N: the point is h u(a) + h' u'(a) with the body angle a - phi. The + // panel's y runs down, so i = N/2 faces the left wall and 3N/4 faces up. + for (let i = 0; i < N; i++) { + const a = (TAU * i) / N, ca = cos(a), sa = sin(a); + support(shape, a - phi, P.round, sup); + px[i] = sup[0] * ca - sup[1] * sa; + py[i] = sup[0] * sa + sup[1] * ca; + } + // The left support line on the inner face of the left wall, x = 1, and + // the top one on the slot's top line. The width is WD in every + // direction, so the right one lands on x = 63 and the bottom one on the + // next slot's top line without being placed there. + const ox = 1 - px[N / 2], oy = top - py[(3 * N) / 4]; + const label = vis.length + 1; + xl.fill(1e9); + xr.fill(-1e9); + for (let i = 0; i < N; i++) { + const xa = px[i] + ox, ya = py[i] + oy, i2 = (i + 1) % N, xb = px[i2] + ox, yb = py[i2] + oy; + if (ya === yb) continue; + const r0 = max(0, ceil(min(ya, yb) - 0.5)), r1 = min(H - 1, floor(max(ya, yb) - 0.5)); + for (let r = r0; r <= r1; r++) { + const x = xa + ((r + 0.5 - ya) * (xb - xa)) / (yb - ya); + if (x < xl[r]) xl[r] = x; + if (x > xr[r]) xr[r] = x; + } + } + for (let r = max(0, floor(top)); r <= min(H - 1, ceil(top + WD)); r++) { + if (xr[r] < xl[r]) continue; + const x0 = max(1, ceil(xl[r] - 0.5)), x1 = min(W - 2, floor(xr[r] - 0.5)); + for (let x = x0; x <= x1; x++) lab[r * W + x] = label; + } + vis.push({ + j, top, c: ((j % CYCLE) + CYCLE) % CYCLE, shape, phi, ox, oy, + left: py[N / 2] + oy, right: py[0] + oy, upX: px[(3 * N) / 4] + ox, downX: px[N / 4] + ox, + }); + } + // Fill, with a rim wherever a 4-neighbour belongs to something else. + for (let y = 0; y < H; y++) + for (let x = 1; x < W - 1; x++) { + const p = y * W + x, l = lab[p]; + if (!l) continue; + const rim = lab[p - 1] !== l || lab[p + 1] !== l || (y > 0 && lab[p - W] !== l) || (y < H - 1 && lab[p + W] !== l); + const o = vis[l - 1].c * 6 + (rim ? 3 : 0), q = p * 3; + (rgb[q] = col[o]), (rgb[q + 1] = col[o + 1]), (rgb[q + 2] = col[o + 2]); + } + // The walls: dim slate, brighter where a slot's line meets them, and white + // over five rows where a curve touches them. + for (let y = 0; y < H; y++) + for (const x of [0, W - 1]) { + const q = (y * W + x) * 3; + (rgb[q] = 0.14), (rgb[q + 1] = 0.18), (rgb[q + 2] = 0.24); + } + const dot = (x, y, r, g, b) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const q = (y * W + x) * 3; + (rgb[q] = r), (rgb[q + 1] = g), (rgb[q + 2] = b); + }; + for (const v of vis) { + const ty = floor(v.top); + for (const x of [0, W - 1]) dot(x, ty, 0.55, 0.6, 0.7); + for (let e = -2; e <= 2; e++) dot(0, floor(v.left) + e, 1, 1, 1), dot(W - 1, floor(v.right) + e, 1, 1, 1); + // Above and below: a small white cross where the curve meets the line. + for (const [cx, cy] of [[v.upX, v.top], [v.downX, v.top + WD]]) { + const x = floor(cx), y = floor(cy - (cy === v.top ? 0 : 1e-3)); + dot(x, y, 1, 1, 1), dot(x - 1, y, 1, 1, 1), dot(x + 1, y, 1, 1, 1), dot(x, y - 1, 1, 1, 1), dot(x, y + 1, 1, 1, 1); + } + } + // The corner trace, kept in each slot's own square so it slides with it. + if (P.trace) { + const seen = new Set(); + for (const v of vis) { + seen.add(v.j); + let t = traces.get(v.j); + if (!t) traces.set(v.j, (t = { xs: [], ys: [], last: null })); + const tr = v.shape.tr, m = tr.length, now = []; + for (let c = 0; c < m; c++) { + // At a peak of the support function h' is 0, so the point is h times + // the unit vector there. + const a = tr[c] + v.phi; + support(v.shape, tr[c], P.round, sup); + const ca = cos(a), sa = sin(a); + now.push([sup[0] * ca - sup[1] * sa + v.ox, sup[0] * sa + sup[1] * ca + v.oy - v.top]); + } + if (t.xs.length < 12000) + for (let c = 0; c < m; c++) { + const [x1, y1] = now[c], [x0, y0] = t.last ? t.last[c] : now[c]; + const steps = max(1, ceil(sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2) / 0.7)); + for (let s = 1; s <= steps; s++) t.xs.push(x0 + ((x1 - x0) * s) / steps), t.ys.push(y0 + ((y1 - y0) * s) / steps); + } + t.last = now; + for (let i = 0; i < t.xs.length; i++) dot(floor(t.xs[i]), floor(t.ys[i] + v.top), 1, 0.78, 0.25); + } + for (const j of traces.keys()) if (!seen.has(j)) traces.delete(j); + } + 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/renyi-parking.js b/incoming/pattern-study/patterns/renyi-parking.js new file mode 100644 index 000000000..564eada08 --- /dev/null +++ b/incoming/pattern-study/patterns/renyi-parking.js @@ -0,0 +1,270 @@ +// ===== Patternflow pattern ===== +// Title: Rényi Parking +// Family: data +// Idea: Each of the 128 rows is a street 60 LEDs long where cars of one length arrive at random points and park only where they fit; the streets scroll down, so the panel runs from empty streets at the top through filling ones to jammed ones at the bottom, packed to about 73% as Rényi's parking problem says, with the latecomers that squeezed into the last gaps lit warm and a gauge at the side holding the last 128 jammed streets against the prediction. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Rényi's parking problem (Alfréd Rényi, 1958), with its lattice form, Flory's dimers (Paul Flory, 1939) +// Round: 35 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Length=6..15, Lattice=0..1 +// +// Knob 1: Hue. Turns the colours round the wheel together. At 0 the first cars to park are azure, later ones violet and rose, and the last ones, squeezed into gaps barely a car long, gold; the gauge's tick is gold too. +// Knob 2: Speed. How fast the streets scroll: 2 rows a second times the knob, so at 2 a street takes 32 seconds from empty at the top to jammed at the bottom, and at 0.2 over five minutes. +// Knob 3: Length. The car length in LEDs, 6 to 15 in whole LEDs, so a street is 10 to 4 car lengths long. A street x car lengths long holds 0.7476 x - 0.2524 cars on average once jammed, 7.224 at 6 LEDs, so the share it fills falls from 0.722 at 6 LEDs to 0.686 at 15; the gold tick moves with it. +// Knob 4: Lattice. Below 0.07 cars park anywhere. Above it they may only stop on a grid of k points to a car length, k = 1 / Lattice rounded: 4 points at 0.25, where a jammed street of 6-LED cars fills 0.784; 2 at 0.5, Flory's dimers, 0.851 here and 1 - e^-2 = 0.865 on an endless street; from 0.67 up every car has its own bay and every bay fills, which for 6-LED cars is the whole street. +// Buttons: K1 tap stops the streets and slides every car to the left end of its street, so each row becomes a bar as long as its fill and the ragged right edge draws how a street fills as it travels down; the gold line is the prediction, and the bars of the jammed rows stand along it. Three seconds later the cars slide back and the streets move on. + +const W = 64, H = 128, S = 60, MAXC = 32, GX = 61; +// Rényi's constant, the share an endless street fills once jammed. +const C = 0.7475979202534114; +const { min, max, floor, ceil, round, log, abs } = 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; +} + +// The street's own clock: t car arrivals have been tried per car length of +// street by the time it has come down tau rows. It runs slowly at the top +// and without end at row 127, so every street leaves the panel jammed. +// With 6-LED cars a street is on average 0.29 full at row 42 (t = 0.45), +// 0.54 at row 85 (t = 1.8) and 0.70 at row 120, and from there to the +// bottom the few cars that still fit squeeze in: the slow end of the +// filling, which on an endless street never stops. +const A = 0.9; +const clockAt = (tau) => (tau >= H - 1 ? Infinity : (A * tau) / (H - 1 - tau)); + +// The mean number of cars a jammed street holds. Parked anywhere, a street +// x car lengths long holds M(x), where M = 0 below 1 and +// M(x) = 1 + 2/(x - 1) times the integral of M from 0 to x - 1, since the +// first car splits the street into two streets of random length. That is +// solved here in steps of 1/2048 of a car: 7.2236 cars at x = 10, the +// 6-LED cars, a fill of 0.7224. Rényi's C (x + 1) - 1 agrees with it to +// 1e-5 from x = 6 up and is 0.0045 cars low at x = 4. On a grid of N +// points with k to a car the same split gives M(n) = 1 + 2/(n - k + 1) +// times the sum of M(j) for j <= n - k, summed exactly. +function renyi(x) { + const h = 1 / 2048, n = round(x / h), one = 2048; + if (n < one) return 0; + const m = new Float64Array(n + 1), area = new Float64Array(n + 1); + for (let i = one; i <= n; i++) { + // area[i] is the integral of M from 1 to i h by trapezoids, M being 0 + // below 1 and jumping to 1 there. + m[i] = i === one ? 1 : 1 + (2 / (i * h - 1)) * area[i - one]; + if (i > one) area[i] = area[i - 1] + (h * (m[i] + m[i - 1])) / 2; + } + return m[n]; +} +function predict(L, k) { + if (!k) return (renyi(S / L) * L) / S; + const N = floor((S * k) / L + 1e-9), m = new Float64Array(N + 1); + let sum = 0; + for (let n = 0; n <= N; n++) { + if (n >= k) sum += m[n - k]; + m[n] = n < k ? 0 : 1 + (2 * sum) / (n - k + 1); + } + return (m[N] * L) / S; +} + +function newStreet() { + return { pos: new Float32Array(MAXC), at: new Float32Array(MAXC), n: 0, L: 6, k: 0, F: 0, next: 0, last: 1 }; +} + +// Where a car can still stop: for every gap the start points that leave room +// for it, as a length in LEDs (anywhere) or a count of grid points (on a +// grid). With pick >= 0 it returns the start point that far into them. +// rate() turns the total into tries that succeed per unit of the street's +// clock: one try per car length of street, spread evenly. +function feasible(st, pick) { + const { pos, n, L, k } = st; + let total = 0, start = 0; + if (!k) { + for (let i = 0; i <= n; i++) { + const end = i < n ? pos[i] : S, room = end - start - L; + if (room > 0) { + if (pick >= 0 && pick < total + room) return start + (pick - total); + total += room; + } + if (i < n) start = pos[i] + L; + } + } else { + const d = L / k, N = floor((S * k) / L + 1e-9); + for (let i = 0; i <= n; i++) { + const end = i < n ? round(pos[i] / d) : N, room = end - start - k + 1; + if (room > 0) { + if (pick >= 0 && pick < total + room) return (start + floor(pick - total)) * d; + total += room; + } + if (i < n) start = round(pos[i] / d) + k; + } + } + return pick >= 0 ? -1 : total; +} + +const rate = (st) => (st.k ? st.F / st.k : st.F / st.L); + +function reset(st, L, k, pred) { + st.n = 0; + st.L = L; + st.k = k; + // The share parked when the last car is due to come, so the colours run + // from the first car to the last whatever the length. + st.last = max(0.05, pred - L / S); + st.F = feasible(st, -1); + st.next = -log(1 - Math.random()) / rate(st); +} + +// Run a street's clock on to t: each try that lands in a feasible stretch +// parks a car there, uniformly, and tries that land anywhere else are +// turned away and change nothing, so only the successes are drawn, at the +// rate of the feasible stretch. Each car keeps the share of the street that +// was already parked when it came. +function advance(st, t) { + while (st.F > 0 && st.next <= t) { + const x = feasible(st, Math.random() * st.F); + if (x < 0) break; + let i = st.n; + while (i > 0 && st.pos[i - 1] > x) (st.pos[i] = st.pos[i - 1]), (st.at[i] = st.at[i - 1]), i--; + st.pos[i] = x; + st.at[i] = (st.n * st.L) / S; + st.n++; + st.F = feasible(st, -1); + if (st.F > 0) st.next += -log(1 - Math.random()) / rate(st); + } +} + +export function setup(params) { + const streets = []; + for (let i = 0; i < H; i++) streets.push(newStreet()); + params.s = { + streets, head: 0, phase: 0.5, primed: false, + hue: 0, speed: 2, L: 6, k: 0, pred: predict(6, 0), + // The fills of the last 128 jammed streets of the current length and + // grid, as they leave the bottom. + outs: new Float32Array(H), outN: 0, outI: 0, + pack: -1, rgb: new Float32Array(W * H * 3), pal: new Float32Array(33 * 3), palHue: NaN, + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, kv = input.knobValues || [0, 2, 6, 0.06], d = cl(dt || 0, 0, 0.1); + P.hue = kv[0]; + P.speed = cl(kv[1], 0.2, 3); + const L = round(cl(kv[2], 6, 15)), g = cl(kv[3], 0, 1), k = g < 0.07 ? 0 : max(1, round(1 / g)); + if (L !== P.L || k !== P.k) { + P.L = L; + P.k = k; + P.pred = predict(L, k); + P.outN = P.outI = 0; + } + // The first frame fills the panel with streets at every stage, each run + // on to the clock of its row, so the picture starts whole. + if (!P.primed) { + for (let r = 0; r < H; r++) { + const st = P.streets[(P.head + r) % H]; + reset(st, L, k, P.pred); + advance(st, clockAt(r + P.phase)); + } + P.primed = true; + } + if (input.btnPressed && input.btnPressed[0] && P.pack < 0) P.pack = 0; + if (P.pack >= 0) { + // The tap: 0.6 s to pack, 3 s held, 0.6 s back; the streets wait. + P.pack += d; + if (P.pack >= 4.2) P.pack = -1; + return; + } + P.phase += d * P.speed * 2; + while (P.phase >= 1) { + P.phase -= 1; + // The bottom street leaves, jammed, and comes back empty at the top. + const bottom = P.streets[(P.head + H - 1) % H]; + if (bottom.L === P.L && bottom.k === P.k) { + P.outs[P.outI] = (bottom.n * bottom.L) / S; + P.outI = (P.outI + 1) % H; + P.outN = min(H, P.outN + 1); + } + P.head = (P.head + H - 1) % H; + reset(bottom, L, k, P.pred); + } + for (let r = 0; r < H; r++) advance(P.streets[(P.head + r) % H], clockAt(r + P.phase)); +} + +export function draw(display, params) { + const P = params.s; + if (!P || !P.primed) return; + const { rgb, pal } = P; + if (P.palHue !== P.hue) { + // 33 steps from the first car to come to the last: azure through + // violet and rose to gold, brighter as it goes, so the latecomers read + // first. Between orange and lime a dim car would read brown or olive, + // so there the dim end is lifted. + for (let i = 0; i <= 32; i++) { + const u = i / 32, h = P.hue + 0.58 + 0.55 * u, band = max(0, 1 - abs(h - floor(h) - 0.14) / 0.13); + const v = 0.6 + 0.4 * u; + hsv(h, 0.75 - 0.1 * u, max(v, v + (0.92 - v) * band), pal, i * 3); + } + P.palHue = P.hue; + } + rgb.fill(0); + const e = P.pack < 0 ? 0 : P.pack < 0.6 ? sm(P.pack / 0.6) : P.pack < 3.6 ? 1 : sm((4.2 - P.pack) / 0.6); + for (let r = 0; r < H; r++) { + const st = P.streets[(P.head + r) % H], L = st.L; + for (let i = 0; i < st.n; i++) { + // A car covers the LEDs whose centres fall inside it, less half an + // LED at each end: L - 1 LEDs and a dark one, so cars that touch + // still read as two. + const x = st.pos[i] + (i * L - st.pos[i]) * e, x0 = max(0, ceil(x)), x1 = min(S - 1, ceil(x + L - 1) - 1); + const c = min(32, round((st.at[i] / st.last) * 32)) * 3; + for (let px = x0; px <= x1; px++) { + const q = (r * W + px) * 3; + (rgb[q] = pal[c]), (rgb[q + 1] = pal[c + 1]), (rgb[q + 2] = pal[c + 2]); + } + } + } + const gold = []; + hsv(P.hue + 0.13, 0.7, 1, gold, 0); + // Packed, the gold line stands at the predicted fill across the streets. + if (e > 0.5) { + const gx = min(S - 1, floor(P.pred * S)); + for (let r = 0; r < H; r++) { + const q = (r * W + gx) * 3; + (rgb[q] = gold[0]), (rgb[q + 1] = gold[1]), (rgb[q + 2] = gold[2]); + } + } + // The gauge, three LEDs wide, from a fill of 0.65 at the bottom to 1 at + // the top: a bar to the mean of the last jammed streets, a white tick at + // Rényi's constant for an endless street and a gold one at the + // prediction for this one. + const rowOf = (f) => cl(round(H - 1 - ((f - 0.65) / 0.35) * (H - 1)), 0, H - 1); + if (P.outN) { + let s = 0; + for (let i = 0; i < P.outN; i++) s += P.outs[i]; + const top = rowOf(s / P.outN); + for (let y = top; y < H; y++) + for (let x = GX; x < W; x++) { + const q = (y * W + x) * 3; + (rgb[q] = 0.36), (rgb[q + 1] = 0.4), (rgb[q + 2] = 0.5); + } + } + const tick = (f, c) => { + const y = rowOf(f); + for (let x = GX - 1; x < W; x++) { + const q = (y * W + x) * 3; + (rgb[q] = c[0]), (rgb[q + 1] = c[1]), (rgb[q + 2] = c[2]); + } + }; + tick(C, [0.85, 0.85, 0.85]); + tick(P.pred, gold); + 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)); +} From 7dd7e255abacc17e93800835b41c683a6e54c91c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 17:06:17 +0000 Subject: [PATCH 3/9] incoming: add kari-culik and kolakoski (pattern round 35) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SW1UfDNBr6xKa7hi5BogQV --- incoming/pattern-study/patterns/kari-culik.js | 225 ++++++++++++++++++ incoming/pattern-study/patterns/kolakoski.js | 136 +++++++++++ 2 files changed, 361 insertions(+) create mode 100644 incoming/pattern-study/patterns/kari-culik.js create mode 100644 incoming/pattern-study/patterns/kolakoski.js diff --git a/incoming/pattern-study/patterns/kari-culik.js b/incoming/pattern-study/patterns/kari-culik.js new file mode 100644 index 000000000..6e7afa402 --- /dev/null +++ b/incoming/pattern-study/patterns/kari-culik.js @@ -0,0 +1,225 @@ +// ===== Patternflow pattern ===== +// Title: Kari-Culik Tiles +// Family: algo +// Idea: Kari's fourteen Wang tiles cover the panel without ever repeating: each row of tiles multiplies a real number x by 2 or by 2/3, its top edges spelling x as a Beatty sequence of digits and its bottom edges the product, the carries passing sideways from tile to tile, and every tile is drawn as four triangles so the matching edges join into diamonds, blue for digits and warm or green for carries, while the tiling scrolls down through new rows reached by running the multiplication backwards. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Aperiodic Wang tiles from Beatty sequences (Jarkko Kari, 1996; Karel Culik II, 1996), Kari's set of 14, pattern round 35 +// Round: 35 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Start=0..1, Size=8..16 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the digit diamonds are navy for 0, steel blue for 1 and ice for 2; the carries of the times-2 rows are lime for 0 and sea green for -1, and those of the times-2/3 rows run violet, rose, orange and gold for -1/3, 0, 1/3 and 2/3. +// Knob 2: Speed. How fast the tiling slides: down 1.5 LEDs a second per unit and sideways 0.5, so at 2 a new row of 8-LED tiles comes in at the top every 2.7 s, and at 0.2 every 27 s. +// Knob 3: Start. The number x of the top row when the knob was last turned, anywhere in [2/3, 2): at 0 it is the square root of 2, and every other setting is a different tiling by the same 14 tiles. +// Knob 4: Size. Tile width in LEDs, in even steps: 8 gives 8 columns of 16 rows, 16 gives 4 columns of 8. +// Buttons: K1 tap lifts the tiles apart for three seconds, a gap of a quarter tile, so each one stands alone with its four triangles, then sets them back edge to edge. + +// The rule behind the tiles. Write x by its Beatty digits +// a_n = floor((n + 1) x) - floor(n x), n over all the integers. A tile in a +// row that multiplies by q has a_n(x) on top, a_n(qx) underneath, the carry +// s_n = q floor(n x) - floor(n q x) on its left and s_(n+1) on its right, and +// q a_n + s_n = b_n + s_(n+1) holds for each one, so the row is a long +// multiplication done digit by digit. Rows stack with x_(i+1) = T(x_i), +// 2x on [2/3, 1) and 2x/3 on [1, 2); T maps [2/3, 2) onto itself one to one, +// so the rows can run either way. Kari states T on [1/2, 2], but [1/2, 2/3) +// has nothing above it, and a tiling that scrolls up for ever lives in +// [2/3, 2). No product of 2s and 2/3s is 1, so no row ever comes back. +// +// Counted over 5,400 rows from nine starts and 800 columns each, with exact +// arithmetic and the identity checked on every tile, the rows use exactly 14 +// tiles, Kari's set: 4 that multiply by 2 and 10 by 2/3. The same count over +// Culik's rows (3x on [1/3, 2/3), x/2 on [2/3, 2)) gives 12, one of them the +// tile with 0 on all four edges, which covers the plane by itself. An +// aperiodic set cannot hold such a tile, so Culik's 13 are not this plain +// count, and the picture uses Kari's. +// +// In the log of x, T is a turn by log 2 round a circle log 3 long, so the +// kinds of row follow a Sturmian word: a times-2 row (37%, log 1.5 / log 3 +// of them) is never next to another, and one or two times-2/3 rows lie +// between them. + +const W = 64, H = 128; +const { floor, min, round } = 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 Z = BigInt(0), ONE = BigInt(1), TWO = BigInt(2), THREE = BigInt(3), FOUR = BigInt(4); +// Start = 0 lands on the square root of 2. +const START = 0.5606601717798213; + +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; +} + +// Each row keeps x as an exact fraction p/q of BigInts. With floats the +// product drifts off the true 2x/3 in its last bit, and a test run that way +// turned up tiles with a carry of 1, which no row can have. The fractions +// grow by a bit a row: 10,000 rows up, seven hours at Speed 2, they are +// 10,053 bits long and one floor(n x) takes 9 microseconds. +function floorDiv(n, p, q) { + const v = BigInt(n) * p; + let d = v / q; + if (v < Z && d * q !== v) d -= ONE; + return Number(d); +} + +function reduce(r) { + while ((r.p & ONE) === Z && (r.q & ONE) === Z) (r.p >>= ONE), (r.q >>= ONE); + while (r.p % THREE === Z && r.q % THREE === Z) (r.p /= THREE), (r.q /= THREE); + // kind 0 multiplies by 2 (x < 1), kind 1 by 2/3. + r.kind = r.p < r.q ? 0 : 1; + r.F = new Map(); + return r; +} +const forward = (r) => reduce({ p: TWO * r.p, q: r.kind === 0 ? r.q : THREE * r.q }); +// The row above: y/2 when y is 4/3 or more (it came from a times-2 row), +// 3y/2 below that (from a times-2/3 row). +const backward = (r) => reduce({ p: THREE * r.p >= FOUR * r.q ? r.p : THREE * r.p, q: TWO * r.q }); + +// floor(n x) for a row, kept per column while the row is on the panel. +function fx(r, n) { + let v = r.F.get(n); + if (v === undefined) r.F.set(n, (v = floorDiv(n, r.p, r.q))); + return v; +} + +// A new tiling with x at row index `at`. x is a fraction over 2^52, so the +// top row's digits repeat only after 2^52 columns. +function build(P, u, at) { + const x = 2 / 3 + (4 / 3) * ((((u + START) % 1) + 1) % 1); + const K = 2 ** 52; + P.rows = new Map([[at, reduce({ p: BigInt(round(x * K)), q: BigInt(K) })]]); + P.lo = P.hi = at; + P.start = u; +} + +function row(P, i) { + while (i < P.lo) { + const r = backward(P.rows.get(P.lo)); + P.rows.set(--P.lo, r); + } + while (i > P.hi) { + const r = forward(P.rows.get(P.hi)); + P.rows.set(++P.hi, r); + } + return P.rows.get(i); +} + +// Which part of a D x D tile each pixel shows: 0 top, 1 right, 2 bottom, +// 3 left, or 4 for a pixel on a diagonal, left dark so the triangles stay +// apart. Two triangles that meet across a tile edge always carry the same +// label, so the dark diagonals are all that is seen of the tile grid. A +// lifted tile stands alone in its gap and needs no seams: its diagonal +// pixels go to the top and bottom triangles. +function triangles(D, seams) { + const t = new Uint8Array(D * D); + for (let v = 0; v < D; v++) + for (let u = 0; u < D; u++) { + const dT = v, dB = D - 1 - v, dL = u, dR = D - 1 - u, m = min(dT, dB, dL, dR); + const ties = (dT === m) + (dB === m) + (dL === m) + (dR === m); + t[v * D + u] = seams && ties > 1 ? 4 : dT === m ? 0 : dB === m ? 2 : dR === m ? 1 : 3; + } + return t; +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, size: 8, scroll: 0, pan: 0, lift: 0, liftT: -1, + pal: new Float32Array(9 * 3), palHue: NaN, tri: new Map(), lab: new Int8Array(4), + }; + build(params.s, 0, 0); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 8], d = cl(dt || 0, 0, 0.1); + P.hue = k[0]; + P.speed = cl(k[1], 0.2, 3); + P.size = 2 * round(cl(k[3], 8, 16) / 2); + // A turn of Start puts its x on the top row showing now, and the rest of + // the tiling grows from there. + const u = cl(k[2], 0, 1); + if (u !== P.start) build(P, u, floor(-floor(P.scroll) / P.size)); + P.scroll += d * P.speed * 1.5; + P.pan += d * P.speed * 0.5; + if (input.btnPressed && input.btnPressed[0]) P.liftT = 0; + if (P.liftT >= 0) { + P.liftT += d; + const t = P.liftT; + P.lift = t < 0.5 ? sm(t / 0.5) : t < 3.5 ? 1 : sm((4 - t) / 0.5); + if (t >= 4) (P.liftT = -1), (P.lift = 0); + } +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const S = P.size, pal = P.pal, lab = P.lab; + if (P.palHue !== P.hue) { + // Labels 0-2 are the digits, a ramp in one blue; 3-4 the carries 0 and + // -1 of the times-2 rows; 5-8 the carries -1/3, 0, 1/3, 2/3 of the + // times-2/3 rows. Where the hue knob turns the blues to red, orange or + // yellow the dark digits are laid lighter, as a dim orange reads brown. + const c = [ + [0.62, 0.85, 0.45], [0.58, 0.62, 0.78], [0.53, 0.22, 1], + [0.25, 0.8, 1], [0.45, 0.7, 0.9], + [0.78, 0.7, 1], [0.92, 0.62, 1], [0.04, 0.72, 1], [0.12, 0.6, 1], + ]; + for (let j = 0; j < 9; j++) { + let [h, s, v] = c[j]; + h += P.hue; + const hh = h - floor(h) > 0.9 ? h - floor(h) - 1 : h - floor(h), warm = sm((hh + 0.04) / 0.08) * sm((0.24 - hh) / 0.06); + if (j < 2) (v += (0.85 - v) * warm), (s *= 1 - 0.25 * warm); + hsv(h, s, v, pal, j * 3); + } + P.palHue = P.hue; + } + // The lift opens a gap of up to a quarter tile, in whole LEDs, and draws + // each tile that much smaller in the middle of its cell. + const gap = round(P.lift * (S / 4)), D = S - gap, off = gap >> 1; + const key = 2 * D + (gap === 0 ? 1 : 0); + let tri = P.tri.get(key); + if (!tri) P.tri.set(key, (tri = triangles(D, gap === 0))); + const sy = floor(P.scroll), sx = floor(P.pan); + const i0 = floor(-sy / S), i1 = floor((H - 1 - sy) / S); + const n0 = floor(sx / S), n1 = floor((W - 1 + sx) / S); + for (let i = i0; i <= i1; i++) { + const r = row(P, i), r2 = row(P, i + 1); + for (let n = n0; n <= n1; n++) { + const f0 = fx(r, n), f1 = fx(r, n + 1), g0 = fx(r2, n), g1 = fx(r2, n + 1); + lab[0] = f1 - f0; + lab[2] = g1 - g0; + // Left and right carries: s = 2 floor(nx) - floor(2nx) is 0 or -1; + // 3s = 2 floor(nx) - 3 floor(2nx/3) is -1, 0, 1 or 2. + if (r.kind === 0) (lab[3] = 3 + g0 - 2 * f0), (lab[1] = 3 + g1 - 2 * f1); + else (lab[3] = 6 + 2 * f0 - 3 * g0), (lab[1] = 6 + 2 * f1 - 3 * g1); + const X0 = n * S - sx, Y0 = i * S + sy; + for (let v = 0; v < S; v++) { + const Y = Y0 + v; + if (Y < 0 || Y >= H) continue; + const tv = v - off; + for (let u = 0; u < S; u++) { + const X = X0 + u; + if (X < 0 || X >= W) continue; + const tu = u - off; + const t = tv < 0 || tu < 0 || tv >= D || tu >= D ? 4 : tri[tv * D + tu]; + if (t === 4) { + display.setPixel(X, Y, 0, 0, 0); + continue; + } + const o = lab[t] * 3; + display.setPixel(X, Y, pal[o] * 255, pal[o + 1] * 255, pal[o + 2] * 255); + } + } + } + } + // Rows that have slid off the bottom are let go; the panel only ever + // needs the rows from i0 down to i1 + 1. + while (P.hi > i1 + 2) P.rows.delete(P.hi--); +} diff --git a/incoming/pattern-study/patterns/kolakoski.js b/incoming/pattern-study/patterns/kolakoski.js new file mode 100644 index 000000000..bab6b0e50 --- /dev/null +++ b/incoming/pattern-study/patterns/kolakoski.js @@ -0,0 +1,136 @@ +// ===== Patternflow pattern ===== +// Title: Kolakoski +// Family: number +// Idea: The Kolakoski sequence of 1s and 2s, the one that is its own run-length code, hangs as an icicle of ten bands: the bottom band is 64 consecutive terms one LED each, gold for 1 and violet for 2, and every band above holds the terms whose runs make the band below, each term as wide as the run it describes, so each band is the same sequence again with two thirds as many terms, the whole tree slides left one term at a time, and a slim gauge along the top shows how far the 1s so far are ahead of the 2s. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Kolakoski sequence (William Kolakoski, 1965), pattern round 35 +// Round: 35 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Bands=10..16, Falloff=-1..1 +// +// Knob 1: Hue. Turns both colours round the wheel together: at 0 the 1s are gold and the 2s violet, each band pale at its top and full at its foot. +// Knob 2: Speed. Terms a second that the bottom band slides left: 1.5 per unit, so 3 at 2 and the 64 LEDs are new every 21 s; at 0.2 one term every 3.3 s. +// Knob 3: Bands. How many generations of the tree are shown, from 10 to 16. Ten bands at the start reach from terms 3,000 to 3,063 at the bottom up to terms 77 to 79 at the top; 16 reach term 7, where one term covers more than the panel. +// Knob 4: Falloff. Band heights: at 0 all bands are equal, about 12 rows for ten; toward 1 each band is 1.3 times the height of the one below it, so the wide terms at the top get tall bands, and toward -1 the other way round. +// Buttons: K1 tap jumps the window 100,000 terms on down the sequence, to 103,000 on the first tap, wrapping round before the million terms run out. + +// Term i of the sequence is the length of run i: 1, 2, 2, 1, 1, 2, 1 is a +// run of one 1, one of two 2s, two 1s, one 2, one 1, ... and the runs +// alternate 1s and 2s. So term k has K[k] children, the terms of run k, +// and up[j] is the run that term j sits in: its parent. Term 0 and term 1 +// are their own parents, and every other term climbs to term 1, from term +// 3,000 in 19 steps, from term 100,000 in 27. +// +// Rufus Oldenburger described it first in 1939; Kolakoski asked about it in +// the American Mathematical Monthly in 1965. Keane's question, whether the +// share of 1s tends to 1/2, is still open. Counted here: 49,972 1s in the +// first 100,000 terms, 499,986 in the first million, and the 1s never run +// more than 66 ahead of the 2s or behind them in all that way. + +const W = 64, H = 128, N = 1 << 20, W0 = 3000, JUMP = 100000, TOP = 3; +const { floor, max, round, pow } = 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; +} + +export function setup(params) { + // 2^20 terms, written by reading the sequence itself two terms behind the + // pen: 28 ms. + const K = new Uint8Array(N); + K[0] = 1; + K[1] = 2; + K[2] = 2; + for (let len = 3, read = 2, val = 1; len < N; val = 3 - val) + for (let c = 0, r = K[read++]; c < r && len < N; c++) K[len++] = val; + const up = new Int32Array(N), ones = new Int32Array(N + 1); + for (let k = 0, pos = 0; pos < N; k++) for (let c = 0; c < K[k] && pos < N; c++) up[pos++] = k; + for (let i = 0; i < N; i++) ones[i + 1] = ones[i] + (K[i] === 1 ? 1 : 0); + params.s = { + K, up, ones, w: W0, acc: 0, hue: 0, speed: 2, bands: 10, fall: 0.06, + pal: new Float32Array(12), palHue: NaN, anc: new Int32Array(16 * W), + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 10, 0.06], d = cl(dt || 0, 0, 0.1); + P.hue = k[0]; + P.speed = cl(k[1], 0.2, 3); + P.bands = round(cl(k[2], 10, 16)); + P.fall = cl(k[3], -1, 1); + // Whole terms only: a window between two terms would smear every band. + P.acc += d * P.speed * 1.5; + while (P.acc >= 1) (P.acc -= 1), P.w++; + if (input.btnPressed && input.btnPressed[0]) P.w += JUMP; + if (P.w + W >= N) P.w = W0 + ((P.w - W0) % (N - W - W0)); +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const { K, up, anc, pal } = P; + if (P.palHue !== P.hue) { + // Pale and full gold for the 1s, pale and deep violet for the 2s: each + // band runs from the first at its top row to the second at its foot. + // The gold keeps its brightness all the way down, as a darker gold + // reads olive, and where the knob turns the violet to red, orange or + // yellow it is lifted the same way. + const h2 = P.hue + 0.72 - floor(P.hue + 0.72), hw = h2 > 0.9 ? h2 - 1 : h2, warm = cl((hw + 0.06) / 0.08, 0, 1) * cl((0.32 - hw) / 0.08, 0, 1); + hsv(P.hue + 0.11, 0.28, 1, pal, 0); + hsv(P.hue + 0.11, 0.7, 1, pal, 3); + hsv(h2, 0.38, 0.8 + 0.15 * warm, pal, 6); + hsv(h2, 0.7, 0.56 + 0.36 * warm, pal, 9); + P.palHue = P.hue; + } + // anc[b W + x] is the ancestor b generations up of the term under column + // x. A band's term boundaries fall where this changes, so each term is + // exactly as wide as its descendants in the window. + const B = P.bands; + for (let x = 0; x < W; x++) + for (let b = 0, j = P.w + x; b < B; b++, j = up[j]) anc[b * W + x] = j; + // Band heights grow by `ratio` a band going up, with a dark row between + // bands; the top band takes what rounding leaves. + const avail = H - TOP - (B - 1), ratio = pow(1.3, P.fall); + let sum = 0; + for (let b = 0; b < B; b++) sum += pow(ratio, b); + for (let b = 0, y = H; b < B; b++) { + const hgt = b === B - 1 ? y - TOP : max(3, round((avail * pow(ratio, b)) / sum)), y0 = y - hgt, row = b * W; + for (let x = 0; x < W; x++) { + const a = anc[row + x], v = K[a], o = (v - 1) * 6; + // Neighbours of different value part by colour. Two of the same, the + // two terms of one run, get a dark seam on the first column of the + // right one when it is two LEDs or more; two 1-LED terms run together + // and the band above tells them apart. + let g = 255; + if (x > 0 && anc[row + x - 1] !== a && K[anc[row + x - 1]] === v && x + 1 < W && anc[row + x + 1] === a) g = 40; + for (let yy = y0; yy < y; yy++) { + const t = hgt > 1 ? (yy - y0) / (hgt - 1) : 1, u = 1 - t; + display.setPixel(x, yy, (pal[o] * u + pal[o + 3] * t) * g, (pal[o + 1] * u + pal[o + 4] * t) * g, (pal[o + 2] * u + pal[o + 5] * t) * g); + } + } + if (y0 - 1 >= TOP) for (let x = 0; x < W; x++) display.setPixel(x, y0 - 1, 0, 0, 0); + y = y0 - 1; + } + // The gauge: 1s minus 2s among the first w + 64 terms, half an LED a + // term from the middle, toward the gold side when the 1s are ahead. The + // track is dim violet on the left, dim gold on the right. + const n = P.w + W, lead = 2 * P.ones[n] - n, mx = cl(32 + round(lead / 2), 0, W - 1); + for (let x = 0; x < W; x++) { + const o = x < 32 ? 9 : 3, g = x === 32 ? 0 : 70; + for (let yy = 0; yy < 2; yy++) { + if (x === mx) display.setPixel(x, yy, 255, 255, 255); + else if (g === 0) display.setPixel(x, yy, 110, 110, 110); + else display.setPixel(x, yy, pal[o] * g, pal[o + 1] * g, pal[o + 2] * g); + } + display.setPixel(x, 2, 0, 0, 0); + } +} From 790a7df71030ecf90f42a4cce0b0ec5f243699ec Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 17:13:07 +0000 Subject: [PATCH 4/9] incoming: add bath-fizz and toner-pad (pattern round 35) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SW1UfDNBr6xKa7hi5BogQV --- incoming/pattern-study/patterns/bath-fizz.js | 364 +++++++++++++++++++ incoming/pattern-study/patterns/toner-pad.js | 353 ++++++++++++++++++ 2 files changed, 717 insertions(+) create mode 100644 incoming/pattern-study/patterns/bath-fizz.js create mode 100644 incoming/pattern-study/patterns/toner-pad.js diff --git a/incoming/pattern-study/patterns/bath-fizz.js b/incoming/pattern-study/patterns/bath-fizz.js new file mode 100644 index 000000000..218c135bb --- /dev/null +++ b/incoming/pattern-study/patterns/bath-fizz.js @@ -0,0 +1,364 @@ +// ===== Patternflow pattern ===== +// Title: Bath Fizz +// Family: cosmetic +// Idea: Seen through bath water, fizzing powder somewhere below the panel sends up clouds of saturated pigment in three colours that billow up the 128 rows and roll over into mushroom caps, running into each other at their edges, while streams of fine bubbles rise through them and drag the colour up with them. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 35, after underwater film of a fizzing bath powder clouding the water +// Round: 35 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Fizz=0..1, Mix=0..1 +// +// Knob 1: Hue. Turns the three pigments and the deep water round the whole wheel together, opening on pink, violet and aqua. A pigment that lands between orange and lime is laid paler and brighter, peach or butter, and thins to a pale haze, and water that would go brown turns deep indigo. +// Knob 2: Speed. How fast the water moves: at 2 a bubble crosses the 128 rows in about four seconds and a cloud's colour thins away over five; at 0.2 everything runs ten times slower. +// Knob 3: Fizz. How hard the powder fizzes: at 0 a gentle chug of about 60 bubbles a second and a puff from each colour every 1.6 s, the first cloud reaching the top 20 rows after 7.6 s; at 1 about 360 bubbles a second, a puff every 0.8 s, and the first cloud is up there in 2.4 s. +// Knob 4: Mix. How far the colours run into each other: at 0 the strongest pigment takes each LED up to a seam a LED or two wide; at 1 the pigments spread nine times as fast and blend where they overlap, so pink and aqua meet in lilac. +// Buttons: K1 tap sets off a fresh burst: all three colours at once across the bottom and a rush of bubbles. + +const W = 64, H = 128, N = W * H; +// The water is worked out on a grid of 2x2 LEDs; the pigment is carried at +// full resolution through it, so the clouds keep a fine edge. +const GW = 32, GH = 64, GN = GW * GH; +const MAXB = 1200; +// Deep indigo, given at full brightness like the water colour it replaces. +const INDIGO = [0.34, 0.3, 1]; +const { min, max, floor, sqrt, sin, cos, exp, abs, pow, 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 rnd = () => Math.random(); + +function hsv(h, s, v, out, o) { + const f = (n) => { + const k = (n + (h - floor(h)) * 6) % 6; + return v - v * s * max(0, min(k, 4 - k, 1)); + }; + out[o] = f(5); + out[o + 1] = f(3); + out[o + 2] = f(1); +} + +export function setup(params) { + const F = (n) => new Float32Array(n); + params.s = { + U: F(GN), V: F(GN), U0: F(GN), V0: F(GN), P: F(GN), DV: F(GN), L: F(GN), L0: F(GN), CU: F(GN), FB: F(GN), + A: [F(N), F(N), F(N)], TA: F(N), PI0: new Int32Array(N), PF: F(2 * N), BB: F(N), BT: F(N), DD: F(N), + bx: F(MAXB), by: F(MAXB), bs: F(MAXB), bp: F(MAXB), nb: 0, + // Three places along the bottom where the powder fizzes, one per colour. + src: [0, 1, 2].map((c) => ({ c, ph: (2 * PI * c) / 3 + rnd(), w: [0.11, 0.07, 0.09][c], next: 0.2 + 0.5 * c, puff: 0, bacc: 0 })), + T: 0, RS: F(W), RC: F(W), PW: F(256), pe: 0, hue: 0, speed: 2, fizz: 0, mix: 0.06, started: false, col: F(20), key: NaN, + }; +} + +// Bilinear read of a grid field at grid coordinates. +function samp(F, x, y) { + x = cl(x, 0, GW - 1.001); + y = cl(y, 0, GH - 1.001); + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy, i = iy * GW + ix; + return (F[i] * (1 - fx) + F[i + 1] * fx) * (1 - fy) + (F[i + GW] * (1 - fx) + F[i + GW + 1] * fx) * fy; +} + +// A soft round blob added to a full-resolution field. +function blob(A, cx, cy, r, w) { + const x0 = max(0, floor(cx - 2 * r)), x1 = min(W - 1, floor(cx + 2 * r)), y0 = max(0, floor(cy - 2 * r)), y1 = min(H - 1, floor(cy + 2 * r)); + const k = 1 / (r * r); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const dx = x + 0.5 - cx, dy = y + 0.5 - cy; + A[y * W + x] += w * exp(-(dx * dx + dy * dy) * k); + } +} + +function gblob(G, cx, cy, r, w) { + // The same on the water's grid, given in LEDs. + const gx = cx / 2, gy = cy / 2, gr = r / 2, k = 1 / (gr * gr); + for (let y = max(0, floor(gy - 2 * gr)); y <= min(GH - 1, floor(gy + 2 * gr)); y++) + for (let x = max(0, floor(gx - 2 * gr)); x <= min(GW - 1, floor(gx + 2 * gr)); x++) { + const dx = x + 0.5 - gx, dy = y + 0.5 - gy; + G[y * GW + x] += w * exp(-(dx * dx + dy * dy) * k); + } +} + +function bubbles(S, x, n, spread) { + for (let j = 0; j < n && S.nb < MAXB; j++) { + const i = S.nb++; + S.bx[i] = x + (rnd() + rnd() - 1) * spread; + S.by[i] = H + 1 + rnd() * 4; + S.bs[i] = rnd(); + S.bp[i] = rnd() * 2 * PI; + } +} + +// A burst from one place: pigment and lift at the bottom, and its bubbles. +function puff(S, s, x, k) { + blob(S.A[s.c], x, H - 2, 5 + 2 * k, 5.4 * k); + gblob(S.L, x, H - 3, 6 + 2 * k, 2.2 * k); +} + +function burst(S) { + for (const s of S.src) { + const x = 6 + rnd() * 52; + puff(S, s, x, 2.2); + bubbles(S, x, 70, 5); + } +} + +function fluid(S, st) { + const U = S.U, V = S.V, U0 = S.U0, V0 = S.V0, L = S.L, L0 = S.L0, P = S.P, DV = S.DV, CU = S.CU, FB = S.FB, f = S.fizz; + // Forces: the lift the fizz leaves in the water, and the drag of each + // rising bubble on the water round it. + const buoy = 34 * (1 + 0.9 * f); + for (let i = 0; i < GN; i++) { + V[i] -= st * (buoy * L[i] + 7 * FB[i]); + FB[i] = 0; + } + // Vorticity confinement keeps the small curls that roll a cloud's edge over. + for (let y = 1; y < GH - 1; y++) + for (let x = 1; x < GW - 1; x++) { + const i = y * GW + x; + CU[i] = 0.5 * (V[i + 1] - V[i - 1] - U[i + GW] + U[i - GW]); + } + const eps = 2.2; + for (let y = 2; y < GH - 2; y++) + for (let x = 2; x < GW - 2; x++) { + const i = y * GW + x; + const nx = 0.5 * (abs(CU[i + 1]) - abs(CU[i - 1])), ny = 0.5 * (abs(CU[i + GW]) - abs(CU[i - GW])), nl = sqrt(nx * nx + ny * ny) + 1e-5; + U[i] += st * eps * (ny / nl) * CU[i]; + V[i] -= st * eps * (nx / nl) * CU[i]; + } + // Carry the water along itself. + U0.set(U); + V0.set(V); + L0.set(L); + const fade = exp(-st / 2.4); + for (let y = 0, i = 0; y < GH; y++) + for (let x = 0; x < GW; x++, i++) { + const px = x - st * U0[i], py = y - st * V0[i]; + U[i] = samp(U0, px, py) * 0.995; + V[i] = samp(V0, px, py) * 0.995; + L[i] = samp(L0, px, py) * fade; + } + // Walls all round: the sides of the bath, its floor below the panel and the + // surface above. + for (let y = 0; y < GH; y++) { + U[y * GW] = U[y * GW + GW - 1] = 0; + } + for (let x = 0; x < GW; x++) { + V[x] = V[(GH - 1) * GW + x] = 0; + } + // Make it incompressible: what rises in the middle comes down at the sides. + for (let y = 0, i = 0; y < GH; y++) + for (let x = 0; x < GW; x++, i++) { + const ur = x < GW - 1 ? U[i + 1] : 0, ul = x > 0 ? U[i - 1] : 0, vd = y < GH - 1 ? V[i + GW] : 0, vu = y > 0 ? V[i - GW] : 0; + DV[i] = 0.5 * (ur - ul + vd - vu); + } + for (let it = 0; it < 24; it++) + for (let y = 0, i = 0; y < GH; y++) + for (let x = 0; x < GW; x++, i++) { + const p = P[i]; + const s = (x > 0 ? P[i - 1] : p) + (x < GW - 1 ? P[i + 1] : p) + (y > 0 ? P[i - GW] : p) + (y < GH - 1 ? P[i + GW] : p); + P[i] = p + 1.6 * ((s - DV[i]) / 4 - p); + } + for (let y = 0, i = 0; y < GH; y++) + for (let x = 0; x < GW; x++, i++) { + const p = P[i]; + U[i] -= 0.5 * ((x < GW - 1 ? P[i + 1] : p) - (x > 0 ? P[i - 1] : p)); + V[i] -= 0.5 * ((y < GH - 1 ? P[i + GW] : p) - (y > 0 ? P[i - GW] : p)); + } +} + +function pigment(S, st) { + const U = S.U, V = S.V, TA = S.TA, mix = S.mix, PI0 = S.PI0, PF = S.PF; + // The pigment is carried by the water, read at each LED's own place so it + // stays half a cell finer than the water. The trace back is worked out once + // for all three pigments. + for (let y = 0, i = 0; y < H; y++) + for (let x = 0; x < W; x++, i++) { + const gx = (x + 0.5) / 2 - 0.5, gy = (y + 0.5) / 2 - 0.5; + const px = cl(x - 2 * st * samp(U, gx, gy), 0, W - 1.001), py = cl(y - 2 * st * samp(V, gx, gy), 0, H - 1.001); + const ix = floor(px), iy = floor(py); + PI0[i] = iy * W + ix; + PF[2 * i] = px - ix; + PF[2 * i + 1] = py - iy; + } + // Then it spreads a little, more the higher Mix, and slowly thins into the bath. + const kap = cl(st * (0.6 + 5 * mix), 0, 0.24), fade = exp(-st / 5); + for (const A of S.A) { + TA.set(A); + for (let i = 0; i < N; i++) { + const j = PI0[i], fx = PF[2 * i], fy = PF[2 * i + 1]; + A[i] = (TA[j] * (1 - fx) + TA[j + 1] * fx) * (1 - fy) + (TA[j + W] * (1 - fx) + TA[j + W + 1] * fx) * fy; + } + TA.set(A); + for (let y = 0, i = 0; y < H; y++) + for (let x = 0; x < W; x++, i++) { + const c = TA[i]; + const l = (x > 0 ? TA[i - 1] : c) + (x < W - 1 ? TA[i + 1] : c) + (y > 0 ? TA[i - W] : c) + (y < H - 1 ? TA[i + W] : c) - 4 * c; + A[i] = (c + kap * l) * fade; + } + } +} + +function step(S, st) { + S.T += st; + const f = S.fizz; + for (const s of S.src) { + // Each place wanders along the bottom as the powder tumbles, each at its + // own pace, so now and then two pass and their clouds go up together. + s.ph += st * s.w; + const x = 32 + 22 * sin(s.ph) + 4 * sin(2.7 * s.ph + s.c); + s.next -= st; + if (s.next <= 0) { + s.puff = 0.8 + 0.6 * rnd(); + s.next = (1.6 - 0.8 * f) * (0.6 + 0.8 * rnd()); + } + s.puff *= exp(-st / 0.3); + const k = st * (0.5 + 0.4 * f + 4 * s.puff); + puff(S, s, x, k); + s.bacc += st * (30 + 110 * f) * (0.5 + s.puff); + const nb = floor(s.bacc); + s.bacc -= nb; + bubbles(S, x, nb, 1.5 + 1.5 * s.puff); + } + // Bubbles: small ones rise slower and zig-zag more, all ride the water + // sideways, and each pulls the water up with it. + const U = S.U, V = S.V, FB = S.FB; + let k = 0; + for (let i = 0; i < S.nb; i++) { + let x = S.bx[i], y = S.by[i]; + const s = S.bs[i], gx = x / 2 - 0.5, gy = y / 2 - 0.5; + const ph = (S.bp[i] += st * (7 + 5 * (1 - s))); + const rise = (24 + 20 * s) * (0.85 + 0.5 * f); + x += st * (1.2 * samp(U, gx, gy) + (2 + 3 * (1 - s)) * cos(ph)); + y += st * (2 * samp(V, gx, gy) * 0.6 - rise); + if (y < -1 || x < -1 || x > W + 1) continue; + const ci = cl(floor(y / 2), 0, GH - 1) * GW + cl(floor(x / 2), 0, GW - 1); + FB[ci] += 0.5 + 0.5 * s; + S.bx[k] = x; + S.by[k] = y; + S.bs[k] = s; + S.bp[k] = S.bp[i]; + k++; + } + S.nb = k; + fluid(S, st); + pigment(S, st); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 0.06]; + const d = cl(dt || 0, 0, 0.1); + S.hue = kv[0] || 0; + S.speed = cl(kv[1], 0.2, 3); + S.fizz = cl(kv[2] || 0, 0, 1); + S.mix = cl(kv[3], 0, 1); + if (!S.started) { + S.started = true; + // Open with the water already clouding: six and a half seconds of fizz, + // in coarse steps. + for (let i = 0; i < 80; i++) step(S, 1 / 12); + } + if (input.btnPressed && input.btnPressed[0]) burst(S); + step(S, (d * S.speed) / 2); +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.started) return; + const col = S.col, mix = S.mix, A0 = S.A[0], A1 = S.A[1], A2 = S.A[2], T = S.T; + // Palette: three pigments spread round the wheel from the hue, and deep + // water. A pigment from orange to lime is paled and its cloud thickens + // faster, because those hues at half brightness read brown and olive. + const h0 = S.hue + 0.92, key = h0 + 3 * mix; + if (S.key !== key) { + const hs = [h0, h0 - 0.17, h0 - 0.42]; + for (let c = 0; c < 3; c++) { + const hf = hs[c] - floor(hs[c]), wm = sm((hf - 0.02) / 0.05) * (1 - sm((hf - 0.24) / 0.06)); + hsv(hs[c], 0.95 - 0.45 * wm, 1, col, c * 3); + col[9 + c] = 4.5 * (1 + wm); + col[16 + c] = wm; + } + // Deep water opposite the pigments; where that lands on orange or gold it + // would be brown, so there the water turns deep indigo. + const hw = h0 - 0.32, hf = hw - floor(hw), wm = sm((hf + 0.02) / 0.06) * (1 - sm((hf - 0.27) / 0.08)); + hsv(hw, 0.8, 1, col, 12); + for (let c = 0; c < 3; c++) col[12 + c] += (INDIGO[c] - col[12 + c]) * wm; + S.key = key; + } + // Bubbles: splatted, then a 3x3 blur so each is a soft bump two or three + // LEDs across rather than a hard dot. + const BB = S.BB, BT = S.BT; + BB.fill(0); + for (let i = 0; i < S.nb; i++) { + const x = S.bx[i] - 0.5, y = S.by[i] - 0.5, ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy, w = 0.7 + 0.5 * S.bs[i]; + if (ix < 0 || iy < 0 || ix >= W - 1 || iy >= H - 1) continue; + const p = iy * W + ix; + BB[p] += w * (1 - fx) * (1 - fy); + BB[p + 1] += w * fx * (1 - fy); + BB[p + W] += w * (1 - fx) * fy; + BB[p + W + 1] += w * fx * fy; + } + for (let y = 0, i = 0; y < H; y++) + for (let x = 0; x < W; x++, i++) BT[i] = (x > 0 ? BB[i - 1] : 0) + 2 * BB[i] + (x < W - 1 ? BB[i + 1] : 0); + for (let y = 0, i = 0; y < H; y++) + for (let x = 0; x < W; x++, i++) BB[i] = ((y > 0 ? BT[i - W] : 0) + 2 * BT[i] + (y < H - 1 ? BT[i + W] : 0)) / 16; + // How sharply the strongest pigment takes a LED: at Mix 0 it takes it up to + // a narrow seam, at 1 the pigments add. + const pe = 1 + 7 * (1 - mix) * (1 - mix), PW = S.PW; + if (S.pe !== pe) { + for (let j = 0; j < 256; j++) PW[j] = pow((j + 0.5) / 255, pe); + PW[255] = 1; + S.pe = pe; + } + const DD = S.DD; + for (let i = 0; i < N; i++) DD[i] = A0[i] + A1[i] + A2[i]; + // Slow shafts of light slant down through the water from the surface: + // sin(a(x) + 0.08 y), split so each row and column is worked out once. + const RS = S.RS, RC = S.RC; + for (let x = 0; x < W; x++) { + const a = 0.16 * x + 0.5 * T + 1.2 * sin(0.05 * x - 0.3 * T); + RS[x] = sin(a); + RC[x] = cos(a); + } + for (let y = 0, i = 0; y < H; y++) { + // The water: light from the surface, falling off towards the floor. + const wl = 0.1 + 0.08 * exp(-y / 40), wr = 0.25 * wl * exp(-y / 70), sy = sin(0.08 * y), cy = cos(0.08 * y); + for (let x = 0; x < W; x++, i++) { + const a0 = A0[i], a1 = A1[i], a2 = A2[i], D = DD[i]; + const lw = wl + wr * (1 + RS[x] * cy + RC[x] * sy); + let r = col[12] * lw, g = col[13] * lw, b = col[14] * lw; + if (D > 0.002) { + // Each pigment's weight is its share of the strongest, to the power pe. + const am = a0 > a1 ? (a0 > a2 ? a0 : a2) : a1 > a2 ? a1 : a2, iv = 1 / am; + const q0 = a0 * iv, q1 = a1 * iv, q2 = a2 * iv; + const w0 = PW[(q0 * 255) | 0], w1 = PW[(q1 * 255) | 0], w2 = PW[(q2 * 255) | 0]; + const ws = w0 + w1 + w2; + let cr = (w0 * col[0] + w1 * col[3] + w2 * col[6]) / ws, cg = (w0 * col[1] + w1 * col[4] + w2 * col[7]) / ws, cb = (w0 * col[2] + w1 * col[5] + w2 * col[8]) / ws; + // Opacity goes with the cube of the pigment, e^3 / (e^3 + 1): half at + // e = 1, nine tenths at e = 2 and nearly clear below a half, so old + // colour thins away and fresh clouds stand out on the water. + const e = a0 * col[9] + a1 * col[10] + a2 * col[11], e3 = e * e * e, op = e3 / (e3 + 1); + // A thinning cloud of orange, gold or lime would dim through brown and + // olive, so as it thins it pales towards a grey haze instead. + const wp = ((w0 * col[16] + w1 * col[17] + w2 * col[18]) / ws) * (1 - op * op); + cr += (0.8 - cr) * wp; + cg += (0.8 - cg) * wp; + cb += (0.8 - cb) * wp; + // Light from above catches the top of each billow and the cloud is + // brighter where it is thick. + const up = y > 1 ? D - DD[i - 2 * W] : 0; + const lv = cl(0.76 + 0.25 * (1 - exp(-2 * D)) + 0.35 * up, 0.5, 1); + r += (cr * lv - r) * op; + g += (cg * lv - g) * op; + b += (cb * lv - b) * op; + } + const bb = min(0.55, BB[i]); + r += bb * (1 - 0.4 * r); + g += bb * (1 - 0.4 * g); + b += bb * (1 - 0.4 * b); + display.setPixel(x, y, 255 * min(1, r), 255 * min(1, g), 255 * min(1, b)); + } + } +} diff --git a/incoming/pattern-study/patterns/toner-pad.js b/incoming/pattern-study/patterns/toner-pad.js new file mode 100644 index 000000000..9a0e6b347 --- /dev/null +++ b/incoming/pattern-study/patterns/toner-pad.js @@ -0,0 +1,353 @@ +// ===== Patternflow pattern ===== +// Title: Toner Pad +// Family: cosmetic +// Idea: A close-up of a cotton pad, a white nonwoven web of fine soft fibres crossing at random, where drops of tinted toner land one after another and wick outward, the wet front running ahead along the fibres in fingers while the soaked cotton turns translucent and glows with the toner's tint from the light behind, its fibres darker against it, until each patch dries back from its edges to white. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 35, after a cotton pad soaking up toner +// Round: 35 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Toner=0..1, Fibre=0..1 +// +// Knob 1: Hue. The toner's tint, all the way round the wheel from rose at 0; the dry pad keeps only a breath of it. From coral round through orange and gold to lime the tint is laid paler and brighter, peach or butter, and the fibres in it go to a cool grey, because dark there reads brown and olive. +// Knob 2: Speed. How fast it all runs: at 2 a drop lands every 3.2 s, its front is most of the way out in a second and a half, holds, and the patch dries back over 3.6 s; at 0.2 ten times slower. +// Knob 3: Toner. How much toner each drop carries: at 0 it wets about 1800 LEDs, its fingers reaching about 33 LEDs from where it fell; at 1 three times as much, 5400 LEDs across the whole width, with fingers out to about 65. +// Knob 4: Fibre. How coarse the pad is: at 0 a fine web of about 400 thin strands, at 1 an open one of 120 thick ones; the coarser the web, the fewer strands the front runs along and the straighter its edges. +// Buttons: K1 tap lets a drop fall at once, away from the patches still wet. + +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); +const sm = (v) => ((v = cl(v, 0, 1)), v * v * (3 - 2 * v)); +const rnd = () => Math.random(); +// Drops on the pad at once: a fifth, from quick taps, takes the place of the oldest. +const MAXD = 4; + +function hsv(h, s, v, out, o) { + const f = (n) => { + const k = (n + (h - floor(h)) * 6) % 6; + return v - v * s * max(0, min(k, 4 - k, 1)); + }; + out[o] = f(5); + out[o + 1] = f(3); + out[o + 2] = f(1); +} + +export function setup(params) { + const F = (n) => new Float32Array(n); + params.s = { + F: F(N), F2: F(N), tmp: F(N), CE: F(N), CS: F(N), CSE: F(N), CSW: F(N), J: F(N), + drops: [], pool: [F(N), F(N), F(N), F(N), F(N)], + // The heap's keys are 32-bit like the reach they are checked against, so a + // key is never read as stale because it was rounded on the way in. + hk: F(8 * N + 16), hv: new Int32Array(8 * N + 16), + T: 0, next: 0, hue: 0, speed: 2, toner: 0, fibre: 0.06, webKey: NaN, started: false, col: F(12), key: NaN, + }; +} + +// The web: fibres laid at random, each a slightly wavy strand, drawn soft +// into F (how much fibre each LED sees) and marked on the links between +// neighbouring LEDs it runs through, which is where the toner wicks fastest. +function web(S) { + const g = S.fibre, Fd = S.F, CE = S.CE, CS = S.CS, CSE = S.CSE, CSW = S.CSW; + Fd.fill(0); + CE.fill(0); + CS.fill(0); + CSE.fill(0); + CSW.fill(0); + for (let i = 0; i < N; i++) S.J[i] = 0.9 + 0.2 * rnd(); + const count = floor(420 - 300 * g), wid = 0.6 + 1.4 * g; + for (let f = 0; f < count; f++) { + let x = -20 + rnd() * (W + 40), y = -20 + rnd() * (H + 40), th = rnd() * PI * 2, bend = 0; + const len = 40 + 50 * rnd(), b = 0.45 + 0.55 * rnd(); + // Most strands carry the toner a little faster than the gaps; a few + // carry it about six times as fast, and the front runs out along them. + const u = rnd(), wick = 0.12 + 1.1 * u * u * u * u; + let px = -1, py = -1; + for (let t = 0; t < len; t += 0.6) { + bend += (rnd() - 0.5) * 0.05; + bend *= 0.95; + th += bend * 0.5; + x += 0.6 * cos(th); + y += 0.6 * sin(th); + if (x < 0 || y < 0 || x >= W - 1 || y >= H - 1) { + px = -1; + continue; + } + // Soft splat of the strand. + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy, o = iy * W + ix, w = 0.32 * b * wid; + Fd[o] += w * (1 - fx) * (1 - fy); + Fd[o + 1] += w * fx * (1 - fy); + Fd[o + W] += w * (1 - fx) * fy; + Fd[o + W + 1] += w * fx * fy; + // The link it crossed. + const cx = floor(x + 0.5), cy = floor(y + 0.5); + if (px >= 0 && (cx !== px || cy !== py)) { + const dx = cx - px, dy = cy - py, a = Math.min(cy, py) * W, k = wick; + if (abs(dx) <= 1 && abs(dy) <= 1) { + if (dy === 0) CE[cy * W + min(cx, px)] += k; + else if (dx === 0) CS[a + cx] += k; + else if (dx === dy) CSE[a + min(cx, px)] += k; + else CSW[a + max(cx, px)] += k; + } + } + px = cx; + py = cy; + } + } + for (let i = 0; i < N; i++) Fd[i] = 1 - exp(-Fd[i] * 1.6); + // Deeper in the pad the fibres are out of focus: a second web, laid the + // same way and blurred over five LEDs, gives the white its soft depth. + const F2 = S.F2, tmp = S.tmp; + F2.fill(0); + for (let f = 0; f < 260; f++) { + let x = rnd() * W, y = rnd() * H, th = rnd() * PI * 2; + for (let t = 0; t < 50; t += 1) { + th += (rnd() - 0.5) * 0.08; + x += cos(th); + y += sin(th); + if (x >= 0 && y >= 0 && x < W && y < H) F2[floor(y) * W + floor(x)] += 0.5; + } + } + for (let pass = 0; pass < 2; pass++) { + for (let y = 0, i = 0; y < H; y++) + for (let x = 0; x < W; x++, i++) { + let a = 0; + for (let k = -2; k <= 2; k++) a += F2[y * W + cl(x + k, 0, W - 1)]; + tmp[i] = a / 5; + } + for (let y = 0, i = 0; y < H; y++) + for (let x = 0; x < W; x++, i++) { + let a = 0; + for (let k = -2; k <= 2; k++) a += tmp[cl(y + k, 0, H - 1) * W + x]; + F2[i] = a / 5; + } + } + for (let i = 0; i < N; i++) F2[i] = 1 - exp(-F2[i] * 3); +} + +// How far the toner has to wick to reach each LED from a drop at (x0, y0): +// the cheapest path, where crossing a link costs its length divided by how +// well fibres carry along it. A front at a given reach has run furthest +// along the fibres, so it is fingered and follows them. +function reach(S, d, x0, y0) { + const hk = S.hk, hv = S.hv, CE = S.CE, CS = S.CS, CSE = S.CSE, CSW = S.CSW, J = S.J, LIM = 45; + d.fill(1e9); + let n = 0; + const push = (k, v) => { + let i = n++; + while (i > 0) { + const p = (i - 1) >> 1; + if (hk[p] <= k) break; + hk[i] = hk[p]; + hv[i] = hv[p]; + i = p; + } + hk[i] = k; + hv[i] = v; + }; + for (let y = max(0, floor(y0 - 3)); y <= min(H - 1, floor(y0 + 3)); y++) + for (let x = max(0, floor(x0 - 3)); x <= min(W - 1, floor(x0 + 3)); x++) { + const r = Math.hypot(x + 0.5 - x0, y + 0.5 - y0); + if (r < 3) { + d[y * W + x] = r * 0.4; + push(r * 0.4, y * W + x); + } + } + const speed = (c) => 1 / (1 + 4.5 * c); + while (n > 0) { + const k = hk[0], v = hv[0]; + // Pop. + n--; + const lk = hk[n], lv = hv[n]; + let i = 0; + while (true) { + let c = 2 * i + 1; + if (c >= n) break; + if (c + 1 < n && hk[c + 1] < hk[c]) c++; + if (hk[c] >= lk) break; + hk[i] = hk[c]; + hv[i] = hv[c]; + i = c; + } + hk[i] = lk; + hv[i] = lv; + if (k > d[v] || k > LIM) continue; + const x = v % W, y = (v - x) / W, j = J[v]; + const rel = (u, cost) => { + const nk = k + cost * j; + if (nk < d[u]) { + d[u] = nk; + push(nk, u); + } + }; + if (x < W - 1) rel(v + 1, speed(CE[v])); + if (x > 0) rel(v - 1, speed(CE[v - 1])); + if (y < H - 1) { + rel(v + W, speed(CS[v])); + if (x < W - 1) rel(v + W + 1, 1.414 * speed(CSE[v])); + if (x > 0) rel(v + W - 1, 1.414 * speed(CSW[v])); + } + if (y > 0) { + rel(v - W, speed(CS[v - W])); + if (x > 0) rel(v - W - 1, 1.414 * speed(CSE[v - W - 1])); + if (x < W - 1) rel(v - W + 1, 1.414 * speed(CSW[v - W + 1])); + } + } + // Paths on a grid of LEDs come out in stair steps; two passes of a 3x3 + // blur round the front off while keeping its fingers. + const tmp = S.tmp; + for (let pass = 0; pass < 2; pass++) { + for (let yy = 0, i = 0; yy < H; yy++) + for (let xx = 0; xx < W; xx++, i++) tmp[i] = (xx > 0 ? d[i - 1] : d[i]) + d[i] + (xx < W - 1 ? d[i + 1] : d[i]); + for (let yy = 0, i = 0; yy < H; yy++) + for (let xx = 0; xx < W; xx++, i++) d[i] = ((yy > 0 ? tmp[i - W] : tmp[i]) + tmp[i] + (yy < H - 1 ? tmp[i + W] : tmp[i])) / 9; + } +} + +function drop(S, x, y, age) { + if (S.drops.length >= MAXD) S.pool.push(S.drops.shift().d); + const d = S.pool.pop(); + reach(S, d, x, y); + S.drops.push({ d, x, y, t: age, R: spread(d, S.toner) }); +} + +// How far a drop's front runs: the reach at which the wet patch covers the +// area its toner can fill, 1800 LEDs at Toner 0 and 5400 at 1, whatever the +// web. Found by halving. +function spread(d, toner) { + const A = 1800 + 3600 * toner; + let lo = 0, hi = 40; + for (let it = 0; it < 18; it++) { + const R = (lo + hi) / 2; + let n = 0; + for (let i = 0; i < N; i++) if (d[i] < R) n++; + if (n < A) lo = R; + else hi = R; + } + return (lo + hi) / 2; +} + +// Somewhere new: of six tries, the one furthest from every patch still wet. +function spot(S) { + let bx = 32, by = 64, best = -1; + for (let k = 0; k < 6; k++) { + const x = 10 + rnd() * 44, y = 12 + rnd() * 104; + let dd = 1e9; + for (const e of S.drops) dd = min(dd, Math.hypot(x - e.x, (y - e.y) * 0.7)); + if (dd > best) (best = dd), (bx = x), (by = y); + } + return [bx, by]; +} + +// A drop's front: running out fast and slowing as the toner spreads thin, +// holding, then drying back from the far edges towards where it fell. +const TS = 0.8, THOLD = 3.2, TDRY = 3.6; +function front(e) { + const t = e.t; + if (t < 0) return -9; + if (t < THOLD) return e.R * sqrt(1 - exp(-t / TS)); + const full = e.R * sqrt(1 - exp(-THOLD / TS)); + return full * (1 - (t - THOLD) / TDRY); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 0.06]; + const d = cl(dt || 0, 0, 0.1); + S.hue = kv[0] || 0; + S.speed = cl(kv[1], 0.2, 3); + S.toner = cl(kv[2] || 0, 0, 1); + S.fibre = cl(kv[3], 0, 1); + const wk = Math.round(S.fibre * 25); + if (wk !== S.webKey) { + S.webKey = wk; + web(S); + for (const e of S.drops) { + reach(S, e.d, e.x, e.y); + e.R = spread(e.d, S.toner); + } + } + if (!S.started) { + S.started = true; + // Open on a patch drying in the lower half and a fresh drop about to land higher up. + drop(S, 22 + rnd() * 20, 88 + rnd() * 20, 4.2); + drop(S, 22 + rnd() * 20, 26 + rnd() * 24, -0.2); + S.next = 3.2; + } + const st = (d * S.speed) / 2; + S.T += st; + for (const e of S.drops) e.t += st; + S.drops = S.drops.filter((e) => { + if (e.t < THOLD + TDRY) return true; + S.pool.push(e.d); + return false; + }); + S.next -= st; + if ((input.btnPressed && input.btnPressed[0]) || S.next <= 0) { + const [x, y] = spot(S); + drop(S, x, y, 0); + S.next = 3.2; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.started) return; + const col = S.col, Fd = S.F, F2 = S.F2, T = S.T; + const h = S.hue + 0.95, key = h; + if (S.key !== key) { + const hf = h - floor(h), wm = sm((hf + 0.03) / 0.06) * (1 - sm((hf - 0.24) / 0.06)); + hsv(h, 0.03, 1, col, 0); + hsv(h, 0.55 - 0.2 * wm, 1, col, 3); + col[6] = wm; + S.key = key; + } + const wm = col[6]; + const fr = S.drops.map(front); + // A drying patch also pales a little as it shrinks. + const mo = S.drops.map((e) => (e.t < THOLD ? 1 : 1 - (0.4 * (e.t - THOLD)) / TDRY)); + // A soft light behind the pad drifts slowly; wet cotton lets it through. + const gx = 32 + 20 * sin(T * 0.43), gy = 64 + 44 * sin(T * 0.29 + 1); + for (let y = 0, i = 0; y < H; y++) + for (let x = 0; x < W; x++, i++) { + let wet = 0, deep = 0; + for (let k = 0; k < S.drops.length; k++) { + const R = fr[k]; + if (R <= 0) continue; + const dd = S.drops[k].d[i], w = sm((R - dd) / 1.6 + 0.5) * mo[k]; + if (w > wet) wet = w; + const dp = w * cl(1 - dd / (R + 2), 0, 1); + if (dp > deep) deep = dp; + } + const f = Fd[i], f2 = F2[i]; + const gdx = x - gx, gdy = y - gy, glow = exp(-(gdx * gdx + gdy * gdy) / 900); + // Dry: matte white, the fibres a little brighter than the gaps. + const ld = 0.8 + 0.1 * f2 + 0.12 * f + 0.06 * glow; + // Wet: translucent and lit from behind in the toner's tint, deeper + // towards where the drop fell, and the fibres dark against it. A dark + // fibre in orange or gold would be a brown line, so the fibres go + // towards a cool grey, there more and only half as dark. + const lw = (0.94 + 0.04 * wm + 0.22 * glow) * (1 - 0.1 * deep - 0.12 * f2) * (1 - (0.58 - 0.3 * wm) * f), ti = 0.65 + 0.35 * deep; + let wr = (col[0] + (col[3] - col[0]) * ti) * lw, wg = (col[1] + (col[4] - col[1]) * ti) * lw, wb = (col[2] + (col[5] - col[2]) * ti) * lw; + const l0 = (0.94 + 0.04 * wm + 0.22 * glow) * (1 - 0.1 * deep - 0.12 * f2), ds = f * (0.25 + 0.55 * wm); + wr += (0.4 * l0 - wr) * ds; + wg += (0.43 * l0 - wg) * ds; + wb += (0.5 * l0 - wb) * ds; + let r = col[0] * ld + (wr - col[0] * ld) * wet, g = col[1] * ld + (wg - col[1] * ld) * wet, b = col[2] * ld + (wb - col[2] * ld) * wet; + // A drop that has just landed sits as a glossy bead for a moment before + // it soaks in: deeper tint under it and a highlight on its upper left. + for (const e of S.drops) { + if (e.t < 0 || e.t > 0.6) continue; + const u = e.t / 0.6, rb = 3.2 * (1 - u * u), bx = x + 0.5 - e.x, by = y + 0.5 - e.y, q = (bx * bx + by * by) / (rb * rb + 0.01); + if (q >= 1.6) continue; + const cov = sm((1.3 - q) / 0.5), hx = bx / rb + 0.4, hy = by / rb + 0.4, hl = exp(-(hx * hx + hy * hy) * 5) * 0.6 * cov; + r = r * (1 - 0.25 * cov) + hl; + g = g * (1 - 0.25 * cov) + hl; + b = b * (1 - 0.25 * cov) + hl; + } + display.setPixel(x, y, 255 * min(1, r), 255 * min(1, g), 255 * min(1, b)); + } +} From b156c119097955093b869404c4d64478cf4a964a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 17:21:10 +0000 Subject: [PATCH 5/9] incoming: add chimera-state and joukowsky-airfoil (pattern round 35) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SW1UfDNBr6xKa7hi5BogQV --- .../pattern-study/patterns/chimera-state.js | 245 +++++++++++++++++ .../patterns/joukowsky-airfoil.js | 257 ++++++++++++++++++ 2 files changed, 502 insertions(+) create mode 100644 incoming/pattern-study/patterns/chimera-state.js create mode 100644 incoming/pattern-study/patterns/joukowsky-airfoil.js diff --git a/incoming/pattern-study/patterns/chimera-state.js b/incoming/pattern-study/patterns/chimera-state.js new file mode 100644 index 000000000..13c4c9a1d --- /dev/null +++ b/incoming/pattern-study/patterns/chimera-state.js @@ -0,0 +1,245 @@ +// ===== Patternflow pattern ===== +// Title: Chimera State +// Family: rhythm +// Idea: 256 identical phase oscillators on a ring, each pulled by all the others and hardest by its near neighbours, split into a stretch that moves in step and a stretch whose phases scatter; the top 20 rows plot every phase against its place, a smooth arc beside a spray of dots, and below them each row is one moment, newest at the top, so the part in step runs down the middle as one smooth cyan band while the scattered sides fill with the dark strokes of oscillators slipping a turn behind. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Chimera states (Yoshiki Kuramoto and Dorjsuren Battogtokh, 2002) +// Round: 35 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Lag=80..88, Stride=0.8..4 +// +// Knob 1: Hue. Turns the wheel that colours the phase. At 0 the part in step is cyan, and an oscillator slipping a turn goes through blue and violet, dark at half a turn, and back through yellow and green. +// Knob 2: Speed. Four time units of the equation a second times the knob. The oscillators in the middle of the scattered stretch slip a turn every 10.8 time units, so at 2 about every 1.4 s, and at Stride 0.8 the history moves down 10 rows a second. +// Knob 3: Lag. The phase lag α in degrees. At 80 (β = 90 - α = 10°, near Abrams and Strogatz's 0.18 rad) 30% of the ring moves in step, a band 19 columns wide; at 84 22%, at 88 14%, 9 columns. The lag follows the knob a tenth of a degree per time unit, so the whole range takes 80 time units, 10 s at Speed 2, and the band widens or narrows as it scrolls. +// Knob 4: Stride. Time units between two rows of the history, 0.8 to 4. At 0.8 the 107 rows hold 86 time units and each slip is a thick dark stroke, the strokes lined up in chevrons; at 4 they hold 428 and the strokes break into speckle. +// Buttons: K1 tap draws a new random start: two thirds of the ring are thrown out of step around a band about 45% wide, and in the next 30 time units, 4 s at Speed 2, the scattered stretch eats back into the band. Each new draw grows at 84 degrees for 100 time units before the lag returns to the knob. + +// The ring form is Daniel Abrams and Steven Strogatz's (2004): N identical +// phase oscillators at x_j = -pi + 2 pi j / N, each pulled by all the others +// through the kernel G(x) = (1 + A cos x) / 2 pi and a phase lag alpha, +// dθ_j/dt = ω - (2 pi / N) Σ_k G(x_j - x_k) sin(θ_j - θ_k + α), +// their A = 0.995 and their start θ_j = 6 r_j exp(-0.76 x_j²), r_j uniform +// in [-1/2, 1/2]. Every oscillator obeys the same law, yet the ring splits +// into a stretch that moves in step and a stretch whose phases scatter. +const W = 64, H = 128, N = 256, PER = N / W, TOP = 20, Y0 = TOP + 1, ROWS = H - Y0; +const A = 0.995, STEP = 0.1, MAXSTEPS = 30, RATE = 4, HUE0 = 0.55; +const { min, floor, round, sqrt, sin, cos, exp, atan2, abs, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const wrap = (v, m) => v - m * round(v / m); + +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 new random draw of the Abrams-Strogatz start, its scattered hump at x0. +// The part in step forms on the calm side, opposite x0; on the first draw +// the columns are set to put it in the middle of the panel. +function seed(P, x0, place) { + for (let j = 0; j < N; j++) { + const d = wrap(-PI + (TAU * j) / N - x0, TAU); + P.th[j] = 6 * (Math.random() - 0.5) * exp(-0.76 * d * d); + } + if (place) { + P.xs = ((x0 + 2 * PI) / TAU) * W; + P.xs -= W * floor(P.xs / W); + P.col = round(P.xs) % W; + } + P.dead = 0; + P.age = 0; + P.lag = 84; +} + +// The sum over k is the local field R_j e^{iΘ_j} = (1/N) Σ_k (1 + A cos(x_j +// - x_k)) e^{iθ_k}, and with cos(x_j - x_k) = cos x_j cos x_k + sin x_j sin +// x_k it is three sums over the ring, Σ e^{iθ}, Σ cos x e^{iθ} and +// Σ sin x e^{iθ}, read off at each x_j: O(N) a step instead of O(N²). Then +// dθ_j/dt = ω - R_j sin(θ_j - Θ_j + α). +function deriv(P, t, d, frame) { + const { cx, sx, c: C, s: S } = P; + let p0r = 0, p0i = 0, pcr = 0, pci = 0, psr = 0, psi = 0; + for (let k = 0; k < N; k++) { + const c = cos(t[k]), s = sin(t[k]); + C[k] = c; + S[k] = s; + p0r += c; + p0i += s; + pcr += cx[k] * c; + pci += cx[k] * s; + psr += sx[k] * c; + psi += sx[k] * s; + } + const ca = P.ca, sa = P.sa; + let wx = 0, wy = 0; + for (let j = 0; j < N; j++) { + const rr = (p0r + A * (cx[j] * pcr + sx[j] * psr)) / N, ri = (p0i + A * (cx[j] * pci + sx[j] * psi)) / N; + const c = C[j], s = S[j]; + d[j] = -((s * ca + c * sa) * rr - (c * ca - s * sa) * ri); + if (frame) { + const r2 = rr * rr + ri * ri; + wx += r2 * cx[j]; + wy += r2 * sx[j]; + } + } + if (frame) { + // The field is strongest over the part in step. Its centre xc, and the + // field's phase there, give the frame that turns with that part. + const xc = atan2(wy, wx), cc = cos(xc), sc = sin(xc); + P.xc = xc; + P.phi = atan2(p0i + A * (cc * pci + sc * psi), p0r + A * (cc * pcr + sc * psr)); + P.z = sqrt(p0r * p0r + p0i * p0i) / N; + } +} + +// One RK4 step of 0.1. The right side is never more than 1 + A in size, so +// the step is far inside RK4's reach at every Lag. Before it the whole ring +// is turned back by the phase of the part in step, which the equation does +// not see (every oscillator is the same, so ω is free): that part's phases +// stand still and only the scattered ones move. +function step(P) { + const { th, k1, k2, k3, k4, tmp } = P, h = STEP; + // Each draw grows at 84 degrees for 100 time units, and only then does + // the lag move to the knob, a tenth of a degree per time unit. From the + // Abrams-Strogatz start right at 80 degrees, 6 draws in 30 fell into step + // everywhere within 65 time units; grown at 84 and brought down to 80 (or + // up to 88) after, none of 30 did in 1000. A sudden jump from 88 to 80 on + // a grown chimera ended 1 of 20, hence the slow turn. + P.age += h; + const want = P.age < 100 ? 84 : P.lagKnob; + P.lag += cl(want - P.lag, -0.01, 0.01); + P.ca = cos((P.lag * PI) / 180); + P.sa = sin((P.lag * PI) / 180); + deriv(P, th, k1, true); + const phi = P.phi; + for (let j = 0; j < N; j++) th[j] -= phi; + for (let j = 0; j < N; j++) tmp[j] = th[j] + 0.5 * h * k1[j]; + deriv(P, tmp, k2, false); + for (let j = 0; j < N; j++) tmp[j] = th[j] + 0.5 * h * k2[j]; + deriv(P, tmp, k3, false); + for (let j = 0; j < N; j++) tmp[j] = th[j] + h * k3[j]; + deriv(P, tmp, k4, false); + for (let j = 0; j < N; j++) th[j] = wrap(th[j] + (h / 6) * (k1[j] + 2 * k2[j] + 2 * k3[j] + k4[j]), TAU); + // With 256 oscillators the chimera wanders round the ring, a few columns + // in a thousand time units. The history keeps the ring's own columns, so + // the wandering shows as a lean in the band; the panel as a whole slides + // a column after it once the part in step is three columns off the middle. + P.xs = wrap(P.xs + 0.002 * wrap(((P.xc + PI) / TAU) * W - P.xs, W), W); + if (abs(wrap(P.xs - P.col, W)) > 3) P.col = ((round(P.xs) % W) + W) % W; + // A draw can still fall into step everywhere, the other state this ring + // can rest in. After 30 time units of that, a new draw. + P.dead = P.z > 0.99 ? P.dead + h : 0; + if (P.dead > 30) seed(P, (TAU * P.col) / W, false); +} + +const osc = (P, c, k) => ((((P.col - W / 2 + c) * PER + k) % N) + N) % N; + +// A row of the history keeps one oscillator of every four, in the ring's +// own columns. +function record(P) { + P.head = (P.head + ROWS - 1) % ROWS; + const o = P.head * W; + for (let c = 0; c < W; c++) P.hist[o + c] = P.th[c * PER + 1]; +} + +export function setup(params) { + const cx = new Float64Array(N), sx = new Float64Array(N); + for (let j = 0; j < N; j++) (cx[j] = cos(-PI + (TAU * j) / N)), (sx[j] = sin(-PI + (TAU * j) / N)); + const f = () => new Float64Array(N); + params.s = { + cx, sx, th: f(), k1: f(), k2: f(), k3: f(), k4: f(), tmp: f(), c: f(), s: f(), + ca: 0, sa: 1, lag: 84, lagKnob: 80, age: 0, xc: PI, phi: 0, z: 0, xs: 0, col: 0, dead: 0, + hist: new Float32Array(ROWS * W), head: 0, clock: 0, since: 0, fresh: true, + hue: 0, speed: 2, stride: 1, pal: new Float32Array(256 * 3), dot: new Float32Array(256 * 3), palHue: NaN, + acc: new Float32Array(W * TOP * 3), + }; + seed(params.s, 0, true); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 80, 0.06], d = cl(dt || 0, 0, 0.1); + P.hue = k[0]; + P.speed = cl(k[1], 0.2, 3); + P.lagKnob = cl(k[2], 80, 88); + P.stride = cl(k[3], 0.8, 4); + if (P.fresh) { + // 150 time units to grow and reach the knob's lag; a draw that still + // falls into step is drawn again. Then the whole history is run once so + // the panel opens full: 5,780 steps at Stride 4, about 0.3 s once. + for (let tries = 0; tries < 8; tries++) { + if (tries) seed(P, 0, true); + for (let i = 0; i < 1500; i++) step(P); + if (P.z < 0.9) break; + } + for (let r = 0; r < ROWS; r++) { + for (let t = 0; t < P.stride - 1e-9; t += STEP) step(P); + record(P); + } + P.fresh = false; + } + if (input.btnPressed && input.btnPressed[0]) seed(P, (TAU * P.col) / W, false); + P.clock += d * RATE * P.speed; + let n = 0; + for (; P.clock >= STEP && n < MAXSTEPS; n++) { + step(P); + P.clock -= STEP; + P.since += STEP; + if (P.since >= P.stride - 1e-9) { + record(P); + P.since -= P.stride; + } + } + if (n === MAXSTEPS) P.clock = 0; +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const { pal, dot, hist, th, acc } = P; + if (P.palHue !== P.hue) { + // Phase round the colour wheel. In the history the brightness also + // follows the phase, full at the phase of the part in step and dark + // half a turn away, so each slip of a scattered oscillator is a dark + // stroke through its column. + for (let i = 0; i < 256; i++) { + const t = (i / 256) * TAU; + hsv(P.hue + HUE0 + i / 256, 0.85, 0.5 + 0.5 * cos(t), pal, i * 3); + hsv(P.hue + HUE0 + i / 256, 0.75, 1, dot, i * 3); + } + P.palHue = P.hue; + } + // The present, in the top 20 rows: every oscillator a dot, its phase up + // and down against its place on the ring. + acc.fill(0); + for (let c = 0; c < W; c++) + for (let k = 0; k < PER; k++) { + const t = th[osc(P, c, k)], y = TOP / 2 - 0.5 + (t / PI) * (TOP / 2 - 0.5), y0 = floor(y), f = y - y0; + const q = (floor((t / TAU + 1) * 256) & 255) * 3; + if (y0 >= 0) { + const o = (y0 * W + c) * 3, a = 0.7 * (1 - f); + (acc[o] += a * dot[q]), (acc[o + 1] += a * dot[q + 1]), (acc[o + 2] += a * dot[q + 2]); + } + if (y0 + 1 < TOP) { + const o = ((y0 + 1) * W + c) * 3, a = 0.7 * f; + (acc[o] += a * dot[q]), (acc[o + 1] += a * dot[q + 1]), (acc[o + 2] += a * dot[q + 2]); + } + } + for (let y = 0, o = 0; y < TOP; y++) + for (let x = 0; x < W; x++, o += 3) display.setPixel(x, y, min(255, acc[o] * 255), min(255, acc[o + 1] * 255), min(255, acc[o + 2] * 255)); + for (let x = 0; x < W; x++) display.setPixel(x, TOP, 0, 0, 0); + // The past: one row per moment, the newest at the top. + for (let r = 0; r < ROWS; r++) { + const o = ((P.head + r) % ROWS) * W, c0 = P.col + W / 2; + for (let x = 0; x < W; x++) { + const q = (floor((hist[o + ((c0 + x) % W)] / TAU + 1) * 256) & 255) * 3; + display.setPixel(x, Y0 + r, pal[q] * 255, pal[q + 1] * 255, pal[q + 2] * 255); + } + } +} diff --git a/incoming/pattern-study/patterns/joukowsky-airfoil.js b/incoming/pattern-study/patterns/joukowsky-airfoil.js new file mode 100644 index 000000000..a256ede9e --- /dev/null +++ b/incoming/pattern-study/patterns/joukowsky-airfoil.js @@ -0,0 +1,257 @@ +// ===== Patternflow pattern ===== +// Title: Joukowsky Airfoil +// Family: physics +// Idea: A cambered Joukowsky wing 44 rows long stands in the middle of the panel with the ideal flow running down past it, faint streamlines bending round it, while lines of dye let go across the top row once a second come down, wrap the rounded nose where they split at a red stagnation point, and leave the sharp trailing edge with the half that went over the suction side well ahead of the half that went under, a lead that grows with the angle of attack and is gone when the circulation is taken away. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Joukowsky airfoil (Nikolai Zhukovsky, 1910) +// Round: 35 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Attack=0..15, Camber=0..0.12 +// +// Knob 1: Hue. Turns the four dyes round the wheel together. At 0 the lines come gold, green, blue and pink in turn, so one line can be followed past the wing and its two halves matched below it. +// Knob 2: Speed. How fast the free stream runs down the panel: 10 LEDs a second times the knob. At 2 a line crosses the 128 rows in 6.4 s and a new one leaves the top row every second; at 0.2 every ten. +// Knob 3: Attack. The angle of attack, 0 to 15 degrees; the nose tips right into the stream. With Camber at 0.06 the half of a line that goes over the right side reaches the trailing edge 12.6 rows ahead of the other half at 0 degrees, 30 rows at 8, 46 at 15, and the front stagnation point slides down the left side of the nose. +// Knob 4: Camber. The camber of the wing, 0 to 12% of the chord, by lifting the circle's centre. At 0 the wing is symmetric and at 0 degrees the two halves of every line meet again at the trailing edge; at 12% and 15 degrees the lead is 69 rows. +// Buttons: K1 tap takes the circulation away for 10 s (a second tap gives it back). The flow then turns round the sharp trailing edge from the left, the rear stagnation point leaves the edge and moves up the right side, 0.9 LED at 8 degrees and 2.8 at 15 degrees with 12% camber, and the two halves of every line arrive within 6 rows of each other, the left one now first. + +// Ideal flow past a circle in the ζ-plane, carried to flow past an airfoil +// by z = ζ + c²/ζ. The circle goes through ζ = c, which lands on the sharp +// trailing edge at z = 2c, and its centre ζ0 = -ε + iδ sits left of the +// origin for thickness and above it for camber. With U = 1, +// W(ζ) = ζ' e^{-iα} + R² e^{iα}/ζ' + (iΓ/2π) log ζ', ζ' = ζ - ζ0, +// and the Kutta condition, that the flow leaves the trailing edge smoothly, +// fixes Γ = 4πR sin(α + β), β the angle of ζ0 seen from c. Lengths are in +// LEDs: c = 11, so the chord is 44 rows, centred on the panel, and ε = +// 0.12 c makes the wing 6 LEDs at its thickest, 14% of the chord. The free +// stream runs down the panel at one LED per unit of time. +const W = 64, H = 128, NP = W * H, CX = 32, CY = 64; +const C = 11, EPS = 0.12 * C, LAM = 20, DUTY = 2.2 / 20, TMAX = 400, CHUNK = 768, SPACE = 6; +const { min, max, floor, round, sqrt, sin, cos, atan2, abs, exp, log, PI } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// One flow: the circle, the angle of attack and the circulation. +function makeFlow(alphaDeg, camber, kutta) { + const a = (alphaDeg * PI) / 180, d = camber * 2 * C; + const z0r = -EPS, z0i = d, R = sqrt((C + EPS) ** 2 + d * d), beta = atan2(d, C + EPS); + const gam = kutta ? 4 * PI * R * sin(a + beta) : 0; + return { a, ca: cos(a), sa: sin(a), z0r, z0i, R, R2: R * R, beta, gam, key: alphaDeg.toFixed(1) + "/" + camber.toFixed(3) + "/" + kutta }; +} + +// The flow at ζ: o[0], o[1] = dζ/dt, o[2] = speed on the panel, o[3] = +// |dz/dζ|. dζ/dt = conj(dW/dζ) / |dz/dζ|². At the trailing edge dz/dζ is 0; +// with the Kutta condition dW/dζ is 0 there too and the floor on |dz/dζ|² +// only keeps 0/0 away, without it the speed there is capped at 6. +function flowAt(F, zr, zi, o) { + const pr = zr - F.z0r, pi = zi - F.z0i, r2 = pr * pr + pi * pi; + const ir = pr / r2, ii = -pi / r2, i2r = ir * ir - ii * ii, i2i = 2 * ir * ii; + const er = F.ca * i2r - F.sa * i2i, ei = F.ca * i2i + F.sa * i2r, g = F.gam / TAU; + const wr = F.ca - F.R2 * er - g * ii, wi = -F.sa - F.R2 * ei + g * ir; + const q2 = zr * zr + zi * zi, jr = zr / q2, ji = -zi / q2, j2r = jr * jr - ji * ji, j2i = 2 * jr * ji; + const dr = 1 - C * C * j2r, di = -C * C * j2i, J2 = max(1e-8, dr * dr + di * di); + let sp = sqrt((wr * wr + wi * wi) / J2), k = 1; + if (sp > 6) (k = 6 / sp), (sp = 6); + o[0] = (k * wr) / J2; + o[1] = (-k * wi) / J2; + o[2] = sp; + o[3] = sqrt(J2); +} + +// ζ to the panel: z = ζ + c²/ζ, turned back by α so the free stream runs +// down the panel and the suction side faces right. o[0] = X, o[1] = Y. +function toPanel(F, zr, zi, o) { + const q2 = zr * zr + zi * zi, xr = zr + (C * C * zr) / q2, xi = zi - (C * C * zi) / q2; + o[0] = CX + xi * F.ca - xr * F.sa; + o[1] = CY + xr * F.ca + xi * F.sa; +} + +// The panel to ζ: of the two roots of ζ² - zζ + c² = 0, the one outside +// the circle. o[2] = |ζ'|²; less than R² means the LED is inside the wing. +function toZeta(F, X, Y, o) { + const pr = Y - CY, pi = X - CX, zr = pr * F.ca - pi * F.sa, zi = pr * F.sa + pi * F.ca; + const wr = zr * zr - zi * zi - 4 * C * C, wi = 2 * zr * zi, m = sqrt(wr * wr + wi * wi); + const sr = sqrt(max(0, (m + wr) / 2)), si = (wi < 0 ? -1 : 1) * sqrt(max(0, (m - wr) / 2)); + const ar = (zr + sr) / 2, ai = (zi + si) / 2, br = (zr - sr) / 2, bi = (zi - si) / 2; + const da = (ar - F.z0r) ** 2 + (ai - F.z0i) ** 2, db = (br - F.z0r) ** 2 + (bi - F.z0i) ** 2; + if (da >= db) (o[0] = ar), (o[1] = ai), (o[2] = da); + else (o[0] = br), (o[1] = bi), (o[2] = db); +} + +// When the fluid at an LED crossed the top row: back along its streamline, +// in the ζ-plane where the wing is a circle and easy to keep out of, adding +// up time. Midpoint steps of 0.35 LED by the surface, up to 3 LEDs and 3 +// time units away from it. +function arrival(F, X, Y, f, pos) { + toZeta(F, X, Y, f); + if (f[2] < F.R2) return -1; + let zr = f[0], zi = f[1], T = 0; + for (let it = 0; it < 400; it++) { + flowAt(F, zr, zi, f); + const pr = zr - F.z0r, pi = zi - F.z0i, dist = (sqrt(pr * pr + pi * pi) - F.R) * f[3]; + const dt = min(cl(0.35 + 0.3 * dist, 0.35, 3) / max(f[2], 1e-6), 3); + flowAt(F, zr - 0.5 * dt * f[0], zi - 0.5 * dt * f[1], f); + zr -= dt * f[0]; + zi -= dt * f[1]; + const qr = zr - F.z0r, qi = zi - F.z0i, q2 = qr * qr + qi * qi; + if (q2 < F.R2) { + const s = (F.R * 1.0001) / sqrt(q2); + zr = F.z0r + qr * s; + zi = F.z0i + qi * s; + } + T += dt; + toPanel(F, zr, zi, pos); + // Past the top row: give back the part of the step beyond it. + if (pos[1] <= 0) return max(0, T + pos[1] / max(0.3, f[2])); + if (T > TMAX) return TMAX; + } + return TMAX; +} + +function newField(F) { + return { F, T: new Float32Array(NP), G: new Float32Array(NP), psi: new Float32Array(NP), sp: new Float32Array(NP), body: new Float32Array(NP), next: 0, st: [0, 0, 0, 0] }; +} + +// Arrival times for LEDs from..to of a field; the rest once they are all in. +function fill(Fd, to) { + const { F, T, G, psi, sp, body } = Fd, f = new Float64Array(4), pos = [0, 0]; + for (let p = Fd.next; p < to; p++) T[p] = arrival(F, (p % W) + 0.5, floor(p / W) + 0.5, f, pos); + Fd.next = to; + if (to < NP) return false; + for (let p = 0; p < NP; p++) { + toZeta(F, (p % W) + 0.5, floor(p / W) + 0.5, f); + const zr = f[0], zi = f[1], pr = zr - F.z0r, pi = zi - F.z0i, r2 = pr * pr + pi * pi; + flowAt(F, zr, zi, f); + sp[p] = f[2]; + // Inside the wing or within half an LED of its edge, by the distance + // to the circle stretched by |dz/dζ|. + body[p] = cl(0.5 - (sqrt(r2) - F.R) * f[3], 0, 1); + // ψ = Im W. + psi[p] = pi * F.ca - pr * F.sa + (F.R2 * (F.sa * pr - F.ca * pi)) / r2 + (F.gam / (2 * TAU)) * log(r2); + } + // How fast the arrival time changes across one LED, from the smaller of + // the two one-sided steps, so the jump in the wake behind the trailing + // edge does not count as steep. + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, t = T[p], d = (q) => (T[q] < 0 ? 50 : min(50, abs(T[q] - t))); + const gx = min(x > 0 ? d(p - 1) : 50, x < W - 1 ? d(p + 1) : 50), gy = min(y > 0 ? d(p - W) : 50, y < H - 1 ? d(p + W) : 50); + G[p] = sqrt(gx * gx + gy * gy); + } + // The two stagnation points, where sin(θ - α) = -Γ / 4πR on the circle. + const s0 = cl(-F.gam / (4 * PI * F.R), -1, 1), th = [F.a + Math.asin(s0), F.a + PI - Math.asin(s0)]; + for (let i = 0; i < 2; i++) { + toPanel(F, F.z0r + F.R * cos(th[i]), F.z0i + F.R * sin(th[i]), pos); + Fd.st[i * 2] = pos[0]; + Fd.st[i * 2 + 1] = pos[1]; + } + return true; +} + +export function setup(params) { + params.s = { cur: null, old: null, nxt: null, fade: 1, s: 0, hue: 0, speed: 2, off: 0, pal: new Float32Array(15), palHue: NaN, rgb: new Float32Array(NP * 3) }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + P.hue = k[0]; + P.speed = cl(k[1], 0.2, 3); + // K1 takes the circulation away for 10 s; a second tap gives it back. + if (input.btnPressed && input.btnPressed[0]) P.off = P.off > 0 ? 0 : 10; + P.off = max(0, P.off - d); + const want = makeFlow(cl(k[2], 0, 15), cl(k[3], 0, 0.12), P.off <= 0); + if (!P.cur) { + P.cur = newField(want); + fill(P.cur, NP); + } else if (!P.nxt && want.key !== P.cur.F.key) P.nxt = newField(want); + // A new flow is worked out 768 LEDs a frame, eleven frames for the panel, + // then faded in over 0.4 s; a knob still turning starts the next one. + if (P.nxt && fill(P.nxt, min(NP, P.nxt.next + CHUNK))) { + P.old = P.cur; + P.cur = P.nxt; + P.nxt = null; + P.fade = 0; + } + P.fade = min(1, P.fade + d / 0.4); + P.s += d * 10 * P.speed; +} + +function paint(P, Fd, wgt, rgb) { + const { T, G, psi, sp, body } = Fd, pal = P.pal; + for (let p = 0, o = 0; p < NP; p++, o += 3) { + let r = 0.012, g = 0.016, b = 0.05; + // Streamlines every 6 units of ψ, about one LED wide wherever they are. + const u = psi[p] / SPACE, du = (abs(u - round(u)) * SPACE) / max(0.05, sp[p]), sl = 0.15 * exp(-2 * du * du); + r += sl * 0.45; + g += sl * 0.55; + b += sl * 0.8; + const t = T[p]; + if (t >= 0) { + // The dye: a puff across the top row every 20 time units, lasting 2.2 + // of them, so in the free stream a line 2.2 LEDs thick. Where the flow + // slows the lines crowd and thin; a line thinner than 1.6 LEDs is drawn + // 1.6 wide and dimmer, and where they come closer than 4 LEDs they + // merge into the haze they would make, 2.2/20 of full. + const ph = (P.s - t) / LAM - DUTY / 2, kk = round(ph), per = LAM / max(0.02, G[p]); + const wpx = DUTY * per, hw = max(0.8, wpx / 2), dpx = abs(ph - kk) * per; + const line = cl(hw + 0.5 - dpx, 0, 1) * min(1, wpx / (2 * hw)), mix = cl((per - 4) / 4, 0, 1); + const c = (((kk % 4) + 4) % 4) * 3, dye = line * mix, haze = DUTY * (1 - mix); + r += dye * pal[c] + haze * pal[12]; + g += dye * pal[c + 1] + haze * pal[13]; + b += dye * pal[c + 2] + haze * pal[14]; + } + const bo = body[p]; + r += (0.82 - r) * bo; + g += (0.84 - g) * bo; + b += (0.9 - b) * bo; + rgb[o] += wgt * r; + rgb[o + 1] += wgt * g; + rgb[o + 2] += wgt * b; + } +} + +// The two stagnation points, a red LED each on the wing's edge. +function marks(Fd, wgt, rgb) { + for (let i = 0; i < 2; i++) { + const x = round(Fd.st[i * 2] - 0.5), y = round(Fd.st[i * 2 + 1] - 0.5); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const o = (y * W + x) * 3; + rgb[o] += (1 - rgb[o]) * wgt; + rgb[o + 1] += (0.12 - rgb[o + 1]) * wgt; + rgb[o + 2] += (0.1 - rgb[o + 2]) * wgt; + } +} + +export function draw(display, params) { + const P = params.s; + if (!P || !P.cur) return; + const { pal, rgb } = P; + if (P.palHue !== P.hue) { + // Four dyes in turn, so a line can be followed past the wing: gold, + // green, blue and pink at Hue 0. The haze is their mean. + pal.fill(0, 12); + for (let i = 0; i < 4; i++) { + hsv(P.hue + 0.12 + i * 0.25, 0.7, 1, pal, i * 3); + for (let c = 0; c < 3; c++) pal[12 + c] += pal[i * 3 + c] / 4; + } + P.palHue = P.hue; + } + rgb.fill(0); + if (P.old && P.fade < 1) paint(P, P.old, 1 - P.fade, rgb); + const w = P.old && P.fade < 1 ? P.fade : 1; + paint(P, P.cur, w, rgb); + if (w < 1) marks(P.old, 1 - w, rgb); + marks(P.cur, w, rgb); + for (let p = 0, o = 0; p < NP; p++, o += 3) display.setPixel(p % W, floor(p / W), min(255, rgb[o] * 255), min(255, rgb[o + 1] * 255), min(255, rgb[o + 2] * 255)); +} From 2de3693dcfc5bbdb7ff31d17b6a5427f99c79dbd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 17:21:31 +0000 Subject: [PATCH 6/9] incoming: add sheet-mask and gilded-cream (pattern round 35) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SW1UfDNBr6xKa7hi5BogQV --- .../pattern-study/patterns/gilded-cream.js | 364 +++++++++++++++ incoming/pattern-study/patterns/sheet-mask.js | 426 ++++++++++++++++++ 2 files changed, 790 insertions(+) create mode 100644 incoming/pattern-study/patterns/gilded-cream.js create mode 100644 incoming/pattern-study/patterns/sheet-mask.js diff --git a/incoming/pattern-study/patterns/gilded-cream.js b/incoming/pattern-study/patterns/gilded-cream.js new file mode 100644 index 000000000..70ad9633a --- /dev/null +++ b/incoming/pattern-study/patterns/gilded-cream.js @@ -0,0 +1,364 @@ +// ===== Patternflow pattern ===== +// Title: Gilded Cream +// Family: cosmetic +// Idea: A thick ivory cream with flakes of gold leaf in it is stretched and folded over itself again and again like a baker's dough, seen from the side, so the few flakes are drawn out into long gold veins and then into thinner and more of them, layer on layer, while a small lamp drifts over the cream's soft relief and the gold glints where it finds it, until the gold is a fine shimmer, the cream settles smooth and fresh flakes are scattered in for the next round. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 35, after the folded baker's map of ergodic theory and gold-leaf cream in white and gold packaging +// Round: 35 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Fold=2..4, Gold=0..1 +// +// Knob 1: Hue. Tints the cream once round the wheel while the leaf stays gold. At 0 ivory, at 0.1 pistachio, at 0.25 mint, at 0.45 sky blue, at 0.6 lavender, at 0.78 pink, at 0.9 blush, at 0.95 peach. Round blush, peach, ivory and pistachio the tint is kept pale, because a deeper cream there reads beige or khaki next to the gold. +// Knob 2: Speed. At 2 a fold takes 2 seconds and the cream rests 0.7 between folds; a round of five folds, the settling and the new flakes takes about 15 seconds. At 0.2 all of it runs ten times slower. +// Knob 3: Fold. How many layers each fold makes. At 2 the cream is stretched to twice its length and folded once over itself, five folds a round; at 3 it is stretched threefold and folded in thirds like a letter, three folds a round; at 4 fourfold, three folds a round, and the last one leaves only a shimmer. +// Knob 4: Gold. How much leaf is scattered in: 6 flakes about 7 by 10 LEDs at the opening, up to 17 flakes half as big again at 1. +// Buttons: K1 tap makes one fast fold: the fold under way, or the next one, runs four times as fast, half a second at Speed 2. + +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); +const sm = (v) => ((v = cl(v, 0, 1)), v * v * (3 - 2 * v)); +const hash = (i) => { + let h = Math.imul(i ^ 0x5bd1e995, 0x27d4eb2d); + h = Math.imul(h ^ (h >>> 15), 0x165667b1); + return ((h ^ (h >>> 13)) >>> 0) / 4294967296; +}; + +// Smooth noise in a 128 x 128 tile that wraps, octaves of 8, 16 and 32 +// cells: the cream's own soft relief. +const T = 128, TM = T - 1, TEX = new Float32Array(T * T); +function buildTex() { + 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 * 17 + o * 6007) * 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, o) { + const f = (n) => { + const k = (n + (h - floor(h)) * 6) % 6; + return v - v * s * max(0, min(k, 4 - k, 1)); + }; + out[o] = f(5); + out[o + 1] = f(3); + out[o + 2] = f(1); +} + +// The cream as it was before any fold, in its own coordinates: u along the +// slab, 0 to 1 across the panel, and v up through it, 0 at the bottom to 1 +// at the top. A raster of RU x RV holds where the gold is, which flake it +// belongs to and how each crumpled facet of the leaf is tilted. +const RU = 256, RV = 256; +const MAXF = 17; + +function scatter(P) { + const { GC, GI, GX, GY } = P; + GC.fill(0); + GI.fill(-1); + const n = 6 + floor(11 * P.gold + 0.5), sf = 1 + 0.5 * P.gold, R = P.round * 977; + const vx = new Float32Array(10), vy = new Float32Array(10), va = new Float32Array(11); + for (let i = 0; i < n; i++) { + const s = R + i * 53; + // Half sizes in LEDs: across the slab and up through it. + const a = (2.8 + 2.2 * hash(s)) * sf, b = (4 + 3.4 * hash(s + 1)) * sf; + const cu = 0.1 + 0.8 * hash(s + 2), cv = 0.08 + 0.84 * hash(s + 3), rot = (hash(s + 4) - 0.5) * 1.2; + // A torn flake: 6 to 9 corners at their own distances round the middle. + const k = 6 + floor(4 * hash(s + 5)); + for (let m = 0; m < k; m++) { + const an = (2 * PI * (m + 0.35 * (hash(s + 11 + m) - 0.5))) / k, r = 0.6 + 0.45 * hash(s + 23 + m); + va[m] = an; + vx[m] = r * cos(an); + vy[m] = r * sin(an); + } + va[k] = va[0] + 2 * PI; + const cr = cos(rot), sr = sin(rot), ext = max(a, b) * 1.1; + const i0 = max(0, floor((cu - ext / W) * RU)), i1 = min(RU - 1, floor((cu + ext / W) * RU)); + const j0 = max(0, floor((cv - ext / H) * RV)), j1 = min(RV - 1, floor((cv + ext / H) * RV)); + for (let j = j0; j <= j1; j++) + for (let q0 = i0; q0 <= i1; q0++) { + const dx = ((q0 + 0.5) / RU - cu) * W, dy = ((j + 0.5) / RV - cv) * H; + const lx = dx * cr + dy * sr, ly = -dx * sr + dy * cr, ex = lx / a, ey = ly / b, d = sqrt(ex * ex + ey * ey); + let th = Math.atan2(ey, ex); + if (th < va[0]) th += 2 * PI; + let m = 0; + while (m < k - 1 && th > va[m + 1]) m++; + // Where the ray from the middle meets the outline between corners m + // and m + 1. + const m1 = (m + 1) % k, ax = vx[m], ay = vy[m], bx = vx[m1] - ax, by = vy[m1] - ay; + const dc = cos(th) * by - sin(th) * bx, re = abs(dc) > 1e-6 ? (ax * by - ay * bx) / dc : 1; + const cov = cl((re - d) * min(a, b) * 2, 0, 1); + const q = j * RU + q0; + if (cov <= GC[q]) continue; + GC[q] = cov; + GI[q] = i; + // Crumple facets about 2.5 LEDs across, each tilted its own way. + const fx = floor(lx / 2.5 + 50), fy = floor(ly / 2.5 + 50); + GX[q] = hash(s * 31 + fx * 7 + fy * 131) - 0.5; + GY[q] = hash(s * 37 + fx * 11 + fy * 173) - 0.5; + } + P.land[i] = 0.08 + 0.55 * hash(s + 9); + } +} + +export function setup(params) { + buildTex(); + params.s = { + c: 0, hue: 0, speed: 2, k3: 2, gold: 0.06, goldBuilt: -1, + round: 0, folds: [], fk: 2, p: 0, ph: 0, pt: 0, fast: false, + GC: new Float32Array(RU * RV), GI: new Int8Array(RU * RV), GX: new Float32Array(RU * RV), GY: new Float32Array(RU * RV), + land: new Float32Array(MAXF), + hgt: new Float32Array(N), surf: new Float32Array(N), gcov: new Float32Array(N), gfx: new Float32Array(N), gfy: new Float32Array(N), gfl: new Float32Array(N), + hole: new Float32Array(N), lipd: new Float32Array(N), ob: new Float32Array(N), pal: new Float32Array(12), lastHue: NaN, + }; +} + +// The round, in seconds at Speed 2: the flakes land, then folds one after +// another with a rest between, then the cream settles smooth. +const LAND = 0, FOLD = 1, REST = 2, SETTLE = 3; +const LAND_T = 0.7, FOLD_T = 2.0, REST_T = 0.7, SETTLE_T = 1.8; +// Folds a round: until the leaf has been stretched past twenty times. +const foldsFor = (k) => (k === 2 ? 5 : 3); + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 2, 0.06], d = (cl(dt || 0, 0, 0.1) * cl(k[1], 0.2, 3)) / 2; + P.hue = k[0]; + P.speed = cl(k[1], 0.2, 3); + P.k3 = floor(cl(k[2], 2, 4) + 0.5); + P.gold = cl(k[3], 0, 1); + if (abs(P.gold - P.goldBuilt) > 0.004) (scatter(P), (P.goldBuilt = P.gold)); + P.c += d; + // A tap waits for the next fold if none is under way. + if (input.btnPressed && input.btnPressed[0]) P.fast = true; + P.pt += P.ph === FOLD && P.fast ? d * 4 : d; + if (P.ph === LAND && P.pt >= LAND_T) (P.ph = FOLD), (P.pt = 0), (P.fk = P.k3); + else if (P.ph === REST && (P.pt >= REST_T || P.fast)) (P.ph = FOLD), (P.pt = 0), (P.fk = P.k3); + else if (P.ph === FOLD && P.pt >= FOLD_T) { + P.folds.push(P.fk); + P.pt = 0; + P.fast = false; + P.ph = P.folds.length >= foldsFor(P.fk) ? SETTLE : REST; + } else if (P.ph === SETTLE && P.pt >= SETTLE_T) { + // Fresh cream: the folds are forgotten and new flakes fall in. + P.round++; + P.folds = []; + P.ph = LAND; + P.pt = 0; + scatter(P); + } + P.p = P.ph === FOLD ? sm(P.pt / FOLD_T) : 0; +} + +// Sub-samples down each LED: the folds pack the layers thinner than an LED. +const SUB = 4; +// The sliding layer's end is rounded like a rolled lip, LIP LEDs deep. +const LIP = 4.5; + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const { GC, GI, GX, GY, hgt, surf, gcov, gfx, gfy, gfl, hole, lipd, ob, pal, folds, land } = P; + if (P.lastHue !== P.hue) { + // The cream's tint. Round blush, peach, ivory and pistachio it stays + // pale and light, where a deeper cream reads beige or khaki beside the + // gold. + let h = P.hue + 0.11; + h -= floor(h); + const warm = sm((h + 0.03) / 0.07) * (1 - sm((h - 0.22) / 0.08)); + hsv(h, 0.27 - 0.13 * warm, 0.93 + 0.03 * warm, pal, 0); + // The same cream in its folds' shade: a little deeper, never grey. + hsv(h - 0.01, 0.36 - 0.14 * warm, 0.74 + 0.1 * warm, pal, 3); + // The cream down in the gap under a layer still sliding over. + hsv(h - 0.01, 0.31 - 0.15 * warm, 0.82 + 0.07 * warm, pal, 6); + P.lastHue = P.hue; + } + const c = P.c, L = 1 + (P.fk - 1) * P.p, nF = folds.length, folding = P.ph === FOLD; + // Mid-fold the slab's far end stands inside the panel. + const open = folding && L - floor(L) > 0.001; + // Settling: the layers' relief and the leaf fade away and the cream lies + // smooth; then the new flakes land one by one and the relief comes back. + const settle = P.ph === SETTLE ? sm(P.pt / (SETTLE_T * 0.8)) : 0; + const keep = P.ph === LAND ? sm(P.pt / LAND_T) : 1 - settle; + // How deep the groove between layers is for each fold, newest first: the + // newest folds show, the older ones knit together. + const amp = []; + let th = H / L; + amp.push(folding ? 2 * min(1.6, 0.1 * th) * sm(P.p * 3) : 0); + for (let i = nF - 1; i >= 0; i--) { + th /= folds[i]; + amp.push(2 * min(1.6, 0.1 * th) * 0.62 ** (nF - 1 - i + P.p)); + } + const landT = P.ph === LAND ? P.pt : 99; + + for (let x = 0; x < W; x++) { + const ux = (x + 0.5) / W; + // Under the lip the top of the layer below swells a little, unevenly. + const bul = (2 + 1.3 * sin(ux * 7.5 + 0.6 * c) + 0.7 * sin(ux * 17 - c)) / H; + for (let y = 0; y < H; y++) { + let g = 0, gx = 0, gy = 0, fl = 0, hh = 0, emp = 0, nS = 0, dl = 99; + for (let sI = 0; sI < SUB; sI++) { + let u = ux, v = (H - y - (sI + 0.5) / SUB) / H; + let rel = 0; + // The fold under way: the slab stretched to L times its length and + // as much thinner, laid back and forth across the panel from the + // bottom up, each layer the other way up from the one below. + { + let j = floor(v * L), w = v * L - j, s = j + (j & 1 ? 1 - u : u); + if (open && j + 1 > L) { + const send = L - ((1 - 2 * sqrt(w * (1 - w))) * LIP) / W; + let cv = cl((send - s) * W + 0.5, 0, 1); + if (cv < 1 && j > 0 && v - j / L < bul) { + cv = 1; + j -= 1; + w = 0.999; + s = j + (j & 1 ? 1 - u : u); + } + emp += (1 - cv) / SUB; + if (cv <= 0) { + dl = min(dl, (s - send) * W); + continue; + } + } + // Each layer is rounded across its thickness, so a groove runs + // between it and the next. + if (amp[0] > 0) rel += amp[0] * (sqrt(w * (1 - w)) - 0.39); + u = s / L; + v = j & 1 ? 1 - w : w; + } + // The folds already made, newest first, back to the cream as it was. + for (let i = nF - 1, lv = 1; i >= 0; i--, lv++) { + const k = folds[i], j = min(k - 1, floor(v * k)), w = v * k - j, s = j + (j & 1 ? 1 - u : u); + rel += amp[lv] * (sqrt(w * (1 - w)) - 0.39); + u = s / k; + v = j & 1 ? 1 - w : w; + } + // The cream's own soft relief, drawn out with it, read once an LED. + if (nS === 0) hh += 0.55 * SUB * tap(u * 40, v * 64 + 17); + hh += rel; + nS++; + // The leaf. + const fu = cl(u * RU - 0.5, 0, RU - 1.001), fv = cl(v * RV - 0.5, 0, RV - 1.001); + const i0 = floor(fu), j0 = floor(fv), q = j0 * RU + i0; + const c00 = GC[q], c10 = GC[q + 1], c01 = GC[q + RU], c11 = GC[q + RU + 1]; + if (c00 + c10 + c01 + c11 > 0) { + const tx = fu - i0, ty = fv - j0; + let cc = c00 + (c10 - c00) * tx + (c01 - c00) * ty + (c00 - c10 - c01 + c11) * tx * ty; + const qq = ty < 0.5 ? (tx < 0.5 ? q : q + 1) : tx < 0.5 ? q + RU : q + RU + 1, id = GI[qq]; + if (id >= 0 && landT < 99) { + // A flake that is landing grows in with a flash. + const t = (landT - land[id]) / 0.18; + cc *= sm(t); + fl = max(fl, t > 0 ? exp(-((t - 1) ** 2) * 1.5) : 0); + } + g += cc; + gx += GX[qq] * cc; + gy += GY[qq] * cc; + } + } + const p = y * W + x; + hgt[p] = nS ? (hh / nS) * keep : 0; + gcov[p] = nS ? g / nS : 0; + gfx[p] = g > 0 ? gx / g : 0; + gfy[p] = g > 0 ? gy / g : 0; + gfl[p] = fl; + hole[p] = emp; + lipd[p] = dl; + } + } + // The gap, softened over a few LEDs: the cream rounds down into it. + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let a = 0; + for (let d = -2; d <= 2; d++) a += hole[y * W + cl(x + d, 0, W - 1)] * (3 - abs(d)); + surf[y * W + x] = a / 9; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let a = 0; + for (let d = -2; d <= 2; d++) a += surf[cl(y + d, 0, H - 1) * W + x] * (3 - abs(d)); + ob[y * W + x] = a / 9; + } + for (let p = 0; p < N; p++) surf[p] = hgt[p] * (1 - hole[p]) - 2.6 * ob[p]; + + // A small lamp drifts over the cream; the cream answers with a soft satin + // sheen and the leaf with a sharp glint off whichever facets face it. + const Lx = 32 + 38 * sin(0.29 * c + 0.4), Ly = 64 + 58 * sin(0.21 * c + 1.1), Lz = 55; + const Ex = 32, Ey = 64, Ez = 220; + const cr = pal[0], cg = pal[1], cb = pal[2], dr = pal[3], dg = pal[4], db = pal[5], pr = pal[6], pg = pal[7], pb = pal[8]; + // The leaf fades into the cream as it settles. + const gk = 1 - settle; + for (let y = 0; y < H; y++) { + for (let x = 0; x < W; x++) { + const p = y * W + x, ow = hole[p]; + const zx = (surf[y * W + min(W - 1, x + 1)] - surf[y * W + max(0, x - 1)]) / 2; + const zy = (surf[min(H - 1, y + 1) * W + x] - surf[max(0, y - 1) * W + x]) / 2; + const nl = 1 / sqrt(zx * zx + zy * zy + 1), nx = -zx * nl, ny = -zy * nl, nz = nl; + let lx = Lx - x, ly = Ly - y, lz = Lz - surf[p]; + const li = 1 / sqrt(lx * lx + ly * ly + lz * lz); + (lx *= li), (ly *= li), (lz *= li); + let vx = Ex - x, vy = Ey - y, vz = Ez; + const vi = 1 / sqrt(vx * vx + vy * vy + vz * vz); + let hx = lx + vx * vi, hy = ly + vy * vi, hz = lz + vz * vi; + const hi = 1 / sqrt(hx * hx + hy * hy + hz * hz); + (hx *= hi), (hy *= hi), (hz *= hi); + const dif = nx * lx + ny * ly + nz * lz; + // The cream: lit against a level surface, its folds a deeper tint and + // a little shade where it rounds down into the gap. + const sh = cl(1 + 1.1 * (dif - lz) - 0.25 * ob[p], 0, 1.3); + const dk = 1 - sm(sh), lift = max(0, sh - 1); + let r = cr + (dr - cr) * dk + lift * 0.25, gg = cg + (dg - cg) * dk + lift * 0.25, b = cb + (db - cb) * dk + lift * 0.25; + const nh = max(0, nx * hx + ny * hy + nz * hz); + const n2 = nh * nh, n4 = n2 * n2, n8 = n4 * n4, n16 = n8 * n8; + const sat = 0.1 * n8 * n4 * n2 + 0.12 * n16 * n16 * n16 * n8 * n4; + r += sat; + gg += sat; + b += sat; + // The leaf: deep gold turned from the lamp, bright gold towards it, + // and a white-gold glint where a facet mirrors it. + const gc = gcov[p] * gk; + if (gc > 0.002) { + const tx = gfx[p] * 0.9, ty = gfy[p] * 0.9; + const fnx = nx + tx, fny = ny + ty, fnz = nz, fn = 1 / sqrt(fnx * fnx + fny * fny + fnz * fnz); + const fd = max(0, (fnx * lx + fny * ly + fnz * lz) * fn), fh = max(0, (fnx * hx + fny * hy + fnz * hz) * fn); + const f2 = fh * fh, f8 = f2 * f2 * f2 * f2, f32 = f8 * f8 * f8 * f8; + const lit = 0.5 + 0.62 * fd, gl = 1.3 * f32 * f8 + 0.6 * gfl[p]; + // A vein thinner than an LED still reads as a line of gold. + const a = min(1, gc * (1.25 + 0.3 * nF)); + r += (0.9 * lit + gl - r) * a; + gg += (0.62 * lit + gl * 0.92 - gg) * a; + b += (0.16 * lit + gl * 0.7 - b) * a; + } + // Down in the gap: the cream below in its own shade. + if (ow > 0) { + // The sliding layer's lip shades the cream just past it. + const gs = cl(0.92 + 0.5 * (dif - lz), 0.76, 1.05) * (0.84 + 0.16 * sm(lipd[p] / 9)); + r += (pr * gs - r) * ow; + gg += (pg * gs - gg) * ow; + b += (pb * gs - b) * ow; + } + 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/sheet-mask.js b/incoming/pattern-study/patterns/sheet-mask.js new file mode 100644 index 000000000..ab54681ac --- /dev/null +++ b/incoming/pattern-study/patterns/sheet-mask.js @@ -0,0 +1,426 @@ +// ===== Patternflow pattern ===== +// Title: Sheet Mask +// Family: cosmetic +// Idea: A serum-soaked sheet mask with a fine fibre texture is laid over a pearly skin-toned ground from the top of the panel down, wetting through to a translucent tint as it touches and trapping air in pillowy white blisters with a bright rim, until an unseen palm sweeps down and squeezes every blister sideways out at the panel's edges, leaving the sheet flat and glossy with the serum's sheen sliding under a moving light, and then the sheet lifts away and a new one comes down. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 35, after a cellulose sheet mask smoothed onto the face; the reverse of Peel-Off +// Round: 35 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Bubbles=6..20, Weave=0..1 +// +// Knob 1: Hue. The serum's tint in the wet sheet, once round the wheel from aqua at 0: periwinkle at 0.1, orchid at 0.3, rose at 0.45, peach at 0.58, nude at 0.63, butter at 0.7, spring green at 0.78, mint at 0.9. From coral round to lime the tint is paler and the wet sheet lighter, because at the aqua's depth those hues read as tan and olive. +// Knob 2: Speed. At 2 the sheet's hem reaches the bottom in 1.7 seconds and the sheet has soaked through by 2.6; the blisters sit for 1, the palm takes 4.2 to sweep down, the flat sheet shines for 3, lifts off in 1.4 and the skin shows bare for 0.7, 13 seconds a round. At 0.2 all of it runs ten times slower. +// Knob 3: Bubbles. How much air the sheet traps as it settles: 6 blisters 7 to 16 LEDs across at the opening, up to 20 of them 10 to 23 across, and the more air, the more milky half-touching patches the sheet shows until the palm presses them clear. +// Knob 4: Weave. The fabric. At 0 a fine nonwoven of cellulose fibres 3 to 5 LEDs long lying every which way; towards 1 the fibres grow to 11 to 21 LEDs, stand out more and line up across and along the panel into an open cotton gauze. +// Buttons: K1 tap presses the whole sheet down at once: the palm runs down the panel in 0.8 seconds and squirts every blister out at the sides. On a sheet already flat it lifts it off and lays a new one. + +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); +const sm = (v) => ((v = cl(v, 0, 1)), v * v * (3 - 2 * v)); +const hash = (i) => { + let h = Math.imul(i ^ 0x2c1b3c6d, 0x297a2d39); + h = Math.imul(h ^ (h >>> 15), 0x85ebca6b); + return ((h ^ (h >>> 13)) >>> 0) / 4294967296; +}; +// A generator of its own for the fabric, so turning Weave changes only the +// fabric and never where the next round's blisters land. +function rng(seed) { + let a = seed >>> 0; + return () => { + a = (a + 0x6d2b79f5) >>> 0; + let t = Math.imul(a ^ (a >>> 15), a | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +// Smooth noise in a 128 x 128 tile that wraps, octaves of 8, 16 and 32 +// cells: the skin's contour under the sheet and the mottle of the fabric. +const T = 128, TM = T - 1, TEX = new Float32Array(T * T); +function buildTex() { + 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 * 13 + o * 7919) * 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); +} + +// The fabric, one value an LED round 0.5, wrapping both ways: a cloudy +// mottle and short fibres laid in it. Weave makes the fibres longer and +// thicker and turns them towards the two directions of a woven gauze. +function weave(P, k) { + const F = P.F, R = rng(90731), s = 1 + 3 * k, align = sm((k - 0.15) / 0.75), wd = 0.6 + 0.35 * (s - 1); + for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) F[y * W + x] = 0.5 * tap((x * 2) / s + 11, (y * 2) / s + 5); + const splat = (fx, fy, a) => { + const ix = floor(fx), iy = floor(fy), tx = fx - ix, ty = fy - iy; + const x0 = ((ix % W) + W) % W, x1 = (x0 + 1) % W, y0 = ((iy % H) + H) % H, y1 = (y0 + 1) % H; + F[y0 * W + x0] += a * (1 - tx) * (1 - ty); + F[y0 * W + x1] += a * tx * (1 - ty); + F[y1 * W + x0] += a * (1 - tx) * ty; + F[y1 * W + x1] += a * tx * ty; + }; + const n = floor(1500 / s); + for (let i = 0; i < n; i++) { + const x0 = R() * W, y0 = R() * H, len = (2.8 + 2.4 * R()) * s; + let th = R() * PI; + if (R() < align) th = (R() < 0.5 ? 0 : PI / 2) + (R() - 0.5) * 0.22; + const a = 0.3 + 0.7 * R(), dx = cos(th), dy = sin(th); + for (let w = -wd / 2; w <= wd / 2 + 1e-6; w += 0.7) + for (let t = -len / 2; t <= len / 2; t += 0.5) splat(x0 + dx * t - dy * w, y0 + dy * t + dx * w, a * 0.5); + } + let m = 0, q = 0; + for (let p = 0; p < N; p++) m += F[p]; + m /= N; + for (let p = 0; p < N; p++) q += (F[p] - m) ** 2; + const sd = sqrt(q / N) || 1; + for (let p = 0; p < N; p++) F[p] = cl(0.5 + (0.22 * (F[p] - m)) / sd, 0, 1); + P.weaveK = k; +} + +// The round: the sheet comes down, settles, the palm smooths it, it shines, +// it lifts off, the skin shows bare. Durations in seconds at Speed 2. +const DESC = 0, HOLD = 1, SMOOTH = 2, GLOSS = 3, LIFT = 4, BARE = 5; +const DUR = [2.6, 1.0, 4.2, 3.0, 1.4, 0.7]; +// The palm's edge is a bow, its middle A rows ahead of its ends. +const A = 22, MAXB = 20; + +// Where each blister of round n would be trapped, its size and shape. All +// twenty are placed every round, so Bubbles only says how many of them are +// trapped and how big they swell, and the first six stay where they were. +function trap(P) { + const n = P.round, b = P.bub; + for (let i = 0; i < MAXB; i++) { + let x = 0, y = 0, r = 0; + for (let tr = 0; tr < 10; tr++) { + const s = n * 7919 + i * 131 + tr * 17; + x = 7 + 50 * hash(s); + y = 10 + 108 * hash(s + 1); + r = 3.6 + 4.6 * hash(s + 2) ** 1.5; + let ok = true; + for (let j = 0; j < i && ok; j++) if ((b[j].x - x) ** 2 + ((b[j].y - y) / 1.1) ** 2 < ((b[j].r + r) * 1.2 + 2) ** 2) ok = false; + if (ok) break; + } + const e = 0.8 + 0.45 * hash(n * 7919 + i * 131 + 999); + Object.assign(b[i], { x, y, r, rx: r * e, ry: r / e, h: 0.4 + 0.2 * r, age: -1, alive: true, side: hash(n * 31 + i) < 0.5 ? -1 : 1 }); + } + // Wrinkles the sheet lays down with: a few long shallow ridges of air. + P.wr = []; + for (let i = 0; i < 2; i++) { + // Down the sheet, as it hung, one in each half. + const s = n * 4447 + i * 59, th = PI / 2 + (hash(s) - 0.5) * 0.9; + P.wr.push({ x: 8 + 20 * hash(s + 1) + 28 * i, y: 30 + 68 * hash(s + 2), tx: cos(th), ty: sin(th), len: 14 + 12 * hash(s + 3) }); + } +} + +export function setup(params) { + buildTex(); + const bub = []; + for (let i = 0; i < MAXB; i++) bub.push({}); + params.s = { + c: 0, hue: 0, speed: 2, nb: 6, grow: 0, gs: 1, k4: 0.06, weaveK: -1, + ph: DESC, pt: 0, round: 0, front: -40, fast: false, + bub, wr: [], F: new Float32Array(N), Z: new Float32Array(N), AIR: new Float32Array(N), RIM: new Float32Array(N), SHD: new Float32Array(N), + SQ: new Float32Array(N), SL: new Float32Array(N), + tint: new Float32Array(3), warm: 0, lastHue: NaN, + }; + // The skin's pearl, laid once: where its sheen sits on the colour wheel + // and how light it is. + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + params.s.SQ[y * W + x] = 0.5 * tap(x * 0.4 + 7, y * 0.4) + (x * 0.6 + y) / 170; + params.s.SL[y * W + x] = 0.86 + 0.03 * tap(x * 1.3 + 50, y * 1.3); + } + trap(params.s); +} + +// The sheet's hem and the line where it has wet through to the skin, as it +// comes down: the hem falls fast and slows, the wet line follows. +const hemAt = (q) => -4 + 140 * (1 - (1 - min(1, q / 0.65)) ** 2); +const wetAt = (q) => -10 + 146 * sm(q); + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 6, 0.06], d0 = cl(dt || 0, 0, 0.1); + P.hue = k[0]; + P.speed = cl(k[1], 0.2, 3); + P.nb = floor(cl(k[2], 6, 20) + 0.5); + P.grow = (P.nb - 6) / 14; + P.gs = 1 + 0.4 * P.grow; + P.k4 = cl(k[3], 0, 1); + if (abs(P.k4 - P.weaveK) > 0.004) weave(P, P.k4); + const d = (d0 * P.speed) / 2; + P.c += d; + P.pt += d; + + const tap1 = input.btnPressed && input.btnPressed[0]; + if (tap1) { + if (P.ph === DESC || P.ph === HOLD) { + // Blisters the wet line had not reached yet are never trapped. + for (const b of P.bub) if (b.age < 0) b.alive = false; + (P.ph = SMOOTH), (P.pt = 0), (P.fast = true), (P.front = -A - 10); + } else if (P.ph === SMOOTH) P.fast = true; + else if (P.ph === GLOSS) (P.ph = LIFT), (P.pt = 0); + } + // Blisters beyond the count asked for are never trapped; once the palm is + // on the sheet the set is what it is. + if (P.ph === DESC || P.ph === HOLD) for (let i = 0; i < MAXB; i++) P.bub[i].alive = i < P.nb; + if (P.ph === SMOOTH) { + P.front = P.fast ? P.front + d * 220 : -A - 10 + (H + 2 * A + 30) * sm(P.pt / DUR[SMOOTH]) ** 0.9; + // The palm pushes each blister it reaches ahead of it and out towards + // the nearer side, faster the further out it already is; squeezed, the + // blister flattens along the palm's edge and loses a little air. + for (const b of P.bub) { + if (!b.alive) continue; + const u = (b.x - 31.5) / 32, yf = P.front - A * u * u, top = b.y - b.ry * P.gs * 0.55; + if (yf > top) { + const dy = yf - top; + const side = abs(u) < 0.08 ? b.side : u < 0 ? -1 : 1; + b.y += dy; + b.x += side * dy * (0.9 + 1.8 * abs(u)); + b.ry = max(b.r * 0.55, b.ry * (1 - 0.012 * dy)); + b.rx = min(b.r * 1.5, b.rx * (1 + 0.008 * dy)); + b.h *= 1 - 0.006 * dy; + } + if (b.x - b.rx * P.gs * 0.3 > W || b.x + b.rx * P.gs * 0.3 < 0 || b.y - b.ry * P.gs > H) b.alive = false; + } + if (P.front >= H + A + 19.9) (P.ph = GLOSS), (P.pt = 0), (P.fast = false); + } + if (P.ph !== SMOOTH && P.pt >= DUR[P.ph]) { + P.pt = 0; + if (P.ph === BARE) { + P.round++; + trap(P); + P.ph = DESC; + } else P.ph = P.ph === HOLD ? ((P.front = -A - 10), SMOOTH) : P.ph + 1; + } + // A blister fills as the wet line passes it, with a little overshoot. + if (P.ph === DESC || P.ph === HOLD) { + const wl = P.ph === DESC ? wetAt(P.pt / DUR[DESC]) : 999; + for (const b of P.bub) if (b.alive && b.age < 0 && wl > b.y - b.ry * P.gs * 0.3) b.age = 0; + } + for (const b of P.bub) if (b.age >= 0) b.age += d; +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const { F, Z, AIR, RIM, SHD, SQ, SL, tint } = P, c = P.c, ph = P.ph, q = P.pt / DUR[ph]; + if (P.lastHue !== P.hue) { + // From coral through peach and butter to lime the tint is paler and the + // wet sheet lighter: at the aqua's depth those hues read tan and olive. + let h = P.hue + 0.5; + h -= floor(h); + P.warm = sm((h + 0.02) / 0.07) * (1 - sm((h - 0.21) / 0.09)); + hsv(h, 0.56 - 0.28 * P.warm, 1, tint); + P.lastHue = P.hue; + } + const warm = P.warm; + + // Where the sheet is: below its hem there is only skin while it comes + // down, and behind the wet line it has soaked through. + const hem = ph === DESC ? hemAt(q) : ph === BARE ? -99 : 999; + const wl = ph === DESC ? wetAt(q) : ph >= BARE ? -99 : 999; + // Lifting: in the first third air runs in under the sheet from its edges + // and it goes white, then it rises off and fades. + const air = ph === LIFT ? sm(q / 0.32) : 0, fade = ph === LIFT ? sm((q - 0.3) / 0.7) : ph === BARE ? 1 : 0; + const rise = ph === LIFT ? 9 * sm((q - 0.25) / 0.75) : 0; + const pressedAll = ph === GLOSS || ph === LIFT; + // A small lamp drifts over the panel; the wet sheet mirrors it as a spot + // that the fibres break into glints and the blisters bend round. + const Lx = 32 + 40 * sin(0.37 * c + 0.5), Ly = 64 + 62 * sin(0.23 * c + 2), Lz = 45; + const Ex = 32, Ey = 70, Ez = 240; + + // Heights: the skin's soft contour, the fibres standing a little, the + // wrinkles of a sheet not yet pressed, and the blisters. + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, fy = (((y + floor(rise)) % H) + H) % H; + Z[p] = 1.1 * tap(x * 0.5, y * 0.5 + 40) + (0.22 + 0.3 * P.k4) * (F[fy * W + x] - 0.5); + AIR[p] = 0; + RIM[p] = 0; + SHD[p] = 0; + } + if (ph <= SMOOTH) { + for (const w of P.wr) { + const ex = abs(w.tx) * w.len + 8, ey = abs(w.ty) * w.len + 8; + for (let y = max(0, floor(w.y - ey)); y < min(H, w.y + ey); y++) { + for (let x = max(0, floor(w.x - ex)); x < min(W, w.x + ex); x++) { + const dx = x - w.x, dy = y - w.y, a = dx * w.tx + dy * w.ty, n = -dx * w.ty + dy * w.tx; + if (abs(n) > 7 || abs(a) > w.len) continue; + // The palm presses a wrinkle flat as it passes. + const u = (x - 31.5) / 32, flat = ph === SMOOTH ? 1 - sm((P.front - A * u * u - y) / 2.5) : 1; + const v = exp(-((n / 3) ** 2)) * sm((w.len - abs(a)) / 9) * flat; + const p = y * W + x; + Z[p] += 0.45 * v; + AIR[p] = max(AIR[p], 0.25 * v); + } + } + } + for (const b of P.bub) { + if (!b.alive || b.age < 0) continue; + // Filling: a springy swell to full size in about a third of a second. + const f = 1 - exp(-b.age / 0.09) * cos(b.age * 11), br = 1 + (ph === HOLD ? 0.03 * sin(c * 4 + b.r) : 0); + const rx = b.rx * P.gs * min(1.08, f) * br, ry = b.ry * P.gs * min(1.08, f) * br, hh = b.h * P.gs * f, rm = min(rx, ry); + let tlx = Lx - b.x, tly = Ly - b.y; + const tl = 1 / (sqrt(tlx * tlx + tly * tly) || 1); + (tlx *= tl), (tly *= tl); + const x0 = max(0, floor(b.x - rx - 4)), x1 = min(W - 1, floor(b.x + rx + 4)); + const y0 = max(0, floor(b.y - ry - 4)), y1 = min(H - 1, floor(b.y + ry + 4)); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + const ex = (x - b.x) / rx, ey = (y - b.y) / ry, dd = ex * ex + ey * ey, p = y * W + x, dr = sqrt(dd); + const face = dr > 0 ? (ex * tlx + ey * tly) / dr : 0; + if (dd < 1) { + // A pillow, rounder on top than a dome. + Z[p] += hh * sm((1 - dd) / 0.75); + AIR[p] = max(AIR[p], sm((1 - dr) / 0.3)); + } + // A bright rim where the sheet lifts off the skin, brightest on + // the lamp's side, and a soft shadow on the skin past the far side. + RIM[p] = max(RIM[p], exp(-((((dr - 0.92) * rm) / 0.8) ** 2)) * (0.3 + 0.7 * max(0, face))); + if (dr > 0.9) SHD[p] = max(SHD[p], exp(-((((dr - 1) * rm) / 1.8) ** 2)) * max(0, -face)); + } + } + } + + const tr = tint[0], tg = tint[1], tb = tint[2]; + const wetV = 0.88 + 0.09 * warm, mixT = 0.55 - 0.08 * warm; + const front = P.front; + for (let y = 0; y < H; y++) { + for (let x = 0; x < W; x++) { + const p = y * W + x; + const zx = (Z[y * W + min(W - 1, x + 1)] - Z[y * W + max(0, x - 1)]) / 2; + const zy = (Z[min(H - 1, y + 1) * W + x] - Z[max(0, y - 1) * W + x]) / 2; + const nl = 1 / sqrt(zx * zx + zy * zy + 1), nx = -zx * nl, ny = -zy * nl, nz = nl; + let lx = Lx - x, ly = Ly - y, lz = Lz - Z[p]; + const li = 1 / sqrt(lx * lx + ly * ly + lz * lz); + (lx *= li), (ly *= li), (lz *= li); + let vx = Ex - x, vy = Ey - y, vz = Ez; + const vi = 1 / sqrt(vx * vx + vy * vy + vz * vz); + (vx *= vi), (vy *= vi), (vz *= vi); + let hx = lx + vx, hy = ly + vy, hz = lz + vz; + const hi = 1 / sqrt(hx * hx + hy * hy + hz * hz); + const nh = max(0, (nx * hx + ny * hy + nz * hz) * hi); + // Shading against what a level sheet would get, so only slopes show. + const shade = cl(1 + (nx * lx + ny * ly + nz * lz - lz), 0.55, 1.35); + + // The skin: a warm pearl whose sheen turns faintly pink, lilac and + // mint across the panel. + const qq = SQ[p] + c * 0.03, Ls = SL[p]; + let sr = (0.98 + 0.03 * cos(6.283 * qq)) * Ls, sg = (0.85 + 0.03 * cos(6.283 * (qq - 0.33))) * Ls, sb = (0.79 + 0.04 * cos(6.283 * (qq - 0.67))) * Ls; + + // How far the sheet has soaked onto the skin here, 0 (air under it) + // to 1 (wet through), and how far the palm has pressed it. + const u = (x - 31.5) / 32, ai = AIR[p]; + let contact = 1, pressed = pressedAll ? 1 : 0; + if (ph === DESC) contact = sm((wl + 3 * sin(0.21 * x + 3 * c) + 2 * tap(x * 1.7, 9) - y) / 5); + if (ph <= SMOOTH) { + const yf = front - A * u * u; + pressed = ph === SMOOTH ? sm((yf - y) / 2.5) : 0; + // Not yet pressed, the sheet only half touches in places. + const part = 0.76 - 0.26 * P.grow + (0.24 + 0.26 * P.grow) * sm(0.5 + 1.6 * tap(x * 1.2 + 90, y * 1.2 + 30)); + contact *= part + (1 - part) * pressed; + contact *= 1 - ai * (1 - pressed); + } + if (ph === LIFT) { + const ed = (0.6 * min(x + 0.5, W - 0.5 - x, (y + 0.5) / 2, (H - 0.5 - y) / 2)) / 32 + 0.4 * sm(0.5 + 1.4 * tap(x * 0.9 + 3, y * 0.9 + 70)); + contact *= 1 - sm((air * 1.1 - ed) / 0.08); + } + const fib = F[((((y + floor(rise)) % H) + H) % H) * W + x] - 0.5; + + // Wet through: the skin seen through the serum's tint, darker as + // anything wet is. Over air: a hanging sheet is near white with its + // fibres showing; a blister is the soaked sheet lit from inside, a + // pale clear tint. + const fk = 1 + 0.6 * P.k4, at = 0.08 + 0.42 * ai, af = (0.93 + 0.02 * ai) * (1 + 0.18 * fk * fib); + const ar = (1 - at + tr * at) * af, ag = (1 - at + tg * at) * af, ab = (1 - at + tb * at) * af; + const wk = wetV * (1 - 0.06 * pressed) * (1 + 0.1 * fk * fib); + const wr = (sr * (1 - mixT) + tr * mixT) * wk, wg = (sg * (1 - mixT) + tg * mixT) * wk, wb = (sb * (1 - mixT) + tb * mixT) * wk; + let r = ar + (wr - ar) * contact, g = ag + (wg - ag) * contact, b = ab + (wb - ab) * contact; + const sd = 1 - 0.2 * SHD[p] * (1 - pressed); + r *= shade * sd; + g *= shade * sd; + b *= shade * sd; + // The sheen: broad and soft on the half-dry sheet, tighter and much + // brighter where the palm has pressed the serum through. + const ks = 0.1 + 0.22 * contact + 0.22 * pressed, shin = 12 + 20 * contact + 110 * pressed; + // The blister's skin is wet serum too: a small sharp spot on each one. + let sp = ks * nh ** shin + 0.1 * pressed * nh ** 24 + 0.55 * ai * nh ** 70 + 0.3 * RIM[p] * (1 - pressed); + // The palm's edge pushes a bead of serum ahead of it: a bright line. + if (ph === SMOOTH) { + const e = y - (front - A * u * u); + sp += (0.18 + 0.1 * fib) * exp(-(((e - 1) / 1.1) ** 2)) + 0.08 * exp(-(((e - 3) / 1.8) ** 2)); + } + r += sp * (0.92 + 0.08 * tr); + g += sp * (0.92 + 0.08 * tg); + b += sp * (0.92 + 0.08 * tb); + + // Over the skin: the sheet is there above its hem and not yet lifted. + let have = 1 - fade; + if (ph === DESC) { + const hy = hem + 1.5 * sin(0.15 * x + 1.3 * c); + have = sm(hy - y + 0.5); + // The hanging sheet: drape folds running down from the wet line, + // and its shadow on the skin below the hem. + if (y > wl && y < hy) { + const fold = 0.045 * sin(x * 0.42 + 2.2 * sin(y * 0.05 + c) + 1.5 * tap(x * 0.5, y * 0.2)) * sm((y - wl) / 12); + r *= 1 + fold; + g *= 1 + fold; + b *= 1 + fold; + } + const sh = 1 - 0.28 * exp(-max(0, y - hy) / 3.5) * sm(y - hy + 0.5); + (sr *= sh), (sg *= sh), (sb *= sh); + // The cut edge of the fabric catches the light. + const ed = 0.22 * exp(-(((y - hy + 0.8) / 0.7) ** 2)); + (r += ed), (g += ed), (b += ed); + } + if (ph === LIFT) { + // The lifted sheet's shadow on the skin, softening as it rises. + const sh = 1 - 0.22 * (1 - fade) * air; + (sr *= sh), (sg *= sh), (sb *= sh); + } + // Bare skin keeps a film of serum that shines and slowly soaks in. + const dew = ph === LIFT ? sm((q - 0.2) / 0.5) : ph === BARE ? 1 - 0.6 * q : 0; + if (dew > 0) { + const ds = dew * 0.4 * nh ** 70; + (sr += ds), (sg += ds), (sb += ds * 0.96); + } + r = sr + (r - sr) * have; + g = sg + (g - sg) * have; + b = sb + (b - sb) * have; + display.setPixel(x, y, cl(r * 255, 0, 255), cl(g * 255, 0, 255), cl(b * 255, 0, 255)); + } + } +} From 9557c16b2915353834c6ab2d0d56fa6544febc3b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 17:48:17 +0000 Subject: [PATCH 7/9] =?UTF-8?q?=EC=97=B0=EA=B5=AC=20=EB=85=B8=ED=8A=B8?= =?UTF-8?q?=EC=97=90=2035=ED=8C=90=20=ED=91=9C=EC=99=80=20=ED=8C=90=20?= =?UTF-8?q?=EC=A0=88,=20=EB=8B=A4=EC=9D=8C=20=ED=8C=90=20=EA=B0=90?= =?UTF-8?q?=EC=9D=84=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_01SW1UfDNBr6xKa7hi5BogQV --- incoming/pattern-study/README.md | 83 +++++++++++++++++-- .../patterns/joukowsky-airfoil.js | 6 +- .../pattern-study/patterns/renyi-parking.js | 6 +- 3 files changed, 83 insertions(+), 12 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index a72dca02a..767117f00 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 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 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 248개를 쉰다섯 갈래로 나눠 만들었어요. 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판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 35판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. [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판에서 세로로 다시 짰어요. @@ -399,6 +399,21 @@ | [Penrose Room](patterns/penrose-room.js) | 물리·스프링 | 거울로 된 펜로즈의 방에서 빛 한 점이 떠돌고, 수백 개의 광선이 반사하며 복도와 위아래 반타원을 밝히는 동안 양옆 방은 캄캄해요. 빛이 반타원을 지나 옆방으로 건너가면 밝은 곳과 어두운 곳이 뒤바뀌어요. K3 은 복도 벽을 초점에서 4 LED 까지 옮겨 어둠에 빛이 새게 하고, K1 을 누르면 빛이 바로 건너가요. | | [Sorting Network](patterns/sorting-network.js) | 선·궤적 | 실 열여섯 가닥이 이웃끼리 한 쌍씩 자리를 바꿔 128줄 만에 순서가 거꾸로 되고, 다음 네트워크가 이어받아 실마다 패널을 사인 곡선으로 가로질러요. 균일한 무작위 정렬 네트워크라 바꾼 자리를 찍은 점이 가운데로 반원 꼴로 몰려요. K3 은 실을 32가닥까지 늘리고, K1 을 누르면 실 하나만 8초 동안 밝혀 그 곡선을 보여 줘요. | +### 35판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Bath Fizz](patterns/bath-fizz.js) | 화장품 팝업 | 패널 아래에서 녹는 배스 파우더가 깊은 물속으로 안료 구름을 올려 보내요. 분홍에서 색상환을 따라 벌려 놓은 안료 세 가지가 버섯 머리로 말리며 128줄을 채우고 맞닿은 가장자리에서 섞여요. 가는 거품 줄기가 그 사이를 뚫고 올라가며 물을 끌어올려요. K3 은 거품을 1초에 60개에서 360개로 늘려 첫 구름이 위 20줄에 닿는 때를 7.6초에서 2.4초로 당기고, K1 을 누르면 세 색이 바닥에서 한꺼번에 터져요. | +| [Chimera State](patterns/chimera-state.js) | 리듬·펄스 | 똑같은 위상 진동자 256개가 고리에 앉아 서로 당기는데, 고리가 발맞춰 도는 30% 와 위상이 흩어진 나머지로 갈라져요. 위 20줄은 진동자마다 위상을 제자리에 점으로 찍어 매끈한 호 옆에 흩뿌린 점을 보이고, 그 아래로 한 줄이 한 순간이라 가운데로 하늘색 띠가 곧게 흐르고 양옆은 한 바퀴씩 뒤처지는 진동자의 어두운 획으로 채워져요. K3 은 위상 지연을 80도에서 88도로 돌려 발맞춘 띠를 19열에서 9열로 좁히고, K1 을 누르면 새 무작위 출발에서 키메라가 다시 자라요. | +| [Constant Width](patterns/constant-width.js) | 기하·대칭 | 너비 62 LED 의 통로를 폭이 일정한 곡선이 위아래로 맞닿아 쌓인 채 내려가요. 뢸로 삼각형과 오각형, 칠각형, 홀수 배음으로 지은 매끈한 곡선과 원이 저마다 다른 빠르기로 도는데도 늘 두 벽과 위아래 경계선에 닿고, 닿은 자리마다 흰 점이 서요. K3 은 곡선마다 같은 너비의 원을 섞어 둘레 194.8 은 그대로 두고 삼각형의 넓이 2709 LED 를 원의 3019 까지 부풀리고, K1 을 누르면 꼭짓점이 지나간 길이 금색으로 남아 삼각형이 귀만 둥근 정사각형을 그려요. | +| [Gilded Cream](patterns/gilded-cream.js) | 화장품 팝업 | 금박 조각 여섯 개가 든 아이보리 크림을 옆에서 본 단면이 제빵사의 반죽처럼 두 배로 늘어나 제 위로 접혀요. 접을 때마다 조각이 길게 늘어 금빛 결이 되고, 다섯 번 접으면 서른두 겹의 가는 금선이 반짝임만 남기고 크림에 녹아든 뒤 새 조각이 떨어져요. K3 은 한 번에 세 겹이나 네 겹으로 접게 하고, K1 은 크림을 색상환 한 바퀴로 물들이며 누르면 한 번을 네 배 빠르게 접어요. | +| [Joukowsky Airfoil](patterns/joukowsky-airfoil.js) | 물리·스프링 | 주콥스키 사상으로 원을 지나는 이상 흐름을 날개를 지나는 흐름으로 옮겼어요. 44줄 길이의 날개 위로 1초마다 맨 윗줄에서 놓은 염료 줄이 내려와 앞전의 붉은 정체점에서 갈라지고, 오른쪽 흡입면으로 간 반쪽이 뒷전에 12.6줄 먼저 닿아요. K3 은 받음각을 0도에서 15도로 올려 그 앞섬을 46줄까지 늘리고, K1 을 누르면 순환을 10초 동안 빼서 두 반쪽이 거의 같이 닿아요. | +| [Kari-Culik Tiles](patterns/kari-culik.js) | 알고리즘 | 카리의 왕 타일 14장이 되풀이 없이 판을 덮어요. 한 줄이 x 를 2 나 2/3 로 곱하는 긴 곱셈이라 위아래 변에는 비티 숫자가 파랑 마름모로, 옆 변에는 올림수가 초록이나 보라에서 금색까지의 마름모로 이어져요. K3 은 맨 윗줄의 x 를 바꿔 다른 타일링을 깔고, K1 을 누르면 3초 동안 타일이 4분의 1 칸씩 떨어져 한 장씩 보여요. | +| [Kolakoski](patterns/kolakoski.js) | 수론 | 제 런 길이 부호와 같은 1 과 2 의 수열이 열 단의 고드름으로 매달려요. 맨 아래 단은 3,000번 항부터 64항이 LED 하나씩 금색 1 과 보라 2 로 서고, 그 위 단마다 아래 단의 런을 만든 항이 런 너비로 앉아 같은 수열이 단마다 3분의 2 로 줄며 왼쪽으로 한 항씩 흘러요. K3 은 단을 16 까지 늘리고, K1 을 누르면 창이 10만 항 뒤로 건너뛰어 맨 위 눈금이 1 이 4 개 앞선 자리에서 2 가 46 개 앞선 자리로 옮겨요. | +| [Rényi Parking](patterns/renyi-parking.js) | 데이터 그림 | 128줄이 저마다 60 LED 길이의 거리이고, 6 LED 짜리 차가 아무 데나 와서 들어갈 틈이 있을 때만 서요. 거리는 위에서 빈 채로 들어와 내려가며 차고, 바닥에 닿으면 차 길이만 한 틈이 하나도 없이 72% 쯤 막혀요. 먼저 온 차는 하늘색, 마지막 틈에 끼어든 차는 금색이며 옆 눈금이 바닥을 떠난 거리 128개의 평균을 예측값 0.722 의 금색 눈금에 대 보고, K3 은 차 길이를 6~15 LED 로 바꾸고 K1 을 누르면 차가 다 왼쪽으로 밀려 줄마다 채운 몫만 한 막대가 돼요. | +| [Sheet Mask](patterns/sheet-mask.js) | 화장품 팝업 | 세럼에 젖은 시트 마스크가 위에서 내려와 진줏빛 살결에 닿는 대로 맑은 청록으로 비치고, 공기가 갇힌 자리는 흰 공기 방울 여섯 개로 볼록 솟아요. 보이지 않는 손바닥이 4.2초 동안 위에서 아래로 쓸며 방울을 양옆 끝으로 밀어내면, 매끈해진 시트 위로 떠도는 빛 한 점이 섬유 결에 부서져 반짝여요. K3 은 방울을 20개까지 늘리고 크게 부풀리며, K1 은 색상환을 다 돌고 누르면 손바닥이 0.8초 만에 한 번에 눌러 펴요. | +| [Toner Pad](patterns/toner-pad.js) | 화장품 팝업 | 섬유 400가닥이 아무렇게나 엇갈린 흰 솜 패드에 장밋빛 토너가 3.2초마다 한 방울씩 떨어져요. 젖는 앞은 잘 빨아올리는 섬유를 따라 손가락처럼 먼저 뻗고, 젖은 곳은 반투명해져 뒤의 빛을 토너 색으로 받으며 그 안의 섬유가 어둡게 드러나요. 패치는 가장자리부터 말라 흰 솜으로 돌아가요. K3 은 한 방울이 적시는 넓이를 1800 LED 에서 5400 LED 로 늘리고, K1 을 누르면 아직 젖은 곳을 피해 한 방울이 바로 떨어져요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -1549,6 +1564,50 @@ E8 의 근 240개를 고셋의 4₂₁ 다면체 꼭짓점으로 그렸어요. 주제 6 으로 필오프 젤 마스크가 한 장으로 벗겨지는 것을 그렸어요. 막은 떨어지는 줄에서 원통으로 말리는데, 바깥으로 나온 면이 막의 안쪽이라 색이 옅고 위쪽에 흰 빛 줄이 서요. 벗기는 선에는 막이 1초쯤 붙잡혀 2.5~5.5줄 늘어났다가 0.2초 만에 곧은 자리를 지나치며 풀리는 곳을 더해서, 자로 그은 선이 되지 않아요. 움직임과 모양 바뀜은 가장 밝은 채널만 읽어서 막을 제 색의 0.6 밝기로 깊게 두고 살결은 0.8 쯤으로 밝혔고, 움직임 0.232, 모양 바뀜 0.226 이 나왔어요. 그 깊이에서는 K1 0.30 의 주황이 황갈색으로, 0.45 의 연두가 올리브로 보여서 빨강부터 연두까지는 막을 살결보다 밝게 올리고 컬 밑 그림자도 반으로 줄였어요. 처음 판은 젖은 새 막의 두께 잡음이 빛 띠를 벌레처럼 비틀어서, 잡음을 18에서 4로 줄이고 앞머리 뒤로 잦아드는 물결 줄을 따로 그렸어요. +## 35판 + +35판도 주제 5 「낯선 수학」 여섯과 주제 6 「화장품 브랜드 팝업」 넷이에요. 같이 돈 34판(#228)이 톰슨 문제와 와다의 호수, 자석 젤 네일 같은 것을 먼저 골라서, 이 판은 그와 겹치지 않게 골랐어요. 수학은 Constant Width, Rényi Parking, Kari-Culik Tiles, Kolakoski, Chimera State, Joukowsky Airfoil 이고, 화장품은 Bath Fizz, Toner Pad, Sheet Mask, Gilded Cream 이에요. 콜라코스키 수열은 28판이, 렌니의 주차 문제는 30판이 「다음 판 감」에 적어 둔 감이고, 나머지 넷은 이 판이 목록 밖에서 찾았어요. 여섯 다 있는 갈래에 들어가서 주제 2 로 연 갈래는 없어요. Kolakoski 가 수론 갈래의 여섯째예요. 화장품 넷 가운데 시트 마스크와 금박 크림은 「다음에 해 볼 질감」에 적혀 있던 감이에요. 주제 1 은 238개의 `--gaps` 가 다 `none` 이라 33판처럼 패턴이 하나뿐인 칸을 겨눴고, 34판이 겨눈 가장자리 × 채움 1,1 은 피했어요. Constant Width 가 Rainstorm 혼자이던 가장자리 × 모양 바뀜 4,3 에, Bath Fizz 가 Lava Lamp 혼자이던 가장자리 × 색 1,4 에 섰어요. Bath Fizz 는 같은 값으로 Brushstrokes 혼자이던 복잡도 × 색 4,4 와 Balloon Box 혼자이던 색 × 모양 바뀜 4,3 에도 섰어요. 패턴이 하나뿐인 칸은 238개에서 24 였고 248개에서 20 이에요. 열 개 다 어느 축의 끝도 넘지 않아서 칸 경계는 30판 그대로이고, 지도 21장이 다 `none` 이에요. 주제 4 는 Constant Width 와 Rényi Parking 이에요. Constant Width 는 높이도 늘 62 인 곡선 둘이 판을 채워 `thirds` 가 77/74/67 이고, Rényi Parking 은 거리가 위에서 비어 들어와 바닥에서 막혀서 11/34/53 이에요. + +### Constant Width + +주제 5 와 4 에 주제 1 의 가장자리 × 모양 바뀜 4,3 을 같이 겨눴어요. Rainstorm 혼자이던 칸이라 가장자리가 0.79 를 넘고 모양 바뀜이 0.217~0.331 이어야 해요. 벽 사이를 패널 너비대로 62 LED 로 두고 곡선마다 지지 함수로 왼쪽 지지선을 x = 1 에, 위 지지선을 제 칸의 윗선에 놓았더니, 너비가 어느 방향으로나 62 라서 오른쪽 벽과 아랫칸의 윗선에는 맞추지 않아도 닿았어요. 면을 한 색으로 칠하고 테두리를 면보다 49 단계 넘게 밝혀 가장자리는 처음부터 1.000 이었어요. 처음 판은 열고 27초 동안 뢸로 삼각형이 나오지 않았고 K3 반응이 0.06 이었는데, 시작 자리를 옮겨 삼각형을 가운데에 두자 모양 바뀜 0.277 에 노브 반응이 0.31, 0.24, 0.10, 0.14 가 됐고 `thirds` 는 77/74/67 이에요. 여덟 곡선을 한 밝기로 칠해도 모양 바뀜은 0.263 이라, 그 값은 곡선끼리의 밝기 차보다 정사각형 네 귀의 어둠이 곡선을 따라 도는 데서 와요. K1 로 꼭짓점의 길을 남기면 삼각형은 귀만 3 LED 둥근 정사각형을 그려 와츠 드릴이 정사각형 구멍을 뚫는 까닭을 보여 주고, 오각형은 귀가 16 LED 둥글어요. + +### Rényi Parking + +주제 5 와 4 로 「다음 판 감」의 「낯선 수학」에 적혀 있던 렌니의 주차 문제(1958)를 그렸어요. 퇴짜 맞은 차는 거리를 바꾸지 않아서, 차가 설 수 있는 자리 안에서만 고르게 뽑고 그 자리의 길이를 빠르기로 삼아도 같은 과정이에요. 예측은 첫 차가 거리를 둘로 나눈다는 적분 방정식을 차 길이의 1/2048 걸음으로 풀어 6 LED 차에서 7.2236 대, 채운 몫 0.7224 를 얻었고, 거리 4만 개를 막히게 해 본 평균은 0.7221 ± 0.0003 이었어요. 처음 판은 4 LED 차로 열어 3 LED 점선이 잡음처럼 깔렸고, 색을 0.8 까지의 몫으로 칠했는데 마지막 차가 오는 몫이 0.67 이라 금색이 하나도 없었어요. 차 길이를 6~15 로 옮기고 색을 마지막 차가 올 몫으로 나누자 바닥이 벽돌로 읽히고 늦게 온 차가 금색으로 섰어요. K1 로 차를 왼쪽에 몰면 줄마다 막대가 돼서, 오른쪽 끝의 계단이 거리가 내려가며 차는 꼴을 그리고 막힌 줄의 막대는 금색 선 양옆의 42 와 48 LED 에서 끝나요. 움직임은 1.100, 모양 바뀜은 0.066 이고 `thirds` 는 11/34/53, 노브 반응은 0.15, 0.19, 0.20, 0.22 예요. + +### Kari-Culik Tiles + +주제 5 로 카리(1996)의 왕 타일 14장을 깔았어요. 한 줄은 x 의 비티 숫자 ⌊(n+1)x⌋ − ⌊nx⌋ 를 위 변에, 2x 나 2x/3 의 숫자를 아래 변에 두고 올림수를 옆 타일로 넘기는 긴 곱셈이에요. 카리는 줄을 잇는 사상을 [1/2, 2] 에 적었지만 [1/2, 2/3) 위로는 올 줄이 없어서, 위로 끝없이 흐르는 이 그림은 [2/3, 2) 에서 돌려요. 처음에는 x 를 부동소수로 곱했다가 2/3 을 곱한 끝자리가 어긋나 올림수가 1 인 타일이 나왔고, BigInt 분수로 바꾸자 9개 출발점의 5,400줄이 정확히 14장만 썼어요. 같은 셈을 쿨릭의 3배와 1/2배 줄로 하면 혼자서도 판을 덮는 네 변 0 의 타일이 낀 12장이 나와서, 13장 판은 짓지 않았어요. 대각선을 1 LED 어둡게 두자 맞닿은 삼각형이 마름모로 이어졌고, 가장자리 1.000 에 촘촘함 0.333 으로 축 끝 0.374 아래에 들어요. 초록 올림수의 2배 줄은 늘 2/3배 줄 하나나 둘을 사이에 두는데, x 의 로그에서 이 사상이 log 3 둘레를 log 2 씩 도는 회전이라 줄의 차례가 스투름 단어여서예요. + +### Kolakoski + +주제 5 로 콜라코스키 수열(1965)을 그렸어요. 28판이 「다음 판 감」에 적어 둔 감이에요. 셋업에서 2^20 항을 28 ms 에 쓰고 항마다 그 항이 든 런의 번호를 부모로 적어 두어서, 위 단은 부모를 따라 올라가기만 해요. 항을 자손 수만큼 넓게 그리니 부모가 늘 자식 위에 서고, 창이 한 항 움직이면 열 단이 다 한 LED 씩 같이 밀려 노이즈가 아니라 한 수열로 읽혀요. 처음 판은 두 색을 평평하게 칠해 복잡도가 0.044 로 축 아래 끝 0.060 밖에 섰어요. 단마다 위는 옅고 아래는 짙게 칠하자 복잡도 0.236 이 됐고, 같은 값의 두 항 사이에만 어두운 이음매를 넣어 두 LED 이상인 항이 하나씩 읽혀요. 맨 위 두 줄의 눈금은 지금까지 1 이 2 보다 몇 개 앞섰는지 보여 주는데, 첫 백만 항에서 그 차는 −66 과 63 사이에 머물러요. + +### Chimera State + +주제 5 로 쿠라모토와 바톡토흐(2002)의 키메라 상태를 리듬·펄스 갈래에 그렸어요. 고리 꼴은 에이브럼스와 스트로가츠(2004)의 코사인 커널이라 커플링 합이 복소 합 셋으로 줄어서, 진동자 256개를 RK4 로 돌려도 한 프레임이 2 ms 안팎이에요. 그들의 출발을 80도에서 바로 돌리면 30번 가운데 6번이 65 시간 단위 안에 고리 전체가 발을 맞춰 버렸고, 연구의 씨앗에서는 처음 두 뽑기가 다 그랬어요. 뽑을 때마다 84도에서 100 시간 단위 키운 뒤 노브의 지연으로 천천히 내리자 30번 다 1000 시간 단위를 버텼어요. 처음 판은 한 열의 진동자 넷의 위상 인자를 평균해 칠했는데, 흩어진 진동자도 거의 내내 발맞춘 위상 곁에 머물다 한 바퀴를 빠르게 미끄러져서 평균이 하늘색으로 뭉개졌어요. 넷 가운데 하나만 칠하고 반 바퀴 어긋난 위상을 어둡게 하자 미끄러질 때마다 어두운 획이 생겨 두 부분이 갈렸어요. 처음 연 Stride 3.5 에서는 모양 바뀜이 0.014 로 축 끝 0.021 아래였고, 0.8 에서 열자 0.032 가 됐어요. + +### Joukowsky Airfoil + +주제 5 로 주콥스키(1910)의 날개를 물리·스프링 갈래에 그렸고, 주인공은 흐름장이 아니라 날개와 염료 줄이에요. 염료는 입자로 흘리지 않고, LED 마다 유선을 따라 맨 윗줄까지 거슬러 가며 걸린 시간을 더해 두었다가 그 시간의 위상으로 줄을 그려요. 거슬러 가는 길은 날개가 원이 되는 ζ 평면에서 걸어서 표면 밖으로 되밀기 쉽고, 한 흐름의 8192 LED 가 25 ms 안팎이라 노브를 돌리면 프레임마다 768 LED 씩 다시 셈해요. 처음 판은 줄 굵기를 2.2 LED 로 고정해서, 줄이 빽빽한 뒷전 뒤에서 겹친 줄이 하얗게 타 흰 점선이 생겼어요. 줄 사이가 좁아지면 줄도 얇아지게 하고 4 LED 보다 빽빽한 곳은 염료 몫 2.2/20 의 옅은 안개로 두자 점선이 사라졌어요. 줄 간격 16 에서는 16×8 칸 한 칸 높이와 같아 칸마다 늘 줄 하나가 들어서 모양 바뀜이 0.017 로 축 끝 0.021 아래였고, 20 으로 넓히자 0.057 이 됐어요. 받음각 0도, 휨 6% 로 열어도 흡입면 반쪽이 뒷전에 12.6줄 먼저 닿고, 15도에서는 46줄 앞서요. + +### Bath Fizz + +주제 6 과 1 로 배스 파우더가 물속에서 녹는 것을 그렸고, Lava Lamp 혼자이던 가장자리 × 색 1,4 칸을 겨눴어요. 물은 2×2 LED 격자의 유체로 풀고 안료 세 가지는 LED 마다 실어 나르는데, 거품마다 둘레의 물을 끌어올리게 하자 구름 머리가 버섯 꼴로 스스로 말렸어요. 처음 판은 구름이 아래 절반에 머물러 색이 0.134 였어요. 안료를 네 배로 늘리고 뜨는 힘이 2.4초 남게 하자 0.401 이 됐고, 묽은 안료도 밝게 칠하자 0.576 이 됐지만 이번에는 판 전체가 대리석 무늬로 덮여 물이 보이지 않았어요. 불투명도를 안료 양의 세제곱으로 걸어 묽은 색은 맑게 빠지게 하고 안료를 조금 더 늘리자 가장자리 0.096, 색 0.592 로 그 칸에 둘째로 섰어요. 세 안료는 가장 밝은 채널이 다 255 언저리라, 색이 바뀌는 이음매가 또렷해도 가장자리 셈에는 잡히지 않았어요. 주황에서 라임 사이에 든 안료는 묽어지며 카키로 가라앉아서, 그 범위에서는 묽을수록 옅은 회색 안개로 바래게 하고 갈색이 될 물은 짙은 남보라로 바꿨어요. + +### Toner Pad + +주제 6 으로 솜 패드에 토너가 스며드는 것을 그렸어요. 젖는 앞을 확산으로 풀면 섬유를 따라 빠른 곳 때문에 한 프레임을 수백 번 쪼개야 해서, 방울마다 섬유를 따라 가는 데 드는 길이를 다익스트라로 한 번 재 두고 앞이 그 값을 따라 퍼졌다가 마르게 했어요. 처음 판은 젖은 자리가 지름 10 LED 남짓이라 움직임이 0.002, 모양 바뀜이 0.010 으로 두 축의 아래 끝 밑이었어요. 길이를 LED 단위로 다시 잡자 젖은 자리에 네모난 구멍이 났는데, 힙의 키는 Float64 이고 길이는 Float32 라 반올림된 키를 지난 값으로 버린 탓이었어요. 섬유 대부분은 틈보다 조금만, 몇 가닥만 여섯 배쯤 빨리 나르게 하고 방울이 적실 넓이를 1800 LED 로 정해 앞이 닿을 길이를 거꾸로 찾자 앞이 섬유를 따라 손가락으로 뻗었고, 움직임 0.039, 모양 바뀜 0.140 이 됐어요. 젖은 섬유를 토너 색 그대로 어둡게 하면 주황과 노랑에서 갈색 줄이 되어서, 그 범위에서는 섬유를 서늘한 회색으로 보냈어요. + +### Sheet Mask + +주제 6 으로 33판 「다음에 해 볼 질감」에 적힌 시트 마스크를 Peel-Off 를 거꾸로 돌린 그림으로 그렸어요. 말려 벗겨지는 보랏빛 막과 다르게 시트는 살결에 붙은 곳이 세럼 색으로 맑게 비치고 공기가 든 곳은 희게 떠요. 붙은 곳은 가장 밝은 채널이 200 언저리, 방울 속은 243~255 라서 붙음과 들뜸이 밝기로 갈려요. 처음 판은 살결 굴곡을 높이 3 으로, 램프를 80 LED 위에 두었더니 시트가 구겨진 얼음처럼 보였고 광택 점은 지름 30 LED 쯤으로 하얗게 탔어요. 굴곡을 1.1 로 낮추고 램프를 45 LED 로 내리자 광택이 좁아져 섬유 결에 부서지는 반짝임으로 읽혔어요. 방울 둘레에 어두운 주름 고리를 두면 조약돌로 보여서, 램프 쪽 가장자리를 밝히고 반대쪽 살결에 옅은 그림자를 떨어뜨렸어요. K3 은 처음에 방울 개수만 바꿔 반응이 0.035 였고, 덜 젖은 시트의 우윳빛 얼룩과 방울 크기를 같이 묶자 0.079 가 됐어요. + +### Gilded Cream + +주제 6 으로 33판 「다음에 해 볼 질감」에 적힌 흰 크림 속 금박이 접히는 그림을 접힌 제빵사 사상(folded baker's map)으로 그렸어요. 크림 단면의 LED 마다 접기를 거꾸로 되짚어 처음 크림의 자리를 찾고, 한 LED 를 위아래 넷으로 나눠 읽어서 한 줄보다 얇아진 금선도 줄로 남겨요. 접는 도중에는 늘어난 층이 아직 덮지 않은 왼쪽 위에 판의 6분의 1까지 빈 틈이 생겨요. 처음 판은 그 틈을 거의 검게 칠해서 채움 0.939 에 `thirds` 83/98/100 이었고, 딱 끊긴 검은 네모가 그림이 깨진 것처럼 보였어요. 틈을 같은 크림의 그늘색 209, 195, 175 로 바꾸고 미끄러지는 층의 끝을 4.5 LED 깊이로 둥글리자 패인 자리로 읽혔고, 채움은 1.000 이 됐어요. 두 배 접기를 다섯 번 하면 금박이 32배 얇아져 0.25~0.47 줄이 되는데, 그대로는 거의 사라져서 접은 횟수마다 금의 덮임을 0.3 씩 더 세게 칠했어요. 움직임 0.274, 모양 바뀜 0.041 이고, 그리는 시간은 층 사이 홈의 sqrt(sin) 을 sqrt(w(1-w)) 로 바꿔 8.2 ms 에서 4.8 ms 로 줄었어요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -1591,7 +1650,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 24일 `dev`(`07b2766`)의 228개에 33판의 열 개를 더한 238개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 24일 `dev`(`e4204d2`)의 238개에 35판의 열 개를 더한 248개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -1605,6 +1664,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 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 로만 올랐어요. +35판의 열 개도 어느 축의 끝을 넘지 않아서 칸 경계는 30판 그대로이고, 248개에서도 지도 21장이 다 `none` 이에요. 주제 1 은 다시 패턴이 하나뿐인 칸을 겨눴고, 238개에서 24 이던 그런 칸이 20 이 됐어요. 가장자리 × 모양 바뀜 4,3 은 또렷한 그림이 2~8초 사이에 블록 평균으로 0.217~0.331 바뀌어야 하는 칸이에요. Constant Width 는 한 색 면의 테두리를 면보다 49 단계 넘게 밝혀 가장자리가 1.000 이에요. 모양 바뀜 0.277 은 곡선끼리의 밝기 차가 아니라, 곡선이 돌 때 정사각형 칸의 네 귀에 남는 어둠이 따라 도는 데서 왔어요. 여덟 곡선을 한 밝기로 칠해도 0.263 이었어요. 가장자리 × 색 1,4 는 번진 그림이 색 0.545 를 넘어야 해요. Bath Fizz 는 안료 세 가지의 가장 밝은 채널을 다 255 언저리에 두어서, 색이 바뀌는 이음매가 또렷해도 가장자리 셈에는 잡히지 않았어요. 묽은 안료를 맑게 빠지게 해 물과 구름을 가르자 가장자리 0.096, 색 0.592 가 됐고, 같은 값으로 Brushstrokes 혼자이던 복잡도 × 색 4,4 와 Balloon Box 혼자이던 색 × 모양 바뀜 4,3 에도 둘째로 섰어요. + 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 를 냈어요. @@ -1619,7 +1680,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ### 아직 없는 갈래 -`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 이 셋째예요. +`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 가 넷을 더해 스물이고, 35판의 Bath Fizz, Toner Pad, Sheet Mask, Gilded Cream 이 넷을 더해 스물넷이에요. 28판이 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table, 33판의 Goldbach Comet, 35판의 Kolakoski 예요. 27판이 연 철도 갈래는 Funicular 하나이고, 필름·영화 갈래는 Horse in Motion 하나예요. 두 갈래는 주제 표가 수학 쪽으로 바뀌기 전에 열었어요. 그 뒤로 ROUND.md 는 사물로 묶는 갈래를 더 열지 않아요. 26판이 연 얼음·극지 갈래는 Iceberg 와 27판의 Sled Dogs 이고, 미생물 갈래는 Dark Field 와 27판의 Mitosis 예요. 25판이 연 농사·밭 갈래는 Rice Terraces 와 26판의 Rice Planting, 27판의 Center Pivot 이고, 화산 갈래는 Eruption 과 26판의 Geyser, 27판의 Lava Lake 예요. 24판이 연 공사·건설 갈래는 Tower Crane 과 25판의 Excavation, 26판의 Implosion 이에요. 24판이 연 도자기 갈래는 Potter's Wheel 과 25판의 Kintsugi, 26판의 Noborigama 예요. 23판이 연 무대·공연 갈래는 Proscenium 과 24판의 Flying Trapeze, 25판의 Marionette, 26판의 Shadow Play 예요. 23판이 연 짐승 갈래는 Giraffe 와 24판의 Emergence, 25판의 Snow Monkeys 예요. 22판이 연 몸 갈래는 Circulation 과 23판의 Axon, 24판의 Breath, 27판의 Fluoroscope 예요. 22판이 연 비행 갈래는 Liftoff 와 23판의 Final Approach, 24판의 Thermal, 25판의 Skydiver 예요. 20판이 연 새 갈래는 Kingfisher 와 21판의 Woodpecker, 22판의 Weaver, 23판의 Skein 이에요. 20판이 연 화학·실험 갈래는 Chromatography 와 21판의 Chemical Garden, 22판의 Flame Test, 23판의 Elephant Toothpaste 예요. 21판이 연 종이 갈래는 Pop-up Book 과 22판의 Senbazuru, 23판의 Hanging Scroll, 26판의 Origami 예요. 21판이 연 운동·경기 갈래는 Lap Pool 과 22판의 Bowling Lane, 23판의 High Dive, 26판의 Pole Vault, 27판의 Climbing Wall 이에요. 위아래로 움직이는 경기는 High Dive 와 Pole Vault 뒤로 27판에 실내 암벽이 섰어요. 19판이 연 부엌 갈래는 Popcorn 과 20판의 Kettle, 21판의 Pancakes 이고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판이 연 유리 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori, 27판의 Glassblower 예요. 광물·결정 갈래는 Bismuth 와 19판의 Dripstone, 20판의 Agate 이고, 놀이공원 갈래는 Drop Tower 와 19판의 Helter Skelter, 20판의 High Striker, 25판의 Ferris Wheel 이에요. 말이 오르내리는 회전목마는 아직 없어요. 17판이 연 전자 회로 갈래는 Circuit Board 와 18판의 Server Rack, 21판의 Nixie 이고, 장난감 갈래는 Marble Run 과 18판의 Peg Light, 21판의 Slinky, 22판의 Pin Art 예요. 16판이 연 바다 갈래는 Abyss 와 17판의 Shoreline, 20판의 Sunglint, 23판의 Bait Ball 이고, 곤충 갈래는 Fireflies 와 17판의 Ant Farm, 20판의 Moth Lamp, 24판의 Waggle Dance 예요. 15판이 연 도시 갈래는 Metro Map 과 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판이 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables, 24판의 Pipe Organ 이에요. 4판이 연 직물 갈래는 Jacquard Loom 과 15판의 Plait, 19판의 Needlepoint, 23판의 Sequins 이고, 8판이 연 기계 갈래는 Gear Train 과 15판의 Cam Wave, 19판의 Paternoster 예요. 12판이 연 천체 갈래는 Star Trails 와 15판의 Analemma, 16판의 Lunation, 22판의 Galilean Moons 예요. 위성의 궤도는 Galilean Moons 가 그렸고, 해를 도는 행성은 아직 없어요. 리듬·펄스 갈래는 1판의 Euclid Pulse 와 Pulse Bars 뒤로 20판의 Change Ringing, 32판의 Arnold Tongues, 35판의 Chimera State 가 섰어요. ### 멋진 것 더 멋지게 @@ -1631,6 +1692,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 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 가 됐어요. +35판의 열 개는 가장 낮은 노브가 Sheet Mask 의 K4 Weave 와 Gilded Cream 의 K4 Gold 로 둘 다 0.05 예요. Sheet Mask 의 K3 Bubbles 는 방울 개수만 바꿀 때 0.035 였고, 덜 젖은 시트의 우윳빛 얼룩과 방울 크기를 같이 묶자 0.079 가 됐어요. Constant Width 의 K3 Round 는 처음 판에서 0.06 이었어요. 열고 27초 동안 뢸로 삼각형이 나오지 않아 2~3초 창의 곡선이 원과 거의 같았고, 시작 자리를 옮겨 삼각형을 가운데에 두자 0.10 이 됐어요. + Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. ### 세로 64×128 @@ -1647,11 +1710,13 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 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 예요. +35판에서는 Constant Width 가 너비 62 인 곡선이 어느 쪽으로 돌아도 높이 62 라서, 위아래로 맞닿은 곡선 둘이 판을 채워 `thirds` 가 77/74/67 이에요. 벽 두 열은 판의 3% 라 빼고 세도 3분의 1마다 5 를 한참 넘어요. Rényi Parking 은 거리가 위에서 비어 들어와 바닥에서 막혀서 11/34/53 이고, 위 3분의 1은 막 들어온 거리에 선 차 몇 대뿐이에요. Kolakoski 는 열 단의 고드름이 판을 다 덮어 90/92/91 이고, Chimera State 는 위 20줄의 위상 점 그림이 어두운 바탕이라 59/94/94 예요. + ### 낯선 수학 -주제 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)이에요. +주제 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)이에요. 35판도 여섯을 더했어요. 뢸로의 폭이 일정한 곡선(Constant Width), 렌니의 주차 문제(Rényi Parking), 카리의 비주기 왕 타일(Kari-Culik Tiles), 콜라코스키 수열(Kolakoski), 쿠라모토와 바톡토흐의 키메라 상태(Chimera State), 주콥스키 날개(Joukowsky Airfoil)예요. -목록 밖에서 다음에 해 볼 것이에요. 콜라코스키 수열(1965)은 제 길이 부호를 읽으면 제가 되는 1 과 2 의 줄이라 리듬·펄스에 맞아요. 영 도형의 플랑셰렐 성장은 상자가 하나씩 붙으며 로건-셰프와 베르시크-케로프의 극한 곡선(1977)으로 다가가요. 아즈텍 다이아몬드의 북극권과 같은 극한 모양이라 같은 판에 두지 않아요. 슈타이너 사슬은 반전 하나로 고리의 원들이 미끄러지는 포리즘이고, 민코프스키의 물음표 함수(1904)는 페리 나무를 이진 나무로 펴는 계단이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 렌니의 주차 문제(1958)는 줄마다 길이 1 인 차가 아무 데나 서다가 빈자리가 다 1 보다 짧아지면 멈추는데, 128줄이 다 멈추면 채운 몫이 렌니 상수 0.7476 언저리에 모여요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. +목록 밖에서 다음에 해 볼 것이에요. 영 도형의 플랑셰렐 성장은 상자가 하나씩 붙으며 로건-셰프와 베르시크-케로프의 극한 곡선(1977)으로 다가가요. 아즈텍 다이아몬드의 북극권과 같은 극한 모양이라 같은 판에 두지 않아요. 슈타이너 사슬은 반전 하나로 고리의 원들이 미끄러지는 포리즘이고, 민코프스키의 물음표 함수(1904)는 페리 나무를 이진 나무로 펴는 계단이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. 28판에서 본 것이에요. 수학 대상은 셈이 정확해서, 끝없는 확대나 흐름이 거의 다 작은 틀 하나로 돌아요. Ford Circles 는 창을 이웃한 두 수렴 분수의 틀로 잡아 1800초 뒤에도 수가 작고, Hyperbolic Band 는 59 LED 마다, Rauzy Fractal 은 세 단계마다, Gosset Polytope 는 36초마다 화면이 정확히 돌아와요. 반대로 무작위가 주인공인 셋은 2~8초 창이 어느 때에 걸리느냐가 값을 정했어요. Stadium Billiard 는 2~5초만 재면 움직임 0.63, 기체만 재면 3.1 이었어요. 차례대로 자라는 Look and Say 는 빈 쪽에서 열리면 2~8초의 `thirds` 가 24/62/64 였고, 처음 스무 줄을 빠르게 쳐 내자 48/68/69 가 됐어요. 수학을 그대로 그린 화면은 그대로 두면 너무 고요하거나 너무 어두웠어요. Ford Circles 의 고른 원판은 채움 0.971 의 회색 판이었고, Gosset Polytope 의 모서리 그물은 LED 하나에 0.012 씩만 쌓였어요. 빛을 테두리나 고리처럼 수학이 정한 자리에 모으면 읽혔어요. @@ -1665,10 +1730,14 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 33판에서 본 것이에요. 분포가 정리의 주인공인 대상은 고르게 뽑는 셈이 그림을 정했어요. 정렬 네트워크는 이웃 쌍을 아무렇게나 바꾸면 사인 곡선이 나오지 않아서, 훅 걷기로 영 타블로를 고르게 뽑아 에델만-그린 대응으로 옮겼어요. 베르트랑의 역설은 그 뽑는 법 셋이 그대로 원 셋이에요. 예측이 매끈한 값을 줄 때는 나누는 값을 정확히 맞춰야 줄기가 섰어요. Goldbach Comet 은 n/ln²n 으로 나누면 줄기가 4 LED 기울었고, 적분 꼴로 나누자 곧게 섰어요. 정리가 어둠을 말하는 대상은 그 어둠이 정말 0 인지 셈으로 먼저 봤어요. Penrose Room 은 광선 2만 개를 40번씩 튕겨 초점 밖으로 나간 광선이 하나도 없는 것을 본 뒤에 노출을 걸었어요. 3차원 대상은 수학이 허락하는 만큼 늘여 패널에 맞췄고, Borromean Rings 는 세로로 2.3배 늘여도 같은 고리예요. +35판에서 본 것이에요. 타일이 맞물리는지는 정확한 셈이 정했어요. Kari-Culik Tiles 는 x 를 부동소수로 2/3 배 하자 200줄 안에 있을 수 없는 타일이 나와서 BigInt 분수로 셌고, 그렇게 센 5,400줄이 카리의 14장을 하나도 빼지 않고 썼어요. 쿨릭의 3배와 1/2배 줄은 같은 셈에서 혼자서도 판을 덮는 네 변 0 의 타일이 낀 12장이 나와서, 13장 판은 이 꼴로 지어지지 않았어요. 정리가 극한을 말하는 대상은 패널 크기의 예측을 따로 셈했어요. Rényi Parking 의 60 LED 거리는 차 열 대 길이라 채운 몫이 렌니 상수 0.7476 이 아니라 0.7224 이고, 그 값은 첫 차가 거리를 둘로 나눈다는 적분 방정식을 풀어 얻었어요. 무작위 출발이 그림을 정하는 대상은 출발을 다듬어야 했어요. Chimera State 는 위상 지연 80도에서 바로 돌리면 30번 가운데 6번 고리 전체가 발을 맞췄고, 84도에서 키운 뒤 내리자 30번 다 버텼어요. 칠하는 법이 수학을 지우기도 했어요. 흩어진 진동자를 넷씩 평균하면 하늘색으로 뭉개져서, 넷 가운데 하나만 칠하자 흩어진 양옆이 보였어요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 모자 타일(스미스, 마이어스, 캐플런, 굿먼-스트라우스, 2023)은 한 가지 타일로만 평면을 비주기로 덮고, 뒤집어 놓인 타일이 판에 드문드문 흩어져요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 뷔퐁의 바늘(1777)은 Bertrand Paradox 와 같은 꼴로, 금을 넘는 바늘의 몫이 2/π 로 모이는 막대와 함께 세울 수 있어요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. +목록 밖에서 35판 뒤에 해 볼 것이에요. 마이스너 사면체(1911)는 폭이 일정한 입체라 두 평면 사이에서 굴려도 늘 두 면에 닿아요. 원판의 무작위 순차 흡착(페더, 1980)은 Rényi Parking 을 평면으로 넓힌 것이라 판이 0.547 언저리에서 막혀요. 쥬앙델과 라오(2015)의 왕 타일 11장은 Kari-Culik Tiles 의 마름모 꼴로 깔 수 있어요. 1 과 3 으로 짠 콜라코스키 수열은 단마다 항이 두 배 넘게 넓어져 대여섯 단에서 한 항이 판을 다 덮어요. 두 무리로 나눈 키메라(에이브럼스, 미롤로, 스트로가츠, 와일리, 2008)는 흩어진 무리의 질서 변수가 숨 쉬듯 출렁여서 세로 패널의 위아래 두 칸에 맞아요. 카르만-트레프츠 날개(1918)는 사상 지수 하나로 뒷전에 각을 줘서 Joukowsky Airfoil 의 셈을 그대로 써요. + ### 화장품 팝업 주제 6 으로 쓴 질감과 다음에 해 볼 질감이에요. 29판이 처음 넷을 썼어요. 팔레트 나이프로 펴 바르는 크림(Cream Swirl), 구슬이 뜬 젤과 빛 그물(Serum Gel), 쏟아져 쌓이는 글리터(Glitter Fall), 퍼프가 눌렀다 든 압축 파우더(Loose Powder)예요. 넷 다 K1 이 색상환 한 바퀴라서, 색을 돌리면 주황과 노랑이 갈색이나 올리브로 보이는 자리가 있었어요. Serum Gel 은 그 둘레에서만 밝기를 0.62 와 0.3 에서 0.92 와 0.68 로 올렸어요. 밝은 바탕은 반짝임을 지웠어요. 200 위에 55 를 더한 반짝임은 거의 안 보여서 Glitter Fall 은 어두운 바탕에 섰고, 흰 바탕과 금색 한 줄은 Loose Powder 의 아래 띠로만 썼어요. 크림과 젤은 가장자리 0.028 과 0.079 로 연구에서 가장 번지는 그림이에요. 그 둘이 가장자리 축의 아래 끝을 옮겨서 지도에 새 빈 칸이 났어요. 크림은 빛만 움직여서는 움직임이 0.019 에 머물렀어요. 결이 깊어야 빛이 돌 때 바뀌는 LED 가 생겨요. @@ -1681,7 +1750,9 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 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 를 거꾸로 돌린 그림이에요. +35판이 넷을 더 썼어요. 물속에서 녹는 배스 파우더의 안료 구름(Bath Fizz), 솜 패드에 스미는 토너(Toner Pad), 공기 방울을 밀어내며 붙는 시트 마스크(Sheet Mask), 제빵사의 반죽처럼 접히는 금박 크림(Gilded Cream)이에요. 뒤의 둘은 「다음에 해 볼 질감」에 적혀 있던 감이에요. 넷 다 바탕을 다 켜서 채움이 1.000 이고 `thirds` 는 100/100/100 이에요. 주황과 노랑 쪽의 갈색과 올리브는 이번에도 왔어요. Bath Fizz 는 그 범위의 묽은 안료를 옅은 회색 안개로 바래게 하고 갈색이 될 물은 짙은 남보라로 바꿨고, Toner Pad 는 젖은 섬유를 서늘한 회색으로 보냈어요. 빈 곳은 검게 두면 그림이 깨져 보였어요. Gilded Cream 은 접는 동안 열리는 틈을 검게 칠했더니 끊긴 검은 네모가 됐고, 같은 크림의 그늘색 209, 195, 175 로 두자 패인 자리로 읽혔어요. 젖음과 들뜸도 밝기로 옮겨야 잡혔어요. Toner Pad 는 젖은 곳을 마른 솜보다 어둡게 두고 한 방울이 적실 넓이를 1800 LED 로 정해서야 움직임 0.039, 모양 바뀜 0.140 이 됐고, Sheet Mask 는 붙은 곳을 가장 밝은 채널 200 언저리에, 공기 방울 속을 243~255 에 두었어요. + +다음에 해 볼 질감이에요. 세팅 미스트가 유리판에 맺혀 굵어지고 흘러내리는 것은 Lip Gloss 와 Setting Mist 사이라 같은 판에 두지 않아요. 매니큐어 붓이 남긴 줄이 스스로 펴지는 것은 Cream Swirl 과 가까워서 같은 판에 두지 않아요. 자석 젤 네일은 보이지 않는 자석을 따라 반짝이 띠가 모였다 흩어지는 그림이에요. 젖은 립글로스나 탑코트가 굳는 것은 Clay Mask 처럼 광이 먼저, 색이 나중에 빠지는 두 문턱으로 그릴 만해요. 립밤이 체온에 녹아 번지거나 고체 향수가 문질러 펴지는 것은 Cleansing Balm 의 녹는 경계를 가로로 돌린 그림이에요. 크림 한 방울이 젤 위에 떨어져 천천히 잠기는 것은 Essence Drop 의 거리장을 그대로 써요. 물 위에 매니큐어 고리를 떨어뜨려 겹치고 바늘로 끄는 워터 마블 네일은 Bath Fizz 의 유체를 그대로 써요. 기름종이가 피지를 빨아들인 자리만 반투명해지는 것은 Toner Pad 의 길이장으로 그릴 만해요. 하이드로겔 마스크처럼 두껍고 맑은 젤 시트가 붙는 것은 Sheet Mask 의 젖는 선을 그대로 써요. 두 색 크림을 꼬아 늘이는 마블링은 Gilded Cream 의 접기 대신 핀 셋이 감아 도는 태피 당김으로 그릴 만해요. ## 다시 돌리기 diff --git a/incoming/pattern-study/patterns/joukowsky-airfoil.js b/incoming/pattern-study/patterns/joukowsky-airfoil.js index a256ede9e..500bdba09 100644 --- a/incoming/pattern-study/patterns/joukowsky-airfoil.js +++ b/incoming/pattern-study/patterns/joukowsky-airfoil.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Joukowsky Airfoil // Family: physics -// Idea: A cambered Joukowsky wing 44 rows long stands in the middle of the panel with the ideal flow running down past it, faint streamlines bending round it, while lines of dye let go across the top row once a second come down, wrap the rounded nose where they split at a red stagnation point, and leave the sharp trailing edge with the half that went over the suction side well ahead of the half that went under, a lead that grows with the angle of attack and is gone when the circulation is taken away. +// Idea: A cambered Joukowsky wing 44 rows long stands in the middle of the panel with the ideal flow running down past it, faint stream lines bending round it, while lines of dye let go across the top row once a second come down, wrap the rounded nose where they split at a red stagnation point, and leave the sharp trailing edge with the half that went over the suction side well ahead of the half that went under, a lead that grows with the angle of attack and is gone when the circulation is taken away. // Author: patternflow-lab // Date: 2026-10-24 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -88,7 +88,7 @@ function toZeta(F, X, Y, o) { else (o[0] = br), (o[1] = bi), (o[2] = db); } -// When the fluid at an LED crossed the top row: back along its streamline, +// When the fluid at an LED crossed the top row: back along its stream line, // in the ζ-plane where the wing is a circle and easy to keep out of, adding // up time. Midpoint steps of 0.35 LED by the surface, up to 3 LEDs and 3 // time units away from it. @@ -191,7 +191,7 @@ function paint(P, Fd, wgt, rgb) { const { T, G, psi, sp, body } = Fd, pal = P.pal; for (let p = 0, o = 0; p < NP; p++, o += 3) { let r = 0.012, g = 0.016, b = 0.05; - // Streamlines every 6 units of ψ, about one LED wide wherever they are. + // Stream lines every 6 units of ψ, about one LED wide wherever they are. const u = psi[p] / SPACE, du = (abs(u - round(u)) * SPACE) / max(0.05, sp[p]), sl = 0.15 * exp(-2 * du * du); r += sl * 0.45; g += sl * 0.55; diff --git a/incoming/pattern-study/patterns/renyi-parking.js b/incoming/pattern-study/patterns/renyi-parking.js index 564eada08..3bd0bf3ae 100644 --- a/incoming/pattern-study/patterns/renyi-parking.js +++ b/incoming/pattern-study/patterns/renyi-parking.js @@ -122,8 +122,8 @@ function reset(st, L, k, pred) { st.next = -log(1 - Math.random()) / rate(st); } -// Run a street's clock on to t: each try that lands in a feasible stretch -// parks a car there, uniformly, and tries that land anywhere else are +// Run a street's clock on to t: a try that lands in a feasible stretch +// parks a car at a uniform place in it. Tries that land anywhere else are // turned away and change nothing, so only the successes are drawn, at the // rate of the feasible stretch. Each car keeps the share of the street that // was already parked when it came. @@ -186,7 +186,7 @@ export function update(dt, input, params) { P.phase += d * P.speed * 2; while (P.phase >= 1) { P.phase -= 1; - // The bottom street leaves, jammed, and comes back empty at the top. + // The jammed bottom street leaves and comes back empty at the top. const bottom = P.streets[(P.head + H - 1) % H]; if (bottom.L === P.L && bottom.k === P.k) { P.outs[P.outI] = (bottom.n * bottom.L) / S; From 699083d863fe475049fcc0a35f86ae780cdd577d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 18:32:01 +0000 Subject: [PATCH 8/9] =?UTF-8?q?index.html=20=EC=9D=84=2035=ED=8C=90=20?= =?UTF-8?q?=EC=97=B4=20=EA=B0=9C=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20=EC=A7=93?= =?UTF-8?q?=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SW1UfDNBr6xKa7hi5BogQV --- 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 b8c8832a2..c5f867d15 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -141,7 +141,7 @@

패턴 연구

- + +