From 36a4c6f8ddc487f3fe8544d13ff3649d1bea7424 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 11:23:02 +0000 Subject: [PATCH 1/9] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=20=EC=97=B0=EA=B5=AC=2060=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_015ttLM9zxviQho5eHL89da2 From a99577a6321f27d375d30b9a58df1ca871d0f4ee Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 12:09:45 +0000 Subject: [PATCH 2/9] pattern study: Cornu Spiral and Richardson Dividers, round 60 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015ttLM9zxviQho5eHL89da2 --- .../pattern-study/patterns/cornu-spiral.js | 293 ++++++++++++++ .../patterns/richardson-dividers.js | 369 ++++++++++++++++++ 2 files changed, 662 insertions(+) create mode 100644 incoming/pattern-study/patterns/cornu-spiral.js create mode 100644 incoming/pattern-study/patterns/richardson-dividers.js diff --git a/incoming/pattern-study/patterns/cornu-spiral.js b/incoming/pattern-study/patterns/cornu-spiral.js new file mode 100644 index 000000000..9604f7a13 --- /dev/null +++ b/incoming/pattern-study/patterns/cornu-spiral.js @@ -0,0 +1,293 @@ +// ===== Patternflow pattern ===== +// Title: Cornu Spiral +// Family: line +// Idea: The Euler spiral curls its two arms into their eyes at (1/2, 1/2) and (-1/2, -1/2) across the top of the panel, a bright phasor chord slides along it from one eye or across a fixed arc of it, and the square of that chord's length, the light Fresnel diffraction puts at one point behind a straight edge or a slit, is traced below as a fringe profile and shown under it as the fringes themselves. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Euler spiral (Leonhard Euler, 1744), Fresnel integrals (Augustin-Jean Fresnel, 1818), Cornu spiral as a diffraction calculator (Marie Alfred Cornu, 1874), pattern round 60 +// Round: 60 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Slit=0..1, Spread=0..1 +// +// Knob 1: Hue. Turns the spiral and the light round the wheel together. At 0 the spiral is sky blue and the light, the chord, the profile and the fringes, gold; at 0.25 violet and green, at 0.5 red and blue, at 0.75 green and pink. The spiral skips the orange part of the wheel, which reads brown when dim. +// Knob 2: Speed. At 2 the point on the screen crosses it in 5 s and comes back in 5 s, slowing at both ends, so the chord's free end runs up the spiral and back; at 0.2 a crossing takes 50 s, at 3 it takes 3.3 s. +// Knob 3: Slit. At 0 a straight edge: the chord runs from the lower eye, the screen spans w = -2 to 5 in Fresnel units, the light rises from 0 in the shadow through 0.25 at the edge to 1.37 at w = 1.22 and rings down to 1, the dotted line. Above 0 a slit of width a = 0.6 + 7.4 (1 - K3)^1.5, the screen w = -6 to 6, and the chord joins the two ends of an arc a long: at 0.25 (a = 5.4) Fresnel fringes stand inside the slit's image, at 0.5 (a = 3.2) two humps of 1.42 with a dip to 1.05 between them, at 0.75 (a = 1.5) one peak of 1.08 with dark zeros at w = +-1.31, and at 1 (a = 0.6) a broad hump of only 0.18 whose zeros sit at w = +-3.33 = 2 / a, the Fraunhofer sinc squared. The profile is scaled so its highest point stands 42 rows tall. +// Knob 4: Spread. The light's band of wavelengths. At 0 one wavelength in the colour of K1; at 1 white light, 650, 530 and 450 nm in red, green and blue, each with its own chord to its own point of the spiral, since w scales as one over the square root of the wavelength. Behind the straight edge the first fringe is then white with a blue rim toward the shadow and an orange one away from it, the next ones pass through pink, sky blue, yellow, violet and green, and past w = 4.4 they wash out to white. It opens at 0.06, close to one wavelength. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, sqrt, cos, sin, exp, round, ceil, hypot, PI } = 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; +} +// The wheel without its orange and yellow, 0.03 to 0.17. +const wheel = (h) => 0.17 + (h - floor(h)) * 0.86; + +// C(t) and S(t), the Fresnel integrals of cos and sin of pi s^2 / 2 from 0 +// to t, tabled every 1/200 for |t| <= 12 by Simpson's rule on eight +// substeps a step: C(1) = 0.7798934004 and S(1) = 0.4382591474, and the +// whole table is within 5e-11 of one built on 64 substeps. Between entries +// the spiral is read as a straight chord, at most 0.005 LED off the curve +// at t = 12, where at 39.5 LEDs a unit a coil is one LED across; everything +// past that is the eye. +const TM = 12, TS = 200, NT = 2 * TM * TS + 1, I0 = TM * TS; +const FC = new Float64Array(NT), FS = new Float64Array(NT); +let tabled = false; +function fresnel() { + if (tabled) return; + const h = 1 / TS, m = 8, q = h / m; + let c = 0, s = 0; + for (let i = 1; i <= I0; i++) { + const a = (i - 1) * h; + let sc = 0, ss = 0; + for (let j = 0; j <= m; j++) { + const u = a + j * q, ph = (PI * u * u) / 2, wt = j === 0 || j === m ? 1 : j & 1 ? 4 : 2; + sc += wt * cos(ph); + ss += wt * sin(ph); + } + c += (sc * q) / 3; + s += (ss * q) / 3; + FC[I0 + i] = c; + FS[I0 + i] = s; + FC[I0 - i] = -c; + FS[I0 - i] = -s; + } + tabled = true; +} +const ZP = [0, 0]; +function Z(t) { + let f = (cl(t, -TM, TM) + TM) * TS, i = floor(f); + if (i > NT - 2) i = NT - 2; + f -= i; + ZP[0] = FC[i] + (FC[i + 1] - FC[i]) * f; + ZP[1] = FS[i] + (FS[i + 1] - FS[i]) * f; + return ZP; +} + +// The spiral's plane on the panel: 39.5 LEDs a unit about (31.5, 30), S up. +// C runs to +-0.780 (columns 0.7 to 62.3), S to +-0.714 (rows 1.8 to 58.2); +// the eyes sit at (11.75, 49.75) and (51.25, 10.25). +const SC = 39.5, CX = 31.5, CY = 30; +const lx = (c) => CX + c * SC, ly = (s) => CY - s * SC; +// The profile: I = 0 on row 112, the highest value 42 rows up; the screen +// on rows 116 to 127, its LEDs as bright as the light that falls there. +const PB = 112, PH = 42, BAND = 116; +// Wavelengths over the middle one at full Spread: 650 / 530 and 450 / 530. +const DL = [0.226, 0, -0.151], PRIM = [1, 0, 0, 0, 1, 0, 0, 0, 1]; + +export function setup(params) { + fresnel(); + // The whole spiral once, as the length of curve under each LED. + const D = new Float32Array(W * 64); + const step = SC / TS; + for (let i = 0; i < NT; i++) { + const x = lx(FC[i]), y = ly(FS[i]), ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + const put = (a, b, v) => { + if (a >= 0 && a < W && b >= 0 && b < 64) D[b * W + a] += v; + }; + put(ix, iy, (1 - fx) * (1 - fy) * step); + put(ix + 1, iy, fx * (1 - fy) * step); + put(ix, iy + 1, (1 - fx) * fy * step); + put(ix + 1, iy + 1, fx * fy * step); + } + for (let p = 0; p < D.length; p++) D[p] = min(1, D[p]) ** 0.75; + params.s = { + hue: 0, speed: 2, slit: 0, spread: 0.06, clock: 0, phase: 0, warm: false, D, + last: new Float32Array(W), xh: 0, I: new Float32Array(W * 3), + A: new Float32Array(W * 65), R: new Float32Array(N), G: new Float32Array(N), B: new Float32Array(N), c: new Float32Array(24), + }; +} + +// Where the screen point is, 0..1 across, from the sweep's phase: there and +// back, slowing at both ends. +const ease = (ph) => { + const u = ph - 2 * floor(ph / 2), f = u < 1 ? u : 2 - u; + return (1 - cos(PI * f)) / 2; +}; + +function mark(S, x0, x1, t) { + // Columns the head passed between x0 and x1 were lit at time t. + const a = max(0, ceil(min(x0, x1) - 1e-9)), b = min(W - 1, floor(max(x0, x1))); + for (let c = a; c <= b; c++) S.last[c] = t; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.slit = cl(k[2], 0, 1); + S.spread = cl(k[3], 0, 1); + const rate = S.speed / 10; // crossings a second + if (!S.warm) { + // Open as if the head had just come back across the screen from the + // right, so the whole profile is already there, brightest at the left. + S.last.fill(-99); + let px = W - 0.5; + for (let i = 1; i <= 200; i++) { + const ph = i / 200 - 1, x = ease(ph) * W - 0.5; + mark(S, px, x, ph / rate); + px = x; + } + S.xh = -0.5; + S.warm = true; + } + S.clock += d; + S.phase += d * rate; + const x = ease(S.phase) * W - 0.5; + mark(S, S.xh, x, S.clock); + S.xh = x; +} + +// Accumulate light into the float frame: a bilinear splat, and lines as +// splats every third of an LED so a line carries the same light per LED of +// length at any slope. +function splat(S, x, y, v, c, o) { + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + add(S, ix, iy, (1 - fx) * (1 - fy) * v, c, o); + add(S, ix + 1, iy, fx * (1 - fy) * v, c, o); + add(S, ix, iy + 1, (1 - fx) * fy * v, c, o); + add(S, ix + 1, iy + 1, fx * fy * v, c, o); +} +function add(S, x, y, v, c, o) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const p = y * W + x; + S.R[p] += v * c[o]; + S.G[p] += v * c[o + 1]; + S.B[p] += v * c[o + 2]; +} +function line(S, x0, y0, x1, y1, v, c, o, ends) { + const L = hypot(x1 - x0, y1 - y0), n = max(1, ceil(L * 3)), w = (v * L) / n; + for (let i = ends ? 0 : 1; i <= n; i++) splat(S, x0 + ((x1 - x0) * i) / n, y0 + ((y1 - y0) * i) / n, w, c, o); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { R, G, B, c, D, I, last } = S, sp = S.spread, slit = S.slit > 0; + R.fill(0); + G.fill(0); + B.fill(0); + // Colours: 0 the dim spiral, 3 the lit arc, 6 the light, 9.. the three + // wavelengths, 18 white, 21 grey. + hsv(wheel(0.48 + S.hue), 0.8, 0.32, c, 0); + hsv(wheel(0.48 + S.hue), 0.4, 0.8, c, 3); + hsv(0.13 + S.hue, 0.85, 1, c, 6); + for (let q = 0; q < 3; q++) + for (let ch = 0; ch < 3; ch++) c[9 + q * 3 + ch] = ((1 - sp) * c[6 + ch]) / 3 + sp * PRIM[q * 3 + ch]; + c[18] = c[19] = c[20] = 1; + c[21] = c[22] = c[23] = 0.2; + + // The screen in Fresnel units and the slit, a = 0.6 + 7.4 (1 - K3)^1.5. + const a = 0.6 + 7.4 * (1 - S.slit) ** 1.5; + const w0 = slit ? -6 : -2, w1 = slit ? 6 : 5, ws = (w1 - w0) / W; + const rs = [0, 1, 2].map((q) => 1 / sqrt(1 + sp * DL[q])); + let Imax = 0; + for (let col = 0; col < W; col++) { + const w = w0 + (col + 0.5) * ws; + for (let q = 0; q < 3; q++) { + let dx, dy; + if (slit) { + const p = Z((w - a / 2) * rs[q]), px = p[0], py = p[1], e = Z((w + a / 2) * rs[q]); + dx = e[0] - px; + dy = e[1] - py; + } else { + const e = Z(w * rs[q]); + dx = e[0] + 0.5; + dy = e[1] + 0.5; + } + const v = (I[col * 3 + q] = (dx * dx + dy * dy) / 2); + if (v > Imax) Imax = v; + } + } + const ks = PH / Imax; + + // The spiral, dim, and the part of it that lets light through bright: from + // the lower eye up to the chord's end behind a straight edge, the arc a + // long inside a slit. + for (let y = 0, p = 0; y < 64; y++) + for (let x = 0; x < W; x++, p++) { + const v = D[p]; + if (v > 0) (R[p] = v * c[0]), (G[p] = v * c[1]), (B[p] = v * c[2]); + } + const wh = w0 + (S.xh + 0.5) * ws; + const t1 = slit ? wh - a / 2 : -TM, t2 = slit ? wh + a / 2 : wh; + const i1 = max(0, ceil((t1 + TM) * TS)), i2 = min(NT - 1, floor((t2 + TM) * TS)), st = SC / TS; + // The lit arc goes through the same tone curve as the spiral, so the + // coils round an eye stay rings instead of piling up into a white disc. + const A = S.A; + A.fill(0); + for (let i = i1; i <= i2; i++) { + const x = lx(FC[i]), y = ly(FS[i]), ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy, p = iy * W + ix; + A[p] += (1 - fx) * (1 - fy) * st; + A[p + 1] += fx * (1 - fy) * st; + A[p + W] += (1 - fx) * fy * st; + A[p + W + 1] += fx * fy * st; + } + for (let p = 0; p < A.length; p++) + if (A[p] > 0) { + const v = min(1, A[p]) ** 0.75; + R[p] += v * c[3]; + G[p] += v * c[4]; + B[p] += v * c[5]; + } + // The unobstructed wave's chord, eye to eye, length sqrt 2: I = 1. + for (let u = 0.04; u < 1; u += 0.06) splat(S, lx(u - 0.5), ly(u - 0.5), 0.9, c, 21); + // The chords, one a wavelength, each from its own two points. + for (let q = 0; q < 3; q++) { + let x0, y0; + if (slit) { + const p = Z((wh - a / 2) * rs[q]); + (x0 = lx(p[0])), (y0 = ly(p[1])); + } else (x0 = lx(-0.5)), (y0 = ly(-0.5)); + const e = Z((slit ? wh + a / 2 : wh) * rs[q]), x1 = lx(e[0]), y1 = ly(e[1]); + line(S, x0, y0, x1, y1, 1.15, c, 9 + q * 3, true); + splat(S, x1, y1, 0.9, c, 9 + q * 3); + if (slit) splat(S, x0, y0, 0.9, c, 9 + q * 3); + } + splat(S, lx(0.5), ly(0.5), 0.5, c, 18); + splat(S, lx(-0.5), ly(-0.5), 0.5, c, 18); + + // The profile: I = 1 dotted, the geometric shadow's edges dotted, the + // curve brightest where the head has just been, and the head itself. + if (PB - ks > PB - PH - 6) for (let x = 0; x < W; x += 2) add(S, x, round(PB - ks), 0.9, c, 21); + const edges = slit ? [-a / 2, a / 2] : [0]; + for (const e of edges) { + const ex = round((e - w0) / ws - 0.5); + for (let y = PB - PH; y <= PB; y += 2) add(S, ex, y, 0.8, c, 21); + } + const now = S.clock, glow = (col) => 0.45 + 0.55 * exp(-(now - last[col]) / 2.2); + for (let q = 0; q < 3; q++) + for (let col = 0; col < W - 1; col++) { + const g = (glow(col) + glow(col + 1)) / 2; + line(S, col, PB - I[col * 3 + q] * ks, col + 1, PB - I[(col + 1) * 3 + q] * ks, g, c, 9 + q * 3, col === 0); + } + const xh = S.xh, ia = floor(cl(xh, 0, W - 2)), fa = cl(xh - ia, 0, 1); + const Ih = I[ia * 3 + 1] * (1 - fa) + I[(ia + 1) * 3 + 1] * fa, yh = PB - Ih * ks; + for (let y = yh + 2; y <= PB; y += 2) splat(S, xh, y, 0.5, c, 21); + splat(S, xh, yh, 1.6, c, 18); + // The screen. + for (let col = 0; col < W; col++) { + const r = [0, 0, 0]; + for (let q = 0; q < 3; q++) { + const v = I[col * 3 + q] / Imax; + for (let ch = 0; ch < 3; ch++) r[ch] += v * c[9 + q * 3 + ch]; + } + for (let y = BAND; y < H; y++) { + const p = y * W + col; + (R[p] = r[0]), (G[p] = r[1]), (B[p] = r[2]); + } + } + splat(S, xh, BAND - 2, 1, c, 18); + + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) + display.setPixel(x, y, round(min(1, R[p]) * 255), round(min(1, G[p]) * 255), round(min(1, B[p]) * 255)); +} diff --git a/incoming/pattern-study/patterns/richardson-dividers.js b/incoming/pattern-study/patterns/richardson-dividers.js new file mode 100644 index 000000000..7e1e2a47c --- /dev/null +++ b/incoming/pattern-study/patterns/richardson-dividers.js @@ -0,0 +1,369 @@ +// ===== Patternflow pattern ===== +// Title: Richardson Dividers +// Family: fractal +// Idea: A random self-similar curve of dimension D runs corner to corner across the top of the panel while a pair of dividers walks along it at a fixed opening, swinging from point to point and laying down a polygon of equal chords that cuts off the curve's bays, and each finished walk drops the point (log s, log L) onto a log-log plot below, so as the opening shrinks from 23 LEDs to under one in ten steps of 0.7 the points climb a straight line of slope 1 - D. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Richardson's divider method (Lewis Fry Richardson, 1961), fractal dimension of a coastline (Benoit Mandelbrot, 1967), dimension of random recursive constructions (R. Daniel Mauldin and S. C. Williams, 1986), pattern round 60 +// Round: 60 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Roughness=-1..1, Cutoff=0..1 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the curve is lilac, the dividers' polygon, its bays and the measured points sea green and the dividers white; at 0.25 the curve pink and the polygon blue, at 0.5 lime and pink, at 0.75 teal and yellow. The curve skips the orange part of the wheel, which reads brown when dim. +// Knob 2: Speed. At 2 each opening takes 1.45 s, a walk of 1.1 s and a pause of 0.35 s with the polygon complete, so the ten openings of one curve take 14.5 s before a fresh curve fades in; at 0.2 ten times slower, at 3 half as fast again. +// Knob 3: Roughness. Sets the dimension D = 1.27 + 0.25 x K3: 1.02 at -1, about the coast of South Africa in Richardson's table, 1.27 at 0, a little above the west coast of Britain, and 1.52 at 1. The curve grows from 109 LEDs long, a gentle wobble, through 501 to 2,185, a crumpled line that is shrunk to fit, and the slope of the line on the plot goes from -0.02 to -0.52; over 60 curves each the fitted slope of the ten points averaged -0.019, -0.137, -0.261, -0.371 and -0.496 at the five stops, a little shallower than 1 - D. The curve keeps its random choices, so turning the knob bends the same curve. +// Knob 4: Cutoff. The curve stops dividing once a piece is no longer than 0.3 x 100^K4 LEDs: 0.40 at the opening 0.06, where the pieces average 0.19 LEDs, far under the smallest opening of 0.93, so the points keep to the line; 3 at 0.5, pieces of 1.3, and 30 at 1, where the curve is a polygon of 10 to 16 pieces averaging 12.5 LEDs. The plot's dotted mark stands at the mean piece, and to the left of it the points go flat: dividers shorter than a piece measure the polygon's own length. +// Buttons: K1 tap draws a fresh curve and starts again from the widest opening. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, abs, sqrt, sin, cos, acos, atan2, pow, hypot, log10, round, ceil, PI } = 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; +} +const wheel = (h) => 0.17 + (h - floor(h)) * 0.86; + +// A 32-bit hash for the curve's random choices: each piece's choices come +// from its own id, and its four children's ids from it, so a change of D +// or of the cutoff redraws the same tree of choices. +function hash(a) { + a = Math.imul(a ^ (a >>> 16), 0x7feb352d); + a = Math.imul(a ^ (a >>> 15), 0x846ca68b); + return (a ^ (a >>> 16)) >>> 0; +} + +// The curve runs from one corner of rows 0 to 85 to the other, (4, 4) to +// (60, 82) or (60, 4) to (4, 82), 96 LEDs. Every piece P -> Q is replaced +// by four: P -> P1 and P3 -> Q along the piece, each r long, and two up to +// an apex off to one side, a coin flip deciding which. r is the Koch-type +// ratio for D, 1 / (2 + 2 cos theta) with 2 + 2 cos theta = 4^(1/D), times +// a random factor in 0.85..1.15; the apex sits at 0.5 + delta along the +// piece, delta random in -0.07..0.07, and at the height h that makes +// 2 r^D + r2^D + r3^D = 1, to 1e-15 after four Newton steps. Every +// generator then has similarity dimension D, so by Mauldin and Williams +// the curve has dimension D, and the random ratios smear the log-periodic +// staircase one fixed ratio puts into L(s). +const TOP = 86; +function makeCurve(seed, D, cut) { + const c = pow(4, 1 / D) / 2 - 1, rK = 1 / (2 + 2 * c), hK = rK * sin(acos(c)); + const right = seed & 1, sx = right ? 4 : 60, ex = right ? 60 : 4; + const X = [sx], Y = [4]; + const emit = (ax, ay, bx, by, id, depth) => { + const dx = bx - ax, dy = by - ay, len = hypot(dx, dy); + if (len <= cut || depth > 9) { + X.push(bx); + Y.push(by); + return; + } + const z = hash(id), f = z & 2 ? 1 : -1; + const de = 0.07 * ((((z >>> 2) & 1023) / 1023) * 2 - 1); + const r = min(rK * (1 + 0.15 * ((((z >>> 12) & 1023) / 1023) * 2 - 1)), 0.48 - abs(de)); + const a1 = 0.5 + de - r, a2 = 0.5 - de - r, rd = 2 * pow(r, D); + let h = hK; + for (let it = 0; it < 4; it++) { + const q2 = a1 * a1 + h * h, q3 = a2 * a2 + h * h; + const g = rd + pow(q2, D / 2) + pow(q3, D / 2) - 1, gp = D * h * (pow(q2, D / 2 - 1) + pow(q3, D / 2 - 1)); + h = max(1e-4, h - g / max(gp, 1e-6)); + } + const nx = -dy * f, ny = dx * f; + const p1x = ax + r * dx, p1y = ay + r * dy, p3x = bx - r * dx, p3y = by - r * dy; + const p2x = ax + (0.5 + de) * dx + h * nx, p2y = ay + (0.5 + de) * dy + h * ny; + const k = Math.imul(id, 4) + 0x9e37; + emit(ax, ay, p1x, p1y, hash(k), depth + 1); + emit(p1x, p1y, p2x, p2y, hash(k + 1), depth + 1); + emit(p2x, p2y, p3x, p3y, hash(k + 2), depth + 1); + emit(p3x, p3y, bx, by, hash(k + 3), depth + 1); + }; + emit(sx, 4, ex, 82, hash(seed), 0); + // Shrink it about its middle into columns 1 to 62 and rows 1 to 84 if it + // came out bigger; the plot divides L by the end-to-end length. + let x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9; + for (let i = 0; i < X.length; i++) (x0 = min(x0, X[i])), (x1 = max(x1, X[i])), (y0 = min(y0, Y[i])), (y1 = max(y1, Y[i])); + const sc = min(1, 61 / (x1 - x0), 83 / (y1 - y0)), mx = (x0 + x1) / 2, my = (y0 + y1) / 2; + const n = X.length - 1, PX = new Float32Array(n + 1), PY = new Float32Array(n + 1); + for (let i = 0; i <= n; i++) (PX[i] = 31.5 + (X[i] - mx) * sc), (PY[i] = 42.5 + (Y[i] - my) * sc); + let len = 0; + for (let i = 0; i < n; i++) len += hypot(PX[i + 1] - PX[i], PY[i + 1] - PY[i]); + const C = { seed, D, cut, n, X: PX, Y: PY, l0: hypot(PX[n] - PX[0], PY[n] - PY[0]), piece: len / n, walks: [], rows: null, ras: null }; + for (let k = 0; k < NO; k++) C.walks.push(walk(C, OPEN[k])); + return C; +} + +// The openings, 23 LEDs down to 0.93 in ten steps of 0.7. +const NO = 10, OPEN = []; +for (let k = 0; k < NO; k++) OPEN.push(23 * pow(0.7, k)); + +// Richardson's walk: from the tip on the curve, the next tip is the first +// point further along the curve at distance s, found on the piece that +// first leaves the circle of radius s; the walk ends when the rest of the +// curve stays inside the circle, and the length is n s plus that last gap. +function walk(C, s) { + const { X, Y, n } = C, tx = [X[0]], ty = [Y[0]], tj = [0]; + let cx = X[0], cy = Y[0], ax = cx, ay = cy, i = 0; + const s2 = s * s; + for (;;) { + let hit = false; + for (; i < n; i++) { + const bx = X[i + 1], by = Y[i + 1]; + if ((bx - cx) * (bx - cx) + (by - cy) * (by - cy) >= s2) { + const dx = bx - ax, dy = by - ay, fx = ax - cx, fy = ay - cy, A = dx * dx + dy * dy, B = 2 * (fx * dx + fy * dy); + const Cq = fx * fx + fy * fy - s2, t = (-B + sqrt(max(0, B * B - 4 * A * Cq))) / (2 * A); + cx = ax = ax + t * dx; + cy = ay = ay + t * dy; + tx.push(cx); + ty.push(cy); + tj.push(i); + hit = true; + break; + } + ax = bx; + ay = by; + } + if (!hit) break; + } + const steps = tx.length - 1, gap = hypot(X[n] - cx, Y[n] - cy); + return { s, tx, ty, tj, steps, gap, L: steps * s + gap }; +} + +// What a frame needs of a curve beyond its points: the light it leaves on +// each LED, and where it crosses the middle of each row, for the bays. +function prepare(C) { + const { X, Y, n } = C, ras = new Float32Array(W * TOP), rx = [], ri = []; + for (let y = 0; y < TOP; y++) rx.push([]), ri.push([]); + for (let i = 0; i < n; i++) { + const x0 = X[i], y0 = Y[i], x1 = X[i + 1], y1 = Y[i + 1], len = hypot(x1 - x0, y1 - y0), m = max(1, ceil(len * 3)); + for (let j = 0; j < m; j++) { + const t = (j + 0.5) / m, x = x0 + (x1 - x0) * t, y = y0 + (y1 - y0) * t, ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy, w = len / m; + if (ix >= 0 && ix < W - 1 && iy >= 0 && iy < TOP - 1) { + const p = iy * W + ix; + ras[p] += (1 - fx) * (1 - fy) * w; + ras[p + 1] += fx * (1 - fy) * w; + ras[p + W] += (1 - fx) * fy * w; + ras[p + W + 1] += fx * fy * w; + } + } + const ya = min(y0, y1), yb = max(y0, y1); + for (let y = max(0, ceil(ya - 0.5)); y < TOP && y + 0.5 < yb; y++) { + rx[y].push(x0 + ((x1 - x0) * (y + 0.5 - y0)) / (y1 - y0)); + ri[y].push(i); + } + } + for (let p = 0; p < ras.length; p++) ras[p] = min(1, ras[p]) ** 0.6; + C.ras = ras; + C.rows = rx.map((a) => Float32Array.from(a)); + C.rowi = ri.map((a) => Int32Array.from(a)); +} + +const T_WALK = 1.1, T_HOLD = 0.35, T_OPEN = T_WALK + T_HOLD, T_FADE = 0.5; +const points = (C) => C.walks.map((w) => [log10(w.s), log10(w.L / C.l0)]); + +export function setup(params) { + params.s = { + hue: 0, speed: 2, D: 1.27, cut: 0.39, warm: false, k: 0, t: 0, fade: 1, ay: null, + cur: null, old: null, ghosts: [], + R: new Float32Array(N), G: new Float32Array(N), B: new Float32Array(N), c: new Float32Array(18), xs: new Float32Array(4096), + }; +} + +const newSeed = () => (Math.random() * 4294967296) >>> 0; + +function fresh(S) { + if (S.cur) { + // The finished curve's points stay on the plot, dimmer, for two curves. + S.ghosts.unshift({ D: S.cur.D, cut: S.cur.cut, pts: points(S.cur) }); + S.ghosts.length = min(S.ghosts.length, 2); + S.old = S.cur; + } + S.cur = makeCurve(newSeed(), S.D, S.cut); + prepare(S.cur); + S.k = 0; + S.t = 0; + S.fade = S.old ? 0 : 1; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + const D = 1.27 + 0.25 * cl(k[2], -1, 1), cut = 0.3 * pow(100, cl(k[3], 0, 1)); + if (!S.warm) { + // Open with two curves already measured, so the plot shows the line. + S.D = D; + S.cut = cut; + for (let g = 0; g < 2; g++) S.ghosts.push({ D, cut, pts: points(makeCurve(newSeed(), D, cut)) }); + fresh(S); + S.warm = true; + } + if (abs(D - S.D) > 0.002 || abs(cut - S.cut) / S.cut > 0.01) { + // The same choices bent to the new D or cut at the new length; points + // measured on another curve leave the plot. + S.D = D; + S.cut = cut; + S.cur = makeCurve(S.cur.seed, D, cut); + prepare(S.cur); + S.ghosts = S.ghosts.filter((g) => abs(g.D - D) < 0.002 && abs(g.cut - cut) / cut < 0.01); + } + if (input.btnPressed && input.btnPressed[0]) fresh(S); + const ds = (d * S.speed) / 2; + // Where the plot's middle sits: the mean of the ghost points and of all + // ten points of the curve being walked, known before it is walked. + let sum = 0, cnt = 0; + for (const g of S.ghosts) for (const p of g.pts) (sum += p[1]), cnt++; + for (const p of points(S.cur)) (sum += p[1]), cnt++; + const want = sum / cnt; + S.ay = S.ay === null ? want : S.ay + (want - S.ay) * min(1, ds * 6); + S.fade = min(1, S.fade + ds / T_FADE); + S.t += ds; + while (S.t >= T_OPEN) { + S.t -= T_OPEN; + if (++S.k >= NO) fresh(S); + } +} + +function add(S, x, y, v, c, o) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const p = y * W + x; + S.R[p] += v * c[o]; + S.G[p] += v * c[o + 1]; + S.B[p] += v * c[o + 2]; +} +function splat(S, x, y, v, c, o) { + const ix = floor(x), iy = floor(y), fx = x - ix, fy = y - iy; + add(S, ix, iy, (1 - fx) * (1 - fy) * v, c, o); + add(S, ix + 1, iy, fx * (1 - fy) * v, c, o); + add(S, ix, iy + 1, (1 - fx) * fy * v, c, o); + add(S, ix + 1, iy + 1, fx * fy * v, c, o); +} +function line(S, x0, y0, x1, y1, v, c, o) { + const L = hypot(x1 - x0, y1 - y0), n = max(1, ceil(L * 3)), w = (v * L) / n; + for (let i = 1; i <= n; i++) splat(S, x0 + ((x1 - x0) * i) / n, y0 + ((y1 - y0) * i) / n, w, c, o); +} + +// The plot: log10 s across, 37.4 LEDs a decade, from -0.1 at column 3 to +// 1.45 at column 61, and log10 (L / L0) up at 44 rows a decade on rows 87 +// to 124. Only the slope means anything on a log-log plot, so the plot +// holds the middle of all its points, which falls at the openings' mean +// log10 s = 0.665, on row 105.5, and the points spread up and down from it. +const KX = 58 / 1.55, KY = 44, PB = 124, PT = 87, PC = 105.5, MLS = log10(23) + 4.5 * log10(0.7); +const plx = (ls) => 3 + (ls + 0.1) * KX; + +export function draw(display, params) { + const S = params.s; + if (!S || !S.cur) return; + const { R, G, B, c, cur } = S; + R.fill(0); + G.fill(0); + B.fill(0); + // 0 the curve, 3 the polygon and points, 6 the bays, 9 white, 12 grey, + // 15 the dim points of earlier curves. + hsv(wheel(0.65 + S.hue), 0.62, 0.7, c, 0); + hsv(0.42 + S.hue, 0.8, 1, c, 3); + hsv(0.42 + S.hue, 0.8, 0.38, c, 6); + c[9] = c[10] = c[11] = 1; + c[12] = c[13] = c[14] = 0.3; + hsv(0.42 + S.hue, 0.45, 0.6, c, 15); + + const w = cur.walks[S.k], done = S.t >= T_WALK, prog = done ? w.steps : (S.t / T_WALK) * w.steps; + const m = min(w.steps, floor(prog)), fr = done ? 0 : prog - m; + // Bays: the curve up to the tip and the polygon back to the start bound + // them, filled by the even-odd rule along the middle of each row. + const jt = done ? cur.n : w.tj[m], tipx = w.tx[m], tipy = w.ty[m], xs = S.xs; + for (let y = 0; y < TOP; y++) { + const rx = cur.rows[y], ri = cur.rowi[y], yc = y + 0.5; + let n = 0; + for (let q = 0; q < rx.length && n < 4000; q++) if (ri[q] < jt) xs[n++] = rx[q]; + const cross = (x0, y0, x1, y1) => { + if (y0 <= yc !== y1 <= yc && n < 4096) xs[n++] = x0 + ((x1 - x0) * (yc - y0)) / (y1 - y0); + }; + if (!done) cross(cur.X[jt], cur.Y[jt], tipx, tipy); + for (let q = 0; q < m; q++) cross(w.tx[q], w.ty[q], w.tx[q + 1], w.ty[q + 1]); + if (done) cross(tipx, tipy, cur.X[cur.n], cur.Y[cur.n]); + if (n < 2) continue; + const a = xs.subarray(0, n).sort(); + for (let q = 0; q + 1 < n; q += 2) + for (let x = max(0, ceil(a[q] - 0.5)); x <= min(W - 1, floor(a[q + 1] - 0.5)); x++) add(S, x, y, 1, c, 6); + } + // The curve, the one it replaces fading out. + const f = S.fade, ease = f * f * (3 - 2 * f); + for (let p = 0; p < W * TOP; p++) { + let v = cur.ras[p] * ease; + if (S.old && f < 1) v += S.old.ras[p] * (1 - ease); + if (v > 0) (R[p] += v * c[0]), (G[p] += v * c[1]), (B[p] += v * c[2]); + } + // The polygons of the openings already walked on this curve stay, dim, + // each finer one closer to the curve, down to 7.9 LEDs; finer ones lie on + // the curve. The one under way is bright, less so the finer it is, so a + // fine polygon does not bury the curve under it. + for (let o = 0; o < min(S.k, 4); o++) { + const v = cur.walks[o]; + for (let q = 0; q < v.steps; q++) line(S, v.tx[q], v.ty[q], v.tx[q + 1], v.ty[q + 1], 0.6, c, 15); + line(S, v.tx[v.steps], v.ty[v.steps], cur.X[cur.n], cur.Y[cur.n], 0.6, c, 15); + } + // The polygon so far, and the gap to the curve's end once it is done. + const pv = 0.2 + 0.75 * min(1, w.s / 8); + for (let q = 0; q < m; q++) line(S, w.tx[q], w.ty[q], w.tx[q + 1], w.ty[q + 1], pv, c, 3); + if (done) line(S, tipx, tipy, cur.X[cur.n], cur.Y[cur.n], pv, c, 3); + // The dividers: standing on the last two tips, or pivoting on the last + // tip while the other leg swings over to the next one, the hinge 0.45 s + // off the chord. + const px = tipx, py = tipy; + let qx, qy; + if (!done && m < w.steps) { + const a0 = m > 0 ? atan2(w.ty[m - 1] - py, w.tx[m - 1] - px) : atan2(w.ty[1] - py, w.tx[1] - px) - PI; + let a1 = atan2(w.ty[m + 1] - py, w.tx[m + 1] - px); + while (a1 < a0) a1 += 2 * PI; + const a = a0 + (a1 - a0) * fr; + qx = px + w.s * cos(a); + qy = py + w.s * sin(a); + } else if (m > 0) (qx = w.tx[m - 1]), (qy = w.ty[m - 1]); + else (qx = px + w.s * 0.6), (qy = py + w.s * 0.8); + const ex = qx - px, ey = qy - py, el = hypot(ex, ey) || 1; + const hx = (px + qx) / 2 + (ey / el) * 0.45 * w.s, hy = (py + qy) / 2 - (ex / el) * 0.45 * w.s; + line(S, px, py, hx, hy, 0.9, c, 9); + line(S, qx, qy, hx, hy, 0.9, c, 9); + splat(S, px, py, 1.4, c, 9); + splat(S, qx, qy, 1.4, c, 9); + + // The plot. The line has slope 1 - D through the middle of the points. + const ay = S.ay, ply = (ll) => PC - (ll - ay) * KY; + const mine = points(cur).slice(0, S.k + (done ? 1 : 0)); + for (let x = 1; x < W - 1; x += 0.5) { + const yy = ply(ay + (1 - S.D) * ((x - 3) / KX - 0.1 - MLS)); + if (yy >= PT - 1 && yy <= PB + 1) splat(S, x, yy, 0.5, c, 12); + } + // The openings: a faint column each, evenly spaced in log s, and a tick + // under each along the bottom, the one being walked white. + for (let q = 0; q < NO; q++) { + const x = round(plx(log10(OPEN[q])) - 0.5); + for (let y = PT + (q & 1); y <= PB; y += 2) add(S, x, y, 0.55, c, 12); + add(S, x, 126, q === S.k ? 1 : 0.8, c, q === S.k ? 9 : 12); + add(S, x, 127, q === S.k ? 1 : 0.8, c, q === S.k ? 9 : 12); + } + // The curve's mean piece, where the line has to stop. + const lf = plx(log10(cur.piece)); + if (lf > 1 && lf < W - 1) for (let y = PT; y <= PB; y += 3) splat(S, lf, y, 1, c, 12); + for (let g = S.ghosts.length - 1; g >= 0; g--) + for (const p of S.ghosts[g].pts) { + const x = plx(p[0]), y = ply(p[1]); + if (y > PT - 3 && y < PB + 2) for (let q = 0; q < 4; q++) add(S, round(x - 0.5) + (q & 1), round(y - 0.5) + (q >> 1), g ? 0.5 : 0.75, c, 15); + } + for (const p of mine) { + const x = round(plx(p[0]) - 0.5), y = round(ply(p[1]) - 0.5); + if (y > PT - 3 && y < PB + 2) for (let q = 0; q < 4; q++) add(S, x + (q & 1), y + (q >> 1), 1, c, 3); + } + // The walk under way: its length so far rising as a bar at its opening. + const xo = plx(log10(w.s)), sofar = log10(max(1e-6, (m + fr) * w.s + (done ? w.gap : 0)) / cur.l0); + for (let y = PB; y >= max(PT - 3, ply(sofar)); y -= 1) splat(S, xo, y, 0.4, c, 9); + + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) + display.setPixel(x, y, round(min(1, R[p]) * 255), round(min(1, G[p]) * 255), round(min(1, B[p]) * 255)); +} From 4b7dcd18cdc0f146b3268547650e5da44aed37c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 12:10:04 +0000 Subject: [PATCH 3/9] pattern study: Phase Spiral and Giant Component, round 60 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015ttLM9zxviQho5eHL89da2 --- .../pattern-study/patterns/giant-component.js | 221 ++++++++++++++++++ .../pattern-study/patterns/phase-spiral.js | 201 ++++++++++++++++ 2 files changed, 422 insertions(+) create mode 100644 incoming/pattern-study/patterns/giant-component.js create mode 100644 incoming/pattern-study/patterns/phase-spiral.js diff --git a/incoming/pattern-study/patterns/giant-component.js b/incoming/pattern-study/patterns/giant-component.js new file mode 100644 index 000000000..3dc24b814 --- /dev/null +++ b/incoming/pattern-study/patterns/giant-component.js @@ -0,0 +1,221 @@ +// ===== Patternflow pattern ===== +// Title: Giant Component +// Family: data +// Idea: Random edges join the dots of a 21 by 32 grid one at a time, every component wearing a colour of its own and the largest one cream, and just after the edges pass half the number of dots the cream giant component floods the panel, while a plot underneath draws its share against the edges added over the curve Erdős and Rényi's theory gives. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: random graph phase transition (Paul Erdős and Alfréd Rényi, 1959 and 1960), explosive percolation (Dimitris Achlioptas, Raissa D'Souza and Joel Spencer, 2009), pattern round 60 +// Round: 60 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Size=-1..1, Choice=0..1 +// +// Knob 1: Hue. Turns the components' colours round the wheel; a vertex alone stays a dim slate dot and the largest component stays cream. At 0 the components take hues from azure through blue and violet to pink, at 0.25 from violet through red to lime, at 0.5 from rose through lime and green to cyan, at 0.75 from green through cyan and blue to violet; orange and dark yellow are skipped, as they read brown. The plot's dim lines turn with them, blue at 0. +// Knob 2: Speed. Edges added a second, 0.064 x Speed x n: at 2 the first graph opens with 0.2 n edges already in, passes the threshold n / 2 at 2.3 s and reaches 1.28 n, where the plot ends, at 8.4 s; it holds 2 s and a fresh graph starts from no edges, every 12 s. At 0.2 ten times slower, at 3 one and a half times faster. Each new edge is drawn as a line for 0.25 s at Speed 2, faint when it only brings in a vertex that was alone, brighter the larger the smaller of the two components it joins. +// Knob 3: Size. The grid, rounded: at -1 16 x 24 = 384 vertices as 3 x 3 LED dots 4 LEDs apart, at the opening 0 21 x 32 = 672 as 2 x 2 dots 3 apart, at 1 32 x 48 = 1536 single LEDs 2 apart. The larger the graph, the sharper the turn: over 200 graphs each, the largest component held 11.9, 10.4 and 8.0 percent of the vertices at n / 2 edges, falling as n^(-1/3), and at 0.75 n edges 57.5, 57.8 and 58.2 percent against the 58.3 of theory. A new value starts a fresh graph. +// Knob 4: Choice. The chance that an edge is chosen by the product rule of Achlioptas, D'Souza and Spencer: two candidates are drawn and the one whose two components have the smaller product of sizes is kept. It opens at 0.06, almost the plain random graph. At 1 the components stay small and many past n / 2, the giant waits until 0.83 n edges and then grows from a tenth of the vertices to seven tenths in 0.07 n more, 0.5 s at Speed 2, where the plain graph takes 0.35 n and 2.75 s, and the plot rises as a cliff. Over 200 graphs of 672 vertices the edges it took the largest component to grow from the square root of n to n / 2 were 0.30 n with no choice and 0.16 n with it, and for 200000 vertices, off the panel, 0.23 n and 0.022 n: the jump grows sharper without end, explosive percolation. +// Buttons: K1 tap starts a fresh graph from no edges. + +const W = 64, H = 128, NP = W * H; +const { floor, min, max, exp, log2, round, random, abs } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The vertex grids: spacing, columns, rows, dot size, left margin. Each +// stands 95 rows tall over the top of the panel, one dark LED between dots. +const GRIDS = [[4, 16, 24, 3, 0], [3, 21, 32, 2, 1], [2, 32, 48, 1, 0]]; +const NMAX = 32 * 48; +// The plot below them: edges added across, 0 to TMAX n over its 64 +// columns, the largest component's share up, 0 on row PY0 and 1 PH rows +// higher. +const PY0 = 126, PH = 25, PC = 64, TMAX = 1.28; +const RATE = 0.064, T0 = 0.2, HOLD = 2, FADE = 0.6, LIFE = 0.25, NL = 3; + +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; +} +// Orange and yellow read brown and olive on an LED unless they are bright, +// so the components take their hues from a wheel without 0.03 to 0.17, and +// the dim lines of the plot from one that runs only from green to rose. +const wheel = (h) => 0.17 + 0.86 * (h - floor(h)); +const dimWheel = (h) => 0.3 + 0.7 * (h - floor(h)); + +// The share of vertices in the giant component of G(n, m) as n grows, +// m = t n: the root theta of theta = 1 - exp(-2 t theta), zero up to t = 1/2. +const THEORY = new Float32Array(PC); +for (let c = 0; c < PC; c++) { + const k = (2 * (c + 0.5) * TMAX) / PC; + let th = 1; + for (let i = 0; i < 400; i++) th = 1 - exp(-k * th); + THEORY[c] = k <= 1 ? 0 : th; +} + +export function setup(params) { + params.s = { + par: new Int32Array(NMAX), sz: new Int32Array(NMAX), hu: new Float32Array(NMAX), + share: new Float32Array(PC), lines: new Int32Array(NL * 3), age: new Float32Array(NL).fill(9), nl: 0, + rgb: new Float32Array(NP * 3), old: new Float32Array(NP * 3), c: new Float32Array(3), + n: 0, grid: 1, t: 0, m: 0, big: 1, bigRoot: 0, hold: 0, fade: 0, started: false, + hue: 0, speed: 2, size: 1, choice: 0.06, + }; +} + +// Union-find, with path halving and union by size. +function find(par, a) { + while (par[a] !== a) a = par[a] = par[par[a]]; + return a; +} + +function fresh(S) { + const g = GRIDS[S.size]; + S.grid = S.size; + S.n = g[1] * g[2]; + for (let v = 0; v < S.n; v++) (S.par[v] = v), (S.sz[v] = 1), (S.hu[v] = random()); + S.share.fill(-1); + S.t = S.m = S.hold = S.nl = 0; + S.big = 1; + S.bigRoot = 0; + S.age.fill(9); +} + +// One edge. With probability Choice two candidate edges are drawn and the +// one whose two components have the smaller product of sizes is kept, the +// product rule of Achlioptas, D'Souza and Spencer; otherwise one edge, +// uniformly at random, as in G(n, m). The line it leaves remembers the +// smaller of the two components it joined, 0 for an edge inside one. +function addEdge(S) { + const { par, sz } = S, n = S.n; + let a = floor(random() * n), b = floor(random() * (n - 1)); + if (b >= a) b++; + if (random() < S.choice) { + let c = floor(random() * n), d = floor(random() * (n - 1)); + if (d >= c) d++; + if (sz[find(par, c)] * sz[find(par, d)] < sz[find(par, a)] * sz[find(par, b)]) (a = c), (b = d); + } + let ra = find(par, a), rb = find(par, b), small = 0; + if (ra !== rb) { + if (sz[ra] < sz[rb]) { + const t = ra; + ra = rb; + rb = t; + } + small = sz[rb]; + par[rb] = ra; + sz[ra] += small; + if (sz[ra] >= S.big) (S.big = sz[ra]), (S.bigRoot = ra); + } + const k = S.nl++ % NL; + S.lines[k * 3] = a; + S.lines[k * 3 + 1] = b; + S.lines[k * 3 + 2] = small; + S.age[k] = 0; + S.m++; +} + +// Edges up to t n, the largest share noted in the column t falls in. +function grow(S, t) { + S.t = min(TMAX, t); + const target = floor(S.t * S.n); + while (S.m < target) { + addEdge(S); + S.share[min(PC - 1, floor((S.m / S.n / TMAX) * PC))] = S.big / S.n; + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.size = cl(round(k[2]), -1, 1) + 1; + S.choice = cl(k[3], 0, 1); + // The first graph opens with T0 n edges already in, so the threshold + // comes 2.3 s in at Speed 2; the later ones start from no edges. + if (!S.started) { + fresh(S); + grow(S, T0); + S.started = true; + } + if (S.hold > HOLD || S.size !== S.grid || (input.btnPressed && input.btnPressed[0])) { + S.old.set(S.rgb); + S.fade = 1; + fresh(S); + } + const h = d * (S.speed / 2); + S.fade = max(0, S.fade - d / FADE); + for (let i = 0; i < NL; i++) S.age[i] += h; + if (S.t < TMAX) grow(S, S.t + 2 * h * RATE); + else S.hold += h; +} + +function add(rgb, x, y, r, g, b) { + if (x < 0 || y < 0 || x >= W || y >= H) return; + const o = (y * W + x) * 3; + rgb[o] += r; + rgb[o + 1] += g; + rgb[o + 2] += b; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { rgb, old, par, sz, hu, c } = S, g = GRIDS[S.grid], sp = g[0], nc = g[1], dot = g[3], ox = g[4], fd = S.fade, h = S.hue; + rgb.fill(0); + const cx = (v) => (v % nc) * sp + ox, cy = (v) => floor(v / nc) * sp; + // The vertices: the largest component cream, a vertex alone a dim slate + // dot, every other component a colour of its own that it keeps through + // each merge it wins, brighter as it grows. + const lift = dot === 1 ? 1.15 : 1; + for (let v = 0; v < S.n; v++) { + const r = find(par, v), s = sz[r]; + if (r === S.bigRoot && s > 1) hsv(h + 0.12, 0.12, 1, c, 0); + else if (s === 1) hsv(0.62, 0.4, 0.2 * lift, c, 0); + else hsv(wheel(h + 0.4 + 0.5 * hu[r]), 0.8, min(1, (0.6 + 0.3 * min(1, log2(s) / 5)) * lift), c, 0); + const x0 = cx(v), y0 = cy(v), R = c[0] * 255, G = c[1] * 255, B = c[2] * 255; + for (let yy = 0; yy < dot; yy++) for (let xx = 0; xx < dot; xx++) add(rgb, x0 + xx, y0 + yy, R, G, B); + } + // The newest edges, as lines between the centres of their dots: faint + // when they only bring in a vertex that was alone or close a cycle, + // bright when they join two components of some size. + const half = (dot - 1) / 2; + for (let i = 0; i < NL; i++) { + if (S.age[i] >= LIFE) continue; + const a = S.lines[i * 3], b = S.lines[i * 3 + 1], small = S.lines[i * 3 + 2]; + const w = (small ? 80 + 130 * min(1, log2(small) / 4) : 50) * (1 - S.age[i] / LIFE); + const ax = cx(a) + half, ay = cy(a) + half, bx = cx(b) + half, by = cy(b) + half; + const steps = max(1, round(max(abs(bx - ax), abs(by - ay)))); + for (let j = 0; j <= steps; j++) { + const u = j / steps; + add(rgb, round(ax + (bx - ax) * u), round(ay + (by - ay) * u), w, w, w); + } + } + // The plot: the threshold n / 2 dashed, the share the theory gives as n + // grows dim behind, and the share measured so far, with the ground under + // it tinted and its newest point brightest. + hsv(dimWheel(h + 0.5), 0.6, 0.45, c, 0); + const tc = floor((0.5 / TMAX) * PC), TR = c[0] * 255, TG = c[1] * 255, TB = c[2] * 255; + for (let y = PY0 - PH; y <= PY0; y += 2) add(rgb, tc, y, TR, TG, TB); + let last = -1; + for (let x = 0; x < PC; x++) { + add(rgb, x, PY0 + 1, 28, 28, 30); + add(rgb, x, round(PY0 - THEORY[x] * PH), TR * 0.85, TG * 0.85, TB * 0.85); + if (S.share[x] >= 0) last = x; + } + hsv(h + 0.12, 0.12, 1, c, 0); + for (let x = 0; x <= last; x++) { + const s = S.share[x] >= 0 ? S.share[x] : 0, y1 = round(PY0 - s * PH), w = x === last ? 255 : 190; + for (let y = y1 + 1; y <= PY0; y++) add(rgb, x, y, c[0] * 44, c[1] * 44, c[2] * 44); + add(rgb, x, y1, c[0] * w, c[1] * w, c[2] * w); + } + for (let p = 0, y = 0; y < H; y++) + for (let x = 0; x < W; x++, p += 3) { + let r = min(255, rgb[p]), gg = min(255, rgb[p + 1]), b = min(255, rgb[p + 2]); + if (fd > 0) (r = r * (1 - fd) + old[p] * fd), (gg = gg * (1 - fd) + old[p + 1] * fd), (b = b * (1 - fd) + old[p + 2] * fd); + (rgb[p] = r), (rgb[p + 1] = gg), (rgb[p + 2] = b); + display.setPixel(x, y, r, gg, b); + } +} diff --git a/incoming/pattern-study/patterns/phase-spiral.js b/incoming/pattern-study/patterns/phase-spiral.js new file mode 100644 index 000000000..a7c3b2b28 --- /dev/null +++ b/incoming/pattern-study/patterns/phase-spiral.js @@ -0,0 +1,201 @@ +// ===== Patternflow pattern ===== +// Title: Phase Spiral +// Family: sim +// Idea: Three thousand parallel sheets of mass fall through each other along a line, each pulled by a force that only changes when two of them cross, and their phase portrait, position across the panel and velocity up it, winds a cold strip into a two-armed spiral whose turns multiply and thin round a settling white core, every sheet coloured by where it began on the strip. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: one-dimensional self-gravitating sheet model (Michel Hénon, 1964; Frank Hohl and Marc Feix, 1967), violent relaxation (Donald Lynden-Bell, 1967), pattern round 60 +// Round: 60 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Spread=0.1..1.2, Lumps=0..1 +// +// Knob 1: Hue. Turns the ramp along the strip round the wheel. At 0 the sheet that starts at the left end is azure and the one at the right end rose, through blue, violet and magenta between; at 0.25 magenta through red to yellow, at 0.5 orange through yellow to sea green, at 0.75 green through cyan to blue. Where many sheets share an LED the colour goes toward white, so the core is pale at every setting. +// Knob 2: Speed. Sheet time per second, 1.6 x Speed: 3.2 at 2, where the cold strip falls to the middle in 0.49 s and, once it has settled, the sheet from the end of the strip goes round in 2.3 s and one from a tenth or a quarter of the way along in 1 s, and that difference is what winds the spiral; 0.32 at 0.2 and 4.8 at 3. A fresh strip starts every 72 units of sheet time, 22.5 s at 2 (the first one after 17.5 s, as it opens 16 units in). +// Knob 3: Spread. Half the height of the starting strip, its velocities spread evenly over plus and minus this, in units where the strip is 2 wide and a sheet with all the others on one side feels an acceleration of 1. Twice the kinetic energy over the potential starts at Spread squared: 0.01 at the opening 0.1, a cold collapse that winds the thinnest spiral; 1 at Spread 1, a waterbag in virial balance that only rocks and sheds two arms from its corners; 1.44 at 1.2, a hot one that swells first. A new value starts a fresh strip. +// Knob 4: Lumps. Bunches the strip's sheets into four lumps, moving each by 0.95 x Lumps x sin(4 pi u) / (4 pi) from its even place u: at 1 the strip is 39 times as dense in the middle of a lump as halfway between two. Each lump falls in on itself first and spins up a little spiral, and the four then fall together and merge, which keeps the cloud ringing: the virial ratio still swings between 0.3 and 3 after 36 units, where a smooth strip has settled within 25 percent of 1 by 16. It opens at 0.06, almost smooth. A new value starts a fresh strip. +// Buttons: K1 tap starts a fresh strip at once, falling from rest; the old spiral fades out over 0.9 s at Speed 2. + +const W = 64, H = 128, NP = W * H; +const { floor, min, max, exp, round, random, sin, abs, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// Units: the starting strip runs from x = -1 to 1, and a sheet with every +// other sheet on one side is pulled toward them at 1. Inside a uniform +// strip the pull is -x, so a cold strip would swing as a pendulum of period +// 2 pi and fall to a point at pi / 2; any spread of velocities or of +// density breaks that up. Phase space (x, v) is drawn with x across the +// panel, SX LEDs to a unit, and v up it, SV LEDs to a unit. +const N = 3072; +const XC = 32, VC = 64, SX = 22, SV = 44; +const DT = 0.04, RATE = 1.6, PRE = 16, CYCLE = 72, FADE = 3; + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +export function setup(params) { + params.s = { + x: new Float64Array(N), xs: new Float64Array(N), v: new Float64Array(N), a: new Float64Array(N), f: new Float32Array(N), + ord: new Int32Array(N), bin: new Int32Array(N), cnt: new Int32Array(N + 1), + px: new Float32Array(N), py: new Float32Array(N), fresh: true, used: [0.1, 0.06], + D: new Float32Array(NP), F: new Float32Array(NP), rgb: new Float32Array(NP * 3), old: new Float32Array(NP * 3), + lut: new Float32Array(256 * 3), t: 0, fade: 0, started: false, hue: 0, speed: 2, spread: 0.1, lumps: 0.06, + }; +} + +// The pull on every sheet, from its rank. A counting sort into N bins +// across the sheets' present extent puts them nearly in order, and an +// insertion sort mends the few that share a bin, so the order is exact and +// the cost does not grow with the number of crossings, about N^2 every +// period. A sheet with k sheets on its left and N - 1 - k on its right is +// pulled toward the heavier side by (N - 1 - 2k) / N, constant until the +// next crossing: between crossings every sheet flies a parabola. +function ranks(S, lo, hi) { + const { x, xs, a, ord, bin, cnt } = S, sc = (N - 1) / (hi - lo + 1e-12); + cnt.fill(0); + for (let i = 0; i < N; i++) { + const b = ((x[i] - lo) * sc) | 0; + bin[i] = b; + cnt[b + 1]++; + } + for (let b = 1; b <= N; b++) cnt[b] += cnt[b - 1]; + for (let i = 0; i < N; i++) { + const k = cnt[bin[i]]++; + ord[k] = i; + xs[k] = x[i]; + } + for (let k = 1; k < N; k++) { + const xk = xs[k]; + if (xs[k - 1] <= xk) continue; + const j = ord[k]; + let m = k - 1; + while (m >= 0 && xs[m] > xk) (xs[m + 1] = xs[m]), (ord[m + 1] = ord[m]), m--; + (xs[m + 1] = xk), (ord[m + 1] = j); + } + for (let k = 0; k < N; k++) a[ord[k]] = (N - 1 - 2 * k) / N; +} + +// A leapfrog step: kick with the pull of the last order, drift, sort again. +// Across one cycle from rest at DT the total energy moved by 1 part in +// 10^4; the coarser opening run, 0.05 through the first collapse and 0.1 +// after, lands within 0.4 percent of it. +function step(S, dt) { + const { x, v, a } = S; + let lo = 1e9, hi = -1e9; + for (let i = 0; i < N; i++) { + v[i] += dt * a[i]; + const xi = (x[i] += dt * v[i]); + if (xi < lo) lo = xi; + if (xi > hi) hi = xi; + } + ranks(S, lo, hi); + S.t += dt; +} + +// A fresh strip, a waterbag: the sheets evenly along x, bunched into four +// lumps by Lumps, and their velocities spread evenly over +-Spread by the +// golden-ratio sequence, which fills the rectangle without the clumps and +// holes of random draws. f is a sheet's place along the strip, 0 at the +// left end and 1 at the right, and picks its colour for good. +function fresh(S) { + const { x, v, f } = S, A = 0.95 * S.lumps, V0 = S.spread, r0 = random(); + for (let i = 0; i < N; i++) { + const q = (i + 0.5) / N, u = 2 * q - 1; + f[i] = q; + x[i] = u + (A / (4 * PI)) * sin(4 * PI * u); + v[i] = V0 * (2 * ((i * 0.6180339887 + r0) % 1) - 1); + } + S.t = 0; + S.used = [V0, S.lumps]; + ranks(S, -1, 1); + S.fresh = true; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0.1, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.spread = cl(k[2], 0.1, 1.2); + S.lumps = cl(k[3], 0, 1); + // The first strip opens 16 units in, past the violent part of its + // relaxation, so the spiral has two turns by 2 s; the later ones start + // from rest and show the collapse. + if (!S.started) { + fresh(S); + while (S.t < PRE) step(S, S.t < 3 ? 0.05 : 0.1); + S.started = true; + } + const moved = abs(S.spread - S.used[0]) > 0.01 || abs(S.lumps - S.used[1]) > 0.01; + if (S.t > CYCLE || moved || (input.btnPressed && input.btnPressed[0])) { + S.old.set(S.rgb); + S.fade = 1; + fresh(S); + } + const h = d * S.speed * RATE; + S.fade = max(0, S.fade - h / FADE); + const n = Math.ceil(h / DT - 1e-9); + for (let i = 0; i < n; i++) step(S, h / n); +} + +// Brightness and the share of white for a density d, the number of +// sheets on an LED, in steps of 1/32. +const NB = 512, BR = new Float32Array(NB), WH = new Float32Array(NB); +for (let e = 0; e < NB; e++) (BR[e] = (1 - exp(-(e + 0.5) / 32 / 0.35)) * 255), (WH[e] = cl(((e + 0.5) / 32 - 2) / 6, 0, 0.7)); + +function splat(D, F, px, py, w, f) { + const ix = floor(px), iy = floor(py); + if (ix < 0 || iy < 0 || ix >= W - 1 || iy >= H - 1) return; + const tx = px - ix, ty = py - iy, p = iy * W + ix; + const w00 = w * (1 - tx) * (1 - ty), w10 = w * tx * (1 - ty), w01 = w * (1 - tx) * ty, w11 = w * tx * ty; + D[p] += w00; + D[p + 1] += w10; + D[p + W] += w01; + D[p + W + 1] += w11; + F[p] += w00 * f; + F[p + 1] += w10 * f; + F[p + W] += w01 * f; + F[p + W + 1] += w11 * f; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { x, v, f, D, F, rgb, old, lut, px, py } = S, fd = S.fade; + for (let i = 0; i < 256; i++) hsv(S.hue + 0.55 + 0.4 * (i / 255), 0.8, 1, lut, i * 3); + D.fill(0); + F.fill(0); + // Each sheet is laid down bilinearly where it is now and, if it moved + // more than 0.8 LED since the last frame, halfway back as well, so an + // arm sliding along itself reads as a line and not a row of dots. An LED + // shows the mean place along the strip of the sheets on it. + for (let i = 0; i < N; i++) { + const qx = XC + x[i] * SX - 0.5, qy = VC - v[i] * SV - 0.5; + if (S.fresh) (px[i] = qx), (py[i] = qy); + const mx = 0.5 * (qx + px[i]), my = 0.5 * (qy + py[i]); + if ((qx - mx) ** 2 + (qy - my) ** 2 < 0.16) splat(D, F, qx, qy, 1, f[i]); + else splat(D, F, qx, qy, 0.5, f[i]), splat(D, F, mx, my, 0.5, f[i]); + (px[i] = qx), (py[i] = qy); + } + S.fresh = false; + for (let y = 0, p = 0; y < H; y++) + for (let x2 = 0; x2 < W; x2++, p++) { + const d = D[p], o = p * 3; + let r = 0, g = 0, b = 0; + if (d > 0.004) { + const q = min(255, round((F[p] / d) * 255)) * 3, e = min(NB - 1, (d * 32) | 0), br = BR[e], wh = WH[e]; + r = (lut[q] * (1 - wh) + wh) * br; + g = (lut[q + 1] * (1 - wh) + wh) * br; + b = (lut[q + 2] * (1 - wh) + wh) * br; + } + if (fd > 0) (r = r * (1 - fd) + old[o] * fd), (g = g * (1 - fd) + old[o + 1] * fd), (b = b * (1 - fd) + old[o + 2] * fd); + (rgb[o] = r), (rgb[o + 1] = g), (rgb[o + 2] = b); + display.setPixel(x2, y, r, g, b); + } +} From b96b878d3bf858b3e8bc9b7924ee624b555cda10 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 12:11:26 +0000 Subject: [PATCH 4/9] pattern study: Hough Transform and Jordan Curve, round 60 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015ttLM9zxviQho5eHL89da2 --- .../pattern-study/patterns/hough-transform.js | 384 +++++++++++++++++ .../pattern-study/patterns/jordan-curve.js | 389 ++++++++++++++++++ 2 files changed, 773 insertions(+) create mode 100644 incoming/pattern-study/patterns/hough-transform.js create mode 100644 incoming/pattern-study/patterns/jordan-curve.js diff --git a/incoming/pattern-study/patterns/hough-transform.js b/incoming/pattern-study/patterns/hough-transform.js new file mode 100644 index 000000000..fb8dc84a4 --- /dev/null +++ b/incoming/pattern-study/patterns/hough-transform.js @@ -0,0 +1,384 @@ +// ===== Patternflow pattern ===== +// Title: Hough Transform +// Family: algo +// Idea: Points land one by one on the top square, most of them on a few slowly turning line segments and the rest anywhere, and as each lands its sinusoid rho = x cos theta + y sin theta is swept column by column into the accumulator below while the line through the point at that theta turns round it above; the sinusoids of collinear points all cross at one bright peak, the strongest peaks are picked, and each one's line is drawn back across the square and its sinusoids tinted in one colour, while old points and their votes fade. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Hough transform (Paul Hough, 1962), its rho-theta form (Richard Duda and Peter Hart, 1972), pattern round 60 +// Round: 60 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Lines=2..6, Noise=0.3..0.9 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the accumulator glows deep blue through azure to white and the found lines are lime, magenta, gold, sky, violet and green; the accumulator keeps to a strip of the wheel without orange and yellow, which go brown when dim, so at 0.25 it is violet, at 0.5 rose and magenta, at 0.75 teal and green, while the lines go the whole way round. The points stay white. +// Knob 2: Speed. At 2 eight points land a second, the newest point's sinusoid sweeps the 64 columns in 0.11 s, a point and its votes fade by e every 6 s and are gone after 18 s, and each line turns 0.4 to 1 degree a second while its distance from the centre drifts 3 to 7 LEDs either way over 1.5 to 3.5 minutes; at 0.2 all of it ten times slower, a point every 1.25 s whose sweep takes 1.06 s, at 3 half as fast again. +// Knob 3: Lines. How many line segments the points come from and how many peaks are picked, 2 to 6, rounded. At 2, with the opening's noise, each line holds about 16 of the 46 points' worth of weight, and in three 2-minute runs the picked peaks found the lines 99% of the time, within 0.15 rad of theta and 4 LEDs of rho; with 4 lines 81%, with 6 lines 64%, and a third of the 6 peaks then stand on crossings of noise. +// Knob 4: Noise. The share of landing points that fall anywhere on the square instead of on a line: 0.3 at the opening, so the noise sinusoids lace the accumulator thinly in blue; at 0.6 the 2 lines are still found 87% of the time; at 0.9 one point in ten is on a line, the noise weaves a mesh whose crossings stand as high as the lines' peaks, and the 2 lines are found 20% of the time. + +const W = 64, H = 128, SQ = 64, NT = 64, NR = 64; +const { floor, min, max, round, abs, sin, cos, exp, random, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The accumulator's colours come off a strip of the wheel that skips red, +// orange and yellow, which turn brown and olive when dim: u from 0 to 1 +// runs green, cyan, blue, violet, magenta to rose. +const wheel = (u) => 0.34 + 0.63 * (u - floor(u)); + +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; +} + +// Image space is the top 64 x 64, with its origin at the centre of the +// square, between LEDs 31 and 32, and y down. A line is the set of points +// with x cos theta + y sin theta = rho, theta in [0, pi) across the 64 +// columns of the bottom square and rho in [-32, 32) down its 64 rows, one +// LED of rho a row. Only a point in a corner, more than 32 LEDs from the +// centre, has a sinusoid that leaves through the top or the bottom for a +// stretch of theta, and every line that passes within 32 LEDs of the +// centre has its bin. Crossing theta = pi turns (theta, rho) into (theta - pi, -rho): +// column 64 is column 0 with its rows upside down. +const R = 32, DT = PI / NT, C0 = 31.5; +const CE = new Float64Array(NT + 1), SE = new Float64Array(NT + 1); +for (let j = 0; j <= NT; j++) (CE[j] = cos(j * DT)), (SE[j] = sin(j * DT)); +const rowOf = (rho) => ((rho + R) / (2 * R)) * NR; + +// Points land at RATE a second and fade by e every TAU seconds, both in +// panel time, Speed 2 being 1; a point older than 3 TAU is gone. The +// accumulator is cast afresh every frame from the points there are, each +// with its weight e^(-age / TAU), so it is always the exact transform of +// the points on the square, and it fades as they do. FLASH is how long the +// newest point's sweep across the columns takes. +const RATE = 8, TAU = 6, FLASH = 0.85 / RATE, MAXP = 200, LMAX = 6; +// The colours the found lines and their peaks take, as hue offsets. +const LINE_HUES = [0.3, 0.86, 0.14, 0.55, 0.74, 0.4]; +// Where the six lines start in theta, as fractions of pi. +const BASE = [0.3, 0.66, 0.48, 0.12, 0.84, 0.2]; + +export function setup(params) { + const lines = []; + for (let i = 0; i < LMAX; i++) { + // Spread the lines out in theta, the first two a third of the way in + // from either end of the accumulator, and give each a turn of its own, + // so two peaks start apart and drift past each other only now and then. + // A line's points come from the stretch a to b of its chord across the + // square, which slides 0.12 of the chord either way. + lines.push({ + th: PI * (BASE[i] + 0.06 * random()), w: (random() < 0.5 ? -1 : 1) * (0.007 + 0.01 * random()), + r0: (random() * 2 - 1) * 14, ra: 3 + 4 * random(), rw: 0.03 + 0.04 * random(), rp: 6.3 * random(), + a: 0.04 + 0.3 * random(), b: 0.65 + 0.3 * random(), sp: 0.2 + 0.2 * random(), + }); + } + params.s = { + hue: 0, speed: 2, L: 2, q: 0.3, T: 0, acc: 0, warm: false, vref: 8, + lines, n: 0, newest: -1, + px: new Int8Array(MAXP), py: new Int8Array(MAXP), age: new Float32Array(MAXP), ln: new Int8Array(MAXP), own: new Int8Array(MAXP), + A: new Float32Array(NT * NR), + peaks: [], + rgb: new Float32Array(W * H * 3), c: new Float32Array(3), + }; +} + +// Where line i is now: its normal angle and its distance from the centre. +function lineNow(S, i) { + const L = S.lines[i]; + return [L.th + L.w * S.T, L.r0 + L.ra * sin(L.rw * S.T + L.rp)]; +} + +// Take point i out, moving the last point into its place. +function drop(S, i) { + const e = --S.n; + S.px[i] = S.px[e], S.py[i] = S.py[e], S.age[i] = S.age[e], S.ln[i] = S.ln[e]; + if (S.newest === e) S.newest = i; +} + +function spawn(S) { + // Drop the oldest point when the store is full. + if (S.n >= MAXP) { + let o = 0; + for (let i = 1; i < S.n; i++) if (S.age[i] > S.age[o]) o = i; + drop(S, o); + } + let x, y, li = -1; + if (random() < S.q) { + x = floor(random() * SQ); + y = floor(random() * SQ); + } else { + li = floor(random() * S.L); + const [th, rho] = lineNow(S, li), L = S.lines[li]; + const c = cos(th), s = sin(th), fx = rho * c, fy = rho * s; + // The chord of the line across the square, t along the direction + // (-sin, cos) from the foot of the normal, and the segment the points + // come from: a stretch of the chord that slides slowly along it. + let t0 = -99, t1 = 99; + for (const [p, d] of [[fx, -s], [fy, c]]) { + if (abs(d) < 1e-6) continue; + const ta = (-C0 - p) / d, tb = (C0 - p) / d; + t0 = max(t0, min(ta, tb)); + t1 = min(t1, max(ta, tb)); + } + if (t1 - t0 < 8) return; + const slide = 0.12 * sin(L.sp * S.T + L.rp); + const a = cl(L.a + slide, 0, 0.6), b = cl(L.b + slide, a + 0.3, 1); + const t = t0 + (t1 - t0) * (a + (b - a) * random()); + x = round(fx - s * t + C0); + y = round(fy + c * t + C0); + if (x < 0 || x >= SQ || y < 0 || y >= SQ) return; + } + const k = S.n++; + S.px[k] = x, S.py[k] = y, S.age[k] = 0, S.ln[k] = li; + S.newest = k; +} + +// One point's votes: in column j the sinusoid runs from rho(theta_j) to +// rho(theta_j+1), and the point's weight is spread evenly over the rows it +// passes, at least one row's width, so every column gets the same weight +// and a steep stretch of the curve is drawn as a dim run, not a dotted one. +function cast(A, x, y, w, nj) { + const X = x - C0 + 0.5, Y = y - C0 + 0.5; + let ra = rowOf(X * CE[0] + Y * SE[0]); + for (let j = 0; j < nj; j++) { + const rb = rowOf(X * CE[j + 1] + Y * SE[j + 1]); + let lo = min(ra, rb), hi = max(ra, rb); + ra = rb; + if (hi - lo < 1) { + const m = (lo + hi) / 2; + lo = m - 0.5; + hi = m + 0.5; + } + const k = w / (hi - lo); + for (let r = max(0, floor(lo)), e = min(NR - 1, floor(hi)); r <= e; r++) { + const o = min(hi, r + 1) - max(lo, r); + if (o > 0) A[r * NT + j] += k * o; + } + } +} + +// The accumulator at column j, row r, reading past the ends through the +// theta = pi seam. +function at(A, j, r) { + if (j < 0) (j += NT), (r = NR - 1 - r); + else if (j >= NT) (j -= NT), (r = NR - 1 - r); + return r < 0 || r >= NR ? 0 : A[r * NT + j]; +} + +// The L strongest peaks: bins at least as high as every bin within 3, over +// a quarter of the highest and over 2.5 points' worth of votes, refined to +// a fraction of a bin by a parabola through each axis's three bins. +function findPeaks(S) { + const A = S.A, cand = []; + let top = 0; + for (let i = 0; i < NT * NR; i++) top = max(top, A[i]); + const floorV = max(2.5, 0.25 * top); + for (let r = 0; r < NR; r++) + for (let j = 0; j < NT; j++) { + const v = A[r * NT + j]; + if (v < floorV) continue; + let ok = true; + for (let dr = -3; dr <= 3 && ok; dr++) + for (let dj = -3; dj <= 3; dj++) { + if (!dr && !dj) continue; + const u = at(A, j + dj, r + dr); + if (u > v || (u === v && (dr < 0 || (dr === 0 && dj < 0)))) { + ok = false; + break; + } + } + if (ok) cand.push([v, j, r]); + } + cand.sort((a, b) => b[0] - a[0]); + const out = []; + for (const [v, j, r] of cand) { + if (out.length >= S.L) break; + const l = at(A, j - 1, r), rr = at(A, j + 1, r), u = at(A, j, r - 1), d = at(A, j, r + 1); + const fj = j + cl((0.5 * (l - rr)) / (l - 2 * v + rr || -1), -0.5, 0.5); + const fr = r + cl((0.5 * (u - d)) / (u - 2 * v + d || -1), -0.5, 0.5); + out.push({ v, j, r, th: (fj + 0.5) * DT, rho: ((fr + 0.5) / NR) * 2 * R - R, slot: -1 }); + } + // Each peak keeps the colour of the peak nearest it last frame, within 6 + // bins; a new one takes a free colour. + const used = new Uint8Array(LMAX); + for (const p of out) { + let best = -1, bd = 36; + S.peaks.forEach((q, i) => { + if (q.slot < 0 || used[q.slot]) return; + let dj = abs(q.j - p.j), dr = abs(q.r - p.r); + if (dj > NT / 2) (dj = NT - dj), (dr = abs(NR - 1 - q.r - p.r)); + const dd = dj * dj + dr * dr; + if (dd < bd) (bd = dd), (best = i); + }); + if (best >= 0) p.slot = S.peaks[best].slot; + else for (let k = 0; k < LMAX; k++) if (!used[k] && !out.some((o) => o.slot === k)) { p.slot = k; break; } + if (p.slot >= 0) used[p.slot] = 1; + } + S.peaks = out; +} + +// How many columns of a point's sinusoid are cast: all 64 once its sweep +// of FLASH seconds is over. Only the newest point sweeps, so when the next +// one lands a sweep still running is finished at once. +const sweepAge = (S, i) => (i === S.newest ? S.age[i] : 1e9); +const sweepOf = (age) => min(NT, floor((NT * age) / FLASH) + 1); + +function step(S, d) { + S.T += d; + for (let i = 0; i < S.n; i++) S.age[i] += d; + for (let i = 0; i < S.n; ) + if (S.age[i] > 3 * TAU) drop(S, i); + else i++; + S.acc += d * RATE; + while (S.acc >= 1) { + S.acc -= 1; + spawn(S); + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 2, 0.3], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.L = round(cl(k[2], 2, 6)); + S.q = cl(k[3], 0.3, 0.9); + if (!S.warm) { + // Open on a square that has been filling for 12 s. + for (let i = 0; i < 360; i++) step(S, 1 / 30); + S.warm = true; + } + step(S, (d * S.speed) / 2); + S.A.fill(0); + // The newest point casts its votes column by column as its sweep passes. + for (let i = 0; i < S.n; i++) cast(S.A, S.px[i], S.py[i], exp(-S.age[i] / TAU), sweepOf(sweepAge(S, i))); + findPeaks(S); + // Each point within 1 LED of a found line belongs to it, the nearest if + // two are that close: those are the votes that made the peak. + for (let i = 0; i < S.n; i++) { + const X = S.px[i] - C0 + 0.5, Y = S.py[i] - C0 + 0.5; + let best = -1, bd = 1; + for (const p of S.peaks) { + const dd = abs(X * cos(p.th) + Y * sin(p.th) - p.rho); + if (p.slot >= 0 && dd < bd) (bd = dd), (best = p.slot); + } + S.own[i] = best; + } + let top = 0; + for (let i = 0; i < NT * NR; i++) top = max(top, S.A[i]); + // The brightness scale follows the highest bin slowly, so a peak that + // grows brightens instead of the whole accumulator dimming round it. + S.vref += (max(4, top) - S.vref) * min(1, d * 1.5); +} + +function add(S, x, y, r, g, b) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const p = (y * W + x) * 3, o = S.rgb; + o[p] += r, o[p + 1] += g, o[p + 2] += b; +} +function lift(S, x, y, c) { + const p = (y * W + x) * 3, o = S.rgb; + o[p] = max(o[p], c[0] * 255), o[p + 1] = max(o[p + 1], c[1] * 255), o[p + 2] = max(o[p + 2], c[2] * 255); +} +function put(S, x, y, r, g, b) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const p = (y * W + x) * 3, o = S.rgb; + o[p] = r, o[p + 1] = g, o[p + 2] = b; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const o = S.rgb, c = S.c, A = S.A, hu = S.hue; + o.fill(0); + // The accumulator: deep blue at a trace of a vote, azure, then white only + // near the top, on brightness (v / vref)^0.85, so one fresh noise + // sinusoid alone shows at about 45 of 255 and the peaks stand out of the + // band of flat sinusoids that the points near the centre lay along rho = 0. + for (let r = 0; r < NR; r++) + for (let j = 0; j < NT; j++) { + const v = A[r * NT + j] / S.vref; + if (v <= 0.004) continue; + const b = min(1, v) ** 0.85; + hsv(wheel(hu + 0.476 - 0.15 * b), 1 - (0.8 * max(0, b - 0.7)) / 0.3, 0.06 + 0.94 * b, c, 0); + put(S, j, SQ + r, c[0] * 255, c[1] * 255, c[2] * 255); + } + // The sinusoids of the points a found line owns, in that line's colour + // at their weight, each LED kept at the brighter of it and the heat, so + // the bundle runs into its peak and the peak stays white. + for (let i = 0; i < S.n; i++) { + if (S.own[i] < 0 || sweepAge(S, i) < FLASH) continue; + hsv(hu + LINE_HUES[S.own[i]], 0.85, 0.55 * exp(-S.age[i] / TAU) ** 1.5, c, 0); + const X = S.px[i] - C0 + 0.5, Y = S.py[i] - C0 + 0.5; + let ra = rowOf(X * CE[0] + Y * SE[0]); + for (let j = 0; j < NT; j++) { + const rb = rowOf(X * CE[j + 1] + Y * SE[j + 1]); + for (let r = floor(min(ra, rb)), e = floor(max(ra, rb)); r <= e; r++) if (r >= 0 && r < NR) lift(S, j, SQ + r, c); + ra = rb; + } + } + // The newest points' sinusoids in white over the columns cast so far, the + // cursor column brightest; a finished one fades over the next 1.5 sweeps. + // While a point sweeps, the line through it whose normal is at the + // cursor's theta turns round it on the square: each bin of the sinusoid + // is one of those lines. + const HOLD = 1.5 / RATE; + for (let i = 0; i < S.n; i++) { + const age = S.age[i]; + if (age >= FLASH + HOLD) continue; + const sa = sweepAge(S, i), nj = sweepOf(sa), a = sa < FLASH ? 1 : min(1, 1 - (age - FLASH) / HOLD); + const X = S.px[i] - C0 + 0.5, Y = S.py[i] - C0 + 0.5; + let ra = rowOf(X * CE[0] + Y * SE[0]); + for (let j = 0; j < nj; j++) { + const rb = rowOf(X * CE[j + 1] + Y * SE[j + 1]), g = (j === nj - 1 && sa < FLASH ? 255 : 200) * a; + for (let r = floor(min(ra, rb)), e = floor(max(ra, rb)); r <= e; r++) if (r >= 0 && r < NR) add(S, j, SQ + r, g, g, g); + ra = rb; + } + if (sa < FLASH) { + const th = (nj - 0.5) * DT, cs = cos(th), sn = sin(th), rho = X * cs + Y * sn; + for (let y = 0; y < SQ; y++) + for (let x = 0; x < SQ; x++) { + const dd = abs((x - C0 + 0.5) * cs + (y - C0 + 0.5) * sn - rho); + if (dd < 1) add(S, x, y, 90 * (1 - dd), 90 * (1 - dd), 90 * (1 - dd)); + } + } + } + // The found lines across the image square, each in its peak's colour, + // 2.4 LEDs wide and fading out from the middle. + for (const p of S.peaks) { + if (p.slot < 0) continue; + hsv(hu + LINE_HUES[p.slot], 0.85, 1, c, 0); + const cs = cos(p.th), sn = sin(p.th), str = 0.35 + 0.4 * min(1, p.v / S.vref); + for (let y = 0; y < SQ; y++) + for (let x = 0; x < SQ; x++) { + const dd = abs((x - C0) * cs + (y - C0) * sn - p.rho); + if (dd < 1.2) { + const a = (1 - dd / 1.2) * str * 255; + add(S, x, y, c[0] * a, c[1] * a, c[2] * a); + } + } + // Its peak: four LEDs in the same colour at the corners of the 5 x 5 + // round the bin, leaving the glow of the crossing inside them alone. + for (let dr = -2; dr <= 2; dr += 4) + for (let dj = -2; dj <= 2; dj += 4) { + let j = p.j + dj, r = p.r + dr; + if (j < 0) (j += NT), (r = NR - 1 - r); + else if (j >= NT) (j -= NT), (r = NR - 1 - r); + if (r >= 0 && r < NR) put(S, j, SQ + r, c[0] * 255, c[1] * 255, c[2] * 255); + } + } + // The points, white, as bright as their weight, 255 new and 50 when they + // go; the point sweeping has a ring. + for (let i = 0; i < S.n; i++) { + const w = exp(-S.age[i] / TAU), x = S.px[i], y = S.py[i], g = 40 + 215 * w; + put(S, x, y, g, g, g); + if (sweepAge(S, i) < FLASH) { + const e = 200; + for (const [dx, dy] of [[-2, 0], [2, 0], [0, -2], [0, 2], [-1, -1], [1, -1], [-1, 1], [1, 1]]) + if (y + dy >= 0 && y + dy < SQ) add(S, x + dx, y + dy, e, e, e); + } + } + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p += 3) display.setPixel(x, y, min(255, round(o[p])), min(255, round(o[p + 1])), min(255, round(o[p + 2]))); +} diff --git a/incoming/pattern-study/patterns/jordan-curve.js b/incoming/pattern-study/patterns/jordan-curve.js new file mode 100644 index 000000000..e271fd568 --- /dev/null +++ b/incoming/pattern-study/patterns/jordan-curve.js @@ -0,0 +1,389 @@ +// ===== Patternflow pattern ===== +// Title: Jordan Curve +// Family: topology +// Idea: One long winding simple closed curve fills the panel and slowly reshapes without ever touching itself; a probe wanders over it casting a ray to the far end of the panel, the ray's crossings with the curve are marked in gold and counted in pips along the top, and from the probe a flood of the colour the count's parity names, odd inside and even outside, spreads through the region it stands in until inside and outside come up as two interlocking territories. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Jordan curve theorem (Camille Jordan, 1887), its first proof accepted as complete (Oswald Veblen, 1905), the crossing-number test for a point in a polygon (M. Shimrat, 1962), pattern round 60 +// Round: 60 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Tension=-1..1, Probes=1..4 +// +// Knob 1: Hue. Moves both territories along a strip of the wheel that leaves out red, orange and yellow, which go brown when dim: at 0 the inside is teal and the outside violet, at 0.25 blue and crimson, at 0.5 violet and teal, at 0.75 crimson and blue. The curve stays white, the crossings and the count gold, the probe white. +// Knob 2: Speed. At 2 the probe walks 7 LEDs a second, a flood takes 250 LEDs a frame and rests 0.8 s once its region is full, fresh colour fades by e every 5 s down to a third, and the curve is offered 60 changes a second, of which about 7 cells take one, each sliding across in 0.7 s; at 0.2 all of it ten times slower, at 3 half as fast again. +// Knob 3: Tension. How much the curve's length costs: a cell changes side with Metropolis probability min(1, e^-dE), E = -beta L + 0.03 (A - A*)^2, L the lattice edges the curve crosses, and beta = 0.4 - 0.5 x Tension below 0 and 0.4 - 1.1 x Tension above. At -1 (beta 0.9) inside and outside are two trees of corridors one cell wide and the curve is about 1390 LEDs long; at the opening 0 (beta 0.4) it is about 1220 LEDs round a branching coral of 127 of the 242 free cells, and a ray crosses it 8 times on average, up to 18; at 0.5 about 940 LEDs; at 1 (beta -0.7) one lobed blob of about 310. +// Knob 4: Probes. 1 to 4, rounded, each with its own ray, its own row of pips and its own two floods, so with 4 both territories are refreshed most of the time. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, round, abs, sqrt, sin, cos, exp, random, PI } = 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; +} +// Region colours come off a strip of the wheel that skips red, orange and +// yellow, which turn brown and olive when dim. +const wheel = (u) => 0.34 + 0.63 * (u - floor(u)); + +// The curve is the zero set of a function that is linear on each triangle +// of a jittered triangular lattice of cells SP LEDs apart. A cell holds a +// value u in [-1, 1] and is inside where u is positive. Within a triangle +// whose corners are not all one sign the zero set is one straight segment, +// so the zero set is a union of disjoint simple closed curves and never +// touches itself. A cell changes side only when that keeps the cells +// inside one connected piece and the cells outside another, so there is +// exactly one curve. The cells within MARGIN of the panel's edge are held +// outside, so the outside reaches the edge all round and a ray that ends +// there ends outside. +const BETA0 = 0.4, LAMBDA = 0.03, FILL = 0.47; +// Tries a second, the seconds a changing cell takes to slide across, the +// floor and fade time of fresh colour, LEDs a flood takes a frame at Speed +// 1, the probe's speed, the ray's swing rate and a flood's rest. +const TRY = 60, SLIDE = 0.7, PFLOOR = 0.35, PTAU = 5, FLOOD = 125, VEL = 7, RAYW = 0.35, REST = 0.8; +const SP = 5.5, RH = (SP * Math.sqrt(3)) / 2, MARGIN = 4.5; +const NCOL = Math.ceil(W / SP) + 3, NROW = Math.ceil(H / RH) + 3, NC = NCOL * NROW; +// Six neighbours of each cell in turning order: right, up-right, up-left, +// left, down-left, down-right. +const NB = new Int32Array(NC * 6).fill(-1); +for (let j = 0; j < NROW; j++) + for (let i = 0; i < NCOL; i++) { + const c = j * NCOL + i, odd = j & 1; + const nb = [[i + 1, j], [i + odd, j - 1], [i - 1 + odd, j - 1], [i - 1, j], [i - 1 + odd, j + 1], [i + odd, j + 1]]; + nb.forEach(([a, b], k) => { + if (a >= 0 && a < NCOL && b >= 0 && b < NROW) NB[c * 6 + k] = b * NCOL + a; + }); + } +// Triangles: each cell with its right and down-right neighbours, and with +// its down-right and down-left ones. +const TRI = []; +for (let c = 0; c < NC; c++) { + const r = NB[c * 6], dr = NB[c * 6 + 5], dl = NB[c * 6 + 4]; + if (r >= 0 && dr >= 0) TRI.push(c, dr, r); + if (dr >= 0 && dl >= 0) TRI.push(c, dl, dr); +} +const NT = TRI.length / 3; + +export function setup(params) { + const CX = new Float32Array(NC), CY = new Float32Array(NC), FIXED = new Uint8Array(NC), LT = new Int32Array(N); + // The lattice is jittered once, by up to 0.22 of the spacing, small + // enough that no triangle turns over. + for (let j = 0; j < NROW; j++) + for (let i = 0; i < NCOL; i++) { + const c = j * NCOL + i, a = random() * 2 * PI, r = 0.22 * SP * sqrt(random()); + CX[c] = (i - 1.5 + (j & 1 ? 0.5 : 0)) * SP + (W - (NCOL - 3) * SP) / 2 + r * cos(a); + CY[c] = (j - 1.5) * RH + (H - (NROW - 3) * RH) / 2 + r * sin(a); + let full = true; + for (let k = 0; k < 6; k++) if (NB[c * 6 + k] < 0) full = false; + FIXED[c] = !full || CX[c] < MARGIN || CX[c] > W - 1 - MARGIN || CY[c] < MARGIN || CY[c] > H - 1 - MARGIN ? 1 : 0; + } + // Each LED's triangle and its three barycentric weights; and each + // triangle's gradient of the three weights, for the distance to the curve. + const LV = new Int32Array(N * 3).fill(-1), LW = new Float32Array(N * 3), GR = new Float32Array(NT * 6); + for (let t = 0; t < NT; t++) { + const a = TRI[3 * t], b = TRI[3 * t + 1], c = TRI[3 * t + 2]; + const x0 = CX[a], y0 = CY[a], x1 = CX[b], y1 = CY[b], x2 = CX[c], y2 = CY[c]; + const det = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0); + // d lambda_k / dx and / dy. + GR[6 * t] = (y1 - y2) / det, GR[6 * t + 1] = (x2 - x1) / det; + GR[6 * t + 2] = (y2 - y0) / det, GR[6 * t + 3] = (x0 - x2) / det; + GR[6 * t + 4] = (y0 - y1) / det, GR[6 * t + 5] = (x1 - x0) / det; + const xa = max(0, floor(min(x0, x1, x2))), xb = min(W - 1, Math.ceil(max(x0, x1, x2))); + const ya = max(0, floor(min(y0, y1, y2))), yb = min(H - 1, Math.ceil(max(y0, y1, y2))); + for (let y = ya; y <= yb; y++) + for (let x = xa; x <= xb; x++) { + const p = y * W + x; + if (LV[3 * p] >= 0) continue; + const l1 = ((x - x0) * (y2 - y0) - (x2 - x0) * (y - y0)) / det; + const l2 = ((x1 - x0) * (y - y0) - (x - x0) * (y1 - y0)) / det; + const l0 = 1 - l1 - l2; + if (l0 < -1e-6 || l1 < -1e-6 || l2 < -1e-6) continue; + LV[3 * p] = a, LV[3 * p + 1] = b, LV[3 * p + 2] = c; + LW[3 * p] = l0, LW[3 * p + 1] = l1, LW[3 * p + 2] = l2; + LT[p] = t; + } + } + const u = new Float32Array(NC).fill(-1), tgt = new Int8Array(NC).fill(-1); + // Start from one cell in the middle. + let c0 = 0, bd = 1e9; + for (let c = 0; c < NC; c++) { + const d = (CX[c] - W / 2) ** 2 + (CY[c] - H / 2) ** 2; + if (!FIXED[c] && d < bd) (bd = d), (c0 = c); + } + u[c0] = 1, tgt[c0] = 1; + let free = 0; + for (let c = 0; c < NC; c++) if (!FIXED[c]) free++; + params.s = { + hue: 0, speed: 2, tension: 0, nprobe: 1, warm: false, + LV, LW, GR, LT, FIXED, CX, CY, u, tgt, area: 1, free, busy: [], + f: new Float32Array(N), gt: new Float32Array(NT), + paint: new Float32Array(N), cls: new Int8Array(N), heal: new Int32Array(2 * N), glow: new Float32Array(N), side: new Int8Array(N), + probes: [], tryAcc: 0, rgb: new Float32Array(N * 3), c: new Float32Array(3), c2: new Float32Array(3), + }; +} + +// Can cell c change sides without changing the topology? Its neighbours, +// read round in turn, must be one run of in and one run of out: then adding +// it to either side glues a disc to that side along an arc. +function simple(S, c) { + let runs = 0; + const u = S.u; + for (let k = 0; k < 6; k++) { + const a = u[NB[c * 6 + k]] > 0, b = u[NB[c * 6 + ((k + 1) % 6)]] > 0; + if (a !== b) runs++; + } + return runs === 2; +} + +function inCount(S, c) { + let n = 0; + for (let k = 0; k < 6; k++) if (S.u[NB[c * 6 + k]] > 0) n++; + return n; +} + +// One Metropolis try: a cell, if simple and with no neighbour already +// moving, changes sides with probability min(1, e^-dE), where +// E = -beta L + LAMBDA (A - A*)^2, L the number of lattice edges the curve +// crosses, A the cells inside and A* = FILL of the free cells. No two +// neighbours move at once, so a cell that was simple when it set off is +// still simple when its value crosses zero. instant flips it at once (for +// the warm-up); otherwise its value starts to slide towards the other sign. +function attempt(S, beta, instant) { + const c = floor(random() * NC); + if (S.FIXED[c] || S.u[c] !== S.tgt[c]) return; + for (let k = 0; k < 6; k++) { + const n = NB[c * 6 + k]; + if (S.u[n] !== S.tgt[n]) return; + } + if (!simple(S, c)) return; + const k = inCount(S, c), isIn = S.u[c] > 0; + const dL = isIn ? 2 * k - 6 : 6 - 2 * k, dA = isIn ? -1 : 1; + const A0 = S.free * FILL, a = S.area - A0; + const dE = -beta * dL + LAMBDA * ((a + dA) ** 2 - a ** 2); + if (dE > 0 && random() >= exp(-dE)) return; + S.area += dA; + S.tgt[c] = isIn ? -1 : 1; + if (instant) S.u[c] = S.tgt[c]; + else S.busy.push(c); +} + +// A probe keeps a flood for each side, so a flood it left behind on +// crossing the curve runs on to the end of its region. +const flood = (sign, cls) => ({ sign, cls, q: new Int32Array(N), seen: new Int32Array(N), gen: 0, head: 0, tail: 0, rest: 0 }); +function newProbe(S, i) { + return { x: 12 + random() * 40, y: 16 + random() * 96, h: random() * 2 * PI, ray: PI / 2, down: 1, ph: random() * 2 * PI, F: [flood(1, 1), flood(-1, 2)], hits: [], i }; +} + +// The function at any point of the panel, read bilinearly off the LEDs. +function fAt(S, x, y) { + const f = S.f; + x = cl(x, 0, W - 1.001); + y = cl(y, 0, H - 1.001); + const i = floor(x), j = floor(y), a = x - i, b = y - j, p = j * W + i; + return (f[p] * (1 - a) + f[p + 1] * a) * (1 - b) + (f[p + W] * (1 - a) + f[p + W + 1] * a) * b; +} + +// March the probe's ray to the panel's edge in quarter-LED steps; every +// change of sign is a crossing, placed where the line through the two +// samples meets zero. A tangency the steps miss loses two crossings at +// once and leaves the parity alone. +function castRay(S, P) { + const dx = cos(P.ray), dy = sin(P.ray); + // From the centre of the LED the probe stands on, so the parity and the + // sign the flood follows are read at the same point. + P.sx = round(P.x); + P.sy = round(P.y); + let tmax = 1e9; + if (dx > 1e-6) tmax = min(tmax, (W - 0.5 - P.sx) / dx); + if (dx < -1e-6) tmax = min(tmax, (-0.5 - P.sx) / dx); + if (dy > 1e-6) tmax = min(tmax, (H - 0.5 - P.sy) / dy); + if (dy < -1e-6) tmax = min(tmax, (-0.5 - P.sy) / dy); + P.tmax = tmax; + P.hits.length = 0; + let prev = fAt(S, P.sx, P.sy); + for (let t = 0.25; t <= tmax; t += 0.25) { + const v = fAt(S, P.sx + dx * t, P.sy + dy * t); + if ((v > 0) !== (prev > 0)) P.hits.push(t - 0.25 + (0.25 * prev) / (prev - v)); + prev = v; + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 1], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.tension = cl(k[2], -1, 1); + S.nprobe = round(cl(k[3], 1, 4)); + const beta = BETA0 - (S.tension < 0 ? 0.5 : 1.1) * S.tension, ds = (d * S.speed) / 2; + if (!S.warm) { + // Grow the opening curve from one cell, 90000 tries at the knob's beta. + for (let i = 0; i < 90000; i++) attempt(S, beta, true); + S.warm = true; + } + // Shape: tries at TRY a second, each accepted one sliding its cell's + // value across zero in SLIDE seconds. + S.tryAcc += ds * TRY; + while (S.tryAcc >= 1) { + S.tryAcc -= 1; + attempt(S, beta, false); + } + const step = (2 * ds) / SLIDE; + for (let i = 0; i < S.busy.length; ) { + const c = S.busy[i], g = S.tgt[c]; + S.u[c] = g > 0 ? min(1, S.u[c] + step) : max(-1, S.u[c] - step); + if (S.u[c] === g) { + S.busy[i] = S.busy[S.busy.length - 1]; + S.busy.pop(); + } else i++; + } + // The function on every LED, and each triangle's gradient length. An LED + // the curve has swept over to the other side loses its colour. Fresh + // paint fades by e every PTAU seconds down to PFLOOR, the glow of a flood + // front by e every 0.15 s. + const { LV, LW, GR, u, f } = S; + const cls = S.cls, side = S.side, paint = S.paint, glow = S.glow, heal = S.heal; + const fade = min(1, ds / PTAU), gf = exp(-ds / 0.15); + for (let p = 0; p < N; p++) { + const q = 3 * p; + const v = (f[p] = LV[q] < 0 ? -1 : LW[q] * u[LV[q]] + LW[q + 1] * u[LV[q + 1]] + LW[q + 2] * u[LV[q + 2]]); + const sd = v > 0 ? 1 : -1; + if (sd !== side[p]) (cls[p] = 0), (side[p] = sd); + if (paint[p] > PFLOOR) paint[p] -= (paint[p] - PFLOOR) * fade; + glow[p] *= gf; + } + for (let t = 0; t < NT; t++) { + const a = u[TRI[3 * t]], b = u[TRI[3 * t + 1]], c = u[TRI[3 * t + 2]]; + const gx = a * GR[6 * t] + b * GR[6 * t + 2] + c * GR[6 * t + 4], gy = a * GR[6 * t + 1] + b * GR[6 * t + 3] + c * GR[6 * t + 5]; + S.gt[t] = sqrt(gx * gx + gy * gy) + 1e-6; + } + // A dark LED takes the colour of a coloured neighbour on its own side, one + // LED a frame, so colour creeps on within a region it has already reached + // and never crosses the curve. A region no flood has reached stays dark. + let nh = 0; + for (let p = 0; p < N; p++) { + if (cls[p]) continue; + const x = p & 63, sd = side[p]; + const q = x > 0 && side[p - 1] === sd && cls[p - 1] > 0 ? p - 1 + : x < W - 1 && side[p + 1] === sd && cls[p + 1] > 0 ? p + 1 + : p >= W && side[p - W] === sd && cls[p - W] > 0 ? p - W + : p < N - W && side[p + W] === sd && cls[p + W] > 0 ? p + W : -1; + if (q >= 0) (heal[nh++] = p), (heal[nh++] = q); + } + for (let i = 0; i < nh; i += 2) (cls[heal[i]] = cls[heal[i + 1]]), (paint[heal[i]] = paint[heal[i + 1]]); + while (S.probes.length < S.nprobe) S.probes.push(newProbe(S, S.probes.length)); + S.probes.length = S.nprobe; + const budget = round(FLOOD * S.speed); + for (const P of S.probes) { + // Wander: a heading that turns at random, turned back off the margin. + P.h += (random() - 0.5) * 6 * ds; + let vx = cos(P.h) * VEL * ds, vy = sin(P.h) * VEL * ds; + if (P.x + vx < 8 || P.x + vx > W - 9) (P.h = PI - P.h), (vx = -vx); + if (P.y + vy < 8 || P.y + vy > H - 9) (P.h = -P.h), (vy = -vy); + P.x += vx; + P.y += vy; + // The ray aims at the farther end of the panel, down from the top half + // and up from the bottom half with 32 rows of slack between, leans + // towards the middle column by up to 29 degrees and swings 29 degrees + // either way about that; it turns at most 1.5 radians a second, so a + // change of end sweeps it round through the horizontal. + if (P.down > 0 && P.y > 80) P.down = -1; + else if (P.down < 0 && P.y < 48) P.down = 1; + P.ph += RAYW * ds; + let dr = P.down * (PI / 2 + (0.5 * (P.x - 31.5)) / 31.5) + 0.5 * sin(P.ph) - P.ray; + dr -= 2 * PI * round(dr / (2 * PI)); + P.ray += cl(dr, -1.5 * ds, 1.5 * ds); + castRay(S, P); + // Odd: the probe is inside, and the inside's flood is the one it may + // start, once that flood has run out and rested REST seconds. + const Fh = P.F[P.hits.length & 1 ? 0 : 1], pl = P.sy * W + P.sx; + if (Fh.head >= Fh.tail && (Fh.rest -= ds) <= 0) { + Fh.rest = REST; + Fh.gen++; + Fh.head = Fh.tail = 0; + Fh.q[Fh.tail++] = pl; + Fh.seen[pl] = Fh.gen; + } + // The flood takes a random LED off its front each time, Eden's growth + // rather than a breadth-first wave, so in the open it spreads as a + // ragged disc instead of a diamond. + for (const F of P.F) + for (let n = 0; n < budget && F.head < F.tail; n++) { + const r = F.head + floor(random() * (F.tail - F.head)), p = F.q[r]; + F.q[r] = F.q[F.head++]; + if ((f[p] > 0 ? 1 : -1) !== F.sign) continue; + S.cls[p] = F.cls; + S.paint[p] = 1; + S.glow[p] = 1; + const x = p & 63, y = p >> 6; + if (x > 0) push(S, F, p - 1); + if (x < W - 1) push(S, F, p + 1); + if (y > 0) push(S, F, p - W); + if (y < H - 1) push(S, F, p + W); + } + } +} +function push(S, P, p) { + if (P.seen[p] === P.gen) return; + if ((S.f[p] > 0 ? 1 : -1) !== P.sign) return; + P.seen[p] = P.gen; + P.q[P.tail++] = p; +} + +function put(S, x, y, r, g, b) { + x = round(x); + y = round(y); + if (x < 0 || x >= W || y < 0 || y >= H) return; + const p = (y * W + x) * 3, o = S.rgb; + o[p] = r, o[p + 1] = g, o[p + 2] = b; +} +function lift(S, x, y, v) { + x = round(x); + y = round(y); + if (x < 0 || x >= W || y < 0 || y >= H) return; + const p = (y * W + x) * 3, o = S.rgb; + o[p] = max(o[p], v), o[p + 1] = max(o[p + 1], v), o[p + 2] = max(o[p + 2], v); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const o = S.rgb, c = S.c, c2 = S.c2, hu = S.hue; + // Inside and outside colours, at 150 fresh and 52 faded; an LED no flood + // has coloured is all but dark, and a flood front glows 70 brighter. + hsv(wheel(hu + 0.2), 0.85, 1, c, 0); + hsv(wheel(hu + 0.68), 0.75, 1, c2, 0); + for (let p = 0; p < N; p++) { + const q = 3 * p, kc = S.cls[p], a = S.paint[p] * 150, g = S.glow[p] * 70; + const col = kc === 1 ? c : c2; + let r = 0, gg = 0, b = 0; + if (kc) (r = col[0] * a + g), (gg = col[1] * a + g), (b = col[2] * a + g); + else r = gg = b = 8; + // The curve, white and about 1.7 LEDs wide: an LED's distance to the + // zero set is |f| over the gradient's length on its triangle. + const t = S.LT[p], dd = abs(S.f[p]) / S.gt[t], k = cl2(1 - dd / 0.85); + o[q] = r + (235 - r) * k, o[q + 1] = gg + (235 - gg) * k, o[q + 2] = b + (235 - b) * k; + } + // Each probe's ray, a pale line no brighter than the curve; its + // crossings as gold beads on the curve; its count as gold pips along the + // top, one row a probe; the probe a white cross. + for (const P of S.probes) { + const dx = cos(P.ray), dy = sin(P.ray); + for (let t = 1.5; t < P.tmax; t += 0.5) lift(S, P.sx + dx * t, P.sy + dy * t, 150); + for (const t of P.hits) { + const x = P.sx + dx * t, y = P.sy + dy * t; + for (const [ex, ey] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) put(S, x + ex, y + ey, 190, 120, 10); + put(S, x, y, 255, 200, 40); + } + for (let i = 0; i < P.hits.length; i++) put(S, 2 + 2 * (i % 30), 1 + 2 * P.i + 8 * floor(i / 30), 255, 200, 40); + for (const [ex, ey] of [[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]]) put(S, P.sx + ex, P.sy + ey, 255, 255, 255); + } + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p += 3) display.setPixel(x, y, min(255, round(o[p])), min(255, round(o[p + 1])), min(255, round(o[p + 2]))); +} +const cl2 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v); From de257696ef1a3c27ac876199fc671493c9bb6e9e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 12:12:33 +0000 Subject: [PATCH 5/9] pattern study: Rutherford Shadow and Euler's Officers, round 60 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015ttLM9zxviQho5eHL89da2 --- .../pattern-study/patterns/eulers-officers.js | 215 +++++++++++++ .../patterns/rutherford-shadow.js | 304 ++++++++++++++++++ 2 files changed, 519 insertions(+) create mode 100644 incoming/pattern-study/patterns/eulers-officers.js create mode 100644 incoming/pattern-study/patterns/rutherford-shadow.js diff --git a/incoming/pattern-study/patterns/eulers-officers.js b/incoming/pattern-study/patterns/eulers-officers.js new file mode 100644 index 000000000..c4e7167c4 --- /dev/null +++ b/incoming/pattern-study/patterns/eulers-officers.js @@ -0,0 +1,215 @@ +// ===== Patternflow pattern ===== +// Title: Euler's Officers +// Family: cells +// Idea: Two officer squares stand one above the other, each cell a ring in the colour of its rank and a core in the colour of its regiment: on top Euler's 36 officers in a best attempt where four cells at the corners of a rectangle repeat two pairs and pulse white, below an order-10 Graeco-Latin square where all 100 colour pairs appear once, while one regiment after another lights up to show it is a transversal of the ranks, one cell in every row, column and rank. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Euler's 36 officers problem (Leonhard Euler, 1782), no pair of order 6 (Gaston Tarry, 1900), orthogonal Latin squares of order 10, "Euler's spoilers" (R. C. Bose, S. S. Shrikhande and E. T. Parker, 1959), pattern round 60 +// Round: 60 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Core=-1..1, Shuffle=0..1 +// +// Knob 1: Hue. Turns every colour round the wheel together. At 0 the ten ranks and regiments of the bottom square are red, orange, yellow, lime, green, teal, azure, blue, violet and magenta, and the six of the top square red, amber, green, teal, azure and purple, spread round the same wheel. Each 0.1 moves the bottom square's colours one place on; at 0.5 every colour stands opposite where it was, red turned teal. +// Knob 2: Speed. At 2 the next regiment lights up every 1.5 s, so the ten transversals of the order-10 square come round in 15 s and the six regiments of the order-6 attempt in 9 s, and the clash cells fade to white and back every 2.4 s. At 0.2 all of it is ten times slower, at 3 half as fast again. +// Knob 3: Core. How much of a cell the regiment's core takes. At -1 there is no core and each square shows only its ranks, a Latin square on its own; at -0.5 the core is the centre LED of the 5 x 5 cells below and 3 x 3 of the 9 x 9 cells on top; at 0 3 x 3 and 5 x 5; at 0.5 the bottom square shows only regiments and the top one a 7 x 7 core in a ring one LED wide; at 1 both show only the regiments, the second Latin square. +// Knob 4: Shuffle. Swaps two rows or two columns of one square, both of its Latin squares together, which keeps every pair it had: 2.4 x K4 swaps a second at Speed 2, so one every 7 s at the opening 0.06 and none at 0. A swap fades the colours of the two lines into each other over 0.4 s, round the wheel so no LED dims, and puts white ticks at their ends. + +const W = 64, H = 128; +const { floor, min, max, round, cos, random, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// The squares: R gives each cell's rank row by row, G its regiment. +// Order 6 (top) is the best a search found for Euler's officers. It took 40 +// random rank squares and split each one every way into six regiments, and +// 15 of them reached 34 of the 36 pairs, 15 reached 32, three 31 and seven +// 30: a square with no transversal cannot do better than 6 x 5. 34 is the +// most there can be. Tarry's 1900 search of every case rules out 36, and 35 +// cannot happen either, since five regiments that are transversals leave a +// sixth that is one too. Here the four cells in rows 1 and 5, columns 3 and +// 6 (from the top left) hold only two pairs between them, amber with green +// and red with purple at Hue 0, each twice; red with green and amber with +// purple never appear. The code below counts the pairs and marks the cells +// of every pair it meets twice. +const R6 = "231450523104015243104532450321342015"; +const G6 = "012345253401301254140523435012524130"; +// Order 10 (bottom): the fifth random rank square the search drew had 872 +// transversals, and an exact-cover search picked 10 disjoint ones out of +// them; transversal k is regiment k. (The first four, with 804 to 892 +// transversals, gave no cover inside two million steps.) All 100 pairs +// appear once, which the same count finds here. +const R10 = "8054267913170842536951360427983285679041041953867268921504379647803125792138450645739162802360791854"; +const G10 = "3621054789217486039595034286171365792840573864902180429715636290135478795628310404873169524819507236"; + +// Ten hues placed by eye on the LED wheel, which crowds the greens: rank or +// regiment k of an order-n square sits at slot k / n + Hue, and a slot maps +// to a hue through these stops. +const STOPS = [0, 0.075, 0.15, 0.25, 0.37, 0.47, 0.56, 0.65, 0.76, 0.87, 1]; +function slotHue(t) { + t -= floor(t); + const i = floor(t * 10), f = t * 10 - i; + return STOPS[i] + (STOPS[i + 1] - STOPS[i]) * f; +} + +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; +} + +// Both squares are 59 LEDs square at columns 2..60. The top one has cells of +// 9 x 9 LEDs at a pitch of 10, rows 2..60; the bottom one 5 x 5 at a pitch +// of 6, rows 67..125. A dark LED line runs between the cells. +function makeSquare(n, ranks, regs, oy) { + const pitch = n === 6 ? 10 : 6, lit = pitch - 1, nn = n * n; + const A = new Int8Array(nn), B = new Int8Array(nn), clash = new Uint8Array(nn); + for (let i = 0; i < nn; i++) (A[i] = +ranks[i]), (B[i] = +regs[i]); + // Orthogonality, counted: a pair met twice marks both of its cells. + const seen = new Int16Array(nn); + for (let i = 0; i < nn; i++) seen[A[i] * n + B[i]]++; + let clashes = 0; + for (let i = 0; i < nn; i++) if (seen[A[i] * n + B[i]] > 1) (clash[i] = 1), clashes++; + const perm = () => Array.from({ length: n }, (_, i) => i); + return { + n, pitch, lit, ox: 2, oy, A, B, clash, clashes, rowP: perm(), colP: perm(), + // Display cell colours as slots, with the fade of a swap. + fromA: new Float32Array(nn), fromB: new Float32Array(nn), t0: new Float32Array(nn).fill(-9), + }; +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, core: 0, shuffle: 0.06, clock: 0, swapAcc: 0.5, + sq: [makeSquare(6, R6, G6, 2), makeSquare(10, R10, G10, 67)], + ticks: [], rgb: new Float32Array(W * H * 3), c: new Float32Array(6), + }; +} + +const FADE = 0.4; + +// The slot a display cell shows now, part way through a swap's fade. +function slotNow(S, q, i, from, to) { + const a = cl((S.clock - q.t0[i]) / FADE, 0, 1); + if (a >= 1) return to; + let d = to - from; + d -= round(d); + return from + d * (0.5 - 0.5 * cos(PI * a)); +} + +function swap(S) { + const q = S.sq[random() < 0.5 ? 0 : 1], n = q.n, rows = random() < 0.5; + const i = floor(random() * n); + let j = floor(random() * (n - 1)); + if (j >= i) j++; + const P = rows ? q.rowP : q.colP; + // Note where each touched cell's colours are, then swap the two lines. + for (let k = 0; k < n; k++) + for (const l of [i, j]) { + const d = rows ? l * n + k : k * n + l; + const r = rows ? P[l] : q.rowP[k], c = rows ? q.colP[k] : P[l]; + q.fromA[d] = slotNow(S, q, d, q.fromA[d], q.A[r * n + c] / n); + q.fromB[d] = slotNow(S, q, d, q.fromB[d], q.B[r * n + c] / n); + q.t0[d] = S.clock; + } + const t = P[i]; + P[i] = P[j]; + P[j] = t; + S.ticks.push({ q, rows, i, j, t0: S.clock }); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.core = cl(k[2], -1, 1); + S.shuffle = cl(k[3], 0, 1); + const ds = (d * S.speed) / 2; + S.clock += ds; + S.swapAcc += ds * 2.4 * S.shuffle; + while (S.swapAcc >= 1) { + S.swapAcc -= 1; + swap(S); + } + S.ticks = S.ticks.filter((t) => S.clock - t.t0 < FADE + 0.2); +} + +// How lit a regiment is: each one in turn for 1.5 s, rising and falling +// over the first and last 0.4 s. +const STEP = 1.5, FRAME = 0.25, BASE = 0.88, SAT = 0.88; +function glow(S, n, reg) { + const u = S.clock / STEP, now = floor(u) % n; + if (reg !== now) return 0; + const f = u - floor(u), e = min(f, 1 - f) / (0.4 / STEP); + return e >= 1 ? 1 : 0.5 - 0.5 * cos(PI * e); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const rgb = S.rgb, c = S.c; + rgb.fill(0); + // A cell's LED lies in the core when its square ring (Chebyshev distance + // from the cell's centre) is under rho times half the pitch. + const rho = (1 + S.core) / 2; + // The clash cells fade to white and back every 2.4 s; white keeps the + // LED at the level it had, so the pulse moves nothing on the map. + const pulse = 0.5 - 0.5 * cos((2 * PI * S.clock) / 2.4); + for (const q of S.sq) { + const n = q.n, half = (q.lit - 1) / 2, rc = rho * (half + 1); + for (let r = 0; r < n; r++) + for (let col = 0; col < n; col++) { + const d = r * n + col, cell = q.rowP[r] * n + q.colP[col]; + const sa = slotNow(S, q, d, q.fromA[d], q.A[cell] / n); + const sb = slotNow(S, q, d, q.fromB[d], q.B[cell] / n); + const g = glow(S, n, q.B[cell]); + // Cells stand at 0.88 of full and the lit regiment at full, with a + // faint white frame in the dark line round each of its cells. + const v = BASE + (1 - BASE) * g; + const whiten = q.clash[cell] ? pulse : 0; + hsv(slotHue(sa + S.hue), SAT * (1 - whiten), v, c, 0); + hsv(slotHue(sb + S.hue), SAT, v, c, 3); + const x0 = q.ox + col * q.pitch, y0 = q.oy + r * q.pitch, L = q.lit; + for (let yy = 0; yy < L; yy++) + for (let xx = 0; xx < L; xx++) { + const cheb = max(Math.abs(xx - half), Math.abs(yy - half)); + const p = ((y0 + yy) * W + x0 + xx) * 3, o = cheb < rc ? 3 : 0; + // The lit regiment's centre LED turns white: one white point in + // every row and every column. + const wh = cheb === 0 ? g : 0; + rgb[p] = wh * v + c[o] * (1 - wh); + rgb[p + 1] = wh * v + c[o + 1] * (1 - wh); + rgb[p + 2] = wh * v + c[o + 2] * (1 - wh); + } + if (g > 0) + for (let k = -1; k <= L; k++) + for (const [x, y] of [[x0 + k, y0 - 1], [x0 + k, y0 + L], [x0 - 1, y0 + k], [x0 + L, y0 + k]]) { + const p = (y * W + x) * 3; + rgb[p] = rgb[p + 1] = rgb[p + 2] = max(rgb[p], FRAME * g); + } + } + } + // White ticks at the two ends of each swapped row or column. + for (const t of S.ticks) { + const q = t.q, a = cl(1 - (S.clock - t.t0 - FADE) / 0.2, 0, 1) * 0.8; + const span = q.n * q.pitch - 1; + for (const l of [t.i, t.j]) + for (let s = 0; s < q.lit; s++) { + const along = l * q.pitch + s; + const ends = t.rows + ? [[0, q.oy + along], [1, q.oy + along], [q.ox + span + 1, q.oy + along], [q.ox + span + 2, q.oy + along]] + : [[q.ox + along, q.oy - 1], [q.ox + along, q.oy + span + 1]]; + for (const [x, y] of ends) { + if (x < 0 || x >= W || y < 0 || y >= H) continue; + const p = (y * W + x) * 3; + rgb[p] = rgb[p + 1] = rgb[p + 2] = max(rgb[p], a); + } + } + } + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p += 3) + display.setPixel(x, y, round(rgb[p] * 255), round(rgb[p + 1] * 255), round(rgb[p + 2] * 255)); +} diff --git a/incoming/pattern-study/patterns/rutherford-shadow.js b/incoming/pattern-study/patterns/rutherford-shadow.js new file mode 100644 index 000000000..cf4ec8d84 --- /dev/null +++ b/incoming/pattern-study/patterns/rutherford-shadow.js @@ -0,0 +1,304 @@ +// ===== Patternflow pattern ===== +// Title: Rutherford Shadow +// Family: physics +// Idea: A parallel beam of equal-energy particles streams up the panel past a repelling nucleus a third of the way up, each particle on its own Coulomb hyperbola with the nucleus at the outer focus, and since every hyperbola only touches the parabola with its focus at the nucleus and its vertex d below it, the beam leaves a sharp parabolic shadow above the nucleus, its edge lit by the orbits that bunch along it while the few head-on particles turn back down in gold. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Rutherford scattering, tan(theta/2) = d / 2b (Ernest Rutherford, 1911), after the gold-foil scattering of alpha particles (Hans Geiger and Ernest Marsden, 1909), pattern round 60 +// Round: 60 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Charge=1..6, Attract=0..1 +// +// Knob 1: Hue. Turns the beam's colours round the wheel together. At 0 particles that came in on the left of the nucleus are cyan, those on the right violet, and those turned back by more than 90 degrees gold; at 0.25 violet, red and green; at 0.5 red, lime and azure; at 0.75 lime, cyan and magenta. The nucleus stays warm white. +// Knob 2: Speed. At 2 a particle far from the nucleus moves 45 LEDs a second and crosses the panel in about 3 s, its trail about 10 LEDs long. At 0.2 all of it is ten times slower, at 3 half as fast again; the trails keep their length. +// Knob 3: Charge. The head-on turning distance d, in LEDs, which grows with the nucleus's charge over the beam's energy. The shadow is the parabola Y = X^2 / 4d - d, Y up the panel from the nucleus: at 1 it is 4 LEDs wide level with the nucleus and 37 wide on the top row, at 2 8 and 53, at 3 12 wide and filling the top 4 rows, at 6 24 wide and filling every row above row 49. A particle with impact parameter under d / 2 turns back by more than 90 degrees, 1 in 72 of the beam at 1 and 1 in 12 at 6. +// Knob 4: Attract. Turns the charge through zero: the turning distance is d cos(pi K4), 0.98 d at the opening 0.06, 0.71 d at 0.25, nothing at 0.5, where the beam runs straight through, and -d at 1, where the nucleus attracts as hard as it repelled. There the orbits bend in, cross behind the nucleus and fill the shadow, cyan into the violet half and violet into the cyan, and a particle that would pass within 0.35 LED of the centre is taken in, those with impact parameter under 0.69 LED at d = 1. + +const W = 64, H = 128, N = W * H; +const { floor, min, max, sqrt, sin, cos, atan, round, random, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +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; +} + +// Units: one LED, and one second at Speed 2. The nucleus sits at the corner +// of four LEDs, 42 rows above the bottom edge. Orbits are worked in its own +// frame: X to the right, Y up the panel (downstream), so an LED at panel +// (x, y) is X = x - XN, Y = YN - y. +const XN = 32, YN = 86; +const V0 = 45; // beam speed far from the nucleus, LEDs a second +const BMAX = 36; // impact parameters fill -36..36, 4 LEDs past each edge +const FLUX = 1.2; // particles a second per LED of beam width +const TAU = 0.1; // trail fade, seconds +const TRAIL = 0.9; // what a trail lays on one LED +const TAU_L = 3, GLOW = 0.016, GLOW_REF = 0.1; // the long exposure's fade and gain +const RNUC = 0.35; // an attracted particle this close is taken in +const CAP = 1400; + +// The shadow. Put X = r cos(phi), Y = r sin(phi) into the orbit equation +// below and it becomes b^2 - X b + (s / 2)(r + Y) = 0, a quadratic in the +// impact parameter b of the orbits through (X, Y). Two orbits, both from +// the same side, pass a point where X^2 > 2 s (r + Y); none where it is +// less; and on X^2 = 2 s (r + Y), which is the parabola Y = X^2 / 4s - s, +// the two meet in one, b = X / 2, which touches the parabola there. A +// separate leapfrog run of 2000 orbits from 4000 LEDs below, energy kept to +// 1e-9, found the nearest any came to the axis at heights -0.9 d to 80 +// LEDs equal to 2 sqrt(d (Y + d)) to four decimals, at b = X / 2 each time. +// In this pattern, 20 s at each of five repelling settings put one of +// 572,000 particle positions inside the parabola, by 0.00003 LED; every +// position lay within 0.026 LED of its exact hyperbola and every energy +// within 0.06% of V0^2 / 2. Attracting (s < 0) the discriminant +// X^2 + 2|s|(r + Y) never vanishes: through every point pass two orbits, +// one from each side, and there is no envelope and no shadow. Above about +// Y = d/2 the two even share the beam's own flux between them, so in the +// plane they cross behind the nucleus without bunching. + +export function setup(params) { + params.s = { + hue: 0, speed: 2, d: 1, q: 1, acc: 0, warm: false, n: 0, + X: new Float64Array(CAP), Y: new Float64Array(CAP), VX: new Float64Array(CAP), VY: new Float64Array(CAP), + B: new Float32Array(CAP), px: new Float32Array(CAP), py: new Float32Array(CAP), + R: new Float32Array(N), G: new Float32Array(N), Bl: new Float32Array(N), c: [0, 0, 0], + L: new Float32Array(N * 3), Lt: new Float32Array(N * 3), Lb: new Float32Array(N * 3), head: new Int16Array(N), + }; +} + +// Place a new particle exactly on its incoming orbit where it crosses Y. +// For impact parameter b > 0 and signed turning distance s (s > 0 repels), +// the orbit equation u'' + u = -s / 2b^2 with the particle coming in from +// phi = -pi/2 gives u = 1/r = cos(phi) / b - (s / 2b^2) (1 + sin(phi)). +// Energy and angular momentum b V0 then give the velocity. +function spawn(S, b, Y) { + if (S.n >= CAP) return; + const s = S.d * S.q, ab = b < 0 ? -b : b, sg = b < 0 ? -1 : 1; + const c = s / (2 * ab * ab); + const u = (ph) => cos(ph) / ab - c * (1 + sin(ph)); + let X, YY, vx, vy; + if (s === 0) { + X = ab; + YY = Y; + vx = 0; + vy = V0; + } else { + // y(phi) = sin(phi) / u(phi) rises from -infinity on the incoming leg + // up to the closest approach (repelled) or to the side of the nucleus. + let lo = -PI / 2 + 1e-12, hi = s > 0 ? -atan(s / (2 * ab)) : 0; + for (let i = 0; i < 44; i++) { + const m = 0.5 * (lo + hi); + if (sin(m) / u(m) < Y) lo = m; + else hi = m; + } + const ph = 0.5 * (lo + hi), uu = u(ph), r = 1 / uu; + X = r * cos(ph); + YY = r * sin(ph); + const v2 = V0 * V0 * (1 - s * uu), vt = ab * V0 * uu, vr = -sqrt(max(0, v2 - vt * vt)); + vx = vr * cos(ph) - vt * sin(ph); + vy = vr * sin(ph) + vt * cos(ph); + } + const i = S.n++; + S.X[i] = sg * X; + S.Y[i] = YY; + S.VX[i] = sg * vx; + S.VY[i] = vy; + S.B[i] = b; + S.px[i] = XN + S.X[i]; + S.py[i] = YN - S.Y[i]; +} + +function kill(S, i) { + const j = --S.n; + S.X[i] = S.X[j]; + S.Y[i] = S.Y[j]; + S.VX[i] = S.VX[j]; + S.VY[i] = S.VY[j]; + S.B[i] = S.B[j]; + S.px[i] = S.px[j]; + S.py[i] = S.py[j]; +} + +// Kick-drift-kick leapfrog in the nucleus's frame, the step cut so that no +// step moves a particle more than a twentieth of its distance from the +// nucleus or lets the force turn it by more than about that. +function advance(S, ds) { + const k = (S.d * S.q * V0 * V0) / 2, ak = k < 0 ? -k : k; + for (let i = 0; i < S.n; i++) { + let x = S.X[i], y = S.Y[i], vx = S.VX[i], vy = S.VY[i], t = ds, dead = false; + for (let it = 0; it < 4000 && t > 0; it++) { + const r2 = x * x + y * y, r = sqrt(r2), sp = sqrt(vx * vx + vy * vy) + 1e-9; + if (r < RNUC) { + dead = true; + break; + } + let h = 0.05 * (r / sp); + if (ak > 0) h = min(h, 0.05 * sqrt((r2 * r) / ak)); + h = min(h, t); + t -= h; + let f = k / (r2 * r); + vx += 0.5 * h * f * x; + vy += 0.5 * h * f * y; + x += h * vx; + y += h * vy; + const q2 = x * x + y * y; + f = k / (q2 * sqrt(q2)); + vx += 0.5 * h * f * x; + vy += 0.5 * h * f * y; + } + S.X[i] = x; + S.Y[i] = y; + S.VX[i] = vx; + S.VY[i] = vy; + const px = XN + x, py = YN - y; + if (dead || py < -3 || py > H + 4 || px < -4 || px > W + 4) { + kill(S, i); + i--; + } + } +} + +function feed(S, ds) { + // Poisson arrivals across the beam; each one starts a random part of this + // step's travel below the panel's bottom edge. + S.acc += ds * FLUX * 2 * BMAX; + while (S.acc >= 1) { + S.acc -= 1; + let b = (2 * random() - 1) * BMAX; + if (b > -0.01 && b < 0.01) b = 0.01; + spawn(S, b, YN - H - 1 - random() * V0 * ds); + } +} + +// One trail colour per particle: the side of the beam it came from, cyan on +// the left and violet on the right, and gold once it has been turned back +// by more than 90 degrees, tan(theta / 2) = |s| / 2|b| > 1. +function colour(S, b, out) { + const ab = b < 0 ? -b : b, s = S.d * S.q; + if ((s < 0 ? -s : s) > 2 * ab) hsv(S.hue + 0.12, 0.85, 1, out); + else hsv(S.hue + (b < 0 ? 0.52 : 0.78), 0.9, 1, out); +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 1, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0]; + S.speed = cl(kv[1], 0.2, 3); + S.d = cl(kv[2], 1, 6); + S.q = cos(PI * cl(kv[3], 0, 1)); + if (Math.abs(S.q) < 1e-6) S.q = 0; + if (!S.warm) { + // Open on the beam already flowing: 3.27 s of it, enough for a + // particle to cross all 128 rows. The first 2.93 s go in steps of + // 1/15 s and only lay the long exposure, weighted by how much of it + // would have faded by then; the trails need just the last 0.33 s. + S.warm = true; + for (let i = 0; i < 44; i++) step(S, 1 / 15, exp(-(43 - i) / 15 / TAU_L)); + for (let i = 0; i < 10; i++) step(S, 1 / 30, 0); + return; + } + step(S, (d * S.speed) / 2); +} + +function step(S, ds, early) { + for (let j = 0; j < S.n; j++) (S.px[j] = XN + S.X[j]), (S.py[j] = YN - S.Y[j]); + feed(S, ds); + advance(S, ds); + lay(S, ds, early); +} + +// Trails add, so where the orbits bunch the LEDs burn brighter. +function dep(S, x, y, r, g, b) { + const ix = floor(x), iy = floor(y); + if (ix < 0 || ix >= W || iy < 0 || iy >= H) return; + const p = iy * W + ix; + S.R[p] += r; + S.G[p] += g; + S.Bl[p] += b; +} + +// Fade the trails by one step and lay each particle's path over it, the +// path it took this step, dimmer towards where it was. An early step of the +// opening lays only the long exposure, at weight early. +function lay(S, ds, early) { + const R = S.R, G = S.G, B = S.Bl, c = S.c, L = S.L, fade = exp(-ds / TAU), fadeL = exp(-ds / TAU_L); + if (!early) { + for (let p = 0; p < N; p++) { + R[p] *= fade; + G[p] *= fade; + B[p] *= fade; + } + for (let p = 0; p < N * 3; p++) L[p] *= fadeL; + } + for (let i = 0; i < S.n; i++) { + const x1 = XN + S.X[i], y1 = YN - S.Y[i], x0 = S.px[i], y0 = S.py[i]; + colour(S, S.B[i], c); + const len = sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2), m = min(40, 1 + floor(len * 2)); + // Each sample stands for len / m of path, so a trail lays TRAIL on each + // LED of path it crosses, at any speed. + const share = (TRAIL * len) / m; + for (let j = 0; j < m; j++) { + const a = j / m, w = share * exp((-(1 - a) * ds) / TAU); + const x = x0 + (x1 - x0) * a, y = y0 + (y1 - y0) * a; + if (!early) dep(S, x, y, c[0] * w, c[1] * w, c[2] * w); + // The long exposure: path length, in the particle's colour. + const ix = floor(x), iy = floor(y); + if (ix >= 0 && ix < W && iy >= 0 && iy < H) { + const p = (iy * W + ix) * 3, e = (len / m) * (early || 1); + L[p] += c[0] * e; + L[p + 1] += c[1] * e; + L[p + 2] += c[2] * e; + } + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const R = S.R, G = S.G, B = S.Bl, L = S.L, Lb = S.Lb; + // The long exposure, blurred over 3 x 3 LEDs (across, then down), glows + // faintly under the trails: the beam's own density, dark in the shadow + // and brightest just outside its edge. + const T = S.Lt; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const q = (y * W + x) * 3, a = x > 0 ? q - 3 : q, b = x < W - 1 ? q + 3 : q; + for (let ch = 0; ch < 3; ch++) T[q + ch] = L[a + ch] + L[q + ch] + L[b + ch]; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const q = (y * W + x) * 3, a = y > 0 ? q - 3 * W : q, b = y < H - 1 ? q + 3 * W : q; + const r = T[a] + T[q] + T[b], g = T[a + 1] + T[q + 1] + T[b + 1], bl = T[a + 2] + T[q + 2] + T[b + 2]; + // Squared, so the glow is faint over the open beam and twice the + // density along the edge shows four times as bright. + const k = (max(r, g, bl) * GLOW * GLOW) / (81 * GLOW_REF); + Lb[q] = r * k; + Lb[q + 1] = g * k; + Lb[q + 2] = bl * k; + } + // Each head is a near-white LED drawn over its trail, left out of the + // trail itself so a slow beam does not smear it. + const head = S.head, c = S.c; + head.fill(-1); + for (let i = 0; i < S.n; i++) { + const x = floor(XN + S.X[i]), y = floor(YN - S.Y[i]); + if (x >= 0 && x < W && y >= 0 && y < H) head[y * W + x] = i; + } + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p++) { + // The nucleus: four warm white LEDs. + if ((x === XN - 1 || x === XN) && (y === YN - 1 || y === YN)) display.setPixel(x, y, 255, 236, 200); + else if (head[p] >= 0) { + colour(S, S.B[head[p]], c); + display.setPixel(x, y, round(140 + 115 * c[0]), round(140 + 115 * c[1]), round(140 + 115 * c[2])); + } else { + const q = p * 3; + display.setPixel(x, y, round(min(1, R[p] + Lb[q]) * 255), round(min(1, G[p] + Lb[q + 1]) * 255), round(min(1, B[p] + Lb[q + 2]) * 255)); + } + } +} From e8e037cb074af18ce45685f9a40042e6512e964b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 12:15:23 +0000 Subject: [PATCH 6/9] pattern study: Penrose Diagram and RANDU Planes, round 60 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015ttLM9zxviQho5eHL89da2 --- .../pattern-study/patterns/penrose-diagram.js | 251 ++++++++++++++++++ .../pattern-study/patterns/randu-planes.js | 176 ++++++++++++ 2 files changed, 427 insertions(+) create mode 100644 incoming/pattern-study/patterns/penrose-diagram.js create mode 100644 incoming/pattern-study/patterns/randu-planes.js diff --git a/incoming/pattern-study/patterns/penrose-diagram.js b/incoming/pattern-study/patterns/penrose-diagram.js new file mode 100644 index 000000000..107aecd48 --- /dev/null +++ b/incoming/pattern-study/patterns/penrose-diagram.js @@ -0,0 +1,251 @@ +// ===== Patternflow pattern ===== +// Title: Penrose Diagram +// Family: space +// Idea: All of flat spacetime folded into one triangle the panel's exact shape, the worldline r = 0 up the left edge and light at 45 degrees, its curves of constant time and radius crowding into the corners at infinity, while a now surface climbs from past to future infinity, gold pulses leave r = 0 once a unit of time, and an observer accelerating away carries a mirror that sends back every pulse sent before t = 0, later and later, while every pulse sent after runs past its horizon to future null infinity. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: conformal compactification of Minkowski spacetime (Roger Penrose, 1963), Penrose-Carter diagrams (Brandon Carter, 1966), hyperbolic motion (Max Born, 1909), the horizon of the uniformly accelerated frame (Wolfgang Rindler, 1966), pattern round 60 +// Round: 60 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Grid=0..2, Accel=-2..3 +// +// Knob 1: Hue. Turns the colours round the wheel together. At 0 the curves of constant time are blue, the curves of constant radius green, the pulses gold and the observer magenta; at 0.5 the time curves turn amber, the radius curves rose, the pulses blue and the observer green. The edges and the now surface stay white, the haze of curves along the null infinities slate, the region past the horizon a faint violet and the region behind the mirror a faint plum. +// Knob 2: Speed. How fast the now surface climbs. Its foot runs up the left edge at a constant pace on the panel, so t = tan(theta) in time: at 2 from i- to i+ in 6.3 s, then 0.7 s in which the pulses fade before the next climb; at 0.2 a climb takes 63 s, at 3 it takes 4.2 s. The first climb opens two ninths of the way up, at t = -1.2. +// Knob 3: Grid. The spacing of the grid curves and of the pulses, 2^-Grid: one unit of time and radius at 0, half a unit at 1, a quarter at 2. A pulse leaves r = 0 where a curve of constant time meets the left edge, leaving out any that would start within 4 LEDs of the last, so 11 pulses cross the panel at 0, 17 at 1 and 21 at 2. +// Knob 4: Accel. The observer's proper acceleration a = 2^(Accel - 1), from 1/8 to 4, opening at 0.52. Its worldline r^2 - t^2 = 1/a^2 crosses t = 0 at r = 1/a, 58 LEDs from the left edge at a = 1/8, 44 at the opening, 31.5 at a = 1, where it is the straight column R = pi/2, and 10 at a = 4. A pulse sent at t_k < 0 comes back to r = 0 at t = 1/(a^2 |t_k|): at the opening the one sent at t = -1 returns at 3.68 and the one at -9 at 0.41. + +const W = 64, H = 128, N = W * H; +const { floor, round, atan, tan, sqrt, max, min, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +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; +} + +// The diagram. With u = t - r and v = t + r, U = atan u and V = atan v, the +// coordinates T = U + V and R = V - U fill the triangle R >= 0, +// |T| + R <= pi. One LED is pi/63 in both, so i+ sits on LED (0, 0), i0 on +// (63, 63) and i- on (0, 126): the triangle is the panel's 64 x 127 LEDs, +// and a light ray, T - R or T + R constant, is an exact LED diagonal. +// Future null infinity is the diagonal x = y, past null infinity +// x + y = 126, and the worldline r = 0 is column 0. Row 127 lies outside. +const K = PI / 63; +const IN = new Uint8Array(N), TT = new Float64Array(N), RR = new Float64Array(N); +for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x, T = (63 - y) * K, R = x * K; + if (x + Math.abs(63 - y) >= 63) continue; + const u = tan((T - R) / 2), v = tan((T + R) / 2); + IN[p] = 1; + TT[p] = (u + v) / 2; + RR[p] = (v - u) / 2; + } +// An LED's four neighbours that are inside too, so a contour test reads +// only finite fields. +const NB = []; +for (let p = 0; p < N; p++) { + const x = p & 63, y = p >> 6, l = []; + if (IN[p]) for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { + const q = p + dy * W + dx; + if (x + dx >= 0 && x + dx < W && y + dy >= 0 && y + dy < H && IN[q]) l.push(q); + } + NB.push(l); +} + +// A level curve f = c drawn as LEDs one wide: an LED on the high side with +// a neighbour on the low side. Where two or more curves pass between an +// LED and its neighbour, toward the corners and along the null +// infinities, the curves crowd closer than the LEDs and the LED is marked +// haze (val + 2) instead: drawn dim, as the infinitely many curves there. +function contour(F, step, off, out, val) { + for (let p = 0; p < N; p++) { + if (!IN[p]) continue; + const f = floor((F[p] - off) / step); + let lo = 0, g = 0; + for (const q of NB[p]) { + lo = max(lo, f - floor((F[q] - off) / step)); + g = max(g, Math.abs(F[p] - F[q]) / step); + } + if (g > HAZE) { + if (out[p] === 0 || out[p] > 2) out[p] = val + 2; + } else if (lo === 1) out[p] = val; + } +} +const HAZE = 0.5; + +// Map an event (t, r) to its LED. +function led(t, r) { + const U = atan(t - r), V = atan(t + r); + return [round((V - U) / K), round(63 - (U + V) / K)]; +} + +export function setup(params) { + params.s = { + hue: 0, speed: 2, grid: -1, acc: 99, phase: 0.2, tn: -1e9, + G: new Uint8Array(N), O: new Uint8Array(N), now: new Uint8Array(N), sim: new Uint8Array(N), F: new Float64Array(N), gO: new Float64Array(N), ox: 0, oy: 0, + pulses: [], R: new Uint8Array(N), Gc: new Uint8Array(N), B: new Uint8Array(N), c: [0, 0, 0], + }; +} + +// The grid: 1 marks a curve of constant t, 2 of constant r, 3 and 4 their +// haze, all at the spacing d, and a pulse at each t = k d. +function buildGrid(S, d) { + S.G.fill(0); + contour(RR, d, 0, S.G, 2); + contour(TT, d, 0, S.G, 1); + S.pulses = []; + // Toward i+ and i- the rays crowd closer than an LED. Counting out from + // t = 0 both ways, a pulse whose ray would start within 4 LEDs of the + // last one kept is left out, so each ray drawn stands on its own. + for (const dir of [1, -1]) { + let last = 63; + for (let k = dir > 0 ? 0 : -1; Math.abs(k) <= 400; k += dir) { + const tk = k * d, c = round(63 - (2 * atan(tk)) / K); + if (k !== 0 && Math.abs(c - last) < 4) continue; + if (c < 2 || c > 124) break; + S.pulses.push({ tk, c }); + last = c; + } + } +} + +// The observer: r^2 - t^2 = 1/a^2, that is u v = -1/a^2, with u < 0. Read +// the triangle as the half x >= 0 of 1+1 Minkowski spacetime, x for r, and +// the observer, its mirror, its horizon u = 0 and its line of simultaneity +// are exact; read as the radial diagram of 3+1 spacetime, each LED is a +// sphere of radius r and each pulse a spherical shell. A pulse sent at +// t_k < 0 runs out along u = t_k, meets the observer at v = -1/(a^2 t_k) +// and comes back along that v; a pulse sent at t_k >= 0 never meets it. +// gO > 1 is the side of the worldline away from r = 0. +function buildObserver(S, a) { + S.O.fill(0); + const g = S.gO; + for (let p = 0; p < N; p++) if (IN[p]) g[p] = a * a * (RR[p] * RR[p] - TT[p] * TT[p]); + for (let p = 0; p < N; p++) { + if (!IN[p] || g[p] < 1) continue; + for (const q of NB[p]) + if (g[q] < 1) { + S.O[p] = 1; + break; + } + } +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + const grid = cl(k[2], 0, 2), acc = cl(k[3], -2, 3); + if (grid !== S.grid) { + S.grid = grid; + buildGrid(S, 2 ** -grid); + } + if (acc !== S.acc) { + S.acc = acc; + S.a = 2 ** (acc - 1); + buildObserver(S, S.a); + } + // One cycle is 14/speed seconds: 90% climbing, 10% fading. + S.phase += (d * S.speed) / 14; + if (S.phase >= 1) S.phase -= 1; + const climb = min(1, S.phase / 0.9); + const th = cl(-PI / 2 + PI * climb, -PI / 2 + 1e-4, PI / 2 - 1e-4); + S.tn = tan(th); + S.fade = S.phase < 0.9 ? 1 : 1 - (S.phase - 0.9) / 0.1; + S.now.fill(0); + contour(TT, 1e9, S.tn, S.now, 1); + // The observer's own now: its line of simultaneity at this event is + // t = r tanh(eta), eta its rapidity, a line through the origin event + // (0, 63) that runs to i0 and tips toward the horizon as eta grows. + const ro = sqrt(1 / (S.a * S.a) + S.tn * S.tn), beta = S.tn / ro, F = S.F; + for (let p = 0; p < N; p++) F[p] = IN[p] ? TT[p] - beta * RR[p] : 0; + S.sim.fill(0); + contour(F, 1e9, 0, S.sim, 1); + [S.ox, S.oy] = led(S.tn, ro); +} + +function put(S, p, h, s, v) { + const c = S.c; + hsv(h, s, v / 255, c); + S.R[p] = round(c[0] * 255); + S.Gc[p] = round(c[1] * 255); + S.B[p] = round(c[2] * 255); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const hu = S.hue, G = S.G, O = S.O, tn = S.tn, f = S.fade, ox = S.ox, oy = S.oy; + S.R.fill(0); + S.Gc.fill(0); + S.B.fill(0); + for (let p = 0; p < N; p++) { + if (!IN[p]) continue; + const x = p & 63, y = p >> 6; + // Above the horizon u = 0 lie the events the observer never sees, and + // behind the mirror, r^2 - t^2 > 1/a^2, those no pulse reaches. + if (x + y < 63) put(S, p, 0.78, 0.5, 34); + else if (S.gO[p] > 1) put(S, p, 0.9, 0.55, 30); + const g = G[p]; + if (g === 2) put(S, p, hu + 0.4, 0.8, 150); + else if (g === 1) put(S, p, hu + 0.6, 0.7, 170); + else if (g) put(S, p, 0.62, 0.35, 50); + if (O[p]) put(S, p, hu + 0.86, 0.75, 230); + else if (x + y === 63 && y >= 32 && (y & 1)) put(S, p, hu + 0.86, 0.6, 170); + } + // Pulses: each ray runs from (0, c) up the diagonal x + y = c, drawn as + // far as the now surface has come. The observer carries a mirror: a + // pulse sent before t = 0 meets it and comes back in along the diagonal + // x - y = constant to r = 0, arriving at t = v = -1/(a^2 t_k), later the + // closer t_k is to 0. A pulse sent after t = 0 never meets it and runs on + // to future null infinity at x = c / 2. + if (f > 0) + for (const P of S.pulses) { + if (P.tk > tn) continue; + let head = -1, x = 0, y = P.c; + for (; x <= 63; x++, y--) { + const p = y * W + x; + if (y < 0 || !IN[p] || TT[p] > tn || S.gO[p] >= 1) break; + put(S, p, hu + 0.13, 0.85, 215 * f); + head = p; + } + if (P.tk < 0 && x <= 63 && S.gO[y * W + x] >= 1 && TT[y * W + x] <= tn) { + // The echo turns at the last LED before the mirror and runs back up + // to r = 0. + for (x -= 2; x >= 0; x--, y--) { + const p = y * W + x; + if (TT[p] > tn) break; + put(S, p, hu + 0.11, 0.7, 215 * f); + head = p; + } + } + if (head >= 0 && f === 1) put(S, head, 0, 0, 255); + } + for (let p = 0; p < N; p++) if (S.now[p]) put(S, p, hu + 0.55, 0.12, 255); + // The edges: the worldline r = 0 and the two null infinities. + for (let y = 1; y < 126; y++) put(S, y * W, 0, 0, 190); + for (let x = 1; x < 63; x++) { + put(S, x * W + x, hu + 0.75, 0.55, 200); + put(S, (126 - x) * W + x, hu + 0.75, 0.55, 200); + } + for (const p of [0, 63 * W + 63, 126 * W]) put(S, p, 0, 0, 255); + if (f < 1) return out(display, S); + // The observer's own now: its line of simultaneity. + for (let p = 0; p < N; p++) if (S.sim[p] && !S.now[p]) put(S, p, hu + 0.86, 0.5, 230); + for (const [dx, dy] of [[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]]) { + const x = ox + dx, y = oy + dy; + if (x >= 0 && x < W && y >= 0 && y < H) put(S, y * W + x, 0, 0, 255); + } + out(display, S); +} + +function out(display, S) { + for (let p = 0; p < N; p++) display.setPixel(p & 63, p >> 6, S.R[p], S.Gc[p], S.B[p]); +} diff --git a/incoming/pattern-study/patterns/randu-planes.js b/incoming/pattern-study/patterns/randu-planes.js new file mode 100644 index 000000000..53d48065e --- /dev/null +++ b/incoming/pattern-study/patterns/randu-planes.js @@ -0,0 +1,176 @@ +// ===== Patternflow pattern ===== +// Title: RANDU Planes +// Family: number +// Idea: Consecutive triples of IBM's RANDU generator tumble as a cloud of points in a unit cube, and each time the view passes along the planes 9x - 6y + z = n the cloud snaps into the 15 parallel lines it always lay on, one colour per plane. +// Author: patternflow-lab +// Date: 2026-10-26 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: multiplicative congruential generator (D. H. Lehmer, 1949), RANDU (IBM, 1960s), the spectral test (R. R. Coveyou and R. D. MacPherson, 1967), random numbers fall mainly in the planes (George Marsaglia, 1968), pattern round 60 +// Round: 60 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Generator=0..3, Points=1.8..8 +// +// Knob 1: Hue. Turns the plane colours round the wheel. At 0 the 15 planes run from azure through violet, red and orange to green, top to bottom at one alignment and bottom to top at the next, as the cube turns over; the cube's edges stay a faint grey. +// Knob 2: Speed. How fast the cube tumbles. At 2 the view lines up with the planes every 3.2 s, first at 2.5 s, and lingers there: it turns at a tenth of its mean pace while the planes stand edge on, so the lines hold for about 0.4 s, and at 1.9 times it while they face the viewer. At 0.2 the alignments come 32 s apart, at 3 every 2.1 s. +// Knob 3: Generator. Picks the multiplier a of x -> a x mod m, and the view always passes along that generator's own widest planes. At 0 RANDU's 65539 = 2^16 + 3 puts the triples on 15 planes 5.2 LEDs apart; at 1 65541 = 2^16 + 5 on 35 planes 2.1 LEDs apart, the same trick one step on; at 2 George Marsaglia's 69069 mod 2^32 (1972) on 394 planes 0.16 LEDs apart, and at 3 the minimal standard 16807 mod 2^31 - 1 of Stephen Park and Keith Miller (1988) on 764 planes 0.09 LEDs apart, so for these two the cloud never lines up. +// Knob 4: Points. How many triples, in thousands: 1800 at the opening, 8000 at the top, where every line at an alignment is solid. + +const W = 64, H = 128, N = W * H; +const { floor, round, sqrt, sin, cos, abs, min, max, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +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; +} + +// The generators: multiplier, modulus, and the shortest integer u with +// u1 + a u2 + a^2 u3 = 0 mod m/g, found by lattice reduction, where g is +// the gcd of the differences between outputs: 2 for RANDU, 4 for the +// multipliers 5 mod 8 that keep x mod 4, 1 for the prime modulus. Every +// triple p = (x, a x mod m, a^2 x mod m) / m then has u . p in Z + c for +// one fixed c, so it lies on one of the planes u . p = n + c, 1/|u| +// apart. All products stay below 2^53, so the doubles are exact. +const GENS = [ + { a: 65539, m: 2 ** 31, u: [9, -6, 1] }, + { a: 65541, m: 2 ** 31, u: [25, -10, 1] }, + { a: 69069, m: 2 ** 32, u: [22, 13, -359] }, + { a: 16807, m: 2 ** 31 - 1, u: [90, -44, 631] }, +]; +const MAXP = 8000; +const LING = 0.9; +const mulmod = (a, x, m) => { + const y = a * x; + return y - floor(y / m) * m; +}; + +export function setup(params) { + params.s = { + hue: 0, speed: 2, gen: -9, count: 1800, psi: 0.687, + qh: new Float64Array(MAXP), qn: new Float64Array(MAXP), qw: new Float64Array(MAXP), col: new Uint8Array(MAXP), + ncol: 15, sc: 56, h: [1, 0, 0], n: [0, 1, 0], w: [0, 0, 1], + R: new Uint8Array(N), G: new Uint8Array(N), B: new Uint8Array(N), Z: new Float32Array(N), c: [0, 0, 0], + }; +} + +const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; +const unit = (a) => { + const l = sqrt(dot(a, a)); + return [a[0] / l, a[1] / l, a[2] / l]; +}; +const l1 = (a) => abs(a[0]) + abs(a[1]) + abs(a[2]); + +// The view: the cube tumbles about a screen-horizontal axis h that lies in +// the planes, so twice a turn the view looks along them and they stand +// edge on as horizontal lines stacked down the panel. Of all such axes, h +// is the one along which the cube is narrowest, so it fits the 64 LEDs. +function aim(S, g) { + const n = unit(g.u); + const e1 = unit(cross(n, abs(n[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0])), e2 = cross(n, e1); + let best = null; + for (let i = 0; i < 360; i++) { + const t = (i * PI) / 360, h = [0, 1, 2].map((j) => e1[j] * cos(t) + e2[j] * sin(t)); + if (!best || l1(h) < l1(best)) best = h; + } + S.n = n; + S.h = best; + S.w = cross(n, best); + S.sc = min(56, 62 / l1(best)); +} + +function build(S) { + const g = GENS[S.gen], m = g.m, u = g.u; + aim(S, g); + // Overlapping triples of consecutive outputs from the odd seed 12345. + let x = 12345, y = mulmod(g.a, x, m), z = mulmod(g.a, y, m); + const pl = new Int32Array(MAXP); + let lo = 1e9, hi = -1e9; + for (let k = 0; k < MAXP; k++) { + const p = [x / m - 0.5, y / m - 0.5, z / m - 0.5]; + S.qh[k] = dot(p, S.h); + S.qn[k] = dot(p, S.n); + S.qw[k] = dot(p, S.w); + pl[k] = floor((u[0] * x + u[1] * y + u[2] * z) / m + 0.125); + lo = min(lo, pl[k]); + hi = max(hi, pl[k]); + x = y; + y = z; + z = mulmod(g.a, z, m); + } + // One colour per plane where there are at most 15; past that the plane + // index mod 15, so neighbouring planes never share a colour. + const count = hi - lo + 1; + S.ncol = min(15, count); + for (let k = 0; k < MAXP; k++) S.col[k] = (pl[k] - lo) % 15; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + const gen = round(cl(k[2], 0, 3)); + if (gen !== S.gen) { + S.gen = gen; + build(S); + } + S.count = round(1000 * cl(k[3], 1.8, 8)); + // psi runs at a constant rate, pi every 3.2 s at Speed 2; the tumble + // angle phi = psi - LING sin(2 psi) / 2 is psi itself at each alignment + // (psi a multiple of pi) but moves there at a tenth of the pace, so the + // lines hold for about 0.4 s. + S.psi += (d * S.speed * PI) / 6.4; +} + +function put(S, x, y, z, r, g, b) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const p = y * W + x; + if (z < S.Z[p]) return; + S.Z[p] = z; + S.R[p] = r; + S.G[p] = g; + S.B[p] = b; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const phi = S.psi - (LING * sin(2 * S.psi)) / 2, cp = cos(phi), sp = sin(phi), sc = S.sc, c = S.c; + S.R.fill(0); + S.G.fill(0); + S.B.fill(0); + S.Z.fill(-9); + // The cube's edges, faint, behind everything. + const corner = (i) => { + const q = [(i & 1) - 0.5, ((i >> 1) & 1) - 0.5, ((i >> 2) & 1) - 0.5]; + const a = dot(q, S.n), b = dot(q, S.w); + return [31.5 + sc * dot(q, S.h), 63.5 - sc * (a * cp + b * sp)]; + }; + for (let i = 0; i < 8; i++) + for (const bit of [1, 2, 4]) { + if (i & bit) continue; + const [x0, y0] = corner(i), [x1, y1] = corner(i | bit); + const n = Math.ceil(max(abs(x1 - x0), abs(y1 - y0))); + for (let t = 0; t <= n; t++) put(S, round(x0 + ((x1 - x0) * t) / n), round(y0 + ((y1 - y0) * t) / n), -5, 34, 36, 44); + } + const pal = []; + for (let j = 0; j < 15; j++) { + hsv(S.hue + 0.55 + (0.8 * j) / max(1, S.ncol - 1), 0.85, 1, c); + pal.push([c[0], c[1], c[2]]); + } + for (let k = 0; k < S.count; k++) { + const a = S.qn[k], b = S.qw[k]; + // Screen right is h and up is n cos phi + w sin phi, so the depth + // toward the viewer, h x up, is n sin phi - w cos phi. + const y = 63.5 - sc * (a * cp + b * sp), z = a * sp - b * cp; + // Nearer points brighter: 0.5 at the back of the cube, 1 at the front. + const v = 255 * (0.75 + 0.29 * z), col = pal[S.col[k]]; + put(S, round(31.5 + sc * S.qh[k]), round(y), z, round(col[0] * v), round(col[1] * v), round(col[2] * v)); + } + for (let p = 0; p < N; p++) display.setPixel(p & 63, p >> 6, S.R[p], S.G[p], S.B[p]); +} From 6e8c46b9da9e3360938c10a166a625d27053d5e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 12:23:14 +0000 Subject: [PATCH 7/9] =?UTF-8?q?=EC=97=B0=EA=B5=AC=20=EB=85=B8=ED=8A=B8?= =?UTF-8?q?=EC=97=90=2060=ED=8C=90=20=EC=97=B4=20=EA=B0=9C=EC=99=80=20?= =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EC=B9=B8,=20=EB=8B=A4=EC=9D=8C=20?= =?UTF-8?q?=ED=8C=90=20=EA=B0=90=EC=9D=84=20=EB=8D=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015ttLM9zxviQho5eHL89da2 --- incoming/pattern-study/README.md | 73 ++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 85779c861..c79a9bb23 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 488개를 쉰여섯 갈래로 나눠 만들었어요. 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판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 498개를 쉰여섯 갈래로 나눠 만들었어요. 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판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 60판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. [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판에서 세로로 다시 짰어요. @@ -780,6 +780,21 @@ | [Hexagrammum Mysticum](patterns/hexagrammum-mysticum.js) | 선·궤적 | 타원 위를 미끄러지는 여섯 점의 육각형 60개가 1초에 하나씩 그려져요. 변이 마주 보는 변과 만날 때까지 뻗어 세 점에 고리가 켜지면 그 셋을 지나는 파스칼 선이 하얗게 번쩍이고, 세 번째 육각형마다 같은 색 세 선이 만나는 슈타이너 점이 금빛으로 피어요. | | [Print Gallery](patterns/print-gallery.js) | 프랙탈 | 두 배씩 커지는 사각 고리 넷에 체커를 깐 드로스테 그림을 드 스밋과 렌스트라의 사상으로 비틀어, 네 빛깔 고리가 나선 팔 넷으로 이어지고 흰 액자는 나선 선 하나가 돼요. 5.6초마다 10.2배 확대와 59도 회전을 마치면 LED 하나 다르지 않은 같은 그림으로 돌아와요. | +### 60판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [RANDU Planes](patterns/randu-planes.js) | 수론 | IBM 의 RANDU 가 낸 연속한 세 수가 정육면체 속 점 구름으로 구르다가, 시선이 평면 9x − 6y + z = n 을 따라 지나는 순간 열다섯 색의 가로줄 열다섯 개로 접혀요. | +| [Euler's Officers](patterns/eulers-officers.js) | 보로노이·셀 | 위에는 오일러의 36 장교를 세운 6 × 6 의 가장 나은 시도가, 아래에는 100쌍이 한 번씩 서는 10 × 10 그레코-라틴 방진이 있고, 칸마다 바깥 고리가 계급 색, 안쪽 심이 연대 색이에요. 연대가 1.5초마다 하나씩 밝아져 줄과 칸과 계급을 한 번씩 지나는 가로지르기를 보여 주고, 위 방진에서 두 쌍이 겹친 네 칸은 하얗게 맥박쳐요. | +| [Penrose Diagram](patterns/penrose-diagram.js) | 공간·원근 | 끝없는 평평한 시공간을 패널 꼴 그대로인 삼각형에 접어 넣었어요. 지금 면이 과거 무한대에서 미래 무한대로 오르는 동안, r = 0 에서 떠난 금빛 펄스가 가속하는 관찰자의 거울에 튕겨 돌아오거나 지평선 너머 미래 널 무한대로 빠져나가요. | +| [Rutherford Shadow](patterns/rutherford-shadow.js) | 물리·스프링 | 같은 에너지의 입자 빔이 아래에서 올라와 3분의 1 높이의 핵에 밀려 저마다 쿨롱 쌍곡선을 그리고, 어떤 궤도도 들어가지 못하는 포물선이 핵 위에 날카로운 그림자로 남아요. 그림자 가장자리에는 궤도가 몰려 빛나고, 정면으로 온 몇 개는 금색으로 되튀어 내려가요. | +| [Cornu Spiral](patterns/cornu-spiral.js) | 선·궤적 | 오일러 나선의 두 팔이 두 눈으로 감기고, 한 눈에서 나오거나 정해진 호만큼 떨어진 두 점을 잇는 현이 나선을 따라 미끄러져요. 그 현 길이의 제곱이 아래에 직선 모서리나 슬릿 뒤의 프레넬 무늬로 쌓여요. | +| [Richardson Dividers](patterns/richardson-dividers.js) | 프랙탈 | 통계적으로 자기 닮은 무작위 곡선을 디바이더가 정한 벌림으로 걸으며 같은 길이의 현으로 다각형을 놓아요. 벌림이 23 LED 에서 0.93 LED 로 줄 때마다 (log s, log L) 점이 아래 로그-로그 그림의 기울기 1 − D 직선 위에 쌓여요. | +| [Phase Spiral](patterns/phase-spiral.js) | 시뮬레이션 | 한 줄 위에서 서로 끌어당기는 면 3072개의 위치를 가로로, 속도를 세로로 찍어요. 차가운 띠가 무너지며 두 팔 나선으로 감기고, 감길수록 팔이 가늘어지는 동안 가운데 하얀 핵이 가라앉아요. | +| [Giant Component](patterns/giant-component.js) | 데이터 그림 | 점 672개에 무작위 변이 하나씩 더해지며 성분마다 제 색을 입다가, 변이 점 수의 절반을 넘자 크림색 거대 성분이 패널을 덮어요. 아래 그래프는 가장 큰 성분의 몫을 이론 곡선 위에 그리고, Choice 를 올리면 그 곡선이 절벽처럼 솟아요. | +| [Hough Transform](patterns/hough-transform.js) | 알고리즘 | 위 정사각형에 점이 하나씩 내려앉을 때마다 그 점의 사인 곡선이 아래 누산기로 왼쪽부터 쓸려 들어가요. 한 직선 위 점들의 곡선이 모두 만나는 봉우리를 골라 그 직선을 위에 같은 색으로 다시 그어요. | +| [Jordan Curve](patterns/jordan-curve.js) | 위상·매듭 | 패널을 채운 구불구불한 닫힌 곡선 위로 탐침이 돌아다니며 패널 끝까지 반직선을 쏘고, 곡선을 건넌 곳에 금색 점을 찍어 세요. 그 수가 홀수면 안의 색, 짝수면 밖의 색이 탐침에서부터 번져 안과 밖이 맞물린 두 땅으로 드러나요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -3030,6 +3045,50 @@ E8 의 근 240개를 고셋의 4₂₁ 다면체 꼭짓점으로 그렸어요. 주제 5 로 에스허르의 「판화 화랑」(1956) 뒤에 있는 등각 사상을 프랙탈 갈래에 세웠어요. 바르트 드 스밋과 헨드릭 렌스트라(2003)는 1/s 로 줄어든 자기를 품은 드로스테 그림이 log z 평면에서 격자 Z log s + Z 2πi 로 되풀이된다는 데서 출발해, log z 를 β = 2πi/(2πi + log s) 로 나누면 겹겹의 사본이 나선 하나로 이어진다는 것을 보였어요. 곱할지 나눌지는 2πi/β 가 log s + 2πi, 격자의 벡터가 되는지로 셈해서 나누는 쪽으로 정했어요. 바탕은 두 배씩 커지는 사각 고리마다 체커를 깔고 고리 m 개마다 색이 한 바퀴 돌며 그 첫 고리 안쪽에 흰 액자를 둔 그림이라, s = 2^m 이에요. 패널에서는 LED 마다 2 × 2, 가운데 20 LED 안에서는 3 × 3 표본으로 칸 모서리를 고르고, 한 LED 가 고리보다 넓어지는 가운데 몇 LED 는 평균색으로 녹여요. 처음엔 Scale 을 4..64 로 두어 s = 4 에서 열었더니 비틀림이 약해 기울어진 사각형으로만 보였고, 채움은 1.000 으로 축 끝에 붙고 한 장에 6.2 ms 가 걸렸어요. 그래서 범위를 16..256 으로 옮겨 에스허르의 256 을 끝에 두고, 칸 사이에 어두운 줄눈을 넣고, 3 × 3 표본은 가운데에만 남겼어요. Lab 이 K4 를 0.06 에서 여니 0 이 에스허르의 사상이 되도록 노브를 Unwind 로 뒤집었어요. 마지막 값은 움직임 2.137, 촘촘함 0.126, 가장자리 0.659, 복잡도 0.618, 채움 0.923, 색 0.468 이고 한 장에 4 ms 안팎이에요. Lab 에서 한 주기 떨어진 두 장은 s = 16, 32, 64, 256 모두 채널 24576개가 하나도 다르지 않았고, s = 256 의 비튼 그림은 22.58 배 확대와 157.6 도 회전에 무작위 점 2만 개의 칸이 다 그대로였어요. 사상의 야코비안은 회전과 확대에서 4e-9 안으로만 벗어났어요. 뜻밖이었던 것은 Unwind 가 만드는 틈이었어요. s = 16 에서 비틀림 0.25, 0.5, 0.75 의 틈은 꼭 고리 한 칸, 두 칸, 세 칸이라 왼쪽 반직선 너머의 색이 한 칸씩 밀리고, 1 에 이르러 네 칸, 한 주기가 되어서야 닫혀요. +## 60판 + +60판도 열 개 모두 주제 5 「수학 · 추상」이에요. 같이 돈 59판(#272)이 PR 본문에 밝힌 갈래와 수학, 지도 칸 둘을 읽고 피했고, 앞 바퀴 PR 61개에는 댓글도 리뷰도 없어서 따를 글이 없었어요. dev 의 488개에 59판이 고른 열 갈래를 하나씩 더해 세면, 추상 갈래 열다섯 가운데 기하·대칭과 물결·간섭이 열여덟이고 나머지 열셋이 다 열일곱이에요. 59판이 비켜 간 프랙탈과 선·궤적, 물리·스프링, 시뮬레이션에 하나씩 넣고, 남은 여섯은 열일곱인 공간·원근과 수론, 보로노이·셀, 데이터 그림, 알고리즘, 위상·매듭에 하나씩 넣었어요. 주제 1 은 488개에 빈 칸도 둘뿐인 칸도 없어서, 셋뿐인 칸 스물여섯 가운데 59판이 고른 둘을 빼고 움직임 × 채움 4,1 에 RANDU Planes 를, 움직임 × 색 1,4 에 Euler's Officers 를 세웠어요. 주제 4 는 Penrose Diagram 과 Rutherford Shadow 예요. 열 개 다 「흔한 것」에도 「낯선 수학 예」에도 없어요. Richardson Dividers 의 곡선은 코흐 꼴 생성기로 짓지만 조각마다 비율과 꼭짓점과 봉우리 쪽을 새로 뽑아서 코흐 눈송이로 읽히지 않고, 그림의 주인공은 곡선이 아니라 디바이더가 잰 길이와 그 로그-로그 기울기라서 「교과서 프랙탈」과 다르다고 봤어요. Jordan Curve 는 미로처럼 굽은 곡선이지만 격자에 벽을 파는 10 PRINT 와 달리 흔든 삼각 격자 위 함수의 0 집합이고, 길 대신 안과 밖 두 땅을 칠해서 흔한 것과 다르다고 봤어요. 열 개는 일꾼 다섯이 레포 밖 worktree 에서 둘씩 짓고, 접촉 시트(처음 노브를 시간에 따라 여섯 장, 노브마다 다섯 자리)로 하나씩 본 뒤 합쳤어요. + +### RANDU Planes + +주제 1 과 5 로 RANDU 의 평면을 수론 갈래에 세우고, 패턴이 셋뿐이던 움직임 × 채움 4,1 칸을 겨눴어요. 움직임이 1.529 를 넘으면서 채움이 0.302 아래인 칸이에요. 1960년대 IBM 의 RANDU 는 x → 65539x mod 2³¹ 이고, 65539 = 2¹⁶ + 3 이라 x_{k+2} = 6x_{k+1} − 9x_k 가 되어 연속한 세 수가 모두 평면 9x − 6y + z = n 열다섯 장 위에 놓여요. 조지 마살리아(1968)가 「난수는 대개 평면에 떨어진다」에서 밝힌 일이에요. 패널은 정육면체를 그 평면들 안에 놓인 가로축으로 굴려 한 바퀴에 두 번 시선이 평면을 따라 지나게 하고, 그런 축 가운데 정육면체가 가장 좁아 보이는 축을 골라 폭 62 LED 에 넣어서 열다섯 줄이 5.2 LED 간격으로 가로로 서요. 평면 번호는 (9X − 6Y + Z)/2³¹ 을 정수 그대로 셈하는데, 점 8000개에서 −5 부터 9 까지 열다섯이 나왔고 가운데 평면에 905개, 양 끝 평면에 72개와 75개가 앉아서 맨 위와 맨 아래 줄은 점선이에요. 점 1400개로 연 첫 판은 움직임 1.957, 채움 0.184 로 칸 안이었지만 채움 축의 아래 끝 0.117 에 가까웠어요. 점을 1800개로 늘리고 정렬 앞뒤에서 회전을 평균 빠르기의 10분의 1로 늦춰 줄이 0.4초쯤 머물게 하자 움직임 2.159, 채움 0.206, 촘촘함 0.152, 가장자리 0.861, 색 0.265 가 됐어요. 정육면체가 폭에 맞춰 가운데 83줄만 써서 thirds 는 9/42/9 예요. 뜻밖이었던 것은 마살리아 자신의 69069 였어요. 법 2³² 로 격자를 줄이면 가장 짧은 법선이 (−88, −52, 1436) 인데, 이 곱수는 x mod 4 를 바꾸지 않아서 점들은 그 4분의 1 인 (22, 13, −359) 의 평면 394장, 네 배 넓은 간격에 놓여요. 그래도 그 간격이 0.16 LED 라 구름은 줄지어 서지 않고, Generator 노브로 고르는 2¹⁶ + 5 만 2.1 LED 간격의 서른다섯 줄로 겨우 보여요. + +### Euler's Officers + +주제 1 과 5 로 오일러의 36 장교 문제를 보로노이·셀 갈래에 세우고, 패턴이 셋뿐이던 움직임 × 색 1,4 칸을 겨눴어요. 움직임이 0.061 아래이면서 색이 0.5445 를 넘는 칸이에요. 레온하르트 오일러(1782)는 여섯 연대의 여섯 계급 장교 36명을 줄마다 계급과 연대가 한 번씩 오게 세울 수 없다고 보았고 가스통 타리(1900)가 모든 경우를 따져 이를 확인했어요. 4k + 2 차수는 다 안 된다는 오일러의 짐작은 R. C. 보스, S. S. 슈리칸데, E. T. 파커(1959)가 10차를 지어 깼어요. 패널 위에는 9 × 9 LED 칸으로 6차의 가장 나은 시도를, 아래에는 5 × 5 LED 칸으로 10차 그레코-라틴 방진을 두고, 칸마다 바깥 고리를 계급 색, 안쪽 심을 연대 색으로 칠해요. 10차 표는 다섯째로 뽑은 무작위 라틴 방진의 가로지르기 872개에서 정확 덮개 탐색으로 서로 겹치지 않는 열 개를 골라 지었어요. 6차는 무작위 방진 40개를 연대로 나누는 모든 방법을 훑어 15개가 36쌍 가운데 34쌍에 닿았어요. 다섯 연대가 가로지르기이면 남은 여섯째도 가로지르기가 되어 35쌍은 나올 수 없으니, 34 가 끝이에요. 처음 판은 위 방진이 열 색의 앞 여섯만 써서 0.66 밝기의 노랑과 주황이 올리브와 갈색으로 보였고, 움직임 0.043, 색 0.579 였어요. 여섯 색을 바퀴에 고르게 펴고 밝기와 채도를 0.88 로 맞춘 뒤 연대 하나가 1.5초씩 차례로 밝아지며 흰 틀을 두르게 하자 움직임 0.036, 색 0.625 로 칸 안에 섰어요. 겹친 네 칸이 하얗게 바래는 맥박은 가장 밝은 채널을 그대로 두어 움직임에 들지 않아요. 뜻밖이었던 것은 겹침이 서는 모양이었어요. 고른 방진에서 34쌍에 닿는 나눔 넷은 모두 겹친 네 칸을 한 직사각형의 네 귀에 두는데, 가로지르기가 32개인 다른 방진은 나눔 48가지가 34 에 닿으면서도 네 칸을 네 줄 두 칸이나 두 줄 네 칸에 흩어 놓았어요. + +### Penrose Diagram + +주제 4 와 5 로 펜로즈 도형을 공간·원근 갈래에 세웠어요. 로저 펜로즈(1963)는 u = t − r, v = t + r 에 아크탄젠트를 씌워 끝없는 민코프스키 시공간을 유한한 그림에 담았고, 브랜던 카터(1966)가 이 도형을 그려 썼어요. 반지름 쪽 절반은 너비 π, 높이 2π 의 삼각형이라 한 LED 를 π/63 으로 두면 i⁺ 가 (0, 0), i⁰ 가 (63, 63), i⁻ 가 (0, 126) 에 앉고, 삼각형은 꼭 4096 LED 로 패널의 절반이며 빛줄기는 LED 대각선 그대로예요. 시간과 반지름이 일정한 곡선은 LED 마다 t 와 r 을 셈한 등고선으로 한 LED 폭으로 긋고, 곡선 사이가 2 LED 보다 좁아지는 널 무한대 쪽은 어두운 안개로 칠해서 곡선이 모서리로 몰리는 것이 보여요. 첫 판의 관찰자는 펄스를 받기만 해서 Accel 노브의 반응이 0.0093 으로 검사에 걸렸어요. 관찰자에게 거울을 들려 t = 0 전에 보낸 펄스가 t = 1/(a²|t_k|) 에 r = 0 으로 돌아오게 하고, 관찰자의 동시면과 거울 뒤를 그린 다음 a 를 0.52 에서 열자 0.037 이 됐어요. 같은 간격으로 보낸 펄스는 i⁻ 쪽에서 1 LED 안으로 몰려 첫 판의 아래 3분의 1 이 금빛 쐐기로 메워졌는데, 앞 펄스와 4 LED 안인 펄스를 빼자 11개가 따로 보였어요. 마지막 값은 움직임 0.155, 가장자리 0.925, 채움 0.300, thirds 32/38/18 이에요. 관찰자와 지평선과 거울은 삼각형을 1+1 차원 시공간의 x ≥ 0 반쪽으로 읽을 때 정확하고, 3+1 차원의 반지름 그림으로 읽으면 가속하는 관찰자의 지평선은 빛 원뿔 u = 0 이 아니라 평면 t = x 예요. 코드 주석이 어느 것을 어느 쪽으로 읽는지 적어 둬요. 뜻밖이었던 것은 a = 1 의 쌍곡선 r² − t² = 1 이 이 그림에서 R = π/2 인 곧은 세로줄이 된다는 거예요. uv = −1 이면 V − U 가 늘 π/2 라서, 그때 거울은 c 줄에서 떠난 펄스를 꼭 c − 63 줄로 돌려보내는 평면거울이 돼요. + +### Rutherford Shadow + +주제 4 와 5 로 러더퍼드 산란이 남기는 그림자를 물리·스프링 갈래에 세웠어요. 한스 가이거와 어니스트 마스든(1909)의 금박에서 알파 입자 몇 개가 되튀어 나오자, 어니스트 러더퍼드(1911)는 점 전하에 밀린 입자가 핵을 바깥 초점에 둔 쌍곡선을 그리며 충돌 변수 b 의 입자가 tan(θ/2) = d/2b 만큼 꺾인다고 셈했어요. 패널에서는 핵을 아래에서 42줄 높이에 두고, 새 입자를 궤도 방정식으로 들어오는 쌍곡선 위에 정확히 놓은 뒤 걸음을 잘게 자르는 립프로그로 밀어 올려요. 궤도 방정식을 b 의 2차식 b² − Xb + (d/2)(r + Y) = 0 으로 고쳐 쓰면 판별식이 0 인 곳이 초점은 핵이고 꼭짓점은 d 아래인 포물선이에요. 따로 돌린 궤도 2000개는 높이 80 LED 까지 그 포물선에 소수 넷째 자리까지 닿았고, 닿는 궤도는 늘 b = X/2 였어요. 패턴 안에서도 다섯 설정을 20초씩 돌린 위치 57만 2천 개 가운데 포물선 안으로 들어간 것은 0.00003 LED 들어간 하나뿐이었어요. 처음 판은 꼬리 짧은 점만 그려서 움직임 2.278, thirds 17/21/22 였고, 시간 평균으로는 가장자리가 바깥보다 두 배쯤 밝은데도 한 장에서는 그 몰림이 보이지 않았어요. 꼬리를 10 LED 로 늘리고 3초 노출을 제곱해 흐리게 깔자 가장자리가 네 배로 드러났고, 움직임 1.668, 채움 0.295, thirds 28/30/29 가 됐어요. 겨누지 않았는데 이 값은 RANDU Planes 가 겨눈 움직임 × 채움 4,1 칸 안이에요. 뜻밖이었던 것은 Attract 를 끝까지 돌린 끌림 쪽이었어요. 판별식이 X² + 2d(r + Y) 로 바뀌어 0 이 되지 않으니 평면에서는 양쪽에서 온 두 궤도가 핵 뒤에서 엇갈릴 뿐 모이는 선이 없고, 핵 위 d/2 쯤부터는 두 흐름이 빔의 선속을 꼭 나눠 가져서 밝기가 그대로예요. + +### Cornu Spiral + +주제 5 로 코르뉴 나선을 선·궤적 갈래에 세웠고, 겨눈 지도 칸은 없어요. 레온하르트 오일러(1744)는 곡률이 호 길이에 비례해 자라는 곡선을 다뤘고, 오귀스탱 장 프레넬(1818)의 적분 C(t), S(t) 가 그 좌표예요. 마리 알프레드 코르뉴(1874)는 이 나선 위 두 점을 잇는 현의 길이 제곱으로 직선 모서리와 슬릿 뒤의 회절 세기를 읽었어요. 패널은 위 60줄에 한 단위 39.5 LED 로 나선을 그리고, 그 아래 42줄 높이에 세기 곡선을, 맨 아래 12줄에 그 세기만큼 밝힌 화면을 둬요. 프레넬 적분은 setup 에서 1/200 간격으로, 칸마다 심프슨 여덟 토막을 써서 표로 지어요. C(1) = 0.7798934004 와 S(1) = 0.4382591474 가 알려진 값과 열 자리까지 같았고, 직선 모서리 뒤 첫 마루는 w = 1.217 에서 1.3704, 첫 골은 w = 1.873 에서 0.7783 이었어요. 처음 판은 화면을 한 번 가로지르는 데 10초가 걸려서 Speed 반응이 0.0096 으로 걸렸어요. 가로지르는 시간을 5초로 줄이고, 빛이 지나는 호를 밝기 0.72 에서 0.8 로 올리고 나머지 나선을 0.42 에서 0.32 로 낮추자 0.029 가 됐어요. 마지막 값은 움직임 0.171, 촘촘함 0.051, 가장자리 0.554, 복잡도 0.470, 채움 0.187, 색 0.184, 모양 바뀜 0.026 이에요. Slit 은 0 에서 직선 모서리이고 0 을 조금만 넘어도 너비 7.3 의 슬릿으로 건너뛰어요. 슬릿 쪽 세기 곡선은 가장 높은 곳을 42줄에 맞춰 그려서, 좁은 슬릿의 0.18 혹도 직선 모서리의 1.37 마루만큼 크게 서요. 뜻밖이었던 것은 Spread 를 끝까지 돌린 흰빛이었어요. 파장마다 현이 나선의 다른 점에 닿아 위쪽 눈 둘레에서 현이 부채처럼 벌어지고, 화면의 첫 무늬는 그늘 쪽이 파랗고 바깥쪽이 주황인 흰 띠로 서다가 w = 4.4 너머에서 다시 희게 녹았어요. + +### Richardson Dividers + +주제 5 로 리처드슨의 디바이더 측정을 프랙탈 갈래에 세웠고, 겨눈 지도 칸은 없어요. 루이스 프라이 리처드슨(1961)은 해안선과 국경을 벌림 s 의 디바이더로 걸어 잰 길이 L 이 s^(1−D) 로 자란다는 것을 찾았고, 브누아 망델브로(1967)는 그 D 를 프랙탈 차원으로 읽었어요. 곡선은 코흐 꼴 생성기를 조각마다 새로 뽑아요. 봉우리가 놓이는 쪽은 동전으로 정하고, 비율은 0.85~1.15 배로, 꼭짓점 자리는 ±0.07 로 흔들어요. 네 조각의 비율 r 이 Σ r^D = 1 을 맞추도록 봉우리 높이를 뉴턴 네 걸음으로 맞춰서 모든 생성기의 닮음 차원이 꼭 D 이고, R. 대니얼 몰딘과 S. C. 윌리엄스(1986)를 따르면 곡선의 차원도 D 예요. 처음 판은 곡선을 세로로 세웠더니 가운데에 큰 봉우리 하나가 늘 서서 패널 폭의 3분의 1 만 썼고, 채움이 0.064 로 축 끝 0.1168 밖이었어요. 곡선을 모서리에서 모서리로 눕히고, 굵은 벌림의 다각형을 흐리게 남기고, 벌림마다 옅은 점선 기둥을 세우자 채움이 0.128 이 됐어요. 씨앗 60개씩 잰 열 점의 기울기는 D = 1.02, 1.145, 1.27, 1.395, 1.52 에서 평균 −0.019, −0.137, −0.261, −0.371, −0.496 으로 1 − D 보다 조금 얕았어요. 벌림이 23 LED 에서 0.93 LED 까지 열 배 단위로 1.39 칸만 줄고, 가장 넓은 벌림에서도 L 이 양 끝 사이 거리 아래로 내려가지 못해서예요. 마지막 값은 움직임 0.062, 촘촘함 0.044, 가장자리 0.738, 복잡도 0.402, 채움 0.128, 색 0.097, 모양 바뀜 0.021 이에요. 뜻밖이었던 것은 비율 하나로만 지은 곡선의 계단이었어요. 생성기 비율 0.336 과 벌림 비율 0.7 의 로그 비가 3.06 이라 L(s) 가 벌림 세 번마다 한 번씩 뛰었고, 비율을 흔들자 그 계단이 직선으로 펴졌어요. + +### Phase Spiral + +주제 5 로 1차원 자기중력 면 모형의 위상 공간 나선을 시뮬레이션 갈래에 세웠고, 겨눈 지도 칸은 없어요. 미셸 에농(1964)과 프랭크 홀과 마르크 페익스(1967)의 면 모형에서는 면마다 오른쪽 면 수에서 왼쪽 면 수를 뺀 만큼 일정하게 끌려서 두 면이 엇갈릴 때만 힘이 바뀌어요. 도널드 린든-벨(1967)은 차가운 덩어리가 무너지며 이렇게 에너지가 뒤섞이는 것을 격렬한 이완이라고 불렀어요. 패널은 면 3072개를 걸음마다 계수 정렬로 다시 세워 순위에서 힘을 얻고, 위치를 가로 64 LED 에, 속도를 세로 128 LED 에 놓고, 면마다 처음 띠 위의 자리로 하늘색에서 장밋빛까지 칠해요. 첫 판의 넷째 노브는 띠를 가운데에서 갈라 벌리는 Split 이었는데, 차가운 고른 띠는 한 점으로 한꺼번에 무너져서 처음 자리 0.06 의 틈만으로도 두 덩어리로 갈라졌어요. 그래서 띠에 네 덩어리의 밀도 물결을 얹는 Lumps 로 바꿨고, 프레임마다 점으로 찍혀 점선이던 바깥 팔은 지난 프레임과의 중간에 한 번 더 찍어 선으로 이었어요. 면 4096개에 한 장 1.8 ms 이던 것은 면 3072개, 걸음 0.04, 밝기 표로 1.1 ms 가 됐고, 단위 시간 16 을 미리 돌려 2초에는 v = 0 줄을 팔이 여섯 번 지나요. 한 주기 동안 전체 에너지는 1만분의 1 만 움직였고, 운동 에너지 두 배를 퍼텐셜로 나눈 값은 0.01 에서 첫 붕괴 때 10 까지 튀었다가 단위 시간 16 뒤로는 1 의 25% 안에 머물렀어요. 잰 값은 움직임 2.033, 촘촘함 0.146, 가장자리 0.708, 복잡도 1.049, 채움 0.304, 색 0.311, 모양 바뀜 0.131 이에요. 뜻밖이었던 것은 이완이 면들의 에너지 순서를 거의 다 섞어 버린 것이었어요. 단위 시간 60 에서 띠 가운데 10분의 1 에서 온 면은 에너지 순위가 평균 45번째 백분위였고, 가장 깊이 가라앉은 것은 반너비의 10분의 2 에서 4 사이에서 온 면(37번째)이었으며, 끝 10분의 1 에서 온 면도 71번째에 그쳤어요. + +### Giant Component + +주제 5 로 에르되시-레니 무작위 그래프의 거대 성분을 데이터 그림 갈래에 세웠고, 겨눈 지도 칸은 없어요. 폴 에르되시와 알프레드 레니(1959, 1960)는 꼭짓점 n 개에 무작위 변을 하나씩 더할 때 변이 n/2 보다 적으면 모든 성분이 log n 크기에 머물고, n/2 를 넘자마자 n 에 비례하는 거대 성분이 생긴다는 것을 보였어요. 패널은 점 21 × 32 = 672 개를 3 LED 간격으로 깔고 유니온-파인드로 성분을 이어서 성분마다 제 색을, 가장 큰 성분에는 크림색을 입히고, 아래 25줄에는 가장 큰 성분의 몫을 이론 곡선 θ = 1 − e^(−2tθ) 위에 그려요. Choice 노브는 디미트리스 아클리옵타스, 라이사 드수자, 조엘 스펜서(2009)의 곱 규칙을 쓸 확률로, 후보 변 둘 가운데 잇는 두 성분 크기의 곱이 작은 쪽을 남겨요. 첫 판은 최근 변 8개를 0.3초씩 그려 긴 선이 엉켰고, 문턱이 3.9초에 와서 시트의 2.5초 칸이 노브마다 같은 색종이였어요. 선을 3개, 0.25초로 줄이고 밝기를 이은 쪽 작은 성분의 크기에 매겼으며, 첫 그래프를 변 0.2n 에서 열어 문턱을 2.3초로 당기자 노브 반응이 0.08, 0.10, 0.14, 0.07 에서 0.09, 0.12, 0.22, 0.09 가 됐어요. 그래프 200개씩의 평균으로 변 0.75n 에서 가장 큰 성분의 몫은 384, 672, 1536 개에서 57.5, 57.8, 58.2% 로 이론의 58.3% 에 붙었고, n/2 에서는 11.9, 10.4, 8.0% 로 n^(−1/3) 을 따라 줄었어요. 잰 값은 움직임 0.203, 촘촘함 0.266, 가장자리 0.991, 복잡도 0.160, 채움 0.383, 색 0.154, 모양 바뀜 0.144 예요. 그래프에는 자리가 없어서 거대 성분은 한 덩어리로 번지지 않고, 크림색 점이 판 곳곳에서 늘어나는 것으로 보여요. 뜻밖이었던 것은 곱 규칙이 터지기 직전의 패널이 가장 알록달록하다는 것이었어요. 변 0.8n 에서 꼭짓점 열 개 이상인 성분이 20개이고 혼자인 꼭짓점은 10% 뿐인데, 보통 그래프는 n/2 에서 그런 성분이 7개, 혼자인 꼭짓점이 37% 라서, 곱 규칙은 큰 성분을 막는 동안 중간 크기를 쌓아 두었다가 한꺼번에 이어요. + +### Hough Transform + +주제 5 로 허프 변환을 알고리즘 갈래에 세웠고, 겨눈 지도 칸은 없어요. 폴 허프(1962)는 거품 상자 사진에서 곧은 궤적을 찾는 특허를 냈고, 리처드 두다와 피터 하트(1972)가 점 하나를 ρ = x cos θ + y sin θ 의 사인 곡선 하나로 옮기는 꼴로 바꿔, 한 직선 위 점들의 곡선이 모두 그 직선의 (θ, ρ) 한 점에서 만나게 했어요. 패널은 위 64 × 64 를 그림 공간으로, 아래 64 × 64 를 θ 가 가로이고 ρ 가 세로인 누산기로 써요. 점이 하나 내려앉을 때마다 그 사인 곡선이 0.11초 동안 왼쪽부터 쓸려 들어가고, 그사이 위에서는 그 θ 의 직선이 점을 축으로 돌아요. 누산기는 매 프레임 남은 점들을 무게 e^(−age/6) 로 다시 쌓아서 늘 지금 점들의 정확한 변환이고, 고른 봉우리의 직선과 그 직선 1 LED 안의 점들이 그린 사인 곡선을 같은 색으로 칠해요. 처음엔 ρ 를 반대각선 45 LED 까지 두었더니 곡선이 누산기 가운데 절반에 몰려서, ±32 로 줄여 한 줄을 ρ 1 LED 로 맞췄어요. 점이 1초에 6개이고 무게가 4초에 e 로 줄던 판은 전체 무게가 23 이라, 씨앗마다 2분씩 돌려 보니 선 2개는 81~94%, 4개는 43~52%, 6개는 19~31% 만 찾았어요. 점을 1초에 8개로, 무게가 e 로 주는 시간을 6초로 늘리고, 선이 도는 빠르기를 초당 0.7~1.6도에서 0.4~1도로 줄이자 무게가 46 이 되어 2개 99%, 4개 81%, 6개 64% 가 됐어요. 잰 값은 움직임 0.313, 촘촘함 0.070, 가장자리 0.394, 복잡도 1.100, 채움 0.409, 색 0.146, 모양 바뀜 0.050, thirds 11/35/76 이에요. Lines 노브는 선의 수와 고를 봉우리의 수를 함께 정해서, 선이 몇 개인지는 변환이 찾지 않아요. 뜻밖이었던 것은 봉우리가 지금의 직선보다 평균 2.2도 뒤에 선다는 거예요. 봉우리는 지난 몇 초 동안 내려앉은 점들의 무게 평균이라서, 도는 선을 쫓는 꼬리를 누산기에 남기며 따라가요. + +### Jordan Curve + +주제 5 로 조르당 곡선 정리를 위상·매듭 갈래에 세웠고, 겨눈 지도 칸은 없어요. 카미유 조르당(1887)은 평면의 단순 닫힌 곡선이 평면을 안과 밖 꼭 둘로 나눈다고 했고, 오즈월드 베블런(1905)의 증명이 처음으로 온전하다고 받아들여졌어요. 곡선이 아주 구불구불하면 눈으로는 어느 쪽이 안인지 모르지만, 그 점에서 가장자리까지 그은 반직선이 곡선을 홀수 번 건너면 안이에요. M. 심랏(1962)이 다각형 안의 점을 가리는 알고리즘으로 낸 셈이에요. 패널은 간격 5.5 LED 로 흔든 삼각 격자의 칸마다 값을 두고, 삼각형마다 선형으로 이은 함수의 0 집합을 곡선으로 그려서 곡선이 제 몸에 닿을 수가 없어요. 칸은 이웃 여섯이 안과 밖 한 줄씩으로 나뉠 때만 곡선 길이에 무게를 둔 메트로폴리스 걸음으로 편을 바꿔서, 720 번 센 칸의 연결 성분은 매번 안 하나, 밖 하나였어요. 처음 판은 탐침의 정확한 자리에서 반직선을 쏘고 반올림한 LED 에서 칠을 시작해서, 240 프레임 가운데 12 프레임에서 둘이 어긋났고 그때마다 밖의 큰 덩어리가 안의 색으로 칠해졌어요. 반직선을 LED 한가운데에서 쏘자 3600 프레임 모두 홀짝과 칠이 맞았고, 반직선을 패널의 먼 끝으로 겨누자 교차가 평균 3.7 번에서 8.0 번으로 늘었어요. 잰 값은 움직임 0.361, 촘촘함 0.064, 가장자리 0.530, 복잡도 1.077, 채움 1.000, 색 0.359, 모양 바뀜 0.079 이고, 곡선 길이는 처음 노브에서 1220 LED 쯤이에요. 채움은 축의 위 끝 1 과 같아요. 뜻밖이었던 것은 칸 하나가 편을 바꾸는 0.7초 사이에 LED 로 센 안이 두 조각이 되기도 해서, 720 번 가운데 9 번은 1 LED 보다 가는 목 너머로 칠이 이웃에서 번져 와야 했다는 거예요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -3072,7 +3131,7 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 ## 다음 판 감 -10월 25일 `dev`(`f72355d`)의 328개에 43판부터 58판까지의 백예순 개를 더한 488개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. +10월 25일 `dev`(`f72355d`)의 328개에 43판부터 58판까지와 60판의 백일흔 개를 더한 498개에서 잰 값이에요. 패턴 바퀴는 주제 안에서 무엇을 할지 고를 때 이 절을 읽고, 한 것은 빼고 새로 본 것을 더해요. ### 지도의 빈 칸 @@ -3146,6 +3205,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 58판은 57판까지 든 478개에 둘뿐인 칸으로 남은 가장자리 × 촘촘함 1,4 와 2,4 를 겨눴어요. 두 칸 다 촘촘함이 0.297 을 넘어야 하고, 가장자리는 1,4 가 0.210 아래, 2,4 가 0.210~0.500 이에요. 두 패턴 다 한 LED 체커 위에 지었어요. x + y 가 홀수인 LED 는 늘 끄고 짝수인 빈 LED 는 밝기 40 에 두면, 빈 LED 끼리의 이웃 쌍은 다 꼭 40 차이라 무른 쌍으로 세이고 촘촘함에는 0.157 을 보태요. 그 위에 켠 LED 가 이웃과 맞닿지 않으면, 켠 LED 의 수와 밝기만으로 두 값이 셈으로 정해져요. Mirror Curves 는 45도로만 달리는 곡선이라 짝수 LED 만 밟고, 255 로 켠 LED 마다 네 이웃이 꺼진 0 이에요. 켠 LED 가 n 개면 가장자리가 4n/16192 쯤이라 n 이 830 을 넘으면 칸을 벗어나서, 거울 32개 판에서 n 을 늘 800 으로 두자 씨앗 셋 모두 가장자리 0.195, 촘촘함 0.318 이었어요. Hard Squares 는 입자끼리 이웃하지 못하는 규칙이 같은 셈을 줘요. 가장자리는 입자에 닿는 쌍의 몫인 0.400 이고, 입자를 이웃보다 110 밝게 두어 촘촘함 0.326 이에요. 첫 판은 평균 밀도 0.237 에 촘촘함 0.397 이라 축의 위 끝 0.375 를 넘을 판이었고, 얼어붙는 줄을 아래로 내려 입자를 줄였어요. 두 패턴은 어느 축의 끝도 넘지 않아서 칸 경계는 56판 그대로예요. 58판까지 든 488개에서 지도 21장은 다 `none` 이고, 패턴이 둘뿐인 칸도 없어요. 두 칸에는 셋째가 섰고, 57판의 Medial Axis 와 Solenoid 가 선 가장자리 × 촘촘함 1,3 과 촘촘함 × 모양 바뀜 4,4 도 셋이에요. 이제 칸마다 패턴이 셋 이상 서서, 다음 주제 1 은 셋뿐인 칸 스물여섯 가운데서 고르게 돼요. 한 LED 체커를 까는 셈은 가장자리의 문턱이 `step > 40` 이라 꼭 40 차이가 무른 쌍으로 세이는 데 기대요. +60판은 58판까지 든 488개에 빈 칸도 둘뿐인 칸도 없어서, 셋뿐인 칸 스물여섯 가운데 같이 돈 59판이 고른 움직임 × 촘촘함 1,4 와 가장자리 × 채움 1,1 을 빼고 움직임 × 채움 4,1 과 움직임 × 색 1,4 를 겨눴어요. 4,1 은 움직임이 1.529 를 넘으면서 채움이 0.302 아래인 칸이에요. 움직임은 LED 8192개의 평균이라 드문 점이 거의 다 프레임마다 자리를 옮겨야 해요. RANDU Planes 는 점 1800개가 구르는 정육면체로 움직임 2.159, 채움 0.206 이고, 겨누지 않은 Rutherford Shadow 도 꼬리 단 입자의 흐름으로 움직임 1.668, 채움 0.295 라 같은 칸에 섰어요. 1,4 는 움직임이 0.061 아래이면서 색이 0.5445 를 넘는 칸이에요. 가장 밝은 채널로 재는 움직임은 채도 높은 색끼리 바꿔 칠해도 늘지 않아서, Euler's Officers 는 판의 71% 를 채도 0.88 의 색으로 덮고 연대 하나씩만 밝히며 움직임 0.036, 색 0.625 예요. 채도를 1 로 올리면 색이 축의 위 끝 0.675 를 넘을 판이라 0.88 에 두었어요. 열 개 다 어느 축의 끝도 넘지 않아서 칸 경계는 56판 그대로예요. 60판까지 든 498개에서 지도 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 가 섰어요. @@ -3272,9 +3333,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 58판에서는 Hanging Chain 과 Rayleigh-Taylor, Hard Squares 가 128줄을 써요. Hanging Chain 은 윗줄 가운데에 매단 사슬이 바닥 가까이까지 내려오고, 흔들림이 가장 작은 매단 점 둘레에서 사슬이 한 LED 위에 오래 머물러 긴 노출이 가장 밝게 타서 `thirds` 가 14/33/51 이에요. Rayleigh-Taylor 는 두 유체가 판을 다 채워 100/100/100 이고, 무거운 손가락이 6초 안팎에 바닥에 닿아요. Hard Squares 는 52/60/63 이지만 그 대부분은 밝기 40 의 흐린 바둑판이에요. `thirds` 는 24 를 넘는 LED 를 다 켜진 것으로 세서, 체커 바탕을 까는 패턴은 그림과 상관없이 세 칸이 50 을 넘어요. 그래서 입자만 따로 세어 위 3분의 1 이 5 를 넘도록 얼어붙는 줄을 104번에 두었고, 입자만으로는 5.9/18.9/34.2 예요. 같은 바탕을 깐 Mirror Curves 는 50/50/50 이라 주제 4 로 세지 않았어요. Print Gallery(89/97/89)와 Beckett-Gray(47/42/33), Hexagrammum Mysticum(28/35/22), Loop Soup(21/24/16), KP Web(15/21/15)도 세 칸이 다 5 를 넘지만 주제 4 로 세지 않았어요. Tukey Depth 는 점 구름이 가운데에 모여 7/50/37 이에요. +60판에서는 Penrose Diagram 과 Rutherford Shadow 가 128줄을 써요. Penrose Diagram 은 민코프스키 시공간의 반지름 쪽 절반이 너비 π, 높이 2π 의 삼각형이라 패널 꼴 그대로이고, i⁺ 와 i⁻ 로 좁아지는 위아래 3분의 1 도 32% 와 18% 가 켜져요. 삼각형이 127줄이라 맨 아래 한 줄은 늘 꺼져 있어요. Rutherford Shadow 는 빔이 바닥에서 올라와 핵 위로 그림자를 남기며 꼭대기까지 가서 thirds 가 28/30/29 예요. 나머지 여덟도 세 3분의 1 이 다 9% 이상 켜져요. + ### 낯선 수학 -주제 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)이에요. +주제 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)이에요. 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)예요. 목록 밖에서 다음에 해 볼 것이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -3340,6 +3403,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 58판에서 본 것이에요. 정리가 못 박는 수는 이번에도 패턴의 셈으로 지켜봤어요. Hexagrammum Mysticum 은 7,200 프레임 동안 육각형 60개의 세 교점이 1.7e-15 안에서 한 줄에 있었고, 세 선 묶음 34,220개 가운데 한 점을 지나는 것은 늘 260개였어요. Tukey Depth 는 LED 221만 개의 깊이를 두 길로 재서 하나도 어긋나지 않았고, 가장 깊은 층이 한 번도 n/3 아래로 내려가지 않았어요. Mirror Curves 는 72가지 판에서 테두리만 있는 판의 가닥 수가 늘 gcd(a, b) 였고, Beckett-Gray 의 6비트 코드는 퇴장 32번이 다 차례를 지켰어요. Print Gallery 는 한 주기 떨어진 두 장의 채널 24,576개가 하나도 다르지 않았고, Hard Squares 는 너비 14 띠의 전달 행렬로 셈한 임계 밀도 0.3677 을 104번 줄 바로 아래에서 넘었어요. 이산 셈이 연속 이론과 어긋난 자리도 셈이 짚었어요. Hanging Chain 의 구슬 사슬은 자유단을 마지막 구슬보다 반 링크 아래에 두어야 베셀 영점 비와 0.22% 안에서 맞았고, Rayleigh-Taylor 의 성장률은 √(Agk) 보다 8~15% 느렸는데 2 LED 두께 경계의 이산 선형 문제와는 0.5% 안이었어요. KP Web 의 O 형과 P 형은 위상 이동이 1~2 LED 라 보이지 않아서, 교차점의 높이로 둘이 갈렸어요. Loop Soup 은 c = 1 에서도 무리가 위아래를 잇지 않았어요. 이 창에서는 c = 4 가 돼야 프레임의 86% 에서 가장 큰 무리가 패널을 건너요. +60판에서 본 것이에요. 정리가 못 박는 수는 이번에도 패턴의 셈으로 지켜봤어요. RANDU Planes 는 점 8000개의 평면 번호 (9X − 6Y + Z)/2³¹ 이 다 정수였고 −5 부터 9 까지 열다섯이었어요. Euler's Officers 는 10차 방진의 100쌍이 다 한 번씩이었고, 6차는 무작위 방진 40개를 훑어도 34쌍이 끝이었어요. Rutherford Shadow 의 위치 57만 2천 개 가운데 포물선 안으로 들어간 것은 0.00003 LED 하나였고, Penrose Diagram 의 a = 1 쌍곡선은 R = π/2 의 곧은 세로줄이었어요. Giant Component 는 변 0.75n 에서 가장 큰 성분이 이론의 58.3% 에 0.1~0.8 포인트 안으로 붙었고, Cornu Spiral 의 프레넬 적분은 열 자리까지 맞았어요. 셈이 이론과 어긋난 자리도 있었어요. Richardson Dividers 의 기울기는 1 − D 보다 0.01~0.025 얕았는데, 벌림이 열 배 단위로 1.39 칸만 줄고 넓은 쪽 끝이 양 끝 거리에 막혀서예요. Hough Transform 의 봉우리는 도는 직선보다 평균 2.2도 뒤에 섰고, Jordan Curve 는 LED 로 센 안이 칸 하나가 넘어가는 0.7초 동안 두 조각이 되기도 했어요. Rutherford Shadow 의 끌림 쪽에는 처음에 기대한 모임선이 없었어요. 평면 빔에서는 판별식이 0 이 되지 않아서, 궤도는 핵 뒤에서 엇갈리기만 해요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. @@ -3392,6 +3457,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 목록 밖에서 58판 뒤에 해 볼 것이에요. 새뮤얼 로모나코와 루이스 카우프먼(2008)의 매듭 모자이크는 열한 가지 타일로 n × n 판에 매듭을 깔아서, Mirror Curves 의 판에서 타일을 하나씩 바꿔 끼우며 매듭 종류가 바뀌는 것을 보일 수 있어요. 로드니 백스터(1980)가 정확히 푼 딱딱한 육각형 기체는 임계 활동도가 (11 + 5√5)/2 이고, 질서 상에서 부분격자 셋이 갈려 영역이 세 색으로 읽혀요. 앨프리드 조지 그린힐(1881)의 제 무게로 휘는 기둥은 휘는 높이가 J₋₁/₃ 의 첫 영점으로 정해져서, 키를 노브에 걸면 곧은 기둥이 그 높이를 넘는 순간 휘어 누워요. 로버트 리히트마이어(1960)와 예브게니 메시코프(1969)의 불안정은 충격파가 지나간 경계의 잔물결이 지수가 아니라 시간에 비례해 자라서, Rayleigh-Taylor 의 풀이에 충격 하나를 더하면 돼요. 레지나 리우(1990)의 단체 깊이는 LED 를 품는 데이터 삼각형의 몫으로 칠해서, 터키 깊이와 달리 등고선이 볼록하지 않게 휘는 곳이 생길 수 있어요. 이브 르 장(2010)에 따르면 c = 1 고리 수프가 자리마다 머문 시간은 이산 가우스 자유장 제곱의 반과 분포가 같아서, 고리 대신 머문 시간으로 칠하면 자유장의 제곱이 떠요. 알렉산더 포스트니코프(2006)의 플래빅 그래프는 양의 그라스만 칸을 그림으로 적어서, KP Web 의 웹 옆에 그 칸의 그래프를 나란히 세울 수 있어요. 7비트에서 1 이 셋이나 넷인 상태 70개만 한 비트씩 바꾸며 다 지나는 고리가 있다는 중간 층 정리는 이반 하벨(1983)이 묻고 토르스텐 뮈체(2016)가 증명했어요. 샤를 쥘리앵 브리앙숑(1806)의 정리는 파스칼 정리의 쌍대라, 원뿔곡선에 외접한 육각형의 대각선 셋이 늘 한 점을 지나는 그림이 돼요. 엘빈 크리스토펠(1867)과 헤르만 슈바르츠(1869)의 사상으로 위 반평면의 격자를 다각형 안으로 펴면, 꼭짓점마다 직각이 접히며 모여요. 조지 마살리아(1968)는 선형 합동 생성기의 연속한 세 수가 몇 장의 평면 위에만 놓인다는 것을 보였어요. RANDU 의 점을 정육면체에 찍어 돌리면 한 방향에서 15장의 평면이 한꺼번에 줄을 서요. +목록 밖에서 60판 뒤에 해 볼 것이에요. 10차 직교 라틴 방진은 둘까지 알려졌고 셋이 서로 직교하는 묶음은 아직 아무도 찾지 못했어요. Euler's Officers 의 정확 덮개 탐색을 셋째 방진 쪽으로 돌리면, 두 방진의 가로지르기가 몇 개까지 겹치는지가 그림이 될 만해요. 마틴 크루스칼(1960)과 죄르지 세케레시(1960)의 좌표로 슈바르츠실트 블랙홀을 끝까지 펴면, 펜로즈 도형이 지평선 둘이 엇갈려 나눈 네 구역과 위아래의 특이점 선으로 바뀌어요. Penrose Diagram 의 아크탄젠트 셈을 그대로 쓸 수 있어요. 요한 라돈(1917)의 변환은 허프 변환의 연속판이라, Hough Transform 의 누산기를 사인 곡선 대신 투영의 합으로 채우고 여과 역투영으로 위 정사각형을 되살리는 CT 그림이 될 만해요. 아르투어 쇤플리스(1906)는 조르당 곡선의 안이 원판과 같은 꼴이라는 것을 보였어요. Jordan Curve 의 안을 원판으로 펴는 등각 사상은 곡선이 구불거릴수록 가지 끝마다 원 둘레의 아주 좁은 호로 몰려요. 에르되시-레니 그래프에서 둘째로 큰 성분은 문턱 n/2 에서 가장 커졌다가 거대 성분에 먹혀 줄어들어서, Giant Component 의 그래프에 둘째 곡선을 겹치면 문턱이 봉우리로 보여요. 로버트 코베유와 로버트 맥퍼슨(1967)의 스펙트럼 검사로 선형 합동 생성기의 평면 간격을 2차원부터 6차원까지 셈하면, RANDU 가 3차원에서 갑자기 무너지는 것이 막대 하나로 보여요. + ### 화장품 팝업 주제 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 93118ec6ccc2494c71f016d5e87cbae56be8e578 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 12:48:00 +0000 Subject: [PATCH 8/9] =?UTF-8?q?=EC=97=B0=EA=B5=AC=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20498=EA=B0=9C=EB=A1=9C=20=EB=8B=A4=EC=8B=9C?= =?UTF-8?q?=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_015ttLM9zxviQho5eHL89da2 --- 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 8678ea4e9..df62404ff 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

패턴 연구

- - + + - + +