From 0da69939f11e21af61826a6baa598e8c14f77930 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:03:06 +0000 Subject: [PATCH 01/26] =?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=2066=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_01Gdqi6PeQBKagUB2vLqwQZR From 1f8b622edfcbc2710d2c577bfd60a86eff2b93c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:40:30 +0000 Subject: [PATCH 02/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Golomb=20Ruler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pattern-study/patterns/golomb-ruler.js | 267 ++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 incoming/pattern-study/patterns/golomb-ruler.js diff --git a/incoming/pattern-study/patterns/golomb-ruler.js b/incoming/pattern-study/patterns/golomb-ruler.js new file mode 100644 index 000000000..ae5a894f7 --- /dev/null +++ b/incoming/pattern-study/patterns/golomb-ruler.js @@ -0,0 +1,267 @@ +// ===== Patternflow pattern ===== +// Title: Golomb Ruler +// Family: number +// Idea: A ruler runs down the panel with a mark on some of its 128 rows, every pair of marks is joined by an arc as tall as the distance it measures, and a caliper sweeps the lengths 1 to 127, lighting the arcs of each length and ticking a length scale on the right edge, so the optimal 14-mark Golomb ruler lights one arc or none at each length and Wichmann's ruler lights at least one, with the sevens piling up. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Sidon sets (Simon Sidon, 1932) and Golomb rulers (W. C. Babcock's radio channels, 1953; named for Solomon W. Golomb), sparse rulers (Brian Wichmann, 1963), the greedy Sidon sequence (Abdul Majid Mian and Sarvadaman Chowla, 1944), pattern round 66 +// Round: 66 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Ruler=0..3, Shape=-1..1 +// +// Knob 1: Hue. Turns the length colours round the wheel: at 0 the short arcs are sea green and the long ones run through blue and violet to rose; at 0.5 they run from rose through yellow to green. +// Knob 2: Speed. How fast the caliper measures: 16 lengths a second at 2, so a sweep of 1 to 127 takes 8 s, then the whole picture holds 1.5 s and fades back to its ghost in 1 s. At 0.2 a sweep takes 80 s, at 3 it takes 5.3 s. +// Knob 3: Ruler. Which marks: 0 the optimal Golomb ruler, 14 marks on 0..127 whose 91 arcs measure every length from 1 to 55 once and 36 longer ones never; 1 Mian and Chowla's greedy Sidon set, 12 marks on 0..122 whose 66 arcs already miss 22; 2 fourteen random marks with both ends, drawn anew each sweep, which on average measure 66 lengths and repeat 20 of them; 3 Wichmann's ruler W(1,16), 23 marks whose 253 arcs measure every length, 7 sixteen times, 14 fifteen times and so on down the multiples of 7. +// Knob 4: Shape. The outline of every arc, a half superellipse: straight chevrons at -1, where the arcs from one mark run along one ray and the picture turns into a lattice of crossing rays; half ellipses near the opening 0.06; square brackets like the dimension lines of a drawing at 1. + +const W = 64, H = 128, L = 127; +const { floor, ceil, min, max, abs, sqrt, sin, cos, exp, pow, round, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v, 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); + o[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + o[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + o[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; + return o; +} + +// The optimal Golomb ruler with 14 marks: its 91 differences are all +// different and 127 is the shortest length that holds 14 such marks. +const GOLOMB = [0, 4, 6, 20, 35, 52, 59, 77, 78, 86, 89, 99, 122, 127]; + +// Mian and Chowla's greedy Sidon sequence 1, 2, 4, 8, 13, ...: each term the +// least number that keeps all pairwise sums (so all differences) distinct. +// Shifted to start at 0, twelve terms fit, up to 122. +function mianChowla() { + const T = [1], sums = new Set([2]); + for (let k = 2; k - 1 <= L; k++) { + let ok = !sums.has(2 * k); + for (const a of T) if (sums.has(k + a)) ok = false; + if (!ok) continue; + for (const a of T) sums.add(k + a); + sums.add(2 * k); + T.push(k); + } + return T.map((a) => a - 1); +} + +// Wichmann's ruler W(r, s): steps 1 (r times), r+1, 2r+1 (r times), +// 4r+3 (s times), 2r+2 (r+1 times), 1 (r times). Length +// 4r(r+s+2)+3(s+1) with 4r+s+3 marks; W(1,16) is 127 long with 23. +function wichmann(r, s) { + const steps = []; + const put = (v, n) => { for (let i = 0; i < n; i++) steps.push(v); }; + put(1, r), put(r + 1, 1), put(2 * r + 1, r), put(4 * r + 3, s), put(2 * r + 2, r + 1), put(1, r); + const m = [0]; + for (const v of steps) m.push(m[m.length - 1] + v); + return m; +} + +// Fourteen random marks on 0..127, the ends always among them. +function randomRuler() { + const set = new Set([0, L]); + while (set.size < 14) set.add(1 + floor(Math.random() * (L - 1))); + return [...set].sort((a, b) => a - b); +} + +const RULERS = [() => GOLOMB, mianChowla, randomRuler, () => wichmann(1, 16)]; + +// Geometry. The spine stands at x = X0; an arc for the marks a < b leaves +// the spine at rows a and b and bulges right to X0 + A, A in proportion to +// b - a, so the 127-row arc reaches column X0 + AMAX. The length scale is +// the right edge: row d is the length d. +const X0 = 1.5, AMAX = 52.5, SX = 63; + +// Superellipse exponent from the Shape knob: 1 chevron, 2 ellipse, 8 bracket. +const expo = (k) => (k < 0 ? pow(2, 1 + k) : pow(2, 1 + 2 * k)); + +// One arc's light as a list of pixels and weights: the half superellipse +// sampled along its angle, each piece of the polyline cut into steps of at +// most 0.4 LED and splatted bilinearly with its length as weight. Each arc +// owns a slot of cap entries in the shared arrays. +const TMP = new Float32Array(W * H), TOUCH = new Int32Array(W * H); +const cap = (d) => 104 + 12 * d; +function arcPixels(S, i, p) { + const [a, b, d] = S.pairs[i], A = (AMAX * d) / L, B = d / 2, c = (a + b) / 2, e = 2 / p; + const n = 24 + 2 * ceil(A + B), at = S.off[i], room = cap(d), I = S.I, Wt = S.Wt; + let nt = 0, px = X0, py = a; + const splat = (x, y, w) => { + const i = floor(x), j = floor(y), fx = x - i, fy = y - j; + for (let q = 0; q < 4; q++) { + const ii = i + (q & 1), jj = j + (q >> 1); + if (ii < 0 || ii >= W || jj < 0 || jj >= H) continue; + const ww = w * (q & 1 ? fx : 1 - fx) * (q >> 1 ? fy : 1 - fy); + if (ww <= 0) continue; + const k = jj * W + ii; + if (TMP[k] === 0) { + if (nt >= room) continue; + TOUCH[nt++] = k; + } + TMP[k] += ww; + } + }; + for (let s = 1; s <= n; s++) { + const th = -PI / 2 + (PI * s) / n, cs = max(0, cos(th)), sn = sin(th); + const x = X0 + A * pow(cs, e), y = c + B * (sn < 0 ? -pow(-sn, e) : pow(sn, e)); + const len = sqrt((x - px) * (x - px) + (y - py) * (y - py)), k = max(1, ceil(len / 0.4)); + for (let u = 0; u < k; u++) { + const f = (u + 0.5) / k; + splat(px + (x - px) * f, py + (y - py) * f, len / k); + } + px = x, py = y; + } + for (let q = 0; q < nt; q++) { + const k = TOUCH[q]; + I[at + q] = k, Wt[at + q] = min(1.4, TMP[k]); + TMP[k] = 0; + } + S.np[i] = nt; +} + +// Marks, pairs (shortest first) and how many pairs measure each length. The +// arcs are drawn over the next few frames, shortest first. +function useRuler(S, which, p) { + S.which = which; + const m = RULERS[which](); + const P = []; + for (let i = 0; i < m.length; i++) for (let j = i + 1; j < m.length; j++) P.push([m[i], m[j], m[j] - m[i]]); + P.sort((u, v) => u[2] - v[2]); + const cnt = new Int32Array(L + 1); + for (const q of P) cnt[q[2]]++; + S.marks = m, S.pairs = P, S.cnt = cnt; + // Denser rulers draw each arc fainter so 253 arcs do not burn to white. + S.gain = min(1, pow(91 / P.length, 0.75)); + S.tm = new Float64Array(P.length).fill(-1); + const off = new Int32Array(P.length + 1); + for (let i = 0; i < P.length; i++) off[i + 1] = off[i] + cap(P[i][2]); + S.off = off, S.np = new Int32Array(P.length); + S.I = new Int32Array(off[P.length]), S.Wt = new Float32Array(off[P.length]); + S.geoP = p, S.redo = 0; +} + +function restart(S) { + S.d = 0, S.phase = 0, S.pt = 0; + S.tm.fill(-1); + S.next = 0; +} + +export function setup(params) { + const S = (params.s = { + t: 0, hue: 0, speed: 2, which: -1, shape: 0.06, d: 0, phase: 0, pt: 0, next: 0, + acc: new Float32Array(W * H * 3), mf: new Float32Array(L + 1), c: [0, 0, 0], + }); + useRuler(S, 0, expo(0.06)); + restart(S); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.t += d; + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.shape = cl(k[3], -1, 1); + const which = cl(round(k[2]), 0, 3), p = expo(S.shape); + if (which !== S.which) useRuler(S, which, p), restart(S); + // When the shape has moved by a visible amount the arcs are drawn again, + // shortest first and a share each frame, so the new outline spreads out + // from the spine over a few frames instead of costing one long frame. A + // pass runs to the end before the next one starts. + if (S.redo >= S.pairs.length && abs(p - S.geoP) / p > 0.02) (S.geoP = p), (S.redo = 0); + for (let budget = 1800; S.redo < S.pairs.length && budget > 0; S.redo++) { + arcPixels(S, S.redo, p); + budget -= 20 + S.pairs[S.redo][2]; + } + // Sweep: the caliper opens from 0 to 127, then the picture holds and fades. + if (S.phase === 0) { + S.d += 8 * S.speed * d; + const P = S.pairs; + while (S.next < P.length && P[S.next][2] <= S.d) S.tm[S.next++] = S.t; + if (S.d >= L + 1) (S.phase = 1), (S.pt = 0); + } else { + S.pt += d * (0.5 + S.speed / 4); + if (S.pt > 2.5) { + if (S.which === 2) useRuler(S, 2, S.geoP); + restart(S); + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.I) return; + const acc = S.acc, c = S.c, P = S.pairs, I = S.I, Wt = S.Wt, off = S.off, np = S.np, t = S.t; + acc.fill(0); + const h0 = 0.44 + S.hue; + // Measured arcs hold their colour; after the hold they fade to the ghost. + const fade = S.phase === 1 ? cl((S.pt - 1.5) / 1, 0, 1) : 0; + const ghost = 0.16, mf = S.mf; + mf.fill(0); + for (let i = 0; i < P.length; i++) { + const dd = P[i][2], tm = S.tm[i]; + let e = ghost, flash = 0; + if (tm >= 0) { + e = 1 - (1 - ghost) * fade; + flash = exp(-(t - tm) / 0.2); + mf[P[i][0]] = max(mf[P[i][0]], flash), mf[P[i][1]] = max(mf[P[i][1]], flash); + } + hsv(h0 + 0.46 * (dd / L), 0.8 - 0.6 * flash, 1, c); + const gn = S.gain * e, r = c[0] * gn + flash, g = c[1] * gn + flash, b = c[2] * gn + flash; + for (let q = off[i], qe = off[i] + np[i]; q < qe; q++) { + const k = I[q] * 3, w = Wt[q]; + acc[k] += r * w, acc[k + 1] += g * w, acc[k + 2] += b * w; + } + } + // The caliper's jaw: a faint vertical line where arcs of the current + // length touch it with their tips. + if (S.phase === 0) { + const xj = X0 + (AMAX * S.d) / L, i = floor(xj), f = xj - i; + for (let y = 0; y < H; y++) { + if (i >= 0 && i < W) acc[(y * W + i) * 3 + 2] += 0.12 * (1 - f), acc[(y * W + i) * 3] += 0.06 * (1 - f), acc[(y * W + i) * 3 + 1] += 0.06 * (1 - f); + if (i + 1 < W) acc[(y * W + i + 1) * 3 + 2] += 0.12 * f, acc[(y * W + i + 1) * 3] += 0.06 * f, acc[(y * W + i + 1) * 3 + 1] += 0.06 * f; + } + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const k = (y * W + x) * 3; + display.setPixel(x, y, (255 * (1 - exp(-1.5 * acc[k]))) | 0, (255 * (1 - exp(-1.5 * acc[k + 1]))) | 0, (255 * (1 - exp(-1.5 * acc[k + 2]))) | 0); + } + // The spine and its marks. + for (let y = 0; y < H; y++) display.setPixel(0, y, 40, 40, 48); + // A mark burns white while an arc from it is being measured. + for (const m of S.marks) { + const f = mf[m], v = 170 + 85 * f; + display.setPixel(0, m, 255, 250, 240); + display.setPixel(1, m, v, v - 5, v - 15); + if (f > 0.05) display.setPixel(2, m, 255 * f, 255 * f, 255 * f); + } + // The length scale: row d ticks once measured, 2 + n LEDs long when n + // pairs share the length, white for a moment as the caliper passes; a + // length nobody measures blinks dark red and stays a gap. Rows the + // caliper has not reached keep a faint dot. + const cur = S.phase === 0 ? floor(S.d) : L + 1, rate = 8 * S.speed; + for (let dd = 1; dd <= L; dd++) { + const n = S.cnt[dd], since = S.phase === 0 ? (S.d - dd) / rate : 9; + if (dd > cur) { + display.setPixel(SX, dd, 34, 34, 40); + continue; + } + if (n === 0) { + const b = exp(-since / 0.5); + if (b > 0.1) display.setPixel(SX, dd, 200 * b, 30 * b, 40 * b); + continue; + } + const fl = exp(-since / 0.25); + hsv(h0 + 0.46 * (dd / L), 0.7 * (1 - fl), 1 - 0.6 * fade, c); + const len = 2 + n; + for (let x = SX; x > SX - len; x--) display.setPixel(x, dd, (c[0] * 255) | 0, (c[1] * 255) | 0, (c[2] * 255) | 0); + } + if (S.phase === 0) { + display.setPixel(SX, cur, 255, 255, 255); + display.setPixel(SX - 1, cur, 255, 255, 255); + } +} From 1d47d61ebb8b6eac12bc3b69f0d6d3c72834fb12 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:41:09 +0000 Subject: [PATCH 03/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Hodograph?= 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_01Gdqi6PeQBKagUB2vLqwQZR --- incoming/pattern-study/patterns/hodograph.js | 331 +++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 incoming/pattern-study/patterns/hodograph.js diff --git a/incoming/pattern-study/patterns/hodograph.js b/incoming/pattern-study/patterns/hodograph.js new file mode 100644 index 000000000..bf2117e93 --- /dev/null +++ b/incoming/pattern-study/patterns/hodograph.js @@ -0,0 +1,331 @@ +// ===== Patternflow pattern ===== +// Title: Hodograph +// Family: physics +// Idea: Bodies of different eccentricity circle one Sun in the upper half with Kepler's timing, each sweeping a fan of equal-time slices, and in the lower half each body's velocity, drawn from one common origin, runs round a circle set off from the origin by e times its radius. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: hodograph of motion under an inverse-square force (William Rowan Hamilton, 1846), Kepler's equation and the law of equal areas (Johannes Kepler, 1609), Feynman's lost lecture (Richard Feynman, 1964; David and Judith Goodstein, 1996), pattern round 66 +// Round: 66 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Eccentricity=0.6..0.95, Bodies=3..6 +// +// Knob 1: Hue. Turns the bodies' colours round the wheel together. +// Knob 2: Speed. How fast time runs. +// Knob 3: Eccentricity. The largest eccentricity. +// Knob 4: Bodies. How many bodies. + +const W = 64, H = 128; +const { floor, min, max, abs, sqrt, sin, cos, atan2, PI, round, hypot } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const wrap = (v) => v - TAU * floor(v / TAU); + +// Upper half: the orbits round the Sun. Lower half: velocity space, every +// velocity drawn from one origin O. World y runs up in both. Each half is +// fitted to its own box, BOX LEDs square, centred on row MID[0] or MID[1]. +const BOX = 59, MID = [31, 96]; +// Apoapses run from Q0 to Q1 in world units, and an orbit with semi-major +// axis 20 takes T20 seconds at Speed 1. One Sun, so one mu for every body. +const Q0 = 13, Q1 = 29.5, T20 = 8; +const MU = (TAU / T20) ** 2 * 8000; +// Each body's fan covers this share of its period, in BANDS equal-time slices. +const FAN = 0.3, BANDS = 3; +// The arrow at a body is this share of the same velocity's length below. +const ARROW = 0.55; +// Equal steps of true anomaly marked on every orbit and hodograph. +const BEADS = 12; +const TAB = 1024; +// Body colours at Hue 0: cyan, magenta, green, azure, rose, yellow. +const HUES = [0.5, 0.83, 0.3, 0.64, 0.97, 0.14]; + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// Kepler's equation M = E - e sin E by Newton's method; from pi it +// converges for every M in [0, 2 pi) and every e below 1. +function kepler(M, e) { + let E = e > 0.8 ? PI : M; + for (let k = 0; k < 40; k++) { + const d = (E - e * sin(E) - M) / (1 - e * cos(E)); + E -= d; + if (abs(d) < 1e-13) break; + } + return E; +} + +// Mean anomaly at true anomaly th in [0, 2 pi], through the eccentric anomaly. +function meanAt(e, th) { + const E = 2 * atan2(sqrt(1 - e) * sin(th / 2), sqrt(1 + e) * cos(th / 2)); + return E - e * sin(E); +} + +// The claim, checked: velocities from Kepler's equation at 720 equal steps +// of time, a least-squares circle through them (Kasa's fit, which is not +// told the answer), the largest distance of a velocity from that circle as +// a share of its radius, and how far the circle's offset from the origin +// is from e times its radius. +function check() { + let radial = 0, offset = 0; + for (let e = 0; e < 0.96; e += 0.05) { + const pts = []; + for (let k = 0; k < 720; k++) { + const E = kepler((TAU * k) / 720, e), q = 1 - e * cos(E); + pts.push([-sin(E) / q, (sqrt(1 - e * e) * cos(E)) / q]); + } + let a = 0, b = 0, c = 0, d = 0, f = 0, n = pts.length, u = 0, v = 0, z = 0; + for (const [x, y] of pts) { + const r = x * x + y * y; + a += x * x; b += x * y; c += y * y; d += x; f += y; u += x * r; v += y * r; z += r; + } + const det3 = (m) => + m[0] * (m[4] * m[8] - m[5] * m[7]) - m[1] * (m[3] * m[8] - m[5] * m[6]) + m[2] * (m[3] * m[7] - m[4] * m[6]); + const A = [a, b, d, b, c, f, d, f, n], rhs = [-u, -v, -z], D = det3(A); + const sol = [0, 1, 2].map((col) => det3(A.map((m, i) => (i % 3 === col ? rhs[floor(i / 3)] : m))) / D); + const cx = -sol[0] / 2, cy = -sol[1] / 2, R = sqrt(cx * cx + cy * cy - sol[2]); + for (const [x, y] of pts) radial = max(radial, abs(hypot(x - cx, y - cy) - R) / R); + offset = max(offset, abs(hypot(cx, cy) / R - e)); + } + return { radial, offset }; +} + +function body() { + return { + e: 0, a: 1, p: 1, Q: 1, nm: 1, R: 1, w: 0, cw: 1, sw: 0, M0: 0, M: 0, + x: 0, y: 0, vx: 0, vy: 0, hx: 0, hy: 0, hr: 1, mt: new Float64Array(TAB + 1), col: new Float32Array(3), + }; +} + +export function setup(params) { + params.s = { + t: 0, n: 0, emax: -1, hue: 0, + // Orbit scale and the Sun's LED; hodograph scale and the origin's LED. + so: 1, fx: 31.5, fy: 31, hs: 1, ox: 31.5, oy: 96, + b: [0, 1, 2, 3, 4, 5].map(body), + rgb: new Float32Array(W * H * 3), c: new Float32Array(3), + check: check(), + }; + return params.s; +} + +// Scale and centre that fit a world box into a half of the panel. +function fit(x0, x1, y0, y1, row) { + const s = BOX / max(x1 - x0, y1 - y0); + return [s, 31.5 - (s * (x0 + x1)) / 2, row + (s * (y0 + y1)) / 2]; +} + +// The bodies: eccentricities spread evenly up to the knob's, apoapses from +// Q0 to Q1, apse lines turned by the golden angle from one another. Then +// both halves are fitted: the orbits by their boxes, the hodographs by +// theirs, each circle of radius R = mu / h centred e R from O, square to +// the apse line. +function build(S, n, emax) { + S.n = n; + S.emax = emax; + let x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9, u0 = 1e9, u1 = -1e9, v0 = 1e9, v1 = -1e9; + for (let i = 0; i < n; i++) { + const B = S.b[i], e = (emax * (i + 1)) / n; + B.e = e; + B.Q = Q0 + ((Q1 - Q0) * i) / (n - 1); + B.a = B.Q / (1 + e); + B.p = B.a * (1 - e * e); + B.nm = sqrt(MU / (B.a * B.a * B.a)); + B.R = sqrt(MU / B.p); + B.w = 1.1 + i * 2.39996; + B.cw = cos(B.w); + B.sw = sin(B.w); + B.M0 = 0.6 + i * 2.6; + for (let k = 0; k <= TAB; k++) B.mt[k] = meanAt(e, (TAU * k) / TAB); + const b = B.a * sqrt(1 - e * e), cx = -B.a * e * B.cw, cy = -B.a * e * B.sw; + const ex = sqrt(B.a * B.a * B.cw * B.cw + b * b * B.sw * B.sw), ey = sqrt(B.a * B.a * B.sw * B.sw + b * b * B.cw * B.cw); + x0 = min(x0, cx - ex); x1 = max(x1, cx + ex); y0 = min(y0, cy - ey); y1 = max(y1, cy + ey); + const hx = -e * B.R * B.sw, hy = e * B.R * B.cw; + u0 = min(u0, hx - B.R); u1 = max(u1, hx + B.R); v0 = min(v0, hy - B.R); v1 = max(v1, hy + B.R); + } + [S.so, S.fx, S.fy] = fit(x0, x1, y0, y1, MID[0]); + [S.hs, S.ox, S.oy] = fit(u0, u1, v0, v1, MID[1]); + for (let i = 0; i < n; i++) { + const B = S.b[i]; + B.hr = B.R * S.hs; + B.hx = -B.e * B.hr * B.sw; + B.hy = B.e * B.hr * B.cw; + } +} + +function meanOf(B, th) { + const f = (wrap(th) / TAU) * TAB, k = min(TAB - 1, floor(f)); + return B.mt[k] + (B.mt[k + 1] - B.mt[k]) * (f - k); +} + +// Position (from the Sun) and velocity of a body at time t, by Kepler's +// equation, both in world axes. +function place(B, t) { + const e = B.e, M = wrap(B.M0 + B.nm * t), E = kepler(M, e); + const cE = cos(E), sE = sin(E), s1 = sqrt(1 - e * e), k = (B.nm * B.a) / (1 - e * cE); + const px = B.a * (cE - e), py = B.a * s1 * sE, vx = -k * sE, vy = k * s1 * cE; + B.M = M; + B.x = px * B.cw - py * B.sw; + B.y = px * B.sw + py * B.cw; + B.vx = vx * B.cw - vy * B.sw; + B.vy = vx * B.sw + vy * B.cw; +} + +export function update(dt, input, params) { + const S = params.s || setup(params); + const kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0] || 0; + const speed = cl(kv[1], 0.2, 3), emax = cl(kv[2], 0.6, 0.95), n = cl(round(kv[3]), 3, 6); + if (n !== S.n || emax !== S.emax) build(S, n, emax); + S.t += d * speed; + for (let i = 0; i < n; i++) { + const B = S.b[i]; + place(B, S.t); + hsv(S.hue + HUES[i], 0.8, 1, B.col); + } +} + +// Brightness of a fan at a given age, as a share of the fan's window: +// equal-time slices alternate bright and dim, fading toward the oldest. +function fan(age) { + return (1 - age) ** 0.7 * (floor(age * BANDS) % 2 ? 0.26 : 0.5); +} + +function put(rgb, x, y, c, v) { + const o = (y * W + x) * 3; + if (rgb[o] < c[0] * v) rgb[o] = c[0] * v; + if (rgb[o + 1] < c[1] * v) rgb[o + 1] = c[1] * v; + if (rgb[o + 2] < c[2] * v) rgb[o + 2] = c[2] * v; +} + +// A soft point at panel coordinates (px, py), weights split over the four +// nearest LEDs, each keeping the brighter value. +function dot(rgb, px, py, c, v) { + const x0 = floor(px), y0 = floor(py), fx = px - x0, fy = py - 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; + put(rgb, x, y, c, v * (i ? fx : 1 - fx) * (j ? fy : 1 - fy)); + } +} + +function line(rgb, x0, y0, x1, y1, c, v) { + const n = max(1, Math.ceil(hypot(x1 - x0, y1 - y0) * 2.2)); + for (let k = 0; k <= n; k++) dot(rgb, x0 + ((x1 - x0) * k) / n, y0 + ((y1 - y0) * k) / n, c, v); +} + +function disc(rgb, px, py, rad, c, core) { + for (let y = max(0, floor(py - rad - 1)); y <= min(H - 1, Math.ceil(py + rad + 1)); y++) + for (let x = max(0, floor(px - rad - 1)); x <= min(W - 1, Math.ceil(px + rad + 1)); x++) { + const d = hypot(x - px, y - py), v = cl(rad + 0.5 - d, 0, 1); + if (v <= 0) continue; + const w = core * cl(1 - d / rad, 0, 1), o = (y * W + x) * 3; + rgb[o] = max(rgb[o], c[0] * v + (1 - c[0]) * w); + rgb[o + 1] = max(rgb[o + 1], c[1] * v + (1 - c[1]) * w); + rgb[o + 2] = max(rgb[o + 2], c[2] * v + (1 - c[2]) * w); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.n) return; + const rgb = S.rgb, n = S.n, bs = S.b, c = S.c, win = TAU * FAN; + const so = S.so, fx = S.fx, fy = S.fy, hs = S.hs, ox = S.ox, oy = S.oy; + rgb.fill(0); + + // The fans in the orbits: an LED's direction from the Sun is a true + // anomaly on every orbit, the orbit's radius there says whether the LED is + // inside, and the time since the body passed that direction picks the slice. + for (let y = 0; y < 64; y++) { + const dy = (fy - y) / so; + for (let x = 0; x < W; x++) { + const dx = (x - fx) / so, d = sqrt(dx * dx + dy * dy), phi = atan2(dy, dx); + for (let i = 0; i < n; i++) { + const B = bs[i]; + if (d > B.Q + 1) continue; + const th = phi - B.w, edge = (B.p / (1 + B.e * cos(th)) - d) * so + 0.5; + if (edge <= 0) continue; + const age = wrap(B.M - meanOf(B, th)) / win; + if (age >= 1) continue; + put(rgb, x, y, B.col, fan(age) * min(1, edge)); + } + } + } + // The same fans in velocity space: the ray from O through an LED meets + // each hodograph at one velocity, whose angle round its circle's centre is + // the true anomaly plus a quarter turn. + for (let y = 64; y < H; y++) { + const wy = oy - y; + for (let x = 0; x < W; x++) { + const wx = x - ox, d = sqrt(wx * wx + wy * wy); + if (d < 1e-6) continue; + const ux = wx / d, uy = wy / d; + for (let i = 0; i < n; i++) { + const B = bs[i], ex = wx - B.hx, ey = wy - B.hy, edge = B.hr + 0.5 - sqrt(ex * ex + ey * ey); + if (edge <= 0) continue; + const uc = ux * B.hx + uy * B.hy; + const s = uc + sqrt(max(0, uc * uc - B.hx * B.hx - B.hy * B.hy + B.hr * B.hr)); + const th = atan2(s * uy - B.hy, s * ux - B.hx) - B.w - PI / 2; + const age = wrap(B.M - meanOf(B, th)) / win; + if (age >= 1) continue; + put(rgb, x, y, B.col, fan(age) * min(1, edge)); + } + } + } + + for (let i = 0; i < n; i++) { + const B = bs[i], e = B.e, s1 = sqrt(1 - e * e), A = B.a * so; + // The orbit, brighter where the body has just been. + for (let k = 0; k < 420; k++) { + const E = (TAU * k) / 420, px = A * (cos(E) - e), py = A * s1 * sin(E); + const age = wrap(B.M - (E - e * sin(E))) / win; + dot(rgb, fx + px * B.cw - py * B.sw, fy - (px * B.sw + py * B.cw), B.col, age < 1 ? 0.95 - 0.5 * age : 0.4); + } + // The hodograph, the same way. + const nh = max(90, round(B.hr * 9)); + for (let k = 0; k < nh; k++) { + const th = (TAU * k) / nh, vx = -B.hr * sin(th), vy = B.hr * (e + cos(th)); + const age = wrap(B.M - meanAt(e, th)) / win; + dot(rgb, ox + vx * B.cw - vy * B.sw, oy - (vx * B.sw + vy * B.cw), B.col, age < 1 ? 0.95 - 0.5 * age : 0.4); + } + // The circle's centre, e times its radius from O. + dot(rgb, ox + B.hx, oy - B.hy, B.col, 0.6); + // Feynman's beads: the orbit at equal steps of angle round the Sun, and + // the velocities there, which fall at equal steps round the circle. + for (let k = 0; k < 3; k++) c[k] = 0.55 + 0.45 * B.col[k]; + for (let k = 0; k < BEADS; k++) { + const th = (TAU * k) / BEADS, r = (B.p * so) / (1 + e * cos(th)); + const px = r * cos(th), py = r * sin(th), vx = -B.hr * sin(th), vy = B.hr * (e + cos(th)); + dot(rgb, fx + px * B.cw - py * B.sw, fy - (px * B.sw + py * B.cw), c, 1); + dot(rgb, ox + vx * B.cw - vy * B.sw, oy - (vx * B.sw + vy * B.cw), c, 1); + } + } + for (let i = 0; i < n; i++) { + const B = bs[i], col = B.col; + for (let k = 0; k < 3; k++) c[k] = 0.5 + 0.5 * col[k]; + const bx = fx + B.x * so, by = fy - B.y * so, tx = ox + B.vx * hs, ty = oy - B.vy * hs; + // A thread from the body to the tip of its velocity, the velocity arrow + // at the body, and the same velocity from O. + line(rgb, bx, by, tx, ty, col, 0.16); + line(rgb, bx, by, bx + (tx - ox) * ARROW, by + (ty - oy) * ARROW, c, 1); + line(rgb, ox, oy, tx, ty, c, 1); + disc(rgb, tx, ty, 1.2, col, 0.6); + disc(rgb, bx, by, 1.5, col, 0.8); + } + c[0] = 1; c[1] = 0.86; c[2] = 0.6; + disc(rgb, fx, fy, 1.8, c, 1); + c[0] = c[1] = c[2] = 1; + disc(rgb, ox, oy, 0.9, c, 1); + + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const o = (y * W + x) * 3; + display.setPixel(x, y, min(255, rgb[o] * 255) | 0, min(255, rgb[o + 1] * 255) | 0, min(255, rgb[o + 2] * 255) | 0); + } +} From bb3505843e4753de895cd02a774287f00f590822 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:42:16 +0000 Subject: [PATCH 04/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Golomb=20Ruler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- incoming/pattern-study/patterns/golomb-ruler.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/incoming/pattern-study/patterns/golomb-ruler.js b/incoming/pattern-study/patterns/golomb-ruler.js index ae5a894f7..ea77843bc 100644 --- a/incoming/pattern-study/patterns/golomb-ruler.js +++ b/incoming/pattern-study/patterns/golomb-ruler.js @@ -234,10 +234,10 @@ export function draw(display, params) { for (let y = 0; y < H; y++) display.setPixel(0, y, 40, 40, 48); // A mark burns white while an arc from it is being measured. for (const m of S.marks) { - const f = mf[m], v = 170 + 85 * f; + const f = mf[m], v = (170 + 85 * f) | 0; display.setPixel(0, m, 255, 250, 240); display.setPixel(1, m, v, v - 5, v - 15); - if (f > 0.05) display.setPixel(2, m, 255 * f, 255 * f, 255 * f); + if (f > 0.05) display.setPixel(2, m, (255 * f) | 0, (255 * f) | 0, (255 * f) | 0); } // The length scale: row d ticks once measured, 2 + n LEDs long when n // pairs share the length, white for a moment as the caliper passes; a @@ -252,7 +252,7 @@ export function draw(display, params) { } if (n === 0) { const b = exp(-since / 0.5); - if (b > 0.1) display.setPixel(SX, dd, 200 * b, 30 * b, 40 * b); + if (b > 0.1) display.setPixel(SX, dd, (200 * b) | 0, (30 * b) | 0, (40 * b) | 0); continue; } const fl = exp(-since / 0.25); From 16c350972867edf811f090d10530da5c52e0a2a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:44:34 +0000 Subject: [PATCH 05/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Shapley-Folkman?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pattern-study/patterns/shapley-folkman.js | 359 ++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 incoming/pattern-study/patterns/shapley-folkman.js diff --git a/incoming/pattern-study/patterns/shapley-folkman.js b/incoming/pattern-study/patterns/shapley-folkman.js new file mode 100644 index 000000000..b46e67341 --- /dev/null +++ b/incoming/pattern-study/patterns/shapley-folkman.js @@ -0,0 +1,359 @@ +// ===== Patternflow pattern ===== +// Title: Shapley-Folkman +// Family: geometry +// Idea: Four bands down the panel show one nonconvex shape and its Minkowski averages of 2, 4 and 8 copies, each computed exactly as a sum of lattice point sets, so the bays and holes close band by band toward the convex hull drawn faintly round each one, the gap halving at every step. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Shapley-Folkman lemma (Lloyd Shapley and Jon Folkman, first published by Ross Starr, 1969), pattern round 66 +// Round: 66 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Shape=0..4, Hull=0..1 +// +// Knob 1: Hue. Turns the band colours round the wheel together. +// Knob 2: Speed. How fast the shape turns or moves. +// Knob 3: Shape. Moon, pearls, open ring, comb and star at 0 to 4, blended in between. +// Knob 4: Hull. How strongly the convex hull and its uncovered part are drawn. + +const W = 64, H = 128; +const { floor, min, max, abs, sqrt, cos, sin, atan2, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const TAU = 2 * PI; + +// One band per n. The shape's lattice is SW by SH points, one per LED, +// placed at (X0, Y0) inside each 32-row band. +const NS = [1, 2, 4, 8], NMAX = 8; +const SW = 59, SH = 28, X0 = 4, Y0 = 2, BAND = 32; +const CX = (SW - 1) / 2, CY = (SH - 1) / 2; + +function hsv(h, s, v, out) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// ---- Lattice sets as bitmaps: w by h points, rows of 32-bit words. ---- +function bitmap(w, h) { + const ww = ((w + 31) >> 5) + 1; + return { w, h, ww, b: new Uint32Array(ww * h) }; +} +const has = (B, x, y) => (B.b[y * B.ww + (x >> 5)] >>> (x & 31)) & 1; + +// dst |= src moved by (dx, dy). +function orShift(dst, src, dx, dy) { + const q = dx >> 5, r = dx & 31, sw = src.ww, dw = dst.ww, sb = src.b, db = dst.b; + const used = (src.w + 31) >> 5; + for (let y = 0; y < src.h; y++) { + const so = y * sw, d0 = (y + dy) * dw + q; + if (r === 0) for (let i = 0; i < used; i++) db[d0 + i] |= sb[so + i]; + else + for (let i = 0; i < used; i++) { + const v = sb[so + i]; + if (!v) continue; + db[d0 + i] |= v << r; + db[d0 + i + 1] |= v >>> (32 - r); + } + } +} + +// The points of B with one of their eight neighbours outside B. +function boundary(B, xs, ys) { + const { h, ww, b } = B, used = (B.w + 31) >> 5; + const hor = (o, i) => { + const m = b[o + i]; + if (!m) return 0; + return m & ((m << 1) | (i > 0 ? b[o + i - 1] >>> 31 : 0)) & ((m >>> 1) | (i + 1 < used ? b[o + i + 1] << 31 : 0)); + }; + let n = 0; + for (let y = 0; y < h; y++) + for (let i = 0; i < used; i++) { + const m = b[y * ww + i]; + if (!m) continue; + const inner = y > 0 && y < h - 1 ? hor(y * ww, i) & hor((y - 1) * ww, i) & hor((y + 1) * ww, i) : 0; + let e = m & ~inner; + while (e) { + const t = e & -e; + xs[n] = i * 32 + 31 - Math.clz32(t); + ys[n++] = y; + e ^= t; + } + } + return n; +} + +// One point from each 8-connected piece of S. +function pieces(S, lab, stack, out) { + out.length = 0; + lab.fill(0); + for (let y = 0; y < SH; y++) + for (let x = 0; x < SW; x++) { + if (lab[y * SW + x] || !has(S, x, y)) continue; + out.push(x, y); + let sp = 0; + stack[sp++] = y * SW + x; + lab[y * SW + x] = 1; + while (sp) { + const p = stack[--sp], px = p % SW, py = (p - px) / SW; + for (let yy = max(0, py - 1); yy <= min(SH - 1, py + 1); yy++) + for (let xx = max(0, px - 1); xx <= min(SW - 1, px + 1); xx++) { + const k = yy * SW + xx; + if (!lab[k] && has(S, xx, yy)) (lab[k] = 1), (stack[sp++] = k); + } + } + } +} + +// C = A + S, the Minkowski sum of lattice sets. Shifting the small S to +// every point of A would do; it is enough to shift it to the boundary +// points of A, and add one copy of A for each piece of S. For a piece P of +// S and a sum x = p + a, the set x - P is connected and holds a; either it +// leaves A, and then it crosses A's boundary, or it stays inside A and +// x lies in A shifted by any one point of P. S is at most 64 points wide, +// so each of its rows, moved by 0 to 31 bits, is kept ready as three words. +function sum(C, A, St, reps, xs, ys) { + const cb = C.b, cw = C.ww, pre = St.pre, y0 = St.ylo, y1 = St.yhi; + cb.fill(0); + const n = boundary(A, xs, ys); + for (let k = 0; k < n; k++) { + const x = xs[k], p0 = (x & 31) * SH * 3; + let o = (ys[k] + y0) * cw + (x >> 5); + for (let y = y0, p = p0 + y0 * 3; y <= y1; y++, p += 3, o += cw) { + cb[o] |= pre[p]; + cb[o + 1] |= pre[p + 1]; + cb[o + 2] |= pre[p + 2]; + } + } + for (let k = 0; k < reps.length; k += 2) orShift(C, A, reps[k], reps[k + 1]); +} + +// ---- The shapes, as rough signed distances in LEDs (inside < 0). ---- +// The lattice is 59 wide and 28 tall; U and V scale a unit circle to it. +const U = 28.5, V = 13; +const len = (x, y) => sqrt(x * x + y * y); +const SHAPES = [ + // Moon: an ellipse with a bite out of it, the bite going round. + (x, y, p) => { + const u = x / U, v = y / V, bu = u - 0.45 * cos(p), bv = v - 0.45 * sin(p); + return max(len(u, v) - 1, 0.75 - len(bu, bv)) * V; + }, + // Pearls: three discs going round an ellipse. + (x, y, p) => { + let d = 1e9; + for (let j = 0; j < 3; j++) { + const a = p + (TAU * j) / 3; + d = min(d, len(x - 21 * cos(a), y - 6.5 * sin(a)) - 6.5); + } + return d; + }, + // Open ring: an elliptic band with a gap going round. + (x, y, p) => { + const u = x / U, v = y / V, r = len(u, v); + const a = atan2(v, u) - p, g = abs(atan2(sin(a), cos(a))); + return max(abs(r - 0.8) - 0.2, (0.55 - g) * r) * V; + }, + // Comb: a back along the bottom and four teeth rising and falling. + (x, y, p) => { + let d = max(abs(x) - 28.5, abs(y - 10.5) - 3); + for (let j = 0; j < 4; j++) { + const tx = -21.4 + 14.25 * j, top = -13 + 7 * (0.5 + 0.5 * sin(p * 1.3 + j * 1.9)); + d = min(d, max(abs(x - tx) - 3.2, max(top - y, y - 10))); + } + return d; + }, + // Star: five points turning, seen flattened to fit the band. + (x, y, p) => { + const u = x / U, v = y / V, r = len(u, v); + const a = atan2(v, u) - p, k = (a * 5) / TAU, f = abs(k - floor(k) - 0.5) * 2; + return (r - (0.38 + 0.62 * f * f)) * V * 1.4; + }, +]; + +export function setup(params) { + const sums = [null]; + for (let k = 1; k <= NMAX; k++) sums.push(bitmap(k * (SW - 1) + 1, k * (SH - 1) + 1)); + const big = sums[NMAX - 1].w * sums[NMAX - 1].h; + params.s = { + t: 0, phase: 0, hue: 0, shape: 0, hk: 0.06, + sums, xs: new Int32Array(big), ys: new Int32Array(big), + lab: new Uint8Array(SW * SH), stack: new Int32Array(SW * SH * 9), reps: [], + cov: new Float32Array(NS.length * SW * SH), sd: new Float32Array(SW * SH), + hull: [], pre: new Uint32Array(32 * SH * 3), ylo: 0, yhi: -1, ex: new Float64Array(SW * SH * 3), + pop: new Uint8Array(256).map((_, i) => (i.toString(2).match(/1/g) || []).length), + c: new Float32Array(3), lay: new Float32Array(NS.length * 9), unc: new Float32Array(SW * SH), rgb: new Float32Array(W * H * 3), + }; + return params.s; +} + +// Rasterise the shape at this phase and knob, then the sums 2..8. +function build(S) { + const A = S.sums[1]; + A.b.fill(0); + const k = cl(S.shape, 0, 4), i0 = min(3, floor(k)), f = k - i0; + const fa = SHAPES[i0], fb = SHAPES[i0 + 1]; + for (let y = 0; y < SH; y++) + for (let x = 0; x < SW; x++) { + const px = x - CX, py = y - CY; + let d = fa(px, py, S.phase); + if (f > 0) d = d * (1 - f) + fb(px, py, S.phase) * f; + if (d <= 0) A.b[y * A.ww + (x >> 5)] |= 1 << (x & 31); + } + pieces(A, S.lab, S.stack, S.reps); + S.ylo = SH; + S.yhi = -1; + for (let y = 0; y < SH; y++) { + const w0 = A.b[y * A.ww], w1 = A.b[y * A.ww + 1]; + if (w0 | w1) (S.ylo = min(S.ylo, y)), (S.yhi = y); + for (let r = 0; r < 32; r++) { + const p = (r * SH + y) * 3; + S.pre[p] = w0 << r; + S.pre[p + 1] = r ? (w0 >>> (32 - r)) | (w1 << r) : w1; + S.pre[p + 2] = r ? w1 >>> (32 - r) : 0; + } + } + for (let n = 1; n < NMAX; n++) sum(S.sums[n + 1], S.sums[n], S, S.reps, S.xs, S.ys); + hullOf(S); + // Coverage of each LED: the share of the n by n lattice points of the + // n-fold sum that fall in it, since A(n) is that sum shrunk by 1/n. + for (let bi = 0; bi < NS.length; bi++) { + const n = NS[bi], B = S.sums[n], o = bi * SW * SH, h = n >> 1, mask = (1 << n) - 1; + for (let y = 0; y < SH; y++) + for (let x = 0; x < SW; x++) { + let c = 0; + const a = n * x - h; + for (let r = n * y - h; r < n * y - h + n; r++) { + if (r < 0 || r >= B.h) continue; + const row = r * B.ww; + if (a < 0) { + c += S.pop[B.b[row] & ((1 << (a + n)) - 1)]; + continue; + } + const wi = a >> 5, off = a & 31; + let v = B.b[row + wi] >>> off; + if (off + n > 32) v |= B.b[row + wi + 1] << (32 - off); + c += S.pop[v & mask]; + } + S.cov[o + y * SW + x] = c / (n * n); + } + } +} + +// Convex hull of the shape's lattice points (monotone chain), then each +// LED's signed distance to it, negative inside. +function hullOf(S) { + const n = boundary(S.sums[1], S.xs, S.ys), P = []; + for (let k = 0; k < n; k++) P.push([S.xs[k], S.ys[k]]); + P.sort((a, b) => a[0] - b[0] || a[1] - b[1]); + const cr = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); + const lo = [], up = []; + for (const p of P) { + while (lo.length >= 2 && cr(lo[lo.length - 2], lo[lo.length - 1], p) <= 0) lo.pop(); + lo.push(p); + } + for (let i = P.length - 1; i >= 0; i--) { + const p = P[i]; + while (up.length >= 2 && cr(up[up.length - 2], up[up.length - 1], p) <= 0) up.pop(); + up.push(p); + } + const Hl = lo.slice(0, -1).concat(up.slice(0, -1)); + S.hull = Hl; + // The chain turns left at every corner, reading y as up, so the inside + // is on the left of each edge; an LED's distance is the largest it lies + // outside any edge, which is exact inside and close enough near it. + const m = Hl.length, E = S.ex; + for (let i = 0; i < m; i++) { + const a = Hl[i], b = Hl[(i + 1) % m], ex = b[0] - a[0], ey = b[1] - a[1], L = sqrt(ex * ex + ey * ey) || 1; + E[i * 3] = ey / L; + E[i * 3 + 1] = -ex / L; + E[i * 3 + 2] = (a[1] * ex - a[0] * ey) / L; + } + for (let y = 0; y < SH; y++) + for (let x = 0; x < SW; x++) { + let d = -1e9; + for (let i = 0; i < m * 3; i += 3) d = max(d, x * E[i] + y * E[i + 1] + E[i + 2]); + S.sd[y * SW + x] = m < 3 ? 9 : d; + } +} + +export function update(dt, input, params) { + const S = params.s || setup(params); + const kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0] || 0; + const speed = cl(kv[1] ?? 2, 0.2, 3); + S.shape = cl(kv[2] || 0, 0, 4); + S.hk = cl(kv[3] ?? 0.06, 0, 1); + S.t += d; + S.phase += d * speed * 0.35; + build(S); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const rgb = S.rgb, c = S.c, L = S.lay, unc = S.unc, cov = S.cov, A = SW * SH; + rgb.fill(0); + const hk = S.hk; + // Colour by n: each band's points are coloured by the band where they + // first appear, since A(1), A(2), A(4) and A(8) each hold the one before. + // Older layers are deep, the layer new in this band is pale and bright. + for (let j = 0; j < NS.length; j++) { + const h = S.hue + 0.47 + 0.12 * j; + hsv(h, 0.9, 0.5, c); + L.set(c, j * 9); + hsv(h + 0.03, 0.45, 1, c); + L.set(c, j * 9 + 3); + hsv(h, 0.9, 0.85, c); + L.set(c, j * 9 + 6); + } + hsv(S.hue + 0.13, 0.8, 1, c); + const fr = c[0], fg = c[1], fb = c[2]; + for (let bi = 0; bi < NS.length; bi++) { + const n = NS[bi], o = bi * A; + for (let k = 0; k < A; k++) unc[k] = cl(0.5 - S.sd[k], 0, 1) * (1 - cov[o + k]); + for (let y = 0; y < SH; y++) + for (let x = 0; x < SW; x++) { + const k = y * SW + x, cv = cov[o + k], sd = S.sd[k]; + let r = 0, g = 0, b = 0, acc = 0; + for (let j = 0; j <= bi; j++) { + const cj = min(cov[j * A + k], cv), lj = cj - acc; + if (lj <= 0) continue; + acc = cj; + const q = j * 9 + (bi === 0 ? 6 : j === bi ? 3 : 0); + r += L[q] * lj; + g += L[q + 1] * lj; + b += L[q + 2] * lj; + } + // The filling edge: lit points next to the part of the hull still + // uncovered glow in the new layer's pale colour. + let u = 0; + if (cv > 0) { + if (x > 0) u = max(u, unc[k - 1]); + if (x < SW - 1) u = max(u, unc[k + 1]); + if (y > 0) u = max(u, unc[k - SW]); + if (y < SH - 1) u = max(u, unc[k + SW]); + } + const glow = u * cv * 0.7, gq = bi * 9 + 3; + const line = cl(1 - abs(sd) / 0.8, 0, 1) * (0.14 + 0.5 * hk); + const gap = unc[k] * hk * 0.85; + const q = ((bi * BAND + Y0 + y) * W + X0 + x) * 3; + rgb[q] = r + L[gq] * glow + fr * gap + line * 0.8; + rgb[q + 1] = g + L[gq + 1] * glow + fg * gap + line * 0.75; + rgb[q + 2] = b + L[gq + 2] * glow + fb * gap + line * 0.6; + } + // n dots at the left edge: the number of copies summed. + for (let j = 0; j < n; j++) { + const Y = bi * BAND + 16 - ((n - 1) * 3) / 2 + j * 3; + const q = ((Y | 0) * W + 1) * 3; + rgb[q] = L[bi * 9 + 3]; + rgb[q + 1] = L[bi * 9 + 4]; + rgb[q + 2] = L[bi * 9 + 5]; + } + } + for (let Y = 0; Y < H; Y++) + for (let X = 0; X < W; X++) { + const q = (Y * W + X) * 3; + display.setPixel(X, Y, min(255, rgb[q] * 255) | 0, min(255, rgb[q + 1] * 255) | 0, min(255, rgb[q + 2] * 255) | 0); + } +} From cb9ea12c02d548c4cdb86079fa10e780b2927fca Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:44:53 +0000 Subject: [PATCH 06/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Hodograph?= 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_01Gdqi6PeQBKagUB2vLqwQZR --- incoming/pattern-study/patterns/hodograph.js | 82 +++++++++++++------- 1 file changed, 55 insertions(+), 27 deletions(-) diff --git a/incoming/pattern-study/patterns/hodograph.js b/incoming/pattern-study/patterns/hodograph.js index bf2117e93..30cda0d22 100644 --- a/incoming/pattern-study/patterns/hodograph.js +++ b/incoming/pattern-study/patterns/hodograph.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Hodograph // Family: physics -// Idea: Bodies of different eccentricity circle one Sun in the upper half with Kepler's timing, each sweeping a fan of equal-time slices, and in the lower half each body's velocity, drawn from one common origin, runs round a circle set off from the origin by e times its radius. +// Idea: Bodies of different eccentricity go round one Sun in the upper half with Kepler's timing, each trailing a fan of three equal-time slices, and below them each body's velocity, drawn from one common origin, runs round a circle set off from that origin by e times its radius. // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -11,10 +11,10 @@ // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Eccentricity=0.6..0.95, Bodies=3..6 // -// Knob 1: Hue. Turns the bodies' colours round the wheel together. -// Knob 2: Speed. How fast time runs. -// Knob 3: Eccentricity. The largest eccentricity. -// Knob 4: Bodies. How many bodies. +// Knob 1: Hue. Turns the bodies' colours round the wheel together: at 0 the first three are cyan, magenta and green and the fourth to sixth azure, rose and yellow, while the Sun stays pale gold and the origin of the velocities white. +// Knob 2: Speed. How fast time runs. At 1 the three opening bodies go round in 3.2, 5.3 and 7.1 seconds, the period growing as the 3/2 power of the semi-major axis; at the opening 2 they take 1.6, 2.6 and 3.5 seconds. +// Knob 3: Eccentricity. The eccentricity of the most eccentric body, with the others spread evenly below it: 0.2, 0.4 and 0.6 at the opening 0.6. At 0.95 that body moves 39 times faster at periapsis than at apoapsis, its hodograph is the largest circle with the origin 0.05 of a radius inside its rim, and the other hodographs shrink round the origin. +// Knob 4: Bodies. Three to six bodies, their apoapses spread evenly from 13 to 29.5 units and their apse lines turned 137.5 degrees (the golden angle) from one another; each half of the panel is fitted to the set again. const W = 64, H = 128; const { floor, min, max, abs, sqrt, sin, cos, atan2, PI, round, hypot } = Math; @@ -25,7 +25,7 @@ const wrap = (v) => v - TAU * floor(v / TAU); // Upper half: the orbits round the Sun. Lower half: velocity space, every // velocity drawn from one origin O. World y runs up in both. Each half is // fitted to its own box, BOX LEDs square, centred on row MID[0] or MID[1]. -const BOX = 59, MID = [31, 96]; +const BOX = 57, MID = [30, 97]; // Apoapses run from Q0 to Q1 in world units, and an orbit with semi-major // axis 20 takes T20 seconds at Speed 1. One Sun, so one mu for every body. const Q0 = 13, Q1 = 29.5, T20 = 8; @@ -48,11 +48,11 @@ function hsv(h, s, v, out) { out[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; } -// Kepler's equation M = E - e sin E by Newton's method; from pi it -// converges for every M in [0, 2 pi) and every e below 1. +// Kepler's equation M = E - e sin E by Newton's method from E = M. Over e +// up to 0.95 and 20 000 values of M it settles within 7 steps. function kepler(M, e) { - let E = e > 0.8 ? PI : M; - for (let k = 0; k < 40; k++) { + let E = M; + for (let k = 0; k < 20; k++) { const d = (E - e * sin(E) - M) / (1 - e * cos(E)); E -= d; if (abs(d) < 1e-13) break; @@ -97,8 +97,8 @@ function check() { function body() { return { - e: 0, a: 1, p: 1, Q: 1, nm: 1, R: 1, w: 0, cw: 1, sw: 0, M0: 0, M: 0, - x: 0, y: 0, vx: 0, vy: 0, hx: 0, hy: 0, hr: 1, mt: new Float64Array(TAB + 1), col: new Float32Array(3), + e: 0, a: 1, p: 1, Q: 1, nm: 1, R: 1, w0: 0, w: 0, cw: 1, sw: 0, M0: 0, M: 0, + x: 0, y: 0, vx: 0, vy: 0, psi: 0, span: 0, hx: 0, hy: 0, hr: 1, mt: new Float64Array(TAB + 1), col: new Float32Array(3), }; } @@ -120,15 +120,27 @@ function fit(x0, x1, y0, y1, row) { return [s, 31.5 - (s * (x0 + x1)) / 2, row + (s * (y0 + y1)) / 2]; } +// The orbits' box with every apse line turned by tilt, as [x0, x1, y0, y1]. +function orbitBox(S, tilt) { + let x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9; + for (let i = 0; i < S.n; i++) { + const B = S.b[i], w = B.w0 + tilt, cw = cos(w), sw = sin(w), b = B.a * sqrt(1 - B.e * B.e); + const cx = -B.a * B.e * cw, cy = -B.a * B.e * sw; + const ex = sqrt(B.a * B.a * cw * cw + b * b * sw * sw), ey = sqrt(B.a * B.a * sw * sw + b * b * cw * cw); + x0 = min(x0, cx - ex); x1 = max(x1, cx + ex); y0 = min(y0, cy - ey); y1 = max(y1, cy + ey); + } + return [x0, x1, y0, y1]; +} + // The bodies: eccentricities spread evenly up to the knob's, apoapses from -// Q0 to Q1, apse lines turned by the golden angle from one another. Then -// both halves are fitted: the orbits by their boxes, the hodographs by +// Q0 to Q1, apse lines turned by the golden angle from one another, and the +// whole set turned to the tilt that lets it stand largest in its square. +// Then both halves are fitted: the orbits by their box, the hodographs by // theirs, each circle of radius R = mu / h centred e R from O, square to // the apse line. function build(S, n, emax) { S.n = n; S.emax = emax; - let x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9, u0 = 1e9, u1 = -1e9, v0 = 1e9, v1 = -1e9; for (let i = 0; i < n; i++) { const B = S.b[i], e = (emax * (i + 1)) / n; B.e = e; @@ -137,17 +149,25 @@ function build(S, n, emax) { B.p = B.a * (1 - e * e); B.nm = sqrt(MU / (B.a * B.a * B.a)); B.R = sqrt(MU / B.p); - B.w = 1.1 + i * 2.39996; - B.cw = cos(B.w); - B.sw = sin(B.w); + B.w0 = i * 2.39996; B.M0 = 0.6 + i * 2.6; for (let k = 0; k <= TAB; k++) B.mt[k] = meanAt(e, (TAU * k) / TAB); - const b = B.a * sqrt(1 - e * e), cx = -B.a * e * B.cw, cy = -B.a * e * B.sw; - const ex = sqrt(B.a * B.a * B.cw * B.cw + b * b * B.sw * B.sw), ey = sqrt(B.a * B.a * B.sw * B.sw + b * b * B.cw * B.cw); - x0 = min(x0, cx - ex); x1 = max(x1, cx + ex); y0 = min(y0, cy - ey); y1 = max(y1, cy + ey); - const hx = -e * B.R * B.sw, hy = e * B.R * B.cw; + } + let tilt = 0, best = 1e9; + for (let k = 0; k < 60; k++) { + const [x0, x1, y0, y1] = orbitBox(S, (PI * k) / 60), size = max(x1 - x0, y1 - y0); + if (size < best - 1e-9) (best = size), (tilt = (PI * k) / 60); + } + let u0 = 1e9, u1 = -1e9, v0 = 1e9, v1 = -1e9; + for (let i = 0; i < n; i++) { + const B = S.b[i]; + B.w = B.w0 + tilt; + B.cw = cos(B.w); + B.sw = sin(B.w); + const hx = -B.e * B.R * B.sw, hy = B.e * B.R * B.cw; u0 = min(u0, hx - B.R); u1 = max(u1, hx + B.R); v0 = min(v0, hy - B.R); v1 = max(v1, hy + B.R); } + const [x0, x1, y0, y1] = orbitBox(S, tilt); [S.so, S.fx, S.fy] = fit(x0, x1, y0, y1, MID[0]); [S.hs, S.ox, S.oy] = fit(u0, u1, v0, v1, MID[1]); for (let i = 0; i < n; i++) { @@ -174,6 +194,12 @@ function place(B, t) { B.y = px * B.sw + py * B.cw; B.vx = vx * B.cw - vy * B.sw; B.vy = vx * B.sw + vy * B.cw; + // The directions of the velocity now and at the oldest edge of the fan. + // O is inside every hodograph, so the direction turns one way only, and + // the fan below lies between these two. + const Eo = kepler(wrap(M - TAU * FAN), e); + B.psi = atan2(vy, vx) + B.w; + B.span = wrap(B.psi - atan2(s1 * cos(Eo), -sin(Eo)) - B.w); } export function update(dt, input, params) { @@ -249,10 +275,10 @@ export function draw(display, params) { for (let i = 0; i < n; i++) { const B = bs[i]; if (d > B.Q + 1) continue; - const th = phi - B.w, edge = (B.p / (1 + B.e * cos(th)) - d) * so + 0.5; - if (edge <= 0) continue; - const age = wrap(B.M - meanOf(B, th)) / win; + const th = phi - B.w, age = wrap(B.M - meanOf(B, th)) / win; if (age >= 1) continue; + const edge = (B.p / (1 + B.e * cos(th)) - d) * so + 0.5; + if (edge <= 0) continue; put(rgb, x, y, B.col, fan(age) * min(1, edge)); } } @@ -265,9 +291,11 @@ export function draw(display, params) { for (let x = 0; x < W; x++) { const wx = x - ox, d = sqrt(wx * wx + wy * wy); if (d < 1e-6) continue; - const ux = wx / d, uy = wy / d; + const ux = wx / d, uy = wy / d, psi = atan2(wy, wx); for (let i = 0; i < n; i++) { - const B = bs[i], ex = wx - B.hx, ey = wy - B.hy, edge = B.hr + 0.5 - sqrt(ex * ex + ey * ey); + const B = bs[i]; + if (wrap(B.psi - psi) > B.span) continue; + const ex = wx - B.hx, ey = wy - B.hy, edge = B.hr + 0.5 - sqrt(ex * ex + ey * ey); if (edge <= 0) continue; const uc = ux * B.hx + uy * B.hy; const s = uc + sqrt(max(0, uc * uc - B.hx * B.hx - B.hy * B.hy + B.hr * B.hr)); From 380bd9009f3b61c971cdc470d38f080ab5497805 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:45:02 +0000 Subject: [PATCH 07/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Gridfonts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- incoming/pattern-study/patterns/gridfonts.js | 427 +++++++++++++++++++ 1 file changed, 427 insertions(+) create mode 100644 incoming/pattern-study/patterns/gridfonts.js diff --git a/incoming/pattern-study/patterns/gridfonts.js b/incoming/pattern-study/patterns/gridfonts.js new file mode 100644 index 000000000..1b0efa4fd --- /dev/null +++ b/incoming/pattern-study/patterns/gridfonts.js @@ -0,0 +1,427 @@ +// ===== Patternflow pattern ===== +// Title: Gridfonts +// Family: type +// Idea: Three lines of three lowercase letters are built only from short strokes joining neighbouring points of a faint 3 by 7 dot grid, as in Douglas Hofstadter's gridfonts, and when the style changes every letter on the panel takes back and draws in its strokes by the same rule at once, cutting its corners, raising gables or opening stencil gaps. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: gridfonts and the Letter Spirit project (Douglas Hofstadter, 1980s; Gary McGraw's thesis, 1995; Hofstadter and the Fluid Analogies Research Group, Fluid Concepts and Creative Analogies, 1995), letters and styles drawn for this pattern, pattern round 66 +// Round: 66 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Style=0..6, Grid=0..1 +// +// Knob 1: Hue. Turns the letters round the wheel from sea green at 0; each style keeps its own small step off that colour. +// Knob 2: Speed. How fast strokes draw and the pages turn: at 2 something changes every 4 seconds, at 0.2 every 40. +// Knob 3: Style. At 0 the six styles take turns, every other change a new style on the same words and then new words in that style; 1 to 6 hold one style: Square, Cut, Lean, Gable, Stencil, Inline. +// Knob 4: Grid. The grid under the letters: dim points at 0, brighter points from 0.06, and toward 1 all 56 strokes every letter could use, faintly, in each letter's cell. + +const W = 64, H = 128, N = W * H; +const { floor, ceil, round, min, max, abs, sqrt, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const smooth = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// The grid of one letter: 3 points across and 7 down, point p = 3 * row + +// column with row 0 at the top. Rows 2 to 4 are the central zone, from the +// x-height down to the baseline; rows 0 and 1 hold ascenders, 5 and 6 +// descenders. +const GC = 3, GR = 7, GP = GC * GR; +// A quantum joins two points that are neighbours across, down or on a +// diagonal. Counted here: 14 across, 18 down and 24 diagonals, 56 in all. +const QA = [], QB = []; +const QID = new Int8Array(GP * GP).fill(-1); +for (let p = 0; p < GP; p++) + for (let q = p + 1; q < GP; q++) + if (max(abs((q % GC) - (p % GC)), abs(floor(q / GC) - floor(p / GC))) === 1) { + QID[p * GP + q] = QID[q * GP + p] = QA.length; + QA.push(p); + QB.push(q); + } +const NQ = QA.length; + +// The panel: points 7 LEDs apart, three letters a line with an empty square +// between letters, so the points of all nine letters fall on one lattice of +// 9 by 19 points. Lines share a row of points: one line's descender row is +// the next line's ascender row, and the pages are chosen so that no +// descender meets an ascender there. +const SQ = 7, X0 = 3, PITCH = 3 * SQ, LINE = 6 * SQ, CELLS = 9; + +// The base face, written as pen paths. Each pair of digits is a point, +// column then row, and the pen goes straight from one to the next, so one +// step of a path may run over two quanta in a line. +const BASE = { + a: "022224040323", + b: "0004242202", + c: "22020424", + d: "2024040222", + e: "032322020424", + f: "201014 0222", + g: "240402222606", + h: "0004 022224", + i: "1214 1011", + j: "121606 1011", + k: "0004 221303 1324", + l: "1014", + m: "04022224 1214", + n: "04022224", + o: "0222240402", + p: "0602222404", + q: "2622020424", + r: "040222", + s: "220203232404", + t: "1114 0222 1424", + u: "02042422", + v: "0203142322", + w: "0204132422", + x: "0224 2204", + y: "020424 222606", + z: "02220424", +}; + +// A path string to directed quanta [from point, to point] in pen order. +function parse(src) { + const out = []; + for (const path of src.split(" ")) + for (let i = 2; i + 1 < path.length; i += 2) { + let c = +path[i - 2], r = +path[i - 1]; + const c1 = +path[i], r1 = +path[i + 1]; + const sc = Math.sign(c1 - c), sr = Math.sign(r1 - r); + for (let n = max(abs(c1 - c), abs(r1 - r)); n > 0; n--) { + out.push([r * GC + c, (r + sr) * GC + c + sc]); + c += sc; + r += sr; + } + } + return out; +} + +const col = (p) => p % GC, row = (p) => floor(p / GC); +const other = (s, p) => (s[0] === p ? s[1] : s[0]); + +// Style rules. Each takes a letter's directed quanta and gives new ones, the +// same rule for every letter, which is what Hofstadter meant by a gridfont +// having one spirit. + +// Cut corners. Where exactly two quanta meet at a point, one across and one +// down, both go and the diagonal across the corner takes their place. The +// mask says which corners of a box: 1 top left, 2 top right, 4 bottom left, +// 8 bottom right. A quantum is used by one corner at most. +function cut(list, mask) { + const at = Array.from({ length: GP }, () => []); + list.forEach((s, i) => (at[s[0]].push(i), at[s[1]].push(i))); + const out = list.slice(), gone = new Uint8Array(list.length); + list.forEach((s, i) => { + for (const p of [s[1], s[0]]) { + if (gone[i] || at[p].length !== 2) continue; + const j = at[p][0] === i ? at[p][1] : at[p][0]; + if (gone[j]) continue; + const a = other(s, p), b = other(list[j], p); + const horiz = (q) => row(q) === row(p), vert = (q) => col(q) === col(p); + let hq, vq; + if (horiz(a) && vert(b)) (hq = a), (vq = b); + else if (vert(a) && horiz(b)) (hq = b), (vq = a); + else continue; + const east = col(hq) > col(p), south = row(vq) > row(p); + const bit = east ? (south ? 1 : 4) : south ? 2 : 8; + if (!(mask & bit)) continue; + out[i] = s[1] === p ? [a, b] : [b, a]; + gone[i] = gone[j] = 1; + out[j] = null; + } + }); + return out.filter(Boolean); +} + +// Gables. A full bar at the x-height whose both ends go on down becomes a +// roof peaked one square higher; a full bar on the baseline whose both ends +// come up becomes a keel one square lower. A stroke that met the middle of +// the bar is carried on to the peak or the keel. +function gable(list) { + const out = list.map((s) => s.slice()); + const has = (a, b) => out.findIndex((s) => (s[0] === a && s[1] === b) || (s[0] === b && s[1] === a)); + const goes = (p, dr) => out.some((s) => (s[0] === p || s[1] === p) && row(other(s, p)) === row(p) + dr); + for (const [r, dr] of [[2, 1], [4, -1]]) { + const L = r * GC, M = L + 1, R = L + 2, apex = M - dr * GC; + const i = has(L, M), j = has(M, R); + if (i < 0 || j < 0 || !goes(L, dr) || !goes(R, dr)) continue; + const meets = out.some((s, k) => k !== i && k !== j && (s[0] === M || s[1] === M)); + out[i] = out[i].map((p) => (p === M ? apex : p)); + out[j] = out[j].map((p) => (p === M ? apex : p)); + if (meets) out.push(dr > 0 ? [apex, M] : [M, apex]); + } + return out; +} + +// Six styles. cut is a corner mask for cut(), gable says gable() runs, gap +// opens every quantum away from the points it joins so each one stands +// alone as in a stencil, ring draws a stroke as its outline, and hue is the +// style's step off the Hue knob. +const STYLES = [ + { name: "Square", hue: 0, cut: 0, gable: 0, gap: 0, ring: 0 }, + { name: "Cut", hue: 0.06, cut: 15, gable: 0, gap: 0, ring: 0 }, + { name: "Lean", hue: -0.06, cut: 1 | 8, gable: 0, gap: 0, ring: 0 }, + { name: "Gable", hue: 0.12, cut: 0, gable: 1, gap: 0, ring: 0 }, + { name: "Stencil", hue: -0.12, cut: 0, gable: 0, gap: 1, ring: 0 }, + { name: "Outline", hue: 0.18, cut: 0, gable: 0, gap: 0, ring: 1 }, +]; +const NS = STYLES.length; + +const idOf = (s) => QID[s[0] * GP + s[1]]; +const keyOf = (l) => l.map(idOf).sort((a, b) => a - b).join(); +const bits = (m) => (m & 1) + ((m >> 1) & 1) + ((m >> 2) & 1) + ((m >> 3) & 1); + +// Every letter in every style, as directed quanta with no quantum twice. +const FACES = STYLES.map((st) => { + const make = (ch, mask) => { + let l = parse(BASE[ch]); + if (mask) l = cut(l, mask); + if (st.gable) l = gable(l); + const seen = new Uint8Array(NQ); + return l.filter((s) => { + const id = idOf(s); + if (id < 0 || seen[id]) return false; + return (seen[id] = 1); + }); + }; + const face = {}; + for (const ch in BASE) face[ch] = make(ch, st.cut); + // The letter comes before the style. Cut makes u the same quanta as v, so + // a letter the rule turns into another letter backs off, cutting as many + // of its corners as it can and still be itself. + const clash = (ch) => Object.keys(face).some((o) => o !== ch && keyOf(face[o]) === keyOf(face[ch])); + for (const ch in BASE) { + if (!st.cut || !clash(ch) || keyOf(face[ch]) === keyOf(parse(BASE[ch]))) continue; + const masks = []; + for (let m = st.cut - 1; m >= 0; m--) if ((m & st.cut) === m) masks.push(m); + masks.sort((a, b) => bits(b) - bits(a)); + for (const m of masks) if (((face[ch] = make(ch, m)), !clash(ch))) break; + } + return face; +}); + +// Pages of three three-letter lines. The alphabet pages are the specimen +// sheet every gridfont was shown on. +const PAGES = ["the", "sly", "fox", "abc", "def", "ghi", "web", "zig", "zag", "jkl", "mno", "pqr", "sea", "and", "sky", "stu", "vwx", "yz ", "art", "for", "all"] + .join("") + .match(/.{9}/g); + +// Timing in seconds at Speed 2: a change every BEAT; quanta that leave take +// back over BACK, those that come draw over DRAW one STEP apart in pen order, +// after the leaving is done; each letter starts LAG after the one before. +const BEAT = 4, BACK = 0.3, DRAW = 0.2, STEP = 0.09, LAG = 0.06, CLEAR = 0.4; + +// Stroke profile against distance from the stroke's middle line, 64 steps +// a LED: a 1 LED core, a shoulder to 1.25 LEDs and a glow that dies by 3.5. +const PROF = new Float32Array(256); +for (let k = 0; k < 256; k++) { + const d = k / 64; + PROF[k] = d < 0.55 ? 1 : d < 1.25 ? 1 - ((d - 0.55) / 0.7) * 0.55 : 0.45 * exp(-(d - 1.25) * 1.35); +} +const REACH = 3.5; +// The outline style lights the edge of a stroke this far out. +const RING = 2.1; + +export function setup(params) { + // The lattice points, and every quantum of every cell as a one-LED line. + const dot = new Uint8Array(N), mesh = new Uint8Array(N); + for (let gy = 0; gy <= 18; gy++) for (let gx = 0; gx < 9; gx++) dot[gy * SQ * W + X0 + gx * SQ] = 1; + for (let c = 0; c < CELLS; c++) { + const ox = X0 + (c % 3) * PITCH, oy = floor(c / 3) * LINE; + for (let id = 0; id < NQ; id++) { + const ax = ox + col(QA[id]) * SQ, ay = oy + row(QA[id]) * SQ; + const sx = col(QB[id]) - col(QA[id]), sy = row(QB[id]) - row(QA[id]); + for (let k = 1; k < SQ; k++) { + const x = ax + sx * k, y = ay + sy * k; + if (y < H) mesh[y * W + x] = 1; + } + } + } + const Q = CELLS * NQ; + params.s = { + dot, mesh, D: new Float32Array(N), B: new Float32Array(N), Wh: new Float32Array(N), + kind: new Uint8Array(Q), t0: new Float32Array(Q), dir: new Uint8Array(Q), ord: new Uint8Array(Q), + T: 0, next: BEAT, page: 0, style: 0, knob: -1, flip: 0, ready: false, + from: STYLES[0], to: STYLES[0], styleT: -9, hue: 0, grid: 0.06, + }; +} + +// Send the current page in the current style to the cells. Quanta the old +// and the new letter share stay lit; the rest take back or draw in. kind is +// 0 dark, 1 drawing or drawn, 2 taking back. +function show(S, first) { + const text = PAGES[S.page], face = FACES[S.style]; + const want = new Int16Array(NQ); + for (let c = 0; c < CELLS; c++) { + want.fill(-1); + const list = face[text[c]] || []; + list.forEach((s, j) => (want[idOf(s)] = j)); + for (let id = 0; id < NQ; id++) { + const k = c * NQ + id; + if (S.kind[k] !== 1 || want[id] >= 0) continue; + // One that has not begun to draw just never comes. + if (S.t0[k] > S.T) S.kind[k] = 0; + else (S.kind[k] = 2), (S.t0[k] = S.T + c * LAG); + } + let n = 0; + const start = S.T + c * LAG + (first ? 0 : CLEAR); + list.forEach((s, j) => { + const id = idOf(s), k = c * NQ + id; + S.dir[k] = s[0] === QA[id] ? 0 : 1; + S.ord[k] = j; + if (S.kind[k] === 1) return; + S.kind[k] = 1; + S.t0[k] = start + n++ * STEP; + }); + } +} + +const blend = (S) => { + const e = smooth((S.T - S.styleT) / 0.9), f = S.from, t = S.to; + return { hue: f.hue + (t.hue - f.hue) * e, gap: f.gap + (t.gap - f.gap) * e, ring: f.ring + (t.ring - f.ring) * e }; +}; + +function restyle(S, style) { + // The blend starts from wherever the last one had got to. + S.from = blend(S); + S.to = STYLES[style]; + S.style = style; + S.styleT = S.T; +} + +export function update(dt, input, params) { + const S = params.s; + const k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.grid = cl(k[3], 0, 1); + const want = cl(round(k[2]), 0, NS); + if (!S.ready) { + S.ready = true; + S.knob = want; + S.style = want > 0 ? want - 1 : 0; + S.from = S.to = STYLES[S.style]; + show(S, true); + } else if (want !== S.knob) { + S.knob = want; + if (want > 0 && want - 1 !== S.style) { + restyle(S, want - 1); + show(S, false); + } + } + S.T += (dt * cl(k[1], 0.05, 3)) / 2; + while (S.T >= S.next) { + S.next += BEAT; + // Held: new words each beat. Taking turns: a new style on the same + // words, then new words in that style. + if (S.knob === 0 && (S.flip ^= 1)) restyle(S, (S.style + 1) % NS); + else S.page = (S.page + 1) % PAGES.length; + show(S, false); + } +} + +// One stroke from (x0, y0) to (x1, y1), its pen position running s0 to s1. +// Each LED keeps the distance to the nearest stroke (so the outline of +// strokes that meet is one outline) and how near that stroke's stretch is +// to the light running along the pen path at P. +function stroke(S, x0, y0, x1, y1, s0, s1, P, fade) { + const { D, B } = S; + const bx0 = max(0, floor(min(x0, x1) - REACH)), bx1 = min(W - 1, ceil(max(x0, x1) + REACH)); + const by0 = max(0, floor(min(y0, y1) - REACH)), by1 = min(H - 1, ceil(max(y0, y1) + REACH)); + const dx = x1 - x0, dy = y1 - y0, L2 = dx * dx + dy * dy || 1e-6; + for (let y = by0; y <= by1; y++) + for (let x = bx0; x <= bx1; x++) { + const t = cl(((x - x0) * dx + (y - y0) * dy) / L2, 0, 1); + const ex = x - x0 - t * dx, ey = y - y0 - t * dy; + const d = sqrt(ex * ex + ey * ey) + fade, p = y * W + x; + if (d >= D[p]) continue; + D[p] = d; + const u = s0 + (s1 - s0) * t - P; + B[p] = u * u < 9 ? exp(-u * u * 1.2) : 0; + } +} + +// The pen's head while a quantum draws: a small white spark. +function spark(Wh, x, y, lv) { + for (let yy = max(0, floor(y - 2)); yy <= min(H - 1, ceil(y + 2)); yy++) + for (let xx = max(0, floor(x - 2)); xx <= min(W - 1, ceil(x + 2)); xx++) { + const d2 = (xx - x) * (xx - x) + (yy - y) * (yy - y); + const v = lv * exp(-d2 / 1.1), p = yy * W + xx; + if (v > Wh[p]) Wh[p] = v; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S.ready) return; + const { D, B, Wh, kind, t0, dir, ord, T } = S; + D.fill(REACH); + B.fill(0); + Wh.fill(0); + const { hue, gap, ring } = blend(S); + for (let c = 0; c < CELLS; c++) { + const ox = X0 + (c % 3) * PITCH, oy = floor(c / 3) * LINE; + // A light runs along each letter's pen path, a quantum in 0.3 s, the + // letters a little apart in reading order. + const P = ((T - c * 0.23) * 3.3) % 18 - 2; + for (let id = 0; id < NQ; id++) { + const k = c * NQ + id, kd = kind[k]; + if (!kd) continue; + const tt = T - t0[k]; + let u0 = 0, u1 = 1, fade = 0, head = 0; + if (kd === 1) { + const p = tt / DRAW; + if (p <= 0) continue; + if (p < 1) (u1 = p), (head = 1 - p * 0.5); + } else { + const q = tt / BACK; + if (q >= 1) continue; + if (q > 0) (u0 = q), (fade = 1.2 * q); + } + let a = QA[id], b = QB[id]; + if (dir[k]) [a, b] = [b, a]; + const ax = ox + col(a) * SQ, ay = oy + row(a) * SQ; + const dx = (col(b) - col(a)) * SQ, dy = (row(b) - row(a)) * SQ; + // Stencil: pull both ends back from the points. + const g = (gap * 1.8) / sqrt(dx * dx + dy * dy); + u0 = max(u0, g); + u1 = min(u1, 1 - g); + if (u1 <= u0) continue; + const o = ord[k]; + stroke(S, ax + dx * u0, ay + dy * u0, ax + dx * u1, ay + dy * u1, o + u0, o + u1, P, fade); + if (head > 0) spark(Wh, ax + dx * u1, ay + dy * u1, head); + } + } + const [lr, lg, lb] = hsv(0.42 + S.hue + hue, 0.72, 1); + const [gr, gg, gb] = hsv(0.36 + S.hue + hue, 0.35, 1); + const G = S.grid; + const dotL = 44 + 60 * min(1, G * 3), meshL = 78 * max(0, (G - 0.08) / 0.92); + const rc = RING * ring; + const { dot, mesh } = S; + for (let p = 0; p < N; p++) { + const d = D[p]; + let v = 0, w = Wh[p]; + if (d < REACH) { + const k = (abs(d - rc) * 64) | 0; + v = k < 256 ? PROF[k] : 0; + const b = B[p]; + w += 0.3 * b * v; + v *= 0.7 + 0.3 * b; + } + const base = (dot[p] ? dotL : mesh[p] ? meshL : 0) * (1 - v); + // The middle of a lit stroke runs a little toward white. + const hot = v > 0.75 ? (v - 0.75) * 1.2 : 0; + display.setPixel( + p % W, + (p / W) | 0, + min(255, 255 * (lr * v + (1 - lr) * hot + w) + gr * base), + min(255, 255 * (lg * v + (1 - lg) * hot + w) + gg * base), + min(255, 255 * (lb * v + (1 - lb) * hot + w) + gb * base), + ); + } +} From 5cf33a1df3c5d284f3560a31e11b3e71ea16678b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:45:19 +0000 Subject: [PATCH 08/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Hodograph?= 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_01Gdqi6PeQBKagUB2vLqwQZR --- incoming/pattern-study/patterns/hodograph.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/incoming/pattern-study/patterns/hodograph.js b/incoming/pattern-study/patterns/hodograph.js index 30cda0d22..4c4550f9b 100644 --- a/incoming/pattern-study/patterns/hodograph.js +++ b/incoming/pattern-study/patterns/hodograph.js @@ -14,7 +14,7 @@ // Knob 1: Hue. Turns the bodies' colours round the wheel together: at 0 the first three are cyan, magenta and green and the fourth to sixth azure, rose and yellow, while the Sun stays pale gold and the origin of the velocities white. // Knob 2: Speed. How fast time runs. At 1 the three opening bodies go round in 3.2, 5.3 and 7.1 seconds, the period growing as the 3/2 power of the semi-major axis; at the opening 2 they take 1.6, 2.6 and 3.5 seconds. // Knob 3: Eccentricity. The eccentricity of the most eccentric body, with the others spread evenly below it: 0.2, 0.4 and 0.6 at the opening 0.6. At 0.95 that body moves 39 times faster at periapsis than at apoapsis, its hodograph is the largest circle with the origin 0.05 of a radius inside its rim, and the other hodographs shrink round the origin. -// Knob 4: Bodies. Three to six bodies, their apoapses spread evenly from 13 to 29.5 units and their apse lines turned 137.5 degrees (the golden angle) from one another; each half of the panel is fitted to the set again. +// Knob 4: Bodies. Three to six bodies, their eccentricities spread evenly up to Knob 3's (six at 0.6 run 0.1, 0.2 ... 0.6), their apoapses from 13 to 29.5 units and their apse lines turned 137.5 degrees (the golden angle) from one another; each half of the panel is fitted to the new set. const W = 64, H = 128; const { floor, min, max, abs, sqrt, sin, cos, atan2, PI, round, hypot } = Math; From f9f514a13b7618c23bea9df82163386660ee109e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:45:28 +0000 Subject: [PATCH 09/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Etaoin=20Shrdlu?= 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_01Gdqi6PeQBKagUB2vLqwQZR --- .../pattern-study/patterns/etaoin-shrdlu.js | 461 ++++++++++++++++++ 1 file changed, 461 insertions(+) create mode 100644 incoming/pattern-study/patterns/etaoin-shrdlu.js diff --git a/incoming/pattern-study/patterns/etaoin-shrdlu.js b/incoming/pattern-study/patterns/etaoin-shrdlu.js new file mode 100644 index 000000000..007d59824 --- /dev/null +++ b/incoming/pattern-study/patterns/etaoin-shrdlu.js @@ -0,0 +1,461 @@ +// ===== Patternflow pattern ===== +// Title: Etaoin Shrdlu +// Family: data +// Idea: A passage set in a 3 by 5 face comes loose one letter at a time from its last line, and each letter falls into one of sixteen columns kept sorted by how often the letter has come, so the columns trade places as one overtakes another and the labels under them end up spelling the passage's own etaoin shrdlu above the order the Linotype keyboard was built on. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: pattern round 66, after the Linotype (Ottmar Mergenthaler, 1886), whose lowercase keys ran etaoin shrdlu cmfwyp vbgkqj by letter frequency, and the line ETAOIN SHRDLU that operators left in newspapers; counting Lewis Carroll's Alice's Adventures in Wonderland (1865), Lorem ipsum from Cicero's De finibus bonorum et malorum (45 BC), ten pangrams, and the cryptogram Legrand breaks by counting in Edgar Allan Poe's The Gold-Bug (1843); the 3 by 5 face from the Typewriter pattern (round 18), with figures and Poe's symbols added +// Round: 66 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Text=0..3, Forget=0..1 +// +// Knob 1: Hue. Turns the colours round the wheel. Each letter keeps one colour, set by its place in the Linotype's order: at 0 E is cyan, the rest of ETAOIN blue, SHRDLU indigo to magenta and the rare letters red, orange and gold, so a passage close to that order reads as one rainbow across the columns and Lorem ipsum as a broken one. +// Knob 2: Speed. Letters coming loose a second: 8 at 2, so Alice's 464 letters take 58 s; 0.8 at 0.2 and 12 at 3. They also fall a little harder as it turns up. +// Knob 3: Text. Each ends on its own order: 0 Alice, ETIONS AHRDWG, six of the twelve in the Linotype's places; 1 Lorem ipsum, IETAOU NLRDSM, two; 2 ten pangrams, every letter 10 times or more and 17 of them exactly 10, none; 3 Poe's cryptogram, 8;4)‡* 56(†10, which is ETHSON AIRDFL, five. +// Knob 4: Forget. How far back the columns count: the last 520 letters at 0.06, which is all of every passage here, 113 at 0.5 and only the last 20 at 1, where each letter is a sort 15 rows tall with its face on it and the order changes at 80 of 100 landings against 18 at 0.06. + +const W = 64, H = 128; +const { floor, round, min, max, exp, sqrt, sin, abs } = 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); +}; + +// Layout, rows y down. Seven lines of type at the top (6 rows each), the +// reading line last; sixteen columns 4 LEDs apart standing on row 113 and at +// most 62 rows tall; the passage's labels on rows 116-120 and the +// Linotype's order on rows 122-126. +const LINES = 7; +const READ_Y = 37; +const BASE = 113; +const MAXH = 62; +const LABEL_Y = 116; +const REF_Y = 122; +const SLOTS = 16; +// Under floor(S) letters in the tallest column the scale stops growing, so +// early in a passage each letter is a sort 62 / 9 = 6.9 rows tall with its +// face on it. Counting only the last 20, the floor drops to 4 and a sort is +// 15.5 rows. +const floorUnits = (S) => cl(S.keep / 5, 4, 9); +const HOLD = 6; +const DRAIN = 1.2; + +// The Linotype's lowercase keys, column by column. +const LINO = "ETAOINSHRDLUCMFWYPVBGKQJXZ"; + +// The 3 by 5 face, rows top to bottom, one bit per column. The letters and +// most punctuation are Typewriter's; the figures and Poe's dagger, double +// dagger, pilcrow, asterisk and brackets are new. +const GLYPHS = { + A: "010101111101101", B: "110101110101110", C: "011100100100011", D: "110101101101110", + E: "111100110100111", F: "111100110100100", G: "011100101101011", H: "101101111101101", + I: "111010010010111", J: "001001001101010", K: "101101110101101", L: "100100100100111", + M: "101111111101101", N: "110101101101101", O: "010101101101010", P: "110101110100100", + Q: "010101101111011", R: "110101110101101", S: "011100010001110", T: "111010010010010", + U: "101101101101011", V: "101101101010010", W: "101101111111101", X: "101101010101101", + Y: "101101010010010", Z: "111001010100111", ".": "000000000000010", ",": "000000000010100", + "-": "000000111000000", "!": "010010010000010", "?": "110001010000010", "'": "010010000000000", + ":": "000010000010000", ";": "000010000010100", + "0": "111101101101111", "1": "010110010010111", "2": "111001111100111", "3": "111001011001111", + "4": "101101111001001", "5": "111100111001111", "6": "111100111101111", "8": "111101111101111", + "9": "111101111001111", "†": "010111010010010", "‡": "010111010111010", "¶": "011111011011011", + "*": "101010111010101", "(": "001010010010001", ")": "100010010010100", +}; +const BITS = {}; +for (const ch in GLYPHS) BITS[ch] = GLYPHS[ch].split("").map(Number); + +// Poe's key, letter to symbol, as Legrand reads it out: 8 is e, ; is t, and +// so on. The cryptogram is the plaintext below put through it, so the counts +// are the plaintext's: 8 33, ; 26, 4 19, the start of Legrand's own table. +const KEY = { A: "5", B: "2", C: "-", D: "†", E: "8", F: "1", G: "3", H: "4", I: "6", L: "0", M: "9", N: "*", O: "‡", P: ".", R: "(", S: ")", T: ";", U: "?", V: "¶", Y: ":" }; +const PLAIN = + "A GOOD GLASS IN THE BISHOP'S HOSTEL IN THE DEVIL'S SEAT FORTY-ONE DEGREES AND THIRTEEN MINUTES NORTHEAST AND BY NORTH MAIN BRANCH SEVENTH LIMB EAST SIDE SHOOT FROM THE LEFT EYE OF THE DEATH'S-HEAD A BEE LINE FROM THE TREE THROUGH THE SHOT FIFTY FEET OUT"; + +const TEXTS = [ + // Alice's Adventures in Wonderland (1865), the first two sentences. + "ALICE WAS BEGINNING TO GET VERY TIRED OF SITTING BY HER SISTER ON THE BANK, AND OF HAVING NOTHING TO DO: ONCE OR TWICE SHE HAD PEEPED INTO THE BOOK HER SISTER WAS READING, BUT IT HAD NO PICTURES OR CONVERSATIONS IN IT, 'AND WHAT IS THE USE OF A BOOK,' THOUGHT ALICE 'WITHOUT PICTURES OR CONVERSATIONS?' SO SHE WAS CONSIDERING IN HER OWN MIND (AS WELL AS SHE COULD, FOR THE HOT DAY MADE HER FEEL VERY SLEEPY AND STUPID), WHETHER THE PLEASURE OF MAKING A DAISY-CHAIN WOULD BE WORTH THE TROUBLE OF GETTING UP AND PICKING THE DAISIES, WHEN SUDDENLY A WHITE RABBIT WITH PINK EYES RAN CLOSE BY HER.", + "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT, SED DO EIUSMOD TEMPOR INCIDIDUNT UT LABORE ET DOLORE MAGNA ALIQUA. UT ENIM AD MINIM VENIAM, QUIS NOSTRUD EXERCITATION ULLAMCO LABORIS NISI UT ALIQUIP EX EA COMMODO CONSEQUAT. DUIS AUTE IRURE DOLOR IN REPREHENDERIT IN VOLUPTATE VELIT ESSE CILLUM DOLORE EU FUGIAT NULLA PARIATUR. EXCEPTEUR SINT OCCAECAT CUPIDATAT NON PROIDENT, SUNT IN CULPA QUI OFFICIA DESERUNT MOLLIT ANIM ID EST LABORUM.", + "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG. PACK MY BOX WITH FIVE DOZEN LIQUOR JUGS. SPHINX OF BLACK QUARTZ, JUDGE MY VOW. JACKDAWS LOVE MY BIG SPHINX OF QUARTZ. HOW VEXINGLY QUICK DAFT ZEBRAS JUMP! THE FIVE BOXING WIZARDS JUMP QUICKLY. WALTZ, BAD NYMPH, FOR QUICK JIGS VEX. GLIB JOCKS QUIZ NYMPH TO VEX DWARF. QUICK ZEPHYRS BLOW, VEXING DAFT JIM. TWO DRIVEN JOCKS HELP FAX MY BIG QUIZ.", + null, +]; + +// A passage as lines of at most 16 characters and the list of letters to +// count, each with its line, column, token (what it looks like) and key (the +// letter it stands for, which sets its colour). +function prepare(t) { + const lines = []; + if (t === 3) { + // Poe printed the cryptogram run together, so it breaks every 16. + let s = ""; + for (const ch of PLAIN) if (KEY[ch]) s += KEY[ch]; + for (let i = 0; i < s.length; i += 16) lines.push(s.slice(i, i + 16)); + } else { + let line = ""; + for (const word of TEXTS[t].split(" ")) { + if (line && line.length + 1 + word.length > 16) { + lines.push(line); + line = word; + } else line = line ? line + " " + word : word; + } + lines.push(line); + } + const back = {}; + for (const k in KEY) back[KEY[k]] = k; + const letters = []; + lines.forEach((s, li) => { + for (let c = 0; c < s.length; c++) { + const ch = s[c]; + if (t === 3 ? back[ch] : ch >= "A" && ch <= "Z") letters.push({ tok: ch, key: t === 3 ? back[ch] : ch, line: li, col: c }); + } + }); + return { lines, letters, cipher: t === 3 }; +} + +function hsv(h, s, v) { + h = (((h % 1) + 1) % 1) * 6; + const i = floor(h), f = h - i; + const p = v * (1 - s), q = v * (1 - s * f), u = v * (1 - s * (1 - f)); + const c = [[v, u, p], [q, v, p], [p, v, u], [p, q, v], [u, p, v], [v, p, q]][i % 6]; + return [c[0] * 255, c[1] * 255, c[2] * 255]; +} + +// A letter's colour: its key's place in the Linotype's order round the wheel. +const colourOf = (S, tok) => hsv(0.5 + S.hue + LINO.indexOf(S.keyOf[tok] || tok) * 0.031, 0.78, 1); + +export function setup(params) { + params.s = { started: false }; +} + +// Start a passage: no counts, no columns, its first line onto the galley. +function begin(S, t) { + S.textIdx = t; + S.text = prepare(t); + S.read = 0; + S.line = -1; + S.cnt = {}; + S.keyOf = {}; + for (const L of S.text.letters) S.keyOf[L.tok] = L.key; + S.cols = []; + S.cx = {}; + S.flash = {}; + S.queue = []; + S.head = 0; + S.fall = []; + S.acc = 0; + S.phase = 0; + S.pt = 0; + S.drain = 1; + feed(S, 0, false); +} + +// Put passage lines up to li onto the galley; it scrolls up a line for each. +function feed(S, li, still) { + while (S.line < li) { + S.line++; + S.galley.push({ s: S.text.lines[S.line], slug: null, li: S.line }); + if (!still) S.scroll += 6; + } + if (S.galley.length > 12) S.galley.splice(0, S.galley.length - 12); +} + +const slotX = (i) => i * 4; + +function bubbleLeft(S, i) { + const c = S.cols, n = S.cnt; + while (i > 0 && n[c[i]] > n[c[i - 1]]) { + const a = c[i]; + c[i] = c[i - 1]; + c[i - 1] = a; + i--; + } +} + +// The letter not in a column with the highest count, if it beats the last column. +function promote(S) { + const c = S.cols; + let best = null; + for (const k in S.cnt) if (S.cnt[k] > 0 && !c.includes(k) && (best === null || S.cnt[k] > S.cnt[best])) best = k; + if (best === null) return; + if (c.length < SLOTS) { + c.push(best); + if (S.cx[best] === undefined) S.cx[best] = slotX(c.length - 1); + bubbleLeft(S, c.length - 1); + } else if (S.cnt[best] > S.cnt[c[SLOTS - 1]]) { + S.cx[best] = S.cx[c[SLOTS - 1]]; + c[SLOTS - 1] = best; + bubbleLeft(S, SLOTS - 1); + } +} + +// A letter has landed: count it, forget the oldest past the keep, re-sort. +function land(S, tok) { + S.cnt[tok] = (S.cnt[tok] || 0) + 1; + S.queue.push(tok); + const i = S.cols.indexOf(tok); + if (i >= 0) { + bubbleLeft(S, i); + S.flash[tok] = 1; + } else promote(S); + trim(S); +} + +function trim(S) { + while (S.queue.length - S.head > S.keep) { + const old = S.queue[S.head++]; + S.cnt[old]--; + const c = S.cols; + let i = c.indexOf(old); + if (i >= 0) { + if (S.cnt[old] <= 0) c.splice(i, 1); + else + while (i < c.length - 1 && S.cnt[c[i]] < S.cnt[c[i + 1]]) { + c[i] = c[i + 1]; + c[i + 1] = old; + i++; + } + } + promote(S); + } + if (S.head > 2000) { + S.queue = S.queue.slice(S.head); + S.head = 0; + } +} + +const tallest = (S) => { + let m = 0; + for (const k of S.cols) m = max(m, S.cnt[k]); + return m; +}; + +// One step of the machine. +function step(S, dt) { + const L = S.text.letters; + S.scroll = max(0, S.scroll - max(S.scroll * (1 - exp(-dt * 7)), dt * 6)); + if (S.phase === 0) { + S.acc += dt * S.rate; + while (S.acc >= 1 && S.read < L.length) { + S.acc -= 1; + const l = L[S.read++]; + // The slot in the reading line before the scroll moves it. + const y0 = READ_Y + S.scroll - 6 * (S.line - l.line); + // While there are free columns a new letter takes one as it leaves. + if (S.cols.length < SLOTS && !S.cols.includes(l.tok)) { + S.cnt[l.tok] = S.cnt[l.tok] || 0; + S.cols.push(l.tok); + if (S.cx[l.tok] === undefined) S.cx[l.tok] = slotX(S.cols.length - 1); + } + S.fall.push({ tok: l.tok, x: l.col * 4, x0: l.col * 4, y: y0, y0, t: 0, T: 0 }); + if (S.read < L.length) feed(S, L[S.read].line, false); + } + if (S.read >= L.length) S.acc = 0; + } + for (const f of S.fall) { + f.t += dt; + if (!f.T) { + const i = S.cols.indexOf(f.tok); + const top = i >= 0 ? BASE - S.cnt[f.tok] * S.scale - 4 : BASE - 4; + const d = max(1, top - f.y0); + f.T = (S.vy + sqrt(S.vy * S.vy + 2 * S.g * d)) / S.g; + } + f.y = f.y0 - S.vy * f.t + 0.5 * S.g * f.t * f.t; + const i = S.cols.indexOf(f.tok); + f.pi = i < 0; + if (!f.pi) { + const u = sm(0, 1, f.t / f.T); + f.x = f.x0 + (S.cx[f.tok] - f.x0) * u; + if (f.y >= BASE - S.cnt[f.tok] * S.scale - 4) f.done = true; + } else { + f.x0 = f.x; + if (f.y >= BASE - 4) f.done = true; + } + } + if (S.fall.some((f) => f.done)) { + for (const f of S.fall) if (f.done) land(S, f.tok); + S.fall = S.fall.filter((f) => !f.done); + } + // Columns slide to their slots; the scale follows the tallest. + S.cols.forEach((k, i) => { + S.cx[k] += (slotX(i) - S.cx[k]) * (1 - exp(-dt * 9)); + }); + for (const k in S.flash) S.flash[k] = max(0, S.flash[k] - dt * 3); + const target = MAXH / max(floorUnits(S), tallest(S)); + S.scale += (target - S.scale) * (1 - exp(-dt * 5)); + + if (S.phase === 0 && S.read >= L.length && S.fall.length === 0) { + // The passage is spent: cast its own order as a line. + S.phase = 1; + S.pt = 0; + const c = S.cols; + const s = c.slice(0, 6).join("") + " " + c.slice(6, 12).join(""); + S.galley.push({ s, slug: s.split("").map((ch) => (ch === " " ? null : colourOf(S, ch))), li: -1 }); + S.scroll += 6; + } else if (S.phase === 1) { + S.pt += dt; + if (S.pt > HOLD) { + S.phase = 2; + S.pt = 0; + } + } else if (S.phase === 2) { + S.pt += dt; + S.drain = 1 - sm(0, DRAIN, S.pt); + if (S.pt > DRAIN) begin(S, S.next); + } +} + +export function update(dt, input, params) { + const S = params.s; + const k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + const speed = cl(k[1] === undefined ? 2 : k[1], 0.2, 3); + S.rate = 4 * speed; + S.g = 150 * (0.6 + 0.2 * speed); + S.vy = 18; + const tx = cl(round(k[2] || 0), 0, 3); + S.keep = round(20 * 32 ** (1 - cl(k[3] === undefined ? 0.06 : k[3], 0, 1))); + S.next = tx; + if (!S.started) { + S.started = true; + S.galley = []; + S.scroll = 0; + S.scale = MAXH / floorUnits(S); + begin(S, tx); + // Read 28% of the passage (50 to 130 letters) before the first frame, + // then run a second and a half so letters are in the air. + const L = S.text.letters; + const pre = cl(round(0.28 * L.length), 50, 130); + for (let i = 0; i < pre; i++) { + const l = L[S.read++]; + if (S.read < L.length) feed(S, L[S.read].line, true); + land(S, l.tok); + } + S.scroll = 0; + S.cols.forEach((c, i) => (S.cx[c] = slotX(i))); + S.scale = MAXH / max(floorUnits(S), tallest(S)); + for (const c in S.flash) S.flash[c] = 0; + for (let i = 0; i < 45; i++) step(S, 1 / 30); + } + if (tx !== S.textIdx && S.phase !== 2) { + // A new text: the columns drain and the new passage starts from its top. + S.phase = 2; + S.pt = 0; + S.fall = []; + } + trim(S); + step(S, dt); +} + +function glyph(display, ch, x0, y0, r, g, b, a) { + const bits = BITS[ch]; + if (!bits || a <= 0.02) return; + const R = round(min(255, r * a)), G = round(min(255, g * a)), B = round(min(255, b * a)); + for (let j = 0; j < 15; j++) { + if (!bits[j]) continue; + const x = x0 + (j % 3), y = y0 + floor(j / 3); + if (x >= 0 && x < W && y >= 0 && y < H) display.setPixel(x, y, R, G, B); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S.started) return; + const colour = {}; + const col = (tok) => colour[tok] || (colour[tok] = colourOf(S, tok)); + const pulse = S.phase === 1 ? 0.8 + 0.2 * sin(S.pt * 5) : 1; + + // Letters with no column fall behind the columns and go out at the floor. + for (const f of S.fall) if (f.pi) glyph(display, f.tok, round(f.x), round(f.y), 150, 150, 160, 0.75); + + // The columns: a solid bar with a seam at each letter, or, when a letter + // has room, a stack of sorts each showing its letter in the middle. + const s = S.scale; + const sorts = sm(5, 6.5, s); + const face = floor(max(0, s - 6) / 2); + for (const tok of S.cols) { + const c = col(tok); + const x0 = round(S.cx[tok]); + const n = S.cnt[tok]; + const h = n * s * S.drain; + const bits = BITS[tok]; + const fl = S.flash[tok] || 0; + for (let r = 0; r < h && r < MAXH + 2; r++) { + const y = BASE - r; + const cover = min(1, h - r); + const unit = floor(r / s), o = r - unit * s; + const top = unit >= n - 1; + for (let dx = 0; dx < 3; dx++) { + const seam = s >= 2.5 && o > s - 1; + const solid = seam ? 0.42 : 0.9; + const gr = 4 - floor(o - face); + const stamp = seam ? 0 : gr >= 0 && gr < 5 ? (bits[gr * 3 + dx] ? 1 : 0.28) : 0.28; + const v = (solid + (stamp - solid) * sorts) * cover; + if (v <= 0.02) continue; + const w = top ? fl * 0.8 : 0; + const R = c[0] * v * (1 - w) + 255 * w * cover, G = c[1] * v * (1 - w) + 255 * w * cover, B = c[2] * v * (1 - w) + 255 * w * cover; + const x = x0 + dx; + if (x >= 0 && x < W) display.setPixel(x, y, round(min(255, R)), round(min(255, G)), round(min(255, B))); + } + } + } + + // Labels under the columns, and under them the Linotype's order. Where a + // column's letter stands where the Linotype put it, both light. At the end + // of Poe's cryptogram the bottom row turns into the key. + const keyRow = S.text.cipher && S.phase > 0; + S.cols.forEach((tok) => { + const c = col(tok); + const x = round(S.cx[tok]); + glyph(display, tok, x, LABEL_Y, c[0], c[1], c[2], pulse * S.drain); + }); + for (let i = 0; i < SLOTS; i++) { + const tok = S.cols[i]; + const settled = tok !== undefined && abs(S.cx[tok] - slotX(i)) < 0.6; + const match = settled && S.keyOf[tok] === LINO[i]; + if (keyRow) { + if (tok === undefined) continue; + const c = col(tok); + glyph(display, S.keyOf[tok], round(S.cx[tok]), REF_Y, c[0], c[1], c[2], (match ? 1 : 0.55) * S.drain); + } else if (match) { + const c = col(tok); + glyph(display, LINO[i], slotX(i), REF_Y, 0.5 * c[0] + 128, 0.5 * c[1] + 128, 0.5 * c[2] + 128, S.drain); + } else glyph(display, LINO[i], slotX(i), REF_Y, 84, 80, 92, 1); + } + + // The galley: what has been read is dim, the rest of the reading line is + // lit and the next letter to come loose shows its colour. A cast order + // line keeps its colours. + const G = S.galley; + const L = S.text.letters; + const next = S.phase === 0 && S.read < L.length ? L[S.read] : null; + for (let j = 0; j < G.length && j < LINES + 1; j++) { + const ln = G[G.length - 1 - j]; + const y = READ_Y - 6 * j + round(S.scroll); + if (y < -5 || y > READ_Y + 6) continue; + const live = ln.li >= 0 && ln.li === S.line && j === 0 && S.phase === 0; + for (let c = 0; c < ln.s.length; c++) { + const ch = ln.s[c]; + if (ch === " ") continue; + if (ln.slug) { + const cc = ln.slug[c]; + const a = j === 0 ? pulse : 0.45; + glyph(display, ch, c * 4, y, cc[0], cc[1], cc[2], a); + } else if (live && next && c === next.col) { + const cc = col(ch); + glyph(display, ch, c * 4, y, 0.4 * cc[0] + 150, 0.4 * cc[1] + 150, 0.4 * cc[2] + 150, 1); + } else if (live && next && c > next.col) glyph(display, ch, c * 4, y, 236, 224, 202, 1); + else glyph(display, ch, c * 4, y, 236, 224, 202, 0.33); + } + } + + // Letters in the air, white as they leave the line and their colour as they fall. + for (const f of S.fall) { + if (f.pi) continue; + const c = col(f.tok); + const w = max(0, 1 - f.t / 0.35); + glyph(display, f.tok, round(f.x), round(f.y), c[0] + (255 - c[0]) * w, c[1] + (255 - c[1]) * w, c[2] + (255 - c[2]) * w, 1); + } +} From cf3cc35f937260421c7108d5940289a924e5f09a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:46:08 +0000 Subject: [PATCH 10/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Syllable=20Odometer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../patterns/syllable-odometer.js | 544 ++++++++++++++++++ 1 file changed, 544 insertions(+) create mode 100644 incoming/pattern-study/patterns/syllable-odometer.js diff --git a/incoming/pattern-study/patterns/syllable-odometer.js b/incoming/pattern-study/patterns/syllable-odometer.js new file mode 100644 index 000000000..d960f82f6 --- /dev/null +++ b/incoming/pattern-study/patterns/syllable-odometer.js @@ -0,0 +1,544 @@ +// ===== Patternflow pattern ===== +// Title: Syllable Odometer +// Family: type +// Idea: Three reels of Hangul jamo roll like the wheels of an odometer through all 11,172 modern syllables in Unicode order, the final fastest, then the vowel, then the initial, while a big syllable above them shows what they spell, solid when it is one of the 2,350 that KS C 5601-1987 encoded and hollow when it is not, two rolling counters count every syllable and the encoded ones, and a map of the current initial's 21 vowels by 28 finals lights up as the odometer passes. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the Hangul Syllables block, U+AC00 + (initial x 21 + vowel) x 28 + final (Unicode 2.0, 1996), against the 2,350 precomposed syllables (wansung) of KS C 5601-1987, now KS X 1001; the 9 by 11 combining Hangul face of Wongoji (pattern round 64); pattern round 66 +// Round: 66 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Initial=0..18, Skip=0..1 +// +// Knob 1: Hue. Turns the palette round the wheel: at 0 the encoded syllables are amber and the missing ones blue-violet, at 0.25 green and rose, at 0.5 azure and lime; the reels and the first counter stay a pale tint of the encoded colour. +// Knob 2: Speed. Steps a second: 0.5 at 0.2, 6 at the opening 2 (the first vowel row in 4.6 seconds, the 588 syllables of ㄱ in 94, all 11,172 in 30 minutes), 48 at 3. +// Knob 3: Initial. Sets the slowest wheel by hand, 0 for ㄱ (가) up to 18 for ㅎ (하): the panel opens on the first syllable of that initial, and turned later the odometer spins on to it, starts a new map and counts on from there. +// Knob 4: Skip. How long the odometer stays on a syllable KS C 5601 left out, as a share of a step: all of it at 0, 0.94 at the opening 0.06, none at 1, where it counts like a wansung machine that has only the 2,350 and spins over the other 8,822 between them (all 11,172 in 6.5 minutes at Speed 2). + +const W = 64, H = 128; +const { floor, round, min, max, abs, sqrt, asin, cos, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- The 2,350. Bit i is syllable U+AC00 + i, six bits a character, low +// bit first. Built offline by decoding the EUC-KR bytes B0A1..C8FE (rows +// B0..C8, cells A1..FE: 25 rows of 94) and marking each syllable they give. +// The decoded set has 2,350 syllables in Unicode order, misses 똠 (U+B620), +// and per initial ㄱ..ㅎ holds 171 120 141 128 86 127 129 129 73 163 86 208 +// 135 82 112 107 106 105 142. +const A64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +const KS = + "Tew/+EBsDMRAoAREAAwkFsnHRA7AXGwOSEBoAMZlrBTUwKQEBIDMRArAREgCwkHuGMRAwABAACwEBsAERAAATOwKQAAAAMZBrR" + + "XUwOyEBsDMQAAAAAAAwFBsDMBApAREAGSAAAAMVArDDEAMwAAACERAjABAAAwEBuGEQAwATEwEQEBMAEAAAATV4KCAAAAMRArA" + + "Xew+6EBsDMRAhAAAAAwGNsDORA7ATEwMREAAAMRBrwREAEAAAAAERALATEgKwkBsCEAAQABAAAQEBMAMQAjATcwaUEBAAMRBrQ" + + "X+4+4EBsDEAAAAAAAAwlNsKNRA7AREAMQEAAAMZFrKREAAQAAABERALABAAAwEBsCEAAQABAAIQEBoCERAhATWwKQAAAAMZA7B" + + "TEwO4EBsDAAAAAAAAAwENsDORA7ABAAEAAAAAMRAgABEAAQAAAAEBAAAAAAAwEBGCAAAAABAAAQEBMCAAAAATGwCQEBMAERArA" + + "TEwe8EBsDMBAoAAAAAwEBsDORArATEwOQEAoAMRArARAAIAAAABERArAREgKwEBsCEAAQABAAAwEBkCMRArATEwaGAAAAMRArA" + + "Tfwa6EBsHMQAgAAAAAwEFsGORA7ATEAuQAAAAsRBrARAAMAAAAAERAqAREgCwlHsiGRAKABAAAQEBAAERAJAREQCAAAAAMZB7K" + + "f+wKyEBsjMBACAAAAAwkFsGMZA7ATEgOSEAAAcRArARAAEQAAABMRADARAAAwkHsiFQAQABAAAwEBACERApATEwCAAAAAMRBrD" + + "TUwO4EBsDMAABAAAAAwkBkDEAAgADAwOAAAAAMRAjAAAAAAAAAAEAAAABAAIwEBkCAAAAAAAAAAAAAAEAAgAREwAAAAAAMRArA" + + "bewOyEBsDMRArAREQIwnNsDNRA7ATEwOQEBACcRBrITEAIQEBkBERALATEwKwkBsqFAAQATEQIwEBsCMQApATMwKAAAAAMZgrQ" + + "bEwM4EBMDAAAgAAAAAwEFMDERAAAAAAAAEAAAMZBjATAAEQAAABERADABAAAwEBMCEAAQABAAAQEAAAAAAgATU4AQEBEAMRArA" + + "zd4O2EBsDMRCroREgAwsH8XNRArAX0wPeEBsBMxlrCTEwOwEAkCMRArATEwKwEHsCMRAzATEwIwEBsCMRArCTE06fEBkAMxh7R" + + "TXwewEBsDMVAhAREAAwEFsGMRArAREwMQAAAAMRBrjDEgKQAAADERArATAAIwEHsCEAAQABAAAwEBsAERABATEwKAAAAAMZBrZ" + + "TlwOwEBsDEBAgAAAAAwEBsDEAAgABAAEAAAAAMRArCDEAEQAAABERADAAAAIwEBMCEAAwAAAAAQAAAAEAAAABAQKAAAAAMRAjh" + + "TFwewEBsDEVAhAAAAAwEBsDMRArARAAEQEAACMRArAREAIAAAAAERArABAQAwEBsCEAAQARAAAQEBsCERAhATEwKAAAAAM5ArA" + + "TEwKwEBsDMAAgAAAAAwkBsDMRArAREwOQAAAAMRArATEQIQAAACEQAAABAAAwEBsCERAgABAAIwEBsCERABATEwIAAAAAMRArA" + + "TMwOwEBsDEAAgAAAAAwEFsDMRArARAAEQEAAAMRArQRAAAQAAAAEBAoABAAAwEBsCEAAQABAAAwEBMCERAhATWwCQEBMAMRArA" + + "XUwOyEBsDMAAAAAAAAwEBsDMRArAREwMQABoAMRArABAAIAAAAAEBAAAREgCwkFsCEAAgAAAAAQEBkAERApAREwCAAAAAMRArA" + + "TEyKwEBsDEAAgAAAAAwEFsCMRArATEwOQEBIAMRIrITEAKwEAgCMRAqAREgCwEhkCERAhATEAIwEBsCMRApATPwKSEBMCMRArA"; +const ALL = 11172; + +// ---- The face: Wongoji's 9 by 11 combining Hangul (pattern round 64). +// Jamo are strokes, lists of points in a box w by h; a syllable puts them in +// boxes that depend on its vowel and on whether it has a final. +const INI = "g gg n d dd r m b bb s ss o j jj c k t p h".split(" "); +const FIN = ". g gg gs n nj nh d r rg rm rb rs rt rp rh m b bs s ss o j c k t p h".split(" "); + +function ring(w, h, ox, oy) { + const rw = min(w, h + 2), rh = min(h, w + 1); + ox += (w - rw) >> 1; + oy += (h - rh) >> 1; + const R = rw - 1, B = rh - 1, c = R >> 1; + const pts = rh <= 2 ? [[1, 0], [R - 1, 0], [R - 1, B], [1, B]] : [[c, 0], [1, 0], [0, 1], [0, B - 1], [1, B], [R - 1, B], [R, B - 1], [R, 1], [R - 1, 0], [c + 1, 0]]; + return pts.map(([x, y]) => [x + ox, y + oy]); +} +function legs(w, h, y0) { + const c = (w - 1) >> 1, d = h - 1 - y0; + const xl = max(0, round(c - 1.3 * d)), xr = min(w - 1, round(c + (w % 2 ? 0 : 1) + 1.3 * d)); + return [[[c, y0], [xl, h - 1]], [[c, y0 + max(1, round(d / 3))], [xr, h - 1]]]; +} +function cons(k, w, h, tail) { + const c = (w - 1) >> 1, m = (h - 1) >> 1, S = []; + switch (k) { + case "g": + S.push(tail && h >= 6 ? [[0, 0], [w - 1, 0], [w - 1, h - 3], [w - 3, h - 1]] : [[0, 0], [w - 1, 0], [w - 1, h - 1]]); + break; + case "n": + S.push([[0, 0], [0, h - 1], [w - 1, h - 1]]); + break; + case "d": + S.push([[0, 0], [w - 1, 0]], [[0, 1], [0, h - 1], [w - 1, h - 1]]); + break; + case "r": + if (h >= 5) S.push([[0, 0], [w - 1, 0], [w - 1, m]], [[0, m], [w - 2, m]], [[0, m + 1], [0, h - 1], [w - 1, h - 1]]); + else { + const z = w >> 1; + S.push([[0, 0], [w - 1, 0], [w - 1, 1], [z, 1]], [[z, 2], [0, 2], [0, h - 1], [w - 1, h - 1]]); + } + break; + case "m": + S.push([[0, 0], [0, h - 1]], [[0, 0], [w - 1, 0], [w - 1, h - 1]], [[1, h - 1], [w - 2, h - 1]]); + break; + case "b": + S.push([[0, 0], [0, h - 1]], [[w - 1, 0], [w - 1, h - 1]], [[1, m], [w - 2, m]], [[1, h - 1], [w - 2, h - 1]]); + break; + case "s": + S.push(...legs(w, h, 0)); + break; + case "o": + S.push(ring(w, h, 0, 0)); + break; + case "j": + S.push([[0, 0], [w - 1, 0]], ...legs(w, h, 1)); + break; + case "c": + S.push([[c, 0], [c, 0]], [[0, 1], [w - 1, 1]], ...legs(w, h, 2)); + break; + case "k": + S.push([[0, 0], [w - 1, 0], [w - 1, h - 1]], [[0, h >> 1], [w - 2, h >> 1]]); + break; + case "t": + S.push([[0, 0], [w - 1, 0]], [[1, m], [w - 1, m]], [[0, 1], [0, h - 1], [w - 1, h - 1]]); + break; + case "p": { + const a = w >= 5 ? 1 : 0; + S.push([[0, 0], [w - 1, 0]], [[a, 1], [a, h - 2]], [[w - 1 - a, 1], [w - 1 - a, h - 2]], [[0, h - 1], [w - 1, h - 1]]); + break; + } + case "h": + S.push(w >= 7 ? [[c - 1, 0], [c + 1, 0]] : [[c, 0], [c, 0]], [[0, 1], [w - 1, 1]], ring(w, h - 2, 0, 2)); + break; + } + return S; +} +// One consonant, or a double or cluster side by side, in a box at (x, y). +function group(key, x, y, w, h, tail) { + const parts = [...key], out = []; + const put = (list, dx) => list.forEach((s) => out.push(s.map(([a, b]) => [a + x + dx, b + y]))); + if (parts.length === 1) put(cons(parts[0], w, h, tail), 0); + else { + const w1 = (w - 1) >> 1; + put(cons(parts[0], w1, h, false), 0); + put(cons(parts[1], w - 1 - w1, h, false), w1 + 1); + } + return out; +} +// Vowels, as in Wongoji: vertical ones are the initial's width, then lines +// [0, x] and ticks [1, x0, x1, slot]; horizontal ones ticks above [2, x], +// bars [3, x0, x1] and stems below [4, x]; compounds have both. +const VV = { + 0: [5, [0, 6], [1, 7, 8, 0]], + 1: [5, [0, 6], [1, 7, 7, 0], [0, 8]], + 2: [5, [0, 6], [1, 7, 8, 1], [1, 7, 8, 2]], + 3: [5, [0, 6], [1, 7, 7, 1], [1, 7, 7, 2], [0, 8]], + 4: [5, [1, 6, 7, 0], [0, 8]], + 5: [4, [1, 5, 5, 0], [0, 6], [0, 8]], + 6: [5, [1, 6, 7, 1], [1, 6, 7, 2], [0, 8]], + 7: [4, [1, 5, 5, 1], [1, 5, 5, 2], [0, 6], [0, 8]], + 20: [6, [0, 7]], +}; +const HV = { 8: [[2, 4], [3, 0, 8]], 12: [[2, 2], [2, 6], [3, 0, 8]], 13: [[3, 0, 8], [4, 4]], 17: [[3, 0, 8], [4, 2], [4, 6]], 18: [[3, 0, 8]] }; +const MV = { + 9: [5, [[2, 2], [3, 0, 4]], [[0, 6], [1, 7, 8, 0]]], + 10: [5, [[2, 2], [3, 0, 4]], [[0, 6], [1, 7, 7, 0], [0, 8]]], + 11: [6, [[2, 2], [3, 0, 5]], [[0, 7]]], + 14: [5, [[3, 0, 4], [4, 2]], [[1, 5, 6, 3], [0, 8]]], + 15: [4, [[3, 0, 3], [4, 2]], [[1, 5, 5, 3], [0, 6], [0, 8]]], + 16: [6, [[3, 0, 5], [4, 3]], [[0, 7]]], + 19: [6, [[3, 0, 5]], [[0, 7]]], +}; +const FREE = new Set(["g", "gg", "s", "ss", "j", "jj", "c", "k"]); +const NEED4 = new Set(["r", "b", "bb", "c", "k", "t", "h"]); + +function vparts(parts, y0, y1, bar, out) { + const mid = (y0 + y1) >> 1; + for (const p of parts) { + if (p[0] === 0) out.push([[p[1], y0], [p[1], y1]]); + else { + const y = p[3] === 0 ? mid : p[3] === 1 ? mid - 1 : p[3] === 2 ? mid + 1 : bar + 1; + out.push([[p[1], y], [p[2], y]]); + } + } +} +function hparts(parts, bar, stem, out) { + for (const p of parts) { + if (p[0] === 2) out.push([[p[1], bar - 1], [p[1], bar - 1]]); + else if (p[0] === 3) out.push([[p[1], bar], [p[2], bar]]); + else out.push([[p[1], bar + 1], [p[1], bar + stem]]); + } +} + +// Syllable s (0..11171) as three stroke lists: initial, vowel, final. +function jamo(s) { + const ci = floor(s / 588), vi = floor((s % 588) / 28), fi = s % 28; + const ini = INI[ci], has = fi > 0, vow = []; + let box; + const fbox = FIN[fi].length > 1 ? [0, 7, 9, 4] : [1, 7, 7, 4]; + if (VV[vi]) { + const [iw, ...parts] = VV[vi]; + box = has ? [0, 0, iw, 5] : [0, 2, iw, 7]; + vparts(parts, has ? 0 : 1, has ? 5 : 9, 0, vow); + } else if (HV[vi]) { + const up = vi === 8 || vi === 12, down = vi === 13 || vi === 17; + let bar = 7; + if (!has) { + box = [1, 1, 7, 5]; + if (up) bar = 8; + } else if (up) { + box = FREE.has(ini) ? [1, 0, 7, 4] : [1, 0, 7, 3]; + bar = 5; + } else if (down) { + box = NEED4.has(ini) ? [1, 0, 7, 4] : [1, 0, 7, 3]; + bar = box[3] + 1; + } else { + box = [1, 0, 7, 4]; + bar = 5; + } + hparts(HV[vi], bar, has ? 1 : 2, vow); + } else { + const [iw, hp, vp] = MV[vi]; + const up = hp[0][0] === 2, flat = hp.length === 1; + let bar; + if (!has) { + box = [0, 1, iw, flat ? 5 : 4]; + bar = up || flat ? 7 : 6; + } else { + box = [0, 0, iw, flat ? 4 : 3]; + bar = up || flat ? 5 : 4; + } + hparts(hp, bar, has ? 1 : 2, vow); + vparts(vp, has ? 0 : 1, has ? 5 : 9, bar, vow); + } + const tail = !has && VV[vi] && (ini === "g" || ini === "k"); + return [group(ini, box[0], box[1], box[2], box[3], tail), vow, has ? group(FIN[fi], fbox[0], fbox[1], fbox[2], fbox[3], false) : []]; +} + +// Strokes to LEDs: a bitmap bw by bh, and the diagonal steps inside each +// stroke, which the big glyph joins (the legs of ㅅ, ㅈ, ㅊ). +function bitmap(strokes, bw, bh) { + const bits = new Uint8Array(bw * bh), diag = []; + for (const st of strokes) { + let lx = null, ly = null; + for (let i = 0; i < st.length; i++) { + const [x0, y0] = st[i], [x1, y1] = st[min(i + 1, st.length - 1)]; + const n = max(abs(x1 - x0), abs(y1 - y0)); + for (let j = 0; j <= n; j++) { + const x = round(x0 + ((x1 - x0) * j) / (n || 1)), y = round(y0 + ((y1 - y0) * j) / (n || 1)); + if (x === lx && y === ly) continue; + if (x >= 0 && y >= 0 && x < bw && y < bh) bits[y * bw + x] = 1; + if (lx !== null && x !== lx && y !== ly) diag.push([lx, ly, x, y]); + lx = x; + ly = y; + } + } + } + return { bits, diag }; +} + +// ---- Layout. +// The big glyph: the 9 by 11 face drawn 5 LEDs a column and 4 a row, each +// lit LED of the face a round dot joined to its lit neighbours. +const GX = 9.5, GY = 2, SX = 5, SY = 4, GR = 2.0, GH = 46; +// The counters, the reels and the map. +const CY = 49; +const RY = 70, RH = 14, RT = 15, RW = 20, RXS = [1, 22, 43], P = 21; +const MX = 4, MY = 86; + +// A row of the 3 by 5 digits, each row a 3-bit number, left bit high. +const DIG = ["75557", "26227", "71747", "71317", "55711", "74717", "74757", "71122", "75757", "75717"].map((s) => [...s].map(Number)); + +// One reel's tape: each symbol drawn two LEDs a dot, centred in its own +// stretch of P rows. +function tape(symbols) { + const T = new Float32Array(RW * symbols.length * P); + symbols.forEach((strokes, k) => { + if (!strokes) { + // The empty final: a dotted slot. + for (let i = 0; i < 7; i++) + for (let j = 0; j < 7; j++) { + if ((i > 0 && i < 6 && j > 0 && j < 6) || (i + j) % 2) continue; + for (let a = 0; a < 2; a++) for (let b = 0; b < 2; b++) T[(k * P + 3 + 2 * j + b) * RW + 3 + 2 * i + a] = 0.55; + } + return; + } + const { bits } = bitmap(strokes, 9, 11); + let x0 = 9, x1 = -1, y0 = 11, y1 = -1; + for (let y = 0; y < 11; y++) + for (let x = 0; x < 9; x++) + if (bits[y * 9 + x]) { + x0 = min(x0, x); + x1 = max(x1, x); + y0 = min(y0, y); + y1 = max(y1, y); + } + const ox = (RW >> 1) - (x1 + x0 + 1), oy = k * P + (P >> 1) - (y1 + y0 + 1); + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + if (!bits[y * 9 + x]) continue; + for (let a = 0; a < 2; a++) for (let b = 0; b < 2; b++) T[(oy + 2 * y + b) * RW + ox + 2 * x + a] = 1; + } + }); + return T; +} + +export function setup(params) { + const S = (params.s = { t: 0, n: 0, p: 0, acc: 0, k3: -1, shown: -1, mapC: -1, fade: 0, rk: -1 }); + // The set, and for each syllable how many of the 2,350 come at or before it. + S.ks = new Uint8Array(ALL); + S.rank = new Uint16Array(ALL); + let i = 0; + for (let c = 0; c < KS.length; c++) { + const v = A64.indexOf(KS[c]); + for (let b = 0; b < 6 && i < ALL; b++) S.ks[i++] = (v >> b) & 1; + } + for (let s = 0, r = 0; s < ALL; s++) S.rank[s] = r += S.ks[s]; + // The three tapes: 19 initials, 21 vowels (as they stand beside or under + // ㅇ), 28 finals with the empty one first. + const box = (key) => group(key, key.length > 1 ? 0 : 1, 2, key.length > 1 ? 9 : 7, 7, false); + S.tapes = [ + tape(INI.map(box)), + tape(Array.from({ length: 21 }, (_, v) => jamo(11 * 588 + v * 28)[1])), + tape(FIN.map((f, k) => (k ? box(f) : null))), + ]; + S.row = new Float32Array(RW); + S.G = new Float32Array(W * GH); + S.E = new Float32Array(W * GH); + S.B = new Float32Array(W * GH); + S.Hl = new Float32Array(W * GH); + S.F = new Float32Array(W * H * 3); +} + +// The big glyph of syllable s into S.G (coverage) and S.E (its outline). +function bigGlyph(S, s) { + const { bits, diag } = bitmap(jamo(s).flat(), 9, 11); + const segs = diag.slice(); + for (let y = 0; y < 11; y++) + for (let x = 0; x < 9; x++) { + if (!bits[y * 9 + x]) continue; + segs.push([x, y, x, y]); + if (x < 8 && bits[y * 9 + x + 1]) segs.push([x, y, x + 1, y]); + if (y < 10 && bits[(y + 1) * 9 + x]) segs.push([x, y, x, y + 1]); + } + const G = S.G; + G.fill(0); + for (const [a, b, c, d] of segs) { + const ax = GX + (a + 0.5) * SX, ay = GY + (b + 0.5) * SY, bx = GX + (c + 0.5) * SX, by = GY + (d + 0.5) * SY; + const dx = bx - ax, dy = by - ay, L = dx * dx + dy * dy; + for (let y = max(0, floor(min(ay, by) - GR - 1)); y <= min(GH - 1, floor(max(ay, by) + GR + 1)); y++) + for (let x = max(0, floor(min(ax, bx) - GR - 1)); x <= min(W - 1, floor(max(ax, bx) + GR + 1)); x++) { + const px = x + 0.5 - ax, py = y + 0.5 - ay; + const u = L ? cl((px * dx + py * dy) / L, 0, 1) : 0; + const ex = px - u * dx, ey = py - u * dy; + const v = cl(GR + 0.5 - sqrt(ex * ex + ey * ey), 0, 1); + if (v > G[y * W + x]) G[y * W + x] = v; + } + } + // The outline: lit LEDs with an unlit neighbour. + const E = S.E; + for (let y = 0; y < GH; y++) + for (let x = 0; x < W; x++) { + const g = G[y * W + x]; + let lo = 1; + if (g > 0.3) for (const [ox, oy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) lo = min(lo, x + ox < 0 || x + ox >= W || y + oy < 0 || y + oy >= GH ? 0 : G[(y + oy) * W + x + ox]); + E[y * W + x] = g > 0.3 && lo < 0.5 ? g : 0; + } + // A soft halo round the strokes, for the encoded syllables: a box blur + // five LEDs wide, across and then down. + const B = S.B, Hl = S.Hl; + for (let y = 0; y < GH; y++) + for (let x = 0; x < W; x++) { + let s = 0; + for (let i = -2; i <= 2; i++) s += x + i >= 0 && x + i < W ? G[y * W + x + i] : 0; + B[y * W + x] = s / 5; + } + for (let y = 0; y < GH; y++) + for (let x = 0; x < W; x++) { + let s = 0; + for (let i = -2; i <= 2; i++) s += y + i >= 0 && y + i < GH ? B[(y + i) * W + x] : 0; + Hl[y * W + x] = max(0, s / 5 - G[y * W + x]); + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + S.t += dt; + S.hue = k[0] || 0; + const speed = cl(k[1], 0.2, 3); + const d = speed - 2; + S.rate = 6 * exp(1.83 * d + 0.25 * d * d); + S.skip = cl(k[3], 0, 1); + // The Initial knob sets the slowest wheel when it is turned; the first + // frame starts there without spinning. + const c = round(cl(k[2], 0, 18)); + if (c !== S.k3) { + const base = floor(S.n / ALL) * ALL; + let to = base + c * 588; + if (S.k3 >= 0 && to <= S.n) to += ALL; + if (S.k3 < 0) S.p = to; + S.n = to; + S.acc = 0; + S.k3 = c; + } + // Count: the odometer stays a whole step on each syllable, on a missing + // one 1 - Skip of a step. + const stay = (s) => (S.ks[s % ALL] ? 1 : 1 - S.skip); + S.acc += dt * S.rate; + for (let i = 0; i < 4000 && S.acc >= stay(S.n); i++) { + S.acc -= stay(S.n); + S.n++; + } + // The wheels rest on the count and turn to the next syllable in the last + // 0.08 seconds of the stay (the whole stay when stays are shorter), + // then a quick spring smooths them, so a jump spins through what lies + // between. + const g = S.n % ALL, next = (g + 1) % ALL, cost = stay(g); + const w = cost > 0 ? min(1, (0.08 * S.rate) / cost) : 1, phi = cost > 0 ? cl(S.acc / cost, 0, 1) : 1; + const e = cl((phi - 1 + w) / w, 0, 1), turn = w >= 1 ? e : e * e * (3 - 2 * e); + const spring = 1 - exp(-dt * 30); + S.p += (S.n + turn - S.p) * spring; + if (abs(S.n + turn - S.p) < 0.002) S.p = S.n + turn; + // The glyph and the map read the count. + if (g !== S.shown) { + S.shown = g; + bigGlyph(S, g); + } + // A new initial starts a new map; the last one fades out over it. + const ci = floor(g / 588); + if (ci !== S.mapC) { + S.mapOld = S.mapC; + S.oldAt = S.mapC === (ci + 18) % 19 ? 587 : S.at; + S.mapC = ci; + S.fade = S.mapOld < 0 ? 0 : 1; + } + S.at = g % 588; + // The count of the 2,350 turns with the wheels when the next syllable is + // one of them, and starts again after 힣. + const rk = S.rank[g] + (S.ks[next] ? turn : 0); + S.rk = rk < S.rk - 50 || S.rk < 0 ? rk : S.rk + (rk - S.rk) * spring; + if (abs(rk - S.rk) < 0.002) S.rk = rk; + S.fade = max(0, S.fade - dt / 0.6); +} + +export function draw(display, params) { + const S = params.s; + if (!S || S.shown < 0) return; + const F = S.F; + F.fill(0); + const hue = 0.1 + S.hue; + const lit = hsv(hue, 0.85, 255), miss = hsv(hue + 0.6, 0.6, 255), pale = hsv(hue + 0.02, 0.3, 255); + const add = (x, y, c, a) => { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + F[o] += c[0] * a; + F[o + 1] += c[1] * a; + F[o + 2] += c[2] * a; + }; + const g = S.shown, inSet = S.ks[g] === 1; + + // The big glyph: solid when encoded, a hollow outline when not. + for (let y = 0; y < GH; y++) + for (let x = 0; x < W; x++) { + if (inSet) { + const v = S.G[y * W + x] + 0.3 * S.Hl[y * W + x]; + if (v > 0) add(x, y, lit, v); + } else { + const v = S.E[y * W + x]; + if (v > 0) add(x, y, miss, 0.75 * v); + } + } + + // The counters, rolling like the reels: the syllable's place among all + // 11,172, and among the 2,350. A digit turns while every digit to its + // right turns from 9 to 0. + const roll = (val, digits, x0, c, a) => { + const fl = floor(val), fr = val - fl; + for (let d = 0, m = 1; d < digits; d++, m *= 10) { + const base = floor(fl / m) % 10, sh = round((fl % m === m - 1 ? fr : 0) * 7), x = x0 + (digits - 1 - d) * 4; + for (const [dig, oy] of [[base, -sh], [(base + 1) % 10, 7 - sh]]) + for (let r = 0; r < 5; r++) { + if (r + oy < 0 || r + oy > 4) continue; + for (let b = 0; b < 3; b++) if (DIG[dig][r] & (4 >> b)) add(x + b, CY + r + oy, c, a); + } + } + }; + roll(min(ALL, (S.p % ALL) + 1), 5, 1, pale, 0.8); + roll(S.rk, 4, 48, inSet ? lit : miss, inSet ? 1 : 0.5); + + // The reels: each a wheel of radius RT seen through a window 2 RH tall, + // its symbols squeezed and dimmed towards the window's edges. + const p = S.p, fr = p - floor(p), q = floor(p); + const pos = [ + (floor(q / 588) % 19) + (q % 588 === 587 ? fr : 0), + (floor(q / 28) % 21) + (q % 28 === 27 ? fr : 0), + (q % 28) + fr, + ]; + const len = [19, 21, 28]; + for (let r = 0; r < 3; r++) { + const T = S.tapes[r], span = len[r] * P, uc = pos[r] * P + (P >> 1); + for (let y = RY - RH; y < RY + RH; y++) { + let shade = 0; + const row = S.row.fill(0); + for (let s = 0; s < 4; s++) { + const dy = y + (s + 0.5) / 4 - RY, th = asin(cl(dy / RT, -1, 1)); + let u = floor(uc + RT * th) % span; + if (u < 0) u += span; + const w = cos(th); + shade += w / 4; + for (let x = 0; x < RW; x++) row[x] += (T[u * RW + x] * w) / 4; + } + for (let x = 0; x < RW; x++) { + add(RXS[r] + x, y, pale, 0.11 * shade + 0.89 * row[x]); + } + } + } + + // The map: the current initial's 21 vowels down and 28 finals across. + // Ahead of the odometer the encoded cells wait faintly; behind it they + // burn, fresh ones brighter for a moment. + const ci = S.mapC, at = g % 588; + const cell = (x, y, c, a) => { + add(x, y, c, a); + add(x + 1, y, c, a); + add(x, y + 1, c, a * 0.45); + add(x + 1, y + 1, c, a * 0.45); + }; + for (let v = 0; v < 21; v++) + for (let f = 0; f < 28; f++) { + const k = v * 28 + f, x = MX + 2 * f, y = MY + 2 * v, on = S.ks[ci * 588 + k]; + let c, a; + if (k === at) (c = [255, 255, 255]), (a = 1); + else if (k > at) (c = pale), (a = on ? 0.24 : 0.06); + else { + const age = (at - k) / S.rate, glow = exp(-age / 0.7); + c = on ? lit : miss; + a = on ? 0.75 + 0.25 * glow : 0.2 + 0.3 * glow; + } + if (S.fade > 0 && k <= S.oldAt) { + const o = S.ks[S.mapOld * 588 + k]; + cell(x, y, o ? lit : miss, (o ? 0.9 : 0.2) * S.fade * S.fade); + } + cell(x, y, c, a); + } + // The current vowel row, marked at both sides. + for (const x of [1, 2, 61, 62]) add(x, MY + 2 * floor(at / 28), pale, 0.7); + + for (let y = 0, o = 0; y < H; y++) for (let x = 0; x < W; x++, o += 3) display.setPixel(x, y, round(min(255, F[o])), round(min(255, F[o + 1])), round(min(255, F[o + 2]))); +} From b763f08630e4558fb632efe8f46e2c9fc1b4bfb8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:46:34 +0000 Subject: [PATCH 11/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Zermelo=20Navigation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../patterns/zermelo-navigation.js | 357 ++++++++++++++++++ 1 file changed, 357 insertions(+) create mode 100644 incoming/pattern-study/patterns/zermelo-navigation.js diff --git a/incoming/pattern-study/patterns/zermelo-navigation.js b/incoming/pattern-study/patterns/zermelo-navigation.js new file mode 100644 index 000000000..6daa0ad65 --- /dev/null +++ b/incoming/pattern-study/patterns/zermelo-navigation.js @@ -0,0 +1,357 @@ +// ===== Patternflow pattern ===== +// Title: Zermelo Navigation +// Family: line +// Idea: A harbour at the foot of the panel sends out, twice a second, the front of every place a boat rowing at one speed could have reached by then, and the fronts climb the whole panel bent by a current of eddies or a wandering jet, fold into corners where two ways round meet, string beads along the thin quickest routes to six places, and stop at hatched shadows where water pours in from beyond the panel faster than any boat can row. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Zermelo's navigation problem (Ernst Zermelo, 1931), arrival times by Dijkstra's algorithm (Edsger Dijkstra, 1959) on a 32-heading stencil, pattern round 66 +// Round: 66 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Current=0..2.5, Shape=0..1, Harbour=-1..1 +// +// Knob 1: Hue. Turns the colours round the wheel together: at 0 a front leaves the harbour gold, turns green halfway and reaches the far coast blue, and the shadows are rose. +// Knob 2: Current. The fastest water in boat speeds. At 0 the fronts are circles and the routes straight; up to 1 every place can be reached; past 1 shadows open, at Shape 0 on 2.4% of the panel at 1.5, 8.9% at 2 (the opening) and 18.5% at 2.5. +// Knob 3: Shape. Three eddies centred near the panel's sides at 0, a jet leaving the coast at the bottom centre and wandering as it climbs at 1, both at full strength at 0.5. With the harbour inside a jet faster than the boat, the fronts are teardrops carried up and only a cone above the harbour is reached first. +// Knob 4: Harbour. Where the harbour stands along the coast, from column 8 at -1 to column 56 at 1; the shadows stay where they are, since they belong to the current and the panel's edge, not to the harbour. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, sqrt, exp, sin, cos, round, PI, cosh } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const INF = 1e9; +const HY = 121; // the harbour's row +const RATE = 18; // the boat's speed in LEDs per second +const GAP = 9; // a new front every GAP / RATE seconds +const SOLVE = 20; // frames over which one field of arrival times is solved +const NP = 240; // tracers in the water + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255; + out[o + 1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255; + out[o + 2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255; +} + +// The stencil: every step (a, b) with |a|, |b| <= 3 and gcd 1, 32 headings. +// The widest gap between headings is 18.4 degrees, so in still water a +// stencil path is at most 1 / cos(9.2 deg) - 1 = 1.3% longer than the +// straight line; measured over the panel it is 0.66% on average. +const SA = [], SB = [], SL = [], SUX = [], SUY = []; +const gcd = (a, b) => (b ? gcd(b, a % b) : a); +for (let a = -3; a <= 3; a++) + for (let b = -3; b <= 3; b++) + if ((a || b) && gcd(abs(a), abs(b)) === 1) { + const l = sqrt(a * a + b * b); + SA.push(a); SB.push(b); SL.push(l); SUX.push(a / l); SUY.push(b / l); + } +const ND = SA.length; + +// Ground speed along unit heading u in current w, the boat rowing at 1: +// s solves |s u - w| = 1, s = u.w + sqrt(1 - |w|^2 + (u.w)^2). Returns the +// time per unit length, or INF when no heading of the boat makes way along +// u. While |w| < 1 every u makes way; past 1 only a cone around w does. +function slow(wx, wy, ux, uy) { + const d = ux * wx + uy * wy, disc = 1 - wx * wx - wy * wy + d * d; + if (disc < 0) return INF; + const s = d + sqrt(disc); + return s > 1e-4 ? 1 / s : INF; +} + +// The current at (x, y) in LEDs, in boat speeds. Both parts come from a +// stream function psi, w = (dpsi/dy, -dpsi/dx), so no water is made or lost +// inside the panel; it only comes in and goes out through the edges. +function current(F, x, y, out) { + let wx = 0, wy = 0; + if (F.ge) { + // Gaussian eddies, psi = G exp(-r^2 / 2R^2), fastest at r = R. The coast + // calms them: psi is multiplied by c(y), 1 down to row 92 and 0 at the + // bottom edge, so the harbour never sits in a strong eddy. + const R2 = 169, z = (y - 92) / 36; + const c = z <= 0 ? 1 : z >= 1 ? 0 : 0.5 + 0.5 * cos(PI * z), dc = z <= 0 || z >= 1 ? 0 : (-0.5 * PI * sin(PI * z)) / 36; + for (let k = 0; k < 9; k += 3) { + const dx = x - F.E[k], dy = y - F.E[k + 1], p = F.E[k + 2] * F.ge * exp(-(dx * dx + dy * dy) / (2 * R2)), e = p / R2; + wx += -dy * e * c + p * dc; + wy += dx * e * c; + } + } + if (F.gj) { + // A jet leaving the coast at the bottom centre, wandering more as it + // climbs: psi = J L tanh((x - X(y)) / L), X = 32 + A(y) sin(k y - phase). + const L = 13, k = (2 * PI) / 52, A = 9 * (1 - y / H), ph = k * y - F.th; + const X = 32 + A * sin(ph), dX = (-9 / H) * sin(ph) + A * k * cos(ph), c = 1 / cosh((x - X) / L), j = F.gj * c * c; + wx -= j * dX; + wy -= j; + } + out[0] = wx; + out[1] = wy; +} + +// Where the quickest routes go: the top corners, two places on the far +// coast and one on each side halfway up. +const TARGETS = [4, 2, 22, 1, 42, 1, 60, 2, 2, 62, 62, 62]; + +export function setup(params) { + params.s = { + WX: new Float32Array(N), WY: new Float32Array(N), Tn: new Float32Array(N), pred: new Int32Array(N), + done: new Uint8Array(N), pos: new Int32Array(N), hp: new Int32Array(N), hn: 0, + Ta: new Float32Array(N), Tb: new Float32Array(N), T: new Float32Array(N), DWX: new Float32Array(N), DWY: new Float32Array(N), + ra: 120, rb: 120, routes: [], fade: 1, solving: false, src: 0, t: 0, tau: 0, started: false, + px: new Float32Array(NP), py: new Float32Array(NP), pa: new Float32Array(NP), + rgb: new Float32Array(N * 3), lut: new Float32Array(65 * 3), sh: new Float32Array(3), hue: 0, + F: { ge: 0, gj: 0, th: 0, hx: 32, E: new Float32Array(9) }, w: [0, 0], + }; + return params.s; +} + +// An indexed binary heap keyed on Tn, so a node's key can drop in place. +function up(S, i, v) { + const hp = S.hp, pos = S.pos, T = S.Tn, key = T[v]; + while (i > 0) { + const p = (i - 1) >> 1, pv = hp[p]; + if (T[pv] <= key) break; + hp[i] = pv; + pos[pv] = i; + i = p; + } + hp[i] = v; + pos[v] = i; +} + +function pop(S) { + const hp = S.hp, pos = S.pos, T = S.Tn, top = hp[0]; + pos[top] = -1; + const n = --S.hn; + if (n > 0) { + const last = hp[n], key = T[last]; + let i = 0; + for (;;) { + let c = 2 * i + 1; + if (c >= n) break; + if (c + 1 < n && T[hp[c + 1]] < T[hp[c]]) c++; + if (T[hp[c]] >= key) break; + hp[i] = hp[c]; + pos[hp[i]] = i; + i = c; + } + hp[i] = last; + pos[last] = i; + } + return top; +} + +// Start a solve for the knobs and the clock `at` seconds: the eddies ride +// small circles and the jet's meander travels up it, once in 40 s. +function begin(S, kv, at) { + const F = S.F, th = (at * 2 * PI) / 40, cur = cl(kv[1] ?? 2, 0, 2.5), m = cl(kv[2] || 0, 0, 1); + F.th = th; + F.ge = cur * 13 * 1.6487 * min(1, 2 - 2 * m); + F.gj = cur * min(1, 2 * m); + F.hx = 32 + 24 * cl(kv[3] ?? 0.06, -1, 1); + const E = F.E; + E[0] = 4 + 3 * sin(th); E[1] = 98 + 4 * cos(th); E[2] = 1; + E[3] = 60 - 3 * sin(th + 2); E[4] = 62 + 4 * cos(th + 2); E[5] = -1; + E[6] = 4 + 3 * sin(th + 4); E[7] = 26 + 4 * cos(th + 4); E[8] = 1; + const w = S.w; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + current(F, x + 0.5, y + 0.5, w); + S.WX[y * W + x] = w[0]; + S.WY[y * W + x] = w[1]; + } + S.Tn.fill(INF); + S.done.fill(0); + S.pos.fill(-1); + S.hn = 0; + const src = (S.src = HY * W + round(F.hx)); + S.Tn[src] = 0; + S.pred[src] = -1; + up(S, S.hn++, src); + S.solving = true; +} + +// Dijkstra over the stencil, settling at most `budget` LEDs. A step's time +// is its length times the mean slowness at its two ends; a step with an end +// where no heading makes way along it is not taken. +function work(S, budget) { + const T = S.Tn, WX = S.WX, WY = S.WY, done = S.done, pred = S.pred, pos = S.pos; + while (S.hn > 0 && budget-- > 0) { + const u = pop(S); + done[u] = 1; + const ux = u % W, uy = (u - ux) / W, tu = T[u], wx = WX[u], wy = WY[u]; + for (let k = 0; k < ND; k++) { + const vx = ux + SA[k], vy = uy + SB[k]; + if (vx < 0 || vx >= W || vy < 0 || vy >= H) continue; + const v = vy * W + vx; + if (done[v]) continue; + const a = slow(wx, wy, SUX[k], SUY[k]); + if (a >= INF) continue; + const b = slow(WX[v], WY[v], SUX[k], SUY[k]); + if (b >= INF) continue; + const tv = tu + SL[k] * 0.5 * (a + b); + if (tv < T[v]) { + T[v] = tv; + pred[v] = u; + up(S, pos[v] >= 0 ? pos[v] : S.hn++, v); + } + } + } + return S.hn === 0; +} + +// A solved field becomes the one faded in; the routes are traced back from +// each target along the steps that reached it, and the colour scale is set +// by the mean time to the far coast. +function finish(S) { + S.Ta.set(S.Tb); + S.Tb.set(S.Tn); + S.DWX.set(S.WX); + S.DWY.set(S.WY); + S.fade = 0; + S.solving = false; + S.routes = []; + for (let k = 0; k < TARGETS.length; k += 2) { + let v = TARGETS[k + 1] * W + TARGETS[k]; + if (S.Tn[v] >= INF) continue; + const path = []; + for (; v >= 0 && path.length < N; v = S.pred[v]) path.push(v); + S.routes.push(path); + } + let sum = 0, n = 0; + for (let x = 0; x < W; x++) + if (S.Tn[x] < INF) { + sum += S.Tn[x]; + n++; + } + S.ra = S.rb; + S.rb = n ? max(40, sum / n) : 120; +} + +function spawn(S, i) { + S.px[i] = Math.random() * W; + S.py[i] = Math.random() * H; + S.pa[i] = 0; +} + +export function update(dt, input, params) { + const S = params.s || setup(params); + const kv = input.knobValues || [0, 2, 0, 0.06]; + const d = cl(dt || 0, 0, 0.1); + S.hue = kv[0] || 0; + S.t += d; + S.tau += d * RATE; + if (!S.started) { + // The first field is solved whole, and fronts have been leaving the + // harbour for ever, so the panel opens full. + S.started = true; + begin(S, kv, S.t); + work(S, N); + finish(S); + S.Ta.set(S.Tb); + S.ra = S.rb; + S.fade = 1; + for (let i = 0; i < NP; i++) { + spawn(S, i); + S.pa[i] = Math.random() * 3; + } + } + // Then one field is solved every SOLVE frames, a slice per frame, for the + // knobs as they are and the clock when it will be shown, and fades in. + if (!S.solving) begin(S, kv, S.t + SOLVE / 30); + if (work(S, Math.ceil(N / SOLVE))) finish(S); + S.fade = min(1, S.fade + 1 / SOLVE); + for (let i = 0; i < NP; i++) { + const c = (S.py[i] | 0) * W + (S.px[i] | 0); + S.px[i] += S.DWX[c] * RATE * d; + S.py[i] += S.DWY[c] * RATE * d; + S.pa[i] += d; + if (S.px[i] < 0 || S.px[i] >= W || S.py[i] < 0 || S.py[i] >= H || S.pa[i] > 3) spawn(S, i); + } +} + +function put(rgb, x, y, r, g, b) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const o = (y * W + x) * 3; + if (rgb[o] < r) rgb[o] = r; + if (rgb[o + 1] < g) rgb[o + 1] = g; + if (rgb[o + 2] < b) rgb[o + 2] = b; +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.started) return; + const rgb = S.rgb, Ta = S.Ta, Tb = S.Tb, a = S.fade, tau = S.tau, T = S.T, lut = S.lut, sh = S.sh; + rgb.fill(0); + for (let i = 0; i < N; i++) { + const p = Ta[i], q = Tb[i]; + T[i] = p >= INF || q >= INF ? (a < 0.5 ? p : q) : p + (q - p) * a; + } + // Front colours by arrival time over the time to the far coast. + for (let k = 0; k <= 64; k++) hsv(S.hue + 0.13 + (0.55 * k) / 64, 1, 1, lut, k * 3); + const sc = 64 / (S.ra + (S.rb - S.ra) * a); + hsv(S.hue + 0.92, 0.75, 1, sh, 0); + // The current: short pale streaks drifting with the water. + for (let i = 0; i < NP; i++) { + const x = S.px[i], y = S.py[i], o = (y | 0) * W + (x | 0), wx = S.DWX[o], wy = S.DWY[o]; + const f = min(1, S.pa[i] * 2, (3 - S.pa[i]) * 2), n = wx * wx + wy * wy < 0.09 ? 1 : 3; + for (let k = 0; k < n; k++) { + const v = f * (75 - 20 * k); + put(rgb, floor(x - wx * k * 0.9), floor(y - wy * k * 0.9), v * 0.6, v * 0.75, v); + } + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const i = y * W + x, t = T[i], o = i * 3; + const l = x > 0 ? T[i - 1] : INF, r = x < W - 1 ? T[i + 1] : INF, u = y > 0 ? T[i - W] : INF, dn = y < H - 1 ? T[i + W] : INF; + if (t >= INF) { + // The shadow: no heading of the boat ever gets here. Hatched, with + // its rim lit. + const v = l < INF || r < INF || u < INF || dn < INF ? 0.55 : (x + y) % 4 === 0 ? 0.32 : 0; + if (v) { + rgb[o] = max(rgb[o], sh[0] * v); + rgb[o + 1] = max(rgb[o + 1], sh[1] * v); + rgb[o + 2] = max(rgb[o + 2], sh[2] * v); + } + continue; + } + // Distance to the nearest front in LEDs from the slope of T, taken + // as the steeper side so a fold does not smear. + const gx = max(l < INF ? t - l : 0, r < INF ? r - t : 0, l < INF ? l - t : 0, r < INF ? t - r : 0); + const gy = max(u < INF ? t - u : 0, dn < INF ? dn - t : 0, u < INF ? u - t : 0, dn < INF ? t - dn : 0); + const g = max(0.25, sqrt(gx * gx + gy * gy)); + const q = (tau - t) / GAP, f = q - floor(q), dl = (min(f, 1 - f) * GAP) / g, behind = (f * GAP) / g; + // A line on the front, a short glow on the harbour side of it, and + // where fronts crowd closer than two LEDs they fade rather than merge. + const line = dl < 1.1 ? 1 - dl / 1.1 : 0, tail = behind < 9 ? 0.16 * exp(-behind / 2.5) : 0; + const v = max(line, tail) * cl((GAP / g - 1.5) / 2, 0, 1); + if (v <= 0.01) continue; + const c = min(64, (t * sc) | 0) * 3; + rgb[o] = max(rgb[o], lut[c] * v); + rgb[o + 1] = max(rgb[o + 1], lut[c + 1] * v); + rgb[o + 2] = max(rgb[o + 2], lut[c + 2] * v); + } + // The quickest routes, and a bead where each front crosses one: the + // place on the front that route's boat has got to. + for (const path of S.routes) + for (let k = 1; k < path.length; k++) { + const p0 = path[k - 1], p1 = path[k]; + const x0 = p0 % W, y0 = (p0 / W) | 0, x1 = p1 % W, y1 = (p1 / W) | 0, n = max(abs(x1 - x0), abs(y1 - y0)); + for (let j = 0; j < n; j++) put(rgb, round(x0 + ((x1 - x0) * j) / n), round(y0 + ((y1 - y0) * j) / n), 95, 95, 105); + const t0 = T[p0], t1 = T[p1], lv = tau - GAP * Math.ceil((tau - t0) / GAP); + if (t0 > t1 && lv >= t1) { + const s = (lv - t1) / (t0 - t1); + put(rgb, round(x1 + (x0 - x1) * s), round(y1 + (y0 - y1) * s), 255, 255, 255); + } + } + const hx = S.src % W; + for (let dy = -1; dy <= 1; dy++) + for (let dx = -1; dx <= 1; dx++) { + const v = dx || dy ? 120 : 255; + put(rgb, hx + dx, HY + dy, v, v, v); + } + for (let i = 0; i < N; i++) { + const o = i * 3; + display.setPixel(i % W, (i / W) | 0, min(255, rgb[o]) | 0, min(255, rgb[o + 1]) | 0, min(255, rgb[o + 2]) | 0); + } +} From d043c4284919e6b1105a310097ff9a030f3ab6d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:47:37 +0000 Subject: [PATCH 12/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Gridfonts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- incoming/pattern-study/patterns/gridfonts.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/incoming/pattern-study/patterns/gridfonts.js b/incoming/pattern-study/patterns/gridfonts.js index 1b0efa4fd..45880816c 100644 --- a/incoming/pattern-study/patterns/gridfonts.js +++ b/incoming/pattern-study/patterns/gridfonts.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Gridfonts // Family: type -// Idea: Three lines of three lowercase letters are built only from short strokes joining neighbouring points of a faint 3 by 7 dot grid, as in Douglas Hofstadter's gridfonts, and when the style changes every letter on the panel takes back and draws in its strokes by the same rule at once, cutting its corners, raising gables or opening stencil gaps. +// Idea: Three lines of three lowercase letters are built only from short strokes joining neighbouring points of a faint 3 by 7 dot grid, as in Douglas Hofstadter's gridfonts, and when the style changes every letter on the panel takes back and draws in its strokes by the same rule at once, cutting its corners, raising gables, opening stencil gaps or turning to outline. // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -13,8 +13,8 @@ // // Knob 1: Hue. Turns the letters round the wheel from sea green at 0; each style keeps its own small step off that colour. // Knob 2: Speed. How fast strokes draw and the pages turn: at 2 something changes every 4 seconds, at 0.2 every 40. -// Knob 3: Style. At 0 the six styles take turns, every other change a new style on the same words and then new words in that style; 1 to 6 hold one style: Square, Cut, Lean, Gable, Stencil, Inline. -// Knob 4: Grid. The grid under the letters: dim points at 0, brighter points from 0.06, and toward 1 all 56 strokes every letter could use, faintly, in each letter's cell. +// Knob 3: Style. At 0 the six styles take turns: one change puts a new style on the same words, the next brings new words in that style. 1 to 6 hold one style while the words change: Square, Cut, Lean, Gable, Stencil, Outline. +// Knob 4: Grid. What shows of the grid under the letters: only its points at 0, brighter up to 0.33, and from 0.1 upward, more and more, all 56 quanta of each letter's cell, the strokes every letter could have used. const W = 64, H = 128, N = W * H; const { floor, ceil, round, min, max, abs, sqrt, exp } = Math; @@ -205,8 +205,8 @@ const FACES = STYLES.map((st) => { return face; }); -// Pages of three three-letter lines. The alphabet pages are the specimen -// sheet every gridfont was shown on. +// Pages of three three-letter lines. Three of them are the alphabet, a to +// z, the way a gridfont is shown. const PAGES = ["the", "sly", "fox", "abc", "def", "ghi", "web", "zig", "zag", "jkl", "mno", "pqr", "sea", "and", "sky", "stu", "vwx", "yz ", "art", "for", "all"] .join("") .match(/.{9}/g); From 32aa9309536b829c2cf35fb0ca099fab0e97fe9b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:48:01 +0000 Subject: [PATCH 13/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Syllable=20Odometer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- incoming/pattern-study/patterns/syllable-odometer.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/incoming/pattern-study/patterns/syllable-odometer.js b/incoming/pattern-study/patterns/syllable-odometer.js index d960f82f6..ffd65db6a 100644 --- a/incoming/pattern-study/patterns/syllable-odometer.js +++ b/incoming/pattern-study/patterns/syllable-odometer.js @@ -298,7 +298,7 @@ function tape(symbols) { } export function setup(params) { - const S = (params.s = { t: 0, n: 0, p: 0, acc: 0, k3: -1, shown: -1, mapC: -1, fade: 0, rk: -1 }); + const S = (params.s = { n: 0, p: 0, acc: 0, k3: -1, shown: -1, mapC: -1, fade: 0, rk: -1 }); // The set, and for each syllable how many of the 2,350 come at or before it. S.ks = new Uint8Array(ALL); S.rank = new Uint16Array(ALL); @@ -379,7 +379,6 @@ export function update(dt, input, params) { if (!params.s) setup(params); const S = params.s, k = input.knobValues || [0, 2, 0, 0.06]; dt = cl(dt || 0, 0, 0.1); - S.t += dt; S.hue = k[0] || 0; const speed = cl(k[1], 0.2, 3); const d = speed - 2; From 67bcc566006b280deac5f7670491197310546569 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:49:18 +0000 Subject: [PATCH 14/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Shapley-Folkman?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pattern-study/patterns/shapley-folkman.js | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/incoming/pattern-study/patterns/shapley-folkman.js b/incoming/pattern-study/patterns/shapley-folkman.js index b46e67341..035e2c354 100644 --- a/incoming/pattern-study/patterns/shapley-folkman.js +++ b/incoming/pattern-study/patterns/shapley-folkman.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Shapley-Folkman // Family: geometry -// Idea: Four bands down the panel show one nonconvex shape and its Minkowski averages of 2, 4 and 8 copies, each computed exactly as a sum of lattice point sets, so the bays and holes close band by band toward the convex hull drawn faintly round each one, the gap halving at every step. +// Idea: Four bands down the panel hold one nonconvex shape and its Minkowski averages of 2, 4 and 8 copies, each summed exactly on a lattice, so its bays and holes close band by band toward the convex hull drawn faintly round it, the distance still to go halving at every band and every point coloured by the band where it first appears. // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -11,10 +11,24 @@ // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Shape=0..4, Hull=0..1 // -// Knob 1: Hue. Turns the band colours round the wheel together. -// Knob 2: Speed. How fast the shape turns or moves. -// Knob 3: Shape. Moon, pearls, open ring, comb and star at 0 to 4, blended in between. -// Knob 4: Hull. How strongly the convex hull and its uncovered part are drawn. +// Knob 1: Hue. Turns the four layer colours round a wheel that skips orange. At 0 the shape is teal and the parts added at 2, 4 and 8 copies azure, indigo and violet; at 0.25 blue, violet, magenta and yellow; at 0.75 lime, green, cyan and azure. The uncovered part of the hull is gold at 0 and turns with them. +// Knob 2: Speed. How fast the shape moves: at 2 the moon's bite, the pearls and the ring's gap go round in 9 s, the star turns one point in 1.8 s and the comb's teeth rise and fall every 6.9 s; at 0.2 ten times slower, at 3 two thirds of that. Every band follows at once, since all four are summed again each frame. +// Knob 3: Shape. 0 a moon (an ellipse with a bite), 1 three pearls, 2 an open ring, 3 a comb, 4 a five-pointed star; in between, the two neighbours blend and break into pieces. At 1 copy they leave 43, 51, 45, 37 and 45% of their hull uncovered, at 8 copies 3.7, 1.3, 0.7, 0.9 and 4.4%. +// Knob 4: Hull. The convex hull is a faint line at 0 and 0.06; toward 1 the line brightens and the part of the hull each band has not yet covered fills with gold, which is the gap the lemma says shrinks like 1/n. + +// The Shapley-Folkman lemma: in the plane, a point of the convex hull of a +// sum of n sets is a sum of n points, one from each set, of which at most +// two come from a set's hull instead of the set. So the average of n copies +// of a shape S, A(n) = (S + ... + S) / n, comes within 2D/n of every point +// of S's hull, where D is how far the hull reaches from S itself: the +// average fills toward the hull like 1/n. A(1), A(2), A(4) and A(8) also +// nest, since (a + b) / 2 = (a + a + b + b) / 4. +// +// The panel takes S as the lattice points, one per LED, inside the shape, +// sums the point sets exactly (8 copies make a lattice of 465 by 217), +// and lights each LED by the share of A(n)'s points that fall inside it. +// Brightness is never how many ways a point can be reached: that count is +// the bell of the central limit theorem, a different statement. const W = 64, H = 128; const { floor, min, max, abs, sqrt, cos, sin, atan2, PI } = Math; @@ -298,9 +312,12 @@ export function draw(display, params) { // Colour by n: each band's points are coloured by the band where they // first appear, since A(1), A(2), A(4) and A(8) each hold the one before. // Older layers are deep, the layer new in this band is pale and bright. + // The wheel skips orange, which goes brown at the depth of the old + // layers: the knob runs from yellow through green, blue and violet to red. + const wheel = (p) => 0.16 + (p - floor(p)) * 0.84; for (let j = 0; j < NS.length; j++) { - const h = S.hue + 0.47 + 0.12 * j; - hsv(h, 0.9, 0.5, c); + const h = wheel(S.hue + 0.352 + 0.136 * j); + hsv(h, 0.88, 0.58, c); L.set(c, j * 9); hsv(h + 0.03, 0.45, 1, c); L.set(c, j * 9 + 3); From 58bed82ab266ace4b2a22861853a96dba97b4fb7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:49:50 +0000 Subject: [PATCH 15/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Wilkinson's=20Polynomial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../patterns/wilkinson-polynomial.js | 314 ++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 incoming/pattern-study/patterns/wilkinson-polynomial.js diff --git a/incoming/pattern-study/patterns/wilkinson-polynomial.js b/incoming/pattern-study/patterns/wilkinson-polynomial.js new file mode 100644 index 000000000..81e13015a --- /dev/null +++ b/incoming/pattern-study/patterns/wilkinson-polynomial.js @@ -0,0 +1,314 @@ +// ===== Patternflow pattern ===== +// Title: Wilkinson's Polynomial +// Family: algo +// Idea: The twenty roots of (x - 1)(x - 2)...(x - 20) stand up the panel as beads on the real axis, and as a perturbation of the x^19 coefficient sweeps up past Wilkinson's 2^-23 and back the upper ten slide together in pairs and fly apart sideways into the complex plane, leaving arcs, while clouds of roots of randomly nudged copies show how far each root can be pushed and a faint dust maps the smallest nudge that would put a root anywhere. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Wilkinson's polynomial (James H. Wilkinson, 1959; "The perfidious polynomial", 1984), Aberth's simultaneous root iteration (Oliver Aberth, 1973), pseudozero sets (Ronald G. Mosier, 1986; Kim-Chuan Toh and Lloyd N. Trefethen, 1994), pattern round 66 +// Round: 66 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Phase=0..1, Cloud=0..1 +// +// Knob 1: Hue. Turns every colour round the wheel together. +// Knob 2: Speed. How fast the perturbation sweeps. +// Knob 3: Phase. The direction of the perturbation in the complex plane. +// Knob 4: Cloud. The size of the random nudges whose roots make the clouds. +// Buttons: K1 tap holds the perturbation at 2^-23. K3 tap moves the perturbation down one power of x. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, exp, log2, cos, sin, sqrt, hypot, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const rnd = () => Math.random(); + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// The plane: the real axis runs up the panel, RY rows to a unit, with +// Re = RE0 at the bottom edge, so root k sits on the middle of row +// 123 - 6 (k - 1); the imaginary axis runs across, CX columns to a unit, +// with Im = 0 on the middle of column 31. +const RY = 6, RE0 = 0.25, CX = 9, XC = 31.5; +const toX = (im) => XC + im * CX, toY = (re) => H - (re - RE0) * RY; + +// The coefficients a_j of p(x) = (x - 1)...(x - 20), lowest first. Doubles +// are enough here: they only scale the perturbations. +const A = new Float64Array(21); +A[0] = 1; +for (let k = 1; k <= 20; k++) for (let j = k; j >= 0; j--) A[j] = (j ? A[j - 1] : 0) - k * A[j]; +const AB = A.map(abs); +// SENS[k]: the largest first-order shift a relative change of a_k can give any +// root, |a_k| r^k / |p'(r)| at its worst r. Every coefficient's sweep is +// scaled so that it pushes that root as hard as Wilkinson's 2^-23 on x^19. +const SENS = new Float64Array(20); +for (let k = 0; k < 20; k++) + for (let r = 1; r <= 20; r++) { + let d = 1; + for (let j = 1; j <= 20; j++) if (j !== r) d *= abs(r - j); + SENS[k] = max(SENS[k], (AB[k] * r ** k) / d); + } + +// The sweep runs log2 of the perturbation from LO to HI and back, easing at +// both ends; at LO the roots sit on the integers to within 0.001. +const LO = -35, HI = -21, WILK = -23; + +// Aberth's iteration for the roots of p(z) + sum C_j z^j, j < 20, from the +// starting points in Z (re, im pairs), at most `iters` sweeps, roots with +// index below `from` held where they are. p and p' come from the product +// form, which stays accurate next to the roots where the expanded +// coefficients would cancel. Returns the largest last step squared. +const STEP = new Float64Array(20); +function aberth(Z, Cr, Ci, iters, from) { + let big = 0; + for (let it = 0; it < iters; it++) { + big = 0; + for (let i = from; i < 20; i++) { + const zr = Z[2 * i], zi = Z[2 * i + 1]; + let pr = 1, pi = 0, dr = 0, di = 0; + for (let k = 1; k <= 20; k++) { + const ur = zr - k; + const t = dr * ur - di * zi + pr; + di = dr * zi + di * ur + pi; + dr = t; + const s = pr * ur - pi * zi; + pi = pr * zi + pi * ur; + pr = s; + } + let qr = 0, qi = 0, er = 0, ei = 0; + for (let j = 19; j >= 0; j--) { + const t = er * zr - ei * zi + qr; + ei = er * zi + ei * zr + qi; + er = t; + const s = qr * zr - qi * zi + Cr[j]; + qi = qr * zi + qi * zr + Ci[j]; + qr = s; + } + const fr = pr + qr, fi = pi + qi, gr = dr + er, gi = di + ei; + let m = gr * gr + gi * gi; + if (!(m > 0)) continue; + const wr = (fr * gr + fi * gi) / m, wi = (fi * gr - fr * gi) / m; + let ar = 0, ai = 0; + for (let j = 0; j < 20; j++) + if (j !== i) { + const xr = zr - Z[2 * j], xi = zi - Z[2 * j + 1], mm = xr * xr + xi * xi; + if (mm > 0) (ar += xr / mm), (ai -= xi / mm); + } + const br = 1 - (wr * ar - wi * ai), bi = -(wr * ai + wi * ar); + m = br * br + bi * bi; + if (!(m > 0)) continue; + const sr = (wr * br + wi * bi) / m, si = (wi * br - wr * bi) / m; + Z[2 * i] -= sr; + Z[2 * i + 1] -= si; + const st = sr * sr + si * si; + STEP[i] = st; + if (st > big) big = st; + } + if (big < 1e-24) break; + } + return big; +} + +// A real polynomial keeps real iterates real, so a root that is on the +// axis gets a tiny imaginary part, alternating up the axis, before each +// solve: two roots that meet can then leave the axis in opposite ways. +const ORD = []; +function seed(Z) { + ORD.length = 0; + for (let i = 0; i < 20; i++) ORD.push(i); + ORD.sort((a, b) => Z[2 * a] - Z[2 * b]); + for (let k = 0; k < 20; k++) { + const i = ORD[k]; + if (abs(Z[2 * i + 1]) < 1e-9) Z[2 * i + 1] = k & 1 ? 1e-9 : -1e-9; + } +} + +export function setup(params) { + const F = (n) => new Float32Array(n); + const Z = new Float64Array(40); + for (let i = 0; i < 20; i++) Z[2 * i] = i + 1; + params.s = { + t: 0, ph: 0.17, e2: LO, pow: 19, pow0: 19, mix: 1, hold: 0, warm: 0, + Z, Zo: new Float64Array(40), W: new Float64Array(40), + Cr: new Float64Array(20), Ci: new Float64Array(20), Br: new Float64Array(20), Bi: new Float64Array(20), + TI: F(N), TC: new Uint8Array(N), CR: F(N), CG: F(N), CB: F(N), DR: F(N), DG: F(N), DB: F(N), + pal: F(60), col: F(3), hue: 0, ld: -46, th: 0, + }; +} + +// Trails: each root draws the segment it moved along this frame into TI as +// a line about three LEDs wide, keeping the brighter of what is there and +// the new line. +function trail(S, x0, y0, x1, y1, c) { + const n = max(1, Math.ceil(2 * hypot(x1 - x0, y1 - y0))); + if (n > 200) return; + for (let s = 0; s <= n; s++) { + const x = x0 + ((x1 - x0) * s) / n, y = y0 + ((y1 - y0) * s) / n; + for (let yy = floor(y - 2); yy <= y + 1; yy++) + for (let xx = floor(x - 2); xx <= x + 1; xx++) { + if (xx < 0 || xx >= W || yy < 0 || yy >= H) continue; + const w = (1 - hypot(xx + 0.5 - x, yy + 0.5 - y) / 1.7) * (0.6 + 0.4 * rnd()), p = yy * W + xx; + if (w > S.TI[p]) (S.TI[p] = w), (S.TC[p] = c); + } + } +} + +function splat(R, G, B, x, y, r, g, b) { + const ix = floor(x - 0.5), iy = floor(y - 0.5), ax = x - 0.5 - ix, ay = y - 0.5 - iy; + for (let q = 0; q < 4; q++) { + const xx = ix + (q & 1), yy = iy + (q >> 1); + if (xx < 0 || xx >= W || yy < 0 || yy >= H) continue; + const w = (q & 1 ? ax : 1 - ax) * (q >> 1 ? ay : 1 - ay), p = yy * W + xx; + R[p] += w * r, G[p] += w * g, B[p] += w * b; + } +} + +// The perturbation: the coefficient of x^pow times 1 + eta e^(2 pi i th), +// with eta scaled so that 2^e2 on x^19 is Wilkinson's own change. After a +// K3 tap it slides from the old power to the new one over half a second. +function perturb(S) { + const a = 2 * PI * S.th, m = S.mix; + S.Cr.fill(0), S.Ci.fill(0); + for (const [k, f] of [[S.pow0, 1 - m], [S.pow, m]]) { + const e = f * 2 ** S.e2 * (AB[k] / 210) * (SENS[19] / SENS[k]) * Math.sign(A[k]); + S.Cr[k] += e * cos(a); + S.Ci[k] += e * sin(a); + } +} + +function step(S, d, sp, clouds) { + // While K1's hold lasts the sweep waits and the perturbation eases to + // 2^-23; at most ten octaves a second either way. + if (S.hold > 0) S.hold -= d; + else S.ph += (d * sp) / 24; + S.mix = min(1, S.mix + d * 2); + const u = (1 - cos(2 * PI * S.ph)) / 2, to = S.hold > 0 ? WILK : LO + (HI - LO) * u; + S.e2 += cl(to - S.e2, -10 * d, 10 * d); + perturb(S); + const { Z, Zo } = S; + Zo.set(Z); + seed(Z); + aberth(Z, S.Cr, S.Ci, 40, 0); + for (let i = 0; i < 20; i++) trail(S, toX(Zo[2 * i + 1]), toY(Zo[2 * i]), toX(Z[2 * i + 1]), toY(Z[2 * i]), i); + if (clouds) cloud(S, clouds); +} + +// Clouds: the roots of p + C with every coefficient below the top one also +// multiplied by 1 + delta u_j, u_j uniform in [-1, 1], solved from the +// current roots. Each root lands as a dot of its root's colour and its own +// brightness. +function cloud(S, n) { + const { Z, W: Zw, Br, Bi, Cr, Ci, CR, CG, CB, pal } = S, dl = 2 ** S.ld; + for (let m = 0; m < n; m++) { + for (let j = 0; j < 20; j++) (Br[j] = Cr[j] + dl * (2 * rnd() - 1) * A[j]), (Bi[j] = Ci[j]); + Zw.set(Z); + seed(Zw); + aberth(Zw, Br, Bi, 12, 4); + for (let i = 4; i < 20; i++) { + if (STEP[i] > 1e-8) continue; + const w = 0.25 + 0.75 * rnd(); + const x = floor(toX(Zw[2 * i + 1])), y = floor(toY(Zw[2 * i])); + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const q = y * W + x; + (CR[q] = w * pal[3 * i]), (CG[q] = w * pal[3 * i + 1]), (CB[q] = w * pal[3 * i + 2]); + } + } +} + +// Dust: LEDs picked at random take a grain coloured by the pseudozero level +// at a random point inside them, L = log2 |p(z) + C(z)| / sum |a_j| |z|^j: +// the smallest relative change of the coefficients that makes z a root. +function dust(S, n) { + const { Cr, Ci, DR, DG, DB, col } = S; + for (let m = 0; m < n; m++) { + const p = floor(rnd() * N), x = (p & 63) + rnd(), y = (p >> 6) + rnd(); + const zr = RE0 + (H - y) / RY, zi = (x - XC) / CX; + let pr = 1, pi = 0; + for (let j = 1; j <= 20; j++) { + const ur = zr - j, s = pr * ur - pi * zi; + pi = pr * zi + pi * ur; + pr = s; + } + // Plus the perturbation, and sum |a_j| |z|^j. + let qr = 0, qi = 0; + for (let j = 19; j >= 0; j--) { + const s = qr * zr - qi * zi + Cr[j]; + qi = qr * zi + qi * zr + Ci[j]; + qr = s; + } + (pr += qr), (pi += qi); + const r = hypot(zr, zi); + let sum = 0; + for (let j = 19; j >= 0; j--) sum = sum * r + AB[j]; + const L = log2(hypot(pr, pi) / sum + 1e-300), depth = S.ld - L, q = cl((depth + 8) / 14, 0, 1); + hsv(S.hue + 0.68 - 0.2 * q, 0.8, 1, col, 0); + if (depth > -8) { + const v = rnd() < 0.2 + 0.5 * q ? 25 + (40 + 190 * q) * rnd() : 24.9 * rnd(), w = 0.4 * rnd(); + DR[p] = v * (col[0] + w * (1 - col[0])), DG[p] = v * (col[1] + w * (1 - col[1])), DB[p] = v * (col[2] + w * (1 - col[2])); + } else (DR[p] = 24.99 * (0.15 * col[0] + 0.85 * rnd())), (DG[p] = 24.99 * (0.15 * col[1] + 0.85 * rnd())), (DB[p] = 24.99 * (0.15 * col[2] + 0.85 * rnd())); + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = cl(kv[0], 0, 1); + const sp = cl(kv[1], 0.2, 3); + S.th = cl(kv[2], 0, 1); + S.ld = -45 + 6 * cl(kv[3], 0, 1); + for (let i = 0; i < 20; i++) hsv(S.hue + 0.8 + (0.17 * i) / 19, 0.75, 1, S.pal, 3 * i); + const p = input.btnPressed || []; + if (p[0]) S.hold = 3; + if (p[2]) (S.pow0 = S.pow), (S.pow = S.pow ? S.pow - 1 : 19), (S.mix = 0); + // The first frame starts a second and a half into the sweep, so the + // first pairs have left the axis and drawn their arcs. + if (!S.warm) { + S.warm = 1; + const u = (1 - cos(2 * PI * S.ph)) / 2, e0 = LO + (HI - LO) * u; + for (let e = LO; e < e0; e += 0.25) (S.e2 = min(e, e0)), perturb(S), seed(S.Z), aberth(S.Z, S.Cr, S.Ci, 40, 0); + S.e2 = e0; + for (let i = 0; i < 45; i++) step(S, 1 / 30, sp, i > 30 ? 4 : 0), fade(S, 1 / 30), dust(S, 900); + } + S.t += d; + step(S, d, sp, 8); + fade(S, d); + dust(S, 1200); +} + +function fade(S, d) { + const ft = exp(-d / 3.5), fc = exp(-d / 2), fd = exp(-d / 3); + const { TI, CR, CG, CB, DR, DG, DB } = S; + for (let p = 0; p < N; p++) { + TI[p] *= ft; + CR[p] *= fc, CG[p] *= fc, CB[p] *= fc; + DR[p] *= fd, DG[p] *= fd, DB[p] *= fd; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.warm) return; + const { TI, TC, CR, CG, CB, DR, DG, DB, pal, Z } = S; + const R = new Float32Array(N), G = new Float32Array(N), B = new Float32Array(N); + for (let p = 0; p < N; p++) { + const ti = TI[p], c = 3 * TC[p], tv = ti > 0.02 ? 190 * ti : 0, cg = 0.35; + R[p] = DR[p] + tv * pal[c] + 235 * CR[p]; + G[p] = DG[p] + tv * pal[c + 1] + 235 * CG[p]; + B[p] = DB[p] + tv * pal[c + 2] + 235 * CB[p]; + } + // The integers 1 to 20 marked on both edges, longer at every fifth. + for (let k = 1; k <= 20; k++) { + const y = 123 - 6 * (k - 1), l = k % 5 ? 2 : 3; + for (let x = 0; x < l; x++) for (const xx of [x, 63 - x]) (R[y * W + xx] += 30), (G[y * W + xx] += 34), (B[y * W + xx] += 40); + } + for (let i = 0; i < 20; i++) { + const x = toX(Z[2 * i + 1]), y = toY(Z[2 * i]), c = 3 * i; + splat(R, G, B, x, y, 160 + 300 * pal[c], 160 + 300 * pal[c + 1], 160 + 300 * pal[c + 2]); + } + for (let p = 0; p < N; p++) display.setPixel(p & 63, p >> 6, cl(R[p], 0, 255) | 0, cl(G[p], 0, 255) | 0, cl(B[p], 0, 255) | 0); +} From 0d288a21e9e6c4ed90befd34c52fb133b3ac5b2d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:51:03 +0000 Subject: [PATCH 16/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Shapley-Folkman?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- incoming/pattern-study/patterns/shapley-folkman.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/incoming/pattern-study/patterns/shapley-folkman.js b/incoming/pattern-study/patterns/shapley-folkman.js index 035e2c354..6412c0d19 100644 --- a/incoming/pattern-study/patterns/shapley-folkman.js +++ b/incoming/pattern-study/patterns/shapley-folkman.js @@ -14,7 +14,7 @@ // Knob 1: Hue. Turns the four layer colours round a wheel that skips orange. At 0 the shape is teal and the parts added at 2, 4 and 8 copies azure, indigo and violet; at 0.25 blue, violet, magenta and yellow; at 0.75 lime, green, cyan and azure. The uncovered part of the hull is gold at 0 and turns with them. // Knob 2: Speed. How fast the shape moves: at 2 the moon's bite, the pearls and the ring's gap go round in 9 s, the star turns one point in 1.8 s and the comb's teeth rise and fall every 6.9 s; at 0.2 ten times slower, at 3 two thirds of that. Every band follows at once, since all four are summed again each frame. // Knob 3: Shape. 0 a moon (an ellipse with a bite), 1 three pearls, 2 an open ring, 3 a comb, 4 a five-pointed star; in between, the two neighbours blend and break into pieces. At 1 copy they leave 43, 51, 45, 37 and 45% of their hull uncovered, at 8 copies 3.7, 1.3, 0.7, 0.9 and 4.4%. -// Knob 4: Hull. The convex hull is a faint line at 0 and 0.06; toward 1 the line brightens and the part of the hull each band has not yet covered fills with gold, which is the gap the lemma says shrinks like 1/n. +// Knob 4: Hull. The convex hull is a faint line at 0 and at the opening 0.06. From 0.1 the part of the hull a band has not covered yet is hatched in gold, the hatching at full strength by 0.4, and from there it fills in until at 1 that whole part is gold. It is the gap the lemma says shrinks like 1/n: for the moon, 43, 20, 8.6 and 3.7% of the hull in the four bands. // The Shapley-Folkman lemma: in the plane, a point of the convex hull of a // sum of n sets is a sum of n points, one from each set, of which at most @@ -190,7 +190,7 @@ export function setup(params) { for (let k = 1; k <= NMAX; k++) sums.push(bitmap(k * (SW - 1) + 1, k * (SH - 1) + 1)); const big = sums[NMAX - 1].w * sums[NMAX - 1].h; params.s = { - t: 0, phase: 0, hue: 0, shape: 0, hk: 0.06, + phase: 0, hue: 0, shape: 0, hk: 0.06, sums, xs: new Int32Array(big), ys: new Int32Array(big), lab: new Uint8Array(SW * SH), stack: new Int32Array(SW * SH * 9), reps: [], cov: new Float32Array(NS.length * SW * SH), sd: new Float32Array(SW * SH), @@ -298,7 +298,6 @@ export function update(dt, input, params) { const speed = cl(kv[1] ?? 2, 0.2, 3); S.shape = cl(kv[2] || 0, 0, 4); S.hk = cl(kv[3] ?? 0.06, 0, 1); - S.t += d; S.phase += d * speed * 0.35; build(S); } @@ -308,7 +307,10 @@ export function draw(display, params) { if (!S) return; const rgb = S.rgb, c = S.c, L = S.lay, unc = S.unc, cov = S.cov, A = SW * SH; rgb.fill(0); - const hk = S.hk; + // The uncovered part of the hull: gold hatching that comes in from Hull + // 0.1 to 0.4, then fills between its lines up to 1. Gold dimmed evenly + // would read brown. + const hk = S.hk, hatch = cl((hk - 0.1) / 0.3, 0, 1) * 0.85, fill = cl((hk - 0.4) / 0.6, 0, 1) * 0.85; // Colour by n: each band's points are coloured by the band where they // first appear, since A(1), A(2), A(4) and A(8) each hold the one before. // Older layers are deep, the layer new in this band is pale and bright. @@ -353,7 +355,7 @@ export function draw(display, params) { } const glow = u * cv * 0.7, gq = bi * 9 + 3; const line = cl(1 - abs(sd) / 0.8, 0, 1) * (0.14 + 0.5 * hk); - const gap = unc[k] * hk * 0.85; + const gap = unc[k] * ((x + y) % 3 ? fill : hatch); const q = ((bi * BAND + Y0 + y) * W + X0 + x) * 3; rgb[q] = r + L[gq] * glow + fr * gap + line * 0.8; rgb[q + 1] = g + L[gq + 1] * glow + fg * gap + line * 0.75; From 4d54b943ad7b4a5609d6dc9a75286c60f9a20be2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:53:55 +0000 Subject: [PATCH 17/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Barrier=20Grid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pattern-study/patterns/barrier-grid.js | 335 ++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 incoming/pattern-study/patterns/barrier-grid.js diff --git a/incoming/pattern-study/patterns/barrier-grid.js b/incoming/pattern-study/patterns/barrier-grid.js new file mode 100644 index 000000000..e8680339a --- /dev/null +++ b/incoming/pattern-study/patterns/barrier-grid.js @@ -0,0 +1,335 @@ +// ===== Patternflow pattern ===== +// Title: Barrier Grid +// Family: type +// Idea: A barrier of dark bars with one slit in every two to four columns slides one column at a time over a print that interleaves a word and its negative column by column, so each step turns the word from light to shadow and back, every fourth step brings the next word, the frames mix while the barrier glides, and a tilted barrier lays moire bands across the word. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: barrier-grid animation as in the scanimation books of Rufus Butler Seder (Gallop!, 2007); moire of two superposed line gratings (Lord Rayleigh, 1874), pattern round 66 +// Round: 66 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Frames=2..4, Tilt=0..1 +// +// Knob 1: Hue. The light behind the slits: at 0 the word glows gold and its negative is the opposite blue, and turning it moves the pair round the wheel, always opposite each other the way a colour negative is. +// Knob 2: Speed. How fast the barrier steps: at 2 a step every 1.6 seconds (held 0.96 s, gliding 0.64 s) and a new word every 6.4 seconds, at 0.2 a step every 16 seconds, at 3 one every 1.07. +// Knob 3: Frames. How many frames the print interleaves, which is the period of the line screen and of the barrier: at 2 every other column is lit, at 3 every third, at 4 every fourth, with the bars wider in between. +// Knob 4: Tilt. Turns the barrier against the print, so the slits drift sideways down the panel by up to three periods at 1 (tilt squared, so 0.06 is nearly level): moire bands cross the word, light in one band and shadow in the next, and roll down it while the barrier glides. + +const W = 64, H = 128, N = W * H; +const Q = 3; // supersampling of the glyph masks +const { floor, round, min, max, ceil } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// Words of light and of looking. A Hangul word is one syllable filling the +// panel; a Latin word is four letters, two by two, 29 by 58 LEDs each. +const WORDS = ["빛", "LOOK", "달", "NEON", "별", "HALO", "ë°€", "SEEN"]; + +// Shapes: [kind, add, ...]. kind 0 a rounded box x0 y0 x1 y1 tl tr br bl, +// kind 1 a thick segment x0 y0 x1 y1 width. +function glyph(ch, w, h, s, t) { + const P = []; + const rr = (x0, y0, x1, y1, tl = 0, tr = 0, br = 0, bl = 0, add = 1) => P.push([0, add, x0, y0, x1, y1, tl, tr, br, bl]); + const ln = (x0, y0, x1, y1, wd, add = 1) => P.push([1, add, x0, y0, x1, y1, wd]); + const R = 13, r = 5; + switch (ch) { + case "O": + rr(0, 0, w, h, R, R, R, R); + rr(s, t, w - s, h - t, r, r, r, r, 0); + break; + case "L": + rr(0, 0, s, h); + rr(0, h - t, w, h); + break; + case "K": + rr(0, 0, s, h); + ln(s - 1, h * 0.58, w - 5, 4, 10); + ln(s + 5, h * 0.42, w - 5, h - 4, 10); + rr(w - 9, 0, w, 6, 0, 0, 0, 0, 0); + break; + case "N": + rr(0, 0, s, h); + rr(w - s, 0, w, h); + ln(4, 4, w - 4, h - 4, 10); + break; + case "E": + rr(0, 0, s, h); + rr(0, 0, w, t); + rr(0, round(h / 2 - t / 2), w - 4, round(h / 2 + t / 2)); + rr(0, h - t, w, h); + break; + case "H": + rr(0, 0, s, h); + rr(w - s, 0, w, h); + rr(0, round(h / 2 - t / 2), w, round(h / 2 + t / 2)); + break; + case "A": + rr(0, 0, w, h, R, R, 0, 0); + rr(s, t, w - s, h + 1, r, r, 0, 0, 0); + rr(0, round(h * 0.55), w, round(h * 0.55) + t); + break; + case "S": { + // Two bowls sharing the middle bar, each open on one side. + const m = round(h / 2 - t / 2); + rr(0, 0, w, m + t, R, R, R, R); + rr(s, t, w - s, m, r, r, r, r, 0); + rr(w - s, t + 4, w + 1, m, 0, 0, 0, 0, 0); + rr(0, m, w, h, R, R, R, R); + rr(s, m + t, w - s, h - t, r, r, r, r, 0); + rr(-1, m + t, s, h - t - 4, 0, 0, 0, 0, 0); + break; + } + default: + hangul(ch, w, h, s, t, rr, ln); + } + return P; +} + +// The four Hangul syllables, each one filling the panel: the initial top +// left, the vowel's stem on the right, the final across the lower half. The +// coordinates are the panel's, and the upright strokes stand on multiples of +// 4 LEDs, so a stroke covers whole columns of the line screen. +function hangul(ch, w, h, s, t, rr, ln) { + // ㅂ, ㄷ, ㄹ and ㅁ from bars. + const b = (x0, y0, x1, y1) => { + const m = round(y0 + (y1 - y0) * 0.45); + rr(x0, y0, x0 + s, y1); + rr(x1 - s, y0, x1, y1); + rr(x0, m, x1, m + t); + rr(x0, y1 - t, x1, y1); + }; + const d = (x0, y0, x1, y1) => { + rr(x0, y0, x1, y0 + t); + rr(x0, y0, x0 + s, y1); + rr(x0, y1 - t, x1, y1); + }; + const rieul = (x0, y0, x1, y1) => { + const m = round((y0 + y1) / 2 - t / 2); + rr(x0, y0, x1, y0 + t); + rr(x1 - s, y0, x1, m + t); + rr(x0, m, x1, m + t); + rr(x0, m, x0 + s, y1); + rr(x0, y1 - t, x1, y1); + }; + const mieum = (x0, y0, x1, y1) => { + rr(x0, y0, x1, y1, 2, 2, 2, 2); + rr(x0 + s, y0 + t, x1 - s, y1 - t, 0, 0, 0, 0, 0); + }; + // ㅏ: the stem with its arm to the right. + const a = () => { + rr(44, 4, 56, 68); + rr(56, 30, 64, 39); + }; + if (ch === "빛") { + b(4, 8, 40, 64); + rr(48, 4, 60, 70); + rr(28, 74, 36, 82); + rr(4, 84, 60, 93); + ln(32, 95, 12, 124, 13); + ln(32, 95, 52, 124, 13); + } else if (ch === "달") { + d(4, 8, 36, 64); + a(); + rieul(8, 76, 56, 124); + } else if (ch === "별") { + b(4, 8, 32, 64); + rr(48, 4, 60, 70); + rr(36, 20, 48, 29); + rr(36, 42, 48, 51); + rieul(8, 76, 56, 124); + } else if (ch === "ë°€") { + b(4, 8, 36, 64); + a(); + mieum(8, 76, 56, 124); + } +} + +function inside(p, x, y) { + if (p[0] === 0) { + if (x < p[2] || x > p[4] || y < p[3] || y > p[5]) return false; + const cx = (p[2] + p[4]) / 2, cy = (p[3] + p[5]) / 2; + const left = x < cx, top = y < cy; + const r = left ? (top ? p[6] : p[9]) : top ? p[7] : p[8]; + if (r <= 0) return true; + const dx = left ? p[2] + r - x : x - (p[4] - r); + const dy = top ? p[3] + r - y : y - (p[5] - r); + if (dx <= 0 || dy <= 0) return true; + return dx * dx + dy * dy <= r * r; + } + const ex = p[4] - p[2], ey = p[5] - p[3]; + const u = cl(((x - p[2]) * ex + (y - p[3]) * ey) / (ex * ex + ey * ey), 0, 1); + const dx = x - p[2] - u * ex, dy = y - p[3] - u * ey; + return dx * dx + dy * dy <= (p[6] * p[6]) / 4; +} + +// The shapes of a word placed on the panel, each with the rows and columns +// of the supersampled mask it may touch. +function layout(word) { + const chars = [...word]; + const big = chars.length === 1; + const cells = big ? [[0, 0, 64, 128]] : [[2, 4, 29, 58], [33, 4, 29, 58], [2, 66, 29, 58], [33, 66, 29, 58]]; + const out = []; + chars.forEach((ch, gi) => { + const [cx, cy, w, h] = cells[gi]; + for (const p of glyph(ch, w, h, big ? 12 : 8, big ? 9 : 7)) { + let x0, y0, x1, y1; + if (p[0] === 0) (x0 = p[2]), (y0 = p[3]), (x1 = p[4]), (y1 = p[5]); + else { + const r = p[6] / 2; + (x0 = min(p[2], p[4]) - r), (x1 = max(p[2], p[4]) + r), (y0 = min(p[3], p[5]) - r), (y1 = max(p[3], p[5]) + r); + } + out.push({ + p, cx, cy, + sx0: max(cx * Q, floor((cx + x0) * Q)), sx1: min((cx + w) * Q, ceil((cx + x1) * Q)), + sy0: max(cy * Q, floor((cy + y0) * Q)), sy1: min((cy + h) * Q, ceil((cy + y1) * Q)), + }); + } + }); + return out; +} + +// A word's mask is built 8 rows at a time, a few frames ahead of the step +// that needs it, so no frame pays for a whole word. Each shape inks (1) or +// cuts (0) the samples inside it, in order, and the samples are averaged +// back to coverage per LED. +const CHUNK = 8; +function buildRows(B, rows) { + const r0 = B.row, r1 = min(H, r0 + rows); + const hi = B.hi, WQ = W * Q; + for (const sh of B.shapes) { + const sy0 = max(sh.sy0, r0 * Q), sy1 = min(sh.sy1, r1 * Q); + for (let sy = sy0; sy < sy1; sy++) + for (let sx = sh.sx0; sx < sh.sx1; sx++) + if (inside(sh.p, (sx + 0.5) / Q - sh.cx, (sy + 0.5) / Q - sh.cy)) hi[sy * WQ + sx] = sh.p[1]; + } + for (let y = r0; y < r1; y++) + for (let x = 0; x < W; x++) { + let n = 0; + for (let j = 0; j < Q; j++) for (let i = 0; i < Q; i++) n += hi[(y * Q + j) * WQ + x * Q + i]; + B.m[y * W + x] = n / (Q * Q); + } + B.row = r1; +} + +function job(S, wi) { + if (!S.jobs[wi]) S.jobs[wi] = { shapes: layout(WORDS[wi]), hi: new Uint8Array(W * Q * H * Q), m: new Float32Array(N), row: 0 }; + return S.jobs[wi]; +} + +// The mask now: finished at once if the background builder has not got there. +function mask(S, wi) { + const B = job(S, wi); + if (B.row < H) buildRows(B, H); + return B.m; +} + +export function setup(params) { + params.s = { jobs: [], u: 0, hue: 0, n: 2, tilt: 0.06, col: new Float32Array(H * 6), bar: new Float32Array(6) }; +} + +// The stream of frames: each word four times, light, shadow, light, shadow. +const REPEAT = 4; +const wordOf = (f) => floor(f / REPEAT) % WORDS.length; + +// A step lasts STEP seconds at Speed 2, and the barrier glides for the last +// GLIDE of it. The bars are pierced on a checkerboard: a hole catches G1 of +// the light in the slits beside it and lets G2 of the print under the bar +// show through. OVER brightens a half-open column, so in mid-glide both +// frames shine at 0.8. +const STEP = 1.6, GLIDE = 0.4; +const G1 = 0.35, G2 = 0.2, OVER = 1.6; + +export function update(dt, input, params) { + const S = params.s; + const k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt, 0, 0.1); + S.hue = k[0]; + S.n = cl(round(k[2]), 2, 4); + S.tilt = cl(k[3], 0, 1); + S.u += (dt * cl(k[1], 0.05, 3)) / 2 / STEP; + // Build ahead: the first unfinished word among those the print holds now + // and the next one it will need. + for (let f = floor(S.u); f <= floor(S.u) + S.n + REPEAT; f++) { + const B = job(S, wordOf(f)); + if (B.row < H) { + buildRows(B, CHUNK); + break; + } + } +} + +export function draw(display, params) { + const S = params.s; + const n = S.n; + const K = floor(S.u); + const e = S.u - K; + const g = e < 1 - GLIDE ? 0 : (e - 1 + GLIDE) / GLIDE; + // The barrier's place in columns: on step K the slits stand over the + // columns of slot K mod n, and in the glide they ease one column right. + const b = K + g * g * (3 - 2 * g); + // Slot s of the print holds the frame f of K..K+n-1 with f = s mod n. A + // slot the barrier has just left is printed again with the frame n steps + // on, so the print always carries the frame on show and the next n-1. + const fw = [], fn = []; + for (let s = 0; s < n; s++) { + const f = K + ((((s - K) % n) + n) % n); + fw.push(mask(S, wordOf(f))); + fn.push(f % 2); + } + // Light for the word (gold at Hue 0) and for its negative (the opposite + // blue), a little warmer down the panel. + const col = S.col; + for (let y = 0; y < H; y++) { + hsv(0.11 + S.hue - 0.05 * (y / H), 0.62, 1, col, y * 6); + hsv(0.61 + S.hue - 0.05 * (y / H), 0.62, 1, col, y * 6 + 3); + } + // The bars' own faint indigo, and the pale light of the print under them. + const bar = S.bar; + hsv(0.7, 0.7, 0.1, bar, 0); + hsv(0.7, 0.3, 1, bar, 3); + const shift = S.tilt * S.tilt * 3 * n; + const o = [0, 0, 0, 0], V = [0, 0, 0, 0]; + const g1 = (G1 * 2) / n, g2 = (G2 * 2) / n; + for (let y = 0; y < H; y++) { + // How open each slot's columns are in this row: a slit one column wide, + // box-filtered over the LED, so the openings of the n slots add to 1. + const by = b + shift * (y / (H - 1) - 0.5); + for (let k = 0; k < n; k++) { + let d = (k - by) % n; + if (d < 0) d += n; + o[k] = max(0, 1 - min(d, n - d)); + } + const c0 = y * 6; + for (let x = 0; x < W; x++) { + const s = x % n, p = y * W + x; + // Every slot's print at this LED, and the light the slits pass, which + // is what a hole in a bar here would catch. + let vr = 0, vg = 0, vb = 0; + for (let k = 0; k < n; k++) { + const v = fn[k] ? 1 - fw[k][p] : fw[k][p]; + V[k] = v; + const c = c0 + fn[k] * 3, w = o[k] * v; + vr += col[c] * w; + vg += col[c + 1] * w; + vb += col[c + 2] * w; + } + const os = o[s], lit = V[s] * min(1, OVER * os), c = c0 + fn[s] * 3, shut = 1 - os; + let r = col[c] * lit + bar[0] * shut, gg = col[c + 1] * lit + bar[1] * shut, bb = col[c + 2] * lit + bar[2] * shut; + if ((x + y) & 1) { + const h1 = shut * g1, h2 = shut * g2 * V[s]; + r += h1 * vr + h2 * bar[3]; + gg += h1 * vg + h2 * bar[4]; + bb += h1 * vb + h2 * bar[5]; + } + display.setPixel(x, y, min(255, r * 255) | 0, min(255, gg * 255) | 0, min(255, bb * 255) | 0); + } + } +} From 592c3fb5ee091261acd6c902e6a072260e3ae3b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:54:52 +0000 Subject: [PATCH 18/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Subpixel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- incoming/pattern-study/patterns/subpixel.js | 484 ++++++++++++++++++++ 1 file changed, 484 insertions(+) create mode 100644 incoming/pattern-study/patterns/subpixel.js diff --git a/incoming/pattern-study/patterns/subpixel.js b/incoming/pattern-study/patterns/subpixel.js new file mode 100644 index 000000000..c36ad1ddd --- /dev/null +++ b/incoming/pattern-study/patterns/subpixel.js @@ -0,0 +1,484 @@ +// ===== Patternflow pattern ===== +// Title: Subpixel +// Family: type +// Idea: Two letters stored as TrueType outlines drift by fractions of a pixel over a magnified pixel grid that is scan-converted again every frame, as red, green and blue subpixel stripes, as grey coverage or as one-bit pixels, so the raster jumps and its colour fringes shimmer while the thin outline glides on top. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: TrueType quadratic outlines and centre-sampling scan conversion (Apple, 1991), ClearType subpixel rendering (Microsoft, announced 1998), FreeType's default LCD filter and light hinting, pattern round 66 +// Round: 66 +// =============================== +// @matrix 64x128 +// @knobs Letters=0..2, Speed=0.2..3, Mode=0..3, Hint=0..2 +// +// Knob 1: Letters. Which pair: a over g at 13.2 pixels to the em or R over & at 12.2, on pixels six LEDs square, or 한 over Ꞁ at 19.5 on pixels three LEDs square. +// Knob 2: Speed. How fast the outlines drift and breathe: at 2 they travel half a pixel a second and swell and shrink by 5.5 percent over 15 seconds, at 0.2 ten times slower. +// Knob 3: Mode. How the grid is filled: 0 subpixel stripes, each pixel three LED columns red, green and blue, each lit by the coverage of its own third after FreeType's five-tap filter; 1 the same three values as the colour of the whole pixel, where the fringes show; 2 grey coverage; 3 one bit, a pixel lit when its centre is inside, TrueType's first rule. +// Knob 4: Hint. Snapping to the grid: 0 none; 1 heights and horizontal strokes only, as FreeType's light hinting does; 2 the vertical stems as well, each stem a whole number of pixels wide. +// Buttons: K2 tap holds the outlines still, tap again to let them drift. + +const W = 64, H = 128; +const { floor, ceil, round, min, max, abs, sin, cos, sqrt, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// ---- Outlines. Units per em 1000, y up, baseline 0. A contour is a list of +// points [x, y, on]: on 1 lies on the curve, on 0 is a quadratic control +// point off it, and two off points in a row have an implied on point half +// way between them, which is how TrueType stores a glyph. As in TrueType, +// outer contours run clockwise and holes counter-clockwise, contours may +// overlap, and the fill rule is nonzero winding. Each contour carries its +// own stems for the hinter. + +// "x,y" on the curve, "~x,y" off it. +const pts = (s) => s.trim().split(/\s+/).map((t) => { + const off = t[0] === "~"; + const [x, y] = t.slice(off ? 1 : 0).split(",").map(Number); + return [x, y, off ? 0 : 1]; +}); + +function rect(x0, y0, x1, y1) { + return { p: [[x0, y0, 1], [x0, y1, 1], [x1, y1, 1], [x1, y0, 1]], hx: [[x0, x1]], hy: [[y0, y1]] }; +} + +// An oval as eight off points and no on points at all: the implied on +// points between them fall on the ellipse at every 45 degrees, the extremes +// among them. A ring is the oval and a smaller one turned the other way. +function oval(cx, cy, rx, ry, hole) { + const k = 1 / cos(PI / 8), p = []; + for (let i = 0; i < 8; i++) { + const a = PI / 8 - (i * PI) / 4; + p.push([cx + rx * k * cos(a), cy + ry * k * sin(a), 0]); + } + return hole ? p.reverse() : p; +} +function ring(cx, cy, rx, ry, tx, ty) { + const hx = [[cx - rx, cx - rx + tx], [cx + rx - tx, cx + rx]], hy = [[cy - ry, cy - ry + ty], [cy + ry - ty, cy + ry]]; + return [{ p: oval(cx, cy, rx, ry, false), hx, hy }, { p: oval(cx, cy, rx - tx, ry - ty, true), hx, hy }]; +} + +// A stroke: a centre line of on points with at most one control point +// between them, "x,y,w" giving the width from that point on. The outline is +// the line offset to the left going forward and to the right coming back, +// each control point moved to where the offset tangents meet, so the +// result is again quadratic. The ends are cut square. +function stroke(s, hx, hy) { + const c = s.trim().split(/\s+/).map((t) => { + const off = t[0] === "~"; + const v = t.slice(off ? 1 : 0).split(",").map(Number); + return { x: v[0], y: v[1], on: !off, w: v[2] }; + }); + let w = 0; + for (const q of c) if (q.on) (w = q.w || w), (q.w = w); + const n = c.length; + const dir = (a, b) => { + const dx = b.x - a.x, dy = b.y - a.y, l = Math.hypot(dx, dy) || 1; + return [dx / l, dy / l]; + }; + // Tangent at each point. + const t = c.map((q, i) => { + const a = i > 0 ? dir(c[i - 1], q) : null, b = i < n - 1 ? dir(q, c[i + 1]) : null; + if (!a) return b; + if (!b || !q.on) return a; + const x = a[0] + b[0], y = a[1] + b[1], l = Math.hypot(x, y) || 1; + return [x / l, y / l]; + }); + const side = (sg) => { + const out = []; + for (let i = 0; i < n; i++) { + const q = c[i]; + if (q.on) { + const d = (sg * q.w) / 2; + out.push([q.x + d * t[i][1], q.y - d * t[i][0], 1]); + continue; + } + // Meet the offset tangent lines of the two on points either side. + const A = c[i - 1], B = c[i + 1], ta = dir(A, q), tb = dir(q, B); + const da = (sg * A.w) / 2, db = (sg * B.w) / 2; + const ax = A.x + da * ta[1], ay = A.y - da * ta[0], bx = B.x + db * tb[1], by = B.y - db * tb[0]; + const den = ta[0] * tb[1] - ta[1] * tb[0]; + if (abs(den) < 1e-6) out.push([(ax + bx) / 2, (ay + by) / 2, 0]); + else { + const u = ((bx - ax) * tb[1] - (by - ay) * tb[0]) / den; + out.push([ax + u * ta[0], ay + u * ta[1], 0]); + } + } + return out; + }; + return { p: [...side(-1), ...side(1).reverse()], hx: hx || [], hy: hy || [] }; +} + +const poly = (s, hx, hy) => ({ p: pts(s), hx: hx || [], hy: hy || [] }); + +const GLYPHS = { + // Double-storey a: stem, arch with a cut terminal, and the bowl hung + // from a shelf. + a: () => [ + rect(416, 0, 512, 372), + stroke("464,360,96 ~464,490 290,490,84 ~176,490 112,424,80", [[416, 512]], [[448, 532]]), + stroke("470,259,82 300,259 ~95,259 95,145,94 ~95,31 235,31,86 ~385,31 462,124,90", [[48, 142], [416, 512]], [[-12, 74], [218, 300]]), + ], + // Double-storey g: a round upper bowl with an ear, a link and a lower loop. + g: () => [ + ...ring(250, 352, 172, 180, 90, 80), + rect(360, 468, 548, 532), + stroke("178,204,76 ~112,132 180,98,72 ~226,64 330,64,74 ~500,64 500,-62,88 ~500,-194 275,-194,72 ~50,-194 50,-72,88 ~50,34 190,74,70", [[6, 94], [456, 544]], [[27, 101], [-230, -158]]), + ], + R: () => [ + rect(80, 0, 182, 700), + stroke("130,655,90 330,655 ~484,655 484,491,98 ~484,327 330,327,90 130,327", [[80, 182], [435, 533]], [[610, 700], [282, 372]]), + poly("440,0 288,330 404,330 558,0", [], [[0, 330]]), + ], + // Ampersand as one stroke: up the diagonal from the right foot, round the + // small upper loop, down across itself into the bowl and up the arm. + "&": () => [ + stroke("588,26,100 ~468,160 332,308,94 ~202,452 205,548,88 ~208,662 305,662,84 ~402,662 402,562,86 ~402,470 292,400,90 ~85,270 85,160,96 ~85,31 252,31,86 ~420,31 490,158,92 ~528,228 546,330,88", [[37, 133], [161, 249], [359, 445]], [[-12, 74], [620, 704]]), + ], + // Hangul in a 1000 square, gothic strokes 100 wide and 85 high. + "한": () => [ + rect(245, 862, 405, 942), rect(80, 728, 570, 812), ...ring(325, 553, 178, 123, 92, 80), + rect(650, 372, 750, 950), rect(750, 622, 885, 706), + rect(150, 70, 250, 330), rect(150, 70, 882, 155), + ], + "Ꞁ": () => [ + rect(130, 845, 835, 930), rect(742, 700, 835, 930), + rect(45, 560, 955, 645), + rect(150, 385, 850, 466), rect(762, 228, 852, 466), rect(150, 228, 852, 308), + rect(150, 70, 240, 308), rect(150, 70, 870, 150), + ], +}; + +// Alignment zones for the Latin letters: the flat height and the +// overshoot of round letters past it. Hinted, both land on one pixel line. +const ZONES = [[0, -12], [520, 532], [700, 712], [-220, -232]]; + +// A set: the glyphs, how many LEDs one pixel takes, and the size in pixels +// per em. Hangul packs three jamo into one square, so it gets twice the pixels. +const SETS = [ + { g: ["a", "g"], zoom: 6, ppem: 13.2, gap: 1.3, zones: true }, + { g: ["R", "&"], zoom: 6, ppem: 12.2, gap: 1.3, zones: true }, + { g: ["한", "Ꞁ"], zoom: 3, ppem: 19.5, gap: 3.2, zones: false }, +]; + +// Flatten a contour list to line segments at unit scale, for the bounds. +function bounds(contours) { + let x0 = 1e9, y0 = 1e9, x1 = -1e9, y1 = -1e9; + const seg = new Float32Array(4096); + const n = flatten(contours.map((c) => c.p.map((q) => [q[0], -q[1], q[2]])), seg, 0, 4); + for (let i = 0; i < n * 4; i += 2) { + x0 = min(x0, seg[i]), x1 = max(x1, seg[i]); + y0 = min(y0, -seg[i + 1]), y1 = max(y1, -seg[i + 1]); + } + return { x0, y0, x1, y1 }; +} + +// Decode TrueType contours (already in pixels, y down) to quadratic pieces +// and cut each into straight segments fine enough at `fine` LEDs a pixel. +// Writes x0 y0 x1 y1 per segment from index n; returns the new count. +function flatten(cs, seg, n, fine) { + const cap = seg.length >> 2; + const line = (ax, ay, bx, by) => { + if (n < cap) (seg[n * 4] = ax), (seg[n * 4 + 1] = ay), (seg[n * 4 + 2] = bx), (seg[n * 4 + 3] = by), n++; + }; + const quad = (ax, ay, qx, qy, bx, by) => { + const dev = (Math.hypot(ax - 2 * qx + bx, ay - 2 * qy + by) / 4) * fine; + const k = cl(ceil(sqrt(dev / 0.08)), 1, 24); + let px = ax, py = ay; + for (let i = 1; i <= k; i++) { + const u = i / k, v = 1 - u; + const x = v * v * ax + 2 * u * v * qx + u * u * bx, y = v * v * ay + 2 * u * v * qy + u * u * by; + line(px, py, x, y); + (px = x), (py = y); + } + }; + for (const p of cs) { + const m = p.length; + let s = p.findIndex((q) => q[2]); + let sx, sy; + if (s < 0) (sx = (p[m - 1][0] + p[0][0]) / 2), (sy = (p[m - 1][1] + p[0][1]) / 2), (s = 0); + else (sx = p[s][0]), (sy = p[s][1]), (s = s + 1); + let cx = sx, cy = sy, ctl = null; + for (let k = 0; k < m; k++) { + const q = p[(s + k) % m]; + if (q[2]) { + if (ctl) quad(cx, cy, ctl[0], ctl[1], q[0], q[1]); + else line(cx, cy, q[0], q[1]); + (cx = q[0]), (cy = q[1]), (ctl = null); + } else if (ctl) { + const mx = (ctl[0] + q[0]) / 2, my = (ctl[1] + q[1]) / 2; + quad(cx, cy, ctl[0], ctl[1], mx, my); + (cx = mx), (cy = my), (ctl = q); + } else ctl = q; + } + if (ctl) quad(cx, cy, ctl[0], ctl[1], sx, sy); + else line(cx, cy, sx, sy); + } + return n; +} + +// ---- Hinting. Each stem's two edges go to whole pixel lines with the +// stem's width rounded, never under one pixel; an edge in an alignment zone +// goes to the zone's line, which takes the overshoot with it at these sizes. +// Every other point is moved in proportion between the edges either side of +// it, the way TrueType's IUP instruction moves the points no instruction +// touched, here per contour. `zones` are the zones the contour reaches. +const inZone = (z, u) => u >= min(z[0], z[1]) - 2 && u <= max(z[0], z[1]) + 2; +function edgeMap(stems, zones, f) { + const out = []; + const zoneOf = (u) => zones.find((z) => inZone(z, u)) || null; + for (const [a, b] of stems) { + const pa = f(a), pb = f(b), w = max(1, round(abs(pb - pa))), sg = pb > pa ? 1 : -1; + const za = zoneOf(a), zb = zoneOf(b); + let ha; + if (za) ha = round(f(za[0])); + else if (zb) ha = round(f(zb[0])) - sg * w; + else ha = round((pa + pb) / 2 - (sg * w) / 2); + out.push([a, ha], [b, ha + sg * w]); + } + for (const z of zones) out.push([z[0], round(f(z[0]))], [z[1], round(f(z[0]))]); + out.sort((p, q) => p[0] - q[0]); + // Keep the order: a later edge never crosses an earlier one. + const dec = f(1) < f(0); + for (let i = 1; i < out.length; i++) { + if (dec ? out[i][1] > out[i - 1][1] : out[i][1] < out[i - 1][1]) out[i][1] = out[i - 1][1]; + } + return out; +} +function mapEdge(E, f, u) { + const n = E.length; + if (!n) return f(u); + if (u <= E[0][0]) return E[0][1] + f(u) - f(E[0][0]); + if (u >= E[n - 1][0]) return E[n - 1][1] + f(u) - f(E[n - 1][0]); + let i = 1; + while (E[i][0] < u) i++; + const [ua, ha] = E[i - 1], [ub, hb] = E[i]; + return ub > ua ? ha + ((u - ua) / (ub - ua)) * (hb - ha) : ha; +} + +// The FreeType default LCD filter, in 256ths across five subpixels. +const FIR = [8, 77, 86, 77, 8].map((v) => v / 256); + +export function setup(params) { + const sets = SETS.map((s) => { + const glyphs = s.g.map((ch) => { + const contours = GLYPHS[ch]().flat(); + // A contour reaches a zone when one of its points or stem edges is in it. + const reach = (c, z) => c.p.some((q) => inZone(z, q[1])) || c.hy.some((e) => inZone(z, e[0]) || inZone(z, e[1])); + for (const c of contours) c.zones = s.zones ? ZONES.filter((z) => reach(c, z)) : []; + return { contours, box: bounds(contours) }; + }); + // Stack the two glyphs with a gap and centre the stack on the grid. + const GW = floor(W / s.zoom), GH = floor(H / s.zoom), sc = s.ppem / 1000; + const hs = glyphs.map((g) => (g.box.y1 - g.box.y0) * sc); + let y = (GH - hs[0] - hs[1] - s.gap) / 2; + glyphs.forEach((g, i) => { + g.cx = GW / 2; + g.cy = y + hs[i] / 2; + g.ux = (g.box.x0 + g.box.x1) / 2; + g.uy = (g.box.y0 + g.box.y1) / 2; + y += hs[i] + s.gap; + }); + return { ...s, glyphs, GW, GH, X0: (W - GW * s.zoom) >> 1, Y0: (H - GH * s.zoom) >> 1 }; + }); + params.s = { + sets, set: 0, mode: 0, hint: 0, T: 0, hold: false, + seg: new Float32Array(4 * 6000), outl: new Float32Array(4 * 6000), nSeg: 0, nOut: 0, + cx: new Float32Array(256 * MAXC), cd: new Int8Array(256 * MAXC), cnt: new Int32Array(256), + bins: new Float32Array(43 * 64), bit: new Uint8Array(43 * 22), fir: new Float32Array(43 * 64), + mask: new Uint8Array(W * H), line: new Float32Array(W * H), offs: new Float32Array(1024), nOff: 0, + }; +} + +// Where glyph i of the set sits now: drift in pixels and a breathing scale. +function pose(S, set, i) { + const T = S.T, ph = i * 2.1; + const g = set.glyphs[i]; + const dx = 0.55 * sin(0.37 * T + ph) + 0.3 * sin(0.83 * T + 1.3 * ph); + const dy = 0.45 * sin(0.29 * T + 0.7 + ph) + 0.25 * sin(0.71 * T + 2 * ph); + const sc = (set.ppem / 1000) * (1 + 0.055 * sin(0.21 * T + 1.7 * ph)); + return { ox: g.cx + dx, oy: g.cy + dy, sc, g }; +} + +// Place one glyph's contours in grid pixels, y down, hinted on the axes +// asked. A hinted axis first puts the glyph's origin on a whole pixel, the +// baseline on a pixel row as text lines are set, so the hinted glyph keeps +// one shape wherever the outline drifts and moves only in whole pixels. +function place(S, set, i, hint) { + const { ox, oy, sc, g } = pose(S, set, i); + const fx = (u) => ox + (u - g.ux) * sc, fy = (v) => oy - (v - g.uy) * sc; + return g.contours.map((c) => { + if (!hint) return c.p.map((q) => [fx(q[0]), fy(q[1]), q[2]]); + const y0 = fy(0), sy = (v) => fy(v) + round(y0) - y0; + const Ey = edgeMap(c.hy, c.zones, sy); + if (hint < 2) return c.p.map((q) => [fx(q[0]), mapEdge(Ey, sy, q[1]), q[2]]); + const x0 = fx(0), sx = (u) => fx(u) + round(x0) - x0; + const Ex = edgeMap(c.hx, [], sx); + return c.p.map((q) => [mapEdge(Ex, sx, q[0]), mapEdge(Ey, sy, q[1]), q[2]]); + }); +} + +// ---- Scan conversion. Horizontal sample lines k = 0..L-1 sit at +// y = (k + 0.5 - b) / a in the segments' units. On each, the crossings are +// sorted and walked under nonzero winding, and span(k, x0, x1) gets every +// run that is inside, exact in x. +const MAXC = 48; +function scan(S, seg, nseg, L, a, b, span) { + const { cx, cd, cnt } = S; + cnt.fill(0, 0, L); + for (let s = 0; s < nseg; s++) { + const x0 = seg[s * 4], y0 = seg[s * 4 + 1], x1 = seg[s * 4 + 2], y1 = seg[s * 4 + 3]; + if (y0 === y1) continue; + const d = y1 > y0 ? 1 : -1, ya = min(y0, y1), yb = max(y0, y1); + const k0 = max(0, ceil(a * ya + b - 0.5)), k1 = min(L - 1, ceil(a * yb + b - 0.5) - 1); + for (let k = k0; k <= k1; k++) { + if (cnt[k] >= MAXC) continue; + const y = (k + 0.5 - b) / a, j = k * MAXC + cnt[k]++; + cx[j] = x0 + ((y - y0) * (x1 - x0)) / (y1 - y0); + cd[j] = d; + } + } + for (let k = 0; k < L; k++) { + const n = cnt[k], o = k * MAXC; + for (let i = 1; i < n; i++) { + const x = cx[o + i], dd = cd[o + i]; + let j = i - 1; + while (j >= 0 && cx[o + j] > x) (cx[o + j + 1] = cx[o + j]), (cd[o + j + 1] = cd[o + j]), j--; + (cx[o + j + 1] = x), (cd[o + j + 1] = dd); + } + let w = 0, start = 0; + for (let i = 0; i < n; i++) { + const was = w; + w += cd[o + i]; + if (!was && w) start = cx[o + i]; + else if (was && !w) span(k, start, cx[o + i]); + } + } +} + +// Seven sample lines a pixel row (five on the finer Hangul grid). Coverage +// is gathered per third of a pixel, so one pass gives the grey value of a +// pixel and of its three subpixels. The middle line runs through the pixel +// centres and gives the one-bit raster: TrueType's first rule, a pixel is +// on when its centre is inside the outline. +function rasterise(S, set) { + const { GW, GH } = set; + const SS = set.zoom > 3 ? 7 : 5, mid = (SS - 1) >> 1, NB = GW * 3; + const { bins, bit } = S; + bins.fill(0, 0, GH * NB); + bit.fill(0, 0, GH * GW); + scan(S, S.seg, S.nSeg, GH * SS, SS, 0, (k, xa, xb) => { + const row = (k / SS) | 0, u0 = cl(xa * 3, 0, NB), u1 = cl(xb * 3, 0, NB); + for (let q = floor(u0); q < u1 && q < NB; q++) bins[row * NB + q] += min(u1, q + 1) - max(u0, q); + if (k % SS === mid) for (let p = max(0, ceil(xa - 0.5)); p < min(GW, ceil(xb - 0.5)); p++) bit[row * GW + p] = 1; + }); + for (let i = 0; i < GH * NB; i++) bins[i] /= SS; +} + +export function update(dt, input, params) { + const S = params.s; + const k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt, 0, 0.1); + S.set = cl(round(k[0]), 0, SETS.length - 1); + S.mode = cl(round(k[2]), 0, 3); + S.hint = cl(round(k[3]), 0, 2); + if (input.btnPressed && input.btnPressed[1]) S.hold = !S.hold; + if (!S.hold) S.T += dt * cl(k[1], 0.05, 3); + const set = S.sets[S.set]; + // The true outline, for drawing, and the one the raster is made from. + let n = 0; + S.nOff = 0; + for (let i = 0; i < 2; i++) { + const cs = place(S, set, i, 0); + for (const c of cs) for (const q of c) if (!q[2] && S.nOff < 512) (S.offs[S.nOff * 2] = q[0]), (S.offs[S.nOff * 2 + 1] = q[1]), S.nOff++; + n = flatten(cs, S.outl, n, set.zoom); + } + S.nOut = n; + if (S.hint) { + n = 0; + for (let i = 0; i < 2; i++) n = flatten(place(S, set, i, S.hint), S.seg, n, set.zoom); + } else S.seg.set(S.outl.subarray(0, n * 4)); + S.nSeg = n; + rasterise(S, set); +} + +// An outline as a thin line on the LEDs, added into acc. Overlapping +// contours leave edges inside the letter, so a stretch of line is drawn only +// where one side of it is outside: the outline is first filled at one sample +// per LED. +function trace(S, set, sg, nseg, acc) { + const { mask } = S, z = set.zoom, X0 = set.X0, Y0 = set.Y0; + mask.fill(0); + scan(S, sg, nseg, H, z, Y0, (k, xa, xb) => { + for (let x = max(0, ceil(X0 + xa * z - 0.5)); x < min(W, ceil(X0 + xb * z - 0.5)); x++) mask[k * W + x] = 1; + }); + const inside = (x, y) => { + const i = round(x), j = round(y); + return i >= 0 && j >= 0 && i < W && j < H && mask[j * W + i]; + }; + acc.fill(0); + for (let s = 0; s < nseg; s++) { + const ax = X0 + sg[s * 4] * z - 0.5, ay = Y0 + sg[s * 4 + 1] * z - 0.5; + const bx = X0 + sg[s * 4 + 2] * z - 0.5, by = Y0 + sg[s * 4 + 3] * z - 0.5; + const len = Math.hypot(bx - ax, by - ay); + if (len < 1e-6) continue; + const nx = (by - ay) / len, ny = (ax - bx) / len, k = max(1, ceil(len / 0.3)), wt = len / k; + for (let i = 0; i < k; i++) { + const u = (i + 0.5) / k, x = ax + u * (bx - ax), y = ay + u * (by - ay); + if (inside(x + nx * 1.2, y + ny * 1.2) && inside(x - nx * 1.2, y - ny * 1.2)) continue; + const xi = floor(x), yi = floor(y), fx = x - xi, fy = y - yi; + for (let q = 0; q < 4; q++) { + const px = xi + (q & 1), py = yi + (q >> 1); + if (px < 0 || py < 0 || px >= W || py >= H) continue; + acc[py * W + px] += wt * (q & 1 ? fx : 1 - fx) * (q >> 1 ? fy : 1 - fy); + } + } + } +} + +// The outline's pen: white over the stripes, rose over white pixels. +const PEN = [[255, 255, 255], [255, 64, 176], [255, 64, 176], [255, 64, 176]]; + +export function draw(display, params) { + const S = params.s; + const set = S.sets[S.set]; + const { GW, GH, zoom: z, X0, Y0 } = set, NB = GW * 3; + const { bins, bit, fir, line, mode } = S; + // The subpixel values, filtered along each row across pixel borders. + if (mode < 2) + for (let j = 0; j < GH; j++) + for (let q = 0; q < NB; q++) { + let v = 0; + for (let t = -2; t <= 2; t++) { + const r = q + t; + if (r >= 0 && r < NB) v += FIR[t + 2] * bins[j * NB + r]; + } + fir[j * NB + q] = v; + } + trace(S, set, S.outl, S.nOut, line); + const pen = z > 3 ? 1.25 : 0.7, P = PEN[mode]; + // The control points off the curve, one LED each. + for (let i = 0; i < S.nOff; i++) { + const x = round(X0 + S.offs[i * 2] * z - 0.5), y = round(Y0 + S.offs[i * 2 + 1] * z - 0.5); + if (x >= 0 && y >= 0 && x < W && y < H) line[y * W + x] = max(line[y * W + x], 0.6 / pen); + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let r = 0, g = 0, b = 0; + const gx = x - X0, gy = y - Y0; + if (gx >= 0 && gy >= 0 && gx < GW * z && gy < GH * z) { + const i = (gx / z) | 0, j = (gy / z) | 0, o = j * NB + i * 3; + if (mode === 0) { + // The LCD under a loupe: each third of the pixel a stripe of its own primary. + const sub = (((gx - i * z) * 3) / z) | 0, v = fir[o + sub] * 255; + if (sub === 0) r = v; + else if (sub === 1) g = v; + else b = v; + } else if (mode === 1) (r = fir[o] * 255), (g = fir[o + 1] * 255), (b = fir[o + 2] * 255); + else r = g = b = (mode === 2 ? (bins[o] + bins[o + 1] + bins[o + 2]) / 3 : bit[j * GW + i]) * 255; + // The grid: a faint dot at each pixel corner where nothing is lit. + if (gx % z === 0 && gy % z === 0 && r + g + b < 40) r = g = b = 30; + } + const a = min(1, line[y * W + x] * pen); + if (a > 0) (r += (P[0] - r) * a), (g += (P[1] - g) * a), (b += (P[2] - b) * a); + display.setPixel(x, y, r, g, b); + } +} From 36e2fd6ac9a4c957d8700d5158c9b22219af38bb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:55:05 +0000 Subject: [PATCH 19/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Barrier=20Grid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- incoming/pattern-study/patterns/barrier-grid.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/patterns/barrier-grid.js b/incoming/pattern-study/patterns/barrier-grid.js index e8680339a..1d8f1acf9 100644 --- a/incoming/pattern-study/patterns/barrier-grid.js +++ b/incoming/pattern-study/patterns/barrier-grid.js @@ -30,7 +30,7 @@ function hsv(h, s, v, out, o) { } // Words of light and of looking. A Hangul word is one syllable filling the -// panel; a Latin word is four letters, two by two, 29 by 58 LEDs each. +// panel; a Latin word is four letters, two by two, 28 by 58 LEDs each. const WORDS = ["빛", "LOOK", "달", "NEON", "별", "HALO", "ë°€", "SEEN"]; // Shapes: [kind, add, ...]. kind 0 a rounded box x0 y0 x1 y1 tl tr br bl, @@ -175,7 +175,7 @@ function inside(p, x, y) { function layout(word) { const chars = [...word]; const big = chars.length === 1; - const cells = big ? [[0, 0, 64, 128]] : [[2, 4, 29, 58], [33, 4, 29, 58], [2, 66, 29, 58], [33, 66, 29, 58]]; + const cells = big ? [[0, 0, 64, 128]] : [[2, 4, 28, 58], [34, 4, 28, 58], [2, 66, 28, 58], [34, 66, 28, 58]]; const out = []; chars.forEach((ch, gi) => { const [cx, cy, w, h] = cells[gi]; From 69e9c5f7dfa409ba0bfa1eb7a9eae97cbde284bd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:56:40 +0000 Subject: [PATCH 20/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Subpixel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- incoming/pattern-study/patterns/subpixel.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/incoming/pattern-study/patterns/subpixel.js b/incoming/pattern-study/patterns/subpixel.js index c36ad1ddd..97aef7ec4 100644 --- a/incoming/pattern-study/patterns/subpixel.js +++ b/incoming/pattern-study/patterns/subpixel.js @@ -5,7 +5,7 @@ // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: TrueType quadratic outlines and centre-sampling scan conversion (Apple, 1991), ClearType subpixel rendering (Microsoft, announced 1998), FreeType's default LCD filter and light hinting, pattern round 66 +// Lineage: TrueType quadratic outlines and its pixel-centre rule for scan conversion (Apple, 1991), ClearType subpixel rendering (Microsoft, announced 1998), FreeType's default LCD filter and light hinting, pattern round 66 // Round: 66 // =============================== // @matrix 64x128 @@ -14,7 +14,7 @@ // Knob 1: Letters. Which pair: a over g at 13.2 pixels to the em or R over & at 12.2, on pixels six LEDs square, or 한 over Ꞁ at 19.5 on pixels three LEDs square. // Knob 2: Speed. How fast the outlines drift and breathe: at 2 they travel half a pixel a second and swell and shrink by 5.5 percent over 15 seconds, at 0.2 ten times slower. // Knob 3: Mode. How the grid is filled: 0 subpixel stripes, each pixel three LED columns red, green and blue, each lit by the coverage of its own third after FreeType's five-tap filter; 1 the same three values as the colour of the whole pixel, where the fringes show; 2 grey coverage; 3 one bit, a pixel lit when its centre is inside, TrueType's first rule. -// Knob 4: Hint. Snapping to the grid: 0 none; 1 heights and horizontal strokes only, as FreeType's light hinting does; 2 the vertical stems as well, each stem a whole number of pixels wide. +// Knob 4: Hint. Snapping to the grid: 0 none; 1 the baseline, heights and horizontal strokes on pixel rows, as FreeType's light hinting does; 2 the vertical stems as well, each a whole number of pixels wide, so the hinted letter keeps its shape and moves only in whole-pixel jumps while the outline glides. // Buttons: K2 tap holds the outlines still, tap again to let them drift. const W = 64, H = 128; @@ -383,7 +383,8 @@ export function update(dt, input, params) { if (input.btnPressed && input.btnPressed[1]) S.hold = !S.hold; if (!S.hold) S.T += dt * cl(k[1], 0.05, 3); const set = S.sets[S.set]; - // The true outline, for drawing, and the one the raster is made from. + // The true outline and its off-curve points, for drawing, and the outline + // the raster is made from, which is the hinted one when hinting is on. let n = 0; S.nOff = 0; for (let i = 0; i < 2; i++) { From a3df4428d6d135fc0317ce5aeadf55d2c31cdeed Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:56:59 +0000 Subject: [PATCH 21/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Zermelo=20Navigation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../patterns/zermelo-navigation.js | 52 ++++++++++++------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/incoming/pattern-study/patterns/zermelo-navigation.js b/incoming/pattern-study/patterns/zermelo-navigation.js index 6daa0ad65..121857380 100644 --- a/incoming/pattern-study/patterns/zermelo-navigation.js +++ b/incoming/pattern-study/patterns/zermelo-navigation.js @@ -12,9 +12,9 @@ // @knobs Hue=0..1, Current=0..2.5, Shape=0..1, Harbour=-1..1 // // Knob 1: Hue. Turns the colours round the wheel together: at 0 a front leaves the harbour gold, turns green halfway and reaches the far coast blue, and the shadows are rose. -// Knob 2: Current. The fastest water in boat speeds. At 0 the fronts are circles and the routes straight; up to 1 every place can be reached; past 1 shadows open, at Shape 0 on 2.4% of the panel at 1.5, 8.9% at 2 (the opening) and 18.5% at 2.5. -// Knob 3: Shape. Three eddies centred near the panel's sides at 0, a jet leaving the coast at the bottom centre and wandering as it climbs at 1, both at full strength at 0.5. With the harbour inside a jet faster than the boat, the fronts are teardrops carried up and only a cone above the harbour is reached first. -// Knob 4: Harbour. Where the harbour stands along the coast, from column 8 at -1 to column 56 at 1; the shadows stay where they are, since they belong to the current and the panel's edge, not to the harbour. +// Knob 2: Current. The fastest water, in boat speeds. At 0 the fronts are circles and the routes straight lines; up to 1 every LED is reached, at any shape, harbour and moment; past 1 shadows open, at Shape 0 on 2.5% of the panel at 1.5, 9.2% at 2 (the opening) and 19.7% at 2.5. +// Knob 3: Shape. Three eddies at 0, two on the left side and one on the right, and at 1 a jet leaving the coast at the bottom centre that wanders more as it climbs; between, both at part strength. With the harbour inside a jet faster than the boat the fronts become teardrops carried up the jet, and at first only a cone above the harbour is reached: at Shape 1 and Current 2 the water there runs at 1.99 boat speeds and the cone's half-angle is asin(1 / 1.99) = 30 degrees. +// Knob 4: Harbour. Where the harbour stands on the coast, from column 8 at -1 to column 56 at 1. At Shape 0 the shadows do not move with it, the same 756 LEDs at Current 2 wherever it stands, since they belong to the current and the panel's edges. const W = 64, H = 128, N = W * H; const { floor, min, max, abs, sqrt, exp, sin, cos, round, PI, cosh } = Math; @@ -65,21 +65,21 @@ function slow(wx, wy, ux, uy) { function current(F, x, y, out) { let wx = 0, wy = 0; if (F.ge) { - // Gaussian eddies, psi = G exp(-r^2 / 2R^2), fastest at r = R. The coast - // calms them: psi is multiplied by c(y), 1 down to row 92 and 0 at the - // bottom edge, so the harbour never sits in a strong eddy. - const R2 = 169, z = (y - 92) / 36; - const c = z <= 0 ? 1 : z >= 1 ? 0 : 0.5 + 0.5 * cos(PI * z), dc = z <= 0 || z >= 1 ? 0 : (-0.5 * PI * sin(PI * z)) / 36; + // Gaussian eddies, psi = G exp(-r^2 / 2R^2) with R = 13 and G = S R + // sqrt(e), so the fastest water, at r = R, runs at S. Their centres sit + // 4 LEDs inside the sides, and the half of each that reaches past the + // edge brings water in through it. + const R2 = 169; for (let k = 0; k < 9; k += 3) { - const dx = x - F.E[k], dy = y - F.E[k + 1], p = F.E[k + 2] * F.ge * exp(-(dx * dx + dy * dy) / (2 * R2)), e = p / R2; - wx += -dy * e * c + p * dc; - wy += dx * e * c; + const dx = x - F.E[k], dy = y - F.E[k + 1], e = (F.E[k + 2] * F.ge * exp(-(dx * dx + dy * dy) / (2 * R2))) / R2; + wx -= dy * e; + wy += dx * e; } } if (F.gj) { // A jet leaving the coast at the bottom centre, wandering more as it - // climbs: psi = J L tanh((x - X(y)) / L), X = 32 + A(y) sin(k y - phase). - const L = 13, k = (2 * PI) / 52, A = 9 * (1 - y / H), ph = k * y - F.th; + // climbs: psi = J L tanh((x - X(y)) / L), X = 32 + A(y) sin(k y + phase). + const L = 13, k = (2 * PI) / 52, A = 9 * (1 - y / H), ph = k * y + F.th; const X = 32 + A * sin(ph), dX = (-9 / H) * sin(ph) + A * k * cos(ph), c = 1 / cosh((x - X) / L), j = F.gj * c * c; wx -= j * dX; wy -= j; @@ -146,12 +146,12 @@ function pop(S) { function begin(S, kv, at) { const F = S.F, th = (at * 2 * PI) / 40, cur = cl(kv[1] ?? 2, 0, 2.5), m = cl(kv[2] || 0, 0, 1); F.th = th; - F.ge = cur * 13 * 1.6487 * min(1, 2 - 2 * m); - F.gj = cur * min(1, 2 * m); + F.ge = cur * 13 * 1.6487 * (1 - m); + F.gj = cur * m; F.hx = 32 + 24 * cl(kv[3] ?? 0.06, -1, 1); const E = F.E; - E[0] = 4 + 3 * sin(th); E[1] = 98 + 4 * cos(th); E[2] = 1; - E[3] = 60 - 3 * sin(th + 2); E[4] = 62 + 4 * cos(th + 2); E[5] = -1; + E[0] = 4 + 3 * sin(th); E[1] = 88 + 3 * cos(th); E[2] = 1; + E[3] = 60 - 3 * sin(th + 2); E[4] = 57 + 4 * cos(th + 2); E[5] = -1; E[6] = 4 + 3 * sin(th + 4); E[7] = 26 + 4 * cos(th + 4); E[8] = 1; const w = S.w; for (let y = 0; y < H; y++) @@ -204,6 +204,17 @@ function work(S, budget) { // each target along the steps that reached it, and the colour scale is set // by the mean time to the far coast. function finish(S) { + // A lone LED left unreached among reached ones is the stencil's doing, not + // the current's (30 of 1,713 shadows over 45 settings, never wider than + // one LED): it takes the mean of its four neighbours. + const Tn = S.Tn; + for (let y = 1; y < H - 1; y++) + for (let x = 1; x < W - 1; x++) { + const i = y * W + x; + if (Tn[i] < INF || Tn[i - 1] >= INF || Tn[i + 1] >= INF || Tn[i - W] >= INF || Tn[i + W] >= INF) continue; + Tn[i] = (Tn[i - 1] + Tn[i + 1] + Tn[i - W] + Tn[i + W]) / 4; + S.pred[i] = i - 1; + } S.Ta.set(S.Tb); S.Tb.set(S.Tn); S.DWX.set(S.WX); @@ -321,10 +332,11 @@ export function draw(display, params) { const gy = max(u < INF ? t - u : 0, dn < INF ? dn - t : 0, u < INF ? u - t : 0, dn < INF ? t - dn : 0); const g = max(0.25, sqrt(gx * gx + gy * gy)); const q = (tau - t) / GAP, f = q - floor(q), dl = (min(f, 1 - f) * GAP) / g, behind = (f * GAP) / g; - // A line on the front, a short glow on the harbour side of it, and - // where fronts crowd closer than two LEDs they fade rather than merge. + // A line on the front and a short glow on the harbour side of it. + // Where the boats barely make way the fronts crowd closer than two + // LEDs; there they melt into a steady dim glow rather than shimmer. const line = dl < 1.1 ? 1 - dl / 1.1 : 0, tail = behind < 9 ? 0.16 * exp(-behind / 2.5) : 0; - const v = max(line, tail) * cl((GAP / g - 1.5) / 2, 0, 1); + const sp = cl((GAP / g - 1.5) / 2, 0, 1), v = max(line, tail) * sp + 0.12 * (1 - sp); if (v <= 0.01) continue; const c = min(64, (t * sc) | 0) * 3; rgb[o] = max(rgb[o], lut[c] * v); From ed91ebc8c38acb7f0123285737602adf33986e24 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:58:08 +0000 Subject: [PATCH 22/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Wilkinson's=20Polynomial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../patterns/wilkinson-polynomial.js | 144 +++++++++++------- 1 file changed, 85 insertions(+), 59 deletions(-) diff --git a/incoming/pattern-study/patterns/wilkinson-polynomial.js b/incoming/pattern-study/patterns/wilkinson-polynomial.js index 81e13015a..f1add0e98 100644 --- a/incoming/pattern-study/patterns/wilkinson-polynomial.js +++ b/incoming/pattern-study/patterns/wilkinson-polynomial.js @@ -1,24 +1,24 @@ // ===== Patternflow pattern ===== // Title: Wilkinson's Polynomial // Family: algo -// Idea: The twenty roots of (x - 1)(x - 2)...(x - 20) stand up the panel as beads on the real axis, and as a perturbation of the x^19 coefficient sweeps up past Wilkinson's 2^-23 and back the upper ten slide together in pairs and fly apart sideways into the complex plane, leaving arcs, while clouds of roots of randomly nudged copies show how far each root can be pushed and a faint dust maps the smallest nudge that would put a root anywhere. +// Idea: The twenty roots of (x - 1)(x - 2)...(x - 20) stand up the panel as beads, and as a change in the x^19 coefficient sweeps on a log scale up past Wilkinson's 2^-23 and back, the upper ten meet in pairs and fly apart sideways into the complex plane leaving arcs, inside a sparkling nebula of everywhere a root can be pushed by nudging every coefficient a little, whose bright core fills a fifth of the panel while the roots sit on the integers and shrinks to specks once they have split. // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: Wilkinson's polynomial (James H. Wilkinson, 1959; "The perfidious polynomial", 1984), Aberth's simultaneous root iteration (Oliver Aberth, 1973), pseudozero sets (Ronald G. Mosier, 1986; Kim-Chuan Toh and Lloyd N. Trefethen, 1994), pattern round 66 +// Lineage: Wilkinson's polynomial (James H. Wilkinson, 1959; "The perfidious polynomial", 1984), the Ehrlich-Aberth simultaneous root iteration (Louis W. Ehrlich, 1967; Oliver Aberth, 1973), pseudozero sets (Ronald G. Mosier, 1986; Kim-Chuan Toh and Lloyd N. Trefethen, 1994), pattern round 66 // Round: 66 // =============================== // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Phase=0..1, Cloud=0..1 // -// Knob 1: Hue. Turns every colour round the wheel together. -// Knob 2: Speed. How fast the perturbation sweeps. -// Knob 3: Phase. The direction of the perturbation in the complex plane. -// Knob 4: Cloud. The size of the random nudges whose roots make the clouds. -// Buttons: K1 tap holds the perturbation at 2^-23. K3 tap moves the perturbation down one power of x. +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the arcs and cloud dots run from purple at root 1 to rose at root 20 and the nebula is blue at its edge and cyan in its core; at 0.33 the arcs are yellow to green on a red and magenta nebula, at 0.66 cyan to blue on green and amber. +// Knob 2: Speed. At 2 the change climbs from 2^-35 to 2^-21 in 6 seconds and comes back down in 6, lingering at both ends; at 0.2 a round takes two minutes, at 3 eight seconds. +// Knob 3: Phase. Which way the change pushes, as a turn of the complex plane: a_19 becomes -210 - eps e^(2 pi i Phase). At 0 it is Wilkinson's: roots 16 and 17 meet first, at eps = 1.42e-10, then 14 and 15, 18 and 19, 12 and 13, and 10 and 11 at 1.91e-8, so at 2^-23 there are five pairs, the widest 16.73 +- 2.81i. At 0.5 eps pushes the other way and the neighbours change partners: 15 and 16 meet first, then 17-18, 13-14, 19-20, 11-12, and 9-10 just past 2^-23. At 0.25 and 0.75 eps is imaginary, there are no conjugate pairs, and no two roots come closer than 0.91: they peel off the axis one by one, alternately to the left and to the right. +// Knob 4: Cloud. delta, the size of the random relative nudges given to every coefficient below the top one, from 2^-45 at 0 to 2^-39 at 1; at the start 2^-44.6, about 330 times the rounding error of a double. There, with the roots on the integers, the dots of roots 13 to 16 land up to 1.7 away while root 5 moves 1e-4 and root 1 1e-11, and the delta set the clouds stay inside covers 19% of the panel; at 2^-23 it covers 2.4%. At 1 it covers 46% at the bottom of the sweep and still 45% at 2^-23. +// Buttons: K1 tap eases the change to exactly 2^-23 and holds it there for three seconds. K3 tap moves the change one power down, x^19, x^18 and on to the constant term 20!, then x^19 again: the roots first go home, then the sweep starts over on the new coefficient, scaled so that it pushes its most sensitive root as hard as 2^-23 pushes root 16; at x^0 the pairs run from 3-4 to 17-18. const W = 64, H = 128, N = W * H; -const { floor, min, max, abs, exp, log2, cos, sin, sqrt, hypot, PI } = Math; +const { floor, min, max, abs, exp, log2, cos, sin, hypot, PI } = Math; const cl = (v, a, b) => (v < a ? a : v > b ? b : v); const rnd = () => Math.random(); @@ -37,15 +37,16 @@ function hsv(h, s, v, out, o) { const RY = 6, RE0 = 0.25, CX = 9, XC = 31.5; const toX = (im) => XC + im * CX, toY = (re) => H - (re - RE0) * RY; -// The coefficients a_j of p(x) = (x - 1)...(x - 20), lowest first. Doubles -// are enough here: they only scale the perturbations. +// The coefficients a_j of p(x) = (x - 1)...(x - 20), lowest first: a_19 = +// -210, a_0 = 20!. In doubles a_3 to a_7 are already rounded, which is +// harmless here: they only scale the nudges and the level below. const A = new Float64Array(21); A[0] = 1; for (let k = 1; k <= 20; k++) for (let j = k; j >= 0; j--) A[j] = (j ? A[j - 1] : 0) - k * A[j]; const AB = A.map(abs); -// SENS[k]: the largest first-order shift a relative change of a_k can give any -// root, |a_k| r^k / |p'(r)| at its worst r. Every coefficient's sweep is -// scaled so that it pushes that root as hard as Wilkinson's 2^-23 on x^19. +// SENS[k]: the largest first-order shift a relative change of a_k gives any +// root, |a_k| r^k / |p'(r)| at its worst r (root 16 for a_19, root 10 for +// a_0). A K3 tap moves the change to another power, scaled by these. const SENS = new Float64Array(20); for (let k = 0; k < 20; k++) for (let r = 1; r <= 20; r++) { @@ -54,15 +55,23 @@ for (let k = 0; k < 20; k++) SENS[k] = max(SENS[k], (AB[k] * r ** k) / d); } -// The sweep runs log2 of the perturbation from LO to HI and back, easing at -// both ends; at LO the roots sit on the integers to within 0.001. +// The sweep runs log2 of the change from LO to HI and back, easing at both +// ends. At LO every root is within 0.07 of home (root 16 the farthest, under +// half a row); at HI root 20 is at 21.39 and the widest pair at +// 16.77 +- 3.49i, inside the panel's 31 columns either side. const LO = -35, HI = -21, WILK = -23; -// Aberth's iteration for the roots of p(z) + sum C_j z^j, j < 20, from the -// starting points in Z (re, im pairs), at most `iters` sweeps, roots with -// index below `from` held where they are. p and p' come from the product -// form, which stays accurate next to the roots where the expanded -// coefficients would cancel. Returns the largest last step squared. +// The Ehrlich-Aberth iteration for all roots of p(z) + sum C_j z^j, j < 20, +// at once, from the starting points in Z (re, im pairs), at most `iters` +// sweeps, roots with index below `from` held where they are. p and p' come +// from the product form, which stays accurate next to the roots where the +// expanded coefficients would cancel to nothing. Run from rest with eps = +// 2^-23 it gives Wilkinson's table to nine places: 1 to 4 to the last +// digit, 4.999999928, 6.000006944, 6.999697234, 8.007267603, 8.917250249, +// 10.095266145 +- 0.643500904i, 11.793633881 +- 1.652329728i, +// 13.992358137 +- 2.518830070i, 16.730737466 +- 2.812624894i, +// 19.502439400 +- 1.940330347i and 20.846908101. STEP[i] keeps each root's +// last step squared. const STEP = new Float64Array(20); function aberth(Z, Cr, Ci, iters, from) { let big = 0; @@ -133,17 +142,18 @@ export function setup(params) { const Z = new Float64Array(40); for (let i = 0; i < 20; i++) Z[2 * i] = i + 1; params.s = { - t: 0, ph: 0.17, e2: LO, pow: 19, pow0: 19, mix: 1, hold: 0, warm: 0, - Z, Zo: new Float64Array(40), W: new Float64Array(40), + ph: 0.17, e2: LO, pow: 19, pk: 19, hold: 0, warm: 0, + Z, Zo: new Float64Array(40), Zw: new Float64Array(40), Cr: new Float64Array(20), Ci: new Float64Array(20), Br: new Float64Array(20), Bi: new Float64Array(20), TI: F(N), TC: new Uint8Array(N), CR: F(N), CG: F(N), CB: F(N), DR: F(N), DG: F(N), DB: F(N), - pal: F(60), col: F(3), hue: 0, ld: -46, th: 0, + R: F(N), G: F(N), B: F(N), pal: F(60), col: F(3), hue: 0, ld: -45, th: 0, }; } // Trails: each root draws the segment it moved along this frame into TI as -// a line about three LEDs wide, keeping the brighter of what is there and -// the new line. +// a line about three LEDs wide, each LED it touches at its own brightness +// between 0.6 and 1 of the line's, keeping the brighter of that and what +// is there. Trails fade over three and a half seconds. function trail(S, x0, y0, x1, y1, c) { const n = max(1, Math.ceil(2 * hypot(x1 - x0, y1 - y0))); if (n > 200) return; @@ -168,49 +178,57 @@ function splat(R, G, B, x, y, r, g, b) { } } -// The perturbation: the coefficient of x^pow times 1 + eta e^(2 pi i th), -// with eta scaled so that 2^e2 on x^19 is Wilkinson's own change. After a -// K3 tap it slides from the old power to the new one over half a second. +// The change: a_k, k = S.pk, becomes a_k + sign(a_k) eps e^(2 pi i th), +// so with k = 19 and th = 0 it is Wilkinson's -210 - eps, eps = 2^e2. For +// another k, eps is scaled by |a_k| SENS[19] / (210 SENS[k]). function perturb(S) { - const a = 2 * PI * S.th, m = S.mix; + const k = S.pk, a = 2 * PI * S.th, e = 2 ** S.e2 * (AB[k] / 210) * (SENS[19] / SENS[k]) * Math.sign(A[k]); S.Cr.fill(0), S.Ci.fill(0); - for (const [k, f] of [[S.pow0, 1 - m], [S.pow, m]]) { - const e = f * 2 ** S.e2 * (AB[k] / 210) * (SENS[19] / SENS[k]) * Math.sign(A[k]); - S.Cr[k] += e * cos(a); - S.Ci[k] += e * sin(a); - } + S.Cr[k] = e * cos(a); + S.Ci[k] = e * sin(a); } function step(S, d, sp, clouds) { - // While K1's hold lasts the sweep waits and the perturbation eases to - // 2^-23; at most ten octaves a second either way. - if (S.hold > 0) S.hold -= d; - else S.ph += (d * sp) / 24; - S.mix = min(1, S.mix + d * 2); - const u = (1 - cos(2 * PI * S.ph)) / 2, to = S.hold > 0 ? WILK : LO + (HI - LO) * u; + // A K3 tap first takes the change back down to LO, where every root is + // home, then moves it to the next power and starts the sweep again from + // the bottom. K1's hold stops the sweep and eases the change to 2^-23. + // Either way the change moves at most ten octaves a second. + let to; + if (S.pk !== S.pow) { + to = LO; + if (S.e2 < LO + 0.3) (S.pk = S.pow), (S.ph = 0); + } else if (S.hold > 0) (S.hold -= d), (to = WILK); + else { + S.ph += (d * sp) / 24; + to = LO + ((HI - LO) * (1 - cos(2 * PI * S.ph))) / 2; + } S.e2 += cl(to - S.e2, -10 * d, 10 * d); perturb(S); const { Z, Zo } = S; Zo.set(Z); seed(Z); aberth(Z, S.Cr, S.Ci, 40, 0); + for (let i = 0; i < 40; i++) if (!Number.isFinite(Z[i])) Z.set(Zo), (i = 40); for (let i = 0; i < 20; i++) trail(S, toX(Zo[2 * i + 1]), toY(Zo[2 * i]), toX(Z[2 * i + 1]), toY(Z[2 * i]), i); if (clouds) cloud(S, clouds); } -// Clouds: the roots of p + C with every coefficient below the top one also -// multiplied by 1 + delta u_j, u_j uniform in [-1, 1], solved from the -// current roots. Each root lands as a dot of its root's colour and its own -// brightness. +// Clouds: eight times a frame, every coefficient below the top one of the +// current polynomial is also multiplied by 1 + delta u_j, u_j uniform in +// [-1, 1], and all twenty roots are solved again from the current ones. +// Each root whose last step was under 1e-4 lands as a dot of its root's +// colour and its own brightness, and fades over two seconds. With 24 +// sweeps 99.9% land even with the roots on the integers, where the +// nudged copies are hardest. function cloud(S, n) { - const { Z, W: Zw, Br, Bi, Cr, Ci, CR, CG, CB, pal } = S, dl = 2 ** S.ld; + const { Z, Zw, Br, Bi, Cr, Ci, CR, CG, CB, pal } = S, dl = 2 ** S.ld; for (let m = 0; m < n; m++) { for (let j = 0; j < 20; j++) (Br[j] = Cr[j] + dl * (2 * rnd() - 1) * A[j]), (Bi[j] = Ci[j]); Zw.set(Z); seed(Zw); - aberth(Zw, Br, Bi, 12, 4); - for (let i = 4; i < 20; i++) { - if (STEP[i] > 1e-8) continue; + aberth(Zw, Br, Bi, 24, 0); + for (let i = 0; i < 20; i++) { + if (!(STEP[i] <= 1e-8)) continue; const w = 0.25 + 0.75 * rnd(); const x = floor(toX(Zw[2 * i + 1])), y = floor(toY(Zw[2 * i])); if (x < 0 || x >= W || y < 0 || y >= H) continue; @@ -220,9 +238,17 @@ function cloud(S, n) { } } -// Dust: LEDs picked at random take a grain coloured by the pseudozero level -// at a random point inside them, L = log2 |p(z) + C(z)| / sum |a_j| |z|^j: -// the smallest relative change of the coefficients that makes z a root. +// The nebula. z is a root of some polynomial whose coefficients below the +// top one differ from the current ones by at most a share delta each +// exactly when |p(z) + C(z)| <= delta sum |a_j| |z|^j (Mosier 1986), so +// L = log2 of the ratio is the smallest such delta, in octaves. Each frame +// 1200 LEDs picked at random take a grain for a random point inside them: +// in the delta set (L < ld) a grain lights with a chance up to 70% and up +// to full brightness, in the 16 delta and 256 delta sets around it the +// chance and the brightness fall off to 20% and 65, and the colour runs +// from cyan in the core to blue at the edge. Beyond 256 delta the grains +// stay under 25, the LEDs' threshold, and take any colour. Grains fade over +// three seconds. function dust(S, n) { const { Cr, Ci, DR, DG, DB, col } = S; for (let m = 0; m < n; m++) { @@ -250,7 +276,7 @@ function dust(S, n) { if (depth > -8) { const v = rnd() < 0.2 + 0.5 * q ? 25 + (40 + 190 * q) * rnd() : 24.9 * rnd(), w = 0.4 * rnd(); DR[p] = v * (col[0] + w * (1 - col[0])), DG[p] = v * (col[1] + w * (1 - col[1])), DB[p] = v * (col[2] + w * (1 - col[2])); - } else (DR[p] = 24.99 * (0.15 * col[0] + 0.85 * rnd())), (DG[p] = 24.99 * (0.15 * col[1] + 0.85 * rnd())), (DB[p] = 24.99 * (0.15 * col[2] + 0.85 * rnd())); + } else (DR[p] = 24.99 * rnd()), (DG[p] = 24.99 * rnd()), (DB[p] = 24.99 * rnd()); } } @@ -264,9 +290,11 @@ export function update(dt, input, params) { for (let i = 0; i < 20; i++) hsv(S.hue + 0.8 + (0.17 * i) / 19, 0.75, 1, S.pal, 3 * i); const p = input.btnPressed || []; if (p[0]) S.hold = 3; - if (p[2]) (S.pow0 = S.pow), (S.pow = S.pow ? S.pow - 1 : 19), (S.mix = 0); - // The first frame starts a second and a half into the sweep, so the - // first pairs have left the axis and drawn their arcs. + if (p[2]) S.pow = S.pow ? S.pow - 1 : 19; + // The roots are walked up from home to the sweep's opening place, 2^-31.4, + // and the first second and a half is run before the first frame, so the + // first pairs have left the axis and drawn their arcs; at Speed 2, 2^-23 + // then comes at 1 s and the top at 2.5 s. if (!S.warm) { S.warm = 1; const u = (1 - cos(2 * PI * S.ph)) / 2, e0 = LO + (HI - LO) * u; @@ -274,7 +302,6 @@ export function update(dt, input, params) { S.e2 = e0; for (let i = 0; i < 45; i++) step(S, 1 / 30, sp, i > 30 ? 4 : 0), fade(S, 1 / 30), dust(S, 900); } - S.t += d; step(S, d, sp, 8); fade(S, d); dust(S, 1200); @@ -293,10 +320,9 @@ function fade(S, d) { export function draw(display, params) { const S = params.s; if (!S || !S.warm) return; - const { TI, TC, CR, CG, CB, DR, DG, DB, pal, Z } = S; - const R = new Float32Array(N), G = new Float32Array(N), B = new Float32Array(N); + const { TI, TC, CR, CG, CB, DR, DG, DB, R, G, B, pal, Z } = S; for (let p = 0; p < N; p++) { - const ti = TI[p], c = 3 * TC[p], tv = ti > 0.02 ? 190 * ti : 0, cg = 0.35; + const ti = TI[p], c = 3 * TC[p], tv = ti > 0.02 ? 190 * ti : 0; R[p] = DR[p] + tv * pal[c] + 235 * CR[p]; G[p] = DG[p] + tv * pal[c + 1] + 235 * CG[p]; B[p] = DB[p] + tv * pal[c + 2] + 235 * CB[p]; From b31e922ee683081fb7b251e79123af2060a03b38 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 11:02:28 +0000 Subject: [PATCH 23/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Zermelo=20Navigation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../patterns/zermelo-navigation.js | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/incoming/pattern-study/patterns/zermelo-navigation.js b/incoming/pattern-study/patterns/zermelo-navigation.js index 121857380..3c5d1035d 100644 --- a/incoming/pattern-study/patterns/zermelo-navigation.js +++ b/incoming/pattern-study/patterns/zermelo-navigation.js @@ -12,9 +12,9 @@ // @knobs Hue=0..1, Current=0..2.5, Shape=0..1, Harbour=-1..1 // // Knob 1: Hue. Turns the colours round the wheel together: at 0 a front leaves the harbour gold, turns green halfway and reaches the far coast blue, and the shadows are rose. -// Knob 2: Current. The fastest water, in boat speeds. At 0 the fronts are circles and the routes straight lines; up to 1 every LED is reached, at any shape, harbour and moment; past 1 shadows open, at Shape 0 on 2.5% of the panel at 1.5, 9.2% at 2 (the opening) and 19.7% at 2.5. +// Knob 2: Current. The fastest water, in boat speeds. At 0 the fronts are circles and the routes straight lines; up to 1 every LED is reached, at any shape, harbour and moment; past 1 shadows open, at Shape 0 on 1.6% of the panel at 1.5, 5.5% at 2 (the opening) and 11.5% at 2.5. // Knob 3: Shape. Three eddies at 0, two on the left side and one on the right, and at 1 a jet leaving the coast at the bottom centre that wanders more as it climbs; between, both at part strength. With the harbour inside a jet faster than the boat the fronts become teardrops carried up the jet, and at first only a cone above the harbour is reached: at Shape 1 and Current 2 the water there runs at 1.99 boat speeds and the cone's half-angle is asin(1 / 1.99) = 30 degrees. -// Knob 4: Harbour. Where the harbour stands on the coast, from column 8 at -1 to column 56 at 1. At Shape 0 the shadows do not move with it, the same 756 LEDs at Current 2 wherever it stands, since they belong to the current and the panel's edges. +// Knob 4: Harbour. Where the harbour stands on the coast, from column 8 at -1 to column 56 at 1. At Shape 0 the shadows do not move with it, the same 448 LEDs at Current 2 wherever it stands, since they belong to the current and the panel's edges. const W = 64, H = 128, N = W * H; const { floor, min, max, abs, sqrt, exp, sin, cos, round, PI, cosh } = Math; @@ -171,9 +171,14 @@ function begin(S, kv, at) { S.solving = true; } -// Dijkstra over the stencil, settling at most `budget` LEDs. A step's time -// is its length times the mean slowness at its two ends; a step with an end -// where no heading makes way along it is not taken. +// Dijkstra over the stencil, settling at most `budget` LEDs. A step is +// rowed in the mean of the currents at its two ends, and a step along which +// no heading makes way in that current is not taken. The mean of the two +// ends' slowness was tried first: it shut every step with either end too +// fast to row along, and the opening's shadow shrank as the grid was made +// finer, 756 LEDs on the LED grid and 472 on a grid four times finer. With +// the mean current it is 441 and 444, and the arrival times of the two +// grids differ by 0.69% at the median. function work(S, budget) { const T = S.Tn, WX = S.WX, WY = S.WY, done = S.done, pred = S.pred, pos = S.pos; while (S.hn > 0 && budget-- > 0) { @@ -185,11 +190,9 @@ function work(S, budget) { if (vx < 0 || vx >= W || vy < 0 || vy >= H) continue; const v = vy * W + vx; if (done[v]) continue; - const a = slow(wx, wy, SUX[k], SUY[k]); + const a = slow(0.5 * (wx + WX[v]), 0.5 * (wy + WY[v]), SUX[k], SUY[k]); if (a >= INF) continue; - const b = slow(WX[v], WY[v], SUX[k], SUY[k]); - if (b >= INF) continue; - const tv = tu + SL[k] * 0.5 * (a + b); + const tv = tu + SL[k] * a; if (tv < T[v]) { T[v] = tv; pred[v] = u; @@ -205,7 +208,7 @@ function work(S, budget) { // by the mean time to the far coast. function finish(S) { // A lone LED left unreached among reached ones is the stencil's doing, not - // the current's (30 of 1,713 shadows over 45 settings, never wider than + // the current's (51 of 1,713 shadows over 45 settings, never wider than // one LED): it takes the mean of its four neighbours. const Tn = S.Tn; for (let y = 1; y < H - 1; y++) From 38ce5b6f1e86fc7808fb1ed2d1a84210ff0e4ab8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 11:02:45 +0000 Subject: [PATCH 24/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20Zermelo=20Navigation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- incoming/pattern-study/patterns/zermelo-navigation.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/patterns/zermelo-navigation.js b/incoming/pattern-study/patterns/zermelo-navigation.js index 3c5d1035d..489185a18 100644 --- a/incoming/pattern-study/patterns/zermelo-navigation.js +++ b/incoming/pattern-study/patterns/zermelo-navigation.js @@ -336,8 +336,9 @@ export function draw(display, params) { const g = max(0.25, sqrt(gx * gx + gy * gy)); const q = (tau - t) / GAP, f = q - floor(q), dl = (min(f, 1 - f) * GAP) / g, behind = (f * GAP) / g; // A line on the front and a short glow on the harbour side of it. - // Where the boats barely make way the fronts crowd closer than two - // LEDs; there they melt into a steady dim glow rather than shimmer. + // Where the boats barely make way the fronts crowd together; from 3.5 + // LEDs apart down to 1.5 they melt into a steady dim glow rather than + // shimmer. const line = dl < 1.1 ? 1 - dl / 1.1 : 0, tail = behind < 9 ? 0.16 * exp(-behind / 2.5) : 0; const sp = cl((GAP / g - 1.5) / 2, 0, 1), v = max(line, tail) * sp + 0.12 * (1 - sp); if (v <= 0.01) continue; From a36091d039c7dcaab0f99d9bea6c6b7a54a88bd1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 11:08:24 +0000 Subject: [PATCH 25/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20README=20=EC=97=90=20=EC=97=B4=20=EA=B0=9C?= =?UTF-8?q?=EC=9D=98=20=ED=91=9C=EC=99=80=20=ED=8C=90=20=EC=A0=88,=20?= =?UTF-8?q?=EB=8B=A4=EC=9D=8C=20=ED=8C=90=20=EA=B0=90=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_01Gdqi6PeQBKagUB2vLqwQZR --- incoming/pattern-study/README.md | 99 +++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index bdcd0a73f..8612e5135 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 팹턮 연구 2026-10 -새 팹턮 548개륌 쉰여섯 갈래로 나눠 만듀었얎요. 1판에서 엎섞 갈래로 23개륌 만듀고, 2판에서 1판에 없던 아홉 갈래로 16개륌, 3판에서 하나륌, 4판에서 새 갈래읞 직묌로 하나륌 더했얎요. 5판은 새 팹턮 없읎 1판의 Euclid Pulse 륌 닀시 ì§°ê³ , 6판은 섞욎 팚널의 128쀄을 위아래로 ë‹€ 쓰는 팹턮 하나륌 더했얎요. 7판은 또렷한 몚양읎 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 8판은 새 갈래읞 Ʞ계로 하나륌 더했얎요. 9판은 새 팹턮 없읎 1판의 Sprout 을 닀시 ì§°ì–Žìš”. 10판은 섞욎 팚널을 떚얎지는 랔록의 우묌로 쓰는 팹턮 하나륌 더했얎요. 11판은 여러 색읎 ꜀ 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 12판은 새 갈래읞 천첎로 하나륌 더했얎요. 13판은 새 팹턮 없읎 1판의 Glyph Rain 을 닀시 ì§°ì–Žìš”. 14판은 절찚가 바뀌Ʞ 전에 띄욎 바퀎띌서, 섞욎 팚널의 높읎륌 ë‹€ 떚얎지는 폭포 하나륌 더했얎요. 15판은 바뀐 절찚의 첫 판읎띌 ì—Ž 갈래에 새 팚턎을 하나씩, 몚두 ì—Ž 개륌 더했얎요. 도시와 음악 갈래는 귞때 새로 엎었얎요. 16판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 바닀와 곀충 갈래륌 새로 엎었얎요. 17판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 전자 회로와 장난감 갈래륌 새로 엎었얎요. 18판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 유늬와 ꎑ묌·결정, 놀읎공원 갈래륌 새로 엎었얎요. 19판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 부엌곌 버섯·균사 갈래륌 새로 엎었얎요. 20판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 새와 화학·싀험 갈래륌 새로 엎었얎요. 21판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 종읎와 욎동·겜Ʞ 갈래륌 새로 엎었얎요. 22판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 몞곌 비행 갈래륌 새로 엎었얎요. 23판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 묎대·공연곌 짐승 갈래륌 새로 엎었얎요. 24판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 공사·걎섀곌 도자Ʞ 갈래륌 새로 엎었얎요. 25판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 농사·밭곌 화산 갈래륌 새로 엎었얎요. 26판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 얌음·극지와 믞생묌 갈래륌 새로 엎었얎요. 27판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 철도와 필늄·영화 갈래륌 새로 엎었얎요. 28판은 죌제 표가 수학 쪜윌로 바뀐 첫 판읎띌 읎늄 있는 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했고, 수론 갈래륌 새로 엎었얎요. 29판은 죌제 표가 수학 여섯곌 화장품 팝업 넷윌로 나뉜 첫 판읎띌, 읎늄 있는 수학 대상 여섯곌 화장품 맀장 벜에 틀 질감 넷을 더했고, 위상·맀듭곌 화장품 팝업 갈래륌 새로 엎었얎요. 30판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 맀장의 LED 벜에 틀 질감 넷을 화장품 팝업 갈래에 더했얎요. 31판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 ì—Žì§€ 않았얎요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜의 질감 넷을 화장품 팝업 갈래에 더했얎요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 41판은 죌제 표가 수학·추상 닀섯곌 할로윈 닀섯윌로 바뀐 첫 판읎띌, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고 할로윈 갈래륌 새로 ì—Žì–Ž 10월 31음에 틀 팹턮 닀섯을 넣었얎요. 42판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 41판읎 연 할로윈 갈래에 더했얎요. 43판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 44판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 45판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 ì—Ž 개 아래읞 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 46판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 47판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 48판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 49판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 50판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 적은 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 51판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 52판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 적은 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 53판은 할로윈 몫읎 쉬게 된 첫 판읎띌, 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 54판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 ì—Ž 갈래에 하나씩 더했얎요. 55판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 56판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 57판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 ì—Ž 갈래에 하나씩 더했얎요. 58판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 ì—Ž 갈래에 하나씩 더했얎요. 59판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 60판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 61판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 갈래부터 ì—Ž 갈래에 하나씩 더했얎요. 62판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 63판은 64판곌 같읎 죌제 표에 「타읎포」가 듀얎옚 첫 판읎띌, Ꞁ자가 듀얎간 팹턮 닀섯을 Ꞁ자 갈래에, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더했얎요. 64판은 죌제 표가 바뀌얎 Ꞁ자 팹턮 닀섯을 Ꞁ자 갈래에, 수학 대상 닀섯을 ë„€ 갈래에 더했얎요. [index.html](index.html) 을 람띌우저로 엎멎 전부 한 장에서 돌아가요. ë§š 위 지도에서 점을 누륎멎 ê·ž 팚턎윌로 가고, 칎드의 슬띌읎더가 녾뾌 넷읎에요. 마음에 드는 것은 「윔드 복사」로 가젞가 Pattern Lab 의 CODE 칞에 붙여 넣윌멎 같은 귞늌읎 나와요. 페읎지는 파음 하나띌서 바깥에서 받아 였는 것읎 없얎요. +새 팹턮 558개륌 쉰여섯 갈래로 나눠 만듀었얎요. 1판에서 엎섞 갈래로 23개륌 만듀고, 2판에서 1판에 없던 아홉 갈래로 16개륌, 3판에서 하나륌, 4판에서 새 갈래읞 직묌로 하나륌 더했얎요. 5판은 새 팹턮 없읎 1판의 Euclid Pulse 륌 닀시 ì§°ê³ , 6판은 섞욎 팚널의 128쀄을 위아래로 ë‹€ 쓰는 팹턮 하나륌 더했얎요. 7판은 또렷한 몚양읎 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 8판은 새 갈래읞 Ʞ계로 하나륌 더했얎요. 9판은 새 팹턮 없읎 1판의 Sprout 을 닀시 ì§°ì–Žìš”. 10판은 섞욎 팚널을 떚얎지는 랔록의 우묌로 쓰는 팹턮 하나륌 더했얎요. 11판은 여러 색읎 ꜀ 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 12판은 새 갈래읞 천첎로 하나륌 더했얎요. 13판은 새 팹턮 없읎 1판의 Glyph Rain 을 닀시 ì§°ì–Žìš”. 14판은 절찚가 바뀌Ʞ 전에 띄욎 바퀎띌서, 섞욎 팚널의 높읎륌 ë‹€ 떚얎지는 폭포 하나륌 더했얎요. 15판은 바뀐 절찚의 첫 판읎띌 ì—Ž 갈래에 새 팚턎을 하나씩, 몚두 ì—Ž 개륌 더했얎요. 도시와 음악 갈래는 귞때 새로 엎었얎요. 16판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 바닀와 곀충 갈래륌 새로 엎었얎요. 17판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 전자 회로와 장난감 갈래륌 새로 엎었얎요. 18판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 유늬와 ꎑ묌·결정, 놀읎공원 갈래륌 새로 엎었얎요. 19판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 부엌곌 버섯·균사 갈래륌 새로 엎었얎요. 20판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 새와 화학·싀험 갈래륌 새로 엎었얎요. 21판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 종읎와 욎동·겜Ʞ 갈래륌 새로 엎었얎요. 22판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 몞곌 비행 갈래륌 새로 엎었얎요. 23판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 묎대·공연곌 짐승 갈래륌 새로 엎었얎요. 24판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 공사·걎섀곌 도자Ʞ 갈래륌 새로 엎었얎요. 25판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 농사·밭곌 화산 갈래륌 새로 엎었얎요. 26판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 얌음·극지와 믞생묌 갈래륌 새로 엎었얎요. 27판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 철도와 필늄·영화 갈래륌 새로 엎었얎요. 28판은 죌제 표가 수학 쪜윌로 바뀐 첫 판읎띌 읎늄 있는 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했고, 수론 갈래륌 새로 엎었얎요. 29판은 죌제 표가 수학 여섯곌 화장품 팝업 넷윌로 나뉜 첫 판읎띌, 읎늄 있는 수학 대상 여섯곌 화장품 맀장 벜에 틀 질감 넷을 더했고, 위상·맀듭곌 화장품 팝업 갈래륌 새로 엎었얎요. 30판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 맀장의 LED 벜에 틀 질감 넷을 화장품 팝업 갈래에 더했얎요. 31판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 ì—Žì§€ 않았얎요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜의 질감 넷을 화장품 팝업 갈래에 더했얎요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 41판은 죌제 표가 수학·추상 닀섯곌 할로윈 닀섯윌로 바뀐 첫 판읎띌, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고 할로윈 갈래륌 새로 ì—Žì–Ž 10월 31음에 틀 팹턮 닀섯을 넣었얎요. 42판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 41판읎 연 할로윈 갈래에 더했얎요. 43판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 44판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 45판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 ì—Ž 개 아래읞 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 46판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 47판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 48판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 49판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 50판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 적은 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 51판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 52판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 적은 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 53판은 할로윈 몫읎 쉬게 된 첫 판읎띌, 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 54판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 ì—Ž 갈래에 하나씩 더했얎요. 55판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 56판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 57판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 ì—Ž 갈래에 하나씩 더했얎요. 58판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 ì—Ž 갈래에 하나씩 더했얎요. 59판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 60판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 61판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 갈래부터 ì—Ž 갈래에 하나씩 더했얎요. 62판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 63판은 64판곌 같읎 죌제 표에 「타읎포」가 듀얎옚 첫 판읎띌, Ꞁ자가 듀얎간 팹턮 닀섯을 Ꞁ자 갈래에, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더했얎요. 64판은 죌제 표가 바뀌얎 Ꞁ자 팹턮 닀섯을 Ꞁ자 갈래에, 수학 대상 닀섯을 ë„€ 갈래에 더했얎요. 66판도 같은 표로 돌아서 Ꞁ자 팹턮 넷을 Ꞁ자 갈래에, 하나륌 데읎터 귞늌 갈래에 더하고, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더했얎요. [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판에서 섞로로 닀시 ì§°ì–Žìš”. @@ -870,6 +870,21 @@ | [Hill's Regions](patterns/hill-regions.js) | 입자 | 두 질량을 위아래로 섞욎 회전 좌표계에서 입자 음곱읎 알윔비 상수의 영속도 곡선 안에 갇혀 돌아요. Jacobi 륌 낎늬멎 L1 부터 L3 까지 목읎 찚례로 엎렀서 입자가 작은 질량 쪜윌로 걎너가고 팹널 위로 빠젞나가요. | | [Indian Rope Trick](patterns/indian-rope-trick.js) | 묌늬·슀프링 | 섞 마디 막대 사슬읎 거꟞로 서서 팹널 킀륌 ë‹€ 채우고, 눈에 볎읎지 않을 만큌 작고 빠륎게 떚늬는 받칚점 덕에 쓰러지지 않아요. 몇 쎈마닀 톡 치멎 흔듀늬닀 수직윌로 돌아였고, Shake 륌 묞턱 아래로 낎늬멎 넘얎젞 받칚 아래로 휘돌았닀가 닀시 섞워젞요. | +### 66판 + +| 팹턮 | 갈래 | 한 쀄 | +| --- | --- | --- | +| [Gridfonts](patterns/gridfonts.js) | Ꞁ자 | 혞프슀태터의 gridfont 처럌 Ꞁ자 아홉읎 희믞한 점 격자륌 잇는 짧은 선분윌로만 서고, 녞람가 슀타음을 바꟞멎 몚든 Ꞁ자가 같은 규칙윌로 획을 거뒀닀가 새로 ê·žì–Žìš”. | +| [Etaoin Shrdlu](patterns/etaoin-shrdlu.js) | 데읎터 귞늌 | 앚늬슀 첫 두 묞장읎 한 자씩 ë–šì–Žì ž 잊은 찚례로 쀄 선 막대 엎여섯 개에 쌓읎고, ë‹€ 읜윌멎 읎늄표가 ETIONS AHRDWG 가 되얎 띌읎녞타읎프 자판의 ETAOIN SHRDLU 와 여섯 자늬가 맞아요. | +| [Barrier Grid](patterns/barrier-grid.js) | Ꞁ자 | 낱말곌 ê·ž 음화륌 한 칞씩 엇갈렀 ì§  읞쇄 위로 슬늿 난 막대 격자가 한 칞씩 믞끄러젞, ꞈ빛 「빛」읎 파란 바탕의 검은 「빛」윌로 뒀집히고 ë„€ 걞음마닀 닀음 낱말읎 와요. | +| [Subpixel](patterns/subpixel.js) | Ꞁ자 | a 와 g 의 TrueType 윀곜읎 여섯 배로 킀욎 픜셀 격자 위륌 픜셀 몇 분의 음씩 믞끄러지고, 밑의 격자는 ë§€ 프레임 닀시 래슀터화돌 RGB 서람픜셀 쀄묎늬와 색 번짐윌로 ë›°ì–Žìš”. | +| [Syllable Odometer](patterns/syllable-odometer.js) | Ꞁ자 | 자몚 바퀮 셋읎 죌행 Ʞ록계처럌 받칚부터 돌며 한Ꞁ 11,172 음절을 섞고, 완성형 2,350 에 든 음절은 ꜉ ì°¬ 혞박색윌로, 빠진 음절은 속 빈 볎띌 윀곜윌로 지나가며 아래 지도륌 채워요. | +| [Wilkinson's Polynomial](patterns/wilkinson-polynomial.js) | 알고늬슘 | (x − 1)
(x − 20) 의 ê·Œ 슀묎 개가 섞로 싀수축에 구슬로 서고, x¹⁹ 계수륌 2⁻²³ 너뚞까지 걎드늬멎 위쪜 ì—Ž 개가 둘씩 만나 옆윌로 날아가며 분홍 활을 낚겚요. 계수륌 삎짝 흔든 닀항식듀의 귌읎 둘레에 반짝읎는 성욎을 읎뀄요. | +| [Golomb Ruler](patterns/golomb-ruler.js) | 수론 | 14 눈ꞈ 최적 Golomb 자의 눈ꞈ 쌍 91개가 저마닀 닀륞 크Ʞ의 혞로 겹쳐 서고, 캘늬퍌가 Ꞟ읎 1부터 127까지 훑윌며 Ꞟ읎마닀 혞 하나륌 쌜거나 였륞쪜 Ꞟ읎 자에 빈칞을 낚겚요. | +| [Zermelo Navigation](patterns/zermelo-navigation.js) | 선·궀적 | 바닥 항구에서 0.5쎈마닀 배가 지ꞈ까지 닿을 수 있는 곳의 앞선읎 떠나, 소용돌읎나 제튞 묌삎에 휘고 접히며 팹널 끝까지 올띌가요. 가장 빠륞 êžž 여섯 쀄에 흰 점읎 싀렀 가고, 묌읎 배볎닀 빚늬 듀얎였는 가장자늬에는 끝낮 닿지 못하는 귞늌자가 빗ꞈ윌로 낚아요. | +| [Hodograph](patterns/hodograph.js) | 묌늬·슀프링 | 핮 하나륌 읎심률읎 닀륞 천첎 셋읎 쌀플러의 박자로 돌며 같은 시간 부채륌 쓞고, 아래에서는 한 원점에서 귞은 속도 화삎의 끝읎 원점에서 반지늄의 e 배만큌 비쌜 선 원을 따띌 돌아요. | +| [Shapley-Folkman](patterns/shapley-folkman.js) | Ʞ하·대칭 | 몚양 하나륌 1, 2, 4, 8 개 더핮 ê·ž 수로 나눈 믌윔프슀킀 평균읎 띠 넷윌로 쌓읎고, 였목한 만읎 띠마닀 절반씩 메워지며 흐늰 볌록 껍질에 닀가가요. | + ## 1판 갈래마닀 녾뾌 넷의 자늬는 갈래가 달띌도 거의 같아요. K1 은 색읎고 K2 는 빠륎Ʞ예요. K3 은 몚양을 바꟞고, K4 는 ê·ž 갈래만의 손잡읎예요. 귞래서 처음 볎는 팹턮도 K1 곌 K2 는 손읎 뚌저 알아요. @@ -3424,6 +3439,70 @@ Lab 에서 핮 볎Ʞ: Text 레읎얎에 Newsreader 로 IT WAS 륌 크게, 위 죌제 5 와 4 로 아치슚의 읞도 밧쀄 묘Ʞ륌 묌늬·슀프링 갈래에 섞웠얎요. 앀드룚 슀티랐슚(1908)은 받칚점을 위아래로 빠륎게 떚멎 거꟞로 선 진자가 선닀는 것을 볎였고, 표튞륎 칎플찚(1951)가 빠륞 떚늌을 평균한 유횚 퍌텐셜로 ê·ž 까닭을 풀었얎요. 데읎비드 아치슚(1993)은 마디가 여럿읞 사슬도 떚늌윌로 섞욞 수 있닀는 정늬륌 냈고, 톰 멀늰곌 핚께 『Nature』(1993)에 섞 마디 진자륌 거꟞로 섞욎 싀험을 싀었얎요. 팚널은 똑같은 막대 n 개륌 겜첩윌로 읎은 사슬의 띌귞랑죌 욎동 방정식을 받칚점 좌표계의 쀑력 g − Aω² cos ωt 로 풀얎요. 계산마닀 n×n 질량 행렬을 풀고, 4ì°š 룜게-쿠타로 떚늌 한 죌Ʞ륌 25걞음씩 나눠 섞 마디음 때 1쎈에 1,663걞음을 셈핎요. 사슬 í‚€ 116 LED 에 대핮 √(L/g) 륌 0.75쎈로 두었더니 섞 마디의 받칚점은 66 Hz 로 0.74 LED 만 였륎낎렀서, 팚널에서는 떚늌읎 거의 볎읎지 않고 사슬읎 귞냥 서 있는 것처럌 볎여요. Shake 륌 사슬 전첎 킀로 잰 받칚점 최고 속도 Aω/√(gL) 로 두자, 섞 마디부터 여섯 마디까지 서는 묞턱읎 몚두 1.25~1.3 윌로 거의 같았얎요. 막대 하나의 칎플찚 묞턱 √(4/3) = 1.155 에 가까욎 값읎에요. 처음 판은 마디 마찰만 넣얎서 톡 친 흔듀늌읎 24쎈 동안 거의 쀄지 않았고, 3쎈마닀 였는 닀음 탭읎 쌓여 ꌭ대Ʞ가 29 LED 까지 벌얎졌얎요. 질량 행렬에 비례하는 공Ʞ 저항을 더하자 한 번 흔듀늎 때마닀 0.6 배로 쀄며 수직윌로 돌아왔얎요. 마디의 겜로륌 싀처럌 낚Ʞ던 판은 가로쀄읎 사슬에 걞늰 깃발처럌 읜혀서 지우고, 0.3쎈 잔ꎑ윌로 흔듀늰 자늬륌 부채ꌎ로 낚게얎요. 잰 값은 움직임 0.251, 쎘쎘핚 0.019, 가장자늬 0.321, 복잡도 0.452, 채움 0.145, 색 0.122, 몚양 바뀜 0.104, thirds 16/12/15 읎고, 녾뾌는 0.04, 0.09, 0.03, 0.12 예요. 뜻밖읎었던 것은 너묎 섞게 떹 때였얎요. 서 있는 범위에는 위쪜 끝도 있얎서, 여섯 마디는 Shake 2.15 부터, 닀섯 마디는 2.7 부터 닀시 묎너젞요. Shake 3 의 여섯 마디는 옆윌로 넘얎지는 대신 받칚점 위에서 접히고 뒀엉쌜 한 덩얎늬가 돌요. +## 66판 + +66판은 65판(#328)곌 같읎 돌았얎요. 앞 바퀮 PR 에는 따륌 댓Ꞁ도 늬뷰도 없었얎요. 65판읎 PR 볞묞에 뚌저 밝힌 ì—Ž 개와 지도 칞 둘을 읜고 겹치지 않게 곚랐는데, 처음 떠올늰 슀윧 김의 앰비귞랚곌 훈믌정음의 제자 원늬, 큎띌읞의 돛은 65판읎 읎믞 곚띌 두얎서 접었얎요. 타읎포 닀섯은 「타읎포 싀험」의 출발점 가욎데 셋을 맡았얎요. Gridfonts 는 녞람와 Ꞁ, Barrier Grid 는 Ꞁ자와 빛읎고, Etaoin Shrdlu 와 Syllable Odometer 는 숫자와 Ʞ혞 놀읎예요. Subpixel 은 출발점 밖에서 곚랐고, 윀곜 Ꞁ자가 LED 격자에 떚얎지는 자늬가 죌읞공읎에요. Ꞁ자 ꌎ도 닀섯읎 ë‹€ 달띌요. Gridfonts 는 3×7 점 격자의 선분 56개로 소묞자 26자륌 짓고, Etaoin Shrdlu 는 18판 Typewriter 의 3×5 대묞자륌 옮겚 숫자와 포의 Ʞ혞륌 더했얎요. Barrier Grid 는 한 음절읎 팚널을 ë‹€ 채우는 굵은 한Ꞁ 넷곌 띌틎 Ꞁ자 여덟을 ê·žë žê³ , Subpixel 은 TrueType 처럌 2ì°š 베지에 윀곜윌로 a, g, R, &, 한, Ꞁ 여섯 자륌 적었얎요. Syllable Odometer 는 64판 Wongoji 의 9×11 조합 Ꞁꌎ을 킀워 썚요. 수학 닀섯은 548개에 없던 대상읎고 「낯선 수학 예」에서는 하나도 고륎지 않았얎요. 알고늬슘, 수론, 선·궀적, 묌늬·슀프링, Ʞ하·대칭에 하나씩 넣었얎요. 죌제 1 은 셋뿐읞 칞 ì—Žì…‹ 가욎데 65판읎 고륞 쎘쎘핚 × 색 4,4 와 색 × 몚양 바뀜 4,4 륌 빌고 쎘쎘핚 × 몚양 바뀜 4,4 와 복잡도 × 채움 4,1 을 겚눎얎요. 죌제 4 는 Golomb Ruler 와 Zermelo Navigation 읎고, Etaoin Shrdlu 도 128쀄을 ë‹€ 썚요. + +### Gridfonts + +죌제 7 의 「녞람와 Ꞁ」로, 녾뾌 하나가 Ꞁ자 아홉 자의 슀타음을 한꺌번에 바꟞는 팚턎읎에요. 더Ꞁ러슀 혞프슀태터(Douglas Hofstadter)는 1980년대에 가로 3점, 섞로 7점 격자에서 읎웃한 두 점을 잇는 짧은 선분(quantum)만윌로 소묞자 26자륌 짓는 gridfont 륌 ê·žë žê³ , 게늬 맥귞로(Gary McGraw)의 박사 녌묞(1995)곌 『Fluid Concepts and Creative Analogies』(1995)의 Letter Spirit 은 한 gridfont 의 몚든 Ꞁ자에 같은 슀타음읎 흐륎는 것을 Ꞁ자의 정신윌로 닀뀘얎요. 윔드에서 읎웃한 점 쌍을 섞얎 볎니 가로와 섞로가 14 와 18, 대각선읎 24 띌서 몚두 56 개였얎요. 팚널은 점 사읎륌 7 LED 로 두고 한 쀄에 섞 Ꞁ자륌 빈 칞 하나씩 띄워 놓아서, 아홉 Ꞁ자의 점읎 9×19 점짜늬 격자 하나에 ë–šì–Žì žìš”. 쀄끌늬 점 한 쀄을 나눠 쓰니 아래로 낮며 획곌 위로 올늰 획읎 거Ʞ서 만나지 않는 낱말만 곚랐고, 음곱 쪜(the sly fox, abc def ghi 같은 것)읎 여섯 슀타음 얎디서도 ê·ž 쀄에서 닿지 않는 것을 윔드로 셌얎요. 슀타음은 Square 바탕 Ꞁꌎ에 규칙을 하나씩 걞얎요. Cut 은 가로 quantum 하나와 섞로 quantum 하나만 만나는 몚서늬륌 대각선 하나로 바꟞고, Lean 은 왌쪜 위와 였륞쪜 아래 몚서늬만 깎아서 Ʞ욎 Ꞁ씚처럌 볎여요. Gable 은 귞늇 위 가로대륌 한 칞 위 ꌭ짓점윌로 올늬고 귞늇 아래 가로대는 한 칞 아래로 낎렀요. Stencil 은 quantum 마닀 양 끝을 1.8 LED 씩 떌고, Outline 은 획에서 2.1 LED ë–šì–Žì§„ 둘레만 쌜요. 슀타음읎 바뀌멎 두 Ꞁ자가 같읎 가진 quantum 은 귞대로 서고, 빠질 것은 끝 쪜윌로 빚렀 듀얎간 ë’€ 새 것읎 펜 찚례대로 0.09 쎈 간격윌로 ê·žì–Žì žìš”. 첫 판은 움직임읎 0.018 읎띌 지도 축의 아래 끝 0.019 밖에 ì„°ì–Žìš”. 바뀜곌 바뀜 사읎 4 쎈 동안 Ꞁ자가 가만히 서 있었Ʞ 때묞읎에요. Ꞁ자마닀 펜 Ꞟ을 따띌 빛읎 quantum 하나에 0.3 쎈씩 지나가게 하자 0.046 읎 됐얎요. 처음 Inline 읎띌 부륞 슀타음은 quantum 마닀 평행선 두 쀄을 귞얎서 몚서늬마닀 선읎 엇갈늰 # ꌎ읎 생게얎요. LED 마닀 가장 가까욎 획까지의 거늬륌 뚌저 몚윌고 ê·ž 거늬가 2.1 읞 곳을 쌜자, 만나는 획듀읎 둘레 하나륌 나눠 가젞 넀옚ꎀ 같은 Outline 읎 됐얎요. Cut 은 u 륌 v 와 똑같은 quantum ë„€ 개로 만듀었얎요. 귞래서 규칙읎 닀륞 Ꞁ자륌 만듀멎 ê·ž Ꞁ자만 묌러서서, 자Ʞ로 낹는 데까지만 깎게 했얎요. u 는 왌쪜 아래 몚서늬륌 낚겚요. 잰 값은 움직임 0.046, 쎘쎘핚 0.052, 가장자늬 0.460, 복잡도 0.331, 채움 0.267, 색 0.172, 몚양 바뀜 0.045 읎고, 녾뾌는 0.08, 0.08, 0.14, 0.07 읎에요. Style 녾뾌 0 에서는 여섯 슀타음읎 돌아가며 한 번은 같은 낱말에 새 슀타음을, 닀음 번은 ê·ž 슀타음로 새 낱말을 볎여 죌고, Grid 녞람륌 올늬멎 칞마닀 56 quantum 귞묌읎 희믞하게 깔렀요. 새로 볞 것은 대각선 quantum 읎 읎 팚널에서 9.9 LED 띌는 거예요. Cut 은 알파벳의 quantum 을 187 개에서 145 개로 22% 쀄읎는데 획 Ꞟ읎는 1,344 LED 에서 1,172 LED 로 13% 만 쀄고, Grid 0 에서 음곱 쪜을 재멎 쌜진 LED 도 29.3% 에서 25.3% 로 14% 쀄얎요. + +Lab 에서 핮 볎Ʞ: Text 레읎얎륌 마슀크로 두고 Inter 로 큰 소묞자 한 자(예: g, 크Ʞ 120)륌 쓎 ë’€ Grid 녞람륌 0.6 쯀 올늬멎, gridfont Ꞁ쀄곌 quantum 귞묌읎 ê·ž 큰 Ꞁ자 안에서만 볎여서 곡선 Ꞁꌎ 한 자륌 직선 quantum 읎 채워요. + +Ʞ능 감: 녾뾌 값에 읎늄을 닀는 Ꞟ읎요. Style 녾뾌는 0 부터 6 까지 음곱 자늬(돌아가Ʞ, Square, Cut, Lean, Gable, Stencil, Outline)읞데 `@knobs` 는 읎늄곌 범위만 받아서 Lab 녞람가 읎얎진 슬띌읎더로 볎읎고, 지ꞈ 얎느 슀타음읞지는 `// Knob 3:` 죌석을 읜얎알 알아요. `Style=0..6` 뒀에 자늬 읎늄을 적윌멎 Lab 읎 칞을 끊얎 읎늄을 볎여 죌멎 좋겠얎요. + +### Etaoin Shrdlu + +죌제 7 의 「숫자와 Ʞ혞 놀읎」와 죌제 4 륌 겚눎고, Ꞁ자륌 읜을거늬가 아니띌 셀 거늬로 볎는 귞늌읎에요. 였튞마얎 메륎겐탈러의 띌읎녞타읎프(1886)는 소묞자 자판을 쓰읎는 횟수대로 놓아서 왌쪜 두 쀄읎 etaoin 곌 shrdlu 였얎요. 쀄을 잘못 친 식자공읎 ê·ž 두 쀄을 손가띜윌로 쓞얎낎렀 쀄을 채우멎, ê·ž 쀄읎 가끔 신묞에 귞대로 찍혔얎요. 팚널은 위 7쀄에 3×5 대묞자로 Ꞁ을 짜고, ë§š 아래 읜는 쀄에서 Speed 2 Ʞ쀀 1쎈에 8자씩 Ꞁ자가 ë–šì–Žì žìš”. ë–šì–Žì§„ Ꞁ자는 폭 3 LED 의 막대 엎여섯 개 가욎데 제 막대로 가서 쌓읎고, 막대는 섌 수가 많은 찚례로 서 있닀가 하나가 앞 막대륌 넘윌멎 0.3쎈쯀 믞끄러젞 자늬륌 바꿔요. 막대 밑에는 ê·ž Ꞁ자가, ê·ž 밑에는 띌읎녞타읎프의 ì°šë¡€ ETAOINSHRDLUCMFW 가 흐늬게 있고, 같은 자늬에 같은 Ꞁ자가 서멎 둘 ë‹€ 밝아젞요. 색은 Ꞁ자마닀 띌읎녞타읎프 찚례에서의 자늬로 정핎서, ê·ž 찚례륌 닮은 Ꞁ은 막대가 묎지개로 읎얎지고 Lorem ipsum 은 묎지개가 끊겚요. Ꞁ을 ë‹€ 읜윌멎 막대의 앞 엎두 Ꞁ자가 한 쀄로 죌조되얎 Ꞁ쀄 ë§š 아래에 붙얎요. Text 녞람의 Ꞁ은 룚읎슀 캐럎의 『읎상한 나띌의 앚늬슀』(1865) 첫 두 묞장, 킀쌀로의 『최고선악론』(Ʞ원전 45년)에서 옚 Lorem ipsum, 팬귞랚 ì—Ž 개, 에드거 앚런 포의 「황ꞈ 벌레」(1843) 암혞묞읎에요. 앚늬슀 464자는 ETIONS AHRDWG 로 끝나 엎두 자늬 가욎데 여섯읎 띌읎녞타읎프와 같고, 엎두 Ꞁ자 가욎데 엎읎 겹쳐요. 빠진 것은 L 곌 U 읎고, WAS 와 WITH 같은 낱말 덕에 W 가 14번윌로 11위에 듀었얎요. Lorem ipsum 369자는 IETAOU NLRDSM 윌로 같은 자늬가 둘읎고, 띌틎얎 얎믞 -US 와 -UM 때묞에 U 가 6위예요. 팬귞랚 303자는 I 가 23번윌로 가장 많고 엎음곱 Ꞁ자가 ꌭ 10번씩읎띌 같은 자늬가 하나도 없얎요. 포의 암혞묞은 평묞을 레귞랑의 엎쇠로 닀시 바꿔 넣었는데, 섞얎 볎니 8 읎 33번, ; 가 26번윌로 레귞랑읎 소섀에서 섞는 표의 첫뚞늬와 같았얎요. 203자는 8;4)‡* 56(†10, ê³§ ETHSON AIRDFL 로 끝나 닀섯 자늬가 맞고, 읎 Ꞁ만은 끝나멎 ë§š 아래 쀄읎 엎쇠로 바뀌얎 Ʞ혞마닀 뜻하는 Ꞁ자륌 볎여 쀘요. 첫 판은 Ꞁ을 새로 시작하멎 막대가 비얎 있얎서, 새 Ꞁ자는 처음 한 자가 칞 없읎 회색윌로 72쀄을 ë‹€ ë–šì–Žì ž 바닥에 닿은 뒀에알 칞을 얻었얎요. Ꞁ자가 쀄을 떠나는 순간 빈 칞을 하나 맡게 하자 읎늄표가 뚌저 서고 Ꞁ자가 귞늬로 떚얎졌얎요. 또 Forget 1 에서는 마지막 20자만 섞서 가장 ꞎ 막대도 4자였는데, 한 자륌 6.9쀄로 두는 바닥값 때묞에 막대가 62쀄 가욎데 28쀄에서 멈췄얎요. 바닥값을 Ʞ억하는 Ꞁ자 수의 5분의 1(4~9)로 바꟞자 한 자가 가욎데에 Ꞁ자륌 새ꞎ 15.5쀄짜늬 활자가 되얎 막대가 위까지 찌얎요. 잰 값은 움직임 0.539, 쎘쎘핚 0.149, 가장자늬 0.995, 복잡도 0.155, 채움 0.312, 색 0.287, 몚양 바뀜 0.058 읎고, thirds 는 26/17/49, 늘 쌜 둔 띌읎녞타읎프 쀄을 빌멎 27/17/44 예요. 녾뾌는 0.10, 0.09, 0.12, 0.10 읎고, Forget 은 0.06 에서 앚늬슀 착지 100번에 순서륌 18번 바꟞고 1 에서는 80번 바꿔요. 읎 팚널에서 새로 볞 것은 동점읎 찚례륌 정하는 거예요. 앚늬슀에서 T 와 I 는 ꌭ 40번씩읞데, 뚌저 40에 닿은 T 가 왌쪜에 낚아서 띌읎녞타읎프와 같은 자늬가 닀섯에서 여섯읎 됐얎요. 팬귞랚은 10번씩읞 음곱 막대가 킀가 같아 ë–šì–Žì§„ 순서로만 찚례가 갈늬는데, Speed 0.2~2 에서는 J 가 C 앞읎고 3 에서는 C 가 J 앞읎에요. 막대마닀 떚얎지는 거늬가 달띌서, Ꞁ자가 빚늬 나였멎 뒀에 읜은 Ꞁ자가 뚌저 닿Ʞ도 핎요. + +Lab 에서 핮 볎Ʞ: Text 레읎얎에 Inter 로 E 한 자륌 크Ʞ 100, sharp 로 팹널 가욎데에 두고 role 을 mask 로 바꟞멎, 막대와 떚얎지는 Ꞁ자가 큰 E 안에서만 볎여서 가장 잊은 Ꞁ자가 제 빈도 막대로 칠핎젞요. maskInvert 륌 쌜멎 거꟞로 E 자늬만 비얎요. + +Ʞ능 감: 팚턎읎 Text 레읎얎의 Ꞁ을 받아 섞는 Ꞟ읎요. ë„€ Ꞁ을 윔드에 박아서 Text 녞람가 넷 가욎데서만 고륎는데, 받는 Ꞟ읎 생겚도 Text 레읎얎는 200자까지띌(`TEXT_MAX_CHARS`) 앚늬슀 첫 200자의 157Ꞁ자만 듀얎가고, 귞만큌윌로 섞멎 같은 자늬가 464Ꞁ자음 때의 여섯에서 셋윌로 쀄얎요. 팹턮 쪜윌로 가는 Ꞁ에는 더 ꞎ 한도가 있얎알 핎요. + +### Barrier Grid + +죌제 7 의 「Ꞁ자와 빛」곌 죌제 1 의 쎘쎘핚 × 몚양 바뀜 4,4 칞을 같읎 겚눎얎요. ê·ž 칞에는 팚턎읎 셋뿐읎었얎요. 룚퍌슀 버틀러 시더(Rufus Butler Seder)의 『Gallop!』(2007)은 여섯 장멎을 띠로 엇갈렀 한 장에 찍고, 쀄묎늬 아섞테읎튞가 ê·ž 위륌 지나가며 장멎을 하나씩 드러낎요. 읎 팚턎은 ê·ž 막대 격자 애니메읎션을 Ꞁ자로 핎요. 읞쇄는 낱말곌 ê·ž 음화륌 한 칞씩 엇갈렀 짜고, 한 죌Ʞ에 슬늿읎 하나 난 막대 격자가 한 걞음에 한 칞씩 믞끄러젞요. 걞음마닀 ꞈ빛 낱말읎 파란 바탕의 검은 낱말로 뒀집히고, ë„€ 걞음마닀 닀음 낱말읎 와요. 낱말은 빛, LOOK, 달, NEON, 별, HALO, ë°€, SEEN 여덟읎에요. 한Ꞁ은 한 음절읎 팚널을 ë‹€ 채우고 섞로획읎 12 LED 읎고, 띌틎 Ꞁ자는 28×58 칞 넷에 섞로획읎 8 LED 예요. 음화의 빛은 색상환의 맞은펞읎띌 ꞈ빛의 음화는 파랑읎에요. 막대가 믞끄러지는 동안에는 두 장멎읎 섞여요. Tilt 륌 돌늬멎 막대가 êž°ìšžì–Ž, 레음늬 겜읎 1874년 회절 격자에 ꎀ한 Ꞁ에서 처음 섀명한 평행선 묶음 둘의 몚아레 띠가 낱말을 가로질러요. 띠 하나에서는 낱말읎 빛읎고 닀음 띠에서는 귞늘읎에요. 첫 판은 죌Ʞ 2 에 막대 아래 읞쇄가 14% 비치게 했는데, 쎘쎘핚 0.250 곌 몚양 바뀜 0.228 로 칞에 못 믞쳀얎요. 몚양 바뀜읎 볎는 4×16 랔록에서 죌Ʞ 2 의 선은 ë„€ ì—Ž 가욎데 둘만 쌜젞서 랔록 평균읎 많아알 0.5 만큌 움직여요. 거Ʞ에 막대 아래 비친 음화가 얎두욎 쪜을 듀얎 올렞고, 2쎈 장멎도 막대가 믞끄러지는 한가욎데에 걞렞얎요. 막대에 바둑판윌로 구멍을 낎자 0.310 곌 0.342 가 됐얎요. 구멍은 옆 슬늿 빛의 0.35 륌 받고 아래 읞쇄륌 0.2 만큌 비춰요. 받은 빛은 랔록을 밝히멎서 읎웃 LED 사읎 찚읎륌 귞대로 두고, 비친 읞쇄는 ê·ž 찚읎만 더핎요. 귞닀음 「빛」의 섞로획을 4 LED 의 배수에 섞웠얎요. 랔록읎 Ꞁ자나 바탕 한쪜에 옚전히 드는 정도(|2a − 1| 의 평균)가 0.643 에서 0.726 윌로 올띌 몚양 바뀜읎 0.385 가 됐얎요. 잰 값은 움직임 0.348, 쎘쎘핚 0.316, 가장자늬 0.814, 복잡도 0.279, 채움 0.705, 색 0.294, 몚양 바뀜 0.381 읎고, 녾뾌는 0.20, 0.36, 0.20, 0.18 읎에요. 채움에는 밝Ʞ 25 로 비치는 막대 색까지 듀얎가요. 『Gallop!』은 여섯 장멎을 짜지만 Frames 는 4 에서 멈췄얎요. 8 LED 획읎멎 ë„€ 칞 죌Ʞ에서 쌜진 쀄읎 획마닀 둘뿐읎에요. 새로 볞 것은 숚은 귞늌읎에요. 낱말곌 음화륌 ì§  읞쇄는 두 칞마닀 ꌭ 하나가 쌜진 고륞 쀄묎늬띌서, 막대가 반 칞 지난 순간 밝Ʞ만 볎멎 낱말읎 사띌젞요. 「빛」은 4,096 쌍 가욎데 4,047 쌍읎 귞렇고, 섞로획읎 ë‹€ 짝수 엎에서 시작하는 「달」은 4,096 쌍읎 ë‹€ 귞래요. ê·ž 순간 낱말은 ꞈ빛곌 파랑의 색윌로만 낚아요. 띌틎 칞을 29 에서 28 LED 로 쀄여 획을 쌍에 맞추자 LOOK 도 3,971 쌍(97%)읎 됐얎요. + +Lab 에서 핮 볎Ʞ: 읎 팹턮 위에 Text 레읎얎륌 올늬고 Mask 의 masks layer below 와 invert 륌 쌠 ë’€ Inter 로 EYE 륌 size 28 쯀 가욎데에 두멎 ê·ž 자늬만 꺌젞요. ꞈ빛 장에서는 얎둠에 묻히거나 ꞈ빛 획을 조ꞈ 깎을 뿐읎고, 파란 음화 장에서만 검은 낱말로 떠서 걞음마닀 숚었닀 나였는 두 번짞 Ꞁ읎 돌요. + +Ʞ능 감: 윔드 레읎얎가 Text 레읎얎의 Ꞁ을 낱말 목록윌로, 낱말마닀 덮임 비튞맵윌로 받는 Ꞟ읎요. 읎 팚턎은 걞음마닀 닀륞 낱말읎 필요핎서 Text 레읎얎 한 장의 귞늌윌로는 몚자띌고, 귞래서 빛, 달, 별, ë°€ ë„€ 음절곌 띌틎 Ꞁ자 여덟 자(A E H K L N O S)륌 윔드에 손윌로 ê·žë žì–Žìš”. + +### Subpixel + +죌제 7 「타읎포」로, 벡터 Ꞁ자가 픜셀읎 되는 자늬륌 Ꞁ자 갈래에 섞웠얎요. 팚널읎 ê·ž 자첎로 RGB LED 격자띌서 LCD 의 서람픜셀을 LED 엎로 귞대로 옮Ꞟ 수 있닀는 데서 시작했얎요. Ꞁ자 ꌎ은 애플읎 1991년에 낎놓은 TrueType 처럌 곡선 위 점곌 곡선 밖 2ì°š 베지에 조종점의 목록윌로 적고, 조종점 둘읎 읎얎지멎 ê·ž 가욎데에 곡선 위 점읎 숚얎 있닀는 규칙도 따띌요. 타원은 곡선 위 점 없읎 조종점 여덟 개로만 적었고, 숚은 점 여덟읎 몚두 타원 위에 놓여요(였찚 4×10⁻¹⁶). 겹쳐도 되는 윀곜 여러 개륌 0 아닌 감김수로 칠하고, 바깥 윀곜은 TrueType 처럌 시계 방향읎에요. a 와 g 는 13.2 ppem, R 곌 & 는 12.2 ppem 윌로 래슀터화핎 한 픜셀을 6×6 LED 로 킀우고, 한곌 Ꞁ은 한 칞에 자몚 셋읎 듀얎가서 19.5 ppem 에 3×3 LED 로 킀워요. 픜셀 행마닀 표볞 쀄 음곱(한Ꞁ은 닀섯)에서 윀곜 안쪜 구간을 x 로는 정확히 재얎 픜셀을 셋윌로 나눈 칞에 몚윌멎, 한 번에 회색 덮임곌 서람픜셀 값읎 나와요. Mode 0 은 Microsoft 가 1998년에 발표한 ClearType 처럌 픜셀의 섞 칞을 따로 표볞윌로 삌고 FreeType 의 Ʞ볞 LCD 필터(8, 77, 86, 77, 8 을 256 윌로 나눈 닀섯 탭)륌 걎 ë’€, 6 LED 픜셀을 두 엎씩 빚강, 쎈록, 파랑윌로 밝혀요. Mode 1 은 같은 섞 값을 픜셀 하나의 색윌로 칠핎서, 얎두욎 바탕의 밝은 Ꞁ자는 왌쪜 가장자늬가 파랗고 였륞쪜읎 죌황윌로 번젞요. Mode 3 은 픜셀 쀑심읎 윀곜 안에 있을 때만 쌜는 TrueType 의 첫 규칙읎띌, g 의 고늬륌 잇는 목곌 & 의 위 고늬가 끊게닀 읎얎젞요. 처음 판은 겹친 윀곜의 안쪜 변까지 선윌로 귞렀서 a 의 쀄Ʞ 속에 사닀늬ꌎ읎, ㄎ 곌 ㄹ 의 몚서늬에 사각형 테두늬가 비쳀얎요. 윀곜을 LED 마닀 한 번 칠한 마슀크륌 두고 선 양쪜 1.2 LED 가 ë‹€ 안쪜읎멎 Ꞌ지 않자 바깥 윀곜만 낚았얎요. 힌팅도 처음엔 쀄Ʞ마닀 흐륎는 격자에 대고 반올늌핎서, a 의 x 높읎가 떠 있는 자늬에 따띌 6, 7, 8 행윌로 바뀌었고 1비튞 래슀터는 프레임의 14.4% 에서 바뀌었얎요. Ꞁ자 원점을 뚌저 정수 픜셀에 놓고 ê·ž 위에서 반올늌하자 x 높읎는 ±5.5% 로 숚 쉬는 크Ʞ에만 따띌 7 행(프레임의 91%)곌 6 행을 였가고, 래슀터는 프레임의 6.4% 에서만 바뀌얎요. 힌팅을 끄멎 55.4% 예요. 잰 값은 움직임 0.221, 쎘쎘핚 0.066, 가장자늬 0.492, 복잡도 0.706, 채움 0.445, 색 0.408, 몚양 바뀜 0.223 읎고, 녾뾌는 0.23, 0.16, 0.19, 0.03 읎에요. 새로 볞 것은 쀄Ʞ륌 정수 픜셀에 맞춰도 색 번짐읎 낚는닀는 거예요. 한 픜셀 폭윌로 맞춘 섞로 쀄Ʞ는 읎 필터륌 지나 (171, 240, 171) 로 쎈록읎 돌고, 양옆 픜셀에 (0, 8, 85) 와 (85, 8, 0) 읎 새얎요. 귞래서 a 와 g 에서 섞 값읎 4분의 1 넘게 벌얎진 픜셀읎 힌팅 없읎는 쌜진 픜셀의 46% 였는데, 쀄Ʞ까지 맞추자 60% 로 늘었얎요. + +Lab 에서 핮 볎Ʞ: Text 레읎얎에 Inter 로 ag 륌 size 13 에 두고 sharp 륌 쌰닀 끄멎, 읎 팚턎읎 여섯 배로 킀욎 13.2 ppem Ꞁ자륌 Lab 읎 제 크Ʞ로 귞늰 것곌 견쀄 수 있얎요. Lab 은 canvas 가 칠한 알파륌 덮읞 양윌로만 읜얎 회색 덮임밖에 없고, sharp 에서는 128/255 읎상 덮읞 LED 륌 쌜서 Mode 3 의 픜셀 쀑심 규칙곌 쌜지는 LED 가 달띌요. 큰 Ꞁ자와 겹치니 effect 륌 twinkle 로 두얎 가끔만 볎읎게 핎요. + +Ʞ능 감: 팹턮 윔드가 Text 레읎얎 Ꞁꌎ의 Ꞁ자 윀곜을 2ì°š 베지에 점 목록윌로 받을 Ꞟ읎 있윌멎 좋겠얎요. Ꞁꌎ을 읜을 수 없얎서 a, g, R, &, 한, Ꞁ 여섯 자륌 쀑심선곌 굵Ʞ로 손윌로 적었는데, 윀곜읎 듀얎였멎 같은 래슀터띌읎저가 Inter 나 사람읎 올늰 .ttf 의 아묎 Ꞁ자나 픜셀로 바꿔 볎여 쀘요. + +### Syllable Odometer + +Jamo Assembly 비교): 죌제 7 의 「숫자와 Ʞ혞 놀읎」륌 한Ꞁ로 핎서 Ꞁ자 갈래에 섞웠얎요. 63판 Jamo Assembly 가 한 음절의 윔드 값읎 였륎는 것을 볎여 쀬닀멎, 읎 팚턎은 ê·ž 셈 전첎륌 죌행 Ʞ록계로 돌렀요. 유니윔드 2.0(1996)은 현대 한Ꞁ 19 × 21 × 28 = 11,172 음절을 U+AC00 + (쎈성 × 21 + 쀑성) × 28 + 종성 자늬에 늘얎놓아서, 받칚읎 가장 빚늬 도는 바퀎읎고 몚음읎 귞닀음, 쎈성읎 가장 느며 바퀎예요. 1987년의 KS C 5601(지ꞈ KS X 1001)은 완성형윌로 2,350 음절에만 윔드륌 쀬얎요. 만듀 때 Node 의 TextDecoder("euc-kr")로 B0A1~C8FE, 25행 × 94칞을 풀얎 볎니 2,350 음절읎 겹치지 않고 유니윔드 ì°šë¡€ 귞대로였고 똠(U+B620)은 없었얎요. 쎈성별로는 ㅇ 읎 208 로 가장 많고 ㅃ 읎 73 윌로 가장 적얎요. 읎 집합은 한 Ꞁ자에 6비튞씩 1,862자 묞자엎로 윔드에 듀얎가요. 팹널 위 46쀄에는 64판 Wongoji 의 9×11 조합 Ꞁꌎ을 가로 5 LED, 섞로 4 LED 로 킀욎 큰 음절읎 서요. Ꞁꌎ의 쌜진 점마닀 둥귌 점을 찍고 읎웃끌늬 읎얎서 획읎 5 LED 굵Ʞ예요. 완성형에 든 음절은 혞박색윌로 ꜉ ì°šê³ , 빠진 음절은 볎띌 윀곜만 낚아요. ê·ž 아래 칎욎터 둘은 11,172 가욎데 몇짞읞지와 2,350 가욎데 몇짞읞지륌 굎렀 섞고, 빠진 음절에서는 였륞쪜 수가 멈춰 있얎요. 가욎데 28쀄에서는 자몚 바퀮 셋읎 원통에 감ꞎ 띠처럌 돌고, 아래 42쀄은 지ꞈ 쎈성의 21 몚음 × 28 받칚 지도예요. 첫 판은 지도에서 아직 지나지 않은 칞을 0.13 밝Ʞ의 점윌로만 찍얎서, 처음 8쎈에 색읎 든 칞읎 588칞 가욎데 80칞읎었고 아래 3분의 1 읎 빈 격자로 볎였얎요. 앞윌로 올 완성형 칞을 옅게 믞늬 깔았는데 0.15 밝Ʞ로는 흐렀서 0.24 로 올늬자, ㄱ 의 171칞 묎늬가 처음부터 서고 채움읎 0.247 에서 0.284 가 됐얎요. Skip 을 1 로 둔 첫 셈은 빠진 음절을 걎너뛎 ë’€ ê·ž 자늬에서 한 걞음을 ë‹€ 뚞묌러서 8쎈 240프레임 가욎데 141프레임읎 빠진 음절에 서 있었는데, 뚞묎는 시간을 지ꞈ 선 음절의 몫윌로 고치자 0 읎 됐얎요. 빠진 음절의 윀곜도 획읎 3 LED 음 때는 바둑판처럌 얜혔닀가 5 LED 로 굵히자 속 빈 Ꞁ자로 읜혔얎요. 여는 속도는 쎈당 6걞음읎띌 ㄱ 의 588 음절읎 94쎈, 11,172 음절읎 29.6분읎고, Skip 을 1 로 두멎 2,350 음절만 밟아 6.5분읎에요. 잰 값은 움직임 0.646, 쎘쎘핚 0.069, 가장자늬 0.360, 복잡도 0.186, 채움 0.297, 색 0.173, 몚양 바뀜 0.100 읎고, 녾뾌 반응은 0.06, 0.07, 0.06, 0.06 읎에요. 2,350 가욎데 받칚 없음읎 349, ㄎ 277, ㄹ 262, ㅇ 241 로 가장 많아서 지도마닀 읎 넷읎 밝은 섞로 쀄읎 되고, 겹받칚 칞은 거의 얎두워요. ㄿ 은 엎아홉 장을 ë‹€ 돌아도 읊 한 칞에서만 쌜지고, 받칚 없는 399 음절 가욎데서도 쌰륌 비롯한 50 음절읎 빠졌얎요. ㄞ 지도는 588칞 가욎데 86칞만 쌜젞요. ㅗ 쀄에서는 또, 똑, 똔, 똘 닀음 엎두 칞읎 속 빈 Ꞁ자로 지나가는데 똠읎 ê·ž 가욎데 하나읎고, 똥에서알 닀시 쌜젞요. + +Lab 에서 핮 볎Ʞ: Text 레읎얎에 「KS」륌 Silkscreen 윌로 칎욎터와 같은 5 LED 높읎(size 8 얞저늬)로 썚서 칎욎터 쀄 가욎데 빈 자늬(x 34, y 51)에 두고 색을 #ffa826 윌로 하멎, 였륞쪜 수가 완성형 2,350 을 섞는 칎욎터띌는 읎늄표가 돌요. 큰 Ꞁ자와 바퀮, 지도 사읎에 낹는 빈 자늬는 읎 5쀄뿐읎에요. + +Ʞ능 감: 팚턎끌늬 윔드륌 나눠 쓰는 Ꞟ읎요. 읎 팚턎은 64판 Wongoji 의 9×11 한Ꞁ 조합 Ꞁꌎ 175쀄을 귞대로 옮겚 싀얎서, 23.9 KB 가욎데 6.7 KB 가 Ꞁꌎ읎에요. Lab 읎 읎 Ꞁꌎ을 공용윌로 죌멎 Text 레읎얎도 한Ꞁ을 귞늎 수 있얎요. 지ꞈ Text 레읎얎의 사읎튞 Ꞁꌎ 넷(Silkscreen, Inter, JetBrains Mono, Newsreader)은 latin subset 만 싀얎서, 한Ꞁ은 System Ꞁꌎ읎나 직접 올늰 Ꞁꌎ 파음에 Ʞ대알 핎요. + +### Wilkinson's Polynomial + +죌제 5 와 1 로 윌킚슚 닀항식을 알고늬슘 갈래에 섞우고, Lava Lake 와 Moth Lamp, Leapfrogging Vortices 만 서 있던 복잡도 × 채움 4,1 칞을 겚눎얎요. 복잡도가 2.172 륌 넘윌멎서 채움읎 0.302 아래읞 칞읎에요. 제임슀 윌킚슚(1959)은 (x − 1)(x − 2)
(x − 20) 의 x¹⁹ 계수 −210 에서 2⁻²³ 만 빌도 ê·Œ 10~19 가 복소수 닀섯 쌍윌로 갈띌진닀고 적었고, 「The perfidious polynomial」(1984)에서 읎 닀항식을 만난 음을 되짚었얎요. 팚널은 싀수축을 섞로로 섞워 ê·Œ k 륌 123 − 6(k − 1) 번 쀄에 두고, 허수축은 한 닚위륌 9 LED 로 가로에 둬요. ê·Œ 슀묎 개는 룚읎슀 에륌늬히(1967)와 올늬버 애버슀(1973)의 동시 반복윌로 지난 프레임의 귌에서 닀시 풀얎요. p 와 p′ 는 전개한 계수가 아니띌 ê³± ꌎ로 셈핎서 ê·Œ 곁에서도 자늿수가 묎너지지 않아요. 읎 풀읎륌 2⁻²³ 에서 돌늬멎 윌킚슚의 표가 소수 아홉짞 자늬까지 귞대로 나였고, 가장 넓은 쌍은 16.730737466 ± 2.812624894i 예요. 섭동은 Speed 2 에서 로귞 눈ꞈ윌로 2⁻³⁵ 와 2⁻²¹ 사읎륌 12쎈에 한 번 였륎낎렀요. 셈핎 볎니 ê·Œ 16 곌 17 읎 1.42e-10 에서 가장 뚌저 만나고, 10 곌 11 은 1.91e-8 에서 만나요. 둘레의 성욎은 로널드 몚지얎(1986)의 의사귌 집합읎에요. 계수마닀 제 크Ʞ의 ÎŽ ë°° 안에서 흔듀 때 귌읎 갈 수 있는 곳은 |p(z)| ≀ ÎŽ Σ|a_j||z|^j 읞 z 띌서, 알갱읎마닀 ê·ž 비의 로귞륌 셈핎 ÎŽ 집합에서 256ÎŽ 집합까지 쌜요. ê·ž 안에 프레임마닀 계수륌 묎작위로 흔든 닀항식 여덟 개의 귌을 점윌로 찍얎요. 첫 판은 성욎을 24 아래 뚌지로만 두얎 복잡도 1.259, 채움 0.060 읎었얎요. ÎŽ 집합 안만 쌜자 1.822, 0.101 읎 됐고, 둘레 띠까지 쌜자 채움읎 0.47~0.53 윌로 넘쳀얎요. ÎŽ 집합은 귌읎 정수에 있을 때 팚널의 18.7% 읎닀가 2⁻²³ 에서 2.4% 로 쀄지만, 256ÎŽ 집합은 58.1% 와 58.0% 로 거의 같아요. 귞래서 알갱읎마닀 쌀지륌 뚌저 20~70% 로 뜑고, 쌠 알갱읎의 밝Ʞ는 25 위로 고륎게 뜑되 위끝을 바깥 띠의 65 에서 안쪜의 255 까지 올렞얎요. 귞러자 쌍읎 갈띌진 ꌭ대Ʞ 프레임도 채움 0.250, 복잡도 2.117 을 지쌰얎요. 256ÎŽ 밖의 알갱읎는 24 아래띌 팚널에서는 꺌젞 볎여요. 잰 값은 움직임 0.426, 쎘쎘핚 0.103, 가장자늬 0.371, 복잡도 2.203, 채움 0.283, 색 0.136, 몚양 바뀜 0.068 읎고, 녾뾌는 0.04, 0.09, 0.07, 0.09 예요. 새로 볞 것은 윌킚슚의 섭동읎 닀항식을 도늬얎 덜 예믌하게 만든닀는 거예요. ÎŽ = 2⁻⁎⁎·⁶ 에서 ê·Œ 16 의 구늄은 귌읎 정수에 있을 때 1.63 까지 퍌지는데, 2⁻²³ 에서는 0.01 읎에요. + +### Golomb Ruler + +죌제 5 「수학 · 추상」곌 죌제 4 륌 핚께 맡아, 128쀄을 귞대로 자의 눈ꞈ윌로 쓰는 귞늌을 수론 갈래에 섞웠얎요. 눈ꞈ 사읎 거늬가 몚두 닀륞 정수 집합은 시몬 시돈(Simon Sidon)읎 1932년 푞늬에 ꞉수에서 묌었고, W. C. Babcock 은 1953년 띌디였 채널의 혌신을 플하렀 같은 집합을 썌얎요. 지ꞈ은 솔로몬 곚롬(Solomon W. Golomb)의 읎늄을 붙여 Golomb 자띌고 불러요. 눈ꞈ 14개읞 최적 자는 {0, 4, 6, 20, 35, 52, 59, 77, 78, 86, 89, 99, 122, 127} 로 Ꞟ읎가 127 읎띌 팹널 128쀄에 ꌭ 맞고, 91개 거늬가 몚두 닀륎닀는 것을 윔드로 확읞했얎요. 반대펞은 람띌읎얞 위히만(Brian Wichmann)읎 1963년에 낾 성ꞎ 자 W(r, s) 예요. W(1,16) 은 눈ꞈ 23개로 Ꞟ읎 127읎고 1부터 127까지 몚든 Ꞟ읎륌 한 번 읎상 재요. 닀만 가장 성ꞎ 자는 아니얎서, W(3,4) 에 127 눈ꞈ 하나륌 더한 20개짜늬도 127까지 ë‹€ 재요. 팚널은 왌쪜 끝 섞로쀄을 자로 두고, 눈ꞈ 쌍 (a, b) 마닀 a 행곌 b 행에서 출발핎 였륞쪜윌로 부푞는 반 타원을 귞렀요. 부푌 폭읎 거늬에 비례핎서 거늬 127 의 혞가 52.5 LED 까지 나가고, 짧은 혾는 청록, ꞎ 혾는 장믞색읎에요. 캘늬퍌는 쎈당 16 Ꞟ읎씩 엎늬며 ê·ž Ꞟ읎의 혞륌 하얗게 쌜고, 였륞쪜 끝 Ꞟ읎 자의 d 행에 눈ꞈ을 새겚요. ê·ž Ꞟ읎륌 재는 쌍읎 n 개멎 눈ꞈ읎 2+n LED 읎고, 아묎도 재지 않는 Ꞟ읎는 붉게 한 번 깜박읞 ë’€ 빈칞윌로 낚아요. Ruler 녾뾌는 최적 Golomb 자와 Mian 곌 Chowla 의 탐욕 시돈 수엎(1944) 12개, 묎작위 14개, Wichmann 자 사읎륌 곚띌요. 묎작위 14개륌 만 번 뜑아 볎니 평균 66 Ꞟ읎륌 재고 20 Ꞟ읎륌 겹쳐 재며, Golomb 자는 한 번도 나였지 않았얎요. Shape 녾뾌는 혞륌 쎈타원윌로 바꿔 -1 에서 셰람론, 1 에서 도멎의 치수선 같은 꺟쇠가 돌요. 첫 판은 캘늬퍌가 쎈당 25.4 Ꞟ읎로 달늬고 번쩍임읎 0.35쎈 낚아서 아홉 Ꞟ읎쯀읎 한꺌번에 하얗게 ë–Žê³ , Ꞟ읎 하나에 혞 하나띌는 것읎 흰 띠에 묻혔얎요. 쎈당 16 Ꞟ읎와 0.2쎈로 쀄읎자 하얀 혞가 셋 안팎읎 됐얎요. Wichmann 자의 혞 253개는 Golomb 의 91개와 같은 밝Ʞ로 쌓읎자 분홍빛 흰 덩얎늬가 되얎서, 혞 하나의 밝Ʞ륌 (91/253)^0.75 = 0.46 배로 낮췄얎요. Shape 륌 돌늬멎 혞륌 ë‹€ 닀시 귞렀서 Wichmann 에서 한 프레임읎 10.4 ms 였는데, 짧은 혞부터 한 프레임에 몫만큌 귞늬게 바꿔 1.9 ms 아래로 낎렞얎요. 잰 값은 움직임 0.103, 쎘쎘핚 0.116, 가장자늬 0.469, 복잡도 1.823, 채움 0.573, 색 0.265, 몚양 바뀜 0.254, thirds 55/68/48 읎에요. 왌쪜 자와 였륞쪜 Ꞟ읎 자륌 빌고 재멎 thirds 가 49/63/44 읎고, 녾뾌는 0.11, 0.12, 0.16, 0.18 읎에요. 새로 볞 것은 최적 Golomb 자가 1부터 55까지는 한 Ꞟ읎도 빠뜚늬지 않는닀는 거예요. 첫 빈칞읎 56 읎띌 Ꞟ읎 자의 위 43% 는 ꜉ ì°¬ 막대읎고 ê·ž 아래만 빗삎읎 돌요. Wichmann 자의 Ꞟ읎 자에서는 7 에서 16, 14 에서 15 하는 식윌로 112 까지 하나씩 짧아지는 가시가 계닚처럌 낎렀가요. + +### Zermelo Navigation + +죌제 5 와 죌제 4 로 선·궀적 갈래에 섞웠얎요. 겚눈 수학은 에륞슀튞 첎륎멜로의 항핮 묞제(1931)예요. 묌에 대핮 늘 빠륎Ʞ 1로 녾 젓는 배가 묌삎 w 속에서 방향 u 로 가멎, 땅에 대한 빠륎Ʞ s 는 |s u − w| = 1 의 ê·Œ s = u·w + √(1 − |w|² + (u·w)²) 읎에요. |w| 가 1 을 넘윌멎 배는 w 둘레 원뿔 안의 방향윌로만 나아가요. 팚널은 LED 하나륌 마디 하나로 두고, |a|, |b| ≀ 3 읎고 서로소읞 걞음 (a, b) 32방향윌로 에잠허륎 데읎크슀튞띌의 알고늬슘(1959)을 돌렀 121번 쀄 항구에서 닿는 시간 T 륌 셈핎요. 방향 사읎 가장 넓은 틈읎 18.4도띌 잔잔한 묌에서 Ꞟ은 직선볎닀 많아알 1.31% êžžê³ , 팹널 평균은 0.66% 였얎요. 0.5쎈마닀 T 의 등고선 하나가 항구륌 떠나 잔잔한 묌에서 1쎈에 18 LED 씩 올띌가요. 여섯 곳윌로 가는 가장 빠륞 Ꞟ은 거꟞로 따띌 회색 선윌로 Ꞌ고, 앞선읎 ê·ž Ꞟ을 지나는 자늬에 흰 점을 찍얎요. 한 번도 닿지 못하는 LED 는 장믞색 빗ꞈ윌로 낚겚요. 처음 판은 소용돌읎 셋을 가장자늬에서 18 LED 읎상 듀얎옚 안쪜에 두었는데, Current 2.5 에서도 닿지 못하는 LED 가 하나도 없었얎요. 묌읎 생Ʞ지도 사띌지지도 않는 묌삎 안에서는 돌아가멎 얎디든 닿아서, 귞늌자는 묌읎 배볎닀 빠륎게 가장자늬 너뚞에서 듀얎였는 자늬에만 서요. 소용돌읎 쀑심을 옆 가장자늬에서 4 LED 안윌로 옮Ʞ자 귞늌자가 생게고, 섀정 45가지륌 2쎈마닀 섞얎 볞 귞늌자 1,662개가 몚두 가장자늬에 닿아 있었얎요. 닀음에는 항구가 갇혔얎요. Harbour −1 에서는 1~3쎈 동안 8,192 LED 가욎데 8,161 개가 닿지 못했얎요. 핎안 쪜 묌삎을 죜읎렀고 흐멄 핚수에 곱한 감쇠는 Current 2.5 에서 핎안을 따띌 2.3 배속의 띠륌 만듀얎, Shape 0.5 에서 7,749 LED 륌 닀시 가렞얎요. 감쇠륌 빌고 아래 소용돌읎륌 88번 쀄로 올렞얎요. 두 묌삎도 1 − Shape 와 Shape 의 몫윌로 섞자, 섀정 75가지륌 41쎈씩 돌렀도 가장 많읎 가렀진 때가 1,213 LED(14.8%)였얎요. 제튞도 처음에는 위에서 낎렀와 핎안 앞에서 양옆윌로 갈띌졌는데, 옆윌로 빠지는 묌읎 Current 2 에서 2.9 배속읎띌 Ꞟ읎 몚두 벜을 탔얎요. 핎안 가욎데에서 위로 나가는 제튞로 바꟞자 항구가 제튞 안에 서서 첎륎멜로의 원뿔읎 귞대로 볎여요. Shape 1, Current 2 에서 항구의 묌은 1.99 배속읎띌 원뿔의 반각읎 asin(1/1.99) = 30도예요. 앞선은 위로 싀렀 가는 눈묌방욞읎 되고, 항구 아래에는 V 자 귞늌자가 낚아요. 걞음 시간은 처음에 두 끝의 느늌을 평균핎 셈했는데, 한 끝읎띌도 ê·ž 방향윌로 저을 수 없윌멎 걞음읎 막혀서 격자륌 잘게 할수록 귞늌자가 쀄었얎요. 여는 자늬의 귞늌자가 LED 격자에서 756 LED, ë„€ ë°° 고욎 격자에서 472 LED 였얎요. 두 끝 묌삎의 평균윌로 걞음을 젓게 바꟞자 441 곌 444 가 됐고, 두 격자의 도착 시간은 가욎데값윌로 0.69% 달띌서 따로 고욎 격자륌 두지 않았얎요. 색을 T/160 윌로 맀ꞎ 판은 제튞에서 가장 늊게 닿는 곳도 T 가 66~89 띌 녞랑곌 쎈록뿐읎었얎요. 지ꞈ은 뚌 핎안까지 걞늬는 평균 시간윌로 나눠서 얎느 묌삎에서도 ꞈ색에서 파랑까지 가요. 앞선읎 1.5 LED 볎닀 쎘쎘한 곳을 지우던 판은 소용돌읎 뒀에 검은 구멍읎 났고, ê·ž 자늬륌 12% 빛윌로 메우자 채움읎 0.347 에서 0.589 로 올랐얎요. 잰 값은 움직임 2.452, 쎘쎘핚 0.135, 가장자늬 0.612, 복잡도 1.531, 채움 0.570, 색 0.277, 몚양 바뀜 0.059 읎고, `thirds` 는 66/64/39 예요. 녾뾌 반응은 0.14, 0.14, 0.15, 0.11 읎에요. Shape 0 에서 Current 2 는 팚널의 5.5% 륌 귞늌자로 낚Ʞ고, 2.5 는 11.5% 륌 낚겚요. 읎 팚널에서 새로 볞 것은 묞턱읎에요. Current 가 ꌭ 1 음 때는 몚양곌 항구 자늬륌 바꟞며 41쎈륌 돌렀도 닿지 못하는 LED 가 0 읎고, 1.05 로 올늬멎 4 개가 생겚요. Shape 0 의 귞늌자는 항구가 8번 엎에 서든 56번 엎에 서든 같은 448 LED 였얎요. 귞늌자는 항구가 아니띌 묌삎곌 팹널 가장자늬에 딾며 것읎에요. + +### Hodograph + +죌제 5 「수학 · 추상」윌로, 묌늬 갈래에 속도 공간을 섞웠얎요. 윌늬엄 로얞 핮밀턮(William Rowan Hamilton)은 1846년 아음랜드 왕늜 아칎데믞에 혞도귞래프륌 냈얎요. 움직읎는 묌첎의 속도륌 한 원점에서 화삎로 귞늬멎 화삎 끝읎 곡선을 Ꞌ는데, 역제곱 힘 아래에서는 궀도의 읎심률읎 얌마든 ê·ž 곡선읎 원읎에요. 늬처드 파읞뚌은 1964년 칌텍 강의에서 읎 Ꞟ을 거꟞로 걞었얎요. 핎에서 볞 각도가 같은 만큌 돌 때마닀 속도가 같은 크Ʞ로 바뀌니 속도 끝은 원을 돌고, ê·ž 원에서 타원읎 나와요. 데읎비드와 죌디슀 굿슀타읞읎 1996년에 ê·ž 강의륌 책윌로 엮었얎요. 팹널 위 절반에서는 핮 하나륌 쎈점윌로 나눠 가진 타원 궀도륌 천첎 셋읎 돌아요. 위치는 쌀플러 방정식(1609)을 뉎턎법윌로 풀얎 구하니 제 시간에 맞아요. 천첎마닀 지난 0.3 죌Ʞ에 쓎 부채륌 같은 시간 섞 조각윌로 나눠 밝고 얎둡게 번갈아 칠핎서, 핮 가까읎의 ì§§ê³  넓은 조각곌 멀늬의 êžžê³  가는 조각읎 같은 넓읎로 볎여요. 아래 절반은 속도 공간읎에요. 흰 원점에서 천첎마닀 같은 색 속도 화삎을 Ꞌ고, 화삎 끝읎 도는 원곌 ê·ž 쀑심을 귞렀요. 같은 시간 찜의 부채는 원점에서 원까지 칠핎요. 위의 천첎에도 같은 화삎을 0.55 배로 붙읎고, 천첎와 화삎 끝 사읎륌 흐늰 싀로 읎얎 두 귞늌을 묶었얎요. 궀도와 원에는 핎에서 30도 간격읞 자늬에 밝은 점을 찍었얎요. 궀도에서는 간격읎 듀쭉날쭉하던 점읎 원에서는 고륎게 서서 파읞뚌의 귞늌읎 돌요. 원읎띌는 것은 윔드로 확읞했얎요. 쌀플러 방정식윌로 구한 속도 720개에 최소제곱 원(KÃ¥sa, 1976)을 맞추멎, 읎심률 0 부터 0.95 까지 슀묎 겜우 몚두 원에서 벗얎난 거늬가 반지늄의 3.3×10⁻¹⁵ 읎하읎고, 원의 쀑심곌 원점 사읎는 반지늄의 e 배와 6.7×10⁻¹⁶ 안에서 같아요. 쌀플러 방정식 없읎 뉎턎의 힘을 RK4 로 적분한 속도로 핮도 벗얎낚은 반지늄의 5.6×10⁻¹⁎ 읎하였얎요. 첫 판은 핎와 원점을 절반마닀 가욎데에 박고 가장 뚌 점을 29.5 LED 에 맞췄는데, 두 귞늌읎 절반볎닀 작게 서서 50~68행읎 비었얎요. 채움은 0.231, 가욎데 3분의 1 은 12% 였얎요. ì…‹ì§ž 천첎는 죌황읎띌 반 밝Ʞ 부채가 갈색윌로 읜혔고, 원지점을 18~29.5 로 둔 탓에 죌Ʞ가 Speed 1 에서 3.9, 4.7, 5.3 쎈로 비슷핎 천첎듀읎 거의 같읎 돌았얎요. 절반마닀 귞늌을 제 상자에 맞추고 궀도 묶음을 가장 크게 서는 Ʞ욞Ʞ로 돌늬자 채움읎 0.322 가 됐얎요. 색은 청록곌 자홍곌 쎈록윌로 바꿚고, 원지점을 13~29.5 로 넓혀 죌Ʞ륌 3.2 쎈에서 7.1 쎈까지 벌렞얎요. 잰 값은 움직임 0.579, 쎘쎘핚 0.048, 가장자늬 0.437, 복잡도 0.971, 채움 0.322, 색 0.147, 몚양 바뀜 0.102 읎고, 녾뾌는 0.06, 0.08, 0.09, 0.12 예요. 새로 볞 것은 원점에서 볞 속도 화삎읎 원지점에서 크게 돈닀는 거예요. 읎심률 0.95 에서 원지점 앞뒀 0.3 죌Ʞ 동안 천첎는 핎륌 9도만 도는데, 짧아진 화삎은 원점 바로 옆을 슀치며 120도륌 돌아요. 여는 화멎의 섞 원은 궀도 크Ʞ가 달띌도 반지늄읎 22.7, 20.5, 21.3 LED 로 거의 같아요. 반지늄 ÎŒ/h 는 반통겜 p 만윌로 정핎지는데, 섞 궀도의 p 가 10.4, 12.75, 11.8 로 가깝Ʞ 때묞읎에요. + +### Shapley-Folkman + +죌제 5 「수학 · 추상」윌로, 볌록하지 않은 몚양을 여러 번 더핮 평균하멎 볌록핎진닀는 것을 Ʞ하 갈래에 섞웠얎요. 섀플늬-포크뚌 볎조정늬는 로읎드 섀플늬와 졎 포크뚌읎 슝명했고, 로슀 슀타가 1969년 『Econometrica』 37권 1혞에 낾 「Quasi-equilibria in markets with non-convex preferences」의 부록에 처음 싀렞얎요. 평멎에서 n 개 집합의 합의 볌록 껍질에 든 점은 집합마닀 점 하나씩 n 개의 합윌로 쓞 수 있고, ê·ž 가욎데 집합 밖 껍질에서 빌렀 였는 점은 많아알 둘읎에요. 귞래서 몚양 S 륌 n 번 더핮 n 윌로 나눈 평균 A(n) 은 껍질의 얎느 점에서도 2D/n 안에 와요. D 는 n=1 음 때 껍질읎 S 에서 가장 멀늬 ë–šì–Žì§„ 거늬예요. 팚널은 위에서부터 32쀄짜늬 띠 넷에 A(1), A(2), A(4), A(8) 을 같은 크Ʞ로 귞렀요. 몚양은 59×28 격자에서 LED 하나가 격자점 하나읎고, 합은 격자점 집합을 비튞맵윌로 정확히 더핎요. 8 번 더하멎 465×217 격자가 돌요. S 륌 A 의 몚든 점윌로 옮겚 OR 하는 대신 A 의 가장자늬 점윌로만 옮Ʞ고 S 의 조각마닀 A 륌 한 벌 더하는데, 몚든 점윌로 옮겚 OR 한 결곌와 녾뾌 자늬 9 곳에서 40 프레임씩 2,520 번 맞춰 뎐서 한 비튞도 닀륎지 않았얎요. LED 밝Ʞ는 ê·ž 칞에 든 A(n) 격자점의 몫읎에요. 한 점에 닿는 Ꞟ의 수로 밝히멎 쀑심 극한 정늬의 종 몚양읎 되니, 귞걎 읎 볎조정늬와 닀륞 읎알Ʞ띌서 쓰지 않았얎요. 첫 판은 띠마닀 한 색읎고 읎번 띠에서 새로 ì°¬ 곳만 옅게 칠핎서 띠 넷읎 따로 녾는 귞늌 넷윌로 읜혔고, 한 프레임읎 2.9 ms 였얎요. A(1)⊂A(2)⊂A(4)⊂A(8) 로 포개지니 점마닀 처음 나타난 띠의 색을 죌자, ë§š 아래 띠가 청록 달곌 하늘색, 낚색, 볎띌 테륌 나읎테처럌 ë‹€ 품게 됐얎요. S 의 행을 0~31 비튞 옮ꞎ ꌎ로 믞늬 두고 껍질 변을 믞늬 셈핎서 2.1 ms 가 됐얎요. Hull 녞람의 ꞈ빛 메움은 0.5 에서 갈색윌로 볎여서 빗ꞈ윌로 바꿚얎요. 달을 위상 12 곳에서 재멎 껍질 가욎데 덮읎지 않은 몫읎 n=1, 2, 4, 8 에서 42.6, 19.6, 8.6, 3.7% 읎고, 하우슀도륎프 거늬는 11.7, 5.7, 2.8, 1.37 LED 예요. n 을 곱하멎 11.7, 11.5, 11.4, 10.9 로 거의 귞대로띌 거늬는 1/n 윌로 쀄얎요. 넓읎는 n 을 곱한 값읎 0.43 에서 0.30 윌로 낎렀가 1/n 볎닀 조ꞈ 빚늬 쀄얎요. 별은 n=1 부터 16 까지 n 을 곱한 거늬가 5.78 로 똑같아요. 몚양 닀섯을 위상 12 곳에서 n=1 부터 16 까지 잰 960 겜우가 몚두 2D/n 안에 듀었얎요. 잰 값은 움직임 0.214, 쎘쎘핚 0.051, 가장자늬 0.502, 복잡도 0.359, 채움 0.429, 색 0.347, 몚양 바뀜 0.163 읎고, 녾뾌는 0.18, 0.15, 0.20, 0.07 읎에요. 새로 볞 것은 1/n 읎 흐렀지는 몚습읎 아니띌 홈의 수로 볎읞닀는 거예요. 달의 현을 따띌 A(n) 의 빈 자늬가 n 개의 홈윌로 갈띌지고(위상 여섯 ê³³ 가욎데 넷에서 1, 2, 4, 8 개 귞대로, 나뚞지 둘에서는 하나가 더 잡혔얎요), 가장 깊은 홈읎 1/n 로 ì–•ì•„ì žìš”. + ## 지도 지도의 자늬는 손윌로 맀ꞎ 점수가 아니띌 잰 값읎에요. 팚턎마닀 처음 녾뾌 값윌로 Lab 엔진에서 8쎈륌 돌늬고, 2쎈부터 6쎈 동안의 화멎을 재요. LED 하나의 밝Ʞ는 섞 채널 가욎데 가장 밝은 값윌로 뎐요. @@ -3466,7 +3545,7 @@ ESP32-S3 로 옮Ꞟ 때 걞늎 것도 있얎요. Reaction Diffusion 은 Node 에 ## 닀음 판 감 -10월 25음 `dev`(`f72355d`)의 328개에 43판부터 64판까지의 읎백슀묎 개륌 더한 548개에서 잰 값읎에요. 팹턮 바퀮는 죌제 안에서 묎엇을 할지 고륌 때 읎 절을 읜고, 한 것은 빌고 새로 볞 것을 더핎요. +10월 25음 `dev`(`f72355d`)의 328개에 43판부터 64판까지의 읎백슀묎 개와 66판의 ì—Ž 개륌 더한 558개에서 잰 값읎에요. 팹턮 바퀮는 죌제 안에서 묎엇을 할지 고륌 때 읎 절을 읜고, 한 것은 빌고 새로 볞 것을 더핎요. ### 지도의 빈 칞 @@ -3552,6 +3631,8 @@ ESP32-S3 로 옮Ꞟ 때 걞늎 것도 있얎요. Reaction Diffusion 은 Node 에 64판은 62판까지 든 528개에 빈 칞도 둘뿐읞 칞도 없얎서, 셋뿐읞 칞 엎음곱 가욎데 움직임 × 몚양 바뀜 1,3 곌 채움 × 색 1,4 륌 겚눎얎요. 같읎 돈 63판(#326)볎닀 뚌저 PR 볞묞에 적얎서 두 바퀎가 같은 칞을 겚누지 않았얎요. 움직임 × 몚양 바뀜 1,3 은 1쎈에 바뀌는 양읎 0.061 아래읎멎서 몚양 바뀜읎 0.211~0.328 읞 칞읎에요. 몚양 바뀜은 2~8쎈의 랔록 평균읎 첫 화멎에서 멀얎진 정도띌 ê·ž 여섯 쎈 동안 바뀐 양을 넘지 못하고, 귞래서 읎 칞은 몚양 바뀜읎 움직임의 3.5배륌 넘얎알 서요. 반짝임은 움직임만 볎태서, Polynomial Amoeba 는 등고선 반짝임을 빌고 아메바가 한 방향윌로 쀄얎드는 구간을 ê·ž 찜에 맞춰 움직임 0.053, 몚양 바뀜 0.265 예요. 채움 × 색 1,4 는 24/255 넘게 쌜진 LED 가 0.302 아래읎멎서 색읎 0.545 륌 넘는 칞읎에요. 읎옚 30개만 귞늬멎 채움읎 축의 아래 끝 0.117 에도 못 믞쳐서, Kibble-Zurek Ions 는 사슬 양옆윌로 지귞재귞의 역사륌 갈비처럌 밀얎낎고 두 방향을 자홍곌 쎈록윌로 칠핎 채움 0.281, 색 0.562 예요. 같은 팚턎읎 쎘쎘핚 0.203 윌로 겚누지 않은 쎘쎘핚 × 채움 3,1 에도 ì„°ì–Žìš”. ì—Ž 개 ë‹€ 얎느 축의 끝도 넘지 않아서 칞 겜계는 60판 귞대로예요. 64판까지 든 538개에서 지도 21장은 ë‹€ `none` 읎고, 셋뿐읞 칞은 엎음곱에서 엎넷윌로 쀄었얎요. 겚눈 두 칞은 둘 ë‹€ 넷읎 됐얎요. +66판은 64판까지 든 548개에 빈 칞도 둘뿐읞 칞도 없얎서, 셋뿐읞 칞 ì—Žì…‹ 가욎데 같읎 돈 65판(#328)읎 고륞 쎘쎘핚 × 색 4,4 와 색 × 몚양 바뀜 4,4 륌 빌고 쎘쎘핚 × 몚양 바뀜 4,4 와 복잡도 × 채움 4,1 을 겚눎얎요. 쎘쎘핚 × 몚양 바뀜 4,4 는 읎웃 찚읎 평균읎 0.297 을 넘윌멎서 몚양 바뀜읎 0.328 을 넘는 칞읎에요. 고욎 결은 16×8 랔록 안에서 평균되얎 지워지니, 결은 둔 채로 랔록 평균읎 통짞로 옮겚 가알 핎요. Barrier Grid 는 낱말곌 음화륌 한 칞씩 엇갈렀 ì§  읞쇄 위로 막대 격자가 믞끄러젞, ꞈ빛 Ꞁ자가 파란 바탕의 검은 Ꞁ자로 뒀집혀요. 첫 판은 쎘쎘핚 0.250, 몚양 바뀜 0.228 읎었고, 막대에 바둑판 구멍을 낎고 「빛」의 섞로획을 4 LED 의 배수에 섞우자 0.316 곌 0.381 읎 됐얎요. 복잡도 × 채움 4,1 은 복잡도가 2.172 륌 넘윌멎서 채움읎 0.302 아래읞 칞읎에요. Wilkinson's Polynomial 은 의사귌 집합 안의 알갱읎마닀 쌀지와 밝Ʞ륌 따로 뜑아 deflate 가 쀄읎지 못하게 하고, 집합 밖 알갱읎는 24 아래에 두얎 복잡도 2.203, 채움 0.283 읎에요. 성욎을 24 아래 뚌지로만 둔 첫 판은 1.259 와 0.060 읎었얎요. ì—Ž 개 ë‹€ 음곱 축의 끝 안에 있얎서 칞 겜계는 60판 귞대로예요. 66판까지 든 558개에서 지도 21장은 ë‹€ `none` 읎고, 셋뿐읞 칞은 엎셋에서 엎하나로 쀄었얎요. 겚눈 두 칞은 둘 ë‹€ 넷읎 됐얎요. + ### 아직 없는 갈래 `families.json` 의 쉰여섯 갈래에 없는 것 가욎데 섞욎 팚널에 맞을 것을 적얎 둬요. 29판읎 연 위상·맀듭 갈래는 Hopf Fibration 곌 31판의 Boy's Surface, 33판의 Borromean Rings, 34판의 Lakes of Wada, 36판의 Heawood Torus, 38판의 Sperner's Lemma, 39판의 Borsuk-Ulam, 44판의 Ham Sandwich, 46판의 Reeb Graph, 48판의 Möbius Energy, 49판의 Square Peg, 51판의 Persistence Barcode, 53판의 Belt Trick, 55판의 Universal Cover 예요. 28판읎 엎렀닀 접은 갈래띌 28판의 Hairy Ball 은 녞읎슈·흐늄에 귞대로 있얎요. 섞잎 맀듭은 48판의 Möbius Energy 가 처음 섞웠얎요. 29판은 화장품 팝업 갈래도 엎었얎요. 죌제 6 의 몫읎띌 죌제 2 로 섞지 않았고, Cream Swirl, Serum Gel, Glitter Fall, Loose Powder 넷읎 ë‹€ 듀얎 있얎요. 30판의 Swatch Row, Pearl Sheen, Bi-Phase, Satin Fold 가 ê·ž 갈래에 넷을 더했고, 31판의 Lip Gloss, Foam Lather, Setting Mist, Sponge Blend 가 넷을 더 더핮 엎둘읎에요. 32판의 Cushion Mesh, Clay Mask, Sugar Scrub, Chrome Powder 가 넷을 더핮 엎여섯읎고, 33판의 Cleansing Balm, Gold Leaf, Essence Drop, Peel-Off 가 넷을 더핮 슀묌읎에요. 34판의 Cat Eye, Velvet Nap, Mood Tint, Hair Gloss 가 넷을 더핮 슀묌넷읎고, 35판의 Bath Fizz, Toner Pad, Sheet Mask, Gilded Cream 읎 넷을 더핮 슀묌여덟읎고, 36판의 Lotion Ribbon, Shimmer Oil, Blooming Gel, Whipped Soap 읎 넷을 더핮 서륞둘읎에요. 38판의 Milk Ring, Couette Shimmer, Photochromic, Serum Fingers 가 넷을 더핮 서륞여섯읎에요. 39판의 Soap Film, Blotting Paper, Serum Drip, Crackle Polish 가 넷을 더핮, 37판의 넷까지 마흔넷읎에요. 41판은 할로윈 갈래륌 엎었얎요. 죌제 6 의 몫읎띌 죌제 2 로 섞지 않았고, Jack-o'-Lantern, Sheet Ghosts, Spider Drop, Cauldron, Eyes in the Dark 닀섯읎 듀얎 있얎요. 28판읎 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table, 33판의 Goldbach Comet, 35판의 Kolakoski, 36판의 Elliptic Chords, 39판의 Explicit Formula, 44판의 Bulgarian Solitaire, 48판의 Chebyshev's Bias, 49판의 Curlicue, 51판의 Sato-Tate, 53판의 Gaussian Periods, 55판의 Question Mark Function 읎에요. 27판읎 연 철도 갈래는 Funicular 하나읎고, 필늄·영화 갈래는 Horse in Motion 하나예요. 두 갈래는 죌제 표가 수학 쪜윌로 바뀌Ʞ 전에 엎었얎요. ê·ž 뒀로 ROUND.md 는 사묌로 묶는 갈래륌 더 ì—Žì§€ 않아요. 26판읎 연 얌음·극지 갈래는 Iceberg 와 27판의 Sled Dogs 읎고, 믞생묌 갈래는 Dark Field 와 27판의 Mitosis 예요. 25판읎 연 농사·밭 갈래는 Rice Terraces 와 26판의 Rice Planting, 27판의 Center Pivot 읎고, 화산 갈래는 Eruption 곌 26판의 Geyser, 27판의 Lava Lake 예요. 24판읎 연 공사·걎섀 갈래는 Tower Crane 곌 25판의 Excavation, 26판의 Implosion 읎에요. 24판읎 연 도자Ʞ 갈래는 Potter's Wheel 곌 25판의 Kintsugi, 26판의 Noborigama 예요. 23판읎 연 묎대·공연 갈래는 Proscenium 곌 24판의 Flying Trapeze, 25판의 Marionette, 26판의 Shadow Play 예요. 23판읎 연 짐승 갈래는 Giraffe 와 24판의 Emergence, 25판의 Snow Monkeys 예요. 22판읎 연 몞 갈래는 Circulation 곌 23판의 Axon, 24판의 Breath, 27판의 Fluoroscope 예요. 22판읎 연 비행 갈래는 Liftoff 와 23판의 Final Approach, 24판의 Thermal, 25판의 Skydiver 예요. 20판읎 연 새 갈래는 Kingfisher 와 21판의 Woodpecker, 22판의 Weaver, 23판의 Skein 읎에요. 20판읎 연 화학·싀험 갈래는 Chromatography 와 21판의 Chemical Garden, 22판의 Flame Test, 23판의 Elephant Toothpaste 예요. 21판읎 연 종읎 갈래는 Pop-up Book 곌 22판의 Senbazuru, 23판의 Hanging Scroll, 26판의 Origami 예요. 21판읎 연 욎동·겜Ʞ 갈래는 Lap Pool 곌 22판의 Bowling Lane, 23판의 High Dive, 26판의 Pole Vault, 27판의 Climbing Wall 읎에요. 위아래로 움직읎는 겜Ʞ는 High Dive 와 Pole Vault 뒀로 27판에 싀낎 암벜읎 ì„°ì–Žìš”. 19판읎 연 부엌 갈래는 Popcorn 곌 20판의 Kettle, 21판의 Pancakes 읎고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판읎 연 유늬 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori, 27판의 Glassblower 예요. ꎑ묌·결정 갈래는 Bismuth 와 19판의 Dripstone, 20판의 Agate 읎고, 놀읎공원 갈래는 Drop Tower 와 19판의 Helter Skelter, 20판의 High Striker, 25판의 Ferris Wheel 읎에요. 말읎 였륎낎늬는 회전목마는 아직 없얎요. 17판읎 연 전자 회로 갈래는 Circuit Board 와 18판의 Server Rack, 21판의 Nixie 읎고, 장난감 갈래는 Marble Run 곌 18판의 Peg Light, 21판의 Slinky, 22판의 Pin Art 예요. 16판읎 연 바닀 갈래는 Abyss 와 17판의 Shoreline, 20판의 Sunglint, 23판의 Bait Ball 읎고, 곀충 갈래는 Fireflies 와 17판의 Ant Farm, 20판의 Moth Lamp, 24판의 Waggle Dance 예요. 15판읎 연 도시 갈래는 Metro Map 곌 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판읎 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables, 24판의 Pipe Organ 읎에요. 4판읎 연 직묌 갈래는 Jacquard Loom 곌 15판의 Plait, 19판의 Needlepoint, 23판의 Sequins 읎고, 8판읎 연 Ʞ계 갈래는 Gear Train 곌 15판의 Cam Wave, 19판의 Paternoster 예요. 12판읎 연 천첎 갈래는 Star Trails 와 15판의 Analemma, 16판의 Lunation, 22판의 Galilean Moons 예요. 위성의 궀도는 Galilean Moons 가 ê·žë žê³ , 핎륌 도는 행성은 아직 없얎요. 늬듬·펄슀 갈래는 1판의 Euclid Pulse 와 Pulse Bars 뒀로 20판의 Change Ringing, 32판의 Arnold Tongues, 35판의 Chimera State, 38판의 Infinity Series, 41판의 Siteswap, 44판의 Kac Ring, 46판의 Stochastic Resonance 가 ì„°ì–Žìš”. @@ -3691,6 +3772,8 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. 64판에서는 Wongoji 와 Indian Rope Trick 읎 128쀄을 썚요. Wongoji 는 12 LED 칞 ì—Ž 개가 섞로로 서는 원고지띌 격자만윌로도 섞 3분의 1 읎 ë‹€ 쌜젞서, 격자륌 끄고 한 번 더 쟀얎요. Ꞁ씚만윌로 6/9/6 읎고 격자까지 23/24/23 읎에요. 시의 행읎 대개 ì—Ž 칞볎닀 짧아서 ë‹€ 쓎 쪜도 아래 3분의 1 읎 비Ʞ 쉬워, 「윀동죌」와 「하늘을」읎 아래에 드는 첫 장에서 ì—Žì–Žìš”. Indian Rope Trick 은 사슬 킀가 116 LED 띌 서 있는 동안 thirds 가 16/12/15 읎고, Ʞ볞 Shake 2 가 섞 마디의 묞턱 1.25~1.3 위띌서 2~8쎈 낮낮 서 있얎요. 나뚞지 여덟 가욎데 Letter Sand 는 낱말읎 묎너지는 2~8쎈에 9/7/42 로 아래가 묎겁고, 나뚞지 음곱은 섞 3분의 1 읎 ë‹€ 13% 읎상 쌜젞요. +66판에서는 Golomb Ruler 와 Zermelo Navigation 읎 128쀄을 썚요. Golomb Ruler 는 눈ꞈ 14개읞 최적 자의 Ꞟ읎가 127 읎띌 눈ꞈ 0 읎 ë§š 위 쀄에, 127 읎 ë§š 아래 쀄에 서고, 91개 혞가 ê·ž 사읎륌 덮얎 thirds 가 55/68/48 읎에요. 왌쪜 자와 였륞쪜 Ꞟ읎 자륌 빌고 재도 49/63/44 예요. Zermelo Navigation 은 121번 쀄 항구에서 0.5쎈마닀 떠난 앞선읎 1쎈에 18 LED 씩 올띌가 팹널 끝에 닿아서 66/64/39 예요. Etaoin Shrdlu 도 위에 Ꞁ쀄읎, 아래에 막대가 서서 26/17/49 읎고, 늘 쌜 둔 띌읎녞타읎프 쀄을 빌멎 27/17/44 예요. 가욎데 3분의 1 은 떚얎지는 Ꞁ자만 지나가서 가장 비얎요. 나뚞지 가욎데 Wilkinson's Polynomial 은 ê·Œ 1~7 읎 거의 움직읎지 않는 점윌로 낚아 아래 3분의 1 읎 4 예요. + ### 낯선 수학 죌제 5 로 쓎 수학곌 닀음에 핮 볌 수학읎에요. 28판읎 처음 ì—Ž 개륌 썌얎요. 슈람-뢰람너 진화(Loewner Trace), 포드 원(Ford Circles), 턾 난 ê³µ 정늬(Hairy Ball), 아슈텍 닀읎아몬드와 북극권(Aztec Diamond), 부니몚비치 겜Ʞ장(Stadium Billiard), 띌우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 닀멎첎와 E8(Gosset Polytope), 윘웚읎의 볎고 말하Ʞ 수엎(Look and Say), 위토프 게임의 귞런디 값(Wythoff Nim), 쌍곡 평멎의 띠 몚형(Hyperbolic Band)읎에요. 29판은 여섯을 더했얎요. 큎띌읞의 j 불변량(J-Invariant), 혾프 올뭉치(Hopf Fibration), 닀읎슚 람띌욎 욎동(Dyson Gas), 가쌀알 바늘 묞제의 페론 나묎(Perron Tree), 임계선 위의 늬만 제타 핚수(Zeta Loops), Ꞟ버튞-섀넌-늬슈 늬플곌 베읎얎-디아윔니슀의 음곱 번(Seven Shuffles)읎에요. 30판도 여섯을 더했얎요. 섞 간격 정늬(Three Gaps), 혞프슀태터 나비와 TKNN 정수(Hofstadter Butterfly), 녞읎만의 왞부 당구(Outer Billiards), 볌드의 탄도 퇎적곌 KPZ 앞멎(Ballistic Deposition), 아륎놀튞의 믞앀더(Meander), 람로드벀튞와 핎뚞슬늬의 슀믞Ʞ(Percolation)예요. 31판도 여섯을 더했얎요. 마슀킷 조각의 큎띌읞 êµ° 극한 집합(Indra's Pearls), 늬틀우드 닀항식의 ê·Œ(Littlewood Roots), 고든의 가우슀 핎자(Gaussian Moat), 아륎놀튞의 고양읎 사상(Cat Map), 자부슀킀와 크룚슀칌의 KdV 솔늬톀(KdV Solitons), 볎읎 곡멎(Boy's Surface)읎에요. ROUND.md 의 「낯선 수학 예」 여덟 가욎데 쓎 것은 슈람-뢰람너 진화와 아슈텍 닀읎아몬드, 몚듈러 형식, 혾프 올뭉치, 섞 간격 정늬, 왞부 당구 여섯읎었고, 31판읎 늬틀우드 닀항식의 귌곌 큎띌읞 군의 극한 집합을 썚서 여덟을 ë‹€ 썌얎요. 32판도 여섯을 더했얎요. 타벗 횚곌와 양자 칎펫(Talbot Carpet), Ꞟ람레슀 추잡의 표(Gilbreath Table), 원 사상의 아륎놀튞 혀(Arnold Tongues), 윘웚읎와 띌딘의 핀휠 타음링(Pinwheel Tiling), 윌킚슚곌 윌렘섌의 칚입 슀믞Ʞ(Invasion Percolation), 욞람의 묞제와 핎뚞슬늬 곌정(Hammersley Lines)읎에요. 33판도 여섯을 더했얎요. 에읞절, 홀로읎드, 로믹, 비띌귞의 묎작위 정렬 넀튞워크(Sorting Network), 베륎튞랑의 ì—­ì„€(Bertrand Paradox), 펜로슈의 비출 수 없는 ë°©(Penrose Room), 당듀랭 구(Dandelin Spheres), 테읎튞의 볎로메였 고늬(Borromean Rings), 곚드바흐 혜성(Goldbach Comet)읎에요. 34판도 여섯을 더했얎요. 톰슚 묞제와 였음러가 낚Ʞ는 닀섯 읎웃 엎둘(Thomson Problem), 쌈빈의 ë°° 묌결(Kelvin Wake), 요넀알마가 낾 와닀의 혞수(Lakes of Wada), 샀읎데거의 강 귞묌(Scheidegger Rivers), 몚저의 소파 묞제와 핎뚞슬늬의 소파(Moving Sofa), 크누슀의 춀추는 링크로 푌 펜토믞녞 덮Ʞ(Pentomino Cover)예요. 35판도 여섯을 더했얎요. 뢞로의 폭읎 음정한 곡선(Constant Width), 렌니의 죌찚 묞제(Rényi Parking), 칎늬의 비죌Ʞ 왕 타음(Kari-Culik Tiles), 윜띌윔슀킀 수엎(Kolakoski), 쿠띌몚토와 바톡토흐의 킀메띌 상태(Chimera State), 죌윥슀킀 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했얎요. 귞레읎슚의 정늬가 말하는 곡선 닚축 흐멄(Curve Shortening), 헀우드의 음곱 색 토러슀 지도(Heawood Torus), 슈바륎잠 랜턮(Schwarz Lantern), 윌람러햄곌 깁슀의 현상(Gibbs Phenomenon), 타원 곡선의 현곌 접선(Elliptic Chords), 지뚌의 파국 Ʞ계(Catastrophe Machine)예요. 37판도 여섯을 더했얎요. 포천의 쓞Ʞ 선 알고늬슘(Fortune's Sweep), 앙투안의 목걞읎(Antoine's Necklace), 칎륎만 소용돌읎 Ꞟ의 안정성(Kármán Street), 당당한 원판의 헥사틱 상(Hexatic Phase), 졎슚-멜 쪜맀와 아람띌믞 법칙(Johnson-Mehl), 쿠띌몚토-시바신슀킀 방정식(Kuramoto-Sivashinsky)읎에요. 38판도 여섯을 더했얎요. 앀더슚 국소화(Anderson Localization), 묎얎가 ì°Ÿê³  Chenciner 와 Montgomery 가 슝명한 섞 몞 묞제의 8자 핮(Figure-Eight Orbit), 예섌곌 빈튞너, 에륎되시의 베륎누읎 합성곱(Bernoulli Convolutions), 람띌우얎륎의 고정점 정늬륌 슝명하는 슈페륎너 볎조정늬(Sperner's Lemma), 에겐베륎거와 폎늬아의 항아늬(Pólya Urn), 뇌륎고륎의 묎한 수엎(Infinity Series)읎에요. 39판도 여섯을 더했얎요. 고든, 웹, 월퍌튞의 등슀펙튞럌 북(Isospectral Drums), 폰 망곚튞의 ꌎ로 쓎 늬만의 명시 공식(Explicit Formula), 요한 베륎누읎의 최닚 강하선(Brachistochrone), 슈타읎너의 포늬슘(Steiner Chain), 몚런 집닚윌로 귞늰 킹뚌의 합류(Kingman Coalescent), 볎륎수크-욞람 정늬(Borsuk-Ulam)예요. 40판도 여섯을 더했얎요. 앚더슀의 람띌욎 연속 랜덀 나묎(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 핚수(Hydrogen Orbital), 저도비치 귌사의 팬쌀읎크(Zeldovich Pancakes), 망덞람로가 낎닀볎고 례러, 슈람, 베륎너가 슝명한 람띌욎 욎동의 바깥 겜계(Brownian Frontier), 복소 ꞎ슈부륎크-란닀우 방정식의 결핚 난류(Defect Turbulence), 룚퍌튞 왕자의 정육멎첎(Prince Rupert's Cube)예요. 41판은 닀섯을 더했얎요. 홀곌 왜걎의 현수선 Ꞟ곌 정닀각형 바퀮(Square Wheels), 퐁슬레의 포늬슘(Poncelet Porism), 슐레플늬의 4찚원 정닀포첎륌 자륞 닚멎(Polytope Slices), 큎늌쌁곌 티뚌, 마귞누손의 사읎튞슀왑(Siteswap), Dudeney 의 잡화상 퍌슐곌 월늬슀-볎여읎-게륎비엔 정늬(Hinged Dissection)예요. 42판은 닀섯을 더했얎요. 바륞슀도륎프 규칙윌로 도는 Ʞ사 여행(Knight's Tour), 페륎믞와 파슀타, 욞람, 칭구의 되돌아옎(FPUT Recurrence), 포슬늬에-늜킚 반전Ʞ(Peaucellier Linkage), 몰늬의 삌등분선 정늬(Morley Triangle), 랄곌 마드띌슀, 소칌의 플벗 알고늬슘윌로 돌며 자Ʞ 회플 걞음(Pivot Walk)읎에요. 43판은 닀섯을 더했얎요. 월늬슀 워컀의 칌레읎도사읎큎(Kaleidocycle), 쌍곡형에서 록소드롬형윌로 감Ʞ는 뫌비우슀 변환의 흐멄(Möbius Loxodrome), 터튞의 용수철 정늬(Tutte Embedding), 디펀튞와 크래비잠의 고늬 정늬륌 품은 히토메자시 사시윔(Hitomezashi), 룜게 현상곌 첎비쇌프 점(Runge Phenomenon)읎에요. 44판은 닀섯을 더했얎요. 마크 칎잠의 고늬 몚형(Kac Ring), 슀톀곌 튜킀의 햄 샌드위치 정늬(Ham Sandwich), 람란튞의 불가늬아 솔늬테얎(Bulgarian Solitaire), 엘슀쌄슀와 프늬시의 탄도 소멞(Ballistic Annihilation), 페레귞늰의 솔늬톀(Peregrine Soliton)읎에요. 45판은 닀섯을 더했얎요. 로걎-셰프와 베륎시크-쌀로프의 극한 곡선윌로 닀가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도람룚신 겜계 조걎을 걎 임계 읎징 몚형의 SLE(3) 겜계(Ising Interface), 프늬예제프와 동료듀의 였음러 걷개로 쌓는 로터 띌우터 응집곌 레빈-페레슀의 슀맀시 합(Rotor-Router), 폎늬아의 곌수원 묞제(Pólya's Orchard), 넬슚의 평멎 색칠 묞제와 아읎슈벚의 음곱 색(Hadwiger-Nelson)읎에요. 46판은 닀섯을 더했얎요. 람늎룚앙의 구역(Brillouin Zones), 몚슀 읎론곌 레람의 귞래프(Reeb Graph), 로렌잠 Ʞ첎와 시나읎의 당구(Lorentz Gas), 늬얞 Ꞁ래슀의 점묎늬(Glass Patterns), 벀치와 수테띌, 불플아니의 확률 공명(Stochastic Resonance)읎에요. 47판은 닀섯을 더했얎요. 였킀프와 홍, 슀튞로가잠의 슀워멀레읎터(Swarmalators), 빌슀의 왞로욎 달늬Ʞ 선수 추잡(Lonely Runner), 샀잠의 올로읎드와 ê·ž 전개도(Oloid), 쟚베의 원 채우Ʞ 정늬와 윜늰슀-슀티랐슚의 반복(Koebe Packing), 윘웚읎의 병사와 황ꞈ비 묎게(Conway's Soldiers)예요. 48판은 닀섯을 더했얎요. 였하띌의 맀듭 뫌비우슀 에너지(Möbius Energy), 첎비쇌프의 펞향곌 룚빈슀타읞-사륎낙의 밀도(Chebyshev's Bias), 섀든곌 레킀엔, 마슀든의 읎쀑 소용돌읎 능선(Double Gyre), 아자늬와 프로슀튞의 깜빡읎는 래칫(Flashing Ratchet), 망덞람로의 프랙탈 슀믞Ʞ(Fractal Percolation)예요. 49판은 닀섯을 더했얎요. 뒀러의 전개 묞제와 셰퍌드의 추잡(DÃŒrer's Unfolding), 베늬와 곚드버귞가 재규격화한 섞타 합의 ꌬ부랑 곡선(Curlicue), 였음러 방정식곌 푞앵소의 폎혞드로 볞 테니슀 띌쌓 정늬(Tennis Racket), 쌐프의 사슬곌 ë„€ 색 정늬(Kempe Chains), 퇎플늬잠의 낎접 정사각형 묞제와 귞늰-로람의 직사각형(Square Peg)읎에요. 50판은 닀섯을 더했얎요. 크레읎치넌곌 배철러의 2찚원 난류가 거꟞로 가는 캐슀쌀읎드와 맥윌늬엄슀의 소용돌읎(Inverse Cascade), 사포발곌 로소, 구예의 확산 앞쀄(Diffusion Front), 필늬와 마륎쌀티, 쌀읎잠와 타읎왞륎의 욎동성 상분늬(Motility Clusters), 박곌 슀넀펜의 진화 몚형(Bak-Sneppen), 늬섞의 영원한 가속(Risset Rhythm)읎에요. 51판은 닀섯을 더했얎요. 슀믞슀와 마읎얎슀, 캐플런, 굿뚌-슀튞띌우슀의 몚자 타음곌 H/T/P/F 메타타음 치환(Hat Monotile), 반도와 동료듀의 최적 속도 몚형곌 슀Ʞ알마의 원형 튾랙 싀험(Phantom Jam), 사토와 테읎튞의 법칙곌 타원곡선의 a_p(Sato-Tate), 칌띌비와 알우의 공간에서 핞슚읎 잘띌 귞늰 5ì°š 페륎마 곡선(Calabi-Yau Quintic), 에덞슀람룚너와 레처, 조몚로디얞의 지속 혞몰로지와 바윔드(Persistence Barcode)예요. 52판은 닀섯을 더했얎요. 슈람곌 셰필드가 등고선을 SLE(4) 로 볎읞 2찚원 가우슀 자유장(Gaussian Free Field), 당튀와 마슈묎닀륎, 베링거의 ꎑ탄성 힘 사슬(Force Chains), 앀슀컎의 사쀑죌와 마테읎칎, 플잠몚늬슀의 같은 통계 닎ꞈ질(Anscombe's Quartet), 드 람띌윈 수엎곌 BEST 정늬(de Bruijn Sequence), 띌읞슀의 베타 평멎 띠 제튞(Zonal Jets)예요. 53판은 엎을 더했얎요. 듀크, 가륎시아, 룚잠가 귞늰 가우슀 죌Ʞ(Gaussian Periods), 부륎가 볎읞 현수멎곌 나선멎의 휹(Catenoid-Helicoid), 디랙의 벚튞 묘Ʞ(Belt Trick), 헬늄홀잠와 러람의 ì—°êž° 고늬 개구늬뜀(Leapfrogging Vortices), 람룩슀, 슀믞슀, 슀톀, 텃의 정사각형 분할(Squared Rectangle), 포드와 풀컀슚의 최대 흐멄 최소 절닚(Max-Flow Min-Cut), 윔슀타슀 ë°°ì—Ž(Costas Array), 바띌니, 비얎든, 칞의 묎게쀑심 분할(Barycentric Subdivision), 묎윔파디아알와 크넀저의 ë„€ ꌭ짓점 정늬(Four-Vertex Theorem), 였음러의 엘띌슀티칎(Euler's Elastica)예요. 54판은 엎을 더했얎요. 베늬의 묎작위 파동곌 볎고몰니, 슈믞튞가 슀믞Ʞ로 섌 마디 영역(Nodal Domains), 플얎험버튞가 사읞 흐늄에서 찟은 읎상 고유 몚드(Strange Eigenmode), 플얎시 적분곌 베늬, 업슀틞의 첚점 회절 파국(Pearcey Cusp), 나겔곌 바읎슀의 STIT 몚자읎크(STIT Tessellation), 레비의 아크사읞 법칙(Arcsine Law), 슀퇎륎메륎가 닀룬 쌍극자 장 속 대전 입자(StÞrmer Trap), 게음곌 섀플늬의 지연 수띜(Stable Marriage), 슀메음의 말굜(Smale Horseshoe), 욞람 수엎곌 슈타읎너베륎거가 찟은 숚은 진동수(Ulam Sequence), 위얎와 펠런의 거품곌 쌈빈의 깎은 팔멎첎(Weaire-Phelan)예요. 55판은 엎을 더했얎요. 밀러와 셰필드의 허수 Ʞ하(Imaginary Geometry), 티소의 지시 타원곌 메륎칎토륎 투영(Tissot's Indicatrix), 믌윔프슀킀의 묌음표 핚수(Question Mark Function), 버로슀와 휠러의 변환(Burrows-Wheeler), 뷔퐁의 바늘곌 바륎비에의 국수(Buffon's Noodle), 부자의 늬듬 타음링 캐넌(Vuza Canon), 하읞곌 낎시의 헥슀와 게음의 헥슀 정늬(Hex Theorem), 8자 고늬의 범플복곌 자유군의 쌀음늬 귞래프(Universal Cover), 였람친니윔프와 저도비치, 투생곌 윌첵의 확산 제한 소멞(Toussaint-Wilczek), 헬늄홀잠와 쌈빈의 와류 시튞와 크띌슀니의 와동 덩읎(Vortex Sheet)예요. 56판은 엎을 더했얎요. 마테롱의 죜은 잎곌 쌄듀, 퇎넀슀가 아래에서 볞 완전 표볞(Dead Leaves), 곚롬의 튞로믞녞 깔Ʞ(Golomb Trominoes), 윌슚의 고늬 지욎 걞음곌 례러, 슈람, 베륎너의 균등 생성나묎 페아녞 곡선(Tree Peano Curve), 랔로흐와 제너의 진동곌 워니얎의 사닀늬(Bloch Oscillations), 앀드룚슀의 곡선곌 아시몚프의 귞랜드 투얎(Andrews Curves), 지니람륎와 Ʞ륎윔의 원 법칙곌 좀뚞슀, 크늬산티, 솜폎늰슀킀, 슀타읞의 타원 법칙(Elliptic Law), 에륎되시의 불음치 묞제와 볎웚읞, 최, 쿀슀의 3진 수엎(Erdős Discrepancy), 망덞람로의 곱셈 폭포와 칎안, 페읎늬에륎의 묞턱(Multiplicative Cascade), 윔륎테, 첎읎킚, 곚럜, 파읞의 묎작위 조직화(Random Organization), 칌룚가레아누, 화읎튞, 풀러의 ꌬ임곌 뒀틀늌(Twist and Writhe)읎에요. 57판도 엎을 더했얎요. 랔룞의 쀑심축곌 듀불 변환(Medial Axis), 슀메음곌 윌늬엄슀의 솔레녞읎드(Solenoid), 솔로몬의 묎작위 환겜 속 걞음곌 시나읎의 걞음(Sinai's Walk), 테렐곌 펜로슈의 회전(Terrell Rotation), 갈페늰의 π 당구(Galperin Billiard), 페륎마의 두 제곱수 정늬와 자Ʞ에의 한 묞장 슝명(Zagier's Windmills), 슀토크슀 표류와 게륎슀튞너의 튞로윔읎드 파도(Stokes Drift), 띌읞하륎튞부터 ë§š, 맀큎띌우드맚, 폰 데띌우까지의 볌록 였각형 타음링(Pentagon Tilings), 페로와 하위헌슀의 튞랙튞늭슀와 자전거 바퀮 자국(Bicycle Tracks), 였슀튞발튞 숙성곌 LSW 읎론(Ostwald Ripening)읎에요. 58판도 엎을 더했얎요. 알랔란곌 게륎데슀의 ê±°ìšž 곡선(Mirror Curves), 가욎튞와 플셔, 백슀터의 딱딱한 정사각형 격자 Ʞ첎(Hard Squares), 닀니엘 베륎누읎의 맀달늰 사슬곌 베셀 핚수 J0(Hanging Chain), 레음늬와 테음러의 불안정성(Rayleigh-Taylor), 터킀의 반평멎 깊읎와 띌도의 쀑심점 정늬(Tukey Depth), 례러와 베륎너의 람띌욎 고늬 수프(Loop Soup), 칎돔첎프-페튞비아시빌늬 방정식곌 윔닀마의 선 솔늬톀 웹(KP Web), 크누슀가 읎늄 붙읞 베쌀튞-귞레읎 윔드(Beckett-Gray), 파슀칌의 신비 육각형곌 슈타읎너, 컀크뚌의 점(Hexagrammum Mysticum), 드 슀밋곌 렌슀튞띌의 판화 화랑 사상(Print Gallery)읎에요. 59판도 엎을 더했얎요. 륎장드륎와 가우슀의 읎찚 상혞 법칙(Quadratic Reciprocity), 위귞너 핚수와 유륎쌀, 슀톚러의 쌜 고양읎(Wigner Cat), 아비닚곌 샀믞륎의 심 칎빙(Seam Carving), 슐늬히팅곌 녞륎튞마륎크, 에거슀의 알갱읎 Ʞ첎 속 맥슀웰의 도깚비(Granular Demon), 큎렙시의 대각 3ì°š 곡멎곌 쌀음늬, 새뚌의 직선 27개(Clebsch Surface), 볎데륎베륎크의 나선 타음링(Voderberg Tiling), 자읎페륎튞의 곡멎(Seifert Surface), 혞크슀의 자Ʞ 흥분 점곌정(Hawkes Process), 윔혞넚의 자Ʞ조직 지도(Kohonen Map), 레음늬의 위상자 걞음곌 굿맚의 슀페큎 통계(Laser Speckle)예요. 60판도 엎을 더했얎요. 마삎늬아가 짚은 RANDU 의 평멎(RANDU Planes), 였음러의 36 장교와 볎슀, 슈늬칞데, 파컀의 10ì°š 귞레윔-띌틎 방진(Euler's Officers), 펜로슈와 칎터의 등각 윀팩튞화 도형(Penrose Diagram), 러더퍌드 산란의 포묌선 귞늌자(Rutherford Shadow), 였음러 나선곌 프레넬 적분, 윔륎뉎의 회절 셈(Cornu Spiral), 늬처드슚의 디바읎더와 망덞람로의 핎안선 찚원(Richardson Dividers), 에농, 홀, 페익슀의 1찚원 자Ʞ쀑력 멎 몚형곌 며든-벚의 격렬한 읎완(Phase Spiral), 에륎되시와 레니의 거대 성분곌 아큎늬옵타슀, 드수자, 슀펜서의 폭발적 슀믞Ʞ(Giant Component), 허프와 두닀, 하튞의 직선 ì°Ÿêž°(Hough Transform), 조륎당의 곡선 정늬와 심랏의 교찚 섞Ʞ(Jordan Curve)예요. 61판도 엎을 더했얎요. 섞슀나와 닀섯 사람의 영옚 묎작위장 읎징 눈사태(Crackling Noise), 몜죌의 흙 옮ꞰꞰ와 맀캔의 옮김 볎간(Monge Transport), 파욞의 덫곌 마티왞 방정식(Paul Trap), 귞늰힐의 제 묎게 êž°ë‘¥(Greenhill Column), 흐바탈의 믞술ꎀ 정늬와 플슀크의 섞 색 슝명(Art Gallery), 홀디치의 고늬(Holditch Ring), 톰슚의 꺟읞 역선곌 헀비사읎드의 움직읎는 전하(Thomson Kink), 슈나읎더와 바읎슀의 쌍성 렌슈(Binary Lens), 테음러의 되섞Ʞ(Taylor Unmixing), 람누아, 칌로, 디에넀륎 부부의 칎나륎 폭발(Canard Explosion)읎에요. 62판도 엎을 더했얎요. 아람늬윔소프와 큎띌읎너, 로슀, 였틀러의 소용돌읎 격자(Abrikosov Lattice), 욞늬크니의 청색 잡음곌 빈자늬와 뭉치 방법(Void and Cluster), 파읞뚌곌 힙슀의 첎컀볎드(Feynman Checkerboard), 띌도의 바쁜 비버와 마크섌, 분튞로크의 닀섯 상태 챔플얞(Busy Beaver), 폭슀와 컀시너, 비치의 평평한 곡멎 위 직선 흐멄(Veech Octagon), 늬만의 믞분 불가능 핚수와 거버, 닀위슀테륎마튞의 복소 곡선(Riemann's Function), 헀슈와 킚슐레의 한 타음 형(Heesch Tiles), 찚크띌볎륎티와 헀읎슀, 볎고시안의 마당 장터 몚형(Yard-Sale Model), 베륎귞렌의 플타고띌슀 섞 수 나묎(Berggren Tree), 더람띌윈곌 람, 렁의 닚위귌 합곌 밀륞의 완전히 균형 잡힌 늬듬(Balanced Rhythms)읎에요. 63판은 닀섯을 더했얎요. 몚팻의 몚서늬 소용돌읎와 닀넀닀의 사진(Moffatt Eddies), 맥슀웰의 묌고Ʞ 눈 렌슈(Maxwell's Fish-Eye), 읎지쌀비치의 슀파읎크 신겜망(Izhikevich Network), 애듀러와 하소퍌, 우슬늬의 듀뜞 집합 였음러 지표(Excursion Sets), 심슚곌 랔띌읎슀의 ì—­ì„€(Simpson's Paradox)읎에요. 64판은 닀섯을 더했얎요. 암만곌 베읞컀륎의 팔각 타음링을 더 람띌윈의 닀쀑 격자로 지은 것(Ammann-Beenker), 겔판튞, 칎프띌녞프, 저레빈슀킀의 아메바와 포륎슀베늬, 파사레, 치흐의 구멍(Polynomial Amoeba), 킀랔곌 죌렉의 결핚 얌Ʞ와 플시뚌, 데 킀아띌, 칌띌륎윔, 몚늬지의 지귞재귞 전읎(Kibble-Zurek Ions), 알윔비의 적분곌 힐의 영속도 곡선(Hill's Regions), 슀티랐슚곌 칎플찚의 거꟞로 선 진자와 아치슚, 멀늰의 읞도 밧쀄 묘Ʞ(Indian Rope Trick)예요. @@ -3771,6 +3854,8 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. 64판에서 볞 것읎에요. 읎번에도 정늬가 말하는 것을 팚턎의 셈윌로 지쌜뎀얎요. Polynomial Amoeba 는 계수륌 하륎낙 부혞 (-1)^(ij) 의 싀수로 둔 Phase 0 의 넓읎가 아메바가 쀄었닀 부푞는 닀섯 자늬에서 늘 가장 컞얎요(t = 2 에서 3,372 LED, 닀륞 Phase 는 2,061~3,191 LED). 믞할킚곌 룰고륎(2001)의 넓읎 최대가 읎 팚널에서도 셈윌로 맞았얎요. Indian Rope Trick 은 받칚점 최고 속도륌 사슬 킀로 재멎 섞 마디부터 여섯 마디까지 서는 묞턱읎 ë‹€ 1.25~1.3 읎띌, 막대 하나의 칎플찚 묞턱 √(4/3) = 1.155 가까읎 몚였얎요. 위쪜 끝도 있얎서 여섯 마디는 2.15 부터 닀시 묎너졌얎요. Hill's Regions 의 알윔비 상수는 RK4 한 프레임에 평균 1.5×10⁻⁹ 만 얎Ꞌ났고, Kibble-Zurek Ions 는 닎ꞈ질 60번씩 섞얎 Quench 0.2 에서 ꌬ임 0.15 개, 2 에서 1.9 개였얎요. 처음에 Ʞ대한 것곌 얎Ꞌ난 자늬는 Ammann-Beenker 였얎요. 위상자 뒀집Ʞ가 하나씩 올 쀄 알았는데, e1 읎 e0 + e2 방향읎띌 한 묶음의 선 하나가 정사각 격자의 대각선 위 교찚점을 한순간에 ë‹€ 지나서 육각형 서너 개가 한 쀄로 뒀집혀요. +66판에서 볞 것읎에요. 정늬가 수륌 못 박는 대상은 읎번에도 팚턎의 셈윌로 뚌저 맞춰 뎀얎요. Wilkinson's Polynomial 은 ê³± ꌎ로 셈한 동시 반복읎 윌킚슚의 1959년 표륌 소수 아홉짞 자늬까지 귞대로 낎서, 가장 넓은 쌍읎 16.730737466 ± 2.812624894i 였얎요. Hodograph 의 속도 끝 720개는 읎심률 0 부터 0.95 까지 원에서 반지늄의 3.3×10⁻¹⁵ 안에 있었고, Golomb Ruler 의 14 눈ꞈ 자는 91개 거늬가 ë‹€ 달랐얎요. 극한을 말하는 정늬는 팚널에서 ê·ž 빠륎Ʞ륌 쟀얎요. Shapley-Folkman 은 달 몚양의 하우슀도륎프 거늬에 n 을 곱한 값읎 n = 1, 2, 4, 8 에서 11.7, 11.5, 11.4, 10.9 로 거의 귞대로였고, 덮읎지 않은 넓읎는 1/n 볎닀 조ꞈ 빚늬 쀄었얎요. 짐작곌 셈읎 얎Ꞌ난 자늬는 Zermelo Navigation 읎었얎요. 묌삎읎 배볎닀 빠륎멎 소용돌읎 뒀에 귞늌자가 질 쀄 알았는데, 묌읎 생Ʞ지도 사띌지지도 않는 묌삎 안에서는 돌아가멎 얎디든 닿아서, 귞늌자 1,662개가 몚두 팹널 가장자늬에 닿아 있었얎요. Wilkinson's Polynomial 도 짐작곌 달랐얎요. 윌킚슚의 섭동을 걎 닀항식은 귌읎 정수에 있을 때볎닀 덜 예믌핎서, 같은 ÎŽ 에서 ê·Œ 16 의 구늄읎 1.63 에서 0.01 로 쀄었얎요. + 목록 밖에서 31판 뒀에 핮 볌 것읎에요. 페륎믞-파슀타-욞람-칭구의 되돌아옎(1955)은 KdV Solitons 와 같은 뿌늬띌 같은 판에 두지 않아요. 목록 밖에서 33판 뒀에 핮 볌 것읎에요. 토칎륎슀킀의 ë°©(1995)은 거욞로 된 닀각형 방에서 한 점의 빛읎 닀륞 한 점에만 닿지 못하는 방읎띌 Penrose Room 의 ꎑ선 추적을 귞대로 썚요. 프늬드뚌곌 슀윔띌(1987)는 둥귌 원 셋윌로는 볎로메였 고늬륌 만듀 수 없닀는 것을 볎였는데, 원을 밀얎 넣을 때 얎디서 부딪히는지가 귞늌읎 돌요. 정렬 넀튞워크의 시간 t 의 순엎을 점윌로 찍윌멎 대각선에서 원판을 지나 반대 대각선윌로 도는 점 구늄읎 돌요. ê·ž 원판읎 2007년 녌묞읎 낎닀볞 아륎킀메데슀 법칙읎에요. 간격읎 2k 읞 소수 쌍의 수는 Goldbach Comet 곌 같은 FFT 로 섞고, k 마닀 하디-늬틀우드 읞수로 갈늬는 쀄Ʞ 귞늌읎 돌요. @@ -3835,6 +3920,8 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. 목록 밖에서 64판 뒀에 핮 볌 것읎에요. 더 람띌윈의 닀쀑 격자(1981)륌 30도 간격 여섯 묶음윌로 늘늬멎 마늄몚가 30, 60, 90 도의 섞 ꌎ읞 12ê²¹ 타음링읎 되고, Ammann-Beenker 의 교찚점 엎거와 뒀집Ʞ 감지륌 귞대로 썚요. 아메바의 등댈(믞칎엘 파사레와 한슀 룰고륎, 2004)는 Polynomial Amoeba 의 행 풀읎가 arg w 마닀 낮는 z 의 귌윌로 론킚 핚수륌 쀄마닀 1찚원 적분윌로 구하멎, ê·ž 핚수가 여집합 조각마닀 평멎읞 자늬륌 읎얎 귞늎 수 있얎요. 프렌쌈-윘토로바 몚형(알윔프 프렌쌈곌 타티알나 윘토로바, 1938)은 Kibble-Zurek Ions 의 감쇠 분자동역학 사슬을 죌Ʞ 퍌텐셜 위에 눕히멎 되고, ꌬ임 표시와 갈비 역사로 얎Ꞌ낚읎 생Ʞ고 움직읎는 것을 볎음 수 있얎요. L1 둘레 랮푾녾프 궀도의 안정·불안정 닀양첎가 읎룚는 ꎀ(왕상 ì¿€, 마틮 로, 제럎드 마슀든, 셰읞 로슀, 2000)은 Hill's Regions 의 적분Ʞ와 알윔비 껍질 되돌늬Ʞ에 죌Ʞ 궀도 ì°Ÿêž°ë§Œ 더하멎 목을 지나는 Ꞟ로 볎여요. 마티왞 방정식의 안정 지도(에밀 마티왞, 1868)는 Indian Rope Trick 의 RK4 륌 떚늌 한 죌Ʞ만 돌렀 플로쌀 행렬의 대각합윌로 LED 마닀 칠하멎, 거꟞로 선 사슬읎 서는 혀 몚양 영역읎 한 장에 볎여요. +목록 밖에서 66판 뒀에 핮 볌 것읎에요. 제임슀 싱얎(James Singer, 1938)의 완전 찚집합은 91 을 법윌로 {0, 1, 3, 9, 27, 49, 56, 61, 77, 81} ì—Ž 점읎 0 아닌 나뚞지륌 ꌭ 한 번씩 재서, Golomb Ruler 의 혞 귞늌을 91쀄 원통에 감윌멎 빈칞도 겹칚도 없는 Ꞟ읎 자가 돌요. 아읎작 뉎턎의 회전 궀도 정늬(『프늰킀플아』 1권 9절, 1687)는 Hodograph 의 쌀플러 풀읎에서 핎에서 볞 각도만 k 배로 늘늬멎 되고, 역섞제곱 힘을 더한 궀도의 혞도귞래프가 원에서 벗얎나는 몚습을 나란히 볌 수 있얎요. Zermelo Navigation 의 32방향 데읎크슀튞띌에 묌삎 대신 얞덕 높읎륌 넣윌멎, 한 점에서 퍌지는 잡지선 앞선읎 만나 접히는 절닚 궀적(앙늬 푞앵칎레, 1905)읎 귞렀젞요. Shapley-Folkman 의 격자 믌윔프슀킀 합윌로 두 몚양을 섞은 (1−t)A + tB 의 넓읎륌 띠마닀 재멎, 넓읎의 제곱귌읎 t 에 대핮 였목하닀는 람룬-믌윔프슀킀 부등식(헀륎만 람룬, 1887; 헀륎만 믌윔프슀킀, 1896)읎 볎여요. 같은 슀묎 귌을 번슀타읞 Ʞ저로 적윌멎 의사귌 구늄읎 얌마나 쀄얎드는지(늬닀 파룚킀와 V. T. 띌잔, 1987)륌 Wilkinson's Polynomial 의 동시 반복곌 의사귌 지도로 팹널 위아래에 나란히 귞늎 수 있얎요. + ### 타읎포 죌제 7 로 핮 볞 Ꞁ자 싀험곌 닀음에 핮 볌 것읎에요. 63판곌 64판읎 같읎 돌며 닀섯씩 처음 만듀었얎요. 63판의 닀섯은 한Ꞁ 자몚가 날아와 몚아쓰Ʞ로 잠Ʞ는 조늜(Jamo Assembly), 놀턎곌 하뚌의 Ʞ혞 몚자읎크(Type Mosaic), 혞프슀태터의 튞늜렛 귞늌자(Trip-let), 칎프늬칎의 계산 종읎(Kaprekar's Routine), 캐럎의 낱말 사닀늬(Word Ladder)예요. @@ -3849,6 +3936,14 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. 같은 펜 몚덞로 한Ꞁ을 썚 볌 만핎요. Broad Nib 의 펜읎 쓞고 지나가는 셈에 자몚 획을 궁첎 붓 각도로 적윌멎, 쎈성, 쀑성, 종성 찚례로 펜읎 한 칞에 몚여요. 칎욎터 닀음은 Ꞁ자 사읎의 흰 자늬예요. Counterforms 의 flood fill 에 Ꞁ자마닀 볌록 껍질을 더하멎 읎웃한 두 Ꞁ자 사읎륌 따로 떌얎 빛나게 할 수 있고, 녞람로 자간을 좁히멎 빛읎 쀄얎요. Ꞁ자의 몞은 몚래 닀음에 묌로 바꿔 볌 만핎요. Letter Sand 의 비튞맵 Ꞁ자륌 입자 유첎로 채우멎 획읎 녹아 바닥을 따띌 퍌졌닀가 닀음 낱말 틀에 닀시 부얎젞요. 한Ꞁ 양쪜 맞춀도 낚았얎요. 한Ꞁ 조판은 낱말 사읎볎닀 Ꞁ자 사읎륌 고륎게 늘늬는 음읎 많아서, Rivers of White 의 묞닚을 한Ꞁ로 짜멎 강 대신 섞로로 곧게 선 Ꞁ자 엎읎 생Ʞ는지 볌 수 있얎요. +66판의 닀섯은 혞프슀태터의 gridfont 륌 녞람로 닀시 짓는 격자 Ꞁꌎ(Gridfonts), Ꞁ에서 ë–šì–Žì§„ Ꞁ자륌 섞얎 띌읎녞타읎프 자판의 찚례와 견죌는 막대(Etaoin Shrdlu), 낱말곌 ê·ž 음화륌 막대 격자로 번갈아 드러낎는 읞쇄(Barrier Grid), 윀곜 Ꞁ자가 RGB LED 격자에 래슀터되는 êžž(Subpixel), 유니윔드 한Ꞁ 11,172 음절을 섞는 죌행 Ʞ록계(Syllable Odometer)예요. + +66판에서 볞 것읎에요. 읎번에도 닀섯읎 ë‹€ Ꞁ자 ꌎ을 윔드에 적었고, 옮겚 쓎 것읎 둘읎에요. Etaoin Shrdlu 는 Typewriter 의 3×5 대묞자륌, Syllable Odometer 는 Wongoji 의 9×11 조합 Ꞁꌎ을 싀얎서 23.9 KB 가욎데 6.7 KB 가 Ꞁꌎ읎에요. 가는 선윌로 지은 Ꞁ자는 가만히 서 있윌멎 지도의 아래 끝을 넘었얎요. Gridfonts 의 첫 판은 슀타음읎 바뀌는 사읎 4쎈 동안 Ꞁ자가 멈춰 움직임 0.018 읎었고, 펜 Ꞟ을 따띌 빛읎 지나가게 하자 0.046 읎 됐얎요. Ꞁ자가 숚는 자늬는 밝Ʞ가 고륞 자늬였얎요. Barrier Grid 의 읞쇄는 두 칞마닀 ꌭ 하나가 쌜진 쀄묎늬띌서 막대가 반 칞 지난 순간 「빛」의 4,096 쌍 가욎데 4,047 쌍읎 같은 밝Ʞ가 되고, 낱말은 ꞈ빛곌 파랑의 색윌로만 낚아요. 픜셀 격자는 맞춰도 색을 낚게얎요. Subpixel 에서 한 픜셀 폭윌로 맞춘 섞로 쀄Ʞ는 LCD 필터륌 지나 (171, 240, 171) 로 쎈록읎 돌았얎요. 섞는 팚턎에서는 동점곌 빠진 칞읎 귞늌을 정했얎요. Etaoin Shrdlu 의 앚늬슀는 T 와 I 가 ꌭ 40번씩읎띌 뚌저 닿은 T 가 왌쪜에 낚아 띌읎녞타읎프와 같은 자늬가 여섯읎 됐고, Syllable Odometer 의 ㄞ 지도는 588칞 가욎데 86칞만 쌜지고 똠은 똘 닀음 속 빈 엎두 칞 가욎데 하나로 지나가요. + +Ʞ능 감은 읎번에도 Text 레읎얎의 Ꞁ을 윔드가 받는 Ꞟ로 몚였고, 받는 ꌎ읎 팚턎마닀 달랐얎요. Etaoin Shrdlu 는 Ꞁ을 받아 섞렀는데 Text 레읎얎가 200자까지띌(`TEXT_MAX_CHARS`) 앚늬슀 첫 두 묞장의 464Ꞁ자가 157Ꞁ자로 쀄얎요. Barrier Grid 는 걞음마닀 닀륞 낱말읎 필요핎서 낱말 목록곌 낱말마닀의 덮임 비튞맵을 바랐고, Subpixel 은 Ꞁꌎ의 Ꞁ자 윀곜을 2ì°š 베지에 점 목록윌로 받Ʞ륌 바랐얎요. Syllable Odometer 는 팚턎끌늬 Ꞁꌎ을 나눠 쓰는 Ꞟ을 바랐얎요. Lab 의 사읎튞 Ꞁꌎ 넷은 latin subset 만 싀얎서 한Ꞁ 픜셀 Ꞁꌎ읎 아직 없얎요. Gridfonts 는 닀륞 것을 바랐얎요. Style 녞람의 음곱 자늬에 읎늄을 달 수 있게 `@knobs` 가 칞 읎늄을 받윌멎, 지ꞈ 얎느 슀타음읞지 죌석을 읜지 않아도 알 수 있얎요. + +66판 뒀에 핮 볌 것읎에요. 데읎비드 허프뚌의 부혞(1952)는 Etaoin Shrdlu 가 섞고 쀄 섞욎 엎여섯 칞 위에서 지을 수 있얎요. 가장 낮은 두 막대가 하나로 합쳐 올띌가Ʞ륌 되풀읎하멎 팹널 위로 자띌는 나묎가 되고, 잊은 Ꞁ자음수록 뿌늬까지 가지가 짧아요. 게럎드 였슀터와 니시지마 알슀녞늬(Scientific American, 1963)가 볎읞 대로 죌Ʞ 구조 둘을 겹치멎 몚아레가 생Ʞ니, Barrier Grid 의 읞쇄와 막대륌 동심원 죌Ʞ로 짜멎 낱말읎 원 띠마닀 빛곌 귞늘로 갈렀요. 크늬슀 귞늰읎 Valve 에서 SIGGRAPH 2007 에 낾 거늬장 Ꞁ자 확대는 Subpixel 의 윀곜을 거친 격자의 거늬장윌로 바꿔 킀욎 ë’€ 묞턱윌로 자륎멎 되고, 몚서늬가 둥Ꞁ게 녹는 것읎 픜셀 닚위로 볎여요. 안상수첎(안상수, 1985)는 자몚마닀 ꌎ읎 한 벌뿐읞 탈넀몚ꌎ읎띌, Syllable Odometer 의 바퀮 셋읎 고륞 자몚륌 제자늬에 겹쳐 놓Ʞ만 핮도 11,172 음절읎 ë‹€ 서요. + ### 화장품 팝업 죌제 6 윌로 쓎 질감곌 닀음에 핮 볌 질감읎에요. 29판읎 처음 넷을 썌얎요. 팔레튾 나읎프로 펮 바륎는 크늌(Cream Swirl), 구슬읎 뜬 저곌 빛 귞묌(Serum Gel), 쏟아젞 쌓읎는 Ꞁ늬터(Glitter Fall), 퍌프가 눌렀닀 든 압축 파우더(Loose Powder)예요. 넷 ë‹€ K1 읎 색상환 한 바퀎띌서, 색을 돌늬멎 죌황곌 녞랑읎 갈색읎나 올늬람로 볎읎는 자늬가 있었얎요. Serum Gel 은 ê·ž 둘레에서만 밝Ʞ륌 0.62 와 0.3 에서 0.92 와 0.68 로 올렞얎요. 밝은 바탕은 반짝임을 지웠얎요. 200 위에 55 륌 더한 반짝임은 거의 안 볎여서 Glitter Fall 은 얎두욎 바탕에 섰고, 흰 바탕곌 ꞈ색 한 쀄은 Loose Powder 의 아래 띠로만 썌얎요. 크늌곌 저은 가장자늬 0.028 곌 0.079 로 연구에서 가장 번지는 귞늌읎에요. ê·ž 둘읎 가장자늬 축의 아래 끝을 옮겚서 지도에 새 빈 칞읎 났얎요. 크늌은 빛만 움직여서는 움직임읎 0.019 에 뚞묌렀얎요. 결읎 깊얎알 빛읎 돌 때 바뀌는 LED 가 생겚요. 비누막읎 터질 때 구멍 가장자늬가 테음러와 컬늭의 속도(1959, 1960)로 고륎게 벌얎지며 묌방욞 테륌 낚Ʞ는 것은 Soap Film 의 끝을 늘읞 귞늌읎에요. 큎렌징 였음읎 묌을 만나 가장자늬부터 우윳빛윌로 유화되는 것은 Blotting Paper 처럌 LED 마닀 한 번만 바뀌는 셈윌로 귞늎 만핎요. From cb21ddd2e3ed8e67b520af00fb8b3ed869b43b3d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 11:45:23 +0000 Subject: [PATCH 26/26] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2066=ED=8C=90:=20index.html=20=EC=9D=84=20558=EA=B0=9C?= =?UTF-8?q?=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20=EC=A7=93=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_01Gdqi6PeQBKagUB2vLqwQZR --- incoming/pattern-study/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index 28f72c962..32227b9c0 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

팹턮 연구

- - + +