From 488da4d5ed466c9c9ac06b9b9432edbef93c515b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 10:51:43 +0000 Subject: [PATCH 01/15] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=2030=ED=8C=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QuEe7qhvCVuDuGNB5fZ113 From 998afcaf299788a7f6051259fd77c0812c342862 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 10:59:10 +0000 Subject: [PATCH 02/15] =?UTF-8?q?=ED=99=94=EC=9E=A5=ED=92=88=20=ED=8C=9D?= =?UTF-8?q?=EC=97=85=20=EA=B0=88=EB=9E=98=20=EC=A4=84=EC=9D=84=20=EB=8D=94?= =?UTF-8?q?=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_01QuEe7qhvCVuDuGNB5fZ113 --- incoming/pattern-study/families.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/incoming/pattern-study/families.json b/incoming/pattern-study/families.json index c8e696b22..20c22c427 100644 --- a/incoming/pattern-study/families.json +++ b/incoming/pattern-study/families.json @@ -51,5 +51,6 @@ "microbe": { "label": "미생물", "color": "#aa00aa" }, "rail": { "label": "철도", "color": "#00bb66" }, "film": { "label": "필름·영화", "color": "#ff0000" }, - "number": { "label": "수론", "color": "#00aaff" } + "number": { "label": "수론", "color": "#00aaff" }, + "cosmetic": { "label": "화장품 팝업", "color": "#f2c6c2" } } From ef2e0029bbd400f7f846c5b198d25596f466a5c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 11:30:39 +0000 Subject: [PATCH 03/15] =?UTF-8?q?Hofstadter=20Butterfly=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=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 --- .../patterns/hofstadter-butterfly.js | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 incoming/pattern-study/patterns/hofstadter-butterfly.js diff --git a/incoming/pattern-study/patterns/hofstadter-butterfly.js b/incoming/pattern-study/patterns/hofstadter-butterfly.js new file mode 100644 index 000000000..ba3d8e6de --- /dev/null +++ b/incoming/pattern-study/patterns/hofstadter-butterfly.js @@ -0,0 +1,177 @@ +// ===== Patternflow pattern ===== +// Title: Hofstadter Butterfly +// Family: fractal +// Idea: The energy spectrum of an electron on a square lattice in a magnetic field, energy across and flux per cell up from 0 to 1, with every gap coloured by its Hall integer t and lines of equal electron density sliding up the wings where t is positive and down where it is negative. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Hofstadter's butterfly (Douglas Hofstadter, 1976), coloured by the gap labels of Thouless, Kohmoto, Nightingale and den Nijs (1982), pattern round 30 +// Round: 30 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Depth=16..64, Coupling=-1..1 +// +// Knob 1: Hue. Turns every gap colour round the wheel together. At 0 the gaps with t = +1 are cyan and t = -1 magenta; each step up in |t| moves the plus side towards green and the minus side towards blue, and dims it, down to a third from |t| = 12 on. +// Knob 2: Speed. At 2 a line of equal density climbs a t = +1 wing 16 rows a second (eight in a t = +2 gap, and so on), and the bands breathe through one turn of the phase in 3.1 seconds; at 0.2 both run ten times slower. +// Knob 3: Depth. The largest denominator q. Each row's flux is replaced by the nearest fraction p/q with q up to Depth, and the row shows that fraction's spectrum exactly. At 16 four rows share 1/2, the four at each end share 0 and 1 and show one wide band, and the fans above and below are coarse; at 64 two rows share 1/2 and the fans fill in. +// Knob 4: Coupling. lambda = 2^K4, the potential against the hopping, held at exactly 1 (the classic butterfly) within 0.08 of the middle. At -1 (lambda 1/2) the bands open into white sheets a third of the width; at +1 (lambda 2) the same sheets come back grey with one narrow lit sliver each, because Aubry-Andre duality sends lambda to 1/lambda and the energy axis, scaled by 2 + 2 lambda, puts every gap back where it was. +// Buttons: K1 tap fades the colours out for six seconds and leaves Hofstadter's own picture of 1976: the spectrum alone, white on black. + +const W = 64, H = 128, S = 6, NS = W * S; +const { floor, round, cos, abs, min, max, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const BAND = 127, TM = 40; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + const rgb = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + for (let c = 0; c < 3; c++) out[o + c] = rgb[c] * 255; +} + +// The nearest fraction p/q to a with q at most Q; on a tie the smaller q, so +// p/q comes out in lowest terms. +function nearest(a, Q) { + let bp = 0, bq = 1, bd = 9; + for (let q = 1; q <= Q; q++) { + const p = round(a * q), d = abs(a - p / q); + if (d < bd - 1e-12) (bd = d), (bp = p), (bq = q); + } + return [bp, bq]; +} + +// Harper's equation, psi(n+1) + psi(n-1) + 2 lambda cos(2 pi alpha n + nu) +// psi(n) = E psi(n), at alpha = p/q. Row y has flux (127 - y) / 127, so 0 at +// the bottom and 1 at the top; column x has energy across -(2 + 2 lambda) to +// 2 + 2 lambda, six samples to an LED. +// +// For each sample: the trace D of the product of the q transfer matrices +// [[E - V(n), -1], [1, 0]], taken at nu = pi / 2q. Chambers' relation says the +// trace at any other nu is D - 2 lambda^q cos(q nu), so E is in the spectrum +// at phase nu where |D - 2 lambda^q cos(q nu)| <= 2, and at some phase where +// |D| <= 2 + 2 lambda^q. Both are checked against the one cached D, so the +// phase turns every frame for nothing. +// +// In a gap, r bands lie below E. r comes from a Sturm count: the negative +// pivots of the chain cut off at 3q sites count its levels below E, which are +// 3r give or take one. The gap labelling theorem puts the density there at +// N = r / q = s + t p / q with whole s and t, so r = t p mod q, and t is r times +// the inverse of p mod q, taken between -q/2 and q/2. t is the Hall +// conductance of a filled gap in units of e^2/h. t = 0 only below and above +// the whole spectrum. +const V = new Float64Array(128); +function buildRow(P, y) { + const a = (H - 1 - y) / (H - 1), [p, q] = nearest(a, P.Q), lam = P.lam; + if (P.rp[y] === p && P.rq[y] === q && P.rlam[y] === lam) return 0; + (P.rp[y] = p), (P.rq[y] = q), (P.rlam[y] = lam); + const Em = 2 + 2 * lam, lq = lam ** q, lim = 2 + 2 * lq; + let pin = 1; + while ((p * pin) % q !== 1 % q) pin++; + for (let n = 0; n < q; n++) V[n] = 2 * lam * cos((2 * PI * p * (n + 1)) / q + PI / (2 * q)); + P.rl[y] = 2 * lq; + const o = y * NS; + for (let j = 0; j < NS; j++) { + const E = -Em + ((j + 0.5) * 2 * Em) / NS; + let a0 = 1, b0 = 0, c0 = 0, d0 = 1; + for (let n = 0; n < q; n++) { + const v = E - V[n], na = v * a0 - c0, nb = v * b0 - d0; + (c0 = a0), (d0 = b0), (a0 = na), (b0 = nb); + } + const D = a0 + d0; + P.D[o + j] = D; + if (abs(D) <= lim) { + (P.T[o + j] = BAND), (P.R[o + j] = 255); + continue; + } + let cnt = 0, dp = 1e300; + for (let k = 0; k < 3; k++) + for (let n = 0; n < q; n++) { + let dd = V[n] - E - 1 / dp; + if (dd === 0) dd = 1e-300; + if (dd < 0) cnt++; + dp = dd; + } + const r = round(cnt / 3); + let t = (r * pin) % q; + if (t > q / 2) t -= q; + P.T[o + j] = r === 0 || r === q ? 0 : t; + P.R[o + j] = r; + } + return 1; +} + +export function setup(params) { + params.s = { + D: new Float64Array(H * NS), T: new Int8Array(H * NS), R: new Uint8Array(H * NS), + rp: new Int32Array(H).fill(-1), rq: new Int32Array(H), rlam: new Float64Array(H), rl: new Float64Array(H), + Q: -1, lam: -1, next: H, phi: 0, wave: 0, hue: NaN, bw: 0, hold: 0, + pal: new Float32Array((2 * TM + 1) * 3), M: new Float32Array(2 * TM + 1), + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 16, 0.06], d = cl(dt || 0, 0, 0.1); + const Q = round(cl(k[2], 16, 64)), K4 = cl(k[3], -1, 1); + const lam = abs(K4) < 0.08 ? 1 : 2 ** (round(K4 * 50) / 50); + // The first frame builds all 128 rows (9 ms at Depth 16, 40 at 64). After a + // turn of K3 or K4 the rows are built again from the bottom, about 512 + // periods' worth a frame, so a full redraw takes half a second; a row whose + // fraction and lambda did not change is kept. + const first = P.Q < 0; + if (Q !== P.Q || lam !== P.lam) (P.Q = Q), (P.lam = lam), (P.next = 0); + for (let budget = first ? 1e9 : 512; budget > 0 && P.next < H; ) budget -= buildRow(P, H - 1 - P.next++) * P.Q; + const sp = cl(k[1], 0.2, 3); + P.phi += d * sp; + P.wave += d * sp * 0.125; + if (input.btnPressed && input.btnPressed[0]) P.hold = 6; + P.hold = max(0, P.hold - d); + P.bw = cl(P.bw + (P.hold > 0 ? d : -d) * 1.5, 0, 1); + if (P.hue !== k[0]) { + P.hue = k[0]; + for (let t = -TM; t <= TM; t++) { + const o = (t + TM) * 3, at = abs(t); + if (t === 0) (P.pal[o] = 0), (P.pal[o + 1] = 0), (P.pal[o + 2] = 0); + else hsv(k[0] + (t > 0 ? 0.5 : 0.85) - 0.05 * min(at - 1, 4), 0.82, max(0.32, 1 / (1 + 0.2 * (at - 1))), P.pal, o); + } + } +} + +// In a gap with label t, the density N = s + t alpha climbs t whole units from +// the bottom of the panel to the top (Streda's formula: dN / d alpha = t). +// Each gap is lit by cos(2 pi (2 N - w)), which needs only t alpha because s is +// whole: as w grows, the lines of equal N rise through a gap at 1 / t of the +// pace of the t = 1 wings, and sink where t is negative. +const WK = 2; +export function draw(display, params) { + const P = params.s; + if (!P || P.Q < 0) return; + const pal = P.pal, M = P.M, bw = P.bw, cw = 1 - bw; + for (let y = 0; y < H; y++) { + const c = P.rl[y] * cos(P.phi), o = y * NS, al = (H - 1 - y) / (H - 1); + for (let t = -TM; t <= TM; t++) M[t + TM] = cw * (0.7 + 0.3 * cos(2 * PI * (WK * t * al - P.wave))); + let prev = P.R[o]; + for (let x = 0; x < W; x++) { + let r = 0, g = 0, b = 0, miss = 0; + for (let j = x * S; j < x * S + S; j++) { + const t = P.T[o + j], rr = P.R[o + j]; + if (t === BAND) { + // White where the band is at the present phase, grey where it is at + // some other phase. In the 0, 1/2 and 1 rows the white slides across. + const w = abs(P.D[o + j] - c) <= 2 ? 255 : 110; + (r += w), (g += w), (b += w); + } else { + // r changed between two gap samples: bands thinner than a sample lie + // between them, and their count lights the LED. + if (prev !== 255 && rr !== prev) miss += abs(rr - prev); + const q = (t + TM) * 3, f = M[t + TM]; + (r += pal[q] * f), (g += pal[q + 1] * f), (b += pal[q + 2] * f); + } + prev = rr; + } + const wm = min(1, miss * (0.2 + 0.5 * bw)); + (r /= S), (g /= S), (b /= S); + display.setPixel(x, y, r + (210 - r) * wm, g + (210 - g) * wm, b + (210 - b) * wm); + } + } +} From 2ae3ff90ae3a578b3fd48728166d797d71e3536a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 11:30:58 +0000 Subject: [PATCH 04/15] =?UTF-8?q?Meander=20=ED=8C=A8=ED=84=B4=EC=9D=84=20?= =?UTF-8?q?=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- incoming/pattern-study/patterns/meander.js | 339 +++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 incoming/pattern-study/patterns/meander.js diff --git a/incoming/pattern-study/patterns/meander.js b/incoming/pattern-study/patterns/meander.js new file mode 100644 index 000000000..ceec9c6d7 --- /dev/null +++ b/incoming/pattern-study/patterns/meander.js @@ -0,0 +1,339 @@ +// ===== Patternflow pattern ===== +// Title: Meander +// Family: line +// Idea: A random meander, one closed curve that crosses a straight road down the middle of the panel 12 to 32 times, filled solid with the colour wheel laid once round its length; every few seconds the arcs on one side fold flat onto the road and open again as a new set that still closes into one loop. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: meanders (Vladimir Arnold, 1988), pattern round 30 +// Round: 30 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Crossings=12..32, Hollow=0..1 +// +// Knob 1: Hue. Turns the colour wheel that lies once round the loop. At 0 the loop starts azure where the pulse first sets off and runs on through blue, magenta, red, gold and green back to azure; the colours then flow forward along the curve with the pulse. +// Knob 2: Speed. At 2 the pulse runs 80 LEDs of curve a second, the colours flow round the loop once in 12.5 seconds, and one side of the meander folds and reopens every 5 seconds; at 0.2 all of it is ten times slower, at 3 half as fast again. +// Knob 3: Crossings. How many times the curve crosses the road, 2n for a meander of order n: 12 at the start, 4 LEDs apart at 32. A new order folds both sides flat and opens a new meander. +// Knob 4: Hollow. At 0 the inside of the loop burns in full colour; turned up it dims, and at 1 only the curve and the road are left. +// Buttons: K1 tap folds both sides at once and opens a fresh meander of the same order. + +// The road runs down the middle, between columns 31 and 32. The 2n crossings +// sit evenly on it from row 2 to row 126, 4 LEDs apart at n = 16, and the +// widest arc on a side reaches 28.5 LEDs out, so its line stays off the wall. +// A meander of order n is a pair of non-crossing matchings of those points, +// arcs on the left and arcs on the right, that join into one loop. Arnold +// asked how many there are: 1, 2, 8, 42, 262, 1828, ..., each about ten +// times the last by n = 16 and the ratio creeping up towards 12.26. No +// formula is known. +const W = 64, H = 128, NP = W * H, MID = 32, WMAX = 28.5, TOP = 2, SPAN = 124; +const { min, max, floor, ceil, sqrt, cos, acos, abs, exp, round, random, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ease = (u) => u * u * (3 - 2 * u); +// The arc's distance from the road: its semicircle r, cut off softly at the +// width w its nesting gives it. Both grow from an arc to any arc round it, +// so nested arcs never touch. +const smin = (w, r) => (w <= 0 || r <= 0 ? 0 : (w * r) / sqrt(sqrt(w * w * w * w + r * r * r * r))); + +// A non-crossing perfect matching of 2n points as a uniform random Dyck word, +// by the cycle lemma: shuffle n opens and n + 1 closes, start just after the +// first lowest point of the walk, drop the last close. +const WALK = new Int8Array(33); +function dyck(n, P) { + const m = 2 * n + 1, a = WALK; + for (let i = 0; i < m; i++) a[i] = i < n ? 1 : -1; + for (let i = m - 1; i > 0; i--) { + const j = floor(random() * (i + 1)), t = a[i]; + (a[i] = a[j]), (a[j] = t); + } + let s = 0, lo = 0, at = 0; + for (let i = 0; i < m; i++) if ((s += a[i]) < lo) (lo = s), (at = i); + const st = []; + for (let k = 0; k < 2 * n; k++) { + if (a[(at + 1 + k) % m] > 0) st.push(k); + else { + const o = st.pop(); + (P[o] = k), (P[k] = o); + } + } +} + +// Left arcs and right arcs together are one closed curve when walking left, +// right, left, ... from point 0 meets every point before it comes back. +function oneLoop(L, R, n2) { + let i = 0, c = 0; + do (i = R[L[i]]), (c += 2); + while (i !== 0 && c <= n2); + return c === n2; +} + +// Each arc's height: 0 when no arc lies inside it, else one more than the +// highest arc directly inside. It sets the arc's width, so a nest of any +// depth fits between the road and the wall. +function heights(P, n2, h) { + const st = new Int8Array(n2 + 1); + let sp = 0; + st[0] = -1; + for (let i = 0; i < n2; i++) { + if (P[i] > i) st[++sp] = -1; + else { + const v = st[sp--] + 1; + h[i] = h[P[i]] = v; + if (v > st[sp]) st[sp] = v; + } + } + return st[0]; +} + +function dress(n, L, R) { + const hL = new Int8Array(2 * n), hR = new Int8Array(2 * n); + return { n, L, R, hL, hR, HL: heights(L, 2 * n, hL), HR: heights(R, 2 * n, hR) }; +} + +// A uniform random meander of order n: draw both sides until they close into +// one loop. About one pair in 10 does at n = 6 and one in 210 at n = 16. +function meander(n) { + for (; n > 1; n--) { + const L = new Int8Array(2 * n), R = new Int8Array(2 * n); + for (let t = 0; t < 4000; t++) { + dyck(n, L), dyck(n, R); + if (oneLoop(L, R, 2 * n)) return dress(n, L, R); + } + } + return dress(1, new Int8Array([1, 0]), new Int8Array([1, 0])); +} + +// The same meander with side X drawn again, the other side kept. +function reside(M, X) { + const n = M.n, Q = new Int8Array(2 * n); + for (let t = 0; t < 4000; t++) { + dyck(n, Q); + if (X ? oneLoop(M.L, Q, 2 * n) : oneLoop(Q, M.R, 2 * n)) return X ? dress(n, M.L, Q) : dress(n, Q, M.R); + } + return M; +} + +export function setup(params) { + params.s = { + M: null, n2: 0, ph: 0, side: 2, u: 0, next: 0, both: false, flow: 0, ck: 0, cu: 0, + // the loop, arc by arc: from point, to point, side (0 left, 1 right) + AF: new Int8Array(32), AT: new Int8Array(32), AS: new Int8Array(32), + // each kept arc's place on the loop before the last swap, at its start and end + OS: new Float32Array(32), OE: new Float32Array(32), OK: new Uint8Array(32), old: 0, + len: new Float32Array(32), cum: new Float32Array(33), + gp: new Float32Array(32 * 6), D: new Float32Array(NP), LA: new Int8Array(NP), LT: new Float32Array(NP), + hue: 0, speed: 2, hollow: 0.06, nT: 6, + }; +} + +// Walk the loop from point a, first along side s. +function order(P, a, s) { + const M = P.M; + for (let k = 0; k < P.n2; k++) { + const b = s ? M.R[a] : M.L[a]; + (P.AF[k] = a), (P.AT[k] = b), (P.AS[k] = s); + (a = b), (s ^= 1); + } +} + +// Swap in new arcs while the side is flat on the road. A one-side swap keeps +// the pulse on the kept arc it is on, or the next one, and lets every kept +// arc carry its old colours over into the new loop's while the side opens. +function swap(P) { + const X = P.side; + if (X === 2) { + P.M = meander(P.nT); + P.n2 = 2 * P.M.n; + order(P, floor(random() * P.n2), floor(random() * 2)); + (P.ck = 0), (P.cu = 0), (P.old = 0), (P.side = 2); + return; + } + const Q = 1 - X, n2 = P.n2, L = P.cum[n2] || 1; + const os = new Float32Array(n2), oe = new Float32Array(n2); + for (let k = 0; k < n2; k++) + if (P.AS[k] === Q) { + const i = P.AF[k], j = P.AT[k]; + (os[i] = P.cum[k] / L), (oe[i] = P.cum[k + 1] / L); + (os[j] = P.cum[k + 1] / L), (oe[j] = P.cum[k] / L); + } + let a = P.AF[P.ck]; + if (P.AS[P.ck] !== Q) (a = P.AT[P.ck]), (P.cu = 0); + P.M = reside(P.M, X); + order(P, a, Q); + P.ck = 0; + for (let k = 0; k < n2; k++) { + P.OK[k] = P.AS[k] === Q ? 1 : 0; + (P.OS[k] = os[P.AF[k]]), (P.OE[k] = oe[P.AF[k]]); + } + P.old = 1; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 12, 0.06], d = cl(dt || 0, 0, 0.1); + P.hue = k[0]; + P.speed = cl(k[1], 0.2, 3); + P.nT = cl(round(cl(k[2], 12, 32) / 2), 6, 16); + P.hollow = cl(k[3], 0, 1); + if (!P.M) { + P.M = meander(P.nT); + P.n2 = 2 * P.M.n; + order(P, floor(random() * P.n2), 0); + (P.ph = 2), (P.side = 2), (P.u = 0); + } + const s = P.speed; + if (input.btnPressed && input.btnPressed[0]) { + P.both = true; + if (P.ph === 0) P.u = 1; + } + // 0 hold, 1 close, 2 open; in clock units that run at Speed a second. + if (P.ph === 0) { + P.u += (d * s) / 7; + if (P.M.n !== P.nT) P.u = 1; + if (P.u >= 1) { + P.side = P.both || P.M.n !== P.nT ? 2 : P.next; + if (P.side < 2) P.next ^= 1; + (P.both = false), (P.ph = 1), (P.u = 0); + } + } else if (P.ph === 1) { + P.u += (d * s) / 1.2; + if (P.u >= 1) swap(P), (P.ph = 2), (P.u = 0); + } else { + P.u += (d * s) / 1.8; + if (P.u >= 1) (P.ph = 0), (P.u = 0), (P.old = 0); + } + P.flow += d * s * 0.04; + // The pulse: 40 LEDs of curve a second per unit of Speed. + P.cu += (d * s * 40) / max(4, P.len[P.ck]); + while (P.cu >= 1) (P.cu -= 1), (P.ck = (P.ck + 1) % P.n2); +} + +// The colour wheel once round the loop, 1024 steps. +const LUT = new Float32Array(1024 * 3); +for (let i = 0; i < 1024; i++) { + const h = (i / 1024) * 6, j = floor(h), f = h - j; + const c = [[1, f, 0], [1 - f, 1, 0], [0, 1, f], [0, 1 - f, 1], [f, 0, 1], [1, 0, 1 - f]][j]; + for (let q = 0; q < 3; q++) LUT[i * 3 + q] = c[q] ** 0.9; +} + +export function draw(display, params) { + const P = params.s; + if (!P || !P.M) return; + const M = P.M, n2 = P.n2, sp = SPAN / (n2 - 1), g = P.gp; + const open = P.ph === 0 ? 1 : P.ph === 1 ? 1 - ease(P.u) : ease(P.u); + const sL = P.side === 1 ? 1 : open, sR = P.side === 0 ? 1 : open; + const mix = P.old ? ease(P.ph === 2 ? P.u : 1) : 1; + // Each arc: top and bottom y, centre, radius, width, side sign. While a + // side folds, its arcs all lose width at one rate, so the innermost lie flat + // first and the nest never crosses itself; it opens outermost first. + for (let k = 0; k < n2; k++) { + const a = P.AF[k], b = P.AT[k], side = P.AS[k]; + const h = side ? M.hR[a] : M.hL[a], Hs = side ? M.HR : M.HL, so = side ? sR : sL; + const ya = TOP + a * sp, yb = TOP + b * sp, o = k * 6; + (g[o] = min(ya, yb)), (g[o + 1] = max(ya, yb)); + (g[o + 2] = (ya + yb) / 2), (g[o + 3] = abs(yb - ya) / 2); + g[o + 4] = (WMAX / (Hs + 1)) * cl(so * (Hs + 1) - (Hs - h), 0, h + 1); + g[o + 5] = side ? 1 : -1; + } + // The curve: sample every arc in loop order, keep for each LED the nearest + // sample, which arc it was on and how far along. + const D = P.D, LA = P.LA, LT = P.LT; + D.fill(9); + P.cum[0] = 0; + for (let k = 0; k < n2; k++) { + const o = k * 6, c = g[o + 2], R = g[o + 3], w = g[o + 4], sg = g[o + 5]; + const down = P.AF[k] < P.AT[k], ns = max(4, ceil((PI * R) / 0.4)); + let px = 0, py = 0, len = 0; + for (let j = 0; j <= ns; j++) { + const t = (PI * j) / ns, X = MID + sg * smin(w, R * Math.sin(t)), Y = c - R * cos(t); + if (j) len += sqrt((X - px) ** 2 + (Y - py) ** 2); + (px = X), (py = Y); + const tau = down ? j / ns : 1 - j / ns; + for (let yy = ceil(Y - 2.3), y1 = floor(Y + 1.3); yy <= y1; yy++) { + if (yy < 0 || yy >= H) continue; + const ey = yy + 0.5 - Y; + for (let xx = ceil(X - 2.3), x1 = floor(X + 1.3); xx <= x1; xx++) { + if (xx < 0 || xx >= W) continue; + const ex = xx + 0.5 - X, dd = ex * ex + ey * ey, p = yy * W + xx; + if (dd < D[p]) (D[p] = dd), (LA[p] = k), (LT[p] = tau); + } + } + } + P.len[k] = len; + P.cum[k + 1] = P.cum[k] + len; + } + const Ltot = P.cum[n2] || 1, head = P.cum[P.ck] + P.cu * P.len[P.ck]; + // Where on the loop a point is, 0..1, with a kept arc's old place blended + // into its new one while its side opens. + const at = (k, tau) => { + let f = (P.cum[k] + tau * P.len[k]) / Ltot; + if (mix < 1 && P.OK[k]) { + let e = P.OS[k] + tau * (P.OE[k] - P.OS[k]) - f; + e -= round(e); + f += (1 - mix) * e; + } + return f; + }; + // The pulse's head, for the glow round it. + const ho = P.ck * 6, hr = g[ho + 3], ht = PI * (P.AF[P.ck] < P.AT[P.ck] ? P.cu : 1 - P.cu); + const hx = MID + g[ho + 5] * smin(g[ho + 4], hr * Math.sin(ht)), hy = g[ho + 2] - hr * cos(ht); + const hue0 = P.hue + 0.55 - P.flow, fillV = 255 * 0.97 * (1 - P.hollow) ** 1.2; + const lineS = 0.5 + 0.35 * P.hollow; + // The inside by the even-odd rule, row by row: a LED is inside when a ray + // from it to the right wall crosses the curve an odd number of times. The + // arcs a row meets on one side are nested, so the band a LED sits in takes + // the colour of the arc just outside it, at this row. + const fl = new Float32Array(16), ka = new Int8Array(16), ta = new Float32Array(16); + const fr = new Float32Array(16), kb = new Int8Array(16), tb = new Float32Array(16); + for (let yy = 0; yy < H; yy++) { + const yc = yy + 0.5; + let nl = 0, nr = 0; + for (let k = 0; k < n2; k++) { + const o = k * 6; + if (!(g[o] < yc && yc < g[o + 1])) continue; + const c = g[o + 2], R = g[o + 3], dy = yc - c, f = smin(g[o + 4], sqrt(R * R - dy * dy)); + const t = acos(cl(-dy / R, -1, 1)) / PI, tau = P.AF[k] < P.AT[k] ? t : 1 - t; + const A = g[o + 5] > 0 ? fr : fl, KK = g[o + 5] > 0 ? kb : ka, T = g[o + 5] > 0 ? tb : ta; + let i = g[o + 5] > 0 ? nr++ : nl++; + while (i > 0 && A[i - 1] > f) (A[i] = A[i - 1]), (KK[i] = KK[i - 1]), (T[i] = T[i - 1]), i--; + (A[i] = f), (KK[i] = k), (T[i] = tau); + } + for (let half = 0; half < 2; half++) { + const A = half ? fr : fl, KK = half ? kb : ka, T = half ? tb : ta, n = half ? nr : nl; + let i = 0; + for (let q = 0; q < 32; q++) { + const xx = half ? MID + q : MID - 1 - q, dist = q + 0.5, p = yy * W + xx; + while (i < n && A[i] < dist) i++; + let r = 0, gg = 0, b = 0; + const inside = half ? (nr - i) & 1 : (i + nr) & 1; + if (inside && fillV > 0) { + const f = at(KK[i], T[i]), ix = floor((((hue0 + f) % 1) + 1) % 1 * 1024) * 3; + (r = LUT[ix] * fillV), (gg = LUT[ix + 1] * fillV), (b = LUT[ix + 2] * fillV); + } + // The road, a faint line on the dark and a darker one through the colour. + if (q === 0) { + if (inside && fillV > 0) (r *= 0.6), (gg *= 0.6), (b *= 0.6); + else (r += 34), (gg += 36), (b += 44); + } + const dl = sqrt(D[p]); + if (dl < 2.3) { + const k = LA[p], f = at(k, LT[p]), ix = floor((((hue0 + f) % 1) + 1) % 1 * 1024) * 3; + let behind = (head - (P.cum[k] + LT[p] * P.len[k])) % Ltot; + if (behind < 0) behind += Ltot; + const pulse = exp(-behind / 24) + (behind > Ltot - 3 ? (behind - Ltot + 3) / 3 : 0); + const I = cl(1.75 - dl * 1.1, 0, 1), J = cl(2 - dl * 0.75, 0, 1) * pulse, wh = 1 - lineS; + let lr = 255 * (wh + lineS * LUT[ix]), lg = 255 * (wh + lineS * LUT[ix + 1]), lb = 255 * (wh + lineS * LUT[ix + 2]); + const pv = min(1, pulse); + (lr += (255 - lr) * pv), (lg += (250 - lg) * pv), (lb += (235 - lb) * pv); + const m = max(I, J); + (r += (lr - r) * m), (gg += (lg - gg) * m), (b += (lb - b) * m); + } + const ex = xx + 0.5 - hx, ey = yc - hy, e2 = ex * ex + ey * ey; + if (e2 < 30) { + const gl = 0.9 * exp(-e2 / 6); + (r += (255 - r) * gl), (gg += (250 - gg) * gl), (b += (235 - b) * gl); + } + display.setPixel(xx, yy, min(255, r), min(255, gg), min(255, b)); + } + } + } +} From 82e4fe1ad99a78f2db55e112cebd21cdca115c57 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 11:31:54 +0000 Subject: [PATCH 05/15] =?UTF-8?q?Three=20Gaps=20=ED=8C=A8=ED=84=B4?= =?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 --- incoming/pattern-study/patterns/three-gaps.js | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 incoming/pattern-study/patterns/three-gaps.js diff --git a/incoming/pattern-study/patterns/three-gaps.js b/incoming/pattern-study/patterns/three-gaps.js new file mode 100644 index 000000000..6c2f9250f --- /dev/null +++ b/incoming/pattern-study/patterns/three-gaps.js @@ -0,0 +1,182 @@ +// ===== Patternflow pattern ===== +// Title: Three Gaps +// Family: number +// Idea: Each row is the circle unrolled across the panel with one more of the points 0, a, 2a, 3a, ... (mod 1) on it than the row above, every stretch coloured by which of the at most three gap lengths it lies in, while a drifts between a golden-ratio number and the fractions beside it and the gaps split, merge and collapse into repeating columns. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: three-gap theorem (conjectured by Hugo Steinhaus, proved by Vera T. Sós, 1957), pattern round 30 +// Round: 30 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Alpha=0..1, Points=0..1 +// +// Knob 1: Hue. Turns the three colours round the wheel together. At 0 the longest gaps are yellow, the middle ones cyan and the shortest magenta; two lengths within 15% of each other share the colour between theirs, green at 0 for the long pair. +// Knob 2: Speed. How fast a travels. At 2 it goes from 0.618 to 13/21 in 2.2 s, back, out to 8/13 in 3.5 s and back again, 11.4 s round; at 0.2 the round takes 114 s. +// Knob 3: Alpha. Where a sets out from: 1/phi = 0.618 at 0, sliding down to the bronze ratio (sqrt 13 - 3)/2 = 0.303 at 1. From there a visits the nearest fraction with denominator up to 21 on each side, 13/21 and 8/13 at 0, 3/10 and 4/13 at 1. At 0.25 a starts at 0.539, just off 7/13, and the rows past 13 lean into a V of thirteen columns. +// Knob 4: Points. How many points the bottom row holds: 30 at 0, 36 at the start, 130 at 1. The rows share them out evenly, 3 or 4 rows to each count at the start. Past about 64 points a gap is narrower than an LED and each LED blends four samples. +// Buttons: K1 tap sends a straight to the stop at the end of the leg it is on. + +const W = 64, H = 128, SS = 4, NMAX = 160, Q = 21; +const { min, max, floor, abs } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ease = (u) => u * u * (3 - 2 * u); +const GOLD = (Math.sqrt(5) - 1) / 2, BRONZE = (Math.sqrt(13) - 3) / 2; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + const rgb = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + for (let c = 0; c < 3; c++) out[o + c] = rgb[c]; +} + +// The two fractions with denominator at most 21 that hold a0 between them, +// found by walking down the Stern-Brocot tree: 8/13 and 13/21 for 1/phi. +function bracket(a0) { + let a = 0, b = 1, c = 1, d = 1; + for (;;) { + const p = a + c, q = b + d; + if (q > Q) break; + if (a0 < p / q) (c = p), (d = q); + else (a = p), (b = q); + } + return [a / b, c / d]; +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, k3: -1, a0: GOLD, ends: [GOLD, GOLD], u: 0, nmax: 36, alpha: GOLD, + pos: new Float64Array(NMAX + 1), idx: new Int32Array(NMAX + 1), sep: new Float32Array(W), + row: new Float32Array(W * 3), lut: new Float32Array(3 * 64), lutHue: NaN, + }; +} + +const stop = (P, i) => (i & 1 ? P.ends[i >> 1] : P.a0); + +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.nmax = floor(30 + 100 * cl(k[3], 0, 1)); + const k3 = cl(k[2], 0, 1); + if (k3 !== P.k3) { + P.k3 = k3; + P.a0 = GOLD + (BRONZE - GOLD) * k3; + // The nearer fraction first. On a fraction exactly, points would land on + // each other and floating point would shuffle their order; 3e-7 off it, + // on a0's side, they stay in order and the picture is the same. + const [lo, hi] = bracket(P.a0); + P.ends = P.a0 - lo < hi - P.a0 ? [lo + 3e-7, hi - 3e-7] : [hi - 3e-7, lo + 3e-7]; + } + if (input.btnPressed && input.btnPressed[0]) P.u = floor(P.u) + 1; + // a goes a0, first fraction, a0, second fraction, a0, easing in and out of + // each stop; a longer leg takes longer, so the bottom row never runs away. + const s = floor(P.u) & 3, from = stop(P, s), to = stop(P, (s + 1) & 3); + P.u += (d * P.speed * 0.15) / (0.4 + abs(to - from) / 0.004); + const s2 = floor(P.u) & 3, A = stop(P, s2), B = stop(P, (s2 + 1) & 3); + P.alpha = A + (B - A) * ease(P.u - floor(P.u)); +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const { pos, idx, sep, row, lut } = P, a = P.alpha, nmax = P.nmax; + if (P.lutHue !== P.hue) { + // Colour by the rank of the length: yellow the longest, cyan the middle, + // magenta the shortest, and mixes for ranks in between. Mixed as light, + // not round the wheel, which would put a dark blue between cyan and + // magenta, then brought back to saturation 0.7 so a mix is not grey. + const c3 = new Float32Array(9); + for (let c = 0; c < 3; c++) hsv(P.hue + 0.15 + c * 0.36, 0.88, 1, c3, c * 3); + for (let i = 0; i < 64; i++) { + const r = (i / 63) * 2, j = min(1, floor(r)), f = r - j, o = i * 3; + for (let ch = 0; ch < 3; ch++) lut[o + ch] = c3[j * 3 + ch] * (1 - f) + c3[j * 3 + 3 + ch] * f; + const mx = max(lut[o], lut[o + 1], lut[o + 2]), sat = 1 - min(lut[o], lut[o + 1], lut[o + 2]) / mx; + if (sat < 0.7) for (let ch = 0; ch < 3; ch++) lut[o + ch] = mx - ((mx - lut[o + ch]) * 0.7) / max(sat, 1e-3); + } + P.lutHue = P.hue; + } + const dv = [0, 0, 0, 0], dl = [0, 0, 0, 0], rank = [0, 0, 0, 0]; + let n = 0; + for (let y = 0; y < H; y++) { + // Row y holds n points, n growing down the panel. New points go into the + // sorted list one at a time, so a frame sorts nothing twice. + const want = 1 + floor((y * nmax) / H); + let tick = -1; + while (n < want) { + const p = n * a - floor(n * a); + let lo = 0, hi = n; + while (lo < hi) { + const m = (lo + hi) >> 1; + if (pos[m] < p) lo = m + 1; + else hi = m; + } + for (let j = n; j > lo; j--) (pos[j] = pos[j - 1]), (idx[j] = idx[j - 1]); + pos[lo] = p; + idx[lo] = n; + tick = floor(p * W); + n++; + } + pos[n] = 1; + idx[n] = 0; + // The gap from point i to point j is frac((j - i) a) long, so the index + // difference names the length, and the theorem says there are at most + // three. The newest point always lands in a longest gap and cuts it into + // one gap of each of the two shorter lengths already on the circle, which + // is why the longest is the sum of the other two. When the last longest + // gap is cut, the middle length is the longest and the cutting goes on. + let nc = 0; + for (let i = 0; i < n; i++) { + const dd = idx[i + 1] - idx[i]; + let c = 0; + while (c < nc && dv[c] !== dd) c++; + if (c === nc && nc < 4) (dv[nc] = dd), (dl[nc] = pos[i + 1] - pos[i]), nc++; + } + // Rank: how many lengths are longer, counting a length within 15% as + // half. Near a fraction p/q the shortest shrinks towards nothing and the + // other two meet, so their gaps go one colour as the three become two. + for (let c = 0; c < nc; c++) { + let r = 0; + for (let e = 0; e < nc; e++) if (e !== c) r += cl(0.5 + (dl[e] - dl[c]) / (0.3 * max(dl[e], dl[c])), 0, 1); + rank[c] = min(2, r); + } + // Points closer than a third of an LED count as one. A lone one between + // two gaps wider than an LED draws a dark line, darker the wider they are. + sep.fill(0); + let first = 0; + while (first < n - 1 && (pos[first + 1] - pos[first]) * W < 0.34) first++; + for (let i = 0, prev = -1; i < n; i++) { + const gr = (pos[i + 1] - pos[i]) * W; + if (gr < 0.34 && i < n - 1) continue; + const gl = prev < 0 ? (pos[first] + 1 - pos[n - 1]) * W : (pos[i] - pos[prev]) * W; + const x = floor(pos[i] * W); + sep[x] = max(sep[x], cl((min(gl, gr) - 1.1) / 2.4, 0, 1)); + prev = i; + } + // Four samples to an LED. Brightness follows the gap's length times n, + // which runs about 0.4 to 1.9 at 1/phi: a gap brightens down the panel + // until a point cuts it. + row.fill(0); + let g = 0; + for (let x = 0; x < W; x++) + for (let s = 0; s < SS; s++) { + const u = (x + (s + 0.5) / SS) / W; + while (pos[g + 1] <= u) g++; + const dd = idx[g + 1] - idx[g]; + let c = 0; + while (c < nc - 1 && dv[c] !== dd) c++; + const v = 0.6 + 0.4 * cl((n * (pos[g + 1] - pos[g]) - 0.4) / 1.3, 0, 1), q = floor(rank[c] * 31.5) * 3; + row[x * 3] += lut[q] * v; + row[x * 3 + 1] += lut[q + 1] * v; + row[x * 3 + 2] += lut[q + 2] * v; + } + // The newest point, white on the row where it arrives: they fall on a + // lattice, and each heads the dark line its point draws below. + for (let x = 0; x < W; x++) { + const f = (255 / SS) * (1 - 0.85 * sep[x]); + if (x === tick) display.setPixel(x, y, 255, 255, 255); + else display.setPixel(x, y, row[x * 3] * f, row[x * 3 + 1] * f, row[x * 3 + 2] * f); + } + } +} From de5ba505834066bd65804705573ed0a07c527886 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 11:32:05 +0000 Subject: [PATCH 06/15] =?UTF-8?q?Percolation=20=ED=8C=A8=ED=84=B4=EC=9D=84?= =?UTF-8?q?=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_01QuEe7qhvCVuDuGNB5fZ113 --- .../pattern-study/patterns/percolation.js | 313 ++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 incoming/pattern-study/patterns/percolation.js diff --git a/incoming/pattern-study/patterns/percolation.js b/incoming/pattern-study/patterns/percolation.js new file mode 100644 index 000000000..cb3f34558 --- /dev/null +++ b/incoming/pattern-study/patterns/percolation.js @@ -0,0 +1,313 @@ +// ===== Patternflow pattern ===== +// Title: Percolation +// Family: cells +// Idea: Site percolation on the panel's 64x128 square lattice: every LED keeps one random number and is open while that number is below p, open neighbours join into clusters of their own colour, and as p sweeps slowly up through the critical point 0.5927 the clusters merge until one first joins the top row to the bottom row, flashes white and burns gold, with its shortest open path traced in white and drops running down it. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: percolation (Simon Broadbent and John Hammersley, 1957), pattern round 30 +// Round: 30 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Window=0.04..0.2, Small=-1..1 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the clusters take hues from green through cyan and blue to magenta (0.33 to 0.88 of the wheel) and the crossing cluster is gold at 0.125; the gold always sits in the middle of the gap the cluster hues leave, so at 0.5 the clusters run magenta through red and orange to green and the crossing one is periwinkle. +// Knob 2: Speed. How fast p rises and falls. At 2 one rise and fall takes 12 s at the opening Window (25 s at Window 0.2), and the first crossing comes 2.7 s in; at 3 it comes after 1.8 s, at 0.2 after 27 s. +// Knob 3: Window. How far below p_c the sweep goes; it always ends 0.3 of that above p*, the lowest p at which this lattice has an open path from top to bottom. At 0.04 p runs 0.553 to 0.616 on the study's lattice and stays where clusters of every size touch; at 0.2 it reaches down to 0.39, where the open sites stand apart in small clusters on black, and ends at 0.66, where one cluster holds nearly every open site. +// Knob 4: Small. How bright the smaller clusters burn against the largest, which is always at full. At -1 brightness goes as the fourth power of log size over log largest size, so only the few biggest clusters show; at 0 a lone open site burns at 30%, and at 1 at 80%, and every cluster shows as a patch of its own colour. +// Buttons: K1 tap draws a fresh number for every site, finds the new lattice's p* and sets the sweep to reach it 2.7 seconds later. + +const W = 64, H = 128, N = W * H; +const { min, max, floor, exp, log, cos, sqrt, acos, imul } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// Site percolation threshold of the square lattice with edge neighbours, +// found numerically (Newman and Ziff, 2000, give 0.59274621). Below it every +// cluster on the infinite lattice is finite; above it one is infinite; at it +// the clusters are fractal, of dimension 91/48, at every scale. +const PC = 0.592746; +// Phase speed of the sweep per unit of Speed, over the root of the sweep's +// width in p, so a wide Window sweeps faster in p but not in proportion. +const RATE = 0.0657; +// Crossing comes this many seconds after the start, at Speed 2. +const LEAD = 2.7; + +// Union-find with union by size and path halving. +function find(par, i) { + while (par[i] !== i) { + const g = par[par[i]]; + par[i] = g; + i = g; + } + return i; +} + +function join(par, sz, a, b) { + a = find(par, a); + b = find(par, b); + if (a === b) return; + if (sz[a] < sz[b]) { + const t = a; + a = b; + b = t; + } + par[b] = a; + sz[a] += sz[b]; +} + +function hash(i) { + let h = imul(i ^ 0x9e3779b9, 0x85ebca6b); + h ^= h >>> 13; + h = imul(h, 0xc2b2ae35); + h ^= h >>> 16; + return (h >>> 0) / 4294967296; +} + +// p*, the lattice's own crossing point: open the sites one at a time in the +// order of their numbers, as Newman and Ziff do, with the top row joined to +// one extra node and the bottom row to another, and stop when the two meet. +// On a 64-wide strip it lands a few hundredths either side of p_c; the +// study's seed gives 0.6038. +function threshold(P) { + const { u, par, sz } = P; + const order = Array.from({ length: N }, (_, i) => i).sort((a, b) => u[a] - u[b]); + const on = new Uint8Array(N); + (par[N] = N), (par[N + 1] = N + 1), (sz[N] = sz[N + 1] = 1); + for (const i of order) { + (on[i] = 1), (par[i] = i), (sz[i] = 1); + const x = i % W, y = (i / W) | 0; + if (x > 0 && on[i - 1]) join(par, sz, i, i - 1); + if (x < W - 1 && on[i + 1]) join(par, sz, i, i + 1); + if (y === 0) join(par, sz, i, N); + else if (on[i - W]) join(par, sz, i, i - W); + if (y === H - 1) join(par, sz, i, N + 1); + else if (on[i + W]) join(par, sz, i, i + W); + if (find(par, N) === find(par, N + 1)) return u[i]; + } + return 1; +} + +function lattice(P) { + for (let i = 0; i < N; i++) P.u[i] = Math.random(); + P.pstar = threshold(P); + P.id.fill(-1); + (P.span = false), (P.soft = 1); +} + +// The sweep runs from W below p_c (or below p*, on a lattice that crosses +// early) to 0.3 W above p*, so every rise ends in a crossing. Returns the +// phase speed. +function sweep(P, win, speed) { + P.lo = min(PC, P.pstar) - win; + P.hi = P.pstar + 0.3 * win; + return (RATE * speed) / sqrt(P.hi - P.lo); +} + +// Puts the phase LEAD seconds before the crossing at the given Window and +// Speed. p = lo + (hi - lo)(1 - cos phase) / 2. +function aim(P, win, speed) { + const w = sweep(P, win, speed); + P.phase = acos(cl(1 - (2 * (P.pstar - P.lo)) / (P.hi - P.lo), -1, 1)) - LEAD * w; +} + +export function setup(params) { + const I = () => new Int32Array(N + 2), F = () => new Float32Array(N); + const P = (params.s = { + u: F(), jit: F(), sat: F(), glow: F(), cr: F(), cg: F(), cb: F(), + par: I(), sz: I(), root: I(), id: I(), idSize: I(), idHue: F(), + best: I(), bestS: I(), muSite: I(), rid: I(), owner: I(), prev: I(), queue: I(), path: I(), pathK: I().fill(-1), + flag: new Uint8Array(N), used: new Uint8Array(N), + pathLen: 0, cur: 0, maxSize: 1, spanRoot: -1, flash: 0, pulse: 0, p: 0.5, pstar: PC, dt: 1 / 30, + hue: 0, speed: 2, win: 0.04, small: 0.06, soft: 1, phase: 0, span: false, lo: 0.55, hi: 0.62, + }); + // A fixed tint per LED: hue off by up to 0.05 and saturation 0.6 to 0.98. + for (let i = 0; i < N; i++) (P.jit[i] = (hash(i + 7919) - 0.5) * 0.1), (P.sat[i] = 0.6 + 0.38 * hash(i + 104729)); + lattice(P); + // The opening is aimed for Window 0.04 and Speed 2 whatever the knobs say, + // so a faster Speed crosses sooner and a wider Window starts further down. + aim(P, 0.04, 2); +} + +// Labels the clusters at the current p and hands each one a colour. A +// cluster's colour is an id with a hue. When clusters merge, the merged one +// keeps the id of whichever part was biggest a frame ago; when one splits, +// the biggest piece keeps it and the rest take fresh ids, hued from their +// lowest-numbered site. So the big clusters hold their colour through the +// whole sweep and only the small ones change. +function label(P) { + const { u, par, sz, root, flag, muSite, best, bestS, rid, owner, used, id, idSize, idHue, p } = P; + for (let i = 0; i < N; i++) { + if (u[i] < p) (par[i] = i), (sz[i] = 1); + else par[i] = -1; + } + for (let i = 0; i < N; i++) { + if (par[i] < 0) continue; + if (i % W && par[i - 1] >= 0) join(par, sz, i, i - 1); + if (i >= W && par[i - W] >= 0) join(par, sz, i, i - W); + } + for (let i = 0; i < N; i++) { + const r = par[i] < 0 ? -1 : find(par, i); + root[i] = r; + if (r === i) (flag[i] = 0), (muSite[i] = i), (best[i] = -1), (bestS[i] = -1); + } + // Which rows each cluster touches (1 top, 2 bottom), its lowest site, and + // of the ids its sites wore last frame, the one whose cluster was biggest. + for (let i = 0; i < N; i++) { + const r = root[i]; + if (r < 0) continue; + if (i < W) flag[r] |= 1; + if (i >= N - W) flag[r] |= 2; + if (u[i] < u[muSite[r]]) muSite[r] = i; + const a = id[i]; + if (a >= 0 && idSize[a] > bestS[r]) (bestS[r] = idSize[a]), (best[r] = a); + } + owner.fill(-1); + for (let r = 0; r < N; r++) { + if (root[r] !== r) continue; + const c = best[r]; + if (c >= 0 && (owner[c] < 0 || sz[r] > sz[owner[c]])) owner[c] = r; + } + used.fill(0); + for (let r = 0; r < N; r++) { + if (root[r] !== r) continue; + const c = best[r]; + if (c >= 0 && owner[c] === r) (rid[r] = c), (used[c] = 1); + else rid[r] = -1; + } + let big = 1, span = -1; + for (let r = 0; r < N; r++) { + if (root[r] !== r) continue; + if (rid[r] < 0) { + while (used[P.cur]) P.cur = (P.cur + 1) % N; + (rid[r] = P.cur), (used[P.cur] = 1), (idHue[P.cur] = hash(muSite[r]) - 0.5); + } + idSize[rid[r]] = sz[r]; + if (sz[r] > big) big = sz[r]; + if (flag[r] === 3 && (span < 0 || sz[r] > sz[span])) span = r; + } + for (let i = 0; i < N; i++) id[i] = root[i] < 0 ? -1 : rid[root[i]]; + P.maxSize = big; + P.spanRoot = span; +} + +// The shortest open path from the top row to the bottom row inside the +// crossing cluster: a breadth-first search from all its top-row sites at +// once, stopped at the first bottom-row site it reaches. +function shortest(P, R) { + const { root, prev, queue, pathK, path } = P; + for (let j = 0; j < P.pathLen; j++) pathK[path[j]] = -1; + P.pathLen = 0; + if (R < 0) return; + prev.fill(-2); + let qh = 0, qt = 0, end = -1; + for (let x = 0; x < W; x++) if (root[x] === R) (prev[x] = -1), (queue[qt++] = x); + while (qh < qt) { + const i = queue[qh++]; + if (i >= N - W) { + end = i; + break; + } + const x = i % W; + let n = i + W; + if (root[n] === R && prev[n] === -2) (prev[n] = i), (queue[qt++] = n); + n = i - 1; + if (x > 0 && root[n] === R && prev[n] === -2) (prev[n] = i), (queue[qt++] = n); + n = i + 1; + if (x < W - 1 && root[n] === R && prev[n] === -2) (prev[n] = i), (queue[qt++] = n); + n = i - W; + if (n >= 0 && root[n] === R && prev[n] === -2) (prev[n] = i), (queue[qt++] = n); + } + let len = 0; + for (let i = end; i >= 0; i = prev[i]) len++; + let j = len; + for (let i = end; i >= 0; i = prev[i]) (path[--j] = i), (pathK[i] = j); + P.pathLen = len; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0.04, 0.06], d = cl(dt || 0, 0, 0.1); + P.hue = k[0]; + P.speed = cl(k[1], 0.2, 3); + P.win = cl(k[2], 0.04, 0.2); + P.small = cl(k[3], -1, 1); + // K1: a fresh lattice, crossed 2.7 s from now at the current knobs. + if (input.btnPressed && input.btnPressed[0]) lattice(P), aim(P, P.win, P.speed); + P.phase += sweep(P, P.win, P.speed) * d; + P.p = P.lo + ((P.hi - P.lo) * (1 - cos(P.phase))) / 2; + label(P); + // The flash comes on the frame a cluster first joins top to bottom and + // fades in 0.6 s; the gold stays while it spans and fades in 0.35 s after. + const spanning = P.spanRoot >= 0; + if (spanning && !P.span) P.flash = 1; + P.span = spanning; + P.flash *= exp(-d / 0.6); + P.soft *= exp(-d / 0.5); + P.pulse += (30 + 25 * P.speed) * d; + const fall = exp(-d / 0.35), { root, flag, glow } = P; + for (let i = 0; i < N; i++) { + const r = root[i]; + if (r >= 0 && flag[r] === 3) glow[i] = 1; + else glow[i] *= fall; + } + shortest(P, P.spanRoot); + P.dt = d; +} + +const C = [0, 0, 0]; +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = h | 0, f = h - i, p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + if (i === 0) (C[0] = v), (C[1] = t), (C[2] = p); + else if (i === 1) (C[0] = q), (C[1] = v), (C[2] = p); + else if (i === 2) (C[0] = p), (C[1] = v), (C[2] = t); + else if (i === 3) (C[0] = p), (C[1] = q), (C[2] = v); + else if (i === 4) (C[0] = t), (C[1] = p), (C[2] = v); + else (C[0] = v), (C[1] = p), (C[2] = q); +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const { u, root, id, sz, idHue, jit, sat, glow, pathK, cr, cg, cb, p } = P; + const gold = 0.125 + P.hue, sm = P.small, fl = P.flash; + // Small: brightness lo + (1 - lo) f^gam, f = log size / log largest size. + const lo = sm < 0 ? 0.3 * (1 + sm) : 0.3 + 0.5 * sm, gam = sm < 0 ? 1.2 - 2.8 * sm : 1.2; + const lb = 1 / log(max(2, P.maxSize)); + // Each LED eases to its colour in 0.1 s, so a merge washes over a cluster + // instead of blinking; after a K1 tap, in 0.7 s. + const kk = 1 - exp(-P.dt / (0.1 + 0.6 * P.soft)); + for (let i = 0; i < N; i++) { + const r = root[i]; + let R, G, B; + if (r < 0) { + // A closed site keeps an ember under 20 of 255, brightest when its + // number is just above p and it is next to open. + const e = 1 - (u[i] - p) / (1 - p); + hsv(gold + 0.5 + jit[i], sat[i], 0.075 * e * e); + (R = C[0]), (G = C[1]), (B = C[2]); + } else { + // An open site burns brighter the longer it has been open, from 42% at + // u = p to full at u = 0, and comes up over its first 0.004 of p. + const f = log(sz[r]) * lb, v = lo + (1 - lo) * f ** gam; + const g = (0.42 + 0.58 * (1 - u[i] / p)) * cl((p - u[i]) / 0.004, 0, 1); + hsv(gold + 0.475 + 0.55 * idHue[id[i]] + jit[i], sat[i], v * g); + (R = C[0]), (G = C[1]), (B = C[2]); + const w = glow[i]; + if (w > 0.002) { + hsv(gold + 0.4 * jit[i], (0.4 + 0.45 * sat[i]) * (1 - fl), (0.6 + 0.38 * g) * (1 - fl) + fl); + (R += (C[0] - R) * w), (G += (C[1] - G) * w), (B += (C[2] - B) * w); + } + // The shortest path: white, with a drop every 48 sites running down it. + const j = pathK[i]; + if (j >= 0) { + const q = (((P.pulse - j) % 48) + 48) % 48, b = 0.65 + 0.35 * max(0, 1 - q / 14); + (R += (1 - R) * b), (G += (0.97 - G) * b), (B += (0.9 - B) * b); + } + } + cr[i] += (R * 255 - cr[i]) * kk; + cg[i] += (G * 255 - cg[i]) * kk; + cb[i] += (B * 255 - cb[i]) * kk; + display.setPixel(i % W, (i / W) | 0, cr[i], cg[i], cb[i]); + } +} From 81369e891868c5d92f4ca7b7b004a1ed875e5dac Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 11:33:08 +0000 Subject: [PATCH 07/15] =?UTF-8?q?Outer=20Billiards=20=ED=8C=A8=ED=84=B4?= =?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_01QuEe7qhvCVuDuGNB5fZ113 --- .../pattern-study/patterns/outer-billiards.js | 304 ++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 incoming/pattern-study/patterns/outer-billiards.js diff --git a/incoming/pattern-study/patterns/outer-billiards.js b/incoming/pattern-study/patterns/outer-billiards.js new file mode 100644 index 000000000..3c0479452 --- /dev/null +++ b/incoming/pattern-study/patterns/outer-billiards.js @@ -0,0 +1,304 @@ +// ===== Patternflow pattern ===== +// Title: Outer Billiards +// Family: geometry +// Idea: A regular pentagon in the middle of the panel and the plane round it coloured by how many outer billiard jumps bring each point back, so Tabachnikov's necklaces of decagons and pentagons ring it to the top and bottom of the panel; one white point hops along its orbit, each jump a streak through the corner it reflects in. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: outer billiards (Bernhard Neumann, 1959) and Sergei Tabachnikov's regular pentagon (1995), pattern round 30 +// Round: 30 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Sides=5..12, Zoom=-1..1 +// +// Knob 1: Hue. Turns every period's colour round the wheel together. At 0 the five big decagons round the pentagon (period 5) are violet, the ring of pentagons outside them (period 20) pink and the big decagons above and below (period 15) orange; at 0.25 the same three are red, yellow and green. +// Knob 2: Speed. At 2 the point jumps 2.4 times a second and the plane turns once in 70 seconds; at 0.2 it jumps once in 4 seconds and the turn takes 12 minutes. +// Knob 3: Sides. The polygon, 5 to 12 sides, opening on the pentagon. The hexagon (6) has its corners on a lattice and its plane breaks into hexagons and triangles that repeat; every other count gives rings of polygons of many sizes with dark residue between. A new count fills in outward from the polygon in about half a second. +// Knob 4: Zoom. How many LEDs a corner of the polygon stands from its centre: 5.4 at the start, 2.3 at -1, where the panel's corners reach 31 times that out, and 12 at 1. +// Buttons: K1 tap drops the orbit and starts another, in the biggest domain of forty random tries whose period is not one of the last two. + +const W = 64, H = 128, NP = W * H; +const { min, max, floor, ceil, sqrt, sin, cos, atan2, log, exp, PI, round, random } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const TAU = 2 * PI; + +// The map has the polygon's symmetry, turns by 2pi/n and mirrors in its +// axes (a mirror turns the map into its inverse, which has the same periods), +// so one wedge of angle pi/n holds it all. The wedge is gridded log-polar: +// radius RA to RMAX in CR cells per e-fold, angle in steps of 1/CR. +const RA = 0.7, RMAX = 33, CR = 110, NI = ceil(CR * log(RMAX / RA)); +const MAXIT = 4000, INSIDE = 65535, BUDGET = 500000; + +// Four sample points in each LED: radius in LEDs, CR times its log, angle in turns. +const SR = new Float32Array(NP * 4), SL = new Float32Array(NP * 4), SA = new Float32Array(NP * 4); +for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) + for (let s = 0; s < 4; s++) { + const q = (y * W + x) * 4 + s, dx = x + (s & 1 ? 0.75 : 0.25) - 32, dy = 64 - y - (s & 2 ? 0.75 : 0.25); + SR[q] = sqrt(dx * dx + dy * dy); + SL[q] = CR * log(SR[q]); + SA[q] = atan2(dy, dx) / TAU; + } + +// A regular n-gon on the unit circle, vertex 0 on the x axis; edge k runs +// from vertex k to k + 1 and its outward normal is NX, NY. +function makePoly(n) { + const G = { n, r: cos(PI / n), NJ: ceil((PI / n) * CR), next: 0, VX: [], VY: [], NX: [], NY: [] }; + for (let k = 0; k < n; k++) { + G.VX.push(cos((TAU * k) / n)); + G.VY.push(sin((TAU * k) / n)); + G.NX.push(cos((TAU * (k + 0.5)) / n)); + G.NY.push(sin((TAU * (k + 0.5)) / n)); + } + G.M = new Uint16Array(NI * G.NJ); + return G; +} + +// The vertex x reflects in: the first, counterclockwise, of the run of edges +// x can see, so the polygon lies right of the ray from x through it. -1 inside. +function vtx(G, x, y) { + const { n, NX, NY, r } = G; + let prev = x * NX[n - 1] + y * NY[n - 1] > r; + for (let k = 0; k < n; k++) { + const vis = x * NX[k] + y * NY[k] > r; + if (vis && !prev) return k; + prev = vis; + } + return -1; +} + +// Two jumps make a translation, so after an even number of jumps a periodic +// point is back exactly, up to rounding. A domain whose centre has odd period p +// is turned half round by p jumps, and its points come back after 2p; every +// point of a domain gets the same number that way. +let spent = 0; +function period(G, x0, y0) { + let x = x0, y = y0; + for (let it = 1; it <= MAXIT; it++) { + const k = vtx(G, x, y); + if (k < 0) return (spent += it), INSIDE; + x = 2 * G.VX[k] - x; + y = 2 * G.VY[k] - y; + if (!(it & 1) && (x - x0) ** 2 + (y - y0) ** 2 < 1e-14) return (spent += it), it; + } + spent += MAXIT; + return 0; +} + +// Fill the wedge outward from the polygon, BUDGET jumps a frame. +function grow(G) { + spent = 0; + const total = NI * G.NJ; + while (G.next < total && spent < BUDGET) { + const c = G.next++, i = (c / G.NJ) | 0, a = (c - i * G.NJ + 0.5) / CR, rho = RA * exp((i + 0.5) / CR); + G.M[c] = period(G, rho * cos(a), rho * sin(a)); + } +} + +// The domain of a periodic point: the points that see the same vertices for p +// jumps. After j jumps a point X is at s X + c; each jump adds two half +// planes, and a square clipped by them all is the domain. Its centroid. +function domain(G, x0, y0, p) { + let poly = [[x0 - 30, y0 - 30], [x0 + 30, y0 - 30], [x0 + 30, y0 + 30], [x0 - 30, y0 + 30]]; + const clip = (ax, ay, b) => { + const out = []; + for (let i = 0; i < poly.length; i++) { + const A = poly[i], B = poly[(i + 1) % poly.length], d1 = ax * A[0] + ay * A[1] - b, d2 = ax * B[0] + ay * B[1] - b; + if (d1 <= 0) out.push(A); + if (d1 < 0 !== d2 < 0) out.push([A[0] + (d1 / (d1 - d2)) * (B[0] - A[0]), A[1] + (d1 / (d1 - d2)) * (B[1] - A[1])]); + } + poly = out; + }; + let s = 1, cx = 0, cy = 0, x = x0, y = y0; + for (let j = 0; j < p && poly.length; j++) { + const k = vtx(G, x, y), m = (k + G.n - 1) % G.n; + clip(-s * G.NX[k], -s * G.NY[k], G.NX[k] * cx + G.NY[k] * cy - G.r); + clip(s * G.NX[m], s * G.NY[m], G.r - G.NX[m] * cx - G.NY[m] * cy); + x = 2 * G.VX[k] - x; + y = 2 * G.VY[k] - y; + cx = 2 * G.VX[k] - cx; + cy = 2 * G.VY[k] - cy; + s = -s; + } + let A = 0, X = 0, Y = 0; + for (let i = 0; i < poly.length; i++) { + const [x1, y1] = poly[i], [x2, y2] = poly[(i + 1) % poly.length], c = x1 * y2 - x2 * y1; + A += c; + X += (x1 + x2) * c; + Y += (y1 + y2) * c; + } + return A > 1e-9 ? [X / (3 * A), Y / (3 * A), A / 2] : [x0, y0, 0]; +} + +const POLYS = {}; +export function setup(params) { + params.s = { + hue: 0, speed: 2, n: 5, zoom: 5.2, rot: PI / 2, t: 0, + orb: null, k: 0, ph: 0, hops: 0, seen: [], marks: [], + lut: new Float32Array((MAXIT + 1) * 3), lutKey: "", + buf: new Float32Array(NP * 3), + }; +} + +// A seed: of forty random cells of the wedge, the one whose domain is biggest +// on the panel, give or take, and not one of the last two periods, moved to its +// domain's centroid. The orbit is kept as a list, so it closes exactly however +// long it runs, and must stay within 44 LEDs of the centre: the 128 rows hold +// all of it and the 64 columns most. +function pick(P, G) { + const Z = P.zoom, lo = 1.1, hi = 40 / Z; + let best = 0; + for (let t = 0; t < 40; t++) { + const i = floor(CR * log((lo + (hi - lo) * random()) / RA)), j = floor((random() * PI * CR) / G.n), c = i * G.NJ + j; + if (c >= G.next) continue; + const p = G.M[c], rho = RA * exp((i + 0.5) / CR), a = (j + 0.5) / CR; + if (!p || p === INSIDE || p > 12 * G.n) continue; + const [cx, cy, area] = domain(G, rho * cos(a), rho * sin(a), p); + const score = sqrt(min(area * Z * Z, 300)) * (0.5 + random()) * (P.seen.includes(p) ? 0.25 : 1); + if (area * Z * Z < 8 || score <= best) continue; + const ox = [cx], oy = [cy]; + let far = 0; + for (let h = 0; h < p; h++) { + const k = vtx(G, ox[h], oy[h]); + if (k < 0) break; + ox.push(2 * G.VX[k] - ox[h]); + oy.push(2 * G.VY[k] - oy[h]); + far = max(far, sqrt(ox[h] ** 2 + oy[h] ** 2)); + } + if (ox.length <= p || far * Z > 44) continue; + best = score; + P.orb = { ox, oy, p, far }; + } + if (best) { + P.seen = [P.orb.p, P.seen[0]]; + P.k = P.ph = P.hops = 0; + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + P.hue = kv[0]; + P.speed = cl(kv[1], 0.2, 3); + const n = cl(round(kv[2]), 5, 12); + P.zoom = 5.2 * 2 ** (1.2 * cl(kv[3], -1, 1)); + if (n !== P.n || !P.G) { + P.n = n; + P.G = POLYS[n] || (POLYS[n] = makePoly(n)); + P.orb = null; + } + const G = P.G; + grow(G); + P.t += d; + P.rot += d * P.speed * 0.045; + // K1, or a zoom that has pushed the orbit off the panel: a new orbit. + if ((input.btnPressed && input.btnPressed[0]) || (P.orb && P.orb.far * P.zoom > 52)) P.orb = null; + if (!P.orb) { + if (G.next > CR * log(30 / P.zoom / RA) * G.NJ) pick(P, G); + } else { + P.ph += d * P.speed * 1.2; + while (P.ph >= 1) { + P.ph -= 1; + const o = P.orb; + P.k = (P.k + 1) % o.p; + P.marks.push([o.ox[P.k], o.oy[P.k], P.t, cl(o.p / (P.speed * 1.2), 4, 12)]); + if (P.marks.length > 120) P.marks.shift(); + if (++P.hops >= max(o.p, 2 * o.p * (o.p < 16))) P.orb = null; + } + } +} + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + const rgb = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + for (let c = 0; c < 3; c++) out[o + c] = rgb[c]; +} + +// The colour of a period. +function makeLut(P) { + const L = P.lut; + for (let p = 1; p <= MAXIT; p++) { + const v = cl(1.1 - 0.12 * log(p / (2 * P.n)) / log(2), 0.5, 1); + hsv(P.hue + 0.55 + 0.618034 * p, 0.85, v * 255, L, p * 3); + } + P.lutKey = P.hue + "," + P.n; +} + +// Additive splat with bilinear weights, in LEDs. +function splat(buf, x, y, a, r, g, b) { + const x0 = floor(x - 0.5), y0 = floor(y - 0.5), fx = x - 0.5 - x0, fy = y - 0.5 - y0; + for (let j = 0; j < 2; j++) + for (let i = 0; i < 2; i++) { + const X = x0 + i, Y = y0 + j; + if (X < 0 || X >= W || Y < 0 || Y >= H) continue; + const w = a * (i ? fx : 1 - fx) * (j ? fy : 1 - fy), q = (Y * W + X) * 3; + buf[q] += w * r; + buf[q + 1] += w * g; + buf[q + 2] += w * b; + } +} + +export function draw(display, params) { + const P = params.s; + if (!P || !P.G) return; + const G = P.G, { n, M, NJ, r } = G, Z = P.zoom, buf = P.buf, L = P.lut; + if (P.lutKey !== P.hue + "," + n) makeLut(P); + const off = CR * (log(Z) + log(RA)), turn = (P.rot / TAU) * n, K = (TAU / n) * CR, pin = PI / n; + const w = 0.55 / Z; + for (let p = 0; p < NP; p++) { + let R = 0, Gc = 0, B = 0, first = -1, mixed = 0, tab = 0, rim = 0; + for (let s = 0; s < 4; s++) { + const q = p * 4 + s, rho = SR[q] / Z; + let a = SA[q] * n - turn; + a -= floor(a); + if (a > 0.5) a = 1 - a; + let per = 0; + // In the polygon, or on its rim. + if (rho < 1 + w) { + const dd = rho * cos(pin - (a * TAU) / n) - r; + if (dd < w) { + dd > -w ? rim++ : tab++; + continue; + } + } + const i = floor(SL[q] - off), c = i * NJ + floor(a * K); + if (i >= 0 && i < NI && c < G.next) per = M[c]; + if (per === INSIDE) per = 0; + if (first < 0) first = per; + else if (per !== first) mixed = 1; + if (per) (R += L[per * 3]), (Gc += L[per * 3 + 1]), (B += L[per * 3 + 2]); + } + // Where the period changes inside a LED, a dark seam. + const f = mixed || (tab && R + Gc + B) ? 0.15 : 0.25; + buf[p * 3] = R * f + rim * 64 + tab * 9; + buf[p * 3 + 1] = Gc * f + rim * 63 + tab * 9; + buf[p * 3 + 2] = B * f + rim * 61 + tab * 12; + } + // The orbit: the points it has landed on, each fading over the time one + // round takes (4 to 12 seconds), and the jump under way, a streak from the + // point through the vertex to its image. + const ca = cos(P.rot), sa = sin(P.rot); + const sx = (x, y) => 32 + Z * (x * ca - y * sa), sy = (x, y) => 64 - Z * (x * sa + y * ca); + for (const [x, y, t, life] of P.marks) { + const a = 1 - (P.t - t) / life; + if (a > 0) splat(buf, sx(x, y), sy(x, y), a * 255, 1, 1, 1); + } + const o = P.orb; + if (o) { + const k = P.k, k1 = (k + 1) % o.p, x0 = o.ox[k], y0 = o.oy[k], x1 = o.ox[k1], y1 = o.oy[k1]; + const u = cl(P.ph / 0.4, 0, 1), e = u * u * (3 - 2 * u), fade = P.ph < 0.4 ? 1 : 1 - (P.ph - 0.4) / 0.6; + const ax = sx(x0, y0), ay = sy(x0, y0), bx = sx(x1, y1), by = sy(x1, y1); + const len = sqrt((bx - ax) ** 2 + (by - ay) ** 2), steps = ceil((len * e) / 0.4); + for (let j = 0; j <= steps; j++) { + const v = steps ? (j / steps) * e : 0; + splat(buf, ax + (bx - ax) * v, ay + (by - ay) * v, 170 * fade * (0.3 + 0.7 * (v / (e || 1))), 1, 0.97, 0.9); + } + const hx = ax + (bx - ax) * e, hy = ay + (by - ay) * e; + splat(buf, hx, hy, 400, 1, 1, 1); + const vx = (ax + bx) / 2, vy = (ay + by) / 2, fl = exp(-(((e - 0.5) * 5) ** 2)) * fade; + splat(buf, vx, vy, 500 * fl, 1, 1, 1); + for (let j = 0; j < 8; j++) splat(buf, vx + 1.5 * cos((j * PI) / 4), vy + 1.5 * sin((j * PI) / 4), 140 * fl, 1, 1, 1); + } + for (let p = 0, y = 0; y < H; y++) for (let x = 0; x < W; x++, p += 3) display.setPixel(x, y, min(255, buf[p]), min(255, buf[p + 1]), min(255, buf[p + 2])); +} From 57dfdc0b2b8bb23bf4a90e002894a1f799777dea Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 11:33:33 +0000 Subject: [PATCH 08/15] =?UTF-8?q?Swatch=20Row=20=ED=8C=A8=ED=84=B4?= =?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_01QuEe7qhvCVuDuGNB5fZ113 --- incoming/pattern-study/patterns/swatch-row.js | 271 ++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 incoming/pattern-study/patterns/swatch-row.js diff --git a/incoming/pattern-study/patterns/swatch-row.js b/incoming/pattern-study/patterns/swatch-row.js new file mode 100644 index 000000000..ff09cc77e --- /dev/null +++ b/incoming/pattern-study/patterns/swatch-row.js @@ -0,0 +1,271 @@ +// ===== Patternflow pattern ===== +// Title: Swatch Row +// Family: cosmetic +// Idea: A shade range swatched down an ivory card: an unseen finger drags one stroke of cream pigment at a time, thick and opaque where it lands and breaking into feathered streaks where the pigment runs out, and the stack of eight glides up a row for every new one. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 30, after the makeup artist's swatch +// Round: 30 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Range=-1..1, Finish=-1..1 +// +// Knob 1: Hue. Turns the whole range round the colour wheel. At 0 it is rose, pale pink at the light end and berry at the deep end; at 0.12 it is a foundation range from nude to bronze, at 0.3 olive, at 0.5 mint to jade, at 0.75 periwinkle to indigo, at 0.85 lilac to plum. +// Knob 2: Speed. At 2 a finger drags one swatch across in 1.05 seconds, it rests 0.45 seconds, and the stack glides up 16 LEDs in 0.55 seconds, a new swatch every 2.05 seconds; at 0.2 one every 20 seconds, at 3 one every 1.4. +// Knob 3: Range. How far apart the shades are. At -1 all eight swatches are nearly one shade, at 0 they span the middle half of the range, at 1 they go from a pale tint at 98% brightness to a deep shade at 42%, the light ones leaning warm and the deep ones cool. The range runs light to deep and back over 14 swatches, so the stack shows a stretch of it. +// Knob 4: Finish. At -1 matte, flat pigment with only its drag streaks; at 0 satin, a soft highlight along the upper third of every stroke that brightens as a band of light crosses the card every 18 seconds; at 1 metallic, a narrow champagne highlight, a darker lower half with a second thin light along its edge, and one LED in seven of the pigment a fixed sparkle that twinkles. +// Buttons: K1 tap wipes the card clean from the top down and swatches all eight shades again at once, each with a new stroke. + +const W = 64, H = 128, SL = 16, ROWS = H / SL, NT = W * SL; +const { min, floor, sqrt, sin, exp, abs, round, random, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => { + const t = cl((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; +const mix = (a, b, t) => a + (b - a) * t; +const hash = (a, b, c) => { + let h = (a * 374761393 + b * 668265263 + c * 1013904223) | 0; + h = Math.imul(h ^ (h >>> 13), 1274126177); + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +}; + +// Rose sits at 0.955 of the wheel; K1 adds to it. +const ROSE = 0.955; + +// The card: warm ivory, 198/186/164 in the middle, 10% dimmer at the side +// edges and 7% at the ends, with a fixed paper grain of about 1%. +const GR = new Float32Array(W * H * 3); +for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const dx = (x + 0.5 - 32) / 32, dy = (y + 0.5 - 64) / 64; + const v = 1 - 0.1 * dx * dx - 0.07 * dy * dy + 0.024 * (hash(x, y, 7) - 0.5), i = (y * W + x) * 3; + (GR[i] = 198 * v), (GR[i + 1] = 186 * v), (GR[i + 2] = 164 * v); + } + +// Smooth value noise over a table of random numbers. +const n1 = (T, f) => { + const i = cl(floor(f), 0, T.length - 2), t = cl(f - i, 0, 1); + return T[i] + (T[i + 1] - T[i]) * t * t * (3 - 2 * t); +}; +const n2 = (T, n, fx, fy) => { + const i = cl(floor(fx), 0, T.length / n - 2), t = cl(fx - i, 0, 1), s = t * t * (3 - 2 * t); + return mix(n1(T.subarray(i * n, i * n + n), fy), n1(T.subarray(i * n + n, i * n + 2 * n), fy), s); +}; +const table = (n) => Float32Array.from({ length: n }, () => random()); + +// One swatch, baked once into fields over its 64x16 slot: how opaque the +// pigment is (A), how light against its own shade (V), where across the +// stroke the LED sits (Q, -1 at the top edge, 1 at the bottom), how much the +// highlight catches there (G), how far the finger must have come before it +// lays this LED (R), and a fixed number for the sparkle (K). +// +// The stroke lands with a round cap of radius r, runs straight with a little +// waver, and from a fraction ut of its length on the pigment runs out: the +// stroke narrows, goes sheer, and the drag streaks, which follow the stroke, +// drop out one by one, the weakest first, until the last one ends at xe. +function swatch(n) { + const s = { + n, jit: (random() - 0.5) * 0.05, seed: floor(random() * 1e6), + A: new Float32Array(NT), V: new Float32Array(NT), Q: new Float32Array(NT), G: new Float32Array(NT), R: new Float32Array(NT), K: new Float32Array(NT), + p: 0, go: false, wait: 0, xa: 0, xb: 0, prev: null, wipe: 1, wwait: 0, + }; + const r = 5.3 + random() * 0.6, x0 = 2 + random() * 3.5, xe = 58 + random() * 6, ut = 0.4 + random() * 0.16; + const slope = (random() - 0.5) * 0.04, amp = 0.2 + random() * 0.35, fr = 0.05 + random() * 0.06, ph = random() * 2 * PI, xm = (x0 + xe) / 2; + const T1 = table(24), T2 = table(24 * 10), T3 = table(24 * 24), TE = table(20); + for (let j = 0; j < SL; j++) + for (let x = 0; x < W; x++) { + const i = j * W + x, xc = x + 0.5; + const yc = 7.5 + slope * (xc - xm) + amp * sin(xc * fr + ph), q = j + 0.5 - yc, u = (xc - x0) / (xe - x0); + let hw; + if (xc < x0 + r) { + const d = (x0 + r - xc) / r; + hw = d >= 1 ? -1 : r * sqrt(1 - d * d); + } else hw = r * (1 - 0.06 * u - 0.24 * sm(ut, 1.1, u)); + hw += 0.5 * (n1(TE, xc / 4 + 2) - 0.5); + const cov = cl(hw - abs(q) + 0.5, 0, 1); + const st = 0.62 * n1(T1, q * 1.15 + 11) + 0.38 * n2(T2, 24, xc / 8, q + 11); + // In the tail the streaks also break along their length, where the + // last of the pigment skips over the card. + const thin = sm(ut, 1, u), brk = n2(T3, 24, xc / 2.6, q * 1.3 + 11); + const keep = sm(-0.2, 0.2, st * 1.1 + 0.3 - thin * (1.05 + 0.7 * brk)); + s.A[i] = cov * (0.93 + 0.07 * st) * keep * (1 - 0.5 * thin) * (1 - sm(0.94, 1.04, u)); + // Where the finger landed the pigment piles up a shade deeper, and the + // drag pushes a little more of it to the two long edges. + const ce = xc - (x0 + r - sqrt(Math.max(0, r * r - q * q))); + let v = (1 + 0.24 * (st - 0.5)) * (1 - 0.1 * sm(0.55, 1, abs(q) / Math.max(hw, 0.5))); + if (ce < 2.5) v *= 0.8 + 0.2 * cl(ce / 2.5, 0, 1); + s.V[i] = v; + s.Q[i] = q / Math.max(hw, 1); + s.G[i] = (0.45 + st) * (1 - 0.7 * thin); + s.R[i] = xc + 0.85 * (r - sqrt(Math.max(0, r * r - q * q))); + s.K[i] = hash(x, j, s.seed); + } + (s.xa = x0 - 1.5), (s.xb = xe + r + 2); + return s; +} + +// The finger sets off quickly and slows as the pigment runs out. +const travel = (p) => 0.4 * p * p * (3 - 2 * p) + 0.6 * (1 - (1 - p) * (1 - p)); +const fingerAt = (s) => s.xa + (s.xb - s.xa) * travel(s.p); + +export function setup(params) { + const L = []; + for (let k = 0; k <= ROWS; k++) { + const s = swatch(k); + if (k < ROWS - 1) s.p = 1; + L.push(s); + } + // The card opens with seven swatches down and the eighth being dragged. + (L[ROWS - 1].go = true), (L[ROWS - 1].p = 0.12); + params.s = { + L, n: ROWS + 1, off: 0, phase: 0, pt: 0, t: 0, sweep: 30, hue: 0, speed: 2, width: 0.52, fin: 0.06, + col: new Float32Array((ROWS + 1) * 6), SH: new Float32Array(64), SW: new Float32Array(180), + }; +} + +// The stack glides up a row: the top swatch leaves, an empty slot comes in +// at the bottom and the finger starts on the slot above it. +function shift(P) { + P.L.shift(); + P.L.push(swatch(P.n++)); + (P.off = 0), (P.phase = 0), (P.pt = 0); + const B = P.L[ROWS - 1]; + (B.go = true), (B.p = 0); +} + +// K1: a pad wipes each swatch off left to right, row after row, and a +// finger follows each wipe with a fresh stroke of the same shade. +function reswatch(P) { + if (P.phase === 2) shift(P); + P.phase = 0; + for (let k = 0; k < ROWS; k++) { + const old = P.L[k], s = swatch(old.n); + old.prev = null; + if (old.p > 0) (s.prev = old), (s.wipe = 0), (s.wwait = 0.07 * k); + (s.go = true), (s.p = 0), (s.wait = 0.07 * k + 0.3); + P.L[k] = s; + } +} + +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.width = 0.04 + 0.48 * (cl(k[2], -1, 1) + 1); + P.fin = cl(k[3], -1, 1); + const sp = P.speed, drag = 2.1 / sp, hold = 0.9 / sp, glide = 1.1 / sp; + P.t += d; + // A band of light crosses the card corner to corner every 18 s at Speed 2. + P.sweep += d * 7 * sp; + if (P.sweep > 200) P.sweep -= 250; + if (input.btnPressed && input.btnPressed[0]) reswatch(P); + for (const s of P.L) { + if (s.prev) { + if (s.wwait > 0) s.wwait -= d; + else if ((s.wipe += d / 0.45) >= 1) s.prev = null; + } + if (s.go && s.p < 1) { + if (s.wait > 0) s.wait -= d; + else s.p = min(1, s.p + d / drag); + } + } + const B = P.L[ROWS - 1]; + if (P.phase === 0) { + if (B.p >= 1) (P.phase = 1), (P.pt = 0); + } else if (P.phase === 1) { + if ((P.pt += d) >= hold) (P.phase = 2), (P.pt = 0); + } else { + P.pt += d; + const u = min(1, P.pt / glide); + P.off = SL * u * u * (3 - 2 * u); + if (u >= 1) shift(P); + } +} + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + const rgb = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + for (let c = 0; c < 3; c++) out[o + c] = 255 * rgb[c]; +} + +// The shade of swatch n: the range runs light to deep and back over 14 +// swatches, so the eight on the card always show a stretch of it. +function tone(P, s, m, out, o) { + const a = s.n % 14, tri = (a <= 7 ? a : 14 - a) / 7, w = P.width; + const d = cl(0.5 + w * (tri - 0.5) + s.jit, 0, 1); + const h = P.hue + ROSE + 0.05 * w * (0.5 - d); + hsv(h, 0.3 + 0.58 * d, (0.98 - 0.56 * d) * (1 + 0.12 * m), out, o); + // The highlight: a pale tint of the same hue, and towards metallic a + // champagne one. + hsv(h + 0.02 * m, (0.3 + 0.58 * d) * 0.35, 1, out, o + 3); + for (let c = 0; c < 3; c++) out[o + 3 + c] = mix(mix(out[o + 3 + c], 255, 0.35), [255, 226, 186][c], 0.55 * m); +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const f = P.fin, m = sm(0.15, 1, f), col = P.col, SH = P.SH, SW = P.SW, L = P.L; + // Matte keeps 3% of a sheen, satin 50%, metallic 95%, narrower as it + // goes; metallic also throws a weaker second light off the lower edge. + const kS = 0.03 + 0.47 * sm(-1, 0, f) + 0.45 * sm(0, 1, f), wS = f < 0 ? mix(0.4, 0.75, -f) : mix(0.4, 0.26, f); + for (let b = 0; b < 64; b++) { + const q = -1.6 + (3.2 * b) / 63, e = (q + 0.3) / wS, e2 = (q - 0.72) / 0.16; + SH[b] = kS * exp(-e * e) + 0.35 * m * exp(-e2 * e2); + } + const base = mix(0.85, 0.5, m), amp = mix(0.45, 1.4, m); + for (let g = 0; g < 180; g++) { + const e = (g - P.sweep) / 15; + SW[g] = exp(-e * e); + } + for (let k = 0; k <= ROWS; k++) tone(P, L[k], m, col, k * 6); + const dens = 1 - 0.15 * m, t = P.t, off = round(P.off); + const XF = L.map((s) => (s.p < 1 ? fingerAt(s) : 1e3)), PF = L.map((s) => (s.prev ? fingerAt(s.prev) : 0)); + const WX = L.map((s) => -6 + 78 * s.wipe); + for (let y = 0; y < H; y++) { + const ty = y + off, k = floor(ty / SL), j = ty - k * SL, s = L[k], o = k * 6, xf = XF[k]; + for (let x = 0; x < W; x++) { + const gi = (y * W + x) * 3; + let r = GR[gi], g = GR[gi + 1], b = GR[gi + 2]; + const i = j * W + x; + // The swatch being wiped off, still there right of the round edge of + // the pad. + if (s.prev) { + const S = s.prev, e = WX[k] - 2.5 * S.Q[i] * S.Q[i], a = S.A[i] * cl((PF[k] - S.R[i]) * 0.8 + 0.5, 0, 1) * sm(e - 2.5, e + 2.5, x); + if (a > 0.004) { + const v = S.V[i]; + (r = mix(r, col[o] * v, a)), (g = mix(g, col[o + 1] * v, a)), (b = mix(b, col[o + 2] * v, a)); + } + } + let a = s.A[i]; + if (a > 0.004) { + let wet = 0; + if (xf < 1e3) { + const dx = xf - s.R[i]; + a *= cl(dx * 0.8 + 0.5, 0, 1); + if (dx > 0 && dx < 4) wet = 0.5 * (1 - dx / 4); + } + if (a > 0.004) { + const Q = s.Q[i], v = s.V[i] * (1 - 0.42 * m * sm(-0.1, 0.6, Q)); + let pr = col[o] * v, pg = col[o + 1] * v, pb = col[o + 2] * v; + const lt = SW[cl(round(y + 0.55 * x), 0, 179)]; + const h = min(1, SH[cl(round(((Q + 1.6) * 63) / 3.2), 0, 63)] * s.G[i] * (base + amp * lt) + wet); + (pr = mix(pr, col[o + 3], h)), (pg = mix(pg, col[o + 4], h)), (pb = mix(pb, col[o + 5], h)); + const K = s.K[i]; + if (m > 0 && K > dens && a > 0.4) { + const tw = sin(K * 6283 + t * 2.6); + if (tw > 0.3) { + const z = (tw - 0.3) / 0.7, spk = z * z * m * (0.45 + 0.9 * lt); + (pr = mix(pr, 255, spk)), (pg = mix(pg, 248, spk)), (pb = mix(pb, 232, spk)); + } + } + (r = mix(r, pr, a)), (g = mix(g, pg, a)), (b = mix(b, pb, a)); + } + } + display.setPixel(x, y, min(255, r), min(255, g), min(255, b)); + } + } +} From 2dc146feb41cb7932e738e9f9326607a58ee8b70 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 11:36:24 +0000 Subject: [PATCH 09/15] =?UTF-8?q?Ballistic=20Deposition=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=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_01QuEe7qhvCVuDuGNB5fZ113 --- .../patterns/ballistic-deposition.js | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 incoming/pattern-study/patterns/ballistic-deposition.js diff --git a/incoming/pattern-study/patterns/ballistic-deposition.js b/incoming/pattern-study/patterns/ballistic-deposition.js new file mode 100644 index 000000000..1698e5d7c --- /dev/null +++ b/incoming/pattern-study/patterns/ballistic-deposition.js @@ -0,0 +1,183 @@ +// ===== Patternflow pattern ===== +// Title: Ballistic Deposition +// Family: particle +// Idea: Particles fall straight down on to 64 columns and stick at the first thing they touch, the top of their own column or the side of a taller neighbour, so a porous deposit grows up the panel in coloured bands whose boundaries are the old fronts, rough and getting rougher, while the camera climbs with the front. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: ballistic deposition (Marjorie J. Vold, 1959), whose surface is in the Kardar-Parisi-Zhang class (1986), pattern round 30 +// Round: 30 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Slip=0..1, Layers=6..24 +// +// Knob 1: Hue. Turns both band colours round the wheel together. At 0 the bands alternate cyan and violet, and over every ten bands the pair wanders to azure and pink and back; at 0.5 they are red and green. +// Knob 2: Speed. Particles a second: 4 at 0.2, so each one can be followed down, 64 at 2, one layer a second with the front climbing 2 rows a second, and 300 at 3, a hail that pushes the deposit off the bottom 10 rows a second. +// Knob 3: Slip. The chance that a particle slides past a taller neighbour's side and lands on its own column. At 0 every one sticks: the deposit fills 47% of its cells and the front's width settles near 4.5 rows after about 30 layers. At 1 it is random deposition: no holes, no sideways reach, needles of every length, and a width that grows as the square root of time without end, 10 rows after 100 layers and 30 after 1000. +// Knob 4: Layers. How many layers of 64 particles each colour band holds: 6 at the start, a band about 13 rows thick in the porous deposit, up to 24, about 50 rows. +// Buttons: K1 tap lays a flat white lid across the panel one row above the highest particle. The particles in the air land on it, and the front starts flat again and roughens above it, band by band. + +// On 64 columns that wrap round, the front's width grows from a flat start +// as t^(1/3), the KPZ exponent, and stops near 4.5 rows once its bumps are +// as wide as the panel; the bands keep each stage of it as it scrolls by. +const W = 64, H = 128, R = 512, RM = R - 1; +const { min, max, floor, round, random } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const MAXF = 1024, MAXL = 256; +const FALL = 70; // rows a second +const FRONT = 0.8; // the camera holds the mean front this far up the panel +const LID = 0x7fffffff; + +export function setup(params) { + params.s = { + fresh: true, + h: new Int32Array(W), // rows taken in each column, counting the particles still falling to it + vh: new Int32Array(W), // the same for the particles that have landed + cell: new Int32Array(W * R), // particle number, 0 for a hole + row: new Int32Array(R).fill(-1), // the world row each ring row holds + n: 0, base: 0, acc: 0, t: 0, + fx: new Int16Array(MAXF), fy: new Float32Array(MAXF), fto: new Int32Array(MAXF), fn: new Int32Array(MAXF), nf: 0, + lx: new Int16Array(MAXL), ly: new Int32Array(MAXL), lt: new Float32Array(MAXL).fill(-9), li: 0, + hue: 0, layers: 6, + pal: new Float32Array(256 * 3), palKey: new Int32Array(256).fill(-1), + }; +} + +// Vold's rule. A particle falling down column x stops on its own column's top +// or, if it sticks sideways, beside the top particle of a taller neighbour: +// h_x <- max(h_{x-1}, h_x + 1, h_{x+1}). The columns wrap round. +function deposit(P, x, stick) { + const h = P.h; + let y = h[x]; + if (stick) y = max(y, h[(x + 63) & 63] - 1, h[(x + 1) & 63] - 1); + h[x] = y + 1; + return y; +} + +// The deposit lives in a ring of 512 rows; a row is wiped when a particle +// first reaches it, and rows far below the camera are written over. +function place(P, x, y, n) { + const rr = y & RM; + if (P.row[rr] !== y) { + if (P.row[rr] > y) return; + P.row[rr] = y; + P.cell.fill(0, rr * W, rr * W + W); + } + P.cell[rr * W + x] = n; + if (y >= P.vh[x]) P.vh[x] = y + 1; +} + +const meanH = (h) => { + let s = 0; + for (let x = 0; x < W; x++) s += h[x]; + return s / W; +}; + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 0, 6], d = cl(dt || 0, 0, 0.1); + const speed = cl(k[1], 0.2, 3), slip = cl(k[2], 0, 1); + P.hue = k[0]; + P.layers = cl(k[3], 6, 24); + P.t += d; + // The opening deposit is laid on a flat floor before the first frame, + // until its mean front stands at the camera's mark. + if (P.fresh) { + P.fresh = false; + while (meanH(P.h) < FRONT * H) + for (let i = 0; i < W; i++) { + const x = floor(random() * W); + place(P, x, deposit(P, x, random() >= slip), ++P.n); + } + } + // K1: a flat lid one row above the highest particle. The front starts + // flat again and roughens on top of it. + if (input.btnPressed && input.btnPressed[0]) { + let y = 0; + for (let x = 0; x < W; x++) y = max(y, P.h[x]); + for (let x = 0; x < W; x++) place(P, x, y, LID), (P.h[x] = y + 1); + for (let i = 0; i < P.nf; i++) P.fto[i] = deposit(P, P.fx[i], random() >= slip); + } + // Each particle's landing is settled the moment it leaves the top, against + // the deposit as all earlier particles left it, so the order is Vold's + // one at a time; the fall to that place is only shown. + P.acc += 4 * Math.pow(75, (speed - 0.2) / 2.8) * d; + while (P.acc >= 1) { + P.acc -= 1; + if (P.nf >= MAXF) continue; + const i = P.nf++, x = floor(random() * W); + P.fx[i] = x; + P.fto[i] = deposit(P, x, random() >= slip); + P.fn[i] = ++P.n; + P.fy[i] = max(P.base + H + random() * FALL * d, P.fto[i] + 1); + } + for (let i = 0; i < P.nf; i++) { + P.fy[i] -= FALL * d; + if (P.fy[i] > P.fto[i]) continue; + place(P, P.fx[i], P.fto[i], P.fn[i]); + const j = P.li++ & (MAXL - 1); + (P.lx[j] = P.fx[i]), (P.ly[j] = P.fto[i]), (P.lt[j] = P.t); + P.nf--; + (P.fx[i] = P.fx[P.nf]), (P.fy[i] = P.fy[P.nf]), (P.fto[i] = P.fto[P.nf]), (P.fn[i] = P.fn[P.nf]); + i--; + } + const goal = meanH(P.vh) - FRONT * H; + if (goal > P.base) P.base += (goal - P.base) * min(1, d * 4); +} + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + const rgb = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + for (let c = 0; c < 3; c++) out[o + c] = rgb[c] * 255; +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const B = round(P.base), per = W * P.layers, pal = P.pal, key = P.palKey; + // A band is Layers times 64 particles, counted in landing order. Bands + // alternate between two colours 0.3 of the wheel apart, and the pair + // wanders 0.125 up the wheel and back over ten bands. + const colour = (n) => { + const b = floor((n - 1) / per), q = b & 255; + if (key[q] !== b) { + key[q] = b; + const odd = b & 1, u = b % 10, tri = u < 5 ? u : 10 - u; + hsv(P.hue + 0.48 + 0.025 * tri + (odd ? 0.3 : 0), odd ? 0.8 : 0.9, odd ? 0.85 : 1, pal, q * 3); + } + return q * 3; + }; + for (let sy = 0; sy < H; sy++) { + // Older deposit dims with depth, to 55% at the bottom edge. + const wy = B + H - 1 - sy, rr = wy & RM, ok = wy >= 0 && P.row[rr] === wy, fade = 1 - 0.45 * cl((sy - 30) / 98, 0, 1); + for (let x = 0; x < W; x++) { + const n = ok ? P.cell[rr * W + x] : 0; + if (!n) display.setPixel(x, sy, 0, 0, 0); + else if (n === LID) display.setPixel(x, sy, 200, 200, 200); + else { + const q = colour(n); + // The top particle of each column, the front itself, burns pale. + if (wy === P.vh[x] - 1) display.setPixel(x, sy, pal[q] * 0.35 + 166, pal[q + 1] * 0.35 + 166, pal[q + 2] * 0.35 + 166); + else display.setPixel(x, sy, pal[q] * fade, pal[q + 1] * fade, pal[q + 2] * fade); + } + } + } + // Fresh landings flash white and settle into their band's colour. + for (let j = 0; j < MAXL; j++) { + const age = P.t - P.lt[j]; + if (age > 0.4) continue; + const y = P.ly[j], sy = B + H - 1 - y, rr = y & RM, n = P.row[rr] === y ? P.cell[rr * W + P.lx[j]] : 0; + if (sy < 0 || sy >= H || !n || n === LID) continue; + const q = colour(n), a = age / 0.4, w = 255 * (1 - a); + display.setPixel(P.lx[j], sy, w + pal[q] * a, w + pal[q + 1] * a, w + pal[q + 2] * a); + } + // Particles in flight: a white head and a short grey tail. + for (let i = 0; i < P.nf; i++) { + const sy = B + H - 1 - floor(P.fy[i]); + for (let t = 0; t < 3; t++) { + const y = sy - t, v = t ? 150 - 45 * t : 255; + if (y >= 0 && y < H) display.setPixel(P.fx[i], y, v, v, v); + } + } +} From 8e762ec67b61775f9eb81f0790241649927984df Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 11:40:41 +0000 Subject: [PATCH 10/15] =?UTF-8?q?Satin=20Fold=20=ED=8C=A8=ED=84=B4?= =?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_01QuEe7qhvCVuDuGNB5fZ113 --- incoming/pattern-study/patterns/satin-fold.js | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 incoming/pattern-study/patterns/satin-fold.js diff --git a/incoming/pattern-study/patterns/satin-fold.js b/incoming/pattern-study/patterns/satin-fold.js new file mode 100644 index 000000000..ee0604ca8 --- /dev/null +++ b/incoming/pattern-study/patterns/satin-fold.js @@ -0,0 +1,254 @@ +// ===== Patternflow pattern ===== +// Title: Satin Fold +// Family: cosmetic +// Idea: A pale sheet of satin hangs in folds from a point above the panel and sways under a light from the left; its fibres run across the folds, so the sheen lies in long streaks along them, and one thin gold line crosses the cloth and bends over every fold it meets. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 30, after satin weave sheen and gold-foil packaging; the sheen is a fibre highlight (James Kajiya and Timothy Kay, 1989) with an anisotropic lobe (Gregory Ward, 1992) +// Round: 30 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Folds=1..5, Gold=0..1 +// +// Knob 1: Hue. The dye of the satin, once round the wheel, while the gold line stays gold. At 0 blush rose, at 0.1 nude champagne, at 0.25 pistachio, at 0.45 mint, at 0.6 powder blue and at 0.8 lilac. +// Knob 2: Speed. How fast the folds sway. At 2 they cross the middle of the panel at about 6 LEDs a second and the glints run 26 LEDs a second along the gold line; at 0.2 the folds go a tenth as fast and the glints half. +// Knob 3: Folds. One fold family more at each whole step, fading in over the step before. At 1 only the fan hanging from a point 160 LEDs above the panel's middle, 22 folds to the radian; 2 adds a diagonal drape 46 LEDs across, 3 and 4 two narrower ones at other angles, and 5 a pressed fold with sharp creases. The cloth also gets deeper, from 0.75 of the folds' base height at 1 to 1.47 at 5. +// Knob 4: Gold. The gold line. At 0 there is none, at 0.06 a thread about 2 LEDs wide with a glint every 54 LEDs, at 1 a ribbon 13 LEDs wide with a glint every 29. It shades the cloth on its right, 1 LED wide at the thread and 3.5 at the ribbon. +// Buttons: K1 tap sends a gust under the cloth: a bowed swell 5.5 LEDs high runs up the sheet from below the bottom edge to past the top in 2.5 seconds. + +const W = 64, H = 128; +const { sin, cos, sqrt, exp, floor, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => { + const t = cl((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; + +// The first fold family is a fan: the cloth is held at a point 160 LEDs +// above the panel's middle and a little left, and 22 folds a radian hang from +// it, tight at the top of the panel and wide at the bottom. +const PX = -14, PY = -160, FAN = 22, FAN_A = 6.5, RC = 160; +// The others are straight waves: crest angle in degrees, wavelength in LEDs, +// height in LEDs, bend of the crest, travel in radians a second at Speed 1, +// swell phase. The last is pressed: its profile has sharp valleys. The first +// row stands for the fan, which reads only its travel and phase from it. +const FOLDS = [ + [0, 1, 0, 0, 0.42, 0.0], + [58, 46, 4.2, -0.004, -0.33, 2.1], + [100, 30, 2.6, 0.007, 0.55, 4.0], + [128, 24, 1.8, -0.008, -0.71, 1.2], + [76, 28, 1.4, 0.004, 0.62, 3.3], +].map(([ang, lam, A, c, w, ph]) => { + const a = (ang * PI) / 180; + return { tx: cos(a), ty: sin(a), nx: -sin(a), ny: cos(a), k: (2 * PI) / lam, A, c, w, ph }; +}); + +// Light from the left, a little above and in front; the eye straight on. +// y runs down. +const norm3 = (x, y, z) => { + const l = sqrt(x * x + y * y + z * z); + return [x / l, y / l, z / l]; +}; +const [LX, LY, LZ] = norm3(-0.6, -0.35, 0.72); +const [HX, HY, HZ] = norm3(LX, LY, LZ + 1); +// The floats of the satin weave run nearly across the panel, rising a little +// to the right, so they cross the hanging folds and catch the light only +// where they tilt square to the half vector. +const FX = 0.985, FY = -0.17; +// The gold line in the cloth's own coordinates: through the middle, going +// down to the right, bowed a little. +const [GX, GY] = (() => { + const l = Math.hypot(0.42, 1); + return [0.42 / l, 1 / l]; +})(); +const MX = -GY, MY = GX; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + const rgb = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + for (let c = 0; c < 3; c++) out[o + c] = rgb[c] * 255; +} + +// The satin's ramp from the deepest hollow (0) to the brightest sheen (1): +// the hue deep and saturated in the hollows, pale over the body, near white +// in the streaks. +const RAMP = 256; +function ramp(P) { + const hue = P.hue + 0.975; + for (let i = 0; i < RAMP; i++) { + const s = i / (RAMP - 1); + let sat, val; + if (s < 0.5) { + const u = s / 0.5; + sat = 0.46 + (0.18 - 0.46) * u; + val = 0.5 + (0.8 - 0.5) * u; + } else { + const u = (s - 0.5) / 0.5; + sat = 0.18 + (0.04 - 0.18) * u; + val = 0.8 + (0.97 - 0.8) * u; + } + hsv(hue - 0.015 * (1 - s), sat, val, P.lut, i * 3); + } + P.lutHue = P.hue; +} + +export function setup(params) { + params.s = { + t: 0, clock: 0, hue: 0, speed: 2, folds: 1, gold: 0.06, gust: -1, + lut: new Float32Array(RAMP * 3), lutHue: NaN, cw: new Float32Array(W), cd: new Float32Array(W), + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const P = params.s, k = input.knobValues || [0, 2, 1, 0.06], d = cl(dt || 0, 0, 0.1); + P.hue = k[0]; + P.speed = cl(k[1], 0.2, 3); + P.folds = cl(k[2], 1, 5); + P.gold = cl(k[3], 0, 1); + P.t += d * P.speed; + P.clock += d; + if (P.gust >= 0) { + P.gust += d / 2.5; + if (P.gust >= 1) P.gust = -1; + } else if (input.btnPressed && input.btnPressed[0]) P.gust = 0; +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + if (P.lutHue !== P.hue) ramp(P); + const T = P.t, F = P.folds, lut = P.lut; + // Each family fades in over one step of Folds, and the whole cloth gets + // deeper as it does. + const depth = 0.75 + 0.18 * (F - 1); + const act = []; + for (let i = 0; i < FOLDS.length; i++) { + const f = FOLDS[i], wgt = sm(0, 1, F - i); + if (wgt <= 0) continue; + const swell = 0.72 + 0.28 * sin(0.37 * T + f.ph); + act.push({ ...f, A: (i ? f.A * swell : FAN_A) * wgt * depth, phase: f.w * T + f.ph, press: i === 4 }); + } + const na = act.length, fan = act[0]; + let tot = 0; + for (const f of act) tot += f.A; + // The gust: one ridge across the sheet that runs from the bottom up. + let gA = 0, gy0 = 0; + if (P.gust >= 0) { + gA = 5.5 * sin(PI * P.gust); + gy0 = 80 - 190 * P.gust; + } + const wa = 3.2; + // The panel sees the cloth a little from below, so a line on it shifts + // with the height of the cloth under it: 0.65 LED a LED of height at one + // fold family, less on the deep drapes. + const lean = 0.65 / (1 + 0.15 * (F - 1)); + const g = P.gold, gAlpha = sm(0, 0.04, g), hw = 0.6 + 6 * g, sw = 1 + 2.5 * g; + const gf = 0.11 + 0.11 * g, gT = P.clock * (0.6 + 0.4 * P.speed); + // The warp's sideways part, one value a column. + const CW = P.cw, CD = P.cd; + for (let x = 0; x < W; x++) { + const u = (x - 31.5) * 0.07 - T * 0.17 + 1.3; + CW[x] = wa * sin(u); + CD[x] = wa * 0.07 * cos(u); + } + for (let y = 0; y < H; y++) { + const py = y - 63.5; + const s1 = sin(py * 0.045 + T * 0.23), c1 = cos(py * 0.045 + T * 0.23); + const dxdy = wa * 0.045 * c1; + for (let x = 0; x < W; x++) { + const px = x - 31.5; + const dydx = CD[x]; + const qx = px + wa * s1, qy = py + CW[x]; + let h = 0, gx = 0, gy = 0; + { + // The fan: the fold phase goes with the angle round the point it + // hangs from, and leans with the distance. Its height grows as the + // square root of the distance, and a swell runs down it. + const dx = qx - PX, dy = qy - PY, r2 = dx * dx + dy * dy, r = sqrt(r2); + const th = FAN * Math.atan2(dx, dy) + 0.025 * r - fan.phase; + const sv = 0.045 * r - 0.5 * T, ms = 0.75 + 0.25 * sin(sv), rr = sqrt(r / RC); + const A = fan.A * rr * ms, dA = A / (2 * r) + fan.A * rr * 0.01125 * cos(sv); + const sn = sin(th), cs = cos(th); + h += A * sn; + const gr = dA * sn + A * cs * 0.025, ga = (A * cs * FAN) / r2; + gx += (gr * dx) / r + ga * dy; + gy += (gr * dy) / r - ga * dx; + } + for (let j = 1; j < na; j++) { + const f = act[j]; + const a = qx * f.tx + qy * f.ty, s = qx * f.nx + qy * f.ny + f.c * a * a; + const th = f.k * s - f.phase; + let v, dv; + if (f.press) { + // sqrt(sin^2 + e^2): round crests, sharp valleys. + const sn = sin(th), cs = cos(th), r = sqrt(sn * sn + 0.04); + v = f.A * (r - 0.6); + dv = (f.A * f.k * sn * cs) / r; + } else { + v = f.A * sin(th); + dv = f.A * f.k * cos(th); + } + h += v; + const ex = f.nx + 2 * f.c * a * f.tx, ey = f.ny + 2 * f.c * a * f.ty; + gx += dv * ex; + gy += dv * ey; + } + if (gA > 0) { + const e = (qy - gy0 - 0.006 * qx * qx) / 12, b = gA * exp(-e * e), d = (-2 * e * b) / 12; + h += b; + gx += d * -0.012 * qx; + gy += d; + } + // Back from the cloth's coordinates to the panel's. + const hx = gx + gy * dydx, hy = gx * dxdy + gy; + // Normal and the fibre's tangent on the surface. + const nl = 1 / sqrt(hx * hx + hy * hy + 1), Nx = -hx * nl, Ny = -hy * nl, Nz = nl; + let Tx = FX, Ty = FY, Tz = FX * hx + FY * hy; + const tl = 1 / sqrt(Tx * Tx + Ty * Ty + Tz * Tz); + (Tx *= tl), (Ty *= tl), (Tz *= tl); + const NL = Nx * LX + Ny * LY + Nz * LZ; + const dif = cl((NL + 0.45) / 1.45, 0, 1); + // Ward-like lobe: narrow along the fibre, broad across it. + const TH = Tx * HX + Ty * HY + Tz * HZ; + const Bx = Ny * Tz - Nz * Ty, By = Nz * Tx - Nx * Tz, Bz = Nx * Ty - Ny * Tx; + const BH = Bx * HX + By * HY + Bz * HZ; + const spec = 0.45 * exp(-(TH * TH) / 0.012 - (BH * BH) / 0.35) + 0.5 * exp(-(TH * TH) / 0.06 - (BH * BH) / 0.6); + // The hollows get less of the light, and less of the sheen. + const ao = sm(-0.9 * tot, 0.6 * tot, h); + const shade = cl(0.05 + 0.7 * dif * (0.4 + 0.6 * ao) + 0.5 * spec * (0.25 + 0.75 * ao), 0, 1); + const o = ((shade * (RAMP - 1)) | 0) * 3; + let r = lut[o], gg = lut[o + 1], b = lut[o + 2]; + if (gAlpha > 0) { + const al = qx * GX + qy * GY; + const vv = qx * MX + qy * MY + 0.0018 * al * al - 4 + lean * h; + const ggx = MX + 0.0036 * al * GX, ggy = MY + 0.0036 * al * GY; + const vx = ggx + ggy * dydx + lean * hx, vy = ggx * dxdy + ggy + lean * hy; + // Where a fold turns the line back on itself it passes behind the + // fold, so it fades out there instead of spreading into a blot. + const gl = sqrt(vx * vx + vy * vy), sd = vv / Math.max(0.3, gl), dist = Math.abs(sd), seen = gAlpha * sm(0.3, 0.7, gl); + // The line stands a little off the cloth and shades it on the side + // away from the light. + if (sd < 0) { + const sh = 1 - 0.3 * seen * sm(hw + 0.5 + sw, hw - 0.5, dist); + (r *= sh), (gg *= sh), (b *= sh); + } + const cov = cl(hw + 0.5 - dist, 0, 1) * seen; + if (cov > 0) { + const NH = Nx * HX + Ny * HY + Nz * HZ; + const gs = NH > 0.95 ? ((NH - 0.95) / 0.05) ** 4 : 0; + const tw = 0.5 + 0.5 * sin(al * gf - gT * 2.2); + const glint = tw ** 24 + gs; + const lit = 0.62 + 0.38 * dif, gw = cl(glint, 0, 1); + const gr = 252 * lit + (255 - 252 * lit) * gw, ggr = 184 * lit + (246 - 184 * lit) * gw, gb = 58 * lit + (205 - 58 * lit) * gw; + r += (gr - r) * cov; + gg += (ggr - gg) * cov; + b += (gb - b) * cov; + } + } + display.setPixel(x, y, r, gg, b); + } + } +} From 6d14a52ab6ddae6d9ac23dd62189bb29a664ce0a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 11:43:27 +0000 Subject: [PATCH 11/15] =?UTF-8?q?Pearl=20Sheen=20=ED=8C=A8=ED=84=B4?= =?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_01QuEe7qhvCVuDuGNB5fZ113 --- .../pattern-study/patterns/pearl-sheen.js | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 incoming/pattern-study/patterns/pearl-sheen.js diff --git a/incoming/pattern-study/patterns/pearl-sheen.js b/incoming/pattern-study/patterns/pearl-sheen.js new file mode 100644 index 000000000..1bfb09df6 --- /dev/null +++ b/incoming/pattern-study/patterns/pearl-sheen.js @@ -0,0 +1,213 @@ +// ===== Patternflow pattern ===== +// Title: Pearl Sheen +// Family: cosmetic +// Idea: Pearl pigment pours slowly down the panel in long satin streaks while a strip light's reflection slides across it, and each LED takes its colour from thin-film interference in the coat of the mica flakes, so flakes lying flat show one pearl colour and flakes tilted along the streaks flip to a second. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 30, after thin-film interference in pearlescent mica pigments (titania-coated mica) +// Round: 30 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Shift=0..1, Sheen=0..1 +// +// Knob 1: Hue. Turns the whole pigment round the colour wheel, flip colour and all. At 0 the flat flakes are pearl rose and the tilted ones peach; at 0.5 aqua turning periwinkle, at 0.75 lavender turning magenta. +// Knob 2: Speed. How fast the pour runs. At 2 the broad swells slide down about 10 LEDs a second and the fine striations inside them 48, so the top layer slips past the body of the pour; at 0.2 a tenth of that. +// Knob 3: Shift. The duochrome's strength: how much shorter a tilted flake's light path is than a flat one's. At 0 it is 3%, 950 nm against 980, a faint peach on rose; at 1 it is 14.5%, 840 nm, and the flanks of the streaks turn gold. +// Knob 4: Sheen. The width of the strip light's reflection. At 0 a soft satin glow about 45 LEDs across over a pale body; at 1 a wet gloss, a band about 10 LEDs across that the swells break into white-hot lines, over deeper colour. +// Buttons: K1 tap drags a spatula down through the pour: in 2.5 s a kink runs from the top to the bottom pushing the streaks up to 10 LEDs aside, and over the next 6 s they ease back. + +const W = 64, H = 128, N = W * H; +const { floor, sqrt, sin, cos, exp, tanh, atan2, round, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// Smooth noise in a 256 x 256 tile, three octaves of 8, 16 and 32 cells, +// built once in setup. The streaks are this tile stretched about twelve times +// along the flow. +const T = 256, TM = T - 1; +const TEX = new Float32Array(T * T); +const fade = (t) => t * t * (3 - 2 * t); +function buildTex() { + TEX.fill(0); + let amp = 1, tot = 0; + for (let o = 0; o < 3; o++) { + const cells = 8 << o, size = T / cells, g = new Float32Array(cells * cells); + for (let i = 0; i < g.length; i++) g[i] = Math.random() * 2 - 1; + for (let y = 0; y < T; y++) { + const gy = floor(y / size), fy = fade(y / size - gy), y1 = (gy + 1) % cells; + for (let x = 0; x < T; x++) { + const gx = floor(x / size), fx = fade(x / size - gx), x1 = (gx + 1) % cells; + const a = g[gy * cells + gx], b = g[gy * cells + x1], c = g[y1 * cells + gx], d = g[y1 * cells + x1]; + const top = a + (b - a) * fx; + TEX[y * T + x] += (top + (c + (d - c) * fx - top) * fy) * amp; + } + } + tot += amp; + amp *= 0.45; + } + for (let i = 0; i < TEX.length; i++) TEX[i] /= tot; +} +function tap(u, v) { + const fu = floor(u), fv = floor(v), x0 = fu & TM, y0 = fv & TM, x1 = (x0 + 1) & TM, y1 = (y0 + 1) & TM; + const tx = u - fu, ty = v - fv; + const a = TEX[y0 * T + x0], b = TEX[y0 * T + x1], c = TEX[y1 * T + x0], d = TEX[y1 * T + x1]; + return a + (b - a) * tx + (c - a) * ty + (a - b - c + d) * tx * ty; +} + +// The interference colour of the flakes' coat against the optical path +// difference 2 n d cos(theta), 0 to 1600 nm in 1024 steps. Each entry sums +// the two-beam reflectance over the spectrum, 400 to 700 nm every 5 nm, under +// three bell-shaped channel curves at 600, 545 and 455 nm. The air-to-titania +// face turns its beam over and the titania-to-mica face does not, so the +// reflectance is (1 - cos(2 pi path / lambda)) / 2. Only the chroma is kept, +// the colour less its grey, scaled so the second order's strongest is about 1. +const LN = 1024, OPD_MAX = 1600; +const RAW = new Float32Array(LN * 3); +function buildRaw() { + const chan = [[600, 45], [545, 40], [455, 32]], norm = [0, 0, 0], wts = []; + for (let l = 400; l <= 700; l += 5) { + const w = chan.map(([m, s]) => exp(-(((l - m) / s) ** 2) / 2)); + wts.push([l, w]); + for (let c = 0; c < 3; c++) norm[c] += w[c]; + } + for (let i = 0; i < LN; i++) { + const opd = (i / (LN - 1)) * OPD_MAX, acc = [0, 0, 0]; + for (const [l, w] of wts) { + const r = 0.5 - 0.5 * cos((2 * PI * opd) / l); + for (let c = 0; c < 3; c++) acc[c] += r * w[c]; + } + for (let c = 0; c < 3; c++) acc[c] /= norm[c]; + const m = (acc[0] + acc[1] + acc[2]) / 3; + for (let c = 0; c < 3; c++) RAW[i * 3 + c] = (acc[c] - m) / 0.45; + } +} + +// The angle of a chroma round the grey axis, red at 0. +const chromaAngle = (r, g, b) => atan2((sqrt(3) / 2) * (g - b), r - (g + b) / 2); + +// Hue turns the table round the grey axis. Warm entries get LIFT: orange and +// yellow at half light read as brown and olive on the panel, so they are +// lifted and their deep tones kept lighter. +const LUT = new Float32Array(LN * 3), LIFT = new Float32Array(LN); +let lutRot = NaN; +function buildLut(a) { + const c = cos(a), s = sin(a), k = (1 - c) / 3, q = s / sqrt(3), m0 = c + k, m1 = k - q, m2 = k + q; + for (let i = 0, j = 0; j < LN; i += 3, j++) { + const r = RAW[i], g = RAW[i + 1], b = RAW[i + 2]; + LUT[i] = r * m0 + g * m1 + b * m2; + LUT[i + 1] = r * m2 + g * m0 + b * m1; + LUT[i + 2] = r * m1 + g * m2 + b * m0; + const y = cos(chromaAngle(LUT[i], LUT[i + 1], LUT[i + 2]) - 1); + LIFT[j] = y > 0 ? y * y : 0; + } +} + +// A flat flake's light path: the second order's magenta, 980 nm. A tilted +// flake sees the light slant through its coat, 2 n d cos(theta) is shorter, +// and the colour moves on through orange to gold. Hue then turns the table so +// the flat colour sits on the knob's hue and the gold on its neighbour. +const BASE = 980; +let baseAng = 0; + +export function setup(params) { + buildTex(); + buildRaw(); + const bi = round((BASE / OPD_MAX) * (LN - 1)) * 3; + baseAng = chromaAngle(RAW[bi], RAW[bi + 1], RAW[bi + 2]); + lutRot = NaN; + params.s = { + t: 0, flow: 0, hue: 0, shift: 0, sheen: 0.06, stir: -1, + u: new Float32Array(N), h: new Float32Array(N), big: new Float32Array(N), fine: new Float32Array(N), + }; +} + +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.shift = cl(k[2], 0, 1); + P.sheen = cl(k[3], 0, 1); + P.t += d; + P.flow += d * cl(k[1], 0.2, 3); + if (P.stir >= 0) { + P.stir += d; + if (P.stir > 12) P.stir = -1; + } + if (input.btnPressed && input.btnPressed[0]) P.stir = 0; +} + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const rot = 2 * PI * (P.hue + 0.97) - baseAng; + if (rot !== lutRot) buildLut((lutRot = rot)); + const ub = P.u, hb = P.h, bb = P.big, fb = P.fine, t = P.t, f = P.flow; + + // Across-the-flow coordinate u: the panel's x bent by two slow warps, one + // carried down by the pour and one drifting on its own. A streak is a line + // of constant u. The spatula pushes u aside above its edge. + const sa0 = P.stir, sy = sa0 >= 0 ? -0.3 + (4.6 * sa0) / 2.5 : 0; + const sAmp = sa0 < 0 ? 0 : 0.32 * (sa0 < 2.5 ? 1 : exp(-(sa0 - 2.5) / 1.6)); + for (let y = 0; y < H; y++) { + const Y = y / 32, push = sAmp > 0 ? sAmp * 0.5 * (1 + tanh((sy - Y) / 0.22)) : 0; + for (let x = 0; x < W; x++) { + const X = x / 32; + ub[y * W + x] = X + tap(X * 6 + 40, Y * 5 - f * 0.5 + 7) * 1.4 + tap(X * 3 + t * 0.3, Y * 3 + 90) * 0.9 - push * sin((PI * (x + 0.5)) / W); + } + } + // The surface: broad swells (big) and fine striations (fine), both long + // along u. Where the warp squeezes the striations finer than two LEDs can + // hold, they fade out instead of breaking into stair steps. + for (let y = 0; y < H; y++) { + const Y = y / 32, yu = (y > 0 ? y - 1 : y) * W, yd = (y < H - 1 ? y + 1 : y) * W; + for (let x = 0; x < W; x++) { + const i = y * W + x, u = ub[i]; + const dx = ub[y * W + (x < W - 1 ? x + 1 : x)] - ub[y * W + (x > 0 ? x - 1 : x)], dy = ub[yd + x] - ub[yu + x]; + const over = 27 * sqrt(dx * dx + dy * dy) - 2; + const big = tap(u * 22 + 130, Y * 4 - f * 0.6); + const fine = tap(u * 55, Y * 4 - f * 3) / (1 + (over > 0 ? over * over : 0)); + bb[i] = big; + fb[i] = fine; + hb[i] = big + fine * 0.12; + } + } + + const sh = P.shift, sp = 0.2 + 0.8 * P.sheen; + // How much shorter a fully tilted flake's path is, and how much the coat's + // thickness varies along the striations. + const dmax = 0.03 + 0.115 * sh, thin = 10 + 40 * sh; + // A long strip light seen in the surface. Its reflection sweeps across and + // back every 21 s with a second, dimmer strip trailing it; Sheen narrows both. + const sw = 0.8 - 0.65 * sp, iw = 1 / sw, amp = 0.46 + 0.3 * sp; + const sc = 1.2 * sin(t * 0.3), sc2 = sc + (sc > 0 ? -1.6 : 1.6), sa = 0.25 * sin(t * 0.13); + for (let y = 0; y < H; y++) { + const yu = (y > 0 ? y - 1 : y) * W, yd = (y < H - 1 ? y + 1 : y) * W; + for (let x = 0; x < W; x++) { + const i = y * W + x, xl = y * W + (x > 0 ? x - 1 : x), xr = y * W + (x < W - 1 ? x + 1 : x); + const hx = (hb[xr] - hb[xl]) * 8, hy = (hb[yd + x] - hb[yu + x]) * 8; + const n2 = 1 / (1 + hx * hx + hy * hy); + // The straight-on view reflected, r = 2 (n.v) n - v, on a swatch that + // bulges a little, so the strip lands as one broad band. + const bulge = 0.8 * ((x - 32) / 32 + (y - 64) / 160); + const rq = -2 * n2 * (hx + sa * hy) + bulge, q = (rq - sc) * iw, q2 = (rq - sc2) * iw; + const glow = exp(-q * q) + 0.6 * exp(-q2 * q2); + // The flakes lie along the flow and tilt with the fine striations. A + // tilted flake sees the light at an angle, which shortens its path: + // flip runs from 0 for flakes within 18 degrees of flat to 1 past 45. + const fx = ((fb[xr] - fb[xl]) * 4 + hx * 0.4) * 2.2, fy = ((fb[yd + x] - fb[yu + x]) * 4 + hy * 0.4) * 2.2; + const c2 = 1 / (1 + fx * fx + fy * fy); + const a = cl((0.9 - c2) * 2.5, 0, 1), flip = a * a * (3 - 2 * a); + const j = cl(round((((BASE + thin * fb[i]) * (1 - dmax * flip)) / OPD_MAX) * (LN - 1)), 0, LN - 1), li = j * 3; + // The flakes catch the strip in finer lines of their own. + const fq = (bulge - 2 * c2 * (fx + sa * fy) - sc) * 2.2; + const gl = 0.75 * glow + 0.25 * exp(-fq * fq); + // Deep tones only in the troughs of the broad swells. + const B = bb[i], deep = B < -0.1 ? (B > -0.35 ? (-0.1 - B) / 0.25 : 1) : 0; + let lum = 0.64 - 0.12 * sp + amp * gl - 0.26 * deep + (0.1 * flip * dmax) / 0.13; + if (lum > 0.95) lum = 0.95 + (lum - 0.95) * 0.4; + const sat = 0.45 + 0.12 * sp - 0.32 * gl + 0.45 * deep + 0.3 * sh * flip; + const yl = LIFT[j], L = (lum + 0.12 * yl + 0.2 * yl * deep) * 235, S = sat * (1 - 0.3 * yl); + display.setPixel(x, y, cl(L * (1 + S * LUT[li]), 0, 255), cl(L * (1 + S * LUT[li + 1]), 0, 255), cl(L * (1 + S * LUT[li + 2]), 0, 255)); + } + } +} From b0e32dc34670125bf55830df95a13c387ba65508 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 12:07:00 +0000 Subject: [PATCH 12/15] =?UTF-8?q?Bi-Phase=20=ED=8C=A8=ED=84=B4=EC=9D=84=20?= =?UTF-8?q?=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_01QuEe7qhvCVuDuGNB5fZ113 --- incoming/pattern-study/patterns/bi-phase.js | 382 ++++++++++++++++++++ 1 file changed, 382 insertions(+) create mode 100644 incoming/pattern-study/patterns/bi-phase.js diff --git a/incoming/pattern-study/patterns/bi-phase.js b/incoming/pattern-study/patterns/bi-phase.js new file mode 100644 index 000000000..d67a27497 --- /dev/null +++ b/incoming/pattern-study/patterns/bi-phase.js @@ -0,0 +1,382 @@ +// ===== Patternflow pattern ===== +// Title: Bi-Phase +// Family: cosmetic +// Idea: The panel is the inside of a two-phase cleanser just shaken: tinted oil drops in pale water merge into bigger round drops, the big ones rise fastest, and the oil gathers on top in one layer under a bright meniscus line until the bottle is shaken again. +// Author: patternflow-lab +// Date: 2026-10-24 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: original, pattern round 30, after two-phase cleansers and phase-field separation of two liquids (Cahn-Hilliard, John W. Cahn and John E. Hilliard, 1958; Allen-Cahn, Samuel M. Allen and John W. Cahn, 1979) +// Round: 30 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Oil=0.3..0.6, Shake=0..1 +// +// Knob 1: Hue. Turns the oil round the colour wheel; the water stays a pale cool white. At 0 the oil is peach, at 0.25 green, at 0.5 sky blue, at 0.75 violet and at 0.85 rose. +// Knob 2: Speed. How fast the liquid runs. At 2 a drop of radius 3 rises 14 LEDs a second and one of radius 8 35, and the oil has settled into its layer 6 to 7 s after a shake; at 0.2 everything, the shake too, takes ten times as long. +// Knob 3: Oil. The share of the bottle that is oil, 30% to 60%. At 0.3 the meniscus settles 38 LEDs from the top and a shake makes oil drops in water; past 0.5 it makes water drops in oil, and at 0.6 the line settles 77 LEDs down. +// Knob 4: Shake. How hard and how often the bottle is shaken. At 0 every 23 s at Speed 2, into drops of radius 2.2 to 5 LEDs, and the layers stand calm for most of that; at 1 every 5 s, into drops of radius 1.5 to 2.4 that rise slowly, so the oil never quite settles. +// Buttons: K1 tap shakes the bottle now. + +const W = 64, H = 128, N = W * H; +// The flow is solved on a coarse grid of 4x4 LED cells. +const CW = 16, CH = 32, CN = CW * CH; +const { min, max, floor, sqrt, sin, cos, exp, log, tanh, abs, PI, random } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const sm = (a, b, v) => { + const t = cl((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; + +// Allen-Cahn: phi_t = M (K lap phi + phi - phi^3) + lambda, phi = +1 oil and +// -1 water, lambda the multiplier that holds the amount of oil to K3. The +// interface is tanh(d / sqrt(2K)) across, about 2 LEDs, and moves at M K +// times its curvature. h M (8K + 2) < 2 keeps the step stable: 4 a frame at +// Speed 2. +const KAP = 0.5, MOB = 1.5, HMAX = 0.21; +// Gravity in the free energy, on the interfaces only: a rim higher up gains +// oil from one lower down in the same patch. +const GRAV = 2.5; +// Time units a second at Speed 1; a shake lasts TS of them, 1.1 s at Speed 2. +const RATE = 12, TS = 26; +// The push of the oil, and the screening of the flat bottle's walls. +const BUOY = 0.5, AL2 = 0.16; +// Drops under AMAX LEDs move as one piece; ROUND and DRIFT below. +const AMAX = 700, ROUND = 0.06, DRIFT = 0.08; + +// Sine transforms of the coarse grid (DST-II forward, DST-III back), and the +// screened Stokes operator (lap - a^2)^2 psi = d(f_y)/dx of a flat cell, +// diagonal in that basis, with free-slip walls. +const SX = new Float32Array(CW * CW), SY = new Float32Array(CH * CH), KI = new Float32Array(CN); +for (let m = 0; m < CW; m++) for (let i = 0; i < CW; i++) SX[m * CW + i] = sin((PI * (m + 1) * (i + 0.5)) / CW); +for (let n = 0; n < CH; n++) for (let j = 0; j < CH; j++) SY[n * CH + j] = sin((PI * (n + 1) * (j + 0.5)) / CH); +for (let n = 0; n < CH; n++) + for (let m = 0; m < CW; m++) { + const kx = 4 * sin((PI * (m + 1)) / (2 * CW)) ** 2, ky = 4 * sin((PI * (n + 1)) / (2 * CH)) ** 2; + KI[n * CW + m] = ((m === CW - 1 ? 1 : 2) * (n === CH - 1 ? 1 : 2)) / (CN * (kx + ky + AL2) ** 2); + } + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h); + const p = v * (1 - s), q = v * (1 - s * f), t = v * (1 - s * (1 - f)); + const c = [[v, t, p], [q, v, p], [p, v, t], [p, q, v], [t, p, v], [v, p, q]][i]; + (o[0] = c[0]), (o[1] = c[1]), (o[2] = c[2]); +} + +export function setup(params) { + params.s = null; +} + +function init(k) { + const f = () => new Float32Array(N), g = () => new Float32Array(CN); + const P = { + phi: f(), nxt: f(), S: f(), tmp: f(), blur: f(), u: f(), v: f(), b1: f(), b2: f(), + lab: new Int32Array(N), stk: new Int32Array(N), cu: f(), cv: f(), ca: f(), px: f(), py: f(), rad: f(), + F: g(), A: g(), G: g(), psi: g(), U: g(), V: g(), + hue: k[0], speed: 2, oil: cl(k[2], 0.3, 0.6), shake: cl(k[3], 0, 1), + mT: 0, c: 0.85 * TS, time: 0, rt: 0, deep: [0, 0, 0], pale: [0, 0, 0], + }; + // Open at the end of a shake, the new drops just formed. + P.mT = 2 * P.oil - 1; + shake(P); + P.phi.set(P.S); + for (let s = 0; s < 8; s++) relax(P, HMAX); + return P; +} + +// A shake's emulsion: round drops of the lesser liquid dropped at random until +// they make up its share, radius 2.2 to 5 LEDs after a gentle shake and 1.5 +// to 2.4 after a hard one, small ones the most common. Most land a LED clear +// of the others; one in twenty may touch. +function shake(P) { + const S = P.S, inv = P.mT > 0, f = inv ? (1 - P.mT) / 2 : (1 + P.mT) / 2; + const r0 = 2.2 - 0.7 * P.shake, r1 = 5 - 2.6 * P.shake; + S.fill(-1); + let area = 0; + for (let k = 0; k < 5000 && area < f * N; k++) { + const r = r0 * (r1 / r0) ** (random() ** 1.6), cx = random() * W, cy = random() * H; + const xa = max(0, floor(cx - r - 2)), xb = min(W - 1, floor(cx + r + 2)), ya = max(0, floor(cy - r - 2)), yb = min(H - 1, floor(cy + r + 2)); + if (k < 4500 && random() < 0.95) { + let hit = false; + const rr = (r + 1.2) * (r + 1.2); + for (let y = ya; y <= yb && !hit; y++) + for (let x = xa; x <= xb; x++) { + const ex = x + 0.5 - cx, ey = y + 0.5 - cy; + if (ex * ex + ey * ey < rr && S[y * W + x] > -0.9) hit = true; + } + if (hit) continue; + } + for (let y = ya; y <= yb; y++) + for (let x = xa; x <= xb; x++) { + const ex = x + 0.5 - cx, ey = y + 0.5 - cy, i = y * W + x, v = tanh((r - sqrt(ex * ex + ey * ey)) / 0.8); + if (v > S[i]) (area += (v - S[i]) / 2), (S[i] = v); + } + } + if (inv) for (let i = 0; i < N; i++) S[i] = -S[i]; +} + +// Box blur of radius r, along x and then y, by running sums. +function box(src, dst, tmp, r) { + const n = 2 * r + 1; + for (let y = 0; y < H; y++) { + const o = y * W; + let s = src[o] * (r + 1); + for (let x = 1; x <= r; x++) s += src[o + x]; + for (let x = 0; x < W; x++) (tmp[o + x] = s / n), (s += src[o + min(x + r + 1, W - 1)] - src[o + max(x - r, 0)]); + } + for (let x = 0; x < W; x++) { + let s = tmp[x] * (r + 1); + for (let y = 1; y <= r; y++) s += tmp[y * W + x]; + for (let y = 0; y < H; y++) (dst[y * W + x] = s / n), (s += tmp[min(y + r + 1, H - 1) * W + x] - tmp[max(y - r, 0) * W + x]); + } +} + +// One Allen-Cahn step. The multiplier acts on the interfaces only, weighted +// by g = (1 - phi^2)^2, and is worked out over each 11x11 patch rather than +// the whole panel, so what a drop's rim loses to its curvature comes back on +// the same rim: a drop pulls round without shrinking, oil cannot go into +// solution in the water, and a drop only feeds a neighbour it nearly touches, +// as with Cahn-Hilliard. What the patches miss goes back over all the rims. +function relax(P, h) { + const phi = P.phi, r = P.nxt, g = P.b1, rb = P.b2, gb = P.blur, a = h * MOB; + for (let y = 0; y < H; y++) { + const up = y > 0 ? -W : 0, dn = y < H - 1 ? W : 0, gr = (GRAV * (H - 2 * y)) / H; + for (let x = 0; x < W; x++) { + const i = y * W + x, p = phi[i], q = 1 - p * p, w = q * q; + const lap = phi[x > 0 ? i - 1 : i] + phi[x < W - 1 ? i + 1 : i] + phi[i + up] + phi[i + dn] - 4 * p; + r[i] = a * (KAP * lap + p - p * p * p + gr * w); + g[i] = w; + } + } + box(r, rb, P.tmp, 5); + box(g, gb, P.tmp, 5); + let sum = 0, gs = 0; + for (let i = 0; i < N; i++) { + const q = (phi[i] += r[i] - (g[i] * rb[i]) / (gb[i] + 1e-9)); + sum += q; + gs += g[i]; + } + const d = (P.mT * N - sum) / (gs + 1e-6); + for (let i = 0; i < N; i++) phi[i] += d * g[i]; +} + +// The flow. Oil is lighter, so each coarse cell pushes up by how much oil it +// holds; screened Stokes flow carries the drops, big ones faster, and water +// comes down round them. A shake adds a swirl of the two lowest modes. +function flow(P, swirl, phase) { + const { phi, F, A, G, psi, U, V } = P; + F.fill(0); + for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) F[(y >> 2) * CW + (x >> 2)] += phi[y * W + x]; + // The curl of the push, -B dF/dx, then along x, along y, scaled, and back. + for (let j = 0; j < CH; j++) + for (let i = 0; i < CW; i++) G[j * CW + i] = (-BUOY / 32) * (F[j * CW + min(i + 1, CW - 1)] - F[j * CW + max(i - 1, 0)]); + for (let j = 0; j < CH; j++) + for (let m = 0; m < CW; m++) { + let s = 0; + for (let i = 0; i < CW; i++) s += G[j * CW + i] * SX[m * CW + i]; + A[j * CW + m] = s; + } + for (let n = 0; n < CH; n++) + for (let m = 0; m < CW; m++) { + let s = 0; + for (let j = 0; j < CH; j++) s += A[j * CW + m] * SY[n * CH + j]; + G[n * CW + m] = s * KI[n * CW + m]; + } + for (let j = 0; j < CH; j++) + for (let m = 0; m < CW; m++) { + let s = 0; + for (let n = 0; n < CH; n++) s += G[n * CW + m] * SY[n * CH + j]; + A[j * CW + m] = s; + } + const c1 = cos(phase), s1 = sin(phase); + for (let j = 0; j < CH; j++) + for (let i = 0; i < CW; i++) { + let s = 0; + for (let m = 0; m < CW; m++) s += A[j * CW + m] * SX[m * CW + i]; + const X = (PI * (i + 0.5)) / CW, Y = (PI * (j + 0.5)) / CH; + psi[j * CW + i] = s + swirl * (sin(X) * sin(2 * Y) * c1 + 0.8 * sin(2 * X) * sin(Y) * s1); + } + // u = dpsi/dy, v = -dpsi/dx, in LEDs per time unit; psi is 0 past the walls. + for (let j = 0; j < CH; j++) + for (let i = 0; i < CW; i++) { + const k = j * CW + i, p = psi[k]; + U[k] = 2 * ((j < CH - 1 ? psi[k + CW] : -p) - (j > 0 ? psi[k - CW] : -p)); + V[k] = -2 * ((i < CW - 1 ? psi[k + 1] : -p) - (i > 0 ? psi[k - 1] : -p)); + } +} + +// The flow at every LED. A drop of either liquid under AMAX LEDs moves as one +// piece at the mean of the flow over it, as surface tension holds a small +// drop together, and so do the two rings of LEDs round it, which it is about +// to take the place of. On top of that each drop is drawn towards the circle +// of its own area: every LED of it moves along the radius at ROUND times its +// distance from that circle, which curvature alone would take seconds over +// for a big drop. A drop on a side wall slides up it and is drawn to a half +// circle standing on the wall. Every drop rises, or sinks if it is water, at +// least DRIFT LEDs a time unit faster than the coarse flow says, so the last +// small ones still get there. The layers and the liquid between the drops +// follow the flow smoothed over 5 LEDs; during a shake the swirl takes over. +function velocity(P, rigid) { + const { U, V, u, v, b1, b2, phi, lab, stk, cu, cv, ca, px, py, rad } = P; + for (let y = 0; y < H; y++) { + const gy = cl((y - 1.5) / 4, 0, CH - 1), j0 = gy | 0, fy = gy - j0, a = j0 * CW, b = min(j0 + 1, CH - 1) * CW; + for (let x = 0; x < W; x++) { + const gx = cl((x - 1.5) / 4, 0, CW - 1), i0 = gx | 0, i1 = min(i0 + 1, CW - 1), fx = gx - i0, i = y * W + x; + u[i] = (U[a + i0] * (1 - fx) + U[a + i1] * fx) * (1 - fy) + (U[b + i0] * (1 - fx) + U[b + i1] * fx) * fy; + v[i] = (V[a + i0] * (1 - fx) + V[a + i1] * fx) * (1 - fy) + (V[b + i0] * (1 - fx) + V[b + i1] * fx) * fy; + } + } + box(u, b1, P.tmp, 2); + box(v, b2, P.tmp, 2); + if (rigid <= 0) return u.set(b1), v.set(b2); + // Label the pieces of each liquid, 4-connected. + lab.fill(-1); + let nc = 0; + for (let s = 0; s < N; s++) { + if (lab[s] >= 0) continue; + const ph = phi[s] > 0; + let top = 0, area = 0, su = 0, sv = 0, mx = 0, my = 0, wall = false; + stk[top++] = s; + lab[s] = nc; + while (top > 0) { + const i = stk[--top], x = i % W; + area++; + if (x === 0 || x === W - 1) wall = true; + (su += u[i]), (sv += v[i]), (mx += x), (my += (i - x) / W); + if (x > 0 && lab[i - 1] < 0 && phi[i - 1] > 0 === ph) (lab[i - 1] = nc), (stk[top++] = i - 1); + if (x < W - 1 && lab[i + 1] < 0 && phi[i + 1] > 0 === ph) (lab[i + 1] = nc), (stk[top++] = i + 1); + if (i >= W && lab[i - W] < 0 && phi[i - W] > 0 === ph) (lab[i - W] = nc), (stk[top++] = i - W); + if (i < N - W && lab[i + W] < 0 && phi[i + W] > 0 === ph) (lab[i + W] = nc), (stk[top++] = i + W); + } + (ca[nc] = area), (cu[nc] = wall ? 0 : su / area), (cv[nc] = sv / area + (ph ? -DRIFT : DRIFT)); + (px[nc] = wall ? (mx / area < W / 2 ? -0.5 : W - 0.5) : mx / area), (py[nc] = my / area); + rad[nc] = sqrt(((wall ? 2 : 1) * area) / PI); + nc++; + } + const put = (i, k) => { + const dx = (i % W) - px[k], dy = ((i / W) | 0) - py[k], w = ROUND * (rad[k] / (sqrt(dx * dx + dy * dy) + 0.5) - 1); + (u[i] = cu[k] + w * dx), (v[i] = cv[k] + w * dy), (lab[i] = k); + }; + let top = 0; + for (let i = 0; i < N; i++) { + if (ca[lab[i]] < AMAX) put(i, lab[i]), (stk[top++] = i); + else (u[i] = b1[i]), (v[i] = b2[i]), (lab[i] = -1); + } + // Two rings round each drop, nearest drop first. + for (let ring = 0, from = 0; ring < 2; ring++) { + const end = top; + for (let t = from; t < end; t++) { + const i = stk[t], x = i % W, k = lab[i]; + if (x > 0 && lab[i - 1] < 0) put(i - 1, k), (stk[top++] = i - 1); + if (x < W - 1 && lab[i + 1] < 0) put(i + 1, k), (stk[top++] = i + 1); + if (i >= W && lab[i - W] < 0) put(i - W, k), (stk[top++] = i - W); + if (i < N - W && lab[i + W] < 0) put(i + W, k), (stk[top++] = i + W); + } + from = end; + } + if (rigid < 1) for (let i = 0; i < N; i++) (u[i] = b1[i] + (u[i] - b1[i]) * rigid), (v[i] = b2[i] + (v[i] - b2[i]) * rigid); +} + +// Semi-Lagrangian advection of a field over T time units, from src into dst. +// Past the top the bottle holds oil (top) and past the bottom water (bot), so +// a drop leaving the floor does not draw a streak of itself up behind it. +function advect(P, src, dst, T, top, bot) { + const { u, v } = P; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x, sx = cl(x - u[i] * T, 0, W - 1), sy = y - v[i] * T; + const x0 = sx | 0, x1 = min(x0 + 1, W - 1), ex = sx - x0, y0 = floor(sy), ey = sy - y0; + const a = y0 < 0 ? top : y0 > H - 1 ? bot : src[y0 * W + x0] * (1 - ex) + src[y0 * W + x1] * ex; + const b = y0 < -1 ? top : y0 > H - 2 ? bot : src[(y0 + 1) * W + x0] * (1 - ex) + src[(y0 + 1) * W + x1] * ex; + dst[i] = a * (1 - ey) + b * ey; + } +} + +export function update(dt, input, params) { + const k = input.knobValues || [0, 2, 0.3, 0.06]; + if (!params.s) params.s = init(k); + const P = params.s, d = cl(dt || 0, 0, 0.1); + P.hue = k[0]; + P.speed = cl(k[1], 0.2, 3); + P.oil = cl(k[2], 0.3, 0.6); + P.shake = cl(k[3], 0, 1); + // The amount of oil follows K3 at a fifth of the bottle a second, so the + // layer grows or thins instead of the water clouding over. + P.mT += cl(2 * P.oil - 1 - P.mT, -0.4 * d, 0.4 * d); + const T = d * P.speed * RATE; + P.rt += d; + P.time += T; + P.c += T; + // A shake every 560 time units (23 s at Speed 2) at K4 0, down to 120 at 1. + if ((input.btnPressed && input.btnPressed[0] && P.c > TS) || P.c > 560 - 440 * sqrt(P.shake)) (P.c = 0), shake(P); + // The swirl winds the liquid round for the first two thirds of a shake, and + // as it dies the liquid breaks up into the new drops. + const shaking = P.c < TS, u = P.c / TS, e = shaking ? sin(PI * min(1, u / 0.65)) ** 2 : 0; + flow(P, 10 * e, P.time * 0.3); + velocity(P, 1 - e); + // phi is carried as atanh(phi), which runs nearly straight through an + // interface, so the linear interpolation does not smear the drops' edges. + const phi = P.phi, q = P.nxt; + for (let i = 0; i < N; i++) { + const p = cl(phi[i], -0.99999, 0.99999); + q[i] = 0.5 * log((1 + p) / (1 - p)); + } + advect(P, q, phi, T, 6, -6); + for (let i = 0; i < N; i++) phi[i] = 1 - 2 / (exp(2 * phi[i]) + 1); + if (shaking) { + const w = 1 - exp(-0.5 * sm(0.4, 0.8, u) * T), S = P.S; + for (let i = 0; i < N; i++) phi[i] += w * (S[i] - phi[i]); + } + const steps = Math.ceil(T / HMAX - 1e-6); + for (let s = 0; s < steps; s++) relax(P, T / steps); + // A drop shrunk to one LED inside the other liquid cannot go anywhere under + // the local multiplier and would stay as a dimple: fill it in. + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x, p = phi[i]; + if (p > 0.97 || p < -0.97) continue; + const a = phi[x > 0 ? i - 1 : i + 1], b = phi[x < W - 1 ? i + 1 : i - 1], c = phi[y > 0 ? i - W : i + W], e = phi[y < H - 1 ? i + W : i - W]; + const lo = min(a, b, c, e), hi = max(a, b, c, e); + if ((lo > 0.5 && p < lo - 0.1) || (hi < -0.5 && p > hi + 0.1)) phi[i] = (a + b + c + e) / 4; + } +} + +// Light from the upper left and in front, and the half vector for the glint. +const LX = -0.42, LY = -0.56, LZ = 0.71, HN = Math.hypot(LX, LY, LZ + 1), HX = LX / HN, HY = LY / HN, HZ = (LZ + 1) / HN; + +export function draw(display, params) { + const P = params.s; + if (!P) return; + const phi = P.phi, bl = P.blur, deep = P.deep, pale = P.pale; + // phi blurred over 5x5 is read as the height of the oil: a drop is a dome + // and the layer a plateau, lit from the upper left. + box(phi, bl, P.tmp, 2); + hsv(P.hue + 0.07, 0.62, 1, deep); + hsv(P.hue + 0.07, 0.26, 1, pale); + // A soft band of shop light drifts across the glass every 14 s. + const band = W / 2 + 46 * sin((2 * PI * P.rt) / 14); + for (let y = 0; y < H; y++) { + const shop = 0.86 + 0.14 * (1 - y / H); + // Water: a pale cool white, a shade bluer low down. + const wr = 0.76 - 0.06 * (y / H), wg = 0.82 - 0.03 * (y / H), wb = 0.93; + for (let x = 0; x < W; x++) { + const i = y * W + x, p = phi[i], o = sm(-0.6, 0.6, p); + const gx = phi[x < W - 1 ? i + 1 : i] - phi[x > 0 ? i - 1 : i], gy = phi[y < H - 1 ? i + W : i] - phi[y > 0 ? i - W : i]; + const nx = -2.4 * (bl[x < W - 1 ? i + 1 : i] - bl[x > 0 ? i - 1 : i]), ny = -2.4 * (bl[y < H - 1 ? i + W : i] - bl[y > 0 ? i - W : i]); + const nn = 1 / sqrt(nx * nx + ny * ny + 1), c1 = max(0, (nx * HX + ny * HY + HZ) * nn), c4 = c1 * c1 * c1 * c1, c16 = c4 * c4 * c4 * c4; + // Oil: pale at a drop's rim and deep in its middle and in the layer, + // shaded as a dome, never below 80% so it does not go brown. + const dpt = sm(-0.2, 0.9, bl[i]), sh = max(0.8, 1 + 0.6 * ((nx * LX + ny * LY + LZ) * nn - LZ)); + const r = wr + ((pale[0] + (deep[0] - pale[0]) * dpt) * sh - wr) * o; + const g = wg + ((pale[1] + (deep[1] - pale[1]) * dpt) * sh - wg) * o; + const b = wb + ((pale[2] + (deep[2] - pale[2]) * dpt) * sh - wb) * o; + // The rim catches the light from above, a dome its glint, and a long + // flat interface, the meniscus, shows as a bright line. + const q = 1 - p * p, rim = q * q, lit = cl(gy * 0.6 + gx * 0.25, 0, 1), flat = (gy * gy) / (gx * gx + gy * gy + 1e-6); + const men = rim * flat * flat * sm(0.45, 0.75, abs(ny / 2.4)); + const bx = (x - band + 0.4 * (y - H / 2)) / 11, sheen = bx * bx < 9 ? exp(-bx * bx) * (0.05 + 0.1 * o) : 0; + const hl = cl(0.1 * rim + 0.45 * rim * lit + 0.7 * c16 * c16 * o + 0.8 * men + sheen, 0, 1); + display.setPixel(x, y, (r + (1 - r) * hl) * shop * 255, (g + (1 - g) * hl) * shop * 255, (b + (1 - b) * hl) * shop * 255); + } + } +} From 6d89dae6a5903147e9e84cf6981cb02c069d8beb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 12:09:38 +0000 Subject: [PATCH 13/15] =?UTF-8?q?30=ED=8C=90=20=EC=97=B0=EA=B5=AC=20?= =?UTF-8?q?=EB=85=B8=ED=8A=B8=EB=A5=BC=20README=20=EC=97=90=20=EC=A0=81?= =?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_01QuEe7qhvCVuDuGNB5fZ113 --- incoming/pattern-study/README.md | 79 ++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 326695810..768e7e6ba 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 188개를 쉰세 갈래로 나눠 만들었어요. 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판은 주제 표가 수학 쪽으로 바뀐 첫 판이라 이름 있는 수학 대상 열 개를 열 갈래에 하나씩 더했고, 수론 갈래를 새로 열었어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 198개를 쉰네 갈래로 나눠 만들었어요. 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판은 주제 표가 수학 쪽으로 바뀐 첫 판이라 이름 있는 수학 대상 열 개를 열 갈래에 하나씩 더했고, 수론 갈래를 새로 열었어요. 30판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 뒤의 판이라, 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고 팝업 매장의 LED 벽에 틀 질감 넷을 더했어요. 화장품 팝업 갈래는 그때 새로 열었어요. [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판에서 세로로 다시 짰어요. @@ -324,6 +324,21 @@ | [Stadium Billiard](patterns/stadium-billiard.js) | 물리·스프링 | 공 이백 개가 방향을 1000분의 3 라디안 안에서만 달리해 한 점에서 흰 구슬 하나로 떠나요. 패널만 한 부니모비치 경기장의 둥근 끝이 구슬을 무지개 선으로 늘이고 접어서, 몇 초 만에 공들이 기체처럼 판을 채워요. K3 을 돌리면 탁자가 원이 되어 공이 흩어지지 않고, 누르면 새 무리가 떠나요. | | [Wythoff Nim](patterns/wythoff-nim.js) | 알고리즘 | 위토프 게임의 스프라그-그런디 값 표를 모서리로 세웠어요. 지는 자리(값 0)는 황금비 두 줄을 따라 금빛이고, 값 0, 1, 2 가 차례로 쓸고 지나가며 저마다의 두 줄에서 번쩍였다 표로 가라앉아요. 누르면 흰 말이 떨어져 이기는 수로 모서리까지 게임을 해요. | +### 30판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Ballistic Deposition](patterns/ballistic-deposition.js) | 입자 | 입자가 64열 위로 곧게 떨어져 처음 닿는 자리에 붙어요. 처마 밑이 빈 퇴적물이 내려앉은 차례대로 띠를 이루고, 띠의 경계가 거칠어지는 앞면의 지난 모습이에요. | +| [Bi-Phase](patterns/bi-phase.js) | 화장품 팝업 | 흔들어 섞은 이중층 클렌저가 패널 가득 기름 방울로 흩어졌다가, 방울끼리 합쳐 떠올라 위에 한 층으로 모이고 경계선이 맑게 서요. 그러고 나면 다시 흔들려요. | +| [Hofstadter Butterfly](patterns/hofstadter-butterfly.js) | 프랙탈 | 자기장 속 격자 전자의 에너지 띠를 가로에, 자속을 세로에 놓았어요. 띠 사이 틈마다 홀 전도도의 정수로 칠해 날개가 서고, 같은 밀도의 줄이 날개를 따라 흘러요. | +| [Meander](patterns/meander.js) | 선·궤적 | 패널 한가운데 세로로 난 길을 12번에서 32번 건너는 닫힌 곡선 하나예요. 한쪽 아치들이 길 위로 누웠다가 새 짝으로 펴져도 곡선은 늘 하나로 닫혀요. | +| [Outer Billiards](patterns/outer-billiards.js) | 기하·대칭 | 정오각형 밖의 평면을 점마다 꼭짓점 대칭 뛰기 몇 번 만에 제자리로 오는지로 칠했어요. 십각형과 오각형 목걸이가 둘레를 감고, 흰 점 하나가 제 궤도를 뛰어요. | +| [Pearl Sheen](patterns/pearl-sheen.js) | 화장품 팝업 | 펄 안료를 부은 면이 새틴 결로 흘러내려요. 누운 결은 로즈이고 기운 결은 박막 간섭으로 금빛 쪽으로 넘어가며, 긴 반사띠가 면을 가로질러요. | +| [Percolation](patterns/percolation.js) | 보로노이·셀 | LED 마다 정해 둔 난수가 p 보다 작으면 열리고, 이어진 자리끼리 제 색의 클러스터가 돼요. p 가 오르다 한 클러스터가 위아래 128줄을 이으면 금빛으로 켜지고 가장 짧은 길이 그어져요. | +| [Satin Fold](patterns/satin-fold.js) | 화장품 팝업 | 한 점에 잡힌 연한 새틴이 부채꼴 주름으로 흔들리고, 광택이 주름을 따라 긴 줄로 서요. 금색 실 하나가 천을 건너며 주름마다 휘어요. | +| [Swatch Row](patterns/swatch-row.js) | 화장품 팝업 | 보이지 않는 손가락이 아이보리 카드에 크림 안료를 한 줄씩 끌어 색조표를 채워요. 줄마다 한 단계씩 밝거나 짙고, K1 하나로 어떤 색 라인으로든 바뀌어요. | +| [Three Gaps](patterns/three-gaps.js) | 수론 | 원 위에 α 씩 돌아가며 점을 찍으면 이웃 간격은 많아야 세 가지예요. 줄마다 점을 하나씩 더해 간격을 길이 등급으로 칠하고, α 가 분수에 닿으면 아래쪽이 줄무늬로 무너져요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -1254,6 +1269,50 @@ E8 의 근 240개를 고셋의 4₂₁ 다면체 꼭짓점으로 그렸어요. 위토프 게임의 스프라그-그런디 값 표예요. 지는 자리(값 0)는 (⌊nφ⌋, ⌊nφ²⌋) 와 그 거울이라 황금비 두 줄을 따르고, 다른 값 k 도 저마다 그 기울기 언저리의 두 줄에 모이며 주기 없이 흔들려요. 처음에는 x 를 가로, y 를 아래로 그렸더니 가파른 줄이 103번 줄에서 패널을 나가고 왼쪽 아래가 빈 안개였어요. 표를 45° 돌려(u = x−y, v = x+y) 세우자 두 줄이 세로에서 ±13° 로 맞서서 아래 두 귀퉁이까지 닿았어요. 두 더미에서 같이 덜어 내는 수는 그 덕에 곧장 위를 가리켜요. 처음 색표는 천천히 올라가서 값 0~5 가 한 금빛 띠로 읽혔고 정확한 0 줄이 묻혔어요. 0 에만 밝은 금빛을 주고 1 부터 진홍으로 빨리 올리자 풀렸어요. 정수가 아닌 Zoom 은 들쭉날쭉한 무아레 마름모를 만들어서 정수로 반올림했어요. 쓸기는 값 K = 12(e^(2.5t/12) − 1) 로 가서 Speed 2 에서 3초에 10, 8초에 50 에 닿아요. K2 Speed 의 반응 0.04 가 넷 가운데 가장 약해요. `thirds` 는 62/100/100 이에요. +## 30판 + +30판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 뒤에 돈 판이에요. 같이 돈 29판(#219)이 j 불변량과 호프 올뭉치, 크림과 젤 같은 것을 먼저 골라서, 이 판은 그와 겹치지 않게 골랐어요. 주제 5 의 여섯은 세 간격 정리, 호프스태터 나비, 외부 당구, 탄도 퇴적, 미앤더, 스미기예요. 「낯선 수학 예」에서는 세 간격 정리와 외부 당구 둘을 골랐어요. 여섯 다 있는 갈래에 들어가서 주제 2 로 연 갈래는 없어요. 주제 6 의 넷은 손가락 스와치와 펄 안료, 이중층 클렌저, 새틴이고 화장품 팝업 갈래(`cosmetic`)에 모았어요. ROUND.md 첫머리는 열 개의 갈래가 모두 다르다고 하고 주제 6 은 넷을 한 갈래에 모으라고 해서, 수학 여섯만 서로 다른 갈래에 두었어요. 주제 1 은 기준 188개의 21장이 이번에도 다 `none` 이라 28판처럼 패턴이 하나뿐인 칸 둘을 겨눴어요. Percolation 이 Waggle Dance 혼자이던 복잡도 × 채움 4,3 에, Meander 가 Bauhaus 혼자이던 채움 × 색 2,4 에 섰어요. 주제 4 는 Three Gaps 와 Hofstadter Butterfly 예요. 둘 다 수학이 세로축을 정해서, 한쪽은 줄 번호가 점의 수이고 다른 쪽은 줄의 높이가 자속이에요. Ballistic Deposition, Meander, Percolation 도 3분의 1마다 24% 를 넘게 켜요. + +### Three Gaps + +주제 4 와 5 로 세 간격 정리를 줄마다 그렸어요. 원 위에 0, α, 2α, … 를 찍으면 점이 몇 개든 이웃 사이 간격은 많아야 세 가지이고, 가장 긴 것이 나머지 둘의 합이에요. 한 줄이 원 하나를 64칸에 편 것이고, 맨 윗줄은 점 하나, 그 아래로 한 줄마다 점이 하나씩 늘어요. 칸마다 그 칸이 든 간격의 길이 등급으로 칠해서 가장 긴 것은 노랑, 가운데는 청록, 가장 짧은 것은 자홍이에요. 간격의 길이는 양 끝 점 번호의 차이 j−i 로 정해지는 frac((j−i)α) 라서, 허용오차 없이 등급을 셌어요. α 는 1/φ 에서 출발해 분모 21 이하의 가장 가까운 분수 13/21 과 8/13 사이를 오가요. 분수에 닿으면 21번째 줄 아래가 모두 같은 21칸 줄무늬로 무너지고, Speed 2 에서 그때가 2.2초예요. 1/φ 에서는 색이 띠째로 바뀌는 줄이 피보나치 수 8, 13, 21, 34 번째에 서요. 처음 판은 가장 짧은 간격을 밝기 0.25 까지 눌러서 보라가 거의 검게 보였고, 2 px 간격마다 1 px 검은 점 줄을 그어 아래 절반이 바코드였어요. 밝기 바닥을 0.6 으로 올리고 점 줄을 간격 폭에 따라 흐리게 하자 움직임이 0.185 에서 0.138 로, 색이 0.659 에서 0.703 으로 갔어요. 마지막 판은 움직임 0.148, 채움 0.997, 색 0.643 이고 `thirds` 는 99/99/99 예요. + +### Hofstadter Butterfly + +주제 4 와 5 로 하퍼 방정식의 스펙트럼을 세웠어요. 가로 64열이 에너지이고 세로 128줄이 자속 α 라서 아래가 0, 위가 1이에요. 줄마다 α 에 가장 가까운 분수 p/q(q ≤ Depth)를 고르고, 전달행렬 q개를 곱한 대각합으로 띠를 정해요. 체임버스 관계 Δ(E,ν) = D − 2λ^q cos(qν) 는 Node 에서 1e-6 안으로 맞는 것을 확인했어요. 띠 사이 틈은 TKNN 의 정수 t 로 칠했어요. 틈 아래 띠의 수 r 을 3q 칸 사슬의 Sturm 수로 세고 t ≡ r·p⁻¹ (mod q) 를 풀어요. t = +1 날개는 청록, t = −1 날개는 자홍이에요. 틈마다 밝기를 cos(2π(2N − w)) 로 일렁이게 해서, 같은 상태 밀도의 줄이 t > 0 날개에서는 오르고 t < 0 날개에서는 내려가요. 그 빠르기가 1/t 에 비례해서 스트레다 공식 dN/dα = t 가 그대로 보여요. λ = 1 에서 띠는 판의 6% 뿐이라, 64열에서는 띠보다 틈의 색이 그림을 만들어요. 틈의 깊이로 날개 안쪽을 어둡게 한 판은 q 가 작은 줄과 큰 줄이 이어지지 않아 가로줄 무늬가 생겼고 채움이 0.713 으로 떨어져서 뺐어요. Depth 를 12 에서 열면 1/2 과 1/3 줄이 굵은 회색 막대라, 날개가 온전해지는 16 을 최솟값으로 했어요. K4 Coupling 은 λ = 2^K4 이고, E 축을 2+2λ 로 늘이면 λ = 1/2 과 2 의 틈 라벨이 표본 49,152개 모두에서 같았어요. 오브리-앙드레 쌍대성이에요. `thirds` 80/67/80, 색 0.434 예요. + +### Outer Billiards + +주제 5 로 노이만의 외부 당구(1959)를 정오각형 둘레에 그렸어요. 다각형 밖의 점을, 다각형이 오른쪽에 오게 댄 접선의 꼭짓점에 대해 점대칭으로 뒤집어요. 평면을 점마다 몇 번 뛰어 제자리로 오는지로 칠하면 타바치니코프(1995)의 목걸이가 나와요. 오각형 바로 둘레에 주기 5 인 큰 십각형 다섯, 그 밖에 주기 20 인 오각형 고리, 위아래에 주기 15 인 큰 십각형이에요. 두 번 뛰면 평행이동이라 짝수 번째에서만 1e-7 안에 돌아왔는지 봐요. 주기 지도는 D_n 대칭이라 각도 π/n 쐐기 하나만 로그 극좌표 격자로 셈하고, 그 셈을 한 프레임에 50만 번 뛰기까지 나눠서 16프레임 안에 끝내요. 같은 모양의 오각형 영역 넓이비가 17.9, φ⁶ 이라서 길이비 φ³ 의 자기닮음이 숫자로 보였어요. 흰 점 하나가 궤도를 뛰면서 꼭짓점을 지나는 줄을 남기고, 내려앉은 자리는 한 바퀴 동안 남아 목걸이를 드러내요. 처음에는 오각형 옆의 작은 영역 궤도가 뽑혀 줄이 거의 안 보였고, 그 뒤에는 2분 내내 주기 10 궤도만 나왔어요. 최근 두 주기를 덜 뽑고 궤도 한계를 44 LED 로 넓히자 주기 10, 20, 50 이 번갈아 나와요. 회전을 늦춰 움직임이 0.696 에서 0.516 이 됐어요. 십각형은 지름 10 LED 아래에서 원으로 보이고, 오각형은 6 LED 에서도 오각형으로 읽혀요. + +### Ballistic Deposition + +주제 5 로 볼드의 탄도 퇴적(1959)을 쌓았어요. 입자가 64열 위로 곧게 떨어져 제 열의 꼭대기나 더 높은 이웃 열의 옆면 가운데 처음 닿는 곳에 붙어요. 그래서 h_i ← max(h_{i−1}, h_i+1, h_{i+1}) 이고, 처마 밑이 비어 셀의 47% 만 차요. 쌓인 입자는 내려앉은 차례로 두 색을 번갈아 받아서 띠 경계가 지난날의 앞면이에요. 앞면은 카르다르-파리시-장 부류라 폭이 t^(1/3) 로 자라요. 40번 평균으로 3층에서 10층 사이 폭의 비가 1.55 였고 (10/3)^(1/3) 은 1.49 예요. 64열에서는 30층쯤에서 폭 4.5줄로 멈춰서, 거칠어지는 과정은 평평한 바닥에서 시작한 처음 화면과 K1 이 까는 흰 뚜껑 위에서 보여요. K3 Slip 은 옆면에 붙지 않고 제 열로 미끄러질 확률이고, 1 이면 구멍 없는 무작위 퇴적이 되어 바늘 같은 열이 √t 로 끝없이 벌어져요. 내려앉을 자리를 도착할 때 정한 첫 판은 한 프레임에 50개가 떨어지는 사이 이웃 열이 입자 위로 자라 넓은 V자 부채꼴이 생겼어요. 떠나는 순간 정하게 바꾸자 볼드의 한 번에 하나씩 순서와 같아졌어요. 띠마다 색상을 0.06 씩 돌린 판은 무지개라 경계가 흐렸고, 두 색을 번갈게 하자 경계가 읽혔어요. `thirds` 24/48/50 이에요. + +### Meander + +주제 5 로 아르놀트의 미앤더(1988)를 세웠어요. 곧은 길을 2n 번 건너는 닫힌 곡선 하나이고, 길 양쪽의 겹치지 않는 짝 맞춤 둘이 고리 하나로 이어질 때만 미앤더예요. 길이 패널 한가운데를 위아래로 지나고, 왼쪽 아치와 오른쪽 아치가 이어져요. 곡선 안쪽은 짝홀 규칙으로 채우고, 색상환 한 바퀴를 고리 길이를 따라 한 번 깔아서 닫힌 곡선 위에서 색이 끊기지 않아요. 짝 맞춤은 순환 보조정리로 고른 고른 Dyck 단어이고, 고리가 하나일 때까지 다시 뽑아요. 통과율은 n = 6 에서 1828/132², 열 번에 한 번쯤이에요. 5초마다 한쪽 아치가 가장 안쪽부터 길 위로 눕고, 반대쪽과 고리 하나를 이루는 새 짝이 바깥 아치부터 펴져요. 처음 생각한 반타원은 서로 지나갔어요. 바깥 아치가 그 행에서 안쪽 아치보다 좁아지는 자리가 있었어요. 반원 반지름을 깊이 폭에서 w·r/(w⁴+r⁴)^(1/4) 로 부드럽게 잘라 두 값에 대해 늘기만 하게 하자 포개진 아치가 닿지 않았어요. 주제 1 로 채움 × 색 2,4 를 겨눠서, 채움 밝기를 214/255 에서 230/255 언저리로 올려 색이 0.517 에서 0.549 가 됐어요. 채움은 0.498 이에요. Hasler 와 Süsstrunk 의 값은 꺼진 LED 도 세서, 반쯤 켜진 그림에서는 색이 몇 가지냐보다 켜진 쪽의 밝기가 더 크게 들었어요. + +### Percolation + +주제 5 로 브로드벤트와 해머슬리(1957)의 자리 스미기를 패널 한 장에 깔았어요. LED 마다 난수 u 를 한 번 정하고 u < p 이면 열려요. 변으로 이웃한 열린 자리가 클러스터가 되고, p 가 천천히 오르내려요. 어떤 클러스터가 맨 윗줄과 맨 아랫줄을 처음 이으면 그 클러스터가 하얗게 번쩍인 뒤 금색이 되고, 그 안의 가장 짧은 위아래 길이 흰 선으로 그어져요. 클러스터 색은 합쳐질 때 지난 프레임에 더 컸던 쪽을 물려받아서 깜박이지 않아요. 128줄을 잇는 p* 는 뉴먼-지프(2000) 식으로 자리를 u 차례로 열어 찾았고 연구의 씨앗에서 0.6038 이에요. 씨앗 열둘에서는 0.568~0.622 로 흩어졌어요. 너비 64 에 높이 128 인 띠는 p_c 0.5927 에서 위아래가 이어질 확률이 낮아서, 쓸기를 p_c 가 아니라 그 격자의 p* 에 걸어야 사건이 매번 보였어요. 주제 1 로 복잡도 × 채움 4,3 을 겨눴어요. 큰 클러스터가 한 색이면 압축이 잘 돼서 첫 판의 복잡도는 2.041 이었고, 자리마다 색상 ±0.05 와 열린 지 오래된 정도에 따른 밝기를 한 번 정해 두자 2.538 이 됐어요. 채움은 0.608 이에요. 닫힌 자리는 19/255 아래로 눌러 채움에 들지 않게 했어요. + +### Swatch Row + +주제 6 으로 메이크업 아티스트의 스와치를 그렸어요. 아이보리 카드 위에 64×16 칸마다 스와치가 하나씩 여덟 줄이 서서 128줄을 다 채워요. 보이지 않는 손가락이 맨 아래 빈 칸에 크림 안료를 1.05초 동안 왼쪽에서 오른쪽으로 끌고, 쌓인 줄이 16 LED 위로 미끄러지면 맨 위 스와치가 밖으로 나가요. 획은 반원으로 시작해 닿은 자리 2.5 LED 가 20% 짙고, 길이의 40~56% 에서부터 가늘어지며 결이 약한 줄부터 끊겨 나가요. 줄마다 밝기가 한 단계씩 달라서 색조표가 돼요. 밝은 쪽은 색상이 따뜻한 쪽으로 기울고 짙은 쪽은 찬 쪽으로 기울어요. K4 Finish 는 매트, 새틴, 메탈릭이고 메탈릭의 반짝임은 안료에 붙어서 같이 미끄러져요. 첫 판의 바탕 180/168/152 는 회갈색으로 보여서 198/186/164 로 올렸고, 색이 0.171 에서 0.199 가 됐어요. 바탕을 밝게 두어 채움 1.000 이고, 밝은 색조가 바탕보다 밝아야 아이보리 위에서 색으로 읽혔어요. 노브 반응은 0.12, 0.06, 0.05, 0.05 로 이번 열 개 가운데 낮은 편이에요. + +### Pearl Sheen + +주제 6 으로 펄 안료의 광택을 흘렸어요. 티타니아를 입힌 마이카 판이 박막 간섭으로 색을 내서, 광로차 0~1600 nm 마다 400~700 nm 를 5 nm 간격으로 더한 1024칸 표를 미리 만들어요. 평평한 판은 2차 자홍(980 nm)에 앉고, 결을 따라 기운 판은 빛이 비스듬히 지나 광로가 짧아져서 주황과 금색으로 넘어가요. 그것이 듀오크롬의 둘째 색이에요. K1 은 그 표를 회색축 둘레로 돌려서 0 에서 로즈와 복숭아빛, 0.5 에서 아쿠아와 페리윙클이 돼요. 결은 흐름 방향으로 12배 길게 늘인 노이즈이고, 긴 스트립 조명의 반사띠가 볼록한 면 위로 21초에 한 번 지나가요. 굴절률로 각도 이동을 그대로 건 판은 1 nm 에 색이 1° 쯤 돌아 Shift 0 에서도 산호색이 번졌고 1 에서는 무지개였어요. 기울기 문턱 18°~45° 와 최대 이동 비율로 바꾸자 두 색이 갈렸어요. 하이라이트를 0.85 위에서 누른 판은 반사띠가 사라져 움직임이 0.028 이었고, 바탕을 0.64 로 내리고 띠를 세게 하자 0.051 로 돌아왔어요. 가장자리 0.000 은 이번 열 개 가운데 가장 번진 값이에요. + +### Bi-Phase + +주제 6 으로 흔든 이중층 클렌저 속을 패널 가득 보여 줘요. K1 색의 기름 방울이 옅고 찬 물속에 흩어졌다가 합쳐지며 커지고, 큰 방울일수록 빨리 떠올라요. 위 기름층에 닿은 방울은 아래로 매달린 손가락처럼 보이다가 펴지고, 패널을 가로지르는 밝은 경계선 하나가 남으면 다시 흔들려요. 위상장 φ 를 앨런-칸 식으로 풀고, 기름 양을 지키는 승수를 계면에만 걸어 11×11 조각마다 맞춰요. 승수를 패널 전체에 하나만 건 판은 외톨이 방울이 물에 녹아서 반지름 5 방울의 넓이가 78 에서 16 으로 줄었어요. 조각마다 맞추자 76~82 로 남았어요. 부력 흐름은 16×32 격자에서 가려진 스토크스 흐름을 사인 변환으로 풀었는데, 그 흐름이 방울을 버섯과 캡 모양으로 찢어서 700 LED 아래 방울은 한 덩어리로 움직이게 했어요. Speed 2 에서 반지름 3 방울이 1초에 14 LED, 8 방울이 35 LED 떠올라요. 첫 판은 2초 안에 다 갈라져 움직임이 0.002 였고, 흐름을 늦춰 6~8초로 늘렸어요. 14초마다 지나가는 매장 조명 띠를 넣자 움직임이 0.018 에서 0.060 이 됐어요. 어두운 쪽 음영을 0.8 아래로 내리면 피치가 갈색으로 보였어요. Lab 안에서는 픽셀 루프의 `** 30` 과 `Math.log` 가 느려서, 곱셈과 구조분해한 `log` 로 바꾸자 한 프레임이 4.0 ms 에서 3.2 ms 가 됐어요. 파일은 19.4 KB 로 이번 열 개 가운데 가장 커요. + +### Satin Fold + +주제 6 으로 ROUND.md 가 적은 흰 바탕에 금색 한 줄을 천으로 그렸어요. 연한 새틴이 패널 160 LED 위의 한 점에 잡혀 부채꼴로 접혀 내려오고, 그 위에 직선 주름 넷이 차례로 더해져요. 광택은 카지야-케이(1989)의 섬유 하이라이트에 워드(1992) 식 이방성 로브를 써서, 새틴의 가로 결이 세로 주름을 가로지르는 곳에서만 빛을 잡아 긴 줄로 서요. 금색 실 하나가 천의 좌표 위 곡선으로 놓여 주름마다 휘고, 실이 주름 뒤로 접혀 넘어가는 곳에서는 흐려져요. 처음에는 섬유를 세로로, 큰 주름을 62° 로 두어서 둘이 거의 나란했고, 광택이 서지 않는 탁한 모브 면이었어요. 섬유를 가로로 돌리자 줄 광택이 서서 움직임이 0.030 에서 0.057 이 됐어요. 직선 주름만 겹친 판은 물결 커스틱처럼 보였고, 한 점에 잡힌 부채꼴로 바꾸자 커튼으로 읽혔어요. 흰 천에 가까웠던 첫 판은 LED 의 48% 가 224 를 넘었고, 가운데로 맞추자 움직임이 0.094, K2 반응이 0.22 가 됐어요. K4 Gold 를 0 으로 돌리면 실이 사라지고 1 이면 13 LED 리본이에요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -1296,7 +1355,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 24일 `dev`(`d99e838`)의 178개에 28판의 열 개를 더한 188개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 24일 `dev`(`d5edbb7`)의 188개에 30판의 열 개를 더한 198개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -1316,7 +1375,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ### 아직 없는 갈래 -`families.json` 의 쉰세 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 28판이 연 수론 갈래는 Ford Circles 하나예요. 위상·매듭 갈래는 28판이 열려다가 한 판에 하나라는 새 표에 따라 열지 않아서, Hairy Ball 은 노이즈·흐름에 있어요. 호프 올뭉치나 매듭이 들어오는 판이 위상 갈래를 열 수 있어요. 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` 의 쉰네 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 30판이 연 화장품 팝업 갈래는 Swatch Row, Pearl Sheen, Bi-Phase, Satin Fold 넷이에요. 이 갈래는 ROUND.md 의 주제 6 이 따로 정한 갈래라 주제 2 몫으로 세지 않아요. 28판이 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps 예요. 위상·매듭 갈래는 28판이 열려다가 한 판에 하나라는 새 표에 따라 열지 않아서, Hairy Ball 은 노이즈·흐름에 있어요. 호프 올뭉치나 매듭이 들어오는 판이 위상 갈래를 열 수 있어요. 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 이 셋째예요. ### 멋진 것 더 멋지게 @@ -1334,12 +1393,22 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. ### 낯선 수학 -주제 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)이에요. ROUND.md 의 「낯선 수학 예」 여덟 가운데 쓴 것은 슈람-뢰브너 진화와 아즈텍 다이아몬드 둘이고, 모듈러 형식과 리틀우드 다항식의 근, 호프 올뭉치, 클라인 군의 극한 집합, 세 간격 정리, 외부 당구가 남았어요. +주제 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)이에요. ROUND.md 의 「낯선 수학 예」 여덟 가운데 28판이 쓴 것은 슈람-뢰브너 진화와 아즈텍 다이아몬드 둘이에요. 30판은 여섯을 더 썼어요. 세 간격 정리(Three Gaps), 호프스태터 나비와 TKNN 정수(Hofstadter Butterfly), 노이만의 외부 당구(Outer Billiards), 볼드의 탄도 퇴적과 KPZ 앞면(Ballistic Deposition), 아르놀트의 미앤더(Meander), 브로드벤트와 해머슬리의 스미기(Percolation)예요. 「낯선 수학 예」에서는 세 간격 정리와 외부 당구를 써서, 모듈러 형식과 리틀우드 다항식의 근, 호프 올뭉치, 클라인 군의 극한 집합이 남았어요. -목록 밖에서 다음에 해 볼 것이에요. 리만 제타 함수를 임계선 위에서 리만-지겔 공식으로 셈하면 하디의 Z(t) 가 세운 패널을 따라 흐르고 영점이 지나갈 때마다 번쩍여요. t 가 수백이어도 항이 열 개 안팎이라 가벼워요. 길브레스 추측(1958)의 표는 소수의 차를 거듭 절댓값으로 빼 내린 줄들이라 128줄이 그대로 표가 되고, 맨 앞 칸이 늘 1 이에요. 콜라코스키 수열(1965)은 제 길이 부호를 읽으면 제가 되는 1 과 2 의 줄이라 리듬·펄스에 맞아요. 영 도형의 플랑셰렐 성장은 상자가 하나씩 붙으며 로건-셰프와 베르시크-케로프의 극한 곡선(1977)으로 다가가요. 아즈텍 다이아몬드의 북극권과 같은 극한 모양이라 같은 판에 두지 않아요. 슈타이너 사슬은 반전 하나로 고리의 원들이 미끄러지는 포리즘이고, 민코프스키의 물음표 함수(1904)는 페리 나무를 이진 나무로 펴는 계단이에요. Ford Circles 와 같은 수론이라 그 갈래의 둘째로 맞아요. +목록 밖에서 다음에 해 볼 것이에요. 리만 제타 함수를 임계선 위에서 리만-지겔 공식으로 셈하면 하디의 Z(t) 가 세운 패널을 따라 흐르고 영점이 지나갈 때마다 번쩍여요. t 가 수백이어도 항이 열 개 안팎이라 가벼워요. 길브레스 추측(1958)의 표는 소수의 차를 거듭 절댓값으로 빼 내린 줄들이라 128줄이 그대로 표가 되고, 맨 앞 칸이 늘 1 이에요. 콜라코스키 수열(1965)은 제 길이 부호를 읽으면 제가 되는 1 과 2 의 줄이라 리듬·펄스에 맞아요. 영 도형의 플랑셰렐 성장은 상자가 하나씩 붙으며 로건-셰프와 베르시크-케로프의 극한 곡선(1977)으로 다가가요. 아즈텍 다이아몬드의 북극권과 같은 극한 모양이라 같은 판에 두지 않아요. 슈타이너 사슬은 반전 하나로 고리의 원들이 미끄러지는 포리즘이고, 민코프스키의 물음표 함수(1904)는 페리 나무를 이진 나무로 펴는 계단이에요. 수론 갈래는 30판의 Three Gaps 로 둘이 돼서 셋째 자리예요. 아르놀트 혀(1961)는 원 사상의 결합 세기를 세로에, 회전수를 가로에 놓으면 분수마다 바닥에서 위로 벌어지는 혀가 128줄을 채워요. 로지스틱 분기도와 다른 그림이지만 같은 원 사상이라 「흔한 것」에 걸리는지 먼저 가려야 해요. 윌킨슨과 윌렘센의 침입 스미기(1983)는 바닥에서 문턱이 가장 낮은 이웃으로 한 칸씩 들어가며 갇힌 물을 남기는 성장이라, 30판의 Percolation 과 같은 격자로 다른 그림이 돼요. 렌니의 주차 문제(1958)는 줄마다 길이 1 인 차가 아무 데나 서다가 빈자리가 다 1 보다 짧아지면 멈추는데, 128줄이 다 멈추면 채운 몫이 렌니 상수 0.7476 언저리에 모여요. 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 씩만 쌓였어요. 빛을 테두리나 고리처럼 수학이 정한 자리에 모으면 읽혔어요. +30판에서 본 것이에요. 세로축을 수학이 정하면 128줄이 그대로 그림이 됐어요. Three Gaps 는 줄 번호가 점의 수이고 Hofstadter Butterfly 는 줄의 높이가 자속이라, 둘 다 `thirds` 가 3분의 1마다 67% 를 넘어요. 정수로 셀 수 있는 것은 정수로 셌어요. Three Gaps 는 간격의 길이를 양 끝 점 번호의 차이로 이름 붙여 허용오차 없이 등급을 나눴고, Hofstadter Butterfly 는 틈 아래 띠의 수를 Sturm 수로 세서 t 를 풀었어요. 부동소수로 길이나 띠 끝을 비교한 셈은 분수 언저리에서 순서가 뒤섞였어요. 무작위 대상은 이번에도 유한한 크기가 값을 정했어요. 64열의 탄도 퇴적은 30층쯤에서 거칠기가 멈춰서 평평한 출발이 있어야 t^(1/3) 이 보였고, 64×128 의 스미기는 p_c 가 아니라 그 격자가 처음 위아래를 잇는 p* 에 쓸기를 걸어야 사건이 매번 일어났어요. + +### 화장품 팝업 + +주제 6 으로 쓴 질감과 다음에 해 볼 질감이에요. 30판이 넷을 썼어요. 손가락으로 끈 스와치의 색조표(Swatch Row), 펄 안료의 박막 간섭과 듀오크롬(Pearl Sheen), 이중층 클렌저의 상 분리(Bi-Phase), 흰 새틴과 금색 실 하나(Satin Fold)예요. 같이 돈 29판이 크림과 젤, 글리터, 파우더를 맡아서 넷 다 그것을 비켜 골랐어요. + +다음에 해 볼 것이에요. 세팅 스프레이의 안개는 고운 물방울이 원뿔로 뿜어졌다가 유리판에 맺혀 굵어지고 흘러내려요. 클레이 마스크는 마르면서 가장자리부터 옅어지고 갈라지는데, 26판의 Drying Mud 와 겹치지 않게 갈라짐보다 색이 빠지는 쪽을 주인공으로 둬야 해요. 리퀴드 립의 드립은 진한 안료 한 방울이 흰 바탕을 천천히 흘러내리며 뒤에 광택 줄을 남겨서 세운 패널에 맞아요. 압축 파우더의 엠보싱은 29판의 Loose Powder 와 같은 가루라 그 판이 머지된 뒤 겹치는지 보고 골라요. + +30판에서 본 것이에요. 바탕을 밝게 두면 채움이 1.000 이고 `thirds` 가 100/100/100 이라, 이 넷은 지도에서 채움 축의 위 끝에 모여요. 아이보리 바탕 198/186/164 에서는 밝은 색조가 바탕보다 밝아야 색으로 읽혔고, 180/168/152 는 회갈색이었어요. 연한 색 위의 Hue 노브는 채도가 낮아 반응이 작을 것 같았는데, Satin Fold 는 주름 골의 채도 0.46 이 그 몫을 해서 K1 이 0.11 이에요. 주황과 노랑 쪽 색조는 이번에도 중간 밝기에서 갈색과 올리브로 갔어요. Pearl Sheen 은 색이 노랑에 가까운 만큼 밝기를 올리고 채도를 내려서 샴페인을 밝은 베이지로 남겼어요. + ## 다시 돌리기 `web/` 에서 실행해요. `web/node_modules` 가 있어야 해요. From 899e0a1df0e62d7b17a89ae75d3f74eceb11b624 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 12:20:59 +0000 Subject: [PATCH 14/15] =?UTF-8?q?index.html=20=EC=9D=84=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EC=A7=93=EA=B3=A0=20=EC=83=88=EB=A1=9C=20=EB=82=9C?= =?UTF-8?q?=20=EB=B9=88=20=EC=B9=B8=EC=9D=84=20=EC=A0=81=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_01QuEe7qhvCVuDuGNB5fZ113 --- incoming/pattern-study/README.md | 8 +++++--- incoming/pattern-study/index.html | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 768e7e6ba..6cea9d711 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1271,7 +1271,7 @@ E8 의 근 240개를 고셋의 4₂₁ 다면체 꼭짓점으로 그렸어요. ## 30판 -30판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 뒤에 돈 판이에요. 같이 돈 29판(#219)이 j 불변량과 호프 올뭉치, 크림과 젤 같은 것을 먼저 골라서, 이 판은 그와 겹치지 않게 골랐어요. 주제 5 의 여섯은 세 간격 정리, 호프스태터 나비, 외부 당구, 탄도 퇴적, 미앤더, 스미기예요. 「낯선 수학 예」에서는 세 간격 정리와 외부 당구 둘을 골랐어요. 여섯 다 있는 갈래에 들어가서 주제 2 로 연 갈래는 없어요. 주제 6 의 넷은 손가락 스와치와 펄 안료, 이중층 클렌저, 새틴이고 화장품 팝업 갈래(`cosmetic`)에 모았어요. ROUND.md 첫머리는 열 개의 갈래가 모두 다르다고 하고 주제 6 은 넷을 한 갈래에 모으라고 해서, 수학 여섯만 서로 다른 갈래에 두었어요. 주제 1 은 기준 188개의 21장이 이번에도 다 `none` 이라 28판처럼 패턴이 하나뿐인 칸 둘을 겨눴어요. Percolation 이 Waggle Dance 혼자이던 복잡도 × 채움 4,3 에, Meander 가 Bauhaus 혼자이던 채움 × 색 2,4 에 섰어요. 주제 4 는 Three Gaps 와 Hofstadter Butterfly 예요. 둘 다 수학이 세로축을 정해서, 한쪽은 줄 번호가 점의 수이고 다른 쪽은 줄의 높이가 자속이에요. Ballistic Deposition, Meander, Percolation 도 3분의 1마다 24% 를 넘게 켜요. +30판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 뒤에 돈 판이에요. 같이 돈 29판(#219)이 j 불변량과 호프 올뭉치, 크림과 젤 같은 것을 먼저 골라서, 이 판은 그와 겹치지 않게 골랐어요. 주제 5 의 여섯은 세 간격 정리, 호프스태터 나비, 외부 당구, 탄도 퇴적, 미앤더, 스미기예요. 「낯선 수학 예」에서는 세 간격 정리와 외부 당구 둘을 골랐어요. 여섯 다 있는 갈래에 들어가서 주제 2 로 연 갈래는 없어요. 주제 6 의 넷은 손가락 스와치와 펄 안료, 이중층 클렌저, 새틴이고 화장품 팝업 갈래(`cosmetic`)에 모았어요. ROUND.md 첫머리는 열 개의 갈래가 모두 다르다고 하고 주제 6 은 넷을 한 갈래에 모으라고 해서, 수학 여섯만 서로 다른 갈래에 두었어요. 주제 1 은 기준 188개의 21장이 이번에도 다 `none` 이라 28판처럼 패턴이 하나뿐인 칸 둘을 겨눴어요. Percolation 이 Waggle Dance 혼자이던 복잡도 × 채움 4,3 에, Meander 가 Bauhaus 혼자이던 채움 × 색 2,4 에 섰어요. 패턴이 하나뿐인 칸은 30 에서 26 이 됐어요. 그 대신 화장품 넷이 가장자리 축의 아래 끝을 0.120 에서 0.000 으로 늘려서 `*` 가 붙은 빈 칸 다섯이 새로 났어요. 「다음 판 감」의 지도의 빈 칸에 적었어요. 색 축의 위 끝은 Hyperbolic Band 의 0.642 에서 Outer Billiards 의 0.675 로 올라갔어요. 주제 4 는 Three Gaps 와 Hofstadter Butterfly 예요. 둘 다 수학이 세로축을 정해서, 한쪽은 줄 번호가 점의 수이고 다른 쪽은 줄의 높이가 자속이에요. Ballistic Deposition, Meander, Percolation 도 3분의 1마다 24% 를 넘게 켜요. ### Three Gaps @@ -1303,7 +1303,7 @@ E8 의 근 240개를 고셋의 4₂₁ 다면체 꼭짓점으로 그렸어요. ### Pearl Sheen -주제 6 으로 펄 안료의 광택을 흘렸어요. 티타니아를 입힌 마이카 판이 박막 간섭으로 색을 내서, 광로차 0~1600 nm 마다 400~700 nm 를 5 nm 간격으로 더한 1024칸 표를 미리 만들어요. 평평한 판은 2차 자홍(980 nm)에 앉고, 결을 따라 기운 판은 빛이 비스듬히 지나 광로가 짧아져서 주황과 금색으로 넘어가요. 그것이 듀오크롬의 둘째 색이에요. K1 은 그 표를 회색축 둘레로 돌려서 0 에서 로즈와 복숭아빛, 0.5 에서 아쿠아와 페리윙클이 돼요. 결은 흐름 방향으로 12배 길게 늘인 노이즈이고, 긴 스트립 조명의 반사띠가 볼록한 면 위로 21초에 한 번 지나가요. 굴절률로 각도 이동을 그대로 건 판은 1 nm 에 색이 1° 쯤 돌아 Shift 0 에서도 산호색이 번졌고 1 에서는 무지개였어요. 기울기 문턱 18°~45° 와 최대 이동 비율로 바꾸자 두 색이 갈렸어요. 하이라이트를 0.85 위에서 누른 판은 반사띠가 사라져 움직임이 0.028 이었고, 바탕을 0.64 로 내리고 띠를 세게 하자 0.051 로 돌아왔어요. 가장자리 0.000 은 이번 열 개 가운데 가장 번진 값이에요. +주제 6 으로 펄 안료의 광택을 흘렸어요. 티타니아를 입힌 마이카 판이 박막 간섭으로 색을 내서, 광로차 0~1600 nm 마다 400~700 nm 를 5 nm 간격으로 더한 1024칸 표를 미리 만들어요. 평평한 판은 2차 자홍(980 nm)에 앉고, 결을 따라 기운 판은 빛이 비스듬히 지나 광로가 짧아져서 주황과 금색으로 넘어가요. 그것이 듀오크롬의 둘째 색이에요. K1 은 그 표를 회색축 둘레로 돌려서 0 에서 로즈와 복숭아빛, 0.5 에서 아쿠아와 페리윙클이 돼요. 결은 흐름 방향으로 12배 길게 늘인 노이즈이고, 긴 스트립 조명의 반사띠가 볼록한 면 위로 21초에 한 번 지나가요. 굴절률로 각도 이동을 그대로 건 판은 1 nm 에 색이 1° 쯤 돌아 Shift 0 에서도 산호색이 번졌고 1 에서는 무지개였어요. 기울기 문턱 18°~45° 와 최대 이동 비율로 바꾸자 두 색이 갈렸어요. 하이라이트를 0.85 위에서 누른 판은 반사띠가 사라져 움직임이 0.028 이었고, 바탕을 0.64 로 내리고 띠를 세게 하자 0.051 로 돌아왔어요. 가장자리 0.000 은 198개 가운데 가장 번진 값이라 가장자리 축의 새 아래 끝이에요. ### Bi-Phase @@ -1361,7 +1361,9 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 `study.ts --check --gaps` 가 `*` 를 붙인 칸 가운데 그림이 떠오르는 칸이에요. 칸은 가로와 세로를 낮은 쪽부터 1~4 로 세요. 페이지 기본 지도(가장자리 × 움직임)는 16칸이 다 차 있어요. -23판 뒤로는 `*` 가 붙은 칸이 없어요. 22판까지 남은 두 칸, 촘촘함 × 모양 바뀜 3,4 와 4,3 에 23판의 Bait Ball 과 Sequins 가 섰어요. 하나 남은 빈 칸이던 복잡도 × 채움 4,3 에는 24판의 Waggle Dance 가 섰어요. 복잡도가 2.173 을 넘고 채움이 0.558~0.815 인 자리예요. 복잡도 축 끝의 Contour Map(2.735)을 빼고 세면 차는 칸이라 `*` 가 붙지 않았고, 그 아래 복잡도 2.4~2.6 에 선 Woodpecker, Dripstone, Agate, Guilloché 는 채움이 다 0.93 을 넘었어요. 이제 지도 21장의 칸이 다 차서 `--gaps` 는 모든 줄에 `none` 을 찍어요. 주제 1 은 축 끝의 패턴 하나가 축을 늘려 새 빈 칸이 날 때까지 고를 칸이 없어요. Waggle Dance 에서 어려운 쪽은 채움이었어요. 벌집은 꺼진 LED 가 거의 없는 그림이라, 빈 방 바닥과 봉한 방 사이 이음매를 24 아래로 눌러 채움 0.732 를 냈어요. 복잡도는 켜진 LED 의 세 채널을 따로 흔드는 낟알로 쉽게 올라서, 첫 판은 오히려 2.766 으로 축 끝을 넘었어요. 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칸이에요. 남은 칸 가운데 수학으로 겨눠 볼 만한 것은 가장자리 × 움직임 1,4(Mandelbrot 혼자)예요. 번지면서 바쁜 자리라, 모듈러 형식의 영역 색칠에서 |f| 의 등고선이 빠르게 흘러가면 들어설 수 있어요. 색상만 도는 것은 움직임이 거의 없어요. 움직임은 가장 밝은 채널의 바뀜만 세요. +30판 뒤로는 `*` 가 붙은 칸이 다섯이에요. 30판의 화장품 질감 넷이 가장자리 축의 아래 끝을 Fire 의 0.120 에서 Pearl Sheen 의 0.000 으로 끌어내려서, 가장자리 칸의 경계가 0.305, 0.560, 0.815 에서 0.210, 0.500, 0.790 으로 옮겨졌어요. Bi-Phase 도 0.009 라서 축 끝의 하나를 빼고 세도 칸이 비어 `*` 가 붙어요. 다섯 다 가장자리가 0.210 아래인 열이에요. 가장자리 × 움직임 1,4 는 움직임이 1.49 를 넘는 번진 그림의 자리이고, 그 칸에 혼자 서던 Mandelbrot 은 0.221 이라 경계 바로 오른쪽으로 넘어갔어요. 가장자리 × 촘촘함 1,2 와 1,3, 1,4 는 이웃 LED 사이 차이가 평균 0.082 를 넘으면서도 그 차이가 거의 다 40/255 아래인 그림이에요. 잘지만 계단이 없는 결이에요. 가장자리 × 채움 1,1 은 채움이 0.302 아래인 번진 그림이에요. 화장품 넷은 바탕을 다 켜서 채움이 1.000 이라 이 칸들에 서지 않아요. + +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칸이에요. 남은 칸 가운데 수학으로 겨눠 볼 만한 것은 가장자리 × 움직임 1,4(Mandelbrot 혼자)예요. 번지면서 바쁜 자리라, 모듈러 형식의 영역 색칠에서 |f| 의 등고선이 빠르게 흘러가면 들어설 수 있어요. 색상만 도는 것은 움직임이 거의 없어요. 움직임은 가장 밝은 채널의 바뀜만 세요. 촘촘함이 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 를 냈어요. diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index 175ee5e46..ddf84673e 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -141,9 +141,9 @@

패턴 연구

- + From f039685d785a100f9d124fd7bd71dbd74840c345 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 12:59:52 +0000 Subject: [PATCH 15/15] =?UTF-8?q?29=ED=8C=90=EC=9D=84=20=ED=95=A9=EC=B9=9C?= =?UTF-8?q?=20208=EA=B0=9C=EB=A1=9C=20index.html=20=EC=9D=84=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EC=A7=93=EA=B3=A0=20=EB=B9=88=20=EC=B9=B8=20?= =?UTF-8?q?=EC=88=98=EB=A5=BC=20=EA=B3=A0=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QuEe7qhvCVuDuGNB5fZ113 --- incoming/pattern-study/README.md | 6 +++--- incoming/pattern-study/index.html | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 272a0acf4..a0dc7be44 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1330,7 +1330,7 @@ E8 의 근 240개를 고셋의 4₂₁ 다면체 꼭짓점으로 그렸어요. ## 30판 -30판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 뒤에 돈 판이에요. 같이 돈 29판(#219)이 j 불변량과 호프 올뭉치, 크림과 젤 같은 것을 먼저 골라서, 이 판은 그와 겹치지 않게 골랐어요. 주제 5 의 여섯은 세 간격 정리, 호프스태터 나비, 외부 당구, 탄도 퇴적, 미앤더, 스미기예요. 「낯선 수학 예」에서는 세 간격 정리와 외부 당구 둘을 골랐어요. 여섯 다 있는 갈래에 들어가서 주제 2 로 연 갈래는 없어요. 주제 6 의 넷은 손가락 스와치와 펄 안료, 이중층 클렌저, 새틴이고 화장품 팝업 갈래(`cosmetic`)에 모았어요. ROUND.md 첫머리는 열 개의 갈래가 모두 다르다고 하고 주제 6 은 넷을 한 갈래에 모으라고 해서, 수학 여섯만 서로 다른 갈래에 두었어요. 주제 1 은 기준 188개의 21장이 이번에도 다 `none` 이라 28판처럼 패턴이 하나뿐인 칸 둘을 겨눴어요. Percolation 이 Waggle Dance 혼자이던 복잡도 × 채움 4,3 에, Meander 가 Bauhaus 혼자이던 채움 × 색 2,4 에 섰어요. 패턴이 하나뿐인 칸은 30 에서 26 이 됐어요. 그 대신 화장품 넷이 가장자리 축의 아래 끝을 0.120 에서 0.000 으로 늘려서 `*` 가 붙은 빈 칸 다섯이 새로 났어요. 「다음 판 감」의 지도의 빈 칸에 적었어요. 색 축의 위 끝은 Hyperbolic Band 의 0.642 에서 Outer Billiards 의 0.675 로 올라갔어요. 주제 4 는 Three Gaps 와 Hofstadter Butterfly 예요. 둘 다 수학이 세로축을 정해서, 한쪽은 줄 번호가 점의 수이고 다른 쪽은 줄의 높이가 자속이에요. Ballistic Deposition, Meander, Percolation 도 3분의 1마다 24% 를 넘게 켜요. +30판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 뒤에 돈 판이에요. 같이 돈 29판(#219)이 j 불변량과 호프 올뭉치, 크림과 젤 같은 것을 먼저 골라서, 이 판은 그와 겹치지 않게 골랐어요. 주제 5 의 여섯은 세 간격 정리, 호프스태터 나비, 외부 당구, 탄도 퇴적, 미앤더, 스미기예요. 「낯선 수학 예」에서는 세 간격 정리와 외부 당구 둘을 골랐어요. 여섯 다 있는 갈래에 들어가서 주제 2 로 연 갈래는 없어요. 주제 6 의 넷은 손가락 스와치와 펄 안료, 이중층 클렌저, 새틴이고 화장품 팝업 갈래(`cosmetic`)에 모았어요. ROUND.md 첫머리는 열 개의 갈래가 모두 다르다고 하고 주제 6 은 넷을 한 갈래에 모으라고 해서, 수학 여섯만 서로 다른 갈래에 두었어요. 주제 1 은 기준 188개의 21장이 이번에도 다 `none` 이라 28판처럼 패턴이 하나뿐인 칸 둘을 겨눴어요. Percolation 이 Waggle Dance 혼자이던 복잡도 × 채움 4,3 에, Meander 가 Bauhaus 혼자이던 채움 × 색 2,4 에 섰어요. 패턴이 하나뿐인 칸은 29판까지 더한 198개에서 24 였고, 30판을 더한 208개에서도 24 예요. 겨눈 두 칸과 Sequins, Guilloché 가 혼자이던 칸 둘에 둘째가 섰지만, 축의 끝이 옮겨져 Hopf Fibration, Bokeh, Penrose, Neon Alley 가 혼자 선 칸이 새로 났어요. 색 축의 위 끝은 Hyperbolic Band 의 0.642 에서 Outer Billiards 의 0.675 로 올라갔고, 가장자리 축의 아래 끝은 29판의 0.028 에서 Pearl Sheen 의 0.000 으로 내려갔어요. 그래서 `*` 가 붙은 빈 칸이 넷이에요. 「다음 판 감」의 지도의 빈 칸에 적었어요. 주제 4 는 Three Gaps 와 Hofstadter Butterfly 예요. 둘 다 수학이 세로축을 정해서, 한쪽은 줄 번호가 점의 수이고 다른 쪽은 줄의 높이가 자속이에요. Ballistic Deposition, Meander, Percolation 도 3분의 1마다 24% 를 넘게 켜요. ### Three Gaps @@ -1362,7 +1362,7 @@ E8 의 근 240개를 고셋의 4₂₁ 다면체 꼭짓점으로 그렸어요. ### Pearl Sheen -주제 6 으로 펄 안료의 광택을 흘렸어요. 티타니아를 입힌 마이카 판이 박막 간섭으로 색을 내서, 광로차 0~1600 nm 마다 400~700 nm 를 5 nm 간격으로 더한 1024칸 표를 미리 만들어요. 평평한 판은 2차 자홍(980 nm)에 앉고, 결을 따라 기운 판은 빛이 비스듬히 지나 광로가 짧아져서 주황과 금색으로 넘어가요. 그것이 듀오크롬의 둘째 색이에요. K1 은 그 표를 회색축 둘레로 돌려서 0 에서 로즈와 복숭아빛, 0.5 에서 아쿠아와 페리윙클이 돼요. 결은 흐름 방향으로 12배 길게 늘인 노이즈이고, 긴 스트립 조명의 반사띠가 볼록한 면 위로 21초에 한 번 지나가요. 굴절률로 각도 이동을 그대로 건 판은 1 nm 에 색이 1° 쯤 돌아 Shift 0 에서도 산호색이 번졌고 1 에서는 무지개였어요. 기울기 문턱 18°~45° 와 최대 이동 비율로 바꾸자 두 색이 갈렸어요. 하이라이트를 0.85 위에서 누른 판은 반사띠가 사라져 움직임이 0.028 이었고, 바탕을 0.64 로 내리고 띠를 세게 하자 0.051 로 돌아왔어요. 가장자리 0.000 은 198개 가운데 가장 번진 값이라 가장자리 축의 새 아래 끝이에요. +주제 6 으로 펄 안료의 광택을 흘렸어요. 티타니아를 입힌 마이카 판이 박막 간섭으로 색을 내서, 광로차 0~1600 nm 마다 400~700 nm 를 5 nm 간격으로 더한 1024칸 표를 미리 만들어요. 평평한 판은 2차 자홍(980 nm)에 앉고, 결을 따라 기운 판은 빛이 비스듬히 지나 광로가 짧아져서 주황과 금색으로 넘어가요. 그것이 듀오크롬의 둘째 색이에요. K1 은 그 표를 회색축 둘레로 돌려서 0 에서 로즈와 복숭아빛, 0.5 에서 아쿠아와 페리윙클이 돼요. 결은 흐름 방향으로 12배 길게 늘인 노이즈이고, 긴 스트립 조명의 반사띠가 볼록한 면 위로 21초에 한 번 지나가요. 굴절률로 각도 이동을 그대로 건 판은 1 nm 에 색이 1° 쯤 돌아 Shift 0 에서도 산호색이 번졌고 1 에서는 무지개였어요. 기울기 문턱 18°~45° 와 최대 이동 비율로 바꾸자 두 색이 갈렸어요. 하이라이트를 0.85 위에서 누른 판은 반사띠가 사라져 움직임이 0.028 이었고, 바탕을 0.64 로 내리고 띠를 세게 하자 0.051 로 돌아왔어요. 가장자리 0.000 은 208개 가운데 가장 번진 값이라 가장자리 축의 새 아래 끝이에요. ### Bi-Phase @@ -1420,7 +1420,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 `study.ts --check --gaps` 가 `*` 를 붙인 칸 가운데 그림이 떠오르는 칸이에요. 칸은 가로와 세로를 낮은 쪽부터 1~4 로 세요. 페이지 기본 지도(가장자리 × 움직임)는 16칸이 다 차 있어요. -30판 뒤로는 `*` 가 붙은 칸이 다섯이에요. 30판의 화장품 질감 넷이 가장자리 축의 아래 끝을 Fire 의 0.120 에서 Pearl Sheen 의 0.000 으로 끌어내려서, 가장자리 칸의 경계가 0.305, 0.560, 0.815 에서 0.210, 0.500, 0.790 으로 옮겨졌어요. Bi-Phase 도 0.009 라서 축 끝의 하나를 빼고 세도 칸이 비어 `*` 가 붙어요. 다섯 다 가장자리가 0.210 아래인 열이에요. 가장자리 × 움직임 1,4 는 움직임이 1.49 를 넘는 번진 그림의 자리이고, 그 칸에 혼자 서던 Mandelbrot 은 0.221 이라 경계 바로 오른쪽으로 넘어갔어요. 가장자리 × 촘촘함 1,2 와 1,3, 1,4 는 이웃 LED 사이 차이가 평균 0.082 를 넘으면서도 그 차이가 거의 다 40/255 아래인 그림이에요. 잘지만 계단이 없는 결이에요. 가장자리 × 채움 1,1 은 채움이 0.302 아래인 번진 그림이에요. 화장품 넷은 바탕을 다 켜서 채움이 1.000 이라 이 칸들에 서지 않아요. +30판 뒤로는 `*` 가 붙은 칸이 넷이에요. 29판의 Cream Swirl 이 0.028 로 내린 가장자리 축의 아래 끝을 30판의 Pearl Sheen 이 0.000 으로 더 내려서, 가장자리 칸의 경계가 0.210, 0.500, 0.790 으로 옮겨졌어요. Bi-Phase 0.009 와 Cream Swirl 0.028 도 그 끝에 붙어 있어서, 축 끝의 하나를 빼고 세도 칸이 비어 넷 다 `*` 가 붙어요. 넷 다 가장자리가 0.210 아래인 열이에요. 가장자리 × 촘촘함 1,2 와 1,3, 1,4 는 이웃 LED 사이 차이가 평균 0.082 를 넘으면서도 그 차이가 거의 다 40/255 아래인 그림이에요. 잘지만 계단이 없는 결이에요. 가장자리 × 채움 1,1 은 채움이 0.302 아래인 번진 그림이에요. 가장자리가 0.08 아래인 화장품 여섯은 바탕을 다 켜서 채움이 1.000 이라 이 칸들에 서지 않아요. 가장자리 × 움직임 1,4 는 경계가 0.210 으로 내려와도 29판의 Hopf Fibration(가장자리 0.207)이 그 안에 서 있어요. Mandelbrot 은 0.221 이라 경계 바로 오른쪽으로 넘어갔어요. 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칸이에요. diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index fa53e7b6f..02917bc58 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -141,7 +141,7 @@

패턴 연구

- +